mirror of
https://github.com/KytyPS5/KytyPS5.git
synced 2026-10-27 07:52:39 +00:00
Compare commits
32
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fd6b28c4b2 | ||
|
|
6cee2dc0bd | ||
|
|
a526a8b9c9 | ||
|
|
1ec69f8e7d | ||
|
|
2463e3aaf0 | ||
|
|
556538bed5 | ||
|
|
770438cee0 | ||
|
|
db12f0c5fb | ||
|
|
3f9666acbb | ||
|
|
7eb8abb279 | ||
|
|
91a5ff6cb7 | ||
|
|
95a507a8ff | ||
|
|
b2053c2861 | ||
|
|
9801fd862b | ||
|
|
78adb9293d | ||
|
|
0a72641678 | ||
|
|
99f4eba223 | ||
|
|
04bfd9070c | ||
|
|
ffa3992c1a | ||
|
|
1b70938052 | ||
|
|
914a3bb4b8 | ||
|
|
ffb6ad6cab | ||
|
|
ff5d16eefc | ||
|
|
d118afbeae | ||
|
|
a3ac6d0d32 | ||
|
|
f297c0bfe1 | ||
|
|
20bb268a48 | ||
|
|
d3768c00e9 | ||
|
|
dfcf1c0b03 | ||
|
|
de3ed905c8 | ||
|
|
0a140911ad | ||
|
|
2483573651 |
No files matched your search
@@ -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
|
||||
|
||||
@@ -98,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);
|
||||
|
||||
@@ -2060,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();
|
||||
|
||||
|
||||
@@ -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);
|
||||
@@ -966,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;
|
||||
|
||||
@@ -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_;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -1003,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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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]));
|
||||
}
|
||||
@@ -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) {
|
||||
@@ -811,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;
|
||||
@@ -846,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};
|
||||
};
|
||||
|
||||
@@ -291,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;
|
||||
|
||||
@@ -585,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:
|
||||
|
||||
@@ -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;
|
||||
@@ -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},
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
@@ -570,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));
|
||||
@@ -616,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:
|
||||
@@ -685,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:
|
||||
|
||||
@@ -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,
|
||||
@@ -708,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,
|
||||
|
||||
@@ -291,7 +291,7 @@ void Translator::V_MAD_U64_U32(const Decoder::Instruction& inst) {
|
||||
inst.dst2.kind != Decoder::OperandKind::Unknown) {
|
||||
// The 32x32 product fits in 64 bits, so only the addition can carry.
|
||||
const auto carry = IR::U1(ir.Emit(IR::ValueOpcode::ULessThan64, {result, product}));
|
||||
WriteMask(inst.dst2, carry, VectorMask(carry));
|
||||
WriteVectorMask(inst.dst2, carry);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
@@ -941,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);
|
||||
@@ -1029,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:
|
||||
@@ -1166,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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -157,12 +157,15 @@ 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, 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)
|
||||
|
||||
@@ -572,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;
|
||||
@@ -579,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;
|
||||
};
|
||||
@@ -606,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;
|
||||
@@ -634,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;
|
||||
};
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -269,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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+24
-1
@@ -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);
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
+42
-2
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
@@ -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")
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
@@ -884,10 +945,12 @@ int main() {
|
||||
TestCarryAndBitFields();
|
||||
TestInvariantAndDivergentPhi();
|
||||
TestOperandMutationLifecycle();
|
||||
TestReplacementKeepsUseOrder();
|
||||
TestControlDependentStandaloneLoadStaysTyped();
|
||||
TestRuntime64BitDescriptorOps();
|
||||
TestUniformFirstLaneSamplerLod();
|
||||
TestFloatComparisonDescriptorInputs();
|
||||
TestSignedConversionDescriptorInputs();
|
||||
TestSharedIntegerRuntimeDependencies();
|
||||
TestConstantBufferBounds();
|
||||
TestReadLaneElimination();
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
+31
-11
@@ -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 ®isters = 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;
|
||||
|
||||
@@ -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
|
||||
|
||||
+161
-11
@@ -2001,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);
|
||||
@@ -4715,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;
|
||||
|
||||
@@ -5657,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 &&
|
||||
@@ -5671,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;
|
||||
@@ -5687,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
|
||||
};
|
||||
@@ -10739,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},
|
||||
};
|
||||
@@ -12593,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;
|
||||
|
||||
@@ -15160,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);
|
||||
@@ -15222,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,
|
||||
@@ -17014,6 +17162,7 @@ int main() {
|
||||
// here.
|
||||
TestScalarAshrI64Decoder();
|
||||
TestImageAtomicWidthDecoder();
|
||||
TestImageGatherDecoder();
|
||||
TestDsReturningAtomic64Decoder();
|
||||
TestDsCompareStore32Decoder();
|
||||
TestDsFloatMinMaxDecoder();
|
||||
@@ -17113,6 +17262,7 @@ int main() {
|
||||
#if KYTY_PLATFORM != KYTY_PLATFORM_WINDOWS
|
||||
TestFinalSsaRejectsRegisterStatePseudos();
|
||||
#endif
|
||||
TestSsaRewriteSealsFilledBlocks();
|
||||
TestValuePhiValidation();
|
||||
TestLaneMaskProjection();
|
||||
TestLateConstantPropagation();
|
||||
|
||||
Reference in new issue
Block a user