diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/Sampler.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/Sampler.hpp index 76804ccfc..d7749e2da 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/Sampler.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/Sampler.hpp @@ -66,7 +66,7 @@ private: }; void RequireFilterMinmax(const Context& context, VkFormat format, std::uint32_t samplerMask, std::span> samplers); -void RequireDegammaFormat(std::uint32_t guestFormat, std::uint32_t samplerMask, std::span> samplers); +bool ForcesDegammaOn(std::uint32_t guestFormat, std::uint32_t samplerMask, std::span> samplers); void RequireBorderSwizzle(std::uint32_t bcSwizzle, std::uint32_t samplerMask, std::span> samplers); void RequireNonSeamlessCube(bool cube, std::uint32_t samplerMask, std::span> samplers); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp index 51d1b8694..897399d08 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp @@ -466,6 +466,7 @@ private: std::vector> textures; std::vector textureFirstLayer; std::vector textureBaseLevel; + std::vector textureSrgb; std::vector> storageTextures; std::vector storageMips; std::vector storageKeys; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp index a8eb2efeb..5b65edf1e 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp @@ -81,6 +81,8 @@ public: VkImageView View() const; VkImageView FirstLayerView() const { return firstLayerView; } VkImageView BaseLevelView() const; + bool HasSrgbView() const { return srgbInfo.has_value(); } + VkImageView SrgbView(bool firstLayer, bool baseLevel) const; struct ViewRange { VkImageViewType type; std::uint32_t levels; @@ -108,6 +110,7 @@ private: void release() noexcept; void createFirstLayerView(const GuestTextureResource& descriptor, VkImageViewCreateInfo viewInfo); void prepareBaseLevelView(const GuestTextureResource& descriptor, VkImageViewCreateInfo viewInfo); + void prepareSrgbView(const GuestTextureResource& descriptor, VkImageViewCreateInfo viewInfo); // Held by value: cached textures outlive the Context of the draw that created them. Context context; @@ -122,6 +125,10 @@ private: std::optional baseLevelInfo; mutable std::mutex baseLevelMutex; mutable VkImageView baseLevelView = VK_NULL_HANDLE; + std::optional srgbInfo; + float srgbMinLod = 0.0f; + mutable std::mutex srgbMutex; + mutable std::array srgbViews{}; VkImageLayout layout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; VkDeviceSize allocationBytes = 0; VkDeviceSize countedBytes = 0; @@ -137,6 +144,7 @@ private: }; VkFormat AttachmentProxyFormat(const Context& context, VkFormat format); +VkFormat SrgbTwinFormat(const Context& context, VkFormat format); // A guest texture a shader writes through a storage image. It is uploaded like a sampled texture; // after the GPU work completes its results are stored to guest memory (retiled, changed bytes only), diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Sampler.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Sampler.cpp index 3bf2f9556..214fcaf05 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Sampler.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Sampler.cpp @@ -113,10 +113,12 @@ namespace AgcDriver::Graphics { return requiresNonSeamlessCube; } - void RequireDegammaFormat(std::uint32_t guestFormat, std::uint32_t samplerMask, std::span> samplers) { + bool ForcesDegammaOn(std::uint32_t guestFormat, std::uint32_t samplerMask, std::span> samplers) { + if (IsSrgbTextureFormat(guestFormat)) return false; for (std::uint32_t element = 0; element < 32u && element < samplers.size(); ++element) { - if (((samplerMask >> element) & 1u) != 0u && samplers[element]->ForcesDegamma() && !IsSrgbTextureFormat(guestFormat)) Require(false, "a texture of guest format " + std::to_string(guestFormat) + ", which is not sRGB, is sampled through a sampler that forces sRGB decoding, which is not implemented"); + if (((samplerMask >> element) & 1u) != 0u && samplers[element]->ForcesDegamma()) return true; } + return false; } void RequireBorderSwizzle(std::uint32_t bcSwizzle, std::uint32_t samplerMask, std::span> samplers) { diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp index 552145a60..4bef1d859 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp @@ -1617,7 +1617,7 @@ void ShaderResources::buildComplete() { write.pImageInfo = images.data() + images.size(); for (const auto index : binding.imageAllocations) { if (index == std::numeric_limits::max()) images.push_back(absentImage(binding.layout.binding)); - else images.push_back({VK_NULL_HANDLE, textureBaseLevel[index] ? textures[index]->BaseLevelView() : textureFirstLayer[index] ? textures[index]->FirstLayerView() : textures[index]->View(), textures[index]->Layout()}); + else images.push_back({VK_NULL_HANDLE, textureSrgb[index] ? textures[index]->SrgbView(textureFirstLayer[index], textureBaseLevel[index]) : textureBaseLevel[index] ? textures[index]->BaseLevelView() : textureFirstLayer[index] ? textures[index]->FirstLayerView() : textures[index]->View(), textures[index]->Layout()}); } Require(binding.imageAllocations.size() == binding.layout.descriptorCount, "descriptor allocations disagree with compact binding"); break; @@ -3308,7 +3308,7 @@ void ShaderResources::resolveImageBinding(const ShaderRecompiler::DescriptorBind continue; } const auto resource = record != nullptr && record->decoded ? record->resource : DecodeTextureResource(words); - RequireDegammaFormat(resource.format, binding.imageSamplers[element], shaderSamplers); + const bool forcedSrgb = ForcesDegammaOn(resource.format, binding.imageSamplers[element], shaderSamplers); RequireNonSeamlessCube(resource.dimension == TextureDimension::kCube, binding.imageSamplers[element], shaderSamplers); const bool firstLayer = binding.imageShape == ShaderRecompiler::DescriptorImageShape::Image2D && resource.dimension == TextureDimension::k2DArray; if (!firstLayer && !MatchesGuestDimension(*binding.imageShape, resource.dimension)) throw std::runtime_error("AGC graphics: guest texture dimension disagrees with the shader's declared image shape (shape " + std::to_string(static_cast(*binding.imageShape)) + ", dimension " + std::to_string(static_cast(resource.dimension)) + ")"); @@ -3335,6 +3335,7 @@ void ShaderResources::resolveImageBinding(const ShaderRecompiler::DescriptorBind } RequireFilterMinmax(context, texture->ViewFormat(), binding.imageSamplers[element], shaderSamplers); RequireBorderSwizzle(resource.bcSwizzle, binding.imageSamplers[element], shaderSamplers); + if (forcedSrgb && !texture->HasSrgbView()) Require(false, "a texture of guest format " + std::to_string(resource.format) + ", which is not sRGB and has no sRGB view, is sampled through a sampler that forces sRGB decoding, which is not implemented"); if (!shaderSamplers.empty()) { const bool singleLevel = DescriptorSingleLevel(words); const auto firstSampler = static_cast(shaderSamplers.data() - samplers.data()); @@ -3352,6 +3353,7 @@ void ShaderResources::resolveImageBinding(const ShaderRecompiler::DescriptorBind textures.push_back(std::move(texture)); textureFirstLayer.push_back(firstLayer); textureBaseLevel.push_back(baseLevel); + textureSrgb.push_back(forcedSrgb); describedRanges.push_back({"texture", resource.baseAddress, guestBytes, resource.width, resource.height, resource.format, static_cast(resource.tileMode), resource.dccAddress}); item.imageAllocations.push_back(textures.size() - 1); } diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp index c6bed0bba..b82b62c9a 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp @@ -11,6 +11,7 @@ #include "prx/libSceAgcDriver/Graphics/include/TextureTiling.hpp" #include "prx/libSceAgcDriver/Graphics/include/UnitShadow.hpp" #include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "RdnaDecoder/include/RdnaDecoder/RdnaDescriptorFormat.hpp" #include #include #include @@ -267,7 +268,7 @@ Texture::Texture(const Context& context, TextureDetiler& detiler, const GuestTex const auto linearBytes = sliceLinearBytes * arrayLayers; VkImageCreateInfo imageInfo{VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO}; - imageInfo.flags = descriptor.dimension == TextureDimension::kCube ? VK_IMAGE_CREATE_CUBE_COMPATIBLE_BIT : 0u; + imageInfo.flags = (descriptor.dimension == TextureDimension::kCube ? VK_IMAGE_CREATE_CUBE_COMPATIBLE_BIT : 0u) | (SrgbTwinFormat(context, vkFormat) != VK_FORMAT_UNDEFINED ? VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT : 0u); imageInfo.imageType = ImageTypeFor(descriptor.dimension); imageInfo.format = vkFormat; imageInfo.extent = {descriptor.width, descriptor.height, geometry.imageDepth}; @@ -446,6 +447,7 @@ Texture::Texture(const Context& context, TextureDetiler& detiler, const GuestTex viewRange = {viewInfo.viewType, viewInfo.subresourceRange.levelCount, viewInfo.subresourceRange.layerCount}; createFirstLayerView(descriptor, viewInfo); prepareBaseLevelView(descriptor, viewInfo); + prepareSrgbView(descriptor, viewInfo); if (profile) { auto& totals = Profile(); totals.view += timer.lap(); @@ -648,6 +650,7 @@ Texture::Texture(const Context& context, const std::shared_ptr& viewRange = {viewInfo.viewType, viewInfo.subresourceRange.levelCount, viewInfo.subresourceRange.layerCount}; createFirstLayerView(descriptor, viewInfo); prepareBaseLevelView(descriptor, viewInfo); + prepareSrgbView(descriptor, viewInfo); if (profile) { auto& totals = Profile(); totals.view += timer.lap(); @@ -700,6 +703,44 @@ void Texture::prepareBaseLevelView(const GuestTextureResource& descriptor, VkIma baseLevelInfo = viewInfo; } +void Texture::prepareSrgbView(const GuestTextureResource& descriptor, VkImageViewCreateInfo viewInfo) { + const auto format = SrgbTwinFormat(context, viewInfo.format); + if (format == VK_FORMAT_UNDEFINED) return; + viewInfo.pNext = nullptr; + viewInfo.format = format; + srgbInfo = viewInfo; + srgbMinLod = EffectiveMinLod(descriptor); +} + +VkImageView Texture::SrgbView(bool firstLayer, bool baseLevel) const { + Require(srgbInfo.has_value(), "AGC graphics: a texture of view format " + std::to_string(viewFormat) + " has no sRGB view for a sampler that forces sRGB decoding"); + std::lock_guard lock(srgbMutex); + auto& slot = srgbViews[baseLevel ? 2u : firstLayer ? 1u : 0u]; + if (slot != VK_NULL_HANDLE) return slot; + VkImageViewCreateInfo info = *srgbInfo; + VkImageViewUsageCreateInfo usage{VK_STRUCTURE_TYPE_IMAGE_VIEW_USAGE_CREATE_INFO}; + usage.usage = VK_IMAGE_USAGE_SAMPLED_BIT; + info.pNext = &usage; + VkImageViewMinLodCreateInfoEXT minLod{VK_STRUCTURE_TYPE_IMAGE_VIEW_MIN_LOD_CREATE_INFO_EXT}; + if (baseLevel) { + info.viewType = info.viewType == VK_IMAGE_VIEW_TYPE_1D || info.viewType == VK_IMAGE_VIEW_TYPE_1D_ARRAY ? VK_IMAGE_VIEW_TYPE_1D : VK_IMAGE_VIEW_TYPE_2D; + info.subresourceRange.levelCount = 1; + info.subresourceRange.layerCount = 1; + } else { + if (firstLayer) { + info.viewType = VK_IMAGE_VIEW_TYPE_2D; + info.subresourceRange.layerCount = 1; + } + if (srgbMinLod != 0.0f) { + minLod.minLod = srgbMinLod; + minLod.pNext = info.pNext; + info.pNext = &minLod; + } + } + Check(context.Function("vkCreateImageView")(context.device, &info, nullptr, &slot), "vkCreateImageView sRGB"); + return slot; +} + VkImageView Texture::BaseLevelView() const { Require(baseLevelInfo.has_value(), "a base-level view needs a mipmapped 1D, 2D or 2D array texture"); std::lock_guard lock(baseLevelMutex); @@ -715,6 +756,10 @@ void Texture::release() noexcept { firstLayerView = VK_NULL_HANDLE; if (baseLevelView) context.Function("vkDestroyImageView")(context.device, baseLevelView, nullptr); baseLevelView = VK_NULL_HANDLE; + for (auto& srgbView : srgbViews) { + if (srgbView) context.Function("vkDestroyImageView")(context.device, srgbView, nullptr); + srgbView = VK_NULL_HANDLE; + } // The image and its memory go with the last holder: this texture, or the batch still uploading it. owned.reset(); image = VK_NULL_HANDLE; @@ -851,6 +896,23 @@ bool StorageClearAvailable(const Context& context, std::uint32_t guestFormat, Dc return IsDccClear(keys) && StorageFormatAvailable(context, guestFormat) && ClearColorFor(StorageFormatForGuest(context, guestFormat), keys, clear); } +VkFormat SrgbTwinFormat(const Context& context, VkFormat format) { + const auto decoded = [&](ShaderRecompiler::IrBufferFormat srgb) { return (context.srgbDecodeFormats & ShaderRecompiler::SrgbDecodeBit(srgb)) != 0u; }; + switch (format) { + case VK_FORMAT_R8_UNORM: return decoded(ShaderRecompiler::IrBufferFormat::Format8Srgb) ? VK_FORMAT_UNDEFINED : VK_FORMAT_R8_SRGB; + case VK_FORMAT_R8G8_UNORM: return decoded(ShaderRecompiler::IrBufferFormat::Format8_8Srgb) ? VK_FORMAT_UNDEFINED : VK_FORMAT_R8G8_SRGB; + case VK_FORMAT_R8G8B8A8_UNORM: return VK_FORMAT_R8G8B8A8_SRGB; + case VK_FORMAT_B8G8R8A8_UNORM: return VK_FORMAT_B8G8R8A8_SRGB; + case VK_FORMAT_A8B8G8R8_UNORM_PACK32: return VK_FORMAT_A8B8G8R8_SRGB_PACK32; + case VK_FORMAT_BC1_RGB_UNORM_BLOCK: return VK_FORMAT_BC1_RGB_SRGB_BLOCK; + case VK_FORMAT_BC1_RGBA_UNORM_BLOCK: return VK_FORMAT_BC1_RGBA_SRGB_BLOCK; + case VK_FORMAT_BC2_UNORM_BLOCK: return VK_FORMAT_BC2_SRGB_BLOCK; + case VK_FORMAT_BC3_UNORM_BLOCK: return VK_FORMAT_BC3_SRGB_BLOCK; + case VK_FORMAT_BC7_UNORM_BLOCK: return VK_FORMAT_BC7_SRGB_BLOCK; + default: return VK_FORMAT_UNDEFINED; + } +} + VkFormat AttachmentProxyFormat(const Context& context, VkFormat format) { if (format != VK_FORMAT_R8_SRGB) return VK_FORMAT_UNDEFINED; struct Table { diff --git a/core/libs/prx/libSceAgcDriver/tests/TextureFormat.cpp b/core/libs/prx/libSceAgcDriver/tests/TextureFormat.cpp index 33d1a9f45..2f3d85b93 100644 --- a/core/libs/prx/libSceAgcDriver/tests/TextureFormat.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/TextureFormat.cpp @@ -1,6 +1,7 @@ #include "GraphicsTests.hpp" #include "prx/libSceAgcDriver/Graphics/include/DccMetadata.hpp" #include "prx/libSceAgcDriver/Graphics/include/GuestTextureResource.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Texture.hpp" #include "prx/libSceAgcDriver/Graphics/include/TextureFormat.hpp" #include "RdnaDecoder/RdnaDescriptorFormat.hpp" #include @@ -48,6 +49,20 @@ void srgbDecodeTests() { Require(SampledTextureFormat(context, 130) == VK_FORMAT_R8G8B8A8_SRGB && SampledTextureFormat(context, 14) == VK_FORMAT_R8G8_UNORM && SampledTextureFormat(context, 170) == VK_FORMAT_BC1_RGBA_SRGB_BLOCK, "formats without a shader decode must keep their views"); } +void srgbTwinTests() { + constexpr std::uint32_t srgb8 = 1u; + constexpr std::uint32_t srgb8_8 = 2u; + Context context{}; + Require(SrgbTwinFormat(context, VK_FORMAT_R8_UNORM) == VK_FORMAT_R8_SRGB && SrgbTwinFormat(context, VK_FORMAT_R8G8_UNORM) == VK_FORMAT_R8G8_SRGB, "a device that samples R8_SRGB and R8G8_SRGB must give R8 and R8G8 their sRGB twins"); + context.srgbDecodeFormats = srgb8_8; + Require(SrgbTwinFormat(context, VK_FORMAT_R8G8_UNORM) == VK_FORMAT_UNDEFINED, "a device that decodes 8_8_SRGB in the shader must not give R8G8 an sRGB twin"); + Require(SrgbTwinFormat(context, VK_FORMAT_R8_UNORM) == VK_FORMAT_R8_SRGB, "R8 must keep its sRGB twin when only 8_8_SRGB is decoded in the shader"); + context.srgbDecodeFormats = srgb8 | srgb8_8; + Require(SrgbTwinFormat(context, VK_FORMAT_R8_UNORM) == VK_FORMAT_UNDEFINED, "a device that decodes 8_SRGB in the shader must not give R8 an sRGB twin"); + Require(SrgbTwinFormat(context, VK_FORMAT_R8G8B8A8_UNORM) == VK_FORMAT_R8G8B8A8_SRGB && SrgbTwinFormat(context, VK_FORMAT_BC7_UNORM_BLOCK) == VK_FORMAT_BC7_SRGB_BLOCK, "formats without a shader decode must keep their sRGB twins"); + Require(SrgbTwinFormat(context, VK_FORMAT_R32_SFLOAT) == VK_FORMAT_UNDEFINED, "formats without an sRGB counterpart have no twin"); +} + template void reject(TAction action, std::string_view reason) { try { @@ -146,6 +161,7 @@ void packedFloatDccClearTests() { void RunTextureFormatTests() { srgbDecodeTests(); + srgbTwinTests(); packedChannelTests(); Require(ResolveTextureFormat(1) == VK_FORMAT_R8_UNORM, "format 1 must resolve to R8_UNORM"); Require(ResolveTextureFormat(2) == VK_FORMAT_R8_SNORM, "format 2 must resolve to R8_SNORM"); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/ImageSampleForceDegamma.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ImageSampleForceDegamma.cpp index e106d7cfa..bab71fa85 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/ImageSampleForceDegamma.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ImageSampleForceDegamma.cpp @@ -24,6 +24,7 @@ constexpr std::uint32_t Threads = 32; constexpr std::uint32_t Size = 4; constexpr std::uint32_t Format8888UNorm = 56; constexpr std::uint32_t Format8888Srgb = 130; +constexpr std::uint32_t Format32Float = 22; constexpr std::uint32_t Type2D = 9; constexpr std::uint32_t FilterPoint = 0; constexpr std::uint32_t FilterBilinear = 1; @@ -126,7 +127,7 @@ void RequireSame(const std::array& forced, const std::array< } } -void RequireDecoded(const std::array& sampled) { +void RequireDecoded(const std::array& sampled, const char* name) { for (std::uint32_t tid = 0; tid < Threads; ++tid) { const auto x = tid % Size; const auto y = (tid / Size) % Size; @@ -134,7 +135,7 @@ void RequireDecoded(const std::array& sampled) { const auto code = Code8(x, y, channel); const float expected = channel == 3u ? static_cast(code) / 255.0f : SrgbToLinear(code); const float value = sampled[tid * 4u + channel]; - Require(std::fabs(value - expected) <= 0.005f, "8_8_8_8_SRGB through FORCE_DEGAMMA: thread " + std::to_string(tid) + " component " + std::to_string(channel) + " sampled " + std::to_string(value) + ", expected " + std::to_string(expected)); + Require(std::fabs(value - expected) <= 0.005f, std::string(name) + " through FORCE_DEGAMMA: thread " + std::to_string(tid) + " component " + std::to_string(channel) + " sampled " + std::to_string(value) + ", expected " + std::to_string(expected)); } } } @@ -158,13 +159,21 @@ int main() { FillTexels(); FillInput(true); const auto forcedPoint = Sample(*device, Format8888Srgb, FilterPoint, ForceDegamma); - RequireDecoded(forcedPoint); + RequireDecoded(forcedPoint, "8_8_8_8_SRGB"); RequireSame(forcedPoint, Sample(*device, Format8888Srgb, FilterPoint, 0u), "8_8_8_8_SRGB, point"); FillInput(false); const auto forcedBilinear = Sample(*device, Format8888Srgb, FilterBilinear, ForceDegamma); RequireSame(forcedBilinear, Sample(*device, Format8888Srgb, FilterBilinear, 0u), "8_8_8_8_SRGB, bilinear"); RequireSame(Sample(*device, Format8888Srgb, FilterAnisoBilinear, ForceDegamma, AnisoOverride), forcedBilinear, "8_8_8_8_SRGB, single level", "anisotropically under ANISO_OVERRIDE", "bilinearly"); - Reject(*device, Format8888UNorm, "guest format 56, which is not sRGB, is sampled through a sampler that forces sRGB decoding"); + FillInput(true); + const auto unormPoint = Sample(*device, Format8888UNorm, FilterPoint, ForceDegamma); + RequireDecoded(unormPoint, "8_8_8_8_UNORM"); + RequireSame(unormPoint, forcedPoint, "8_8_8_8_UNORM, point", "through FORCE_DEGAMMA", "as 8_8_8_8_SRGB"); + const auto unormPlain = Sample(*device, Format8888UNorm, FilterPoint, 0u); + Require(unormPlain[0] != unormPoint[0] || unormPlain[4] != unormPoint[4], "8_8_8_8_UNORM without FORCE_DEGAMMA sampled the decoded values"); + FillInput(false); + RequireSame(Sample(*device, Format8888UNorm, FilterBilinear, ForceDegamma), forcedBilinear, "8_8_8_8_UNORM, bilinear", "through FORCE_DEGAMMA", "as 8_8_8_8_SRGB"); + Reject(*device, Format32Float, "guest format 22, which is not sRGB and has no sRGB view, is sampled through a sampler that forces sRGB decoding"); std::puts("image sample force degamma tests passed"); return 0; } catch (const std::exception& error) {