From eac223504ce84e2559a4fc94018a8ebe65eea1c9 Mon Sep 17 00:00:00 2001 From: dfattal Date: Fri, 2 Oct 2026 15:07:28 -0700 Subject: [PATCH 1/3] fix(vk_native): composite window-space layers in linear light, sampled as declared (#1795) vk_hud_blend sampled every window-space source through a hard-coded R8G8B8A8_UNORM view and blended into the encoded atlas, so the layer's declared format was ignored (37 and 43 gave byte-identical atlases) and a B8G8R8A8 source would have been read with R and B swapped. - vk_hud_blend: views cached per (image, format); new vk_hud_blend_draw_no_layout_fmt() samples the source as a given format. Existing callers keep the legacy view. - vk_native: the window-space pass raw-copies the atlas into the private compose target (UNORM + MUTABLE, _SRGB view), blends each layer through that view with the source read as its declared format, and raw-copies back. Two atlas copies, only on frames with window-space layers. Falls back to the encoded path under the legacy hatch or if anything cannot be created. Measured on the win box (windowspace_handle_vk_win, opaque byte 38): _SRGB 38, UNORM 108 (one encode) - now identical to D3D11/D3D12/GL. The cube_handle_vk_win HUD's 50% backdrop lands at (7,7,45), byte-identical to D3D11. The comp_multi (macOS/Linux service) blends are the next step. Co-Authored-By: Claude Opus 5.5 --- .../ADR-044-colour-contract-per-backend.md | 2 +- src/xrt/auxiliary/vk/vk_hud_blend.c | 41 +- src/xrt/auxiliary/vk/vk_hud_blend.h | 34 +- .../vk_native/comp_vk_native_compositor.c | 466 +++++++++++++----- .../vk_native/comp_vk_native_renderer.c | 22 +- .../vk_native/comp_vk_native_renderer.h | 24 + .../vk_native/comp_vk_native_swapchain.c | 9 + .../vk_native/comp_vk_native_swapchain.h | 13 + 8 files changed, 484 insertions(+), 127 deletions(-) diff --git a/docs/adr/ADR-044-colour-contract-per-backend.md b/docs/adr/ADR-044-colour-contract-per-backend.md index 7fe0a67e59..c6eb0ca2f8 100644 --- a/docs/adr/ADR-044-colour-contract-per-backend.md +++ b/docs/adr/ADR-044-colour-contract-per-backend.md @@ -146,7 +146,7 @@ The helper keys on the *target's* format. Pinning a format and calling the helpe - GL `gl_bind_layer_source(…, compose=false)`; - vk_native `get_image_view`, not `get_true_image_view`. - **vk_native window-space layers have the same gap.** `vk_hud_blend` samples every source through a hard-coded `R8G8B8A8_UNORM` view (`auxiliary/vk/vk_hud_blend.c`) and blends into the atlas in encoded space. Measured on the model viewer's displayxr-common button bar: the atlas bytes are identical with swapchain 37 and 43, and the 55 % black fill lands at exactly 0.45 × the background's encoded value. The same hard-coded view would swap R and B for a `B8G8R8A8` window-space swapchain. D3D11, D3D12 and GL window-space *are* honest. Measured with the `windowspace_handle_*_win` probes, which draw opaque solid layers with authored byte 38: on D3D11, D3D12 and GL an `_SRGB` swapchain reads back 38 and a UNORM one reads back 108 (exactly one encode). On vk_native both read back 38. + **vk_native window-space layers had the same gap until #1795.** `vk_hud_blend` sampled every source through a hard-coded `R8G8B8A8_UNORM` view and blended into the atlas in encoded space: the model viewer's displayxr-common button bar gave byte-identical atlases with swapchain 37 and 43, and the hard-coded view would have swapped R and B for a `B8G8R8A8` source. Since #1795 the in-process vk_native pass raw-copies the atlas into the private compose target, blends each layer through its `_SRGB` view with the source sampled as its **declared** format, and raw-copies back (`vk_compositor_render_window_space_linear`). All four Windows backends are now honest. Measured with the `windowspace_handle_*_win` probes, which draw opaque layers authored at byte 38: an `_SRGB` swapchain reads back 38 and a UNORM one reads back 108 (exactly one encode) on D3D11, D3D12, GL and vk_native. The service-side `comp_multi` chrome / overlay / HUD blends (macOS and Linux service) still sample through the legacy view; that is the next step of #1795, and the macOS shell (displayxr-shell-pvt#121) moves to `_SRGB` only together with it. Encoded bytes therefore pass through whether the swapchain says UNORM or `_SRGB`. Neither is washed out today, but a true-linear UNORM Local2D app renders too dark. Local2D is also flattened after the atlas, so §5's atlas capture cannot see it. App guidance does not change: submit Local2D as `_SRGB` with raw-copied bytes (displayxr-unity v2.21.0 already does). **Order matters:** apps move first. Making the flatten honest before they do would wash out every app that still declares UNORM Local2D. - **The displayxr-common Windows window-space HUD swapchain** stays `R8G8B8A8_UNORM` with display-referred bytes. On any format-honest backend it is encoded twice; its move to `_SRGB` is pending (displayxr-common). diff --git a/src/xrt/auxiliary/vk/vk_hud_blend.c b/src/xrt/auxiliary/vk/vk_hud_blend.c index be369f2e9a..0b0ef21fee 100644 --- a/src/xrt/auxiliary/vk/vk_hud_blend.c +++ b/src/xrt/auxiliary/vk/vk_hud_blend.c @@ -346,10 +346,16 @@ vk_hud_blend_init_ex(struct vk_hud_blend *blend, static VkDescriptorSet get_or_create_image_desc(struct vk_hud_blend *blend, struct vk_bundle *vk, - VkImage hud_image) + VkImage hud_image, + VkFormat view_fmt) { + // #1795: the legacy view. Encoded-space callers keep it; honest callers + // pass the swapchain's declared format. + if (view_fmt == VK_FORMAT_UNDEFINED) { + view_fmt = VK_FORMAT_R8G8B8A8_UNORM; + } for (uint32_t i = 0; i < blend->image_count; i++) { - if (blend->cached_images[i].image == hud_image) { + if (blend->cached_images[i].image == hud_image && blend->cached_images[i].format == view_fmt) { return blend->cached_images[i].desc_set; } } @@ -363,7 +369,7 @@ get_or_create_image_desc(struct vk_hud_blend *blend, .sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO, .image = hud_image, .viewType = VK_IMAGE_VIEW_TYPE_2D, - .format = VK_FORMAT_R8G8B8A8_UNORM, + .format = view_fmt, .subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1}, }; VkImageView view = VK_NULL_HANDLE; @@ -401,6 +407,7 @@ get_or_create_image_desc(struct vk_hud_blend *blend, vk->vkUpdateDescriptorSets(vk->device, 1, &write, 0, NULL); blend->cached_images[blend->image_count].image = hud_image; + blend->cached_images[blend->image_count].format = view_fmt; blend->cached_images[blend->image_count].view = view; blend->cached_images[blend->image_count].desc_set = desc_set; blend->image_count++; @@ -429,7 +436,7 @@ vk_hud_blend_draw(struct vk_hud_blend *blend, (void)hud_w; (void)hud_h; - VkDescriptorSet desc_set = get_or_create_image_desc(blend, vk, hud_image); + VkDescriptorSet desc_set = get_or_create_image_desc(blend, vk, hud_image, VK_FORMAT_UNDEFINED); if (desc_set == VK_NULL_HANDLE) { return; } @@ -554,13 +561,32 @@ vk_hud_blend_draw_no_layout_ex(struct vk_hud_blend *blend, uint32_t dst_w, uint32_t dst_h, bool premultiplied) +{ + vk_hud_blend_draw_no_layout_fmt(blend, vk, cmd, fb, fb_w, fb_h, hud_image, VK_FORMAT_UNDEFINED, dst_x, dst_y, + dst_w, dst_h, premultiplied); +} + +void +vk_hud_blend_draw_no_layout_fmt(struct vk_hud_blend *blend, + struct vk_bundle *vk, + VkCommandBuffer cmd, + VkFramebuffer fb, + uint32_t fb_w, + uint32_t fb_h, + VkImage hud_image, + VkFormat src_view_fmt, + 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) { return; } - VkDescriptorSet desc_set = get_or_create_image_desc(blend, vk, hud_image); + VkDescriptorSet desc_set = get_or_create_image_desc(blend, vk, hud_image, src_view_fmt); if (desc_set == VK_NULL_HANDLE) { return; } @@ -594,8 +620,10 @@ vk_hud_blend_forget_image(struct vk_hud_blend *blend, struct vk_bundle *vk, VkIm return; } - for (uint32_t i = 0; i < blend->image_count; i++) { + // #1795: an image may be cached under more than one view format. + for (uint32_t i = 0; i < blend->image_count;) { if (blend->cached_images[i].image != hud_image) { + i++; continue; } vk->vkDestroyImageView(vk->device, blend->cached_images[i].view, NULL); @@ -604,7 +632,6 @@ vk_hud_blend_forget_image(struct vk_hud_blend *blend, struct vk_bundle *vk, VkIm // Order does not matter: move the last entry into the hole. blend->image_count--; blend->cached_images[i] = blend->cached_images[blend->image_count]; - return; } } diff --git a/src/xrt/auxiliary/vk/vk_hud_blend.h b/src/xrt/auxiliary/vk/vk_hud_blend.h index cfcd94b0b4..44bc1f316a 100644 --- a/src/xrt/auxiliary/vk/vk_hud_blend.h +++ b/src/xrt/auxiliary/vk/vk_hud_blend.h @@ -58,6 +58,7 @@ struct vk_hud_blend //! Lookup is O(N) but N is bounded (≤ swapchain image count). struct { VkImage image; + VkFormat format; //!< View format the entry was created with (#1795). VkImageView view; VkDescriptorSet desc_set; } cached_images[VK_HUD_BLEND_MAX_IMAGES]; @@ -163,6 +164,37 @@ vk_hud_blend_draw_no_layout_ex(struct vk_hud_blend *blend, uint32_t dst_h, bool premultiplied); +/*! + * vk_hud_blend_draw_no_layout_ex(), sampling the source through a view of + * @p src_view_fmt instead of the legacy hard-coded R8G8B8A8_UNORM (#1795). + * + * Pass the swapchain's DECLARED format to read it honestly: an `_SRGB` + * source then decodes to linear on sample, a UNORM source reads the linear + * values it holds, and a B8G8R8A8 source is no longer read with R and B + * swapped. Honest sampling only belongs in a target that blends in linear + * and encodes on write (an `_SRGB` attachment); an encoded UNORM target + * keeps the legacy view. VK_FORMAT_UNDEFINED selects the legacy view. + * + * The view must be legal for the image: its own creation format always is; + * a sibling needs VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT. + * + * @ingroup aux_vk + */ +void +vk_hud_blend_draw_no_layout_fmt(struct vk_hud_blend *blend, + struct vk_bundle *vk, + VkCommandBuffer cmd, + VkFramebuffer fb, + uint32_t fb_w, + uint32_t fb_h, + VkImage hud_image, + VkFormat src_view_fmt, + 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 @@ -197,7 +229,7 @@ vk_hud_blend_draw_no_layout(struct vk_hud_blend *blend, uint32_t dst_h); /*! - * Drop the cached view + descriptor set for a HUD source image (#1782). + * Drop the cached view(s) + descriptor set(s) for a HUD source image (#1782). * * The cache is keyed by the raw VkImage handle, and drivers hand a destroyed * image's handle value to the next image they create. An entry that outlives 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 3a502d26c0..d77aa97f0e 100644 --- a/src/xrt/compositor/vk_native/comp_vk_native_compositor.c +++ b/src/xrt/compositor/vk_native/comp_vk_native_compositor.c @@ -1049,6 +1049,21 @@ struct comp_vk_native_compositor //! HUD per-eye disparity / parallax (#210). struct vk_hud_blend window_space_blend; bool window_space_blend_attempted; + /*! + * #1795: the LINEAR window-space path. The layers blend into the + * renderer's private compose target through its `_SRGB` view, sampled + * as their declared format, so a window-space layer obeys the same + * ADR-044 contract as a projection layer. @ref window_space_blend stays + * as the encoded-space fallback (legacy hatch, or no compose target). + * @{ + */ + struct vk_hud_blend window_space_blend_linear; + VkFramebuffer ws_linear_fb; + VkImageView ws_linear_fb_view; + uint32_t ws_linear_fb_w, ws_linear_fb_h; + bool ws_linear_failed; //!< sticky: init failed once, stay on the fallback + bool ws_linear_logged; + //! @} /*! * Swapchain images destroyed since the last window-space pass (#1782). * @@ -2478,9 +2493,14 @@ vk_compositor_forget_dead_window_space_images(struct comp_vk_native_compositor * vk_hud_blend_forget_image(&c->window_space_blend, vk, c->window_space_blend.cached_images[0].image); } + while (c->window_space_blend_linear.image_count > 0) { + vk_hud_blend_forget_image(&c->window_space_blend_linear, vk, + c->window_space_blend_linear.cached_images[0].image); + } } else { for (uint32_t i = 0; i < c->ws_dead.count; i++) { vk_hud_blend_forget_image(&c->window_space_blend, vk, c->ws_dead.images[i]); + vk_hud_blend_forget_image(&c->window_space_blend_linear, vk, c->ws_dead.images[i]); } } c->ws_dead.count = 0; @@ -2488,6 +2508,323 @@ vk_compositor_forget_dead_window_space_images(struct comp_vk_native_compositor * os_mutex_unlock(&c->ws_dead.mutex); } +/*! + * Stamp every window-space layer, per tile, into @p fb (#1795: shared by the + * linear and the encoded-space paths). The target must be in + * COLOR_ATTACHMENT_OPTIMAL; it is left there. Each source is transitioned to + * SHADER_READ_ONLY_OPTIMAL for its draws and handed back to the app. + * + * @p honest selects the source view: true → the swapchain's declared format, + * for a target that blends in linear and encodes on write; false → the legacy + * non-decoding view, for the encoded atlas. + */ +static void +vk_compositor_stamp_window_space_layers(struct comp_vk_native_compositor *c, + VkCommandBuffer cmd, + struct vk_hud_blend *blend, + VkFramebuffer fb, + bool honest, + uint32_t atlas_w, + uint32_t atlas_h, + uint32_t view_w, + uint32_t view_h, + uint32_t tile_columns, + uint32_t tile_rows) +{ + struct vk_bundle *vk = &c->vk; + + for (uint32_t i = 0; i < c->layer_accum.layer_count; i++) { + struct comp_layer *layer = &c->layer_accum.layers[i]; + if (layer->data.type != XRT_LAYER_WINDOW_SPACE) { + continue; + } + struct xrt_swapchain *xsc = layer->sc_array[0]; + if (xsc == NULL) { + continue; + } + + 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) { + continue; + } + // #1795: honest → sample the app's bytes as their declared format + // (decode an _SRGB source, read UNORM as linear, keep BGRA's channel + // order); encoded-space → the legacy R8G8B8A8_UNORM view. + const VkFormat src_fmt = honest ? (VkFormat)comp_vk_native_swapchain_get_declared_format(xsc) + : VK_FORMAT_UNDEFINED; + + // Source: COLOR_ATTACHMENT_OPTIMAL → SHADER_READ_ONLY_OPTIMAL. + VkImageMemoryBarrier src_to_sample = { + .sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER, + .srcAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT, + .dstAccessMask = VK_ACCESS_SHADER_READ_BIT, + .oldLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, + .newLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, + .image = src_image, + .subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, + ws->sub.array_index, 1}, + }; + vk->vkCmdPipelineBarrier(cmd, + VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT, + VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, + 0, 0, NULL, 0, NULL, 1, &src_to_sample); + + // Per-view pass with disparity shift in tile-fraction → atlas px. + // Mirrors the metal/GL compositors (#413). The caller passes the + // frame's EFFECTIVE content grid (#542): mono content arrives as a + // 1×1 grid whose tile spans the full content region, so the old + // hardware-keyed full-region special case is just the grid math. + uint32_t effective_views = tile_columns * tile_rows; + float half_disp = ws->disparity / 2.0f; + for (uint32_t eye = 0; eye < effective_views; eye++) { + uint32_t tile_x = eye % tile_columns; + uint32_t tile_y = eye / tile_columns; + + float tile_origin_x = (float)(tile_x * view_w); + float tile_origin_y = (float)(tile_y * view_h); + float tile_w = (float)view_w; + float tile_h = (float)view_h; + + // Per-view horizontal disparity, graded across the view sweep + // (view index = baseline order, same as the projection views): + // first view = -half, last = +half. Degenerates to the classic + // -/+ pair for 2-view modes and to 0 for a single view. + float eye_shift = 0.0f; + if (effective_views > 1) { + float t = (float)eye / (float)(effective_views - 1); + eye_shift = -half_disp + ws->disparity * t; + } + + int32_t dx = (int32_t)(tile_origin_x + (ws->x + eye_shift) * tile_w); + int32_t dy = (int32_t)(tile_origin_y + ws->y * tile_h); + int32_t dw_i = (int32_t)(ws->width * tile_w); + int32_t dh_i = (int32_t)(ws->height * tile_h); + if (dw_i <= 0 || dh_i <= 0) { + continue; + } + + // One-shot per-geometry diagnostic (#413): logs the resolved + // per-view placement whenever the mode/layout/dims change, so a + // missing-HUD report can be pinned to placement vs crop without + // a custom build. Not per-frame (see debug-logging conventions). + { + static uint32_t logged_key = 0; + uint32_t key = (tile_columns << 28) ^ (tile_rows << 24) ^ + (effective_views << 20) ^ (view_w << 8) ^ view_h ^ + ((uint32_t)c->hardware_display_3d << 31); + if (key != logged_key && eye == 0) { + logged_key = key; + U_LOG_W("[VK native] window-space placement: 3d=%d tiles=%ux%u " + "views=%u view=%ux%u atlas=%ux%u first stamp dst=(%d,%d " + "%dx%d) ws=(%.3f,%.3f %.3fx%.3f disp=%.3f)", + (int)c->hardware_display_3d, tile_columns, tile_rows, + effective_views, view_w, view_h, atlas_w, atlas_h, + dx, dy, dw_i, dh_i, + ws->x, ws->y, ws->width, ws->height, ws->disparity); + } + } + + vk_hud_blend_draw_no_layout_fmt(blend, vk, cmd, fb, atlas_w, atlas_h, src_image, src_fmt, dx, dy, + (uint32_t)dw_i, (uint32_t)dh_i, ws_premultiplied); + } + + // Source back to COLOR_ATTACHMENT_OPTIMAL so the app can rerender. + VkImageMemoryBarrier src_back = { + .sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER, + .srcAccessMask = VK_ACCESS_SHADER_READ_BIT, + .dstAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT, + .oldLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, + .newLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, + .image = src_image, + .subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, + ws->sub.array_index, 1}, + }; + vk->vkCmdPipelineBarrier(cmd, + VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, + VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT, + 0, 0, NULL, 0, NULL, 1, &src_back); + } +} + +//! Record one layout transition for a single-mip, single-layer colour image. +static void +ws_linear_barrier(struct vk_bundle *vk, + VkCommandBuffer cmd, + VkImage image, + VkImageLayout from, + VkImageLayout to, + VkAccessFlags src_access, + VkAccessFlags dst_access, + VkPipelineStageFlags src_stage, + VkPipelineStageFlags dst_stage) +{ + VkImageMemoryBarrier b = { + .sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER, + .srcAccessMask = src_access, + .dstAccessMask = dst_access, + .oldLayout = from, + .newLayout = to, + .srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, + .dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, + .image = image, + .subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1}, + }; + vk->vkCmdPipelineBarrier(cmd, src_stage, dst_stage, 0, 0, NULL, 0, NULL, 1, &b); +} + +/*! + * #1795 - composite the window-space layers in LINEAR light. + * + * The atlas is plain B8G8R8A8_UNORM holding encoded bytes, and it cannot be + * given an `_SRGB` view (the deposit atlas is an imported D3D texture). So, + * the same shape as the D3D11 window-space pass drawing into its compose RTV + * and then publishing to the atlas: + * + * 1. raw-copy the published atlas into the private compose target; + * 2. blend each layer onto it through the target's `_SRGB` view, sampling + * the layer as its DECLARED format (an `_SRGB` source decodes, a UNORM + * source is read as the linear values it holds, BGRA keeps its channel + * order), so the blend happens in linear light and the view encodes + * once on write; + * 3. raw-copy the result back into the atlas. + * + * Both copies are vkCmdCopyImage between two B8G8R8A8_UNORM images: bytes, + * never a conversion. The cost, two atlas copies, is paid only on frames + * that carry window-space layers. + * + * Returns false, recording nothing, when the path is unavailable; the caller + * then takes the encoded-space path, which is what every frame did before. + */ +static bool +vk_compositor_render_window_space_linear(struct comp_vk_native_compositor *c, + VkCommandBuffer cmd, + VkImage atlas_image, + uint32_t atlas_w, + uint32_t atlas_h, + uint32_t view_w, + uint32_t view_h, + uint32_t tile_columns, + uint32_t tile_rows) +{ + struct vk_bundle *vk = &c->vk; + + if (c->ws_linear_failed) { + return false; + } + if ((VkFormat)comp_vk_native_renderer_get_format(c->renderer) != VK_FORMAT_B8G8R8A8_UNORM) { + return false; // a raw copy to/from the compose target would swap channels + } + uint64_t img_u = 0; + uint64_t view_u = 0; + uint32_t cw = 0; + uint32_t ch = 0; + if (!comp_vk_native_renderer_get_linear_compose_target(c->renderer, &img_u, &view_u, &cw, &ch)) { + return false; + } + if (cw != atlas_w || ch != atlas_h) { + return false; // a resize in flight: this frame keeps the old path + } + VkImage comp_image = (VkImage)(uintptr_t)img_u; + VkImageView comp_view = (VkImageView)(uintptr_t)view_u; + + if (!c->window_space_blend_linear.initialized) { + // Alpha written too, as on the encoded path (#1780). + if (!vk_hud_blend_init_ex(&c->window_space_blend_linear, vk, VK_FORMAT_B8G8R8A8_SRGB, true)) { + U_LOG_E("[VK native] linear window-space blend init failed; staying on the encoded path"); + c->ws_linear_failed = true; + return false; + } + } + if (c->ws_linear_fb == VK_NULL_HANDLE || c->ws_linear_fb_view != comp_view || c->ws_linear_fb_w != cw || + c->ws_linear_fb_h != ch) { + // Safe to destroy: the previous frame's submit is waited on before + // this recording begins (same rule as the atlas_ws_fb ring). + if (c->ws_linear_fb != VK_NULL_HANDLE) { + vk->vkDestroyFramebuffer(vk->device, c->ws_linear_fb, NULL); + c->ws_linear_fb = VK_NULL_HANDLE; + } + VkFramebufferCreateInfo fb_ci = { + .sType = VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO, + .renderPass = c->window_space_blend_linear.render_pass, + .attachmentCount = 1, + .pAttachments = &comp_view, + .width = cw, + .height = ch, + .layers = 1, + }; + if (vk->vkCreateFramebuffer(vk->device, &fb_ci, NULL, &c->ws_linear_fb) != VK_SUCCESS) { + U_LOG_E("[VK native] linear window-space framebuffer failed (%ux%u); staying on the encoded path", + cw, ch); + c->ws_linear_fb = VK_NULL_HANDLE; + c->ws_linear_failed = true; + return false; + } + c->ws_linear_fb_view = comp_view; + c->ws_linear_fb_w = cw; + c->ws_linear_fb_h = ch; + } + + const VkImageCopy whole = { + .srcSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1}, + .dstSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1}, + .extent = {cw, ch, 1}, + }; + + // 1. atlas -> compose target (bytes). + ws_linear_barrier(vk, cmd, atlas_image, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, + VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_TRANSFER_WRITE_BIT, + VK_ACCESS_TRANSFER_READ_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, + VK_PIPELINE_STAGE_TRANSFER_BIT); + ws_linear_barrier(vk, cmd, comp_image, VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, + VK_ACCESS_TRANSFER_READ_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT, + VK_ACCESS_TRANSFER_WRITE_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, + VK_PIPELINE_STAGE_TRANSFER_BIT); + vk->vkCmdCopyImage(cmd, atlas_image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, comp_image, + VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, &whole); + + // 2. blend in linear light. + ws_linear_barrier(vk, cmd, comp_image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, + VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, VK_ACCESS_TRANSFER_WRITE_BIT, + VK_ACCESS_COLOR_ATTACHMENT_READ_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT, + VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT); + vk_compositor_stamp_window_space_layers(c, cmd, &c->window_space_blend_linear, c->ws_linear_fb, + /*honest=*/true, atlas_w, atlas_h, view_w, view_h, tile_columns, + tile_rows); + + // 3. compose target -> atlas (bytes), atlas back to the DP's layout. + ws_linear_barrier(vk, cmd, comp_image, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, + VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT, + VK_ACCESS_TRANSFER_READ_BIT, VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT, + VK_PIPELINE_STAGE_TRANSFER_BIT); + ws_linear_barrier(vk, cmd, atlas_image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, + VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_ACCESS_TRANSFER_READ_BIT, + VK_ACCESS_TRANSFER_WRITE_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, + VK_PIPELINE_STAGE_TRANSFER_BIT); + vk->vkCmdCopyImage(cmd, comp_image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, atlas_image, + VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, &whole); + ws_linear_barrier(vk, cmd, atlas_image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, + VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, VK_ACCESS_TRANSFER_WRITE_BIT, + VK_ACCESS_SHADER_READ_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, + VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT); + + if (!c->ws_linear_logged) { + c->ws_linear_logged = true; + U_LOG_W("[VK native] window-space layers composite in linear light, sampled as their declared " + "format (#1795)"); + } + return true; +} + static void vk_compositor_render_window_space_into_atlas(struct comp_vk_native_compositor *c, VkCommandBuffer cmd, @@ -2518,6 +2855,11 @@ vk_compositor_render_window_space_into_atlas(struct comp_vk_native_compositor *c return; } + if (vk_compositor_render_window_space_linear(c, cmd, atlas_image, atlas_w, atlas_h, view_w, view_h, + tile_columns, tile_rows)) { + return; + } + if (!c->window_space_blend.initialized && !c->window_space_blend_attempted) { c->window_space_blend_attempted = true; VkFormat atlas_fmt = (VkFormat)comp_vk_native_renderer_get_format(c->renderer); @@ -2609,123 +2951,8 @@ vk_compositor_render_window_space_into_atlas(struct comp_vk_native_compositor *c VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT, 0, 0, NULL, 0, NULL, 1, &atlas_to_color); - for (uint32_t i = 0; i < c->layer_accum.layer_count; i++) { - struct comp_layer *layer = &c->layer_accum.layers[i]; - if (layer->data.type != XRT_LAYER_WINDOW_SPACE) { - continue; - } - struct xrt_swapchain *xsc = layer->sc_array[0]; - if (xsc == NULL) { - continue; - } - - 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) { - continue; - } - - // Source: COLOR_ATTACHMENT_OPTIMAL → SHADER_READ_ONLY_OPTIMAL. - VkImageMemoryBarrier src_to_sample = { - .sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER, - .srcAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT, - .dstAccessMask = VK_ACCESS_SHADER_READ_BIT, - .oldLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, - .newLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, - .image = src_image, - .subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, - ws->sub.array_index, 1}, - }; - vk->vkCmdPipelineBarrier(cmd, - VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT, - VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, - 0, 0, NULL, 0, NULL, 1, &src_to_sample); - - // Per-view pass with disparity shift in tile-fraction → atlas px. - // Mirrors the metal/GL compositors (#413). The caller passes the - // frame's EFFECTIVE content grid (#542): mono content arrives as a - // 1×1 grid whose tile spans the full content region, so the old - // hardware-keyed full-region special case is just the grid math. - uint32_t effective_views = tile_columns * tile_rows; - float half_disp = ws->disparity / 2.0f; - for (uint32_t eye = 0; eye < effective_views; eye++) { - uint32_t tile_x = eye % tile_columns; - uint32_t tile_y = eye / tile_columns; - - float tile_origin_x = (float)(tile_x * view_w); - float tile_origin_y = (float)(tile_y * view_h); - float tile_w = (float)view_w; - float tile_h = (float)view_h; - - // Per-view horizontal disparity, graded across the view sweep - // (view index = baseline order, same as the projection views): - // first view = -half, last = +half. Degenerates to the classic - // -/+ pair for 2-view modes and to 0 for a single view. - float eye_shift = 0.0f; - if (effective_views > 1) { - float t = (float)eye / (float)(effective_views - 1); - eye_shift = -half_disp + ws->disparity * t; - } - - int32_t dx = (int32_t)(tile_origin_x + (ws->x + eye_shift) * tile_w); - int32_t dy = (int32_t)(tile_origin_y + ws->y * tile_h); - int32_t dw_i = (int32_t)(ws->width * tile_w); - int32_t dh_i = (int32_t)(ws->height * tile_h); - if (dw_i <= 0 || dh_i <= 0) { - continue; - } - - // One-shot per-geometry diagnostic (#413): logs the resolved - // per-view placement whenever the mode/layout/dims change, so a - // missing-HUD report can be pinned to placement vs crop without - // a custom build. Not per-frame (see debug-logging conventions). - { - static uint32_t logged_key = 0; - uint32_t key = (tile_columns << 28) ^ (tile_rows << 24) ^ - (effective_views << 20) ^ (view_w << 8) ^ view_h ^ - ((uint32_t)c->hardware_display_3d << 31); - if (key != logged_key && eye == 0) { - logged_key = key; - U_LOG_W("[VK native] window-space placement: 3d=%d tiles=%ux%u " - "views=%u view=%ux%u atlas=%ux%u first stamp dst=(%d,%d " - "%dx%d) ws=(%.3f,%.3f %.3fx%.3f disp=%.3f)", - (int)c->hardware_display_3d, tile_columns, tile_rows, - effective_views, view_w, view_h, atlas_w, atlas_h, - dx, dy, dw_i, dh_i, - ws->x, ws->y, ws->width, ws->height, ws->disparity); - } - } - - 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. - VkImageMemoryBarrier src_back = { - .sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER, - .srcAccessMask = VK_ACCESS_SHADER_READ_BIT, - .dstAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT, - .oldLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, - .newLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, - .image = src_image, - .subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, - ws->sub.array_index, 1}, - }; - vk->vkCmdPipelineBarrier(cmd, - VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, - VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT, - 0, 0, NULL, 0, NULL, 1, &src_back); - } + vk_compositor_stamp_window_space_layers(c, cmd, &c->window_space_blend, c->atlas_ws_fb, /*honest=*/false, + atlas_w, atlas_h, view_w, view_h, tile_columns, tile_rows); // Atlas: COLOR_ATTACHMENT_OPTIMAL → SHADER_READ_ONLY_OPTIMAL for DP. VkImageMemoryBarrier atlas_back = { @@ -8503,6 +8730,11 @@ vk_compositor_destroy(struct xrt_compositor *xc) } c->atlas_ws_fb = VK_NULL_HANDLE; vk_hud_blend_fini(&c->window_space_blend, vk); + if (c->ws_linear_fb != VK_NULL_HANDLE) { + vk->vkDestroyFramebuffer(vk->device, c->ws_linear_fb, NULL); + c->ws_linear_fb = VK_NULL_HANDLE; + } + vk_hud_blend_fini(&c->window_space_blend_linear, vk); // #439 Phase 3 — masked composite pipelines + scratch images. (The active // zone mask is owned by the oxr handle, freed via zone_mask_destroy.) diff --git a/src/xrt/compositor/vk_native/comp_vk_native_renderer.c b/src/xrt/compositor/vk_native/comp_vk_native_renderer.c index 3febe9d0d5..f761f9c784 100644 --- a/src/xrt/compositor/vk_native/comp_vk_native_renderer.c +++ b/src/xrt/compositor/vk_native/comp_vk_native_renderer.c @@ -1425,7 +1425,10 @@ zone_compose_target_ensure(struct comp_vk_native_renderer *r) .arrayLayers = 1, .samples = VK_SAMPLE_COUNT_1_BIT, .tiling = VK_IMAGE_TILING_OPTIMAL, - .usage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT, + // TRANSFER_DST: the window-space pass copies the published atlas back + // in to blend over it in linear light (#1795). + .usage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT | + VK_IMAGE_USAGE_TRANSFER_DST_BIT, .initialLayout = VK_IMAGE_LAYOUT_UNDEFINED, }; VkResult res = vk->vkCreateImage(vk->device, &ici, NULL, &r->zone.compose_image); @@ -2566,6 +2569,23 @@ comp_vk_native_renderer_get_format(struct comp_vk_native_renderer *r) return (int32_t)r->format; } +bool +comp_vk_native_renderer_get_linear_compose_target(struct comp_vk_native_renderer *r, + uint64_t *out_image, + uint64_t *out_view, + uint32_t *out_width, + uint32_t *out_height) +{ + if (u_color_legacy_unorm_encoded() || !zone_compose_target_ensure(r)) { + return false; + } + *out_image = (uint64_t)(uintptr_t)r->zone.compose_image; + *out_view = (uint64_t)(uintptr_t)r->zone.compose_view; + *out_width = r->zone.compose_w; + *out_height = r->zone.compose_h; + return true; +} + xrt_result_t comp_vk_native_renderer_resize(struct comp_vk_native_renderer *r, uint32_t new_view_width, diff --git a/src/xrt/compositor/vk_native/comp_vk_native_renderer.h b/src/xrt/compositor/vk_native/comp_vk_native_renderer.h index b10d293e0a..7a55e71ac1 100644 --- a/src/xrt/compositor/vk_native/comp_vk_native_renderer.h +++ b/src/xrt/compositor/vk_native/comp_vk_native_renderer.h @@ -171,6 +171,30 @@ comp_vk_native_renderer_get_atlas_dimensions(struct comp_vk_native_renderer *ren int32_t comp_vk_native_renderer_get_format(struct comp_vk_native_renderer *renderer); +/*! + * The private linear-blend compose target (#1610), for a pass that must blend + * in linear light after the projection pass has published the atlas (#1795: + * window-space layers). + * + * The image is B8G8R8A8_UNORM + MUTABLE_FORMAT, sized to the allocated atlas, + * usable as TRANSFER_SRC, TRANSFER_DST and COLOR_ATTACHMENT; @p out_view is + * its `_SRGB` view, so a render pass on it blends in linear and encodes on + * write, and a raw vkCmdCopyImage moves the encoded bytes to and from the + * atlas. Its layout on return is unspecified: the caller transitions from + * UNDEFINED and overwrites it. + * + * Returns false — keep the encoded-space path — under the legacy colour + * hatch (the view would be UNORM) or when the target cannot be created. + * + * @ingroup comp_vk_native + */ +bool +comp_vk_native_renderer_get_linear_compose_target(struct comp_vk_native_renderer *renderer, + uint64_t *out_image, + uint64_t *out_view, + uint32_t *out_width, + uint32_t *out_height); + /*! * Resize the renderer's atlas texture. * diff --git a/src/xrt/compositor/vk_native/comp_vk_native_swapchain.c b/src/xrt/compositor/vk_native/comp_vk_native_swapchain.c index c6b762f755..75246546b9 100644 --- a/src/xrt/compositor/vk_native/comp_vk_native_swapchain.c +++ b/src/xrt/compositor/vk_native/comp_vk_native_swapchain.c @@ -623,6 +623,15 @@ comp_vk_native_swapchain_is_srgb(struct xrt_swapchain *xsc) return f == VK_FORMAT_R8G8B8A8_SRGB || f == VK_FORMAT_B8G8R8A8_SRGB; } +int32_t +comp_vk_native_swapchain_get_declared_format(struct xrt_swapchain *xsc) +{ + if (xsc == NULL) { + return (int32_t)VK_FORMAT_UNDEFINED; + } + return (int32_t)xrt_format_to_vk(vk_sc(xsc)->info.format); +} + uint64_t comp_vk_native_swapchain_get_image(struct xrt_swapchain *xsc, uint32_t index) { diff --git a/src/xrt/compositor/vk_native/comp_vk_native_swapchain.h b/src/xrt/compositor/vk_native/comp_vk_native_swapchain.h index f5c19ef89e..658e71d1fa 100644 --- a/src/xrt/compositor/vk_native/comp_vk_native_swapchain.h +++ b/src/xrt/compositor/vk_native/comp_vk_native_swapchain.h @@ -88,6 +88,19 @@ comp_vk_native_swapchain_get_true_image_view(struct xrt_swapchain *xsc, uint32_t bool comp_vk_native_swapchain_is_srgb(struct xrt_swapchain *xsc); +/*! + * The VkFormat the app REQUESTED for this swapchain (#1795). + * + * A view in this format is always legal on the swapchain's images: either it + * is the image's own format, or the image is the UNORM sibling created + * MUTABLE_FORMAT with both siblings in its format list (#1559/#1572). Use it + * to sample the app's bytes as what they are. + * + * @ingroup comp_vk_native + */ +int32_t +comp_vk_native_swapchain_get_declared_format(struct xrt_swapchain *xsc); + /*! * Get the dimensions of a swapchain. * From 7a15bfabc1acb8d717166de5ec37231d657fc449 Mon Sep 17 00:00:00 2001 From: dfattal Date: Fri, 2 Oct 2026 15:26:09 -0700 Subject: [PATCH 2/3] fix(multi): macOS shared-surface decorations blend in linear light, sampled as declared (#1795) The workspace chrome/cursor/overlay decorations on the macOS shared surface were sampled through vk_hud_blend's hard-coded R8G8B8A8_UNORM view (illegal on a non-mutable _SRGB/BGRA image) and blended into the encoded atlas, so a surface's declared format was ignored. The atlas is now created MUTABLE_FORMAT with its _SRGB sibling; decorations render through that view with each source read as its declared format. Legacy hatch / any creation failure keeps the old path (sticky, one WARN). Also adds the atlas's missing COLOR_ATTACHMENT usage. ORDERING: the macOS shell submits UNORM display-referred bytes today and must move to _SRGB (displayxr-shell-pvt#121) in the same release. Linux registers no workspace surfaces, so it has no such site. Co-Authored-By: Claude Opus 5.5 --- src/xrt/compositor/multi/comp_multi_private.h | 18 + src/xrt/compositor/multi/comp_multi_system.c | 333 ++++++++++++++++-- 2 files changed, 328 insertions(+), 23 deletions(-) diff --git a/src/xrt/compositor/multi/comp_multi_private.h b/src/xrt/compositor/multi/comp_multi_private.h index 2a9ed6b1d7..a91bd68bac 100644 --- a/src/xrt/compositor/multi/comp_multi_private.h +++ b/src/xrt/compositor/multi/comp_multi_private.h @@ -1305,6 +1305,24 @@ struct multi_system_compositor //! shared_atlas_fb (render-pass compatible with shared_chrome_blend). struct comp_multi_content_blend shared_content_blend; bool shared_content_blend_initialized; + + //! #1795 (ADR-044 §7): format-honest decorations. The atlas holds ENCODED + //! bytes (the DP and the content pass see its plain view), so the chrome / + //! overlay / cursor pass renders through an `_SRGB` view of it instead: + //! sources are sampled as their DECLARED format, blend in linear light and + //! are encoded once on write. The atlas is created MUTABLE_FORMAT with that + //! sibling in its view-format list, so the view is legal and no copy is + //! needed. All UNDEFINED / VK_NULL_HANDLE under the legacy hatch. + VkFormat shared_atlas_deco_format; //!< `_SRGB` sibling the atlas is mutable for. + VkImageView shared_atlas_deco_view; //!< `_SRGB` view of the atlas (decoration target). + VkFramebuffer shared_atlas_deco_fb; //!< Framebuffer over shared_atlas_deco_view. + VkImageView shared_atlas_deco_fb_view; //!< The deco view the fb was built for (recreate key). + struct vk_hud_blend shared_deco_blend; //!< Flat decorations into the `_SRGB` view. + bool shared_deco_blend_initialized; + struct comp_multi_content_blend shared_deco_content_blend; //!< Quad / SBS decorations, `_SRGB` view. + bool shared_deco_content_blend_initialized; + bool shared_deco_honest_failed; //!< Sticky: setup failed once, legacy decorations from then on. + bool shared_deco_logged; //!< The one-time regime WARN has been emitted. //! @} #endif }; diff --git a/src/xrt/compositor/multi/comp_multi_system.c b/src/xrt/compositor/multi/comp_multi_system.c index 401663463b..1f7bf0ea74 100644 --- a/src/xrt/compositor/multi/comp_multi_system.c +++ b/src/xrt/compositor/multi/comp_multi_system.c @@ -37,6 +37,7 @@ #include "util/u_debug.h" #include "util/u_trace_marker.h" #include "util/u_distortion_mesh.h" +#include "util/u_color_encoding.h" #ifdef XRT_OS_LINUX #include "util/u_linux.h" @@ -2269,6 +2270,13 @@ session_render_hud_overlay(struct multi_compositor *mc, * cursor all blend RGBA over the same target format, so they reuse one * @ref vk_hud_blend instance (the field is named chrome_blend for historical * reasons; it backs all three). (#48) + * + * #1795: deliberately NOT made format-honest. The workspace registry these + * layers come from is only filled on macOS (ipc_server_handler.c registers + * chrome / cursor / overlays under XRT_OS_MACOS alone), and this per-session + * path is compiled out on macOS, so no controller surface reaches it today. + * The format-honest decoration blend lives on the macOS shared surface + * (shared_resolve_deco_target). */ static bool ensure_workspace_blend(struct multi_compositor *mc, struct vk_bundle *vk) @@ -3914,6 +3922,20 @@ shared_surface_fini(struct multi_system_compositor *msc) vk_hud_blend_fini(&msc->shared_chrome_blend, vk); msc->shared_chrome_blend_initialized = false; } + // #1795 honest-decoration target: same order (framebuffer before pipelines). + if (vk != NULL && msc->shared_atlas_deco_fb != VK_NULL_HANDLE) { + vk->vkDestroyFramebuffer(vk->device, msc->shared_atlas_deco_fb, NULL); + msc->shared_atlas_deco_fb = VK_NULL_HANDLE; + msc->shared_atlas_deco_fb_view = VK_NULL_HANDLE; + } + if (vk != NULL && msc->shared_deco_blend_initialized) { + vk_hud_blend_fini(&msc->shared_deco_blend, vk); + msc->shared_deco_blend_initialized = false; + } + if (vk != NULL && msc->shared_deco_content_blend_initialized) { + comp_multi_content_blend_fini(&msc->shared_deco_content_blend, vk); + msc->shared_deco_content_blend_initialized = false; + } // Task 9 content composite: owns its own render pass + pipeline (no fb). if (vk != NULL && msc->shared_content_blend_initialized) { comp_multi_content_blend_fini(&msc->shared_content_blend, vk); @@ -3922,6 +3944,10 @@ shared_surface_fini(struct multi_system_compositor *msc) // The one combined stereo atlas (view + image + memory). if (vk != NULL && msc->shared_atlas_initialized) { + if (msc->shared_atlas_deco_view != VK_NULL_HANDLE) + vk->vkDestroyImageView(vk->device, msc->shared_atlas_deco_view, NULL); + msc->shared_atlas_deco_view = VK_NULL_HANDLE; + msc->shared_atlas_deco_format = VK_FORMAT_UNDEFINED; if (msc->shared_atlas_view != VK_NULL_HANDLE) vk->vkDestroyImageView(vk->device, msc->shared_atlas_view, NULL); if (msc->shared_atlas_image != VK_NULL_HANDLE) @@ -3976,6 +4002,107 @@ shared_surface_fini(struct multi_system_compositor *msc) msc->shared_surface_initialized = false; } +//! #1795: the `_SRGB` sibling of an 8-bit colour format (itself if already +//! `_SRGB`), VK_FORMAT_UNDEFINED if it has none. +static VkFormat +shared_srgb_sibling(VkFormat f) +{ + switch (f) { + case VK_FORMAT_R8G8B8A8_UNORM: + case VK_FORMAT_R8G8B8A8_SRGB: return VK_FORMAT_R8G8B8A8_SRGB; + case VK_FORMAT_B8G8R8A8_UNORM: + case VK_FORMAT_B8G8R8A8_SRGB: return VK_FORMAT_B8G8R8A8_SRGB; + default: return VK_FORMAT_UNDEFINED; + } +} + +static inline bool +shared_format_is_srgb(VkFormat f) +{ + return f == VK_FORMAT_R8G8B8A8_SRGB || f == VK_FORMAT_B8G8R8A8_SRGB; +} + +/*! + * #1795: the `_SRGB` view format the decoration pass should render the atlas + * through, or VK_FORMAT_UNDEFINED when the atlas needs no separate view (it is + * `_SRGB` already) or must stay legacy (the ADR-044 hatch, an earlier failure, + * a format with no `_SRGB` sibling, or a sibling that cannot be a blended + * colour attachment on this device). + */ +static VkFormat +shared_honest_deco_format(struct multi_system_compositor *msc, struct vk_bundle *vk, VkFormat atlas_fmt) +{ + if (u_color_legacy_unorm_encoded() || msc->shared_deco_honest_failed || shared_format_is_srgb(atlas_fmt)) { + return VK_FORMAT_UNDEFINED; + } + VkFormat srgb = shared_srgb_sibling(atlas_fmt); + if (srgb == VK_FORMAT_UNDEFINED) { + return VK_FORMAT_UNDEFINED; + } + VkFormatProperties props = {0}; + vk->vkGetPhysicalDeviceFormatProperties(vk->physical_device, srgb, &props); + const VkFormatFeatureFlags need = + VK_FORMAT_FEATURE_COLOR_ATTACHMENT_BIT | VK_FORMAT_FEATURE_COLOR_ATTACHMENT_BLEND_BIT; + if ((props.optimalTilingFeatures & need) != need) { + return VK_FORMAT_UNDEFINED; + } + return srgb; +} + +/*! + * #1795: create the atlas image in @p format, MUTABLE_FORMAT with + * {@p format, @p srgb} as its view-format list, so an `_SRGB` view of the + * encoded atlas is legal. Cleans up after itself on failure. + */ +static VkResult +shared_create_mutable_atlas(struct vk_bundle *vk, + VkExtent2D extent, + VkFormat format, + VkFormat srgb, + VkImageUsageFlags usage, + VkDeviceMemory *out_mem, + VkImage *out_image) +{ + VkFormat view_formats[2] = {format, srgb}; + VkImageFormatListCreateInfoKHR fmt_list = { + .sType = VK_STRUCTURE_TYPE_IMAGE_FORMAT_LIST_CREATE_INFO_KHR, + .viewFormatCount = 2, + .pViewFormats = view_formats, + }; + VkImageCreateInfo ici = { + .sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO, + // The list is an optimisation hint; MUTABLE_FORMAT alone makes the view legal. + .pNext = vk->has_KHR_image_format_list ? &fmt_list : NULL, + .flags = VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT, + .imageType = VK_IMAGE_TYPE_2D, + .format = format, + .extent = {extent.width, extent.height, 1}, + .mipLevels = 1, + .arrayLayers = 1, + .samples = VK_SAMPLE_COUNT_1_BIT, + .tiling = VK_IMAGE_TILING_OPTIMAL, + .usage = usage, + .sharingMode = VK_SHARING_MODE_EXCLUSIVE, + .initialLayout = VK_IMAGE_LAYOUT_UNDEFINED, + }; + VkImage image = VK_NULL_HANDLE; + VkResult ret = vk->vkCreateImage(vk->device, &ici, NULL, &image); + if (ret != VK_SUCCESS) { + return ret; + } + VkMemoryRequirements reqs = {0}; + vk->vkGetImageMemoryRequirements(vk->device, image, &reqs); + VkDeviceMemory mem = VK_NULL_HANDLE; + ret = vk_alloc_and_bind_image_memory(vk, image, &reqs, NULL, __func__, &mem); + if (ret != VK_SUCCESS) { + vk->vkDestroyImage(vk->device, image, NULL); + return ret; + } + *out_mem = mem; + *out_image = image; + return VK_SUCCESS; +} + /*! * Ensure the one combined stereo atlas exists at (tile_columns*eye_w) × eye_h. */ @@ -3994,6 +4121,12 @@ shared_ensure_atlas(struct multi_system_compositor *msc, return true; } if (msc->shared_atlas_initialized) { + // The #1795 deco framebuffer is rebuilt lazily, keyed on the deco view + // (the same pattern as shared_atlas_fb). + if (msc->shared_atlas_deco_view != VK_NULL_HANDLE) + vk->vkDestroyImageView(vk->device, msc->shared_atlas_deco_view, NULL); + msc->shared_atlas_deco_view = VK_NULL_HANDLE; + msc->shared_atlas_deco_format = VK_FORMAT_UNDEFINED; if (msc->shared_atlas_view != VK_NULL_HANDLE) vk->vkDestroyImageView(vk->device, msc->shared_atlas_view, NULL); if (msc->shared_atlas_image != VK_NULL_HANDLE) @@ -4004,9 +4137,29 @@ shared_ensure_atlas(struct multi_system_compositor *msc, } VkExtent2D extent = {(uint32_t)atlas_w, (uint32_t)atlas_h}; - VkImageUsageFlags usage = VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_SAMPLED_BIT; - VkResult ret = - vk_create_image_simple(vk, extent, format, usage, &msc->shared_atlas_memory, &msc->shared_atlas_image); + // COLOR_ATTACHMENT: the content and decoration passes render into the atlas + // through a framebuffer, which the usage never declared. + VkImageUsageFlags usage = + VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT; + + // #1795: an encoded (non-`_SRGB`) atlas is created MUTABLE with its `_SRGB` + // sibling so the decoration pass can blend in linear through that view. + VkFormat deco_fmt = shared_honest_deco_format(msc, vk, format); + VkResult ret = VK_ERROR_INITIALIZATION_FAILED; + if (deco_fmt != VK_FORMAT_UNDEFINED) { + ret = shared_create_mutable_atlas(vk, extent, format, deco_fmt, usage, &msc->shared_atlas_memory, + &msc->shared_atlas_image); + if (ret != VK_SUCCESS) { + U_LOG_W("[#59] #1795: mutable atlas failed (%s); decorations stay legacy (encoded-space blend)", + vk_result_string(ret)); + msc->shared_deco_honest_failed = true; + deco_fmt = VK_FORMAT_UNDEFINED; + } + } + if (ret != VK_SUCCESS) { + ret = vk_create_image_simple(vk, extent, format, usage, &msc->shared_atlas_memory, + &msc->shared_atlas_image); + } if (ret != VK_SUCCESS) { U_LOG_E("[#59] failed to create combined atlas: %s", vk_result_string(ret)); return false; @@ -4017,6 +4170,18 @@ shared_ensure_atlas(struct multi_system_compositor *msc, U_LOG_E("[#59] failed to create combined atlas view: %s", vk_result_string(ret)); return false; } + if (deco_fmt != VK_FORMAT_UNDEFINED) { + ret = vk_create_view(vk, msc->shared_atlas_image, VK_IMAGE_VIEW_TYPE_2D, deco_fmt, range, + &msc->shared_atlas_deco_view); + if (ret != VK_SUCCESS) { + U_LOG_W("[#59] #1795: `_SRGB` atlas view failed (%s); decorations stay legacy", + vk_result_string(ret)); + msc->shared_atlas_deco_view = VK_NULL_HANDLE; + msc->shared_deco_honest_failed = true; + deco_fmt = VK_FORMAT_UNDEFINED; + } + } + msc->shared_atlas_deco_format = deco_fmt; msc->shared_atlas_w = atlas_w; msc->shared_atlas_h = atlas_h; msc->shared_eye_w = eye_w; @@ -4280,17 +4445,132 @@ shared_ensure_content_blend(struct multi_system_compositor *msc, struct vk_bundl return true; } +/*! + * Where this frame's decorations (chrome / overlays / cursor) are drawn, and + * how their sources are read (#1795, ADR-044 §1). + */ +struct shared_deco_target +{ + struct vk_hud_blend *blend; //!< Flat (axis-aligned) decorations. + struct comp_multi_content_blend *content; //!< Quad / stereo-SBS decorations. + VkFramebuffer fb; //!< Shared by both pipelines (render-pass compatible). + //! Sources are sampled as their DECLARED format and blended in linear + //! light into an `_SRGB` attachment. False = legacy: the flat path reads + //! raw bytes through R8G8B8A8_UNORM and blends in the atlas's encoded space. + bool honest; +}; + +//! #1795: one-time WARN naming the regime the decorations run under. +static void +shared_deco_log_once(struct multi_system_compositor *msc, const char *regime) +{ + if (msc->shared_deco_logged) { + return; + } + msc->shared_deco_logged = true; + U_LOG_W("[#59] #1795 workspace decorations: %s (atlas fmt=%d, deco view fmt=%d)", regime, + msc->shared_atlas_format, msc->shared_atlas_deco_format); +} + +/*! + * #1795: resolve the decoration target. Always succeeds when the legacy target + * (shared_chrome_blend over shared_atlas_fb) can be built; the honest target is + * used when the hatch is off and everything it needs exists. A failure while + * building the honest target is sticky and falls back to legacy for good, so + * there is one WARN, not one per frame. + */ +static bool +shared_resolve_deco_target(struct multi_system_compositor *msc, struct vk_bundle *vk, struct shared_deco_target *out) +{ + if (!shared_ensure_chrome_blend(msc, vk)) { + return false; + } + out->blend = &msc->shared_chrome_blend; + // The quad pipeline is optional, as before: without it only quad / SBS + // decorations are skipped. + out->content = shared_ensure_content_blend(msc, vk) ? &msc->shared_content_blend : NULL; + out->fb = msc->shared_atlas_fb; + out->honest = false; + + if (u_color_legacy_unorm_encoded()) { + shared_deco_log_once(msc, "LEGACY (DXR_COLOR_LEGACY_UNORM_ENCODED=1): encoded-space blend"); + return true; + } + if (shared_format_is_srgb(msc->shared_atlas_format)) { + // The atlas attachment encodes on write already: sampling honestly is all. + shared_deco_log_once(msc, "format-honest (atlas is _SRGB)"); + out->honest = true; + return true; + } + if (msc->shared_deco_honest_failed || msc->shared_atlas_deco_view == VK_NULL_HANDLE) { + shared_deco_log_once(msc, "LEGACY (no _SRGB view of the atlas): encoded-space blend"); + return true; + } + + VkFormat deco_fmt = msc->shared_atlas_deco_format; + if (!msc->shared_deco_blend_initialized) { + if (!vk_hud_blend_init(&msc->shared_deco_blend, vk, deco_fmt)) { + U_LOG_W("[#59] #1795: _SRGB decoration blend init failed; decorations stay legacy"); + msc->shared_deco_honest_failed = true; + return true; + } + msc->shared_deco_blend_initialized = true; + } + if (!msc->shared_deco_content_blend_initialized) { + if (!comp_multi_content_blend_init(&msc->shared_deco_content_blend, vk, deco_fmt)) { + U_LOG_W("[#59] #1795: _SRGB decoration quad blend init failed; decorations stay legacy"); + msc->shared_deco_honest_failed = true; + return true; + } + msc->shared_deco_content_blend_initialized = true; + } + if (msc->shared_atlas_deco_fb == VK_NULL_HANDLE || + msc->shared_atlas_deco_fb_view != msc->shared_atlas_deco_view) { + if (msc->shared_atlas_deco_fb != VK_NULL_HANDLE) { + vk->vkDestroyFramebuffer(vk->device, msc->shared_atlas_deco_fb, NULL); + msc->shared_atlas_deco_fb = VK_NULL_HANDLE; + } + VkFramebufferCreateInfo fb_info = { + .sType = VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO, + .renderPass = msc->shared_deco_blend.render_pass, + .attachmentCount = 1, + .pAttachments = &msc->shared_atlas_deco_view, + .width = (uint32_t)msc->shared_atlas_w, + .height = (uint32_t)msc->shared_atlas_h, + .layers = 1, + }; + if (vk->vkCreateFramebuffer(vk->device, &fb_info, NULL, &msc->shared_atlas_deco_fb) != VK_SUCCESS) { + U_LOG_W("[#59] #1795: _SRGB decoration framebuffer failed; decorations stay legacy"); + msc->shared_atlas_deco_fb = VK_NULL_HANDLE; + msc->shared_deco_honest_failed = true; + return true; + } + msc->shared_atlas_deco_fb_view = msc->shared_atlas_deco_view; + } + + shared_deco_log_once(msc, "format-honest (sources as declared, linear blend through an _SRGB atlas view)"); + out->blend = &msc->shared_deco_blend; + out->content = &msc->shared_deco_content_blend; + out->fb = msc->shared_atlas_deco_fb; + out->honest = true; + return true; +} + /*! * Alpha-blend one decoration source image into the combined atlas (atlas must be * COLOR_ATTACHMENT_OPTIMAL on entry/exit). The dst rect is clamped to [clip_x0, * clip_x1) × [clip_y0, clip_y1) — the eye's tile — so a decoration can't bleed * into the other eye or off the atlas. The cross-process source rests in GENERAL. + * @p src_fmt is the source swapchain's declared format; it is read as that + * format when @p t is honest (#1795) and ignored on the legacy target. */ static void shared_blend_into_atlas(struct multi_system_compositor *msc, struct vk_bundle *vk, VkCommandBuffer cmd, + const struct shared_deco_target *t, VkImage src_image, + VkFormat src_fmt, int dst_x, int dst_y, int dst_w, @@ -4326,9 +4606,12 @@ shared_blend_into_atlas(struct multi_system_compositor *msc, vk->vkCmdPipelineBarrier(cmd, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, 0, 0, NULL, 0, NULL, 1, &to_src); - vk_hud_blend_draw_no_layout(&msc->shared_chrome_blend, vk, cmd, msc->shared_atlas_fb, - (uint32_t)msc->shared_atlas_w, (uint32_t)msc->shared_atlas_h, src_image, x0, y0, - (uint32_t)(x1 - x0), (uint32_t)(y1 - y0)); + // #1795: VK_FORMAT_UNDEFINED keeps vk_hud_blend's legacy R8G8B8A8_UNORM view. + // The declared format is always a legal view (the image's own format). + vk_hud_blend_draw_no_layout_fmt(t->blend, vk, cmd, t->fb, (uint32_t)msc->shared_atlas_w, + (uint32_t)msc->shared_atlas_h, src_image, + t->honest ? src_fmt : VK_FORMAT_UNDEFINED, x0, y0, (uint32_t)(x1 - x0), + (uint32_t)(y1 - y0), false); VkImageMemoryBarrier to_general = { .sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER, @@ -4363,7 +4646,10 @@ shared_composite_decorations(struct multi_system_compositor *msc, float px_per_m_y, uint32_t tile_columns) { - if (!shared_ensure_chrome_blend(msc, vk)) { + // #1795: legacy or format-honest target, resolved once per frame so every + // decoration blends in the same space. + struct shared_deco_target t; + if (!shared_resolve_deco_target(msc, vk, &t)) { return; } @@ -4417,7 +4703,7 @@ shared_composite_decorations(struct multi_system_compositor *msc, float ccz = e->pose_z + woff.z; // One render pass over both eyes (barriers must wrap, not nest, the pass). - if (shared_ensure_content_blend(msc, vk)) { + if (t.content != NULL) { VkImageMemoryBarrier to_read = { .sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER, .srcAccessMask = 0, @@ -4430,8 +4716,7 @@ shared_composite_decorations(struct multi_system_compositor *msc, vk->vkCmdPipelineBarrier(cmd, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, 0, 0, NULL, 0, NULL, 1, &to_read); - comp_multi_content_blend_begin(&msc->shared_content_blend, vk, cmd, - msc->shared_atlas_fb, (uint32_t)msc->shared_atlas_w, + comp_multi_content_blend_begin(t.content, vk, cmd, t.fb, (uint32_t)msc->shared_atlas_w, (uint32_t)msc->shared_atlas_h); for (uint32_t eye = 0; eye < tile_columns; eye++) { uint32_t ei = (eye < eye_pos->count) ? eye : (eye_pos->count - 1); @@ -4453,14 +4738,14 @@ shared_composite_decorations(struct multi_system_compositor *msc, .use_src_alpha = 1.0f, }; memcpy(pcq.corners, corners, sizeof(corners)); - comp_multi_content_blend_draw_quad(&msc->shared_content_blend, vk, cmd, + comp_multi_content_blend_draw_quad(t.content, vk, cmd, chrome_img, 0, sc->vkic.info.format, VK_NULL_HANDLE, VK_FORMAT_UNDEFINED, &pcq, tile_x0, 0, (uint32_t)eye_w, (uint32_t)eye_h, (uint32_t)msc->shared_atlas_w, (uint32_t)msc->shared_atlas_h); } - comp_multi_content_blend_end(&msc->shared_content_blend, vk, cmd); + comp_multi_content_blend_end(t.content, vk, cmd); VkImageMemoryBarrier to_general = { .sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER, .srcAccessMask = VK_ACCESS_SHADER_READ_BIT, @@ -4488,8 +4773,8 @@ shared_composite_decorations(struct multi_system_compositor *msc, eye_pos->eyes[ei].x, eye_pos->eyes[ei].y, eye_pos->eyes[ei].z, eye_w, eye_h, px_per_m_x, px_per_m_y, &rx, &ry, &rw, &rh); int tile_x0 = (int)eye * eye_w; - shared_blend_into_atlas(msc, vk, cmd, chrome_img, tile_x0 + rx, ry, rw, rh, tile_x0, - tile_x0 + eye_w, 0, eye_h); + shared_blend_into_atlas(msc, vk, cmd, &t, chrome_img, (VkFormat)sc->vkic.info.format, + tile_x0 + rx, ry, rw, rh, tile_x0, tile_x0 + eye_w, 0, eye_h); } } @@ -4534,7 +4819,7 @@ shared_composite_decorations(struct multi_system_compositor *msc, // src_uv + use_src_alpha) as a flat (W = 1) axis-aligned quad — // the chrome-pill treatment, minus the tilt. Mirror of the D3D11 // service stereo_sbs src-rect split (comp_d3d11_service.cpp). - if (!shared_ensure_content_blend(msc, vk)) { + if (t.content == NULL) { continue; } VkImageMemoryBarrier ov_to_read = { @@ -4549,8 +4834,8 @@ shared_composite_decorations(struct multi_system_compositor *msc, vk->vkCmdPipelineBarrier(cmd, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, 0, 0, NULL, 0, NULL, 1, &ov_to_read); - comp_multi_content_blend_begin(&msc->shared_content_blend, vk, cmd, msc->shared_atlas_fb, - (uint32_t)msc->shared_atlas_w, (uint32_t)msc->shared_atlas_h); + comp_multi_content_blend_begin(t.content, vk, cmd, t.fb, (uint32_t)msc->shared_atlas_w, + (uint32_t)msc->shared_atlas_h); for (uint32_t eye = 0; eye < tile_columns; eye++) { int tile_x0 = (int)eye * eye_w; // Flat axis-aligned quad over the (clipped) overlay rect, in @@ -4587,14 +4872,14 @@ shared_composite_decorations(struct multi_system_compositor *msc, }; #undef NDX #undef NDY - comp_multi_content_blend_draw_quad(&msc->shared_content_blend, vk, cmd, img, 0, + comp_multi_content_blend_draw_quad(t.content, vk, cmd, img, 0, sc->vkic.info.format, VK_NULL_HANDLE, VK_FORMAT_UNDEFINED, &pcq, tile_x0, 0, (uint32_t)eye_w, (uint32_t)eye_h, (uint32_t)msc->shared_atlas_w, (uint32_t)msc->shared_atlas_h); } - comp_multi_content_blend_end(&msc->shared_content_blend, vk, cmd); + comp_multi_content_blend_end(t.content, vk, cmd); VkImageMemoryBarrier ov_to_general = { .sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER, .srcAccessMask = VK_ACCESS_SHADER_READ_BIT, @@ -4612,8 +4897,9 @@ shared_composite_decorations(struct multi_system_compositor *msc, for (uint32_t eye = 0; eye < tile_columns; eye++) { int tile_x0 = (int)eye * eye_w; - shared_blend_into_atlas(msc, vk, cmd, img, tile_x0 + base_x, base_y, ow, oh, - tile_x0, tile_x0 + eye_w, 0, eye_h); + shared_blend_into_atlas(msc, vk, cmd, &t, img, (VkFormat)sc->vkic.info.format, + tile_x0 + base_x, base_y, ow, oh, tile_x0, tile_x0 + eye_w, 0, + eye_h); } } } @@ -4635,8 +4921,9 @@ shared_composite_decorations(struct multi_system_compositor *msc, int dy = pyg - (int)(cur.hot_y * (float)size_px); for (uint32_t eye = 0; eye < tile_columns; eye++) { int tile_x0 = (int)eye * eye_w; - shared_blend_into_atlas(msc, vk, cmd, img, tile_x0 + dx, dy, size_px, - size_px, tile_x0, tile_x0 + eye_w, 0, eye_h); + shared_blend_into_atlas(msc, vk, cmd, &t, img, + (VkFormat)sc->vkic.info.format, tile_x0 + dx, dy, + size_px, size_px, tile_x0, tile_x0 + eye_w, 0, eye_h); } } } From 469b39114b1b3422b35702475bdcd9064f01c448 Mon Sep 17 00:00:00 2001 From: dfattal Date: Fri, 2 Oct 2026 15:48:37 -0700 Subject: [PATCH 3/3] docs(adr-044): the macOS service decorations are format-honest too (#1795) Co-Authored-By: Claude Opus 5.5 --- docs/adr/ADR-044-colour-contract-per-backend.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/adr/ADR-044-colour-contract-per-backend.md b/docs/adr/ADR-044-colour-contract-per-backend.md index c6eb0ca2f8..01c9a4a016 100644 --- a/docs/adr/ADR-044-colour-contract-per-backend.md +++ b/docs/adr/ADR-044-colour-contract-per-backend.md @@ -146,7 +146,7 @@ The helper keys on the *target's* format. Pinning a format and calling the helpe - GL `gl_bind_layer_source(…, compose=false)`; - vk_native `get_image_view`, not `get_true_image_view`. - **vk_native window-space layers had the same gap until #1795.** `vk_hud_blend` sampled every source through a hard-coded `R8G8B8A8_UNORM` view and blended into the atlas in encoded space: the model viewer's displayxr-common button bar gave byte-identical atlases with swapchain 37 and 43, and the hard-coded view would have swapped R and B for a `B8G8R8A8` source. Since #1795 the in-process vk_native pass raw-copies the atlas into the private compose target, blends each layer through its `_SRGB` view with the source sampled as its **declared** format, and raw-copies back (`vk_compositor_render_window_space_linear`). All four Windows backends are now honest. Measured with the `windowspace_handle_*_win` probes, which draw opaque layers authored at byte 38: an `_SRGB` swapchain reads back 38 and a UNORM one reads back 108 (exactly one encode) on D3D11, D3D12, GL and vk_native. The service-side `comp_multi` chrome / overlay / HUD blends (macOS and Linux service) still sample through the legacy view; that is the next step of #1795, and the macOS shell (displayxr-shell-pvt#121) moves to `_SRGB` only together with it. + **vk_native window-space layers had the same gap until #1795.** `vk_hud_blend` sampled every source through a hard-coded `R8G8B8A8_UNORM` view and blended into the atlas in encoded space: the model viewer's displayxr-common button bar gave byte-identical atlases with swapchain 37 and 43, and the hard-coded view would have swapped R and B for a `B8G8R8A8` source. Since #1795 the in-process vk_native pass raw-copies the atlas into the private compose target, blends each layer through its `_SRGB` view with the source sampled as its **declared** format, and raw-copies back (`vk_compositor_render_window_space_linear`). All four Windows backends are now honest. Measured with the `windowspace_handle_*_win` probes, which draw opaque layers authored at byte 38: an `_SRGB` swapchain reads back 38 and a UNORM one reads back 108 (exactly one encode) on D3D11, D3D12, GL and vk_native. The macOS service's workspace decorations (shell chrome, cursor, overlays, drawn into the shared atlas by `comp_multi`) got the same fix in #1795. The atlas is created `MUTABLE_FORMAT` with its `_SRGB` sibling, and the decorations render through that view, each sampled as its declared format. That change ships together with displayxr-shell-pvt#121, which declares the shell's CoreGraphics-filled macOS surfaces `_SRGB` on runtime ≥ v2.23.0; a UNORM shell on the fixed service would be washed out. Linux registers no workspace surfaces, so it has no such site. Encoded bytes therefore pass through whether the swapchain says UNORM or `_SRGB`. Neither is washed out today, but a true-linear UNORM Local2D app renders too dark. Local2D is also flattened after the atlas, so §5's atlas capture cannot see it. App guidance does not change: submit Local2D as `_SRGB` with raw-copied bytes (displayxr-unity v2.21.0 already does). **Order matters:** apps move first. Making the flatten honest before they do would wash out every app that still declares UNORM Local2D. - **The displayxr-common Windows window-space HUD swapchain** stays `R8G8B8A8_UNORM` with display-referred bytes. On any format-honest backend it is encoded twice; its move to `_SRGB` is pending (displayxr-common).