feat(agc): carry spanned waves in the target and require full subgroups

APS5_SPANNED_WAVE is now read once by the driver, which sets
SpirvTarget::spannedWaves only on a device with 32-lane compute subgroup
control and computeFullSubgroups. The recompiler no longer reads the
environment. The field is part of the cache key and of the request
serialization (version 32), so a cached or replayed module keeps the mode
it was built in.

A spanned module numbers its lanes SubgroupId * 32 +
SubgroupLocalInvocationId, which needs two full 32-lane subgroups. The
artifact records this as fullSubgroups (disk cache and pipeline identity
included), and the driver creates those pipelines with
REQUIRE_FULL_SUBGROUPS. Compute pipeline captures accept the flag with a
required subgroup size.

The two-lane fallback now catches only SpannedWaveUnsupported, thrown for
a shuffle wider than 32 bits; other emit errors surface as before.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
Alex ColliniandClaude Opus 5.5 committed 2026-10-16 14:12:58 +02:00
1 parent adf516f6f1
commit 7a6cc71786
15 files changed
+94 -31

No files matched your search

@@ -58,6 +58,7 @@ public:
const std::uint32_t* WorkGroupCountLimit() const;
std::uint32_t VendorId() const;
ShaderRecompiler::SpirvTarget ComputeTarget(std::uint32_t waveSize) const;
bool ComputeFullSubgroups() const;
// Distinguishes this device from every earlier one in the process (a memo keyed by the device
// cannot rely on the pointer, which a replacement may reuse).
std::uint64_t Serial() const { return serial; }
@@ -329,6 +330,7 @@ private:
Graphics::Context graphicsContext() const;
VkPipelineCreateFlags computePipelineFlags() const;
std::uint32_t computeSubgroupSize(const ShaderRecompiler::RecompileResult& shader) const;
VkPipelineShaderStageCreateFlags computeStageFlags(const ShaderRecompiler::RecompileResult& shader) const;
std::shared_ptr<ComputePipelineObjects> createComputePipeline(const Graphics::Context& context, const ShaderRecompiler::RecompileResult& shader, VkDescriptorSetLayout setLayout, std::span<const std::uint32_t> layoutKey, VkShaderStageFlags pushStages, std::uint64_t programAddress);
ShaderRecompiler::SpirvTarget buildTarget() const;
Graphics::Context buildContext() const;
@@ -94,7 +94,7 @@ void ComputePipelineCapture::Validate() const {
RequireCapture(mainEntry, "compute main entry point missing");
RequireCapture(deviceName.size() < VK_MAX_PHYSICAL_DEVICE_NAME_SIZE && deviceName.find('\0') == std::string::npos, "invalid device name");
RequireCapture((flags & ~(VK_PIPELINE_CREATE_DISPATCH_BASE_BIT | VK_PIPELINE_CREATE_CAPTURE_STATISTICS_BIT_KHR)) == 0, "unsupported pipeline flags");
RequireCapture(stageFlags == 0, "unsupported shader stage flags");
RequireCapture(stageFlags == 0 || (stageFlags == VK_PIPELINE_SHADER_STAGE_CREATE_REQUIRE_FULL_SUBGROUPS_BIT && requiredSubgroupSize != 0), "unsupported shader stage flags");
RequireCapture(requiredSubgroupSize == 0 || (requiredSubgroupSize <= 128 && (requiredSubgroupSize & (requiredSubgroupSize - 1u)) == 0), "invalid required subgroup size");
RequireCapture(sets.size() <= 8, "too many descriptor sets");
std::size_t totalBindings = 0;
@@ -353,6 +353,8 @@ struct VulkanDevice::State {
bool computeWave32 = false;
bool meshWave32 = false;
std::uint32_t maxComputeSubgroupSize = 0;
bool computeFullSubgroups = false;
bool spannedWaves = false;
// Indirect draw features enabled (see Graphics::Context).
bool drawIndirectFirstInstance = false;
bool multiDrawIndirect = false;
@@ -821,6 +823,7 @@ ComputePipelineReplayResult VulkanDevice::ReplayComputePipeline(const ComputePip
VkPhysicalDeviceSubgroupSizeControlPropertiesEXT subgroup{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SUBGROUP_SIZE_CONTROL_PROPERTIES_EXT};
VkPhysicalDeviceProperties2 properties{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PROPERTIES_2, &subgroup};
state->InstanceFunction<PFN_vkGetPhysicalDeviceProperties2>("vkGetPhysicalDeviceProperties2")(state->physical, &properties);
require(capture.stageFlags == 0 || state->computeFullSubgroups, "captured full compute subgroups are unsupported");
require(state->computeWave32 && capture.requiredSubgroupSize >= subgroup.minSubgroupSize && capture.requiredSubgroupSize <= subgroup.maxSubgroupSize && (subgroup.requiredSubgroupSizeStages & VK_SHADER_STAGE_COMPUTE_BIT) != 0, "captured required subgroup size is unsupported");
}
const auto target = Target();
@@ -1747,10 +1750,12 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window, bool compileOnly) :
state->meshWave32 = state->meshShader && subgroupSizeFeatures.subgroupSizeControl == VK_TRUE && subgroupSize.minSubgroupSize <= 32u && subgroupSize.maxSubgroupSize >= 32u &&
(subgroupSize.requiredSubgroupSizeStages & VK_SHADER_STAGE_MESH_BIT_EXT) != 0;
state->maxComputeSubgroupSize = subgroupSize.maxSubgroupSize;
state->computeFullSubgroups = state->computeWave32 && subgroupSizeFeatures.computeFullSubgroups == VK_TRUE;
}
if (state->computeWave32 || state->meshWave32) {
subgroupSizeFeatures = {VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SUBGROUP_SIZE_CONTROL_FEATURES_EXT};
subgroupSizeFeatures.subgroupSizeControl = VK_TRUE;
subgroupSizeFeatures.computeFullSubgroups = state->computeFullSubgroups ? VK_TRUE : VK_FALSE;
deviceExtensions.push_back(VK_EXT_SUBGROUP_SIZE_CONTROL_EXTENSION_NAME);
deviceInfo.enabledExtensionCount = static_cast<std::uint32_t>(deviceExtensions.size());
deviceInfo.ppEnabledExtensionNames = deviceExtensions.data();
@@ -1758,6 +1763,10 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window, bool compileOnly) :
byteFeatures.pNext = &subgroupSizeFeatures;
APS5_LOG_OUT("Wave32 programs run on subgroups of %u: compute %d, mesh %d", 32u, state->computeWave32, state->meshWave32);
}
if (std::getenv("APS5_SPANNED_WAVE") != nullptr) {
state->spannedWaves = state->computeFullSubgroups;
APS5_LOG_OUT("APS5_SPANNED_WAVE: single-wave wave64 compute spans two full 32-lane subgroups: %d", state->spannedWaves);
}
timelineFeatures = {VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_TIMELINE_SEMAPHORE_FEATURES_KHR};
timelineFeatures.timelineSemaphore = VK_TRUE;
if (state->timelineSemaphores) {
@@ -1988,6 +1997,16 @@ std::uint32_t VulkanDevice::computeSubgroupSize(const ShaderRecompiler::Recompil
return state->computeWave32 && (shader.hostSubgroupSize == 32u || shader.hostSubgroupSize > state->maxComputeSubgroupSize) ? std::min(shader.hostSubgroupSize, state->maxComputeSubgroupSize) : 0u;
}
VkPipelineShaderStageCreateFlags VulkanDevice::computeStageFlags(const ShaderRecompiler::RecompileResult& shader) const {
if (!shader.fullSubgroups) return 0;
require(state->computeFullSubgroups && computeSubgroupSize(shader) == 32u, "a spanned wave64 program needs full 32-lane compute subgroups");
return VK_PIPELINE_SHADER_STAGE_CREATE_REQUIRE_FULL_SUBGROUPS_BIT;
}
bool VulkanDevice::ComputeFullSubgroups() const {
return state->computeFullSubgroups;
}
void VulkanDevice::WaitForPipelineJournalReplay() {
startJournalReplay();
if (state->journalReplay != nullptr) state->journalReplay->Wait();
@@ -3456,6 +3475,7 @@ ShaderRecompiler::SpirvTarget VulkanDevice::buildTarget() const {
target.narrowSubgroupClock = state->narrowSubgroupClock;
target.float64DenormPreserve = state->float64DenormPreserve;
target.float16DenormPreserve = state->float16DenormPreserve;
target.spannedWaves = state->spannedWaves;
target.fixedPushSlots = state->graphicsPipelineLibrary;
target.registrationModules = state->graphicsPipelineLibrary;
target.srgbDecodeFormats = state->srgbDecodeFormats;
@@ -4538,6 +4558,7 @@ std::shared_ptr<ComputePipelineObjects> VulkanDevice::createComputePipeline(cons
const VkPipelineCreateFlags pipelineFlags = computePipelineFlags();
const Graphics::PipelineSpecialization specialization(shader);
const auto subgroupSize = computeSubgroupSize(shader);
const auto stageFlags = computeStageFlags(shader);
if (const char* directory = std::getenv("APS5_DUMP_COMPUTE_PIPELINES"); directory != nullptr && *directory != '\0' && CaptureComputePipeline(shader.spirv.size())) {
ComputePipelineCapture capture;
capture.spirv = shader.spirv.Copy();
@@ -4553,6 +4574,7 @@ std::shared_ptr<ComputePipelineObjects> VulkanDevice::createComputePipeline(cons
if (pushStages != 0) capture.pushConstants.push_back(push);
for (const auto& value : shader.specialization) capture.specialization.push_back({value.id, value.value});
capture.requiredSubgroupSize = subgroupSize;
capture.stageFlags = stageFlags;
capture.flags = pipelineFlags;
capture.deviceName = state->properties.deviceName;
capture.vendorId = state->properties.vendorID;
@@ -4564,7 +4586,7 @@ std::shared_ptr<ComputePipelineObjects> VulkanDevice::createComputePipeline(cons
std::fprintf(stderr, "[compute-capture] %s\n", path.string().c_str());
}
if (profile && shader.spirv.size() > 100000) std::fprintf(stderr, "[dispatch] creating a pipeline for %zu SPIR-V words (program 0x%llx)\n", shader.spirv.size(), static_cast<unsigned long long>(programAddress));
CreateComputePipeline(state->device, state->DeviceFunction<PFN_vkCreateComputePipelines>("vkCreateComputePipelines"), context.pipelineCache, objects->module, objects->layout, pipelineFlags, 0, specialization.Info(), subgroupSize, &objects->pipeline, context.pipelineCacheSynchronization);
CreateComputePipeline(state->device, state->DeviceFunction<PFN_vkCreateComputePipelines>("vkCreateComputePipelines"), context.pipelineCache, objects->module, objects->layout, pipelineFlags, stageFlags, specialization.Info(), subgroupSize, &objects->pipeline, context.pipelineCacheSynchronization);
objects->destroyModule(objects->device, objects->module, nullptr);
objects->module = VK_NULL_HANDLE;
computePipelinesCreated.fetch_add(1, std::memory_order_relaxed);
@@ -62,6 +62,10 @@ void CheckRoundTrip() {
changed = decoded;
changed.requiredSubgroupSize = 64;
Require(changed.Identity() != capture.Identity(), "subgroup size did not affect capture identity");
changed = decoded;
changed.stageFlags = VK_PIPELINE_SHADER_STAGE_CREATE_REQUIRE_FULL_SUBGROUPS_BIT;
Require(AgcDriver::ComputePipelineCapture::Decode(changed.Encode()).stageFlags == changed.stageFlags, "full subgroups were lost in the capture");
Require(changed.Identity() != capture.Identity(), "full subgroups did not affect capture identity");
Directory directory;
const auto file = capture.Write(directory.path);
Require(AgcDriver::ComputePipelineCapture::Read(file).Encode() == encoded, "capture file changed pipeline inputs");
@@ -144,7 +148,11 @@ void CheckMalformedRecords() {
invalid([](auto& copy) { copy.spirv[11] = 4; });
invalid([](auto& copy) { copy.spirv[13] = 0; });
invalid([](auto& copy) { copy.flags = VK_PIPELINE_CREATE_DERIVATIVE_BIT; });
invalid([](auto& copy) { copy.stageFlags = VK_PIPELINE_SHADER_STAGE_CREATE_REQUIRE_FULL_SUBGROUPS_BIT; });
invalid([](auto& copy) { copy.stageFlags = VK_PIPELINE_SHADER_STAGE_CREATE_ALLOW_VARYING_SUBGROUP_SIZE_BIT; });
invalid([](auto& copy) {
copy.stageFlags = VK_PIPELINE_SHADER_STAGE_CREATE_REQUIRE_FULL_SUBGROUPS_BIT;
copy.requiredSubgroupSize = 0;
});
invalid([](auto& copy) { copy.requiredSubgroupSize = 3; });
invalid([](auto& copy) { copy.specialization.push_back(copy.specialization[0]); });
invalid([](auto& copy) { copy.pushConstants = {{VK_SHADER_STAGE_VERTEX_BIT, 0, 4}}; });
@@ -75,7 +75,7 @@ void requireSameArtifact(const CompiledShaderArtifact& left, const CompiledShade
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.hostSubgroupSize == right.hostSubgroupSize && left.fullSubgroups == right.fullSubgroups, 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");
@@ -170,6 +170,7 @@ RecompileResult sampleResult() {
result.bdaAbiVersion = 3;
result.memoryOffsetDword = 7;
result.hostSubgroupSize = 32;
result.fullSubgroups = true;
result.workgroupMemoryDwords = 32769u;
result.vertexInputs = {{1, 4, 2}, {5, 2, 0}};
result.vertexInputPatches = {{1, 20, {7, 8, 9}}, {5, 40, {10, 11, 12}}};
@@ -620,6 +621,7 @@ void verifyKeySensitivity() {
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("spanned waves", [](SampleRequest& sample) { sample.request.target.spannedWaves = 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; });
@@ -3067,7 +3067,7 @@ void verifyPixelRequestSerialization() {
minimal.context.pixel->layerExported = true;
minimal.context.pixel->ditheredAlphaToCoverage = true;
const auto encoded = serializer.Serialize(minimal);
require(requestPrefix(encoded, 8u) == "NVNQQR8AAAA=", "new requests did not use serialization version 31");
require(requestPrefix(encoded, 8u) == "NVNQQSAAAAA=", "new requests did not use serialization version 32");
const std::array<std::uint32_t, 2> fallbacks{0x40u, 0x1ac4u};
auto withFallbacks = minimal;
withFallbacks.context.linkedCallFallbacks = fallbacks;
@@ -3185,7 +3185,7 @@ void verifyPixelRequestSerialization() {
for (std::size_t bytes = 0; bytes < 45u; ++bytes) {
expectFailure([&] { static_cast<void>(serializer.Deserialize(requestPrefix(encoded, mappingOffset + bytes))); }, "truncated data", "a truncated pixel mapping, packing, dual-source, alpha-to-coverage, stencil reference export flag, primitive class, layer export, dither or front-face flag, quad pixel mask or quad sample mask was accepted");
}
for (const auto unsupported : {"NVNQQQAAAAA=", "NVNQQSAAAAA="}) {
for (const auto unsupported : {"NVNQQQAAAAA=", "NVNQQSEAAAA="}) {
expectFailure([&] { static_cast<void>(serializer.Deserialize(unsupported)); }, "serialization version", "an unsupported request version was accepted");
}
static constexpr std::string_view version18Request{
@@ -3276,6 +3276,7 @@ void verifyVertexInfoVersion19() {
require(offset + 4u <= plain.size() && flagged[offset] == 0xefu && flagged[offset + 1u] == 0xcdu && flagged[offset + 2u] == 0xabu && flagged[offset + 3u] == 0x89u, "PA_CL_VS_OUT_CNTL is not the four bytes the version 20 vertex info adds");
require(std::equal(plain.begin() + offset + 4u, plain.end(), flagged.begin() + offset + 4u), "PA_CL_VS_OUT_CNTL moved the fields after it");
auto version20Vertex = flagged;
version20Vertex.pop_back();
version20Vertex[4] = 20u;
const auto replay20Vertex = serializer.Deserialize(encodeBase64(version20Vertex));
require(replay20Vertex.request.context.vertex.has_value() && replay20Vertex.request.context.vertex->paClVsOutCntl == 0x89abcdefu, "a main version20 request lost PA_CL_VS_OUT_CNTL");
@@ -3284,6 +3285,7 @@ void verifyVertexInfoVersion19() {
const auto layered = serializer.Deserialize(serializer.Serialize(vertex));
require(layered.request.context.vertex.has_value() && layered.request.context.vertex->paClVsOutCntl == vertex.context.vertex->paClVsOutCntl, "a layered vertex info lost its clip distances or render target index");
auto legacy = plain;
legacy.pop_back();
legacy.erase(legacy.begin() + offset, legacy.begin() + offset + 4u);
for (const auto version : {16u, 17u, 18u, 19u}) {
legacy[4] = static_cast<std::uint8_t>(version);
@@ -3331,21 +3333,25 @@ void verifyVertexInfoVersion19() {
const auto sampledBytes = decodeBase64(serializer.Serialize(sampledPixel));
require(std::equal(maskedBytes.begin(), maskedBytes.begin() + static_cast<std::ptrdiff_t>(maskOffset + 1u), sampledBytes.begin()) && sampledBytes[maskOffset + 1u] == 0x1u && sampledBytes[maskOffset + 5u] == 0x2u && sampledBytes[maskOffset + 9u] == 0x4u && sampledBytes[maskOffset + 13u] == 0x8u, "the quad sample masks are not the four words after the quad pixel mask");
auto version29 = sampledBytes;
version29.pop_back();
version29.erase(version29.begin() + static_cast<std::ptrdiff_t>(maskOffset + 1u + sampleMaskBytes), version29.begin() + static_cast<std::ptrdiff_t>(maskOffset + 1u + sampleMaskBytes + 4u));
version29[4] = 29u;
const auto replay29 = serializer.Deserialize(encodeBase64(version29));
require(replay29.request.context.pixel.has_value() && replay29.request.context.pixel->quadSampleMask == std::array<std::uint16_t, 4>{0x1u, 0x2u, 0x4u, 0x8u} && replay29.request.context.pixel->reversedBlend == 0u && decodeBase64(serializer.Serialize(replay29.request)) == sampledBytes, "a version 29 request read a reversed-order blend code or misread the fields after the quad sample masks");
auto version28 = sampledBytes;
version28.pop_back();
version28.erase(version28.begin() + static_cast<std::ptrdiff_t>(maskOffset + 1u), version28.begin() + static_cast<std::ptrdiff_t>(maskOffset + 1u + sampleMaskBytes + 4u));
version28[4] = 28u;
const auto replay28 = serializer.Deserialize(encodeBase64(version28));
require(replay28.request.context.pixel.has_value() && replay28.request.context.pixel->quadPixelMask == 0x2u && replay28.request.context.pixel->quadSampleMask == std::array<std::uint16_t, 4>{0xffffu, 0xffffu, 0xffffu, 0xffffu} && replay28.request.context.pixel->reversedBlend == 0u && decodeBase64(serializer.Serialize(replay28.request)) == maskedBytes, "a version 28 request read quad sample masks or a reversed-order blend code, or misread the fields after them");
auto version27 = maskedBytes;
version27.pop_back();
version27.erase(version27.begin() + static_cast<std::ptrdiff_t>(maskOffset), version27.begin() + static_cast<std::ptrdiff_t>(maskOffset + 1u + sampleMaskBytes + 4u));
version27[4] = 27u;
const auto replay27 = serializer.Deserialize(encodeBase64(version27));
require(replay27.request.context.pixel.has_value() && replay27.request.context.pixel->quadPixelMask == 0xfu && replay27.request.context.pixel->reversedBlend == 0u && serializer.Serialize(replay27.request) == encodedPixel, "a version 27 request read a quad pixel mask or reversed-order blend code, or misread the fields after it");
auto legacyPixel = plainPixel;
legacyPixel.pop_back();
legacyPixel.erase(legacyPixel.begin() + static_cast<std::ptrdiff_t>(allBitsOffset), legacyPixel.begin() + static_cast<std::ptrdiff_t>(allBitsOffset + 2u + sampleMaskBytes + 4u));
legacyPixel[4] = 26u;
const auto replay26 = serializer.Deserialize(encodeBase64(legacyPixel));
+1
View File
@@ -261,6 +261,7 @@ private:
append(key, value.registrationWordLimit);
append(key, value.vertexSubgroups);
append(key, value.float16DenormPreserve);
append(key, value.spannedWaves);
}
};
@@ -778,7 +778,7 @@ std::string RequestSerializer::Serialize(const RecompileRequest& request) const
std::string buffer;
Writer writer(buffer);
writer.WriteU32(0x41505335u);
writer.WriteU32(31u);
writer.WriteU32(32u);
writeShaderBinary(writer, request.shader);
writeGuestContext(writer, request.context);
writeSpirvTarget(writer, request.target);
@@ -803,6 +803,7 @@ std::string RequestSerializer::Serialize(const RecompileRequest& request) const
writer.WriteBool(request.target.vertexSubgroups);
writer.WriteBool(request.target.float16DenormPreserve);
writer.WriteU32Span(request.context.linkedCallFallbacks);
writer.WriteBool(request.target.spannedWaves);
return base64Encode(buffer);
}
@@ -811,7 +812,7 @@ DeserializedRequest RequestSerializer::Deserialize(std::string_view text) const
Reader reader(decoded);
if (reader.ReadU32() != 0x41505335u) throw std::runtime_error("invalid recompile request signature");
const auto version = reader.ReadU32();
if (version < 1u || version > 31u) throw std::runtime_error("unsupported recompile request serialization version");
if (version < 1u || version > 32u) throw std::runtime_error("unsupported recompile request serialization version");
DeserializedRequest result{};
result.request.shader = readShaderBinary(reader, result.shaderCode, result.shaderHeader);
result.request.context = readGuestContext(reader, result, version);
@@ -847,6 +848,7 @@ DeserializedRequest RequestSerializer::Deserialize(std::string_view text) const
result.linkedCallFallbacks = reader.ReadU32Vector();
result.request.context.linkedCallFallbacks = result.linkedCallFallbacks;
}
if (version >= 32u) result.request.target.spannedWaves = reader.ReadBool();
if (version >= 21u && reader.Remaining() != 0u) throw std::runtime_error("RequestSerializer: unexpected trailing data");
return result;
}
+6 -5
View File
@@ -524,16 +524,15 @@ CompiledVariant emitVariant(const RecompileRequest& request, IrProgram& program)
targetOptions.registrationModules = registration;
targetOptions.fragmentDescriptorSet = request.target.registrationModules;
targetOptions.float16DenormPreserve = request.target.float16DenormPreserve;
static const bool spannedWaves = std::getenv("APS5_SPANNED_WAVE") != nullptr;
targetOptions.spannedWaves = spannedWaves;
targetOptions.spannedWaves = request.target.spannedWaves;
constexpr SpirvEmitter spirvEmitter;
CompiledShaderArtifact result;
result.variantId = nextVariantId();
SpirvEmitInfo emitted;
try {
result.spirv = spirvEmitter.Emit(program, inputInfo, bindings, targetOptions);
} catch (const std::exception& error) {
if (!targetOptions.spannedWaves) throw;
result.spirv = spirvEmitter.Emit(program, inputInfo, bindings, targetOptions, &emitted);
} catch (const SpannedWaveUnsupported& error) {
std::fprintf(stderr, "[spanned-wave] 0x%llx falls back to two lanes per invocation: %s\n", static_cast<unsigned long long>(request.shader.codeAddress), error.what());
targetOptions.spannedWaves = false;
result.spirv = spirvEmitter.Emit(program, inputInfo, bindings, targetOptions);
@@ -558,6 +557,7 @@ CompiledVariant emitVariant(const RecompileRequest& request, IrProgram& program)
result.imageMetadataDword = bindings.layout.ImageMetadataDword();
result.runtimeImageCount = bindings.layout.runtimeImageCount;
result.hostSubgroupSize = HostSubgroupSize(request);
result.fullSubgroups = emitted.fullSubgroups;
result.vertexOffsetSgpr = program.Info().vertexOffsetSgpr;
result.instanceOffsetSgpr = program.Info().instanceOffsetSgpr;
result.vertexOffsetShared = program.Info().vertexOffsetShared;
@@ -874,6 +874,7 @@ RecompileResult materializeResult(const CompiledVariant& variant, const Recompil
.vertexOffsetConflict = artifact.vertexOffsetConflict,
.instanceOffsetConflict = artifact.instanceOffsetConflict,
.hostSubgroupSize = artifact.hostSubgroupSize,
.fullSubgroups = artifact.fullSubgroups,
.parameterExports = artifact.parameterExports,
.fragmentParameters = artifact.fragmentParameters,
.barycentricEmulation = artifact.barycentricEmulation,
+3 -1
View File
@@ -267,6 +267,7 @@ struct SpirvTarget {
std::uint32_t registrationWordLimit = 200000u;
bool vertexSubgroups = true;
bool float16DenormPreserve = false;
bool spannedWaves = false;
};
struct BindingLayout {
@@ -603,6 +604,7 @@ struct CompiledShaderArtifact {
bool vertexOffsetConflict = false;
bool instanceOffsetConflict = false;
std::uint32_t hostSubgroupSize = 0;
bool fullSubgroups = false;
std::vector<std::uint32_t> parameterExports;
std::vector<FragmentParameter> fragmentParameters;
BarycentricEmulation barycentricEmulation;
@@ -657,7 +659,7 @@ struct RecompileResult : CompiledShaderArtifact, ShaderInvocation {
};
mix(specialization.size());
for (const auto& constant : specialization) mix(constant.id | (static_cast<std::uint64_t>(constant.value) << 32u));
mix(hostSubgroupSize | (static_cast<std::uint64_t>(workgroupMemoryDwords) << 32u));
mix(hostSubgroupSize | (fullSubgroups ? 0x10000u : 0u) | (static_cast<std::uint64_t>(workgroupMemoryDwords) << 32u));
mix(bindings.size());
for (const auto& binding : bindings) mix(binding.descriptorSet | (static_cast<std::uint64_t>(binding.binding) << 16u) | (static_cast<std::uint64_t>(binding.kind) << 40u) | (static_cast<std::uint64_t>(binding.count) << 48u));
if (identity.Empty()) identity.low = 1;
+4 -2
View File
@@ -84,9 +84,9 @@ std::filesystem::path ShaderCacheDirectory() {
namespace ShaderRecompiler::ShaderDiskCache {
#if defined(__linux__) && defined(__x86_64__) && defined(__GLIBCXX__)
static_assert(sizeof(CompiledShaderArtifact) == 232, "CompiledShaderArtifact changed: update the artifact encoder");
static_assert(sizeof(CompiledShaderArtifact) == 240, "CompiledShaderArtifact changed: update the artifact encoder");
static_assert(sizeof(ShaderInvocation) == 200, "ShaderInvocation changed: update the invocation encoder");
static_assert(sizeof(RecompileResult) == 464, "RecompileResult changed: update EncodeResult and DecodeResult");
static_assert(sizeof(RecompileResult) == 472, "RecompileResult changed: update EncodeResult and DecodeResult");
static_assert(sizeof(DescriptorBinding) == 592, "DescriptorBinding changed: update the binding encoder");
static_assert(sizeof(VertexAttribute) == 32, "VertexAttribute changed: update the attribute encoder");
static_assert(sizeof(VertexInput) == 16, "VertexInput changed: update the vertex input encoder");
@@ -486,6 +486,7 @@ void encodeArtifact(Writer& writer, const CompiledShaderArtifact& result) {
writer.Value(result.vertexOffsetConflict);
writer.Value(result.instanceOffsetConflict);
writer.Value(result.hostSubgroupSize);
writer.Value(result.fullSubgroups);
writer.Values(std::span<const std::uint32_t>(result.parameterExports));
writer.List(result.fragmentParameters, [](Writer& out, const FragmentParameter& parameter) {
out.Value(parameter.location);
@@ -532,6 +533,7 @@ void decodeArtifact(Reader& reader, CompiledShaderArtifact& result) {
reader.Value(result.vertexOffsetConflict);
reader.Value(result.instanceOffsetConflict);
reader.Value(result.hostSubgroupSize);
reader.Value(result.fullSubgroups);
reader.Values(result.parameterExports);
reader.List(result.fragmentParameters, 10, [](Reader& in, FragmentParameter& parameter) {
in.Value(parameter.location);
@@ -5,6 +5,7 @@
#include "Optimization/BindingAllocator.hpp"
#include "Optimization/ShaderStageInputInfo.hpp"
#include <cstdint>
#include <stdexcept>
#include <vector>
#include <span>
#include <string_view>
@@ -27,10 +28,18 @@ struct SpirvTargetOptions {
bool spannedWaves = false;
};
struct SpirvEmitInfo {
bool fullSubgroups = false;
};
struct SpannedWaveUnsupported : std::runtime_error {
using std::runtime_error::runtime_error;
};
class SpirvEmitter {
public:
[[nodiscard]] std::vector<std::uint32_t> Emit(const IrProgram& program, const CompiledBindingLayout& bindings, const SpirvTargetOptions& target) const;
[[nodiscard]] std::vector<std::uint32_t> Emit(const IrProgram& program, const ShaderStageInputInfo& inputInfo, const CompiledBindingLayout& bindings, const SpirvTargetOptions& target) const;
[[nodiscard]] std::vector<std::uint32_t> Emit(const IrProgram& program, const ShaderStageInputInfo& inputInfo, const CompiledBindingLayout& bindings, const SpirvTargetOptions& target, SpirvEmitInfo* info = nullptr) const;
};
@@ -240,7 +240,7 @@ std::vector<std::uint32_t> SpirvEmitter::Emit(const IrProgram& program, const Co
return Emit(program, ShaderStageInputInfo {}, bindings, target);
}
std::vector<std::uint32_t> SpirvEmitter::Emit(const IrProgram& program, const ShaderStageInputInfo& inputInfo, const CompiledBindingLayout& bindings, const SpirvTargetOptions& target) const {
std::vector<std::uint32_t> SpirvEmitter::Emit(const IrProgram& program, const ShaderStageInputInfo& inputInfo, const CompiledBindingLayout& bindings, const SpirvTargetOptions& target, SpirvEmitInfo* info) const {
if (program.Resources().stage != IrShaderStage::Compute && program.Resources().stage != IrShaderStage::Vertex && program.Resources().stage != IrShaderStage::Pixel && program.Resources().stage != IrShaderStage::Mesh && program.Resources().stage != IrShaderStage::Local && program.Resources().stage != IrShaderStage::TessellationControl && program.Resources().stage != IrShaderStage::TessellationEvaluation) {
FailProgram(program, "binary SPIR-V emitter received an unsupported shader stage");
}
@@ -293,6 +293,7 @@ std::vector<std::uint32_t> SpirvEmitter::Emit(const IrProgram& program, const Sh
EmitModuleHeader(state, bindings);
EmitProgram(state);
state.module.EmitEntryPoint(ExecutionModelForStage(state.program.Resources().stage), state.mainFunc, "main", state.interfaceVariables);
if (info != nullptr) info->fullSubgroups = state.spannedWave;
return state.module.Finalize();
}
@@ -66,7 +66,7 @@ namespace ShaderRecompiler
void SpannedShuffle(SpirvEmitterState& state, std::uint32_t type, std::uint32_t result, std::uint32_t value, std::uint32_t lane) {
const bool boolean = type == TypeBool(state);
if (!boolean && type != TypeU32(state) && type != TypeF32(state)) FailEmit("a spanned wave shuffles only 32-bit and boolean values");
if (!boolean && type != TypeU32(state) && type != TypeF32(state)) throw SpannedWaveUnsupported("a spanned wave shuffles only 32-bit and boolean values");
std::uint32_t word = value;
if (boolean) word = Select(state, TypeU32(state), value, ConstantU32(state, 1u), ConstantU32(state, 0u));
else if (type != TypeU32(state)) word = Unary(state, spv::OpBitcast, TypeU32(state), value);
@@ -206,11 +206,11 @@ int main() {
Require(request.graphics->draw.indexAddress == 0xabcdef3400u && request.graphics->draw.indexCount == 57u && request.graphics->draw.indexElementBytes == 4u && request.graphics->draw.instanceCount == 9u);
Require(request.target.float16DenormPreserve == preserve);
auto upgraded = bytes;
upgraded[4] = 31u;
upgraded[4] = 32u;
if (fixture.pixel) upgraded.insert(upgraded.begin() + 258u, {0u, 0u, 0u});
if (fixture.pixel) InsertPixelDefaultsAfterFrontFace(upgraded);
if (fixture.tessellation) upgraded.insert(upgraded.end() - 35u, 5u, 0u);
upgraded.insert(upgraded.end(), sizeof(std::uint64_t), 0u);
upgraded.insert(upgraded.end(), sizeof(std::uint64_t) + 1u, 0u);
Require(serializer.Serialize(request) == EncodeBase64(upgraded));
auto truncated = bytes;
truncated.pop_back();
@@ -245,11 +245,11 @@ int main() {
layerRejected = std::string_view(error.what()) == "RequestSerializer: truncated data";
}
Require(layerRejected);
layered[4] = 31u;
layered[4] = 32u;
layered.insert(layered.begin() + 259u, {0u, 0u});
InsertPixelDefaultsAfterFrontFace(layered);
if (fixture.tessellation) layered.insert(layered.end() - 35u, 5u, 0u);
layered.insert(layered.end(), sizeof(std::uint64_t), 0u);
layered.insert(layered.end(), sizeof(std::uint64_t) + 1u, 0u);
Require(serializer.Serialize(layerReplay.request) == EncodeBase64(layered));
}
}
@@ -270,10 +270,10 @@ int main() {
Require(replay.request.context.pixel->rasterPrimitive == ShaderRecompiler::RasterPrimitive::Rectangle);
Require(replay.request.context.vertex->paClVsOutCntl == 0x00050a05u);
Require(replay.request.target.float16DenormPreserve == preserve);
bytes[4] = 31u;
bytes[4] = 32u;
bytes.insert(bytes.begin() + 259u, {0u, 0u});
InsertPixelDefaultsAfterFrontFace(bytes);
bytes.insert(bytes.end(), sizeof(std::uint64_t), 0u);
bytes.insert(bytes.end(), sizeof(std::uint64_t) + 1u, 0u);
Require(serializer.Serialize(replay.request) == EncodeBase64(bytes));
auto truncated23 = DecodeBase64(encoded23);
@@ -394,10 +394,10 @@ int main() {
Require(request.graphics->draw.indexAddress == 0xabcdef3400u && request.graphics->draw.indexCount == 57u && request.graphics->draw.indexElementBytes == 4u && request.graphics->draw.instanceCount == 9u);
Require(request.target.float16DenormPreserve == preserve);
auto upgraded = bytes;
upgraded[4] = 31u;
upgraded[4] = 32u;
if (fixture.pixel) upgraded.insert(upgraded.begin() + 259u, {0u, 0u});
if (fixture.pixel) InsertPixelDefaultsAfterFrontFace(upgraded);
upgraded.insert(upgraded.end(), sizeof(std::uint64_t), 0u);
upgraded.insert(upgraded.end(), sizeof(std::uint64_t) + 1u, 0u);
Require(serializer.Serialize(request) == EncodeBase64(upgraded));
for (const bool dither : {false, true}) {
if (!fixture.pixel && dither) continue;
@@ -407,12 +407,12 @@ int main() {
const auto replay25 = serializer.Deserialize(EncodeBase64(main25));
Require(!fixture.pixel || (replay25.request.context.pixel->layerExported == layer && replay25.request.context.pixel->ditheredAlphaToCoverage == dither && replay25.request.context.pixel->reversedBlend == 0u));
Require(replay25.request.target.float16DenormPreserve == preserve && (!fixture.tessellation || replay25.request.graphics->tessellation->primitiveGroupSize == 37u));
main25[4] = 31u;
main25[4] = 32u;
if (fixture.pixel) {
main25.insert(main25.begin() + 260u, 0u);
InsertPixelDefaultsAfterFrontFace(main25);
}
main25.insert(main25.end(), sizeof(std::uint64_t), 0u);
main25.insert(main25.end(), sizeof(std::uint64_t) + 1u, 0u);
Require(serializer.Serialize(replay25.request) == EncodeBase64(main25));
}
}
@@ -426,9 +426,9 @@ int main() {
const auto replay = serializer.Deserialize(encoded);
Require(replay.request.target.float16DenormPreserve == preserve);
auto bytes = DecodeBase64(encoded);
Require(bytes.size() >= 8u && bytes[4] == 31u && bytes[5] == 0u && bytes[6] == 0u && bytes[7] == 0u);
Require(bytes.size() >= 8u && bytes[4] == 32u && bytes[5] == 0u && bytes[6] == 0u && bytes[7] == 0u);
auto future = bytes;
future[4] = 32u;
future[4] = 33u;
bool futureRejected = false;
try {
static_cast<void>(serializer.Deserialize(EncodeBase64(future)));
@@ -446,5 +446,10 @@ int main() {
Require(rejected);
}
for (const bool spanned : {false, true}) {
request.target.spannedWaves = spanned;
Require(serializer.Deserialize(serializer.Serialize(request)).request.target.spannedWaves == spanned);
}
return 0;
}