diff --git a/src/xrt/auxiliary/vk/vk_local2d_composite.c b/src/xrt/auxiliary/vk/vk_local2d_composite.c index f7b81c4740..1e6a13ecc7 100644 --- a/src/xrt/auxiliary/vk/vk_local2d_composite.c +++ b/src/xrt/auxiliary/vk/vk_local2d_composite.c @@ -350,6 +350,56 @@ vk_local2d_composite_init(struct vk_local2d_composite *lc, return true; } +bool +vk_local2d_composite_init_srgb_flatten(struct vk_local2d_composite *lc, struct vk_bundle *vk, VkFormat srgb_fmt) +{ + if (!lc->initialized) { + return false; + } + if (lc->flatten_srgb_premul_pipe != VK_NULL_HANDLE && lc->flatten_srgb_fmt == srgb_fmt) { + return true; + } + + // Same blend states as the scratch_fmt flatten — only the attachment format + // (and so the render pass the pipelines are compatible with) differs. + VkPipelineColorBlendAttachmentState premul_blend = { + .blendEnable = VK_TRUE, + .colorWriteMask = VK_COLOR_COMPONENT_R_BIT | VK_COLOR_COMPONENT_G_BIT | VK_COLOR_COMPONENT_B_BIT | + VK_COLOR_COMPONENT_A_BIT, + .srcColorBlendFactor = VK_BLEND_FACTOR_ONE, + .dstColorBlendFactor = VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA, + .colorBlendOp = VK_BLEND_OP_ADD, + .srcAlphaBlendFactor = VK_BLEND_FACTOR_ONE, + .dstAlphaBlendFactor = VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA, + .alphaBlendOp = VK_BLEND_OP_ADD, + }; + VkPipelineColorBlendAttachmentState unpremul_blend = premul_blend; + unpremul_blend.srcColorBlendFactor = VK_BLEND_FACTOR_SRC_ALPHA; + + if (make_render_pass(vk, srgb_fmt, VK_ATTACHMENT_LOAD_OP_LOAD, &lc->flatten_srgb_rp) != VK_SUCCESS || + make_pipeline(vk, lc->vert_mod, lc->flatten_frag_mod, lc->flatten_layout, lc->flatten_srgb_rp, + &premul_blend, &lc->flatten_srgb_premul_pipe) != VK_SUCCESS || + make_pipeline(vk, lc->vert_mod, lc->flatten_frag_mod, lc->flatten_layout, lc->flatten_srgb_rp, + &unpremul_blend, &lc->flatten_srgb_unpremul_pipe) != VK_SUCCESS) { + U_LOG_E("[local2d] _SRGB flatten (format %d) creation failed", (int)srgb_fmt); + if (lc->flatten_srgb_premul_pipe != VK_NULL_HANDLE) { + vk->vkDestroyPipeline(vk->device, lc->flatten_srgb_premul_pipe, NULL); + lc->flatten_srgb_premul_pipe = VK_NULL_HANDLE; + } + if (lc->flatten_srgb_unpremul_pipe != VK_NULL_HANDLE) { + vk->vkDestroyPipeline(vk->device, lc->flatten_srgb_unpremul_pipe, NULL); + lc->flatten_srgb_unpremul_pipe = VK_NULL_HANDLE; + } + if (lc->flatten_srgb_rp != VK_NULL_HANDLE) { + vk->vkDestroyRenderPass(vk->device, lc->flatten_srgb_rp, NULL); + lc->flatten_srgb_rp = VK_NULL_HANDLE; + } + return false; + } + lc->flatten_srgb_fmt = srgb_fmt; + return true; +} + void vk_local2d_composite_begin_frame(struct vk_local2d_composite *lc, struct vk_bundle *vk) { @@ -663,26 +713,32 @@ vk_local2d_composite_raster_mask_zones(struct vk_local2d_composite *lc, vk->vkCmdEndRenderPass(cmd); } -void -vk_local2d_composite_flatten_draw(struct vk_local2d_composite *lc, - struct vk_bundle *vk, - VkCommandBuffer cmd, - VkFramebuffer scratch_fb, - uint32_t fb_w, - uint32_t fb_h, - VkImageView src_view, - int32_t dst_x, - int32_t dst_y, - uint32_t dst_w, - uint32_t dst_h, - float src_x, - float src_y, - float src_w, - float src_h, - bool unpremultiplied) +// The flatten draw, parameterised on the render pass + pipeline pair so the +// scratch_fmt flatten and the #1795 `_SRGB` flatten share one body. +static void +flatten_draw_impl(struct vk_local2d_composite *lc, + struct vk_bundle *vk, + VkCommandBuffer cmd, + VkRenderPass rp, + VkPipeline premul_pipe, + VkPipeline unpremul_pipe, + VkFramebuffer scratch_fb, + uint32_t fb_w, + uint32_t fb_h, + VkImageView src_view, + int32_t dst_x, + int32_t dst_y, + uint32_t dst_w, + uint32_t dst_h, + float src_x, + float src_y, + float src_w, + float src_h, + bool unpremultiplied) { - if (!lc->initialized || scratch_fb == VK_NULL_HANDLE || src_view == VK_NULL_HANDLE || dst_w == 0 || - dst_h == 0) { + if (!lc->initialized || rp == VK_NULL_HANDLE || premul_pipe == VK_NULL_HANDLE || + unpremul_pipe == VK_NULL_HANDLE || scratch_fb == VK_NULL_HANDLE || src_view == VK_NULL_HANDLE || + dst_w == 0 || dst_h == 0) { return; } @@ -693,7 +749,7 @@ vk_local2d_composite_flatten_draw(struct vk_local2d_composite *lc, VkRenderPassBeginInfo rp_bi = { .sType = VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO, - .renderPass = lc->flatten_rp, + .renderPass = rp, .framebuffer = scratch_fb, .renderArea = {{0, 0}, {fb_w, fb_h}}, }; @@ -706,8 +762,7 @@ vk_local2d_composite_flatten_draw(struct vk_local2d_composite *lc, VkRect2D scissor = {{dst_x, dst_y}, {dst_w, dst_h}}; vk->vkCmdSetScissor(cmd, 0, 1, &scissor); - vk->vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, - unpremultiplied ? lc->flatten_unpremul_pipe : lc->flatten_premul_pipe); + vk->vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, unpremultiplied ? unpremul_pipe : premul_pipe); vk->vkCmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, lc->flatten_layout, 0, 1, &set, 0, NULL); @@ -718,6 +773,52 @@ vk_local2d_composite_flatten_draw(struct vk_local2d_composite *lc, vk->vkCmdEndRenderPass(cmd); } +void +vk_local2d_composite_flatten_draw(struct vk_local2d_composite *lc, + struct vk_bundle *vk, + VkCommandBuffer cmd, + VkFramebuffer scratch_fb, + uint32_t fb_w, + uint32_t fb_h, + VkImageView src_view, + int32_t dst_x, + int32_t dst_y, + uint32_t dst_w, + uint32_t dst_h, + float src_x, + float src_y, + float src_w, + float src_h, + bool unpremultiplied) +{ + flatten_draw_impl(lc, vk, cmd, lc->flatten_rp, lc->flatten_premul_pipe, lc->flatten_unpremul_pipe, scratch_fb, + fb_w, fb_h, src_view, dst_x, dst_y, dst_w, dst_h, src_x, src_y, src_w, src_h, + unpremultiplied); +} + +void +vk_local2d_composite_flatten_draw_srgb(struct vk_local2d_composite *lc, + struct vk_bundle *vk, + VkCommandBuffer cmd, + VkFramebuffer scratch_fb, + uint32_t fb_w, + uint32_t fb_h, + VkImageView src_view, + int32_t dst_x, + int32_t dst_y, + uint32_t dst_w, + uint32_t dst_h, + float src_x, + float src_y, + float src_w, + float src_h, + bool unpremultiplied) +{ + flatten_draw_impl(lc, vk, cmd, lc->flatten_srgb_rp, lc->flatten_srgb_premul_pipe, + lc->flatten_srgb_unpremul_pipe, scratch_fb, fb_w, fb_h, src_view, dst_x, dst_y, dst_w, dst_h, + src_x, src_y, src_w, src_h, unpremultiplied); +} + void vk_local2d_composite_draw(struct vk_local2d_composite *lc, struct vk_bundle *vk, @@ -839,6 +940,8 @@ vk_local2d_composite_fini(struct vk_local2d_composite *lc, struct vk_bundle *vk) DESTROY(vkDestroyPipeline, lc->composite_pipe); DESTROY(vkDestroyPipeline, lc->flatten_premul_pipe); DESTROY(vkDestroyPipeline, lc->flatten_unpremul_pipe); + DESTROY(vkDestroyPipeline, lc->flatten_srgb_premul_pipe); + DESTROY(vkDestroyPipeline, lc->flatten_srgb_unpremul_pipe); DESTROY(vkDestroyPipelineLayout, lc->composite_layout); DESTROY(vkDestroyPipelineLayout, lc->flatten_layout); DESTROY(vkDestroyDescriptorSetLayout, lc->composite_dsl); @@ -846,6 +949,7 @@ vk_local2d_composite_fini(struct vk_local2d_composite *lc, struct vk_bundle *vk) DESTROY(vkDestroyDescriptorPool, lc->desc_pool); DESTROY(vkDestroyRenderPass, lc->composite_rp); DESTROY(vkDestroyRenderPass, lc->flatten_rp); + DESTROY(vkDestroyRenderPass, lc->flatten_srgb_rp); DESTROY(vkDestroyRenderPass, lc->mask_rp); DESTROY(vkDestroySampler, lc->sampler_point); DESTROY(vkDestroySampler, lc->sampler_linear); diff --git a/src/xrt/auxiliary/vk/vk_local2d_composite.h b/src/xrt/auxiliary/vk/vk_local2d_composite.h index 9c8e7a348b..6541f85ad2 100644 --- a/src/xrt/auxiliary/vk/vk_local2d_composite.h +++ b/src/xrt/auxiliary/vk/vk_local2d_composite.h @@ -75,6 +75,14 @@ struct vk_local2d_composite VkPipeline flatten_premul_pipe; //!< One / OneMinusSrcAlpha. VkPipeline flatten_unpremul_pipe; //!< SrcAlpha / OneMinusSrcAlpha. + //! #1795 / ADR-044 §7 — OPTIONAL linear-light flatten (see + //! vk_local2d_composite_init_srgb_flatten). VK_NULL_HANDLE until a caller + //! opts in; the scratch_fmt flatten above is untouched either way. + VkFormat flatten_srgb_fmt; + VkRenderPass flatten_srgb_rp; //!< flatten_srgb_fmt, LOAD_OP_LOAD. + VkPipeline flatten_srgb_premul_pipe; //!< One / OneMinusSrcAlpha. + VkPipeline flatten_srgb_unpremul_pipe; //!< SrcAlpha / OneMinusSrcAlpha. + VkDescriptorPool desc_pool; //!< Reset each frame by begin_frame. bool initialized; @@ -96,6 +104,23 @@ vk_local2d_composite_init(struct vk_local2d_composite *lc, VkFormat target_fmt, VkFormat scratch_fmt); +/*! + * #1795 / ADR-044 §7 — opt in to a second flatten render pass + pipeline pair + * whose attachment format is the `_SRGB` sibling @p srgb_fmt of the scratch. + * + * Drawn through vk_local2d_composite_flatten_draw_srgb into an `_SRGB` view of + * a MUTABLE scratch, the blend runs in linear light and the attachment encodes + * once on write — the encode is a property of the render target, never shader + * arithmetic. The scratch's UNORM view still reads ENCODED bytes, so every + * downstream reader is unchanged. Callers that never call this keep exactly + * the scratch_fmt flatten they had. Call after vk_local2d_composite_init. + * + * @return true when the `_SRGB` flatten is usable. + * @ingroup aux_vk + */ +bool +vk_local2d_composite_init_srgb_flatten(struct vk_local2d_composite *lc, struct vk_bundle *vk, VkFormat srgb_fmt); + /*! * Reset the per-frame descriptor pool. Call once before the frame's flatten / * composite draws (the vk_native compositor drains the GPU each frame, so any @@ -206,6 +231,32 @@ vk_local2d_composite_flatten_draw(struct vk_local2d_composite *lc, float src_h, bool unpremultiplied); +/*! + * #1795 — vk_local2d_composite_flatten_draw into an `_SRGB` attachment: same + * contract, but @p scratch_fb must be built over `flatten_srgb_rp` with an + * `_SRGB` view, and @p src_view should be the layer's FORMAT-HONEST view (an + * `_SRGB` source decodes on sample, a UNORM source is read as the linear values + * it holds). No-op unless vk_local2d_composite_init_srgb_flatten succeeded. + * @ingroup aux_vk + */ +void +vk_local2d_composite_flatten_draw_srgb(struct vk_local2d_composite *lc, + struct vk_bundle *vk, + VkCommandBuffer cmd, + VkFramebuffer scratch_fb, + uint32_t fb_w, + uint32_t fb_h, + VkImageView src_view, + int32_t dst_x, + int32_t dst_y, + uint32_t dst_w, + uint32_t dst_h, + float src_x, + float src_y, + float src_w, + float src_h, + bool unpremultiplied); + /*! * Masked composite into the target. Samples @p twod_view / @p mask_view / * @p weave_view at 1:1 over the region and lerps M*weave + (1-M)*twod, or — when diff --git a/src/xrt/compositor/d3d11/comp_d3d11_compositor.cpp b/src/xrt/compositor/d3d11/comp_d3d11_compositor.cpp index 6378f08a70..6429954861 100644 --- a/src/xrt/compositor/d3d11/comp_d3d11_compositor.cpp +++ b/src/xrt/compositor/d3d11/comp_d3d11_compositor.cpp @@ -5902,6 +5902,14 @@ d3d11_sync_zone_mask_to_dp(struct comp_d3d11_compositor *c) // on every REALLOCATION so the bridge knows to re-open. Under the split these // are allocated at the PANEL extent exactly once, so the generation moves during // warmup and never again. +// #1795 / ADR-044: do the Local2D flattens (over and 2D-under) blend in linear +// through an `_SRGB` view? Off only under the legacy colour hatch. +static inline bool +d3d11_local2d_honest(void) +{ + return !u_color_legacy_unorm_encoded(); +} + static bool d3d11_ensure_rt_srv_scratch(ID3D11Device *device, ID3D11Texture2D **tex, @@ -5912,13 +5920,19 @@ d3d11_ensure_rt_srv_scratch(ID3D11Device *device, DXGI_FORMAT fmt, const char *what, HANDLE *share = nullptr, - uint64_t *gen = nullptr) + uint64_t *gen = nullptr, + DXGI_FORMAT rtv_fmt = DXGI_FORMAT_UNKNOWN) { + // #1795: an RTV in a different member of the family (the `_SRGB` sibling, + // so a pass blends in linear and encodes on write) needs a TYPELESS + // texture; the SRV stays @p fmt, so every reader sees the same bytes. + const bool split_views = rtv_fmt != DXGI_FORMAT_UNKNOWN && rtv_fmt != fmt; + const DXGI_FORMAT tex_fmt = split_views ? d3d_dxgi_format_to_typeless_dxgi(fmt) : fmt; bool need_alloc = *tex == nullptr || *rtv == nullptr; if (!need_alloc) { D3D11_TEXTURE2D_DESC cur; (*tex)->GetDesc(&cur); - need_alloc = (cur.Width != w || cur.Height != h || cur.Format != fmt); + need_alloc = (cur.Width != w || cur.Height != h || cur.Format != tex_fmt); } if (!need_alloc) { return true; @@ -5944,7 +5958,7 @@ d3d11_ensure_rt_srv_scratch(ID3D11Device *device, td.Height = h; td.MipLevels = 1; td.ArraySize = 1; - td.Format = fmt; + td.Format = tex_fmt; td.SampleDesc.Count = 1; td.Usage = D3D11_USAGE_DEFAULT; td.BindFlags = D3D11_BIND_SHADER_RESOURCE | D3D11_BIND_RENDER_TARGET; @@ -5975,14 +5989,21 @@ d3d11_ensure_rt_srv_scratch(ID3D11Device *device, (*gen)++; } } - hr = device->CreateShaderResourceView(*tex, nullptr, srv); + D3D11_SHADER_RESOURCE_VIEW_DESC sd = {}; + sd.Format = fmt; + sd.ViewDimension = D3D11_SRV_DIMENSION_TEXTURE2D; + sd.Texture2D.MipLevels = 1; + hr = device->CreateShaderResourceView(*tex, &sd, srv); if (FAILED(hr) || *srv == nullptr) { U_LOG_W("%s: RT scratch SRV failed: 0x%08x", what, hr); (*tex)->Release(); *tex = nullptr; return false; } - hr = device->CreateRenderTargetView(*tex, nullptr, rtv); + D3D11_RENDER_TARGET_VIEW_DESC rd = {}; + rd.Format = split_views ? rtv_fmt : fmt; + rd.ViewDimension = D3D11_RTV_DIMENSION_TEXTURE2D; + hr = device->CreateRenderTargetView(*tex, &rd, rtv); if (FAILED(hr) || *rtv == nullptr) { U_LOG_W("%s: RT scratch RTV failed: 0x%08x", what, hr); (*srv)->Release(); @@ -6591,10 +6612,16 @@ d3d11_flatten_one_local2d_layer(struct comp_d3d11_compositor *c, return; } uint32_t img_idx = layer->data.local_2d.sub.image_index; - // sRGB-passthrough: get_srv returns the swapchain's UNORM sibling SRV - // (no auto-decode), the same SRV the projection draws sample. - ID3D11ShaderResourceView *src_srv = - static_cast(comp_d3d11_swapchain_get_srv(sc, img_idx)); + // #1795: the view follows the TARGET. Into an `_SRGB` RTV (the honest + // path) sample the layer as its declared format — an `_SRGB` source + // decodes, a UNORM source is read as the linear values it holds — so the + // blend is linear and the RTV encodes once. Into a UNORM RTV (legacy + // hatch) keep the non-decoding view: bytes pass through as before. + D3D11_RENDER_TARGET_VIEW_DESC rtv_desc = {}; + rtv->GetDesc(&rtv_desc); + const bool linear_target = d3d_dxgi_format_is_srgb(rtv_desc.Format); + ID3D11ShaderResourceView *src_srv = static_cast( + linear_target ? comp_d3d11_swapchain_get_compose_srv(sc, img_idx) : comp_d3d11_swapchain_get_srv(sc, img_idx)); if (src_srv == nullptr) { return; // swapchain not SAMPLED — nothing to flatten } @@ -6941,10 +6968,13 @@ d3d11_flatten_backdrop_2d(struct comp_d3d11_compositor *c, uint32_t dst_w, uint3 const bool split = c->split_active; const uint32_t alloc_w = split ? c->split_panel_w : region_w; const uint32_t alloc_h = split ? c->split_panel_h : region_h; + // #1795: same contract as the over-flatten — `_SRGB` RTV, encoded bytes. if (!d3d11_ensure_rt_srv_scratch(c->device, &c->backdrop_scratch, &c->backdrop_scratch_srv, &c->backdrop_scratch_rtv, alloc_w, alloc_h, DXGI_FORMAT_R8G8B8A8_UNORM, "backdrop scratch", split ? &c->backdrop_scratch_share : nullptr, - split ? &c->backdrop_scratch_gen : nullptr)) { + split ? &c->backdrop_scratch_gen : nullptr, + d3d11_local2d_honest() ? DXGI_FORMAT_R8G8B8A8_UNORM_SRGB + : DXGI_FORMAT_UNKNOWN)) { return nullptr; } @@ -7245,11 +7275,17 @@ d3d11_composite_zone_mask(struct comp_d3d11_compositor *c, */ const uint32_t alloc_w = c->split_active ? c->split_panel_w : region_w; const uint32_t alloc_h = c->split_active ? c->split_panel_h : region_h; + // #1795: the flatten writes through the `_SRGB` sibling, so the + // Local2D layers blend in linear and are encoded once on write; + // the scratch still holds ENCODED bytes for every reader (weave + // snapshot lerp, bridge plane). The legacy hatch keeps UNORM. + const DXGI_FORMAT l2d_rtv_fmt = + d3d11_local2d_honest() ? d3d_dxgi_format_srgb_rtv(unorm_fmt) : DXGI_FORMAT_UNKNOWN; if (!d3d11_ensure_rt_srv_scratch(c->device, &c->local2d_scratch, &c->local2d_scratch_srv, &c->local2d_scratch_rtv, alloc_w, alloc_h, unorm_fmt, "local2d scratch", c->split_active ? &c->local2d_scratch_share : nullptr, - c->split_active ? &c->local2d_scratch_gen : nullptr)) { + c->split_active ? &c->local2d_scratch_gen : nullptr, l2d_rtv_fmt)) { c->repaint.composite_bail = 4; return false; } diff --git a/src/xrt/compositor/d3d11_service/comp_d3d11_service.cpp b/src/xrt/compositor/d3d11_service/comp_d3d11_service.cpp index d45bbc4f5d..a4b822b871 100644 --- a/src/xrt/compositor/d3d11_service/comp_d3d11_service.cpp +++ b/src/xrt/compositor/d3d11_service/comp_d3d11_service.cpp @@ -533,6 +533,10 @@ struct d3d11_client_render_resources */ bool compose_mirrors_atlas; + //! #1795: last pure-zones compose decision logged for this client + //! (0 = none yet, 1 = passthrough, 2 = composing) — on-change WARN only. + uint8_t zones_compose_logged; + //! Standalone atlas-clear bookkeeping. The per-commit clear-to-black is //! only safe when every slot is guaranteed to be re-blitted this frame — //! and it is NOT: the fence / KeyedMutex paths deliberately skip a view's @@ -15814,8 +15818,9 @@ multi_compositor_render(struct d3d11_service_system *sys) // format here instead kept two such clients on Model A, blending them in // encoded space and passing the encoded bytes on as linear. The gate itself // is deliberately NOT widened to "any two clients": an atlas that genuinely - // holds linear bytes still exists (a pure-zones UNORM commit, and every - // client under DXR_COLOR_LEGACY_UNORM_ENCODED=1), and Model B would + // holds linear bytes still exists (every UNORM client under + // DXR_COLOR_LEGACY_UNORM_ENCODED=1 — a pure-zones UNORM commit composes + // since #1795), and Model B would // double-decode it. int honest_srgb_count = 0; for (int ri = 0; ri < render_count; ri++) { @@ -18220,15 +18225,16 @@ struct service_acquired_image_scope * Resolve the SRV to sample a zone/Local-2D source through, matched to where * this commit's writes land. * - * - **Passthrough** (the commit is not composing — every pure-zones commit - * today): the write goes to the UNORM atlas RTV with b1 zero, so the sample - * must return the stored bytes — service_raw_sample_srv(). The atlas then - * holds the app's bytes verbatim, byte-identically to the projection fast - * path's raw copy. - * - **Composing** (a mixed frame whose projection layer opened the private - * `_SRGB`-view target, `active_write_rtv` set): the image's own - * format-honest SRV — an `_SRGB` source decodes, the compose shader passes - * the linear value through and the target re-encodes on write. + * - **Passthrough** (the commit is not composing — a pure-zones commit whose + * every source is `_SRGB`, or any under the legacy hatch): the write goes to + * the UNORM atlas RTV with b1 zero, so the sample must return the stored + * bytes — service_raw_sample_srv(). The atlas then holds the app's bytes + * verbatim, byte-identically to the projection fast path's raw copy. + * - **Composing** (`active_write_rtv` set: a mixed frame whose projection + * layer opened the private `_SRGB`-view target, or — #1795 — a pure-zones + * commit with a non-`_SRGB` source): the image's own format-honest SRV — an + * `_SRGB` source decodes, a UNORM one reads linear, the compose shader + * passes the linear value through and the target encodes on write. * * #1591 dropped an sRGB-typed SRV the standalone branch built "because the DP * expects linear input" and declared the result "sample raw". It was not: the @@ -18377,6 +18383,11 @@ service_composite_zones_frame(struct d3d11_service_system *sys, struct service_acquired_image acquired[XRT_MAX_LAYERS * XRT_MAX_VIEWS]; uint32_t acquired_count = 0; + // #1795 (ADR-044 §7): is every source this pass samples DECLARED `_SRGB`? + // Walked over exactly the set the draws below read (same skips), so the + // compose decision and the draws cannot disagree on what was sampled. + bool all_sources_srgb = true; + // Acquire EVERY source image up front, all-or-nothing: the composite // clears the tile before blitting, so a per-view skip would flash that // placement black for a frame (visible blink). Skipping the whole pass @@ -18415,6 +18426,11 @@ service_composite_zones_frame(struct d3d11_service_system *sys, } return false; } + if (all_sources_srgb && sc->images[img].texture) { + D3D11_TEXTURE2D_DESC fd = {}; + sc->images[img].texture->GetDesc(&fd); + all_sources_srgb = is_srgb_format(fd.Format); + } } } @@ -18444,6 +18460,61 @@ service_composite_zones_frame(struct d3d11_service_system *sys, // acquire (it must read what the draws would have read). const uint64_t raw_pass = service_raw_copy_pass_begin(); + /* + * #1795 (ADR-044 §7) — does this PURE-zones commit compose? + * + * A mixed frame already decided on its projection write path (zones / + * Local-2D count as an extra painter there, so it composes unless the + * legacy hatch is on) and `projection_rendered` says so; that decision is + * left alone. A pure-zones commit never reached it, so every one was a + * passthrough and a UNORM source's declared format was ignored: its stored + * bytes landed in the encoded atlas raw. + * + * It composes only when a source OWES the encode — some source is not + * `_SRGB`, and the hatch is off. Every-`_SRGB` (the shipping population) + * stays the passthrough it was: same draws, same raw copies, same atlas + * bytes, no compose target touched. This is a narrower rule than + * u_color_compose_fast_path() states for sub-rect layers (it would compose + * any zones frame, to blend in linear); the all-`_SRGB` passthrough is kept + * byte-identical on purpose, and its edge blends stay encoded-space as + * before. + * + * Same target, seed rule and bookkeeping as the projection decision. The + * publish is the projection path's whole-atlas branch (`compose_publish_full` + * stays at its default for a commit with no projection layer). The seed is + * under the immediate_ctx_mutex this pass already holds — the projection + * path takes exactly that lock for it — and adds no wait. + */ + if (!projection_rendered) { + c->render.active_write_rtv = nullptr; + if (!all_sources_srgb && !u_color_legacy_unorm_encoded()) { + ID3D11RenderTargetView *compose = client_ensure_compose_target(sys, &c->render); + if (compose != nullptr) { + // A pure-zones commit always publishes the whole atlas, so the + // projection rule's `compose_publish_full` term is always true. + if (!c->render.compose_was_active || !c->render.compose_mirrors_atlas) { + sys->context->CopyResource(c->render.compose_texture.get(), + c->render.atlas_texture.get()); + c->render.compose_mirrors_atlas = true; + } + c->render.active_write_rtv = compose; + } + } + // Recorded on EVERY pure-zones commit that paints, passthrough ones + // included: a passthrough writes the atlas behind the private target's + // back, so the next composing commit must re-seed. + c->render.compose_was_active = c->render.active_write_rtv != nullptr; + // Per client and on change only (two zones clients under the shell can + // disagree, and a shared latch would then log every frame). + const uint8_t zones_state = c->render.active_write_rtv != nullptr ? 2u : 1u; + if (c->render.zones_compose_logged != zones_state) { + c->render.zones_compose_logged = zones_state; + U_LOG_W("Color (#1795) [d3d11_service]: client %p pure-zones commit %s (all_sources_srgb=%d legacy=%d)", + (void *)c, zones_state == 2u ? "COMPOSES through the _SRGB-view target" : "passes through", + all_sources_srgb ? 1 : 0, u_color_legacy_unorm_encoded() ? 1 : 0); + } + } + // Transparent-slot semantics — cleared only now that every source is // safely acquired. A mixed frame (a projection layer already blitted // this commit) skips the clear and composites the placed layers over @@ -18508,12 +18579,10 @@ service_composite_zones_frame(struct d3d11_service_system *sys, // the bulk of the atlas bytes. // // #1665: the same "what the runtime DID" rule as the - // projection stamp, through the same helper. A pure-zones - // commit never reaches the compose decision (it lives on - // the projection write path — the known gap), so - // `active_write_rtv` is null here today and the write is a - // passthrough into the atlas RTV: the atlas holds encoded - // bytes exactly when the source did AND the sample was raw. + // projection stamp, through the same helper. #1795: a + // pure-zones commit decides above; passthrough, the atlas + // holds encoded bytes exactly when the source did AND the + // sample was raw, composing it holds them regardless. // #1769: it used to pass the source's format alone, while // the sample decoded — an `_SRGB` zone stamped ENCODED over // linear values. Written this way so wiring the gap up @@ -18612,6 +18681,16 @@ service_composite_zones_frame(struct d3d11_service_system *sys, } } + // #1795: a composing pure-zones commit wrote through the `_SRGB`-view + // target, so the atlas holds ENCODED bytes whatever the sources were. The + // zone stamp above already says so when a zone painted; a Local-2D-only + // commit has no zone to stamp from, and composing is the one case where + // the answer needs no source at all. + if (!src_format_recorded && !projection_rendered && c->render.active_write_rtv != nullptr) { + c->atlas_holds_srgb_bytes = u_color_atlas_holds_encoded( + /*source_is_srgb=*/false, /*composed_through_srgb_target=*/true, u_color_legacy_unorm_encoded()); + } + ctx_lock.unlock(); // Release every cross-process mutex the pass acquired. diff --git a/src/xrt/compositor/d3d12/comp_d3d12_compositor.cpp b/src/xrt/compositor/d3d12/comp_d3d12_compositor.cpp index 90f57cdc47..1b670f9986 100644 --- a/src/xrt/compositor/d3d12/comp_d3d12_compositor.cpp +++ b/src/xrt/compositor/d3d12/comp_d3d12_compositor.cpp @@ -584,8 +584,14 @@ struct comp_d3d12_compositor uint32_t zone_wish_rect_count; /*! - * Flattened Local2D layers (the `twod` source). R8G8B8A8_UNORM render - * target — dedicated. Lazily (re)allocated window-sized. + * Flattened Local2D layers (the `twod` source). R8G8B8A8 render target — + * dedicated. Lazily (re)allocated window-sized. + * + * #1795 / ADR-044: allocated R8G8B8A8_TYPELESS with an `_SRGB` RTV, so the + * flatten blends in LINEAR and the render target encodes once, on write; + * every reader (the composite's SRV, the bridge / deposit plane copy) + * still sees the ENCODED bytes through the UNORM member. Under the legacy + * colour hatch it stays concrete UNORM with a UNORM RTV, as before. * * #918 D12-4: under the split this is a BRIDGE PLANE SOURCE and is allocated * at the PANEL instead — once, and then never resized, which is what keeps it @@ -7372,13 +7378,51 @@ d3d12_zone_cmd_execute(struct comp_d3d12_compositor *c) gpu_wait_idle_app(c); } +// #1795 / ADR-044: do the Local2D flattens (over and 2D-under) blend in linear +// through an `_SRGB` RTV? Off only under the legacy colour hatch. +static inline bool +d3d12_local2d_honest(void) +{ + return !u_color_legacy_unorm_encoded(); +} + +// #1795: the resource format of a Local2D / backdrop flatten scratch. An `_SRGB` +// RTV over UNORM-read bytes needs a TYPELESS resource (D3D12 cannot view a +// concrete format as another family member); the legacy hatch keeps UNORM. +static inline DXGI_FORMAT +d3d12_local2d_scratch_format(void) +{ + return d3d12_local2d_honest() ? DXGI_FORMAT_R8G8B8A8_TYPELESS : DXGI_FORMAT_R8G8B8A8_UNORM; +} + +// #1795: the RTV the flatten writes through — the `_SRGB` member when the +// scratch is TYPELESS (linear blend, encode on write), otherwise UNORM. +static inline DXGI_FORMAT +d3d12_local2d_rtv_format(DXGI_FORMAT scratch_fmt) +{ + return scratch_fmt == DXGI_FORMAT_R8G8B8A8_TYPELESS ? DXGI_FORMAT_R8G8B8A8_UNORM_SRGB + : DXGI_FORMAT_R8G8B8A8_UNORM; +} + +// #1795: does this flatten scratch's RTV encode on write? Read from the TARGET +// itself (its resource format decides its RTV), exactly as the D3D11 leg asks +// its RTV — so the flatten's source view can never disagree with its target. +static inline bool +d3d12_local2d_scratch_is_linear(ID3D12Resource *scratch) +{ + return scratch != nullptr && scratch->GetDesc().Format == DXGI_FORMAT_R8G8B8A8_TYPELESS; +} + // #439 Phase 3 — (re)allocate the dedicated Local2D flatten scratch -// (R8G8B8A8_UNORM, ALLOW_RENDER_TARGET, steady COMMON) + its RTV heap. +// (R8G8B8A8_TYPELESS + `_SRGB` RTV per #1795, or R8G8B8A8_UNORM under the +// legacy hatch; ALLOW_RENDER_TARGET, steady COMMON) + its RTV heap. // Returns false on allocation failure. static bool d3d12_ensure_local2d_scratch(struct comp_d3d12_compositor *c, uint32_t w, uint32_t h) { - if (c->local2d_scratch != nullptr && c->local2d_scratch_w == w && c->local2d_scratch_h == h) { + const DXGI_FORMAT tex_fmt = d3d12_local2d_scratch_format(); + if (c->local2d_scratch != nullptr && c->local2d_scratch_w == w && c->local2d_scratch_h == h && + c->local2d_scratch->GetDesc().Format == tex_fmt) { return true; } if (c->local2d_scratch != nullptr) { @@ -7394,15 +7438,18 @@ d3d12_ensure_local2d_scratch(struct comp_d3d12_compositor *c, uint32_t w, uint32 desc.Height = h; desc.DepthOrArraySize = 1; desc.MipLevels = 1; - desc.Format = DXGI_FORMAT_R8G8B8A8_UNORM; + desc.Format = tex_fmt; desc.SampleDesc.Count = 1; desc.Flags = D3D12_RESOURCE_FLAG_ALLOW_RENDER_TARGET; D3D12_HEAP_PROPERTIES heap = {}; heap.Type = D3D12_HEAP_TYPE_DEFAULT; + // The optimized clear names the format the clear is ISSUED through (the + // RTV); a TYPELESS resource cannot carry one of its own. + const DXGI_FORMAT rtv_fmt = d3d12_local2d_rtv_format(tex_fmt); D3D12_CLEAR_VALUE clear = {}; - clear.Format = DXGI_FORMAT_R8G8B8A8_UNORM; // transparent (flatten clears to 0,0,0,0) + clear.Format = rtv_fmt; // transparent (flatten clears to 0,0,0,0) HRESULT hr = c->device->CreateCommittedResource( &heap, D3D12_HEAP_FLAG_NONE, &desc, D3D12_RESOURCE_STATE_COMMON, &clear, IID_PPV_ARGS(&c->local2d_scratch)); @@ -7425,7 +7472,10 @@ d3d12_ensure_local2d_scratch(struct comp_d3d12_compositor *c, uint32_t w, uint32 return false; } } - c->device->CreateRenderTargetView(c->local2d_scratch, nullptr, + D3D12_RENDER_TARGET_VIEW_DESC rtv_view = {}; + rtv_view.Format = rtv_fmt; + rtv_view.ViewDimension = D3D12_RTV_DIMENSION_TEXTURE2D; + c->device->CreateRenderTargetView(c->local2d_scratch, &rtv_view, c->local2d_scratch_rtv_heap->GetCPUDescriptorHandleForHeapStart()); c->local2d_scratch_w = w; c->local2d_scratch_h = h; @@ -8092,6 +8142,9 @@ static bool d3d12_flatten_local_2d_layers(struct comp_d3d12_compositor *c, uint32_t region_w, uint32_t region_h, int32_t proj_idx) { D3D12_CPU_DESCRIPTOR_HANDLE rtv = c->local2d_scratch_rtv_heap->GetCPUDescriptorHandleForHeapStart(); + // #1795: the source view follows the TARGET — `_SRGB` RTV ⟹ sample each + // layer as declared and blend in linear; UNORM RTV (legacy) ⟹ passthrough. + const bool linear_target = d3d12_local2d_scratch_is_linear(c->local2d_scratch); // Scratch COMMON → RENDER_TARGET, clear transparent. Where a pixel is M=0 // (2D) but no layer covers it, twod stays (0,0,0,0) → final.a → 0 → the @@ -8176,7 +8229,7 @@ d3d12_flatten_local_2d_layers(struct comp_d3d12_compositor *c, uint32_t region_w // which share flatten_srv_heap within the one deferred cmd list). comp_d3d12_renderer_flatten_local_2d(c->renderer, c->cmd_list, rtv.ptr, src_res, i, x0, y0, (uint32_t)(x1 - x0), (uint32_t)(y1 - y0), src_x, src_y, src_w, - src_h, unpremult); + src_h, unpremult, linear_target); } // Scratch → sampleable for the masked composite. @@ -8192,10 +8245,16 @@ d3d12_flatten_local_2d_layers(struct comp_d3d12_compositor *c, uint32_t region_w // #491 part 3 — ensure the 2D-under backdrop scratch (clone of // d3d12_ensure_local2d_scratch; separate so a model switch can't dangle it). +// +// #1795: same contract as the over-flatten — TYPELESS + `_SRGB` RTV (legacy +// hatch: UNORM), ENCODED bytes for every reader (the DP's own R8G8B8A8_UNORM +// SRV, the bridge / deposit BACKDROP plane copy). static bool d3d12_ensure_backdrop_scratch(struct comp_d3d12_compositor *c, uint32_t w, uint32_t h) { - if (c->backdrop_scratch != nullptr && c->backdrop_scratch_w == w && c->backdrop_scratch_h == h) { + const DXGI_FORMAT tex_fmt = d3d12_local2d_scratch_format(); + if (c->backdrop_scratch != nullptr && c->backdrop_scratch_w == w && c->backdrop_scratch_h == h && + c->backdrop_scratch->GetDesc().Format == tex_fmt) { return true; } if (c->backdrop_scratch != nullptr) { @@ -8211,15 +8270,16 @@ d3d12_ensure_backdrop_scratch(struct comp_d3d12_compositor *c, uint32_t w, uint3 desc.Height = h; desc.DepthOrArraySize = 1; desc.MipLevels = 1; - desc.Format = DXGI_FORMAT_R8G8B8A8_UNORM; + desc.Format = tex_fmt; desc.SampleDesc.Count = 1; desc.Flags = D3D12_RESOURCE_FLAG_ALLOW_RENDER_TARGET; D3D12_HEAP_PROPERTIES heap = {}; heap.Type = D3D12_HEAP_TYPE_DEFAULT; + const DXGI_FORMAT rtv_fmt = d3d12_local2d_rtv_format(tex_fmt); D3D12_CLEAR_VALUE clear = {}; - clear.Format = DXGI_FORMAT_R8G8B8A8_UNORM; + clear.Format = rtv_fmt; HRESULT hr = c->device->CreateCommittedResource(&heap, D3D12_HEAP_FLAG_NONE, &desc, D3D12_RESOURCE_STATE_COMMON, &clear, @@ -8243,7 +8303,10 @@ d3d12_ensure_backdrop_scratch(struct comp_d3d12_compositor *c, uint32_t w, uint3 return false; } } - c->device->CreateRenderTargetView(c->backdrop_scratch, nullptr, + D3D12_RENDER_TARGET_VIEW_DESC rtv_view = {}; + rtv_view.Format = rtv_fmt; + rtv_view.ViewDimension = D3D12_RTV_DIMENSION_TEXTURE2D; + c->device->CreateRenderTargetView(c->backdrop_scratch, &rtv_view, c->backdrop_scratch_rtv_heap->GetCPUDescriptorHandleForHeapStart()); c->backdrop_scratch_w = w; c->backdrop_scratch_h = h; @@ -8323,6 +8386,8 @@ d3d12_flatten_backdrop_2d(struct comp_d3d12_compositor *c, uint32_t dst_w, uint3 } D3D12_CPU_DESCRIPTOR_HANDLE rtv = c->backdrop_scratch_rtv_heap->GetCPUDescriptorHandleForHeapStart(); + // #1795: same target-chosen source view as the over-flatten. + const bool linear_target = d3d12_local2d_scratch_is_linear(c->backdrop_scratch); D3D12_RESOURCE_BARRIER to_rt = {}; to_rt.Type = D3D12_RESOURCE_BARRIER_TYPE_TRANSITION; @@ -8385,7 +8450,7 @@ d3d12_flatten_backdrop_2d(struct comp_d3d12_compositor *c, uint32_t dst_w, uint3 bool unpremult = (layer->data.flags & XRT_LAYER_COMPOSITION_UNPREMULTIPLIED_ALPHA_BIT) != 0; comp_d3d12_renderer_flatten_local_2d(c->renderer, c->cmd_list, rtv.ptr, src_res, i, x0, y0, (uint32_t)(x1 - x0), (uint32_t)(y1 - y0), src_x, src_y, src_w, - src_h, unpremult); + src_h, unpremult, linear_target); } D3D12_RESOURCE_BARRIER to_psr = {}; @@ -9018,6 +9083,8 @@ d3d12_composite_zone_mask(struct comp_d3d12_compositor *c, uint64_t hash = 0; d3d12_local2d_digest(c, zones_frame ? -1 : proj_idx, /*over=*/true, region_w, region_h, &box, &hash); + // #1795: the chain stays UNORM (ENCODED bytes); a TYPELESS scratch + // into its own family's concrete chain is accepted (#1663). const bool twod_bound = comp_xbridge_bind_plane_resource( c->xbridge, COMP_XBRIDGE_PLANE_LOCAL2D, c->local2d_scratch, c->local2d_scratch_gen, (uint32_t)DXGI_FORMAT_R8G8B8A8_UNORM, c->split_panel_w, c->split_panel_h); diff --git a/src/xrt/compositor/d3d12/comp_d3d12_renderer.cpp b/src/xrt/compositor/d3d12/comp_d3d12_renderer.cpp index 6e7197602e..729bfa3f94 100644 --- a/src/xrt/compositor/d3d12/comp_d3d12_renderer.cpp +++ b/src/xrt/compositor/d3d12/comp_d3d12_renderer.cpp @@ -507,6 +507,11 @@ struct comp_d3d12_renderer ID3D12RootSignature *flatten_root_signature; ID3D12PipelineState *flatten_pso_premul; // SrcBlend = ONE ID3D12PipelineState *flatten_pso_straight; // SrcBlend = SRC_ALPHA + //! #1795 / ADR-044: the same two blends into the scratch's `_SRGB` RTV + //! (a D3D12 PSO bakes its RTV format), so the flatten blends in LINEAR + //! and the render target applies the OETF once, on write. + ID3D12PipelineState *flatten_pso_premul_srgb; + ID3D12PipelineState *flatten_pso_straight_srgb; //! Shader-visible SRV heap for flatten source images — one slot per layer //! draw (D3D12 consumes descriptors at GPU-execute time, so concurrent //! draws in one cmd-list each need their own slot). Written fresh / frame. @@ -2617,8 +2622,9 @@ comp_d3d12_renderer_create(struct comp_d3d12_compositor *c, } // Flatten PSOs: shared masked_composite VS + local2d_flatten PS. The - // scratch is always R8G8B8A8_UNORM (the masked composite is channel- - // agnostic), so one RTV format covers both blend variants. + // scratch is R8G8B8A8 (the masked composite is channel-agnostic); #1795 + // gives each blend variant a UNORM-RTV PSO (legacy hatch) and an + // `_SRGB`-RTV PSO (the honest path: linear blend, encode on write). ID3DBlob *flat_vs_blob = nullptr; ID3DBlob *flat_ps_blob = nullptr; hr = compile_shader(masked_composite_vs_source, "VSMain", "vs_5_0", &flat_vs_blob); @@ -2661,12 +2667,23 @@ comp_d3d12_renderer_create(struct comp_d3d12_compositor *c, hr = device->CreateGraphicsPipelineState(&flat_pso_desc, __uuidof(ID3D12PipelineState), reinterpret_cast(&r->flatten_pso_straight)); + if (SUCCEEDED(hr)) { + flat_pso_desc.RTVFormats[0] = DXGI_FORMAT_R8G8B8A8_UNORM_SRGB; + hr = device->CreateGraphicsPipelineState(&flat_pso_desc, __uuidof(ID3D12PipelineState), + reinterpret_cast(&r->flatten_pso_straight_srgb)); + } if (SUCCEEDED(hr)) { // Premultiplied "over": SrcBlend = ONE (the default for Local2D). flat_pso_desc.BlendState.RenderTarget[0].SrcBlend = D3D12_BLEND_ONE; + flat_pso_desc.RTVFormats[0] = DXGI_FORMAT_R8G8B8A8_UNORM; hr = device->CreateGraphicsPipelineState(&flat_pso_desc, __uuidof(ID3D12PipelineState), reinterpret_cast(&r->flatten_pso_premul)); } + if (SUCCEEDED(hr)) { + flat_pso_desc.RTVFormats[0] = DXGI_FORMAT_R8G8B8A8_UNORM_SRGB; + hr = device->CreateGraphicsPipelineState(&flat_pso_desc, __uuidof(ID3D12PipelineState), + reinterpret_cast(&r->flatten_pso_premul_srgb)); + } flat_vs_blob->Release(); flat_ps_blob->Release(); if (FAILED(hr)) { @@ -2717,6 +2734,12 @@ comp_d3d12_renderer_destroy(struct comp_d3d12_renderer **renderer_ptr) if (r->flatten_pso_straight != nullptr) { r->flatten_pso_straight->Release(); } + if (r->flatten_pso_premul_srgb != nullptr) { + r->flatten_pso_premul_srgb->Release(); + } + if (r->flatten_pso_straight_srgb != nullptr) { + r->flatten_pso_straight_srgb->Release(); + } if (r->flatten_root_signature != nullptr) { r->flatten_root_signature->Release(); } @@ -3728,8 +3751,10 @@ comp_d3d12_renderer_composite_2d_masked(struct comp_d3d12_renderer *renderer, // Write the 3 SRVs fresh into the dedicated shader-visible heap // (t0 = 2D source scratch, t1 = authored mask staged copy, t2 = weave - // snapshot scratch). Formats come from each resource — the scratches are - // runtime-created with concrete formats, the mask is R8_UNORM. + // snapshot scratch). Formats come from each resource — the mask is + // R8_UNORM. #1795: the Local2D scratch is TYPELESS (its flatten writes an + // `_SRGB` RTV) and a view cannot be typeless, so it resolves to the UNORM + // member: the composite still reads the ENCODED bytes, unchanged. ID3D12Resource *srcs[3] = { static_cast(twod_resource), static_cast(mask_resource), @@ -3739,7 +3764,7 @@ comp_d3d12_renderer_composite_2d_masked(struct comp_d3d12_renderer *renderer, for (int i = 0; i < 3; i++) { D3D12_RESOURCE_DESC rd = srcs[i]->GetDesc(); D3D12_SHADER_RESOURCE_VIEW_DESC srv_desc = {}; - srv_desc.Format = rd.Format; + srv_desc.Format = d3d_dxgi_typeless_to_typed_dxgi(rd.Format); srv_desc.ViewDimension = D3D12_SRV_DIMENSION_TEXTURE2D; srv_desc.Shader4ComponentMapping = D3D12_DEFAULT_SHADER_4_COMPONENT_MAPPING; srv_desc.Texture2D.MipLevels = 1; @@ -3819,7 +3844,8 @@ comp_d3d12_renderer_flatten_local_2d(struct comp_d3d12_renderer *renderer, float src_y, float src_w, float src_h, - bool unpremultiplied) + bool unpremultiplied, + bool linear_target) { if (renderer == nullptr || cmd_list_ptr == nullptr || scratch_rtv_handle == 0 || src_resource == nullptr) { return XRT_ERROR_DEVICE_CREATION_FAILED; @@ -3834,8 +3860,7 @@ comp_d3d12_renderer_flatten_local_2d(struct comp_d3d12_renderer *renderer, auto *cmd_list = static_cast(cmd_list_ptr); auto *src = static_cast(src_resource); - // Source image → sampleable. Sample app color swapchains as their UNORM - // sibling (no implicit sRGB decode — the DP wants display-referred bytes). + // Source image → sampleable (the view is chosen below, by TARGET). D3D12_RESOURCE_BARRIER src_barrier = {}; src_barrier.Type = D3D12_RESOURCE_BARRIER_TYPE_TRANSITION; src_barrier.Transition.pResource = src; @@ -3846,7 +3871,14 @@ comp_d3d12_renderer_flatten_local_2d(struct comp_d3d12_renderer *renderer, D3D12_SHADER_RESOURCE_VIEW_DESC srv_desc = {}; // #1503: TYPELESS resource -> resolve the view format from the app's request. - srv_desc.Format = comp_d3d12_swapchain_sample_format(src); + // #1795: the view follows the TARGET (a D3D12 CPU RTV handle cannot be + // asked its format, so the caller says). Into an `_SRGB` RTV (the honest + // path) sample the layer as its declared format — an `_SRGB` source + // decodes, a UNORM source is read as the linear values it holds — so the + // blend is linear and the RTV encodes once. Into a UNORM RTV (legacy + // hatch) keep the non-decoding view: bytes pass through as before. + srv_desc.Format = + linear_target ? comp_d3d12_swapchain_compose_format(src) : comp_d3d12_swapchain_sample_format(src); srv_desc.ViewDimension = D3D12_SRV_DIMENSION_TEXTURE2D; srv_desc.Texture2D.MipLevels = 1; srv_desc.Shader4ComponentMapping = D3D12_DEFAULT_SHADER_4_COMPONENT_MAPPING; @@ -3856,7 +3888,14 @@ comp_d3d12_renderer_flatten_local_2d(struct comp_d3d12_renderer *renderer, cmd_list->SetDescriptorHeaps(1, &renderer->flatten_srv_heap); cmd_list->SetGraphicsRootSignature(renderer->flatten_root_signature); - cmd_list->SetPipelineState(unpremultiplied ? renderer->flatten_pso_straight : renderer->flatten_pso_premul); + // The PSO's baked RTV format must match the RTV it draws into. + if (linear_target) { + cmd_list->SetPipelineState(unpremultiplied ? renderer->flatten_pso_straight_srgb + : renderer->flatten_pso_premul_srgb); + } else { + cmd_list->SetPipelineState(unpremultiplied ? renderer->flatten_pso_straight + : renderer->flatten_pso_premul); + } D3D12_CPU_DESCRIPTOR_HANDLE rtv = {}; rtv.ptr = static_cast(scratch_rtv_handle); diff --git a/src/xrt/compositor/d3d12/comp_d3d12_renderer.h b/src/xrt/compositor/d3d12/comp_d3d12_renderer.h index cd5b4e1954..dcaa577ac4 100644 --- a/src/xrt/compositor/d3d12/comp_d3d12_renderer.h +++ b/src/xrt/compositor/d3d12/comp_d3d12_renderer.h @@ -338,12 +338,16 @@ comp_d3d12_renderer_composite_2d_masked(struct comp_d3d12_renderer *renderer, * * @param renderer The renderer. * @param cmd_list Open D3D12 command list (void* = ID3D12GraphicsCommandList*). - * @param scratch_rtv_handle CPU RTV handle of the Local2D scratch (R8G8B8A8_UNORM). + * @param scratch_rtv_handle CPU RTV handle of the Local2D scratch (R8G8B8A8_UNORM, + * or R8G8B8A8_UNORM_SRGB on a TYPELESS scratch — see @p linear_target). * @param src_resource The layer's swapchain image (ID3D12Resource*). * @param slot_index Unique flatten-heap SRV slot for this draw (< XRT_MAX_LAYERS). * @param dst_x,dst_y,dst_w,dst_h Clipped dest sub-rect (viewport) in pixels. * @param src_x,src_y,src_w,src_h Source rect (normalized; src_h < 0 => flip_y). * @param unpremultiplied True for straight-alpha layers (XRT_LAYER_COMPOSITION_UNPREMULTIPLIED_ALPHA_BIT). + * @param linear_target #1795: true when @p scratch_rtv_handle is an `_SRGB` RTV — the source is + * then sampled format-honestly (an `_SRGB` layer decodes) and the blend runs in linear; + * false (legacy hatch, UNORM RTV) samples the non-decoding view. * * @ingroup comp_d3d12 */ @@ -361,7 +365,8 @@ comp_d3d12_renderer_flatten_local_2d(struct comp_d3d12_renderer *renderer, float src_y, float src_w, float src_h, - bool unpremultiplied); + bool unpremultiplied, + bool linear_target); /*! * Resize the renderer's atlas texture to match a new view size. diff --git a/src/xrt/compositor/gl/comp_gl_compositor.cpp b/src/xrt/compositor/gl/comp_gl_compositor.cpp index 32b6411ef4..2831651a83 100644 --- a/src/xrt/compositor/gl/comp_gl_compositor.cpp +++ b/src/xrt/compositor/gl/comp_gl_compositor.cpp @@ -1002,6 +1002,18 @@ struct comp_gl_compositor //! composites `backdrop over captured-desktop` under the 3D weave. Own FBO. GLuint backdrop_scratch_tex, backdrop_scratch_fbo; uint32_t backdrop_scratch_w, backdrop_scratch_h; + //! #1795 / ADR-044 §7 — the private GL_SRGB8_ALPHA8 flatten target both + //! Local2D flattens (over and 2D-under) draw into, with + //! GL_FRAMEBUFFER_SRGB on, so overlapping layers blend in LINEAR and are + //! encoded once on write; the result is then COPIED verbatim into + //! local2d_scratch / backdrop_scratch, which stay GL_RGBA8 holding ENCODED + //! bytes for every reader. The Local2D twin of compose_texture. 0 under the + //! legacy hatch or when the stack cannot copy (then the flatten writes the + //! scratch directly, as before #1795). `local2d_linear_broken` latches a + //! target that would not come out framebuffer-complete. + GLuint local2d_linear_tex, local2d_linear_fbo; + uint32_t local2d_linear_w, local2d_linear_h; + bool local2d_linear_broken; uint32_t implicit_mask_w, implicit_mask_h; uint32_t implicit_rect_count; struct xrt_rect implicit_rects[XRT_MAX_LAYERS]; @@ -3748,13 +3760,157 @@ gl_update_zone_feather_mask(struct comp_gl_compositor *c, return c->feather_mask_tex; } +/*! + * #1795 / ADR-044 §7 — build (or rebuild) the private linear-blend target the + * Local2D flattens draw into, at @p w x @p h. + * + * The Local2D twin of gl_ensure_compose_target(), and the same three pieces: + * a GL_SRGB8_ALPHA8 attachment, GL_FRAMEBUFFER_SRGB on for the flatten draws + * (so the fixed-function blender works in LINEAR and the OETF is applied + * exactly once, on write, by the hardware — never shader arithmetic), and a + * verbatim glCopyImageSubData into the GL_RGBA8 scratch afterwards + * (gl_publish_local2d_linear). The scratches therefore keep holding ENCODED + * bytes, so neither reader — the post-weave masked composite (and its #885 + * diag dump) nor the display processor's set_background_2d — changes. + * + * Returns false when the flatten must write the scratch directly, exactly as + * before #1795: the legacy colour hatch (the one switch — checked first, as + * gl_frame_takes_fast_path does), no glCopyImageSubData (GL < 4.3 / Apple + * 4.1: a draw into the scratch would re-encode), or an attachment that did not + * come out framebuffer-complete (latched, logged once). + * + * Deliberately NOT gated on GL_EXT_texture_sRGB_decode, for the same reason + * gl_frame_takes_fast_path() is gated the other way: this path only ever + * samples with decode ON (the GL default), which every stack can do. It is the + * legacy non-decoding read that needs the extension; without it an `_SRGB` + * source is decoded regardless, and only this path re-encodes it. + */ +static bool +gl_ensure_local2d_linear_target(struct comp_gl_compositor *c, uint32_t w, uint32_t h) +{ + if (c->legacy_color || c->local2d_linear_broken || w == 0 || h == 0) { + return false; + } + if (glCopyImageSubData == NULL) { + static bool warned_no_copy = false; + if (!warned_no_copy) { + warned_no_copy = true; + U_LOG_W("Color (#1795) [gl]: glCopyImageSubData did not resolve (GL 4.3 / ARB_copy_image) — " + "Local2D flatten blends in encoded space, as before #1795"); + } + return false; + } + if (c->local2d_linear_tex != 0 && c->local2d_linear_w == w && c->local2d_linear_h == h) { + return true; + } + + if (c->local2d_linear_tex != 0) { + glDeleteTextures(1, &c->local2d_linear_tex); + c->local2d_linear_tex = 0; + } + if (c->local2d_linear_fbo == 0) { + glGenFramebuffers(1, &c->local2d_linear_fbo); + } + glGenTextures(1, &c->local2d_linear_tex); + glBindTexture(GL_TEXTURE_2D, c->local2d_linear_tex); + glTexImage2D(GL_TEXTURE_2D, 0, GL_SRGB8_ALPHA8, (GLsizei)w, (GLsizei)h, 0, GL_RGBA, GL_UNSIGNED_BYTE, NULL); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); + glBindTexture(GL_TEXTURE_2D, 0); + + GLuint prev_fbo = 0; + glGetIntegerv(GL_FRAMEBUFFER_BINDING, (GLint *)&prev_fbo); + glBindFramebuffer(GL_FRAMEBUFFER, c->local2d_linear_fbo); + glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, c->local2d_linear_tex, 0); + const GLenum status = glCheckFramebufferStatus(GL_FRAMEBUFFER); + glBindFramebuffer(GL_FRAMEBUFFER, prev_fbo); + if (status != GL_FRAMEBUFFER_COMPLETE) { + U_LOG_W("Color (#1795) [gl]: Local2D flatten target %ux%u is not framebuffer-complete (0x%x) — " + "blending in encoded space, as before #1795", + w, h, (unsigned)status); + glDeleteTextures(1, &c->local2d_linear_tex); + c->local2d_linear_tex = 0; + c->local2d_linear_broken = true; + return false; + } + + c->local2d_linear_w = w; + c->local2d_linear_h = h; + // One-off per (re)allocation — a lifecycle event, never per frame. + U_LOG_W("Color (#1795) [gl]: Local2D flatten target %ux%u storage=GL_SRGB8_ALPHA8 -> scratch=GL_RGBA8 " + "(linear blend, encoded once on write, published by glCopyImageSubData)", + w, h); + return true; +} + +/*! + * #1795 — bind the FBO a Local2D flatten draws into, and set the write-side + * conversion to match. @p linear true: the private GL_SRGB8_ALPHA8 target with + * GL_FRAMEBUFFER_SRGB on (the previous enable is returned in @p prev_srgb for + * gl_local2d_flatten_end). @p linear false: the scratch itself, as before. + */ +static void +gl_local2d_flatten_begin(struct comp_gl_compositor *c, GLuint scratch_fbo, bool linear, GLboolean *prev_srgb) +{ + *prev_srgb = glIsEnabled(GL_FRAMEBUFFER_SRGB); + if (!linear) { + glBindFramebuffer(GL_FRAMEBUFFER, scratch_fbo); + return; + } + glBindFramebuffer(GL_FRAMEBUFFER, c->local2d_linear_fbo); + // The PASS owns this enable, not a draw (#1610): on for the clear + every + // layer of this flatten, restored by gl_local2d_flatten_end. + glEnable(GL_FRAMEBUFFER_SRGB); +} + +/*! + * #1795 — publish the linear-blend flatten into its scratch. + * + * `glCopyImageSubData`, deliberately, for the reason gl_publish_compose_to_atlas + * spells out: GL_SRGB8_ALPHA8 and GL_RGBA8 are the same 32-bit RGBA class, so + * this moves the ENCODED bytes the sRGB attachment just wrote without touching + * them. A draw or a glBlitFramebuffer would risk re-applying the transfer + * function. Do not "optimise" it into a draw. + */ +static void +gl_publish_local2d_linear(struct comp_gl_compositor *c, GLuint scratch_tex, uint32_t w, uint32_t h) +{ + glCopyImageSubData(c->local2d_linear_tex, GL_TEXTURE_2D, 0, 0, 0, 0, // + scratch_tex, GL_TEXTURE_2D, 0, 0, 0, 0, // + (GLsizei)w, (GLsizei)h, 1); +} + +//! #1795 — the other half of gl_local2d_flatten_begin: restore the write-side +//! conversion, then publish the linear-blend flatten into @p scratch_tex. +static void +gl_local2d_flatten_end( + struct comp_gl_compositor *c, bool linear, GLboolean prev_srgb, GLuint scratch_tex, uint32_t w, uint32_t h) +{ + if (!linear) { + return; + } + if (!prev_srgb) { + glDisable(GL_FRAMEBUFFER_SRGB); + } + gl_publish_local2d_linear(c, scratch_tex, w, h); +} + // #439 Phase 3 — draw one Local2D layer into the currently-bound flatten FBO // (premultiplied or straight "over"). Assumes program_window_space is bound and // the loc_* uniforms fetched by the caller. Dest rect clips to the window region; -// Y is flipped for the bottom-left GL framebuffer. +// Y is flipped for the bottom-left GL framebuffer. @p linear: the bound FBO is +// the #1795 GL_SRGB8_ALPHA8 flatten target with GL_FRAMEBUFFER_SRGB on. static void -gl_flatten_one_local2d_layer(struct comp_gl_compositor *c, struct comp_layer *layer, uint32_t region_w, - uint32_t region_h, GLint loc_rect, GLint loc_tex, GLint loc_src) +gl_flatten_one_local2d_layer(struct comp_gl_compositor *c, + struct comp_layer *layer, + uint32_t region_w, + uint32_t region_h, + GLint loc_rect, + GLint loc_tex, + GLint loc_src, + bool linear) { struct xrt_swapchain *sc = layer->sc_array[0]; if (sc == NULL) { @@ -3815,14 +3971,16 @@ gl_flatten_one_local2d_layer(struct comp_gl_compositor *c, struct comp_layer *la } glActiveTexture(GL_TEXTURE0); - // #1589: DELIBERATELY the non-decoding read, never this frame's - // compose_active. The Local2D flatten is not part of the atlas pass — it - // hands the app's bytes on into local2d_scratch, which the POST-weave - // composite mixes with the DP's already-encoded output, so encoded is the - // space that scratch lives in. Same deliberate exception the D3D12 leg - // carries (its flatten is the one direct comp_d3d12_swapchain_sample_format - // call left in the renderer). - gl_bind_layer_source(GL_TEXTURE_2D, src_tex, /*compose=*/false); + // #1795: the read follows the flatten's TARGET, never this frame's + // compose_active (the flatten is not part of the atlas pass). Into the + // GL_SRGB8_ALPHA8 flatten target (the honest path) sample the layer as + // its declared format — an `_SRGB` source decodes, a GL_RGBA8 source is + // read as the linear values it holds — so the blend is linear and the + // target encodes once. Into the GL_RGBA8 scratch (legacy hatch / no + // copy) keep the non-decoding read: the app's bytes pass through as + // before. Either way the scratch ends up ENCODED, the space the POST-weave + // composite mixes with the DP's output. + gl_bind_layer_source(GL_TEXTURE_2D, src_tex, /*compose=*/linear); glUniform1i(loc_tex, 0); glUniform4f(loc_rect, nx, ny, nw, nh); glUniform4f(loc_src, src_x, src_y, src_w, src_h); @@ -3836,7 +3994,10 @@ gl_flatten_one_local2d_layer(struct comp_gl_compositor *c, struct comp_layer *la static void gl_flatten_local_2d_layers(struct comp_gl_compositor *c, uint32_t region_w, uint32_t region_h, int32_t proj_idx) { - glBindFramebuffer(GL_FRAMEBUFFER, c->local2d_scratch_fbo); + // #1795: blend in linear through the private sRGB target, publish encoded. + const bool linear = gl_ensure_local2d_linear_target(c, region_w, region_h); + GLboolean prev_srgb = GL_FALSE; + gl_local2d_flatten_begin(c, c->local2d_scratch_fbo, linear, &prev_srgb); glViewport(0, 0, region_w, region_h); glDisable(GL_SCISSOR_TEST); glClearColor(0.0f, 0.0f, 0.0f, 0.0f); // transparent → desktop where uncovered (final.a=0) @@ -3857,11 +4018,12 @@ gl_flatten_local_2d_layers(struct comp_gl_compositor *c, uint32_t region_w, uint if (proj_idx >= 0 && (int32_t)i < proj_idx) { continue; } - gl_flatten_one_local2d_layer(c, layer, region_w, region_h, loc_rect, loc_tex, loc_src); + gl_flatten_one_local2d_layer(c, layer, region_w, region_h, loc_rect, loc_tex, loc_src, linear); } glDisable(GL_BLEND); glBindTexture(GL_TEXTURE_2D, 0); + gl_local2d_flatten_end(c, linear, prev_srgb, c->local2d_scratch_tex, region_w, region_h); glBindFramebuffer(GL_FRAMEBUFFER, 0); } @@ -3918,7 +4080,10 @@ gl_flatten_backdrop_2d(struct comp_gl_compositor *c, uint32_t dst_w, uint32_t ds return 0; } - glBindFramebuffer(GL_FRAMEBUFFER, c->backdrop_scratch_fbo); + // #1795: same contract as the over-flatten — linear blend, encoded bytes. + const bool linear = gl_ensure_local2d_linear_target(c, region_w, region_h); + GLboolean prev_srgb = GL_FALSE; + gl_local2d_flatten_begin(c, c->backdrop_scratch_fbo, linear, &prev_srgb); glViewport(0, 0, region_w, region_h); glDisable(GL_SCISSOR_TEST); glClearColor(0.0f, 0.0f, 0.0f, 0.0f); // transparent where no under-layer covers @@ -3935,11 +4100,12 @@ gl_flatten_backdrop_2d(struct comp_gl_compositor *c, uint32_t dst_w, uint32_t ds if (layer->data.type != XRT_LAYER_LOCAL_2D) { continue; } - gl_flatten_one_local2d_layer(c, layer, region_w, region_h, loc_rect, loc_tex, loc_src); + gl_flatten_one_local2d_layer(c, layer, region_w, region_h, loc_rect, loc_tex, loc_src, linear); } glDisable(GL_BLEND); glBindTexture(GL_TEXTURE_2D, 0); + gl_local2d_flatten_end(c, linear, prev_srgb, c->backdrop_scratch_tex, region_w, region_h); glBindFramebuffer(GL_FRAMEBUFFER, 0); static bool logged = false; @@ -5510,8 +5676,9 @@ gl_frame_takes_fast_path(struct comp_gl_compositor *c) break; default: // Local2D is composited POST-weave, not into the atlas, so - // it is not this decision's business (and keeps its own - // passthrough read — see gl_flatten_one_local2d_layer). + // it is not this decision's business (it owns its own + // linear-blend target, #1795 — see + // gl_ensure_local2d_linear_target). // Cylinder / equirect1 / cube are accepted by the // compositor, warned about, and never drawn on this backend // (#1544). A type that starts drawing into the atlas here @@ -6712,6 +6879,9 @@ gl_compositor_destroy(struct xrt_compositor *xc) // #491 part 3 — 2D-under backdrop scratch. if (c->backdrop_scratch_tex) glDeleteTextures(1, &c->backdrop_scratch_tex); if (c->backdrop_scratch_fbo) glDeleteFramebuffers(1, &c->backdrop_scratch_fbo); + // #1795 — private linear-blend Local2D flatten target. + if (c->local2d_linear_tex) glDeleteTextures(1, &c->local2d_linear_tex); + if (c->local2d_linear_fbo) glDeleteFramebuffers(1, &c->local2d_linear_fbo); #ifdef XRT_OS_WINDOWS // Clean up D3D11 shared-texture present resources (readback bridge; no interop). 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 d77aa97f0e..26162130d5 100644 --- a/src/xrt/compositor/vk_native/comp_vk_native_compositor.c +++ b/src/xrt/compositor/vk_native/comp_vk_native_compositor.c @@ -1134,6 +1134,8 @@ struct comp_vk_native_compositor //! B8G8R8A8_UNORM for both the target and the scratch — see the init). struct vk_local2d_composite local2d; bool local2d_initialized; + //! #1795 — local2d's `_SRGB` (B8G8R8A8_SRGB) flatten pipelines exist. + bool local2d_srgb_flatten_ready; //! twod flatten scratch (B8G8R8A8_UNORM, COLOR_ATTACHMENT|SAMPLED). The //! frame's OVER Local2D layers (after the projection in list order) are @@ -1144,6 +1146,12 @@ struct comp_vk_native_compositor VkImageView local2d_scratch_view; VkFramebuffer local2d_scratch_fb; uint32_t local2d_scratch_w, local2d_scratch_h; + //! #1795 / ADR-044 §7 — the flatten's `_SRGB` view of local2d_scratch + //! (MUTABLE, {UNORM,_SRGB}). Non-NULL ⟺ local2d_scratch_fb is built over it + //! and local2d.flatten_srgb_rp, so the layers blend in linear light and the + //! attachment encodes once; local2d_scratch_view (UNORM) still reads the + //! ENCODED bytes every downstream reader expects. NULL = legacy hatch. + VkImageView local2d_scratch_srgb_view; //! #491 part 3 — 2D-under backdrop scratch (same fmt/usage as //! local2d_scratch). The frame's UNDER Local2D layers (before the projection @@ -1156,6 +1164,8 @@ struct comp_vk_native_compositor VkImageView backdrop_scratch_view; VkFramebuffer backdrop_scratch_fb; uint32_t backdrop_scratch_w, backdrop_scratch_h; + //! #1795 — same contract as local2d_scratch_srgb_view. + VkImageView backdrop_scratch_srgb_view; //! Weave snapshot scratch (target format, TRANSFER_DST|SAMPLED). The DP //! wrote the woven 3D into the target (RT≠SRV), so the lerp reads this @@ -10396,6 +10406,17 @@ comp_vk_native_compositor_create(struct xrt_device *xdev, VK_FORMAT_B8G8R8A8_UNORM); if (!c->local2d_initialized) { U_LOG_W("VK Local2D composite init failed — 2D-over-3D masking disabled this session"); + } else if (!u_color_legacy_unorm_encoded()) { + // #1795 / ADR-044 §7 — the Local2D flattens blend in linear light: + // a second flatten pipeline pair whose attachment is the scratch's + // `_SRGB` sibling. The scratch's own format (and every reader of it) + // is unchanged. The legacy hatch never creates it. + c->local2d_srgb_flatten_ready = + vk_local2d_composite_init_srgb_flatten(&c->local2d, &c->vk, VK_FORMAT_B8G8R8A8_SRGB); + if (!c->local2d_srgb_flatten_ready) { + U_LOG_W( + "VK Local2D: _SRGB flatten unavailable — Local2D layers flatten byte-passthrough (#1795)"); + } } // Populate supported swapchain formats (Vulkan formats) @@ -12548,6 +12569,149 @@ vk_destroy_rt(struct comp_vk_native_compositor *c, } } +//! #1795 — vk_destroy_rt plus the flatten scratch's `_SRGB` view, in +//! dependency order (framebuffer, then views, then the image). +static void +vk_destroy_flatten_rt(struct comp_vk_native_compositor *c, + VkImage *img, + VkDeviceMemory *mem, + VkImageView *view, + VkImageView *srgb_view, + VkFramebuffer *fb) +{ + struct vk_bundle *vk = &c->vk; + if (fb != NULL && *fb != VK_NULL_HANDLE) { + vk->vkDestroyFramebuffer(vk->device, *fb, NULL); + *fb = VK_NULL_HANDLE; + } + if (*srgb_view != VK_NULL_HANDLE) { + vk->vkDestroyImageView(vk->device, *srgb_view, NULL); + *srgb_view = VK_NULL_HANDLE; + } + vk_destroy_rt(c, img, mem, view, fb); +} + +/*! + * #1795 / ADR-044 §7 — create/reuse a Local2D FLATTEN scratch (B8G8R8A8). + * + * Honest path (local2d_srgb_flatten_ready, i.e. not the legacy hatch): the image + * is B8G8R8A8_UNORM + MUTABLE_FORMAT with a {UNORM, _SRGB} format list — the + * shape of the renderer's private compose target (#1799). @p view is its UNORM + * view, so every reader (the masked composite, the DP backdrop, the #73 dumps) + * still sees ENCODED bytes; @p srgb_view is the `_SRGB` view the flatten + * renders through, and @p fb is built over it and local2d.flatten_srgb_rp. The + * attachment does the encode — no blit, no shader arithmetic. + * + * Legacy hatch (or if the mutable image cannot be made): exactly the old + * vk_ensure_rt scratch, UNORM fb over local2d.flatten_rp, @p srgb_view NULL. + * The caller reads `*srgb_view != VK_NULL_HANDLE` as "this target is _SRGB". + */ +static bool +vk_ensure_flatten_rt(struct comp_vk_native_compositor *c, + VkImage *img, + VkDeviceMemory *mem, + VkImageView *view, + VkImageView *srgb_view, + VkFramebuffer *fb, + uint32_t *cw, + uint32_t *ch, + uint32_t w, + uint32_t h, + VkImageUsageFlags usage, + const char *what) +{ + struct vk_bundle *vk = &c->vk; + const bool honest = c->local2d_srgb_flatten_ready; + + if (*img != VK_NULL_HANDLE && *cw == w && *ch == h && (*srgb_view != VK_NULL_HANDLE) == honest) { + return true; + } + // Any reallocation (dims, or the target kind flipping) starts from nothing: + // a same-dims UNORM image left behind would let vk_ensure_rt early-out + // with no fb. + vk_destroy_flatten_rt(c, img, mem, view, srgb_view, fb); + *cw = 0; + *ch = 0; + + if (honest) { + VkFormat list[2] = {VK_FORMAT_B8G8R8A8_UNORM, VK_FORMAT_B8G8R8A8_SRGB}; + VkImageFormatListCreateInfo fmt_list = { + .sType = VK_STRUCTURE_TYPE_IMAGE_FORMAT_LIST_CREATE_INFO, + .viewFormatCount = 2, + .pViewFormats = list, + }; + VkImageCreateInfo ici = { + .sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO, + .pNext = &fmt_list, + .flags = VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT, + .imageType = VK_IMAGE_TYPE_2D, + .format = VK_FORMAT_B8G8R8A8_UNORM, + .extent = {w, h, 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, + }; + bool ok = vk->vkCreateImage(vk->device, &ici, NULL, img) == VK_SUCCESS; + if (!ok) { + *img = VK_NULL_HANDLE; + } else { + VkMemoryRequirements mr; + vk->vkGetImageMemoryRequirements(vk->device, *img, &mr); + uint32_t type_id = 0; + ok = vk_get_memory_type(vk, mr.memoryTypeBits, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, &type_id); + if (ok) { + VkMemoryAllocateInfo mai = { + .sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO, + .allocationSize = mr.size, + .memoryTypeIndex = type_id, + }; + ok = vk->vkAllocateMemory(vk->device, &mai, NULL, mem) == VK_SUCCESS; + if (!ok) { + *mem = VK_NULL_HANDLE; + } + ok = ok && vk->vkBindImageMemory(vk->device, *img, *mem, 0) == VK_SUCCESS; + } + } + ok = ok && vk_create_view(vk, *img, VK_IMAGE_VIEW_TYPE_2D, VK_FORMAT_B8G8R8A8_UNORM, k_color_sub, + view) == VK_SUCCESS; + ok = ok && vk_create_view(vk, *img, VK_IMAGE_VIEW_TYPE_2D, VK_FORMAT_B8G8R8A8_SRGB, k_color_sub, + srgb_view) == VK_SUCCESS; + if (ok && fb != NULL) { + VkFramebufferCreateInfo fb_ci = { + .sType = VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO, + .renderPass = c->local2d.flatten_srgb_rp, + .attachmentCount = 1, + .pAttachments = srgb_view, + .width = w, + .height = h, + .layers = 1, + }; + ok = vk->vkCreateFramebuffer(vk->device, &fb_ci, NULL, fb) == VK_SUCCESS; + } + if (ok) { + *cw = w; + *ch = h; + return true; + } + + // Fall back to the legacy scratch for the rest of the session rather + // than lose the 2D: the flatten pipelines are fine, the image is not. + U_LOG_W( + "[local2d] %s: MUTABLE {UNORM,_SRGB} %ux%u scratch failed — Local2D flattens " + "byte-passthrough from here (#1795)", + what, w, h); + vk_destroy_flatten_rt(c, img, mem, view, srgb_view, fb); + c->local2d_srgb_flatten_ready = false; + } + + return vk_ensure_rt(c, img, mem, view, fb, cw, ch, w, h, VK_FORMAT_B8G8R8A8_UNORM, usage, c->local2d.flatten_rp, + what); +} + #ifdef XRT_OS_WINDOWS /*! * VK-1b (#1178) — (re)build the flatten framebuffer over a bridge PLANE's image. @@ -12665,10 +12829,20 @@ vk_local2d_begin_frame_once(struct comp_vk_native_compositor *c) // region. Shared by the pre-weave backdrop flatten (#491 part 3, under-layers) // and the post-weave overlay flatten (#439 Phase 3, over-layers) so the source // geometry / flip / unpremult handling stays identical between the two. +// +// #1795 / ADR-044 §7: @p srgb_target says @p target_fb is an `_SRGB` view built +// over local2d.flatten_srgb_rp (vk_ensure_flatten_rt's honest scratch). The +// SOURCE VIEW FOLLOWS THE TARGET, as on D3D11 (9d8b073d5): into an `_SRGB` +// attachment sample the layer as its declared format — an `_SRGB` swapchain +// decodes on sample, a UNORM one is read as the linear values it holds — so +// the blend is linear and the attachment encodes once. Into a UNORM attachment +// (legacy hatch, or a bridge plane) keep the non-decoding view: bytes pass +// through exactly as before. static void vk_flatten_one_local2d_layer(struct comp_vk_native_compositor *c, VkCommandBuffer cmd, VkFramebuffer target_fb, + bool srgb_target, struct comp_layer *layer, uint32_t region_w, uint32_t region_h) @@ -12679,10 +12853,9 @@ vk_flatten_one_local2d_layer(struct comp_vk_native_compositor *c, return; } uint32_t img_idx = layer->data.local_2d.sub.image_index; - // sRGB-passthrough: sample the layer's own view (the projection path - // samples the same). UNORM-sibling decode is a follow-up if a layer - // ever uses an _SRGB swapchain. - VkImageView src_view = (VkImageView)(uintptr_t)comp_vk_native_swapchain_get_image_view(sc, img_idx); + VkImageView src_view = + (VkImageView)(uintptr_t)(srgb_target ? comp_vk_native_swapchain_get_true_image_view(sc, img_idx) + : comp_vk_native_swapchain_get_image_view(sc, img_idx)); if (src_view == VK_NULL_HANDLE) { return; } @@ -12723,11 +12896,157 @@ vk_flatten_one_local2d_layer(struct comp_vk_native_compositor *c, } bool unpremult = (layer->data.flags & XRT_LAYER_COMPOSITION_UNPREMULTIPLIED_ALPHA_BIT) != 0; - vk_local2d_composite_flatten_draw(&c->local2d, vk, cmd, target_fb, region_w, region_h, src_view, x0, y0, - (uint32_t)(x1 - x0), (uint32_t)(y1 - y0), src_x, src_y, src_w, src_h, - unpremult); + if (srgb_target) { + vk_local2d_composite_flatten_draw_srgb(&c->local2d, vk, cmd, target_fb, region_w, region_h, src_view, + x0, y0, (uint32_t)(x1 - x0), (uint32_t)(y1 - y0), src_x, src_y, + src_w, src_h, unpremult); + } else { + vk_local2d_composite_flatten_draw(&c->local2d, vk, cmd, target_fb, region_w, region_h, src_view, x0, y0, + (uint32_t)(x1 - x0), (uint32_t)(y1 - y0), src_x, src_y, src_w, src_h, + unpremult); + } } +#ifdef XRT_OS_WINDOWS +/*! + * #1795 / ADR-044 §7 — the HONEST flatten under the weave-on-scanout split. + * + * A bridge plane is an imported typed-UNORM D3D11 texture, so it has no `_SRGB` + * view to render through. Instead the selected Local2D layers flatten into the + * private MUTABLE scratch through its `_SRGB` view — exactly the non-split + * flatten — and the region is then vkCmdCopyImage'd into the plane. That copy is + * a raw texel copy between two B8G8R8A8 images (never a blit, which would + * convert), so the plane carries the same ENCODED bytes the scratch's UNORM view + * reads, and the output-device lerp is unchanged. + * + * It records into the caller's plane command buffer, so it lands inside the + * plane-write window the direct flatten used (same timeline signal/wait, same + * keyed-mutex timing-only ordering, #1274), and like the direct flatten it only + * runs on an app frame (#868/#875). + * + * Pre: @p plane's image has just been CLEARED and is in TRANSFER_DST_OPTIMAL. + * Post (returns true): the region has been copied in and the plane is still in + * TRANSFER_DST_OPTIMAL — the caller makes it SHADER_READ. Returns false having + * recorded nothing when the honest path is unavailable (legacy hatch, no `_SRGB` + * pipelines, a plane format the raw copy cannot serve, or no mutable scratch); + * the caller then flattens straight into the plane as before. + * + * @param under true = the 2D-under layers (backdrop plane, backdrop_scratch); + * false = the over-layers (Local2D plane, local2d_scratch). + */ +static bool +vk_split_flatten_via_scratch(struct comp_vk_native_compositor *c, + VkCommandBuffer cmd, + const struct comp_vk_deposit_plane *plane, + bool under, + int32_t proj_idx, + uint32_t region_w, + uint32_t region_h) +{ + struct vk_bundle *vk = &c->vk; + if (!c->local2d_srgb_flatten_ready || plane == NULL || region_w == 0 || region_h == 0) { + return false; + } + // The scratch is B8G8R8A8: a raw copy is only the identity into the same + // texel layout and channel order. + if (plane->format != VK_FORMAT_B8G8R8A8_UNORM && plane->format != VK_FORMAT_B8G8R8A8_SRGB) { + static bool fmt_logged = false; + if (!fmt_logged) { + fmt_logged = true; + U_LOG_W("VK split: plane format %d is not B8G8R8A8 — Local2D flattens byte-passthrough (#1795)", + (int)plane->format); + } + return false; + } + + VkImage *s_img = under ? &c->backdrop_scratch : &c->local2d_scratch; + VkDeviceMemory *s_mem = under ? &c->backdrop_scratch_mem : &c->local2d_scratch_mem; + VkImageView *s_view = under ? &c->backdrop_scratch_view : &c->local2d_scratch_view; + VkImageView *s_srgb = under ? &c->backdrop_scratch_srgb_view : &c->local2d_scratch_srgb_view; + VkFramebuffer *s_fb = under ? &c->backdrop_scratch_fb : &c->local2d_scratch_fb; + uint32_t *s_w = under ? &c->backdrop_scratch_w : &c->local2d_scratch_w; + uint32_t *s_h = under ? &c->backdrop_scratch_h : &c->local2d_scratch_h; + if (*s_img != VK_NULL_HANDLE && (*s_w != region_w || *s_h != region_h) && vk->vkQueueWaitIdle != NULL) { + // On-change only (a region resize): the plane pass runs on a ring of + // in-flight command buffers, so the old scratch may still be read by + // a previous frame's copy when vk_ensure_flatten_rt frees it. + vk->vkQueueWaitIdle(vk->main_queue->queue); + } + if (!vk_ensure_flatten_rt(c, s_img, s_mem, s_view, s_srgb, s_fb, s_w, s_h, region_w, region_h, + VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_SAMPLED_BIT | + VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT, + under ? "backdrop scratch (split)" : "local2d scratch (split)") || + *s_srgb == VK_NULL_HANDLE) { + return false; + } + + static bool logged = false; + if (!logged) { + logged = true; + U_LOG_W( + "VK split: Local2D flattens in linear light into the private _SRGB scratch, raw-copied " + "into the bridge plane (format %d) (#1795)", + (int)plane->format); + } + + // Scratch: clear transparent → COLOR_ATTACHMENT, flatten, → TRANSFER_SRC. + // The source scope covers the previous frame's copy-out and flatten of this + // single-buffered scratch (WAR), in case a plane pass is still in flight. + vk_cmd_image_barrier_locked(vk, cmd, *s_img, 0, VK_ACCESS_TRANSFER_WRITE_BIT, VK_IMAGE_LAYOUT_UNDEFINED, + VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, + VK_PIPELINE_STAGE_TRANSFER_BIT | VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT, + VK_PIPELINE_STAGE_TRANSFER_BIT, k_color_sub); + VkClearColorValue transparent = {.float32 = {0.0f, 0.0f, 0.0f, 0.0f}}; + vk->vkCmdClearColorImage(cmd, *s_img, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, &transparent, 1, &k_color_sub); + vk_cmd_image_barrier_locked(vk, cmd, *s_img, VK_ACCESS_TRANSFER_WRITE_BIT, + VK_ACCESS_COLOR_ATTACHMENT_READ_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT, + VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, + VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT, + k_color_sub); + + 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_LOCAL_2D) { + continue; + } + // Exactly the direct plane flattens' selections: the backdrop takes + // every Local2D layer before the projection (vk_flatten_backdrop_2d); + // the overlay skips those unless it is a zones frame, which has no + // under/over split (vk_split_flatten_local2d). + const bool before_proj = proj_idx >= 0 && (int32_t)i < proj_idx; + const bool take = under ? before_proj : !(!c->zones_frame && before_proj); + if (!take) { + continue; + } + vk_flatten_one_local2d_layer(c, cmd, *s_fb, true, layer, region_w, region_h); + } + + vk_cmd_image_barrier_locked(vk, cmd, *s_img, VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT, VK_ACCESS_TRANSFER_READ_BIT, + VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, + VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, + k_color_sub); + + // Plane: order the copy after the caller's whole-surface clear (WAW, same + // layout), then copy the region top-left — the layout the bridge and the + // output-device composite assume (#464). + const VkImage plane_img = (VkImage)(uintptr_t)plane->image; + vk_cmd_image_barrier_locked(vk, cmd, plane_img, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, + VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, + VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, k_color_sub); + VkImageCopy region = { + .srcSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1}, + .srcOffset = {0, 0, 0}, + .dstSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1}, + .dstOffset = {0, 0, 0}, + .extent = {region_w < plane->width ? region_w : plane->width, + region_h < plane->height ? region_h : plane->height, 1}, + }; + vk->vkCmdCopyImage(cmd, *s_img, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, plane_img, + VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, ®ion); + return true; +} +#endif + // #491 part 3 — flatten the frame's 2D-UNDER Local2D layers (those BEFORE the // projection in xrEndFrame list order) into backdrop_scratch (premultiplied), // PRE-weave, and return its view + dims so the caller can hand it to the DP via @@ -12786,8 +13105,6 @@ vk_flatten_backdrop_2d(struct comp_vk_native_compositor *c, return VK_NULL_HANDLE; } - const VkFormat scratch_fmt = VK_FORMAT_B8G8R8A8_UNORM; - /* * VK-1b (#1178) — WHERE THIS FLATTENS. * @@ -12804,6 +13121,10 @@ vk_flatten_backdrop_2d(struct comp_vk_native_compositor *c, VkImage bd_image; VkFramebuffer bd_fb; VkImageView bd_view; + // #1795: only the private scratch can be `_SRGB`-viewed; the bridge plane is + // a typed-UNORM D3D11 texture, so a DIRECT plane flatten is byte-passthrough + // (the honest plane route is vk_split_flatten_via_scratch, below). + bool bd_srgb = false; #ifdef XRT_OS_WINDOWS if (plane != NULL) { bd_image = (VkImage)(uintptr_t)plane->image; @@ -12816,17 +13137,20 @@ vk_flatten_backdrop_2d(struct comp_vk_native_compositor *c, } else #endif { - if (!vk_ensure_rt(c, &c->backdrop_scratch, &c->backdrop_scratch_mem, &c->backdrop_scratch_view, - &c->backdrop_scratch_fb, &c->backdrop_scratch_w, &c->backdrop_scratch_h, region_w, - region_h, scratch_fmt, - VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_SAMPLED_BIT | - VK_IMAGE_USAGE_TRANSFER_DST_BIT, - c->local2d.flatten_rp, "backdrop scratch")) { + // #1795: same contract as the over-flatten — `_SRGB` attachment, + // encoded bytes through backdrop_scratch_view for the DP. + if (!vk_ensure_flatten_rt(c, &c->backdrop_scratch, &c->backdrop_scratch_mem, &c->backdrop_scratch_view, + &c->backdrop_scratch_srgb_view, &c->backdrop_scratch_fb, + &c->backdrop_scratch_w, &c->backdrop_scratch_h, region_w, region_h, + VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_SAMPLED_BIT | + VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT, + "backdrop scratch")) { return VK_NULL_HANDLE; } bd_image = c->backdrop_scratch; bd_view = c->backdrop_scratch_view; bd_fb = c->backdrop_scratch_fb; + bd_srgb = c->backdrop_scratch_srgb_view != VK_NULL_HANDLE; } vk_local2d_begin_frame_once(c); @@ -12839,25 +13163,41 @@ vk_flatten_backdrop_2d(struct comp_vk_native_compositor *c, VK_PIPELINE_STAGE_TRANSFER_BIT, k_color_sub); VkClearColorValue transparent = {.float32 = {0.0f, 0.0f, 0.0f, 0.0f}}; vk->vkCmdClearColorImage(cmd, bd_image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, &transparent, 1, &k_color_sub); - vk_cmd_image_barrier_locked(vk, cmd, bd_image, VK_ACCESS_TRANSFER_WRITE_BIT, - VK_ACCESS_COLOR_ATTACHMENT_READ_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT, - VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, - VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT, - k_color_sub); - // Flatten ONLY the under-layers (before the projection) into the backdrop. - for (int32_t i = 0; i < proj_idx; i++) { - struct comp_layer *layer = &c->layer_accum.layers[i]; - if (layer->data.type != XRT_LAYER_LOCAL_2D) { - continue; - } - vk_flatten_one_local2d_layer(c, cmd, bd_fb, layer, region_w, region_h); + // #1795: under the split, the honest route flattens into the private + // `_SRGB` scratch and raw-copies the region into the (cleared) plane. + bool via_scratch = false; +#ifdef XRT_OS_WINDOWS + if (plane != NULL) { + via_scratch = vk_split_flatten_via_scratch(c, cmd, plane, /*under=*/true, proj_idx, region_w, region_h); } +#endif + if (via_scratch) { + vk_cmd_image_barrier_locked(vk, cmd, bd_image, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT, + VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, + VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, VK_PIPELINE_STAGE_TRANSFER_BIT, + VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, k_color_sub); + } else { + vk_cmd_image_barrier_locked(vk, cmd, bd_image, VK_ACCESS_TRANSFER_WRITE_BIT, + VK_ACCESS_COLOR_ATTACHMENT_READ_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT, + VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, + VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, VK_PIPELINE_STAGE_TRANSFER_BIT, + VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT, k_color_sub); - vk_cmd_image_barrier_locked(vk, cmd, bd_image, VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT, - VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, - VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT, - VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, k_color_sub); + // Flatten ONLY the under-layers (before the projection) into the backdrop. + for (int32_t i = 0; i < proj_idx; i++) { + struct comp_layer *layer = &c->layer_accum.layers[i]; + if (layer->data.type != XRT_LAYER_LOCAL_2D) { + continue; + } + vk_flatten_one_local2d_layer(c, cmd, bd_fb, bd_srgb, layer, region_w, region_h); + } + + vk_cmd_image_barrier_locked( + vk, cmd, bd_image, VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT, + VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, + VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT, VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, k_color_sub); + } static bool logged = false; if (!logged) { @@ -13279,6 +13619,17 @@ vk_split_flatten_local2d(struct comp_vk_native_compositor *c, VK_PIPELINE_STAGE_TRANSFER_BIT, k_color_sub); VkClearColorValue transparent = {.float32 = {0.0f, 0.0f, 0.0f, 0.0f}}; vk->vkCmdClearColorImage(cmd, img, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, &transparent, 1, &k_color_sub); + + // #1795 / ADR-044 §7: the honest route flattens into the private `_SRGB` + // scratch and raw-copies the region into the (just cleared) plane. + if (vk_split_flatten_via_scratch(c, cmd, plane, /*under=*/false, proj_idx, region_w, region_h)) { + vk_cmd_image_barrier_locked(vk, cmd, img, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT, + VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, + VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, VK_PIPELINE_STAGE_TRANSFER_BIT, + VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, k_color_sub); + return true; + } + vk_cmd_image_barrier_locked(vk, cmd, img, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_COLOR_ATTACHMENT_READ_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, @@ -13296,7 +13647,9 @@ vk_split_flatten_local2d(struct comp_vk_native_compositor *c, if (!c->zones_frame && proj_idx >= 0 && (int32_t)i < proj_idx) { continue; } - vk_flatten_one_local2d_layer(c, cmd, c->plane_l2d_fb, layer, region_w, region_h); + // #1795: legacy route — the plane is a typed-UNORM D3D11 texture + // (no `_SRGB` view), so a direct flatten is byte-passthrough. + vk_flatten_one_local2d_layer(c, cmd, c->plane_l2d_fb, false, layer, region_w, region_h); } vk_cmd_image_barrier_locked(vk, cmd, img, VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT, @@ -13826,12 +14179,16 @@ vk_composite_local_2d(struct comp_vk_native_compositor *c, } // Resolve the `twod` source: flatten the Local2D layers into local2d_scratch. - if (!vk_ensure_rt(c, &c->local2d_scratch, &c->local2d_scratch_mem, &c->local2d_scratch_view, - &c->local2d_scratch_fb, &c->local2d_scratch_w, &c->local2d_scratch_h, region_w, - region_h, scratch_fmt, - VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_SAMPLED_BIT | - VK_IMAGE_USAGE_TRANSFER_DST_BIT, - c->local2d.flatten_rp, "local2d scratch")) { + // #1795 / ADR-044 §7: the flatten writes through the scratch's `_SRGB` + // view, so the layers blend in linear light and are encoded once on write; + // local2d_scratch_view (UNORM) still holds ENCODED bytes for the lerp + // against the raw weave snapshot. The legacy hatch keeps the UNORM scratch. + if (!vk_ensure_flatten_rt(c, &c->local2d_scratch, &c->local2d_scratch_mem, &c->local2d_scratch_view, + &c->local2d_scratch_srgb_view, &c->local2d_scratch_fb, &c->local2d_scratch_w, + &c->local2d_scratch_h, region_w, region_h, + VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_SAMPLED_BIT | + VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT, + "local2d scratch")) { return false; } // Weave snapshot scratch (the lerp reads a copy; dst is RT≠SRV). @@ -13887,7 +14244,9 @@ vk_composite_local_2d(struct comp_vk_native_compositor *c, if (!zones_frame && proj_idx >= 0 && (int32_t)i < proj_idx) { continue; } - vk_flatten_one_local2d_layer(c, cmd, c->local2d_scratch_fb, layer, region_w, region_h); + vk_flatten_one_local2d_layer(c, cmd, c->local2d_scratch_fb, + c->local2d_scratch_srgb_view != VK_NULL_HANDLE, layer, region_w, + region_h); } vk_cmd_image_barrier_locked(vk, cmd, c->local2d_scratch, VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, @@ -14448,10 +14807,10 @@ vk_release_local2d_state(struct comp_vk_native_compositor *c) c->composite_target_fb = VK_NULL_HANDLE; c->composite_target_fb_view = VK_NULL_HANDLE; } - vk_destroy_rt(c, &c->local2d_scratch, &c->local2d_scratch_mem, &c->local2d_scratch_view, - &c->local2d_scratch_fb); - vk_destroy_rt(c, &c->backdrop_scratch, &c->backdrop_scratch_mem, &c->backdrop_scratch_view, - &c->backdrop_scratch_fb); + vk_destroy_flatten_rt(c, &c->local2d_scratch, &c->local2d_scratch_mem, &c->local2d_scratch_view, + &c->local2d_scratch_srgb_view, &c->local2d_scratch_fb); + vk_destroy_flatten_rt(c, &c->backdrop_scratch, &c->backdrop_scratch_mem, &c->backdrop_scratch_view, + &c->backdrop_scratch_srgb_view, &c->backdrop_scratch_fb); vk_destroy_rt(c, &c->weave_scratch, &c->weave_scratch_mem, &c->weave_scratch_view, NULL); vk_destroy_rt(c, &c->implicit_mask_tex, &c->implicit_mask_mem, &c->implicit_mask_view, &c->implicit_mask_fb); diff --git a/src/xrt/compositor/vk_native/comp_vk_native_deposit.cpp b/src/xrt/compositor/vk_native/comp_vk_native_deposit.cpp index 898d89cf4a..86900e30f7 100644 --- a/src/xrt/compositor/vk_native/comp_vk_native_deposit.cpp +++ b/src/xrt/compositor/vk_native/comp_vk_native_deposit.cpp @@ -1089,6 +1089,7 @@ comp_vk_deposit_plane_get(struct comp_vk_deposit *dep, uint32_t plane, struct co out->view = (uint64_t)(uintptr_t)p->view; out->width = p->width; out->height = p->height; + out->format = p->format; out->generation = p->generation; return true; } diff --git a/src/xrt/compositor/vk_native/comp_vk_native_deposit.h b/src/xrt/compositor/vk_native/comp_vk_native_deposit.h index 9936dabc52..8ed712785b 100644 --- a/src/xrt/compositor/vk_native/comp_vk_native_deposit.h +++ b/src/xrt/compositor/vk_native/comp_vk_native_deposit.h @@ -367,6 +367,7 @@ struct comp_vk_deposit_plane uint64_t view; //!< `VkImageView` over @ref image. uint32_t width; //!< Allocated extent — the PANEL for the 2D planes. uint32_t height; + VkFormat format; //!< #1795: the plane image's VkFormat (gates the scratch → plane raw copy). /*! * Bumped on every REALLOCATION, never on a content change. This is what * `comp_xbridge_bind_plane`'s @p generation wants: a change re-opens the diff --git a/test_apps/handle/cube_handle_d3d11_win/main.cpp b/test_apps/handle/cube_handle_d3d11_win/main.cpp index c5f561c9a8..01d8a40a4e 100644 --- a/test_apps/handle/cube_handle_d3d11_win/main.cpp +++ b/test_apps/handle/cube_handle_d3d11_win/main.cpp @@ -790,6 +790,9 @@ struct RenderState { // (ADR-044 / INV-4.6): the CPU-authored bytes are display-referred, so a // UNORM declaration would have a format-honest compositor encode them twice. static int64_t PickPanelFormat(XrSession session, int64_t srgb, int64_t unorm) { + // A/B knob, same switch as displayxr-common's swapchains (#1795). + const char* enc = getenv("DXR_SWAPCHAIN_ENCODING"); + if (enc != nullptr && strcmp(enc, "unorm") == 0) return unorm; uint32_t n = 0; xrEnumerateSwapchainFormats(session, 0, &n, nullptr); std::vector formats(n); diff --git a/test_apps/handle/cube_handle_d3d12_win/main.cpp b/test_apps/handle/cube_handle_d3d12_win/main.cpp index 4b5792340a..81353db15d 100644 --- a/test_apps/handle/cube_handle_d3d12_win/main.cpp +++ b/test_apps/handle/cube_handle_d3d12_win/main.cpp @@ -347,6 +347,9 @@ static void UpdatePerformanceStats(PerformanceStats& stats) { // (ADR-044 / INV-4.6): the CPU-authored bytes are display-referred, so a // UNORM declaration would have a format-honest compositor encode them twice. static int64_t PickPanelFormat(XrSession session, int64_t srgb, int64_t unorm) { + // A/B knob, same switch as displayxr-common's swapchains (#1795). + const char* enc = getenv("DXR_SWAPCHAIN_ENCODING"); + if (enc != nullptr && strcmp(enc, "unorm") == 0) return unorm; uint32_t n = 0; xrEnumerateSwapchainFormats(session, 0, &n, nullptr); std::vector formats(n); diff --git a/test_apps/handle/cube_handle_gl_win/main.cpp b/test_apps/handle/cube_handle_gl_win/main.cpp index 114eefcc41..05d068a786 100644 --- a/test_apps/handle/cube_handle_gl_win/main.cpp +++ b/test_apps/handle/cube_handle_gl_win/main.cpp @@ -441,6 +441,9 @@ static void UpdatePerformanceStats(PerformanceStats& stats) { // (ADR-044 / INV-4.6): the CPU-authored bytes are display-referred, so a // UNORM declaration would have a format-honest compositor encode them twice. static int64_t PickPanelFormat(XrSession session, int64_t srgb, int64_t unorm) { + // A/B knob, same switch as displayxr-common's swapchains (#1795). + const char* enc = getenv("DXR_SWAPCHAIN_ENCODING"); + if (enc != nullptr && strcmp(enc, "unorm") == 0) return unorm; uint32_t n = 0; xrEnumerateSwapchainFormats(session, 0, &n, nullptr); std::vector formats(n); diff --git a/test_apps/handle/cube_handle_vk_win/main.cpp b/test_apps/handle/cube_handle_vk_win/main.cpp index 3fcb83ea88..41829fa498 100644 --- a/test_apps/handle/cube_handle_vk_win/main.cpp +++ b/test_apps/handle/cube_handle_vk_win/main.cpp @@ -378,6 +378,9 @@ static uint32_t FindMemoryTypeIdx(VkPhysicalDevice phys, uint32_t typeBits, VkMe // (ADR-044 / INV-4.6): the CPU-authored bytes are display-referred, so a // UNORM declaration would have a format-honest compositor encode them twice. static int64_t PickPanelFormat(XrSession session, int64_t srgb, int64_t unorm) { + // A/B knob, same switch as displayxr-common's swapchains (#1795). + const char* enc = getenv("DXR_SWAPCHAIN_ENCODING"); + if (enc != nullptr && strcmp(enc, "unorm") == 0) return unorm; uint32_t n = 0; xrEnumerateSwapchainFormats(session, 0, &n, nullptr); std::vector formats(n); diff --git a/test_apps/handle/cube_zones_d3d11_win/main.cpp b/test_apps/handle/cube_zones_d3d11_win/main.cpp index 197b11c9eb..01e589ed27 100644 --- a/test_apps/handle/cube_zones_d3d11_win/main.cpp +++ b/test_apps/handle/cube_zones_d3d11_win/main.cpp @@ -466,6 +466,9 @@ struct RenderState { // advertises one (INV-4.6); the CPU-authored bytes are display-referred, so // declaring them _SRGB keeps the compositor's decode→encode round-trip honest. static int64_t PickStripFormat(XrSessionManager& xr) { + // A/B knob, same switch as displayxr-common's swapchains (#1795). + const char* enc = getenv("DXR_SWAPCHAIN_ENCODING"); + if (enc != nullptr && strcmp(enc, "unorm") == 0) return (int64_t)DXGI_FORMAT_B8G8R8A8_UNORM; uint32_t n = 0; xrEnumerateSwapchainFormats(xr.session, 0, &n, nullptr); std::vector formats(n); diff --git a/test_apps/handle/cube_zones_d3d12_win/main.cpp b/test_apps/handle/cube_zones_d3d12_win/main.cpp index 22271d61fd..72ce1fc5b2 100644 --- a/test_apps/handle/cube_zones_d3d12_win/main.cpp +++ b/test_apps/handle/cube_zones_d3d12_win/main.cpp @@ -671,6 +671,9 @@ static void HandleZoneKeys(XrSessionManager& xr, D3D12Renderer& renderer) { // Pick the strip's swapchain format: BGRA (sRGB preferred), else first offered. static int64_t PickStripFormat(XrSessionManager& xr) { + // A/B knob, same switch as displayxr-common's swapchains (#1795). + const char* enc = getenv("DXR_SWAPCHAIN_ENCODING"); + if (enc != nullptr && strcmp(enc, "unorm") == 0) return (int64_t)DXGI_FORMAT_B8G8R8A8_UNORM; uint32_t n = 0; xrEnumerateSwapchainFormats(xr.session, 0, &n, nullptr); std::vector formats(n); diff --git a/test_apps/handle/cube_zones_gl_win/main.cpp b/test_apps/handle/cube_zones_gl_win/main.cpp index 9ef55bc56e..8e1307f40e 100644 --- a/test_apps/handle/cube_zones_gl_win/main.cpp +++ b/test_apps/handle/cube_zones_gl_win/main.cpp @@ -489,6 +489,9 @@ static void UpdatePerformanceStats(PerformanceStats& stats) { // declaring them _SRGB keeps the compositor's decode→encode round-trip honest. // GL codes: GL_SRGB8_ALPHA8=0x8C43, GL_RGBA8=0x8058. static int64_t PickStripFormat(XrSessionManager& xr) { + // A/B knob, same switch as displayxr-common's swapchains (#1795). + const char* enc = getenv("DXR_SWAPCHAIN_ENCODING"); + if (enc != nullptr && strcmp(enc, "unorm") == 0) return (int64_t)0x8058; uint32_t n = 0; xrEnumerateSwapchainFormats(xr.session, 0, &n, nullptr); std::vector formats(n); diff --git a/tests/tests_comp_color_policy.cpp b/tests/tests_comp_color_policy.cpp index ae2b50c86e..5dd7510139 100644 --- a/tests/tests_comp_color_policy.cpp +++ b/tests/tests_comp_color_policy.cpp @@ -303,7 +303,8 @@ TEST_CASE("colour: the D3D12 leg keeps its two source views apart (#1589)") // Every SRV the renderer builds over an APP image goes through that // helper; the only direct sample_format() calls left are the ones that - // genuinely hand the app's bytes on unchanged (the Local2D flatten). + // genuinely hand the app's bytes on unchanged (the Local2D flatten's + // legacy arm; #1795 picks its view by the scratch RTV, not compose_active). size_t direct = 0; size_t pos = 0; while ((pos = src.find("comp_d3d12_swapchain_sample_format(", pos)) != std::string::npos) { @@ -321,7 +322,8 @@ TEST_CASE("colour: the D3D12 leg keeps its two source views apart (#1589)") INFO("the D3D12 renderer calls comp_d3d12_swapchain_sample_format() directly " << direct << " time(s), expected 2 — one inside layer_source_format() (the fast-path arm) and one " - "in comp_d3d12_renderer_flatten_local_2d(), which hands the app's bytes on unchanged. " + "in comp_d3d12_renderer_flatten_local_2d(), whose legacy (UNORM-RTV) arm hands the app's " + "bytes on unchanged — its `_SRGB`-RTV arm takes the honest view (#1795). " "A new direct call is a draw that blends in the wrong space."); CHECK(direct == 2); } @@ -372,6 +374,21 @@ TEST_CASE("colour: the GL leg spells the same model in GL (#1589/#1610)") CHECK(publish.find(forbidden) == std::string::npos); } + // #1795: the Local2D flatten's linear-blend target reaches its GL_RGBA8 + // scratch the same way — and for the same reason — as the compose target + // reaches the atlas. + const std::string l2d_publish = function_body(src, "gl_publish_local2d_linear"); + INFO("gl_publish_local2d_linear() must exist — the Local2D flatten's publish is one named step"); + REQUIRE_FALSE(l2d_publish.empty()); + INFO("...and must publish with glCopyImageSubData(): the scratch must receive the encoded " + "bytes the GL_SRGB8_ALPHA8 flatten target wrote, verbatim"); + CHECK(l2d_publish.find("glCopyImageSubData(") != std::string::npos); + for (const char *forbidden : {"glDrawArrays", "glDrawElements", "glBlitFramebuffer"}) { + INFO("gl_publish_local2d_linear() calls " << forbidden + << " — the publish must be a raw same-class copy"); + CHECK(l2d_publish.find(forbidden) == std::string::npos); + } + INFO("the GL compositor must pick its source read through ONE helper, so a new bind site " "cannot quietly keep the non-decoding read while the target encodes"); CHECK(contains_in_code(src, "gl_bind_layer_source("));