mirror of
https://github.com/boykopovar/AnyPS5.git
synced 2026-10-23 09:03:22 -05:00
Since #1463, every f16 operand read decodes sign, exponent and mantissa with integer operations, 8 to 12 instructions more per read than UnpackHalf2x16 on NVIDIA. The decode works around an AMD Windows driver that folds the mask of a byte-selected half into UnpackHalf2x16. A new target flag, float16DenormPreserve, is set when the device reports shaderDenormPreserveFloat16 and denormBehaviorIndependence ALL, the same rule as float64DenormPreserve. With it, EmitF16BitsToF32 uses UnpackHalf2x16 and declares DenormPreserve for 16-bit floats, under which SPV_KHR_float_controls keeps unpacked subnormal halves. NVIDIA returns the default NaN for every NaN half, so the NaN bits are rebuilt from the half: its sign at bit 31, its payload at bits 22:13. The DenormPreserve 16 mode is declared once, shared with the native f32 to f16 conversion. Without the flag (lavapipe; RADV reports 32_BIT_ONLY) the integer decode stays. Halves of fields narrower than 16 bits, such as SDWA byte selects, keep the integer decode on every device. The flag is serialized (request version 17) and part of the cache key. New test agc_driver_f16_to_f32_unpack converts all 65,536 halves, with nonzero high bits, through v_cvt_f32_f16, and byte-selected halves through v_add_f16 SDWA, with each decode the device supports, and compares the bits with an exact decode. agc_driver_f16_to_f32_unpack_spirv checks which decode the SPIR-V uses with and without the flag.
1134 lines
62 KiB
C++
1134 lines
62 KiB
C++
#include "ShaderDiskCache.hpp"
|
|
#include "CacheKey.hpp"
|
|
#include "VertexInputSpecialization.hpp"
|
|
#include "SpirvBackend/SpirvSpecialization.hpp"
|
|
#include "Optimization/ResourceMaterializer.hpp"
|
|
#include "Optimization/ResourceProgram.hpp"
|
|
#include "Optimization/DescriptorBindingBuilder.hpp"
|
|
#include "ShaderCacheDirectory.hpp"
|
|
#include "ControlFlow/RequestSerializer.hpp"
|
|
#include <algorithm>
|
|
#include <array>
|
|
#include <chrono>
|
|
#include <cstdio>
|
|
#include <cstdlib>
|
|
#include <cstring>
|
|
#include <filesystem>
|
|
#include <functional>
|
|
#include <future>
|
|
#include <iostream>
|
|
#include <map>
|
|
#include <random>
|
|
#include <stdexcept>
|
|
#include <string>
|
|
#include <string_view>
|
|
#include <vector>
|
|
#ifdef _WIN32
|
|
#ifndef NOMINMAX
|
|
#define NOMINMAX
|
|
#endif
|
|
#ifndef WIN32_LEAN_AND_MEAN
|
|
#define WIN32_LEAN_AND_MEAN
|
|
#endif
|
|
#include <windows.h>
|
|
#endif
|
|
|
|
namespace {
|
|
|
|
using namespace ShaderRecompiler;
|
|
|
|
void require(bool condition, const std::string& message) {
|
|
if (!condition) throw std::runtime_error(message);
|
|
}
|
|
|
|
void setEnvironment(const char* name, const std::string& value) {
|
|
#ifdef _WIN32
|
|
_putenv_s(name, value.c_str());
|
|
#else
|
|
setenv(name, value.c_str(), 1);
|
|
#endif
|
|
}
|
|
|
|
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.bufferRead == right.bufferRead && left.samplerUnnormalized == right.samplerUnnormalized && left.samplerIntegerBorder == right.samplerIntegerBorder && left.imageUnnormalized == right.imageUnnormalized && left.imageUnnormalizedBaseLevel == right.imageUnnormalizedBaseLevel && left.imageSamplers == right.imageSamplers;
|
|
}
|
|
|
|
bool sameBindings(const std::vector<DescriptorBinding>& left, const std::vector<DescriptorBinding>& right) {
|
|
if (left.size() != right.size()) return false;
|
|
for (std::size_t i = 0; i < left.size(); ++i) {
|
|
if (!sameBinding(left[i], right[i])) return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
void requireSameArtifact(const CompiledShaderArtifact& left, const CompiledShaderArtifact& right, const char* what) {
|
|
const std::string prefix = std::string(what) + ": ";
|
|
require(left.spirv.Words() == right.spirv.Words(), prefix + "SPIR-V differs");
|
|
require(left.bdaAbiVersion == right.bdaAbiVersion, prefix + "BDA ABI version differs");
|
|
require(left.runtimeAbiVersion == right.runtimeAbiVersion, prefix + "runtime ABI version differs");
|
|
require(left.memoryOffsetDword == right.memoryOffsetDword, prefix + "memory offset differs");
|
|
require(left.shaderDataDwords == right.shaderDataDwords && left.imageMetadataDword == right.imageMetadataDword && left.runtimeImageCount == right.runtimeImageCount && left.runtimeImageResources == right.runtimeImageResources, prefix + "compact data layout differs");
|
|
require(left.hostSubgroupSize == right.hostSubgroupSize, prefix + "host subgroup size differs");
|
|
require(left.vertexInputs == right.vertexInputs, prefix + "vertex inputs differ");
|
|
require(left.vertexInputPatches == right.vertexInputPatches, prefix + "vertex type patches differ");
|
|
require(left.registrationModule == right.registrationModule, prefix + "registration module flags differ");
|
|
require(left.vertexOffsetSgpr == right.vertexOffsetSgpr && left.instanceOffsetSgpr == right.instanceOffsetSgpr, prefix + "offset SGPRs differ");
|
|
require(left.vertexOffsetShared == right.vertexOffsetShared && left.instanceOffsetShared == right.instanceOffsetShared && left.vertexOffsetConflict == right.vertexOffsetConflict && left.instanceOffsetConflict == right.instanceOffsetConflict, prefix + "offset flags differ");
|
|
require(left.parameterExports == right.parameterExports, prefix + "parameter exports differ");
|
|
require(left.fragmentParameters.size() == right.fragmentParameters.size(), prefix + "fragment parameter count differs");
|
|
for (std::size_t i = 0; i < left.fragmentParameters.size(); ++i) {
|
|
const auto& a = left.fragmentParameters[i];
|
|
const auto& b = right.fragmentParameters[i];
|
|
require(a.location == b.location && a.sourceLocation == b.sourceLocation && a.flat == b.flat && a.perVertex == b.perVertex && a.custom == b.custom, prefix + "fragment parameter differs");
|
|
}
|
|
}
|
|
|
|
void requireSameResult(const RecompileResult& left, const RecompileResult& right, const char* what) {
|
|
requireSameArtifact(left, right, what);
|
|
const std::string prefix = std::string(what) + ": ";
|
|
require(sameBindings(left.bindings, right.bindings), prefix + "bindings differ");
|
|
require(left.pushConstants == right.pushConstants, prefix + "push constants differ");
|
|
require(left.specialization == right.specialization, prefix + "specialization constants differ");
|
|
require(left.workgroupMemoryDwords == right.workgroupMemoryDwords, prefix + "workgroup memory stride differs");
|
|
require(left.poisonedSrtReads == right.poisonedSrtReads, prefix + "poisoned SRT read counts differ");
|
|
require(left.vertexAttributes.size() == right.vertexAttributes.size(), prefix + "vertex attribute count differs");
|
|
for (std::size_t i = 0; i < left.vertexAttributes.size(); ++i) {
|
|
const auto& a = left.vertexAttributes[i];
|
|
const auto& b = right.vertexAttributes[i];
|
|
require(a.location == b.location && a.components == b.components && a.resource.fields == b.resource.fields && a.fetchIndex == b.fetchIndex && a.formatComponents == b.formatComponents, prefix + "vertex attribute differs");
|
|
}
|
|
require(left.barycentricEmulation.active == right.barycentricEmulation.active && left.barycentricEmulation.smooth == right.barycentricEmulation.smooth && left.barycentricEmulation.linear == right.barycentricEmulation.linear, prefix + "barycentric emulation differs");
|
|
}
|
|
|
|
void requireSameVariant(const CompiledVariant& left, const CompiledVariant& right, const char* what) {
|
|
requireSameArtifact(left.artifact, right.artifact, what);
|
|
const std::string prefix = std::string(what) + ": ";
|
|
const auto& a = left.info;
|
|
const auto& b = right.info;
|
|
require(a.stage == b.stage && a.shaderHash == b.shaderHash && a.waveSize == b.waveSize && a.userDataBase == b.userDataBase && a.userDataCount == b.userDataCount && a.scratchDwords == b.scratchDwords && a.paramExportMask == b.paramExportMask, prefix + "compiled info differs");
|
|
require(a.info == b.info, prefix + "shader info differs");
|
|
require(a.bindings == b.bindings, prefix + "info binding layout differs");
|
|
require(left.bindings.layout == right.bindings.layout, prefix + "allocation layout differs");
|
|
require(left.bindings.pushConstantOffsetBytes == right.bindings.pushConstantOffsetBytes && left.bindings.pushConstantSizeBytes == right.bindings.pushConstantSizeBytes, prefix + "push constant range differs");
|
|
}
|
|
|
|
DescriptorBinding sampleBinding(std::uint32_t seed) {
|
|
DescriptorBinding binding{};
|
|
binding.kind = DescriptorKind::StorageImage;
|
|
binding.role = DescriptorRole::GuestImages;
|
|
binding.descriptorSet = 0;
|
|
binding.binding = 29 + seed;
|
|
binding.count = 3;
|
|
binding.guestDescriptor = {0x11111111u * seed, 0xdeadbeefu, 0x80000000u, 7u, 0u, 0xffffffffu, 42u, seed};
|
|
binding.readOnly = seed % 2 == 0;
|
|
binding.imageShape = DescriptorImageShape::Image2DArray;
|
|
binding.samplerDepthCompare = {true, false, true};
|
|
binding.imageDepthCompare = {false, true, false};
|
|
binding.imageWritten = {false, true, true};
|
|
binding.imageAtomic = {false, true, false};
|
|
binding.bufferAtomic = {true};
|
|
binding.bufferWritten = {false, false, true, true, false};
|
|
binding.samplerUnnormalized = {false, true, true};
|
|
binding.samplerIntegerBorder = {true, false, seed % 2 == 1};
|
|
binding.imageUnnormalized = {true, false, seed % 2 == 0};
|
|
binding.imageUnnormalizedBaseLevel = {false, true, seed % 3 == 0};
|
|
binding.imageSamplers = {0x5u, 0u, 0x80000000u};
|
|
binding.bufferRead = {true, false, false, true, true, false};
|
|
return binding;
|
|
}
|
|
|
|
RecompileResult sampleResult() {
|
|
RecompileResult result;
|
|
std::vector<std::uint32_t> words(1000);
|
|
for (std::size_t i = 0; i < words.size(); ++i) words[i] = static_cast<std::uint32_t>(i * 2654435761u);
|
|
words[0] = 0x07230203u;
|
|
result.spirv = std::move(words);
|
|
result.bindings = {sampleBinding(1), sampleBinding(2)};
|
|
result.bindings[1].kind = DescriptorKind::StorageBuffer;
|
|
result.bindings[1].role = DescriptorRole::GuestBuffers;
|
|
result.bindings[1].imageShape.reset();
|
|
result.pushConstants = {std::byte{1}, std::byte{0xff}, std::byte{0}, std::byte{0x80}, std::byte{7}};
|
|
result.bdaAbiVersion = 3;
|
|
result.memoryOffsetDword = 7;
|
|
result.hostSubgroupSize = 32;
|
|
result.workgroupMemoryDwords = 32769u;
|
|
result.vertexInputs = {{1, 4, 2}, {5, 2, 0}};
|
|
result.vertexInputPatches = {{1, 20, {7, 8, 9}}, {5, 40, {10, 11, 12}}};
|
|
result.registrationModule = true;
|
|
result.specialization = {{512, 4}, {516, 0x3f800000u}, {517, 0}};
|
|
result.vertexAttributes = {{1, 4, {{0x1000u, 0x20000u, 0x30u, 0x4u}}, 2}, {5, 2, {{9u, 8u, 7u, 6u}}, 0}};
|
|
result.vertexInputs[0].outputMask = 5u;
|
|
result.vertexAttributes[0].formatComponents = 3u;
|
|
result.vertexOffsetSgpr = 12;
|
|
result.instanceOffsetSgpr = -1;
|
|
result.vertexOffsetShared = true;
|
|
result.instanceOffsetShared = false;
|
|
result.vertexOffsetConflict = false;
|
|
result.instanceOffsetConflict = true;
|
|
result.parameterExports = {0, 3, 7};
|
|
result.fragmentParameters = {{0, 1, true, false, true}, {2, 3, false, true}};
|
|
result.poisonedSrtReads = 3;
|
|
result.barycentricEmulation = {true, false, true};
|
|
result.variantId = 99;
|
|
return result;
|
|
}
|
|
|
|
CompiledVariant sampleVariant() {
|
|
CompiledVariant variant;
|
|
variant.artifact = sampleResult();
|
|
variant.layout = {0, 0, 0, 128};
|
|
auto& info = variant.info;
|
|
info.stage = IrShaderStage::Compute;
|
|
info.shaderHash = 0x123456789abcdefull;
|
|
info.waveSize = 32;
|
|
info.userDataBase = 4;
|
|
info.userDataCount = 16;
|
|
info.scratchDwords = 8;
|
|
info.paramExportMask = 0x5;
|
|
info.info.scratchDwords = 8;
|
|
info.info.sharedMemoryBytes = 4096;
|
|
BufferResource buffer{};
|
|
buffer.source = 3;
|
|
buffer.firstUsePc = 0x40;
|
|
buffer.maxByteExtent = 256;
|
|
buffer.imageAlias = 1;
|
|
buffer.read = true;
|
|
buffer.atomic = true;
|
|
buffer.scalar = true;
|
|
buffer.descriptorFormatted = true;
|
|
buffer.formattedReadMask = 5u;
|
|
buffer.typedAlignment = 2u;
|
|
info.info.buffers = {buffer, BufferResource{}};
|
|
ImageResource image{};
|
|
image.source = 5;
|
|
image.firstUsePc = 0x80;
|
|
image.resourceClass = ImageResourceClass::Storage;
|
|
image.numericClass = IrTextureNumericClass::Uint;
|
|
image.mipMode = ImageMipMode::DynamicStorage;
|
|
image.mipCount = 4;
|
|
image.shaderSwizzle = 0x321u;
|
|
image.written = true;
|
|
image.srgbDecode = true;
|
|
image.cube = true;
|
|
image.r128 = true;
|
|
image.fmaskCompatible = false;
|
|
image.depthBitsCompatible = false;
|
|
image.constantSwizzle = true;
|
|
image.constantSwizzleCompatible = false;
|
|
image.flatVolumeCompatible = false;
|
|
image.flatLineCompatible = false;
|
|
image.foldTexelOffsetsOnly = true;
|
|
image.byElements = 4;
|
|
image.byComponents = 1;
|
|
image.indirectRoot = 0;
|
|
image.indirectMappingOffset = 12;
|
|
image.indirectSearchIterations = 3;
|
|
image.indirectResources = {1, 2, 3};
|
|
info.info.images = {image};
|
|
ResourceMaterializer::PrepareImageModes(info.info);
|
|
info.info.samplers = {{7, 0x10, 3, true, false, SamplerUseExplicitLod | SamplerUseGather, true}};
|
|
info.info.sampledPairs = {{0, 0, 0x10}};
|
|
StageInput input{};
|
|
input.kind = StageInputKind::GlobalInvocationId;
|
|
input.location = 2;
|
|
input.componentCount = 3;
|
|
input.debugName = "gid";
|
|
input.perVertex = true;
|
|
info.info.inputs = {input};
|
|
StageOutput output{};
|
|
output.kind = StageOutputKind::Mrt;
|
|
output.index = 1;
|
|
output.location = 4;
|
|
output.debugName = "mrt1";
|
|
info.info.outputs = {output};
|
|
info.info.vertexFetchComponents[3] = 4;
|
|
info.info.vertexOffsetSgpr = 6;
|
|
info.info.hasBitwiseXor = true;
|
|
info.info.usesDma = true;
|
|
info.info.usesFaultBuffer = true;
|
|
info.info.rasterPrimitive = true;
|
|
info.bindings.pushDataStartDword = 2;
|
|
info.bindings.memoryOffsetDword = 1;
|
|
info.bindings.memoryOffsetCount = 5;
|
|
info.bindings.userDataRegisters = {4, 5, 9};
|
|
info.bindings.descriptors = {{DescriptorBindingKind::Buffers, {0, 1}}, {DescriptorBindingKind::FlattenedSrt, {}}};
|
|
variant.bindings.layout = info.bindings;
|
|
variant.bindings.pushConstantOffsetBytes = 16;
|
|
variant.bindings.pushConstantSizeBytes = 112;
|
|
return variant;
|
|
}
|
|
|
|
struct SampleRequest {
|
|
std::vector<std::uint32_t> code{0xe0700000u, 0x80000000u, 0xbf810000u, 0x12345678u};
|
|
std::vector<std::uint32_t> userData{0x10000000u, 0x00100000u, 0x40u, 0x00027facu};
|
|
std::array<std::uint32_t, 2> capabilities{1u, 61u};
|
|
std::array<std::string_view, 1> extensions{"SPV_KHR_storage_buffer_storage_class"};
|
|
std::array<std::byte, 16> header{};
|
|
RecompileRequest request{};
|
|
std::uint32_t hostSubgroupSize = 32;
|
|
|
|
SampleRequest() {
|
|
request.shader = {ShaderStage::Compute, 0x20000u, code, 0x1f000u, header};
|
|
request.context.waveSize = 64;
|
|
request.context.userDataBaseRegister = 0;
|
|
request.context.userData = userData;
|
|
request.context.compute = ShaderComputeStageInfo{{64u, 1u, 1u}, 0u, {false, false, false}, false, 1u};
|
|
request.target.vulkanVersion = 0x00403000u;
|
|
request.target.spirvVersion = 0x00010600u;
|
|
request.target.subgroupSize = 32;
|
|
request.target.bdaAbiVersion = 1;
|
|
request.target.supportedCapabilities = capabilities;
|
|
request.target.supportedExtensions = extensions;
|
|
request.target.maxWorkgroupSize = {1024u, 1024u, 64u};
|
|
request.target.maxWorkgroupInvocations = 1024;
|
|
request.target.maxWorkgroupSharedMemoryBytes = 49152;
|
|
request.layout = {0, 0, 0, 128};
|
|
}
|
|
|
|
std::vector<std::byte> Key() {
|
|
request.shader.code = code;
|
|
request.context.userData = userData;
|
|
request.target.supportedCapabilities = capabilities;
|
|
std::vector<std::byte> key;
|
|
ShaderDiskCache::BuildKey(request, hostSubgroupSize, key);
|
|
return key;
|
|
}
|
|
};
|
|
|
|
void verifyResultRoundTrip() {
|
|
const auto result = sampleResult();
|
|
std::vector<std::byte> bytes;
|
|
ShaderDiskCache::EncodeResult(result, bytes);
|
|
RecompileResult decoded;
|
|
require(ShaderDiskCache::DecodeResult(bytes, decoded), "an encoded result does not decode");
|
|
requireSameResult(result, decoded, "result round trip");
|
|
require(decoded.variantId == 0 && !decoded.cacheHit, "the per-process fields were stored");
|
|
for (std::size_t size = 0; size < bytes.size(); size += size < 256 ? 1 : 97) {
|
|
RecompileResult partial;
|
|
require(!ShaderDiskCache::DecodeResult(std::span(bytes).first(size), partial), "a result truncated to " + std::to_string(size) + " bytes decodes");
|
|
}
|
|
auto longer = bytes;
|
|
longer.push_back(std::byte{0});
|
|
require(!ShaderDiskCache::DecodeResult(longer, decoded), "a result with a trailing byte decodes");
|
|
bytes.clear();
|
|
ShaderDiskCache::EncodeResult(RecompileResult{}, bytes);
|
|
require(ShaderDiskCache::DecodeResult(bytes, decoded), "an empty result does not decode");
|
|
requireSameResult(RecompileResult{}, decoded, "empty result round trip");
|
|
auto incompatible = result;
|
|
incompatible.runtimeAbiVersion = RuntimeAbi::Version + 1u;
|
|
bytes.clear();
|
|
ShaderDiskCache::EncodeResult(incompatible, bytes);
|
|
require(!ShaderDiskCache::DecodeResult(bytes, decoded), "an incompatible runtime ABI result decodes");
|
|
}
|
|
|
|
void verifyEntryRoundTrip() {
|
|
SampleRequest sample;
|
|
const auto key = sample.Key();
|
|
const auto variant = sampleVariant();
|
|
const auto file = ShaderDiskCache::EncodeEntry(key, variant);
|
|
CompiledVariant decoded;
|
|
require(ShaderDiskCache::DecodeEntry(file, key, decoded) == ShaderDiskCache::LoadStatus::Loaded, "an encoded entry does not decode");
|
|
requireSameVariant(variant, decoded, "entry round trip");
|
|
require(decoded.artifact.variantId == 0, "the artifact stored a per-process id");
|
|
|
|
for (std::size_t size = 0; size < file.size(); size += size < 512 ? 1 : 131) {
|
|
CompiledVariant partial;
|
|
require(ShaderDiskCache::DecodeEntry(std::span(file).first(size), key, partial) == ShaderDiskCache::LoadStatus::Rejected, "an entry truncated to " + std::to_string(size) + " bytes is not rejected");
|
|
}
|
|
for (std::size_t offset = 0; offset < file.size(); offset += offset < 512 ? 1 : 61) {
|
|
auto damaged = file;
|
|
damaged[offset] ^= std::byte{0x10};
|
|
CompiledVariant partial;
|
|
const auto status = ShaderDiskCache::DecodeEntry(damaged, key, partial);
|
|
require(status == ShaderDiskCache::LoadStatus::Rejected, "an entry with byte " + std::to_string(offset) + " damaged is not rejected");
|
|
}
|
|
auto longer = file;
|
|
longer.push_back(std::byte{0});
|
|
require(ShaderDiskCache::DecodeEntry(longer, key, decoded) == ShaderDiskCache::LoadStatus::Rejected, "an entry with a trailing byte is not rejected");
|
|
auto otherKey = key;
|
|
otherKey.back() ^= std::byte{1};
|
|
require(ShaderDiskCache::DecodeEntry(file, otherKey, decoded) == ShaderDiskCache::LoadStatus::KeyMismatch, "an entry for another key loads");
|
|
auto previousVersion = file;
|
|
previousVersion[4] = static_cast<std::byte>(ShaderDiskCache::FormatVersion - 1u);
|
|
require(ShaderDiskCache::DecodeEntry(previousVersion, key, decoded) == ShaderDiskCache::LoadStatus::Rejected, "an entry with the previous format version loads");
|
|
auto incompatible = variant;
|
|
incompatible.artifact.runtimeAbiVersion = RuntimeAbi::Version + 1u;
|
|
const auto incompatibleFile = ShaderDiskCache::EncodeEntry(key, incompatible);
|
|
require(ShaderDiskCache::DecodeEntry(incompatibleFile, key, decoded) == ShaderDiskCache::LoadStatus::Rejected, "an entry with an incompatible runtime ABI loads");
|
|
}
|
|
|
|
void verifyArtifactStorageIsolation() {
|
|
SampleRequest sample;
|
|
const auto key = sample.Key();
|
|
auto result = sampleResult();
|
|
auto variant = sampleVariant();
|
|
variant.artifact = result;
|
|
const auto before = ShaderDiskCache::EncodeEntry(key, variant);
|
|
result.bindings[0].guestDescriptor[0] ^= 0x10000u;
|
|
result.pushConstants[0] ^= std::byte{0xff};
|
|
result.vertexAttributes[0].resource.fields[0] ^= 0x10000u;
|
|
variant.artifact = result;
|
|
require(ShaderDiskCache::EncodeEntry(key, variant) == before, "invocation data changed the stored artifact");
|
|
result.vertexInputs[0].components = 2;
|
|
variant.artifact = result;
|
|
require(ShaderDiskCache::EncodeEntry(key, variant) != before, "the stored artifact ignores vertex input metadata");
|
|
}
|
|
|
|
void verifyKeySensitivity() {
|
|
SampleRequest base;
|
|
const auto key = base.Key();
|
|
require(base.Key() == key, "the key is not deterministic");
|
|
require(ShaderDiskCache::EntryName(key).size() == 36, "unexpected entry name length");
|
|
|
|
const auto changes = [&](const std::string& what, const std::function<void(SampleRequest&)>& change) {
|
|
SampleRequest sample;
|
|
change(sample);
|
|
const auto changed = sample.Key();
|
|
require(changed != key, "the key ignores " + what);
|
|
require(ShaderDiskCache::EntryName(changed) != ShaderDiskCache::EntryName(key), "the entry name ignores " + what);
|
|
};
|
|
for (std::size_t word = 0; word < base.code.size(); ++word) {
|
|
for (std::uint32_t bit = 0; bit < 32; ++bit) {
|
|
changes("code word " + std::to_string(word) + " bit " + std::to_string(bit), [&](SampleRequest& sample) { sample.code[word] ^= 1u << bit; });
|
|
}
|
|
}
|
|
changes("a code word appended", [](SampleRequest& sample) { sample.code.push_back(0); });
|
|
changes("the stage", [](SampleRequest& sample) { sample.request.shader.stage = ShaderStage::Fragment; sample.request.context.compute.reset(); });
|
|
changes("the wave size", [](SampleRequest& sample) { sample.request.context.waveSize = 32; });
|
|
changes("the user data base", [](SampleRequest& sample) { sample.request.context.userDataBaseRegister = 2; });
|
|
changes("the user data count", [](SampleRequest& sample) { sample.userData.push_back(0); });
|
|
for (std::size_t axis = 0; axis < 3; ++axis) {
|
|
changes("thread count " + std::to_string(axis), [&](SampleRequest& sample) { sample.request.context.compute->numThreads[axis] += 1; });
|
|
changes("group id enable " + std::to_string(axis), [&](SampleRequest& sample) { sample.request.context.compute->groupIdEnable[axis] = true; });
|
|
}
|
|
changes("the LDS size", [](SampleRequest& sample) { sample.request.context.compute->ldsSizeDwords = 64; });
|
|
changes("the thread group size enable", [](SampleRequest& sample) { sample.request.context.compute->tgSizeEnable = true; });
|
|
changes("the thread id component count", [](SampleRequest& sample) { sample.request.context.compute->threadIdComponentCount = 3; });
|
|
changes("the Vulkan version", [](SampleRequest& sample) { sample.request.target.vulkanVersion = 0x00402000u; });
|
|
changes("the SPIR-V version", [](SampleRequest& sample) { sample.request.target.spirvVersion = 0x00010500u; });
|
|
changes("the target subgroup size", [](SampleRequest& sample) { sample.request.target.subgroupSize = 64; });
|
|
changes("the BDA ABI version", [](SampleRequest& sample) { sample.request.target.bdaAbiVersion = 2; });
|
|
changes("a capability", [](SampleRequest& sample) { sample.capabilities[1] = 62u; });
|
|
changes("an extension", [](SampleRequest& sample) { sample.extensions[0] = "SPV_KHR_storage_buffer_storage_clasS"; });
|
|
changes("barycentrics", [](SampleRequest& sample) { sample.request.target.fragmentShaderBarycentricEnabled = true; });
|
|
changes("non-constant texel offsets", [](SampleRequest& sample) { sample.request.target.nonConstantImageOffsets = true; });
|
|
changes("the sRGB formats decoded in the shader", [](SampleRequest& sample) { sample.request.target.srgbDecodeFormats = 2u; });
|
|
changes("the narrow subgroup clock", [](SampleRequest& sample) { sample.request.target.narrowSubgroupClock = true; });
|
|
changes("f64 denormal preservation", [](SampleRequest& sample) { sample.request.target.float64DenormPreserve = true; });
|
|
changes("f16 denormal preservation", [](SampleRequest& sample) { sample.request.target.float16DenormPreserve = true; });
|
|
changes("the workgroup size limit", [](SampleRequest& sample) { sample.request.target.maxWorkgroupSize[2] = 128; });
|
|
changes("the invocation limit", [](SampleRequest& sample) { sample.request.target.maxWorkgroupInvocations = 512; });
|
|
changes("the shared memory limit", [](SampleRequest& sample) { sample.request.target.maxWorkgroupSharedMemoryBytes = 32768; });
|
|
changes("the host subgroup size", [](SampleRequest& sample) { sample.hostSubgroupSize = 64; });
|
|
changes("the descriptor set", [](SampleRequest& sample) { sample.request.layout.descriptorSet = 1; });
|
|
changes("the first binding", [](SampleRequest& sample) { sample.request.layout.firstBinding = 1; });
|
|
changes("the push constant offset", [](SampleRequest& sample) { sample.request.layout.pushConstantOffsetBytes = 16; });
|
|
changes("the push constant size", [](SampleRequest& sample) { sample.request.layout.pushConstantSizeBytes = 64; });
|
|
changes("a float mode", [](SampleRequest& sample) { sample.request.context.floatMode = ShaderFloatMode{}; });
|
|
const ShaderFloatMode astroMode{0xc0u, true, false, false};
|
|
const auto withMode = [&](const ShaderFloatMode& mode) {
|
|
SampleRequest sample;
|
|
sample.request.context.floatMode = mode;
|
|
return std::pair{sample.Key(), RecompileCacheKey::ContextHash(sample.request)};
|
|
};
|
|
const auto astro = withMode(astroMode);
|
|
require(withMode(astroMode) == astro, "the float mode key is not deterministic");
|
|
for (const auto& [what, mode] : std::initializer_list<std::pair<const char*, ShaderFloatMode>>{
|
|
{"FLOAT_MODE", {0x00u, true, false, false}}, {"DX10_CLAMP", {0xc0u, false, false, false}},
|
|
{"IEEE_MODE", {0xc0u, true, true, false}}, {"FP16_OVFL", {0xc0u, true, false, true}}}) {
|
|
const auto changed = withMode(mode);
|
|
require(changed.first != astro.first && changed.second != astro.second, std::string("the key ignores ") + what);
|
|
}
|
|
|
|
SampleRequest moved;
|
|
moved.userData[0] ^= 0x10000u;
|
|
moved.request.shader.codeAddress += 0x1000000u;
|
|
moved.request.shader.headerAddress += 0x1000000u;
|
|
moved.header[3] = std::byte{1};
|
|
require(moved.Key() == key, "the key depends on the user data values or the addresses");
|
|
std::array<std::byte, 32> descriptors{};
|
|
const std::array regions{MemoryRegion{0x100000u, descriptors}};
|
|
moved.request.context.memory = regions;
|
|
std::fill(moved.userData.begin(), moved.userData.end(), 0xffffffffu);
|
|
require(moved.Key() == key, "runtime descriptors changed the artifact key");
|
|
SampleRequest vertex;
|
|
vertex.request.shader.stage = ShaderStage::Vertex;
|
|
vertex.request.context.compute.reset();
|
|
vertex.request.context.vertex.emplace();
|
|
auto& input = *vertex.request.context.vertex;
|
|
input.resourcesNum = 1u;
|
|
input.resources[0].fields = {0x10000u, 16u << 16u, 64u, (22u << 12u) | 0xfacu};
|
|
input.resourcesDst[0] = {0u, 4u, 0u, 0u};
|
|
const auto vertexKey = vertex.Key();
|
|
const auto contextKey = RecompileCacheKey::ContextHash(vertex.request);
|
|
input.resources[0].fields = {0x20000u, 32u << 16u, 128u, (77u << 12u) | 0xfa9u};
|
|
require(vertex.Key() == vertexKey && RecompileCacheKey::ContextHash(vertex.request) == contextKey, "runtime vertex format, stride, swizzle or address changed the static interface key");
|
|
input.resources[0].fields[3] = (20u << 12u) | 0xfacu;
|
|
require(vertex.Key() != vertexKey && RecompileCacheKey::ContextHash(vertex.request) != contextKey, "integer vertex input reused a float interface");
|
|
input.resources[0].fields[3] = (77u << 12u) | 0xfa9u;
|
|
input.resourcesDst[0].registersNum = 2u;
|
|
require(vertex.Key() != vertexKey, "vertex component interface did not change the artifact key");
|
|
input.fetchEmbedded = true;
|
|
const auto embeddedKey = vertex.Key();
|
|
input.resources[0].fields = {};
|
|
input.resourcesDst[0].fetchIndex = 1u;
|
|
require(vertex.Key() == embeddedKey, "runtime vertex fetch descriptors changed the artifact key");
|
|
input.resources[0].fields[3] = (20u << 12u) | 0xfacu;
|
|
require(vertex.Key() == embeddedKey, "embedded numeric class changed the prepared template key");
|
|
input.resourcesNum = 0u;
|
|
require(vertex.Key() != embeddedKey, "embedded semantic interface did not change the artifact key");
|
|
SampleRequest fragment;
|
|
fragment.request.shader.stage = ShaderStage::Fragment;
|
|
fragment.request.context.compute.reset();
|
|
fragment.request.context.pixel.emplace();
|
|
fragment.request.context.pixel->targetOutputMode[0] = 9;
|
|
const auto fragmentKey = fragment.Key();
|
|
const auto fragmentContextKey = RecompileCacheKey::ContextHash(fragment.request);
|
|
fragment.request.context.pixel->targetExportMapping.fill(0x1bu);
|
|
require(fragment.Key() == fragmentKey && RecompileCacheKey::ContextHash(fragment.request) == fragmentContextKey, "runtime export mapping changed the fragment artifact key");
|
|
fragment.request.context.pixel->targetOutputMode[0] = 7;
|
|
require(fragment.Key() != fragmentKey, "integer fragment output reused a float interface");
|
|
}
|
|
|
|
void verifyFloatModeSerialization() {
|
|
const RequestSerializer serializer;
|
|
SampleRequest sample;
|
|
require(!serializer.Deserialize(serializer.Serialize(sample.request)).request.context.floatMode.has_value(), "an unknown float mode came back from serialization");
|
|
const ShaderFloatMode mode{0x04u, false, true, true};
|
|
sample.request.context.floatMode = mode;
|
|
const auto back = serializer.Deserialize(serializer.Serialize(sample.request));
|
|
require(back.request.context.floatMode == mode, "the float mode did not survive serialization");
|
|
}
|
|
|
|
void verifyAtomicReplacement(const std::filesystem::path& directory) {
|
|
struct Reader {
|
|
#ifdef _WIN32
|
|
HANDLE file;
|
|
explicit Reader(const std::filesystem::path& path)
|
|
: file(CreateFileW(path.c_str(), GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_DELETE,
|
|
nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr)) {
|
|
require(file != INVALID_HANDLE_VALUE, "cannot retain the cache file");
|
|
}
|
|
~Reader() { CloseHandle(file); }
|
|
std::vector<std::byte> Read() {
|
|
require(SetFilePointerEx(file, {}, nullptr, FILE_BEGIN), "cannot seek the retained cache file");
|
|
std::array<std::byte, 32> bytes{};
|
|
DWORD count = 0;
|
|
require(ReadFile(file, bytes.data(), static_cast<DWORD>(bytes.size()), &count, nullptr), "cannot read the retained cache file");
|
|
return {bytes.begin(), bytes.begin() + count};
|
|
}
|
|
#else
|
|
std::ifstream file;
|
|
explicit Reader(const std::filesystem::path& path) : file(path, std::ios::binary) {
|
|
require(file.is_open(), "cannot retain the cache file");
|
|
}
|
|
std::vector<std::byte> Read() {
|
|
file.clear();
|
|
file.seekg(0);
|
|
require(!file.fail(), "cannot seek the retained cache file");
|
|
std::array<std::byte, 32> bytes{};
|
|
file.read(reinterpret_cast<char*>(bytes.data()), bytes.size());
|
|
require(file.eof() && !file.bad(), "cannot read the retained cache file");
|
|
return {bytes.begin(), bytes.begin() + file.gcount()};
|
|
}
|
|
#endif
|
|
Reader(const Reader&) = delete;
|
|
Reader& operator=(const Reader&) = delete;
|
|
};
|
|
const auto root = directory / "atomic-replacement";
|
|
const auto path = root / "entry.bin";
|
|
const std::vector<std::byte> original{std::byte{0x12}, std::byte{0x00}, std::byte{0x34}};
|
|
const std::vector<std::byte> replacement{std::byte{0xab}, std::byte{0xcd}, std::byte{0x00}, std::byte{0xef}};
|
|
require(WriteFileAtomically(path, original), "cannot write the initial cache entry");
|
|
{
|
|
Reader old(path);
|
|
require(old.Read() == original, "the retained reader did not read the original entry");
|
|
require(WriteFileAtomically(path, replacement), "cannot replace cache entry with a retained reader");
|
|
require(old.Read() == original, "replacement changed the retained original entry");
|
|
std::vector<std::byte> fresh;
|
|
require(ReadWholeFile(path, fresh) && fresh == replacement, "a fresh reader did not read the replacement entry");
|
|
Reader current(path);
|
|
require(current.Read() == replacement, "the replacement reader did not retain its entry");
|
|
require(std::filesystem::remove(path), "cannot delete the replacement while readers retain it");
|
|
require(!ReadWholeFile(path, fresh), "the deleted cache entry remains readable by path");
|
|
require(old.Read() == original && current.Read() == replacement, "deletion invalidated retained cache readers");
|
|
}
|
|
require(std::filesystem::is_empty(root), "atomic replacement left a temporary file behind");
|
|
require(std::filesystem::remove(root), "cannot remove the empty replacement directory");
|
|
}
|
|
|
|
void verifyStore() {
|
|
require(ShaderDiskCache::Enabled(), "the disk cache is not enabled");
|
|
SampleRequest sample;
|
|
const auto key = sample.Key();
|
|
const auto variant = std::make_shared<const CompiledVariant>(sampleVariant());
|
|
const auto before = ShaderDiskCache::Totals();
|
|
CompiledVariant loaded;
|
|
require(!ShaderDiskCache::Load(key, loaded), "an entry loads before it was stored");
|
|
ShaderDiskCache::Store(key, variant);
|
|
ShaderDiskCache::Flush();
|
|
require(ShaderDiskCache::Totals().writes == before.writes + 1, "the store did not write the entry");
|
|
const auto path = ShaderDiskCache::EntryDirectory() / ShaderDiskCache::EntryName(key);
|
|
require(std::filesystem::exists(path), "no entry file at " + path.string());
|
|
require(ShaderDiskCache::Load(key, loaded), "a stored entry does not load");
|
|
requireSameVariant(*variant, loaded, "store round trip");
|
|
require(ShaderDiskCache::Totals().hits == before.hits + 1, "the load was not counted as a hit");
|
|
|
|
std::vector<std::byte> file;
|
|
require(ReadWholeFile(path, file), "cannot read the entry back");
|
|
require(WriteFileAtomically(path, std::span(file).first(file.size() / 2)), "cannot truncate the entry");
|
|
require(!ShaderDiskCache::Load(key, loaded), "a truncated entry loads");
|
|
auto corrupt = file;
|
|
corrupt[corrupt.size() - 5] ^= std::byte{0x40};
|
|
require(WriteFileAtomically(path, corrupt), "cannot damage the entry");
|
|
require(!ShaderDiskCache::Load(key, loaded), "a corrupt entry loads");
|
|
require(ShaderDiskCache::Totals().loadFailures == before.loadFailures + 2, "the rejected entries were not counted");
|
|
ShaderDiskCache::Store(key, variant);
|
|
ShaderDiskCache::Flush();
|
|
require(ShaderDiskCache::Load(key, loaded), "a rewritten entry does not load");
|
|
}
|
|
|
|
struct ComputeRequest {
|
|
std::vector<std::uint32_t> code{0xe0700000u, 0x80000000u, 0xbf810000u};
|
|
std::array<std::uint32_t, 4> userData{0x10000000u, 0x00000000u, 0x40u, 0x00027facu};
|
|
std::array<std::uint32_t, 4> capabilities{1u, 11u, 5347u, 4448u};
|
|
std::array<std::string_view, 2> extensions{"SPV_KHR_physical_storage_buffer", "SPV_KHR_8bit_storage"};
|
|
RecompileRequest request{};
|
|
|
|
explicit ComputeRequest(bool useCache) {
|
|
request.shader = {ShaderStage::Compute, 0x20000u, code, 0, {}};
|
|
request.context.waveSize = 64;
|
|
request.context.userDataBaseRegister = 0;
|
|
request.context.userData = userData;
|
|
request.context.compute = ShaderComputeStageInfo{{64u, 1u, 1u}, 0u, {false, false, false}, false, 1u};
|
|
request.target.vulkanVersion = 0x00401000u;
|
|
request.target.spirvVersion = 0x00010300u;
|
|
request.target.subgroupSize = 32;
|
|
request.target.supportedCapabilities = capabilities;
|
|
request.target.supportedExtensions = extensions;
|
|
request.target.bdaAbiVersion = 1u;
|
|
request.layout.pushConstantSizeBytes = 128;
|
|
request.useCache = useCache;
|
|
}
|
|
};
|
|
|
|
int runLoadingProcess() {
|
|
ComputeRequest cached(true);
|
|
const auto loaded = Recompile(cached.request);
|
|
const auto totals = ShaderDiskCache::Totals();
|
|
require(totals.hits == 1 && totals.misses == 0 && totals.loadFailures == 0, "the second process did not load the stored variant (hits " + std::to_string(totals.hits) + ", misses " + std::to_string(totals.misses) + ")");
|
|
ComputeRequest fresh(false);
|
|
const auto compiled = Recompile(fresh.request);
|
|
requireSameResult(compiled, loaded, "loaded against compiled");
|
|
require(!loaded.spirv.empty(), "the loaded SPIR-V is empty");
|
|
std::cout << "second process loaded " << loaded.spirv.size() << " SPIR-V words from the disk cache\n";
|
|
return 0;
|
|
}
|
|
|
|
void verifyAcrossProcesses(const char* self) {
|
|
ComputeRequest request(true);
|
|
const auto before = ShaderDiskCache::Totals();
|
|
const auto compiled = Recompile(request.request);
|
|
ShaderDiskCache::Flush();
|
|
const auto after = ShaderDiskCache::Totals();
|
|
require(after.misses == before.misses + 1 && after.writes == before.writes + 1, "the first compile was not looked up and stored");
|
|
require(!compiled.spirv.empty(), "the compile produced no SPIR-V");
|
|
const std::string command = "\"" + std::string(self) + "\" --load";
|
|
require(std::system(command.c_str()) == 0, "the loading process failed");
|
|
}
|
|
|
|
struct ClockRequest {
|
|
std::vector<std::uint32_t> code{0xf4900100u, 0x00000000u, 0xbf8cc07fu, 0x7e020204u, 0xe0700000u, 0x80000100u, 0xf800180fu, 0x01010101u, 0xbf810000u};
|
|
std::array<std::uint32_t, 4> userData{0x10000000u, 0x00000000u, 0x40u, 0x00027facu};
|
|
std::array<std::uint32_t, 4> capabilities{1u, 11u, 5347u, 4448u};
|
|
std::array<std::string_view, 2> extensions{"SPV_KHR_physical_storage_buffer", "SPV_KHR_8bit_storage"};
|
|
RecompileRequest request{};
|
|
|
|
explicit ClockRequest(std::uint32_t stride = 0) {
|
|
userData[1] = stride << 16u;
|
|
ShaderPixelStageInfo pixel{};
|
|
pixel.inputAddr = PixelInputBit(PixelInput::PerspectiveCenter);
|
|
pixel.hasPerspectiveCenterVgpr = true;
|
|
pixel.targetOutputMode[0] = 9u;
|
|
pixel.targetExportMapping.fill(0xe4u);
|
|
request.shader = {ShaderStage::Fragment, 0x30000u, code, 0, {}};
|
|
request.context.waveSize = 64;
|
|
request.context.userDataBaseRegister = 0;
|
|
request.context.userData = userData;
|
|
request.context.pixel = pixel;
|
|
request.target.vulkanVersion = 0x00401000u;
|
|
request.target.spirvVersion = 0x00010300u;
|
|
request.target.subgroupSize = 32;
|
|
request.target.supportedCapabilities = capabilities;
|
|
request.target.supportedExtensions = extensions;
|
|
request.target.bdaAbiVersion = 1u;
|
|
request.layout.pushConstantSizeBytes = 128;
|
|
}
|
|
};
|
|
|
|
std::string emissionFailure(const RecompileRequest& request) {
|
|
try {
|
|
static_cast<void>(PrepareShader(request));
|
|
} catch (const std::runtime_error& error) {
|
|
const std::string message = error.what();
|
|
require(message.find("reads the shader clock, which needs the device's VK_KHR_shader_clock") != std::string::npos, "unexpected failure: " + message.substr(0, message.find("RecompileRequest:")));
|
|
return message;
|
|
}
|
|
throw std::runtime_error("a shader clock read compiled without VK_KHR_shader_clock");
|
|
}
|
|
|
|
std::uint64_t diskMisses() {
|
|
return ShaderDiskCache::Totals().misses;
|
|
}
|
|
|
|
void verifyEmissionFailureMemo() {
|
|
const auto before = ShaderDiskCache::Totals();
|
|
ClockRequest clock;
|
|
const auto first = emissionFailure(clock.request);
|
|
require(diskMisses() == before.misses + 1, "the first compile was not looked up");
|
|
require(emissionFailure(clock.request) == first, "the second compile failed with another message");
|
|
require(diskMisses() == before.misses + 1, "the second compile looked up and emitted the program again");
|
|
|
|
ClockRequest layout;
|
|
layout.request.layout = {0, 0, 16, 112};
|
|
static_cast<void>(emissionFailure(layout.request));
|
|
require(diskMisses() == before.misses + 2, "another binding layout reused the failure");
|
|
ClockRequest stride(16);
|
|
static_cast<void>(emissionFailure(stride.request));
|
|
require(diskMisses() == before.misses + 2, "a runtime buffer stride change repeated static preparation");
|
|
ClockRequest relocated;
|
|
relocated.request.shader.codeAddress += 0x1000u;
|
|
static_cast<void>(emissionFailure(relocated.request));
|
|
require(diskMisses() == before.misses + 3, "a relocated copy reused the failure");
|
|
require(emissionFailure(clock.request) == first && diskMisses() == before.misses + 3, "the other requests replaced the failure");
|
|
|
|
ClockRequest concurrent;
|
|
concurrent.request.layout.pushConstantSizeBytes = 64;
|
|
std::array<std::future<std::string>, 4> failures;
|
|
for (auto& future : failures) future = std::async(std::launch::async, [&concurrent] { return emissionFailure(concurrent.request); });
|
|
std::vector<std::string> messages;
|
|
for (auto& future : failures) messages.push_back(future.get());
|
|
require(std::all_of(messages.begin(), messages.end(), [&](const std::string& message) { return message == messages.front(); }), "concurrent requests failed with different messages");
|
|
require(diskMisses() == before.misses + 4, "concurrent requests emitted the program " + std::to_string(diskMisses() - before.misses - 3) + " times");
|
|
|
|
ShaderDiskCache::Flush();
|
|
require(ShaderDiskCache::Totals().writes == before.writes, "an emission failure was stored in the disk cache");
|
|
}
|
|
|
|
int runWithoutFailureMemo() {
|
|
ClockRequest clock;
|
|
const auto first = emissionFailure(clock.request);
|
|
require(emissionFailure(clock.request) == first, "the second compile failed with another message");
|
|
require(diskMisses() == 2, "APS5_NO_FAILURE_MEMO=1 did not emit the program again (" + std::to_string(diskMisses()) + " lookups)");
|
|
std::cout << "APS5_NO_FAILURE_MEMO=1 emitted the failing program twice\n";
|
|
return 0;
|
|
}
|
|
|
|
void verifyFailureMemoSwitch(const char* self) {
|
|
setEnvironment("APS5_NO_FAILURE_MEMO", "1");
|
|
const std::string command = "\"" + std::string(self) + "\" --no-failure-memo";
|
|
require(std::system(command.c_str()) == 0, "the process with APS5_NO_FAILURE_MEMO=1 failed");
|
|
}
|
|
|
|
void verifyInvocationIsolation() {
|
|
ComputeRequest request(true);
|
|
const auto first = Recompile(request.request);
|
|
request.userData[0] += 0x10000u;
|
|
const auto second = Recompile(request.request);
|
|
require(first.variantId != 0 && first.variantId == second.variantId, "a buffer address change did not reuse the artifact");
|
|
require(second.cacheHit, "the second invocation missed the compiled cache");
|
|
requireSameArtifact(first, second, "invocations sharing an artifact");
|
|
require(first.spirv.data() == second.spirv.data(), "invocations duplicated SPIR-V storage");
|
|
require(!sameBindings(first.bindings, second.bindings) || first.pushConstants != second.pushConstants, "the second invocation reused stale resource data");
|
|
request.userData[0] -= 0x10000u;
|
|
const auto restored = Recompile(request.request);
|
|
requireSameResult(first, restored, "restored invocation");
|
|
const auto original = request.userData;
|
|
const std::array<std::array<std::uint32_t, 4>, 10> descriptors{{
|
|
{original[0] + 0x20000u, original[1], original[2], original[3]},
|
|
{original[0] + 1u, original[1], original[2], original[3]},
|
|
{original[0] + 2u, original[1], original[2], original[3]},
|
|
{original[0] + 3u, original[1], original[2], original[3]},
|
|
{original[0], original[1] | 1u, original[2], original[3]},
|
|
{original[0], original[1], original[2] / 2u, original[3]},
|
|
{original[0], original[1] | (16u << 16u), original[2], original[3]},
|
|
{original[0], original[1], original[2], 0x31004688u},
|
|
{original[0], original[1] | 0x80100000u, original[2], original[3] | 0x00600000u},
|
|
{0u, 0u, 0u, 0u}
|
|
}};
|
|
for (const auto& descriptor : descriptors) {
|
|
request.userData = descriptor;
|
|
const auto changed = Recompile(request.request);
|
|
require(changed.cacheHit && first.variantId == changed.variantId, "buffer metadata changed the compiled variant");
|
|
if ((descriptor[0] & 3u) == (original[0] & 3u) && (descriptor[1] & 0xffff0000u) == (original[1] & 0xffff0000u) && descriptor[3] == original[3] && (descriptor[0] != 0u || (descriptor[1] & 0xffffu) != 0u)) {
|
|
require(first.spirv.data() == changed.spirv.data() && first.PipelineVariantId() == changed.PipelineVariantId(), "buffer address or size rebuilt the specialized module");
|
|
}
|
|
ComputeRequest fresh(false);
|
|
fresh.userData = descriptor;
|
|
requireSameResult(Recompile(fresh.request), changed, "reused binding plan against fresh compilation");
|
|
const auto repeated = Recompile(request.request);
|
|
require(changed.spirv.data() == repeated.spirv.data() && changed.PipelineVariantId() == repeated.PipelineVariantId(), "buffer specialization did not reuse its module");
|
|
}
|
|
request.userData = original;
|
|
request.userData[3] |= 0x40000000u;
|
|
bool rejected = false;
|
|
try { static_cast<void>(Recompile(request.request)); }
|
|
catch (const std::runtime_error&) { rejected = true; }
|
|
require(rejected, "an unsupported buffer descriptor type was accepted");
|
|
}
|
|
|
|
void verifyBufferAlignmentSpecialization() {
|
|
ComputeRequest request(true);
|
|
request.request.context.waveSize = 32u;
|
|
request.request.context.compute->numThreads = {32u, 1u, 1u};
|
|
request.userData[3] = 0x31027facu;
|
|
static_cast<void>(PrepareShader(request.request));
|
|
const auto misses = diskMisses();
|
|
const auto aligned = Recompile(request.request);
|
|
const auto countAtomics = [](const RecompileResult& result) {
|
|
std::size_t atomics = 0;
|
|
for (std::size_t cursor = 5; cursor < result.spirv.size();) {
|
|
const auto count = result.spirv[cursor] >> 16u;
|
|
require(count != 0u && count <= result.spirv.size() - cursor, "invalid specialized buffer instruction");
|
|
if ((result.spirv[cursor] & 0xffffu) == spv::OpAtomicCompareExchange) ++atomics;
|
|
cursor += count;
|
|
}
|
|
return atomics;
|
|
};
|
|
require(countAtomics(aligned) == 0u, "aligned DWORD store retained unaligned atomic updates");
|
|
for (std::uint32_t offset = 1; offset < 4u; ++offset) {
|
|
request.userData[0] = 0x10000000u + offset;
|
|
const auto unaligned = Recompile(request.request);
|
|
require(unaligned.variantId == aligned.variantId && diskMisses() == misses, "base alignment repeated static shader preparation");
|
|
require(countAtomics(unaligned) == 2u, "unaligned DWORD store did not split into two atomic updates");
|
|
request.userData[0] += 0x10000u;
|
|
const auto relocated = Recompile(request.request);
|
|
require(unaligned.spirv.data() == relocated.spirv.data(), "relocation with unchanged alignment rebuilt the buffer specialization");
|
|
}
|
|
request.userData[0] = 0x10000000u;
|
|
const auto restored = Recompile(request.request);
|
|
require(restored.spirv.data() == aligned.spirv.data(), "unaligned buffers replaced the aligned specialization");
|
|
}
|
|
|
|
void verifyBindingPlanSelection() {
|
|
ShaderInfo info{};
|
|
info.buffers.resize(2);
|
|
info.buffers[0].atomic = true;
|
|
info.buffers[1].read = true;
|
|
CompiledBindingLayout compiled{};
|
|
compiled.layout.descriptors = {{DescriptorBindingKind::Buffers, {0, 1}}, {DescriptorBindingKind::FlattenedSrt, {}}, {DescriptorBindingKind::ShaderData, {}}, {DescriptorBindingKind::Gds, {}}};
|
|
compiled.layout.userDataRegisters = {0};
|
|
compiled.layout.memoryOffsetDword = 1;
|
|
ResourceSnapshot snapshot{};
|
|
snapshot.buffers.resize(2);
|
|
for (auto& buffer : snapshot.buffers) {
|
|
buffer.dwordCount = 4u;
|
|
buffer.dwords = {0x10000000u, 0u, 64u, 0x31016facu};
|
|
}
|
|
snapshot.userData = {17u};
|
|
snapshot.flattenedSrt = {23u};
|
|
const DescriptorBindingBuilder builder;
|
|
const auto plan = builder.Prepare(compiled.layout, info, IrShaderStage::Compute, snapshot);
|
|
BindingAllocationResult full;
|
|
builder.Populate(full, compiled, plan, 0u, snapshot, {});
|
|
require(full.bindings.size() == 4u, "binding plan lost a descriptor before module selection");
|
|
require(full.bindings[0].bufferAtomic == std::vector<bool>{true, false} && full.bindings[0].bufferWritten == std::vector<bool>{true, false} && full.bindings[0].bufferRead == std::vector<bool>{false, true}, "binding plan lost buffer access metadata");
|
|
const std::array liveBindings{full.bindings[0].binding, full.bindings[0].binding, full.bindings[3].binding};
|
|
const auto selected = builder.Select(plan, liveBindings);
|
|
snapshot.userData.clear();
|
|
snapshot.flattenedSrt.clear();
|
|
snapshot.buffers[0].dwords[0] += 0x10000u;
|
|
snapshot.buffers[1].dwords[2] = 128u;
|
|
BindingAllocationResult populated;
|
|
builder.Populate(populated, compiled, selected, 0u, snapshot, {});
|
|
require(populated.bindings.size() == 2u && populated.pushConstants.empty(), "module selection retained an unused descriptor");
|
|
auto expected = full.bindings[0];
|
|
expected.guestDescriptor[0] += 0x10000u;
|
|
expected.guestDescriptor[6] = 128u;
|
|
require(sameBinding(expected, populated.bindings[0]) && sameBinding(full.bindings[3], populated.bindings[1]), "selected plan changed binding order or reused stale buffer data");
|
|
bool rejected = false;
|
|
try { static_cast<void>(builder.Select(plan, std::array{UINT32_MAX})); }
|
|
catch (const std::runtime_error&) { rejected = true; }
|
|
require(rejected, "module selection accepted an unknown binding");
|
|
snapshot.buffers[0].dwordCount = 3u;
|
|
rejected = false;
|
|
try { builder.Populate(populated, compiled, selected, 0u, snapshot, {}); }
|
|
catch (const std::runtime_error&) { rejected = true; }
|
|
require(rejected, "selected plan accepted an invalid live buffer descriptor");
|
|
snapshot.buffers[0].dwordCount = 4u;
|
|
const auto rejectsAlignment = [&](const char* diagnostic) {
|
|
try { static_cast<void>(builder.Prepare(compiled.layout, info, IrShaderStage::Compute, snapshot)); }
|
|
catch (const std::runtime_error& error) {
|
|
require(std::string_view(error.what()).find(diagnostic) != std::string_view::npos, "alignment check returned an unrelated failure");
|
|
return;
|
|
}
|
|
throw std::runtime_error("binding plan accepted an unsupported buffer alignment");
|
|
};
|
|
snapshot.buffers[0].dwords[0] |= 2u;
|
|
rejectsAlignment("buffer atomic on a V# whose base is not DWORD aligned");
|
|
snapshot.buffers[0].dwords[0] &= ~3u;
|
|
info.buffers[1].typedAlignment = 2u;
|
|
snapshot.buffers[1].dwords[0] |= 2u;
|
|
static_cast<void>(builder.Prepare(compiled.layout, info, IrShaderStage::Compute, snapshot));
|
|
snapshot.buffers[1].dwords[0] |= 1u;
|
|
rejectsAlignment("base is not aligned to its element");
|
|
info.buffers[1].typedAlignment = 1u;
|
|
info.buffers[1].descriptorFormatted = true;
|
|
snapshot.buffers[1].dwords[3] = 0x3100bfacu;
|
|
rejectsAlignment("base is not aligned to its element");
|
|
snapshot.buffers[1].dwords[0] &= ~1u;
|
|
static_cast<void>(builder.Prepare(compiled.layout, info, IrShaderStage::Compute, snapshot));
|
|
snapshot.buffers[1].dwords[3] = 0x31014facu;
|
|
rejectsAlignment("base is not aligned to its element");
|
|
}
|
|
|
|
void verifyVertexTypeSpecialization() {
|
|
CompiledShaderArtifact artifact;
|
|
artifact.vertexInputs = {{0, 4, 0, 1}, {1, 4, 0, 1}};
|
|
artifact.spirv = std::vector<std::uint32_t>{
|
|
spv::MagicNumber, 0x00010000u, 0, 28, 0,
|
|
(2u << 16u) | spv::OpCapability, spv::CapabilityShader,
|
|
(3u << 16u) | spv::OpMemoryModel, spv::AddressingModelLogical, spv::MemoryModelGLSL450,
|
|
(7u << 16u) | spv::OpEntryPoint, spv::ExecutionModelVertex, 20, 0x6e69616du, 0, 10, 11,
|
|
(4u << 16u) | spv::OpDecorate, 10, spv::DecorationLocation, 0,
|
|
(4u << 16u) | spv::OpDecorate, 11, spv::DecorationLocation, 1,
|
|
(2u << 16u) | spv::OpTypeVoid, 1,
|
|
(3u << 16u) | spv::OpTypeFloat, 2, 32,
|
|
(4u << 16u) | spv::OpTypeInt, 3, 32, 0,
|
|
(4u << 16u) | spv::OpTypeVector, 4, 2, 4,
|
|
(4u << 16u) | spv::OpTypePointer, 5, spv::StorageClassInput, 4,
|
|
(4u << 16u) | spv::OpTypePointer, 6, spv::StorageClassInput, 2,
|
|
(3u << 16u) | spv::OpTypeFunction, 7, 1,
|
|
(4u << 16u) | spv::OpConstant, 3, 8, 0,
|
|
(4u << 16u) | spv::OpVariable, 5, 10, spv::StorageClassInput,
|
|
(4u << 16u) | spv::OpVariable, 5, 11, spv::StorageClassInput,
|
|
(5u << 16u) | spv::OpFunction, 1, 20, spv::FunctionControlMaskNone, 7,
|
|
(2u << 16u) | spv::OpLabel, 21,
|
|
(5u << 16u) | spv::OpAccessChain, 6, 22, 10, 8,
|
|
(4u << 16u) | spv::OpLoad, 2, 23, 22,
|
|
(4u << 16u) | spv::OpBitcast, 3, 24, 23,
|
|
(5u << 16u) | spv::OpAccessChain, 6, 25, 11, 8,
|
|
(4u << 16u) | spv::OpLoad, 2, 26, 25,
|
|
(4u << 16u) | spv::OpBitcast, 3, 27, 26,
|
|
(1u << 16u) | spv::OpReturn,
|
|
(1u << 16u) | spv::OpFunctionEnd
|
|
};
|
|
PrepareVertexInputSpecialization(artifact);
|
|
const auto original = artifact.spirv.Words();
|
|
for (std::uint32_t first = 0; first < 3u; ++first) {
|
|
for (std::uint32_t second = 0; second < 3u; ++second) {
|
|
const std::array classes{first, second};
|
|
const auto specialized = SpecializeVertexInputTypes(artifact, classes);
|
|
const auto& words = specialized.Words();
|
|
std::map<std::uint32_t, std::uint32_t> scalarKinds;
|
|
std::uint32_t checked = 0;
|
|
for (std::size_t cursor = 5; cursor < words.size(); cursor += words[cursor] >> 16u) {
|
|
const auto op = static_cast<spv::Op>(words[cursor] & 0xffffu);
|
|
if (op == spv::OpTypeFloat) scalarKinds.emplace(words[cursor + 1u], 0u);
|
|
if (op == spv::OpTypeInt) scalarKinds.emplace(words[cursor + 1u], words[cursor + 3u] != 0u ? 1u : 2u);
|
|
if (op == spv::OpLoad) {
|
|
const auto attribute = words[cursor + 2u] == 23u ? 0u : 1u;
|
|
require(scalarKinds.at(words[cursor + 1u]) == classes[attribute], "vertex input loaded the wrong numeric type");
|
|
++checked;
|
|
}
|
|
if (op == spv::OpBitcast || op == spv::OpCopyObject) {
|
|
const auto attribute = words[cursor + 2u] == 24u ? 0u : 1u;
|
|
require(op == (classes[attribute] == 2u ? spv::OpCopyObject : spv::OpBitcast), "vertex input did not preserve raw component bits");
|
|
++checked;
|
|
}
|
|
}
|
|
require(checked == 4u, "vertex specialization lost attribute loads");
|
|
require(artifact.spirv.Words() == original, "vertex specialization mutated the shared template");
|
|
}
|
|
}
|
|
auto invalid = artifact;
|
|
invalid.vertexInputPatches.front().word = static_cast<std::uint32_t>(original.size());
|
|
bool rejected = false;
|
|
try { static_cast<void>(SpecializeVertexInputTypes(invalid, std::array{0u, 0u})); }
|
|
catch (const std::runtime_error&) { rejected = true; }
|
|
require(rejected, "out-of-range vertex type patch was accepted");
|
|
}
|
|
|
|
void verifyBuiltinSpecialization() {
|
|
std::vector<std::uint32_t> words{spv::MagicNumber, 0x00010300u, 0u, 40u, 0u};
|
|
const auto emit = [&](spv::Op op, std::initializer_list<std::uint32_t> operands) {
|
|
words.push_back((static_cast<std::uint32_t>(operands.size() + 1u) << 16u) | op);
|
|
words.insert(words.end(), operands);
|
|
};
|
|
emit(spv::OpCapability, {spv::CapabilityShader});
|
|
emit(spv::OpMemoryModel, {spv::AddressingModelLogical, spv::MemoryModelGLSL450});
|
|
emit(spv::OpEntryPoint, {spv::ExecutionModelGLCompute, 10u, 0x6e69616du, 0u});
|
|
emit(spv::OpExecutionMode, {10u, spv::ExecutionModeLocalSize, 1u, 1u, 1u});
|
|
emit(spv::OpTypeVoid, {1u});
|
|
emit(spv::OpTypeBool, {2u});
|
|
emit(spv::OpTypeInt, {3u, 32u, 0u});
|
|
emit(spv::OpTypeVector, {4u, 3u, 4u});
|
|
emit(spv::OpTypeFunction, {5u, 1u});
|
|
emit(spv::OpConstant, {3u, 6u, 0u});
|
|
emit(spv::OpConstant, {3u, 7u, 1u});
|
|
emit(spv::OpConstant, {3u, 8u, 2u});
|
|
emit(spv::OpConstant, {3u, 9u, 3u});
|
|
emit(spv::OpConstantComposite, {4u, 30u, 6u, 7u, 8u, 9u});
|
|
emit(spv::OpTypePointer, {32u, spv::StorageClassPrivate, 4u});
|
|
emit(spv::OpVariable, {32u, 31u, spv::StorageClassPrivate});
|
|
emit(spv::OpTypePointer, {33u, spv::StorageClassPrivate, 3u});
|
|
emit(spv::OpVariable, {33u, 34u, spv::StorageClassPrivate});
|
|
emit(spv::OpFunction, {1u, 10u, spv::FunctionControlMaskNone, 5u});
|
|
emit(spv::OpLabel, {11u});
|
|
emit(spv::OpBitFieldUExtract, {3u, 12u, 9u, 7u, 7u});
|
|
emit(spv::OpSelectionMerge, {16u, spv::SelectionControlMaskNone});
|
|
emit(spv::OpSwitch, {12u, 15u, 1u, 14u});
|
|
emit(spv::OpLabel, {14u});
|
|
emit(spv::OpBranch, {16u});
|
|
emit(spv::OpLabel, {15u});
|
|
emit(spv::OpIAdd, {3u, 17u, 8u, 9u});
|
|
emit(spv::OpBranch, {16u});
|
|
emit(spv::OpLabel, {16u});
|
|
emit(spv::OpPhi, {3u, 18u, 7u, 14u, 17u, 15u});
|
|
emit(spv::OpStore, {34u, 18u});
|
|
emit(spv::OpIEqual, {2u, 19u, 18u, 7u});
|
|
emit(spv::OpSelectionMerge, {22u, spv::SelectionControlMaskNone});
|
|
emit(spv::OpBranchConditional, {19u, 20u, 21u});
|
|
emit(spv::OpLabel, {20u});
|
|
emit(spv::OpVectorExtractDynamic, {3u, 23u, 30u, 6u});
|
|
emit(spv::OpVectorExtractDynamic, {3u, 24u, 30u, 7u});
|
|
emit(spv::OpVectorExtractDynamic, {3u, 25u, 30u, 8u});
|
|
emit(spv::OpVectorExtractDynamic, {3u, 26u, 30u, 9u});
|
|
emit(spv::OpCompositeConstruct, {4u, 27u, 23u, 24u, 25u, 26u});
|
|
emit(spv::OpStore, {31u, 27u});
|
|
emit(spv::OpBranch, {22u});
|
|
emit(spv::OpLabel, {21u});
|
|
emit(spv::OpUnreachable, {});
|
|
emit(spv::OpLabel, {22u});
|
|
emit(spv::OpReturn, {});
|
|
emit(spv::OpFunctionEnd, {});
|
|
const auto specialized = SpecializeSpirv(words);
|
|
bool identity = false;
|
|
std::uint32_t phiValue = 0u;
|
|
for (std::size_t cursor = 5; cursor < specialized.size();) {
|
|
const auto count = specialized[cursor] >> 16u;
|
|
require(count != 0u && count <= specialized.size() - cursor, "builtin specialization produced a truncated instruction");
|
|
const auto op = static_cast<spv::Op>(specialized[cursor] & 0xffffu);
|
|
require(op != spv::OpSwitch && op != spv::OpBranchConditional && op != spv::OpPhi && op != spv::OpVectorExtractDynamic, "builtin specialization retained constant control flow or dynamic exports");
|
|
if (op == spv::OpStore && specialized[cursor + 1u] == 31u) identity = specialized[cursor + 2u] == 30u;
|
|
if (op == spv::OpStore && specialized[cursor + 1u] == 34u) phiValue = specialized[cursor + 2u];
|
|
cursor += count;
|
|
}
|
|
bool correctPhi = false;
|
|
for (std::size_t cursor = 5; cursor < specialized.size(); cursor += specialized[cursor] >> 16u) {
|
|
if ((specialized[cursor] & 0xffffu) == spv::OpConstant && specialized[cursor + 2u] == phiValue) correctPhi = specialized[cursor + 3u] == 1u;
|
|
}
|
|
require(identity && correctPhi, "builtin specialization selected the wrong export or phi value");
|
|
require(SpecializeSpirv(specialized) == specialized, "builtin specialization is not stable");
|
|
}
|
|
|
|
void verifySpecializationLiveness() {
|
|
std::vector<std::uint32_t> words{spv::MagicNumber, 0x00010300u, 0u, 64u, 0u};
|
|
const auto emit = [&](spv::Op op, std::initializer_list<std::uint32_t> operands) {
|
|
words.push_back((static_cast<std::uint32_t>(operands.size() + 1u) << 16u) | op);
|
|
words.insert(words.end(), operands);
|
|
};
|
|
emit(spv::OpCapability, {spv::CapabilityShader});
|
|
emit(spv::OpMemoryModel, {spv::AddressingModelLogical, spv::MemoryModelGLSL450});
|
|
emit(spv::OpEntryPoint, {spv::ExecutionModelGLCompute, 10u, 0x6e69616du, 0u});
|
|
emit(spv::OpExecutionMode, {10u, spv::ExecutionModeLocalSize, 1u, 1u, 1u});
|
|
emit(spv::OpDecorate, {27u, spv::DecorationRelaxedPrecision});
|
|
emit(spv::OpTypeVoid, {1u});
|
|
emit(spv::OpTypeInt, {3u, 32u, 0u});
|
|
emit(spv::OpTypePointer, {4u, spv::StorageClassPrivate, 3u});
|
|
emit(spv::OpTypeFunction, {5u, 1u});
|
|
emit(spv::OpConstant, {3u, 6u, 0u});
|
|
emit(spv::OpConstant, {3u, 7u, 1u});
|
|
emit(spv::OpTypeStruct, {24u, 3u, 3u, 3u, 3u, 3u, 3u, 3u, 3u, 3u, 3u, 3u, 3u, 3u, 3u, 3u, 3u});
|
|
emit(spv::OpConstantComposite, {24u, 25u, 7u, 7u, 7u, 7u, 7u, 7u, 7u, 7u, 7u, 7u, 7u, 7u, 7u, 7u, 7u, 7u});
|
|
emit(spv::OpVariable, {4u, 8u, spv::StorageClassPrivate, 6u});
|
|
emit(spv::OpFunction, {1u, 10u, spv::FunctionControlMaskNone, 5u});
|
|
emit(spv::OpLabel, {11u});
|
|
emit(spv::OpLoad, {3u, 12u, 8u});
|
|
emit(spv::OpIMul, {3u, 13u, 12u, 7u});
|
|
emit(spv::OpIAdd, {3u, 14u, 13u, 7u});
|
|
emit(spv::OpCopyObject, {3u, 15u, 12u});
|
|
emit(spv::OpCopyObject, {3u, 16u, 15u});
|
|
emit(spv::OpIAdd, {3u, 17u, 16u, 7u});
|
|
emit(spv::OpStore, {8u, 17u});
|
|
emit(spv::OpLoad, {3u, 18u, 8u, spv::MemoryAccessVolatileMask});
|
|
emit(spv::OpFunctionCall, {1u, 20u, 30u});
|
|
emit(spv::OpCompositeExtract, {3u, 26u, 25u, 15u});
|
|
emit(spv::OpStore, {8u, 26u});
|
|
emit(spv::OpCopyObject, {3u, 27u, 12u});
|
|
emit(spv::OpStore, {8u, 27u});
|
|
emit(spv::OpLoad, {3u, 28u, 8u});
|
|
emit(spv::OpReturn, {});
|
|
emit(spv::OpFunctionEnd, {});
|
|
emit(spv::OpFunction, {1u, 30u, spv::FunctionControlMaskNone, 5u});
|
|
emit(spv::OpLabel, {31u});
|
|
emit(spv::OpReturn, {});
|
|
emit(spv::OpFunctionEnd, {});
|
|
const auto specialized = SpecializeSpirv(words);
|
|
std::map<std::uint32_t, std::vector<std::uint32_t>> results;
|
|
std::size_t stores = 0;
|
|
for (std::size_t cursor = 5; cursor < specialized.size();) {
|
|
const auto count = specialized[cursor] >> 16u;
|
|
const auto op = static_cast<spv::Op>(specialized[cursor] & 0xffffu);
|
|
require(count != 0u && count <= specialized.size() - cursor, "dead computation removal truncated an instruction");
|
|
if (op == spv::OpLoad || op == spv::OpCopyObject || op == spv::OpIAdd || op == spv::OpIMul || op == spv::OpCompositeExtract || op == spv::OpFunctionCall) results.emplace(specialized[cursor + 2u], std::vector<std::uint32_t>(specialized.begin() + cursor, specialized.begin() + cursor + count));
|
|
if (op == spv::OpStore) ++stores;
|
|
cursor += count;
|
|
}
|
|
for (const auto id : {13u, 14u, 15u, 16u, 28u}) require(!results.contains(id), "specialization retained a dead computation or copy chain");
|
|
require(results.at(17u).at(3) == 12u, "copy propagation lost the live source");
|
|
require(results.at(26u).at(4) == 15u, "copy propagation rewrote a literal as an ID");
|
|
require(results.contains(18u) && results.contains(20u) && results.contains(27u) && stores == 3u, "specialization removed an effect or decorated copy");
|
|
require(SpecializeSpirv(specialized) == specialized, "dead computation removal is not stable");
|
|
}
|
|
|
|
void verifyDefaultDirectory(const char* self) {
|
|
setEnvironment("ANYPS5_SHADER_CACHE_DIR", "");
|
|
setEnvironment("ANYPS5_NO_SHADER_CACHE", "1");
|
|
require(ShaderRecompiler::ShaderCacheDirectory().empty(), "ANYPS5_NO_SHADER_CACHE=1 did not disable the cache");
|
|
setEnvironment("ANYPS5_NO_SHADER_CACHE", "0");
|
|
const auto directory = ShaderRecompiler::ShaderCacheDirectory();
|
|
require(directory.filename() == "shader_cache", "the default cache directory is not named shader_cache");
|
|
std::error_code error;
|
|
require(std::filesystem::equivalent(directory.parent_path(), std::filesystem::absolute(self).parent_path(), error) && !error, "the default cache directory is not beside the executable");
|
|
}
|
|
|
|
}
|
|
|
|
int main(int argc, char** argv) {
|
|
try {
|
|
if (argc == 2 && std::string_view(argv[1]) == "--load") return runLoadingProcess();
|
|
if (argc == 2 && std::string_view(argv[1]) == "--no-failure-memo") return runWithoutFailureMemo();
|
|
const auto directory = std::filesystem::temp_directory_path() / ("aps5-shader-disk-cache-test-" + std::to_string(std::random_device{}()));
|
|
std::filesystem::remove_all(directory);
|
|
verifyDefaultDirectory(argv[0]);
|
|
setEnvironment("ANYPS5_NO_SHADER_CACHE", "0");
|
|
setEnvironment("ANYPS5_SHADER_CACHE_DIR", directory.string());
|
|
verifyAtomicReplacement(directory);
|
|
verifyResultRoundTrip();
|
|
verifyEntryRoundTrip();
|
|
verifyArtifactStorageIsolation();
|
|
verifyKeySensitivity();
|
|
verifyFloatModeSerialization();
|
|
verifyStore();
|
|
verifyAcrossProcesses(argv[0]);
|
|
verifyEmissionFailureMemo();
|
|
verifyFailureMemoSwitch(argv[0]);
|
|
verifyInvocationIsolation();
|
|
verifyBufferAlignmentSpecialization();
|
|
verifyBindingPlanSelection();
|
|
verifyVertexTypeSpecialization();
|
|
verifyBuiltinSpecialization();
|
|
verifySpecializationLiveness();
|
|
ShaderRecompiler::ShaderDiskCache::Flush();
|
|
std::error_code error;
|
|
std::filesystem::remove_all(directory, error);
|
|
require(!error, "cannot remove the shader cache test directory: " + error.message());
|
|
std::cout << "shader disk cache tests passed\n";
|
|
return 0;
|
|
} catch (const std::exception& error) {
|
|
std::cerr << error.what() << '\n';
|
|
return 1;
|
|
}
|
|
}
|