Merge pull request #1487 from Celegans12/feat/agc-forced-degamma-srgb
feat(agc): accept samplers that force sRGB decoding of sRGB textures
This commit is contained in:
commit
ba6f1e5bd6
|
|
@ -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<const std::uint32_t> words, bool unnormalizedProven = false);
|
||||
GuestSamplerResource DecodeSamplerResource(std::span<const std::uint32_t> words, bool unnormalizedProven = false, bool forceDegammaPaired = false);
|
||||
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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<Sampler> Get(const Context& context, std::span<const std::uint32_t> words, bool compareEnable, bool unnormalizedProven = false);
|
||||
std::shared_ptr<Sampler> Get(const Context& context, std::span<const std::uint32_t> 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<const std::shared_ptr<Sampler>> samplers);
|
||||
void RequireDegammaFormat(std::uint32_t guestFormat, std::uint32_t samplerMask, std::span<const std::shared_ptr<Sampler>> samplers);
|
||||
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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<std::uint32_t> FindGuestTextureFormat(VkFormat format, std::uint32_t elementBytes);
|
||||
std::optional<std::uint32_t> FindGuestColorTargetFormat(VkFormat format, std::uint32_t elementBytes);
|
||||
std::uint32_t BytesPerElement(std::uint32_t guestFormat);
|
||||
|
|
|
|||
|
|
@ -54,7 +54,7 @@ float toSignedLodBias(std::uint32_t raw) {
|
|||
|
||||
}
|
||||
|
||||
GuestSamplerResource DecodeSamplerResource(std::span<const std::uint32_t> words, bool unnormalizedProven) {
|
||||
GuestSamplerResource DecodeSamplerResource(std::span<const std::uint32_t> 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<const std::uint32_t> 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<const std::uint32_t> 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;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
#include "prx/libSceAgcDriver/Graphics/include/Sampler.hpp"
|
||||
#include "prx/libSceAgcDriver/Graphics/include/TextureFormat.hpp"
|
||||
#include <algorithm>
|
||||
#include <cstdlib>
|
||||
#include <string>
|
||||
|
|
@ -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<std::size_t>(capacity, 1)) {}
|
||||
|
||||
std::shared_ptr<Sampler> SamplerCache::Get(const Context& context, std::span<const std::uint32_t> words, bool compareEnable, bool unnormalizedProven) {
|
||||
std::shared_ptr<Sampler> SamplerCache::Get(const Context& context, std::span<const std::uint32_t> words, bool compareEnable, bool unnormalizedProven, bool forceDegammaPaired) {
|
||||
Require(words.size() == 4, "guest sampler descriptor must contain 4 dwords");
|
||||
const std::array<std::uint32_t, 5> 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<Sampler>(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<const std::shared_ptr<Sampler>> 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");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<Sampler>(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<int>(*binding.imageShape)) + ", dimension " + std::to_string(static_cast<int>(resource.dimension)) + ")");
|
||||
const VkComponentMapping components = ViewComponents(resource);
|
||||
|
|
|
|||
|
|
@ -133,6 +133,19 @@ VkFormat SampledTextureFormat(const Context& context, std::uint32_t guestFormat)
|
|||
return ResolveTextureFormat(static_cast<std::uint32_t>(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<std::uint32_t> FindGuestTextureFormat(VkFormat format, std::uint32_t elementBytes) {
|
||||
for (const auto& entry : kFormatLookup) {
|
||||
if (!entry.blockCompressed && entry.vkFormat == format && entry.bytesPerElement == elementBytes) return entry.guestFormat;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 <algorithm>
|
||||
#include <array>
|
||||
#include <bit>
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <iostream>
|
||||
#include <span>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
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<float, Threads * 3> Input{};
|
||||
alignas(256) std::array<float, Threads * 4> Output{};
|
||||
alignas(4096) std::array<std::uint8_t, 4096> Texels{};
|
||||
|
||||
alignas(256) constexpr std::array<std::uint32_t, 13> Code{
|
||||
0x1614008c, 0xe03c1000, 0x8000010a, 0xbf8c3f70, 0xf0900f08, 0x00820401, 0xbf8c3f70,
|
||||
0x34160084, 0xe0781000, 0x8001040b, 0xbf810000, 0xbf810000, 0xbf810000,
|
||||
};
|
||||
|
||||
std::array<std::uint32_t, 4> BufferDescriptor(const void* data, std::uint32_t bytes) {
|
||||
const auto address = reinterpret_cast<std::uintptr_t>(data);
|
||||
return {static_cast<std::uint32_t>(address), static_cast<std::uint32_t>((address >> 32u) & 0xffffu), bytes, 0x31016facu};
|
||||
}
|
||||
|
||||
std::array<std::uint32_t, 8> TextureDescriptor(std::uint32_t format) {
|
||||
const auto address = static_cast<std::uint64_t>(reinterpret_cast<std::uintptr_t>(Texels.data()));
|
||||
return {
|
||||
static_cast<std::uint32_t>(address >> 8u),
|
||||
static_cast<std::uint32_t>((address >> 40u) & 0xffu) | (format << 20u) | (((Size - 1u) & 3u) << 30u),
|
||||
((Size - 1u) >> 2u) | ((Size - 1u) << 14u),
|
||||
0xfacu | (Type2D << 28u),
|
||||
0u,
|
||||
0u,
|
||||
0u,
|
||||
0u,
|
||||
};
|
||||
}
|
||||
|
||||
std::array<std::uint32_t, 4> 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<std::uint8_t>((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<std::uint64_t>(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<float>(x) + 0.5f) / static_cast<float>(Size) : (static_cast<float>((tid * 5u + 1u) % 13u) + 0.3f) / 13.0f;
|
||||
Input[tid * 3u + 1u] = centers ? (static_cast<float>(y) + 0.5f) / static_cast<float>(Size) : (static_cast<float>((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<float>(code) / 255.0f;
|
||||
return c <= 0.04045f ? c / 12.92f : std::pow((c + 0.055f) / 1.055f, 2.4f);
|
||||
}
|
||||
|
||||
std::array<float, Threads * 4> Sample(AgcDriver::VulkanDevice& device, std::uint32_t format, std::uint32_t filter, std::uint32_t forced) {
|
||||
std::vector<std::uint32_t> userData(20, 0u);
|
||||
const auto input = BufferDescriptor(Input.data(), static_cast<std::uint32_t>(sizeof(Input)));
|
||||
const auto output = BufferDescriptor(Output.data(), static_cast<std::uint32_t>(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<ShaderRecompiler::MemoryRegion, 1> memory{{{reinterpret_cast<std::uintptr_t>(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<std::uintptr_t>(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<std::uintptr_t>(Code.data()));
|
||||
device.WaitIdle();
|
||||
return Output;
|
||||
}
|
||||
|
||||
void RequireSame(const std::array<float, Threads * 4>& forced, const std::array<float, Threads * 4>& plain, const char* name) {
|
||||
for (std::uint32_t index = 0; index < forced.size(); ++index) {
|
||||
Require(std::bit_cast<std::uint32_t>(forced[index]) == std::bit_cast<std::uint32_t>(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<float, Threads * 4>& 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<float>(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<void>(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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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).
|
||||
|
|
|
|||
Loading…
Reference in New Issue