From 603d4222ce15cee65363a773ffed68c8849defd7 Mon Sep 17 00:00:00 2001 From: David Date: Thu, 1 Oct 2026 23:30:03 -0700 Subject: [PATCH 1/2] vk_native: honour the premultiplied flag on window-space layers (#1786) vk_hud_blend stamped every window-space layer with a straight-alpha colour blend (SRC_ALPHA / ONE_MINUS_SRC_ALPHA) and ignored the layer flags, so a layer submitted with premultiplied bytes (SOURCE_ALPHA_BIT clear) came out as rgb*a^2: translucent pixels and antialiased edges too dark. Since #1784 composites the HUD's alpha too, those panels now show over the desktop in a transparent session, darkened. vk_hud_blend now builds a second pipeline in the same instance, identical except for colour src ONE, and vk_hud_blend_draw_no_layout_ex() picks one per draw. Both share the render pass, layouts and the per-image cache, so cache eviction keeps working on the one instance. vk_native picks it with the D3D11/D3D12 window-space rule: premultiplied when SOURCE_ALPHA_BIT is clear, straight when set. Deliberately not comp_layer_blend_mode() (#1599). The alpha blend from #1784 is unchanged. The GL compositor's window-space pass had the same fixed straight blend; it gets the same one-line rule. vk_hud_blend_init(), vk_hud_blend_draw() and vk_hud_blend_draw_no_layout() keep the straight pipeline, so the comp_multi_system.c callers are unchanged. Co-Authored-By: Claude Opus 5.5 --- src/xrt/auxiliary/vk/vk_hud_blend.c | 33 ++++++++++++++++++- src/xrt/auxiliary/vk/vk_hud_blend.h | 27 ++++++++++++++- src/xrt/compositor/gl/comp_gl_compositor.cpp | 6 +++- .../vk_native/comp_vk_native_compositor.c | 12 +++++-- 4 files changed, 72 insertions(+), 6 deletions(-) diff --git a/src/xrt/auxiliary/vk/vk_hud_blend.c b/src/xrt/auxiliary/vk/vk_hud_blend.c index bf5cf71e0..be369f2e9 100644 --- a/src/xrt/auxiliary/vk/vk_hud_blend.c +++ b/src/xrt/auxiliary/vk/vk_hud_blend.c @@ -324,6 +324,16 @@ vk_hud_blend_init_ex(struct vk_hud_blend *blend, return false; } + // #1786: premultiplied variant, same state but colour src ONE. Picked per + // draw by vk_hud_blend_draw_no_layout_ex(); everything else uses the + // straight pipeline above. + blend_att.srcColorBlendFactor = VK_BLEND_FACTOR_ONE; + ret = vk->vkCreateGraphicsPipelines(vk->device, VK_NULL_HANDLE, 1, &pipe_ci, NULL, &blend->pipeline_premul); + if (ret != VK_SUCCESS) { + U_LOG_E("[HUD blend] Failed to create premultiplied pipeline: %d", ret); + return false; + } + blend->initialized = true; return true; } @@ -527,6 +537,23 @@ vk_hud_blend_draw_no_layout(struct vk_hud_blend *blend, int32_t dst_y, uint32_t dst_w, uint32_t dst_h) +{ + vk_hud_blend_draw_no_layout_ex(blend, vk, cmd, fb, fb_w, fb_h, hud_image, dst_x, dst_y, dst_w, dst_h, false); +} + +void +vk_hud_blend_draw_no_layout_ex(struct vk_hud_blend *blend, + struct vk_bundle *vk, + VkCommandBuffer cmd, + VkFramebuffer fb, + uint32_t fb_w, + uint32_t fb_h, + VkImage hud_image, + int32_t dst_x, + int32_t dst_y, + uint32_t dst_w, + uint32_t dst_h, + bool premultiplied) { if (!blend->initialized || hud_image == VK_NULL_HANDLE || fb == VK_NULL_HANDLE || dst_w == 0 || dst_h == 0) { @@ -551,7 +578,8 @@ vk_hud_blend_draw_no_layout(struct vk_hud_blend *blend, VkRect2D scissor = {{dst_x, dst_y}, {dst_w, dst_h}}; vk->vkCmdSetScissor(cmd, 0, 1, &scissor); - vk->vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, blend->pipeline); + vk->vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, + premultiplied ? blend->pipeline_premul : blend->pipeline); vk->vkCmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, blend->pipe_layout, 0, 1, &desc_set, 0, NULL); vk->vkCmdDraw(cmd, 3, 1, 0, 0); @@ -598,6 +626,9 @@ vk_hud_blend_fini(struct vk_hud_blend *blend, struct vk_bundle *vk) if (blend->pipeline != VK_NULL_HANDLE) { vk->vkDestroyPipeline(vk->device, blend->pipeline, NULL); } + if (blend->pipeline_premul != VK_NULL_HANDLE) { + vk->vkDestroyPipeline(vk->device, blend->pipeline_premul, NULL); + } if (blend->render_pass != VK_NULL_HANDLE) { vk->vkDestroyRenderPass(vk->device, blend->render_pass, NULL); } diff --git a/src/xrt/auxiliary/vk/vk_hud_blend.h b/src/xrt/auxiliary/vk/vk_hud_blend.h index 5bd97ac6b..cfcd94b0b 100644 --- a/src/xrt/auxiliary/vk/vk_hud_blend.h +++ b/src/xrt/auxiliary/vk/vk_hud_blend.h @@ -45,7 +45,8 @@ extern "C" { struct vk_hud_blend { VkRenderPass render_pass; - VkPipeline pipeline; + VkPipeline pipeline; //!< Straight-alpha colour blend (src SRC_ALPHA). + VkPipeline pipeline_premul; //!< Premultiplied colour blend (src ONE), #1786. VkPipelineLayout pipe_layout; VkDescriptorSetLayout desc_layout; VkDescriptorPool desc_pool; @@ -138,6 +139,30 @@ vk_hud_blend_draw(struct vk_hud_blend *blend, uint32_t dst_w, uint32_t dst_h); +/*! + * vk_hud_blend_draw_no_layout(), choosing the colour blend per draw (#1786). + * + * @p premultiplied false is the straight-alpha blend every other draw uses + * (colour src SRC_ALPHA, dst ONE_MINUS_SRC_ALPHA). True treats the source as + * premultiplied (colour src ONE, dst ONE_MINUS_SRC_ALPHA). The alpha blend is + * whatever the blend was initialized with, in both cases. + * + * @ingroup aux_vk + */ +void +vk_hud_blend_draw_no_layout_ex(struct vk_hud_blend *blend, + struct vk_bundle *vk, + VkCommandBuffer cmd, + VkFramebuffer fb, + uint32_t fb_w, + uint32_t fb_h, + VkImage hud_image, + int32_t dst_x, + int32_t dst_y, + uint32_t dst_w, + uint32_t dst_h, + bool premultiplied); + /*! * Draw without managing target image layout transitions or framebuffer * caching. Use this when blending into a non-swapchain target (e.g. an diff --git a/src/xrt/compositor/gl/comp_gl_compositor.cpp b/src/xrt/compositor/gl/comp_gl_compositor.cpp index d1e122beb..32b6411ef 100644 --- a/src/xrt/compositor/gl/comp_gl_compositor.cpp +++ b/src/xrt/compositor/gl/comp_gl_compositor.cpp @@ -6244,7 +6244,11 @@ gl_compositor_layer_commit_locked(struct xrt_compositor *xc, xrt_graphics_sync_h glUseProgram(c->program_window_space); glEnable(GL_BLEND); - glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + // #1786: same rule as D3D11/D3D12/vk_native's window-space pass — + // SOURCE_ALPHA_BIT clear means premultiplied bytes, set means + // straight. Deliberately not comp_layer_blend_mode() (#1599). + bool ws_premultiplied = (layer->data.flags & XRT_LAYER_COMPOSITION_BLEND_TEXTURE_SOURCE_ALPHA_BIT) == 0; + glBlendFunc(ws_premultiplied ? GL_ONE : GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); GLint loc_ws_rect = glGetUniformLocation(c->program_window_space, "u_rect"); GLint loc_ws_tex = glGetUniformLocation(c->program_window_space, "u_texture"); diff --git a/src/xrt/compositor/vk_native/comp_vk_native_compositor.c b/src/xrt/compositor/vk_native/comp_vk_native_compositor.c index b31bd481f..3a502d26c 100644 --- a/src/xrt/compositor/vk_native/comp_vk_native_compositor.c +++ b/src/xrt/compositor/vk_native/comp_vk_native_compositor.c @@ -2622,6 +2622,12 @@ vk_compositor_render_window_space_into_atlas(struct comp_vk_native_compositor *c const struct xrt_layer_window_space_data *ws = &layer->data.window_space; uint32_t sc_index = ws->sub.image_index; + // #1786: same rule as D3D11/D3D12's window-space pass — + // SOURCE_ALPHA_BIT clear means premultiplied bytes, set means + // straight. Deliberately not comp_layer_blend_mode() (#1599): see + // the comment in comp_d3d11_renderer.cpp's window-space draw. + bool ws_premultiplied = (layer->data.flags & XRT_LAYER_COMPOSITION_BLEND_TEXTURE_SOURCE_ALPHA_BIT) == 0; + VkImage src_image = (VkImage)(uintptr_t) comp_vk_native_swapchain_get_image(xsc, sc_index); if (src_image == VK_NULL_HANDLE) { @@ -2699,9 +2705,9 @@ vk_compositor_render_window_space_into_atlas(struct comp_vk_native_compositor *c } } - vk_hud_blend_draw_no_layout(&c->window_space_blend, vk, cmd, - c->atlas_ws_fb, atlas_w, atlas_h, - src_image, dx, dy, (uint32_t)dw_i, (uint32_t)dh_i); + vk_hud_blend_draw_no_layout_ex(&c->window_space_blend, vk, cmd, c->atlas_ws_fb, atlas_w, + atlas_h, src_image, dx, dy, (uint32_t)dw_i, (uint32_t)dh_i, + ws_premultiplied); } // Source back to COLOR_ATTACHMENT_OPTIMAL so the app can rerender. From 6b5358225154a5d09f00cb2f1dd96b8a82cac130 Mon Sep 17 00:00:00 2001 From: David Date: Fri, 2 Oct 2026 00:40:02 -0700 Subject: [PATCH 2/2] tests: allow GL's window-space SOURCE_ALPHA_BIT read (#1786) The #1621 guard pins each backend's direct reads of the source-alpha bit. #1786 gives GL's window-space pass the same premul/straight decision D3D11 and D3D12 already have an allowance for (#1581), so GL moves from 0 to 1. Co-Authored-By: Claude Opus 5.5 --- tests/tests_comp_layer_view_camera.cpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tests/tests_comp_layer_view_camera.cpp b/tests/tests_comp_layer_view_camera.cpp index 2d1204105..88c4f004f 100644 --- a/tests/tests_comp_layer_view_camera.cpp +++ b/tests/tests_comp_layer_view_camera.cpp @@ -1134,7 +1134,10 @@ TEST_CASE("comp_layer_blend_mode: no backend reimplements the blend rule (#1621) const char *rel; uint32_t allowed_flag_reads; } backends[] = { - {"gl/comp_gl_compositor.cpp", 0}, + // #1786: GL's one allowance is the SAME Local2D / window-space + // channel D3D11's is (its own premul/straight decision, deliberately + // not comp_layer_blend_mode() -- see comp_d3d11_renderer.cpp). + {"gl/comp_gl_compositor.cpp", 1}, {"metal/comp_metal_compositor.m", 0}, // The Local2D / window-space channel, above. {"d3d11/comp_d3d11_renderer.cpp", 1},