Merge pull request #2369 from karanvora2599/perf/shader-coherent-alias
perf(shader): load from buffers without Coherent when the load has no GLC or DLC
This commit is contained in:
commit
9a9bd3fb3c
|
|
@ -1980,6 +1980,102 @@ void verifyInt64AtomicCapabilities() {
|
|||
require(declares(compile(imageAtomic, spv::CapabilityInt64ImageEXT), spv::CapabilityInt64ImageEXT), "64-bit atomics: an image_atomic_swap on a 32_32 image does not declare Int64ImageEXT");
|
||||
}
|
||||
|
||||
void verifyCoherentBufferAccesses() {
|
||||
using namespace ShaderRecompiler;
|
||||
alignas(256) static std::array<std::uint32_t, 64> input{};
|
||||
alignas(256) static std::array<std::uint32_t, 64> output{};
|
||||
const auto inputAddress = static_cast<std::uint64_t>(reinterpret_cast<std::uintptr_t>(input.data()));
|
||||
const auto outputAddress = static_cast<std::uint64_t>(reinterpret_cast<std::uintptr_t>(output.data()));
|
||||
const std::array<std::uint32_t, 8> userData{
|
||||
static_cast<std::uint32_t>(inputAddress), static_cast<std::uint32_t>((inputAddress >> 32u) & 0xffffu), 256u, 0x31016facu,
|
||||
static_cast<std::uint32_t>(outputAddress), static_cast<std::uint32_t>((outputAddress >> 32u) & 0xffffu), 256u, 0x31016facu};
|
||||
const std::array<std::string_view, 2> extensions{"SPV_KHR_physical_storage_buffer", "SPV_KHR_8bit_storage"};
|
||||
const std::array<std::uint32_t, 3> capabilities{spv::CapabilityInt64, spv::CapabilityPhysicalStorageBufferAddresses, spv::CapabilityStorageBuffer8BitAccess};
|
||||
struct Module {
|
||||
std::map<std::uint32_t, std::string> names;
|
||||
std::map<std::uint32_t, std::vector<std::uint32_t>> decorations;
|
||||
std::vector<std::string> loads;
|
||||
std::vector<std::string> stores;
|
||||
};
|
||||
const auto compile = [&](bool invalidate, bool glcLoad, bool glcStore) {
|
||||
std::vector<std::uint32_t> code{0x34020082u};
|
||||
if (invalidate) code.insert(code.end(), {0xe1c40000u, 0u});
|
||||
code.insert(code.end(), {0xe0301000u | (glcLoad ? 0x4000u : 0u), 0x80000201u, 0xbf8c3f70u, 0xe0701000u | (glcStore ? 0x4000u : 0u), 0x80010201u, 0xbf810000u});
|
||||
RecompileRequest request{};
|
||||
request.shader = {ShaderStage::Compute, 0x5a000u, code, 0, {}};
|
||||
request.context.waveSize = 32;
|
||||
request.context.userDataBaseRegister = 0;
|
||||
request.context.userData = userData;
|
||||
request.context.compute = ShaderComputeStageInfo{{32u, 1u, 1u}, 0u, {false, false, false}, false, 1u};
|
||||
request.target.vulkanVersion = 0x00401000u;
|
||||
request.target.spirvVersion = 0x00010300u;
|
||||
request.target.subgroupSize = 32;
|
||||
request.target.bdaAbiVersion = BdaAbi::Version;
|
||||
request.target.supportedCapabilities = capabilities;
|
||||
request.target.supportedExtensions = extensions;
|
||||
request.target.fragmentShaderBarycentricEnabled = false;
|
||||
request.layout.pushConstantSizeBytes = 128;
|
||||
request.useCache = false;
|
||||
const auto words = Recompile(request).spirv;
|
||||
Module module;
|
||||
std::map<std::uint32_t, std::uint32_t> bases;
|
||||
const auto base = [&](std::uint32_t pointer) {
|
||||
const auto found = bases.find(pointer);
|
||||
const auto variable = found == bases.end() ? pointer : found->second;
|
||||
const auto name = module.names.find(variable);
|
||||
return name == module.names.end() ? std::string() : name->second;
|
||||
};
|
||||
for (std::size_t cursor = 5; cursor < words.size();) {
|
||||
const auto count = words[cursor] >> 16u;
|
||||
const auto opcode = words[cursor] & 0xffffu;
|
||||
require(count != 0 && count <= words.size() - cursor, "coherent buffers: truncated SPIR-V instruction");
|
||||
if (opcode == spv::OpName) module.names[words[cursor + 1]] = reinterpret_cast<const char*>(&words[cursor + 2]);
|
||||
if (opcode == spv::OpDecorate) module.decorations[words[cursor + 1]].push_back(words[cursor + 2]);
|
||||
if (opcode == spv::OpAccessChain || opcode == spv::OpInBoundsAccessChain) {
|
||||
const auto found = bases.find(words[cursor + 3]);
|
||||
bases[words[cursor + 2]] = found == bases.end() ? words[cursor + 3] : found->second;
|
||||
}
|
||||
if (opcode == spv::OpLoad && base(words[cursor + 3]).starts_with("buffers")) module.loads.push_back(base(words[cursor + 3]));
|
||||
if (opcode == spv::OpStore && base(words[cursor + 1]).starts_with("buffers")) module.stores.push_back(base(words[cursor + 1]));
|
||||
cursor += count;
|
||||
}
|
||||
return module;
|
||||
};
|
||||
const auto decorated = [](const Module& module, std::string_view name, spv::Decoration decoration) {
|
||||
for (const auto& [id, variable] : module.names) {
|
||||
if (variable != name) continue;
|
||||
const auto found = module.decorations.find(id);
|
||||
return found != module.decorations.end() && std::ranges::find(found->second, static_cast<std::uint32_t>(decoration)) != found->second.end();
|
||||
}
|
||||
return false;
|
||||
};
|
||||
const auto declared = [](const Module& module, std::string_view name) {
|
||||
return std::ranges::any_of(module.names, [&](const auto& entry) { return entry.second == name; });
|
||||
};
|
||||
const auto only = [](const std::vector<std::string>& accesses, std::string_view name) {
|
||||
return !accesses.empty() && std::ranges::all_of(accesses, [&](const std::string& access) { return access == name; });
|
||||
};
|
||||
|
||||
const auto plain = compile(false, false, false);
|
||||
require(!declared(plain, "buffers_coherent") && !decorated(plain, "buffers", spv::DecorationCoherent), "coherent buffers: a program without GLC or DLC accesses declares coherent buffers");
|
||||
require(only(plain.loads, "buffers") && only(plain.stores, "buffers"), "coherent buffers: a program without GLC or DLC accesses does not use the buffer array");
|
||||
|
||||
const auto store = compile(false, false, true);
|
||||
require(declared(store, "buffers_coherent") && decorated(store, "buffers_coherent", spv::DecorationCoherent), "coherent buffers: a GLC store has no coherent buffer array");
|
||||
require(!decorated(store, "buffers", spv::DecorationCoherent), "coherent buffers: a GLC store makes the loads without GLC coherent");
|
||||
require(decorated(store, "buffers", spv::DecorationAliased) && decorated(store, "buffers_coherent", spv::DecorationAliased), "coherent buffers: the two buffer arrays are not decorated Aliased");
|
||||
require(only(store.loads, "buffers"), "coherent buffers: a load without GLC goes through the coherent buffer array");
|
||||
require(only(store.stores, "buffers_coherent"), "coherent buffers: a GLC store goes through the buffer array without Coherent");
|
||||
|
||||
const auto load = compile(false, true, false);
|
||||
require(only(load.loads, "buffers_coherent"), "coherent buffers: a GLC load goes through the buffer array without Coherent");
|
||||
require(only(load.stores, "buffers_coherent"), "coherent buffers: a store in a program with GLC accesses goes through the buffer array without Coherent");
|
||||
|
||||
const auto invalidated = compile(true, false, true);
|
||||
require(!declared(invalidated, "buffers_coherent") && decorated(invalidated, "buffers", spv::DecorationCoherent), "coherent buffers: a program that invalidates the vector caches does not keep every buffer access coherent");
|
||||
require(only(invalidated.loads, "buffers") && only(invalidated.stores, "buffers"), "coherent buffers: a program that invalidates the vector caches does not use the coherent buffer array");
|
||||
}
|
||||
|
||||
void verifyUnnormalizedSamplers() {
|
||||
using namespace ShaderRecompiler;
|
||||
constexpr std::uint32_t Format8888UNorm = 56;
|
||||
|
|
@ -2853,6 +2949,7 @@ int main(int argc, char** argv) {
|
|||
verifyComputedTexelOffsets();
|
||||
verifyShaderClockScopes();
|
||||
verifyInt64AtomicCapabilities();
|
||||
verifyCoherentBufferAccesses();
|
||||
verifyUnnormalizedSamplers();
|
||||
verifyUnusedUnnormalizedSampler();
|
||||
verifyWaveUniformValues();
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ struct IrProgramMetadata {
|
|||
std::uint32_t pixelLinearInputs = 0;
|
||||
std::uint32_t pixelPerspectiveInputs = 0;
|
||||
bool barycentricEmulation = false;
|
||||
bool invalidatesCaches = false;
|
||||
std::vector<IrValue*> dynamicReads;
|
||||
bool shaderInfoComplete = false;
|
||||
IrBindingLayout bindings;
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ struct SpirvRequirements {
|
|||
bool sharedInt64Atomics = false;
|
||||
bool float64 = false;
|
||||
bool coherentBuffers = false;
|
||||
bool coherentBufferAlias = false;
|
||||
std::vector<std::uint32_t> capabilities;
|
||||
std::vector<std::string> extensions;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -103,6 +103,7 @@ struct SpirvEmitterState {
|
|||
bool tableIndexNonUniform = true;
|
||||
std::uint32_t storageBufferVariable = 0;
|
||||
std::uint32_t storageBufferU64Variable = 0;
|
||||
std::uint32_t storageBufferCoherentVariable = 0;
|
||||
std::vector<std::uint32_t> memoryByteOffsets;
|
||||
std::map<std::array<std::uint32_t, 6>, std::uint32_t> formattedBufferFunctions;
|
||||
std::map<std::array<std::uint32_t, 6>, std::uint32_t> formattedGpuBufferFunctions;
|
||||
|
|
|
|||
|
|
@ -444,6 +444,7 @@ SpirvRequirements AnalyzeProgramRequirements(const IrProgram& program) {
|
|||
requirements.functionLdsDwords = static_cast<std::uint32_t>(std::max<std::uint64_t>((needed + 63u) & ~63ull, 64u));
|
||||
}
|
||||
}
|
||||
requirements.coherentBufferAlias = requirements.coherentBuffers && !program.Metadata().invalidatesCaches;
|
||||
return requirements;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -387,13 +387,15 @@ void DefineDescriptors(SpirvEmitterState& state) {
|
|||
switch (binding.kind) {
|
||||
case DescriptorBindingKind::Buffers:
|
||||
state.storageBufferVariable = Define(ArrayType(StorageBufferBlockType(state)), "buffers");
|
||||
if (state.requirements.bufferInt64Atomics) {
|
||||
state.storageBufferU64Variable = Define(ArrayType(StorageBufferU64BlockType(state)), "buffers_u64");
|
||||
state.module.AddAnnotation(spv::OpDecorate, state.storageBufferVariable, spv::DecorationAliased);
|
||||
state.module.AddAnnotation(spv::OpDecorate, state.storageBufferU64Variable, spv::DecorationAliased);
|
||||
if (state.requirements.coherentBufferAlias) state.storageBufferCoherentVariable = Define(ArrayType(StorageBufferBlockType(state)), "buffers_coherent");
|
||||
if (state.requirements.bufferInt64Atomics) state.storageBufferU64Variable = Define(ArrayType(StorageBufferU64BlockType(state)), "buffers_u64");
|
||||
if (state.storageBufferU64Variable != 0u || state.storageBufferCoherentVariable != 0u) {
|
||||
for (const auto variable : {state.storageBufferVariable, state.storageBufferCoherentVariable, state.storageBufferU64Variable}) {
|
||||
if (variable != 0u) state.module.AddAnnotation(spv::OpDecorate, variable, spv::DecorationAliased);
|
||||
}
|
||||
}
|
||||
if (state.requirements.coherentBuffers) {
|
||||
state.module.AddAnnotation(spv::OpDecorate, state.storageBufferVariable, spv::DecorationCoherent);
|
||||
state.module.AddAnnotation(spv::OpDecorate, state.storageBufferCoherentVariable != 0u ? state.storageBufferCoherentVariable : state.storageBufferVariable, spv::DecorationCoherent);
|
||||
if (state.storageBufferU64Variable != 0u) state.module.AddAnnotation(spv::OpDecorate, state.storageBufferU64Variable, spv::DecorationCoherent);
|
||||
}
|
||||
break;
|
||||
|
|
|
|||
|
|
@ -120,10 +120,10 @@ RuntimeAddress address(SpirvValueEmitContext& context, const IrValue& instructio
|
|||
return {AddBdaAddress(context, instruction, resource.base, Unary(state, spv::OpUConvert, TypeScalarU64(state), byte), false), inBounds};
|
||||
}
|
||||
|
||||
MemoryResourceAccess directAccess(SpirvValueEmitContext& context, const IrValue& instruction, bool wide = false) {
|
||||
MemoryResourceAccess directAccess(SpirvValueEmitContext& context, const IrValue& instruction, bool wide = false, bool plainLoad = false) {
|
||||
auto& state = context.state;
|
||||
const auto& memory = context.Memory(instruction);
|
||||
const auto variable = wide ? state.storageBufferU64Variable : state.storageBufferVariable;
|
||||
const auto variable = wide ? state.storageBufferU64Variable : plainLoad || state.storageBufferCoherentVariable == 0u ? state.storageBufferVariable : state.storageBufferCoherentVariable;
|
||||
if (variable == 0u) context.Fail(instruction, "direct buffer descriptor array is missing");
|
||||
const auto element = ResourceForDescriptor(state, DescriptorBindingKind::Buffers, memory.resource);
|
||||
MemoryResourceAccess access;
|
||||
|
|
@ -153,7 +153,7 @@ std::uint32_t directWordIndex(SpirvEmitterState& state, const MemoryResourceAcce
|
|||
std::uint32_t readBuffer(SpirvValueEmitContext& context, const IrValue& instruction, std::uint32_t address, std::uint32_t bits) {
|
||||
if (context.Memory(instruction).gpuDescriptor) return EmitBdaRead(context, instruction, address, bits);
|
||||
auto& state = context.state;
|
||||
const auto access = directAccess(context, instruction);
|
||||
const auto access = directAccess(context, instruction, false, !context.Memory(instruction).coherent);
|
||||
const auto index = directWordIndex(state, access, address);
|
||||
const auto misalignment = baseMisalignment(context, instruction);
|
||||
const auto unaligned = nonzero(state, misalignment);
|
||||
|
|
|
|||
|
|
@ -29,13 +29,16 @@ bool TranslationContext::emitMemory(const RdnaInstruction& inst) {
|
|||
switch (inst.op) {
|
||||
case RdnaOpcode::SGl1Inv:
|
||||
case RdnaOpcode::SDcacheInv:
|
||||
case RdnaOpcode::BufferGl0Inv:
|
||||
case RdnaOpcode::BufferGl1Inv:
|
||||
program.Metadata().invalidatesCaches = true;
|
||||
emitControlNop();
|
||||
return true;
|
||||
case RdnaOpcode::SDcacheWb:
|
||||
case RdnaOpcode::SAtcProbe:
|
||||
case RdnaOpcode::SAtcProbeBuffer:
|
||||
case RdnaOpcode::SDcacheDiscard:
|
||||
case RdnaOpcode::SDcacheDiscardX2:
|
||||
case RdnaOpcode::BufferGl0Inv:
|
||||
case RdnaOpcode::BufferGl1Inv:
|
||||
emitControlNop();
|
||||
return true;
|
||||
case RdnaOpcode::SMemtime:
|
||||
|
|
|
|||
|
|
@ -540,6 +540,7 @@ Throughout the project, every function at every stage either **does exactly what
|
|||
- [FORCE_DEGAMMA](../../core/libs/prx/libSceAgcDriver/Graphics/src/Sampler.cpp) (libSceAgcDriver) - an S# that forces sRGB decoding adds nothing to an sRGB texture, as in shadPS4 (PS4). Measured on RDNA2 (gfx1035), not on the console: `8`, `8_8` and `8_8_8_8` sRGB textures sample bit for bit the same with and without the bit (point and bilinear; BC formats not measured). Other textures throw; on the same hardware their UNORM `8`, `8_8` and `8_8_8_8` forms sample bit for bit as the sRGB format (decoded before filtering).
|
||||
- [Tessellation factor ring](../../core/libs/prx/libSceAgcDriver/State/src/Configuration.cpp) (libSceAgcDriver) - `sceAgcDriverSetTFRing` records the ring and `sceAgcDriverGetTFRing` reports it. Before a title sets one, it reports the console's default, 0xFF0000000 and 128 KiB, as measured in [PS5_Vulkan's hardware findings](https://github.com/mihawk-99/PS5_Vulkan/blob/fde9e3798c2ec1d8fccc90d5067cd6060490607f/docs/HARDWARE_FINDINGS.md). Nothing is written to the recorded ring: tessellated draws run as Vulkan tessellation stages, which keep their factors on the host, so a title that reads tessellation factors from the ring finds none. A null base or a zero size throws; [ps5-opengl's AGC runtime](https://github.com/blackbearreloaded/ps5-opengl/blob/67c873f985eaefe0a72f713529e5632eaa01139c/src/platform/ps5_agc_native_runtime.c) calls `sceAgcDriverSetTFRing(0, 0)` in its cleanup, and what that does on the console is unknown.
|
||||
- [Raw `buffer_load_dwordx2`](../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp) is one 64-bit access on the hardware (with or without glc), so it never sees half of a `buffer_atomic_swap_x2`. Only a glc load that is 8-byte aligned and in bounds, in a program that has the 64-bit storage view (it uses 64-bit buffer atomics), is read with one 64-bit atomic load; every other dwordx2 load is two dword loads and can tear against a concurrent 64-bit write: loads without glc, and loads in a program without 64-bit buffer atomics while another shader writes the pair with `swap_x2`.
|
||||
- [Buffer loads without GLC or DLC](../../core/shader/recompiler/SpirvBackend/src/SpirvAnalysis.cpp) (shader recompiler) are not coherent in a program that has GLC or DLC buffer accesses and no `buffer_gl0_inv`, `buffer_gl1_inv`, `s_gl1_inv` or `s_dcache_inv`: only its GLC and DLC loads, stores and atomics use the `Coherent` buffer view, so a plain load can return a value cached before another work-group's write. This follows the [LLVM AMDGPU memory model for GFX10](https://llvm.org/docs/AMDGPUUsage.html#memory-model-gfx10-gfx11), in which a load-acquire is a GLC DLC load followed by `buffer_gl1_inv` and `buffer_gl0_inv`; it is not measured on the console or on RDNA hardware.
|
||||
- [DS src2 operations](../../core/shader/recompiler/Translation/src/MemoryInstructions/SharedMemoryInstructions.cpp) (`ds_*_src2_*`, GFX10.1 only) read `DS[B]`, with `B = A + 4 * sext(offset1[6:0]:offset0)`, and then update `DS[A]` with the matching 32-bit atomic, so the read of `B` is not atomic with the update of `A`. The form that takes the offset from the address (`offset1[7]` set), the 64-bit forms and GDS throw: whether the LDS address keeps `A[31:17]` in that form, and whether the 64-bit forms scale the offset by 4 or 8, is not documented. `ds_min_src2_f32`, `ds_max_src2_f32` and `ds_add_src2_f32` throw: on RDNA2 (gfx1035, the oracle's hardware, which has no src2 opcodes) `ds_min_f32`, `ds_max_f32` and `ds_add_f32` ignore `IEEE_MODE` but depend on whether f32 denormals are flushed (28, 28 and 12 of 196 special rows differ), and that is not modelled for LDS float atomics yet (#981).
|
||||
- [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
|
||||
- [The narrow printf family](../../core/libs/prx/libc/src/Formatting.cpp) (libc) converts 16-bit guest wide arguments to UTF-8 on both platforms, independently of the host locale. It does not use the guest's single-byte `wcrtomb`; the console's treatment of invalid UTF-16 and locale-specific encoding remains unverified. Linux retains host formatting for formats without wide arguments, so a narrow `%p` there still follows the host (glibc prints a null pointer as `(nil)`, where FreeBSD prints `0x0`); positional arguments, the `'` flag and `%m` throw when a format requires the guest formatter
|
||||
|
|
|
|||
Loading…
Reference in New Issue