Merge pull request #4487 from Pipodi/feat/agc-forced-srgb-views

feat(agc): sample non-sRGB textures through FORCE_DEGAMMA samplers as sRGB views
This commit is contained in:
Dean 2026-10-14 13:31:02 +13:00 committed by GitHub
commit afc8541d18
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 110 additions and 10 deletions

View File

@ -66,7 +66,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);
bool ForcesDegammaOn(std::uint32_t guestFormat, std::uint32_t samplerMask, std::span<const std::shared_ptr<Sampler>> samplers);
void RequireBorderSwizzle(std::uint32_t bcSwizzle, std::uint32_t samplerMask, std::span<const std::shared_ptr<Sampler>> samplers);
void RequireNonSeamlessCube(bool cube, std::uint32_t samplerMask, std::span<const std::shared_ptr<Sampler>> samplers);

View File

@ -466,6 +466,7 @@ private:
std::vector<std::shared_ptr<Texture>> textures;
std::vector<bool> textureFirstLayer;
std::vector<bool> textureBaseLevel;
std::vector<bool> textureSrgb;
std::vector<std::shared_ptr<StorageTexture>> storageTextures;
std::vector<std::uint32_t> storageMips;
std::vector<std::uint64_t> storageKeys;

View File

@ -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<VkImageViewCreateInfo> baseLevelInfo;
mutable std::mutex baseLevelMutex;
mutable VkImageView baseLevelView = VK_NULL_HANDLE;
std::optional<VkImageViewCreateInfo> srgbInfo;
float srgbMinLod = 0.0f;
mutable std::mutex srgbMutex;
mutable std::array<VkImageView, 3> 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),

View File

@ -113,10 +113,12 @@ namespace AgcDriver::Graphics {
return requiresNonSeamlessCube;
}
void RequireDegammaFormat(std::uint32_t guestFormat, std::uint32_t samplerMask, std::span<const std::shared_ptr<Sampler>> samplers) {
bool ForcesDegammaOn(std::uint32_t guestFormat, std::uint32_t samplerMask, std::span<const std::shared_ptr<Sampler>> 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<const std::shared_ptr<Sampler>> samplers) {

View File

@ -1617,7 +1617,7 @@ void ShaderResources::buildComplete() {
write.pImageInfo = images.data() + images.size();
for (const auto index : binding.imageAllocations) {
if (index == std::numeric_limits<std::size_t>::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<int>(*binding.imageShape)) + ", dimension " + std::to_string(static_cast<int>(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<std::size_t>(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<int>(resource.tileMode), resource.dccAddress});
item.imageAllocations.push_back(textures.size() - 1);
}

View File

@ -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 <algorithm>
#include <array>
#include <bit>
@ -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<StorageTexture>&
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<PFN_vkCreateImageView>("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<PFN_vkDestroyImageView>("vkDestroyImageView")(context.device, baseLevelView, nullptr);
baseLevelView = VK_NULL_HANDLE;
for (auto& srgbView : srgbViews) {
if (srgbView) context.Function<PFN_vkDestroyImageView>("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 {

View File

@ -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 <array>
@ -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<typename TAction>
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");

View File

@ -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<float, Threads * 4>& forced, const std::array<
}
}
void RequireDecoded(const std::array<float, Threads * 4>& sampled) {
void RequireDecoded(const std::array<float, Threads * 4>& 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<float, Threads * 4>& sampled) {
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));
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) {