diff --git a/src/xrt/auxiliary/d3d/d3d_dxgi_formats.h b/src/xrt/auxiliary/d3d/d3d_dxgi_formats.h index d03397312..676ff1dd0 100644 --- a/src/xrt/auxiliary/d3d/d3d_dxgi_formats.h +++ b/src/xrt/auxiliary/d3d/d3d_dxgi_formats.h @@ -81,6 +81,53 @@ d3d_dxgi_format_to_typeless_dxgi(DXGI_FORMAT format) } } +/*! + * Map an sRGB DXGI format to its plain UNORM sibling (identity for non-sRGB). + * + * Used to build the runtime's *internal* sampling view of an app color + * swapchain so the GPU does NOT auto-decode sRGB->linear when the compositor + * samples it. The display processor expects display-referred (sRGB-encoded) + * bytes, so the compositor must pass the app's bytes through unchanged rather + * than linearizing them (which would arrive ~2.2x too dark). Mirrors the GL + * GL_TEXTURE_SRGB_DECODE_EXT=GL_SKIP_DECODE_EXT fix. + */ +static inline DXGI_FORMAT +d3d_dxgi_format_srgb_to_unorm(DXGI_FORMAT format) +{ + switch (format) { + case DXGI_FORMAT_R8G8B8A8_UNORM_SRGB: return DXGI_FORMAT_R8G8B8A8_UNORM; + case DXGI_FORMAT_B8G8R8A8_UNORM_SRGB: return DXGI_FORMAT_B8G8R8A8_UNORM; + default: return format; + } +} + +/*! + * Pick the format for the runtime's INTERNAL sampling SRV of an app color + * swapchain so the GPU does NOT auto-decode sRGB->linear. Maps the 8-bit + * BGRA/RGBA family — whether the resource is TYPELESS, sRGB, or already UNORM — + * to its plain UNORM form. Identity for everything else. Used by the D3D12 + * compositor where the swapchain resource is promoted to TYPELESS so the + * runtime SRV can reinterpret it as UNORM (a concrete sRGB resource can't be + * SRV-cast in D3D12). See d3d_dxgi_format_srgb_to_unorm for the comment on why + * passthrough (no decode) is correct. + */ +static inline DXGI_FORMAT +d3d_dxgi_format_to_unorm_sample(DXGI_FORMAT format) +{ + switch (format) { + case DXGI_FORMAT_R8G8B8A8_TYPELESS: + case DXGI_FORMAT_R8G8B8A8_UNORM_SRGB: + case DXGI_FORMAT_R8G8B8A8_UNORM: + return DXGI_FORMAT_R8G8B8A8_UNORM; + case DXGI_FORMAT_B8G8R8A8_TYPELESS: + case DXGI_FORMAT_B8G8R8A8_UNORM_SRGB: + case DXGI_FORMAT_B8G8R8A8_UNORM: + return DXGI_FORMAT_B8G8R8A8_UNORM; + default: + return format; + } +} + static inline int64_t d3d_dxgi_format_to_vk(DXGI_FORMAT format) { diff --git a/src/xrt/compositor/d3d11/comp_d3d11_swapchain.cpp b/src/xrt/compositor/d3d11/comp_d3d11_swapchain.cpp index c4895981a..f65a44ebb 100644 --- a/src/xrt/compositor/d3d11/comp_d3d11_swapchain.cpp +++ b/src/xrt/compositor/d3d11/comp_d3d11_swapchain.cpp @@ -408,8 +408,17 @@ comp_d3d11_swapchain_create(struct comp_d3d11_compositor *c, DXGI_FORMAT typeless = d3d_dxgi_format_to_typeless_dxgi(dxgi_format); if (typeless != dxgi_format) { texture_format = typeless; - // srv_format and rtv_format remain as the original concrete format + // rtv_format remains the original concrete format so the app's own + // render path (incl. any sRGB encode it relies on) is unchanged. } + // The runtime's internal SRV samples this image to build the composited + // atlas handed to the display processor. Use the UNORM sibling of an + // sRGB format so the sample does NOT auto-decode sRGB->linear: the DP + // expects display-referred (sRGB-encoded) bytes, so pass the app's + // bytes through unchanged. No-op for already-UNORM formats. Mirrors the + // GL GL_SKIP_DECODE_EXT fix. (Only the composited path samples this SRV; + // the single-layer zero-copy path hands the texture straight to the DP.) + srv_format = d3d_dxgi_format_srgb_to_unorm(srv_format); } // Create textures diff --git a/src/xrt/compositor/d3d12/comp_d3d12_renderer.cpp b/src/xrt/compositor/d3d12/comp_d3d12_renderer.cpp index af61e7def..3645b2785 100644 --- a/src/xrt/compositor/d3d12/comp_d3d12_renderer.cpp +++ b/src/xrt/compositor/d3d12/comp_d3d12_renderer.cpp @@ -13,6 +13,7 @@ #include "util/comp_layer_accum.h" #include "util/u_logging.h" +#include "d3d/d3d_dxgi_formats.h" #include "math/m_api.h" #define WIN32_LEAN_AND_MEAN @@ -402,7 +403,11 @@ render_window_space_layer(struct comp_d3d12_renderer *r, D3D12_SHADER_RESOURCE_VIEW_DESC srv_desc = {}; D3D12_RESOURCE_DESC res_desc = src_resource->GetDesc(); - srv_desc.Format = res_desc.Format; + // Sample app color swapchains as UNORM (not their sRGB sibling) so the GPU + // does NOT auto-decode sRGB->linear; the DP wants display-referred bytes, so + // pass them through. Resource is TYPELESS for 8-bit color (see swapchain), + // which this maps to UNORM; identity for other/non-color formats. + srv_desc.Format = d3d_dxgi_format_to_unorm_sample(res_desc.Format); srv_desc.ViewDimension = D3D12_SRV_DIMENSION_TEXTURE2D; srv_desc.Texture2D.MipLevels = 1; srv_desc.Shader4ComponentMapping = D3D12_DEFAULT_SHADER_4_COMPONENT_MAPPING; @@ -1016,7 +1021,9 @@ comp_d3d12_renderer_draw_projection_pass(struct comp_d3d12_renderer *renderer, } D3D12_SHADER_RESOURCE_VIEW_DESC srv_desc = {}; - srv_desc.Format = src_desc.Format; + // UNORM sample (see the other SRV site): no sRGB auto-decode; the + // app's display-referred bytes pass through to the DP unchanged. + srv_desc.Format = d3d_dxgi_format_to_unorm_sample(src_desc.Format); srv_desc.ViewDimension = D3D12_SRV_DIMENSION_TEXTURE2D; srv_desc.Texture2D.MipLevels = 1; srv_desc.Shader4ComponentMapping = D3D12_DEFAULT_SHADER_4_COMPONENT_MAPPING; diff --git a/src/xrt/compositor/d3d12/comp_d3d12_swapchain.cpp b/src/xrt/compositor/d3d12/comp_d3d12_swapchain.cpp index 6cbbe45da..551971534 100644 --- a/src/xrt/compositor/d3d12/comp_d3d12_swapchain.cpp +++ b/src/xrt/compositor/d3d12/comp_d3d12_swapchain.cpp @@ -263,6 +263,27 @@ comp_d3d12_swapchain_create(struct comp_d3d12_compositor *c, default: break; } + } else { + // For 8-bit color formats, create the resource TYPELESS so the runtime + // can build a UNORM sampling SRV that does NOT auto-decode sRGB->linear + // when compositing (the DP wants display-referred bytes; pass them + // through unchanged). The app still creates its own typed RTV from the + // format it requested. Bounded to the 8-bit BGRA/RGBA family where the + // TYPELESS->UNORM mapping is unambiguous; other formats stay concrete. + // (Unlike D3D11, D3D12 cannot SRV-cast a concrete sRGB resource, so the + // TYPELESS promotion is required here.) Mirrors the GL skip-decode fix. + switch (dxgi_format) { + case DXGI_FORMAT_R8G8B8A8_UNORM: + case DXGI_FORMAT_R8G8B8A8_UNORM_SRGB: + resource_format = DXGI_FORMAT_R8G8B8A8_TYPELESS; + break; + case DXGI_FORMAT_B8G8R8A8_UNORM: + case DXGI_FORMAT_B8G8R8A8_UNORM_SRGB: + resource_format = DXGI_FORMAT_B8G8R8A8_TYPELESS; + break; + default: + break; + } } // Create committed resources diff --git a/src/xrt/compositor/metal/comp_metal_compositor.m b/src/xrt/compositor/metal/comp_metal_compositor.m index 5a8659651..e5cfff06e 100644 --- a/src/xrt/compositor/metal/comp_metal_compositor.m +++ b/src/xrt/compositor/metal/comp_metal_compositor.m @@ -324,6 +324,20 @@ * */ +// Map an sRGB Metal pixel format to its plain UNORM sibling (identity +// otherwise). Used to sample an app sRGB swapchain through a non-decoding view +// so the compositor passes the app's display-referred bytes through to the DP +// unchanged. Mirrors the GL/D3D/VK sRGB-passthrough fixes. +static MTLPixelFormat +metal_srgb_to_unorm(MTLPixelFormat format) +{ + switch (format) { + case MTLPixelFormatRGBA8Unorm_sRGB: return MTLPixelFormatRGBA8Unorm; + case MTLPixelFormatBGRA8Unorm_sRGB: return MTLPixelFormatBGRA8Unorm; + default: return format; + } +} + static MTLPixelFormat xrt_format_to_metal(int64_t format) { @@ -912,7 +926,12 @@ - (BOOL)wantsUpdateLayer width:info->width height:info->height mipmapped:(info->mip_count > 1) ? YES : NO]; - desc.usage = MTLTextureUsageRenderTarget | MTLTextureUsageShaderRead; + // PixelFormatView lets the compositor sample an sRGB swapchain through a + // UNORM view (sRGB passthrough — see the compose pass) without the GPU + // auto-decoding sRGB->linear. The app's own render target keeps the sRGB + // format, so its encoding (if any) is unchanged. + desc.usage = MTLTextureUsageRenderTarget | MTLTextureUsageShaderRead | + MTLTextureUsagePixelFormatView; desc.storageMode = MTLStorageModeShared; if (info->array_size > 1) { desc.textureType = MTLTextureType2DArray; @@ -1657,6 +1676,18 @@ - (BOOL)wantsUpdateLayer if (src_tex == nil) { continue; } + // sRGB passthrough: sample an app sRGB swapchain through a UNORM + // view so the GPU does not auto-decode sRGB->linear; the DP wants + // display-referred bytes. No-op for UNORM. Mirrors GL/D3D/VK. + { + MTLPixelFormat unorm_fmt = metal_srgb_to_unorm(src_tex.pixelFormat); + if (unorm_fmt != src_tex.pixelFormat) { + id v = [src_tex newTextureViewWithPixelFormat:unorm_fmt]; + if (v != nil) { + src_tex = v; + } + } + } // Use sub-image norm_rect to sample correct region of source texture struct xrt_normalized_rect nr = layer->data.proj.v[eye].sub.norm_rect; @@ -1790,6 +1821,16 @@ - (BOOL)wantsUpdateLayer if (src_tex == nil) { continue; } + // sRGB passthrough (see projection pass): sample through a UNORM view. + { + MTLPixelFormat unorm_fmt = metal_srgb_to_unorm(src_tex.pixelFormat); + if (unorm_fmt != src_tex.pixelFormat) { + id v = [src_tex newTextureViewWithPixelFormat:unorm_fmt]; + if (v != nil) { + src_tex = v; + } + } + } // Source UV sub-rect (default to full texture if not specified) struct xrt_normalized_rect nr = ws->sub.norm_rect; diff --git a/src/xrt/compositor/vk_native/comp_vk_native_swapchain.c b/src/xrt/compositor/vk_native/comp_vk_native_swapchain.c index 1e32aa836..c92fb3b9f 100644 --- a/src/xrt/compositor/vk_native/comp_vk_native_swapchain.c +++ b/src/xrt/compositor/vk_native/comp_vk_native_swapchain.c @@ -258,10 +258,43 @@ comp_vk_native_swapchain_create(struct comp_vk_native_compositor *c, usage |= VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT; } + // sRGB passthrough (mirrors the GL/D3D11/D3D12 fixes): the compose + // vkCmdBlitImage reads the source in the IMAGE's format, so an sRGB image + // would auto-decode sRGB->linear with no re-encode into the UNORM atlas, + // and the DP (which wants display-referred bytes) would get ~2.2x-too-dark + // content. There is no view to retag for a blit, so create the color image + // in the UNORM sibling — the blit then passes the app's stored bytes through + // unchanged. Expose the requested sRGB format via MUTABLE_FORMAT + a format + // list so the app can still create an sRGB view to render with encode. + VkFormat image_format = vk_format; + VkFormat srgb_view_format = VK_FORMAT_UNDEFINED; + if (!depth) { + switch (vk_format) { + case VK_FORMAT_R8G8B8A8_SRGB: + image_format = VK_FORMAT_R8G8B8A8_UNORM; + srgb_view_format = vk_format; + break; + case VK_FORMAT_B8G8R8A8_SRGB: + image_format = VK_FORMAT_B8G8R8A8_UNORM; + srgb_view_format = vk_format; + break; + default: break; + } + } + const bool mutable_srgb = (srgb_view_format != VK_FORMAT_UNDEFINED); + VkFormat view_format_list[2] = {image_format, srgb_view_format}; + VkImageFormatListCreateInfo format_list_ci = { + .sType = VK_STRUCTURE_TYPE_IMAGE_FORMAT_LIST_CREATE_INFO, + .viewFormatCount = 2, + .pViewFormats = view_format_list, + }; + VkImageCreateInfo image_ci = { .sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO, + .pNext = mutable_srgb ? &format_list_ci : NULL, + .flags = mutable_srgb ? VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT : 0, .imageType = VK_IMAGE_TYPE_2D, - .format = vk_format, + .format = image_format, .extent = {info->width, info->height, 1}, .mipLevels = info->mip_count > 0 ? info->mip_count : 1, .arrayLayers = info->array_size > 0 ? info->array_size : 1, @@ -321,12 +354,13 @@ comp_vk_native_swapchain_create(struct comp_vk_native_compositor *c, return XRT_ERROR_VULKAN; } - // Create image view + // Create image view (UNORM base for sRGB swapchains — passthrough, no + // sample-time decode; see the image-format note above). VkImageViewCreateInfo view_ci = { .sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO, .image = sc->images[i], .viewType = view_type, - .format = vk_format, + .format = image_format, .subresourceRange = { .aspectMask = aspect, .baseMipLevel = 0,