shader: emit dynamic sample offsets with the enabled maintenance8 feature

This commit is contained in:
Ali Almohaya 2026-10-12 19:11:00 +02:00 committed by nmzik
parent 652154aead
commit 460a196fca
11 changed files with 135 additions and 26 deletions

View File

@ -33,7 +33,7 @@ struct GraphicContext {
// bool fp64_denorm_preserve = false; // Temporarily disabled.
bool attachment_feedback_loop_enabled = false;
bool provoking_vertex_last_enabled = false;
bool depth_color_copy_enabled = false;
bool maintenance8_enabled = false;
bool supports_block_texel_view = false;
bool mesh_shader_enabled = false;
vk::PhysicalDeviceMeshShaderPropertiesEXT mesh_shader_properties = {};

View File

@ -148,7 +148,7 @@ void DumpShaderOriginal(const char* stage_name, uint64_t shader_hash,
}
bool ValidateShaderSpirv(const char* label, uint64_t shader_hash,
const std::vector<uint32_t>& spirv) {
const std::vector<uint32_t>& spirv, bool maintenance8_enabled) {
if (!Config::ShaderValidationEnabled()) {
return true;
}
@ -160,7 +160,9 @@ bool ValidateShaderSpirv(const char* label, uint64_t shader_hash,
static_cast<int>(position.column), static_cast<int>(position.index),
message);
});
if (tools.Validate(spirv)) {
spvtools::ValidatorOptions validator_options;
validator_options.SetAllowOffsetTextureOperand(maintenance8_enabled);
if (tools.Validate(spirv.data(), spirv.size(), validator_options)) {
return true;
}
spvtools::SpirvTools disassembler(SPV_ENV_VULKAN_1_2);
@ -296,14 +298,15 @@ struct PipelineCache::ProgramCache {
std::span<const uint32_t> function_code) {
auto result = ShaderRecompiler::CompileProgram(std::move(translated), options,
specialization, push_data_start_dword);
if (!ValidateShaderSpirv(options.dump_label, options.shader_hash, result.spirv)) {
if (!ValidateShaderSpirv(options.dump_label, options.shader_hash, result.spirv,
options.maintenance8_enabled)) {
DumpShaderSpirv(stage_name, options.shader_hash, result.spirv);
EXIT("%s failed hash=0x%016" PRIx64 ": SPIR-V validation failed\n", options.dump_label,
options.shader_hash);
}
DumpShaderSpirv(stage_name, options.shader_hash, result.spirv);
const auto module = CompileSPV(result.spirv, device);
const auto module = CompileSPV(result.spirv, graphics.device);
EXIT_IF(module == nullptr);
if (options.dump_ir) {
LOGF("%s SPIR-V words=%" PRIu64 " wave_size=%u\n", options.dump_label,
@ -384,6 +387,7 @@ struct PipelineCache::ProgramCache {
}
ShaderRecompiler::CompileOptions options;
options.stage = stage;
options.maintenance8_enabled = graphics.maintenance8_enabled;
options.shader_hash = params.hash;
options.user_data = user_data;
options.back_code = params.back_code;
@ -456,26 +460,26 @@ struct PipelineCache::ProgramCache {
return permutation.handle;
}
explicit ProgramCache(vk::Device device): device(device) {
explicit ProgramCache(GraphicContext& graphics): graphics(graphics) {
lookup_key.static_state.reserve(MaxStaticKeyWords);
}
~ProgramCache() {
for (const auto& [key, entry]: programs) {
(void)key;
for (const auto& permutation: entry.permutations) {
device.destroyShaderModule(permutation.handle.module, nullptr);
graphics.device.destroyShaderModule(permutation.handle.module, nullptr);
}
}
}
std::unordered_map<ProgramKey, SourceEntry, ProgramKeyHash> programs;
ProgramKey lookup_key;
vk::Device device;
GraphicContext& graphics;
uint64_t next_shader_id = 0;
};
PipelineCache::PipelineCache(GraphicContext& graphics)
: m_graphics(graphics), m_program_cache(std::make_unique<ProgramCache>(graphics.device)) {
: m_graphics(graphics), m_program_cache(std::make_unique<ProgramCache>(graphics)) {
EXIT_NOT_IMPLEMENTED(!Common::Thread::IsMainThread());
InitializeDriverCache();
}

View File

@ -1038,7 +1038,7 @@ bool RenderExecutor::CopyDepthToColor(CommandBuffer& buffer, const DrawCallInfo&
const auto& hw = buffer.GetRegisters();
auto& depth = state.depth_info;
auto& color = state.color_info[0];
EXIT_NOT_IMPLEMENTED(!m_context.GetGraphics().depth_color_copy_enabled ||
EXIT_NOT_IMPLEMENTED(!m_context.GetGraphics().maintenance8_enabled ||
!Prospero::IsRectList(ucfg.GetPrimType()) ||
draw.IsIndexed() || draw.index_count != 3 || draw.instance_count != 1 ||
emit.first_vertex != 0 || emit.first_instance != 0 ||

View File

@ -553,9 +553,9 @@ static vk::Device VulkanCreateDevice(GraphicContext& graphics,
graphics.attachment_feedback_loop_enabled =
feedback_extensions && feedback_layout.attachmentFeedbackLoopLayout &&
feedback_dynamic.attachmentFeedbackLoopDynamicState;
graphics.depth_color_copy_enabled = maintenance8_extension && maintenance8.maintenance8;
LOGF("Vulkan depth/color copy support: %s\n",
graphics.depth_color_copy_enabled ? "true" : "false");
graphics.maintenance8_enabled = maintenance8_extension && maintenance8.maintenance8;
LOGF("Vulkan maintenance8 support: %s\n",
graphics.maintenance8_enabled ? "true" : "false");
LOGF("Vulkan depth feedback support: %s\n",
graphics.attachment_feedback_loop_enabled ? "true" : "false");
if (graphics.mesh_shader_enabled) {
@ -652,7 +652,7 @@ static vk::Device VulkanCreateDevice(GraphicContext& graphics,
image_atomic_int64.sparseImageInt64Atomics = VK_FALSE;
create_info.pNext = &image_atomic_int64;
}
if (graphics.depth_color_copy_enabled) {
if (graphics.maintenance8_enabled) {
maintenance8.pNext = const_cast<void*>(create_info.pNext);
create_info.pNext = &maintenance8;
}

View File

@ -885,7 +885,8 @@ CompileResult CompileProgram(TranslateResult translated, const CompileOptions& o
LOGF("%s phase begin: stage=%s hash=0x%016" PRIx64 " SPIR-V EmitProgram\n",
GetDumpLabel(options), StageName(ir.stage), ir.shader_hash);
auto spirv = Spirv::EmitProgram(ir, options.input_info, push_data_start_dword);
auto spirv = Spirv::EmitProgram(ir, options.input_info, push_data_start_dword,
options.maintenance8_enabled);
LOGF("%s phase end: stage=%s hash=0x%016" PRIx64 " SPIR-V EmitProgram words=%" PRIu64
" elapsed_ms=%" PRIu64 "\n",
GetDumpLabel(options), StageName(ir.stage), ir.shader_hash,

View File

@ -21,6 +21,7 @@ struct CompileOptions {
uint64_t shader_hash = 0;
bool dump_ir = true;
bool early_dump = false;
bool maintenance8_enabled = false;
const char* dump_label = nullptr;
std::span<const uint32_t> user_data;
std::span<const uint32_t> back_code;

View File

@ -197,7 +197,7 @@ void ValidateNativeProgram(const IR::Program& program, bool lds_storage) {
} // namespace
std::vector<uint32_t> EmitProgram(IR::Program& program, ShaderStageInputInfo input_info,
uint32_t push_data_start_dword) {
uint32_t push_data_start_dword, bool maintenance8_enabled) {
using namespace Emitter;
if (program.stage != ShaderType::Compute && program.stage != ShaderType::Vertex &&
@ -215,7 +215,7 @@ std::vector<uint32_t> EmitProgram(IR::Program& program, ShaderStageInputInfo inp
}
IR::ValidateProgram(program, true);
program.bindings = {.push_data_start_dword = push_data_start_dword};
EmitterState state(program, input_info);
EmitterState state(program, input_info, maintenance8_enabled);
const auto* workgroup = ShaderWorkgroupInput(program.stage, input_info);
state.lane_count =
workgroup != nullptr && program.wave_size == 64u && workgroup->host_subgroup_size == 32u

View File

@ -11,7 +11,8 @@ namespace Libs::Graphics::ShaderRecompiler::Spirv {
// Resets and writes backend IDs and descriptor indices; do not emit the same program concurrently.
std::vector<uint32_t> EmitProgram(IR::Program& program, ShaderStageInputInfo input_info,
uint32_t push_data_start_dword = 0);
uint32_t push_data_start_dword = 0,
bool maintenance8_enabled = false);
} // namespace Libs::Graphics::ShaderRecompiler::Spirv

View File

@ -333,6 +333,7 @@ uint32_t PackedOffset(ValueEmitContext& ctx, IR::Value value, ImageDimension dim
TypeI32Vector(ctx.state, components),
std::span(values).first(components));
}
ctx.state.builder.RequireCapability(spv::CapabilityImageGatherExtended);
mask |= spv::ImageOperandsOffsetMask;
const auto packed = Unary(ctx.state, spv::OpBitcast, TypeI32(ctx.state), ctx.Def(value));
for (uint32_t index = 0; index < components; index++) {
@ -937,8 +938,8 @@ void EmitImage(ValueEmitContext& ctx, const IR::Inst& inst) {
operand_count = 2;
}
if (layout.offset != NoImageComponent &&
!offset_value.IsImmediate()) {
ctx.Fail(inst, "has a nonconstant sample texel offset");
!offset_value.IsImmediate() && !state.maintenance8_enabled) {
ctx.Fail(inst, "requires maintenance8 for a dynamic sample texel offset");
}
const auto samplers = EmitImageSamplers(ctx, inst);
const auto EmitSample = [&](uint32_t resource, uint32_t array_index) {

View File

@ -73,9 +73,9 @@ struct ImageDefinition {
};
struct EmitterState {
EmitterState(IR::Program& program_, ShaderStageInputInfo input_info_)
EmitterState(IR::Program& program_, ShaderStageInputInfo input_info_, bool maintenance8_enabled_)
: builder(program_.stage == ShaderType::Mesh ? 0x00010400u : 0x00010300u),
program(program_), input_info(input_info_) {
program(program_), input_info(input_info_), maintenance8_enabled(maintenance8_enabled_) {
if (ShaderWorkgroupInput(program.stage, input_info) != nullptr) {
lds_storage_class = spv::StorageClassWorkgroup;
}
@ -88,6 +88,7 @@ struct EmitterState {
Builder builder;
IR::Program& program;
ShaderStageInputInfo input_info;
bool maintenance8_enabled;
uint32_t void_type = 0;
uint32_t bool_type = 0;
uint32_t u32_type = 0;

View File

@ -1429,7 +1429,9 @@ void ValidateSpirv(const char *shader_name, const std::vector<u32> &spirv) {
messages += buffer;
});
if (!tools.Validate(spirv)) {
spvtools::ValidatorOptions options;
options.SetAllowOffsetTextureOperand(true);
if (!tools.Validate(spirv.data(), spirv.size(), options)) {
Fail(shader_name, "SPIR-V validation", messages);
}
}
@ -1648,6 +1650,7 @@ CompiledShader CompileCase(const TestCase &test, u32 host_subgroup_size = 64) {
}
ShaderRecompiler::CompileOptions options;
options.stage = ShaderType::Compute;
options.maintenance8_enabled = true;
options.dump_ir = true;
auto compute_info = test.compute_info;
compute_info.host_subgroup_size = host_subgroup_size;
@ -1717,7 +1720,8 @@ CompiledShader CompileCase(const TestCase &test, u32 host_subgroup_size = 64) {
}
result.program.shader_info_complete = false;
ShaderRecompiler::IR::CollectShaderInfo(result.program, options.input_info);
result.spirv = ShaderRecompiler::Spirv::EmitProgram(result.program, options.input_info);
result.spirv = ShaderRecompiler::Spirv::EmitProgram(
result.program, options.input_info, 0u, options.maintenance8_enabled);
const auto shader_data = result.program.bindings.descriptor_counts[
static_cast<size_t>(ShaderRecompiler::IR::DescriptorBindingKind::ShaderData)];
Require(test.name, "binding layout",
@ -1825,6 +1829,7 @@ CompiledShader CompileFragmentCase(const GraphicsCase &test) {
ShaderRecompiler::CompileOptions options;
options.stage = ShaderType::Pixel;
options.maintenance8_enabled = true;
options.dump_ir = false;
options.input_info.pixel = &pixel_info;
options.user_data = user_data;
@ -18712,6 +18717,7 @@ private:
m_runtime_context.queue_family = m_queue_family;
m_runtime_context.queue = m_queue;
m_runtime_context.shader_image_int64_atomics_enabled = true;
m_runtime_context.maintenance8_enabled = true;
m_runtime_context.attachment_feedback_loop_enabled = m_rasterization_supported;
m_runtime_context.provoking_vertex_last_enabled = m_rasterization_supported;
const vk::PhysicalDeviceImageFormatInfo2 block_texel_view_info{
@ -18859,9 +18865,11 @@ private:
available_provoking_vertex.pNext = &available_feedback_dynamic;
vk::PhysicalDeviceImageViewMinLodFeaturesEXT available_min_lod{};
available_min_lod.pNext = &available_provoking_vertex;
vk::PhysicalDeviceMaintenance8FeaturesKHR available_maintenance8{};
available_maintenance8.pNext = &available_min_lod;
vk::PhysicalDeviceFeatures2 available_features2{};
available_features2.sType = vk::StructureType::ePhysicalDeviceFeatures2;
available_features2.pNext = &available_min_lod;
available_features2.pNext = &available_maintenance8;
m_physical_device.getFeatures2(&available_features2);
Require("VulkanHarness", "dispatch",
available_features.shaderStorageImageWriteWithoutFormat == true,
@ -18899,6 +18907,8 @@ private:
"nonuniform storage buffer indexing is not supported");
Require("VulkanHarness", "dispatch", available_min_lod.minLod == true,
"image view minimum LOD is not supported");
Require("VulkanHarness", "dispatch", available_maintenance8.maintenance8 == true,
"maintenance8 dynamic image offsets are not supported");
Require("VulkanHarness", "graphics", available_features12.shaderOutputLayer == true,
"vertex layer output is not supported");
m_rasterization_supported = available_features.fillModeNonSolid &&
@ -18977,7 +18987,10 @@ private:
min_lod.pNext = m_rasterization_supported
? static_cast<void *>(&provoking_vertex)
: static_cast<void *>(&derivatives);
device_info.pNext = &min_lod;
vk::PhysicalDeviceMaintenance8FeaturesKHR maintenance8{};
maintenance8.maintenance8 = true;
maintenance8.pNext = &min_lod;
device_info.pNext = &maintenance8;
vk::PhysicalDeviceFeatures device_features{};
device_features.shaderStorageImageWriteWithoutFormat = true;
device_features.shaderImageGatherExtended = true;
@ -18994,6 +19007,7 @@ private:
VK_KHR_FRAGMENT_SHADER_BARYCENTRIC_EXTENSION_NAME,
VK_KHR_WORKGROUP_MEMORY_EXPLICIT_LAYOUT_EXTENSION_NAME,
VK_EXT_SHADER_IMAGE_ATOMIC_INT64_EXTENSION_NAME,
VK_KHR_MAINTENANCE_8_EXTENSION_NAME,
VK_EXT_IMAGE_VIEW_MIN_LOD_EXTENSION_NAME};
if (m_rasterization_supported) {
device_extensions.push_back(VK_EXT_DEPTH_CLIP_ENABLE_EXTENSION_NAME);
@ -35228,6 +35242,88 @@ template <u32 wave_size> TestCase ImageSampleOffsetsReadFourNeighbors() {
return test;
}
void CheckDynamicSampleOffsets(VulkanHarness &vulkan, u32 wave_size) {
using O = ShaderOpcode;
TestCase test;
test.name = wave_size == 32u ? "DynamicSampleOffsets32" : "DynamicSampleOffsets64";
test.has_compute_info = test.has_user_data = true;
test.compute_info.wave_size = wave_size;
test.compute_info.host_subgroup_size = vulkan.SubgroupSize();
test.compute_info.threads_num[0] = wave_size;
test.compute_info.threads_num[1] = test.compute_info.threads_num[2] = 1u;
test.compute_info.thread_ids_num = 1;
test.user_data = MakeSampledTextureData(Prospero::BufferFormat::k32_32_32_32Float);
test.user_data[2] = 7u | (7u << 14u);
test.user_data[3] |= 1u << 16u;
test.user_data[5] = 1u << 4u;
test.user_data[50] = wave_size * sizeof(u32);
// Read the offset selector from a texture, as the native shader does.
test.code = {EncodeVop2(0x1b, 20, InlineU32(7), 0),
EncodeVop2(0x16, 21, InlineU32(3), 0),
EncodeVop2(0x1b, 21, InlineU32(1), 21),
EncodeMimg0(0x00, 0x1), EncodeMimg1(4, 20),
EncodeVop2(0x1b, 20, InlineU32(3), 4),
EncodeVop2(0x16, 22, InlineU32(2), 4),
EncodeVop2(0x1b, 22, InlineU32(3), 22)};
for (u32 reg : {20u, 22u}) {
test.code.push_back(EncodeVop2(0x25, reg, InlineU32(63), reg));
test.code.push_back(EncodeVop2(0x1b, reg, InlineU32(63), reg));
}
test.code.push_back(EncodeVop2(0x1a, 22, InlineU32(8), 22));
test.code.push_back(EncodeVop2(0x1c, 20, Vgpr(22), 20));
AppendVMovLiteral(&test.code, 21, std::bit_cast<u32>(0.375f));
AppendVMovLiteral(&test.code, 22, std::bit_cast<u32>(0.375f));
AppendVMovLiteral(&test.code, 23, std::bit_cast<u32>(1.0f));
test.code.push_back(EncodeMimg0(0x34, 0x1)); // SAMPLE_L_O: offset is applied at mip 1.
test.code.push_back(EncodeMimg1(5, 20));
AppendStoreVgprAtLaneDwordOffset(&test.code, 5, 0, 0);
AppendEnd(&test.code);
test.opcodes = {O::V_AND_B32, O::V_LSHRREV_B32, O::V_ADD_NC_U32, O::V_LSHLREV_B32,
O::V_OR_B32, O::V_MOV_B32, O::IMAGE_LOAD, O::IMAGE_SAMPLE,
O::BUFFER_STORE_DWORD, O::S_ENDPGM};
test.required_spirv = {"ImageGatherExtended", "Lod|Offset", "OpBitFieldSExtract %int"};
test.forbidden_spirv = {"OpSwitch", "OpImageQuerySizeLod", "OpFDiv"};
test.initial.assign(wave_size, 0xdeadbeefu);
for (u32 lane = 0; lane < wave_size; ++lane)
test.expected.push_back(std::bit_cast<u32>(float(100u + (lane & 15u))));
auto compiled = CompileCase(test, vulkan.SubgroupSize());
spvtools::SpirvTools tools(SPV_ENV_VULKAN_1_2);
std::string text;
const auto halves = wave_size > vulkan.SubgroupSize() ? 2u : 1u;
Require(test.name, "direct dynamic offset", tools.Disassemble(compiled.spirv, &text) &&
CountText(text, "OpImageSampleExplicitLod") == halves &&
CountText(text, "OpBitFieldSExtract %int") == 2u * halves,
"dynamic offsets expanded samples or lost signed six-bit components");
#if KYTY_PLATFORM != KYTY_PLATFORM_WINDOWS
if (wave_size == 32u) {
Require(test.name, "default feature gate", !ShaderRecompiler::CompileOptions{}.maintenance8_enabled,
"maintenance8 must reflect the enabled device feature");
ExpectFatal("DynamicSampleOffsetsRequireMaintenance8", [&] {
ShaderRecompiler::Spirv::EmitProgram(compiled.program, {.compute = &test.compute_info});
});
}
#endif
auto base = MakeRgbaImage(8, 8);
auto mip = MakeRgbaImage(4, 4);
for (u32 selector = 0; selector < 16u; ++selector) {
SetRgbaPixel(&base, 8, selector & 7u, selector >> 3u, 0x3f800000u | selector, 0, 0, 0);
SetRgbaPixel(&mip, 4, selector & 3u, selector >> 2u,
std::bit_cast<u32>(float(100u + selector)), 0, 0, 0);
}
auto texture = vulkan.CreateImageMips(test.name, 8, 8, vk::Format::eR32G32B32A32Sfloat,
vk::ImageUsageFlagBits::eSampled, {base, mip}, 4u, vk::ImageLayout::eShaderReadOnlyOptimal,
vk::ImageType::e2D, vk::ImageViewType::e2D, 1u);
const auto sampler = vulkan.CreateSampler(test.name, 2u);
auto output = vulkan.CreateStorageBuffer(test.name, test.initial, test.initial.size());
vulkan.Dispatch(test, compiled, output, nullptr, &texture, nullptr, nullptr, sampler);
CompareWords(test, "image-derived offsets -1..2 at mip 1", test.expected,
vulkan.ReadBuffer(test.name, output, test.expected.size()));
vulkan.DestroyBuffer(&output);
vulkan.Device().destroySampler(sampler);
vulkan.DestroyImage(&texture);
std::printf("[compute] %-32s ok\n", test.name);
}
TestCase ImageSampleA16CompareBiasRdna2AddressOrder() {
using O = ShaderOpcode;
@ -43794,6 +43890,8 @@ int main(int argc, char **argv) {
}
if (argc == 2 && std::strcmp(argv[1], "--image-sample-offset-only") == 0) {
VulkanHarness vulkan;
CheckDynamicSampleOffsets(vulkan, 32u);
CheckDynamicSampleOffsets(vulkan, 64u);
RunCase(&vulkan, ImageSampleOffsetsReadFourNeighbors<32>());
RunCase(&vulkan, ImageSampleOffsetsReadFourNeighbors<64>());
RunCase(&vulkan, ImageSampleA16OffsetKeepsTexelOffset32BitOnGpu<32>());
@ -44463,6 +44561,8 @@ int main(int argc, char **argv) {
CheckRuntimeBufferRecords(vulkan);
CheckComputeThreadDimensions(vulkan);
CheckIndirectImageKeySwitch(vulkan);
CheckDynamicSampleOffsets(vulkan, 32u);
CheckDynamicSampleOffsets(vulkan, 64u);
CheckFiniteInlineSamplerPhi(vulkan);
CheckWave64WholeWaveResults();
CheckPs5GameExampleImageClearRuntimeShape();