feat(agc): accept samplers that force sRGB decoding of sRGB textures

A sampler with FORCE_DEGAMMA (S# word 0 bit 20) threw at decode, so
every dispatch or draw sampling through one was skipped.

DecodeSamplerResource now records the bit, but accepts it only for a
caller that pairs the sampler with its images. The direct sampler
binding does, and RequireDegammaFormat checks each sampled image
against its paired samplers: only already sRGB formats (8, 8_8 and
8_8_8_8 SRGB, BC1, BC2, BC3 and BC7 SRGB) pass, decoded once, as
shadPS4 (PS4) models it. Other textures and unpaired decodes still
throw; gfx10 behaviour is not measured.
This commit is contained in:
Celegans12 2026-10-09 03:40:54 +02:00 committed by Adrià Franch
parent a291ae0f88
commit 619ec7ad39
11 changed files with 238 additions and 11 deletions

View File

@ -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);
}

View File

@ -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);
}

View File

@ -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);

View File

@ -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;

View File

@ -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");
}
}
}

View File

@ -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);

View File

@ -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;

View File

@ -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;

View File

@ -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();
}

View File

@ -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;
}
}

View File

@ -362,6 +362,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); not measured on gfx10. Other textures throw.
- [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).