diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/GuestSamplerResource.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/GuestSamplerResource.hpp index 5312d9b5b..e85a1a6f8 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/GuestSamplerResource.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/GuestSamplerResource.hpp @@ -24,12 +24,13 @@ struct GuestSamplerResource { float lodBias; VkBorderColor borderColor; VkSamplerReductionMode reductionMode = VK_SAMPLER_REDUCTION_MODE_WEIGHTED_AVERAGE_EXT; + bool forceDegamma = false; bool compareEnable = false; VkCompareOp compareOp = VK_COMPARE_OP_NEVER; bool unnormalizedCoordinates = false; }; -GuestSamplerResource DecodeSamplerResource(std::span words, bool unnormalizedProven = false); +GuestSamplerResource DecodeSamplerResource(std::span words, bool unnormalizedProven = false, bool forceDegammaPaired = false); } diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/Sampler.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/Sampler.hpp index 564679119..a98203b7c 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/Sampler.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/Sampler.hpp @@ -19,6 +19,7 @@ public: Sampler(const Sampler&) = delete; Sampler& operator=(const Sampler&) = delete; + bool ForcesDegamma() const; VkSampler Handle() const; bool RequiresFilterMinmax() const; @@ -26,6 +27,7 @@ private: void release() noexcept; Context context; + bool forcesDegamma = false; VkSampler sampler = VK_NULL_HANDLE; bool requiresFilterMinmax = false; }; @@ -41,7 +43,7 @@ public: explicit SamplerCache(std::size_t capacity = 1024); SamplerCache(const SamplerCache&) = delete; SamplerCache& operator=(const SamplerCache&) = delete; - std::shared_ptr Get(const Context& context, std::span words, bool compareEnable, bool unnormalizedProven = false); + std::shared_ptr Get(const Context& context, std::span words, bool compareEnable, bool unnormalizedProven = false, bool forceDegammaPaired = false); // APS5_PROFILE_DRAW counters: lookups served by an existing sampler, and samplers created. std::uint64_t Hits() const { return hits; } std::uint64_t Misses() const { return misses; } @@ -60,6 +62,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); } diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/TextureFormat.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/TextureFormat.hpp index 8820c85bc..47552e4dc 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/TextureFormat.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/TextureFormat.hpp @@ -15,6 +15,7 @@ struct Context; VkFormat ResolveTextureFormat(std::uint32_t guestFormat); std::uint32_t SrgbDecodeFormats(PFN_vkGetPhysicalDeviceFormatProperties formatProperties, VkPhysicalDevice physical); VkFormat SampledTextureFormat(const Context& context, std::uint32_t guestFormat); +bool IsSrgbTextureFormat(std::uint32_t guestFormat); std::optional FindGuestTextureFormat(VkFormat format, std::uint32_t elementBytes); std::optional FindGuestColorTargetFormat(VkFormat format, std::uint32_t elementBytes); std::uint32_t BytesPerElement(std::uint32_t guestFormat); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestSamplerResource.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestSamplerResource.cpp index e70e48018..58581a558 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestSamplerResource.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestSamplerResource.cpp @@ -54,7 +54,7 @@ float toSignedLodBias(std::uint32_t raw) { } -GuestSamplerResource DecodeSamplerResource(std::span words, bool unnormalizedProven) { +GuestSamplerResource DecodeSamplerResource(std::span words, bool unnormalizedProven, bool forceDegammaPaired) { Require(words.size() == 4, "guest sampler descriptor must contain 4 dwords"); const auto clampX = (words[0] >> 0u) & 0x7u; @@ -99,7 +99,7 @@ GuestSamplerResource DecodeSamplerResource(std::span words, } else { Require(!unnormalizedProven, "guest sampler descriptor is bound as unnormalized without FORCE_UNNORMALIZED"); } - Require(!forceSrgb, "guest sampler descriptor forces sRGB decoding which is not implemented"); + Require(!forceSrgb || forceDegammaPaired, "guest sampler descriptor forces sRGB decoding which is not implemented"); // TRUNC_COORD picks point-sampled texels by truncation instead of rounding, and the perf fields // trade mip/depth precision for speed; Vulkan's nearest filtering already floors, so these only // move texel selection by half a texel at most and are accepted as is. ANISO_THRESHOLD and @@ -161,6 +161,7 @@ GuestSamplerResource DecodeSamplerResource(std::span words, result.reductionMode = reductionMode; const std::array compareOps{VK_COMPARE_OP_NEVER, VK_COMPARE_OP_LESS, VK_COMPARE_OP_EQUAL, VK_COMPARE_OP_LESS_OR_EQUAL, VK_COMPARE_OP_GREATER, VK_COMPARE_OP_NOT_EQUAL, VK_COMPARE_OP_GREATER_OR_EQUAL, VK_COMPARE_OP_ALWAYS}; result.compareOp = compareOps.at(depthCompareFunc); + result.forceDegamma = forceSrgb; if (forceUnormCoords) { result.unnormalizedCoordinates = true; result.mipmapMode = VK_SAMPLER_MIPMAP_MODE_NEAREST; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Sampler.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Sampler.cpp index 851ae3bf7..6bbe3bf7b 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Sampler.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Sampler.cpp @@ -1,4 +1,5 @@ #include "prx/libSceAgcDriver/Graphics/include/Sampler.hpp" +#include "prx/libSceAgcDriver/Graphics/include/TextureFormat.hpp" #include #include #include @@ -10,6 +11,7 @@ namespace AgcDriver::Graphics { Require(descriptor.maxAnisotropy <= context.limits.maxSamplerAnisotropy, "guest sampler descriptor requests an anisotropy ratio beyond the device limit"); Require(descriptor.lodBias >= -context.limits.maxSamplerLodBias && descriptor.lodBias <= context.limits.maxSamplerLodBias, "guest sampler descriptor requests a LOD bias beyond the device limit"); Require(!(descriptor.unnormalizedCoordinates && descriptor.compareEnable), "guest sampler descriptor with unnormalized coordinates enables depth comparison, which is not implemented"); + forcesDegamma = descriptor.forceDegamma; VkSamplerCreateInfo info{VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO}; VkSamplerReductionModeCreateInfoEXT reduction{VK_STRUCTURE_TYPE_SAMPLER_REDUCTION_MODE_CREATE_INFO_EXT, nullptr, descriptor.reductionMode}; @@ -55,18 +57,19 @@ namespace AgcDriver::Graphics { SamplerCache::SamplerCache(std::size_t capacity) : capacity(std::max(capacity, 1)) {} - std::shared_ptr SamplerCache::Get(const Context& context, std::span words, bool compareEnable, bool unnormalizedProven) { + std::shared_ptr SamplerCache::Get(const Context& context, std::span words, bool compareEnable, bool unnormalizedProven, bool forceDegammaPaired) { Require(words.size() == 4, "guest sampler descriptor must contain 4 dwords"); const std::array key{words[0], words[1], words[2], words[3], (compareEnable ? 1u : 0u) | (unnormalizedProven ? 2u : 0u)}; std::lock_guard lock(mutex); ++clock; if (const auto found = entries.find(key); found != entries.end()) { + Require(forceDegammaPaired || !found->second.sampler->ForcesDegamma(), "guest sampler descriptor forces sRGB decoding which is not implemented"); ++hits; found->second.lastUse = clock; return found->second.sampler; } ++misses; - auto resource = DecodeSamplerResource(words, unnormalizedProven); + auto resource = DecodeSamplerResource(words, unnormalizedProven, forceDegammaPaired); resource.compareEnable = compareEnable; auto sampler = std::make_shared(context, resource); // The cap keeps live samplers well below the device's limit (NVIDIA: ~4000); a set in flight @@ -92,4 +95,14 @@ namespace AgcDriver::Graphics { if ((properties.optimalTilingFeatures & VK_FORMAT_FEATURE_SAMPLED_IMAGE_FILTER_MINMAX_BIT_EXT) == 0) Require(false, "a sampled texture whose format " + std::to_string(format) + " does not support min/max filtering is sampled through a min or max reduction sampler with linear filtering"); } + bool Sampler::ForcesDegamma() const { + return forcesDegamma; + } + + void RequireDegammaFormat(std::uint32_t guestFormat, std::uint32_t samplerMask, std::span> samplers) { + 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"); + } + } + } diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp index 59daae04e..709986446 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp @@ -2711,9 +2711,9 @@ void ShaderResources::addImageBinding(const ShaderRecompiler::DescriptorBinding& const bool unnormalized = element < binding.samplerUnnormalized.size() && binding.samplerUnnormalized[element]; static const bool noSamplerCache = std::getenv("APS5_NO_SAMPLER_CACHE") != nullptr; if (context.samplerCache != nullptr && !noSamplerCache) { - samplers.push_back(context.samplerCache->Get(context, words, compareEnable, unnormalized)); + samplers.push_back(context.samplerCache->Get(context, words, compareEnable, unnormalized, true)); } else { - auto resource = DecodeSamplerResource(words, unnormalized); + auto resource = DecodeSamplerResource(words, unnormalized, true); resource.compareEnable = compareEnable; samplers.push_back(std::make_shared(context, resource)); } @@ -2866,6 +2866,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 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)) + ")"); const VkComponentMapping components = ViewComponents(resource); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/TextureFormat.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/TextureFormat.cpp index d5978cf76..ad66fdbab 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/TextureFormat.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/TextureFormat.cpp @@ -133,6 +133,19 @@ VkFormat SampledTextureFormat(const Context& context, std::uint32_t guestFormat) return ResolveTextureFormat(static_cast(ShaderRecompiler::SrgbUnormFormat(format))); } +bool IsSrgbTextureFormat(std::uint32_t guestFormat) { + switch (findFormatEntry(guestFormat).vkFormat) { + case VK_FORMAT_R8_SRGB: + case VK_FORMAT_R8G8_SRGB: + case VK_FORMAT_R8G8B8A8_SRGB: + case VK_FORMAT_BC1_RGBA_SRGB_BLOCK: + case VK_FORMAT_BC2_SRGB_BLOCK: + case VK_FORMAT_BC3_SRGB_BLOCK: + case VK_FORMAT_BC7_SRGB_BLOCK: return true; + default: return false; + } +} + std::optional FindGuestTextureFormat(VkFormat format, std::uint32_t elementBytes) { for (const auto& entry : kFormatLookup) { if (!entry.blockCompressed && entry.vkFormat == format && entry.bytesPerElement == elementBytes) return entry.guestFormat; diff --git a/core/libs/prx/libSceAgcDriver/tests/GuestSamplerResource.cpp b/core/libs/prx/libSceAgcDriver/tests/GuestSamplerResource.cpp index 4a423ed0a..c27a28492 100644 --- a/core/libs/prx/libSceAgcDriver/tests/GuestSamplerResource.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/GuestSamplerResource.cpp @@ -221,6 +221,21 @@ void RunSamplerReductionTests(const Fields& base) { reject([&] { pointFiltered(pack(maxLinearMip)); }, "needs point filtering"); } +void RunSamplerCacheDegammaTests(const Fields& base) { + Context context{}; + context.limits.maxSamplerAnisotropy = 1.0f; + context.deviceProc = captureProc; + Fields forcedSrgb = base; + forcedSrgb.forceSrgb = true; + const auto words = pack(forcedSrgb); + SamplerCache unpaired; + reject([&] { unpaired.Get(context, words, false); }, "forces sRGB decoding"); + SamplerCache cache; + const auto paired = cache.Get(context, words, false, false, true); + Require(paired->ForcesDegamma() && cache.Get(context, words, false, false, true) == paired && cache.Misses() == 1u, "a paired FORCE_DEGAMMA sampler must be created once and then served from the cache"); + reject([&] { cache.Get(context, words, false); }, "forces sRGB decoding"); +} + } void RunGuestSamplerResourceTests() { @@ -360,9 +375,14 @@ void RunGuestSamplerResourceTests() { reject([&] { DecodeSamplerResource(truncatedBlend, true); }, "unnormalized coordinates with MC_COORD_TRUNC"); rejectUnnormalized(base, "bound as unnormalized without FORCE_UNNORMALIZED"); - Fields badSrgb = base; - badSrgb.forceSrgb = true; - rejectFields(badSrgb, "forces sRGB decoding"); + Fields forcedSrgb = base; + forcedSrgb.forceSrgb = true; + rejectFields(forcedSrgb, "forces sRGB decoding"); + const auto forced = DecodeSamplerResource(pack(forcedSrgb), false, true); + const auto plain = DecodeSamplerResource(pack(base)); + Require(forced.forceDegamma && !plain.forceDegamma, "FORCE_DEGAMMA must be decoded into forceDegamma"); + Require(forced.magFilter == plain.magFilter && forced.minFilter == plain.minFilter && forced.mipmapMode == plain.mipmapMode && forced.maxLod == plain.maxLod, "FORCE_DEGAMMA must not change the host sampler state"); + RunSamplerCacheDegammaTests(base); Fields truncated = base; truncated.truncCoord = true; diff --git a/core/libs/prx/libSceAgcDriver/tests/TextureFormat.cpp b/core/libs/prx/libSceAgcDriver/tests/TextureFormat.cpp index 245766a02..0e632a0de 100644 --- a/core/libs/prx/libSceAgcDriver/tests/TextureFormat.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/TextureFormat.cpp @@ -116,5 +116,7 @@ void RunTextureFormatTests() { reject([] { ResolveTextureFormat(9999); }, "unsupported guest texture format"); reject([] { BytesPerElement(2); }, "unsupported guest texture format"); reject([] { IsBlockCompressed(200); }, "unsupported guest texture format"); + for (const std::uint32_t format : {128u, 129u, 130u, 170u, 172u, 174u, 182u}) Require(IsSrgbTextureFormat(format), "guest format " + std::to_string(format) + " must be an sRGB texture format"); + for (const std::uint32_t format : {1u, 14u, 56u, 71u, 169u, 171u, 173u, 181u}) Require(!IsSrgbTextureFormat(format), "guest format " + std::to_string(format) + " must not be an sRGB texture format"); convertedDccClearTests(); } diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/ImageSampleForceDegamma.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ImageSampleForceDegamma.cpp new file mode 100644 index 000000000..965994506 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ImageSampleForceDegamma.cpp @@ -0,0 +1,171 @@ +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/TextureTiling.hpp" +#include "Recompiler.hpp" +#include "VulkanTestDevice.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; +using ShaderRecompiler::ShaderStage; + +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 Type2D = 9; +constexpr std::uint32_t FilterPoint = 0; +constexpr std::uint32_t FilterBilinear = 1; +constexpr std::uint32_t ForceDegamma = 1u << 20u; + +alignas(256) std::array Input{}; +alignas(256) std::array Output{}; +alignas(4096) std::array Texels{}; + +alignas(256) constexpr std::array Code{ + 0x1614008c, 0xe03c1000, 0x8000010a, 0xbf8c3f70, 0xf0900f08, 0x00820401, 0xbf8c3f70, + 0x34160084, 0xe0781000, 0x8001040b, 0xbf810000, 0xbf810000, 0xbf810000, +}; + +std::array BufferDescriptor(const void* data, std::uint32_t bytes) { + const auto address = reinterpret_cast(data); + return {static_cast(address), static_cast((address >> 32u) & 0xffffu), bytes, 0x31016facu}; +} + +std::array TextureDescriptor(std::uint32_t format) { + const auto address = static_cast(reinterpret_cast(Texels.data())); + return { + static_cast(address >> 8u), + static_cast((address >> 40u) & 0xffu) | (format << 20u) | (((Size - 1u) & 3u) << 30u), + ((Size - 1u) >> 2u) | ((Size - 1u) << 14u), + 0xfacu | (Type2D << 28u), + 0u, + 0u, + 0u, + 0u, + }; +} + +std::array SamplerDescriptor(std::uint32_t filter, std::uint32_t forced) { + return {0x92u | forced, 0x00fff000u, (filter << 20u) | (filter << 22u) | (1u << 24u), 0u}; +} + +std::uint8_t Code8(std::uint32_t x, std::uint32_t y, std::uint32_t channel) { + return static_cast((y * 61u + x * 17u + channel * 97u + 5u) % 256u); +} + +void FillTexels() { + const auto mips = AgcDriver::Graphics::ComputeMipLayout(AgcDriver::Graphics::TextureTileMode::kLinear, Format8888Srgb, Size, Size, 1); + Require(AgcDriver::Graphics::ComputeSurfaceSize(mips, 1) <= Texels.size(), "image sample force degamma: the texture does not fit its storage"); + Texels.fill(0); + for (std::uint32_t y = 0; y < Size; ++y) { + for (std::uint32_t x = 0; x < Size; ++x) { + auto* texel = &Texels[mips[0].tiledOffset + static_cast(y) * mips[0].pitchBytes + x * 4u]; + for (std::uint32_t channel = 0; channel < 4u; ++channel) texel[channel] = Code8(x, y, channel); + } + } +} + +void FillInput(bool centers) { + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + const auto x = tid % Size; + const auto y = (tid / Size) % Size; + Input[tid * 3u + 0u] = centers ? (static_cast(x) + 0.5f) / static_cast(Size) : (static_cast((tid * 5u + 1u) % 13u) + 0.3f) / 13.0f; + Input[tid * 3u + 1u] = centers ? (static_cast(y) + 0.5f) / static_cast(Size) : (static_cast((tid * 7u + 3u) % 13u) + 0.6f) / 13.0f; + Input[tid * 3u + 2u] = 0.0f; + } +} + +float SrgbToLinear(std::uint8_t code) { + const float c = static_cast(code) / 255.0f; + return c <= 0.04045f ? c / 12.92f : std::pow((c + 0.055f) / 1.055f, 2.4f); +} + +std::array Sample(AgcDriver::VulkanDevice& device, std::uint32_t format, std::uint32_t filter, std::uint32_t forced) { + std::vector userData(20, 0u); + const auto input = BufferDescriptor(Input.data(), static_cast(sizeof(Input))); + const auto output = BufferDescriptor(Output.data(), static_cast(sizeof(Output))); + const auto texture = TextureDescriptor(format); + const auto sampler = SamplerDescriptor(filter, forced); + std::copy(input.begin(), input.end(), userData.begin()); + std::copy(output.begin(), output.end(), userData.begin() + 4); + std::copy(texture.begin(), texture.end(), userData.begin() + 8); + std::copy(sampler.begin(), sampler.end(), userData.begin() + 16); + const std::array memory{{{reinterpret_cast(Code.data()), std::as_bytes(std::span(Code))}}}; + const ShaderRecompiler::ShaderComputeStageInfo compute{{Threads, 1, 1}, 0u, {false, false, false}, false, 1}; + ShaderRecompiler::RecompileRequest request{ + {ShaderStage::Compute, reinterpret_cast(Code.data()), Code, 0, {}}, + {32, 0, userData, compute, std::nullopt, std::nullopt, memory}, + device.Target(), + {0, 0, 0, 128} + }; + request.useCache = false; + const auto result = ShaderRecompiler::Recompile(request); + Output.fill(-1.0f); + device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast(Code.data())); + device.WaitIdle(); + return Output; +} + +void RequireSame(const std::array& forced, const std::array& plain, const char* name) { + for (std::uint32_t index = 0; index < forced.size(); ++index) { + Require(std::bit_cast(forced[index]) == std::bit_cast(plain[index]), std::string(name) + ": thread " + std::to_string(index / 4u) + " component " + std::to_string(index % 4u) + " sampled " + std::to_string(forced[index]) + " through FORCE_DEGAMMA, " + std::to_string(plain[index]) + " without it"); + } +} + +void RequireDecoded(const std::array& sampled) { + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + const auto x = tid % Size; + const auto y = (tid / Size) % Size; + for (std::uint32_t channel = 0; channel < 4u; ++channel) { + 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)); + } + } +} + +void Reject(AgcDriver::VulkanDevice& device, std::uint32_t format, std::string_view reason) { + try { + static_cast(Sample(device, format, FilterPoint, ForceDegamma)); + } catch (const std::exception& error) { + Require(std::string_view(error.what()).find(reason) != std::string_view::npos, std::string("unexpected rejection: ") + error.what()); + return; + } + Require(false, std::string("expected rejection: ") + std::string(reason)); +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + FillTexels(); + FillInput(true); + const auto forcedPoint = Sample(*device, Format8888Srgb, FilterPoint, ForceDegamma); + RequireDecoded(forcedPoint); + 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"); + Reject(*device, Format8888UNorm, "guest format 56, which is not sRGB, is sampled through a sampler that forces sRGB decoding"); + std::puts("image sample force degamma tests passed"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index e321319d5..48cf59f50 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -363,6 +363,7 @@ Throughout the project, every function at every stage either **does exactly what - [Vector buffer offsets that are not a multiple of 4](../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp) (shader recompiler) - dword accesses count whole dwords from the V# base, where the hardware reads the exact bytes at base + offset. - [Min/max S# reduction](../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestSamplerResource.cpp) (libSceAgcDriver) - `FILTER_MODE` 1 and 2 use Vulkan's min and max reduction (the same `SQ_IMG_FILTER_MODE` values RADV programs). Measured on RDNA2 (gfx1035), not on the console: only texels of non-zero weight are reduced, and the weight is taken after rounding to the 8-bit subtexel grid (a sample within 1/512 texel of a centre reduces that texel alone); a driver with other `subTexelPrecisionBits` reduces a different set there. Gathers ignore the mode. - [Min/max S#](../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestSamplerResource.cpp) (libSceAgcDriver, shader recompiler) - throws with anisotropy, a linear mip filter, depth or color-texture compare, or bilinear filtering of an image the recompiler point-samples (sint, converted, depth-bits). +- [FORCE_DEGAMMA](../../core/libs/prx/libSceAgcDriver/Graphics/src/Sampler.cpp) (libSceAgcDriver) - an S# that forces sRGB decoding adds nothing to an sRGB texture, as in shadPS4 (PS4). Measured on RDNA2 (gfx1035), not on the console: `8`, `8_8` and `8_8_8_8` sRGB textures sample bit for bit the same with and without the bit (point and bilinear; BC formats not measured). Other textures throw; on the same hardware their UNORM `8`, `8_8` and `8_8_8_8` forms sample bit for bit as the sRGB format (decoded before filtering). - [Tessellation factor ring](../../core/libs/prx/libSceAgcDriver/State/src/Configuration.cpp) (libSceAgcDriver) - `sceAgcDriverSetTFRing` records the ring and `sceAgcDriverGetTFRing` reports it. Before a title sets one, it reports the console's default, 0xFF0000000 and 128 KiB, as measured in [PS5_Vulkan's hardware findings](https://github.com/mihawk-99/PS5_Vulkan/blob/fde9e3798c2ec1d8fccc90d5067cd6060490607f/docs/HARDWARE_FINDINGS.md). Nothing is written to the recorded ring: tessellated draws run as Vulkan tessellation stages, which keep their factors on the host, so a title that reads tessellation factors from the ring finds none. A null base or a zero size throws; [ps5-opengl's AGC runtime](https://github.com/blackbearreloaded/ps5-opengl/blob/67c873f985eaefe0a72f713529e5632eaa01139c/src/platform/ps5_agc_native_runtime.c) calls `sceAgcDriverSetTFRing(0, 0)` in its cleanup, and what that does on the console is unknown. - [Raw `buffer_load_dwordx2`](../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp) is one 64-bit access on the hardware (with or without glc), so it never sees half of a `buffer_atomic_swap_x2`. Only a glc load that is 8-byte aligned and in bounds, in a program that has the 64-bit storage view (it uses 64-bit buffer atomics), is read with one 64-bit atomic load; every other dwordx2 load is two dword loads and can tear against a concurrent 64-bit write: loads without glc, and loads in a program without 64-bit buffer atomics while another shader writes the pair with `swap_x2`. - [DS src2 operations](../../core/shader/recompiler/Translation/src/MemoryInstructions/SharedMemoryInstructions.cpp) (`ds_*_src2_*`, GFX10.1 only) read `DS[B]`, with `B = A + 4 * sext(offset1[6:0]:offset0)`, and then update `DS[A]` with the matching 32-bit atomic, so the read of `B` is not atomic with the update of `A`. The form that takes the offset from the address (`offset1[7]` set), the 64-bit forms and GDS throw: whether the LDS address keeps `A[31:17]` in that form, and whether the 64-bit forms scale the offset by 4 or 8, is not documented. `ds_min_src2_f32`, `ds_max_src2_f32` and `ds_add_src2_f32` throw: on RDNA2 (gfx1035, the oracle's hardware, which has no src2 opcodes) `ds_min_f32`, `ds_max_f32` and `ds_add_f32` ignore `IEEE_MODE` but depend on whether f32 denormals are flushed (28, 28 and 12 of 196 special rows differ), and that is not modelled for LDS float atomics yet (#981).