Compare commits

..
Author SHA1 Message Date
nmzik fd6b28c4b2 build: target x86-64-v3 without floating-point contraction 2026-10-27 07:04:38 +01:00
Senaxx 6cee2dc0bd shader: inline the hot IR operand accessors 2026-10-27 07:04:38 +01:00
Senaxx a526a8b9c9 shader: hand IR uses over at once on replacement and skip use lists at program teardown 2026-10-27 07:04:38 +01:00
Senaxx 1ec69f8e7d gpu: make cull mode and front face dynamic state 2026-10-27 07:04:38 +01:00
nmzik 2463e3aaf0 gpu: write end-of-pipe labels beside GPU-written bytes to the backing store 2026-10-27 07:04:38 +01:00
nmzikandstrxno 556538bed5 gpu: write D16 demotion results with one 16-bit store
Co-authored-by: strxno <rokas.pettersen@gmail.com>
2026-10-27 07:04:38 +01:00
Mr. Big BritchesandClaude Opus 5.5 770438cee0 ci: fix the Linux build and the macOS SRT walker test crashes (#1922)
* graphics: give InlinePageOwnerList's const_iterator iterator traits

The Linux build failed in textureCache.cpp: std::any_of over the image
owners of a page needs std::iterator_traits, which libstdc++ only fills
from the iterator's own member types. MSVC's STL and libc++ don't
consult them there, so Windows and macOS built.

Declare the five member types. The category is input, not forward,
because operator* returns a copy.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* tests: assert InlinePageOwnerList's const_iterator has an input category

Only libstdc++ needs the iterator traits for std::any_of, so removing
them would compile on Windows and macOS and break only the Linux build.
The assert fails the build on every platform instead.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* tests: serve SRT walker test loads on macOS too

FakeGuestMemory only installed its fault handler on Linux and Windows,
so on macOS the walker's deliberately faulting loads crashed
scalar_provenance, resource_materialization and resource_tracking.

Share the Linux sigaction path with macOS, read the registers from the
macOS thread state, and catch SIGBUS as well as SIGSEGV there, like the
emulator's own handler.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 06:26:08 +01:00
nmzik db12f0c5fb shader: select consecutive GPU-selected sampler table entries without a switch 2026-10-27 04:23:01 +01:00
nmzik 3f9666acbb shader: seal SSA blocks once their predecessors are filled 2026-10-27 04:23:01 +01:00
nmzik 7eb8abb279 shader: drop the duplicate-use scan from Inst::AddUse 2026-10-27 04:23:01 +01:00
Claxten 91a5ff6cb7 shader: decode s_dcache_inv as a no-op 2026-10-27 04:23:01 +01:00
Claxten 95a507a8ff shader: support 64-bit buffer atomic minimum 2026-10-27 04:23:01 +01:00
Claxten b2053c2861 shader: bound per-lane descriptor table loops by the program's uniform loop count 2026-10-27 04:23:01 +01:00
Claxten 9801fd862b shader: zero image descriptors that only unreached blocks use 2026-10-27 04:23:01 +01:00
nmzikandshadowbeat070 78adb9293d shader: give A16 1D and late 3D gradients their own address VGPRs
Co-authored-by: shadowbeat070 <70843316+shadowbeat070@users.noreply.github.com>
2026-10-27 04:23:01 +01:00
Claxten 0a72641678 shader: evaluate saturating float-to-signed conversions in runtime descriptor values 2026-10-27 04:23:01 +01:00
Claxten 99f4eba223 shader: read bit-scan records only at ranks their mask buffer words set 2026-10-27 04:23:01 +01:00
nmzikandshadowbeat070 04bfd9070c shader: give an A16 bias its own address VGPR
Co-authored-by: shadowbeat070 <70843316+shadowbeat070@users.noreply.github.com>
2026-10-27 04:23:01 +01:00
nmzikandshadowbeat070 ffa3992c1a shader: decode every PS5 image_gather4 encoding
Co-authored-by: shadowbeat070 <70843316+shadowbeat070@users.noreply.github.com>
2026-10-27 04:23:00 +01:00
Claxten 1b70938052 agc: emit async memory semaphores and execute them in the CP 2026-10-27 04:23:00 +01:00
Claxten 914a3bb4b8 shader: enumerate set ranks of eight-word light bitmaps 2026-10-27 04:23:00 +01:00
Claxten ffb6ad6cab shader: bound selected-lane record indices by guarded shared append lists 2026-10-27 04:23:00 +01:00
Claxten ff5d16eefc tests: expect an inclusive child limit to bound one more record 2026-10-27 04:23:00 +01:00
Claxten d118afbeae tests: expect scalar selector records past the V# to read zero 2026-10-27 04:23:00 +01:00
Claxten a3ac6d0d32 shader: bound table keys by an inclusive guard with a constant limit 2026-10-27 04:23:00 +01:00
Claxten f297c0bfe1 shader: bound bit-scan record keys and drop the key a guard rules out 2026-10-27 04:23:00 +01:00
Claxten 20bb268a48 shader: bound raw image table keys by a signed clamp on the key or a sibling read 2026-10-27 04:23:00 +01:00
Claxten d3768c00e9 shader: accept indirect image rows whose type reads the leading sample coordinates 2026-10-27 04:23:00 +01:00
Claxten dfcf1c0b03 shader: bound waterfall image table keys converted from clamped float arithmetic 2026-10-27 04:23:00 +01:00
Claxten de3ed905c8 shader: track image table keys divided from a runtime base plus the workgroup ID 2026-10-27 04:23:00 +01:00
Claxten 0a140911ad kernel: default flexible memory to 448 MiB and charge loaded modules' segments to it 2026-10-27 04:23:00 +01:00
nmzik 2483573651 shader: mask V_MAD_U64_U32 carry-out bits of lanes outside EXEC 2026-10-27 04:23:00 +01:00
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
b2c9ec7609 renderer: decode R8, R8G8 and R16_UINT color register clears (#1893)
DecodePackedColorClear had no case for R8_UNORM/UINT, R8G8_UNORM/UINT and
R16_UINT, so a CMASK fast clear of such a render target was dropped and the
target kept its old contents. UFC 6 (PPSA23566) fast-clears an R8_UINT
region map this way every frame and then writes only part of it; a capture
showed the map never cleared. DWORD fills of these formats are decoded only
when every pixel in the word holds the same value.

Co-authored-by: 4wgycvn84p-afk <346802885+4wgycvn84p-afk@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 18:47:45 +01:00
herrMirtoandAndre Michi 99044c7c49 libs: provide NpEntitlementAccessPft state query used by FC27 (#1897)
Co-authored-by: Andre Michi <herrmirto@proton.me>
2026-10-25 18:38:26 +01:00
93 changed files with 4596 additions and 1208 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
+4
View File
@@ -407,6 +407,8 @@ add_executable(scalar_provenance_tests EXCLUDE_FROM_ALL
target_link_libraries(scalar_provenance_tests fmt::fmt xbyak::xbyak)
target_include_directories(scalar_provenance_tests PRIVATE ${inc_headers})
add_executable(build_flags_tests EXCLUDE_FROM_ALL "${KYTY_TESTS_DIR}/BuildFlagsTests.cpp")
add_executable(page_manager_tests EXCLUDE_FROM_ALL
"${KYTY_TESTS_DIR}/PageManagerTests.cpp"
"${KYTY_SOURCE_DIR}/graphics/host_gpu/pageManager.cpp"
@@ -650,6 +652,7 @@ if(BUILD_TESTING)
add_test(NAME image_page_table COMMAND $<TARGET_FILE:image_page_table_tests>)
add_test(NAME memory_tracker COMMAND $<TARGET_FILE:memory_tracker_tests>)
add_test(NAME page_manager COMMAND $<TARGET_FILE:page_manager_tests>)
add_test(NAME build_flags COMMAND $<TARGET_FILE:build_flags_tests>)
add_test(NAME archive_file COMMAND $<TARGET_FILE:archive_file_tests>)
add_test(NAME trophy_system COMMAND $<TARGET_FILE:trophy_system_tests>)
add_test(NAME avplayer_file COMMAND $<TARGET_FILE:avplayer_file_tests>)
@@ -743,6 +746,7 @@ if(BUILD_TESTING)
image_page_table_tests
memory_tracker_tests
page_manager_tests
build_flags_tests
archive_file_tests
trophy_system_tests
avplayer_file_tests
@@ -27,6 +27,7 @@ struct IndirectDrawRegisters {
uint32_t vertex_offset;
uint32_t instance_offset;
uint32_t index_offset;
uint32_t draw_index;
};
class Pm4Execution {
@@ -97,6 +98,7 @@ public:
template <typename T>
void WaitRegMem(uint32_t func, const T* addr, T ref, T mask, uint32_t poll, uint32_t wait_op);
void MemSemaphore(uint64_t* counter, bool signal, bool write);
void WriteData(uint32_t* dst, const uint32_t* src, uint32_t dw_num, uint32_t write_control);
void WriteReferenceClock(uint64_t dst_address, uint32_t num_bytes);
void DmaData(uint8_t engine, uint8_t dst_sel, uint8_t dst_cache_policy,
@@ -202,6 +202,7 @@ constexpr auto MakeOpcodeDispatchTable() {
g_cp_op_func[Pm4::IT_DMA_DATA] = CpOpDmaData;
g_cp_op_func[Pm4::IT_ACQUIRE_MEM] = CpOpAcquireMem;
g_cp_op_func[Pm4::IT_RELEASE_MEM] = CpOpReleaseMem;
g_cp_op_func[Pm4::IT_MEM_SEMAPHORE] = CpOpMemSemaphore;
g_cp_op_func[Pm4::IT_REWIND] = CpOpRewind;
g_cp_op_func[Pm4::IT_SET_CONTEXT_REG] = CpOpSetContextReg;
g_cp_op_func[Pm4::IT_SET_SH_REG] = CpOpSetShaderReg;
@@ -136,6 +136,7 @@ uint32_t CpOpEventWrite(CommandProcessor&, uint32_t, const uint32_t*, uint32_t,
uint32_t CpOpDmaData(CommandProcessor&, uint32_t, const uint32_t*, uint32_t, uint32_t);
uint32_t CpOpAcquireMem(CommandProcessor&, uint32_t, const uint32_t*, uint32_t, uint32_t);
uint32_t CpOpReleaseMem(CommandProcessor&, uint32_t, const uint32_t*, uint32_t, uint32_t);
uint32_t CpOpMemSemaphore(CommandProcessor&, uint32_t, const uint32_t*, uint32_t, uint32_t);
uint32_t CpOpSetContextReg(CommandProcessor&, uint32_t, const uint32_t*, uint32_t, uint32_t);
uint32_t CpOpSetShaderReg(CommandProcessor&, uint32_t, const uint32_t*, uint32_t, uint32_t);
uint32_t CpOpSetUconfigReg(CommandProcessor&, uint32_t, const uint32_t*, uint32_t, uint32_t);
@@ -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;
@@ -2058,6 +2060,21 @@ KYTY_CP_OP_PARSER(CpOpReleaseMem) {
return 7;
}
KYTY_CP_OP_PARSER(CpOpMemSemaphore) {
KYTY_PROFILER_FUNCTION();
const auto control = buffer[2];
EXIT_NOT_IMPLEMENTED(cmd_id != 0xc0023900 || (control & ~0xe0110000u) != 0 ||
(control >> 29u) < 6u);
auto* counter = reinterpret_cast<uint64_t*>(
(buffer[0] & ~0x7u) | (static_cast<uint64_t>(buffer[1] & 0x3ffffu) << 32u));
// A signal updates the counter before the next packet, so its mailbox stall never waits.
cp.MemSemaphore(counter, (control >> 29u) == 6u, ((control >> 20u) & 0x1u) != 0);
return 3;
}
KYTY_CP_OP_PARSER(CpOpSetContextReg) {
KYTY_PROFILER_FUNCTION();
+30 -6
View File
@@ -281,6 +281,16 @@ template void CommandProcessor::WaitRegMem<uint32_t>(uint32_t, const uint32_t*,
template void CommandProcessor::WaitRegMem<uint64_t>(uint32_t, const uint64_t*, uint64_t, uint64_t,
uint32_t, uint32_t);
void CommandProcessor::MemSemaphore(uint64_t* counter, bool signal, bool write) {
if (signal) {
*counter = write ? 1u : *counter + 1u;
} else if (*counter == 0) {
SuspendPm4();
} else {
(*counter)--;
}
}
void CommandProcessor::WriteData(uint32_t* dst, const uint32_t* src, uint32_t dw_num,
uint32_t write_control) {
const uint32_t dst_sel = ((write_control >> 30u) & 0x1u) | ((write_control >> 7u) & 0x1eu);
@@ -789,7 +799,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 +811,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 +850,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 +866,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 +878,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),
@@ -964,16 +976,28 @@ void CommandProcessor::ReleaseMem(uint32_t data_sel, uint32_t interrupt_selector
// Dumped occlusion counters are written before any end-of-pipe signal reaches the guest.
m_renderer.GetOcclusionQueries().Publish();
// A label beside GPU-written bytes goes to the backing store, so they are not downloaded.
// Other labels go through the guest mapping: its fault keeps a clean cached copy current and
// downloads GPU-written bytes under the label before the store.
const auto write_label = [this, address](const void* value, uint64_t size) {
const auto vaddr = reinterpret_cast<uint64_t>(address);
auto& buffers = m_renderer.GetBufferCache();
if (!GuestRange {vaddr, size}.Valid() || !buffers.IsRegionGpuModified(vaddr, size) ||
buffers.HasGpuDirtyBytes(vaddr, size) ||
!LibKernel::Memory::TryWriteBacking(vaddr, value, size)) {
std::memcpy(address, value, size);
}
};
// EndOfPipeData: kNone, kImmediate32b, kImmediate64b, kRefClk; 5 reads GDS.
switch (data_sel) {
case 0: break;
case 1: {
const auto value = static_cast<uint32_t>(data);
std::memcpy(address, &value, sizeof(value));
write_label(&value, sizeof(value));
break;
}
case 3: data = Sync::ReadReferenceClock(); [[fallthrough]];
case 2: std::memcpy(address, &data, sizeof(data)); break;
case 2: write_label(&data, sizeof(data)); break;
case 5: {
GetScheduler().Finish();
const auto gds = static_cast<uint32_t>(data);
@@ -8,6 +8,7 @@
#include <cstddef>
#include <cstdint>
#include <cstring>
#include <iterator>
#include <memory>
#include <type_traits>
#include <utility>
@@ -118,6 +119,13 @@ public:
class const_iterator final {
public:
// libstdc++'s algorithms need these. Input, not forward, because operator* returns a copy.
using iterator_category = std::input_iterator_tag;
using value_type = OwnerT;
using difference_type = std::ptrdiff_t;
using pointer = void;
using reference = OwnerT;
[[nodiscard]] OwnerT operator*() const noexcept { return m_owners->At(m_index); }
const_iterator& operator++() noexcept {
++m_index;
+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,
@@ -125,6 +125,14 @@ void DynamicState::Commit(const GraphicContext& graphics, vk::CommandBuffer cmdb
}
}
}
if (dirty_state.cull_mode) {
dirty_state.cull_mode = false;
cmdbuf.setCullMode(cull_mode);
}
if (dirty_state.front_face) {
dirty_state.front_face = false;
cmdbuf.setFrontFace(front_face);
}
if (dirty_state.blend_constants) {
dirty_state.blend_constants = false;
cmdbuf.setBlendConstants(blend_constants.data());
@@ -52,6 +52,9 @@ struct DynamicState {
bool stencil_back_write_mask : 1;
bool stencil_back_compare_mask : 1;
bool cull_mode : 1;
bool front_face : 1;
bool blend_constants : 1;
bool color_write_enables : 1;
bool line_width : 1;
@@ -86,6 +89,9 @@ struct DynamicState {
uint32_t stencil_back_write_mask {};
uint32_t stencil_back_compare_mask {};
vk::CullModeFlags cull_mode {};
vk::FrontFace front_face {};
std::array<float, 4> blend_constants {};
std::array<vk::Bool32, RENDER_COLOR_ATTACHMENTS_MAX> color_write_enables {};
uint32_t num_color_write_enables {};
@@ -221,6 +227,20 @@ struct DynamicState {
}
}
void SetCullMode(const vk::CullModeFlags cull_mode_) {
if (cull_mode != cull_mode_) {
cull_mode = cull_mode_;
dirty_state.cull_mode = true;
}
}
void SetFrontFace(const vk::FrontFace front_face_) {
if (front_face != front_face_) {
front_face = front_face_;
dirty_state.front_face = true;
}
}
void SetBlendConstants(const std::array<float, 4> blend_constants_) {
if (blend_constants != blend_constants_) {
blend_constants = blend_constants_;
@@ -425,15 +425,15 @@ void Image::Resolve(Image& source, const ImageSubresourceRange& source_range,
const auto destination_height =
std::max(backing.extent.height >> destination_range.base_level, 1u);
const bool copy = source.backing.samples == 1;
EXIT_IF(layers == 0 || info.extent.width > source_width || info.extent.height > source_height ||
info.extent.width > destination_width || info.extent.height > destination_height ||
EXIT_IF(layers == 0 ||
(copy ? !ImageViewOps::FormatsCompatible(source.backing.format, backing.format)
: source.backing.format != backing.format));
auto resolved_source_range = source_range;
auto resolved_destination_range = destination_range;
resolved_source_range.layer_count = layers;
resolved_destination_range.layer_count = layers;
const vk::Extent3D resolve_extent {info.extent.width, info.extent.height, 1};
const vk::Extent3D resolve_extent {std::min(source_width, destination_width),
std::min(source_height, destination_height), 1};
m_scheduler.EndRendering();
auto command = m_scheduler.Current().Handle();
@@ -378,6 +378,9 @@ inline constexpr std::array<VideoOutFormatPolicy, 8> VIDEO_OUT_FORMAT_POLICIES {
const auto encoded = static_cast<float>(value & 0xffu) / 255.0f;
return encoded <= 0.04045f ? encoded / 12.92f : std::pow((encoded + 0.055f) / 1.055f, 2.4f);
};
const auto unorm16 = [](uint64_t value) {
return static_cast<float>(value & 0xffffu) / 65535.0f;
};
const auto float16 = [](uint32_t value) {
const auto sign = (value & 0x8000u) << 16u;
const auto exponent = (value >> 10u) & 0x1fu;
@@ -403,19 +406,49 @@ inline constexpr std::array<VideoOutFormatPolicy, 8> VIDEO_OUT_FORMAT_POLICIES {
next.float32[1] = float16(packed >> 16u);
[[fallthrough]];
case vk::Format::eR16Sfloat: next.float32[0] = float16(packed); break;
case vk::Format::eR16G16Unorm:
next.float32[1] = static_cast<float>(packed >> 16u) / 65535.0f;
case vk::Format::eR16G16B16A16Unorm:
next.float32[2] = unorm16(clear_words >> 32u);
next.float32[3] = unorm16(clear_words >> 48u);
[[fallthrough]];
case vk::Format::eR16Unorm:
next.float32[0] = static_cast<float>(packed & 0xffffu) / 65535.0f;
case vk::Format::eR16G16Unorm:
next.float32[1] = unorm16(packed >> 16u);
[[fallthrough]];
case vk::Format::eR16Unorm: next.float32[0] = unorm16(packed); break;
case vk::Format::eR16G16B16A16Uint:
for (uint32_t i = 0; i < 4; i++) {
next.uint32[i] = static_cast<uint16_t>(clear_words >> (16u * i));
}
break;
// Each clear register stores one raw 32-bit float channel.
case vk::Format::eR16G16B16A16Sint:
for (uint32_t i = 0; i < 4; i++) {
next.int32[i] = static_cast<int16_t>(clear_words >> (16u * i));
}
break;
// Each clear register stores one raw 32-bit channel.
case vk::Format::eR32G32Sfloat:
next.float32[1] = std::bit_cast<float>(static_cast<uint32_t>(clear_words >> 32u));
[[fallthrough]];
case vk::Format::eR32Sfloat: next.float32[0] = std::bit_cast<float>(packed); break;
case vk::Format::eR32Uint: next.uint32[0] = packed; break;
case vk::Format::eR32Sint: next.int32[0] = static_cast<int32_t>(packed); break;
case vk::Format::eR32G32Uint:
case vk::Format::eR32G32Sint:
next.uint32[1] = static_cast<uint32_t>(clear_words >> 32u);
[[fallthrough]];
case vk::Format::eR32Uint:
case vk::Format::eR32Sint: next.uint32[0] = packed; break;
// Narrow targets hold their channels in the low bits of the first word. UFC 6 (PPSA23566)
// fast-clears an R8_UINT map through CMASK every frame and then writes only part of it;
// without these the clear was dropped and old contents piled up.
case vk::Format::eR8Uint: next.uint32[0] = packed & 0xffu; break;
case vk::Format::eR8Unorm: next.float32[0] = unorm8(packed); break;
case vk::Format::eR8G8Uint:
next.uint32[0] = packed & 0xffu;
next.uint32[1] = (packed >> 8u) & 0xffu;
break;
case vk::Format::eR8G8Unorm:
next.float32[0] = unorm8(packed);
next.float32[1] = unorm8(packed >> 8u);
break;
case vk::Format::eR16Uint: next.uint32[0] = packed & 0xffffu; break;
case vk::Format::eR8G8B8A8Srgb:
next.float32[0] = srgb8(packed);
next.float32[1] = srgb8(packed >> 8u);
@@ -462,10 +495,16 @@ inline constexpr std::array<VideoOutFormatPolicy, 8> VIDEO_OUT_FORMAT_POLICIES {
// Keep this separate: the first register word alone cannot describe a 64-bit color.
[[nodiscard]] inline bool DecodeColorDwordFill(vk::Format format, uint32_t packed,
vk::ClearColorValue& clear) {
if ((format == vk::Format::eR16Sfloat || format == vk::Format::eR16Unorm) &&
if ((format == vk::Format::eR16Sfloat || format == vk::Format::eR16Unorm ||
format == vk::Format::eR16Uint || format == vk::Format::eR8G8Unorm ||
format == vk::Format::eR8G8Uint) &&
(packed & 0xffffu) != (packed >> 16u)) {
return false;
}
if ((format == vk::Format::eR8Unorm || format == vk::Format::eR8Uint) &&
packed != (packed & 0xffu) * 0x01010101u) {
return false;
}
if (format == vk::Format::eR16G16B16A16Sfloat) {
if (packed != 0) {
return false;
@@ -484,7 +484,7 @@ void TileManager::ConvertD16(Result source, Result target, D16Direction directio
vk::AccessFlagBits::eHostWrite |
vk::AccessFlagBits::eTransferWrite |
vk::AccessFlagBits::eShaderWrite;
barriers[1].dstAccessMask = vk::AccessFlagBits::eShaderRead | vk::AccessFlagBits::eShaderWrite;
barriers[1].dstAccessMask = vk::AccessFlagBits::eShaderWrite;
command.pipelineBarrier(
vk::PipelineStageFlagBits::eAllCommands | vk::PipelineStageFlagBits::eHost,
vk::PipelineStageFlagBits::eComputeShader, {}, 0, nullptr, 2, barriers, 0, nullptr);
@@ -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,
@@ -991,15 +1003,11 @@ PipelineCache::Pipeline& PipelineCache::GetGraphicsPipeline(const CommandBuffer&
}
static_params.alpha_to_coverage_enable = ps_active && ctx.GetShaderRegisters().AlphaToMask();
const bool rect_list = Prospero::IsRectList(command.GetUserConfig().GetPrimType());
static_params.cull_back = !rect_list && mc.cull_back;
static_params.cull_front = !rect_list && mc.cull_front;
static_params.face = mc.face;
static_params.provoking_vtx_last = mc.provoking_vtx_last;
static_params.polygon_mode =
ResolvePolygonMode(mc, static_params.cull_front, static_params.cull_back);
ResolvePolygonMode(mc, !rect_list && mc.cull_front, !rect_list && mc.cull_back);
if (tessellation) {
static_params.patch_control_points =
static_cast<uint8_t>(vs_input_info.tess.input_control_points);
static_params.patch_control_points = vs_input_info.tess.input_control_points;
} else if (!mesh && static_params.topology == vk::PrimitiveTopology::ePatchList) {
// Rectangle lists expand through generated tessellation stages.
static_params.patch_control_points = 3;
@@ -54,15 +54,12 @@ struct PipelineStaticParameters {
bool depth_clip_enable = true;
vk::PrimitiveTopology topology = vk::PrimitiveTopology::ePointList;
// 0 without tessellation, 3 for a rectangle list, else the input control points.
uint8_t patch_control_points = 0;
uint32_t patch_control_points = 0;
bool primitive_restart_enable = false;
uint32_t samples = 1;
bool sample_shading_enable = false;
bool alpha_to_coverage_enable = false;
uint32_t color_mask[RENDER_COLOR_ATTACHMENTS_MAX] = {};
bool cull_front = false;
bool cull_back = false;
bool face = false;
bool provoking_vtx_last = false;
vk::PolygonMode polygon_mode = vk::PolygonMode::eFill;
uint8_t color_srcblend[RENDER_COLOR_ATTACHMENTS_MAX] = {};
@@ -219,17 +219,6 @@ void CreatePipelineInternal(GraphicContext& graphics, PipelineCache::Pipeline& p
vk::PipelineViewportStateCreateInfo viewport_state {};
viewport_state.pNext = &depth_clip_control;
vk::CullModeFlags cull_mode = vk::CullModeFlagBits::eNone;
if (static_params.cull_back) {
cull_mode |= vk::CullModeFlagBits::eBack;
}
if (static_params.cull_front) {
cull_mode |= vk::CullModeFlagBits::eFront;
}
vk::FrontFace front_face =
(static_params.face ? vk::FrontFace::eClockwise : vk::FrontFace::eCounterClockwise);
vk::PipelineRasterizationDepthClipStateCreateInfoEXT clip_ext {};
clip_ext.depthClipEnable = static_params.depth_clip_enable ? VK_TRUE : VK_FALSE;
@@ -250,8 +239,6 @@ void CreatePipelineInternal(GraphicContext& graphics, PipelineCache::Pipeline& p
provoking_vertex.pNext = rasterizer.pNext;
rasterizer.pNext = &provoking_vertex;
}
rasterizer.cullMode = cull_mode;
rasterizer.frontFace = front_face;
rasterizer.polygonMode = static_params.polygon_mode;
rasterizer.lineWidth = 1.0f;
@@ -347,6 +334,8 @@ void CreatePipelineInternal(GraphicContext& graphics, PipelineCache::Pipeline& p
vk::DynamicState::eViewportWithCount,
vk::DynamicState::eScissorWithCount,
vk::DynamicState::eLineWidth,
vk::DynamicState::eCullMode,
vk::DynamicState::eFrontFace,
vk::DynamicState::eDepthTestEnable,
vk::DynamicState::eDepthWriteEnable,
vk::DynamicState::eDepthCompareOp,
+22 -3
View File
@@ -403,6 +403,20 @@ static void UpdateGraphicsDynamicState(const CommandBuffer& buffer, DynamicState
dynamic_state.SetDepthBias(constant_factor, poly_offset.clamp, slope_factor);
}
// Rect lists are drawn without culling.
vk::CullModeFlags cull_mode {};
if (!Prospero::IsRectList(buffer.GetUserConfig().GetPrimType())) {
if (mode.cull_front) {
cull_mode |= vk::CullModeFlagBits::eFront;
}
if (mode.cull_back) {
cull_mode |= vk::CullModeFlagBits::eBack;
}
}
dynamic_state.SetCullMode(cull_mode);
dynamic_state.SetFrontFace(mode.face ? vk::FrontFace::eClockwise
: vk::FrontFace::eCounterClockwise);
dynamic_state.SetStencilTestEnabled(depth.stencil_test_enable);
if (depth.stencil_test_enable) {
const auto& front = depth.stencil_front;
@@ -843,6 +857,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 +1175,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 +1493,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;
}
@@ -1,10 +1,11 @@
#version 450
#extension GL_EXT_shader_16bit_storage : require
layout(constant_id = 0) const uint D32 = 0u;
layout(local_size_x = 64) in;
layout(set = 0, binding = 0, std430) readonly buffer Input { uint data[]; } input_buffer;
layout(set = 0, binding = 1, std430) buffer Output { uint data[]; } output_buffer;
layout(set = 0, binding = 1, std430) buffer Output { uint16_t data[]; } output_buffer;
layout(push_constant, std430) uniform Conversion {
uint src_base;
@@ -32,9 +33,5 @@ void main() {
}
uint src = params.src_base + y * params.source_pitch + x * 4u;
uint dst = params.dst_base + y * params.target_pitch + x * 2u;
uint shift = (dst & 2u) * 8u;
uint mask = 0xffffu << shift;
uint word = dst >> 2u;
atomicAnd(output_buffer.data[word], ~mask);
atomicOr(output_buffer.data[word], decode(input_buffer.data[src >> 2u]) << shift);
output_buffer.data[dst >> 1u] = uint16_t(decode(input_buffer.data[src >> 2u]));
}
@@ -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();
@@ -38,7 +38,8 @@ bool HasFlag(const IR::MemoryInfo& mem, uint32_t flag) {
uint32_t AddressU32(ValueEmitContext& ctx, const IR::MemoryInfo& mem, const IR::Inst& address,
uint32_t component) {
const auto layout = Decoder::ImageAddressComponentLayout(mem.image_sample_flags, component);
const auto layout = Decoder::ImageAddressComponentLayout(mem.image_sample_flags,
mem.image_dimension, component);
const auto packed = layout.bit_offset / 32u;
if (packed >= address.NumArgs()) return ConstantU32(ctx.state, 0);
auto value = ctx.Def(address.Arg(packed));
@@ -55,10 +56,11 @@ uint32_t AddressU32(ValueEmitContext& ctx, const IR::MemoryInfo& mem, const IR::
uint32_t AddressF32(ValueEmitContext& ctx, const IR::MemoryInfo& mem, const IR::Inst& address,
uint32_t component) {
const auto value = AddressU32(ctx, mem, address, component);
return Decoder::ImageAddressComponentLayout(mem.image_sample_flags, component).bit_width == 16u
? EmitF16BitsToF32(ctx.state, value)
: Unary(ctx.state, spv::OpBitcast, TypeF32(ctx.state), value);
const auto value = AddressU32(ctx, mem, address, component);
const auto layout = Decoder::ImageAddressComponentLayout(mem.image_sample_flags,
mem.image_dimension, component);
return layout.bit_width == 16u ? EmitF16BitsToF32(ctx.state, value)
: Unary(ctx.state, spv::OpBitcast, TypeF32(ctx.state), value);
}
ImageSampleLayout Layout(const IR::MemoryInfo& mem) {
@@ -77,6 +79,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;
}
@@ -810,6 +813,25 @@ uint32_t EmitImageResult(ValueEmitContext& ctx, const IR::Inst& inst, const Emit
return mixed ? value : result_bits(mem.resource, value);
}
// Ordinals outside the table select candidates[0]. Past ordinal 0, a consecutive table is an
// offset of ordinal - 1, which wraps at 0 so that one unsigned compare checks both ends.
uint32_t SamplerArrayIndex(EmitterState& state, uint32_t ordinal,
const std::vector<uint32_t>& candidates) {
const auto count = static_cast<uint32_t>(candidates.size());
const auto gap = std::adjacent_find(candidates.begin() + 1, candidates.end(),
[](uint32_t lhs, uint32_t rhs) { return rhs != lhs + 1u; });
if (count == 1u || gap != candidates.end()) {
return EmitIndexSwitch(state, ordinal, count, TypeU32(state),
[&](uint32_t item) { return ConstantU32(state, candidates[item]); });
}
const auto offset = Binary(state, spv::OpISub, TypeU32(state), ordinal, ConstantU32(state, 1u));
const auto in_table = Binary(state, spv::OpULessThan, TypeBool(state), offset,
ConstantU32(state, count - 1u));
const auto index = Binary(state, spv::OpIAdd, TypeU32(state), offset,
ConstantU32(state, candidates[1]));
return Select(state, TypeU32(state), in_table, index, ConstantU32(state, candidates[0]));
}
struct ImageSampler {
uint32_t id = 0;
bool dynamic = false;
@@ -845,9 +867,7 @@ std::array<ImageSampler, 2> LoadImageSamplers(ValueEmitContext& ctx, const IR::I
selected = EmitIndirectResourceIndex(state, selected,
samplers[index].indirect_mapping_offset,
samplers[index].indirect_search_iterations, 0u);
array_index = EmitIndexSwitch(
state, selected, static_cast<uint32_t>(candidates.size()),
TypeU32(state), [&](uint32_t ordinal) { return ConstantU32(state, candidates[ordinal]); });
array_index = SamplerArrayIndex(state, selected, candidates);
}
sampler = {LoadSamplerDescriptor(state, index, array_index), array_index != 0u};
};
@@ -1069,7 +1089,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 +1106,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 +1134,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);
@@ -289,6 +291,7 @@ inline constexpr auto EmitBufferAtomicIAdd32 = EmitAtomic32;
inline constexpr auto EmitBufferAtomicISub32 = EmitAtomic32;
inline constexpr auto EmitBufferAtomicSMin32 = EmitAtomic32;
inline constexpr auto EmitBufferAtomicUMin32 = EmitAtomic32;
inline constexpr auto EmitBufferAtomicUMin64 = EmitBufferAtomic64;
inline constexpr auto EmitBufferAtomicSMax32 = EmitAtomic32;
inline constexpr auto EmitBufferAtomicUMax32 = EmitAtomic32;
inline constexpr auto EmitBufferAtomicAnd32 = EmitAtomic32;
@@ -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;
}
@@ -576,6 +585,7 @@ spv::Op SpirvAtomicOpcode(IR::ValueOpcode opcode) {
case IR::ValueOpcode::ImageAtomicUMin32:
case IR::ValueOpcode::ImageAtomicUMin64:
case IR::ValueOpcode::BufferAtomicUMin32:
case IR::ValueOpcode::BufferAtomicUMin64:
case IR::ValueOpcode::SharedAtomicUMin32: return spv::OpAtomicUMin;
case IR::ValueOpcode::ImageAtomicSMax32:
case IR::ValueOpcode::BufferAtomicSMax32:
@@ -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);
@@ -14,12 +14,6 @@ struct MimgSampleInfo {
uint32_t flags = 0;
};
struct MimgGatherInfo {
uint32_t encoding = 0;
Opcode decoded = Opcode::UNSUPPORTED;
uint32_t flags = 0;
};
struct MimgAtomicInfo {
uint32_t encoding = 0;
Opcode decoded = Opcode::UNSUPPORTED;
@@ -73,7 +67,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) {
@@ -181,23 +175,72 @@ constexpr MimgSampleInfo MIMG_SAMPLE_OPCODE_LIST[] = {
ImageSampleFlagOffset | ImageSampleFlagAdjust},
};
constexpr MimgGatherInfo MIMG_GATHER_OPCODE_LIST[] = {
{0x44u, Opcode::IMAGE_GATHER4_L, ImageSampleFlagLod},
{0x47u, Opcode::IMAGE_GATHER4_LZ, ImageSampleFlagLevelZero},
{0x48u, Opcode::IMAGE_GATHER4_C, ImageSampleFlagCompare},
{0x4cu, Opcode::IMAGE_GATHER4_C_L, ImageSampleFlagCompare | ImageSampleFlagLod},
{0x4fu, Opcode::IMAGE_GATHER4_C_LZ,
ImageSampleFlagCompare | ImageSampleFlagLevelZero},
{0x57u, Opcode::IMAGE_GATHER4_LZ_O,
ImageSampleFlagLevelZero | ImageSampleFlagOffset},
{0x58u, Opcode::IMAGE_GATHER4_C_O,
ImageSampleFlagCompare | ImageSampleFlagOffset},
{0x5fu, Opcode::IMAGE_GATHER4_C_LZ_O,
constexpr MimgSampleInfo MIMG_GATHER_OPCODE_LIST[] = {
{0x40u, "image_gather4", 0},
{0x41u, "image_gather4_cl", ImageSampleFlagLodClamp},
{0x44u, "image_gather4_l", ImageSampleFlagLod},
{0x45u, "image_gather4_b", ImageSampleFlagBias},
{0x46u, "image_gather4_b_cl", ImageSampleFlagBias | ImageSampleFlagLodClamp},
{0x47u, "image_gather4_lz", ImageSampleFlagLevelZero},
{0x48u, "image_gather4_c", ImageSampleFlagCompare},
{0x49u, "image_gather4_c_cl", ImageSampleFlagCompare | ImageSampleFlagLodClamp},
{0x4cu, "image_gather4_c_l", ImageSampleFlagCompare | ImageSampleFlagLod},
{0x4du, "image_gather4_c_b", ImageSampleFlagCompare | ImageSampleFlagBias},
{0x4eu, "image_gather4_c_b_cl",
ImageSampleFlagCompare | ImageSampleFlagBias | ImageSampleFlagLodClamp},
{0x4fu, "image_gather4_c_lz", ImageSampleFlagCompare | ImageSampleFlagLevelZero},
{0x50u, "image_gather4_o", ImageSampleFlagOffset},
{0x51u, "image_gather4_cl_o", ImageSampleFlagLodClamp | ImageSampleFlagOffset},
{0x54u, "image_gather4_l_o", ImageSampleFlagLod | ImageSampleFlagOffset},
{0x55u, "image_gather4_b_o", ImageSampleFlagBias | ImageSampleFlagOffset},
{0x56u, "image_gather4_b_cl_o",
ImageSampleFlagBias | ImageSampleFlagLodClamp | ImageSampleFlagOffset},
{0x57u, "image_gather4_lz_o", ImageSampleFlagLevelZero | ImageSampleFlagOffset},
{0x58u, "image_gather4_c_o", ImageSampleFlagCompare | ImageSampleFlagOffset},
{0x59u, "image_gather4_c_cl_o",
ImageSampleFlagCompare | ImageSampleFlagLodClamp | ImageSampleFlagOffset},
{0x5cu, "image_gather4_c_l_o",
ImageSampleFlagCompare | ImageSampleFlagLod | ImageSampleFlagOffset},
{0x5du, "image_gather4_c_b_o",
ImageSampleFlagCompare | ImageSampleFlagBias | ImageSampleFlagOffset},
{0x5eu, "image_gather4_c_b_cl_o",
ImageSampleFlagCompare | ImageSampleFlagBias | ImageSampleFlagLodClamp |
ImageSampleFlagOffset},
{0x5fu, "image_gather4_c_lz_o",
ImageSampleFlagCompare | ImageSampleFlagLevelZero | ImageSampleFlagOffset},
{0x61u, Opcode::IMAGE_GATHER4H, ImageSampleFlagGatherHorizontal},
{0xc8u, Opcode::IMAGE_GATHER4_C, ImageSampleFlagCompare | ImageSampleFlagAdjust},
{0xd8u, Opcode::IMAGE_GATHER4_C_O,
{0x61u, "image_gather4h", ImageSampleFlagGatherHorizontal},
{0xc0u, "image_gather4_a", ImageSampleFlagAdjust},
{0xc1u, "image_gather4_cl_a", ImageSampleFlagLodClamp | ImageSampleFlagAdjust},
{0xc5u, "image_gather4_b_a", ImageSampleFlagBias | ImageSampleFlagAdjust},
{0xc6u, "image_gather4_b_cl_a",
ImageSampleFlagBias | ImageSampleFlagLodClamp | ImageSampleFlagAdjust},
{0xc8u, "image_gather4_c_a", ImageSampleFlagCompare | ImageSampleFlagAdjust},
{0xc9u, "image_gather4_c_cl_a",
ImageSampleFlagCompare | ImageSampleFlagLodClamp | ImageSampleFlagAdjust},
{0xcdu, "image_gather4_c_b_a",
ImageSampleFlagCompare | ImageSampleFlagBias | ImageSampleFlagAdjust},
{0xceu, "image_gather4_c_b_cl_a",
ImageSampleFlagCompare | ImageSampleFlagBias | ImageSampleFlagLodClamp |
ImageSampleFlagAdjust},
{0xd0u, "image_gather4_o_a", ImageSampleFlagOffset | ImageSampleFlagAdjust},
{0xd1u, "image_gather4_cl_o_a",
ImageSampleFlagLodClamp | ImageSampleFlagOffset | ImageSampleFlagAdjust},
{0xd5u, "image_gather4_b_o_a",
ImageSampleFlagBias | ImageSampleFlagOffset | ImageSampleFlagAdjust},
{0xd6u, "image_gather4_b_cl_o_a",
ImageSampleFlagBias | ImageSampleFlagLodClamp | ImageSampleFlagOffset |
ImageSampleFlagAdjust},
{0xd8u, "image_gather4_c_o_a",
ImageSampleFlagCompare | ImageSampleFlagOffset | ImageSampleFlagAdjust},
{0xd9u, "image_gather4_c_cl_o_a",
ImageSampleFlagCompare | ImageSampleFlagLodClamp | ImageSampleFlagOffset |
ImageSampleFlagAdjust},
{0xddu, "image_gather4_c_b_o_a",
ImageSampleFlagCompare | ImageSampleFlagBias | ImageSampleFlagOffset |
ImageSampleFlagAdjust},
{0xdeu, "image_gather4_c_b_cl_o_a",
ImageSampleFlagCompare | ImageSampleFlagBias | ImageSampleFlagLodClamp |
ImageSampleFlagOffset | ImageSampleFlagAdjust},
};
constexpr MimgAtomicInfo MIMG_ATOMIC_OPCODE_LIST[] = {
@@ -219,13 +262,13 @@ constexpr auto MIMG_SAMPLE_OPS = Detail::MakeOpcodeTable<0x100>(MIMG_SAMPLE_OPCO
constexpr auto MIMG_GATHER_OPS = Detail::MakeOpcodeTable<0x100>(MIMG_GATHER_OPCODE_LIST);
constexpr auto MIMG_ATOMIC_OPS = Detail::MakeOpcodeTable<0x100>(MIMG_ATOMIC_OPCODE_LIST);
Opcode DecodeMimgOpcode(uint32_t opcode, const MimgSampleInfo* sample, const MimgGatherInfo* gather,
Opcode DecodeMimgOpcode(uint32_t opcode, const MimgSampleInfo* sample, const MimgSampleInfo* gather,
const MimgAtomicInfo* atomic) {
if (sample != nullptr) {
return Opcode::IMAGE_SAMPLE;
}
if (gather != nullptr) {
return gather->decoded;
return Opcode::IMAGE_GATHER4;
}
if (atomic != nullptr) {
return atomic->decoded;
@@ -244,24 +287,10 @@ Opcode DecodeMimgOpcode(uint32_t opcode, const MimgSampleInfo* sample, const Mim
}
}
uint32_t DecodeMimgSampleFlags(const MimgSampleInfo* sample, const MimgGatherInfo* gather) {
if (sample != nullptr) {
return sample->flags;
}
if (gather != nullptr) {
return gather->flags;
}
return 0;
}
uint32_t DecodeMimgAddressComponents(uint32_t opcode, ImageDimension dimension,
const MimgSampleInfo* sample, const MimgGatherInfo* gather,
const MimgAtomicInfo* atomic) {
if (sample != nullptr) {
return ImageSampleAddressComponents(sample->flags, dimension);
}
if (gather != nullptr) {
return ImageSampleAddressComponents(gather->flags, dimension);
const MimgSampleInfo* sampled, const MimgAtomicInfo* atomic) {
if (sampled != nullptr) {
return ImageSampleAddressComponents(sampled->flags, dimension);
}
if (atomic != nullptr) {
return ImageCoordComponents(dimension);
@@ -290,33 +319,31 @@ uint32_t CountDmaskComponents(uint32_t dmask) {
} // namespace
ImageAddressComponent ImageAddressComponentLayout(uint32_t flags, uint32_t component) {
const auto width = [flags](uint32_t index) {
if ((flags & ImageSampleFlagA16) == 0u) return 32u;
uint32_t cursor = 0;
if ((flags & ImageSampleFlagOffset) != 0u) {
if (index == cursor++) return 32u;
}
if ((flags & ImageSampleFlagBias) != 0u) {
if (index == cursor++) return 16u;
}
if ((flags & ImageSampleFlagCompare) != 0u && index == cursor) return 32u;
return 16u;
};
uint32_t offset = 0;
for (uint32_t index = 0; index < component; index++) {
const auto bits = width(index);
if (bits == 32u) offset = (offset + 31u) & ~31u;
offset += bits;
ImageAddressComponent ImageAddressComponentLayout(uint32_t flags, ImageDimension dimension,
uint32_t component) {
if ((flags & ImageSampleFlagA16) == 0u) return {component * 32u, 32u};
// A16 offsets, bias and PCF reference each take a whole VGPR, the bias as a half in its low
// 16 bits. The other components pack two per VGPR, except that each 1D derivative and the last
// two 3D ones take the low half of their own VGPR.
const bool has_bias = (flags & ImageSampleFlagBias) != 0u;
const uint32_t bias_index = (flags & ImageSampleFlagOffset) != 0u ? 1u : 0u;
const uint32_t whole_vgprs = std::popcount(
flags & (ImageSampleFlagOffset | ImageSampleFlagBias | ImageSampleFlagCompare));
if (component < whole_vgprs) {
return {component * 32u, has_bias && component == bias_index ? 16u : 32u};
}
const auto bits = width(component);
if (bits == 32u) offset = (offset + 31u) & ~31u;
return {offset, bits};
const uint32_t axes =
(flags & ImageSampleFlagDerivative) != 0u ? ImageGradientComponents(dimension) : 0u;
const uint32_t unpaired = axes % 2u * 2u;
const uint32_t paired = axes * 2u - unpaired;
const uint32_t index = component - whole_vgprs;
const uint32_t half = index + std::clamp(index, paired, paired + unpaired) - paired;
return {whole_vgprs * 32u + half * 16u, 16u};
}
uint32_t ImageAddressDwordCount(uint32_t flags, uint32_t components) {
uint32_t ImageAddressDwordCount(uint32_t flags, ImageDimension dimension, uint32_t components) {
if (components == 0u) return 0u;
const auto last = ImageAddressComponentLayout(flags, components - 1u);
const auto last = ImageAddressComponentLayout(flags, dimension, components - 1u);
return (last.bit_offset + last.bit_width + 31u) / 32u;
}
@@ -329,16 +356,17 @@ void DecodeMimg(uint32_t pc, std::span<const uint32_t> code, uint32_t word_index
const auto dimension = DecodeImageDimension((word0 >> 3u) & 0x7u);
const uint32_t word_count = 2u + nsa_dwords;
const uint32_t vdata = (word1 >> 8u) & 0xffu;
const uint32_t vaddr = word1 & 0xffu;
const uint32_t srsrc = (word1 >> 16u) & 0x1fu;
const uint32_t ssamp = (word1 >> 21u) & 0x1fu;
const bool r128 = ((word0 >> 15u) & 0x1u) != 0u;
const bool a16 = ((word1 >> 30u) & 0x1u) != 0u;
const bool d16 = ((word1 >> 31u) & 0x1u) != 0u;
const auto* sample = Detail::FindOpcode(MIMG_SAMPLE_OPS, opcode);
const auto* gather = Detail::FindOpcode(MIMG_GATHER_OPS, opcode);
const auto* atomic = Detail::FindOpcode(MIMG_ATOMIC_OPS, opcode);
const uint32_t vdata = (word1 >> 8u) & 0xffu;
const uint32_t vaddr = word1 & 0xffu;
const uint32_t srsrc = (word1 >> 16u) & 0x1fu;
const uint32_t ssamp = (word1 >> 21u) & 0x1fu;
const bool r128 = ((word0 >> 15u) & 0x1u) != 0u;
const bool a16 = ((word1 >> 30u) & 0x1u) != 0u;
const bool d16 = ((word1 >> 31u) & 0x1u) != 0u;
const auto* sample = Detail::FindOpcode(MIMG_SAMPLE_OPS, opcode);
const auto* gather = Detail::FindOpcode(MIMG_GATHER_OPS, opcode);
const auto* atomic = Detail::FindOpcode(MIMG_ATOMIC_OPS, opcode);
const auto* sampled = sample != nullptr ? sample : gather;
inst.pc = pc;
inst.word_count = word_count;
@@ -351,11 +379,11 @@ void DecodeMimg(uint32_t pc, std::span<const uint32_t> code, uint32_t word_index
inst.data_dwords = d16 ? (inst.data_components + 1u) / 2u : inst.data_components;
inst.glc = ((word0 >> 13u) & 1u) != 0;
inst.slc = ((word0 >> 25u) & 1u) != 0;
inst.image_sample_flags = DecodeMimgSampleFlags(sample, gather);
inst.image_sample_flags = sampled != nullptr ? sampled->flags : 0u;
if (a16) {
inst.image_sample_flags |= ImageSampleFlagA16;
}
if ((sample != nullptr || gather != nullptr) && (word0 & (1u << 12u)) != 0u) {
if (sampled != nullptr && (word0 & (1u << 12u)) != 0u) {
inst.image_sample_flags |= ImageSampleFlagUnnormalized;
}
inst.image_dimension = dimension;
@@ -364,8 +392,7 @@ void DecodeMimg(uint32_t pc, std::span<const uint32_t> code, uint32_t word_index
for (uint32_t i = 0; i < nsa_dwords * 4u; i++) {
inst.image_nsa_addr[i] = (code[word_index + 2u + i / 4u] >> ((i % 4u) * 8u)) & 0xffu;
}
inst.image_address_components =
DecodeMimgAddressComponents(opcode, dimension, sample, gather, atomic);
inst.image_address_components = DecodeMimgAddressComponents(opcode, dimension, sampled, atomic);
SetRawWords(inst, code, word_index, word_count);
if (inst.opcode == Opcode::UNSUPPORTED) {
@@ -389,8 +416,8 @@ void DecodeMimg(uint32_t pc, std::span<const uint32_t> code, uint32_t word_index
}
}
}
const bool supports_d16 = sample != nullptr || gather != nullptr || opcode == 0x00u ||
opcode == 0x01u || opcode == 0x08u || opcode == 0x09u;
const bool supports_d16 = sampled != nullptr || opcode == 0x00u || opcode == 0x01u ||
opcode == 0x08u || opcode == 0x09u;
if (d16 && !supports_d16) {
SetUnsupported(inst, Family::MIMG, opcode, "MIMG opcode does not support D16 data");
}
@@ -407,9 +434,10 @@ void DecodeMimg(uint32_t pc, std::span<const uint32_t> code, uint32_t word_index
inst.src_count = 3;
}
const char* MimgSampleOpcodeName(uint32_t opcode) {
const auto* sample = Detail::FindOpcode(MIMG_SAMPLE_OPS, opcode);
return sample != nullptr ? sample->name : nullptr;
const char* MimgSampledOpcodeName(uint32_t opcode) {
const auto* sample = Detail::FindOpcode(MIMG_SAMPLE_OPS, opcode);
const auto* sampled = sample != nullptr ? sample : Detail::FindOpcode(MIMG_GATHER_OPS, opcode);
return sampled != nullptr ? sampled->name : nullptr;
}
} // namespace Libs::Graphics::ShaderRecompiler::Decoder
@@ -8,15 +8,16 @@ namespace Libs::Graphics::ShaderRecompiler::Decoder {
void DecodeMimg(uint32_t pc, std::span<const uint32_t> code, uint32_t word_index,
Instruction& inst);
const char* MimgSampleOpcodeName(uint32_t opcode);
const char* MimgSampledOpcodeName(uint32_t opcode);
struct ImageAddressComponent {
uint32_t bit_offset;
uint32_t bit_width;
};
ImageAddressComponent ImageAddressComponentLayout(uint32_t flags, uint32_t component);
uint32_t ImageAddressDwordCount(uint32_t flags, uint32_t components);
ImageAddressComponent ImageAddressComponentLayout(uint32_t flags, ImageDimension dimension,
uint32_t component);
uint32_t ImageAddressDwordCount(uint32_t flags, ImageDimension dimension, uint32_t components);
} // namespace Libs::Graphics::ShaderRecompiler::Decoder
@@ -22,7 +22,7 @@ constexpr MemoryOpcodeInfo SMEM_OPCODE_LIST[] = {
{0x04u, Opcode::S_LOAD_DWORDX16, 16, 32}, {0x08u, Opcode::S_BUFFER_LOAD_DWORD, 1, 32},
{0x09u, Opcode::S_BUFFER_LOAD_DWORDX2, 2, 32}, {0x0au, Opcode::S_BUFFER_LOAD_DWORDX4, 4, 32},
{0x0bu, Opcode::S_BUFFER_LOAD_DWORDX8, 8, 32}, {0x0cu, Opcode::S_BUFFER_LOAD_DWORDX16, 16, 32},
{0x25u, Opcode::S_MEMREALTIME, 2, 32},
{0x20u, Opcode::S_DCACHE_INV, 0, 0}, {0x25u, Opcode::S_MEMREALTIME, 2, 32},
};
constexpr MemoryOpcodeInfo MUBUF_OPCODE_LIST[] = {
@@ -69,6 +69,7 @@ constexpr MemoryOpcodeInfo MUBUF_OPCODE_LIST[] = {
{0x40u, Opcode::BUFFER_ATOMIC_FMAX, 1, 32},
{0x50u, Opcode::BUFFER_ATOMIC_SWAP_X2, 2, 32},
{0x52u, Opcode::BUFFER_ATOMIC_ADD_X2, 2, 32},
{0x56u, Opcode::BUFFER_ATOMIC_UMIN_X2, 2, 32},
{0x59u, Opcode::BUFFER_ATOMIC_AND_X2, 2, 32},
{0x5au, Opcode::BUFFER_ATOMIC_OR_X2, 2, 32},
{0x71u, Opcode::BUFFER_GL0_INV, 0, 0},
@@ -95,14 +96,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[] = {
@@ -285,6 +286,9 @@ void DecodeSmem(uint32_t pc, std::span<const uint32_t> code, uint32_t word_index
if (inst.opcode == Opcode::UNSUPPORTED) {
SetUnsupported(inst, Family::SMEM, opcode, "SMEM opcode is not implemented");
}
if (inst.opcode == Opcode::S_DCACHE_INV) {
return;
}
DecodeScalarDestination(sdst, pc, inst.dst);
if (inst.opcode == Opcode::S_MEMREALTIME) {
@@ -419,9 +423,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;
@@ -123,10 +123,9 @@ std::string ImageSampleFlagsToString(uint32_t flags) {
}
std::string FormatMimg(const Instruction& inst) {
const char* sample_name =
inst.opcode == Opcode::IMAGE_SAMPLE ? MimgSampleOpcodeName(inst.opcode_id) : nullptr;
const char* sampled_name = MimgSampledOpcodeName(inst.opcode_id);
const std::string_view name =
sample_name != nullptr ? sample_name : magic_enum::enum_name(inst.opcode);
sampled_name != nullptr ? sampled_name : magic_enum::enum_name(inst.opcode);
std::string text =
fmt::format("0x{:08x}: {} {}, {}, {}, {} ; dmask=0x{:x} image_dim={}", inst.pc, name,
OperandToString(inst.dst).c_str(), OperandToString(inst.src0).c_str(),
@@ -138,22 +137,10 @@ std::string FormatMimg(const Instruction& inst) {
if (inst.image_r128) {
text += " r128=1";
}
switch (inst.opcode) {
case Opcode::IMAGE_SAMPLE:
case Opcode::IMAGE_GATHER4_L:
case Opcode::IMAGE_GATHER4_LZ:
case Opcode::IMAGE_GATHER4_C:
case Opcode::IMAGE_GATHER4_C_L:
case Opcode::IMAGE_GATHER4_C_LZ:
case Opcode::IMAGE_GATHER4_LZ_O:
case Opcode::IMAGE_GATHER4_C_O:
case Opcode::IMAGE_GATHER4_C_LZ_O:
case Opcode::IMAGE_GATHER4H:
text += fmt::format(" sample_flags={} addr_components={}",
ImageSampleFlagsToString(inst.image_sample_flags).c_str(),
inst.image_address_components);
break;
default: break;
if (sampled_name != nullptr) {
text += fmt::format(" sample_flags={} addr_components={}",
ImageSampleFlagsToString(inst.image_sample_flags).c_str(),
inst.image_address_components);
}
if (inst.image_nsa_dwords != 0) {
text += fmt::format(" nsa_dwords={} nsa_addr=", inst.image_nsa_dwords);
@@ -258,7 +245,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 +448,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;
}
@@ -566,6 +557,7 @@ std::string InstructionToString(const Instruction& inst) {
OperandToString(inst.src0).c_str()));
case Opcode::S_BARRIER:
return WithUnsupportedReason(inst, fmt::format("0x{:08x}: s_barrier", inst.pc));
case Opcode::S_DCACHE_INV:
case Opcode::BUFFER_GL0_INV:
case Opcode::BUFFER_GL1_INV:
return fmt::format("0x{:08x}: {}", inst.pc, magic_enum::enum_name(inst.opcode));
@@ -612,15 +604,7 @@ std::string InstructionToString(const Instruction& inst) {
case Opcode::IMAGE_LOAD_MIP:
case Opcode::IMAGE_GET_RESINFO:
case Opcode::IMAGE_GET_LOD:
case Opcode::IMAGE_GATHER4_L:
case Opcode::IMAGE_GATHER4_LZ:
case Opcode::IMAGE_GATHER4_C:
case Opcode::IMAGE_GATHER4_C_L:
case Opcode::IMAGE_GATHER4_C_LZ:
case Opcode::IMAGE_GATHER4_LZ_O:
case Opcode::IMAGE_GATHER4_C_O:
case Opcode::IMAGE_GATHER4_C_LZ_O:
case Opcode::IMAGE_GATHER4H: return WithUnsupportedReason(inst, FormatMimg(inst));
case Opcode::IMAGE_GATHER4: return WithUnsupportedReason(inst, FormatMimg(inst));
case Opcode::S_LOAD_DWORD:
case Opcode::S_LOAD_DWORDX2:
case Opcode::S_LOAD_DWORDX4:
@@ -681,6 +665,7 @@ std::string InstructionToString(const Instruction& inst) {
case Opcode::BUFFER_ATOMIC_SUB:
case Opcode::BUFFER_ATOMIC_SMIN:
case Opcode::BUFFER_ATOMIC_UMIN:
case Opcode::BUFFER_ATOMIC_UMIN_X2:
case Opcode::BUFFER_ATOMIC_SMAX:
case Opcode::BUFFER_ATOMIC_UMAX:
case Opcode::BUFFER_ATOMIC_AND:
@@ -697,6 +682,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:
@@ -544,6 +544,7 @@ enum class Opcode {
S_BUFFER_LOAD_DWORDX8,
S_BUFFER_LOAD_DWORDX16,
S_MEMREALTIME,
S_DCACHE_INV,
BUFFER_LOAD_FORMAT_X,
BUFFER_LOAD_FORMAT_XY,
BUFFER_LOAD_FORMAT_XYZ,
@@ -596,6 +597,7 @@ enum class Opcode {
BUFFER_ATOMIC_SUB,
BUFFER_ATOMIC_SMIN,
BUFFER_ATOMIC_UMIN,
BUFFER_ATOMIC_UMIN_X2,
BUFFER_ATOMIC_SMAX,
BUFFER_ATOMIC_UMAX,
BUFFER_ATOMIC_AND,
@@ -612,6 +614,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,
@@ -707,15 +710,7 @@ enum class Opcode {
IMAGE_ATOMIC_FMIN,
IMAGE_ATOMIC_FMAX,
IMAGE_SAMPLE,
IMAGE_GATHER4_L,
IMAGE_GATHER4_LZ,
IMAGE_GATHER4_C,
IMAGE_GATHER4_C_L,
IMAGE_GATHER4_C_LZ,
IMAGE_GATHER4_LZ_O,
IMAGE_GATHER4_C_O,
IMAGE_GATHER4_C_LZ_O,
IMAGE_GATHER4H,
IMAGE_GATHER4,
V_INTERP_P1_F32,
V_INTERP_P2_F32,
V_INTERP_MOV_F32,
@@ -766,7 +761,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}));
WriteVectorMask(inst.dst2, 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) {
@@ -322,8 +322,8 @@ IR::Value Translator::MakeImageAddress(const Decoder::Instruction& inst,
const Decoder::Operand& base) {
std::array<IR::Value, 13> components {};
components.fill(IR::Value(0u));
const auto count =
Decoder::ImageAddressDwordCount(inst.image_sample_flags, inst.image_address_components);
const auto count = Decoder::ImageAddressDwordCount(
inst.image_sample_flags, inst.image_dimension, inst.image_address_components);
EXIT_IF(count > components.size());
const auto nsa_components =
std::min(inst.image_nsa_dwords * 4u, Decoder::MaxImageNsaAddressComponents);
@@ -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))),
@@ -943,6 +941,8 @@ void Translator::DS_PERMUTE(const Decoder::Instruction& inst, bool backward) {
void Translator::EmitMemory(const Decoder::Instruction& inst) {
switch (inst.opcode) {
// Translated scalar loads have no cache of their own to invalidate.
case Decoder::Opcode::S_DCACHE_INV: return;
case Decoder::Opcode::BUFFER_GL0_INV:
case Decoder::Opcode::BUFFER_GL1_INV:
ir.Emit(IR::ValueOpcode::DataCacheInvalidate);
@@ -1031,6 +1031,8 @@ void Translator::EmitMemory(const Decoder::Instruction& inst) {
return BUFFER_ATOMIC(inst, IR::ValueOpcode::BufferAtomicSMin32);
case Decoder::Opcode::BUFFER_ATOMIC_UMIN:
return BUFFER_ATOMIC(inst, IR::ValueOpcode::BufferAtomicUMin32);
case Decoder::Opcode::BUFFER_ATOMIC_UMIN_X2:
return BUFFER_ATOMIC(inst, IR::ValueOpcode::BufferAtomicUMin64);
case Decoder::Opcode::BUFFER_ATOMIC_SMAX:
return BUFFER_ATOMIC(inst, IR::ValueOpcode::BufferAtomicSMax32);
case Decoder::Opcode::BUFFER_ATOMIC_UMAX:
@@ -1148,6 +1150,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:
@@ -1167,15 +1170,7 @@ void Translator::EmitMemory(const Decoder::Instruction& inst) {
case Decoder::Opcode::IMAGE_STORE:
case Decoder::Opcode::IMAGE_STORE_MIP: return IMAGE_STORE(inst);
case Decoder::Opcode::IMAGE_SAMPLE: return IMAGE_SAMPLE(inst);
case Decoder::Opcode::IMAGE_GATHER4_L:
case Decoder::Opcode::IMAGE_GATHER4_LZ:
case Decoder::Opcode::IMAGE_GATHER4_C:
case Decoder::Opcode::IMAGE_GATHER4_C_L:
case Decoder::Opcode::IMAGE_GATHER4_C_LZ:
case Decoder::Opcode::IMAGE_GATHER4_LZ_O:
case Decoder::Opcode::IMAGE_GATHER4_C_O:
case Decoder::Opcode::IMAGE_GATHER4_C_LZ_O:
case Decoder::Opcode::IMAGE_GATHER4H: return IMAGE_GATHER(inst);
case Decoder::Opcode::IMAGE_GATHER4: return IMAGE_GATHER(inst);
case Decoder::Opcode::DS_MIN_F32:
return DS_ATOMIC(inst, IR::ValueOpcode::SharedAtomicFMin32, false);
@@ -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;
@@ -118,15 +118,14 @@ ResourcePlan& ResourcePlan::operator=(ResourcePlan&& other) noexcept {
}
Program::~Program() {
// Planning expressions can refer to block values but outlive block storage in the base class.
// Planning expressions can refer to block values but outlive block storage in the base class,
// and values cross block boundaries: detach everything before any storage is destroyed.
for (auto& inst: value_storage) {
inst.Invalidate();
inst.DropArgs();
}
// Values may cross block boundaries. Detach all arguments before any block starts destroying
// its instruction storage so reverse-use links always point to live definitions.
for (auto* block: blocks) {
for (auto& inst: *block) {
inst.Invalidate();
inst.DropArgs();
}
}
}
@@ -169,6 +168,8 @@ bool EquivalentSelectorDomain(const ResourcePlan& program,
const DescriptorSource::IndirectDescriptor& right) {
return left.selector == right.selector && left.signed_count == right.signed_count &&
left.workgroup_axis == right.workgroup_axis &&
left.workgroup_multiplier == right.workgroup_multiplier &&
left.workgroup_shift == right.workgroup_shift &&
EquivalentValue(program, left.key_count, right.key_count) &&
EquivalentValue(program, left.selector_first, right.selector_first) &&
EquivalentValue(program, left.selector_mask, right.selector_mask);
@@ -155,14 +155,17 @@ 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::SelectorRead::MaskRanks, a, b, c)
KYTY_IO_FIELDS(DescriptorSource::IndirectDescriptor::SelectorRead, a, b, c, d, e, f, g, h, i, j, k,
l)
KYTY_IO_FIELDS(DescriptorSource::IndirectDescriptor::PointerTable, a, b, c, d)
KYTY_IO_FIELDS(DescriptorSource::IndirectDescriptor, a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p)
KYTY_IO_FIELDS(DescriptorSource::IndirectDescriptor, a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p,
q, r)
KYTY_IO_FIELDS(DescriptorSource, a, b, c)
KYTY_IO_FIELDS(SrtRead, a, b)
KYTY_IO_FIELDS(ResourceBlock, a, b, c, d)
KYTY_IO_FIELDS(ResourceBlock, a, b, c, d, e)
KYTY_IO_FIELDS(UniformFill, a, b, c, d, e, f)
KYTY_IO_FIELDS(UniformFillPlan, a, b, c, d)
KYTY_IO_FIELDS(ResourceSpecialization::Buffer, a, b, c, d, e, f, g, h)
+25 -4
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;
@@ -565,6 +572,13 @@ struct DescriptorSource {
bool operator==(const CountedRows& other) const = default;
};
struct MaskRanks {
uint32_t source = UINT32_MAX;
uint32_t stride = 0; // Byte spacing of the mask words its load can address.
uint32_t offset = 0;
bool operator==(const MaskRanks& other) const = default;
};
uint32_t source = UINT32_MAX;
uint32_t stride = 0;
uint32_t offset = 0;
@@ -572,10 +586,12 @@ struct DescriptorSource {
uint32_t bit_offset = 0;
uint32_t bit_mask = UINT32_MAX;
bool global = false;
// Four DWORD bitmap words per record; set-bit ranks subtract from the last key.
// Four or eight DWORD bitmap words per record; set ranks subtract from the last key.
bool bitset = false;
std::optional<CountedRows> rows;
std::vector<std::pair<uint32_t, uint32_t>> byte_ranges;
std::optional<uint32_t> excluded; // A guard keeps this key from the read.
std::optional<MaskRanks> ranks; // Records are the bits some mask word sets.
bool operator==(const SelectorRead& other) const = default;
};
@@ -599,8 +615,12 @@ struct DescriptorSource {
bool indexed = false;
bool signed_count = false;
uint32_t workgroup_axis = UINT32_MAX;
// Nonzero shift: workgroup keys are (selector_first + ID) * multiplier >> shift.
uint32_t workgroup_multiplier = 0;
uint32_t workgroup_shift = 0;
Value key_count;
// First selector index, or first descriptor key for a direct or bitmap range.
// First selector index, workgroup key base, or first descriptor key for a direct or bitmap
// range.
Value selector_first;
Value selector_mask;
std::vector<uint32_t> sources;
@@ -627,6 +647,7 @@ struct ResourceBlock {
Value condition;
std::vector<uint32_t> successors;
std::vector<uint32_t> sources;
std::vector<uint32_t> images;
std::vector<uint32_t> srt_reads;
};
+15 -74
View File
@@ -25,22 +25,6 @@ Value Value::F32(float value) {
return Value(Type::F32, std::bit_cast<uint32_t>(value));
}
bool Value::IsEmpty() const {
return type == Type::Void;
}
bool Value::IsImmediate() const {
return type != Type::Opaque;
}
bool Value::IsIdentity() const {
return type == Type::Opaque && inst->GetOpcode() == ValueOpcode::Identity;
}
bool Value::IsPhi() const {
return type == Type::Opaque && inst->GetOpcode() == ValueOpcode::Phi;
}
Type Value::GetType() const {
if (IsPhi()) {
return inst->Flags<Type>();
@@ -56,19 +40,6 @@ Inst* Value::Instruction() const {
return inst;
}
Inst* Value::TryInstruction() const {
return type == Type::Opaque ? inst : nullptr;
}
Inst* Value::ResolveInstruction() const {
EXIT_IF(type != Type::Opaque);
return IsIdentity() ? inst->Arg(0).ResolveInstruction() : inst;
}
Value Value::Resolve() const {
return IsIdentity() ? inst->Arg(0).Resolve() : *this;
}
ScalarReg Value::ScalarRegister() const {
EXIT_IF(type != Type::ScalarReg);
return scalar_reg;
@@ -94,11 +65,6 @@ uint16_t Value::U16() const {
return imm_u16;
}
uint32_t Value::U32() const {
EXIT_IF(type != Type::U32);
return imm_u32;
}
uint64_t Value::U64() const {
EXIT_IF(type != Type::U64);
return imm_u64;
@@ -152,10 +118,6 @@ Inst::~Inst() {
ClearArgs();
}
ValueOpcode Inst::GetOpcode() const {
return opcode;
}
Type Inst::GetType() const {
if (opcode == ValueOpcode::Phi) {
return static_cast<Type>(flags);
@@ -178,51 +140,25 @@ size_t Inst::UseCount() const {
return uses.size();
}
size_t Inst::NumArgs() const {
return num_args == PhiArity ? phi_args.size() : num_args;
}
size_t Inst::NumPhiBlocks() const {
return num_args == PhiArity ? phi_args.size() : 0;
}
Value Inst::Arg(size_t index) const {
EXIT_IF(index >= NumArgs());
if (num_args <= InlineArity) {
return fixed_args[index];
}
return num_args == PhiArity ? phi_args[index].second : large_args[index];
}
Block* Inst::PhiBlock(size_t index) const {
EXIT_IF(opcode != ValueOpcode::Phi || index >= phi_args.size());
return phi_args[index].first;
}
Block* Inst::Parent() const {
return parent;
}
const std::vector<Use>& Inst::Uses() const {
return uses;
}
void Inst::SetParent(Block* block) {
parent = block;
}
void Inst::SetArg(size_t index, Value value) {
const auto old = Arg(index);
if (auto* old_inst = old.TryInstruction(); old_inst != nullptr) {
auto& arg = ArgSlot(*this, index);
if (auto* old_inst = arg.TryInstruction(); old_inst != nullptr) {
RemoveUse(old_inst, index);
}
if (num_args <= InlineArity) {
fixed_args[index] = value;
} else if (num_args == PhiArity) {
phi_args[index].second = value;
} else {
large_args[index] = value;
}
arg = value;
if (auto* new_inst = value.TryInstruction(); new_inst != nullptr) {
AddUse(new_inst, index);
}
@@ -238,11 +174,15 @@ void Inst::AddPhiOperand(Block* predecessor, Value value) {
}
void Inst::ReplaceUsesWith(Value replacement, bool preserve) {
const auto old_uses = uses;
for (const auto& use: old_uses) {
use.user->SetArg(use.operand, replacement);
}
// Detach the arguments first (a phi can use itself), then move every use over in order.
Invalidate();
const auto old_uses = std::exchange(uses, {});
for (const auto& use: old_uses) {
ArgSlot(*use.user, use.operand) = replacement;
}
if (auto* target = replacement.TryInstruction(); target != nullptr) {
target->uses.insert(target->uses.end(), old_uses.begin(), old_uses.end());
}
if (preserve) {
opcode = ValueOpcode::Identity;
num_args = 1;
@@ -256,9 +196,6 @@ void Inst::Invalidate() {
}
void Inst::AddUse(Inst* used, size_t operand) {
const auto found = std::ranges::find_if(
used->uses, [&](const Use& use) { return use.user == this && use.operand == operand; });
EXIT_IF(found != used->uses.end());
used->uses.push_back({this, operand});
}
@@ -275,6 +212,10 @@ void Inst::ClearArgs() {
RemoveUse(value_inst, index);
}
}
DropArgs();
}
void Inst::DropArgs() {
if (num_args == PhiArity) {
std::destroy_at(&phi_args);
std::construct_at(&fixed_args);
+50 -6
View File
@@ -33,14 +33,14 @@ public:
static Value F16(uint16_t bits);
static Value F32(float value);
[[nodiscard]] bool IsEmpty() const;
[[nodiscard]] bool IsImmediate() const;
[[nodiscard]] bool IsEmpty() const { return type == Type::Void; }
[[nodiscard]] bool IsImmediate() const { return type != Type::Opaque; }
[[nodiscard]] bool IsIdentity() const;
[[nodiscard]] bool IsPhi() const;
[[nodiscard]] Type GetType() const;
[[nodiscard]] Inst* Instruction() const;
[[nodiscard]] Inst* TryInstruction() const;
[[nodiscard]] Inst* TryInstruction() const { return type == Type::Opaque ? inst : nullptr; }
[[nodiscard]] Inst* ResolveInstruction() const;
[[nodiscard]] Value Resolve() const;
[[nodiscard]] ScalarReg ScalarRegister() const;
@@ -112,7 +112,7 @@ public:
Inst(Inst&&) = delete;
Inst& operator=(Inst&&) = delete;
[[nodiscard]] ValueOpcode GetOpcode() const;
[[nodiscard]] ValueOpcode GetOpcode() const { return opcode; }
[[nodiscard]] Type GetType() const;
[[nodiscard]] bool MayHaveSideEffects() const;
[[nodiscard]] bool HasUses() const;
@@ -121,8 +121,8 @@ public:
[[nodiscard]] size_t NumPhiBlocks() const;
[[nodiscard]] Value Arg(size_t index) const;
[[nodiscard]] Block* PhiBlock(size_t index) const;
[[nodiscard]] Block* Parent() const;
[[nodiscard]] const std::vector<Use>& Uses() const;
[[nodiscard]] Block* Parent() const { return parent; }
[[nodiscard]] const std::vector<Use>& Uses() const { return uses; }
// SRT walker output index, assigned by the resource plan that owns this instruction.
[[nodiscard]] uint32_t WalkOutput() const { return walk_output; }
void SetWalkOutput(uint32_t index) const { walk_output = index; }
@@ -138,6 +138,8 @@ public:
void AddPhiOperand(Block* predecessor, Value value);
void ReplaceUsesWith(Value replacement, bool preserve = true);
void Invalidate();
// Drops the arguments without updating their use lists, for teardown that destroys both sides.
void DropArgs();
template <typename T>
requires(sizeof(T) <= sizeof(uint64_t) && std::is_trivially_copyable_v<T>)
@@ -157,6 +159,8 @@ public:
private:
friend void EliminateDeadCode(const std::vector<Block*>& blocks);
template <typename Self>
static auto& ArgSlot(Self& self, size_t index);
void AddUse(Inst* used, size_t operand);
void RemoveUse(Inst* used, size_t operand);
void ClearArgs();
@@ -181,4 +185,44 @@ private:
static_assert(sizeof(Inst) <= 120, "Inst operand and backend storage unintentionally increased");
// Called per operand by the IR passes and the resource walker; inline, as the build has no LTO.
inline bool Value::IsIdentity() const {
return type == Type::Opaque && inst->GetOpcode() == ValueOpcode::Identity;
}
inline bool Value::IsPhi() const {
return type == Type::Opaque && inst->GetOpcode() == ValueOpcode::Phi;
}
inline Inst* Value::ResolveInstruction() const {
EXIT_IF(type != Type::Opaque);
return IsIdentity() ? inst->Arg(0).ResolveInstruction() : inst;
}
inline Value Value::Resolve() const {
return IsIdentity() ? inst->Arg(0).Resolve() : *this;
}
inline uint32_t Value::U32() const {
EXIT_IF(type != Type::U32);
return imm_u32;
}
inline size_t Inst::NumArgs() const {
return num_args == PhiArity ? phi_args.size() : num_args;
}
template <typename Self>
auto& Inst::ArgSlot(Self& self, size_t index) {
EXIT_IF(index >= self.NumArgs());
if (self.num_args <= InlineArity) {
return self.fixed_args[index];
}
return self.num_args == PhiArity ? self.phi_args[index].second : self.large_args[index];
}
inline Value Inst::Arg(size_t index) const {
return ArgSlot(*this, index);
}
} // namespace Libs::Graphics::ShaderRecompiler::IR
@@ -123,6 +123,7 @@ BufferAccess BufferAccessOf(ValueOpcode opcode) {
case ValueOpcode::BufferAtomicISub32:
case ValueOpcode::BufferAtomicSMin32:
case ValueOpcode::BufferAtomicUMin32:
case ValueOpcode::BufferAtomicUMin64:
case ValueOpcode::BufferAtomicSMax32:
case ValueOpcode::BufferAtomicUMax32:
case ValueOpcode::BufferAtomicAnd32:
@@ -140,6 +141,7 @@ uint32_t BufferComponentCount(ValueOpcode opcode) {
switch (opcode) {
case ValueOpcode::BufferAtomicSwap64:
case ValueOpcode::BufferAtomicIAdd64:
case ValueOpcode::BufferAtomicUMin64:
case ValueOpcode::BufferAtomicAnd64:
case ValueOpcode::BufferAtomicOr64:
case ValueOpcode::LoadBufferU32x2:
@@ -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)
@@ -267,6 +269,7 @@ VALUE_OPCODE(BufferAtomicIAdd64, U64, BufferResource, U32, U32, U32, U64, U1)
VALUE_OPCODE(BufferAtomicISub32, U32, BufferResource, U32, U32, U32, U32, U1)
VALUE_OPCODE(BufferAtomicSMin32, U32, BufferResource, U32, U32, U32, U32, U1)
VALUE_OPCODE(BufferAtomicUMin32, U32, BufferResource, U32, U32, U32, U32, U1)
VALUE_OPCODE(BufferAtomicUMin64, U64, BufferResource, U32, U32, U32, U64, U1)
VALUE_OPCODE(BufferAtomicSMax32, U32, BufferResource, U32, U32, U32, U32, U1)
VALUE_OPCODE(BufferAtomicUMax32, U32, BufferResource, U32, U32, U32, U32, U1)
VALUE_OPCODE(BufferAtomicAnd32, U32, BufferResource, U32, U32, U32, U32, U1)
@@ -309,8 +312,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();
@@ -331,6 +331,18 @@ bool ResolveSelectorKeys(const ResourcePlan& program, const DescriptorSource::In
keys.resize(count);
return true;
};
// Reads the key of each record whose rank is set.
const auto read_ranks = [&](uint64_t base, uint64_t size, uint64_t first, uint64_t step,
uint32_t ranks) {
keys.reserve(std::popcount(ranks));
for (; ranks != 0u; ranks &= ranks - 1u) {
const auto offset = first + std::countr_zero(ranks) * step;
uint32_t key = 0;
if (offset > UINT32_MAX || !read_selector(base, size, offset, {&key, 1})) return false;
keys.push_back(key);
}
return true;
};
if (selector->rows) {
const auto& rows = *selector->rows;
DescriptorValue count_value;
@@ -377,19 +389,21 @@ bool ResolveSelectorKeys(const ResourcePlan& program, const DescriptorSource::In
}
} else if (selector->bitset) {
ShaderBufferResource material;
std::array<uint32_t, 4> masks {};
std::array<uint32_t, 8> storage {};
const auto record_words = std::min<size_t>(selector->stride / 4u, storage.size());
const auto masks = std::span(storage).first(record_words);
uint32_t first = 0, count = 0;
if (!DecodeBufferDescriptor(material_value, material) ||
!WalkedValue(program, indirect.selector_first, first) ||
!EvaluateIndirectCount(program, indirect, runtime, count) || count != masks.size() * 32u ||
selector->stride != sizeof(masks) || selector->offset != 0u ||
material.Type() != 0u || material.Stride() != sizeof(masks) ||
selector->stride != masks.size_bytes() || selector->offset != 0u ||
material.Type() != 0u || material.Stride() != selector->stride ||
material.SwizzleEnabled() || material.AddTid() || material.OutOfBounds() != 0u ||
(material.Base48() & 3u) != 0u) return false;
if (material.Format() != Prospero::BufferFormat::kInvalid) {
const auto words = material.GetSize() / sizeof(uint32_t);
// Bitmap storage can grow with resolution while its candidate domain stays
// at 128 keys. Bound scratch memory, preserving each coherent read's capture.
// Bitmap storage can grow with resolution while its candidate domain stays at 32 keys
// per record word. Bound scratch memory, preserving each coherent read's capture.
for (uint64_t first_word = 0; first_word < words; first_word += keys.size()) {
keys.resize(std::min<uint64_t>(words - first_word, MaxIndirectDescriptorProbes));
if (!ReadScalarTable(material.Base48(), material.GetSize(), first_word * 4u,
@@ -445,12 +459,35 @@ bool ResolveSelectorKeys(const ResourcePlan& program, const DescriptorSource::In
uint64_t {first} + count > material.NumRecords())) return false;
const auto last = count != 0u ? uint64_t {first} + count - 1u : 0u;
if (last > UINT32_MAX) return false;
// Out-of-bounds scalar reads return zero.
const auto end = count != 0u ? last * step + selector->offset + 4u : 0u;
if (end > uint64_t {UINT32_MAX} + 1u ||
(indirect.workgroup_axis == UINT32_MAX && end > size))
return false;
if (!read_keys(base, size, uint64_t {first} * step + selector->offset, step, count))
if (end > uint64_t {UINT32_MAX} + 1u) return false;
const auto start = uint64_t {first} * step + selector->offset;
if (selector->ranks) {
// A scan selects the ranks some word of its mask's address class sets.
const auto& mask = *selector->ranks;
DescriptorValue mask_value;
ShaderBufferResource masks;
if (count > 32u || !WalkedDescriptor(program, mask.source, mask_value) ||
!DecodeBufferDescriptor(mask_value, masks)) return false;
// Each chunk holds whole strides of the power-of-two class, or one of its words.
const auto words = mask.stride / 4u;
const auto chunk =
words <= MaxIndirectDescriptorProbes ? MaxIndirectDescriptorProbes : 1u;
uint32_t ranks = 0;
for (auto at = uint64_t {mask.offset}; at + 4u <= masks.GetSize();
at += uint64_t {std::max(chunk, words)} * 4u) {
keys.resize(std::min<uint64_t>((masks.GetSize() - at) / 4u, chunk));
if (!ReadScalarTable(masks.Base48(), masks.GetSize(), at, runtime, keys))
return false;
for (size_t word = 0; word < keys.size(); word += words) ranks |= keys[word];
}
keys.clear();
if (count < 32u) ranks &= (1u << count) - 1u;
if (!read_ranks(base, size, start, step, ranks)) return false;
} else if (!read_keys(base, size, start, step, count)) {
return false;
}
} else if (!indirect.selector_mask.IsEmpty()) {
uint32_t mask = 0;
uint32_t count = 0;
@@ -462,18 +499,8 @@ bool ResolveSelectorKeys(const ResourcePlan& program, const DescriptorSource::In
if (count < 32u) mask &= (1u << count) - 1u;
const auto material_base =
(static_cast<uint64_t>(material_value.dwords[1]) << 32u) | material_value.dwords[0];
keys.reserve(std::popcount(mask));
while (mask != 0u) {
const auto index = std::countr_zero(mask);
const auto offset = static_cast<uint64_t>(selector->offset) +
static_cast<uint64_t>(index) * selector->stride;
if (offset > UINT32_MAX) return false;
uint32_t key = 0;
if (!read_selector(material_base, UINT64_MAX, static_cast<uint32_t>(offset),
{&key, 1})) return false;
keys.push_back(key);
mask &= mask - 1u;
}
if (!read_ranks(material_base, UINT64_MAX, selector->offset, selector->stride, mask))
return false;
} else if (selector->stride == 0u) {
ShaderBufferResource material;
if (!DecodeBufferDescriptor(material_value, material) ||
@@ -503,6 +530,7 @@ bool ResolveSelectorKeys(const ResourcePlan& program, const DescriptorSource::In
if (selector->bit_offset != 0u || selector->bit_mask != UINT32_MAX || selector->maximum != UINT32_MAX)
for (auto& key: keys)
key = std::min((key >> selector->bit_offset) & selector->bit_mask, selector->maximum);
if (selector->excluded) std::erase(keys, *selector->excluded);
std::ranges::sort(keys);
keys.erase(std::unique(keys.begin(), keys.end()), keys.end());
}
@@ -756,6 +784,12 @@ bool MaterializeIndirectDescriptor(const ResourcePlan& p
std::iota(keys.begin(), keys.end(), first_key);
if (!keys.empty() && keys.back() < keys.front())
std::rotate(keys.begin(), keys.begin() + (UINT32_MAX - first_key + 1ull), keys.end());
if (indirect.workgroup_shift != 0u) {
for (auto& key: keys)
key = static_cast<uint32_t>(uint64_t {key} * indirect.workgroup_multiplier >>
indirect.workgroup_shift);
keys.erase(std::unique(keys.begin(), keys.end()), keys.end());
}
} else {
if (!indirect.selector_mask.IsEmpty() && table_value.dword_count != 2u) return false;
if (!selector_keys->resolved) {
@@ -1041,6 +1075,26 @@ static bool BuildResourceSpecialization(const ResourcePlan& program, ResourceSna
return dimension == Decoder::ImageDimension::Dim1D ||
dimension == Decoder::ImageDimension::Dim2D;
};
// The descriptor's type selects the address layout. A 2D or 3D candidate reads the leading
// coordinates of an equal or larger dimension, but an array with more than one slice reads
// the next as its slice. Derivative, LOD, clamp and mip operands shift with the dimension.
const auto leading = [&](Decoder::ImageDimension dimension, const DescriptorValue& value) {
const bool sliced =
static_cast<Prospero::ImageType>((value.dwords[3] >> 28u) & 0xfu) ==
Prospero::ImageType::kColor2DArray &&
(value.dwords[4] & 0x1fffu) != ((value.dwords[4] >> 16u) & 0x1fffu);
return !sliced &&
(dimension == Decoder::ImageDimension::Dim2D || dimension == base.dimension);
};
const bool leading_coordinates = (base.dimension == Decoder::ImageDimension::Dim2D ||
base.dimension == Decoder::ImageDimension::Dim3D) &&
leading(image_class.dimension, snapshot.images[exemplar]) &&
std::ranges::none_of(program.memory_info, [&](const MemoryInfo& memory) {
return memory.kind == ResourceKind::Image && memory.resource == root_index &&
(memory.image_has_mip || (memory.image_sample_flags &
(Decoder::ImageSampleFlagLod | Decoder::ImageSampleFlagDerivative |
Decoder::ImageSampleFlagLodClamp)) != 0u);
});
for (uint32_t candidate = 0; candidate < specialization.images.size(); candidate++) {
auto& image = specialization.images[candidate];
if (image.indirect_root != root_index) {
@@ -1060,8 +1114,10 @@ static bool BuildResourceSpecialization(const ResourcePlan& program, ResourceSna
base.dimension == Decoder::ImageDimension::Dim2D &&
!image.cube && !image_class.cube && is_1d_or_2d(image.dimension) &&
is_1d_or_2d(image_class.dimension);
const bool prefix_coordinates =
leading_coordinates && leading(image.dimension, snapshot.images[candidate]);
if ((!same_coordinates && !(is_2d(image.dimension) && is_2d(image_class.dimension)) &&
!unfiltered_coordinates) ||
!unfiltered_coordinates && !prefix_coordinates) ||
image.conversion_format != image_class.conversion_format ||
(!storage_swizzle && image.shader_swizzle != image_class.shader_swizzle)) {
return SpecializationFail(
@@ -1164,14 +1220,16 @@ static std::vector<ResourceBlock> ResourceControlFlow(const Program& program, co
block.sources.push_back(program.info.buffers.at(memory.resource).source);
} else {
block.sources.push_back(program.info.images.at(memory.resource).source);
block.images.push_back(memory.resource);
if (image.needs_sampler) {
block.sources.push_back(program.info.samplers.at(memory.sampler).source);
}
}
}
std::ranges::sort(block.sources);
block.sources.erase(std::unique(block.sources.begin(), block.sources.end()),
block.sources.end());
for (auto* list: {&block.sources, &block.images}) {
std::ranges::sort(*list);
list->erase(std::unique(list->begin(), list->end()), list->end());
}
}
for (auto& block: blocks) block.condition = predicate(block.condition);
if (std::ranges::none_of(
@@ -1645,7 +1703,7 @@ bool MaterializeResources(const ResourcePlan& program, const SrtRuntime& runtime
}
if (source->indirect_descriptor.has_value()) {
snapshot.images[i] = {.dword_count = 8u};
if (!WalkedActive(program, image.source)) {
if (!WalkedActive(program, image.source) || !WalkedImageActive(program, i)) {
continue;
}
const auto& indirect = *source->indirect_descriptor;
@@ -1665,7 +1723,9 @@ bool MaterializeResources(const ResourcePlan& program, const SrtRuntime& runtime
if (!evaluate(image.source, snapshot.images[i])) {
return false;
}
if (!ValidImageDescriptor(snapshot.images[i], image.r128)) {
// An image listed only by unreached blocks reads zero, not its source's descriptor.
if (!WalkedImageActive(program, i) ||
!ValidImageDescriptor(snapshot.images[i], image.r128)) {
snapshot.images[i].dwords.fill(0);
}
}
@@ -1711,8 +1771,9 @@ bool MaterializeResources(const ResourcePlan& program, const SrtRuntime& runtime
}
uint32_t ImageHeapKind(const ImageResource& root, DescriptorValue& value) {
// Mirrors BuildResourceSpecialization for sampled roots: entries it would reject, and
// entries that need shader-side conversion, read as null instead of failing the draw.
// Mirrors BuildResourceSpecialization for sampled roots: entries it would reject, entries of
// another dimension and entries that need shader-side conversion read as null instead of
// failing the draw.
const auto format = static_cast<Prospero::BufferFormat>((value.dwords[1] >> 20u) & 0x1ffu);
const auto tile = static_cast<Prospero::TileMode>((value.dwords[3] >> 20u) & 0x1fu);
const auto numeric_class =
@@ -7,6 +7,8 @@
#include <algorithm>
#include <bit>
#include <cmath>
#include <cstdio>
#include <fmt/format.h>
#include <map>
#include <numeric>
@@ -1014,7 +1016,9 @@ private:
(!shared_image_heap && (a.table_offset != b.table_offset ||
a.table_immediate != b.table_immediate ||
a.table_stride != b.table_stride)) || a.indexed != b.indexed ||
a.workgroup_axis != b.workgroup_axis || a.sources != b.sources ||
a.workgroup_axis != b.workgroup_axis ||
a.workgroup_multiplier != b.workgroup_multiplier ||
a.workgroup_shift != b.workgroup_shift || a.sources != b.sources ||
a.signed_count != b.signed_count ||
!EquivalentValue(m_program, a.key_count, b.key_count) ||
a.selector_first.IsEmpty() != b.selector_first.IsEmpty() ||
@@ -1136,31 +1140,63 @@ private:
LaneQuantifier lanes = LaneQuantifier::All;
};
// The immediate that edge proves value differs from on every lane, if any.
std::optional<uint32_t> ExcludedBy(const EdgePredicate& edge, Value value) const {
if (edge.lanes != LaneQuantifier::All) return {};
const auto* test = edge.condition.TryInstruction();
if (test != nullptr && test->NumArgs() == 2u &&
((test->GetOpcode() == ValueOpcode::INotEqual32 && edge.positive) ||
(test->GetOpcode() == ValueOpcode::IEqual32 && !edge.positive))) {
for (uint32_t arg = 0; arg < 2u; ++arg) {
uint32_t immediate;
if (ImmediateU32(test->Arg(arg), immediate) &&
EquivalentValue(m_program, test->Arg(arg ^ 1u), value)) return immediate;
}
}
return {};
}
bool NonzeroOnEntry(Value value, const Block* block) const {
if (block == nullptr) return false;
bool witnessed = false;
const bool guarded = GuardedOnEntry(block, [](const Block* current) {
return current->ImmPredecessors().size() != 1u;
}, [&](const EdgePredicate& edge) {
if (edge.lanes != LaneQuantifier::All) return false;
const auto* test = edge.condition.TryInstruction();
if (test != nullptr && test->NumArgs() == 2u &&
((test->GetOpcode() == ValueOpcode::INotEqual32 && edge.positive) ||
(test->GetOpcode() == ValueOpcode::IEqual32 && !edge.positive))) {
for (uint32_t arg = 0; arg < 2u; ++arg) {
uint32_t immediate;
if (ImmediateU32(test->Arg(arg), immediate) && immediate == 0u &&
EquivalentValue(m_program, test->Arg(arg ^ 1u), value)) return witnessed = true;
}
}
return false;
return ExcludedBy(edge, value) == 0u && (witnessed = true);
});
return guarded && witnessed;
}
uint32_t WorkgroupAxis(Value key) const {
// The key that every path from its definition to block rules out, if any.
std::optional<uint32_t> ExcludedKeyOnEntry(Value key, const Block* block) const {
const auto* definition = key.Resolve().TryInstruction();
if (definition == nullptr) return {};
std::optional<uint32_t> excluded;
const auto defined = [&](const Block* current) { return current == definition->Parent(); };
const bool guarded = GuardedOnEntry(block, defined, [&](const EdgePredicate& edge) {
const auto immediate = ExcludedBy(edge, key);
if (!immediate || (excluded && excluded != immediate)) return false;
excluded = immediate;
return true;
});
return guarded ? excluded : std::nullopt;
}
// The workgroup ID axis of key; with first, also of a runtime first + workgroup ID sum.
uint32_t WorkgroupAxis(Value key, Value* first = nullptr) const {
const auto* builtin = key.Resolve().TryInstruction();
uint32_t axis = UINT32_MAX;
if (first != nullptr && builtin != nullptr && builtin->GetOpcode() == ValueOpcode::IAdd32) {
for (uint32_t arm = 0; arm < 2u; ++arm) {
axis = WorkgroupAxis(builtin->Arg(arm));
const auto base = builtin->Arg(arm ^ 1u);
if (axis == UINT32_MAX ||
!ValidateRuntimeValue(m_program, base, RuntimeValueType::Integer)) continue;
*first = base;
return axis;
}
return UINT32_MAX;
}
if (m_program.stage != ShaderType::Compute || builtin == nullptr ||
builtin->GetOpcode() != ValueOpcode::GetBuiltin || builtin->NumArgs() != 2u ||
builtin->Arg(0) != Value(static_cast<uint32_t>(StageInputKind::WorkgroupId)) ||
@@ -1168,6 +1204,27 @@ private:
return axis;
}
// Matches key = (first + workgroup ID) * multiplier >> shift with shift >= 32; first and the
// division are optional.
bool MatchWorkgroupKey(Value key, DescriptorSource::IndirectDescriptor& indirect) const {
uint32_t bits = 0, multiplier = 0;
const auto* shift = key.Resolve().TryInstruction();
if (shift != nullptr && shift->GetOpcode() == ValueOpcode::ShiftRightLogical32) {
const auto* high = shift->Arg(0).Resolve().TryInstruction();
if (high != nullptr && high->GetOpcode() == ValueOpcode::UMulHi &&
ImmediateU32(shift->Arg(1), bits) && bits < 32u &&
ImmediateU32(high->Arg(1), multiplier)) {
key = high->Arg(0);
bits += 32u;
}
}
indirect.workgroup_axis = WorkgroupAxis(key, &indirect.selector_first);
if (indirect.workgroup_axis == UINT32_MAX) return false;
indirect.workgroup_multiplier = multiplier;
indirect.workgroup_shift = bits;
return true;
}
bool MatchTableOffset(Value value, Value& key, uint32_t& offset, uint32_t& stride) const {
offset = 0;
for (;;) {
@@ -1672,6 +1729,8 @@ private:
uint32_t initial_arm;
};
static bool ZeroBased(uint32_t initial, uint32_t step) { return initial == 0u && step == 1u; }
std::optional<Induction> MatchInduction(Value key, const auto& accepts_step) const {
key = key.Resolve();
const auto* phi = key.TryInstruction();
@@ -1733,8 +1792,7 @@ private:
}
uint32_t PostTestLoopMaximum(Value key) const {
const auto induction = MatchInduction(key,
[](uint32_t initial, uint32_t step) { return initial == 0u && step == 1u; });
const auto induction = MatchInduction(key, ZeroBased);
if (!induction) return UINT32_MAX;
const auto [phi, step, initial_arm] = *induction;
const auto* incoming = phi->PhiBlock(initial_arm ^ 1u);
@@ -1767,8 +1825,7 @@ private:
const Inst* BoundedLoop(Value key, const Block* use, const auto& accepts_bound,
LaneQuantifier lanes = LaneQuantifier::Any) const {
return BoundedInduction(key, use,
[](uint32_t initial, uint32_t step) { return initial == 0u && step == 1u; },
return BoundedInduction(key, use, ZeroBased,
[&](const Inst& compare, uint32_t initial) {
return (compare.GetOpcode() == ValueOpcode::SLessThan32 ||
compare.GetOpcode() == ValueOpcode::ULessThan32) &&
@@ -1793,6 +1850,29 @@ private:
});
}
// A zero-based key that only per-lane data bounds takes the count all uniform zero-based
// loops in the program share, assuming each lane's count stays within it.
bool ProgramLoopCount(Value key, DescriptorSource::IndirectDescriptor& indirect) const {
if (!MatchInduction(key, ZeroBased) || BoundedLoop(key, nullptr) != nullptr) return false;
const Inst* count = nullptr;
for (auto* block: m_program.blocks) {
for (auto& phi: *block) {
if (phi.GetOpcode() != ValueOpcode::Phi) break;
const auto* compare = BoundedLoop(Value(&phi), nullptr);
if (compare == nullptr) continue;
if (count != nullptr && !EquivalentValue(m_program, compare->Arg(1), count->Arg(1)))
return false;
count = compare;
}
}
if (count == nullptr) return false;
std::printf("Warning: shader 0x%016llx bounds a per-lane table loop by its uniform loops' "
"count\n", static_cast<unsigned long long>(m_program.shader_hash));
indirect.key_count = count->Arg(1);
indirect.signed_count = count->GetOpcode() == ValueOpcode::SLessThan32;
return true;
}
Value ActiveValue(Value value, Value guard) const {
value = value.Resolve();
while (const auto* select = value.TryInstruction()) {
@@ -1829,6 +1909,10 @@ private:
}
bool Dominates(const Block* initial, const Block* use, Value witness = {}) const {
// The proven local-zero invocation fails every all-lane bypass condition that excludes it,
// such as an all-inactive or an all-nonzero bypass.
const auto* negated = SimplifyGuard(witness).TryInstruction();
const bool nonzero = negated != nullptr && negated->GetOpcode() == ValueOpcode::LogicalNot;
std::vector<const Block*> pending {use};
for (size_t index = 0; index < pending.size(); ++index) {
const auto* current = pending[index];
@@ -1837,9 +1921,9 @@ private:
for (const auto* previous: current->ImmPredecessors()) {
if (!witness.IsEmpty()) {
const auto edge = ConditionalEdge(previous, current);
// The proven local-zero invocation makes an all-inactive bypass impossible.
if (edge && edge->lanes == LaneQuantifier::All && !edge->positive &&
Implies(witness, edge->condition)) continue;
if (edge && edge->lanes == LaneQuantifier::All &&
(edge->positive ? nonzero && Implies(edge->condition, negated->Arg(0))
: Implies(witness, edge->condition))) continue;
}
if (std::ranges::find(pending, previous) == pending.end()) pending.push_back(previous);
}
@@ -1948,7 +2032,10 @@ private:
bool LocalZeroWitness(Value guard) const {
const auto* equal = SimplifyGuard(guard).TryInstruction();
if (equal == nullptr || equal->GetOpcode() != ValueOpcode::IEqual32) return false;
const bool negated = equal != nullptr && equal->GetOpcode() == ValueOpcode::LogicalNot;
if (negated) equal = SimplifyGuard(equal->Arg(0)).TryInstruction();
const auto expected = negated ? ValueOpcode::INotEqual32 : ValueOpcode::IEqual32;
if (equal == nullptr || equal->GetOpcode() != expected) return false;
for (uint32_t arg = 0; arg < 2u; ++arg) {
if (equal->Arg(arg).Resolve() != Value(0u)) continue;
Value induction;
@@ -1982,6 +2069,10 @@ private:
EquivalentValue(m_program, test->Arg(0), value)) return test->Arg(1);
if ((positive ? op == ValueOpcode::UGreaterThan32 : op == ValueOpcode::ULessThanEqual32) &&
EquivalentValue(m_program, test->Arg(1), value)) return test->Arg(0);
uint32_t limit = 0;
if ((positive ? op == ValueOpcode::ULessThanEqual32 : op == ValueOpcode::UGreaterThan32) &&
ImmediateU32(test->Arg(1), limit) && limit != UINT32_MAX &&
EquivalentValue(m_program, test->Arg(0), value)) return Value(limit + 1u);
const auto* scan = value.Resolve().TryInstruction();
if (finite_bitscan && scan != nullptr && scan->GetOpcode() == ValueOpcode::FindILsb32 &&
(positive ? op == ValueOpcode::INotEqual32 : op == ValueOpcode::IEqual32)) {
@@ -2056,7 +2147,7 @@ private:
const auto* read = ActiveValue(value, active).TryInstruction();
if (read == nullptr || read->GetOpcode() != ValueOpcode::LoadSharedU32 ||
m_program.stage != ShaderType::Compute ||
read->Arg(1).Resolve() != Value(true)) return {};
!Implies(active, read->Arg(1))) return {};
uint64_t group_size = 1u;
for (const auto dimension: m_input.compute->threads_num) {
if (dimension == 0u || dimension > m_input.compute->wave_size) return {};
@@ -2087,10 +2178,16 @@ private:
const auto* limit = BoundedLoop(read_offset.index, read->Parent(),
[](const Inst& compare, uint32_t) { return compare.GetOpcode() == ValueOpcode::ULessThan32; });
if (limit == nullptr) return {};
auto counter_active = active;
const auto* counter_read = ActiveValue(limit->Arg(1), active).TryInstruction();
// Every lane reads the same counter word, so a selected lane carries it.
if (counter_read != nullptr && counter_read->GetOpcode() == ValueOpcode::ReadFirstLane) {
counter_active = counter_read->Arg(1);
counter_read = ActiveValue(counter_read->Arg(0), counter_active).TryInstruction();
}
uint32_t consumed_counter = 0;
if (counter_read == nullptr || counter_read->GetOpcode() != ValueOpcode::LoadSharedU32 ||
counter_read->Arg(1).Resolve() != Value(true) ||
!Implies(counter_active, counter_read->Arg(1)) ||
!constant_address(*counter_read, consumed_counter)) return {};
struct Initializer { const Inst* inst; uint32_t address; uint32_t words; };
@@ -2304,25 +2401,23 @@ private:
((offset | stride | memory->offset) & 3u) == 0u &&
uint64_t {offset} + memory->offset <= UINT32_MAX;
if (affine) {
workgroup_axis = WorkgroupAxis(index);
if (workgroup_axis == UINT32_MAX) {
const auto* sum = index.Resolve().TryInstruction();
if (sum != nullptr && sum->GetOpcode() == ValueOpcode::IAdd32) {
for (uint32_t arm = 0; arm < 2u; ++arm) {
const auto axis = WorkgroupAxis(sum->Arg(arm));
const auto base = sum->Arg(arm ^ 1u);
if (axis == UINT32_MAX ||
!ValidateRuntimeValue(m_program, base, RuntimeValueType::Integer)) continue;
workgroup_axis = axis;
first = base;
break;
}
}
}
workgroup_axis = WorkgroupAxis(index, &first);
const auto* bound = BoundedLoop(index, read->Parent());
if (bound != nullptr && bound->GetOpcode() == ValueOpcode::ULessThan32) {
count = bound->Arg(1);
} else if (workgroup_axis == UINT32_MAX && !require_bound) {
// Other bounded indices, such as bit scans, read the records up to their maximum;
// a bit scan of a nonzero mask load reads only the ranks its words set.
const auto maximum = SelectorMaximum(index);
if (maximum == 31u) selector.ranks = BitScanRanks(index);
if (maximum < MaxIndirectDescriptorProbes) count = Value(maximum + 1u);
}
// A record selected from a shared append list is below the list's bound.
const auto* lane = index.Resolve().TryInstruction();
if (count.IsEmpty() && lane != nullptr &&
lane->GetOpcode() == ValueOpcode::ReadFirstLane &&
HasActiveLane(lane->Arg(1), lane->Parent()))
count = SharedLoadBound(lane->Arg(0), lane->Arg(1));
}
// Raw pointers have no V# bounds for enumerating an unbounded selector domain.
if (memory->kind == ResourceKind::ScalarAddress && count.IsEmpty() &&
@@ -2498,7 +2593,8 @@ private:
// Each equation is an inductive subset obligation. Phis require every incoming
// value; AND needs either operand. Grounding rejects cycles without an initial
// value, then the greatest fixed point checks every initial and carried edge.
bool BitsetMaskSubset(Value mask, Value active, Value word, const Inst& read) const {
bool BitsetMaskSubset(Value mask, Value active, Value word,
std::span<const Inst* const> reads) const {
struct State {
Value value;
Value guard;
@@ -2510,7 +2606,10 @@ private:
bool all = true;
std::vector<size_t> terms;
};
const auto load_guard = read.Arg(4).Resolve();
// A scalar load reads one word without EXEC; four-word loads take their guard.
const bool scalar = reads[0]->GetOpcode() == ValueOpcode::ReadConstBuffer;
const auto load_guard = scalar ? Value(true) : reads[0]->Arg(4).Resolve();
const auto words = static_cast<uint32_t>(reads.size()) * (scalar ? 1u : 4u);
std::vector<State> states(2);
std::vector<Equation> equations(2);
const auto add = [&](State state) {
@@ -2521,9 +2620,9 @@ private:
equations.emplace_back();
return states.size() - 1u;
};
std::array<size_t, 5> roots {add({.value = active})};
for (uint32_t component = 0; component < 4u; ++component)
roots[component + 1u] = add({mask, active, component, component});
std::vector<size_t> roots {add({.value = active})};
for (uint32_t component = 0; component < words; ++component)
roots.push_back(add({mask, active, component, component}));
for (size_t index = 2; index < states.size(); ++index) {
const auto state = states[index];
const auto* inst = state.value.TryInstruction();
@@ -2573,11 +2672,14 @@ private:
switch (inst->GetOpcode()) {
case ValueOpcode::CompositeExtractU32x4: {
uint32_t component;
if (inst->Arg(0).Resolve().TryInstruction() == &read &&
ImmediateU32(inst->Arg(1), component) && component == state.component)
if (inst->Arg(0).Resolve().TryInstruction() == reads[state.component / 4u] &&
ImmediateU32(inst->Arg(1), component) && component == state.component % 4u)
equation.terms = {1u};
break;
}
case ValueOpcode::ReadConstBuffer:
if (scalar && inst == reads[state.component]) equation.terms = {1u};
break;
case ValueOpcode::Phi:
// The predecessor may be an earlier outer iteration. Establish
// this register's invariant without the current word or EXEC.
@@ -2598,17 +2700,17 @@ private:
const auto* masked = key.TryInstruction();
uint32_t bound = 0;
if (masked != nullptr && masked->GetOpcode() == ValueOpcode::BitwiseAnd32 &&
DescendingLoop(word, masked->Parent(), bound) && bound == 3u) {
DescendingLoop(word, masked->Parent(), bound) && bound == words - 1u) {
for (uint32_t side = 0; side < 2u; ++side) {
uint32_t bits;
if (ImmediateU32(masked->Arg(side), bits) && (bits & 3u) == 3u)
if (ImmediateU32(masked->Arg(side), bits) && (bits & bound) == bound)
key = masked->Arg(side ^ 1u).Resolve();
}
}
return EquivalentValue(m_program, key, word);
});
uint32_t literal;
if (ImmediateU32(equality, literal) && literal < 4u) selected_word = literal;
if (ImmediateU32(equality, literal) && literal < words) selected_word = literal;
equation.terms.push_back(term(inst->Arg(1), inst->Arg(0), selected_word));
}
if (!selected || !*selected)
@@ -2646,7 +2748,53 @@ private:
return std::ranges::all_of(roots, [&](size_t root) { return valid[root]; });
}
bool MatchBitsetSelector(Value rank, Value active, SelectorBounds bounds,
// The mask words whose set bits are every record a bit scan of index selects: the scanned
// value keeps only bits of one scalar load, which reads a word its address can reach.
std::optional<DescriptorSource::IndirectDescriptor::SelectorRead::MaskRanks>
BitScanRanks(Value index) {
const auto* scan = index.Resolve().TryInstruction();
if (scan == nullptr || scan->GetOpcode() != ValueOpcode::FindILsb32) return {};
const auto* masked = scan->Arg(0).Resolve().TryInstruction();
if (masked == nullptr) return {};
std::vector<const Inst*> values {masked};
const Inst* load = nullptr;
for (size_t i = 0; i < values.size() && load == nullptr; ++i) {
const auto* value = values[i];
if (value->GetOpcode() == ValueOpcode::ReadConstBuffer) load = value;
if (value->GetOpcode() != ValueOpcode::Phi &&
value->GetOpcode() != ValueOpcode::BitwiseAnd32) continue;
for (size_t arg = 0; arg < value->NumArgs(); ++arg) {
const auto* next = value->Arg(arg).Resolve().TryInstruction();
if (next != nullptr && std::ranges::find(values, next) == values.end())
values.push_back(next);
}
}
uint32_t memory_index = 0;
const auto* memory = load != nullptr ? ScalarReadMemory(*load, memory_index) : nullptr;
if (memory == nullptr || memory->offset > INT32_MAX ||
!MemoryIndexBelongsTo(memory_index, *load) ||
!BitsetMaskSubset(scan->Arg(0), Value(true), Value(0u), std::array {load})) return {};
auto address = load->Arg(1);
auto active = Value(true);
const auto* lane = address.Resolve().TryInstruction();
if (lane != nullptr && lane->GetOpcode() == ValueOpcode::ReadFirstLane) {
if (!HasActiveLane(lane->Arg(1), lane->Parent())) return {};
active = lane->Arg(1);
address = lane->Arg(0);
}
// A left shift keeps the address a multiple of its scale; other addresses read any word.
const auto* shift = ActiveValue(address, active).TryInstruction();
uint32_t bits = 0;
if (shift == nullptr || shift->GetOpcode() != ValueOpcode::ShiftLeftLogical32 ||
!ImmediateU32(shift->Arg(1), bits) || bits >= 32u) bits = 0;
DescriptorSource source;
if (!MakeRuntimeTableSource(*load, source)) return {};
return DescriptorSource::IndirectDescriptor::SelectorRead::MaskRanks {
.source = InternSource(source), .stride = std::max(1u << bits, 4u),
.offset = memory->offset & ~3u};
}
bool MatchBitsetSelector(Value rank, Value active, SelectorBounds bounds, uint32_t words,
DescriptorSource::IndirectDescriptor& indirect) {
const auto selected_value = [&](Value value) {
for (;;) {
@@ -2670,7 +2818,7 @@ private:
}
const auto* word_inst = word.TryInstruction();
if (word_inst == nullptr || std::ranges::none_of(bounds, [&](const auto& bound) {
return bound.first == word_inst && bound.second == 3u;
return bound.first == word_inst && bound.second == words - 1u;
})) return false;
// FFBH's leading-zero count is inverted again to recover the set-bit rank.
for (uint32_t inverse = 0; inverse < 2u; ++inverse) {
@@ -2682,21 +2830,34 @@ private:
const auto* scan = bit.TryInstruction();
if (scan == nullptr || scan->GetOpcode() != ValueOpcode::FindUMsb32 ||
!ImpliesNonzero(active, scan->Arg(0))) return false;
const auto bitmap_load = [&](const Inst& read, uint32_t offset) {
const auto index = read.Flags<MemoryFlags>().index;
if (read.GetOpcode() != ValueOpcode::LoadBufferU32x4 || InCycle(read.Parent()) ||
index >= m_program.memory_info.size()) return false;
const auto& memory = m_program.memory_info[index];
return memory.kind == ResourceKind::Buffer && !memory.typed && !memory.formatted &&
memory.idxen && !memory.offen && memory.offset == offset &&
memory.data_bits == 32u && memory.data_dwords == 4u &&
read.Arg(2).Resolve() == Value(0u) && read.Arg(3).Resolve() == Value(0u);
};
for (const auto* block: m_program.blocks) {
for (const auto& read: *block) {
if (read.GetOpcode() != ValueOpcode::LoadBufferU32x4 || InCycle(block)) continue;
const auto index = read.Flags<MemoryFlags>().index;
if (index >= m_program.memory_info.size()) continue;
const auto& memory = m_program.memory_info[index];
if (memory.kind != ResourceKind::Buffer || memory.typed || memory.formatted ||
!memory.idxen || memory.offen || memory.offset != 0u || memory.data_bits != 32u ||
memory.data_dwords != 4u || read.Arg(2).Resolve() != Value(0u) ||
read.Arg(3).Resolve() != Value(0u) ||
!BitsetMaskSubset(scan->Arg(0), active, word, read)) continue;
if (!bitmap_load(read, 0u)) continue;
// An eight-word record continues in a second four-word load of the same buffer and EXEC.
std::array<const Inst*, 2> reads {&read};
if (words == 8u)
for (const auto* other: m_program.blocks)
for (const auto& load: *other)
if (reads[1] == nullptr && bitmap_load(load, 16u) &&
EquivalentValue(m_program, load.Arg(0), read.Arg(0)) &&
EquivalentValue(m_program, load.Arg(4), read.Arg(4))) reads[1] = &load;
const auto parts = std::span(reads).first(words / 4u);
DescriptorSource source;
if (!MakeRuntimeTableSource(read, source)) continue;
if (std::ranges::find(parts, nullptr) != parts.end() ||
!BitsetMaskSubset(scan->Arg(0), active, word, parts) ||
!MakeRuntimeTableSource(read, source)) continue;
indirect.selector.emplace(DescriptorSource::IndirectDescriptor::SelectorRead {
.source = InternSource(source), .stride = 16u, .offset = 0u, .bitset = true});
.source = InternSource(source), .stride = words * 4u, .offset = 0u, .bitset = true});
return true;
}
}
@@ -2726,7 +2887,8 @@ private:
first.SetArg(1, Value(maximum));
indirect.selector_first = Value(&first);
indirect.key_count = Value(maximum + 1u);
if (maximum == 127u) MatchBitsetSelector(lane->Arg(0), lane->Arg(1), bounds, indirect);
if (maximum == 127u || maximum == 255u)
MatchBitsetSelector(lane->Arg(0), lane->Arg(1), bounds, (maximum + 1u) / 32u, indirect);
return true;
}
@@ -2993,6 +3155,47 @@ private:
return true;
}
// The EXEC predicate of the image access through use; empty for any other use.
Value AccessPredicate(const Use& use, bool sampler) const {
const auto& access = *use.user;
if (use.operand != (sampler ? 1u : 0u) ||
ImageOpcodeInfoOf(access.GetOpcode()).access == ImageAccess::None) return {};
if (sampler && (access.GetOpcode() != ValueOpcode::ImageSampleRaw ||
(m_program.memory_info[access.Flags<MemoryFlags>().index].image_sample_flags &
Decoder::ImageSampleFlagAdjust) != 0u)) return {};
const auto mask = access.Arg(access.NumArgs() - 1u);
return mask.GetType() == Type::U1 ? mask : Value {};
}
// A key no bound covers takes its maximum from a signed clamp to a constant, on the key or on
// the key plus a constant in an earlier-tracked sibling read of the same table. Negative keys
// and keys past that maximum read the first row.
uint32_t ClampedTableMaximum(Value key, const DescriptorSource& table,
const DescriptorSource::IndirectDescriptor& indirect) const {
uint32_t row = 0, offset = 0;
const auto clamped = [&](Value value) {
const auto* min = value.Resolve().TryInstruction();
if (min != nullptr && min->GetOpcode() == ValueOpcode::SMin32)
for (uint32_t arg = 0; arg < 2u; ++arg)
if (ImmediateU32(min->Arg(arg), row)) return min->Arg(arg ^ 1u);
return Value {};
};
if (!clamped(key).IsEmpty()) return row;
for (const auto& sibling: m_indirect_descriptors) {
const auto& read = *m_sources[sibling.source].indirect_descriptor;
const auto* add = clamped(sibling.key).Resolve().TryInstruction();
if (add == nullptr || add->GetOpcode() != ValueOpcode::IAdd32 ||
m_sources[read.table_source] != table ||
read.table_offset != indirect.table_offset ||
read.table_stride != indirect.table_stride ||
read.table_immediate != indirect.table_immediate) continue;
for (uint32_t arg = 0; arg < 2u; ++arg)
if (ImmediateU32(add->Arg(arg), offset) &&
EquivalentValue(m_program, add->Arg(arg ^ 1u), key)) return row;
}
return UINT32_MAX;
}
bool TryMakeTableDescriptor(Inst& handle, const DescriptorSource& descriptor,
IndirectDescriptorPlan& plan) {
const bool sampler = handle.GetOpcode() == ValueOpcode::GetSamplerResource;
@@ -3019,6 +3222,7 @@ private:
} else if (!plan.table_indexed &&
MatchScalarMaterialKey(key, indirect, material_source,
!sampler && table_source.dword_count == 4u, sampler)) {
indirect.selector->excluded = ExcludedKeyOnEntry(key, handle.Parent());
// Keep live descriptor payloads, but project descriptor-only SMEM onto
// the GPU key using the same plan as other indirect image reads.
plan.retain_reads |= table_source.dword_count == 4u;
@@ -3080,36 +3284,42 @@ private:
if (indirect.key_count.IsEmpty() &&
!MatchUniformizedMaterialKey(key, handle, indirect, material_source, false) &&
!MatchUniformizedMaterialKey(key, handle, indirect, material_source, true) &&
!MatchReversedImageRange(key, indirect)) {
const auto maximum = SelectorMaximum(key);
if (maximum >= MaxIndirectDescriptorProbes) return false;
indirect.key_count = Value(maximum + 1u);
!MatchReversedImageRange(key, indirect) && !MatchWorkgroupKey(key, indirect)) {
auto maximum = SelectorMaximum(key);
if (maximum >= MaxIndirectDescriptorProbes) {
// Each image access bounds the key on the lanes it executes.
maximum = 0u;
for (const auto& use: handle.Uses()) {
const auto active = AccessPredicate(use, false);
if (active.IsEmpty()) {
maximum = UINT32_MAX;
break;
}
maximum = std::max(maximum, SelectorMaximum(key, {}, 0, active));
}
}
if (maximum >= MaxIndirectDescriptorProbes)
maximum = ClampedTableMaximum(key, table_source, indirect);
if (maximum < MaxIndirectDescriptorProbes)
indirect.key_count = Value(maximum + 1u);
else if (!ProgramLoopCount(key, indirect))
return false;
}
if ((table_offset & 3u) != 0u) return false;
} else {
// Keep the GPU selector and descriptor reads, and preserve the proven scalar
// offset domain within the bounded image table.
if (sampler || plan.split_offsets) return false;
const auto active = [&](const Use& use, bool sampler) -> Value {
const auto& access = *use.user;
if (use.operand != (sampler ? 1u : 0u) ||
ImageOpcodeInfoOf(access.GetOpcode()).access == ImageAccess::None) return {};
if (sampler && (access.GetOpcode() != ValueOpcode::ImageSampleRaw ||
(m_program.memory_info[access.Flags<MemoryFlags>().index].image_sample_flags &
Decoder::ImageSampleFlagAdjust) != 0u)) return {};
const auto mask = access.Arg(access.NumArgs() - 1u);
return mask.GetType() == Type::U1 ? mask : Value {};
};
bool shared_selector = false;
if (!plan.table_indexed) {
for (const auto& owner: m_indirect_descriptors) {
const auto& selected = *m_sources[owner.source].indirect_descriptor;
if (!selected.selector || !EquivalentValue(m_program, key, owner.key) ||
!std::ranges::all_of(handle.Uses(), [&](const Use& target) {
const auto target_active = active(target, false);
const auto target_active = AccessPredicate(target, false);
return !target_active.IsEmpty() &&
std::ranges::any_of(owner.handle->Uses(), [&](const Use& witness) {
const auto owner_active = active(witness,
const auto owner_active = AccessPredicate(witness,
owner.handle->GetOpcode() == ValueOpcode::GetSamplerResource);
return !owner_active.IsEmpty() && AvailableBefore(*witness.user, *target.user) &&
Implies(target_active, owner_active);
@@ -3430,6 +3640,42 @@ private:
return result;
}
// Bounds of an F32 value on the lanes where guard holds; NaN when unknown.
std::pair<float, float> FloatRange(Value value, Value guard, uint32_t depth = 0) const {
constexpr std::pair unknown {NAN, NAN};
value = ActiveValue(value, guard);
if (value.IsImmediate() && value.GetType() == Type::F32)
return {value.F32Value(), value.F32Value()};
const auto* inst = value.TryInstruction();
// Each multiply-add evaluates three operands; keep the search shallow.
if (depth > 12u || inst == nullptr) return unknown;
const auto range = [&](uint32_t arg) {
return FloatRange(inst->Arg(arg), guard, depth + 1u);
};
switch (inst->GetOpcode()) {
case ValueOpcode::BitCastF32U32:
case ValueOpcode::BitCastU32F32: return range(0);
// Clamping maps NaN to zero.
case ValueOpcode::FPSaturate32: return {0.0f, 1.0f};
case ValueOpcode::FPFloor32: {
const auto [low, high] = range(0);
return {std::floor(low), std::floor(high)};
}
case ValueOpcode::FPMad32: {
// The rounded multiply and add are monotonic, so the bounds lie at the corners.
const auto a = range(0), b = range(1), c = range(2);
if (!std::isfinite(a.first) || !std::isfinite(a.second) ||
!std::isfinite(b.first) || !std::isfinite(b.second)) return unknown;
const auto [low, high] = std::minmax({a.first * b.first, a.first * b.second,
a.second * b.first, a.second * b.second});
const std::pair bounds {low + c.first, high + c.second};
if (!std::isfinite(bounds.first) || !std::isfinite(bounds.second)) return unknown;
return bounds;
}
default: return unknown;
}
}
uint32_t SelectorMaximum(Value value, SelectorBounds bounds = {}, uint32_t depth = 0,
Value guard = {}) const {
if (depth > 32u) return UINT32_MAX;
@@ -3445,9 +3691,13 @@ private:
switch (inst->GetOpcode()) {
case ValueOpcode::ReadFirstLane:
return SelectedSelectorDomain(*inst, guard).maximum;
case ValueOpcode::ReadLane:
// ReadLane ignores EXEC; only context-free bounds on every source lane apply.
return PossibleU32Bits(inst->Arg(0));
case ValueOpcode::ReadLane: {
// ReadLane ignores EXEC; only context-free bounds on every source lane apply,
// unless guard proves a waterfall key equal to the executing lanes' local value.
const auto local = EqualLocalKey(guard, value);
return local.IsEmpty() ? PossibleU32Bits(inst->Arg(0))
: SelectorMaximum(local, bounds, depth + 1u, guard);
}
case ValueOpcode::Phi: {
const auto initial = InitialCandidateMask(value);
if (!initial.IsEmpty()) return SelectorMaximum(initial, bounds, depth + 1u, guard);
@@ -3474,6 +3724,18 @@ private:
NonzeroOnEntry(inst->Arg(0), inst->Parent()) ? 31u : UINT32_MAX;
case ValueOpcode::UMin32:
case ValueOpcode::BitwiseAnd32: return std::min(maximum(0), maximum(1));
case ValueOpcode::SMin32: {
// Signed and unsigned minimums agree on nonnegative operands.
const auto left = maximum(0), right = maximum(1);
if (std::max(left, right) <= INT32_MAX) return std::min(left, right);
break;
}
case ValueOpcode::ConvertS32F32: {
// The conversion truncates toward zero.
const auto [low, high] = FloatRange(inst->Arg(0), guard);
if (low >= 0.0f && high < 0x1p31f) return static_cast<uint32_t>(high);
break;
}
case ValueOpcode::SelectU32:
if (!guard.IsEmpty() && Implies(guard, inst->Arg(0))) return maximum(1);
return SelectorPredicateFalse(inst->Arg(0), bounds, depth + 1u, guard)
@@ -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;
@@ -82,6 +82,7 @@ bool IsRuntimeUniformOp(ValueOpcode op) {
case ValueOpcode::BitCastU32F32:
case ValueOpcode::BitCastF32U32:
case ValueOpcode::ConvertU32F32:
case ValueOpcode::ConvertS32F32:
case ValueOpcode::ConvertF32U32:
case ValueOpcode::CompositeConstructU64:
case ValueOpcode::CompositeExtractU64:
@@ -147,8 +148,8 @@ bool ImmediateBits(Value value, uint64_t& bits) {
}
// Scratch layout in bytes: per descriptor source its eight DWORDs and a known DWORD, one
// activity byte per source, then one qword per walked value (all ones when unknown). The
// walker's value slots, captured addresses, reachability and states follow.
// activity byte per source, then per image, then one qword per walked value (all ones when
// unknown). The walker's value slots, captured addresses, reachability and states follow.
constexpr uint32_t DescriptorStride = 9u * sizeof(uint32_t);
uint32_t ActiveOffset(const ResourcePlan& plan) {
@@ -156,7 +157,8 @@ uint32_t ActiveOffset(const ResourcePlan& plan) {
}
uint32_t OutputOffset(const ResourcePlan& plan) {
return (ActiveOffset(plan) + static_cast<uint32_t>(plan.descriptor_sources.size()) + 7u) & ~7u;
const auto active = plan.descriptor_sources.size() + plan.info.images.size();
return (ActiveOffset(plan) + static_cast<uint32_t>(active) + 7u) & ~7u;
}
const uint8_t* Scratch(const ResourcePlan& plan) {
@@ -1038,6 +1040,23 @@ void WalkerCompiler::EmitStrict(const Node& node) {
c.L(done);
break;
}
case ValueOpcode::ConvertS32F32: {
// The conversion saturates and maps NaN to zero. cvttss2si's out-of-range result,
// INT32_MIN, is already right for negative overflow.
Xbyak::Label done;
c.movd(c.xmm0, c.eax);
c.xor_(c.eax, c.eax);
c.ucomiss(c.xmm0, c.xmm0);
c.jp(done, Near);
c.mov(c.ecx, 0x4f000000u);
c.movd(c.xmm1, c.ecx);
c.mov(c.eax, INT32_MAX);
c.ucomiss(c.xmm0, c.xmm1);
c.jae(done, Near);
c.cvttss2si(c.eax, c.xmm0);
c.L(done);
break;
}
case ValueOpcode::FPMul32:
c.movd(c.xmm0, c.eax);
c.movd(c.xmm1, c.ecx);
@@ -1272,11 +1291,14 @@ void WalkerCompiler::Compile(std::span<const Value> values) {
for (const auto slot: blocks[block].srt_reads) flat(slot);
c.L(skip);
}
// A source no block lists is always active.
for (uint32_t source = 0; source < sources.size(); ++source) {
// A source or image no block lists is always active.
for (uint32_t index = 0; index < sources.size() + m_plan.info.images.size(); ++index) {
const bool image = index >= sources.size();
const uint32_t id = image ? index - static_cast<uint32_t>(sources.size()) : index;
bool listed = false;
for (uint32_t block = 0; block < blocks.size(); ++block) {
if (std::ranges::find(blocks[block].sources, source) == blocks[block].sources.end()) continue;
const auto& list = image ? blocks[block].images : blocks[block].sources;
if (std::ranges::find(list, id) == list.end()) continue;
if (listed) {
c.or_(c.al, c.byte[c.rdx + m_reach + block]);
} else {
@@ -1284,7 +1306,7 @@ void WalkerCompiler::Compile(std::span<const Value> values) {
}
listed = true;
}
const auto active = c.byte[c.rdx + ActiveOffset(m_plan) + source];
const auto active = c.byte[c.rdx + ActiveOffset(m_plan) + index];
if (listed) {
c.mov(active, c.al);
} else {
@@ -1391,6 +1413,10 @@ bool WalkedActive(const ResourcePlan& plan, uint32_t source) {
return Scratch(plan)[ActiveOffset(plan) + source] != 0u;
}
bool WalkedImageActive(const ResourcePlan& plan, uint32_t image) {
return Scratch(plan)[ActiveOffset(plan) + plan.descriptor_sources.size() + image] != 0u;
}
bool ValidateRuntimeValue(const ResourcePlan& program, Value value, RuntimeValueType type) {
return RuntimeValidator(program, type).Run(value);
}
@@ -78,6 +78,7 @@ bool WalkSrt(const ResourcePlan& plan, const SrtRuntime& runtime, std::vector<ui
bool WalkedValue(const ResourcePlan& plan, Value value, uint32_t& result);
bool WalkedDescriptor(const ResourcePlan& plan, uint32_t source, DescriptorValue& result);
bool WalkedActive(const ResourcePlan& plan, uint32_t source);
bool WalkedImageActive(const ResourcePlan& plan, uint32_t image);
// Length of the guest load at rip if it belongs to the walker running on this thread, else 0.
// The exception handler completes such a load as a zero read.
uint32_t SrtWalkerLoadLength(uint64_t rip);
@@ -348,14 +348,20 @@ void VisitInstruction(Pass& pass, Block* block, Inst& inst) {
} // namespace
void RewriteToSsa(const BlockList& blocks) {
Pass pass;
Pass pass;
std::unordered_map<Block*, size_t> filled_predecessors;
for (auto* block: blocks) {
if (block->ImmPredecessors().empty()) {
pass.Seal(block);
}
for (auto& inst: *block) {
VisitInstruction(pass, block, inst);
}
}
for (auto* block: blocks) {
pass.Seal(block);
for (auto* successor: block->ImmSuccessors()) {
if (++filled_predecessors[successor] == successor->ImmPredecessors().size()) {
pass.Seal(successor);
}
}
}
}
+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;
+24 -1
View File
@@ -67,7 +67,7 @@ constexpr uint64_t PAGE_TABLE_POOL_SIZE = 4ull * 1024ull * 1024ull * 1024ull;
constexpr uint64_t PAGE_TABLE_GRANULARITY = 2ull * 1024ull * 1024ull;
constexpr int PAGE_TABLE_POOL_ENTRIES =
static_cast<int>(PAGE_TABLE_POOL_SIZE / PAGE_TABLE_GRANULARITY);
constexpr uint64_t DEFAULT_FLEXIBLE_MEMORY_SIZE = 1ull * 1024ull * 1024ull * 1024ull;
constexpr uint64_t DEFAULT_FLEXIBLE_MEMORY_SIZE = 448ull * 1024ull * 1024ull;
static uint64_t g_flexible_memory_size = DEFAULT_FLEXIBLE_MEMORY_SIZE;
static bool g_flexible_memory_size_frozen = false;
@@ -738,6 +738,8 @@ public:
static uint64_t Size() { return g_flexible_memory_size; }
uint64_t Available();
void Charge(uint64_t size);
void Release(uint64_t size);
bool Map(uint64_t vaddr, size_t len, int prot, VirtualMemory::Mode mode, GpuAccessMode gpu_mode,
const char* name);
@@ -1967,6 +1969,19 @@ uint64_t FlexibleMemory::Available() {
return (Size() >= m_allocated_total ? Size() - m_allocated_total : 0);
}
void FlexibleMemory::Charge(uint64_t size) {
Common::LockGuard lock(m_mutex);
m_allocated_total += size;
}
void FlexibleMemory::Release(uint64_t size) {
Common::LockGuard lock(m_mutex);
EXIT_IF(size > m_allocated_total);
m_allocated_total -= size;
}
void PooledMemory::AddFreeUnlocked(uint64_t start, uint64_t size) {
if (size == 0) {
return;
@@ -3770,6 +3785,14 @@ uint64_t AllocateProgramMemory(uint64_t search_addr, uint64_t size, VirtualMemor
return AllocateGuestRuntimeMemory(search_addr, size, mode, name, VirtualRangeType::Code, false);
}
void ChargeFlexibleMemory(uint64_t size) {
g_flexible_memory->Charge(size);
}
void ReleaseFlexibleMemory(uint64_t size) {
g_flexible_memory->Release(size);
}
void SetProgramMemoryProtection(uint64_t vaddr, uint64_t size, VirtualMemory::Mode mode) {
std::lock_guard<std::recursive_mutex> memory_operation_lock(g_memory_operation_mutex);
+3
View File
@@ -197,6 +197,9 @@ int KYTY_SYSV_ABI KernelMemoryPoolGetBlockStats(KernelMemoryPoolBlockStats* outp
uint64_t AllocateProgramMemory(uint64_t search_addr, uint64_t size,
Common::VirtualMemory::Mode mode, const char* name);
// Loaded modules' segments use flexible memory without taking its backing.
void ChargeFlexibleMemory(uint64_t size);
void ReleaseFlexibleMemory(uint64_t size);
void SetProgramMemoryProtection(uint64_t vaddr, uint64_t size, Common::VirtualMemory::Mode mode);
uint64_t AllocateRuntimeMemory(uint64_t search_addr, uint64_t size,
Common::VirtualMemory::Mode mode, const char* name,
+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[]);
+42 -2
View File
@@ -2480,6 +2480,9 @@ static constexpr uint32_t AGC_DRAW_INDIRECT_MULTI_END_TAG = 0xc6000000u;
static constexpr uint32_t AGC_WAIT_USER_DATA_BEGIN_32_TAG = 0xc8010000u;
static constexpr uint32_t AGC_WAIT_USER_DATA_BEGIN_64_TAG = 0xc8020000u;
static constexpr uint32_t AGC_WAIT_USER_DATA_END_TAG = 0xc8000000u;
static constexpr uint32_t AGC_MEM_SEMAPHORE_SIGNAL_BEGIN_TAG = 0xc9010000u;
static constexpr uint32_t AGC_MEM_SEMAPHORE_WAIT_BEGIN_TAG = 0xc9020000u;
static constexpr uint32_t AGC_MEM_SEMAPHORE_END_TAG = 0xc9000000u;
static constexpr uint32_t AGC_WAIT_USER_DATA_BEGIN_DW = 4u;
static constexpr uint32_t AGC_INTERNAL_DATA_PACKET_SIZE_DW = 3u;
@@ -3077,6 +3080,42 @@ uint32_t* KYTY_SYSV_ABI AgcAcbWaitRegMem(CommandBuffer* buf, uint8_t size, uint8
poll_cycles);
}
uint32_t* KYTY_SYSV_ABI AgcAcbMemSemaphore(CommandBuffer* buf, const volatile void* address,
uint8_t mailbox, uint8_t signal_type, uint8_t op) {
EXIT_NOT_IMPLEMENTED(op != 6u && op != 7u);
auto* cmd =
buf->AllocateDW(AGC_WAIT_USER_DATA_BEGIN_DW + AGC_INTERNAL_DATA_PACKET_SIZE_DW + 4u);
if (cmd == nullptr) {
return nullptr;
}
const auto address_value = reinterpret_cast<uint64_t>(address);
const bool signal = (op == 6u);
cmd[0] = KYTY_PM4(AGC_WAIT_USER_DATA_BEGIN_DW, Pm4::IT_SET_UCONFIG_REG, 1u);
cmd[1] = AGC_INTERNAL_DATA_REGISTER;
cmd[2] = (signal ? AGC_MEM_SEMAPHORE_SIGNAL_BEGIN_TAG : AGC_MEM_SEMAPHORE_WAIT_BEGIN_TAG) |
static_cast<uint32_t>((address_value >> 32u) & 0xffffu);
cmd[3] = static_cast<uint32_t>(address_value);
// A signal pads with a NOP header where a wait ends with its end marker.
auto* semaphore = cmd + AGC_WAIT_USER_DATA_BEGIN_DW;
if (signal) {
semaphore[0] = KYTY_PM4(AGC_INTERNAL_DATA_PACKET_SIZE_DW, Pm4::IT_NOP, 0u);
semaphore += AGC_INTERNAL_DATA_PACKET_SIZE_DW;
}
semaphore[0] = KYTY_PM4(4, Pm4::IT_MEM_SEMAPHORE, 0u);
semaphore[1] = static_cast<uint32_t>(address_value) & ~0x7u;
semaphore[2] = static_cast<uint32_t>(address_value >> 32u);
semaphore[3] = (static_cast<uint32_t>(op) << 29u) | ((signal_type & 0x1u) << 20u) |
((mailbox & 0x1u) << 16u);
if (!signal) {
write_agc_internal_data_packet(semaphore + 4, AGC_MEM_SEMAPHORE_END_TAG);
}
return cmd;
}
uint64_t KYTY_SYSV_ABI AgcAcbWaitOnAddressGetSize(uint8_t size) {
PRINT_NAME();
@@ -3840,11 +3879,12 @@ bool AgcIsInternalDataPacket(uint32_t cmd_id, const uint32_t* payload) {
const auto size_dw = KYTY_PM4_LEN(cmd_id);
if (size_dw == AGC_WAIT_USER_DATA_BEGIN_DW) {
const auto tag = payload[1] & 0xffff0000u;
return tag == AGC_WAIT_USER_DATA_BEGIN_32_TAG || tag == AGC_WAIT_USER_DATA_BEGIN_64_TAG;
return tag == AGC_WAIT_USER_DATA_BEGIN_32_TAG || tag == AGC_WAIT_USER_DATA_BEGIN_64_TAG ||
tag == AGC_MEM_SEMAPHORE_SIGNAL_BEGIN_TAG || tag == AGC_MEM_SEMAPHORE_WAIT_BEGIN_TAG;
}
return size_dw == AGC_INTERNAL_DATA_PACKET_SIZE_DW &&
(payload[1] == AGC_WAIT_USER_DATA_END_TAG ||
(payload[1] == AGC_WAIT_USER_DATA_END_TAG || payload[1] == AGC_MEM_SEMAPHORE_END_TAG ||
payload[1] == AGC_DRAW_INDIRECT_MULTI_BEGIN_TAG ||
payload[1] == AGC_DRAW_INDIRECT_MULTI_END_TAG);
}
+2
View File
@@ -216,6 +216,8 @@ uint32_t* KYTY_SYSV_ABI AgcAcbWaitRegMem(CommandBuffer* buf, uint8_t size, uint8
uint8_t cache_policy, const volatile void* address,
uint64_t reference, uint64_t mask, uint32_t poll_cycles);
uint64_t KYTY_SYSV_ABI AgcAcbWaitOnAddressGetSize(uint8_t size);
uint32_t* KYTY_SYSV_ABI AgcAcbMemSemaphore(CommandBuffer* buf, const volatile void* address,
uint8_t mailbox, uint8_t signal_type, uint8_t op);
uint32_t* KYTY_SYSV_ABI AgcAcbDmaData(CommandBuffer* buf, uint8_t dst, uint8_t dst_cache_policy,
uint64_t dst_address_or_offset, uint8_t src,
uint8_t src_cache_policy,
+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
@@ -78,6 +78,7 @@ LIB_DEFINE(InitAgcDriver_1) {
LIB_FUNC("b-oySn+G2tE", Gen5::AgcAcbJumpGetSize);
LIB_FUNC("DwICrVxerkY", Gen5::AgcAcbRewind);
LIB_FUNC("htn36gPnBk4", Gen5::AgcAcbWaitRegMem);
LIB_FUNC("q4VuU-QsLOE", Gen5::AgcAcbMemSemaphore);
LIB_FUNC("idlaArvdXEs", Gen5::AgcAcbWaitOnAddressGetSize);
LIB_FUNC("-RnpfpxIhec", Gen5::AgcAcbDmaData);
LIB_FUNC("qzMN2XKGA4k", Gen5::AgcAcbCopyData);
+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);
+4
View File
@@ -21,6 +21,9 @@ namespace Libs {
namespace LibNpEntitlementAccess {
void InitNet_1_NpEntitlementAccess(Loader::SymbolDatabase* s);
}
namespace LibNpEntitlementAccessPft {
void InitNet_1_NpEntitlementAccessPft(Loader::SymbolDatabase* s);
}
namespace LibNpTrophy2 {
void InitNet_1_NpTrophy2(Loader::SymbolDatabase* s);
}
@@ -2846,6 +2849,7 @@ LIB_DEFINE(InitNet_1) {
LibNpManager::InitNet_1_NpManager(s);
LibNpSessionSignaling::InitNet_1_NpSessionSignaling(s);
LibNpEntitlementAccess::InitNet_1_NpEntitlementAccess(s);
LibNpEntitlementAccessPft::InitNet_1_NpEntitlementAccessPft(s);
LibNpAuth::InitNet_1_NpAuth(s);
LibNpTrophy2::InitNet_1_NpTrophy2(s);
LibNpUniversalDataSystem::InitNet_1_NpUniversalDataSystem(s);
+22
View File
@@ -513,4 +513,26 @@ LIB_DEFINE(InitNet_1_NpEntitlementAccess) {
} // namespace LibNpEntitlementAccess
namespace LibNpEntitlementAccessPft {
LIB_VERSION("NpEntitlementAccessPft", 1, "NpEntitlementAccess", 1, 1);
// Unknown function; return state 2 for the FC27 investigation.
static int KYTY_SYSV_ABI NpEntitlementAccessPftUnknowneDXKe9FndlE(uint32_t* state) {
PRINT_NAME();
if (state == nullptr) {
return LibNpEntitlementAccess::NP_ENTITLEMENT_ACCESS_ERROR_PARAMETER;
}
*state = 2;
return 0;
}
LIB_DEFINE(InitNet_1_NpEntitlementAccessPft) {
LIB_FUNC("eDXKe9FndlE", NpEntitlementAccessPftUnknowneDXKe9FndlE);
}
} // namespace LibNpEntitlementAccessPft
} // namespace Libs
+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);
+20 -2
View File
@@ -1636,6 +1636,8 @@ void RuntimeLinker::DeleteTls(Program* program, int thread_id) {
}
}
constexpr uint64_t GUEST_PAGE_SIZE = 0x4000;
static uint64_t CalcBaseSize(const Elf64_Ehdr* ehdr, const Elf64_Phdr* phdr) {
uint64_t base_size = 0;
for (Elf64_Half i = 0; i < ehdr->e_phnum; i++) {
@@ -1649,6 +1651,20 @@ static uint64_t CalcBaseSize(const Elf64_Ehdr* ehdr, const Elf64_Phdr* phdr) {
return base_size;
}
// A module's E, R and RW LOAD segments use flexible memory in 16 KiB pages; GNU_RELRO adds none
// since it lies inside an RW LOAD.
uint64_t CalcFlexibleMemorySize(const Elf64_Phdr* phdr, Elf64_Half phnum) {
uint64_t size = 0;
for (Elf64_Half i = 0; i < phnum; i++) {
const auto flags = phdr[i].p_flags;
if (phdr[i].p_type == PT_LOAD &&
(flags == PF_X || flags == PF_R || flags == (PF_R | PF_W))) {
size += AlignUp(phdr[i].p_memsz, GUEST_PAGE_SIZE);
}
}
return size;
}
// NOLINTNEXTLINE(readability-function-cognitive-complexity)
void RuntimeLinker::LoadProgramToMemory(Program* program) {
KYTY_PROFILER_FUNCTION();
@@ -1668,8 +1684,7 @@ void RuntimeLinker::LoadProgramToMemory(Program* program) {
EXIT_IF(phdr == nullptr || ehdr == nullptr);
program->base_size = CalcBaseSize(ehdr, phdr);
constexpr uint64_t GUEST_PAGE_SIZE = 0x4000;
program->base_size = CalcBaseSize(ehdr, phdr);
EXIT_IF(program->base_size > UINT64_MAX - (GUEST_PAGE_SIZE - 1));
program->base_size_aligned = AlignUp(program->base_size, GUEST_PAGE_SIZE);
@@ -1694,6 +1709,8 @@ void RuntimeLinker::LoadProgramToMemory(Program* program) {
g_desired_base_addr, program->mapped_size, Common::VirtualMemory::Mode::ExecuteReadWrite,
Common::PathToString(program->file_name.filename()).c_str());
EXIT_IF(program->base_vaddr == 0);
program->flexible_size = CalcFlexibleMemorySize(phdr, ehdr->e_phnum);
Libs::LibKernel::Memory::ChargeFlexibleMemory(program->flexible_size);
if (patch_guest_instructions) {
const auto trampoline_addr = program->base_vaddr + program->base_size_aligned;
@@ -1879,6 +1896,7 @@ void RuntimeLinker::DeleteProgram(Program* p) {
}
EXIT_IF(
!Libs::LibKernel::Memory::FreeGuestMemory(program->base_vaddr, program->mapped_size));
Libs::LibKernel::Memory::ReleaseFlexibleMemory(program->flexible_size);
}
}
+4
View File
@@ -21,6 +21,7 @@ struct EntryParams {
};
class Elf64;
struct Elf64_Phdr;
struct Elf64_Sym;
struct Elf64_Rela;
class RuntimeLinker;
@@ -128,6 +129,7 @@ struct Program {
uint64_t base_size = 0;
uint64_t base_size_aligned = 0;
uint64_t mapped_size = 0;
uint64_t flexible_size = 0;
uint64_t instruction_trampoline_size = 0;
std::unique_ptr<SymbolDatabase> export_symbols;
std::unique_ptr<SymbolDatabase> import_symbols;
@@ -203,6 +205,8 @@ private:
application_heap_posix_memalign_func_t m_application_heap_posix_memalign = nullptr;
};
uint64_t CalcFlexibleMemorySize(const Elf64_Phdr* phdr, uint16_t phnum);
#if defined(KYTY_VIRTUAL_MEMORY_ALLOCATION_TESTS)
bool TestMainEntryUsesGuestStack();
bool TestModuleRelocationUsesWritableHostMapping();
+4 -1
View File
@@ -65,6 +65,8 @@ else()
SET(CMAKE_NINJA_FORCE_RESPONSE_FILE 1 CACHE INTERNAL "")
endif()
# Guest code runs natively and needs x86-64-v3 (AVX2, BMI2, FMA), so every host has it.
# No FMA contraction: a * b + c keeps both roundings.
if(KYTY_CLANG_CL)
if(CMAKE_CXX_FLAGS MATCHES "/W[0-4]")
string(REGEX REPLACE "/W[0-4]" "/W3" CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS}")
@@ -79,7 +81,7 @@ if(KYTY_CLANG_CL)
string(REGEX REPLACE "/MD" "/MT" CMAKE_C_FLAGS_RELEASE "${CMAKE_C_FLAGS_RELEASE}")
string(REGEX REPLACE "/MD" "/MT" CMAKE_C_FLAGS "${CMAKE_C_FLAGS}")
set(KYTY_CPP_FLAGS "${KYTY_CPP_FLAGS} /utf-8 /Oy- /wd4244 /wd4305 /wd4800 /wd4345")
set(KYTY_CPP_FLAGS "${KYTY_CPP_FLAGS} /utf-8 /Oy- /clang:-march=x86-64-v3 /clang:-ffp-contract=off /wd4244 /wd4305 /wd4800 /wd4345")
if(KYTY_WARNINGS_ARE_ERRORS)
#set(KYTY_CPP_FLAGS "${KYTY_CPP_FLAGS} /WX")
@@ -103,6 +105,7 @@ elseif(CLANG OR GCC)
else()
set(KYTY_CPP_FLAGS "${KYTY_CPP_FLAGS} -fno-exceptions -fdiagnostics-color=always -finput-charset=UTF-8 -fexec-charset=UTF-8 -static-libgcc -static-libstdc++ -g -fno-strict-aliasing -fno-omit-frame-pointer -Wall -Wno-unused-value -fmessage-length=0")
endif()
set(KYTY_CPP_FLAGS "${KYTY_CPP_FLAGS} -march=x86-64-v3 -ffp-contract=off")
if(KYTY_WARNINGS_ARE_ERRORS)
#set(KYTY_CPP_FLAGS "${KYTY_CPP_FLAGS} -Werror")
+17
View File
@@ -0,0 +1,17 @@
#include <cstdio>
#if !defined(__AVX2__) || !defined(__BMI2__) || !defined(__FMA__)
#error "x86-64 builds target x86-64-v3"
#endif
int main() {
// (1 + 2^-23)^2 = 1 + 2^-22 + 2^-46; only a fused multiply-add keeps the 2^-46.
volatile float a = 1.0f + 0x1p-23f;
const float result = a * a - (1.0f + 0x1p-22f);
if (result != 0.0f) {
std::fprintf(stderr, "BuildFlagsTests: failed: a * a - b was contracted (%a)\n", result);
return 1;
}
std::puts("BuildFlagsTests: all cases passed");
return 0;
}
+7
View File
@@ -3,6 +3,8 @@
#include <cstdint>
#include <cstdio>
#include <cstdlib>
#include <iterator>
#include <type_traits>
#include <utility>
#include <vector>
@@ -13,6 +15,11 @@ using Table = Libs::Graphics::MultiLevelPageTable<Owners, 20, 44, 14>;
using PageOwners = Libs::Graphics::InlinePageOwnerList<uint32_t, 16>;
using OwnerTable = Libs::Graphics::MultiLevelPageTable<PageOwners, 20, 44, 14>;
// MSVC's STL and libc++ don't need these traits for std::any_of, but libstdc++ does, so only a
// Linux build would notice them missing.
static_assert(std::is_same_v<std::iterator_traits<PageOwners::const_iterator>::iterator_category,
std::input_iterator_tag>);
void Check(bool value, const char* text) {
if (!value) {
std::fprintf(stderr, "ImagePageTableTests: failed: %s\n", text);
+57
View File
@@ -1314,6 +1314,62 @@ void TestSignedStorageSpecialization() {
}
}
// One descriptor serves a store, an atomic that a user-data condition guards, and an image no
// block lists. Unreached, the atomic reads a zero descriptor instead of the RGBA8 format the
// others use; reached, it rejects that format.
void TestUnreachedImageIgnoresSharedDescriptor(bool atomic64) {
using namespace Libs::Graphics::ShaderRecompiler::IR;
using Libs::Graphics::ShaderRecompiler::CFG::TerminatorKind;
using Libs::Graphics::Prospero::BufferFormat;
Program program;
program.stage = Libs::Graphics::ShaderType::Compute;
program.srt_plan_complete = program.resource_tracking_complete = true;
program.user_data_count = 2u;
auto &entry = AddValueBlock(program);
auto &guarded = AddValueBlock(program);
auto &done = AddValueBlock(program);
const auto user = [&](uint32_t index) {
return Value(&entry.AppendNewInst(ValueOpcode::GetUserData, {Value(ScalarReg(index))}));
};
DescriptorSource source;
source.dword_count = 8u;
source.dwords.fill(Value(0u));
source.dwords[0] = Value(0x100u);
source.dwords[1] = user(0);
source.dwords[3] = Value(uint32_t(Libs::Graphics::Prospero::ImageType::kColor2D) << 28u);
program.descriptor_sources.push_back(source);
const auto dimension = Libs::Graphics::ShaderRecompiler::Decoder::ImageDimension::Dim2D;
for (const bool atomic : {false, true, false})
program.info.images.push_back({.source = 0u, .resource_class = ImageResourceClass::Storage,
.dimension = dimension, .written = true, .atomic = atomic, .atomic64 = atomic && atomic64});
for (uint32_t image = 0; image < 2u; ++image)
program.memory_info.push_back({.kind = ResourceKind::Image, .resource = image});
entry.AppendNewInst(ValueOpcode::ImageWrite, {Value(0u), Value(0u), Value(0u), Value(true)})
.SetFlags(MemoryFlags{.index = 0});
guarded.AppendNewInst(atomic64 ? ValueOpcode::ImageAtomicUMax64 : ValueOpcode::ImageAtomicUMax32,
{Value(0u), Value(0u), Value(0u), Value(true)})
.SetFlags(MemoryFlags{.index = 1});
entry.condition = Value(&entry.AppendNewInst(ValueOpcode::INotEqual32, {user(1), Value(0u)}));
entry.terminator.kind = TerminatorKind::ConditionalBranch;
entry.terminator.true_block = &guarded;
entry.terminator.false_block = &done;
guarded.terminator.kind = TerminatorKind::Branch;
guarded.terminator.true_block = &done;
done.terminator.kind = TerminatorKind::Return;
auto plan = Extract(program);
std::array<uint32_t, 2> data{uint32_t(BufferFormat::k8_8_8_8UNorm) << 20u, 0u};
const SrtRuntime runtime{.user_data = data};
ResourceSnapshot snapshot;
ResourceSpecialization specialization;
Check(MaterializeResources(plan, runtime, snapshot, specialization) &&
snapshot.images[0].dwords[1] == data[0] && snapshot.images[2].dwords[1] == data[0] &&
snapshot.images[1] == DescriptorValue{.dword_count = 8u},
"unreached atomic image kept the shared descriptor or an unlisted image lost it");
data[1] = 1u;
Check(!MaterializeResources(plan, runtime, snapshot, specialization),
"reached atomic image accepted a format its width rejects");
}
void TestIndirectImageSamplerClasses(bool mixed) {
using namespace Libs::Graphics::ShaderRecompiler::IR;
using Libs::Graphics::Prospero::BufferFormat;
@@ -1451,6 +1507,7 @@ int main() {
TestEmptyScalarBufferSpecialization();
TestMixedSamplerVariantsShareRuntimeDescriptor();
TestSignedStorageSpecialization();
for (const bool atomic64 : {false, true}) TestUnreachedImageIgnoresSharedDescriptor(atomic64);
for (const bool mixed : {false, true}) TestIndirectImageSamplerClasses(mixed);
TestShaderCacheRecords();
std::puts("ResourceMaterializationTests: all cases passed");
+150 -53
View File
@@ -1282,15 +1282,18 @@ void TestIndexedPointerImageTable() {
{image, sampler, fixture->ImageAddress(), parent}, fixture->AddMemory(image_memory, 0x8bcu));
return fixture;
};
const auto nested_geometry = [](const auto& plan, uint32_t count) {
const auto& indirect = *plan.descriptor_sources[plan.info.images[0].source].indirect_descriptor;
return indirect.table_maximum == 1u && indirect.pointer_table &&
indirect.pointer_table->indexed && indirect.pointer_table->count == count &&
indirect.pointer_table->stride == 368u && indirect.pointer_table->offset == 64u;
};
auto fixture = make();
fixture->PlanAndTrack();
const auto plan = ExtractResourcePlan(fixture->program);
Check(plan.info.images.size() == 1u && plan.info.buffers.size() == 1u,
"indexed nested table expanded IR candidates or discarded the GPU pointer load");
const auto& indirect = *plan.descriptor_sources[plan.info.images[0].source].indirect_descriptor;
Check(indirect.table_maximum == 1u && indirect.pointer_table &&
indirect.pointer_table->indexed && indirect.pointer_table->count == 3u &&
indirect.pointer_table->stride == 368u && indirect.pointer_table->offset == 64u,
Check(nested_geometry(plan, 3u),
"nested table did not preserve separate parent and child geometry");
constexpr uint64_t high = uint64_t{1} << 32u;
constexpr uint32_t table = 0x2000u, objects = 0x10000u, row_bytes = 1536u;
@@ -1389,8 +1392,13 @@ void TestIndexedPointerImageTable() {
"explicit null pointer bypassed the specialization reader");
auto reversed = make(Case::ReverseSigned);
reversed->PlanAndTrack();
// An inclusive child limit bounds one more child record, never the exclusive count.
auto inclusive = make(Case::InclusiveChild);
inclusive->PlanAndTrack();
Check(nested_geometry(ExtractResourcePlan(inclusive->program), 4u),
"an inclusive child limit did not bound one more child record");
for (const auto variant : {Case::MissingParent, Case::WrongParent, Case::MissingChild,
Case::InclusiveChild, Case::MismatchedPair, Case::DifferentLaneMask,
Case::MismatchedPair, Case::DifferentLaneMask,
Case::ExtraUse, Case::UnboundedChoice, Case::MissingOuterWitness, Case::WrongOuterWitness,
Case::CyclicChoice, Case::ExcessiveDomain}) {
auto invalid = make(variant);
@@ -2497,11 +2505,15 @@ void TestDescendingImageRange() {
enum class Variant {
Valid, WrongStep, IncrementBypass, MissingNonzero, NegativeInitial,
Bitmap, BitmapAddsBit, BitmapWrongWord, BitmapUnknownInitial,
BitmapByteOffset, BitmapScalarOffset, BitmapOuterClobber, BitmapWrongLoadMask
BitmapByteOffset, BitmapScalarOffset, BitmapOuterClobber, BitmapWrongLoadMask,
Bitmap8, Bitmap8MisplacedLoad, Bitmap8OtherBuffer, Bitmap8OuterClobber, Bitmap8WrongUpperWord,
Bitmap8NarrowMask, Bitmap8OtherGuard
};
const auto make_plan = [](Variant variant) {
Fixture fixture;
const bool bitmap = variant >= Variant::Bitmap;
// Eight-word records are two four-word loads of one buffer record.
const uint32_t bitmap_words = variant >= Variant::Bitmap8 ? 8u : 4u;
auto *entry = fixture.block;
auto *header = fixture.AddBlock();
auto *guard = fixture.AddBlock();
@@ -2524,11 +2536,11 @@ void TestDescendingImageRange() {
branch(latch, header);
auto &induction = header->AppendNewInst(ValueOpcode::Phi);
induction.SetFlags(Type::U32);
std::array<Inst *, 4> outer_words {}, remaining_words {};
std::array<Value, 4> loaded_words {};
std::array<Inst *, 8> outer_words {}, remaining_words {};
std::array<Value, 8> loaded_words {};
if (bitmap)
for (auto& word : outer_words)
word = &header->AppendNewInst(ValueOpcode::Phi, {}, uint64_t(Type::U32));
for (uint32_t word = 0; word < bitmap_words; ++word)
outer_words[word] = &header->AppendNewInst(ValueOpcode::Phi, {}, uint64_t(Type::U32));
const auto bound = fixture.Emit(ValueOpcode::SGreaterThanEqual32,
{Value(&induction), Value(0u)}, 0, header);
choose(header, guard, variant == Variant::IncrementBypass ? latch : exit, bound);
@@ -2545,14 +2557,27 @@ void TestDescendingImageRange() {
read.idxen = true;
read.offen = variant == Variant::BitmapByteOffset;
read.data_dwords = 4u;
const auto packed = fixture.Emit(ValueOpcode::LoadBufferU32x4,
{buffer, lane,
Value(variant == Variant::BitmapByteOffset ? 4u : 0u),
Value(variant == Variant::BitmapScalarOffset ? 4u : 0u), load_active},
fixture.AddMemory(read, 0x10));
for (uint32_t word = 0; word < 4u; ++word) {
std::array<Value, 2> packed;
for (uint32_t part = 0; part < bitmap_words / 4u; ++part) {
read.offset = part * (variant == Variant::Bitmap8MisplacedLoad ? 32u : 16u);
const auto source = variant == Variant::Bitmap8OtherBuffer && part != 0u
? fixture.Buffer({fixture.UserData(8), fixture.UserData(9), fixture.UserData(10),
fixture.UserData(11)}) : buffer;
const auto guard = variant == Variant::Bitmap8OtherGuard && part != 0u
? fixture.Emit(ValueOpcode::LogicalAnd,
{load_active, fixture.Emit(ValueOpcode::ULessThan32, {lane, Value(16u)})})
: load_active;
packed[part] = fixture.Emit(ValueOpcode::LoadBufferU32x4,
{source, lane,
Value(variant == Variant::BitmapByteOffset ? 4u : 0u),
Value(variant == Variant::BitmapScalarOffset ? 4u : 0u), guard},
fixture.AddMemory(read, 0x10 + part * 8u));
}
for (uint32_t word = 0; word < bitmap_words; ++word) {
const bool wrong_word = variant == Variant::BitmapWrongWord ||
(variant == Variant::Bitmap8WrongUpperWord && word >= 4u);
auto component = fixture.Emit(ValueOpcode::CompositeExtractU32x4,
{packed, Value(variant == Variant::BitmapWrongWord ? (word + 1u) % 4u : word)}, 0, entry);
{packed[word / 4u], Value((wrong_word ? word + 1u : word) % 4u)}, 0, entry);
component = fixture.Emit(ValueOpcode::SelectU32,
{load_active, component, fixture.Emit(ValueOpcode::UndefU32)});
loaded_words[word] = component;
@@ -2565,7 +2590,7 @@ void TestDescendingImageRange() {
remaining_words[word]->AddPhiOperand(header, Value(outer_words[word]));
}
auto selected = fixture.Emit(ValueOpcode::UndefU32, {}, 0, guard);
for (uint32_t word = 0; word < 4u; ++word) {
for (uint32_t word = 0; word < bitmap_words; ++word) {
selected = fixture.Emit(ValueOpcode::SelectU32,
{fixture.Emit(ValueOpcode::IEqual32, {Value(&induction), Value(word)}, 0, guard),
Value(remaining_words[word]), selected}, 0, guard);
@@ -2628,32 +2653,39 @@ void TestDescendingImageRange() {
{fixture.Emit(ValueOpcode::ShiftLeftLogical32, {Value(1u), bit})})});
if (variant == Variant::BitmapAddsBit)
next = fixture.Emit(ValueOpcode::BitwiseOr32, {next, Value(1u << 21u)});
for (uint32_t word = 0; word < 4u; ++word) {
for (uint32_t word = 0; word < bitmap_words; ++word) {
// Relative register writes index with M0 & 0xff; a narrower mask aliases upper words.
const auto index = bitmap_words == 4u ? Value(&induction)
: fixture.Emit(ValueOpcode::BitwiseAnd32,
{Value(&induction), Value(variant == Variant::Bitmap8NarrowMask ? 3u : 255u)});
const auto selected_word = fixture.Emit(ValueOpcode::LogicalAnd,
{active, fixture.Emit(ValueOpcode::IEqual32, {Value(&induction), Value(word)})});
{active, fixture.Emit(ValueOpcode::IEqual32, {index, Value(word)})});
remaining_words[word]->AddPhiOperand(body, fixture.Emit(ValueOpcode::SelectU32,
{selected_word, next, Value(remaining_words[word])}));
auto carried = Value(remaining_words[word]);
if (variant == Variant::BitmapOuterClobber)
if (variant == Variant::BitmapOuterClobber || variant == Variant::Bitmap8OuterClobber)
carried = fixture.Emit(ValueOpcode::SelectU32,
{fixture.Emit(ValueOpcode::IEqual32,
{Value(&induction), Value(word + 1u)}, 0, latch),
loaded_words[(word + 1u) % 4u], carried}, 0, latch);
loaded_words[(word + 1u) % bitmap_words], carried}, 0, latch);
outer_words[word]->AddPhiOperand(latch, carried);
}
}
const auto step = fixture.Emit(ValueOpcode::IAdd32,
{Value(&induction), Value(variant == Variant::WrongStep ? UINT32_MAX - 1u : UINT32_MAX)}, 0, latch);
induction.AddPhiOperand(entry, Value(variant == Variant::NegativeInitial ? UINT32_MAX : 3u));
induction.AddPhiOperand(entry,
Value(variant == Variant::NegativeInitial ? UINT32_MAX : bitmap_words - 1u));
induction.AddPhiOperand(latch, step);
fixture.PlanAndTrack();
Check(ordinal.Instruction()->Arg(1).Resolve() == bit.Resolve(),
"descending range retained impossible relative-register stores");
const auto &indirect = fixture.program.descriptor_sources[
fixture.program.info.images[0].source].indirect_descriptor;
Check(indirect && indirect->selector.has_value() == (variant == Variant::Bitmap) &&
(!indirect->selector || indirect->selector->bitset) &&
indirect->key_count == Value(128u) &&
const bool selects = variant == Variant::Bitmap || variant == Variant::Bitmap8;
Check(indirect && indirect->selector.has_value() == selects &&
(!indirect->selector || (indirect->selector->bitset &&
indirect->selector->stride == bitmap_words * 4u)) &&
indirect->key_count == Value(bitmap_words * 32u) &&
!indirect->selector_first.IsEmpty(),
"descending bitscan range lost its bound or accepted an unproved bitmap subset");
return ExtractResourcePlan(fixture.program);
@@ -2693,10 +2725,14 @@ void TestDescendingImageRange() {
}
auto bitmap_plan = make_plan(Variant::Bitmap);
auto wide_plan = make_plan(Variant::Bitmap8);
for (const auto variant : {Variant::BitmapAddsBit, Variant::BitmapWrongWord,
Variant::BitmapUnknownInitial, Variant::BitmapByteOffset,
Variant::BitmapScalarOffset, Variant::BitmapOuterClobber,
Variant::BitmapWrongLoadMask})
Variant::BitmapWrongLoadMask, Variant::Bitmap8MisplacedLoad,
Variant::Bitmap8OtherBuffer, Variant::Bitmap8OuterClobber,
Variant::Bitmap8WrongUpperWord, Variant::Bitmap8NarrowMask,
Variant::Bitmap8OtherGuard})
make_plan(variant);
// Every descriptor is distinct, so retained keys are visible in the snapshot as well as
// its lookup table. An unrelated table slot must never be read, even when it is malformed.
@@ -2711,23 +2747,25 @@ void TestDescendingImageRange() {
const SrtRuntime bitmap_runtime {.user_data = bitmap_data, .userdata = &memory,
.read_specialization_memory = ReadLinearTestMemory};
memory.watched_address = bitmap_address;
auto *checked_plan = &bitmap_plan;
const auto check_bitmap = [&](std::initializer_list<uint32_t> ranks, uint32_t count) {
bitmap_data[2] = count;
memory.watched_reads = memory.descriptor_reads = 0u;
std::vector<uint32_t> keys;
for (const auto rank : ranks) keys.push_back(count - rank);
std::ranges::sort(keys);
Check(MaterializeResources(bitmap_plan, bitmap_runtime, snapshot, specialization) &&
const auto dwords = bitmap_data[6] * (bitmap_data[5] >> 18u);
Check(MaterializeResources(*checked_plan, bitmap_runtime, snapshot, specialization) &&
memory.watched_reads == uint32_t(bitmap_data[6] != 0u) &&
(bitmap_data[6] == 0u || memory.watched_dwords ==
std::min(bitmap_data[6] * 4u, MaxIndirectDescriptorProbes)) &&
std::min(dwords, MaxIndirectDescriptorProbes)) &&
memory.descriptor_reads == keys.size() &&
snapshot.images.size() == std::max<size_t>(keys.size(), 1u),
"bitmap selector lost bounded reads or retained unreachable descriptors");
for (uint32_t first = 0; first < bitmap_data[6] * 4u; first += MaxIndirectDescriptorProbes)
for (uint32_t first = 0; first < dwords; first += MaxIndirectDescriptorProbes)
Check(std::ranges::find(snapshot.specialization_reads,
std::pair{uint64_t{bitmap_address} + first * 4u,
uint64_t{std::min(bitmap_data[6] * 4u - first, MaxIndirectDescriptorProbes)} * 4u}) !=
uint64_t{std::min(dwords - first, MaxIndirectDescriptorProbes)} * 4u}) !=
snapshot.specialization_reads.end(),
"bitmap chunk was omitted from same-dispatch write-alias protection");
if (keys.empty()) {
@@ -2779,8 +2817,8 @@ void TestDescendingImageRange() {
memory.fail_address = UINT64_MAX;
const auto valid_data = bitmap_data;
for (const auto [word, value] : std::array<std::pair<uint32_t, uint32_t>, 6>{{
{4u, bitmap_address + 1u}, {5u, 8u << 16u}, {5u, (16u << 16u) | (1u << 31u)},
for (const auto [word, value] : std::array<std::pair<uint32_t, uint32_t>, 7>{{
{4u, bitmap_address + 1u}, {5u, 8u << 16u}, {5u, 32u << 16u}, {5u, (16u << 16u) | (1u << 31u)},
{7u, 0x5204u | (1u << 23u)}, {7u, 0x5204u | (1u << 28u)},
{7u, 0x5204u | (1u << 30u)}}}) {
bitmap_data[word] = value;
@@ -2820,6 +2858,23 @@ void TestDescendingImageRange() {
return read.first >= memory.fail_address;
}),
"a complete bitmap domain read redundant chunks or lost candidate keys");
// Eight-word records keep their upper words' ranks, and a key below the table is never read.
checked_plan = &wide_plan;
bitmap_data = valid_data;
bitmap_data[5] = 32u << 16u;
bitmap_data[6] = 2u;
std::fill_n(memory.words.begin() + bitmap_word, 16u, 0u);
memory.words[bitmap_word] = 1u | (1u << 2u);
memory.words[bitmap_word + 12u] = 1u << 1u;
memory.fail_address = 0x2000u - 32u;
check_bitmap({0u, 2u, 129u}, 2u);
memory.fail_address = UINT64_MAX;
for (const auto stride : {16u, 64u}) {
bitmap_data[5] = stride << 16u;
Check(!MaterializeResources(wide_plan, bitmap_runtime, snapshot, specialization),
"an eight-word bitmap selector accepted a different record stride");
}
}
void TestUniformizedMaterialImageKeys() {
@@ -4804,7 +4859,8 @@ void TestSharedAppendSelectorBounds() {
Valid, PayloadOob, MissingInit, InactiveInit, UnknownInit, DuplicateAtomic,
WrongAtomicGuard, UnguardedCandidate, WrongStep, WrongLocalIndex, Multiwave,
OuterReentry, InnerAppendCycle, EarlyCounterRead, WrongConsumerBound, ExtraWriter, CounterAlias,
PayloadWrap, CounterOob
PayloadWrap, CounterOob, Lane, LaneUnimpliedRead, LaneUnimpliedCounter, LaneCounterMask,
LaneOtherBypass, LaneInactiveSelect
};
const auto make_plan = [](Variant variant, uint32_t first_base = 1032u, uint32_t consumed_base = 8u) {
Fixture fixture;
@@ -4837,6 +4893,7 @@ void TestSharedAppendSelectorBounds() {
auto *entry = fixture.block;
auto *initialize = fixture.AddBlock();
auto *initialized = fixture.AddBlock();
const bool lane = variant >= Variant::Lane;
const auto material = fixture.Buffer({fixture.UserData(0), fixture.UserData(1),
fixture.UserData(2), fixture.UserData(3)});
const auto table = fixture.Address(fixture.UserData(4), fixture.UserData(5));
@@ -4855,12 +4912,18 @@ void TestSharedAppendSelectorBounds() {
{Value(static_cast<uint32_t>(StageInputKind::LocalInvocationId)), Value(1u)});
const auto local = fixture.Emit(ValueOpcode::IAdd32,
{x, fixture.Emit(ValueOpcode::ShiftLeftLogical32, {y, Value(3u)})});
auto zero_lane = fixture.Emit(ValueOpcode::IEqual32, {local, Value(0u)});
// The negated form skips the initializer only when every lane is nonzero.
const auto local_is = [&](uint32_t value) {
return fixture.Emit(ValueOpcode::LogicalNot,
{fixture.Emit(ValueOpcode::INotEqual32, {local, Value(value)})});
};
auto zero_lane = lane ? local_is(0u) : fixture.Emit(ValueOpcode::IEqual32, {local, Value(0u)});
if (variant == Variant::InactiveInit) zero_lane = Value(false);
if (variant == Variant::UnknownInit)
zero_lane = fixture.Emit(ValueOpcode::LogicalAnd,
{zero_lane, fixture.Emit(ValueOpcode::IEqual32, {fixture.UserData(8), Value(0u)})});
choose(entry, initialize, initialized, zero_lane, CFG::BranchCondition::ExecNonZero);
choose(entry, initialize, initialized, variant == Variant::LaneOtherBypass ? local_is(1u) : zero_lane,
CFG::BranchCondition::ExecNonZero);
if (variant != Variant::MissingInit)
fixture.Emit(ValueOpcode::WriteSharedU32x2,
{Value(0u), Value(0u), Value(0u), zero_lane}, shared(0u), initialize);
@@ -4929,13 +4992,25 @@ void TestSharedAppendSelectorBounds() {
if (variant == Variant::ExtraWriter)
fixture.Emit(ValueOpcode::WriteSharedU32,
{Value(0u), Value(UINT32_MAX), Value(true)}, shared(12u), previous);
if (final_counter.IsEmpty())
final_counter = fixture.Emit(ValueOpcode::LoadSharedU32,
{Value(0u), Value(true)}, shared(0u), previous);
// Lane variants consume under a lane mask, which their counter and record reads use.
Value lanes(true), other;
if (lane) {
lanes = fixture.Emit(ValueOpcode::ULessThan32, {y, fixture.UserData(8)}, 0, previous);
other = fixture.Emit(ValueOpcode::ULessThan32, {x, fixture.UserData(8)}, 0, previous);
}
auto *guarded = lane ? fixture.AddBlock() : previous;
auto *header = fixture.AddBlock();
auto *body = fixture.AddBlock();
auto *latch = fixture.AddBlock();
auto *exit = fixture.AddBlock();
if (lane) choose(previous, guarded, exit, lanes, CFG::BranchCondition::ExecNonZero);
previous = guarded;
if (final_counter.IsEmpty())
final_counter = fixture.Emit(ValueOpcode::LoadSharedU32,
{Value(0u), variant == Variant::LaneUnimpliedCounter ? other : lanes}, shared(0u), previous);
if (lane)
final_counter = fixture.Emit(ValueOpcode::ReadFirstLane,
{final_counter, variant == Variant::LaneCounterMask ? other : lanes}, 0, previous);
branch(previous, header);
auto &index = header->AppendNewInst(ValueOpcode::Phi);
index.SetFlags(Type::U32);
@@ -4944,12 +5019,22 @@ void TestSharedAppendSelectorBounds() {
choose(header, body, exit, in_list, CFG::BranchCondition::VccNonZero);
fixture.block = body;
const auto selected = fixture.Emit(ValueOpcode::LoadSharedU32,
{fixture.Emit(ValueOpcode::ShiftLeftLogical32, {Value(&index), Value(2u)}), Value(true)},
shared(consumed_base));
const auto offset32 = fixture.Emit(ValueOpcode::ShiftLeftLogical32, {selected, Value(5u)});
const auto offset = fixture.Emit(ValueOpcode::ReadFirstLane,
{fixture.Emit(ValueOpcode::IAdd32,
{fixture.Emit(ValueOpcode::ShiftLeftLogical32, {offset32, Value(2u)}), offset32}), Value(true)});
{fixture.Emit(ValueOpcode::ShiftLeftLogical32, {Value(&index), Value(2u)}),
variant == Variant::LaneUnimpliedRead ? other : lanes}, shared(consumed_base));
Value offset;
if (lane) {
// The record is selected from one lane and scaled on the scalar side.
const auto mask = variant == Variant::LaneInactiveSelect
? fixture.Emit(ValueOpcode::LogicalAnd, {lanes, other}) : lanes;
offset = fixture.Emit(ValueOpcode::IMul32,
{fixture.Emit(ValueOpcode::ReadFirstLane, {selected, mask}), Value(160u)});
} else {
const auto offset32 = fixture.Emit(ValueOpcode::ShiftLeftLogical32, {selected, Value(5u)});
offset = fixture.Emit(ValueOpcode::ReadFirstLane,
{fixture.Emit(ValueOpcode::IAdd32,
{fixture.Emit(ValueOpcode::ShiftLeftLogical32, {offset32, Value(2u)}), offset32}),
Value(true)});
}
MemoryInfo scalar;
scalar.kind = ResourceKind::ScalarBuffer;
scalar.offset = 144u;
@@ -4978,7 +5063,8 @@ void TestSharedAppendSelectorBounds() {
fixture.PlanAndTrack({.compute = &compute});
const auto &indirect = fixture.program.descriptor_sources[
fixture.program.info.images[0].source].indirect_descriptor;
const bool proven = variant == Variant::Valid || variant == Variant::PayloadOob;
const bool proven = variant == Variant::Valid || variant == Variant::PayloadOob ||
variant == Variant::Lane;
Check(indirect && indirect->selector && indirect->selector->offset == 144u &&
indirect->selector->stride == (proven ? 160u : 32u) &&
indirect->key_count.IsEmpty() != proven &&
@@ -4995,6 +5081,11 @@ void TestSharedAppendSelectorBounds() {
Variant::InnerAppendCycle, Variant::EarlyCounterRead, Variant::WrongConsumerBound, Variant::ExtraWriter,
Variant::CounterAlias, Variant::PayloadWrap, Variant::CounterOob})
make_plan(variant);
auto lane_plan = make_plan(Variant::Lane);
for (const auto variant : {Variant::LaneUnimpliedRead, Variant::LaneUnimpliedCounter,
Variant::LaneCounterMask, Variant::LaneOtherBypass, Variant::LaneInactiveSelect})
CheckFatal([&] { make_plan(variant); }, "not a valid runtime value",
"shared append provenance accepted an unimplied lane mask or initializer bypass");
LinearTestMemory memory;
for (uint32_t index = 0; index < 8u; ++index) memory.words[(144u + 160u * index) / 4u] = index + 1u;
@@ -5007,7 +5098,7 @@ void TestSharedAppendSelectorBounds() {
memory.words[word + 3u] = Libs::Graphics::DstSel(4, 5, 6, 7) |
(static_cast<uint32_t>(Libs::Graphics::Prospero::ImageType::kColor2D) << 28u);
}
std::array<uint32_t, 9> user_data {0x1000u, 160u << 16u, 8u, 0u, 0x2000u, 0u, 0x1800u, 0u, 0u};
std::array<uint32_t, 9> user_data {0x1000u, 160u << 16u, 8u, 0u, 0x2000u, 0u, 0x1800u, 0u, 8u};
SrtRuntime runtime {.user_data = user_data, .userdata = &memory,
.read_specialization_memory = ReadLinearTestMemory};
FakeGuestMemory fake(ReadRuntimeMemory, &runtime);
@@ -5022,10 +5113,11 @@ void TestSharedAppendSelectorBounds() {
memory.words[0x800u / 4u] = counts[0];
memory.words[0x804u / 4u] = counts[1];
const auto expected = counts[1];
Check(MaterializeResources(plan, runtime, snapshot, specialization) &&
snapshot.images.size() == std::max(expected, 1u) &&
(expected == 0u || snapshot.images.back().dwords[0] == 0x100u + expected),
"disjoint shared append lists or refreshed counts selected unrelated material keys");
for (const auto *tested : {&plan, &lane_plan})
Check(MaterializeResources(*tested, runtime, snapshot, specialization) &&
snapshot.images.size() == std::max(expected, 1u) &&
(expected == 0u || snapshot.images.back().dwords[0] == 0x100u + expected),
"disjoint shared append lists or refreshed counts selected unrelated material keys");
}
memory.words[0x800u / 4u] = 24u;
memory.words[0x804u / 4u] = 2u;
@@ -5047,9 +5139,14 @@ void TestSharedAppendSelectorBounds() {
Check(MaterializeResources(plan, runtime, snapshot, specialization) &&
snapshot.images.size() == 2u && snapshot.images.back().dwords[0] == 0x104u,
"shared append materialization reused stale selector contents");
// Record 1 lies past a one-record V#, so its scalar key reads zero instead of key 4.
user_data[2] = 1u;
Check(!MaterializeResources(plan, runtime, snapshot, specialization),
"shared append materialization accepted a range outside its V# bounds");
memory.fail_address = 0x1000u + 144u + 160u;
Check(MaterializeResources(plan, runtime, snapshot, specialization) &&
snapshot.images.size() == 2u && snapshot.images[0].dwords[0] == 0x100u &&
snapshot.images[1].dwords[0] == 0x101u,
"shared append materialization did not read records past its V# as zero");
memory.fail_address = UINT64_MAX;
user_data[2] = 8u;
for (const auto too_large : {65537u, UINT32_MAX}) {
for (uint32_t list = 0; list < 2u; ++list) {
+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();
+111 -41
View File
@@ -300,8 +300,12 @@ void TestOperandMutationLifecycle() {
auto *source = definition.TryInstruction();
const auto inserted = fixture.Emit(ValueOpcode::BitFieldInsert,
{definition, definition, Value(4u), Value(4u)});
Check(source->UseCount() == 2, "inline operands lost distinct reverse-use indices");
inserted.TryInstruction()->SetArg(1, Value(3u));
auto *consumer = inserted.TryInstruction();
Check(source->Uses() == std::vector<Use>({{consumer, 0}, {consumer, 1}}),
"inline operands lost distinct reverse-use indices");
consumer->SetArg(0, definition);
Check(source->UseCount() == 2, "re-setting an operand to its value duplicated its use");
consumer->SetArg(1, Value(3u));
Check(source->UseCount() == 1, "inline operand mutation did not detach its old use");
source->ReplaceUsesWith(Value(0xaau));
uint32_t result = 0;
@@ -349,6 +353,35 @@ void TestOperandMutationLifecycle() {
"Phi replacement retained incoming uses or changed the consumer value");
}
void TestReplacementKeepsUseOrder() {
Fixture fixture(2);
const auto base = fixture.Emit(ValueOpcode::IAdd32, {Value(1u), Value(2u)});
auto *before = fixture.Emit(ValueOpcode::IAdd32, {base, Value(3u)}).TryInstruction();
auto *identity = fixture.Emit(ValueOpcode::Identity, {base}).TryInstruction();
auto *pair = fixture.Emit(ValueOpcode::IAdd32, {Value(identity), Value(identity)}).TryInstruction();
auto *after = fixture.Emit(ValueOpcode::IAdd32, {Value(4u), base}).TryInstruction();
auto &large = fixture.program.value_storage.emplace_back(ValueOpcode::GetImageResource);
const auto last = large.NumArgs() - 1;
large.SetArg(last, Value(identity));
identity->ReplaceUsesWith(base, false);
Check(!identity->HasUses() && identity->NumArgs() == 0 && pair->Arg(0) == base &&
pair->Arg(1) == base && large.Arg(last) == base &&
base.TryInstruction()->Uses() ==
std::vector<Use>({{before, 0}, {after, 1}, {pair, 0}, {pair, 1}, {&large, last}}),
"identity removal lost or reordered the uses of its argument");
auto &phi = fixture.BlockAt(1).AppendNewInst(ValueOpcode::Phi);
phi.SetFlags(Type::U32);
phi.AddPhiOperand(&fixture.BlockAt(0), Value(after));
phi.AddPhiOperand(&fixture.BlockAt(1), Value(&phi));
auto *user = fixture.Emit(ValueOpcode::IAdd32, {Value(&phi), Value(&phi)}, 0, 1).TryInstruction();
phi.ReplaceUsesWith(Value(after));
Check(phi.GetOpcode() == ValueOpcode::Identity && phi.Arg(0) == Value(after) && !phi.HasUses() &&
user->Arg(0) == Value(after) && user->Arg(1) == Value(after) &&
after->Uses() == std::vector<Use>({{user, 0}, {user, 1}, {&phi, 0}}),
"self-referencing Phi replacement kept a use of itself or reordered the moved uses");
}
void TestControlDependentStandaloneLoadStaysTyped() {
Fixture fixture(3);
const auto memory = fixture.AddMemory(ResourceKind::ScalarAddress);
@@ -515,6 +548,34 @@ void TestFloatComparisonDescriptorInputs() {
}
}
void TestSignedConversionDescriptorInputs() {
// 3.75 and -3.75 truncate, the largest float below 2^31 converts exactly, 2^31 and +inf
// saturate up, -2^31 and -1e10 saturate down, and -0.0, a denormal and both NaNs read zero.
constexpr std::array<std::pair<uint32_t, uint32_t>, 11> inputs = {{
{0x40700000u, 3u}, {0xc0700000u, 0xfffffffdu}, {0x4effffffu, 0x7fffff80u},
{0x4f000000u, 0x7fffffffu}, {0x7f800000u, 0x7fffffffu}, {0xcf000000u, 0x80000000u},
{0xd01502f9u, 0x80000000u}, {0x80000000u, 0u}, {0x00000001u, 0u},
{0x7fc00000u, 0u}, {0xffc00000u, 0u}}};
Fixture fixture;
const auto user = fixture.Emit(
ValueOpcode::GetUserData, {Value(static_cast<ScalarReg>(2))});
const auto converted = fixture.Emit(
ValueOpcode::ConvertS32F32, {fixture.Emit(ValueOpcode::BitCastF32U32, {user})});
Check(ValidateRuntimeValue(fixture.program, converted),
"signed float conversion was rejected as a runtime value");
fixture.program.descriptor_sources.push_back(
{.dwords = {converted}, .dword_count = 1});
auto plan = ExtractResourcePlan(fixture.program);
for (const auto [bits, expected] : inputs) {
const std::array user_data{bits};
DescriptorValue result;
Check(EvaluateSource(plan, {.user_data = user_data}, 0, result) &&
result.dwords[0] == expected,
"descriptor signed conversion disagrees with the native conversion");
}
}
void TestSharedIntegerRuntimeDependencies() {
Fixture fixture;
const auto lane = fixture.Emit(ValueOpcode::LaneId);
@@ -628,42 +689,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 +739,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 +749,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 +763,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 +783,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 +803,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");
}
}
}
@@ -877,14 +945,16 @@ int main() {
TestCarryAndBitFields();
TestInvariantAndDivergentPhi();
TestOperandMutationLifecycle();
TestReplacementKeepsUseOrder();
TestControlDependentStandaloneLoadStaysTyped();
TestRuntime64BitDescriptorOps();
TestUniformFirstLaneSamplerLod();
TestFloatComparisonDescriptorInputs();
TestSignedConversionDescriptorInputs();
TestSharedIntegerRuntimeDependencies();
TestConstantBufferBounds();
TestReadLaneElimination();
TestCompleteUMinReduction();
TestCompleteWaveReduction();
TestOptimizationPipeline();
TestControlFlowValueSurvivesReadLaneFolding();
TestDeadPhiCyclesAndPlanningRoots();
File diff suppressed because it is too large. Load diff
+31 -11
View File
@@ -6,12 +6,16 @@
#include <xbyak/xbyak.h>
#include <csignal>
#include <cstddef>
#include <cstdlib>
#include <iterator>
#include <utility>
#include <vector>
#if defined(__linux__)
#include <ucontext.h>
#elif defined(__APPLE__)
#include <sys/ucontext.h>
#elif defined(_WIN32)
#include <windows.h>
#endif
@@ -23,18 +27,20 @@ public:
explicit FakeGuestMemory(Libs::Graphics::ShaderRecompiler::IR::SrtMemoryReader reader = nullptr,
void *userdata = nullptr) {
Current() = {reader, userdata};
#if defined(__linux__)
#if defined(__linux__) || defined(__APPLE__)
struct sigaction action {};
action.sa_sigaction = Handle;
action.sa_flags = SA_SIGINFO;
sigaction(SIGSEGV, &action, &m_previous);
for (size_t index = 0; index < std::size(kSignals); ++index)
sigaction(kSignals[index], &action, &m_previous[index]);
#elif defined(_WIN32)
m_handler = AddVectoredExceptionHandler(1, Handle);
#endif
}
~FakeGuestMemory() {
#if defined(__linux__)
sigaction(SIGSEGV, &m_previous, nullptr);
#if defined(__linux__) || defined(__APPLE__)
for (size_t index = 0; index < std::size(kSignals); ++index)
sigaction(kSignals[index], &m_previous[index], nullptr);
#elif defined(_WIN32)
RemoveVectoredExceptionHandler(m_handler);
#endif
@@ -54,16 +60,30 @@ private:
const auto [reader, userdata] = Current();
return reader != nullptr && reader(userdata, address, {&word, 1}) ? word : 0u;
}
#if defined(__linux__)
#if defined(__linux__) || defined(__APPLE__)
#if defined(__APPLE__)
// macOS raises SIGBUS for some protection faults and SIGSEGV for others.
static constexpr int kSignals[] = {SIGSEGV, SIGBUS};
#else
static constexpr int kSignals[] = {SIGSEGV};
#endif
static void Handle(int, siginfo_t *, void *context) {
auto &registers = static_cast<ucontext_t *>(context)->uc_mcontext.gregs;
const auto length =
Libs::Graphics::ShaderRecompiler::IR::SrtWalkerLoadLength(registers[REG_RIP]);
auto *native = static_cast<ucontext_t *>(context);
#if defined(__APPLE__)
auto &rip = native->uc_mcontext->__ss.__rip;
auto &rax = native->uc_mcontext->__ss.__rax;
const auto r10 = native->uc_mcontext->__ss.__r10;
#else
auto &rip = native->uc_mcontext.gregs[REG_RIP];
auto &rax = native->uc_mcontext.gregs[REG_RAX];
const auto r10 = native->uc_mcontext.gregs[REG_R10];
#endif
const auto length = Libs::Graphics::ShaderRecompiler::IR::SrtWalkerLoadLength(rip);
if (length == 0) std::abort();
registers[REG_RAX] = Read(registers[REG_R10]);
registers[REG_RIP] += length;
rax = Read(r10);
rip += length;
}
struct sigaction m_previous {};
struct sigaction m_previous[std::size(kSignals)] {};
#elif defined(_WIN32)
static LONG CALLBACK Handle(EXCEPTION_POINTERS *info) {
auto &context = *info->ContextRecord;
+33
View File
@@ -7,6 +7,7 @@
#include "kernel/memory.h"
#include "kernel/pthread.h"
#include "libs/errno.h"
#include "loader/elf.h"
#include "loader/guestInstructionPatcher.h"
#include "loader/jit.h"
#include "loader/runtimeLinker.h"
@@ -88,6 +89,7 @@ using Libs::LibKernel::Memory::VirtualQueryInfo;
// Prospero ABI?
constexpr uint64_t SceKernelPageSize = 0x4000;
constexpr uint64_t SceKernelTotalPhysicalSize = 13824ull * 1024ull * 1024ull;
constexpr uint64_t SceKernelFlexibleMemorySize = 448ull * 1024ull * 1024ull;
constexpr uint64_t TestFlexibleMemorySize = 3072ull * 1024ull * 1024ull;
constexpr int SceKernelProtCpuRead = 0x01;
constexpr int SceKernelProtCpuRw = 0x02;
@@ -188,6 +190,10 @@ void InitSubsystems() {
"failed to remove temporary param.json");
Check("InitSubsystems", flexible_memory_size == TestFlexibleMemorySize,
"failed to read flexible memory size from param.json");
size_t default_flexible_memory_size = 0;
Libs::LibKernel::Memory::KernelConfiguredFlexibleMemorySize(&default_flexible_memory_size);
Check("InitSubsystems", default_flexible_memory_size == SceKernelFlexibleMemorySize,
"flexible memory does not default to 448 MiB without param.json");
Libs::LibKernel::Memory::SetFlexibleMemorySize(flexible_memory_size);
subsystems.Initialize<Libs::LibKernel::Memory::Lifecycle>();
@@ -3650,6 +3656,32 @@ void TestModuleRelocationUsesWritableHostMapping() {
std::printf("[host] %-48s ok\n", test);
}
void TestModuleSegmentsUseFlexibleMemory() {
const char* test = "ModuleSegmentsUseFlexibleMemory";
using namespace Loader;
// E, R and RW LOAD pages; GNU_RELRO over the RW page and the flagless LOAD add none.
const std::array<Elf64_Phdr, 6> phdrs {{
{PT_LOAD, PF_X, 0x4000, 0x0000, 0x0000, 0x1ac, 0x1ac, 0x4000},
{PT_LOAD, PF_R, 0x8000, 0x4000, 0x4000, 0x83, 0x83, 0x4000},
{PT_LOAD, PF_R | PF_W, 0xc000, 0x8000, 0x8000, 0x338, 0x338, 0x4000},
{0x6474e552, PF_R, 0xc000, 0x8000, 0x8000, 0x338, 0x4000, 0x1},
{PT_LOAD, PF_R | PF_W, 0x10000, 0xc000, 0xc000, 0x21, 0x21, 0x4000},
{PT_LOAD, 0, 0x10030, 0xc030, 0xc030, 0x550, 0x550, 0x4000},
}};
const auto size = CalcFlexibleMemorySize(phdrs.data(), phdrs.size());
Check(test, size == 4 * SceKernelPageSize, "module segments counted wrong");
const auto baseline = AvailableFlexibleMemory(test);
Libs::LibKernel::Memory::ChargeFlexibleMemory(size);
Check(test, AvailableFlexibleMemory(test) == baseline - size,
"loaded module did not use flexible memory");
Libs::LibKernel::Memory::ReleaseFlexibleMemory(size);
Check(test, AvailableFlexibleMemory(test) == baseline,
"unloaded module did not return flexible memory");
std::printf("[host] %-48s ok\n", test);
}
#if defined(__linux__) || KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS
volatile sig_atomic_t g_instruction_traps = 0;
@@ -5054,6 +5086,7 @@ int main(int argc, char** argv) {
RunTest(TestMemoryPoolCommitDecommitQueryFlags);
RunTest(TestProgramMemoryAllocationAndProtection);
RunTest(TestModuleRelocationUsesWritableHostMapping);
RunTest(TestModuleSegmentsUseFlexibleMemory);
#if defined(__linux__) || KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS
RunTest(TestTlsStubsPreserveFlags);
#endif
+441 -32
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);
@@ -1976,18 +2001,21 @@ void TestNggVertexEntryState() {
void TestNativeDataCacheInvalidation() {
using namespace ShaderRecompiler;
// Captured f5edf70dbb1f6057:0x194 and 0x1a4, each followed by S_WAITCNT.
// Captured f5edf70dbb1f6057:0x194 and 0x1a4, each followed by S_WAITCNT. The S_DCACHE_INV
// after them emits no fence.
const std::array cache_ops = {0xe1c40000u, 0u, 0xbf8c0000u,
0xe1c80000u, 0u, 0xbf8c0000u, EncodeSopp(0x01)};
0xe1c80000u, 0u, 0xbf8c0000u,
0xf4800000u, 0xfa000000u, EncodeSopp(0x01)};
Decoder::Program decoded;
Decoder::DecodeProgram(cache_ops, decoded);
for (const uint32_t index : {0u, 2u}) {
for (const auto [index, opcode] : {std::pair{0u, Decoder::Opcode::BUFFER_GL0_INV},
std::pair{2u, Decoder::Opcode::BUFFER_GL1_INV},
std::pair{4u, Decoder::Opcode::S_DCACHE_INV}}) {
const auto &inst = decoded.instructions[index];
Check(inst.opcode == (index == 0 ? Decoder::Opcode::BUFFER_GL0_INV
: Decoder::Opcode::BUFFER_GL1_INV) &&
Check(inst.opcode == opcode &&
inst.word_count == 2 && inst.src_count == 0 && inst.data_dwords == 0 &&
inst.dst.kind == Decoder::OperandKind::Unknown,
"RDNA2 cache invalidation acquired a buffer operand or lost its encoding");
"RDNA2 cache invalidation acquired an operand or lost its encoding");
}
auto options = MakeCompileOptions(ShaderType::Compute);
@@ -4690,6 +4718,73 @@ void TestImageAtomicWidthDecoder() {
}
}
void TestImageGatherDecoder() {
using namespace ShaderRecompiler::Decoder;
struct Gather {
uint32_t opcode;
const char *name;
uint32_t address_vgprs;
};
// Every PS5 image_gather4 encoding, with the vaddr VGPR count of its 2D full-float form.
constexpr Gather gathers[] = {
{0x40, "image_gather4", 2}, {0x41, "image_gather4_cl", 3},
{0x44, "image_gather4_l", 3}, {0x45, "image_gather4_b", 3},
{0x46, "image_gather4_b_cl", 4}, {0x47, "image_gather4_lz", 2},
{0x48, "image_gather4_c", 3}, {0x49, "image_gather4_c_cl", 4},
{0x4c, "image_gather4_c_l", 4}, {0x4d, "image_gather4_c_b", 4},
{0x4e, "image_gather4_c_b_cl", 5}, {0x4f, "image_gather4_c_lz", 3},
{0x50, "image_gather4_o", 3}, {0x51, "image_gather4_cl_o", 4},
{0x54, "image_gather4_l_o", 4}, {0x55, "image_gather4_b_o", 4},
{0x56, "image_gather4_b_cl_o", 5}, {0x57, "image_gather4_lz_o", 3},
{0x58, "image_gather4_c_o", 4}, {0x59, "image_gather4_c_cl_o", 5},
{0x5c, "image_gather4_c_l_o", 5}, {0x5d, "image_gather4_c_b_o", 5},
{0x5e, "image_gather4_c_b_cl_o", 6}, {0x5f, "image_gather4_c_lz_o", 4},
{0x61, "image_gather4h", 2}, {0xc0, "image_gather4_a", 2},
{0xc1, "image_gather4_cl_a", 3}, {0xc5, "image_gather4_b_a", 3},
{0xc6, "image_gather4_b_cl_a", 4}, {0xc8, "image_gather4_c_a", 3},
{0xc9, "image_gather4_c_cl_a", 4}, {0xcd, "image_gather4_c_b_a", 4},
{0xce, "image_gather4_c_b_cl_a", 5}, {0xd0, "image_gather4_o_a", 3},
{0xd1, "image_gather4_cl_o_a", 4}, {0xd5, "image_gather4_b_o_a", 4},
{0xd6, "image_gather4_b_cl_o_a", 5}, {0xd8, "image_gather4_c_o_a", 4},
{0xd9, "image_gather4_c_cl_o_a", 5}, {0xdd, "image_gather4_c_b_o_a", 5},
{0xde, "image_gather4_c_b_cl_o_a", 6},
};
const auto expected_flags = [](std::string_view name) {
if (name == "image_gather4h") return uint32_t{ImageSampleFlagGatherHorizontal};
uint32_t flags = 0;
for (size_t at = name.find('_', 6); at != std::string_view::npos;) {
const auto end = name.find('_', at + 1);
const auto modifier = name.substr(at + 1, end - at - 1);
if (modifier == "c") flags |= ImageSampleFlagCompare;
if (modifier == "b") flags |= ImageSampleFlagBias;
if (modifier == "cl") flags |= ImageSampleFlagLodClamp;
if (modifier == "l") flags |= ImageSampleFlagLod;
if (modifier == "lz") flags |= ImageSampleFlagLevelZero;
if (modifier == "o") flags |= ImageSampleFlagOffset;
if (modifier == "a") flags |= ImageSampleFlagAdjust;
at = end;
}
return flags;
};
for (uint32_t opcode = 0; opcode < 0x100u; ++opcode) {
const auto *gather = std::ranges::find(gathers, opcode, &Gather::opcode);
const uint32_t words[] = {EncodeMimg0(opcode, 0x1), EncodeMimg1(8, 0, 1, 0)};
Instruction decoded;
DecodeInstruction(words, 0, decoded);
if (gather == std::end(gathers)) {
Check(decoded.opcode != Opcode::IMAGE_GATHER4,
"a non-gather MIMG encoding decoded as image_gather4");
continue;
}
Check(decoded.opcode == Opcode::IMAGE_GATHER4 && decoded.data_components == 4u &&
decoded.image_sample_flags == expected_flags(gather->name) &&
decoded.image_address_components == gather->address_vgprs &&
InstructionToString(decoded).find(std::string(gather->name) + " ") !=
std::string::npos,
"a PS5 gather encoding lost its opcode, name, modifiers or address count");
}
}
void TestNewShaderDecoderArchitecture() {
using namespace ShaderRecompiler::Decoder;
@@ -5632,7 +5727,7 @@ void TestImageAddressOperands() {
const uint32_t captured[] = {0xf130010au, 0x00a30013u, 0x00161a19u};
Decoder::Instruction gather;
Decoder::DecodeInstruction(captured, 0, gather);
Check(gather.opcode == Decoder::Opcode::IMAGE_GATHER4_C_L &&
Check(gather.opcode == Decoder::Opcode::IMAGE_GATHER4 &&
gather.opcode_id == 0x4cu && gather.word_count == 3u &&
gather.image_dimension == Decoder::ImageDimension::Dim2D &&
gather.dmask == 1u && gather.data_components == 4u &&
@@ -5646,7 +5741,7 @@ void TestImageAddressOperands() {
gather.image_nsa_addr[2] == 22u && !gather.glc && !gather.slc &&
!gather.image_r128,
"captured IMAGE_GATHER4_C_L lost compare/LOD, NSA, or result metadata");
Check(Decoder::InstructionToString(gather).find("IMAGE_GATHER4_C_L") != std::string::npos,
Check(Decoder::InstructionToString(gather).find("image_gather4_c_l") != std::string::npos,
"captured IMAGE_GATHER4_C_L is missing from the decoded dump");
struct Case {
uint32_t opcode;
@@ -5662,6 +5757,9 @@ void TestImageAddressOperands() {
Case{0x20, 1, true, false, 2, 1}, // packed xy
Case{0x3d, 1, true, false, 5, 4}, // offset, half bias, full dref, packed xy
Case{0x20, 2, true, true, 3, 2}, // packed xy/z in nonconsecutive VGPRs
Case{0x22, 4, true, false, 4, 3}, // a VGPR per 1D gradient, then packed x/slice
Case{0x22, 1, true, false, 6, 3}, // packed 2D gradients and xy
Case{0x22, 2, true, false, 9, 6}, // packed first four 3D gradients, a VGPR per last two
Case{0x4c, 1, false, true, 4, 4}, // full dref, xy, and LOD in NSA VGPRs
Case{0x0e, 1, false, false, 1, 1}, // resource query mip
};
@@ -7464,7 +7562,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 +7589,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 +9652,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 +10737,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 +10781,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,
@@ -10693,6 +10812,8 @@ void TestCapturedBufferAtomicsX2() {
"BufferAtomicIAdd64", "OpAtomicIAdd", ValueOpcode::BufferAtomicIAdd64},
{{0xe1680018u, 0x80000000u}, 0u, 241u, "BUFFER_ATOMIC_OR_X2",
"BufferAtomicOr64", "OpAtomicOr", ValueOpcode::BufferAtomicOr64},
{{0xe1582000u, 0x8003092au}, 9u, 237u, "BUFFER_ATOMIC_UMIN_X2",
"BufferAtomicUMin64", "OpAtomicUMin", ValueOpcode::BufferAtomicUMin64},
{{0xe1402000u, 0x80000913u}, 9u, 229u, "BUFFER_ATOMIC_SWAP_X2",
"BufferAtomicSwap64", "OpAtomicExchange", ValueOpcode::BufferAtomicSwap64},
};
@@ -10716,14 +10837,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 +11122,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 +11862,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 +11907,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");
}
}
@@ -12536,6 +12668,71 @@ void TestFinalSsaRejectsRegisterStatePseudos() {
}
#endif
void TestSsaRewriteSealsFilledBlocks() {
using namespace ShaderRecompiler::IR;
constexpr ScalarReg s0{0};
const auto add_blocks = [](Program &program) {
std::array<Block *, 4> blocks{};
for (auto &block : blocks) {
block =
program.block_storage.emplace_back(std::make_unique<Block>()).get();
program.blocks.push_back(block);
}
return blocks;
};
const auto use = [&](Block *block) {
const auto value = IREmitter(block).GetScalarReg(s0);
return &block->AppendNewInst(ValueOpcode::ReferenceU32, {value});
};
const auto is_phi = [](Value value, const Block *first, Value first_value,
const Block *second, Value second_value) {
const auto *phi = value.TryInstruction();
return phi != nullptr && phi->GetOpcode() == ValueOpcode::Phi &&
phi->NumArgs() == 2u && phi->PhiBlock(0) == first &&
phi->Arg(0) == first_value && phi->PhiBlock(1) == second &&
phi->Arg(1) == second_value;
};
{
// entry(s0 = 7) -> left(s0 = 9) | right -> join.
Program program;
const auto [entry, left, right, join] = add_blocks(program);
entry->AddBranch(left);
entry->AddBranch(right);
left->AddBranch(join);
right->AddBranch(join);
IREmitter(entry).SetScalarReg(s0, U32(Value(7u)));
IREmitter(left).SetScalarReg(s0, U32(Value(9u)));
const auto *right_use = use(right);
const auto *join_use = use(join);
RewriteToSsa(program.blocks);
Check(right_use->Arg(0) == Value(7u) && right->Instructions().size() == 2u,
"SSA read in a single-predecessor block went through a trivial Phi");
Check(is_phi(join_use->Arg(0), left, Value(9u), right, Value(7u)),
"SSA join Phi lost a predecessor value");
}
// entry(s0 = 1) -> header <-> latch(s0 += 1), header -> tail.
Program program;
const auto [entry, header, latch, tail] = add_blocks(program);
entry->AddBranch(header);
header->AddBranch(latch);
header->AddBranch(tail);
latch->AddBranch(header);
IREmitter(entry).SetScalarReg(s0, U32(Value(1u)));
const auto *header_use = use(header);
IREmitter latch_ir(latch);
const auto sum = latch_ir.IAdd(latch_ir.GetScalarReg(s0), U32(Value(1u)));
latch_ir.SetScalarReg(s0, sum);
RewriteToSsa(program.blocks);
const auto phi = header_use->Arg(0);
Check(is_phi(phi, entry, Value(1u), latch, sum),
"SSA loop header Phi lost its entry or back-edge value");
Check(sum.Instruction()->Arg(0) == phi && latch->Instructions().size() == 3u,
"SSA read after a loop header went through a trivial Phi");
}
void TestValuePhiValidation() {
using namespace ShaderRecompiler::IR;
@@ -13602,6 +13799,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 +13941,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 +13956,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 +13995,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 +14022,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 =
@@ -15052,7 +15300,7 @@ void TestBoundedScalarMaterialImageKeys() {
}
return true;
}
for (const uint32_t key : {5567u, 5582u, 5573u, 5588u}) {
for (const uint32_t key : {0u, 5567u, 5582u, 5573u, 5588u}) {
if (address != 0x100000u + key * 32u || words.size() != 8) continue;
++m.images;
std::ranges::fill(words, 0u);
@@ -15114,9 +15362,17 @@ void TestBoundedScalarMaterialImageKeys() {
Check(!MaterializeResources(plan, runtime, snapshot, specialization),
"unavailable selected material key was accepted");
}
// Row 1's 0x94 key straddles a 311-byte V#, so it reads zero instead of 5573.
memory = {.stride = 1, .records = 311};
Check(!MaterializeResources(plan, runtime, snapshot, specialization) && memory.key_reads == 0,
"scalar material byte range escaped V# bounds");
Check(MaterializeResources(plan, runtime, snapshot, specialization),
"scalar material table past its V# was rejected");
for (uint32_t root = 0; root < plan.info.images.size(); ++root) {
const auto &selector =
*plan.descriptor_sources[plan.info.images[root].source].indirect_descriptor->selector;
const auto offset = specialization.images[root].indirect_mapping_offset;
Check(snapshot.flattened_srt[offset + 1u] == (selector.offset == 0x94u ? 0u : 5567u),
"scalar material key past its V# did not read as zero");
}
memory = {.mixed = true};
Check(MaterializeResources(plan, runtime, snapshot, specialization) &&
snapshot.images.size() == 4 && specialization.images.size() == 4,
@@ -16022,6 +16278,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 +16946,6 @@ void TestNewShaderRecompilerSpirvSizeBaselines() {
.variables = 2,
.loads = 9,
.stores = 4,
.array_lengths = 2,
.phis = 5,
.labels = 34,
.selection_merges = 10,
@@ -16744,7 +17146,7 @@ int main() {
TestNewShaderRecompilerNativeWideBufferIr();
TestNewShaderRecompilerScalarB64LaneTranslation();
TestNewShaderRecompilerMubufFormatTranslation();
TestNewShaderRecompilerFormattedStoreUsesRuntimeArrayLengthOnly();
TestNewShaderRecompilerFormattedStoreUsesRuntimeBindingSizeOnly();
TestNewShaderRecompilerTypedBufferTranslation();
TestNewShaderRecompilerDsReadWrite2Translation();
TestNewShaderRecompilerDsWideAndAtomicTranslation();
@@ -16760,6 +17162,7 @@ int main() {
// here.
TestScalarAshrI64Decoder();
TestImageAtomicWidthDecoder();
TestImageGatherDecoder();
TestDsReturningAtomic64Decoder();
TestDsCompareStore32Decoder();
TestDsFloatMinMaxDecoder();
@@ -16815,6 +17218,7 @@ int main() {
TestNativeGuardedSamplerSource(false);
TestNativeGuardedSamplerSource(true);
TestNativeConstantSamplerSelection();
TestPackedZeroHighSamplerTableKey();
for (const bool scalar_key : {false, true}) TestBoundedMaterialBufferStores(scalar_key);
TestNativeDescriptorProvenanceKeepsGpuSelection();
TestCfgSiblingSharedExit();
@@ -16858,6 +17262,7 @@ int main() {
#if KYTY_PLATFORM != KYTY_PLATFORM_WINDOWS
TestFinalSsaRejectsRegisterStatePseudos();
#endif
TestSsaRewriteSealsFilledBlocks();
TestValuePhiValidation();
TestLaneMaskProjection();
TestLateConstantPropagation();
@@ -16909,6 +17314,10 @@ int main() {
TestPixelProgramCacheBindingIdentity();
TestGraphicsPushConstantPlacement();
TestCapturedBufferLoadUbyteD16Decode();
TestCapturedGlobalLoadShortD16HiDecode();
TestFlatLoadShortD16HiLlvmEncodingAnchor();
TestFlatLoadShortD16HiAddressFields();
TestFlatLoadShortD16HiCompilesAndValidates();
TestCapturedBufferStoreShortD16HiDecode();
TestCapturedBufferStoreFormatD16HiXDecode();
TestCapturedBufferLoadD16XyDecode();