diff --git a/3rdparty/wlroots/include/render/vulkan.h b/3rdparty/wlroots/include/render/vulkan.h index 5bcf001f40..fdddd296b5 100644 --- a/3rdparty/wlroots/include/render/vulkan.h +++ b/3rdparty/wlroots/include/render/vulkan.h @@ -43,6 +43,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; }; @@ -208,6 +210,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. // This requires that the image support an _SRGB VkFormat, and does @@ -260,6 +263,13 @@ 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; +}; + // Vulkan wlr_renderer implementation on top of a wlr_vk_device. struct wlr_vk_renderer { struct wlr_renderer wlr_renderer; @@ -296,6 +306,39 @@ 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 + bool texture_barrier_batch_active; + bool texture_barrier_batch_release; // current batch is the release phase + + // 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 @@ -370,6 +413,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; @@ -476,7 +526,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..208138585f 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); @@ -30,6 +31,9 @@ VkInstance wlr_vk_renderer_get_instance(struct wlr_renderer *renderer); VkPhysicalDevice wlr_vk_renderer_get_physical_device(struct wlr_renderer *renderer); VkDevice wlr_vk_renderer_get_device(struct wlr_renderer *renderer); uint32_t wlr_vk_renderer_get_queue_family(struct wlr_renderer *renderer); +VkQueue wlr_vk_renderer_get_queue(struct wlr_renderer *renderer); +bool wlr_vk_renderer_has_separate_depth_stencil_layouts( + struct wlr_renderer *renderer); bool wlr_renderer_is_vk(struct wlr_renderer *wlr_renderer); bool wlr_texture_is_vk(struct wlr_texture *texture); @@ -38,15 +42,59 @@ void wlr_vk_texture_get_image_attribs(struct wlr_texture *texture, struct wlr_vk_image_attribs *attribs); bool wlr_vk_texture_has_alpha(struct wlr_texture *texture); -/* waylib extensions (not upstream): Qt Quick RHI bridge helpers. - * Reuse wlroots' own wlr_vk_render_buffer (same path as tinywl/scene), - * instead of creating a parallel VkImage import of the scanout dmabuf. */ +bool wlr_vk_renderer_prepare_texture_for_sampling(struct wlr_renderer *renderer, + struct wlr_texture *texture, VkCommandBuffer cb, + struct wlr_vk_image_attribs *attribs); +bool wlr_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 wlr_vk_renderer_begin_texture_sync_batch(struct wlr_renderer *renderer); +bool wlr_vk_renderer_flush_texture_sync_batch(struct wlr_renderer *renderer); +void wlr_vk_renderer_abort_texture_sync_batch(struct wlr_renderer *renderer); + +// Batch the per-texture queue-family-ownership/layout barriers that +// wlr_vk_renderer_prepare_texture_for_sampling() and +// wlr_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 wlr_vk_renderer_begin_texture_barrier_batch(struct wlr_renderer *renderer, + bool release); +bool wlr_vk_renderer_flush_texture_barrier_batch(struct wlr_renderer *renderer, + VkCommandBuffer cb); +void wlr_vk_renderer_abort_texture_barrier_batch(struct wlr_renderer *renderer); + +// 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 wlr_vk_renderer_set_stage_async_enabled(struct wlr_renderer *renderer, + bool enabled); + +bool wlr_vk_renderer_get_render_buffer_attribs(struct wlr_renderer *renderer, + struct wlr_buffer *buffer, struct wlr_vk_image_attribs *attribs); +bool wlr_vk_renderer_record_render_buffer_acquire(struct wlr_renderer *renderer, + struct wlr_buffer *buffer, VkCommandBuffer cb); +bool wlr_vk_renderer_record_render_buffer_release(struct wlr_renderer *renderer, + struct wlr_buffer *buffer, VkCommandBuffer cb, VkImageLayout old_layout); + +/* Compatibility names used by the mainline Qt Quick bridge. New waylib code + * should call the wlr_vk_renderer_* variants above directly. */ bool waylib_vk_renderer_get_render_buffer_attribs(struct wlr_renderer *renderer, struct wlr_buffer *buffer, struct wlr_vk_image_attribs *attribs); bool waylib_vk_renderer_record_render_buffer_acquire(struct wlr_renderer *renderer, struct wlr_buffer *buffer, VkCommandBuffer cb); bool waylib_vk_renderer_record_render_buffer_release(struct wlr_renderer *renderer, struct wlr_buffer *buffer, VkCommandBuffer cb, VkImageLayout old_layout); -bool waylib_vk_renderer_flush_stage(struct wlr_renderer *renderer); - #endif diff --git a/3rdparty/wlroots/render/vulkan/pixel_format.c b/3rdparty/wlroots/render/vulkan/pixel_format.c index 9319d0daa7..541dc3e497 100644 --- a/3rdparty/wlroots/render/vulkan/pixel_format.c +++ b/3rdparty/wlroots/render/vulkan/pixel_format.c @@ -428,6 +428,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 802642f3c8..7f127c188c 100644 --- a/3rdparty/wlroots/render/vulkan/renderer.c +++ b/3rdparty/wlroots/render/vulkan/renderer.c @@ -413,15 +413,78 @@ 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) { - if (wlr_renderer == NULL || !wlr_renderer_is_vk(wlr_renderer)) { - return false; - } - struct wlr_vk_renderer *renderer = vulkan_get_renderer(wlr_renderer); +bool vulkan_submit_stage_async(struct wlr_vk_renderer *renderer) { if (renderer->stage.cb == NULL) { return true; } - return vulkan_submit_stage_wait(renderer); + + 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) { + 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); + 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 wlr_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( @@ -489,6 +552,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; @@ -496,6 +560,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) { @@ -911,7 +979,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; } @@ -970,123 +1039,21 @@ static struct wlr_vk_render_buffer *get_or_create_render_buffer( bool waylib_vk_renderer_get_render_buffer_attribs(struct wlr_renderer *wlr_renderer, struct wlr_buffer *wlr_buffer, struct wlr_vk_image_attribs *attribs) { - if (wlr_renderer == NULL || wlr_buffer == NULL || attribs == NULL) { - return false; - } - if (!wlr_renderer_is_vk(wlr_renderer)) { - return false; - } - - struct wlr_vk_renderer *renderer = vulkan_get_renderer(wlr_renderer); - struct wlr_vk_render_buffer *render_buffer = - get_or_create_render_buffer(renderer, wlr_buffer); - if (render_buffer == NULL) { - return false; - } - - struct wlr_dmabuf_attributes dmabuf = {0}; - if (!wlr_buffer_get_dmabuf(wlr_buffer, &dmabuf)) { - wlr_log(WLR_ERROR, "wlr_buffer_get_dmabuf() failed"); - return false; - } - - const struct wlr_vk_format *format = vulkan_get_format_from_drm(dmabuf.format); - if (format == NULL) { - wlr_log(WLR_ERROR, "Unsupported pixel format %"PRIx32 " (%.4s)", - dmabuf.format, (const char *)&dmabuf.format); - return false; - } - - attribs->image = render_buffer->image; - attribs->layout = VK_IMAGE_LAYOUT_GENERAL; - attribs->format = format->vk; - return true; + return wlr_vk_renderer_get_render_buffer_attribs( + wlr_renderer, wlr_buffer, attribs); } bool waylib_vk_renderer_record_render_buffer_acquire(struct wlr_renderer *wlr_renderer, struct wlr_buffer *wlr_buffer, VkCommandBuffer cb) { - if (wlr_renderer == NULL || wlr_buffer == NULL || cb == VK_NULL_HANDLE) { - return false; - } - if (!wlr_renderer_is_vk(wlr_renderer)) { - return false; - } - - struct wlr_vk_renderer *renderer = vulkan_get_renderer(wlr_renderer); - struct wlr_vk_render_buffer *render_buffer = - get_or_create_render_buffer(renderer, wlr_buffer); - if (render_buffer == NULL) { - return false; - } - - /* Match wlroots pass.c / tinywl: foreign ownership transfer into the - * graphics queue before writing the scanout image. */ - VkImageLayout src_layout = VK_IMAGE_LAYOUT_GENERAL; - if (!render_buffer->srgb.transitioned && !render_buffer->plain.transitioned) { - src_layout = VK_IMAGE_LAYOUT_PREINITIALIZED; - } - // The sRGB and plain pathways share the imported VkImage. Once this - // acquire barrier runs, both pathways observe GENERAL on subsequent use. - render_buffer->srgb.transitioned = true; - render_buffer->plain.transitioned = true; - - VkImageMemoryBarrier barrier = { - .sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER, - .srcQueueFamilyIndex = VK_QUEUE_FAMILY_FOREIGN_EXT, - .dstQueueFamilyIndex = renderer->dev->queue_family, - .image = render_buffer->image, - .oldLayout = src_layout, - .newLayout = VK_IMAGE_LAYOUT_GENERAL, - .srcAccessMask = 0, - .dstAccessMask = VK_ACCESS_COLOR_ATTACHMENT_READ_BIT | - VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT, - .subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT, - .subresourceRange.layerCount = 1, - .subresourceRange.levelCount = 1, - }; - - vkCmdPipelineBarrier(cb, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, - VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT, - 0, 0, NULL, 0, NULL, 1, &barrier); - return true; + return wlr_vk_renderer_record_render_buffer_acquire( + wlr_renderer, wlr_buffer, cb); } bool waylib_vk_renderer_record_render_buffer_release(struct wlr_renderer *wlr_renderer, struct wlr_buffer *wlr_buffer, VkCommandBuffer cb, VkImageLayout old_layout) { - if (wlr_renderer == NULL || wlr_buffer == NULL || cb == VK_NULL_HANDLE) { - return false; - } - if (!wlr_renderer_is_vk(wlr_renderer)) { - return false; - } - - struct wlr_vk_renderer *renderer = vulkan_get_renderer(wlr_renderer); - struct wlr_vk_render_buffer *render_buffer = - get_render_buffer(renderer, wlr_buffer); - if (render_buffer == NULL) { - return false; - } - - VkImageMemoryBarrier barrier = { - .sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER, - .srcQueueFamilyIndex = renderer->dev->queue_family, - .dstQueueFamilyIndex = VK_QUEUE_FAMILY_FOREIGN_EXT, - .image = render_buffer->image, - .oldLayout = old_layout, - .newLayout = VK_IMAGE_LAYOUT_GENERAL, - .srcAccessMask = VK_ACCESS_COLOR_ATTACHMENT_READ_BIT | - VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT, - .dstAccessMask = 0, - .subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT, - .subresourceRange.layerCount = 1, - .subresourceRange.levelCount = 1, - }; - - vkCmdPipelineBarrier(cb, VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT, - VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT, - 0, 0, NULL, 0, NULL, 1, &barrier); - return true; + return wlr_vk_renderer_record_render_buffer_release( + wlr_renderer, wlr_buffer, cb, old_layout); } static bool buffer_export_sync_file(struct wlr_vk_renderer *renderer, struct wlr_buffer *buffer, @@ -1238,6 +1205,9 @@ static void vulkan_destroy(struct wlr_renderer *wlr_renderer) { return; } + wlr_vk_renderer_abort_texture_sync_batch(wlr_renderer); + wlr_vk_renderer_abort_texture_barrier_batch(wlr_renderer); + VkResult res = vkDeviceWaitIdle(renderer->dev->dev); if (res != VK_SUCCESS) { wlr_vk_error("vkDeviceWaitIdle", res); @@ -1318,6 +1288,23 @@ 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); + + 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); @@ -2427,7 +2414,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, @@ -2518,7 +2506,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, @@ -2619,6 +2608,13 @@ 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); + renderer->texture_sync_force_poll = getenv("WLR_VK_FORCE_SYNC_POLL") != NULL; + renderer->stage_force_block = getenv("WLR_VK_FORCE_STAGE_BLOCK") != NULL; uint64_t cap_syncobj_timeline; if (dev->drm_fd >= 0 && drmGetCap(dev->drm_fd, DRM_CAP_SYNCOBJ_TIMELINE, &cap_syncobj_timeline) == 0) { @@ -2657,6 +2653,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: @@ -2664,6 +2680,124 @@ struct wlr_renderer *vulkan_renderer_create_for_device(struct wlr_vk_device *dev return NULL; } +bool wlr_vk_renderer_get_render_buffer_attribs(struct wlr_renderer *wlr_renderer, + struct wlr_buffer *wlr_buffer, struct wlr_vk_image_attribs *attribs) { + if (wlr_renderer == NULL || wlr_buffer == NULL || attribs == NULL) { + return false; + } + if (!wlr_renderer_is_vk(wlr_renderer)) { + return false; + } + + struct wlr_vk_renderer *renderer = vulkan_get_renderer(wlr_renderer); + struct wlr_vk_render_buffer *render_buffer = + get_or_create_render_buffer(renderer, wlr_buffer); + if (render_buffer == NULL) { + return false; + } + + struct wlr_dmabuf_attributes dmabuf = {0}; + if (!wlr_buffer_get_dmabuf(wlr_buffer, &dmabuf)) { + wlr_log(WLR_ERROR, "wlr_buffer_get_dmabuf failed"); + return false; + } + + const struct wlr_vk_format *format = vulkan_get_format_from_drm(dmabuf.format); + if (format == NULL) { + wlr_log(WLR_ERROR, "Unsupported pixel format %"PRIx32 " (%.4s)", + dmabuf.format, (const char *)&dmabuf.format); + return false; + } + + attribs->image = render_buffer->image; + attribs->layout = VK_IMAGE_LAYOUT_GENERAL; + attribs->format = format->vk; + attribs->usage = render_buffer->image_usage; + return true; +} + +bool wlr_vk_renderer_record_render_buffer_acquire(struct wlr_renderer *wlr_renderer, + struct wlr_buffer *wlr_buffer, VkCommandBuffer cb) { + if (wlr_renderer == NULL || wlr_buffer == NULL || cb == VK_NULL_HANDLE) { + return false; + } + if (!wlr_renderer_is_vk(wlr_renderer)) { + return false; + } + + struct wlr_vk_renderer *renderer = vulkan_get_renderer(wlr_renderer); + struct wlr_vk_render_buffer *render_buffer = + get_or_create_render_buffer(renderer, wlr_buffer); + if (render_buffer == NULL) { + return false; + } + + VkImageLayout src_layout = VK_IMAGE_LAYOUT_GENERAL; + if (!render_buffer->srgb.transitioned || !render_buffer->plain.transitioned) { + src_layout = VK_IMAGE_LAYOUT_PREINITIALIZED; + render_buffer->srgb.transitioned = true; + render_buffer->plain.transitioned = true; + } + + VkImageMemoryBarrier barrier = { + .sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER, + .srcQueueFamilyIndex = VK_QUEUE_FAMILY_FOREIGN_EXT, + .dstQueueFamilyIndex = renderer->dev->queue_family, + .image = render_buffer->image, + .oldLayout = src_layout, + .newLayout = VK_IMAGE_LAYOUT_GENERAL, + .srcAccessMask = 0, + .dstAccessMask = VK_ACCESS_COLOR_ATTACHMENT_READ_BIT | + VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT, + .subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT, + .subresourceRange.layerCount = 1, + .subresourceRange.levelCount = 1, + }; + + vkCmdPipelineBarrier(cb, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, + VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT, + 0, 0, NULL, 0, NULL, 1, &barrier); + return true; +} + +bool wlr_vk_renderer_record_render_buffer_release(struct wlr_renderer *wlr_renderer, + struct wlr_buffer *wlr_buffer, VkCommandBuffer cb, + VkImageLayout old_layout) { + if (wlr_renderer == NULL || wlr_buffer == NULL || cb == VK_NULL_HANDLE) { + return false; + } + if (!wlr_renderer_is_vk(wlr_renderer)) { + return false; + } + + struct wlr_vk_renderer *renderer = vulkan_get_renderer(wlr_renderer); + struct wlr_vk_render_buffer *render_buffer = + get_render_buffer(renderer, wlr_buffer); + if (render_buffer == NULL) { + return false; + } + + VkImageMemoryBarrier barrier = { + .sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER, + .srcQueueFamilyIndex = renderer->dev->queue_family, + .dstQueueFamilyIndex = VK_QUEUE_FAMILY_FOREIGN_EXT, + .image = render_buffer->image, + .oldLayout = old_layout, + .newLayout = VK_IMAGE_LAYOUT_GENERAL, + .srcAccessMask = VK_ACCESS_COLOR_ATTACHMENT_READ_BIT | + VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT, + .dstAccessMask = 0, + .subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT, + .subresourceRange.layerCount = 1, + .subresourceRange.levelCount = 1, + }; + + vkCmdPipelineBarrier(cb, VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT, + VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT, + 0, 0, NULL, 0, NULL, 1, &barrier); + return true; +} + struct wlr_renderer *wlr_vk_renderer_create_with_drm_fd(int drm_fd) { wlr_log(WLR_INFO, "The vulkan renderer is only experimental and " "not expected to be ready for daily use"); @@ -2716,3 +2850,18 @@ 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 wlr_vk_renderer_get_queue(struct wlr_renderer *renderer) { + struct wlr_vk_renderer *vk_renderer = vulkan_get_renderer(renderer); + return vk_renderer->dev->queue; +} + +bool wlr_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/texture.c b/3rdparty/wlroots/render/vulkan/texture.c index 61a6040eec..75c4cc3f03 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; @@ -502,10 +506,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); @@ -545,6 +553,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 @@ -566,7 +582,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; @@ -617,6 +633,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}; @@ -750,7 +769,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; } @@ -830,9 +849,765 @@ void wlr_vk_texture_get_image_attribs(struct wlr_texture *texture, attribs->format = vk_texture->format->vk; attribs->layout = 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 wlr_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 wlr_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 wlr_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"); + wlr_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"); + wlr_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) { + 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; + } +} + +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 wlr_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) { + wlr_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); + wlr_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); + wlr_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); + wlr_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); + wlr_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); + wlr_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); + wlr_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); + bool cpu_ok = wait_texture_sync_entries(&waits[imported_count], + wait_count - imported_count); + wlr_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); + wlr_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; +} + +bool wlr_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->owned) { + 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; + } + } + + 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)); + if (slot != NULL) { + *slot = barrier; + deferred = true; + } else { + wlr_log_errno(WLR_ERROR, + "Failed to queue Vulkan texture acquire barrier"); + } + } + if (!deferred) { + vkCmdPipelineBarrier(cb, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, + VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, 0, + 0, NULL, 0, NULL, 1, &barrier); + } + + texture->owned = true; + } + + texture->transitioned = true; + fill_sampling_attribs(texture, attribs); + return true; +} + +bool wlr_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->owned) { + 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)); + if (slot != NULL) { + *slot = barrier; + deferred = true; + } else { + wlr_log_errno(WLR_ERROR, + "Failed to queue Vulkan texture release barrier"); + } + } + if (!deferred) { + vkCmdPipelineBarrier(cb, VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, + VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT, 0, + 0, NULL, 0, NULL, 1, &barrier); + } + texture->owned = false; + return true; +} + +bool wlr_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"); + wlr_vk_renderer_abort_texture_barrier_batch(wlr_renderer); + } + + renderer->texture_barrier_batch_active = true; + renderer->texture_barrier_batch_release = release; + return true; +} + +bool wlr_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; + } + + 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) { + VkPipelineStageFlags src_stage = release + ? VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT + : VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT; + VkPipelineStageFlags dst_stage = release + ? VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT + : VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT; + vkCmdPipelineBarrier(cb, src_stage, dst_stage, 0, + 0, NULL, 0, NULL, (uint32_t)count, barriers->data); + } + + barriers->size = 0; + renderer->texture_barrier_batch_active = false; + renderer->texture_barrier_batch_release = false; + return true; +} + +void wlr_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); + renderer->texture_acquire_barriers.size = 0; + renderer->texture_release_barriers.size = 0; + renderer->texture_barrier_batch_active = false; + renderer->texture_barrier_batch_release = false; +} diff --git a/3rdparty/wlroots/render/vulkan/vulkan.c b/3rdparty/wlroots/render/vulkan/vulkan.c index 78bc259419..dcc9dd3a43 100644 --- a/3rdparty/wlroots/render/vulkan/vulkan.c +++ b/3rdparty/wlroots/render/vulkan/vulkan.c @@ -546,15 +546,40 @@ 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_CREATE_RENDERPASS_2_EXTENSION_NAME; + 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"); const float prio = 1.f; VkDeviceQueueCreateInfo qinfo = { @@ -585,11 +610,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, diff --git a/CMakeLists.txt b/CMakeLists.txt index 7bb1673c11..42afe8d5a3 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -64,10 +64,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/qml/Effects/+vulkan/Blur.qml b/src/core/qml/Effects/+vulkan/Blur.qml index acd34851da..8c07555061 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,86 @@ 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 + + // 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 + } + } + } + } + } + } } diff --git a/src/core/qml/PrimaryOutput.qml b/src/core/qml/PrimaryOutput.qml index c8ce8f9f55..2eb75b3780 100644 --- a/src/core/qml/PrimaryOutput.qml +++ b/src/core/qml/PrimaryOutput.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/src/core/treeland.cpp b/src/core/treeland.cpp index f642fd3e74..2233ec21b1 100644 --- a/src/core/treeland.cpp +++ b/src/core/treeland.cpp @@ -1,4 +1,4 @@ -// Copyright (C) 2023 Dingyuan Zhang . +// Copyright (C) 2023-2026 Dingyuan Zhang . // SPDX-License-Identifier: Apache-2.0 OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only #include "treeland.h" @@ -20,6 +20,7 @@ #endif #include +#include #include #include @@ -31,9 +32,11 @@ #include #include #include +#include #include #include +#include #include #include #include @@ -72,6 +75,17 @@ class TreelandPrivate : public QObject connect(qmlEngine, &QQmlEngine::quit, q, &Treeland::quit, Qt::QueuedConnection); 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); + }); + } qputenv("WLR_XWAYLAND", QByteArray(LIBEXEC_DIR) + "/treeland-xwayland"); helper->init(q); diff --git a/src/main.cpp b/src/main.cpp index 41d112a3b9..d6acf7e740 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -3,6 +3,7 @@ #include #include "core/treeland.h" +#include "common/treelandlogging.h" #include "utils/cmdline.h" #include @@ -12,9 +13,11 @@ #include #include +#include #include #include #include +#include #include #if QT_VERSION >= QT_VERSION_CHECK(6, 10, 0) @@ -47,6 +50,8 @@ class QDeepinTheme : public QGenericUnixTheme int main(int argc, char *argv[]) { WLog::init(); + const QByteArray wlrRenderer = qgetenv("WLR_RENDERER"); + const bool explicitVulkanRenderer = wlrRenderer == "vulkan"; DTK_GUI_NAMESPACE::DGuiApplicationHelper::setAttribute( DTK_GUI_NAMESPACE::DGuiApplicationHelper::DontSaveApplicationTheme, true); @@ -55,7 +60,11 @@ int main(int argc, char *argv[]) }); QQuickStyle::setStyle("Chameleon"); - QGuiApplication::setAttribute(Qt::AA_UseOpenGLES); + if (explicitVulkanRenderer) + QQuickWindow::setGraphicsApi(QSGRendererInterface::Vulkan); + else + QGuiApplication::setAttribute(Qt::AA_UseOpenGLES); + QGuiApplication::setHighDpiScaleFactorRoundingPolicy( Qt::HighDpiScaleFactorRoundingPolicy::PassThrough); QGuiApplication::setQuitOnLastWindowClosed(false); @@ -77,6 +86,9 @@ int main(int argc, char *argv[]) } #endif DLogManager::registerJournalAppender(); + if (explicitVulkanRenderer) { + qCInfo(lcTlCore) << "Explicit Vulkan renderer requested; Qt::AA_UseOpenGLES was not set"; + } WRenderHelper::setupRendererBackend(); if (CmdLine::ref().tryExec()) diff --git a/src/modules/capture/capture.cpp b/src/modules/capture/capture.cpp index 5410733707..abf475b0ec 100644 --- a/src/modules/capture/capture.cpp +++ b/src/modules/capture/capture.cpp @@ -20,13 +20,17 @@ #include #include #include +#include #include #include #include #include #include +#include +#include +#include #include static inline QRectF scaledRect(const QRectF &rect, qreal devicePixelRatio) @@ -136,11 +140,26 @@ void CaptureContextV1::onCapture(treeland_capture_frame_v1 *frame) } m_frame = frame; auto notifyBuffer = [this] { + if (!m_frame) + return; + if (!m_captureSource || !m_captureSource->imageValid()) { + qCWarning(lcTlCapture) << "Cannot notify capture buffer without a valid source image"; + m_frame->sendFailed(); + return; + } + + const QRect crop = source()->cropRect(); + if (crop.isEmpty()) { + qCWarning(lcTlCapture) << "Cannot notify capture buffer for empty crop rect" << crop; + m_frame->sendFailed(); + 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, @@ -154,6 +173,16 @@ void CaptureContextV1::onCapture(treeland_capture_frame_v1 *frame) if (m_captureSource->imageValid()) { notifyBuffer(); } else { + connect(m_captureSource, + &CaptureSource::imageFailed, + this, + [this] { + if (!m_frame) + return; + qCWarning(lcTlCapture) << "Failed to create one-shot capture image"; + m_frame->sendFailed(); + }, + Qt::SingleShotConnection); connect(m_captureSource, &CaptureSource::imageReady, this, notifyBuffer); } Q_EMIT finishSelect(); @@ -161,8 +190,12 @@ void CaptureContextV1::onCapture(treeland_capture_frame_v1 *frame) void CaptureContextV1::handleFrameCopy(wlr_buffer *buffer) { - if (m_captureSource) { - m_captureSource->copyBuffer(buffer); + if (m_captureSource && m_frame) { + if (!m_captureSource->copyBuffer(buffer)) { + qCWarning(lcTlCapture) << "Failed to copy capture image into client buffer"; + m_frame->sendFailed(); + return; + } m_frame->sendReady(); } else { wl_client_post_implementation_error(wl_resource_get_client(m_handle->resource), @@ -679,14 +712,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(); @@ -926,21 +965,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() @@ -964,23 +1065,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) @@ -992,10 +1149,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()->qwBuffer(); - 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()->qwBuffer(); + return nullptr; } QRect CaptureSourceOutput::cropRect() const @@ -1024,12 +1189,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()->qwBuffer(); - } 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()->qwBuffer(); + return nullptr; } CaptureSource::CaptureSourceType CaptureSourceRegion::sourceType() diff --git a/src/modules/capture/capture.h b/src/modules/capture/capture.h index 72a1aee0f7..e8bc6ac379 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; diff --git a/src/seat/helper.cpp b/src/seat/helper.cpp index 6f74c9d8c5..157f3e5b81 100644 --- a/src/seat/helper.cpp +++ b/src/seat/helper.cpp @@ -65,12 +65,15 @@ #include "workspace/workspace.h" #include +#include #include #include #include #include +#include #include +#include #include #include #include @@ -252,6 +255,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) @@ -1981,14 +2002,33 @@ 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 (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())) + 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 diff --git a/waylib/README.md b/waylib/README.md index b518257ef4..e2337eae7e 100644 --- a/waylib/README.md +++ b/waylib/README.md @@ -55,7 +55,7 @@ You can also packaging by using the command "nix build -v -L". Step 3: Execute the following commands ```bash -cmake -B build -DWITH_SUBMODULE_QWLROOTS=ON +cmake -B build cmake --build build ``` diff --git a/waylib/README.zh_CN.md b/waylib/README.zh_CN.md index 072dbe9e42..f763b6a9aa 100644 --- a/waylib/README.zh_CN.md +++ b/waylib/README.zh_CN.md @@ -55,7 +55,7 @@ NixOS 步骤三:运行以下命令 ```bash -cmake -B build -DWITH_SUBMODULE_QWLROOTS=ON +cmake -B build cmake --build build ``` diff --git a/waylib/src/server/CMakeLists.txt b/waylib/src/server/CMakeLists.txt index 6baa7b6435..33302f9602 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() add_compile_definitions(WLR_PRIVATE=) @@ -195,6 +200,7 @@ set(SOURCES ${WAYLAND_PROTOCOLS_OUTPUTDIR}/ext-foreign-toplevel-list-v1-protocol.c #TODO Remove after wlroots 0.20 utils/wtools.cpp + utils/wvulkantrace.cpp utils/wthreadutils.cpp utils/wimagebuffer.cpp utils/wcursorimage.cpp @@ -230,6 +236,8 @@ set(SOURCES protocols/ext_foreign_toplevel_image_capture_source.c #TODO: Remove after wlroots 0.20 protocols/wcursorshapemanagerv1.cpp protocols/woutputmanagerv1.cpp + protocols/wlinuxdmabufv1.cpp + protocols/wpresentation.cpp protocols/wextforeigntoplevellistv1.cpp protocols/wxdgdialogmanagerv1.cpp protocols/wsecuritycontextmanager.cpp @@ -334,6 +342,10 @@ set(HEADERS protocols/WCursorShapeManagerV1 protocols/woutputmanagerv1.h protocols/WOutputManagerV1 + protocols/wlinuxdmabufv1.h + protocols/WLinuxDmabufV1 + protocols/wpresentation.h + protocols/WPresentation protocols/wlayershell.h protocols/WLayerShell protocols/wxwayland.h @@ -387,6 +399,7 @@ set(PRIVATE_HEADERS protocols/private/wtextinputv2_p.h protocols/private/wtextinputv3_p.h protocols/private/wvirtualkeyboardv1_p.h + utils/private/wvulkantrace_p.h ) if(NOT DISABLE_XWAYLAND) @@ -460,6 +473,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 5f9ae8179d..5342b9de7e 100644 --- a/waylib/src/server/kernel/wlr_all.h +++ b/waylib/src/server/kernel/wlr_all.h @@ -91,6 +91,7 @@ extern "C" { #include #include #include +#include #include #include #include @@ -100,6 +101,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 c9440d2b8a..3beb47b533 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 @@ -94,6 +95,19 @@ WOutput::WOutput(wlr_output *handle, WBackend *backend) if (event->state->committed & WLR_OUTPUT_STATE_ENABLED) Q_EMIT enabledChanged(); }); + 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() @@ -307,18 +321,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, @@ -338,10 +352,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 && @@ -379,7 +396,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; } @@ -405,23 +426,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; @@ -430,12 +464,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 9649613b78..a7c93b8b01 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 bc610f1c65..d1352c52de 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 @@ -72,6 +73,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/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/wlinuxdmabufv1.cpp b/waylib/src/server/protocols/wlinuxdmabufv1.cpp new file mode 100644 index 0000000000..7f6a3474b0 --- /dev/null +++ b/waylib/src/server/protocols/wlinuxdmabufv1.cpp @@ -0,0 +1,78 @@ +// 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. +} + +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/protocols/wpresentation.cpp b/waylib/src/server/protocols/wpresentation.cpp new file mode 100644 index 0000000000..15a90070a4 --- /dev/null +++ b/waylib/src/server/protocols/wpresentation.cpp @@ -0,0 +1,80 @@ +// 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. +} + +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/private/wbufferrenderer.cpp b/waylib/src/server/qtquick/private/wbufferrenderer.cpp index a5c0f5cf79..a6add3d04c 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,23 @@ wlr_buffer *WBufferRenderer::lastBuffer() const QRhiTexture *WBufferRenderer::currentRenderTarget() const { - if (!state.sgRenderTarget.rt) + auto renderTarget = state.activeSgRenderTarget.rt + ? state.activeSgRenderTarget.rt + : 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(); } +QRhiRenderTarget *WBufferRenderer::currentVulkanBackdropResumeTarget() const +{ + return state.vulkanBackdropResumeSgRenderTarget.rt; +} + bool WBufferRenderer::isColorPreserved() const { return state.renderTarget.colorPreserved(); @@ -278,6 +315,9 @@ wlr_damage_ring *WBufferRenderer::damageRing() bool WBufferRenderer::isTextureProvider() const { + if (isPrimaryOutputRendererForVulkan()) + return false; + return true; } @@ -288,6 +328,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 +346,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) { @@ -329,9 +386,39 @@ 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) + return false; + + QStack items; + items.push(root); + while (!items.isEmpty()) { + auto *item = items.pop(); + 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)->paintOrderChildItems(); + for (auto child = children.crbegin(); child != children.crend(); ++child) + items.push(*child); + } + + return false; +} + 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 +428,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 +441,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 +451,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 +462,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,13 +483,29 @@ 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); + 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, 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; @@ -412,11 +526,7 @@ 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); - sgRT.rpDesc = rtd->u.rhiRt->renderPassDescriptor(); + sgRT = toRhiSgRenderTarget(wd, rt.rt()); #ifndef QT_NO_OPENGL if (wd->rhi->backend() == QRhi::OpenGLES2) { @@ -436,6 +546,39 @@ 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; } @@ -445,8 +588,52 @@ 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) +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; + + QSet seen; + const auto items = WOutputRenderWindow::paintOrderItemList(root, [] (QQuickItem *item) { + return item + && item->isVisible() + && item->flags().testFlag(QQuickItem::ItemHasContents) + && item->isTextureProvider(); + }); + + providers.reserve(items.size()); + for (auto item : items) { + if (!item) + continue; + + auto provider = qobject_cast(item->textureProvider()); + if (!provider || seen.contains(provider)) + continue; + + seen.insert(provider); + providers.append(provider); + } + + return providers; +} + +bool WBufferRenderer::render(int sourceIndex, const QMatrix4x4 &renderMatrix, + const QRectF &sourceRect, const QRectF &targetRect, + std::optional preserveColorContentsOverride) { Q_ASSERT(state.buffer); @@ -458,6 +645,21 @@ 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 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. @@ -480,16 +682,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 @@ -532,7 +735,7 @@ void 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()) { @@ -573,18 +776,45 @@ 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); + } + } + } + } + + 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, 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; } } -#ifdef ENABLE_VULKAN_RENDER - if (m_renderHelper) - m_renderHelper->prepareVulkanRenderTarget(state.sgRenderTarget.cb, state.renderTarget); -#endif 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,24 +869,85 @@ void WBufferRenderer::render(int sourceIndex, const QMatrix4x4 &renderMatrix, dr->currentFrameCommandBuffer()->resourceUpdate(resourceUpdates); } - if (shouldCacheBuffer()) + 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); + + 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); 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) { + 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 +969,11 @@ void WBufferRenderer::updateTextureProvider() if (!m_textureProvider) return; + if (isPrimaryOutputRendererForVulkan()) { + m_textureProvider->setBuffer(nullptr); + return; + } + if (shouldCacheBuffer()) { const bool hasCachedBuffer = m_textureProvider->qwBuffer(); // Ensure only update the buffer when the "shouldCacheBuffer" state is changed. @@ -691,12 +987,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 +1017,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..72efda59a8 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 { @@ -72,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(); @@ -94,8 +98,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 +119,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 +135,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; @@ -140,7 +149,14 @@ 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; QPointer m_output; diff --git a/waylib/src/server/qtquick/private/wrenderbuffernode.cpp b/waylib/src/server/qtquick/private/wrenderbuffernode.cpp index b870e8190d..20a43cb05b 100644 --- a/waylib/src/server/qtquick/private/wrenderbuffernode.cpp +++ b/waylib/src/server/qtquick/private/wrenderbuffernode.cpp @@ -12,12 +12,15 @@ #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 @@ -31,8 +34,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. @@ -93,6 +96,72 @@ static inline void rendererRender(QSGRenderer *r) { WAYLIB_SERVER_BEGIN_NAMESPACE +static bool recordVulkanDepthStencilPassBarrier(QRhi *rhi, + QRhiCommandBuffer *commandBuffer, + QRhiTexture *depthStencilTexture) +{ + if (!rhi || rhi->backend() != QRhi::Vulkan || !commandBuffer + || !depthStencilTexture) { + return false; + } + + const auto *nativeHandles = static_cast( + rhi->nativeHandles()); + auto *vkTexture = static_cast(depthStencilTexture); + if (!nativeHandles || !nativeHandles->inst + || nativeHandles->dev == VK_NULL_HANDLE + || vkTexture->image == VK_NULL_HANDLE) { + return false; + } + + QVulkanDeviceFunctions *deviceFunctions = + nativeHandles->inst->deviceFunctions(nativeHandles->dev); + if (!deviceFunctions) + return false; + + commandBuffer->beginExternal(); + const auto *commandBufferHandles = + static_cast( + commandBuffer->nativeHandles()); + if (!commandBufferHandles + || commandBufferHandles->commandBuffer == VK_NULL_HANDLE) { + commandBuffer->endExternal(); + return false; + } + + VkImageMemoryBarrier barrier = {}; + barrier.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER; + barrier.srcAccessMask = VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_READ_BIT + | VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT; + barrier.dstAccessMask = VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_READ_BIT + | VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT; + barrier.oldLayout = VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL; + barrier.newLayout = VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL; + barrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + barrier.image = vkTexture->image; + barrier.subresourceRange.aspectMask = VK_IMAGE_ASPECT_DEPTH_BIT + | VK_IMAGE_ASPECT_STENCIL_BIT; + barrier.subresourceRange.baseMipLevel = 0; + barrier.subresourceRange.levelCount = 1; + barrier.subresourceRange.baseArrayLayer = 0; + barrier.subresourceRange.layerCount = 1; + + constexpr VkPipelineStageFlags depthStencilStages = + VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT + | VK_PIPELINE_STAGE_LATE_FRAGMENT_TESTS_BIT; + deviceFunctions->vkCmdPipelineBarrier( + commandBufferHandles->commandBuffer, + depthStencilStages, + depthStencilStages, + VK_DEPENDENCY_BY_REGION_BIT, + 0, nullptr, + 0, nullptr, + 1, &barrier); + commandBuffer->endExternal(); + return true; +} + class Q_DECL_HIDDEN DataManagerBase : public QObject { public: @@ -134,6 +203,9 @@ class Q_DECL_HIDDEN DataManagerPointer DataManagerPointer &operator=(const DataManagerPointer &other) noexcept { + if (this == &other) + return *this; + deref(); pointer = other.pointer; ref(); @@ -142,6 +214,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 +227,9 @@ class Q_DECL_HIDDEN DataManagerPointer } inline DataManagerPointer &operator=(T* p) { + if (pointer == p) + return *this; + deref(); pointer = p; ref(); @@ -354,47 +433,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; } }; @@ -528,18 +667,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 @@ -570,9 +716,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() { @@ -596,6 +742,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()); @@ -662,28 +1008,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 { @@ -712,6 +1072,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(); @@ -738,15 +1102,69 @@ 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; + } + + 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 + || !currentDesc.depthTexture() + || currentDesc.depthTexture() != resumeDesc.depthTexture() + || currentDesc.depthStencilBuffer() + || resumeDesc.depthStencilBuffer() + || !resumeTextureTarget->flags().testFlag( + QRhiTextureRenderTarget::PreserveColorContents) + || !resumeTextureTarget->flags().testFlag( + QRhiTextureRenderTarget::PreserveDepthStencilContents)) { + 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); @@ -754,10 +1172,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) { @@ -780,6 +1202,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; @@ -791,10 +1214,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(); @@ -806,17 +1262,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) { @@ -825,6 +1304,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 { @@ -836,6 +1388,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()); @@ -950,6 +1508,303 @@ 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; + + 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 both the + // shared color and depth-stencil attachments. + 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); + + auto *depthStencilTexture = static_cast( + renderTarget()) + ->description() + .depthTexture(); + if (!recordVulkanDepthStencilPassBarrier( + rhi->rhi(), cb, depthStencilTexture)) { + // Restore the outer pass before disabling this node. The caller + // still owns that pass and expects it to remain active. + resumeOuterPass(); + setVulkanUnavailable( + "failed to record the depth-stencil pass barrier", + currentTexture); + return; + } + if (WVulkanTrace::enabled()) { + qCDebug(lcWlRenderBuffer).noquote() + << QStringLiteral("VKTRACE event=blitter-pass node=%1 sequence=%2 phase=depth-stencil-barrier depth=%3") + .arg(quintptr(this), 0, 16) + .arg(passSequence) + .arg(quintptr(depthStencilTexture), 0, 16); + } + + 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(); @@ -964,6 +1819,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; @@ -975,6 +1834,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() { @@ -1006,6 +1874,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/woutputitem.cpp b/waylib/src/server/qtquick/woutputitem.cpp index 2166a7c7d8..51e4a132f4 100644 --- a/waylib/src/server/qtquick/woutputitem.cpp +++ b/waylib/src/server/qtquick/woutputitem.cpp @@ -12,6 +12,8 @@ #include +#include + #include WAYLIB_SERVER_BEGIN_NAMESPACE @@ -157,6 +159,15 @@ void WOutputItemPrivate::updateCursors() } updateCursorVisible(safeOc.data()); + // On the Vulkan path the cursor item geometry is detached from the + // live pointer position (see PrimaryOutput.qml), so moving the cursor + // no longer dirties the scene. Schedule an output frame explicitly to + // repaint/move the cursor layer. GLES2 keeps relying on scene dirtying. + auto *outputItem = safeOc->output(); + if (outputItem && outputItem->output() && outputItem->window() + && outputItem->window()->graphicsApi() == QSGRendererInterface::Vulkan) { + outputItem->output()->scheduleFrame(); + } }); } diff --git a/waylib/src/server/qtquick/woutputrenderwindow.cpp b/waylib/src/server/qtquick/woutputrenderwindow.cpp index deadb722b2..2ee61711da 100644 --- a/waylib/src/server/qtquick/woutputrenderwindow.cpp +++ b/waylib/src/server/qtquick/woutputrenderwindow.cpp @@ -12,14 +12,19 @@ #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" #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 +34,16 @@ #include #include -#include +#include #include #include #include +#include +#include #include #include #include +#include #include #include @@ -55,6 +63,9 @@ #include #include +extern "C" { +#include +} #include #include @@ -229,8 +240,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"); @@ -250,6 +262,9 @@ 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; @@ -359,6 +374,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 +488,34 @@ 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); + 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); + void clearPresentationFeedbackForOutput(WOutput *output); + void clearPresentationFeedback(); + 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,9 +540,16 @@ class Q_DECL_HIDDEN WOutputRenderWindowPrivate : public QQuickWindowPrivate bool componentCompleted = true; bool inRendering = false; bool renderEnabled = true; + bool frameFailed = false; + QString fatalRenderError; + bool textureSyncQueueValidated = false; + bool textureSyncBatchDisabled = false; WPointer m_renderer; WPointer m_allocator; + QPointer m_presentation; + QPointer m_currentPresentationOutput; + QHash>> m_pendingPresentationSurfaces; QList outputs; QList layers; @@ -471,6 +561,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 @@ -526,7 +618,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(); @@ -582,6 +682,7 @@ void OutputHelper::cleanCursorRender() m_cursorRenderer->deleteLater(); m_cursorRenderer = nullptr; m_cursorLayerProxy = nullptr; + m_cursorSourceItem = nullptr; } } @@ -593,11 +694,33 @@ 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); + } + + 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; } static QQuickItem *createVisualRectangle(QQuickItem *target, const QColor &color) { @@ -654,8 +777,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 = output()->mapToViewport(source->parentItem()); + } else if (isRef) { viewportMatrix = output()->mapToViewport(layer->mapTo); const auto xScale = layer->mapTo->width() / layer->mapFrom->output()->width(); const auto yScale = layer->mapTo->height() / layer->mapFrom->output()->height(); @@ -677,8 +809,26 @@ wlr_buffer *OutputHelper::renderLayer(LayerData *layer, bool *dontEndRenderAndRe // matrix function: map source to output buffer const auto outputMatrix = viewportMatrix * output()->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" << output() + << "source" << source; + return nullptr; + } + if (!cursorItem->cursor()) + return nullptr; + + const QPointF outputLocalPosition = cursorItem->cursor()->position() + - QPointF(output()->output()->position()); + noClipMapRect = QRectF(outputLocalPosition - cursorItem->hotSpot(), source->size()); + mapRect = noClipMapRect; + } else { + noClipMapRect = outputMatrix.mapRect(noClipMapRect); + mapRect = outputMatrix.mapRect(mapRect); + } QTransform revertScaleTransform; if (!sizeSensitive) { @@ -718,7 +868,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; } @@ -768,15 +918,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 +1000,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 +1154,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 +1241,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 @@ -1095,6 +1324,8 @@ bool OutputHelper::commit(WBufferRenderer *buffer) bool OutputHelper::tryToHardwareCursor(const LayerData *layer) { + const bool isVulkan = WRenderHelper::getGraphicsApi(renderWindowD()->rc()) + == QSGRendererInterface::Vulkan; do { auto set_cursor = qwoutput()->impl->set_cursor; auto buffer = layer && layer->renderer->lastBuffer() @@ -1173,14 +1404,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 @@ -1190,10 +1450,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(); @@ -1206,25 +1478,86 @@ 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; } 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(); + // 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 (!set_cursor(qwoutput(), 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; @@ -1241,9 +1574,22 @@ bool OutputHelper::tryToHardwareCursor(const LayerData *layer) qwoutput()->transform, outputSize.width(), outputSize.height()); if (!move_cursor(qwoutput(), cleanTransform.x, cleanTransform.y)) { + qCWarning(lcWlCursor) + << "wlr output move_cursor failed; falling back to software cursor" + << "output" << output() + << "position" << QPoint(cleanTransform.x, cleanTransform.y); 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); @@ -1331,6 +1677,13 @@ void WOutputRenderWindowPrivate::init() void WOutputRenderWindowPrivate::init(OutputHelper *helper) { + if (graphicsApi() == QSGRendererInterface::Vulkan) { + qCInfo(lcWlCursor) << "Vulkan hardware cursor path enabled for output" + << "output" << helper->output()->output() + << "forceSoftwareCursor" + << helper->output()->output()->forceSoftwareCursor(); + } + QMetaObject::invokeMethod(helper, &WOutputHelper::scheduleFrame, Qt::QueuedConnection); helper->init(); QObject::connect(helper->output(), &WOutputViewport::dependsChanged, helper, [this] { @@ -1417,6 +1770,8 @@ bool WOutputRenderWindowPrivate::initRCWithRhi() Q_ASSERT(rcd->rhi == result.rhi); Q_ASSERT(!swapchain); + WVulkanTrace::activate(rhiSupport->rhiBackend() == QRhi::Vulkan); + return true; } @@ -1452,6 +1807,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->qwoutput() != 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->output())->renderable()) + || !shouldRender) + continue; + + if (!(helper->needsFrame() || helper->contentIsDirty())) + continue; + + if (!helper->contentIsDirty()) { + renderResults.append(helper); + continue; + } + } + + Q_ASSERT(helper->output()->output()->scale() + <= helper->output()->devicePixelRatio()); + + const auto &format = helper->qwoutput()->render_format; + const auto renderMatrix = helper->output()->renderMatrix(); + + // maybe using the other WOutputViewport's QSGTextureProvider + if (!helper->output()->depends().isEmpty()) + updateDirtyNodes(); + + wlr_buffer *buffer = helper->beginRender( + helper->bufferRenderer(), + helper->output()->output()->size(), + format, + WBufferRenderer::RedirectOpenGLContextDefaultFrameBufferObject, + helper->output()->colorContentsMode()); + Q_ASSERT(buffer == helper->bufferRenderer()->currentBuffer()); + if (buffer) { + helper->render(helper->bufferRenderer(), + 0, + renderMatrix, + helper->output()->effectiveSourceRect(), + helper->output()->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)) { @@ -1497,9 +1923,22 @@ WOutputRenderWindowPrivate::doRenderOutputs(wlr_output *needsFrameOutput, const helper->output()->colorContentsMode()); Q_ASSERT(buffer == helper->bufferRenderer()->currentBuffer()); if (buffer) { - helper->render(helper->bufferRenderer(), 0, renderMatrix, - helper->output()->effectiveSourceRect(), - helper->output()->targetRect()); + if (!helper->render(helper->bufferRenderer(), 0, renderMatrix, + helper->output()->effectiveSourceRect(), + helper->output()->targetRect())) { + qCWarning(lcWlBufferRenderer) << "Skipping output buffer because render pass failed" + << "renderer" << helper->bufferRenderer() + << "currentBuffer" << helper->bufferRenderer()->currentBuffer() + << "output" << helper->output(); + 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); } @@ -1517,6 +1956,374 @@ WOutputRenderWindowPrivate::doRenderOutputs(wlr_output *needsFrameOutput, const return needsCommit; } +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); + } + } +} + +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->qwBuffer(); + 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; +} + +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) { @@ -1544,30 +2351,205 @@ 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->output()->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); + clearPresentationFeedback(); + + const auto finishFrame = [this, q] (const QList> &committedOutputs) { + WRenderHelper::abortTextureSyncBatch(m_renderer); + clearPresentationFeedback(); + m_currentPreparedTextures = nullptr; + m_currentPreparedTextureSet.clear(); + + 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 { + failCurrentFrame(); + qCWarning(lcWlRenderer) << "Skipping Vulkan output frame because QRhi beginOffscreenFrame failed" + << "frameResult" << beginResult; + } + finishFrame({}); + return; + } + if (!textureSyncBatchDisabled) { + 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 + // retrying queue validation in the frame hot path. + textureSyncBatchDisabled = true; + } + } + } 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) { + if (!WRenderHelper::flushTextureSyncBatch(m_renderer)) { + // 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")); + } else { + 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 @@ -1576,13 +2558,19 @@ 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)) { - if (Q_UNLIKELY(!i.first->framePending())) { - if (Q_LIKELY(i.first->commit(i.second))) { + auto output = i.first->output()->output(); + if (allowCommit && 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); + if (Q_LIKELY(commitOk)) { // Make sure the output is still valid after commit - auto output = i.first->output()->output(); if (Q_LIKELY(needsFrameOutput)) { Q_ASSERT(output->handle() == needsFrameOutput); if (committedOutputs.isEmpty()) @@ -1591,6 +2579,8 @@ void WOutputRenderWindowPrivate::doRender(wlr_output *needsFrameOutput, committedOutputs.append(output); } } + } else { + clearPresentationFeedbackForOutput(output); } if (i.second->currentBuffer()) { @@ -1599,20 +2589,20 @@ void WOutputRenderWindowPrivate::doRender(wlr_output *needsFrameOutput, i.first->resetState(); } + } else { + clearPresentationFeedback(); } - 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); + } + finishFrame(committedOutputs); + if (vulkanFrameCompleted) + runAndClearJobs(&afterSwapJobs); } // TODO: Support QWindow::setCursor @@ -1638,11 +2628,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); @@ -1906,6 +2896,49 @@ 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); +} + +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 1e20ef10d2..dc8a1ac634 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,7 +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 @@ -52,6 +58,19 @@ 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; + void setPresentation(WPresentation *presentation); + void markSurfaceTexturedForPresentation(WSurface *surface); + + 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; 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 dca9a92990..83952d5614 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,10 +60,417 @@ 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); +} + +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 (wlr_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 VulkanDepthStencilAllocation { + QRhi *rhi = nullptr; + QVulkanDeviceFunctions *deviceFunctions = nullptr; + VkDevice device = VK_NULL_HANDLE; + VkImage image = VK_NULL_HANDLE; + VkDeviceMemory memory = VK_NULL_HANDLE; + VkFormat format = VK_FORMAT_UNDEFINED; + bool registeredWithRhi = false; + + ~VulkanDepthStencilAllocation() + { + release(); + } + + void release() + { + if (image == VK_NULL_HANDLE && memory == VK_NULL_HANDLE) + return; + + if (registeredWithRhi) { + // QRhi defers framebuffer and image-view destruction. Flush that + // queue before releasing the attachment they reference. This path + // only runs when render targets are retired, never per frame. + if (!rhi || rhi->finish() != QRhi::FrameOpSuccess) { + qCWarning(lcWlRenderHelper) + << "Keeping native Vulkan depth-stencil allocation alive after QRhi finish failed" + << "image" << vkImageName(image) + << "format" << hex32(format); + image = VK_NULL_HANDLE; + memory = VK_NULL_HANDLE; + return; + } + } + + if (deviceFunctions && device != VK_NULL_HANDLE) { + if (image != VK_NULL_HANDLE) + deviceFunctions->vkDestroyImage(device, image, nullptr); + if (memory != VK_NULL_HANDLE) + deviceFunctions->vkFreeMemory(device, memory, nullptr); + } + + if (WVulkanTrace::enabled()) { + qCDebug(lcWlRenderHelper).noquote() + << QStringLiteral("VKTRACE event=blitter-depth-destroy image=%1 format=%2") + .arg(vkImageValue(image), 0, 16) + .arg(quint32(format), 0, 16); + } + + image = VK_NULL_HANDLE; + memory = VK_NULL_HANDLE; + } +}; + +static bool findVulkanDepthStencilMemoryType( + const VkPhysicalDeviceMemoryProperties &properties, + uint32_t compatibleTypes, + uint32_t *memoryTypeIndex, + VkMemoryPropertyFlags *memoryTypeFlags) +{ + const auto find = [&] (VkMemoryPropertyFlags required) { + for (uint32_t i = 0; i < properties.memoryTypeCount; ++i) { + const VkMemoryPropertyFlags flags = properties.memoryTypes[i].propertyFlags; + if ((compatibleTypes & (uint32_t(1) << i)) == 0 + || (flags & required) != required + || (flags & VK_MEMORY_PROPERTY_LAZILY_ALLOCATED_BIT)) { + continue; + } + + *memoryTypeIndex = i; + *memoryTypeFlags = flags; + return true; + } + return false; + }; + + return find(VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT) || find(0); +} + +static bool createNativeVulkanDepthStencilTexture( + QRhi *rhi, + const QSize &pixelSize, + int sampleCount, + std::unique_ptr *texture, + std::unique_ptr *allocation) +{ + if (!rhi || rhi->backend() != QRhi::Vulkan || pixelSize.isEmpty() + || sampleCount != 1) { + return false; + } + + const auto *nativeHandles = static_cast( + rhi->nativeHandles()); + if (!nativeHandles || !nativeHandles->inst + || nativeHandles->physDev == VK_NULL_HANDLE + || nativeHandles->dev == VK_NULL_HANDLE) { + qCWarning(lcWlRenderHelper) + << "Cannot create attachment-only Vulkan depth-stencil texture without native handles"; + return false; + } + + QVulkanFunctions *instanceFunctions = nativeHandles->inst->functions(); + QVulkanDeviceFunctions *deviceFunctions = nativeHandles->inst->deviceFunctions( + nativeHandles->dev); + if (!instanceFunctions || !deviceFunctions) + return false; + + constexpr VkFormat candidates[] = { + VK_FORMAT_D24_UNORM_S8_UINT, + VK_FORMAT_D32_SFLOAT_S8_UINT, + VK_FORMAT_D16_UNORM_S8_UINT, + }; + VkFormat format = VK_FORMAT_UNDEFINED; + VkFormatProperties formatProperties = {}; + for (VkFormat candidate : candidates) { + instanceFunctions->vkGetPhysicalDeviceFormatProperties( + nativeHandles->physDev, candidate, &formatProperties); + if (!(formatProperties.optimalTilingFeatures + & VK_FORMAT_FEATURE_DEPTH_STENCIL_ATTACHMENT_BIT)) { + continue; + } + + VkImageFormatProperties imageFormatProperties = {}; + const VkResult formatResult = + instanceFunctions->vkGetPhysicalDeviceImageFormatProperties( + nativeHandles->physDev, + candidate, + VK_IMAGE_TYPE_2D, + VK_IMAGE_TILING_OPTIMAL, + VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT, + 0, + &imageFormatProperties); + if (formatResult != VK_SUCCESS + || imageFormatProperties.maxExtent.width + < uint32_t(pixelSize.width()) + || imageFormatProperties.maxExtent.height + < uint32_t(pixelSize.height()) + || !(imageFormatProperties.sampleCounts & VK_SAMPLE_COUNT_1_BIT)) { + continue; + } + + format = candidate; + break; + } + if (format == VK_FORMAT_UNDEFINED) { + qCWarning(lcWlRenderHelper) + << "No attachment-capable Vulkan depth-stencil format is available"; + return false; + } + + auto nativeAllocation = std::make_unique(); + nativeAllocation->rhi = rhi; + nativeAllocation->deviceFunctions = deviceFunctions; + nativeAllocation->device = nativeHandles->dev; + nativeAllocation->format = format; + + VkImageCreateInfo imageInfo = {}; + imageInfo.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO; + imageInfo.imageType = VK_IMAGE_TYPE_2D; + imageInfo.format = format; + imageInfo.extent = { + uint32_t(pixelSize.width()), + uint32_t(pixelSize.height()), + 1, + }; + imageInfo.mipLevels = 1; + imageInfo.arrayLayers = 1; + imageInfo.samples = VK_SAMPLE_COUNT_1_BIT; + imageInfo.tiling = VK_IMAGE_TILING_OPTIMAL; + imageInfo.usage = VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT; + imageInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE; + imageInfo.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; + + VkResult result = deviceFunctions->vkCreateImage( + nativeHandles->dev, &imageInfo, nullptr, &nativeAllocation->image); + if (result != VK_SUCCESS) { + qCWarning(lcWlRenderHelper) + << "Failed to create attachment-only Vulkan depth-stencil image" + << "format" << hex32(format) + << "size" << pixelSize + << "vkResult" << result; + return false; + } + + VkMemoryRequirements memoryRequirements = {}; + deviceFunctions->vkGetImageMemoryRequirements( + nativeHandles->dev, nativeAllocation->image, &memoryRequirements); + VkPhysicalDeviceMemoryProperties memoryProperties = {}; + instanceFunctions->vkGetPhysicalDeviceMemoryProperties( + nativeHandles->physDev, &memoryProperties); + + uint32_t memoryTypeIndex = std::numeric_limits::max(); + VkMemoryPropertyFlags memoryTypeFlags = 0; + if (!findVulkanDepthStencilMemoryType(memoryProperties, + memoryRequirements.memoryTypeBits, + &memoryTypeIndex, + &memoryTypeFlags)) { + qCWarning(lcWlRenderHelper) + << "No non-lazy Vulkan memory type is available for depth-stencil attachment" + << "format" << hex32(format) + << "memoryTypeBits" << hex32(memoryRequirements.memoryTypeBits); + return false; + } + + VkMemoryAllocateInfo memoryInfo = {}; + memoryInfo.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO; + memoryInfo.allocationSize = memoryRequirements.size; + memoryInfo.memoryTypeIndex = memoryTypeIndex; + result = deviceFunctions->vkAllocateMemory( + nativeHandles->dev, &memoryInfo, nullptr, &nativeAllocation->memory); + if (result != VK_SUCCESS) { + qCWarning(lcWlRenderHelper) + << "Failed to allocate Vulkan depth-stencil attachment memory" + << "format" << hex32(format) + << "allocationSize" << memoryRequirements.size + << "memoryTypeIndex" << memoryTypeIndex + << "vkResult" << result; + return false; + } + + result = deviceFunctions->vkBindImageMemory(nativeHandles->dev, + nativeAllocation->image, + nativeAllocation->memory, + 0); + if (result != VK_SUCCESS) { + qCWarning(lcWlRenderHelper) + << "Failed to bind Vulkan depth-stencil attachment memory" + << "format" << hex32(format) + << "vkResult" << result; + return false; + } + + std::unique_ptr depthTexture( + rhi->newTexture(QRhiTexture::D24S8, pixelSize, sampleCount, + QRhiTexture::RenderTarget)); + if (!depthTexture) + return false; + + auto *vkTexture = static_cast(depthTexture.get()); + vkTexture->owns = false; + vkTexture->image = nativeAllocation->image; + vkTexture->imageView = VK_NULL_HANDLE; + vkTexture->imageAlloc = nullptr; + vkTexture->usageState = { + VK_IMAGE_LAYOUT_UNDEFINED, + 0, + 0, + }; + vkTexture->vkformat = format; + vkTexture->viewFormat = format; + vkTexture->viewFormatForSampling = format; + vkTexture->mipLevelCount = 1; + vkTexture->samples = VK_SAMPLE_COUNT_1_BIT; + vkTexture->generation += 1; + depthTexture->setName( + QByteArrayLiteral("WaylibVulkanBackdropAttachmentOnlyDepthStencilTexture")); + + auto *rhiImplementation = static_cast( + W_PRIVATE_MEMBER(*rhi, QRhi_d_tag {})); + if (!rhiImplementation) + return false; + rhiImplementation->registerResource(vkTexture, false); + nativeAllocation->registeredWithRhi = true; + + if (WVulkanTrace::enabled()) { + qCDebug(lcWlRenderHelper).noquote() + << QStringLiteral("VKTRACE event=blitter-depth-create path=attachment-only image=%1 format=%2 size=%3x%4 memoryType=%5 memoryFlags=%6") + .arg(vkImageValue(nativeAllocation->image), 0, 16) + .arg(quint32(format), 0, 16) + .arg(pixelSize.width()) + .arg(pixelSize.height()) + .arg(memoryTypeIndex) + .arg(quint32(memoryTypeFlags), 0, 16); + } + + *texture = std::move(depthTexture); + *allocation = std::move(nativeAllocation); + return true; +} + +static bool createVulkanBackdropDepthStencilTexture( + QRhi *rhi, + const QSize &pixelSize, + int sampleCount, + std::unique_ptr *texture, + std::unique_ptr *allocation) +{ + if (rhi->isTextureFormatSupported(QRhiTexture::D24S8)) { + std::unique_ptr depthTexture( + rhi->newTexture(QRhiTexture::D24S8, pixelSize, sampleCount, + QRhiTexture::RenderTarget)); + if (depthTexture) { + depthTexture->setName( + QByteArrayLiteral("WaylibVulkanBackdropDepthStencilTexture")); + if (depthTexture->create()) { + if (WVulkanTrace::enabled()) { + qCDebug(lcWlRenderHelper).noquote() + << QStringLiteral("VKTRACE event=blitter-depth-create path=qrhi-d24s8 texture=%1 size=%2x%3") + .arg(quintptr(depthTexture.get()), 0, 16) + .arg(pixelSize.width()) + .arg(pixelSize.height()); + } + *texture = std::move(depthTexture); + return true; + } + } + + qCDebug(lcWlRenderHelper) + << "QRhi D24S8 depth-stencil texture creation failed; trying attachment-only fallback" + << "size" << pixelSize + << "sampleCount" << sampleCount; + } + + return createNativeVulkanDepthStencilTexture( + rhi, pixelSize, sampleCount, texture, allocation); +} +#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(); } @@ -51,14 +480,27 @@ 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) + std::unique_ptr vulkanBackdropDepthStencilAllocation; + QRhi *vulkanBackdropRhi = nullptr; +#endif + bool vulkanBackdropUnavailableLogged = false; 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; } @@ -66,31 +508,31 @@ 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 (windowRenderTarget.renderTarget == it->renderTarget) { + if (target.renderTarget == it->renderTarget) { it = s_rhiRenderBuffers->erase(it); break; } @@ -98,29 +540,67 @@ struct Q_DECL_HIDDEN BufferData { } } - if (windowRenderTarget.owns) { - delete windowRenderTarget.renderTarget; - delete windowRenderTarget.rpDesc; - delete windowRenderTarget.texture; - delete windowRenderTarget.renderBuffer; - delete windowRenderTarget.depthStencil; - delete windowRenderTarget.paintDevice; + if (target.owns) { + delete target.renderTarget; + delete target.rpDesc; + delete target.texture; + delete target.renderBuffer; + delete target.depthStencil; + delete target.paintDevice; } - windowRenderTarget.renderTarget = nullptr; - windowRenderTarget.rpDesc = nullptr; - windowRenderTarget.texture = nullptr; - windowRenderTarget.renderBuffer = nullptr; - windowRenderTarget.depthStencil = nullptr; - windowRenderTarget.paintDevice = nullptr; - windowRenderTarget.owns = false; + 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() { + // 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) + vulkanBackdropDepthStencilAllocation.reset(); +#endif + } + +#if defined(ENABLE_VULKAN_RENDER) && QT_VERSION >= QT_VERSION_CHECK(6, 8, 0) + void registerVulkanBackdropCleanup(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(); + }); + } +#endif }; class WRenderHelper::RenderTarget::Private { public: std::weak_ptr data; + QQuickRenderTarget selectedTarget; }; WRenderHelper::RenderTarget::RenderTarget() : d(new Private) {} @@ -146,7 +626,9 @@ QQuickRenderTarget WRenderHelper::RenderTarget::rt() const if (!d) return {}; auto data = d->data.lock(); - return data ? data->renderTarget : QQuickRenderTarget(); + if (!data) + return {}; + return d->selectedTarget.isNull() ? data->renderTarget : d->selectedTarget; } wlr_buffer *WRenderHelper::RenderTarget::buffer() const @@ -183,6 +665,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) { #if QT_VERSION >= QT_VERSION_CHECK(6, 8, 0) @@ -198,11 +787,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(); } @@ -213,9 +812,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; @@ -224,7 +824,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()) { @@ -247,8 +863,57 @@ static bool createRhiRenderTarget(const QRhiColorAttachment &colorAttachment, return true; } +#if QT_VERSION >= QT_VERSION_CHECK(6, 8, 0) +static bool createRhiRenderTargetWithDepthTexture( + const QRhiColorAttachment &colorAttachment, + const QSize &pixelSize, + int sampleCount, + QRhi *rhi, + QQuickWindowRenderTarget &dst, + QRhiTextureRenderTarget::Flags flags, + QRhiTexture *sharedDepthStencilTexture, + const char *name = "WaylibVulkanBackdropRenderTarget") +{ + if (!sharedDepthStencilTexture) + return false; + + QRhiTextureRenderTargetDescription rtDesc(colorAttachment); + rtDesc.setDepthTexture(sharedDepthStencilTexture); + std::unique_ptr rt( + rhi->newTextureRenderTarget(rtDesc, flags)); + std::unique_ptr rp( + newCompatibleRenderPassDescriptor(rt.get(), QByteArray(name))); + if (!rp) { + qCWarning(lcWlRenderHelper) + << "Failed to build Vulkan backdrop render pass" + << "name" << name + << "pixelSize" << pixelSize + << "sampleCount" << sampleCount + << "flags" << flags.toInt(); + return false; + } + rt->setRenderPassDescriptor(rp.get()); + rt->setName(QByteArray(name)); + if (!rt->create()) { + qCWarning(lcWlRenderHelper) + << "Failed to build Vulkan backdrop texture render target" + << "name" << name + << "pixelSize" << pixelSize + << "sampleCount" << sampleCount + << "flags" << flags.toInt(); + return false; + } + + dst.rt.renderTarget = rt.release(); + dst.res.rpDesc = rp.release(); + dst.rt.owns = true; + return true; +} +#endif + bool createRhiRenderTarget(QRhi *rhi, const QQuickRenderTarget &source, QQuickWindowRenderTarget &dst, - QRhiTextureRenderTarget::Flags flags) + QRhiTextureRenderTarget::Flags rtFlags = {}, + QRhiTexture::Flags extraTextureFlags = {}) { auto rtd = QQuickRenderTargetPrivate::get(&source); @@ -256,7 +921,7 @@ 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 @@ -272,7 +937,7 @@ bool createRhiRenderTarget(QRhi *rhi, const QQuickRenderTarget &source, QQuickWi #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; #if QT_VERSION >= QT_VERSION_CHECK(6, 8, 0) dst.res.texture = texture.release(); @@ -288,7 +953,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")); #if QT_VERSION >= QT_VERSION_CHECK(6, 8, 0) @@ -315,28 +980,57 @@ 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 = {}); + bool ensureVulkanBackdropRenderTargets(QQuickRenderControl *rc, + BufferData *data); 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 @@ -355,8 +1049,10 @@ 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(); #if QT_VERSION < QT_VERSION_CHECK(6, 6, 0) @@ -365,7 +1061,8 @@ bool WRenderHelperPrivate::ensureRhiRenderTarget(QQuickRenderControl *rc, Buffer auto rhi = rc->rhi(); #endif 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; #if QT_VERSION >= QT_VERSION_CHECK(6, 8, 0) @@ -376,7 +1073,197 @@ bool WRenderHelperPrivate::ensureRhiRenderTarget(QQuickRenderControl *rc, Buffer 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()); + } + + 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; + const bool compatibleRenderer = renderer && wlr_renderer_is_vk(renderer) + && wlr_vk_renderer_has_separate_depth_stencil_layouts(renderer); + const bool sameDevice = compatibleRenderer && 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 == wlr_vk_renderer_get_queue(renderer); + if (!rhi || rhi->backend() != QRhi::Vulkan || !colorTexture + || colorTexture->sampleCount() != 1 + || !colorTexture->flags().testFlag(QRhiTexture::UsedAsTransferSource) + || !compatibleRenderer || !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)) + << "separateDepthStencilLayouts" << compatibleRenderer + << "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 = {}; + data->vulkanBackdropDepthStencilAllocation.reset(); + + const QSize pixelSize = colorTexture->pixelSize(); + const int sampleCount = colorTexture->sampleCount(); + const QRhiColorAttachment colorAttachment(colorTexture); + std::unique_ptr ownedDepthTexture; + std::unique_ptr nativeDepthStencilAllocation; + + bool ok = createVulkanBackdropDepthStencilTexture( + rhi, pixelSize, sampleCount, &ownedDepthTexture, + &nativeDepthStencilAllocation); + QRhiTexture *depthTexture = ownedDepthTexture.get(); + if (ok) { + ok = createRhiRenderTargetWithDepthTexture( + colorAttachment, pixelSize, sampleCount, rhi, + data->vulkanBackdropWindowRenderTarget, {}, depthTexture, + "WaylibVulkanBackdropClearRenderTarget"); + } + if (ok) { + ok = createRhiRenderTargetWithDepthTexture( + colorAttachment, pixelSize, sampleCount, rhi, + data->vulkanBackdropPreserveWindowRenderTarget, + QRhiTextureRenderTarget::PreserveColorContents, + depthTexture, + "WaylibVulkanBackdropPreserveColorRenderTarget"); + } + if (ok) { + ok = createRhiRenderTargetWithDepthTexture( + colorAttachment, pixelSize, sampleCount, rhi, + data->vulkanBackdropResumeWindowRenderTarget, + QRhiTextureRenderTarget::PreserveColorContents + | QRhiTextureRenderTarget::PreserveDepthStencilContents, + depthTexture, + "WaylibVulkanBackdropResumeRenderTarget"); + } + + if (!ok) { + BufferData::cleanupWindowRenderTarget(data->vulkanBackdropResumeWindowRenderTarget); + BufferData::cleanupWindowRenderTarget(data->vulkanBackdropPreserveWindowRenderTarget); + BufferData::cleanupWindowRenderTarget(data->vulkanBackdropWindowRenderTarget); + ownedDepthTexture.reset(); + nativeDepthStencilAllocation.reset(); + if (!data->vulkanBackdropUnavailableLogged) { + qCWarning(lcWlRenderHelper) + << "Vulkan backdrop targets are unavailable" + << "reason" << "target creation failed" + << "pixelSize" << pixelSize + << "sampleCount" << sampleCount; + data->vulkanBackdropUnavailableLogged = true; + } + return false; + } + + // Keep exactly one owner for the texture while all three render targets + // borrow it. The native allocation, when used, must outlive the wrapper. + data->vulkanBackdropWindowRenderTarget.implicitBuffers.depthStencilTexture = + ownedDepthTexture.release(); + data->vulkanBackdropDepthStencilAllocation = + std::move(nativeDepthStencilAllocation); + data->registerVulkanBackdropCleanup(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 depth=%3 clear=%4 preserveColor=%5 resume=%6 size=%7x%8") + .arg(quintptr(data->buffer.get()), 0, 16) + .arg(quintptr(colorTexture), 0, 16) + .arg(quintptr(depthTexture), 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) @@ -392,15 +1279,244 @@ QSize WRenderHelper::size() const return d->size; } -void WRenderHelper::setSize(const QSize &size) -{ - W_D(WRenderHelper); - if (d->size == size) - return; - d->size = size; - d->resetRenderBuffer(); +void WRenderHelper::setSize(const QSize &size) +{ + W_D(WRenderHelper); + if (d->size == size) + return; + d->size = size; + 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; + } + + const bool ok = wlr_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; + } + + const bool ok = wlr_vk_renderer_record_render_buffer_release( + d->renderer, buffer, handles->commandBuffer, oldLayout); + commandBuffer->endExternal(); - Q_EMIT sizeChanged(); + 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) @@ -566,17 +1682,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; } @@ -725,7 +1830,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); @@ -764,8 +1870,17 @@ 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; + if (useVulkanBackdrop + && d->ensureVulkanBackdropRenderTargets(rc, data.get())) { + result.d->selectedTarget = data->vulkanBackdropRenderTarget; + } return result; } } @@ -775,6 +1890,8 @@ WRenderHelper::RenderTarget WRenderHelper::acquireRenderTarget(QQuickRenderContr bufferData->colorPreserved = needPreserve; QQuickRenderTarget rt; + bool needsVulkanRenderBufferAcquire = false; + QRhiTexture::Flags renderTargetTextureFlags; if (isSoftware) { WUniquePointer texture( @@ -790,15 +1907,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)) { @@ -818,16 +1944,26 @@ 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() }); + if (bufferData->preserveWindowRenderTarget.rt.renderTarget) { + s_rhiRenderBuffers->append({ bufferData->preserveWindowRenderTarget.rt.renderTarget, + texture, bufferData->buffer.get() }); + } } } @@ -844,101 +1980,50 @@ 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; } -WRenderHelper::RenderTarget WRenderHelper::lastRenderTarget() const +QQuickRenderTarget WRenderHelper::preserveRenderTarget(wlr_buffer *buffer, + bool useVulkanBackdrop) const { W_DC(WRenderHelper); - auto data = d->lastBuffer.lock(); - if (!data) - return {}; + for (auto data : std::as_const(d->buffers)) { + if (data->buffer == buffer) { + return useVulkanBackdrop + ? data->vulkanBackdropPreserveRenderTarget + : data->preserveRenderTarget; + } + } - RenderTarget result; - result.d->data = data; - return result; + return {}; } -#ifdef ENABLE_VULKAN_RENDER -void WRenderHelper::prepareVulkanRenderTarget(QRhiCommandBuffer *cb, const RenderTarget &rt) +QQuickRenderTarget WRenderHelper::vulkanBackdropResumeRenderTarget(wlr_buffer *buffer) const { - 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; + W_DC(WRenderHelper); + for (auto data : std::as_const(d->buffers)) { + if (data->buffer == buffer) + return data->vulkanBackdropResumeRenderTarget; } - // 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(); + return {}; } -void WRenderHelper::finishVulkanRenderTarget(QRhiCommandBuffer *cb, const RenderTarget &rt) +WRenderHelper::RenderTarget WRenderHelper::lastRenderTarget() const { - 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; - -#if QT_VERSION >= QT_VERSION_CHECK(6, 8, 0) - QRhiTexture *qtTexture = data->windowRenderTarget.res.texture; -#else - QRhiTexture *qtTexture = data->windowRenderTarget.texture; -#endif - if (!qtTexture) { - qCWarning(lcWlRenderHelper, "Vulkan render buffer release: missing Qt QRhiTexture"); - return; - } - - const auto native = qtTexture->nativeTexture(); -#if QT_VERSION < QT_VERSION_CHECK(6, 6, 0) - const auto oldLayout = VkImageLayout(native.layout); -#else - const auto oldLayout = VkImageLayout(native.layout); -#endif - 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(); + W_DC(WRenderHelper); + auto data = d->lastBuffer.lock(); + if (!data) + return {}; - // Keep QRhi's external-image tracker in sync with the post-release layout. - qtTexture->setNativeLayout(VK_IMAGE_LAYOUT_GENERAL); + RenderTarget result; + result.d->data = data; + return result; } -#endif // ENABLE_VULKAN_RENDER static wlr_renderer *createRendererWithType(const char *type, wlr_backend *backend) { @@ -1007,6 +2092,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; } @@ -1033,18 +2119,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) @@ -1100,7 +2192,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); @@ -1113,6 +2205,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) { @@ -1124,54 +2217,556 @@ 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(); + }; + + 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; + } - texture->setTextureFromNativeTexture(rhi, - vkimage_cast(attribs.image), - attribs.layout, attribs.format, size, - {}, {}); - texture->setHasAlphaChannel(wlr_vk_texture_has_alpha(handle)); + 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 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 != wlr_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 (!wlr_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 (!wlr_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)) + wlr_vk_renderer_abort_texture_sync_batch(renderer); +#else + Q_UNUSED(renderer); +#endif +} + +bool WRenderHelper::beginTextureBarrierBatch(wlr_renderer *renderer, bool release) +{ +#ifdef ENABLE_VULKAN_RENDER + if (!renderer || !wlr_renderer_is_vk(renderer)) + return true; + + if (!wlr_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; + wlr_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(); + wlr_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; + wlr_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; + wlr_vk_renderer_abort_texture_barrier_batch(renderer); + return false; + } + + const bool ok = wlr_vk_renderer_flush_texture_barrier_batch( + renderer, handles->commandBuffer); + commandBuffer->endExternal(); + + if (!ok) { + qCWarning(lcWlRenderHelper) << "Failed to flush Vulkan texture barrier batch" + << "purpose" << purpose; + wlr_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)) + wlr_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)) + wlr_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; + } + + const bool ok = wlr_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; + } + + const bool ok = wlr_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; } @@ -1226,10 +2821,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 (!wlr_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..057eb0417c 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; @@ -59,21 +62,46 @@ 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); + 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 bool makeTexture(QRhi *rhi, wlr_texture *handle, QSGPlainTexture *texture); + static QSGRendererInterface::GraphicsApi probe(wlr_backend *testBackend, + const QList &apiList); + + 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 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); struct TextureEntry { wlr_buffer *buffer; diff --git a/waylib/src/server/qtquick/wsgtextureprovider.cpp b/waylib/src/server/qtquick/wsgtextureprovider.cpp index 25e281c740..59d47acffe 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; + + 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) + { + } - Q_ASSERT(window); - class TextureCleanupJob : public QRunnable + ~TextureCleanupJob() override { - public: - TextureCleanupJob(QRhiTexture *texture) - : texture(texture) { } - void run() override { - texture->deleteLater(); + cleanup(); + } + + void run() override + { + WVulkanTrace::cleanupRunning(cleanupToken); + cleanup(); + } + + private: + void cleanup() + { + if (rhiTexture) { + if (deleteRhiTextureImmediately) + delete rhiTexture; + else + rhiTexture->deleteLater(); + rhiTexture = nullptr; } - QRhiTexture *texture; - }; + if (ownsTexture && texture) { + wlr_texture_destroy(texture); + texture = nullptr; + ownsTexture = false; + } + } + + 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); + } + + 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 (ownsTexture && texture) - wlr_texture_destroy(texture); + 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,206 @@ 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; + } + + // 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 + // 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(); } @@ -183,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); @@ -192,7 +603,7 @@ wlr_texture *WSGTextureProvider::qwTexture() const wlr_buffer *WSGTextureProvider::qwBuffer() const { W_DC(WSGTextureProvider); - return d->buffer; + return d->isVulkanRhi() ? d->buffer.get() : d->legacyBuffer; } bool WSGTextureProvider::smooth() const @@ -209,6 +620,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 714288e3e0..aed4246c0e 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 *qwBuffer() const; diff --git a/waylib/src/server/qtquick/wsurfaceitem.cpp b/waylib/src/server/qtquick/wsurfaceitem.cpp index d0c4ffefa6..c7c8649bb6 100644 --- a/waylib/src/server/qtquick/wsurfaceitem.cpp +++ b/waylib/src/server/qtquick/wsurfaceitem.cpp @@ -13,6 +13,7 @@ #include "wscoplistener.h" #include "wsurfaceitem_p.h" #include "wayliblogging.h" +#include "utils/private/wvulkantrace_p.h" #include @@ -25,9 +26,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 @@ -165,7 +176,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 { @@ -189,6 +200,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) { @@ -233,6 +252,11 @@ class Q_DECL_HIDDEN WSurfaceItemContentPrivate: public QQuickItemPrivate Q_ASSERT(!updateTextureConnection); + if (usesVulkanRhi(q->outputRenderWindow())) { + pendingBuffer.reset(); + pendingBufferValid = false; + } + if (dontCacheLastBuffer) { buffer.reset(); cleanTextureProvider(); @@ -262,8 +286,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(); } @@ -273,6 +303,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; @@ -337,8 +378,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) { @@ -371,6 +422,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; @@ -467,7 +519,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 { @@ -495,6 +553,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(); } @@ -589,21 +657,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) { @@ -612,15 +750,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()); @@ -1642,13 +1786,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 19dcd709fb..8ed6816d29 100644 --- a/waylib/src/server/wayliblogging.cpp +++ b/waylib/src/server/wayliblogging.cpp @@ -53,6 +53,8 @@ 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(lcWlPresentation, "waylib.protocols.presentation", QtInfoMsg) // Presentation-time feedback // Layer shell Q_LOGGING_CATEGORY(lcWlLayerShell, "waylib.layer.shell", QtWarningMsg) // wlr-layer-shell surface requests diff --git a/waylib/src/server/wayliblogging.h b/waylib/src/server/wayliblogging.h index 1705a60927..71bdc49981 100644 --- a/waylib/src/server/wayliblogging.h +++ b/waylib/src/server/wayliblogging.h @@ -46,6 +46,8 @@ 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(lcWlPresentation) // Layer shell Q_DECLARE_LOGGING_CATEGORY(lcWlLayerShell)