Compare commits

...
Author SHA1 Message Date
MrFr3di 986721f5bc shader: decode FLAT, GLOBAL and SCRATCH D16 high-half loads (#1924)
FLAT-family opcode 0x25 (FLAT/GLOBAL/SCRATCH_LOAD_SHORT_D16_HI) loads an
unsigned halfword into bits [31:16] of the destination and keeps bits [15:0].
Decode it beside the low-half variant and let it select the high half of the
existing partial destination write, so FLAT_LOAD needs no new path.

Ghost of Yotei CS 0x79b0066f0cd1f3cd stopped at
global_load_short_d16_hi v16, v18, s[0:1] offset:196 (0xdc9480c4 0x10000012).
2026-10-26 22:34:54 +01:00
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
57 changed files with 1906 additions and 717 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,35 @@ 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.
@@ -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) {
@@ -95,14 +95,14 @@ constexpr MemoryOpcodeInfo MTBUF_OPCODE_LIST[] = {
};
constexpr MemoryOpcodeInfo FLAT_OPCODE_LIST[] = {
{0x08u, Opcode::FLAT_LOAD_UBYTE, 1, 8}, {0x09u, Opcode::FLAT_LOAD_SBYTE, 1, 8, true},
{0x0au, Opcode::FLAT_LOAD_USHORT, 1, 16}, {0x0bu, Opcode::FLAT_LOAD_SSHORT, 1, 16, true},
{0x0cu, Opcode::FLAT_LOAD_DWORD, 1, 32}, {0x0du, Opcode::FLAT_LOAD_DWORDX2, 2, 32},
{0x0eu, Opcode::FLAT_LOAD_DWORDX4, 4, 32}, {0x0fu, Opcode::FLAT_LOAD_DWORDX3, 3, 32},
{0x18u, Opcode::FLAT_STORE_BYTE, 1, 8}, {0x1au, Opcode::FLAT_STORE_SHORT, 1, 16},
{0x1cu, Opcode::FLAT_STORE_DWORD, 1, 32}, {0x1du, Opcode::FLAT_STORE_DWORDX2, 2, 32},
{0x1eu, Opcode::FLAT_STORE_DWORDX4, 4, 32}, {0x1fu, Opcode::FLAT_STORE_DWORDX3, 3, 32},
{0x24u, Opcode::FLAT_LOAD_SHORT_D16, 1, 16},
{0x08u, Opcode::FLAT_LOAD_UBYTE, 1, 8}, {0x09u, Opcode::FLAT_LOAD_SBYTE, 1, 8, true},
{0x0au, Opcode::FLAT_LOAD_USHORT, 1, 16}, {0x0bu, Opcode::FLAT_LOAD_SSHORT, 1, 16, true},
{0x0cu, Opcode::FLAT_LOAD_DWORD, 1, 32}, {0x0du, Opcode::FLAT_LOAD_DWORDX2, 2, 32},
{0x0eu, Opcode::FLAT_LOAD_DWORDX4, 4, 32}, {0x0fu, Opcode::FLAT_LOAD_DWORDX3, 3, 32},
{0x18u, Opcode::FLAT_STORE_BYTE, 1, 8}, {0x1au, Opcode::FLAT_STORE_SHORT, 1, 16},
{0x1cu, Opcode::FLAT_STORE_DWORD, 1, 32}, {0x1du, Opcode::FLAT_STORE_DWORDX2, 2, 32},
{0x1eu, Opcode::FLAT_STORE_DWORDX4, 4, 32}, {0x1fu, Opcode::FLAT_STORE_DWORDX3, 3, 32},
{0x24u, Opcode::FLAT_LOAD_SHORT_D16, 1, 16}, {0x25u, Opcode::FLAT_LOAD_SHORT_D16_HI, 1, 16},
};
constexpr MemoryOpcodeInfo DS_OPCODE_LIST[] = {
@@ -419,9 +419,10 @@ void DecodeFlat(uint32_t pc, std::span<const uint32_t> code, uint32_t word_index
}
DecodeVectorGpr(IsFlatStoreOpcode(inst.opcode) ? data : vdst, inst.dst);
if (inst.opcode == Opcode::FLAT_LOAD_SHORT_D16) {
// D16 loads reuse partial destinations to preserve the untouched high half.
inst.dst.sdwa_sel = 4u;
if (inst.opcode == Opcode::FLAT_LOAD_SHORT_D16 ||
inst.opcode == Opcode::FLAT_LOAD_SHORT_D16_HI) {
// D16 loads reuse partial destinations to preserve the untouched half.
inst.dst.sdwa_sel = inst.opcode == Opcode::FLAT_LOAD_SHORT_D16_HI ? 5u : 4u;
}
DecodeVectorGpr(addr, inst.src0);
inst.src_count = 1;
@@ -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;
}
@@ -697,6 +701,7 @@ std::string InstructionToString(const Instruction& inst) {
case Opcode::FLAT_LOAD_USHORT:
case Opcode::FLAT_LOAD_SSHORT:
case Opcode::FLAT_LOAD_SHORT_D16:
case Opcode::FLAT_LOAD_SHORT_D16_HI:
case Opcode::FLAT_LOAD_DWORD:
case Opcode::FLAT_LOAD_DWORDX2:
case Opcode::FLAT_LOAD_DWORDX3:
@@ -612,6 +612,7 @@ enum class Opcode {
FLAT_LOAD_USHORT,
FLAT_LOAD_SSHORT,
FLAT_LOAD_SHORT_D16,
FLAT_LOAD_SHORT_D16_HI,
FLAT_LOAD_DWORD,
FLAT_LOAD_DWORDX2,
FLAT_LOAD_DWORDX3,
@@ -766,7 +767,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))),
@@ -1148,6 +1146,7 @@ void Translator::EmitMemory(const Decoder::Instruction& inst) {
case Decoder::Opcode::FLAT_LOAD_USHORT:
case Decoder::Opcode::FLAT_LOAD_SSHORT:
case Decoder::Opcode::FLAT_LOAD_SHORT_D16:
case Decoder::Opcode::FLAT_LOAD_SHORT_D16_HI:
case Decoder::Opcode::FLAT_LOAD_DWORD:
case Decoder::Opcode::FLAT_LOAD_DWORDX2:
case Decoder::Opcode::FLAT_LOAD_DWORDX3:
@@ -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();
File diff suppressed because it is too large. Load diff
+280 -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 =
@@ -16022,6 +16130,153 @@ void TestCapturedBufferLoadUbyteD16Decode() {
"captured BUFFER_LOAD_UBYTE_D16 lost its preserved half, address, or coherence flags");
}
void TestCapturedGlobalLoadShortD16HiDecode() {
namespace Decoder = ShaderRecompiler::Decoder;
// Ghost of Yotei CS 0x79b0066f0cd1f3cd: global_load_short_d16_hi v16, v18, s[0:1] offset:196.
const uint32_t code[] = {0xdc9480c4u, 0x10000012u};
Decoder::Instruction inst;
Decoder::DecodeInstruction(code, 0, inst);
Check(inst.opcode == Decoder::Opcode::FLAT_LOAD_SHORT_D16_HI &&
inst.family == Decoder::Family::FLAT && inst.word_count == 2 &&
inst.data_dwords == 1 && inst.data_bits == 16 && !inst.data_signed &&
inst.memory_segment == 2 && inst.offset == 196 && !inst.glc && !inst.slc &&
!inst.dlc,
"captured GLOBAL_LOAD_SHORT_D16_HI lost its halfword payload or address fields");
Check(inst.dst.kind == Decoder::OperandKind::Vgpr && inst.dst.reg == 16 &&
inst.dst.sdwa_sel == 5 && inst.dst.sdwa_dst_unused == 2,
"captured GLOBAL_LOAD_SHORT_D16_HI must write only the high half of v16");
Check(inst.src0.kind == Decoder::OperandKind::Vgpr && inst.src0.reg == 18 &&
inst.src1.kind == Decoder::OperandKind::Sgpr && inst.src1.reg == 0,
"captured GLOBAL_LOAD_SHORT_D16_HI lost its VGPR offset or s[0:1] base");
Check(Decoder::InstructionToString(inst).find("FLAT_LOAD_SHORT_D16_HI") != std::string::npos,
"captured GLOBAL_LOAD_SHORT_D16_HI diagnostic text lost the opcode name");
}
void TestFlatLoadShortD16HiLlvmEncodingAnchor() {
namespace Decoder = ShaderRecompiler::Decoder;
// llvm/test/MC/AMDGPU/flat-gfx10.s: flat_load_short_d16_hi v1, v[3:4]
// encodes as [0x00,0x00,0x94,0xdc,0x03,0x00,0x7d,0x01].
const uint32_t code[] = {0xdc940000u, 0x017d0003u};
Decoder::Instruction inst;
Decoder::DecodeInstruction(code, 0, inst);
Check(inst.opcode == Decoder::Opcode::FLAT_LOAD_SHORT_D16_HI && inst.word_count == 2 &&
inst.memory_segment == 0 && inst.offset == 0 && inst.data_dwords == 1 &&
inst.data_bits == 16 && !inst.data_signed,
"LLVM flat_load_short_d16_hi encoding did not decode as a FLAT halfword load");
Check(inst.dst.kind == Decoder::OperandKind::Vgpr && inst.dst.reg == 1 &&
inst.dst.sdwa_sel == 5 && inst.dst.sdwa_dst_unused == 2 &&
inst.src0.kind == Decoder::OperandKind::Vgpr && inst.src0.reg == 3 &&
inst.src1.kind == Decoder::OperandKind::Vgpr && inst.src1.reg == 4,
"LLVM flat_load_short_d16_hi v1, v[3:4] lost its destination half or address pair");
}
void TestFlatLoadShortD16HiCompilesAndValidates() {
// The captured high-half load followed by a buffer store of the merged register so that the
// load stays live: global_load_short_d16_hi v16, v18, s[0:1] offset:196.
const uint32_t shader[] = {
0xdc9480c4u, 0x10000012u,
EncodeMubuf0(0x1c, 0, false), EncodeMubuf1(16, 12, 0),
EncodeSopp(0x01),
};
auto options = MakeCompileOptions(ShaderType::Compute);
options.dump_ir = true;
auto result = RecompileForTest(shader, options);
Check(result.decoded_dump.find("FLAT_LOAD_SHORT_D16_HI") != std::string::npos &&
result.decoded_dump.find("unsupported family=") == std::string::npos,
"GLOBAL_LOAD_SHORT_D16_HI did not reach the decoded program");
Check(result.ir_dump.find("LoadAddressU16") != std::string::npos &&
result.ir_dump.find("BitFieldInsert") != std::string::npos,
"GLOBAL_LOAD_SHORT_D16_HI did not become a 16-bit load merged into the old register");
CheckSpirvBinaryValidates(result.spirv);
}
void TestFlatLoadShortD16HiAddressFields() {
namespace Decoder = ShaderRecompiler::Decoder;
// Modifier bits of the first FLAT-family word.
constexpr uint32_t DlcBit = 1u << 12u;
constexpr uint32_t LdsBit = 1u << 13u;
constexpr uint32_t GlcBit = 1u << 16u;
constexpr uint32_t SlcBit = 1u << 17u;
constexpr uint32_t SegFlat = 0;
constexpr uint32_t SegScratch = 1;
constexpr uint32_t SegGlobal = 2;
constexpr uint32_t SegReserved = 3;
constexpr uint32_t SaddrOff = 0x7du;
const auto decode = [](uint32_t seg, uint32_t offset_field, uint32_t saddr, uint32_t flags = 0) {
const uint32_t code[] = {EncodeFlat0(0x25, seg, offset_field) | flags,
EncodeFlat1(7, saddr, 0, 9)};
Decoder::Instruction inst;
Decoder::DecodeInstruction(code, 0, inst);
return inst;
};
struct OffsetCase {
uint32_t seg;
uint32_t field;
int32_t expected;
};
const OffsetCase offsets[] = {
// FLAT ignores OFFSET[11] and uses an unsigned 11-bit byte offset.
{SegFlat, 0x000, 0},
{SegFlat, 0x7ff, 2047},
{SegFlat, 0x800, 0},
{SegFlat, 0xfff, 2047},
// GLOBAL and SCRATCH sign-extend the 12-bit field.
{SegGlobal, 0x000, 0},
{SegGlobal, 0x7ff, 2047},
{SegGlobal, 0x800, -2048},
{SegGlobal, 0xfff, -1},
{SegScratch, 0x000, 0},
{SegScratch, 0x7ff, 2047},
{SegScratch, 0x800, -2048},
{SegScratch, 0xfff, -1},
};
for (const auto &test : offsets) {
const auto inst = decode(test.seg, test.field, SaddrOff);
Check(inst.opcode == Decoder::Opcode::FLAT_LOAD_SHORT_D16_HI &&
inst.memory_segment == test.seg &&
inst.offset == static_cast<uint32_t>(test.expected),
"FLAT-family D16 high-half load decoded the wrong byte offset for its segment");
Check(inst.dst.kind == Decoder::OperandKind::Vgpr && inst.dst.reg == 7 &&
inst.dst.sdwa_sel == 5 && inst.dst.sdwa_dst_unused == 2 &&
inst.src0.kind == Decoder::OperandKind::Vgpr && inst.src0.reg == 9,
"FLAT-family D16 high-half load lost its destination half or address VGPR");
}
// FLAT takes a VGPR pair whatever the SADDR field says; GLOBAL takes either an SGPR base plus
// a VGPR offset or, with SADDR off, a VGPR pair.
const auto flat = decode(SegFlat, 0, 2);
Check(flat.opcode == Decoder::Opcode::FLAT_LOAD_SHORT_D16_HI &&
flat.src1.kind == Decoder::OperandKind::Vgpr && flat.src1.reg == 10,
"FLAT D16 high-half load must use the VGPR pair and ignore SADDR");
const auto global_sgpr = decode(SegGlobal, 0, 2);
Check(global_sgpr.opcode == Decoder::Opcode::FLAT_LOAD_SHORT_D16_HI &&
global_sgpr.src_count == 2 && global_sgpr.src0.kind == Decoder::OperandKind::Vgpr &&
global_sgpr.src0.reg == 9 && global_sgpr.src1.kind == Decoder::OperandKind::Sgpr &&
global_sgpr.src1.reg == 2,
"GLOBAL D16 high-half load with SADDR s[2:3] lost its scalar base");
const auto global_pair = decode(SegGlobal, 0, SaddrOff);
Check(global_pair.opcode == Decoder::Opcode::FLAT_LOAD_SHORT_D16_HI &&
global_pair.src_count == 2 && global_pair.src0.kind == Decoder::OperandKind::Vgpr &&
global_pair.src0.reg == 9 && global_pair.src1.kind == Decoder::OperandKind::Vgpr &&
global_pair.src1.reg == 10,
"GLOBAL D16 high-half load with SADDR off must use the VGPR pair");
// DLC is only a cache hint and stays visible; LDS, GLC and SLC on a load and the reserved
// segment are not implemented and must keep failing loudly.
const auto dlc = decode(SegGlobal, 4, 2, DlcBit);
Check(dlc.opcode == Decoder::Opcode::FLAT_LOAD_SHORT_D16_HI && dlc.dlc && !dlc.glc && !dlc.slc,
"GLOBAL D16 high-half load lost its DLC hint");
Check(decode(SegGlobal, 0, 2, LdsBit).opcode == Decoder::Opcode::UNSUPPORTED,
"FLAT-family D16 high-half load accepted LDS=1");
Check(decode(SegGlobal, 0, 2, GlcBit).opcode == Decoder::Opcode::UNSUPPORTED,
"FLAT-family D16 high-half load accepted GLC=1");
Check(decode(SegGlobal, 0, 2, SlcBit).opcode == Decoder::Opcode::UNSUPPORTED,
"FLAT-family D16 high-half load accepted SLC=1 on a load");
Check(decode(SegReserved, 0, 2).opcode == Decoder::Opcode::UNSUPPORTED,
"FLAT-family D16 high-half load accepted the reserved segment");
}
void TestCapturedBufferStoreShortD16HiDecode() {
namespace Decoder = ShaderRecompiler::Decoder;
const uint32_t code[] = {0xe06c6000u, 0x80010326u};
@@ -16543,7 +16798,6 @@ void TestNewShaderRecompilerSpirvSizeBaselines() {
.variables = 2,
.loads = 9,
.stores = 4,
.array_lengths = 2,
.phis = 5,
.labels = 34,
.selection_merges = 10,
@@ -16744,7 +16998,7 @@ int main() {
TestNewShaderRecompilerNativeWideBufferIr();
TestNewShaderRecompilerScalarB64LaneTranslation();
TestNewShaderRecompilerMubufFormatTranslation();
TestNewShaderRecompilerFormattedStoreUsesRuntimeArrayLengthOnly();
TestNewShaderRecompilerFormattedStoreUsesRuntimeBindingSizeOnly();
TestNewShaderRecompilerTypedBufferTranslation();
TestNewShaderRecompilerDsReadWrite2Translation();
TestNewShaderRecompilerDsWideAndAtomicTranslation();
@@ -16815,6 +17069,7 @@ int main() {
TestNativeGuardedSamplerSource(false);
TestNativeGuardedSamplerSource(true);
TestNativeConstantSamplerSelection();
TestPackedZeroHighSamplerTableKey();
for (const bool scalar_key : {false, true}) TestBoundedMaterialBufferStores(scalar_key);
TestNativeDescriptorProvenanceKeepsGpuSelection();
TestCfgSiblingSharedExit();
@@ -16909,6 +17164,10 @@ int main() {
TestPixelProgramCacheBindingIdentity();
TestGraphicsPushConstantPlacement();
TestCapturedBufferLoadUbyteD16Decode();
TestCapturedGlobalLoadShortD16HiDecode();
TestFlatLoadShortD16HiLlvmEncodingAnchor();
TestFlatLoadShortD16HiAddressFields();
TestFlatLoadShortD16HiCompilesAndValidates();
TestCapturedBufferStoreShortD16HiDecode();
TestCapturedBufferStoreFormatD16HiXDecode();
TestCapturedBufferLoadD16XyDecode();