feat(agc): implement min and max sampler reduction modes

An S# with FILTER_MODE (word0 bits 29-30) set threw in
DecodeSamplerResource. Modes 1 and 2 now map to Vulkan's MIN and MAX
sampler reduction through VK_EXT_sampler_filter_minmax, which the device
enables when it reports both filterMinmax properties; mode 3 throws.

Min/max still throws with anisotropy, a linear mip filter (lavapipe and
NVIDIA disagree there), depth comparison, color-texture compare
emulation, and, in PointFilteredSamplerWord (S# words 0 and 2), when it
filters between texels or mips of an image the recompiler point-samples.
A texture sampled through a linear min/max sampler needs
SAMPLED_IMAGE_FILTER_MINMAX on its view format; the image-sampler pairs
travel in DescriptorBinding::imageSamplers (shader disk cache format 5).

Adds the agc_driver_image_sample_reduction GPU test and extends the
sampler, emulated color compare and disk cache tests.
This commit is contained in:
Celegans12 2026-10-06 10:17:22 +02:00 committed by Adrià Franch
parent db3e659870
commit 1afcd20281
22 changed files with 592 additions and 13 deletions

View File

@ -158,6 +158,7 @@ public:
void Resize(std::uint32_t width, std::uint32_t height);
bool Presentable() const;
bool PrimitiveListRestart() const;
bool SamplerFilterMinmax() const;
// A presentation is a few steps so the presenter holds GuestMemory::GpuMutex only while it
// touches the queue. Presentations are slots (FlipInFlight() + 1, each with its own command
// buffer, fence, kept resident image and dump buffer): RetirePresents(keep) (no mutex) retires

View File

@ -204,6 +204,7 @@ struct VulkanDevice::State {
bool depthRangeUnrestricted = false;
bool samplerAnisotropy = false;
bool textureCompressionBC = false;
bool samplerFilterMinmax = false;
// VK_KHR_timeline_semaphore enabled: the recorder's unlocked waits are available.
bool timelineSemaphores = false;
bool computeWave32 = false;
@ -763,6 +764,13 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window) : state(std::make_u
// drops those writes instead of faulting the device.
const bool imageRobustness = hasExtension(VK_EXT_IMAGE_ROBUSTNESS_EXTENSION_NAME);
if (imageRobustness) deviceExtensions.push_back(VK_EXT_IMAGE_ROBUSTNESS_EXTENSION_NAME);
if (hasExtension(VK_EXT_SAMPLER_FILTER_MINMAX_EXTENSION_NAME)) {
VkPhysicalDeviceSamplerFilterMinmaxPropertiesEXT minmaxProperties{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SAMPLER_FILTER_MINMAX_PROPERTIES_EXT};
VkPhysicalDeviceProperties2 properties{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PROPERTIES_2, &minmaxProperties};
state->InstanceFunction<PFN_vkGetPhysicalDeviceProperties2>("vkGetPhysicalDeviceProperties2")(selected, &properties);
state->samplerFilterMinmax = minmaxProperties.filterMinmaxSingleComponentFormats == VK_TRUE && minmaxProperties.filterMinmaxImageComponentMapping == VK_TRUE;
if (state->samplerFilterMinmax) deviceExtensions.push_back(VK_EXT_SAMPLER_FILTER_MINMAX_EXTENSION_NAME);
}
// Indirect draws with a GPU-side count (DRAW_INDIRECT_MULTI with count_indirect); a device
// without it resolves such draws on the CPU.
state->drawIndirectCount = hasExtension(VK_KHR_DRAW_INDIRECT_COUNT_EXTENSION_NAME);
@ -2341,6 +2349,10 @@ bool VulkanDevice::PrimitiveListRestart() const {
return state->primitiveListRestart;
}
bool VulkanDevice::SamplerFilterMinmax() const {
return state->samplerFilterMinmax;
}
Graphics::Context VulkanDevice::graphicsContext() const {
static const bool noCache = std::getenv("APS5_NO_CONTEXT_CACHE") != nullptr;
if (state->contextReady && !noCache) return state->context;
@ -2383,6 +2395,7 @@ Graphics::Context VulkanDevice::buildContext() const {
context.multiDrawIndirect = state->multiDrawIndirect;
context.depthBounds = state->depthBounds;
context.depthBiasClamp = state->depthBiasClamp;
context.samplerFilterMinmax = state->samplerFilterMinmax;
context.drawIndirectCount = state->drawIndirectCount;
context.occlusionQueryPrecise = state->occlusionQueryPrecise;
context.emptyBuffer = state->emptyBuffer ? state->emptyBuffer->Handle() : VK_NULL_HANDLE;

View File

@ -123,6 +123,7 @@ struct Context {
bool occlusionQueryPrecise = false;
bool depthBounds = false;
bool depthBiasClamp = false;
bool samplerFilterMinmax = false;
VkBuffer emptyBuffer = VK_NULL_HANDLE;
// The device's list of recorded dispatches whose copied written buffers await a CPU write-back
// (VulkanDevice's State::copiedWriters; the draw counterpart is DrawCopiedWriters): an indirect

View File

@ -23,6 +23,7 @@ struct GuestSamplerResource {
float maxLod;
float lodBias;
VkBorderColor borderColor;
VkSamplerReductionMode reductionMode = VK_SAMPLER_REDUCTION_MODE_WEIGHTED_AVERAGE_EXT;
bool compareEnable = false;
VkCompareOp compareOp = VK_COMPARE_OP_NEVER;
bool unnormalizedCoordinates = false;

View File

@ -20,12 +20,14 @@ public:
Sampler& operator=(const Sampler&) = delete;
VkSampler Handle() const;
bool RequiresFilterMinmax() const;
private:
void release() noexcept;
Context context;
VkSampler sampler = VK_NULL_HANDLE;
bool requiresFilterMinmax = false;
};
// One VkSampler per distinct S# (its 4 words plus the shader's depth-compare use and the
@ -57,6 +59,8 @@ private:
std::uint64_t misses = 0;
};
void RequireFilterMinmax(const Context& context, VkFormat format, std::uint32_t samplerMask, std::span<const std::shared_ptr<Sampler>> samplers);
}
#endif

View File

@ -326,7 +326,7 @@ private:
// Stage B: the record's texture when the fastRevalidate predicate proves it current under the
// lock and the cache still holds it; null sends the element to cachedTexture.
std::shared_ptr<Texture> fastTexture(const ImageRecord& record);
void resolveImageBinding(const ShaderRecompiler::DescriptorBinding& binding, Binding& item);
void resolveImageBinding(const ShaderRecompiler::DescriptorBinding& binding, Binding& item, std::span<const std::shared_ptr<Sampler>> shaderSamplers);
void forgetDeferredInputs();
void release() noexcept;
void prepareAddressBindings(std::span<const CompiledShader> shaders, std::span<const GuestMemorySnapshot> snapshots);
@ -436,6 +436,8 @@ private:
struct DeferredImages {
const ShaderRecompiler::DescriptorBinding* binding;
std::size_t index;
std::size_t firstSampler = 0;
std::size_t samplerCount = 0;
};
std::vector<DeferredImages> deferredImages;
std::uint64_t storageBuffers = 0;

View File

@ -75,6 +75,7 @@ public:
// The layout the image is kept in while sampled.
VkImageLayout Layout() const { return layout; }
VkDeviceSize AllocationBytes() const { return allocationBytes; }
VkFormat ViewFormat() const { return viewFormat; }
// Whether this texture is a view of a storage image (no snapshot of its own).
bool ViewsStorageImage() const { return storageSource != nullptr; }
const StorageTexture* StorageSource() const { return storageSource.get(); }
@ -100,6 +101,7 @@ private:
ViewRange firstLayerRange{};
VkImageLayout layout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
VkDeviceSize allocationBytes = 0;
VkFormat viewFormat = VK_FORMAT_UNDEFINED;
std::shared_ptr<ResidentColor> source;
std::shared_ptr<StorageTexture> storageSource;
std::unique_ptr<CommandBatch> upload;

View File

@ -20,6 +20,15 @@ bool isAnisoFilter(std::uint32_t raw) {
return raw == 2 || raw == 3;
}
VkSamplerReductionMode toVkReductionMode(std::uint32_t raw) {
switch (raw) {
case 0: return VK_SAMPLER_REDUCTION_MODE_WEIGHTED_AVERAGE_EXT;
case 1: return VK_SAMPLER_REDUCTION_MODE_MIN_EXT;
case 2: return VK_SAMPLER_REDUCTION_MODE_MAX_EXT;
default: throw std::runtime_error("AGC graphics: guest sampler descriptor uses an unknown reduction filter mode " + std::to_string(raw));
}
}
VkSamplerAddressMode toVkAddressMode(std::uint32_t raw) {
switch (raw) {
case 0: return VK_SAMPLER_ADDRESS_MODE_REPEAT;
@ -99,15 +108,17 @@ GuestSamplerResource DecodeSamplerResource(std::span<const std::uint32_t> words,
static_cast<void>(anisoThreshold);
static_cast<void>(anisoBias);
Require(!disableCubeWrap, "guest sampler descriptor disables seamless cube filtering which is not implemented");
Require(filterMode == 0, "guest sampler descriptor uses a reduction filter mode which is not implemented");
const auto reductionMode = toVkReductionMode(filterMode);
Require(!disableDegamma, "guest sampler descriptor disables degamma which is not implemented");
Require(lodBiasSec == 0, "guest sampler descriptor uses a secondary LOD bias which is not implemented");
Require(!pointPreclamp, "guest sampler descriptor uses point preclamping which is not implemented");
Require(!anisoOverride, "guest sampler descriptor uses an anisotropy override which is not implemented");
Require(!blendZeroPrt, "guest sampler descriptor uses PRT blend-zero which is not implemented");
if (mipFilter > 2u) Require(false, "guest sampler descriptor uses an unknown mip filter " + std::to_string(mipFilter));
Require(mipFilter != 2u || reductionMode == VK_SAMPLER_REDUCTION_MODE_WEIGHTED_AVERAGE_EXT, "guest sampler descriptor combines a min or max reduction with a linear mip filter, which is not implemented");
const auto aniso = isAnisoFilter(xyMagFilter) || isAnisoFilter(xyMinFilter);
Require(!aniso || reductionMode == VK_SAMPLER_REDUCTION_MODE_WEIGHTED_AVERAGE_EXT, "guest sampler descriptor combines a min or max reduction with anisotropic filtering, which is not implemented");
auto anisoRatio = 1.0f;
if (aniso) {
if (maxAnisoRatio > 4u) Require(false, "guest sampler descriptor uses an unknown anisotropy ratio " + std::to_string(maxAnisoRatio));
@ -147,6 +158,7 @@ GuestSamplerResource DecodeSamplerResource(std::span<const std::uint32_t> words,
result.maxLod = maxLod;
result.lodBias = toSignedLodBias(lodBiasRaw);
result.borderColor = border;
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);
if (forceUnormCoords) {

View File

@ -1,6 +1,7 @@
#include "prx/libSceAgcDriver/Graphics/include/Sampler.hpp"
#include <algorithm>
#include <cstdlib>
#include <string>
namespace AgcDriver::Graphics {
@ -11,6 +12,13 @@ namespace AgcDriver::Graphics {
Require(!(descriptor.unnormalizedCoordinates && descriptor.compareEnable), "guest sampler descriptor with unnormalized coordinates enables depth comparison, which is not implemented");
VkSamplerCreateInfo info{VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO};
VkSamplerReductionModeCreateInfoEXT reduction{VK_STRUCTURE_TYPE_SAMPLER_REDUCTION_MODE_CREATE_INFO_EXT, nullptr, descriptor.reductionMode};
if (descriptor.reductionMode != VK_SAMPLER_REDUCTION_MODE_WEIGHTED_AVERAGE_EXT) {
Require(context.samplerFilterMinmax, "guest sampler descriptor uses a min or max reduction which the device does not support");
Require(!descriptor.compareEnable, "guest sampler descriptor combines a min or max reduction with depth comparison, which is not implemented");
info.pNext = &reduction;
requiresFilterMinmax = descriptor.magFilter == VK_FILTER_LINEAR || descriptor.minFilter == VK_FILTER_LINEAR;
}
info.magFilter = descriptor.magFilter;
info.minFilter = descriptor.minFilter;
info.mipmapMode = descriptor.mipmapMode;
@ -41,6 +49,10 @@ namespace AgcDriver::Graphics {
return sampler;
}
bool Sampler::RequiresFilterMinmax() const {
return requiresFilterMinmax;
}
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) {
@ -67,4 +79,17 @@ namespace AgcDriver::Graphics {
return sampler;
}
void RequireFilterMinmax(const Context& context, VkFormat format, std::uint32_t samplerMask, std::span<const std::shared_ptr<Sampler>> samplers) {
bool filtered = false;
for (std::uint32_t element = 0; element < 32u; ++element) {
if (((samplerMask >> element) & 1u) == 0u) continue;
if (element >= samplers.size()) Require(false, "a sampled texture is paired with sampler element " + std::to_string(element) + ", which its shader does not bind");
filtered = filtered || samplers[element]->RequiresFilterMinmax();
}
if (!filtered) return;
VkFormatProperties properties{};
context.formatProperties(context.physical, format, &properties);
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");
}
}

View File

@ -899,6 +899,8 @@ void ShaderResources::buildPrepare(std::span<const CompiledShader> shaders, cons
std::uint64_t stageDescriptors = 0;
std::vector<std::size_t> offsetsInData;
std::int64_t shaderData = -1;
const auto firstSampler = samplers.size();
const auto firstDeferred = deferredImages.size();
for (const auto& binding : shader.program->bindings) {
Require(binding.descriptorSet == 0, "unexpected descriptor set: every shader resource must use descriptor set zero");
Require(occupied.insert(binding.binding).second, "duplicate shader binding");
@ -948,6 +950,10 @@ void ShaderResources::buildPrepare(std::span<const CompiledShader> shaders, cons
bindings.push_back(std::move(item));
}
for (const auto index : offsetsInData) allocations[index].dataAllocation = shaderData;
for (auto index = firstDeferred; index < deferredImages.size(); ++index) {
deferredImages[index].firstSampler = firstSampler;
deferredImages[index].samplerCount = samplers.size() - firstSampler;
}
}
Require(storageBuffers <= context.limits.maxDescriptorSetStorageBuffers, "pipeline descriptors exceed device limits");
timing.bindingsMs = phase(BuildPhase::Bindings);
@ -1015,7 +1021,7 @@ void ShaderResources::buildComplete() {
try {
// The lookups run in plan order: Revalidate walks the bindings the same way, and consecutive
// storage elements of one mip chain share the previous element's image.
for (const auto& deferred : deferredImages) resolveImageBinding(*deferred.binding, bindings[deferred.index]);
for (const auto& deferred : deferredImages) resolveImageBinding(*deferred.binding, bindings[deferred.index], std::span<const std::shared_ptr<Sampler>>(samplers).subspan(deferred.firstSampler, deferred.samplerCount));
deferredImages.clear();
// The records served their purpose: each holds the cache entry's objects as of stage A,
// which would otherwise keep a replaced texture or an evicted storage image (and its device
@ -1213,6 +1219,8 @@ std::vector<std::uint32_t> ShaderResources::ContentKey(const CompiledShader& sha
// Read-only elements are bound without a write set: an object built for one written set
// must not serve a build with another (the variant implies it, this makes it explicit).
packBits(binding.bufferWritten);
key.push_back(static_cast<std::uint32_t>(binding.imageSamplers.size()));
key.insert(key.end(), binding.imageSamplers.begin(), binding.imageSamplers.end());
}
return key;
}
@ -2523,11 +2531,12 @@ std::shared_ptr<Texture> ShaderResources::fastTexture(const ImageRecord& record)
return record.texture;
}
void ShaderResources::resolveImageBinding(const ShaderRecompiler::DescriptorBinding& binding, Binding& item) {
void ShaderResources::resolveImageBinding(const ShaderRecompiler::DescriptorBinding& binding, Binding& item, std::span<const std::shared_ptr<Sampler>> shaderSamplers) {
auto& counters = TextureCounts();
// The element's stage-A record, when the pass ran (records follow the plan order exactly).
const auto nextRecord = [&]() -> const ImageRecord* { return nextImageRecord < imageRecords.size() ? &imageRecords[nextImageRecord++] : nullptr; };
if (binding.kind == ShaderRecompiler::DescriptorKind::SampledImage) {
Require(binding.imageSamplers.size() == binding.count, "guest sampled image binding is missing its image-sampler pairs");
const auto elementWords = binding.guestDescriptor.size() / binding.count;
for (std::uint32_t element = 0; element < binding.count; ++element) {
const auto words = std::span<const std::uint32_t>(binding.guestDescriptor).subspan(static_cast<std::size_t>(element) * elementWords, elementWords);
@ -2548,6 +2557,7 @@ void ShaderResources::resolveImageBinding(const ShaderRecompiler::DescriptorBind
const bool singleLevel = range.levels == 1u && range.layers == 1u && resource.baseLevel == 0u && EffectiveMinLod(resource) == 0.0f;
if (!singleLevel || (range.type != VK_IMAGE_VIEW_TYPE_1D && range.type != VK_IMAGE_VIEW_TYPE_2D)) throw std::runtime_error("AGC graphics: guest texture sampled with unnormalized coordinates is not a single-level, single-layer 1D or 2D view starting at mip 0, which is not implemented (base level " + std::to_string(resource.baseLevel) + ", levels " + std::to_string(range.levels) + ", layers " + std::to_string(range.layers) + ", view type " + std::to_string(static_cast<int>(range.type)) + ")");
}
RequireFilterMinmax(context, texture->ViewFormat(), binding.imageSamplers[element], shaderSamplers);
textures.push_back(std::move(texture));
textureFirstLayer.push_back(firstLayer);
describedRanges.push_back({"texture", resource.baseAddress, guestBytes, resource.width, resource.height, resource.format, static_cast<int>(resource.tileMode), resource.dccAddress});

View File

@ -423,6 +423,7 @@ Texture::Texture(const Context& context, TextureDetiler& detiler, const GuestTex
viewInfo.image = image;
viewInfo.viewType = ViewTypeFor(descriptor.dimension, viewLayerCount);
viewInfo.format = vkFormat;
viewFormat = vkFormat;
viewInfo.components = depthCompare ? VkComponentMapping{} : components;
viewInfo.subresourceRange = {aspect, descriptor.baseLevel, viewLevelCount, descriptor.baseArray, viewLayerCount};
VkImageViewMinLodCreateInfoEXT minLod{VK_STRUCTURE_TYPE_IMAGE_VIEW_MIN_LOD_CREATE_INFO_EXT};
@ -469,6 +470,7 @@ Texture::Texture(const Context& context, const std::shared_ptr<StorageTexture>&
viewInfo.image = source->Image();
viewInfo.viewType = ViewTypeFor(descriptor.dimension, viewLayerCount);
viewInfo.format = vkFormat;
viewFormat = vkFormat;
viewInfo.components = components;
viewInfo.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, descriptor.baseLevel, viewLevelCount, descriptor.baseArray, viewLayerCount};
VkImageViewMinLodCreateInfoEXT minLod{VK_STRUCTURE_TYPE_IMAGE_VIEW_MIN_LOD_CREATE_INFO_EXT};
@ -494,6 +496,7 @@ Texture::Texture(const Context& context, VkImage depthImage, VkFormat depthForma
viewInfo.image = depthImage;
viewInfo.viewType = VK_IMAGE_VIEW_TYPE_2D;
viewInfo.format = depthFormat;
viewFormat = depthFormat;
viewInfo.components = components;
viewInfo.subresourceRange = {aspect, 0, 1, 0, 1};
Check(context.Function<PFN_vkCreateImageView>("vkCreateImageView")(context.device, &viewInfo, nullptr, &view), "vkCreateImageView depth plane");

View File

@ -1,9 +1,14 @@
#include "GraphicsTests.hpp"
#include "Optimization/DescriptorBindingBuilder.hpp"
#include "prx/libSceAgcDriver/Graphics/include/GuestSamplerResource.hpp"
#include "prx/libSceAgcDriver/Graphics/include/Sampler.hpp"
#include <array>
#include <cmath>
#include <cstring>
#include <memory>
#include <string>
#include <string_view>
#include <type_traits>
namespace {
@ -84,6 +89,138 @@ bool nearlyEqual(float a, float b) {
return std::fabs(a - b) < 0.001f;
}
struct SamplerCapture {
bool created = false;
const void* next = nullptr;
VkSamplerReductionMode reductionMode = VK_SAMPLER_REDUCTION_MODE_MAX_ENUM;
};
SamplerCapture samplerCapture;
template<typename THandle>
THandle fakeHandle(std::uint64_t value) {
if constexpr (std::is_pointer_v<THandle>) return reinterpret_cast<THandle>(static_cast<std::uintptr_t>(value));
else return static_cast<THandle>(value);
}
VKAPI_ATTR VkResult VKAPI_CALL captureCreateSampler(VkDevice, const VkSamplerCreateInfo* info, const VkAllocationCallbacks*, VkSampler* sampler) {
samplerCapture.created = true;
samplerCapture.next = info->pNext;
const auto* reduction = static_cast<const VkSamplerReductionModeCreateInfoEXT*>(info->pNext);
if (reduction != nullptr && reduction->sType == VK_STRUCTURE_TYPE_SAMPLER_REDUCTION_MODE_CREATE_INFO_EXT && reduction->pNext == nullptr) samplerCapture.reductionMode = reduction->reductionMode;
*sampler = fakeHandle<VkSampler>(0x5a);
return VK_SUCCESS;
}
VKAPI_ATTR void VKAPI_CALL captureDestroySampler(VkDevice, VkSampler, const VkAllocationCallbacks*) {}
PFN_vkVoidFunction VKAPI_CALL captureProc(VkDevice, const char* name) {
if (std::strcmp(name, "vkCreateSampler") == 0) return reinterpret_cast<PFN_vkVoidFunction>(captureCreateSampler);
if (std::strcmp(name, "vkDestroySampler") == 0) return reinterpret_cast<PFN_vkVoidFunction>(captureDestroySampler);
return nullptr;
}
SamplerCapture createSampler(const Context& context, const GuestSamplerResource& resource) {
samplerCapture = {};
const Sampler sampler(context, resource);
Require(sampler.Handle() != VK_NULL_HANDLE, "sampler test did not create a sampler");
return samplerCapture;
}
void RunSamplerReductionTests(const Fields& base) {
Context context{};
context.limits.maxSamplerAnisotropy = 1.0f;
context.deviceProc = captureProc;
context.samplerFilterMinmax = true;
const std::array<std::uint32_t, 4> capturedSampler{0x20000092u, 0x00fff000u, 0x05500000u, 0u};
const auto captured = DecodeSamplerResource(capturedSampler);
Require(captured.reductionMode == VK_SAMPLER_REDUCTION_MODE_MIN_EXT, "captured min-reduction sampler decoded to another reduction mode");
Require(captured.magFilter == VK_FILTER_LINEAR && captured.minFilter == VK_FILTER_LINEAR && captured.mipmapMode == VK_SAMPLER_MIPMAP_MODE_NEAREST, "captured min-reduction sampler filters decoded incorrectly");
Require(captured.addressModeU == VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE && captured.addressModeV == VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE && captured.addressModeW == VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE, "captured min-reduction sampler address modes decoded incorrectly");
Require(!captured.anisotropyEnable && captured.compareOp == VK_COMPARE_OP_NEVER && nearlyEqual(captured.maxLod, 4095.0f / 256.0f), "captured min-reduction sampler fields decoded incorrectly");
const std::array modes{VK_SAMPLER_REDUCTION_MODE_WEIGHTED_AVERAGE_EXT, VK_SAMPLER_REDUCTION_MODE_MIN_EXT, VK_SAMPLER_REDUCTION_MODE_MAX_EXT};
for (std::uint32_t mode = 0; mode < modes.size(); ++mode) {
for (const std::uint32_t filter : {0u, 1u}) {
Fields reduced = base;
reduced.filterMode = mode;
reduced.xyMagFilter = filter;
reduced.xyMinFilter = filter;
reduced.mipFilter = 1;
const auto decoded = DecodeSamplerResource(pack(reduced));
Require(decoded.reductionMode == modes.at(mode), "sampler reduction mode decoded incorrectly");
const auto created = createSampler(context, decoded);
Require(created.created, "sampler test did not reach vkCreateSampler");
if (mode == 0u) Require(created.next == nullptr, "a weighted-average sampler chained a reduction mode");
else Require(created.reductionMode == modes.at(mode), "a min or max sampler did not chain its reduction mode");
const Sampler sampler(context, decoded);
Require(sampler.RequiresFilterMinmax() == (mode != 0u && filter == 1u), "sampler min/max format requirement is wrong");
}
}
Fields noMip = base;
noMip.filterMode = 2;
noMip.mipFilter = 0;
Require(DecodeSamplerResource(pack(noMip)).reductionMode == VK_SAMPLER_REDUCTION_MODE_MAX_EXT, "a max sampler without mip filtering was not decoded");
Fields linearMip = base;
linearMip.filterMode = 1;
rejectFields(linearMip, "min or max reduction with a linear mip filter");
linearMip.xyMagFilter = 0;
linearMip.xyMinFilter = 0;
rejectFields(linearMip, "min or max reduction with a linear mip filter");
Fields badFilterMode = base;
badFilterMode.filterMode = 3;
rejectFields(badFilterMode, "unknown reduction filter mode 3");
Fields anisoReduction = base;
anisoReduction.filterMode = 1;
anisoReduction.xyMagFilter = 3;
anisoReduction.xyMinFilter = 3;
anisoReduction.maxAnisoRatio = 2;
anisoReduction.mipFilter = 1;
rejectFields(anisoReduction, "min or max reduction with anisotropic filtering");
anisoReduction.filterMode = 0;
Require(DecodeSamplerResource(pack(anisoReduction)).anisotropyEnable, "anisotropic weighted-average sampler was rejected");
auto compared = captured;
compared.compareEnable = true;
reject([&] { createSampler(context, compared); }, "min or max reduction with depth comparison");
auto weightedCompare = DecodeSamplerResource(pack(base));
weightedCompare.compareEnable = true;
Require(createSampler(context, weightedCompare).created, "weighted-average comparison sampler was rejected");
context.samplerFilterMinmax = false;
reject([&] { createSampler(context, captured); }, "min or max reduction which the device does not support");
Require(createSampler(context, DecodeSamplerResource(pack(base))).created, "weighted-average sampler needs min/max support");
context.formatProperties = [](VkPhysicalDevice, VkFormat format, VkFormatProperties* properties) {
*properties = {};
if (format == VK_FORMAT_R32_SFLOAT) properties->optimalTilingFeatures = VK_FORMAT_FEATURE_SAMPLED_IMAGE_BIT | VK_FORMAT_FEATURE_SAMPLED_IMAGE_FILTER_MINMAX_BIT_EXT;
if (format == VK_FORMAT_R32G32_SFLOAT) properties->optimalTilingFeatures = VK_FORMAT_FEATURE_SAMPLED_IMAGE_BIT | VK_FORMAT_FEATURE_SAMPLED_IMAGE_FILTER_LINEAR_BIT;
if (format == VK_FORMAT_R8G8B8A8_UNORM) properties->linearTilingFeatures = VK_FORMAT_FEATURE_SAMPLED_IMAGE_FILTER_MINMAX_BIT_EXT;
};
context.samplerFilterMinmax = true;
Fields minPoint = base;
minPoint.filterMode = 1;
minPoint.xyMagFilter = 0;
minPoint.xyMinFilter = 0;
minPoint.mipFilter = 1;
const std::array samplers{std::make_shared<Sampler>(context, DecodeSamplerResource(pack(base))), std::make_shared<Sampler>(context, captured), std::make_shared<Sampler>(context, DecodeSamplerResource(pack(minPoint)))};
RequireFilterMinmax(context, VK_FORMAT_R32_SFLOAT, 0b010u, samplers);
RequireFilterMinmax(context, VK_FORMAT_R8G8B8A8_UNORM, 0u, samplers);
RequireFilterMinmax(context, VK_FORMAT_R8G8B8A8_UNORM, 0b101u, samplers);
reject([&] { RequireFilterMinmax(context, VK_FORMAT_R8G8B8A8_UNORM, 0b010u, samplers); }, "format 37 does not support min/max filtering is sampled through a min or max reduction sampler");
reject([&] { RequireFilterMinmax(context, VK_FORMAT_R32G32_SFLOAT, 0b011u, samplers); }, "does not support min/max filtering");
reject([&] { RequireFilterMinmax(context, VK_FORMAT_R32_SFLOAT, 0b1000u, samplers); }, "sampler element 3, which its shader does not bind");
const auto pointFiltered = [](const std::array<std::uint32_t, 4>& words) { return ShaderRecompiler::PointFilteredSamplerWord(words[0], words[2]); };
Require(pointFiltered(pack(base)) == 0x05000000u, "a point-filtered weighted-average sampler kept its bilinear or linear mip filter");
Require(pointFiltered(pack(minPoint)) == pack(minPoint)[2], "a point-filtered min sampler that already point-samples changed");
reject([&] { pointFiltered(capturedSampler); }, "needs point filtering");
Fields maxLinearMip = minPoint;
maxLinearMip.filterMode = 2;
maxLinearMip.mipFilter = 2;
reject([&] { pointFiltered(pack(maxLinearMip)); }, "needs point filtering");
}
}
void RunGuestSamplerResourceTests() {
@ -239,9 +376,7 @@ void RunGuestSamplerResourceTests() {
badCubeWrap.disableCubeWrap = true;
rejectFields(badCubeWrap, "seamless cube filtering");
Fields badFilterMode = base;
badFilterMode.filterMode = 1;
rejectFields(badFilterMode, "reduction filter mode");
RunSamplerReductionTests(base);
Fields badDegamma = base;
badDegamma.disableDegamma = true;

View File

@ -31,7 +31,7 @@ void setEnvironment(const char* name, const std::string& value) {
}
bool sameBinding(const DescriptorBinding& left, const DescriptorBinding& right) {
return left.kind == right.kind && left.role == right.role && left.descriptorSet == right.descriptorSet && left.binding == right.binding && left.count == right.count && left.guestDescriptor == right.guestDescriptor && left.readOnly == right.readOnly && left.imageShape == right.imageShape && left.samplerDepthCompare == right.samplerDepthCompare && left.imageWritten == right.imageWritten && left.imageDepthCompare == right.imageDepthCompare && left.imageAtomic == right.imageAtomic && left.bufferAtomic == right.bufferAtomic && left.bufferWritten == right.bufferWritten && left.samplerUnnormalized == right.samplerUnnormalized && left.imageUnnormalized == right.imageUnnormalized;
return left.kind == right.kind && left.role == right.role && left.descriptorSet == right.descriptorSet && left.binding == right.binding && left.count == right.count && left.guestDescriptor == right.guestDescriptor && left.readOnly == right.readOnly && left.imageShape == right.imageShape && left.samplerDepthCompare == right.samplerDepthCompare && left.imageWritten == right.imageWritten && left.imageDepthCompare == right.imageDepthCompare && left.imageAtomic == right.imageAtomic && left.bufferAtomic == right.bufferAtomic && left.bufferWritten == right.bufferWritten && left.samplerUnnormalized == right.samplerUnnormalized && left.imageUnnormalized == right.imageUnnormalized && left.imageSamplers == right.imageSamplers;
}
bool sameBindings(const std::vector<DescriptorBinding>& left, const std::vector<DescriptorBinding>& right) {
@ -97,6 +97,7 @@ DescriptorBinding sampleBinding(std::uint32_t seed) {
binding.bufferWritten = {false, false, true, true, false};
binding.samplerUnnormalized = {false, true, true};
binding.imageUnnormalized = {true, false, seed % 2 == 0};
binding.imageSamplers = {0x5u, 0u, 0x80000000u};
return binding;
}

View File

@ -38,6 +38,7 @@ constexpr std::uint32_t BorderTable = 3;
constexpr std::uint32_t FilterPoint = 0;
constexpr std::uint32_t FilterBilinear = 1;
constexpr std::uint32_t FilterAnisoBilinear = 3;
constexpr std::uint32_t ReductionMin = 1;
constexpr std::array<std::uint8_t, 4> Red{0, 64, 128, 255};
alignas(256) std::array<float, Threads * 3> Input{};
@ -53,6 +54,7 @@ struct Sampler {
std::uint32_t clamp;
std::uint32_t filter;
std::uint32_t border = BorderBlack;
std::uint32_t reduction = 0;
};
std::array<std::uint32_t, 4> BufferDescriptor(const void* data, std::uint32_t bytes) {
@ -73,7 +75,7 @@ std::array<std::uint32_t, 8> TextureDescriptor(std::uint32_t format) {
std::array<std::uint32_t, 4> SamplerDescriptor(const Sampler& sampler) {
return {
sampler.clamp | (sampler.clamp << 3u) | (ClampEdge << 6u) | (LessEqual << 12u),
sampler.clamp | (sampler.clamp << 3u) | (ClampEdge << 6u) | (LessEqual << 12u) | (sampler.reduction << 29u),
0u,
(sampler.filter << 20u) | (sampler.filter << 22u),
sampler.border << 30u,
@ -214,6 +216,7 @@ int main() {
Reject(*device, Format8888UNorm, {ClampHalfBorder, FilterPoint}, "wrap, clamp-to-edge or clamp-to-border");
Reject(*device, Format8888UNorm, {ClampBorder, FilterPoint, BorderTable}, "border color table");
Reject(*device, Format8888UNorm, {ClampEdge, FilterAnisoBilinear}, "point or bilinear");
Reject(*device, Format8888UNorm, {ClampEdge, FilterBilinear, BorderBlack, ReductionMin}, "min or max reduction");
std::puts("emulated color compare tests passed");
return 0;
} catch (const std::exception& error) {

View File

@ -0,0 +1,319 @@
#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 Levels = 2;
constexpr std::uint32_t Format32SInt = 21;
constexpr std::uint32_t Format32Float = 22;
constexpr std::uint32_t Format8888UNorm = 56;
constexpr std::uint32_t Type2D = 9;
constexpr std::uint32_t ReductionMin = 1;
constexpr std::uint32_t ReductionMax = 2;
constexpr std::uint32_t FilterPoint = 0;
constexpr std::uint32_t FilterBilinear = 1;
constexpr std::uint32_t MipNone = 0;
constexpr std::uint32_t MipPoint = 1;
constexpr std::uint32_t MipLinear = 2;
constexpr float Margin = 0.05f;
constexpr std::array<float, Size * Size> Level0{17, 11, 23, 14, 20, 25, 12, 19, 13, 22, 16, 24, 21, 10, 18, 15};
constexpr std::array<float, (Size / 2) * (Size / 2)> Level1{5, 30, 16.5f, 9};
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(4096) std::array<std::uint8_t, 4096> Colors{};
alignas(256) constexpr std::array<std::uint32_t, 13> Code{
0x1614008c, 0xe03c1000, 0x8000010a, 0xbf8c3f70, 0xf0900108, 0x00820401, 0xbf8c3f70,
0x34160082, 0xe0701000, 0x8001040b, 0xbf810000, 0xbf810000, 0xbf810000,
};
alignas(256) constexpr std::array<std::uint32_t, 13> GatherCode{
0x1614008c, 0xe03c1000, 0x8000010a, 0xbf8c3f70, 0xf11c0108, 0x00820401, 0xbf8c3f70,
0x34160084, 0xe0781000, 0x8001040b, 0xbf810000, 0xbf810000, 0xbf810000,
};
alignas(256) constexpr std::array<std::uint32_t, 19> LoadedCode{
0x1614008c, 0xe03c1000, 0x8000010a, 0x36180083, 0x7e1a0280, 0xbf8c3f70, 0xf0900108, 0x00820401, 0xf0000108, 0x0005050c,
0xbf8c3f70, 0x34160082, 0xe0701000, 0x8001040b, 0xe0701080, 0x8001050b, 0xbf810000, 0xbf810000, 0xbf810000,
};
alignas(256) constexpr std::array<std::uint32_t, 17> SampledCode{
0x1614008c, 0xe03c1000, 0x8000010a, 0xbf8c3f70, 0xf0900108, 0x00820401, 0xf0900108, 0x00850501, 0xbf8c3f70,
0x34160082, 0xe0701000, 0x8001040b, 0xe0701080, 0x8001050b, 0xbf810000, 0xbf810000, 0xbf810000,
};
struct Sampler {
std::uint32_t reduction;
std::uint32_t filter;
std::uint32_t mip;
};
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, 0x01016facu};
}
std::array<std::uint32_t, 8> TextureDescriptor(std::uint32_t format, const void* texels = Texels.data(), std::uint32_t levels = Levels) {
const auto address = static_cast<std::uint64_t>(reinterpret_cast<std::uintptr_t>(texels));
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 | ((levels - 1u) << 16u) | (Type2D << 28u),
0u,
(levels - 1u) << 4u,
0u,
0u,
};
}
std::array<std::uint32_t, 4> SamplerDescriptor(const Sampler& sampler) {
return {0x92u | (sampler.reduction << 29u), 0x00fff000u, (sampler.filter << 20u) | (sampler.filter << 22u) | (1u << 24u) | (sampler.mip << 26u), 0u};
}
void FillTexels() {
const auto mips = AgcDriver::Graphics::ComputeMipLayout(AgcDriver::Graphics::TextureTileMode::kLinear, Format32Float, Size, Size, Levels);
Require(AgcDriver::Graphics::ComputeSurfaceSize(mips, 1) <= Texels.size(), "image sample reduction: the mip chain does not fit the texel storage");
Texels.fill(0);
for (std::uint32_t level = 0; level < Levels; ++level) {
const auto& mip = mips[level];
const auto& values = level == 0 ? std::span<const float>(Level0) : std::span<const float>(Level1);
for (std::uint32_t y = 0; y < mip.height; ++y) {
for (std::uint32_t x = 0; x < mip.width; ++x) {
const auto bits = std::bit_cast<std::uint32_t>(values[y * mip.width + x]);
auto* texel = &Texels[mip.tiledOffset + static_cast<std::uint64_t>(y) * mip.pitchBytes + x * 4u];
for (std::uint32_t byte = 0; byte < 4u; ++byte) texel[byte] = static_cast<std::uint8_t>(bits >> (byte * 8u));
}
}
}
}
std::uint8_t Red(int x, int y) {
const auto cx = static_cast<std::uint32_t>(std::clamp(x, 0, static_cast<int>(Size) - 1));
const auto cy = static_cast<std::uint32_t>(std::clamp(y, 0, static_cast<int>(Size) - 1));
return static_cast<std::uint8_t>(17u * ((cy * 7u + cx * 5u) % 16u));
}
void FillColors() {
const auto mips = AgcDriver::Graphics::ComputeMipLayout(AgcDriver::Graphics::TextureTileMode::kLinear, Format8888UNorm, Size, Size, 1);
Require(AgcDriver::Graphics::ComputeSurfaceSize(mips, 1) <= Colors.size(), "image sample reduction: the 8_8_8_8 texture does not fit its storage");
Colors.fill(0);
for (std::uint32_t y = 0; y < Size; ++y) {
for (std::uint32_t x = 0; x < Size; ++x) {
auto* texel = &Colors[mips[0].tiledOffset + static_cast<std::uint64_t>(y) * mips[0].pitchBytes + x * 4u];
texel[0] = Red(static_cast<int>(x), static_cast<int>(y));
texel[3] = 255;
}
}
}
bool Safe(float coordinate) {
for (std::uint32_t size = Size; size >= Size / 2; size /= 2) {
for (const float offset : {0.0f, -0.5f}) {
const float scaled = coordinate * static_cast<float>(size) + offset;
const float fraction = scaled - std::floor(scaled);
if (fraction < Margin || fraction > 1.0f - Margin) return false;
}
}
return true;
}
void FillInput() {
for (std::uint32_t tid = 0; tid < Threads; ++tid) {
const float u = (static_cast<float>((tid * 5u + 1u) % 12u) - 1.7f) / 8.0f;
const float v = (static_cast<float>((tid * 7u + 3u) % 12u) - 1.7f) / 8.0f;
Require(Safe(u) && Safe(v), "image sample reduction: thread " + std::to_string(tid) + " samples too close to a texel boundary");
Input[tid * 3u + 0u] = u;
Input[tid * 3u + 1u] = v;
Input[tid * 3u + 2u] = tid % 2u == 0u ? 0.25f : 0.75f;
}
}
float Texel(std::uint32_t level, int x, int y) {
const int size = static_cast<int>(Size >> level);
const auto cx = static_cast<std::uint32_t>(std::clamp(x, 0, size - 1));
const auto cy = static_cast<std::uint32_t>(std::clamp(y, 0, size - 1));
return level == 0 ? Level0[cy * Size + cx] : Level1[cy * (Size / 2) + cx];
}
float Expected(std::uint32_t tid, const Sampler& sampler) {
const float u = Input[tid * 3u + 0u];
const float v = Input[tid * 3u + 1u];
const float lod = Input[tid * 3u + 2u];
std::vector<float> footprint;
const auto gather = [&](std::uint32_t level) {
const float size = static_cast<float>(Size >> level);
if (sampler.filter == FilterPoint) {
footprint.push_back(Texel(level, static_cast<int>(std::floor(u * size)), static_cast<int>(std::floor(v * size))));
return;
}
const int x = static_cast<int>(std::floor(u * size - 0.5f));
const int y = static_cast<int>(std::floor(v * size - 0.5f));
for (int dy = 0; dy < 2; ++dy) {
for (int dx = 0; dx < 2; ++dx) footprint.push_back(Texel(level, x + dx, y + dy));
}
};
gather(sampler.mip == MipPoint && lod > 0.5f ? 1u : 0u);
return sampler.reduction == ReductionMin ? *std::min_element(footprint.begin(), footprint.end()) : *std::max_element(footprint.begin(), footprint.end());
}
std::array<float, 4> ExpectedGather(std::uint32_t tid) {
const int x = static_cast<int>(std::floor(Input[tid * 3u + 0u] * static_cast<float>(Size) - 0.5f));
const int y = static_cast<int>(std::floor(Input[tid * 3u + 1u] * static_cast<float>(Size) - 0.5f));
return {Texel(0, x, y + 1), Texel(0, x + 1, y + 1), Texel(0, x + 1, y), Texel(0, x, y)};
}
ShaderRecompiler::RecompileResult Compile(AgcDriver::VulkanDevice& device, std::uint32_t format, const Sampler& sampler, std::span<const std::uint32_t> code = Code) {
std::vector<std::uint32_t> userData(28, 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 samplerWords = SamplerDescriptor(sampler);
const auto colors = TextureDescriptor(Format8888UNorm, Colors.data(), 1);
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(samplerWords.begin(), samplerWords.end(), userData.begin() + 16);
std::copy(colors.begin(), colors.end(), userData.begin() + 20);
const std::array<ShaderRecompiler::MemoryRegion, 1> memory{{{reinterpret_cast<std::uintptr_t>(code.data()), std::as_bytes(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;
return ShaderRecompiler::Recompile(request);
}
void Run(AgcDriver::VulkanDevice& device, const Sampler& sampler, const char* name) {
Output.fill(-1.0f);
const auto result = Compile(device, Format32Float, sampler);
device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast<std::uintptr_t>(Code.data()));
device.WaitIdle();
for (std::uint32_t tid = 0; tid < Threads; ++tid) {
const float expected = Expected(tid, sampler);
Require(Output[tid] == expected, std::string(name) + ": thread " + std::to_string(tid) + " sampled " + std::to_string(Output[tid]) + ", expected " + std::to_string(expected));
}
}
void Gather(AgcDriver::VulkanDevice& device, const Sampler& sampler, const char* name) {
Output.fill(-1.0f);
const auto result = Compile(device, Format32Float, sampler, GatherCode);
device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast<std::uintptr_t>(GatherCode.data()));
device.WaitIdle();
for (std::uint32_t tid = 0; tid < Threads; ++tid) {
const auto expected = ExpectedGather(tid);
for (std::uint32_t component = 0; component < 4u; ++component) {
const float sampled = Output[tid * 4u + component];
Require(sampled == expected[component], std::string(name) + ": thread " + std::to_string(tid) + " component " + std::to_string(component) + " gathered " + std::to_string(sampled) + ", expected " + std::to_string(expected[component]));
}
}
}
std::uint32_t PairedSamplers(const ShaderRecompiler::RecompileResult& result, std::uint32_t format) {
for (const auto& binding : result.bindings) {
if (binding.kind != ShaderRecompiler::DescriptorKind::SampledImage) continue;
for (std::uint32_t element = 0; element < binding.count; ++element) {
if (((binding.guestDescriptor.at(element * 8u + 1u) >> 20u) & 0x1ffu) == format) return binding.imageSamplers.at(element);
}
}
Require(false, "image sample reduction: no sampled image of format " + std::to_string(format));
return 0;
}
float ExpectedColor(std::uint32_t tid, bool sampled) {
if (!sampled) return static_cast<float>(Red(static_cast<int>(tid & 3u), 0)) / 255.0f;
const int x = static_cast<int>(std::floor(Input[tid * 3u + 0u] * static_cast<float>(Size) - 0.5f));
const int y = static_cast<int>(std::floor(Input[tid * 3u + 1u] * static_cast<float>(Size) - 0.5f));
return static_cast<float>(std::min({Red(x, y), Red(x + 1, y), Red(x, y + 1), Red(x + 1, y + 1)})) / 255.0f;
}
void RunWithColors(AgcDriver::VulkanDevice& device, const ShaderRecompiler::RecompileResult& result, std::span<const std::uint32_t> code, bool sampled, const char* name) {
const Sampler sampler{ReductionMin, FilterBilinear, MipPoint};
Output.fill(-1.0f);
device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast<std::uintptr_t>(code.data()));
device.WaitIdle();
for (std::uint32_t tid = 0; tid < Threads; ++tid) {
const float expected = Expected(tid, sampler);
Require(Output[tid] == expected, std::string(name) + ": thread " + std::to_string(tid) + " sampled " + std::to_string(Output[tid]) + " from 32_FLOAT, expected " + std::to_string(expected));
const float color = ExpectedColor(tid, sampled);
Require(std::fabs(Output[Threads + tid] - color) <= 1.0f / 1024.0f, std::string(name) + ": thread " + std::to_string(tid) + " read " + std::to_string(Output[Threads + tid]) + " from 8_8_8_8, expected " + std::to_string(color));
}
}
void Reject(AgcDriver::VulkanDevice& device, std::uint32_t format, const Sampler& sampler, std::string_view reason) {
try {
static_cast<void>(Compile(device, format, sampler));
} 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();
FillColors();
FillInput();
const Sampler minBilinear{ReductionMin, FilterBilinear, MipPoint};
const auto loaded = Compile(*device, Format32Float, minBilinear, LoadedCode);
Require(PairedSamplers(loaded, Format32Float) == 1u && PairedSamplers(loaded, Format8888UNorm) == 0u, "image_load of 8_8_8_8 next to a min sampler: wrong image-sampler pairs");
const auto sampled = Compile(*device, Format32Float, minBilinear, SampledCode);
Require(PairedSamplers(sampled, Format32Float) == 1u && PairedSamplers(sampled, Format8888UNorm) == 1u, "8_8_8_8 sampled through a min sampler: wrong image-sampler pairs");
static_cast<void>(Compile(*device, Format32SInt, {ReductionMin, FilterPoint, MipPoint}));
Reject(*device, Format32SInt, {ReductionMin, FilterBilinear, MipPoint}, "needs point filtering");
Reject(*device, Format32SInt, {ReductionMax, FilterPoint, MipLinear}, "needs point filtering");
if (!device->SamplerFilterMinmax()) {
std::puts("skipped, the device has no min/max sampler reduction with component mapping");
return VulkanTestSkipped;
}
Run(*device, {ReductionMin, FilterBilinear, MipPoint}, "min, bilinear, point mip");
Run(*device, {ReductionMax, FilterBilinear, MipPoint}, "max, bilinear, point mip");
Run(*device, {ReductionMin, FilterPoint, MipPoint}, "min, point, point mip");
Run(*device, {ReductionMax, FilterPoint, MipPoint}, "max, point, point mip");
Run(*device, {ReductionMin, FilterBilinear, MipNone}, "min, bilinear, no mip");
Run(*device, {ReductionMax, FilterPoint, MipNone}, "max, point, no mip");
Gather(*device, {ReductionMin, FilterBilinear, MipPoint}, "gather4_lz, min");
Gather(*device, {ReductionMax, FilterBilinear, MipPoint}, "gather4_lz, max");
RunWithColors(*device, loaded, LoadedCode, false, "min 32_FLOAT, image_load 8_8_8_8");
try {
RunWithColors(*device, sampled, SampledCode, true, "min 32_FLOAT and 8_8_8_8");
std::puts("the device filters 8_8_8_8 UNORM with min/max reduction");
} catch (const std::exception& error) {
Require(std::string_view(error.what()).find("format 37 does not support min/max filtering is sampled through a min or max reduction sampler") != std::string_view::npos, std::string("min 32_FLOAT and 8_8_8_8: unexpected error: ") + error.what());
}
std::puts("image sample reduction tests passed");
return 0;
} catch (const std::exception& error) {
std::cerr << error.what() << '\n';
return 1;
}
}

View File

@ -4,9 +4,12 @@
#include "IntermediateRepresentation/IrProgram.hpp"
#include "Optimization/BindingAllocator.hpp"
#include "Recompiler.hpp"
#include <cstdint>
namespace ShaderRecompiler {
std::uint32_t PointFilteredSamplerWord(std::uint32_t word0, std::uint32_t filter);
class DescriptorBindingBuilder {
public:
void Populate(BindingAllocationResult& allocation, const IrProgram& program, const ResourceSnapshot& snapshot, const std::array<std::uint32_t, 3>& partialThreads) const;

View File

@ -240,6 +240,34 @@ UnnormalizedProof ProveUnnormalized(const ShaderInfo& info, const ResourceSnapsh
return proof;
}
std::vector<std::uint32_t> SamplerElements(const IrBindingLayout& layout, const ShaderInfo& info) {
std::vector<std::uint32_t> elements(info.samplers.size(), ShaderInfo::MaxSamplers);
for (const IrDescriptorBinding& logical : layout.descriptors) {
if (logical.kind != DescriptorBindingKind::Samplers) {
continue;
}
for (std::uint32_t element = 0; element < logical.resources.size() && element < ShaderInfo::MaxSamplers; element++) {
elements.at(logical.resources[element]) = element;
}
}
return elements;
}
std::uint32_t ImageSamplerMask(const ShaderInfo& info, const std::vector<std::uint32_t>& samplerElements, std::uint32_t resource) {
const std::uint32_t root = info.images.at(resource).indirectRoot;
std::uint32_t mask = 0;
for (const SampledResourcePair& pair : info.sampledPairs) {
if (pair.image != resource && pair.image != root) {
continue;
}
if (pair.sampler >= samplerElements.size() || samplerElements[pair.sampler] >= ShaderInfo::MaxSamplers) {
fail("DescriptorBindingBuilder::Populate sampled image pair names a sampler outside the first " + std::to_string(ShaderInfo::MaxSamplers) + " elements of the sampler binding");
}
mask |= 1u << samplerElements[pair.sampler];
}
return mask;
}
std::vector<std::uint32_t> ShaderDataDwordsFor(const IrBindingLayout& layout, std::uint32_t userDataBase, const ResourceSnapshot& snapshot, const std::array<std::uint32_t, 3>& partialThreads) {
std::vector<std::uint32_t> result(layout.ShaderDataDwords(), 0u);
for (std::size_t i = 0; i < layout.userDataRegisters.size(); i++) {
@ -260,6 +288,15 @@ std::vector<std::uint32_t> ShaderDataDwordsFor(const IrBindingLayout& layout, st
}
std::uint32_t PointFilteredSamplerWord(std::uint32_t word0, std::uint32_t filter) {
const bool reduced = ((word0 >> 29u) & 3u) != 0u;
if (reduced && (((filter >> 20u) & 0xfu) != 0u || ((filter >> 26u) & 3u) == 2u)) {
fail("DescriptorBindingBuilder: a min or max reduction sampler that filters between texels or mip levels samples an image that needs point filtering (sint, converted or depth-bits format), which is not implemented");
}
const bool mipmapped = ((filter >> 26u) & 3u) != 0u;
return (filter & ~(0xffu << 20u)) | (1u << 24u) | (mipmapped ? 1u << 26u : 0u);
}
void DescriptorBindingBuilder::Populate(BindingAllocationResult& allocation, const IrProgram& program, const ResourceSnapshot& snapshot, const std::array<std::uint32_t, 3>& partialThreads) const {
Populate(allocation, program.Info(), program.Resources().stage, program.Resources().userDataBase, snapshot, partialThreads);
}
@ -268,6 +305,7 @@ void DescriptorBindingBuilder::Populate(BindingAllocationResult& allocation, con
const IrBindingLayout& layout = allocation.layout;
const std::vector<std::uint32_t> shaderData = ShaderDataDwordsFor(layout, userDataBase, snapshot, partialThreads);
const UnnormalizedProof unnormalized = ProveUnnormalized(info, snapshot);
const std::vector<std::uint32_t> samplerElements = SamplerElements(layout, info);
std::vector<DescriptorBinding> bindings;
bindings.reserve(layout.descriptors.size());
@ -305,6 +343,7 @@ void DescriptorBindingBuilder::Populate(BindingAllocationResult& allocation, con
physical.imageDepthCompare.push_back(image.depthCompare);
physical.imageAtomic.push_back(image.atomic);
physical.imageUnnormalized.push_back(unnormalized.images.at(resource));
physical.imageSamplers.push_back(ImageSamplerMask(info, samplerElements, resource));
}
break;
case DescriptorRole::GuestSamplers:
@ -315,8 +354,7 @@ void DescriptorBindingBuilder::Populate(BindingAllocationResult& allocation, con
physical.samplerUnnormalized.push_back(unnormalized.samplers.at(logical.resources[element]));
if (sampler.forcePointFiltering) {
auto& filter = physical.guestDescriptor.at(element * 4u + 2u);
const bool mipmapped = ((filter >> 26u) & 3u) != 0u;
filter = (filter & ~(0xffu << 20u)) | (1u << 24u) | (mipmapped ? 1u << 26u : 0u);
filter = PointFilteredSamplerWord(physical.guestDescriptor.at(element * 4u), filter);
}
}
break;

View File

@ -536,6 +536,7 @@ std::uint32_t emulatedCompareState(const IrResourcePlan& plan, const ResourceSna
const auto clampY = (words[0] >> 3u) & 0x7u;
const auto function = (words[0] >> 12u) & 0x7u;
const bool unnormalized = ((words[0] >> 15u) & 0x1u) != 0u;
if (((words[0] >> 29u) & 0x3u) != 0u) throw std::runtime_error("comparison sampling of a color texture through a min or max reduction sampler is not implemented");
const auto lodBias = words[2] & 0x3fffu;
const auto magFilter = (words[2] >> 20u) & 0x3u;
const auto minFilter = (words[2] >> 22u) & 0x3u;

View File

@ -330,6 +330,7 @@ struct DescriptorBinding {
std::vector<bool> bufferWritten;
std::vector<bool> samplerUnnormalized;
std::vector<bool> imageUnnormalized;
std::vector<std::uint32_t> imageSamplers;
};
struct VertexAttribute {

View File

@ -64,7 +64,7 @@ namespace ShaderRecompiler::ShaderDiskCache {
#if defined(__linux__) && defined(__x86_64__) && defined(__GLIBCXX__)
static_assert(sizeof(RecompileResult) == 176, "RecompileResult changed: update EncodeResult and DecodeResult");
static_assert(sizeof(DescriptorBinding) == 384, "DescriptorBinding changed: update the binding encoder");
static_assert(sizeof(DescriptorBinding) == 408, "DescriptorBinding changed: update the binding encoder");
static_assert(sizeof(VertexAttribute) == 28, "VertexAttribute changed: update the attribute encoder");
static_assert(sizeof(FragmentParameter) == 12, "FragmentParameter changed: update the parameter encoder");
static_assert(sizeof(CompiledShaderInfo) == 304, "CompiledShaderInfo changed: update the info encoder");
@ -253,6 +253,7 @@ void encodeBinding(Writer& writer, const DescriptorBinding& binding) {
writer.Flags(binding.bufferWritten);
writer.Flags(binding.samplerUnnormalized);
writer.Flags(binding.imageUnnormalized);
writer.Values(std::span<const std::uint32_t>(binding.imageSamplers));
}
void decodeBinding(Reader& reader, DescriptorBinding& binding) {
@ -274,6 +275,7 @@ void decodeBinding(Reader& reader, DescriptorBinding& binding) {
reader.Flags(binding.bufferWritten);
reader.Flags(binding.samplerUnnormalized);
reader.Flags(binding.imageUnnormalized);
reader.Values(binding.imageSamplers);
}
void encodeResult(Writer& writer, const RecompileResult& result) {

View File

@ -12,7 +12,7 @@
namespace ShaderRecompiler::ShaderDiskCache {
inline constexpr std::uint32_t FormatVersion = 5;
inline constexpr std::uint32_t FormatVersion = 6;
enum class LoadStatus {
Loaded,

View File

@ -222,6 +222,8 @@ Throughout the project, every function at every stage either **does exactly what
- [Converted unorm images](../../core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp) (10_11_11_UNORM, read and written through an R32_UINT view): samples, gathers, LOD queries, 16-bit data and a DST_SEL of the missing W channel throw.
- [Converted unorm image loads](../../core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp) divide by 2^n - 1 with the buffer format path's `OpFDiv`, which Vulkan allows to be up to 2.5 ULP off, so some drivers are not bit-exact.
- [64-bit LDS atomics](../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp) (`ds_*_u64`, `ds_*_b64`, `ds_*_f64`) read and write the two dwords under a workgroup lock stored after the guest LDS, as LDS is a 32-bit array without 64-bit atomics. They are atomic with respect to each other only: 32-bit accesses to the same dwords issued concurrently are not ordered by the lock. 64-bit GDS atomics throw. `ds_min/max(_rtn)_f64` and `ds_cmpst(_rtn)_f64` follow the NaN and signed-zero rules measured for the f32 forms; the f64 forms were not measured on hardware.
- [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, assumed to reduce the same texels as the console's filter (those of non-zero weight).
- [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).
- [wcsrtombs_s](../../core/libs/prx/libc/src/Multibyte.cpp) (libc) follows the console's `libc.prx`, but a runtime-constraint violation only returns its error: the default handler there writes the message and a newline to `stderr` before returning, which is not done here. Conversion uses the C locale of `wcrtomb` (`MB_CUR_MAX` 1), so the path where a multibyte character does not fit the remaining space is not reached
- [collate<char>::_Getcat](../../core/libs/prx/libc/src/LocaleSupport.cpp) (libc) builds the facet for the C locale only and throws for any other locale name. The console's `libc.prx` stores pointers to its collation state tables at offsets 0x10 and 0x18 of the facet; ours leaves them null, as only the facet's own functions read them. `do_transform` throws for input with embedded null characters, which the console drops depending on the output buffer size, and for results of 4095 characters or more, whose string storage uses the guest's over-aligned large allocation.
- [Thread exit destructors](../../core/libs/prx/libSceLibcInternal/Export.cpp) (libSceLibcInternal) run when a libkernel thread finishes; on the main or a host thread only `_sceLibcInternalThreadDtors` runs them, as `exit` does, else they are dropped