Compare commits

...
Author SHA1 Message Date
f90a8b1589 graphics: decode register clears of RGBA16 UNORM, UINT and SINT and RG32 UINT and SINT targets
Co-authored-by: Senaxx <michelbeld@gmail.com>
Co-authored-by: Jetsku <59046009+Jetsku@users.noreply.github.com>
2026-10-26 07:43:11 +01:00
nmzikandusa-ratt 17a0f53d39 graphics: resolve MSAA color into the level and layer the texture cache placed each target at
Co-authored-by: usa-ratt <188116257+usa-ratt@users.noreply.github.com>
2026-10-26 07:43:11 +01:00
nmzikandSenaxx 943bc19f46 shader: leave the start instance out of mesh and NGG instance ID VGPRs
Co-authored-by: Senaxx <michelbeld@gmail.com>
2026-10-26 07:43:11 +01:00
nmzik 5429888d4c shader: keep pixel helper lanes out of the EXEC and VCC words 2026-10-26 07:43:11 +01:00
nmzikandSenaxx 025d1f7edf shader: decode the SHARED_LIMIT and PRIVATE_LIMIT inline constants
Co-authored-by: Senaxx <michelbeld@gmail.com>
2026-10-26 06:35:17 +01:00
nmzik 3c62269751 ci: build Windows with Visual Studio 2026 on windows-2025 2026-10-26 05:40:00 +01:00
shadowbeat070 ceb154d9e4 shader: acquire stored buffers and images at S_BARRIER 2026-10-26 05:32:02 +01:00
26ca609236 shader: type pixel outputs of UINT and SINT render targets as integer vectors
Co-authored-by: Ali Almohaya <AliAlmohaya@gmail.com>
Co-authored-by: Mac <me@itsmemac.com>
Co-authored-by: Senaxx <michelbeld@gmail.com>
2026-10-26 05:32:02 +01:00
nmzikandSenaxx 4ebae87a1b shader: apply the LOD clamp of image_sample _cl variants
Co-authored-by: Senaxx <michelbeld@gmail.com>
2026-10-26 05:32:02 +01:00
nmzikandAli Almohaya b4209dde1b graphics: write the subdraw index of multi-draw indirect packets
Co-authored-by: Ali Almohaya <AliAlmohaya@gmail.com>
2026-10-26 05:32:02 +01:00
nmzik 72d7da7266 texture_cache: view BC images for raw reads of their layers instead of evicting them 2026-10-26 05:32:02 +01:00
ce5e57aa36 shader: fold complementary masks so wave reductions see full EXEC
Co-authored-by: Senaxx <michelbeld@gmail.com>
Co-authored-by: Mac <me@itsmemac.com>
2026-10-26 05:32:02 +01:00
6357740fa1 shader: lower wave32 whole-wave UMin and UMax reductions to subgroup reductions
Co-authored-by: Senaxx <michelbeld@gmail.com>
Co-authored-by: Mac <me@itsmemac.com>
2026-10-26 05:32:02 +01:00
nmzik be1c84f4d7 graphics: shorten the mesh draw skip warning 2026-10-26 05:32:02 +01:00
nmzik 560756f34c graphics: skip draws of mesh shaders that exceed host mesh limits 2026-10-26 05:32:02 +01:00
52c80be266 savedata: stop logging empty sceSaveDataGetEventResult polls
Co-authored-by: Mac <me@itsmemac.com>
Co-authored-by: Senaxx <michelbeld@gmail.com>
2026-10-26 05:32:02 +01:00
22a9392d23 savedata: implement sceSaveDataConvert's synchronous checks
Co-authored-by: Mac <me@itsmemac.com>
Co-authored-by: Senaxx <michelbeld@gmail.com>
2026-10-26 05:32:02 +01:00
nmzik 95bbc9c65d graphics: hold image heap descriptors whose address another extent pins 2026-10-26 05:32:02 +01:00
Senaxx c5f7644751 audio: stub sceAcmBatchJobPriority 2026-10-26 05:32:02 +01:00
nmzik 4db225008b graphics: resize image heap tables in place when their descriptor size changes 2026-10-26 05:32:02 +01:00
nmzik dc3bd68dee gpu: bound null storage bindings to zero bytes 2026-10-26 05:32:02 +01:00
nmzik c1dfb309a1 graphics: drop image heap table snapshots superseded at the same base 2026-10-26 05:32:02 +01:00
nmzik ba7f9969ef shader: bound storage buffers by pushed binding sizes instead of OpArrayLength 2026-10-26 05:32:02 +01:00
7524e8f79e shader: count one SGPR for lane reads and wave32 lane masks in call targets
Co-authored-by: Ali Almohaya <AliAlmohaya@gmail.com>
Co-authored-by: Senaxx <michelbeld@gmail.com>
Co-authored-by: Mac <me@itsmemac.com>
2026-10-26 05:32:02 +01:00
nmzik 153dadd0b3 shader: emit 16-bit packs with a zero high lane as a mask 2026-10-26 05:32:02 +01:00
nmzik 463a096249 graphics: unwatch image heap pages on writes next to a table 2026-10-26 05:32:02 +01:00
LloydNicholson 597137e8b1 vulkan: enable VK_EXT_extended_dynamic_state when available (#1906) 2026-10-26 02:08:43 +01:00
b2c9ec7609 renderer: decode R8, R8G8 and R16_UINT color register clears (#1893)
DecodePackedColorClear had no case for R8_UNORM/UINT, R8G8_UNORM/UINT and
R16_UINT, so a CMASK fast clear of such a render target was dropped and the
target kept its old contents. UFC 6 (PPSA23566) fast-clears an R8_UINT
region map this way every frame and then writes only part of it; a capture
showed the map never cleared. DWORD fills of these formats are decoded only
when every pixel in the word holds the same value.

Co-authored-by: 4wgycvn84p-afk <346802885+4wgycvn84p-afk@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 18:47:45 +01:00
56 changed files with 1591 additions and 678 deletions

No files matched your search

+1 -1
View File
@@ -32,7 +32,7 @@ jobs:
windows:
name: Build KytyPS5 (Windows)
needs: metadata
runs-on: windows-2022
runs-on: windows-2025
steps:
- name: Checkout repository
@@ -27,6 +27,7 @@ struct IndirectDrawRegisters {
uint32_t vertex_offset;
uint32_t instance_offset;
uint32_t index_offset;
uint32_t draw_index;
};
class Pm4Execution {
@@ -1582,7 +1582,7 @@ KYTY_CP_OP_PARSER(CpOpDrawIndirect) {
// The native indexed packet enables the high-half first-index destination in word 3.
const IndirectDrawRegisters registers {
buffer[1] & 0xffffu, buffer[2] & 0xffffu,
indexed && (buffer[2] & (1u << 28u)) != 0 ? buffer[1] >> 16u : Pm4::SH_NOP};
indexed && (buffer[2] & (1u << 28u)) != 0 ? buffer[1] >> 16u : Pm4::SH_NOP, Pm4::SH_NOP};
cp.DrawIndirect(data_offset, registers, draw_initiator, indexed);
@@ -1602,10 +1602,12 @@ KYTY_CP_OP_PARSER(CpOpDrawIndirectMulti) {
const auto stride_in_bytes = buffer[7];
const auto draw_initiator = buffer[8];
const bool indexed = (cmd_id == 0xc0083800);
// Multi-draw moves the first-index enable to word 4; the destinations stay in 2/3.
// Multi-draw moves the first-index enable to word 4; the destinations stay in 2/3. Word 4
// bit 31 enables the subdraw index, written to the register in its low half.
const IndirectDrawRegisters registers {
buffer[1] & 0xffffu, buffer[2] & 0xffffu,
indexed && (buffer[3] & (1u << 28u)) != 0 ? buffer[1] >> 16u : Pm4::SH_NOP};
indexed && (buffer[3] & (1u << 28u)) != 0 ? buffer[1] >> 16u : Pm4::SH_NOP,
(buffer[3] >> 31u) != 0 ? buffer[3] & 0xffffu : Pm4::SH_NOP};
if (count_indirect == 0) {
count_addr = nullptr;
+6 -4
View File
@@ -789,7 +789,7 @@ void CommandProcessor::DrawIndexOffset(uint32_t index_offset, uint32_t index_cou
static void PatchIndirectDrawOffsets(CommandProcessor& cp, IndirectDrawRegisters registers,
uint32_t vertex_offset, uint32_t instance_offset,
uint32_t first_index) {
uint32_t first_index, uint32_t draw_index) {
const auto write = [&](uint32_t location, uint32_t value) {
if (location == Pm4::SH_NOP) {
return;
@@ -801,6 +801,7 @@ static void PatchIndirectDrawOffsets(CommandProcessor& cp, IndirectDrawRegisters
write(registers.vertex_offset, vertex_offset);
write(registers.instance_offset, instance_offset);
write(registers.index_offset, first_index);
write(registers.draw_index, draw_index);
}
void CommandProcessor::DrawIndirect(uint32_t data_offset, IndirectDrawRegisters registers,
@@ -839,7 +840,8 @@ void CommandProcessor::DrawIndirectMulti(uint32_t data_offset, uint32_t max_coun
// while they are zero.
const DrawIndirectArgs records {args_addr, draw_count, stride_in_bytes};
if (registers.vertex_offset == Pm4::SH_NOP && registers.instance_offset == Pm4::SH_NOP &&
registers.index_offset == Pm4::SH_NOP && (!indexed || m_index_buffer_size != 0) &&
registers.index_offset == Pm4::SH_NOP && registers.draw_index == Pm4::SH_NOP &&
(!indexed || m_index_buffer_size != 0) &&
(indexed ? executor.DrawIndex(m_submit_id, CurrentBuffer(),
{.index_count = m_index_buffer_size,
.index_addr = IndexAddress(0),
@@ -854,7 +856,7 @@ void CommandProcessor::DrawIndirectMulti(uint32_t data_offset, uint32_t max_coun
const auto addr = args_addr + static_cast<uint64_t>(i) * stride_in_bytes;
if (!indexed) {
const auto& args = *reinterpret_cast<const vk::DrawIndirectCommand*>(addr);
PatchIndirectDrawOffsets(*this, registers, args.firstVertex, args.firstInstance, 0);
PatchIndirectDrawOffsets(*this, registers, args.firstVertex, args.firstInstance, 0, i);
executor.DrawAuto(m_submit_id, CurrentBuffer(),
{.vertex_count = args.vertexCount,
.instance_count = args.instanceCount,
@@ -866,7 +868,7 @@ void CommandProcessor::DrawIndirectMulti(uint32_t data_offset, uint32_t max_coun
const auto& args = *reinterpret_cast<const vk::DrawIndexedIndirectCommand*>(addr);
PatchIndirectDrawOffsets(*this, registers, static_cast<uint32_t>(args.vertexOffset),
args.firstInstance, args.firstIndex);
args.firstInstance, args.firstIndex, i);
executor.DrawIndex(m_submit_id, CurrentBuffer(),
{.index_count = std::min(args.indexCount, max_indices),
.index_addr = IndexAddress(args.firstIndex),
+22 -1
View File
@@ -783,7 +783,15 @@ TextureCache::OverlapResult TextureCache::ResolveOverlap(const ImageInfo& reques
if (!cached.registered) {
return {merged_id};
}
if (BcAlias::Matches(requested, cached.info) && !cached.IsCpuDirty()) {
// PS5: image heaps keep BC images and raw block views of their layers resident together, so a
// raw read of a layer subset with the same levels views the BC image instead of evicting it.
const bool raw_view = binding == BindingType::Texture && !requested.IsBlock() &&
requested.type == cached.info.type &&
requested.data.address == cached.info.data.address &&
requested.BlockExtent() == cached.info.BlockExtent() &&
requested.resources.levels == cached.info.resources.levels &&
requested.resources.layers < cached.info.resources.layers;
if (!raw_view && BcAlias::Matches(requested, cached.info) && !cached.IsCpuDirty()) {
BcAlias::Resolve(*this, cached_id, merged_id);
return {merged_id};
}
@@ -1302,6 +1310,19 @@ ImageId TextureCache::FindImage(ImageDesc& desc, bool exact_format) {
view_layer = overlap.layer;
}
}
// PS5: image heaps hold a descriptor per dynamic-resolution extent of a render target,
// and only one extent at an address holds data, so heap lookups create no second one.
if (!result && desc.heap &&
std::any_of(candidates.begin(), candidates.end(), [&](ImageId id) {
const auto* image = m_slot_images.try_get(id);
return image != nullptr && image->registered &&
image->heap_refs.load(std::memory_order_relaxed) != 0u &&
image->info.data.address == desc.info.data.address &&
image->info.type == desc.info.type &&
image->info.extent != desc.info.extent;
})) {
return {};
}
} else if (block_alias) {
for (const auto candidate: candidates) {
const auto& owner = m_slot_images[candidate];
+1
View File
@@ -43,6 +43,7 @@ public:
ImageInfo info;
ImageViewInfo view_info;
BindingType type = BindingType::Texture;
bool heap = false; // PS5: resolved for a resident image heap.
};
TextureCache(GraphicContext& graphics, CommandScheduler& scheduler, PageManager& page_manager,
@@ -425,15 +425,15 @@ void Image::Resolve(Image& source, const ImageSubresourceRange& source_range,
const auto destination_height =
std::max(backing.extent.height >> destination_range.base_level, 1u);
const bool copy = source.backing.samples == 1;
EXIT_IF(layers == 0 || info.extent.width > source_width || info.extent.height > source_height ||
info.extent.width > destination_width || info.extent.height > destination_height ||
EXIT_IF(layers == 0 ||
(copy ? !ImageViewOps::FormatsCompatible(source.backing.format, backing.format)
: source.backing.format != backing.format));
auto resolved_source_range = source_range;
auto resolved_destination_range = destination_range;
resolved_source_range.layer_count = layers;
resolved_destination_range.layer_count = layers;
const vk::Extent3D resolve_extent {info.extent.width, info.extent.height, 1};
const vk::Extent3D resolve_extent {std::min(source_width, destination_width),
std::min(source_height, destination_height), 1};
m_scheduler.EndRendering();
auto command = m_scheduler.Current().Handle();
@@ -378,6 +378,9 @@ inline constexpr std::array<VideoOutFormatPolicy, 8> VIDEO_OUT_FORMAT_POLICIES {
const auto encoded = static_cast<float>(value & 0xffu) / 255.0f;
return encoded <= 0.04045f ? encoded / 12.92f : std::pow((encoded + 0.055f) / 1.055f, 2.4f);
};
const auto unorm16 = [](uint64_t value) {
return static_cast<float>(value & 0xffffu) / 65535.0f;
};
const auto float16 = [](uint32_t value) {
const auto sign = (value & 0x8000u) << 16u;
const auto exponent = (value >> 10u) & 0x1fu;
@@ -403,19 +406,49 @@ inline constexpr std::array<VideoOutFormatPolicy, 8> VIDEO_OUT_FORMAT_POLICIES {
next.float32[1] = float16(packed >> 16u);
[[fallthrough]];
case vk::Format::eR16Sfloat: next.float32[0] = float16(packed); break;
case vk::Format::eR16G16Unorm:
next.float32[1] = static_cast<float>(packed >> 16u) / 65535.0f;
case vk::Format::eR16G16B16A16Unorm:
next.float32[2] = unorm16(clear_words >> 32u);
next.float32[3] = unorm16(clear_words >> 48u);
[[fallthrough]];
case vk::Format::eR16Unorm:
next.float32[0] = static_cast<float>(packed & 0xffffu) / 65535.0f;
case vk::Format::eR16G16Unorm:
next.float32[1] = unorm16(packed >> 16u);
[[fallthrough]];
case vk::Format::eR16Unorm: next.float32[0] = unorm16(packed); break;
case vk::Format::eR16G16B16A16Uint:
for (uint32_t i = 0; i < 4; i++) {
next.uint32[i] = static_cast<uint16_t>(clear_words >> (16u * i));
}
break;
// Each clear register stores one raw 32-bit float channel.
case vk::Format::eR16G16B16A16Sint:
for (uint32_t i = 0; i < 4; i++) {
next.int32[i] = static_cast<int16_t>(clear_words >> (16u * i));
}
break;
// Each clear register stores one raw 32-bit channel.
case vk::Format::eR32G32Sfloat:
next.float32[1] = std::bit_cast<float>(static_cast<uint32_t>(clear_words >> 32u));
[[fallthrough]];
case vk::Format::eR32Sfloat: next.float32[0] = std::bit_cast<float>(packed); break;
case vk::Format::eR32Uint: next.uint32[0] = packed; break;
case vk::Format::eR32Sint: next.int32[0] = static_cast<int32_t>(packed); break;
case vk::Format::eR32G32Uint:
case vk::Format::eR32G32Sint:
next.uint32[1] = static_cast<uint32_t>(clear_words >> 32u);
[[fallthrough]];
case vk::Format::eR32Uint:
case vk::Format::eR32Sint: next.uint32[0] = packed; break;
// Narrow targets hold their channels in the low bits of the first word. UFC 6 (PPSA23566)
// fast-clears an R8_UINT map through CMASK every frame and then writes only part of it;
// without these the clear was dropped and old contents piled up.
case vk::Format::eR8Uint: next.uint32[0] = packed & 0xffu; break;
case vk::Format::eR8Unorm: next.float32[0] = unorm8(packed); break;
case vk::Format::eR8G8Uint:
next.uint32[0] = packed & 0xffu;
next.uint32[1] = (packed >> 8u) & 0xffu;
break;
case vk::Format::eR8G8Unorm:
next.float32[0] = unorm8(packed);
next.float32[1] = unorm8(packed >> 8u);
break;
case vk::Format::eR16Uint: next.uint32[0] = packed & 0xffffu; break;
case vk::Format::eR8G8B8A8Srgb:
next.float32[0] = srgb8(packed);
next.float32[1] = srgb8(packed >> 8u);
@@ -462,10 +495,16 @@ inline constexpr std::array<VideoOutFormatPolicy, 8> VIDEO_OUT_FORMAT_POLICIES {
// Keep this separate: the first register word alone cannot describe a 64-bit color.
[[nodiscard]] inline bool DecodeColorDwordFill(vk::Format format, uint32_t packed,
vk::ClearColorValue& clear) {
if ((format == vk::Format::eR16Sfloat || format == vk::Format::eR16Unorm) &&
if ((format == vk::Format::eR16Sfloat || format == vk::Format::eR16Unorm ||
format == vk::Format::eR16Uint || format == vk::Format::eR8G8Unorm ||
format == vk::Format::eR8G8Uint) &&
(packed & 0xffffu) != (packed >> 16u)) {
return false;
}
if ((format == vk::Format::eR8Unorm || format == vk::Format::eR8Uint) &&
packed != (packed & 0xffu) * 0x01010101u) {
return false;
}
if (format == vk::Format::eR16G16B16A16Sfloat) {
if (packed != 0) {
return false;
@@ -604,8 +604,13 @@ TextureBinding RenderExecutor::ResolveTexture(const ShaderRecompiler::IR::ImageR
view_levels, desc.info.resources.layers);
desc.view_info.base_level = view_base;
desc.type = storage ? TextureCache::BindingType::Storage : TextureCache::BindingType::Texture;
desc.heap = resource.heap;
auto id = texture_cache.FindImage(desc, shader_conversion);
auto id = texture_cache.FindImage(desc, shader_conversion);
if (!id) {
// Another extent at the address holds it; the heap retries once an image is created.
return {.desc = std::move(desc), .layout = vk::ImageLayout::eGeneral};
}
auto* image = &texture_cache.GetImage(id);
const bool stencil_association = static_cast<bool>(image->depth_id);
if (stencil_association) {
@@ -801,6 +806,9 @@ void RenderExecutor::RebindBuffers(PreparedBindings& prepared) {
prepared.buffers[index] = NativeStorageBuffer(m_context, prepared.buffer_sources[index],
resource, buffer_offset);
pack_memory_offset(index, buffer_offset);
// Null bindings allow 0 bytes, not the shared null buffer's 16, so no store reaches it.
prepared.shader_data[layout.MemorySizeDword() + index] =
static_cast<uint32_t>(prepared.buffer_sources[index].size + buffer_offset);
}
if (layout.descriptor_counts[static_cast<size_t>(BindingKind::FlattenedSrt)] != 0) {
prepared.flattened_srt = NativeUpload(m_context, snapshot.flattened_srt);
@@ -12,6 +12,7 @@
#include "kernel/memory.h"
#include <algorithm>
#include <bit>
#include <xxhash.h>
namespace Libs::Graphics {
@@ -44,6 +45,20 @@ void Widen(size_t& begin, size_t& end, size_t first, size_t last) {
end = empty ? last : std::max(end, last);
}
// Probes at first + k * step whose first dword lies within a table of size bytes.
uint64_t Probes(uint64_t size, uint64_t first, uint64_t step) {
return size >= first + 4u ? (size - first - 4u) / step + 1u : 0u;
}
// Tables grow a little on most frames, so their slot words get room to double.
std::unique_ptr<Buffer> SlotBuffer(RenderContext& context, uint64_t size) {
return std::make_unique<Buffer>(
context.GetGraphics(), context.GetCommandScheduler(), MemoryUsage::DeviceLocal, 0,
vk::BufferUsageFlagBits::eTransferDst | vk::BufferUsageFlagBits::eStorageBuffer |
vk::BufferUsageFlagBits::eShaderDeviceAddress,
std::bit_ceil(size + 4u));
}
// Guest bytes of a table that its tracker pages [begin, end) cover.
std::pair<uint64_t, uint64_t> PageRange(uint64_t base, uint64_t size, size_t begin, size_t end) {
const auto origin = Common::AlignDown(base, TRACKER_PAGE_SIZE);
@@ -122,22 +137,17 @@ bool ImageHeap::Acquire(const IR::ImageHeapRequest& request, IR::ImageHeapView&
((request.base | request.first | request.step) & 3u) != 0u) {
return false;
}
auto memory = std::ranges::find_if(m_memories, [&](const auto& candidate) {
return candidate->base == request.base && candidate->size == size;
});
if (memory == m_memories.end()) {
auto memory = std::ranges::find_if(
m_memories, [&](const auto& candidate) { return candidate->base == request.base; });
// A table follows its descriptor's size, as the first program of a draw acquires it.
if (memory == m_memories.end() || ((*memory)->size != size && (*memory)->draw != m_draw)) {
if (!m_context.IsMapped(request.base, size)) return false;
auto created = std::make_unique<Memory>();
created->base = request.base;
created->size = size;
created->words.resize(size / 4u);
created->dead.resize(size / 4u);
created->dirty_pages.assign((Common::AlignUp(request.base + size, TRACKER_PAGE_SIZE) -
Common::AlignDown(request.base, TRACKER_PAGE_SIZE)) /
TRACKER_PAGE_SIZE,
uint8_t {1});
std::scoped_lock lock {m_mutex};
memory = m_memories.insert(m_memories.end(), std::move(created));
if (memory == m_memories.end()) {
std::scoped_lock lock {m_mutex};
memory = m_memories.insert(m_memories.end(),
std::make_unique<Memory>(Memory {.base = request.base}));
}
Resize(**memory, size);
}
// A draw refreshes a table and a view takes its changes once, so all programs of the draw see
// one snapshot and one set of kinds.
@@ -146,27 +156,28 @@ bool ImageHeap::Acquire(const IR::ImageHeapRequest& request, IR::ImageHeapView&
(*memory)->draw = m_draw;
}
auto& views = (*memory)->views;
const auto bytes = (*memory)->size;
const auto profile = IR::ImageHeapResource(*request.root, 0u);
const auto probes = Probes(size, request.first, request.step);
const auto limit = request.count < probes ? request.count : UINT64_MAX;
auto view = std::ranges::find_if(views, [&](const auto& candidate) {
return candidate->step == request.step && candidate->first == request.first &&
candidate->count == request.count && candidate->profile == profile;
candidate->limit == limit && candidate->profile == profile;
});
if (view == views.end()) {
const auto epoch = Image::heap_epoch.load(std::memory_order_relaxed);
const auto count = std::min(limit, Probes(bytes, request.first, request.step));
auto created = std::make_unique<View>(View {.memory = **memory,
.step = request.step,
.first = request.first,
.count = request.count,
.count = count,
.limit = limit,
.profile = profile,
.resolve_epoch = epoch,
.layout_epoch = epoch});
created->slots.resize(size / 4u + 1u);
created->buffer = std::make_unique<Buffer>(
m_context.GetGraphics(), m_context.GetCommandScheduler(), MemoryUsage::DeviceLocal, 0,
vk::BufferUsageFlagBits::eTransferDst | vk::BufferUsageFlagBits::eStorageBuffer |
vk::BufferUsageFlagBits::eShaderDeviceAddress,
size + 4u);
UpdateProbes(*created, 0u, size);
created->slots.resize(bytes / 4u + 1u);
created->buffer = SlotBuffer(m_context, bytes);
UpdateProbes(*created, 0u, bytes);
// The first upload also clears the slots no probe selects.
created->dirty_begin = 0;
created->dirty_end = created->slots.size();
@@ -254,10 +265,12 @@ void ImageHeap::Unwatch(const Memory& memory) {
void ImageHeap::InvalidateMemory(uint64_t address, uint64_t size) {
std::scoped_lock lock {m_mutex};
for (auto& memory: m_memories) {
const auto begin = std::max(address, memory->base);
const auto end = std::min(address + size, memory->base + memory->size);
if (begin >= end) continue;
// Watches cover whole pages, so a write next to the table must unwatch its page too.
const auto origin = Common::AlignDown(memory->base, TRACKER_PAGE_SIZE);
const auto begin = std::max(address, origin);
const auto end = std::min(address + size,
origin + memory->dirty_pages.size() * TRACKER_PAGE_SIZE);
if (begin >= end) continue;
MarkDirty(*memory, (begin - origin) / TRACKER_PAGE_SIZE,
(end - 1u - origin) / TRACKER_PAGE_SIZE + 1u);
}
@@ -381,15 +394,56 @@ uint32_t ImageHeap::Allocate() {
}
void ImageHeap::Retire(View& view) {
if (view.released.empty()) return;
// A retired index is reused once its tick ends, so no held entry may keep it.
std::erase_if(view.held, [&](uint32_t index) { return m_elements[index].refs == 0u; });
const auto tick = m_context.GetCommandScheduler().CurrentTick();
for (const auto index: view.released) m_retired.emplace_back(tick, index);
view.released.clear();
}
void ImageHeap::Resize(Memory& memory, uint64_t size) {
{
std::scoped_lock lock {m_mutex};
Unwatch(memory);
memory.size = size;
// The next refresh rereads the whole table.
memory.dirty_pages.assign((Common::AlignUp(memory.base + size, TRACKER_PAGE_SIZE) -
Common::AlignDown(memory.base, TRACKER_PAGE_SIZE)) /
TRACKER_PAGE_SIZE,
uint8_t {1});
}
memory.words.resize(size / 4u);
memory.dead.resize(size / 4u);
auto& scheduler = m_context.GetCommandScheduler();
for (auto& view: memory.views) {
const auto count = std::min(view->limit, Probes(size, view->first, view->step));
for (auto probe = count; probe < view->count; ++probe) {
Assign(*view, (view->first + probe * view->step) / 4u, 0u);
}
view->count = count;
const auto slots = view->slots.size();
view->slots.resize(size / 4u + 1u);
view->dirty_end = std::min(view->dirty_end, view->slots.size());
view->dirty_begin = std::min(view->dirty_begin, view->dirty_end);
if (view->slots.size() * sizeof(uint32_t) > view->buffer->Size()) {
if (scheduler.Active()) {
scheduler.DeferOperation([buffer = std::move(view->buffer)] {});
}
view->buffer = SlotBuffer(m_context, size);
Widen(view->dirty_begin, view->dirty_end, 0u, view->slots.size());
} else if (slots < view->slots.size()) {
// Device words past the old end may hold stale slots.
Widen(view->dirty_begin, view->dirty_end, slots, view->slots.size());
}
}
}
void ImageHeap::Rebind(RenderExecutor& executor) {
KYTY_PROFILER_FUNCTION();
auto& textures = m_context.GetTextureCache();
const auto epoch = Image::heap_epoch.load(std::memory_order_relaxed);
const auto serial = Image::serials.load(std::memory_order_relaxed);
for (auto* view: m_active) {
bool stale = false;
const auto mark = [&](uint32_t index) { m_elements[index].stale = stale = true; };
@@ -418,6 +472,19 @@ void ImageHeap::Rebind(RenderExecutor& executor) {
mark(index);
return true;
});
// Held elements retry once an image is created, such as a render target of their extent.
if (view->held_serial != serial) {
view->held_serial = serial;
std::erase_if(view->held, [&](uint32_t index) {
const auto& element = m_elements[index];
if (element.refs == 0u || element.stale) return true;
const auto binding = executor.ResolveTexture(
IR::ImageHeapResource(view->profile, element.kind), element.value);
if (binding.image_id) mark(index);
// An unmapped texture stays null.
return binding.image_id || binding.desc.info.data.Empty();
});
}
if (stale) {
// A changed image needs a new element; pending draws still read the old one. The new
// element keeps the old value, since a view takes table changes only when acquired.
@@ -446,6 +513,8 @@ void ImageHeap::Rebind(RenderExecutor& executor) {
view->dcc.push_back(index);
}
view->away.push_back(index);
} else if (!binding.desc.info.data.Empty()) {
view->held.push_back(index);
}
m_elements[index].binding = std::move(binding);
m_writes.push_back(index);
@@ -22,10 +22,11 @@ class RenderExecutor;
struct ImageHeapTestAccess;
// Guest image tables that shaders index by GPU-computed offsets, kept resident in one bindless
// sampled-image array. Each view of a table owns device slot words, one per table dword, that
// select an array element. CPU writes to a table fault in its snapshot, which each draw refreshes
// once; a view takes the changes when a draw first acquires it. Changes to the images behind
// elements bump Image::heap_epoch.
// sampled-image array. A table is keyed by its base and follows its descriptor's size. Each view
// of a table owns device slot words, one per table dword, that select an array element. CPU
// writes to a table fault in its snapshot, which each draw refreshes once; a view takes the
// changes when a draw first acquires it. Changes to the images behind elements bump
// Image::heap_epoch.
class ImageHeap final: public ShaderRecompiler::IR::ImageHeapSource {
public:
ImageHeap(RenderContext& context, PageManager& page_manager);
@@ -70,6 +71,7 @@ private:
uint64_t step = 0;
uint64_t first = 0;
uint64_t count = 0;
uint64_t limit = 0; // Probes a key domain allows, if fewer.
ShaderRecompiler::IR::ImageResource profile;
std::vector<uint32_t> slots; // Plus a trailing null slot.
std::vector<uint32_t> kind_keys;
@@ -79,6 +81,7 @@ private:
std::vector<uint32_t> released; // No slot selects them after the next upload.
std::vector<uint32_t> away; // Not yet moved to the read layout.
std::vector<uint32_t> dcc; // Color metadata clears apply on each discovery.
std::vector<uint32_t> held; // Another extent holds their address.
std::unique_ptr<Buffer> buffer;
size_t dirty_begin = 0; // Slots to upload.
size_t dirty_end = 0;
@@ -86,6 +89,7 @@ private:
size_t changed_end = 0;
uint64_t resolve_epoch = 0;
uint64_t layout_epoch = 0;
uint64_t held_serial = 0; // Image::serials when held was last retried.
};
struct Memory {
@@ -113,6 +117,7 @@ private:
Image* ImageOf(const Element& element) const;
uint32_t Allocate();
void Retire(View& view);
void Resize(Memory& memory, uint64_t size);
void WriteDescriptors();
RenderContext& m_context;
@@ -711,8 +711,13 @@ void PipelineCache::GetGraphicsPrograms(const CommandBuffer& command, DrawRender
mesh.max_vertices > limits.maxMeshOutputVertices ||
mesh.max_primitives > limits.maxMeshOutputPrimitives ||
mesh.lds_size_dwords * sizeof(uint32_t) > limits.maxMeshSharedMemorySize) {
EXIT("mesh shader exceeds host limits: threads=%u vertices=%u primitives=%u LDS=%u\n",
host_threads, mesh.max_vertices, mesh.max_primitives, mesh.lds_size_dwords);
static std::atomic_bool warned = false;
if (!warned.exchange(true, std::memory_order_relaxed)) {
Log::WriteToConsoleAndLog(fmt::format(
"Warning: dropping mesh draws the host does not support (shader 0x{:016x}).\n",
vertex_params[0].hash));
}
return;
}
}
ShaderParams pixel_params;
@@ -736,6 +741,11 @@ void PipelineCache::GetGraphicsPrograms(const CommandBuffer& command, DrawRender
const auto& rt = context.GetRenderTarget(slot);
const auto target_mask = render_target_mask_slot(context.GetRenderTargetMask(), slot);
if (rt.base.addr == 0 || target_mask == 0u) continue;
if (rt.info.channel_type == Prospero::ChannelType::kUInt) {
pixel_info.target_uint_mask |= 1u << export_index;
} else if (rt.info.channel_type == Prospero::ChannelType::kSInt) {
pixel_info.target_sint_mask |= 1u << export_index;
}
const auto format = Prospero::ResolveRenderTargetFormat(rt.info.format, rt.info.channel_type);
if (format.buffer_format != Prospero::BufferFormat::k11_11_10UNorm) continue;
const auto mapping = pixel_info.target_export_mapping[export_index];
@@ -767,9 +777,11 @@ void PipelineCache::GetGraphicsPrograms(const CommandBuffer& command, DrawRender
(blend.separate_alpha_blend && (BlendFactorIsDualSource(blend.alpha_srcblend) ||
BlendFactorIsDualSource(blend.alpha_destblend))));
if (pixel_info.dual_source_blending) {
// MRT1 supplies the second blend source for target 0.
// MRT1 supplies the second blend source for target 0; blended targets are not integer.
pixel_info.target_output_mode[1] = pixel_info.target_output_mode[0];
pixel_info.target_export_mapping[1] = pixel_info.target_export_mapping[0];
pixel_info.target_uint_mask &= ~2u;
pixel_info.target_sint_mask &= ~2u;
} else if (blend.enable && !context.GetRenderTarget(0).info.blend_bypass &&
pixel_info.target_output_mode[0] != 0 && pixel_info.target_output_mode[0] != 7 &&
std::all_of(std::begin(pixel_info.target_output_mode) + 1,
@@ -843,6 +843,11 @@ bool RenderExecutor::PrepareDrawRenderState(CommandBuffer& buffer, const DrawCal
LogDrawPhase(draw.Name(), "GetGraphicsPrograms");
}
m_context.GetPipelineCache().GetGraphicsPrograms(buffer, state);
// Vertex programs beyond the host mesh limits or with unsupported call targets are not
// compiled.
if (state.vertex_info[0].stage.module == nullptr) {
return false;
}
if (draw.IsIndexed()) {
LogDrawPhase(draw.Name(), "ResolveRenderColorTarget");
}
@@ -1156,7 +1161,6 @@ void RenderExecutor::ExecutePreparedDraw(uint64_t submit_id, CommandBuffer& buff
if (mesh_active) {
draw_data = {draw.index_count,
static_cast<uint32_t>(emit.vertex_offset),
emit.first_instance,
index_source.guest_element_size,
static_cast<uint32_t>(index_source.address),
static_cast<uint32_t>(index_source.address >> 32u)};
@@ -1475,8 +1479,9 @@ bool RenderExecutor::ResolveColorTargets(CommandBuffer& buffer, uint32_t render_
cache.MarkGpuWritten(dst.image_id);
auto& source = cache.GetImage(src.image_id);
auto& destination = cache.GetImage(dst.image_id);
destination.Resolve(source, {src.guest_mip_level, 1, src.guest_array_layer, 1},
{dst.guest_mip_level, 1, dst.guest_array_layer, 1});
destination.Resolve(source,
{src.desc.view_info.base_level, 1, src.desc.view_info.base_layer, 1},
{dst.desc.view_info.base_level, 1, dst.desc.view_info.base_layer, 1});
return true;
}
@@ -337,6 +337,7 @@ static void VulkanFindPhysicalDevice(vk::Instance instance, vk::SurfaceKHR surfa
check_feature(device_features2.features.shaderImageGatherExtended,
"shaderImageGatherExtended");
check_feature(device_features2.features.shaderResourceMinLod, "shaderResourceMinLod");
check_feature(device_features2.features.independentBlend, "independentBlend");
check_feature(device_features2.features.tessellationShader, "tessellationShader");
@@ -636,6 +637,7 @@ static vk::Device VulkanCreateDevice(GraphicContext& graphics,
device_features.shaderStorageImageMultisample =
supported_features2.features.shaderStorageImageMultisample;
device_features.shaderImageGatherExtended = VK_TRUE;
device_features.shaderResourceMinLod = VK_TRUE;
device_features.independentBlend = VK_TRUE;
device_features.dualSrcBlend = VK_TRUE;
device_features.tessellationShader = VK_TRUE;
@@ -733,6 +735,15 @@ static vk::Device VulkanCreateDevice(GraphicContext& graphics,
conditional_rendering.pNext = const_cast<void*>(create_info.pNext);
create_info.pNext = &conditional_rendering;
}
// The renderer only uses the Vulkan 1.3 core dynamic states, but RADV (Mesa 26.3) creates its
// PS epilog cache only when this EXT feature is enabled and crashes in vkCmdDraw when a
// depth- or stencil-writing fragment shader needs an epilog.
vk::PhysicalDeviceExtendedDynamicStateFeaturesEXT extended_dynamic_state {};
extended_dynamic_state.extendedDynamicState = VK_TRUE;
if (HasExtension(device_extensions, VK_EXT_EXTENDED_DYNAMIC_STATE_EXTENSION_NAME)) {
extended_dynamic_state.pNext = const_cast<void*>(create_info.pNext);
create_info.pNext = &extended_dynamic_state;
}
create_info.pQueueCreateInfos = &queue_create_info;
create_info.queueCreateInfoCount = 1;
create_info.enabledExtensionCount = static_cast<uint32_t>(device_extensions.size());
@@ -1111,7 +1122,8 @@ void WindowContext::CreateVulkan() {
VK_KHR_WORKGROUP_MEMORY_EXPLICIT_LAYOUT_EXTENSION_NAME,
VK_EXT_DEPTH_RANGE_UNRESTRICTED_EXTENSION_NAME,
VK_KHR_FRAGMENT_SHADER_BARYCENTRIC_EXTENSION_NAME,
VK_EXT_CONDITIONAL_RENDERING_EXTENSION_NAME}) {
VK_EXT_CONDITIONAL_RENDERING_EXTENSION_NAME,
VK_EXT_EXTENDED_DYNAMIC_STATE_EXTENSION_NAME}) {
if (HasExtension(available_extensions, extension)) {
device_extensions.push_back(extension);
}
@@ -467,6 +467,7 @@ Decoder::Program DecodeFusedProgram(std::span<const uint32_t> front, std::span<c
void PrepareCallTargets(ShaderSource& source, const CompileOptions& options) {
using namespace IR;
EXIT_NOT_IMPLEMENTED(options.stage != ShaderType::Compute);
source.wave_size = options.wave_size;
for (uint32_t i = 0; i < source.decoded.instructions.size(); ++i) {
if (source.decoded.instructions[i].opcode != Decoder::Opcode::S_SWAPPC_B64) continue;
source.calls.push_back({.instruction = i});
@@ -503,7 +504,8 @@ void PrepareCallTargets(ShaderSource& source, const CompileOptions& options) {
};
Register = [&](uint32_t before, uint32_t code) -> Value {
std::vector<uint32_t> crossed_calls;
const auto index = CFG::FindScalarDefinition(source.decoded, graph, before, code, crossed_calls);
const auto index = CFG::FindScalarDefinition(source.decoded, graph, before, code,
source.wave_size, crossed_calls);
for (const auto crossed: crossed_calls) {
std::ranges::find(source.calls, crossed, &ShaderSource::Call::instruction)->preserved_registers.set(code);
}
@@ -702,10 +704,11 @@ const Decoder::Program& RefreshShaderSource(ShaderSource& source, const IR::SrtR
EXIT_IF(function_pc > UINT32_MAX - function_size);
const auto function_offset = static_cast<uint32_t>(linked.code.size());
for (auto inst: function) {
EXIT_IF(CFG::MayWriteScalarRegister(inst, call.dst.reg) ||
CFG::MayWriteScalarRegister(inst, call.dst.reg + 1u));
EXIT_IF(CFG::MayWriteScalarRegister(inst, call.dst.reg, source.wave_size) ||
CFG::MayWriteScalarRegister(inst, call.dst.reg + 1u, source.wave_size));
for (uint32_t code = 0; code < site.preserved_registers.size(); ++code) {
if (site.preserved_registers[code] && CFG::MayWriteScalarRegister(inst, code))
if (site.preserved_registers[code] &&
CFG::MayWriteScalarRegister(inst, code, source.wave_size))
EXIT("shader call at pc 0x%08x clobbers scalar target dependency s%u", call.pc, code);
}
linked.code.insert(linked.code.end(), inst.raw, inst.raw + inst.word_count);
@@ -62,6 +62,7 @@ struct ShaderSource {
IR::ResourcePlan call_targets;
std::vector<Call> calls;
uint32_t unsupported_call_pc = UINT32_MAX;
uint32_t wave_size = 64;
std::optional<Linked> linked;
uint64_t revision = 0;
std::vector<std::pair<uint64_t, uint64_t>> reads;
@@ -30,6 +30,13 @@ bool PixelParameterIsCustom(const EmitterState& state, uint32_t attr) {
ShaderPixelParameterIsCustom(*state.input_info.pixel, attr);
}
uint32_t MrtOutputType(EmitterState& state, uint32_t index) {
const auto& pixel = *state.input_info.pixel;
if ((pixel.target_sint_mask & (1u << index)) != 0u) return TypeI32Vector(state, 4);
if ((pixel.target_uint_mask & (1u << index)) != 0u) return TypeU32Vector(state, 4);
return TypeF32Vector(state, 4);
}
uint32_t OutputVariableForExport(const EmitterState& state, const IR::ExportInfo& exp) {
if (exp.kind == IR::ExportTargetKind::Position && exp.index == 0) {
return state.per_vertex_variable;
@@ -291,9 +291,10 @@ uint32_t ExportRawComponent(ValueEmitContext& ctx, uint32_t vector, uint32_t com
}
uint32_t ExportVector(ValueEmitContext& ctx, uint32_t data, const IR::ExportInfo& exp,
bool uint_output) {
auto& state = ctx.state;
if (exp.compr && !uint_output) {
uint32_t vector_type) {
auto& state = ctx.state;
const bool integer_output = vector_type != TypeF32Vector(state, 4);
if (exp.compr && !integer_output) {
auto unpack = GLSLstd450UnpackHalf2x16;
switch (MrtOutputMode(state, exp)) {
case 5: unpack = GLSLstd450UnpackUnorm2x16; break;
@@ -330,9 +331,11 @@ uint32_t ExportVector(ValueEmitContext& ctx, uint32_t data, const IR::ExportInfo
ConstantU32(state, 0),
ConstantU32(state, 0),
ConstantU32(state, 0),
ConstantU32(state, uint_output ? 1u : 0x3f800000u),
ConstantU32(state, integer_output ? 1u : 0x3f800000u),
};
if (exp.compr) {
const auto extract = vector_type == TypeU32Vector(state, 4) ? spv::OpBitFieldUExtract
: spv::OpBitFieldSExtract;
for (uint32_t pair = 0; pair < 2u; pair++) {
if ((exp.en & (3u << (pair * 2u))) == 0u) {
continue;
@@ -344,8 +347,8 @@ uint32_t ExportVector(ValueEmitContext& ctx, uint32_t data, const IR::ExportInfo
continue;
}
raw[component] = state.builder.AllocateId();
state.builder.AddFunction(spv::OpBitFieldUExtract, TypeU32(state), raw[component],
packed, ConstantU32(state, lane * 16u),
state.builder.AddFunction(extract, TypeU32(state), raw[component], packed,
ConstantU32(state, lane * 16u),
ConstantU32(state, 16));
}
}
@@ -360,12 +363,10 @@ uint32_t ExportVector(ValueEmitContext& ctx, uint32_t data, const IR::ExportInfo
state.builder.AddFunction(spv::OpCompositeConstruct, TypeU32Vector(state, 4), data,
raw[0], raw[1], raw[2], raw[3]);
}
if (uint_output) {
if (vector_type == TypeU32Vector(state, 4)) {
return data;
}
const auto vector = state.builder.AllocateId();
state.builder.AddFunction(spv::OpBitcast, TypeF32Vector(state, 4), vector, data);
return vector;
return Unary(state, spv::OpBitcast, vector_type, data);
}
uint32_t DistancePointer(EmitterState& state, IR::StageOutputKind kind, uint32_t index) {
@@ -563,11 +564,11 @@ void EmitSetAttribute(ValueEmitContext& ctx, const IR::Inst& inst) {
if (state.program.stage != ShaderType::Mesh && variable == 0) {
return;
}
const bool uint_output = MrtOutputMode(state, exp) == 7u;
const auto vector_type = uint_output ? TypeU32Vector(state, 4) : TypeF32Vector(state, 4);
auto value = ExportVector(ctx, data, exp, uint_output);
const auto vector_type = MrtOutputMode(state, exp) != 0u ? MrtOutputType(state, exp.index)
: TypeF32Vector(state, 4);
auto value = ExportVector(ctx, data, exp, vector_type);
if (state.program.stage == ShaderType::Pixel && exp.kind == IR::ExportTargetKind::Mrt &&
exp.index == 0 && !uint_output &&
exp.index == 0 && vector_type == TypeF32Vector(state, 4) &&
state.input_info.pixel->alpha_blend_source != ShaderAlphaBlendSource::None) {
// Broadcast logical alpha before swizzling the primary output.
const auto blend_output =
@@ -711,9 +712,16 @@ void EmitBarrier(EmitterState& state) {
return;
}
const auto memory_scope = tessellation ? spv::ScopeInvocation : spv::ScopeWorkgroup;
const auto memory = state.lds_storage_class == spv::StorageClassStorageBuffer
? spv::MemorySemanticsUniformMemoryMask
: spv::MemorySemanticsWorkgroupMemoryMask;
auto memory = state.lds_storage_class == spv::StorageClassStorageBuffer
? spv::MemorySemanticsUniformMemoryMask
: spv::MemorySemanticsWorkgroupMemoryMask;
// Shaders read other waves' buffer and image stores with plain loads right after S_BARRIER.
const auto written = [](const auto& resource) { return resource.written; };
if (std::ranges::any_of(state.program.info.buffers, written) ||
std::ranges::any_of(state.program.info.images, written)) {
memory = memory | spv::MemorySemanticsUniformMemoryMask |
spv::MemorySemanticsImageMemoryMask;
}
const auto semantics = tessellation ? spv::MemorySemanticsMaskNone
: spv::MemorySemanticsAcquireReleaseMask | memory;
state.builder.AddFunction(spv::OpControlBarrier, ConstantU32(state, spv::ScopeWorkgroup),
@@ -811,6 +819,13 @@ uint32_t EmitBallot(ValueEmitContext& ctx, IR::Value predicate) {
return ctx.Ballot(predicate);
}
uint32_t EmitLiveMask(EmitterState& state) {
const auto helper =
EmitNative<spv::OpLoad, IR::Type::U1>(state, state.helper_invocation_variable);
return EmitNative<spv::OpGroupNonUniformBallot, IR::Type::U32x4>(
state, ConstantU32(state, spv::ScopeSubgroup), EmitLogicalNotBool(state, helper));
}
uint32_t EmitReadFirstLane(ValueEmitContext& ctx, const IR::Inst& inst) {
const auto ballot = ctx.Ballot(inst.Arg(1));
const auto lane = ctx.FirstLane(ballot);
@@ -821,18 +836,28 @@ uint32_t EmitReadLane(ValueEmitContext& ctx, const IR::Inst& inst) {
return ctx.Shuffle(inst, 0, ctx.Arg(inst, 1));
}
uint32_t EmitSubgroupUMinU32(ValueEmitContext& ctx, const IR::Inst& inst) {
template <spv::Op Reduce, auto Combine>
static uint32_t EmitSubgroupReduceU32(ValueEmitContext& ctx, const IR::Inst& inst) {
auto& state = ctx.state;
auto value = ctx.Arg(inst, 0);
if (ctx.other_half != nullptr) {
value = EmitUMin32(state, ctx.HalfArg(inst, 0, 0), ctx.HalfArg(inst, 0, 1));
value = Combine(state, ctx.HalfArg(inst, 0, 0), ctx.HalfArg(inst, 0, 1));
}
const auto result = state.builder.AllocateId();
state.builder.AddFunction(spv::OpGroupNonUniformUMin, TypeU32(state), result,
ConstantU32(state, spv::ScopeSubgroup), spv::GroupOperationReduce, value);
state.builder.AddFunction(Reduce, TypeU32(state), result,
ConstantU32(state, spv::ScopeSubgroup), spv::GroupOperationReduce,
value);
return result;
}
uint32_t EmitSubgroupUMinU32(ValueEmitContext& ctx, const IR::Inst& inst) {
return EmitSubgroupReduceU32<spv::OpGroupNonUniformUMin, EmitUMin32>(ctx, inst);
}
uint32_t EmitSubgroupUMaxU32(ValueEmitContext& ctx, const IR::Inst& inst) {
return EmitSubgroupReduceU32<spv::OpGroupNonUniformUMax, EmitUMax32>(ctx, inst);
}
uint32_t EmitWriteLane(ValueEmitContext& ctx, const IR::Inst& inst) {
auto& state = ctx.state;
const auto hit = state.builder.AllocateId();
@@ -77,6 +77,7 @@ ImageSampleLayout Layout(const IR::MemoryInfo& mem) {
layout.coord = cursor;
cursor += info.coordinate_components;
if (HasFlag(mem, Decoder::ImageSampleFlagLod)) layout.lod = cursor++;
if (HasFlag(mem, Decoder::ImageSampleFlagLodClamp)) layout.clamp = cursor++;
return layout;
}
@@ -1069,7 +1070,12 @@ void EmitImage(ValueEmitContext& ctx, const IR::Inst& inst) {
dref_value = AddressF32(ctx, mem, *address, layout.dref);
}
}
std::array<uint32_t, 4> operands {};
// _cl clamps the computed LOD from below.
uint32_t min_lod = 0;
if (layout.clamp != NoImageComponent) {
min_lod = AddressF32(ctx, mem, *address, layout.clamp);
}
std::array<uint32_t, 5> operands {};
size_t operand_count = 0;
if (HasFlag(mem, Decoder::ImageSampleFlagDerivative)) {
operands[0] = spv::ImageOperandsGradMask;
@@ -1081,6 +1087,11 @@ void EmitImage(ValueEmitContext& ctx, const IR::Inst& inst) {
if (HasFlag(mem, Decoder::ImageSampleFlagLod) && layout.lod != NoImageComponent) {
lod = AddressF32(ctx, mem, *address, layout.lod);
}
// SPIR-V takes MinLod only with implicit LOD or Grad.
if (min_lod != 0) {
lod = EmitGlsl<GLSLstd450FMax, IR::Type::F32>(state, lod, min_lod);
min_lod = 0;
}
operands[0] = spv::ImageOperandsLodMask;
operands[1] = lod;
operand_count = 2;
@@ -1104,6 +1115,13 @@ void EmitImage(ValueEmitContext& ctx, const IR::Inst& inst) {
sample_operands[sample_operand_count++] = PackedOffset(
ctx, offset_value, candidate.dimension, sample_operands[0]);
}
// MinLod follows the offset in operand order.
if (min_lod != 0) {
state.builder.RequireCapability(spv::CapabilityMinLod);
sample_operand_count = std::max(size_t{1}, sample_operand_count);
sample_operands[0] |= spv::ImageOperandsMinLodMask;
sample_operands[sample_operand_count++] = min_lod;
}
const auto coord =
CoordF32(ctx, mem, *address, layout.coord,
ImageDimensionInfoFor(candidate.dimension).coordinate_components,
@@ -230,10 +230,12 @@ uint32_t EmitDppUpdateU32(ValueEmitContext& ctx, const IR::Inst& in
uint32_t EmitWqmU64(EmitterState& state, uint32_t value);
uint32_t EmitLaneId(EmitterState& state);
uint32_t EmitBallot(ValueEmitContext& ctx, IR::Value predicate);
uint32_t EmitLiveMask(EmitterState& state);
uint32_t EmitConditionRef(ValueEmitContext& ctx, const IR::Inst& inst);
uint32_t EmitReadFirstLane(ValueEmitContext& ctx, const IR::Inst& inst);
uint32_t EmitReadLane(ValueEmitContext& ctx, const IR::Inst& inst);
uint32_t EmitSubgroupUMinU32(ValueEmitContext& ctx, const IR::Inst& inst);
uint32_t EmitSubgroupUMaxU32(ValueEmitContext& ctx, const IR::Inst& inst);
uint32_t EmitWriteLane(ValueEmitContext& ctx, const IR::Inst& inst);
uint32_t EmitPermlane16U32(ValueEmitContext& ctx, const IR::Inst& inst);
uint32_t EmitGetAttribute(ValueEmitContext& ctx, const IR::Inst& inst);
@@ -112,6 +112,7 @@ struct EmitterState {
uint32_t lane_half = 0;
std::array<BufferDefinition, 4> storage_buffers {};
std::vector<uint32_t> memory_byte_offsets;
std::vector<uint32_t> memory_byte_sizes;
uint32_t bda_pagetable_variable = 0;
uint32_t fault_buffer_variable = 0;
uint32_t bda_pointer_function = 0;
@@ -145,6 +146,7 @@ struct EmitterState {
const IR::Block* current_block = nullptr;
uint32_t pixel_valid_mask_variable = 0;
uint32_t subgroup_local_invocation_id_variable = 0;
uint32_t helper_invocation_variable = 0;
uint32_t per_vertex_variable = 0;
uint32_t point_size_variable = 0;
uint32_t clip_distance_variable = 0;
@@ -282,6 +284,7 @@ struct ImageSampleLayout {
uint32_t bias = NoImageComponent;
uint32_t coord = 0;
uint32_t lod = NoImageComponent;
uint32_t clamp = NoImageComponent;
uint32_t grad_x = NoImageComponent;
uint32_t grad_y = NoImageComponent;
};
@@ -305,6 +308,7 @@ uint32_t VertexParameterScalarType(EmitterState& state, VertexInputScalarKind ki
uint32_t OutputVariableForExport(const EmitterState& state, const IR::ExportInfo& exp);
uint32_t MrtOutputType(EmitterState& state, uint32_t index);
uint32_t ConstantU32(EmitterState& state, uint32_t value);
@@ -414,7 +418,7 @@ Prospero::BufferFormat StorageBufferFormat(const EmitterState& state, const IR::
uint32_t SharedIndirectBufferResource(const EmitterState& state, uint32_t resource);
void EmitMemoryOffsets(EmitterState& state);
void EmitMemoryBindings(EmitterState& state);
uint32_t LdsDwordCount(const EmitterState& state);
void EnsureLdsStorage(EmitterState& state);
@@ -1541,16 +1541,16 @@ uint32_t EmitAppendConsume(ValueEmitContext& ctx, const IR::Inst& inst) {
const auto index =
Binary(state, spv::OpShiftRightLogical, TypeU32(state), address, ConstantU32(state, 2));
const auto access = PrepareMemoryResourceAccess(state, mem);
const auto exec = ctx.Arg(inst, 1);
const auto ballot = ctx.Ballot(inst.Arg(1));
const auto low = state.builder.AllocateId();
const auto high = state.builder.AllocateId();
state.builder.AddFunction(spv::OpCompositeExtract, TypeU32(state), low, ballot, 0);
state.builder.AddFunction(spv::OpCompositeExtract, TypeU32(state), high, ballot, 1);
// Elect and count from the guest EXEC words, which V_MBCNT also ranks lanes by.
const auto low = ctx.Arg(inst, 1);
const auto high = ctx.Arg(inst, 2);
const auto count = Binary(state, spv::OpIAdd, TypeU32(state),
Unary(state, spv::OpBitCount, TypeU32(state), low),
Unary(state, spv::OpBitCount, TypeU32(state), high));
const auto first = ctx.FirstLane(ballot);
const auto mask = state.builder.AllocateId();
state.builder.AddFunction(spv::OpCompositeConstruct, TypeU32Vector(state, 4), mask, low, high,
ConstantU32(state, 0), ConstantU32(state, 0));
const auto first = ctx.FirstLane(mask);
const auto source_lane =
state.lane_count == 2
? Binary(state, spv::OpBitwiseAnd, TypeU32(state), first, ConstantU32(state, 31))
@@ -1564,9 +1564,7 @@ uint32_t EmitAppendConsume(ValueEmitContext& ctx, const IR::Inst& inst) {
const auto condition = AndCondition(
state, is_first,
AndCondition(state,
state.lane_count == 2 ? Binary(state, spv::OpINotEqual, TypeBool(state), count,
ConstantU32(state, 0))
: exec,
Binary(state, spv::OpINotEqual, TypeBool(state), count, ConstantU32(state, 0)),
bounds));
const auto atomic = EmitValueOrZeroIfCondition(state, condition, [&]() {
const auto value = state.builder.AllocateId();
@@ -154,9 +154,10 @@ uint32_t SharedIndirectBufferResource(const EmitterState& state, uint32_t resour
return representative;
}
void EmitMemoryOffsets(EmitterState& state) {
void EmitMemoryBindings(EmitterState& state) {
const auto count = state.program.bindings.descriptor_counts[static_cast<size_t>(IR::DescriptorBindingKind::Buffers)];
state.memory_byte_offsets.resize(count);
state.memory_byte_sizes.resize(count);
std::vector<bool> dynamic_offsets(count);
for (uint32_t resource = 0; resource < state.program.info.buffers.size(); ++resource) {
if (SharedIndirectBufferResource(state, resource) == UINT32_MAX) continue;
@@ -174,6 +175,8 @@ void EmitMemoryOffsets(EmitterState& state) {
if (dynamic_offsets[i + lane]) continue;
state.memory_byte_offsets[i + lane] = EmitNative<spv::OpBitFieldUExtract, IR::Type::U32>(
state, word, ConstantU32(state, lane * 8u), ConstantU32(state, 8u));
state.memory_byte_sizes[i + lane] =
EmitShaderDataDwordLoad(state, state.program.bindings.MemorySizeDword() + i + lane);
}
}
}
@@ -257,22 +260,28 @@ MemoryResourceAccess PrepareStorageBufferResourceAccess(EmitterState& state,
state.builder.RequireCapability(spv::CapabilityShaderNonUniform);
state.builder.RequireCapability(spv::CapabilityStorageBufferArrayNonUniformIndexing);
state.builder.AddAnnotation(spv::OpDecorate, access.object_pointer, spv::DecorationNonUniform);
const auto offsets_start = state.program.bindings.memory_offset_dword +
(state.program.bindings.UsesPushData() ? state.program.bindings.push_data_start_dword : 0u);
const auto& bindings = state.program.bindings;
const auto data_start = bindings.UsesPushData() ? bindings.push_data_start_dword : 0u;
const auto word = EmitShaderDataDwordLoadAt(state, EmitAddU32(
state, ConstantU32(state, offsets_start),
state, ConstantU32(state, bindings.memory_offset_dword + data_start),
Binary(state, spv::OpShiftRightLogical, TypeU32(state), array_index, ConstantU32(state, 2))));
const auto shift = Binary(state, spv::OpShiftLeftLogical, TypeU32(state),
Binary(state, spv::OpBitwiseAnd, TypeU32(state), array_index, ConstantU32(state, 3)),
ConstantU32(state, 3));
access.byte_offset = EmitNative<spv::OpBitFieldUExtract, IR::Type::U32>(
state, word, shift, ConstantU32(state, 8));
access.length = EmitShaderDataDwordLoadAt(state, EmitAddU32(
state, ConstantU32(state, bindings.MemorySizeDword() + data_start), array_index));
} else {
access.byte_offset = state.memory_byte_offsets[static_index];
access.length = state.memory_byte_sizes[static_index];
}
// storage_buffers[i] views the binding as 1 << i byte elements.
const auto shift = static_cast<uint32_t>(&buffer - state.storage_buffers.data());
if (shift != 0u) {
access.length = Binary(state, spv::OpShiftRightLogical, TypeU32(state), access.length,
ConstantU32(state, shift));
}
access.length = state.builder.AllocateId();
state.builder.AddFunction(spv::OpArrayLength, TypeU32(state), access.length,
access.object_pointer, 0);
return access;
}
@@ -595,6 +595,13 @@ void DefineInputs(EmitterState& state) {
state.builder.AddAnnotation(spv::OpDecorate, variable, spv::DecorationFlat);
}
}
if (state.program.info.helper_invocation) {
const auto variable = DefineInterfaceVariable(state, TypeBool(state), spv::StorageClassInput,
"gl_HelperInvocation");
state.helper_invocation_variable = variable;
state.builder.AddAnnotation(spv::OpDecorate, variable, spv::DecorationBuiltIn,
spv::BuiltInHelperInvocation);
}
}
void DefineOutputs(EmitterState& state) {
@@ -691,13 +698,15 @@ void DefineOutputs(EmitterState& state) {
break;
case IR::StageOutputKind::Parameter:
case IR::StageOutputKind::Mrt: {
const bool uint_output =
binding.kind == IR::StageOutputKind::Mrt &&
state.program.stage == ShaderType::Pixel &&
binding.index < std::size(state.input_info.pixel->target_output_mode) &&
(state.input_info.pixel->target_output_mode[binding.index] == 7u ||
(state.input_info.pixel->target_packed_unorm_mask & (1u << binding.index)) != 0u);
const auto type = uint_output ? TypeU32Vector(state, 4) : TypeF32Vector(state, 4);
auto type = TypeF32Vector(state, 4);
if (binding.kind == IR::StageOutputKind::Mrt &&
state.program.stage == ShaderType::Pixel && binding.index < 8u) {
// Packed UNorm stores its encoded bits.
const auto bit = 1u << binding.index;
type = (state.input_info.pixel->target_packed_unorm_mask & bit) != 0u
? TypeU32Vector(state, 4)
: MrtOutputType(state, binding.index);
}
binding.variable_id = DefineInterfaceVariable(state, type, spv::StorageClassOutput,
binding.debug_name.c_str());
const bool dual_source = binding.kind == IR::StageOutputKind::Mrt &&
@@ -235,7 +235,7 @@ void EmitDirectInstruction(ValueEmitContext& ctx, const IR::Inst& inst) {
const auto op = inst.GetOpcode();
if (ctx.half != 0 &&
(op == Op::Ballot || op == Op::ReadFirstLane || op == Op::SubgroupUMinU32 ||
op == Op::ReadConstBuffer || op == Op::ReadRealtimeClock ||
op == Op::SubgroupUMaxU32 || op == Op::ReadConstBuffer || op == Op::ReadRealtimeClock ||
(op == Op::LoadAddressU32 && ctx.Memory(inst).kind == IR::ResourceKind::ScalarAddress))) {
// Whole-wave results, clocks and SMEM loads (SGPR operands, no EXEC) match in both halves.
ctx.Define(inst, ctx.other_half->Result(inst));
@@ -746,7 +746,7 @@ void EmitProgram(EmitterState& state) {
state.lds_base_dwords = EmitBinaryU32(state, spv::OpIMul, index,
ConstantU32(state, LdsDwordCount(state)));
}
EmitMemoryOffsets(state);
EmitMemoryBindings(state);
if (program.blocks.empty()) {
EmitReturn(ctx);
} else if (state.program.dispatcher_fallback) {
@@ -1710,21 +1710,25 @@ Graph Structurize(const Graph& graph) {
return GotoStructurizer(graph).Run();
}
bool MayWriteScalarRegister(const Decoder::Instruction& inst, uint32_t code) {
// Vector comparisons and carry results can write a scalar pair in wave64.
bool MayWriteScalarRegister(const Decoder::Instruction& inst, uint32_t code, uint32_t wave_size) {
// A vector lane mask (comparison or carry) spans wave_size / 32 SGPRs; a lane read writes one.
if (inst.family == Decoder::Family::VOP1 || inst.family == Decoder::Family::VOP2 ||
inst.family == Decoder::Family::VOPC || inst.family == Decoder::Family::VOP3 ||
inst.family == Decoder::Family::VOP3P) {
const bool lane_read =
inst.opcode == Opcode::V_READLANE_B32 || inst.opcode == Opcode::V_READFIRSTLANE_B32;
const uint32_t size = lane_read ? 1u : wave_size / 32u;
uint32_t destination = 0;
for (const auto* operand: {&inst.dst, &inst.dst2})
if (ScalarOperandCode(*operand, destination) && code >= destination && code <= destination + 1u)
if (ScalarOperandCode(*operand, destination) && code >= destination && code < destination + size)
return true;
}
return InstructionWritesScalarCode(inst, code);
}
uint32_t FindScalarDefinition(const Decoder::Program& program, const Graph& graph,
uint32_t before, uint32_t code, std::vector<uint32_t>& crossed_calls) {
uint32_t before, uint32_t code, uint32_t wave_size,
std::vector<uint32_t>& crossed_calls) {
EXIT_IF(before >= program.instructions.size());
const auto found = std::ranges::find_if(graph.blocks, [before](const auto& block) {
return block.inst_begin <= before && before < block.inst_end;
@@ -1743,11 +1747,11 @@ uint32_t FindScalarDefinition(const Decoder::Program& program, const Graph& grap
for (uint32_t i = point.second; i > block->inst_begin;) {
const auto& inst = program.instructions[--i];
if (inst.opcode == Opcode::S_SWAPPC_B64) {
EXIT_NOT_IMPLEMENTED(MayWriteScalarRegister(inst, code));
EXIT_NOT_IMPLEMENTED(MayWriteScalarRegister(inst, code, wave_size));
crossed_calls.push_back(i);
continue;
}
if (MayWriteScalarRegister(inst, code)) { index = i; break; }
if (MayWriteScalarRegister(inst, code, wave_size)) { index = i; break; }
}
if (index != UINT32_MAX || block->id == graph.entry_block) {
EXIT_IF(definition && *definition != index);
@@ -113,9 +113,10 @@ struct Graph {
};
Graph BuildGraph(const Decoder::Program& program);
bool MayWriteScalarRegister(const Decoder::Instruction& inst, uint32_t code);
bool MayWriteScalarRegister(const Decoder::Instruction& inst, uint32_t code, uint32_t wave_size);
uint32_t FindScalarDefinition(const Decoder::Program& program, const Graph& graph,
uint32_t before, uint32_t code, std::vector<uint32_t>& crossed_calls);
uint32_t before, uint32_t code, uint32_t wave_size,
std::vector<uint32_t>& crossed_calls);
// Returns structured control flow or failure diagnostics without changing the native graph.
// On failure, failure_block is an original block ID or UINT32_MAX.
Graph Structurize(const Graph& graph);
@@ -73,7 +73,7 @@ constexpr uint32_t ImageSampleAddressComponents(uint32_t flags, ImageDimension d
if ((flags & ImageSampleFlagBias) != 0) {
components++;
}
if ((flags & ImageSampleFlagLod) != 0) {
if ((flags & (ImageSampleFlagLod | ImageSampleFlagLodClamp)) != 0) {
components++;
}
if ((flags & ImageSampleFlagDerivative) != 0) {
@@ -258,7 +258,9 @@ void DecodeScalarSource(uint32_t code, uint32_t pc, Operand& operand) {
case 126u: operand.kind = OperandKind::ExecLo; return;
case 127u: operand.kind = OperandKind::ExecHi; return;
case 235u: operand.kind = OperandKind::SharedBase; return;
case 236u: operand.kind = OperandKind::SharedLimit; return;
case 237u: operand.kind = OperandKind::PrivateBase; return;
case 238u: operand.kind = OperandKind::PrivateLimit; return;
case 239u: operand.kind = OperandKind::PopsExitingWaveId; return;
case 248u:
operand.kind = OperandKind::FloatInlineConstant;
@@ -459,7 +461,9 @@ std::string OperandToString(const Operand& operand) {
case OperandKind::M0: text = "m0"; break;
case OperandKind::PopsExitingWaveId: text = "pops_exiting_wave_id"; break;
case OperandKind::SharedBase: text = "shared_base"; break;
case OperandKind::SharedLimit: text = "shared_limit"; break;
case OperandKind::PrivateBase: text = "private_base"; break;
case OperandKind::PrivateLimit: text = "private_limit"; break;
case OperandKind::Null: text = "null"; break;
default: text = "unknown"; break;
}
@@ -766,7 +766,9 @@ enum class OperandKind {
M0,
PopsExitingWaveId,
SharedBase,
SharedLimit,
PrivateBase,
PrivateLimit,
Null,
Vgpr,
};
@@ -5,30 +5,20 @@
namespace Libs::Graphics::ShaderRecompiler::Frontend {
void Translator::EmitCompareResult(const Decoder::Instruction& inst, IR::U1 value, bool scalar,
bool cmpx) {
void Translator::EmitCompareResult(const Decoder::Instruction& inst, IR::U1 value, bool scalar) {
if (scalar) {
ir.SetScc(value);
return;
}
const auto masked = ir.LogicalAnd(ir.GetExec(), value);
if (cmpx) {
const auto mask = BallotMask(masked);
ir.SetExec(masked);
ir.SetExecLo(mask[0]);
ir.SetExecHi(mask[1]);
return;
}
WriteMask(inst.dst, masked);
WriteVectorMask(inst.dst, value);
}
void Translator::EmitCompareConstant(const Decoder::Instruction& inst, bool value, bool scalar,
bool cmpx) {
EmitCompareResult(inst, IR::U1(IR::Value(value)), scalar, cmpx);
void Translator::EmitCompareConstant(const Decoder::Instruction& inst, bool value, bool scalar) {
EmitCompareResult(inst, IR::U1(IR::Value(value)), scalar);
}
void Translator::EmitIntegerCompare(const Decoder::Instruction& inst, IR::ValueOpcode opcode,
IR::Type type, bool scalar, bool cmpx, bool swap_operands) {
IR::Type type, bool scalar, bool swap_operands) {
// The equality compares share their IR opcode with the unsigned ones.
const bool signed_64 = opcode == IR::ValueOpcode::SLessThan64 ||
opcode == IR::ValueOpcode::SLessThanEqual64 ||
@@ -49,18 +39,18 @@ void Translator::EmitIntegerCompare(const Decoder::Instruction& inst, IR::ValueO
if (swap_operands) {
std::swap(lhs, rhs);
}
EmitCompareResult(inst, IR::U1(ir.Emit(opcode, {lhs, rhs})), scalar, cmpx);
EmitCompareResult(inst, IR::U1(ir.Emit(opcode, {lhs, rhs})), scalar);
}
void Translator::EmitInteger16Compare(const Decoder::Instruction& inst, IR::ValueOpcode opcode,
bool signed_value, bool cmpx) {
bool signed_value) {
const auto lhs = ReadU16AsU32(inst.src0, signed_value);
const auto rhs = ReadU16AsU32(inst.src1, signed_value);
EmitCompareResult(inst, IR::U1(ir.Emit(opcode, {lhs, rhs})), false, cmpx);
EmitCompareResult(inst, IR::U1(ir.Emit(opcode, {lhs, rhs})), false);
}
void Translator::EmitFloatCompare(const Decoder::Instruction& inst, IR::ValueOpcode opcode,
bool half, bool cmpx) {
bool half) {
const auto type = IR::ArgTypeOf(opcode, 0);
const auto lhs =
half ? IR::Value(ReadF16AsF32(inst.src0)) : ReadOperand(inst.src0, type);
@@ -68,10 +58,10 @@ void Translator::EmitFloatCompare(const Decoder::Instruction& inst, IR::ValueOpc
half ? IR::Value(ReadF16AsF32(inst.src1)) : ReadOperand(inst.src1, type);
const IR::FPCompareFlags flags{
.flush_input_denorms = !half && type == IR::Type::F32 && flush_f32_inputs};
EmitCompareResult(inst, IR::U1(ir.Emit(opcode, {lhs, rhs}, flags)), false, cmpx);
EmitCompareResult(inst, IR::U1(ir.Emit(opcode, {lhs, rhs}, flags)), false);
}
void Translator::EmitFloatOrderedCompare(const Decoder::Instruction& inst, bool ordered, bool cmpx,
void Translator::EmitFloatOrderedCompare(const Decoder::Instruction& inst, bool ordered,
bool is64) {
const auto type = is64 ? IR::Type::F64 : IR::Type::F32;
const auto is_nan_op = is64 ? IR::ValueOpcode::FPIsNan64 : IR::ValueOpcode::FPIsNan32;
@@ -79,16 +69,16 @@ void Translator::EmitFloatOrderedCompare(const Decoder::Instruction& inst, bool
const auto rhs = ReadOperand(inst.src1, type);
const auto unordered = ir.LogicalOr(IR::U1(ir.Emit(is_nan_op, {lhs})),
IR::U1(ir.Emit(is_nan_op, {rhs})));
EmitCompareResult(inst, ordered ? ir.LogicalNot(unordered) : unordered, false, cmpx);
EmitCompareResult(inst, ordered ? ir.LogicalNot(unordered) : unordered, false);
}
void Translator::EmitFloatClassCompare(const Decoder::Instruction& inst, bool cmpx, bool half) {
void Translator::EmitFloatClassCompare(const Decoder::Instruction& inst, bool half) {
const auto value = half ? IR::Value(Read16LaneBits(inst.src0, false))
: ReadOperand(inst.src0, IR::Type::F32);
const auto mask = half ? IR::Value(Read16LaneBits(inst.src1, false))
: ReadOperand(inst.src1, IR::Type::U32);
const auto opcode = half ? IR::ValueOpcode::FPCmpClass16 : IR::ValueOpcode::FPCmpClass32;
EmitCompareResult(inst, IR::U1(ir.Emit(opcode, {value, mask})), false, cmpx);
EmitCompareResult(inst, IR::U1(ir.Emit(opcode, {value, mask})), false);
}
} // namespace Libs::Graphics::ShaderRecompiler::Frontend
@@ -51,33 +51,33 @@ void Translator::S_SUBVECTOR_LOOP(const Decoder::Instruction& inst, bool begin)
void Translator::S_SAVEEXEC(const Decoder::Instruction& inst, IR::ValueOpcode operation,
bool negate_exec, bool negate_source, bool write_64) {
if (!write_64) {
// Read the encoded scalar word and preserve EXEC_HI, including in wave32.
const auto old = ir.GetExecLo();
const auto src = ReadU32(inst.src0);
const auto combine = [&](IR::U32 old, IR::U32 src) {
const auto lhs = negate_exec ? ir.BitwiseNot(old) : old;
const auto rhs = negate_source ? ir.BitwiseNot(src) : src;
IR::U32 result;
switch (operation) {
case IR::ValueOpcode::LogicalAnd: result = ir.BitwiseAnd(lhs, rhs); break;
case IR::ValueOpcode::LogicalOr: result = ir.BitwiseOr(lhs, rhs); break;
default: EXIT("unsupported SAVEEXEC operation");
}
return operation == IR::ValueOpcode::LogicalAnd ? ir.BitwiseAnd(lhs, rhs)
: ir.BitwiseOr(lhs, rhs);
};
if (!write_64) {
// Read the encoded scalar word and preserve EXEC_HI, including in wave32.
const auto old = ir.GetExecLo();
const auto result = combine(old, ReadU32(inst.src0));
WriteRawU32(inst.dst, old);
WriteRawU32(ConditionOperand(Decoder::OperandKind::ExecLo), result);
ir.SetScc(ir.INotEqual(result, IR::U32(IR::Value(0u))));
return;
}
const auto old = ir.GetExec();
const auto src = ReadMask(inst.src0);
const auto lhs = negate_exec ? ir.LogicalNot(old) : old;
const auto rhs = negate_source ? ir.LogicalNot(src) : src;
const auto result = IR::U1(ir.Emit(operation, {lhs, rhs}));
WriteMask(inst.dst, old, true);
const auto mask = BallotMask(result);
ir.SetExec(result);
ir.SetExecLo(mask[0]);
ir.SetExecHi(mask[1]);
// EXEC words combine bitwise; the per-lane EXEC combines the per-lane masks, so helper lanes
// stay enabled.
const auto old = ir.GetExec();
const auto src = ReadMask(inst.src0);
const auto lhs = negate_exec ? ir.LogicalNot(old) : old;
const auto rhs = negate_source ? ir.LogicalNot(src) : src;
const auto src_mask = ReadU32Pair(inst.src0);
const std::array old_mask {ir.GetExecLo(), ir.GetExecHi()};
const std::array mask {combine(old_mask[0], src_mask[0]), combine(old_mask[1], src_mask[1])};
WriteMask(inst.dst, old, old_mask, true);
WriteMask(ConditionOperand(Decoder::OperandKind::ExecLo), IR::U1(ir.Emit(operation, {lhs, rhs})),
mask);
ir.SetScc(ir.INotEqual(ir.BitwiseOr(mask[0], mask[1]), IR::U32(IR::Value(0u))));
}
@@ -95,7 +95,7 @@ void Translator::ADD_U32(const Decoder::Instruction& inst, bool vector, bool use
}
WriteOperand(DestinationOperand(inst), result);
if (vector) {
WriteMask(inst.dst2, ir.LogicalAnd(ir.GetExec(), carry));
WriteVectorMask(inst.dst2, carry);
} else {
ir.SetScc(carry);
}
@@ -108,7 +108,7 @@ void Translator::SUB_U32(const Decoder::Instruction& inst, bool vector, bool rev
const auto borrow = ir.UGreaterThan(rhs, lhs);
WriteOperand(DestinationOperand(inst), result);
if (vector) {
WriteMask(inst.dst2, ir.LogicalAnd(ir.GetExec(), borrow));
WriteVectorMask(inst.dst2, borrow);
} else {
ir.SetScc(borrow);
}
@@ -126,7 +126,7 @@ void Translator::SUBB_U32(const Decoder::Instruction& inst, bool vector, bool re
const auto borrow = ir.LogicalOr(borrow0, borrow1);
WriteOperand(DestinationOperand(inst), result);
if (vector) {
WriteMask(inst.dst2, ir.LogicalAnd(ir.GetExec(), borrow));
WriteVectorMask(inst.dst2, borrow);
} else {
ir.SetScc(borrow);
}
@@ -135,6 +135,8 @@ void Translator::V_PACK_B32_F16(const Decoder::Instruction& inst) {
}
IR::U32 Translator::PackU16Lanes(IR::U32 low, IR::U32 high) {
// A zero high lane zero-extends; descriptor tracking bounds a mask, not a bitfield insert.
if (high == IR::Value(0u)) return ir.BitwiseAnd(low, IR::U32(IR::Value(0xffffu)));
return IR::U32(
ir.Emit(IR::ValueOpcode::BitFieldInsert, {low, high, IR::Value(16u), IR::Value(16u)}));
}
@@ -107,19 +107,7 @@ void Translator::S_U64_MASK(const Decoder::Instruction& inst, IR::ValueOpcode lo
default: return false;
}
};
if (is_exec_or_vcc(inst.dst) || is_exec_or_vcc(inst.src0) ||
(inst.src_count > 1u && is_exec_or_vcc(inst.src1))) {
const auto mask = WriteMask(inst.dst, invocation_result, true);
ir.SetScc(ir.INotEqual(ir.BitwiseOr(mask[0], mask[1]), IR::U32(IR::Value(0u))));
return;
}
// Descriptor tracking reasons about each scalar word independently.
const auto lhs = ReadU32Pair(inst.src0);
auto mask_valid = ReadMaskValid(inst.src0);
if (inst.src_count > 1u) {
mask_valid = ir.LogicalAnd(mask_valid, ReadMaskValid(inst.src1));
}
const auto lhs = ReadU32Pair(inst.src0);
std::array<IR::U32, 2> result;
if (unary) {
result = {ir.BitwiseNot(lhs[0]), ir.BitwiseNot(lhs[1])};
@@ -132,11 +120,21 @@ void Translator::S_U64_MASK(const Decoder::Instruction& inst, IR::ValueOpcode lo
result[component] = negate_result ? ir.BitwiseNot(IR::U32(value)) : IR::U32(value);
}
}
WriteU32Pair(inst.dst, result);
if (inst.dst.kind == Decoder::OperandKind::Sgpr) {
const auto dst = static_cast<IR::ScalarReg>(inst.dst.reg);
ir.SetThreadBitScalarReg(dst, invocation_result);
ir.SetScalarMaskTag(dst, mask_valid);
if (is_exec_or_vcc(inst.dst) || is_exec_or_vcc(inst.src0) ||
(inst.src_count > 1u && is_exec_or_vcc(inst.src1))) {
WriteMask(inst.dst, invocation_result, result, true);
} else {
// Descriptor tracking reasons about each scalar word independently.
auto mask_valid = ReadMaskValid(inst.src0);
if (inst.src_count > 1u) {
mask_valid = ir.LogicalAnd(mask_valid, ReadMaskValid(inst.src1));
}
WriteU32Pair(inst.dst, result);
if (inst.dst.kind == Decoder::OperandKind::Sgpr) {
const auto dst = static_cast<IR::ScalarReg>(inst.dst.reg);
ir.SetThreadBitScalarReg(dst, invocation_result);
ir.SetScalarMaskTag(dst, mask_valid);
}
}
ir.SetScc(ir.INotEqual(ir.BitwiseOr(result[0], result[1]), IR::U32(IR::Value(0u))));
}
@@ -292,7 +290,8 @@ void Translator::V_MAD_U64_U32(const Decoder::Instruction& inst) {
if (inst.dst2.kind != Decoder::OperandKind::Null &&
inst.dst2.kind != Decoder::OperandKind::Unknown) {
// The 32x32 product fits in 64 bits, so only the addition can carry.
WriteMask(inst.dst2, IR::U1(ir.Emit(IR::ValueOpcode::ULessThan64, {result, product})));
const auto carry = IR::U1(ir.Emit(IR::ValueOpcode::ULessThan64, {result, product}));
WriteMask(inst.dst2, carry, VectorMask(carry));
}
}
@@ -472,7 +471,7 @@ void Translator::S_BITCMP_B32(const Decoder::Instruction& inst, bool expected) {
const auto offset = ir.BitwiseAnd(ReadU32(inst.src1), IR::U32(IR::Value(31u)));
const auto bit =
IR::U32(ir.Emit(IR::ValueOpcode::BitFieldUExtract, {value, offset, IR::Value(1u)}));
WriteCompareResult(inst.dst, ir.IEqual(bit, IR::U32(IR::Value(expected ? 1u : 0u))));
ir.SetScc(ir.IEqual(bit, IR::U32(IR::Value(expected ? 1u : 0u))));
}
void Translator::S_BITCMP_B64(const Decoder::Instruction& inst, bool expected) {
@@ -483,7 +482,7 @@ void Translator::S_BITCMP_B64(const Decoder::Instruction& inst, bool expected) {
ir.Select(ir.ULessThan(offset, IR::U32(IR::Value(32u))), value[0], value[1]);
const auto bit =
IR::U32(ir.Emit(IR::ValueOpcode::BitFieldUExtract, {word, word_bit, IR::Value(1u)}));
WriteCompareResult(inst.dst, ir.IEqual(bit, IR::U32(IR::Value(expected ? 1u : 0u))));
ir.SetScc(ir.IEqual(bit, IR::U32(IR::Value(expected ? 1u : 0u))));
}
void Translator::V_ALIGN_B32(const Decoder::Instruction& inst, bool byte_offset) {
@@ -863,9 +863,10 @@ void Translator::DS_WRITE2(const Decoder::Instruction& inst) {
void Translator::DS_APPEND_CONSUME(const Decoder::Instruction& inst, IR::ValueOpcode opcode) {
const auto memory = MemoryInfoFromDecoded(inst);
WriteOperand(inst.dst, ir.Emit(opcode,
{ReadU32(MemorySourceAt(inst, 0)), ir.GetExec()},
AddMemoryInfo(memory, inst.pc)));
const auto exec = ExecMask();
WriteOperand(inst.dst,
ir.Emit(opcode, {ReadU32(MemorySourceAt(inst, 0)), exec[0], exec[1], ir.GetExec()},
AddMemoryInfo(memory, inst.pc)));
}
void Translator::DS_ADDTID(const Decoder::Instruction& inst, bool write) {
@@ -901,10 +902,7 @@ void Translator::DS_ORDERED_COUNT(const Decoder::Instruction& inst) {
const auto packer = ir.BitwiseAnd(m0, IR::U32(IR::Value(0xfu)));
address = ir.IAdd(address, ir.ShiftLeftLogical(packer, IR::U32(IR::Value(2u))));
}
const auto exec = ir.GetExec();
const auto ballot = ir.Emit(IR::ValueOpcode::Ballot, {exec});
const auto low = ir.CompositeExtract(ballot, 0u);
const auto high = ir.CompositeExtract(ballot, 1u);
const auto [low, high] = ExecMask();
const auto active = ir.INotEqual(ir.BitwiseOr(low, high), IR::U32(IR::Value(0u)));
const auto first = ir.Select(
ir.INotEqual(low, IR::U32(IR::Value(0u))),
@@ -73,43 +73,41 @@ void Translator::EmitScalar(const Decoder::Instruction& inst) {
case O::S_CMP_EQ_U32:
case O::S_CMP_EQ_I32:
EmitIntegerCompare(inst, IR::ValueOpcode::IEqual32, IR::Type::U32, true, false);
EmitIntegerCompare(inst, IR::ValueOpcode::IEqual32, IR::Type::U32, true);
return;
case O::S_CMP_LG_U32:
case O::S_CMP_LG_I32:
EmitIntegerCompare(inst, IR::ValueOpcode::INotEqual32, IR::Type::U32, true, false);
EmitIntegerCompare(inst, IR::ValueOpcode::INotEqual32, IR::Type::U32, true);
return;
case O::S_CMP_GT_U32:
EmitIntegerCompare(inst, IR::ValueOpcode::UGreaterThan32, IR::Type::U32, true, false);
EmitIntegerCompare(inst, IR::ValueOpcode::UGreaterThan32, IR::Type::U32, true);
return;
case O::S_CMP_GE_U32:
EmitIntegerCompare(inst, IR::ValueOpcode::UGreaterThanEqual32, IR::Type::U32, true,
false);
EmitIntegerCompare(inst, IR::ValueOpcode::UGreaterThanEqual32, IR::Type::U32, true);
return;
case O::S_CMP_LT_U32:
EmitIntegerCompare(inst, IR::ValueOpcode::ULessThan32, IR::Type::U32, true, false);
EmitIntegerCompare(inst, IR::ValueOpcode::ULessThan32, IR::Type::U32, true);
return;
case O::S_CMP_LE_U32:
EmitIntegerCompare(inst, IR::ValueOpcode::ULessThanEqual32, IR::Type::U32, true, false);
EmitIntegerCompare(inst, IR::ValueOpcode::ULessThanEqual32, IR::Type::U32, true);
return;
case O::S_CMP_GT_I32:
EmitIntegerCompare(inst, IR::ValueOpcode::SGreaterThan32, IR::Type::U32, true, false);
EmitIntegerCompare(inst, IR::ValueOpcode::SGreaterThan32, IR::Type::U32, true);
return;
case O::S_CMP_GE_I32:
EmitIntegerCompare(inst, IR::ValueOpcode::SGreaterThanEqual32, IR::Type::U32, true,
false);
EmitIntegerCompare(inst, IR::ValueOpcode::SGreaterThanEqual32, IR::Type::U32, true);
return;
case O::S_CMP_LT_I32:
EmitIntegerCompare(inst, IR::ValueOpcode::SLessThan32, IR::Type::U32, true, false);
EmitIntegerCompare(inst, IR::ValueOpcode::SLessThan32, IR::Type::U32, true);
return;
case O::S_CMP_LE_I32:
EmitIntegerCompare(inst, IR::ValueOpcode::SLessThanEqual32, IR::Type::U32, true, false);
EmitIntegerCompare(inst, IR::ValueOpcode::SLessThanEqual32, IR::Type::U32, true);
return;
case O::S_CMP_EQ_U64:
EmitIntegerCompare(inst, IR::ValueOpcode::IEqual64, IR::Type::U64, true, false);
EmitIntegerCompare(inst, IR::ValueOpcode::IEqual64, IR::Type::U64, true);
return;
case O::S_CMP_LG_U64:
EmitIntegerCompare(inst, IR::ValueOpcode::INotEqual64, IR::Type::U64, true, false);
EmitIntegerCompare(inst, IR::ValueOpcode::INotEqual64, IR::Type::U64, true);
return;
case O::S_AND_B64:
@@ -129,6 +129,20 @@ std::array<IR::U32, 2> Translator::BallotMask(IR::U1 value) {
program.wave_size == 64u ? ir.CompositeExtract(mask, 1) : IR::U32(IR::Value(0u))};
}
std::array<IR::U32, 2> Translator::ExecMask() {
return {ir.GetExecLo(), program.wave_size == 64u ? ir.GetExecHi() : IR::U32(IR::Value(0u))};
}
// Pixel EXEC words exclude the helper invocations that the per-lane EXEC keeps enabled.
std::array<IR::U32, 2> Translator::VectorMask(IR::U1 value) {
auto mask = BallotMask(value);
if (program.stage == ShaderType::Pixel) {
const auto exec = ExecMask();
mask = {ir.BitwiseAnd(mask[0], exec[0]), ir.BitwiseAnd(mask[1], exec[1])};
}
return mask;
}
IR::U32 Translator::ReadRawU32(const Decoder::Operand& operand) {
switch (operand.kind) {
case Decoder::OperandKind::LiteralConstant:
@@ -138,6 +152,8 @@ IR::U32 Translator::ReadRawU32(const Decoder::Operand& operand) {
case Decoder::OperandKind::SharedBase:
case Decoder::OperandKind::PrivateBase:
case Decoder::OperandKind::PopsExitingWaveId: return IR::U32(IR::Value(0u));
case Decoder::OperandKind::SharedLimit:
case Decoder::OperandKind::PrivateLimit: return IR::U32(IR::Value(0xffffffffu));
case Decoder::OperandKind::Sgpr:
return ir.GetScalarReg(static_cast<IR::ScalarReg>(operand.reg));
case Decoder::OperandKind::Vgpr:
@@ -388,21 +404,9 @@ void Translator::WriteOperand(const Decoder::Operand& operand, IR::Value value)
switch (operand.kind) {
case Decoder::OperandKind::Scc: ir.SetScc(IR::U1(value)); return;
case Decoder::OperandKind::ExecLo:
case Decoder::OperandKind::ExecHi: {
const auto mask = BallotMask(IR::U1(value));
ir.SetExec(IR::U1(value));
ir.SetExecLo(mask[0]);
ir.SetExecHi(mask[1]);
return;
}
case Decoder::OperandKind::ExecHi:
case Decoder::OperandKind::VccLo:
case Decoder::OperandKind::VccHi: {
const auto mask = BallotMask(IR::U1(value));
ir.SetVcc(IR::U1(value));
ir.SetVccLo(mask[0]);
ir.SetVccHi(mask[1]);
return;
}
case Decoder::OperandKind::VccHi: EXIT("lane masks are written through WriteMask");
default:
WriteRawU32(operand, ir.Select(IR::U1(value), IR::U32(IR::Value(1u)),
IR::U32(IR::Value(0u))));
@@ -471,17 +475,17 @@ IR::U32 Translator::ReadU32(const Decoder::Operand& operand) {
}
std::array<IR::U32, 2> Translator::ReadU32Pair(const Decoder::Operand& operand) {
if (operand.kind == Decoder::OperandKind::PrivateBase ||
operand.kind == Decoder::OperandKind::SharedBase) {
return {IR::U32(IR::Value(0u)), IR::U32(IR::Value(
operand.kind == Decoder::OperandKind::PrivateBase ? Decoder::PrivateApertureHigh
: Decoder::SharedApertureHigh))};
}
if (operand.kind == Decoder::OperandKind::ExecLo) {
return {ir.GetExecLo(), ir.GetExecHi()};
}
if (operand.kind == Decoder::OperandKind::VccLo) {
return {ir.GetVccLo(), ir.GetVccHi()};
// Each aperture limit is its base plus 0xffffffff.
switch (operand.kind) {
case Decoder::OperandKind::SharedBase:
case Decoder::OperandKind::SharedLimit:
return {ReadRawU32(operand), IR::U32(IR::Value(Decoder::SharedApertureHigh))};
case Decoder::OperandKind::PrivateBase:
case Decoder::OperandKind::PrivateLimit:
return {ReadRawU32(operand), IR::U32(IR::Value(Decoder::PrivateApertureHigh))};
case Decoder::OperandKind::ExecLo: return {ir.GetExecLo(), ir.GetExecHi()};
case Decoder::OperandKind::VccLo: return {ir.GetVccLo(), ir.GetVccHi()};
default: break;
}
if (operand.kind == Decoder::OperandKind::FloatInlineConstant) {
const auto bits =
@@ -639,7 +643,7 @@ IR::U1 Translator::ReadMask(const Decoder::Operand& operand) {
}
case Decoder::OperandKind::ExecLo:
case Decoder::OperandKind::ExecHi: return ir.GetExec();
case Decoder::OperandKind::VccLo:
case Decoder::OperandKind::VccLo: return ir.GetVcc();
case Decoder::OperandKind::VccHi:
return program.wave_size == 32u
? ThreadBit({ReadRawU32(operand), IR::U32(IR::Value(0u))})
@@ -677,9 +681,8 @@ IR::U1 Translator::ReadMaskValid(const Decoder::Operand& operand) {
}
}
std::array<IR::U32, 2> Translator::WriteMask(const Decoder::Operand& operand, IR::U1 value,
bool write_64) {
const auto mask = BallotMask(value);
void Translator::WriteMask(const Decoder::Operand& operand, IR::U1 value,
const std::array<IR::U32, 2>& mask, bool write_64) {
switch (operand.kind) {
case Decoder::OperandKind::Sgpr: {
const auto reg = static_cast<IR::ScalarReg>(operand.reg);
@@ -700,43 +703,43 @@ std::array<IR::U32, 2> Translator::WriteMask(const Decoder::Operand& operand, IR
ir.SetThreadBitScalarReg(high, IR::U1(IR::Value(false)));
ir.SetScalarMaskTag(high, IR::U1(IR::Value(false)));
}
return mask;
return;
}
case Decoder::OperandKind::ExecLo:
case Decoder::OperandKind::ExecHi: {
ir.SetExec(value);
ir.SetExecLo(mask[0]);
ir.SetExecHi(mask[1]);
return mask;
return;
}
case Decoder::OperandKind::VccLo:
case Decoder::OperandKind::VccHi: {
if (!write_64 && program.wave_size == 32u) {
if (operand.kind == Decoder::OperandKind::VccHi && !write_64 &&
program.wave_size == 32u) {
WriteRawU32(operand, mask[0]);
return mask;
return;
}
ir.SetVcc(value);
ir.SetVccLo(mask[0]);
ir.SetVccHi(mask[1]);
return mask;
if (write_64 || program.wave_size == 64u) {
ir.SetVccHi(mask[1]);
}
return;
}
case Decoder::OperandKind::Scc: ir.SetScc(value); return mask;
case Decoder::OperandKind::Scc: ir.SetScc(value); return;
default:
WriteRawU32(operand, mask[0]);
WriteRawU32(OffsetOperand(operand, 1), mask[1]);
return mask;
return;
}
}
void Translator::WriteCompareResult(const Decoder::Operand& operand, IR::U1 value) {
if (operand.kind == Decoder::OperandKind::Scc) {
WriteOperand(operand, value);
return;
}
void Translator::WriteVectorMask(const Decoder::Operand& operand, IR::U1 value) {
// VALU instructions only write lanes enabled by the incoming EXEC mask. This is
// especially important for V_CMPX: replacing EXEC with the ungated comparison would
// reactivate lanes disabled by an enclosing divergent region.
WriteMask(operand, ir.LogicalAnd(ir.GetExec(), value));
const auto active = ir.LogicalAnd(ir.GetExec(), value);
WriteMask(operand, active, VectorMask(active));
}
void Translator::AddBranchCondition(const CFG::Graph& graph, const CFG::BasicBlock& source,
@@ -1153,7 +1156,10 @@ IR::Program TranslateProgram(const Decoder::Program& decoded, const CFG::Graph&
}
}
entry_ir.SetExec(initial_exec);
const auto initial_mask = entry_ir.Emit(IR::ValueOpcode::Ballot, {initial_exec});
// Pixel EXEC words start with the live (non-helper) lanes; helpers keep the per-lane EXEC.
const auto initial_mask = options.stage == ShaderType::Pixel
? entry_ir.Emit(IR::ValueOpcode::LiveMask)
: entry_ir.Emit(IR::ValueOpcode::Ballot, {initial_exec});
const auto initial_lo = entry_ir.CompositeExtract(initial_mask, 0);
const auto initial_hi = options.wave_size == 64u ? entry_ir.CompositeExtract(initial_mask, 1)
: IR::U32(IR::Value(0u));
@@ -1238,12 +1244,12 @@ IR::Program TranslateProgram(const Decoder::Program& decoded, const CFG::Graph&
entry_ir.BitwiseOr(wave_info, entry_ir.BitwiseOr(entry_ir.ShiftLeftLogical(
primitive_count, u32(8)),
vertex_count)));
// Instance IDs exclude the start instance, which shaders add from user data.
if (mesh.fast_launch) {
// Fast launch broadcasts the subgroup's base vertex and instance to every lane.
entry_ir.SetVectorReg(static_cast<IR::VectorReg>(5), entry_ir.IAdd(draw(1), group));
entry_ir.SetVectorReg(
static_cast<IR::VectorReg>(6),
entry_ir.IAdd(draw(2), builtin(IR::StageInputKind::WorkgroupId, 1)));
entry_ir.SetVectorReg(static_cast<IR::VectorReg>(6),
builtin(IR::StageInputKind::WorkgroupId, 1));
} else {
// GS adjacency addresses local ES records in LDS. Fans retain the draw's
// center in every subgroup; strip winding follows the global primitive.
@@ -1271,14 +1277,14 @@ IR::Program TranslateProgram(const Decoder::Program& decoded, const CFG::Graph&
entry_ir.ShiftLeftLogical(second, u32(18))));
entry_ir.SetVectorReg(static_cast<IR::VectorReg>(1),
entry_ir.ShiftLeftLogical(third, u32(2)));
const auto index_bytes = draw(3);
const auto index_bytes = draw(2);
const auto indexed = entry_ir.INotEqual(index_bytes, u32(0));
const auto index_low = draw(4);
const auto index_low = draw(3);
const auto byte_offset = entry_ir.IAdd(entry_ir.BitwiseAnd(index_low, u32(3)),
entry_ir.IMul(input_vertex, index_bytes));
const auto index_resource = entry_ir.Emit(
IR::ValueOpcode::GetAddressResource,
{entry_ir.BitwiseAnd(index_low, u32(~3u)), draw(5)});
{entry_ir.BitwiseAnd(index_low, u32(~3u)), draw(4)});
const auto memory_index = static_cast<uint32_t>(result.memory_info.size());
result.memory_info.push_back({.kind = IR::ResourceKind::Global});
const auto packed_index = entry_ir.Emit(
@@ -1292,9 +1298,8 @@ IR::Program TranslateProgram(const Decoder::Program& decoded, const CFG::Graph&
entry_ir.IMul(index_bytes, u32(8))}));
entry_ir.SetVectorReg(static_cast<IR::VectorReg>(5),
entry_ir.IAdd(draw(1), entry_ir.Select(indexed, index, input_vertex)));
entry_ir.SetVectorReg(
static_cast<IR::VectorReg>(8),
entry_ir.IAdd(draw(2), builtin(IR::StageInputKind::WorkgroupId, 1)));
entry_ir.SetVectorReg(static_cast<IR::VectorReg>(8),
builtin(IR::StageInputKind::WorkgroupId, 1));
}
} else if (options.stage == ShaderType::Local) {
entry_ir.SetScalarReg(static_cast<IR::ScalarReg>(3), IR::U32(IR::Value(64u)));
@@ -27,6 +27,8 @@ private:
Decoder::Operand ScalarDestinationOperand(const Decoder::Operand& operand, uint32_t offset);
Decoder::Operand PlainOperand(const Decoder::Operand& operand);
std::array<IR::U32, 2> BallotMask(IR::U1 value);
std::array<IR::U32, 2> ExecMask();
std::array<IR::U32, 2> VectorMask(IR::U1 value);
IR::U32 ReadRawU32(const Decoder::Operand& operand);
IR::U32 ReadScalarCode(uint32_t code);
IR::U32 ApplyBitSourceModifiers(const Decoder::Operand& operand, IR::U32 value);
@@ -52,9 +54,9 @@ private:
IR::U32 ConditionBit(const Decoder::Operand& operand);
IR::U1 ReadMask(const Decoder::Operand& operand);
IR::U1 ReadMaskValid(const Decoder::Operand& operand);
std::array<IR::U32, 2> WriteMask(const Decoder::Operand& operand, IR::U1 value,
bool write_64 = false);
void WriteCompareResult(const Decoder::Operand& operand, IR::U1 value);
void WriteMask(const Decoder::Operand& operand, IR::U1 value,
const std::array<IR::U32, 2>& mask, bool write_64 = false);
void WriteVectorMask(const Decoder::Operand& operand, IR::U1 value);
IR::MemoryFlags AddMemoryInfo(const IR::MemoryInfo& memory, uint32_t pc);
IR::ExportFlags AddExportInfo(const Decoder::Instruction& inst);
@@ -115,17 +117,15 @@ private:
IR::F32 SelectF32(IR::U1 condition, IR::F32 true_value, IR::F32 false_value);
IR::U32 PackU16Lanes(IR::U32 low, IR::U32 high);
IR::U32 Saturate16(IR::U32 value, bool signed_value);
void EmitCompareResult(const Decoder::Instruction& inst, IR::U1 value, bool scalar, bool cmpx);
void EmitCompareConstant(const Decoder::Instruction& inst, bool value, bool scalar, bool cmpx);
void EmitCompareResult(const Decoder::Instruction& inst, IR::U1 value, bool scalar);
void EmitCompareConstant(const Decoder::Instruction& inst, bool value, bool scalar);
void EmitIntegerCompare(const Decoder::Instruction& inst, IR::ValueOpcode opcode, IR::Type type,
bool scalar, bool cmpx, bool swap_operands = false);
bool scalar, bool swap_operands = false);
void EmitInteger16Compare(const Decoder::Instruction& inst, IR::ValueOpcode opcode,
bool signed_value, bool cmpx);
void EmitFloatCompare(const Decoder::Instruction& inst, IR::ValueOpcode opcode, bool half,
bool cmpx);
void EmitFloatOrderedCompare(const Decoder::Instruction& inst, bool ordered, bool cmpx,
bool is64 = false);
void EmitFloatClassCompare(const Decoder::Instruction& inst, bool cmpx, bool half = false);
bool signed_value);
void EmitFloatCompare(const Decoder::Instruction& inst, IR::ValueOpcode opcode, bool half);
void EmitFloatOrderedCompare(const Decoder::Instruction& inst, bool ordered, bool is64 = false);
void EmitFloatClassCompare(const Decoder::Instruction& inst, bool half = false);
void V_CVT_F32_UBYTE(const Decoder::Instruction& inst, uint32_t byte_index);
void V_CVT_F32_U32(const Decoder::Instruction& inst);
void V_CVT_F32_I32(const Decoder::Instruction& inst);
@@ -25,444 +25,310 @@ void Translator::EmitVector(const Decoder::Instruction& inst) {
case O::V_PERMLANEX16_B32: V_PERMLANE16_B32(inst, true); return;
case O::V_CMP_F_I32:
case O::V_CMP_F_U32: EmitCompareConstant(inst, false, false, false); return;
case O::V_CMP_F_U32: EmitCompareConstant(inst, false, false); return;
case O::V_CMP_T_I32:
case O::V_CMP_T_U32: EmitCompareConstant(inst, true, false, false); return;
case O::V_CMP_T_U32: EmitCompareConstant(inst, true, false); return;
case O::V_CMP_EQ_U32:
case O::V_CMP_EQ_I32:
EmitIntegerCompare(inst, IR::ValueOpcode::IEqual32, IR::Type::U32, false, false);
return;
case O::V_CMPX_EQ_U32:
case O::V_CMPX_EQ_I32:
EmitIntegerCompare(inst, IR::ValueOpcode::IEqual32, IR::Type::U32, false, true);
EmitIntegerCompare(inst, IR::ValueOpcode::IEqual32, IR::Type::U32, false);
return;
case O::V_CMP_NE_U32:
case O::V_CMP_NE_I32:
EmitIntegerCompare(inst, IR::ValueOpcode::INotEqual32, IR::Type::U32, false, false);
return;
case O::V_CMPX_NE_U32:
case O::V_CMPX_NE_I32:
EmitIntegerCompare(inst, IR::ValueOpcode::INotEqual32, IR::Type::U32, false, true);
EmitIntegerCompare(inst, IR::ValueOpcode::INotEqual32, IR::Type::U32, false);
return;
case O::V_CMP_GT_U32:
EmitIntegerCompare(inst, IR::ValueOpcode::UGreaterThan32, IR::Type::U32, false, false);
return;
case O::V_CMPX_GT_U32:
EmitIntegerCompare(inst, IR::ValueOpcode::UGreaterThan32, IR::Type::U32, false, true);
EmitIntegerCompare(inst, IR::ValueOpcode::UGreaterThan32, IR::Type::U32, false);
return;
case O::V_CMP_GE_U32:
EmitIntegerCompare(inst, IR::ValueOpcode::UGreaterThanEqual32, IR::Type::U32, false,
false);
return;
case O::V_CMPX_GE_U32:
EmitIntegerCompare(inst, IR::ValueOpcode::UGreaterThanEqual32, IR::Type::U32, false,
true);
EmitIntegerCompare(inst, IR::ValueOpcode::UGreaterThanEqual32, IR::Type::U32, false);
return;
case O::V_CMP_LT_U32:
EmitIntegerCompare(inst, IR::ValueOpcode::ULessThan32, IR::Type::U32, false, false);
return;
case O::V_CMPX_LT_U32:
EmitIntegerCompare(inst, IR::ValueOpcode::ULessThan32, IR::Type::U32, false, true);
EmitIntegerCompare(inst, IR::ValueOpcode::ULessThan32, IR::Type::U32, false);
return;
case O::V_CMP_LE_U32:
EmitIntegerCompare(inst, IR::ValueOpcode::ULessThanEqual32, IR::Type::U32, false,
false);
return;
case O::V_CMPX_LE_U32:
EmitIntegerCompare(inst, IR::ValueOpcode::ULessThanEqual32, IR::Type::U32, false, true);
EmitIntegerCompare(inst, IR::ValueOpcode::ULessThanEqual32, IR::Type::U32, false);
return;
case O::V_CMP_GT_I32:
EmitIntegerCompare(inst, IR::ValueOpcode::SGreaterThan32, IR::Type::U32, false, false);
return;
case O::V_CMPX_GT_I32:
EmitIntegerCompare(inst, IR::ValueOpcode::SGreaterThan32, IR::Type::U32, false, true);
EmitIntegerCompare(inst, IR::ValueOpcode::SGreaterThan32, IR::Type::U32, false);
return;
case O::V_CMP_GE_I32:
EmitIntegerCompare(inst, IR::ValueOpcode::SGreaterThanEqual32, IR::Type::U32, false,
false);
return;
case O::V_CMPX_GE_I32:
EmitIntegerCompare(inst, IR::ValueOpcode::SGreaterThanEqual32, IR::Type::U32, false,
true);
EmitIntegerCompare(inst, IR::ValueOpcode::SGreaterThanEqual32, IR::Type::U32, false);
return;
case O::V_CMP_LT_I32:
EmitIntegerCompare(inst, IR::ValueOpcode::SLessThan32, IR::Type::U32, false, false);
return;
case O::V_CMPX_LT_I32:
EmitIntegerCompare(inst, IR::ValueOpcode::SLessThan32, IR::Type::U32, false, true);
EmitIntegerCompare(inst, IR::ValueOpcode::SLessThan32, IR::Type::U32, false);
return;
case O::V_CMP_LE_I32:
EmitIntegerCompare(inst, IR::ValueOpcode::SLessThanEqual32, IR::Type::U32, false,
false);
return;
case O::V_CMPX_LE_I32:
EmitIntegerCompare(inst, IR::ValueOpcode::SLessThanEqual32, IR::Type::U32, false, true);
EmitIntegerCompare(inst, IR::ValueOpcode::SLessThanEqual32, IR::Type::U32, false);
return;
case O::V_CMP_F_I64:
case O::V_CMP_F_U64: EmitCompareConstant(inst, false, false, false); return;
case O::V_CMP_T_I64:
case O::V_CMP_T_U64: EmitCompareConstant(inst, true, false, false); return;
case O::V_CMP_F_U64:
case O::V_CMPX_F_I64:
case O::V_CMPX_F_U64: EmitCompareConstant(inst, false, false, true); return;
case O::V_CMPX_F_U64: EmitCompareConstant(inst, false, false); return;
case O::V_CMP_T_I64:
case O::V_CMP_T_U64:
case O::V_CMPX_T_I64:
case O::V_CMPX_T_U64: EmitCompareConstant(inst, true, false, true); return;
case O::V_CMPX_T_U64: EmitCompareConstant(inst, true, false); return;
case O::V_CMP_EQ_I64:
case O::V_CMP_EQ_U64:
EmitIntegerCompare(inst, IR::ValueOpcode::IEqual64, IR::Type::U64, false, false);
return;
case O::V_CMPX_EQ_I64:
case O::V_CMPX_EQ_U64:
EmitIntegerCompare(inst, IR::ValueOpcode::IEqual64, IR::Type::U64, false, true);
EmitIntegerCompare(inst, IR::ValueOpcode::IEqual64, IR::Type::U64, false);
return;
case O::V_CMP_LT_U64:
EmitIntegerCompare(inst, IR::ValueOpcode::ULessThan64, IR::Type::U64, false, false);
return;
case O::V_CMPX_LT_U64:
EmitIntegerCompare(inst, IR::ValueOpcode::ULessThan64, IR::Type::U64, false, true);
EmitIntegerCompare(inst, IR::ValueOpcode::ULessThan64, IR::Type::U64, false);
return;
case O::V_CMP_GT_U64:
EmitIntegerCompare(inst, IR::ValueOpcode::UGreaterThan64, IR::Type::U64, false, false);
return;
case O::V_CMPX_GT_U64:
EmitIntegerCompare(inst, IR::ValueOpcode::UGreaterThan64, IR::Type::U64, false, true);
EmitIntegerCompare(inst, IR::ValueOpcode::UGreaterThan64, IR::Type::U64, false);
return;
case O::V_CMP_NE_I64:
case O::V_CMP_NE_U64:
EmitIntegerCompare(inst, IR::ValueOpcode::INotEqual64, IR::Type::U64, false, false);
return;
case O::V_CMPX_NE_I64:
case O::V_CMPX_NE_U64:
EmitIntegerCompare(inst, IR::ValueOpcode::INotEqual64, IR::Type::U64, false, true);
EmitIntegerCompare(inst, IR::ValueOpcode::INotEqual64, IR::Type::U64, false);
return;
case O::V_CMP_LT_I64:
EmitIntegerCompare(inst, IR::ValueOpcode::SLessThan64, IR::Type::U64, false, false);
return;
case O::V_CMPX_LT_I64:
EmitIntegerCompare(inst, IR::ValueOpcode::SLessThan64, IR::Type::U64, false, true);
EmitIntegerCompare(inst, IR::ValueOpcode::SLessThan64, IR::Type::U64, false);
return;
case O::V_CMP_LE_I64:
EmitIntegerCompare(inst, IR::ValueOpcode::SLessThanEqual64, IR::Type::U64, false, false);
return;
case O::V_CMPX_LE_I64:
EmitIntegerCompare(inst, IR::ValueOpcode::SLessThanEqual64, IR::Type::U64, false, true);
EmitIntegerCompare(inst, IR::ValueOpcode::SLessThanEqual64, IR::Type::U64, false);
return;
// The IR has no signed 64-bit greater-than: a > b is b < a, and a >= b is b <= a.
case O::V_CMP_GT_I64:
EmitIntegerCompare(inst, IR::ValueOpcode::SLessThan64, IR::Type::U64, false, false,
true);
return;
case O::V_CMPX_GT_I64:
EmitIntegerCompare(inst, IR::ValueOpcode::SLessThan64, IR::Type::U64, false, true,
true);
EmitIntegerCompare(inst, IR::ValueOpcode::SLessThan64, IR::Type::U64, false, true);
return;
case O::V_CMP_GE_I64:
EmitIntegerCompare(inst, IR::ValueOpcode::SLessThanEqual64, IR::Type::U64, false, false,
true);
return;
case O::V_CMPX_GE_I64:
EmitIntegerCompare(inst, IR::ValueOpcode::SLessThanEqual64, IR::Type::U64, false, true,
true);
EmitIntegerCompare(inst, IR::ValueOpcode::SLessThanEqual64, IR::Type::U64, false, true);
return;
case O::V_CMP_LE_U64:
EmitIntegerCompare(inst, IR::ValueOpcode::ULessThanEqual64, IR::Type::U64, false, false);
return;
case O::V_CMPX_LE_U64:
EmitIntegerCompare(inst, IR::ValueOpcode::ULessThanEqual64, IR::Type::U64, false, true);
EmitIntegerCompare(inst, IR::ValueOpcode::ULessThanEqual64, IR::Type::U64, false);
return;
case O::V_CMP_GE_U64:
EmitIntegerCompare(inst, IR::ValueOpcode::UGreaterThanEqual64, IR::Type::U64, false, false);
return;
case O::V_CMPX_GE_U64:
EmitIntegerCompare(inst, IR::ValueOpcode::UGreaterThanEqual64, IR::Type::U64, false, true);
EmitIntegerCompare(inst, IR::ValueOpcode::UGreaterThanEqual64, IR::Type::U64, false);
return;
case O::V_CMP_EQ_U16:
EmitInteger16Compare(inst, IR::ValueOpcode::IEqual32, false, false);
case O::V_CMPX_EQ_U16:
EmitInteger16Compare(inst, IR::ValueOpcode::IEqual32, false);
return;
case O::V_CMP_EQ_I16:
EmitInteger16Compare(inst, IR::ValueOpcode::IEqual32, true, false);
return;
case O::V_CMPX_EQ_I16:
EmitInteger16Compare(inst, IR::ValueOpcode::IEqual32, true, true);
EmitInteger16Compare(inst, IR::ValueOpcode::IEqual32, true);
return;
case O::V_CMP_NE_U16:
EmitInteger16Compare(inst, IR::ValueOpcode::INotEqual32, false, false);
return;
case O::V_CMPX_NE_U16:
EmitInteger16Compare(inst, IR::ValueOpcode::INotEqual32, false, true);
EmitInteger16Compare(inst, IR::ValueOpcode::INotEqual32, false);
return;
case O::V_CMP_NE_I16:
EmitInteger16Compare(inst, IR::ValueOpcode::INotEqual32, true, false);
return;
case O::V_CMPX_NE_I16:
EmitInteger16Compare(inst, IR::ValueOpcode::INotEqual32, true, true);
EmitInteger16Compare(inst, IR::ValueOpcode::INotEqual32, true);
return;
case O::V_CMP_GT_U16:
EmitInteger16Compare(inst, IR::ValueOpcode::UGreaterThan32, false, false);
return;
case O::V_CMPX_GT_U16:
EmitInteger16Compare(inst, IR::ValueOpcode::UGreaterThan32, false, true);
EmitInteger16Compare(inst, IR::ValueOpcode::UGreaterThan32, false);
return;
case O::V_CMP_GE_U16:
EmitInteger16Compare(inst, IR::ValueOpcode::UGreaterThanEqual32, false, false);
EmitInteger16Compare(inst, IR::ValueOpcode::UGreaterThanEqual32, false);
return;
case O::V_CMP_LT_U16:
EmitInteger16Compare(inst, IR::ValueOpcode::ULessThan32, false, false);
return;
case O::V_CMPX_LT_U16:
EmitInteger16Compare(inst, IR::ValueOpcode::ULessThan32, false, true);
return;
case O::V_CMPX_EQ_U16:
EmitInteger16Compare(inst, IR::ValueOpcode::IEqual32, false, true);
EmitInteger16Compare(inst, IR::ValueOpcode::ULessThan32, false);
return;
case O::V_CMP_LE_U16:
EmitInteger16Compare(inst, IR::ValueOpcode::ULessThanEqual32, false, false);
EmitInteger16Compare(inst, IR::ValueOpcode::ULessThanEqual32, false);
return;
case O::V_CMP_GT_I16:
EmitInteger16Compare(inst, IR::ValueOpcode::SGreaterThan32, true, false);
return;
case O::V_CMPX_GT_I16:
EmitInteger16Compare(inst, IR::ValueOpcode::SGreaterThan32, true, true);
EmitInteger16Compare(inst, IR::ValueOpcode::SGreaterThan32, true);
return;
case O::V_CMP_GE_I16:
EmitInteger16Compare(inst, IR::ValueOpcode::SGreaterThanEqual32, true, false);
return;
case O::V_CMPX_GE_I16:
EmitInteger16Compare(inst, IR::ValueOpcode::SGreaterThanEqual32, true, true);
EmitInteger16Compare(inst, IR::ValueOpcode::SGreaterThanEqual32, true);
return;
case O::V_CMP_LT_I16:
EmitInteger16Compare(inst, IR::ValueOpcode::SLessThan32, true, false);
return;
case O::V_CMPX_LT_I16:
EmitInteger16Compare(inst, IR::ValueOpcode::SLessThan32, true, true);
EmitInteger16Compare(inst, IR::ValueOpcode::SLessThan32, true);
return;
case O::V_CMP_LE_I16:
EmitInteger16Compare(inst, IR::ValueOpcode::SLessThanEqual32, true, false);
return;
case O::V_CMPX_LE_I16:
EmitInteger16Compare(inst, IR::ValueOpcode::SLessThanEqual32, true, true);
EmitInteger16Compare(inst, IR::ValueOpcode::SLessThanEqual32, true);
return;
case O::V_CMP_F_F32: EmitCompareConstant(inst, false, false, false); return;
case O::V_CMP_TRU_F32: EmitCompareConstant(inst, true, false, false); return;
case O::V_CMP_F_F32: EmitCompareConstant(inst, false, false); return;
case O::V_CMP_TRU_F32: EmitCompareConstant(inst, true, false); return;
case O::V_CMP_EQ_F32:
EmitFloatCompare(inst, IR::ValueOpcode::FPOrdEqual32, false, false);
case O::V_CMPX_EQ_F32:
EmitFloatCompare(inst, IR::ValueOpcode::FPOrdEqual32, false);
return;
case O::V_CMP_EQ_F64:
EmitFloatCompare(inst, IR::ValueOpcode::FPOrdEqual64, false, false);
case O::V_CMPX_EQ_F64:
EmitFloatCompare(inst, IR::ValueOpcode::FPOrdEqual64, false);
return;
case O::V_CMP_LE_F64:
EmitFloatCompare(inst, IR::ValueOpcode::FPOrdLessThanEqual64, false, false);
return;
case O::V_CMPX_LE_F64:
EmitFloatCompare(inst, IR::ValueOpcode::FPOrdLessThanEqual64, false, true);
return;
case O::V_CMPX_GE_F64:
EmitFloatCompare(inst, IR::ValueOpcode::FPOrdGreaterThanEqual64, false, true);
return;
case O::V_CMP_F_F64: EmitCompareConstant(inst, false, false, false); return;
case O::V_CMP_TRU_F64: EmitCompareConstant(inst, true, false, false); return;
case O::V_CMPX_F_F64: EmitCompareConstant(inst, false, false, true); return;
case O::V_CMPX_TRU_F64: EmitCompareConstant(inst, true, false, true); return;
case O::V_CMP_LT_F64:
EmitFloatCompare(inst, IR::ValueOpcode::FPOrdLessThan64, false, false);
return;
case O::V_CMPX_LT_F64:
EmitFloatCompare(inst, IR::ValueOpcode::FPOrdLessThan64, false, true);
return;
case O::V_CMPX_EQ_F64:
EmitFloatCompare(inst, IR::ValueOpcode::FPOrdEqual64, false, true);
return;
case O::V_CMP_GT_F64:
EmitFloatCompare(inst, IR::ValueOpcode::FPOrdGreaterThan64, false, false);
return;
case O::V_CMPX_GT_F64:
EmitFloatCompare(inst, IR::ValueOpcode::FPOrdGreaterThan64, false, true);
return;
case O::V_CMP_LG_F64:
EmitFloatCompare(inst, IR::ValueOpcode::FPOrdNotEqual64, false, false);
return;
case O::V_CMPX_LG_F64:
EmitFloatCompare(inst, IR::ValueOpcode::FPOrdNotEqual64, false, true);
EmitFloatCompare(inst, IR::ValueOpcode::FPOrdLessThanEqual64, false);
return;
case O::V_CMP_GE_F64:
EmitFloatCompare(inst, IR::ValueOpcode::FPOrdGreaterThanEqual64, false, false);
case O::V_CMPX_GE_F64:
EmitFloatCompare(inst, IR::ValueOpcode::FPOrdGreaterThanEqual64, false);
return;
case O::V_CMP_F_F64:
case O::V_CMPX_F_F64: EmitCompareConstant(inst, false, false); return;
case O::V_CMP_TRU_F64:
case O::V_CMPX_TRU_F64: EmitCompareConstant(inst, true, false); return;
case O::V_CMP_LT_F64:
case O::V_CMPX_LT_F64:
EmitFloatCompare(inst, IR::ValueOpcode::FPOrdLessThan64, false);
return;
case O::V_CMP_GT_F64:
case O::V_CMPX_GT_F64:
EmitFloatCompare(inst, IR::ValueOpcode::FPOrdGreaterThan64, false);
return;
case O::V_CMP_LG_F64:
case O::V_CMPX_LG_F64:
EmitFloatCompare(inst, IR::ValueOpcode::FPOrdNotEqual64, false);
return;
case O::V_CMP_NLG_F64:
EmitFloatCompare(inst, IR::ValueOpcode::FPUnordEqual64, false, false);
return;
case O::V_CMPX_NLG_F64:
EmitFloatCompare(inst, IR::ValueOpcode::FPUnordEqual64, false, true);
EmitFloatCompare(inst, IR::ValueOpcode::FPUnordEqual64, false);
return;
case O::V_CMP_NEQ_F64:
EmitFloatCompare(inst, IR::ValueOpcode::FPUnordNotEqual64, false, false);
return;
case O::V_CMPX_NEQ_F64:
EmitFloatCompare(inst, IR::ValueOpcode::FPUnordNotEqual64, false, true);
EmitFloatCompare(inst, IR::ValueOpcode::FPUnordNotEqual64, false);
return;
case O::V_CMP_NLE_F64:
EmitFloatCompare(inst, IR::ValueOpcode::FPUnordGreaterThan64, false, false);
return;
case O::V_CMPX_NLE_F64:
EmitFloatCompare(inst, IR::ValueOpcode::FPUnordGreaterThan64, false, true);
EmitFloatCompare(inst, IR::ValueOpcode::FPUnordGreaterThan64, false);
return;
case O::V_CMP_NLT_F64:
EmitFloatCompare(inst, IR::ValueOpcode::FPUnordGreaterThanEqual64, false, false);
return;
case O::V_CMPX_NLT_F64:
EmitFloatCompare(inst, IR::ValueOpcode::FPUnordGreaterThanEqual64, false, true);
EmitFloatCompare(inst, IR::ValueOpcode::FPUnordGreaterThanEqual64, false);
return;
case O::V_CMP_NGE_F64:
EmitFloatCompare(inst, IR::ValueOpcode::FPUnordLessThan64, false, false);
return;
case O::V_CMPX_NGE_F64:
EmitFloatCompare(inst, IR::ValueOpcode::FPUnordLessThan64, false, true);
EmitFloatCompare(inst, IR::ValueOpcode::FPUnordLessThan64, false);
return;
case O::V_CMP_NGT_F64:
EmitFloatCompare(inst, IR::ValueOpcode::FPUnordLessThanEqual64, false, false);
return;
case O::V_CMPX_NGT_F64:
EmitFloatCompare(inst, IR::ValueOpcode::FPUnordLessThanEqual64, false, true);
return;
case O::V_CMP_O_F64: EmitFloatOrderedCompare(inst, true, false, true); return;
case O::V_CMP_U_F64: EmitFloatOrderedCompare(inst, false, false, true); return;
case O::V_CMPX_O_F64: EmitFloatOrderedCompare(inst, true, true, true); return;
case O::V_CMPX_U_F64: EmitFloatOrderedCompare(inst, false, true, true); return;
case O::V_CMPX_EQ_F32:
EmitFloatCompare(inst, IR::ValueOpcode::FPOrdEqual32, false, true);
EmitFloatCompare(inst, IR::ValueOpcode::FPUnordLessThanEqual64, false);
return;
case O::V_CMP_O_F64:
case O::V_CMPX_O_F64: EmitFloatOrderedCompare(inst, true, true); return;
case O::V_CMP_U_F64:
case O::V_CMPX_U_F64: EmitFloatOrderedCompare(inst, false, true); return;
case O::V_CMP_LG_F32:
EmitFloatCompare(inst, IR::ValueOpcode::FPOrdNotEqual32, false, false);
return;
case O::V_CMPX_LG_F32:
EmitFloatCompare(inst, IR::ValueOpcode::FPOrdNotEqual32, false, true);
EmitFloatCompare(inst, IR::ValueOpcode::FPOrdNotEqual32, false);
return;
case O::V_CMP_GT_F32:
EmitFloatCompare(inst, IR::ValueOpcode::FPOrdGreaterThan32, false, false);
return;
case O::V_CMPX_GT_F32:
EmitFloatCompare(inst, IR::ValueOpcode::FPOrdGreaterThan32, false, true);
EmitFloatCompare(inst, IR::ValueOpcode::FPOrdGreaterThan32, false);
return;
case O::V_CMP_GE_F32:
EmitFloatCompare(inst, IR::ValueOpcode::FPOrdGreaterThanEqual32, false, false);
return;
case O::V_CMPX_GE_F32:
EmitFloatCompare(inst, IR::ValueOpcode::FPOrdGreaterThanEqual32, false, true);
EmitFloatCompare(inst, IR::ValueOpcode::FPOrdGreaterThanEqual32, false);
return;
case O::V_CMP_LT_F32:
EmitFloatCompare(inst, IR::ValueOpcode::FPOrdLessThan32, false, false);
return;
case O::V_CMPX_LT_F32:
EmitFloatCompare(inst, IR::ValueOpcode::FPOrdLessThan32, false, true);
EmitFloatCompare(inst, IR::ValueOpcode::FPOrdLessThan32, false);
return;
case O::V_CMP_LE_F32:
EmitFloatCompare(inst, IR::ValueOpcode::FPOrdLessThanEqual32, false, false);
return;
case O::V_CMPX_LE_F32:
EmitFloatCompare(inst, IR::ValueOpcode::FPOrdLessThanEqual32, false, true);
EmitFloatCompare(inst, IR::ValueOpcode::FPOrdLessThanEqual32, false);
return;
case O::V_CMP_NLG_F32:
EmitFloatCompare(inst, IR::ValueOpcode::FPUnordEqual32, false, false);
return;
case O::V_CMPX_NLG_F32:
EmitFloatCompare(inst, IR::ValueOpcode::FPUnordEqual32, false, true);
EmitFloatCompare(inst, IR::ValueOpcode::FPUnordEqual32, false);
return;
case O::V_CMP_NEQ_F32:
EmitFloatCompare(inst, IR::ValueOpcode::FPUnordNotEqual32, false, false);
return;
case O::V_CMPX_NEQ_F32:
EmitFloatCompare(inst, IR::ValueOpcode::FPUnordNotEqual32, false, true);
EmitFloatCompare(inst, IR::ValueOpcode::FPUnordNotEqual32, false);
return;
case O::V_CMP_NLE_F32:
EmitFloatCompare(inst, IR::ValueOpcode::FPUnordGreaterThan32, false, false);
return;
case O::V_CMPX_NLE_F32:
EmitFloatCompare(inst, IR::ValueOpcode::FPUnordGreaterThan32, false, true);
EmitFloatCompare(inst, IR::ValueOpcode::FPUnordGreaterThan32, false);
return;
case O::V_CMP_NLT_F32:
EmitFloatCompare(inst, IR::ValueOpcode::FPUnordGreaterThanEqual32, false, false);
return;
case O::V_CMPX_NLT_F32:
EmitFloatCompare(inst, IR::ValueOpcode::FPUnordGreaterThanEqual32, false, true);
EmitFloatCompare(inst, IR::ValueOpcode::FPUnordGreaterThanEqual32, false);
return;
case O::V_CMP_NGE_F32:
EmitFloatCompare(inst, IR::ValueOpcode::FPUnordLessThan32, false, false);
return;
case O::V_CMPX_NGE_F32:
EmitFloatCompare(inst, IR::ValueOpcode::FPUnordLessThan32, false, true);
EmitFloatCompare(inst, IR::ValueOpcode::FPUnordLessThan32, false);
return;
case O::V_CMP_NGT_F32:
EmitFloatCompare(inst, IR::ValueOpcode::FPUnordLessThanEqual32, false, false);
return;
case O::V_CMPX_NGT_F32:
EmitFloatCompare(inst, IR::ValueOpcode::FPUnordLessThanEqual32, false, true);
EmitFloatCompare(inst, IR::ValueOpcode::FPUnordLessThanEqual32, false);
return;
case O::V_CMP_EQ_F16:
EmitFloatCompare(inst, IR::ValueOpcode::FPOrdEqual32, true, false);
return;
case O::V_CMPX_EQ_F16:
EmitFloatCompare(inst, IR::ValueOpcode::FPOrdEqual32, true, true);
EmitFloatCompare(inst, IR::ValueOpcode::FPOrdEqual32, true);
return;
case O::V_CMP_LG_F16:
EmitFloatCompare(inst, IR::ValueOpcode::FPOrdNotEqual32, true, false);
EmitFloatCompare(inst, IR::ValueOpcode::FPOrdNotEqual32, true);
return;
case O::V_CMP_GT_F16:
EmitFloatCompare(inst, IR::ValueOpcode::FPOrdGreaterThan32, true, false);
return;
case O::V_CMPX_GT_F16:
EmitFloatCompare(inst, IR::ValueOpcode::FPOrdGreaterThan32, true, true);
EmitFloatCompare(inst, IR::ValueOpcode::FPOrdGreaterThan32, true);
return;
case O::V_CMP_GE_F16:
EmitFloatCompare(inst, IR::ValueOpcode::FPOrdGreaterThanEqual32, true, false);
return;
case O::V_CMPX_GE_F16:
EmitFloatCompare(inst, IR::ValueOpcode::FPOrdGreaterThanEqual32, true, true);
EmitFloatCompare(inst, IR::ValueOpcode::FPOrdGreaterThanEqual32, true);
return;
case O::V_CMP_LT_F16:
EmitFloatCompare(inst, IR::ValueOpcode::FPOrdLessThan32, true, false);
return;
case O::V_CMPX_LT_F16:
EmitFloatCompare(inst, IR::ValueOpcode::FPOrdLessThan32, true, true);
EmitFloatCompare(inst, IR::ValueOpcode::FPOrdLessThan32, true);
return;
case O::V_CMP_LE_F16:
EmitFloatCompare(inst, IR::ValueOpcode::FPOrdLessThanEqual32, true, false);
return;
case O::V_CMPX_LE_F16:
EmitFloatCompare(inst, IR::ValueOpcode::FPOrdLessThanEqual32, true, true);
EmitFloatCompare(inst, IR::ValueOpcode::FPOrdLessThanEqual32, true);
return;
case O::V_CMP_NGE_F16:
EmitFloatCompare(inst, IR::ValueOpcode::FPUnordLessThan32, true, false);
EmitFloatCompare(inst, IR::ValueOpcode::FPUnordLessThan32, true);
return;
case O::V_CMP_NGT_F16:
EmitFloatCompare(inst, IR::ValueOpcode::FPUnordLessThanEqual32, true, false);
return;
case O::V_CMPX_NGT_F16:
EmitFloatCompare(inst, IR::ValueOpcode::FPUnordLessThanEqual32, true, true);
EmitFloatCompare(inst, IR::ValueOpcode::FPUnordLessThanEqual32, true);
return;
case O::V_CMPX_NLE_F16:
EmitFloatCompare(inst, IR::ValueOpcode::FPUnordGreaterThan32, true, true);
EmitFloatCompare(inst, IR::ValueOpcode::FPUnordGreaterThan32, true);
return;
case O::V_CMP_NEQ_F16:
EmitFloatCompare(inst, IR::ValueOpcode::FPUnordNotEqual32, true, false);
return;
case O::V_CMPX_NEQ_F16:
EmitFloatCompare(inst, IR::ValueOpcode::FPUnordNotEqual32, true, true);
EmitFloatCompare(inst, IR::ValueOpcode::FPUnordNotEqual32, true);
return;
case O::V_CMP_NLT_F16:
EmitFloatCompare(inst, IR::ValueOpcode::FPUnordGreaterThanEqual32, true, false);
return;
case O::V_CMPX_NLT_F16:
EmitFloatCompare(inst, IR::ValueOpcode::FPUnordGreaterThanEqual32, true, true);
EmitFloatCompare(inst, IR::ValueOpcode::FPUnordGreaterThanEqual32, true);
return;
case O::V_CMP_O_F32: EmitFloatOrderedCompare(inst, true, false); return;
case O::V_CMP_U_F32: EmitFloatOrderedCompare(inst, false, false); return;
case O::V_CMPX_O_F32: EmitFloatOrderedCompare(inst, true, true); return;
case O::V_CMPX_U_F32: EmitFloatOrderedCompare(inst, false, true); return;
case O::V_CMP_CLASS_F32: EmitFloatClassCompare(inst, false); return;
case O::V_CMPX_CLASS_F32: EmitFloatClassCompare(inst, true); return;
case O::V_CMP_CLASS_F16: EmitFloatClassCompare(inst, false, true); return;
case O::V_CMPX_CLASS_F16: EmitFloatClassCompare(inst, true, true); return;
case O::V_CMP_O_F32:
case O::V_CMPX_O_F32: EmitFloatOrderedCompare(inst, true); return;
case O::V_CMP_U_F32:
case O::V_CMPX_U_F32: EmitFloatOrderedCompare(inst, false); return;
case O::V_CMP_CLASS_F32:
case O::V_CMPX_CLASS_F32: EmitFloatClassCompare(inst); return;
case O::V_CMP_CLASS_F16:
case O::V_CMPX_CLASS_F16: EmitFloatClassCompare(inst, true); return;
case O::V_CVT_F32_UBYTE0: V_CVT_F32_UBYTE(inst, 0); return;
case O::V_CVT_F32_UBYTE1: V_CVT_F32_UBYTE(inst, 1); return;
@@ -155,7 +155,7 @@ KYTY_IO_FIELDS(SampledResourcePair, a, b, c)
KYTY_IO_FIELDS(StageInput, a, b, c, d, e)
KYTY_IO_FIELDS(StageOutput, a, b, c, d)
KYTY_IO_FIELDS(ShaderInfo, a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, t, u, v, w, x, y, z,
A, B, C, D, E, F, G, H, I, J)
A, B, C, D, E, F, G, H, I, J, K)
KYTY_IO_FIELDS(DescriptorSource::IndirectDescriptor::SelectorRead::CountedRows, a, b)
KYTY_IO_FIELDS(DescriptorSource::IndirectDescriptor::SelectorRead, a, b, c, d, e, f, g, h, i, j)
KYTY_IO_FIELDS(DescriptorSource::IndirectDescriptor::PointerTable, a, b, c, d)
+9 -2
View File
@@ -365,7 +365,7 @@ static_assert(static_cast<uint32_t>(DescriptorBindingKind::Count) == 58u);
struct PushData {
static constexpr uint32_t DwordCount = 32;
static constexpr uint32_t MeshDrawDwordCount = 6;
static constexpr uint32_t MeshDrawDwordCount = 5;
static constexpr uint32_t NoStart = UINT32_MAX;
std::array<uint32_t, DwordCount> dwords {};
@@ -495,10 +495,16 @@ struct Bindings {
uint32_t dispatch_thread_dword = PushData::NoStart;
uint32_t memory_offset_dword = 0;
[[nodiscard]] uint32_t ShaderDataDwords() const {
// Packed 8-bit binding offsets, then each binding's bound range in bytes: OpArrayLength
// reads 0 for ranges of 2 GiB or more on NVIDIA.
[[nodiscard]] uint32_t MemorySizeDword() const {
return memory_offset_dword +
(descriptor_counts[static_cast<size_t>(DescriptorBindingKind::Buffers)] + 3u) / 4u;
}
[[nodiscard]] uint32_t ShaderDataDwords() const {
return MemorySizeDword() +
descriptor_counts[static_cast<size_t>(DescriptorBindingKind::Buffers)];
}
[[nodiscard]] bool UsesPushData() const {
return push_data_start_dword != PushData::NoStart;
}
@@ -541,6 +547,7 @@ struct ShaderInfo {
bool subgroup_shuffle = false;
bool subgroup_arithmetic = false;
bool subgroup_local_invocation_id = false;
bool helper_invocation = false;
bool compute_derivatives = false;
bool image_gather_extended = false;
bool function_lds = false;
@@ -219,9 +219,11 @@ VALUE_OPCODE(FPFract64, F64, F64)
VALUE_OPCODE(LaneId, U32)
VALUE_OPCODE(Ballot, U32x4, U1)
VALUE_OPCODE(LiveMask, U32x4)
VALUE_OPCODE(ReadFirstLane, U32, U32, U1)
VALUE_OPCODE(ReadLane, U32, U32, U32)
VALUE_OPCODE(SubgroupUMinU32, U32, U32)
VALUE_OPCODE(SubgroupUMaxU32, U32, U32)
VALUE_OPCODE(WriteLane, U32, U32, U32, U32)
VALUE_OPCODE(Permlane16U32, U32, U32, U32, U32, U1, U32)
VALUE_OPCODE(PermuteU32, U32, U32, U32, U1)
@@ -309,8 +311,8 @@ VALUE_OPCODE(SharedAtomicOr32, U32, U32, U32, U1)
VALUE_OPCODE(SharedAtomicOr64, Void, U32, U64, U1)
VALUE_OPCODE(SharedAtomicXor32, U32, U32, U32, U1)
VALUE_OPCODE(SharedAtomicMaskedOr32, Void, U32, U32, U32, U1)
VALUE_OPCODE(DataAppend, U32, U32, U1)
VALUE_OPCODE(DataConsume, U32, U32, U1)
VALUE_OPCODE(DataAppend, U32, U32, U32, U32, U1)
VALUE_OPCODE(DataConsume, U32, U32, U32, U32, U1)
VALUE_OPCODE(SwizzleU32, U32, U32, U32, U1)
VALUE_OPCODE(ImageQueryDimensions, U32x4, ImageResource, ImageAddress)
@@ -167,6 +167,16 @@ bool ReplaceBinaryIdentity(Inst& inst, Type type, uint64_t identity) {
return false;
}
// Whether one operand is the other's negation.
bool IsComplementPair(const Inst& inst, ValueOpcode negation) {
for (uint32_t arg = 0; arg < 2u; ++arg) {
const auto* negate = Arg(inst, arg).TryInstruction();
if (negate != nullptr && negate->GetOpcode() == negation &&
Arg(*negate, 0) == Arg(inst, arg ^ 1u)) return true;
}
return false;
}
bool FoldSelect(Inst& inst) {
const auto condition = Arg(inst, 0);
const auto true_value = Arg(inst, 1);
@@ -626,8 +636,9 @@ void FoldInstruction(Block& block, Block::iterator instruction,
case ValueOpcode::BitwiseXor32:
if (!FoldU32(inst, [opcode = inst.GetOpcode()](uint32_t a, uint32_t b) {
return opcode == ValueOpcode::BitwiseOr32 ? a | b : a ^ b;
})) {
ReplaceBinaryIdentity(inst, Type::U32, 0u);
}) && !ReplaceBinaryIdentity(inst, Type::U32, 0u) &&
IsComplementPair(inst, ValueOpcode::BitwiseNot32)) {
Replace(inst, Value(UINT32_MAX));
}
return;
case ValueOpcode::BitwiseNot32: {
@@ -802,6 +813,8 @@ void FoldInstruction(Block& block, Block::iterator instruction,
Replace(inst, lhs.U1() ? lhs : rhs);
} else if (IsImmediate(rhs, Type::U1)) {
Replace(inst, rhs.U1() ? rhs : lhs);
} else if (IsComplementPair(inst, ValueOpcode::LogicalNot)) {
Replace(inst, Value(true));
} else {
const auto* left = lhs.TryInstruction();
const auto* right = rhs.TryInstruction();
@@ -892,8 +905,10 @@ private:
if (inst.GetOpcode() != ValueOpcode::CompositeExtractU32x4 ||
!Immediate(Arg(inst, 1), 0u)) return {};
const auto* source = Arg(inst, 0).TryInstruction();
return source != nullptr && source->GetOpcode() == ValueOpcode::Ballot
? Arg(*source, 0) : Value {};
if (source == nullptr) return {};
// Helper lanes keep the per-lane EXEC, so the live mask projects to true.
if (source->GetOpcode() == ValueOpcode::LiveMask) return Value(true);
return source->GetOpcode() == ValueOpcode::Ballot ? Arg(*source, 0) : Value {};
}
bool CanProject(Value value) {
@@ -99,89 +99,66 @@ Value GetRealValue(PhiMap& phi_map, Value source, uint32_t lane, uint32_t wave_s
return Value(&phi);
}
// A full-EXEC prefix scan with both half-wave endpoint reads is a reduction.
// A full-EXEC prefix scan with both half-wave endpoint reads is a reduction. Wave64 first joins
// the two rows of each half; a wave32 half is one row.
// Fragment subgroups may have no physical invocation at either endpoint.
class UMinReductionMatcher {
class WaveReductionMatcher {
public:
explicit UMinReductionMatcher(Block* block): block(block) {}
WaveReductionMatcher(const Block* block, ValueOpcode operation)
: block(block), operation(operation) {}
Value Match(const Inst& minimum) {
const auto* low = Get(minimum.Arg(0), ValueOpcode::ReadLane);
const auto* high = Get(minimum.Arg(1), ValueOpcode::ReadLane);
Value Match(const Inst& reduction, uint32_t wave_size) const {
const auto* low = Get(reduction.Arg(0), ValueOpcode::ReadLane);
const auto* high = Get(reduction.Arg(1), ValueOpcode::ReadLane);
if (low == nullptr || high == nullptr || low->Arg(0).Resolve() != high->Arg(0).Resolve()) {
return {};
}
const auto lane = [](Value value, uint32_t expected) {
const auto lane = [&](Value value, uint32_t expected) {
value = value.Resolve();
return value.IsImmediate() && value.GetType() == Type::U32 &&
value.U32() % 64u == expected;
value.U32() % wave_size == expected;
};
if (!((lane(low->Arg(1), 31) && lane(high->Arg(1), 63)) ||
(lane(low->Arg(1), 63) && lane(high->Arg(1), 31)))) {
const auto half = wave_size / 2u;
if (!((lane(low->Arg(1), half - 1u) && lane(high->Arg(1), wave_size - 1u)) ||
(lane(low->Arg(1), wave_size - 1u) && lane(high->Arg(1), half - 1u)))) {
return {};
}
const auto* joined = Get(Unwrap(low->Arg(0)), ValueOpcode::UMin32);
if (wave_size == 32u) return MatchScan(low->Arg(0));
const auto* joined = Get(low->Arg(0), operation);
if (joined == nullptr) return {};
for (size_t index = 0; index < 2; index++) {
auto attempt = *this;
const auto* cross = attempt.Get(attempt.Unwrap(joined->Arg(index)), ValueOpcode::Permlane16U32);
const auto* cross = Get(joined->Arg(index), ValueOpcode::Permlane16U32);
if (cross == nullptr) continue;
const auto flags = cross->Flags<PermlaneFlags>();
auto prefix = attempt.Unwrap(joined->Arg(1 - index));
const auto flags = cross->Flags<PermlaneFlags>();
const auto prefix = joined->Arg(1 - index).Resolve();
if (!flags.x16 || flags.fetch_inactive || !flags.bound_control ||
cross->Arg(1).Resolve() != Value(UINT32_MAX) ||
cross->Arg(2).Resolve() != Value(UINT32_MAX) ||
!attempt.FullExec(cross->Arg(3)) || attempt.Unwrap(cross->Arg(0)) != prefix) {
cross->Arg(2).Resolve() != Value(UINT32_MAX) || !FullExec(cross->Arg(3)) ||
cross->Arg(0).Resolve() != prefix) {
continue;
}
for (const uint32_t shift: {8u, 4u, 2u, 1u}) {
prefix = attempt.MatchRow(prefix, shift);
}
prefix = attempt.Unwrap(prefix);
if (prefix.IsEmpty() || attempt.saved_exec.IsEmpty()) return prefix;
const auto* seed = attempt.Get(prefix, ValueOpcode::SelectU32);
if (seed != nullptr && seed->Arg(2).Resolve() == Value(UINT32_MAX)) {
const auto predicate = seed->Arg(0).Resolve();
const auto* subset = attempt.Get(predicate, ValueOpcode::LogicalAnd);
if (predicate == attempt.saved_exec ||
(subset != nullptr && (subset->Arg(0).Resolve() == attempt.saved_exec ||
subset->Arg(1).Resolve() == attempt.saved_exec))) {
return prefix;
}
}
if (const auto seed = MatchScan(prefix); !seed.IsEmpty()) return seed;
}
return {};
}
private:
// The row scan's input.
Value MatchScan(Value prefix) const {
for (const uint32_t shift: {8u, 4u, 2u, 1u}) {
prefix = MatchRow(prefix, shift);
}
return prefix;
}
Inst* Get(Value value, ValueOpcode opcode) const {
auto* inst = value.Resolve().TryInstruction();
return inst != nullptr && inst->Parent() == block && inst->GetOpcode() == opcode
? inst : nullptr;
}
bool FullExec(Value predicate) {
predicate = predicate.Resolve();
if (predicate == Value(true)) return true;
const auto* either = Get(predicate, ValueOpcode::LogicalOr);
if (either == nullptr) return false;
for (size_t index = 0; index < 2; index++) {
const auto* negate = Get(either->Arg(index), ValueOpcode::LogicalNot);
if (negate == nullptr || negate->Arg(0).Resolve() != either->Arg(1 - index).Resolve()) continue;
const auto original = negate->Arg(0).Resolve();
if (saved_exec.IsEmpty()) saved_exec = original;
return saved_exec == original;
}
return false;
}
Value Unwrap(Value value) {
while (const auto* select = Get(value, ValueOpcode::SelectU32)) {
if (!FullExec(select->Arg(0))) break;
value = select->Arg(1);
}
return value.Resolve();
}
// Constant propagation folds the x | ~x EXEC of s_orn2_saveexec to true.
static bool FullExec(Value predicate) { return predicate.Resolve() == Value(true); }
static bool IsRowShift(const Inst& inst, uint32_t shift) {
const auto flags = inst.Flags<DppMoveFlags>();
@@ -189,22 +166,24 @@ private:
flags.bank_mask == 0xf && !flags.fetch_inactive && !flags.bound_control && !flags.dpp8;
}
Value MatchRow(Value value, uint32_t shift) {
const auto* update = Get(Unwrap(value), ValueOpcode::DppUpdateU32);
Value MatchRow(Value value, uint32_t shift) const {
const auto* update = Get(value, ValueOpcode::DppUpdateU32);
if (update == nullptr || !IsRowShift(*update, shift) || !FullExec(update->Arg(2))) return {};
const auto* minimum = Get(update->Arg(0), ValueOpcode::UMin32);
if (minimum == nullptr) return {};
const auto* combined = Get(update->Arg(0), operation);
if (combined == nullptr) return {};
for (size_t index = 0; index < 2; index++) {
const auto* move = Get(minimum->Arg(index), ValueOpcode::DppMoveU32);
const auto* move = Get(combined->Arg(index), ValueOpcode::DppMoveU32);
if (move == nullptr || !IsRowShift(*move, shift) || !FullExec(move->Arg(1))) continue;
const auto previous = Unwrap(minimum->Arg(1 - index));
if (Unwrap(move->Arg(0)) == previous && Unwrap(update->Arg(1)) == previous) return previous;
const auto previous = combined->Arg(1 - index).Resolve();
if (move->Arg(0).Resolve() == previous && update->Arg(1).Resolve() == previous) {
return previous;
}
}
return {};
}
Block* block;
Value saved_exec;
const Block* block;
ValueOpcode operation;
};
} // namespace
@@ -217,18 +196,22 @@ ReadLaneStats EliminateReadLane(Program& program, uint32_t wave_size) {
for (auto* block: program.blocks) {
for (auto& inst: *block) {
if (wave_size == 64u && inst.GetOpcode() == ValueOpcode::UMin32) {
const auto seed = UMinReductionMatcher(block).Match(inst);
const auto opcode = inst.GetOpcode();
if (opcode == ValueOpcode::UMin32 || opcode == ValueOpcode::UMax32) {
const auto seed = WaveReductionMatcher(block, opcode).Match(inst, wave_size);
if (!seed.IsEmpty()) {
const auto position = std::find_if(block->begin(), block->end(),
[&](const Inst& candidate) { return &candidate == &inst; });
auto& reduction = *block->PrependNewInst(position, ValueOpcode::SubgroupUMinU32, {seed});
const auto reduce = opcode == ValueOpcode::UMin32
? ValueOpcode::SubgroupUMinU32
: ValueOpcode::SubgroupUMaxU32;
auto& reduction = *block->PrependNewInst(position, reduce, {seed});
inst.ReplaceUsesWith(Value(&reduction));
stats.rewritten_reductions++;
}
continue;
}
if (inst.GetOpcode() != ValueOpcode::ReadLane) {
if (opcode != ValueOpcode::ReadLane) {
continue;
}
const auto selector = inst.Arg(1).Resolve();
@@ -366,7 +366,6 @@ void Visit(Program& program, ShaderStageInputInfo input_info, InputUsage& inputs
info.function_lds |= kind == ResourceKind::Lds &&
program.stage != ShaderType::Compute && program.stage != ShaderType::Mesh;
if (shared_access == SharedAccess::Append || shared_access == SharedAccess::Consume) {
info.subgroup_ballot = true;
info.subgroup_shuffle = true;
info.subgroup_local_invocation_id = true;
}
@@ -517,7 +516,9 @@ void Visit(Program& program, ShaderStageInputInfo input_info, InputUsage& inputs
inst.Flags<CFG::BranchCondition>() != CFG::BranchCondition::ScalarInstruction;
break;
case ValueOpcode::Ballot: info.subgroup_ballot = true; break;
case ValueOpcode::SubgroupUMinU32: info.subgroup_arithmetic = true; break;
case ValueOpcode::LiveMask: info.subgroup_ballot = info.helper_invocation = true; break;
case ValueOpcode::SubgroupUMinU32:
case ValueOpcode::SubgroupUMaxU32: info.subgroup_arithmetic = true; break;
case ValueOpcode::DppMoveU32:
case ValueOpcode::ReadFirstLane:
case ValueOpcode::ReadLane: {
@@ -576,7 +577,7 @@ void CollectShaderInfo(Program& program, ShaderStageInputInfo input_info) {
info.has_bitwise_xor = info.uses_lds = info.uses_gds = info.uses_flattened_srt =
info.uses_dispatch_threads = info.uses_swizzle = false;
info.bvh = info.subgroup_ballot = info.subgroup_vote = info.subgroup_barrier =
info.subgroup_shuffle = info.subgroup_arithmetic =
info.subgroup_shuffle = info.subgroup_arithmetic = info.helper_invocation =
info.subgroup_local_invocation_id = info.compute_derivatives = info.image_gather_extended =
info.function_lds = info.function_scratch = info.pixel_valid_mask = info.buffer_int64_atomics =
info.buffer_u8 = info.buffer_u16 = info.shared_int64_atomics = info.coherent_buffers = info.float64 = false;
+3 -1
View File
@@ -490,7 +490,9 @@ void BuildStageStaticKey(const ShaderPixelInputInfo& info, std::vector<uint32_t>
key.push_back(static_cast<uint32_t>(info.object_primitive_id));
key.push_back(static_cast<uint32_t>(info.alpha_blend_source));
key.insert(key.end(), std::begin(info.target_output_mode), std::end(info.target_output_mode));
key.push_back(info.target_packed_unorm_mask | (uint32_t {info.target_round_mode} << 8u));
key.push_back(info.target_packed_unorm_mask | (uint32_t {info.target_round_mode} << 8u) |
(uint32_t {info.target_uint_mask} << 16u) |
(uint32_t {info.target_sint_mask} << 24u));
const auto mask = info.target_shader_mask;
key.push_back((mask | (mask >> 1u) | (mask >> 2u) | (mask >> 3u)) & 0x11111111u);
for (uint32_t base = 0; base < info.target_export_mapping.size(); base += 4u) {
+3
View File
@@ -181,6 +181,9 @@ struct ShaderPixelInputInfo {
uint32_t target_shader_mask = UINT32_MAX;
std::array<Prospero::ColorComponentMapping, 8> target_export_mapping = {};
uint8_t target_packed_unorm_mask = 0;
// Exports to UINT and SINT targets, which take integer outputs of the same signedness.
uint8_t target_uint_mask = 0;
uint8_t target_sint_mask = 0;
// CB round mode per converted export: zero adds half an LSB, one truncates.
uint8_t target_round_mode = 0;
uint32_t scratch_size_dwords = 0;
+7
View File
@@ -96,6 +96,13 @@ int KYTY_SYSV_ABI AcmBatchJobNotification(AcmBatchInfo* batch_info) {
return OK;
}
int KYTY_SYSV_ABI AcmBatchJobPriority(AcmBatchInfo* batch_info, uint32_t priority) {
PRINT_NAME();
(void)batch_info;
(void)priority;
return OK;
}
int KYTY_SYSV_ABI AcmConvReverbSharedInput(AcmBatchInfo* batch_info, uint32_t block_count, void* in,
uint32_t count, const void* const ir[],
const float* gain, void* const out[]) {
+1
View File
@@ -24,6 +24,7 @@ int KYTY_SYSV_ABI AcmBatchStartBuffers(AcmContextId context, uint32_t batch_info
AcmBatchError* batch_error, AcmBatchId* batch);
int KYTY_SYSV_ABI AcmBatchWait(AcmContextId context, AcmBatchId batch, uint32_t timeout);
int KYTY_SYSV_ABI AcmBatchJobNotification(AcmBatchInfo* batch_info);
int KYTY_SYSV_ABI AcmBatchJobPriority(AcmBatchInfo* batch_info, uint32_t priority);
int KYTY_SYSV_ABI AcmConvReverbSharedInput(AcmBatchInfo* batch_info, uint32_t block_count, void* in,
uint32_t count, const void* const ir[],
const float* gain, void* const out[]);
+2
View File
@@ -658,6 +658,8 @@ constexpr int SAVE_DATA_ERROR_BUSY_FOR_SAVING = -2137063402
constexpr int SAVE_DATA_ERROR_LIMITATION_OVER = -2137063401; /* 0x809F0017 */
constexpr int SAVE_DATA_ERROR_EVENT_BUSY = -2137063400; /* 0x809F0018 */
constexpr int SAVE_DATA_ERROR_PARAMSFO_TRANSFER_TITLE_ID_NOT_FOUND = -2137063399; /* 0x809F0019 */
constexpr int SAVE_DATA_ERROR_NO_NEED_CONVERT = -2137063394; /* 0x809F001E */
constexpr int SAVE_DATA_ERROR_BLOCKS_FEWER_THAN_SRC = -2137063392; /* 0x809F0020 */
} // namespace SaveData
+1
View File
@@ -579,6 +579,7 @@ LIB_DEFINE(InitAudio_1_Acm) {
LIB_FUNC("8fe55ktlNVo", Acm::AcmBatchStartBuffers);
LIB_FUNC("RLN3gRlXJLE", Acm::AcmBatchWait);
LIB_FUNC("r7z5YQFZo+U", Acm::AcmBatchJobNotification);
LIB_FUNC("GTfJ64qFCPw", Acm::AcmBatchJobPriority);
LIB_FUNC("u70oWo92SYQ", Acm::AcmConvReverbSharedInput);
LIB_FUNC("9nLbWmRDpa8", Acm::AcmConvReverbSharedIr);
LIB_FUNC("KovqaFbmtsM", Acm::AcmFft);
+72 -9
View File
@@ -231,6 +231,15 @@ struct SaveDataDelete {
int32_t pad2;
};
struct SaveDataConvert {
int32_t user_id;
int32_t pad;
const SceSaveDataDirName* src_dir_name;
const SceSaveDataDirName* dst_dir_name;
uint64_t dst_blocks;
uint8_t reserved[24];
};
struct SaveDataEvent {
uint32_t type;
int32_t error_code;
@@ -309,14 +318,20 @@ static std::filesystem::path save_user_directory(std::string_view title_id, int3
return directory / title_id;
}
static std::filesystem::path save_directory(std::string_view title_id, std::string_view dir_name,
int32_t user_id) {
static bool is_memory_dir_name(std::string_view dir_name) {
for (uint32_t slot = 0; slot < 4; slot++) {
if (dir_name == memory_dir_name(slot)) {
return std::filesystem::path(SAVE_DATA_DIR) / title_id / dir_name /
std::to_string(user_id);
return true;
}
}
return false;
}
static std::filesystem::path save_directory(std::string_view title_id, std::string_view dir_name,
int32_t user_id) {
if (is_memory_dir_name(dir_name)) {
return std::filesystem::path(SAVE_DATA_DIR) / title_id / dir_name / std::to_string(user_id);
}
return save_user_directory(title_id, user_id) / dir_name;
}
@@ -1150,13 +1165,9 @@ int KYTY_SYSV_ABI SaveDataSyncSaveDataMemory(const SaveDataMemorySync* sync_para
return OK;
}
int KYTY_SYSV_ABI SaveDataGetEventResult(const void* event_param, SaveDataEvent* event) {
int KYTY_SYSV_ABI SaveDataGetEventResult(const void* /*event_param*/, SaveDataEvent* event) {
PRINT_NAME();
LOGF("\t event_param = 0x%016" PRIx64 "\n"
"\t event = 0x%016" PRIx64 "\n",
reinterpret_cast<uint64_t>(event_param), reinterpret_cast<uint64_t>(event));
if (event == nullptr) {
return SAVE_DATA_ERROR_PARAMETER;
}
@@ -1180,6 +1191,56 @@ int KYTY_SYSV_ABI SaveDataGetEventResult(const void* event_param, SaveDataEvent*
return OK;
}
int KYTY_SYSV_ABI SaveDataConvert(const SaveDataConvert* convert) {
PRINT_NAME();
const auto valid_name = [](const SceSaveDataDirName* name) {
return valid_path_component(name->data) && !is_memory_dir_name(name->data);
};
if (convert == nullptr || convert->src_dir_name == nullptr ||
!valid_name(convert->src_dir_name) ||
(convert->dst_dir_name != nullptr && !valid_name(convert->dst_dir_name)) ||
convert->dst_blocks < SAVE_DATA_BLOCKS_MIN || convert->dst_blocks > SAVE_DATA_BLOCKS_MAX) {
return SAVE_DATA_ERROR_PARAMETER;
}
if (convert->user_id < 0) {
return SAVE_DATA_ERROR_INVALID_LOGIN_USER;
}
LOGF("\t user_id = %" PRId32 "\n"
"\t src_dir_name = %s\n"
"\t dst_dir_name = %s\n"
"\t dst_blocks = %" PRIu64 "\n",
convert->user_id, convert->src_dir_name->data,
convert->dst_dir_name != nullptr ? convert->dst_dir_name->data : "<none>",
convert->dst_blocks);
Common::LockGuard lock(g_mount_mutex);
const auto source =
save_directory(get_title_id(), convert->src_dir_name->data, convert->user_id);
if (!Common::File::IsDirectoryExisting(source)) {
return SAVE_DATA_ERROR_NOT_FOUND;
}
if (g_mount_slots.FindAvailable(source) == SaveDataMountSlots::BUSY) {
return SAVE_DATA_ERROR_BUSY;
}
if (convert->dst_dir_name != nullptr &&
Common::File::IsDirectoryExisting(
save_directory(get_title_id(), convert->dst_dir_name->data, convert->user_id))) {
return SAVE_DATA_ERROR_EXISTS;
}
uint64_t blocks = 0;
if (read_save_blocks(source, &blocks) == OK) {
if (convert->dst_blocks == blocks) {
return SAVE_DATA_ERROR_NO_NEED_CONVERT;
}
if (convert->dst_blocks < blocks) {
return SAVE_DATA_ERROR_BLOCKS_FEWER_THAN_SRC;
}
}
EXIT("save data conversion of %s is not implemented\n", convert->src_dir_name->data);
}
int KYTY_SYSV_ABI SaveDataBackup(const SaveDataBackup* backup) {
PRINT_NAME();
@@ -1313,6 +1374,7 @@ LIB_DEFINE(InitSaveDataNative_1) {
LIB_FUNC("cduy9v4YmT4", ::Libs::SaveData::SaveDataSetSaveDataMemory2);
LIB_FUNC("wiT9jeC7xPw", ::Libs::SaveData::SaveDataSyncSaveDataMemory);
LIB_FUNC("j8xKtiFj0SY", ::Libs::SaveData::SaveDataGetEventResult);
LIB_FUNC("2mfSRGdshtk", ::Libs::SaveData::SaveDataConvert);
LIB_FUNC("WAzWTZm1H+I", ::Libs::SaveData::SaveDataTransferringMount);
LIB_FUNC("RjMlsR8EXrw", ::Libs::SaveData::SaveDataTransferringMount);
LIB_FUNC("z1JA8-iJt3k", ::Libs::SaveData::SaveDataBackup);
@@ -1343,6 +1405,7 @@ LIB_DEFINE(InitSaveData_1) {
LIB_FUNC("cduy9v4YmT4", SaveData::SaveDataSetSaveDataMemory2);
LIB_FUNC("wiT9jeC7xPw", SaveData::SaveDataSyncSaveDataMemory);
LIB_FUNC("j8xKtiFj0SY", SaveData::SaveDataGetEventResult);
LIB_FUNC("2mfSRGdshtk", SaveData::SaveDataConvert);
LIB_FUNC("WAzWTZm1H+I", SaveData::SaveDataTransferringMount);
LIB_FUNC("RjMlsR8EXrw", SaveData::SaveDataTransferringMount);
LIB_FUNC("z1JA8-iJt3k", SaveData::SaveDataBackup);
+33
View File
@@ -396,6 +396,38 @@ void TestClassicSavePaths() {
CHECK(Search(1) == names);
}
void TestConvert() {
Reset("CONVERT");
const auto source = DirName("save");
const auto memory = DirName("sce_sdmemory");
struct SaveDataConvert convert {};
convert.user_id = 1;
convert.src_dir_name = &memory;
convert.dst_blocks = 96;
CHECK(SaveDataConvert(&convert) == SAVE_DATA_ERROR_PARAMETER);
// A missing source fails at once.
convert.src_dir_name = &source;
CHECK(SaveDataConvert(&convert) == SAVE_DATA_ERROR_NOT_FOUND);
struct SaveDataMount3 mount {};
mount.user_id = 1;
mount.dir_name = &source;
mount.mount_mode = 4;
mount.blocks = 96;
SaveDataMountResult mounted {};
CHECK(SaveDataMount3(&mount, &mounted) == OK);
CHECK(SaveDataConvert(&convert) == SAVE_DATA_ERROR_BUSY);
CHECK(SaveDataUmount2(0, &mounted.mount_point) == OK);
CHECK(SaveDataConvert(&convert) == SAVE_DATA_ERROR_NO_NEED_CONVERT);
convert.dst_blocks = 95;
CHECK(SaveDataConvert(&convert) == SAVE_DATA_ERROR_BLOCKS_FEWER_THAN_SRC);
convert.dst_dir_name = &source;
CHECK(SaveDataConvert(&convert) == SAVE_DATA_ERROR_EXISTS);
for (const uint64_t blocks: {47u, 16385u}) {
convert.dst_blocks = blocks;
CHECK(SaveDataConvert(&convert) == SAVE_DATA_ERROR_PARAMETER);
}
}
void TestGuestSaveIsolation() {
Reset("MULTIUSER");
const auto name = DirName("slot");
@@ -670,6 +702,7 @@ int main(int argc, char** argv) {
TestIsolationAndSync();
TestFailedWritePreservesSave();
TestClassicSavePaths();
TestConvert();
TestGuestSaveIsolation();
TestSaveAllocations();
TestClassicSaveParams();
+46 -39
View File
@@ -628,42 +628,45 @@ void TestReadLaneElimination() {
"dynamic-lane read was rewritten unsafely");
}
void TestCompleteUMinReduction() {
void TestCompleteWaveReduction() {
enum class Case {
Masked, Subset, Literal, LiteralMasked, LiteralMove, Wave32,
UnrelatedSeed, UnmaskedSymbolic, WrongNeutral, RowMask, BankMask,
Masked, ScalarExec, Wave32, Wave32Rows, Wave32Maximum, RowMask, BankMask,
Shift, FetchInactive, BoundControl, Dpp8, MoveExec, UpdateExec,
Previous, DppSource, CrossExec, CrossRow, CrossSelectorLow,
CrossSelectorHigh, CrossFetch, CrossBound, CrossSource, FinalExec,
Endpoint, EndpointSource, DifferentWitness, CrossBlock, WrongOperation
Endpoint, EndpointSource, CrossBlock, WrongOperation
};
for (const auto test : {Case::Masked, Case::Subset, Case::Literal,
Case::LiteralMasked, Case::LiteralMove, Case::Wave32, Case::UnrelatedSeed,
Case::UnmaskedSymbolic, Case::WrongNeutral, Case::RowMask, Case::BankMask,
for (const auto test : {Case::Masked, Case::ScalarExec, Case::Wave32,
Case::Wave32Rows, Case::Wave32Maximum, Case::RowMask, Case::BankMask,
Case::Shift, Case::FetchInactive, Case::BoundControl, Case::Dpp8,
Case::MoveExec, Case::UpdateExec, Case::Previous, Case::DppSource,
Case::CrossExec, Case::CrossRow, Case::CrossSelectorLow,
Case::CrossSelectorHigh, Case::CrossFetch, Case::CrossBound,
Case::CrossSource, Case::FinalExec, Case::Endpoint, Case::EndpointSource,
Case::DifferentWitness, Case::CrossBlock, Case::WrongOperation}) {
Case::CrossBlock, Case::WrongOperation}) {
Fixture fixture(test == Case::CrossBlock ? 2 : 1);
const auto source = fixture.Emit(ValueOpcode::LaneId);
const auto mask = fixture.Emit(ValueOpcode::ULessThan32, {source, Value(19u)});
const auto other_mask = fixture.Emit(ValueOpcode::IEqual32, {source, Value(7u)});
const auto negated = fixture.Emit(ValueOpcode::LogicalNot, {mask});
const bool literal = test == Case::Literal || test == Case::LiteralMasked;
const auto enabled = literal ? Value(true) :
fixture.Emit(ValueOpcode::LogicalOr, {negated, mask});
auto seed_mask = test == Case::UnrelatedSeed ? other_mask : mask;
if (test == Case::Subset)
seed_mask = fixture.Emit(ValueOpcode::LogicalAnd, {other_mask, mask});
const auto seed = test == Case::Literal || test == Case::UnmaskedSymbolic ? source :
fixture.Emit(ValueOpcode::SelectU32,
{seed_mask, source, Value(test == Case::WrongNeutral ? 0u : UINT32_MAX)});
auto enabled = fixture.Emit(ValueOpcode::LogicalOr, {negated, mask});
if (test == Case::ScalarExec) {
// s_orn2_saveexec of a mask that is not a ballot, read back as a lane bit.
const auto exec = fixture.Emit(ValueOpcode::GetUserData, {Value(static_cast<ScalarReg>(2))});
const auto full = fixture.Emit(ValueOpcode::BitwiseOr32,
{fixture.Emit(ValueOpcode::BitwiseNot32, {exec}), exec});
const auto bit = fixture.Emit(ValueOpcode::BitwiseAnd32, {source, Value(31u)});
const auto shifted = fixture.Emit(ValueOpcode::ShiftRightLogical32, {full, bit});
enabled = fixture.Emit(ValueOpcode::INotEqual32,
{fixture.Emit(ValueOpcode::BitwiseAnd32, {shifted, Value(1u)}), Value(0u)});
}
// A UMax scan seeds lanes outside EXEC with zero.
const bool maximum = test == Case::Wave32Maximum;
const uint32_t neutral = maximum ? 0u : UINT32_MAX;
const auto seed = fixture.Emit(ValueOpcode::SelectU32, {mask, source, Value(neutral)});
auto prefix = fixture.Emit(ValueOpcode::SelectU32, {enabled, seed, source});
Value preserved;
const auto operation = test == Case::WrongOperation ? ValueOpcode::BitwiseOr32 :
ValueOpcode::UMin32;
auto operation = maximum ? ValueOpcode::UMax32 : ValueOpcode::UMin32;
if (test == Case::WrongOperation) operation = ValueOpcode::BitwiseOr32;
for (const uint32_t shift : {1u, 2u, 4u, 8u}) {
DppMoveFlags flags{.control = 0x110u + shift};
if (shift == 2) {
@@ -675,8 +678,7 @@ void TestCompleteUMinReduction() {
if (test == Case::Dpp8) flags.dpp8 = true;
}
const auto move = fixture.Emit(ValueOpcode::DppMoveU32,
{test == Case::DppSource ? source : prefix,
test == Case::MoveExec ? mask : test == Case::LiteralMove ? Value(true) : enabled});
{test == Case::DppSource ? source : prefix, test == Case::MoveExec ? mask : enabled});
move.Instruction()->SetFlags(flags);
const auto combined = fixture.Emit(operation, {move, prefix});
const auto update = fixture.Emit(ValueOpcode::DppUpdateU32,
@@ -686,11 +688,7 @@ void TestCompleteUMinReduction() {
prefix = update;
if (shift == 2) preserved = fixture.Emit(ValueOpcode::ReferenceU32, {prefix});
}
auto cross_exec = test == Case::CrossExec ? mask : enabled;
if (test == Case::DifferentWitness) {
const auto other_not = fixture.Emit(ValueOpcode::LogicalNot, {other_mask});
cross_exec = fixture.Emit(ValueOpcode::LogicalOr, {other_mask, other_not});
}
const auto cross_exec = test == Case::CrossExec ? mask : enabled;
const auto cross = fixture.Emit(ValueOpcode::Permlane16U32,
{test == Case::CrossSource ? source : prefix,
Value(test == Case::CrossSelectorLow ? 0xfffffff0u : UINT32_MAX),
@@ -704,10 +702,16 @@ void TestCompleteUMinReduction() {
const auto final = fixture.Emit(ValueOpcode::SelectU32,
{test == Case::FinalExec ? mask : enabled, combined, prefix});
const uint32_t result_block = test == Case::CrossBlock ? 1u : 0u;
// Wave32 reads each half's row endpoint from the scan itself.
const bool rows = test == Case::Wave32Rows || test == Case::ScalarExec || maximum;
const auto endpoints = rows ? prefix : final;
uint32_t low_lane = test == Case::Endpoint ? 30u : 31u;
if (rows) low_lane = 15u;
const auto low = fixture.Emit(ValueOpcode::ReadLane,
{final, Value(test == Case::Endpoint ? 30u : 31u)}, 0, result_block);
{endpoints, Value(low_lane)}, 0, result_block);
const auto high = fixture.Emit(ValueOpcode::ReadLane,
{test == Case::EndpointSource ? prefix : final, Value(63u)}, 0, result_block);
{test == Case::EndpointSource ? prefix : endpoints, Value(rows ? 31u : 63u)}, 0,
result_block);
const auto result = fixture.Emit(operation, {high, low}, 0, result_block);
const auto use = fixture.Emit(ValueOpcode::ReferenceU32, {result}, 0, result_block);
using Terminator = Libs::Graphics::ShaderRecompiler::CFG::TerminatorKind;
@@ -718,16 +722,19 @@ void TestCompleteUMinReduction() {
fixture.BlockAt().terminator.true_block = &fixture.BlockAt(1);
}
ValidateProgram(fixture.program, true);
const auto stats = EliminateReadLane(fixture.program, test == Case::Wave32 ? 32u : 64u);
const bool matches = test == Case::Masked || test == Case::Subset ||
test == Case::Literal || test == Case::LiteralMasked || test == Case::LiteralMove;
// The pipeline folds full-EXEC predicates to true before eliminating reads.
const uint32_t wave_size = test == Case::Wave32 || rows ? 32u : 64u;
ConstantPropagationPass(fixture.program.blocks, wave_size);
const auto stats = EliminateReadLane(fixture.program, wave_size);
const bool matches = test == Case::Masked || rows;
Check(stats.rewritten_reductions == (matches ? 1u : 0u) && stats.rewritten_reads == 0,
("UMin reduction proof failed case " + std::to_string(static_cast<unsigned>(test))).c_str());
("wave reduction proof failed case " + std::to_string(static_cast<unsigned>(test))).c_str());
if (matches) {
const auto *reduction = use.Instruction()->Arg(0).ResolveInstruction();
Check(reduction->GetOpcode() == ValueOpcode::SubgroupUMinU32 &&
Check(reduction->GetOpcode() ==
(maximum ? ValueOpcode::SubgroupUMaxU32 : ValueOpcode::SubgroupUMinU32) &&
reduction->Arg(0).Resolve() == seed.Resolve(),
"UMin reduction changed the neutral-masked input");
"wave reduction changed the neutral-masked input");
RemoveIdentities(fixture.program);
EliminateDeadCode(fixture.program.blocks);
uint32_t updates = 0;
@@ -735,14 +742,14 @@ void TestCompleteUMinReduction() {
updates += inst.GetOpcode() == ValueOpcode::DppUpdateU32;
Check(inst.GetOpcode() != ValueOpcode::ReadLane &&
inst.GetOpcode() != ValueOpcode::Permlane16U32,
"UMin reduction retained unused endpoint or cross-row operations");
"wave reduction retained unused endpoint or cross-row operations");
}
Check(updates == 2 && preserved.Instruction()->Arg(0).Instruction()->HasUses(),
"UMin reduction destroyed an independently consumed prefix or kept its dead suffix");
"wave reduction destroyed an independently consumed prefix or kept its dead suffix");
ValidateProgram(fixture.program, true);
} else {
Check(use.Instruction()->Arg(0).Resolve() == result,
"rejected UMin proof changed its original result");
"rejected wave reduction proof changed its original result");
}
}
}
@@ -884,7 +891,7 @@ int main() {
TestSharedIntegerRuntimeDependencies();
TestConstantBufferBounds();
TestReadLaneElimination();
TestCompleteUMinReduction();
TestCompleteWaveReduction();
TestOptimizationPipeline();
TestControlFlowValueSurvivesReadLaneFolding();
TestDeadPhiCyclesAndPlanningRoots();
+571 -15
View File
@@ -242,6 +242,10 @@ struct ImageHeapTestAccess {
static ImageId ImageOf(const ImageHeap &heap, uint32_t entry) {
return Element(heap, entry).binding.image_id;
}
// Whether the newest table no longer watches its last page.
static bool TailPageDirty(const ImageHeap &heap) {
return heap.m_memories.back()->dirty_pages.back() != 0u;
}
};
struct TextureCacheTestAccess {
@@ -576,6 +580,10 @@ struct RenderExecutorTestAccess {
executor.ResetBindings();
}
static bool ResolveColorTargets(RenderExecutor &executor, CommandBuffer &buffer) {
return executor.ResolveColorTargets(buffer, 0);
}
static bool BoundImagesInOrder(const RenderExecutor &executor, ImageId first,
ImageId second) {
return executor.m_bound_images.size() == 2 &&
@@ -1432,6 +1440,7 @@ struct GraphicsCase {
u32 pixel_custom_interpolation_mask = 0;
u32 width = 1;
std::vector<u32> object_ids;
std::vector<u32> gds_initial;
};
struct CompiledShader {
@@ -5507,6 +5516,93 @@ public:
ImageHeapTestAccess::ImageOf(heap, filtered.slots[240]),
"a reactivated view kept an element whose image slot holds a new image");
// Dynamic-resolution tables hold a descriptor per extent of one render target, each with
// its own pitch. Only the first gets an image; the others wait until an image of their
// extent exists.
const auto extent = [&](uint32_t width) {
auto words = descriptor(50, Format::k8_8_8_8UNorm);
words[2] = (width - 1u) | (3u << 14u);
return words;
};
const auto resolve = [&](const auto &root, const auto &words) {
return executor.ResolveTexture(root, {.dwords = words, .dword_count = 8u}).image_id;
};
write(50, extent(64));
write(51, extent(128));
Require(name, "extents", acquire(sampled), "the view was declined");
draw();
Require(name, "held extent",
ImageHeapTestAccess::ImageOf(heap, filtered.slots[400]) &&
!ImageHeapTestAccess::ImageOf(heap, filtered.slots[408]),
"a second extent of one address got its own image");
// A non-heap lookup, as for a render target, creates the second extent.
const auto rendered = resolve(sampled, extent(128));
Require(name, "rendered extent", acquire(sampled), "the view was declined");
draw();
Require(name, "held descriptor",
rendered && ImageHeapTestAccess::ImageOf(heap, filtered.slots[408]) == rendered,
"a held descriptor missed the image made for its extent");
// A released held element's index is reused once its tick ends; it is no longer held.
write(52, extent(256));
Require(name, "third extent", acquire(sampled), "the view was declined");
draw();
const auto held = filtered.slots[416];
write(52, {});
Require(name, "released extent", acquire(sampled), "the view was declined");
draw();
write(53, descriptor(53, Format::k8_8_8_8UNorm));
Require(name, "reused element", acquire(sampled), "the view was declined");
draw();
Require(name, "retry", acquire(sampled), "the view was declined");
draw();
Require(name, "reused held element", held != 0u && filtered.slots[424] == held,
"a reused element was retried as the held element it replaced");
// A 2D view of a heap-pinned volume is another image type, not another extent.
auto plane_root = sampled;
plane_root.heap = true;
auto volume_root = plane_root;
volume_root.dimension = ShaderRecompiler::Decoder::ImageDimension::Dim3D;
auto volume_words = descriptor(54, Format::k8_8_8_8UNorm);
volume_words[3] = DstSel(4, 5, 6, 7) |
(static_cast<uint32_t>(Prospero::ImageType::kColor3D) << 28u);
volume_words[4] = 3u;
const auto volume = resolve(volume_root, volume_words);
Require(name, "volume", static_cast<bool>(volume), "a volume texture was not resolved");
auto &volume_refs = textures.GetImage(volume).heap_refs;
volume_refs.fetch_add(1u);
const auto plane = resolve(plane_root, descriptor(54, Format::k8_8_8_8UNorm));
volume_refs.fetch_sub(1u);
Require(name, "volume plane", static_cast<bool>(plane),
"a 2D view of a pinned volume was held as another extent");
// A table follows its descriptor's size and keeps its elements. Growing probes the new
// words, shrinking releases them, and a draw sees one size.
write(table_size / 32u, descriptor(40, Format::k8_8_8_8UNorm));
const auto elements = ImageHeapTestAccess::ElementCount(heap);
const IR::ImageHeapRequest grown{table, table_size * 2u, 32u, 0u, table_size / 16u, &sampled};
Require(name, "grown table",
heap.Acquire(grown, view) && view.slot_count == table_size / 2u &&
ImageHeapTestAccess::MemoryCount(heap) == 1u,
"a grown table was declined or kept apart from its old size");
draw();
const auto added = ImageHeapTestAccess::ImageOf(heap, filtered.slots[table_size / 4u]);
Require(name, "grown descriptor",
added && ImageHeapTestAccess::ElementCount(heap) <= elements + 1u,
"a grown table missed its new descriptor or interned its old ones again");
Require(name, "shrunk table",
acquire(sampled) && view.slot_count == table_size / 4u &&
filtered.slots.back() == 0u && textures.GetImage(added).heap_refs.load() == 0u,
"a shrunk table kept a descriptor past its end");
Require(name, "one size per draw",
heap.Acquire(grown, view) && view.slot_count == table_size / 4u,
"a draw saw two sizes of one table");
draw();
Require(name, "regrown table", heap.Acquire(grown, view), "a regrown table was declined");
draw();
Require(name, "regrown descriptor",
ImageHeapTestAccess::ImageOf(heap, filtered.slots[table_size / 4u]) == added,
"a table grown again after shrinking missed its descriptor");
heap.UnmapMemory(table, table_size);
Require(name, "unmapped table",
ImageHeapTestAccess::MemoryCount(heap) == 0u &&
@@ -5518,6 +5614,13 @@ public:
acquire(sampled, idle_table) && view.kinds.empty() &&
ImageHeapTestAccess::MemoryCount(heap) == 1u,
"an all-null table was not kept resident");
// A write past a table's end, in its watched tail page, must unwatch the page.
constexpr uint64_t unaligned_table = base + 0x1a0800;
Require(name, "tail page fault",
acquire(sampled, unaligned_table) &&
context.HandleFault(PageFaultAccess::Write, unaligned_table + table_size) &&
ImageHeapTestAccess::TailPageDirty(heap),
"a write fault next to a table left its page watched");
draw();
context.UnmapMemory(base, idle_table - base);
scheduler.Finish();
@@ -7594,6 +7697,40 @@ public:
verify_array();
}
// Image heaps keep a BC6H array and a raw block view of its first cube resident together.
// That raw read views the BC image and creates its view; a raw writer or a raw image of
// other levels, whose layers lie elsewhere, still replaces it.
for (uint32_t scenario = 0; scenario < 3; ++scenario) {
const uint64_t address = base + 0x1b00000 + scenario * 0x180000;
auto bc = MakeLinearDesc(address, 0, vk::Format::eBc6HUfloatBlock,
Prospero::BufferFormat::kBc6UFloat, Prospero::ImageType::kColor2D,
{256, 256, 1}, 12, 16, 1);
auto raw = MakeLinearDesc(address, 0, vk::Format::eR32G32B32A32Uint,
Prospero::BufferFormat::k32_32_32_32UInt, Prospero::ImageType::kColor2D,
{64, 64, 1}, 6, 16, 1);
bc.info.resources.levels = 6;
raw.info.resources.levels = scenario == 2 ? 1 : 6;
for (auto *desc : {&bc, &raw}) {
desc->info.tile_mode = Prospero::TileMode::kStandard4KB;
desc->info.UpdateSize();
desc->view_info.level_count = desc->info.resources.levels;
}
if (scenario == 1) {
raw.type = BindingType::Storage;
raw.view_info.usage = vk::ImageUsageFlagBits::eStorage;
}
const auto bc_image = texture_cache.FindImage(bc);
(void)texture_cache.FindTexture(bc_image, bc);
const auto raw_image = texture_cache.FindImage(raw);
const bool viewed = scenario == 0;
Require(name, "raw block view of a BC image",
(raw_image == bc_image) == viewed &&
texture_cache.GetImage(bc_image).registered == viewed,
"a raw block read did not view the clean BC image, or a raw writer or "
"raw image of other levels kept it");
if (viewed) (void)texture_cache.FindTexture(raw_image, raw);
}
// A write through a separate buffer must not replace untouched image
// texels with stale backing bytes. Exact-base writes still refresh it.
constexpr uint64_t buffer_alias_offset = 0x1a00000;
@@ -10547,6 +10684,98 @@ public:
std::printf("[gpu] %-32s ok\n", name);
}
void CheckColorResolvePlacedTarget() {
constexpr const char *name = "ColorResolvePlacedTarget";
constexpr uintptr_t base = 0x0000000209000000ull;
constexpr uint64_t destination = base + 0x100000;
constexpr uint32_t extent = 256;
constexpr uint64_t allocation_alignment = 0x10000;
constexpr uint64_t allocation_size = 0x200000;
EnsureRuntimeContext();
int64_t direct_offset = -1;
Require(name, "direct allocation",
Libs::LibKernel::Memory::KernelAllocateDirectMemory(
0, Libs::LibKernel::Memory::KernelGetDirectMemorySize(),
allocation_size, allocation_alignment, 0, &direct_offset) == 0,
"resolve direct-memory allocation failed");
void *mapped = reinterpret_cast<void *>(base);
Require(name, "direct mapping",
Libs::LibKernel::Memory::KernelMapDirectMemory(
&mapped, allocation_size, 0x3, 0x10, direct_offset,
allocation_alignment) == 0 &&
mapped == reinterpret_cast<void *>(base),
"resolve fixed mapping failed");
for (const bool mip : {false, true}) {
std::memset(mapped, 0, allocation_size);
RenderContext context(m_runtime_context);
auto &scheduler = context.GetCommandScheduler();
HW::Context registers{};
HW::UserConfig user_config{};
HW::Shader shaders{};
const auto set_target = [&](uint32_t slot, uint64_t address, uint32_t side,
uint32_t layers, uint32_t levels) {
registers.SetColorBase(slot, {.addr = address});
registers.SetColorInfo(slot, {.format = Prospero::ChannelLayout::k8_8_8_8,
.channel_type = Prospero::ChannelType::kUNorm,
.channel_order = Prospero::ChannelOrder::kStandard});
registers.SetColorView(slot, {.last_array_slice_index = layers - 1});
registers.SetColorAttrib2(slot, {.height = side - 1, .width = side - 1,
.num_mip_levels = levels - 1});
registers.SetColorAttrib3(slot, {.tile_mode = Prospero::TileMode::kRenderTarget,
.dimension = 1,
.metadata_pipe_aligned = true});
};
set_target(1, destination, extent, mip ? 1 : 2, mip ? 2 : 1);
registers.SetRenderTargetMask(0xff);
scheduler.Begin(registers, user_config, shaders);
auto &executor = context.GetRenderExecutor();
context.MapMemory(base, allocation_size);
RenderColorInfo whole{};
RenderExecutorTestAccess::ResolveRenderColorTarget(executor, scheduler.Current(),
whole, 1);
const auto &layout = whole.desc.info.mip_layout;
const auto side = mip ? extent / 2 : extent;
set_target(1, destination + (mip ? layout[1].offset : layout[0].size / 2), side, 1, 1);
set_target(0, base, side, 1, 1);
RenderColorInfo placed{};
RenderColorInfo source{};
RenderExecutorTestAccess::ResolveRenderColorTarget(executor, scheduler.Current(),
placed, 1);
RenderExecutorTestAccess::ResolveRenderColorTarget(executor, scheduler.Current(),
source, 0);
vk::ClearValue red{};
red.color.float32 = std::array{1.0f, 0.0f, 0.0f, 1.0f};
TextureCacheTestAccess::ClearImage(context.GetTextureCache(), scheduler.Current(),
source.image_id,
{vk::ImageAspectFlagBits::eColor, 0, 1, 0, 1}, red);
registers.SetColorControl({.mode = 3});
Require(name, "resolve",
RenderExecutorTestAccess::ResolveColorTargets(executor, scheduler.Current()),
"the resolve draw was not consumed");
Require(name, "placed subresource",
ReadCachedTexel(name, context, whole.image_id, {}, {1, 1, 1}, mip ? 0 : 1,
mip ? 1 : 0) == std::vector<u32>{0xff0000ffu} &&
ReadCachedTexel(name, context, whole.image_id) == std::vector<u32>{0u},
"the resolve wrote the register level and layer, not the placed ones");
RenderExecutorTestAccess::ResetBindings(executor);
context.UnmapMemory(base, allocation_size);
scheduler.Finish();
}
Require(name, "unmap direct backing",
Libs::LibKernel::Memory::KernelMunmap(base, allocation_size) == 0,
"resolve direct mapping release failed");
Require(name, "release direct backing",
Libs::LibKernel::Memory::KernelReleaseDirectMemory(
direct_offset, allocation_size) == 0,
"resolve direct-memory allocation release failed");
std::printf("[gpu] %-32s ok\n", name);
}
void CheckRenderExecutorColorVolumeDiscovery() {
constexpr const char *name = "RenderExecutorColorVolumeDiscovery";
constexpr uintptr_t base = 0x0000000203e00000ull;
@@ -11317,7 +11546,7 @@ public:
constexpr uintptr_t base = 0x0000000204c00000ull;
constexpr uint64_t size = 0x10000;
constexpr auto output = base + 0x2000;
constexpr u32 value = 0x80000578u;
constexpr u32 value = 0x80000578u, fill = 0xa5a5a5a5u;
std::array<std::vector<u32>, 2> codes;
for (u32 scalar = 0; scalar < codes.size(); ++scalar) {
auto &code = codes[scalar];
@@ -11356,15 +11585,18 @@ public:
context.MapMemory(base, size);
auto &cache = context.GetBufferCache();
auto &shaders = processor.GetShCtx();
struct Case { u32 mode, records; bool scalar; u32 expected; };
constexpr Case cases[]{{2, 1, false, value}, {2, 0, false, 0},
// The first case stores through a NumRecords=0 output, which binds the shared null
// buffer; a later NumRecords=0 load must still read zero.
struct Case { u32 mode, records; bool scalar; u32 expected; bool null_output = false; };
constexpr Case cases[]{{3, 4, false, fill, true},
{2, 1, false, value}, {2, 0, false, 0},
{3, 1, false, 0}, {3, 4, false, value},
{2, 1, true, 0}, {2, 4, true, value}};
for (const auto wave : {32u, 64u}) {
for (const auto prefix : {0u, 8u}) {
const auto address = base + 0x1000 + prefix;
for (const auto test : cases) {
cache.FillBuffer(output, wave * sizeof(u32), 0xa5a5a5a5u, false);
cache.FillBuffer(output, wave * sizeof(u32), fill, false);
shaders.SetCsShader({
.data_addr = reinterpret_cast<uint64_t>(codes[test.scalar].data()),
.num_thread_x = wave, .num_thread_y = 1, .num_thread_z = 1,
@@ -11376,7 +11608,7 @@ public:
(static_cast<u32>(Prospero::BufferFormat::k32UInt) << 12u) |
(test.mode << 28u);
out.UpdateAddress48(output);
out.fields[2] = wave * sizeof(u32);
out.fields[2] = test.null_output ? 0u : wave * sizeof(u32);
out.fields[3] = DstSel(4, 5, 6, 7) |
(static_cast<u32>(Prospero::BufferFormat::k32UInt) << 12u) | (3u << 28u);
for (u32 i = 0; i < 4; ++i) {
@@ -12274,6 +12506,10 @@ public:
PackedCase{vk::Format::eR16G16Unorm, 0x0000ffffu, {1.0f, 0.0f}},
PackedCase{vk::Format::eR16G16Unorm, 0x80000001u,
{1.0f / 65535.0f, 32768.0f / 65535.0f}},
PackedCase{vk::Format::eR16G16B16A16Unorm, 0x000180000000ffffull,
{1.0f, 0.0f, 32768.0f / 65535.0f, 1.0f / 65535.0f}},
PackedCase{vk::Format::eR8Unorm, 0xffffff80u, {128.0f / 255.0f}},
PackedCase{vk::Format::eR8G8Unorm, 0xffff40ffu, {1.0f, 64.0f / 255.0f}},
};
for (const auto &test : packed_cases) {
vk::ClearColorValue clear{};
@@ -12300,10 +12536,45 @@ public:
DecodeColorDwordFill(format, 0x3c003c00u, clear),
"alternating R16 pixels were treated as a uniform clear");
}
struct UintCase {
vk::Format format;
uint64_t word;
std::array<uint32_t, 4> channels;
};
for (const auto &test : {UintCase{vk::Format::eR8Uint, 0xffffff05u, {5u}},
UintCase{vk::Format::eR8G8Uint, 0xffff0a05u, {5u, 10u}},
UintCase{vk::Format::eR16Uint, 0xffff1234u, {0x1234u}},
UintCase{vk::Format::eR16G16B16A16Uint, 0x4444333322221111ull,
{0x1111u, 0x2222u, 0x3333u, 0x4444u}},
UintCase{vk::Format::eR16G16B16A16Sint, 0x7fff0001ffff8000ull,
{0xffff8000u, 0xffffffffu, 1u, 0x7fffu}},
UintCase{vk::Format::eR32G32Uint, 0x89abcdef01234567ull,
{0x01234567u, 0x89abcdefu}},
UintCase{vk::Format::eR32G32Sint, 0x80000000ffffffffull,
{0xffffffffu, 0x80000000u}}}) {
Require(name, "packed integer color clear",
DecodePackedColorClear(test.format, test.word, clear) &&
std::bit_cast<std::array<uint32_t, 4>>(clear.uint32) == test.channels,
"integer clear lost a channel, its sign or bits of the next one");
}
Require(name, "R8 DWORD fill",
!DecodeColorDwordFill(vk::Format::eR8Uint, 0x01020304u, clear) &&
DecodeColorDwordFill(vk::Format::eR8Uint, 0x07070707u, clear) &&
clear.uint32[0] == 7u,
"differing R8 pixels were treated as a uniform clear");
Require(name, "RG8 DWORD fill",
!DecodeColorDwordFill(vk::Format::eR8G8Unorm, 0x00ff00feu, clear) &&
DecodeColorDwordFill(vk::Format::eR8G8Unorm, 0x00ff00ffu, clear) &&
clear.float32[0] == 1.0f && clear.float32[1] == 0.0f,
"alternating RG8 pixels were treated as a uniform clear");
Require(name, "RG32 DWORD fill",
DecodeColorDwordFill(vk::Format::eR32G32Sfloat, 0x3f800000u, clear) &&
clear.float32[0] == 1.0f && clear.float32[1] == 1.0f,
"a repeated DWORD fill was decoded as a two-register clear");
Require(name, "RGBA16 DWORD fill",
DecodeColorDwordFill(vk::Format::eR16G16B16A16Uint, 0x22221111u, clear) &&
clear.uint32[2] == 0x1111u && clear.uint32[3] == 0x2222u,
"a DWORD fill did not repeat across the 64-bit pixel");
constexpr uintptr_t base = 0x0000000204100000ull;
constexpr uint64_t allocation_size = 0x1000000;
constexpr uint64_t allocation_alignment = 0x10000;
@@ -17730,7 +18001,7 @@ public:
}
std::vector<u32> ReadBuffer(const char *shader_name, const Buffer &buffer,
size_t dword_count) {
size_t dword_count, size_t first_dword = 0) {
if (!buffer.coherent) {
vk::MappedMemoryRange range{};
range.sType = vk::StructureType::eMappedMemoryRange;
@@ -17747,7 +18018,7 @@ public:
m_device.mapMemory(buffer.memory, 0, buffer.size, {}, &data),
"vkMapMemory");
std::vector<u32> ret(dword_count, 0);
std::memcpy(ret.data(), data, dword_count * sizeof(u32));
std::memcpy(ret.data(), static_cast<const u32 *>(data) + first_dword, dword_count * sizeof(u32));
m_device.unmapMemory(buffer.memory);
return ret;
}
@@ -17977,6 +18248,7 @@ public:
Require(test.name, "dispatch", info.range <= buffer.size,
"storage buffer descriptor range exceeds backing buffer");
}
shader_data[layout.MemorySizeDword() + index] = static_cast<u32>(info.range);
}
vk::WriteDescriptorSet write{};
write.sType = vk::StructureType::eWriteDescriptorSet;
@@ -19647,6 +19919,40 @@ public:
}
Require(name, "packed rounding program distinction", half_up_program != truncate_program,
"half-up and truncate exports shared one shader specialization");
// UINT and SINT targets give their exports integer outputs.
registers.Reset();
registers.SetViewportTransformControl(0x300);
registers.SetRenderTargetMask(0xffu);
registers.SetShaderMask(0xffu);
shaders.SetPsShaderBase(pixel_address);
for (const auto [slot, type] : {std::pair{0u, Prospero::ChannelType::kUInt},
std::pair{1u, Prospero::ChannelType::kSInt}}) {
registers.SetTargetOutputMode(slot, 9);
registers.SetColorBase(slot, {.addr = depth_address + 0x10000 * (slot + 1)});
registers.SetColorInfo(slot, {.format = Prospero::ChannelLayout::k32, .channel_type = type,
.channel_order = Prospero::ChannelOrder::kStandard});
registers.SetColorAttrib2(slot, {.height = extent - 1, .width = extent - 1});
registers.SetColorAttrib3(slot, {.tile_mode = Prospero::TileMode::kLinear, .dimension = 1});
}
native_programs();
Require(name, "integer target outputs",
pixel.target_uint_mask == 1u && pixel.target_sint_mask == 2u,
"UINT and SINT targets did not select integer export outputs");
// MRT1 is the second blend source of a dual-source float target 0, not integer target 1.
HW::BlendControl dual_blend{};
dual_blend.enable = true;
dual_blend.color_srcblend = static_cast<uint8_t>(Prospero::BlendFactor::kSrc1Color);
registers.SetBlendControl(0, dual_blend);
for (const auto [slot, type] : {std::pair{0u, Prospero::ChannelType::kFloat},
std::pair{1u, Prospero::ChannelType::kUInt}}) {
registers.SetColorInfo(slot, {.format = Prospero::ChannelLayout::k32, .channel_type = type,
.channel_order = Prospero::ChannelOrder::kStandard});
native_programs();
Require(name, "dual-source float outputs",
pixel.dual_source_blending && pixel.target_uint_mask == 0u &&
pixel.target_sint_mask == 0u,
"the second blend source kept the integer type of render target 1");
}
DestroyBuffer(&packed_output);
m_device.destroySampler(packed_sampler);
DestroyBuffer(&stencil_readback);
@@ -19724,8 +20030,40 @@ public:
push_constant_range.size = ShaderRecompiler::IR::NativePushConstantSize;
}
using Kind = ShaderRecompiler::IR::DescriptorBindingKind;
const bool uses_gds = !test.gds_initial.empty();
const auto gds_bit = uint64_t{1} << static_cast<u32>(Kind::Gds);
Require(test.name, "graphics", fragment_bind.descriptor_mask == (uses_gds ? gds_bit : 0u),
"a graphics case binds GDS only, and only with initial GDS data");
Buffer gds_buffer;
vk::DescriptorBufferInfo gds_info{};
vk::WriteDescriptorSet gds_write{};
vk::DescriptorSetLayout descriptor_layout = nullptr;
if (uses_gds) {
gds_buffer = CreateStorageBuffer(test.name, test.gds_initial, test.gds_initial.size());
gds_info = {gds_buffer.buffer, 0, gds_buffer.size};
const vk::DescriptorSetLayoutBinding binding{
ShaderRecompiler::IR::NativeBinding(ShaderType::Pixel, Kind::Gds),
vk::DescriptorType::eStorageBuffer, 1, vk::ShaderStageFlagBits::eFragment};
vk::DescriptorSetLayoutCreateInfo layout_info{};
layout_info.sType = vk::StructureType::eDescriptorSetLayoutCreateInfo;
layout_info.flags = vk::DescriptorSetLayoutCreateFlagBits::ePushDescriptorKHR;
layout_info.bindingCount = 1;
layout_info.pBindings = &binding;
RequireVk(test.name, "graphics",
m_device.createDescriptorSetLayout(&layout_info, nullptr, &descriptor_layout),
"vkCreateDescriptorSetLayout");
gds_write.sType = vk::StructureType::eWriteDescriptorSet;
gds_write.dstBinding = binding.binding;
gds_write.descriptorCount = 1;
gds_write.descriptorType = vk::DescriptorType::eStorageBuffer;
gds_write.pBufferInfo = &gds_info;
}
vk::PipelineLayoutCreateInfo pipeline_layout_info{};
pipeline_layout_info.sType = vk::StructureType::ePipelineLayoutCreateInfo;
pipeline_layout_info.setLayoutCount = uses_gds ? 1u : 0u;
pipeline_layout_info.pSetLayouts = &descriptor_layout;
pipeline_layout_info.pushConstantRangeCount =
push_constant_range.size != 0 ? 1u : 0u;
pipeline_layout_info.pPushConstantRanges =
@@ -19882,6 +20220,10 @@ public:
}
cmd.beginRendering(rendering);
cmd.bindPipeline(vk::PipelineBindPoint::eGraphics, pipeline);
if (uses_gds) {
cmd.pushDescriptorSetKHR(vk::PipelineBindPoint::eGraphics, pipeline_layout, 0, 1,
&gds_write);
}
if (push_constant_range.size != 0) {
ShaderRecompiler::IR::PushData push_data;
if (object_id) {
@@ -19909,6 +20251,8 @@ public:
m_device.destroyPipeline(pipeline, nullptr);
m_device.destroyPipelineLayout(pipeline_layout, nullptr);
m_device.destroyDescriptorSetLayout(descriptor_layout, nullptr);
DestroyBuffer(&gds_buffer);
m_device.destroyShaderModule(fragment_module, nullptr);
m_device.destroyShaderModule(vertex_module, nullptr);
DestroyBuffer(&vertex_buffer);
@@ -21398,8 +21742,9 @@ private:
"shaderStorageImageWriteWithoutFormat is not supported");
Require("VulkanHarness", "dispatch",
available_features.shaderImageGatherExtended == true &&
available_features.shaderResourceMinLod == true &&
available_derivatives.computeDerivativeGroupQuads == true,
"image gather or compute derivative quads are not supported");
"image gather, MinLod or compute derivative quads are not supported");
Require("VulkanHarness", "dispatch",
available_features11.storageBuffer16BitAccess == true &&
available_features12.storageBuffer8BitAccess == true,
@@ -21443,6 +21788,7 @@ private:
available_features.drawIndirectFirstInstance &&
available_features.multiDrawIndirect &&
available_features.depthBounds &&
available_features.fragmentStoresAndAtomics &&
available_depth_clip.depthClipEnable &&
available_clip_control.depthClipControl &&
available_color_write.colorWriteEnable &&
@@ -21533,6 +21879,7 @@ private:
device_features.shaderStorageImageWriteWithoutFormat = true;
device_features.shaderStorageImageMultisample = available_features.shaderStorageImageMultisample;
device_features.shaderImageGatherExtended = true;
device_features.shaderResourceMinLod = true;
device_features.sampleRateShading = true;
device_features.shaderInt64 = true;
device_features.shaderFloat64 = available_features.shaderFloat64;
@@ -21541,6 +21888,7 @@ private:
device_features.drawIndirectFirstInstance = m_rasterization_supported;
device_features.multiDrawIndirect = m_rasterization_supported;
device_features.depthBounds = m_rasterization_supported;
device_features.fragmentStoresAndAtomics = m_rasterization_supported;
device_info.pEnabledFeatures = &device_features;
std::vector<const char *> device_extensions{
VK_KHR_PUSH_DESCRIPTOR_EXTENSION_NAME,
@@ -22675,6 +23023,23 @@ TestCase Rdna2ScalarOpcodes() {
O::S_SLEEP, O::V_MOV_B32, O::BUFFER_STORE_DWORD, O::S_ENDPGM}};
}
TestCase ScalarApertureLimits() {
using O = ShaderOpcode;
std::vector<u32> code;
code.push_back(EncodeSop1(0x04, 20, 236));
AppendStoreSgprPair(&code, 20, 0);
code.push_back(EncodeSop1(0x04, 20, 238));
AppendStoreSgprPair(&code, 20, 2);
AppendEnd(&code);
return {"ScalarApertureLimits",
code,
{},
{0xffffffffu, 0x80000000u, 0xffffffffu, 0x70000000u},
{O::S_MOV_B64, O::V_MOV_B32, O::BUFFER_STORE_DWORD, O::S_ENDPGM}};
}
TestCase ScalarTrapDisabledFallsThroughExactRaw() {
using O = ShaderOpcode;
@@ -39063,6 +39428,48 @@ TestCase ImageGatherExplicitLod() {
return test;
}
TestCase ImageSampleLodClamp() {
using O = ShaderOpcode;
// _cl clamps the computed LOD from below: compute's implicit LOD and zero
// derivatives would both select mip 0, so a clamp of 1 must read mip 1.
std::vector<u32> code;
AppendVMovU32(&code, 20, 0);
AppendVMovU32(&code, 21, 0);
AppendVMovU32(&code, 22, 0);
AppendVMovU32(&code, 23, 0);
AppendVMovLiteral(&code, 24, 0x3f000000u);
AppendVMovLiteral(&code, 25, 0x3f000000u);
AppendVMovLiteral(&code, 26, 0x3f800000u);
code.push_back(EncodeMimg0(0x21, 0x1));
code.push_back(EncodeMimg1(0, 24, 0, 2));
code.push_back(EncodeMimg0(0x23, 0x1));
code.push_back(EncodeMimg1(1, 20, 0, 2));
AppendStoreVgpr(&code, 0, 0);
AppendStoreVgpr(&code, 1, 1);
AppendEnd(&code);
TestCase test;
test.name = "ImageSampleLodClamp";
test.code = std::move(code);
test.opcodes = {O::V_MOV_B32, O::IMAGE_SAMPLE, O::BUFFER_STORE_DWORD, O::S_ENDPGM};
test.expected = std::vector<u32>(2, 0x41000000u);
test.sampled_image_rgba_mips = {std::vector<u32>(4 * 4 * 4, 0x3f800000u),
std::vector<u32>(2 * 2 * 4, 0x41000000u)};
test.user_data = MakeSampledTextureData(Prospero::BufferFormat::k32_32_32_32Float);
test.user_data[1] |= 3u << 30u;
test.user_data[2] = 3u << 14u;
test.user_data[3] |= 1u << 16u;
test.user_data[8] = 0x92u;
test.user_data[9] = 0xfff000u;
test.user_data[10] = 0x05000000u;
test.user_data[50] = test.expected.size() * sizeof(u32);
test.user_data[51] = 3u << 28u;
test.has_user_data = true;
test.required_spirv = {"MinLod"};
return test;
}
template <u32 mode> TestCase ImageGatherLodPerLane() {
using O = ShaderOpcode;
constexpr std::array names{
@@ -40570,6 +40977,50 @@ void CheckFiniteBufferSelection(VulkanHarness &vulkan) {
}
}
void CheckStorageBufferBindingOver2GiB(VulkanHarness &vulkan) {
// OpArrayLength reads 0 for ranges of 2 GiB or more on NVIDIA; bounds use the pushed binding size.
constexpr u32 bytes = 0x80000010u, end = bytes / 4u, last = end - 1u;
TestCase test;
test.name = "StorageBufferBindingOver2GiB";
Require(test.name, "device limit",
vulkan.RuntimeContext().GetPhysicalDeviceProperties().limits.maxStorageBufferRange >= bytes,
"storage buffer range cannot cover 2 GiB");
AppendVMovLiteral(&test.code, 4, last * 4u);
AppendVMovLiteral(&test.code, 6, end * 4u);
test.code.insert(test.code.end(), {EncodeMubuf0(0x0c), EncodeMubuf1(5, 0, 4),
EncodeMubuf0(0x0c), EncodeMubuf1(7, 0, 6),
EncodeMubuf0(0x1c), EncodeMubuf1(5, 0, 6), EncodeSopp(0x0c, 0)});
AppendStoreVgpr(&test.code, 5, 4);
AppendStoreVgpr(&test.code, 7, 5);
AppendEnd(&test.code);
test.user_data = MakeStructuredStorageBufferData(0, bytes);
test.user_data[50] = 1u << 20u;
test.user_data[51] = 3u << 28u;
test.has_user_data = true;
test.forbidden_spirv = {"OpArrayLength"};
const auto compiled = CompileCase(test, vulkan.SubgroupSize());
for (const auto &value : compiled.resources.buffers) {
ShaderBufferResource descriptor{};
std::copy_n(value.dwords.begin(), 4u, descriptor.fields);
test.storage_buffer_ranges.emplace_back(descriptor.Base48(), u32(descriptor.GetSize()));
}
VulkanHarness::Buffer buffer;
{
std::vector<u32> initial(end + 4u, 0u);
initial[5] = 0xdeadbeefu;
initial[last] = 0x44444444u;
initial[end] = 0xcafebabeu;
buffer = vulkan.CreateStorageBuffer(test.name, initial, initial.size());
}
vulkan.Dispatch(test, compiled, buffer);
const auto loads = vulkan.ReadBuffer(test.name, buffer, 2u, 4u);
const auto canary = vulkan.ReadBuffer(test.name, buffer, 1u, end);
vulkan.DestroyBuffer(&buffer);
CompareWords(test, "a load at the last dword, a zero load and a dropped store at the 2 GiB range end",
{0x44444444u, 0u, 0xcafebabeu}, {loads[0], loads[1], canary[0]});
std::printf("[compute] %-32s ok\n", test.name);
}
void CheckIndirectBufferStore(VulkanHarness &vulkan) {
using namespace ShaderRecompiler::IR;
TestCase test;
@@ -40682,23 +41133,20 @@ void CheckIndirectBufferStore(VulkanHarness &vulkan) {
test.required_spirv = {"StorageBufferArrayNonUniformIndexing"};
test.forbidden_spirv.push_back("OpSwitch");
std::set<u32> nonuniform;
std::vector<u32> accessed_pointers;
std::vector<u32> stored_pointers;
u32 prefix_extracts = 0;
for (size_t offset = 5; offset < compiled.spirv.size();) {
const auto words = std::span(compiled.spirv).subspan(offset, compiled.spirv[offset] >> 16u);
const auto opcode = static_cast<spv::Op>(words[0] & 0xffffu);
if (opcode == spv::OpDecorate && words[2] == spv::DecorationNonUniform)
nonuniform.insert(words[1]);
if (opcode == spv::OpArrayLength) accessed_pointers.push_back(words[3]);
if (opcode == spv::OpStore) accessed_pointers.push_back(words[1]);
if (opcode == spv::OpStore) stored_pointers.push_back(words[1]);
if (opcode == spv::OpBitFieldUExtract) ++prefix_extracts;
offset += words.size();
}
Require(test.name, "shared store and nonuniform pointers",
prefix_extracts == 1u && accessed_pointers.size() == 2u &&
std::ranges::all_of(accessed_pointers, [&](u32 pointer) {
return nonuniform.contains(pointer);
}),
prefix_extracts == 1u && stored_pointers.size() == 1u &&
nonuniform.contains(stored_pointers.front()),
"indirect store retained static prefixes, was duplicated or lost a NonUniform pointer decoration");
} else if (single) {
test.forbidden_spirv.push_back("OpSwitch");
@@ -43801,6 +44249,92 @@ GraphicsCase GraphicsDsAddtidScratchExport() {
O::DS_READ_ADDTID_B32, O::EXP, O::S_ENDPGM}};
}
// Covers only pixel 1 of a 2x1 target, so the rest of its quad runs as helper invocations, and
// starts a GDS counter at 10 in dword 5.
void CoverOnePixelOfQuad(GraphicsCase *test) {
test->width = 2;
test->vertices = {0x00000000u, 0xbf800000u, 0, 0, 0, 0,
0x40400000u, 0xbf800000u, 0, 0, 0, 0,
0x00000000u, 0x40400000u, 0, 0, 0, 0};
test->gds_initial = {0, 0, 0, 0, 0, 10};
}
GraphicsCase GraphicsDsAppendSkipsHelperLanes(bool consume) {
using O = ShaderOpcode;
// The EXEC and VCC words exclude the helper lanes. Exports the first counter value, the second
// minus the first minus s_bcnt1(exec) (reversed for consume), s_bcnt1(exec), and the EXEC plus
// VCC counts after an if/endif on a compare of the interpolant (zero in every lane).
const u32 op = consume ? 0x3d : 0x3e;
std::vector<u32> code;
code.push_back(EncodeVintrp(0x00, 7, 0, 0, 0));
code.push_back(EncodeVintrp(0x01, 7, 0, 0, 1));
AppendSMovLiteral(&code, 124, 0x100);
code.push_back(EncodeDs0(op, 20, true));
code.push_back(EncodeDs1(1, 0, 0));
code.push_back(EncodeDs0(op, 20, true));
code.push_back(EncodeDs1(2, 0, 0));
code.push_back(EncodeSop1(0x10, 0, 126));
code.push_back(EncodeVop2(0x26, 3, Vgpr(consume ? 1 : 2), consume ? 2 : 1));
code.push_back(EncodeVop2(0x27, 3, 0, 3));
code.push_back(EncodeVop1(0x06, 4, Vgpr(1)));
code.push_back(EncodeVop1(0x05, 5, Vgpr(3)));
code.push_back(EncodeVop1(0x06, 6, 0));
code.push_back(EncodeVopc(0xc2, InlineU32(0), 7));
code.push_back(EncodeSop1(0x24, 2, 106));
code.push_back(EncodeSop2(0x11, 126, 126, 2));
code.push_back(EncodeSop1(0x10, 1, 126));
code.push_back(EncodeSop1(0x10, 4, 106));
code.push_back(EncodeSop2(0x00, 1, 1, 4));
code.push_back(EncodeVop1(0x06, 7, 1));
code.push_back(EncodeExp0(0x00, 0xf));
code.push_back(EncodeExp1(4, 5, 6, 7));
AppendEnd(&code);
GraphicsCase test;
test.name = consume ? "GraphicsDsConsumeSkipsHelperLanes" : "GraphicsDsAppendSkipsHelperLanes";
test.fragment_code = std::move(code);
CoverOnePixelOfQuad(&test);
test.expected_pixel = {0, 0, 0, 0, 0x41200000u, 0, 0x3f800000u, 0x40000000u};
test.opcodes = {O::V_INTERP_P1_F32, O::V_INTERP_P2_F32, O::S_MOV_B32,
consume ? O::DS_CONSUME : O::DS_APPEND, O::S_BCNT1_I32_B64,
O::V_SUB_NC_U32, O::V_SUBREV_NC_U32, O::V_CVT_F32_U32, O::V_CVT_F32_I32,
O::V_CMP_EQ_U32, O::S_AND_SAVEEXEC_B64, O::S_OR_B64, O::S_ADD_U32, O::EXP,
O::S_ENDPGM};
return test;
}
GraphicsCase GraphicsDsOrderedCountSkipsHelperLanes() {
using O = ShaderOpcode;
// A live lane adds the first lane's ADDR value (1) for the wave. Exports the first counter
// value and the difference to the second.
std::vector<u32> code;
code.push_back(EncodeVintrp(0x00, 7, 0, 0, 0));
code.push_back(EncodeVintrp(0x01, 7, 0, 0, 1));
code.push_back(EncodeVop1(0x01, 2, InlineU32(1)));
AppendSMovLiteral(&code, 124, 0x00100000u); // M0 base 0x10; offset0 adds 4: dword 5.
code.push_back(EncodeDs0(0x3f, 7, true));
code.push_back(EncodeDs1(1, 2, 2));
code.push_back(EncodeDs0(0x3f, 7, true));
code.push_back(EncodeDs1(3, 2, 2));
code.push_back(EncodeVop2(0x26, 4, Vgpr(3), 1));
code.push_back(EncodeVop1(0x06, 5, Vgpr(1)));
code.push_back(EncodeVop1(0x06, 6, Vgpr(4)));
code.push_back(EncodeExp0(0x00, 0xf));
code.push_back(EncodeExp1(5, 6, 6, 6));
AppendEnd(&code);
GraphicsCase test;
test.name = "GraphicsDsOrderedCountSkipsHelperLanes";
test.fragment_code = std::move(code);
CoverOnePixelOfQuad(&test);
test.expected_pixel = {0, 0, 0, 0, 0x41200000u, 0x3f800000u, 0x3f800000u, 0x3f800000u};
test.opcodes = {O::V_INTERP_P1_F32, O::V_INTERP_P2_F32, O::V_MOV_B32, O::S_MOV_B32,
O::DS_ORDERED_COUNT, O::V_SUB_NC_U32, O::V_CVT_F32_U32, O::EXP, O::S_ENDPGM};
return test;
}
GraphicsCase GraphicsDirectSgprPushConstantExport() {
using O = ShaderOpcode;
@@ -44066,6 +44600,7 @@ std::vector<TestCase> MakeCases() {
AddCase(ExactPushConstantExtent);
AddCase(ScalarShiftCountsMaskLowBits);
AddCase(Rdna2ScalarOpcodes);
AddCase(ScalarApertureLimits);
AddCase(ScalarTrapDisabledFallsThroughExactRaw);
AddCase(ScalarWaitcntDepctrCapturedVmVsrc);
AddCase(ScalarExtendedArithmetic);
@@ -44586,6 +45121,7 @@ std::vector<TestCase> MakeCases() {
AddCase(ImageSampleAndGather);
AddCase(ImageCubeGradientsPreserveDerivatives);
AddCase(ImageGatherExplicitLod);
AddCase(ImageSampleLodClamp);
AddCase(ImageGatherLodPerLane<0>);
AddCase(ImageGatherLodPerLane<1>);
AddCase(ImageGatherLodPerLane<2>);
@@ -44671,6 +45207,9 @@ std::vector<GraphicsCase> MakeGraphicsCases() {
GraphicsFrontFaceBits(true, true),
GraphicsFlatInterpolatorExport(),
GraphicsDsAddtidScratchExport(),
GraphicsDsAppendSkipsHelperLanes(false),
GraphicsDsAppendSkipsHelperLanes(true),
GraphicsDsOrderedCountSkipsHelperLanes(),
GraphicsDirectSgprPushConstantExport(),
GraphicsInlineSrtScalarPromotionExport(),
GraphicsNullVmExportDiscardsInactiveExec(),
@@ -49762,6 +50301,13 @@ void CheckPm4DrawIndirectMultiPacket(RenderContext &renderer) {
KYTY_PM4(10, Pm4::IT_DRAW_INDIRECT_MULTI, 0), 0,
auto_packet[2], auto_packet[3], Pm4::SH_NOP, 2, 0, 0, 16, 2};
check_offsets(auto_multi_packet, hs, {53, 15}, "non-indexed multi-draw offsets");
// Word 4 bit 31 has the CP write each subdraw's index, even with no offsets to patch.
shaders.SetHsUserSgpr(11, 77);
auto_multi_packet[2] = auto_multi_packet[3] = Pm4::SH_NOP;
auto_multi_packet[4] = (1u << 31u) | (Pm4::SPI_SHADER_USER_DATA_HS_0 + 11);
Require("Pm4DrawIndirectMulti", "multi-draw index",
execute(auto_multi_packet.data(), auto_multi_packet.size()) && hs[11] == 1,
"multi-draw did not write the last subdraw index");
arguments = {0, 1, 99, 101};
auto_packet[2] = Pm4::SH_NOP;
check_offsets(auto_packet, hs, {53, 101}, "independent destinations");
@@ -50661,6 +51207,13 @@ int main(int argc, char **argv) {
RunCase(&vulkan, Vop3pIntegerNegationCapturedAndSelectedHalves());
return 0;
}
if (argc == 2 && std::strcmp(argv[1], "--helper-lanes-only") == 0) {
VulkanHarness vulkan;
RunGraphicsCase(&vulkan, GraphicsDsAppendSkipsHelperLanes(false));
RunGraphicsCase(&vulkan, GraphicsDsAppendSkipsHelperLanes(true));
RunGraphicsCase(&vulkan, GraphicsDsOrderedCountSkipsHelperLanes());
return 0;
}
if (argc == 2 && std::strcmp(argv[1], "--dpp-only") == 0) {
VulkanHarness vulkan;
RunGraphicsCase(&vulkan, GraphicsSparseWaveMinimum());
@@ -50996,6 +51549,7 @@ int main(int argc, char **argv) {
VulkanHarness vulkan;
vulkan.CheckComparisonDepthTexture();
RunCase(&vulkan, ImageGatherExplicitLod());
RunCase(&vulkan, ImageSampleLodClamp());
RunCase(&vulkan, ImageGatherLodPerLane<0>());
RunCase(&vulkan, ImageGatherLodPerLane<1>());
RunCase(&vulkan, ImageGatherLodPerLane<2>());
@@ -51733,6 +52287,7 @@ int main(int argc, char **argv) {
CheckGuardedWaterfallBufferRecords(vulkan);
CheckGpuSelectedFormattedRecordStore(vulkan);
CheckFiniteBufferSelection(vulkan);
CheckStorageBufferBindingOver2GiB(vulkan);
CheckRuntimeBufferRecords(vulkan);
CheckSharedIndirectBufferLoads(vulkan);
CheckComputeThreadDimensions(vulkan);
@@ -51772,6 +52327,7 @@ int main(int argc, char **argv) {
vulkan.CheckGraphicsPushConstantBank();
vulkan.CheckRenderExecutorColorDiscovery();
vulkan.CheckRenderExecutorColor1DArrayDiscovery();
vulkan.CheckColorResolvePlacedTarget();
vulkan.CheckRenderExecutorColorVolumeDiscovery();
vulkan.CheckRenderExecutorColorMetadataClear();
vulkan.CheckSampledDccClear();
+129 -21
View File
@@ -1847,6 +1847,31 @@ void TestShaderStageBarriers() {
"SPIR-V barrier does not use workgroup acquire-release memory semantics");
CheckSpirvBinaryValidates(result.spirv);
// s_barrier also acquires other waves' buffer and image stores.
const auto acquires = [](std::span<const uint32_t> code,
const ShaderRecompiler::CompileOptions &options) {
const auto result = RecompileForTest(code, options);
CheckSpirvBinaryValidates(result.spirv);
// AcquireRelease | UniformMemory | WorkgroupMemory | ImageMemory.
return DisassembleSpirvBinary(result.spirv).find(
"OpControlBarrier %uint_2 %uint_2 %uint_2376") != std::string::npos;
};
const uint32_t buffer_store[] = {
EncodeMubuf0(0x1c, 0, false), EncodeMubuf1(1, 0, 0), // buffer_store_dword
EncodeSopp(0x0a), EncodeSopp(0x01),
};
Check(acquires(buffer_store, MakeCompileOptions(ShaderType::Compute)),
"s_barrier after buffer stores does not acquire buffer memory");
const uint32_t image_store[] = {
EncodeMimg0(0x08, 0xf), EncodeMimg1(20, 0, 0, 4), // image_store
EncodeSopp(0x0a), EncodeSopp(0x01),
};
const auto image_user_data = ImageTestUserData();
auto image_options = MakeCompileOptions(ShaderType::Compute);
image_options.user_data = image_user_data;
Check(acquires(image_store, image_options),
"s_barrier after image stores does not acquire image memory");
const auto vertex_result =
RecompileForTest(shader, MakeCompileOptions(ShaderType::Vertex));
CheckSpirvBinaryValidates(vertex_result.spirv);
@@ -7464,7 +7489,7 @@ void TestNewShaderRecompilerMubufFormatTranslation() {
CheckSpirvBinaryValidates(result.spirv);
}
void TestNewShaderRecompilerFormattedStoreUsesRuntimeArrayLengthOnly() {
void TestNewShaderRecompilerFormattedStoreUsesRuntimeBindingSizeOnly() {
const uint32_t shader[] = {
EncodeMubuf0(0x04),
EncodeMubuf1(0, 0, 1), // buffer_store_format_x
@@ -7491,8 +7516,9 @@ void TestNewShaderRecompilerFormattedStoreUsesRuntimeArrayLengthOnly() {
CheckSpirvBinaryValidates(result.spirv);
const auto source = DisassembleSpirvBinary(result.spirv);
Check((source.find("OpArrayLength") != std::string::npos),
"formatted store SPIR-V lacks runtime storage-buffer bounds check");
Check(source.find("OpULessThan") != std::string::npos &&
source.find("OpArrayLength") == std::string::npos,
"formatted store SPIR-V lacks a bounds check against its pushed binding size");
Check(
!SpirvSourceHasInstructionUsing(source, "OpULessThan", "%uint_5"),
"formatted store SPIR-V baked descriptor NumRecords into a store guard");
@@ -9553,6 +9579,28 @@ void TestNativeConstantSamplerSelection() {
CheckSpirvBinaryValidates(result.spirv);
}
void TestPackedZeroHighSamplerTableKey() {
// s_pack_ll_b32_b16 with a zero high lane zero-extends a GPU-selected table key, so the
// sampler table stays bounded to 64Ki records.
const uint32_t shader[] = {
EncodeMubuf0(0x0c, 0, true), EncodeMubuf1(1, 3, 0), EncodeSopp(0x0c, 0),
EncodeVop1(0x02, 28, 257), EncodeSop2(0x32, 29, 28, 128),
EncodeSop2(0x1e, 30, 29, 132),
EncodeSmem0(0x0a, 24, 4), 30u << 25u, EncodeSopp(0x0c, 0),
EncodeMimg0(0x20, 0x1), EncodeMimg1(0, 4, 6, 2),
EncodeExp0(0x00, 0x1), EncodeExp1(0, 0, 0, 0),
EncodeSopp(0x01),
};
auto options = MakeCompileOptions(ShaderType::Pixel);
options.user_data = ImageTestUserData();
const auto translated = ShaderRecompiler::TranslateProgram(shader, options);
const auto plan = ShaderRecompiler::IR::ExtractResourcePlan(translated.program);
const auto& indirect =
plan.descriptor_sources[plan.info.samplers.at(0).source].indirect_descriptor;
Check(indirect && indirect->table_stride == 16u && indirect->table_maximum == 0xffffu,
"a zero-extended sampler table key lost its 16-bit bound");
}
void TestBoundedMaterialBufferStores(bool scalar_key) {
// Captured batch search and descriptor waterfall, with deformation arithmetic removed.
const uint32_t shader[] = {
@@ -10616,19 +10664,18 @@ void TestNewShaderRecompilerBufferLoadsGuardedByExec() {
const auto source = DisassembleSpirvBinary(result.spirv);
const auto exec_branch =
source.find("OpBranchConditional", 0);
const auto array_length =
source.find("OpArrayLength", 0);
const auto bounds_branch = source.find("OpBranchConditional", array_length);
const auto bounds = source.find("OpULessThan", 0);
const auto bounds_branch = source.find("OpBranchConditional", bounds);
const auto element_access = source.find("OpAccessChain %_ptr_StorageBuffer_uint", 0);
Check(exec_branch != std::string::npos,
"buffer load SPIR-V lacks EXEC guard branch");
Check(array_length != std::string::npos,
"buffer load SPIR-V lacks storage buffer array-length bounds check");
Check(bounds != std::string::npos,
"buffer load SPIR-V lacks storage buffer bounds check");
Check(bounds_branch != std::string::npos,
"buffer load SPIR-V lacks storage buffer bounds branch");
Check(element_access != std::string::npos,
"buffer load SPIR-V lacks storage element access");
Check(exec_branch < array_length,
Check(exec_branch < bounds,
"buffer load bounds check was emitted outside EXEC guard");
Check(bounds_branch < element_access,
"buffer load storage element pointer was formed before bounds guard");
@@ -10661,13 +10708,12 @@ void TestNewShaderRecompilerBufferAtomicsGuardedByBounds() {
CheckSpirvBinaryValidates(result.spirv);
const auto source = DisassembleSpirvBinary(result.spirv);
const auto array_length =
source.find("OpArrayLength", 0);
const auto bounds_branch = source.find("OpBranchConditional", array_length);
const auto bounds = source.find("OpULessThan", 0);
const auto bounds_branch = source.find("OpBranchConditional", bounds);
const auto atomic = source.find(test.spirv, 0);
const auto memory_barrier =
source.find("OpMemoryBarrier", atomic);
Check(array_length != std::string::npos &&
Check(bounds != std::string::npos &&
bounds_branch != std::string::npos &&
atomic != std::string::npos &&
memory_barrier != std::string::npos,
@@ -10716,14 +10762,13 @@ void TestCapturedBufferAtomicsX2() {
"64-bit buffer atomic storage view does not use eight-byte elements");
Check(CountSourceOccurrences(source, "Aliased") == 2u,
"both storage-buffer views must declare that they alias");
const auto array_length =
source.find("OpArrayLength", 0);
const auto bounds_branch = source.find("OpBranchConditional", array_length);
const auto bounds = source.find("OpULessThan", 0);
const auto bounds_branch = source.find("OpBranchConditional", bounds);
const auto atomic =
source.find(test.spirv_name, 0);
const auto memory_barrier =
source.find("OpMemoryBarrier", atomic);
Check(array_length != std::string::npos &&
Check(bounds != std::string::npos &&
bounds_branch != std::string::npos &&
atomic != std::string::npos && bounds_branch < atomic,
"64-bit buffer atomic was not guarded by storage-buffer bounds");
@@ -11002,6 +11047,18 @@ void TestMemoryFedScalarLeafCall() {
ExpectFatal([&] { (void)PrepareShaderSource(multiple, options); },
"a native return pair was accepted as a later call target");
#endif
// A lane read and a wave32 lane mask write one SGPR, so neither s13 writer defines s14.
for (const auto& [wave_size, writer] : {std::pair{64u, 0x360u}, std::pair{32u, 0xc2u}}) {
const uint32_t neighbor[] = {EncodeVop3Word0(writer, 13), EncodeVop3Word1(256, 128, 0),
EncodeSop1(0x21, 14, 14), EncodeSopp(0x01)};
auto neighbor_options = options;
neighbor_options.wave_size = wave_size;
const auto neighbor_source = PrepareShaderSource(neighbor, neighbor_options);
const auto* target = neighbor_source.calls.front().target[0].TryInstruction();
Check(target != nullptr && target->GetOpcode() == IR::ValueOpcode::GetUserData &&
IR::RegIndex(target->Arg(0).ScalarRegister()) == 14u,
"a single-SGPR vector write was taken as the next SGPR's call-target definition");
}
}
void TestNewShaderRecompilerSetpcBranch() {
@@ -11730,7 +11787,7 @@ void TestMeshInputAssembly() {
options.user_data_count = 0;
options.input_info.vertex = &input;
auto program = Frontend::TranslateProgram(decoded, graph, options);
const uint32_t draw[] = {test.count, test.base_vertex, 7, test.width,
const uint32_t draw[] = {test.count, test.base_vertex, test.width,
test.address_low, 0x12};
Inst *load = nullptr;
for (auto &inst : *program.blocks.front()) {
@@ -11775,7 +11832,7 @@ void TestMeshInputAssembly() {
Check(sgprs[2] == test.group_info && sgprs[3] == test.wave_info &&
vgprs[0] == ((test.first << 2) | (test.second << 18)) &&
vgprs[1] == test.third * 4 && vgprs[5] == test.vertex_id &&
vgprs[test.fast_launch ? 6 : 8] == 9u,
vgprs[test.fast_launch ? 6 : 8] == 2u,
"mesh prolog changed input assembly, wave counts, vertex ID, or instance ID");
}
}
@@ -13602,6 +13659,34 @@ void TestNewShaderRecompilerPerInvocationMasksWithoutMirrors() {
}
}
// The wave32 live mask projects to the entry predicate, and VCC keeps its compare predicate
// after WQM.
const uint32_t live_mask_shader[] = {
EncodeSop1(0x03, 0, 126), // s_mov_b32 s0, exec_lo
EncodeSop2(0x0e, 126, 126, 0), // s_and_b32 exec_lo, exec_lo, s0
EncodeVop1(0x01, 0, 242), // v_mov_b32 v0, 1.0
EncodeExp0(0x00, 0x1), EncodeExp1(0, 0, 0, 0),
EncodeSopp(0x01),
};
const uint32_t wqm_vcc_shader[] = {
EncodeSop1(0x09, 126, 126), // s_wqm_b32 exec_lo, exec_lo
EncodeVop2(0x23, 0, 193, 1), // v_mbcnt_lo_u32_b32 v0, -1, v1
EncodeVopc(0xc1, 129, 0), // v_cmp_lt_u32 vcc_lo, 1, v0
EncodeVop2(0x01, 2, 128, 0), // v_cndmask_b32 v2, 0, v0, vcc_lo
EncodeExp0(0x00, 0x1), EncodeExp1(2, 2, 2, 2),
EncodeSopp(0x01),
};
auto wave32_pixel_options = MakeCompileOptions(ShaderType::Pixel);
wave32_pixel_options.wave_size = 32u;
for (const std::span<const uint32_t> shader : {std::span<const uint32_t>(live_mask_shader),
std::span<const uint32_t>(wqm_vcc_shader)}) {
const auto pixel_result = RecompileForTest(shader, wave32_pixel_options);
CheckSpirvBinaryValidates(pixel_result.spirv);
Check(DisassembleSpirvBinary(pixel_result.spirv).find("OpGroupNonUniformBallot") ==
std::string::npos,
"wave32 pixel mask rebuilt a known predicate from a ballot");
}
const uint32_t local_shader[] = {
EncodeVopc(0xc1, 5 + 256, 8), // v_cmp_lt_u32 vcc, v5, v8
EncodeSop2(0x0f, 2, 126, 106), // s_and_b64 s[2:3], exec, vcc
@@ -13716,6 +13801,7 @@ void TestNewShaderRecompilerExpPixelOutputs() {
ShaderPixelInputInfo uint16_info;
uint16_info.target_output_mode[0] = 7;
uint16_info.target_uint_mask = 1;
options.input_info.pixel = &uint16_info;
auto uint16_result = RecompileForTest(shader, options);
const auto uint16_source = DisassembleSpirvBinary(uint16_result.spirv);
@@ -13730,6 +13816,18 @@ void TestNewShaderRecompilerExpPixelOutputs() {
"compressed UINT16 MRT export was incorrectly decoded as FP16");
CheckSpirvBinaryValidates(uint16_result.spirv);
ShaderPixelInputInfo sint16_info;
sint16_info.target_output_mode[0] = 8;
sint16_info.target_sint_mask = 1;
options.input_info.pixel = &sint16_info;
auto sint16_result = RecompileForTest(shader, options);
const auto sint16_source = DisassembleSpirvBinary(sint16_result.spirv);
Check(sint16_source.find("OpVariable %_ptr_Output_v4int Output") != std::string::npos &&
CountSourceOccurrences(sint16_source, "OpBitFieldSExtract") == 4u &&
!SpirvContainsExtInst(sint16_result.spirv, 62),
"compressed SINT16 MRT export did not sign-extend into a signed output");
CheckSpirvBinaryValidates(sint16_result.spirv);
ShaderPixelInputInfo unorm16_info;
unorm16_info.target_output_mode[0] = 5;
options.input_info.pixel = &unorm16_info;
@@ -13757,6 +13855,7 @@ void TestNewShaderRecompilerExpPixelOutputs() {
ShaderPixelInputInfo uint_info;
uint_info.target_output_mode[0] = 7;
uint_info.target_uint_mask = 1;
options.input_info.pixel = &uint_info;
auto partial_uint_result =
RecompileForTest(partial_shader, options);
@@ -13783,6 +13882,15 @@ void TestNewShaderRecompilerExpPixelOutputs() {
"compressed UINT16 BA-only export did not read and unpack VSRC1");
CheckSpirvBinaryValidates(compressed_ba_result.spirv);
// 32-bit exports keep their raw bits in a UINT target's integer output.
uint_info.target_output_mode[0] = 9;
auto raw_uint_result = RecompileForTest(partial_shader, options);
const auto raw_uint_source = DisassembleSpirvBinary(raw_uint_result.spirv);
Check(raw_uint_source.find("OpVariable %_ptr_Output_v4uint Output") != std::string::npos &&
raw_uint_source.find("OpBitcast %v4float") == std::string::npos,
"32-bit MRT export to a UINT target did not use an unsigned integer output");
CheckSpirvBinaryValidates(raw_uint_result.spirv);
unorm16_info.target_output_mode[0] = 5;
options.input_info.pixel = &unorm16_info;
auto unorm16_ba_result =
@@ -16543,7 +16651,6 @@ void TestNewShaderRecompilerSpirvSizeBaselines() {
.variables = 2,
.loads = 9,
.stores = 4,
.array_lengths = 2,
.phis = 5,
.labels = 34,
.selection_merges = 10,
@@ -16744,7 +16851,7 @@ int main() {
TestNewShaderRecompilerNativeWideBufferIr();
TestNewShaderRecompilerScalarB64LaneTranslation();
TestNewShaderRecompilerMubufFormatTranslation();
TestNewShaderRecompilerFormattedStoreUsesRuntimeArrayLengthOnly();
TestNewShaderRecompilerFormattedStoreUsesRuntimeBindingSizeOnly();
TestNewShaderRecompilerTypedBufferTranslation();
TestNewShaderRecompilerDsReadWrite2Translation();
TestNewShaderRecompilerDsWideAndAtomicTranslation();
@@ -16815,6 +16922,7 @@ int main() {
TestNativeGuardedSamplerSource(false);
TestNativeGuardedSamplerSource(true);
TestNativeConstantSamplerSelection();
TestPackedZeroHighSamplerTableKey();
for (const bool scalar_key : {false, true}) TestBoundedMaterialBufferStores(scalar_key);
TestNativeDescriptorProvenanceKeepsGpuSelection();
TestCfgSiblingSharedExit();