From 460a27d47f1d55feed64418e7b9ab5be328f921b Mon Sep 17 00:00:00 2001 From: LFRon Date: Tue, 1 Sep 2026 21:54:00 +0800 Subject: [PATCH 01/15] 3rdparty/wlroots: expose Vulkan interop helpers Expose the Vulkan texture and render-buffer primitives Waylib needs to share wlroots-owned images with the Qt Quick QRhi bridge. - Add an image_usage out-parameter to vulkan_import_dmabuf() so callers can learn the actual usage flags a render image was created with. - Store it on struct wlr_vk_render_buffer and return it through waylib_vk_renderer_get_render_buffer_attribs() via a new usage field on struct wlr_vk_image_attribs. - Add waylib_vk_renderer_get_queue() to expose the VkQueue so Qt and wlroots can be verified to share the same queue. No GLES2 behavior is affected; these are additive waylib bridge helpers. --- 3rdparty/wlroots/include/render/vulkan.h | 3 ++- 3rdparty/wlroots/include/wlr/render/vulkan.h | 4 ++++ 3rdparty/wlroots/render/vulkan/renderer.c | 9 ++++++++- 3rdparty/wlroots/render/vulkan/texture.c | 11 +++++++++-- 4 files changed, 23 insertions(+), 4 deletions(-) diff --git a/3rdparty/wlroots/include/render/vulkan.h b/3rdparty/wlroots/include/render/vulkan.h index f96619f2c6..6e36142e60 100644 --- a/3rdparty/wlroots/include/render/vulkan.h +++ b/3rdparty/wlroots/include/render/vulkan.h @@ -234,6 +234,7 @@ struct wlr_vk_render_buffer { VkDeviceMemory memories[WLR_DMABUF_MAX_PLANES]; uint32_t mem_count; VkImage image; + VkImageUsageFlags image_usage; // Framebuffer and image view for rendering directly onto the buffer image, // without any color transform. @@ -515,7 +516,7 @@ struct wlr_vk_texture *vulkan_get_texture(struct wlr_texture *wlr_texture); VkImage vulkan_import_dmabuf(struct wlr_vk_renderer *renderer, const struct wlr_dmabuf_attributes *attribs, VkDeviceMemory mems[static WLR_DMABUF_MAX_PLANES], uint32_t *n_mems, - bool for_render, bool *using_mutable_srgb); + bool for_render, bool *using_mutable_srgb, VkImageUsageFlags *image_usage); struct wlr_texture *vulkan_texture_from_buffer( struct wlr_renderer *wlr_renderer, struct wlr_buffer *buffer); void vulkan_texture_destroy(struct wlr_vk_texture *texture); diff --git a/3rdparty/wlroots/include/wlr/render/vulkan.h b/3rdparty/wlroots/include/wlr/render/vulkan.h index 5ee84e05fb..cb699b5073 100644 --- a/3rdparty/wlroots/include/wlr/render/vulkan.h +++ b/3rdparty/wlroots/include/wlr/render/vulkan.h @@ -22,6 +22,7 @@ struct wlr_vk_image_attribs { VkImage image; VkImageLayout layout; VkFormat format; + VkImageUsageFlags usage; }; struct wlr_renderer *wlr_vk_renderer_create_with_drm_fd(int drm_fd); @@ -49,4 +50,7 @@ bool waylib_vk_renderer_record_render_buffer_release(struct wlr_renderer *render struct wlr_buffer *buffer, VkCommandBuffer cb, VkImageLayout old_layout); bool waylib_vk_renderer_flush_stage(struct wlr_renderer *renderer); +/* waylib extensions (not upstream): Qt Quick RHI texture-sampling helpers. */ +VkQueue waylib_vk_renderer_get_queue(struct wlr_renderer *renderer); + #endif diff --git a/3rdparty/wlroots/render/vulkan/renderer.c b/3rdparty/wlroots/render/vulkan/renderer.c index 90d4b8ab56..cf05e2bb65 100644 --- a/3rdparty/wlroots/render/vulkan/renderer.c +++ b/3rdparty/wlroots/render/vulkan/renderer.c @@ -930,7 +930,8 @@ static struct wlr_vk_render_buffer *create_render_buffer( bool using_mutable_srgb = false; buffer->image = vulkan_import_dmabuf(renderer, &dmabuf, - buffer->memories, &buffer->mem_count, true, &using_mutable_srgb); + buffer->memories, &buffer->mem_count, true, &using_mutable_srgb, + &buffer->image_usage); if (!buffer->image) { goto error; } @@ -1019,6 +1020,7 @@ bool waylib_vk_renderer_get_render_buffer_attribs(struct wlr_renderer *wlr_rende attribs->image = render_buffer->image; attribs->layout = VK_IMAGE_LAYOUT_GENERAL; attribs->format = format->vk; + attribs->usage = render_buffer->image_usage; return true; } @@ -2788,3 +2790,8 @@ uint32_t wlr_vk_renderer_get_queue_family(struct wlr_renderer *renderer) { struct wlr_vk_renderer *vk_renderer = vulkan_get_renderer(renderer); return vk_renderer->dev->queue_family; } + +VkQueue waylib_vk_renderer_get_queue(struct wlr_renderer *renderer) { + struct wlr_vk_renderer *vk_renderer = vulkan_get_renderer(renderer); + return vk_renderer->dev->queue; +} diff --git a/3rdparty/wlroots/render/vulkan/texture.c b/3rdparty/wlroots/render/vulkan/texture.c index 57db97a567..fa8b2645c3 100644 --- a/3rdparty/wlroots/render/vulkan/texture.c +++ b/3rdparty/wlroots/render/vulkan/texture.c @@ -504,10 +504,14 @@ static bool is_dmabuf_disjoint(const struct wlr_dmabuf_attributes *attribs) { VkImage vulkan_import_dmabuf(struct wlr_vk_renderer *renderer, const struct wlr_dmabuf_attributes *attribs, VkDeviceMemory mems[static WLR_DMABUF_MAX_PLANES], uint32_t *n_mems, - bool for_render, bool *using_mutable_srgb) { + bool for_render, bool *using_mutable_srgb, + VkImageUsageFlags *image_usage) { VkResult res; VkDevice dev = renderer->dev->dev; *n_mems = 0u; + if (image_usage != NULL) { + *image_usage = 0; + } struct wlr_vk_format_props *fmt = vulkan_format_props_from_drm(renderer->dev, attribs->format); @@ -619,6 +623,9 @@ VkImage vulkan_import_dmabuf(struct wlr_vk_renderer *renderer, wlr_vk_error("vkCreateImage", res); return VK_NULL_HANDLE; } + if (image_usage != NULL) { + *image_usage = usage; + } unsigned mem_count = disjoint ? plane_count : 1u; VkBindImageMemoryInfo bindi[WLR_DMABUF_MAX_PLANES] = {0}; @@ -752,7 +759,7 @@ static struct wlr_vk_texture *vulkan_texture_from_dmabuf( bool using_mutable_srgb = false; texture->image = vulkan_import_dmabuf(renderer, attribs, - texture->memories, &texture->mem_count, false, &using_mutable_srgb); + texture->memories, &texture->mem_count, false, &using_mutable_srgb, NULL); if (!texture->image) { goto error; } From a50d149f4cd7356cc98073153dd99224d8738fae Mon Sep 17 00:00:00 2001 From: LFRon Date: Tue, 1 Sep 2026 22:02:11 +0800 Subject: [PATCH 02/15] 3rdparty/wlroots: expose Vulkan backdrop capabilities Enable the device capabilities Qt Quick needs to implement the Vulkan blur/backdrop effect (RenderBufferBlitter) with split render passes, and round out the device extensions Qt documents for an adopted VkDevice. - Probe and enable VK_KHR_separate_depth_stencil_layouts on the device and expose it via waylib_vk_renderer_has_separate_depth_stencil_layouts() so Qt can safely split and resume render passes around a depth-stencil pass. - Advertise transfer-source usage for render modifiers and record the max transfer-source extent, so render buffers can be created with VK_IMAGE_USAGE_TRANSFER_SRC_BIT when supported (used to sample the output buffer as the blur source). - Include color-attachment reads in both compatible render-pass external dependencies so preserved color attachments (effect split passes) observe the load operation as well as attachment writes. This fixes color issues in apps such as Youdao Cloud Notes that rely on preserved contents. - Qt Quick RHI adopts wlroots' VkDevice when the Qt Quick bridge runs on this renderer, and QRhiVulkanInitParams::preferredExtensionsForImportedDevice() lists VK_KHR_create_renderpass2, VK_KHR_depth_stencil_resolve and VK_EXT_vertex_attribute_divisor as expected on the imported device; without them Qt silently loses functionality on a per-driver basis. Enable all three whenever the driver offers them and decouple create_renderpass2 from the separate-depth-stencil-layouts probe. VK_KHR_swapchain is deliberately not added: the compositor drives Qt through an offscreen QQuickRenderControl and never presents a swapchain. No GLES2 behavior is affected; these are additive waylib bridge helpers. --- 3rdparty/wlroots/include/render/vulkan.h | 2 + 3rdparty/wlroots/include/wlr/render/vulkan.h | 2 + 3rdparty/wlroots/render/vulkan/pixel_format.c | 17 ++++++ 3rdparty/wlroots/render/vulkan/renderer.c | 16 +++++- 3rdparty/wlroots/render/vulkan/vulkan.c | 52 +++++++++++++++++++ 5 files changed, 87 insertions(+), 2 deletions(-) diff --git a/3rdparty/wlroots/include/render/vulkan.h b/3rdparty/wlroots/include/render/vulkan.h index 6e36142e60..eb56469fb5 100644 --- a/3rdparty/wlroots/include/render/vulkan.h +++ b/3rdparty/wlroots/include/render/vulkan.h @@ -44,6 +44,7 @@ struct wlr_vk_device { bool sync_file_import_export; bool implicit_sync_interop; bool sampler_ycbcr_conversion; + bool separate_depth_stencil_layouts; // we only ever need one queue for rendering and transfer commands uint32_t queue_family; @@ -98,6 +99,7 @@ const struct wlr_vk_format *vulkan_get_format_from_drm(uint32_t drm_format); struct wlr_vk_format_modifier_props { VkDrmFormatModifierPropertiesEXT props; VkExtent2D max_extent; + VkExtent2D transfer_src_max_extent; bool has_mutable_srgb; }; diff --git a/3rdparty/wlroots/include/wlr/render/vulkan.h b/3rdparty/wlroots/include/wlr/render/vulkan.h index cb699b5073..3d8637d517 100644 --- a/3rdparty/wlroots/include/wlr/render/vulkan.h +++ b/3rdparty/wlroots/include/wlr/render/vulkan.h @@ -52,5 +52,7 @@ bool waylib_vk_renderer_flush_stage(struct wlr_renderer *renderer); /* waylib extensions (not upstream): Qt Quick RHI texture-sampling helpers. */ VkQueue waylib_vk_renderer_get_queue(struct wlr_renderer *renderer); +bool waylib_vk_renderer_has_separate_depth_stencil_layouts( + struct wlr_renderer *renderer); #endif diff --git a/3rdparty/wlroots/render/vulkan/pixel_format.c b/3rdparty/wlroots/render/vulkan/pixel_format.c index 9b17d079a0..a1938cc815 100644 --- a/3rdparty/wlroots/render/vulkan/pixel_format.c +++ b/3rdparty/wlroots/render/vulkan/pixel_format.c @@ -448,6 +448,23 @@ static bool query_modifier_support(struct wlr_vk_device *dev, } if (supported) { + // Transfer-source support is optional and must not remove an + // otherwise valid render modifier. Users can inspect the actual + // image usage and fall back when this stricter query fails. + const VkImageUsageFlags transfer_src_usage = + vulkan_render_usage | VK_IMAGE_USAGE_TRANSFER_SRC_BIT; + const VkFormat variant = p.has_mutable_srgb ? + props->format.vk_srgb : VK_FORMAT_UNDEFINED; + struct wlr_vk_format_modifier_props transfer_src = {0}; + const char *transfer_src_errmsg = NULL; + if ((m.drmFormatModifierTilingFeatures & + VK_FORMAT_FEATURE_TRANSFER_SRC_BIT) && + query_modifier_usage_support(dev, props->format.vk, + variant, transfer_src_usage, &m, &transfer_src, + &transfer_src_errmsg)) { + p.transfer_src_max_extent = transfer_src.max_extent; + } + props->dmabuf.render_mods[props->dmabuf.render_mod_count++] = p; wlr_drm_format_set_add(&dev->dmabuf_render_formats, props->format.drm, m.drmFormatModifier); diff --git a/3rdparty/wlroots/render/vulkan/renderer.c b/3rdparty/wlroots/render/vulkan/renderer.c index cf05e2bb65..9811ac3b3e 100644 --- a/3rdparty/wlroots/render/vulkan/renderer.c +++ b/3rdparty/wlroots/render/vulkan/renderer.c @@ -2485,7 +2485,8 @@ static struct wlr_vk_render_format_setup *find_or_create_render_setup( VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT, .dstSubpass = 0, .dstStageMask = VK_PIPELINE_STAGE_ALL_GRAPHICS_BIT, - .dstAccessMask = VK_ACCESS_UNIFORM_READ_BIT | + .dstAccessMask = VK_ACCESS_COLOR_ATTACHMENT_READ_BIT | + VK_ACCESS_UNIFORM_READ_BIT | VK_ACCESS_VERTEX_ATTRIBUTE_READ_BIT | VK_ACCESS_INDIRECT_COMMAND_READ_BIT | VK_ACCESS_SHADER_READ_BIT, @@ -2599,7 +2600,8 @@ static struct wlr_vk_render_format_setup *find_or_create_render_setup( VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT, .dstSubpass = 0, .dstStageMask = VK_PIPELINE_STAGE_ALL_GRAPHICS_BIT, - .dstAccessMask = VK_ACCESS_UNIFORM_READ_BIT | + .dstAccessMask = VK_ACCESS_COLOR_ATTACHMENT_READ_BIT | + VK_ACCESS_UNIFORM_READ_BIT | VK_ACCESS_VERTEX_ATTRIBUTE_READ_BIT | VK_ACCESS_INDIRECT_COMMAND_READ_BIT | VK_ACCESS_SHADER_READ_BIT, @@ -2795,3 +2797,13 @@ VkQueue waylib_vk_renderer_get_queue(struct wlr_renderer *renderer) { struct wlr_vk_renderer *vk_renderer = vulkan_get_renderer(renderer); return vk_renderer->dev->queue; } + +bool waylib_vk_renderer_has_separate_depth_stencil_layouts( + struct wlr_renderer *renderer) { + if (renderer == NULL || !wlr_renderer_is_vk(renderer)) { + return false; + } + + struct wlr_vk_renderer *vk_renderer = vulkan_get_renderer(renderer); + return vk_renderer->dev->separate_depth_stencil_layouts; +} diff --git a/3rdparty/wlroots/render/vulkan/vulkan.c b/3rdparty/wlroots/render/vulkan/vulkan.c index 0c0445d802..3932585c67 100644 --- a/3rdparty/wlroots/render/vulkan/vulkan.c +++ b/3rdparty/wlroots/render/vulkan/vulkan.c @@ -546,15 +546,58 @@ struct wlr_vk_device *vulkan_device_create(struct wlr_vk_instance *ini, VkPhysicalDeviceSamplerYcbcrConversionFeatures phdev_sampler_ycbcr_features = { .sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SAMPLER_YCBCR_CONVERSION_FEATURES, }; + VkPhysicalDeviceSeparateDepthStencilLayoutsFeaturesKHR + phdev_separate_depth_stencil_layouts_features = { + .sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SEPARATE_DEPTH_STENCIL_LAYOUTS_FEATURES_KHR, + .pNext = &phdev_sampler_ycbcr_features, + }; + // VK_KHR_separate_depth_stencil_layouts depends on + // VK_KHR_create_renderpass2 when the instance targets Vulkan 1.1. + bool has_create_renderpass2 = check_extension(avail_ext_props, avail_extc, + VK_KHR_CREATE_RENDERPASS_2_EXTENSION_NAME); + bool has_separate_depth_stencil_layouts = has_create_renderpass2 && + check_extension(avail_ext_props, avail_extc, + VK_KHR_SEPARATE_DEPTH_STENCIL_LAYOUTS_EXTENSION_NAME); VkPhysicalDeviceFeatures2 phdev_features = { .sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2, .pNext = &phdev_sampler_ycbcr_features, }; + if (has_separate_depth_stencil_layouts) { + phdev_features.pNext = &phdev_separate_depth_stencil_layouts_features; + } vkGetPhysicalDeviceFeatures2(phdev, &phdev_features); dev->sampler_ycbcr_conversion = phdev_sampler_ycbcr_features.samplerYcbcrConversion; wlr_log(WLR_DEBUG, "Sampler YCbCr conversion %s", dev->sampler_ycbcr_conversion ? "supported" : "not supported"); + dev->separate_depth_stencil_layouts = has_separate_depth_stencil_layouts && + phdev_separate_depth_stencil_layouts_features.separateDepthStencilLayouts; + if (dev->separate_depth_stencil_layouts) { + extensions[extensions_len++] = + VK_KHR_SEPARATE_DEPTH_STENCIL_LAYOUTS_EXTENSION_NAME; + } + wlr_log(WLR_DEBUG, "Separate depth/stencil layouts %s", + dev->separate_depth_stencil_layouts ? "enabled" : "disabled"); + + // Qt Quick RHI adopts this device when waylib drives Qt on the wlroots + // Vulkan renderer. QRhiVulkanInitParams::preferredExtensionsForImportedDevice() + // documents VK_KHR_create_renderpass2, VK_KHR_depth_stencil_resolve and + // VK_EXT_vertex_attribute_divisor (VK_KHR_swapchain is intentionally not + // added: the compositor renders through an offscreen QQuickRenderControl + // and never creates a swapchain, and the device extension would be inert + // without companion instance surface support). Without them Qt silently + // loses functionality; enabling them is purely additive for wlroots. + if (has_create_renderpass2) { + extensions[extensions_len++] = VK_KHR_CREATE_RENDERPASS_2_EXTENSION_NAME; + } + if (check_extension(avail_ext_props, avail_extc, + VK_KHR_DEPTH_STENCIL_RESOLVE_EXTENSION_NAME)) { + extensions[extensions_len++] = VK_KHR_DEPTH_STENCIL_RESOLVE_EXTENSION_NAME; + } + if (check_extension(avail_ext_props, avail_extc, + VK_EXT_VERTEX_ATTRIBUTE_DIVISOR_EXTENSION_NAME)) { + extensions[extensions_len++] = VK_EXT_VERTEX_ATTRIBUTE_DIVISOR_EXTENSION_NAME; + } const float prio = 1.f; VkDeviceQueueCreateInfo qinfo = { @@ -585,11 +628,20 @@ struct wlr_vk_device *vulkan_device_create(struct wlr_vk_instance *ini, .sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SAMPLER_YCBCR_CONVERSION_FEATURES, .samplerYcbcrConversion = dev->sampler_ycbcr_conversion, }; + VkPhysicalDeviceSeparateDepthStencilLayoutsFeaturesKHR + separate_depth_stencil_layouts_features = { + .sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SEPARATE_DEPTH_STENCIL_LAYOUTS_FEATURES_KHR, + .pNext = &sampler_ycbcr_features, + .separateDepthStencilLayouts = dev->separate_depth_stencil_layouts, + }; VkPhysicalDeviceSynchronization2FeaturesKHR sync2_features = { .sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SYNCHRONIZATION_2_FEATURES_KHR, .pNext = &sampler_ycbcr_features, .synchronization2 = VK_TRUE, }; + if (dev->separate_depth_stencil_layouts) { + sync2_features.pNext = &separate_depth_stencil_layouts_features; + } VkPhysicalDeviceTimelineSemaphoreFeaturesKHR timeline_features = { .sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_TIMELINE_SEMAPHORE_FEATURES_KHR, .pNext = &sync2_features, From 3f91d633e723d714d822dbbb166d5514c078349e Mon Sep 17 00:00:00 2001 From: LFRon Date: Tue, 1 Sep 2026 22:03:34 +0800 Subject: [PATCH 03/15] 3rdparty/wlroots: add Qt Quick texture-sync bridge and async staging MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bridge the Qt/QRhi render path to wlroots' Vulkan textures when both share the same VkQueue, so client DMA-BUF/shm textures can be sampled by Qt Quick without per-frame CPU stalls. - waylib_vk_renderer_prepare_texture_for_sampling() / finish_texture_sampling(): record the queue-family-ownership and layout transition for one imported texture on Qt's command buffer. - Texture sync batch (begin/flush/abort_texture_sync_batch): collect the foreign-texture DMA-BUF sync_files for a frame, import them into reusable binary semaphores, and submit a single wait-only bridge command buffer that also records an all-commands memory barrier, so the later Qt submission observes the producer's writes. - Texture barrier batch (begin/flush/abort_texture_barrier_batch): defer the per-texture acquire/release barriers and record a single vkCmdPipelineBarrier per phase instead of one per texture. - Same-frame aliasing dependency barrier: a render buffer written by the current frame's command buffer (output/layer/cursor buffers) can be re-imported as a sampled texture within that same command buffer (layer-buffer compositing, cached compositor render targets). The two are separate logical VkImages aliasing the same DMA-BUF memory, and per §Memory Aliasing the ordering between uses of two aliases must be a memory dependency whose access scope covers both: neither the ownership-transfer acquire (its source access mask is ignored) nor an image memory barrier naming the *sampled* image covers writes performed through the render buffer image. So remember such producer buffers (frame_render_buffers) and, when the acquire batch flushes, record one global (resource-free) VkMemoryBarrier, MEMORY_WRITE -> MEMORY_READ, ALL_COMMANDS -> FRAGMENT_SHADER - the same shape the bridge above uses. - WAR direction of the same aliasing: prepare_texture_for_sampling() registers every sampled buffer (frame_sampled_buffers, deduplicated and append-only like frame_render_buffers) and waylib_vk_renderer_record_render_buffer_acquire() records an additional global MEMORY_READ -> MEMORY_WRITE dependency for such a buffer, so a render pass through the render buffer image can never begin before aliasing sampling finished. - waylib_vk_renderer_record_render_buffer_acquire() declares oldLayout VK_IMAGE_LAYOUT_GENERAL on the first acquire as well: PREINITIALIZED asserts host-written memory, which is never true for a freshly imported DMA-BUF scanout buffer; GENERAL is the single bookkeeping layout of this renderer's acquire/release ping-pong (matching waylib_vk_renderer_get_render_buffer_attribs() and the sampled-texture path), which also removes an oldLayout-01197 mismatch for validation. - waylib_vk_renderer_set_stage_async_enabled() + vulkan_submit_stage_async(): submit SHM staging uploads without blocking the CPU; the texture-sync bridge orders the upload before the consumer samples it. WLR_VK_FORCE_STAGE_BLOCK and WLR_VK_FORCE_SYNC_POLL force the legacy blocking paths. - wlr_vk_texture_get_image_attribs(): report the layout of DMA-BUF textures from the owned state instead of the stale "transitioned" flag, so the reported layout matches the acquire/release GENERAL <-> SHADER_READ_ONLY_OPTIMAL ping-pong; pixel textures keep the permanent post-upload layout. - destroy_render_buffer(): wait for the renderer timeline to catch up with the last submit point instead of vkQueueWaitIdle(). The async staging submissions added here make a non-empty queue a legitimate state on this path, while every wlroots submission (sync/async stage, bridge, render pass) completes by signalling the timeline semaphore, so matching timeline_point is the exact and cheaper equivalent. - get_command_buffer(): fail with a log when every slot of the fixed pool is recording; the bridge and staging submissions added here made that state reachable. - Add new renderer state (timeline semaphore, semaphore pool, barrier arrays, stage-async flags, alias-tracking lists) with matching create/destroy handling. No GLES2 behavior is affected. Follow-up hardening pass (folded): - Separate the Qt Quick RHI / readback acquire bookkeeping from wlroots' render-pass 'owned' flag with a dedicated qt_sampling_acquired flag, and self-link the foreign-texture list node in vulkan_texture_destroy(). An earlier wlroots render pass that failed between add and drain could leave owned=true and permanently suppress acquire for Qt and for readback; the foreign list entry of a destroyed texture is now unlinked instead of being left dangling. - Reset the frame alias lists at frame completion (waylib_vk_renderer_reset_frame_alias_lists(), called by waylib after the Qt offscreen frame has CPU-waited): same-queue submission order already orders the previous frame's GPU work ahead of the next frame's, so entries only need to live for one frame. This bounds the CPU scan cost and the conservative global alias barriers to the frame that created them. - Reclaim binary-semaphore slots used by a failed bridge submit (discard_texture_sync_sems + slot recreation in acquire_texture_sync_sem): a semaphore whose wait submission failed has consumed the temporary sync_fd import and cannot be reused for a new import, and its slot would otherwise be stuck at release_point=UINT64_MAX. - Add the out-of-frame readback gate waylib_vk_texture_begin_readback()/end_readback(): reads of DMA-BUF imported wlr_textures via wlr_texture_read_pixels() wait the buffer's DMA-BUF sync_files and record the FOREIGN<->own ownership transition around the staging command buffer (same-queue ordering covers the execution dependency; the staging submit still CPU-waits). The gate refuses multi-plane/YCbCr textures and textures whose foreign ownership is taken by a live acquire. Failure-path hardening (folded): - The barrier batch now keeps the per-texture bookkeeping next to each deferred barrier (texture_acquire_textures/texture_release_textures) and rolls it back whenever the batch is aborted or cannot be recorded (rollback_deferred_texture_barriers): a dropped acquire leaves the image FOREIGN-owned (it must be re-acquired on the next use), a dropped release leaves the graphics queue owning it (it must not be acquired again). A flush without a command buffer fails closed instead of silently dropping the batch, and aborting an active batch rolls it back before rearming. WAYLIB_VK_TEST_FLUSH_FAIL_AT= (one-shot, test-only) makes the n-th flush behave as if no command buffer were available, so the rollback path can be exercised on a live compositor. - Widen the batched acquire/release barrier stage masks from VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT to ALL_COMMANDS: the release's first scope must cover every stage that may still read the texture (not only fragment sampling), and the acquire barriers are recorded with the same shape. - vulkan_submit_stage_async() cleans up a failed submit (drop the stage buffer, recycle its command buffer slot) and reclaim_pending_stage_buffers() reclaims still-staged buffers from the renderer teardown instead of leaking them with the device. - waylib_vk_texture_end_readback() keeps the acquire when the foreign release cannot be recorded (fail closed), so the texture is never sampled or read again without its matching release. --- 3rdparty/wlroots/include/render/vulkan.h | 117 ++ 3rdparty/wlroots/include/wlr/render/vulkan.h | 77 ++ 3rdparty/wlroots/render/vulkan/renderer.c | 279 +++- 3rdparty/wlroots/render/vulkan/texture.c | 1222 +++++++++++++++++- 4 files changed, 1679 insertions(+), 16 deletions(-) diff --git a/3rdparty/wlroots/include/render/vulkan.h b/3rdparty/wlroots/include/render/vulkan.h index eb56469fb5..fe6b35f345 100644 --- a/3rdparty/wlroots/include/render/vulkan.h +++ b/3rdparty/wlroots/include/render/vulkan.h @@ -4,6 +4,7 @@ #include #include #include +#include #include #include #include @@ -293,6 +294,44 @@ struct wlr_vk_command_buffer { #define VULKAN_COMMAND_BUFFERS_CAP 64 +// Binary semaphore holding an imported foreign-texture DMA-BUF sync_file. +// Reusable once the texture_sync timeline semaphore reaches release_point. +struct wlr_vk_texture_sync_sem { + VkSemaphore semaphore; + uint64_t release_point; +}; + +// Append a wlr_buffer pointer to an append-only alias tracking list (dedup). +// Stale entries after buffer destruction may alias recycled pointers, which +// only ever adds a conservative extra memory dependency. Returns false when +// the allocation failed (the entry was then not added). +static inline bool vulkan_alias_list_add(struct wl_array *list, + struct wlr_buffer *buffer) { + struct wlr_buffer **entry; + wl_array_for_each(entry, list) { + if (*entry == buffer) { + return true; + } + } + struct wlr_buffer **slot = wl_array_add(list, sizeof(*slot)); + if (slot == NULL) { + return false; + } + *slot = buffer; + return true; +} + +static inline bool vulkan_alias_list_contains(const struct wl_array *list, + struct wlr_buffer *buffer) { + const struct wlr_buffer **entry; + wl_array_for_each(entry, list) { + if (*entry == buffer) { + return true; + } + } + return false; +} + // Vulkan wlr_renderer implementation on top of a wlr_vk_device. struct wlr_vk_renderer { struct wlr_renderer wlr_renderer; @@ -329,6 +368,72 @@ struct wlr_vk_renderer { VkSemaphore timeline_semaphore; uint64_t timeline_point; + // Frame-batched waiting on foreign texture DMA-BUF sync_files (Qt/QRhi + // path). Unsignaled sync_files are imported into binary semaphores and + // waited on by a bridge command buffer. Its pipeline barrier extends the + // wait dependency to the later Qt submission on the same queue. + VkSemaphore texture_sync_timeline_semaphore; + uint64_t texture_sync_timeline_point; + struct wl_array texture_sync_semaphores; // struct wlr_vk_texture_sync_sem + struct wl_array texture_sync_pending; // private texture sync wait entries + struct wl_array texture_sync_wait_infos; // VkSemaphoreSubmitInfoKHR scratch + bool texture_sync_batch_active; + bool texture_sync_force_poll; // env WLR_VK_FORCE_SYNC_POLL + + // Per-pass batching of foreign-texture acquire/release barriers (Qt/QRhi + // path). Instead of emitting one vkCmdPipelineBarrier per texture, the + // barriers are accumulated while a batch is active and recorded as a + // single call before (acquire phase) or after (release phase) the Qt draw. + struct wl_array texture_acquire_barriers; // VkImageMemoryBarrier + struct wl_array texture_release_barriers; // VkImageMemoryBarrier + // struct wlr_vk_texture * parallel to the barrier arrays above. A deferred + // barrier that is discarded without being recorded (batch abort, or a + // flush without a command buffer) must roll the per-texture ownership + // bookkeeping back, otherwise qt_sampling_acquired would claim a transfer + // that never happened. + struct wl_array texture_acquire_textures; // struct wlr_vk_texture * + struct wl_array texture_release_textures; // struct wlr_vk_texture * + // Set while a batch is active when a texture about to be sampled aliases + // (second import of the same DMA-BUF) a buffer that was rendered as a + // color attachment and is recorded in frame_render_buffers. The producer's + // writes happened through the other logical VkImage, so neither an + // ownership-transfer acquire (its source access mask is ignored) nor an + // image barrier naming the sampled image covers them in its memory scope: + // §Memory Aliasing requires a memory dependency, and only a global + // (resource-free) memory barrier can span the two aliases. One such + // barrier is recorded when the batch flushes. + bool frame_producer_alias_dep; + bool texture_barrier_batch_active; + bool texture_barrier_batch_release; // current batch is the release phase + + // wlr_buffer * rendered as a color attachment by the current frame's + // command buffer (output, layer, and cursor render buffers). Append-only + // with dedup, cleared by waylib_vk_renderer_reset_frame_alias_lists() at + // the compositor frame boundary: same-queue submission order already + // covers next-frame dependencies, so the lists only need to span one + // frame's recording. Stale entries would only ever add a conservative + // extra dependency, never remove one. + struct wl_array frame_render_buffers; + // wlr_buffer * sampled as a texture by the (Qt/QRhi) frame's command + // buffer. Same lifecycle and conservative policy as + // frame_render_buffers: a render-buffer acquire for one of these buffers + // records a global read-before-write (WAR) alias dependency, because the + // sampling may have read the aliased memory through another logical + // image whose barriers are out of this image's access scope. + struct wl_array frame_sampled_buffers; + + // Force the legacy blocking staging-upload path instead of the + // GPU-side asynchronous one (env WLR_VK_FORCE_STAGE_BLOCK). + bool stage_force_block; + // Enable the GPU-side asynchronous staging-upload path. Must only be set + // once the consumer (Qt/QRhi) is confirmed to submit to the same VkQueue, + // since the path relies on the same-queue texture-sync bridge to order the + // upload before the uploaded texture is sampled. + bool stage_async_enabled; + // A stage upload was submitted asynchronously and needs the texture-sync + // bridge barrier before the consumer submits its command buffer. + bool stage_async_needs_bridge; + size_t last_pool_size; struct wl_list descriptor_pools; // wlr_vk_descriptor_pool.link struct wl_list render_format_setups; // wlr_vk_render_format_setup.link @@ -411,6 +516,13 @@ VkCommandBuffer vulkan_record_stage_cb(struct wlr_vk_renderer *renderer); // finished execution. bool vulkan_submit_stage_wait(struct wlr_vk_renderer *renderer); +// Submits the current stage command buffer without blocking the CPU. The +// staging buffers it used are hidden in the command buffer's stage_buffers +// list and reclaimed once its timeline point is reached. Callers that sample +// the uploaded content must flush the texture-sync bridge and then submit +// their own command buffer to the same queue. +bool vulkan_submit_stage_async(struct wlr_vk_renderer *renderer); + struct wlr_vk_render_pass_texture { struct wlr_vk_texture *texture; @@ -500,6 +612,11 @@ struct wlr_vk_texture { struct wlr_vk_command_buffer *last_used_cb; // to track when it can be destroyed bool dmabuf_imported; bool owned; // if dmabuf_imported: whether we have ownership of the image + bool qt_sampling_acquired; // waylib/Qt-only bookkeeping: the Qt Quick RHI + // path (waylib_vk_renderer_prepare_texture_for_sampling() / + // waylib_vk_renderer_begin_readback()) currently holds a foreign->own + // acquire. Decoupled from 'owned', which is reserved for wlroots' own render-pass bookkeeping. + bool readback_acquired; // waylib/Qt-only: a readback acquire is in flight bool transitioned; // if dma_imported: whether we transitioned it away from preinit bool has_alpha; // whether the image is has alpha channel bool using_mutable_srgb; // can be accessed through _SRGB format view diff --git a/3rdparty/wlroots/include/wlr/render/vulkan.h b/3rdparty/wlroots/include/wlr/render/vulkan.h index 3d8637d517..0e3e9919ad 100644 --- a/3rdparty/wlroots/include/wlr/render/vulkan.h +++ b/3rdparty/wlroots/include/wlr/render/vulkan.h @@ -55,4 +55,81 @@ VkQueue waylib_vk_renderer_get_queue(struct wlr_renderer *renderer); bool waylib_vk_renderer_has_separate_depth_stencil_layouts( struct wlr_renderer *renderer); +// Prepare a wlroots-owned Vulkan texture for sampling on the given command +// buffer, transitioning it to SHADER_READ_ONLY_OPTIMAL and transferring queue +// ownership to the graphics queue if it is not already owned. The optional +// attribs out-parameter receives the texture's image attributes. +bool waylib_vk_renderer_prepare_texture_for_sampling(struct wlr_renderer *renderer, + struct wlr_texture *texture, VkCommandBuffer cb, + struct wlr_vk_image_attribs *attribs); +bool waylib_vk_renderer_finish_texture_sampling(struct wlr_renderer *renderer, + struct wlr_texture *texture, VkCommandBuffer cb); + +// Collect foreign-texture sync_files while a compositor frame is recorded, +// then submit one semaphore wait with a bridge barrier before the compositor +// command buffer is submitted to the same queue. The barrier extends the wait +// dependency to that later submission. The abort function is idempotent. +bool waylib_vk_renderer_begin_texture_sync_batch(struct wlr_renderer *renderer); +bool waylib_vk_renderer_flush_texture_sync_batch(struct wlr_renderer *renderer); +void waylib_vk_renderer_abort_texture_sync_batch(struct wlr_renderer *renderer); + +// Batch the per-texture queue-family-ownership/layout barriers that +// waylib_vk_renderer_prepare_texture_for_sampling() and +// waylib_vk_renderer_finish_texture_sampling() would otherwise emit one at a +// time. Begin a batch (release=false for the pre-draw acquire phase, +// release=true for the post-draw release phase), call prepare/finish for each +// texture, then flush once to record a single vkCmdPipelineBarrier covering +// every accumulated texture. While a batch is active, prepare defers acquire +// barriers and finish defers release barriers; a call belonging to the other +// phase still records immediately so the two phases never mix in one flush. +// abort discards any pending barriers and is idempotent. +bool waylib_vk_renderer_begin_texture_barrier_batch(struct wlr_renderer *renderer, + bool release); +bool waylib_vk_renderer_flush_texture_barrier_batch(struct wlr_renderer *renderer, + VkCommandBuffer cb); +void waylib_vk_renderer_abort_texture_barrier_batch(struct wlr_renderer *renderer); + +// Clear the frame's color-attachment-producer and sampled-texture alias +// lists. Call from the compositor frame boundary once the frame's GPU work +// has completed (e.g. after a synchronous Qt Offscreen endFrame on the same +// queue): every submission of the finished frame is ordered implicit with the +// next one on the same queue, and the in-frame alias barriers are recorded +// from the lists during the next frame, so keeping stale entries only adds +// permanent global barriers and unbounded CPU scans. +void waylib_vk_renderer_reset_frame_alias_lists(struct wlr_renderer *renderer); + +// Readback ownership gate for DMA-BUF-imported wlroots textures read through +// wlr_texture_read_pixels()/vulkan_read_pixels() from outside the Qt frame. +// The import lacks a known Vulkan-side layout and may carry an unsignaled +// producer fence, so reading it without this gate uses FOREIGN-owned memory. +// Call begin_readback() before the read: for a dmabuf texture it waits the +// buffer's DMA-BUF fences (sync_file poll) and records/submit a FOREIGN->own +// acquire barrier leaving the image in VK_IMAGE_LAYOUT_GENERAL; the follow-up +// read_pixels() staging command buffer is submitted later on the same queue, +// which gives the required execution dependency. ACQUIRED means end_readback() +// MUST be called after the read (recorded on its own command buffer); NOOP is +// returned for textures that are not DMA-BUF imports (SHM upload textures); +// ERROR fails closed for multi-plane/disjoint/YCbCr textures or a texture +// whose foreign ownership is already held. begin_readback() never waits the +// read itself; the read_pixels() staging submit CPU-waits as usual. +enum wlr_vk_texture_readback_state { + WLR_VK_TEXTURE_READBACK_ERROR = 0, + WLR_VK_TEXTURE_READBACK_ACQUIRED, + WLR_VK_TEXTURE_READBACK_NOOP, +}; +enum wlr_vk_texture_readback_state waylib_vk_texture_begin_readback( + struct wlr_renderer *renderer, struct wlr_texture *texture); +void waylib_vk_texture_end_readback(struct wlr_renderer *renderer, + struct wlr_texture *texture); + +// Enable the GPU-side asynchronous staging-upload path used for shared-memory +// (CPU-rendered) client buffers. It submits the staging copy without blocking +// the caller and chains the upload through the texture-sync bridge before the +// texture is sampled, so it must only be enabled when the consumer (e.g. +// Qt/QRhi) submits its command buffers to the same VkQueue as the renderer and +// flushes that bridge before submission. When disabled (the default), staging +// uploads use a blocking wait. +void waylib_vk_renderer_set_stage_async_enabled(struct wlr_renderer *renderer, + bool enabled); + #endif diff --git a/3rdparty/wlroots/render/vulkan/renderer.c b/3rdparty/wlroots/render/vulkan/renderer.c index 9811ac3b3e..c22df3f2dd 100644 --- a/3rdparty/wlroots/render/vulkan/renderer.c +++ b/3rdparty/wlroots/render/vulkan/renderer.c @@ -417,6 +417,7 @@ bool vulkan_submit_stage_wait(struct wlr_vk_renderer *renderer) { return vulkan_wait_command_buffer(cb, renderer); } + bool waylib_vk_renderer_flush_stage(struct wlr_renderer *wlr_renderer) { assert(wlr_renderer_is_vk(wlr_renderer)); struct wlr_vk_renderer *renderer = vulkan_get_renderer(wlr_renderer); @@ -426,6 +427,106 @@ bool waylib_vk_renderer_flush_stage(struct wlr_renderer *wlr_renderer) { return vulkan_submit_stage_wait(renderer); } +// Returns the staging spans referenced by a stage command buffer that was +// never submitted (or whose submission failed) to the allocator. Safe because +// no GPU work referencing them was ever queued; the same release is performed +// by release_completed_stage_buffers() after a blocking submission completes. +static void reclaim_pending_stage_buffers(struct wlr_vk_renderer *renderer) { + int64_t now = get_current_time_msec(); + struct wlr_vk_shared_buffer *buf; + wl_list_for_each(buf, &renderer->stage.buffers, link) { + if (buf->allocs.size == 0) { + continue; + } + + buf->allocs.size = 0; + buf->last_used_ms = now; + } +} + +bool vulkan_submit_stage_async(struct wlr_vk_renderer *renderer) { + if (renderer->stage.cb == NULL) { + return true; + } + + struct wlr_vk_command_buffer *cb = renderer->stage.cb; + renderer->stage.cb = NULL; + + uint64_t timeline_point = vulkan_end_command_buffer(cb, renderer); + if (timeline_point == 0) { + // The command buffer is invalid and was never submitted: make the + // slot reusable again and return its staging spans to the pool. + cb->timeline_point = 0; + vulkan_reset_command_buffer(cb); + reclaim_pending_stage_buffers(renderer); + return false; + } + + VkTimelineSemaphoreSubmitInfoKHR timeline_submit_info = { + .sType = VK_STRUCTURE_TYPE_TIMELINE_SEMAPHORE_SUBMIT_INFO_KHR, + .signalSemaphoreValueCount = 1, + .pSignalSemaphoreValues = &timeline_point, + }; + VkSubmitInfo submit_info = { + .sType = VK_STRUCTURE_TYPE_SUBMIT_INFO, + .pNext = &timeline_submit_info, + .commandBufferCount = 1, + .pCommandBuffers = &cb->vk, + .signalSemaphoreCount = 1, + .pSignalSemaphores = &renderer->timeline_semaphore, + }; + VkResult res = vkQueueSubmit(renderer->dev->queue, 1, &submit_info, VK_NULL_HANDLE); + if (res != VK_SUCCESS) { + wlr_vk_error("vkQueueSubmit", res); + cb->timeline_point = 0; + vulkan_reset_command_buffer(cb); + // Nothing executed: the staging spans are safe to reuse. + reclaim_pending_stage_buffers(renderer); + return false; + } + + renderer->stage.last_timeline_point = timeline_point; + renderer->stage_async_needs_bridge = true; + + // Hide the staging buffers used by this submission from the allocator. + // release_command_buffer_resources() returns them (with their allocations + // reset) once this command buffer's timeline point is reached, so the CPU + // never reuses a span the GPU is still reading. The texture-sync bridge + // inserts the dependency required before a later Qt sampling submission. + size_t hidden = 0; + struct wlr_vk_shared_buffer *buf, *tmp; + wl_list_for_each_safe(buf, tmp, &renderer->stage.buffers, link) { + if (buf->allocs.size == 0) { + continue; + } + wl_list_remove(&buf->link); + wl_list_insert(&cb->stage_buffers, &buf->link); + ++hidden; + } + + static bool logged_async_stage; + if (!logged_async_stage) { + logged_async_stage = true; + wlr_log(WLR_INFO, "vk-stage: using GPU-side asynchronous staging " + "uploads (set WLR_VK_FORCE_STAGE_BLOCK=1 to force the blocking path)"); + } + wlr_log(WLR_DEBUG, "vk-stage: submitted staging command buffer " + "timeline_point=%llu buffers_hidden=%zu", + (unsigned long long)timeline_point, hidden); + + return true; +} + +void waylib_vk_renderer_set_stage_async_enabled(struct wlr_renderer *wlr_renderer, + bool enabled) { + if (wlr_renderer == NULL || !wlr_renderer_is_vk(wlr_renderer)) { + return; + } + + struct wlr_vk_renderer *renderer = vulkan_get_renderer(wlr_renderer); + renderer->stage_async_enabled = enabled; +} + struct wlr_vk_format_props *vulkan_format_props_from_drm( struct wlr_vk_device *dev, uint32_t drm_fmt) { for (size_t i = 0u; i < dev->format_prop_count; ++i) { @@ -482,6 +583,53 @@ bool vulkan_wait_command_buffer(struct wlr_vk_command_buffer *cb, return true; } +// Waits until every submission issued by this renderer has completed on the +// GPU. All of them (stage uploads, the texture-sync bridge, render passes) +// signal the renderer timeline semaphore with their command buffer's timeline +// point as the final operation, so waiting for the counter to catch up with +// renderer->timeline_point matches queue idleness for this renderer's work +// without a queue-wide flush. Callers that destroy resources may only run on +// the compositor frame boundary, where the Qt/QRhi frame has already been +// submitted and CPU-waited (QRhi offscreen endFrame does the latter), so +// Qt-side submissions of the same queue are complete as well. If that +// invariant changes, Qt submissions must additionally be tracked here. +static bool vulkan_wait_renderer_idle(struct wlr_vk_renderer *renderer) { + VkResult res; + + if (renderer->timeline_semaphore == VK_NULL_HANDLE) { + res = vkQueueWaitIdle(renderer->dev->queue); + if (res != VK_SUCCESS) { + wlr_vk_error("vkQueueWaitIdle", res); + return false; + } + return true; + } + + uint64_t current_point; + res = renderer->dev->api.vkGetSemaphoreCounterValueKHR(renderer->dev->dev, + renderer->timeline_semaphore, ¤t_point); + if (res != VK_SUCCESS) { + wlr_vk_error("vkGetSemaphoreCounterValueKHR", res); + current_point = 0; + } else if (current_point >= renderer->timeline_point) { + return true; + } + + VkSemaphoreWaitInfoKHR wait_info = { + .sType = VK_STRUCTURE_TYPE_SEMAPHORE_WAIT_INFO_KHR, + .semaphoreCount = 1, + .pSemaphores = &renderer->timeline_semaphore, + .pValues = &renderer->timeline_point, + }; + res = renderer->dev->api.vkWaitSemaphoresKHR(renderer->dev->dev, + &wait_info, UINT64_MAX); + if (res != VK_SUCCESS) { + wlr_vk_error("vkWaitSemaphoresKHR", res); + return false; + } + return true; +} + static void release_command_buffer_resources(struct wlr_vk_command_buffer *cb, struct wlr_vk_renderer *renderer, int64_t now) { struct wlr_vk_texture *texture, *texture_tmp; @@ -491,6 +639,7 @@ static void release_command_buffer_resources(struct wlr_vk_command_buffer *cb, wlr_texture_destroy(&texture->wlr_texture); } + size_t reclaimed = 0; struct wlr_vk_shared_buffer *buf, *buf_tmp; wl_list_for_each_safe(buf, buf_tmp, &cb->stage_buffers, link) { buf->allocs.size = 0; @@ -498,6 +647,10 @@ static void release_command_buffer_resources(struct wlr_vk_command_buffer *cb, wl_list_remove(&buf->link); wl_list_insert(&renderer->stage.buffers, &buf->link); + ++reclaimed; + } + if (reclaimed > 0) { + wlr_log(WLR_DEBUG, "vk-stage: reclaimed %zu staging buffers", reclaimed); } if (cb->color_transform) { @@ -566,7 +719,13 @@ static struct wlr_vk_command_buffer *get_command_buffer( return unused; } - // Block until a busy command buffer becomes available + // Block until a busy command buffer becomes available. With every slot + // recording there may be no completed one to wait on; fail instead of + // dereferencing a NULL waiter. + if (wait == NULL) { + wlr_log(WLR_ERROR, "No idle Vulkan command buffer available"); + return NULL; + } if (!vulkan_wait_command_buffer(wait, renderer)) { return NULL; } @@ -628,10 +787,12 @@ static void destroy_render_buffer(struct wlr_vk_render_buffer *buffer) { VkDevice dev = buffer->renderer->dev->dev; // TODO: asynchronously wait for the command buffers using this render - // buffer to complete (just like we do for textures) - VkResult res = vkQueueWaitIdle(buffer->renderer->dev->queue); - if (res != VK_SUCCESS) { - wlr_vk_error("vkQueueWaitIdle", res); + // buffer to complete (just like we do for textures). Until then, wait on + // the renderer timeline (see vulkan_wait_renderer_idle) instead of a + // queue-wide wait so this eviction path does not flush unrelated work. + if (!vulkan_wait_renderer_idle(buffer->renderer)) { + wlr_log(WLR_ERROR, "Failed to wait for outstanding Vulkan submissions " + "before destroying a render buffer; continuing best-effort"); } finish_render_buffer_out(&buffer->linear.out, dev); @@ -1041,15 +1202,16 @@ bool waylib_vk_renderer_record_render_buffer_acquire(struct wlr_renderer *wlr_re } /* Match wlroots pass.c / tinywl: foreign ownership transfer into the - * graphics queue before writing the scanout image. */ + * graphics queue before writing the scanout image. DMA-BUF imports start + * without a Vulkan-side known layout; GENERAL is the accounting layout + * every path in this renderer shares for them (and the steady-state + * layout after each render-buffer release), so the first acquire uses it + * as well instead of PREINITIALIZED, which describes host-written memory + * and never applies here. All output pathways share the imported + * VkImage; once transitioned, each pathway observes GENERAL on + * subsequent use. + */ VkImageLayout src_layout = VK_IMAGE_LAYOUT_GENERAL; - if (!render_buffer->linear.out.transitioned && - !render_buffer->srgb.out.transitioned && - !render_buffer->two_pass.out.transitioned) { - src_layout = VK_IMAGE_LAYOUT_PREINITIALIZED; - } - // All output pathways share the imported VkImage. Once this acquire - // barrier runs, each pathway observes GENERAL on subsequent use. render_buffer->linear.out.transitioned = true; render_buffer->srgb.out.transitioned = true; render_buffer->two_pass.out.transitioned = true; @@ -1072,6 +1234,22 @@ bool waylib_vk_renderer_record_render_buffer_acquire(struct wlr_renderer *wlr_re vkCmdPipelineBarrier(cb, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT, 0, 0, NULL, 0, NULL, 1, &barrier); + + // WAR alias dependency: the texture-sampling path may have recently read + // this buffer's memory through a second logical VkImage. Writes through + // this image must not begin before those reads finished; a barrier naming + // either image cannot cover the other's accesses, so use the global form + // (§Memory Aliasing). + if (vulkan_alias_list_contains(&renderer->frame_sampled_buffers, wlr_buffer)) { + VkMemoryBarrier memory_barrier = { + .sType = VK_STRUCTURE_TYPE_MEMORY_BARRIER, + .srcAccessMask = VK_ACCESS_MEMORY_READ_BIT, + .dstAccessMask = VK_ACCESS_MEMORY_WRITE_BIT, + }; + vkCmdPipelineBarrier(cb, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, + VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, 0, + 1, &memory_barrier, 0, NULL, 0, NULL); + } return true; } @@ -1092,6 +1270,16 @@ bool waylib_vk_renderer_record_render_buffer_release(struct wlr_renderer *wlr_re return false; } + // Remember the buffer as a color-attachment producer aliasing its sampled + // textures. When such a texture is imported and sampled, wlroots records a + // global memory dependency extending these writes to the sampling + // (see record_frame_render_barrier; an ownership-transfer or image-scoped + // barrier cannot cover accesses made through this separate logical image). + if (!vulkan_alias_list_add(&renderer->frame_render_buffers, wlr_buffer)) { + wlr_log_errno(WLR_ERROR, + "Failed to track the frame render buffer producer"); + } + VkImageMemoryBarrier barrier = { .sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER, .srcQueueFamilyIndex = renderer->dev->queue_family, @@ -1113,6 +1301,16 @@ bool waylib_vk_renderer_record_render_buffer_release(struct wlr_renderer *wlr_re return true; } +void waylib_vk_renderer_reset_frame_alias_lists(struct wlr_renderer *wlr_renderer) { + if (wlr_renderer == NULL || !wlr_renderer_is_vk(wlr_renderer)) { + return; + } + + struct wlr_vk_renderer *renderer = vulkan_get_renderer(wlr_renderer); + renderer->frame_render_buffers.size = 0; + renderer->frame_sampled_buffers.size = 0; +} + static bool buffer_export_sync_file(struct wlr_vk_renderer *renderer, struct wlr_buffer *buffer, uint32_t flags, int sync_file_fds[static WLR_DMABUF_MAX_PLANES]) { struct wlr_dmabuf_attributes dmabuf = {0}; @@ -1262,6 +1460,9 @@ static void vulkan_destroy(struct wlr_renderer *wlr_renderer) { return; } + waylib_vk_renderer_abort_texture_sync_batch(wlr_renderer); + waylib_vk_renderer_abort_texture_barrier_batch(wlr_renderer); + VkResult res = vkDeviceWaitIdle(renderer->dev->dev); if (res != VK_SUCCESS) { wlr_vk_error("vkDeviceWaitIdle", res); @@ -1342,6 +1543,27 @@ static void vulkan_destroy(struct wlr_renderer *wlr_renderer) { vkFreeMemory(dev->dev, renderer->dummy3d_mem, NULL); vkDestroySemaphore(dev->dev, renderer->timeline_semaphore, NULL); + + struct wlr_vk_texture_sync_sem *texture_sync_sem; + wl_array_for_each(texture_sync_sem, &renderer->texture_sync_semaphores) { + if (texture_sync_sem->semaphore != VK_NULL_HANDLE) { + vkDestroySemaphore(dev->dev, texture_sync_sem->semaphore, NULL); + } + } + wl_array_release(&renderer->texture_sync_semaphores); + wl_array_release(&renderer->texture_sync_pending); + wl_array_release(&renderer->texture_sync_wait_infos); + wl_array_release(&renderer->texture_acquire_barriers); + wl_array_release(&renderer->texture_release_barriers); + wl_array_release(&renderer->texture_acquire_textures); + wl_array_release(&renderer->texture_release_textures); + wl_array_release(&renderer->frame_sampled_buffers); + wl_array_release(&renderer->frame_render_buffers); + + if (renderer->texture_sync_timeline_semaphore != VK_NULL_HANDLE) { + vkDestroySemaphore(dev->dev, renderer->texture_sync_timeline_semaphore, NULL); + } + vkDestroyPipelineLayout(dev->dev, renderer->output_pipe_layout, NULL); vkDestroyDescriptorSetLayout(dev->dev, renderer->output_ds_srgb_layout, NULL); vkDestroyDescriptorSetLayout(dev->dev, renderer->output_ds_lut3d_layout, NULL); @@ -2687,6 +2909,17 @@ struct wlr_renderer *vulkan_renderer_create_for_device(struct wlr_vk_device *dev wl_list_init(&renderer->render_buffers); wl_list_init(&renderer->color_transforms); wl_list_init(&renderer->pipeline_layouts); + wl_array_init(&renderer->texture_sync_semaphores); + wl_array_init(&renderer->texture_sync_pending); + wl_array_init(&renderer->texture_sync_wait_infos); + wl_array_init(&renderer->texture_acquire_barriers); + wl_array_init(&renderer->texture_release_barriers); + wl_array_init(&renderer->texture_acquire_textures); + wl_array_init(&renderer->texture_release_textures); + wl_array_init(&renderer->frame_sampled_buffers); + wl_array_init(&renderer->frame_render_buffers); + renderer->texture_sync_force_poll = getenv("WLR_VK_FORCE_SYNC_POLL") != NULL; + renderer->stage_force_block = getenv("WLR_VK_FORCE_STAGE_BLOCK") != NULL; renderer->wlr_renderer.color_encodings = WLR_COLOR_ENCODING_BT601 | @@ -2730,6 +2963,26 @@ struct wlr_renderer *vulkan_renderer_create_for_device(struct wlr_vk_device *dev goto error; } + bool can_gpu_wait_texture_sync = dev->implicit_sync_interop + && dev->api.vkImportSemaphoreFdKHR != NULL + && dev->api.vkQueueSubmit2KHR != NULL + && dev->api.vkGetSemaphoreCounterValueKHR != NULL + && !renderer->texture_sync_force_poll; + if (can_gpu_wait_texture_sync) { + res = vkCreateSemaphore(dev->dev, &semaphore_info, NULL, + &renderer->texture_sync_timeline_semaphore); + if (res != VK_SUCCESS) { + wlr_vk_error("vkCreateSemaphore", res); + renderer->texture_sync_timeline_semaphore = VK_NULL_HANDLE; + can_gpu_wait_texture_sync = false; + } + } + + wlr_log(WLR_DEBUG, "Vulkan foreign-texture frame sync initialized " + "(implicit_sync_interop=%d, force_poll=%d, gpu_wait=%s)", + dev->implicit_sync_interop, renderer->texture_sync_force_poll, + can_gpu_wait_texture_sync ? "enabled" : "disabled"); + return &renderer->wlr_renderer; error: diff --git a/3rdparty/wlroots/render/vulkan/texture.c b/3rdparty/wlroots/render/vulkan/texture.c index fa8b2645c3..e7d15fc85a 100644 --- a/3rdparty/wlroots/render/vulkan/texture.c +++ b/3rdparty/wlroots/render/vulkan/texture.c @@ -1,6 +1,9 @@ #include #include +#include #include +#include +#include #include #include #include @@ -12,6 +15,7 @@ #include #include "render/pixel_format.h" #include "render/vulkan.h" +#include "util/time.h" static const struct wlr_texture_impl texture_impl; @@ -186,6 +190,19 @@ void vulkan_texture_destroy(struct wlr_vk_texture *texture) { texture->buffer = NULL; } + // Defensive unlink from the foreign-texture list: wlroots' render pass + // submits that fail before its drain loop strand entries there, and + // destroying the texture's buffer through that path would leave a dangling + // node that the next successful submit's drain would touch (use-after-free). + // pass.c inserts only when both link fields are NULL; self-linking marks + // "not inserted" consistently for the wl_list_empty() checks elsewhere. + if (texture->foreign_link.next != NULL && + !wl_list_empty(&texture->foreign_link)) { + wl_list_remove(&texture->foreign_link); + } + texture->foreign_link.prev = &texture->foreign_link; + texture->foreign_link.next = &texture->foreign_link; + // when we recorded a command to fill this image _this_ frame, // it has to be executed before the texture can be destroyed. // Add it to the renderer->destroy_textures list, destroying @@ -551,6 +568,14 @@ VkImage vulkan_import_dmabuf(struct wlr_vk_renderer *renderer, return VK_NULL_HANDLE; } + VkImageUsageFlags usage = for_render ? + vulkan_render_usage : vulkan_dma_tex_usage; + if (for_render && + (uint32_t)attribs->width <= mod->transfer_src_max_extent.width && + (uint32_t)attribs->height <= mod->transfer_src_max_extent.height) { + usage |= VK_IMAGE_USAGE_TRANSFER_SRC_BIT; + } + // check if we have to create the image disjoint bool disjoint = is_dmabuf_disjoint(attribs); if (disjoint && !(mod->props.drmFormatModifierTilingFeatures @@ -572,7 +597,7 @@ VkImage vulkan_import_dmabuf(struct wlr_vk_renderer *renderer, .sharingMode = VK_SHARING_MODE_EXCLUSIVE, .initialLayout = VK_IMAGE_LAYOUT_UNDEFINED, .extent = (VkExtent3D) { attribs->width, attribs->height, 1 }, - .usage = for_render ? vulkan_render_usage : vulkan_dma_tex_usage, + .usage = usage, }; if (disjoint) { img_info.flags = VK_IMAGE_CREATE_DISJOINT_BIT; @@ -837,11 +862,1202 @@ void wlr_vk_texture_get_image_attribs(struct wlr_texture *texture, struct wlr_vk_texture *vk_texture = vulkan_get_texture(texture); attribs->image = vk_texture->image; attribs->format = vk_texture->format->vk; - attribs->layout = vk_texture->transitioned ? - VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL : VK_IMAGE_LAYOUT_GENERAL; + // For DMA-BUF textures the actual layout follows the acquire/release + // ownership ping-pong: owned textures are transitioned to + // SHADER_READ_ONLY_OPTIMAL by the acquire barrier and released back to + // GENERAL afterwards, so "transitioned" alone would report a stale layout. + // Pixel (non-DMA-BUF) textures keep the permanent post-upload layout. + // qt_sampling_acquired is the Qt-path owner of that ping-pong; 'owned' + // is kept for the wlroots render-pass bookkeeping only (see header). + attribs->layout = vk_texture->dmabuf_imported + ? ((vk_texture->owned || vk_texture->qt_sampling_acquired) + ? VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL + : VK_IMAGE_LAYOUT_GENERAL) + : (vk_texture->transitioned + ? VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL + : VK_IMAGE_LAYOUT_GENERAL); + attribs->usage = vk_texture->dmabuf_imported ? + vulkan_dma_tex_usage : vulkan_shm_tex_usage; } bool wlr_vk_texture_has_alpha(struct wlr_texture *texture) { struct wlr_vk_texture *vk_texture = vulkan_get_texture(texture); return vk_texture->has_alpha; } + +static void release_completed_stage_buffers(struct wlr_vk_renderer *renderer) { + int64_t now = get_current_time_msec(); + struct wlr_vk_shared_buffer *buf; + wl_list_for_each(buf, &renderer->stage.buffers, link) { + if (buf->allocs.size == 0) { + continue; + } + + buf->allocs.size = 0; + buf->last_used_ms = now; + } +} + +static bool submit_pending_stage_uploads(struct wlr_vk_renderer *renderer) { + if (renderer->stage.cb == NULL) { + return true; + } + + // The GPU-side asynchronous path is only safe when the consumer submits + // to the same queue (see waylib_vk_renderer_set_stage_async_enabled). Fall + // back to the legacy blocking path otherwise, or when explicitly forced. + if (!renderer->stage_async_enabled || renderer->stage_force_block) { + static bool logged_blocking_stage; + if (!logged_blocking_stage) { + logged_blocking_stage = true; + wlr_log(WLR_INFO, "vk-stage: using blocking staging uploads " + "(async path %s)", renderer->stage_force_block + ? "forced off via WLR_VK_FORCE_STAGE_BLOCK" + : "not enabled"); + } + if (!vulkan_submit_stage_wait(renderer)) { + wlr_log(WLR_ERROR, "Failed to submit pending Vulkan texture stage upload"); + return false; + } + + release_completed_stage_buffers(renderer); + return true; + } + + // GPU-side asynchronous path: submit the staging command buffer without + // blocking the render thread. The staging spans are reclaimed once the + // submission's timeline point is reached (see vulkan_submit_stage_async). + if (!vulkan_submit_stage_async(renderer)) { + wlr_log(WLR_ERROR, "Failed to submit pending Vulkan texture stage upload"); + return false; + } + + return true; +} + +static void close_sync_file_fds(int sync_file_fds[static WLR_DMABUF_MAX_PLANES]) { + for (size_t i = 0; i < WLR_DMABUF_MAX_PLANES; i++) { + if (sync_file_fds[i] < 0) { + continue; + } + + close(sync_file_fds[i]); + sync_file_fds[i] = -1; + } +} + +struct wlr_vk_texture_sync_wait { + int fd; + size_t semaphore_index; +}; + +// Returns 1 when signaled, 0 on timeout, and -1 on an error or invalid poll +// result. A sync_file is only considered ready when POLLIN is reported. +static int poll_sync_file(int fd, int timeout_ms) { + struct pollfd pollfd = { + .fd = fd, + .events = POLLIN, + }; + + int ret; + do { + ret = poll(&pollfd, 1, timeout_ms); + } while (ret < 0 && errno == EINTR); + + if (ret < 0) { + wlr_log_errno(WLR_ERROR, "Failed to poll texture DMA-BUF sync_file"); + return -1; + } + if (ret == 0) { + return 0; + } + if ((pollfd.revents & (POLLERR | POLLNVAL)) != 0 || + (pollfd.revents & POLLIN) == 0) { + wlr_log(WLR_ERROR, "Invalid texture DMA-BUF sync_file poll result: 0x%x", + pollfd.revents); + return -1; + } + + return 1; +} + +static bool wait_sync_file_fds(int sync_file_fds[static WLR_DMABUF_MAX_PLANES]) { + bool ok = true; + + for (size_t i = 0; i < WLR_DMABUF_MAX_PLANES; i++) { + int fd = sync_file_fds[i]; + if (fd < 0) { + continue; + } + + int ret = poll_sync_file(fd, 1000); + if (ret == 0) { + wlr_log(WLR_ERROR, "Timed out while waiting for texture DMA-BUF sync_file"); + ok = false; + } else if (ret < 0) { + ok = false; + } + + close(fd); + sync_file_fds[i] = -1; + } + + return ok; +} + +static bool collect_sync_file_fds(struct wlr_vk_renderer *renderer, + int sync_file_fds[static WLR_DMABUF_MAX_PLANES]) { + size_t count = 0; + for (size_t i = 0; i < WLR_DMABUF_MAX_PLANES; i++) { + count += sync_file_fds[i] >= 0; + } + if (count == 0) { + return true; + } + + struct wlr_vk_texture_sync_wait *waits = wl_array_add( + &renderer->texture_sync_pending, count * sizeof(*waits)); + if (waits == NULL) { + close_sync_file_fds(sync_file_fds); + return false; + } + + size_t wait_index = 0; + for (size_t i = 0; i < WLR_DMABUF_MAX_PLANES; i++) { + if (sync_file_fds[i] < 0) { + continue; + } + + waits[wait_index++] = (struct wlr_vk_texture_sync_wait) { + .fd = sync_file_fds[i], + .semaphore_index = SIZE_MAX, + }; + sync_file_fds[i] = -1; + } + + return true; +} + +void waylib_vk_renderer_abort_texture_sync_batch(struct wlr_renderer *wlr_renderer) { + if (wlr_renderer == NULL || !wlr_renderer_is_vk(wlr_renderer)) { + return; + } + + struct wlr_vk_renderer *renderer = vulkan_get_renderer(wlr_renderer); + struct wlr_vk_texture_sync_wait *wait; + wl_array_for_each(wait, &renderer->texture_sync_pending) { + if (wait->fd >= 0) { + close(wait->fd); + wait->fd = -1; + } + } + + renderer->texture_sync_pending.size = 0; + renderer->texture_sync_wait_infos.size = 0; + renderer->texture_sync_batch_active = false; +} + +bool waylib_vk_renderer_begin_texture_sync_batch(struct wlr_renderer *wlr_renderer) { + if (wlr_renderer == NULL || !wlr_renderer_is_vk(wlr_renderer)) { + return false; + } + + struct wlr_vk_renderer *renderer = vulkan_get_renderer(wlr_renderer); + if (renderer->texture_sync_batch_active) { + wlr_log(WLR_ERROR, "Vulkan texture sync batch is already active"); + waylib_vk_renderer_abort_texture_sync_batch(wlr_renderer); + return false; + } + if (renderer->texture_sync_pending.size != 0) { + wlr_log(WLR_ERROR, "Discarding stale Vulkan texture sync batch entries"); + waylib_vk_renderer_abort_texture_sync_batch(wlr_renderer); + } + + renderer->texture_sync_batch_active = true; + return true; +} + +static bool wait_texture_sync_entries(struct wlr_vk_texture_sync_wait *waits, + size_t count) { + bool ok = true; + for (size_t i = 0; i < count; i++) { + if (waits[i].fd < 0) { + continue; + } + + int ret = poll_sync_file(waits[i].fd, 1000); + if (ret == 0) { + wlr_log(WLR_ERROR, "Timed out while waiting for texture DMA-BUF sync_file"); + ok = false; + } else if (ret < 0) { + ok = false; + } + + close(waits[i].fd); + waits[i].fd = -1; + } + return ok; +} + +static struct wlr_vk_texture_sync_sem *texture_sync_sem_at( + struct wlr_vk_renderer *renderer, size_t index) { + size_t count = renderer->texture_sync_semaphores.size / + sizeof(struct wlr_vk_texture_sync_sem); + assert(index < count); + return &((struct wlr_vk_texture_sync_sem *) + renderer->texture_sync_semaphores.data)[index]; +} + +// Returns an index instead of a pointer because wl_array_add() may move the +// backing allocation while multiple semaphores are claimed for one batch. +static bool acquire_texture_sync_sem(struct wlr_vk_renderer *renderer, + uint64_t cur_point, size_t *index) { + struct wlr_vk_texture_sync_sem *sems = renderer->texture_sync_semaphores.data; + size_t count = renderer->texture_sync_semaphores.size / sizeof(*sems); + for (size_t i = 0; i < count; i++) { + if (sems[i].release_point <= cur_point) { + if (sems[i].semaphore == VK_NULL_HANDLE) { + // Reclaimed slot from a failed bridge submit (see + // discard_texture_sync_sems): rebuild it here. + VkSemaphoreCreateInfo semaphore_info = { + .sType = VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO, + }; + VkResult cres = vkCreateSemaphore(renderer->dev->dev, + &semaphore_info, NULL, &sems[i].semaphore); + if (cres != VK_SUCCESS) { + wlr_vk_error("vkCreateSemaphore", cres); + return false; + } + } + sems[i].release_point = UINT64_MAX; + *index = i; + return true; + } + } + + VkSemaphoreCreateInfo semaphore_info = { + .sType = VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO, + }; + VkSemaphore semaphore; + VkResult res = vkCreateSemaphore(renderer->dev->dev, &semaphore_info, + NULL, &semaphore); + if (res != VK_SUCCESS) { + wlr_vk_error("vkCreateSemaphore", res); + return false; + } + + struct wlr_vk_texture_sync_sem *sem = wl_array_add( + &renderer->texture_sync_semaphores, sizeof(*sem)); + if (sem == NULL) { + vkDestroySemaphore(renderer->dev->dev, semaphore, NULL); + return false; + } + + *sem = (struct wlr_vk_texture_sync_sem) { + .semaphore = semaphore, + .release_point = UINT64_MAX, + }; + *index = count; + return true; +} + +static void release_claimed_texture_sync_sems(struct wlr_vk_renderer *renderer, + struct wlr_vk_texture_sync_wait *waits, size_t count) { + for (size_t i = 0; i < count; i++) { + if (waits[i].semaphore_index == SIZE_MAX) { + continue; + } + texture_sync_sem_at(renderer, waits[i].semaphore_index)->release_point = 0; + waits[i].semaphore_index = SIZE_MAX; + } +} + +// A failed bridge submit can leave the claimed semaphores holding a consumed +// temporary sync_fd import (or a submitted-wait whose execution never started, +// depending on where the submit failed). Any of those states makes the binary +// semaphore unusable for a later re-import, while its slot would otherwise be +// stuck at release_point=UINT64_MAX forever. Destroy and NULL the semaphores; +// acquire_texture_sync_sem() rebuilds reclaimed slots. All these semaphores +// have no pending wait (the submit that would wait on them failed before +// executing), so destruction is valid. +static void discard_texture_sync_sems(struct wlr_vk_renderer *renderer, + struct wlr_vk_texture_sync_wait *waits, size_t count) { + for (size_t i = 0; i < count; i++) { + if (waits[i].semaphore_index == SIZE_MAX) { + continue; + } + struct wlr_vk_texture_sync_sem *sem = + texture_sync_sem_at(renderer, waits[i].semaphore_index); + if (sem->semaphore != VK_NULL_HANDLE) { + vkDestroySemaphore(renderer->dev->dev, sem->semaphore, NULL); + sem->semaphore = VK_NULL_HANDLE; + } + sem->release_point = 0; + waits[i].semaphore_index = SIZE_MAX; + } +} + +static bool submit_texture_sync_bridge(struct wlr_vk_renderer *renderer, + struct wlr_vk_texture_sync_wait *waits, + VkSemaphoreSubmitInfoKHR *wait_infos, size_t wait_count) { + if (wait_count == 0 && !renderer->stage_async_needs_bridge) { + return true; + } + if (renderer->dev->api.vkQueueSubmit2KHR == NULL) { + wlr_log(WLR_ERROR, "Vulkan texture sync bridge requires vkQueueSubmit2KHR"); + return false; + } + if (wait_count > UINT32_MAX) { + wlr_log(WLR_ERROR, "Too many Vulkan texture sync waits in one batch: %zu", + wait_count); + return false; + } + if (wait_count > 0 && renderer->texture_sync_timeline_point == UINT64_MAX) { + wlr_log(WLR_ERROR, "Vulkan texture sync timeline value exhausted"); + return false; + } + + struct wlr_vk_command_buffer *cb = vulkan_acquire_command_buffer(renderer); + if (cb == NULL) { + return false; + } + + VkCommandBufferBeginInfo begin_info = { + .sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO, + }; + VkResult res = vkBeginCommandBuffer(cb->vk, &begin_info); + if (res != VK_SUCCESS) { + wlr_vk_error("vkBeginCommandBuffer", res); + vulkan_reset_command_buffer(cb); + return false; + } + + // Submission order alone is not an execution dependency. Chain the waits + // (and any earlier asynchronous stage upload) through a real pipeline + // barrier whose second scope includes the later Qt/QRhi submission. + VkMemoryBarrier memory_barrier = { + .sType = VK_STRUCTURE_TYPE_MEMORY_BARRIER, + .srcAccessMask = VK_ACCESS_MEMORY_WRITE_BIT, + .dstAccessMask = VK_ACCESS_MEMORY_READ_BIT | + VK_ACCESS_MEMORY_WRITE_BIT, + }; + vkCmdPipelineBarrier(cb->vk, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, + VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, 0, + 1, &memory_barrier, 0, NULL, 0, NULL); + + uint64_t renderer_point = vulkan_end_command_buffer(cb, renderer); + if (renderer_point == 0) { + vulkan_reset_command_buffer(cb); + return false; + } + + VkCommandBufferSubmitInfoKHR cb_info = { + .sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_SUBMIT_INFO_KHR, + .commandBuffer = cb->vk, + }; + VkSemaphoreSubmitInfoKHR signal_infos[2] = {{ + .sType = VK_STRUCTURE_TYPE_SEMAPHORE_SUBMIT_INFO_KHR, + .semaphore = renderer->timeline_semaphore, + .value = renderer_point, + .stageMask = VK_PIPELINE_STAGE_2_ALL_COMMANDS_BIT, + }}; + uint32_t signal_count = 1; + uint64_t texture_sync_point = 0; + if (wait_count > 0) { + texture_sync_point = renderer->texture_sync_timeline_point + 1; + signal_infos[signal_count++] = (VkSemaphoreSubmitInfoKHR) { + .sType = VK_STRUCTURE_TYPE_SEMAPHORE_SUBMIT_INFO_KHR, + .semaphore = renderer->texture_sync_timeline_semaphore, + .value = texture_sync_point, + .stageMask = VK_PIPELINE_STAGE_2_ALL_COMMANDS_BIT, + }; + } + + VkSubmitInfo2KHR submit = { + .sType = VK_STRUCTURE_TYPE_SUBMIT_INFO_2_KHR, + .waitSemaphoreInfoCount = (uint32_t)wait_count, + .pWaitSemaphoreInfos = wait_infos, + .commandBufferInfoCount = 1, + .pCommandBufferInfos = &cb_info, + .signalSemaphoreInfoCount = signal_count, + .pSignalSemaphoreInfos = signal_infos, + }; + res = renderer->dev->api.vkQueueSubmit2KHR(renderer->dev->queue, + 1, &submit, VK_NULL_HANDLE); + if (res != VK_SUCCESS) { + wlr_vk_error("vkQueueSubmit2KHR", res); + cb->timeline_point = 0; + vulkan_reset_command_buffer(cb); + return false; + } + + renderer->stage_async_needs_bridge = false; + if (wait_count > 0) { + renderer->texture_sync_timeline_point = texture_sync_point; + } + for (size_t i = 0; i < wait_count; i++) { + texture_sync_sem_at(renderer, waits[i].semaphore_index)->release_point = + texture_sync_point; + } + + wlr_log(WLR_DEBUG, "Vulkan texture sync: submitted GPU bridge for %zu " + "DMA-BUF sync_file(s), renderer timeline point %" PRIu64, + wait_count, renderer_point); + return true; +} + +static bool wait_texture_sync_entries_and_bridge( + struct wlr_vk_renderer *renderer, + struct wlr_vk_texture_sync_wait *waits, size_t wait_count) { + if (!wait_texture_sync_entries(waits, wait_count)) { + return false; + } + + // A CPU wait resolves the foreign sync_file dependency directly, but an + // earlier asynchronous staging upload still needs a same-queue barrier + // before Qt submits the sampling command buffer. + return submit_texture_sync_bridge(renderer, NULL, NULL, 0); +} + +bool waylib_vk_renderer_flush_texture_sync_batch(struct wlr_renderer *wlr_renderer) { + if (wlr_renderer == NULL || !wlr_renderer_is_vk(wlr_renderer)) { + return false; + } + + struct wlr_vk_renderer *renderer = vulkan_get_renderer(wlr_renderer); + if (!renderer->texture_sync_batch_active) { + // Staging uploads can outlive an aborted or disabled foreign-texture + // batch. Preserve their dependency before the next Qt submission. + return submit_texture_sync_bridge(renderer, NULL, NULL, 0); + } + + struct wlr_vk_texture_sync_wait *waits = renderer->texture_sync_pending.data; + size_t wait_count = renderer->texture_sync_pending.size / sizeof(*waits); + size_t pending_count = 0; + for (size_t i = 0; i < wait_count; i++) { + int ret = poll_sync_file(waits[i].fd, 0); + if (ret < 0) { + waylib_vk_renderer_abort_texture_sync_batch(wlr_renderer); + return false; + } + if (ret > 0) { + close(waits[i].fd); + waits[i].fd = -1; + continue; + } + + if (pending_count != i) { + waits[pending_count] = waits[i]; + waits[i].fd = -1; + } + pending_count++; + } + renderer->texture_sync_pending.size = pending_count * sizeof(*waits); + wait_count = pending_count; + + if (wait_count == 0) { + bool ok = submit_texture_sync_bridge(renderer, NULL, NULL, 0); + waylib_vk_renderer_abort_texture_sync_batch(wlr_renderer); + return ok; + } + if (wait_count > UINT32_MAX) { + bool ok = wait_texture_sync_entries_and_bridge( + renderer, waits, wait_count); + waylib_vk_renderer_abort_texture_sync_batch(wlr_renderer); + return ok; + } + + bool can_gpu_wait = renderer->texture_sync_timeline_semaphore != VK_NULL_HANDLE + && renderer->dev->api.vkImportSemaphoreFdKHR != NULL + && renderer->dev->api.vkQueueSubmit2KHR != NULL + && renderer->dev->api.vkGetSemaphoreCounterValueKHR != NULL + && !renderer->texture_sync_force_poll; + if (!can_gpu_wait) { + bool ok = wait_texture_sync_entries_and_bridge( + renderer, waits, wait_count); + waylib_vk_renderer_abort_texture_sync_batch(wlr_renderer); + return ok; + } + + uint64_t cur_point = 0; + VkResult res = renderer->dev->api.vkGetSemaphoreCounterValueKHR( + renderer->dev->dev, renderer->texture_sync_timeline_semaphore, &cur_point); + if (res != VK_SUCCESS) { + wlr_vk_error("vkGetSemaphoreCounterValueKHR", res); + bool ok = wait_texture_sync_entries_and_bridge( + renderer, waits, wait_count); + waylib_vk_renderer_abort_texture_sync_batch(wlr_renderer); + return ok; + } + + renderer->texture_sync_wait_infos.size = 0; + VkSemaphoreSubmitInfoKHR *wait_infos = wl_array_add( + &renderer->texture_sync_wait_infos, wait_count * sizeof(*wait_infos)); + if (wait_infos == NULL) { + bool ok = wait_texture_sync_entries_and_bridge( + renderer, waits, wait_count); + waylib_vk_renderer_abort_texture_sync_batch(wlr_renderer); + return ok; + } + + size_t claimed_count = 0; + for (; claimed_count < wait_count; claimed_count++) { + if (!acquire_texture_sync_sem(renderer, cur_point, + &waits[claimed_count].semaphore_index)) { + release_claimed_texture_sync_sems(renderer, waits, claimed_count); + bool ok = wait_texture_sync_entries_and_bridge( + renderer, waits, wait_count); + waylib_vk_renderer_abort_texture_sync_batch(wlr_renderer); + return ok; + } + } + + size_t imported_count = 0; + for (; imported_count < wait_count; imported_count++) { + struct wlr_vk_texture_sync_sem *sem = texture_sync_sem_at(renderer, + waits[imported_count].semaphore_index); + VkImportSemaphoreFdInfoKHR import_info = { + .sType = VK_STRUCTURE_TYPE_IMPORT_SEMAPHORE_FD_INFO_KHR, + .handleType = VK_EXTERNAL_SEMAPHORE_HANDLE_TYPE_SYNC_FD_BIT, + .flags = VK_SEMAPHORE_IMPORT_TEMPORARY_BIT, + .semaphore = sem->semaphore, + .fd = waits[imported_count].fd, + }; + res = renderer->dev->api.vkImportSemaphoreFdKHR(renderer->dev->dev, + &import_info); + if (res != VK_SUCCESS) { + wlr_vk_error("vkImportSemaphoreFdKHR", res); + break; + } + + waits[imported_count].fd = -1; // consumed by successful import + wait_infos[imported_count] = (VkSemaphoreSubmitInfoKHR) { + .sType = VK_STRUCTURE_TYPE_SEMAPHORE_SUBMIT_INFO_KHR, + .semaphore = sem->semaphore, + .stageMask = VK_PIPELINE_STAGE_2_ALL_COMMANDS_BIT, + }; + } + + if (imported_count < wait_count) { + release_claimed_texture_sync_sems(renderer, + &waits[imported_count], wait_count - imported_count); + bool gpu_ok = submit_texture_sync_bridge(renderer, waits, wait_infos, + imported_count); + if (!gpu_ok) { + discard_texture_sync_sems(renderer, waits, imported_count); + } + bool cpu_ok = wait_texture_sync_entries(&waits[imported_count], + wait_count - imported_count); + waylib_vk_renderer_abort_texture_sync_batch(wlr_renderer); + return gpu_ok && cpu_ok; + } + + bool ok = submit_texture_sync_bridge(renderer, waits, wait_infos, wait_count); + if (!ok) { + discard_texture_sync_sems(renderer, waits, wait_count); + } + waylib_vk_renderer_abort_texture_sync_batch(wlr_renderer); + return ok; +} + +static void fill_sampling_attribs(struct wlr_vk_texture *texture, + struct wlr_vk_image_attribs *attribs) { + if (attribs == NULL) { + return; + } + + attribs->image = texture->image; + attribs->format = texture->format->vk; + attribs->layout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; + attribs->usage = texture->dmabuf_imported ? + vulkan_dma_tex_usage : vulkan_shm_tex_usage; +} + +// Returns true when the texture's DMA-BUF was (or may have been) rendered as +// a color attachment by one of this renderer's recent command buffers. The +// producer accesses went through the render buffer's separate logical VkImage, +// which only aliases this texture's image in physical memory. +static bool is_frame_render_buffer(struct wlr_vk_renderer *renderer, + struct wlr_buffer *buffer) { + return vulkan_alias_list_contains(&renderer->frame_render_buffers, buffer); +} + +// Extends the color-attachment writes of an aliasing render-buffer producer +// to the sampling about to be recorded for this texture. Neither the foreign +// ownership acquire (its source access mask is ignored) nor an image barrier +// naming the sampled texture can cover accesses made through the producer's +// different logical image: §Memory Aliasing requires a memory dependency and +// only a global (resource-free) barrier spans both aliases. The mask pair is +// the same shape the texture-sync bridge uses for staged uploads. +static void record_frame_render_barrier(struct wlr_vk_renderer *renderer, + struct wlr_vk_texture *texture, VkCommandBuffer cb) { + if (texture->buffer == NULL + || !is_frame_render_buffer(renderer, texture->buffer)) { + return; + } + + bool deferred = false; + if (renderer->texture_barrier_batch_active + && !renderer->texture_barrier_batch_release) { + renderer->frame_producer_alias_dep = true; + deferred = true; + } + if (!deferred) { + VkMemoryBarrier memory_barrier = { + .sType = VK_STRUCTURE_TYPE_MEMORY_BARRIER, + .srcAccessMask = VK_ACCESS_MEMORY_WRITE_BIT, + .dstAccessMask = VK_ACCESS_MEMORY_READ_BIT, + }; + vkCmdPipelineBarrier(cb, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, + VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, 0, + 1, &memory_barrier, 0, NULL, 0, NULL); + } + + static bool logged_frame_producer_barrier; + if (!logged_frame_producer_barrier) { + logged_frame_producer_barrier = true; + wlr_log(WLR_DEBUG, "Vulkan texture sampling: recording a global " + "alias dependency for a texture whose DMA-BUF was also written " + "as a color attachment through another logical image"); + } +} + +bool waylib_vk_renderer_prepare_texture_for_sampling(struct wlr_renderer *wlr_renderer, + struct wlr_texture *wlr_texture, VkCommandBuffer cb, + struct wlr_vk_image_attribs *attribs) { + if (wlr_renderer == NULL || wlr_texture == NULL || cb == VK_NULL_HANDLE) { + return false; + } + if (!wlr_renderer_is_vk(wlr_renderer) || !wlr_texture_is_vk(wlr_texture)) { + return false; + } + + struct wlr_vk_renderer *renderer = vulkan_get_renderer(wlr_renderer); + struct wlr_vk_texture *texture = vulkan_get_texture(wlr_texture); + if (texture->renderer != renderer) { + wlr_log(WLR_ERROR, "Vulkan texture belongs to a different renderer"); + return false; + } + + if (!submit_pending_stage_uploads(renderer)) { + return false; + } + if (texture->dmabuf_imported && !texture->qt_sampling_acquired) { + if (texture->readback_acquired) { + // A readback acquire holds the foreign->own transition of this + // image. Qt must not sample it until that readback released it; + // by construction readbacks run outside Qt frames, so failing + // closed here only guards against future caller bugs. + wlr_log(WLR_ERROR, "Vulkan texture readback acquire still in " + "flight; refusing to sample it"); + return false; + } + + int sync_file_fds[WLR_DMABUF_MAX_PLANES]; + for (size_t i = 0; i < WLR_DMABUF_MAX_PLANES; i++) { + sync_file_fds[i] = -1; + } + + if (texture->buffer != NULL) { + if (!vulkan_sync_foreign_texture_acquire(texture, sync_file_fds)) { + close_sync_file_fds(sync_file_fds); + wlr_log(WLR_ERROR, "Failed to wait for foreign texture DMA-BUF fence"); + return false; + } + + static bool logged_foreign_sync_method; + if (!logged_foreign_sync_method) { + logged_foreign_sync_method = true; + wlr_log(WLR_DEBUG, "Vulkan foreign texture: handling DMA-BUF " + "sync_file via %s", renderer->texture_sync_batch_active + ? "deferred frame batch" : "immediate CPU poll"); + } + + bool waited = renderer->texture_sync_batch_active + ? collect_sync_file_fds(renderer, sync_file_fds) + : wait_sync_file_fds(sync_file_fds); + if (!waited) { + return false; + } + } + + // Record the sampling side of the alias pair before touching any + // barrier state: a later render-buffer acquire for the same + // wlr_buffer writes through another logical image and needs a WAR + // dependency against this sampling. Failing to track is handled + // fail-closed so the WAR barrier can never be silently lost. + if (texture->buffer != NULL + && !vulkan_alias_list_add(&renderer->frame_sampled_buffers, + texture->buffer)) { + wlr_log_errno(WLR_ERROR, + "Failed to track Vulkan sampled buffer for aliasing"); + return false; + } + + VkImageMemoryBarrier barrier = { + .sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER, + .srcQueueFamilyIndex = VK_QUEUE_FAMILY_FOREIGN_EXT, + .dstQueueFamilyIndex = renderer->dev->queue_family, + .image = texture->image, + .oldLayout = VK_IMAGE_LAYOUT_GENERAL, + .newLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, + .srcAccessMask = 0, + .dstAccessMask = VK_ACCESS_SHADER_READ_BIT, + .subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT, + .subresourceRange.layerCount = 1, + .subresourceRange.levelCount = 1, + }; + + bool deferred = false; + if (renderer->texture_barrier_batch_active + && !renderer->texture_barrier_batch_release) { + VkImageMemoryBarrier *slot = wl_array_add( + &renderer->texture_acquire_barriers, sizeof(barrier)); + struct wlr_vk_texture **track = slot != NULL ? wl_array_add( + &renderer->texture_acquire_textures, sizeof(*track)) : NULL; + if (slot != NULL && track != NULL) { + *slot = barrier; + *track = texture; + deferred = true; + } else { + // Never leave a deferred barrier without its texture + // bookkeeping: undo the partial adds and record it now. + if (track != NULL) { + renderer->texture_acquire_textures.size -= sizeof(*track); + } + if (slot != NULL) { + renderer->texture_acquire_barriers.size -= sizeof(barrier); + } + wlr_log_errno(WLR_ERROR, + "Failed to queue Vulkan texture acquire barrier"); + } + } + if (!deferred) { + // The acquire's second scope must cover every stage the consumer + // may sample from, not only the fragment stage. + vkCmdPipelineBarrier(cb, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, + VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, 0, + 0, NULL, 0, NULL, 1, &barrier); + } + + record_frame_render_barrier(renderer, texture, cb); + + texture->qt_sampling_acquired = true; + } + + texture->transitioned = true; + fill_sampling_attribs(texture, attribs); + return true; +} + +bool waylib_vk_renderer_finish_texture_sampling(struct wlr_renderer *wlr_renderer, + struct wlr_texture *wlr_texture, VkCommandBuffer cb) { + if (wlr_renderer == NULL || wlr_texture == NULL || cb == VK_NULL_HANDLE) { + return false; + } + if (!wlr_renderer_is_vk(wlr_renderer) || !wlr_texture_is_vk(wlr_texture)) { + return false; + } + + struct wlr_vk_renderer *renderer = vulkan_get_renderer(wlr_renderer); + struct wlr_vk_texture *texture = vulkan_get_texture(wlr_texture); + if (texture->renderer != renderer) { + wlr_log(WLR_ERROR, "Vulkan texture belongs to a different renderer"); + return false; + } + + if (!texture->dmabuf_imported || !texture->qt_sampling_acquired) { + return true; + } + + VkImageMemoryBarrier barrier = { + .sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER, + .srcQueueFamilyIndex = renderer->dev->queue_family, + .dstQueueFamilyIndex = VK_QUEUE_FAMILY_FOREIGN_EXT, + .image = texture->image, + .oldLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, + .newLayout = VK_IMAGE_LAYOUT_GENERAL, + .srcAccessMask = VK_ACCESS_SHADER_READ_BIT, + .dstAccessMask = 0, + .subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT, + .subresourceRange.layerCount = 1, + .subresourceRange.levelCount = 1, + }; + + bool deferred = false; + if (renderer->texture_barrier_batch_active + && renderer->texture_barrier_batch_release) { + VkImageMemoryBarrier *slot = wl_array_add( + &renderer->texture_release_barriers, sizeof(barrier)); + struct wlr_vk_texture **track = slot != NULL ? wl_array_add( + &renderer->texture_release_textures, sizeof(*track)) : NULL; + if (slot != NULL && track != NULL) { + *slot = barrier; + *track = texture; + deferred = true; + } else { + // Never leave a deferred barrier without its texture + // bookkeeping: undo the partial adds and record it now. + if (track != NULL) { + renderer->texture_release_textures.size -= sizeof(*track); + } + if (slot != NULL) { + renderer->texture_release_barriers.size -= sizeof(barrier); + } + wlr_log_errno(WLR_ERROR, + "Failed to queue Vulkan texture release barrier"); + } + } + if (!deferred) { + // The release's first scope must cover every stage that may still be + // reading the texture, not only the fragment stage. + vkCmdPipelineBarrier(cb, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, + VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT, 0, + 0, NULL, 0, NULL, 1, &barrier); + } + texture->qt_sampling_acquired = false; + return true; +} + +// A deferred acquire/release barrier that is never recorded must not leave the +// texture's ownership bookkeeping claiming a transfer that did not happen. A +// dropped acquire leaves the image FOREIGN-owned (it must be re-acquired on +// the next use), while a dropped release leaves the graphics queue owning it +// (it must not be acquired again). Roll the per-texture flag back to match the +// image's real ownership before discarding the batch. +static void rollback_deferred_texture_barriers(struct wlr_vk_renderer *renderer) { + const size_t acquire_count = renderer->texture_acquire_textures.size / + sizeof(struct wlr_vk_texture *); + const size_t release_count = renderer->texture_release_textures.size / + sizeof(struct wlr_vk_texture *); + + struct wlr_vk_texture **texture; + wl_array_for_each(texture, &renderer->texture_acquire_textures) { + (*texture)->qt_sampling_acquired = false; + } + wl_array_for_each(texture, &renderer->texture_release_textures) { + (*texture)->qt_sampling_acquired = true; + } + + renderer->texture_acquire_textures.size = 0; + renderer->texture_release_textures.size = 0; + + if (acquire_count > 0 || release_count > 0) { + wlr_log(WLR_DEBUG, "Vulkan texture barrier batch rolled back: " + "%zu dropped acquire(s), %zu dropped release(s)", + acquire_count, release_count); + } +} + +bool waylib_vk_renderer_begin_texture_barrier_batch(struct wlr_renderer *wlr_renderer, + bool release) { + if (wlr_renderer == NULL || !wlr_renderer_is_vk(wlr_renderer)) { + return false; + } + + struct wlr_vk_renderer *renderer = vulkan_get_renderer(wlr_renderer); + if (renderer->texture_barrier_batch_active) { + wlr_log(WLR_ERROR, "Vulkan texture barrier batch is already active"); + waylib_vk_renderer_abort_texture_barrier_batch(wlr_renderer); + } + + renderer->texture_barrier_batch_active = true; + renderer->texture_barrier_batch_release = release; + return true; +} + +bool waylib_vk_renderer_flush_texture_barrier_batch(struct wlr_renderer *wlr_renderer, + VkCommandBuffer cb) { + if (wlr_renderer == NULL || !wlr_renderer_is_vk(wlr_renderer)) { + return false; + } + + struct wlr_vk_renderer *renderer = vulkan_get_renderer(wlr_renderer); + if (!renderer->texture_barrier_batch_active) { + return true; + } + + // Test-only fault injection (one-shot): WAYLIB_VK_TEST_FLUSH_FAIL_AT= + // makes the n-th barrier batch flush of this process behave as if no + // command buffer was available, so the rollback paths can be exercised on + // a live compositor. Unset in normal operation; drop before upstreaming. + { + static long inject_fail_at = -1; + if (inject_fail_at < 0) { + const char *env = getenv("WAYLIB_VK_TEST_FLUSH_FAIL_AT"); + inject_fail_at = env != NULL ? strtol(env, NULL, 10) : 0; + if (inject_fail_at < 0) { + inject_fail_at = 0; + } + } + if (inject_fail_at > 0) { + static long flush_count; + if (++flush_count == inject_fail_at) { + wlr_log(WLR_INFO, "WAYLIB_VK_TEST_FLUSH_FAIL_AT: injecting a " + "one-shot Vulkan texture barrier batch flush failure"); + cb = VK_NULL_HANDLE; + } + } + } + + const bool release = renderer->texture_barrier_batch_release; + struct wl_array *barriers = release + ? &renderer->texture_release_barriers + : &renderer->texture_acquire_barriers; + const size_t count = barriers->size / sizeof(VkImageMemoryBarrier); + + if (cb == VK_NULL_HANDLE && (count > 0 || renderer->frame_producer_alias_dep)) { + // Without a command buffer nothing can be recorded; discarding the + // batch silently would desynchronize qt_sampling_acquired from the + // image's real ownership. Roll the batch back and report the failure. + wlr_log(WLR_ERROR, "Cannot record the pending Vulkan texture %s " + "barrier batch without a command buffer; rolling it back", + release ? "release" : "acquire"); + rollback_deferred_texture_barriers(renderer); + renderer->texture_acquire_barriers.size = 0; + renderer->texture_release_barriers.size = 0; + renderer->frame_producer_alias_dep = false; + renderer->texture_barrier_batch_active = false; + renderer->texture_barrier_batch_release = false; + return false; + } + + if (cb != VK_NULL_HANDLE && count > 0) { + VkPipelineStageFlags src_stage = release + ? VK_PIPELINE_STAGE_ALL_COMMANDS_BIT + : VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT; + VkPipelineStageFlags dst_stage = release + ? VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT + : VK_PIPELINE_STAGE_ALL_COMMANDS_BIT; + vkCmdPipelineBarrier(cb, src_stage, dst_stage, 0, + 0, NULL, 0, NULL, (uint32_t)count, barriers->data); + } + + // Same-frame alias producer dependencies are only queued during the + // acquire (pre-draw) phase and must be recorded after the + // ownership-transfer acquire barriers above. A global memory barrier is + // the only form whose access scope spans the producer's separate logical + // image and this sampling (§Memory Aliasing). + if (cb != VK_NULL_HANDLE && !release && renderer->frame_producer_alias_dep) { + VkMemoryBarrier memory_barrier = { + .sType = VK_STRUCTURE_TYPE_MEMORY_BARRIER, + .srcAccessMask = VK_ACCESS_MEMORY_WRITE_BIT, + .dstAccessMask = VK_ACCESS_MEMORY_READ_BIT, + }; + vkCmdPipelineBarrier(cb, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, + VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, 0, + 1, &memory_barrier, 0, NULL, 0, NULL); + } + + renderer->texture_acquire_barriers.size = 0; + renderer->texture_release_barriers.size = 0; + renderer->texture_acquire_textures.size = 0; + renderer->texture_release_textures.size = 0; + renderer->frame_producer_alias_dep = false; + renderer->texture_barrier_batch_active = false; + renderer->texture_barrier_batch_release = false; + return true; +} + +void waylib_vk_renderer_abort_texture_barrier_batch(struct wlr_renderer *wlr_renderer) { + if (wlr_renderer == NULL || !wlr_renderer_is_vk(wlr_renderer)) { + return; + } + + struct wlr_vk_renderer *renderer = vulkan_get_renderer(wlr_renderer); + rollback_deferred_texture_barriers(renderer); + renderer->texture_acquire_barriers.size = 0; + renderer->texture_release_barriers.size = 0; + renderer->frame_producer_alias_dep = false; + renderer->texture_barrier_batch_active = false; + renderer->texture_barrier_batch_release = false; +} + +// Submit a FOREIGN<->own queue-family ownership barrier (layout untouched, +// GENERAL, as every DMA-BUF import path in this renderer keeps it) on its own +// command buffer. acquire=true transfers ownership from the foreign producer +// to the graphics queue, permitting transfer-read and shader-read of the +// imported memory (which starts in an undefined layout from Vulkan's view); +// acquire=false releases it back. The later read_pixels() staging command +// buffer is submitted afterwards on the same queue: same-queue submission +// order provides the execution dependency between these two batches. +static bool submit_readback_barrier(struct wlr_vk_renderer *renderer, + VkImage image, bool acquire) { + VkResult res; + struct wlr_vk_command_buffer *cb = vulkan_acquire_command_buffer(renderer); + if (cb == NULL) { + return false; + } + + VkCommandBufferBeginInfo begin_info = { + .sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO, + }; + res = vkBeginCommandBuffer(cb->vk, &begin_info); + if (res != VK_SUCCESS) { + wlr_vk_error("vkBeginCommandBuffer", res); + vulkan_reset_command_buffer(cb); + return false; + } + + const VkAccessFlags read_accesses = VK_ACCESS_TRANSFER_READ_BIT + | VK_ACCESS_SHADER_READ_BIT; + VkImageMemoryBarrier barrier = { + .sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER, + .image = image, + .oldLayout = VK_IMAGE_LAYOUT_GENERAL, + .newLayout = VK_IMAGE_LAYOUT_GENERAL, + .subresourceRange = { + .aspectMask = VK_IMAGE_ASPECT_COLOR_BIT, + .baseMipLevel = 0, + .levelCount = 1, + .baseArrayLayer = 0, + .layerCount = 1, + }, + }; + VkPipelineStageFlags src_stage, dst_stage; + if (acquire) { + barrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_FOREIGN_EXT; + barrier.dstQueueFamilyIndex = renderer->dev->queue_family; + barrier.srcAccessMask = 0; + barrier.dstAccessMask = read_accesses; + src_stage = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT; + dst_stage = VK_PIPELINE_STAGE_TRANSFER_BIT + | VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT; + } else { + barrier.srcQueueFamilyIndex = renderer->dev->queue_family; + barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_FOREIGN_EXT; + barrier.srcAccessMask = read_accesses; + barrier.dstAccessMask = 0; + src_stage = VK_PIPELINE_STAGE_TRANSFER_BIT + | VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT; + dst_stage = VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT; + } + + vkCmdPipelineBarrier(cb->vk, src_stage, dst_stage, 0, 0, NULL, 0, NULL, + 1, &barrier); + + uint64_t timeline_point = vulkan_end_command_buffer(cb, renderer); + if (timeline_point == 0) { + vulkan_reset_command_buffer(cb); + return false; + } + + VkTimelineSemaphoreSubmitInfoKHR timeline_submit_info = { + .sType = VK_STRUCTURE_TYPE_TIMELINE_SEMAPHORE_SUBMIT_INFO_KHR, + .signalSemaphoreValueCount = 1, + .pSignalSemaphoreValues = &timeline_point, + }; + VkSubmitInfo submit_info = { + .sType = VK_STRUCTURE_TYPE_SUBMIT_INFO, + .pNext = &timeline_submit_info, + .commandBufferCount = 1, + .pCommandBuffers = &cb->vk, + .signalSemaphoreCount = 1, + .pSignalSemaphores = &renderer->timeline_semaphore, + }; + res = vkQueueSubmit(renderer->dev->queue, 1, &submit_info, VK_NULL_HANDLE); + if (res != VK_SUCCESS) { + wlr_vk_error("vkQueueSubmit", res); + cb->timeline_point = 0; + vulkan_reset_command_buffer(cb); + return false; + } + + wlr_log(WLR_DEBUG, "Vulkan readback: submitted %s command buffer " + "timeline_point=%"PRIu64, acquire ? "acquire" : "release", + timeline_point); + return true; +} + +enum wlr_vk_texture_readback_state waylib_vk_texture_begin_readback( + struct wlr_renderer *wlr_renderer, struct wlr_texture *wlr_texture) { + if (wlr_renderer == NULL || !wlr_renderer_is_vk(wlr_renderer) + || wlr_texture == NULL || !wlr_texture_is_vk(wlr_texture)) { + return WLR_VK_TEXTURE_READBACK_ERROR; + } + + struct wlr_vk_renderer *renderer = vulkan_get_renderer(wlr_renderer); + struct wlr_vk_texture *texture = vulkan_get_texture(wlr_texture); + if (texture->renderer != renderer) { + wlr_log(WLR_ERROR, "Vulkan texture belongs to a different renderer"); + return WLR_VK_TEXTURE_READBACK_ERROR; + } + + if (!texture->dmabuf_imported) { + // Pixel (SHM upload) texture: CPU-written through our own staging + // uploads, TRANSFER_SRC usage present, no foreign ownership cycle. + return WLR_VK_TEXTURE_READBACK_NOOP; + } + + // The readback cycle runs outside Qt frame recording and must be the sole + // holder of the image's foreign ownership while it does. + if (renderer->texture_sync_batch_active + || renderer->texture_barrier_batch_active + || texture->qt_sampling_acquired || texture->readback_acquired + || texture->owned) { + wlr_log(WLR_ERROR, "Vulkan texture readback requested while its " + "foreign ownership is taken or a frame is recording"); + return WLR_VK_TEXTURE_READBACK_ERROR; + } + + // The barrier below covers a single color aspect of one memory plane. + if (texture->mem_count > 1 || vulkan_format_is_ycbcr(texture->format)) { + wlr_log(WLR_ERROR, "Vulkan readback of multi-plane/YCbCr DMA-BUF " + "textures is not supported"); + return WLR_VK_TEXTURE_READBACK_ERROR; + } + + if (!submit_pending_stage_uploads(renderer)) { + wlr_log(WLR_ERROR, "Failed to submit pending Vulkan staging before " + "texture readback"); + return WLR_VK_TEXTURE_READBACK_ERROR; + } + + if (texture->buffer != NULL) { + int sync_file_fds[WLR_DMABUF_MAX_PLANES]; + for (size_t i = 0; i < WLR_DMABUF_MAX_PLANES; i++) { + sync_file_fds[i] = -1; + } + if (!vulkan_sync_foreign_texture_acquire(texture, sync_file_fds)) { + close_sync_file_fds(sync_file_fds); + wlr_log(WLR_ERROR, "Failed to wait for foreign texture DMA-BUF " + "fence before readback"); + return WLR_VK_TEXTURE_READBACK_ERROR; + } + if (!wait_sync_file_fds(sync_file_fds)) { + wlr_log(WLR_ERROR, "Failed to wait for foreign texture DMA-BUF " + "sync_file before readback"); + return WLR_VK_TEXTURE_READBACK_ERROR; + } + } + + if (!submit_readback_barrier(renderer, texture->image, true)) { + wlr_log(WLR_ERROR, "Failed to submit Vulkan texture readback acquire"); + return WLR_VK_TEXTURE_READBACK_ERROR; + } + + texture->readback_acquired = true; + return WLR_VK_TEXTURE_READBACK_ACQUIRED; +} + +void waylib_vk_texture_end_readback(struct wlr_renderer *wlr_renderer, + struct wlr_texture *wlr_texture) { + if (wlr_renderer == NULL || !wlr_renderer_is_vk(wlr_renderer) + || wlr_texture == NULL || !wlr_texture_is_vk(wlr_texture)) { + return; + } + + struct wlr_vk_renderer *renderer = vulkan_get_renderer(wlr_renderer); + struct wlr_vk_texture *texture = vulkan_get_texture(wlr_texture); + if (texture->renderer != renderer || !texture->readback_acquired) { + return; + } + + if (!submit_readback_barrier(renderer, texture->image, false)) { + wlr_log(WLR_ERROR, "Failed to submit Vulkan texture readback release; " + "keeping the readback acquire so the texture is never sampled or " + "read again without its matching release"); + return; + } + texture->readback_acquired = false; +} From cfbf6ba99bf1371f175d9ca9b882f3a3b4ceb6a7 Mon Sep 17 00:00:00 2001 From: LFRon Date: Wed, 2 Sep 2026 11:39:44 +0800 Subject: [PATCH 04/15] feat(vulkan): integrate Qt Quick with wlroots render buffers Make Qt Quick (QRhi) render the compositor canvas, layer buffers, and cursor buffers directly into renderer-allocated DMA-BUFs, so every frame is produced in one offscreen QRhi pass and handed to wlroots as a plain wlr_buffer - no intermediate copies, and GLES2/Pixman paths keep their existing behavior. - WRenderHelper: wrap renderer render buffers and wlroots textures into QRhiTexture/QQuickRenderTarget objects with exact-format Vulkan image views (including the opaque alpha swizzle), record acquire/release ownership-transfer barriers around Qt's use of a render buffer, and defer retired render-buffer/swapchain destruction until the frame's GPU work has completed. - WOutputRenderWindow/WBufferRenderer: run outputs and layers inside one synchronous offscreen frame (beginFrameChecked/endFrameChecked); release every render buffer before the frame is submitted and retire replaced swapchains afterwards; expose the last buffer per output viewport for cache sampling. - WOutput::configurePrimarySwapchain()/configureCursorSwapchain(): hand the replaced swapchain out to the caller instead of destroying it immediately, so its buffers can outlive GPU work still referencing them. - WSGTextureProvider/WSurfaceItem: keep a client buffer and its wlroots texture as one ownership tuple, rebuild Qt wrappers when the buffer changes, and defer texture cleanup to render jobs so GPU sampling has finished before destruction. - Harden WOutputHelperPrivate::acquireBuffer(): this legacy path has no Vulkan callers and destroys a replaced swapchain immediately, which can race with GPU work still referencing its buffers; refuse it on the Vulkan renderer instead of introducing an untested retire mechanism. - Add the renderer-backed linux-dmabuf v4 protocol wrapper and the VKTRACE logging helper (WAYLIB_VULKAN_TRACE) covering frames, passes, texture wrapping, sampling dispositions, cleanups, and output present/commit results. Follow-up hardening pass (folded): - Refuse to record wlroots' queue-family ownership barriers inside an active QRhi render pass: VUID-vkCmdPipelineBarrier-oldLayout-01181 / -srcQueueFamilyIndex-01182 forbid in-pass layout transitions and ownership transfers, so a barrier injected between QRhi passes by mistake would be undefined as well as mis-accounted. The render-buffer acquire/release wrappers and texture-sampling prepare now check QVkCommandBuffer::recordingPass (fail-closed, qCCritical + frame-fail fallback like the existing barriers). - Call waylib_vk_renderer_reset_frame_alias_lists() from doRender once the frame's endFrameChecked has waited for the GPU, keeping the wlroots alias tracking per-frame. - Escalate consecutive beginFrameChecked() errors (non-device-lost) to a fatal render error: those frames never reach endFrameChecked(), so retired resource cleanup would freeze while the compositor keeps skipping frames. Follow-up hardening (folded): null m_handle in WLinuxDmabufV1::destroy() so handle()/global() return null instead of a dangling pointer once wlroots has destroyed the global from its display destroy listener. --- waylib/src/server/CMakeLists.txt | 17 + waylib/src/server/kernel/wlr_all.h | 1 + waylib/src/server/kernel/woutput.cpp | 60 +- waylib/src/server/kernel/woutput.h | 6 +- waylib/src/server/kernel/wsurface.cpp | 8 + waylib/src/server/protocols/WLinuxDmabufV1 | 1 + .../src/server/protocols/wlinuxdmabufv1.cpp | 82 + waylib/src/server/protocols/wlinuxdmabufv1.h | 31 + .../qtquick/private/wbufferrenderer.cpp | 183 ++- .../qtquick/private/wbufferrenderer_p.h | 13 +- waylib/src/server/qtquick/woutputhelper.cpp | 13 + .../server/qtquick/woutputrenderwindow.cpp | 601 ++++++- waylib/src/server/qtquick/woutputviewport.cpp | 6 + waylib/src/server/qtquick/woutputviewport.h | 2 + .../src/server/qtquick/wquicktextureproxy.cpp | 15 +- waylib/src/server/qtquick/wrenderhelper.cpp | 987 ++++++++++-- waylib/src/server/qtquick/wrenderhelper.h | 22 +- .../src/server/qtquick/wsgtextureprovider.cpp | 525 +++++- .../src/server/qtquick/wsgtextureprovider.h | 1 + waylib/src/server/qtquick/wsurfaceitem.cpp | 93 +- .../src/server/utils/private/wvulkantrace_p.h | 109 ++ waylib/src/server/utils/wbufferdumper.cpp | 43 +- waylib/src/server/utils/wbufferdumper.h | 1 + waylib/src/server/utils/wvulkantrace.cpp | 1408 +++++++++++++++++ waylib/src/server/wayliblogging.cpp | 1 + waylib/src/server/wayliblogging.h | 1 + 26 files changed, 3890 insertions(+), 340 deletions(-) create mode 100644 waylib/src/server/protocols/WLinuxDmabufV1 create mode 100644 waylib/src/server/protocols/wlinuxdmabufv1.cpp create mode 100644 waylib/src/server/protocols/wlinuxdmabufv1.h create mode 100644 waylib/src/server/utils/private/wvulkantrace_p.h create mode 100644 waylib/src/server/utils/wvulkantrace.cpp diff --git a/waylib/src/server/CMakeLists.txt b/waylib/src/server/CMakeLists.txt index e5d0b24d67..e1dfcbef2e 100644 --- a/waylib/src/server/CMakeLists.txt +++ b/waylib/src/server/CMakeLists.txt @@ -62,6 +62,11 @@ pkg_search_module(PIXMAN REQUIRED IMPORTED_TARGET pixman-1) pkg_search_module(XKBCOMMON REQUIRED IMPORTED_TARGET xkbcommon) pkg_search_module(XCB REQUIRED IMPORTED_TARGET xcb) pkg_search_module(EGL REQUIRED IMPORTED_TARGET egl) +pkg_get_variable(WAYLIB_WLR_HAVE_VULKAN_RENDERER wlroots-0.19 have_vulkan_renderer) + +if(WAYLIB_WLR_HAVE_VULKAN_RENDERER) + find_package(Vulkan REQUIRED) +endif() find_package(TreelandProtocols REQUIRED) @@ -170,6 +175,7 @@ set(SOURCES ${WAYLAND_PROTOCOLS_OUTPUTDIR}/xdg-shell-protocol.c utils/wtools.cpp + utils/wvulkantrace.cpp utils/wthreadutils.cpp utils/wimagebuffer.cpp utils/wcursorimage.cpp @@ -206,6 +212,7 @@ set(SOURCES protocols/wpointerconstraintsv1.cpp protocols/wrelativepointermanagerv1.cpp protocols/woutputmanagerv1.cpp + protocols/wlinuxdmabufv1.cpp protocols/wextforeigntoplevellistv1.cpp protocols/wxdgdialogmanagerv1.cpp protocols/wsecuritycontextmanager.cpp @@ -318,6 +325,8 @@ set(HEADERS protocols/WRelativePointerManagerV1 protocols/woutputmanagerv1.h protocols/WOutputManagerV1 + protocols/wlinuxdmabufv1.h + protocols/WLinuxDmabufV1 protocols/wlayershell.h protocols/WLayerShell protocols/wxwayland.h @@ -369,6 +378,7 @@ set(PRIVATE_HEADERS protocols/private/wtextinputv3_p.h protocols/private/wvirtualkeyboardv1_p.h protocols/private/wsecuritycontextmanager_p.h + utils/private/wvulkantrace_p.h ) if(NOT DISABLE_XWAYLAND) @@ -447,6 +457,13 @@ target_link_libraries(${TARGET} PkgConfig::EGL ) +if(TARGET Vulkan::Vulkan) + target_link_libraries(${TARGET} + PRIVATE + Vulkan::Vulkan + ) +endif() + target_link_libraries(${TARGET} PUBLIC ${WAYLIB_WLROOTS_PUBLIC_TARGET} diff --git a/waylib/src/server/kernel/wlr_all.h b/waylib/src/server/kernel/wlr_all.h index b9b559ef54..6a1b1ab3a1 100644 --- a/waylib/src/server/kernel/wlr_all.h +++ b/waylib/src/server/kernel/wlr_all.h @@ -100,6 +100,7 @@ extern "C" { #include #include #include +#include #include #include #include diff --git a/waylib/src/server/kernel/woutput.cpp b/waylib/src/server/kernel/woutput.cpp index a30dbab689..441530ce1a 100644 --- a/waylib/src/server/kernel/woutput.cpp +++ b/waylib/src/server/kernel/woutput.cpp @@ -11,6 +11,7 @@ #include "wtools.h" #include "platformplugin/qwlrootscreen.h" #include "private/wglobal_p.h" +#include "utils/private/wvulkantrace_p.h" #include "wayliblogging.h" #include @@ -97,6 +98,19 @@ WOutput::WOutput(wlr_output *handle, WBackend *backend) if (event->state->committed & WLR_OUTPUT_STATE_ADAPTIVE_SYNC_ENABLED) Q_EMIT adaptiveSyncEnabledChanged(); }); + if (WVulkanTrace::enabled()) { + listeners()->add(&handle->events.commit, this, + [this] (wlr_output_event_commit *event) { + WVulkanTrace::outputCommitState(this->handle(), event->state->committed); + }); + listeners()->add(&handle->events.present, this, + [this] (wlr_output_event_present *event) { + WVulkanTrace::outputPresented(this->handle(), event); + }); + listeners()->add(&handle->events.destroy, this, [handle] (void *) { + WVulkanTrace::outputDestroyed(handle); + }); + } } WOutput::~WOutput() @@ -310,18 +324,18 @@ static struct wlr_swapchain *create_swapchain(struct wlr_output *output, if (!allow_modifiers && (format->len != 1 || format->modifiers[0] != DRM_FORMAT_MOD_LINEAR)) { if (!wlr_drm_format_has(format.get(), DRM_FORMAT_MOD_INVALID)) { qCWarning(lcWlOutputDrm) << "No support for implicit modifiers"; - return NULL; + return NULL; } format->len = 0; if (!wlr_drm_format_add(format.get(), DRM_FORMAT_MOD_INVALID)) { qCWarning(lcWlOutputDrm) << "Failed to add implicit modifier to DRM format"; - return NULL; + return NULL; } } struct wlr_swapchain *swapchain = wlr_swapchain_create(allocator, width, height, format.get()); - return swapchain; + return swapchain; } static bool test_swapchain(struct wlr_output *output, @@ -341,10 +355,13 @@ static bool test_swapchain(struct wlr_output *output, static bool wlr_output_configure_primary_swapchain(struct wlr_output *output, int width, int height, uint32_t format, struct wlr_swapchain **swapchain_ptr, - bool test) { + bool test, struct wlr_swapchain **replaced_swapchain_ptr) { WOutputStateGuard empty_state; wlr_output_state *state = empty_state.get(); + if (replaced_swapchain_ptr) + *replaced_swapchain_ptr = nullptr; + // Re-use the existing swapchain if possible struct wlr_swapchain *old_swapchain = *swapchain_ptr; if (old_swapchain != NULL && @@ -382,7 +399,11 @@ static bool wlr_output_configure_primary_swapchain(struct wlr_output *output, in } } - wlr_swapchain_destroy(*swapchain_ptr); + if (replaced_swapchain_ptr) { + *replaced_swapchain_ptr = *swapchain_ptr; + } else if (*swapchain_ptr) { + wlr_swapchain_destroy(*swapchain_ptr); + } *swapchain_ptr = swapchain; return true; } @@ -408,23 +429,36 @@ static bool output_pick_cursor_format(struct wlr_output *output, // End bool WOutput::configurePrimarySwapchain(const QSize &size, uint32_t format, - wlr_swapchain **swapchain, bool doTest) + wlr_swapchain **swapchain, bool doTest, + wlr_swapchain **replacedSwapchain) { W_D(WOutput); Q_ASSERT(!size.isEmpty()); + if (replacedSwapchain) + *replacedSwapchain = nullptr; + wlr_swapchain *sc = *swapchain; + wlr_swapchain *replacedSc = nullptr; bool ok = wlr_output_configure_primary_swapchain(d->handle(), size.width(), size.height(), - format, &sc, doTest); + format, &sc, doTest, + replacedSwapchain ? &replacedSc : nullptr); if (!ok) return false; *swapchain = sc; + if (replacedSwapchain) + *replacedSwapchain = replacedSc; return true; } -bool WOutput::configureCursorSwapchain(const QSize &size, uint32_t drmFormat, wlr_swapchain **swapchain) +bool WOutput::configureCursorSwapchain(const QSize &size, uint32_t drmFormat, + wlr_swapchain **swapchain, + wlr_swapchain **replacedSwapchain) { W_D(WOutput); Q_ASSERT(!size.isEmpty()); + if (replacedSwapchain) + *replacedSwapchain = nullptr; + auto sc = *swapchain; if (!sc || sc->width != size.width() || sc->height != size.height()) { WDrmFormat format; @@ -433,12 +467,18 @@ bool WOutput::configureCursorSwapchain(const QSize &size, uint32_t drmFormat, wl return false; } - wlr_swapchain_destroy(sc); + auto oldSc = sc; sc = wlr_swapchain_create(allocator(), size.width(), size.height(), format.get()); - if (!sc) { + if (!sc) { qCDebug(lcWlOutputBuffer) << "Failed to create cursor swapchain with selected format"; return false; } + + if (replacedSwapchain) { + *replacedSwapchain = oldSc; + } else if (oldSc) { + wlr_swapchain_destroy(oldSc); + } } *swapchain = sc; diff --git a/waylib/src/server/kernel/woutput.h b/waylib/src/server/kernel/woutput.h index 2bdfef112f..b39e3aa8e6 100644 --- a/waylib/src/server/kernel/woutput.h +++ b/waylib/src/server/kernel/woutput.h @@ -67,9 +67,11 @@ class WAYLIB_SERVER_EXPORT WOutput : public QObject, public WObject wlr_allocator *allocator() const; bool configurePrimarySwapchain(const QSize &size, uint32_t format, wlr_swapchain **swapchain, - bool doTest = true); + bool doTest = true, + wlr_swapchain **replacedSwapchain = nullptr); bool configureCursorSwapchain(const QSize &size, uint32_t format, - wlr_swapchain **swapchain); + wlr_swapchain **swapchain, + wlr_swapchain **replacedSwapchain = nullptr); wlr_output *handle() const; diff --git a/waylib/src/server/kernel/wsurface.cpp b/waylib/src/server/kernel/wsurface.cpp index 62863f84b9..21932bbd84 100644 --- a/waylib/src/server/kernel/wsurface.cpp +++ b/waylib/src/server/kernel/wsurface.cpp @@ -5,6 +5,7 @@ #include "wscoplistener.h" #include "wseat.h" #include "private/wsurface_p.h" +#include "utils/private/wvulkantrace_p.h" #include "woutput.h" #include "wsubsurface.h" #include "wayliblogging.h" @@ -64,6 +65,13 @@ void WSurfacePrivate::connect() { W_Q(WSurface); q->listeners()->add(&m_handle->events.commit, this, &WSurfacePrivate::on_commit); + if (WVulkanTrace::enabled()) { + q->listeners()->add(&m_handle->events.commit, q, [q, this] (void *) { + WVulkanTrace::surfaceCommit(q, q->pid(), m_handle->current.seq, + m_handle->current.committed, + q->buffer()); + }); + } q->listeners()->add(&m_handle->events.map, q, &WSurface::mappedChanged); q->listeners()->add(&m_handle->events.unmap, q, &WSurface::mappedChanged); q->listeners()->add(&m_handle->events.new_subsurface, q, diff --git a/waylib/src/server/protocols/WLinuxDmabufV1 b/waylib/src/server/protocols/WLinuxDmabufV1 new file mode 100644 index 0000000000..5587ab15b1 --- /dev/null +++ b/waylib/src/server/protocols/WLinuxDmabufV1 @@ -0,0 +1 @@ +#include "wlinuxdmabufv1.h" diff --git a/waylib/src/server/protocols/wlinuxdmabufv1.cpp b/waylib/src/server/protocols/wlinuxdmabufv1.cpp new file mode 100644 index 0000000000..2f59ee4f75 --- /dev/null +++ b/waylib/src/server/protocols/wlinuxdmabufv1.cpp @@ -0,0 +1,82 @@ +// Copyright (C) 2026 UnionTech Software Technology Co., Ltd. +// SPDX-License-Identifier: Apache-2.0 OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#include "wlinuxdmabufv1.h" + +#include "wayliblogging.h" + +#include + +WAYLIB_SERVER_BEGIN_NAMESPACE + +constexpr uint32_t LinuxDmabufV1Version = 4; + +WLinuxDmabufV1::WLinuxDmabufV1(wlr_renderer *renderer, QObject *parent) + : QObject(parent) + , m_renderer(renderer) +{ +} + +wlr_linux_dmabuf_v1 *WLinuxDmabufV1::handle() const +{ + return nativeInterface(); +} + +QByteArrayView WLinuxDmabufV1::interfaceName() const +{ + return "zwp_linux_dmabuf_v1"; +} + +void WLinuxDmabufV1::create(WServer *server) +{ + if (m_handle) + return; + + if (!m_renderer) { + qCWarning(lcWlLinuxDmabuf) << "Cannot create linux-dmabuf global without renderer"; + return; + } + + const auto *formats = wlr_renderer_get_texture_formats( + m_renderer, WLR_BUFFER_CAP_DMABUF); + if (!formats) { + qCInfo(lcWlLinuxDmabuf) << "Skipping linux-dmabuf global: renderer does not expose DMA-BUF texture formats"; + return; + } + + const int drmFd = wlr_renderer_get_drm_fd(m_renderer); + if (drmFd < 0) { + qCInfo(lcWlLinuxDmabuf) << "Skipping linux-dmabuf global: renderer has no DRM fd"; + return; + } + + m_handle = wlr_linux_dmabuf_v1_create_with_renderer( + server->handle(), LinuxDmabufV1Version, m_renderer); + if (!m_handle) { + qCWarning(lcWlLinuxDmabuf) << "Failed to create renderer-backed linux-dmabuf global"; + return; + } + + qCInfo(lcWlLinuxDmabuf) << "Created renderer-backed linux-dmabuf global" + << "version" << LinuxDmabufV1Version + << "drm fd" << drmFd; +} + +void WLinuxDmabufV1::destroy([[maybe_unused]] WServer *server) +{ + // wlroots owns this global and destroys it from the display destroy + // listener; null m_handle so handle()/global() return null instead of a + // dangling pointer (kept as an explicit override rather than the inherited + // empty base for this hardening). + m_handle = nullptr; +} + +wl_global *WLinuxDmabufV1::global() const +{ + if (auto *native = handle()) + return native->global; + + return nullptr; +} + +WAYLIB_SERVER_END_NAMESPACE diff --git a/waylib/src/server/protocols/wlinuxdmabufv1.h b/waylib/src/server/protocols/wlinuxdmabufv1.h new file mode 100644 index 0000000000..e0b4c984c7 --- /dev/null +++ b/waylib/src/server/protocols/wlinuxdmabufv1.h @@ -0,0 +1,31 @@ +// Copyright (C) 2026 UnionTech Software Technology Co., Ltd. +// SPDX-License-Identifier: Apache-2.0 OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#pragma once + +#include +#include + +WAYLIB_SERVER_BEGIN_NAMESPACE + +class WAYLIB_SERVER_EXPORT WLinuxDmabufV1 : public QObject, public WServerInterface +{ + Q_OBJECT + +public: + explicit WLinuxDmabufV1(wlr_renderer *renderer, QObject *parent = nullptr); + + wlr_linux_dmabuf_v1 *handle() const; + + QByteArrayView interfaceName() const override; + +protected: + void create(WServer *server) override; + void destroy(WServer *server) override; + wl_global *global() const override; + +private: + wlr_renderer *m_renderer = nullptr; +}; + +WAYLIB_SERVER_END_NAMESPACE diff --git a/waylib/src/server/qtquick/private/wbufferrenderer.cpp b/waylib/src/server/qtquick/private/wbufferrenderer.cpp index 224e6cf531..347c37eb4b 100644 --- a/waylib/src/server/qtquick/private/wbufferrenderer.cpp +++ b/waylib/src/server/qtquick/private/wbufferrenderer.cpp @@ -6,15 +6,20 @@ #include "wbufferrenderer_p.h" #include "wayliblogging.h" #include "wrenderhelper.h" +#include "wrenderbufferblitter.h" #include "wqmlhelper_p.h" +#include "woutputviewport_p.h" #include "wtools.h" #include "wsgtextureprovider.h" #include "private/wprivateaccessor_p.h" +#include "utils/private/wvulkantrace_p.h" #include #include #include +#include +#include #include #include @@ -102,10 +107,34 @@ WBufferRenderer::~WBufferRenderer() { cleanTextureProvider(); resetSources(); + cleanupRetiredResources(true); delete m_renderHelper; } +void WBufferRenderer::retireSwapchain(wlr_swapchain *swapchain, bool defer) +{ + if (!swapchain) + return; + + if (!defer) { + wlr_swapchain_destroy(swapchain); + return; + } + + m_retiredSwapchains.append(swapchain); +} + +void WBufferRenderer::cleanupRetiredResources(bool force) +{ + if (m_renderHelper) + m_renderHelper->cleanupRetiredRenderResources(force); + + for (auto *swapchain : std::as_const(m_retiredSwapchains)) + wlr_swapchain_destroy(swapchain); + m_retiredSwapchains.clear(); +} + WOutput *WBufferRenderer::output() const { return m_output; @@ -252,15 +281,17 @@ wlr_buffer *WBufferRenderer::lastBuffer() const QRhiTexture *WBufferRenderer::currentRenderTarget() const { - if (!state.sgRenderTarget.rt) + auto renderTarget = state.sgRenderTarget.rt; + if (!renderTarget) return nullptr; - auto textureRT = static_cast(state.sgRenderTarget.rt); + auto textureRT = static_cast(renderTarget); auto colorAttachment = textureRT->description().colorAttachmentAt(0); if (!colorAttachment) return nullptr; return colorAttachment->texture(); } + bool WBufferRenderer::isColorPreserved() const { return state.renderTarget.colorPreserved(); @@ -278,6 +309,9 @@ wlr_damage_ring *WBufferRenderer::damageRing() bool WBufferRenderer::isTextureProvider() const { + if (isPrimaryOutputRendererForVulkan()) + return false; + return true; } @@ -288,6 +322,9 @@ QSGTextureProvider *WBufferRenderer::textureProvider() const WSGTextureProvider *WBufferRenderer::wTextureProvider() const { + if (isPrimaryOutputRendererForVulkan()) + return nullptr; + auto w = qobject_cast(window()); auto d = QQuickItemPrivate::get(this); if (!w || !d->sceneGraphRenderContext() || QThread::currentThread() != d->sceneGraphRenderContext()->thread()) { @@ -303,6 +340,20 @@ WSGTextureProvider *WBufferRenderer::wTextureProvider() const return m_textureProvider.get(); } +bool WBufferRenderer::isPrimaryOutputRendererForVulkan() const +{ + auto wd = window() ? QQuickWindowPrivate::get(window()) : nullptr; + if (!wd || !wd->rhi || wd->rhi->backend() != QRhi::Vulkan) + return false; + + auto viewport = qobject_cast(parentItem()); + if (!viewport) + return false; + + auto vd = WOutputViewportPrivate::get(viewport); + return vd && vd->attached && vd->bufferRenderer == this; +} + QTransform WBufferRenderer::inputMapToOutput(const QRectF &sourceRect, const QRectF &targetRect, const QSize &pixelSize, const qreal devicePixelRatio) { @@ -330,8 +381,8 @@ QTransform WBufferRenderer::inputMapToOutput(const QRectF &sourceRect, const QRe } wlr_buffer *WBufferRenderer::beginRender(const QSize &pixelSize, qreal devicePixelRatio, - uint32_t format, RenderFlags flags, - WGlobal::ColorContentsMode mode) + uint32_t format, RenderFlags flags, + WGlobal::ColorContentsMode mode) { Q_ASSERT(!state.buffer); Q_ASSERT(m_output); @@ -341,6 +392,9 @@ wlr_buffer *WBufferRenderer::beginRender(const QSize &pixelSize, qreal devicePix Q_EMIT beforeRendering(); + auto wd = QQuickWindowPrivate::get(window()); + const bool isVulkanRhi = wd->rhi && wd->rhi->backend() == QRhi::Vulkan; + // configure swapchain if (flags.testFlag(RenderFlag::DontConfigureSwapchain)) { auto renderFormat = pickFormat(m_output->renderer(), format); @@ -351,6 +405,8 @@ wlr_buffer *WBufferRenderer::beginRender(const QSize &pixelSize, qreal devicePix if (!m_swapchain || QSize(m_swapchain->width, m_swapchain->height) != pixelSize || m_swapchain->format.format != renderFormat->format) { + auto *oldSwapchain = m_swapchain.release(); + retireSwapchain(oldSwapchain, isVulkanRhi); m_swapchain.reset(wlr_swapchain_create(m_output->allocator(), pixelSize.width(), pixelSize.height(), renderFormat)); if (!m_swapchain) return nullptr; @@ -359,7 +415,10 @@ wlr_buffer *WBufferRenderer::beginRender(const QSize &pixelSize, qreal devicePix // The configure helpers take a wlr_swapchain**; hand ownership out // temporarily and take it back (possibly a new swapchain) afterwards. wlr_swapchain *sc = m_swapchain.release(); - bool ok = m_output->configureCursorSwapchain(pixelSize, format, &sc); + wlr_swapchain *replacedSwapchain = nullptr; + bool ok = m_output->configureCursorSwapchain(pixelSize, format, &sc, + isVulkanRhi ? &replacedSwapchain : nullptr); + retireSwapchain(replacedSwapchain, isVulkanRhi); if (!ok) { m_swapchain.reset(sc); return nullptr; @@ -367,8 +426,11 @@ wlr_buffer *WBufferRenderer::beginRender(const QSize &pixelSize, qreal devicePix m_swapchain.reset(sc); } else { wlr_swapchain *sc = m_swapchain.release(); + wlr_swapchain *replacedSwapchain = nullptr; bool ok = m_output->configurePrimarySwapchain(pixelSize, format, &sc, - !flags.testFlag(DontTestSwapchain)); + !flags.testFlag(DontTestSwapchain), + isVulkanRhi ? &replacedSwapchain : nullptr); + retireSwapchain(replacedSwapchain, isVulkanRhi); if (!ok) { m_swapchain.reset(sc); return nullptr; @@ -385,9 +447,9 @@ wlr_buffer *WBufferRenderer::beginRender(const QSize &pixelSize, qreal devicePix m_renderHelper = new WRenderHelper(m_output->renderer()); m_renderHelper->setSize(pixelSize); - auto wd = QQuickWindowPrivate::get(window()); Q_ASSERT(wd->renderControl); - auto rt = m_renderHelper->acquireRenderTarget(wd->renderControl, buffer, mode); + auto rt = m_renderHelper->acquireRenderTarget( + wd->renderControl, buffer, mode); if (rt.isNull()) { wlr_buffer_unlock(buffer); return nullptr; @@ -412,7 +474,6 @@ wlr_buffer *WBufferRenderer::beginRender(const QSize &pixelSize, qreal devicePix } else { state.dirty = QRegion(); - Q_ASSERT(rtd->type == QQuickRenderTargetPrivate::Type::RhiRenderTarget); sgRT.rt = rtd->u.rhiRt; sgRT.cb = wd->redirect.commandBuffer; Q_ASSERT(sgRT.cb); @@ -436,6 +497,9 @@ wlr_buffer *WBufferRenderer::beginRender(const QSize &pixelSize, qreal devicePix state.buffer.reset(buffer); state.renderTarget = rt; state.sgRenderTarget = sgRT; + if (isVulkanRhi) { + state.renderBufferReleasedForCache = false; + } return buffer; } @@ -445,8 +509,9 @@ inline static QRect scaleToRect(const QRectF &s, qreal scale) { (s.size() * scale).toSize()); } -void WBufferRenderer::render(int sourceIndex, const QMatrix4x4 &renderMatrix, - const QRectF &sourceRect, const QRectF &targetRect) +bool WBufferRenderer::render(int sourceIndex, const QMatrix4x4 &renderMatrix, + const QRectF &sourceRect, const QRectF &targetRect, + std::optional preserveColorContentsOverride) { Q_ASSERT(state.buffer); @@ -458,6 +523,10 @@ void WBufferRenderer::render(int sourceIndex, const QMatrix4x4 &renderMatrix, state.renderer = renderer; state.batchRenderer = dynamic_cast(renderer); state.worldTransform = renderMatrix; + const bool isVulkanRhi = wd->rhi && wd->rhi->backend() == QRhi::Vulkan; + bool preserveColorContents = preserveColorContentsOverride.value_or( + state.renderTarget.colorPreserved()); + auto activeSgRenderTarget = state.sgRenderTarget; // The renderer should always receive the window's DPR (Device Pixel Ratio) // because, regardless of the DPR used for rendering, all resources within // a window are loaded based on the window's own DPR. @@ -480,16 +549,17 @@ void WBufferRenderer::render(int sourceIndex, const QMatrix4x4 &renderMatrix, // are always consistent. Otherwise, some Items might render incorrectly. renderer->setDevicePixelRatio(window()->effectiveDevicePixelRatio()); renderer->setDeviceRect(QRect(QPoint(0, 0), state.pixelSize)); - renderer->setRenderTarget(state.sgRenderTarget); + renderer->setRenderTarget(activeSgRenderTarget); const auto viewportRect = scaleToRect(targetRect, devicePixelRatio); auto softwareRenderer = dynamic_cast(renderer); - const bool isVulkanRhi = wd->rhi && wd->rhi->backend() == QRhi::Vulkan; { // before render if (softwareRenderer) { // Avoid do clear before paint, for the software renderer this // work is expensive. - const bool clearColor = !state.renderTarget.colorPreserved(); + if (m_clearColor.alpha() == 0) + preserveColorContents = true; + const bool clearColor = !preserveColorContents; #if QT_VERSION >= QT_VERSION_CHECK(6, 9, 0) softwareRenderer->setClearColorEnabled(clearColor); #else @@ -573,18 +643,26 @@ void WBufferRenderer::render(int sourceIndex, const QMatrix4x4 &renderMatrix, renderer->setProjectionMatrix(projectionMatrix); renderer->setProjectionMatrixWithNativeNDC(projectionMatrixWithNativeNDC); + auto textureRT = static_cast(activeSgRenderTarget.rt); + if (!isVulkanRhi) { + if (preserveColorContents) { + textureRT->setFlags(textureRT->flags() | QRhiTextureRenderTarget::PreserveColorContents); + } else { + textureRT->setFlags(textureRT->flags() & ~QRhiTextureRenderTarget::PreserveColorContents); + } + } } } -#ifdef ENABLE_VULKAN_RENDER - if (m_renderHelper) - m_renderHelper->prepareVulkanRenderTarget(state.sgRenderTarget.cb, state.renderTarget); -#endif + WOutputRenderWindow *outputWindow = nullptr; + constexpr const char *samplingPurpose = "qt-render-pass-texture"; + if (isVulkanRhi) { + outputWindow = renderWindow(); + WVulkanTrace::beginPass(outputWindow, this, state.buffer.get(), samplingPurpose, + sourceIndex, 0); + } + state.context->renderNextFrame(renderer); -#ifdef ENABLE_VULKAN_RENDER - if (m_renderHelper) - m_renderHelper->finishVulkanRenderTarget(state.sgRenderTarget.cb, state.renderTarget); -#endif { // after render if (!softwareRenderer) { @@ -639,13 +717,45 @@ void WBufferRenderer::render(int sourceIndex, const QMatrix4x4 &renderMatrix, dr->currentFrameCommandBuffer()->resourceUpdate(resourceUpdates); } - if (shouldCacheBuffer()) + if (isVulkanRhi) + WVulkanTrace::endPass(outputWindow, true); + + if (shouldCacheBuffer() && !isVulkanRhi) { wTextureProvider()->setBuffer(state.buffer.get()); + } + + return true; } void WBufferRenderer::endRender() { Q_ASSERT(state.buffer.get()); + auto wd = QQuickWindowPrivate::get(window()); + const bool isVulkanRhi = wd->rhi && wd->rhi->backend() == QRhi::Vulkan; + if (!isVulkanRhi) { + { + WBufferUnlockPtr buffer; + buffer.swap(state.buffer); + state.renderer = nullptr; + state.batchRenderer = nullptr; + + m_lastBuffer = buffer.get(); + } + +#ifndef QT_NO_OPENGL + if (state.flags.testFlag(RedirectOpenGLContextDefaultFrameBufferObject) + && wd->rhi && wd->rhi->backend() == QRhi::OpenGLES2) { + auto glContext = QOpenGLContext::currentContext(); + Q_ASSERT(glContext); + QOpenGLContextPrivate::get(glContext)->defaultFboRedirect = GL_NONE; + } +#endif + + Q_EMIT afterRendering(); + return; + } + + const bool canExposeCompletedCache = state.renderBufferReleasedForCache; { WBufferUnlockPtr buffer; buffer.swap(state.buffer); @@ -653,10 +763,19 @@ void WBufferRenderer::endRender() state.batchRenderer = nullptr; m_lastBuffer = buffer.get(); + if (shouldCacheBuffer() && isVulkanRhi) { + if (isPrimaryOutputRendererForVulkan()) { + if (m_textureProvider) + m_textureProvider->setBuffer(nullptr); + } else if (canExposeCompletedCache) { + if (auto provider = wTextureProvider()) + provider->setBuffer(buffer.get()); + } + } } + state.renderBufferReleasedForCache = false; #ifndef QT_NO_OPENGL - auto wd = QQuickWindowPrivate::get(window()); if (state.flags.testFlag(RedirectOpenGLContextDefaultFrameBufferObject) && wd->rhi && wd->rhi->backend() == QRhi::OpenGLES2) { auto glContext = QOpenGLContext::currentContext(); @@ -678,6 +797,11 @@ void WBufferRenderer::updateTextureProvider() if (!m_textureProvider) return; + if (isPrimaryOutputRendererForVulkan()) { + m_textureProvider->setBuffer(nullptr); + return; + } + if (shouldCacheBuffer()) { const bool hasCachedBuffer = m_textureProvider->wlrBuffer(); // Ensure only update the buffer when the "shouldCacheBuffer" state is changed. @@ -691,12 +815,19 @@ void WBufferRenderer::updateTextureProvider() QSGNode *WBufferRenderer::updatePaintNode(QSGNode *oldNode, UpdatePaintNodeData *) { + auto texture = m_textureProvider ? m_textureProvider->texture() : nullptr; + if (!texture) { + delete oldNode; + return nullptr; + } + auto node = static_cast(oldNode); if (Q_UNLIKELY(!node)) { node = window()->createImageNode(); node->setOwnsTexture(false); - node->setTexture(m_textureProvider->texture()); + node->setTexture(texture); } else { + node->setTexture(texture); node->markDirty(QSGNode::DirtyMaterial); } @@ -714,12 +845,14 @@ void WBufferRenderer::invalidateSceneGraph() { if (m_textureProvider) m_textureProvider.reset(); + cleanupRetiredResources(true); resetSources(); } void WBufferRenderer::releaseResources() { cleanTextureProvider(); + cleanupRetiredResources(true); resetSources(); } diff --git a/waylib/src/server/qtquick/private/wbufferrenderer_p.h b/waylib/src/server/qtquick/private/wbufferrenderer_p.h index 0b10e75154..28e9ad391f 100644 --- a/waylib/src/server/qtquick/private/wbufferrenderer_p.h +++ b/waylib/src/server/qtquick/private/wbufferrenderer_p.h @@ -16,9 +16,12 @@ #include #include +#include + Q_MOC_INCLUDE() QT_BEGIN_NAMESPACE +class QRhiRenderTarget; class QSGPlainTexture; class QSGRenderContext; namespace QSGBatchRenderer { @@ -94,8 +97,9 @@ class WAYLIB_SERVER_EXPORT WBufferRenderer : public QQuickItem wlr_buffer *beginRender(const QSize &pixelSize, qreal devicePixelRatio, uint32_t format, RenderFlags flags = {}, WGlobal::ColorContentsMode mode = WGlobal::ColorContentsMode::DontCare); - void render(int sourceIndex, const QMatrix4x4 &renderMatrix, - const QRectF &sourceRect = {}, const QRectF &targetRect = {}); + bool render(int sourceIndex, const QMatrix4x4 &renderMatrix, + const QRectF &sourceRect = {}, const QRectF &targetRect = {}, + std::optional preserveColorContents = std::nullopt); void endRender(); void componentComplete() override; @@ -114,6 +118,9 @@ class WAYLIB_SERVER_EXPORT WBufferRenderer : public QQuickItem Q_SLOT void invalidateSceneGraph(); void releaseResources() override; void cleanTextureProvider(); + bool isPrimaryOutputRendererForVulkan() const; + void retireSwapchain(wlr_swapchain *swapchain, bool defer); + void cleanupRetiredResources(bool force = false); inline bool isRootItem(const QQuickItem *source) const { return nullptr == source; @@ -127,6 +134,7 @@ class WAYLIB_SERVER_EXPORT WBufferRenderer : public QQuickItem WUniquePointer m_swapchain; WRenderHelper *m_renderHelper = nullptr; WPointer m_lastBuffer; + QList m_retiredSwapchains; struct RenderState { RenderFlags flags; @@ -141,6 +149,7 @@ class WAYLIB_SERVER_EXPORT WBufferRenderer : public QQuickItem WRenderHelper::RenderTarget renderTarget; QSGRenderTarget sgRenderTarget; QRegion dirty; + bool renderBufferReleasedForCache = false; } state; QPointer m_output; diff --git a/waylib/src/server/qtquick/woutputhelper.cpp b/waylib/src/server/qtquick/woutputhelper.cpp index 2ad7ea2584..2abb351f38 100644 --- a/waylib/src/server/qtquick/woutputhelper.cpp +++ b/waylib/src/server/qtquick/woutputhelper.cpp @@ -102,6 +102,19 @@ void WOutputHelperPrivate::setContentIsDirty(bool newValue) wlr_buffer *WOutputHelperPrivate::acquireBuffer(wlr_swapchain **sc) { + // This legacy path has no callers on the Vulkan renderer. Its swapchain + // replacement destroys the old swapchain immediately, which can race with + // GPU work (this frame's command buffer) still referencing its buffers. + // The primary rendering path uses WOutput::configurePrimarySwapchain with + // deferred replacement instead, so fail safely here rather than + // introducing an untested retire mechanism. + if (output->renderer() && wlr_renderer_is_vk(output->renderer())) { + qCWarning(lcWlOutputHelper) + << "acquireBuffer is not supported with the Vulkan renderer" + << "output" << output; + return nullptr; + } + bool ok = wlr_output_configure_primary_swapchain(qwoutput(), &state, sc); if (!ok) return nullptr; diff --git a/waylib/src/server/qtquick/woutputrenderwindow.cpp b/waylib/src/server/qtquick/woutputrenderwindow.cpp index da5eac2af7..05bf7eacf9 100644 --- a/waylib/src/server/qtquick/woutputrenderwindow.cpp +++ b/waylib/src/server/qtquick/woutputrenderwindow.cpp @@ -20,6 +20,7 @@ #include "winputdevice.h" #include "wseat.h" #include "wayliblogging.h" +#include "utils/private/wvulkantrace_p.h" #include "platformplugin/qwlrootsintegration.h" #include "platformplugin/qwlrootscreen.h" @@ -29,13 +30,16 @@ #include #include -#include +#include #include #include #include +#include +#include #include #include #include +#include #include #include @@ -55,6 +59,9 @@ #include #include +extern "C" { +#include +} #include #include @@ -229,8 +236,9 @@ class Q_DECL_HIDDEN OutputHelper : public WOutputHelper const QSize &pixelSize, uint32_t format, WBufferRenderer::RenderFlags flags, WGlobal::ColorContentsMode mode = WGlobal::ColorContentsMode::DontCare); - inline void render(WBufferRenderer *renderer, int sourceIndex, const QMatrix4x4 &renderMatrix, - const QRectF &sourceRect, const QRectF &viewportRect); + inline bool render(WBufferRenderer *renderer, int sourceIndex, const QMatrix4x4 &renderMatrix, + const QRectF &sourceRect, const QRectF &viewportRect, + std::optional preserveColorContents = std::nullopt); static bool visualizeLayers() { static bool on = qEnvironmentVariableIsSet("WAYLIB_VISUALIZE_LAYERS"); @@ -359,6 +367,46 @@ class Q_DECL_HIDDEN RenderControl : public QQuickRenderControl public: RenderControl() = default; + QRhi::FrameOpResult beginFrameChecked() + { + QQuickRenderControl::beginFrame(); + + auto *d = QQuickRenderControlPrivate::get(this); + switch (d->frameStatus) { + case QQuickRenderControlPrivate::RecordingFrame: + return QRhi::FrameOpSuccess; + case QQuickRenderControlPrivate::DeviceLostInBeginFrame: + return QRhi::FrameOpDeviceLost; + case QQuickRenderControlPrivate::ErrorInBeginFrame: + return d->rhi && d->rhi->isDeviceLost() + ? QRhi::FrameOpDeviceLost + : QRhi::FrameOpError; + case QQuickRenderControlPrivate::NotRecordingFrame: + break; + } + return QRhi::FrameOpError; + } + + QRhi::FrameOpResult endFrameChecked() + { + auto *d = QQuickRenderControlPrivate::get(this); + if (!d->rhi + || !d->window + || d->frameStatus != QQuickRenderControlPrivate::RecordingFrame + || !d->rhi->isRecordingFrame()) { + return d->rhi && d->rhi->isDeviceLost() + ? QRhi::FrameOpDeviceLost + : QRhi::FrameOpError; + } + + const auto result = d->rhi->endOffscreenFrame(); + // Match QQuickRenderControl::endFrame(), while retaining the QRhi + // submission result which the public void API discards. + d->frameStatus = QQuickRenderControlPrivate::NotRecordingFrame; + Q_EMIT d->window->afterFrameEnd(); + return result; + } + QWindow *renderWindow(QPoint *) override { return m_renderWindow; } @@ -433,6 +481,19 @@ class Q_DECL_HIDDEN WOutputRenderWindowPrivate : public QQuickWindowPrivate QVector> doRenderOutputs(wlr_output *needsFrameOutput, const QList &outputs, bool forceRender); + bool releaseRenderBuffer(WBufferRenderer *renderer, const char *purpose); + bool releaseRenderBuffers(QVector> &needsCommit); + void cleanupRetiredRenderResources(bool force); + inline void failCurrentFrame() + { + frameFailed = true; + } + inline void failCurrentFrameFatally(const QString &message) + { + frameFailed = true; + if (fatalRenderError.isEmpty()) + fatalRenderError = message; + } void doRender(wlr_output *needsFrameOutput, const QList &outputs, bool forceRender, bool doCommit); @@ -457,7 +518,13 @@ class Q_DECL_HIDDEN WOutputRenderWindowPrivate : public QQuickWindowPrivate bool componentCompleted = true; bool inRendering = false; bool renderEnabled = true; - + bool frameFailed = false; + QString fatalRenderError; + // Consecutive QRhi beginOffscreenFrame failures without a completed frame. + // A single Error (not device-lost) failure only skips the frame; repeated + // failures freeze resource retirement forever, so escalate to fatal. + int consecutiveBeginFrameFailures = 0; + static constexpr int FatalBeginFrameFailureThreshold = 3; WPointer m_renderer; WPointer m_allocator; @@ -593,11 +660,29 @@ wlr_buffer *OutputHelper::beginRender(WBufferRenderer *renderer, return renderer->beginRender(pixelSize, devicePixelRatio(), format, flags, mode); } -void OutputHelper::render(WBufferRenderer *renderer, int sourceIndex, const QMatrix4x4 &renderMatrix, - const QRectF &sourceRect, const QRectF &targetRect) +bool OutputHelper::render(WBufferRenderer *renderer, int sourceIndex, const QMatrix4x4 &renderMatrix, + const QRectF &sourceRect, const QRectF &targetRect, + std::optional preserveColorContents) { - renderWindowD()->pushRenderer(renderer); - renderer->render(sourceIndex, renderMatrix, sourceRect, targetRect); + auto *windowPrivate = renderWindowD(); + if (WRenderHelper::getGraphicsApi(windowPrivate->rc()) + != QSGRendererInterface::Vulkan) { + windowPrivate->pushRenderer(renderer); + return renderer->render(sourceIndex, renderMatrix, sourceRect, targetRect, + preserveColorContents); + } + + windowPrivate->pushRenderer(renderer); + const bool ok = renderer->render(sourceIndex, renderMatrix, sourceRect, targetRect, + preserveColorContents); + if (!ok) { + qCWarning(lcWlBufferRenderer) << "WBufferRenderer render pass failed" + << "renderer" << renderer + << "sourceIndex" << sourceIndex + << "currentBuffer" << renderer->currentBuffer() + << "lastBuffer" << renderer->lastBuffer(); + } + return ok; } static QQuickItem *createVisualRectangle(QQuickItem *target, const QColor &color) { @@ -768,15 +853,40 @@ wlr_buffer *OutputHelper::renderLayer(LayerData *layer, bool *dontEndRenderAndRe if (buffer) { const QRectF sr = QRectF(layer->mapRect.topLeft() - layer->noClipMapRect.topLeft(), layer->mapRect.size()); const QRectF tr(QPointF(0, 0), layer->mapRect.size()); + const bool vulkanRenderer = + WRenderHelper::getGraphicsApi(renderWindowD()->rc()) + == QSGRendererInterface::Vulkan; - render(layer->renderer, 0, layer->renderMatrix, sr, tr); - + bool renderOk = render(layer->renderer, 0, layer->renderMatrix, sr, tr); if (visualizeLayers()) - render(layer->renderer, 1, layer->renderMatrix, sr, tr); + renderOk = renderOk + && render(layer->renderer, 1, layer->renderMatrix, sr, tr); + + if (!renderOk) { + qCWarning(lcWlBufferRenderer) + << "Skipping layer buffer because render pass failed" + << "renderer" << layer->renderer + << "currentBuffer" << layer->renderer->currentBuffer() + << "layer" << layer->layer; + if (vulkanRenderer) { + (void)renderWindowD()->releaseRenderBuffer( + layer->renderer, "layer-render-buffer-abort"); + } + layer->renderer->endRender(); + if (dontEndRenderAndReturnNeedsEndRender) + *dontEndRenderAndReturnNeedsEndRender = false; + return nullptr; + } if (dontEndRenderAndReturnNeedsEndRender) { *dontEndRenderAndReturnNeedsEndRender = true; } else { + if (vulkanRenderer + && !renderWindowD()->releaseRenderBuffer( + layer->renderer, "layer-render-buffer")) { + layer->renderer->endRender(); + return nullptr; + } layer->renderer->endRender(); } } else if (dontEndRenderAndReturnNeedsEndRender) { @@ -825,6 +935,14 @@ WBufferRenderer *OutputHelper::afterRender() if (!buffer) continue; + if (needsEndBuffer + && WRenderHelper::getGraphicsApi(renderWindowD()->rc()) + == QSGRendererInterface::Vulkan + && !renderWindowD()->releaseRenderBuffer(i->renderer, "layer-render-buffer")) { + i->renderer->endRender(); + continue; + } + layers.append({ .layer = i->wlrLayer, .buffer = buffer, @@ -971,8 +1089,16 @@ WBufferRenderer *OutputHelper::compositeLayers(const QList layers, b { Q_ASSERT(!layers.isEmpty()); - const bool usingShadowRenderer = forceShadowRenderer + const bool isVulkan = WRenderHelper::getGraphicsApi(renderWindowD()->rc()) + == QSGRendererInterface::Vulkan; + const bool needsShadowRenderer = forceShadowRenderer || !bufferRenderer()->isColorPreserved(); + const bool usingShadowRenderer = needsShadowRenderer && !isVulkan; + if (needsShadowRenderer && !usingShadowRenderer) { + qCDebug(lcWlBufferRenderer) << "Disabled shadow layer compositor on Vulkan; compositing layers into current output buffer" + << "output" << m_output + << "layerCount" << layers.size(); + } if (!m_layerPorxyContainer) { m_layerPorxyContainer = new QQuickItem(renderWindow()->contentItem()); @@ -1050,16 +1176,54 @@ WBufferRenderer *OutputHelper::compositeLayers(const QList layers, b WGlobal::ColorContentsMode::Preserve); if (ok) { + if (WRenderHelper::getGraphicsApi(renderWindowD()->rc()) + != QSGRendererInterface::Vulkan) { + // stop primary render + if (bufferRenderer()->currentBuffer()) + bufferRenderer()->endRender(); + render(bufferRenderer2(), 0, {}, m_output->effectiveSourceRect(), + m_output->targetRect(), true); + return bufferRenderer2(); + } + // stop primary render - if (bufferRenderer()->currentBuffer()) + if (bufferRenderer()->currentBuffer()) { + if (!renderWindowD()->releaseRenderBuffer(bufferRenderer(), "shadow-source-render-buffer")) { + bufferRenderer()->endRender(); + return nullptr; + } bufferRenderer()->endRender(); - render(bufferRenderer2(), 0, {}, m_output->effectiveSourceRect(), m_output->targetRect()); + } + if (!render(bufferRenderer2(), 0, {}, m_output->effectiveSourceRect(), m_output->targetRect(), true)) { + qCWarning(lcWlBufferRenderer) << "Skipping shadow composite buffer because render pass failed" + << "renderer" << bufferRenderer2() + << "currentBuffer" << bufferRenderer2()->currentBuffer() + << "output" << m_output; + (void)renderWindowD()->releaseRenderBuffer(bufferRenderer2(), "shadow-composite-render-buffer-abort"); + bufferRenderer2()->endRender(); + return nullptr; + } return bufferRenderer2(); } } else { if (bufferRenderer()->currentBuffer()) { - render(bufferRenderer(), 1, {}, m_output->effectiveSourceRect(), m_output->targetRect()); + if (WRenderHelper::getGraphicsApi(renderWindowD()->rc()) + != QSGRendererInterface::Vulkan) { + render(bufferRenderer(), 1, {}, m_output->effectiveSourceRect(), + m_output->targetRect(), true); + return bufferRenderer(); + } + + if (!render(bufferRenderer(), 1, {}, m_output->effectiveSourceRect(), m_output->targetRect(), true)) { + qCWarning(lcWlBufferRenderer) << "Skipping layer composite into output because render pass failed" + << "renderer" << bufferRenderer() + << "currentBuffer" << bufferRenderer()->currentBuffer() + << "output" << m_output; + (void)renderWindowD()->releaseRenderBuffer(bufferRenderer(), "layer-composite-render-buffer-abort"); + bufferRenderer()->endRender(); + return nullptr; + } } else { // ###(zccrs): Maybe because contents is not dirty, so not do render // in WOutputRenderWindowPrivate::doRenderOutputs, force mark the @@ -1415,6 +1579,8 @@ bool WOutputRenderWindowPrivate::initRCWithRhi() Q_ASSERT(rcd->rhi == result.rhi); Q_ASSERT(!swapchain); + WVulkanTrace::activate(rhiSupport->rhiBackend() == QRhi::Vulkan); + return true; } @@ -1450,6 +1616,77 @@ QVector> WOutputRenderWindowPrivate::doRenderOutputs(wlr_output *needsFrameOutput, const QList &outputs, bool forceRender) { + if (WRenderHelper::getGraphicsApi(rc()) != QSGRendererInterface::Vulkan) { + QVector renderResults; + renderResults.reserve(outputs.size()); + for (OutputHelper *helper : std::as_const(outputs)) { + if (Q_LIKELY(needsFrameOutput)) { + if (helper->output() != needsFrameOutput) + continue; + else + Q_ASSERT(!helper->framePending()); + } + + if (Q_LIKELY(!forceRender)) { + if (helper->framePending()) + continue; + + // Render if output will be enabled OR has extraState to commit + // Note: Even when disabling, we need to render once to commit the disabled state + // (extraState will contain the ENABLED=false change) + bool shouldRender = helper->willBeEnabled() || helper->extraState(); + if (Q_UNLIKELY(!WOutputViewportPrivate::get(helper->outputViewport())->renderable()) + || !shouldRender) + continue; + + if (!(helper->needsFrame() || helper->contentIsDirty())) + continue; + + if (!helper->contentIsDirty()) { + renderResults.append(helper); + continue; + } + } + + Q_ASSERT(helper->outputViewport()->output()->scale() + <= helper->outputViewport()->devicePixelRatio()); + + const auto &format = helper->output()->render_format; + const auto renderMatrix = helper->outputViewport()->renderMatrix(); + + // maybe using the other WOutputViewport's QSGTextureProvider + if (!helper->outputViewport()->depends().isEmpty()) + updateDirtyNodes(); + + wlr_buffer *buffer = helper->beginRender( + helper->bufferRenderer(), + helper->outputViewport()->output()->size(), + format, + WBufferRenderer::RedirectOpenGLContextDefaultFrameBufferObject, + helper->outputViewport()->colorContentsMode()); + Q_ASSERT(buffer == helper->bufferRenderer()->currentBuffer()); + if (buffer) { + helper->render(helper->bufferRenderer(), + 0, + renderMatrix, + helper->outputViewport()->effectiveSourceRect(), + helper->outputViewport()->targetRect()); + } + renderResults.append(helper); + } + + QVector> needsCommit; + needsCommit.reserve(renderResults.size()); + for (auto helper : std::as_const(renderResults)) { + auto bufferRenderer = helper->afterRender(); + if (bufferRenderer) + needsCommit.append({ helper, bufferRenderer }); + } + + rendererList.clear(); + return needsCommit; + } + QVector renderResults; renderResults.reserve(outputs.size()); for (OutputHelper *helper : std::as_const(outputs)) { @@ -1499,9 +1736,22 @@ WOutputRenderWindowPrivate::doRenderOutputs(wlr_output *needsFrameOutput, const helper->outputViewport()->colorContentsMode()); Q_ASSERT(buffer == helper->bufferRenderer()->currentBuffer()); if (buffer) { - helper->render(helper->bufferRenderer(), 0, renderMatrix, - helper->outputViewport()->effectiveSourceRect(), - helper->outputViewport()->targetRect()); + if (!helper->render(helper->bufferRenderer(), 0, renderMatrix, + helper->outputViewport()->effectiveSourceRect(), + helper->outputViewport()->targetRect())) { + qCWarning(lcWlBufferRenderer) << "Skipping output buffer because render pass failed" + << "renderer" << helper->bufferRenderer() + << "currentBuffer" << helper->bufferRenderer()->currentBuffer() + << "output" << helper->outputViewport(); + if (!releaseRenderBuffer(helper->bufferRenderer(), "compositor-render-buffer-abort")) { + failCurrentFrameFatally(QStringLiteral("Failed to release an aborted Vulkan output render buffer")); + } + helper->bufferRenderer()->endRender(); + helper->resetState(); + if (frameFailed) + break; + continue; + } } renderResults.append(helper); } @@ -1522,7 +1772,105 @@ WOutputRenderWindowPrivate::doRenderOutputs(wlr_output *needsFrameOutput, const return needsCommit; } -// ###: QQuickAnimatorController::advance symbol not export +bool WOutputRenderWindowPrivate::releaseRenderBuffer(WBufferRenderer *renderer, const char *purpose) +{ + if (WRenderHelper::getGraphicsApi(rc()) != QSGRendererInterface::Vulkan) + return true; + + if (!renderer || !renderer->currentBuffer()) + return true; + + if (!renderer->m_renderHelper) { + qCWarning(lcWlRenderHelper) << "Vulkan render buffer release failed: missing render helper" + << "purpose" << purpose + << "renderer" << renderer + << "buffer" << renderer->currentBuffer(); + return false; + } + + const bool ok = renderer->m_renderHelper->releaseRenderBuffer(rc(), + renderer->currentBuffer(), + renderer->currentRenderTarget(), + purpose); + const bool cacheable = !purpose || !std::strstr(purpose, "abort"); + if (ok && cacheable) { + renderer->state.renderBufferReleasedForCache = true; + qCDebug(lcWlRenderHelper) << "Marked Vulkan render buffer released for cache sampling" + << "purpose" << purpose + << "renderer" << renderer + << "buffer" << renderer->currentBuffer() + << "renderTarget" << renderer->currentRenderTarget(); + } else if (ok) { + qCDebug(lcWlRenderHelper) << "Vulkan render buffer release succeeded but cache update is skipped" + << "purpose" << purpose + << "renderer" << renderer + << "buffer" << renderer->currentBuffer() + << "renderTarget" << renderer->currentRenderTarget(); + } + return ok; +} + +bool WOutputRenderWindowPrivate::releaseRenderBuffers(QVector> &needsCommit) +{ + if (WRenderHelper::getGraphicsApi(rc()) != QSGRendererInterface::Vulkan) + return true; + + bool ok = true; + for (qsizetype i = 0; i < needsCommit.size();) { + auto helper = needsCommit[i].first; + auto renderer = needsCommit[i].second; + if (!renderer || !renderer->currentBuffer()) { + ++i; + continue; + } + + if (releaseRenderBuffer(renderer, "compositor-render-buffer")) { + ++i; + continue; + } + + qCCritical(lcWlRenderHelper) << "Vulkan output frame cannot be committed because render buffer release failed" + << "renderer" << renderer + << "buffer" << renderer->currentBuffer(); + ok = false; + renderer->endRender(); + helper->resetState(); + needsCommit.removeAt(i); + } + return ok; +} + +void WOutputRenderWindowPrivate::cleanupRetiredRenderResources(bool force) +{ + QSet seenRenderers; + auto cleanupRenderer = [&seenRenderers, force] (WBufferRenderer *renderer) { + if (!renderer || seenRenderers.contains(renderer)) + return; + + seenRenderers.insert(renderer); + renderer->cleanupRetiredResources(force); + }; + + for (auto helper : std::as_const(outputs)) { + if (!helper) + continue; + + if (helper->m_output) + cleanupRenderer(helper->bufferRenderer()); + + if (helper->m_output2) + cleanupRenderer(helper->bufferRenderer2()); + + if (helper->m_cursorRenderer) + cleanupRenderer(helper->m_cursorRenderer); + + for (auto layer : std::as_const(helper->m_layers)) { + if (layer) + cleanupRenderer(layer->renderer); + } + } +} + static void QQuickAnimatorController_advance(QQuickAnimatorController *ac) { bool running = false; @@ -1549,30 +1897,188 @@ void WOutputRenderWindowPrivate::doRender(wlr_output *needsFrameOutput, if (!renderEnabled) return; - inRendering = true; + if (WRenderHelper::getGraphicsApi(rc()) != QSGRendererInterface::Vulkan) { + inRendering = true; + + W_Q(WOutputRenderWindow); + for (OutputLayer *layer : std::as_const(layers)) { + layer->beforeRender(q); + } + + rc()->polishItems(); + + if (QSGRendererInterface::isApiRhiBased(WRenderHelper::getGraphicsApi())) + rc()->beginFrame(); + rc()->sync(); + + QQuickAnimatorController_advance(animationController.get()); + Q_EMIT q->beforeRendering(); + runAndClearJobs(&beforeRenderingJobs); + + auto needsCommit = doRenderOutputs(needsFrameOutput, outputs, forceRender); + + Q_EMIT q->afterRendering(); + runAndClearJobs(&afterRenderingJobs); + + if (QSGRendererInterface::isApiRhiBased(WRenderHelper::getGraphicsApi())) + rc()->endFrame(); + + // prevent gles2-render exception in wlroots. + // wlroots may have render operations after commit, so do + // not move the location during the reset operation. + // eg: screencopy ext-image-capture + resetGlState(); + + QList> committedOutputs; + if (doCommit) { + committedOutputs.reserve(needsCommit.size()); + for (auto i : std::as_const(needsCommit)) { + if (Q_UNLIKELY(!i.first->framePending())) { + if (Q_LIKELY(i.first->commit(i.second))) { + // Make sure the output is still valid after commit + auto output = i.first->outputViewport()->output(); + if (Q_LIKELY(needsFrameOutput)) { + Q_ASSERT(output->handle() == needsFrameOutput); + if (committedOutputs.isEmpty()) + committedOutputs.append(output); + } else if (!committedOutputs.contains(output)) { + committedOutputs.append(output); + } + } + } + + if (i.second->currentBuffer()) { + i.second->endRender(); + } + i.first->resetState(); + } + } + + resetGlState(); + + // On Intel&Nvidia multi-GPU environment, wlroots using Intel card do render for all + // outputs, and blit nvidia's output buffer in drm_connector_state_update_primary_fb, + // the 'blit' behavior will make EGL context to Nvidia renderer. So must done current + // OpenGL context here in order to ensure QtQuick always make EGL context to Intel + // renderer before next frame. + if (glContext) + glContext->doneCurrent(); + + inRendering = false; + Q_EMIT q->renderEnd(committedOutputs); + return; + } + + inRendering = true; + frameFailed = false; + fatalRenderError.clear(); W_Q(WOutputRenderWindow); + WVulkanTrace::beginFrame(q); + + const auto finishFrame = [this, q] (const QList> &committedOutputs) { + resetGlState(); + + // On Intel&Nvidia multi-GPU environment, wlroots using Intel card do render for all + // outputs, and blit nvidia's output buffer in drm_connector_state_update_primary_fb, + // the 'blit' behavior will make EGL context to Nvidia renderer. So must done current + // OpenGL context here in order to ensure QtQuick always make EGL context to Intel + // renderer before next frame. + if (glContext) + glContext->doneCurrent(); + + WVulkanTrace::setFrameStage(q, WVulkanTrace::FrameStage::RenderEnd); + inRendering = false; + Q_EMIT q->renderEnd(committedOutputs); + WVulkanTrace::endFrame(q, committedOutputs.size()); + + if (!fatalRenderError.isEmpty()) { + qCCritical(lcWlRenderer) << "Fatal Vulkan rendering failure; requesting compositor shutdown" + << "reason" << fatalRenderError; + Q_EMIT q->sceneGraphError(QQuickWindow::ContextNotAvailable, + fatalRenderError); + } + }; + + WVulkanTrace::setFrameStage(q, WVulkanTrace::FrameStage::Polish); for (OutputLayer *layer : std::as_const(layers)) { layer->beforeRender(q); } rc()->polishItems(); - if (QSGRendererInterface::isApiRhiBased(WRenderHelper::getGraphicsApi())) - rc()->beginFrame(); + const auto graphicsApi = WRenderHelper::getGraphicsApi(); + const bool isVulkan = graphicsApi == QSGRendererInterface::Vulkan; + const bool rhiBased = QSGRendererInterface::isApiRhiBased(graphicsApi); + if (rhiBased) { + WVulkanTrace::setFrameStage(q, WVulkanTrace::FrameStage::BeginFrame); + if (isVulkan) { + const auto beginResult = rc()->beginFrameChecked(); + if (beginResult != QRhi::FrameOpSuccess) { + if (beginResult == QRhi::FrameOpDeviceLost) { + failCurrentFrameFatally(QStringLiteral("Vulkan device was lost while beginning the Qt Quick frame")); + } else { + ++consecutiveBeginFrameFailures; + if (consecutiveBeginFrameFailures >= FatalBeginFrameFailureThreshold) { + // beginFrame errors never reach endFrameChecked(), so + // retired resources and after-swap cleanup jobs would + // never run again; escalate instead of freezing. + failCurrentFrameFatally(QStringLiteral( + "Qt Quick Vulkan frame begin failed %1 times in a row") + .arg(consecutiveBeginFrameFailures)); + } else { + failCurrentFrame(); + qCWarning(lcWlRenderer) << "Skipping Vulkan output frame because QRhi beginOffscreenFrame failed" + << "frameResult" << beginResult + << "consecutiveFailures" << consecutiveBeginFrameFailures; + } + } + finishFrame({}); + return; + } + consecutiveBeginFrameFailures = 0; + } else { + rc()->beginFrame(); + } + } + WVulkanTrace::setFrameStage(q, WVulkanTrace::FrameStage::Sync); rc()->sync(); + WVulkanTrace::setFrameStage(q, WVulkanTrace::FrameStage::Recording); QQuickAnimatorController_advance(animationController.get()); Q_EMIT q->beforeRendering(); runAndClearJobs(&beforeRenderingJobs); - auto needsCommit = doRenderOutputs(needsFrameOutput, outputs, forceRender); + QVector> needsCommit; + needsCommit = doRenderOutputs(needsFrameOutput, outputs, forceRender); + WVulkanTrace::setFrameStage(q, WVulkanTrace::FrameStage::AfterRenderingJobs); Q_EMIT q->afterRendering(); runAndClearJobs(&afterRenderingJobs); - if (QSGRendererInterface::isApiRhiBased(WRenderHelper::getGraphicsApi())) - rc()->endFrame(); + if (rhiBased) { + WVulkanTrace::setFrameStage(q, WVulkanTrace::FrameStage::ReleaseRenderBuffers); + if (!releaseRenderBuffers(needsCommit)) { + failCurrentFrameFatally(QStringLiteral("Failed to release one or more Vulkan output render buffers")); + } + } + + bool vulkanFrameCompleted = false; + if (rhiBased) { + WVulkanTrace::setFrameStage(q, WVulkanTrace::FrameStage::BeforeEndFrame); + if (isVulkan) { + const auto endResult = rc()->endFrameChecked(); + vulkanFrameCompleted = endResult == QRhi::FrameOpSuccess; + if (!vulkanFrameCompleted) { + failCurrentFrameFatally(endResult == QRhi::FrameOpDeviceLost + ? QStringLiteral("Vulkan device was lost while submitting the Qt Quick frame") + : QStringLiteral("Failed to submit the Vulkan Qt Quick frame")); + } + } else { + rc()->endFrame(); + } + WVulkanTrace::setFrameStage(q, WVulkanTrace::FrameStage::AfterEndFrame); + } // prevent gles2-render exception in wlroots. // wlroots may have render operations after commit, so do @@ -1581,16 +2087,18 @@ void WOutputRenderWindowPrivate::doRender(wlr_output *needsFrameOutput, resetGlState(); QList> committedOutputs; - if (doCommit) { + WVulkanTrace::setFrameStage(q, WVulkanTrace::FrameStage::OutputCommit); + if (doCommit || (isVulkan && frameFailed)) { + const bool allowCommit = doCommit && !frameFailed; committedOutputs.reserve(needsCommit.size()); for (auto i : std::as_const(needsCommit)) { - // Explicit render(viewport, true) is used for state-only output - // transactions. It must not be suppressed merely because the - // transaction itself scheduled the next frame. - if (forceRender || Q_UNLIKELY(!i.first->framePending())) { - if (Q_LIKELY(i.first->commit(i.second))) { + auto output = i.first->outputViewport()->output(); + if (allowCommit && (forceRender || Q_UNLIKELY(!i.first->framePending()))) { + const quint32 sequenceBefore = output->handle()->commit_seq; + const bool commitOk = i.first->commit(i.second); + WVulkanTrace::outputCommitted(q, output->handle(), sequenceBefore, commitOk); + if (Q_LIKELY(commitOk)) { // Make sure the output is still valid after commit - auto output = i.first->outputViewport()->output(); if (Q_LIKELY(needsFrameOutput)) { Q_ASSERT(output->handle() == needsFrameOutput); if (committedOutputs.isEmpty()) @@ -1609,18 +2117,21 @@ void WOutputRenderWindowPrivate::doRender(wlr_output *needsFrameOutput, } } - resetGlState(); - - // On Intel&Nvidia multi-GPU environment, wlroots using Intel card do render for all - // outputs, and blit nvidia's output buffer in drm_connector_state_update_primary_fb, - // the 'blit' behavior will make EGL context to Nvidia renderer. So must done current - // OpenGL context here in order to ensure QtQuick always make EGL context to Intel - // renderer before next frame. - if (glContext) - glContext->doneCurrent(); - - inRendering = false; - Q_EMIT q->renderEnd(committedOutputs); + if (vulkanFrameCompleted) { + // endOffscreenFrame() has completed all GPU work for this frame. Run + // retirement only after output commit/reset has transferred or dropped + // every buffer reference, so imported QRhi wrappers are destroyed + // before their wlroots textures and buffer locks are released. + cleanupRetiredRenderResources(false); + // All submissions of this completed frame are queue-ordered ahead + // of the next frame's on the shared VkQueue, so the per-frame alias + // lists have served their purpose; drop them to keep the alias + // barriers exact and the scans bounded. + WRenderHelper::resetVulkanFrameAliasLists(m_renderer); + } + finishFrame(committedOutputs); + if (vulkanFrameCompleted) + runAndClearJobs(&afterSwapJobs); } // TODO: Support QWindow::setCursor @@ -1646,11 +2157,11 @@ WOutputRenderWindow::WOutputRenderWindow(QObject *parent) WOutputRenderWindow::~WOutputRenderWindow() { + WVulkanTrace::windowDestroyed(this); Q_D(WOutputRenderWindow); // Drop frame/needs_frame listeners while outputs are still usable; // ~WListenerOwner teardowns cross-object groups automatically. d->listenerOwner.reset(); - qGuiApp->removeEventFilter(this); renderControl()->disconnect(this); diff --git a/waylib/src/server/qtquick/woutputviewport.cpp b/waylib/src/server/qtquick/woutputviewport.cpp index a1f8554b99..fc1b31e95f 100644 --- a/waylib/src/server/qtquick/woutputviewport.cpp +++ b/waylib/src/server/qtquick/woutputviewport.cpp @@ -152,6 +152,12 @@ WOutputRenderWindow *WOutputViewport::outputRenderWindow() const return qobject_cast(window()); } +wlr_buffer *WOutputViewport::lastBuffer() const +{ + W_DC(WOutputViewport); + return d->bufferRenderer->lastBuffer(); +} + QQuickItem *WOutputViewport::input() const { W_DC(WOutputViewport); diff --git a/waylib/src/server/qtquick/woutputviewport.h b/waylib/src/server/qtquick/woutputviewport.h index b7713283b2..b409a4ecdb 100644 --- a/waylib/src/server/qtquick/woutputviewport.h +++ b/waylib/src/server/qtquick/woutputviewport.h @@ -4,6 +4,7 @@ #pragma once #include +#include #include #include @@ -46,6 +47,7 @@ class WAYLIB_SERVER_EXPORT WOutputViewport : public QQuickItem, public virtual W QSGTextureProvider *textureProvider() const override; WSGTextureProvider *wTextureProvider() const override; WOutputRenderWindow *outputRenderWindow() const override; + wlr_buffer *lastBuffer() const; QQuickItem *input() const; void setInput(QQuickItem *item); diff --git a/waylib/src/server/qtquick/wquicktextureproxy.cpp b/waylib/src/server/qtquick/wquicktextureproxy.cpp index ee4de967d2..175ca1dbea 100644 --- a/waylib/src/server/qtquick/wquicktextureproxy.cpp +++ b/waylib/src/server/qtquick/wquicktextureproxy.cpp @@ -188,8 +188,10 @@ class Q_DECL_HIDDEN SGTextureProviderNode : public QObject, public QSGNode m_tp = tp; onTextureChanged(); - connect(tp, &QSGTextureProvider::textureChanged, - this, &SGTextureProviderNode::onTextureChanged, Qt::DirectConnection); + if (tp) { + connect(tp, &QSGTextureProvider::textureChanged, + this, &SGTextureProviderNode::onTextureChanged, Qt::DirectConnection); + } } inline QSGTextureProvider *textureProvider() const { @@ -202,8 +204,13 @@ class Q_DECL_HIDDEN SGTextureProviderNode : public QObject, public QSGNode appendChildNode(image); image->setFlag(QSGNode::OwnedByParent); if (m_tp) { - m_image->setTexture(m_tp->texture()); - Q_ASSERT(m_image->texture()); + auto texture = m_tp->texture(); + if (texture) { + m_image->setTexture(texture); + } else { + delete m_image; + m_image = nullptr; + } } } diff --git a/waylib/src/server/qtquick/wrenderhelper.cpp b/waylib/src/server/qtquick/wrenderhelper.cpp index 131631a62b..fdf17d1b01 100644 --- a/waylib/src/server/qtquick/wrenderhelper.cpp +++ b/waylib/src/server/qtquick/wrenderhelper.cpp @@ -8,25 +8,47 @@ #include "wayliblogging.h" #include "private/wqmlhelper_p.h" #include "private/wglobal_p.h" +#include "private/wprivateaccessor_p.h" +#include "utils/private/wvulkantrace_p.h" + #include #include #include #include +#include +#include #include #include #include +#ifdef ENABLE_VULKAN_RENDER +#include +#endif #include #include #include #include + #ifdef ENABLE_VULKAN_RENDER -#include +#include +#include #endif +extern "C" { +#define static +#include +#undef static +#include +} #include -#include + +#include +#include + +#if defined(ENABLE_VULKAN_RENDER) && QT_VERSION >= QT_VERSION_CHECK(6, 8, 0) +W_DECLARE_PRIVATE_MEMBER(QRhi_d_tag, QRhi, d, QRhiImplementation *); +#endif WAYLIB_SERVER_BEGIN_NAMESPACE @@ -38,6 +60,104 @@ struct Q_DECL_HIDDEN RhiRenderEntry { Q_GLOBAL_STATIC(QVector, s_rhiRenderBuffers) +#ifdef ENABLE_VULKAN_RENDER +static QString hex32(uint32_t value) +{ + return QStringLiteral("0x%1").arg(value, 8, 16, QLatin1Char('0')); +} + +static QString hex64(uint64_t value) +{ + return QStringLiteral("0x%1").arg(value, 16, 16, QLatin1Char('0')); +} + +static quint64 vkImageValue(VkImage image) +{ + if constexpr (std::is_pointer_v) { + return quint64(reinterpret_cast(image)); + } else { + return quint64(image); + } +} + +static QString vkImageName(VkImage image) +{ + return hex64(vkImageValue(image)); +} + +static const char *vkImageLayoutName(VkImageLayout layout) +{ + switch (layout) { + case VK_IMAGE_LAYOUT_UNDEFINED: + return "UNDEFINED"; + case VK_IMAGE_LAYOUT_GENERAL: + return "GENERAL"; + case VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL: + return "COLOR_ATTACHMENT_OPTIMAL"; + case VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL: + return "SHADER_READ_ONLY_OPTIMAL"; + case VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL: + return "TRANSFER_SRC_OPTIMAL"; + case VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL: + return "TRANSFER_DST_OPTIMAL"; + case VK_IMAGE_LAYOUT_PREINITIALIZED: + return "PREINITIALIZED"; + default: + return "UNKNOWN"; + } +} + +static QSize wlrBufferSize(wlr_buffer *buffer) +{ + return QSize(buffer->width, buffer->height); +} + +static QSize wlrTextureSize(wlr_texture *texture) +{ + return QSize(texture->width, texture->height); +} + +#ifdef ENABLE_VULKAN_RENDER +// wlroots-recorded barriers must never go into an active QRhi pass instance +// (see the checks inside WRenderHelper's barrier recording wrappers below). +static bool qtVulkanPassIsRecording(QRhiCommandBuffer *cb) +{ + if (!cb) + return false; + const auto *vkCb = static_cast(cb); + return vkCb->recordingPass != QVkCommandBuffer::NoPass; +} +#endif + +static bool getVulkanRenderBufferAttribs(wlr_renderer *renderer, + wlr_buffer *buffer, + const char *purpose, + wlr_vk_image_attribs *attribs) +{ + if (!renderer || !buffer || !wlr_renderer_is_vk(renderer)) + return false; + + if (waylib_vk_renderer_get_render_buffer_attribs(renderer, buffer, attribs)) { + qCDebug(lcWlRenderHelper) << "Got wlroots Vulkan render buffer attributes" + << "purpose" << purpose + << "buffer" << buffer + << "image" << vkImageName(attribs->image) + << "layout" << vkImageLayoutName(attribs->layout) + << "format" << hex32(attribs->format) + << "usage" << hex32(attribs->usage) + << "size" << wlrBufferSize(buffer); + return true; + } + + qCWarning(lcWlRenderHelper) << "Failed to get wlroots Vulkan render buffer attributes" + << "purpose" << purpose + << "buffer" << buffer + << "size" << wlrBufferSize(buffer); + return false; +} +#endif + +#if defined(ENABLE_VULKAN_RENDER) && QT_VERSION >= QT_VERSION_CHECK(6, 8, 0) struct Q_DECL_HIDDEN BufferData { BufferData() = default; @@ -53,11 +173,12 @@ struct Q_DECL_HIDDEN BufferData { QQuickWindowRenderTarget windowRenderTarget; bool colorPreserved = false; - inline void resetWindowRenderTarget() { + static inline void cleanupWindowRenderTarget(QQuickWindowRenderTarget &target) { +#if QT_VERSION >= QT_VERSION_CHECK(6, 8, 0) { auto it = s_rhiRenderBuffers->begin(); while (it != s_rhiRenderBuffers->end()) { - if (windowRenderTarget.rt.renderTarget == it->renderTarget) { + if (target.rt.renderTarget == it->renderTarget) { it = s_rhiRenderBuffers->erase(it); break; } @@ -65,28 +186,62 @@ struct Q_DECL_HIDDEN BufferData { } } - if (windowRenderTarget.rt.owns) - delete windowRenderTarget.rt.renderTarget; + if (target.rt.owns) + delete target.rt.renderTarget; - delete windowRenderTarget.res.texture; - delete windowRenderTarget.res.renderBuffer; - delete windowRenderTarget.res.rpDesc; + delete target.res.texture; + delete target.res.renderBuffer; + delete target.res.rpDesc; - windowRenderTarget.rt = {}; - windowRenderTarget.res = {}; - { // windowRenderTarget.implicitBuffers.reset(rhi); - delete windowRenderTarget.implicitBuffers.depthStencil; - delete windowRenderTarget.implicitBuffers.depthStencilTexture; - delete windowRenderTarget.implicitBuffers.multisampleTexture; - windowRenderTarget.implicitBuffers = {}; + target.rt = {}; + target.res = {}; + { // target.implicitBuffers.reset(rhi); + delete target.implicitBuffers.depthStencil; + delete target.implicitBuffers.depthStencilTexture; + delete target.implicitBuffers.multisampleTexture; + target.implicitBuffers = {}; } - if (windowRenderTarget.sw.owns) - delete windowRenderTarget.sw.paintDevice; + if (target.sw.owns) + delete target.sw.paintDevice; - windowRenderTarget.sw = {}; + target.sw = {}; +#else + { + auto it = s_rhiRenderBuffers->begin(); + while (it != s_rhiRenderBuffers->end()) { + if (target.renderTarget == it->renderTarget) { + it = s_rhiRenderBuffers->erase(it); + break; + } + ++it; + } + } + + if (target.owns) { + delete target.renderTarget; + delete target.rpDesc; + delete target.texture; + delete target.renderBuffer; + delete target.depthStencil; + delete target.paintDevice; + } + + target.renderTarget = nullptr; + target.rpDesc = nullptr; + target.texture = nullptr; + target.renderBuffer = nullptr; + target.depthStencil = nullptr; + target.paintDevice = nullptr; + target.owns = false; +#endif + } + + inline void resetWindowRenderTarget() { + cleanupWindowRenderTarget(windowRenderTarget); } }; +#endif class WRenderHelper::RenderTarget::Private { public: @@ -116,7 +271,9 @@ QQuickRenderTarget WRenderHelper::RenderTarget::rt() const if (!d) return {}; auto data = d->data.lock(); - return data ? data->renderTarget : QQuickRenderTarget(); + if (!data) + return {}; + return data->renderTarget; } wlr_buffer *WRenderHelper::RenderTarget::buffer() const @@ -153,6 +310,113 @@ static QRhiTextureRenderTarget::Flags rhiRenderTargetFlags(WGlobal::ColorContent : QRhiTextureRenderTarget::Flags{}; } +static std::unique_ptr newCompatibleRenderPassDescriptor( + QRhiTextureRenderTarget *renderTarget, const QByteArray &targetName) +{ +#if defined(ENABLE_VULKAN_RENDER) && QT_VERSION >= QT_VERSION_CHECK(6, 8, 0) + std::unique_ptr descriptor( + renderTarget->newCompatibleRenderPassDescriptor()); + if (!descriptor || !renderTarget->rhi() + || renderTarget->rhi()->backend() != QRhi::Vulkan) { + return descriptor; + } + + const auto flags = renderTarget->flags(); + const bool preserveColor = flags.testFlag( + QRhiTextureRenderTarget::PreserveColorContents); + const bool preserveDepthStencil = flags.testFlag( + QRhiTextureRenderTarget::PreserveDepthStencilContents); + + // Qt's offscreen Vulkan render passes leave subpassDeps empty, and Qt does + // not include them in its render-pass compatibility or pipeline-cache + // key. Vulkan, however, requires otherwise compatible render passes to + // have identical dependencies. Use the same conservative dependency for + // every Waylib-managed target with a given attachment topology, regardless + // of its load/store flags. Besides keeping reused pipelines compatible, + // this makes LOAD operations depend on the preceding attachment writes and + // layout transitions instead of Qt's write-only resource transition. + VkPipelineStageFlags attachmentStages = + VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT; + VkAccessFlags sourceAccess = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT; + VkAccessFlags destinationAccess = VK_ACCESS_COLOR_ATTACHMENT_READ_BIT + | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT; + const auto &description = renderTarget->description(); + if (description.depthTexture() || description.depthStencilBuffer()) { + attachmentStages |= VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT + | VK_PIPELINE_STAGE_LATE_FRAGMENT_TESTS_BIT; + sourceAccess |= VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT; + destinationAccess |= VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_READ_BIT + | VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT; + } + + auto *vulkanDescriptor = static_cast( + descriptor.get()); + VkSubpassDependency dependency = {}; + dependency.srcSubpass = VK_SUBPASS_EXTERNAL; + dependency.dstSubpass = 0; + dependency.srcStageMask = attachmentStages; + dependency.dstStageMask = attachmentStages; + dependency.srcAccessMask = sourceAccess; + dependency.dstAccessMask = destinationAccess; + dependency.dependencyFlags = VK_DEPENDENCY_BY_REGION_BIT; + + const auto dependencyMatches = [&dependency] ( + const VkSubpassDependency &candidate) { + return candidate.srcSubpass == dependency.srcSubpass + && candidate.dstSubpass == dependency.dstSubpass + && candidate.srcStageMask == dependency.srcStageMask + && candidate.dstStageMask == dependency.dstStageMask + && candidate.srcAccessMask == dependency.srcAccessMask + && candidate.dstAccessMask == dependency.dstAccessMask + && candidate.dependencyFlags == dependency.dependencyFlags; + }; + if (vulkanDescriptor->subpassDeps.size() == 1 + && dependencyMatches(vulkanDescriptor->subpassDeps[0])) { + return descriptor; + } + if (!vulkanDescriptor->subpassDeps.isEmpty()) { + qCWarning(lcWlRenderHelper) + << "Cannot normalize Vulkan render-pass dependencies" + << "target" << targetName + << "existingDependencyCount" + << vulkanDescriptor->subpassDeps.size(); + return {}; + } + vulkanDescriptor->subpassDeps.append(dependency); + + // The descriptor returned above already owns a native VkRenderPass. + // Clone it after extending the private Vulkan description so the clone's + // native render pass is created with the dependency. Destroying the + // temporary descriptor queues its native render pass for QRhi's normal + // deferred destruction. + std::unique_ptr synchronizedDescriptor( + vulkanDescriptor->newCompatibleRenderPassDescriptor()); + if (!synchronizedDescriptor) { + qCWarning(lcWlRenderHelper) + << "Failed to create synchronized Vulkan render pass" + << "target" << targetName + << "flags" << flags.toInt(); + return {}; + } + + if (WVulkanTrace::enabled()) { + qCDebug(lcWlRenderHelper).noquote() + << QStringLiteral("VKTRACE event=render-pass-dependency target=%1 colorLoad=%2 depthStencilLoad=%3 stages=0x%4 sourceAccess=0x%5 destinationAccess=0x%6") + .arg(QString::fromUtf8(targetName)) + .arg(preserveColor) + .arg(preserveDepthStencil) + .arg(quint32(attachmentStages), 0, 16) + .arg(quint32(sourceAccess), 0, 16) + .arg(quint32(destinationAccess), 0, 16); + } + return synchronizedDescriptor; +#else + Q_UNUSED(targetName); + return std::unique_ptr( + renderTarget->newCompatibleRenderPassDescriptor()); +#endif +} + static bool recreateRhiRenderTarget(BufferData *data, QRhiTextureRenderTarget::Flags flags) { auto renderTarget = static_cast( @@ -162,11 +426,21 @@ static bool recreateRhiRenderTarget(BufferData *data, QRhiTextureRenderTarget::F renderTarget->destroy(); renderTarget->setFlags(flags); - auto newRpDesc = renderTarget->newCompatibleRenderPassDescriptor(); + std::unique_ptr newRpDesc; +#if defined(ENABLE_VULKAN_RENDER) && QT_VERSION >= QT_VERSION_CHECK(6, 8, 0) + if (renderTarget->rhi() && renderTarget->rhi()->backend() == QRhi::Vulkan) { + newRpDesc = newCompatibleRenderPassDescriptor( + renderTarget, renderTarget->name()); + } else +#endif + { + // Keep non-Vulkan backends on QRhi's unmodified render-pass path. + newRpDesc.reset(renderTarget->newCompatibleRenderPassDescriptor()); + } if (!newRpDesc) return false; delete rpDesc; - rpDesc = newRpDesc; + rpDesc = newRpDesc.release(); renderTarget->setRenderPassDescriptor(rpDesc); return renderTarget->create(); } @@ -177,9 +451,10 @@ static bool createRhiRenderTarget(const QRhiColorAttachment &colorAttachment, int sampleCount, QRhi *rhi, QQuickWindowRenderTarget &dst, - QRhiTextureRenderTarget::Flags flags) + QRhiTextureRenderTarget::Flags flags = {}) { - std::unique_ptr depthStencil(rhi->newRenderBuffer(QRhiRenderBuffer::DepthStencil, pixelSize, sampleCount)); + std::unique_ptr depthStencil( + rhi->newRenderBuffer(QRhiRenderBuffer::DepthStencil, pixelSize, sampleCount)); if (!depthStencil->create()) { qCWarning(lcWlRenderHelper, "Failed to build depth-stencil buffer for QQuickRenderTarget"); return false; @@ -188,7 +463,23 @@ static bool createRhiRenderTarget(const QRhiColorAttachment &colorAttachment, QRhiTextureRenderTargetDescription rtDesc(colorAttachment); rtDesc.setDepthStencilBuffer(depthStencil.get()); std::unique_ptr rt(rhi->newTextureRenderTarget(rtDesc, flags)); - std::unique_ptr rp(rt->newCompatibleRenderPassDescriptor()); + std::unique_ptr rp; +#if defined(ENABLE_VULKAN_RENDER) && QT_VERSION >= QT_VERSION_CHECK(6, 8, 0) + if (rhi->backend() == QRhi::Vulkan) { + rp = newCompatibleRenderPassDescriptor( + rt.get(), QByteArrayLiteral("WaylibTextureRenderTarget")); + if (!rp) { + qCWarning(lcWlRenderHelper, + "Failed to build Vulkan render pass for QQuickRenderTarget"); + return false; + } + } else +#endif + { + // Preserve the non-Vulkan render-target path exactly as provided by + // QRhi; in particular, do not change GLES2 render-pass behavior. + rp.reset(rt->newCompatibleRenderPassDescriptor()); + } rt->setRenderPassDescriptor(rp.get()); if (!rt->create()) { @@ -204,8 +495,13 @@ static bool createRhiRenderTarget(const QRhiColorAttachment &colorAttachment, return true; } +#if QT_VERSION >= QT_VERSION_CHECK(6, 8, 0) + +#endif + bool createRhiRenderTarget(QRhi *rhi, const QQuickRenderTarget &source, QQuickWindowRenderTarget &dst, - QRhiTextureRenderTarget::Flags flags) + QRhiTextureRenderTarget::Flags rtFlags = {}, + QRhiTexture::Flags extraTextureFlags = {}) { auto rtd = QQuickRenderTargetPrivate::get(&source); @@ -213,15 +509,23 @@ bool createRhiRenderTarget(QRhi *rhi, const QQuickRenderTarget &source, QQuickWi case QQuickRenderTargetPrivate::Type::NativeTexture: { const auto format = rtd->u.nativeTexture.rhiFormat == QRhiTexture::UnknownFormat ? QRhiTexture::RGBA8 : QRhiTexture::Format(rtd->u.nativeTexture.rhiFormat); - const auto textureFlags = QRhiTexture::RenderTarget | QRhiTexture::Flags( + const auto textureFlags = QRhiTexture::RenderTarget | extraTextureFlags | QRhiTexture::Flags( +#if QT_VERSION >= QT_VERSION_CHECK(6, 8, 0) rtd->u.nativeTexture.rhiFormatFlags +#else + rtd->u.nativeTexture.rhiFlags +#endif ); std::unique_ptr texture(rhi->newTexture(format, rtd->pixelSize, rtd->sampleCount, textureFlags)); texture->setName(QByteArrayLiteral("WaylibTexture")); +#if QT_VERSION < QT_VERSION_CHECK(6, 6, 0) + if (!texture->createFrom({ rtd->u.nativeTexture.object, rtd->u.nativeTexture.layout })) +#else if (!texture->createFrom({ rtd->u.nativeTexture.object, rtd->u.nativeTexture.layoutOrState })) +#endif return false; QRhiColorAttachment att(texture.get()); - if (!createRhiRenderTarget(att, rtd->pixelSize, rtd->sampleCount, rhi, dst, flags)) + if (!createRhiRenderTarget(att, rtd->pixelSize, rtd->sampleCount, rhi, dst, rtFlags)) return false; dst.res.texture = texture.release(); return true; @@ -233,7 +537,7 @@ bool createRhiRenderTarget(QRhi *rhi, const QQuickRenderTarget &source, QQuickWi return false; } QRhiColorAttachment att(renderbuffer.get()); - if (!createRhiRenderTarget(att, rtd->pixelSize, rtd->sampleCount, rhi, dst, flags)) + if (!createRhiRenderTarget(att, rtd->pixelSize, rtd->sampleCount, rhi, dst, rtFlags)) return false; renderbuffer->setName(QByteArrayLiteral("WaylibRenderBuffer")); dst.res.renderBuffer = renderbuffer.release(); @@ -256,28 +560,55 @@ class Q_DECL_HIDDEN WRenderHelperPrivate : public WObjectPrivate , renderer(renderer) {} ~WRenderHelperPrivate() { - resetRenderBuffer(); + resetRenderBuffer(false); + cleanupRetiredRenderBuffers(true); } - void resetRenderBuffer(); + struct RetiredBuffers { + QList> buffers; + }; + + bool shouldDeferRenderBufferCleanup() const; + void resetRenderBuffer(bool defer); + void cleanupRetiredRenderBuffers(bool force); void onBufferDestroy(BufferData *data); static bool ensureRhiRenderTarget(QQuickRenderControl *rc, BufferData *data, - QRhiTextureRenderTarget::Flags flags); + QRhiTextureRenderTarget::Flags flags, + QRhiTexture::Flags extraTextureFlags = {}); W_DECLARE_PUBLIC(WRenderHelper) wlr_renderer *renderer; QList> buffers; + QList retiredBuffers; std::weak_ptr lastBuffer; QSize size; }; -void WRenderHelperPrivate::resetRenderBuffer() +bool WRenderHelperPrivate::shouldDeferRenderBufferCleanup() const { +#ifdef ENABLE_VULKAN_RENDER + return renderer && wlr_renderer_is_vk(renderer); +#else + return false; +#endif +} + +void WRenderHelperPrivate::resetRenderBuffer(bool defer) +{ + if (defer && !buffers.isEmpty()) { + retiredBuffers.append({buffers}); + } buffers.clear(); lastBuffer.reset(); } +void WRenderHelperPrivate::cleanupRetiredRenderBuffers(bool force) +{ + Q_UNUSED(force); + retiredBuffers.clear(); +} + void WRenderHelperPrivate::onBufferDestroy(BufferData *data) { // wlr_buffer_finish asserts the destroy/release listener lists are empty @@ -296,13 +627,16 @@ void WRenderHelperPrivate::onBufferDestroy(BufferData *data) } } -bool WRenderHelperPrivate::ensureRhiRenderTarget(QQuickRenderControl *rc, BufferData *data, - QRhiTextureRenderTarget::Flags flags) +bool WRenderHelperPrivate::ensureRhiRenderTarget(QQuickRenderControl *rc, + BufferData *data, + QRhiTextureRenderTarget::Flags flags, + QRhiTexture::Flags extraTextureFlags) { data->resetWindowRenderTarget(); auto rhi = rc->rhi(); auto tmp = data->renderTarget; - bool ok = createRhiRenderTarget(rhi, tmp, data->windowRenderTarget, flags); + bool ok = createRhiRenderTarget(rhi, tmp, data->windowRenderTarget, + flags, extraTextureFlags); if (!ok) return false; data->renderTarget = QQuickRenderTarget::fromRhiRenderTarget(data->windowRenderTarget.rt.renderTarget); @@ -312,6 +646,8 @@ bool WRenderHelperPrivate::ensureRhiRenderTarget(QQuickRenderControl *rc, Buffer return true; } + + WRenderHelper::WRenderHelper(wlr_renderer *renderer, QObject *parent) : QObject(parent) , WObject(*new WRenderHelperPrivate(this, renderer)) @@ -331,11 +667,259 @@ void WRenderHelper::setSize(const QSize &size) if (d->size == size) return; d->size = size; - d->resetRenderBuffer(); + d->resetRenderBuffer(d->shouldDeferRenderBufferCleanup()); Q_EMIT sizeChanged(); } +void WRenderHelper::cleanupRetiredRenderResources(bool force) +{ + W_D(WRenderHelper); + d->cleanupRetiredRenderBuffers(force); +} + +bool WRenderHelper::acquireRenderBuffer(QQuickRenderControl *rc, wlr_buffer *buffer, const char *purpose) +{ +#ifdef ENABLE_VULKAN_RENDER + W_D(WRenderHelper); + if (!d->renderer || !wlr_renderer_is_vk(d->renderer)) + return true; + + wlr_vk_image_attribs attribs = {}; + if (!getVulkanRenderBufferAttribs(d->renderer, buffer, purpose, &attribs)) + return false; + + if (!rc || !rc->rhi() || rc->rhi()->backend() != QRhi::Vulkan) { + qCWarning(lcWlRenderHelper) << "Vulkan render buffer acquire failed: missing Vulkan QRhi" + << "purpose" << purpose + << "buffer" << buffer + << "image" << vkImageName(attribs.image) + << "layout" << vkImageLayoutName(attribs.layout) + << "format" << hex32(attribs.format) + << "size" << wlrBufferSize(buffer); + return false; + } + + if (rc->rhi()->isDeviceLost() || !rc->rhi()->isRecordingFrame()) { + qCWarning(lcWlRenderHelper) << "Vulkan render buffer acquire failed: QRhi frame is not usable" + << "purpose" << purpose + << "deviceLost" << rc->rhi()->isDeviceLost() + << "recordingFrame" << rc->rhi()->isRecordingFrame() + << "buffer" << buffer + << "image" << vkImageName(attribs.image) + << "layout" << vkImageLayoutName(attribs.layout) + << "format" << hex32(attribs.format) + << "size" << wlrBufferSize(buffer); + return false; + } + + auto commandBuffer = rc->commandBuffer(); + if (!commandBuffer) { + qCWarning(lcWlRenderHelper) << "Vulkan render buffer acquire failed: missing QRhi command buffer" + << "purpose" << purpose + << "buffer" << buffer + << "image" << vkImageName(attribs.image) + << "layout" << vkImageLayoutName(attribs.layout) + << "format" << hex32(attribs.format) + << "size" << wlrBufferSize(buffer); + return false; + } + + commandBuffer->beginExternal(); + auto handles = static_cast(commandBuffer->nativeHandles()); + if (!handles || handles->commandBuffer == VK_NULL_HANDLE) { + commandBuffer->endExternal(); + qCWarning(lcWlRenderHelper) << "Vulkan render buffer acquire failed: missing native Vulkan command buffer" + << "purpose" << purpose + << "buffer" << buffer + << "image" << vkImageName(attribs.image) + << "layout" << vkImageLayoutName(attribs.layout) + << "format" << hex32(attribs.format) + << "size" << wlrBufferSize(buffer); + return false; + } + + // Queue-family ownership barriers are illegal inside a pass instance; all + // render buffer acquires must be recorded between QRhi passes. + if (Q_UNLIKELY(qtVulkanPassIsRecording(commandBuffer))) { + commandBuffer->endExternal(); + qCCritical(lcWlRenderHelper) + << "Vulkan render buffer acquire requested inside an active QRhi pass" + << "purpose" << purpose << "buffer" << buffer; + return false; + } + + const bool ok = waylib_vk_renderer_record_render_buffer_acquire( + d->renderer, buffer, handles->commandBuffer); + commandBuffer->endExternal(); + + if (!ok) { + qCWarning(lcWlRenderHelper) << "Vulkan render buffer acquire failed" + << "purpose" << purpose + << "buffer" << buffer + << "image" << vkImageName(attribs.image) + << "layout" << vkImageLayoutName(attribs.layout) + << "format" << hex32(attribs.format) + << "size" << wlrBufferSize(buffer); + return false; + } + + qCDebug(lcWlRenderHelper) << "Vulkan render buffer acquire recorded" + << "purpose" << purpose + << "buffer" << buffer + << "image" << vkImageName(attribs.image) + << "layout" << vkImageLayoutName(attribs.layout) + << "format" << hex32(attribs.format) + << "size" << wlrBufferSize(buffer); +#else + Q_UNUSED(rc); + Q_UNUSED(buffer); + Q_UNUSED(purpose); +#endif + return true; +} + +bool WRenderHelper::releaseRenderBuffer(QQuickRenderControl *rc, + wlr_buffer *buffer, + QRhiTexture *renderTargetTexture, + const char *purpose) +{ +#ifdef ENABLE_VULKAN_RENDER + W_D(WRenderHelper); + if (!d->renderer || !wlr_renderer_is_vk(d->renderer)) + return true; + + wlr_vk_image_attribs attribs = {}; + if (!getVulkanRenderBufferAttribs(d->renderer, buffer, purpose, &attribs)) + return false; + + if (!renderTargetTexture) { + qCWarning(lcWlRenderHelper) << "Vulkan render buffer release failed: missing Qt render target texture" + << "purpose" << purpose + << "buffer" << buffer + << "image" << vkImageName(attribs.image) + << "layout" << vkImageLayoutName(attribs.layout) + << "format" << hex32(attribs.format) + << "size" << wlrBufferSize(buffer); + return false; + } + + if (!rc || !rc->rhi() || rc->rhi()->backend() != QRhi::Vulkan) { + qCWarning(lcWlRenderHelper) << "Vulkan render buffer release failed: missing Vulkan QRhi" + << "purpose" << purpose + << "buffer" << buffer + << "image" << vkImageName(attribs.image) + << "layout" << vkImageLayoutName(attribs.layout) + << "format" << hex32(attribs.format) + << "size" << wlrBufferSize(buffer); + return false; + } + + if (rc->rhi()->isDeviceLost() || !rc->rhi()->isRecordingFrame()) { + qCWarning(lcWlRenderHelper) << "Vulkan render buffer release failed: QRhi frame is not usable" + << "purpose" << purpose + << "deviceLost" << rc->rhi()->isDeviceLost() + << "recordingFrame" << rc->rhi()->isRecordingFrame() + << "buffer" << buffer + << "image" << vkImageName(attribs.image) + << "layout" << vkImageLayoutName(attribs.layout) + << "format" << hex32(attribs.format) + << "size" << wlrBufferSize(buffer); + return false; + } + + auto commandBuffer = rc->commandBuffer(); + if (!commandBuffer) { + qCWarning(lcWlRenderHelper) << "Vulkan render buffer release failed: missing QRhi command buffer" + << "purpose" << purpose + << "buffer" << buffer + << "image" << vkImageName(attribs.image) + << "layout" << vkImageLayoutName(attribs.layout) + << "format" << hex32(attribs.format) + << "size" << wlrBufferSize(buffer); + return false; + } + + const auto nativeTexture = renderTargetTexture->nativeTexture(); +#if QT_VERSION < QT_VERSION_CHECK(6, 6, 0) + const auto oldLayout = VkImageLayout(nativeTexture.layout); +#else + const auto oldLayout = VkImageLayout(nativeTexture.layout); +#endif + if (oldLayout == VK_IMAGE_LAYOUT_UNDEFINED) { + qCWarning(lcWlRenderHelper) << "Vulkan render buffer release failed: Qt render target layout is undefined" + << "purpose" << purpose + << "buffer" << buffer + << "image" << vkImageName(attribs.image) + << "wlrootsLayout" << vkImageLayoutName(attribs.layout) + << "oldLayout" << vkImageLayoutName(oldLayout) + << "format" << hex32(attribs.format) + << "size" << wlrBufferSize(buffer); + return false; + } + + commandBuffer->beginExternal(); + auto handles = static_cast(commandBuffer->nativeHandles()); + if (!handles || handles->commandBuffer == VK_NULL_HANDLE) { + commandBuffer->endExternal(); + qCWarning(lcWlRenderHelper) << "Vulkan render buffer release failed: missing native Vulkan command buffer" + << "purpose" << purpose + << "buffer" << buffer + << "image" << vkImageName(attribs.image) + << "wlrootsLayout" << vkImageLayoutName(attribs.layout) + << "oldLayout" << vkImageLayoutName(oldLayout) + << "format" << hex32(attribs.format) + << "size" << wlrBufferSize(buffer); + return false; + } + + // Ownership-release barriers are illegal inside a pass instance. + if (Q_UNLIKELY(qtVulkanPassIsRecording(commandBuffer))) { + commandBuffer->endExternal(); + qCCritical(lcWlRenderHelper) + << "Vulkan render buffer release requested inside an active QRhi pass" + << "purpose" << purpose << "buffer" << buffer; + return false; + } + + const bool ok = waylib_vk_renderer_record_render_buffer_release( + d->renderer, buffer, handles->commandBuffer, oldLayout); + commandBuffer->endExternal(); + + if (!ok) { + qCWarning(lcWlRenderHelper) << "Vulkan render buffer release failed" + << "purpose" << purpose + << "buffer" << buffer + << "image" << vkImageName(attribs.image) + << "wlrootsLayout" << vkImageLayoutName(attribs.layout) + << "oldLayout" << vkImageLayoutName(oldLayout) + << "format" << hex32(attribs.format) + << "size" << wlrBufferSize(buffer); + return false; + } + + // wlroots has recorded a transition and ownership release to GENERAL. + // Keep QRhi's external-image tracker in sync so that a reused output + // buffer starts from the layout which will exist after this submission. + renderTargetTexture->setNativeLayout(VK_IMAGE_LAYOUT_GENERAL); + + qCDebug(lcWlRenderHelper) << "Vulkan render buffer release recorded" + << "purpose" << purpose + << "buffer" << buffer + << "image" << vkImageName(attribs.image) + << "wlrootsLayout" << vkImageLayoutName(attribs.layout) + << "oldLayout" << vkImageLayoutName(oldLayout) + << "format" << hex32(attribs.format) + << "size" << wlrBufferSize(buffer); +#else + Q_UNUSED(rc); + Q_UNUSED(buffer); + Q_UNUSED(renderTargetTexture); + Q_UNUSED(purpose); +#endif + return true; +} + QSGRendererInterface::GraphicsApi WRenderHelper::getGraphicsApi(QQuickRenderControl *rc) { auto d = QQuickRenderControlPrivate::get(rc); @@ -499,17 +1083,6 @@ VkTextureBuffer::VkTextureBuffer(VkInstance instance, VkDevice device, QSGTextur bool VkTextureBuffer::get_dmabuf([[maybe_unused]] struct wlr_buffer *buffer, [[maybe_unused]] wlr_dmabuf_attributes *attribs) { -// static auto vkGetInstanceProcAddr = -// reinterpret_cast(::dlsym(RTLD_DEFAULT, "vkGetInstanceProcAddr")); -// static auto vkGetMemoryFdKHR = -// reinterpret_cast(vkGetInstanceProcAddr(m_instance, "vkGetMemoryFdKHR")); -// static auto vkGetImageMemoryRequirements = -// reinterpret_cast(vkGetInstanceProcAddr(m_instance, "vkGetImageMemoryRequirements")); -// static auto vkGetImageSparseMemoryRequirements = -// reinterpret_cast(vkGetInstanceProcAddr(m_instance, "vkGetImageSparseMemoryRequirements")); -// static auto vkGetImageSubresourceLayout = -// reinterpret_cast(vkGetInstanceProcAddr(m_instance, "vkGetImageSubresourceLayout")); - // TODO return false; } @@ -693,6 +1266,11 @@ WRenderHelper::RenderTarget WRenderHelper::acquireRenderTarget(QQuickRenderContr } data->colorPreserved = needPreserve; d->lastBuffer = data; + if (d->shouldDeferRenderBufferCleanup() + && !acquireRenderBuffer(rc, buffer, + "cached-compositor-render-target")) { + return {}; + } RenderTarget result; result.d->data = data; return result; @@ -704,6 +1282,8 @@ WRenderHelper::RenderTarget WRenderHelper::acquireRenderTarget(QQuickRenderContr bufferData->colorPreserved = needPreserve; QQuickRenderTarget rt; + bool needsVulkanRenderBufferAcquire = false; + QRhiTexture::Flags renderTargetTextureFlags; if (isSoftware) { WUniquePointer texture( @@ -719,15 +1299,24 @@ WRenderHelper::RenderTarget WRenderHelper::acquireRenderTarget(QQuickRenderContr } #ifdef ENABLE_VULKAN_RENDER else if (wlr_renderer_is_vk(d->renderer)) { - // Reuse wlroots' wlr_vk_render_buffer (same VkImage as tinywl/scene), - // not a parallel dmabuf import owned by waylib. wlr_vk_image_attribs attribs = {}; - if (!waylib_vk_renderer_get_render_buffer_attribs(d->renderer, buffer, &attribs)) - return {}; - rt = QQuickRenderTarget::fromVulkanImage(attribs.image, - attribs.layout, - attribs.format, - d->size); + if (getVulkanRenderBufferAttribs(d->renderer, buffer, "new-compositor-render-target", &attribs)) { + rt = QQuickRenderTarget::fromVulkanImage(attribs.image, + attribs.layout, + attribs.format, + wlrBufferSize(buffer)); + if (attribs.usage & VK_IMAGE_USAGE_TRANSFER_SRC_BIT) + renderTargetTextureFlags |= QRhiTexture::UsedAsTransferSource; + needsVulkanRenderBufferAcquire = true; + qCDebug(lcWlRenderHelper) << "Created Qt Vulkan render target from wlroots render buffer" + << "purpose" << "new-compositor-render-target" + << "buffer" << buffer + << "image" << vkImageName(attribs.image) + << "layout" << vkImageLayoutName(attribs.layout) + << "format" << hex32(attribs.format) + << "usage" << hex32(attribs.usage) + << "size" << wlrBufferSize(buffer); + } } #endif else if (wlr_renderer_is_gles2(d->renderer)) { @@ -747,13 +1336,19 @@ WRenderHelper::RenderTarget WRenderHelper::acquireRenderTarget(QQuickRenderContr if (QSGRendererInterface::isApiRhiBased(getGraphicsApi(rc))) { if (!rt.isNull()) { // Force convert to Rhi render target - if (!d->ensureRhiRenderTarget(rc, bufferData.get(), flags)) + if (!d->ensureRhiRenderTarget(rc, bufferData.get(), flags, + renderTargetTextureFlags)) bufferData->renderTarget = {}; } if (bufferData->renderTarget.isNull()) return {}; + if (needsVulkanRenderBufferAcquire + && !acquireRenderBuffer(rc, buffer, "new-compositor-render-target")) { + return {}; + } + if (auto texture = bufferData->windowRenderTarget.res.texture) { s_rhiRenderBuffers->append({ bufferData->windowRenderTarget.rt.renderTarget, texture, bufferData->buffer.get() }); @@ -776,6 +1371,10 @@ WRenderHelper::RenderTarget WRenderHelper::acquireRenderTarget(QQuickRenderContr return result; } + + + + WRenderHelper::RenderTarget WRenderHelper::lastRenderTarget() const { W_DC(WRenderHelper); @@ -788,79 +1387,6 @@ WRenderHelper::RenderTarget WRenderHelper::lastRenderTarget() const return result; } -#ifdef ENABLE_VULKAN_RENDER -void WRenderHelper::prepareVulkanRenderTarget(QRhiCommandBuffer *cb, const RenderTarget &rt) -{ - W_D(WRenderHelper); - if (!rt.d || !cb) - return; - auto data = rt.d->data.lock(); - if (!data || !data->buffer || !d->renderer || !wlr_renderer_is_vk(d->renderer)) - return; - - cb->beginExternal(); - auto handles = static_cast(cb->nativeHandles()); - if (!handles || handles->commandBuffer == VK_NULL_HANDLE) { - cb->endExternal(); - qCWarning(lcWlRenderHelper, "Vulkan render buffer acquire: missing native command buffer"); - return; - } - - // FOREIGN_EXT -> graphics queue, same as wlroots pass.c / PR #1171. - if (!waylib_vk_renderer_record_render_buffer_acquire(d->renderer, - data->buffer.get(), - handles->commandBuffer)) { - qCWarning(lcWlRenderHelper) << "Vulkan render buffer acquire failed for" << data->buffer.get(); - } - cb->endExternal(); -} - -void WRenderHelper::finishVulkanRenderTarget(QRhiCommandBuffer *cb, const RenderTarget &rt) -{ - W_D(WRenderHelper); - if (!rt.d || !cb) - return; - auto data = rt.d->data.lock(); - if (!data || !data->buffer || !d->renderer || !wlr_renderer_is_vk(d->renderer)) - return; - - QRhiTexture *qtTexture = data->windowRenderTarget.res.texture; - if (!qtTexture) { - qCWarning(lcWlRenderHelper, "Vulkan render buffer release: missing Qt QRhiTexture"); - return; - } - - const auto native = qtTexture->nativeTexture(); - const auto oldLayout = VkImageLayout(native.layout); - if (oldLayout == VK_IMAGE_LAYOUT_UNDEFINED) { - qCWarning(lcWlRenderHelper, "Vulkan render buffer release: Qt texture layout is UNDEFINED"); - return; - } - - cb->beginExternal(); - auto handles = static_cast(cb->nativeHandles()); - if (!handles || handles->commandBuffer == VK_NULL_HANDLE) { - cb->endExternal(); - qCWarning(lcWlRenderHelper, "Vulkan render buffer release: missing native command buffer"); - return; - } - - // graphics queue -> FOREIGN_EXT + GENERAL for KMS, using Qt's real old layout. - if (!waylib_vk_renderer_record_render_buffer_release(d->renderer, - data->buffer.get(), - handles->commandBuffer, - oldLayout)) { - qCWarning(lcWlRenderHelper) << "Vulkan render buffer release failed for" << data->buffer.get(); - cb->endExternal(); - return; - } - cb->endExternal(); - - // Keep QRhi's external-image tracker in sync with the post-release layout. - qtTexture->setNativeLayout(VK_IMAGE_LAYOUT_GENERAL); -} -#endif // ENABLE_VULKAN_RENDER - static wlr_renderer *createRendererWithType(const char *type, wlr_backend *backend) { qputenv("WLR_RENDERER", type); @@ -928,6 +1454,7 @@ void WRenderHelper::setupRendererBackend(wlr_backend *testBackend) && qgetenv("QT_QUICK_BACKEND") != "rhi")) { // when environment variable Q*_BACKEND was set, should defer to // the env variable for the graphics API. + WVulkanTrace::activate(getGraphicsApi() == QSGRendererInterface::Vulkan); return; } @@ -954,18 +1481,24 @@ void WRenderHelper::setupRendererBackend(wlr_backend *testBackend) wl_display_destroy(display); } } else if (wlrRenderer == "gles2") { + qCInfo(lcWlRenderHelper) << "Using explicit WLR_RENDERER=gles2 with Qt OpenGL scene graph"; QQuickWindow::setGraphicsApi(QSGRendererInterface::OpenGL); } else if (wlrRenderer == "vulkan") { #ifdef ENABLE_VULKAN_RENDER QQuickWindow::setGraphicsApi(QSGRendererInterface::Vulkan); #else + qCCritical(lcWlRenderHelper) << "WLR_RENDERER=vulkan was requested, but Vulkan support is not enabled"; qFatal("Vulkan support is not enabled"); #endif } else if (wlrRenderer == "pixman") { + qCInfo(lcWlRenderHelper) << "Using explicit WLR_RENDERER=pixman with Qt software scene graph"; QQuickWindow::setGraphicsApi(QSGRendererInterface::Software); } else { + qCCritical(lcWlRenderHelper) << "Unknown or unsupported WLR_RENDERER:" << wlrRenderer; qFatal() << "Unknown/Unsupported wlr renderer: " << wlrRenderer; } + + WVulkanTrace::activate(getGraphicsApi() == QSGRendererInterface::Vulkan); } QSGRendererInterface::GraphicsApi WRenderHelper::probe(wlr_backend *testBackend, const QList &apiList) @@ -1021,7 +1554,7 @@ QSGRendererInterface::GraphicsApi WRenderHelper::probe(wlr_backend *testBackend, return acceptApi; } -static void updateGLTexture(QRhi *rhi, wlr_texture *handle, QSGPlainTexture *texture) { +static bool updateGLTexture(QRhi *rhi, wlr_texture *handle, QSGPlainTexture *texture, bool) { wlr_gles2_texture_attribs attribs; wlr_gles2_texture_get_attribs(handle, &attribs); QSize size(handle->width, handle->height); @@ -1034,6 +1567,7 @@ static void updateGLTexture(QRhi *rhi, wlr_texture *handle, QSGPlainTexture *tex texture->setHasAlphaChannel(attribs.has_alpha); texture->setTextureSize(size); + return true; } static inline quint64 vkimage_cast(void *image) { @@ -1045,55 +1579,200 @@ static inline quint64 vkimage_cast(void *image) { } #ifdef ENABLE_VULKAN_RENDER -static void updateVKTexture(QRhi *rhi, wlr_texture *handle, QSGPlainTexture *texture) { - wlr_vk_image_attribs attribs; +static bool updateVKTexture(QRhi *rhi, wlr_texture *handle, QSGPlainTexture *texture, + bool forceShaderReadOnlyLayout) +{ + if (!rhi || rhi->backend() != QRhi::Vulkan) + return false; + + wlr_vk_image_attribs attribs = {}; wlr_vk_texture_get_image_attribs(handle, &attribs); - QSize size(handle->width, handle->height); + const QSize size(handle->width, handle->height); + const VkImageLayout qtSampleLayout = forceShaderReadOnlyLayout + ? VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL + : attribs.layout; + + QRhiTexture::Flags mappedFlags {}; + const auto mappedFormat = QSGRhiSupport::instance()->toRhiTextureFormat(attribs.format, + &mappedFlags); + // Qt maps both packed 10-bit channel orders to QRhi::RGB10A2, but its + // Vulkan backend always creates an A2B10G10R10 view first. An A2R10 image + // without MUTABLE_FORMAT would fail before we can replace that view. + const bool requiresInexactQtView = + attribs.format == VK_FORMAT_A2R10G10B10_UNORM_PACK32; + if (mappedFormat == QRhiTexture::UnknownFormat || requiresInexactQtView) { + qCDebug(lcWlQtQuickTexture) << "Rejected unsupported wlroots Vulkan texture format" + << "wlrTexture" << handle + << "image" << vkImageName(attribs.image) + << "format" << hex32(attribs.format) + << "reason" << (requiresInexactQtView + ? "inexact-qt-view-format" + : "unknown-qrhi-format") + << "size" << size; + return false; + } + + const auto *nativeHandles = static_cast(rhi->nativeHandles()); + if (!nativeHandles || nativeHandles->dev == VK_NULL_HANDLE || !nativeHandles->inst) { + qCDebug(lcWlQtQuickTexture) << "Cannot create an exact Vulkan texture view without QRhi native handles" + << "wlrTexture" << handle + << "image" << vkImageName(attribs.image) + << "format" << hex32(attribs.format); + return false; + } + + auto *deviceFunctions = nativeHandles->inst->deviceFunctions(nativeHandles->dev); + if (!deviceFunctions) + return false; + + std::unique_ptr candidate(rhi->newTexture(mappedFormat, + size, + 1, + mappedFlags)); + if (!candidate) + return false; + + auto *vkTexture = static_cast(candidate.get()); + // createFrom() assigns an external VkImage before it creates the default + // image view. Keep ownership false even on a partial failure so Qt can + // never enqueue the wlroots-owned image for destruction. + vkTexture->owns = false; + if (!candidate->createFrom({vkimage_cast(attribs.image), qtSampleLayout})) { + qCDebug(lcWlQtQuickTexture) << "Failed to wrap wlroots Vulkan image in QRhiTexture" + << "wlrTexture" << handle + << "image" << vkImageName(attribs.image) + << "format" << hex32(attribs.format) + << "layout" << vkImageLayoutName(qtSampleLayout) + << "size" << size; + return false; + } + + const auto discardUnusedCandidate = [vkTexture, nativeHandles, deviceFunctions] { + if (vkTexture->imageView != VK_NULL_HANDLE) { + deviceFunctions->vkDestroyImageView(nativeHandles->dev, + vkTexture->imageView, + nullptr); + vkTexture->imageView = VK_NULL_HANDLE; + } + // The candidate has never entered a descriptor. Unregister it from + // QRhi after synchronously dropping its unused view, while preserving + // ownership of the external image in wlroots. + vkTexture->destroy(); + }; - texture->setTextureFromNativeTexture(rhi, - vkimage_cast(attribs.image), - attribs.layout, attribs.format, size, - {}, {}); - texture->setHasAlphaChannel(wlr_vk_texture_has_alpha(handle)); + if (vkTexture->imageView == VK_NULL_HANDLE || vkTexture->lastActiveFrameSlot != -1) { + qCDebug(lcWlQtQuickTexture) << "Cannot replace the initial Qt Vulkan texture view" + << "wlrTexture" << handle + << "image" << vkImageName(attribs.image) + << "format" << hex32(attribs.format) + << "hasInitialView" << bool(vkTexture->imageView != VK_NULL_HANDLE) + << "lastActiveFrameSlot" << vkTexture->lastActiveFrameSlot; + discardUnusedCandidate(); + return false; + } + + const bool hasAlpha = wlr_vk_texture_has_alpha(handle); + VkImageViewCreateInfo viewInfo = {}; + viewInfo.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO; + viewInfo.image = attribs.image; + viewInfo.viewType = VK_IMAGE_VIEW_TYPE_2D; + // Do not use QVkTexture::viewFormatForSampling here. Qt maps both + // A2R10G10B10 and A2B10G10R10 to one QRhi format, so only the wlroots + // format preserves the external image's real channel layout. + viewInfo.format = attribs.format; + viewInfo.components.r = VK_COMPONENT_SWIZZLE_IDENTITY; + viewInfo.components.g = VK_COMPONENT_SWIZZLE_IDENTITY; + viewInfo.components.b = VK_COMPONENT_SWIZZLE_IDENTITY; + viewInfo.components.a = hasAlpha ? VK_COMPONENT_SWIZZLE_IDENTITY + : VK_COMPONENT_SWIZZLE_ONE; + viewInfo.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; + viewInfo.subresourceRange.levelCount = 1; + viewInfo.subresourceRange.layerCount = 1; + + VkImageView samplingView = VK_NULL_HANDLE; + const VkResult viewResult = deviceFunctions->vkCreateImageView(nativeHandles->dev, + &viewInfo, + nullptr, + &samplingView); + if (viewResult != VK_SUCCESS) { + qCDebug(lcWlQtQuickTexture) << "Failed to create exact Vulkan sampling view" + << "wlrTexture" << handle + << "image" << vkImageName(attribs.image) + << "format" << hex32(attribs.format) + << "hasAlpha" << hasAlpha + << "vkResult" << viewResult; + discardUnusedCandidate(); + return false; + } + + const VkImageView initialView = vkTexture->imageView; + vkTexture->imageView = samplingView; + vkTexture->viewFormatForSampling = attribs.format; + deviceFunctions->vkDestroyImageView(nativeHandles->dev, initialView, nullptr); + + const char *bridgeClass = hasAlpha ? "exact-alpha-view" : "exact-opaque-view"; + + WVulkanTrace::qtWrap(true, handle, vkImageValue(attribs.image), attribs.format, + static_cast(mappedFormat), bridgeClass, + vkImageLayoutName(qtSampleLayout)); + texture->setTexture(candidate.release()); + WVulkanTrace::qtWrap(false, handle, vkImageValue(attribs.image), attribs.format, + static_cast(mappedFormat), bridgeClass, + vkImageLayoutName(qtSampleLayout)); + texture->setHasAlphaChannel(hasAlpha); texture->setTextureSize(size); + qCDebug(lcWlQtQuickTexture) << "Updated Qt Vulkan texture from wlroots texture" + << "wlrTexture" << handle + << "image" << vkImageName(attribs.image) + << "wlrootsLayout" << vkImageLayoutName(attribs.layout) + << "qtSampleLayout" << vkImageLayoutName(qtSampleLayout) + << "layoutPolicy" << (forceShaderReadOnlyLayout ? "shader-read-only" : "wlroots-raw") + << "format" << hex32(attribs.format) + << "viewClass" << bridgeClass + << "size" << size; + return true; } #endif -static void updateImage(QRhi *, wlr_texture *handle, QSGPlainTexture *texture) { +static bool updateImage(QRhi *, wlr_texture *handle, QSGPlainTexture *texture, bool) { auto image = wlr_pixman_texture_get_image(handle); texture->setImage(WTools::fromPixmanImage(image)); + return true; } -typedef void(*UpdateTextureFunction)(QRhi *, wlr_texture *, QSGPlainTexture *); +typedef bool(*UpdateTextureFunction)(QRhi *, wlr_texture *, QSGPlainTexture *, bool); static UpdateTextureFunction getUpdateTextFunction(wlr_texture *handle) { const auto api = WRenderHelper::getGraphicsApi(); if (api == QSGRendererInterface::OpenGL) { - Q_ASSERT(wlr_texture_is_gles2(handle)); + if (!wlr_texture_is_gles2(handle)) + return nullptr; return updateGLTexture; } #ifdef ENABLE_VULKAN_RENDER else if (api == QSGRendererInterface::Vulkan) { - Q_ASSERT(wlr_texture_is_vk(handle)); + if (!wlr_texture_is_vk(handle)) + return nullptr; return updateVKTexture; } #endif else if (api == QSGRendererInterface::Software) { - Q_ASSERT(wlr_texture_is_pixman(handle)); + if (!wlr_texture_is_pixman(handle)) + return nullptr; return updateImage; } return nullptr; } -bool WRenderHelper::makeTexture(QRhi *rhi, wlr_texture *handle, QSGPlainTexture *texture) +bool WRenderHelper::makeTexture(QRhi *rhi, wlr_texture *handle, QSGPlainTexture *texture, + bool forceVulkanShaderReadOnlyLayout) { auto updateTexture = getUpdateTextFunction(handle); if (Q_UNLIKELY(!updateTexture)) return false; - updateTexture(rhi, handle, texture); - return true; + return updateTexture(rhi, handle, texture, forceVulkanShaderReadOnlyLayout); } WRenderHelper::TextureEntry @@ -1147,10 +1826,18 @@ WRenderHelper::newTexture(wlr_allocator *allocator, wlr_renderer *renderer, qFatal("The current QRhi backend doesn't support creating texture from Vulkan image"); } - wlr_vk_image_attribs attribs; - wlr_vk_texture_get_image_attribs(texture.get(), &attribs); + wlr_vk_image_attribs attribs = {}; + if (!waylib_vk_renderer_get_render_buffer_attribs(renderer, buffer, &attribs)) { + qCWarning(lcWlRenderHelper) << "Failed to create QRhiTexture from wlroots Vulkan render buffer attributes" + << "purpose" << "internal-rhi-texture" + << "wlrBuffer" << buffer + << "size" << size; + wlr_buffer_drop(buffer); + return {}; + } - if (!rhiTexture->createFrom({vkimage_cast(attribs.image), attribs.layout})) { + if (!rhiTexture->createFrom({vkimage_cast(attribs.image), + attribs.layout})) { qCCritical(lcWlRenderHelper, "Failed to create QRhiTexture from Vulkan image"); wlr_buffer_drop(buffer); return {}; diff --git a/waylib/src/server/qtquick/wrenderhelper.h b/waylib/src/server/qtquick/wrenderhelper.h index 0ea45a3ba4..a6619ab210 100644 --- a/waylib/src/server/qtquick/wrenderhelper.h +++ b/waylib/src/server/qtquick/wrenderhelper.h @@ -10,14 +10,17 @@ #include #include +#include + QT_BEGIN_NAMESPACE class QQuickRenderControl; class QSGTexture; class QSGPlainTexture; class QRhi; +class QRhiRenderTarget; +class QRhiTexture; class QRhiCommandBuffer; QT_END_NAMESPACE - WAYLIB_SERVER_BEGIN_NAMESPACE class WRenderHelperPrivate; @@ -60,20 +63,21 @@ class WAYLIB_SERVER_EXPORT WRenderHelper : public QObject, public WObject RenderTarget acquireRenderTarget(QQuickRenderControl *rc, wlr_buffer *buffer, WGlobal::ColorContentsMode mode = WGlobal::ColorContentsMode::DontCare); + bool acquireRenderBuffer(QQuickRenderControl *rc, wlr_buffer *buffer, const char *purpose); + bool releaseRenderBuffer(QQuickRenderControl *rc, wlr_buffer *buffer, + QRhiTexture *renderTargetTexture, const char *purpose); + void cleanupRetiredRenderResources(bool force = false); RenderTarget lastRenderTarget() const; - -#ifdef ENABLE_VULKAN_RENDER - // Record wlroots render_buffer FOREIGN acquire/release around Qt RHI pass. - void prepareVulkanRenderTarget(QRhiCommandBuffer *cb, const RenderTarget &rt); - void finishVulkanRenderTarget(QRhiCommandBuffer *cb, const RenderTarget &rt); -#endif static wlr_renderer *createRenderer(wlr_backend *backend); static wlr_renderer *createRenderer(wlr_backend *backend, QSGRendererInterface::GraphicsApi api); static void setupRendererBackend(wlr_backend *testBackend = nullptr); - static QSGRendererInterface::GraphicsApi probe(wlr_backend *testBackend, const QList &apiList); + static QSGRendererInterface::GraphicsApi probe(wlr_backend *testBackend, + const QList &apiList); - static bool makeTexture(QRhi *rhi, wlr_texture *handle, QSGPlainTexture *texture); + static bool makeTexture(QRhi *rhi, wlr_texture *handle, QSGPlainTexture *texture, + bool forceVulkanShaderReadOnlyLayout = false); + static void resetVulkanFrameAliasLists(wlr_renderer *renderer); struct TextureEntry { wlr_buffer *buffer; diff --git a/waylib/src/server/qtquick/wsgtextureprovider.cpp b/waylib/src/server/qtquick/wsgtextureprovider.cpp index 1c164629e3..3ec3f3f6f3 100644 --- a/waylib/src/server/qtquick/wsgtextureprovider.cpp +++ b/waylib/src/server/qtquick/wsgtextureprovider.cpp @@ -5,78 +5,327 @@ #include "woutputrenderwindow.h" #include "wrenderhelper.h" #include "private/wglobal_p.h" +#include "utils/private/wvulkantrace_p.h" #include "wayliblogging.h" #include #include +#include #include +#include + +extern "C" { +#include +} + WAYLIB_SERVER_BEGIN_NAMESPACE +class Q_DECL_HIDDEN BufferRef +{ +public: + BufferRef() = default; + ~BufferRef() { reset(); } + + BufferRef(const BufferRef &) = delete; + BufferRef &operator=(const BufferRef &) = delete; + + BufferRef(BufferRef &&other) noexcept + { + m_buffer = std::exchange(other.m_buffer, nullptr); + } + + BufferRef &operator=(BufferRef &&other) noexcept + { + if (this == &other) + return *this; + + reset(); + m_buffer = std::exchange(other.m_buffer, nullptr); + return *this; + } + + void reset(wlr_buffer *buffer = nullptr) + { + if (m_buffer == buffer) + return; + + if (buffer) { + wlr_buffer_lock(buffer); + if (auto clientBuffer = wlr_client_buffer_get(buffer)) + clientBuffer->n_ignore_locks++; + } + + release(); + m_buffer = buffer; + } + + wlr_buffer *get() const { return m_buffer; } + +private: + void release() + { + if (!m_buffer) + return; + + if (auto clientBuffer = wlr_client_buffer_get(m_buffer)) { + Q_ASSERT(clientBuffer->n_ignore_locks > 0); + clientBuffer->n_ignore_locks--; + } + wlr_buffer_unlock(m_buffer); + m_buffer = nullptr; + } + + wlr_buffer *m_buffer = nullptr; +}; + class Q_DECL_HIDDEN WSGTextureProviderPrivate : public WObjectPrivate { public: WSGTextureProviderPrivate(WSGTextureProvider *qq, WOutputRenderWindow *window) : WObjectPrivate(qq) , window(window) + , vulkanRhi(window + && ((window->rhi() + && window->rhi()->backend() == QRhi::Vulkan) + || (!window->rhi() + && QQuickWindow::graphicsApi() + == QSGRendererInterface::Vulkan))) { qtTexture.setOwnsTexture(false); qtTexture.setFiltering(smooth ? QSGTexture::Linear : QSGTexture::Nearest); + qtTexture.setMipmapFiltering(smooth ? QSGTexture::Linear + : QSGTexture::Nearest); } ~WSGTextureProviderPrivate() { cleanTexture(); } - void cleanTexture() { - auto *oldRhiTexture = qtTexture.rhiTexture(); - if (oldRhiTexture) { - // QSGPlainTexture does not own the QRhiTexture in this provider. - qtTexture.setOwnsTexture(false); - qtTexture.setTexture(nullptr); + void scheduleCleanup(QRhiTexture *oldRhiTexture, + wlr_texture *oldTexture, + bool oldOwnsTexture, + BufferRef oldBuffer) + { + if (!oldRhiTexture && !oldTexture && !oldBuffer.get()) + return; - Q_ASSERT(window); - class TextureCleanupJob : public QRunnable + const auto cleanupToken = WVulkanTrace::cleanupScheduled(q_func(), window, + oldBuffer.get(), oldTexture); + + class TextureCleanupJob : public QRunnable + { + public: + TextureCleanupJob(QRhiTexture *rhiTexture, + wlr_texture *texture, + bool ownsTexture, + BufferRef buffer, + WVulkanTrace::CleanupToken cleanupToken, + bool deleteRhiTextureImmediately) + : rhiTexture(rhiTexture) + , texture(texture) + , ownsTexture(ownsTexture) + , buffer(std::move(buffer)) + , cleanupToken(cleanupToken) + , deleteRhiTextureImmediately(deleteRhiTextureImmediately) { - public: - TextureCleanupJob(QRhiTexture *texture) - : texture(texture) { } - void run() override { - texture->deleteLater(); + } + + ~TextureCleanupJob() override + { + cleanup(); + } + + void run() override + { + WVulkanTrace::cleanupRunning(cleanupToken); + cleanup(); + } + + private: + void cleanup() + { + if (rhiTexture) { + if (deleteRhiTextureImmediately) + delete rhiTexture; + else + rhiTexture->deleteLater(); + rhiTexture = nullptr; + } + if (ownsTexture && texture) { + wlr_texture_destroy(texture); + texture = nullptr; + ownsTexture = false; } - QRhiTexture *texture; - }; + } + + QRhiTexture *rhiTexture = nullptr; + wlr_texture *texture = nullptr; + bool ownsTexture = false; + BufferRef buffer; + WVulkanTrace::CleanupToken cleanupToken; + bool deleteRhiTextureImmediately = false; + }; + + if (window) { + const bool isVulkan = isVulkanRhi(); + window->scheduleRenderJob(new TextureCleanupJob(oldRhiTexture, + oldTexture, + oldOwnsTexture, + std::move(oldBuffer), + cleanupToken, + isVulkan), + isVulkan ? QQuickWindow::AfterSwapStage + : QQuickWindow::AfterRenderingStage); + return; + } - // Delay clean the qt rhi textures. - window->scheduleRenderJob(new TextureCleanupJob(oldRhiTexture), - QQuickWindow::AfterSynchronizingStage); + WVulkanTrace::cleanupRunning(cleanupToken); + if (oldRhiTexture) { + if (isVulkanRhi()) + delete oldRhiTexture; + else + oldRhiTexture->deleteLater(); } + if (oldOwnsTexture && oldTexture) + wlr_texture_destroy(oldTexture); + } - if (ownsTexture && texture) - wlr_texture_destroy(texture); + void cleanTexture() + { + if (!isVulkanRhi()) { + if (rhiTexture) { + Q_ASSERT(window); + class TextureCleanupJob : public QRunnable + { + public: + explicit TextureCleanupJob(QRhiTexture *texture) + : texture(texture) + { + } + + void run() override + { + texture->deleteLater(); + } + + QRhiTexture *texture; + }; + + // Preserve the established GLES2/Pixman cleanup point. + window->scheduleRenderJob(new TextureCleanupJob(rhiTexture), + QQuickWindow::AfterSynchronizingStage); + rhiTexture = nullptr; + } + + if (ownsTexture && texture) + wlr_texture_destroy(texture); + texture = nullptr; + ownsTexture = false; + return; + } + + auto oldRhiTexture = rhiTexture; + auto oldTexture = texture; + const bool oldOwnsTexture = ownsTexture; + auto oldBuffer = std::move(buffer); + + if (isVulkanRhi()) + qtTexture.setTexture(nullptr); + rhiTexture = nullptr; texture = nullptr; + ownsTexture = false; + failedBuffer = nullptr; + failedTexture = nullptr; + + scheduleCleanup(oldRhiTexture, + oldTexture, + oldOwnsTexture, + std::move(oldBuffer)); } - void flushVulkanStageIfNeeded() { -#ifdef ENABLE_VULKAN_RENDER - if (!window || !window->renderer()) - return; - auto *renderer = window->renderer(); - if (!wlr_renderer_is_vk(renderer)) - return; - // SHM uploads are recorded on wlroots' stage CB and only submitted by - // a wlroots render pass. Flush before Qt samples the image. - if (!waylib_vk_renderer_flush_stage(renderer)) { - qCWarning(lcWlQtQuickTexture) << "Failed to flush Vulkan stage CB for SHM upload"; + void adoptTexture(wlr_texture *newTexture, + bool newOwnsTexture, + BufferRef newBuffer) + { + auto oldRhiTexture = rhiTexture; + auto oldTexture = texture; + const bool oldOwnsTexture = ownsTexture; + auto oldBuffer = std::move(buffer); + + texture = newTexture; + ownsTexture = newOwnsTexture; + buffer = std::move(newBuffer); + rhiTexture = qtTexture.rhiTexture(); + failedBuffer = nullptr; + failedTexture = nullptr; + updateMipmapFiltering(); + + scheduleCleanup(oldRhiTexture, + oldTexture, + oldOwnsTexture, + std::move(oldBuffer)); + } + + bool isVulkanRhi() const + { + return vulkanRhi; + } + + void updateMipmapFiltering() + { + qtTexture.setMipmapFiltering(isVulkanRhi() + ? QSGTexture::None + : (smooth ? QSGTexture::Linear : QSGTexture::Nearest)); + } + + bool updateRhiTexture(wlr_texture *newTexture, wlr_buffer *newBuffer, + QSGPlainTexture *targetTexture = nullptr) + { + Q_ASSERT(newTexture); + if (!targetTexture) + targetTexture = &qtTexture; + const bool forceShaderReadOnlyLayout = isVulkanRhi(); + const bool ok = WRenderHelper::makeTexture(window->rhi(), + newTexture, + targetTexture, + forceShaderReadOnlyLayout); + if (Q_UNLIKELY(!ok)) { + const QSize bufferSize = newBuffer + ? QSize(newBuffer->width, + newBuffer->height) + : QSize(); + const bool repeatedFailure = failedBuffer == newBuffer + && failedTexture == newTexture; + if (!repeatedFailure) { + qCWarning(lcWlQtQuickTexture) << "Failed to make Qt texture from wlroots texture; keeping the previous texture" + << "provider" << q_func() + << "texture" << newTexture + << "buffer" << newBuffer + << "bufferSize" << bufferSize; + } else { + qCDebug(lcWlQtQuickTexture) << "Repeated Qt texture update failure; keeping the previous texture" + << "provider" << q_func() + << "texture" << newTexture + << "buffer" << newBuffer + << "bufferSize" << bufferSize; + } + failedBuffer = newBuffer; + failedTexture = newTexture; + return false; } -#endif + + failedBuffer = nullptr; + failedTexture = nullptr; + return true; } - void updateRhiTexture() { + void updateLegacyRhiTexture() + { Q_ASSERT(texture); - bool ok = WRenderHelper::makeTexture(window->rhi(), texture, &qtTexture); + const bool ok = WRenderHelper::makeTexture(window->rhi(), texture, &qtTexture); if (Q_UNLIKELY(!ok)) { qCWarning(lcWlQtQuickTexture) << "Failed to make texture:" << texture << ", width height:" << texture->width @@ -84,27 +333,31 @@ class Q_DECL_HIDDEN WSGTextureProviderPrivate : public WObjectPrivate return; } - flushVulkanStageIfNeeded(); + rhiTexture = qtTexture.rhiTexture(); } W_DECLARE_PUBLIC(WSGTextureProvider) QPointer window; + const bool vulkanRhi = false; // wlroots resources wlr_texture *texture = nullptr; bool ownsTexture = false; - wlr_buffer *buffer = nullptr; + BufferRef buffer; + wlr_buffer *legacyBuffer = nullptr; // qt resources QSGPlainTexture qtTexture; + QRhiTexture *rhiTexture = nullptr; bool smooth = true; + wlr_buffer *failedBuffer = nullptr; + wlr_texture *failedTexture = nullptr; }; WSGTextureProvider::WSGTextureProvider(WOutputRenderWindow *window) : WObject(*new WSGTextureProviderPrivate(this, window)) { - } WOutputRenderWindow *WSGTextureProvider::window() const @@ -116,54 +369,185 @@ WOutputRenderWindow *WSGTextureProvider::window() const void WSGTextureProvider::setBuffer(wlr_buffer *buffer) { W_D(WSGTextureProvider); - if (buffer == d->buffer) { - // The buffer object is not changed, but maybe the buffer's content is changed. - // So should emit textureChanged() signal too. - if (buffer && d->texture) { - d->flushVulkanStageIfNeeded(); - Q_EMIT textureChanged(); + + if (!d->isVulkanRhi()) { + if (buffer == d->legacyBuffer) { + // Preserve the established GLES2/Pixman same-buffer behavior. + // The buffer object is unchanged, but its content may have changed. + if (buffer) + Q_EMIT textureChanged(); + return; + } + + d->cleanTexture(); + d->legacyBuffer = buffer; + + if (buffer) { + Q_ASSERT(d->window); + if (auto clientBuffer = wlr_client_buffer_get(buffer)) { + // Acquire texture from client buffer. wlroots already generate texture for us if this is a client buffer. + // By the way, there is something wrong with getting texture from a client buffer using wlr_texture_from_buffer, + // See: https://gitlab.freedesktop.org/wlroots/wlroots/-/issues/3897 + // Possible patch: https://gitlab.freedesktop.org/wlroots/wlroots/-/merge_requests/4889 + d->texture = clientBuffer->texture; + d->ownsTexture = false; + } else { + d->texture = wlr_texture_from_buffer(d->window->renderer(), buffer); + d->ownsTexture = true; + } + if (Q_UNLIKELY(!d->texture)) { + qCWarning(lcWlQtQuickTexture) << "Failed to update texture from buffer:" << buffer + << ", width height:" << buffer->width + << buffer->height + << ", n_locks:" << buffer->n_locks; + } else { + d->updateLegacyRhiTexture(); + } } + + Q_EMIT textureChanged(); return; } - d->cleanTexture(); - d->buffer = buffer; - - if (buffer) { - Q_ASSERT(d->window); - if (auto clientBuffer = wlr_client_buffer_get(buffer)) { - // Acquire texture from client buffer. wlroots already generate texture for us if this is a client buffer. - // By the way, there is something wrong with getting texture from a client buffer using wlr_texture_from_buffer, - // See: https://gitlab.freedesktop.org/wlroots/wlroots/-/issues/3897 - // Possible patch: https://gitlab.freedesktop.org/wlroots/wlroots/-/merge_requests/4889 - d->texture = clientBuffer->texture; - d->ownsTexture = false; - } else { - d->texture = wlr_texture_from_buffer(d->window->renderer(), buffer); - d->ownsTexture = true; - } - if (Q_UNLIKELY(!d->texture)) { - qCWarning(lcWlQtQuickTexture) << "Failed to update texture from buffer:" << buffer - << ", width height:" << buffer->width - << buffer->height - << ", n_locks:" << buffer->n_locks; - } else { - d->updateRhiTexture(); + if (buffer == d->buffer.get()) { + if (!buffer) + return; + + if (d->rhiTexture) { + auto *clientBuffer = wlr_client_buffer_get(buffer); + const auto *clientTexture = clientBuffer + ? clientBuffer->texture + : d->texture; + if (clientTexture != d->texture) { + // The owner stayed stable but wlroots replaced its texture. + // Rebuild the Qt wrapper below instead of reusing stale native + // image state. + } else { + WVulkanTrace::providerReuse(this, d->window, buffer, d->texture); + Q_EMIT textureChanged(); + return; + } } + + // A previous Vulkan wrapper creation may have failed while the owner + // stayed unchanged. Fall through and retry instead of treating the + // missing QRhi texture as reusable state. } + if (!buffer) { + d->cleanTexture(); + Q_EMIT textureChanged(); + return; + } + + Q_ASSERT(d->window); + + BufferRef candidateBuffer; + candidateBuffer.reset(buffer); + + wlr_texture *candidateTexture = nullptr; + bool candidateOwnsTexture = false; + if (auto clientBuffer = wlr_client_buffer_get(buffer)) { + // wlroots owns and updates client textures. Qt only wraps the + // resulting VkImage for read-only sampling. + candidateTexture = clientBuffer->texture; + } else { + candidateTexture = wlr_texture_from_buffer(d->window->renderer(), buffer); + candidateOwnsTexture = true; + } + + if (Q_UNLIKELY(!candidateTexture)) { + qCWarning(lcWlQtQuickTexture) << "Failed to update texture from buffer:" << buffer + << ", width height:" << buffer->width + << buffer->height + << ", n_locks:" << buffer->n_locks; + return; + } + + WVulkanTrace::providerBind(this, d->window, candidateBuffer.get(), candidateTexture); + QSGPlainTexture candidateQtTexture; + if (!d->updateRhiTexture(candidateTexture, candidateBuffer.get(), &candidateQtTexture)) { + WVulkanTrace::providerDiscard(this, d->window, candidateTexture, + "qt-wrap-failed"); + if (candidateOwnsTexture) + wlr_texture_destroy(candidateTexture); + return; + } + + // During an active pass publish the candidate only after wlroots + // sampling ownership has been acquired. Outside a pass it will be + // acquired by the normal prepass before the next draw. Until this point + // scene-graph nodes continue to see the old, valid texture. + auto *candidateRhiTexture = candidateQtTexture.rhiTexture(); + Q_ASSERT(candidateRhiTexture); + candidateQtTexture.setOwnsTexture(false); + d->qtTexture.setTexture(candidateRhiTexture); + d->qtTexture.setHasAlphaChannel(candidateQtTexture.hasAlphaChannel()); + d->qtTexture.setTextureSize(candidateQtTexture.textureSize()); + d->adoptTexture(candidateTexture, + candidateOwnsTexture, + std::move(candidateBuffer)); + Q_EMIT textureChanged(); } void WSGTextureProvider::setTexture(wlr_texture *texture, wlr_buffer *srcBuffer) { W_D(WSGTextureProvider); + + if (!d->isVulkanRhi()) { + d->cleanTexture(); + d->texture = texture; + d->legacyBuffer = srcBuffer; + d->ownsTexture = false; + if (texture) + d->updateLegacyRhiTexture(); + + Q_EMIT textureChanged(); + return; + } + + if (texture) { + const char *rejectReason = nullptr; + auto *clientBuffer = srcBuffer ? wlr_client_buffer_get(srcBuffer) : nullptr; + if (!srcBuffer) + rejectReason = "missing-owner"; + else if (!clientBuffer) + rejectReason = "non-client-owner"; + else if (clientBuffer->texture != texture) + rejectReason = "texture-owner-mismatch"; + + if (Q_UNLIKELY(rejectReason)) { + WVulkanTrace::providerReject(this, d->window, srcBuffer, texture, + rejectReason); + qCWarning(lcWlQtQuickTexture) + << "Rejected borrowed Vulkan texture without a matching client-buffer owner; keeping the previous texture" + << "provider" << this + << "texture" << texture + << "buffer" << srcBuffer + << "reason" << rejectReason; + return; + } + + // Route borrowed Vulkan textures through the owner-driven path so the + // buffer and texture identity cannot diverge. + setBuffer(srcBuffer); + return; + } + d->cleanTexture(); d->texture = texture; - d->buffer = srcBuffer; + d->buffer.reset(srcBuffer); d->ownsTexture = false; - if (texture) - d->updateRhiTexture(); + if (texture) { + WVulkanTrace::providerBind(this, d->window, d->buffer.get(), d->texture); + if (!d->updateRhiTexture(d->texture, d->buffer.get())) { + d->cleanTexture(); + } else { + d->rhiTexture = d->qtTexture.rhiTexture(); + d->updateMipmapFiltering(); + } + } Q_EMIT textureChanged(); } @@ -192,7 +576,7 @@ wlr_texture *WSGTextureProvider::qwTexture() const wlr_buffer *WSGTextureProvider::wlrBuffer() const { W_DC(WSGTextureProvider); - return d->buffer; + return d->isVulkanRhi() ? d->buffer.get() : d->legacyBuffer; } bool WSGTextureProvider::smooth() const @@ -209,6 +593,7 @@ void WSGTextureProvider::setSmooth(bool newSmooth) d->smooth = newSmooth; d->qtTexture.setFiltering(newSmooth ? QSGTexture::Linear : QSGTexture::Nearest); + d->updateMipmapFiltering(); Q_EMIT smoothChanged(); } diff --git a/waylib/src/server/qtquick/wsgtextureprovider.h b/waylib/src/server/qtquick/wsgtextureprovider.h index 06875ef04d..e5ddda3b3e 100644 --- a/waylib/src/server/qtquick/wsgtextureprovider.h +++ b/waylib/src/server/qtquick/wsgtextureprovider.h @@ -28,6 +28,7 @@ class WAYLIB_SERVER_EXPORT WSGTextureProvider : public QSGTextureProvider, publi void invalidate(); QSGTexture *texture() const override; + bool hasTexture() const; virtual wlr_texture *qwTexture() const; virtual wlr_buffer *wlrBuffer() const; diff --git a/waylib/src/server/qtquick/wsurfaceitem.cpp b/waylib/src/server/qtquick/wsurfaceitem.cpp index 953a032a45..2b14b7ba8b 100644 --- a/waylib/src/server/qtquick/wsurfaceitem.cpp +++ b/waylib/src/server/qtquick/wsurfaceitem.cpp @@ -12,6 +12,7 @@ #include "wsurface.h" #include "wsurfaceitem_p.h" #include "wayliblogging.h" +#include "utils/private/wvulkantrace_p.h" #include @@ -24,9 +25,19 @@ #include #include #include +#include + +#include WAYLIB_SERVER_BEGIN_NAMESPACE +static bool usesVulkanRhi(WOutputRenderWindow *window) +{ + if (window && window->rhi()) + return window->rhi()->backend() == QRhi::Vulkan; + return QQuickWindow::graphicsApi() == QSGRendererInterface::Vulkan; +} + class Q_DECL_HIDDEN SubsurfaceContainer : public QQuickItem { Q_OBJECT @@ -164,7 +175,7 @@ struct Q_DECL_HIDDEN BufferRef BufferRef &operator=(const BufferRef &) = delete; BufferRef(BufferRef &&other) noexcept { - std::swap(m_buffer, other.m_buffer); + m_buffer = std::exchange(other.m_buffer, nullptr); } BufferRef &operator=(BufferRef &&other) noexcept { @@ -188,6 +199,14 @@ struct Q_DECL_HIDDEN BufferRef wlr_buffer *get() const { return m_buffer; } explicit operator bool() const { return m_buffer != nullptr; } + void take(BufferRef &other) { + if (this == &other) + return; + + release(); + m_buffer = std::exchange(other.m_buffer, nullptr); + } + private: void release() { if (m_buffer) { @@ -223,6 +242,11 @@ class Q_DECL_HIDDEN WSurfaceItemContentPrivate: public QQuickItemPrivate Q_ASSERT(!updateTextureConnection); + if (usesVulkanRhi(q->outputRenderWindow())) { + pendingBuffer.reset(); + pendingBufferValid = false; + } + if (dontCacheLastBuffer) { buffer.reset(); cleanTextureProvider(); @@ -252,8 +276,14 @@ class Q_DECL_HIDDEN WSurfaceItemContentPrivate: public QQuickItemPrivate if (!live) { // Non-live mode: defer to pendingBuffer pendingBuffer.reset(newBuffer); + if (usesVulkanRhi(q->outputRenderWindow())) + pendingBufferValid = true; } else { // Live mode: update buffer immediately + if (usesVulkanRhi(q->outputRenderWindow())) { + pendingBuffer.reset(); + pendingBufferValid = false; + } buffer.reset(newBuffer); q->update(); } @@ -263,6 +293,17 @@ class Q_DECL_HIDDEN WSurfaceItemContentPrivate: public QQuickItemPrivate surface->scheduleFrameIfNeeded(); }); + if (usesVulkanRhi(q->outputRenderWindow())) { + // The SurfaceItem can be created after the client has already + // committed its first buffer (notably for prelaunched XWayland + // surfaces). Seed the visible buffer instead of waiting for a + // future commit that may never arrive. + pendingBuffer.reset(); + pendingBufferValid = false; + buffer.reset(surface->buffer()); + q->update(); + } + updateFrameDoneConnection(); updateSurfaceState(); rendered = true; @@ -327,8 +368,18 @@ class Q_DECL_HIDDEN WSurfaceItemContentPrivate: public QQuickItemPrivate } inline void swapBufferIfNeeded() { - if (pendingBuffer) - buffer = std::move(pendingBuffer); + W_Q(WSurfaceItemContent); + if (!usesVulkanRhi(q->outputRenderWindow())) { + if (pendingBuffer) + buffer = std::move(pendingBuffer); + return; + } + + if (!pendingBufferValid) + return; + + buffer.take(pendingBuffer); + pendingBufferValid = false; } inline void setDevicePixelRatio(qreal dpr) { @@ -361,6 +412,7 @@ class Q_DECL_HIDDEN WSurfaceItemContentPrivate: public QQuickItemPrivate mutable WSGTextureProvider *textureProvider = nullptr; BufferRef buffer; BufferRef pendingBuffer; + bool pendingBufferValid = false; mutable QMetaObject::Connection updateTextureConnection; bool dontCacheLastBuffer = false; bool live = true; @@ -457,7 +509,13 @@ WSGTextureProvider *WSurfaceItemContent::wTextureProvider() const connect(this, &WSurfaceItemContent::smoothChanged, d->textureProvider, &WSGTextureProvider::setSmooth); - if (d->surface) { + if (usesVulkanRhi(w)) { + // Keep the visible buffer and its wlroots texture as one + // inseparable ownership tuple. This also preserves NonLive's + // frozen buffer instead of pairing it with the latest surface + // texture. + d->textureProvider->setBuffer(d->buffer.get()); + } else if (d->surface) { if (auto texture = wlr_surface_get_texture(d->surface->handle())) { d->textureProvider->setTexture(texture, d->buffer.get()); } else { @@ -485,6 +543,16 @@ void WSurfaceItemContent::setCacheLastBuffer(bool newCacheLastBuffer) if (d->dontCacheLastBuffer == !newCacheLastBuffer) return; d->dontCacheLastBuffer = !newCacheLastBuffer; + + if (usesVulkanRhi(outputRenderWindow()) + && d->dontCacheLastBuffer && !d->surface) { + d->pendingBuffer.reset(); + d->pendingBufferValid = false; + d->buffer.reset(); + d->cleanTextureProvider(); + update(); + } + Q_EMIT cacheLastBufferChanged(); } @@ -579,6 +647,7 @@ class Q_DECL_HIDDEN WSGRenderFootprintNode: public QSGRenderNode return NoExternalRendering | BoundedRectRendering | DepthAwareRendering | OpaqueRendering; } +public: QPointer m_owner; }; @@ -1651,13 +1720,21 @@ void WSurfaceItemContentPrivate::cleanTextureProvider() public: WSurfaceItemContentCleanupJob(QObject *object) : m_object(object) { } void run() override { - delete m_object; + delete m_object.data(); } - QObject *m_object; + QPointer m_object; }; - // Delay clean the textures on the next render after. - window->scheduleRenderJob(new WSurfaceItemContentCleanupJob(textureProvider), + auto *provider = textureProvider; + // If scene graph teardown wins the race with the scheduled job, + // delete the provider while QRhi is still alive. The job's + // QPointer then becomes null and is harmless when discarded. + QObject::connect(window, &QQuickWindow::sceneGraphInvalidated, + provider, [provider] { delete provider; }, + Qt::DirectConnection); + + // Delay clean the textures until rendering has finished. + window->scheduleRenderJob(new WSurfaceItemContentCleanupJob(provider), QQuickWindow::AfterRenderingStage); } else { delete textureProvider; diff --git a/waylib/src/server/utils/private/wvulkantrace_p.h b/waylib/src/server/utils/private/wvulkantrace_p.h new file mode 100644 index 0000000000..c5b2137160 --- /dev/null +++ b/waylib/src/server/utils/private/wvulkantrace_p.h @@ -0,0 +1,109 @@ +// Copyright (C) 2026 UnionTech Software Technology Co., Ltd. +// SPDX-License-Identifier: Apache-2.0 OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#pragma once + +#include +#include + +#include + +struct wlr_output_event_present; + +WAYLIB_SERVER_BEGIN_NAMESPACE + +class WOutputRenderWindow; + +namespace WVulkanTrace { + +enum class FrameStage { + Idle, + FrameBegin, + Polish, + BeginFrame, + Sync, + Recording, + AfterRenderingJobs, + ReleaseRenderBuffers, + BeforeEndFrame, + AfterEndFrame, + OutputCommit, + RenderEnd, +}; + +enum class SampleDisposition { + Prepared, + PrepareFailed, + CurrentRenderTarget, + InvalidProvider, + DuplicateTexture, +}; + +struct CleanupToken { + quint64 frameId = 0; + quint64 sampleId = 0; + quint64 clientBufferId = 0; + quint64 sourceBufferId = 0; + quintptr sourceBuffer = 0; + quintptr window = 0; + quintptr provider = 0; + quintptr texture = 0; + bool logLifecycle = false; +}; + +void activate(bool vulkanActive); +bool enabled(); + +void surfaceCommit(const void *surface, qint64 pid, quint64 surfaceSeq, + quint32 committedState, wlr_buffer *clientBuffer); +void providerBind(const void *provider, WOutputRenderWindow *window, + wlr_buffer *clientBuffer, wlr_texture *texture); +void providerReuse(const void *provider, WOutputRenderWindow *window, + wlr_buffer *clientBuffer, wlr_texture *texture); +void providerReject(const void *provider, WOutputRenderWindow *window, + wlr_buffer *clientBuffer, wlr_texture *texture, + const char *reason); +void providerDiscard(const void *provider, WOutputRenderWindow *window, + wlr_texture *texture, + const char *reason); +CleanupToken cleanupScheduled(const void *provider, WOutputRenderWindow *window, + wlr_buffer *clientBuffer, wlr_texture *texture); +void cleanupRunning(const CleanupToken &token); + +void qtWrap(bool begin, wlr_texture *texture, quint64 image, + quint32 vkFormat, int rhiFormat, const char *bridgeClass, + const char *layout); + +void beginFrame(WOutputRenderWindow *window); +void setFrameStage(WOutputRenderWindow *window, FrameStage stage); +void endFrame(WOutputRenderWindow *window, qsizetype committedOutputCount); +void windowDestroyed(WOutputRenderWindow *window); + +void beginPass(WOutputRenderWindow *window, const void *renderer, + wlr_buffer *targetBuffer, const char *purpose, + int sourceIndex, qsizetype providerCount); +void sampleDisposition(WOutputRenderWindow *window, const void *provider, + wlr_texture *texture, + SampleDisposition disposition, qint64 elapsedUsec = 0); +void sampleDynamicDisposition(WOutputRenderWindow *window, + wlr_texture *texture, + SampleDisposition disposition, + qint64 elapsedUsec = 0); +void sampleFinished(WOutputRenderWindow *window, + wlr_texture *texture, bool ok, + qint64 elapsedUsec); +void surfaceFootprint(WOutputRenderWindow *window, const void *provider, + wlr_texture *texture, const void *surface); +void endPass(WOutputRenderWindow *window, bool ok); + +void outputCommitState(wlr_output *output, quint32 committedState); +void outputCommitted(WOutputRenderWindow *window, + wlr_output *output, + quint32 sequenceBefore, bool ok); +void outputPresented(wlr_output *output, + const wlr_output_event_present *event); +void outputDestroyed(wlr_output *output); + +} // namespace WVulkanTrace + +WAYLIB_SERVER_END_NAMESPACE diff --git a/waylib/src/server/utils/wbufferdumper.cpp b/waylib/src/server/utils/wbufferdumper.cpp index 6987af3b14..1a4a1ea603 100644 --- a/waylib/src/server/utils/wbufferdumper.cpp +++ b/waylib/src/server/utils/wbufferdumper.cpp @@ -5,14 +5,15 @@ #include "wtools.h" #include "wayliblogging.h" -#include - +#include #include +#include + WAYLIB_SERVER_BEGIN_NAMESPACE -WBufferDumper::DumpResult WBufferDumper::dumpBufferToImage(wlr_buffer *buffer, - wlr_renderer *renderer, +WBufferDumper::DumpResult WBufferDumper::dumpBufferToImage(wlr_buffer *buffer, + wlr_renderer *renderer, QImage &outputImage) { if (!buffer || !renderer) { @@ -20,21 +21,32 @@ WBufferDumper::DumpResult WBufferDumper::dumpBufferToImage(wlr_buffer *buffer, return DumpResult::InvalidBuffer; } - wlr_texture *texture = wlr_texture_from_buffer(renderer, buffer); + WUniquePointer texture(wlr_texture_from_buffer(renderer, buffer)); if (!texture) { - qCWarning(lcWlBufferDumper) << "Failed to create texture from buffer"; + qCWarning(lcWlBufferDumper) << "Failed to create texture from buffer" + << "buffer" << buffer + << "renderer" << renderer; return DumpResult::TextureCreationFailed; } - uint32_t format = wlr_texture_preferred_read_format(texture); - + uint32_t format = wlr_texture_preferred_read_format(texture.get()); + QImage::Format qImageFormat = WTools::toImageFormat(format); if (qImageFormat == QImage::Format_Invalid) { - wlr_texture_destroy(texture); + qCWarning(lcWlBufferDumper) << "Unsupported read format for buffer dump" + << "format" << format + << "buffer" << buffer; return DumpResult::UnsupportedFormat; } outputImage = QImage(texture->width, texture->height, qImageFormat); + if (outputImage.isNull()) { + qCWarning(lcWlBufferDumper) << "Failed to allocate image for buffer dump" + << "size" << QSize(texture->width, texture->height) + << "format" << qImageFormat; + return DumpResult::TextureReadFailed; + } + uint32_t stride = outputImage.bytesPerLine(); wlr_texture_read_pixels_options options = {}; @@ -42,18 +54,19 @@ WBufferDumper::DumpResult WBufferDumper::dumpBufferToImage(wlr_buffer *buffer, options.format = format; options.stride = stride; - if (!wlr_texture_read_pixels(texture, &options)) { - qCWarning(lcWlBufferDumper) << "Failed to read pixels from texture"; - wlr_texture_destroy(texture); + if (!wlr_texture_read_pixels(texture.get(), &options)) { + qCWarning(lcWlBufferDumper) << "Failed to read pixels from texture" + << "texture" << texture.get() + << "buffer" << buffer + << "format" << format + << "stride" << stride; return DumpResult::TextureReadFailed; } - wlr_texture_destroy(texture); - return DumpResult::Success; } -WBufferDumper::DumpResult WBufferDumper::dumpBufferToFile(wlr_buffer *buffer, +WBufferDumper::DumpResult WBufferDumper::dumpBufferToFile(wlr_buffer *buffer, wlr_renderer *renderer, const QString &filePath) { diff --git a/waylib/src/server/utils/wbufferdumper.h b/waylib/src/server/utils/wbufferdumper.h index bc778922eb..ed62a64bab 100644 --- a/waylib/src/server/utils/wbufferdumper.h +++ b/waylib/src/server/utils/wbufferdumper.h @@ -5,6 +5,7 @@ #include #include + #include #include diff --git a/waylib/src/server/utils/wvulkantrace.cpp b/waylib/src/server/utils/wvulkantrace.cpp new file mode 100644 index 0000000000..eb20474e6b --- /dev/null +++ b/waylib/src/server/utils/wvulkantrace.cpp @@ -0,0 +1,1408 @@ +// Copyright (C) 2026 UnionTech Software Technology Co., Ltd. +// SPDX-License-Identifier: Apache-2.0 OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#include "private/wvulkantrace_p.h" + +#include "wayliblogging.h" +#include "woutputrenderwindow.h" +#include "wscoplistener.h" + +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include +#include +#include +#include +#include +#include + +WAYLIB_SERVER_BEGIN_NAMESPACE + +namespace WVulkanTrace { +namespace { + +constexpr qsizetype MaxTrackedOutputCommits = 32; + +struct BufferRecord { + quint64 sourceBufferId = 0; + quint64 generation = 0; + quint64 releaseSequence = 0; + quint64 lastFrameId = 0; + quint64 lastDrawFrameId = 0; + quint64 lastSampleId = 0; + quintptr lastWindow = 0; + quintptr lastDrawWindow = 0; + quintptr lastSurface = 0; + qint64 pid = 0; + quint64 surfaceSequence = 0; + int width = 0; + int height = 0; + quint32 format = 0; + quint64 modifier = 0; + int planeCount = 0; + QByteArray bufferType = QByteArrayLiteral("other"); + bool signatureLogged = false; + bool qtWrapLogged = false; + bool cleanupLogged = false; + std::array offsets {}; + std::array strides {}; +}; + +struct ClientBufferRecord { + quint64 clientBufferId = 0; + quint64 sourceBufferId = 0; + quintptr sourceBuffer = 0; +}; + +struct ProviderRecord { + quint64 sampleId = 0; + quint64 clientBufferId = 0; + quint64 sourceBufferId = 0; + quintptr sourceBuffer = 0; + quintptr window = 0; + quintptr provider = 0; + quintptr texture = 0; + bool logQtWrap = false; + bool reuseLogged = false; +}; + +struct PassRecord { + quint64 passId = 0; + quintptr renderer = 0; + quintptr targetBuffer = 0; + QByteArray purpose; + int sourceIndex = -1; + qsizetype providerCount = 0; + QSet preparedTextures; + QHash providerDispositions; + int prepareCount = 0; + int prepareFailureCount = 0; + int prepareMetadataMissCount = 0; + int finishCount = 0; + int finishFailureCount = 0; + int finishMetadataMissCount = 0; + int footprintCount = 0; + int footprintMetadataMissCount = 0; + int bindingChangeCount = 0; + int missingProviderCount = 0; + int drawAfterPrepareFailureCount = 0; + int currentTargetFeedbackCount = 0; + int invalidProviderDrawCount = 0; +}; + +struct WindowRecord { + quint64 frameId = 0; + FrameStage stage = FrameStage::Idle; + QElapsedTimer frameTimer; + QElapsedTimer endFrameTimer; + qint64 endFrameUsec = 0; + QVector passStack; + QSet bufferIds; + int passCount = 0; + int prepareCount = 0; + int prepareFailureCount = 0; + int finishCount = 0; + int finishFailureCount = 0; + int footprintCount = 0; + int missingPrepareCount = 0; + int releaseBeforeEndFrameCandidateCount = 0; +}; + +struct OutputCommitRecord { + quint64 frameId = 0; + quint32 committedState = 0; +}; + +QMutex s_mutex; +QHash s_sourceBuffers; +QHash s_clientBuffers; +QHash s_providers; +QHash s_textures; +QHash s_windows; +QHash> s_outputCommits; +QHash> s_outputCommitStates; +std::unordered_map> s_sourceListeners; +std::unordered_map> s_clientListeners; + +std::atomic_bool s_active { false }; +std::atomic s_nextFrameId { 1 }; +std::atomic s_nextPassId { 1 }; +std::atomic s_nextBufferId { 1 }; +std::atomic s_nextSampleId { 1 }; + +QString pointerName(quintptr value) +{ + return QStringLiteral("0x%1").arg(value, QT_POINTER_SIZE * 2, 16, QLatin1Char('0')); +} + +QString hex32(quint32 value) +{ + return QStringLiteral("0x%1").arg(value, 8, 16, QLatin1Char('0')); +} + +QString hex64(quint64 value) +{ + return QStringLiteral("0x%1").arg(value, 16, 16, QLatin1Char('0')); +} + +const char *stageName(FrameStage stage) +{ + switch (stage) { + case FrameStage::Idle: + return "idle"; + case FrameStage::FrameBegin: + return "frame-begin"; + case FrameStage::Polish: + return "polish"; + case FrameStage::BeginFrame: + return "begin-frame"; + case FrameStage::Sync: + return "sync"; + case FrameStage::Recording: + return "recording"; + case FrameStage::AfterRenderingJobs: + return "after-rendering-jobs"; + case FrameStage::ReleaseRenderBuffers: + return "release-render-buffers"; + case FrameStage::BeforeEndFrame: + return "before-end-frame"; + case FrameStage::AfterEndFrame: + return "after-end-frame"; + case FrameStage::OutputCommit: + return "output-commit"; + case FrameStage::RenderEnd: + return "render-end"; + } + return "unknown"; +} + +const char *dispositionName(SampleDisposition disposition) +{ + switch (disposition) { + case SampleDisposition::Prepared: + return "prepared"; + case SampleDisposition::PrepareFailed: + return "prepare-failed"; + case SampleDisposition::CurrentRenderTarget: + return "current-render-target"; + case SampleDisposition::InvalidProvider: + return "invalid-provider"; + case SampleDisposition::DuplicateTexture: + return "duplicate-texture"; + } + return "unknown"; +} + +wlr_buffer *sourceBufferFor(wlr_buffer *clientBuffer, bool createWrapper) +{ + if (!clientBuffer) + return nullptr; + + auto *client = wlr_client_buffer_get(clientBuffer); + if (client) { + if (auto *source = client->source) + return source; + // The producer may already have destroyed its wl_buffer. The locked + // client buffer remains a useful stable identity for lifetime tracing. + return clientBuffer; + } + + Q_UNUSED(createWrapper); + return clientBuffer; +} + +void removeClientBuffer(quintptr key) +{ + std::unique_ptr listeners; + { + QMutexLocker locker(&s_mutex); + s_clientBuffers.remove(key); + auto it = s_clientListeners.find(key); + if (it != s_clientListeners.end()) { + listeners = std::move(it->second); + s_clientListeners.erase(it); + } + } +} + +void sourceReleased(quintptr sourceKey, wlr_buffer *sourceBuffer) +{ + QString message; + bool candidate = false; + { + QMutexLocker locker(&s_mutex); + auto sourceIt = s_sourceBuffers.find(sourceKey); + if (sourceIt == s_sourceBuffers.end()) + return; + + auto &source = sourceIt.value(); + ++source.releaseSequence; + + bool dropped = true; + size_t lockCount = 0; + if (sourceBuffer) { + dropped = sourceBuffer->dropped; + lockCount = sourceBuffer->n_locks; + } + + FrameStage stage = FrameStage::Idle; + auto windowIt = s_windows.find(source.lastDrawWindow); + if (windowIt != s_windows.end()) { + auto &window = windowIt.value(); + stage = window.stage; + candidate = !dropped + && source.lastDrawFrameId != 0 + && source.lastDrawFrameId == window.frameId + && stage != FrameStage::AfterEndFrame + && stage != FrameStage::OutputCommit + && stage != FrameStage::RenderEnd + && stage != FrameStage::Idle; + if (candidate) + ++window.releaseBeforeEndFrameCandidateCount; + } + + message = QStringLiteral("VKTRACE event=source-release-emitted frame=%1 stage=%2 sourceBufferId=%3 sourceBuffer=%4 releaseSeq=%5 sampleId=%6 lastDrawFrame=%7 dropped=%8 locks=%9 releaseBeforeEndFrameCandidate=%10") + .arg(source.lastFrameId) + .arg(QLatin1StringView(stageName(stage))) + .arg(source.sourceBufferId) + .arg(pointerName(sourceKey)) + .arg(source.releaseSequence) + .arg(source.lastSampleId) + .arg(source.lastDrawFrameId) + .arg(dropped) + .arg(lockCount) + .arg(candidate); + } + + if (candidate) + qCWarning(lcWlQtQuickTexture).noquote() << message; + else + qCDebug(lcWlQtQuickTexture).noquote() << message; +} + +void sourceDestroyed(quintptr sourceKey) +{ + QString message; + bool tracked = false; + std::unique_ptr listeners; + { + QMutexLocker locker(&s_mutex); + auto sourceIt = s_sourceBuffers.find(sourceKey); + if (sourceIt != s_sourceBuffers.end()) { + message = QStringLiteral("VKTRACE event=source-destroy sourceBufferId=%1 sourceBuffer=%2 releaseSeq=%3") + .arg(sourceIt->sourceBufferId) + .arg(pointerName(sourceKey)) + .arg(sourceIt->releaseSequence); + s_sourceBuffers.erase(sourceIt); + tracked = true; + } + auto listenerIt = s_sourceListeners.find(sourceKey); + if (listenerIt != s_sourceListeners.end()) { + listeners = std::move(listenerIt->second); + s_sourceListeners.erase(listenerIt); + } + } + if (tracked) + qCDebug(lcWlQtQuickTexture).noquote() << message; +} + +bool registerBuffers(wlr_buffer *clientBuffer, bool createSourceWrapper, + quint64 *clientBufferId, quint64 *sourceBufferId, + quintptr *sourceKey) +{ + if (clientBufferId) + *clientBufferId = 0; + if (sourceBufferId) + *sourceBufferId = 0; + if (sourceKey) + *sourceKey = 0; + if (!clientBuffer) + return false; + + const quintptr clientKey = reinterpret_cast(clientBuffer); + if (!createSourceWrapper) { + QMutexLocker locker(&s_mutex); + const auto client = s_clientBuffers.value(clientKey); + if (!client.clientBufferId || !client.sourceBufferId) + return false; + if (clientBufferId) + *clientBufferId = client.clientBufferId; + if (sourceBufferId) + *sourceBufferId = client.sourceBufferId; + if (sourceKey) + *sourceKey = client.sourceBuffer; + return true; + } + + auto *source = sourceBufferFor(clientBuffer, true); + if (!source) + return false; + + wlr_dmabuf_attributes dmabuf {}; + wlr_shm_attributes shm {}; + const bool isDmabuf = wlr_buffer_get_dmabuf(source, &dmabuf); + const bool isShm = !isDmabuf && wlr_buffer_get_shm(source, &shm); + + const quintptr localSourceKey = reinterpret_cast(source); + bool connectSourceSignals = false; + quint64 localSourceId = 0; + { + QMutexLocker locker(&s_mutex); + auto it = s_sourceBuffers.find(localSourceKey); + if (it == s_sourceBuffers.end()) { + BufferRecord record; + record.sourceBufferId = s_nextBufferId.fetch_add(1, std::memory_order_relaxed); + if (isDmabuf) { + record.bufferType = QByteArrayLiteral("dmabuf"); + record.width = dmabuf.width; + record.height = dmabuf.height; + record.format = dmabuf.format; + record.modifier = dmabuf.modifier; + record.planeCount = dmabuf.n_planes; + for (int i = 0; i < dmabuf.n_planes && i < WLR_DMABUF_MAX_PLANES; ++i) { + record.offsets[i] = dmabuf.offset[i]; + record.strides[i] = dmabuf.stride[i]; + } + } else if (isShm) { + record.bufferType = QByteArrayLiteral("shm"); + record.width = shm.width; + record.height = shm.height; + record.format = shm.format; + record.modifier = DRM_FORMAT_MOD_INVALID; + record.planeCount = 1; + record.offsets[0] = quint32(shm.offset); + record.strides[0] = quint32(shm.stride); + } else { + record.width = source->width; + record.height = source->height; + } + localSourceId = record.sourceBufferId; + s_sourceBuffers.insert(localSourceKey, record); + connectSourceSignals = true; + } else { + localSourceId = it->sourceBufferId; + } + } + + if (connectSourceSignals) { + auto listeners = std::make_unique(); + listeners->add(&source->events.release, + [localSourceKey, source] (void *) { + sourceReleased(localSourceKey, source); + }); + listeners->add(&source->events.destroy, [localSourceKey] (void *) { + sourceDestroyed(localSourceKey); + }); + QMutexLocker locker(&s_mutex); + s_sourceListeners.emplace(localSourceKey, std::move(listeners)); + } + + bool connectClientDestroy = false; + quint64 localClientId = 0; + { + QMutexLocker locker(&s_mutex); + auto it = s_clientBuffers.find(clientKey); + if (it == s_clientBuffers.end()) { + ClientBufferRecord record; + record.clientBufferId = s_nextBufferId.fetch_add(1, std::memory_order_relaxed); + record.sourceBufferId = localSourceId; + record.sourceBuffer = localSourceKey; + localClientId = record.clientBufferId; + s_clientBuffers.insert(clientKey, record); + connectClientDestroy = true; + } else { + localClientId = it->clientBufferId; + it->sourceBufferId = localSourceId; + it->sourceBuffer = localSourceKey; + } + } + if (connectClientDestroy) { + auto listeners = std::make_unique(); + listeners->add(&clientBuffer->events.destroy, [clientKey] (void *) { + removeClientBuffer(clientKey); + }); + QMutexLocker locker(&s_mutex); + s_clientListeners.emplace(clientKey, std::move(listeners)); + } + + if (clientBufferId) + *clientBufferId = localClientId; + if (sourceBufferId) + *sourceBufferId = localSourceId; + if (sourceKey) + *sourceKey = localSourceKey; + return true; +} + +QString formatName(quint32 format) +{ + char *name = drmGetFormatName(format); + if (!name) + return QStringLiteral("unknown"); + const QString result = QString::fromLatin1(name); + free(name); + return result; +} + +QString modifierName(quint64 modifier) +{ + char *name = drmGetFormatModifierName(modifier); + if (!name) + return QStringLiteral("unknown"); + const QString result = QString::fromLatin1(name); + free(name); + return result; +} + +ProviderRecord providerRecordForTexture(quintptr texture) +{ + QMutexLocker locker(&s_mutex); + return s_textures.value(texture); +} + +} // namespace + +void activate(bool vulkanActive) +{ + static const bool requested = qEnvironmentVariableIntValue("WAYLIB_VULKAN_TRACE") != 0; + if (!requested || !vulkanActive) + return; + + bool expected = false; + if (!s_active.compare_exchange_strong(expected, true, std::memory_order_relaxed)) + return; + + qCDebug(lcWlRenderHelper).noquote() + << QStringLiteral("VKTRACE event=trace-start pid=%1") + .arg(QCoreApplication::applicationPid()); +} + +bool enabled() +{ + return s_active.load(std::memory_order_relaxed); +} + +void surfaceCommit(const void *surface, qint64 pid, quint64 surfaceSeq, + quint32 committedState, wlr_buffer *clientBuffer) +{ + if (!enabled() || !(committedState & WLR_SURFACE_STATE_BUFFER)) + return; + + if (!clientBuffer) { + qCDebug(lcWlSurface).noquote() + << QStringLiteral("VKTRACE event=surface-commit surface=%1 pid=%2 surfaceSeq=%3 committed=%4 generation=0 clientBufferId=0 clientBuffer=0x0000000000000000 sourceBufferId=0 sourceBuffer=0x0000000000000000 texture=0x0000000000000000 bufferType=none") + .arg(pointerName(reinterpret_cast(surface))) + .arg(pid) + .arg(surfaceSeq) + .arg(hex32(committedState)); + return; + } + + quint64 clientId = 0; + quint64 sourceId = 0; + quintptr sourceKey = 0; + if (!registerBuffers(clientBuffer, true, &clientId, &sourceId, &sourceKey)) + return; + + quintptr texture = 0; + if (auto *client = wlr_client_buffer_get(clientBuffer)) + texture = reinterpret_cast(client->texture); + + quint64 generation = 0; + QByteArray bufferType; + { + QMutexLocker locker(&s_mutex); + auto &source = s_sourceBuffers[sourceKey]; + generation = ++source.generation; + source.lastSurface = reinterpret_cast(surface); + source.pid = pid; + source.surfaceSequence = surfaceSeq; + bufferType = source.bufferType; + } + + qCDebug(lcWlSurface).noquote() + << QStringLiteral("VKTRACE event=surface-commit surface=%1 pid=%2 surfaceSeq=%3 committed=%4 generation=%5 clientBufferId=%6 clientBuffer=%7 sourceBufferId=%8 sourceBuffer=%9 texture=%10 bufferType=%11") + .arg(pointerName(reinterpret_cast(surface))) + .arg(pid) + .arg(surfaceSeq) + .arg(hex32(committedState)) + .arg(generation) + .arg(clientId) + .arg(pointerName(reinterpret_cast(clientBuffer))) + .arg(sourceId) + .arg(pointerName(sourceKey)) + .arg(pointerName(texture)) + .arg(QString::fromLatin1(bufferType)); +} + +void providerBind(const void *provider, WOutputRenderWindow *window, + wlr_buffer *clientBuffer, wlr_texture *texture) +{ + if (!enabled() || !provider || !window || !clientBuffer || !texture) + return; + + quint64 clientId = 0; + quint64 sourceId = 0; + quintptr sourceKey = 0; + if (!registerBuffers(clientBuffer, false, &clientId, &sourceId, &sourceKey)) + return; + + const auto *ownerClientBuffer = wlr_client_buffer_get(clientBuffer); + const char *ownerStatus = !ownerClientBuffer + ? "provider-owned" + : (ownerClientBuffer->texture == texture + ? "matched" + : "mismatch"); + + ProviderRecord providerRecord; + BufferRecord sourceRecord; + FrameStage stage = FrameStage::Idle; + quint64 frameId = 0; + { + QMutexLocker locker(&s_mutex); + auto sourceIt = s_sourceBuffers.find(sourceKey); + if (sourceIt == s_sourceBuffers.end()) + return; + + providerRecord.sampleId = s_nextSampleId.fetch_add(1, std::memory_order_relaxed); + providerRecord.clientBufferId = clientId; + providerRecord.sourceBufferId = sourceId; + providerRecord.sourceBuffer = sourceKey; + providerRecord.window = reinterpret_cast(window); + providerRecord.provider = reinterpret_cast(provider); + providerRecord.texture = reinterpret_cast(texture); + providerRecord.logQtWrap = true; + sourceIt->qtWrapLogged = true; + sourceIt->signatureLogged = true; + s_providers.insert(providerRecord.provider, providerRecord); + s_textures.insert(providerRecord.texture, providerRecord); + + sourceIt->lastSampleId = providerRecord.sampleId; + sourceIt->lastWindow = providerRecord.window; + sourceRecord = sourceIt.value(); + + auto windowIt = s_windows.find(providerRecord.window); + if (windowIt != s_windows.end()) { + stage = windowIt->stage; + frameId = windowIt->frameId; + windowIt->bufferIds.insert(sourceId); + sourceIt->lastFrameId = frameId; + } + } + + QStringList planes; + for (int i = 0; i < sourceRecord.planeCount && i < WLR_DMABUF_MAX_PLANES; ++i) { + planes.append(QStringLiteral("%1:%2/%3") + .arg(i) + .arg(sourceRecord.offsets[i]) + .arg(sourceRecord.strides[i])); + } + + qCDebug(lcWlQtQuickTexture).noquote() + << QStringLiteral("VKTRACE event=provider-bind action=wrap owner=%1 frame=%2 stage=%3 sampleId=%4 provider=%5 clientBufferId=%6 sourceBufferId=%7 generation=%8 texture=%9 size=%10x%11 bufferType=%12 fourcc=%13 fourccHex=%14 modifier=%15 modifierHex=%16 planeCount=%17 planes=%18") + .arg(QLatin1StringView(ownerStatus)) + .arg(frameId) + .arg(QLatin1StringView(stageName(stage))) + .arg(providerRecord.sampleId) + .arg(pointerName(providerRecord.provider)) + .arg(clientId) + .arg(sourceId) + .arg(sourceRecord.generation) + .arg(pointerName(providerRecord.texture)) + .arg(sourceRecord.width) + .arg(sourceRecord.height) + .arg(QString::fromLatin1(sourceRecord.bufferType)) + .arg(formatName(sourceRecord.format)) + .arg(hex32(sourceRecord.format)) + .arg(modifierName(sourceRecord.modifier)) + .arg(hex64(sourceRecord.modifier)) + .arg(sourceRecord.planeCount) + .arg(planes.join(QLatin1Char(','))); +} + +void providerReuse(const void *provider, WOutputRenderWindow *window, + wlr_buffer *clientBuffer, wlr_texture *texture) +{ + if (!enabled() || !provider || !window || !clientBuffer || !texture) + return; + + const quintptr providerKey = reinterpret_cast(provider); + FrameStage stage = FrameStage::Idle; + quint64 frameId = 0; + ProviderRecord record; + { + QMutexLocker locker(&s_mutex); + auto it = s_providers.find(providerKey); + if (it == s_providers.end() + || it->texture != reinterpret_cast(texture) + || it->reuseLogged) { + return; + } + it->reuseLogged = true; + record = it.value(); + + auto windowIt = s_windows.find(reinterpret_cast(window)); + if (windowIt != s_windows.end()) { + frameId = windowIt->frameId; + stage = windowIt->stage; + } + } + + qCDebug(lcWlQtQuickTexture).noquote() + << QStringLiteral("VKTRACE event=provider-bind action=reuse owner=matched frame=%1 stage=%2 sampleId=%3 provider=%4 clientBufferId=%5 sourceBufferId=%6 texture=%7") + .arg(frameId) + .arg(QLatin1StringView(stageName(stage))) + .arg(record.sampleId) + .arg(pointerName(providerKey)) + .arg(record.clientBufferId) + .arg(record.sourceBufferId) + .arg(pointerName(reinterpret_cast(texture))); +} + +void providerReject(const void *provider, WOutputRenderWindow *window, + wlr_buffer *clientBuffer, wlr_texture *texture, + const char *reason) +{ + if (!enabled() || !provider || !window || !texture) + return; + + FrameStage stage = FrameStage::Idle; + quint64 frameId = 0; + { + QMutexLocker locker(&s_mutex); + auto windowIt = s_windows.find(reinterpret_cast(window)); + if (windowIt != s_windows.end()) { + frameId = windowIt->frameId; + stage = windowIt->stage; + } + } + + qCDebug(lcWlQtQuickTexture).noquote() + << QStringLiteral("VKTRACE event=provider-bind action=reject owner=%1 frame=%2 stage=%3 provider=%4 clientBuffer=%5 texture=%6") + .arg(QLatin1StringView(reason ? reason : "unknown")) + .arg(frameId) + .arg(QLatin1StringView(stageName(stage))) + .arg(pointerName(reinterpret_cast(provider))) + .arg(pointerName(clientBuffer + ? reinterpret_cast(clientBuffer) + : 0)) + .arg(pointerName(reinterpret_cast(texture))); +} + +void providerDiscard(const void *provider, WOutputRenderWindow *window, + wlr_texture *texture, const char *reason) +{ + if (!enabled() || !provider || !window || !texture) + return; + + const quintptr providerKey = reinterpret_cast(provider); + const quintptr textureKey = reinterpret_cast(texture); + ProviderRecord discarded; + FrameStage stage = FrameStage::Idle; + quint64 frameId = 0; + { + QMutexLocker locker(&s_mutex); + auto textureIt = s_textures.find(textureKey); + if (textureIt != s_textures.end() + && textureIt->provider == providerKey) { + discarded = textureIt.value(); + s_textures.erase(textureIt); + } + + auto providerIt = s_providers.find(providerKey); + if (discarded.sampleId + && providerIt != s_providers.end() + && providerIt->sampleId == discarded.sampleId) { + s_providers.erase(providerIt); + + // A transactional replacement can fail after providerBind() has + // made the candidate visible to tracing. Restore the newest older + // sample so later cleanup and draw records still describe the + // texture that the provider actually kept. + ProviderRecord previous; + for (auto it = s_textures.cbegin(); it != s_textures.cend(); ++it) { + if (it->provider == providerKey + && it->sampleId > previous.sampleId) { + previous = it.value(); + } + } + if (previous.sampleId) + s_providers.insert(providerKey, previous); + } + + auto windowIt = s_windows.find(reinterpret_cast(window)); + if (windowIt != s_windows.end()) { + frameId = windowIt->frameId; + stage = windowIt->stage; + } + } + + qCDebug(lcWlQtQuickTexture).noquote() + << QStringLiteral("VKTRACE event=provider-bind action=discard frame=%1 stage=%2 sampleId=%3 provider=%4 clientBufferId=%5 sourceBufferId=%6 texture=%7 reason=%8") + .arg(frameId) + .arg(QLatin1StringView(stageName(stage))) + .arg(discarded.sampleId) + .arg(pointerName(providerKey)) + .arg(discarded.clientBufferId) + .arg(discarded.sourceBufferId) + .arg(pointerName(textureKey)) + .arg(QLatin1StringView(reason ? reason : "unknown")); +} + +CleanupToken cleanupScheduled(const void *provider, WOutputRenderWindow *window, + wlr_buffer *clientBuffer, wlr_texture *texture) +{ + CleanupToken token; + if (!enabled()) + return token; + + const quintptr providerKey = reinterpret_cast(provider); + FrameStage stage = FrameStage::Idle; + { + QMutexLocker locker(&s_mutex); + const auto textureRecord = s_textures.value( + reinterpret_cast(texture)); + const auto providerRecord = textureRecord.provider == providerKey + ? textureRecord + : s_providers.value(providerKey); + token.sampleId = providerRecord.sampleId; + token.clientBufferId = providerRecord.clientBufferId; + token.sourceBufferId = providerRecord.sourceBufferId; + token.sourceBuffer = providerRecord.sourceBuffer; + token.window = reinterpret_cast(window); + token.provider = providerKey; + token.texture = reinterpret_cast(texture); + auto windowIt = s_windows.find(token.window); + if (windowIt != s_windows.end()) { + token.frameId = windowIt->frameId; + stage = windowIt->stage; + } + } + + if (!token.sourceBufferId && clientBuffer) { + QMutexLocker locker(&s_mutex); + const auto client = s_clientBuffers.value(reinterpret_cast(clientBuffer)); + token.clientBufferId = client.clientBufferId; + token.sourceBufferId = client.sourceBufferId; + token.sourceBuffer = client.sourceBuffer; + } + + if (!token.sourceBufferId) + return token; + + { + QMutexLocker locker(&s_mutex); + auto sourceIt = s_sourceBuffers.find(token.sourceBuffer); + if (sourceIt != s_sourceBuffers.end() + && sourceIt->sourceBufferId == token.sourceBufferId + && !sourceIt->cleanupLogged) { + sourceIt->cleanupLogged = true; + token.logLifecycle = true; + } + } + + if (!token.logLifecycle) + return token; + + qCDebug(lcWlQtQuickTexture).noquote() + << QStringLiteral("VKTRACE event=cleanup-scheduled frame=%1 stage=%2 sampleId=%3 provider=%4 clientBufferId=%5 sourceBufferId=%6 texture=%7 cleanupStage=after-rendering-delay") + .arg(token.frameId) + .arg(QLatin1StringView(stageName(stage))) + .arg(token.sampleId) + .arg(pointerName(token.provider)) + .arg(token.clientBufferId) + .arg(token.sourceBufferId) + .arg(pointerName(token.texture)); + return token; +} + +void cleanupRunning(const CleanupToken &token) +{ + if (!enabled() || (!token.sampleId && !token.sourceBufferId)) + return; + + FrameStage stage = FrameStage::Idle; + { + QMutexLocker locker(&s_mutex); + if (s_providers.value(token.provider).sampleId == token.sampleId) + s_providers.remove(token.provider); + if (s_textures.value(token.texture).sampleId == token.sampleId) + s_textures.remove(token.texture); + auto windowIt = s_windows.find(token.window); + if (windowIt != s_windows.end()) + stage = windowIt->stage; + } + if (!token.logLifecycle) + return; + qCDebug(lcWlQtQuickTexture).noquote() + << QStringLiteral("VKTRACE event=cleanup-running frame=%1 stage=%2 sampleId=%3 provider=%4 clientBufferId=%5 sourceBufferId=%6 texture=%7 unlock=imminent") + .arg(token.frameId) + .arg(QLatin1StringView(stageName(stage))) + .arg(token.sampleId) + .arg(pointerName(token.provider)) + .arg(token.clientBufferId) + .arg(token.sourceBufferId) + .arg(pointerName(token.texture)); +} + +void qtWrap(bool begin, wlr_texture *texture, quint64 image, quint32 vkFormat, + int rhiFormat, const char *bridgeClass, const char *layout) +{ + if (!enabled() || !texture) + return; + + const auto provider = providerRecordForTexture(reinterpret_cast(texture)); + if (!provider.sampleId || !provider.logQtWrap) + return; + + quint64 frameId = 0; + FrameStage stage = FrameStage::Idle; + { + QMutexLocker locker(&s_mutex); + auto windowIt = s_windows.find(provider.window); + if (windowIt != s_windows.end()) { + frameId = windowIt->frameId; + stage = windowIt->stage; + } + } + + qCDebug(lcWlQtQuickTexture).noquote() + << QStringLiteral("VKTRACE event=%1 frame=%2 stage=%3 sampleId=%4 sourceBufferId=%5 texture=%6 image=%7 vkFormat=%8 rhiFormat=%9 bridgeClass=%10 layout=%11 result=%12") + .arg(begin ? QStringLiteral("qt-wrap-begin") : QStringLiteral("qt-wrap-end")) + .arg(frameId) + .arg(QLatin1StringView(stageName(stage))) + .arg(provider.sampleId) + .arg(provider.sourceBufferId) + .arg(pointerName(reinterpret_cast(texture))) + .arg(hex64(image)) + .arg(hex32(vkFormat)) + .arg(rhiFormat) + .arg(QLatin1StringView(bridgeClass ? bridgeClass : "unknown")) + .arg(QLatin1StringView(layout ? layout : "unknown")) + .arg(begin ? QStringLiteral("pending") : QStringLiteral("unobservable")); +} + +void beginFrame(WOutputRenderWindow *window) +{ + if (!enabled() || !window) + return; + + { + QMutexLocker locker(&s_mutex); + auto &record = s_windows[reinterpret_cast(window)]; + record = WindowRecord {}; + record.frameId = s_nextFrameId.fetch_add(1, std::memory_order_relaxed); + record.stage = FrameStage::FrameBegin; + record.frameTimer.start(); + } +} + +void setFrameStage(WOutputRenderWindow *window, FrameStage stage) +{ + if (!enabled() || !window) + return; + + { + QMutexLocker locker(&s_mutex); + auto &record = s_windows[reinterpret_cast(window)]; + record.stage = stage; + if (stage == FrameStage::BeforeEndFrame) + record.endFrameTimer.start(); + else if (stage == FrameStage::AfterEndFrame && record.endFrameTimer.isValid()) { + record.endFrameUsec = record.endFrameTimer.nsecsElapsed() / 1000; + } + } +} + +void endFrame(WOutputRenderWindow *window, qsizetype committedOutputCount) +{ + if (!enabled() || !window) + return; + + QString message; + bool unclosedPass = false; + bool relevant = false; + { + QMutexLocker locker(&s_mutex); + auto windowIt = s_windows.find(reinterpret_cast(window)); + if (windowIt == s_windows.end()) + return; + auto &record = windowIt.value(); + unclosedPass = !record.passStack.isEmpty(); + relevant = !record.bufferIds.isEmpty() + || record.missingPrepareCount > 0 + || record.releaseBeforeEndFrameCandidateCount > 0; + auto bufferIds = record.bufferIds.values(); + std::sort(bufferIds.begin(), bufferIds.end()); + QStringList bufferIdNames; + bufferIdNames.reserve(bufferIds.size()); + for (const auto id : std::as_const(bufferIds)) + bufferIdNames.append(QString::number(id)); + message = QStringLiteral("VKTRACE event=frame-summary frame=%1 window=%2 passCount=%3 prepareCount=%4 prepareFailures=%5 finishCount=%6 finishFailures=%7 footprintCount=%8 missingPrepare=%9 releaseBeforeEndFrameCandidates=%10 endFrameUsec=%11 committedOutputs=%12 bufferCount=%13 bufferIds=%14 frameUsec=%15") + .arg(record.frameId) + .arg(pointerName(reinterpret_cast(window))) + .arg(record.passCount) + .arg(record.prepareCount) + .arg(record.prepareFailureCount) + .arg(record.finishCount) + .arg(record.finishFailureCount) + .arg(record.footprintCount) + .arg(record.missingPrepareCount) + .arg(record.releaseBeforeEndFrameCandidateCount) + .arg(record.endFrameUsec) + .arg(committedOutputCount) + .arg(record.bufferIds.size()) + .arg(bufferIdNames.join(QLatin1Char(','))) + .arg(record.frameTimer.isValid() ? record.frameTimer.nsecsElapsed() / 1000 : 0); + record.stage = FrameStage::Idle; + record.passStack.clear(); + } + if (!relevant) + return; + if (unclosedPass) + qCWarning(lcWlRenderHelper).noquote() << message << "unclosedPass=true"; + else + qCDebug(lcWlRenderHelper).noquote() << message; +} + +void windowDestroyed(WOutputRenderWindow *window) +{ + if (!enabled() || !window) + return; + + const quintptr windowKey = reinterpret_cast(window); + QMutexLocker locker(&s_mutex); + s_windows.remove(windowKey); + for (auto it = s_providers.begin(); it != s_providers.end();) { + if (it->window == windowKey) + it = s_providers.erase(it); + else + ++it; + } + for (auto it = s_textures.begin(); it != s_textures.end();) { + if (it->window == windowKey) + it = s_textures.erase(it); + else + ++it; + } +} + +void beginPass(WOutputRenderWindow *window, const void *renderer, + wlr_buffer *targetBuffer, const char *purpose, + int sourceIndex, qsizetype providerCount) +{ + if (!enabled() || !window) + return; + + PassRecord pass; + { + QMutexLocker locker(&s_mutex); + auto &record = s_windows[reinterpret_cast(window)]; + pass.passId = s_nextPassId.fetch_add(1, std::memory_order_relaxed); + pass.renderer = reinterpret_cast(renderer); + pass.targetBuffer = reinterpret_cast(targetBuffer); + pass.purpose = purpose ? purpose : "unknown"; + pass.sourceIndex = sourceIndex; + pass.providerCount = providerCount; + record.passStack.append(pass); + ++record.passCount; + } +} + +void sampleDisposition(WOutputRenderWindow *window, const void *provider, + wlr_texture *texture, SampleDisposition disposition, + qint64 elapsedUsec) +{ + if (!enabled() || !window) + return; + + quint64 frameId = 0; + quint64 passId = 0; + ProviderRecord providerRecord; + { + QMutexLocker locker(&s_mutex); + auto &windowRecord = s_windows[reinterpret_cast(window)]; + if (windowRecord.passStack.isEmpty()) + return; + auto &pass = windowRecord.passStack.last(); + const quintptr providerKey = reinterpret_cast(provider); + const quintptr textureKey = reinterpret_cast(texture); + providerRecord = textureKey ? s_textures.value(textureKey) + : ProviderRecord {}; + if (!providerRecord.sampleId + || (providerKey && providerRecord.provider != providerKey)) { + providerRecord = s_providers.value(providerKey); + } + if (providerKey) + pass.providerDispositions.insert(providerKey, disposition); + if (disposition == SampleDisposition::Prepared) { + if (textureKey) + pass.preparedTextures.insert(textureKey); + } + if (disposition == SampleDisposition::Prepared) { + ++pass.prepareCount; + ++windowRecord.prepareCount; + } else if (disposition == SampleDisposition::PrepareFailed) { + ++pass.prepareFailureCount; + ++windowRecord.prepareFailureCount; + } + if (!providerRecord.sampleId + && (disposition == SampleDisposition::Prepared + || disposition == SampleDisposition::PrepareFailed)) { + ++pass.prepareMetadataMissCount; + } + if (providerRecord.sampleId) { + windowRecord.bufferIds.insert(providerRecord.sourceBufferId); + auto sourceIt = s_sourceBuffers.find(providerRecord.sourceBuffer); + if (sourceIt != s_sourceBuffers.end() + && sourceIt->sourceBufferId == providerRecord.sourceBufferId) { + sourceIt->lastFrameId = windowRecord.frameId; + sourceIt->lastWindow = reinterpret_cast(window); + } + } + frameId = windowRecord.frameId; + passId = pass.passId; + } + + if (disposition != SampleDisposition::PrepareFailed + && !(disposition == SampleDisposition::Prepared && elapsedUsec > 1000)) { + return; + } + + const QString message = QStringLiteral("VKTRACE event=sample-prepare frame=%1 pass=%2 sampleId=%3 sourceBufferId=%4 provider=%5 texture=%6 disposition=%7 elapsedUsec=%8") + .arg(frameId) + .arg(passId) + .arg(providerRecord.sampleId) + .arg(providerRecord.sourceBufferId) + .arg(pointerName(reinterpret_cast(provider))) + .arg(pointerName(reinterpret_cast(texture))) + .arg(QLatin1StringView(dispositionName(disposition))) + .arg(elapsedUsec); + if (disposition == SampleDisposition::PrepareFailed) + qCWarning(lcWlQtQuickTexture).noquote() << message; + else + qCDebug(lcWlQtQuickTexture).noquote() << message; +} + +void sampleDynamicDisposition(WOutputRenderWindow *window, + wlr_texture *texture, + SampleDisposition disposition, + qint64 elapsedUsec) +{ + if (!enabled() || !window || !texture) + return; + + const auto providerRecord = providerRecordForTexture( + reinterpret_cast(texture)); + sampleDisposition(window, + providerRecord.provider + ? reinterpret_cast(providerRecord.provider) + : nullptr, + texture, + disposition, + elapsedUsec); +} + +void sampleFinished(WOutputRenderWindow *window, wlr_texture *texture, + bool ok, qint64 elapsedUsec) +{ + if (!enabled() || !window || !texture) + return; + + quint64 frameId = 0; + quint64 passId = 0; + ProviderRecord providerRecord; + { + QMutexLocker locker(&s_mutex); + auto &windowRecord = s_windows[reinterpret_cast(window)]; + if (windowRecord.passStack.isEmpty()) + return; + auto &pass = windowRecord.passStack.last(); + providerRecord = s_textures.value(reinterpret_cast(texture)); + ++pass.finishCount; + ++windowRecord.finishCount; + if (!providerRecord.sampleId) + ++pass.finishMetadataMissCount; + if (!ok) { + ++pass.finishFailureCount; + ++windowRecord.finishFailureCount; + } + frameId = windowRecord.frameId; + passId = pass.passId; + } + + if (ok && elapsedUsec <= 1000) + return; + + const QString message = QStringLiteral("VKTRACE event=sample-finish frame=%1 pass=%2 sampleId=%3 sourceBufferId=%4 texture=%5 result=%6 elapsedUsec=%7") + .arg(frameId) + .arg(passId) + .arg(providerRecord.sampleId) + .arg(providerRecord.sourceBufferId) + .arg(pointerName(reinterpret_cast(texture))) + .arg(ok) + .arg(elapsedUsec); + if (ok) + qCDebug(lcWlQtQuickTexture).noquote() << message; + else + qCWarning(lcWlQtQuickTexture).noquote() << message; +} + +void surfaceFootprint(WOutputRenderWindow *window, const void *provider, + wlr_texture *texture, const void *surface) +{ + if (!enabled() || !window) + return; + + QMutexLocker locker(&s_mutex); + auto &windowRecord = s_windows[reinterpret_cast(window)]; + if (windowRecord.passStack.isEmpty()) + return; + + auto &pass = windowRecord.passStack.last(); + const quintptr providerKey = reinterpret_cast(provider); + const quintptr textureKey = reinterpret_cast(texture); + + ++pass.footprintCount; + ++windowRecord.footprintCount; + + const auto currentProviderRecord = s_providers.value(providerKey); + auto providerRecord = textureKey ? s_textures.value(textureKey) + : ProviderRecord {}; + if (!providerRecord.sampleId || providerRecord.provider != providerKey) + providerRecord = currentProviderRecord; + + const bool bindingChanged = textureKey + && currentProviderRecord.sampleId + && currentProviderRecord.texture != textureKey; + if (bindingChanged) + ++pass.bindingChangeCount; + + if (!providerRecord.sampleId) { + ++pass.footprintMetadataMissCount; + ++pass.invalidProviderDrawCount; + return; + } + + windowRecord.bufferIds.insert(providerRecord.sourceBufferId); + auto sourceIt = s_sourceBuffers.find(providerRecord.sourceBuffer); + if (sourceIt != s_sourceBuffers.end() + && sourceIt->sourceBufferId == providerRecord.sourceBufferId) { + sourceIt->lastDrawFrameId = windowRecord.frameId; + sourceIt->lastFrameId = windowRecord.frameId; + sourceIt->lastSampleId = providerRecord.sampleId; + sourceIt->lastWindow = reinterpret_cast(window); + sourceIt->lastDrawWindow = reinterpret_cast(window); + sourceIt->lastSurface = reinterpret_cast(surface); + } + + if ((textureKey && pass.preparedTextures.contains(textureKey)) + || (bindingChanged + && pass.preparedTextures.contains(currentProviderRecord.texture))) { + return; + } + + const quintptr dispositionProviderKey = providerRecord.provider + ? providerRecord.provider + : providerKey; + if (!pass.providerDispositions.contains(dispositionProviderKey)) { + if (providerRecord.sampleId) { + ++pass.missingProviderCount; + ++windowRecord.missingPrepareCount; + } else { + ++pass.invalidProviderDrawCount; + } + return; + } + + const auto disposition = pass.providerDispositions.value(dispositionProviderKey); + switch (disposition) { + case SampleDisposition::PrepareFailed: + ++pass.drawAfterPrepareFailureCount; + ++windowRecord.missingPrepareCount; + break; + case SampleDisposition::CurrentRenderTarget: + ++pass.currentTargetFeedbackCount; + ++windowRecord.missingPrepareCount; + break; + case SampleDisposition::InvalidProvider: + ++pass.invalidProviderDrawCount; + break; + case SampleDisposition::Prepared: + case SampleDisposition::DuplicateTexture: + ++pass.missingProviderCount; + ++windowRecord.missingPrepareCount; + break; + } +} + +void endPass(WOutputRenderWindow *window, bool ok) +{ + if (!enabled() || !window) + return; + + QString message; + bool invariantFailure = false; + bool trackedActivity = false; + { + QMutexLocker locker(&s_mutex); + auto &windowRecord = s_windows[reinterpret_cast(window)]; + if (windowRecord.passStack.isEmpty()) + return; + const auto pass = windowRecord.passStack.takeLast(); + trackedActivity = !pass.providerDispositions.isEmpty() + || pass.prepareCount > 0 + || pass.finishCount > 0 + || pass.footprintCount > 0; + // A footprint node is a separately scheduled QSGRenderNode. Its + // execution is useful evidence, but does not prove that the parent + // image geometry sampled the texture. Keep missing-provider reports + // diagnostic-only until an exact QSG draw dependency is available. + invariantFailure = pass.prepareFailureCount > 0 + || pass.drawAfterPrepareFailureCount > 0 + || pass.currentTargetFeedbackCount > 0 + || pass.finishFailureCount > 0 + || pass.prepareCount != pass.finishCount + || !ok; + message = QStringLiteral("VKTRACE event=pass-end frame=%1 pass=%2 result=%3 sourceIndex=%4 providerCount=%5 prepareCount=%6 prepareFailures=%7 prepareMetadataMisses=%8 finishCount=%9 finishFailures=%10 finishMetadataMisses=%11 footprintCount=%12 footprintMetadataMisses=%13 bindingChanges=%14 missingActiveProvider=%15 drawAfterPrepareFailure=%16 currentTargetFeedback=%17 invalidProviderDraw=%18") + .arg(windowRecord.frameId) + .arg(pass.passId) + .arg(ok) + .arg(pass.sourceIndex) + .arg(pass.providerCount) + .arg(pass.prepareCount) + .arg(pass.prepareFailureCount) + .arg(pass.prepareMetadataMissCount) + .arg(pass.finishCount) + .arg(pass.finishFailureCount) + .arg(pass.finishMetadataMissCount) + .arg(pass.footprintCount) + .arg(pass.footprintMetadataMissCount) + .arg(pass.bindingChangeCount) + .arg(pass.missingProviderCount) + .arg(pass.drawAfterPrepareFailureCount) + .arg(pass.currentTargetFeedbackCount) + .arg(pass.invalidProviderDrawCount); + } + if (!trackedActivity) + return; + if (invariantFailure) + qCWarning(lcWlBufferRenderer).noquote() << message; + else + qCDebug(lcWlBufferRenderer).noquote() << message; +} + +void outputCommitState(wlr_output *output, quint32 committedState) +{ + if (!enabled() || !output) + return; + QMutexLocker locker(&s_mutex); + auto &states = s_outputCommitStates[reinterpret_cast(output)]; + states.insert(output->commit_seq, committedState); + while (states.size() > MaxTrackedOutputCommits) + states.erase(states.begin()); +} + +void outputCommitted(WOutputRenderWindow *window, wlr_output *output, + quint32 sequenceBefore, bool ok) +{ + if (!enabled() || !window || !output) + return; + + const quint32 sequenceAfter = output->commit_seq; + const bool sequenceAdvanced = ok && sequenceAfter != sequenceBefore; + quint64 frameId = 0; + quint32 committedState = 0; + bool relevant = false; + { + QMutexLocker locker(&s_mutex); + const auto windowRecord = s_windows.value(reinterpret_cast(window)); + frameId = windowRecord.frameId; + relevant = !windowRecord.bufferIds.isEmpty(); + if (!relevant) + return; + if (sequenceAdvanced) { + committedState = s_outputCommitStates.value(reinterpret_cast(output)) + .value(sequenceAfter); + auto &commits = s_outputCommits[reinterpret_cast(output)]; + commits.insert(sequenceAfter, OutputCommitRecord { frameId, committedState }); + while (commits.size() > MaxTrackedOutputCommits) + commits.erase(commits.begin()); + } + } + + if (ok) + return; + + const QString message = QStringLiteral("VKTRACE event=output-commit frame=%1 output=%2 seqBefore=%3 commitSeq=%4 seqAdvanced=%5 result=%6 stateKnown=%7 committedState=%8 bufferCommitted=%9") + .arg(frameId) + .arg(pointerName(reinterpret_cast(output))) + .arg(sequenceBefore) + .arg(sequenceAfter) + .arg(sequenceAdvanced) + .arg(ok) + .arg(sequenceAdvanced) + .arg(hex32(committedState)) + .arg(bool(committedState & WLR_OUTPUT_STATE_BUFFER)); + qCWarning(lcWlOutputBuffer).noquote() << message; +} + +void outputPresented(wlr_output *output, const wlr_output_event_present *event) +{ + if (!enabled() || !output || !event) + return; + + OutputCommitRecord commit; + bool matched = false; + { + QMutexLocker locker(&s_mutex); + auto outputIt = s_outputCommits.find(reinterpret_cast(output)); + if (outputIt != s_outputCommits.end()) { + auto commitIt = outputIt->find(event->commit_seq); + if (commitIt != outputIt->end()) { + commit = commitIt.value(); + outputIt->erase(commitIt); + matched = true; + } + } + } + + qCDebug(lcWlOutputBuffer).noquote() + << QStringLiteral("VKTRACE event=output-present frame=%1 output=%2 commitSeq=%3 matched=%4 presented=%5 when=%6.%7 refreshNs=%8 flags=%9 bufferCommitted=%10") + .arg(commit.frameId) + .arg(pointerName(reinterpret_cast(output))) + .arg(event->commit_seq) + .arg(matched) + .arg(event->presented) + .arg(event->when.tv_sec) + .arg(event->when.tv_nsec, 9, 10, QLatin1Char('0')) + .arg(event->refresh) + .arg(hex32(event->flags)) + .arg(bool(commit.committedState & WLR_OUTPUT_STATE_BUFFER)); +} + +void outputDestroyed(wlr_output *output) +{ + if (!enabled() || !output) + return; + const quintptr outputKey = reinterpret_cast(output); + QMutexLocker locker(&s_mutex); + s_outputCommits.remove(outputKey); + s_outputCommitStates.remove(outputKey); +} + +} // namespace WVulkanTrace + +WAYLIB_SERVER_END_NAMESPACE diff --git a/waylib/src/server/wayliblogging.cpp b/waylib/src/server/wayliblogging.cpp index 89ebe5de43..f089436b12 100644 --- a/waylib/src/server/wayliblogging.cpp +++ b/waylib/src/server/wayliblogging.cpp @@ -53,6 +53,7 @@ Q_LOGGING_CATEGORY(lcWlForeignToplevel, "waylib.protocols.foreigntoplevel", QtWa Q_LOGGING_CATEGORY(lcWlInputMethod, "waylib.protocols.inputmethod", QtInfoMsg) // Input method v2 and virtual keyboard Q_LOGGING_CATEGORY(lcWlExtForeignToplevel, "waylib.protocols.extforeigntoplevel", QtWarningMsg) // ext-foreign-toplevel-list-v1 Q_LOGGING_CATEGORY(lcWlTextInput, "waylib.protocols.textinput", QtInfoMsg) // Text input v1/v2/v3 protocol +Q_LOGGING_CATEGORY(lcWlLinuxDmabuf, "waylib.protocols.linuxdmabuf", QtInfoMsg) // Renderer-backed linux-dmabuf global Q_LOGGING_CATEGORY(lcWlRemoteSubsurface, "waylib.protocols.remotesubsurface", QtWarningMsg) // treeland-remote-subsurface-unstable-v1 // Layer shell diff --git a/waylib/src/server/wayliblogging.h b/waylib/src/server/wayliblogging.h index 31d553316c..b73dc32f87 100644 --- a/waylib/src/server/wayliblogging.h +++ b/waylib/src/server/wayliblogging.h @@ -46,6 +46,7 @@ Q_DECLARE_LOGGING_CATEGORY(lcWlForeignToplevel) Q_DECLARE_LOGGING_CATEGORY(lcWlInputMethod) Q_DECLARE_LOGGING_CATEGORY(lcWlExtForeignToplevel) Q_DECLARE_LOGGING_CATEGORY(lcWlTextInput) +Q_DECLARE_LOGGING_CATEGORY(lcWlLinuxDmabuf) Q_DECLARE_LOGGING_CATEGORY(lcWlRemoteSubsurface) // Layer shell From d9c12adc69fd75be52b9197a1971e79c33e6fa1b Mon Sep 17 00:00:00 2001 From: LFRon Date: Wed, 2 Sep 2026 11:39:54 +0800 Subject: [PATCH 05/15] feat(vulkan): wait for imported textures on the GPU Follow-up hardening (folded): close the offscreen QRhi frame even when the flushTextureSyncBatch() synchronization before submission fails. Leaving the frame open makes every later beginFrame() a silent no-op inside QQuickRenderControl (Qt requires endFrame() first), freezing the output on the last good image; and at shutdown the QRhi cleanup callbacks run while QRhi is still inFrame, so a backend finish() from such a callback ends the abandoned primary command buffer and aborts Mesa's radv in the amdgpu command-buffer finalization (observed as the Waydroid shutdown crash with large foreign-texture sync batches). endFrameChecked() now always runs after beginFrameChecked() succeeded; its submission result only gates the normal frame-completion path, matching the previous semantics on the flush-ok side. Follow-up (folded): skip the producer-fence export/wait for client buffers whose content has already been synchronized since the client last committed them (qt_content_synced), and clear that shortcut when the client re-attaches the buffer. Only wlr_client_buffer-backed textures take the shortcut. --- 3rdparty/wlroots/include/render/vulkan.h | 7 ++ 3rdparty/wlroots/include/wlr/render/vulkan.h | 9 ++ 3rdparty/wlroots/render/vulkan/texture.c | 46 ++++++++-- .../server/qtquick/woutputrenderwindow.cpp | 46 ++++++++-- waylib/src/server/qtquick/wrenderhelper.cpp | 85 +++++++++++++++++++ waylib/src/server/qtquick/wrenderhelper.h | 6 ++ waylib/src/server/qtquick/wsurfaceitem.cpp | 11 +++ 7 files changed, 200 insertions(+), 10 deletions(-) diff --git a/3rdparty/wlroots/include/render/vulkan.h b/3rdparty/wlroots/include/render/vulkan.h index fe6b35f345..ca410570c9 100644 --- a/3rdparty/wlroots/include/render/vulkan.h +++ b/3rdparty/wlroots/include/render/vulkan.h @@ -617,6 +617,13 @@ struct wlr_vk_texture { // waylib_vk_renderer_begin_readback()) currently holds a foreign->own // acquire. Decoupled from 'owned', which is reserved for wlroots' own render-pass bookkeeping. bool readback_acquired; // waylib/Qt-only: a readback acquire is in flight + // waylib/Qt-only: the DMA-BUF content of this client buffer was already + // waited on (producer sync_file / imported semaphore) since the client + // last attached it. Set after a successful producer wait; cleared by + // waylib_vk_renderer_mark_buffer_content_dirty() when the client + // re-attaches (commits) the buffer. Only ever set for client buffers + // (wlr_client_buffer); every other texture keeps waiting unconditionally. + bool qt_content_synced; bool transitioned; // if dma_imported: whether we transitioned it away from preinit bool has_alpha; // whether the image is has alpha channel bool using_mutable_srgb; // can be accessed through _SRGB format view diff --git a/3rdparty/wlroots/include/wlr/render/vulkan.h b/3rdparty/wlroots/include/wlr/render/vulkan.h index 0e3e9919ad..95e068d583 100644 --- a/3rdparty/wlroots/include/wlr/render/vulkan.h +++ b/3rdparty/wlroots/include/wlr/render/vulkan.h @@ -65,6 +65,15 @@ bool waylib_vk_renderer_prepare_texture_for_sampling(struct wlr_renderer *render bool waylib_vk_renderer_finish_texture_sampling(struct wlr_renderer *renderer, struct wlr_texture *texture, VkCommandBuffer cb); +// Clear the "content already synchronized" shortcut for every wlroots Vulkan +// texture currently importing the given wlr_buffer. Call from the surface +// commit path when the client (re-)attaches a buffer: the producer may have +// written new content, so the next sampling acquire must export and wait for a +// fresh producer fence instead of reusing the previous one. No-op for a +// NULL/invalid renderer, a NULL buffer, or a non-Vulkan renderer. +void waylib_vk_renderer_mark_buffer_content_dirty(struct wlr_renderer *renderer, + struct wlr_buffer *buffer); + // Collect foreign-texture sync_files while a compositor frame is recorded, // then submit one semaphore wait with a bridge barrier before the compositor // command buffer is submitted to the same queue. The barrier extends the wait diff --git a/3rdparty/wlroots/render/vulkan/texture.c b/3rdparty/wlroots/render/vulkan/texture.c index e7d15fc85a..92a4071e3e 100644 --- a/3rdparty/wlroots/render/vulkan/texture.c +++ b/3rdparty/wlroots/render/vulkan/texture.c @@ -11,6 +11,7 @@ #include #include #include +#include #include #include #include "render/pixel_format.h" @@ -1553,12 +1554,23 @@ bool waylib_vk_renderer_prepare_texture_for_sampling(struct wlr_renderer *wlr_re return false; } - int sync_file_fds[WLR_DMABUF_MAX_PLANES]; - for (size_t i = 0; i < WLR_DMABUF_MAX_PLANES; i++) { - sync_file_fds[i] = -1; - } + // Skip the producer fence export/wait when the client has not + // re-attached (committed) this buffer since its current content was + // already synchronized: there is no new producer write to wait for. + // Only client buffers (wlr_client_buffer) ever set qt_content_synced; + // every other texture (e.g. an output swapchain buffer sampled for + // mirroring) keeps waiting on every acquire. + struct wlr_client_buffer *client_buffer = texture->buffer != NULL + ? wlr_client_buffer_get(texture->buffer) : NULL; + const bool skip_producer_wait = client_buffer != NULL + && texture->qt_content_synced; + + if (texture->buffer != NULL && !skip_producer_wait) { + int sync_file_fds[WLR_DMABUF_MAX_PLANES]; + for (size_t i = 0; i < WLR_DMABUF_MAX_PLANES; i++) { + sync_file_fds[i] = -1; + } - if (texture->buffer != NULL) { if (!vulkan_sync_foreign_texture_acquire(texture, sync_file_fds)) { close_sync_file_fds(sync_file_fds); wlr_log(WLR_ERROR, "Failed to wait for foreign texture DMA-BUF fence"); @@ -1579,6 +1591,10 @@ bool waylib_vk_renderer_prepare_texture_for_sampling(struct wlr_renderer *wlr_re if (!waited) { return false; } + + if (client_buffer != NULL) { + texture->qt_content_synced = true; + } } // Record the sampling side of the alias pair before touching any @@ -1719,6 +1735,26 @@ bool waylib_vk_renderer_finish_texture_sampling(struct wlr_renderer *wlr_rendere return true; } +void waylib_vk_renderer_mark_buffer_content_dirty(struct wlr_renderer *wlr_renderer, + struct wlr_buffer *wlr_buffer) { + if (wlr_renderer == NULL || wlr_buffer == NULL + || !wlr_renderer_is_vk(wlr_renderer)) { + return; + } + + struct wlr_vk_renderer *renderer = vulkan_get_renderer(wlr_renderer); + struct wlr_vk_texture *texture; + wl_list_for_each(texture, &renderer->textures, link) { + if (texture->buffer == wlr_buffer) { + // The client re-attached a buffer it owns again: its producer may + // have written new content, so the next sampling acquire must wait + // for a fresh producer fence even though the previous content was + // already synchronized. + texture->qt_content_synced = false; + } + } +} + // A deferred acquire/release barrier that is never recorded must not leave the // texture's ownership bookkeeping claiming a transfer that did not happen. A // dropped acquire leaves the image FOREIGN-owned (it must be re-acquired on diff --git a/waylib/src/server/qtquick/woutputrenderwindow.cpp b/waylib/src/server/qtquick/woutputrenderwindow.cpp index 05bf7eacf9..6b608a5796 100644 --- a/waylib/src/server/qtquick/woutputrenderwindow.cpp +++ b/waylib/src/server/qtquick/woutputrenderwindow.cpp @@ -527,6 +527,8 @@ class Q_DECL_HIDDEN WOutputRenderWindowPrivate : public QQuickWindowPrivate static constexpr int FatalBeginFrameFailureThreshold = 3; WPointer m_renderer; WPointer m_allocator; + bool textureSyncQueueValidated = false; + bool textureSyncBatchDisabled = false; QList outputs; QList layers; @@ -1977,6 +1979,7 @@ void WOutputRenderWindowPrivate::doRender(wlr_output *needsFrameOutput, WVulkanTrace::beginFrame(q); const auto finishFrame = [this, q] (const QList> &committedOutputs) { + WRenderHelper::abortTextureSyncBatch(m_renderer); resetGlState(); // On Intel&Nvidia multi-GPU environment, wlroots using Intel card do render for all @@ -2037,6 +2040,17 @@ void WOutputRenderWindowPrivate::doRender(wlr_output *needsFrameOutput, return; } consecutiveBeginFrameFailures = 0; + if (!textureSyncBatchDisabled) { + const bool batchStarted = WRenderHelper::beginTextureSyncBatch( + rc(), m_renderer, !textureSyncQueueValidated); + if (batchStarted) { + textureSyncQueueValidated = true; + } else { + // The immediate CPU-wait path remains correct, and avoids + // retrying queue validation in the frame hot path. + textureSyncBatchDisabled = true; + } + } } else { rc()->beginFrame(); } @@ -2067,12 +2081,34 @@ void WOutputRenderWindowPrivate::doRender(wlr_output *needsFrameOutput, if (rhiBased) { WVulkanTrace::setFrameStage(q, WVulkanTrace::FrameStage::BeforeEndFrame); if (isVulkan) { + const bool flushed = WRenderHelper::flushTextureSyncBatch(m_renderer); + if (!flushed) { + // The recorded frame may sample a texture whose producer has + // not completed. Never submit it without the required wait. + failCurrentFrameFatally(QStringLiteral("Failed to synchronize Vulkan textures before submitting the Qt Quick frame")); + } + // beginFrameChecked() above succeeded, so QRhi is recording this + // offscreen frame no matter whether the sync-batch flush failed. + // A frame that is left open makes every later beginFrame() return + // early inside QQuickRenderControl (Qt ignores it with + // "beginFrame() must be followed by endFrame()"), freezing the + // output on the last good image; and at shutdown the QRhi cleanup + // callbacks run while QRhi is still inFrame, so a backend flush + // from such a callback ends the abandoned primary command buffer + // and aborts Mesa's radv (amdgpu_cs finalize assertion). Always + // close the frame; its submission result only matters on the + // flush-ok path. const auto endResult = rc()->endFrameChecked(); - vulkanFrameCompleted = endResult == QRhi::FrameOpSuccess; - if (!vulkanFrameCompleted) { - failCurrentFrameFatally(endResult == QRhi::FrameOpDeviceLost - ? QStringLiteral("Vulkan device was lost while submitting the Qt Quick frame") - : QStringLiteral("Failed to submit the Vulkan Qt Quick frame")); + if (flushed) { + vulkanFrameCompleted = endResult == QRhi::FrameOpSuccess; + if (!vulkanFrameCompleted) { + failCurrentFrameFatally(endResult == QRhi::FrameOpDeviceLost + ? QStringLiteral("Vulkan device was lost while submitting the Qt Quick frame") + : QStringLiteral("Failed to submit the Vulkan Qt Quick frame")); + } + } else if (endResult != QRhi::FrameOpSuccess) { + qCWarning(lcWlRenderer) << "Closing the unsynchronized Vulkan frame did not succeed either" + << "frameResult" << endResult; } } else { rc()->endFrame(); diff --git a/waylib/src/server/qtquick/wrenderhelper.cpp b/waylib/src/server/qtquick/wrenderhelper.cpp index fdf17d1b01..035dd438c0 100644 --- a/waylib/src/server/qtquick/wrenderhelper.cpp +++ b/waylib/src/server/qtquick/wrenderhelper.cpp @@ -1775,6 +1775,91 @@ bool WRenderHelper::makeTexture(QRhi *rhi, wlr_texture *handle, QSGPlainTexture return updateTexture(rhi, handle, texture, forceVulkanShaderReadOnlyLayout); } +bool WRenderHelper::beginTextureSyncBatch(QQuickRenderControl *rc, + wlr_renderer *renderer, + bool verifyQueue) +{ +#ifdef ENABLE_VULKAN_RENDER + if (!renderer || !wlr_renderer_is_vk(renderer)) + return true; + + if (verifyQueue) { + if (!rc || !rc->rhi() || rc->rhi()->backend() != QRhi::Vulkan) { + qCWarning(lcWlRenderHelper) << "Cannot begin Vulkan texture sync batch: missing Vulkan QRhi"; + return false; + } + + const auto *nativeHandles = static_cast(rc->rhi()->nativeHandles()); + if (!nativeHandles + || nativeHandles->dev != wlr_vk_renderer_get_device(renderer) + || nativeHandles->gfxQueueFamilyIdx != wlr_vk_renderer_get_queue_family(renderer) + || nativeHandles->gfxQueue != waylib_vk_renderer_get_queue(renderer)) { + qCWarning(lcWlRenderHelper) << "Cannot begin Vulkan texture sync batch: Qt and wlroots do not share the same queue" + << "qtDevice" << (nativeHandles ? nativeHandles->dev : VK_NULL_HANDLE) + << "wlrootsDevice" << wlr_vk_renderer_get_device(renderer) + << "qtQueueFamily" << (nativeHandles ? nativeHandles->gfxQueueFamilyIdx : 0) + << "wlrootsQueueFamily" << wlr_vk_renderer_get_queue_family(renderer); + return false; + } + } + + if (!waylib_vk_renderer_begin_texture_sync_batch(renderer)) { + qCWarning(lcWlRenderHelper) << "Failed to begin Vulkan texture sync batch; using immediate CPU waits for this frame"; + return false; + } +#else + Q_UNUSED(rc); + Q_UNUSED(renderer); + Q_UNUSED(verifyQueue); +#endif + return true; +} + +bool WRenderHelper::flushTextureSyncBatch(wlr_renderer *renderer) +{ +#ifdef ENABLE_VULKAN_RENDER + if (!renderer || !wlr_renderer_is_vk(renderer)) + return true; + + if (!waylib_vk_renderer_flush_texture_sync_batch(renderer)) { + qCWarning(lcWlRenderHelper) << "Failed to flush Vulkan texture sync batch"; + return false; + } +#else + Q_UNUSED(renderer); +#endif + return true; +} + +void WRenderHelper::abortTextureSyncBatch(wlr_renderer *renderer) +{ +#ifdef ENABLE_VULKAN_RENDER + if (renderer && wlr_renderer_is_vk(renderer)) + waylib_vk_renderer_abort_texture_sync_batch(renderer); +#else + Q_UNUSED(renderer); +#endif +} + + +void WRenderHelper::markBufferContentDirty(wlr_renderer *renderer, wlr_buffer *buffer) +{ +#ifdef ENABLE_VULKAN_RENDER + if (!renderer || !buffer || !wlr_renderer_is_vk(renderer)) + return; + + // The client re-attached this buffer: its next sampling acquire must wait + // for a fresh producer fence. The wlroots side clears the per-texture + // "content already synchronized" shortcut for every imported texture of + // this buffer; non-client buffers are unaffected (they are never marked, + // so they keep waiting on every acquire). + waylib_vk_renderer_mark_buffer_content_dirty(renderer, buffer); +#else + Q_UNUSED(renderer); + Q_UNUSED(buffer); +#endif +} + WRenderHelper::TextureEntry WRenderHelper::newTexture(wlr_allocator *allocator, wlr_renderer *renderer, uint32_t drmFormat, uint64_t drmModifier, diff --git a/waylib/src/server/qtquick/wrenderhelper.h b/waylib/src/server/qtquick/wrenderhelper.h index a6619ab210..3e17499014 100644 --- a/waylib/src/server/qtquick/wrenderhelper.h +++ b/waylib/src/server/qtquick/wrenderhelper.h @@ -77,6 +77,12 @@ class WAYLIB_SERVER_EXPORT WRenderHelper : public QObject, public WObject static bool makeTexture(QRhi *rhi, wlr_texture *handle, QSGPlainTexture *texture, bool forceVulkanShaderReadOnlyLayout = false); + static void markBufferContentDirty(wlr_renderer *renderer, wlr_buffer *buffer); + static bool beginTextureSyncBatch(QQuickRenderControl *rc, + wlr_renderer *renderer, + bool verifyQueue); + static bool flushTextureSyncBatch(wlr_renderer *renderer); + static void abortTextureSyncBatch(wlr_renderer *renderer); static void resetVulkanFrameAliasLists(wlr_renderer *renderer); struct TextureEntry { diff --git a/waylib/src/server/qtquick/wsurfaceitem.cpp b/waylib/src/server/qtquick/wsurfaceitem.cpp index 2b14b7ba8b..9175baf763 100644 --- a/waylib/src/server/qtquick/wsurfaceitem.cpp +++ b/waylib/src/server/qtquick/wsurfaceitem.cpp @@ -7,6 +7,7 @@ #include "woutput.h" #include "woutputrenderwindow.h" #include "woutputviewport.h" +#include "wrenderhelper.h" #include "wseat.h" #include "wsgtextureprovider.h" #include "wsurface.h" @@ -273,6 +274,16 @@ class Q_DECL_HIDDEN WSurfaceItemContentPrivate: public QQuickItemPrivate // Get the new buffer pointer from surface auto newBuffer = surface->buffer(); + // The client re-attached a buffer: its producer may have + // written new content, so the next Vulkan sampling acquire + // must wait for a fresh producer fence instead of reusing the + // previous one. No-op on non-Vulkan renderers. + if (auto *renderWindow = q->outputRenderWindow()) { + if (usesVulkanRhi(renderWindow)) + WRenderHelper::markBufferContentDirty(renderWindow->renderer(), + newBuffer); + } + if (!live) { // Non-live mode: defer to pendingBuffer pendingBuffer.reset(newBuffer); From 57c8acbd27cfd1a5c827a81a2144aba029d3e073 Mon Sep 17 00:00:00 2001 From: LFRon Date: Wed, 2 Sep 2026 11:40:06 +0800 Subject: [PATCH 06/15] feat(vulkan): add presentation feedback Report wp_presentation feedback for the frames the Vulkan path commits: WPresentation wraps wlr_presentation, and WOutputRenderWindow collects the surfaces textured on the current output (markSurfaceTexturedForPresentation) and submits surfaceTexturedOnOutput() for them per output commit (submitPresentationFeedbackForOutput), so clients asking for presentation feedback get their texture/present events from the output that actually committed the frame. Follow-up hardening (folded): null m_handle in WPresentation::destroy() so handle() returns null instead of a dangling pointer once wlroots has destroyed the global from its display destroy listener. --- waylib/src/server/CMakeLists.txt | 3 + waylib/src/server/kernel/wlr_all.h | 1 + waylib/src/server/protocols/WPresentation | 1 + waylib/src/server/protocols/wpresentation.cpp | 84 +++++++++++++++++ waylib/src/server/protocols/wpresentation.h | 36 +++++++ .../server/qtquick/woutputrenderwindow.cpp | 94 +++++++++++++++++++ .../src/server/qtquick/woutputrenderwindow.h | 7 +- waylib/src/server/qtquick/wsurfaceitem.cpp | 91 ++++++++++++++++-- waylib/src/server/wayliblogging.cpp | 1 + waylib/src/server/wayliblogging.h | 1 + 10 files changed, 310 insertions(+), 9 deletions(-) create mode 100644 waylib/src/server/protocols/WPresentation create mode 100644 waylib/src/server/protocols/wpresentation.cpp create mode 100644 waylib/src/server/protocols/wpresentation.h diff --git a/waylib/src/server/CMakeLists.txt b/waylib/src/server/CMakeLists.txt index e1dfcbef2e..dbd13b8b27 100644 --- a/waylib/src/server/CMakeLists.txt +++ b/waylib/src/server/CMakeLists.txt @@ -213,6 +213,7 @@ set(SOURCES protocols/wrelativepointermanagerv1.cpp protocols/woutputmanagerv1.cpp protocols/wlinuxdmabufv1.cpp + protocols/wpresentation.cpp protocols/wextforeigntoplevellistv1.cpp protocols/wxdgdialogmanagerv1.cpp protocols/wsecuritycontextmanager.cpp @@ -326,7 +327,9 @@ set(HEADERS protocols/woutputmanagerv1.h protocols/WOutputManagerV1 protocols/wlinuxdmabufv1.h + protocols/wpresentation.h protocols/WLinuxDmabufV1 + protocols/WPresentation protocols/wlayershell.h protocols/WLayerShell protocols/wxwayland.h diff --git a/waylib/src/server/kernel/wlr_all.h b/waylib/src/server/kernel/wlr_all.h index 6a1b1ab3a1..352c37d592 100644 --- a/waylib/src/server/kernel/wlr_all.h +++ b/waylib/src/server/kernel/wlr_all.h @@ -109,6 +109,7 @@ extern "C" { #include #include #include +#include #include #include #include diff --git a/waylib/src/server/protocols/WPresentation b/waylib/src/server/protocols/WPresentation new file mode 100644 index 0000000000..cfb6717e8b --- /dev/null +++ b/waylib/src/server/protocols/WPresentation @@ -0,0 +1 @@ +#include "wpresentation.h" diff --git a/waylib/src/server/protocols/wpresentation.cpp b/waylib/src/server/protocols/wpresentation.cpp new file mode 100644 index 0000000000..8875bd0777 --- /dev/null +++ b/waylib/src/server/protocols/wpresentation.cpp @@ -0,0 +1,84 @@ +// Copyright (C) 2026 UnionTech Software Technology Co., Ltd. +// SPDX-License-Identifier: Apache-2.0 OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#include "wpresentation.h" + +#include "wayliblogging.h" +#include "woutput.h" +#include "wsurface.h" + +#include + +WAYLIB_SERVER_BEGIN_NAMESPACE + +constexpr uint32_t PresentationVersion = 2; + +WPresentation::WPresentation(wlr_backend *backend, QObject *parent) + : QObject(parent) + , m_backend(backend) +{ +} + +wlr_presentation *WPresentation::handle() const +{ + return nativeInterface(); +} + +QByteArrayView WPresentation::interfaceName() const +{ + return "wp_presentation"; +} + +void WPresentation::surfaceTexturedOnOutput(WSurface *surface, WOutput *output) +{ + auto *native = handle(); + if (!native || !surface || !surface->handle() || !output || !output->handle()) + return; + + wlr_presentation_surface_textured_on_output(surface->handle(), output->handle()); + qCDebug(lcWlPresentation) << "Marked surface textured on output" + << "surface" << surface + << "wlrSurface" << surface->handle() + << "output" << output + << "wlrOutput" << output->handle(); +} + +void WPresentation::create(WServer *server) +{ + if (m_handle) + return; + + if (!m_backend) { + qCWarning(lcWlPresentation) << "Cannot create presentation-time global without backend"; + return; + } + + m_handle = wlr_presentation_create(server->handle(), m_backend, + PresentationVersion); + if (!m_handle) { + qCWarning(lcWlPresentation) << "Failed to create presentation-time global"; + return; + } + + qCInfo(lcWlPresentation) << "Created presentation-time global" + << "version" << PresentationVersion; +} + +void WPresentation::destroy([[maybe_unused]] WServer *server) +{ + // wlroots owns this global and destroys it from the display destroy + // listener; null m_handle so handle()/global() return null instead of a + // dangling pointer (kept as an explicit override rather than the inherited + // empty base for this hardening). + m_handle = nullptr; +} + +wl_global *WPresentation::global() const +{ + if (auto *native = handle()) + return native->global; + + return nullptr; +} + +WAYLIB_SERVER_END_NAMESPACE diff --git a/waylib/src/server/protocols/wpresentation.h b/waylib/src/server/protocols/wpresentation.h new file mode 100644 index 0000000000..d6ccfff02d --- /dev/null +++ b/waylib/src/server/protocols/wpresentation.h @@ -0,0 +1,36 @@ +// Copyright (C) 2026 UnionTech Software Technology Co., Ltd. +// SPDX-License-Identifier: Apache-2.0 OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#pragma once + +#include +#include + +WAYLIB_SERVER_BEGIN_NAMESPACE + +class WOutput; +class WSurface; + +class WAYLIB_SERVER_EXPORT WPresentation : public QObject, public WServerInterface +{ + Q_OBJECT + +public: + explicit WPresentation(wlr_backend *backend, QObject *parent = nullptr); + + wlr_presentation *handle() const; + + QByteArrayView interfaceName() const override; + + void surfaceTexturedOnOutput(WSurface *surface, WOutput *output); + +protected: + void create(WServer *server) override; + void destroy(WServer *server) override; + wl_global *global() const override; + +private: + wlr_backend *m_backend = nullptr; +}; + +WAYLIB_SERVER_END_NAMESPACE diff --git a/waylib/src/server/qtquick/woutputrenderwindow.cpp b/waylib/src/server/qtquick/woutputrenderwindow.cpp index 6b608a5796..9609d036de 100644 --- a/waylib/src/server/qtquick/woutputrenderwindow.cpp +++ b/waylib/src/server/qtquick/woutputrenderwindow.cpp @@ -2,6 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only #include "woutputrenderwindow.h" +#include "wpresentation.h" #include #include #include "woutput.h" @@ -484,6 +485,11 @@ class Q_DECL_HIDDEN WOutputRenderWindowPrivate : public QQuickWindowPrivate bool releaseRenderBuffer(WBufferRenderer *renderer, const char *purpose); bool releaseRenderBuffers(QVector> &needsCommit); void cleanupRetiredRenderResources(bool force); + void setCurrentPresentationOutput(WOutput *output); + void markSurfaceTexturedForPresentation(WSurface *surface); + void submitPresentationFeedbackForOutput(WOutput *output); + void clearPresentationFeedbackForOutput(WOutput *output); + void clearPresentationFeedback(); inline void failCurrentFrame() { frameFailed = true; @@ -529,6 +535,9 @@ class Q_DECL_HIDDEN WOutputRenderWindowPrivate : public QQuickWindowPrivate WPointer m_allocator; bool textureSyncQueueValidated = false; bool textureSyncBatchDisabled = false; + QPointer m_presentation; + QPointer m_currentPresentationOutput; + QHash>> m_pendingPresentationSurfaces; QList outputs; QList layers; @@ -674,15 +683,19 @@ bool OutputHelper::render(WBufferRenderer *renderer, int sourceIndex, const QMat preserveColorContents); } + auto *presentationOutput = m_output ? m_output->output() : nullptr; + windowPrivate->setCurrentPresentationOutput(presentationOutput); windowPrivate->pushRenderer(renderer); const bool ok = renderer->render(sourceIndex, renderMatrix, sourceRect, targetRect, preserveColorContents); + windowPrivate->setCurrentPresentationOutput(nullptr); if (!ok) { qCWarning(lcWlBufferRenderer) << "WBufferRenderer render pass failed" << "renderer" << renderer << "sourceIndex" << sourceIndex << "currentBuffer" << renderer->currentBuffer() << "lastBuffer" << renderer->lastBuffer(); + windowPrivate->clearPresentationFeedbackForOutput(presentationOutput); } return ok; } @@ -1873,6 +1886,66 @@ void WOutputRenderWindowPrivate::cleanupRetiredRenderResources(bool force) } } +void WOutputRenderWindowPrivate::setCurrentPresentationOutput(WOutput *output) +{ + m_currentPresentationOutput = output; +} + +void WOutputRenderWindowPrivate::markSurfaceTexturedForPresentation(WSurface *surface) +{ + auto *output = m_currentPresentationOutput.data(); + if (!m_presentation || !m_presentation->isValid() || !surface || !output) + return; + + if (surface->framePacingOutput() != output) + return; + + auto &surfaces = m_pendingPresentationSurfaces[output]; + for (const auto &existing : std::as_const(surfaces)) { + if (existing == surface) + return; + } + + surfaces.append(surface); +} + +void WOutputRenderWindowPrivate::submitPresentationFeedbackForOutput(WOutput *output) +{ + if (!output) { + clearPresentationFeedback(); + return; + } + + const auto surfaces = m_pendingPresentationSurfaces.take(output); + if (surfaces.isEmpty() || !m_presentation || !m_presentation->isValid()) + return; + + int submittedCount = 0; + for (const auto &surface : surfaces) { + if (!surface || surface->framePacingOutput() != output) + continue; + + m_presentation->surfaceTexturedOnOutput(surface, output); + ++submittedCount; + } + + qCDebug(lcWlPresentation) << "Submitted presentation feedback for output" + << "output" << output + << "surfaceCount" << submittedCount; +} + +void WOutputRenderWindowPrivate::clearPresentationFeedbackForOutput(WOutput *output) +{ + if (output) + m_pendingPresentationSurfaces.remove(output); +} + +void WOutputRenderWindowPrivate::clearPresentationFeedback() +{ + m_pendingPresentationSurfaces.clear(); +} + +// ###: QQuickAnimatorController::advance symbol not export static void QQuickAnimatorController_advance(QQuickAnimatorController *ac) { bool running = false; @@ -1977,9 +2050,11 @@ void WOutputRenderWindowPrivate::doRender(wlr_output *needsFrameOutput, fatalRenderError.clear(); W_Q(WOutputRenderWindow); WVulkanTrace::beginFrame(q); + clearPresentationFeedback(); const auto finishFrame = [this, q] (const QList> &committedOutputs) { WRenderHelper::abortTextureSyncBatch(m_renderer); + clearPresentationFeedback(); resetGlState(); // On Intel&Nvidia multi-GPU environment, wlroots using Intel card do render for all @@ -2130,6 +2205,7 @@ void WOutputRenderWindowPrivate::doRender(wlr_output *needsFrameOutput, for (auto i : std::as_const(needsCommit)) { auto output = i.first->outputViewport()->output(); if (allowCommit && (forceRender || Q_UNLIKELY(!i.first->framePending()))) { + submitPresentationFeedbackForOutput(output); const quint32 sequenceBefore = output->handle()->commit_seq; const bool commitOk = i.first->commit(i.second); WVulkanTrace::outputCommitted(q, output->handle(), sequenceBefore, commitOk); @@ -2143,6 +2219,8 @@ void WOutputRenderWindowPrivate::doRender(wlr_output *needsFrameOutput, committedOutputs.append(output); } } + } else { + clearPresentationFeedbackForOutput(output); } if (i.second->currentBuffer()) { @@ -2151,6 +2229,8 @@ void WOutputRenderWindowPrivate::doRender(wlr_output *needsFrameOutput, i.first->resetState(); } + } else { + clearPresentationFeedback(); } if (vulkanFrameCompleted) { @@ -2468,6 +2548,20 @@ wlr_allocator *WOutputRenderWindow::allocator() const return d->m_allocator; } +void WOutputRenderWindow::setPresentation(WPresentation *presentation) +{ + Q_D(WOutputRenderWindow); + d->m_presentation = presentation; + if (!presentation) + d->clearPresentationFeedback(); +} + +void WOutputRenderWindow::markSurfaceTexturedForPresentation(WSurface *surface) +{ + Q_D(WOutputRenderWindow); + d->markSurfaceTexturedForPresentation(surface); +} + WBufferRenderer *WOutputRenderWindow::currentRenderer() const { Q_D(const WOutputRenderWindow); diff --git a/waylib/src/server/qtquick/woutputrenderwindow.h b/waylib/src/server/qtquick/woutputrenderwindow.h index 1e20ef10d2..8383cf649a 100644 --- a/waylib/src/server/qtquick/woutputrenderwindow.h +++ b/waylib/src/server/qtquick/woutputrenderwindow.h @@ -17,6 +17,8 @@ class WOutputViewport; class WOutputLayer; class WBufferRenderer; class WOutputHelper; +class WPresentation; +class WSurface; class WOutputRenderWindowPrivate; class WAYLIB_SERVER_EXPORT WOutputRenderWindow : public QQuickWindow, public QQmlParserStatus { @@ -58,7 +60,10 @@ class WAYLIB_SERVER_EXPORT WOutputRenderWindow : public QQuickWindow, public QQm WBufferRenderer *currentRenderer() const; bool inRendering() const; - void setRenderEnabled(bool enabled); + void setRenderEnabled(bool enabled); void setPresentation(WPresentation *presentation); + void markSurfaceTexturedForPresentation(WSurface *surface); + + static QList> paintOrderItemList(QQuickItem *root, std::function filter); diff --git a/waylib/src/server/qtquick/wsurfaceitem.cpp b/waylib/src/server/qtquick/wsurfaceitem.cpp index 9175baf763..a0f4449456 100644 --- a/waylib/src/server/qtquick/wsurfaceitem.cpp +++ b/waylib/src/server/qtquick/wsurfaceitem.cpp @@ -658,22 +658,91 @@ class Q_DECL_HIDDEN WSGRenderFootprintNode: public QSGRenderNode return NoExternalRendering | BoundedRectRendering | DepthAwareRendering | OpaqueRendering; } +protected: + void traceVulkanBinding(const void *provider, wlr_texture *texture) + { + if (!m_owner) + return; + + auto *ownerPrivate = m_owner->d_func(); + if (auto *renderWindow = m_owner->outputRenderWindow()) { + WVulkanTrace::surfaceFootprint(renderWindow, provider, texture, + ownerPrivate->surface); + renderWindow->markSurfaceTexturedForPresentation(ownerPrivate->surface); + } + } + public: QPointer m_owner; }; +class Q_DECL_HIDDEN WSGVulkanRenderFootprintNode final : public WSGRenderFootprintNode +{ +public: + explicit WSGVulkanRenderFootprintNode(WSurfaceItemContent *owner) + : WSGRenderFootprintNode(owner) + { + } + + void setBinding(const void *provider, wlr_texture *texture) + { + m_provider = provider; + m_texture = texture; + } + + void render(const RenderState *state) override + { + WSGRenderFootprintNode::render(state); + traceVulkanBinding(m_provider, m_texture); + } + +private: + const void *m_provider = nullptr; + wlr_texture *m_texture = nullptr; +}; + QSGNode *WSurfaceItemContent::updatePaintNode(QSGNode *oldNode, UpdatePaintNodeData *) { W_D(WSurfaceItemContent); auto tp = wTextureProvider(); - if (d->live || !tp->texture()) { - auto texture = d->surface ? wlr_surface_get_texture(d->surface->handle()) : nullptr; - if (texture) { - tp->setTexture(texture, d->buffer.get()); - } else { - tp->setBuffer(d->buffer.get()); + const bool vulkanRhi = usesVulkanRhi(tp->window()); + if (!vulkanRhi) { + if (d->live || !tp->texture()) { + auto texture = d->surface ? wlr_surface_get_texture(d->surface->handle()) : nullptr; + if (texture) { + tp->setTexture(texture, d->buffer.get()); + } else { + tp->setBuffer(d->buffer.get()); + } + } + + if (!tp->texture() || width() <= 0 || height() <= 0) { + delete oldNode; + return nullptr; } + + auto node = static_cast(oldNode); + if (Q_UNLIKELY(!node)) { + node = window()->createImageNode(); + node->setOwnsTexture(false); + QSGNode *fpnode = new WSGRenderFootprintNode(this); + node->appendChildNode(fpnode); + } + + auto texture = tp->texture(); + node->setTexture(texture); + const QRectF textureGeometry = d->bufferSourceBox; + node->setSourceRect(textureGeometry); + const QRectF targetGeometry(d->ignoreBufferOffset ? QPointF() : d->bufferOffset, size()); + node->setRect(targetGeometry); + node->setFiltering(smooth() ? QSGTexture::Linear : QSGTexture::Nearest); + + return node; + } + + if (d->live || !tp->texture()) { + tp->setBuffer(d->buffer.get()); } if (!tp->texture() || width() <= 0 || height() <= 0) { @@ -682,15 +751,21 @@ QSGNode *WSurfaceItemContent::updatePaintNode(QSGNode *oldNode, UpdatePaintNodeD } auto node = static_cast(oldNode); + WSGVulkanRenderFootprintNode *vulkanFootprintNode = nullptr; if (Q_UNLIKELY(!node)) { node = window()->createImageNode(); node->setOwnsTexture(false); - QSGNode *fpnode = new WSGRenderFootprintNode(this); - node->appendChildNode(fpnode); + vulkanFootprintNode = new WSGVulkanRenderFootprintNode(this); + node->appendChildNode(vulkanFootprintNode); + } else { + vulkanFootprintNode = + static_cast(node->firstChild()); } auto texture = tp->texture(); node->setTexture(texture); + Q_ASSERT(vulkanFootprintNode); + vulkanFootprintNode->setBinding(tp, tp->qwTexture()); const QRectF textureGeometry = d->bufferSourceBox; node->setSourceRect(textureGeometry); const QRectF targetGeometry(d->ignoreBufferOffset ? QPointF() : d->bufferOffset, size()); diff --git a/waylib/src/server/wayliblogging.cpp b/waylib/src/server/wayliblogging.cpp index f089436b12..31bfc9a386 100644 --- a/waylib/src/server/wayliblogging.cpp +++ b/waylib/src/server/wayliblogging.cpp @@ -54,6 +54,7 @@ Q_LOGGING_CATEGORY(lcWlInputMethod, "waylib.protocols.inputmethod", QtInfoMsg) / Q_LOGGING_CATEGORY(lcWlExtForeignToplevel, "waylib.protocols.extforeigntoplevel", QtWarningMsg) // ext-foreign-toplevel-list-v1 Q_LOGGING_CATEGORY(lcWlTextInput, "waylib.protocols.textinput", QtInfoMsg) // Text input v1/v2/v3 protocol Q_LOGGING_CATEGORY(lcWlLinuxDmabuf, "waylib.protocols.linuxdmabuf", QtInfoMsg) // Renderer-backed linux-dmabuf global +Q_LOGGING_CATEGORY(lcWlPresentation, "waylib.protocols.presentation", QtInfoMsg) // Presentation-time feedback Q_LOGGING_CATEGORY(lcWlRemoteSubsurface, "waylib.protocols.remotesubsurface", QtWarningMsg) // treeland-remote-subsurface-unstable-v1 // Layer shell diff --git a/waylib/src/server/wayliblogging.h b/waylib/src/server/wayliblogging.h index b73dc32f87..6d8f2a3ff9 100644 --- a/waylib/src/server/wayliblogging.h +++ b/waylib/src/server/wayliblogging.h @@ -47,6 +47,7 @@ Q_DECLARE_LOGGING_CATEGORY(lcWlInputMethod) Q_DECLARE_LOGGING_CATEGORY(lcWlExtForeignToplevel) Q_DECLARE_LOGGING_CATEGORY(lcWlTextInput) Q_DECLARE_LOGGING_CATEGORY(lcWlLinuxDmabuf) +Q_DECLARE_LOGGING_CATEGORY(lcWlPresentation) Q_DECLARE_LOGGING_CATEGORY(lcWlRemoteSubsurface) // Layer shell From 6a4140935ddda703cc2767859ba577bf8545c90b Mon Sep 17 00:00:00 2001 From: LFRon Date: Wed, 2 Sep 2026 11:40:06 +0800 Subject: [PATCH 07/15] feat(vulkan): enable DRM hardware cursor Render the cursor QML item into a cursor-plane scanout buffer and hand it to the KMS cursor plane, instead of always compositing it through the canvas. This keeps the cursor cheap on the Vulkan path, where a canvas repaint per pointer move would cost a full synchronous frame. - SourceOutput.qml detaches the cursor item geometry from the live pointer position (pinned at (0,0)) when an output layer carries the cursor, so moving the pointer no longer dirties the main scene. The on-screen placement is computed from wlroots cursor/output coordinates during layer rendering, and moves go through move_cursor; woutputitem schedules an explicit output frame for the placement update, since the detached geometry no longer dirties the scene itself. - OutputHelper::tryToHardwareCursor(): on Vulkan, re-render the cursor QML item directly into a cursor-format scanout buffer (sampling the already FOREIGN-released layer buffer would yield undefined contents), falling back to software compositing when no suitable cursor size exists or the plane rejects the buffer. - Defer set_cursor() until the frame's command buffer has been submitted and completed (endFrameChecked waits on a fence): the buffer handed to the cursor plane is produced by that command buffer, so committing it earlier could let the plane scan out content the GPU has not rendered yet. move_cursor stays in-frame for pointer latency; the deferred commit is cancelled whenever the frame does not complete or the fallback to the software cursor is taken, and a failed deferred set_cursor keeps the cursor on the software canvas path until its content is re-rendered. Follow-up (folded): move the DRM cursor plane directly on pointer motion when it is already set up (moveHardwareCursorForPosition), skipping the full compositor frame that the detached cursor geometry would otherwise schedule; fall back to scheduling a frame when the plane is not ready, a software cursor lock is held, or the move fails. --- src/core/qml/SourceOutput.qml | 16 +- .../server/qtquick/private/woutputitem_p.h | 4 + waylib/src/server/qtquick/woutputitem.cpp | 25 ++ .../server/qtquick/woutputrenderwindow.cpp | 381 +++++++++++++++++- .../src/server/qtquick/woutputrenderwindow.h | 7 + 5 files changed, 414 insertions(+), 19 deletions(-) diff --git a/src/core/qml/SourceOutput.qml b/src/core/qml/SourceOutput.qml index c8ce8f9f55..2eb75b3780 100644 --- a/src/core/qml/SourceOutput.qml +++ b/src/core/qml/SourceOutput.qml @@ -17,7 +17,17 @@ OutputItem { id: cursorItem required property QtObject outputCursor - readonly property point rawPosition: parent.mapFromGlobal(cursor.position.x, cursor.position.y) + // When the cursor is carried by a hardware/output layer on the Vulkan + // path, detach its geometry from the live pointer position so that moving + // the cursor does not dirty the main QML scene. The layer placement is + // then computed from wlroots cursor/output coordinates during layer + // rendering (see OutputHelper::renderLayer). GLES2 keeps the legacy + // scene-bound cursor geometry. + readonly property bool detachedCursorLayer: OutputLayer.enabled + && GraphicsInfo.api === GraphicsInfo.Vulkan + readonly property point rawPosition: detachedCursorLayer + ? Qt.point(0, 0) + : parent.mapFromGlobal(cursor.position.x, cursor.position.y) readonly property real effectiveScale: rootOutputItem.devicePixelRatio || 1.0 // Align cursor position to pixel grid to prevent blur on fractional DPR displays @@ -32,8 +42,8 @@ OutputItem { cursor: outputCursor.cursor output: outputCursor.output.output - x: position.x - hotSpot.x - y: position.y - hotSpot.y + x: detachedCursorLayer ? 0 : position.x - hotSpot.x + y: detachedCursorLayer ? 0 : position.y - hotSpot.y visible: valid && outputCursor.visible OutputLayer.enabled: !outputCursor.output.forceSoftwareCursor OutputLayer.keepLayer: true diff --git a/waylib/src/server/qtquick/private/woutputitem_p.h b/waylib/src/server/qtquick/private/woutputitem_p.h index 255e9074c5..dccca67724 100644 --- a/waylib/src/server/qtquick/private/woutputitem_p.h +++ b/waylib/src/server/qtquick/private/woutputitem_p.h @@ -34,6 +34,10 @@ class WAYLIB_SERVER_EXPORT WOutputCursor : public QObject WCursor *cursor() const; bool visible() const; + // The QML cursor delegate item (a WQuickCursor). Used to drive the DRM + // hardware cursor directly on pointer movement without a full frame. + QQuickItem *cursorItem() const { return item; } + Q_SIGNALS: void visibleChanged(); diff --git a/waylib/src/server/qtquick/woutputitem.cpp b/waylib/src/server/qtquick/woutputitem.cpp index 2166a7c7d8..5361b00a4f 100644 --- a/waylib/src/server/qtquick/woutputitem.cpp +++ b/waylib/src/server/qtquick/woutputitem.cpp @@ -4,6 +4,7 @@ #include "woutputitem.h" #include "woutputitem_p.h" #include "woutput.h" +#include "woutputrenderwindow.h" #include "wquickoutputlayout.h" #include "wquickcursor.h" #include "private/wglobal_p.h" @@ -12,6 +13,8 @@ #include +#include + #include WAYLIB_SERVER_BEGIN_NAMESPACE @@ -157,6 +160,28 @@ void WOutputItemPrivate::updateCursors() } updateCursorVisible(safeOc.data()); + // On the Vulkan path the cursor item geometry is detached from the + // live pointer position (see SourceOutput.qml), so moving the cursor + // no longer dirties the scene. If the DRM hardware cursor plane is + // already set up, move it directly and skip the whole compositor + // frame; otherwise (software cursor fallback, plane not set up yet, + // or the cursor is becoming (in)visible) schedule a frame as before. + // GLES2 keeps relying on scene dirtying. + auto *outputItem = safeOc->output(); + if (outputItem && outputItem->output() && outputItem->window() + && outputItem->window()->graphicsApi() == QSGRendererInterface::Vulkan) { + auto *cursorItem = safeOc->cursorItem(); + if (safeOc->visible() && cursorItem) { + if (auto *renderWindow = + qobject_cast(cursorItem->window())) { + if (renderWindow->tryMoveHardwareCursor(outputItem->output(), + cursorItem)) { + return; + } + } + } + outputItem->output()->scheduleFrame(); + } }); } diff --git a/waylib/src/server/qtquick/woutputrenderwindow.cpp b/waylib/src/server/qtquick/woutputrenderwindow.cpp index 9609d036de..ac14e41d92 100644 --- a/waylib/src/server/qtquick/woutputrenderwindow.cpp +++ b/waylib/src/server/qtquick/woutputrenderwindow.cpp @@ -2,6 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only #include "woutputrenderwindow.h" +#include "wquickcursor.h" #include "wpresentation.h" #include #include @@ -251,6 +252,10 @@ class Q_DECL_HIDDEN OutputHelper : public WOutputHelper WBufferRenderer *compositeLayers(const QList layers, bool forceShadowRenderer); bool commit(WBufferRenderer *buffer); bool tryToHardwareCursor(const LayerData *layer); + bool moveHardwareCursorForPosition(WQuickCursor *cursorItem); + void deferHardwareCursorCommit(wlr_buffer *buffer, const QPoint &hotSpot); + void flushPendingHardwareCursor(); + void cancelPendingHardwareCursor(); private: WOutputViewport *m_output = nullptr; @@ -259,8 +264,22 @@ class Q_DECL_HIDDEN OutputHelper : public WOutputHelper // only for render cursor QPointer m_cursorRenderer; BufferRendererProxy *m_cursorLayerProxy = nullptr; + // Vulkan only: the cursor QML item rendered directly into the scanout + // buffer (instead of sampling the layer buffer released to FOREIGN). + QPointer m_cursorSourceItem; bool m_cursorDirty = false; bool m_hardwareCursorRenderComplete = false; + // Vulkan only: the cursor buffer rendered into the current frame's command + // buffer. set_cursor() must wait until that command buffer has been + // submitted and completed, or the cursor plane can scan out content that + // the GPU has not rendered yet. + wlr_buffer *m_pendingCursorBuffer = nullptr; + QPoint m_pendingCursorHotSpot; + // Set when a deferred set_cursor() failed, so later frames keep the cursor + // on the software canvas path until the cursor content is re-rendered. + // Retrying the same buffer every frame would otherwise busy-loop the + // compositor's render loop. + bool m_hardwareCursorCommitBroken = false; // for compositeLayers QPointer m_output2; @@ -604,7 +623,15 @@ void OutputHelper::detachLayer(OutputLayer *layer) Q_ASSERT(index >= 0); auto l = m_layers.takeAt(index); - if (m_cursorLayerProxy && m_cursorLayerProxy->sourceItem() == l->renderer) { + // The cursor render resources reference either the layer's rendered buffer + // (GLES2 proxy path) or the cursor QML item directly (Vulkan path). Clear + // the hardware cursor and drop them when that source layer goes away so we + // never keep a dangling source. + const bool isCursorSource = + (m_cursorLayerProxy && m_cursorLayerProxy->sourceItem() == l->renderer) + || (m_cursorRenderer && m_cursorSourceItem + && m_cursorSourceItem == l->layer->layer->parent()); + if (isCursorSource) { // Clear hardware cursor tryToHardwareCursor(nullptr); cleanCursorRender(); @@ -660,6 +687,7 @@ void OutputHelper::cleanCursorRender() m_cursorRenderer->deleteLater(); m_cursorRenderer = nullptr; m_cursorLayerProxy = nullptr; + m_cursorSourceItem = nullptr; } } @@ -754,8 +782,17 @@ wlr_buffer *OutputHelper::renderLayer(LayerData *layer, bool *dontEndRenderAndRe const auto layerFlags = layer->layer->layer->flags(); const bool sizeSensitive = layerFlags & WOutputLayer::SizeSensitive; + const bool isVulkanLayer = WRenderHelper::getGraphicsApi(renderWindowD()->rc()) + == QSGRendererInterface::Vulkan; + // On Vulkan the cursor item is detached from the live pointer position + // (fixed at (0,0) in PrimaryOutput.qml), so its output placement must be + // derived from the wlroots cursor/output coordinates rather than the item + // geometry. GLES2 keeps the geometry-driven path unchanged. + const bool cursorLayer = (layerFlags & WOutputLayer::Cursor) && isVulkanLayer; const bool isRef = layer->mapFrom && layer->mapTo; - if (isRef) { + if (cursorLayer) { + viewportMatrix = outputViewport()->mapToViewport(source->parentItem()); + } else if (isRef) { viewportMatrix = outputViewport()->mapToViewport(layer->mapTo); const auto xScale = layer->mapTo->width() / layer->mapFrom->outputViewport()->width(); const auto yScale = layer->mapTo->height() / layer->mapFrom->outputViewport()->height(); @@ -777,8 +814,26 @@ wlr_buffer *OutputHelper::renderLayer(LayerData *layer, bool *dontEndRenderAndRe // matrix function: map source to output buffer const auto outputMatrix = viewportMatrix * outputViewport()->sourceRectToTargetRectTransfrom(); - noClipMapRect = outputMatrix.mapRect(noClipMapRect); - mapRect = outputMatrix.mapRect(mapRect); + if (cursorLayer) { + auto cursorItem = qobject_cast(source); + if (!cursorItem) { + qCWarning(lcWlCursor) + << "Vulkan cursor layer source is not a WQuickCursor; skipping cursor layer" + << "output" << outputViewport() + << "source" << source; + return nullptr; + } + if (!cursorItem->cursor()) + return nullptr; + + const QPointF outputLocalPosition = cursorItem->cursor()->position() + - QPointF(outputViewport()->output()->position()); + noClipMapRect = QRectF(outputLocalPosition - cursorItem->hotSpot(), source->size()); + mapRect = noClipMapRect; + } else { + noClipMapRect = outputMatrix.mapRect(noClipMapRect); + mapRect = outputMatrix.mapRect(mapRect); + } QTransform revertScaleTransform; if (!sizeSensitive) { @@ -818,7 +873,7 @@ wlr_buffer *OutputHelper::renderLayer(LayerData *layer, bool *dontEndRenderAndRe } Q_ASSERT(!pixelSize.isEmpty()); - QMatrix4x4 renderMatrix = revertScaleTransform * viewportMatrix; + QMatrix4x4 renderMatrix = cursorLayer ? QMatrix4x4() : revertScaleTransform * viewportMatrix; if (isRef) { renderMatrix = layer->mapFromLayer->renderMatrix * renderMatrix; } @@ -1274,12 +1329,15 @@ bool OutputHelper::commit(WBufferRenderer *buffer) bool OutputHelper::tryToHardwareCursor(const LayerData *layer) { + const bool isVulkan = WRenderHelper::getGraphicsApi(renderWindowD()->rc()) + == QSGRendererInterface::Vulkan; do { auto set_cursor = output()->impl->set_cursor; auto buffer = layer && layer->renderer->lastBuffer() ? layer->renderer->lastBuffer() : nullptr; if (!buffer) { + cancelPendingHardwareCursor(); if (!m_hardwareCursorRenderComplete) return true; @@ -1352,14 +1410,43 @@ bool OutputHelper::tryToHardwareCursor(const LayerData *layer) } } + // On Vulkan the cursor layer buffer uses the renderer's render + // format/modifier, which is not guaranteed to be scanout-capable for the + // cursor plane (typically LINEAR only). Always repaint the cursor into a + // cursor-format buffer (configured via get_cursor_formats) so set_cursor + // can import it reliably, even when the size already matches. + if (isVulkan && get_cursor_sizes && get_cursor_formsts) + needsRepaintCursor = true; + + if (Q_UNLIKELY(lcWlCursor().isDebugEnabled())) + qCDebug(lcWlCursor) + << "Attempting hardware cursor" + << "output" << output() + << "layerBufferSize" << QSize(buffer->width, buffer->height) + << "scanoutSize" << pixelSize + << "devicePixelRatio" << devicePixelRatio() + << "needsRepaintCursor" << needsRepaintCursor + << "directQmlRender" << isVulkan; + if (needsRepaintCursor) { // needs render cursor again + auto *cursorSourceItem = layer->layer->layer->parent(); if (!m_cursorRenderer) { m_cursorRenderer = new WBufferRenderer(renderWindow()->contentItem()); if (visualizeLayers()) m_cursorRenderer->setClearColor(Qt::cyan); - m_cursorLayerProxy = new BufferRendererProxy(m_cursorRenderer); - m_cursorRenderer->setSourceList({m_cursorLayerProxy}, false); + if (isVulkan) { + // Render the cursor QML item directly into the scanout buffer. + // Sampling the layer buffer is unsafe on Vulkan: it has already + // been released to VK_QUEUE_FAMILY_FOREIGN_EXT, so re-sampling it + // without a queue-family re-acquire yields undefined contents + // (typically a fully transparent, i.e. invisible, cursor). + m_cursorRenderer->setSourceList({cursorSourceItem}, false); + m_cursorSourceItem = cursorSourceItem; + } else { + m_cursorLayerProxy = new BufferRendererProxy(m_cursorRenderer); + m_cursorRenderer->setSourceList({m_cursorLayerProxy}, false); + } m_cursorRenderer->setOutput(m_output->output()); m_cursorRenderer->setVisible(false); // for the new WBufferRenderer and WQuickTextureProxy @@ -1369,10 +1456,22 @@ bool OutputHelper::tryToHardwareCursor(const LayerData *layer) m_cursorDirty = true; }); } - m_cursorLayerProxy->setRenderer(layer->renderer); - // Ensure render size same as source buffer size - m_cursorLayerProxy->setWidth(buffer->width); - m_cursorLayerProxy->setHeight(buffer->height); + + if (isVulkan) { + // The cursor item can be recreated; keep the source list current so + // we never render a stale/dangling item. + if (m_cursorSourceItem != cursorSourceItem) { + m_cursorRenderer->setSourceList({cursorSourceItem}, false); + m_cursorSourceItem = cursorSourceItem; + renderWindowD()->updateDirtyNodes(); + m_cursorDirty = true; + } + } else { + m_cursorLayerProxy->setRenderer(layer->renderer); + // Ensure render size same as source buffer size + m_cursorLayerProxy->setWidth(buffer->width); + m_cursorLayerProxy->setHeight(buffer->height); + } auto newBuffer = m_cursorRenderer->lastBuffer(); @@ -1385,25 +1484,104 @@ bool OutputHelper::tryToHardwareCursor(const LayerData *layer) newBuffer = m_cursorRenderer->beginRender(pixelSize, 1.0, DRM_FORMAT_ARGB8888, WBufferRenderer::UseCursorFormats); if (newBuffer) { - m_cursorRenderer->render(0, {}); + if (!isVulkan) { + m_cursorRenderer->render(0, {}); + } else { + // The cursor QML item is laid out in logical (item) + // coordinates, but the hardware cursor plane scans the buffer + // out 1:1 in device pixels. Scale the item by the output device + // pixel ratio so the cursor is rasterized at its device pixel + // size and therefore follows fractional scaling. This matches + // the GLES2 proxy path, which renders the already device-sized + // layer buffer 1:1. The hotspot (cursorHotSpot * dpr) uses the + // same scale, so it stays consistent with the rendered image. + QMatrix4x4 cursorScale; + cursorScale.scale(devicePixelRatio(), devicePixelRatio()); + if (!m_cursorRenderer->render(0, cursorScale)) { + qCWarning(lcWlBufferRenderer) + << "Skipping cursor buffer because render pass failed" + << "output" << output() + << "renderer" << m_cursorRenderer + << "cursorSourceItem" << m_cursorSourceItem.data() + << "targetSize" << pixelSize + << "currentBuffer" << m_cursorRenderer->currentBuffer(); + (void)renderWindowD()->releaseRenderBuffer( + m_cursorRenderer, "cursor-render-buffer-abort"); + newBuffer = nullptr; + } else if (!renderWindowD()->releaseRenderBuffer( + m_cursorRenderer, "cursor-render-buffer")) { + qCWarning(lcWlBufferRenderer) + << "Skipping cursor buffer because Vulkan release failed" + << "output" << output() + << "renderer" << m_cursorRenderer + << "targetSize" << pixelSize + << "currentBuffer" << m_cursorRenderer->currentBuffer(); + newBuffer = nullptr; + } + } m_cursorRenderer->endRender(); + } else { + qCWarning(lcWlBufferRenderer) + << "Failed to begin cursor buffer render" + << "output" << output() + << "targetSize" << pixelSize + << "directQmlRender" << isVulkan; } - m_cursorDirty = false; + m_cursorDirty = isVulkan ? !newBuffer : false; + // The cursor content was re-rendered into a fresh buffer, so + // a previously failed deferred set_cursor() is worth one + // retry with the new buffer. + if (newBuffer && isVulkan) + m_hardwareCursorCommitBroken = false; } if (newBuffer) { Q_ASSERT(pixelSize.width() == newBuffer->width); Q_ASSERT(pixelSize.height() == newBuffer->height); buffer = newBuffer; + } else if (isVulkan) { + // Never hand a null buffer to set_cursor(): that disables the + // hardware cursor while the caller simultaneously removes the + // cursor layer from software compositing, which makes the cursor + // vanish entirely. Fall back to the software cursor instead. + qCWarning(lcWlCursor) + << "Vulkan hardware cursor buffer unavailable; falling back to software cursor" + << "output" << output() + << "requestedSize" << pixelSize; + break; } else { buffer = nullptr; } } - const auto hotSpot = layer->renderMatrix.map(layer->layer->layer->cursorHotSpot() - * devicePixelRatio()).toPoint(); - if (!set_cursor(output(), buffer, hotSpot.x(), hotSpot.y())) { + // On Vulkan the cursor item is rendered directly at the top-left of the + // scanout buffer with an identity transform, so the hotspot is simply the + // cursor hotspot scaled to device pixels. The GLES2 proxy path keeps the + // historical renderMatrix-based mapping. + const QPoint hotSpot = isVulkan + ? (layer->layer->layer->cursorHotSpot() * devicePixelRatio()).toPoint() + : layer->renderMatrix.map(layer->layer->layer->cursorHotSpot() + * devicePixelRatio()).toPoint(); + if (isVulkan) { + if (m_hardwareCursorCommitBroken) { + // A previous deferred set_cursor() failed for this output; + // keep the cursor on the software canvas path until the cursor + // content is re-rendered. + break; + } + // The buffer content is produced by the current frame's command + // buffer, which is only submitted and awaited at endFrame(). + // Defer set_cursor() until after that point so the cursor plane + // can never scan out content the GPU has not rendered yet. + deferHardwareCursorCommit(buffer, hotSpot); + m_hardwareCursorRenderComplete = true; + } else if (!set_cursor(output(), buffer, hotSpot.x(), hotSpot.y())) { + qCWarning(lcWlCursor) + << "wlr output set_cursor failed; falling back to software cursor" + << "output" << output() + << "bufferSize" << QSize(buffer->width, buffer->height) + << "hotSpot" << hotSpot; break; } else { m_hardwareCursorRenderComplete = true; @@ -1420,9 +1598,25 @@ bool OutputHelper::tryToHardwareCursor(const LayerData *layer) output()->transform, outputSize.width(), outputSize.height()); if (!move_cursor(output(), cleanTransform.x, cleanTransform.y)) { + qCWarning(lcWlCursor) + << "wlr output move_cursor failed; falling back to software cursor" + << "output" << output() + << "position" << QPoint(cleanTransform.x, cleanTransform.y); + // The caller will composite the cursor into the canvas instead, so + // a deferred set_cursor() for this frame must not run afterwards. + cancelPendingHardwareCursor(); break; } + if (Q_UNLIKELY(lcWlCursor().isDebugEnabled())) + qCDebug(lcWlCursor) + << "Hardware cursor committed" + << "output" << output() + << "bufferSize" << QSize(buffer->width, buffer->height) + << "hotSpot" << hotSpot + << "position" << QPoint(cleanTransform.x, cleanTransform.y) + << "directQmlRender" << isVulkan; + return true; } while (false); @@ -1431,6 +1625,117 @@ bool OutputHelper::tryToHardwareCursor(const LayerData *layer) return false; } +bool OutputHelper::moveHardwareCursorForPosition(WQuickCursor *cursorItem) +{ + WOutputViewport *viewport = outputViewport(); + if (!cursorItem || !viewport) + return false; + + WOutput *woutput = viewport->output(); + wlr_output *wlrout = output(); + if (!woutput || !wlrout) + return false; + + const bool isVulkan = WRenderHelper::getGraphicsApi(renderWindowD()->rc()) + == QSGRendererInterface::Vulkan; + if (!isVulkan) + return false; + + auto move_cursor = wlrout->impl->move_cursor; + if (!move_cursor) + return false; + + // A full frame is still required until the hardware cursor plane has been + // set up at least once (and is not in the broken/software fallback state). + if (!m_hardwareCursorRenderComplete || m_hardwareCursorCommitBroken) + return false; + if (wlrout->software_cursor_locks > 0) + return false; + + auto cursor = cursorItem->cursor(); + if (!cursor) + return false; + + // Mirror the Vulkan cursor-layer placement used by renderLayer() + + // tryToHardwareCursor(): on Vulkan the cursor item is detached from the + // live pointer position, so the plane position is derived from the wlroots + // cursor/output coordinates. move_cursor() itself inverts the output + // transform and subtracts the hotspot, so apply the same forward transform + // tryToHardwareCursor() does to compensate. + const qreal dpr = devicePixelRatio(); + const QPointF outputLocalPosition = + cursor->position() - QPointF(woutput->position()); + const QPointF hotSpot = cursorItem->hotSpot(); + QRectF mapRect(QPointF(outputLocalPosition - hotSpot), cursorItem->size()); + mapRect &= QRectF(QPointF(0, 0), viewport->size()); + // Bit-identical to tryToHardwareCursor()'s + // layer->mapToOutput.topLeft() + cursorHotSpot() * dpr + // so a pointer move and a frame-driven move never differ by a rounding pixel. + const QPoint pos = (mapRect.topLeft() * dpr).toPoint() + + (hotSpot * dpr).toPoint(); + + wlr_box box { .x = pos.x(), .y = pos.y(), .width = 0, .height = 0 }; + const auto outputSize = woutput->size(); + wlr_box_transform(&box, &box, wlrout->transform, + outputSize.width(), outputSize.height()); + + if (!move_cursor(wlrout, box.x, box.y)) { + qCWarning(lcWlCursor) + << "Direct hardware cursor move failed; falling back to a full frame" + << "output" << wlrout + << "position" << QPoint(box.x, box.y); + return false; + } + + return true; +} + +void OutputHelper::deferHardwareCursorCommit(wlr_buffer *buffer, const QPoint &hotSpot) +{ + Q_ASSERT(buffer); + m_pendingCursorBuffer = buffer; + m_pendingCursorHotSpot = hotSpot; +} + +void OutputHelper::cancelPendingHardwareCursor() +{ + m_pendingCursorBuffer = nullptr; + m_pendingCursorHotSpot = QPoint(); +} + +void OutputHelper::flushPendingHardwareCursor() +{ + if (!m_pendingCursorBuffer) + return; + + auto set_cursor = output()->impl->set_cursor; + wlr_buffer *buffer = m_pendingCursorBuffer; + const QPoint hotSpot = m_pendingCursorHotSpot; + cancelPendingHardwareCursor(); + + if (!set_cursor) { + m_hardwareCursorRenderComplete = false; + return; + } + + if (!set_cursor(output(), buffer, hotSpot.x(), hotSpot.y())) { + qCWarning(lcWlCursor) + << "Deferred wlr output set_cursor failed; retrying via a new frame with the software cursor" + << "output" << output() + << "bufferSize" << QSize(buffer->width, buffer->height) + << "hotSpot" << hotSpot; + if (set_cursor) + set_cursor(output(), nullptr, 0, 0); + m_hardwareCursorRenderComplete = false; + // Arm the breaker: retrying the same buffer every frame would + // busy-loop the render loop (tryToHardwareCursor() checks this flag + // before deferring a commit and falls back to the software canvas + // instead). It is disarmed when the cursor content is re-rendered. + m_hardwareCursorCommitBroken = true; + update(); + } +} + int WOutputRenderWindowPrivate::indexOfOutputHelper(const WOutputViewport *output) const { for (int i = 0; i < outputs.size(); ++i) { @@ -1510,6 +1815,13 @@ void WOutputRenderWindowPrivate::init() void WOutputRenderWindowPrivate::init(OutputHelper *helper) { + if (graphicsApi() == QSGRendererInterface::Vulkan) { + qCInfo(lcWlCursor) << "Vulkan hardware cursor path enabled for output" + << "output" << helper->outputViewport()->output() + << "forceSoftwareCursor" + << helper->outputViewport()->output()->forceSoftwareCursor(); + } + QMetaObject::invokeMethod(helper, &WOutputHelper::scheduleFrame, Qt::QueuedConnection); helper->init(); QObject::connect(helper->outputViewport(), &WOutputViewport::dependsChanged, helper, [this] { @@ -2055,6 +2367,14 @@ void WOutputRenderWindowPrivate::doRender(wlr_output *needsFrameOutput, const auto finishFrame = [this, q] (const QList> &committedOutputs) { WRenderHelper::abortTextureSyncBatch(m_renderer); clearPresentationFeedback(); + + // Drop cursor commits deferred for a frame that did not complete, so + // they can never outlive the frame whose command buffer produced them. + for (auto helper : std::as_const(this->outputs)) { + if (helper) + helper->cancelPendingHardwareCursor(); + } + resetGlState(); // On Intel&Nvidia multi-GPU environment, wlroots using Intel card do render for all @@ -2191,6 +2511,16 @@ void WOutputRenderWindowPrivate::doRender(wlr_output *needsFrameOutput, WVulkanTrace::setFrameStage(q, WVulkanTrace::FrameStage::AfterEndFrame); } + if (vulkanFrameCompleted) { + // The frame's command buffer has completed on the GPU. Commit any + // hardware cursor buffers rendered into it now, so the cursor plane + // can never scan out content the GPU has not rendered yet. + for (auto helper : std::as_const(outputs)) { + if (helper) + helper->flushPendingHardwareCursor(); + } + } + // prevent gles2-render exception in wlroots. // wlroots may have render operations after commit, so do // not move the location during the reset operation. @@ -2562,6 +2892,25 @@ void WOutputRenderWindow::markSurfaceTexturedForPresentation(WSurface *surface) d->markSurfaceTexturedForPresentation(surface); } +bool WOutputRenderWindow::tryMoveHardwareCursor(WOutput *output, QQuickItem *cursorItem) +{ + Q_D(WOutputRenderWindow); + if (!output || !cursorItem) + return false; + + auto *quickCursor = qobject_cast(cursorItem); + if (!quickCursor) + return false; + + for (auto *helper : std::as_const(d->outputs)) { + if (helper && helper->outputViewport() + && helper->outputViewport()->output() == output) + return helper->moveHardwareCursorForPosition(quickCursor); + } + + return false; +} + WBufferRenderer *WOutputRenderWindow::currentRenderer() const { Q_D(const WOutputRenderWindow); diff --git a/waylib/src/server/qtquick/woutputrenderwindow.h b/waylib/src/server/qtquick/woutputrenderwindow.h index 8383cf649a..a8bb80ed95 100644 --- a/waylib/src/server/qtquick/woutputrenderwindow.h +++ b/waylib/src/server/qtquick/woutputrenderwindow.h @@ -63,6 +63,13 @@ class WAYLIB_SERVER_EXPORT WOutputRenderWindow : public QQuickWindow, public QQm void setRenderEnabled(bool enabled); void setPresentation(WPresentation *presentation); void markSurfaceTexturedForPresentation(WSurface *surface); + // Fast path for pointer movement on the Vulkan/DRM hardware-cursor path: + // when the hardware cursor plane is already set up for \a output, move it + // directly and return true, so the caller can skip scheduling a full + // compositor frame. Returns false when a frame is still required (software + // cursor, cursor not set up yet, broken commit, or non-Vulkan backend). + bool tryMoveHardwareCursor(WOutput *output, QQuickItem *cursorItem); + static QList> paintOrderItemList(QQuickItem *root, std::function filter); From dda1abe1ebe7ed482ea99ce04e408b518145663c Mon Sep 17 00:00:00 2001 From: LFRon Date: Wed, 2 Sep 2026 11:50:43 +0800 Subject: [PATCH 08/15] feat(vulkan): initialize Vulkan renderer protocol stack --- src/seat/helper.cpp | 30 +++++++++++++++++++++++------- 1 file changed, 23 insertions(+), 7 deletions(-) diff --git a/src/seat/helper.cpp b/src/seat/helper.cpp index 02d9f5652c..9f62cdd1b3 100644 --- a/src/seat/helper.cpp +++ b/src/seat/helper.cpp @@ -80,7 +80,9 @@ #include #include +#include #include +#include #include #include #include @@ -2220,14 +2222,28 @@ void Helper::init(Treeland::Treeland *treeland) } m_allocator = wlr_allocator_autocreate(m_backend->handle(), m_renderer); - if (!m_allocator) { + if (!m_allocator) qCFatal(lcTlCore) << "Failed to create allocator"; - } - if (!wlr_renderer_init_wl_display(m_renderer, m_server->handle())) { - qCFatal(lcTlCore) << "Failed to initialize renderer wl_display"; - } - if (!wlr_drm_create(m_server->handle(), m_renderer)) { - qCCritical(lcTlCore) << "Failed to create DRM lease manager"; + + const bool vulkanRenderer = + WRenderHelper::getGraphicsApi() == QSGRendererInterface::Vulkan; + if (vulkanRenderer) { + if (!wlr_renderer_init_wl_shm(m_renderer, m_server->handle())) + qCFatal(lcTlCore) << "Failed to initialize wl_shm for Vulkan renderer"; + + m_server->attach(m_renderer); + auto *presentation = m_server->attach(m_backend->handle()); + m_renderWindow->setPresentation(presentation); + + if (!wlr_drm_create(m_server->handle(), m_renderer)) { + qCCritical(lcTlCore) + << "Failed to create legacy wl_drm global"; + } + } else { + if (!wlr_renderer_init_wl_display(m_renderer, m_server->handle())) + qCFatal(lcTlCore) << "Failed to initialize renderer wl_display"; + if (!wlr_drm_create(m_server->handle(), m_renderer)) + qCCritical(lcTlCore) << "Failed to create legacy wl_drm global"; } // free follow display From 343ed657cdcb880e38d054d39da50aa902ee1317 Mon Sep 17 00:00:00 2001 From: LFRon Date: Wed, 2 Sep 2026 11:50:43 +0800 Subject: [PATCH 09/15] fix(vulkan): gate wl_drm on implicit DMA-BUF support Advertise the legacy wl_drm global for Vulkan only when the renderer exposes an implicit-modifier DMA-BUF texture format. This prevents legacy-buffer clients such as Youdao Note from selecting DRM formats the Vulkan renderer cannot import, which showed up as wrong colors and flicker, while leaving the non-Vulkan wl_drm path unchanged. --- src/seat/helper.cpp | 30 +++++++++++++++++++++++++++--- 1 file changed, 27 insertions(+), 3 deletions(-) diff --git a/src/seat/helper.cpp b/src/seat/helper.cpp index 9f62cdd1b3..8f7d619655 100644 --- a/src/seat/helper.cpp +++ b/src/seat/helper.cpp @@ -75,6 +75,7 @@ #include "workspace/workspace.h" #include +#include #include #include @@ -208,6 +209,24 @@ static bool currentPrimaryMatchesId(RootSurfaceContainer *rootContainer, const Q return rootContainer && outputMatchesId(rootContainer->primaryOutput(), outputId); } +static bool rendererSupportsImplicitDmabufFormats(wlr_renderer *renderer) +{ + const auto *formats = renderer + ? wlr_renderer_get_texture_formats(renderer, WLR_BUFFER_CAP_DMABUF) + : nullptr; + if (!formats) + return false; + + for (size_t i = 0; i < formats->len; ++i) { + const auto &format = formats->formats[i]; + for (size_t j = 0; j < format.len; ++j) { + if (format.modifiers[j] == DRM_FORMAT_MOD_INVALID) + return true; + } + } + return false; +} + Helper *Helper::m_instance = nullptr; Helper::Helper(QObject *parent) @@ -2235,9 +2254,14 @@ void Helper::init(Treeland::Treeland *treeland) auto *presentation = m_server->attach(m_backend->handle()); m_renderWindow->setPresentation(presentation); - if (!wlr_drm_create(m_server->handle(), m_renderer)) { - qCCritical(lcTlCore) - << "Failed to create legacy wl_drm global"; + if (rendererSupportsImplicitDmabufFormats(m_renderer)) { + if (!wlr_drm_create(m_server->handle(), m_renderer)) { + qCWarning(lcTlCore) + << "Failed to create legacy wl_drm global despite implicit DMA-BUF support"; + } + } else { + qCInfo(lcTlCore) + << "Skipping legacy wl_drm global: Vulkan renderer does not support implicit DMA-BUF modifiers"; } } else { if (!wlr_renderer_init_wl_display(m_renderer, m_server->handle())) From 30b78d56d7d6be3ab06ca344c13e27f36403da6f Mon Sep 17 00:00:00 2001 From: LFRon Date: Sun, 6 Sep 2026 16:20:37 +0800 Subject: [PATCH 10/15] feat(vulkan): implement RenderBufferBlitter support Folded follow-ups: track live WRenderBufferBlitter item count (WRenderBufferBlitter::liveCount()) and skip the per-beginRender scene walk entirely when no blitter exists, and gate the Vulkan sampling prepare/finish/ barrier-batch-flush wrappers against QRhi passes (VUID-vkCmdPipelineBarrier- oldLayout-01181 / -srcQueueFamilyIndex-01182) like the render-buffer paths. Follow-up hardening (folded): VulkanDepthStencilAllocation::release() stays away from QRhi and native vk when the backend still is recording a frame (e.g. a shutdown forced by an earlier fatal render error), because finish() on such a leaked frame would end the abandoned command buffer and can abort the driver. The native image and memory are then left for the wlroots device teardown, which reclaims them together with the device. Follow-up hardening (folded): every BufferData that owns QRhi render-target objects now registers a keyed QRhi::addCleanupCallback (registerVulkanRhiTeardownCleanup). At shutdown the window tears the QQuickRenderControl down while WRenderHelper still caches its render targets; QRhi objects destroyed after the backend's VmaAllocator are unreclaimable and vmaDestroyAllocator() aborts the process with "Some allocations were not freed before destruction of this memory block!" (observed in Waydroid / dde-lock shutdown cores). The callback runs from ~QRhi's runCleanup() - the one point while the VkDevice is still alive - and calls resetWindowRenderTarget() first. Follow-up hardening (folded): WRenderHelper::addVulkanTeardownSweep() arms one sweep callback per Vulkan QRhi (registered right after createRhi in initRCWithRhi, and kept idempotently per BufferData): at ~QRhi it force-destroys every still-registered Buffer/Texture/RenderBuffer QRhiResource before the allocator dies. Such resources are produced by client scene-graph items / QQuickWindow caches that outlive the render control and are outside of waylib's ownership; Qt itself only reports them in ~QRhiImplementation, which runs after the VMA assert would already have fired. The per-resource qCWarning (type + Qt object name) makes any leaked owner identifiable in normal logs. Qt's destroy() paths are null-checked/idempotent, so owners deleting the C++ objects later stay safe. Depth-less backdrop targets (folded): the backdrop clear/preserve/resume targets are created without a depth attachment. WRenderBufferNode never advertises DepthAwareRendering, so Qt keeps the 2D depth buffer disabled for the whole batch renderer that contains a blitter; the attachment was only adding a full-attachment depth load/store (and a depth-stencil pass barrier) to every pass split. createRhiRenderTarget() gained a withDepthStencil opt-out that only the backdrop set uses (all other callers, including GLES2, keep their depth attachment exactly as before), the resume target no longer needs PreserveDepthStencilContents, the native depth-stencil texture/allocation machinery is deleted, and WRenderBufferNode::prepare() fails closed if a depth-using renderer ever reaches the blitter. Fast paths (folded): renderVulkan() returns early when the node has no content (nothing consumes the backdrop snapshot, so the canvas copy and the pass split are skipped), the blitter detection respects item visibility (an invisible blitter cannot request the backdrop), and both scene-graph walks run allocation-free (QVarLengthArray + childItems instead of per-beginRender QList/QSet copies). Teardown fix (folded): ~Node() hands the content subtree back from the clip node before deleting it, so the clip node is not leaked when the node is destroyed while clipping is active. --- waylib/src/server/CMakeLists.txt | 2 +- waylib/src/server/kernel/wlr_all.h | 2 +- .../qtquick/private/wbufferrenderer.cpp | 206 +++- .../qtquick/private/wbufferrenderer_p.h | 7 + .../qtquick/private/wrenderbuffernode.cpp | 941 ++++++++++++++++-- .../server/qtquick/woutputrenderwindow.cpp | 277 +++++- .../src/server/qtquick/woutputrenderwindow.h | 28 +- .../server/qtquick/wrenderbufferblitter.cpp | 5 +- .../src/server/qtquick/wrenderbufferblitter.h | 7 + waylib/src/server/qtquick/wrenderhelper.cpp | 703 ++++++++++++- waylib/src/server/qtquick/wrenderhelper.h | 30 +- .../src/server/qtquick/wsgtextureprovider.cpp | 27 + 12 files changed, 2129 insertions(+), 106 deletions(-) diff --git a/waylib/src/server/CMakeLists.txt b/waylib/src/server/CMakeLists.txt index dbd13b8b27..b63fc0d735 100644 --- a/waylib/src/server/CMakeLists.txt +++ b/waylib/src/server/CMakeLists.txt @@ -327,8 +327,8 @@ set(HEADERS protocols/woutputmanagerv1.h protocols/WOutputManagerV1 protocols/wlinuxdmabufv1.h - protocols/wpresentation.h protocols/WLinuxDmabufV1 + protocols/wpresentation.h protocols/WPresentation protocols/wlayershell.h protocols/WLayerShell diff --git a/waylib/src/server/kernel/wlr_all.h b/waylib/src/server/kernel/wlr_all.h index 352c37d592..8b0ee75818 100644 --- a/waylib/src/server/kernel/wlr_all.h +++ b/waylib/src/server/kernel/wlr_all.h @@ -109,9 +109,9 @@ extern "C" { #include #include #include -#include #include #include +#include #include #include #include diff --git a/waylib/src/server/qtquick/private/wbufferrenderer.cpp b/waylib/src/server/qtquick/private/wbufferrenderer.cpp index 347c37eb4b..a2efc18d6f 100644 --- a/waylib/src/server/qtquick/private/wbufferrenderer.cpp +++ b/waylib/src/server/qtquick/private/wbufferrenderer.cpp @@ -18,8 +18,7 @@ #include #include -#include -#include +#include #include #include @@ -281,7 +280,9 @@ wlr_buffer *WBufferRenderer::lastBuffer() const QRhiTexture *WBufferRenderer::currentRenderTarget() const { - auto renderTarget = state.sgRenderTarget.rt; + auto renderTarget = state.activeSgRenderTarget.rt + ? state.activeSgRenderTarget.rt + : state.sgRenderTarget.rt; if (!renderTarget) return nullptr; auto textureRT = static_cast(renderTarget); @@ -291,6 +292,10 @@ QRhiTexture *WBufferRenderer::currentRenderTarget() const return colorAttachment->texture(); } +QRhiRenderTarget *WBufferRenderer::currentVulkanBackdropResumeTarget() const +{ + return state.vulkanBackdropResumeSgRenderTarget.rt; +} bool WBufferRenderer::isColorPreserved() const { @@ -380,6 +385,46 @@ QTransform WBufferRenderer::inputMapToOutput(const QRectF &sourceRect, const QRe return t; } +static QSGRenderTarget toRhiSgRenderTarget(QQuickWindowPrivate *wd, const QQuickRenderTarget &rt); + +static bool hasActiveRenderBufferBlitter(QQuickItem *root) +{ + if (!root || !root->isVisible()) + return false; + + // No blitter exists anywhere: skip the recursive walk entirely. This runs + // for every WBufferRenderer::beginRender() on Vulkan. + if (WRenderBufferBlitter::liveCount() == 0) + return false; + + // An effectively invisible root cannot have an effectively visible + // descendant, so the visibility check above is exhaustive. childItems is + // used instead of paintOrderChildItems() because only existence matters + // here and paintOrderChildItems() returns a QList by value. + QVarLengthArray items; + items.append(root); + while (!items.isEmpty()) { + auto *item = items.last(); + items.removeLast(); + if (!item) + continue; + + if (item->isVisible() + && item->opacity() > 0.0 + && item->width() > 0.0 + && item->height() > 0.0 + && qobject_cast(item)) { + return true; + } + + const auto &children = QQuickItemPrivate::get(item)->childItems; + for (auto child = children.crbegin(); child != children.crend(); ++child) + items.append(*child); + } + + return false; +} + wlr_buffer *WBufferRenderer::beginRender(const QSize &pixelSize, qreal devicePixelRatio, uint32_t format, RenderFlags flags, WGlobal::ColorContentsMode mode) @@ -448,12 +493,28 @@ wlr_buffer *WBufferRenderer::beginRender(const QSize &pixelSize, qreal devicePix m_renderHelper->setSize(pixelSize); Q_ASSERT(wd->renderControl); + bool requestVulkanBackdrop = false; + if (isVulkanRhi) { + for (const auto &source : std::as_const(m_sourceList)) { + auto root = isRootItem(source.source) ? wd->contentItem : source.source; + if (hasActiveRenderBufferBlitter(root)) { + requestVulkanBackdrop = true; + break; + } + } + } + auto rt = m_renderHelper->acquireRenderTarget( - wd->renderControl, buffer, mode); + wd->renderControl, buffer, mode, requestVulkanBackdrop); if (rt.isNull()) { wlr_buffer_unlock(buffer); return nullptr; } + auto vulkanBackdropResumeRenderTarget = requestVulkanBackdrop + ? m_renderHelper->vulkanBackdropResumeRenderTarget(buffer) + : QQuickRenderTarget {}; + const bool vulkanBackdropActive = requestVulkanBackdrop + && !vulkanBackdropResumeRenderTarget.isNull(); // For software renderer, update the dirty parts relative to the last paint device. WPixmanRegion damage; @@ -474,10 +535,7 @@ wlr_buffer *WBufferRenderer::beginRender(const QSize &pixelSize, qreal devicePix } else { state.dirty = QRegion(); - sgRT.rt = rtd->u.rhiRt; - sgRT.cb = wd->redirect.commandBuffer; - Q_ASSERT(sgRT.cb); - sgRT.rpDesc = rtd->u.rhiRt->renderPassDescriptor(); + sgRT = toRhiSgRenderTarget(wd, rt.rt()); #ifndef QT_NO_OPENGL if (wd->rhi->backend() == QRhi::OpenGLES2) { @@ -497,10 +555,40 @@ wlr_buffer *WBufferRenderer::beginRender(const QSize &pixelSize, qreal devicePix state.buffer.reset(buffer); state.renderTarget = rt; state.sgRenderTarget = sgRT; + state.activeSgRenderTarget = sgRT; if (isVulkanRhi) { + state.vulkanBackdropActive = vulkanBackdropActive; + state.preserveRenderTarget = m_renderHelper->preserveRenderTarget( + buffer, vulkanBackdropActive); + if (!state.preserveRenderTarget.isNull()) { + auto preserveRtd = QQuickRenderTargetPrivate::get(&state.preserveRenderTarget); + if (preserveRtd->type == QQuickRenderTargetPrivate::Type::RhiRenderTarget) + state.preserveSgRenderTarget = toRhiSgRenderTarget(wd, state.preserveRenderTarget); + } + state.vulkanBackdropResumeRenderTarget = vulkanBackdropResumeRenderTarget; + if (!state.vulkanBackdropResumeRenderTarget.isNull()) { + auto resumeRtd = QQuickRenderTargetPrivate::get( + &state.vulkanBackdropResumeRenderTarget); + if (resumeRtd->type == QQuickRenderTargetPrivate::Type::RhiRenderTarget) { + state.vulkanBackdropResumeSgRenderTarget = toRhiSgRenderTarget( + wd, state.vulkanBackdropResumeRenderTarget); + } + } state.renderBufferReleasedForCache = false; } + if (isVulkanRhi && WVulkanTrace::enabled()) { + qCDebug(lcWlBufferRenderer).noquote() + << QStringLiteral("VKTRACE event=blitter-target-select renderer=%1 buffer=%2 requested=%3 active=%4 main=%5 preserveColor=%6 resume=%7") + .arg(quintptr(this), 0, 16) + .arg(quintptr(buffer), 0, 16) + .arg(requestVulkanBackdrop) + .arg(vulkanBackdropActive) + .arg(quintptr(state.sgRenderTarget.rt), 0, 16) + .arg(quintptr(state.preserveSgRenderTarget.rt), 0, 16) + .arg(quintptr(state.vulkanBackdropResumeSgRenderTarget.rt), 0, 16); + } + return buffer; } @@ -509,6 +597,54 @@ inline static QRect scaleToRect(const QRectF &s, qreal scale) { (s.size() * scale).toSize()); } +static QSGRenderTarget toRhiSgRenderTarget(QQuickWindowPrivate *wd, const QQuickRenderTarget &rt) +{ + auto rtd = QQuickRenderTargetPrivate::get(&rt); + Q_ASSERT(rtd->type == QQuickRenderTargetPrivate::Type::RhiRenderTarget); + + QSGRenderTarget sgRT; + sgRT.rt = rtd->u.rhiRt; + sgRT.cb = wd->redirect.commandBuffer; + Q_ASSERT(sgRT.cb); + sgRT.rpDesc = rtd->u.rhiRt->renderPassDescriptor(); + return sgRT; +} + +static QVector activeTextureProvidersForPass(QQuickItem *root) +{ + QVector providers; + if (!root) + return providers; + + // Single traversal that collects the providers directly: the previous + // implementation materialised the whole subtree as a QList via + // paintOrderItemList() and then deduplicated with a QSet. Only the set of + // providers matters, so unsorted childItems and a linear dedup are enough + // (the list is tiny) and avoid the per-frame allocations. + QVarLengthArray items; + items.append(root); + while (!items.isEmpty()) { + auto *item = items.last(); + items.removeLast(); + if (!item) + continue; + + if (item->isVisible() + && item->flags().testFlag(QQuickItem::ItemHasContents) + && item->isTextureProvider()) { + auto *provider = qobject_cast(item->textureProvider()); + if (provider && !providers.contains(provider)) + providers.append(provider); + } + + const auto &children = QQuickItemPrivate::get(item)->childItems; + for (auto child = children.crbegin(); child != children.crend(); ++child) + items.append(*child); + } + + return providers; +} + bool WBufferRenderer::render(int sourceIndex, const QMatrix4x4 &renderMatrix, const QRectF &sourceRect, const QRectF &targetRect, std::optional preserveColorContentsOverride) @@ -526,7 +662,18 @@ bool WBufferRenderer::render(int sourceIndex, const QMatrix4x4 &renderMatrix, const bool isVulkanRhi = wd->rhi && wd->rhi->backend() == QRhi::Vulkan; bool preserveColorContents = preserveColorContentsOverride.value_or( state.renderTarget.colorPreserved()); + auto activeRenderTarget = state.renderTarget.rt(); auto activeSgRenderTarget = state.sgRenderTarget; + if (isVulkanRhi && preserveColorContents && state.preserveSgRenderTarget.rt) { + activeRenderTarget = state.preserveRenderTarget; + activeSgRenderTarget = state.preserveSgRenderTarget; + qCDebug(lcWlBufferRenderer) << "Using Vulkan preserve render target for render pass" + << "renderer" << this + << "sourceIndex" << sourceIndex + << "buffer" << state.buffer.get() + << "renderTarget" << activeSgRenderTarget.rt; + } + state.activeSgRenderTarget = activeSgRenderTarget; // The renderer should always receive the window's DPR (Device Pixel Ratio) // because, regardless of the DPR used for rendering, all resources within // a window are loaded based on the window's own DPR. @@ -602,7 +749,7 @@ bool WBufferRenderer::render(int sourceIndex, const QMatrix4x4 &renderMatrix, state.worldTransform.optimize(); bool flipY = wd->rhi ? !wd->rhi->isYUpInNDC() : false; - if (state.renderTarget.rt().mirrorVertically()) + if (activeRenderTarget.mirrorVertically()) flipY = !flipY; if (viewportRect.isValid()) { @@ -654,12 +801,31 @@ bool WBufferRenderer::render(int sourceIndex, const QMatrix4x4 &renderMatrix, } } + QVector preparedTextures; WOutputRenderWindow *outputWindow = nullptr; constexpr const char *samplingPurpose = "qt-render-pass-texture"; if (isVulkanRhi) { outputWindow = renderWindow(); + const auto activeTextureProviders = activeTextureProvidersForPass( + isRootItem(source.source) ? wd->contentItem : source.source); WVulkanTrace::beginPass(outputWindow, this, state.buffer.get(), samplingPurpose, - sourceIndex, 0); + sourceIndex, activeTextureProviders.size()); + if (outputWindow + && !outputWindow->prepareTextureSamplingForRenderPass( + state.buffer.get(), + activeTextureProviders, + samplingPurpose, + sourceIndex, + &preparedTextures)) { + qCWarning(lcWlBufferRenderer) + << "Skipping render pass because Vulkan texture sampling prepare failed" + << "renderer" << this + << "sourceIndex" << sourceIndex + << "currentBuffer" << state.buffer.get() + << "wlrBuffer" << state.buffer.get(); + WVulkanTrace::endPass(outputWindow, false); + return false; + } } state.context->renderNextFrame(renderer); @@ -717,6 +883,20 @@ bool WBufferRenderer::render(int sourceIndex, const QMatrix4x4 &renderMatrix, dr->currentFrameCommandBuffer()->resourceUpdate(resourceUpdates); } + if (isVulkanRhi && outputWindow + && !outputWindow->finishTextureSamplingForRenderPass(preparedTextures, + samplingPurpose, + sourceIndex)) { + qCWarning(lcWlBufferRenderer) << "Skipping render pass because Vulkan texture sampling finish failed" + << "renderer" << this + << "sourceIndex" << sourceIndex + << "currentBuffer" << state.buffer.get() + << "wlrBuffer" << state.buffer.get() + << "preparedTextureCount" << preparedTextures.size(); + WVulkanTrace::endPass(outputWindow, false); + return false; + } + if (isVulkanRhi) WVulkanTrace::endPass(outputWindow, true); @@ -761,6 +941,12 @@ void WBufferRenderer::endRender() buffer.swap(state.buffer); state.renderer = nullptr; state.batchRenderer = nullptr; + state.activeSgRenderTarget = {}; + state.preserveRenderTarget = {}; + state.preserveSgRenderTarget = {}; + state.vulkanBackdropResumeRenderTarget = {}; + state.vulkanBackdropResumeSgRenderTarget = {}; + state.vulkanBackdropActive = false; m_lastBuffer = buffer.get(); if (shouldCacheBuffer() && isVulkanRhi) { diff --git a/waylib/src/server/qtquick/private/wbufferrenderer_p.h b/waylib/src/server/qtquick/private/wbufferrenderer_p.h index 28e9ad391f..72efda59a8 100644 --- a/waylib/src/server/qtquick/private/wbufferrenderer_p.h +++ b/waylib/src/server/qtquick/private/wbufferrenderer_p.h @@ -75,6 +75,7 @@ class WAYLIB_SERVER_EXPORT WBufferRenderer : public QQuickItem wlr_buffer *currentBuffer() const; wlr_buffer *lastBuffer() const; QRhiTexture *currentRenderTarget() const; + QRhiRenderTarget *currentVulkanBackdropResumeTarget() const; bool isColorPreserved() const; const wlr_damage_ring *damageRing() const; wlr_damage_ring *damageRing(); @@ -148,7 +149,13 @@ class WAYLIB_SERVER_EXPORT WBufferRenderer : public QQuickItem WBufferUnlockPtr buffer; WRenderHelper::RenderTarget renderTarget; QSGRenderTarget sgRenderTarget; + QQuickRenderTarget preserveRenderTarget; + QSGRenderTarget preserveSgRenderTarget; + QQuickRenderTarget vulkanBackdropResumeRenderTarget; + QSGRenderTarget vulkanBackdropResumeSgRenderTarget; + QSGRenderTarget activeSgRenderTarget; QRegion dirty; + bool vulkanBackdropActive = false; bool renderBufferReleasedForCache = false; } state; diff --git a/waylib/src/server/qtquick/private/wrenderbuffernode.cpp b/waylib/src/server/qtquick/private/wrenderbuffernode.cpp index 34d858744f..2a0b062bbe 100644 --- a/waylib/src/server/qtquick/private/wrenderbuffernode.cpp +++ b/waylib/src/server/qtquick/private/wrenderbuffernode.cpp @@ -12,16 +12,17 @@ #include "wqmlhelper_p.h" #include "platformplugin/types.h" #include "private/wprivateaccessor_p.h" +#include "utils/private/wvulkantrace_p.h" #include #include #include +#include #include #include #include #include -#include #include #include #include @@ -31,8 +32,8 @@ #include -// QSGRenderer: access protected methods preprocess()/render() and private -// bit-field members m_changed_emitted/m_is_rendering. +// QSGRenderer: access protected render stages and private bit-field members +// m_changed_emitted/m_is_rendering. // // preprocess() and render() are protected virtual functions — the template // accessor [temp.explicit]/12 trick legally bypasses access control here too. @@ -134,6 +135,9 @@ class Q_DECL_HIDDEN DataManagerPointer DataManagerPointer &operator=(const DataManagerPointer &other) noexcept { + if (this == &other) + return *this; + deref(); pointer = other.pointer; ref(); @@ -142,6 +146,10 @@ class Q_DECL_HIDDEN DataManagerPointer DataManagerPointer &operator=(DataManagerPointer &&other) noexcept { + if (this == &other) + return *this; + + deref(); pointer = std::exchange(other.pointer, nullptr); return *this; } @@ -151,6 +159,9 @@ class Q_DECL_HIDDEN DataManagerPointer } inline DataManagerPointer &operator=(T* p) { + if (pointer == p) + return *this; + deref(); pointer = p; ref(); @@ -354,47 +365,107 @@ struct WlrAndRhiTexture { struct wlr_buffer *buffer = nullptr; wlr_texture *wlrTexture = nullptr; QRhiTexture *rhiTexture = nullptr; + uint32_t drmFormat = 0; + uint64_t drmModifier = 0; + QRhiTexture::Flags flags; + bool vulkanOwned = false; }; -class Q_DECL_HIDDEN RhiTextureManager : public DataManager +class Q_DECL_HIDDEN RhiTextureManager : public DataManager { Q_OBJECT friend class DataManager; RhiTextureManager(QQuickWindow *owner) - : DataManager(owner) { + : DataManager(owner) { Q_ASSERT(owner->findChildren(Qt::FindDirectChildrenOnly).size() == 1); } - static bool check(WlrAndRhiTexture *texture, QRhiTexture::Format, uint32_t drmFormat, uint64_t drmModifier, const QSize &size) { - auto *buffer = texture->buffer; - wlr_dmabuf_attributes attribs; - if (!wlr_buffer_get_dmabuf(buffer, &attribs)) { - qCWarning(lcWlRenderBuffer) << "Failed to get dmabuf attributes for texture" << texture << "with buffer" << buffer - << ", Can't check texture without dmabuf attributes"; + static bool check(WlrAndRhiTexture *texture, bool isVulkan, + QRhiTexture::Format, + uint32_t drmFormat, uint64_t drmModifier, + const QSize &size, QRhiTexture::Flags flags) { + if (texture->vulkanOwned != isVulkan) return false; + + if (!isVulkan) { + auto buffer = texture->buffer; + wlr_dmabuf_attributes attribs; + if (!wlr_buffer_get_dmabuf(buffer, &attribs)) { + qCWarning(lcWlRenderBuffer) + << "Failed to get dmabuf attributes for texture" << texture + << "with buffer" << buffer + << ", Can't check texture without dmabuf attributes"; + return false; + } + return attribs.format == drmFormat + && attribs.modifier == drmModifier + && texture->rhiTexture->pixelSize() == size; } - return attribs.format == drmFormat && attribs.modifier == drmModifier && texture->rhiTexture->pixelSize() == size; + + return texture->drmFormat == drmFormat + && texture->drmModifier == drmModifier + && texture->rhiTexture + && texture->rhiTexture->pixelSize() == size + && texture->flags == flags; } - WlrAndRhiTexture *create(QRhiTexture::Format format, uint32_t drmFormat, uint64_t drmModifier, const QSize &size) { - auto ow = qobject_cast(owner()); - auto texture = WRenderHelper::newTexture(ow->allocator(), ow->renderer(), - drmFormat, drmModifier, owner()->rhi(), - size, format, QRhiTexture::RenderTarget); - if (!texture.rhiTexture) + WlrAndRhiTexture *create(bool isVulkan, QRhiTexture::Format format, + uint32_t drmFormat, uint64_t drmModifier, + const QSize &size, QRhiTexture::Flags flags) { + if (!isVulkan) { + auto ow = qobject_cast(owner()); + auto texture = WRenderHelper::newTexture(ow->allocator(), ow->renderer(), + drmFormat, drmModifier, owner()->rhi(), + size, format, QRhiTexture::RenderTarget); + if (!texture.rhiTexture) + return nullptr; + + return new WlrAndRhiTexture { + texture.buffer, + texture.texture, + texture.rhiTexture, + 0, + 0, + {}, + false, + }; + } + + auto texture = owner()->rhi()->newTexture(format, size, 1, + QRhiTexture::RenderTarget | flags); + texture->setName(QByteArrayLiteral("WaylibRenderBufferNodeTexture")); + if (!texture->create()) { + qCWarning(lcWlRenderBuffer) << "Failed to create QRhi texture for WRenderBufferNode" + << "format" << format + << "drmFormat" << drmFormat + << "modifier" << drmModifier + << "size" << size; + delete texture; return nullptr; + } - return new WlrAndRhiTexture{texture.buffer, texture.texture, texture.rhiTexture}; + return new WlrAndRhiTexture { + nullptr, + nullptr, + texture, + drmFormat, + drmModifier, + flags, + true, + }; } static void destroy(WlrAndRhiTexture *texture) { delete texture->rhiTexture; - if (texture->wlrTexture) - wlr_texture_destroy(texture->wlrTexture); - if (texture->buffer) - wlr_buffer_drop(texture->buffer); + if (!texture->vulkanOwned) { + if (texture->wlrTexture) + wlr_texture_destroy(texture->wlrTexture); + if (texture->buffer) + wlr_buffer_drop(texture->buffer); + } delete texture; } }; @@ -519,18 +590,25 @@ class Q_DECL_HIDDEN RhiManager : public DataManager RhiManager(QQuickWindow *owner) : DataManager(owner) { Q_ASSERT(owner->findChildren(Qt::FindDirectChildrenOnly).size() == 1); - std::unique_ptr fallbackSurface(new QW::OffscreenSurface(nullptr)); - fallbackSurface->create(); + if (owner->rhi()->backend() == QRhi::Vulkan) { + m_rhi.reset(new Rhi()); + m_rhi->rhi = owner->rhi(); + m_rhi->own = false; + m_rhi->gc = owner->graphicsConfiguration(); + } else { + std::unique_ptr fallbackSurface(new QW::OffscreenSurface(nullptr)); + fallbackSurface->create(); - auto rhi = QSGRhiSupport::instance()->createRhi(owner, fallbackSurface.get()); - if (!rhi.rhi) - return; - Q_ASSERT(rhi.rhi->backend() == owner->rhi()->backend()); - m_rhi.reset(new Rhi()); - m_rhi->rhi = rhi.rhi; - m_rhi->own = rhi.own; - m_rhi->gc = owner->graphicsConfiguration(); - m_rhi->offscreenSurface = fallbackSurface.release(); + auto rhi = QSGRhiSupport::instance()->createRhi(owner, fallbackSurface.get()); + if (!rhi.rhi) + return; + Q_ASSERT(rhi.rhi->backend() == owner->rhi()->backend()); + m_rhi.reset(new Rhi()); + m_rhi->rhi = rhi.rhi; + m_rhi->own = rhi.own; + m_rhi->gc = owner->graphicsConfiguration(); + m_rhi->offscreenSurface = fallbackSurface.release(); + } context = QQuickWindowPrivate::get(owner)->context; // Don't use RenderMode2D, when use this renderer on an exists renderTarget @@ -561,9 +639,9 @@ class Q_DECL_HIDDEN RhiManager : public DataManager } struct Rhi { - QRhi *rhi; - QOffscreenSurface *offscreenSurface; - bool own; + QRhi *rhi = nullptr; + QOffscreenSurface *offscreenSurface = nullptr; + bool own = false; QQuickGraphicsConfiguration gc; ~Rhi() { @@ -587,6 +665,206 @@ class Q_DECL_HIDDEN RhiManager : public DataManager QScopedPointer m_rhi; }; +// A QSG batch renderer owns its Elements and their shader-resource bindings. +// Reusing one renderer for multiple blitter roots in the same Vulkan command +// buffer lets Qt recycle and update a descriptor set that an earlier root has +// already bound. Keep one renderer per blitter role instead. Qt's render +// context still shares shader and pipeline caches between these renderers. +class Q_DECL_HIDDEN VulkanInlineRenderer +{ +public: + VulkanInlineRenderer(QQuickWindow *owner, const void *blitterNode, + const char *role) + : m_owner(owner) + , m_blitterNode(blitterNode) + , m_role(role) + { + if (!owner || !owner->rhi() + || owner->rhi()->backend() != QRhi::Vulkan) { + return; + } + + m_rhi = owner->rhi(); + m_graphicsConfiguration = owner->graphicsConfiguration(); + m_context = QQuickWindowPrivate::get(owner)->context; + if (!m_context) + return; + + // RenderMode2DNoDepthBuffer matches the existing RhiManager path: the + // surrounding compositor renderer owns the shared depth attachment. + m_renderer = m_context->createRenderer( + QSGRendererInterface::RenderMode2DNoDepthBuffer); + + if (m_renderer && WVulkanTrace::enabled()) { + qCDebug(lcWlRenderBuffer).noquote() + << QStringLiteral("VKTRACE event=blitter-inline-renderer-create node=%1 role=%2 renderer=%3") + .arg(quintptr(m_blitterNode), 0, 16) + .arg(QString::fromLatin1(m_role)) + .arg(quintptr(m_renderer), 0, 16); + } + } + + ~VulkanInlineRenderer() + { + if (!m_renderer) + return; + + Q_ASSERT(!m_prepared); + m_renderer->setRootNode(nullptr); + if (WVulkanTrace::enabled()) { + qCDebug(lcWlRenderBuffer).noquote() + << QStringLiteral("VKTRACE event=blitter-inline-renderer-destroy node=%1 role=%2 renderer=%3") + .arg(quintptr(m_blitterNode), 0, 16) + .arg(QString::fromLatin1(m_role)) + .arg(quintptr(m_renderer), 0, 16); + } + delete m_renderer; + } + + bool isValid() const + { + return m_renderer && m_rhi && m_context; + } + + QQuickWindow *owner() const + { + return m_owner; + } + + QSGRenderer *renderer() const + { + return m_renderer; + } + + QSGRootNode *rootNode() const + { + return m_renderer ? m_renderer->rootNode() : nullptr; + } + + bool sync(const QSize &pixelSize, QSGRootNode *rootNode, + const QMatrix4x4 &matrix = {}, + const QMatrix4x4 &baseProjectionMatrix = {}, + QSGRenderer *base = nullptr, const QVector2D &dpr = {}) + { + if (!isValid() || !rootNode || m_prepared) + return false; + + if (base) { + m_renderer->setDevicePixelRatio(base->devicePixelRatio()); + m_renderer->setDeviceRect(base->deviceRect()); + m_renderer->setViewportRect(base->viewportRect()); + m_renderer->setProjectionMatrix(baseProjectionMatrix); +#if QT_VERSION >= QT_VERSION_CHECK(6, 8, 0) + m_renderer->setProjectionMatrixWithNativeNDC( + base->projectionMatrixWithNativeNDC(0)); +#else + m_renderer->setProjectionMatrixWithNativeNDC( + base->projectionMatrixWithNativeNDC()); +#endif + } else { + if (dpr.x() <= 0.0f || dpr.y() <= 0.0f) + return false; + + m_renderer->setDevicePixelRatio(1.0); + m_renderer->setDeviceRect(QRect(QPoint(0, 0), pixelSize)); + m_renderer->setViewportRect(pixelSize); + const QRectF rect( + QPointF(0, 0), + QSizeF(pixelSize.width() / dpr.x(), + pixelSize.height() / dpr.y())); + // The backdrop is exposed as an ordinary, unmirrored QSGTexture. + // Match QQuickWindow's projection for a redirected render target + // so Vulkan's Y-down NDC does not turn the rotated copy upside down. + m_renderer->setProjectionMatrixToRect( + rect, + !m_rhi->isYUpInNDC() + ? QSGRenderer::MatrixTransformFlipY + : QSGRenderer::MatrixTransformFlag {}); + } + + if (Q_UNLIKELY(!matrix.isIdentity())) { +#if QT_VERSION >= QT_VERSION_CHECK(6, 8, 0) + m_renderer->setProjectionMatrix( + m_renderer->projectionMatrix(0) * matrix); + m_renderer->setProjectionMatrixWithNativeNDC( + m_renderer->projectionMatrixWithNativeNDC(0) * matrix); +#else + m_renderer->setProjectionMatrix( + m_renderer->projectionMatrix() * matrix); + m_renderer->setProjectionMatrixWithNativeNDC( + m_renderer->projectionMatrixWithNativeNDC() * matrix); +#endif + } + + auto *oldRoot = m_renderer->rootNode(); + m_renderer->setRootNode(rootNode); + if (oldRoot != rootNode && WVulkanTrace::enabled()) { + qCDebug(lcWlRenderBuffer).noquote() + << QStringLiteral("VKTRACE event=blitter-inline-renderer-root node=%1 role=%2 renderer=%3 old=%4 new=%5") + .arg(quintptr(m_blitterNode), 0, 16) + .arg(QString::fromLatin1(m_role)) + .arg(quintptr(m_renderer), 0, 16) + .arg(quintptr(oldRoot), 0, 16) + .arg(quintptr(rootNode), 0, 16); + } + return true; + } + + bool prepareInline(QRhiRenderTarget *renderTarget, + QRhiCommandBuffer *commandBuffer) + { + if (!isValid() || !rootNode() || !renderTarget || !commandBuffer + || m_prepared) { + return false; + } + + m_renderer->setRenderTarget( + {renderTarget, renderTarget->renderPassDescriptor(), commandBuffer}); + auto *defaultContext = static_cast(m_context); + m_oldDpr = defaultContext->currentDevicePixelRatio(); + m_oldCommandBuffer = defaultContext->currentFrameCommandBuffer(); + m_context->prepareSync(m_renderer->devicePixelRatio(), commandBuffer, + m_graphicsConfiguration); + + // These public Qt stages deliberately split resource preparation from + // draw recording so the caller can begin the compatible render pass in + // between. They also maintain QSGRenderer's private state themselves. + m_renderer->prepareSceneInline(); + m_prepared = true; + return true; + } + + void renderInline() + { + Q_ASSERT(m_renderer && m_prepared); + m_renderer->renderSceneInline(); + m_context->prepareSync(m_oldDpr, m_oldCommandBuffer, + m_graphicsConfiguration); + m_oldCommandBuffer = nullptr; + m_prepared = false; + } + + void detachRoot() + { + if (!m_renderer) + return; + Q_ASSERT(!m_prepared); + m_renderer->setRootNode(nullptr); + } + +private: + QQuickWindow *m_owner = nullptr; + const void *m_blitterNode = nullptr; + const char *m_role = nullptr; + QRhi *m_rhi = nullptr; + QSGRenderContext *m_context = nullptr; + QSGRenderer *m_renderer = nullptr; + QQuickGraphicsConfiguration m_graphicsConfiguration; + qreal m_oldDpr = 1.0; + QRhiCommandBuffer *m_oldCommandBuffer = nullptr; + bool m_prepared = false; +}; + static QSizeF mapSize(const QRectF &source, const QMatrix4x4 &matrix) { auto topLeft = matrix.map(source.topLeft()); @@ -653,28 +931,42 @@ class Q_DECL_HIDDEN RhiNode : public WRenderBufferNode { } StateFlags changedStates() const override { - if (Q_UNLIKELY(renderData && !contentNode)) - return (RenderTargetState | ViewportState); - - return {0}; - } - - // ###: Should disable DepthAwareRendering here, because - // if enable depth test, Qt will render the opaque nodes first, - // maybe an opaque node's z-order is higher than the render node, - // will get the wrong texture when copy texture in RhiNode::render. - // But the opaque node will be cover the wrong area to the - // RhiNode::renderTarget after RhiNode::render, but if you using - // MultiEffect's blur effect for the copied texture, the blur result - // will contains the opaque node's contains. Fortunately, this kind - // of problem is not very obvious. - // - // We should fix this bug in Qt in the future. + const bool isVulkan = m_item && m_item->window() + && m_item->window()->graphicsApi() == QSGRendererInterface::Vulkan; + if (!isVulkan) { + if (Q_UNLIKELY(renderData && !contentNode)) + return RenderTargetState | ViewportState; + return {}; + } + + return DepthState + | StencilState + | ScissorState + | ColorState + | BlendState + | CullState + | ViewportState + | RenderTargetState; + } + RenderingFlags flags() const override { - if (Q_UNLIKELY(!contentNode)) - return BoundedRectRendering | DepthAwareRendering; + const bool isVulkan = m_item && m_item->window() + && m_item->window()->graphicsApi() + == QSGRendererInterface::Vulkan; + if (!isVulkan) { + if (Q_UNLIKELY(!contentNode)) + return BoundedRectRendering | DepthAwareRendering; + return DepthAwareRendering; + } - return DepthAwareRendering; + // Vulkan backdrop rendering must remain in painter order. Advertising + // depth awareness lets Qt move opaque nodes above this node ahead of + // it, so they get copied into the backdrop and are then overpainted by + // the separately rendered effect content. + RenderingFlags flags = NoExternalRendering; + if (!contentNode) + flags |= BoundedRectRendering; + return flags; } void releaseResources() override { @@ -703,6 +995,10 @@ class Q_DECL_HIDDEN RhiNode : public WRenderBufferNode { void prepare() override { contentNode = nullptr; + vulkanReady = false; + vulkanResumeTarget = nullptr; + vulkanCopySourceRect = {}; + vulkanCopyDestination = {}; if (Q_UNLIKELY(!m_item || !m_item->window())) { reset(); @@ -729,15 +1025,81 @@ class Q_DECL_HIDDEN RhiNode : public WRenderBufferNode { } const auto currentRenderer = maybeBufferRenderer(); + const bool isVulkan = window->rhi() + && window->rhi()->backend() == QRhi::Vulkan; + if (isVulkan) { + if (!currentRenderer) { + setVulkanUnavailable("not rendering through WBufferRenderer", ct); + return; + } + if (!ct->flags().testFlag(QRhiTexture::UsedAsTransferSource)) { + setVulkanUnavailable("output modifier does not support transfer-source usage", ct); + return; + } + if (ct->sampleCount() != 1) { + setVulkanUnavailable("multisampled output copy is unsupported", ct); + return; + } + + // The backdrop targets are created without a depth attachment (see + // WRenderHelperPrivate::ensureVulkanBackdropRenderTargets): resuming + // the outer pass without depth is only valid while the surrounding + // renderer has the 2D depth buffer disabled, which this node's own + // rendering flags force (no DepthAwareRendering). Fail closed if + // that ever stops holding. + if (currentRenderer->currentBatchRenderer() + && rendererUseDepthBuffer(currentRenderer->currentBatchRenderer())) { + setVulkanUnavailable("the surrounding renderer uses the depth buffer", ct); + return; + } + + auto resumeTarget = currentRenderer->currentVulkanBackdropResumeTarget(); + if (!resumeTarget + || resumeTarget->resourceType() != QRhiResource::TextureRenderTarget + || renderTarget()->resourceType() != QRhiResource::TextureRenderTarget) { + setVulkanUnavailable("Vulkan backdrop resume target is unavailable", ct); + return; + } + + auto currentTextureTarget = static_cast(renderTarget()); + auto resumeTextureTarget = static_cast(resumeTarget); + const auto currentDesc = currentTextureTarget->description(); + const auto resumeDesc = resumeTextureTarget->description(); + if (currentDesc.colorAttachmentCount() < 1 + || resumeDesc.colorAttachmentCount() < 1 + || currentDesc.colorAttachmentAt(0)->texture() != ct + || resumeDesc.colorAttachmentAt(0)->texture() != ct + // The backdrop set must stay depth-free on both sides, and the + // resumed pass must preserve the color attachment. + || currentDesc.depthTexture() + || resumeDesc.depthTexture() + || currentDesc.depthStencilBuffer() + || resumeDesc.depthStencilBuffer() + || !resumeTextureTarget->flags().testFlag( + QRhiTextureRenderTarget::PreserveColorContents)) { + setVulkanUnavailable("Vulkan backdrop target attachments are incompatible", ct); + return; + } + vulkanResumeTarget = resumeTextureTarget; + } + // TODO: Apple viewport to matrix, needs get QSGRenderer renderMatrix = currentRenderer ? currentRenderer->currentWorldTransform() * (*this->matrix()) : *this->matrix(); devicePixelRatio = effectiveDevicePixelRatio(); - const auto oldManager = manager; - manager = RhiTextureManager::resolveByOwner(manager, window); - - if (oldManager != manager) { + if (!isVulkan) { + const auto oldManager = manager; + manager = RhiTextureManager::resolveByOwner(manager, window); + if (oldManager != manager) { + sgTexture()->setTexture(nullptr); + if (oldManager) + oldManager->release(texture); + texture.reset(); + } + } else if (Q_UNLIKELY(!manager || manager->owner() != window)) { + const auto oldManager = manager; + manager = RhiTextureManager::resolveByOwner(manager, window); sgTexture()->setTexture(nullptr); if (oldManager) oldManager->release(texture); @@ -745,10 +1107,14 @@ class Q_DECL_HIDDEN RhiNode : public WRenderBufferNode { } Q_ASSERT(ct->rhi() == window->rhi()); - rhi = rhi->resolveByOwner(rhi, window); + if (!isVulkan) + rhi = rhi->resolveByOwner(rhi, window); + else if (Q_UNLIKELY(!rhi || rhi->owner() != window)) + rhi = RhiManager::resolveByOwner(rhi, window); Q_ASSERT(rhi); - const bool hasRotation = renderMatrix.flags().testAnyFlags(QMatrix4x4::Rotation2D | QMatrix4x4::Rotation); + hasRotation = renderMatrix.flags().testAnyFlags( + QMatrix4x4::Rotation2D | QMatrix4x4::Rotation); QSize pixelSize; if (hasRotation) { @@ -771,6 +1137,7 @@ class Q_DECL_HIDDEN RhiNode : public WRenderBufferNode { renderData->rootNode.appendChildNode(renderData->imageNode); } } else { + vulkanRotationRenderer.reset(); renderData.reset(); QSizeF size = renderMatrix.mapRect(m_rect).size() * devicePixelRatio; @@ -782,10 +1149,43 @@ class Q_DECL_HIDDEN RhiNode : public WRenderBufferNode { pixelSize = size.toSize(); } + if (isVulkan && pixelSize.isEmpty()) { + reset(); + return; + } + + if (isVulkan) { + const QRect renderTargetRect(QPoint(0, 0), ct->pixelSize()); + if (hasRotation) { + // Keep the same source coordinate domain as the GLES2 path. + // A projective rotation and a fractional DPR do not commute, + // so a DPR-scaled mapped bounding box can omit pixels that the + // inverse transform still samples. + vulkanCopySourceRect = renderTargetRect; + vulkanCopyDestination = QPoint(0, 0); + } else { + const QPoint sourcePos = + (renderMatrix.map(m_rect.topLeft()) * devicePixelRatio).toPoint(); + vulkanCopySourceRect = QRect(sourcePos, pixelSize) + .intersected(renderTargetRect); + vulkanCopyDestination = vulkanCopySourceRect.topLeft() - sourcePos; + } + + if (vulkanCopySourceRect.isEmpty()) { + reset(); + return; + } + } + { auto format = attribs.format; auto modifier = attribs.modifier; - texture = manager->resolve(texture, ct->format(), std::move(format), std::move(modifier), pixelSize); + QRhiTexture::Flags textureFlags = isVulkan + ? ct->flags() & QRhiTexture::sRGB + : QRhiTexture::Flags {}; + texture = manager->resolve(texture, bool(isVulkan), ct->format(), std::move(format), + std::move(modifier), pixelSize, + std::move(textureFlags)); } if (Q_UNLIKELY(texture.expired())) { reset(); @@ -797,17 +1197,40 @@ class Q_DECL_HIDDEN RhiNode : public WRenderBufferNode { if (renderData) { if (!renderData->rt || sgTexture()->rhiTexture() != texture->data->rhiTexture) { QRhiTextureRenderTargetDescription rtDesc(texture->data->rhiTexture); - const auto flags = QRhiTextureRenderTarget::PreserveColorContents - | QRhiTextureRenderTarget::PreserveDepthStencilContents; + const auto flags = isVulkan + ? QRhiTextureRenderTarget::Flags {} + : QRhiTextureRenderTarget::PreserveColorContents + | QRhiTextureRenderTarget::PreserveDepthStencilContents; auto newRT = rhi->rhi()->newTextureRenderTarget(rtDesc, flags); newRT->setRenderPassDescriptor(newRT->newCompatibleRenderPassDescriptor()); if (!newRT->create()) { delete newRT; + if (isVulkan) + setVulkanUnavailable("failed to create rotated backdrop render target", ct); return; } renderData->rt.reset(newRT); } + + if (isVulkan + && (!renderData->scratchTexture + || renderData->scratchTexture->format() != ct->format() + || renderData->scratchTexture->pixelSize() != vulkanCopySourceRect.size() + || (renderData->scratchTexture->flags() & QRhiTexture::sRGB) + != (ct->flags() & QRhiTexture::sRGB))) { + const QRhiTexture::Flags scratchFlags = ct->flags() & QRhiTexture::sRGB; + std::unique_ptr scratchTexture( + rhi->rhi()->newTexture(ct->format(), vulkanCopySourceRect.size(), + 1, scratchFlags)); + scratchTexture->setName( + QByteArrayLiteral("WaylibRenderBufferNodeScratchTexture")); + if (!scratchTexture->create()) { + setVulkanUnavailable("failed to create rotated backdrop scratch texture", ct); + return; + } + renderData->scratchTexture = std::move(scratchTexture); + } } if (m_content) { @@ -816,6 +1239,79 @@ class Q_DECL_HIDDEN RhiNode : public WRenderBufferNode { contentNode = rootNode; } } + + if (isVulkan) { + const auto ensureInlineRenderer = [&] ( + std::unique_ptr &inlineRenderer, + const char *role) { + if (inlineRenderer && inlineRenderer->owner() != window) + inlineRenderer.reset(); + if (!inlineRenderer) { + inlineRenderer = std::make_unique( + window, this, role); + } + return inlineRenderer->isValid(); + }; + + if (hasRotation + && !ensureInlineRenderer(vulkanRotationRenderer, "rotation")) { + setVulkanUnavailable( + "failed to create the Vulkan rotation inline renderer", ct); + return; + } + + if (contentNode) { + if (!ensureInlineRenderer(vulkanContentRenderer, "content")) { + setVulkanUnavailable( + "failed to create the Vulkan content inline renderer", ct); + return; + } + } else { + vulkanContentRenderer.reset(); + } + } + + vulkanReady = isVulkan; + if (isVulkan && WVulkanTrace::enabled()) { + const bool outerDepthEnabled = currentRenderer + && currentRenderer->currentBatchRenderer() + && rendererUseDepthBuffer( + currentRenderer->currentBatchRenderer()); + qCDebug(lcWlRenderBuffer).noquote() + << QStringLiteral("VKTRACE event=blitter-prepare node=%1 item=%2 current=%3 resume=%4 color=%5 depth=%6 copyX=%7 copyY=%8 copyW=%9 copyH=%10 dstX=%11 dstY=%12 rotated=%13 content=%14 ordering=%15 depthAware=%16 outerDepth=%17") + .arg(quintptr(this), 0, 16) + .arg(quintptr(m_item.data()), 0, 16) + .arg(quintptr(renderTarget()), 0, 16) + .arg(quintptr(vulkanResumeTarget), 0, 16) + .arg(quintptr(ct), 0, 16) + .arg(quintptr(static_cast(renderTarget()) + ->description().depthTexture()), 0, 16) + .arg(vulkanCopySourceRect.x()) + .arg(vulkanCopySourceRect.y()) + .arg(vulkanCopySourceRect.width()) + .arg(vulkanCopySourceRect.height()) + .arg(vulkanCopyDestination.x()) + .arg(vulkanCopyDestination.y()) + .arg(hasRotation) + .arg(bool(contentNode)) + .arg(QStringLiteral("painter")) + .arg(false) + .arg(outerDepthEnabled); + if (hasRotation) { + qCDebug(lcWlRenderBuffer).noquote() + << QStringLiteral("VKTRACE event=blitter-rotation-geometry node=%1 copyX=%2 copyY=%3 copyW=%4 copyH=%5 targetW=%6 targetH=%7 backdropW=%8 backdropH=%9 dpr=%10") + .arg(quintptr(this), 0, 16) + .arg(vulkanCopySourceRect.x()) + .arg(vulkanCopySourceRect.y()) + .arg(vulkanCopySourceRect.width()) + .arg(vulkanCopySourceRect.height()) + .arg(ct->pixelSize().width()) + .arg(ct->pixelSize().height()) + .arg(pixelSize.width()) + .arg(pixelSize.height()) + .arg(devicePixelRatio, 0, 'f', 3); + } + } } void render([[maybe_unused]] const RenderState *state) override { @@ -827,6 +1323,12 @@ class Q_DECL_HIDDEN RhiNode : public WRenderBufferNode { auto ct = currentRenderTexture(); Q_ASSERT(ct); + if (renderWindow()->rhi() + && renderWindow()->rhi()->backend() == QRhi::Vulkan) { + renderVulkan(ct, rhiTexture); + return; + } + if (renderData) { renderData->texture.setTexture(ct); renderData->texture.setTextureSize(ct->pixelSize()); @@ -941,6 +1443,286 @@ class Q_DECL_HIDDEN RhiNode : public WRenderBufferNode { } private: + void setVulkanUnavailable(const char *reason, QRhiTexture *renderTexture) { + vulkanReady = false; + vulkanResumeTarget = nullptr; + const bool hadTexture = sgTexture()->rhiTexture(); + reset(false); + if (hadTexture) + doNotifyTextureChanged(); + if (vulkanUnavailableLogged) + return; + + qCWarning(lcWlRenderBuffer) << "Disabled RenderBufferBlitter for Vulkan render target" + << "reason" << reason + << "item" << m_item + << "itemSize" << m_size + << "renderTargetSize" + << (renderTexture ? renderTexture->pixelSize() : QSize()); + vulkanUnavailableLogged = true; + } + + void renderVulkan(QRhiTexture *currentTexture, QRhiTexture *backdropTexture) { + if (!vulkanReady || !vulkanResumeTarget || !rhi) + return; + + // Nothing consumes the backdrop snapshot when there is no content to + // draw, so the canvas copy and the pass split can be skipped entirely. + if (!contentNode) + return; + + const quint64 passSequence = ++vulkanPassSequence; + + auto cb = commandBuffer(); + if (!cb) { + setVulkanUnavailable("missing current QRhi command buffer", currentTexture); + return; + } + if (hasRotation + && (!renderData || !renderData->rt || !renderData->scratchTexture)) { + setVulkanUnavailable("rotated backdrop resources are incomplete", currentTexture); + return; + } + + auto rub = rhi->rhi()->nextResourceUpdateBatch(); + QRhiTextureCopyDescription copyDescription; + copyDescription.setPixelSize(vulkanCopySourceRect.size()); + copyDescription.setSourceTopLeft(vulkanCopySourceRect.topLeft()); + copyDescription.setDestinationTopLeft(vulkanCopyDestination); + rub->copyTexture(hasRotation ? renderData->scratchTexture.get() : backdropTexture, + currentTexture, + copyDescription); + + if (WVulkanTrace::enabled()) { + qCDebug(lcWlRenderBuffer).noquote() + << QStringLiteral("VKTRACE event=blitter-pass node=%1 sequence=%2 phase=end-main-and-copy commandBuffer=%3 current=%4 backdrop=%5 resume=%6") + .arg(quintptr(this), 0, 16) + .arg(passSequence) + .arg(quintptr(cb), 0, 16) + .arg(quintptr(currentTexture), 0, 16) + .arg(quintptr(backdropTexture), 0, 16) + .arg(quintptr(vulkanResumeTarget), 0, 16); + } + + // Vulkan cannot copy an image while it is an active color attachment. + // Split the outer pass, keep every operation on Qt's command buffer, + // then resume on a render-pass-compatible target that loads the shared + // color attachment (the backdrop set has no depth attachment). + cb->endPass(rub); + bool outerPassActive = false; + const auto passFlags = QRhiCommandBuffer::ExternalContent + | QRhiCommandBuffer::DoNotTrackResourcesForCompute; + const auto resumeOuterPass = [&] { + if (outerPassActive) + return; + cb->beginPass(vulkanResumeTarget, + Qt::transparent, + QRhiDepthStencilClearValue(1.0f, 0), + nullptr, + passFlags); + outerPassActive = true; + if (WVulkanTrace::enabled()) { + qCDebug(lcWlRenderBuffer).noquote() + << QStringLiteral("VKTRACE event=blitter-pass node=%1 sequence=%2 phase=resume-main commandBuffer=%3 resume=%4") + .arg(quintptr(this), 0, 16) + .arg(passSequence) + .arg(quintptr(cb), 0, 16) + .arg(quintptr(vulkanResumeTarget), 0, 16); + } + }; + const auto passGuard = qScopeGuard(resumeOuterPass); + + if (hasRotation) { + if (!vulkanRotationRenderer) { + setVulkanUnavailable( + "missing Vulkan rotation inline renderer", currentTexture); + return; + } + + auto scratchTexture = renderData->scratchTexture.get(); + renderData->texture.setTexture(scratchTexture); + renderData->texture.setTextureSize(scratchTexture->pixelSize()); + + const QPointF sourcePos = renderMatrix.map(m_rect.topLeft()); + const QRectF imageRect( + -(devicePixelRatio - 1) * sourcePos, + scratchTexture->pixelSize()); + const QRectF imageSourceRect( + QPointF(0, 0), scratchTexture->pixelSize()); + + // The full-size scratch texture deliberately reproduces the GLES2 + // image geometry. This keeps every texel reachable by the inverse + // projective transform, including at fractional DPR. + renderData->imageNode->setSourceRect(imageSourceRect); + renderData->imageNode->setRect(imageRect); + + if (WVulkanTrace::enabled()) { + qCDebug(lcWlRenderBuffer).noquote() + << QStringLiteral("VKTRACE event=blitter-rotation-draw node=%1 sequence=%2 rectX=%3 rectY=%4 rectW=%5 rectH=%6 sourceX=%7 sourceY=%8 sourceW=%9 sourceH=%10 scratchW=%11 scratchH=%12") + .arg(quintptr(this), 0, 16) + .arg(passSequence) + .arg(imageRect.x(), 0, 'f', 3) + .arg(imageRect.y(), 0, 'f', 3) + .arg(imageRect.width(), 0, 'f', 3) + .arg(imageRect.height(), 0, 'f', 3) + .arg(imageSourceRect.x(), 0, 'f', 3) + .arg(imageSourceRect.y(), 0, 'f', 3) + .arg(imageSourceRect.width(), 0, 'f', 3) + .arg(imageSourceRect.height(), 0, 'f', 3) + .arg(scratchTexture->pixelSize().width()) + .arg(scratchTexture->pixelSize().height()); + } + + if (!vulkanRotationRenderer->sync( + backdropTexture->pixelSize(), + &renderData->rootNode, + renderMatrix.inverted(), + {}, + nullptr, + {backdropTexture->pixelSize().width() + / float(m_rect.width() * devicePixelRatio), + backdropTexture->pixelSize().height() + / float(m_rect.height() * devicePixelRatio)})) { + setVulkanUnavailable( + "failed to synchronize the Vulkan rotation inline renderer", + currentTexture); + return; + } + if (!vulkanRotationRenderer->prepareInline(renderData->rt.get(), cb)) { + setVulkanUnavailable( + "failed to prepare the Vulkan rotation inline renderer", + currentTexture); + return; + } + cb->beginPass(renderData->rt.get(), + Qt::transparent, + QRhiDepthStencilClearValue(1.0f, 0), + nullptr, + passFlags); + vulkanRotationRenderer->renderInline(); + cb->endPass(); + if (WVulkanTrace::enabled()) { + qCDebug(lcWlRenderBuffer).noquote() + << QStringLiteral("VKTRACE event=blitter-pass node=%1 sequence=%2 phase=rotation-complete renderer=%3 root=%4") + .arg(quintptr(this), 0, 16) + .arg(passSequence) + .arg(quintptr(vulkanRotationRenderer->renderer()), 0, 16) + .arg(quintptr(vulkanRotationRenderer->rootNode()), 0, 16); + } + } + + if (sgTexture()->rhiTexture() != backdropTexture) + sgTexture()->setTexture(backdropTexture); + doNotifyTextureChanged(); + + if (!contentNode) + return; + + if (!vulkanContentRenderer) { + setVulkanUnavailable( + "missing Vulkan content inline renderer", currentTexture); + return; + } + + auto currentRenderer = maybeBufferRenderer(); + if (!currentRenderer) { + reset(); + return; + } + + QSGNode *childContainer = nullptr; + if (clipList() || inheritedOpacity() < 1.0) { + if (!node) + node.reset(new Node); + + node->opacityNode.setOpacity(inheritedOpacity()); + node->transformNode.setMatrix(*this->matrix()); + childContainer = &node->opacityNode; + if (clipList()) { + if (!node->clipNode) { + node->clipNode = new QQuickDefaultClipNode( + QRectF(0, 0, 65535, 65535)); + node->clipNode->setFlag(QSGNode::OwnedByParent, false); + node->clipNode->setClipRect(node->clipNode->rect()); + node->clipNode->update(); + + node->rootNode.reparentChildNodesTo(node->clipNode); + node->rootNode.appendChildNode(node->clipNode); + } + } else { + if (node->clipNode) + node->clipNode->reparentChildNodesTo(&node->rootNode); + + delete node->clipNode; + node->clipNode = nullptr; + } + + overrideChildNodesTo(contentNode, childContainer); + } + + const auto childGuard = qScopeGuard([&] { + if (childContainer) { + // The temporary wrapper borrows the content subtree. Detach + // the dedicated renderer while the borrowed nodes are still + // below its root, then restore their real parent. + if (vulkanContentRenderer) + vulkanContentRenderer->detachRoot(); + restoreChildNodesTo(childContainer, contentNode); + } + }); + + QSGRootNode *contentRenderRoot = nullptr; + QMatrix4x4 contentMatrix; + if (childContainer) { + contentRenderRoot = &node->rootNode; + } else { + node.reset(); + contentRenderRoot = contentNode; + contentMatrix = *this->matrix(); + } + + if (node && node->clipNode) { + if (!node->clipNode->clipList()) { + node->clipNode->setRendererClipList(clipList()); + } else { + auto lastClipNode = node->clipNode->clipList(); + while (auto cliplist = lastClipNode->clipList()) + lastClipNode = cliplist; + Q_ASSERT(!lastClipNode->clipList()); + const_cast(lastClipNode)->setRendererClipList(clipList()); + } + } + + if (!vulkanContentRenderer->sync( + currentTexture->pixelSize(), + contentRenderRoot, + contentMatrix, + *projectionMatrix(), + currentRenderer->currentRenderer())) { + setVulkanUnavailable( + "failed to synchronize the Vulkan content inline renderer", + currentTexture); + return; + } + if (!vulkanContentRenderer->prepareInline(vulkanResumeTarget, cb)) { + setVulkanUnavailable( + "failed to prepare the Vulkan content inline renderer", + currentTexture); + return; + } + + resumeOuterPass(); + vulkanContentRenderer->renderInline(); + if (WVulkanTrace::enabled()) { + qCDebug(lcWlRenderBuffer).noquote() + << QStringLiteral("VKTRACE event=blitter-pass node=%1 sequence=%2 phase=content-complete renderer=%3 root=%4") + .arg(quintptr(this), 0, 16) + .arg(passSequence) + .arg(quintptr(vulkanContentRenderer->renderer()), 0, 16) + .arg(quintptr(vulkanContentRenderer->rootNode()), 0, 16); + } + } + void reset(bool notifyTexture = true) { if (renderData) renderData->rt.reset(); @@ -955,6 +1737,10 @@ class Q_DECL_HIDDEN RhiNode : public WRenderBufferNode { void destroy() { reset(false); + // Inline renderers must detach from their roots before the temporary + // root containers below are destroyed. + vulkanContentRenderer.reset(); + vulkanRotationRenderer.reset(); renderData.reset(); node.reset(); manager = nullptr; @@ -966,6 +1752,15 @@ class Q_DECL_HIDDEN RhiNode : public WRenderBufferNode { DataManagerPointer rhi; QMatrix4x4 renderMatrix; qreal devicePixelRatio; + bool hasRotation = false; + bool vulkanReady = false; + bool vulkanUnavailableLogged = false; + quint64 vulkanPassSequence = 0; + QRhiTextureRenderTarget *vulkanResumeTarget = nullptr; + QRect vulkanCopySourceRect; + QPoint vulkanCopyDestination; + std::unique_ptr vulkanContentRenderer; + std::unique_ptr vulkanRotationRenderer; struct Node { Node() { @@ -976,6 +1771,19 @@ class Q_DECL_HIDDEN RhiNode : public WRenderBufferNode { rootNode.appendChildNode(&transformNode); } + ~Node() { + if (clipNode) { + // The clip node borrows the content subtree, so hand it back + // before deleting it (exactly like the no-clip transition + // below); QSGNode::destroy() then detaches it from rootNode. + // Without this the clip node is leaked whenever the node is + // destroyed while clipping was active. + clipNode->reparentChildNodesTo(&rootNode); + delete clipNode; + clipNode = nullptr; + } + } + QSGRootNode rootNode; QSGTransformNode transformNode; QSGOpacityNode opacityNode; @@ -997,6 +1805,7 @@ class Q_DECL_HIDDEN RhiNode : public WRenderBufferNode { }; std::unique_ptr rt; + std::unique_ptr scratchTexture; QSGRootNode rootNode; QSGImageNode *imageNode; QSGPlainTexture texture; diff --git a/waylib/src/server/qtquick/woutputrenderwindow.cpp b/waylib/src/server/qtquick/woutputrenderwindow.cpp index ac14e41d92..83149ea722 100644 --- a/waylib/src/server/qtquick/woutputrenderwindow.cpp +++ b/waylib/src/server/qtquick/woutputrenderwindow.cpp @@ -2,8 +2,6 @@ // SPDX-License-Identifier: Apache-2.0 OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only #include "woutputrenderwindow.h" -#include "wquickcursor.h" -#include "wpresentation.h" #include #include #include "woutput.h" @@ -14,8 +12,12 @@ #include "woutputviewport_p.h" #include "wqmlhelper_p.h" #include "woutputlayer.h" +#include "wpresentation.h" #include "wbufferrenderer_p.h" +#include "wquickcursor.h" #include "wquicktextureproxy.h" +#include "wsgtextureprovider.h" +#include "wsurface.h" #include "wpointer.h" #include "wscoplistener.h" #include "weventjunkman.h" @@ -504,6 +506,16 @@ class Q_DECL_HIDDEN WOutputRenderWindowPrivate : public QQuickWindowPrivate bool releaseRenderBuffer(WBufferRenderer *renderer, const char *purpose); bool releaseRenderBuffers(QVector> &needsCommit); void cleanupRetiredRenderResources(bool force); + bool prepareTextureSamplingForRenderPass(wlr_buffer *currentBuffer, + const QVector &activeProviders, + const char *purpose, + int sourceIndex, + QVector *preparedTextures); + bool prepareTextureForCurrentRenderPass(wlr_texture *texture, + const char *purpose); + bool finishTextureSamplingForRenderPass(const QVector &preparedTextures, + const char *purpose, + int sourceIndex); void setCurrentPresentationOutput(WOutput *output); void markSurfaceTexturedForPresentation(WSurface *surface); void submitPresentationFeedbackForOutput(WOutput *output); @@ -550,10 +562,11 @@ class Q_DECL_HIDDEN WOutputRenderWindowPrivate : public QQuickWindowPrivate // failures freeze resource retirement forever, so escalate to fatal. int consecutiveBeginFrameFailures = 0; static constexpr int FatalBeginFrameFailureThreshold = 3; - WPointer m_renderer; - WPointer m_allocator; bool textureSyncQueueValidated = false; bool textureSyncBatchDisabled = false; + + WPointer m_renderer; + WPointer m_allocator; QPointer m_presentation; QPointer m_currentPresentationOutput; QHash>> m_pendingPresentationSurfaces; @@ -568,6 +581,8 @@ class Q_DECL_HIDDEN WOutputRenderWindowPrivate : public QQuickWindowPrivate #endif QStack rendererList; + QVector *m_currentPreparedTextures = nullptr; + QSet m_currentPreparedTextureSet; // Owner token for per-output frame/needs_frame listeners registered on // WOutput via WObject::listeners(). ~WListenerOwner/teardown() detaches @@ -1906,6 +1921,13 @@ bool WOutputRenderWindowPrivate::initRCWithRhi() Q_ASSERT(rcd->rhi == result.rhi); Q_ASSERT(!swapchain); + // Arm the per-QRhi leaked-resource safety sweep on every QRhi we adopt, + // including windows whose render control never produces any BufferData: + // client items holding Buffer/Texture/RenderBuffer objects that outlive + // this QRhi would otherwise trip the VMA "Some allocations were not + // freed" assertion at teardown (which was the Waydroid crash class). + WRenderHelper::addVulkanTeardownSweep(rcd->rhi, rcd->rhi); + WVulkanTrace::activate(rhiSupport->rhiBackend() == QRhi::Vulkan); return true; @@ -2198,6 +2220,216 @@ void WOutputRenderWindowPrivate::cleanupRetiredRenderResources(bool force) } } +bool WOutputRenderWindowPrivate::prepareTextureSamplingForRenderPass(wlr_buffer *currentBuffer, + const QVector &activeProviders, + const char *purpose, + int sourceIndex, + QVector *preparedTextures) +{ + Q_ASSERT(preparedTextures); + preparedTextures->clear(); + m_currentPreparedTextures = nullptr; + m_currentPreparedTextureSet.clear(); + + if (WRenderHelper::getGraphicsApi(rc()) != QSGRendererInterface::Vulkan) + return true; + + m_currentPreparedTextures = preparedTextures; + W_Q(WOutputRenderWindow); + // Accumulate the per-texture foreign-acquire barriers and record them as a + // single vkCmdPipelineBarrier once every provider has been prepared, just + // before the Qt draw. Mirrors wlroots' own render-pass barrier batching. + WRenderHelper::beginTextureBarrierBatch(m_renderer, false); + QSet seenTextures; + qsizetype skippedCurrentRenderTarget = 0; + qsizetype invalidProviderCount = 0; + for (auto provider : activeProviders) { + if (!provider) { + ++invalidProviderCount; + continue; + } + + auto providerBuffer = provider->wlrBuffer(); + if (currentBuffer && providerBuffer == currentBuffer) { + ++skippedCurrentRenderTarget; + WVulkanTrace::sampleDisposition(q, provider, provider->qwTexture(), + WVulkanTrace::SampleDisposition::CurrentRenderTarget); + continue; + } + + auto sampleTexture = provider->texture(); + if (sampleTexture && currentBuffer) { + if (auto rhiTexture = sampleTexture->rhiTexture()) { + auto renderTargetBuffer = WRenderHelper::lookupBuffer(rhiTexture); + if (renderTargetBuffer == currentBuffer) { + ++skippedCurrentRenderTarget; + WVulkanTrace::sampleDisposition(q, provider, provider->qwTexture(), + WVulkanTrace::SampleDisposition::CurrentRenderTarget); + continue; + } + } + } + + auto texture = provider->qwTexture(); + if (!texture || !provider->hasTexture()) { + ++invalidProviderCount; + WVulkanTrace::sampleDisposition(q, provider, texture, + WVulkanTrace::SampleDisposition::InvalidProvider); + continue; + } + + if (seenTextures.contains(texture)) { + WVulkanTrace::sampleDisposition(q, provider, texture, + WVulkanTrace::SampleDisposition::DuplicateTexture); + continue; + } + + seenTextures.insert(texture); + QElapsedTimer prepareTimer; + if (WVulkanTrace::enabled()) + prepareTimer.start(); + const bool prepareOk = WRenderHelper::prepareTextureForSampling(rc(), m_renderer, + texture, purpose); + const qint64 prepareUsec = prepareTimer.isValid() ? prepareTimer.nsecsElapsed() / 1000 : 0; + WVulkanTrace::sampleDisposition(q, provider, texture, + prepareOk ? WVulkanTrace::SampleDisposition::Prepared + : WVulkanTrace::SampleDisposition::PrepareFailed, + prepareUsec); + if (!prepareOk) { + qCWarning(lcWlQtQuickTexture) << "Skipping wlroots texture for this Vulkan render pass because sampling prepare failed" + << "purpose" << purpose + << "sourceIndex" << sourceIndex + << "currentBuffer" << currentBuffer + << "provider" << provider + << "providerBuffer" << providerBuffer + << "wlrTexture" << texture + << "qtTextureSize" << (sampleTexture ? sampleTexture->textureSize() : QSize()) + << "activeProviderCount" << activeProviders.size() + << "preparedTextureCount" << preparedTextures->size() + << "skippedCurrentRenderTarget" << skippedCurrentRenderTarget + << "invalidProviderCount" << invalidProviderCount; + failCurrentFrame(); + // Record the acquire barriers deferred so far so they stay matched + // with the release barriers recorded by the rollback below, then + // return ownership of every texture acquired earlier in the prepass + // and keep all providers bound to their previously valid texture. + (void)WRenderHelper::flushTextureBarrierBatch(rc(), m_renderer, purpose); + (void)finishTextureSamplingForRenderPass(*preparedTextures, + purpose, + sourceIndex); + return false; + } + + preparedTextures->append(texture); + m_currentPreparedTextureSet.insert(texture); + } + + if (!WRenderHelper::flushTextureBarrierBatch(rc(), m_renderer, purpose)) { + qCWarning(lcWlQtQuickTexture) << "Failed to record batched Vulkan texture acquire barriers; rolling back the render pass" + << "purpose" << purpose + << "sourceIndex" << sourceIndex + << "preparedTextureCount" << preparedTextures->size(); + failCurrentFrame(); + (void)finishTextureSamplingForRenderPass(*preparedTextures, + purpose, + sourceIndex); + return false; + } + + return true; +} + +bool WOutputRenderWindowPrivate::prepareTextureForCurrentRenderPass(wlr_texture *texture, + const char *purpose) +{ + if (WRenderHelper::getGraphicsApi(rc()) != QSGRendererInterface::Vulkan) + return true; + + if (!texture || !m_currentPreparedTextures) + return true; + + if (m_currentPreparedTextureSet.contains(texture)) + return true; + + W_Q(WOutputRenderWindow); + QElapsedTimer prepareTimer; + if (WVulkanTrace::enabled()) + prepareTimer.start(); + const bool prepareOk = WRenderHelper::prepareTextureForSampling(rc(), + m_renderer, + texture, + purpose); + WVulkanTrace::sampleDynamicDisposition( + q, + texture, + prepareOk ? WVulkanTrace::SampleDisposition::Prepared + : WVulkanTrace::SampleDisposition::PrepareFailed, + prepareTimer.isValid() ? prepareTimer.nsecsElapsed() / 1000 : 0); + if (!prepareOk) { + failCurrentFrame(); + return false; + } + + m_currentPreparedTextureSet.insert(texture); + m_currentPreparedTextures->append(texture); + return true; +} + +bool WOutputRenderWindowPrivate::finishTextureSamplingForRenderPass(const QVector &preparedTextures, + const char *purpose, + int sourceIndex) +{ + if (WRenderHelper::getGraphicsApi(rc()) != QSGRendererInterface::Vulkan) { + m_currentPreparedTextures = nullptr; + m_currentPreparedTextureSet.clear(); + return true; + } + + W_Q(WOutputRenderWindow); + // Accumulate the per-texture foreign-release barriers and record them as a + // single vkCmdPipelineBarrier once every provider has been released, right + // after the Qt draw. + WRenderHelper::beginTextureBarrierBatch(m_renderer, true); + bool ok = true; + for (qsizetype i = preparedTextures.size() - 1; i >= 0; --i) { + auto texture = preparedTextures.at(i); + if (!texture) + continue; + + QElapsedTimer finishTimer; + if (WVulkanTrace::enabled()) + finishTimer.start(); + const bool finishOk = WRenderHelper::finishTextureSampling(rc(), m_renderer, + texture, purpose); + WVulkanTrace::sampleFinished(q, texture, finishOk, + finishTimer.isValid() ? finishTimer.nsecsElapsed() / 1000 : 0); + if (!finishOk) { + qCWarning(lcWlQtQuickTexture) << "Vulkan texture sampling finish failed for render pass" + << "purpose" << purpose + << "sourceIndex" << sourceIndex + << "wlrTexture" << texture + << "preparedTextureCount" << preparedTextures.size() + << "releaseIndex" << i; + ok = false; + } + } + + if (!WRenderHelper::flushTextureBarrierBatch(rc(), m_renderer, purpose)) { + qCWarning(lcWlQtQuickTexture) << "Failed to record batched Vulkan texture release barriers" + << "purpose" << purpose + << "sourceIndex" << sourceIndex + << "preparedTextureCount" << preparedTextures.size(); + ok = false; + } + + m_currentPreparedTextures = nullptr; + m_currentPreparedTextureSet.clear(); + if (!ok) { + failCurrentFrameFatally(QStringLiteral("Failed to return one or more sampled Vulkan textures to wlroots")); + } + return ok; +} + void WOutputRenderWindowPrivate::setCurrentPresentationOutput(WOutput *output) { m_currentPresentationOutput = output; @@ -2367,6 +2599,8 @@ void WOutputRenderWindowPrivate::doRender(wlr_output *needsFrameOutput, const auto finishFrame = [this, q] (const QList> &committedOutputs) { WRenderHelper::abortTextureSyncBatch(m_renderer); clearPresentationFeedback(); + m_currentPreparedTextures = nullptr; + m_currentPreparedTextureSet.clear(); // Drop cursor commits deferred for a frame that did not complete, so // they can never outlive the frame whose command buffer produced them. @@ -2439,6 +2673,12 @@ void WOutputRenderWindowPrivate::doRender(wlr_output *needsFrameOutput, const bool batchStarted = WRenderHelper::beginTextureSyncBatch( rc(), m_renderer, !textureSyncQueueValidated); if (batchStarted) { + if (!textureSyncQueueValidated) { + // Qt and wlroots share the same VkQueue, so the GPU-side + // asynchronous staging-upload path can be chained through + // the texture-sync bridge before Qt submits the frame. + WRenderHelper::setStageAsyncEnabled(m_renderer, true); + } textureSyncQueueValidated = true; } else { // The immediate CPU-wait path remains correct, and avoids @@ -2911,6 +3151,35 @@ bool WOutputRenderWindow::tryMoveHardwareCursor(WOutput *output, QQuickItem *cur return false; } +bool WOutputRenderWindow::prepareTextureSamplingForRenderPass(wlr_buffer *currentBuffer, + const QVector &activeProviders, + const char *purpose, + int sourceIndex, + QVector *preparedTextures) +{ + Q_D(WOutputRenderWindow); + return d->prepareTextureSamplingForRenderPass(currentBuffer, + activeProviders, + purpose, + sourceIndex, + preparedTextures); +} + +bool WOutputRenderWindow::prepareTextureForCurrentRenderPass(wlr_texture *texture, + const char *purpose) +{ + Q_D(WOutputRenderWindow); + return d->prepareTextureForCurrentRenderPass(texture, purpose); +} + +bool WOutputRenderWindow::finishTextureSamplingForRenderPass(const QVector &preparedTextures, + const char *purpose, + int sourceIndex) +{ + Q_D(WOutputRenderWindow); + return d->finishTextureSamplingForRenderPass(preparedTextures, purpose, sourceIndex); +} + WBufferRenderer *WOutputRenderWindow::currentRenderer() const { Q_D(const WOutputRenderWindow); diff --git a/waylib/src/server/qtquick/woutputrenderwindow.h b/waylib/src/server/qtquick/woutputrenderwindow.h index a8bb80ed95..c3366cb2da 100644 --- a/waylib/src/server/qtquick/woutputrenderwindow.h +++ b/waylib/src/server/qtquick/woutputrenderwindow.h @@ -4,10 +4,12 @@ #pragma once #include +#include #include #include #include +#include Q_MOC_INCLUDE() @@ -17,9 +19,11 @@ class WOutputViewport; class WOutputLayer; class WBufferRenderer; class WOutputHelper; +class WSGTextureProvider; class WPresentation; class WSurface; class WOutputRenderWindowPrivate; + class WAYLIB_SERVER_EXPORT WOutputRenderWindow : public QQuickWindow, public QQmlParserStatus { Q_OBJECT @@ -54,13 +58,7 @@ class WAYLIB_SERVER_EXPORT WOutputRenderWindow : public QQuickWindow, public QQm void init(wlr_renderer *renderer, wlr_allocator *allocator); wlr_renderer *renderer() const; wlr_allocator *allocator() const; - - qreal width() const; - qreal height() const; - WBufferRenderer *currentRenderer() const; - bool inRendering() const; - - void setRenderEnabled(bool enabled); void setPresentation(WPresentation *presentation); + void setPresentation(WPresentation *presentation); void markSurfaceTexturedForPresentation(WSurface *surface); // Fast path for pointer movement on the Vulkan/DRM hardware-cursor path: @@ -70,7 +68,23 @@ class WAYLIB_SERVER_EXPORT WOutputRenderWindow : public QQuickWindow, public QQm // cursor, cursor not set up yet, broken commit, or non-Vulkan backend). bool tryMoveHardwareCursor(WOutput *output, QQuickItem *cursorItem); + bool prepareTextureSamplingForRenderPass(wlr_buffer *currentBuffer, + const QVector &activeProviders, + const char *purpose, + int sourceIndex, + QVector *preparedTextures); + bool prepareTextureForCurrentRenderPass(wlr_texture *texture, + const char *purpose); + bool finishTextureSamplingForRenderPass(const QVector &preparedTextures, + const char *purpose, + int sourceIndex); + + qreal width() const; + qreal height() const; + WBufferRenderer *currentRenderer() const; + bool inRendering() const; + void setRenderEnabled(bool enabled); static QList> paintOrderItemList(QQuickItem *root, std::function filter); diff --git a/waylib/src/server/qtquick/wrenderbufferblitter.cpp b/waylib/src/server/qtquick/wrenderbufferblitter.cpp index 9443bd6469..685d530a6c 100644 --- a/waylib/src/server/qtquick/wrenderbufferblitter.cpp +++ b/waylib/src/server/qtquick/wrenderbufferblitter.cpp @@ -12,6 +12,8 @@ WAYLIB_SERVER_BEGIN_NAMESPACE +QAtomicInt WRenderBufferBlitter::s_liveCount{0}; + class Q_DECL_HIDDEN BlitTextureProvider : public QSGTextureProvider { public: BlitTextureProvider() @@ -216,11 +218,12 @@ WRenderBufferBlitter::WRenderBufferBlitter(QQuickItem *parent) setFlag(ItemHasContents); W_D(WRenderBufferBlitter); d->init(); + s_liveCount.fetchAndAddRelaxed(1); } WRenderBufferBlitter::~WRenderBufferBlitter() { - + s_liveCount.fetchAndAddRelaxed(-1); } QQuickItem *WRenderBufferBlitter::content() const diff --git a/waylib/src/server/qtquick/wrenderbufferblitter.h b/waylib/src/server/qtquick/wrenderbufferblitter.h index 5157c39604..98b4f227ce 100644 --- a/waylib/src/server/qtquick/wrenderbufferblitter.h +++ b/waylib/src/server/qtquick/wrenderbufferblitter.h @@ -6,6 +6,7 @@ #include #include #include +#include WAYLIB_SERVER_BEGIN_NAMESPACE @@ -23,6 +24,10 @@ class Q_DECL_EXPORT WRenderBufferBlitter : public QQuickItem, public WObject explicit WRenderBufferBlitter(QQuickItem *parent = nullptr); ~WRenderBufferBlitter(); + // Number of live items. Lets the per-frame scene walk in + // WBufferRenderer short-circuit when no blitter exists anywhere. + static int liveCount() { return s_liveCount.loadRelaxed(); } + QQuickItem *content() const; bool offscreen() const; @@ -35,6 +40,8 @@ private Q_SLOTS: void invalidateSceneGraph(); private: + static QAtomicInt s_liveCount; + QSGNode *updatePaintNode(QSGNode *, UpdatePaintNodeData *) override; void itemChange(ItemChange, const ItemChangeData &) override; void geometryChange(const QRectF &newGeometry, const QRectF &oldGeometry) override; diff --git a/waylib/src/server/qtquick/wrenderhelper.cpp b/waylib/src/server/qtquick/wrenderhelper.cpp index 035dd438c0..0339c149bd 100644 --- a/waylib/src/server/qtquick/wrenderhelper.cpp +++ b/waylib/src/server/qtquick/wrenderhelper.cpp @@ -48,6 +48,8 @@ extern "C" { #if defined(ENABLE_VULKAN_RENDER) && QT_VERSION >= QT_VERSION_CHECK(6, 8, 0) W_DECLARE_PRIVATE_MEMBER(QRhi_d_tag, QRhi, d, QRhiImplementation *); +using QRhiResourceMap = QHash; +W_DECLARE_PRIVATE_MEMBER(QRhiResources_tag, QRhiImplementation, resources, QRhiResourceMap); #endif WAYLIB_SERVER_BEGIN_NAMESPACE @@ -158,10 +160,82 @@ static bool getVulkanRenderBufferAttribs(wlr_renderer *renderer, #endif #if defined(ENABLE_VULKAN_RENDER) && QT_VERSION >= QT_VERSION_CHECK(6, 8, 0) + +// Qt's own leak report in ~QRhiImplementation runs after QRhiVulkan::destroy() +// has already aborted the process through the VMA assertion when any +// QRhiResource created on this QRhi outlives it (QML items or scene graph +// nodes that own QRhiBuffer/QRhiTexture objects and are torn down after the +// render control has released the QRhi). A QRhi cleanup callback still runs +// from ~QRhi before destroy(), while the VkDevice is alive, which is the +// only point where the leaked native objects can still be released safely: +// QVkBuffer/QVkTexture/QVkRenderBuffer::destroy() enqueue a deferred release +// that QRhiVulkan::destroy() force-executes (after vkDeviceWaitIdle) before +// it destroys the VmaAllocator. Qt-internal objects (command buffers, swap +// chains) and resources without VMA memory (samplers, pipelines, shader +// resource bindings, render-pass descriptors) are deliberately untouched. +static void forceReleaseVulkanGraphicsResources(QRhi *rhi) +{ + if (!rhi) + return; + + auto *rhiImplementation = static_cast( + W_PRIVATE_MEMBER(*rhi, QRhi_d_tag {})); + if (!rhiImplementation) + return; + + auto ®isteredResources = W_PRIVATE_MEMBER(*rhiImplementation, QRhiResources_tag {}); + const auto resources = registeredResources.keys(); + int releasedCount = 0; + for (QRhiResource *resource : resources) { + if (!resource) + continue; + switch (resource->resourceType()) { + case QRhiResource::Buffer: + case QRhiResource::Texture: + case QRhiResource::RenderBuffer: + ++releasedCount; + qCWarning(lcWlRenderHelper) + << "Force-releasing leaked QRhi graphics resource at teardown" + << "type" << int(resource->resourceType()) + << "rhi" << Qt::hex << quintptr(rhi) << Qt::dec + << "name" << (resource->name().isEmpty() ? QByteArrayLiteral("") + : resource->name()); + resource->destroy(); + break; + default: + break; + } + } + if (releasedCount) { + qCWarning(lcWlRenderHelper) + << "Force-released leaked QRhi graphics resources before Vulkan allocator teardown" + << "count" << releasedCount; + } +} +#endif + +void WRenderHelper::addVulkanTeardownSweep(QRhi *rhi, const void *key) +{ +#if defined(ENABLE_VULKAN_RENDER) && QT_VERSION >= QT_VERSION_CHECK(6, 8, 0) + if (!rhi || !key || rhi->backend() != QRhi::Vulkan) + return; + rhi->addCleanupCallback(key, [ ] (QRhi *cleanupRhi) { + forceReleaseVulkanGraphicsResources(cleanupRhi); + }); +#else + Q_UNUSED(rhi); + Q_UNUSED(key); +#endif +} + struct Q_DECL_HIDDEN BufferData { BufferData() = default; ~BufferData() { +#if defined(ENABLE_VULKAN_RENDER) && QT_VERSION >= QT_VERSION_CHECK(6, 8, 0) + if (vulkanBackdropRhi) + vulkanBackdropRhi->removeCleanupCallback(this); +#endif resetWindowRenderTarget(); } @@ -171,6 +245,18 @@ struct Q_DECL_HIDDEN BufferData { WImageRenderTarget paintDevice; QQuickRenderTarget renderTarget; QQuickWindowRenderTarget windowRenderTarget; + QQuickRenderTarget preserveRenderTarget; + QQuickWindowRenderTarget preserveWindowRenderTarget; + QQuickRenderTarget vulkanBackdropRenderTarget; + QQuickWindowRenderTarget vulkanBackdropWindowRenderTarget; + QQuickRenderTarget vulkanBackdropPreserveRenderTarget; + QQuickWindowRenderTarget vulkanBackdropPreserveWindowRenderTarget; + QQuickRenderTarget vulkanBackdropResumeRenderTarget; + QQuickWindowRenderTarget vulkanBackdropResumeWindowRenderTarget; +#if defined(ENABLE_VULKAN_RENDER) && QT_VERSION >= QT_VERSION_CHECK(6, 8, 0) + QRhi *vulkanBackdropRhi = nullptr; +#endif + bool vulkanBackdropUnavailableLogged = false; bool colorPreserved = false; static inline void cleanupWindowRenderTarget(QQuickWindowRenderTarget &target) { @@ -238,14 +324,58 @@ struct Q_DECL_HIDDEN BufferData { } inline void resetWindowRenderTarget() { + // The three backdrop targets share the depth texture owned by the + // clear target. Destroy all borrowers before destroying the owner. + cleanupWindowRenderTarget(vulkanBackdropResumeWindowRenderTarget); + cleanupWindowRenderTarget(vulkanBackdropPreserveWindowRenderTarget); + cleanupWindowRenderTarget(vulkanBackdropWindowRenderTarget); + cleanupWindowRenderTarget(preserveWindowRenderTarget); cleanupWindowRenderTarget(windowRenderTarget); } -}; + +#if defined(ENABLE_VULKAN_RENDER) && QT_VERSION >= QT_VERSION_CHECK(6, 8, 0) + // Register (idempotent, keyed by this BufferData) a cleanup callback on + // the QRhi so resetWindowRenderTarget() runs from ~QRhi's runCleanup(), + // before the backend destroys its VmaAllocator. QRhi-backed objects + // (render buffers, texture render targets, render-pass descriptors) must + // not outlive the RHI: at shutdown the window tears the render control + // down while WRenderHelper still caches them, and every allocation that + // survives until vmaDestroyAllocator turns into a fatal "Some allocations + // were not freed before destruction of this memory block!" assertion. + // Only the Vulkan backend registers; the OpenGL flow is unchanged. + void registerVulkanRhiTeardownCleanup(QRhi *rhi) + { + if (vulkanBackdropRhi == rhi) + return; + if (vulkanBackdropRhi) + vulkanBackdropRhi->removeCleanupCallback(this); + + vulkanBackdropRhi = rhi; + if (!vulkanBackdropRhi) + return; + + vulkanBackdropRhi->addCleanupCallback(this, [this] (QRhi *cleanupRhi) { + if (vulkanBackdropRhi != cleanupRhi) + return; + + // The callback list is being cleared by QRhi. Avoid trying to + // remove this callback while it is being invoked. + vulkanBackdropRhi = nullptr; + resetWindowRenderTarget(); + }); + // Also arm the per-QRhi safety sweep once; it force-releases any + // graphics resource that outlives the render control on this QRhi + // (see forceReleaseVulkanGraphicsResources). Keyed by the QRhi, so + // repeated registrations deduplicate. + WRenderHelper::addVulkanTeardownSweep(vulkanBackdropRhi, vulkanBackdropRhi); + } #endif +}; class WRenderHelper::RenderTarget::Private { public: std::weak_ptr data; + QQuickRenderTarget selectedTarget; }; WRenderHelper::RenderTarget::RenderTarget() : d(new Private) {} @@ -273,7 +403,7 @@ QQuickRenderTarget WRenderHelper::RenderTarget::rt() const auto data = d->data.lock(); if (!data) return {}; - return data->renderTarget; + return d->selectedTarget.isNull() ? data->renderTarget : d->selectedTarget; } wlr_buffer *WRenderHelper::RenderTarget::buffer() const @@ -451,17 +581,28 @@ static bool createRhiRenderTarget(const QRhiColorAttachment &colorAttachment, int sampleCount, QRhi *rhi, QQuickWindowRenderTarget &dst, - QRhiTextureRenderTarget::Flags flags = {}) -{ - std::unique_ptr depthStencil( - rhi->newRenderBuffer(QRhiRenderBuffer::DepthStencil, pixelSize, sampleCount)); - if (!depthStencil->create()) { - qCWarning(lcWlRenderHelper, "Failed to build depth-stencil buffer for QQuickRenderTarget"); - return false; + QRhiTextureRenderTarget::Flags flags = {}, + bool withDepthStencil = true) +{ + // The Vulkan backdrop targets are created without a depth attachment: the + // blitter's render node never advertises DepthAwareRendering, so Qt keeps + // the 2D depth buffer disabled for the whole renderer that contains it and + // the attachment would only add a full-attachment depth load/store to every + // pass split. All other callers (including the GLES2 path) keep the depth + // attachment exactly as before. + std::unique_ptr depthStencil; + if (withDepthStencil) { + depthStencil.reset( + rhi->newRenderBuffer(QRhiRenderBuffer::DepthStencil, pixelSize, sampleCount)); + if (!depthStencil->create()) { + qCWarning(lcWlRenderHelper, "Failed to build depth-stencil buffer for QQuickRenderTarget"); + return false; + } } QRhiTextureRenderTargetDescription rtDesc(colorAttachment); - rtDesc.setDepthStencilBuffer(depthStencil.get()); + if (depthStencil) + rtDesc.setDepthStencilBuffer(depthStencil.get()); std::unique_ptr rt(rhi->newTextureRenderTarget(rtDesc, flags)); std::unique_ptr rp; #if defined(ENABLE_VULKAN_RENDER) && QT_VERSION >= QT_VERSION_CHECK(6, 8, 0) @@ -495,10 +636,6 @@ static bool createRhiRenderTarget(const QRhiColorAttachment &colorAttachment, return true; } -#if QT_VERSION >= QT_VERSION_CHECK(6, 8, 0) - -#endif - bool createRhiRenderTarget(QRhi *rhi, const QQuickRenderTarget &source, QQuickWindowRenderTarget &dst, QRhiTextureRenderTarget::Flags rtFlags = {}, QRhiTexture::Flags extraTextureFlags = {}) @@ -575,6 +712,8 @@ class Q_DECL_HIDDEN WRenderHelperPrivate : public WObjectPrivate static bool ensureRhiRenderTarget(QQuickRenderControl *rc, BufferData *data, QRhiTextureRenderTarget::Flags flags, QRhiTexture::Flags extraTextureFlags = {}); + bool ensureVulkanBackdropRenderTargets(QQuickRenderControl *rc, + BufferData *data); W_DECLARE_PUBLIC(WRenderHelper) wlr_renderer *renderer; @@ -643,10 +782,190 @@ bool WRenderHelperPrivate::ensureRhiRenderTarget(QQuickRenderControl *rc, data->renderTarget.setDevicePixelRatio(tmp.devicePixelRatio()); data->renderTarget.setMirrorVertically(tmp.mirrorVertically()); + if (rhi->backend() == QRhi::Vulkan) { + auto rtd = QQuickRenderTargetPrivate::get(&tmp); +#if QT_VERSION >= QT_VERSION_CHECK(6, 8, 0) + auto colorTexture = data->windowRenderTarget.res.texture; +#else + auto colorTexture = data->windowRenderTarget.texture; +#endif + if (!colorTexture) { + qCWarning(lcWlRenderHelper) + << "Failed to build Vulkan preserve render target: missing shared QRhi texture"; + return false; + } + + QRhiColorAttachment colorAttachment(colorTexture); + ok = createRhiRenderTarget(colorAttachment, + rtd->pixelSize, + rtd->sampleCount, + rhi, + data->preserveWindowRenderTarget, + QRhiTextureRenderTarget::PreserveColorContents); + if (!ok) { + qCWarning(lcWlRenderHelper) << "Failed to build Vulkan preserve render target for QQuickRenderTarget"; + return false; + } +#if QT_VERSION >= QT_VERSION_CHECK(6, 8, 0) + data->preserveRenderTarget = QQuickRenderTarget::fromRhiRenderTarget(data->preserveWindowRenderTarget.rt.renderTarget); +#else + data->preserveRenderTarget = QQuickRenderTarget::fromRhiRenderTarget(data->preserveWindowRenderTarget.renderTarget); +#endif + data->preserveRenderTarget.setDevicePixelRatio(tmp.devicePixelRatio()); + data->preserveRenderTarget.setMirrorVertically(tmp.mirrorVertically()); + } + +#if defined(ENABLE_VULKAN_RENDER) && QT_VERSION >= QT_VERSION_CHECK(6, 8, 0) + // Every BufferData owning QRhi objects must release them from ~QRhi's + // runCleanup(), before the VmaAllocator they are backed by dies (see + // registerVulkanRhiTeardownCleanup). + if (rhi->backend() == QRhi::Vulkan) + data->registerVulkanRhiTeardownCleanup(rhi); +#endif return true; } +bool WRenderHelperPrivate::ensureVulkanBackdropRenderTargets(QQuickRenderControl *rc, + BufferData *data) +{ +#if !defined(ENABLE_VULKAN_RENDER) || QT_VERSION < QT_VERSION_CHECK(6, 8, 0) + Q_UNUSED(rc); + Q_UNUSED(data); + return false; +#else + if (data->vulkanBackdropWindowRenderTarget.rt.renderTarget + && data->vulkanBackdropPreserveWindowRenderTarget.rt.renderTarget + && data->vulkanBackdropResumeWindowRenderTarget.rt.renderTarget) { + return true; + } + if (data->vulkanBackdropUnavailableLogged) + return false; + + QRhi *rhi = rc ? rc->rhi() : nullptr; + auto colorTexture = data->windowRenderTarget.res.texture; + const auto *nativeHandles = rhi && rhi->backend() == QRhi::Vulkan + ? static_cast(rhi->nativeHandles()) + : nullptr; + // The blitter's Vulkan render node never advertises DepthAwareRendering, so + // Qt disables the 2D depth buffer for the whole batch renderer as soon as a + // blitter node exists. The backdrop targets are therefore built without a + // depth attachment (see createRhiRenderTarget): the depth contents are never + // read, and keeping it would add a full-attachment depth load/store plus a + // depth barrier to every pass split. WRenderBufferNode::prepare() fails + // closed if a depth-using renderer ever reaches the blitter. + const bool sameDevice = renderer && wlr_renderer_is_vk(renderer) && nativeHandles + && nativeHandles->physDev == wlr_vk_renderer_get_physical_device(renderer) + && nativeHandles->dev == wlr_vk_renderer_get_device(renderer) + && nativeHandles->gfxQueueFamilyIdx == wlr_vk_renderer_get_queue_family(renderer) + && nativeHandles->gfxQueue == waylib_vk_renderer_get_queue(renderer); + if (!rhi || rhi->backend() != QRhi::Vulkan || !colorTexture + || colorTexture->sampleCount() != 1 + || !colorTexture->flags().testFlag(QRhiTexture::UsedAsTransferSource) + || !sameDevice) { + if (!data->vulkanBackdropUnavailableLogged) { + qCWarning(lcWlRenderHelper) + << "Vulkan backdrop targets are unavailable" + << "reason" << "incompatible renderer, device, or color attachment" + << "rhi" << rhi + << "colorTexture" << colorTexture + << "sampleCount" << (colorTexture ? colorTexture->sampleCount() : 0) + << "transferSource" + << (colorTexture + && colorTexture->flags().testFlag(QRhiTexture::UsedAsTransferSource)) + << "sameDevice" << sameDevice; + data->vulkanBackdropUnavailableLogged = true; + } + return false; + } + BufferData::cleanupWindowRenderTarget(data->vulkanBackdropResumeWindowRenderTarget); + BufferData::cleanupWindowRenderTarget(data->vulkanBackdropPreserveWindowRenderTarget); + BufferData::cleanupWindowRenderTarget(data->vulkanBackdropWindowRenderTarget); + data->vulkanBackdropRenderTarget = {}; + data->vulkanBackdropPreserveRenderTarget = {}; + data->vulkanBackdropResumeRenderTarget = {}; + + const QSize pixelSize = colorTexture->pixelSize(); + const int sampleCount = colorTexture->sampleCount(); + const QRhiColorAttachment colorAttachment(colorTexture); + + bool ok = createRhiRenderTarget(colorAttachment, pixelSize, sampleCount, rhi, + data->vulkanBackdropWindowRenderTarget, + QRhiTextureRenderTarget::Flags{}, + /*withDepthStencil=*/false); + if (ok) { + ok = createRhiRenderTarget(colorAttachment, pixelSize, sampleCount, rhi, + data->vulkanBackdropPreserveWindowRenderTarget, + QRhiTextureRenderTarget::PreserveColorContents, + /*withDepthStencil=*/false); + } + if (ok) { + // Only color needs to be preserved now: with no depth attachment the + // PreserveDepthStencilContents flag would be a no-op, and dropping it + // keeps this render pass compatible with the preserve target's. + ok = createRhiRenderTarget(colorAttachment, pixelSize, sampleCount, rhi, + data->vulkanBackdropResumeWindowRenderTarget, + QRhiTextureRenderTarget::PreserveColorContents, + /*withDepthStencil=*/false); + } + + if (!ok) { + BufferData::cleanupWindowRenderTarget(data->vulkanBackdropResumeWindowRenderTarget); + BufferData::cleanupWindowRenderTarget(data->vulkanBackdropPreserveWindowRenderTarget); + BufferData::cleanupWindowRenderTarget(data->vulkanBackdropWindowRenderTarget); + if (!data->vulkanBackdropUnavailableLogged) { + qCWarning(lcWlRenderHelper) + << "Vulkan backdrop targets are unavailable" + << "reason" << "target creation failed" + << "pixelSize" << pixelSize + << "sampleCount" << sampleCount; + data->vulkanBackdropUnavailableLogged = true; + } + return false; + } + + // Idempotent: usually already registered by ensureRhiRenderTarget(). + data->registerVulkanRhiTeardownCleanup(rhi); + + const auto makeQuickTarget = [&] (QQuickWindowRenderTarget &windowTarget) { + QQuickRenderTarget target = QQuickRenderTarget::fromRhiRenderTarget( + windowTarget.rt.renderTarget); + target.setDevicePixelRatio(data->renderTarget.devicePixelRatio()); + target.setMirrorVertically(data->renderTarget.mirrorVertically()); + return target; + }; + data->vulkanBackdropRenderTarget = makeQuickTarget( + data->vulkanBackdropWindowRenderTarget); + data->vulkanBackdropPreserveRenderTarget = makeQuickTarget( + data->vulkanBackdropPreserveWindowRenderTarget); + data->vulkanBackdropResumeRenderTarget = makeQuickTarget( + data->vulkanBackdropResumeWindowRenderTarget); + + s_rhiRenderBuffers->append({ + data->vulkanBackdropWindowRenderTarget.rt.renderTarget, + colorTexture, data->buffer }); + s_rhiRenderBuffers->append({ + data->vulkanBackdropPreserveWindowRenderTarget.rt.renderTarget, + colorTexture, data->buffer }); + s_rhiRenderBuffers->append({ + data->vulkanBackdropResumeWindowRenderTarget.rt.renderTarget, + colorTexture, data->buffer }); + + if (WVulkanTrace::enabled()) { + qCDebug(lcWlRenderHelper).noquote() + << QStringLiteral("VKTRACE event=blitter-targets-create buffer=%1 color=%2 clear=%3 preserveColor=%4 resume=%5 size=%6x%7") + .arg(quintptr(data->buffer.get()), 0, 16) + .arg(quintptr(colorTexture), 0, 16) + .arg(quintptr(data->vulkanBackdropWindowRenderTarget.rt.renderTarget), 0, 16) + .arg(quintptr(data->vulkanBackdropPreserveWindowRenderTarget.rt.renderTarget), 0, 16) + .arg(quintptr(data->vulkanBackdropResumeWindowRenderTarget.rt.renderTarget), 0, 16) + .arg(pixelSize.width()) + .arg(pixelSize.height()); + } + + return true; +#endif +} WRenderHelper::WRenderHelper(wlr_renderer *renderer, QObject *parent) : QObject(parent) @@ -1231,7 +1550,8 @@ wlr_buffer *WRenderHelper::toBuffer(wlr_renderer *renderer, QSGTexture *texture, } WRenderHelper::RenderTarget WRenderHelper::acquireRenderTarget(QQuickRenderControl *rc, wlr_buffer *buffer, - WGlobal::ColorContentsMode mode) + WGlobal::ColorContentsMode mode, + bool useVulkanBackdrop) { W_D(WRenderHelper); Q_ASSERT(buffer); @@ -1273,6 +1593,10 @@ WRenderHelper::RenderTarget WRenderHelper::acquireRenderTarget(QQuickRenderContr } RenderTarget result; result.d->data = data; + if (useVulkanBackdrop + && d->ensureVulkanBackdropRenderTargets(rc, data.get())) { + result.d->selectedTarget = data->vulkanBackdropRenderTarget; + } return result; } } @@ -1352,6 +1676,10 @@ WRenderHelper::RenderTarget WRenderHelper::acquireRenderTarget(QQuickRenderContr if (auto texture = bufferData->windowRenderTarget.res.texture) { s_rhiRenderBuffers->append({ bufferData->windowRenderTarget.rt.renderTarget, texture, bufferData->buffer.get() }); + if (bufferData->preserveWindowRenderTarget.rt.renderTarget) { + s_rhiRenderBuffers->append({ bufferData->preserveWindowRenderTarget.rt.renderTarget, + texture, bufferData->buffer.get() }); + } } } @@ -1368,12 +1696,38 @@ WRenderHelper::RenderTarget WRenderHelper::acquireRenderTarget(QQuickRenderContr RenderTarget result; result.d->data = d->buffers.last(); + if (useVulkanBackdrop + && d->ensureVulkanBackdropRenderTargets(rc, d->buffers.last().get())) { + result.d->selectedTarget = d->buffers.last()->vulkanBackdropRenderTarget; + } return result; } +QQuickRenderTarget WRenderHelper::preserveRenderTarget(wlr_buffer *buffer, + bool useVulkanBackdrop) const +{ + W_DC(WRenderHelper); + for (auto data : std::as_const(d->buffers)) { + if (data->buffer == buffer) { + return useVulkanBackdrop + ? data->vulkanBackdropPreserveRenderTarget + : data->preserveRenderTarget; + } + } + return {}; +} +QQuickRenderTarget WRenderHelper::vulkanBackdropResumeRenderTarget(wlr_buffer *buffer) const +{ + W_DC(WRenderHelper); + for (auto data : std::as_const(d->buffers)) { + if (data->buffer == buffer) + return data->vulkanBackdropResumeRenderTarget; + } + return {}; +} WRenderHelper::RenderTarget WRenderHelper::lastRenderTarget() const { @@ -1841,6 +2195,325 @@ void WRenderHelper::abortTextureSyncBatch(wlr_renderer *renderer) #endif } +bool WRenderHelper::beginTextureBarrierBatch(wlr_renderer *renderer, bool release) +{ +#ifdef ENABLE_VULKAN_RENDER + if (!renderer || !wlr_renderer_is_vk(renderer)) + return true; + + if (!waylib_vk_renderer_begin_texture_barrier_batch(renderer, release)) { + qCWarning(lcWlRenderHelper) << "Failed to begin Vulkan texture barrier batch" + << "release" << release; + return false; + } +#else + Q_UNUSED(renderer); + Q_UNUSED(release); +#endif + return true; +} + +bool WRenderHelper::flushTextureBarrierBatch(QQuickRenderControl *rc, + wlr_renderer *renderer, + const char *purpose) +{ +#ifdef ENABLE_VULKAN_RENDER + if (!renderer || !wlr_renderer_is_vk(renderer)) + return true; + + if (!rc || !rc->rhi() || rc->rhi()->backend() != QRhi::Vulkan) { + qCWarning(lcWlRenderHelper) << "Cannot flush Vulkan texture barrier batch: missing Vulkan QRhi" + << "purpose" << purpose; + waylib_vk_renderer_abort_texture_barrier_batch(renderer); + return false; + } + + if (rc->rhi()->isDeviceLost() || !rc->rhi()->isRecordingFrame()) { + qCWarning(lcWlRenderHelper) << "Cannot flush Vulkan texture barrier batch: QRhi frame is not usable" + << "purpose" << purpose + << "deviceLost" << rc->rhi()->isDeviceLost() + << "recordingFrame" << rc->rhi()->isRecordingFrame(); + waylib_vk_renderer_abort_texture_barrier_batch(renderer); + return false; + } + + auto commandBuffer = rc->commandBuffer(); + if (!commandBuffer) { + qCWarning(lcWlRenderHelper) << "Cannot flush Vulkan texture barrier batch: missing QRhi command buffer" + << "purpose" << purpose; + waylib_vk_renderer_abort_texture_barrier_batch(renderer); + return false; + } + + commandBuffer->beginExternal(); + auto handles = static_cast(commandBuffer->nativeHandles()); + if (!handles || handles->commandBuffer == VK_NULL_HANDLE) { + commandBuffer->endExternal(); + qCWarning(lcWlRenderHelper) << "Cannot flush Vulkan texture barrier batch: missing native Vulkan command buffer" + << "purpose" << purpose; + waylib_vk_renderer_abort_texture_barrier_batch(renderer); + return false; + } + + // Ownership/layout barrier batches must be recorded between QRhi passes. + if (Q_UNLIKELY(qtVulkanPassIsRecording(commandBuffer))) { + commandBuffer->endExternal(); + qCCritical(lcWlRenderHelper) + << "Vulkan texture barrier batch flush requested inside an active QRhi pass" + << "purpose" << purpose; + waylib_vk_renderer_abort_texture_barrier_batch(renderer); + return false; + } + + const bool ok = waylib_vk_renderer_flush_texture_barrier_batch( + renderer, handles->commandBuffer); + commandBuffer->endExternal(); + + if (!ok) { + qCWarning(lcWlRenderHelper) << "Failed to flush Vulkan texture barrier batch" + << "purpose" << purpose; + waylib_vk_renderer_abort_texture_barrier_batch(renderer); + } + return ok; +#else + Q_UNUSED(rc); + Q_UNUSED(renderer); + Q_UNUSED(purpose); + return true; +#endif +} + +void WRenderHelper::abortTextureBarrierBatch(wlr_renderer *renderer) +{ +#ifdef ENABLE_VULKAN_RENDER + if (renderer && wlr_renderer_is_vk(renderer)) + waylib_vk_renderer_abort_texture_barrier_batch(renderer); +#else + Q_UNUSED(renderer); +#endif +} + +void WRenderHelper::setStageAsyncEnabled(wlr_renderer *renderer, bool enabled) +{ +#ifdef ENABLE_VULKAN_RENDER + if (renderer && wlr_renderer_is_vk(renderer)) + waylib_vk_renderer_set_stage_async_enabled(renderer, enabled); +#else + Q_UNUSED(renderer); + Q_UNUSED(enabled); +#endif +} + +bool WRenderHelper::prepareTextureForSampling(QQuickRenderControl *rc, + wlr_renderer *renderer, + wlr_texture *texture, + const char *purpose) +{ +#ifdef ENABLE_VULKAN_RENDER + if (!renderer || !texture) + return true; + + if (!wlr_renderer_is_vk(renderer)) + return true; + + if (!wlr_texture_is_vk(texture)) { + qCWarning(lcWlQtQuickTexture) << "Vulkan texture sampling prepare failed: non-Vulkan wlroots texture" + << "purpose" << purpose + << "wlrTexture" << texture; + return false; + } + + wlr_vk_image_attribs rawAttribs = {}; + wlr_vk_texture_get_image_attribs(texture, &rawAttribs); + + if (!rc || !rc->rhi() || rc->rhi()->backend() != QRhi::Vulkan) { + qCWarning(lcWlQtQuickTexture) << "Vulkan texture sampling prepare failed: missing Vulkan QRhi" + << "purpose" << purpose + << "wlrTexture" << texture + << "image" << vkImageName(rawAttribs.image) + << "wlrootsLayout" << vkImageLayoutName(rawAttribs.layout) + << "format" << hex32(rawAttribs.format) + << "size" << wlrTextureSize(texture); + return false; + } + + if (rc->rhi()->isDeviceLost() || !rc->rhi()->isRecordingFrame()) { + qCWarning(lcWlQtQuickTexture) << "Vulkan texture sampling prepare failed: QRhi frame is not usable" + << "purpose" << purpose + << "deviceLost" << rc->rhi()->isDeviceLost() + << "recordingFrame" << rc->rhi()->isRecordingFrame() + << "wlrTexture" << texture + << "image" << vkImageName(rawAttribs.image) + << "wlrootsLayout" << vkImageLayoutName(rawAttribs.layout) + << "format" << hex32(rawAttribs.format) + << "size" << wlrTextureSize(texture); + return false; + } + + auto commandBuffer = rc->commandBuffer(); + if (!commandBuffer) { + qCWarning(lcWlQtQuickTexture) << "Vulkan texture sampling prepare failed: missing QRhi command buffer" + << "purpose" << purpose + << "wlrTexture" << texture + << "image" << vkImageName(rawAttribs.image) + << "wlrootsLayout" << vkImageLayoutName(rawAttribs.layout) + << "format" << hex32(rawAttribs.format) + << "size" << wlrTextureSize(texture); + return false; + } + + commandBuffer->beginExternal(); + auto handles = static_cast(commandBuffer->nativeHandles()); + if (!handles || handles->commandBuffer == VK_NULL_HANDLE) { + commandBuffer->endExternal(); + qCWarning(lcWlQtQuickTexture) << "Vulkan texture sampling prepare failed: missing native Vulkan command buffer" + << "purpose" << purpose + << "wlrTexture" << texture + << "image" << vkImageName(rawAttribs.image) + << "wlrootsLayout" << vkImageLayoutName(rawAttribs.layout) + << "format" << hex32(rawAttribs.format) + << "size" << wlrTextureSize(texture); + return false; + } + + // Acquire ownership barriers inside a pass instance are illegal; texture + // acquisition must happen between QRhi passes. + if (Q_UNLIKELY(qtVulkanPassIsRecording(commandBuffer))) { + commandBuffer->endExternal(); + qCCritical(lcWlQtQuickTexture) + << "Vulkan texture sampling prepare requested inside an active QRhi pass" + << "purpose" << purpose << "wlrTexture" << texture; + return false; + } + + const bool ok = waylib_vk_renderer_prepare_texture_for_sampling( + renderer, texture, handles->commandBuffer, nullptr); + commandBuffer->endExternal(); + + if (!ok) { + qCWarning(lcWlQtQuickTexture) << "Vulkan texture sampling prepare failed" + << "purpose" << purpose + << "wlrTexture" << texture + << "image" << vkImageName(rawAttribs.image) + << "wlrootsLayout" << vkImageLayoutName(rawAttribs.layout) + << "format" << hex32(rawAttribs.format) + << "size" << wlrTextureSize(texture); + return false; + } + +#else + Q_UNUSED(rc); + Q_UNUSED(renderer); + Q_UNUSED(texture); + Q_UNUSED(purpose); +#endif + return true; +} + +bool WRenderHelper::finishTextureSampling(QQuickRenderControl *rc, + wlr_renderer *renderer, + wlr_texture *texture, + const char *purpose) +{ +#ifdef ENABLE_VULKAN_RENDER + if (!renderer || !texture) + return true; + + if (!wlr_renderer_is_vk(renderer)) + return true; + + if (!wlr_texture_is_vk(texture)) { + qCWarning(lcWlQtQuickTexture) << "Vulkan texture sampling finish failed: non-Vulkan wlroots texture" + << "purpose" << purpose + << "wlrTexture" << texture; + return false; + } + + wlr_vk_image_attribs attribs = {}; + wlr_vk_texture_get_image_attribs(texture, &attribs); + + if (!rc || !rc->rhi() || rc->rhi()->backend() != QRhi::Vulkan) { + qCWarning(lcWlQtQuickTexture) << "Vulkan texture sampling finish failed: missing Vulkan QRhi" + << "purpose" << purpose + << "wlrTexture" << texture + << "image" << vkImageName(attribs.image) + << "layout" << vkImageLayoutName(attribs.layout) + << "format" << hex32(attribs.format) + << "size" << wlrTextureSize(texture); + return false; + } + + if (rc->rhi()->isDeviceLost() || !rc->rhi()->isRecordingFrame()) { + qCWarning(lcWlQtQuickTexture) << "Vulkan texture sampling finish failed: QRhi frame is not usable" + << "purpose" << purpose + << "deviceLost" << rc->rhi()->isDeviceLost() + << "recordingFrame" << rc->rhi()->isRecordingFrame() + << "wlrTexture" << texture + << "image" << vkImageName(attribs.image) + << "layout" << vkImageLayoutName(attribs.layout) + << "format" << hex32(attribs.format) + << "size" << wlrTextureSize(texture); + return false; + } + + auto commandBuffer = rc->commandBuffer(); + if (!commandBuffer) { + qCWarning(lcWlQtQuickTexture) << "Vulkan texture sampling finish failed: missing QRhi command buffer" + << "purpose" << purpose + << "wlrTexture" << texture + << "image" << vkImageName(attribs.image) + << "layout" << vkImageLayoutName(attribs.layout) + << "format" << hex32(attribs.format) + << "size" << wlrTextureSize(texture); + return false; + } + + commandBuffer->beginExternal(); + auto handles = static_cast(commandBuffer->nativeHandles()); + if (!handles || handles->commandBuffer == VK_NULL_HANDLE) { + commandBuffer->endExternal(); + qCWarning(lcWlQtQuickTexture) << "Vulkan texture sampling finish failed: missing native Vulkan command buffer" + << "purpose" << purpose + << "wlrTexture" << texture + << "image" << vkImageName(attribs.image) + << "layout" << vkImageLayoutName(attribs.layout) + << "format" << hex32(attribs.format) + << "size" << wlrTextureSize(texture); + return false; + } + + // Release-ownership barriers are illegal inside a pass instance. + if (Q_UNLIKELY(qtVulkanPassIsRecording(commandBuffer))) { + commandBuffer->endExternal(); + qCCritical(lcWlQtQuickTexture) + << "Vulkan texture sampling finish requested inside an active QRhi pass" + << "purpose" << purpose << "wlrTexture" << texture; + return false; + } + + const bool ok = waylib_vk_renderer_finish_texture_sampling( + renderer, texture, handles->commandBuffer); + commandBuffer->endExternal(); + + if (!ok) { + qCWarning(lcWlQtQuickTexture) << "Vulkan texture sampling finish failed" + << "purpose" << purpose + << "wlrTexture" << texture + << "image" << vkImageName(attribs.image) + << "layout" << vkImageLayoutName(attribs.layout) + << "format" << hex32(attribs.format) + << "size" << wlrTextureSize(texture); + return false; + } + +#else + Q_UNUSED(rc); + Q_UNUSED(renderer); + Q_UNUSED(texture); + Q_UNUSED(purpose); +#endif + return true; +} void WRenderHelper::markBufferContentDirty(wlr_renderer *renderer, wlr_buffer *buffer) { diff --git a/waylib/src/server/qtquick/wrenderhelper.h b/waylib/src/server/qtquick/wrenderhelper.h index 3e17499014..91246df8c2 100644 --- a/waylib/src/server/qtquick/wrenderhelper.h +++ b/waylib/src/server/qtquick/wrenderhelper.h @@ -62,7 +62,11 @@ class WAYLIB_SERVER_EXPORT WRenderHelper : public QObject, public WObject }; RenderTarget acquireRenderTarget(QQuickRenderControl *rc, wlr_buffer *buffer, - WGlobal::ColorContentsMode mode = WGlobal::ColorContentsMode::DontCare); + WGlobal::ColorContentsMode mode = WGlobal::ColorContentsMode::DontCare, + bool useVulkanBackdrop = false); + QQuickRenderTarget preserveRenderTarget(wlr_buffer *buffer, + bool useVulkanBackdrop = false) const; + QQuickRenderTarget vulkanBackdropResumeRenderTarget(wlr_buffer *buffer) const; bool acquireRenderBuffer(QQuickRenderControl *rc, wlr_buffer *buffer, const char *purpose); bool releaseRenderBuffer(QQuickRenderControl *rc, wlr_buffer *buffer, QRhiTexture *renderTargetTexture, const char *purpose); @@ -77,13 +81,37 @@ class WAYLIB_SERVER_EXPORT WRenderHelper : public QObject, public WObject static bool makeTexture(QRhi *rhi, wlr_texture *handle, QSGPlainTexture *texture, bool forceVulkanShaderReadOnlyLayout = false); + static bool prepareTextureForSampling(QQuickRenderControl *rc, + wlr_renderer *renderer, + wlr_texture *texture, + const char *purpose); + static bool finishTextureSampling(QQuickRenderControl *rc, + wlr_renderer *renderer, + wlr_texture *texture, + const char *purpose); static void markBufferContentDirty(wlr_renderer *renderer, wlr_buffer *buffer); static bool beginTextureSyncBatch(QQuickRenderControl *rc, wlr_renderer *renderer, bool verifyQueue); static bool flushTextureSyncBatch(wlr_renderer *renderer); static void abortTextureSyncBatch(wlr_renderer *renderer); + static bool beginTextureBarrierBatch(wlr_renderer *renderer, + bool release); + static bool flushTextureBarrierBatch(QQuickRenderControl *rc, + wlr_renderer *renderer, + const char *purpose); + static void abortTextureBarrierBatch(wlr_renderer *renderer); + static void setStageAsyncEnabled(wlr_renderer *renderer, + bool enabled); static void resetVulkanFrameAliasLists(wlr_renderer *renderer); + // Register a QRhi cleanup callback that force-releases every + // Buffer/Texture/RenderBuffer QRhiResource still owned by the QRhi when + // it is destroyed, before its Vulkan allocator is torn down. Objects + // outside of waylib's control (client QML items, scenegraph caches) that + // outlive a QQuickRenderControl would otherwise leak VMA memory and hit + // the vmaDestroyAllocator "Some allocations were not freed" assertion. + // No-op for non-Vulkan backends; \a key only deduplicates registrations. + static void addVulkanTeardownSweep(QRhi *rhi, const void *key); struct TextureEntry { wlr_buffer *buffer; diff --git a/waylib/src/server/qtquick/wsgtextureprovider.cpp b/waylib/src/server/qtquick/wsgtextureprovider.cpp index 3ec3f3f6f3..9a32a91391 100644 --- a/waylib/src/server/qtquick/wsgtextureprovider.cpp +++ b/waylib/src/server/qtquick/wsgtextureprovider.cpp @@ -474,6 +474,27 @@ void WSGTextureProvider::setBuffer(wlr_buffer *buffer) return; } + // Immediately prepare a texture created during updatePaintNode() when a render + // pass is already active. Otherwise it would miss the preparation performed + // before renderNextFrame(). + const bool prepareOk = d->window->prepareTextureForCurrentRenderPass(candidateTexture, + "setbuffer-immediate"); + if (!prepareOk) { + qCWarning(lcWlQtQuickTexture) + << "Immediate texture preparation failed; keeping the previous Vulkan texture" + << "provider" << this + << "texture" << candidateTexture; + WVulkanTrace::providerDiscard(this, d->window, candidateTexture, + "sampling-prepare-failed"); + auto *candidateRhiTexture = candidateQtTexture.rhiTexture(); + candidateQtTexture.setOwnsTexture(false); + d->scheduleCleanup(candidateRhiTexture, + candidateTexture, + candidateOwnsTexture, + std::move(candidateBuffer)); + return; + } + // During an active pass publish the candidate only after wlroots // sampling ownership has been acquired. Outside a pass it will be // acquired by the normal prepass before the next draw. Until this point @@ -567,6 +588,12 @@ QSGTexture *WSGTextureProvider::texture() const return d->texture ? const_cast(&d->qtTexture) : nullptr; } +bool WSGTextureProvider::hasTexture() const +{ + W_DC(WSGTextureProvider); + return d->texture && d->rhiTexture; +} + wlr_texture *WSGTextureProvider::qwTexture() const { W_DC(WSGTextureProvider); From 854a556cfc7bee268b9704603afad014cfe36adf Mon Sep 17 00:00:00 2001 From: LFRon Date: Sun, 6 Sep 2026 16:20:44 +0800 Subject: [PATCH 11/15] feat(vulkan): allow selecting the Vulkan renderer at startup Folded follow-up: the NVIDIA forceSoftwareCursor workaround (pre-Vulkan, wlr_egl context handoff) is now gated to the OpenGL renderer; the Vulkan backend shares the wlroots VkDevice and defers set_cursor() until the cursor buffer has been submitted and fenced, so forcing a canvas-composited cursor there costs a full synchronous frame per pointer move. The workaround still applies unchanged on GLES2 while a renderer is being selected at startup. --- CMakeLists.txt | 2 +- src/core/treeland.cpp | 13 +++++++++++++ src/core/treelandinit.cpp | 9 ++++++++- src/seat/helper.cpp | 9 ++++++++- 4 files changed, 30 insertions(+), 3 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index db0cf8912b..feda817fef 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -96,10 +96,10 @@ add_definitions("-DQT_MESSAGELOGCONTEXT") set(LOCAL_QML_IMPORT_PATH "${PROJECT_BINARY_DIR}/qt/qml") if(WITH_SUBMODULE_WAYLIB) - include(${CMAKE_CURRENT_SOURCE_DIR}/waylib/cmake/WaylandScannerHelpers.cmake) # Vendored wlroots must be available before waylib consumers. # Scaffold in wlroots/; upstream sources under 3rdparty/wlroots. add_subdirectory(wlroots) + include(${CMAKE_CURRENT_SOURCE_DIR}/waylib/cmake/WaylandScannerHelpers.cmake) add_subdirectory(waylib) list(APPEND LOCAL_QML_IMPORT_PATH "${PROJECT_BINARY_DIR}/waylib/src/server") else() diff --git a/src/core/treeland.cpp b/src/core/treeland.cpp index 7ffe00bbdf..f69e5cf854 100644 --- a/src/core/treeland.cpp +++ b/src/core/treeland.cpp @@ -36,12 +36,14 @@ #include #include #include +#include #include #include #include #include #include +#include #include #include #include @@ -90,6 +92,17 @@ class TreelandPrivate : public QObject auto initializeTreeland = [this, q, onInitialized = std::move(onInitialized)] { helper = qmlEngine->singletonInstance("Treeland", "Helper"); connect(helper, &Helper::requestQuit, q, &Treeland::quit, Qt::QueuedConnection); + if (QQuickWindow::graphicsApi() == QSGRendererInterface::Vulkan) { + connect(helper->window(), + &QQuickWindow::sceneGraphError, + q, + [] (QQuickWindow::SceneGraphError error, const QString &message) { + qCCritical(lcTlCore) << "Scene graph rendering failed; exiting compositor" + << "error" << error + << "message" << message; + QCoreApplication::exit(EXIT_FAILURE); + }); + } if (qEnvironmentVariableIsEmpty("WLR_XWAYLAND")) qputenv("WLR_XWAYLAND", QByteArray(LIBEXEC_DIR) + "/treeland-xwayland"); diff --git a/src/core/treelandinit.cpp b/src/core/treelandinit.cpp index 13647f0ed9..8eab690fd5 100644 --- a/src/core/treelandinit.cpp +++ b/src/core/treelandinit.cpp @@ -6,6 +6,8 @@ #include #include #include +#include +#include #include #include #include @@ -49,7 +51,12 @@ std::unique_ptr preInit(int &argc, char *argv[]) WServer::initializeQPA({}, [](const QString &) { return static_cast(new QDeepinTheme()); }); - QGuiApplication::setAttribute(Qt::AA_UseOpenGLES); + const QByteArray wlrRenderer = qgetenv("WLR_RENDERER"); + const bool explicitVulkanRenderer = wlrRenderer == "vulkan"; + if (explicitVulkanRenderer) + QQuickWindow::setGraphicsApi(QSGRendererInterface::Vulkan); + else + QGuiApplication::setAttribute(Qt::AA_UseOpenGLES); QGuiApplication::setHighDpiScaleFactorRoundingPolicy(Qt::HighDpiScaleFactorRoundingPolicy::PassThrough); QGuiApplication::setQuitOnLastWindowClosed(false); QQuickStyle::setStyle("Chameleon"); diff --git a/src/seat/helper.cpp b/src/seat/helper.cpp index 8f7d619655..432ccf7193 100644 --- a/src/seat/helper.cpp +++ b/src/seat/helper.cpp @@ -2954,7 +2954,14 @@ bool Helper::doGesture(QInputEvent *event) Output *Helper::createNormalOutput(WOutput *output) { Output *o = Output::create(output, qmlEngine(), this); - if (isNvidiaCardPresent()) { + // The NVIDIA software-cursor workaround predates the Vulkan renderer and + // targeted the GLES wlr_egl context handoff; the Vulkan backend shares the + // wlroots VkDevice with Qt and defers set_cursor() until the cursor buffer + // has completed on the GPU, so the workaround is no longer applicable and + // would defeat the hardware-cursor fast path (a full canvas repaint plus + // synchronous frame per pointer move). Do not change the GLES2 behaviour. + if (isNvidiaCardPresent() + && WRenderHelper::getGraphicsApi() != QSGRendererInterface::Vulkan) { o->outputItem()->setProperty("forceSoftwareCursor", true); } o->outputItem()->stackBefore(m_rootSurfaceContainer); From 2156c8ecf9cc680fb19a0587da6a19326c1d7230 Mon Sep 17 00:00:00 2001 From: LFRon Date: Sun, 6 Sep 2026 16:20:44 +0800 Subject: [PATCH 12/15] fix(capture): handle Vulkan capture failures gracefully Folded follow-up for Vulkan-correct capture and preview paths: - WBufferDumper::dumpBufferToImage() on the Vulkan renderer now takes the wlroots readback gate (waylib_vk_texture_begin_readback/end_readback): a DMA-BUF-imported wlr_texture is FOREIGN-owned and behind a producer fence between frames, so reading its pixels without the gate (wlr_texture_ read_pixels -> upstream vulkan_read_pixels, which assumes a GENERAL layout and a queued producer) is undefined contents and a validation error. The gate acquires/releases the foreign ownership around the staging command buffer. - SHM-backed buffers (XWayland, wlr_allocator shm) are read out directly via CPU pixel access; importing them as staging textures and letting the upstream read path walk into them is precisely the layout/ownership mistake avoided above. - WTextureCapturer::doGrabToImage() on Vulkan previously read back the provider's QRhiTexture (a foreign-owned imported image) through QRhi; it now reads the backing wlr_buffer through WBufferDumper instead, so remote sources and portal previews never touch the image without a matching acquire. GLES2 readback is unchanged. Follow-up hardening (folded): - CaptureContextV1::failFrame() funnels every failure path through one helper that sends failed, drops the frame and disconnects imageReady, so a late imageReady can never emit a buffer after failed. - WTextureCapturer::doGrabToImage() falls back to WOutputViewport::lastBuffer() when the provider has no buffer of its own (the primary output viewport on Vulkan), so previews keep working without touching the foreign-owned image; the WPointer-backed last buffer stays valid until the next output commit. --- src/modules/capture/capture.cpp | 251 +++++++++++++++--- src/modules/capture/capture.h | 4 +- .../qtquick/wtextureproviderprovider.cpp | 48 ++++ waylib/src/server/utils/wbufferdumper.cpp | 57 ++++ waylib/src/server/utils/wbufferdumper.h | 3 +- 5 files changed, 326 insertions(+), 37 deletions(-) diff --git a/src/modules/capture/capture.cpp b/src/modules/capture/capture.cpp index 3f2c190b38..aae2e9362f 100644 --- a/src/modules/capture/capture.cpp +++ b/src/modules/capture/capture.cpp @@ -20,6 +20,7 @@ #include #include #include +#include #include #include @@ -27,8 +28,11 @@ #include #include #include +#include #include +#include +#include #include static inline QRectF scaledRect(const QRectF &rect, qreal devicePixelRatio) @@ -138,11 +142,24 @@ void CaptureContextV1::onCapture(treeland_capture_frame_v1 *frame) } m_frame = frame; auto notifyBuffer = [this] { + if (!m_frame) + return; + if (!m_captureSource || !m_captureSource->imageValid()) { + failFrame("without a valid source image"); + return; + } + + const QRect crop = source()->cropRect(); + if (crop.isEmpty()) { + failFrame("for an empty crop rect"); + return; + } + m_frame->sendBuffer( WTools::drmToShmFormat(WTools::toDrmFormat(m_captureSource->image().format())), - source()->cropRect().width(), - source()->cropRect().height(), - source()->cropRect().width() * 4); + crop.width(), + crop.height(), + crop.width() * 4); m_frame->sendBufferDone(); connect(m_frame, &treeland_capture_frame_v1::copy, @@ -156,15 +173,39 @@ void CaptureContextV1::onCapture(treeland_capture_frame_v1 *frame) if (m_captureSource->imageValid()) { notifyBuffer(); } else { + connect(m_captureSource, + &CaptureSource::imageFailed, + this, + [this] { + failFrame("because the one-shot capture image could not be created"); + }, + Qt::SingleShotConnection); connect(m_captureSource, &CaptureSource::imageReady, this, notifyBuffer); } Q_EMIT finishSelect(); } +void CaptureContextV1::failFrame(const char *reason) +{ + if (!m_frame) + return; + + qCWarning(lcTlCapture) << "Failing capture frame" << reason; + m_frame->sendFailed(); + // The request is finished: drop the frame so a late imageReady cannot emit + // a buffer after failed (the client is expected to destroy it now). + m_frame = nullptr; + if (m_captureSource) + disconnect(m_captureSource, &CaptureSource::imageReady, this, nullptr); +} + void CaptureContextV1::handleFrameCopy(wlr_buffer *buffer) { - if (m_captureSource) { - m_captureSource->copyBuffer(buffer); + if (m_captureSource && m_frame) { + if (!m_captureSource->copyBuffer(buffer)) { + failFrame("because the client buffer copy failed"); + return; + } m_frame->sendReady(); } else { wl_client_post_implementation_error(wl_resource_get_client(m_handle->resource), @@ -690,14 +731,20 @@ void CaptureSourceSelector::createImage() this, &CaptureSourceSelector::createImage); if (m_selectedSource) { - m_selectedSource->createImage(); if (m_selectedSource->imageValid()) { releaseMaskSurface(); } else { connect(m_selectedSource, &CaptureSource::imageReady, this, - &CaptureSourceSelector::releaseMaskSurface); + &CaptureSourceSelector::releaseMaskSurface, + Qt::SingleShotConnection); + connect(m_selectedSource, + &CaptureSource::imageFailed, + this, + &CaptureSourceSelector::releaseMaskSurface, + Qt::SingleShotConnection); + m_selectedSource->createImage(); } } else { releaseMaskSurface(); @@ -947,21 +994,83 @@ QImage CaptureSource::image() const return m_image; } +static QImage normalizeCaptureImage(QImage image) +{ + if (image.isNull()) + return {}; + + if (image.format() != QImage::Format_ARGB32_Premultiplied) + image = image.convertToFormat(QImage::Format_ARGB32_Premultiplied); + + return image; +} + void CaptureSource::createImage() { - if (m_sourceList.size() == 1 && m_sourceList.first().first) { - auto grabber = new WTextureCapturer(m_sourceList.first().second, this); - grabber->grabToImage() - .then([this](QImage image) { - m_image = std::move(image); - Q_EMIT imageReady(); - }) - .onFailed([](const std::exception &e) { - qCCritical(lcTlCapture) << e.what(); - }); - } else { - // TODO: support multiple sources + auto *renderWindow = m_sourceList.size() == 1 && m_sourceList.first().second + ? m_sourceList.first().second->outputRenderWindow() + : nullptr; + const bool vulkanRenderer = renderWindow && renderWindow->rhi() + && renderWindow->rhi()->backend() == QRhi::Vulkan; + if (!vulkanRenderer) { + if (m_sourceList.size() == 1 && m_sourceList.first().first) { + auto grabber = new WTextureCapturer(m_sourceList.first().second, this); + grabber->grabToImage() + .then([this](QImage image) { + m_image = std::move(image); + Q_EMIT imageReady(); + }) + .onFailed([](const std::exception &e) { + qCCritical(lcTlCapture) << e.what(); + }); + } + return; + } + + if (m_sourceList.size() != 1 || !m_sourceList.first().first || !m_sourceList.first().second) { + qCWarning(lcTlCapture) << "Cannot create capture image from invalid source list" + << "sourceCount" << m_sourceList.size(); + Q_EMIT imageFailed(); + return; + } + + auto renderer = renderWindow ? renderWindow->renderer() : nullptr; + auto buffer = sourceDMABuffer(); + if (!buffer || !renderer) { + qCWarning(lcTlCapture) << "Cannot create capture image without source buffer or renderer" + << "buffer" << buffer + << "renderer" << renderer; + Q_EMIT imageFailed(); + return; + } + + if (!wlr_buffer_lock(buffer)) { + qCWarning(lcTlCapture) << "Failed to lock capture source buffer" << buffer; + Q_EMIT imageFailed(); + return; + } + + WBufferUnlockPtr bufferGuard(buffer); + + QImage image; + const auto result = WBufferDumper::dumpBufferToImage(buffer, renderer, image); + if (result != WBufferDumper::DumpResult::Success) { + qCWarning(lcTlCapture) << "Failed to dump capture source buffer" + << "result" << WBufferDumper::dumpResultToString(result) + << "buffer" << buffer; + Q_EMIT imageFailed(); + return; } + + image = normalizeCaptureImage(std::move(image)); + if (image.isNull()) { + qCWarning(lcTlCapture) << "Capture source dump produced an invalid image"; + Q_EMIT imageFailed(); + return; + } + + m_image = std::move(image); + Q_EMIT imageReady(); } wlr_buffer *CaptureSource::sourceDMABuffer() @@ -985,23 +1094,79 @@ wlr_buffer *CaptureSource::sourceDMABuffer() return buffer; } -void CaptureSource::copyBuffer(wlr_buffer *buffer) +bool CaptureSource::copyBuffer(wlr_buffer *buffer) { - Q_ASSERT(imageValid()); + if (!imageValid() || !buffer) { + qCWarning(lcTlCapture) << "Cannot copy capture image without valid image or buffer" + << "imageValid" << imageValid() + << "buffer" << buffer; + return false; + } + auto width = cropRect().width(); auto height = cropRect().height(); + if (width <= 0 || height <= 0) { + qCWarning(lcTlCapture) << "Cannot copy capture image with invalid crop rect" + << cropRect(); + return false; + } + uint32_t format; size_t stride; void *data; - wlr_buffer_begin_data_ptr_access(buffer, WLR_BUFFER_DATA_PTR_ACCESS_WRITE, &data, &format, &stride); - Q_ASSERT(stride == static_cast(width) * 4); // For QImage - QImage img = image().copy(cropRect()); - auto bufFormat = WTools::toImageFormat(format); - if (image().format() != bufFormat) { - img = image().convertToFormat(bufFormat); + if (!wlr_buffer_begin_data_ptr_access(buffer, + WLR_BUFFER_DATA_PTR_ACCESS_WRITE, + &data, + &format, + &stride) + || !data) { + qCWarning(lcTlCapture) << "Failed to access client capture buffer for writing" + << "buffer" << buffer; + return false; } - memcpy(data, img.constBits(), stride * height); + + bool copied = false; + do { + auto bufFormat = WTools::toImageFormat(format); + if (bufFormat == QImage::Format_Invalid) { + qCWarning(lcTlCapture) << "Client capture buffer has unsupported format" + << "format" << format; + break; + } + + QImage img = image().copy(cropRect()); + if (img.isNull() || img.width() != width || img.height() != height) { + qCWarning(lcTlCapture) << "Failed to crop capture image" + << "cropRect" << cropRect() + << "imageSize" << image().size(); + break; + } + + if (img.format() != bufFormat) + img = img.convertToFormat(bufFormat); + + if (img.isNull()) { + qCWarning(lcTlCapture) << "Failed to convert capture image to client buffer format" + << "format" << bufFormat; + break; + } + + if (stride < static_cast(img.bytesPerLine())) { + qCWarning(lcTlCapture) << "Client capture buffer stride is too small" + << "stride" << stride + << "imageStride" << img.bytesPerLine(); + break; + } + + auto dst = static_cast(data); + for (int y = 0; y < img.height(); ++y) { + memcpy(dst + stride * y, img.constScanLine(y), img.bytesPerLine()); + } + copied = true; + } while (false); + wlr_buffer_end_data_ptr_access(buffer); + return copied; } CaptureSourceOutput::CaptureSourceOutput(WOutputViewport *viewport) @@ -1013,10 +1178,18 @@ CaptureSourceOutput::CaptureSourceOutput(WOutputViewport *viewport) wlr_buffer *CaptureSourceOutput::internalBuffer() { Q_ASSERT(m_sourceList.size() == 1); - if (m_sourceList.first().first && m_outputViewport->wTextureProvider()) - return m_outputViewport->wTextureProvider()->wlrBuffer(); - else + if (!m_sourceList.first().first || !m_outputViewport) return nullptr; + + auto *renderWindow = m_sourceList.first().second->outputRenderWindow(); + const bool vulkanRenderer = renderWindow && renderWindow->rhi() + && renderWindow->rhi()->backend() == QRhi::Vulkan; + if (vulkanRenderer) + return m_outputViewport->lastBuffer(); + + if (m_outputViewport->wTextureProvider()) + return m_outputViewport->wTextureProvider()->wlrBuffer(); + return nullptr; } QRect CaptureSourceOutput::cropRect() const @@ -1045,12 +1218,20 @@ CaptureSourceRegion::CaptureSourceRegion(WOutputViewport *viewport, const QRect wlr_buffer *CaptureSourceRegion::internalBuffer() { - if (m_sourceList.size() == 1 && m_sourceList.first().first - && m_sourceList.first().second->wTextureProvider()) { - return m_sourceList.first().second->wTextureProvider()->wlrBuffer(); - } else { + if (m_sourceList.size() != 1 || !m_sourceList.first().first + || m_viewportRegions.isEmpty() || !m_viewportRegions.first().first) { return nullptr; } + + auto *renderWindow = m_sourceList.first().second->outputRenderWindow(); + const bool vulkanRenderer = renderWindow && renderWindow->rhi() + && renderWindow->rhi()->backend() == QRhi::Vulkan; + if (vulkanRenderer) + return m_viewportRegions.first().first->lastBuffer(); + + if (m_sourceList.first().second->wTextureProvider()) + return m_sourceList.first().second->wTextureProvider()->wlrBuffer(); + return nullptr; } CaptureSource::CaptureSourceType CaptureSourceRegion::sourceType() diff --git a/src/modules/capture/capture.h b/src/modules/capture/capture.h index 98ce679896..69b47e0bba 100644 --- a/src/modules/capture/capture.h +++ b/src/modules/capture/capture.h @@ -75,6 +75,7 @@ class CaptureSource : public QObject Q_SIGNALS: void imageReady(); + void imageFailed(); void bufferDestroyed(); void targetDestroyed(); void targetResized(); @@ -101,7 +102,7 @@ class CaptureSource : public QObject * @brief copyBuffer render captured contents to a buffer * @param buffer buffer prepared by client */ - void copyBuffer(wlr_buffer *buffer); + bool copyBuffer(wlr_buffer *buffer); // Cropped area of source virtual QRect cropRect() const = 0; @@ -241,6 +242,7 @@ class CaptureContextV1 : public QObject void onCapture(treeland_capture_frame_v1 *frame); void onCreateSession(treeland_capture_session_v1 *session); void handleFrameCopy(wlr_buffer *buffer); + void failFrame(const char *reason); void handleSessionStart(); void handleFrameDone(uint32_t tvSecHi, uint32_t tvSecLo, uint32_t tvUsec); void handleRenderEnd(); diff --git a/waylib/src/server/qtquick/wtextureproviderprovider.cpp b/waylib/src/server/qtquick/wtextureproviderprovider.cpp index 09e5ce233b..cf06e1ea56 100644 --- a/waylib/src/server/qtquick/wtextureproviderprovider.cpp +++ b/waylib/src/server/qtquick/wtextureproviderprovider.cpp @@ -3,7 +3,10 @@ #include "wtextureproviderprovider.h" #include "woutputrenderwindow.h" +#include "woutputviewport.h" #include "private/wglobal_p.h" +#include "utils/wbufferdumper.h" +#include "wsgtextureprovider.h" #include "wayliblogging.h" #include @@ -61,6 +64,51 @@ void WTextureCapturer::doGrabToImage() d->imgPromise.start(); WSGTextureProvider *textureProvider = d->provider->wTextureProvider(); auto *texture = textureProvider ? textureProvider->texture() : nullptr; + + // On Vulkan the provider's QRhiTexture wraps the wlroots-owned sampled + // VkImage, which is FOREIGN-owned and may lag behind the producer fence. + // A plain QRhi readback would sample it without the queue-family acquire + // (undefined contents, validation failure), so read the backing + // wlr_buffer through WBufferDumper, which runs the wlroots readback + // acquire/release gate. If the provider holds no buffer, fail rather + // than touching the imported image. + QRhi *rhi = d->renderWindow->rhi(); + if (rhi && rhi->backend() == QRhi::Vulkan) { + wlr_buffer *wlrBuffer = textureProvider ? textureProvider->wlrBuffer() : nullptr; + if (!wlrBuffer) { + // The primary output viewport has no cached texture provider on + // Vulkan (its buffer is the current render target), so fall back to + // the last committed output buffer, exactly like the capture module + // does for output sources. WOutputViewport::lastBuffer() is backed + // by a WPointer (null once the buffer is destroyed) and the + // swapchain keeps the buffer alive until the next commit. + if (auto *viewport = dynamic_cast(d->provider)) + wlrBuffer = viewport->lastBuffer(); + } + if (!wlrBuffer) { + d->imgPromise.setException(std::make_exception_ptr( + std::runtime_error("Texture provider is not valid."))); + d->imgPromise.finish(); + return; + } + QImage image; + const auto result = WBufferDumper::dumpBufferToImage( + wlrBuffer, d->renderWindow->renderer(), image); + if (result == WBufferDumper::DumpResult::Success && !image.isNull()) { + d->imgPromise.addResult(image); + } else { + qCWarning(lcWlTextureProvider) + << "Vulkan texture-provider readback failed" + << "buffer" << wlrBuffer + << "result" << WBufferDumper::dumpResultToString(result); + d->imgPromise.setException(std::make_exception_ptr(std::runtime_error( + "Vulkan texture readback failed: " + + WBufferDumper::dumpResultToString(result).toStdString()))); + } + d->imgPromise.finish(); + return; + } + if (texture && texture->rhiTexture()) { // Perform rhi texture read back auto *rhiTexture = texture->rhiTexture(); diff --git a/waylib/src/server/utils/wbufferdumper.cpp b/waylib/src/server/utils/wbufferdumper.cpp index 1a4a1ea603..7300d0d579 100644 --- a/waylib/src/server/utils/wbufferdumper.cpp +++ b/waylib/src/server/utils/wbufferdumper.cpp @@ -9,6 +9,7 @@ #include #include +#include WAYLIB_SERVER_BEGIN_NAMESPACE @@ -21,6 +22,34 @@ WBufferDumper::DumpResult WBufferDumper::dumpBufferToImage(wlr_buffer *buffer, return DumpResult::InvalidBuffer; } + const bool isVulkanRenderer = wlr_renderer_is_vk(renderer); + + // On the Vulkan renderer, buffers with a CPU representation (shared + // memory client buffers, e.g. XWayland) are copied directly. This avoids + // importing them as staging pixel textures, whose device layout + // (SHADER_READ_ONLY_OPTIMAL after upload) upstream's vulkan_read_pixels() + // does not account for. + if (isVulkanRenderer) { + void *data = nullptr; + uint32_t shmFormat = 0; + size_t shmStride = 0; + if (wlr_buffer_begin_data_ptr_access(buffer, WLR_BUFFER_DATA_PTR_ACCESS_READ, + &data, &shmFormat, &shmStride)) { + QImage::Format mappedFormat = WTools::toImageFormat(shmFormat); + if (mappedFormat != QImage::Format_Invalid) { + QImage wrapped(static_cast(data), + buffer->width, buffer->height, + int(shmStride), mappedFormat); + if (!wrapped.isNull()) { + outputImage = wrapped.copy(); + wlr_buffer_end_data_ptr_access(buffer); + return DumpResult::Success; + } + } + wlr_buffer_end_data_ptr_access(buffer); + } + } + WUniquePointer texture(wlr_texture_from_buffer(renderer, buffer)); if (!texture) { qCWarning(lcWlBufferDumper) << "Failed to create texture from buffer" @@ -49,6 +78,32 @@ WBufferDumper::DumpResult WBufferDumper::dumpBufferToImage(wlr_buffer *buffer, uint32_t stride = outputImage.bytesPerLine(); + // On the Vulkan renderer a DMA-BUF import is FOREIGN-owned between + // frames, so reading its pixels requires the wlroots-side acquire gate + // (producer fence wait + queue-family ownership) before touching it, and + // the matching release afterwards. Shared-memory buffers were already + // handled by the CPU copy path above; NOOP covers the remaining cases. + bool readbackAcquired = false; + if (isVulkanRenderer) { + const auto state = waylib_vk_texture_begin_readback(renderer, texture.get()); + switch (state) { + case WLR_VK_TEXTURE_READBACK_ACQUIRED: + readbackAcquired = true; + break; + case WLR_VK_TEXTURE_READBACK_NOOP: + break; + case WLR_VK_TEXTURE_READBACK_ERROR: + qCWarning(lcWlBufferDumper) << "Vulkan texture readback acquire failed" + << "texture" << texture.get() + << "buffer" << buffer; + return DumpResult::ReadbackSyncFailed; + } + } + const auto readbackGuard = qScopeGuard([&texture, renderer, readbackAcquired] { + if (readbackAcquired) + waylib_vk_texture_end_readback(renderer, texture.get()); + }); + wlr_texture_read_pixels_options options = {}; options.data = outputImage.bits(); options.format = format; @@ -100,6 +155,8 @@ QString WBufferDumper::dumpResultToString(DumpResult result) return "Unsupported pixel format"; case DumpResult::SaveFailed: return "Failed to save image file"; + case DumpResult::ReadbackSyncFailed: + return "Failed to acquire Vulkan texture for readback"; default: return "Unknown error"; } diff --git a/waylib/src/server/utils/wbufferdumper.h b/waylib/src/server/utils/wbufferdumper.h index ed62a64bab..f488c3ce91 100644 --- a/waylib/src/server/utils/wbufferdumper.h +++ b/waylib/src/server/utils/wbufferdumper.h @@ -20,7 +20,8 @@ class WAYLIB_SERVER_EXPORT WBufferDumper TextureCreationFailed, TextureReadFailed, UnsupportedFormat, - SaveFailed + SaveFailed, + ReadbackSyncFailed }; static DumpResult dumpBufferToFile(wlr_buffer *buffer, From 4f29e1d64e8610dbf1c2a7b83ce33a7a219f7bd3 Mon Sep 17 00:00:00 2001 From: LFRon Date: Sun, 6 Sep 2026 16:20:45 +0800 Subject: [PATCH 13/15] feat(vulkan): adapt blur effect for the Vulkan renderer Rebuild the effect on RenderBufferBlitter: the blitter snapshots the surrounding canvas into the effect's source texture, and the effect renders it back through Qt Quick Effects (MultiEffect / glass), keeping the GLES2 blur look while the compositor canvas stays a single offscreen QRhi pass. Follow-up hardening (folded): the blitter item is now visible only while the effect is enabled and its content is rendered onscreen, mirroring the content loader's own condition (visible: effectEnabled && !contentOffscreen). Hiding the blitter keeps WBufferRenderer::hasActiveRenderBufferBlitter() from requesting the Vulkan backdrop, so a disabled or offscreen-rendered effect no longer pays for a canvas copy and a render-pass split. --- src/core/qml/Effects/+vulkan/Blur.qml | 109 +++++++++++++++++++++----- 1 file changed, 89 insertions(+), 20 deletions(-) diff --git a/src/core/qml/Effects/+vulkan/Blur.qml b/src/core/qml/Effects/+vulkan/Blur.qml index acd34851da..45e1d9a4cd 100644 --- a/src/core/qml/Effects/+vulkan/Blur.qml +++ b/src/core/qml/Effects/+vulkan/Blur.qml @@ -1,14 +1,17 @@ -// Copyright (C) 2026 UnionTech Software Technology Co., Ltd. +// Copyright (C) 2024-2026 UnionTech Software Technology Co., Ltd. // SPDX-License-Identifier: Apache-2.0 OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only import QtQuick +import QtQuick.Effects +import QtQuick.Shapes +import Waylib.Server import Treeland -Rectangle { - id: root - - color: Qt.rgba(0.0, 0.0, 0.0, 0.0) +RenderBufferBlitter { + id: blitter + smooth: true + property real radius: 0 property bool radiusEnabled: radius > 0 property int blurMax: Helper.config.blurStrength property bool blurEnabled: blurMax > 0 && blurAmount > 0 @@ -20,27 +23,93 @@ Rectangle { property real glassSpecular: Helper.config.glassSpecular property real glassTint: 0.0 property bool glassEnabled: Helper.config.glassEnabled - property Item effectContent: effectContentItem + property Item effectContent: contentLoader property bool contentOffscreen: false property bool effectEnabled: true - readonly property Item content: contentContainer - property alias offscreen: root.contentOffscreen - default property alias data: contentContainer.data - Item { - id: contentContainer + z: parent.z ? parent.z - 1 : -1 + anchors.fill: parent + + // The blitter only exists to feed the effect. When the effect is disabled + // (or its content is rendered offscreen by the caller) there is nothing to + // draw, so hide the blitter entirely: this also stops + // WBufferRenderer::hasActiveRenderBufferBlitter() from requesting the + // Vulkan backdrop and splitting the render pass for a disabled effect. + visible: blitter.effectEnabled && !blitter.contentOffscreen + + // Keep the Vulkan effect implementation behind the Vulkan file selector. + // This preserves the GLES2 Blur component and its lifecycle unchanged. + Loader { + id: contentLoader anchors.fill: parent + sourceComponent: blitter.glassEnabled ? glassComponent : blurComponent + visible: blitter.effectEnabled && !blitter.contentOffscreen } - Rectangle { - id: effectContentItem - anchors.fill: parent - radius: root.radius - color: Qt.rgba(1.0, 1.0, 1.0, 0.15) - visible: root.effectEnabled && !root.contentOffscreen - z: -1 + Component { + id: glassComponent + GlassEffect { + anchors.fill: parent + source: blitter.content + radius: blitter.radius + blurEnabled: blitter.blurEnabled + blurMax: blitter.blurMax + blurAmount: blitter.blurAmount + blurMultiplier: blitter.multiplier + bezelWidth: Helper.config.glassBezel + thickness: Helper.config.glassThickness + ior: 1.33 + specular: blitter.glassSpecular + tint: blitter.glassTint + brightness: blitter.brightness + contrast: blitter.contrast + saturation: blitter.saturation + contentEdgePull: 0.0 + contentRampEnd: 0.0 + refractionMaxTan: 3.3 + profilePower: Helper.config.glassProfilePower + innerShadow: 0.0 + } } - z: parent.z ? parent.z - 1 : -1 - anchors.fill: parent + Component { + id: blurComponent + Item { + anchors.fill: parent + + MultiEffect { + id: blur + anchors.fill: parent + layer.enabled: blitter.radiusEnabled + smooth: blitter.radiusEnabled + opacity: blitter.radiusEnabled ? 0 : blitter.opacity + source: blitter.content + autoPaddingEnabled: false + blurEnabled: blitter.blurEnabled + blur: blitter.blurAmount + blurMax: blitter.blurMax + blurMultiplier: blitter.multiplier + saturation: 0.2 + } + + Loader { + x: blur.x + y: blur.y + active: blitter.radiusEnabled + sourceComponent: Shape { + anchors.fill: parent + preferredRendererType: Shape.CurveRenderer + ShapePath { + strokeWidth: 0 + fillItem: blur + PathRectangle { + width: blur.width + height: blur.height + radius: blitter.radius + } + } + } + } + } + } } From dfea78fd490138eee46241e0e42e17004ffcf164 Mon Sep 17 00:00:00 2001 From: LFRon Date: Mon, 7 Sep 2026 21:15:49 +0800 Subject: [PATCH 14/15] fix(vulkan): restrict client texture formats and wrap 10-bit exactly Clients select buffer formats that the compositor must import, wrap into Qt, and sample. Two gaps let them end up with formats that render as a blank white screen: - Restrict the advertised dmabuf/shm texture format sets to the subset the Qt wrapper displays exactly: waylib_vk_renderer_restrict_texture_formats() rebuilds dev->dmabuf_texture_formats and dev->shm_texture_formats from a DRM format whitelist, and Helper::init applies it before wl_shm and linux-dmabuf hand the sets to clients. Verified against RADV: 17/40 dmabuf and 17/31 shm formats kept - the dropped ones are the 16-bit packed, 3-channel, and YCbCr families QRhi cannot map. - Wrap packed 10-bit A2R10G10B10 buffers (XWayland depth-30 windows, e.g. Command & Conquer: Mental Omega under Wine) exactly instead of rejecting them: Qt maps both 10-bit channel orders to RGB10A2 and its Vulkan backend derives an A2B10G10R10 initial view, which cannot be created on a non-MUTABLE A2R10 image. Construct the QVkTexture manually with the exact wlroots format and the alpha swizzle instead of createFrom(). XWayland selects buffer formats from the X visual depth and does not consult the advertised sets, so the exact wrapping is required even with the restriction in place; both mechanisms share the same goal - no client-visible format may fall outside what the compositor can display. Verified with Mental Omega: 817 XR30/AR30 wraps with zero discards, the format sets restricted, and no other rendering behavior changed. GLES2 is not touched. Folded follow-up placement note: WRenderHelper::resetVulkanFrameAliasLists() definition sits directly after this commit's restrictVulkanTextureFormats() (the call site and header declaration were folded earlier in the render buffer integration commit). Follow-up hardening (folded): both restricted sets are rebuilt into temporaries and committed together, so a failure halfway through cannot leave the dmabuf set restricted while the shm set stays untouched (or vice versa); and the updateVKTexture() comment no longer points at the removed attachment-only depth helper. --- 3rdparty/wlroots/include/wlr/render/vulkan.h | 8 + 3rdparty/wlroots/render/vulkan/renderer.c | 70 ++++++ src/seat/helper.cpp | 7 + waylib/src/server/qtquick/wrenderhelper.cpp | 226 ++++++++++++++----- waylib/src/server/qtquick/wrenderhelper.h | 1 + 5 files changed, 251 insertions(+), 61 deletions(-) diff --git a/3rdparty/wlroots/include/wlr/render/vulkan.h b/3rdparty/wlroots/include/wlr/render/vulkan.h index 95e068d583..49bc807ead 100644 --- a/3rdparty/wlroots/include/wlr/render/vulkan.h +++ b/3rdparty/wlroots/include/wlr/render/vulkan.h @@ -141,4 +141,12 @@ void waylib_vk_texture_end_readback(struct wlr_renderer *renderer, void waylib_vk_renderer_set_stage_async_enabled(struct wlr_renderer *renderer, bool enabled); +// Restrict the renderer's advertised dmabuf and shm texture format sets to +// the given DRM format whitelist, dropping every other format so clients +// cannot allocate buffers the compositor is unable to display. Call once +// after renderer creation and before any client connects. Passing an empty +// whitelist fails without touching the sets. +bool waylib_vk_renderer_restrict_texture_formats(struct wlr_renderer *renderer, + const uint32_t *drm_formats, size_t count); + #endif diff --git a/3rdparty/wlroots/render/vulkan/renderer.c b/3rdparty/wlroots/render/vulkan/renderer.c index c22df3f2dd..1aae4af0be 100644 --- a/3rdparty/wlroots/render/vulkan/renderer.c +++ b/3rdparty/wlroots/render/vulkan/renderer.c @@ -3060,3 +3060,73 @@ bool waylib_vk_renderer_has_separate_depth_stencil_layouts( struct wlr_vk_renderer *vk_renderer = vulkan_get_renderer(renderer); return vk_renderer->dev->separate_depth_stencil_layouts; } + +bool waylib_vk_renderer_restrict_texture_formats(struct wlr_renderer *wlr_renderer, + const uint32_t *drm_formats, size_t count) { + if (wlr_renderer == NULL || !wlr_renderer_is_vk(wlr_renderer) + || drm_formats == NULL || count == 0) { + return false; + } + + struct wlr_vk_renderer *renderer = vulkan_get_renderer(wlr_renderer); + + // The texture format sets are only consulted while advertising the + // formats to clients (linux-dmabuf and wl_shm); rebuilding them in place + // before any client connects cannot race with anything. Build both + // restricted sets first and commit them together: a failure halfway + // through must not leave one set restricted and the other untouched. + struct wlr_drm_format_set *const sets[] = { + &renderer->dev->dmabuf_texture_formats, + &renderer->dev->shm_texture_formats, + }; + const char *const names[] = { "dmabuf", "shm" }; + struct wlr_drm_format_set filtered[2] = {0}; + size_t original_count[2] = {0}; + size_t kept[2] = {0}; + + for (size_t s = 0; s < 2; s++) { + const struct wlr_drm_format_set *set = sets[s]; + original_count[s] = set->len; + for (size_t i = 0; i < original_count[s]; i++) { + const struct wlr_drm_format *format = &set->formats[i]; + bool supported = false; + for (size_t f = 0; f < count; f++) { + if (drm_formats[f] == format->format) { + supported = true; + break; + } + } + if (!supported) { + wlr_log(WLR_DEBUG, "Restricting %s texture format %.4s: " + "the compositor cannot wrap it", + names[s], (const char *)&format->format); + continue; + } + for (size_t m = 0; m < format->len; m++) { + if (!wlr_drm_format_set_add(&filtered[s], format->format, + format->modifiers[m])) { + wlr_log(WLR_ERROR, "Failed to rebuild the restricted " + "%s texture format set", names[s]); + goto error; + } + } + kept[s]++; + } + } + + for (size_t s = 0; s < 2; s++) { + wlr_drm_format_set_finish(sets[s]); + *sets[s] = filtered[s]; + filtered[s] = (struct wlr_drm_format_set){0}; + wlr_log(WLR_INFO, "Restricted the %s texture format set to the " + "compositor-wrappable subset (%zu/%zu formats kept)", + names[s], kept[s], original_count[s]); + } + return true; + +error: + for (size_t s = 0; s < 2; s++) { + wlr_drm_format_set_finish(&filtered[s]); + } + return false; +} diff --git a/src/seat/helper.cpp b/src/seat/helper.cpp index 432ccf7193..e3da9ec8a7 100644 --- a/src/seat/helper.cpp +++ b/src/seat/helper.cpp @@ -2247,6 +2247,13 @@ void Helper::init(Treeland::Treeland *treeland) const bool vulkanRenderer = WRenderHelper::getGraphicsApi() == QSGRendererInterface::Vulkan; if (vulkanRenderer) { + // Restrict the advertised texture formats to the ones the Qt wrapper + // can display before wl_shm and linux-dmabuf hand them to clients. + // XWayland picks buffer formats from the X visual depth instead and + // is covered by the exact view seeding in the texture wrapper. + if (!WRenderHelper::restrictVulkanTextureFormats(m_renderer)) + qCWarning(lcTlCore) << "Continuing with the unfiltered Vulkan texture format set"; + if (!wlr_renderer_init_wl_shm(m_renderer, m_server->handle())) qCFatal(lcTlCore) << "Failed to initialize wl_shm for Vulkan renderer"; diff --git a/waylib/src/server/qtquick/wrenderhelper.cpp b/waylib/src/server/qtquick/wrenderhelper.cpp index 0339c149bd..b0d28ba927 100644 --- a/waylib/src/server/qtquick/wrenderhelper.cpp +++ b/waylib/src/server/qtquick/wrenderhelper.cpp @@ -1949,19 +1949,22 @@ static bool updateVKTexture(QRhi *rhi, wlr_texture *handle, QSGPlainTexture *tex QRhiTexture::Flags mappedFlags {}; const auto mappedFormat = QSGRhiSupport::instance()->toRhiTextureFormat(attribs.format, &mappedFlags); - // Qt maps both packed 10-bit channel orders to QRhi::RGB10A2, but its - // Vulkan backend always creates an A2B10G10R10 view first. An A2R10 image - // without MUTABLE_FORMAT would fail before we can replace that view. - const bool requiresInexactQtView = - attribs.format == VK_FORMAT_A2R10G10B10_UNORM_PACK32; - if (mappedFormat == QRhiTexture::UnknownFormat || requiresInexactQtView) { + // Qt maps both packed 10-bit channel orders (A2R10G10B10/A2B10G10R10) to + // QRhiTexture::RGB10A2, and QVkTexture::finishCreate() derives the initial + // image view from viewFormatForSampling - so an A2R10G10B10 external image + // would get an A2B10G10R10 view, which cannot even be created without + // VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT. Such textures are constructed + // manually below (mirroring the attachment-only depth path) with the exact + // wlroots format instead of going through createFrom. + const bool needsExactFormatSeed = + mappedFormat == QRhiTexture::RGB10A2 + && attribs.format != VK_FORMAT_A2B10G10R10_UNORM_PACK32; + if (mappedFormat == QRhiTexture::UnknownFormat) { qCDebug(lcWlQtQuickTexture) << "Rejected unsupported wlroots Vulkan texture format" << "wlrTexture" << handle << "image" << vkImageName(attribs.image) << "format" << hex32(attribs.format) - << "reason" << (requiresInexactQtView - ? "inexact-qt-view-format" - : "unknown-qrhi-format") + << "reason" << "unknown-qrhi-format" << "size" << size; return false; } @@ -1991,39 +1994,6 @@ static bool updateVKTexture(QRhi *rhi, wlr_texture *handle, QSGPlainTexture *tex // image view. Keep ownership false even on a partial failure so Qt can // never enqueue the wlroots-owned image for destruction. vkTexture->owns = false; - if (!candidate->createFrom({vkimage_cast(attribs.image), qtSampleLayout})) { - qCDebug(lcWlQtQuickTexture) << "Failed to wrap wlroots Vulkan image in QRhiTexture" - << "wlrTexture" << handle - << "image" << vkImageName(attribs.image) - << "format" << hex32(attribs.format) - << "layout" << vkImageLayoutName(qtSampleLayout) - << "size" << size; - return false; - } - - const auto discardUnusedCandidate = [vkTexture, nativeHandles, deviceFunctions] { - if (vkTexture->imageView != VK_NULL_HANDLE) { - deviceFunctions->vkDestroyImageView(nativeHandles->dev, - vkTexture->imageView, - nullptr); - vkTexture->imageView = VK_NULL_HANDLE; - } - // The candidate has never entered a descriptor. Unregister it from - // QRhi after synchronously dropping its unused view, while preserving - // ownership of the external image in wlroots. - vkTexture->destroy(); - }; - - if (vkTexture->imageView == VK_NULL_HANDLE || vkTexture->lastActiveFrameSlot != -1) { - qCDebug(lcWlQtQuickTexture) << "Cannot replace the initial Qt Vulkan texture view" - << "wlrTexture" << handle - << "image" << vkImageName(attribs.image) - << "format" << hex32(attribs.format) - << "hasInitialView" << bool(vkTexture->imageView != VK_NULL_HANDLE) - << "lastActiveFrameSlot" << vkTexture->lastActiveFrameSlot; - discardUnusedCandidate(); - return false; - } const bool hasAlpha = wlr_vk_texture_has_alpha(handle); VkImageViewCreateInfo viewInfo = {}; @@ -2043,26 +2013,101 @@ static bool updateVKTexture(QRhi *rhi, wlr_texture *handle, QSGPlainTexture *tex viewInfo.subresourceRange.levelCount = 1; viewInfo.subresourceRange.layerCount = 1; - VkImageView samplingView = VK_NULL_HANDLE; - const VkResult viewResult = deviceFunctions->vkCreateImageView(nativeHandles->dev, - &viewInfo, - nullptr, - &samplingView); - if (viewResult != VK_SUCCESS) { - qCDebug(lcWlQtQuickTexture) << "Failed to create exact Vulkan sampling view" - << "wlrTexture" << handle - << "image" << vkImageName(attribs.image) - << "format" << hex32(attribs.format) - << "hasAlpha" << hasAlpha - << "vkResult" << viewResult; - discardUnusedCandidate(); - return false; - } + const auto discardUnusedCandidate = [vkTexture, nativeHandles, deviceFunctions] { + if (vkTexture->imageView != VK_NULL_HANDLE) { + deviceFunctions->vkDestroyImageView(nativeHandles->dev, + vkTexture->imageView, + nullptr); + vkTexture->imageView = VK_NULL_HANDLE; + } + // The candidate has never entered a descriptor. Unregister it from + // QRhi after synchronously dropping its unused view, while preserving + // ownership of the external image in wlroots. + vkTexture->destroy(); + }; - const VkImageView initialView = vkTexture->imageView; - vkTexture->imageView = samplingView; - vkTexture->viewFormatForSampling = attribs.format; - deviceFunctions->vkDestroyImageView(nativeHandles->dev, initialView, nullptr); + if (needsExactFormatSeed) { + // Bypass createFrom(): QVkTexture::prepareCreate() would derive the + // view format from the QRhi enum again. Fill the QVkTexture by hand + // with the exact wlroots format, then create the final exact view + // (with the alpha swizzle) directly and register it with QRhi. + vkTexture->image = attribs.image; + vkTexture->usageState = { qtSampleLayout, 0, 0 }; + vkTexture->vkformat = attribs.format; + vkTexture->viewFormat = attribs.format; + vkTexture->viewFormatForSampling = attribs.format; + vkTexture->mipLevelCount = 1; + vkTexture->samples = VK_SAMPLE_COUNT_1_BIT; + vkTexture->generation += 1; + vkTexture->lastActiveFrameSlot = -1; + + VkImageView samplingView = VK_NULL_HANDLE; + const VkResult viewResult = deviceFunctions->vkCreateImageView(nativeHandles->dev, + &viewInfo, + nullptr, + &samplingView); + if (viewResult != VK_SUCCESS) { + qCDebug(lcWlQtQuickTexture) << "Failed to create exact Vulkan sampling view" + << "wlrTexture" << handle + << "image" << vkImageName(attribs.image) + << "format" << hex32(attribs.format) + << "hasAlpha" << hasAlpha + << "vkResult" << viewResult; + discardUnusedCandidate(); + return false; + } + vkTexture->imageView = samplingView; + + auto *rhiImplementation = static_cast( + W_PRIVATE_MEMBER(*rhi, QRhi_d_tag {})); + if (!rhiImplementation) { + discardUnusedCandidate(); + return false; + } + rhiImplementation->registerResource(vkTexture, false); + } else { + if (!candidate->createFrom({vkimage_cast(attribs.image), qtSampleLayout})) { + qCDebug(lcWlQtQuickTexture) << "Failed to wrap wlroots Vulkan image in QRhiTexture" + << "wlrTexture" << handle + << "image" << vkImageName(attribs.image) + << "format" << hex32(attribs.format) + << "layout" << vkImageLayoutName(qtSampleLayout) + << "size" << size; + return false; + } + + if (vkTexture->imageView == VK_NULL_HANDLE || vkTexture->lastActiveFrameSlot != -1) { + qCDebug(lcWlQtQuickTexture) << "Cannot replace the initial Qt Vulkan texture view" + << "wlrTexture" << handle + << "image" << vkImageName(attribs.image) + << "format" << hex32(attribs.format) + << "hasInitialView" << bool(vkTexture->imageView != VK_NULL_HANDLE) + << "lastActiveFrameSlot" << vkTexture->lastActiveFrameSlot; + discardUnusedCandidate(); + return false; + } + + VkImageView samplingView = VK_NULL_HANDLE; + const VkResult viewResult = deviceFunctions->vkCreateImageView(nativeHandles->dev, + &viewInfo, + nullptr, + &samplingView); + if (viewResult != VK_SUCCESS) { + qCDebug(lcWlQtQuickTexture) << "Failed to create exact Vulkan sampling view" + << "wlrTexture" << handle + << "image" << vkImageName(attribs.image) + << "format" << hex32(attribs.format) + << "hasAlpha" << hasAlpha + << "vkResult" << viewResult; + discardUnusedCandidate(); + return false; + } + + const VkImageView initialView = vkTexture->imageView; + vkTexture->imageView = samplingView; + vkTexture->viewFormatForSampling = attribs.format; + deviceFunctions->vkDestroyImageView(nativeHandles->dev, initialView, nullptr); + } const char *bridgeClass = hasAlpha ? "exact-alpha-view" : "exact-opaque-view"; @@ -2304,6 +2349,65 @@ void WRenderHelper::setStageAsyncEnabled(wlr_renderer *renderer, bool enabled) #endif } +bool WRenderHelper::restrictVulkanTextureFormats(wlr_renderer *renderer) +{ +#ifdef ENABLE_VULKAN_RENDER + if (!renderer || !wlr_renderer_is_vk(renderer)) + return true; + + // The DRM formats WRenderHelper::makeTexture() can wrap exactly for the + // Vulkan renderer. Restricting the renderer's advertised texture format + // sets to this subset keeps Wayland-native clients from allocating + // buffers the Qt wrapper would reject. XWayland chooses buffer formats + // from the X visual depth instead and does not consult the advertised + // sets; its depth-30 case is covered by the exact view seeding in + // updateVKTexture(). + static const uint32_t qtWrappableFormats[] = { + DRM_FORMAT_ARGB8888, + DRM_FORMAT_XRGB8888, + DRM_FORMAT_ABGR8888, + DRM_FORMAT_XBGR8888, + DRM_FORMAT_ARGB2101010, + DRM_FORMAT_XRGB2101010, + DRM_FORMAT_ABGR2101010, + DRM_FORMAT_XBGR2101010, + DRM_FORMAT_R8, + DRM_FORMAT_GR88, + DRM_FORMAT_R16F, + DRM_FORMAT_GR1616F, + DRM_FORMAT_R32F, + DRM_FORMAT_GR3232F, + DRM_FORMAT_ABGR16161616F, + DRM_FORMAT_XBGR16161616F, + DRM_FORMAT_ABGR32323232F, + }; + + if (!waylib_vk_renderer_restrict_texture_formats( + renderer, qtWrappableFormats, + sizeof(qtWrappableFormats) / sizeof(qtWrappableFormats[0]))) { + qCWarning(lcWlRenderHelper) + << "Failed to restrict the Vulkan texture format sets to the Qt-wrappable subset;" + << " clients may pick formats the Qt wrapper cannot display"; + return false; + } + return true; +#else + Q_UNUSED(renderer); + return true; +#endif +} + +void WRenderHelper::resetVulkanFrameAliasLists(wlr_renderer *renderer) +{ +#ifdef ENABLE_VULKAN_RENDER + if (!renderer || !wlr_renderer_is_vk(renderer)) + return; + waylib_vk_renderer_reset_frame_alias_lists(renderer); +#else + Q_UNUSED(renderer); +#endif +} + bool WRenderHelper::prepareTextureForSampling(QQuickRenderControl *rc, wlr_renderer *renderer, wlr_texture *texture, diff --git a/waylib/src/server/qtquick/wrenderhelper.h b/waylib/src/server/qtquick/wrenderhelper.h index 91246df8c2..2c63741978 100644 --- a/waylib/src/server/qtquick/wrenderhelper.h +++ b/waylib/src/server/qtquick/wrenderhelper.h @@ -103,6 +103,7 @@ class WAYLIB_SERVER_EXPORT WRenderHelper : public QObject, public WObject static void abortTextureBarrierBatch(wlr_renderer *renderer); static void setStageAsyncEnabled(wlr_renderer *renderer, bool enabled); + static bool restrictVulkanTextureFormats(wlr_renderer *renderer); static void resetVulkanFrameAliasLists(wlr_renderer *renderer); // Register a QRhi cleanup callback that force-releases every // Buffer/Texture/RenderBuffer QRhiResource still owned by the QRhi when From cd929e964beb0c2b1dd94f0e16163bf0b80af43d Mon Sep 17 00:00:00 2001 From: LFRon Date: Fri, 18 Sep 2026 23:50:07 +0800 Subject: [PATCH 15/15] fix(seat): tear down seat-owned resources before their native objects die Two Wayland-session shutdown crashes, both in the compositors' seat teardown cascade (observed as Waydroid / dde-lock coredumps): 1. treeland_keyboard_state_notify_v1 attached a WScopedListener to the seat keyboard-groups wlr_keyboard events.modifiers signal, but never detached it: WServerInterface::destroy() only removed the global, and SeatsManager emitted seatRemoved after server->detach() had already run WSeat::destroy. wlroots wlr_keyboard_finish() hard-asserts an empty listener list, so every session exit with the module attached aborted with SIGABRT (wlr_keyboard.c:181). destroy()/~private now tear down the listeners and the SeatManager connections (restart-safe re-setup stays idempotent via the seat-keyed dedup in connectKeyboardGroup), and all three SeatsManager removal paths (removeSeat, loadConfig, destructor) emit seatRemoved before detach, so native listeners are unlinked while the keyboard is alive. 2. WSeat wrappers are registered as server interfaces and ~WServer qDeleteAll()s that list, while SeatsManager kept raw pointers and deleted the same objects in its own destructor: whichever cascade ran second jumped through a freed WSeat vtable (SIGSEGV inside ~SeatsManager). createSeat now hooks QObject::destroyed to prune m_seats/m_defaultSeat (the hook only ever compares pointer values), and ~SeatsManager disconnects the hooks and emits seatRemoved up front, so both destruction orders are safe. --- .../keyboardstatenotifymanagerinterfacev1.cpp | 35 ++++++++++++- src/seat/seatmanager.cpp | 52 ++++++++++++++++++- src/seat/seatmanager.h | 6 +++ 3 files changed, 89 insertions(+), 4 deletions(-) diff --git a/src/modules/keyboard-state-notify/keyboardstatenotifymanagerinterfacev1.cpp b/src/modules/keyboard-state-notify/keyboardstatenotifymanagerinterfacev1.cpp index 78d8398df1..b716080274 100644 --- a/src/modules/keyboard-state-notify/keyboardstatenotifymanagerinterfacev1.cpp +++ b/src/modules/keyboard-state-notify/keyboardstatenotifymanagerinterfacev1.cpp @@ -92,10 +92,12 @@ class TreelandKeyboardStateNotifyManagerInterfaceV1Private { public: explicit TreelandKeyboardStateNotifyManagerInterfaceV1Private(TreelandKeyboardStateNotifyManagerInterfaceV1 *_q); + ~TreelandKeyboardStateNotifyManagerInterfaceV1Private() override; TreelandKeyboardStateNotifyManagerInterfaceV1 *q = nullptr; void setupKeyboardConnections(); + void teardownKeyboardConnections(); void onModifiersEvent(WSeat *seat); wl_global *global() const; @@ -112,6 +114,7 @@ class TreelandKeyboardStateNotifyManagerInterfaceV1Private void connectKeyboardGroup(WSeat *seat, WInputDevice *keyboardDevice); QList> m_keyboardConnections; + QList m_seatManagerSignals; bool m_keyboardConnectionsSetup = false; }; @@ -121,6 +124,29 @@ TreelandKeyboardStateNotifyManagerInterfaceV1Private::TreelandKeyboardStateNotif { } +TreelandKeyboardStateNotifyManagerInterfaceV1Private::~TreelandKeyboardStateNotifyManagerInterfaceV1Private() +{ + teardownKeyboardConnections(); +} + +// The modifiers listeners registered through connectKeyboardGroup() are +// native wl_list listeners on the seat's keyboard-group wlr_keyboard, an +// object this module does not own. wlr_keyboard_finish() hard-asserts that +// its events.modifiers listener list is empty, so the listeners must be +// detached while the seat is still alive: before WServer::stop() reaches +// WSeat::destroy() in the interface teardown loop, and before any dynamic +// seat detach/delete. Also reset the setup state so a server restart can +// re-register everything on the freshly created keyboard group. +void TreelandKeyboardStateNotifyManagerInterfaceV1Private::teardownKeyboardConnections() +{ + m_keyboardConnections.clear(); + s_lastModifiers.clear(); + for (const auto &connection : std::as_const(m_seatManagerSignals)) + QObject::disconnect(connection); + m_seatManagerSignals.clear(); + m_keyboardConnectionsSetup = false; +} + wl_global *TreelandKeyboardStateNotifyManagerInterfaceV1Private::global() const { return m_global; @@ -165,10 +191,10 @@ void TreelandKeyboardStateNotifyManagerInterfaceV1Private::setupKeyboardConnecti handleSeatAdded(seat); } - QObject::connect(helper->seatManager(), &SeatManager::seatAdded, q, [this](WSeat *seat) { + m_seatManagerSignals << QObject::connect(helper->seatManager(), &SeatManager::seatAdded, q, [this](WSeat *seat) { handleSeatAdded(seat); }); - QObject::connect(helper->seatManager(), &SeatManager::seatRemoved, q, [this](WSeat *seat) { + m_seatManagerSignals << QObject::connect(helper->seatManager(), &SeatManager::seatRemoved, q, [this](WSeat *seat) { handleSeatDestroy(seat); }); } @@ -393,6 +419,11 @@ void TreelandKeyboardStateNotifyManagerInterfaceV1::create(WServer *server) void TreelandKeyboardStateNotifyManagerInterfaceV1::destroy([[maybe_unused]] WServer *server) { d->globalRemove(); + // WServer::stop() destroys interfaces in reverse attach order: this + // module is torn down before WSeat, so detaching here still finds the + // group keyboard alive and wlr_keyboard_finish() sees an empty list. + // The interface object itself outlives the keyboard otherwise. + d->teardownKeyboardConnections(); } wl_global *TreelandKeyboardStateNotifyManagerInterfaceV1::global() const diff --git a/src/seat/seatmanager.cpp b/src/seat/seatmanager.cpp index d97be2516e..d906539319 100644 --- a/src/seat/seatmanager.cpp +++ b/src/seat/seatmanager.cpp @@ -28,12 +28,20 @@ SeatManager::SeatManager(WServer *server, QObject *parent) SeatManager::~SeatManager() { + // Prune hooks first: from here on any seat we delete ourselves must not + // run the destroyed hook against members that are being torn down. + for (auto connection : std::as_const(m_seatCleanupHooks)) + QObject::disconnect(connection); + m_seatCleanupHooks.clear(); + QMap seatsToDelete; seatsToDelete.swap(m_seats); m_deviceRules.clear(); m_defaultSeat = nullptr; for (auto seat : std::as_const(seatsToDelete)) { + // See removeSeat(): emit while the seat natives are still alive. + Q_EMIT seatRemoved(seat); if (m_server) { m_server->detach(seat); } @@ -54,6 +62,29 @@ WSeat *SeatManager::createSeat(const QString &name, bool isFallback) m_defaultSeat = seat; } + // A WSeat registered as server interface is owned by WServer too + // (~WServer qDeleteAll(interfaceList)); if the server dies first, this + // manager must not keep or touch the dangling wrapper. ~QObject's + // destroyed signal fires while the manager is still fully alive in that + // order (~WServer runs as a sibling before ~SeatManager's member + // teardown), so pruning from the hook is safe there; ~SeatManager + // disconnects the hooks up front so they can never fire mid-destruction. + m_seatCleanupHooks.insert( + seat, + connect(seat, &QObject::destroyed, this, [this, seat] { + unhookSeat(seat); + const auto names = m_seats.keys(); + for (const auto &seatName : names) { + if (m_seats.value(seatName) == seat) { + m_seats.remove(seatName); + } + } + if (m_defaultSeat == seat) { + m_defaultSeat = m_seats.isEmpty() ? nullptr : m_seats.begin().value(); + qCDebug(lcTlSeat) << "Default seat reset after external seat destruction"; + } + })); + qCDebug(lcTlSeat) << "Created seat:" << name << "fallback:" << isFallback; auto announce = [this, seat, name] { @@ -89,6 +120,15 @@ WSeat *SeatManager::createSeat(const QString &name, bool isFallback) return seat; } +void SeatManager::unhookSeat(WSeat *seat) +{ + auto it = m_seatCleanupHooks.find(seat); + if (it == m_seatCleanupHooks.end()) + return; + QObject::disconnect(it.value()); + m_seatCleanupHooks.erase(it); +} + void SeatManager::removeSeat(const QString &name) { if (!m_seats.contains(name)) { @@ -122,11 +162,17 @@ void SeatManager::removeSeat(const QString &name) } } + // Notify listeners BEFORE detaching: interface objects tear down native + // resources (e.g. wl_signal listeners on this seat's keyboard group) from + // the seatRemoved handlers, and wlr_keyboard_finish() hard-asserts if + // anything is still attached once the keyboard group starts dying. + Q_EMIT seatRemoved(seat); + unhookSeat(seat); + if (m_server) { m_server->detach(seat); } - Q_EMIT seatRemoved(seat); qCDebug(lcTlSeat) << "Removed seat:" << name; delete seat; @@ -321,10 +367,12 @@ void SeatManager::loadConfig(const QJsonObject &config) m_defaultSeat = nullptr; for (auto seat : std::as_const(seatsToDelete)) { + // See removeSeat(): emit while the seat natives are still alive. + Q_EMIT seatRemoved(seat); + unhookSeat(seat); if (m_server) { m_server->detach(seat); } - Q_EMIT seatRemoved(seat); delete seat; } diff --git a/src/seat/seatmanager.h b/src/seat/seatmanager.h index 97ae537161..b700a08c73 100644 --- a/src/seat/seatmanager.h +++ b/src/seat/seatmanager.h @@ -106,5 +106,11 @@ class SeatManager : public QObject QMap m_seats; QMap> m_deviceRules; WSeat *m_defaultSeat = nullptr; + // Per-seat QObject::destroyed hooks that prune m_seats/m_defaultSeat when + // WServer (~WServer's qDeleteAll of interfaceList) deletes a WSeat before + // this manager does. Cleared up-front in ~SeatManager so the hook body + // never touches manager members during self-destruction. + QHash m_seatCleanupHooks; + void unhookSeat(WSeat *seat); mutable QMap m_deviceCache; };