Skip to content
Merged
1 change: 1 addition & 0 deletions core/libs/prx/libSceAgcDriver/Graphics/include/Sampler.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ class Sampler {
bool ReadsOpaqueBlackBorder() const;
bool RequiresNonSeamlessCube() const;
static void Validate(const Context& context, const GuestSamplerResource& descriptor);
static bool ShaderAppliesLodBias(const GuestSamplerResource& descriptor);
static bool ReadsOpaqueBlackBorder(const GuestSamplerResource& descriptor);

private:
Expand Down
9 changes: 7 additions & 2 deletions core/libs/prx/libSceAgcDriver/Graphics/src/Sampler.cpp
Original file line number Diff line number Diff line change
@@ -1,16 +1,21 @@
#include "prx/libSceAgcDriver/Graphics/include/Sampler.hpp"
#include "prx/libSceAgcDriver/Graphics/include/TextureFormat.hpp"
#include "PipelineSpecialization.hpp"
#include <algorithm>
#include <bit>
#include <cstdlib>
#include <string>

namespace AgcDriver::Graphics {

bool Sampler::ShaderAppliesLodBias(const GuestSamplerResource& descriptor) {
return descriptor.lodBias < -ShaderRecompiler::PipelineSpecialization::SamplerLodBiasShaderThreshold || descriptor.lodBias > ShaderRecompiler::PipelineSpecialization::SamplerLodBiasShaderThreshold;
}

void Sampler::Validate(const Context& context, const GuestSamplerResource& descriptor) {
Require(!descriptor.anisotropyEnable || context.samplerAnisotropy, "guest sampler descriptor requests anisotropic filtering which the device does not support");
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(ShaderAppliesLodBias(descriptor) || (descriptor.lodBias >= -context.limits.maxSamplerLodBias && descriptor.lodBias <= context.limits.maxSamplerLodBias), "guest sampler descriptor requests a LOD bias beyond the device limit");
const auto mirrorsOnce = [](VkSamplerAddressMode mode) { return mode == VK_SAMPLER_ADDRESS_MODE_MIRROR_CLAMP_TO_EDGE; };
Require(context.samplerMirrorClampToEdge || !(mirrorsOnce(descriptor.addressModeU) || mirrorsOnce(descriptor.addressModeV) || mirrorsOnce(descriptor.addressModeW)), "guest sampler descriptor uses a mirror-once clamp mode which the device does not support");
Require(!(descriptor.unnormalizedCoordinates && descriptor.compareEnable), "guest sampler descriptor with unnormalized coordinates enables depth comparison, which is not implemented");
Expand All @@ -36,7 +41,7 @@ namespace AgcDriver::Graphics {
info.addressModeU = descriptor.addressModeU;
info.addressModeV = descriptor.addressModeV;
info.addressModeW = descriptor.addressModeW;
info.mipLodBias = descriptor.lodBias;
info.mipLodBias = ShaderAppliesLodBias(descriptor) ? 0.0f : descriptor.lodBias;
info.anisotropyEnable = descriptor.anisotropyEnable ? VK_TRUE : VK_FALSE;
info.maxAnisotropy = descriptor.maxAnisotropy;
info.compareEnable = descriptor.compareEnable ? VK_TRUE : VK_FALSE;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5465,6 +5465,7 @@ void ShaderResources::addSamplerTable(const ShaderRecompiler::DescriptorBinding&
auto resource = DecodeSamplerResource(elementWords);
resource.compareEnable = binding.samplerDepthCompare.at(element);
Sampler::Validate(context, resource);
Require(!Sampler::ShaderAppliesLodBias(resource), "a sampler table entry with a LOD bias beyond the sampler limit needs a per-entry shader bias, which is not implemented");
Require(!Sampler::ReadsOpaqueBlackBorder(resource), "a sampler table entry with an opaque black border needs dynamic texture BC-swizzle pairing, which is not implemented");
Require(resource.addressModeU != VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_BORDER && resource.addressModeV != VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_BORDER && resource.addressModeW != VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_BORDER, "a sampler table entry with a border needs dynamic texture numeric-class pairing, which is not implemented");
Require(!resource.nonSeamlessCube || context.nonSeamlessCubeMap, "a sampler table entry that disables seamless cube filtering needs VK_EXT_non_seamless_cube_map because dynamic texture/sampler pairing is not implemented");
Expand Down
34 changes: 32 additions & 2 deletions core/libs/prx/libSceAgcDriver/tests/Graphics.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -5530,6 +5530,7 @@ struct MockVulkan {
std::vector<MockDescriptorWrite> writes;
std::map<VkSampler, VkSamplerCreateFlags> samplerFlags;
std::uint32_t samplerCreateCount = 0;
float samplerLodBias = 0.0f;
std::vector<VkCopyDescriptorSet> copies;
bool allowCopies = false;
std::uint32_t boundSets = 0;
Expand Down Expand Up @@ -5720,6 +5721,7 @@ VKAPI_ATTR VkResult VKAPI_CALL mockFreeDescriptorSets(VkDevice, VkDescriptorPool
VKAPI_ATTR VkResult VKAPI_CALL mockCreateSampler(VkDevice, const VkSamplerCreateInfo* info, const VkAllocationCallbacks*, VkSampler* sampler) {
*sampler = makeHandle<VkSampler>();
mock.samplerFlags[*sampler] = info->flags;
mock.samplerLodBias = info->mipLodBias;
++mock.samplerCreateCount;
++mock.live;
return VK_SUCCESS;
Expand Down Expand Up @@ -7531,8 +7533,9 @@ void samplerTableDeviceLimitTests(std::optional<std::uint32_t> selected = std::n
namespace Abi = ShaderRecompiler::ImageTableAbi;
constexpr auto count = 2u * Abi::SamplerEntryElements;
const std::array<std::uint32_t, 4> valid{0x92u, (4u * 256u) << 12u, 0u, 0u};
const std::array<std::array<std::uint32_t, 4>, 3> invalid{{
const std::array<std::array<std::uint32_t, 4>, 4> invalid{{
{0x92u, valid[1], 256u, 0u},
{0x92u, valid[1], 0x1000u, 0u},
{0x92u | (2u << 9u), valid[1], (2u << 20u) | (2u << 22u), 0u},
{0x92u | (1u << 29u), valid[1], 0u, 0u}
}};
Expand Down Expand Up @@ -7586,6 +7589,32 @@ void samplerTableDeviceLimitTests(std::optional<std::uint32_t> selected = std::n
}
}

void samplerLodBiasTests() {
mock = MockVulkan{};
auto context = mockContext();
context.samplerAnisotropy = true;
context.limits.maxSamplerAnisotropy = 2.0f;
context.limits.maxSamplerLodBias = 10.0f;
const auto hostBias = [&](std::uint32_t field) {
const std::array<std::uint32_t, 4> words{0x92u, (4u * 256u) << 12u, field, 0u};
AgcDriver::Graphics::Sampler sampler(context, AgcDriver::Graphics::DecodeSamplerResource(words));
return mock.samplerLodBias;
};
Require(hostBias(256u) == 1.0f, "a positive LOD bias within the device limit changed");
Require(hostBias(0x3f00u) == -1.0f, "a negative LOD bias within the device limit changed");
Require(hostBias(0xa00u) == 10.0f, "a LOD bias at the device limit changed");
Require(hostBias(0x1facu) == 0.0f, "a host sampler kept a positive LOD bias the shader applies");
Require(hostBias(0x2000u) == 0.0f, "a host sampler kept a negative LOD bias the shader applies");
bool rejected = false;
try {
hostBias(0xf00u);
} catch (const std::runtime_error&) {
rejected = true;
}
Require(rejected, "a LOD bias beyond the device limit that the shader does not apply was accepted");
Require(mock.live == 0 && mock.samplerFlags.empty(), "the sampler LOD bias checks leaked Vulkan objects");
}

void hostImportBudgetTests() {
using AgcDriver::Graphics::HostImportBudget;
constexpr std::uint64_t MiB = 1ull << 20u;
Expand Down Expand Up @@ -13490,7 +13519,7 @@ int main(int argc, char** argv) {
imageTableSnapshotAllocationTests();
return 0;
}
const std::array<std::string_view, 3> samplerCases{"--sampler-table-lod-bias", "--sampler-table-anisotropy", "--sampler-table-reduction-compare"};
const std::array<std::string_view, 4> samplerCases{"--sampler-table-lod-bias", "--sampler-table-lod-bias-beyond-threshold", "--sampler-table-anisotropy", "--sampler-table-reduction-compare"};
for (std::uint32_t index = 0; index < samplerCases.size(); ++index) {
if (argc == 2 && std::string_view(argv[1]) == samplerCases[index]) {
samplerTableDeviceLimitTests(index);
Expand Down Expand Up @@ -13659,6 +13688,7 @@ int main(int argc, char** argv) {
imageTableAllocationTests();
imageTableSnapshotAllocationTests();
samplerTableNonSeamlessCubeTests();
samplerLodBiasTests();
samplerTableDeviceLimitTests();
tableUpdateAfterBindTests();
misalignedShaderDataTests();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,204 @@
#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp"
#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp"
#include "prx/libSceAgcDriver/Graphics/include/TextureTiling.hpp"
#include "Recompiler.hpp"
#include "VulkanTestDevice.hpp"
#include <algorithm>
#include <array>
#include <bit>
#include <cstdint>
#include <cstdio>
#include <iostream>
#include <span>
#include <string>
#include <vector>

namespace {

using AgcDriver::Graphics::Require;
using ShaderRecompiler::ShaderStage;

constexpr std::uint32_t Width = 64;
constexpr std::uint32_t Height = 16;
constexpr std::uint32_t Size = 64;
constexpr std::uint32_t Mips = 7;
constexpr std::uint32_t Format8888UNorm = 56;
constexpr std::uint32_t Type2D = 9;
constexpr float CoordinateScale = 0.0473f;
constexpr std::uint32_t LevelStep = 20;
alignas(256) std::array<std::byte, Width * Height * 4> Pixels{};
alignas(256) std::array<std::uint8_t, 262144> Texels{};

alignas(256) constexpr std::array<std::uint32_t, 6> VertexCode{
0xe0382000, 0x80000005, 0xbf8c3f70, 0xf80008cf, 0x03020100, 0xbf810000,
};

alignas(256) constexpr std::array<std::uint32_t, 9> BiasPixelCode{
0x10040010, 0x06060300, 0x10060611, 0xf0940f08, 0x00610401, 0xbf8c3f70, 0xf800180f, 0x07060504, 0xbf810000,
};

constexpr std::array<std::array<float, 4>, 3> Triangle{{{-1.0f, -1.0f, 0.5f, 1.0f}, {3.0f, -1.0f, 0.5f, 1.0f}, {-1.0f, 3.0f, 0.5f, 1.0f}}};

std::array<std::uint32_t, 4> BufferDescriptor(const void* data, std::uint32_t stride, std::uint32_t count) {
const auto address = reinterpret_cast<std::uintptr_t>(data);
return {static_cast<std::uint32_t>(address), static_cast<std::uint32_t>((address >> 32u) & 0xffffu) | (stride << 16u), count, 0x01016facu};
}

std::array<std::uint32_t, 8> TextureDescriptor() {
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) | (Format8888UNorm << 20u) | (((Size - 1u) & 3u) << 30u),
((Size - 1u) >> 2u) | ((Size - 1u) << 14u),
0xfacu | ((Mips - 1u) << 16u) | (Type2D << 28u),
0u,
(Mips - 1u) << 4u,
0u,
0u,
};
}

std::array<std::uint32_t, 4> SamplerDescriptor() {
return {0u, 0xfffu << 12u, 1u << 26u, 0u};
}

void FillTexture() {
const auto mips = AgcDriver::Graphics::ComputeMipLayout(AgcDriver::Graphics::TextureTileMode::kLinear, Format8888UNorm, Size, Size, Mips);
Require(AgcDriver::Graphics::ComputeSurfaceSize(mips, 1) <= Texels.size(), "image sample LOD bias sum: the mip chain does not fit the texel storage");
std::fill(Texels.begin(), Texels.end(), 0xeeu);
for (std::uint32_t level = 0; level < Mips; ++level) {
const auto& mip = mips[level];
for (std::uint32_t y = 0; y < mip.height; ++y) {
for (std::uint32_t x = 0; x < mip.width; ++x) {
auto* texel = &Texels[mip.tiledOffset + y * mip.pitchBytes + x * 4u];
texel[0] = static_cast<std::uint8_t>(level * LevelStep);
texel[1] = 0u;
texel[2] = 0u;
texel[3] = 255u;
}
}
}
}

void Draw(AgcDriver::VulkanDevice& device, std::array<std::uint32_t, 8> texture, std::array<std::uint32_t, 4> sampler, float bias) {
const auto& target = device.Target();
alignas(256) std::array<std::uint32_t, 10> code{};
std::copy_n(BiasPixelCode.begin(), 3u, code.begin());
code[3] = 0x7e020212u;
std::copy(BiasPixelCode.begin() + 3, BiasPixelCode.end(), code.begin() + 4);
const auto pixelCode = std::span(code).first(10u);
Pixels.fill(std::byte{0});

std::vector<std::uint32_t> vertexUserData(4, 0u);
const auto vertexBuffer = BufferDescriptor(Triangle.data(), 16u, static_cast<std::uint32_t>(Triangle.size()));
std::copy(vertexBuffer.begin(), vertexBuffer.end(), vertexUserData.begin());
const std::array<ShaderRecompiler::MemoryRegion, 1> vertexMemory{{{reinterpret_cast<std::uintptr_t>(VertexCode.data()), std::as_bytes(std::span(VertexCode))}}};
ShaderRecompiler::RecompileRequest vertex{
{ShaderStage::Vertex, reinterpret_cast<std::uintptr_t>(VertexCode.data()), VertexCode, 0, {}},
{64, 0, vertexUserData, std::nullopt, std::nullopt, ShaderRecompiler::ShaderVertexStageInfo{}, vertexMemory},
target,
{0, 0, 0, 64}
};
vertex.useCache = false;
const auto vertexResult = ShaderRecompiler::Recompile(vertex);
const auto vertexPush = static_cast<std::uint32_t>(vertexResult.pushConstants.size());

ShaderRecompiler::ShaderPixelStageInfo pixel{};
pixel.wave32 = false;
pixel.inputAddr = ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::PositionX) | ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::PositionY);
pixel.posX = true;
pixel.posY = true;
pixel.targetOutputMode[0] = 9;
pixel.targetExportMapping.fill(0xe4u);
std::vector<std::uint32_t> pixelUserData(24, 0u);
std::copy(texture.begin(), texture.end(), pixelUserData.begin() + 4);
std::copy(sampler.begin(), sampler.end(), pixelUserData.begin() + 12);
pixelUserData[16] = std::bit_cast<std::uint32_t>(CoordinateScale);
pixelUserData[17] = std::bit_cast<std::uint32_t>(CoordinateScale);
pixelUserData[18] = std::bit_cast<std::uint32_t>(bias);
const std::array<ShaderRecompiler::MemoryRegion, 1> pixelMemory{{{reinterpret_cast<std::uintptr_t>(pixelCode.data()), std::as_bytes(std::span(pixelCode))}}};
ShaderRecompiler::RecompileRequest fragment{
{ShaderStage::Fragment, reinterpret_cast<std::uintptr_t>(pixelCode.data()), pixelCode, 0, {}},
{64, 0, pixelUserData, std::nullopt, pixel, std::nullopt, pixelMemory},
target,
PixelPushLayout(vertexPush, target)
};
const auto pixelResult = ShaderRecompiler::Recompile(fragment);
Require(!target.registrationModules || pixelResult.registrationModule, "image sample LOD bias sum did not exercise its requested registration module");
const std::array<AgcDriver::Graphics::CompiledShader, 2> shaders{{
{ShaderStage::Vertex, &vertexResult, 0},
{ShaderStage::Fragment, &pixelResult, PixelPushOffset(vertexPush, target)}
}};

AgcDriver::Graphics::State state{};
state.stages = {AgcDriver::Graphics::ShaderPath::Vertex, 0u, 64u, 64u, std::nullopt, std::nullopt};
state.color = {reinterpret_cast<std::uintptr_t>(Pixels.data()), {Width, Height}, VK_FORMAT_R8G8B8A8_UNORM, Pixels.size(), 0xe4u};
state.colors = {state.color};
state.hasColorTarget = true;
state.renderExtent = {Width, Height};
state.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST;
state.viewport = {0, static_cast<float>(Height), static_cast<float>(Width), -static_cast<float>(Height), 0, 1};
state.negativeOneToOne = false;
state.scissor = {{0, 0}, {Width, Height}};
state.cullMode = VK_CULL_MODE_NONE;
state.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE;
state.blend.colorWriteMask = 15;
state.blends = {state.blend};
state.blendConstants = {};
const AgcDriver::Pm4::DrawParameters draw{0, static_cast<std::uint32_t>(Triangle.size()), 0, 1, 0, false};
device.Draw(state, draw, shaders);
device.WaitIdle();
}

struct Case {
std::uint32_t field;
float shaderBias;
std::uint32_t level;
};

void Check(const Case& sample, bool llvmpipe) {
const std::string label = "image sample LOD bias sum: S# field " + std::to_string(sample.field) + " with shader bias " + std::to_string(sample.shaderBias);
std::uint32_t matching = 0;
for (std::uint32_t pixel = 0; pixel < Width * Height; ++pixel) {
const auto red = std::to_integer<std::uint32_t>(Pixels[pixel * 4u]);
if (red == sample.level * LevelStep) {
++matching;
continue;
}
Require(llvmpipe, label + " read " + std::to_string(red) + ", expected level " + std::to_string(sample.level));
}
Require(matching * 4u >= Width * Height * 3u, label + " matched level " + std::to_string(sample.level) + " on only " + std::to_string(matching) + " of " + std::to_string(Width * Height) + " pixels");
}

}

int main() {
try {
const auto device = OpenVulkanTestDevice();
if (!device) return VulkanTestSkipped;
FillTexture();
const bool llvmpipe = device->DeviceName().find("llvmpipe") != std::string::npos;
constexpr std::array<Case, 9> cases{{
{0u, 0.0f, 2u},
{0x100u, 0.0f, 3u},
{0u, 2.0f, 4u},
{0x1000u, -14.0f, 4u},
{0x1facu, -29.0f, 5u},
{0x3000u, 14.0f, 0u},
{0x2000u, 32.0f, 2u},
{0x1facu, 0.0f, 6u},
{0x2000u, 0.0f, 0u},
}};
for (const auto& sample : cases) {
auto sampler = SamplerDescriptor();
sampler[2] |= sample.field;
Draw(*device, TextureDescriptor(), sampler, sample.shaderBias);
Check(sample, llvmpipe);
}
std::puts("image sample LOD bias sum tests passed");
return 0;
} catch (const std::exception& error) {
std::cerr << error.what() << '\n';
return 1;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,10 @@ inline bool MirrorsIfUnnormalized(const SamplerResource& sampler) {
return sampler.table == NoTable && !sampler.depthCompare && sampler.uses != 0u && ((sampler.uses & ~static_cast<std::uint32_t>(SamplerUseExplicitLod | SamplerUseOffset | SamplerUseNonZeroLod)) == 0u || SamplesBaseLevelIfUnnormalized(sampler));
}

inline bool AppliesLodBiasInShader(const SamplerResource& sampler) {
return sampler.table == NoTable && (sampler.uses & (SamplerUseExplicitLod | SamplerUseImplicitLod | SamplerUseGradient)) != 0u;
}

struct SampledResourcePair {
std::uint32_t image = 0;
std::uint32_t sampler = 0;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -400,6 +400,14 @@ DescriptorBindingPlan DescriptorBindingBuilder::Prepare(const IrBindingLayout& l
if (FoldsTexelOffsets(info.samplers[index])) plan.specialization.push_back({PipelineSpecialization::FoldOffsetBase + index, unnormalized.samplers[index] ? 1u : 0u});
if (SamplesBaseLevelIfUnnormalized(info.samplers[index])) plan.specialization.push_back({PipelineSpecialization::UnnormalizedBaseLevelBase + index, unnormalized.samplers[index] ? 1u : 0u});
if (MirrorsIfUnnormalized(info.samplers[index])) plan.specialization.push_back({PipelineSpecialization::MirrorUnnormalizedBase + index, unnormalized.mirroredAxes[index]});
if (info.samplers[index].table != NoTable) continue;
const auto bias = unnormalized.samplers[index] ? 0.0f : static_cast<float>(static_cast<std::int32_t>((snapshot.samplers.at(index).dwords[2] & 0x3fffu) ^ 0x2000u) - 0x2000) / 256.0f;
const bool beyondThreshold = bias < -PipelineSpecialization::SamplerLodBiasShaderThreshold || bias > PipelineSpecialization::SamplerLodBiasShaderThreshold;
if (beyondThreshold && (!AppliesLodBiasInShader(info.samplers[index]) || (info.samplers[index].uses & (SamplerUseQueryLod | SamplerUseAdjust)) != 0u)) fail("a guest sampler LOD bias beyond the sampler limit reaches a shader use that cannot apply it, which is not implemented");
for (const auto& pair : info.sampledPairs) {
if (beyondThreshold && pair.sampler == index && info.images.at(pair.image).cube) fail("a guest sampler LOD bias beyond the sampler limit on a cube image is not implemented");
}
if (AppliesLodBiasInShader(info.samplers[index])) plan.specialization.push_back({PipelineSpecialization::SamplerLodBiasBase + index, beyondThreshold ? std::bit_cast<std::uint32_t>(bias) : 0u});
}
std::vector<std::uint32_t> compareStates(info.images.size());
for (std::uint32_t index = 0; index < info.images.size(); ++index) {
Expand Down
Loading
Loading