diff --git a/3rdparty/wlroots/include/render/vulkan.h b/3rdparty/wlroots/include/render/vulkan.h index f96619f2c6..899177003d 100644 --- a/3rdparty/wlroots/include/render/vulkan.h +++ b/3rdparty/wlroots/include/render/vulkan.h @@ -4,6 +4,7 @@ #include #include #include +#include #include #include #include @@ -44,6 +45,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 +100,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; }; @@ -234,6 +237,7 @@ struct wlr_vk_render_buffer { VkDeviceMemory memories[WLR_DMABUF_MAX_PLANES]; uint32_t mem_count; VkImage image; + VkImageUsageFlags image_usage; // Framebuffer and image view for rendering directly onto the buffer image, // without any color transform. @@ -290,6 +294,44 @@ struct wlr_vk_command_buffer { #define VULKAN_COMMAND_BUFFERS_CAP 64 +// Binary semaphore holding an imported foreign-texture DMA-BUF sync_file. +// Reusable once the texture_sync timeline semaphore reaches release_point. +struct wlr_vk_texture_sync_sem { + VkSemaphore semaphore; + uint64_t release_point; +}; + +// Append a wlr_buffer pointer to an append-only alias tracking list (dedup). +// Stale entries after buffer destruction may alias recycled pointers, which +// only ever adds a conservative extra memory dependency. Returns false when +// the allocation failed (the entry was then not added). +static inline bool vulkan_alias_list_add(struct wl_array *list, + struct wlr_buffer *buffer) { + struct wlr_buffer **entry; + wl_array_for_each(entry, list) { + if (*entry == buffer) { + return true; + } + } + struct wlr_buffer **slot = wl_array_add(list, sizeof(*slot)); + if (slot == NULL) { + return false; + } + *slot = buffer; + return true; +} + +static inline bool vulkan_alias_list_contains(const struct wl_array *list, + struct wlr_buffer *buffer) { + const struct wlr_buffer **entry; + wl_array_for_each(entry, list) { + if (*entry == buffer) { + return true; + } + } + return false; +} + // Vulkan wlr_renderer implementation on top of a wlr_vk_device. struct wlr_vk_renderer { struct wlr_renderer wlr_renderer; @@ -326,6 +368,72 @@ struct wlr_vk_renderer { VkSemaphore timeline_semaphore; uint64_t timeline_point; + // Frame-batched waiting on foreign texture DMA-BUF sync_files (Qt/QRhi + // path). Unsignaled sync_files are imported into binary semaphores and + // waited on by a bridge command buffer. Its pipeline barrier extends the + // wait dependency to the later Qt submission on the same queue. + VkSemaphore texture_sync_timeline_semaphore; + uint64_t texture_sync_timeline_point; + struct wl_array texture_sync_semaphores; // struct wlr_vk_texture_sync_sem + struct wl_array texture_sync_pending; // private texture sync wait entries + struct wl_array texture_sync_wait_infos; // VkSemaphoreSubmitInfoKHR scratch + bool texture_sync_batch_active; + bool texture_sync_force_poll; // env WLR_VK_FORCE_SYNC_POLL + + // Per-pass batching of foreign-texture acquire/release barriers (Qt/QRhi + // path). Instead of emitting one vkCmdPipelineBarrier per texture, the + // barriers are accumulated while a batch is active and recorded as a + // single call before (acquire phase) or after (release phase) the Qt draw. + struct wl_array texture_acquire_barriers; // VkImageMemoryBarrier + struct wl_array texture_release_barriers; // VkImageMemoryBarrier + // struct wlr_vk_texture * parallel to the barrier arrays above. A deferred + // barrier that is discarded without being recorded (batch abort, or a + // flush without a command buffer) must roll the per-texture ownership + // bookkeeping back, otherwise qt_sampling_acquired would claim a transfer + // that never happened. + struct wl_array texture_acquire_textures; // struct wlr_vk_texture * + struct wl_array texture_release_textures; // struct wlr_vk_texture * + // Set while a batch is active when a texture about to be sampled aliases + // (second import of the same DMA-BUF) a buffer that was rendered as a + // color attachment and is recorded in frame_render_buffers. The producer's + // writes happened through the other logical VkImage, so neither an + // ownership-transfer acquire (its source access mask is ignored) nor an + // image barrier naming the sampled image covers them in its memory scope: + // §Memory Aliasing requires a memory dependency, and only a global + // (resource-free) memory barrier can span the two aliases. One such + // barrier is recorded when the batch flushes. + bool frame_producer_alias_dep; + bool texture_barrier_batch_active; + bool texture_barrier_batch_release; // current batch is the release phase + + // wlr_buffer * rendered as a color attachment by the current frame's + // command buffer (output, layer, and cursor render buffers). Append-only + // with dedup, cleared by waylib_vk_renderer_reset_frame_alias_lists() at + // the compositor frame boundary: same-queue submission order already + // covers next-frame dependencies, so the lists only need to span one + // frame's recording. Stale entries would only ever add a conservative + // extra dependency, never remove one. + struct wl_array frame_render_buffers; + // wlr_buffer * sampled as a texture by the (Qt/QRhi) frame's command + // buffer. Same lifecycle and conservative policy as + // frame_render_buffers: a render-buffer acquire for one of these buffers + // records a global read-before-write (WAR) alias dependency, because the + // sampling may have read the aliased memory through another logical + // image whose barriers are out of this image's access scope. + struct wl_array frame_sampled_buffers; + + // Force the legacy blocking staging-upload path instead of the + // GPU-side asynchronous one (env WLR_VK_FORCE_STAGE_BLOCK). + bool stage_force_block; + // Enable the GPU-side asynchronous staging-upload path. Must only be set + // once the consumer (Qt/QRhi) is confirmed to submit to the same VkQueue, + // since the path relies on the same-queue texture-sync bridge to order the + // upload before the uploaded texture is sampled. + bool stage_async_enabled; + // A stage upload was submitted asynchronously and needs the texture-sync + // bridge barrier before the consumer submits its command buffer. + bool stage_async_needs_bridge; + size_t last_pool_size; struct wl_list descriptor_pools; // wlr_vk_descriptor_pool.link struct wl_list render_format_setups; // wlr_vk_render_format_setup.link @@ -408,6 +516,13 @@ VkCommandBuffer vulkan_record_stage_cb(struct wlr_vk_renderer *renderer); // finished execution. bool vulkan_submit_stage_wait(struct wlr_vk_renderer *renderer); +// Submits the current stage command buffer without blocking the CPU. The +// staging buffers it used are hidden in the command buffer's stage_buffers +// list and reclaimed once its timeline point is reached. Callers that sample +// the uploaded content must flush the texture-sync bridge and then submit +// their own command buffer to the same queue. +bool vulkan_submit_stage_async(struct wlr_vk_renderer *renderer); + struct wlr_vk_render_pass_texture { struct wlr_vk_texture *texture; @@ -497,6 +612,24 @@ struct wlr_vk_texture { struct wlr_vk_command_buffer *last_used_cb; // to track when it can be destroyed bool dmabuf_imported; bool owned; // if dmabuf_imported: whether we have ownership of the image + bool qt_sampling_acquired; // waylib/Qt-only bookkeeping: the Qt Quick RHI + // path (waylib_vk_renderer_prepare_texture_for_sampling() / + // waylib_vk_renderer_begin_readback()) currently holds a foreign->own + // acquire. Decoupled from 'owned', which is reserved for wlroots' own render-pass bookkeeping. + bool readback_acquired; // waylib/Qt-only: a readback acquire is in flight + // waylib/Qt-only: a readback cycle failed to record its release, so the + // image may still be owned by the graphics queue. Blocks every further + // readback (and sampling, through readback_acquired staying set) of this + // texture so it is never touched in an unknown ownership state again. + bool readback_release_failed; + // waylib/Qt-only: the DMA-BUF content of this texture was already waited + // on (producer sync_file / imported semaphore) since the client last + // attached the buffer. Set after a successful producer wait for a client + // texture; cleared by waylib_vk_renderer_mark_buffer_content_dirty() when + // the client re-attaches (commits) the buffer, matched through the client + // buffer's source identity. Only ever set for client textures; every + // other texture keeps waiting unconditionally. + bool qt_content_synced; bool transitioned; // if dma_imported: whether we transitioned it away from preinit bool has_alpha; // whether the image is has alpha channel bool using_mutable_srgb; // can be accessed through _SRGB format view @@ -515,7 +648,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..1511e115b6 100644 --- a/3rdparty/wlroots/include/wlr/render/vulkan.h +++ b/3rdparty/wlroots/include/wlr/render/vulkan.h @@ -22,6 +22,7 @@ struct wlr_vk_image_attribs { VkImage image; VkImageLayout layout; VkFormat format; + VkImageUsageFlags usage; }; struct wlr_renderer *wlr_vk_renderer_create_with_drm_fd(int drm_fd); @@ -49,4 +50,115 @@ bool waylib_vk_renderer_record_render_buffer_release(struct wlr_renderer *render struct wlr_buffer *buffer, VkCommandBuffer cb, VkImageLayout old_layout); bool waylib_vk_renderer_flush_stage(struct wlr_renderer *renderer); +/* waylib extensions (not upstream): Qt Quick RHI texture-sampling helpers. */ +VkQueue waylib_vk_renderer_get_queue(struct wlr_renderer *renderer); +bool waylib_vk_renderer_has_separate_depth_stencil_layouts( + struct wlr_renderer *renderer); + +// Prepare a wlroots-owned Vulkan texture for sampling on the given command +// buffer, transitioning it to SHADER_READ_ONLY_OPTIMAL and transferring queue +// ownership to the graphics queue if it is not already owned. The optional +// attribs out-parameter receives the texture's image attributes. +// sampled_buffer is the wlr_buffer the caller is sampling the texture as (may +// be NULL when the caller cannot provide it): it identifies client textures so +// a repeated acquire of an unchanged client buffer can skip the producer fence +// wait (see waylib_vk_renderer_mark_buffer_content_dirty()). +bool waylib_vk_renderer_prepare_texture_for_sampling(struct wlr_renderer *renderer, + struct wlr_texture *texture, struct wlr_buffer *sampled_buffer, + VkCommandBuffer cb, struct wlr_vk_image_attribs *attribs); +bool waylib_vk_renderer_finish_texture_sampling(struct wlr_renderer *renderer, + struct wlr_texture *texture, VkCommandBuffer cb); + +// Clear the "content already synchronized" shortcut for every wlroots Vulkan +// texture currently importing the given wlr_buffer. Call from the surface +// commit path when the client (re-)attaches a buffer: the producer may have +// written new content, so the next sampling acquire must export and wait for a +// fresh producer fence instead of reusing the previous one. The buffer may be +// a wlr_client_buffer, in which case every texture importing either it or its +// source buffer is cleared. No-op for a NULL/invalid renderer, a NULL buffer, +// or a non-Vulkan renderer. +void waylib_vk_renderer_mark_buffer_content_dirty(struct wlr_renderer *renderer, + struct wlr_buffer *buffer); + +// Collect foreign-texture sync_files while a compositor frame is recorded, +// then submit one semaphore wait with a bridge barrier before the compositor +// command buffer is submitted to the same queue. The barrier extends the wait +// dependency to that later submission. The abort function is idempotent. +bool waylib_vk_renderer_begin_texture_sync_batch(struct wlr_renderer *renderer); +bool waylib_vk_renderer_flush_texture_sync_batch(struct wlr_renderer *renderer); +void waylib_vk_renderer_abort_texture_sync_batch(struct wlr_renderer *renderer); + +// Batch the per-texture queue-family-ownership/layout barriers that +// waylib_vk_renderer_prepare_texture_for_sampling() and +// waylib_vk_renderer_finish_texture_sampling() would otherwise emit one at a +// time. Begin a batch (release=false for the pre-draw acquire phase, +// release=true for the post-draw release phase), call prepare/finish for each +// texture, then flush once to record a single vkCmdPipelineBarrier covering +// every accumulated texture. While a batch is active, prepare defers acquire +// barriers and finish defers release barriers; a call belonging to the other +// phase still records immediately so the two phases never mix in one flush. +// abort discards any pending barriers and is idempotent. +bool waylib_vk_renderer_begin_texture_barrier_batch(struct wlr_renderer *renderer, + bool release); +bool waylib_vk_renderer_flush_texture_barrier_batch(struct wlr_renderer *renderer, + VkCommandBuffer cb); +void waylib_vk_renderer_abort_texture_barrier_batch(struct wlr_renderer *renderer); + +// Clear the frame's color-attachment-producer and sampled-texture alias +// lists. Call from the compositor frame boundary once the frame's GPU work +// has completed (e.g. after a synchronous Qt Offscreen endFrame on the same +// queue): every submission of the finished frame is ordered implicit with the +// next one on the same queue, and the in-frame alias barriers are recorded +// from the lists during the next frame, so keeping stale entries only adds +// permanent global barriers and unbounded CPU scans. +void waylib_vk_renderer_reset_frame_alias_lists(struct wlr_renderer *renderer); + +// Readback ownership gate for DMA-BUF-imported wlroots textures read through +// wlr_texture_read_pixels()/vulkan_read_pixels() from outside the Qt frame. +// The import lacks a known Vulkan-side layout and may carry an unsignaled +// producer fence, so reading it without this gate uses FOREIGN-owned memory. +// vulkan_texture_read_pixels() applies this gate internally, which covers the +// wlroots protocol paths (screencopy, ext-image-copy-capture); callers that +// want the gate held around a larger cycle (WBufferDumper) may call +// begin_readback() themselves before the read: for a dmabuf texture it waits +// the buffer's DMA-BUF fences (sync_file poll) and records/submits a +// FOREIGN->own acquire barrier leaving the image in VK_IMAGE_LAYOUT_GENERAL; +// the follow-up read_pixels() staging command buffer is submitted later on the +// same queue, which gives the required execution dependency. ACQUIRED means +// end_readback() MUST be called after the read (recorded on its own command +// buffer); NOOP is returned for textures that are not DMA-BUF imports (SHM +// upload textures) and for nested reads inside an already-held cycle (the +// outer cycle records the matching release); ERROR fails closed for +// multi-plane/disjoint/YCbCr textures, a texture whose foreign ownership is +// already held by a frame, or one whose release previously failed. +// begin_readback() never waits the read itself; the read_pixels() staging +// submit CPU-waits as usual. +enum wlr_vk_texture_readback_state { + WLR_VK_TEXTURE_READBACK_ERROR = 0, + WLR_VK_TEXTURE_READBACK_ACQUIRED, + WLR_VK_TEXTURE_READBACK_NOOP, +}; +enum wlr_vk_texture_readback_state waylib_vk_texture_begin_readback( + struct wlr_renderer *renderer, struct wlr_texture *texture); +void waylib_vk_texture_end_readback(struct wlr_renderer *renderer, + struct wlr_texture *texture); + +// Enable the GPU-side asynchronous staging-upload path used for shared-memory +// (CPU-rendered) client buffers. It submits the staging copy without blocking +// the caller and chains the upload through the texture-sync bridge before the +// texture is sampled, so it must only be enabled when the consumer (e.g. +// Qt/QRhi) submits its command buffers to the same VkQueue as the renderer and +// flushes that bridge before submission. When disabled (the default), staging +// uploads use a blocking wait. +void waylib_vk_renderer_set_stage_async_enabled(struct wlr_renderer *renderer, + bool enabled); + +// Restrict the renderer's advertised dmabuf and shm texture format sets to +// the given DRM format whitelist, dropping every other format so clients +// cannot allocate buffers the compositor is unable to display. Call once +// after renderer creation and before any client connects. Passing an empty +// whitelist fails without touching the sets. +bool waylib_vk_renderer_restrict_texture_formats(struct wlr_renderer *renderer, + const uint32_t *drm_formats, size_t count); + #endif diff --git a/3rdparty/wlroots/render/vulkan/pixel_format.c b/3rdparty/wlroots/render/vulkan/pixel_format.c index 9b17d079a0..a1938cc815 100644 --- a/3rdparty/wlroots/render/vulkan/pixel_format.c +++ b/3rdparty/wlroots/render/vulkan/pixel_format.c @@ -448,6 +448,23 @@ static bool query_modifier_support(struct wlr_vk_device *dev, } if (supported) { + // Transfer-source support is optional and must not remove an + // otherwise valid render modifier. Users can inspect the actual + // image usage and fall back when this stricter query fails. + const VkImageUsageFlags transfer_src_usage = + vulkan_render_usage | VK_IMAGE_USAGE_TRANSFER_SRC_BIT; + const VkFormat variant = p.has_mutable_srgb ? + props->format.vk_srgb : VK_FORMAT_UNDEFINED; + struct wlr_vk_format_modifier_props transfer_src = {0}; + const char *transfer_src_errmsg = NULL; + if ((m.drmFormatModifierTilingFeatures & + VK_FORMAT_FEATURE_TRANSFER_SRC_BIT) && + query_modifier_usage_support(dev, props->format.vk, + variant, transfer_src_usage, &m, &transfer_src, + &transfer_src_errmsg)) { + p.transfer_src_max_extent = transfer_src.max_extent; + } + props->dmabuf.render_mods[props->dmabuf.render_mod_count++] = p; wlr_drm_format_set_add(&dev->dmabuf_render_formats, props->format.drm, m.drmFormatModifier); diff --git a/3rdparty/wlroots/render/vulkan/renderer.c b/3rdparty/wlroots/render/vulkan/renderer.c index 90d4b8ab56..3debbd95d5 100644 --- a/3rdparty/wlroots/render/vulkan/renderer.c +++ b/3rdparty/wlroots/render/vulkan/renderer.c @@ -417,6 +417,7 @@ bool vulkan_submit_stage_wait(struct wlr_vk_renderer *renderer) { return vulkan_wait_command_buffer(cb, renderer); } + bool waylib_vk_renderer_flush_stage(struct wlr_renderer *wlr_renderer) { assert(wlr_renderer_is_vk(wlr_renderer)); struct wlr_vk_renderer *renderer = vulkan_get_renderer(wlr_renderer); @@ -426,6 +427,106 @@ bool waylib_vk_renderer_flush_stage(struct wlr_renderer *wlr_renderer) { return vulkan_submit_stage_wait(renderer); } +// Returns the staging spans referenced by a stage command buffer that was +// never submitted (or whose submission failed) to the allocator. Safe because +// no GPU work referencing them was ever queued; the same release is performed +// by release_completed_stage_buffers() after a blocking submission completes. +static void reclaim_pending_stage_buffers(struct wlr_vk_renderer *renderer) { + int64_t now = get_current_time_msec(); + struct wlr_vk_shared_buffer *buf; + wl_list_for_each(buf, &renderer->stage.buffers, link) { + if (buf->allocs.size == 0) { + continue; + } + + buf->allocs.size = 0; + buf->last_used_ms = now; + } +} + +bool vulkan_submit_stage_async(struct wlr_vk_renderer *renderer) { + if (renderer->stage.cb == NULL) { + return true; + } + + struct wlr_vk_command_buffer *cb = renderer->stage.cb; + renderer->stage.cb = NULL; + + uint64_t timeline_point = vulkan_end_command_buffer(cb, renderer); + if (timeline_point == 0) { + // The command buffer is invalid and was never submitted: make the + // slot reusable again and return its staging spans to the pool. + cb->timeline_point = 0; + vulkan_reset_command_buffer(cb); + reclaim_pending_stage_buffers(renderer); + return false; + } + + VkTimelineSemaphoreSubmitInfoKHR timeline_submit_info = { + .sType = VK_STRUCTURE_TYPE_TIMELINE_SEMAPHORE_SUBMIT_INFO_KHR, + .signalSemaphoreValueCount = 1, + .pSignalSemaphoreValues = &timeline_point, + }; + VkSubmitInfo submit_info = { + .sType = VK_STRUCTURE_TYPE_SUBMIT_INFO, + .pNext = &timeline_submit_info, + .commandBufferCount = 1, + .pCommandBuffers = &cb->vk, + .signalSemaphoreCount = 1, + .pSignalSemaphores = &renderer->timeline_semaphore, + }; + VkResult res = vkQueueSubmit(renderer->dev->queue, 1, &submit_info, VK_NULL_HANDLE); + if (res != VK_SUCCESS) { + wlr_vk_error("vkQueueSubmit", res); + cb->timeline_point = 0; + vulkan_reset_command_buffer(cb); + // Nothing executed: the staging spans are safe to reuse. + reclaim_pending_stage_buffers(renderer); + return false; + } + + renderer->stage.last_timeline_point = timeline_point; + renderer->stage_async_needs_bridge = true; + + // Hide the staging buffers used by this submission from the allocator. + // release_command_buffer_resources() returns them (with their allocations + // reset) once this command buffer's timeline point is reached, so the CPU + // never reuses a span the GPU is still reading. The texture-sync bridge + // inserts the dependency required before a later Qt sampling submission. + size_t hidden = 0; + struct wlr_vk_shared_buffer *buf, *tmp; + wl_list_for_each_safe(buf, tmp, &renderer->stage.buffers, link) { + if (buf->allocs.size == 0) { + continue; + } + wl_list_remove(&buf->link); + wl_list_insert(&cb->stage_buffers, &buf->link); + ++hidden; + } + + static bool logged_async_stage; + if (!logged_async_stage) { + logged_async_stage = true; + wlr_log(WLR_INFO, "vk-stage: using GPU-side asynchronous staging " + "uploads (set WLR_VK_FORCE_STAGE_BLOCK=1 to force the blocking path)"); + } + wlr_log(WLR_DEBUG, "vk-stage: submitted staging command buffer " + "timeline_point=%llu buffers_hidden=%zu", + (unsigned long long)timeline_point, hidden); + + return true; +} + +void waylib_vk_renderer_set_stage_async_enabled(struct wlr_renderer *wlr_renderer, + bool enabled) { + if (wlr_renderer == NULL || !wlr_renderer_is_vk(wlr_renderer)) { + return; + } + + struct wlr_vk_renderer *renderer = vulkan_get_renderer(wlr_renderer); + renderer->stage_async_enabled = enabled; +} + struct wlr_vk_format_props *vulkan_format_props_from_drm( struct wlr_vk_device *dev, uint32_t drm_fmt) { for (size_t i = 0u; i < dev->format_prop_count; ++i) { @@ -482,6 +583,67 @@ bool vulkan_wait_command_buffer(struct wlr_vk_command_buffer *cb, return true; } +// Waits until every submission issued by this renderer has completed on the +// GPU. All of them (stage uploads, the texture-sync bridge, render passes) +// signal the renderer timeline semaphore with their command buffer's timeline +// point as the final operation, so waiting for the counter to catch up with +// renderer->timeline_point matches queue idleness for this renderer's work +// without a queue-wide flush. Callers that destroy resources may only run on +// the compositor frame boundary, where the Qt/QRhi frame has already been +// submitted and CPU-waited (QRhi offscreen endFrame does the latter), so +// Qt-side submissions of the same queue are complete as well. If that +// invariant changes, Qt submissions must additionally be tracked here. +static bool vulkan_wait_renderer_idle(struct wlr_vk_renderer *renderer) { + VkResult res; + + if (renderer->timeline_semaphore == VK_NULL_HANDLE) { + res = vkQueueWaitIdle(renderer->dev->queue); + if (res != VK_SUCCESS) { + wlr_vk_error("vkQueueWaitIdle", res); + return false; + } + return true; + } + + uint64_t current_point; + res = renderer->dev->api.vkGetSemaphoreCounterValueKHR(renderer->dev->dev, + renderer->timeline_semaphore, ¤t_point); + if (res != VK_SUCCESS) { + wlr_vk_error("vkGetSemaphoreCounterValueKHR", res); + current_point = 0; + } else if (current_point >= renderer->timeline_point) { + return true; + } + + VkSemaphoreWaitInfoKHR wait_info = { + .sType = VK_STRUCTURE_TYPE_SEMAPHORE_WAIT_INFO_KHR, + .semaphoreCount = 1, + .pSemaphores = &renderer->timeline_semaphore, + .pValues = &renderer->timeline_point, + }; + // Bounded wait: a failed vkQueueSubmit can leave an allocated timeline + // point unsignaled forever, and waiting on it with an infinite timeout + // would deadlock the compositor. On timeout, fall back to a queue-wide + // wait (which cannot hang on the gap) before continuing best-effort. + res = renderer->dev->api.vkWaitSemaphoresKHR(renderer->dev->dev, + &wait_info, 1000ull * 1000 * 1000); + if (res == VK_TIMEOUT) { + wlr_log(WLR_ERROR, "Timed out waiting for the Vulkan renderer " + "timeline; falling back to vkQueueWaitIdle"); + res = vkQueueWaitIdle(renderer->dev->queue); + if (res != VK_SUCCESS) { + wlr_vk_error("vkQueueWaitIdle", res); + return false; + } + return true; + } + if (res != VK_SUCCESS) { + wlr_vk_error("vkWaitSemaphoresKHR", res); + return false; + } + return true; +} + static void release_command_buffer_resources(struct wlr_vk_command_buffer *cb, struct wlr_vk_renderer *renderer, int64_t now) { struct wlr_vk_texture *texture, *texture_tmp; @@ -491,6 +653,7 @@ static void release_command_buffer_resources(struct wlr_vk_command_buffer *cb, wlr_texture_destroy(&texture->wlr_texture); } + size_t reclaimed = 0; struct wlr_vk_shared_buffer *buf, *buf_tmp; wl_list_for_each_safe(buf, buf_tmp, &cb->stage_buffers, link) { buf->allocs.size = 0; @@ -498,6 +661,10 @@ static void release_command_buffer_resources(struct wlr_vk_command_buffer *cb, wl_list_remove(&buf->link); wl_list_insert(&renderer->stage.buffers, &buf->link); + ++reclaimed; + } + if (reclaimed > 0) { + wlr_log(WLR_DEBUG, "vk-stage: reclaimed %zu staging buffers", reclaimed); } if (cb->color_transform) { @@ -566,7 +733,13 @@ static struct wlr_vk_command_buffer *get_command_buffer( return unused; } - // Block until a busy command buffer becomes available + // Block until a busy command buffer becomes available. With every slot + // recording there may be no completed one to wait on; fail instead of + // dereferencing a NULL waiter. + if (wait == NULL) { + wlr_log(WLR_ERROR, "No idle Vulkan command buffer available"); + return NULL; + } if (!vulkan_wait_command_buffer(wait, renderer)) { return NULL; } @@ -628,10 +801,12 @@ static void destroy_render_buffer(struct wlr_vk_render_buffer *buffer) { VkDevice dev = buffer->renderer->dev->dev; // TODO: asynchronously wait for the command buffers using this render - // buffer to complete (just like we do for textures) - VkResult res = vkQueueWaitIdle(buffer->renderer->dev->queue); - if (res != VK_SUCCESS) { - wlr_vk_error("vkQueueWaitIdle", res); + // buffer to complete (just like we do for textures). Until then, wait on + // the renderer timeline (see vulkan_wait_renderer_idle) instead of a + // queue-wide wait so this eviction path does not flush unrelated work. + if (!vulkan_wait_renderer_idle(buffer->renderer)) { + wlr_log(WLR_ERROR, "Failed to wait for outstanding Vulkan submissions " + "before destroying a render buffer; continuing best-effort"); } finish_render_buffer_out(&buffer->linear.out, dev); @@ -930,7 +1105,8 @@ static struct wlr_vk_render_buffer *create_render_buffer( bool using_mutable_srgb = false; buffer->image = vulkan_import_dmabuf(renderer, &dmabuf, - buffer->memories, &buffer->mem_count, true, &using_mutable_srgb); + buffer->memories, &buffer->mem_count, true, &using_mutable_srgb, + &buffer->image_usage); if (!buffer->image) { goto error; } @@ -1019,6 +1195,7 @@ bool waylib_vk_renderer_get_render_buffer_attribs(struct wlr_renderer *wlr_rende attribs->image = render_buffer->image; attribs->layout = VK_IMAGE_LAYOUT_GENERAL; attribs->format = format->vk; + attribs->usage = render_buffer->image_usage; return true; } @@ -1039,15 +1216,16 @@ bool waylib_vk_renderer_record_render_buffer_acquire(struct wlr_renderer *wlr_re } /* Match wlroots pass.c / tinywl: foreign ownership transfer into the - * graphics queue before writing the scanout image. */ + * graphics queue before writing the scanout image. DMA-BUF imports start + * without a Vulkan-side known layout; GENERAL is the accounting layout + * every path in this renderer shares for them (and the steady-state + * layout after each render-buffer release), so the first acquire uses it + * as well instead of PREINITIALIZED, which describes host-written memory + * and never applies here. All output pathways share the imported + * VkImage; once transitioned, each pathway observes GENERAL on + * subsequent use. + */ VkImageLayout src_layout = VK_IMAGE_LAYOUT_GENERAL; - if (!render_buffer->linear.out.transitioned && - !render_buffer->srgb.out.transitioned && - !render_buffer->two_pass.out.transitioned) { - src_layout = VK_IMAGE_LAYOUT_PREINITIALIZED; - } - // All output pathways share the imported VkImage. Once this acquire - // barrier runs, each pathway observes GENERAL on subsequent use. render_buffer->linear.out.transitioned = true; render_buffer->srgb.out.transitioned = true; render_buffer->two_pass.out.transitioned = true; @@ -1060,16 +1238,36 @@ bool waylib_vk_renderer_record_render_buffer_acquire(struct wlr_renderer *wlr_re .oldLayout = src_layout, .newLayout = VK_IMAGE_LAYOUT_GENERAL, .srcAccessMask = 0, - .dstAccessMask = VK_ACCESS_COLOR_ATTACHMENT_READ_BIT | - VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT, + .dstAccessMask = VK_ACCESS_MEMORY_READ_BIT | + VK_ACCESS_MEMORY_WRITE_BIT, .subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT, .subresourceRange.layerCount = 1, .subresourceRange.levelCount = 1, }; + /* The second scope must cover every access this renderer performs on the + * buffer after the transfer, not only the render-pass color attachment + * use: the Qt path samples the buffer (shader read) and the render-buffer + * blitter copies from it (transfer read). */ vkCmdPipelineBarrier(cb, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, - VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT, + VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, 0, 0, NULL, 0, NULL, 1, &barrier); + + // WAR alias dependency: the texture-sampling path may have recently read + // this buffer's memory through a second logical VkImage. Writes through + // this image must not begin before those reads finished; a barrier naming + // either image cannot cover the other's accesses, so use the global form + // (§Memory Aliasing). + if (vulkan_alias_list_contains(&renderer->frame_sampled_buffers, wlr_buffer)) { + VkMemoryBarrier memory_barrier = { + .sType = VK_STRUCTURE_TYPE_MEMORY_BARRIER, + .srcAccessMask = VK_ACCESS_MEMORY_READ_BIT, + .dstAccessMask = VK_ACCESS_MEMORY_WRITE_BIT, + }; + vkCmdPipelineBarrier(cb, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, + VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, 0, + 1, &memory_barrier, 0, NULL, 0, NULL); + } return true; } @@ -1090,6 +1288,16 @@ bool waylib_vk_renderer_record_render_buffer_release(struct wlr_renderer *wlr_re return false; } + // Remember the buffer as a color-attachment producer aliasing its sampled + // textures. When such a texture is imported and sampled, wlroots records a + // global memory dependency extending these writes to the sampling + // (see record_frame_render_barrier; an ownership-transfer or image-scoped + // barrier cannot cover accesses made through this separate logical image). + if (!vulkan_alias_list_add(&renderer->frame_render_buffers, wlr_buffer)) { + wlr_log_errno(WLR_ERROR, + "Failed to track the frame render buffer producer"); + } + VkImageMemoryBarrier barrier = { .sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER, .srcQueueFamilyIndex = renderer->dev->queue_family, @@ -1097,20 +1305,34 @@ bool waylib_vk_renderer_record_render_buffer_release(struct wlr_renderer *wlr_re .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, + .srcAccessMask = VK_ACCESS_MEMORY_READ_BIT | + VK_ACCESS_MEMORY_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, + /* The first scope must cover every access the renderer made since the + * acquire: the render pass writes the image, while the Qt sampling and + * the render-buffer blitter read it (shader/transfer reads). A release + * that only named the color attachment stage would drop those reads. */ + vkCmdPipelineBarrier(cb, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT, 0, 0, NULL, 0, NULL, 1, &barrier); return true; } +void waylib_vk_renderer_reset_frame_alias_lists(struct wlr_renderer *wlr_renderer) { + if (wlr_renderer == NULL || !wlr_renderer_is_vk(wlr_renderer)) { + return; + } + + struct wlr_vk_renderer *renderer = vulkan_get_renderer(wlr_renderer); + renderer->frame_render_buffers.size = 0; + renderer->frame_sampled_buffers.size = 0; +} + static bool buffer_export_sync_file(struct wlr_vk_renderer *renderer, struct wlr_buffer *buffer, uint32_t flags, int sync_file_fds[static WLR_DMABUF_MAX_PLANES]) { struct wlr_dmabuf_attributes dmabuf = {0}; @@ -1260,6 +1482,9 @@ static void vulkan_destroy(struct wlr_renderer *wlr_renderer) { return; } + waylib_vk_renderer_abort_texture_sync_batch(wlr_renderer); + waylib_vk_renderer_abort_texture_barrier_batch(wlr_renderer); + VkResult res = vkDeviceWaitIdle(renderer->dev->dev); if (res != VK_SUCCESS) { wlr_vk_error("vkDeviceWaitIdle", res); @@ -1340,6 +1565,27 @@ static void vulkan_destroy(struct wlr_renderer *wlr_renderer) { vkFreeMemory(dev->dev, renderer->dummy3d_mem, NULL); vkDestroySemaphore(dev->dev, renderer->timeline_semaphore, NULL); + + struct wlr_vk_texture_sync_sem *texture_sync_sem; + wl_array_for_each(texture_sync_sem, &renderer->texture_sync_semaphores) { + if (texture_sync_sem->semaphore != VK_NULL_HANDLE) { + vkDestroySemaphore(dev->dev, texture_sync_sem->semaphore, NULL); + } + } + wl_array_release(&renderer->texture_sync_semaphores); + wl_array_release(&renderer->texture_sync_pending); + wl_array_release(&renderer->texture_sync_wait_infos); + wl_array_release(&renderer->texture_acquire_barriers); + wl_array_release(&renderer->texture_release_barriers); + wl_array_release(&renderer->texture_acquire_textures); + wl_array_release(&renderer->texture_release_textures); + wl_array_release(&renderer->frame_sampled_buffers); + wl_array_release(&renderer->frame_render_buffers); + + if (renderer->texture_sync_timeline_semaphore != VK_NULL_HANDLE) { + vkDestroySemaphore(dev->dev, renderer->texture_sync_timeline_semaphore, NULL); + } + vkDestroyPipelineLayout(dev->dev, renderer->output_pipe_layout, NULL); vkDestroyDescriptorSetLayout(dev->dev, renderer->output_ds_srgb_layout, NULL); vkDestroyDescriptorSetLayout(dev->dev, renderer->output_ds_lut3d_layout, NULL); @@ -2483,7 +2729,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, @@ -2597,7 +2844,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, @@ -2683,6 +2931,17 @@ struct wlr_renderer *vulkan_renderer_create_for_device(struct wlr_vk_device *dev wl_list_init(&renderer->render_buffers); wl_list_init(&renderer->color_transforms); wl_list_init(&renderer->pipeline_layouts); + wl_array_init(&renderer->texture_sync_semaphores); + wl_array_init(&renderer->texture_sync_pending); + wl_array_init(&renderer->texture_sync_wait_infos); + wl_array_init(&renderer->texture_acquire_barriers); + wl_array_init(&renderer->texture_release_barriers); + wl_array_init(&renderer->texture_acquire_textures); + wl_array_init(&renderer->texture_release_textures); + wl_array_init(&renderer->frame_sampled_buffers); + wl_array_init(&renderer->frame_render_buffers); + renderer->texture_sync_force_poll = getenv("WLR_VK_FORCE_SYNC_POLL") != NULL; + renderer->stage_force_block = getenv("WLR_VK_FORCE_STAGE_BLOCK") != NULL; renderer->wlr_renderer.color_encodings = WLR_COLOR_ENCODING_BT601 | @@ -2726,6 +2985,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: @@ -2788,3 +3067,88 @@ uint32_t wlr_vk_renderer_get_queue_family(struct wlr_renderer *renderer) { struct wlr_vk_renderer *vk_renderer = vulkan_get_renderer(renderer); return vk_renderer->dev->queue_family; } + +VkQueue waylib_vk_renderer_get_queue(struct wlr_renderer *renderer) { + struct wlr_vk_renderer *vk_renderer = vulkan_get_renderer(renderer); + return vk_renderer->dev->queue; +} + +bool waylib_vk_renderer_has_separate_depth_stencil_layouts( + struct wlr_renderer *renderer) { + if (renderer == NULL || !wlr_renderer_is_vk(renderer)) { + return false; + } + + struct wlr_vk_renderer *vk_renderer = vulkan_get_renderer(renderer); + return vk_renderer->dev->separate_depth_stencil_layouts; +} + +bool waylib_vk_renderer_restrict_texture_formats(struct wlr_renderer *wlr_renderer, + const uint32_t *drm_formats, size_t count) { + if (wlr_renderer == NULL || !wlr_renderer_is_vk(wlr_renderer) + || drm_formats == NULL || count == 0) { + return false; + } + + struct wlr_vk_renderer *renderer = vulkan_get_renderer(wlr_renderer); + + // The texture format sets are only consulted while advertising the + // formats to clients (linux-dmabuf and wl_shm); rebuilding them in place + // before any client connects cannot race with anything. Build both + // restricted sets first and commit them together: a failure halfway + // through must not leave one set restricted and the other untouched. + struct wlr_drm_format_set *const sets[] = { + &renderer->dev->dmabuf_texture_formats, + &renderer->dev->shm_texture_formats, + }; + const char *const names[] = { "dmabuf", "shm" }; + struct wlr_drm_format_set filtered[2] = {0}; + size_t original_count[2] = {0}; + size_t kept[2] = {0}; + + for (size_t s = 0; s < 2; s++) { + const struct wlr_drm_format_set *set = sets[s]; + original_count[s] = set->len; + for (size_t i = 0; i < original_count[s]; i++) { + const struct wlr_drm_format *format = &set->formats[i]; + bool supported = false; + for (size_t f = 0; f < count; f++) { + if (drm_formats[f] == format->format) { + supported = true; + break; + } + } + if (!supported) { + wlr_log(WLR_DEBUG, "Restricting %s texture format %.4s: " + "the compositor cannot wrap it", + names[s], (const char *)&format->format); + continue; + } + for (size_t m = 0; m < format->len; m++) { + if (!wlr_drm_format_set_add(&filtered[s], format->format, + format->modifiers[m])) { + wlr_log(WLR_ERROR, "Failed to rebuild the restricted " + "%s texture format set", names[s]); + goto error; + } + } + kept[s]++; + } + } + + for (size_t s = 0; s < 2; s++) { + wlr_drm_format_set_finish(sets[s]); + *sets[s] = filtered[s]; + filtered[s] = (struct wlr_drm_format_set){0}; + wlr_log(WLR_INFO, "Restricted the %s texture format set to the " + "compositor-wrappable subset (%zu/%zu formats kept)", + names[s], kept[s], original_count[s]); + } + return true; + +error: + for (size_t s = 0; s < 2; s++) { + wlr_drm_format_set_finish(&filtered[s]); + } + return false; +} diff --git a/3rdparty/wlroots/render/vulkan/texture.c b/3rdparty/wlroots/render/vulkan/texture.c index 57db97a567..782f917283 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 @@ -8,10 +11,12 @@ #include #include #include +#include #include #include #include "render/pixel_format.h" #include "render/vulkan.h" +#include "util/time.h" static const struct wlr_texture_impl texture_impl; @@ -186,6 +191,19 @@ void vulkan_texture_destroy(struct wlr_vk_texture *texture) { texture->buffer = NULL; } + // Defensive unlink from the foreign-texture list: wlroots' render pass + // submits that fail before its drain loop strand entries there, and + // destroying the texture's buffer through that path would leave a dangling + // node that the next successful submit's drain would touch (use-after-free). + // pass.c inserts only when both link fields are NULL; self-linking marks + // "not inserted" consistently for the wl_list_empty() checks elsewhere. + if (texture->foreign_link.next != NULL && + !wl_list_empty(&texture->foreign_link)) { + wl_list_remove(&texture->foreign_link); + } + texture->foreign_link.prev = &texture->foreign_link; + texture->foreign_link.next = &texture->foreign_link; + // when we recorded a command to fill this image _this_ frame, // it has to be executed before the texture can be destroyed. // Add it to the renderer->destroy_textures list, destroying @@ -237,8 +255,32 @@ static bool vulkan_texture_read_pixels(struct wlr_texture *wlr_texture, void *p = wlr_texture_read_pixel_options_get_data(options); - return vulkan_read_pixels(texture->renderer, texture->format->vk, texture->image, - options->format, options->stride, src.width, src.height, src.x, src.y, 0, 0, p); + // A DMA-BUF import is FOREIGN-owned between uses and may carry an + // unsignaled producer fence, so every read must go through the readback + // gate (producer fence wait + queue-family ownership acquire + matching + // release). Callers that already hold the gate around this read + // (WBufferDumper) get a NOOP and record their own release; SHM/pixel + // textures are a NOOP too. + const enum wlr_vk_texture_readback_state state = + waylib_vk_texture_begin_readback(&texture->renderer->wlr_renderer, + wlr_texture); + if (state == WLR_VK_TEXTURE_READBACK_ERROR) { + wlr_log(WLR_ERROR, "vulkan_read_pixels: DMA-BUF readback acquire " + "failed, refusing to read foreign-owned memory"); + return false; + } + const bool acquired = state == WLR_VK_TEXTURE_READBACK_ACQUIRED; + + const bool ok = vulkan_read_pixels(texture->renderer, texture->format->vk, + texture->image, options->format, options->stride, src.width, src.height, + src.x, src.y, 0, 0, p); + + if (acquired) { + waylib_vk_texture_end_readback(&texture->renderer->wlr_renderer, + wlr_texture); + } + + return ok; } static uint32_t vulkan_texture_preferred_read_format(struct wlr_texture *wlr_texture) { @@ -504,10 +546,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); @@ -547,6 +593,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 @@ -568,7 +622,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; @@ -619,6 +673,9 @@ VkImage vulkan_import_dmabuf(struct wlr_vk_renderer *renderer, wlr_vk_error("vkCreateImage", res); return VK_NULL_HANDLE; } + if (image_usage != NULL) { + *image_usage = usage; + } unsigned mem_count = disjoint ? plane_count : 1u; VkBindImageMemoryInfo bindi[WLR_DMABUF_MAX_PLANES] = {0}; @@ -752,7 +809,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,11 +887,1271 @@ void wlr_vk_texture_get_image_attribs(struct wlr_texture *texture, struct wlr_vk_texture *vk_texture = vulkan_get_texture(texture); attribs->image = vk_texture->image; attribs->format = vk_texture->format->vk; - attribs->layout = vk_texture->transitioned ? - VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL : VK_IMAGE_LAYOUT_GENERAL; + // For DMA-BUF textures the actual layout follows the acquire/release + // ownership ping-pong: owned textures are transitioned to + // SHADER_READ_ONLY_OPTIMAL by the acquire barrier and released back to + // GENERAL afterwards, so "transitioned" alone would report a stale layout. + // Pixel (non-DMA-BUF) textures keep the permanent post-upload layout. + // qt_sampling_acquired is the Qt-path owner of that ping-pong; 'owned' + // is kept for the wlroots render-pass bookkeeping only (see header). + attribs->layout = vk_texture->dmabuf_imported + ? ((vk_texture->owned || vk_texture->qt_sampling_acquired) + ? VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL + : VK_IMAGE_LAYOUT_GENERAL) + : (vk_texture->transitioned + ? VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL + : VK_IMAGE_LAYOUT_GENERAL); + attribs->usage = vk_texture->dmabuf_imported ? + vulkan_dma_tex_usage : vulkan_shm_tex_usage; } bool wlr_vk_texture_has_alpha(struct wlr_texture *texture) { struct wlr_vk_texture *vk_texture = vulkan_get_texture(texture); return vk_texture->has_alpha; } + +static void release_completed_stage_buffers(struct wlr_vk_renderer *renderer) { + int64_t now = get_current_time_msec(); + struct wlr_vk_shared_buffer *buf; + wl_list_for_each(buf, &renderer->stage.buffers, link) { + if (buf->allocs.size == 0) { + continue; + } + + buf->allocs.size = 0; + buf->last_used_ms = now; + } +} + +static bool submit_pending_stage_uploads(struct wlr_vk_renderer *renderer) { + if (renderer->stage.cb == NULL) { + return true; + } + + // The GPU-side asynchronous path is only safe when the consumer submits + // to the same queue (see waylib_vk_renderer_set_stage_async_enabled). Fall + // back to the legacy blocking path otherwise, or when explicitly forced. + if (!renderer->stage_async_enabled || renderer->stage_force_block) { + static bool logged_blocking_stage; + if (!logged_blocking_stage) { + logged_blocking_stage = true; + wlr_log(WLR_INFO, "vk-stage: using blocking staging uploads " + "(async path %s)", renderer->stage_force_block + ? "forced off via WLR_VK_FORCE_STAGE_BLOCK" + : "not enabled"); + } + if (!vulkan_submit_stage_wait(renderer)) { + wlr_log(WLR_ERROR, "Failed to submit pending Vulkan texture stage upload"); + return false; + } + + release_completed_stage_buffers(renderer); + return true; + } + + // GPU-side asynchronous path: submit the staging command buffer without + // blocking the render thread. The staging spans are reclaimed once the + // submission's timeline point is reached (see vulkan_submit_stage_async). + if (!vulkan_submit_stage_async(renderer)) { + wlr_log(WLR_ERROR, "Failed to submit pending Vulkan texture stage upload"); + return false; + } + + return true; +} + +static void close_sync_file_fds(int sync_file_fds[static WLR_DMABUF_MAX_PLANES]) { + for (size_t i = 0; i < WLR_DMABUF_MAX_PLANES; i++) { + if (sync_file_fds[i] < 0) { + continue; + } + + close(sync_file_fds[i]); + sync_file_fds[i] = -1; + } +} + +struct wlr_vk_texture_sync_wait { + int fd; + size_t semaphore_index; +}; + +// Returns 1 when signaled, 0 on timeout, and -1 on an error or invalid poll +// result. A sync_file is only considered ready when POLLIN is reported. +static int poll_sync_file(int fd, int timeout_ms) { + struct pollfd pollfd = { + .fd = fd, + .events = POLLIN, + }; + + int ret; + do { + ret = poll(&pollfd, 1, timeout_ms); + } while (ret < 0 && errno == EINTR); + + if (ret < 0) { + wlr_log_errno(WLR_ERROR, "Failed to poll texture DMA-BUF sync_file"); + return -1; + } + if (ret == 0) { + return 0; + } + if ((pollfd.revents & (POLLERR | POLLNVAL)) != 0 || + (pollfd.revents & POLLIN) == 0) { + wlr_log(WLR_ERROR, "Invalid texture DMA-BUF sync_file poll result: 0x%x", + pollfd.revents); + return -1; + } + + return 1; +} + +static bool wait_sync_file_fds(int sync_file_fds[static WLR_DMABUF_MAX_PLANES]) { + bool ok = true; + + for (size_t i = 0; i < WLR_DMABUF_MAX_PLANES; i++) { + int fd = sync_file_fds[i]; + if (fd < 0) { + continue; + } + + int ret = poll_sync_file(fd, 1000); + if (ret == 0) { + wlr_log(WLR_ERROR, "Timed out while waiting for texture DMA-BUF sync_file"); + ok = false; + } else if (ret < 0) { + ok = false; + } + + close(fd); + sync_file_fds[i] = -1; + } + + return ok; +} + +static bool collect_sync_file_fds(struct wlr_vk_renderer *renderer, + int sync_file_fds[static WLR_DMABUF_MAX_PLANES]) { + size_t count = 0; + for (size_t i = 0; i < WLR_DMABUF_MAX_PLANES; i++) { + count += sync_file_fds[i] >= 0; + } + if (count == 0) { + return true; + } + + struct wlr_vk_texture_sync_wait *waits = wl_array_add( + &renderer->texture_sync_pending, count * sizeof(*waits)); + if (waits == NULL) { + close_sync_file_fds(sync_file_fds); + return false; + } + + size_t wait_index = 0; + for (size_t i = 0; i < WLR_DMABUF_MAX_PLANES; i++) { + if (sync_file_fds[i] < 0) { + continue; + } + + waits[wait_index++] = (struct wlr_vk_texture_sync_wait) { + .fd = sync_file_fds[i], + .semaphore_index = SIZE_MAX, + }; + sync_file_fds[i] = -1; + } + + return true; +} + +void waylib_vk_renderer_abort_texture_sync_batch(struct wlr_renderer *wlr_renderer) { + if (wlr_renderer == NULL || !wlr_renderer_is_vk(wlr_renderer)) { + return; + } + + struct wlr_vk_renderer *renderer = vulkan_get_renderer(wlr_renderer); + struct wlr_vk_texture_sync_wait *wait; + wl_array_for_each(wait, &renderer->texture_sync_pending) { + if (wait->fd >= 0) { + close(wait->fd); + wait->fd = -1; + } + } + + renderer->texture_sync_pending.size = 0; + renderer->texture_sync_wait_infos.size = 0; + renderer->texture_sync_batch_active = false; +} + +bool waylib_vk_renderer_begin_texture_sync_batch(struct wlr_renderer *wlr_renderer) { + if (wlr_renderer == NULL || !wlr_renderer_is_vk(wlr_renderer)) { + return false; + } + + struct wlr_vk_renderer *renderer = vulkan_get_renderer(wlr_renderer); + if (renderer->texture_sync_batch_active) { + wlr_log(WLR_ERROR, "Vulkan texture sync batch is already active"); + waylib_vk_renderer_abort_texture_sync_batch(wlr_renderer); + return false; + } + if (renderer->texture_sync_pending.size != 0) { + wlr_log(WLR_ERROR, "Discarding stale Vulkan texture sync batch entries"); + waylib_vk_renderer_abort_texture_sync_batch(wlr_renderer); + } + + renderer->texture_sync_batch_active = true; + return true; +} + +static bool wait_texture_sync_entries(struct wlr_vk_texture_sync_wait *waits, + size_t count) { + bool ok = true; + for (size_t i = 0; i < count; i++) { + if (waits[i].fd < 0) { + continue; + } + + int ret = poll_sync_file(waits[i].fd, 1000); + if (ret == 0) { + wlr_log(WLR_ERROR, "Timed out while waiting for texture DMA-BUF sync_file"); + ok = false; + } else if (ret < 0) { + ok = false; + } + + close(waits[i].fd); + waits[i].fd = -1; + } + return ok; +} + +static struct wlr_vk_texture_sync_sem *texture_sync_sem_at( + struct wlr_vk_renderer *renderer, size_t index) { + size_t count = renderer->texture_sync_semaphores.size / + sizeof(struct wlr_vk_texture_sync_sem); + assert(index < count); + return &((struct wlr_vk_texture_sync_sem *) + renderer->texture_sync_semaphores.data)[index]; +} + +// Returns an index instead of a pointer because wl_array_add() may move the +// backing allocation while multiple semaphores are claimed for one batch. +static bool acquire_texture_sync_sem(struct wlr_vk_renderer *renderer, + uint64_t cur_point, size_t *index) { + struct wlr_vk_texture_sync_sem *sems = renderer->texture_sync_semaphores.data; + size_t count = renderer->texture_sync_semaphores.size / sizeof(*sems); + for (size_t i = 0; i < count; i++) { + if (sems[i].release_point <= cur_point) { + if (sems[i].semaphore == VK_NULL_HANDLE) { + // Reclaimed slot from a failed bridge submit (see + // discard_texture_sync_sems): rebuild it here. + VkSemaphoreCreateInfo semaphore_info = { + .sType = VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO, + }; + VkResult cres = vkCreateSemaphore(renderer->dev->dev, + &semaphore_info, NULL, &sems[i].semaphore); + if (cres != VK_SUCCESS) { + wlr_vk_error("vkCreateSemaphore", cres); + return false; + } + } + sems[i].release_point = UINT64_MAX; + *index = i; + return true; + } + } + + VkSemaphoreCreateInfo semaphore_info = { + .sType = VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO, + }; + VkSemaphore semaphore; + VkResult res = vkCreateSemaphore(renderer->dev->dev, &semaphore_info, + NULL, &semaphore); + if (res != VK_SUCCESS) { + wlr_vk_error("vkCreateSemaphore", res); + return false; + } + + struct wlr_vk_texture_sync_sem *sem = wl_array_add( + &renderer->texture_sync_semaphores, sizeof(*sem)); + if (sem == NULL) { + vkDestroySemaphore(renderer->dev->dev, semaphore, NULL); + return false; + } + + *sem = (struct wlr_vk_texture_sync_sem) { + .semaphore = semaphore, + .release_point = UINT64_MAX, + }; + *index = count; + return true; +} + +static void release_claimed_texture_sync_sems(struct wlr_vk_renderer *renderer, + struct wlr_vk_texture_sync_wait *waits, size_t count) { + for (size_t i = 0; i < count; i++) { + if (waits[i].semaphore_index == SIZE_MAX) { + continue; + } + texture_sync_sem_at(renderer, waits[i].semaphore_index)->release_point = 0; + waits[i].semaphore_index = SIZE_MAX; + } +} + +// A failed bridge submit can leave the claimed semaphores holding a consumed +// temporary sync_fd import (or a submitted-wait whose execution never started, +// depending on where the submit failed). Any of those states makes the binary +// semaphore unusable for a later re-import, while its slot would otherwise be +// stuck at release_point=UINT64_MAX forever. Destroy and NULL the semaphores; +// acquire_texture_sync_sem() rebuilds reclaimed slots. All these semaphores +// have no pending wait (the submit that would wait on them failed before +// executing), so destruction is valid. +static void discard_texture_sync_sems(struct wlr_vk_renderer *renderer, + struct wlr_vk_texture_sync_wait *waits, size_t count) { + for (size_t i = 0; i < count; i++) { + if (waits[i].semaphore_index == SIZE_MAX) { + continue; + } + struct wlr_vk_texture_sync_sem *sem = + texture_sync_sem_at(renderer, waits[i].semaphore_index); + if (sem->semaphore != VK_NULL_HANDLE) { + vkDestroySemaphore(renderer->dev->dev, sem->semaphore, NULL); + sem->semaphore = VK_NULL_HANDLE; + } + sem->release_point = 0; + waits[i].semaphore_index = SIZE_MAX; + } +} + +static bool submit_texture_sync_bridge(struct wlr_vk_renderer *renderer, + struct wlr_vk_texture_sync_wait *waits, + VkSemaphoreSubmitInfoKHR *wait_infos, size_t wait_count) { + if (wait_count == 0 && !renderer->stage_async_needs_bridge) { + return true; + } + if (renderer->dev->api.vkQueueSubmit2KHR == NULL) { + wlr_log(WLR_ERROR, "Vulkan texture sync bridge requires vkQueueSubmit2KHR"); + return false; + } + if (wait_count > UINT32_MAX) { + wlr_log(WLR_ERROR, "Too many Vulkan texture sync waits in one batch: %zu", + wait_count); + return false; + } + if (wait_count > 0 && renderer->texture_sync_timeline_point == UINT64_MAX) { + wlr_log(WLR_ERROR, "Vulkan texture sync timeline value exhausted"); + return false; + } + + struct wlr_vk_command_buffer *cb = vulkan_acquire_command_buffer(renderer); + if (cb == NULL) { + return false; + } + + VkCommandBufferBeginInfo begin_info = { + .sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO, + }; + VkResult res = vkBeginCommandBuffer(cb->vk, &begin_info); + if (res != VK_SUCCESS) { + wlr_vk_error("vkBeginCommandBuffer", res); + vulkan_reset_command_buffer(cb); + return false; + } + + // Submission order alone is not an execution dependency. Chain the waits + // (and any earlier asynchronous stage upload) through a real pipeline + // barrier whose second scope includes the later Qt/QRhi submission. + VkMemoryBarrier memory_barrier = { + .sType = VK_STRUCTURE_TYPE_MEMORY_BARRIER, + .srcAccessMask = VK_ACCESS_MEMORY_WRITE_BIT, + .dstAccessMask = VK_ACCESS_MEMORY_READ_BIT | + VK_ACCESS_MEMORY_WRITE_BIT, + }; + vkCmdPipelineBarrier(cb->vk, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, + VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, 0, + 1, &memory_barrier, 0, NULL, 0, NULL); + + uint64_t renderer_point = vulkan_end_command_buffer(cb, renderer); + if (renderer_point == 0) { + vulkan_reset_command_buffer(cb); + return false; + } + + VkCommandBufferSubmitInfoKHR cb_info = { + .sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_SUBMIT_INFO_KHR, + .commandBuffer = cb->vk, + }; + VkSemaphoreSubmitInfoKHR signal_infos[2] = {{ + .sType = VK_STRUCTURE_TYPE_SEMAPHORE_SUBMIT_INFO_KHR, + .semaphore = renderer->timeline_semaphore, + .value = renderer_point, + .stageMask = VK_PIPELINE_STAGE_2_ALL_COMMANDS_BIT, + }}; + uint32_t signal_count = 1; + uint64_t texture_sync_point = 0; + if (wait_count > 0) { + texture_sync_point = renderer->texture_sync_timeline_point + 1; + signal_infos[signal_count++] = (VkSemaphoreSubmitInfoKHR) { + .sType = VK_STRUCTURE_TYPE_SEMAPHORE_SUBMIT_INFO_KHR, + .semaphore = renderer->texture_sync_timeline_semaphore, + .value = texture_sync_point, + .stageMask = VK_PIPELINE_STAGE_2_ALL_COMMANDS_BIT, + }; + } + + VkSubmitInfo2KHR submit = { + .sType = VK_STRUCTURE_TYPE_SUBMIT_INFO_2_KHR, + .waitSemaphoreInfoCount = (uint32_t)wait_count, + .pWaitSemaphoreInfos = wait_infos, + .commandBufferInfoCount = 1, + .pCommandBufferInfos = &cb_info, + .signalSemaphoreInfoCount = signal_count, + .pSignalSemaphoreInfos = signal_infos, + }; + res = renderer->dev->api.vkQueueSubmit2KHR(renderer->dev->queue, + 1, &submit, VK_NULL_HANDLE); + if (res != VK_SUCCESS) { + wlr_vk_error("vkQueueSubmit2KHR", res); + cb->timeline_point = 0; + vulkan_reset_command_buffer(cb); + return false; + } + + renderer->stage_async_needs_bridge = false; + if (wait_count > 0) { + renderer->texture_sync_timeline_point = texture_sync_point; + } + for (size_t i = 0; i < wait_count; i++) { + texture_sync_sem_at(renderer, waits[i].semaphore_index)->release_point = + texture_sync_point; + } + + wlr_log(WLR_DEBUG, "Vulkan texture sync: submitted GPU bridge for %zu " + "DMA-BUF sync_file(s), renderer timeline point %" PRIu64, + wait_count, renderer_point); + return true; +} + +static bool wait_texture_sync_entries_and_bridge( + struct wlr_vk_renderer *renderer, + struct wlr_vk_texture_sync_wait *waits, size_t wait_count) { + if (!wait_texture_sync_entries(waits, wait_count)) { + return false; + } + + // A CPU wait resolves the foreign sync_file dependency directly, but an + // earlier asynchronous staging upload still needs a same-queue barrier + // before Qt submits the sampling command buffer. + return submit_texture_sync_bridge(renderer, NULL, NULL, 0); +} + +bool waylib_vk_renderer_flush_texture_sync_batch(struct wlr_renderer *wlr_renderer) { + if (wlr_renderer == NULL || !wlr_renderer_is_vk(wlr_renderer)) { + return false; + } + + struct wlr_vk_renderer *renderer = vulkan_get_renderer(wlr_renderer); + if (!renderer->texture_sync_batch_active) { + // Staging uploads can outlive an aborted or disabled foreign-texture + // batch. Preserve their dependency before the next Qt submission. + return submit_texture_sync_bridge(renderer, NULL, NULL, 0); + } + + struct wlr_vk_texture_sync_wait *waits = renderer->texture_sync_pending.data; + size_t wait_count = renderer->texture_sync_pending.size / sizeof(*waits); + size_t pending_count = 0; + for (size_t i = 0; i < wait_count; i++) { + int ret = poll_sync_file(waits[i].fd, 0); + if (ret < 0) { + waylib_vk_renderer_abort_texture_sync_batch(wlr_renderer); + return false; + } + if (ret > 0) { + close(waits[i].fd); + waits[i].fd = -1; + continue; + } + + if (pending_count != i) { + waits[pending_count] = waits[i]; + waits[i].fd = -1; + } + pending_count++; + } + renderer->texture_sync_pending.size = pending_count * sizeof(*waits); + wait_count = pending_count; + + if (wait_count == 0) { + bool ok = submit_texture_sync_bridge(renderer, NULL, NULL, 0); + waylib_vk_renderer_abort_texture_sync_batch(wlr_renderer); + return ok; + } + if (wait_count > UINT32_MAX) { + bool ok = wait_texture_sync_entries_and_bridge( + renderer, waits, wait_count); + waylib_vk_renderer_abort_texture_sync_batch(wlr_renderer); + return ok; + } + + bool can_gpu_wait = renderer->texture_sync_timeline_semaphore != VK_NULL_HANDLE + && renderer->dev->api.vkImportSemaphoreFdKHR != NULL + && renderer->dev->api.vkQueueSubmit2KHR != NULL + && renderer->dev->api.vkGetSemaphoreCounterValueKHR != NULL + && !renderer->texture_sync_force_poll; + if (!can_gpu_wait) { + bool ok = wait_texture_sync_entries_and_bridge( + renderer, waits, wait_count); + waylib_vk_renderer_abort_texture_sync_batch(wlr_renderer); + return ok; + } + + uint64_t cur_point = 0; + VkResult res = renderer->dev->api.vkGetSemaphoreCounterValueKHR( + renderer->dev->dev, renderer->texture_sync_timeline_semaphore, &cur_point); + if (res != VK_SUCCESS) { + wlr_vk_error("vkGetSemaphoreCounterValueKHR", res); + bool ok = wait_texture_sync_entries_and_bridge( + renderer, waits, wait_count); + waylib_vk_renderer_abort_texture_sync_batch(wlr_renderer); + return ok; + } + + renderer->texture_sync_wait_infos.size = 0; + VkSemaphoreSubmitInfoKHR *wait_infos = wl_array_add( + &renderer->texture_sync_wait_infos, wait_count * sizeof(*wait_infos)); + if (wait_infos == NULL) { + bool ok = wait_texture_sync_entries_and_bridge( + renderer, waits, wait_count); + waylib_vk_renderer_abort_texture_sync_batch(wlr_renderer); + return ok; + } + + size_t claimed_count = 0; + for (; claimed_count < wait_count; claimed_count++) { + if (!acquire_texture_sync_sem(renderer, cur_point, + &waits[claimed_count].semaphore_index)) { + release_claimed_texture_sync_sems(renderer, waits, claimed_count); + bool ok = wait_texture_sync_entries_and_bridge( + renderer, waits, wait_count); + waylib_vk_renderer_abort_texture_sync_batch(wlr_renderer); + return ok; + } + } + + size_t imported_count = 0; + for (; imported_count < wait_count; imported_count++) { + struct wlr_vk_texture_sync_sem *sem = texture_sync_sem_at(renderer, + waits[imported_count].semaphore_index); + VkImportSemaphoreFdInfoKHR import_info = { + .sType = VK_STRUCTURE_TYPE_IMPORT_SEMAPHORE_FD_INFO_KHR, + .handleType = VK_EXTERNAL_SEMAPHORE_HANDLE_TYPE_SYNC_FD_BIT, + .flags = VK_SEMAPHORE_IMPORT_TEMPORARY_BIT, + .semaphore = sem->semaphore, + .fd = waits[imported_count].fd, + }; + res = renderer->dev->api.vkImportSemaphoreFdKHR(renderer->dev->dev, + &import_info); + if (res != VK_SUCCESS) { + wlr_vk_error("vkImportSemaphoreFdKHR", res); + break; + } + + waits[imported_count].fd = -1; // consumed by successful import + wait_infos[imported_count] = (VkSemaphoreSubmitInfoKHR) { + .sType = VK_STRUCTURE_TYPE_SEMAPHORE_SUBMIT_INFO_KHR, + .semaphore = sem->semaphore, + .stageMask = VK_PIPELINE_STAGE_2_ALL_COMMANDS_BIT, + }; + } + + if (imported_count < wait_count) { + release_claimed_texture_sync_sems(renderer, + &waits[imported_count], wait_count - imported_count); + bool gpu_ok = submit_texture_sync_bridge(renderer, waits, wait_infos, + imported_count); + if (!gpu_ok) { + discard_texture_sync_sems(renderer, waits, imported_count); + } + bool cpu_ok = wait_texture_sync_entries(&waits[imported_count], + wait_count - imported_count); + waylib_vk_renderer_abort_texture_sync_batch(wlr_renderer); + return gpu_ok && cpu_ok; + } + + bool ok = submit_texture_sync_bridge(renderer, waits, wait_infos, wait_count); + if (!ok) { + discard_texture_sync_sems(renderer, waits, wait_count); + } + waylib_vk_renderer_abort_texture_sync_batch(wlr_renderer); + return ok; +} + +static void fill_sampling_attribs(struct wlr_vk_texture *texture, + struct wlr_vk_image_attribs *attribs) { + if (attribs == NULL) { + return; + } + + attribs->image = texture->image; + attribs->format = texture->format->vk; + attribs->layout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; + attribs->usage = texture->dmabuf_imported ? + vulkan_dma_tex_usage : vulkan_shm_tex_usage; +} + +// Returns true when the texture's DMA-BUF was (or may have been) rendered as +// a color attachment by one of this renderer's recent command buffers. The +// producer accesses went through the render buffer's separate logical VkImage, +// which only aliases this texture's image in physical memory. +static bool is_frame_render_buffer(struct wlr_vk_renderer *renderer, + struct wlr_buffer *buffer) { + return vulkan_alias_list_contains(&renderer->frame_render_buffers, buffer); +} + +// Extends the color-attachment writes of an aliasing render-buffer producer +// to the sampling about to be recorded for this texture. Neither the foreign +// ownership acquire (its source access mask is ignored) nor an image barrier +// naming the sampled texture can cover accesses made through the producer's +// different logical image: §Memory Aliasing requires a memory dependency and +// only a global (resource-free) barrier spans both aliases. The mask pair is +// the same shape the texture-sync bridge uses for staged uploads. +static void record_frame_render_barrier(struct wlr_vk_renderer *renderer, + struct wlr_vk_texture *texture, VkCommandBuffer cb) { + if (texture->buffer == NULL + || !is_frame_render_buffer(renderer, texture->buffer)) { + return; + } + + bool deferred = false; + if (renderer->texture_barrier_batch_active + && !renderer->texture_barrier_batch_release) { + renderer->frame_producer_alias_dep = true; + deferred = true; + } + if (!deferred) { + VkMemoryBarrier memory_barrier = { + .sType = VK_STRUCTURE_TYPE_MEMORY_BARRIER, + .srcAccessMask = VK_ACCESS_MEMORY_WRITE_BIT, + .dstAccessMask = VK_ACCESS_MEMORY_READ_BIT, + }; + vkCmdPipelineBarrier(cb, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, + VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, 0, + 1, &memory_barrier, 0, NULL, 0, NULL); + } + + static bool logged_frame_producer_barrier; + if (!logged_frame_producer_barrier) { + logged_frame_producer_barrier = true; + wlr_log(WLR_DEBUG, "Vulkan texture sampling: recording a global " + "alias dependency for a texture whose DMA-BUF was also written " + "as a color attachment through another logical image"); + } +} + +bool waylib_vk_renderer_prepare_texture_for_sampling(struct wlr_renderer *wlr_renderer, + struct wlr_texture *wlr_texture, struct wlr_buffer *sampled_buffer, + VkCommandBuffer cb, struct wlr_vk_image_attribs *attribs) { + if (wlr_renderer == NULL || wlr_texture == NULL || cb == VK_NULL_HANDLE) { + return false; + } + if (!wlr_renderer_is_vk(wlr_renderer) || !wlr_texture_is_vk(wlr_texture)) { + return false; + } + + struct wlr_vk_renderer *renderer = vulkan_get_renderer(wlr_renderer); + struct wlr_vk_texture *texture = vulkan_get_texture(wlr_texture); + if (texture->renderer != renderer) { + wlr_log(WLR_ERROR, "Vulkan texture belongs to a different renderer"); + return false; + } + + if (!submit_pending_stage_uploads(renderer)) { + return false; + } + if (texture->dmabuf_imported && !texture->qt_sampling_acquired) { + if (texture->readback_acquired) { + // A readback acquire holds the foreign->own transition of this + // image. Qt must not sample it until that readback released it; + // by construction readbacks run outside Qt frames, so failing + // closed here only guards against future caller bugs. + wlr_log(WLR_ERROR, "Vulkan texture readback acquire still in " + "flight; refusing to sample it"); + return false; + } + + // Skip the producer fence export/wait when the client has not + // re-attached (committed) this buffer since its current content was + // already synchronized: there is no new producer write to wait for. + // Client textures are identified through the sampled client buffer: + // wlroots creates them from the client buffer's source (or keys them + // on the client buffer itself), and + // waylib_vk_renderer_mark_buffer_content_dirty() normalizes both + // keyings when the client re-attaches. Every other texture (e.g. an + // output swapchain buffer sampled for mirroring) keeps waiting on + // every acquire. + struct wlr_client_buffer *client_buffer = sampled_buffer != NULL + ? wlr_client_buffer_get(sampled_buffer) : NULL; + const bool client_texture = client_buffer != NULL + && (texture->buffer == client_buffer->source + || texture->buffer == sampled_buffer); + const bool skip_producer_wait = client_texture + && texture->qt_content_synced; + + if (texture->buffer != NULL && !skip_producer_wait) { + int sync_file_fds[WLR_DMABUF_MAX_PLANES]; + for (size_t i = 0; i < WLR_DMABUF_MAX_PLANES; i++) { + sync_file_fds[i] = -1; + } + + if (!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; + } + + if (client_texture) { + texture->qt_content_synced = true; + } + } + + // Record the sampling side of the alias pair before touching any + // barrier state: a later render-buffer acquire for the same + // wlr_buffer writes through another logical image and needs a WAR + // dependency against this sampling. Failing to track is handled + // fail-closed so the WAR barrier can never be silently lost. + if (texture->buffer != NULL + && !vulkan_alias_list_add(&renderer->frame_sampled_buffers, + texture->buffer)) { + wlr_log_errno(WLR_ERROR, + "Failed to track Vulkan sampled buffer for aliasing"); + return false; + } + + VkImageMemoryBarrier barrier = { + .sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER, + .srcQueueFamilyIndex = VK_QUEUE_FAMILY_FOREIGN_EXT, + .dstQueueFamilyIndex = renderer->dev->queue_family, + .image = texture->image, + .oldLayout = VK_IMAGE_LAYOUT_GENERAL, + .newLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, + .srcAccessMask = 0, + .dstAccessMask = VK_ACCESS_SHADER_READ_BIT, + .subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT, + .subresourceRange.layerCount = 1, + .subresourceRange.levelCount = 1, + }; + + bool deferred = false; + if (renderer->texture_barrier_batch_active + && !renderer->texture_barrier_batch_release) { + VkImageMemoryBarrier *slot = wl_array_add( + &renderer->texture_acquire_barriers, sizeof(barrier)); + struct wlr_vk_texture **track = slot != NULL ? wl_array_add( + &renderer->texture_acquire_textures, sizeof(*track)) : NULL; + if (slot != NULL && track != NULL) { + *slot = barrier; + *track = texture; + deferred = true; + } else { + // Never leave a deferred barrier without its texture + // bookkeeping: undo the partial adds and record it now. + if (track != NULL) { + renderer->texture_acquire_textures.size -= sizeof(*track); + } + if (slot != NULL) { + renderer->texture_acquire_barriers.size -= sizeof(barrier); + } + wlr_log_errno(WLR_ERROR, + "Failed to queue Vulkan texture acquire barrier"); + } + } + if (!deferred) { + // The acquire's second scope must cover every stage the consumer + // may sample from, not only the fragment stage. + vkCmdPipelineBarrier(cb, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, + VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, 0, + 0, NULL, 0, NULL, 1, &barrier); + } + + record_frame_render_barrier(renderer, texture, cb); + + texture->qt_sampling_acquired = true; + } + + texture->transitioned = true; + fill_sampling_attribs(texture, attribs); + return true; +} + +bool waylib_vk_renderer_finish_texture_sampling(struct wlr_renderer *wlr_renderer, + struct wlr_texture *wlr_texture, VkCommandBuffer cb) { + if (wlr_renderer == NULL || wlr_texture == NULL || cb == VK_NULL_HANDLE) { + return false; + } + if (!wlr_renderer_is_vk(wlr_renderer) || !wlr_texture_is_vk(wlr_texture)) { + return false; + } + + struct wlr_vk_renderer *renderer = vulkan_get_renderer(wlr_renderer); + struct wlr_vk_texture *texture = vulkan_get_texture(wlr_texture); + if (texture->renderer != renderer) { + wlr_log(WLR_ERROR, "Vulkan texture belongs to a different renderer"); + return false; + } + + if (!texture->dmabuf_imported || !texture->qt_sampling_acquired) { + return true; + } + + VkImageMemoryBarrier barrier = { + .sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER, + .srcQueueFamilyIndex = renderer->dev->queue_family, + .dstQueueFamilyIndex = VK_QUEUE_FAMILY_FOREIGN_EXT, + .image = texture->image, + .oldLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, + .newLayout = VK_IMAGE_LAYOUT_GENERAL, + .srcAccessMask = VK_ACCESS_SHADER_READ_BIT, + .dstAccessMask = 0, + .subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT, + .subresourceRange.layerCount = 1, + .subresourceRange.levelCount = 1, + }; + + bool deferred = false; + if (renderer->texture_barrier_batch_active + && renderer->texture_barrier_batch_release) { + VkImageMemoryBarrier *slot = wl_array_add( + &renderer->texture_release_barriers, sizeof(barrier)); + struct wlr_vk_texture **track = slot != NULL ? wl_array_add( + &renderer->texture_release_textures, sizeof(*track)) : NULL; + if (slot != NULL && track != NULL) { + *slot = barrier; + *track = texture; + deferred = true; + } else { + // Never leave a deferred barrier without its texture + // bookkeeping: undo the partial adds and record it now. + if (track != NULL) { + renderer->texture_release_textures.size -= sizeof(*track); + } + if (slot != NULL) { + renderer->texture_release_barriers.size -= sizeof(barrier); + } + wlr_log_errno(WLR_ERROR, + "Failed to queue Vulkan texture release barrier"); + } + } + if (!deferred) { + // The release's first scope must cover every stage that may still be + // reading the texture, not only the fragment stage. + vkCmdPipelineBarrier(cb, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, + VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT, 0, + 0, NULL, 0, NULL, 1, &barrier); + } + texture->qt_sampling_acquired = false; + return true; +} + +void waylib_vk_renderer_mark_buffer_content_dirty(struct wlr_renderer *wlr_renderer, + struct wlr_buffer *wlr_buffer) { + if (wlr_renderer == NULL || wlr_buffer == NULL + || !wlr_renderer_is_vk(wlr_renderer)) { + return; + } + + struct wlr_vk_renderer *renderer = vulkan_get_renderer(wlr_renderer); + // The caller passes the client buffer the surface re-attached. Textures + // created by wlroots from a client buffer import its source buffer + // (wlr_client_buffer_create() creates them from the source, not from the + // wlr_client_buffer), while textures created from the client buffer + // itself are keyed on it; clear every texture importing either identity. + struct wlr_client_buffer *client_buffer = wlr_client_buffer_get(wlr_buffer); + struct wlr_buffer *source = client_buffer != NULL + ? client_buffer->source : NULL; + struct wlr_vk_texture *texture; + wl_list_for_each(texture, &renderer->textures, link) { + if (texture->buffer == wlr_buffer + || (source != NULL && texture->buffer == source)) { + // The client re-attached a buffer it owns again: its producer may + // have written new content, so the next sampling acquire must wait + // for a fresh producer fence even though the previous content was + // already synchronized. + texture->qt_content_synced = false; + } + } +} + +// A deferred acquire/release barrier that is never recorded must not leave the +// texture's ownership bookkeeping claiming a transfer that did not happen. A +// dropped acquire leaves the image FOREIGN-owned (it must be re-acquired on +// the next use), while a dropped release leaves the graphics queue owning it +// (it must not be acquired again). Roll the per-texture flag back to match the +// image's real ownership before discarding the batch. +static void rollback_deferred_texture_barriers(struct wlr_vk_renderer *renderer) { + const size_t acquire_count = renderer->texture_acquire_textures.size / + sizeof(struct wlr_vk_texture *); + const size_t release_count = renderer->texture_release_textures.size / + sizeof(struct wlr_vk_texture *); + + struct wlr_vk_texture **texture; + wl_array_for_each(texture, &renderer->texture_acquire_textures) { + (*texture)->qt_sampling_acquired = false; + } + wl_array_for_each(texture, &renderer->texture_release_textures) { + (*texture)->qt_sampling_acquired = true; + } + + renderer->texture_acquire_textures.size = 0; + renderer->texture_release_textures.size = 0; + + if (acquire_count > 0 || release_count > 0) { + wlr_log(WLR_DEBUG, "Vulkan texture barrier batch rolled back: " + "%zu dropped acquire(s), %zu dropped release(s)", + acquire_count, release_count); + } +} + +bool waylib_vk_renderer_begin_texture_barrier_batch(struct wlr_renderer *wlr_renderer, + bool release) { + if (wlr_renderer == NULL || !wlr_renderer_is_vk(wlr_renderer)) { + return false; + } + + struct wlr_vk_renderer *renderer = vulkan_get_renderer(wlr_renderer); + if (renderer->texture_barrier_batch_active) { + wlr_log(WLR_ERROR, "Vulkan texture barrier batch is already active"); + waylib_vk_renderer_abort_texture_barrier_batch(wlr_renderer); + } + + renderer->texture_barrier_batch_active = true; + renderer->texture_barrier_batch_release = release; + return true; +} + +bool waylib_vk_renderer_flush_texture_barrier_batch(struct wlr_renderer *wlr_renderer, + VkCommandBuffer cb) { + if (wlr_renderer == NULL || !wlr_renderer_is_vk(wlr_renderer)) { + return false; + } + + struct wlr_vk_renderer *renderer = vulkan_get_renderer(wlr_renderer); + if (!renderer->texture_barrier_batch_active) { + return true; + } + + // Test-only fault injection (one-shot): WAYLIB_VK_TEST_FLUSH_FAIL_AT= + // makes the n-th barrier batch flush of this process behave as if no + // command buffer was available, so the rollback paths can be exercised on + // a live compositor. Unset in normal operation; drop before upstreaming. + { + static long inject_fail_at = -1; + if (inject_fail_at < 0) { + const char *env = getenv("WAYLIB_VK_TEST_FLUSH_FAIL_AT"); + inject_fail_at = env != NULL ? strtol(env, NULL, 10) : 0; + if (inject_fail_at < 0) { + inject_fail_at = 0; + } + } + if (inject_fail_at > 0) { + static long flush_count; + if (++flush_count == inject_fail_at) { + wlr_log(WLR_INFO, "WAYLIB_VK_TEST_FLUSH_FAIL_AT: injecting a " + "one-shot Vulkan texture barrier batch flush failure"); + cb = VK_NULL_HANDLE; + } + } + } + + const bool release = renderer->texture_barrier_batch_release; + struct wl_array *barriers = release + ? &renderer->texture_release_barriers + : &renderer->texture_acquire_barriers; + const size_t count = barriers->size / sizeof(VkImageMemoryBarrier); + + if (cb == VK_NULL_HANDLE && (count > 0 || renderer->frame_producer_alias_dep)) { + // Without a command buffer nothing can be recorded; discarding the + // batch silently would desynchronize qt_sampling_acquired from the + // image's real ownership. Roll the batch back and report the failure. + wlr_log(WLR_ERROR, "Cannot record the pending Vulkan texture %s " + "barrier batch without a command buffer; rolling it back", + release ? "release" : "acquire"); + rollback_deferred_texture_barriers(renderer); + renderer->texture_acquire_barriers.size = 0; + renderer->texture_release_barriers.size = 0; + renderer->frame_producer_alias_dep = false; + renderer->texture_barrier_batch_active = false; + renderer->texture_barrier_batch_release = false; + return false; + } + + if (cb != VK_NULL_HANDLE && count > 0) { + VkPipelineStageFlags src_stage = release + ? VK_PIPELINE_STAGE_ALL_COMMANDS_BIT + : VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT; + VkPipelineStageFlags dst_stage = release + ? VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT + : VK_PIPELINE_STAGE_ALL_COMMANDS_BIT; + vkCmdPipelineBarrier(cb, src_stage, dst_stage, 0, + 0, NULL, 0, NULL, (uint32_t)count, barriers->data); + } + + // Same-frame alias producer dependencies are only queued during the + // acquire (pre-draw) phase and must be recorded after the + // ownership-transfer acquire barriers above. A global memory barrier is + // the only form whose access scope spans the producer's separate logical + // image and this sampling (§Memory Aliasing). + if (cb != VK_NULL_HANDLE && !release && renderer->frame_producer_alias_dep) { + VkMemoryBarrier memory_barrier = { + .sType = VK_STRUCTURE_TYPE_MEMORY_BARRIER, + .srcAccessMask = VK_ACCESS_MEMORY_WRITE_BIT, + .dstAccessMask = VK_ACCESS_MEMORY_READ_BIT, + }; + vkCmdPipelineBarrier(cb, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, + VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, 0, + 1, &memory_barrier, 0, NULL, 0, NULL); + } + + renderer->texture_acquire_barriers.size = 0; + renderer->texture_release_barriers.size = 0; + renderer->texture_acquire_textures.size = 0; + renderer->texture_release_textures.size = 0; + renderer->frame_producer_alias_dep = false; + renderer->texture_barrier_batch_active = false; + renderer->texture_barrier_batch_release = false; + return true; +} + +void waylib_vk_renderer_abort_texture_barrier_batch(struct wlr_renderer *wlr_renderer) { + if (wlr_renderer == NULL || !wlr_renderer_is_vk(wlr_renderer)) { + return; + } + + struct wlr_vk_renderer *renderer = vulkan_get_renderer(wlr_renderer); + rollback_deferred_texture_barriers(renderer); + renderer->texture_acquire_barriers.size = 0; + renderer->texture_release_barriers.size = 0; + renderer->frame_producer_alias_dep = false; + renderer->texture_barrier_batch_active = false; + renderer->texture_barrier_batch_release = false; +} + +// Submit a FOREIGN<->own queue-family ownership barrier (layout untouched, +// GENERAL, as every DMA-BUF import path in this renderer keeps it) on its own +// command buffer. acquire=true transfers ownership from the foreign producer +// to the graphics queue, permitting transfer-read and shader-read of the +// imported memory (which starts in an undefined layout from Vulkan's view); +// acquire=false releases it back. The later read_pixels() staging command +// buffer is submitted afterwards on the same queue: same-queue submission +// order provides the execution dependency between these two batches. +static bool submit_readback_barrier(struct wlr_vk_renderer *renderer, + VkImage image, bool acquire) { + VkResult res; + struct wlr_vk_command_buffer *cb = vulkan_acquire_command_buffer(renderer); + if (cb == NULL) { + return false; + } + + VkCommandBufferBeginInfo begin_info = { + .sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO, + }; + res = vkBeginCommandBuffer(cb->vk, &begin_info); + if (res != VK_SUCCESS) { + wlr_vk_error("vkBeginCommandBuffer", res); + vulkan_reset_command_buffer(cb); + return false; + } + + const VkAccessFlags read_accesses = VK_ACCESS_TRANSFER_READ_BIT + | VK_ACCESS_SHADER_READ_BIT; + VkImageMemoryBarrier barrier = { + .sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER, + .image = image, + .oldLayout = VK_IMAGE_LAYOUT_GENERAL, + .newLayout = VK_IMAGE_LAYOUT_GENERAL, + .subresourceRange = { + .aspectMask = VK_IMAGE_ASPECT_COLOR_BIT, + .baseMipLevel = 0, + .levelCount = 1, + .baseArrayLayer = 0, + .layerCount = 1, + }, + }; + VkPipelineStageFlags src_stage, dst_stage; + if (acquire) { + barrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_FOREIGN_EXT; + barrier.dstQueueFamilyIndex = renderer->dev->queue_family; + barrier.srcAccessMask = 0; + barrier.dstAccessMask = read_accesses; + src_stage = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT; + dst_stage = VK_PIPELINE_STAGE_TRANSFER_BIT + | VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT; + } else { + barrier.srcQueueFamilyIndex = renderer->dev->queue_family; + barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_FOREIGN_EXT; + barrier.srcAccessMask = read_accesses; + barrier.dstAccessMask = 0; + src_stage = VK_PIPELINE_STAGE_TRANSFER_BIT + | VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT; + dst_stage = VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT; + } + + vkCmdPipelineBarrier(cb->vk, src_stage, dst_stage, 0, 0, NULL, 0, NULL, + 1, &barrier); + + uint64_t timeline_point = vulkan_end_command_buffer(cb, renderer); + if (timeline_point == 0) { + vulkan_reset_command_buffer(cb); + return false; + } + + VkTimelineSemaphoreSubmitInfoKHR timeline_submit_info = { + .sType = VK_STRUCTURE_TYPE_TIMELINE_SEMAPHORE_SUBMIT_INFO_KHR, + .signalSemaphoreValueCount = 1, + .pSignalSemaphoreValues = &timeline_point, + }; + VkSubmitInfo submit_info = { + .sType = VK_STRUCTURE_TYPE_SUBMIT_INFO, + .pNext = &timeline_submit_info, + .commandBufferCount = 1, + .pCommandBuffers = &cb->vk, + .signalSemaphoreCount = 1, + .pSignalSemaphores = &renderer->timeline_semaphore, + }; + res = vkQueueSubmit(renderer->dev->queue, 1, &submit_info, VK_NULL_HANDLE); + if (res != VK_SUCCESS) { + wlr_vk_error("vkQueueSubmit", res); + cb->timeline_point = 0; + vulkan_reset_command_buffer(cb); + return false; + } + + wlr_log(WLR_DEBUG, "Vulkan readback: submitted %s command buffer " + "timeline_point=%"PRIu64, acquire ? "acquire" : "release", + timeline_point); + return true; +} + +enum wlr_vk_texture_readback_state waylib_vk_texture_begin_readback( + struct wlr_renderer *wlr_renderer, struct wlr_texture *wlr_texture) { + if (wlr_renderer == NULL || !wlr_renderer_is_vk(wlr_renderer) + || wlr_texture == NULL || !wlr_texture_is_vk(wlr_texture)) { + return WLR_VK_TEXTURE_READBACK_ERROR; + } + + struct wlr_vk_renderer *renderer = vulkan_get_renderer(wlr_renderer); + struct wlr_vk_texture *texture = vulkan_get_texture(wlr_texture); + if (texture->renderer != renderer) { + wlr_log(WLR_ERROR, "Vulkan texture belongs to a different renderer"); + return WLR_VK_TEXTURE_READBACK_ERROR; + } + + if (!texture->dmabuf_imported) { + // Pixel (SHM upload) texture: CPU-written through our own staging + // uploads, TRANSFER_SRC usage present, no foreign ownership cycle. + return WLR_VK_TEXTURE_READBACK_NOOP; + } + + if (texture->readback_release_failed) { + // A previous cycle could not record its release; the image may still + // be owned by the graphics queue. Never touch it again. + wlr_log(WLR_ERROR, "Vulkan texture readback is unavailable after a " + "failed release; refusing to read the texture"); + return WLR_VK_TEXTURE_READBACK_ERROR; + } + if (texture->readback_acquired) { + // Nested call from the read that the holding gate is performing + // (WBufferDumper): the image is already acquired and the outer cycle + // records the matching release. + return WLR_VK_TEXTURE_READBACK_NOOP; + } + + // The readback cycle runs outside Qt frame recording and must be the sole + // holder of the image's foreign ownership while it does. + if (renderer->texture_sync_batch_active + || renderer->texture_barrier_batch_active + || texture->qt_sampling_acquired + || texture->owned) { + wlr_log(WLR_ERROR, "Vulkan texture readback requested while its " + "foreign ownership is taken or a frame is recording"); + return WLR_VK_TEXTURE_READBACK_ERROR; + } + + // The barrier below covers a single color aspect of one memory plane. + if (texture->mem_count > 1 || vulkan_format_is_ycbcr(texture->format)) { + wlr_log(WLR_ERROR, "Vulkan readback of multi-plane/YCbCr DMA-BUF " + "textures is not supported"); + return WLR_VK_TEXTURE_READBACK_ERROR; + } + + if (!submit_pending_stage_uploads(renderer)) { + wlr_log(WLR_ERROR, "Failed to submit pending Vulkan staging before " + "texture readback"); + return WLR_VK_TEXTURE_READBACK_ERROR; + } + + if (texture->buffer != NULL) { + int sync_file_fds[WLR_DMABUF_MAX_PLANES]; + for (size_t i = 0; i < WLR_DMABUF_MAX_PLANES; i++) { + sync_file_fds[i] = -1; + } + if (!vulkan_sync_foreign_texture_acquire(texture, sync_file_fds)) { + close_sync_file_fds(sync_file_fds); + wlr_log(WLR_ERROR, "Failed to wait for foreign texture DMA-BUF " + "fence before readback"); + return WLR_VK_TEXTURE_READBACK_ERROR; + } + if (!wait_sync_file_fds(sync_file_fds)) { + wlr_log(WLR_ERROR, "Failed to wait for foreign texture DMA-BUF " + "sync_file before readback"); + return WLR_VK_TEXTURE_READBACK_ERROR; + } + } + + if (!submit_readback_barrier(renderer, texture->image, true)) { + wlr_log(WLR_ERROR, "Failed to submit Vulkan texture readback acquire"); + return WLR_VK_TEXTURE_READBACK_ERROR; + } + + texture->readback_acquired = true; + return WLR_VK_TEXTURE_READBACK_ACQUIRED; +} + +void waylib_vk_texture_end_readback(struct wlr_renderer *wlr_renderer, + struct wlr_texture *wlr_texture) { + if (wlr_renderer == NULL || !wlr_renderer_is_vk(wlr_renderer) + || wlr_texture == NULL || !wlr_texture_is_vk(wlr_texture)) { + return; + } + + struct wlr_vk_renderer *renderer = vulkan_get_renderer(wlr_renderer); + struct wlr_vk_texture *texture = vulkan_get_texture(wlr_texture); + if (texture->renderer != renderer || !texture->readback_acquired) { + return; + } + + if (!submit_readback_barrier(renderer, texture->image, false)) { + // Keep the acquire (and mark the texture permanently unusable for + // readbacks) so the image is never sampled or read again without its + // matching release. + texture->readback_release_failed = true; + wlr_log(WLR_ERROR, "Failed to submit Vulkan texture readback release; " + "keeping the readback acquire so the texture is never sampled or " + "read again without its matching release"); + return; + } + texture->readback_acquired = false; +} diff --git a/3rdparty/wlroots/render/vulkan/vulkan.c b/3rdparty/wlroots/render/vulkan/vulkan.c index 0c0445d802..3932585c67 100644 --- a/3rdparty/wlroots/render/vulkan/vulkan.c +++ b/3rdparty/wlroots/render/vulkan/vulkan.c @@ -546,15 +546,58 @@ struct wlr_vk_device *vulkan_device_create(struct wlr_vk_instance *ini, VkPhysicalDeviceSamplerYcbcrConversionFeatures phdev_sampler_ycbcr_features = { .sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SAMPLER_YCBCR_CONVERSION_FEATURES, }; + VkPhysicalDeviceSeparateDepthStencilLayoutsFeaturesKHR + phdev_separate_depth_stencil_layouts_features = { + .sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SEPARATE_DEPTH_STENCIL_LAYOUTS_FEATURES_KHR, + .pNext = &phdev_sampler_ycbcr_features, + }; + // VK_KHR_separate_depth_stencil_layouts depends on + // VK_KHR_create_renderpass2 when the instance targets Vulkan 1.1. + bool has_create_renderpass2 = check_extension(avail_ext_props, avail_extc, + VK_KHR_CREATE_RENDERPASS_2_EXTENSION_NAME); + bool has_separate_depth_stencil_layouts = has_create_renderpass2 && + check_extension(avail_ext_props, avail_extc, + VK_KHR_SEPARATE_DEPTH_STENCIL_LAYOUTS_EXTENSION_NAME); VkPhysicalDeviceFeatures2 phdev_features = { .sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2, .pNext = &phdev_sampler_ycbcr_features, }; + if (has_separate_depth_stencil_layouts) { + phdev_features.pNext = &phdev_separate_depth_stencil_layouts_features; + } vkGetPhysicalDeviceFeatures2(phdev, &phdev_features); dev->sampler_ycbcr_conversion = phdev_sampler_ycbcr_features.samplerYcbcrConversion; wlr_log(WLR_DEBUG, "Sampler YCbCr conversion %s", dev->sampler_ycbcr_conversion ? "supported" : "not supported"); + dev->separate_depth_stencil_layouts = has_separate_depth_stencil_layouts && + phdev_separate_depth_stencil_layouts_features.separateDepthStencilLayouts; + if (dev->separate_depth_stencil_layouts) { + extensions[extensions_len++] = + VK_KHR_SEPARATE_DEPTH_STENCIL_LAYOUTS_EXTENSION_NAME; + } + wlr_log(WLR_DEBUG, "Separate depth/stencil layouts %s", + dev->separate_depth_stencil_layouts ? "enabled" : "disabled"); + + // Qt Quick RHI adopts this device when waylib drives Qt on the wlroots + // Vulkan renderer. QRhiVulkanInitParams::preferredExtensionsForImportedDevice() + // documents VK_KHR_create_renderpass2, VK_KHR_depth_stencil_resolve and + // VK_EXT_vertex_attribute_divisor (VK_KHR_swapchain is intentionally not + // added: the compositor renders through an offscreen QQuickRenderControl + // and never creates a swapchain, and the device extension would be inert + // without companion instance surface support). Without them Qt silently + // loses functionality; enabling them is purely additive for wlroots. + if (has_create_renderpass2) { + extensions[extensions_len++] = VK_KHR_CREATE_RENDERPASS_2_EXTENSION_NAME; + } + if (check_extension(avail_ext_props, avail_extc, + VK_KHR_DEPTH_STENCIL_RESOLVE_EXTENSION_NAME)) { + extensions[extensions_len++] = VK_KHR_DEPTH_STENCIL_RESOLVE_EXTENSION_NAME; + } + if (check_extension(avail_ext_props, avail_extc, + VK_EXT_VERTEX_ATTRIBUTE_DIVISOR_EXTENSION_NAME)) { + extensions[extensions_len++] = VK_EXT_VERTEX_ATTRIBUTE_DIVISOR_EXTENSION_NAME; + } const float prio = 1.f; VkDeviceQueueCreateInfo qinfo = { @@ -585,11 +628,20 @@ struct wlr_vk_device *vulkan_device_create(struct wlr_vk_instance *ini, .sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SAMPLER_YCBCR_CONVERSION_FEATURES, .samplerYcbcrConversion = dev->sampler_ycbcr_conversion, }; + VkPhysicalDeviceSeparateDepthStencilLayoutsFeaturesKHR + separate_depth_stencil_layouts_features = { + .sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SEPARATE_DEPTH_STENCIL_LAYOUTS_FEATURES_KHR, + .pNext = &sampler_ycbcr_features, + .separateDepthStencilLayouts = dev->separate_depth_stencil_layouts, + }; VkPhysicalDeviceSynchronization2FeaturesKHR sync2_features = { .sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SYNCHRONIZATION_2_FEATURES_KHR, .pNext = &sampler_ycbcr_features, .synchronization2 = VK_TRUE, }; + if (dev->separate_depth_stencil_layouts) { + sync2_features.pNext = &separate_depth_stencil_layouts_features; + } VkPhysicalDeviceTimelineSemaphoreFeaturesKHR timeline_features = { .sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_TIMELINE_SEMAPHORE_FEATURES_KHR, .pNext = &sync2_features, diff --git a/CMakeLists.txt b/CMakeLists.txt index fbcdbcef94..28dc80ac91 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -96,10 +96,10 @@ add_definitions("-DQT_MESSAGELOGCONTEXT") set(LOCAL_QML_IMPORT_PATH "${PROJECT_BINARY_DIR}/qt/qml") if(WITH_SUBMODULE_WAYLIB) - include(${CMAKE_CURRENT_SOURCE_DIR}/waylib/cmake/WaylandScannerHelpers.cmake) # Vendored wlroots must be available before waylib consumers. # Scaffold in wlroots/; upstream sources under 3rdparty/wlroots. add_subdirectory(wlroots) + include(${CMAKE_CURRENT_SOURCE_DIR}/waylib/cmake/WaylandScannerHelpers.cmake) add_subdirectory(waylib) list(APPEND LOCAL_QML_IMPORT_PATH "${PROJECT_BINARY_DIR}/waylib/src/server") else() diff --git a/src/core/qml/Effects/+vulkan/Blur.qml b/src/core/qml/Effects/+vulkan/Blur.qml index acd34851da..45e1d9a4cd 100644 --- a/src/core/qml/Effects/+vulkan/Blur.qml +++ b/src/core/qml/Effects/+vulkan/Blur.qml @@ -1,14 +1,17 @@ -// Copyright (C) 2026 UnionTech Software Technology Co., Ltd. +// Copyright (C) 2024-2026 UnionTech Software Technology Co., Ltd. // SPDX-License-Identifier: Apache-2.0 OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only import QtQuick +import QtQuick.Effects +import QtQuick.Shapes +import Waylib.Server import Treeland -Rectangle { - id: root - - color: Qt.rgba(0.0, 0.0, 0.0, 0.0) +RenderBufferBlitter { + id: blitter + smooth: true + property real radius: 0 property bool radiusEnabled: radius > 0 property int blurMax: Helper.config.blurStrength property bool blurEnabled: blurMax > 0 && blurAmount > 0 @@ -20,27 +23,93 @@ Rectangle { property real glassSpecular: Helper.config.glassSpecular property real glassTint: 0.0 property bool glassEnabled: Helper.config.glassEnabled - property Item effectContent: effectContentItem + property Item effectContent: contentLoader property bool contentOffscreen: false property bool effectEnabled: true - readonly property Item content: contentContainer - property alias offscreen: root.contentOffscreen - default property alias data: contentContainer.data - Item { - id: contentContainer + z: parent.z ? parent.z - 1 : -1 + anchors.fill: parent + + // The blitter only exists to feed the effect. When the effect is disabled + // (or its content is rendered offscreen by the caller) there is nothing to + // draw, so hide the blitter entirely: this also stops + // WBufferRenderer::hasActiveRenderBufferBlitter() from requesting the + // Vulkan backdrop and splitting the render pass for a disabled effect. + visible: blitter.effectEnabled && !blitter.contentOffscreen + + // Keep the Vulkan effect implementation behind the Vulkan file selector. + // This preserves the GLES2 Blur component and its lifecycle unchanged. + Loader { + id: contentLoader anchors.fill: parent + sourceComponent: blitter.glassEnabled ? glassComponent : blurComponent + visible: blitter.effectEnabled && !blitter.contentOffscreen } - Rectangle { - id: effectContentItem - anchors.fill: parent - radius: root.radius - color: Qt.rgba(1.0, 1.0, 1.0, 0.15) - visible: root.effectEnabled && !root.contentOffscreen - z: -1 + Component { + id: glassComponent + GlassEffect { + anchors.fill: parent + source: blitter.content + radius: blitter.radius + blurEnabled: blitter.blurEnabled + blurMax: blitter.blurMax + blurAmount: blitter.blurAmount + blurMultiplier: blitter.multiplier + bezelWidth: Helper.config.glassBezel + thickness: Helper.config.glassThickness + ior: 1.33 + specular: blitter.glassSpecular + tint: blitter.glassTint + brightness: blitter.brightness + contrast: blitter.contrast + saturation: blitter.saturation + contentEdgePull: 0.0 + contentRampEnd: 0.0 + refractionMaxTan: 3.3 + profilePower: Helper.config.glassProfilePower + innerShadow: 0.0 + } } - z: parent.z ? parent.z - 1 : -1 - anchors.fill: parent + Component { + id: blurComponent + Item { + anchors.fill: parent + + MultiEffect { + id: blur + anchors.fill: parent + layer.enabled: blitter.radiusEnabled + smooth: blitter.radiusEnabled + opacity: blitter.radiusEnabled ? 0 : blitter.opacity + source: blitter.content + autoPaddingEnabled: false + blurEnabled: blitter.blurEnabled + blur: blitter.blurAmount + blurMax: blitter.blurMax + blurMultiplier: blitter.multiplier + saturation: 0.2 + } + + Loader { + x: blur.x + y: blur.y + active: blitter.radiusEnabled + sourceComponent: Shape { + anchors.fill: parent + preferredRendererType: Shape.CurveRenderer + ShapePath { + strokeWidth: 0 + fillItem: blur + PathRectangle { + width: blur.width + height: blur.height + radius: blitter.radius + } + } + } + } + } + } } diff --git a/src/core/qml/SourceOutput.qml b/src/core/qml/SourceOutput.qml index c8ce8f9f55..2eb75b3780 100644 --- a/src/core/qml/SourceOutput.qml +++ b/src/core/qml/SourceOutput.qml @@ -17,7 +17,17 @@ OutputItem { id: cursorItem required property QtObject outputCursor - readonly property point rawPosition: parent.mapFromGlobal(cursor.position.x, cursor.position.y) + // When the cursor is carried by a hardware/output layer on the Vulkan + // path, detach its geometry from the live pointer position so that moving + // the cursor does not dirty the main QML scene. The layer placement is + // then computed from wlroots cursor/output coordinates during layer + // rendering (see OutputHelper::renderLayer). GLES2 keeps the legacy + // scene-bound cursor geometry. + readonly property bool detachedCursorLayer: OutputLayer.enabled + && GraphicsInfo.api === GraphicsInfo.Vulkan + readonly property point rawPosition: detachedCursorLayer + ? Qt.point(0, 0) + : parent.mapFromGlobal(cursor.position.x, cursor.position.y) readonly property real effectiveScale: rootOutputItem.devicePixelRatio || 1.0 // Align cursor position to pixel grid to prevent blur on fractional DPR displays @@ -32,8 +42,8 @@ OutputItem { cursor: outputCursor.cursor output: outputCursor.output.output - x: position.x - hotSpot.x - y: position.y - hotSpot.y + x: detachedCursorLayer ? 0 : position.x - hotSpot.x + y: detachedCursorLayer ? 0 : position.y - hotSpot.y visible: valid && outputCursor.visible OutputLayer.enabled: !outputCursor.output.forceSoftwareCursor OutputLayer.keepLayer: true diff --git a/src/core/treeland.cpp b/src/core/treeland.cpp index 7ffe00bbdf..f69e5cf854 100644 --- a/src/core/treeland.cpp +++ b/src/core/treeland.cpp @@ -36,12 +36,14 @@ #include #include #include +#include #include #include #include #include #include +#include #include #include #include @@ -90,6 +92,17 @@ class TreelandPrivate : public QObject auto initializeTreeland = [this, q, onInitialized = std::move(onInitialized)] { helper = qmlEngine->singletonInstance("Treeland", "Helper"); connect(helper, &Helper::requestQuit, q, &Treeland::quit, Qt::QueuedConnection); + if (QQuickWindow::graphicsApi() == QSGRendererInterface::Vulkan) { + connect(helper->window(), + &QQuickWindow::sceneGraphError, + q, + [] (QQuickWindow::SceneGraphError error, const QString &message) { + qCCritical(lcTlCore) << "Scene graph rendering failed; exiting compositor" + << "error" << error + << "message" << message; + QCoreApplication::exit(EXIT_FAILURE); + }); + } if (qEnvironmentVariableIsEmpty("WLR_XWAYLAND")) qputenv("WLR_XWAYLAND", QByteArray(LIBEXEC_DIR) + "/treeland-xwayland"); diff --git a/src/core/treelandinit.cpp b/src/core/treelandinit.cpp index 13647f0ed9..8eab690fd5 100644 --- a/src/core/treelandinit.cpp +++ b/src/core/treelandinit.cpp @@ -6,6 +6,8 @@ #include #include #include +#include +#include #include #include #include @@ -49,7 +51,12 @@ std::unique_ptr preInit(int &argc, char *argv[]) WServer::initializeQPA({}, [](const QString &) { return static_cast(new QDeepinTheme()); }); - QGuiApplication::setAttribute(Qt::AA_UseOpenGLES); + const QByteArray wlrRenderer = qgetenv("WLR_RENDERER"); + const bool explicitVulkanRenderer = wlrRenderer == "vulkan"; + if (explicitVulkanRenderer) + QQuickWindow::setGraphicsApi(QSGRendererInterface::Vulkan); + else + QGuiApplication::setAttribute(Qt::AA_UseOpenGLES); QGuiApplication::setHighDpiScaleFactorRoundingPolicy(Qt::HighDpiScaleFactorRoundingPolicy::PassThrough); QGuiApplication::setQuitOnLastWindowClosed(false); QQuickStyle::setStyle("Chameleon"); diff --git a/src/modules/capture/capture.cpp b/src/modules/capture/capture.cpp index 3f2c190b38..aae2e9362f 100644 --- a/src/modules/capture/capture.cpp +++ b/src/modules/capture/capture.cpp @@ -20,6 +20,7 @@ #include #include #include +#include #include #include @@ -27,8 +28,11 @@ #include #include #include +#include #include +#include +#include #include static inline QRectF scaledRect(const QRectF &rect, qreal devicePixelRatio) @@ -138,11 +142,24 @@ void CaptureContextV1::onCapture(treeland_capture_frame_v1 *frame) } m_frame = frame; auto notifyBuffer = [this] { + if (!m_frame) + return; + if (!m_captureSource || !m_captureSource->imageValid()) { + failFrame("without a valid source image"); + return; + } + + const QRect crop = source()->cropRect(); + if (crop.isEmpty()) { + failFrame("for an empty crop rect"); + return; + } + m_frame->sendBuffer( WTools::drmToShmFormat(WTools::toDrmFormat(m_captureSource->image().format())), - source()->cropRect().width(), - source()->cropRect().height(), - source()->cropRect().width() * 4); + crop.width(), + crop.height(), + crop.width() * 4); m_frame->sendBufferDone(); connect(m_frame, &treeland_capture_frame_v1::copy, @@ -156,15 +173,39 @@ void CaptureContextV1::onCapture(treeland_capture_frame_v1 *frame) if (m_captureSource->imageValid()) { notifyBuffer(); } else { + connect(m_captureSource, + &CaptureSource::imageFailed, + this, + [this] { + failFrame("because the one-shot capture image could not be created"); + }, + Qt::SingleShotConnection); connect(m_captureSource, &CaptureSource::imageReady, this, notifyBuffer); } Q_EMIT finishSelect(); } +void CaptureContextV1::failFrame(const char *reason) +{ + if (!m_frame) + return; + + qCWarning(lcTlCapture) << "Failing capture frame" << reason; + m_frame->sendFailed(); + // The request is finished: drop the frame so a late imageReady cannot emit + // a buffer after failed (the client is expected to destroy it now). + m_frame = nullptr; + if (m_captureSource) + disconnect(m_captureSource, &CaptureSource::imageReady, this, nullptr); +} + void CaptureContextV1::handleFrameCopy(wlr_buffer *buffer) { - if (m_captureSource) { - m_captureSource->copyBuffer(buffer); + if (m_captureSource && m_frame) { + if (!m_captureSource->copyBuffer(buffer)) { + failFrame("because the client buffer copy failed"); + return; + } m_frame->sendReady(); } else { wl_client_post_implementation_error(wl_resource_get_client(m_handle->resource), @@ -690,14 +731,20 @@ void CaptureSourceSelector::createImage() this, &CaptureSourceSelector::createImage); if (m_selectedSource) { - m_selectedSource->createImage(); if (m_selectedSource->imageValid()) { releaseMaskSurface(); } else { connect(m_selectedSource, &CaptureSource::imageReady, this, - &CaptureSourceSelector::releaseMaskSurface); + &CaptureSourceSelector::releaseMaskSurface, + Qt::SingleShotConnection); + connect(m_selectedSource, + &CaptureSource::imageFailed, + this, + &CaptureSourceSelector::releaseMaskSurface, + Qt::SingleShotConnection); + m_selectedSource->createImage(); } } else { releaseMaskSurface(); @@ -947,21 +994,83 @@ QImage CaptureSource::image() const return m_image; } +static QImage normalizeCaptureImage(QImage image) +{ + if (image.isNull()) + return {}; + + if (image.format() != QImage::Format_ARGB32_Premultiplied) + image = image.convertToFormat(QImage::Format_ARGB32_Premultiplied); + + return image; +} + void CaptureSource::createImage() { - if (m_sourceList.size() == 1 && m_sourceList.first().first) { - auto grabber = new WTextureCapturer(m_sourceList.first().second, this); - grabber->grabToImage() - .then([this](QImage image) { - m_image = std::move(image); - Q_EMIT imageReady(); - }) - .onFailed([](const std::exception &e) { - qCCritical(lcTlCapture) << e.what(); - }); - } else { - // TODO: support multiple sources + auto *renderWindow = m_sourceList.size() == 1 && m_sourceList.first().second + ? m_sourceList.first().second->outputRenderWindow() + : nullptr; + const bool vulkanRenderer = renderWindow && renderWindow->rhi() + && renderWindow->rhi()->backend() == QRhi::Vulkan; + if (!vulkanRenderer) { + if (m_sourceList.size() == 1 && m_sourceList.first().first) { + auto grabber = new WTextureCapturer(m_sourceList.first().second, this); + grabber->grabToImage() + .then([this](QImage image) { + m_image = std::move(image); + Q_EMIT imageReady(); + }) + .onFailed([](const std::exception &e) { + qCCritical(lcTlCapture) << e.what(); + }); + } + return; + } + + if (m_sourceList.size() != 1 || !m_sourceList.first().first || !m_sourceList.first().second) { + qCWarning(lcTlCapture) << "Cannot create capture image from invalid source list" + << "sourceCount" << m_sourceList.size(); + Q_EMIT imageFailed(); + return; + } + + auto renderer = renderWindow ? renderWindow->renderer() : nullptr; + auto buffer = sourceDMABuffer(); + if (!buffer || !renderer) { + qCWarning(lcTlCapture) << "Cannot create capture image without source buffer or renderer" + << "buffer" << buffer + << "renderer" << renderer; + Q_EMIT imageFailed(); + return; + } + + if (!wlr_buffer_lock(buffer)) { + qCWarning(lcTlCapture) << "Failed to lock capture source buffer" << buffer; + Q_EMIT imageFailed(); + return; + } + + WBufferUnlockPtr bufferGuard(buffer); + + QImage image; + const auto result = WBufferDumper::dumpBufferToImage(buffer, renderer, image); + if (result != WBufferDumper::DumpResult::Success) { + qCWarning(lcTlCapture) << "Failed to dump capture source buffer" + << "result" << WBufferDumper::dumpResultToString(result) + << "buffer" << buffer; + Q_EMIT imageFailed(); + return; } + + image = normalizeCaptureImage(std::move(image)); + if (image.isNull()) { + qCWarning(lcTlCapture) << "Capture source dump produced an invalid image"; + Q_EMIT imageFailed(); + return; + } + + m_image = std::move(image); + Q_EMIT imageReady(); } wlr_buffer *CaptureSource::sourceDMABuffer() @@ -985,23 +1094,79 @@ wlr_buffer *CaptureSource::sourceDMABuffer() return buffer; } -void CaptureSource::copyBuffer(wlr_buffer *buffer) +bool CaptureSource::copyBuffer(wlr_buffer *buffer) { - Q_ASSERT(imageValid()); + if (!imageValid() || !buffer) { + qCWarning(lcTlCapture) << "Cannot copy capture image without valid image or buffer" + << "imageValid" << imageValid() + << "buffer" << buffer; + return false; + } + auto width = cropRect().width(); auto height = cropRect().height(); + if (width <= 0 || height <= 0) { + qCWarning(lcTlCapture) << "Cannot copy capture image with invalid crop rect" + << cropRect(); + return false; + } + uint32_t format; size_t stride; void *data; - wlr_buffer_begin_data_ptr_access(buffer, WLR_BUFFER_DATA_PTR_ACCESS_WRITE, &data, &format, &stride); - Q_ASSERT(stride == static_cast(width) * 4); // For QImage - QImage img = image().copy(cropRect()); - auto bufFormat = WTools::toImageFormat(format); - if (image().format() != bufFormat) { - img = image().convertToFormat(bufFormat); + if (!wlr_buffer_begin_data_ptr_access(buffer, + WLR_BUFFER_DATA_PTR_ACCESS_WRITE, + &data, + &format, + &stride) + || !data) { + qCWarning(lcTlCapture) << "Failed to access client capture buffer for writing" + << "buffer" << buffer; + return false; } - memcpy(data, img.constBits(), stride * height); + + bool copied = false; + do { + auto bufFormat = WTools::toImageFormat(format); + if (bufFormat == QImage::Format_Invalid) { + qCWarning(lcTlCapture) << "Client capture buffer has unsupported format" + << "format" << format; + break; + } + + QImage img = image().copy(cropRect()); + if (img.isNull() || img.width() != width || img.height() != height) { + qCWarning(lcTlCapture) << "Failed to crop capture image" + << "cropRect" << cropRect() + << "imageSize" << image().size(); + break; + } + + if (img.format() != bufFormat) + img = img.convertToFormat(bufFormat); + + if (img.isNull()) { + qCWarning(lcTlCapture) << "Failed to convert capture image to client buffer format" + << "format" << bufFormat; + break; + } + + if (stride < static_cast(img.bytesPerLine())) { + qCWarning(lcTlCapture) << "Client capture buffer stride is too small" + << "stride" << stride + << "imageStride" << img.bytesPerLine(); + break; + } + + auto dst = static_cast(data); + for (int y = 0; y < img.height(); ++y) { + memcpy(dst + stride * y, img.constScanLine(y), img.bytesPerLine()); + } + copied = true; + } while (false); + wlr_buffer_end_data_ptr_access(buffer); + return copied; } CaptureSourceOutput::CaptureSourceOutput(WOutputViewport *viewport) @@ -1013,10 +1178,18 @@ CaptureSourceOutput::CaptureSourceOutput(WOutputViewport *viewport) wlr_buffer *CaptureSourceOutput::internalBuffer() { Q_ASSERT(m_sourceList.size() == 1); - if (m_sourceList.first().first && m_outputViewport->wTextureProvider()) - return m_outputViewport->wTextureProvider()->wlrBuffer(); - else + if (!m_sourceList.first().first || !m_outputViewport) return nullptr; + + auto *renderWindow = m_sourceList.first().second->outputRenderWindow(); + const bool vulkanRenderer = renderWindow && renderWindow->rhi() + && renderWindow->rhi()->backend() == QRhi::Vulkan; + if (vulkanRenderer) + return m_outputViewport->lastBuffer(); + + if (m_outputViewport->wTextureProvider()) + return m_outputViewport->wTextureProvider()->wlrBuffer(); + return nullptr; } QRect CaptureSourceOutput::cropRect() const @@ -1045,12 +1218,20 @@ CaptureSourceRegion::CaptureSourceRegion(WOutputViewport *viewport, const QRect wlr_buffer *CaptureSourceRegion::internalBuffer() { - if (m_sourceList.size() == 1 && m_sourceList.first().first - && m_sourceList.first().second->wTextureProvider()) { - return m_sourceList.first().second->wTextureProvider()->wlrBuffer(); - } else { + if (m_sourceList.size() != 1 || !m_sourceList.first().first + || m_viewportRegions.isEmpty() || !m_viewportRegions.first().first) { return nullptr; } + + auto *renderWindow = m_sourceList.first().second->outputRenderWindow(); + const bool vulkanRenderer = renderWindow && renderWindow->rhi() + && renderWindow->rhi()->backend() == QRhi::Vulkan; + if (vulkanRenderer) + return m_viewportRegions.first().first->lastBuffer(); + + if (m_sourceList.first().second->wTextureProvider()) + return m_sourceList.first().second->wTextureProvider()->wlrBuffer(); + return nullptr; } CaptureSource::CaptureSourceType CaptureSourceRegion::sourceType() diff --git a/src/modules/capture/capture.h b/src/modules/capture/capture.h index 98ce679896..69b47e0bba 100644 --- a/src/modules/capture/capture.h +++ b/src/modules/capture/capture.h @@ -75,6 +75,7 @@ class CaptureSource : public QObject Q_SIGNALS: void imageReady(); + void imageFailed(); void bufferDestroyed(); void targetDestroyed(); void targetResized(); @@ -101,7 +102,7 @@ class CaptureSource : public QObject * @brief copyBuffer render captured contents to a buffer * @param buffer buffer prepared by client */ - void copyBuffer(wlr_buffer *buffer); + bool copyBuffer(wlr_buffer *buffer); // Cropped area of source virtual QRect cropRect() const = 0; @@ -241,6 +242,7 @@ class CaptureContextV1 : public QObject void onCapture(treeland_capture_frame_v1 *frame); void onCreateSession(treeland_capture_session_v1 *session); void handleFrameCopy(wlr_buffer *buffer); + void failFrame(const char *reason); void handleSessionStart(); void handleFrameDone(uint32_t tvSecHi, uint32_t tvSecLo, uint32_t tvUsec); void handleRenderEnd(); diff --git a/src/modules/keyboard-state-notify/keyboardstatenotifymanagerinterfacev1.cpp b/src/modules/keyboard-state-notify/keyboardstatenotifymanagerinterfacev1.cpp index 78d8398df1..b716080274 100644 --- a/src/modules/keyboard-state-notify/keyboardstatenotifymanagerinterfacev1.cpp +++ b/src/modules/keyboard-state-notify/keyboardstatenotifymanagerinterfacev1.cpp @@ -92,10 +92,12 @@ class TreelandKeyboardStateNotifyManagerInterfaceV1Private { public: explicit TreelandKeyboardStateNotifyManagerInterfaceV1Private(TreelandKeyboardStateNotifyManagerInterfaceV1 *_q); + ~TreelandKeyboardStateNotifyManagerInterfaceV1Private() override; TreelandKeyboardStateNotifyManagerInterfaceV1 *q = nullptr; void setupKeyboardConnections(); + void teardownKeyboardConnections(); void onModifiersEvent(WSeat *seat); wl_global *global() const; @@ -112,6 +114,7 @@ class TreelandKeyboardStateNotifyManagerInterfaceV1Private void connectKeyboardGroup(WSeat *seat, WInputDevice *keyboardDevice); QList> m_keyboardConnections; + QList m_seatManagerSignals; bool m_keyboardConnectionsSetup = false; }; @@ -121,6 +124,29 @@ TreelandKeyboardStateNotifyManagerInterfaceV1Private::TreelandKeyboardStateNotif { } +TreelandKeyboardStateNotifyManagerInterfaceV1Private::~TreelandKeyboardStateNotifyManagerInterfaceV1Private() +{ + teardownKeyboardConnections(); +} + +// The modifiers listeners registered through connectKeyboardGroup() are +// native wl_list listeners on the seat's keyboard-group wlr_keyboard, an +// object this module does not own. wlr_keyboard_finish() hard-asserts that +// its events.modifiers listener list is empty, so the listeners must be +// detached while the seat is still alive: before WServer::stop() reaches +// WSeat::destroy() in the interface teardown loop, and before any dynamic +// seat detach/delete. Also reset the setup state so a server restart can +// re-register everything on the freshly created keyboard group. +void TreelandKeyboardStateNotifyManagerInterfaceV1Private::teardownKeyboardConnections() +{ + m_keyboardConnections.clear(); + s_lastModifiers.clear(); + for (const auto &connection : std::as_const(m_seatManagerSignals)) + QObject::disconnect(connection); + m_seatManagerSignals.clear(); + m_keyboardConnectionsSetup = false; +} + wl_global *TreelandKeyboardStateNotifyManagerInterfaceV1Private::global() const { return m_global; @@ -165,10 +191,10 @@ void TreelandKeyboardStateNotifyManagerInterfaceV1Private::setupKeyboardConnecti handleSeatAdded(seat); } - QObject::connect(helper->seatManager(), &SeatManager::seatAdded, q, [this](WSeat *seat) { + m_seatManagerSignals << QObject::connect(helper->seatManager(), &SeatManager::seatAdded, q, [this](WSeat *seat) { handleSeatAdded(seat); }); - QObject::connect(helper->seatManager(), &SeatManager::seatRemoved, q, [this](WSeat *seat) { + m_seatManagerSignals << QObject::connect(helper->seatManager(), &SeatManager::seatRemoved, q, [this](WSeat *seat) { handleSeatDestroy(seat); }); } @@ -393,6 +419,11 @@ void TreelandKeyboardStateNotifyManagerInterfaceV1::create(WServer *server) void TreelandKeyboardStateNotifyManagerInterfaceV1::destroy([[maybe_unused]] WServer *server) { d->globalRemove(); + // WServer::stop() destroys interfaces in reverse attach order: this + // module is torn down before WSeat, so detaching here still finds the + // group keyboard alive and wlr_keyboard_finish() sees an empty list. + // The interface object itself outlives the keyboard otherwise. + d->teardownKeyboardConnections(); } wl_global *TreelandKeyboardStateNotifyManagerInterfaceV1::global() const diff --git a/src/seat/helper.cpp b/src/seat/helper.cpp index 605fbf2b66..71380f2990 100644 --- a/src/seat/helper.cpp +++ b/src/seat/helper.cpp @@ -75,12 +75,15 @@ #include "workspace/workspace.h" #include +#include #include #include #include #include #include +#include +#include #include #include #include @@ -206,6 +209,24 @@ static bool currentPrimaryMatchesId(RootSurfaceContainer *rootContainer, const Q return rootContainer && outputMatchesId(rootContainer->primaryOutput(), outputId); } +static bool rendererSupportsImplicitDmabufFormats(wlr_renderer *renderer) +{ + const auto *formats = renderer + ? wlr_renderer_get_texture_formats(renderer, WLR_BUFFER_CAP_DMABUF) + : nullptr; + if (!formats) + return false; + + for (size_t i = 0; i < formats->len; ++i) { + const auto &format = formats->formats[i]; + for (size_t j = 0; j < format.len; ++j) { + if (format.modifiers[j] == DRM_FORMAT_MOD_INVALID) + return true; + } + } + return false; +} + Helper *Helper::m_instance = nullptr; Helper::Helper(QObject *parent) @@ -2220,14 +2241,40 @@ 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) { + // Restrict the advertised texture formats to the ones the Qt wrapper + // can display before wl_shm and linux-dmabuf hand them to clients. + // XWayland picks buffer formats from the X visual depth instead and + // is covered by the exact view seeding in the texture wrapper. + if (!WRenderHelper::restrictVulkanTextureFormats(m_renderer)) + qCWarning(lcTlCore) << "Continuing with the unfiltered Vulkan texture format set"; + + if (!wlr_renderer_init_wl_shm(m_renderer, m_server->handle())) + qCFatal(lcTlCore) << "Failed to initialize wl_shm for Vulkan renderer"; + + 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 @@ -2914,7 +2961,14 @@ bool Helper::doGesture(QInputEvent *event) Output *Helper::createNormalOutput(WOutput *output) { Output *o = Output::create(output, qmlEngine(), this); - if (isNvidiaCardPresent()) { + // The NVIDIA software-cursor workaround predates the Vulkan renderer and + // targeted the GLES wlr_egl context handoff; the Vulkan backend shares the + // wlroots VkDevice with Qt and defers set_cursor() until the cursor buffer + // has completed on the GPU, so the workaround is no longer applicable and + // would defeat the hardware-cursor fast path (a full canvas repaint plus + // synchronous frame per pointer move). Do not change the GLES2 behaviour. + if (isNvidiaCardPresent() + && WRenderHelper::getGraphicsApi() != QSGRendererInterface::Vulkan) { o->outputItem()->setProperty("forceSoftwareCursor", true); } o->outputItem()->stackBefore(m_rootSurfaceContainer); diff --git a/src/seat/seatmanager.cpp b/src/seat/seatmanager.cpp index d97be2516e..d906539319 100644 --- a/src/seat/seatmanager.cpp +++ b/src/seat/seatmanager.cpp @@ -28,12 +28,20 @@ SeatManager::SeatManager(WServer *server, QObject *parent) SeatManager::~SeatManager() { + // Prune hooks first: from here on any seat we delete ourselves must not + // run the destroyed hook against members that are being torn down. + for (auto connection : std::as_const(m_seatCleanupHooks)) + QObject::disconnect(connection); + m_seatCleanupHooks.clear(); + QMap seatsToDelete; seatsToDelete.swap(m_seats); m_deviceRules.clear(); m_defaultSeat = nullptr; for (auto seat : std::as_const(seatsToDelete)) { + // See removeSeat(): emit while the seat natives are still alive. + Q_EMIT seatRemoved(seat); if (m_server) { m_server->detach(seat); } @@ -54,6 +62,29 @@ WSeat *SeatManager::createSeat(const QString &name, bool isFallback) m_defaultSeat = seat; } + // A WSeat registered as server interface is owned by WServer too + // (~WServer qDeleteAll(interfaceList)); if the server dies first, this + // manager must not keep or touch the dangling wrapper. ~QObject's + // destroyed signal fires while the manager is still fully alive in that + // order (~WServer runs as a sibling before ~SeatManager's member + // teardown), so pruning from the hook is safe there; ~SeatManager + // disconnects the hooks up front so they can never fire mid-destruction. + m_seatCleanupHooks.insert( + seat, + connect(seat, &QObject::destroyed, this, [this, seat] { + unhookSeat(seat); + const auto names = m_seats.keys(); + for (const auto &seatName : names) { + if (m_seats.value(seatName) == seat) { + m_seats.remove(seatName); + } + } + if (m_defaultSeat == seat) { + m_defaultSeat = m_seats.isEmpty() ? nullptr : m_seats.begin().value(); + qCDebug(lcTlSeat) << "Default seat reset after external seat destruction"; + } + })); + qCDebug(lcTlSeat) << "Created seat:" << name << "fallback:" << isFallback; auto announce = [this, seat, name] { @@ -89,6 +120,15 @@ WSeat *SeatManager::createSeat(const QString &name, bool isFallback) return seat; } +void SeatManager::unhookSeat(WSeat *seat) +{ + auto it = m_seatCleanupHooks.find(seat); + if (it == m_seatCleanupHooks.end()) + return; + QObject::disconnect(it.value()); + m_seatCleanupHooks.erase(it); +} + void SeatManager::removeSeat(const QString &name) { if (!m_seats.contains(name)) { @@ -122,11 +162,17 @@ void SeatManager::removeSeat(const QString &name) } } + // Notify listeners BEFORE detaching: interface objects tear down native + // resources (e.g. wl_signal listeners on this seat's keyboard group) from + // the seatRemoved handlers, and wlr_keyboard_finish() hard-asserts if + // anything is still attached once the keyboard group starts dying. + Q_EMIT seatRemoved(seat); + unhookSeat(seat); + if (m_server) { m_server->detach(seat); } - Q_EMIT seatRemoved(seat); qCDebug(lcTlSeat) << "Removed seat:" << name; delete seat; @@ -321,10 +367,12 @@ void SeatManager::loadConfig(const QJsonObject &config) m_defaultSeat = nullptr; for (auto seat : std::as_const(seatsToDelete)) { + // See removeSeat(): emit while the seat natives are still alive. + Q_EMIT seatRemoved(seat); + unhookSeat(seat); if (m_server) { m_server->detach(seat); } - Q_EMIT seatRemoved(seat); delete seat; } diff --git a/src/seat/seatmanager.h b/src/seat/seatmanager.h index 97ae537161..b700a08c73 100644 --- a/src/seat/seatmanager.h +++ b/src/seat/seatmanager.h @@ -106,5 +106,11 @@ class SeatManager : public QObject QMap m_seats; QMap> m_deviceRules; WSeat *m_defaultSeat = nullptr; + // Per-seat QObject::destroyed hooks that prune m_seats/m_defaultSeat when + // WServer (~WServer's qDeleteAll of interfaceList) deletes a WSeat before + // this manager does. Cleared up-front in ~SeatManager so the hook body + // never touches manager members during self-destruction. + QHash m_seatCleanupHooks; + void unhookSeat(WSeat *seat); mutable QMap m_deviceCache; }; diff --git a/waylib/src/server/CMakeLists.txt b/waylib/src/server/CMakeLists.txt index e5d0b24d67..dc8fd3e936 100644 --- a/waylib/src/server/CMakeLists.txt +++ b/waylib/src/server/CMakeLists.txt @@ -63,6 +63,14 @@ pkg_search_module(XKBCOMMON REQUIRED IMPORTED_TARGET xkbcommon) pkg_search_module(XCB REQUIRED IMPORTED_TARGET xcb) pkg_search_module(EGL REQUIRED IMPORTED_TARGET egl) +# WLR_HAVE_VULKAN_RENDERER is exported by the vendored wlroots scaffold into +# the parent scope, and regenerated from waylib-wlroots.pc for standalone +# builds (see the block above). Querying a system pkg-config module here never +# matched the vendored build and silently disabled the Vulkan link. +if(WLR_HAVE_VULKAN_RENDERER) + find_package(Vulkan REQUIRED) +endif() + find_package(TreelandProtocols REQUIRED) add_compile_definitions(WLR_PRIVATE=) @@ -170,6 +178,7 @@ set(SOURCES ${WAYLAND_PROTOCOLS_OUTPUTDIR}/xdg-shell-protocol.c utils/wtools.cpp + utils/wvulkantrace.cpp utils/wthreadutils.cpp utils/wimagebuffer.cpp utils/wcursorimage.cpp @@ -206,6 +215,8 @@ set(SOURCES protocols/wpointerconstraintsv1.cpp protocols/wrelativepointermanagerv1.cpp protocols/woutputmanagerv1.cpp + protocols/wlinuxdmabufv1.cpp + protocols/wpresentation.cpp protocols/wextforeigntoplevellistv1.cpp protocols/wxdgdialogmanagerv1.cpp protocols/wsecuritycontextmanager.cpp @@ -318,6 +329,10 @@ set(HEADERS protocols/WRelativePointerManagerV1 protocols/woutputmanagerv1.h protocols/WOutputManagerV1 + protocols/wlinuxdmabufv1.h + protocols/WLinuxDmabufV1 + protocols/wpresentation.h + protocols/WPresentation protocols/wlayershell.h protocols/WLayerShell protocols/wxwayland.h @@ -369,6 +384,7 @@ set(PRIVATE_HEADERS protocols/private/wtextinputv3_p.h protocols/private/wvirtualkeyboardv1_p.h protocols/private/wsecuritycontextmanager_p.h + utils/private/wvulkantrace_p.h ) if(NOT DISABLE_XWAYLAND) @@ -447,6 +463,13 @@ target_link_libraries(${TARGET} PkgConfig::EGL ) +if(TARGET Vulkan::Vulkan) + target_link_libraries(${TARGET} + PRIVATE + Vulkan::Vulkan + ) +endif() + target_link_libraries(${TARGET} PUBLIC ${WAYLIB_WLROOTS_PUBLIC_TARGET} diff --git a/waylib/src/server/kernel/wlr_all.h b/waylib/src/server/kernel/wlr_all.h index b9b559ef54..8b0ee75818 100644 --- a/waylib/src/server/kernel/wlr_all.h +++ b/waylib/src/server/kernel/wlr_all.h @@ -100,6 +100,7 @@ extern "C" { #include #include #include +#include #include #include #include @@ -110,6 +111,7 @@ extern "C" { #include #include #include +#include #include #include #include diff --git a/waylib/src/server/kernel/woutput.cpp b/waylib/src/server/kernel/woutput.cpp index a30dbab689..441530ce1a 100644 --- a/waylib/src/server/kernel/woutput.cpp +++ b/waylib/src/server/kernel/woutput.cpp @@ -11,6 +11,7 @@ #include "wtools.h" #include "platformplugin/qwlrootscreen.h" #include "private/wglobal_p.h" +#include "utils/private/wvulkantrace_p.h" #include "wayliblogging.h" #include @@ -97,6 +98,19 @@ WOutput::WOutput(wlr_output *handle, WBackend *backend) if (event->state->committed & WLR_OUTPUT_STATE_ADAPTIVE_SYNC_ENABLED) Q_EMIT adaptiveSyncEnabledChanged(); }); + if (WVulkanTrace::enabled()) { + listeners()->add(&handle->events.commit, this, + [this] (wlr_output_event_commit *event) { + WVulkanTrace::outputCommitState(this->handle(), event->state->committed); + }); + listeners()->add(&handle->events.present, this, + [this] (wlr_output_event_present *event) { + WVulkanTrace::outputPresented(this->handle(), event); + }); + listeners()->add(&handle->events.destroy, this, [handle] (void *) { + WVulkanTrace::outputDestroyed(handle); + }); + } } WOutput::~WOutput() @@ -310,18 +324,18 @@ static struct wlr_swapchain *create_swapchain(struct wlr_output *output, if (!allow_modifiers && (format->len != 1 || format->modifiers[0] != DRM_FORMAT_MOD_LINEAR)) { if (!wlr_drm_format_has(format.get(), DRM_FORMAT_MOD_INVALID)) { qCWarning(lcWlOutputDrm) << "No support for implicit modifiers"; - return NULL; + return NULL; } format->len = 0; if (!wlr_drm_format_add(format.get(), DRM_FORMAT_MOD_INVALID)) { qCWarning(lcWlOutputDrm) << "Failed to add implicit modifier to DRM format"; - return NULL; + return NULL; } } struct wlr_swapchain *swapchain = wlr_swapchain_create(allocator, width, height, format.get()); - return swapchain; + return swapchain; } static bool test_swapchain(struct wlr_output *output, @@ -341,10 +355,13 @@ static bool test_swapchain(struct wlr_output *output, static bool wlr_output_configure_primary_swapchain(struct wlr_output *output, int width, int height, uint32_t format, struct wlr_swapchain **swapchain_ptr, - bool test) { + bool test, struct wlr_swapchain **replaced_swapchain_ptr) { WOutputStateGuard empty_state; wlr_output_state *state = empty_state.get(); + if (replaced_swapchain_ptr) + *replaced_swapchain_ptr = nullptr; + // Re-use the existing swapchain if possible struct wlr_swapchain *old_swapchain = *swapchain_ptr; if (old_swapchain != NULL && @@ -382,7 +399,11 @@ static bool wlr_output_configure_primary_swapchain(struct wlr_output *output, in } } - wlr_swapchain_destroy(*swapchain_ptr); + if (replaced_swapchain_ptr) { + *replaced_swapchain_ptr = *swapchain_ptr; + } else if (*swapchain_ptr) { + wlr_swapchain_destroy(*swapchain_ptr); + } *swapchain_ptr = swapchain; return true; } @@ -408,23 +429,36 @@ static bool output_pick_cursor_format(struct wlr_output *output, // End bool WOutput::configurePrimarySwapchain(const QSize &size, uint32_t format, - wlr_swapchain **swapchain, bool doTest) + wlr_swapchain **swapchain, bool doTest, + wlr_swapchain **replacedSwapchain) { W_D(WOutput); Q_ASSERT(!size.isEmpty()); + if (replacedSwapchain) + *replacedSwapchain = nullptr; + wlr_swapchain *sc = *swapchain; + wlr_swapchain *replacedSc = nullptr; bool ok = wlr_output_configure_primary_swapchain(d->handle(), size.width(), size.height(), - format, &sc, doTest); + format, &sc, doTest, + replacedSwapchain ? &replacedSc : nullptr); if (!ok) return false; *swapchain = sc; + if (replacedSwapchain) + *replacedSwapchain = replacedSc; return true; } -bool WOutput::configureCursorSwapchain(const QSize &size, uint32_t drmFormat, wlr_swapchain **swapchain) +bool WOutput::configureCursorSwapchain(const QSize &size, uint32_t drmFormat, + wlr_swapchain **swapchain, + wlr_swapchain **replacedSwapchain) { W_D(WOutput); Q_ASSERT(!size.isEmpty()); + if (replacedSwapchain) + *replacedSwapchain = nullptr; + auto sc = *swapchain; if (!sc || sc->width != size.width() || sc->height != size.height()) { WDrmFormat format; @@ -433,12 +467,18 @@ bool WOutput::configureCursorSwapchain(const QSize &size, uint32_t drmFormat, wl return false; } - wlr_swapchain_destroy(sc); + auto oldSc = sc; sc = wlr_swapchain_create(allocator(), size.width(), size.height(), format.get()); - if (!sc) { + if (!sc) { qCDebug(lcWlOutputBuffer) << "Failed to create cursor swapchain with selected format"; return false; } + + if (replacedSwapchain) { + *replacedSwapchain = oldSc; + } else if (oldSc) { + wlr_swapchain_destroy(oldSc); + } } *swapchain = sc; diff --git a/waylib/src/server/kernel/woutput.h b/waylib/src/server/kernel/woutput.h index 2bdfef112f..b39e3aa8e6 100644 --- a/waylib/src/server/kernel/woutput.h +++ b/waylib/src/server/kernel/woutput.h @@ -67,9 +67,11 @@ class WAYLIB_SERVER_EXPORT WOutput : public QObject, public WObject wlr_allocator *allocator() const; bool configurePrimarySwapchain(const QSize &size, uint32_t format, wlr_swapchain **swapchain, - bool doTest = true); + bool doTest = true, + wlr_swapchain **replacedSwapchain = nullptr); bool configureCursorSwapchain(const QSize &size, uint32_t format, - wlr_swapchain **swapchain); + wlr_swapchain **swapchain, + wlr_swapchain **replacedSwapchain = nullptr); wlr_output *handle() const; diff --git a/waylib/src/server/kernel/wsurface.cpp b/waylib/src/server/kernel/wsurface.cpp index 62863f84b9..21932bbd84 100644 --- a/waylib/src/server/kernel/wsurface.cpp +++ b/waylib/src/server/kernel/wsurface.cpp @@ -5,6 +5,7 @@ #include "wscoplistener.h" #include "wseat.h" #include "private/wsurface_p.h" +#include "utils/private/wvulkantrace_p.h" #include "woutput.h" #include "wsubsurface.h" #include "wayliblogging.h" @@ -64,6 +65,13 @@ void WSurfacePrivate::connect() { W_Q(WSurface); q->listeners()->add(&m_handle->events.commit, this, &WSurfacePrivate::on_commit); + if (WVulkanTrace::enabled()) { + q->listeners()->add(&m_handle->events.commit, q, [q, this] (void *) { + WVulkanTrace::surfaceCommit(q, q->pid(), m_handle->current.seq, + m_handle->current.committed, + q->buffer()); + }); + } q->listeners()->add(&m_handle->events.map, q, &WSurface::mappedChanged); q->listeners()->add(&m_handle->events.unmap, q, &WSurface::mappedChanged); q->listeners()->add(&m_handle->events.new_subsurface, q, diff --git a/waylib/src/server/protocols/WLinuxDmabufV1 b/waylib/src/server/protocols/WLinuxDmabufV1 new file mode 100644 index 0000000000..5587ab15b1 --- /dev/null +++ b/waylib/src/server/protocols/WLinuxDmabufV1 @@ -0,0 +1 @@ +#include "wlinuxdmabufv1.h" diff --git a/waylib/src/server/protocols/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..2f59ee4f75 --- /dev/null +++ b/waylib/src/server/protocols/wlinuxdmabufv1.cpp @@ -0,0 +1,82 @@ +// Copyright (C) 2026 UnionTech Software Technology Co., Ltd. +// SPDX-License-Identifier: Apache-2.0 OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#include "wlinuxdmabufv1.h" + +#include "wayliblogging.h" + +#include + +WAYLIB_SERVER_BEGIN_NAMESPACE + +constexpr uint32_t LinuxDmabufV1Version = 4; + +WLinuxDmabufV1::WLinuxDmabufV1(wlr_renderer *renderer, QObject *parent) + : QObject(parent) + , m_renderer(renderer) +{ +} + +wlr_linux_dmabuf_v1 *WLinuxDmabufV1::handle() const +{ + return nativeInterface(); +} + +QByteArrayView WLinuxDmabufV1::interfaceName() const +{ + return "zwp_linux_dmabuf_v1"; +} + +void WLinuxDmabufV1::create(WServer *server) +{ + if (m_handle) + return; + + if (!m_renderer) { + qCWarning(lcWlLinuxDmabuf) << "Cannot create linux-dmabuf global without renderer"; + return; + } + + const auto *formats = wlr_renderer_get_texture_formats( + m_renderer, WLR_BUFFER_CAP_DMABUF); + if (!formats) { + qCInfo(lcWlLinuxDmabuf) << "Skipping linux-dmabuf global: renderer does not expose DMA-BUF texture formats"; + return; + } + + const int drmFd = wlr_renderer_get_drm_fd(m_renderer); + if (drmFd < 0) { + qCInfo(lcWlLinuxDmabuf) << "Skipping linux-dmabuf global: renderer has no DRM fd"; + return; + } + + m_handle = wlr_linux_dmabuf_v1_create_with_renderer( + server->handle(), LinuxDmabufV1Version, m_renderer); + if (!m_handle) { + qCWarning(lcWlLinuxDmabuf) << "Failed to create renderer-backed linux-dmabuf global"; + return; + } + + qCInfo(lcWlLinuxDmabuf) << "Created renderer-backed linux-dmabuf global" + << "version" << LinuxDmabufV1Version + << "drm fd" << drmFd; +} + +void WLinuxDmabufV1::destroy([[maybe_unused]] WServer *server) +{ + // wlroots owns this global and destroys it from the display destroy + // listener; null m_handle so handle()/global() return null instead of a + // dangling pointer (kept as an explicit override rather than the inherited + // empty base for this hardening). + m_handle = nullptr; +} + +wl_global *WLinuxDmabufV1::global() const +{ + if (auto *native = handle()) + return native->global; + + return nullptr; +} + +WAYLIB_SERVER_END_NAMESPACE diff --git a/waylib/src/server/protocols/wlinuxdmabufv1.h b/waylib/src/server/protocols/wlinuxdmabufv1.h new file mode 100644 index 0000000000..e0b4c984c7 --- /dev/null +++ b/waylib/src/server/protocols/wlinuxdmabufv1.h @@ -0,0 +1,31 @@ +// Copyright (C) 2026 UnionTech Software Technology Co., Ltd. +// SPDX-License-Identifier: Apache-2.0 OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#pragma once + +#include +#include + +WAYLIB_SERVER_BEGIN_NAMESPACE + +class WAYLIB_SERVER_EXPORT WLinuxDmabufV1 : public QObject, public WServerInterface +{ + Q_OBJECT + +public: + explicit WLinuxDmabufV1(wlr_renderer *renderer, QObject *parent = nullptr); + + wlr_linux_dmabuf_v1 *handle() const; + + QByteArrayView interfaceName() const override; + +protected: + void create(WServer *server) override; + void destroy(WServer *server) override; + wl_global *global() const override; + +private: + wlr_renderer *m_renderer = nullptr; +}; + +WAYLIB_SERVER_END_NAMESPACE diff --git a/waylib/src/server/protocols/wpresentation.cpp b/waylib/src/server/protocols/wpresentation.cpp new file mode 100644 index 0000000000..8875bd0777 --- /dev/null +++ b/waylib/src/server/protocols/wpresentation.cpp @@ -0,0 +1,84 @@ +// Copyright (C) 2026 UnionTech Software Technology Co., Ltd. +// SPDX-License-Identifier: Apache-2.0 OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#include "wpresentation.h" + +#include "wayliblogging.h" +#include "woutput.h" +#include "wsurface.h" + +#include + +WAYLIB_SERVER_BEGIN_NAMESPACE + +constexpr uint32_t PresentationVersion = 2; + +WPresentation::WPresentation(wlr_backend *backend, QObject *parent) + : QObject(parent) + , m_backend(backend) +{ +} + +wlr_presentation *WPresentation::handle() const +{ + return nativeInterface(); +} + +QByteArrayView WPresentation::interfaceName() const +{ + return "wp_presentation"; +} + +void WPresentation::surfaceTexturedOnOutput(WSurface *surface, WOutput *output) +{ + auto *native = handle(); + if (!native || !surface || !surface->handle() || !output || !output->handle()) + return; + + wlr_presentation_surface_textured_on_output(surface->handle(), output->handle()); + qCDebug(lcWlPresentation) << "Marked surface textured on output" + << "surface" << surface + << "wlrSurface" << surface->handle() + << "output" << output + << "wlrOutput" << output->handle(); +} + +void WPresentation::create(WServer *server) +{ + if (m_handle) + return; + + if (!m_backend) { + qCWarning(lcWlPresentation) << "Cannot create presentation-time global without backend"; + return; + } + + m_handle = wlr_presentation_create(server->handle(), m_backend, + PresentationVersion); + if (!m_handle) { + qCWarning(lcWlPresentation) << "Failed to create presentation-time global"; + return; + } + + qCInfo(lcWlPresentation) << "Created presentation-time global" + << "version" << PresentationVersion; +} + +void WPresentation::destroy([[maybe_unused]] WServer *server) +{ + // wlroots owns this global and destroys it from the display destroy + // listener; null m_handle so handle()/global() return null instead of a + // dangling pointer (kept as an explicit override rather than the inherited + // empty base for this hardening). + m_handle = nullptr; +} + +wl_global *WPresentation::global() const +{ + if (auto *native = handle()) + return native->global; + + return nullptr; +} + +WAYLIB_SERVER_END_NAMESPACE diff --git a/waylib/src/server/protocols/wpresentation.h b/waylib/src/server/protocols/wpresentation.h new file mode 100644 index 0000000000..d6ccfff02d --- /dev/null +++ b/waylib/src/server/protocols/wpresentation.h @@ -0,0 +1,36 @@ +// Copyright (C) 2026 UnionTech Software Technology Co., Ltd. +// SPDX-License-Identifier: Apache-2.0 OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#pragma once + +#include +#include + +WAYLIB_SERVER_BEGIN_NAMESPACE + +class WOutput; +class WSurface; + +class WAYLIB_SERVER_EXPORT WPresentation : public QObject, public WServerInterface +{ + Q_OBJECT + +public: + explicit WPresentation(wlr_backend *backend, QObject *parent = nullptr); + + wlr_presentation *handle() const; + + QByteArrayView interfaceName() const override; + + void surfaceTexturedOnOutput(WSurface *surface, WOutput *output); + +protected: + void create(WServer *server) override; + void destroy(WServer *server) override; + wl_global *global() const override; + +private: + wlr_backend *m_backend = nullptr; +}; + +WAYLIB_SERVER_END_NAMESPACE diff --git a/waylib/src/server/qtquick/private/wbufferrenderer.cpp b/waylib/src/server/qtquick/private/wbufferrenderer.cpp index 224e6cf531..8648df7f34 100644 --- a/waylib/src/server/qtquick/private/wbufferrenderer.cpp +++ b/waylib/src/server/qtquick/private/wbufferrenderer.cpp @@ -6,15 +6,19 @@ #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 @@ -102,10 +106,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 +280,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 +314,9 @@ wlr_damage_ring *WBufferRenderer::damageRing() bool WBufferRenderer::isTextureProvider() const { + if (isPrimaryOutputRendererForVulkan()) + return false; + return true; } @@ -288,6 +327,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 +345,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 +385,49 @@ QTransform WBufferRenderer::inputMapToOutput(const QRectF &sourceRect, const QRe return t; } +static QSGRenderTarget toRhiSgRenderTarget(QQuickWindowPrivate *wd, const QQuickRenderTarget &rt); + +static bool hasActiveRenderBufferBlitter(QQuickItem *root) +{ + if (!root || !root->isVisible()) + return false; + + // No blitter exists anywhere: skip the recursive walk entirely. This runs + // for every WBufferRenderer::beginRender() on Vulkan. + if (WRenderBufferBlitter::liveCount() == 0) + return false; + + // An effectively invisible root cannot have an effectively visible + // descendant, so the visibility check above is exhaustive. childItems is + // used instead of paintOrderChildItems() because only existence matters + // here and paintOrderChildItems() returns a QList by value. + QVarLengthArray items; + items.append(root); + while (!items.isEmpty()) { + auto *item = items.last(); + items.removeLast(); + if (!item) + continue; + + if (item->isVisible() + && item->opacity() > 0.0 + && item->width() > 0.0 + && item->height() > 0.0 + && qobject_cast(item)) { + return true; + } + + const auto &children = QQuickItemPrivate::get(item)->childItems; + for (auto child = children.crbegin(); child != children.crend(); ++child) + items.append(*child); + } + + return false; +} + wlr_buffer *WBufferRenderer::beginRender(const QSize &pixelSize, qreal devicePixelRatio, - uint32_t format, RenderFlags flags, - WGlobal::ColorContentsMode mode) + uint32_t format, RenderFlags flags, + WGlobal::ColorContentsMode mode) { Q_ASSERT(!state.buffer); Q_ASSERT(m_output); @@ -341,6 +437,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 +450,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 +460,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 +471,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 +492,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 +535,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 +555,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 +597,57 @@ 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; + + // Single traversal that collects the providers directly: the previous + // implementation materialised the whole subtree as a QList via + // paintOrderItemList() and then deduplicated with a QSet. Only the set of + // providers matters, so unsorted childItems and a linear dedup are enough + // (the list is tiny) and avoid the per-frame allocations. + QVarLengthArray items; + items.append(root); + while (!items.isEmpty()) { + auto *item = items.last(); + items.removeLast(); + if (!item) + continue; + + if (item->isVisible() + && item->flags().testFlag(QQuickItem::ItemHasContents) + && item->isTextureProvider()) { + auto *provider = qobject_cast(item->textureProvider()); + if (provider && !providers.contains(provider)) + providers.append(provider); + } + + const auto &children = QQuickItemPrivate::get(item)->childItems; + for (auto child = children.crbegin(); child != children.crend(); ++child) + items.append(*child); + } + + return providers; +} + +bool WBufferRenderer::render(int sourceIndex, const QMatrix4x4 &renderMatrix, + const QRectF &sourceRect, const QRectF &targetRect, + std::optional preserveColorContentsOverride) { Q_ASSERT(state.buffer); @@ -458,6 +659,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 +696,15 @@ 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(); + const bool clearColor = !preserveColorContents; #if QT_VERSION >= QT_VERSION_CHECK(6, 9, 0) softwareRenderer->setClearColorEnabled(clearColor); #else @@ -532,7 +747,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 +788,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 +881,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 +981,11 @@ void WBufferRenderer::updateTextureProvider() if (!m_textureProvider) return; + if (isPrimaryOutputRendererForVulkan()) { + m_textureProvider->setBuffer(nullptr); + return; + } + if (shouldCacheBuffer()) { const bool hasCachedBuffer = m_textureProvider->wlrBuffer(); // Ensure only update the buffer when the "shouldCacheBuffer" state is changed. @@ -691,12 +999,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 +1029,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/woutputitem_p.h b/waylib/src/server/qtquick/private/woutputitem_p.h index 255e9074c5..dccca67724 100644 --- a/waylib/src/server/qtquick/private/woutputitem_p.h +++ b/waylib/src/server/qtquick/private/woutputitem_p.h @@ -34,6 +34,10 @@ class WAYLIB_SERVER_EXPORT WOutputCursor : public QObject WCursor *cursor() const; bool visible() const; + // The QML cursor delegate item (a WQuickCursor). Used to drive the DRM + // hardware cursor directly on pointer movement without a full frame. + QQuickItem *cursorItem() const { return item; } + Q_SIGNALS: void visibleChanged(); diff --git a/waylib/src/server/qtquick/private/wrenderbuffernode.cpp b/waylib/src/server/qtquick/private/wrenderbuffernode.cpp index 34d858744f..2a0b062bbe 100644 --- a/waylib/src/server/qtquick/private/wrenderbuffernode.cpp +++ b/waylib/src/server/qtquick/private/wrenderbuffernode.cpp @@ -12,16 +12,17 @@ #include "wqmlhelper_p.h" #include "platformplugin/types.h" #include "private/wprivateaccessor_p.h" +#include "utils/private/wvulkantrace_p.h" #include #include #include +#include #include #include #include #include -#include #include #include #include @@ -31,8 +32,8 @@ #include -// QSGRenderer: access protected methods preprocess()/render() and private -// bit-field members m_changed_emitted/m_is_rendering. +// QSGRenderer: access protected render stages and private bit-field members +// m_changed_emitted/m_is_rendering. // // preprocess() and render() are protected virtual functions — the template // accessor [temp.explicit]/12 trick legally bypasses access control here too. @@ -134,6 +135,9 @@ class Q_DECL_HIDDEN DataManagerPointer DataManagerPointer &operator=(const DataManagerPointer &other) noexcept { + if (this == &other) + return *this; + deref(); pointer = other.pointer; ref(); @@ -142,6 +146,10 @@ class Q_DECL_HIDDEN DataManagerPointer DataManagerPointer &operator=(DataManagerPointer &&other) noexcept { + if (this == &other) + return *this; + + deref(); pointer = std::exchange(other.pointer, nullptr); return *this; } @@ -151,6 +159,9 @@ class Q_DECL_HIDDEN DataManagerPointer } inline DataManagerPointer &operator=(T* p) { + if (pointer == p) + return *this; + deref(); pointer = p; ref(); @@ -354,47 +365,107 @@ struct WlrAndRhiTexture { struct wlr_buffer *buffer = nullptr; wlr_texture *wlrTexture = nullptr; QRhiTexture *rhiTexture = nullptr; + uint32_t drmFormat = 0; + uint64_t drmModifier = 0; + QRhiTexture::Flags flags; + bool vulkanOwned = false; }; -class Q_DECL_HIDDEN RhiTextureManager : public DataManager +class Q_DECL_HIDDEN RhiTextureManager : public DataManager { Q_OBJECT friend class DataManager; RhiTextureManager(QQuickWindow *owner) - : DataManager(owner) { + : DataManager(owner) { Q_ASSERT(owner->findChildren(Qt::FindDirectChildrenOnly).size() == 1); } - static bool check(WlrAndRhiTexture *texture, QRhiTexture::Format, uint32_t drmFormat, uint64_t drmModifier, const QSize &size) { - auto *buffer = texture->buffer; - wlr_dmabuf_attributes attribs; - if (!wlr_buffer_get_dmabuf(buffer, &attribs)) { - qCWarning(lcWlRenderBuffer) << "Failed to get dmabuf attributes for texture" << texture << "with buffer" << buffer - << ", Can't check texture without dmabuf attributes"; + static bool check(WlrAndRhiTexture *texture, bool isVulkan, + QRhiTexture::Format, + uint32_t drmFormat, uint64_t drmModifier, + const QSize &size, QRhiTexture::Flags flags) { + if (texture->vulkanOwned != isVulkan) return false; + + if (!isVulkan) { + auto buffer = texture->buffer; + wlr_dmabuf_attributes attribs; + if (!wlr_buffer_get_dmabuf(buffer, &attribs)) { + qCWarning(lcWlRenderBuffer) + << "Failed to get dmabuf attributes for texture" << texture + << "with buffer" << buffer + << ", Can't check texture without dmabuf attributes"; + return false; + } + return attribs.format == drmFormat + && attribs.modifier == drmModifier + && texture->rhiTexture->pixelSize() == size; } - return attribs.format == drmFormat && attribs.modifier == drmModifier && texture->rhiTexture->pixelSize() == size; + + return texture->drmFormat == drmFormat + && texture->drmModifier == drmModifier + && texture->rhiTexture + && texture->rhiTexture->pixelSize() == size + && texture->flags == flags; } - WlrAndRhiTexture *create(QRhiTexture::Format format, uint32_t drmFormat, uint64_t drmModifier, const QSize &size) { - auto ow = qobject_cast(owner()); - auto texture = WRenderHelper::newTexture(ow->allocator(), ow->renderer(), - drmFormat, drmModifier, owner()->rhi(), - size, format, QRhiTexture::RenderTarget); - if (!texture.rhiTexture) + WlrAndRhiTexture *create(bool isVulkan, QRhiTexture::Format format, + uint32_t drmFormat, uint64_t drmModifier, + const QSize &size, QRhiTexture::Flags flags) { + if (!isVulkan) { + auto ow = qobject_cast(owner()); + auto texture = WRenderHelper::newTexture(ow->allocator(), ow->renderer(), + drmFormat, drmModifier, owner()->rhi(), + size, format, QRhiTexture::RenderTarget); + if (!texture.rhiTexture) + return nullptr; + + return new WlrAndRhiTexture { + texture.buffer, + texture.texture, + texture.rhiTexture, + 0, + 0, + {}, + false, + }; + } + + auto texture = owner()->rhi()->newTexture(format, size, 1, + QRhiTexture::RenderTarget | flags); + texture->setName(QByteArrayLiteral("WaylibRenderBufferNodeTexture")); + if (!texture->create()) { + qCWarning(lcWlRenderBuffer) << "Failed to create QRhi texture for WRenderBufferNode" + << "format" << format + << "drmFormat" << drmFormat + << "modifier" << drmModifier + << "size" << size; + delete texture; return nullptr; + } - return new WlrAndRhiTexture{texture.buffer, texture.texture, texture.rhiTexture}; + return new WlrAndRhiTexture { + nullptr, + nullptr, + texture, + drmFormat, + drmModifier, + flags, + true, + }; } static void destroy(WlrAndRhiTexture *texture) { delete texture->rhiTexture; - if (texture->wlrTexture) - wlr_texture_destroy(texture->wlrTexture); - if (texture->buffer) - wlr_buffer_drop(texture->buffer); + if (!texture->vulkanOwned) { + if (texture->wlrTexture) + wlr_texture_destroy(texture->wlrTexture); + if (texture->buffer) + wlr_buffer_drop(texture->buffer); + } delete texture; } }; @@ -519,18 +590,25 @@ class Q_DECL_HIDDEN RhiManager : public DataManager RhiManager(QQuickWindow *owner) : DataManager(owner) { Q_ASSERT(owner->findChildren(Qt::FindDirectChildrenOnly).size() == 1); - std::unique_ptr fallbackSurface(new QW::OffscreenSurface(nullptr)); - fallbackSurface->create(); + if (owner->rhi()->backend() == QRhi::Vulkan) { + m_rhi.reset(new Rhi()); + m_rhi->rhi = owner->rhi(); + m_rhi->own = false; + m_rhi->gc = owner->graphicsConfiguration(); + } else { + std::unique_ptr fallbackSurface(new QW::OffscreenSurface(nullptr)); + fallbackSurface->create(); - auto rhi = QSGRhiSupport::instance()->createRhi(owner, fallbackSurface.get()); - if (!rhi.rhi) - return; - Q_ASSERT(rhi.rhi->backend() == owner->rhi()->backend()); - m_rhi.reset(new Rhi()); - m_rhi->rhi = rhi.rhi; - m_rhi->own = rhi.own; - m_rhi->gc = owner->graphicsConfiguration(); - m_rhi->offscreenSurface = fallbackSurface.release(); + auto rhi = QSGRhiSupport::instance()->createRhi(owner, fallbackSurface.get()); + if (!rhi.rhi) + return; + Q_ASSERT(rhi.rhi->backend() == owner->rhi()->backend()); + m_rhi.reset(new Rhi()); + m_rhi->rhi = rhi.rhi; + m_rhi->own = rhi.own; + m_rhi->gc = owner->graphicsConfiguration(); + m_rhi->offscreenSurface = fallbackSurface.release(); + } context = QQuickWindowPrivate::get(owner)->context; // Don't use RenderMode2D, when use this renderer on an exists renderTarget @@ -561,9 +639,9 @@ class Q_DECL_HIDDEN RhiManager : public DataManager } struct Rhi { - QRhi *rhi; - QOffscreenSurface *offscreenSurface; - bool own; + QRhi *rhi = nullptr; + QOffscreenSurface *offscreenSurface = nullptr; + bool own = false; QQuickGraphicsConfiguration gc; ~Rhi() { @@ -587,6 +665,206 @@ class Q_DECL_HIDDEN RhiManager : public DataManager QScopedPointer m_rhi; }; +// A QSG batch renderer owns its Elements and their shader-resource bindings. +// Reusing one renderer for multiple blitter roots in the same Vulkan command +// buffer lets Qt recycle and update a descriptor set that an earlier root has +// already bound. Keep one renderer per blitter role instead. Qt's render +// context still shares shader and pipeline caches between these renderers. +class Q_DECL_HIDDEN VulkanInlineRenderer +{ +public: + VulkanInlineRenderer(QQuickWindow *owner, const void *blitterNode, + const char *role) + : m_owner(owner) + , m_blitterNode(blitterNode) + , m_role(role) + { + if (!owner || !owner->rhi() + || owner->rhi()->backend() != QRhi::Vulkan) { + return; + } + + m_rhi = owner->rhi(); + m_graphicsConfiguration = owner->graphicsConfiguration(); + m_context = QQuickWindowPrivate::get(owner)->context; + if (!m_context) + return; + + // RenderMode2DNoDepthBuffer matches the existing RhiManager path: the + // surrounding compositor renderer owns the shared depth attachment. + m_renderer = m_context->createRenderer( + QSGRendererInterface::RenderMode2DNoDepthBuffer); + + if (m_renderer && WVulkanTrace::enabled()) { + qCDebug(lcWlRenderBuffer).noquote() + << QStringLiteral("VKTRACE event=blitter-inline-renderer-create node=%1 role=%2 renderer=%3") + .arg(quintptr(m_blitterNode), 0, 16) + .arg(QString::fromLatin1(m_role)) + .arg(quintptr(m_renderer), 0, 16); + } + } + + ~VulkanInlineRenderer() + { + if (!m_renderer) + return; + + Q_ASSERT(!m_prepared); + m_renderer->setRootNode(nullptr); + if (WVulkanTrace::enabled()) { + qCDebug(lcWlRenderBuffer).noquote() + << QStringLiteral("VKTRACE event=blitter-inline-renderer-destroy node=%1 role=%2 renderer=%3") + .arg(quintptr(m_blitterNode), 0, 16) + .arg(QString::fromLatin1(m_role)) + .arg(quintptr(m_renderer), 0, 16); + } + delete m_renderer; + } + + bool isValid() const + { + return m_renderer && m_rhi && m_context; + } + + QQuickWindow *owner() const + { + return m_owner; + } + + QSGRenderer *renderer() const + { + return m_renderer; + } + + QSGRootNode *rootNode() const + { + return m_renderer ? m_renderer->rootNode() : nullptr; + } + + bool sync(const QSize &pixelSize, QSGRootNode *rootNode, + const QMatrix4x4 &matrix = {}, + const QMatrix4x4 &baseProjectionMatrix = {}, + QSGRenderer *base = nullptr, const QVector2D &dpr = {}) + { + if (!isValid() || !rootNode || m_prepared) + return false; + + if (base) { + m_renderer->setDevicePixelRatio(base->devicePixelRatio()); + m_renderer->setDeviceRect(base->deviceRect()); + m_renderer->setViewportRect(base->viewportRect()); + m_renderer->setProjectionMatrix(baseProjectionMatrix); +#if QT_VERSION >= QT_VERSION_CHECK(6, 8, 0) + m_renderer->setProjectionMatrixWithNativeNDC( + base->projectionMatrixWithNativeNDC(0)); +#else + m_renderer->setProjectionMatrixWithNativeNDC( + base->projectionMatrixWithNativeNDC()); +#endif + } else { + if (dpr.x() <= 0.0f || dpr.y() <= 0.0f) + return false; + + m_renderer->setDevicePixelRatio(1.0); + m_renderer->setDeviceRect(QRect(QPoint(0, 0), pixelSize)); + m_renderer->setViewportRect(pixelSize); + const QRectF rect( + QPointF(0, 0), + QSizeF(pixelSize.width() / dpr.x(), + pixelSize.height() / dpr.y())); + // The backdrop is exposed as an ordinary, unmirrored QSGTexture. + // Match QQuickWindow's projection for a redirected render target + // so Vulkan's Y-down NDC does not turn the rotated copy upside down. + m_renderer->setProjectionMatrixToRect( + rect, + !m_rhi->isYUpInNDC() + ? QSGRenderer::MatrixTransformFlipY + : QSGRenderer::MatrixTransformFlag {}); + } + + if (Q_UNLIKELY(!matrix.isIdentity())) { +#if QT_VERSION >= QT_VERSION_CHECK(6, 8, 0) + m_renderer->setProjectionMatrix( + m_renderer->projectionMatrix(0) * matrix); + m_renderer->setProjectionMatrixWithNativeNDC( + m_renderer->projectionMatrixWithNativeNDC(0) * matrix); +#else + m_renderer->setProjectionMatrix( + m_renderer->projectionMatrix() * matrix); + m_renderer->setProjectionMatrixWithNativeNDC( + m_renderer->projectionMatrixWithNativeNDC() * matrix); +#endif + } + + auto *oldRoot = m_renderer->rootNode(); + m_renderer->setRootNode(rootNode); + if (oldRoot != rootNode && WVulkanTrace::enabled()) { + qCDebug(lcWlRenderBuffer).noquote() + << QStringLiteral("VKTRACE event=blitter-inline-renderer-root node=%1 role=%2 renderer=%3 old=%4 new=%5") + .arg(quintptr(m_blitterNode), 0, 16) + .arg(QString::fromLatin1(m_role)) + .arg(quintptr(m_renderer), 0, 16) + .arg(quintptr(oldRoot), 0, 16) + .arg(quintptr(rootNode), 0, 16); + } + return true; + } + + bool prepareInline(QRhiRenderTarget *renderTarget, + QRhiCommandBuffer *commandBuffer) + { + if (!isValid() || !rootNode() || !renderTarget || !commandBuffer + || m_prepared) { + return false; + } + + m_renderer->setRenderTarget( + {renderTarget, renderTarget->renderPassDescriptor(), commandBuffer}); + auto *defaultContext = static_cast(m_context); + m_oldDpr = defaultContext->currentDevicePixelRatio(); + m_oldCommandBuffer = defaultContext->currentFrameCommandBuffer(); + m_context->prepareSync(m_renderer->devicePixelRatio(), commandBuffer, + m_graphicsConfiguration); + + // These public Qt stages deliberately split resource preparation from + // draw recording so the caller can begin the compatible render pass in + // between. They also maintain QSGRenderer's private state themselves. + m_renderer->prepareSceneInline(); + m_prepared = true; + return true; + } + + void renderInline() + { + Q_ASSERT(m_renderer && m_prepared); + m_renderer->renderSceneInline(); + m_context->prepareSync(m_oldDpr, m_oldCommandBuffer, + m_graphicsConfiguration); + m_oldCommandBuffer = nullptr; + m_prepared = false; + } + + void detachRoot() + { + if (!m_renderer) + return; + Q_ASSERT(!m_prepared); + m_renderer->setRootNode(nullptr); + } + +private: + QQuickWindow *m_owner = nullptr; + const void *m_blitterNode = nullptr; + const char *m_role = nullptr; + QRhi *m_rhi = nullptr; + QSGRenderContext *m_context = nullptr; + QSGRenderer *m_renderer = nullptr; + QQuickGraphicsConfiguration m_graphicsConfiguration; + qreal m_oldDpr = 1.0; + QRhiCommandBuffer *m_oldCommandBuffer = nullptr; + bool m_prepared = false; +}; + static QSizeF mapSize(const QRectF &source, const QMatrix4x4 &matrix) { auto topLeft = matrix.map(source.topLeft()); @@ -653,28 +931,42 @@ class Q_DECL_HIDDEN RhiNode : public WRenderBufferNode { } StateFlags changedStates() const override { - if (Q_UNLIKELY(renderData && !contentNode)) - return (RenderTargetState | ViewportState); - - return {0}; - } - - // ###: Should disable DepthAwareRendering here, because - // if enable depth test, Qt will render the opaque nodes first, - // maybe an opaque node's z-order is higher than the render node, - // will get the wrong texture when copy texture in RhiNode::render. - // But the opaque node will be cover the wrong area to the - // RhiNode::renderTarget after RhiNode::render, but if you using - // MultiEffect's blur effect for the copied texture, the blur result - // will contains the opaque node's contains. Fortunately, this kind - // of problem is not very obvious. - // - // We should fix this bug in Qt in the future. + const bool isVulkan = m_item && m_item->window() + && m_item->window()->graphicsApi() == QSGRendererInterface::Vulkan; + if (!isVulkan) { + if (Q_UNLIKELY(renderData && !contentNode)) + return RenderTargetState | ViewportState; + return {}; + } + + return DepthState + | StencilState + | ScissorState + | ColorState + | BlendState + | CullState + | ViewportState + | RenderTargetState; + } + RenderingFlags flags() const override { - if (Q_UNLIKELY(!contentNode)) - return BoundedRectRendering | DepthAwareRendering; + const bool isVulkan = m_item && m_item->window() + && m_item->window()->graphicsApi() + == QSGRendererInterface::Vulkan; + if (!isVulkan) { + if (Q_UNLIKELY(!contentNode)) + return BoundedRectRendering | DepthAwareRendering; + return DepthAwareRendering; + } - return DepthAwareRendering; + // Vulkan backdrop rendering must remain in painter order. Advertising + // depth awareness lets Qt move opaque nodes above this node ahead of + // it, so they get copied into the backdrop and are then overpainted by + // the separately rendered effect content. + RenderingFlags flags = NoExternalRendering; + if (!contentNode) + flags |= BoundedRectRendering; + return flags; } void releaseResources() override { @@ -703,6 +995,10 @@ class Q_DECL_HIDDEN RhiNode : public WRenderBufferNode { void prepare() override { contentNode = nullptr; + vulkanReady = false; + vulkanResumeTarget = nullptr; + vulkanCopySourceRect = {}; + vulkanCopyDestination = {}; if (Q_UNLIKELY(!m_item || !m_item->window())) { reset(); @@ -729,15 +1025,81 @@ class Q_DECL_HIDDEN RhiNode : public WRenderBufferNode { } const auto currentRenderer = maybeBufferRenderer(); + const bool isVulkan = window->rhi() + && window->rhi()->backend() == QRhi::Vulkan; + if (isVulkan) { + if (!currentRenderer) { + setVulkanUnavailable("not rendering through WBufferRenderer", ct); + return; + } + if (!ct->flags().testFlag(QRhiTexture::UsedAsTransferSource)) { + setVulkanUnavailable("output modifier does not support transfer-source usage", ct); + return; + } + if (ct->sampleCount() != 1) { + setVulkanUnavailable("multisampled output copy is unsupported", ct); + return; + } + + // The backdrop targets are created without a depth attachment (see + // WRenderHelperPrivate::ensureVulkanBackdropRenderTargets): resuming + // the outer pass without depth is only valid while the surrounding + // renderer has the 2D depth buffer disabled, which this node's own + // rendering flags force (no DepthAwareRendering). Fail closed if + // that ever stops holding. + if (currentRenderer->currentBatchRenderer() + && rendererUseDepthBuffer(currentRenderer->currentBatchRenderer())) { + setVulkanUnavailable("the surrounding renderer uses the depth buffer", ct); + return; + } + + auto resumeTarget = currentRenderer->currentVulkanBackdropResumeTarget(); + if (!resumeTarget + || resumeTarget->resourceType() != QRhiResource::TextureRenderTarget + || renderTarget()->resourceType() != QRhiResource::TextureRenderTarget) { + setVulkanUnavailable("Vulkan backdrop resume target is unavailable", ct); + return; + } + + auto currentTextureTarget = static_cast(renderTarget()); + auto resumeTextureTarget = static_cast(resumeTarget); + const auto currentDesc = currentTextureTarget->description(); + const auto resumeDesc = resumeTextureTarget->description(); + if (currentDesc.colorAttachmentCount() < 1 + || resumeDesc.colorAttachmentCount() < 1 + || currentDesc.colorAttachmentAt(0)->texture() != ct + || resumeDesc.colorAttachmentAt(0)->texture() != ct + // The backdrop set must stay depth-free on both sides, and the + // resumed pass must preserve the color attachment. + || currentDesc.depthTexture() + || resumeDesc.depthTexture() + || currentDesc.depthStencilBuffer() + || resumeDesc.depthStencilBuffer() + || !resumeTextureTarget->flags().testFlag( + QRhiTextureRenderTarget::PreserveColorContents)) { + setVulkanUnavailable("Vulkan backdrop target attachments are incompatible", ct); + return; + } + vulkanResumeTarget = resumeTextureTarget; + } + // TODO: Apple viewport to matrix, needs get QSGRenderer renderMatrix = currentRenderer ? currentRenderer->currentWorldTransform() * (*this->matrix()) : *this->matrix(); devicePixelRatio = effectiveDevicePixelRatio(); - const auto oldManager = manager; - manager = RhiTextureManager::resolveByOwner(manager, window); - - if (oldManager != manager) { + if (!isVulkan) { + const auto oldManager = manager; + manager = RhiTextureManager::resolveByOwner(manager, window); + if (oldManager != manager) { + sgTexture()->setTexture(nullptr); + if (oldManager) + oldManager->release(texture); + texture.reset(); + } + } else if (Q_UNLIKELY(!manager || manager->owner() != window)) { + const auto oldManager = manager; + manager = RhiTextureManager::resolveByOwner(manager, window); sgTexture()->setTexture(nullptr); if (oldManager) oldManager->release(texture); @@ -745,10 +1107,14 @@ class Q_DECL_HIDDEN RhiNode : public WRenderBufferNode { } Q_ASSERT(ct->rhi() == window->rhi()); - rhi = rhi->resolveByOwner(rhi, window); + if (!isVulkan) + rhi = rhi->resolveByOwner(rhi, window); + else if (Q_UNLIKELY(!rhi || rhi->owner() != window)) + rhi = RhiManager::resolveByOwner(rhi, window); Q_ASSERT(rhi); - const bool hasRotation = renderMatrix.flags().testAnyFlags(QMatrix4x4::Rotation2D | QMatrix4x4::Rotation); + hasRotation = renderMatrix.flags().testAnyFlags( + QMatrix4x4::Rotation2D | QMatrix4x4::Rotation); QSize pixelSize; if (hasRotation) { @@ -771,6 +1137,7 @@ class Q_DECL_HIDDEN RhiNode : public WRenderBufferNode { renderData->rootNode.appendChildNode(renderData->imageNode); } } else { + vulkanRotationRenderer.reset(); renderData.reset(); QSizeF size = renderMatrix.mapRect(m_rect).size() * devicePixelRatio; @@ -782,10 +1149,43 @@ class Q_DECL_HIDDEN RhiNode : public WRenderBufferNode { pixelSize = size.toSize(); } + if (isVulkan && pixelSize.isEmpty()) { + reset(); + return; + } + + if (isVulkan) { + const QRect renderTargetRect(QPoint(0, 0), ct->pixelSize()); + if (hasRotation) { + // Keep the same source coordinate domain as the GLES2 path. + // A projective rotation and a fractional DPR do not commute, + // so a DPR-scaled mapped bounding box can omit pixels that the + // inverse transform still samples. + vulkanCopySourceRect = renderTargetRect; + vulkanCopyDestination = QPoint(0, 0); + } else { + const QPoint sourcePos = + (renderMatrix.map(m_rect.topLeft()) * devicePixelRatio).toPoint(); + vulkanCopySourceRect = QRect(sourcePos, pixelSize) + .intersected(renderTargetRect); + vulkanCopyDestination = vulkanCopySourceRect.topLeft() - sourcePos; + } + + if (vulkanCopySourceRect.isEmpty()) { + reset(); + return; + } + } + { auto format = attribs.format; auto modifier = attribs.modifier; - texture = manager->resolve(texture, ct->format(), std::move(format), std::move(modifier), pixelSize); + QRhiTexture::Flags textureFlags = isVulkan + ? ct->flags() & QRhiTexture::sRGB + : QRhiTexture::Flags {}; + texture = manager->resolve(texture, bool(isVulkan), ct->format(), std::move(format), + std::move(modifier), pixelSize, + std::move(textureFlags)); } if (Q_UNLIKELY(texture.expired())) { reset(); @@ -797,17 +1197,40 @@ class Q_DECL_HIDDEN RhiNode : public WRenderBufferNode { if (renderData) { if (!renderData->rt || sgTexture()->rhiTexture() != texture->data->rhiTexture) { QRhiTextureRenderTargetDescription rtDesc(texture->data->rhiTexture); - const auto flags = QRhiTextureRenderTarget::PreserveColorContents - | QRhiTextureRenderTarget::PreserveDepthStencilContents; + const auto flags = isVulkan + ? QRhiTextureRenderTarget::Flags {} + : QRhiTextureRenderTarget::PreserveColorContents + | QRhiTextureRenderTarget::PreserveDepthStencilContents; auto newRT = rhi->rhi()->newTextureRenderTarget(rtDesc, flags); newRT->setRenderPassDescriptor(newRT->newCompatibleRenderPassDescriptor()); if (!newRT->create()) { delete newRT; + if (isVulkan) + setVulkanUnavailable("failed to create rotated backdrop render target", ct); return; } renderData->rt.reset(newRT); } + + if (isVulkan + && (!renderData->scratchTexture + || renderData->scratchTexture->format() != ct->format() + || renderData->scratchTexture->pixelSize() != vulkanCopySourceRect.size() + || (renderData->scratchTexture->flags() & QRhiTexture::sRGB) + != (ct->flags() & QRhiTexture::sRGB))) { + const QRhiTexture::Flags scratchFlags = ct->flags() & QRhiTexture::sRGB; + std::unique_ptr scratchTexture( + rhi->rhi()->newTexture(ct->format(), vulkanCopySourceRect.size(), + 1, scratchFlags)); + scratchTexture->setName( + QByteArrayLiteral("WaylibRenderBufferNodeScratchTexture")); + if (!scratchTexture->create()) { + setVulkanUnavailable("failed to create rotated backdrop scratch texture", ct); + return; + } + renderData->scratchTexture = std::move(scratchTexture); + } } if (m_content) { @@ -816,6 +1239,79 @@ class Q_DECL_HIDDEN RhiNode : public WRenderBufferNode { contentNode = rootNode; } } + + if (isVulkan) { + const auto ensureInlineRenderer = [&] ( + std::unique_ptr &inlineRenderer, + const char *role) { + if (inlineRenderer && inlineRenderer->owner() != window) + inlineRenderer.reset(); + if (!inlineRenderer) { + inlineRenderer = std::make_unique( + window, this, role); + } + return inlineRenderer->isValid(); + }; + + if (hasRotation + && !ensureInlineRenderer(vulkanRotationRenderer, "rotation")) { + setVulkanUnavailable( + "failed to create the Vulkan rotation inline renderer", ct); + return; + } + + if (contentNode) { + if (!ensureInlineRenderer(vulkanContentRenderer, "content")) { + setVulkanUnavailable( + "failed to create the Vulkan content inline renderer", ct); + return; + } + } else { + vulkanContentRenderer.reset(); + } + } + + vulkanReady = isVulkan; + if (isVulkan && WVulkanTrace::enabled()) { + const bool outerDepthEnabled = currentRenderer + && currentRenderer->currentBatchRenderer() + && rendererUseDepthBuffer( + currentRenderer->currentBatchRenderer()); + qCDebug(lcWlRenderBuffer).noquote() + << QStringLiteral("VKTRACE event=blitter-prepare node=%1 item=%2 current=%3 resume=%4 color=%5 depth=%6 copyX=%7 copyY=%8 copyW=%9 copyH=%10 dstX=%11 dstY=%12 rotated=%13 content=%14 ordering=%15 depthAware=%16 outerDepth=%17") + .arg(quintptr(this), 0, 16) + .arg(quintptr(m_item.data()), 0, 16) + .arg(quintptr(renderTarget()), 0, 16) + .arg(quintptr(vulkanResumeTarget), 0, 16) + .arg(quintptr(ct), 0, 16) + .arg(quintptr(static_cast(renderTarget()) + ->description().depthTexture()), 0, 16) + .arg(vulkanCopySourceRect.x()) + .arg(vulkanCopySourceRect.y()) + .arg(vulkanCopySourceRect.width()) + .arg(vulkanCopySourceRect.height()) + .arg(vulkanCopyDestination.x()) + .arg(vulkanCopyDestination.y()) + .arg(hasRotation) + .arg(bool(contentNode)) + .arg(QStringLiteral("painter")) + .arg(false) + .arg(outerDepthEnabled); + if (hasRotation) { + qCDebug(lcWlRenderBuffer).noquote() + << QStringLiteral("VKTRACE event=blitter-rotation-geometry node=%1 copyX=%2 copyY=%3 copyW=%4 copyH=%5 targetW=%6 targetH=%7 backdropW=%8 backdropH=%9 dpr=%10") + .arg(quintptr(this), 0, 16) + .arg(vulkanCopySourceRect.x()) + .arg(vulkanCopySourceRect.y()) + .arg(vulkanCopySourceRect.width()) + .arg(vulkanCopySourceRect.height()) + .arg(ct->pixelSize().width()) + .arg(ct->pixelSize().height()) + .arg(pixelSize.width()) + .arg(pixelSize.height()) + .arg(devicePixelRatio, 0, 'f', 3); + } + } } void render([[maybe_unused]] const RenderState *state) override { @@ -827,6 +1323,12 @@ class Q_DECL_HIDDEN RhiNode : public WRenderBufferNode { auto ct = currentRenderTexture(); Q_ASSERT(ct); + if (renderWindow()->rhi() + && renderWindow()->rhi()->backend() == QRhi::Vulkan) { + renderVulkan(ct, rhiTexture); + return; + } + if (renderData) { renderData->texture.setTexture(ct); renderData->texture.setTextureSize(ct->pixelSize()); @@ -941,6 +1443,286 @@ class Q_DECL_HIDDEN RhiNode : public WRenderBufferNode { } private: + void setVulkanUnavailable(const char *reason, QRhiTexture *renderTexture) { + vulkanReady = false; + vulkanResumeTarget = nullptr; + const bool hadTexture = sgTexture()->rhiTexture(); + reset(false); + if (hadTexture) + doNotifyTextureChanged(); + if (vulkanUnavailableLogged) + return; + + qCWarning(lcWlRenderBuffer) << "Disabled RenderBufferBlitter for Vulkan render target" + << "reason" << reason + << "item" << m_item + << "itemSize" << m_size + << "renderTargetSize" + << (renderTexture ? renderTexture->pixelSize() : QSize()); + vulkanUnavailableLogged = true; + } + + void renderVulkan(QRhiTexture *currentTexture, QRhiTexture *backdropTexture) { + if (!vulkanReady || !vulkanResumeTarget || !rhi) + return; + + // Nothing consumes the backdrop snapshot when there is no content to + // draw, so the canvas copy and the pass split can be skipped entirely. + if (!contentNode) + return; + + const quint64 passSequence = ++vulkanPassSequence; + + auto cb = commandBuffer(); + if (!cb) { + setVulkanUnavailable("missing current QRhi command buffer", currentTexture); + return; + } + if (hasRotation + && (!renderData || !renderData->rt || !renderData->scratchTexture)) { + setVulkanUnavailable("rotated backdrop resources are incomplete", currentTexture); + return; + } + + auto rub = rhi->rhi()->nextResourceUpdateBatch(); + QRhiTextureCopyDescription copyDescription; + copyDescription.setPixelSize(vulkanCopySourceRect.size()); + copyDescription.setSourceTopLeft(vulkanCopySourceRect.topLeft()); + copyDescription.setDestinationTopLeft(vulkanCopyDestination); + rub->copyTexture(hasRotation ? renderData->scratchTexture.get() : backdropTexture, + currentTexture, + copyDescription); + + if (WVulkanTrace::enabled()) { + qCDebug(lcWlRenderBuffer).noquote() + << QStringLiteral("VKTRACE event=blitter-pass node=%1 sequence=%2 phase=end-main-and-copy commandBuffer=%3 current=%4 backdrop=%5 resume=%6") + .arg(quintptr(this), 0, 16) + .arg(passSequence) + .arg(quintptr(cb), 0, 16) + .arg(quintptr(currentTexture), 0, 16) + .arg(quintptr(backdropTexture), 0, 16) + .arg(quintptr(vulkanResumeTarget), 0, 16); + } + + // Vulkan cannot copy an image while it is an active color attachment. + // Split the outer pass, keep every operation on Qt's command buffer, + // then resume on a render-pass-compatible target that loads the shared + // color attachment (the backdrop set has no depth attachment). + cb->endPass(rub); + bool outerPassActive = false; + const auto passFlags = QRhiCommandBuffer::ExternalContent + | QRhiCommandBuffer::DoNotTrackResourcesForCompute; + const auto resumeOuterPass = [&] { + if (outerPassActive) + return; + cb->beginPass(vulkanResumeTarget, + Qt::transparent, + QRhiDepthStencilClearValue(1.0f, 0), + nullptr, + passFlags); + outerPassActive = true; + if (WVulkanTrace::enabled()) { + qCDebug(lcWlRenderBuffer).noquote() + << QStringLiteral("VKTRACE event=blitter-pass node=%1 sequence=%2 phase=resume-main commandBuffer=%3 resume=%4") + .arg(quintptr(this), 0, 16) + .arg(passSequence) + .arg(quintptr(cb), 0, 16) + .arg(quintptr(vulkanResumeTarget), 0, 16); + } + }; + const auto passGuard = qScopeGuard(resumeOuterPass); + + if (hasRotation) { + if (!vulkanRotationRenderer) { + setVulkanUnavailable( + "missing Vulkan rotation inline renderer", currentTexture); + return; + } + + auto scratchTexture = renderData->scratchTexture.get(); + renderData->texture.setTexture(scratchTexture); + renderData->texture.setTextureSize(scratchTexture->pixelSize()); + + const QPointF sourcePos = renderMatrix.map(m_rect.topLeft()); + const QRectF imageRect( + -(devicePixelRatio - 1) * sourcePos, + scratchTexture->pixelSize()); + const QRectF imageSourceRect( + QPointF(0, 0), scratchTexture->pixelSize()); + + // The full-size scratch texture deliberately reproduces the GLES2 + // image geometry. This keeps every texel reachable by the inverse + // projective transform, including at fractional DPR. + renderData->imageNode->setSourceRect(imageSourceRect); + renderData->imageNode->setRect(imageRect); + + if (WVulkanTrace::enabled()) { + qCDebug(lcWlRenderBuffer).noquote() + << QStringLiteral("VKTRACE event=blitter-rotation-draw node=%1 sequence=%2 rectX=%3 rectY=%4 rectW=%5 rectH=%6 sourceX=%7 sourceY=%8 sourceW=%9 sourceH=%10 scratchW=%11 scratchH=%12") + .arg(quintptr(this), 0, 16) + .arg(passSequence) + .arg(imageRect.x(), 0, 'f', 3) + .arg(imageRect.y(), 0, 'f', 3) + .arg(imageRect.width(), 0, 'f', 3) + .arg(imageRect.height(), 0, 'f', 3) + .arg(imageSourceRect.x(), 0, 'f', 3) + .arg(imageSourceRect.y(), 0, 'f', 3) + .arg(imageSourceRect.width(), 0, 'f', 3) + .arg(imageSourceRect.height(), 0, 'f', 3) + .arg(scratchTexture->pixelSize().width()) + .arg(scratchTexture->pixelSize().height()); + } + + if (!vulkanRotationRenderer->sync( + backdropTexture->pixelSize(), + &renderData->rootNode, + renderMatrix.inverted(), + {}, + nullptr, + {backdropTexture->pixelSize().width() + / float(m_rect.width() * devicePixelRatio), + backdropTexture->pixelSize().height() + / float(m_rect.height() * devicePixelRatio)})) { + setVulkanUnavailable( + "failed to synchronize the Vulkan rotation inline renderer", + currentTexture); + return; + } + if (!vulkanRotationRenderer->prepareInline(renderData->rt.get(), cb)) { + setVulkanUnavailable( + "failed to prepare the Vulkan rotation inline renderer", + currentTexture); + return; + } + cb->beginPass(renderData->rt.get(), + Qt::transparent, + QRhiDepthStencilClearValue(1.0f, 0), + nullptr, + passFlags); + vulkanRotationRenderer->renderInline(); + cb->endPass(); + if (WVulkanTrace::enabled()) { + qCDebug(lcWlRenderBuffer).noquote() + << QStringLiteral("VKTRACE event=blitter-pass node=%1 sequence=%2 phase=rotation-complete renderer=%3 root=%4") + .arg(quintptr(this), 0, 16) + .arg(passSequence) + .arg(quintptr(vulkanRotationRenderer->renderer()), 0, 16) + .arg(quintptr(vulkanRotationRenderer->rootNode()), 0, 16); + } + } + + if (sgTexture()->rhiTexture() != backdropTexture) + sgTexture()->setTexture(backdropTexture); + doNotifyTextureChanged(); + + if (!contentNode) + return; + + if (!vulkanContentRenderer) { + setVulkanUnavailable( + "missing Vulkan content inline renderer", currentTexture); + return; + } + + auto currentRenderer = maybeBufferRenderer(); + if (!currentRenderer) { + reset(); + return; + } + + QSGNode *childContainer = nullptr; + if (clipList() || inheritedOpacity() < 1.0) { + if (!node) + node.reset(new Node); + + node->opacityNode.setOpacity(inheritedOpacity()); + node->transformNode.setMatrix(*this->matrix()); + childContainer = &node->opacityNode; + if (clipList()) { + if (!node->clipNode) { + node->clipNode = new QQuickDefaultClipNode( + QRectF(0, 0, 65535, 65535)); + node->clipNode->setFlag(QSGNode::OwnedByParent, false); + node->clipNode->setClipRect(node->clipNode->rect()); + node->clipNode->update(); + + node->rootNode.reparentChildNodesTo(node->clipNode); + node->rootNode.appendChildNode(node->clipNode); + } + } else { + if (node->clipNode) + node->clipNode->reparentChildNodesTo(&node->rootNode); + + delete node->clipNode; + node->clipNode = nullptr; + } + + overrideChildNodesTo(contentNode, childContainer); + } + + const auto childGuard = qScopeGuard([&] { + if (childContainer) { + // The temporary wrapper borrows the content subtree. Detach + // the dedicated renderer while the borrowed nodes are still + // below its root, then restore their real parent. + if (vulkanContentRenderer) + vulkanContentRenderer->detachRoot(); + restoreChildNodesTo(childContainer, contentNode); + } + }); + + QSGRootNode *contentRenderRoot = nullptr; + QMatrix4x4 contentMatrix; + if (childContainer) { + contentRenderRoot = &node->rootNode; + } else { + node.reset(); + contentRenderRoot = contentNode; + contentMatrix = *this->matrix(); + } + + if (node && node->clipNode) { + if (!node->clipNode->clipList()) { + node->clipNode->setRendererClipList(clipList()); + } else { + auto lastClipNode = node->clipNode->clipList(); + while (auto cliplist = lastClipNode->clipList()) + lastClipNode = cliplist; + Q_ASSERT(!lastClipNode->clipList()); + const_cast(lastClipNode)->setRendererClipList(clipList()); + } + } + + if (!vulkanContentRenderer->sync( + currentTexture->pixelSize(), + contentRenderRoot, + contentMatrix, + *projectionMatrix(), + currentRenderer->currentRenderer())) { + setVulkanUnavailable( + "failed to synchronize the Vulkan content inline renderer", + currentTexture); + return; + } + if (!vulkanContentRenderer->prepareInline(vulkanResumeTarget, cb)) { + setVulkanUnavailable( + "failed to prepare the Vulkan content inline renderer", + currentTexture); + return; + } + + resumeOuterPass(); + vulkanContentRenderer->renderInline(); + if (WVulkanTrace::enabled()) { + qCDebug(lcWlRenderBuffer).noquote() + << QStringLiteral("VKTRACE event=blitter-pass node=%1 sequence=%2 phase=content-complete renderer=%3 root=%4") + .arg(quintptr(this), 0, 16) + .arg(passSequence) + .arg(quintptr(vulkanContentRenderer->renderer()), 0, 16) + .arg(quintptr(vulkanContentRenderer->rootNode()), 0, 16); + } + } + void reset(bool notifyTexture = true) { if (renderData) renderData->rt.reset(); @@ -955,6 +1737,10 @@ class Q_DECL_HIDDEN RhiNode : public WRenderBufferNode { void destroy() { reset(false); + // Inline renderers must detach from their roots before the temporary + // root containers below are destroyed. + vulkanContentRenderer.reset(); + vulkanRotationRenderer.reset(); renderData.reset(); node.reset(); manager = nullptr; @@ -966,6 +1752,15 @@ class Q_DECL_HIDDEN RhiNode : public WRenderBufferNode { DataManagerPointer rhi; QMatrix4x4 renderMatrix; qreal devicePixelRatio; + bool hasRotation = false; + bool vulkanReady = false; + bool vulkanUnavailableLogged = false; + quint64 vulkanPassSequence = 0; + QRhiTextureRenderTarget *vulkanResumeTarget = nullptr; + QRect vulkanCopySourceRect; + QPoint vulkanCopyDestination; + std::unique_ptr vulkanContentRenderer; + std::unique_ptr vulkanRotationRenderer; struct Node { Node() { @@ -976,6 +1771,19 @@ class Q_DECL_HIDDEN RhiNode : public WRenderBufferNode { rootNode.appendChildNode(&transformNode); } + ~Node() { + if (clipNode) { + // The clip node borrows the content subtree, so hand it back + // before deleting it (exactly like the no-clip transition + // below); QSGNode::destroy() then detaches it from rootNode. + // Without this the clip node is leaked whenever the node is + // destroyed while clipping was active. + clipNode->reparentChildNodesTo(&rootNode); + delete clipNode; + clipNode = nullptr; + } + } + QSGRootNode rootNode; QSGTransformNode transformNode; QSGOpacityNode opacityNode; @@ -997,6 +1805,7 @@ class Q_DECL_HIDDEN RhiNode : public WRenderBufferNode { }; std::unique_ptr rt; + std::unique_ptr scratchTexture; QSGRootNode rootNode; QSGImageNode *imageNode; QSGPlainTexture texture; diff --git a/waylib/src/server/qtquick/woutputhelper.cpp b/waylib/src/server/qtquick/woutputhelper.cpp index 2ad7ea2584..2abb351f38 100644 --- a/waylib/src/server/qtquick/woutputhelper.cpp +++ b/waylib/src/server/qtquick/woutputhelper.cpp @@ -102,6 +102,19 @@ void WOutputHelperPrivate::setContentIsDirty(bool newValue) wlr_buffer *WOutputHelperPrivate::acquireBuffer(wlr_swapchain **sc) { + // This legacy path has no callers on the Vulkan renderer. Its swapchain + // replacement destroys the old swapchain immediately, which can race with + // GPU work (this frame's command buffer) still referencing its buffers. + // The primary rendering path uses WOutput::configurePrimarySwapchain with + // deferred replacement instead, so fail safely here rather than + // introducing an untested retire mechanism. + if (output->renderer() && wlr_renderer_is_vk(output->renderer())) { + qCWarning(lcWlOutputHelper) + << "acquireBuffer is not supported with the Vulkan renderer" + << "output" << output; + return nullptr; + } + bool ok = wlr_output_configure_primary_swapchain(qwoutput(), &state, sc); if (!ok) return nullptr; diff --git a/waylib/src/server/qtquick/woutputitem.cpp b/waylib/src/server/qtquick/woutputitem.cpp index 2166a7c7d8..5361b00a4f 100644 --- a/waylib/src/server/qtquick/woutputitem.cpp +++ b/waylib/src/server/qtquick/woutputitem.cpp @@ -4,6 +4,7 @@ #include "woutputitem.h" #include "woutputitem_p.h" #include "woutput.h" +#include "woutputrenderwindow.h" #include "wquickoutputlayout.h" #include "wquickcursor.h" #include "private/wglobal_p.h" @@ -12,6 +13,8 @@ #include +#include + #include WAYLIB_SERVER_BEGIN_NAMESPACE @@ -157,6 +160,28 @@ void WOutputItemPrivate::updateCursors() } updateCursorVisible(safeOc.data()); + // On the Vulkan path the cursor item geometry is detached from the + // live pointer position (see SourceOutput.qml), so moving the cursor + // no longer dirties the scene. If the DRM hardware cursor plane is + // already set up, move it directly and skip the whole compositor + // frame; otherwise (software cursor fallback, plane not set up yet, + // or the cursor is becoming (in)visible) schedule a frame as before. + // GLES2 keeps relying on scene dirtying. + auto *outputItem = safeOc->output(); + if (outputItem && outputItem->output() && outputItem->window() + && outputItem->window()->graphicsApi() == QSGRendererInterface::Vulkan) { + auto *cursorItem = safeOc->cursorItem(); + if (safeOc->visible() && cursorItem) { + if (auto *renderWindow = + qobject_cast(cursorItem->window())) { + if (renderWindow->tryMoveHardwareCursor(outputItem->output(), + cursorItem)) { + return; + } + } + } + outputItem->output()->scheduleFrame(); + } }); } diff --git a/waylib/src/server/qtquick/woutputrenderwindow.cpp b/waylib/src/server/qtquick/woutputrenderwindow.cpp index da5eac2af7..a98780e81b 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"); @@ -242,6 +254,10 @@ class Q_DECL_HIDDEN OutputHelper : public WOutputHelper WBufferRenderer *compositeLayers(const QList layers, bool forceShadowRenderer); bool commit(WBufferRenderer *buffer); bool tryToHardwareCursor(const LayerData *layer); + bool moveHardwareCursorForPosition(WQuickCursor *cursorItem); + void deferHardwareCursorCommit(wlr_buffer *buffer, const QPoint &hotSpot); + void flushPendingHardwareCursor(); + void cancelPendingHardwareCursor(); private: WOutputViewport *m_output = nullptr; @@ -250,8 +266,22 @@ class Q_DECL_HIDDEN OutputHelper : public WOutputHelper // only for render cursor QPointer m_cursorRenderer; BufferRendererProxy *m_cursorLayerProxy = nullptr; + // Vulkan only: the cursor QML item rendered directly into the scanout + // buffer (instead of sampling the layer buffer released to FOREIGN). + QPointer m_cursorSourceItem; bool m_cursorDirty = false; bool m_hardwareCursorRenderComplete = false; + // Vulkan only: the cursor buffer rendered into the current frame's command + // buffer. set_cursor() must wait until that command buffer has been + // submitted and completed, or the cursor plane can scan out content that + // the GPU has not rendered yet. + wlr_buffer *m_pendingCursorBuffer = nullptr; + QPoint m_pendingCursorHotSpot; + // Set when a deferred set_cursor() failed, so later frames keep the cursor + // on the software canvas path until the cursor content is re-rendered. + // Retrying the same buffer every frame would otherwise busy-loop the + // compositor's render loop. + bool m_hardwareCursorCommitBroken = false; // for compositeLayers QPointer m_output2; @@ -359,6 +389,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 +503,35 @@ 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, + wlr_buffer *sampledBuffer, + 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 +556,21 @@ class Q_DECL_HIDDEN WOutputRenderWindowPrivate : public QQuickWindowPrivate bool componentCompleted = true; bool inRendering = false; bool renderEnabled = true; + bool frameFailed = false; + QString fatalRenderError; + // Consecutive QRhi beginOffscreenFrame failures without a completed frame. + // A single Error (not device-lost) failure only skips the frame; repeated + // failures freeze resource retirement forever, so escalate to fatal. + int consecutiveBeginFrameFailures = 0; + static constexpr int FatalBeginFrameFailureThreshold = 3; + 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 +582,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 +639,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 +703,7 @@ void OutputHelper::cleanCursorRender() m_cursorRenderer->deleteLater(); m_cursorRenderer = nullptr; m_cursorLayerProxy = nullptr; + m_cursorSourceItem = nullptr; } } @@ -593,11 +715,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 +798,17 @@ wlr_buffer *OutputHelper::renderLayer(LayerData *layer, bool *dontEndRenderAndRe const auto layerFlags = layer->layer->layer->flags(); const bool sizeSensitive = layerFlags & WOutputLayer::SizeSensitive; + const bool isVulkanLayer = WRenderHelper::getGraphicsApi(renderWindowD()->rc()) + == QSGRendererInterface::Vulkan; + // On Vulkan the cursor item is detached from the live pointer position + // (fixed at (0,0) in PrimaryOutput.qml), so its output placement must be + // derived from the wlroots cursor/output coordinates rather than the item + // geometry. GLES2 keeps the geometry-driven path unchanged. + const bool cursorLayer = (layerFlags & WOutputLayer::Cursor) && isVulkanLayer; const bool isRef = layer->mapFrom && layer->mapTo; - if (isRef) { + if (cursorLayer) { + viewportMatrix = outputViewport()->mapToViewport(source->parentItem()); + } else if (isRef) { viewportMatrix = outputViewport()->mapToViewport(layer->mapTo); const auto xScale = layer->mapTo->width() / layer->mapFrom->outputViewport()->width(); const auto yScale = layer->mapTo->height() / layer->mapFrom->outputViewport()->height(); @@ -677,8 +830,26 @@ wlr_buffer *OutputHelper::renderLayer(LayerData *layer, bool *dontEndRenderAndRe // matrix function: map source to output buffer const auto outputMatrix = viewportMatrix * outputViewport()->sourceRectToTargetRectTransfrom(); - noClipMapRect = outputMatrix.mapRect(noClipMapRect); - mapRect = outputMatrix.mapRect(mapRect); + if (cursorLayer) { + auto cursorItem = qobject_cast(source); + if (!cursorItem) { + qCWarning(lcWlCursor) + << "Vulkan cursor layer source is not a WQuickCursor; skipping cursor layer" + << "output" << outputViewport() + << "source" << source; + return nullptr; + } + if (!cursorItem->cursor()) + return nullptr; + + const QPointF outputLocalPosition = cursorItem->cursor()->position() + - QPointF(outputViewport()->output()->position()); + noClipMapRect = QRectF(outputLocalPosition - cursorItem->hotSpot(), source->size()); + mapRect = noClipMapRect; + } else { + noClipMapRect = outputMatrix.mapRect(noClipMapRect); + mapRect = outputMatrix.mapRect(mapRect); + } QTransform revertScaleTransform; if (!sizeSensitive) { @@ -718,7 +889,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 +939,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 +1021,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 +1175,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 +1262,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,12 +1345,15 @@ bool OutputHelper::commit(WBufferRenderer *buffer) bool OutputHelper::tryToHardwareCursor(const LayerData *layer) { + const bool isVulkan = WRenderHelper::getGraphicsApi(renderWindowD()->rc()) + == QSGRendererInterface::Vulkan; do { auto set_cursor = output()->impl->set_cursor; auto buffer = layer && layer->renderer->lastBuffer() ? layer->renderer->lastBuffer() : nullptr; if (!buffer) { + cancelPendingHardwareCursor(); if (!m_hardwareCursorRenderComplete) return true; @@ -1173,14 +1426,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 +1472,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 +1500,104 @@ bool OutputHelper::tryToHardwareCursor(const LayerData *layer) newBuffer = m_cursorRenderer->beginRender(pixelSize, 1.0, DRM_FORMAT_ARGB8888, WBufferRenderer::UseCursorFormats); if (newBuffer) { - m_cursorRenderer->render(0, {}); + if (!isVulkan) { + m_cursorRenderer->render(0, {}); + } else { + // The cursor QML item is laid out in logical (item) + // coordinates, but the hardware cursor plane scans the buffer + // out 1:1 in device pixels. Scale the item by the output device + // pixel ratio so the cursor is rasterized at its device pixel + // size and therefore follows fractional scaling. This matches + // the GLES2 proxy path, which renders the already device-sized + // layer buffer 1:1. The hotspot (cursorHotSpot * dpr) uses the + // same scale, so it stays consistent with the rendered image. + QMatrix4x4 cursorScale; + cursorScale.scale(devicePixelRatio(), devicePixelRatio()); + if (!m_cursorRenderer->render(0, cursorScale)) { + qCWarning(lcWlBufferRenderer) + << "Skipping cursor buffer because render pass failed" + << "output" << output() + << "renderer" << m_cursorRenderer + << "cursorSourceItem" << m_cursorSourceItem.data() + << "targetSize" << pixelSize + << "currentBuffer" << m_cursorRenderer->currentBuffer(); + (void)renderWindowD()->releaseRenderBuffer( + m_cursorRenderer, "cursor-render-buffer-abort"); + newBuffer = nullptr; + } else if (!renderWindowD()->releaseRenderBuffer( + m_cursorRenderer, "cursor-render-buffer")) { + qCWarning(lcWlBufferRenderer) + << "Skipping cursor buffer because Vulkan release failed" + << "output" << output() + << "renderer" << m_cursorRenderer + << "targetSize" << pixelSize + << "currentBuffer" << m_cursorRenderer->currentBuffer(); + newBuffer = nullptr; + } + } m_cursorRenderer->endRender(); + } else { + qCWarning(lcWlBufferRenderer) + << "Failed to begin cursor buffer render" + << "output" << output() + << "targetSize" << pixelSize + << "directQmlRender" << isVulkan; } - m_cursorDirty = false; + m_cursorDirty = isVulkan ? !newBuffer : false; + // The cursor content was re-rendered into a fresh buffer, so + // a previously failed deferred set_cursor() is worth one + // retry with the new buffer. + if (newBuffer && isVulkan) + m_hardwareCursorCommitBroken = false; } if (newBuffer) { Q_ASSERT(pixelSize.width() == newBuffer->width); Q_ASSERT(pixelSize.height() == newBuffer->height); buffer = newBuffer; + } else if (isVulkan) { + // Never hand a null buffer to set_cursor(): that disables the + // hardware cursor while the caller simultaneously removes the + // cursor layer from software compositing, which makes the cursor + // vanish entirely. Fall back to the software cursor instead. + qCWarning(lcWlCursor) + << "Vulkan hardware cursor buffer unavailable; falling back to software cursor" + << "output" << output() + << "requestedSize" << pixelSize; + break; } else { buffer = nullptr; } } - const auto hotSpot = layer->renderMatrix.map(layer->layer->layer->cursorHotSpot() - * devicePixelRatio()).toPoint(); - if (!set_cursor(output(), buffer, hotSpot.x(), hotSpot.y())) { + // On Vulkan the cursor item is rendered directly at the top-left of the + // scanout buffer with an identity transform, so the hotspot is simply the + // cursor hotspot scaled to device pixels. The GLES2 proxy path keeps the + // historical renderMatrix-based mapping. + const QPoint hotSpot = isVulkan + ? (layer->layer->layer->cursorHotSpot() * devicePixelRatio()).toPoint() + : layer->renderMatrix.map(layer->layer->layer->cursorHotSpot() + * devicePixelRatio()).toPoint(); + if (isVulkan) { + if (m_hardwareCursorCommitBroken) { + // A previous deferred set_cursor() failed for this output; + // keep the cursor on the software canvas path until the cursor + // content is re-rendered. + break; + } + // The buffer content is produced by the current frame's command + // buffer, which is only submitted and awaited at endFrame(). + // Defer set_cursor() until after that point so the cursor plane + // can never scan out content the GPU has not rendered yet. + deferHardwareCursorCommit(buffer, hotSpot); + m_hardwareCursorRenderComplete = true; + } else if (!set_cursor(output(), buffer, hotSpot.x(), hotSpot.y())) { + qCWarning(lcWlCursor) + << "wlr output set_cursor failed; falling back to software cursor" + << "output" << output() + << "bufferSize" << QSize(buffer->width, buffer->height) + << "hotSpot" << hotSpot; break; } else { m_hardwareCursorRenderComplete = true; @@ -1241,9 +1614,25 @@ bool OutputHelper::tryToHardwareCursor(const LayerData *layer) output()->transform, outputSize.width(), outputSize.height()); if (!move_cursor(output(), cleanTransform.x, cleanTransform.y)) { + qCWarning(lcWlCursor) + << "wlr output move_cursor failed; falling back to software cursor" + << "output" << output() + << "position" << QPoint(cleanTransform.x, cleanTransform.y); + // The caller will composite the cursor into the canvas instead, so + // a deferred set_cursor() for this frame must not run afterwards. + cancelPendingHardwareCursor(); break; } + if (Q_UNLIKELY(lcWlCursor().isDebugEnabled())) + qCDebug(lcWlCursor) + << "Hardware cursor committed" + << "output" << output() + << "bufferSize" << QSize(buffer->width, buffer->height) + << "hotSpot" << hotSpot + << "position" << QPoint(cleanTransform.x, cleanTransform.y) + << "directQmlRender" << isVulkan; + return true; } while (false); @@ -1252,6 +1641,117 @@ bool OutputHelper::tryToHardwareCursor(const LayerData *layer) return false; } +bool OutputHelper::moveHardwareCursorForPosition(WQuickCursor *cursorItem) +{ + WOutputViewport *viewport = outputViewport(); + if (!cursorItem || !viewport) + return false; + + WOutput *woutput = viewport->output(); + wlr_output *wlrout = output(); + if (!woutput || !wlrout) + return false; + + const bool isVulkan = WRenderHelper::getGraphicsApi(renderWindowD()->rc()) + == QSGRendererInterface::Vulkan; + if (!isVulkan) + return false; + + auto move_cursor = wlrout->impl->move_cursor; + if (!move_cursor) + return false; + + // A full frame is still required until the hardware cursor plane has been + // set up at least once (and is not in the broken/software fallback state). + if (!m_hardwareCursorRenderComplete || m_hardwareCursorCommitBroken) + return false; + if (wlrout->software_cursor_locks > 0) + return false; + + auto cursor = cursorItem->cursor(); + if (!cursor) + return false; + + // Mirror the Vulkan cursor-layer placement used by renderLayer() + + // tryToHardwareCursor(): on Vulkan the cursor item is detached from the + // live pointer position, so the plane position is derived from the wlroots + // cursor/output coordinates. move_cursor() itself inverts the output + // transform and subtracts the hotspot, so apply the same forward transform + // tryToHardwareCursor() does to compensate. + const qreal dpr = devicePixelRatio(); + const QPointF outputLocalPosition = + cursor->position() - QPointF(woutput->position()); + const QPointF hotSpot = cursorItem->hotSpot(); + QRectF mapRect(QPointF(outputLocalPosition - hotSpot), cursorItem->size()); + mapRect &= QRectF(QPointF(0, 0), viewport->size()); + // Bit-identical to tryToHardwareCursor()'s + // layer->mapToOutput.topLeft() + cursorHotSpot() * dpr + // so a pointer move and a frame-driven move never differ by a rounding pixel. + const QPoint pos = (mapRect.topLeft() * dpr).toPoint() + + (hotSpot * dpr).toPoint(); + + wlr_box box { .x = pos.x(), .y = pos.y(), .width = 0, .height = 0 }; + const auto outputSize = woutput->size(); + wlr_box_transform(&box, &box, wlrout->transform, + outputSize.width(), outputSize.height()); + + if (!move_cursor(wlrout, box.x, box.y)) { + qCWarning(lcWlCursor) + << "Direct hardware cursor move failed; falling back to a full frame" + << "output" << wlrout + << "position" << QPoint(box.x, box.y); + return false; + } + + return true; +} + +void OutputHelper::deferHardwareCursorCommit(wlr_buffer *buffer, const QPoint &hotSpot) +{ + Q_ASSERT(buffer); + m_pendingCursorBuffer = buffer; + m_pendingCursorHotSpot = hotSpot; +} + +void OutputHelper::cancelPendingHardwareCursor() +{ + m_pendingCursorBuffer = nullptr; + m_pendingCursorHotSpot = QPoint(); +} + +void OutputHelper::flushPendingHardwareCursor() +{ + if (!m_pendingCursorBuffer) + return; + + auto set_cursor = output()->impl->set_cursor; + wlr_buffer *buffer = m_pendingCursorBuffer; + const QPoint hotSpot = m_pendingCursorHotSpot; + cancelPendingHardwareCursor(); + + if (!set_cursor) { + m_hardwareCursorRenderComplete = false; + return; + } + + if (!set_cursor(output(), buffer, hotSpot.x(), hotSpot.y())) { + qCWarning(lcWlCursor) + << "Deferred wlr output set_cursor failed; retrying via a new frame with the software cursor" + << "output" << output() + << "bufferSize" << QSize(buffer->width, buffer->height) + << "hotSpot" << hotSpot; + if (set_cursor) + set_cursor(output(), nullptr, 0, 0); + m_hardwareCursorRenderComplete = false; + // Arm the breaker: retrying the same buffer every frame would + // busy-loop the render loop (tryToHardwareCursor() checks this flag + // before deferring a commit and falls back to the software canvas + // instead). It is disarmed when the cursor content is re-rendered. + m_hardwareCursorCommitBroken = true; + update(); + } +} + int WOutputRenderWindowPrivate::indexOfOutputHelper(const WOutputViewport *output) const { for (int i = 0; i < outputs.size(); ++i) { @@ -1331,6 +1831,13 @@ void WOutputRenderWindowPrivate::init() void WOutputRenderWindowPrivate::init(OutputHelper *helper) { + if (graphicsApi() == QSGRendererInterface::Vulkan) { + qCInfo(lcWlCursor) << "Vulkan hardware cursor path enabled for output" + << "output" << helper->outputViewport()->output() + << "forceSoftwareCursor" + << helper->outputViewport()->output()->forceSoftwareCursor(); + } + QMetaObject::invokeMethod(helper, &WOutputHelper::scheduleFrame, Qt::QueuedConnection); helper->init(); QObject::connect(helper->outputViewport(), &WOutputViewport::dependsChanged, helper, [this] { @@ -1415,6 +1922,15 @@ bool WOutputRenderWindowPrivate::initRCWithRhi() Q_ASSERT(rcd->rhi == result.rhi); Q_ASSERT(!swapchain); + // Arm the per-QRhi leaked-resource safety sweep on every QRhi we adopt, + // including windows whose render control never produces any BufferData: + // client items holding Buffer/Texture/RenderBuffer objects that outlive + // this QRhi would otherwise trip the VMA "Some allocations were not + // freed" assertion at teardown (which was the Waydroid crash class). + WRenderHelper::addVulkanTeardownSweep(rcd->rhi, rcd->rhi); + + WVulkanTrace::activate(rhiSupport->rhiBackend() == QRhi::Vulkan); + return true; } @@ -1450,6 +1966,81 @@ QVector> WOutputRenderWindowPrivate::doRenderOutputs(wlr_output *needsFrameOutput, const QList &outputs, bool forceRender) { + if (WRenderHelper::getGraphicsApi(rc()) != QSGRendererInterface::Vulkan) { + QVector renderResults; + renderResults.reserve(outputs.size()); + for (OutputHelper *helper : std::as_const(outputs)) { + if (Q_LIKELY(needsFrameOutput)) { + if (helper->output() != needsFrameOutput) + continue; + else + Q_ASSERT(!helper->framePending()); + } + + if (Q_LIKELY(!forceRender)) { + if (helper->framePending()) + continue; + + // Render if output will be enabled OR has extraState to commit + // Note: Even when disabling, we need to render once to commit the disabled state + // (extraState will contain the ENABLED=false change) + bool shouldRender = helper->willBeEnabled() || helper->extraState(); + if (Q_UNLIKELY(!WOutputViewportPrivate::get(helper->outputViewport())->renderable()) + || !shouldRender) + continue; + + if (!(helper->needsFrame() || helper->contentIsDirty())) + continue; + + // Capture sessions (ext-image-copy-capture etc.) lock the output + // via wlr_output_lock_attach_render() and need a buffer commit to + // complete, even if the content didn't change. + bool captureLocked = helper->output()->attach_render_locks > 0; + if (!helper->contentIsDirty() && !captureLocked) { + renderResults.append(helper); + continue; + } + } + + Q_ASSERT(helper->outputViewport()->output()->scale() + <= helper->outputViewport()->devicePixelRatio()); + + const auto &format = helper->output()->render_format; + const auto renderMatrix = helper->outputViewport()->renderMatrix(); + + // maybe using the other WOutputViewport's QSGTextureProvider + if (!helper->outputViewport()->depends().isEmpty()) + updateDirtyNodes(); + + wlr_buffer *buffer = helper->beginRender( + helper->bufferRenderer(), + helper->outputViewport()->output()->size(), + format, + WBufferRenderer::RedirectOpenGLContextDefaultFrameBufferObject, + helper->outputViewport()->colorContentsMode()); + Q_ASSERT(buffer == helper->bufferRenderer()->currentBuffer()); + if (buffer) { + helper->render(helper->bufferRenderer(), + 0, + renderMatrix, + helper->outputViewport()->effectiveSourceRect(), + helper->outputViewport()->targetRect()); + } + renderResults.append(helper); + } + + QVector> needsCommit; + needsCommit.reserve(renderResults.size()); + for (auto helper : std::as_const(renderResults)) { + auto bufferRenderer = helper->afterRender(); + if (bufferRenderer) + needsCommit.append({ helper, bufferRenderer }); + } + + rendererList.clear(); + return needsCommit; + } + QVector renderResults; renderResults.reserve(outputs.size()); for (OutputHelper *helper : std::as_const(outputs)) { @@ -1499,9 +2090,22 @@ WOutputRenderWindowPrivate::doRenderOutputs(wlr_output *needsFrameOutput, const helper->outputViewport()->colorContentsMode()); Q_ASSERT(buffer == helper->bufferRenderer()->currentBuffer()); if (buffer) { - helper->render(helper->bufferRenderer(), 0, renderMatrix, - helper->outputViewport()->effectiveSourceRect(), - helper->outputViewport()->targetRect()); + if (!helper->render(helper->bufferRenderer(), 0, renderMatrix, + helper->outputViewport()->effectiveSourceRect(), + helper->outputViewport()->targetRect())) { + qCWarning(lcWlBufferRenderer) << "Skipping output buffer because render pass failed" + << "renderer" << helper->bufferRenderer() + << "currentBuffer" << helper->bufferRenderer()->currentBuffer() + << "output" << helper->outputViewport(); + if (!releaseRenderBuffer(helper->bufferRenderer(), "compositor-render-buffer-abort")) { + failCurrentFrameFatally(QStringLiteral("Failed to release an aborted Vulkan output render buffer")); + } + helper->bufferRenderer()->endRender(); + helper->resetState(); + if (frameFailed) + break; + continue; + } } renderResults.append(helper); } @@ -1522,6 +2126,377 @@ 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->wlrBuffer(); + if (currentBuffer && providerBuffer == currentBuffer) { + ++skippedCurrentRenderTarget; + WVulkanTrace::sampleDisposition(q, provider, provider->qwTexture(), + WVulkanTrace::SampleDisposition::CurrentRenderTarget); + continue; + } + + auto sampleTexture = provider->texture(); + if (sampleTexture && currentBuffer) { + if (auto rhiTexture = sampleTexture->rhiTexture()) { + auto renderTargetBuffer = WRenderHelper::lookupBuffer(rhiTexture); + if (renderTargetBuffer == currentBuffer) { + ++skippedCurrentRenderTarget; + WVulkanTrace::sampleDisposition(q, provider, provider->qwTexture(), + WVulkanTrace::SampleDisposition::CurrentRenderTarget); + continue; + } + } + } + + auto texture = provider->qwTexture(); + if (!texture || !provider->hasTexture()) { + ++invalidProviderCount; + WVulkanTrace::sampleDisposition(q, provider, texture, + WVulkanTrace::SampleDisposition::InvalidProvider); + continue; + } + + if (seenTextures.contains(texture)) { + WVulkanTrace::sampleDisposition(q, provider, texture, + WVulkanTrace::SampleDisposition::DuplicateTexture); + continue; + } + + seenTextures.insert(texture); + QElapsedTimer prepareTimer; + if (WVulkanTrace::enabled()) + prepareTimer.start(); + const bool prepareOk = WRenderHelper::prepareTextureForSampling(rc(), m_renderer, + texture, providerBuffer, + 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, + wlr_buffer *sampledBuffer, + 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, + sampledBuffer, + 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) { @@ -1549,30 +2524,250 @@ void WOutputRenderWindowPrivate::doRender(wlr_output *needsFrameOutput, if (!renderEnabled) return; - inRendering = true; + if (WRenderHelper::getGraphicsApi(rc()) != QSGRendererInterface::Vulkan) { + inRendering = true; + + W_Q(WOutputRenderWindow); + for (OutputLayer *layer : std::as_const(layers)) { + layer->beforeRender(q); + } + + rc()->polishItems(); + + if (QSGRendererInterface::isApiRhiBased(WRenderHelper::getGraphicsApi())) + rc()->beginFrame(); + rc()->sync(); + + QQuickAnimatorController_advance(animationController.get()); + Q_EMIT q->beforeRendering(); + runAndClearJobs(&beforeRenderingJobs); + + auto needsCommit = doRenderOutputs(needsFrameOutput, outputs, forceRender); + + Q_EMIT q->afterRendering(); + runAndClearJobs(&afterRenderingJobs); + + if (QSGRendererInterface::isApiRhiBased(WRenderHelper::getGraphicsApi())) + rc()->endFrame(); + + // prevent gles2-render exception in wlroots. + // wlroots may have render operations after commit, so do + // not move the location during the reset operation. + // eg: screencopy ext-image-capture + resetGlState(); + + QList> committedOutputs; + if (doCommit) { + committedOutputs.reserve(needsCommit.size()); + for (auto i : std::as_const(needsCommit)) { + if (Q_UNLIKELY(!i.first->framePending())) { + if (Q_LIKELY(i.first->commit(i.second))) { + // Make sure the output is still valid after commit + auto output = i.first->outputViewport()->output(); + if (Q_LIKELY(needsFrameOutput)) { + Q_ASSERT(output->handle() == needsFrameOutput); + if (committedOutputs.isEmpty()) + committedOutputs.append(output); + } else if (!committedOutputs.contains(output)) { + committedOutputs.append(output); + } + } + } + + if (i.second->currentBuffer()) { + i.second->endRender(); + } + + i.first->resetState(); + } + } + + resetGlState(); + // On Intel&Nvidia multi-GPU environment, wlroots using Intel card do render for all + // outputs, and blit nvidia's output buffer in drm_connector_state_update_primary_fb, + // the 'blit' behavior will make EGL context to Nvidia renderer. So must done current + // OpenGL context here in order to ensure QtQuick always make EGL context to Intel + // renderer before next frame. + if (glContext) + glContext->doneCurrent(); + + inRendering = false; + Q_EMIT q->renderEnd(committedOutputs); + return; + } + + inRendering = true; + frameFailed = false; + fatalRenderError.clear(); W_Q(WOutputRenderWindow); + WVulkanTrace::beginFrame(q); + clearPresentationFeedback(); + + const auto finishFrame = [this, q] (const QList> &committedOutputs) { + WRenderHelper::abortTextureSyncBatch(m_renderer); + clearPresentationFeedback(); + m_currentPreparedTextures = nullptr; + m_currentPreparedTextureSet.clear(); + + // Drop cursor commits deferred for a frame that did not complete, so + // they can never outlive the frame whose command buffer produced them. + for (auto helper : std::as_const(this->outputs)) { + if (helper) + helper->cancelPendingHardwareCursor(); + } + + resetGlState(); + + // On Intel&Nvidia multi-GPU environment, wlroots using Intel card do render for all + // outputs, and blit nvidia's output buffer in drm_connector_state_update_primary_fb, + // the 'blit' behavior will make EGL context to Nvidia renderer. So must done current + // OpenGL context here in order to ensure QtQuick always make EGL context to Intel + // renderer before next frame. + if (glContext) + glContext->doneCurrent(); + + WVulkanTrace::setFrameStage(q, WVulkanTrace::FrameStage::RenderEnd); + inRendering = false; + Q_EMIT q->renderEnd(committedOutputs); + WVulkanTrace::endFrame(q, committedOutputs.size()); + + if (!fatalRenderError.isEmpty()) { + qCCritical(lcWlRenderer) << "Fatal Vulkan rendering failure; requesting compositor shutdown" + << "reason" << fatalRenderError; + Q_EMIT q->sceneGraphError(QQuickWindow::ContextNotAvailable, + fatalRenderError); + } + }; + + WVulkanTrace::setFrameStage(q, WVulkanTrace::FrameStage::Polish); for (OutputLayer *layer : std::as_const(layers)) { layer->beforeRender(q); } rc()->polishItems(); - if (QSGRendererInterface::isApiRhiBased(WRenderHelper::getGraphicsApi())) - rc()->beginFrame(); + const auto graphicsApi = WRenderHelper::getGraphicsApi(); + const bool isVulkan = graphicsApi == QSGRendererInterface::Vulkan; + const bool rhiBased = QSGRendererInterface::isApiRhiBased(graphicsApi); + if (rhiBased) { + WVulkanTrace::setFrameStage(q, WVulkanTrace::FrameStage::BeginFrame); + if (isVulkan) { + const auto beginResult = rc()->beginFrameChecked(); + if (beginResult != QRhi::FrameOpSuccess) { + if (beginResult == QRhi::FrameOpDeviceLost) { + failCurrentFrameFatally(QStringLiteral("Vulkan device was lost while beginning the Qt Quick frame")); + } else { + ++consecutiveBeginFrameFailures; + if (consecutiveBeginFrameFailures >= FatalBeginFrameFailureThreshold) { + // beginFrame errors never reach endFrameChecked(), so + // retired resources and after-swap cleanup jobs would + // never run again; escalate instead of freezing. + failCurrentFrameFatally(QStringLiteral( + "Qt Quick Vulkan frame begin failed %1 times in a row") + .arg(consecutiveBeginFrameFailures)); + } else { + failCurrentFrame(); + qCWarning(lcWlRenderer) << "Skipping Vulkan output frame because QRhi beginOffscreenFrame failed" + << "frameResult" << beginResult + << "consecutiveFailures" << consecutiveBeginFrameFailures; + } + } + finishFrame({}); + return; + } + consecutiveBeginFrameFailures = 0; + 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) { + const bool flushed = WRenderHelper::flushTextureSyncBatch(m_renderer); + if (!flushed) { + // The recorded frame may sample a texture whose producer has + // not completed. Never submit it without the required wait. + failCurrentFrameFatally(QStringLiteral("Failed to synchronize Vulkan textures before submitting the Qt Quick frame")); + } + // beginFrameChecked() above succeeded, so QRhi is recording this + // offscreen frame no matter whether the sync-batch flush failed. + // A frame that is left open makes every later beginFrame() return + // early inside QQuickRenderControl (Qt ignores it with + // "beginFrame() must be followed by endFrame()"), freezing the + // output on the last good image; and at shutdown the QRhi cleanup + // callbacks run while QRhi is still inFrame, so a backend flush + // from such a callback ends the abandoned primary command buffer + // and aborts Mesa's radv (amdgpu_cs finalize assertion). Always + // close the frame; its submission result only matters on the + // flush-ok path. + const auto endResult = rc()->endFrameChecked(); + if (flushed) { + vulkanFrameCompleted = endResult == QRhi::FrameOpSuccess; + if (!vulkanFrameCompleted) { + failCurrentFrameFatally(endResult == QRhi::FrameOpDeviceLost + ? QStringLiteral("Vulkan device was lost while submitting the Qt Quick frame") + : QStringLiteral("Failed to submit the Vulkan Qt Quick frame")); + } + } else if (endResult != QRhi::FrameOpSuccess) { + qCWarning(lcWlRenderer) << "Closing the unsynchronized Vulkan frame did not succeed either" + << "frameResult" << endResult; + } + } else { + rc()->endFrame(); + } + WVulkanTrace::setFrameStage(q, WVulkanTrace::FrameStage::AfterEndFrame); + } + + if (vulkanFrameCompleted) { + // The frame's command buffer has completed on the GPU. Commit any + // hardware cursor buffers rendered into it now, so the cursor plane + // can never scan out content the GPU has not rendered yet. + for (auto helper : std::as_const(outputs)) { + if (helper) + helper->flushPendingHardwareCursor(); + } + } // prevent gles2-render exception in wlroots. // wlroots may have render operations after commit, so do @@ -1581,16 +2776,22 @@ void WOutputRenderWindowPrivate::doRender(wlr_output *needsFrameOutput, resetGlState(); QList> committedOutputs; - if (doCommit) { + WVulkanTrace::setFrameStage(q, WVulkanTrace::FrameStage::OutputCommit); + if (doCommit || (isVulkan && frameFailed)) { + const bool allowCommit = doCommit && !frameFailed; committedOutputs.reserve(needsCommit.size()); for (auto i : std::as_const(needsCommit)) { - // Explicit render(viewport, true) is used for state-only output - // transactions. It must not be suppressed merely because the - // transaction itself scheduled the next frame. - if (forceRender || Q_UNLIKELY(!i.first->framePending())) { - if (Q_LIKELY(i.first->commit(i.second))) { + auto output = i.first->outputViewport()->output(); + if (allowCommit && (forceRender || Q_UNLIKELY(!i.first->framePending()))) { + const quint32 sequenceBefore = output->handle()->commit_seq; + const bool commitOk = i.first->commit(i.second); + WVulkanTrace::outputCommitted(q, output->handle(), sequenceBefore, commitOk); + if (Q_LIKELY(commitOk)) { + // The surfaces are only textured for presentation once the + // frame was actually committed; on failure the pending + // feedback stays queued for the next successful frame. + submitPresentationFeedbackForOutput(output); // Make sure the output is still valid after commit - auto output = i.first->outputViewport()->output(); if (Q_LIKELY(needsFrameOutput)) { Q_ASSERT(output->handle() == needsFrameOutput); if (committedOutputs.isEmpty()) @@ -1599,6 +2800,8 @@ void WOutputRenderWindowPrivate::doRender(wlr_output *needsFrameOutput, committedOutputs.append(output); } } + } else { + clearPresentationFeedbackForOutput(output); } if (i.second->currentBuffer()) { @@ -1607,20 +2810,25 @@ 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); + // All submissions of this completed frame are queue-ordered ahead + // of the next frame's on the shared VkQueue, so the per-frame alias + // lists have served their purpose; drop them to keep the alias + // barriers exact and the scans bounded. + WRenderHelper::resetVulkanFrameAliasLists(m_renderer); + } + finishFrame(committedOutputs); + if (vulkanFrameCompleted) + runAndClearJobs(&afterSwapJobs); } // TODO: Support QWindow::setCursor @@ -1646,11 +2854,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); @@ -1921,6 +3129,69 @@ 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::tryMoveHardwareCursor(WOutput *output, QQuickItem *cursorItem) +{ + Q_D(WOutputRenderWindow); + if (!output || !cursorItem) + return false; + + auto *quickCursor = qobject_cast(cursorItem); + if (!quickCursor) + return false; + + for (auto *helper : std::as_const(d->outputs)) { + if (helper && helper->outputViewport() + && helper->outputViewport()->output() == output) + return helper->moveHardwareCursorForPosition(quickCursor); + } + + return false; +} + +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, + wlr_buffer *sampledBuffer, + const char *purpose) +{ + Q_D(WOutputRenderWindow); + return d->prepareTextureForCurrentRenderPass(texture, sampledBuffer, 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..62660876b0 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,27 @@ 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); + + // Fast path for pointer movement on the Vulkan/DRM hardware-cursor path: + // when the hardware cursor plane is already set up for \a output, move it + // directly and return true, so the caller can skip scheduling a full + // compositor frame. Returns false when a frame is still required (software + // cursor, cursor not set up yet, broken commit, or non-Vulkan backend). + bool tryMoveHardwareCursor(WOutput *output, QQuickItem *cursorItem); + + bool prepareTextureSamplingForRenderPass(wlr_buffer *currentBuffer, + const QVector &activeProviders, + const char *purpose, + int sourceIndex, + QVector *preparedTextures); + bool prepareTextureForCurrentRenderPass(wlr_texture *texture, + wlr_buffer *sampledBuffer, + 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/wrenderbufferblitter.cpp b/waylib/src/server/qtquick/wrenderbufferblitter.cpp index 9443bd6469..685d530a6c 100644 --- a/waylib/src/server/qtquick/wrenderbufferblitter.cpp +++ b/waylib/src/server/qtquick/wrenderbufferblitter.cpp @@ -12,6 +12,8 @@ WAYLIB_SERVER_BEGIN_NAMESPACE +QAtomicInt WRenderBufferBlitter::s_liveCount{0}; + class Q_DECL_HIDDEN BlitTextureProvider : public QSGTextureProvider { public: BlitTextureProvider() @@ -216,11 +218,12 @@ WRenderBufferBlitter::WRenderBufferBlitter(QQuickItem *parent) setFlag(ItemHasContents); W_D(WRenderBufferBlitter); d->init(); + s_liveCount.fetchAndAddRelaxed(1); } WRenderBufferBlitter::~WRenderBufferBlitter() { - + s_liveCount.fetchAndAddRelaxed(-1); } QQuickItem *WRenderBufferBlitter::content() const diff --git a/waylib/src/server/qtquick/wrenderbufferblitter.h b/waylib/src/server/qtquick/wrenderbufferblitter.h index 5157c39604..98b4f227ce 100644 --- a/waylib/src/server/qtquick/wrenderbufferblitter.h +++ b/waylib/src/server/qtquick/wrenderbufferblitter.h @@ -6,6 +6,7 @@ #include #include #include +#include WAYLIB_SERVER_BEGIN_NAMESPACE @@ -23,6 +24,10 @@ class Q_DECL_EXPORT WRenderBufferBlitter : public QQuickItem, public WObject explicit WRenderBufferBlitter(QQuickItem *parent = nullptr); ~WRenderBufferBlitter(); + // Number of live items. Lets the per-frame scene walk in + // WBufferRenderer short-circuit when no blitter exists anywhere. + static int liveCount() { return s_liveCount.loadRelaxed(); } + QQuickItem *content() const; bool offscreen() const; @@ -35,6 +40,8 @@ private Q_SLOTS: void invalidateSceneGraph(); private: + static QAtomicInt s_liveCount; + QSGNode *updatePaintNode(QSGNode *, UpdatePaintNodeData *) override; void itemChange(ItemChange, const ItemChangeData &) override; void geometryChange(const QRectF &newGeometry, const QRectF &oldGeometry) override; diff --git a/waylib/src/server/qtquick/wrenderhelper.cpp b/waylib/src/server/qtquick/wrenderhelper.cpp index 131631a62b..bdc771e7a7 100644 --- a/waylib/src/server/qtquick/wrenderhelper.cpp +++ b/waylib/src/server/qtquick/wrenderhelper.cpp @@ -8,25 +8,49 @@ #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 *); +using QRhiResourceMap = QHash; +W_DECLARE_PRIVATE_MEMBER(QRhiResources_tag, QRhiImplementation, resources, QRhiResourceMap); +#endif WAYLIB_SERVER_BEGIN_NAMESPACE @@ -38,10 +62,180 @@ struct Q_DECL_HIDDEN RhiRenderEntry { Q_GLOBAL_STATIC(QVector, s_rhiRenderBuffers) +#ifdef ENABLE_VULKAN_RENDER +static QString hex32(uint32_t value) +{ + return QStringLiteral("0x%1").arg(value, 8, 16, QLatin1Char('0')); +} + +static QString hex64(uint64_t value) +{ + return QStringLiteral("0x%1").arg(value, 16, 16, QLatin1Char('0')); +} + +static quint64 vkImageValue(VkImage image) +{ + if constexpr (std::is_pointer_v) { + return quint64(reinterpret_cast(image)); + } else { + return quint64(image); + } +} + +static QString vkImageName(VkImage image) +{ + return hex64(vkImageValue(image)); +} + +static const char *vkImageLayoutName(VkImageLayout layout) +{ + switch (layout) { + case VK_IMAGE_LAYOUT_UNDEFINED: + return "UNDEFINED"; + case VK_IMAGE_LAYOUT_GENERAL: + return "GENERAL"; + case VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL: + return "COLOR_ATTACHMENT_OPTIMAL"; + case VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL: + return "SHADER_READ_ONLY_OPTIMAL"; + case VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL: + return "TRANSFER_SRC_OPTIMAL"; + case VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL: + return "TRANSFER_DST_OPTIMAL"; + case VK_IMAGE_LAYOUT_PREINITIALIZED: + return "PREINITIALIZED"; + default: + return "UNKNOWN"; + } +} + +static QSize wlrBufferSize(wlr_buffer *buffer) +{ + return QSize(buffer->width, buffer->height); +} + +static QSize wlrTextureSize(wlr_texture *texture) +{ + return QSize(texture->width, texture->height); +} + +#ifdef ENABLE_VULKAN_RENDER +// wlroots-recorded barriers must never go into an active QRhi pass instance +// (see the checks inside WRenderHelper's barrier recording wrappers below). +static bool qtVulkanPassIsRecording(QRhiCommandBuffer *cb) +{ + if (!cb) + return false; + const auto *vkCb = static_cast(cb); + return vkCb->recordingPass != QVkCommandBuffer::NoPass; +} +#endif + +static bool getVulkanRenderBufferAttribs(wlr_renderer *renderer, + wlr_buffer *buffer, + const char *purpose, + wlr_vk_image_attribs *attribs) +{ + if (!renderer || !buffer || !wlr_renderer_is_vk(renderer)) + return false; + + if (waylib_vk_renderer_get_render_buffer_attribs(renderer, buffer, attribs)) { + qCDebug(lcWlRenderHelper) << "Got wlroots Vulkan render buffer attributes" + << "purpose" << purpose + << "buffer" << buffer + << "image" << vkImageName(attribs->image) + << "layout" << vkImageLayoutName(attribs->layout) + << "format" << hex32(attribs->format) + << "usage" << hex32(attribs->usage) + << "size" << wlrBufferSize(buffer); + return true; + } + + qCWarning(lcWlRenderHelper) << "Failed to get wlroots Vulkan render buffer attributes" + << "purpose" << purpose + << "buffer" << buffer + << "size" << wlrBufferSize(buffer); + return false; +} +#endif + +#if defined(ENABLE_VULKAN_RENDER) && QT_VERSION >= QT_VERSION_CHECK(6, 8, 0) + +// Qt's own leak report in ~QRhiImplementation runs after QRhiVulkan::destroy() +// has already aborted the process through the VMA assertion when any +// QRhiResource created on this QRhi outlives it (QML items or scene graph +// nodes that own QRhiBuffer/QRhiTexture objects and are torn down after the +// render control has released the QRhi). A QRhi cleanup callback still runs +// from ~QRhi before destroy(), while the VkDevice is alive, which is the +// only point where the leaked native objects can still be released safely: +// QVkBuffer/QVkTexture/QVkRenderBuffer::destroy() enqueue a deferred release +// that QRhiVulkan::destroy() force-executes (after vkDeviceWaitIdle) before +// it destroys the VmaAllocator. Qt-internal objects (command buffers, swap +// chains) and resources without VMA memory (samplers, pipelines, shader +// resource bindings, render-pass descriptors) are deliberately untouched. +static void forceReleaseVulkanGraphicsResources(QRhi *rhi) +{ + if (!rhi) + return; + + auto *rhiImplementation = static_cast( + W_PRIVATE_MEMBER(*rhi, QRhi_d_tag {})); + if (!rhiImplementation) + return; + + auto ®isteredResources = W_PRIVATE_MEMBER(*rhiImplementation, QRhiResources_tag {}); + const auto resources = registeredResources.keys(); + int releasedCount = 0; + for (QRhiResource *resource : resources) { + if (!resource) + continue; + switch (resource->resourceType()) { + case QRhiResource::Buffer: + case QRhiResource::Texture: + case QRhiResource::RenderBuffer: + ++releasedCount; + qCWarning(lcWlRenderHelper) + << "Force-releasing leaked QRhi graphics resource at teardown" + << "type" << int(resource->resourceType()) + << "rhi" << Qt::hex << quintptr(rhi) << Qt::dec + << "name" << (resource->name().isEmpty() ? QByteArrayLiteral("") + : resource->name()); + resource->destroy(); + break; + default: + break; + } + } + if (releasedCount) { + qCWarning(lcWlRenderHelper) + << "Force-released leaked QRhi graphics resources before Vulkan allocator teardown" + << "count" << releasedCount; + } +} +#endif + +void WRenderHelper::addVulkanTeardownSweep(QRhi *rhi, const void *key) +{ +#if defined(ENABLE_VULKAN_RENDER) && QT_VERSION >= QT_VERSION_CHECK(6, 8, 0) + if (!rhi || !key || rhi->backend() != QRhi::Vulkan) + return; + rhi->addCleanupCallback(key, [ ] (QRhi *cleanupRhi) { + forceReleaseVulkanGraphicsResources(cleanupRhi); + }); +#else + Q_UNUSED(rhi); + Q_UNUSED(key); +#endif +} + struct Q_DECL_HIDDEN BufferData { BufferData() = default; ~BufferData() { +#if defined(ENABLE_VULKAN_RENDER) && QT_VERSION >= QT_VERSION_CHECK(6, 8, 0) + if (vulkanBackdropRhi) + vulkanBackdropRhi->removeCleanupCallback(this); +#endif resetWindowRenderTarget(); } @@ -51,13 +245,26 @@ struct Q_DECL_HIDDEN BufferData { WImageRenderTarget paintDevice; QQuickRenderTarget renderTarget; QQuickWindowRenderTarget windowRenderTarget; + QQuickRenderTarget preserveRenderTarget; + QQuickWindowRenderTarget preserveWindowRenderTarget; + QQuickRenderTarget vulkanBackdropRenderTarget; + QQuickWindowRenderTarget vulkanBackdropWindowRenderTarget; + QQuickRenderTarget vulkanBackdropPreserveRenderTarget; + QQuickWindowRenderTarget vulkanBackdropPreserveWindowRenderTarget; + QQuickRenderTarget vulkanBackdropResumeRenderTarget; + QQuickWindowRenderTarget vulkanBackdropResumeWindowRenderTarget; +#if defined(ENABLE_VULKAN_RENDER) && QT_VERSION >= QT_VERSION_CHECK(6, 8, 0) + QRhi *vulkanBackdropRhi = nullptr; +#endif + bool vulkanBackdropUnavailableLogged = false; bool colorPreserved = false; - inline void resetWindowRenderTarget() { + static inline void cleanupWindowRenderTarget(QQuickWindowRenderTarget &target) { +#if QT_VERSION >= QT_VERSION_CHECK(6, 8, 0) { auto it = s_rhiRenderBuffers->begin(); while (it != s_rhiRenderBuffers->end()) { - if (windowRenderTarget.rt.renderTarget == it->renderTarget) { + if (target.rt.renderTarget == it->renderTarget) { it = s_rhiRenderBuffers->erase(it); break; } @@ -65,32 +272,110 @@ struct Q_DECL_HIDDEN BufferData { } } - if (windowRenderTarget.rt.owns) - delete windowRenderTarget.rt.renderTarget; + if (target.rt.owns) + delete target.rt.renderTarget; + + delete target.res.texture; + delete target.res.renderBuffer; + delete target.res.rpDesc; + + target.rt = {}; + target.res = {}; + { // target.implicitBuffers.reset(rhi); + delete target.implicitBuffers.depthStencil; + delete target.implicitBuffers.depthStencilTexture; + delete target.implicitBuffers.multisampleTexture; + target.implicitBuffers = {}; + } + + if (target.sw.owns) + delete target.sw.paintDevice; - delete windowRenderTarget.res.texture; - delete windowRenderTarget.res.renderBuffer; - delete windowRenderTarget.res.rpDesc; + target.sw = {}; +#else + { + auto it = s_rhiRenderBuffers->begin(); + while (it != s_rhiRenderBuffers->end()) { + if (target.renderTarget == it->renderTarget) { + it = s_rhiRenderBuffers->erase(it); + break; + } + ++it; + } + } - windowRenderTarget.rt = {}; - windowRenderTarget.res = {}; - { // windowRenderTarget.implicitBuffers.reset(rhi); - delete windowRenderTarget.implicitBuffers.depthStencil; - delete windowRenderTarget.implicitBuffers.depthStencilTexture; - delete windowRenderTarget.implicitBuffers.multisampleTexture; - windowRenderTarget.implicitBuffers = {}; + if (target.owns) { + delete target.renderTarget; + delete target.rpDesc; + delete target.texture; + delete target.renderBuffer; + delete target.depthStencil; + delete target.paintDevice; } - if (windowRenderTarget.sw.owns) - delete windowRenderTarget.sw.paintDevice; + target.renderTarget = nullptr; + target.rpDesc = nullptr; + target.texture = nullptr; + target.renderBuffer = nullptr; + target.depthStencil = nullptr; + target.paintDevice = nullptr; + target.owns = false; +#endif + } + + inline void resetWindowRenderTarget() { + // The three backdrop targets share the depth texture owned by the + // clear target. Destroy all borrowers before destroying the owner. + cleanupWindowRenderTarget(vulkanBackdropResumeWindowRenderTarget); + cleanupWindowRenderTarget(vulkanBackdropPreserveWindowRenderTarget); + cleanupWindowRenderTarget(vulkanBackdropWindowRenderTarget); + cleanupWindowRenderTarget(preserveWindowRenderTarget); + cleanupWindowRenderTarget(windowRenderTarget); + } + +#if defined(ENABLE_VULKAN_RENDER) && QT_VERSION >= QT_VERSION_CHECK(6, 8, 0) + // Register (idempotent, keyed by this BufferData) a cleanup callback on + // the QRhi so resetWindowRenderTarget() runs from ~QRhi's runCleanup(), + // before the backend destroys its VmaAllocator. QRhi-backed objects + // (render buffers, texture render targets, render-pass descriptors) must + // not outlive the RHI: at shutdown the window tears the render control + // down while WRenderHelper still caches them, and every allocation that + // survives until vmaDestroyAllocator turns into a fatal "Some allocations + // were not freed before destruction of this memory block!" assertion. + // Only the Vulkan backend registers; the OpenGL flow is unchanged. + void registerVulkanRhiTeardownCleanup(QRhi *rhi) + { + if (vulkanBackdropRhi == rhi) + return; + if (vulkanBackdropRhi) + vulkanBackdropRhi->removeCleanupCallback(this); + + vulkanBackdropRhi = rhi; + if (!vulkanBackdropRhi) + return; - windowRenderTarget.sw = {}; + vulkanBackdropRhi->addCleanupCallback(this, [this] (QRhi *cleanupRhi) { + if (vulkanBackdropRhi != cleanupRhi) + return; + + // The callback list is being cleared by QRhi. Avoid trying to + // remove this callback while it is being invoked. + vulkanBackdropRhi = nullptr; + resetWindowRenderTarget(); + }); + // Also arm the per-QRhi safety sweep once; it force-releases any + // graphics resource that outlives the render control on this QRhi + // (see forceReleaseVulkanGraphicsResources). Keyed by the QRhi, so + // repeated registrations deduplicate. + WRenderHelper::addVulkanTeardownSweep(vulkanBackdropRhi, vulkanBackdropRhi); } +#endif }; class WRenderHelper::RenderTarget::Private { public: std::weak_ptr data; + QQuickRenderTarget selectedTarget; }; WRenderHelper::RenderTarget::RenderTarget() : d(new Private) {} @@ -116,7 +401,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 @@ -153,6 +440,113 @@ static QRhiTextureRenderTarget::Flags rhiRenderTargetFlags(WGlobal::ColorContent : QRhiTextureRenderTarget::Flags{}; } +static std::unique_ptr newCompatibleRenderPassDescriptor( + QRhiTextureRenderTarget *renderTarget, const QByteArray &targetName) +{ +#if defined(ENABLE_VULKAN_RENDER) && QT_VERSION >= QT_VERSION_CHECK(6, 8, 0) + std::unique_ptr descriptor( + renderTarget->newCompatibleRenderPassDescriptor()); + if (!descriptor || !renderTarget->rhi() + || renderTarget->rhi()->backend() != QRhi::Vulkan) { + return descriptor; + } + + const auto flags = renderTarget->flags(); + const bool preserveColor = flags.testFlag( + QRhiTextureRenderTarget::PreserveColorContents); + const bool preserveDepthStencil = flags.testFlag( + QRhiTextureRenderTarget::PreserveDepthStencilContents); + + // Qt's offscreen Vulkan render passes leave subpassDeps empty, and Qt does + // not include them in its render-pass compatibility or pipeline-cache + // key. Vulkan, however, requires otherwise compatible render passes to + // have identical dependencies. Use the same conservative dependency for + // every Waylib-managed target with a given attachment topology, regardless + // of its load/store flags. Besides keeping reused pipelines compatible, + // this makes LOAD operations depend on the preceding attachment writes and + // layout transitions instead of Qt's write-only resource transition. + VkPipelineStageFlags attachmentStages = + VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT; + VkAccessFlags sourceAccess = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT; + VkAccessFlags destinationAccess = VK_ACCESS_COLOR_ATTACHMENT_READ_BIT + | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT; + const auto &description = renderTarget->description(); + if (description.depthTexture() || description.depthStencilBuffer()) { + attachmentStages |= VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT + | VK_PIPELINE_STAGE_LATE_FRAGMENT_TESTS_BIT; + sourceAccess |= VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT; + destinationAccess |= VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_READ_BIT + | VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT; + } + + auto *vulkanDescriptor = static_cast( + descriptor.get()); + VkSubpassDependency dependency = {}; + dependency.srcSubpass = VK_SUBPASS_EXTERNAL; + dependency.dstSubpass = 0; + dependency.srcStageMask = attachmentStages; + dependency.dstStageMask = attachmentStages; + dependency.srcAccessMask = sourceAccess; + dependency.dstAccessMask = destinationAccess; + dependency.dependencyFlags = VK_DEPENDENCY_BY_REGION_BIT; + + const auto dependencyMatches = [&dependency] ( + const VkSubpassDependency &candidate) { + return candidate.srcSubpass == dependency.srcSubpass + && candidate.dstSubpass == dependency.dstSubpass + && candidate.srcStageMask == dependency.srcStageMask + && candidate.dstStageMask == dependency.dstStageMask + && candidate.srcAccessMask == dependency.srcAccessMask + && candidate.dstAccessMask == dependency.dstAccessMask + && candidate.dependencyFlags == dependency.dependencyFlags; + }; + if (vulkanDescriptor->subpassDeps.size() == 1 + && dependencyMatches(vulkanDescriptor->subpassDeps[0])) { + return descriptor; + } + if (!vulkanDescriptor->subpassDeps.isEmpty()) { + qCWarning(lcWlRenderHelper) + << "Cannot normalize Vulkan render-pass dependencies" + << "target" << targetName + << "existingDependencyCount" + << vulkanDescriptor->subpassDeps.size(); + return {}; + } + vulkanDescriptor->subpassDeps.append(dependency); + + // The descriptor returned above already owns a native VkRenderPass. + // Clone it after extending the private Vulkan description so the clone's + // native render pass is created with the dependency. Destroying the + // temporary descriptor queues its native render pass for QRhi's normal + // deferred destruction. + std::unique_ptr synchronizedDescriptor( + vulkanDescriptor->newCompatibleRenderPassDescriptor()); + if (!synchronizedDescriptor) { + qCWarning(lcWlRenderHelper) + << "Failed to create synchronized Vulkan render pass" + << "target" << targetName + << "flags" << flags.toInt(); + return {}; + } + + if (WVulkanTrace::enabled()) { + qCDebug(lcWlRenderHelper).noquote() + << QStringLiteral("VKTRACE event=render-pass-dependency target=%1 colorLoad=%2 depthStencilLoad=%3 stages=0x%4 sourceAccess=0x%5 destinationAccess=0x%6") + .arg(QString::fromUtf8(targetName)) + .arg(preserveColor) + .arg(preserveDepthStencil) + .arg(quint32(attachmentStages), 0, 16) + .arg(quint32(sourceAccess), 0, 16) + .arg(quint32(destinationAccess), 0, 16); + } + return synchronizedDescriptor; +#else + Q_UNUSED(targetName); + return std::unique_ptr( + renderTarget->newCompatibleRenderPassDescriptor()); +#endif +} + static bool recreateRhiRenderTarget(BufferData *data, QRhiTextureRenderTarget::Flags flags) { auto renderTarget = static_cast( @@ -162,11 +556,21 @@ static bool recreateRhiRenderTarget(BufferData *data, QRhiTextureRenderTarget::F renderTarget->destroy(); renderTarget->setFlags(flags); - auto newRpDesc = renderTarget->newCompatibleRenderPassDescriptor(); + std::unique_ptr newRpDesc; +#if defined(ENABLE_VULKAN_RENDER) && QT_VERSION >= QT_VERSION_CHECK(6, 8, 0) + if (renderTarget->rhi() && renderTarget->rhi()->backend() == QRhi::Vulkan) { + newRpDesc = newCompatibleRenderPassDescriptor( + renderTarget, renderTarget->name()); + } else +#endif + { + // Keep non-Vulkan backends on QRhi's unmodified render-pass path. + newRpDesc.reset(renderTarget->newCompatibleRenderPassDescriptor()); + } if (!newRpDesc) return false; delete rpDesc; - rpDesc = newRpDesc; + rpDesc = newRpDesc.release(); renderTarget->setRenderPassDescriptor(rpDesc); return renderTarget->create(); } @@ -177,18 +581,46 @@ static bool createRhiRenderTarget(const QRhiColorAttachment &colorAttachment, int sampleCount, QRhi *rhi, QQuickWindowRenderTarget &dst, - QRhiTextureRenderTarget::Flags flags) + QRhiTextureRenderTarget::Flags flags = {}, + bool withDepthStencil = true) { - 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; + // The Vulkan backdrop targets are created without a depth attachment: the + // blitter's render node never advertises DepthAwareRendering, so Qt keeps + // the 2D depth buffer disabled for the whole renderer that contains it and + // the attachment would only add a full-attachment depth load/store to every + // pass split. All other callers (including the GLES2 path) keep the depth + // attachment exactly as before. + std::unique_ptr depthStencil; + if (withDepthStencil) { + depthStencil.reset( + rhi->newRenderBuffer(QRhiRenderBuffer::DepthStencil, pixelSize, sampleCount)); + if (!depthStencil->create()) { + qCWarning(lcWlRenderHelper, "Failed to build depth-stencil buffer for QQuickRenderTarget"); + return false; + } } QRhiTextureRenderTargetDescription rtDesc(colorAttachment); - rtDesc.setDepthStencilBuffer(depthStencil.get()); + if (depthStencil) + rtDesc.setDepthStencilBuffer(depthStencil.get()); std::unique_ptr rt(rhi->newTextureRenderTarget(rtDesc, flags)); - std::unique_ptr rp(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()) { @@ -205,7 +637,8 @@ static bool createRhiRenderTarget(const QRhiColorAttachment &colorAttachment, } bool createRhiRenderTarget(QRhi *rhi, const QQuickRenderTarget &source, QQuickWindowRenderTarget &dst, - QRhiTextureRenderTarget::Flags flags) + QRhiTextureRenderTarget::Flags rtFlags = {}, + QRhiTexture::Flags extraTextureFlags = {}) { auto rtd = QQuickRenderTargetPrivate::get(&source); @@ -213,15 +646,23 @@ bool createRhiRenderTarget(QRhi *rhi, const QQuickRenderTarget &source, QQuickWi case QQuickRenderTargetPrivate::Type::NativeTexture: { const auto format = rtd->u.nativeTexture.rhiFormat == QRhiTexture::UnknownFormat ? QRhiTexture::RGBA8 : QRhiTexture::Format(rtd->u.nativeTexture.rhiFormat); - const auto textureFlags = QRhiTexture::RenderTarget | QRhiTexture::Flags( + const auto textureFlags = QRhiTexture::RenderTarget | extraTextureFlags | QRhiTexture::Flags( +#if QT_VERSION >= QT_VERSION_CHECK(6, 8, 0) rtd->u.nativeTexture.rhiFormatFlags +#else + rtd->u.nativeTexture.rhiFlags +#endif ); std::unique_ptr texture(rhi->newTexture(format, rtd->pixelSize, rtd->sampleCount, textureFlags)); texture->setName(QByteArrayLiteral("WaylibTexture")); +#if QT_VERSION < QT_VERSION_CHECK(6, 6, 0) + if (!texture->createFrom({ rtd->u.nativeTexture.object, rtd->u.nativeTexture.layout })) +#else if (!texture->createFrom({ rtd->u.nativeTexture.object, rtd->u.nativeTexture.layoutOrState })) +#endif return false; QRhiColorAttachment att(texture.get()); - if (!createRhiRenderTarget(att, rtd->pixelSize, rtd->sampleCount, rhi, dst, flags)) + if (!createRhiRenderTarget(att, rtd->pixelSize, rtd->sampleCount, rhi, dst, rtFlags)) return false; dst.res.texture = texture.release(); return true; @@ -233,7 +674,7 @@ bool createRhiRenderTarget(QRhi *rhi, const QQuickRenderTarget &source, QQuickWi return false; } QRhiColorAttachment att(renderbuffer.get()); - if (!createRhiRenderTarget(att, rtd->pixelSize, rtd->sampleCount, rhi, dst, flags)) + if (!createRhiRenderTarget(att, rtd->pixelSize, rtd->sampleCount, rhi, dst, rtFlags)) return false; renderbuffer->setName(QByteArrayLiteral("WaylibRenderBuffer")); dst.res.renderBuffer = renderbuffer.release(); @@ -256,28 +697,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 @@ -296,20 +766,205 @@ void WRenderHelperPrivate::onBufferDestroy(BufferData *data) } } -bool WRenderHelperPrivate::ensureRhiRenderTarget(QQuickRenderControl *rc, BufferData *data, - QRhiTextureRenderTarget::Flags flags) +bool WRenderHelperPrivate::ensureRhiRenderTarget(QQuickRenderControl *rc, + BufferData *data, + QRhiTextureRenderTarget::Flags flags, + QRhiTexture::Flags extraTextureFlags) { data->resetWindowRenderTarget(); auto rhi = rc->rhi(); auto tmp = data->renderTarget; - bool ok = createRhiRenderTarget(rhi, tmp, data->windowRenderTarget, flags); + bool ok = createRhiRenderTarget(rhi, tmp, data->windowRenderTarget, + flags, extraTextureFlags); if (!ok) return false; data->renderTarget = QQuickRenderTarget::fromRhiRenderTarget(data->windowRenderTarget.rt.renderTarget); data->renderTarget.setDevicePixelRatio(tmp.devicePixelRatio()); data->renderTarget.setMirrorVertically(tmp.mirrorVertically()); + if (rhi->backend() == QRhi::Vulkan) { + auto rtd = QQuickRenderTargetPrivate::get(&tmp); +#if QT_VERSION >= QT_VERSION_CHECK(6, 8, 0) + auto colorTexture = data->windowRenderTarget.res.texture; +#else + auto colorTexture = data->windowRenderTarget.texture; +#endif + if (!colorTexture) { + qCWarning(lcWlRenderHelper) + << "Failed to build Vulkan preserve render target: missing shared QRhi texture"; + return false; + } + + QRhiColorAttachment colorAttachment(colorTexture); + ok = createRhiRenderTarget(colorAttachment, + rtd->pixelSize, + rtd->sampleCount, + rhi, + data->preserveWindowRenderTarget, + QRhiTextureRenderTarget::PreserveColorContents); + if (!ok) { + qCWarning(lcWlRenderHelper) << "Failed to build Vulkan preserve render target for QQuickRenderTarget"; + return false; + } +#if QT_VERSION >= QT_VERSION_CHECK(6, 8, 0) + data->preserveRenderTarget = QQuickRenderTarget::fromRhiRenderTarget(data->preserveWindowRenderTarget.rt.renderTarget); +#else + data->preserveRenderTarget = QQuickRenderTarget::fromRhiRenderTarget(data->preserveWindowRenderTarget.renderTarget); +#endif + data->preserveRenderTarget.setDevicePixelRatio(tmp.devicePixelRatio()); + data->preserveRenderTarget.setMirrorVertically(tmp.mirrorVertically()); + } + +#if defined(ENABLE_VULKAN_RENDER) && QT_VERSION >= QT_VERSION_CHECK(6, 8, 0) + // Every BufferData owning QRhi objects must release them from ~QRhi's + // runCleanup(), before the VmaAllocator they are backed by dies (see + // registerVulkanRhiTeardownCleanup). + if (rhi->backend() == QRhi::Vulkan) + data->registerVulkanRhiTeardownCleanup(rhi); +#endif + return true; +} + +bool WRenderHelperPrivate::ensureVulkanBackdropRenderTargets(QQuickRenderControl *rc, + BufferData *data) +{ +#if !defined(ENABLE_VULKAN_RENDER) || QT_VERSION < QT_VERSION_CHECK(6, 8, 0) + Q_UNUSED(rc); + Q_UNUSED(data); + return false; +#else + if (data->vulkanBackdropWindowRenderTarget.rt.renderTarget + && data->vulkanBackdropPreserveWindowRenderTarget.rt.renderTarget + && data->vulkanBackdropResumeWindowRenderTarget.rt.renderTarget) { + return true; + } + if (data->vulkanBackdropUnavailableLogged) + return false; + + QRhi *rhi = rc ? rc->rhi() : nullptr; + auto colorTexture = data->windowRenderTarget.res.texture; + const auto *nativeHandles = rhi && rhi->backend() == QRhi::Vulkan + ? static_cast(rhi->nativeHandles()) + : nullptr; + // The blitter's Vulkan render node never advertises DepthAwareRendering, so + // Qt disables the 2D depth buffer for the whole batch renderer as soon as a + // blitter node exists. The backdrop targets are therefore built without a + // depth attachment (see createRhiRenderTarget): the depth contents are never + // read, and keeping it would add a full-attachment depth load/store plus a + // depth barrier to every pass split. WRenderBufferNode::prepare() fails + // closed if a depth-using renderer ever reaches the blitter. + const bool sameDevice = renderer && wlr_renderer_is_vk(renderer) && nativeHandles + && nativeHandles->physDev == wlr_vk_renderer_get_physical_device(renderer) + && nativeHandles->dev == wlr_vk_renderer_get_device(renderer) + && nativeHandles->gfxQueueFamilyIdx == wlr_vk_renderer_get_queue_family(renderer) + && nativeHandles->gfxQueue == waylib_vk_renderer_get_queue(renderer); + if (!rhi || rhi->backend() != QRhi::Vulkan || !colorTexture + || colorTexture->sampleCount() != 1 + || !colorTexture->flags().testFlag(QRhiTexture::UsedAsTransferSource) + || !sameDevice) { + if (!data->vulkanBackdropUnavailableLogged) { + qCWarning(lcWlRenderHelper) + << "Vulkan backdrop targets are unavailable" + << "reason" << "incompatible renderer, device, or color attachment" + << "rhi" << rhi + << "colorTexture" << colorTexture + << "sampleCount" << (colorTexture ? colorTexture->sampleCount() : 0) + << "transferSource" + << (colorTexture + && colorTexture->flags().testFlag(QRhiTexture::UsedAsTransferSource)) + << "sameDevice" << sameDevice; + data->vulkanBackdropUnavailableLogged = true; + } + return false; + } + + BufferData::cleanupWindowRenderTarget(data->vulkanBackdropResumeWindowRenderTarget); + BufferData::cleanupWindowRenderTarget(data->vulkanBackdropPreserveWindowRenderTarget); + BufferData::cleanupWindowRenderTarget(data->vulkanBackdropWindowRenderTarget); + data->vulkanBackdropRenderTarget = {}; + data->vulkanBackdropPreserveRenderTarget = {}; + data->vulkanBackdropResumeRenderTarget = {}; + + const QSize pixelSize = colorTexture->pixelSize(); + const int sampleCount = colorTexture->sampleCount(); + const QRhiColorAttachment colorAttachment(colorTexture); + + bool ok = createRhiRenderTarget(colorAttachment, pixelSize, sampleCount, rhi, + data->vulkanBackdropWindowRenderTarget, + QRhiTextureRenderTarget::Flags{}, + /*withDepthStencil=*/false); + if (ok) { + ok = createRhiRenderTarget(colorAttachment, pixelSize, sampleCount, rhi, + data->vulkanBackdropPreserveWindowRenderTarget, + QRhiTextureRenderTarget::PreserveColorContents, + /*withDepthStencil=*/false); + } + if (ok) { + // Only color needs to be preserved now: with no depth attachment the + // PreserveDepthStencilContents flag would be a no-op, and dropping it + // keeps this render pass compatible with the preserve target's. + ok = createRhiRenderTarget(colorAttachment, pixelSize, sampleCount, rhi, + data->vulkanBackdropResumeWindowRenderTarget, + QRhiTextureRenderTarget::PreserveColorContents, + /*withDepthStencil=*/false); + } + + if (!ok) { + BufferData::cleanupWindowRenderTarget(data->vulkanBackdropResumeWindowRenderTarget); + BufferData::cleanupWindowRenderTarget(data->vulkanBackdropPreserveWindowRenderTarget); + BufferData::cleanupWindowRenderTarget(data->vulkanBackdropWindowRenderTarget); + if (!data->vulkanBackdropUnavailableLogged) { + qCWarning(lcWlRenderHelper) + << "Vulkan backdrop targets are unavailable" + << "reason" << "target creation failed" + << "pixelSize" << pixelSize + << "sampleCount" << sampleCount; + data->vulkanBackdropUnavailableLogged = true; + } + return false; + } + + // Idempotent: usually already registered by ensureRhiRenderTarget(). + data->registerVulkanRhiTeardownCleanup(rhi); + + const auto makeQuickTarget = [&] (QQuickWindowRenderTarget &windowTarget) { + QQuickRenderTarget target = QQuickRenderTarget::fromRhiRenderTarget( + windowTarget.rt.renderTarget); + target.setDevicePixelRatio(data->renderTarget.devicePixelRatio()); + target.setMirrorVertically(data->renderTarget.mirrorVertically()); + return target; + }; + data->vulkanBackdropRenderTarget = makeQuickTarget( + data->vulkanBackdropWindowRenderTarget); + data->vulkanBackdropPreserveRenderTarget = makeQuickTarget( + data->vulkanBackdropPreserveWindowRenderTarget); + data->vulkanBackdropResumeRenderTarget = makeQuickTarget( + data->vulkanBackdropResumeWindowRenderTarget); + + s_rhiRenderBuffers->append({ + data->vulkanBackdropWindowRenderTarget.rt.renderTarget, + colorTexture, data->buffer }); + s_rhiRenderBuffers->append({ + data->vulkanBackdropPreserveWindowRenderTarget.rt.renderTarget, + colorTexture, data->buffer }); + s_rhiRenderBuffers->append({ + data->vulkanBackdropResumeWindowRenderTarget.rt.renderTarget, + colorTexture, data->buffer }); + + if (WVulkanTrace::enabled()) { + qCDebug(lcWlRenderHelper).noquote() + << QStringLiteral("VKTRACE event=blitter-targets-create buffer=%1 color=%2 clear=%3 preserveColor=%4 resume=%5 size=%6x%7") + .arg(quintptr(data->buffer.get()), 0, 16) + .arg(quintptr(colorTexture), 0, 16) + .arg(quintptr(data->vulkanBackdropWindowRenderTarget.rt.renderTarget), 0, 16) + .arg(quintptr(data->vulkanBackdropPreserveWindowRenderTarget.rt.renderTarget), 0, 16) + .arg(quintptr(data->vulkanBackdropResumeWindowRenderTarget.rt.renderTarget), 0, 16) + .arg(pixelSize.width()) + .arg(pixelSize.height()); + } + return true; +#endif } WRenderHelper::WRenderHelper(wlr_renderer *renderer, QObject *parent) @@ -331,73 +986,321 @@ void WRenderHelper::setSize(const QSize &size) if (d->size == size) return; d->size = size; - d->resetRenderBuffer(); + d->resetRenderBuffer(d->shouldDeferRenderBufferCleanup()); Q_EMIT sizeChanged(); } -QSGRendererInterface::GraphicsApi WRenderHelper::getGraphicsApi(QQuickRenderControl *rc) +void WRenderHelper::cleanupRetiredRenderResources(bool force) { - auto d = QQuickRenderControlPrivate::get(rc); - return d->sg->rendererInterface(d->rc)->graphicsApi(); + W_D(WRenderHelper); + d->cleanupRetiredRenderBuffers(force); } -QSGRendererInterface::GraphicsApi WRenderHelper::getGraphicsApi() +bool WRenderHelper::acquireRenderBuffer(QQuickRenderControl *rc, wlr_buffer *buffer, const char *purpose) { - auto getApi = [] () { - // Only for get GraphicsApi - QQuickRenderControl rc; - return getGraphicsApi(&rc); - }; +#ifdef ENABLE_VULKAN_RENDER + W_D(WRenderHelper); + if (!d->renderer || !wlr_renderer_is_vk(d->renderer)) + return true; - static auto api = getApi(); - return api; -} + wlr_vk_image_attribs attribs = {}; + if (!getVulkanRenderBufferAttribs(d->renderer, buffer, purpose, &attribs)) + return false; -class Q_DECL_HIDDEN GLTextureBuffer -{ -public: - GLTextureBuffer(wlr_egl *egl, QSGTexture *texture, int width, int height); + 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; + } - wlr_buffer *handle() { return &buffer; } + 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; + } -private: - static const struct wlr_buffer_impl impl; - static bool get_dmabuf(struct wlr_buffer *buffer, struct wlr_dmabuf_attributes *attribs); - static void destroy(struct wlr_buffer *buffer); + 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; + } - static inline GLTextureBuffer *fromBuffer(wlr_buffer *buffer) { - if (buffer->impl != &GLTextureBuffer::impl) - return nullptr; - return W_CONTAINER_OF(buffer, GLTextureBuffer, buffer); + 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; } - wlr_buffer buffer; - wlr_egl *m_egl; - QSGTexture *m_texture; -}; + // Queue-family ownership barriers are illegal inside a pass instance; all + // render buffer acquires must be recorded between QRhi passes. + if (Q_UNLIKELY(qtVulkanPassIsRecording(commandBuffer))) { + commandBuffer->endExternal(); + qCCritical(lcWlRenderHelper) + << "Vulkan render buffer acquire requested inside an active QRhi pass" + << "purpose" << purpose << "buffer" << buffer; + return false; + } -const struct wlr_buffer_impl GLTextureBuffer::impl = { - .destroy = GLTextureBuffer::destroy, - .get_dmabuf = GLTextureBuffer::get_dmabuf, - .get_shm = NULL, - .begin_data_ptr_access = NULL, - .end_data_ptr_access = NULL, -}; + const bool ok = waylib_vk_renderer_record_render_buffer_acquire( + d->renderer, buffer, handles->commandBuffer); + commandBuffer->endExternal(); -GLTextureBuffer::GLTextureBuffer(wlr_egl *egl, QSGTexture *texture, int width, int height) - : m_egl(egl) - , m_texture(texture) -{ - wlr_buffer_init(&buffer, &impl, width, height); + 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 GLTextureBuffer::get_dmabuf(struct wlr_buffer *buffer, wlr_dmabuf_attributes *attribs) +bool WRenderHelper::releaseRenderBuffer(QQuickRenderControl *rc, + wlr_buffer *buffer, + QRhiTexture *renderTargetTexture, + const char *purpose) { - auto *self = GLTextureBuffer::fromBuffer(buffer); - Q_ASSERT(self); - auto rhiTexture = self->m_texture->rhiTexture(); - if (!rhiTexture) +#ifdef ENABLE_VULKAN_RENDER + W_D(WRenderHelper); + if (!d->renderer || !wlr_renderer_is_vk(d->renderer)) + return true; + + wlr_vk_image_attribs attribs = {}; + if (!getVulkanRenderBufferAttribs(d->renderer, buffer, purpose, &attribs)) + return false; + + if (!renderTargetTexture) { + qCWarning(lcWlRenderHelper) << "Vulkan render buffer release failed: missing Qt render target texture" + << "purpose" << purpose + << "buffer" << buffer + << "image" << vkImageName(attribs.image) + << "layout" << vkImageLayoutName(attribs.layout) + << "format" << hex32(attribs.format) + << "size" << wlrBufferSize(buffer); + return false; + } + + if (!rc || !rc->rhi() || rc->rhi()->backend() != QRhi::Vulkan) { + qCWarning(lcWlRenderHelper) << "Vulkan render buffer release failed: missing Vulkan QRhi" + << "purpose" << purpose + << "buffer" << buffer + << "image" << vkImageName(attribs.image) + << "layout" << vkImageLayoutName(attribs.layout) + << "format" << hex32(attribs.format) + << "size" << wlrBufferSize(buffer); + return false; + } + + if (rc->rhi()->isDeviceLost() || !rc->rhi()->isRecordingFrame()) { + qCWarning(lcWlRenderHelper) << "Vulkan render buffer release failed: QRhi frame is not usable" + << "purpose" << purpose + << "deviceLost" << rc->rhi()->isDeviceLost() + << "recordingFrame" << rc->rhi()->isRecordingFrame() + << "buffer" << buffer + << "image" << vkImageName(attribs.image) + << "layout" << vkImageLayoutName(attribs.layout) + << "format" << hex32(attribs.format) + << "size" << wlrBufferSize(buffer); + return false; + } + + auto commandBuffer = rc->commandBuffer(); + if (!commandBuffer) { + qCWarning(lcWlRenderHelper) << "Vulkan render buffer release failed: missing QRhi command buffer" + << "purpose" << purpose + << "buffer" << buffer + << "image" << vkImageName(attribs.image) + << "layout" << vkImageLayoutName(attribs.layout) + << "format" << hex32(attribs.format) + << "size" << wlrBufferSize(buffer); + return false; + } + + const auto nativeTexture = renderTargetTexture->nativeTexture(); +#if QT_VERSION < QT_VERSION_CHECK(6, 6, 0) + const auto oldLayout = VkImageLayout(nativeTexture.layout); +#else + const auto oldLayout = VkImageLayout(nativeTexture.layout); +#endif + if (oldLayout == VK_IMAGE_LAYOUT_UNDEFINED) { + qCWarning(lcWlRenderHelper) << "Vulkan render buffer release failed: Qt render target layout is undefined" + << "purpose" << purpose + << "buffer" << buffer + << "image" << vkImageName(attribs.image) + << "wlrootsLayout" << vkImageLayoutName(attribs.layout) + << "oldLayout" << vkImageLayoutName(oldLayout) + << "format" << hex32(attribs.format) + << "size" << wlrBufferSize(buffer); + return false; + } + + commandBuffer->beginExternal(); + auto handles = static_cast(commandBuffer->nativeHandles()); + if (!handles || handles->commandBuffer == VK_NULL_HANDLE) { + commandBuffer->endExternal(); + qCWarning(lcWlRenderHelper) << "Vulkan render buffer release failed: missing native Vulkan command buffer" + << "purpose" << purpose + << "buffer" << buffer + << "image" << vkImageName(attribs.image) + << "wlrootsLayout" << vkImageLayoutName(attribs.layout) + << "oldLayout" << vkImageLayoutName(oldLayout) + << "format" << hex32(attribs.format) + << "size" << wlrBufferSize(buffer); + return false; + } + + // Ownership-release barriers are illegal inside a pass instance. + if (Q_UNLIKELY(qtVulkanPassIsRecording(commandBuffer))) { + commandBuffer->endExternal(); + qCCritical(lcWlRenderHelper) + << "Vulkan render buffer release requested inside an active QRhi pass" + << "purpose" << purpose << "buffer" << buffer; + return false; + } + + const bool ok = waylib_vk_renderer_record_render_buffer_release( + d->renderer, buffer, handles->commandBuffer, oldLayout); + commandBuffer->endExternal(); + + if (!ok) { + qCWarning(lcWlRenderHelper) << "Vulkan render buffer release failed" + << "purpose" << purpose + << "buffer" << buffer + << "image" << vkImageName(attribs.image) + << "wlrootsLayout" << vkImageLayoutName(attribs.layout) + << "oldLayout" << vkImageLayoutName(oldLayout) + << "format" << hex32(attribs.format) + << "size" << wlrBufferSize(buffer); + return false; + } + + // wlroots has recorded a transition and ownership release to GENERAL. + // Keep QRhi's external-image tracker in sync so that a reused output + // buffer starts from the layout which will exist after this submission. + renderTargetTexture->setNativeLayout(VK_IMAGE_LAYOUT_GENERAL); + + qCDebug(lcWlRenderHelper) << "Vulkan render buffer release recorded" + << "purpose" << purpose + << "buffer" << buffer + << "image" << vkImageName(attribs.image) + << "wlrootsLayout" << vkImageLayoutName(attribs.layout) + << "oldLayout" << vkImageLayoutName(oldLayout) + << "format" << hex32(attribs.format) + << "size" << wlrBufferSize(buffer); +#else + Q_UNUSED(rc); + Q_UNUSED(buffer); + Q_UNUSED(renderTargetTexture); + Q_UNUSED(purpose); +#endif + return true; +} + +QSGRendererInterface::GraphicsApi WRenderHelper::getGraphicsApi(QQuickRenderControl *rc) +{ + auto d = QQuickRenderControlPrivate::get(rc); + return d->sg->rendererInterface(d->rc)->graphicsApi(); +} + +QSGRendererInterface::GraphicsApi WRenderHelper::getGraphicsApi() +{ + auto getApi = [] () { + // Only for get GraphicsApi + QQuickRenderControl rc; + return getGraphicsApi(&rc); + }; + + static auto api = getApi(); + return api; +} + +class Q_DECL_HIDDEN GLTextureBuffer +{ +public: + GLTextureBuffer(wlr_egl *egl, QSGTexture *texture, int width, int height); + + wlr_buffer *handle() { return &buffer; } + +private: + static const struct wlr_buffer_impl impl; + static bool get_dmabuf(struct wlr_buffer *buffer, struct wlr_dmabuf_attributes *attribs); + static void destroy(struct wlr_buffer *buffer); + + static inline GLTextureBuffer *fromBuffer(wlr_buffer *buffer) { + if (buffer->impl != &GLTextureBuffer::impl) + return nullptr; + return W_CONTAINER_OF(buffer, GLTextureBuffer, buffer); + } + + wlr_buffer buffer; + wlr_egl *m_egl; + QSGTexture *m_texture; +}; + +const struct wlr_buffer_impl GLTextureBuffer::impl = { + .destroy = GLTextureBuffer::destroy, + .get_dmabuf = GLTextureBuffer::get_dmabuf, + .get_shm = NULL, + .begin_data_ptr_access = NULL, + .end_data_ptr_access = NULL, +}; + +GLTextureBuffer::GLTextureBuffer(wlr_egl *egl, QSGTexture *texture, int width, int height) + : m_egl(egl) + , m_texture(texture) +{ + wlr_buffer_init(&buffer, &impl, width, height); +} + +bool GLTextureBuffer::get_dmabuf(struct wlr_buffer *buffer, wlr_dmabuf_attributes *attribs) +{ + auto *self = GLTextureBuffer::fromBuffer(buffer); + Q_ASSERT(self); + auto rhiTexture = self->m_texture->rhiTexture(); + if (!rhiTexture) return false; auto display = wlr_egl_get_display(self->m_egl); @@ -499,17 +1402,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; } @@ -658,7 +1550,8 @@ wlr_buffer *WRenderHelper::toBuffer(wlr_renderer *renderer, QSGTexture *texture, } WRenderHelper::RenderTarget WRenderHelper::acquireRenderTarget(QQuickRenderControl *rc, wlr_buffer *buffer, - WGlobal::ColorContentsMode mode) + WGlobal::ColorContentsMode mode, + bool useVulkanBackdrop) { W_D(WRenderHelper); Q_ASSERT(buffer); @@ -693,8 +1586,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; } } @@ -704,6 +1606,8 @@ WRenderHelper::RenderTarget WRenderHelper::acquireRenderTarget(QQuickRenderContr bufferData->colorPreserved = needPreserve; QQuickRenderTarget rt; + bool needsVulkanRenderBufferAcquire = false; + QRhiTexture::Flags renderTargetTextureFlags; if (isSoftware) { WUniquePointer texture( @@ -719,15 +1623,24 @@ WRenderHelper::RenderTarget WRenderHelper::acquireRenderTarget(QQuickRenderContr } #ifdef ENABLE_VULKAN_RENDER else if (wlr_renderer_is_vk(d->renderer)) { - // Reuse wlroots' wlr_vk_render_buffer (same VkImage as tinywl/scene), - // not a parallel dmabuf import owned by waylib. wlr_vk_image_attribs attribs = {}; - if (!waylib_vk_renderer_get_render_buffer_attribs(d->renderer, buffer, &attribs)) - return {}; - rt = QQuickRenderTarget::fromVulkanImage(attribs.image, - attribs.layout, - attribs.format, - d->size); + if (getVulkanRenderBufferAttribs(d->renderer, buffer, "new-compositor-render-target", &attribs)) { + rt = QQuickRenderTarget::fromVulkanImage(attribs.image, + attribs.layout, + attribs.format, + wlrBufferSize(buffer)); + if (attribs.usage & VK_IMAGE_USAGE_TRANSFER_SRC_BIT) + renderTargetTextureFlags |= QRhiTexture::UsedAsTransferSource; + needsVulkanRenderBufferAcquire = true; + qCDebug(lcWlRenderHelper) << "Created Qt Vulkan render target from wlroots render buffer" + << "purpose" << "new-compositor-render-target" + << "buffer" << buffer + << "image" << vkImageName(attribs.image) + << "layout" << vkImageLayoutName(attribs.layout) + << "format" << hex32(attribs.format) + << "usage" << hex32(attribs.usage) + << "size" << wlrBufferSize(buffer); + } } #endif else if (wlr_renderer_is_gles2(d->renderer)) { @@ -747,16 +1660,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() }); + } } } @@ -773,93 +1696,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; - - QRhiTexture *qtTexture = data->windowRenderTarget.res.texture; - if (!qtTexture) { - qCWarning(lcWlRenderHelper, "Vulkan render buffer release: missing Qt QRhiTexture"); - return; - } - - const auto native = qtTexture->nativeTexture(); - const auto oldLayout = VkImageLayout(native.layout); - if (oldLayout == VK_IMAGE_LAYOUT_UNDEFINED) { - qCWarning(lcWlRenderHelper, "Vulkan render buffer release: Qt texture layout is UNDEFINED"); - return; - } - - cb->beginExternal(); - auto handles = static_cast(cb->nativeHandles()); - if (!handles || handles->commandBuffer == VK_NULL_HANDLE) { - cb->endExternal(); - qCWarning(lcWlRenderHelper, "Vulkan render buffer release: missing native command buffer"); - return; - } - - // graphics queue -> FOREIGN_EXT + GENERAL for KMS, using Qt's real old layout. - if (!waylib_vk_renderer_record_render_buffer_release(d->renderer, - data->buffer.get(), - handles->commandBuffer, - oldLayout)) { - qCWarning(lcWlRenderHelper) << "Vulkan render buffer release failed for" << data->buffer.get(); - cb->endExternal(); - return; - } - cb->endExternal(); + 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) { @@ -928,6 +1808,7 @@ void WRenderHelper::setupRendererBackend(wlr_backend *testBackend) && qgetenv("QT_QUICK_BACKEND") != "rhi")) { // when environment variable Q*_BACKEND was set, should defer to // the env variable for the graphics API. + WVulkanTrace::activate(getGraphicsApi() == QSGRendererInterface::Vulkan); return; } @@ -954,18 +1835,24 @@ void WRenderHelper::setupRendererBackend(wlr_backend *testBackend) wl_display_destroy(display); } } else if (wlrRenderer == "gles2") { + qCInfo(lcWlRenderHelper) << "Using explicit WLR_RENDERER=gles2 with Qt OpenGL scene graph"; QQuickWindow::setGraphicsApi(QSGRendererInterface::OpenGL); } else if (wlrRenderer == "vulkan") { #ifdef ENABLE_VULKAN_RENDER QQuickWindow::setGraphicsApi(QSGRendererInterface::Vulkan); #else + qCCritical(lcWlRenderHelper) << "WLR_RENDERER=vulkan was requested, but Vulkan support is not enabled"; qFatal("Vulkan support is not enabled"); #endif } else if (wlrRenderer == "pixman") { + qCInfo(lcWlRenderHelper) << "Using explicit WLR_RENDERER=pixman with Qt software scene graph"; QQuickWindow::setGraphicsApi(QSGRendererInterface::Software); } else { + qCCritical(lcWlRenderHelper) << "Unknown or unsupported WLR_RENDERER:" << wlrRenderer; qFatal() << "Unknown/Unsupported wlr renderer: " << wlrRenderer; } + + WVulkanTrace::activate(getGraphicsApi() == QSGRendererInterface::Vulkan); } QSGRendererInterface::GraphicsApi WRenderHelper::probe(wlr_backend *testBackend, const QList &apiList) @@ -1021,7 +1908,7 @@ QSGRendererInterface::GraphicsApi WRenderHelper::probe(wlr_backend *testBackend, return acceptApi; } -static void updateGLTexture(QRhi *rhi, wlr_texture *handle, QSGPlainTexture *texture) { +static bool updateGLTexture(QRhi *rhi, wlr_texture *handle, QSGPlainTexture *texture, bool) { wlr_gles2_texture_attribs attribs; wlr_gles2_texture_get_attribs(handle, &attribs); QSize size(handle->width, handle->height); @@ -1034,6 +1921,7 @@ static void updateGLTexture(QRhi *rhi, wlr_texture *handle, QSGPlainTexture *tex texture->setHasAlphaChannel(attribs.has_alpha); texture->setTextureSize(size); + return true; } static inline quint64 vkimage_cast(void *image) { @@ -1045,55 +1933,708 @@ 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 (A2R10G10B10/A2B10G10R10) to + // QRhiTexture::RGB10A2, and QVkTexture::finishCreate() derives the initial + // image view from viewFormatForSampling - so an A2R10G10B10 external image + // would get an A2B10G10R10 view, which cannot even be created without + // VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT. Such textures are constructed + // manually below (mirroring the attachment-only depth path) with the exact + // wlroots format instead of going through createFrom. + const bool needsExactFormatSeed = + mappedFormat == QRhiTexture::RGB10A2 + && attribs.format != VK_FORMAT_A2B10G10R10_UNORM_PACK32; + if (mappedFormat == QRhiTexture::UnknownFormat) { + qCDebug(lcWlQtQuickTexture) << "Rejected unsupported wlroots Vulkan texture format" + << "wlrTexture" << handle + << "image" << vkImageName(attribs.image) + << "format" << hex32(attribs.format) + << "reason" << "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; - texture->setTextureFromNativeTexture(rhi, - vkimage_cast(attribs.image), - attribs.layout, attribs.format, size, - {}, {}); - texture->setHasAlphaChannel(wlr_vk_texture_has_alpha(handle)); + 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; + + 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; + + 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 (needsExactFormatSeed) { + // Bypass createFrom(): QVkTexture::prepareCreate() would derive the + // view format from the QRhi enum again. Fill the QVkTexture by hand + // with the exact wlroots format, then create the final exact view + // (with the alpha swizzle) directly and register it with QRhi. + vkTexture->image = attribs.image; + vkTexture->usageState = { qtSampleLayout, 0, 0 }; + vkTexture->vkformat = attribs.format; + vkTexture->viewFormat = attribs.format; + vkTexture->viewFormatForSampling = attribs.format; + vkTexture->mipLevelCount = 1; + vkTexture->samples = VK_SAMPLE_COUNT_1_BIT; + vkTexture->generation += 1; + vkTexture->lastActiveFrameSlot = -1; + + VkImageView samplingView = VK_NULL_HANDLE; + const VkResult viewResult = deviceFunctions->vkCreateImageView(nativeHandles->dev, + &viewInfo, + nullptr, + &samplingView); + if (viewResult != VK_SUCCESS) { + qCDebug(lcWlQtQuickTexture) << "Failed to create exact Vulkan sampling view" + << "wlrTexture" << handle + << "image" << vkImageName(attribs.image) + << "format" << hex32(attribs.format) + << "hasAlpha" << hasAlpha + << "vkResult" << viewResult; + discardUnusedCandidate(); + return false; + } + vkTexture->imageView = samplingView; + + auto *rhiImplementation = static_cast( + W_PRIVATE_MEMBER(*rhi, QRhi_d_tag {})); + if (!rhiImplementation) { + discardUnusedCandidate(); + return false; + } + rhiImplementation->registerResource(vkTexture, false); + } else { + if (!candidate->createFrom({vkimage_cast(attribs.image), qtSampleLayout})) { + qCDebug(lcWlQtQuickTexture) << "Failed to wrap wlroots Vulkan image in QRhiTexture" + << "wlrTexture" << handle + << "image" << vkImageName(attribs.image) + << "format" << hex32(attribs.format) + << "layout" << vkImageLayoutName(qtSampleLayout) + << "size" << size; + return false; + } + + if (vkTexture->imageView == VK_NULL_HANDLE || vkTexture->lastActiveFrameSlot != -1) { + qCDebug(lcWlQtQuickTexture) << "Cannot replace the initial Qt Vulkan texture view" + << "wlrTexture" << handle + << "image" << vkImageName(attribs.image) + << "format" << hex32(attribs.format) + << "hasInitialView" << bool(vkTexture->imageView != VK_NULL_HANDLE) + << "lastActiveFrameSlot" << vkTexture->lastActiveFrameSlot; + discardUnusedCandidate(); + return false; + } + + VkImageView samplingView = VK_NULL_HANDLE; + const VkResult viewResult = deviceFunctions->vkCreateImageView(nativeHandles->dev, + &viewInfo, + nullptr, + &samplingView); + if (viewResult != VK_SUCCESS) { + qCDebug(lcWlQtQuickTexture) << "Failed to create exact Vulkan sampling view" + << "wlrTexture" << handle + << "image" << vkImageName(attribs.image) + << "format" << hex32(attribs.format) + << "hasAlpha" << hasAlpha + << "vkResult" << viewResult; + discardUnusedCandidate(); + return false; + } + + const VkImageView initialView = vkTexture->imageView; + vkTexture->imageView = samplingView; + vkTexture->viewFormatForSampling = attribs.format; + deviceFunctions->vkDestroyImageView(nativeHandles->dev, initialView, nullptr); + } + + const char *bridgeClass = hasAlpha ? "exact-alpha-view" : "exact-opaque-view"; + + 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 != waylib_vk_renderer_get_queue(renderer)) { + qCWarning(lcWlRenderHelper) << "Cannot begin Vulkan texture sync batch: Qt and wlroots do not share the same queue" + << "qtDevice" << (nativeHandles ? nativeHandles->dev : VK_NULL_HANDLE) + << "wlrootsDevice" << wlr_vk_renderer_get_device(renderer) + << "qtQueueFamily" << (nativeHandles ? nativeHandles->gfxQueueFamilyIdx : 0) + << "wlrootsQueueFamily" << wlr_vk_renderer_get_queue_family(renderer); + return false; + } + } + + if (!waylib_vk_renderer_begin_texture_sync_batch(renderer)) { + qCWarning(lcWlRenderHelper) << "Failed to begin Vulkan texture sync batch; using immediate CPU waits for this frame"; + return false; + } +#else + Q_UNUSED(rc); + Q_UNUSED(renderer); + Q_UNUSED(verifyQueue); +#endif + return true; +} + +bool WRenderHelper::flushTextureSyncBatch(wlr_renderer *renderer) +{ +#ifdef ENABLE_VULKAN_RENDER + if (!renderer || !wlr_renderer_is_vk(renderer)) + return true; + + if (!waylib_vk_renderer_flush_texture_sync_batch(renderer)) { + qCWarning(lcWlRenderHelper) << "Failed to flush Vulkan texture sync batch"; + return false; + } +#else + Q_UNUSED(renderer); +#endif + return true; +} + +void WRenderHelper::abortTextureSyncBatch(wlr_renderer *renderer) +{ +#ifdef ENABLE_VULKAN_RENDER + if (renderer && wlr_renderer_is_vk(renderer)) + waylib_vk_renderer_abort_texture_sync_batch(renderer); +#else + Q_UNUSED(renderer); +#endif +} + +bool WRenderHelper::beginTextureBarrierBatch(wlr_renderer *renderer, bool release) +{ +#ifdef ENABLE_VULKAN_RENDER + if (!renderer || !wlr_renderer_is_vk(renderer)) + return true; + + if (!waylib_vk_renderer_begin_texture_barrier_batch(renderer, release)) { + qCWarning(lcWlRenderHelper) << "Failed to begin Vulkan texture barrier batch" + << "release" << release; + return false; + } +#else + Q_UNUSED(renderer); + Q_UNUSED(release); +#endif + return true; +} + +bool WRenderHelper::flushTextureBarrierBatch(QQuickRenderControl *rc, + wlr_renderer *renderer, + const char *purpose) +{ +#ifdef ENABLE_VULKAN_RENDER + if (!renderer || !wlr_renderer_is_vk(renderer)) + return true; + + if (!rc || !rc->rhi() || rc->rhi()->backend() != QRhi::Vulkan) { + qCWarning(lcWlRenderHelper) << "Cannot flush Vulkan texture barrier batch: missing Vulkan QRhi" + << "purpose" << purpose; + waylib_vk_renderer_abort_texture_barrier_batch(renderer); + return false; + } + + if (rc->rhi()->isDeviceLost() || !rc->rhi()->isRecordingFrame()) { + qCWarning(lcWlRenderHelper) << "Cannot flush Vulkan texture barrier batch: QRhi frame is not usable" + << "purpose" << purpose + << "deviceLost" << rc->rhi()->isDeviceLost() + << "recordingFrame" << rc->rhi()->isRecordingFrame(); + waylib_vk_renderer_abort_texture_barrier_batch(renderer); + return false; + } + + auto commandBuffer = rc->commandBuffer(); + if (!commandBuffer) { + qCWarning(lcWlRenderHelper) << "Cannot flush Vulkan texture barrier batch: missing QRhi command buffer" + << "purpose" << purpose; + waylib_vk_renderer_abort_texture_barrier_batch(renderer); + return false; + } + + commandBuffer->beginExternal(); + auto handles = static_cast(commandBuffer->nativeHandles()); + if (!handles || handles->commandBuffer == VK_NULL_HANDLE) { + commandBuffer->endExternal(); + qCWarning(lcWlRenderHelper) << "Cannot flush Vulkan texture barrier batch: missing native Vulkan command buffer" + << "purpose" << purpose; + waylib_vk_renderer_abort_texture_barrier_batch(renderer); + return false; + } + + // Ownership/layout barrier batches must be recorded between QRhi passes. + if (Q_UNLIKELY(qtVulkanPassIsRecording(commandBuffer))) { + commandBuffer->endExternal(); + qCCritical(lcWlRenderHelper) + << "Vulkan texture barrier batch flush requested inside an active QRhi pass" + << "purpose" << purpose; + waylib_vk_renderer_abort_texture_barrier_batch(renderer); + return false; + } + + const bool ok = waylib_vk_renderer_flush_texture_barrier_batch( + renderer, handles->commandBuffer); + commandBuffer->endExternal(); + + if (!ok) { + qCWarning(lcWlRenderHelper) << "Failed to flush Vulkan texture barrier batch" + << "purpose" << purpose; + waylib_vk_renderer_abort_texture_barrier_batch(renderer); + } + return ok; +#else + Q_UNUSED(rc); + Q_UNUSED(renderer); + Q_UNUSED(purpose); return true; +#endif +} + +void WRenderHelper::abortTextureBarrierBatch(wlr_renderer *renderer) +{ +#ifdef ENABLE_VULKAN_RENDER + if (renderer && wlr_renderer_is_vk(renderer)) + waylib_vk_renderer_abort_texture_barrier_batch(renderer); +#else + Q_UNUSED(renderer); +#endif +} + +void WRenderHelper::setStageAsyncEnabled(wlr_renderer *renderer, bool enabled) +{ +#ifdef ENABLE_VULKAN_RENDER + if (renderer && wlr_renderer_is_vk(renderer)) + waylib_vk_renderer_set_stage_async_enabled(renderer, enabled); +#else + Q_UNUSED(renderer); + Q_UNUSED(enabled); +#endif +} + +bool WRenderHelper::restrictVulkanTextureFormats(wlr_renderer *renderer) +{ +#ifdef ENABLE_VULKAN_RENDER + if (!renderer || !wlr_renderer_is_vk(renderer)) + return true; + + // The DRM formats WRenderHelper::makeTexture() can wrap exactly for the + // Vulkan renderer. Restricting the renderer's advertised texture format + // sets to this subset keeps Wayland-native clients from allocating + // buffers the Qt wrapper would reject. XWayland chooses buffer formats + // from the X visual depth instead and does not consult the advertised + // sets; its depth-30 case is covered by the exact view seeding in + // updateVKTexture(). + static const uint32_t qtWrappableFormats[] = { + DRM_FORMAT_ARGB8888, + DRM_FORMAT_XRGB8888, + DRM_FORMAT_ABGR8888, + DRM_FORMAT_XBGR8888, + DRM_FORMAT_ARGB2101010, + DRM_FORMAT_XRGB2101010, + DRM_FORMAT_ABGR2101010, + DRM_FORMAT_XBGR2101010, + DRM_FORMAT_R8, + DRM_FORMAT_GR88, + DRM_FORMAT_R16F, + DRM_FORMAT_R32F, + DRM_FORMAT_ABGR16161616F, + DRM_FORMAT_XBGR16161616F, + DRM_FORMAT_ABGR32323232F, + }; + + if (!waylib_vk_renderer_restrict_texture_formats( + renderer, qtWrappableFormats, + sizeof(qtWrappableFormats) / sizeof(qtWrappableFormats[0]))) { + qCWarning(lcWlRenderHelper) + << "Failed to restrict the Vulkan texture format sets to the Qt-wrappable subset;" + << " clients may pick formats the Qt wrapper cannot display"; + return false; + } + return true; +#else + Q_UNUSED(renderer); + return true; +#endif +} + +void WRenderHelper::resetVulkanFrameAliasLists(wlr_renderer *renderer) +{ +#ifdef ENABLE_VULKAN_RENDER + if (!renderer || !wlr_renderer_is_vk(renderer)) + return; + waylib_vk_renderer_reset_frame_alias_lists(renderer); +#else + Q_UNUSED(renderer); +#endif +} + +bool WRenderHelper::prepareTextureForSampling(QQuickRenderControl *rc, + wlr_renderer *renderer, + wlr_texture *texture, + wlr_buffer *sampledBuffer, + const char *purpose) +{ +#ifdef ENABLE_VULKAN_RENDER + if (!renderer || !texture) + return true; + + if (!wlr_renderer_is_vk(renderer)) + return true; + + if (!wlr_texture_is_vk(texture)) { + qCWarning(lcWlQtQuickTexture) << "Vulkan texture sampling prepare failed: non-Vulkan wlroots texture" + << "purpose" << purpose + << "wlrTexture" << texture; + return false; + } + + wlr_vk_image_attribs rawAttribs = {}; + wlr_vk_texture_get_image_attribs(texture, &rawAttribs); + + if (!rc || !rc->rhi() || rc->rhi()->backend() != QRhi::Vulkan) { + qCWarning(lcWlQtQuickTexture) << "Vulkan texture sampling prepare failed: missing Vulkan QRhi" + << "purpose" << purpose + << "wlrTexture" << texture + << "image" << vkImageName(rawAttribs.image) + << "wlrootsLayout" << vkImageLayoutName(rawAttribs.layout) + << "format" << hex32(rawAttribs.format) + << "size" << wlrTextureSize(texture); + return false; + } + + if (rc->rhi()->isDeviceLost() || !rc->rhi()->isRecordingFrame()) { + qCWarning(lcWlQtQuickTexture) << "Vulkan texture sampling prepare failed: QRhi frame is not usable" + << "purpose" << purpose + << "deviceLost" << rc->rhi()->isDeviceLost() + << "recordingFrame" << rc->rhi()->isRecordingFrame() + << "wlrTexture" << texture + << "image" << vkImageName(rawAttribs.image) + << "wlrootsLayout" << vkImageLayoutName(rawAttribs.layout) + << "format" << hex32(rawAttribs.format) + << "size" << wlrTextureSize(texture); + return false; + } + + auto commandBuffer = rc->commandBuffer(); + if (!commandBuffer) { + qCWarning(lcWlQtQuickTexture) << "Vulkan texture sampling prepare failed: missing QRhi command buffer" + << "purpose" << purpose + << "wlrTexture" << texture + << "image" << vkImageName(rawAttribs.image) + << "wlrootsLayout" << vkImageLayoutName(rawAttribs.layout) + << "format" << hex32(rawAttribs.format) + << "size" << wlrTextureSize(texture); + return false; + } + + commandBuffer->beginExternal(); + auto handles = static_cast(commandBuffer->nativeHandles()); + if (!handles || handles->commandBuffer == VK_NULL_HANDLE) { + commandBuffer->endExternal(); + qCWarning(lcWlQtQuickTexture) << "Vulkan texture sampling prepare failed: missing native Vulkan command buffer" + << "purpose" << purpose + << "wlrTexture" << texture + << "image" << vkImageName(rawAttribs.image) + << "wlrootsLayout" << vkImageLayoutName(rawAttribs.layout) + << "format" << hex32(rawAttribs.format) + << "size" << wlrTextureSize(texture); + return false; + } + + // Acquire ownership barriers inside a pass instance are illegal; texture + // acquisition must happen between QRhi passes. + if (Q_UNLIKELY(qtVulkanPassIsRecording(commandBuffer))) { + commandBuffer->endExternal(); + qCCritical(lcWlQtQuickTexture) + << "Vulkan texture sampling prepare requested inside an active QRhi pass" + << "purpose" << purpose << "wlrTexture" << texture; + return false; + } + + const bool ok = waylib_vk_renderer_prepare_texture_for_sampling( + renderer, texture, sampledBuffer, 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(sampledBuffer); + Q_UNUSED(purpose); +#endif + return true; +} + +bool WRenderHelper::finishTextureSampling(QQuickRenderControl *rc, + wlr_renderer *renderer, + wlr_texture *texture, + const char *purpose) +{ +#ifdef ENABLE_VULKAN_RENDER + if (!renderer || !texture) + return true; + + if (!wlr_renderer_is_vk(renderer)) + return true; + + if (!wlr_texture_is_vk(texture)) { + qCWarning(lcWlQtQuickTexture) << "Vulkan texture sampling finish failed: non-Vulkan wlroots texture" + << "purpose" << purpose + << "wlrTexture" << texture; + return false; + } + + wlr_vk_image_attribs attribs = {}; + wlr_vk_texture_get_image_attribs(texture, &attribs); + + if (!rc || !rc->rhi() || rc->rhi()->backend() != QRhi::Vulkan) { + qCWarning(lcWlQtQuickTexture) << "Vulkan texture sampling finish failed: missing Vulkan QRhi" + << "purpose" << purpose + << "wlrTexture" << texture + << "image" << vkImageName(attribs.image) + << "layout" << vkImageLayoutName(attribs.layout) + << "format" << hex32(attribs.format) + << "size" << wlrTextureSize(texture); + return false; + } + + if (rc->rhi()->isDeviceLost() || !rc->rhi()->isRecordingFrame()) { + qCWarning(lcWlQtQuickTexture) << "Vulkan texture sampling finish failed: QRhi frame is not usable" + << "purpose" << purpose + << "deviceLost" << rc->rhi()->isDeviceLost() + << "recordingFrame" << rc->rhi()->isRecordingFrame() + << "wlrTexture" << texture + << "image" << vkImageName(attribs.image) + << "layout" << vkImageLayoutName(attribs.layout) + << "format" << hex32(attribs.format) + << "size" << wlrTextureSize(texture); + return false; + } + + auto commandBuffer = rc->commandBuffer(); + if (!commandBuffer) { + qCWarning(lcWlQtQuickTexture) << "Vulkan texture sampling finish failed: missing QRhi command buffer" + << "purpose" << purpose + << "wlrTexture" << texture + << "image" << vkImageName(attribs.image) + << "layout" << vkImageLayoutName(attribs.layout) + << "format" << hex32(attribs.format) + << "size" << wlrTextureSize(texture); + return false; + } + + commandBuffer->beginExternal(); + auto handles = static_cast(commandBuffer->nativeHandles()); + if (!handles || handles->commandBuffer == VK_NULL_HANDLE) { + commandBuffer->endExternal(); + qCWarning(lcWlQtQuickTexture) << "Vulkan texture sampling finish failed: missing native Vulkan command buffer" + << "purpose" << purpose + << "wlrTexture" << texture + << "image" << vkImageName(attribs.image) + << "layout" << vkImageLayoutName(attribs.layout) + << "format" << hex32(attribs.format) + << "size" << wlrTextureSize(texture); + return false; + } + + // Release-ownership barriers are illegal inside a pass instance. + if (Q_UNLIKELY(qtVulkanPassIsRecording(commandBuffer))) { + commandBuffer->endExternal(); + qCCritical(lcWlQtQuickTexture) + << "Vulkan texture sampling finish requested inside an active QRhi pass" + << "purpose" << purpose << "wlrTexture" << texture; + return false; + } + + const bool ok = waylib_vk_renderer_finish_texture_sampling( + renderer, texture, handles->commandBuffer); + commandBuffer->endExternal(); + + if (!ok) { + qCWarning(lcWlQtQuickTexture) << "Vulkan texture sampling finish failed" + << "purpose" << purpose + << "wlrTexture" << texture + << "image" << vkImageName(attribs.image) + << "layout" << vkImageLayoutName(attribs.layout) + << "format" << hex32(attribs.format) + << "size" << wlrTextureSize(texture); + return false; + } + +#else + Q_UNUSED(rc); + Q_UNUSED(renderer); + Q_UNUSED(texture); + Q_UNUSED(purpose); +#endif + return true; +} + +void WRenderHelper::markBufferContentDirty(wlr_renderer *renderer, wlr_buffer *buffer) +{ +#ifdef ENABLE_VULKAN_RENDER + if (!renderer || !buffer || !wlr_renderer_is_vk(renderer)) + return; + + // The client re-attached this buffer: its next sampling acquire must wait + // for a fresh producer fence. The wlroots side clears the per-texture + // "content already synchronized" shortcut for every imported texture of + // this buffer; non-client buffers are unaffected (they are never marked, + // so they keep waiting on every acquire). + waylib_vk_renderer_mark_buffer_content_dirty(renderer, buffer); +#else + Q_UNUSED(renderer); + Q_UNUSED(buffer); +#endif } WRenderHelper::TextureEntry @@ -1147,10 +2688,18 @@ WRenderHelper::newTexture(wlr_allocator *allocator, wlr_renderer *renderer, qFatal("The current QRhi backend doesn't support creating texture from Vulkan image"); } - wlr_vk_image_attribs attribs; - wlr_vk_texture_get_image_attribs(texture.get(), &attribs); + wlr_vk_image_attribs attribs = {}; + if (!waylib_vk_renderer_get_render_buffer_attribs(renderer, buffer, &attribs)) { + qCWarning(lcWlRenderHelper) << "Failed to create QRhiTexture from wlroots Vulkan render buffer attributes" + << "purpose" << "internal-rhi-texture" + << "wlrBuffer" << buffer + << "size" << size; + wlr_buffer_drop(buffer); + return {}; + } - if (!rhiTexture->createFrom({vkimage_cast(attribs.image), attribs.layout})) { + if (!rhiTexture->createFrom({vkimage_cast(attribs.image), + attribs.layout})) { qCCritical(lcWlRenderHelper, "Failed to create QRhiTexture from Vulkan image"); wlr_buffer_drop(buffer); return {}; diff --git a/waylib/src/server/qtquick/wrenderhelper.h b/waylib/src/server/qtquick/wrenderhelper.h index 0ea45a3ba4..ffab865b4e 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,58 @@ 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, + wlr_buffer *sampledBuffer, + const char *purpose); + static bool finishTextureSampling(QQuickRenderControl *rc, + wlr_renderer *renderer, + wlr_texture *texture, + const char *purpose); + static void markBufferContentDirty(wlr_renderer *renderer, wlr_buffer *buffer); + static bool beginTextureSyncBatch(QQuickRenderControl *rc, + wlr_renderer *renderer, + bool verifyQueue); + static bool flushTextureSyncBatch(wlr_renderer *renderer); + static void abortTextureSyncBatch(wlr_renderer *renderer); + static bool beginTextureBarrierBatch(wlr_renderer *renderer, + bool release); + static bool flushTextureBarrierBatch(QQuickRenderControl *rc, + wlr_renderer *renderer, + const char *purpose); + static void abortTextureBarrierBatch(wlr_renderer *renderer); + static void setStageAsyncEnabled(wlr_renderer *renderer, + bool enabled); + static bool restrictVulkanTextureFormats(wlr_renderer *renderer); + static void resetVulkanFrameAliasLists(wlr_renderer *renderer); + // Register a QRhi cleanup callback that force-releases every + // Buffer/Texture/RenderBuffer QRhiResource still owned by the QRhi when + // it is destroyed, before its Vulkan allocator is torn down. Objects + // outside of waylib's control (client QML items, scenegraph caches) that + // outlive a QQuickRenderControl would otherwise leak VMA memory and hit + // the vmaDestroyAllocator "Some allocations were not freed" assertion. + // No-op for non-Vulkan backends; \a key only deduplicates registrations. + static void addVulkanTeardownSweep(QRhi *rhi, const void *key); struct TextureEntry { wlr_buffer *buffer; diff --git a/waylib/src/server/qtquick/wsgtextureprovider.cpp b/waylib/src/server/qtquick/wsgtextureprovider.cpp index 1c164629e3..f012f36718 100644 --- a/waylib/src/server/qtquick/wsgtextureprovider.cpp +++ b/waylib/src/server/qtquick/wsgtextureprovider.cpp @@ -5,78 +5,330 @@ #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 and + // detach the wrapper immediately so no node can hold the + // scheduled-for-deletion texture through qtTexture. + qtTexture.setTexture(nullptr); + 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 +336,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 +372,207 @@ 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 && d->texture) + 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, + candidateBuffer.get(), + "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 +592,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 +607,7 @@ wlr_texture *WSGTextureProvider::qwTexture() const wlr_buffer *WSGTextureProvider::wlrBuffer() const { W_DC(WSGTextureProvider); - return d->buffer; + return d->isVulkanRhi() ? d->buffer.get() : d->legacyBuffer; } bool WSGTextureProvider::smooth() const @@ -209,6 +624,7 @@ void WSGTextureProvider::setSmooth(bool newSmooth) d->smooth = newSmooth; d->qtTexture.setFiltering(newSmooth ? QSGTexture::Linear : QSGTexture::Nearest); + d->updateMipmapFiltering(); Q_EMIT smoothChanged(); } diff --git a/waylib/src/server/qtquick/wsgtextureprovider.h b/waylib/src/server/qtquick/wsgtextureprovider.h index 06875ef04d..e5ddda3b3e 100644 --- a/waylib/src/server/qtquick/wsgtextureprovider.h +++ b/waylib/src/server/qtquick/wsgtextureprovider.h @@ -28,6 +28,7 @@ class WAYLIB_SERVER_EXPORT WSGTextureProvider : public QSGTextureProvider, publi void invalidate(); QSGTexture *texture() const override; + bool hasTexture() const; virtual wlr_texture *qwTexture() const; virtual wlr_buffer *wlrBuffer() const; diff --git a/waylib/src/server/qtquick/wsurfaceitem.cpp b/waylib/src/server/qtquick/wsurfaceitem.cpp index 953a032a45..a0f4449456 100644 --- a/waylib/src/server/qtquick/wsurfaceitem.cpp +++ b/waylib/src/server/qtquick/wsurfaceitem.cpp @@ -7,11 +7,13 @@ #include "woutput.h" #include "woutputrenderwindow.h" #include "woutputviewport.h" +#include "wrenderhelper.h" #include "wseat.h" #include "wsgtextureprovider.h" #include "wsurface.h" #include "wsurfaceitem_p.h" #include "wayliblogging.h" +#include "utils/private/wvulkantrace_p.h" #include @@ -24,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 @@ -164,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 { @@ -188,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) { @@ -223,6 +243,11 @@ class Q_DECL_HIDDEN WSurfaceItemContentPrivate: public QQuickItemPrivate Q_ASSERT(!updateTextureConnection); + if (usesVulkanRhi(q->outputRenderWindow())) { + pendingBuffer.reset(); + pendingBufferValid = false; + } + if (dontCacheLastBuffer) { buffer.reset(); cleanTextureProvider(); @@ -249,11 +274,27 @@ class Q_DECL_HIDDEN WSurfaceItemContentPrivate: public QQuickItemPrivate // Get the new buffer pointer from surface auto newBuffer = surface->buffer(); + // The client re-attached a buffer: its producer may have + // written new content, so the next Vulkan sampling acquire + // must wait for a fresh producer fence instead of reusing the + // previous one. No-op on non-Vulkan renderers. + if (auto *renderWindow = q->outputRenderWindow()) { + if (usesVulkanRhi(renderWindow)) + WRenderHelper::markBufferContentDirty(renderWindow->renderer(), + newBuffer); + } + if (!live) { // Non-live mode: defer to pendingBuffer pendingBuffer.reset(newBuffer); + if (usesVulkanRhi(q->outputRenderWindow())) + pendingBufferValid = true; } else { // Live mode: update buffer immediately + if (usesVulkanRhi(q->outputRenderWindow())) { + pendingBuffer.reset(); + pendingBufferValid = false; + } buffer.reset(newBuffer); q->update(); } @@ -263,6 +304,17 @@ class Q_DECL_HIDDEN WSurfaceItemContentPrivate: public QQuickItemPrivate surface->scheduleFrameIfNeeded(); }); + if (usesVulkanRhi(q->outputRenderWindow())) { + // The SurfaceItem can be created after the client has already + // committed its first buffer (notably for prelaunched XWayland + // surfaces). Seed the visible buffer instead of waiting for a + // future commit that may never arrive. + pendingBuffer.reset(); + pendingBufferValid = false; + buffer.reset(surface->buffer()); + q->update(); + } + updateFrameDoneConnection(); updateSurfaceState(); rendered = true; @@ -327,8 +379,18 @@ class Q_DECL_HIDDEN WSurfaceItemContentPrivate: public QQuickItemPrivate } inline void swapBufferIfNeeded() { - if (pendingBuffer) - buffer = std::move(pendingBuffer); + W_Q(WSurfaceItemContent); + if (!usesVulkanRhi(q->outputRenderWindow())) { + if (pendingBuffer) + buffer = std::move(pendingBuffer); + return; + } + + if (!pendingBufferValid) + return; + + buffer.take(pendingBuffer); + pendingBufferValid = false; } inline void setDevicePixelRatio(qreal dpr) { @@ -361,6 +423,7 @@ class Q_DECL_HIDDEN WSurfaceItemContentPrivate: public QQuickItemPrivate mutable WSGTextureProvider *textureProvider = nullptr; BufferRef buffer; BufferRef pendingBuffer; + bool pendingBufferValid = false; mutable QMetaObject::Connection updateTextureConnection; bool dontCacheLastBuffer = false; bool live = true; @@ -457,7 +520,13 @@ WSGTextureProvider *WSurfaceItemContent::wTextureProvider() const connect(this, &WSurfaceItemContent::smoothChanged, d->textureProvider, &WSGTextureProvider::setSmooth); - if (d->surface) { + if (usesVulkanRhi(w)) { + // Keep the visible buffer and its wlroots texture as one + // inseparable ownership tuple. This also preserves NonLive's + // frozen buffer instead of pairing it with the latest surface + // texture. + d->textureProvider->setBuffer(d->buffer.get()); + } else if (d->surface) { if (auto texture = wlr_surface_get_texture(d->surface->handle())) { d->textureProvider->setTexture(texture, d->buffer.get()); } else { @@ -485,6 +554,16 @@ void WSurfaceItemContent::setCacheLastBuffer(bool newCacheLastBuffer) if (d->dontCacheLastBuffer == !newCacheLastBuffer) return; d->dontCacheLastBuffer = !newCacheLastBuffer; + + if (usesVulkanRhi(outputRenderWindow()) + && d->dontCacheLastBuffer && !d->surface) { + d->pendingBuffer.reset(); + d->pendingBufferValid = false; + d->buffer.reset(); + d->cleanTextureProvider(); + update(); + } + Q_EMIT cacheLastBufferChanged(); } @@ -579,21 +658,91 @@ class Q_DECL_HIDDEN WSGRenderFootprintNode: public QSGRenderNode return NoExternalRendering | BoundedRectRendering | DepthAwareRendering | OpaqueRendering; } +protected: + void traceVulkanBinding(const void *provider, wlr_texture *texture) + { + if (!m_owner) + return; + + auto *ownerPrivate = m_owner->d_func(); + if (auto *renderWindow = m_owner->outputRenderWindow()) { + WVulkanTrace::surfaceFootprint(renderWindow, provider, texture, + ownerPrivate->surface); + renderWindow->markSurfaceTexturedForPresentation(ownerPrivate->surface); + } + } + +public: QPointer m_owner; }; +class Q_DECL_HIDDEN WSGVulkanRenderFootprintNode final : public WSGRenderFootprintNode +{ +public: + explicit WSGVulkanRenderFootprintNode(WSurfaceItemContent *owner) + : WSGRenderFootprintNode(owner) + { + } + + void setBinding(const void *provider, wlr_texture *texture) + { + m_provider = provider; + m_texture = texture; + } + + void render(const RenderState *state) override + { + WSGRenderFootprintNode::render(state); + traceVulkanBinding(m_provider, m_texture); + } + +private: + const void *m_provider = nullptr; + wlr_texture *m_texture = nullptr; +}; + QSGNode *WSurfaceItemContent::updatePaintNode(QSGNode *oldNode, UpdatePaintNodeData *) { W_D(WSurfaceItemContent); auto tp = wTextureProvider(); - if (d->live || !tp->texture()) { - auto texture = d->surface ? wlr_surface_get_texture(d->surface->handle()) : nullptr; - if (texture) { - tp->setTexture(texture, d->buffer.get()); - } else { - tp->setBuffer(d->buffer.get()); + const bool vulkanRhi = usesVulkanRhi(tp->window()); + if (!vulkanRhi) { + if (d->live || !tp->texture()) { + auto texture = d->surface ? wlr_surface_get_texture(d->surface->handle()) : nullptr; + if (texture) { + tp->setTexture(texture, d->buffer.get()); + } else { + tp->setBuffer(d->buffer.get()); + } + } + + if (!tp->texture() || width() <= 0 || height() <= 0) { + delete oldNode; + return nullptr; + } + + auto node = static_cast(oldNode); + if (Q_UNLIKELY(!node)) { + node = window()->createImageNode(); + node->setOwnsTexture(false); + QSGNode *fpnode = new WSGRenderFootprintNode(this); + node->appendChildNode(fpnode); } + + auto texture = tp->texture(); + node->setTexture(texture); + const QRectF textureGeometry = d->bufferSourceBox; + node->setSourceRect(textureGeometry); + const QRectF targetGeometry(d->ignoreBufferOffset ? QPointF() : d->bufferOffset, size()); + node->setRect(targetGeometry); + node->setFiltering(smooth() ? QSGTexture::Linear : QSGTexture::Nearest); + + return node; + } + + if (d->live || !tp->texture()) { + tp->setBuffer(d->buffer.get()); } if (!tp->texture() || width() <= 0 || height() <= 0) { @@ -602,15 +751,21 @@ QSGNode *WSurfaceItemContent::updatePaintNode(QSGNode *oldNode, UpdatePaintNodeD } auto node = static_cast(oldNode); + WSGVulkanRenderFootprintNode *vulkanFootprintNode = nullptr; if (Q_UNLIKELY(!node)) { node = window()->createImageNode(); node->setOwnsTexture(false); - QSGNode *fpnode = new WSGRenderFootprintNode(this); - node->appendChildNode(fpnode); + vulkanFootprintNode = new WSGVulkanRenderFootprintNode(this); + node->appendChildNode(vulkanFootprintNode); + } else { + vulkanFootprintNode = + static_cast(node->firstChild()); } auto texture = tp->texture(); node->setTexture(texture); + Q_ASSERT(vulkanFootprintNode); + vulkanFootprintNode->setBinding(tp, tp->qwTexture()); const QRectF textureGeometry = d->bufferSourceBox; node->setSourceRect(textureGeometry); const QRectF targetGeometry(d->ignoreBufferOffset ? QPointF() : d->bufferOffset, size()); @@ -1651,13 +1806,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/qtquick/wtextureproviderprovider.cpp b/waylib/src/server/qtquick/wtextureproviderprovider.cpp index 09e5ce233b..cf06e1ea56 100644 --- a/waylib/src/server/qtquick/wtextureproviderprovider.cpp +++ b/waylib/src/server/qtquick/wtextureproviderprovider.cpp @@ -3,7 +3,10 @@ #include "wtextureproviderprovider.h" #include "woutputrenderwindow.h" +#include "woutputviewport.h" #include "private/wglobal_p.h" +#include "utils/wbufferdumper.h" +#include "wsgtextureprovider.h" #include "wayliblogging.h" #include @@ -61,6 +64,51 @@ void WTextureCapturer::doGrabToImage() d->imgPromise.start(); WSGTextureProvider *textureProvider = d->provider->wTextureProvider(); auto *texture = textureProvider ? textureProvider->texture() : nullptr; + + // On Vulkan the provider's QRhiTexture wraps the wlroots-owned sampled + // VkImage, which is FOREIGN-owned and may lag behind the producer fence. + // A plain QRhi readback would sample it without the queue-family acquire + // (undefined contents, validation failure), so read the backing + // wlr_buffer through WBufferDumper, which runs the wlroots readback + // acquire/release gate. If the provider holds no buffer, fail rather + // than touching the imported image. + QRhi *rhi = d->renderWindow->rhi(); + if (rhi && rhi->backend() == QRhi::Vulkan) { + wlr_buffer *wlrBuffer = textureProvider ? textureProvider->wlrBuffer() : nullptr; + if (!wlrBuffer) { + // The primary output viewport has no cached texture provider on + // Vulkan (its buffer is the current render target), so fall back to + // the last committed output buffer, exactly like the capture module + // does for output sources. WOutputViewport::lastBuffer() is backed + // by a WPointer (null once the buffer is destroyed) and the + // swapchain keeps the buffer alive until the next commit. + if (auto *viewport = dynamic_cast(d->provider)) + wlrBuffer = viewport->lastBuffer(); + } + if (!wlrBuffer) { + d->imgPromise.setException(std::make_exception_ptr( + std::runtime_error("Texture provider is not valid."))); + d->imgPromise.finish(); + return; + } + QImage image; + const auto result = WBufferDumper::dumpBufferToImage( + wlrBuffer, d->renderWindow->renderer(), image); + if (result == WBufferDumper::DumpResult::Success && !image.isNull()) { + d->imgPromise.addResult(image); + } else { + qCWarning(lcWlTextureProvider) + << "Vulkan texture-provider readback failed" + << "buffer" << wlrBuffer + << "result" << WBufferDumper::dumpResultToString(result); + d->imgPromise.setException(std::make_exception_ptr(std::runtime_error( + "Vulkan texture readback failed: " + + WBufferDumper::dumpResultToString(result).toStdString()))); + } + d->imgPromise.finish(); + return; + } + if (texture && texture->rhiTexture()) { // Perform rhi texture read back auto *rhiTexture = texture->rhiTexture(); diff --git a/waylib/src/server/utils/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..7300d0d579 100644 --- a/waylib/src/server/utils/wbufferdumper.cpp +++ b/waylib/src/server/utils/wbufferdumper.cpp @@ -5,14 +5,16 @@ #include "wtools.h" #include "wayliblogging.h" -#include - +#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,40 +22,106 @@ WBufferDumper::DumpResult WBufferDumper::dumpBufferToImage(wlr_buffer *buffer, return DumpResult::InvalidBuffer; } - wlr_texture *texture = wlr_texture_from_buffer(renderer, buffer); + const bool isVulkanRenderer = wlr_renderer_is_vk(renderer); + + // On the Vulkan renderer, buffers with a CPU representation (shared + // memory client buffers, e.g. XWayland) are copied directly. This avoids + // importing them as staging pixel textures, whose device layout + // (SHADER_READ_ONLY_OPTIMAL after upload) upstream's vulkan_read_pixels() + // does not account for. + if (isVulkanRenderer) { + void *data = nullptr; + uint32_t shmFormat = 0; + size_t shmStride = 0; + if (wlr_buffer_begin_data_ptr_access(buffer, WLR_BUFFER_DATA_PTR_ACCESS_READ, + &data, &shmFormat, &shmStride)) { + QImage::Format mappedFormat = WTools::toImageFormat(shmFormat); + if (mappedFormat != QImage::Format_Invalid) { + QImage wrapped(static_cast(data), + buffer->width, buffer->height, + int(shmStride), mappedFormat); + if (!wrapped.isNull()) { + outputImage = wrapped.copy(); + wlr_buffer_end_data_ptr_access(buffer); + return DumpResult::Success; + } + } + wlr_buffer_end_data_ptr_access(buffer); + } + } + + WUniquePointer texture(wlr_texture_from_buffer(renderer, buffer)); if (!texture) { - qCWarning(lcWlBufferDumper) << "Failed to create texture from buffer"; + 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(); + // On the Vulkan renderer a DMA-BUF import is FOREIGN-owned between + // frames, so reading its pixels requires the wlroots-side acquire gate + // (producer fence wait + queue-family ownership) before touching it, and + // the matching release afterwards. Shared-memory buffers were already + // handled by the CPU copy path above; NOOP covers the remaining cases. + bool readbackAcquired = false; + if (isVulkanRenderer) { + const auto state = waylib_vk_texture_begin_readback(renderer, texture.get()); + switch (state) { + case WLR_VK_TEXTURE_READBACK_ACQUIRED: + readbackAcquired = true; + break; + case WLR_VK_TEXTURE_READBACK_NOOP: + break; + case WLR_VK_TEXTURE_READBACK_ERROR: + qCWarning(lcWlBufferDumper) << "Vulkan texture readback acquire failed" + << "texture" << texture.get() + << "buffer" << buffer; + return DumpResult::ReadbackSyncFailed; + } + } + const auto readbackGuard = qScopeGuard([&texture, renderer, readbackAcquired] { + if (readbackAcquired) + waylib_vk_texture_end_readback(renderer, texture.get()); + }); + wlr_texture_read_pixels_options options = {}; options.data = outputImage.bits(); options.format = format; 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) { @@ -87,6 +155,8 @@ QString WBufferDumper::dumpResultToString(DumpResult result) return "Unsupported pixel format"; case DumpResult::SaveFailed: return "Failed to save image file"; + case DumpResult::ReadbackSyncFailed: + return "Failed to acquire Vulkan texture for readback"; default: return "Unknown error"; } diff --git a/waylib/src/server/utils/wbufferdumper.h b/waylib/src/server/utils/wbufferdumper.h index bc778922eb..f488c3ce91 100644 --- a/waylib/src/server/utils/wbufferdumper.h +++ b/waylib/src/server/utils/wbufferdumper.h @@ -5,6 +5,7 @@ #include #include + #include #include @@ -19,7 +20,8 @@ class WAYLIB_SERVER_EXPORT WBufferDumper TextureCreationFailed, TextureReadFailed, UnsupportedFormat, - SaveFailed + SaveFailed, + ReadbackSyncFailed }; static DumpResult dumpBufferToFile(wlr_buffer *buffer, diff --git a/waylib/src/server/utils/wvulkantrace.cpp b/waylib/src/server/utils/wvulkantrace.cpp new file mode 100644 index 0000000000..eb20474e6b --- /dev/null +++ b/waylib/src/server/utils/wvulkantrace.cpp @@ -0,0 +1,1408 @@ +// Copyright (C) 2026 UnionTech Software Technology Co., Ltd. +// SPDX-License-Identifier: Apache-2.0 OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#include "private/wvulkantrace_p.h" + +#include "wayliblogging.h" +#include "woutputrenderwindow.h" +#include "wscoplistener.h" + +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include +#include +#include +#include +#include +#include + +WAYLIB_SERVER_BEGIN_NAMESPACE + +namespace WVulkanTrace { +namespace { + +constexpr qsizetype MaxTrackedOutputCommits = 32; + +struct BufferRecord { + quint64 sourceBufferId = 0; + quint64 generation = 0; + quint64 releaseSequence = 0; + quint64 lastFrameId = 0; + quint64 lastDrawFrameId = 0; + quint64 lastSampleId = 0; + quintptr lastWindow = 0; + quintptr lastDrawWindow = 0; + quintptr lastSurface = 0; + qint64 pid = 0; + quint64 surfaceSequence = 0; + int width = 0; + int height = 0; + quint32 format = 0; + quint64 modifier = 0; + int planeCount = 0; + QByteArray bufferType = QByteArrayLiteral("other"); + bool signatureLogged = false; + bool qtWrapLogged = false; + bool cleanupLogged = false; + std::array offsets {}; + std::array strides {}; +}; + +struct ClientBufferRecord { + quint64 clientBufferId = 0; + quint64 sourceBufferId = 0; + quintptr sourceBuffer = 0; +}; + +struct ProviderRecord { + quint64 sampleId = 0; + quint64 clientBufferId = 0; + quint64 sourceBufferId = 0; + quintptr sourceBuffer = 0; + quintptr window = 0; + quintptr provider = 0; + quintptr texture = 0; + bool logQtWrap = false; + bool reuseLogged = false; +}; + +struct PassRecord { + quint64 passId = 0; + quintptr renderer = 0; + quintptr targetBuffer = 0; + QByteArray purpose; + int sourceIndex = -1; + qsizetype providerCount = 0; + QSet preparedTextures; + QHash providerDispositions; + int prepareCount = 0; + int prepareFailureCount = 0; + int prepareMetadataMissCount = 0; + int finishCount = 0; + int finishFailureCount = 0; + int finishMetadataMissCount = 0; + int footprintCount = 0; + int footprintMetadataMissCount = 0; + int bindingChangeCount = 0; + int missingProviderCount = 0; + int drawAfterPrepareFailureCount = 0; + int currentTargetFeedbackCount = 0; + int invalidProviderDrawCount = 0; +}; + +struct WindowRecord { + quint64 frameId = 0; + FrameStage stage = FrameStage::Idle; + QElapsedTimer frameTimer; + QElapsedTimer endFrameTimer; + qint64 endFrameUsec = 0; + QVector passStack; + QSet bufferIds; + int passCount = 0; + int prepareCount = 0; + int prepareFailureCount = 0; + int finishCount = 0; + int finishFailureCount = 0; + int footprintCount = 0; + int missingPrepareCount = 0; + int releaseBeforeEndFrameCandidateCount = 0; +}; + +struct OutputCommitRecord { + quint64 frameId = 0; + quint32 committedState = 0; +}; + +QMutex s_mutex; +QHash s_sourceBuffers; +QHash s_clientBuffers; +QHash s_providers; +QHash s_textures; +QHash s_windows; +QHash> s_outputCommits; +QHash> s_outputCommitStates; +std::unordered_map> s_sourceListeners; +std::unordered_map> s_clientListeners; + +std::atomic_bool s_active { false }; +std::atomic s_nextFrameId { 1 }; +std::atomic s_nextPassId { 1 }; +std::atomic s_nextBufferId { 1 }; +std::atomic s_nextSampleId { 1 }; + +QString pointerName(quintptr value) +{ + return QStringLiteral("0x%1").arg(value, QT_POINTER_SIZE * 2, 16, QLatin1Char('0')); +} + +QString hex32(quint32 value) +{ + return QStringLiteral("0x%1").arg(value, 8, 16, QLatin1Char('0')); +} + +QString hex64(quint64 value) +{ + return QStringLiteral("0x%1").arg(value, 16, 16, QLatin1Char('0')); +} + +const char *stageName(FrameStage stage) +{ + switch (stage) { + case FrameStage::Idle: + return "idle"; + case FrameStage::FrameBegin: + return "frame-begin"; + case FrameStage::Polish: + return "polish"; + case FrameStage::BeginFrame: + return "begin-frame"; + case FrameStage::Sync: + return "sync"; + case FrameStage::Recording: + return "recording"; + case FrameStage::AfterRenderingJobs: + return "after-rendering-jobs"; + case FrameStage::ReleaseRenderBuffers: + return "release-render-buffers"; + case FrameStage::BeforeEndFrame: + return "before-end-frame"; + case FrameStage::AfterEndFrame: + return "after-end-frame"; + case FrameStage::OutputCommit: + return "output-commit"; + case FrameStage::RenderEnd: + return "render-end"; + } + return "unknown"; +} + +const char *dispositionName(SampleDisposition disposition) +{ + switch (disposition) { + case SampleDisposition::Prepared: + return "prepared"; + case SampleDisposition::PrepareFailed: + return "prepare-failed"; + case SampleDisposition::CurrentRenderTarget: + return "current-render-target"; + case SampleDisposition::InvalidProvider: + return "invalid-provider"; + case SampleDisposition::DuplicateTexture: + return "duplicate-texture"; + } + return "unknown"; +} + +wlr_buffer *sourceBufferFor(wlr_buffer *clientBuffer, bool createWrapper) +{ + if (!clientBuffer) + return nullptr; + + auto *client = wlr_client_buffer_get(clientBuffer); + if (client) { + if (auto *source = client->source) + return source; + // The producer may already have destroyed its wl_buffer. The locked + // client buffer remains a useful stable identity for lifetime tracing. + return clientBuffer; + } + + Q_UNUSED(createWrapper); + return clientBuffer; +} + +void removeClientBuffer(quintptr key) +{ + std::unique_ptr listeners; + { + QMutexLocker locker(&s_mutex); + s_clientBuffers.remove(key); + auto it = s_clientListeners.find(key); + if (it != s_clientListeners.end()) { + listeners = std::move(it->second); + s_clientListeners.erase(it); + } + } +} + +void sourceReleased(quintptr sourceKey, wlr_buffer *sourceBuffer) +{ + QString message; + bool candidate = false; + { + QMutexLocker locker(&s_mutex); + auto sourceIt = s_sourceBuffers.find(sourceKey); + if (sourceIt == s_sourceBuffers.end()) + return; + + auto &source = sourceIt.value(); + ++source.releaseSequence; + + bool dropped = true; + size_t lockCount = 0; + if (sourceBuffer) { + dropped = sourceBuffer->dropped; + lockCount = sourceBuffer->n_locks; + } + + FrameStage stage = FrameStage::Idle; + auto windowIt = s_windows.find(source.lastDrawWindow); + if (windowIt != s_windows.end()) { + auto &window = windowIt.value(); + stage = window.stage; + candidate = !dropped + && source.lastDrawFrameId != 0 + && source.lastDrawFrameId == window.frameId + && stage != FrameStage::AfterEndFrame + && stage != FrameStage::OutputCommit + && stage != FrameStage::RenderEnd + && stage != FrameStage::Idle; + if (candidate) + ++window.releaseBeforeEndFrameCandidateCount; + } + + message = QStringLiteral("VKTRACE event=source-release-emitted frame=%1 stage=%2 sourceBufferId=%3 sourceBuffer=%4 releaseSeq=%5 sampleId=%6 lastDrawFrame=%7 dropped=%8 locks=%9 releaseBeforeEndFrameCandidate=%10") + .arg(source.lastFrameId) + .arg(QLatin1StringView(stageName(stage))) + .arg(source.sourceBufferId) + .arg(pointerName(sourceKey)) + .arg(source.releaseSequence) + .arg(source.lastSampleId) + .arg(source.lastDrawFrameId) + .arg(dropped) + .arg(lockCount) + .arg(candidate); + } + + if (candidate) + qCWarning(lcWlQtQuickTexture).noquote() << message; + else + qCDebug(lcWlQtQuickTexture).noquote() << message; +} + +void sourceDestroyed(quintptr sourceKey) +{ + QString message; + bool tracked = false; + std::unique_ptr listeners; + { + QMutexLocker locker(&s_mutex); + auto sourceIt = s_sourceBuffers.find(sourceKey); + if (sourceIt != s_sourceBuffers.end()) { + message = QStringLiteral("VKTRACE event=source-destroy sourceBufferId=%1 sourceBuffer=%2 releaseSeq=%3") + .arg(sourceIt->sourceBufferId) + .arg(pointerName(sourceKey)) + .arg(sourceIt->releaseSequence); + s_sourceBuffers.erase(sourceIt); + tracked = true; + } + auto listenerIt = s_sourceListeners.find(sourceKey); + if (listenerIt != s_sourceListeners.end()) { + listeners = std::move(listenerIt->second); + s_sourceListeners.erase(listenerIt); + } + } + if (tracked) + qCDebug(lcWlQtQuickTexture).noquote() << message; +} + +bool registerBuffers(wlr_buffer *clientBuffer, bool createSourceWrapper, + quint64 *clientBufferId, quint64 *sourceBufferId, + quintptr *sourceKey) +{ + if (clientBufferId) + *clientBufferId = 0; + if (sourceBufferId) + *sourceBufferId = 0; + if (sourceKey) + *sourceKey = 0; + if (!clientBuffer) + return false; + + const quintptr clientKey = reinterpret_cast(clientBuffer); + if (!createSourceWrapper) { + QMutexLocker locker(&s_mutex); + const auto client = s_clientBuffers.value(clientKey); + if (!client.clientBufferId || !client.sourceBufferId) + return false; + if (clientBufferId) + *clientBufferId = client.clientBufferId; + if (sourceBufferId) + *sourceBufferId = client.sourceBufferId; + if (sourceKey) + *sourceKey = client.sourceBuffer; + return true; + } + + auto *source = sourceBufferFor(clientBuffer, true); + if (!source) + return false; + + wlr_dmabuf_attributes dmabuf {}; + wlr_shm_attributes shm {}; + const bool isDmabuf = wlr_buffer_get_dmabuf(source, &dmabuf); + const bool isShm = !isDmabuf && wlr_buffer_get_shm(source, &shm); + + const quintptr localSourceKey = reinterpret_cast(source); + bool connectSourceSignals = false; + quint64 localSourceId = 0; + { + QMutexLocker locker(&s_mutex); + auto it = s_sourceBuffers.find(localSourceKey); + if (it == s_sourceBuffers.end()) { + BufferRecord record; + record.sourceBufferId = s_nextBufferId.fetch_add(1, std::memory_order_relaxed); + if (isDmabuf) { + record.bufferType = QByteArrayLiteral("dmabuf"); + record.width = dmabuf.width; + record.height = dmabuf.height; + record.format = dmabuf.format; + record.modifier = dmabuf.modifier; + record.planeCount = dmabuf.n_planes; + for (int i = 0; i < dmabuf.n_planes && i < WLR_DMABUF_MAX_PLANES; ++i) { + record.offsets[i] = dmabuf.offset[i]; + record.strides[i] = dmabuf.stride[i]; + } + } else if (isShm) { + record.bufferType = QByteArrayLiteral("shm"); + record.width = shm.width; + record.height = shm.height; + record.format = shm.format; + record.modifier = DRM_FORMAT_MOD_INVALID; + record.planeCount = 1; + record.offsets[0] = quint32(shm.offset); + record.strides[0] = quint32(shm.stride); + } else { + record.width = source->width; + record.height = source->height; + } + localSourceId = record.sourceBufferId; + s_sourceBuffers.insert(localSourceKey, record); + connectSourceSignals = true; + } else { + localSourceId = it->sourceBufferId; + } + } + + if (connectSourceSignals) { + auto listeners = std::make_unique(); + listeners->add(&source->events.release, + [localSourceKey, source] (void *) { + sourceReleased(localSourceKey, source); + }); + listeners->add(&source->events.destroy, [localSourceKey] (void *) { + sourceDestroyed(localSourceKey); + }); + QMutexLocker locker(&s_mutex); + s_sourceListeners.emplace(localSourceKey, std::move(listeners)); + } + + bool connectClientDestroy = false; + quint64 localClientId = 0; + { + QMutexLocker locker(&s_mutex); + auto it = s_clientBuffers.find(clientKey); + if (it == s_clientBuffers.end()) { + ClientBufferRecord record; + record.clientBufferId = s_nextBufferId.fetch_add(1, std::memory_order_relaxed); + record.sourceBufferId = localSourceId; + record.sourceBuffer = localSourceKey; + localClientId = record.clientBufferId; + s_clientBuffers.insert(clientKey, record); + connectClientDestroy = true; + } else { + localClientId = it->clientBufferId; + it->sourceBufferId = localSourceId; + it->sourceBuffer = localSourceKey; + } + } + if (connectClientDestroy) { + auto listeners = std::make_unique(); + listeners->add(&clientBuffer->events.destroy, [clientKey] (void *) { + removeClientBuffer(clientKey); + }); + QMutexLocker locker(&s_mutex); + s_clientListeners.emplace(clientKey, std::move(listeners)); + } + + if (clientBufferId) + *clientBufferId = localClientId; + if (sourceBufferId) + *sourceBufferId = localSourceId; + if (sourceKey) + *sourceKey = localSourceKey; + return true; +} + +QString formatName(quint32 format) +{ + char *name = drmGetFormatName(format); + if (!name) + return QStringLiteral("unknown"); + const QString result = QString::fromLatin1(name); + free(name); + return result; +} + +QString modifierName(quint64 modifier) +{ + char *name = drmGetFormatModifierName(modifier); + if (!name) + return QStringLiteral("unknown"); + const QString result = QString::fromLatin1(name); + free(name); + return result; +} + +ProviderRecord providerRecordForTexture(quintptr texture) +{ + QMutexLocker locker(&s_mutex); + return s_textures.value(texture); +} + +} // namespace + +void activate(bool vulkanActive) +{ + static const bool requested = qEnvironmentVariableIntValue("WAYLIB_VULKAN_TRACE") != 0; + if (!requested || !vulkanActive) + return; + + bool expected = false; + if (!s_active.compare_exchange_strong(expected, true, std::memory_order_relaxed)) + return; + + qCDebug(lcWlRenderHelper).noquote() + << QStringLiteral("VKTRACE event=trace-start pid=%1") + .arg(QCoreApplication::applicationPid()); +} + +bool enabled() +{ + return s_active.load(std::memory_order_relaxed); +} + +void surfaceCommit(const void *surface, qint64 pid, quint64 surfaceSeq, + quint32 committedState, wlr_buffer *clientBuffer) +{ + if (!enabled() || !(committedState & WLR_SURFACE_STATE_BUFFER)) + return; + + if (!clientBuffer) { + qCDebug(lcWlSurface).noquote() + << QStringLiteral("VKTRACE event=surface-commit surface=%1 pid=%2 surfaceSeq=%3 committed=%4 generation=0 clientBufferId=0 clientBuffer=0x0000000000000000 sourceBufferId=0 sourceBuffer=0x0000000000000000 texture=0x0000000000000000 bufferType=none") + .arg(pointerName(reinterpret_cast(surface))) + .arg(pid) + .arg(surfaceSeq) + .arg(hex32(committedState)); + return; + } + + quint64 clientId = 0; + quint64 sourceId = 0; + quintptr sourceKey = 0; + if (!registerBuffers(clientBuffer, true, &clientId, &sourceId, &sourceKey)) + return; + + quintptr texture = 0; + if (auto *client = wlr_client_buffer_get(clientBuffer)) + texture = reinterpret_cast(client->texture); + + quint64 generation = 0; + QByteArray bufferType; + { + QMutexLocker locker(&s_mutex); + auto &source = s_sourceBuffers[sourceKey]; + generation = ++source.generation; + source.lastSurface = reinterpret_cast(surface); + source.pid = pid; + source.surfaceSequence = surfaceSeq; + bufferType = source.bufferType; + } + + qCDebug(lcWlSurface).noquote() + << QStringLiteral("VKTRACE event=surface-commit surface=%1 pid=%2 surfaceSeq=%3 committed=%4 generation=%5 clientBufferId=%6 clientBuffer=%7 sourceBufferId=%8 sourceBuffer=%9 texture=%10 bufferType=%11") + .arg(pointerName(reinterpret_cast(surface))) + .arg(pid) + .arg(surfaceSeq) + .arg(hex32(committedState)) + .arg(generation) + .arg(clientId) + .arg(pointerName(reinterpret_cast(clientBuffer))) + .arg(sourceId) + .arg(pointerName(sourceKey)) + .arg(pointerName(texture)) + .arg(QString::fromLatin1(bufferType)); +} + +void providerBind(const void *provider, WOutputRenderWindow *window, + wlr_buffer *clientBuffer, wlr_texture *texture) +{ + if (!enabled() || !provider || !window || !clientBuffer || !texture) + return; + + quint64 clientId = 0; + quint64 sourceId = 0; + quintptr sourceKey = 0; + if (!registerBuffers(clientBuffer, false, &clientId, &sourceId, &sourceKey)) + return; + + const auto *ownerClientBuffer = wlr_client_buffer_get(clientBuffer); + const char *ownerStatus = !ownerClientBuffer + ? "provider-owned" + : (ownerClientBuffer->texture == texture + ? "matched" + : "mismatch"); + + ProviderRecord providerRecord; + BufferRecord sourceRecord; + FrameStage stage = FrameStage::Idle; + quint64 frameId = 0; + { + QMutexLocker locker(&s_mutex); + auto sourceIt = s_sourceBuffers.find(sourceKey); + if (sourceIt == s_sourceBuffers.end()) + return; + + providerRecord.sampleId = s_nextSampleId.fetch_add(1, std::memory_order_relaxed); + providerRecord.clientBufferId = clientId; + providerRecord.sourceBufferId = sourceId; + providerRecord.sourceBuffer = sourceKey; + providerRecord.window = reinterpret_cast(window); + providerRecord.provider = reinterpret_cast(provider); + providerRecord.texture = reinterpret_cast(texture); + providerRecord.logQtWrap = true; + sourceIt->qtWrapLogged = true; + sourceIt->signatureLogged = true; + s_providers.insert(providerRecord.provider, providerRecord); + s_textures.insert(providerRecord.texture, providerRecord); + + sourceIt->lastSampleId = providerRecord.sampleId; + sourceIt->lastWindow = providerRecord.window; + sourceRecord = sourceIt.value(); + + auto windowIt = s_windows.find(providerRecord.window); + if (windowIt != s_windows.end()) { + stage = windowIt->stage; + frameId = windowIt->frameId; + windowIt->bufferIds.insert(sourceId); + sourceIt->lastFrameId = frameId; + } + } + + QStringList planes; + for (int i = 0; i < sourceRecord.planeCount && i < WLR_DMABUF_MAX_PLANES; ++i) { + planes.append(QStringLiteral("%1:%2/%3") + .arg(i) + .arg(sourceRecord.offsets[i]) + .arg(sourceRecord.strides[i])); + } + + qCDebug(lcWlQtQuickTexture).noquote() + << QStringLiteral("VKTRACE event=provider-bind action=wrap owner=%1 frame=%2 stage=%3 sampleId=%4 provider=%5 clientBufferId=%6 sourceBufferId=%7 generation=%8 texture=%9 size=%10x%11 bufferType=%12 fourcc=%13 fourccHex=%14 modifier=%15 modifierHex=%16 planeCount=%17 planes=%18") + .arg(QLatin1StringView(ownerStatus)) + .arg(frameId) + .arg(QLatin1StringView(stageName(stage))) + .arg(providerRecord.sampleId) + .arg(pointerName(providerRecord.provider)) + .arg(clientId) + .arg(sourceId) + .arg(sourceRecord.generation) + .arg(pointerName(providerRecord.texture)) + .arg(sourceRecord.width) + .arg(sourceRecord.height) + .arg(QString::fromLatin1(sourceRecord.bufferType)) + .arg(formatName(sourceRecord.format)) + .arg(hex32(sourceRecord.format)) + .arg(modifierName(sourceRecord.modifier)) + .arg(hex64(sourceRecord.modifier)) + .arg(sourceRecord.planeCount) + .arg(planes.join(QLatin1Char(','))); +} + +void providerReuse(const void *provider, WOutputRenderWindow *window, + wlr_buffer *clientBuffer, wlr_texture *texture) +{ + if (!enabled() || !provider || !window || !clientBuffer || !texture) + return; + + const quintptr providerKey = reinterpret_cast(provider); + FrameStage stage = FrameStage::Idle; + quint64 frameId = 0; + ProviderRecord record; + { + QMutexLocker locker(&s_mutex); + auto it = s_providers.find(providerKey); + if (it == s_providers.end() + || it->texture != reinterpret_cast(texture) + || it->reuseLogged) { + return; + } + it->reuseLogged = true; + record = it.value(); + + auto windowIt = s_windows.find(reinterpret_cast(window)); + if (windowIt != s_windows.end()) { + frameId = windowIt->frameId; + stage = windowIt->stage; + } + } + + qCDebug(lcWlQtQuickTexture).noquote() + << QStringLiteral("VKTRACE event=provider-bind action=reuse owner=matched frame=%1 stage=%2 sampleId=%3 provider=%4 clientBufferId=%5 sourceBufferId=%6 texture=%7") + .arg(frameId) + .arg(QLatin1StringView(stageName(stage))) + .arg(record.sampleId) + .arg(pointerName(providerKey)) + .arg(record.clientBufferId) + .arg(record.sourceBufferId) + .arg(pointerName(reinterpret_cast(texture))); +} + +void providerReject(const void *provider, WOutputRenderWindow *window, + wlr_buffer *clientBuffer, wlr_texture *texture, + const char *reason) +{ + if (!enabled() || !provider || !window || !texture) + return; + + FrameStage stage = FrameStage::Idle; + quint64 frameId = 0; + { + QMutexLocker locker(&s_mutex); + auto windowIt = s_windows.find(reinterpret_cast(window)); + if (windowIt != s_windows.end()) { + frameId = windowIt->frameId; + stage = windowIt->stage; + } + } + + qCDebug(lcWlQtQuickTexture).noquote() + << QStringLiteral("VKTRACE event=provider-bind action=reject owner=%1 frame=%2 stage=%3 provider=%4 clientBuffer=%5 texture=%6") + .arg(QLatin1StringView(reason ? reason : "unknown")) + .arg(frameId) + .arg(QLatin1StringView(stageName(stage))) + .arg(pointerName(reinterpret_cast(provider))) + .arg(pointerName(clientBuffer + ? reinterpret_cast(clientBuffer) + : 0)) + .arg(pointerName(reinterpret_cast(texture))); +} + +void providerDiscard(const void *provider, WOutputRenderWindow *window, + wlr_texture *texture, const char *reason) +{ + if (!enabled() || !provider || !window || !texture) + return; + + const quintptr providerKey = reinterpret_cast(provider); + const quintptr textureKey = reinterpret_cast(texture); + ProviderRecord discarded; + FrameStage stage = FrameStage::Idle; + quint64 frameId = 0; + { + QMutexLocker locker(&s_mutex); + auto textureIt = s_textures.find(textureKey); + if (textureIt != s_textures.end() + && textureIt->provider == providerKey) { + discarded = textureIt.value(); + s_textures.erase(textureIt); + } + + auto providerIt = s_providers.find(providerKey); + if (discarded.sampleId + && providerIt != s_providers.end() + && providerIt->sampleId == discarded.sampleId) { + s_providers.erase(providerIt); + + // A transactional replacement can fail after providerBind() has + // made the candidate visible to tracing. Restore the newest older + // sample so later cleanup and draw records still describe the + // texture that the provider actually kept. + ProviderRecord previous; + for (auto it = s_textures.cbegin(); it != s_textures.cend(); ++it) { + if (it->provider == providerKey + && it->sampleId > previous.sampleId) { + previous = it.value(); + } + } + if (previous.sampleId) + s_providers.insert(providerKey, previous); + } + + auto windowIt = s_windows.find(reinterpret_cast(window)); + if (windowIt != s_windows.end()) { + frameId = windowIt->frameId; + stage = windowIt->stage; + } + } + + qCDebug(lcWlQtQuickTexture).noquote() + << QStringLiteral("VKTRACE event=provider-bind action=discard frame=%1 stage=%2 sampleId=%3 provider=%4 clientBufferId=%5 sourceBufferId=%6 texture=%7 reason=%8") + .arg(frameId) + .arg(QLatin1StringView(stageName(stage))) + .arg(discarded.sampleId) + .arg(pointerName(providerKey)) + .arg(discarded.clientBufferId) + .arg(discarded.sourceBufferId) + .arg(pointerName(textureKey)) + .arg(QLatin1StringView(reason ? reason : "unknown")); +} + +CleanupToken cleanupScheduled(const void *provider, WOutputRenderWindow *window, + wlr_buffer *clientBuffer, wlr_texture *texture) +{ + CleanupToken token; + if (!enabled()) + return token; + + const quintptr providerKey = reinterpret_cast(provider); + FrameStage stage = FrameStage::Idle; + { + QMutexLocker locker(&s_mutex); + const auto textureRecord = s_textures.value( + reinterpret_cast(texture)); + const auto providerRecord = textureRecord.provider == providerKey + ? textureRecord + : s_providers.value(providerKey); + token.sampleId = providerRecord.sampleId; + token.clientBufferId = providerRecord.clientBufferId; + token.sourceBufferId = providerRecord.sourceBufferId; + token.sourceBuffer = providerRecord.sourceBuffer; + token.window = reinterpret_cast(window); + token.provider = providerKey; + token.texture = reinterpret_cast(texture); + auto windowIt = s_windows.find(token.window); + if (windowIt != s_windows.end()) { + token.frameId = windowIt->frameId; + stage = windowIt->stage; + } + } + + if (!token.sourceBufferId && clientBuffer) { + QMutexLocker locker(&s_mutex); + const auto client = s_clientBuffers.value(reinterpret_cast(clientBuffer)); + token.clientBufferId = client.clientBufferId; + token.sourceBufferId = client.sourceBufferId; + token.sourceBuffer = client.sourceBuffer; + } + + if (!token.sourceBufferId) + return token; + + { + QMutexLocker locker(&s_mutex); + auto sourceIt = s_sourceBuffers.find(token.sourceBuffer); + if (sourceIt != s_sourceBuffers.end() + && sourceIt->sourceBufferId == token.sourceBufferId + && !sourceIt->cleanupLogged) { + sourceIt->cleanupLogged = true; + token.logLifecycle = true; + } + } + + if (!token.logLifecycle) + return token; + + qCDebug(lcWlQtQuickTexture).noquote() + << QStringLiteral("VKTRACE event=cleanup-scheduled frame=%1 stage=%2 sampleId=%3 provider=%4 clientBufferId=%5 sourceBufferId=%6 texture=%7 cleanupStage=after-rendering-delay") + .arg(token.frameId) + .arg(QLatin1StringView(stageName(stage))) + .arg(token.sampleId) + .arg(pointerName(token.provider)) + .arg(token.clientBufferId) + .arg(token.sourceBufferId) + .arg(pointerName(token.texture)); + return token; +} + +void cleanupRunning(const CleanupToken &token) +{ + if (!enabled() || (!token.sampleId && !token.sourceBufferId)) + return; + + FrameStage stage = FrameStage::Idle; + { + QMutexLocker locker(&s_mutex); + if (s_providers.value(token.provider).sampleId == token.sampleId) + s_providers.remove(token.provider); + if (s_textures.value(token.texture).sampleId == token.sampleId) + s_textures.remove(token.texture); + auto windowIt = s_windows.find(token.window); + if (windowIt != s_windows.end()) + stage = windowIt->stage; + } + if (!token.logLifecycle) + return; + qCDebug(lcWlQtQuickTexture).noquote() + << QStringLiteral("VKTRACE event=cleanup-running frame=%1 stage=%2 sampleId=%3 provider=%4 clientBufferId=%5 sourceBufferId=%6 texture=%7 unlock=imminent") + .arg(token.frameId) + .arg(QLatin1StringView(stageName(stage))) + .arg(token.sampleId) + .arg(pointerName(token.provider)) + .arg(token.clientBufferId) + .arg(token.sourceBufferId) + .arg(pointerName(token.texture)); +} + +void qtWrap(bool begin, wlr_texture *texture, quint64 image, quint32 vkFormat, + int rhiFormat, const char *bridgeClass, const char *layout) +{ + if (!enabled() || !texture) + return; + + const auto provider = providerRecordForTexture(reinterpret_cast(texture)); + if (!provider.sampleId || !provider.logQtWrap) + return; + + quint64 frameId = 0; + FrameStage stage = FrameStage::Idle; + { + QMutexLocker locker(&s_mutex); + auto windowIt = s_windows.find(provider.window); + if (windowIt != s_windows.end()) { + frameId = windowIt->frameId; + stage = windowIt->stage; + } + } + + qCDebug(lcWlQtQuickTexture).noquote() + << QStringLiteral("VKTRACE event=%1 frame=%2 stage=%3 sampleId=%4 sourceBufferId=%5 texture=%6 image=%7 vkFormat=%8 rhiFormat=%9 bridgeClass=%10 layout=%11 result=%12") + .arg(begin ? QStringLiteral("qt-wrap-begin") : QStringLiteral("qt-wrap-end")) + .arg(frameId) + .arg(QLatin1StringView(stageName(stage))) + .arg(provider.sampleId) + .arg(provider.sourceBufferId) + .arg(pointerName(reinterpret_cast(texture))) + .arg(hex64(image)) + .arg(hex32(vkFormat)) + .arg(rhiFormat) + .arg(QLatin1StringView(bridgeClass ? bridgeClass : "unknown")) + .arg(QLatin1StringView(layout ? layout : "unknown")) + .arg(begin ? QStringLiteral("pending") : QStringLiteral("unobservable")); +} + +void beginFrame(WOutputRenderWindow *window) +{ + if (!enabled() || !window) + return; + + { + QMutexLocker locker(&s_mutex); + auto &record = s_windows[reinterpret_cast(window)]; + record = WindowRecord {}; + record.frameId = s_nextFrameId.fetch_add(1, std::memory_order_relaxed); + record.stage = FrameStage::FrameBegin; + record.frameTimer.start(); + } +} + +void setFrameStage(WOutputRenderWindow *window, FrameStage stage) +{ + if (!enabled() || !window) + return; + + { + QMutexLocker locker(&s_mutex); + auto &record = s_windows[reinterpret_cast(window)]; + record.stage = stage; + if (stage == FrameStage::BeforeEndFrame) + record.endFrameTimer.start(); + else if (stage == FrameStage::AfterEndFrame && record.endFrameTimer.isValid()) { + record.endFrameUsec = record.endFrameTimer.nsecsElapsed() / 1000; + } + } +} + +void endFrame(WOutputRenderWindow *window, qsizetype committedOutputCount) +{ + if (!enabled() || !window) + return; + + QString message; + bool unclosedPass = false; + bool relevant = false; + { + QMutexLocker locker(&s_mutex); + auto windowIt = s_windows.find(reinterpret_cast(window)); + if (windowIt == s_windows.end()) + return; + auto &record = windowIt.value(); + unclosedPass = !record.passStack.isEmpty(); + relevant = !record.bufferIds.isEmpty() + || record.missingPrepareCount > 0 + || record.releaseBeforeEndFrameCandidateCount > 0; + auto bufferIds = record.bufferIds.values(); + std::sort(bufferIds.begin(), bufferIds.end()); + QStringList bufferIdNames; + bufferIdNames.reserve(bufferIds.size()); + for (const auto id : std::as_const(bufferIds)) + bufferIdNames.append(QString::number(id)); + message = QStringLiteral("VKTRACE event=frame-summary frame=%1 window=%2 passCount=%3 prepareCount=%4 prepareFailures=%5 finishCount=%6 finishFailures=%7 footprintCount=%8 missingPrepare=%9 releaseBeforeEndFrameCandidates=%10 endFrameUsec=%11 committedOutputs=%12 bufferCount=%13 bufferIds=%14 frameUsec=%15") + .arg(record.frameId) + .arg(pointerName(reinterpret_cast(window))) + .arg(record.passCount) + .arg(record.prepareCount) + .arg(record.prepareFailureCount) + .arg(record.finishCount) + .arg(record.finishFailureCount) + .arg(record.footprintCount) + .arg(record.missingPrepareCount) + .arg(record.releaseBeforeEndFrameCandidateCount) + .arg(record.endFrameUsec) + .arg(committedOutputCount) + .arg(record.bufferIds.size()) + .arg(bufferIdNames.join(QLatin1Char(','))) + .arg(record.frameTimer.isValid() ? record.frameTimer.nsecsElapsed() / 1000 : 0); + record.stage = FrameStage::Idle; + record.passStack.clear(); + } + if (!relevant) + return; + if (unclosedPass) + qCWarning(lcWlRenderHelper).noquote() << message << "unclosedPass=true"; + else + qCDebug(lcWlRenderHelper).noquote() << message; +} + +void windowDestroyed(WOutputRenderWindow *window) +{ + if (!enabled() || !window) + return; + + const quintptr windowKey = reinterpret_cast(window); + QMutexLocker locker(&s_mutex); + s_windows.remove(windowKey); + for (auto it = s_providers.begin(); it != s_providers.end();) { + if (it->window == windowKey) + it = s_providers.erase(it); + else + ++it; + } + for (auto it = s_textures.begin(); it != s_textures.end();) { + if (it->window == windowKey) + it = s_textures.erase(it); + else + ++it; + } +} + +void beginPass(WOutputRenderWindow *window, const void *renderer, + wlr_buffer *targetBuffer, const char *purpose, + int sourceIndex, qsizetype providerCount) +{ + if (!enabled() || !window) + return; + + PassRecord pass; + { + QMutexLocker locker(&s_mutex); + auto &record = s_windows[reinterpret_cast(window)]; + pass.passId = s_nextPassId.fetch_add(1, std::memory_order_relaxed); + pass.renderer = reinterpret_cast(renderer); + pass.targetBuffer = reinterpret_cast(targetBuffer); + pass.purpose = purpose ? purpose : "unknown"; + pass.sourceIndex = sourceIndex; + pass.providerCount = providerCount; + record.passStack.append(pass); + ++record.passCount; + } +} + +void sampleDisposition(WOutputRenderWindow *window, const void *provider, + wlr_texture *texture, SampleDisposition disposition, + qint64 elapsedUsec) +{ + if (!enabled() || !window) + return; + + quint64 frameId = 0; + quint64 passId = 0; + ProviderRecord providerRecord; + { + QMutexLocker locker(&s_mutex); + auto &windowRecord = s_windows[reinterpret_cast(window)]; + if (windowRecord.passStack.isEmpty()) + return; + auto &pass = windowRecord.passStack.last(); + const quintptr providerKey = reinterpret_cast(provider); + const quintptr textureKey = reinterpret_cast(texture); + providerRecord = textureKey ? s_textures.value(textureKey) + : ProviderRecord {}; + if (!providerRecord.sampleId + || (providerKey && providerRecord.provider != providerKey)) { + providerRecord = s_providers.value(providerKey); + } + if (providerKey) + pass.providerDispositions.insert(providerKey, disposition); + if (disposition == SampleDisposition::Prepared) { + if (textureKey) + pass.preparedTextures.insert(textureKey); + } + if (disposition == SampleDisposition::Prepared) { + ++pass.prepareCount; + ++windowRecord.prepareCount; + } else if (disposition == SampleDisposition::PrepareFailed) { + ++pass.prepareFailureCount; + ++windowRecord.prepareFailureCount; + } + if (!providerRecord.sampleId + && (disposition == SampleDisposition::Prepared + || disposition == SampleDisposition::PrepareFailed)) { + ++pass.prepareMetadataMissCount; + } + if (providerRecord.sampleId) { + windowRecord.bufferIds.insert(providerRecord.sourceBufferId); + auto sourceIt = s_sourceBuffers.find(providerRecord.sourceBuffer); + if (sourceIt != s_sourceBuffers.end() + && sourceIt->sourceBufferId == providerRecord.sourceBufferId) { + sourceIt->lastFrameId = windowRecord.frameId; + sourceIt->lastWindow = reinterpret_cast(window); + } + } + frameId = windowRecord.frameId; + passId = pass.passId; + } + + if (disposition != SampleDisposition::PrepareFailed + && !(disposition == SampleDisposition::Prepared && elapsedUsec > 1000)) { + return; + } + + const QString message = QStringLiteral("VKTRACE event=sample-prepare frame=%1 pass=%2 sampleId=%3 sourceBufferId=%4 provider=%5 texture=%6 disposition=%7 elapsedUsec=%8") + .arg(frameId) + .arg(passId) + .arg(providerRecord.sampleId) + .arg(providerRecord.sourceBufferId) + .arg(pointerName(reinterpret_cast(provider))) + .arg(pointerName(reinterpret_cast(texture))) + .arg(QLatin1StringView(dispositionName(disposition))) + .arg(elapsedUsec); + if (disposition == SampleDisposition::PrepareFailed) + qCWarning(lcWlQtQuickTexture).noquote() << message; + else + qCDebug(lcWlQtQuickTexture).noquote() << message; +} + +void sampleDynamicDisposition(WOutputRenderWindow *window, + wlr_texture *texture, + SampleDisposition disposition, + qint64 elapsedUsec) +{ + if (!enabled() || !window || !texture) + return; + + const auto providerRecord = providerRecordForTexture( + reinterpret_cast(texture)); + sampleDisposition(window, + providerRecord.provider + ? reinterpret_cast(providerRecord.provider) + : nullptr, + texture, + disposition, + elapsedUsec); +} + +void sampleFinished(WOutputRenderWindow *window, wlr_texture *texture, + bool ok, qint64 elapsedUsec) +{ + if (!enabled() || !window || !texture) + return; + + quint64 frameId = 0; + quint64 passId = 0; + ProviderRecord providerRecord; + { + QMutexLocker locker(&s_mutex); + auto &windowRecord = s_windows[reinterpret_cast(window)]; + if (windowRecord.passStack.isEmpty()) + return; + auto &pass = windowRecord.passStack.last(); + providerRecord = s_textures.value(reinterpret_cast(texture)); + ++pass.finishCount; + ++windowRecord.finishCount; + if (!providerRecord.sampleId) + ++pass.finishMetadataMissCount; + if (!ok) { + ++pass.finishFailureCount; + ++windowRecord.finishFailureCount; + } + frameId = windowRecord.frameId; + passId = pass.passId; + } + + if (ok && elapsedUsec <= 1000) + return; + + const QString message = QStringLiteral("VKTRACE event=sample-finish frame=%1 pass=%2 sampleId=%3 sourceBufferId=%4 texture=%5 result=%6 elapsedUsec=%7") + .arg(frameId) + .arg(passId) + .arg(providerRecord.sampleId) + .arg(providerRecord.sourceBufferId) + .arg(pointerName(reinterpret_cast(texture))) + .arg(ok) + .arg(elapsedUsec); + if (ok) + qCDebug(lcWlQtQuickTexture).noquote() << message; + else + qCWarning(lcWlQtQuickTexture).noquote() << message; +} + +void surfaceFootprint(WOutputRenderWindow *window, const void *provider, + wlr_texture *texture, const void *surface) +{ + if (!enabled() || !window) + return; + + QMutexLocker locker(&s_mutex); + auto &windowRecord = s_windows[reinterpret_cast(window)]; + if (windowRecord.passStack.isEmpty()) + return; + + auto &pass = windowRecord.passStack.last(); + const quintptr providerKey = reinterpret_cast(provider); + const quintptr textureKey = reinterpret_cast(texture); + + ++pass.footprintCount; + ++windowRecord.footprintCount; + + const auto currentProviderRecord = s_providers.value(providerKey); + auto providerRecord = textureKey ? s_textures.value(textureKey) + : ProviderRecord {}; + if (!providerRecord.sampleId || providerRecord.provider != providerKey) + providerRecord = currentProviderRecord; + + const bool bindingChanged = textureKey + && currentProviderRecord.sampleId + && currentProviderRecord.texture != textureKey; + if (bindingChanged) + ++pass.bindingChangeCount; + + if (!providerRecord.sampleId) { + ++pass.footprintMetadataMissCount; + ++pass.invalidProviderDrawCount; + return; + } + + windowRecord.bufferIds.insert(providerRecord.sourceBufferId); + auto sourceIt = s_sourceBuffers.find(providerRecord.sourceBuffer); + if (sourceIt != s_sourceBuffers.end() + && sourceIt->sourceBufferId == providerRecord.sourceBufferId) { + sourceIt->lastDrawFrameId = windowRecord.frameId; + sourceIt->lastFrameId = windowRecord.frameId; + sourceIt->lastSampleId = providerRecord.sampleId; + sourceIt->lastWindow = reinterpret_cast(window); + sourceIt->lastDrawWindow = reinterpret_cast(window); + sourceIt->lastSurface = reinterpret_cast(surface); + } + + if ((textureKey && pass.preparedTextures.contains(textureKey)) + || (bindingChanged + && pass.preparedTextures.contains(currentProviderRecord.texture))) { + return; + } + + const quintptr dispositionProviderKey = providerRecord.provider + ? providerRecord.provider + : providerKey; + if (!pass.providerDispositions.contains(dispositionProviderKey)) { + if (providerRecord.sampleId) { + ++pass.missingProviderCount; + ++windowRecord.missingPrepareCount; + } else { + ++pass.invalidProviderDrawCount; + } + return; + } + + const auto disposition = pass.providerDispositions.value(dispositionProviderKey); + switch (disposition) { + case SampleDisposition::PrepareFailed: + ++pass.drawAfterPrepareFailureCount; + ++windowRecord.missingPrepareCount; + break; + case SampleDisposition::CurrentRenderTarget: + ++pass.currentTargetFeedbackCount; + ++windowRecord.missingPrepareCount; + break; + case SampleDisposition::InvalidProvider: + ++pass.invalidProviderDrawCount; + break; + case SampleDisposition::Prepared: + case SampleDisposition::DuplicateTexture: + ++pass.missingProviderCount; + ++windowRecord.missingPrepareCount; + break; + } +} + +void endPass(WOutputRenderWindow *window, bool ok) +{ + if (!enabled() || !window) + return; + + QString message; + bool invariantFailure = false; + bool trackedActivity = false; + { + QMutexLocker locker(&s_mutex); + auto &windowRecord = s_windows[reinterpret_cast(window)]; + if (windowRecord.passStack.isEmpty()) + return; + const auto pass = windowRecord.passStack.takeLast(); + trackedActivity = !pass.providerDispositions.isEmpty() + || pass.prepareCount > 0 + || pass.finishCount > 0 + || pass.footprintCount > 0; + // A footprint node is a separately scheduled QSGRenderNode. Its + // execution is useful evidence, but does not prove that the parent + // image geometry sampled the texture. Keep missing-provider reports + // diagnostic-only until an exact QSG draw dependency is available. + invariantFailure = pass.prepareFailureCount > 0 + || pass.drawAfterPrepareFailureCount > 0 + || pass.currentTargetFeedbackCount > 0 + || pass.finishFailureCount > 0 + || pass.prepareCount != pass.finishCount + || !ok; + message = QStringLiteral("VKTRACE event=pass-end frame=%1 pass=%2 result=%3 sourceIndex=%4 providerCount=%5 prepareCount=%6 prepareFailures=%7 prepareMetadataMisses=%8 finishCount=%9 finishFailures=%10 finishMetadataMisses=%11 footprintCount=%12 footprintMetadataMisses=%13 bindingChanges=%14 missingActiveProvider=%15 drawAfterPrepareFailure=%16 currentTargetFeedback=%17 invalidProviderDraw=%18") + .arg(windowRecord.frameId) + .arg(pass.passId) + .arg(ok) + .arg(pass.sourceIndex) + .arg(pass.providerCount) + .arg(pass.prepareCount) + .arg(pass.prepareFailureCount) + .arg(pass.prepareMetadataMissCount) + .arg(pass.finishCount) + .arg(pass.finishFailureCount) + .arg(pass.finishMetadataMissCount) + .arg(pass.footprintCount) + .arg(pass.footprintMetadataMissCount) + .arg(pass.bindingChangeCount) + .arg(pass.missingProviderCount) + .arg(pass.drawAfterPrepareFailureCount) + .arg(pass.currentTargetFeedbackCount) + .arg(pass.invalidProviderDrawCount); + } + if (!trackedActivity) + return; + if (invariantFailure) + qCWarning(lcWlBufferRenderer).noquote() << message; + else + qCDebug(lcWlBufferRenderer).noquote() << message; +} + +void outputCommitState(wlr_output *output, quint32 committedState) +{ + if (!enabled() || !output) + return; + QMutexLocker locker(&s_mutex); + auto &states = s_outputCommitStates[reinterpret_cast(output)]; + states.insert(output->commit_seq, committedState); + while (states.size() > MaxTrackedOutputCommits) + states.erase(states.begin()); +} + +void outputCommitted(WOutputRenderWindow *window, wlr_output *output, + quint32 sequenceBefore, bool ok) +{ + if (!enabled() || !window || !output) + return; + + const quint32 sequenceAfter = output->commit_seq; + const bool sequenceAdvanced = ok && sequenceAfter != sequenceBefore; + quint64 frameId = 0; + quint32 committedState = 0; + bool relevant = false; + { + QMutexLocker locker(&s_mutex); + const auto windowRecord = s_windows.value(reinterpret_cast(window)); + frameId = windowRecord.frameId; + relevant = !windowRecord.bufferIds.isEmpty(); + if (!relevant) + return; + if (sequenceAdvanced) { + committedState = s_outputCommitStates.value(reinterpret_cast(output)) + .value(sequenceAfter); + auto &commits = s_outputCommits[reinterpret_cast(output)]; + commits.insert(sequenceAfter, OutputCommitRecord { frameId, committedState }); + while (commits.size() > MaxTrackedOutputCommits) + commits.erase(commits.begin()); + } + } + + if (ok) + return; + + const QString message = QStringLiteral("VKTRACE event=output-commit frame=%1 output=%2 seqBefore=%3 commitSeq=%4 seqAdvanced=%5 result=%6 stateKnown=%7 committedState=%8 bufferCommitted=%9") + .arg(frameId) + .arg(pointerName(reinterpret_cast(output))) + .arg(sequenceBefore) + .arg(sequenceAfter) + .arg(sequenceAdvanced) + .arg(ok) + .arg(sequenceAdvanced) + .arg(hex32(committedState)) + .arg(bool(committedState & WLR_OUTPUT_STATE_BUFFER)); + qCWarning(lcWlOutputBuffer).noquote() << message; +} + +void outputPresented(wlr_output *output, const wlr_output_event_present *event) +{ + if (!enabled() || !output || !event) + return; + + OutputCommitRecord commit; + bool matched = false; + { + QMutexLocker locker(&s_mutex); + auto outputIt = s_outputCommits.find(reinterpret_cast(output)); + if (outputIt != s_outputCommits.end()) { + auto commitIt = outputIt->find(event->commit_seq); + if (commitIt != outputIt->end()) { + commit = commitIt.value(); + outputIt->erase(commitIt); + matched = true; + } + } + } + + qCDebug(lcWlOutputBuffer).noquote() + << QStringLiteral("VKTRACE event=output-present frame=%1 output=%2 commitSeq=%3 matched=%4 presented=%5 when=%6.%7 refreshNs=%8 flags=%9 bufferCommitted=%10") + .arg(commit.frameId) + .arg(pointerName(reinterpret_cast(output))) + .arg(event->commit_seq) + .arg(matched) + .arg(event->presented) + .arg(event->when.tv_sec) + .arg(event->when.tv_nsec, 9, 10, QLatin1Char('0')) + .arg(event->refresh) + .arg(hex32(event->flags)) + .arg(bool(commit.committedState & WLR_OUTPUT_STATE_BUFFER)); +} + +void outputDestroyed(wlr_output *output) +{ + if (!enabled() || !output) + return; + const quintptr outputKey = reinterpret_cast(output); + QMutexLocker locker(&s_mutex); + s_outputCommits.remove(outputKey); + s_outputCommitStates.remove(outputKey); +} + +} // namespace WVulkanTrace + +WAYLIB_SERVER_END_NAMESPACE diff --git a/waylib/src/server/wayliblogging.cpp b/waylib/src/server/wayliblogging.cpp index 89ebe5de43..31bfc9a386 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 Q_LOGGING_CATEGORY(lcWlRemoteSubsurface, "waylib.protocols.remotesubsurface", QtWarningMsg) // treeland-remote-subsurface-unstable-v1 // Layer shell diff --git a/waylib/src/server/wayliblogging.h b/waylib/src/server/wayliblogging.h index 31d553316c..6d8f2a3ff9 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) Q_DECLARE_LOGGING_CATEGORY(lcWlRemoteSubsurface) // Layer shell