Compare commits

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

Co-authored-by: 4wgycvn84p-afk <346802885+4wgycvn84p-afk@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 18:47:45 +01:00
herrMirtoandAndre Michi 99044c7c49 libs: provide NpEntitlementAccessPft state query used by FC27 (#1897)
Co-authored-by: Andre Michi <herrmirto@proton.me>
2026-10-25 18:38:26 +01:00
nmzik 83f8bfd3c9 pipeline: print a console message before the shader cache warm-up 2026-10-25 02:20:44 +02:00
nmzik dd5bc26c64 pipeline: write the shader store on a background thread 2026-10-25 02:20:44 +02:00
nmzik 72903bfce6 shader: use the V_PERMLANE16 row directly instead of copying it 2026-10-25 02:20:44 +02:00
nmzik 59d8256b36 shader: encode 11/10-bit float formatted buffer stores as truncated unsigned floats 2026-10-25 02:20:44 +02:00
nmzik 9a20c6f738 shader: disable DPP16 writes from EXEC-disabled sources without BOUND_CTRL 2026-10-25 02:20:44 +02:00
nmzik 67c89e0915 shader: keep the old V_PERMLANE16 destination for inactive sources without BOUND_CTRL 2026-10-25 02:20:44 +02:00
nmzik 5d8381e0f3 launcher: hide the unused Shader optimization option 2026-10-25 02:20:44 +02:00
TheCruZ e864cb20bd shader: clamp V_FRACT_F16/F32/F64 results below one 2026-10-25 02:20:44 +02:00
nmzik 40aaab482e launcher: add a Cache shaders option, on by default 2026-10-25 02:20:44 +02:00
nmzik ec8b75b244 pipeline: persist shaders and pipelines per game and prewarm them at boot 2026-10-25 02:20:44 +02:00
TheCruZ 9210264d47 shader: saturate V_CVT_PK_U16_U32 and V_CVT_PK_I16_I32 2026-10-25 02:20:44 +02:00
nmzik 66e21a6245 addcont: add user add-ons with --addcont and a launcher list 2026-10-25 02:20:44 +02:00
nmzik 87e49282b8 pipeline: log real shader compiles to the console 2026-10-25 02:20:44 +02:00
nmzik 1816004357 shader: serialize resource plans for the shader cache 2026-10-25 02:20:44 +02:00
nmzik bfc377627a pipeline: create pipelines from GraphicsPipelineKey and shader modules 2026-10-25 02:20:44 +02:00
nmzik 4b651f2ba7 pipeline: fold the program cache into PipelineCache with stable module ids 2026-10-25 02:20:44 +02:00
nmzik 8c01d8e849 libc: fix swapped strcmp/strncmp NIDs and bind strcpy to its own NID instead of strcpy_s's 2026-10-25 02:20:44 +02:00
nmzik e0c5ae090c kernel: send _write to descriptors 0-2 through KernelWrite instead of dropping it 2026-10-25 02:20:44 +02:00
96 changed files with 4080 additions and 1902 deletions

No files matched your search

+1 -1
View File
@@ -32,7 +32,7 @@ jobs:
windows:
name: Build KytyPS5 (Windows)
needs: metadata
runs-on: windows-2022
runs-on: windows-2025
steps:
- name: Checkout repository
+1 -1
View File
@@ -337,7 +337,7 @@ if (KYTY_CLANG_CL)
set_source_files_properties(${kyty_emulator_src} PROPERTIES COMPILE_FLAGS "-Wno-pragma-pack -Wno-deprecated-declarations -D_TIMESPEC_DEFINED")
endif()
set(kyty_emulator_link_libraries common Vulkan::Headers spirv-tools-opt spirv-tools SDL3::SDL3-static imgui xxhash FFmpeg::ffmpeg fmt::fmt nlohmann_json::nlohmann_json LibAtrac9 kyty_git_version Zydis::Zydis xbyak::xbyak opus OpenXR::openxr_loader)
set(kyty_emulator_link_libraries common Vulkan::Headers spirv-tools-opt spirv-tools SDL3::SDL3-static imgui xxhash FFmpeg::ffmpeg fmt::fmt nlohmann_json::nlohmann_json LibAtrac9 kyty_git_version Zydis::Zydis xbyak::xbyak opus OpenXR::openxr_loader libzstd_static)
# Linux system libraries.
if(LINUX)
+19
View File
@@ -109,4 +109,23 @@ std::vector<AddContentEntry> FindAddContent(const std::filesystem::path& dir,
return entries;
}
std::optional<AddContentEntry> ParseAddContent(std::string_view value) {
static const std::regex label_pattern("^[A-Z0-9]{16}$");
const auto eq = value.find('=');
const auto label = value.substr(0, eq);
if (!std::regex_match(label.begin(), label.end(), label_pattern)) {
return std::nullopt;
}
AddContentEntry entry {{}, std::string(label), {}, {}};
if (eq != std::string_view::npos) {
entry.mount_dir = PathFromUtf8(FixFilenameSlash(value.substr(eq + 1)));
std::error_code ec;
if (!std::filesystem::is_directory(entry.mount_dir, ec)) {
return std::nullopt;
}
}
return entry;
}
} // namespace Common
+6 -1
View File
@@ -2,7 +2,9 @@
#define KYTY_COMMON_ADDITIONAL_CONTENT_H_
#include <filesystem>
#include <optional>
#include <string>
#include <string_view>
#include <vector>
namespace Common {
@@ -10,7 +12,7 @@ namespace Common {
struct AddContentEntry {
std::string content_id;
std::string entitlement_label;
std::filesystem::path mount_dir;
std::filesystem::path mount_dir; // Empty for an add-on without extra data.
std::string title;
};
@@ -18,6 +20,9 @@ struct AddContentEntry {
std::vector<AddContentEntry> FindAddContent(const std::filesystem::path& dir,
const std::string& title_id);
// Parses a user add-on: "LABEL" without extra data, or "LABEL=DIR" with its data in DIR.
std::optional<AddContentEntry> ParseAddContent(std::string_view value);
} // namespace Common
#endif /* KYTY_COMMON_ADDITIONAL_CONTENT_H_ */
+4
View File
@@ -133,6 +133,10 @@ bool ShaderValidationEnabled() {
return g_config->shader_validation_enabled;
}
bool ShaderCacheEnabled() {
return g_config->shader_cache_enabled;
}
ShaderOptimizationType GetShaderOptimizationType() {
return g_config->shader_optimization_type;
}
+2
View File
@@ -72,6 +72,7 @@ struct ConfigOptions {
uint32_t console_language = DEFAULT_CONSOLE_LANGUAGE;
bool vulkan_validation_enabled = false;
bool shader_validation_enabled = false;
bool shader_cache_enabled = false;
ShaderOptimizationType shader_optimization_type = ShaderOptimizationType::None;
LogDirection shader_log_direction = LogDirection::Silent;
std::filesystem::path shader_log_folder = "_Shaders";
@@ -132,6 +133,7 @@ uint32_t GetConsoleLanguage();
bool VulkanValidationEnabled();
bool ShaderValidationEnabled();
bool ShaderCacheEnabled();
ShaderOptimizationType GetShaderOptimizationType();
LogDirection GetShaderLogDirection();
std::filesystem::path GetShaderLogFolder();
+1 -1
View File
@@ -188,7 +188,7 @@ void Run(const RunOptions& options) {
MountSandboxDirs();
Loader::AddContentLoad(options.addcont_dir, title_id);
Loader::AddContentLoad(options.addcont_dir, options.addcont, title_id);
auto* rt = Common::Singleton<Loader::RuntimeLinker>::Instance();
Libs::InitAll(rt->Symbols());
+7 -5
View File
@@ -1,6 +1,7 @@
#ifndef EMULATOR_INCLUDE_EMULATOR_EMULATOR_H_
#define EMULATOR_INCLUDE_EMULATOR_EMULATOR_H_
#include "common/additionalContent.h"
#include "common/emulatorConfig.h"
#include "common/stringUtils.h"
@@ -9,11 +10,12 @@
namespace Emulator {
struct RunOptions {
Config::ConfigOptions config;
std::filesystem::path app0_dir;
std::filesystem::path elf;
std::filesystem::path game_patch;
std::filesystem::path addcont_dir;
Config::ConfigOptions config;
std::filesystem::path app0_dir;
std::filesystem::path elf;
std::filesystem::path game_patch;
std::filesystem::path addcont_dir;
std::vector<Common::AddContentEntry> addcont;
};
void Run(const RunOptions& options);
@@ -27,6 +27,7 @@ struct IndirectDrawRegisters {
uint32_t vertex_offset;
uint32_t instance_offset;
uint32_t index_offset;
uint32_t draw_index;
};
class Pm4Execution {
@@ -1582,7 +1582,7 @@ KYTY_CP_OP_PARSER(CpOpDrawIndirect) {
// The native indexed packet enables the high-half first-index destination in word 3.
const IndirectDrawRegisters registers {
buffer[1] & 0xffffu, buffer[2] & 0xffffu,
indexed && (buffer[2] & (1u << 28u)) != 0 ? buffer[1] >> 16u : Pm4::SH_NOP};
indexed && (buffer[2] & (1u << 28u)) != 0 ? buffer[1] >> 16u : Pm4::SH_NOP, Pm4::SH_NOP};
cp.DrawIndirect(data_offset, registers, draw_initiator, indexed);
@@ -1602,10 +1602,12 @@ KYTY_CP_OP_PARSER(CpOpDrawIndirectMulti) {
const auto stride_in_bytes = buffer[7];
const auto draw_initiator = buffer[8];
const bool indexed = (cmd_id == 0xc0083800);
// Multi-draw moves the first-index enable to word 4; the destinations stay in 2/3.
// Multi-draw moves the first-index enable to word 4; the destinations stay in 2/3. Word 4
// bit 31 enables the subdraw index, written to the register in its low half.
const IndirectDrawRegisters registers {
buffer[1] & 0xffffu, buffer[2] & 0xffffu,
indexed && (buffer[3] & (1u << 28u)) != 0 ? buffer[1] >> 16u : Pm4::SH_NOP};
indexed && (buffer[3] & (1u << 28u)) != 0 ? buffer[1] >> 16u : Pm4::SH_NOP,
(buffer[3] >> 31u) != 0 ? buffer[3] & 0xffffu : Pm4::SH_NOP};
if (count_indirect == 0) {
count_addr = nullptr;
+6 -4
View File
@@ -789,7 +789,7 @@ void CommandProcessor::DrawIndexOffset(uint32_t index_offset, uint32_t index_cou
static void PatchIndirectDrawOffsets(CommandProcessor& cp, IndirectDrawRegisters registers,
uint32_t vertex_offset, uint32_t instance_offset,
uint32_t first_index) {
uint32_t first_index, uint32_t draw_index) {
const auto write = [&](uint32_t location, uint32_t value) {
if (location == Pm4::SH_NOP) {
return;
@@ -801,6 +801,7 @@ static void PatchIndirectDrawOffsets(CommandProcessor& cp, IndirectDrawRegisters
write(registers.vertex_offset, vertex_offset);
write(registers.instance_offset, instance_offset);
write(registers.index_offset, first_index);
write(registers.draw_index, draw_index);
}
void CommandProcessor::DrawIndirect(uint32_t data_offset, IndirectDrawRegisters registers,
@@ -839,7 +840,8 @@ void CommandProcessor::DrawIndirectMulti(uint32_t data_offset, uint32_t max_coun
// while they are zero.
const DrawIndirectArgs records {args_addr, draw_count, stride_in_bytes};
if (registers.vertex_offset == Pm4::SH_NOP && registers.instance_offset == Pm4::SH_NOP &&
registers.index_offset == Pm4::SH_NOP && (!indexed || m_index_buffer_size != 0) &&
registers.index_offset == Pm4::SH_NOP && registers.draw_index == Pm4::SH_NOP &&
(!indexed || m_index_buffer_size != 0) &&
(indexed ? executor.DrawIndex(m_submit_id, CurrentBuffer(),
{.index_count = m_index_buffer_size,
.index_addr = IndexAddress(0),
@@ -854,7 +856,7 @@ void CommandProcessor::DrawIndirectMulti(uint32_t data_offset, uint32_t max_coun
const auto addr = args_addr + static_cast<uint64_t>(i) * stride_in_bytes;
if (!indexed) {
const auto& args = *reinterpret_cast<const vk::DrawIndirectCommand*>(addr);
PatchIndirectDrawOffsets(*this, registers, args.firstVertex, args.firstInstance, 0);
PatchIndirectDrawOffsets(*this, registers, args.firstVertex, args.firstInstance, 0, i);
executor.DrawAuto(m_submit_id, CurrentBuffer(),
{.vertex_count = args.vertexCount,
.instance_count = args.instanceCount,
@@ -866,7 +868,7 @@ void CommandProcessor::DrawIndirectMulti(uint32_t data_offset, uint32_t max_coun
const auto& args = *reinterpret_cast<const vk::DrawIndexedIndirectCommand*>(addr);
PatchIndirectDrawOffsets(*this, registers, static_cast<uint32_t>(args.vertexOffset),
args.firstInstance, args.firstIndex);
args.firstInstance, args.firstIndex, i);
executor.DrawIndex(m_submit_id, CurrentBuffer(),
{.index_count = std::min(args.indexCount, max_indices),
.index_addr = IndexAddress(args.firstIndex),
+22 -1
View File
@@ -783,7 +783,15 @@ TextureCache::OverlapResult TextureCache::ResolveOverlap(const ImageInfo& reques
if (!cached.registered) {
return {merged_id};
}
if (BcAlias::Matches(requested, cached.info) && !cached.IsCpuDirty()) {
// PS5: image heaps keep BC images and raw block views of their layers resident together, so a
// raw read of a layer subset with the same levels views the BC image instead of evicting it.
const bool raw_view = binding == BindingType::Texture && !requested.IsBlock() &&
requested.type == cached.info.type &&
requested.data.address == cached.info.data.address &&
requested.BlockExtent() == cached.info.BlockExtent() &&
requested.resources.levels == cached.info.resources.levels &&
requested.resources.layers < cached.info.resources.layers;
if (!raw_view && BcAlias::Matches(requested, cached.info) && !cached.IsCpuDirty()) {
BcAlias::Resolve(*this, cached_id, merged_id);
return {merged_id};
}
@@ -1302,6 +1310,19 @@ ImageId TextureCache::FindImage(ImageDesc& desc, bool exact_format) {
view_layer = overlap.layer;
}
}
// PS5: image heaps hold a descriptor per dynamic-resolution extent of a render target,
// and only one extent at an address holds data, so heap lookups create no second one.
if (!result && desc.heap &&
std::any_of(candidates.begin(), candidates.end(), [&](ImageId id) {
const auto* image = m_slot_images.try_get(id);
return image != nullptr && image->registered &&
image->heap_refs.load(std::memory_order_relaxed) != 0u &&
image->info.data.address == desc.info.data.address &&
image->info.type == desc.info.type &&
image->info.extent != desc.info.extent;
})) {
return {};
}
} else if (block_alias) {
for (const auto candidate: candidates) {
const auto& owner = m_slot_images[candidate];
+1
View File
@@ -43,6 +43,7 @@ public:
ImageInfo info;
ImageViewInfo view_info;
BindingType type = BindingType::Texture;
bool heap = false; // PS5: resolved for a resident image heap.
};
TextureCache(GraphicContext& graphics, CommandScheduler& scheduler, PageManager& page_manager,
-7
View File
@@ -595,13 +595,6 @@ void LogDrawPhase(const char* draw_name, const char* phase) {
}
}
void LogPipelineTrace(const char* phase, uint64_t vertex_program_id, uint64_t pixel_program_id) {
if (graphics_debug_dump_enabled()) {
LOGF("PipelineTrace: %s VS=%" PRIu64 " PS=%" PRIu64 "\n", phase, vertex_program_id,
pixel_program_id);
}
}
static void VpPrint(const char* func, const HW::ScreenViewport& vp,
const HW::ScanModeControl& smc) {
LOGF("%s\n", func);
@@ -425,15 +425,15 @@ void Image::Resolve(Image& source, const ImageSubresourceRange& source_range,
const auto destination_height =
std::max(backing.extent.height >> destination_range.base_level, 1u);
const bool copy = source.backing.samples == 1;
EXIT_IF(layers == 0 || info.extent.width > source_width || info.extent.height > source_height ||
info.extent.width > destination_width || info.extent.height > destination_height ||
EXIT_IF(layers == 0 ||
(copy ? !ImageViewOps::FormatsCompatible(source.backing.format, backing.format)
: source.backing.format != backing.format));
auto resolved_source_range = source_range;
auto resolved_destination_range = destination_range;
resolved_source_range.layer_count = layers;
resolved_destination_range.layer_count = layers;
const vk::Extent3D resolve_extent {info.extent.width, info.extent.height, 1};
const vk::Extent3D resolve_extent {std::min(source_width, destination_width),
std::min(source_height, destination_height), 1};
m_scheduler.EndRendering();
auto command = m_scheduler.Current().Handle();
@@ -378,6 +378,9 @@ inline constexpr std::array<VideoOutFormatPolicy, 8> VIDEO_OUT_FORMAT_POLICIES {
const auto encoded = static_cast<float>(value & 0xffu) / 255.0f;
return encoded <= 0.04045f ? encoded / 12.92f : std::pow((encoded + 0.055f) / 1.055f, 2.4f);
};
const auto unorm16 = [](uint64_t value) {
return static_cast<float>(value & 0xffffu) / 65535.0f;
};
const auto float16 = [](uint32_t value) {
const auto sign = (value & 0x8000u) << 16u;
const auto exponent = (value >> 10u) & 0x1fu;
@@ -403,19 +406,49 @@ inline constexpr std::array<VideoOutFormatPolicy, 8> VIDEO_OUT_FORMAT_POLICIES {
next.float32[1] = float16(packed >> 16u);
[[fallthrough]];
case vk::Format::eR16Sfloat: next.float32[0] = float16(packed); break;
case vk::Format::eR16G16Unorm:
next.float32[1] = static_cast<float>(packed >> 16u) / 65535.0f;
case vk::Format::eR16G16B16A16Unorm:
next.float32[2] = unorm16(clear_words >> 32u);
next.float32[3] = unorm16(clear_words >> 48u);
[[fallthrough]];
case vk::Format::eR16Unorm:
next.float32[0] = static_cast<float>(packed & 0xffffu) / 65535.0f;
case vk::Format::eR16G16Unorm:
next.float32[1] = unorm16(packed >> 16u);
[[fallthrough]];
case vk::Format::eR16Unorm: next.float32[0] = unorm16(packed); break;
case vk::Format::eR16G16B16A16Uint:
for (uint32_t i = 0; i < 4; i++) {
next.uint32[i] = static_cast<uint16_t>(clear_words >> (16u * i));
}
break;
// Each clear register stores one raw 32-bit float channel.
case vk::Format::eR16G16B16A16Sint:
for (uint32_t i = 0; i < 4; i++) {
next.int32[i] = static_cast<int16_t>(clear_words >> (16u * i));
}
break;
// Each clear register stores one raw 32-bit channel.
case vk::Format::eR32G32Sfloat:
next.float32[1] = std::bit_cast<float>(static_cast<uint32_t>(clear_words >> 32u));
[[fallthrough]];
case vk::Format::eR32Sfloat: next.float32[0] = std::bit_cast<float>(packed); break;
case vk::Format::eR32Uint: next.uint32[0] = packed; break;
case vk::Format::eR32Sint: next.int32[0] = static_cast<int32_t>(packed); break;
case vk::Format::eR32G32Uint:
case vk::Format::eR32G32Sint:
next.uint32[1] = static_cast<uint32_t>(clear_words >> 32u);
[[fallthrough]];
case vk::Format::eR32Uint:
case vk::Format::eR32Sint: next.uint32[0] = packed; break;
// Narrow targets hold their channels in the low bits of the first word. UFC 6 (PPSA23566)
// fast-clears an R8_UINT map through CMASK every frame and then writes only part of it;
// without these the clear was dropped and old contents piled up.
case vk::Format::eR8Uint: next.uint32[0] = packed & 0xffu; break;
case vk::Format::eR8Unorm: next.float32[0] = unorm8(packed); break;
case vk::Format::eR8G8Uint:
next.uint32[0] = packed & 0xffu;
next.uint32[1] = (packed >> 8u) & 0xffu;
break;
case vk::Format::eR8G8Unorm:
next.float32[0] = unorm8(packed);
next.float32[1] = unorm8(packed >> 8u);
break;
case vk::Format::eR16Uint: next.uint32[0] = packed & 0xffffu; break;
case vk::Format::eR8G8B8A8Srgb:
next.float32[0] = srgb8(packed);
next.float32[1] = srgb8(packed >> 8u);
@@ -462,10 +495,16 @@ inline constexpr std::array<VideoOutFormatPolicy, 8> VIDEO_OUT_FORMAT_POLICIES {
// Keep this separate: the first register word alone cannot describe a 64-bit color.
[[nodiscard]] inline bool DecodeColorDwordFill(vk::Format format, uint32_t packed,
vk::ClearColorValue& clear) {
if ((format == vk::Format::eR16Sfloat || format == vk::Format::eR16Unorm) &&
if ((format == vk::Format::eR16Sfloat || format == vk::Format::eR16Unorm ||
format == vk::Format::eR16Uint || format == vk::Format::eR8G8Unorm ||
format == vk::Format::eR8G8Uint) &&
(packed & 0xffffu) != (packed >> 16u)) {
return false;
}
if ((format == vk::Format::eR8Unorm || format == vk::Format::eR8Uint) &&
packed != (packed & 0xffu) * 0x01010101u) {
return false;
}
if (format == vk::Format::eR16G16B16A16Sfloat) {
if (packed != 0) {
return false;
@@ -604,8 +604,13 @@ TextureBinding RenderExecutor::ResolveTexture(const ShaderRecompiler::IR::ImageR
view_levels, desc.info.resources.layers);
desc.view_info.base_level = view_base;
desc.type = storage ? TextureCache::BindingType::Storage : TextureCache::BindingType::Texture;
desc.heap = resource.heap;
auto id = texture_cache.FindImage(desc, shader_conversion);
auto id = texture_cache.FindImage(desc, shader_conversion);
if (!id) {
// Another extent at the address holds it; the heap retries once an image is created.
return {.desc = std::move(desc), .layout = vk::ImageLayout::eGeneral};
}
auto* image = &texture_cache.GetImage(id);
const bool stencil_association = static_cast<bool>(image->depth_id);
if (stencil_association) {
@@ -681,7 +686,7 @@ void RenderExecutor::PrepareBindings(const ShaderStageRuntime& runtime,
PreparedBindings& prepared) {
KYTY_PROFILER_FUNCTION();
EXIT_IF(!runtime);
const auto& program = *runtime.program;
const auto& program = runtime.module->info;
const auto& snapshot = *runtime.resources;
prepared.runtime = &runtime;
prepared.gds = {nullptr, 0, VK_WHOLE_SIZE};
@@ -723,7 +728,7 @@ void RenderExecutor::FindBuffers(std::span<PreparedBindings* const> stages) {
for (auto* stage: stages) {
auto& prepared = *stage;
EXIT_IF(prepared.runtime == nullptr || !*prepared.runtime);
const auto& program = *prepared.runtime->program;
const auto& program = prepared.runtime->module->info;
const auto& snapshot = *prepared.runtime->resources;
const auto& layout = program.bindings;
prepared.buffer_sources.resize(layout.descriptor_counts[static_cast<size_t>(BindingKind::Buffers)]);
@@ -780,7 +785,7 @@ void RenderExecutor::FindBuffers(std::span<PreparedBindings* const> stages) {
void RenderExecutor::RebindBuffers(PreparedBindings& prepared) {
KYTY_PROFILER_FUNCTION();
EXIT_IF(prepared.runtime == nullptr || !*prepared.runtime);
const auto& program = *prepared.runtime->program;
const auto& program = prepared.runtime->module->info;
const auto& snapshot = *prepared.runtime->resources;
const auto& layout = program.bindings;
EXIT_IF(prepared.buffer_sources.size() != layout.descriptor_counts[static_cast<size_t>(BindingKind::Buffers)]);
@@ -801,6 +806,9 @@ void RenderExecutor::RebindBuffers(PreparedBindings& prepared) {
prepared.buffers[index] = NativeStorageBuffer(m_context, prepared.buffer_sources[index],
resource, buffer_offset);
pack_memory_offset(index, buffer_offset);
// Null bindings allow 0 bytes, not the shared null buffer's 16, so no store reaches it.
prepared.shader_data[layout.MemorySizeDword() + index] =
static_cast<uint32_t>(prepared.buffer_sources[index].size + buffer_offset);
}
if (layout.descriptor_counts[static_cast<size_t>(BindingKind::FlattenedSrt)] != 0) {
prepared.flattened_srt = NativeUpload(m_context, snapshot.flattened_srt);
@@ -813,7 +821,7 @@ void RenderExecutor::RebindBuffers(PreparedBindings& prepared) {
void RenderExecutor::RebindImages(PreparedBindings& prepared) {
KYTY_PROFILER_FUNCTION();
EXIT_IF(prepared.runtime == nullptr || !*prepared.runtime);
const auto& program = *prepared.runtime->program;
const auto& program = prepared.runtime->module->info;
const auto& snapshot = *prepared.runtime->resources;
auto& images = prepared.images;
EXIT_IF(images.size() != program.info.images.size());
@@ -859,8 +867,8 @@ void RenderExecutor::PrepareGraphicsBindings(std::span<PreparedBindings* const>
bool uses_image_heap = false;
FindBuffers(stages);
for (auto* stage: stages) {
uses_dma |= stage->runtime->program->info.uses_dma;
uses_image_heap |= stage->runtime->program->info.uses_image_heap;
uses_dma |= stage->runtime->module->info.info.uses_dma;
uses_image_heap |= stage->runtime->module->info.info.uses_image_heap;
}
if (uses_dma) {
m_context.PrepareBda();
@@ -909,7 +917,7 @@ void RenderExecutor::CommitBindings(CommandBuffer& buffer,
: vk::ShaderStageFlags {};
for (const auto* prepared: prepared_bindings) {
EXIT_IF(prepared == nullptr || prepared->runtime == nullptr || !*prepared->runtime);
const auto& program = *prepared->runtime->program;
const auto& program = prepared->runtime->module->info;
write_count += std::popcount(program.bindings.descriptor_mask);
for (auto mask = program.bindings.descriptor_mask; mask != 0; mask &= mask - 1) {
const auto index = std::countr_zero(mask);
@@ -930,7 +938,7 @@ void RenderExecutor::CommitBindings(CommandBuffer& buffer,
const auto& reads = reader->runtime->resources->specialization_reads;
if (reads.empty()) continue;
for (const auto* writer: prepared_bindings) {
if (writer->runtime->program->has_address_writes) {
if (writer->runtime->module->info.has_address_writes) {
EXIT("scalar resource reads cannot be proven disjoint from shader address writes\n");
}
}
@@ -947,7 +955,7 @@ void RenderExecutor::CommitBindings(CommandBuffer& buffer,
}
}
for (const auto* writer: prepared_bindings) {
const auto& program = *writer->runtime->program;
const auto& program = writer->runtime->module->info;
for (const auto& resource: program.info.buffers) {
if (!resource.written || resource.descriptor_index == UINT32_MAX) continue;
const auto& written = writer->buffer_sources[resource.descriptor_index];
@@ -973,7 +981,7 @@ void RenderExecutor::CommitBindings(CommandBuffer& buffer,
std::array<vk::DescriptorBufferInfo, 2> address_buffers;
for (auto* prepared: prepared_bindings) {
const auto& program = *prepared->runtime->program;
const auto& program = prepared->runtime->module->info;
auto& descriptors = *prepared;
const auto shader_stage = NativeShaderStage(program.stage);
const auto shader_stages = ShaderPipelineStages(shader_stage);
@@ -12,6 +12,7 @@
#include "kernel/memory.h"
#include <algorithm>
#include <bit>
#include <xxhash.h>
namespace Libs::Graphics {
@@ -44,6 +45,20 @@ void Widen(size_t& begin, size_t& end, size_t first, size_t last) {
end = empty ? last : std::max(end, last);
}
// Probes at first + k * step whose first dword lies within a table of size bytes.
uint64_t Probes(uint64_t size, uint64_t first, uint64_t step) {
return size >= first + 4u ? (size - first - 4u) / step + 1u : 0u;
}
// Tables grow a little on most frames, so their slot words get room to double.
std::unique_ptr<Buffer> SlotBuffer(RenderContext& context, uint64_t size) {
return std::make_unique<Buffer>(
context.GetGraphics(), context.GetCommandScheduler(), MemoryUsage::DeviceLocal, 0,
vk::BufferUsageFlagBits::eTransferDst | vk::BufferUsageFlagBits::eStorageBuffer |
vk::BufferUsageFlagBits::eShaderDeviceAddress,
std::bit_ceil(size + 4u));
}
// Guest bytes of a table that its tracker pages [begin, end) cover.
std::pair<uint64_t, uint64_t> PageRange(uint64_t base, uint64_t size, size_t begin, size_t end) {
const auto origin = Common::AlignDown(base, TRACKER_PAGE_SIZE);
@@ -122,22 +137,17 @@ bool ImageHeap::Acquire(const IR::ImageHeapRequest& request, IR::ImageHeapView&
((request.base | request.first | request.step) & 3u) != 0u) {
return false;
}
auto memory = std::ranges::find_if(m_memories, [&](const auto& candidate) {
return candidate->base == request.base && candidate->size == size;
});
if (memory == m_memories.end()) {
auto memory = std::ranges::find_if(
m_memories, [&](const auto& candidate) { return candidate->base == request.base; });
// A table follows its descriptor's size, as the first program of a draw acquires it.
if (memory == m_memories.end() || ((*memory)->size != size && (*memory)->draw != m_draw)) {
if (!m_context.IsMapped(request.base, size)) return false;
auto created = std::make_unique<Memory>();
created->base = request.base;
created->size = size;
created->words.resize(size / 4u);
created->dead.resize(size / 4u);
created->dirty_pages.assign((Common::AlignUp(request.base + size, TRACKER_PAGE_SIZE) -
Common::AlignDown(request.base, TRACKER_PAGE_SIZE)) /
TRACKER_PAGE_SIZE,
uint8_t {1});
std::scoped_lock lock {m_mutex};
memory = m_memories.insert(m_memories.end(), std::move(created));
if (memory == m_memories.end()) {
std::scoped_lock lock {m_mutex};
memory = m_memories.insert(m_memories.end(),
std::make_unique<Memory>(Memory {.base = request.base}));
}
Resize(**memory, size);
}
// A draw refreshes a table and a view takes its changes once, so all programs of the draw see
// one snapshot and one set of kinds.
@@ -146,27 +156,28 @@ bool ImageHeap::Acquire(const IR::ImageHeapRequest& request, IR::ImageHeapView&
(*memory)->draw = m_draw;
}
auto& views = (*memory)->views;
const auto bytes = (*memory)->size;
const auto profile = IR::ImageHeapResource(*request.root, 0u);
const auto probes = Probes(size, request.first, request.step);
const auto limit = request.count < probes ? request.count : UINT64_MAX;
auto view = std::ranges::find_if(views, [&](const auto& candidate) {
return candidate->step == request.step && candidate->first == request.first &&
candidate->count == request.count && candidate->profile == profile;
candidate->limit == limit && candidate->profile == profile;
});
if (view == views.end()) {
const auto epoch = Image::heap_epoch.load(std::memory_order_relaxed);
const auto count = std::min(limit, Probes(bytes, request.first, request.step));
auto created = std::make_unique<View>(View {.memory = **memory,
.step = request.step,
.first = request.first,
.count = request.count,
.count = count,
.limit = limit,
.profile = profile,
.resolve_epoch = epoch,
.layout_epoch = epoch});
created->slots.resize(size / 4u + 1u);
created->buffer = std::make_unique<Buffer>(
m_context.GetGraphics(), m_context.GetCommandScheduler(), MemoryUsage::DeviceLocal, 0,
vk::BufferUsageFlagBits::eTransferDst | vk::BufferUsageFlagBits::eStorageBuffer |
vk::BufferUsageFlagBits::eShaderDeviceAddress,
size + 4u);
UpdateProbes(*created, 0u, size);
created->slots.resize(bytes / 4u + 1u);
created->buffer = SlotBuffer(m_context, bytes);
UpdateProbes(*created, 0u, bytes);
// The first upload also clears the slots no probe selects.
created->dirty_begin = 0;
created->dirty_end = created->slots.size();
@@ -254,10 +265,12 @@ void ImageHeap::Unwatch(const Memory& memory) {
void ImageHeap::InvalidateMemory(uint64_t address, uint64_t size) {
std::scoped_lock lock {m_mutex};
for (auto& memory: m_memories) {
const auto begin = std::max(address, memory->base);
const auto end = std::min(address + size, memory->base + memory->size);
if (begin >= end) continue;
// Watches cover whole pages, so a write next to the table must unwatch its page too.
const auto origin = Common::AlignDown(memory->base, TRACKER_PAGE_SIZE);
const auto begin = std::max(address, origin);
const auto end = std::min(address + size,
origin + memory->dirty_pages.size() * TRACKER_PAGE_SIZE);
if (begin >= end) continue;
MarkDirty(*memory, (begin - origin) / TRACKER_PAGE_SIZE,
(end - 1u - origin) / TRACKER_PAGE_SIZE + 1u);
}
@@ -381,15 +394,56 @@ uint32_t ImageHeap::Allocate() {
}
void ImageHeap::Retire(View& view) {
if (view.released.empty()) return;
// A retired index is reused once its tick ends, so no held entry may keep it.
std::erase_if(view.held, [&](uint32_t index) { return m_elements[index].refs == 0u; });
const auto tick = m_context.GetCommandScheduler().CurrentTick();
for (const auto index: view.released) m_retired.emplace_back(tick, index);
view.released.clear();
}
void ImageHeap::Resize(Memory& memory, uint64_t size) {
{
std::scoped_lock lock {m_mutex};
Unwatch(memory);
memory.size = size;
// The next refresh rereads the whole table.
memory.dirty_pages.assign((Common::AlignUp(memory.base + size, TRACKER_PAGE_SIZE) -
Common::AlignDown(memory.base, TRACKER_PAGE_SIZE)) /
TRACKER_PAGE_SIZE,
uint8_t {1});
}
memory.words.resize(size / 4u);
memory.dead.resize(size / 4u);
auto& scheduler = m_context.GetCommandScheduler();
for (auto& view: memory.views) {
const auto count = std::min(view->limit, Probes(size, view->first, view->step));
for (auto probe = count; probe < view->count; ++probe) {
Assign(*view, (view->first + probe * view->step) / 4u, 0u);
}
view->count = count;
const auto slots = view->slots.size();
view->slots.resize(size / 4u + 1u);
view->dirty_end = std::min(view->dirty_end, view->slots.size());
view->dirty_begin = std::min(view->dirty_begin, view->dirty_end);
if (view->slots.size() * sizeof(uint32_t) > view->buffer->Size()) {
if (scheduler.Active()) {
scheduler.DeferOperation([buffer = std::move(view->buffer)] {});
}
view->buffer = SlotBuffer(m_context, size);
Widen(view->dirty_begin, view->dirty_end, 0u, view->slots.size());
} else if (slots < view->slots.size()) {
// Device words past the old end may hold stale slots.
Widen(view->dirty_begin, view->dirty_end, slots, view->slots.size());
}
}
}
void ImageHeap::Rebind(RenderExecutor& executor) {
KYTY_PROFILER_FUNCTION();
auto& textures = m_context.GetTextureCache();
const auto epoch = Image::heap_epoch.load(std::memory_order_relaxed);
const auto serial = Image::serials.load(std::memory_order_relaxed);
for (auto* view: m_active) {
bool stale = false;
const auto mark = [&](uint32_t index) { m_elements[index].stale = stale = true; };
@@ -418,6 +472,19 @@ void ImageHeap::Rebind(RenderExecutor& executor) {
mark(index);
return true;
});
// Held elements retry once an image is created, such as a render target of their extent.
if (view->held_serial != serial) {
view->held_serial = serial;
std::erase_if(view->held, [&](uint32_t index) {
const auto& element = m_elements[index];
if (element.refs == 0u || element.stale) return true;
const auto binding = executor.ResolveTexture(
IR::ImageHeapResource(view->profile, element.kind), element.value);
if (binding.image_id) mark(index);
// An unmapped texture stays null.
return binding.image_id || binding.desc.info.data.Empty();
});
}
if (stale) {
// A changed image needs a new element; pending draws still read the old one. The new
// element keeps the old value, since a view takes table changes only when acquired.
@@ -446,6 +513,8 @@ void ImageHeap::Rebind(RenderExecutor& executor) {
view->dcc.push_back(index);
}
view->away.push_back(index);
} else if (!binding.desc.info.data.Empty()) {
view->held.push_back(index);
}
m_elements[index].binding = std::move(binding);
m_writes.push_back(index);
@@ -22,10 +22,11 @@ class RenderExecutor;
struct ImageHeapTestAccess;
// Guest image tables that shaders index by GPU-computed offsets, kept resident in one bindless
// sampled-image array. Each view of a table owns device slot words, one per table dword, that
// select an array element. CPU writes to a table fault in its snapshot, which each draw refreshes
// once; a view takes the changes when a draw first acquires it. Changes to the images behind
// elements bump Image::heap_epoch.
// sampled-image array. A table is keyed by its base and follows its descriptor's size. Each view
// of a table owns device slot words, one per table dword, that select an array element. CPU
// writes to a table fault in its snapshot, which each draw refreshes once; a view takes the
// changes when a draw first acquires it. Changes to the images behind elements bump
// Image::heap_epoch.
class ImageHeap final: public ShaderRecompiler::IR::ImageHeapSource {
public:
ImageHeap(RenderContext& context, PageManager& page_manager);
@@ -70,6 +71,7 @@ private:
uint64_t step = 0;
uint64_t first = 0;
uint64_t count = 0;
uint64_t limit = 0; // Probes a key domain allows, if fewer.
ShaderRecompiler::IR::ImageResource profile;
std::vector<uint32_t> slots; // Plus a trailing null slot.
std::vector<uint32_t> kind_keys;
@@ -79,6 +81,7 @@ private:
std::vector<uint32_t> released; // No slot selects them after the next upload.
std::vector<uint32_t> away; // Not yet moved to the read layout.
std::vector<uint32_t> dcc; // Color metadata clears apply on each discovery.
std::vector<uint32_t> held; // Another extent holds their address.
std::unique_ptr<Buffer> buffer;
size_t dirty_begin = 0; // Slots to upload.
size_t dirty_end = 0;
@@ -86,6 +89,7 @@ private:
size_t changed_end = 0;
uint64_t resolve_epoch = 0;
uint64_t layout_epoch = 0;
uint64_t held_serial = 0; // Image::serials when held was last retried.
};
struct Memory {
@@ -113,6 +117,7 @@ private:
Image* ImageOf(const Element& element) const;
uint32_t Allocate();
void Retire(View& view);
void Resize(Memory& memory, uint64_t size);
void WriteDescriptors();
RenderContext& m_context;
@@ -15,7 +15,10 @@
#include "graphics/host_gpu/renderer/pipeline/blendMapping.h"
#include "graphics/host_gpu/renderer/render.h"
#include "graphics/host_gpu/renderer/renderContext.h"
#include "graphics/shader/recompiler/BufferFormat.h"
#include "graphics/shader/recompiler/ShaderRecompiler.h"
#include "graphics/shader/recompiler/Tessellation.h"
#include "graphics/shader/recompiler/ir/Serialize.h"
#include "graphics/shader/shaderCompiler.h"
#include "kernel/memory.h"
#include "loader/systemContent.h"
@@ -29,9 +32,9 @@
#include <cstring>
#include <fmt/format.h>
#include <limits>
#include <ranges>
#include <span>
#include <spirv-tools/libspirv.hpp>
#include <string_view>
#include <utility>
#include <vector>
#include <xxhash.h>
@@ -114,14 +117,11 @@ bool ReadShaderGuestMemory(void*, uint64_t address, std::span<uint32_t> values)
Libs::LibKernel::Memory::TryReadBufferBacking(address, values.data(), values.size_bytes());
}
void DumpShaderSpirv(const char* stage_name, uint64_t shader_hash,
const std::vector<uint32_t>& spirv) {
void DumpShader(std::span<const uint32_t> words, const std::string& name) {
if (!Config::GraphicsDebugDumpEnabled()) {
return;
}
static std::atomic_int id = 0;
const auto path = Config::GetShaderLogFolder() / fmt::format("{:04d}_new_shader_{}_{:016x}.spv",
id++, stage_name, shader_hash);
const auto path = Config::GetShaderLogFolder() / name;
Common::File::CreateDirectories(path.parent_path());
Common::File file(path);
if (file.IsInvalid()) {
@@ -129,32 +129,13 @@ void DumpShaderSpirv(const char* stage_name, uint64_t shader_hash,
LOGF_COLOR(Log::Color::BrightRed, "Can't create file: %s\n", path_text.c_str());
return;
}
file.Write(spirv.data(), spirv.size() * sizeof(uint32_t));
file.Write(words.data(), words.size_bytes());
}
void DumpShaderOriginal(const char* stage_name, uint64_t shader_hash,
std::span<const uint32_t> code) {
if (!Config::GraphicsDebugDumpEnabled()) {
return;
}
EXIT_IF(code.empty());
static std::atomic_int id = 0;
const auto path = Config::GetShaderLogFolder() / "original" /
fmt::format("{:04d}_new_shader_{}_{:016x}.bin", id++, stage_name, shader_hash);
Common::File::CreateDirectories(path.parent_path());
Common::File file(path);
if (file.IsInvalid()) {
const auto path_text = Common::PathToString(path);
LOGF_COLOR(Log::Color::BrightRed, "Can't create file: %s\n", path_text.c_str());
return;
}
file.Write(code.data(), code.size_bytes());
}
bool ValidateShaderSpirv(const char* label, uint64_t shader_hash,
void ValidateShaderSpirv(const char* stage, uint64_t shader_hash,
const std::vector<uint32_t>& spirv, bool maintenance8_enabled) {
if (!Config::ShaderValidationEnabled()) {
return true;
return;
}
spvtools::SpirvTools tools(SPV_ENV_VULKAN_1_3);
std::string messages;
@@ -167,7 +148,7 @@ bool ValidateShaderSpirv(const char* label, uint64_t shader_hash,
spvtools::ValidatorOptions validator_options;
validator_options.SetAllowOffsetTextureOperand(maintenance8_enabled);
if (tools.Validate(spirv.data(), spirv.size(), validator_options)) {
return true;
return;
}
spvtools::SpirvTools disassembler(SPV_ENV_VULKAN_1_2);
std::string text;
@@ -177,332 +158,320 @@ bool ValidateShaderSpirv(const char* label, uint64_t shader_hash,
static_cast<uint32_t>(SPV_BINARY_TO_TEXT_OPTION_COMMENT) |
static_cast<uint32_t>(SPV_BINARY_TO_TEXT_OPTION_INDENT) |
static_cast<uint32_t>(SPV_BINARY_TO_TEXT_OPTION_COLOR));
LOGF_COLOR(Log::Color::BrightRed, "%s SPIR-V validation failed hash=0x%016" PRIx64 ":\n%s",
label, shader_hash, messages.c_str());
LOGF("%s\n", text.c_str());
return false;
EXIT("ShaderRecompiler %s SPIR-V validation failed hash=0x%016" PRIx64 ":\n%s", stage,
shader_hash, messages.c_str());
}
// Formats with a narrower host variant drop the components the shader does not read.
vk::Format NarrowInputFormat(vk::Format format, uint32_t used_components) {
switch (format) {
case vk::Format::eR32G32B32A32Sfloat:
switch (used_components) {
case 1: return vk::Format::eR32Sfloat;
case 2: return vk::Format::eR32G32Sfloat;
case 3: return vk::Format::eR32G32B32Sfloat;
default: return format;
}
case vk::Format::eR32G32B32Sfloat:
switch (used_components) {
case 1: return vk::Format::eR32Sfloat;
case 2: return vk::Format::eR32G32Sfloat;
default: return format;
}
case vk::Format::eR16G16B16A16Sfloat:
switch (used_components) {
case 1: return vk::Format::eR16Sfloat;
case 2: return vk::Format::eR16G16Sfloat;
default: return format;
}
case vk::Format::eR8G8B8A8Unorm:
switch (used_components) {
case 1: return vk::Format::eR8Unorm;
case 2: return vk::Format::eR8G8Unorm;
default: return format;
}
case vk::Format::eR8G8B8A8Snorm:
return used_components == 2 ? vk::Format::eR8G8Snorm : format;
case vk::Format::eR8G8B8A8Uint:
switch (used_components) {
case 1: return vk::Format::eR8Uint;
case 2: return vk::Format::eR8G8Uint;
default: return format;
}
default: return format;
}
}
vk::Format InputFormat(const ShaderBufferResource& resource, uint32_t used_components) {
switch (resource.RawFormat()) {
// Temporary: invalid PS5 buffer format 113 (VertexAttribFormat::k16_16SInt).
case 113: return NarrowInputFormat(vk::Format::eR32G32B32A32Sfloat, used_components);
case 121: return vk::Format::eR16G16Sfloat;
default: break;
}
const auto format = VulkanFormat(resource.Format());
if (format == vk::Format::eUndefined ||
ShaderRecompiler::Format::GetFormatInfo(resource.Format()).component_count == 0) {
EXIT("unknown vertex format: fmt = %u\n", resource.RawFormat());
}
return NarrowInputFormat(format, used_components);
}
// The host format of each vertex input attribute, narrowed to the components the stage reads.
std::vector<vk::Format> VertexInputFormats(const ShaderVertexInputInfo& input_info,
const ShaderRecompiler::IR::CompiledShaderInfo& program) {
std::vector<vk::Format> formats(input_info.resources_num);
for (int index = 0; index < input_info.resources_num; index++) {
const auto registers_num = input_info.resources_dst[index].registers_num;
EXIT_IF(registers_num < 1 || registers_num > 4);
const auto fetched = program.info.vertex_fetch_components[index];
formats[index] = InputFormat(input_info.resources[index],
fetched > 0 ? fetched : static_cast<uint32_t>(registers_num));
}
return formats;
}
} // namespace
std::size_t PipelineCache::GraphicsPipelineKeyHash::operator()(const GraphicsPipelineKey& key) const {
std::size_t hash = 0;
PipelineKeyHash::Mix(hash, key.rendering.color_count);
for (uint32_t i = 0; i < key.rendering.color_count; i++) {
PipelineKeyHash::Mix(hash, static_cast<uint32_t>(key.rendering.color_formats[i]));
}
PipelineKeyHash::Mix(hash, static_cast<uint32_t>(key.rendering.depth_format));
PipelineKeyHash::Mix(hash, static_cast<uint32_t>(key.rendering.stencil_format));
for (const auto id: key.vertex_shader_ids) {
PipelineKeyHash::Mix(hash, id);
}
PipelineKeyHash::Mix(hash, key.ps_shader_id);
PipelineKeyHash::Mix(hash, key.vertex_input.binding_count);
for (uint32_t i = 0; i < key.vertex_input.binding_count; i++) {
PipelineKeyHash::Mix(hash, key.vertex_input.bindings[i].stride);
PipelineKeyHash::Mix(hash, key.vertex_input.bindings[i].instance);
}
PipelineKeyHash::Mix(hash, key.vertex_input.attribute_count);
for (uint32_t i = 0; i < key.vertex_input.attribute_count; i++) {
PipelineKeyHash::Mix(hash, key.vertex_input.attributes[i].offset);
PipelineKeyHash::Mix(hash, key.vertex_input.attributes[i].binding);
}
PipelineKeyHash::Mix(hash, XXH3_64bits(&key.static_params, sizeof(key.static_params)));
return hash;
ShaderRecompiler::Spirv::Profile SpirvProfile(const GraphicContext& graphics) {
// AMD GPUs (vendor 0x1002) run NMin/NMax as v_min/v_max, which give the guest's NaN and
// signed-zero results; GLSL.std.450 alone does not order -0 below +0.
return {.maintenance8 = graphics.maintenance8_enabled,
.native_float_min_max = graphics.physical_device_properties.vendorID == 0x1002u,
.device_clock_period_ps = graphics.device_clock_period_ps};
}
struct PipelineCache::ProgramCache {
struct ProgramKey {
ShaderType stage = ShaderType::Unknown;
uint64_t hash = 0;
uint32_t user_data_count = 0;
uint32_t code_size = 0;
std::vector<uint32_t> static_state;
std::filesystem::path ShaderStorePath() {
if (KYTY_BUILD != KYTY_BUILD_RELEASE || !Config::ShaderCacheEnabled() ||
Config::GraphicsDebugDumpEnabled()) {
return {};
}
const auto title_id = PipelineCacheTitleId();
if (title_id.empty()) {
return {};
}
return std::filesystem::path("_PipelineCache") / (title_id + ".shaders");
}
bool operator==(const ProgramKey&) const = default;
bool GraphicsPipelineKey::operator==(const GraphicsPipelineKey& other) const {
return std::memcmp(this, &other, sizeof(*this)) == 0;
}
std::size_t PipelineCache::GraphicsPipelineKeyHash::operator()(const GraphicsPipelineKey& key) const {
return XXH3_64bits(&key, sizeof(key));
}
std::span<const uint32_t> PipelineCache::Program::FunctionCode() const {
return call_source
? std::span<const uint32_t>(call_source->linked->code).subspan(call_source->code.size())
: std::span<const uint32_t>{};
}
bool PipelineCache::Program::RefreshCallSource(const ShaderRecompiler::IR::SrtRuntime& runtime) {
const auto revision = call_source->revision;
ShaderRecompiler::RefreshShaderSource(*call_source, runtime);
resource_plan.source_reads = call_source->reads;
return revision != call_source->revision;
}
template <bool HasCalls>
ShaderModule* PipelineCache::Program::FindModule(const ShaderRecompiler::IR::SrtRuntime& runtime,
uint32_t push_data_cursor) {
EXIT_IF(!ShaderRecompiler::IR::MaterializeResources(resource_plan, runtime, resources,
specialization));
for (auto& candidate: modules) {
const auto& layout = candidate.info.bindings;
if (layout.push_data_start_dword != ShaderRecompiler::IR::PushData::StartFor(
push_data_cursor, layout.ShaderDataDwords()) ||
candidate.specialization != specialization) {
continue;
}
if constexpr (HasCalls) {
if (!std::ranges::equal(candidate.function_code, FunctionCode())) continue;
}
return &candidate;
}
return nullptr;
}
ShaderModule& PipelineCache::CompileModule(Program& program, const ProgramKey& key,
ShaderRecompiler::CompileOptions options,
ShaderRecompiler::TranslateResult translated,
uint32_t push_data_cursor) {
Common::DebugCounters::ScopedTimer timer(Common::DebugCounters::Counter::ShadersCompiled,
Common::DebugCounters::Counter::ShaderCompileNs,
Common::DebugCounters::Peak::ShaderCompileNs);
const auto profile = SpirvProfile(m_graphics);
options.profile = &profile;
auto result = ShaderRecompiler::CompileProgram(std::move(translated), options,
program.specialization, push_data_cursor);
auto info = std::move(result.program).TakeCompiledInfo();
// The same inputs give the same module id across runs.
const auto function_code = program.FunctionCode();
ShaderRecompiler::IR::Writer inputs;
ShaderRecompiler::IR::IoAll(inputs, key.stage, key.hash, key.user_data_count, key.static_state,
info.bindings.push_data_start_dword, program.specialization);
inputs.Raw(function_code.data(), function_code.size_bytes()); // Last, so it needs no size.
const auto id = XXH3_64bits(inputs.bytes.data(), inputs.bytes.size());
const auto* stage = ShaderRecompiler::StageName(key.stage);
DumpShader(result.spirv, fmt::format("{}_{:016x}_{:016x}.spv", stage, key.hash, id));
ValidateShaderSpirv(stage, key.hash, result.spirv, profile.maintenance8);
auto& module = program.modules.emplace_back(ShaderModule {
.id = id,
.module = CompileSPV(result.spirv, m_graphics.device),
.info = std::move(info),
.specialization = program.specialization,
.function_code = {function_code.begin(), function_code.end()},
});
EXIT_IF(module.module == nullptr);
m_compiled[static_cast<size_t>(key.stage)]++;
// Guest geometry shaders are compiled through the host mesh stage.
std::printf("Shaders: VS %u | PS %u | CS %u | GS %u | LS %u | HS %u | TES %u\n",
m_compiled[static_cast<size_t>(ShaderType::Vertex)],
m_compiled[static_cast<size_t>(ShaderType::Pixel)],
m_compiled[static_cast<size_t>(ShaderType::Compute)],
m_compiled[static_cast<size_t>(ShaderType::Mesh)],
m_compiled[static_cast<size_t>(ShaderType::Local)],
m_compiled[static_cast<size_t>(ShaderType::TessellationControl)],
m_compiled[static_cast<size_t>(ShaderType::TessellationEvaluation)]);
// The inputs of these come from the stage's static key.
if (key.stage == ShaderType::Vertex || key.stage == ShaderType::Local) {
module.vertex_formats = VertexInputFormats(*options.input_info.vertex, module.info);
} else if (key.stage == ShaderType::Pixel &&
options.input_info.pixel->parameter_mode == ShaderPixelParameterMode::Rectangle) {
module.rect_parameters = RectListParameters(*options.input_info.pixel, module.info);
}
// Programs with GPU-selected calls depend on their callees' current code.
if (program.call_source == nullptr) {
StoreModule(key, program, module, result.spirv);
}
return module;
}
template <typename InputInfo>
const ShaderModule* PipelineCache::GetProgram(const ShaderParams& params, InputInfo& input_info,
uint32_t& push_data_cursor) {
const auto user_data = std::span(params.user_data).first(params.user_data_count);
ShaderRecompiler::IR::SrtRuntime runtime {
.user_data = user_data,
.shader_base = params.Base(),
.read_specialization_memory = ReadShaderGuestMemory,
.image_heap = m_image_heap,
};
struct Permutation {
std::vector<uint32_t> function_code;
ShaderRecompiler::IR::ResourceSpecialization specialization;
ShaderRecompiler::IR::CompiledShaderInfo program;
ShaderProgram handle;
ShaderRecompiler::CompileOptions options {.wave_size = input_info.wave_size};
if constexpr (std::is_same_v<InputInfo, ShaderVertexInputInfo>) {
options.stage = input_info.logical_stage;
options.input_info.vertex = &input_info;
options.user_data_base = 8;
if (options.stage == ShaderType::Mesh ||
options.stage == ShaderType::TessellationControl) {
options.user_data_base = 0;
options.wave_size =
options.stage == ShaderType::Mesh ? input_info.mesh.wave_size : 64u;
}
} else if constexpr (std::is_same_v<InputInfo, ShaderPixelInputInfo>) {
options.stage = ShaderType::Pixel;
options.input_info.pixel = &input_info;
} else {
static_assert(std::is_same_v<InputInfo, ShaderComputeInputInfo>);
options.stage = ShaderType::Compute;
options.input_info.compute = &input_info;
runtime.workgroup_counts = input_info.workgroup_counts;
runtime.indirect_args_address = input_info.indirect_args_address;
}
m_lookup_key.stage = options.stage;
m_lookup_key.hash = params.hash;
m_lookup_key.user_data_count = params.user_data_count;
BuildStageStaticKey(input_info, m_lookup_key.static_state);
auto entry = m_program_cache.find(m_lookup_key);
const auto bind = [&](const ShaderModule& module) {
input_info.stage = {.module = &module, .resources = &entry->second.resources};
module.info.bindings.AdvancePushData(push_data_cursor);
return &module;
};
struct SourceEntry {
SourceEntry(ShaderRecompiler::IR::ResourcePlan plan,
std::unique_ptr<ShaderRecompiler::ShaderSource> call_source)
: call_source(std::move(call_source)), resource_plan(std::move(plan)) {
permutations.reserve(8);
bool source_changed = false;
if (entry != m_program_cache.end()) {
auto& cached = entry->second;
if (cached.skip_dispatch) return nullptr;
ShaderModule* module = nullptr;
if (cached.call_source) {
source_changed = cached.RefreshCallSource(runtime);
if (!source_changed) module = cached.FindModule<true>(runtime, push_data_cursor);
} else {
module = cached.FindModule<false>(runtime, push_data_cursor);
}
std::span<const uint32_t> FunctionCode() const {
return call_source
? std::span<const uint32_t>(call_source->linked->code).subspan(call_source->code.size())
: std::span<const uint32_t>{};
}
bool RefreshCallSource(const ShaderRecompiler::IR::SrtRuntime& runtime) {
const auto revision = call_source->revision;
ShaderRecompiler::RefreshShaderSource(*call_source, runtime);
resource_plan.source_reads = call_source->reads;
return revision != call_source->revision;
}
template <bool HasCalls>
Permutation* FindPermutation(const ShaderRecompiler::IR::SrtRuntime& runtime,
uint32_t push_data_cursor) {
EXIT_IF(!ShaderRecompiler::IR::MaterializeResources(
resource_plan, runtime, resources, specialization));
for (auto& candidate: permutations) {
const auto& layout = candidate.program.bindings;
if (layout.push_data_start_dword != ShaderRecompiler::IR::PushData::StartFor(
push_data_cursor, layout.ShaderDataDwords()) ||
candidate.specialization != specialization) {
continue;
}
if constexpr (HasCalls) {
if (!std::ranges::equal(candidate.function_code, FunctionCode())) continue;
}
return &candidate;
}
return nullptr;
}
std::unique_ptr<ShaderRecompiler::ShaderSource> call_source;
ShaderRecompiler::IR::ResourcePlan resource_plan;
ShaderRecompiler::IR::ResourceSnapshot resources;
ShaderRecompiler::IR::ResourceSpecialization specialization;
std::vector<Permutation> permutations;
bool skip_dispatch = false;
};
struct ProgramKeyHash {
std::size_t operator()(const ProgramKey& key) const {
std::size_t hash = static_cast<std::size_t>(key.stage);
PipelineKeyHash::Mix(hash, static_cast<std::size_t>(key.hash));
if constexpr (sizeof(std::size_t) < sizeof(uint64_t)) {
PipelineKeyHash::Mix(hash, static_cast<std::size_t>(key.hash >> 32u));
}
PipelineKeyHash::Mix(hash, key.user_data_count);
PipelineKeyHash::Mix(hash, key.code_size);
PipelineKeyHash::Mix(hash, key.static_state.size());
// Bucket same-shape static variants by source. ProgramKey equality performs the one
// exact state comparison needed on a stable hit without hashing the full state first.
return hash;
}
};
static constexpr std::size_t MaxStaticKeyWords = 32 + ShaderVertexInputInfo::RES_MAX * 6;
Permutation CompilePermutation(const char* stage_name,
const ShaderRecompiler::CompileOptions& options,
ShaderRecompiler::TranslateResult translated,
ShaderRecompiler::IR::ResourceSpecialization specialization,
uint32_t push_data_start_dword,
std::span<const uint32_t> function_code) {
Common::DebugCounters::ScopedTimer timer(Common::DebugCounters::Counter::ShadersCompiled,
Common::DebugCounters::Counter::ShaderCompileNs,
Common::DebugCounters::Peak::ShaderCompileNs);
auto result = ShaderRecompiler::CompileProgram(std::move(translated), options,
specialization, push_data_start_dword);
if (!ValidateShaderSpirv(options.dump_label, options.shader_hash, result.spirv,
options.profile->maintenance8)) {
DumpShaderSpirv(stage_name, options.shader_hash, result.spirv);
EXIT("%s failed hash=0x%016" PRIx64 ": SPIR-V validation failed\n", options.dump_label,
options.shader_hash);
}
DumpShaderSpirv(stage_name, options.shader_hash, result.spirv);
const auto module = CompileSPV(result.spirv, graphics.device);
EXIT_IF(module == nullptr);
if (options.dump_ir) {
LOGF("%s SPIR-V words=%" PRIu64 " wave_size=%u\n", options.dump_label,
static_cast<uint64_t>(result.spirv.size()), options.wave_size);
}
return {
.function_code = {function_code.begin(), function_code.end()},
.specialization = std::move(specialization),
.program = std::move(result.program).TakeCompiledInfo(),
.handle = {.id = ++next_shader_id, .module = module},
};
if (module != nullptr) return bind(*module);
}
template <typename InputInfo>
ShaderProgram Get(const ShaderParams& params, InputInfo& input_info,
uint32_t& push_data_cursor) {
const auto user_data = std::span(params.user_data).first(params.user_data_count);
ShaderRecompiler::IR::SrtRuntime runtime {
.user_data = user_data,
.shader_base = params.Base(),
.read_specialization_memory = ReadShaderGuestMemory,
.image_heap = image_heap,
};
ShaderRecompiler::CompileOptions options {.wave_size = input_info.wave_size};
if constexpr (std::is_same_v<InputInfo, ShaderVertexInputInfo>) {
options.stage = input_info.logical_stage;
options.input_info.vertex = &input_info;
options.user_data_base = 8;
if (options.stage == ShaderType::Mesh ||
options.stage == ShaderType::TessellationControl) {
options.user_data_base = 0;
options.wave_size =
options.stage == ShaderType::Mesh ? input_info.mesh.wave_size : 64u;
}
} else if constexpr (std::is_same_v<InputInfo, ShaderPixelInputInfo>) {
options.stage = ShaderType::Pixel;
options.input_info.pixel = &input_info;
} else {
static_assert(std::is_same_v<InputInfo, ShaderComputeInputInfo>);
options.stage = ShaderType::Compute;
options.input_info.compute = &input_info;
runtime.workgroup_counts = input_info.workgroup_counts;
runtime.indirect_args_address = input_info.indirect_args_address;
}
lookup_key.stage = options.stage;
lookup_key.hash = params.hash;
lookup_key.user_data_count = params.user_data_count;
lookup_key.code_size = static_cast<uint32_t>(params.code.size());
BuildStageStaticKey(input_info, lookup_key.static_state);
auto entry = programs.find(lookup_key);
const auto bind = [&](const Permutation& permutation) {
input_info.stage = {.program = &permutation.program,
.resources = &entry->second.resources,
.handle = &permutation.handle};
permutation.program.bindings.AdvancePushData(push_data_cursor);
return permutation.handle;
};
bool source_changed = false;
if (entry != programs.end()) {
auto& cached = entry->second;
if (cached.skip_dispatch) return {};
Permutation* permutation = nullptr;
if (cached.call_source) {
source_changed = cached.RefreshCallSource(runtime);
if (!source_changed) permutation = cached.FindPermutation<true>(runtime, push_data_cursor);
} else {
permutation = cached.FindPermutation<false>(runtime, push_data_cursor);
}
if (permutation != nullptr) return bind(*permutation);
}
const char* label = nullptr;
const char* stage_name = nullptr;
switch (options.stage) {
case ShaderType::Vertex: label = "ShaderRecompiler VS"; stage_name = "vs"; break;
case ShaderType::Mesh: label = "ShaderRecompiler MS"; stage_name = "ms"; break;
case ShaderType::Local: label = "ShaderRecompiler LS"; stage_name = "ls"; break;
case ShaderType::TessellationControl: label = "ShaderRecompiler HS"; stage_name = "hs"; break;
case ShaderType::TessellationEvaluation: label = "ShaderRecompiler DS"; stage_name = "ds"; break;
case ShaderType::Pixel: label = "ShaderRecompiler PS"; stage_name = "ps"; break;
case ShaderType::Compute: label = "ShaderRecompiler CS"; stage_name = "cs"; break;
default: EXIT("invalid pipeline shader stage\n");
}
options.profile = &profile;
options.shader_hash = params.hash;
options.user_data = user_data;
options.back_code = params.back_code;
options.dump_ir = Config::GetShaderLogDirection() != Config::LogDirection::Silent;
options.early_dump = options.dump_ir;
options.emit_debug_info = Config::GraphicsDebugDumpEnabled();
options.dump_label = label;
DumpShaderOriginal(stage_name, options.shader_hash, params.code);
ShaderRecompiler::ShaderSource source;
if (entry == programs.end()) source = ShaderRecompiler::PrepareShaderSource(params.code, options);
options.shader_hash = params.hash;
options.user_data = user_data;
options.back_code = params.back_code;
options.dump_ir = Config::GetShaderLogDirection() != Config::LogDirection::Silent;
options.emit_debug_info = Config::GraphicsDebugDumpEnabled();
ShaderRecompiler::ShaderSource source;
if (entry == m_program_cache.end()) {
DumpShader(params.code, fmt::format("original/{}_{:016x}.bin",
ShaderRecompiler::StageName(options.stage), params.hash));
source = ShaderRecompiler::PrepareShaderSource(params.code, options);
if (source.unsupported_call_pc != UINT32_MAX) {
PipelineCacheLog("!!! TEMPORARY COMPUTE DISPATCH SKIP !!! shader=0x{:016x} pc=0x{:08x} "
"reason=unsupported GPU-selected callable target; rendering is incomplete",
params.hash, source.unsupported_call_pc);
programs.try_emplace(lookup_key, ShaderRecompiler::IR::ResourcePlan{}, nullptr)
.first->second.skip_dispatch = true;
return {};
m_program_cache.emplace(m_lookup_key, Program {.skip_dispatch = true});
return nullptr;
}
auto* current = entry == programs.end() ? &source : entry->second.call_source.get();
ShaderRecompiler::TranslateResult translated;
if (current != nullptr) {
if (!current->calls.empty()) {
if (entry == programs.end()) ShaderRecompiler::RefreshShaderSource(*current, runtime);
translated = ShaderRecompiler::TranslateProgram(current->linked->decoded, options);
translated.program.source_reads = current->reads;
} else {
translated = ShaderRecompiler::TranslateProgram(current->decoded, options);
}
}
auto* current = entry == m_program_cache.end() ? &source : entry->second.call_source.get();
ShaderRecompiler::TranslateResult translated;
if (current != nullptr) {
if (!current->calls.empty()) {
if (entry == m_program_cache.end()) ShaderRecompiler::RefreshShaderSource(*current, runtime);
translated = ShaderRecompiler::TranslateProgram(current->linked->decoded, options);
translated.program.source_reads = current->reads;
} else {
translated = ShaderRecompiler::TranslateProgram(params.code, options);
translated = ShaderRecompiler::TranslateProgram(current->decoded, options);
}
if (entry == programs.end()) {
auto retained = !source.calls.empty() ? std::make_unique<ShaderRecompiler::ShaderSource>(std::move(source)) : nullptr;
entry = programs.try_emplace(lookup_key,
ShaderRecompiler::IR::ExtractResourcePlan(translated.program), std::move(retained)).first;
EXIT_IF(!ShaderRecompiler::IR::MaterializeResources(
entry->second.resource_plan, runtime, entry->second.resources,
entry->second.specialization));
} else if (source_changed) {
entry->second.resource_plan = ShaderRecompiler::IR::ExtractResourcePlan(translated.program);
if (const auto* permutation = entry->second.FindPermutation<true>(runtime, push_data_cursor)) {
return bind(*permutation);
}
}
entry->second.permutations.push_back(CompilePermutation(
stage_name, options, std::move(translated), entry->second.specialization, push_data_cursor,
entry->second.FunctionCode()));
std::array<size_t, static_cast<size_t>(ShaderType::TessellationEvaluation) + 1> counts {};
for (const auto& [key, source]: programs) {
counts[static_cast<size_t>(key.stage)] += source.permutations.size();
}
// Guest geometry shaders are compiled through the host mesh stage.
std::printf("Shaders: VS %zu | PS %zu | CS %zu | GS %zu | LS %zu | HS %zu | TES %zu\n",
counts[static_cast<size_t>(ShaderType::Vertex)],
counts[static_cast<size_t>(ShaderType::Pixel)],
counts[static_cast<size_t>(ShaderType::Compute)],
counts[static_cast<size_t>(ShaderType::Mesh)],
counts[static_cast<size_t>(ShaderType::Local)],
counts[static_cast<size_t>(ShaderType::TessellationControl)],
counts[static_cast<size_t>(ShaderType::TessellationEvaluation)]);
return bind(entry->second.permutations.back());
} else {
translated = ShaderRecompiler::TranslateProgram(params.code, options);
}
ProgramCache(GraphicContext& graphics, ShaderRecompiler::IR::ImageHeapSource* image_heap)
: graphics(graphics), image_heap(image_heap),
// AMD GPUs (vendor 0x1002) run NMin/NMax as v_min/v_max, which give the guest's NaN and
// signed-zero results; GLSL.std.450 alone does not order -0 below +0.
profile {.maintenance8 = graphics.maintenance8_enabled,
.native_float_min_max =
graphics.physical_device_properties.vendorID == 0x1002u,
.device_clock_period_ps = graphics.device_clock_period_ps} {
lookup_key.static_state.reserve(MaxStaticKeyWords);
}
~ProgramCache() {
for (const auto& [key, entry]: programs) {
(void)key;
for (const auto& permutation: entry.permutations) {
graphics.device.destroyShaderModule(permutation.handle.module, nullptr);
}
if (entry == m_program_cache.end()) {
auto retained = !source.calls.empty()
? std::make_unique<ShaderRecompiler::ShaderSource>(std::move(source))
: nullptr;
entry = m_program_cache.emplace(m_lookup_key, Program {
.call_source = std::move(retained),
.resource_plan = ShaderRecompiler::IR::ExtractResourcePlan(translated.program),
}).first;
auto& program = entry->second;
EXIT_IF(!ShaderRecompiler::IR::MaterializeResources(
program.resource_plan, runtime, program.resources, program.specialization));
} else if (source_changed) {
entry->second.resource_plan = ShaderRecompiler::IR::ExtractResourcePlan(translated.program);
if (const auto* module = entry->second.FindModule<true>(runtime, push_data_cursor)) {
return bind(*module);
}
}
std::unordered_map<ProgramKey, SourceEntry, ProgramKeyHash> programs;
ProgramKey lookup_key;
GraphicContext& graphics;
ShaderRecompiler::IR::ImageHeapSource* image_heap;
const ShaderRecompiler::Spirv::Profile profile;
uint64_t next_shader_id = 0;
};
return bind(CompileModule(entry->second, entry->first, options, std::move(translated),
push_data_cursor));
}
PipelineCache::PipelineCache(GraphicContext& graphics,
ShaderRecompiler::IR::ImageHeapSource* image_heap)
: m_graphics(graphics), m_program_cache(std::make_unique<ProgramCache>(graphics, image_heap)),
ShaderRecompiler::IR::ImageHeapSource* image_heap,
const std::filesystem::path& shader_store)
: m_graphics(graphics), m_image_heap(image_heap),
m_next_driver_cache_save(std::chrono::steady_clock::now() + DriverCacheSaveInterval) {
EXIT_NOT_IMPLEMENTED(!Common::Thread::IsMainThread());
InitializeDriverCache();
WarmUp(shader_store);
}
PipelineCache::~PipelineCache() {
Save();
auto destroy = [this](const auto& pipelines) {
for (const auto& [key, pipeline]: pipelines) {
(void)key;
for (const auto& pipeline: pipelines | std::views::values) {
m_graphics.device.destroyPipeline(pipeline.pipeline, nullptr);
m_graphics.device.destroyPipelineLayout(pipeline.pipeline_layout, nullptr);
m_graphics.device.destroyDescriptorSetLayout(pipeline.descriptor_set_layout, nullptr);
@@ -510,6 +479,11 @@ PipelineCache::~PipelineCache() {
};
destroy(m_graphics_pipelines);
destroy(m_compute_pipelines);
for (const auto& program: m_program_cache | std::views::values) {
for (const auto& module: program.modules) {
m_graphics.device.destroyShaderModule(module.module, nullptr);
}
}
if (m_driver_cache != nullptr) {
m_graphics.device.destroyPipelineCache(m_driver_cache, nullptr);
}
@@ -695,6 +669,20 @@ void PipelineCache::GetGraphicsPrograms(const CommandBuffer& command, DrawRender
std::array<ShaderParams, 3> vertex_params;
if (tess_active) {
vertex_params = PrepareTessellationPrograms(vertex_regs, context, vertex_info);
// Reflection decodes both programs; the key also covers the VGT fields in tess.
const uint64_t layout_key[] = {vertex_params[0].hash, vertex_params[1].hash,
uint64_t {sh.m_vgtLsHsConfig & 0xfff00u} << 32u |
sh.m_vgtTfParam};
const auto [layout, inserted] = m_tess_layouts.try_emplace(
XXH3_64bits(layout_key, sizeof(layout_key)), vertex_info[0].tess);
if (inserted) {
ShaderRecompiler::AnalyzeTessellationPrograms(vertex_params[0].code,
vertex_params[1].code, layout->second);
StoreLayout(layout->first, layout->second);
}
for (auto& stage: vertex_info) {
stage.tess = layout->second;
}
} else {
vertex_params[0] = PrepareProgram(vertex_regs, context, user_config, vertex_info[0]);
}
@@ -723,8 +711,13 @@ void PipelineCache::GetGraphicsPrograms(const CommandBuffer& command, DrawRender
mesh.max_vertices > limits.maxMeshOutputVertices ||
mesh.max_primitives > limits.maxMeshOutputPrimitives ||
mesh.lds_size_dwords * sizeof(uint32_t) > limits.maxMeshSharedMemorySize) {
EXIT("mesh shader exceeds host limits: threads=%u vertices=%u primitives=%u LDS=%u\n",
host_threads, mesh.max_vertices, mesh.max_primitives, mesh.lds_size_dwords);
static std::atomic_bool warned = false;
if (!warned.exchange(true, std::memory_order_relaxed)) {
Log::WriteToConsoleAndLog(fmt::format(
"Warning: dropping mesh draws the host does not support (shader 0x{:016x}).\n",
vertex_params[0].hash));
}
return;
}
}
ShaderParams pixel_params;
@@ -748,6 +741,11 @@ void PipelineCache::GetGraphicsPrograms(const CommandBuffer& command, DrawRender
const auto& rt = context.GetRenderTarget(slot);
const auto target_mask = render_target_mask_slot(context.GetRenderTargetMask(), slot);
if (rt.base.addr == 0 || target_mask == 0u) continue;
if (rt.info.channel_type == Prospero::ChannelType::kUInt) {
pixel_info.target_uint_mask |= 1u << export_index;
} else if (rt.info.channel_type == Prospero::ChannelType::kSInt) {
pixel_info.target_sint_mask |= 1u << export_index;
}
const auto format = Prospero::ResolveRenderTargetFormat(rt.info.format, rt.info.channel_type);
if (format.buffer_format != Prospero::BufferFormat::k11_11_10UNorm) continue;
const auto mapping = pixel_info.target_export_mapping[export_index];
@@ -779,9 +777,11 @@ void PipelineCache::GetGraphicsPrograms(const CommandBuffer& command, DrawRender
(blend.separate_alpha_blend && (BlendFactorIsDualSource(blend.alpha_srcblend) ||
BlendFactorIsDualSource(blend.alpha_destblend))));
if (pixel_info.dual_source_blending) {
// MRT1 supplies the second blend source for target 0.
// MRT1 supplies the second blend source for target 0; blended targets are not integer.
pixel_info.target_output_mode[1] = pixel_info.target_output_mode[0];
pixel_info.target_export_mapping[1] = pixel_info.target_export_mapping[0];
pixel_info.target_uint_mask &= ~2u;
pixel_info.target_sint_mask &= ~2u;
} else if (blend.enable && !context.GetRenderTarget(0).info.blend_bypass &&
pixel_info.target_output_mode[0] != 0 && pixel_info.target_output_mode[0] != 7 &&
std::all_of(std::begin(pixel_info.target_output_mode) + 1,
@@ -832,8 +832,8 @@ void PipelineCache::GetGraphicsPrograms(const CommandBuffer& command, DrawRender
uint32_t push_data_cursor = ShaderRecompiler::IR::PushData::GraphicsDrawDwords(
mesh_active, object_primitive_id, last_stage.user_clip_planes);
if (pixel_active) {
m_program_cache->Get(pixel_params, pixel_info, push_data_cursor);
for (const auto& output: pixel_info.stage.program->info.outputs) {
const auto& pixel_module = *GetProgram(pixel_params, pixel_info, push_data_cursor);
for (const auto& output: pixel_module.info.info.outputs) {
if (output.kind == ShaderRecompiler::IR::StageOutputKind::Mrt ||
output.kind == ShaderRecompiler::IR::StageOutputKind::ObjectPrimitiveId) {
state.mrt_mask |= 1u << output.location;
@@ -842,52 +842,52 @@ void PipelineCache::GetGraphicsPrograms(const CommandBuffer& command, DrawRender
state.mrt_mask &= color_output_mask;
}
for (uint32_t i = 0; i < (tess_active ? 3u : 1u); i++) {
m_program_cache->Get(vertex_params[i], vertex_info[i], push_data_cursor);
GetProgram(vertex_params[i], vertex_info[i], push_data_cursor);
}
}
ShaderProgram PipelineCache::GetComputeProgram(const HW::ComputeShaderInfo& regs,
ShaderComputeInputInfo& input_info) {
const ShaderModule* PipelineCache::GetComputeProgram(const HW::ComputeShaderInfo& regs,
ShaderComputeInputInfo& input_info) {
input_info.host_subgroup_size = m_graphics.SupportsComputeWave64() ? 64u : 32u;
const auto params = PrepareProgram(regs, input_info);
input_info.lds_storage = input_info.lds_size_dwords * 4u >
m_graphics.GetPhysicalDeviceProperties().limits.maxComputeSharedMemorySize;
uint32_t push_data_cursor = 0;
return m_program_cache->Get(params, input_info, push_data_cursor);
}
bool PipelineStaticParameters::operator==(const PipelineStaticParameters& other) const noexcept {
return std::memcmp(this, &other, sizeof(*this)) == 0;
return GetProgram(params, input_info, push_data_cursor);
}
PipelineCache::Pipeline& PipelineCache::GetGraphicsPipeline(const CommandBuffer& command,
const DrawRenderState& state) {
const auto* colors = state.color_info;
const auto& depth = state.depth_info;
const auto vertex_info = state.VertexStages();
const auto& ps_input_info = state.ps_input_info;
const auto& vs_input_info = vertex_info.front();
const auto* vertex_program = vs_input_info.stage.handle;
const auto* pixel_program = state.ps_input_info.stage.handle;
const auto* colors = state.color_info;
const auto& depth = state.depth_info;
const auto vertex_info = state.VertexStages();
const auto& ps_input_info = state.ps_input_info;
const auto& vs_input_info = vertex_info.front();
KYTY_PROFILER_BLOCK("PipelineCache::CreatePipeline(Gfx)", profiler::colors::DeepOrangeA200);
EXIT_IF(vertex_program == nullptr);
const bool ps_active = state.ps_active;
EXIT_IF(ps_active && pixel_program == nullptr);
const bool ps_active = state.ps_active;
const bool tessellation = vertex_info.size() == 3;
std::array<const ShaderModule*, 4> stages {};
for (uint32_t i = 0; i < vertex_info.size(); i++) {
stages[i] = vertex_info[i].stage.module;
}
if (ps_active) {
stages[3] = ps_input_info.stage.module;
}
EXIT_IF(stages[0] == nullptr || (ps_active && stages[3] == nullptr));
const bool mesh = stages[0]->info.stage == ShaderType::Mesh;
const auto color_count = state.color_count;
auto& ctx = command.GetRegisters();
const HW::ModeControl& mc = ctx.GetModeControl();
const auto vs_id = vertex_program->id;
const auto ps_id = ps_active ? pixel_program->id : 0;
GraphicsPipelineKey key {};
for (uint32_t i = 0; i < vertex_info.size(); i++) {
key.vertex_shader_ids[i] = vertex_info[i].stage.handle->id;
for (uint32_t i = 0; i < stages.size(); i++) {
if (stages[i] != nullptr) {
key.stage_ids[i] = stages[i]->id;
}
}
key.ps_shader_id = ps_id;
auto& static_params = key.static_params;
auto& rendering = key.rendering;
rendering.color_count = 0;
@@ -1009,8 +1009,15 @@ PipelineCache::Pipeline& PipelineCache::GetGraphicsPipeline(const CommandBuffer&
static_params.provoking_vtx_last = mc.provoking_vtx_last;
static_params.polygon_mode =
ResolvePolygonMode(mc, static_params.cull_front, static_params.cull_back);
if (tessellation) {
static_params.patch_control_points =
static_cast<uint8_t>(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;
}
if (vs_input_info.stage.program->stage != ShaderType::Mesh) {
if (!mesh) {
EXIT_IF(vs_input_info.buffers_num < 0 ||
vs_input_info.buffers_num > ShaderVertexInputInfo::RES_MAX ||
vs_input_info.resources_num < 0 ||
@@ -1045,22 +1052,17 @@ PipelineCache::Pipeline& PipelineCache::GetGraphicsPipeline(const CommandBuffer&
ShaderDbgDumpInputInfo(ps_input_info);
}
LOGF("PipelineTrace: shader modules VS=%" PRIu64 " module=%p PS=%" PRIu64 " module=%p\n",
vs_id, static_cast<void*>(vertex_program->module), ps_id,
ps_active ? static_cast<void*>(pixel_program->module) : nullptr);
key.stage_ids[0], static_cast<void*>(stages[0]->module), key.stage_ids[3],
ps_active ? static_cast<void*>(stages[3]->module) : nullptr);
}
LogPipelineTrace("CreatePipelineInternal begin", vs_id, ps_id);
{
Common::DebugCounters::ScopedTimer timer(Common::DebugCounters::Counter::PipelinesCreated,
Common::DebugCounters::Counter::PipelineCreateNs,
Common::DebugCounters::Peak::PipelineCreateNs);
CreatePipelineInternal(m_graphics, pipeline, rendering, key.vertex_input, state,
static_params, m_driver_cache);
CreatePipelineInternal(m_graphics, pipeline, key, stages, m_driver_cache);
}
LogPipelineTrace("CreatePipelineInternal done", vs_id, ps_id);
EXIT_NOT_IMPLEMENTED(pipeline.pipeline == nullptr);
EXIT_NOT_IMPLEMENTED(pipeline.pipeline_layout == nullptr);
StorePipeline(key);
CheckpointDriverCache();
return pipeline;
@@ -1069,10 +1071,10 @@ PipelineCache::Pipeline& PipelineCache::GetGraphicsPipeline(const CommandBuffer&
PipelineCache::Pipeline& PipelineCache::GetComputePipeline(const ShaderComputeInputInfo& input_info) {
KYTY_PROFILER_BLOCK("PipelineCache::CreatePipeline(Compute)", profiler::colors::RedA100);
EXIT_IF(input_info.stage.handle == nullptr);
const auto& compute_program = *input_info.stage.handle;
EXIT_IF(input_info.stage.module == nullptr);
const auto& module = *input_info.stage.module;
auto [iter, inserted] = m_compute_pipelines.try_emplace(compute_program.id);
auto [iter, inserted] = m_compute_pipelines.try_emplace(module.id);
auto& pipeline = iter->second;
if (!inserted) {
return pipeline;
@@ -1086,12 +1088,8 @@ PipelineCache::Pipeline& PipelineCache::GetComputePipeline(const ShaderComputeIn
Common::DebugCounters::ScopedTimer timer(Common::DebugCounters::Counter::PipelinesCreated,
Common::DebugCounters::Counter::PipelineCreateNs,
Common::DebugCounters::Peak::PipelineCreateNs);
CreatePipelineInternal(m_graphics, pipeline, input_info, compute_program.module,
m_driver_cache);
CreatePipelineInternal(m_graphics, pipeline, module, m_driver_cache);
}
EXIT_NOT_IMPLEMENTED(pipeline.pipeline == nullptr);
EXIT_NOT_IMPLEMENTED(pipeline.pipeline_layout == nullptr);
CheckpointDriverCache();
return pipeline;
@@ -4,35 +4,57 @@
#include "common/abi.h"
#include "common/assert.h"
#include "common/common.h"
#include "common/file.h"
#include "common/uniqueFunction.h"
#include "graphics/host_gpu/renderer/colorRenderTarget.h"
#include "graphics/host_gpu/renderer/depthRenderTarget.h"
#include "graphics/host_gpu/renderer/renderTarget.h"
#include "graphics/host_gpu/vulkanCommon.h"
#include "graphics/shader/recompiler/ir/passes/ResourceMaterialization.h"
#include "graphics/shader/rectListShader.h"
#include "graphics/shader/shader.h"
#include <chrono>
#include <condition_variable>
#include <cstddef>
#include <filesystem>
#include <memory>
#include <mutex>
#include <queue>
#include <span>
#include <thread>
#include <type_traits>
#include <unordered_map>
#include <vector>
namespace Libs::Graphics {
struct GraphicContext;
struct ShaderParams;
class CommandBuffer;
namespace ShaderRecompiler {
struct CompileOptions;
struct ShaderSource;
struct TranslateResult;
} // namespace ShaderRecompiler
namespace ShaderRecompiler::IR {
class ImageHeapSource;
} // namespace ShaderRecompiler::IR
namespace ShaderRecompiler::Spirv {
struct Profile;
} // namespace ShaderRecompiler::Spirv
#pragma pack(push, 1)
struct PipelineStaticParameters {
bool negative_one_to_one = false;
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;
bool primitive_restart_enable = false;
uint32_t samples = 1;
bool sample_shading_enable = false;
@@ -51,51 +73,59 @@ struct PipelineStaticParameters {
uint8_t alpha_destblend[RENDER_COLOR_ATTACHMENTS_MAX] = {};
bool separate_alpha_blend[RENDER_COLOR_ATTACHMENTS_MAX] = {};
bool blend_enable[RENDER_COLOR_ATTACHMENTS_MAX] = {};
bool operator==(const PipelineStaticParameters& other) const noexcept;
};
#pragma pack(pop)
static_assert(std::is_trivially_copyable_v<PipelineStaticParameters>);
static_assert(std::is_standard_layout_v<PipelineStaticParameters>);
static_assert(alignof(PipelineStaticParameters) == 1);
static_assert(sizeof(PipelineStaticParameters) == 117);
struct PipelineRenderingState {
std::array<vk::Format, RENDER_COLOR_ATTACHMENTS_MAX> color_formats {};
vk::Format depth_format = vk::Format::eUndefined;
vk::Format stencil_format = vk::Format::eUndefined;
uint32_t color_count = 0;
bool operator==(const PipelineRenderingState&) const = default;
};
struct PipelineVertexInputState {
struct Binding {
uint32_t stride = 0;
bool instance = false;
bool operator==(const Binding&) const = default;
uint32_t stride = 0;
uint8_t instance = 0;
};
struct Attribute {
uint32_t offset = 0;
uint8_t binding = 0;
bool operator==(const Attribute&) const = default;
uint32_t offset = 0;
uint8_t binding = 0;
};
std::array<Binding, ShaderVertexInputInfo::RES_MAX> bindings {};
std::array<Attribute, ShaderVertexInputInfo::RES_MAX> attributes {};
uint8_t binding_count = 0;
uint8_t attribute_count = 0;
bool operator==(const PipelineVertexInputState&) const = default;
};
struct ShaderProgram {
uint64_t id = 0;
vk::ShaderModule module = nullptr;
// Everything a graphics pipeline is made of besides the stage modules its ids name. Aligned so
// that rendering.color_formats can be passed to Vulkan.
struct alignas(4) GraphicsPipelineKey {
// VS, LS or mesh; HS; TES; PS. 0 marks an absent stage.
std::array<uint64_t, 4> stage_ids {};
PipelineRenderingState rendering;
PipelineVertexInputState vertex_input;
PipelineStaticParameters static_params;
explicit operator bool() const { return id != 0 && module != nullptr; }
bool operator==(const GraphicsPipelineKey& other) const;
};
#pragma pack(pop)
// Keys are compared and hashed as bytes.
static_assert(std::has_unique_object_representations_v<GraphicsPipelineKey>);
// A compiled stage with what pipeline creation needs from it besides the key.
struct ShaderModule {
uint64_t id = 0;
vk::ShaderModule module = nullptr;
ShaderRecompiler::IR::CompiledShaderInfo info;
ShaderRecompiler::IR::ResourceSpecialization specialization;
std::vector<uint32_t> function_code;
// VS or LS: the host format of each vertex input attribute.
std::vector<vk::Format> vertex_formats;
// Rectangle-list PS: the parameters it reads.
std::vector<RectListParameter> rect_parameters;
};
// A draw's topology, shader stages (from PipelineCache::GetGraphicsPrograms) and targets.
@@ -116,10 +146,35 @@ struct DrawRenderState {
}
};
// Writes the shader store on its own thread, running jobs in the order they were posted.
class ShaderStoreWriter {
public:
// Keeps the first `kept` bytes of the store at `path`, or starts it over with `header`. The
// thread runs only if that succeeded.
void Open(const std::filesystem::path& path, std::span<const uint8_t> header, uint64_t kept);
explicit operator bool() const { return m_thread.joinable(); }
// Compresses `record` and appends it on the store thread, in call order.
void Append(std::vector<uint8_t>&& record);
private:
friend struct PipelineCacheTestAccess;
void Post(Common::UniqueFunction<void>&& job);
Common::File m_file;
std::mutex m_mutex;
std::condition_variable_any m_ready;
std::queue<Common::UniqueFunction<void>> m_jobs;
// Declared last, so it drains the queue and joins before the rest is destroyed.
std::jthread m_thread;
};
// The owning renderer serializes access, including saves while the GPU is running.
class PipelineCache {
public:
PipelineCache(GraphicContext& graphics, ShaderRecompiler::IR::ImageHeapSource* image_heap);
// Without a shader store path nothing is kept across runs.
PipelineCache(GraphicContext& graphics, ShaderRecompiler::IR::ImageHeapSource* image_heap,
const std::filesystem::path& shader_store = {});
~PipelineCache();
KYTY_CLASS_NO_COPY(PipelineCache);
void Save();
@@ -134,64 +189,104 @@ public:
// Reads the shaders and registers bound in `command` into the state's shader stages.
void GetGraphicsPrograms(const CommandBuffer& command, DrawRenderState& state);
// An empty program skips the dispatch.
ShaderProgram GetComputeProgram(const HW::ComputeShaderInfo& regs,
ShaderComputeInputInfo& input_info);
// Null skips the dispatch.
const ShaderModule* GetComputeProgram(const HW::ComputeShaderInfo& regs,
ShaderComputeInputInfo& input_info);
Pipeline& GetGraphicsPipeline(const CommandBuffer& command, const DrawRenderState& state);
Pipeline& GetComputePipeline(const ShaderComputeInputInfo& input_info);
private:
struct ProgramCache;
friend struct PipelineCacheTestAccess;
struct GraphicsPipelineKey {
PipelineRenderingState rendering;
std::array<uint64_t, 3> vertex_shader_ids {};
uint64_t ps_shader_id = 0;
PipelineVertexInputState vertex_input;
PipelineStaticParameters static_params;
struct Program {
std::unique_ptr<ShaderRecompiler::ShaderSource> call_source;
ShaderRecompiler::IR::ResourcePlan resource_plan;
ShaderRecompiler::IR::ResourceSnapshot resources;
ShaderRecompiler::IR::ResourceSpecialization specialization;
std::vector<ShaderModule> modules;
bool skip_dispatch = false;
bool operator==(const GraphicsPipelineKey& other) const {
return rendering == other.rendering && vertex_shader_ids == other.vertex_shader_ids &&
ps_shader_id == other.ps_shader_id && vertex_input == other.vertex_input &&
static_params == other.static_params;
}
[[nodiscard]] std::span<const uint32_t> FunctionCode() const;
bool RefreshCallSource(const ShaderRecompiler::IR::SrtRuntime& runtime);
template <bool HasCalls>
ShaderModule* FindModule(const ShaderRecompiler::IR::SrtRuntime& runtime,
uint32_t push_data_cursor);
};
struct PipelineKeyHash {
static void Mix(std::size_t& hash, std::size_t value) {
hash ^= value + static_cast<std::size_t>(0x9e3779b97f4a7c15ull) + (hash << 6u) +
(hash >> 2u);
}
struct ProgramKey {
ShaderType stage = ShaderType::Unknown;
uint64_t hash = 0;
uint32_t user_data_count = 0;
std::vector<uint32_t> static_state;
bool operator==(const ProgramKey&) const = default;
};
// Variants of one source share a bucket; ProgramKey equality compares the state.
struct ProgramKeyHash {
std::size_t operator()(const ProgramKey& key) const { return key.hash; }
};
struct GraphicsPipelineKeyHash {
std::size_t operator()(const GraphicsPipelineKey& key) const;
};
GraphicContext& m_graphics;
std::unique_ptr<ProgramCache> m_program_cache;
using ModuleMap = std::unordered_map<uint64_t, const ShaderModule*>;
GraphicContext& m_graphics;
ShaderRecompiler::IR::ImageHeapSource* m_image_heap;
std::unordered_map<ProgramKey, Program, ProgramKeyHash> m_program_cache;
// Modules compiled per stage.
std::array<uint32_t, static_cast<size_t>(ShaderType::TessellationEvaluation) + 1> m_compiled {};
ProgramKey m_lookup_key;
vk::PipelineCache m_driver_cache = nullptr;
std::filesystem::path m_driver_cache_path;
std::chrono::steady_clock::time_point m_next_driver_cache_save;
bool m_driver_cache_dirty = false;
std::unordered_map<GraphicsPipelineKey, Pipeline, GraphicsPipelineKeyHash> m_graphics_pipelines;
std::unordered_map<uint64_t, Pipeline> m_compute_pipelines;
std::unordered_map<uint64_t, ShaderTessellationInputInfo> m_tess_layouts;
// Records everything first created; closed without a shader store.
ShaderStoreWriter m_store;
template <typename InputInfo>
const ShaderModule* GetProgram(const ShaderParams& params, InputInfo& input_info,
uint32_t& push_data_cursor);
ShaderModule& CompileModule(Program& program, const ProgramKey& key,
ShaderRecompiler::CompileOptions options,
ShaderRecompiler::TranslateResult translated,
uint32_t push_data_cursor);
void InitializeDriverCache();
void CheckpointDriverCache();
// pipelineSerialization.cpp
void WarmUp(const std::filesystem::path& path);
bool LoadRecord(std::span<const uint8_t>& records, std::vector<GraphicsPipelineKey>& pipelines);
void StoreModule(const ProgramKey& key, const Program& program, const ShaderModule& module,
const std::vector<uint32_t>& spirv);
void StorePipeline(const GraphicsPipelineKey& key);
void StoreLayout(uint64_t key, const ShaderTessellationInputInfo& layout);
// Every module by id, with id 0 naming an absent stage.
[[nodiscard]] ModuleMap MapModules() const;
// False when a stage id of the key is not in `modules`.
bool CreateGraphicsPipeline(Pipeline& pipeline, const GraphicsPipelineKey& key,
const ModuleMap& modules);
};
void LogPipelineTrace(const char* phase, uint64_t vertex_program_id, uint64_t pixel_program_id);
// The shader store of the running title; empty unless --shader-cache is on in a Release build
// without debug dumps, which change the SPIR-V.
std::filesystem::path ShaderStorePath();
// Host capabilities that change the emitted SPIR-V.
ShaderRecompiler::Spirv::Profile SpirvProfile(const GraphicContext& graphics);
// Stages are indexed like GraphicsPipelineKey::stage_ids.
void CreatePipelineInternal(GraphicContext& graphics, PipelineCache::Pipeline& pipeline,
const PipelineRenderingState& rendering,
const PipelineVertexInputState& vertex_input,
const DrawRenderState& state,
const PipelineStaticParameters& static_params,
vk::PipelineCache driver_cache);
const GraphicsPipelineKey& key,
const std::array<const ShaderModule*, 4>& stages,
vk::PipelineCache driver_cache);
void CreatePipelineInternal(GraphicContext& graphics, PipelineCache::Pipeline& pipeline,
const ShaderComputeInputInfo& input_info,
vk::ShaderModule compute_module, vk::PipelineCache driver_cache);
const ShaderModule& module, vk::PipelineCache driver_cache);
} // namespace Libs::Graphics
@@ -0,0 +1,329 @@
#include "graphics/host_gpu/renderer/pipeline/pipelineCache.h"
#include "common/logging/log.h"
#include "common/stringUtils.h"
#include "graphics/host_gpu/vulkanCommon.h"
#include "graphics/shader/recompiler/ShaderRecompiler.h"
#include "graphics/shader/recompiler/ir/Serialize.h"
#include <algorithm>
#include <chrono>
#include <cstring>
#include <fmt/format.h>
#include <ranges>
#include <xxhash.h>
#include <zstd.h>
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#elif defined(__APPLE__)
#include <mach-o/dyld.h>
#endif
// The shader store: a header, then zstd-compressed records of everything a run created first.
// Records are only appended, so a store cut short by a crash keeps every complete record.
namespace Libs::Graphics {
namespace {
using ShaderRecompiler::IR::IoAll;
using ShaderRecompiler::IR::Reader;
using ShaderRecompiler::IR::Writer;
enum class Record : uint8_t { Module, GraphicsPipeline, TessellationLayout };
// Precedes each record's zstd frame.
struct Frame {
uint32_t compressed_size = 0;
uint32_t raw_size = 0;
uint64_t checksum = 0; // XXH3 of the compressed bytes.
};
// Changes with every build, so records never meet code that encodes them differently. Zero when
// the executable cannot be found.
uint64_t BuildStamp() {
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS
std::array<wchar_t, 32768> name {};
GetModuleFileNameW(nullptr, name.data(), static_cast<DWORD>(name.size()));
const std::filesystem::path path = name.data();
#elif defined(__APPLE__)
std::array<char, 4096> name {};
auto size = static_cast<uint32_t>(name.size());
_NSGetExecutablePath(name.data(), &size);
const std::filesystem::path path = name.data();
#else
const std::filesystem::path path = "/proc/self/exe";
#endif
std::error_code error;
const auto time = std::filesystem::last_write_time(path, error).time_since_epoch().count();
return error ? 0 : static_cast<uint64_t>(time);
}
// What every record depends on besides its own fields; empty when the build is unknown.
std::vector<uint8_t> StoreHeader(const GraphicContext& graphics) {
const auto stamp = BuildStamp();
if (stamp == 0) {
return {};
}
Writer header;
IoAll(header, stamp, SpirvProfile(graphics),
graphics.GetPhysicalDeviceProperties().limits.maxComputeSharedMemorySize);
return std::move(header.bytes);
}
// Record fields, shared by writing and reading.
template <class S, class Key> void IoKey(S& io, Key& key) {
auto& [stage, hash, user_data_count, static_state] = key;
IoAll(io, stage, hash, user_data_count, static_state);
}
template <class S, class Module> void IoModule(S& io, Module& stage) {
// The handle comes from the SPIR-V; stored programs have no function code.
auto& [id, handle, info, specialization, function_code, vertex_formats, rect_parameters] = stage;
IoAll(io, id, info, specialization, vertex_formats, rect_parameters);
}
} // namespace
void ShaderStoreWriter::Open(const std::filesystem::path& path, std::span<const uint8_t> header,
uint64_t kept) {
Common::File::CreateDirectories(path.parent_path());
if (kept == 0) {
if (m_file.Create(path)) {
m_file.Write(header.data(), static_cast<uint32_t>(header.size()));
}
} else if (m_file.Open(path, Common::File::Mode::ReadWrite)) {
m_file.Truncate(kept);
m_file.Seek(kept);
}
if (m_file.IsInvalid()) {
return;
}
m_thread = std::jthread([this](std::stop_token stop) {
std::unique_lock lock(m_mutex);
// Once stop is requested the wait returns false only when the queue is empty.
while (m_ready.wait(lock, stop, [this] { return !m_jobs.empty(); })) {
const auto job = std::move(m_jobs.front());
m_jobs.pop();
lock.unlock();
job();
lock.lock();
}
});
}
void ShaderStoreWriter::Append(std::vector<uint8_t>&& record) {
Post([this, record = std::move(record)] {
std::vector<uint8_t> bytes(sizeof(Frame) + ZSTD_compressBound(record.size()));
const auto size = ZSTD_compress(bytes.data() + sizeof(Frame), bytes.size() - sizeof(Frame),
record.data(), record.size(), 3);
EXIT_IF(ZSTD_isError(size));
const Frame frame {.compressed_size = static_cast<uint32_t>(size),
.raw_size = static_cast<uint32_t>(record.size()),
.checksum = XXH3_64bits(bytes.data() + sizeof(Frame), size)};
std::memcpy(bytes.data(), &frame, sizeof(frame));
// A failed write leaves a partial record; the next run drops it and everything after it.
m_file.Write(bytes.data(), static_cast<uint32_t>(sizeof(Frame) + size));
});
}
void ShaderStoreWriter::Post(Common::UniqueFunction<void>&& job) {
{
std::scoped_lock lock(m_mutex);
m_jobs.push(std::move(job));
}
m_ready.notify_one();
}
void PipelineCache::WarmUp(const std::filesystem::path& path) {
if (path.empty()) {
return;
}
const auto header = StoreHeader(m_graphics);
if (header.empty()) {
return;
}
const auto start = std::chrono::steady_clock::now();
std::vector<uint8_t> bytes;
if (Common::File file;
Common::File::IsFileExisting(path) && file.Open(path, Common::File::Mode::Read)) {
bytes.resize(file.Size());
uint32_t read = 0;
file.Read(bytes.data(), static_cast<uint32_t>(bytes.size()), &read);
bytes.resize(read);
}
Log::WriteToConsoleAndLog(
fmt::format("Shader cache: compiling shaders and pipelines from {} ({:.1f} MB)\n",
Common::PathToString(path), static_cast<double>(bytes.size()) / 1'000'000.0));
std::vector<GraphicsPipelineKey> pipelines;
auto records = std::span<const uint8_t>(bytes);
const bool reusable =
records.size() >= header.size() && std::ranges::equal(records.first(header.size()), header);
if (reusable) {
records = records.subspan(header.size());
while (LoadRecord(records, pipelines)) {
}
}
// A damaged record ends the store; the records before it stay.
m_store.Open(path, header, reusable ? bytes.size() - records.size() : 0);
const auto modules = MapModules();
for (const auto& key: pipelines) {
Pipeline pipeline;
if (CreateGraphicsPipeline(pipeline, key, modules)) {
m_graphics_pipelines.emplace(key, pipeline);
}
}
// A compute pipeline depends only on its module.
for (const auto* module: modules | std::views::values) {
if (module != nullptr && module->info.stage == ShaderType::Compute) {
CreatePipelineInternal(m_graphics, m_compute_pipelines[module->id], *module,
m_driver_cache);
}
}
if (!m_graphics_pipelines.empty() || !m_compute_pipelines.empty()) {
CheckpointDriverCache();
}
Log::WriteToConsoleAndLog(fmt::format(
"Shader cache: loaded {} programs, {} modules, {} pipelines in {} ms\n",
m_program_cache.size(), modules.size() - 1,
m_graphics_pipelines.size() + m_compute_pipelines.size(),
std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::steady_clock::now() -
start)
.count()));
}
bool PipelineCache::LoadRecord(std::span<const uint8_t>& records,
std::vector<GraphicsPipelineKey>& pipelines) {
Frame frame;
if (records.size() < sizeof(frame)) {
return false;
}
std::memcpy(&frame, records.data(), sizeof(frame));
if (records.size() - sizeof(frame) < frame.compressed_size) {
return false;
}
const auto compressed = records.subspan(sizeof(frame), frame.compressed_size);
if (XXH3_64bits(compressed.data(), compressed.size()) != frame.checksum ||
ZSTD_getFrameContentSize(compressed.data(), compressed.size()) != frame.raw_size) {
return false;
}
std::vector<uint8_t> raw(frame.raw_size);
if (ZSTD_decompress(raw.data(), raw.size(), compressed.data(), compressed.size()) != raw.size()) {
return false;
}
Reader reader {raw};
auto type = Record::Module;
const auto complete = [&] { return reader.ok && reader.bytes.empty(); };
IoAll(reader, type);
switch (type) {
case Record::Module: {
ProgramKey key;
std::vector<uint8_t> plan;
ShaderModule module;
std::vector<uint32_t> spirv;
IoKey(reader, key);
IoAll(reader, plan);
IoModule(reader, module);
IoAll(reader, spirv);
if (!complete()) {
return false;
}
const auto [program, inserted] = m_program_cache.try_emplace(std::move(key));
if (inserted &&
!ShaderRecompiler::IR::LoadResourcePlan(plan, program->second.resource_plan)) {
m_program_cache.erase(program);
return false;
}
module.module = CompileSPV(spirv, m_graphics.device);
if (module.module == nullptr) {
return false;
}
program->second.modules.push_back(std::move(module));
break;
}
case Record::GraphicsPipeline: {
GraphicsPipelineKey key;
reader.Raw(&key, sizeof(key));
if (!complete()) {
return false;
}
pipelines.push_back(key);
break;
}
case Record::TessellationLayout: {
uint64_t key = 0;
ShaderTessellationInputInfo layout;
IoAll(reader, key, layout);
if (!complete()) {
return false;
}
m_tess_layouts.try_emplace(key, layout);
break;
}
default: return false;
}
records = records.subspan(sizeof(frame) + compressed.size());
return true;
}
// Records are encoded here, since programs and modules change later.
void PipelineCache::StoreModule(const ProgramKey& key, const Program& program,
const ShaderModule& module, const std::vector<uint32_t>& spirv) {
if (!m_store) {
return;
}
Writer record;
IoAll(record, Record::Module);
IoKey(record, key);
IoAll(record, ShaderRecompiler::IR::SaveResourcePlan(program.resource_plan));
IoModule(record, module);
IoAll(record, spirv);
m_store.Append(std::move(record.bytes));
}
void PipelineCache::StorePipeline(const GraphicsPipelineKey& key) {
if (!m_store) {
return;
}
Writer record;
IoAll(record, Record::GraphicsPipeline);
record.Raw(&key, sizeof(key));
m_store.Append(std::move(record.bytes));
}
void PipelineCache::StoreLayout(uint64_t key, const ShaderTessellationInputInfo& layout) {
if (!m_store) {
return;
}
Writer record;
IoAll(record, Record::TessellationLayout, key, layout);
m_store.Append(std::move(record.bytes));
}
PipelineCache::ModuleMap PipelineCache::MapModules() const {
ModuleMap modules {{0, nullptr}};
for (const auto& program: m_program_cache | std::views::values) {
for (const auto& module: program.modules) {
modules.emplace(module.id, &module);
}
}
return modules;
}
bool PipelineCache::CreateGraphicsPipeline(Pipeline& pipeline, const GraphicsPipelineKey& key,
const ModuleMap& modules) {
std::array<const ShaderModule*, 4> stages {};
for (size_t i = 0; i < stages.size(); i++) {
const auto found = modules.find(key.stage_ids[i]);
if (found == modules.end()) {
return false;
}
stages[i] = found->second;
}
CreatePipelineInternal(m_graphics, pipeline, key, stages, m_driver_cache);
return true;
}
} // namespace Libs::Graphics
@@ -1,6 +1,7 @@
#include "graphics/host_gpu/renderer/pipeline/shaderResourceBarrier.h"
#include "common/assert.h"
#include "graphics/host_gpu/renderer/pipeline/pipelineCache.h"
#include "graphics/shader/shader.h"
#include "graphics/shader/shaderBindings.h"
@@ -90,7 +91,7 @@ vk::BufferMemoryBarrier MakeGdsDependency(vk::Buffer buffer) {
bool HasShaderBufferWrites(const ShaderStageRuntime& runtime) {
EXIT_IF(!runtime);
const auto& program = *runtime.program;
const auto& program = runtime.module->info;
const auto& resources = *runtime.resources;
EXIT_IF(resources.buffers.size() != program.info.buffers.size());
// Address writes reach memory outside the bound buffer descriptors.
@@ -13,7 +13,6 @@
#include "graphics/host_gpu/renderer/renderContext.h"
#include "graphics/host_gpu/renderer/renderTarget.h"
#include "graphics/host_gpu/vulkanCommon.h"
#include "graphics/shader/recompiler/BufferFormat.h"
#include "graphics/shader/recompiler/ir/ShaderIR.h"
#include "graphics/shader/rectListShader.h"
#include "graphics/shader/shader.h"
@@ -21,124 +20,11 @@
#include <algorithm>
#include <array>
#include <bit>
#include <limits>
#include <span>
#include <vector>
namespace Libs::Graphics {
// IDK: maybe we can remove it?
constexpr uint8_t kTemporaryVertexAttribFormat113 =
static_cast<uint8_t>(Prospero::VertexAttribFormat::k16_16SInt);
constexpr uint32_t kTemporaryPs5BufferFormat121 = 121u;
static bool NarrowInputFormat(vk::Format& format, uint32_t& size, uint32_t used_components) {
if (used_components == 0 || used_components >= size) {
return false;
}
switch (format) {
case vk::Format::eR32G32B32A32Sfloat:
switch (used_components) {
case 1: format = vk::Format::eR32Sfloat; break;
case 2: format = vk::Format::eR32G32Sfloat; break;
case 3: format = vk::Format::eR32G32B32Sfloat; break;
default: return false;
}
size = used_components;
return true;
case vk::Format::eR32G32B32Sfloat:
switch (used_components) {
case 1: format = vk::Format::eR32Sfloat; break;
case 2: format = vk::Format::eR32G32Sfloat; break;
default: return false;
}
size = used_components;
return true;
case vk::Format::eR16G16B16A16Sfloat:
switch (used_components) {
case 1: format = vk::Format::eR16Sfloat; break;
case 2: format = vk::Format::eR16G16Sfloat; break;
default: return false;
}
size = used_components;
return true;
case vk::Format::eR8G8B8A8Unorm:
switch (used_components) {
case 1: format = vk::Format::eR8Unorm; break;
case 2: format = vk::Format::eR8G8Unorm; break;
default: return false;
}
size = used_components;
return true;
case vk::Format::eR8G8B8A8Snorm:
if (used_components != 2) {
return false;
}
format = vk::Format::eR8G8Snorm;
size = 2;
return true;
case vk::Format::eR8G8B8A8Uint:
switch (used_components) {
case 1: format = vk::Format::eR8Uint; break;
case 2: format = vk::Format::eR8G8Uint; break;
default: return false;
}
size = used_components;
return true;
default: break;
}
return false;
}
static void GetInputFormat(const ShaderBufferResource& res, vk::Format& format, uint32_t& size,
uint32_t used_components) {
const auto fmt = res.Format();
const auto raw_format = res.RawFormat();
if (raw_format == kTemporaryVertexAttribFormat113) {
static bool logged_113 = false;
if (!logged_113) {
LOGF("InputFormat: temporary: accepting invalid PS5 buffer format 113 as "
"vk::Format::eR32G32B32A32Sfloat\n");
logged_113 = true;
}
format = vk::Format::eR32G32B32A32Sfloat;
size = 4;
if (NarrowInputFormat(format, size, used_components)) {
LOGF("InputFormat: narrowing fmt=%u to %s for used_components=%u\n", raw_format,
vk::to_string(format).c_str(), used_components);
}
return;
}
if (raw_format == kTemporaryPs5BufferFormat121) {
static bool logged_121 = false;
if (!logged_121) {
LOGF("InputFormat: accepting PS5 buffer format 121 as vk::Format::eR16G16Sfloat\n");
logged_121 = true;
}
format = vk::Format::eR16G16Sfloat;
size = 2;
return;
}
format = VulkanFormat(fmt);
size = ShaderRecompiler::Format::GetFormatInfo(fmt).component_count;
if (format == vk::Format::eUndefined || size == 0) {
EXIT("unknown vertex format: fmt = %u\n", raw_format);
}
if (NarrowInputFormat(format, size, used_components)) {
static std::atomic<uint64_t> log_count = 0;
auto log_id = log_count.fetch_add(1);
if (log_id < 32) {
LOGF("VertexInput: narrowed vertex format to %" PRIu32
" component(s) for shader fetch\n",
used_components);
}
}
}
static vk::BlendFactor GetBlendFactor(uint32_t factor) {
switch (static_cast<Prospero::BlendFactor>(factor)) {
case Prospero::BlendFactor::kZero: return vk::BlendFactor::eZero;
@@ -242,30 +128,27 @@ static void CreateDescriptorLayout(GraphicContext& graphics, PipelineCache::Pipe
// NOLINTNEXTLINE(readability-function-cognitive-complexity)
void CreatePipelineInternal(GraphicContext& graphics, PipelineCache::Pipeline& pipeline,
const PipelineRenderingState& rendering,
const PipelineVertexInputState& vertex_input,
const DrawRenderState& state,
const PipelineStaticParameters& static_params,
vk::PipelineCache driver_cache) {
const auto vertex_info = state.VertexStages();
const auto& vs_input_info = vertex_info.front();
const auto* ps_input_info = state.ps_active ? &state.ps_input_info : nullptr;
const auto& vertex_program = *vs_input_info.stage.handle;
const auto* pixel_program = state.ps_input_info.stage.handle;
const bool tessellation = vertex_info.size() == 3;
const bool ps_active = state.ps_active;
const bool with_depth = rendering.depth_format != vk::Format::eUndefined ||
rendering.stencil_format != vk::Format::eUndefined;
const bool mesh = vs_input_info.stage.program->stage == ShaderType::Mesh;
const GraphicsPipelineKey& key,
const std::array<const ShaderModule*, 4>& stages,
vk::PipelineCache driver_cache) {
const auto& rendering = key.rendering;
const auto& vertex_input = key.vertex_input;
const auto& static_params = key.static_params;
const auto& vertex = *stages[0];
const auto* pixel = stages[3];
const bool with_depth = rendering.depth_format != vk::Format::eUndefined ||
rendering.stencil_format != vk::Format::eUndefined;
const bool mesh = vertex.info.stage == ShaderType::Mesh;
EXIT_NOT_IMPLEMENTED(mesh && !graphics.mesh_shader_enabled);
const bool rect_list =
!mesh && !tessellation && static_params.topology == vk::PrimitiveTopology::ePatchList;
const bool rect_list = static_params.patch_control_points != 0 && stages[1] == nullptr;
vk::ShaderModule tess_control_shader_module = nullptr;
vk::ShaderModule tess_eval_shader_module = nullptr;
if (rect_list) {
const auto shaders = BuildRectListShaders(vs_input_info, ps_input_info);
std::span<const RectListParameter> parameters;
if (pixel != nullptr) parameters = pixel->rect_parameters;
const auto shaders = BuildRectListShaders(parameters, vertex.info.param_export_mask);
tess_control_shader_module = CompileSPV(shaders.control, graphics.device);
if (graphics_debug_dump_enabled()) {
LOGF("PipelineTrace: vkCreateShaderModule RectList TCS done module=%p\n",
@@ -284,11 +167,12 @@ void CreatePipelineInternal(GraphicContext& graphics, PipelineCache::Pipeline& p
vk::PipelineShaderStageCreateInfo shader_stages[4] {};
uint32_t shader_stage_count = 0;
for (uint32_t i = 0; i < vertex_info.size(); i++) {
shader_stages[shader_stage_count++] = {.stage =
NativeShaderStage(vertex_info[i].logical_stage),
.module = vertex_info[i].stage.handle->module,
.pName = "main"};
for (const auto* stage: stages) {
if (stage != nullptr) {
shader_stages[shader_stage_count++] = {.stage = NativeShaderStage(stage->info.stage),
.module = stage->module,
.pName = "main"};
}
}
if (rect_list) {
shader_stages[shader_stage_count++] = {.stage =
@@ -300,11 +184,6 @@ void CreatePipelineInternal(GraphicContext& graphics, PipelineCache::Pipeline& p
.module = tess_eval_shader_module,
.pName = "main"};
}
if (ps_active) {
shader_stages[shader_stage_count++] = {.stage = vk::ShaderStageFlagBits::eFragment,
.module = pixel_program->module,
.pName = "main"};
}
vk::VertexInputAttributeDescription input_attr[ShaderVertexInputInfo::RES_MAX] {};
vk::VertexInputBindingDescription input_desc[ShaderVertexInputInfo::RES_MAX] {};
@@ -320,46 +199,7 @@ void CreatePipelineInternal(GraphicContext& graphics, PipelineCache::Pipeline& p
input_attr[index].binding = vertex_input.attributes[index].binding;
input_attr[index].location = index;
input_attr[index].offset = vertex_input.attributes[index].offset;
uint32_t attr_size = 4;
const auto registers_num = vs_input_info.resources_dst[index].registers_num;
const auto compiled_components =
vs_input_info.stage.program->info.vertex_fetch_components[index];
const auto used_components =
compiled_components > 0 ? static_cast<int>(compiled_components) : registers_num;
GetInputFormat(vs_input_info.resources[index], input_attr[index].format, attr_size,
static_cast<uint32_t>(used_components));
if (graphics_debug_dump_enabled()) {
static std::atomic_uint log_count = 0;
const auto log_id = log_count.fetch_add(1, std::memory_order_relaxed);
if (log_id < 128) {
LOGF("VertexInputState[%u]: attr=%u binding=%u offset=%u stride=%u fmt=%d "
"src_fmt=%u dst=v%u regs=%u"
" fetched_components=%u attr_size=%u swizzle=%u,%u,%u,%u\n",
log_id, index, input_attr[index].binding, input_attr[index].offset,
input_desc[input_attr[index].binding].stride,
static_cast<int>(input_attr[index].format),
static_cast<uint32_t>(vs_input_info.resources[index].Format()),
static_cast<uint32_t>(vs_input_info.resources_dst[index].register_start),
static_cast<uint32_t>(registers_num), static_cast<uint32_t>(used_components),
attr_size, static_cast<uint32_t>(vs_input_info.resources[index].DstSelX()),
static_cast<uint32_t>(vs_input_info.resources[index].DstSelY()),
static_cast<uint32_t>(vs_input_info.resources[index].DstSelZ()),
static_cast<uint32_t>(vs_input_info.resources[index].DstSelW()));
}
}
if (vs_input_info.resources[index].OutOfBounds() != 0) {
static bool logged = false;
if (!logged) {
LOGF("VertexInput: temporary: accepting PS5 out-of-bounds behavior %" PRIu8 "\n",
vs_input_info.resources[index].OutOfBounds());
logged = true;
}
}
EXIT_IF(registers_num < 1 || registers_num > 4);
input_attr[index].format = vertex.vertex_formats[index];
}
vk::PipelineVertexInputStateCreateInfo vertex_input_info {};
@@ -467,16 +307,13 @@ void CreatePipelineInternal(GraphicContext& graphics, PipelineCache::Pipeline& p
std::vector<vk::DescriptorSetLayoutBinding> descriptor_bindings;
vk::ShaderStageFlags graphics_stages = vk::ShaderStageFlagBits::eFragment;
for (const auto& stage: vertex_info) {
const auto native_stage = NativeShaderStage(stage.logical_stage);
AddLayoutBindings(descriptor_bindings, *stage.stage.program, native_stage);
pipeline.uses_image_heap |= stage.stage.program->info.uses_image_heap;
graphics_stages |= native_stage;
}
if (ps_active) {
AddLayoutBindings(descriptor_bindings, *ps_input_info->stage.program,
vk::ShaderStageFlagBits::eFragment);
pipeline.uses_image_heap |= ps_input_info->stage.program->info.uses_image_heap;
for (const auto* stage: stages) {
if (stage != nullptr) {
const auto native_stage = NativeShaderStage(stage->info.stage);
AddLayoutBindings(descriptor_bindings, stage->info, native_stage);
pipeline.uses_image_heap |= stage->info.info.uses_image_heap;
graphics_stages |= native_stage;
}
}
CreateDescriptorLayout(graphics, pipeline, descriptor_bindings);
const vk::PushConstantRange push_constants {graphics_stages, 0,
@@ -494,7 +331,7 @@ void CreatePipelineInternal(GraphicContext& graphics, PipelineCache::Pipeline& p
if (graphics_debug_dump_enabled()) {
LOGF("PipelineTrace: vkCreatePipelineLayout begin VS=%" PRIu64 " PS=%" PRIu64
" set_layouts=1 push_constants=%" PRIu32 "\n",
vertex_program.id, ps_active ? pixel_program->id : 0, 1u);
key.stage_ids[0], key.stage_ids[3], 1u);
}
auto result = graphics.device.createPipelineLayout(&pipeline_layout_info, nullptr,
&pipeline.pipeline_layout);
@@ -504,8 +341,6 @@ void CreatePipelineInternal(GraphicContext& graphics, PipelineCache::Pipeline& p
}
EXIT_NOT_IMPLEMENTED(result != vk::Result::eSuccess);
EXIT_NOT_IMPLEMENTED(pipeline.pipeline_layout == nullptr);
vk::PipelineDepthStencilStateCreateInfo depth_stencil_info {};
std::vector<vk::DynamicState> dynamic_states {
@@ -551,9 +386,9 @@ void CreatePipelineInternal(GraphicContext& graphics, PipelineCache::Pipeline& p
pipeline_info.pVertexInputState = mesh ? nullptr : &vertex_input_info;
pipeline_info.pInputAssemblyState = mesh ? nullptr : &input_assembly;
vk::PipelineTessellationStateCreateInfo tessellation_state {};
tessellation_state.patchControlPoints =
tessellation ? vs_input_info.tess.input_control_points : 3u;
pipeline_info.pTessellationState = (rect_list || tessellation) ? &tessellation_state : nullptr;
tessellation_state.patchControlPoints = static_params.patch_control_points;
pipeline_info.pTessellationState =
static_params.patch_control_points != 0 ? &tessellation_state : nullptr;
pipeline_info.pViewportState = &viewport_state;
pipeline_info.pRasterizationState = &rasterizer;
pipeline_info.pMultisampleState = &multisampling;
@@ -569,7 +404,7 @@ void CreatePipelineInternal(GraphicContext& graphics, PipelineCache::Pipeline& p
LOGF("PipelineTrace: vkCreateGraphicsPipelines begin VS=%" PRIu64 " PS=%" PRIu64
" topology=%" PRIu32 " color_mask=0x%08" PRIx32
" depth=%s blend=%s dyn_states=%" PRIu32 "\n",
vertex_program.id, ps_active ? pixel_program->id : 0,
key.stage_ids[0], key.stage_ids[3],
static_cast<uint32_t>(static_params.topology), static_params.color_mask[0],
(with_depth ? "true" : "false"), (static_params.blend_enable[0] ? "true" : "false"),
dynamic_state.dynamicStateCount);
@@ -582,29 +417,18 @@ void CreatePipelineInternal(GraphicContext& graphics, PipelineCache::Pipeline& p
}
EXIT_NOT_IMPLEMENTED(result != vk::Result::eSuccess);
EXIT_NOT_IMPLEMENTED(pipeline.pipeline == nullptr);
if (tess_control_shader_module != nullptr) {
graphics.device.destroyShaderModule(tess_control_shader_module, nullptr);
}
if (tess_eval_shader_module != nullptr) {
graphics.device.destroyShaderModule(tess_eval_shader_module, nullptr);
}
graphics.device.destroyShaderModule(tess_control_shader_module, nullptr);
graphics.device.destroyShaderModule(tess_eval_shader_module, nullptr);
}
// NOLINTNEXTLINE(readability-function-cognitive-complexity)
void CreatePipelineInternal(GraphicContext& graphics, PipelineCache::Pipeline& pipeline,
const ShaderComputeInputInfo& input_info,
vk::ShaderModule compute_module, vk::PipelineCache driver_cache) {
EXIT_IF(compute_module == nullptr);
const ShaderModule& module, vk::PipelineCache driver_cache) {
vk::PipelineShaderStageCreateInfo comp_shader_stage_info {};
vk::PipelineShaderStageRequiredSubgroupSizeCreateInfo comp_subgroup_size {};
comp_shader_stage_info.stage = vk::ShaderStageFlagBits::eCompute;
comp_shader_stage_info.module = compute_module;
comp_shader_stage_info.module = module.module;
comp_shader_stage_info.pName = "main";
EXIT_IF(!input_info.stage);
const auto wave_size = input_info.stage.program->wave_size;
const auto wave_size = module.info.wave_size;
if (graphics.compute_subgroup_size_control_enabled &&
wave_size >= graphics.min_subgroup_size && wave_size <= graphics.max_subgroup_size) {
comp_subgroup_size.requiredSubgroupSize = wave_size;
@@ -612,9 +436,8 @@ void CreatePipelineInternal(GraphicContext& graphics, PipelineCache::Pipeline& p
}
std::vector<vk::DescriptorSetLayoutBinding> descriptor_bindings;
AddLayoutBindings(descriptor_bindings, *input_info.stage.program,
vk::ShaderStageFlagBits::eCompute);
pipeline.uses_image_heap = input_info.stage.program->info.uses_image_heap;
AddLayoutBindings(descriptor_bindings, module.info, vk::ShaderStageFlagBits::eCompute);
pipeline.uses_image_heap = module.info.info.uses_image_heap;
CreateDescriptorLayout(graphics, pipeline, descriptor_bindings);
const vk::PushConstantRange push_constants {vk::ShaderStageFlagBits::eCompute, 0,
ShaderRecompiler::IR::NativePushConstantSize};
@@ -628,18 +451,12 @@ void CreatePipelineInternal(GraphicContext& graphics, PipelineCache::Pipeline& p
EXIT_IF(pipeline.pipeline_layout != nullptr);
LOGF("PipelineTrace: vkCreatePipelineLayout CS begin set_layouts=1 push_constants=%u\n",
1u);
auto result = graphics.device.createPipelineLayout(&pipeline_layout_info, nullptr,
&pipeline.pipeline_layout);
LOGF("PipelineTrace: vkCreatePipelineLayout CS done result=%s layout=%p\n",
vk::to_string(result).c_str(), static_cast<void*>(pipeline.pipeline_layout));
EXIT_NOT_IMPLEMENTED(result != vk::Result::eSuccess);
EXIT_NOT_IMPLEMENTED(pipeline.pipeline_layout == nullptr);
vk::ComputePipelineCreateInfo info {};
if (input_info.stage.program->info.uses_gds) {
if (module.info.info.uses_gds) {
info.flags = vk::PipelineCreateFlagBits::eDispatchBase;
}
info.stage = comp_shader_stage_info;
@@ -648,15 +465,9 @@ void CreatePipelineInternal(GraphicContext& graphics, PipelineCache::Pipeline& p
EXIT_IF(pipeline.pipeline != nullptr);
LOGF("PipelineTrace: vkCreateComputePipelines begin layout=%p\n",
static_cast<void*>(pipeline.pipeline_layout));
result = graphics.device.createComputePipelines(driver_cache, 1, &info, nullptr,
&pipeline.pipeline);
LOGF("PipelineTrace: vkCreateComputePipelines done result=%s pipeline=%p\n",
vk::to_string(result).c_str(), static_cast<void*>(pipeline.pipeline));
EXIT_NOT_IMPLEMENTED(result != vk::Result::eSuccess);
EXIT_NOT_IMPLEMENTED(pipeline.pipeline == nullptr);
}
} // namespace Libs::Graphics
@@ -53,7 +53,7 @@ static bool FillSourcesDisjoint(std::span<const ShaderRecompiler::IR::Descriptor
bool RenderExecutor::TryConsumeComputeMetaClear(const ShaderComputeInputInfo& input,
const CommandBuffer& buffer) {
const auto& program = *input.stage.program;
const auto& program = input.stage.module->info;
const auto& resources = *input.stage.resources;
if (resources.buffers.size() != program.info.buffers.size()) {
EXIT("compute runtime buffer count does not match shader metadata\n");
@@ -135,7 +135,7 @@ bool ResolveComputeBufferFill(const ShaderComputeInputInfo& input, uint32_t grou
bool RenderExecutor::TryConsumeComputeFill(const ShaderComputeInputInfo& input,
CommandBuffer& command, uint32_t group_x,
uint32_t group_y, uint32_t group_z, uint32_t mode) {
const auto& program = *input.stage.program;
const auto& program = input.stage.module->info;
const auto& resources = *input.stage.resources;
const auto& fill = resources.uniform_fill;
auto& cache = command.GetContext().GetTextureCache();
@@ -208,7 +208,7 @@ bool RenderExecutor::TryConsumeComputeFill(const ShaderComputeInputInfo& input,
if (logged_clears.fetch_add(1, std::memory_order_relaxed) < 32) {
LOGF("GraphicsRenderDispatchDirect: compute %s shader=0x%016" PRIx64
" addr=0x%016" PRIx64 " size=0x%016" PRIx64 " value=0x%08" PRIx32 "\n",
image_clear ? "image clear" : "buffer fill", input.stage.program->shader_hash,
image_clear ? "image clear" : "buffer fill", input.stage.module->info.shader_hash,
descriptor.Base48(), size, packed_clear);
}
return true;
@@ -216,7 +216,7 @@ bool RenderExecutor::TryConsumeComputeFill(const ShaderComputeInputInfo& input,
static void BindSharedMemory(RenderContext& context, ShaderComputeInputInfo& input,
PreparedBindings& bindings, uint64_t indirect_args = 0) {
if (input.stage.program->bindings.descriptor_counts[
if (input.stage.module->info.bindings.descriptor_counts[
static_cast<size_t>(ShaderRecompiler::IR::DescriptorBindingKind::SharedMemory)] == 0) {
return;
}
@@ -317,7 +317,7 @@ void RenderExecutor::DispatchDirect(uint64_t submit_id, CommandBuffer& buffer,
input_info.dispatch_threads_num[2] = thread_group_z;
}
const auto& program = *input_info.stage.program;
const auto& program = input_info.stage.module->info;
const auto& resources = *input_info.stage.resources;
if (resources.specialization_reads.empty() &&
(TryConsumeComputeMetaClear(input_info, buffer) ||
@@ -504,7 +504,7 @@ void RenderExecutor::DispatchIndirect(uint64_t submit_id, CommandBuffer& buffer,
PrepareBindings(input_info.stage, bindings);
PreparedBindings* descriptor_stage = &bindings;
FindBuffers(std::span {&descriptor_stage, 1u});
const auto& program = *input_info.stage.program;
const auto& program = input_info.stage.module->info;
if (program.info.uses_dma) {
m_context.PrepareBda();
}
@@ -10,10 +10,10 @@
namespace Libs::Graphics {
RenderContext::RenderContext(GraphicContext& graphics)
RenderContext::RenderContext(GraphicContext& graphics, const std::filesystem::path& shader_store)
: m_graphics(graphics), m_render_executor(*this), m_command_scheduler(*this, graphics),
m_descriptor_heap(graphics, m_command_scheduler.GetMasterSemaphore()),
m_pipeline_cache(graphics, &m_image_heap), m_sampler_cache(graphics),
m_pipeline_cache(graphics, &m_image_heap, shader_store), m_sampler_cache(graphics),
m_buffer_cache(graphics, m_command_scheduler, m_page_manager, m_texture_cache),
m_texture_cache(graphics, m_command_scheduler, m_page_manager, m_buffer_cache),
m_image_heap(*this, m_page_manager), m_occlusion_queries(graphics, m_command_scheduler) {
@@ -31,7 +31,8 @@ class GuestGpu;
class RenderContext {
public:
explicit RenderContext(GraphicContext& graphics);
explicit RenderContext(GraphicContext& graphics,
const std::filesystem::path& shader_store = ShaderStorePath());
~RenderContext();
KYTY_CLASS_NO_COPY(RenderContext);
+14 -9
View File
@@ -57,7 +57,7 @@ std::pair<int32_t, uint32_t> ResolveDrawOffsets(uint32_t index_offset,
}
EXIT_IF(!vs_input_info.stage);
const auto& program = *vs_input_info.stage.program;
const auto& program = vs_input_info.stage.module->info;
const auto& resources = *vs_input_info.stage.resources;
if (index_offset == 0 &&
program.info.vertex_offset_sgpr >= static_cast<int32_t>(program.user_data_base)) {
@@ -197,7 +197,7 @@ static void LogDrawTargetState(const char* draw_name, const RenderColorInfo& col
const auto& dc = ctx.GetDepthControl();
const auto& vp = ctx.GetScreenViewport();
const auto& vp0 = vp.viewports[0];
const auto& ps_resources = ps_input_info.stage.program->info;
const auto& ps_resources = ps_input_info.stage.module->info.info;
const auto sampled_images = std::count_if(
ps_resources.images.begin(), ps_resources.images.end(), [](const auto& image) {
return image.resource_class == ShaderRecompiler::IR::ImageResourceClass::Sampled;
@@ -320,7 +320,7 @@ static void UpdateGraphicsDynamicState(const CommandBuffer& buffer, DynamicState
const auto& ctx = buffer.GetRegisters();
const auto& vp = ctx.GetScreenViewport();
const vk::Extent2D framebuffer_extent {rendering.width, rendering.height};
const auto& outputs = vs_input_info.stage.program->info.outputs;
const auto& outputs = vs_input_info.stage.module->info.info.outputs;
const bool indexed_viewports =
std::any_of(outputs.begin(), outputs.end(), [](const auto& output) {
return output.kind == ShaderRecompiler::IR::StageOutputKind::ViewportIndex;
@@ -843,6 +843,11 @@ bool RenderExecutor::PrepareDrawRenderState(CommandBuffer& buffer, const DrawCal
LogDrawPhase(draw.Name(), "GetGraphicsPrograms");
}
m_context.GetPipelineCache().GetGraphicsPrograms(buffer, state);
// Vertex programs beyond the host mesh limits or with unsupported call targets are not
// compiled.
if (state.vertex_info[0].stage.module == nullptr) {
return false;
}
if (draw.IsIndexed()) {
LogDrawPhase(draw.Name(), "ResolveRenderColorTarget");
}
@@ -1054,7 +1059,7 @@ void RenderExecutor::ExecutePreparedDraw(uint64_t submit_id, CommandBuffer& buff
return;
}
const auto vertex_stages = state.VertexStages();
const bool mesh_active = state.vertex_info[0].stage.program->stage == ShaderType::Mesh;
const bool mesh_active = state.vertex_info[0].stage.module->info.stage == ShaderType::Mesh;
uint32_t mesh_groups = 0;
if (mesh_active) {
const auto& mesh = state.vertex_info[0].mesh;
@@ -1156,7 +1161,6 @@ void RenderExecutor::ExecutePreparedDraw(uint64_t submit_id, CommandBuffer& buff
if (mesh_active) {
draw_data = {draw.index_count,
static_cast<uint32_t>(emit.vertex_offset),
emit.first_instance,
index_source.guest_element_size,
static_cast<uint32_t>(index_source.address),
static_cast<uint32_t>(index_source.address >> 32u)};
@@ -1241,7 +1245,7 @@ static std::optional<DrawEmitInfo> ResolveDrawEmit(const HW::UserConfig&
uint32_t first_instance,
const DrawIndirectArgs& indirect) {
if (indirect.address != 0) {
if (vs.stage.program->stage == ShaderType::Mesh ||
if (vs.stage.module->info.stage == ShaderType::Mesh ||
ucfg.GetPrimType() == Prospero::PrimitiveType::kQuadListLegacy) {
return std::nullopt;
}
@@ -1423,7 +1427,7 @@ bool RenderExecutor::DrawAuto(uint64_t submit_id, CommandBuffer& buffer, const D
const bool rect_list = Prospero::IsRectList(ucfg.GetPrimType());
if (rect_list && state.vertex_info[0].buffers_num == 0 &&
state.vertex_info[0].stage.program->param_export_mask == 0 &&
state.vertex_info[0].stage.module->info.param_export_mask == 0 &&
state.ps_input_info.input_num != 0) {
if (graphics_debug_dump_enabled()) {
LOGF("DrawIndexAuto: skipping rect-list draw with no VS param exports and PS inputs: "
@@ -1475,8 +1479,9 @@ bool RenderExecutor::ResolveColorTargets(CommandBuffer& buffer, uint32_t render_
cache.MarkGpuWritten(dst.image_id);
auto& source = cache.GetImage(src.image_id);
auto& destination = cache.GetImage(dst.image_id);
destination.Resolve(source, {src.guest_mip_level, 1, src.guest_array_layer, 1},
{dst.guest_mip_level, 1, dst.guest_array_layer, 1});
destination.Resolve(source,
{src.desc.view_info.base_level, 1, src.desc.view_info.base_layer, 1},
{dst.desc.view_info.base_level, 1, dst.desc.view_info.base_layer, 1});
return true;
}
@@ -337,6 +337,7 @@ static void VulkanFindPhysicalDevice(vk::Instance instance, vk::SurfaceKHR surfa
check_feature(device_features2.features.shaderImageGatherExtended,
"shaderImageGatherExtended");
check_feature(device_features2.features.shaderResourceMinLod, "shaderResourceMinLod");
check_feature(device_features2.features.independentBlend, "independentBlend");
check_feature(device_features2.features.tessellationShader, "tessellationShader");
@@ -636,6 +637,7 @@ static vk::Device VulkanCreateDevice(GraphicContext& graphics,
device_features.shaderStorageImageMultisample =
supported_features2.features.shaderStorageImageMultisample;
device_features.shaderImageGatherExtended = VK_TRUE;
device_features.shaderResourceMinLod = VK_TRUE;
device_features.independentBlend = VK_TRUE;
device_features.dualSrcBlend = VK_TRUE;
device_features.tessellationShader = VK_TRUE;
@@ -733,6 +735,15 @@ static vk::Device VulkanCreateDevice(GraphicContext& graphics,
conditional_rendering.pNext = const_cast<void*>(create_info.pNext);
create_info.pNext = &conditional_rendering;
}
// The renderer only uses the Vulkan 1.3 core dynamic states, but RADV (Mesa 26.3) creates its
// PS epilog cache only when this EXT feature is enabled and crashes in vkCmdDraw when a
// depth- or stencil-writing fragment shader needs an epilog.
vk::PhysicalDeviceExtendedDynamicStateFeaturesEXT extended_dynamic_state {};
extended_dynamic_state.extendedDynamicState = VK_TRUE;
if (HasExtension(device_extensions, VK_EXT_EXTENDED_DYNAMIC_STATE_EXTENSION_NAME)) {
extended_dynamic_state.pNext = const_cast<void*>(create_info.pNext);
create_info.pNext = &extended_dynamic_state;
}
create_info.pQueueCreateInfos = &queue_create_info;
create_info.queueCreateInfoCount = 1;
create_info.enabledExtensionCount = static_cast<uint32_t>(device_extensions.size());
@@ -1111,7 +1122,8 @@ void WindowContext::CreateVulkan() {
VK_KHR_WORKGROUP_MEMORY_EXPLICIT_LAYOUT_EXTENSION_NAME,
VK_EXT_DEPTH_RANGE_UNRESTRICTED_EXTENSION_NAME,
VK_KHR_FRAGMENT_SHADER_BARYCENTRIC_EXTENSION_NAME,
VK_EXT_CONDITIONAL_RENDERING_EXTENSION_NAME}) {
VK_EXT_CONDITIONAL_RENDERING_EXTENSION_NAME,
VK_EXT_EXTENDED_DYNAMIC_STATE_EXTENSION_NAME}) {
if (HasExtension(available_extensions, extension)) {
device_extensions.push_back(extension);
}
@@ -30,10 +30,6 @@ namespace Libs::Graphics::ShaderRecompiler {
namespace {
const char* GetDumpLabel(const CompileOptions& options) {
return options.dump_label != nullptr ? options.dump_label : "ShaderRecompiler";
}
std::string MakeIrDump(std::string_view cfg, const IR::Program& ir) {
std::string dump = "CFG:\n";
dump += cfg;
@@ -44,30 +40,17 @@ std::string MakeIrDump(std::string_view cfg, const IR::Program& ir) {
return dump;
}
const char* StageName(ShaderType stage) {
switch (stage) {
case ShaderType::Compute: return "CS";
case ShaderType::Vertex: return "VS";
case ShaderType::Local: return "LS";
case ShaderType::TessellationControl: return "HS";
case ShaderType::TessellationEvaluation: return "TES";
case ShaderType::Mesh: return "MS";
case ShaderType::Pixel: return "PS";
default: return "unknown";
}
}
void LogDispatcherFallback(const CompileOptions& options, const CFG::Graph& cfg, const char* phase) {
const auto* block = cfg.FindBlock(cfg.failure_block);
const auto start = block != nullptr ? block->start_pc : UINT32_MAX;
const auto end = block != nullptr ? block->end_pc : UINT32_MAX;
const auto predecessors = block != nullptr ? block->predecessors.size() : 0u;
const auto successors = block != nullptr ? block->successors.size() : 0u;
LOGF("%s CFG dispatcher fallback: stage=%s hash=0x%016" PRIx64
LOGF("ShaderRecompiler %s CFG dispatcher fallback: hash=0x%016" PRIx64
" phase=%s failure=%s block=%" PRIu32 " pc=0x%08" PRIx32 "..0x%08" PRIx32 " preds=%" PRIu64
" succs=%" PRIu64 " blocks=%" PRIu64 " loops=%" PRIu64 " back_edges=%" PRIu64
" reason=%s\n",
GetDumpLabel(options), StageName(options.stage), options.shader_hash, phase,
StageName(options.stage), options.shader_hash, phase,
CFG::FailureKindToString(cfg.failure_kind).c_str(), cfg.failure_block, start, end,
static_cast<uint64_t>(predecessors), static_cast<uint64_t>(successors),
static_cast<uint64_t>(cfg.blocks.size()), static_cast<uint64_t>(cfg.natural_loops.size()),
@@ -484,6 +467,7 @@ Decoder::Program DecodeFusedProgram(std::span<const uint32_t> front, std::span<c
void PrepareCallTargets(ShaderSource& source, const CompileOptions& options) {
using namespace IR;
EXIT_NOT_IMPLEMENTED(options.stage != ShaderType::Compute);
source.wave_size = options.wave_size;
for (uint32_t i = 0; i < source.decoded.instructions.size(); ++i) {
if (source.decoded.instructions[i].opcode != Decoder::Opcode::S_SWAPPC_B64) continue;
source.calls.push_back({.instruction = i});
@@ -520,7 +504,8 @@ void PrepareCallTargets(ShaderSource& source, const CompileOptions& options) {
};
Register = [&](uint32_t before, uint32_t code) -> Value {
std::vector<uint32_t> crossed_calls;
const auto index = CFG::FindScalarDefinition(source.decoded, graph, before, code, crossed_calls);
const auto index = CFG::FindScalarDefinition(source.decoded, graph, before, code,
source.wave_size, crossed_calls);
for (const auto crossed: crossed_calls) {
std::ranges::find(source.calls, crossed, &ShaderSource::Call::instruction)->preserved_registers.set(code);
}
@@ -595,6 +580,19 @@ void PrepareCallTargets(ShaderSource& source, const CompileOptions& options) {
} // namespace
const char* StageName(ShaderType stage) {
switch (stage) {
case ShaderType::Compute: return "CS";
case ShaderType::Vertex: return "VS";
case ShaderType::Local: return "LS";
case ShaderType::TessellationControl: return "HS";
case ShaderType::TessellationEvaluation: return "TES";
case ShaderType::Mesh: return "MS";
case ShaderType::Pixel: return "PS";
default: return "unknown";
}
}
ShaderSource PrepareShaderSource(std::span<const uint32_t> code, const CompileOptions& options) {
EXIT_IF(code.empty());
ShaderSource source;
@@ -706,10 +704,11 @@ const Decoder::Program& RefreshShaderSource(ShaderSource& source, const IR::SrtR
EXIT_IF(function_pc > UINT32_MAX - function_size);
const auto function_offset = static_cast<uint32_t>(linked.code.size());
for (auto inst: function) {
EXIT_IF(CFG::MayWriteScalarRegister(inst, call.dst.reg) ||
CFG::MayWriteScalarRegister(inst, call.dst.reg + 1u));
EXIT_IF(CFG::MayWriteScalarRegister(inst, call.dst.reg, source.wave_size) ||
CFG::MayWriteScalarRegister(inst, call.dst.reg + 1u, source.wave_size));
for (uint32_t code = 0; code < site.preserved_registers.size(); ++code) {
if (site.preserved_registers[code] && CFG::MayWriteScalarRegister(inst, code))
if (site.preserved_registers[code] &&
CFG::MayWriteScalarRegister(inst, code, source.wave_size))
EXIT("shader call at pc 0x%08x clobbers scalar target dependency s%u", call.pc, code);
}
linked.code.insert(linked.code.end(), inst.raw, inst.raw + inst.word_count);
@@ -764,6 +763,7 @@ TranslateResult TranslateProgram(const Decoder::Program& decoded, const CompileO
static_cast<unsigned>(options.stage));
}
const auto* stage = StageName(options.stage);
const auto compile_begin = std::chrono::steady_clock::now();
const auto phase_ms = [&compile_begin]() {
return static_cast<uint64_t>(std::chrono::duration_cast<std::chrono::milliseconds>(
@@ -773,27 +773,25 @@ TranslateResult TranslateProgram(const Decoder::Program& decoded, const CompileO
std::string decoded_dump;
if (options.dump_ir) {
decoded_dump = Decoder::ProgramToString(decoded);
if (options.early_dump) {
LOGF("%s decoded RDNA2 (early):\n%s", GetDumpLabel(options), decoded_dump.c_str());
}
LOGF("ShaderRecompiler %s decoded RDNA2:\n%s", stage, decoded_dump.c_str());
}
LOGF("%s phase begin: stage=%s hash=0x%016" PRIx64 " CFG BuildGraph\n", GetDumpLabel(options),
StageName(options.stage), options.shader_hash);
LOGF("ShaderRecompiler %s phase begin: hash=0x%016" PRIx64 " CFG BuildGraph\n", stage,
options.shader_hash);
auto native_cfg = CFG::BuildGraph(decoded);
CFG::Graph structured_cfg;
auto* selected_cfg = &native_cfg;
LOGF("%s phase end: stage=%s hash=0x%016" PRIx64 " CFG BuildGraph blocks=%" PRIu64
LOGF("ShaderRecompiler %s phase end: hash=0x%016" PRIx64 " CFG BuildGraph blocks=%" PRIu64
" loops=%" PRIu64 " back_edges=%" PRIu64 " elapsed_ms=%" PRIu64 "\n",
GetDumpLabel(options), StageName(options.stage), options.shader_hash,
stage, options.shader_hash,
static_cast<uint64_t>(native_cfg.blocks.size()),
static_cast<uint64_t>(native_cfg.natural_loops.size()),
static_cast<uint64_t>(native_cfg.back_edges.size()), phase_ms());
if (native_cfg.irreducible) {
LogDispatcherFallback(options, native_cfg, "build");
} else {
LOGF("%s phase begin: stage=%s hash=0x%016" PRIx64 " CFG Structurize\n",
GetDumpLabel(options), StageName(options.stage), options.shader_hash);
LOGF("ShaderRecompiler %s phase begin: hash=0x%016" PRIx64 " CFG Structurize\n", stage,
options.shader_hash);
structured_cfg = CFG::Structurize(native_cfg);
if (structured_cfg.unsupported) {
native_cfg.unsupported = true;
@@ -803,12 +801,12 @@ TranslateResult TranslateProgram(const Decoder::Program& decoded, const CompileO
LogDispatcherFallback(options, native_cfg, "structurize");
} else {
selected_cfg = &structured_cfg;
LOGF("%s structured CFG success: blocks=%" PRIu64 "\n", GetDumpLabel(options),
LOGF("ShaderRecompiler %s structured CFG success: blocks=%" PRIu64 "\n", stage,
static_cast<uint64_t>(selected_cfg->blocks.size()));
}
LOGF("%s phase end: stage=%s hash=0x%016" PRIx64 " CFG Structurize blocks=%" PRIu64
LOGF("ShaderRecompiler %s phase end: hash=0x%016" PRIx64 " CFG Structurize blocks=%" PRIu64
" loops=%" PRIu64 " elapsed_ms=%" PRIu64 "\n",
GetDumpLabel(options), StageName(options.stage), options.shader_hash,
stage, options.shader_hash,
static_cast<uint64_t>(selected_cfg->blocks.size()),
static_cast<uint64_t>(selected_cfg->natural_loops.size()), phase_ms());
}
@@ -821,7 +819,7 @@ TranslateResult TranslateProgram(const Decoder::Program& decoded, const CompileO
decoded, *options.input_info.vertex, options.user_data_base,
static_cast<uint32_t>(options.user_data.size()), options.wave_size);
if (!embedded_fetch.loads.empty()) {
LOGF("%s embedded vertex fetch plan: detected=%" PRIu64 "\n", GetDumpLabel(options),
LOGF("ShaderRecompiler %s embedded vertex fetch plan: detected=%" PRIu64 "\n", stage,
static_cast<uint64_t>(embedded_fetch.loads.size()));
}
}
@@ -834,12 +832,12 @@ TranslateResult TranslateProgram(const Decoder::Program& decoded, const CompileO
.input_info = options.input_info,
.embedded_fetch = embedded_fetch.loads.empty() ? nullptr : &embedded_fetch,
};
LOGF("%s phase begin: stage=%s hash=0x%016" PRIx64 " IR TranslateProgram\n",
GetDumpLabel(options), StageName(options.stage), options.shader_hash);
LOGF("ShaderRecompiler %s phase begin: hash=0x%016" PRIx64 " IR TranslateProgram\n", stage,
options.shader_hash);
auto ir = Frontend::TranslateProgram(decoded, cfg, translate_options);
LOGF("%s phase end: stage=%s hash=0x%016" PRIx64 " IR TranslateProgram blocks=%" PRIu64
LOGF("ShaderRecompiler %s phase end: hash=0x%016" PRIx64 " IR TranslateProgram blocks=%" PRIu64
" elapsed_ms=%" PRIu64 "\n",
GetDumpLabel(options), StageName(options.stage), options.shader_hash,
stage, options.shader_hash,
static_cast<uint64_t>(ir.blocks.size()), phase_ms());
IR::RewriteToSsa(ir.blocks);
const auto simplify = [&ir] {
@@ -850,8 +848,9 @@ TranslateResult TranslateProgram(const Decoder::Program& decoded, const CompileO
simplify();
const auto read_lane_stats = IR::EliminateReadLane(ir, ir.wave_size);
if (read_lane_stats.rewritten_reads != 0 || read_lane_stats.rewritten_reductions != 0) {
LOGF("%s read-lane elimination: reads=%" PRIu32 " reductions=%" PRIu32 "\n",
GetDumpLabel(options), read_lane_stats.rewritten_reads, read_lane_stats.rewritten_reductions);
LOGF("ShaderRecompiler %s read-lane elimination: reads=%" PRIu32
" reductions=%" PRIu32 "\n",
stage, read_lane_stats.rewritten_reads, read_lane_stats.rewritten_reductions);
}
// Cleanup also exposes profitable folds by removing dead predicate consumers.
simplify();
@@ -865,10 +864,8 @@ TranslateResult TranslateProgram(const Decoder::Program& decoded, const CompileO
std::string cfg_dump;
if (options.dump_ir) {
cfg_dump = CFG::GraphToString(cfg);
if (options.early_dump) {
LOGF("%s native IR before resource tracking:\n%s", GetDumpLabel(options),
MakeIrDump(cfg_dump, ir).c_str());
}
LOGF("ShaderRecompiler %s native IR before resource tracking:\n%s", stage,
MakeIrDump(cfg_dump, ir).c_str());
}
IR::TrackResources(ir, decoded, native_cfg, options.input_info);
TranslateResult result;
@@ -885,6 +882,7 @@ CompileResult CompileProgram(TranslateResult translated, const CompileOptions& o
uint32_t push_data_start_dword) {
const auto emit_begin = std::chrono::steady_clock::now();
auto& ir = translated.program;
const auto* stage = StageName(ir.stage);
IR::ApplyResourceSpecialization(ir, specialization);
// The resource plan owns host descriptor evaluation now. Keep only dependencies consumed
// by GPU memory operations; bound descriptor dwords must not retain shader instructions.
@@ -931,19 +929,16 @@ CompileResult CompileProgram(TranslateResult translated, const CompileOptions& o
std::string ir_dump;
if (options.dump_ir) {
ir_dump = MakeIrDump(translated.cfg_dump, ir);
if (options.early_dump) {
LOGF("%s native IR (early):\n%s", GetDumpLabel(options), ir_dump.c_str());
}
LOGF("ShaderRecompiler %s native IR:\n%s", stage, ir_dump.c_str());
}
LOGF("%s phase begin: stage=%s hash=0x%016" PRIx64 " SPIR-V EmitProgram\n",
GetDumpLabel(options), StageName(ir.stage), ir.shader_hash);
LOGF("ShaderRecompiler %s phase begin: hash=0x%016" PRIx64 " SPIR-V EmitProgram\n", stage,
ir.shader_hash);
auto spirv = Spirv::EmitProgram(ir, options.input_info, push_data_start_dword, *options.profile,
options.emit_debug_info);
LOGF("%s phase end: stage=%s hash=0x%016" PRIx64 " SPIR-V EmitProgram words=%" PRIu64
" elapsed_ms=%" PRIu64 "\n",
GetDumpLabel(options), StageName(ir.stage), ir.shader_hash,
static_cast<uint64_t>(spirv.size()),
LOGF("ShaderRecompiler %s phase end: hash=0x%016" PRIx64 " SPIR-V EmitProgram words=%" PRIu64
" wave_size=%u elapsed_ms=%" PRIu64 "\n",
stage, ir.shader_hash, static_cast<uint64_t>(spirv.size()), ir.wave_size,
static_cast<uint64_t>(std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::steady_clock::now() - emit_begin)
.count()));
@@ -22,10 +22,8 @@ struct CompileOptions {
uint32_t user_data_base = 0;
uint64_t shader_hash = 0;
bool dump_ir = true;
bool early_dump = false;
bool emit_debug_info = false;
const Spirv::Profile* profile = &Spirv::DefaultProfile;
const char* dump_label = nullptr;
std::span<const uint32_t> user_data;
std::span<const uint32_t> back_code;
ShaderStageInputInfo input_info;
@@ -64,11 +62,13 @@ struct ShaderSource {
IR::ResourcePlan call_targets;
std::vector<Call> calls;
uint32_t unsupported_call_pc = UINT32_MAX;
uint32_t wave_size = 64;
std::optional<Linked> linked;
uint64_t revision = 0;
std::vector<std::pair<uint64_t, uint64_t>> reads;
};
[[nodiscard]] const char* StageName(ShaderType stage);
[[nodiscard]] ShaderSource PrepareShaderSource(std::span<const uint32_t> code,
const CompileOptions& options);
const Decoder::Program& RefreshShaderSource(ShaderSource& source, const IR::SrtRuntime& runtime);
@@ -54,9 +54,30 @@ uint32_t EmitExt(EmitterState& state, uint32_t type, uint32_t opcode,
return result;
}
uint32_t EmitF32ToF16RtzBits(EmitterState& state, uint32_t value) {
uint32_t EmitF32ToU32(EmitterState& state, uint32_t src, bool signed_value) {
// Clamping first keeps the truncating conversion defined; NaN and values past the largest
// float below the integer limit are selected afterwards.
const auto clamped =
EmitExt(state, TypeF32(state), GLSLstd450NClamp,
{src, ConstantF32(state, signed_value ? 0xcf000000u : 0u),
ConstantF32(state, signed_value ? 0x4effffffu : 0x4f7fffffu)});
const auto converted = signed_value
? Unary(state, spv::OpBitcast, TypeU32(state),
Unary(state, spv::OpConvertFToS, TypeI32(state), clamped))
: Unary(state, spv::OpConvertFToU, TypeU32(state), clamped);
const auto above = Binary(state, spv::OpFOrdGreaterThanEqual, TypeBool(state), src,
ConstantF32(state, signed_value ? 0x4f000000u : 0x4f800000u));
const auto high =
Select(state, TypeU32(state), above,
ConstantU32(state, signed_value ? 0x7fffffffu : 0xffffffffu), converted);
return Select(state, TypeU32(state), Unary(state, spv::OpIsNan, TypeBool(state), src),
ConstantU32(state, 0), high);
}
} // namespace
uint32_t EmitF32BitsToF16RtzBits(EmitterState& state, uint32_t bits) {
// Native half conversion causes regressions; use compact integer RTZ conversion.
const auto bits = EmitBitcastF32ToU32(state, value);
const auto abs = EmitAndConstant(state, bits, 0x7fffffffu);
const auto sign = EmitAndConstant(
state, Binary(state, spv::OpShiftRightLogical, TypeU32(state), bits, ConstantU32(state, 16)),
@@ -92,28 +113,6 @@ uint32_t EmitF32ToF16RtzBits(EmitterState& state, uint32_t value) {
return EmitOrU32(state, sign, magnitude);
}
uint32_t EmitF32ToU32(EmitterState& state, uint32_t src, bool signed_value) {
// Clamping first keeps the truncating conversion defined; NaN and values past the largest
// float below the integer limit are selected afterwards.
const auto clamped =
EmitExt(state, TypeF32(state), GLSLstd450NClamp,
{src, ConstantF32(state, signed_value ? 0xcf000000u : 0u),
ConstantF32(state, signed_value ? 0x4effffffu : 0x4f7fffffu)});
const auto converted = signed_value
? Unary(state, spv::OpBitcast, TypeU32(state),
Unary(state, spv::OpConvertFToS, TypeI32(state), clamped))
: Unary(state, spv::OpConvertFToU, TypeU32(state), clamped);
const auto above = Binary(state, spv::OpFOrdGreaterThanEqual, TypeBool(state), src,
ConstantF32(state, signed_value ? 0x4f000000u : 0x4f800000u));
const auto high =
Select(state, TypeU32(state), above,
ConstantU32(state, signed_value ? 0x7fffffffu : 0xffffffffu), converted);
return Select(state, TypeU32(state), Unary(state, spv::OpIsNan, TypeBool(state), src),
ConstantU32(state, 0), high);
}
} // namespace
uint32_t EmitFPFma32(EmitterState& state, uint32_t a, uint32_t b, uint32_t c) {
const auto result = EmitGlsl<GLSLstd450Fma, IR::Type::F32>(state, a, b, c);
state.builder.AddAnnotation(spv::OpDecorate, result, spv::DecorationNoContraction);
@@ -199,7 +198,8 @@ uint32_t EmitCompositeExtractU32x2(EmitterState& state, uint32_t arg0, IR::Value
uint32_t EmitPackFloat2x16Rtz(EmitterState& state, uint32_t arg0, uint32_t arg1) {
return EmitNative<spv::OpBitFieldInsert, IR::Type::U32>(
state, EmitF32ToF16RtzBits(state, arg0), EmitF32ToF16RtzBits(state, arg1),
state, EmitF32BitsToF16RtzBits(state, EmitBitcastF32ToU32(state, arg0)),
EmitF32BitsToF16RtzBits(state, EmitBitcastF32ToU32(state, arg1)),
ConstantU32(state, 16), ConstantU32(state, 16));
}
@@ -30,6 +30,13 @@ bool PixelParameterIsCustom(const EmitterState& state, uint32_t attr) {
ShaderPixelParameterIsCustom(*state.input_info.pixel, attr);
}
uint32_t MrtOutputType(EmitterState& state, uint32_t index) {
const auto& pixel = *state.input_info.pixel;
if ((pixel.target_sint_mask & (1u << index)) != 0u) return TypeI32Vector(state, 4);
if ((pixel.target_uint_mask & (1u << index)) != 0u) return TypeU32Vector(state, 4);
return TypeF32Vector(state, 4);
}
uint32_t OutputVariableForExport(const EmitterState& state, const IR::ExportInfo& exp) {
if (exp.kind == IR::ExportTargetKind::Position && exp.index == 0) {
return state.per_vertex_variable;
@@ -114,6 +114,18 @@ uint32_t EmitBuiltinU32(EmitterState& state, IR::StageInputKind kind, uint32_t c
return EmitInputComponentU32(state, kind, component);
}
// Whether EXEC enables a source lane within the caller's 32-lane half.
uint32_t EmitSourceLaneActive(ValueEmitContext& ctx, uint32_t exec, uint32_t lane) {
auto& state = ctx.state;
const auto scope = ConstantU32(state, spv::ScopeSubgroup);
const auto ballot = EmitNative<spv::OpGroupNonUniformBallot, IR::Type::U32x4>(state, scope, exec);
if (ctx.half != 0) {
lane = EmitAndConstant(state, lane, 31);
}
return EmitNative<spv::OpGroupNonUniformBallotBitExtract, IR::Type::U1>(state, scope, ballot,
lane);
}
uint32_t EmitDppWriteCondition(ValueEmitContext& ctx, const IR::DppMoveFlags& flags,
uint32_t exec) {
auto& state = ctx.state;
@@ -135,9 +147,24 @@ uint32_t EmitDppWriteCondition(ValueEmitContext& ctx, const IR::DppMoveFlags& fl
// Only row shifts (0x101-0x10f, 0x111-0x11f) can vacate a lane; other controls always read one.
const bool row_shift = flags.control > 0x100u && flags.control < 0x120u &&
flags.control != 0x110u;
if (!flags.bound_control && row_shift) {
if (flags.bound_control || (flags.fetch_inactive && !row_shift)) {
return writable;
}
// Without BOUND_CTRL, a vacated source or, without FI, an EXEC-disabled one disables the write.
const auto target = EmitDppTargetLane(state, flags);
if (row_shift) {
writable = Binary(state, spv::OpLogicalAnd, TypeBool(state), writable, target.valid);
}
if (!flags.fetch_inactive) {
// Vacated lanes target outside their row; wrapping keeps the ballot index in range.
auto source = target.lane;
if (row_shift) {
lane = lane != 0 ? lane : EmitSubgroupLocalInvocationId(state);
source = EmitNative<spv::OpBitFieldInsert, IR::Type::U32>(
state, lane, source, ConstantU32(state, 0), ConstantU32(state, 4));
}
writable = Binary(state, spv::OpLogicalAnd, TypeBool(state), writable,
EmitDppTargetLane(state, flags).valid);
EmitSourceLaneActive(ctx, exec, source));
}
return writable;
}
@@ -264,9 +291,10 @@ uint32_t ExportRawComponent(ValueEmitContext& ctx, uint32_t vector, uint32_t com
}
uint32_t ExportVector(ValueEmitContext& ctx, uint32_t data, const IR::ExportInfo& exp,
bool uint_output) {
auto& state = ctx.state;
if (exp.compr && !uint_output) {
uint32_t vector_type) {
auto& state = ctx.state;
const bool integer_output = vector_type != TypeF32Vector(state, 4);
if (exp.compr && !integer_output) {
auto unpack = GLSLstd450UnpackHalf2x16;
switch (MrtOutputMode(state, exp)) {
case 5: unpack = GLSLstd450UnpackUnorm2x16; break;
@@ -303,9 +331,11 @@ uint32_t ExportVector(ValueEmitContext& ctx, uint32_t data, const IR::ExportInfo
ConstantU32(state, 0),
ConstantU32(state, 0),
ConstantU32(state, 0),
ConstantU32(state, uint_output ? 1u : 0x3f800000u),
ConstantU32(state, integer_output ? 1u : 0x3f800000u),
};
if (exp.compr) {
const auto extract = vector_type == TypeU32Vector(state, 4) ? spv::OpBitFieldUExtract
: spv::OpBitFieldSExtract;
for (uint32_t pair = 0; pair < 2u; pair++) {
if ((exp.en & (3u << (pair * 2u))) == 0u) {
continue;
@@ -317,8 +347,8 @@ uint32_t ExportVector(ValueEmitContext& ctx, uint32_t data, const IR::ExportInfo
continue;
}
raw[component] = state.builder.AllocateId();
state.builder.AddFunction(spv::OpBitFieldUExtract, TypeU32(state), raw[component],
packed, ConstantU32(state, lane * 16u),
state.builder.AddFunction(extract, TypeU32(state), raw[component], packed,
ConstantU32(state, lane * 16u),
ConstantU32(state, 16));
}
}
@@ -333,12 +363,10 @@ uint32_t ExportVector(ValueEmitContext& ctx, uint32_t data, const IR::ExportInfo
state.builder.AddFunction(spv::OpCompositeConstruct, TypeU32Vector(state, 4), data,
raw[0], raw[1], raw[2], raw[3]);
}
if (uint_output) {
if (vector_type == TypeU32Vector(state, 4)) {
return data;
}
const auto vector = state.builder.AllocateId();
state.builder.AddFunction(spv::OpBitcast, TypeF32Vector(state, 4), vector, data);
return vector;
return Unary(state, spv::OpBitcast, vector_type, data);
}
uint32_t DistancePointer(EmitterState& state, IR::StageOutputKind kind, uint32_t index) {
@@ -536,11 +564,11 @@ void EmitSetAttribute(ValueEmitContext& ctx, const IR::Inst& inst) {
if (state.program.stage != ShaderType::Mesh && variable == 0) {
return;
}
const bool uint_output = MrtOutputMode(state, exp) == 7u;
const auto vector_type = uint_output ? TypeU32Vector(state, 4) : TypeF32Vector(state, 4);
auto value = ExportVector(ctx, data, exp, uint_output);
const auto vector_type = MrtOutputMode(state, exp) != 0u ? MrtOutputType(state, exp.index)
: TypeF32Vector(state, 4);
auto value = ExportVector(ctx, data, exp, vector_type);
if (state.program.stage == ShaderType::Pixel && exp.kind == IR::ExportTargetKind::Mrt &&
exp.index == 0 && !uint_output &&
exp.index == 0 && vector_type == TypeF32Vector(state, 4) &&
state.input_info.pixel->alpha_blend_source != ShaderAlphaBlendSource::None) {
// Broadcast logical alpha before swizzling the primary output.
const auto blend_output =
@@ -684,9 +712,16 @@ void EmitBarrier(EmitterState& state) {
return;
}
const auto memory_scope = tessellation ? spv::ScopeInvocation : spv::ScopeWorkgroup;
const auto memory = state.lds_storage_class == spv::StorageClassStorageBuffer
? spv::MemorySemanticsUniformMemoryMask
: spv::MemorySemanticsWorkgroupMemoryMask;
auto memory = state.lds_storage_class == spv::StorageClassStorageBuffer
? spv::MemorySemanticsUniformMemoryMask
: spv::MemorySemanticsWorkgroupMemoryMask;
// Shaders read other waves' buffer and image stores with plain loads right after S_BARRIER.
const auto written = [](const auto& resource) { return resource.written; };
if (std::ranges::any_of(state.program.info.buffers, written) ||
std::ranges::any_of(state.program.info.images, written)) {
memory = memory | spv::MemorySemanticsUniformMemoryMask |
spv::MemorySemanticsImageMemoryMask;
}
const auto semantics = tessellation ? spv::MemorySemanticsMaskNone
: spv::MemorySemanticsAcquireReleaseMask | memory;
state.builder.AddFunction(spv::OpControlBarrier, ConstantU32(state, spv::ScopeWorkgroup),
@@ -784,6 +819,13 @@ uint32_t EmitBallot(ValueEmitContext& ctx, IR::Value predicate) {
return ctx.Ballot(predicate);
}
uint32_t EmitLiveMask(EmitterState& state) {
const auto helper =
EmitNative<spv::OpLoad, IR::Type::U1>(state, state.helper_invocation_variable);
return EmitNative<spv::OpGroupNonUniformBallot, IR::Type::U32x4>(
state, ConstantU32(state, spv::ScopeSubgroup), EmitLogicalNotBool(state, helper));
}
uint32_t EmitReadFirstLane(ValueEmitContext& ctx, const IR::Inst& inst) {
const auto ballot = ctx.Ballot(inst.Arg(1));
const auto lane = ctx.FirstLane(ballot);
@@ -794,18 +836,28 @@ uint32_t EmitReadLane(ValueEmitContext& ctx, const IR::Inst& inst) {
return ctx.Shuffle(inst, 0, ctx.Arg(inst, 1));
}
uint32_t EmitSubgroupUMinU32(ValueEmitContext& ctx, const IR::Inst& inst) {
template <spv::Op Reduce, auto Combine>
static uint32_t EmitSubgroupReduceU32(ValueEmitContext& ctx, const IR::Inst& inst) {
auto& state = ctx.state;
auto value = ctx.Arg(inst, 0);
if (ctx.other_half != nullptr) {
value = EmitUMin32(state, ctx.HalfArg(inst, 0, 0), ctx.HalfArg(inst, 0, 1));
value = Combine(state, ctx.HalfArg(inst, 0, 0), ctx.HalfArg(inst, 0, 1));
}
const auto result = state.builder.AllocateId();
state.builder.AddFunction(spv::OpGroupNonUniformUMin, TypeU32(state), result,
ConstantU32(state, spv::ScopeSubgroup), spv::GroupOperationReduce, value);
state.builder.AddFunction(Reduce, TypeU32(state), result,
ConstantU32(state, spv::ScopeSubgroup), spv::GroupOperationReduce,
value);
return result;
}
uint32_t EmitSubgroupUMinU32(ValueEmitContext& ctx, const IR::Inst& inst) {
return EmitSubgroupReduceU32<spv::OpGroupNonUniformUMin, EmitUMin32>(ctx, inst);
}
uint32_t EmitSubgroupUMaxU32(ValueEmitContext& ctx, const IR::Inst& inst) {
return EmitSubgroupReduceU32<spv::OpGroupNonUniformUMax, EmitUMax32>(ctx, inst);
}
uint32_t EmitWriteLane(ValueEmitContext& ctx, const IR::Inst& inst) {
auto& state = ctx.state;
const auto hit = state.builder.AllocateId();
@@ -816,27 +868,23 @@ uint32_t EmitWriteLane(ValueEmitContext& ctx, const IR::Inst& inst) {
}
uint32_t EmitPermlane16U32(ValueEmitContext& ctx, const IR::Inst& inst) {
auto& state = ctx.state;
const auto flags = inst.Flags<IR::PermlaneFlags>();
const auto subid = EmitSubgroupLocalInvocationId(state);
const auto row = state.builder.AllocateId();
const auto row_value = state.builder.AllocateId();
const auto lane = state.builder.AllocateId();
const auto lane8 = state.builder.AllocateId();
const auto shift = state.builder.AllocateId();
const auto upper = state.builder.AllocateId();
const auto selected = state.builder.AllocateId();
const auto shifted = state.builder.AllocateId();
const auto index = state.builder.AllocateId();
const auto target = state.builder.AllocateId();
auto& state = ctx.state;
const auto flags = inst.Flags<IR::PermlaneFlags>();
const auto subid = EmitSubgroupLocalInvocationId(state);
const auto row = state.builder.AllocateId();
const auto lane = state.builder.AllocateId();
const auto lane8 = state.builder.AllocateId();
const auto shift = state.builder.AllocateId();
const auto upper = state.builder.AllocateId();
const auto selected = state.builder.AllocateId();
const auto shifted = state.builder.AllocateId();
const auto index = state.builder.AllocateId();
const auto target = state.builder.AllocateId();
state.builder.AddFunction(spv::OpBitwiseAnd, TypeU32(state), row, subid,
ConstantU32(state, 0xfffffff0u));
if (flags.x16) {
state.builder.AddFunction(spv::OpBitwiseXor, TypeU32(state), row_value, row,
ConstantU32(state, 16));
} else {
state.builder.AddFunction(spv::OpCopyObject, TypeU32(state), row_value, row);
}
const auto row_value =
flags.x16 ? Binary(state, spv::OpBitwiseXor, TypeU32(state), row, ConstantU32(state, 16))
: row;
state.builder.AddFunction(spv::OpBitwiseAnd, TypeU32(state), lane, subid,
ConstantU32(state, 15));
state.builder.AddFunction(spv::OpBitwiseAnd, TypeU32(state), lane8, lane,
@@ -852,14 +900,11 @@ uint32_t EmitPermlane16U32(ValueEmitContext& ctx, const IR::Inst& inst) {
ConstantU32(state, 15));
state.builder.AddFunction(spv::OpBitwiseOr, TypeU32(state), target, row_value, index);
const auto shuffled = ctx.Shuffle(inst, 0, target);
uint32_t result = shuffled;
if (!flags.fetch_inactive) {
const auto source_exec = ctx.Shuffle(inst, 3, target);
result = state.builder.AllocateId();
state.builder.AddFunction(spv::OpSelect, TypeU32(state), result, source_exec, shuffled,
ConstantU32(state, 0));
if (flags.fetch_inactive) {
return shuffled;
}
return result;
return Select(state, TypeU32(state), EmitSourceLaneActive(ctx, ctx.Arg(inst, 3), target),
shuffled, ctx.Arg(inst, 4));
}
uint32_t EmitGetAttribute(ValueEmitContext& ctx, const IR::Inst& inst) {
@@ -77,6 +77,7 @@ ImageSampleLayout Layout(const IR::MemoryInfo& mem) {
layout.coord = cursor;
cursor += info.coordinate_components;
if (HasFlag(mem, Decoder::ImageSampleFlagLod)) layout.lod = cursor++;
if (HasFlag(mem, Decoder::ImageSampleFlagLodClamp)) layout.clamp = cursor++;
return layout;
}
@@ -1069,7 +1070,12 @@ void EmitImage(ValueEmitContext& ctx, const IR::Inst& inst) {
dref_value = AddressF32(ctx, mem, *address, layout.dref);
}
}
std::array<uint32_t, 4> operands {};
// _cl clamps the computed LOD from below.
uint32_t min_lod = 0;
if (layout.clamp != NoImageComponent) {
min_lod = AddressF32(ctx, mem, *address, layout.clamp);
}
std::array<uint32_t, 5> operands {};
size_t operand_count = 0;
if (HasFlag(mem, Decoder::ImageSampleFlagDerivative)) {
operands[0] = spv::ImageOperandsGradMask;
@@ -1081,6 +1087,11 @@ void EmitImage(ValueEmitContext& ctx, const IR::Inst& inst) {
if (HasFlag(mem, Decoder::ImageSampleFlagLod) && layout.lod != NoImageComponent) {
lod = AddressF32(ctx, mem, *address, layout.lod);
}
// SPIR-V takes MinLod only with implicit LOD or Grad.
if (min_lod != 0) {
lod = EmitGlsl<GLSLstd450FMax, IR::Type::F32>(state, lod, min_lod);
min_lod = 0;
}
operands[0] = spv::ImageOperandsLodMask;
operands[1] = lod;
operand_count = 2;
@@ -1104,6 +1115,13 @@ void EmitImage(ValueEmitContext& ctx, const IR::Inst& inst) {
sample_operands[sample_operand_count++] = PackedOffset(
ctx, offset_value, candidate.dimension, sample_operands[0]);
}
// MinLod follows the offset in operand order.
if (min_lod != 0) {
state.builder.RequireCapability(spv::CapabilityMinLod);
sample_operand_count = std::max(size_t{1}, sample_operand_count);
sample_operands[0] |= spv::ImageOperandsMinLodMask;
sample_operands[sample_operand_count++] = min_lod;
}
const auto coord =
CoordF32(ctx, mem, *address, layout.coord,
ImageDimensionInfoFor(candidate.dimension).coordinate_components,
@@ -56,6 +56,7 @@ EMIT_NATIVE(BitFieldSExtract, OpBitFieldSExtract, U32, uint32_t, uint32_t, uint3
EMIT_NATIVE(SelectU1, OpSelect, U1, uint32_t, uint32_t, uint32_t)
EMIT_NATIVE(SelectU32, OpSelect, U32, uint32_t, uint32_t, uint32_t)
EMIT_NATIVE(SelectF32, OpSelect, F32, uint32_t, uint32_t, uint32_t)
EMIT_NATIVE(SelectF64, OpSelect, F64, uint32_t, uint32_t, uint32_t)
EMIT_NATIVE(IAdd32, OpIAdd, U32, uint32_t, uint32_t)
EMIT_NATIVE(ISub32, OpISub, U32, uint32_t, uint32_t)
EMIT_NATIVE(IMul32, OpIMul, U32, uint32_t, uint32_t)
@@ -229,10 +230,12 @@ uint32_t EmitDppUpdateU32(ValueEmitContext& ctx, const IR::Inst& in
uint32_t EmitWqmU64(EmitterState& state, uint32_t value);
uint32_t EmitLaneId(EmitterState& state);
uint32_t EmitBallot(ValueEmitContext& ctx, IR::Value predicate);
uint32_t EmitLiveMask(EmitterState& state);
uint32_t EmitConditionRef(ValueEmitContext& ctx, const IR::Inst& inst);
uint32_t EmitReadFirstLane(ValueEmitContext& ctx, const IR::Inst& inst);
uint32_t EmitReadLane(ValueEmitContext& ctx, const IR::Inst& inst);
uint32_t EmitSubgroupUMinU32(ValueEmitContext& ctx, const IR::Inst& inst);
uint32_t EmitSubgroupUMaxU32(ValueEmitContext& ctx, const IR::Inst& inst);
uint32_t EmitWriteLane(ValueEmitContext& ctx, const IR::Inst& inst);
uint32_t EmitPermlane16U32(ValueEmitContext& ctx, const IR::Inst& inst);
uint32_t EmitGetAttribute(ValueEmitContext& ctx, const IR::Inst& inst);
@@ -112,6 +112,7 @@ struct EmitterState {
uint32_t lane_half = 0;
std::array<BufferDefinition, 4> storage_buffers {};
std::vector<uint32_t> memory_byte_offsets;
std::vector<uint32_t> memory_byte_sizes;
uint32_t bda_pagetable_variable = 0;
uint32_t fault_buffer_variable = 0;
uint32_t bda_pointer_function = 0;
@@ -145,6 +146,7 @@ struct EmitterState {
const IR::Block* current_block = nullptr;
uint32_t pixel_valid_mask_variable = 0;
uint32_t subgroup_local_invocation_id_variable = 0;
uint32_t helper_invocation_variable = 0;
uint32_t per_vertex_variable = 0;
uint32_t point_size_variable = 0;
uint32_t clip_distance_variable = 0;
@@ -282,6 +284,7 @@ struct ImageSampleLayout {
uint32_t bias = NoImageComponent;
uint32_t coord = 0;
uint32_t lod = NoImageComponent;
uint32_t clamp = NoImageComponent;
uint32_t grad_x = NoImageComponent;
uint32_t grad_y = NoImageComponent;
};
@@ -305,6 +308,7 @@ uint32_t VertexParameterScalarType(EmitterState& state, VertexInputScalarKind ki
uint32_t OutputVariableForExport(const EmitterState& state, const IR::ExportInfo& exp);
uint32_t MrtOutputType(EmitterState& state, uint32_t index);
uint32_t ConstantU32(EmitterState& state, uint32_t value);
@@ -414,7 +418,7 @@ Prospero::BufferFormat StorageBufferFormat(const EmitterState& state, const IR::
uint32_t SharedIndirectBufferResource(const EmitterState& state, uint32_t resource);
void EmitMemoryOffsets(EmitterState& state);
void EmitMemoryBindings(EmitterState& state);
uint32_t LdsDwordCount(const EmitterState& state);
void EnsureLdsStorage(EmitterState& state);
@@ -531,6 +535,7 @@ inline constexpr auto EmitFNegateValue = EmitNative<spv::OpFNegate, IR::Type::F3
inline constexpr auto EmitFAbsValue = EmitGlsl<GLSLstd450FAbs, IR::Type::F32, uint32_t>;
uint32_t EmitF16BitsToF32(EmitterState& state, uint32_t bits);
uint32_t EmitF32BitsToF16RtzBits(EmitterState& state, uint32_t bits);
void EmitProgram(EmitterState& state);
@@ -1541,16 +1541,16 @@ uint32_t EmitAppendConsume(ValueEmitContext& ctx, const IR::Inst& inst) {
const auto index =
Binary(state, spv::OpShiftRightLogical, TypeU32(state), address, ConstantU32(state, 2));
const auto access = PrepareMemoryResourceAccess(state, mem);
const auto exec = ctx.Arg(inst, 1);
const auto ballot = ctx.Ballot(inst.Arg(1));
const auto low = state.builder.AllocateId();
const auto high = state.builder.AllocateId();
state.builder.AddFunction(spv::OpCompositeExtract, TypeU32(state), low, ballot, 0);
state.builder.AddFunction(spv::OpCompositeExtract, TypeU32(state), high, ballot, 1);
// Elect and count from the guest EXEC words, which V_MBCNT also ranks lanes by.
const auto low = ctx.Arg(inst, 1);
const auto high = ctx.Arg(inst, 2);
const auto count = Binary(state, spv::OpIAdd, TypeU32(state),
Unary(state, spv::OpBitCount, TypeU32(state), low),
Unary(state, spv::OpBitCount, TypeU32(state), high));
const auto first = ctx.FirstLane(ballot);
const auto mask = state.builder.AllocateId();
state.builder.AddFunction(spv::OpCompositeConstruct, TypeU32Vector(state, 4), mask, low, high,
ConstantU32(state, 0), ConstantU32(state, 0));
const auto first = ctx.FirstLane(mask);
const auto source_lane =
state.lane_count == 2
? Binary(state, spv::OpBitwiseAnd, TypeU32(state), first, ConstantU32(state, 31))
@@ -1564,9 +1564,7 @@ uint32_t EmitAppendConsume(ValueEmitContext& ctx, const IR::Inst& inst) {
const auto condition = AndCondition(
state, is_first,
AndCondition(state,
state.lane_count == 2 ? Binary(state, spv::OpINotEqual, TypeBool(state), count,
ConstantU32(state, 0))
: exec,
Binary(state, spv::OpINotEqual, TypeBool(state), count, ConstantU32(state, 0)),
bounds));
const auto atomic = EmitValueOrZeroIfCondition(state, condition, [&]() {
const auto value = state.builder.AllocateId();
@@ -154,9 +154,10 @@ uint32_t SharedIndirectBufferResource(const EmitterState& state, uint32_t resour
return representative;
}
void EmitMemoryOffsets(EmitterState& state) {
void EmitMemoryBindings(EmitterState& state) {
const auto count = state.program.bindings.descriptor_counts[static_cast<size_t>(IR::DescriptorBindingKind::Buffers)];
state.memory_byte_offsets.resize(count);
state.memory_byte_sizes.resize(count);
std::vector<bool> dynamic_offsets(count);
for (uint32_t resource = 0; resource < state.program.info.buffers.size(); ++resource) {
if (SharedIndirectBufferResource(state, resource) == UINT32_MAX) continue;
@@ -174,6 +175,8 @@ void EmitMemoryOffsets(EmitterState& state) {
if (dynamic_offsets[i + lane]) continue;
state.memory_byte_offsets[i + lane] = EmitNative<spv::OpBitFieldUExtract, IR::Type::U32>(
state, word, ConstantU32(state, lane * 8u), ConstantU32(state, 8u));
state.memory_byte_sizes[i + lane] =
EmitShaderDataDwordLoad(state, state.program.bindings.MemorySizeDword() + i + lane);
}
}
}
@@ -257,22 +260,28 @@ MemoryResourceAccess PrepareStorageBufferResourceAccess(EmitterState& state,
state.builder.RequireCapability(spv::CapabilityShaderNonUniform);
state.builder.RequireCapability(spv::CapabilityStorageBufferArrayNonUniformIndexing);
state.builder.AddAnnotation(spv::OpDecorate, access.object_pointer, spv::DecorationNonUniform);
const auto offsets_start = state.program.bindings.memory_offset_dword +
(state.program.bindings.UsesPushData() ? state.program.bindings.push_data_start_dword : 0u);
const auto& bindings = state.program.bindings;
const auto data_start = bindings.UsesPushData() ? bindings.push_data_start_dword : 0u;
const auto word = EmitShaderDataDwordLoadAt(state, EmitAddU32(
state, ConstantU32(state, offsets_start),
state, ConstantU32(state, bindings.memory_offset_dword + data_start),
Binary(state, spv::OpShiftRightLogical, TypeU32(state), array_index, ConstantU32(state, 2))));
const auto shift = Binary(state, spv::OpShiftLeftLogical, TypeU32(state),
Binary(state, spv::OpBitwiseAnd, TypeU32(state), array_index, ConstantU32(state, 3)),
ConstantU32(state, 3));
access.byte_offset = EmitNative<spv::OpBitFieldUExtract, IR::Type::U32>(
state, word, shift, ConstantU32(state, 8));
access.length = EmitShaderDataDwordLoadAt(state, EmitAddU32(
state, ConstantU32(state, bindings.MemorySizeDword() + data_start), array_index));
} else {
access.byte_offset = state.memory_byte_offsets[static_index];
access.length = state.memory_byte_sizes[static_index];
}
// storage_buffers[i] views the binding as 1 << i byte elements.
const auto shift = static_cast<uint32_t>(&buffer - state.storage_buffers.data());
if (shift != 0u) {
access.length = Binary(state, spv::OpShiftRightLogical, TypeU32(state), access.length,
ConstantU32(state, shift));
}
access.length = state.builder.AllocateId();
state.builder.AddFunction(spv::OpArrayLength, TypeU32(state), access.length,
access.object_pointer, 0);
return access;
}
@@ -472,6 +481,17 @@ uint32_t EncodeFormattedStoreComponent(EmitterState& state, const Format::Buffer
UnormRounding rounding) {
EXIT_IF(rounding != UnormRounding::RoundEven && info.type != Format::ComponentType::Unorm);
const auto bits = info.component_bits[component];
if (info.type == Format::ComponentType::Float && bits < 16u) {
// Truncate to f16, whose bias the unsigned 11/10-bit floats share, and drop mantissa
// bits; negatives and NaN, whose f16 bits lie above +inf, store zero.
const auto half = data_bits == 16u ? EmitAndConstant(state, data, 0xffffu)
: EmitF32BitsToF16RtzBits(state, data);
return Select(state, TypeU32(state),
EmitCompareU32Constant(state, spv::OpUGreaterThan, half, 0x7c00u),
ConstantU32(state, 0),
EmitBinaryU32(state, spv::OpShiftRightLogical, half,
ConstantU32(state, 15u - bits)));
}
if (data_bits == 16u) {
if (info.type == Format::ComponentType::Uint) {
data = EmitBitFieldUExtract(state, data, ConstantU32(state, 0), ConstantU32(state, 16));
@@ -595,6 +595,13 @@ void DefineInputs(EmitterState& state) {
state.builder.AddAnnotation(spv::OpDecorate, variable, spv::DecorationFlat);
}
}
if (state.program.info.helper_invocation) {
const auto variable = DefineInterfaceVariable(state, TypeBool(state), spv::StorageClassInput,
"gl_HelperInvocation");
state.helper_invocation_variable = variable;
state.builder.AddAnnotation(spv::OpDecorate, variable, spv::DecorationBuiltIn,
spv::BuiltInHelperInvocation);
}
}
void DefineOutputs(EmitterState& state) {
@@ -691,13 +698,15 @@ void DefineOutputs(EmitterState& state) {
break;
case IR::StageOutputKind::Parameter:
case IR::StageOutputKind::Mrt: {
const bool uint_output =
binding.kind == IR::StageOutputKind::Mrt &&
state.program.stage == ShaderType::Pixel &&
binding.index < std::size(state.input_info.pixel->target_output_mode) &&
(state.input_info.pixel->target_output_mode[binding.index] == 7u ||
(state.input_info.pixel->target_packed_unorm_mask & (1u << binding.index)) != 0u);
const auto type = uint_output ? TypeU32Vector(state, 4) : TypeF32Vector(state, 4);
auto type = TypeF32Vector(state, 4);
if (binding.kind == IR::StageOutputKind::Mrt &&
state.program.stage == ShaderType::Pixel && binding.index < 8u) {
// Packed UNorm stores its encoded bits.
const auto bit = 1u << binding.index;
type = (state.input_info.pixel->target_packed_unorm_mask & bit) != 0u
? TypeU32Vector(state, 4)
: MrtOutputType(state, binding.index);
}
binding.variable_id = DefineInterfaceVariable(state, type, spv::StorageClassOutput,
binding.debug_name.c_str());
const bool dual_source = binding.kind == IR::StageOutputKind::Mrt &&
@@ -235,7 +235,7 @@ void EmitDirectInstruction(ValueEmitContext& ctx, const IR::Inst& inst) {
const auto op = inst.GetOpcode();
if (ctx.half != 0 &&
(op == Op::Ballot || op == Op::ReadFirstLane || op == Op::SubgroupUMinU32 ||
op == Op::ReadConstBuffer || op == Op::ReadRealtimeClock ||
op == Op::SubgroupUMaxU32 || op == Op::ReadConstBuffer || op == Op::ReadRealtimeClock ||
(op == Op::LoadAddressU32 && ctx.Memory(inst).kind == IR::ResourceKind::ScalarAddress))) {
// Whole-wave results, clocks and SMEM loads (SGPR operands, no EXEC) match in both halves.
ctx.Define(inst, ctx.other_half->Result(inst));
@@ -746,7 +746,7 @@ void EmitProgram(EmitterState& state) {
state.lds_base_dwords = EmitBinaryU32(state, spv::OpIMul, index,
ConstantU32(state, LdsDwordCount(state)));
}
EmitMemoryOffsets(state);
EmitMemoryBindings(state);
if (program.blocks.empty()) {
EmitReturn(ctx);
} else if (state.program.dispatcher_fallback) {
@@ -1710,21 +1710,25 @@ Graph Structurize(const Graph& graph) {
return GotoStructurizer(graph).Run();
}
bool MayWriteScalarRegister(const Decoder::Instruction& inst, uint32_t code) {
// Vector comparisons and carry results can write a scalar pair in wave64.
bool MayWriteScalarRegister(const Decoder::Instruction& inst, uint32_t code, uint32_t wave_size) {
// A vector lane mask (comparison or carry) spans wave_size / 32 SGPRs; a lane read writes one.
if (inst.family == Decoder::Family::VOP1 || inst.family == Decoder::Family::VOP2 ||
inst.family == Decoder::Family::VOPC || inst.family == Decoder::Family::VOP3 ||
inst.family == Decoder::Family::VOP3P) {
const bool lane_read =
inst.opcode == Opcode::V_READLANE_B32 || inst.opcode == Opcode::V_READFIRSTLANE_B32;
const uint32_t size = lane_read ? 1u : wave_size / 32u;
uint32_t destination = 0;
for (const auto* operand: {&inst.dst, &inst.dst2})
if (ScalarOperandCode(*operand, destination) && code >= destination && code <= destination + 1u)
if (ScalarOperandCode(*operand, destination) && code >= destination && code < destination + size)
return true;
}
return InstructionWritesScalarCode(inst, code);
}
uint32_t FindScalarDefinition(const Decoder::Program& program, const Graph& graph,
uint32_t before, uint32_t code, std::vector<uint32_t>& crossed_calls) {
uint32_t before, uint32_t code, uint32_t wave_size,
std::vector<uint32_t>& crossed_calls) {
EXIT_IF(before >= program.instructions.size());
const auto found = std::ranges::find_if(graph.blocks, [before](const auto& block) {
return block.inst_begin <= before && before < block.inst_end;
@@ -1743,11 +1747,11 @@ uint32_t FindScalarDefinition(const Decoder::Program& program, const Graph& grap
for (uint32_t i = point.second; i > block->inst_begin;) {
const auto& inst = program.instructions[--i];
if (inst.opcode == Opcode::S_SWAPPC_B64) {
EXIT_NOT_IMPLEMENTED(MayWriteScalarRegister(inst, code));
EXIT_NOT_IMPLEMENTED(MayWriteScalarRegister(inst, code, wave_size));
crossed_calls.push_back(i);
continue;
}
if (MayWriteScalarRegister(inst, code)) { index = i; break; }
if (MayWriteScalarRegister(inst, code, wave_size)) { index = i; break; }
}
if (index != UINT32_MAX || block->id == graph.entry_block) {
EXIT_IF(definition && *definition != index);
@@ -113,9 +113,10 @@ struct Graph {
};
Graph BuildGraph(const Decoder::Program& program);
bool MayWriteScalarRegister(const Decoder::Instruction& inst, uint32_t code);
bool MayWriteScalarRegister(const Decoder::Instruction& inst, uint32_t code, uint32_t wave_size);
uint32_t FindScalarDefinition(const Decoder::Program& program, const Graph& graph,
uint32_t before, uint32_t code, std::vector<uint32_t>& crossed_calls);
uint32_t before, uint32_t code, uint32_t wave_size,
std::vector<uint32_t>& crossed_calls);
// Returns structured control flow or failure diagnostics without changing the native graph.
// On failure, failure_block is an original block ID or UINT32_MAX.
Graph Structurize(const Graph& graph);
@@ -73,7 +73,7 @@ constexpr uint32_t ImageSampleAddressComponents(uint32_t flags, ImageDimension d
if ((flags & ImageSampleFlagBias) != 0) {
components++;
}
if ((flags & ImageSampleFlagLod) != 0) {
if ((flags & (ImageSampleFlagLod | ImageSampleFlagLodClamp)) != 0) {
components++;
}
if ((flags & ImageSampleFlagDerivative) != 0) {
@@ -258,7 +258,9 @@ void DecodeScalarSource(uint32_t code, uint32_t pc, Operand& operand) {
case 126u: operand.kind = OperandKind::ExecLo; return;
case 127u: operand.kind = OperandKind::ExecHi; return;
case 235u: operand.kind = OperandKind::SharedBase; return;
case 236u: operand.kind = OperandKind::SharedLimit; return;
case 237u: operand.kind = OperandKind::PrivateBase; return;
case 238u: operand.kind = OperandKind::PrivateLimit; return;
case 239u: operand.kind = OperandKind::PopsExitingWaveId; return;
case 248u:
operand.kind = OperandKind::FloatInlineConstant;
@@ -459,7 +461,9 @@ std::string OperandToString(const Operand& operand) {
case OperandKind::M0: text = "m0"; break;
case OperandKind::PopsExitingWaveId: text = "pops_exiting_wave_id"; break;
case OperandKind::SharedBase: text = "shared_base"; break;
case OperandKind::SharedLimit: text = "shared_limit"; break;
case OperandKind::PrivateBase: text = "private_base"; break;
case OperandKind::PrivateLimit: text = "private_limit"; break;
case OperandKind::Null: text = "null"; break;
default: text = "unknown"; break;
}
@@ -766,7 +766,9 @@ enum class OperandKind {
M0,
PopsExitingWaveId,
SharedBase,
SharedLimit,
PrivateBase,
PrivateLimit,
Null,
Vgpr,
};
@@ -5,30 +5,20 @@
namespace Libs::Graphics::ShaderRecompiler::Frontend {
void Translator::EmitCompareResult(const Decoder::Instruction& inst, IR::U1 value, bool scalar,
bool cmpx) {
void Translator::EmitCompareResult(const Decoder::Instruction& inst, IR::U1 value, bool scalar) {
if (scalar) {
ir.SetScc(value);
return;
}
const auto masked = ir.LogicalAnd(ir.GetExec(), value);
if (cmpx) {
const auto mask = BallotMask(masked);
ir.SetExec(masked);
ir.SetExecLo(mask[0]);
ir.SetExecHi(mask[1]);
return;
}
WriteMask(inst.dst, masked);
WriteVectorMask(inst.dst, value);
}
void Translator::EmitCompareConstant(const Decoder::Instruction& inst, bool value, bool scalar,
bool cmpx) {
EmitCompareResult(inst, IR::U1(IR::Value(value)), scalar, cmpx);
void Translator::EmitCompareConstant(const Decoder::Instruction& inst, bool value, bool scalar) {
EmitCompareResult(inst, IR::U1(IR::Value(value)), scalar);
}
void Translator::EmitIntegerCompare(const Decoder::Instruction& inst, IR::ValueOpcode opcode,
IR::Type type, bool scalar, bool cmpx, bool swap_operands) {
IR::Type type, bool scalar, bool swap_operands) {
// The equality compares share their IR opcode with the unsigned ones.
const bool signed_64 = opcode == IR::ValueOpcode::SLessThan64 ||
opcode == IR::ValueOpcode::SLessThanEqual64 ||
@@ -49,18 +39,18 @@ void Translator::EmitIntegerCompare(const Decoder::Instruction& inst, IR::ValueO
if (swap_operands) {
std::swap(lhs, rhs);
}
EmitCompareResult(inst, IR::U1(ir.Emit(opcode, {lhs, rhs})), scalar, cmpx);
EmitCompareResult(inst, IR::U1(ir.Emit(opcode, {lhs, rhs})), scalar);
}
void Translator::EmitInteger16Compare(const Decoder::Instruction& inst, IR::ValueOpcode opcode,
bool signed_value, bool cmpx) {
bool signed_value) {
const auto lhs = ReadU16AsU32(inst.src0, signed_value);
const auto rhs = ReadU16AsU32(inst.src1, signed_value);
EmitCompareResult(inst, IR::U1(ir.Emit(opcode, {lhs, rhs})), false, cmpx);
EmitCompareResult(inst, IR::U1(ir.Emit(opcode, {lhs, rhs})), false);
}
void Translator::EmitFloatCompare(const Decoder::Instruction& inst, IR::ValueOpcode opcode,
bool half, bool cmpx) {
bool half) {
const auto type = IR::ArgTypeOf(opcode, 0);
const auto lhs =
half ? IR::Value(ReadF16AsF32(inst.src0)) : ReadOperand(inst.src0, type);
@@ -68,10 +58,10 @@ void Translator::EmitFloatCompare(const Decoder::Instruction& inst, IR::ValueOpc
half ? IR::Value(ReadF16AsF32(inst.src1)) : ReadOperand(inst.src1, type);
const IR::FPCompareFlags flags{
.flush_input_denorms = !half && type == IR::Type::F32 && flush_f32_inputs};
EmitCompareResult(inst, IR::U1(ir.Emit(opcode, {lhs, rhs}, flags)), false, cmpx);
EmitCompareResult(inst, IR::U1(ir.Emit(opcode, {lhs, rhs}, flags)), false);
}
void Translator::EmitFloatOrderedCompare(const Decoder::Instruction& inst, bool ordered, bool cmpx,
void Translator::EmitFloatOrderedCompare(const Decoder::Instruction& inst, bool ordered,
bool is64) {
const auto type = is64 ? IR::Type::F64 : IR::Type::F32;
const auto is_nan_op = is64 ? IR::ValueOpcode::FPIsNan64 : IR::ValueOpcode::FPIsNan32;
@@ -79,16 +69,16 @@ void Translator::EmitFloatOrderedCompare(const Decoder::Instruction& inst, bool
const auto rhs = ReadOperand(inst.src1, type);
const auto unordered = ir.LogicalOr(IR::U1(ir.Emit(is_nan_op, {lhs})),
IR::U1(ir.Emit(is_nan_op, {rhs})));
EmitCompareResult(inst, ordered ? ir.LogicalNot(unordered) : unordered, false, cmpx);
EmitCompareResult(inst, ordered ? ir.LogicalNot(unordered) : unordered, false);
}
void Translator::EmitFloatClassCompare(const Decoder::Instruction& inst, bool cmpx, bool half) {
void Translator::EmitFloatClassCompare(const Decoder::Instruction& inst, bool half) {
const auto value = half ? IR::Value(Read16LaneBits(inst.src0, false))
: ReadOperand(inst.src0, IR::Type::F32);
const auto mask = half ? IR::Value(Read16LaneBits(inst.src1, false))
: ReadOperand(inst.src1, IR::Type::U32);
const auto opcode = half ? IR::ValueOpcode::FPCmpClass16 : IR::ValueOpcode::FPCmpClass32;
EmitCompareResult(inst, IR::U1(ir.Emit(opcode, {value, mask})), false, cmpx);
EmitCompareResult(inst, IR::U1(ir.Emit(opcode, {value, mask})), false);
}
} // namespace Libs::Graphics::ShaderRecompiler::Frontend
@@ -51,33 +51,33 @@ void Translator::S_SUBVECTOR_LOOP(const Decoder::Instruction& inst, bool begin)
void Translator::S_SAVEEXEC(const Decoder::Instruction& inst, IR::ValueOpcode operation,
bool negate_exec, bool negate_source, bool write_64) {
if (!write_64) {
// Read the encoded scalar word and preserve EXEC_HI, including in wave32.
const auto old = ir.GetExecLo();
const auto src = ReadU32(inst.src0);
const auto combine = [&](IR::U32 old, IR::U32 src) {
const auto lhs = negate_exec ? ir.BitwiseNot(old) : old;
const auto rhs = negate_source ? ir.BitwiseNot(src) : src;
IR::U32 result;
switch (operation) {
case IR::ValueOpcode::LogicalAnd: result = ir.BitwiseAnd(lhs, rhs); break;
case IR::ValueOpcode::LogicalOr: result = ir.BitwiseOr(lhs, rhs); break;
default: EXIT("unsupported SAVEEXEC operation");
}
return operation == IR::ValueOpcode::LogicalAnd ? ir.BitwiseAnd(lhs, rhs)
: ir.BitwiseOr(lhs, rhs);
};
if (!write_64) {
// Read the encoded scalar word and preserve EXEC_HI, including in wave32.
const auto old = ir.GetExecLo();
const auto result = combine(old, ReadU32(inst.src0));
WriteRawU32(inst.dst, old);
WriteRawU32(ConditionOperand(Decoder::OperandKind::ExecLo), result);
ir.SetScc(ir.INotEqual(result, IR::U32(IR::Value(0u))));
return;
}
const auto old = ir.GetExec();
const auto src = ReadMask(inst.src0);
const auto lhs = negate_exec ? ir.LogicalNot(old) : old;
const auto rhs = negate_source ? ir.LogicalNot(src) : src;
const auto result = IR::U1(ir.Emit(operation, {lhs, rhs}));
WriteMask(inst.dst, old, true);
const auto mask = BallotMask(result);
ir.SetExec(result);
ir.SetExecLo(mask[0]);
ir.SetExecHi(mask[1]);
// EXEC words combine bitwise; the per-lane EXEC combines the per-lane masks, so helper lanes
// stay enabled.
const auto old = ir.GetExec();
const auto src = ReadMask(inst.src0);
const auto lhs = negate_exec ? ir.LogicalNot(old) : old;
const auto rhs = negate_source ? ir.LogicalNot(src) : src;
const auto src_mask = ReadU32Pair(inst.src0);
const std::array old_mask {ir.GetExecLo(), ir.GetExecHi()};
const std::array mask {combine(old_mask[0], src_mask[0]), combine(old_mask[1], src_mask[1])};
WriteMask(inst.dst, old, old_mask, true);
WriteMask(ConditionOperand(Decoder::OperandKind::ExecLo), IR::U1(ir.Emit(operation, {lhs, rhs})),
mask);
ir.SetScc(ir.INotEqual(ir.BitwiseOr(mask[0], mask[1]), IR::U32(IR::Value(0u))));
}
@@ -95,7 +95,7 @@ void Translator::ADD_U32(const Decoder::Instruction& inst, bool vector, bool use
}
WriteOperand(DestinationOperand(inst), result);
if (vector) {
WriteMask(inst.dst2, ir.LogicalAnd(ir.GetExec(), carry));
WriteVectorMask(inst.dst2, carry);
} else {
ir.SetScc(carry);
}
@@ -108,7 +108,7 @@ void Translator::SUB_U32(const Decoder::Instruction& inst, bool vector, bool rev
const auto borrow = ir.UGreaterThan(rhs, lhs);
WriteOperand(DestinationOperand(inst), result);
if (vector) {
WriteMask(inst.dst2, ir.LogicalAnd(ir.GetExec(), borrow));
WriteVectorMask(inst.dst2, borrow);
} else {
ir.SetScc(borrow);
}
@@ -126,7 +126,7 @@ void Translator::SUBB_U32(const Decoder::Instruction& inst, bool vector, bool re
const auto borrow = ir.LogicalOr(borrow0, borrow1);
WriteOperand(DestinationOperand(inst), result);
if (vector) {
WriteMask(inst.dst2, ir.LogicalAnd(ir.GetExec(), borrow));
WriteVectorMask(inst.dst2, borrow);
} else {
ir.SetScc(borrow);
}
@@ -465,9 +465,13 @@ void Translator::V_PERMLANE16_B32(const Decoder::Instruction& inst, bool x16) {
.fetch_inactive = inst.dst.op_sel,
.bound_control = inst.dst.op_sel_hi,
};
const auto result =
ir.Emit(IR::ValueOpcode::Permlane16U32,
{ReadU32(inst.src0), ReadU32(inst.src1), ReadU32(inst.src2), ir.GetExec()}, flags);
// An EXEC-disabled source lane without FI disables the write unless BOUND_CTRL reads zero.
const auto inactive = flags.fetch_inactive || flags.bound_control
? IR::U32(IR::Value(0u))
: ir.GetVectorReg(static_cast<IR::VectorReg>(inst.dst.reg));
const auto result = ir.Emit(
IR::ValueOpcode::Permlane16U32,
{ReadU32(inst.src0), ReadU32(inst.src1), ReadU32(inst.src2), ir.GetExec(), inactive}, flags);
auto dst = DestinationOperand(inst);
dst.op_sel = false;
dst.op_sel_hi = false;
@@ -49,18 +49,11 @@ void Translator::V_CVT_F16_16(const Decoder::Instruction& inst, bool signed_valu
}
void Translator::V_CVT_16_F16(const Decoder::Instruction& inst, bool signed_value) {
const auto value = ReadF16AsF32(inst.src0);
// The 32-bit conversion already saturates and maps NaN to zero.
if (signed_value) {
const auto converted = ir.Emit(IR::ValueOpcode::ConvertS32F32, {value});
const auto low = ir.Emit(IR::ValueOpcode::SMax32, {converted, IR::Value(0xffff8000u)});
Write16Bits(DestinationOperand(inst),
IR::U32(ir.Emit(IR::ValueOpcode::SMin32, {low, IR::Value(0x7fffu)})));
return;
}
const auto converted = ir.Emit(IR::ValueOpcode::ConvertU32F32, {value});
Write16Bits(DestinationOperand(inst),
IR::U32(ir.Emit(IR::ValueOpcode::UMin32, {converted, IR::Value(0xffffu)})));
const auto converted = IR::U32(
ir.Emit(signed_value ? IR::ValueOpcode::ConvertS32F32 : IR::ValueOpcode::ConvertU32F32,
{ReadF16AsF32(inst.src0)}));
Write16Bits(DestinationOperand(inst), Saturate16(converted, signed_value));
}
void Translator::V_CVT_RPI_I32_F32(const Decoder::Instruction& inst) {
@@ -130,14 +123,30 @@ void Translator::V_CVT_PK_U8_F32(const Decoder::Instruction& inst) {
{ReadU32(SourceAt(inst, 2)), byte, shift, IR::Value(8u)}));
}
void Translator::V_CVT_PK_16_32(const Decoder::Instruction& inst, bool signed_value) {
WriteOperand(DestinationOperand(inst),
PackU16Lanes(Saturate16(ReadU32(inst.src0), signed_value),
Saturate16(ReadU32(inst.src1), signed_value)));
}
void Translator::V_PACK_B32_F16(const Decoder::Instruction& inst) {
WriteOperand(DestinationOperand(inst),
PackU16Lanes(Read16LaneBits(inst.src0, false), Read16LaneBits(inst.src1, false)));
}
IR::U32 Translator::PackU16Lanes(IR::U32 low, IR::U32 high) {
// A zero high lane zero-extends; descriptor tracking bounds a mask, not a bitfield insert.
if (high == IR::Value(0u)) return ir.BitwiseAnd(low, IR::U32(IR::Value(0xffffu)));
return IR::U32(
ir.Emit(IR::ValueOpcode::BitFieldInsert, {low, high, IR::Value(16u), IR::Value(16u)}));
}
IR::U32 Translator::Saturate16(IR::U32 value, bool signed_value) {
if (!signed_value) {
return IR::U32(ir.Emit(IR::ValueOpcode::UMin32, {value, IR::Value(0xffffu)}));
}
const auto low = ir.Emit(IR::ValueOpcode::SMax32, {value, IR::Value(0xffff8000u)});
return IR::U32(ir.Emit(IR::ValueOpcode::SMin32, {low, IR::Value(0x7fffu)}));
}
} // namespace Libs::Graphics::ShaderRecompiler::Frontend
@@ -1,6 +1,7 @@
#include "graphics/shader/recompiler/frontend/translate/Translator.h"
#include <array>
#include <bit>
#include <utility>
namespace Libs::Graphics::ShaderRecompiler::Frontend {
@@ -118,6 +119,33 @@ void Translator::FloatUnary(const Decoder::Instruction& inst, IR::ValueOpcode op
WriteOperand(DestinationOperand(inst), ir.Emit(opcode, {ReadOperand(inst.src0, type)}));
}
void Translator::V_FRACT(const Decoder::Instruction& inst, IR::Type type) {
// x - floor(x) rounds to 1.0 for a tiny negative x; v_fract returns the largest value below
// one instead. The ordered compare lets NaN through.
const auto destination = DestinationOperand(inst);
if (type == IR::Type::F64) {
const auto result = ir.Emit(IR::ValueOpcode::FPFract64, {ReadOperand(inst.src0, type)});
const auto below_one = ir.Emit(IR::ValueOpcode::BitCastF64U64,
{IR::Value(uint64_t {0x3fefffffffffffff})});
const auto above = ir.Emit(IR::ValueOpcode::FPOrdGreaterThan64, {result, below_one});
WriteOperand(destination, ir.Emit(IR::ValueOpcode::SelectF64, {above, below_one, result}));
return;
}
const bool half = type == IR::Type::F16;
const auto source = half ? ReadF16AsF32(inst.src0) : IR::F32(ReadOperand(inst.src0, type));
const auto result = IR::F32(ir.Emit(IR::ValueOpcode::FPFract32, {source}));
// Clamp in the destination precision: 0x3f7fe000 is the half 0x3bff.
const auto below_one =
IR::F32(IR::Value::F32(std::bit_cast<float>(half ? 0x3f7fe000u : 0x3f7fffffu)));
const auto above = IR::U1(ir.Emit(IR::ValueOpcode::FPOrdGreaterThan32, {result, below_one}));
const auto clamped = SelectF32(above, below_one, result);
if (half) {
WriteF16(destination, clamped);
} else {
WriteOperand(destination, clamped);
}
}
void Translator::FloatBinary(const Decoder::Instruction& inst, IR::ValueOpcode opcode,
bool reverse) {
std::array<IR::Value, 2> args;
@@ -25,8 +25,7 @@ void Translator::V_MAD_I16(const Decoder::Instruction& inst) {
const auto rhs = ReadU16AsU32(inst.src1, true);
auto result = ir.IAdd(ir.IMul(lhs, rhs), ReadU16AsU32(inst.src2, true));
if (inst.dst.clamp) {
result = IR::U32(ir.Emit(IR::ValueOpcode::SMax32, {result, IR::Value(0xffff8000u)}));
result = IR::U32(ir.Emit(IR::ValueOpcode::SMin32, {result, IR::Value(0x7fffu)}));
result = Saturate16(result, true);
}
Write16Bits(DestinationOperand(inst), result);
}
@@ -108,19 +107,7 @@ void Translator::S_U64_MASK(const Decoder::Instruction& inst, IR::ValueOpcode lo
default: return false;
}
};
if (is_exec_or_vcc(inst.dst) || is_exec_or_vcc(inst.src0) ||
(inst.src_count > 1u && is_exec_or_vcc(inst.src1))) {
const auto mask = WriteMask(inst.dst, invocation_result, true);
ir.SetScc(ir.INotEqual(ir.BitwiseOr(mask[0], mask[1]), IR::U32(IR::Value(0u))));
return;
}
// Descriptor tracking reasons about each scalar word independently.
const auto lhs = ReadU32Pair(inst.src0);
auto mask_valid = ReadMaskValid(inst.src0);
if (inst.src_count > 1u) {
mask_valid = ir.LogicalAnd(mask_valid, ReadMaskValid(inst.src1));
}
const auto lhs = ReadU32Pair(inst.src0);
std::array<IR::U32, 2> result;
if (unary) {
result = {ir.BitwiseNot(lhs[0]), ir.BitwiseNot(lhs[1])};
@@ -133,11 +120,21 @@ void Translator::S_U64_MASK(const Decoder::Instruction& inst, IR::ValueOpcode lo
result[component] = negate_result ? ir.BitwiseNot(IR::U32(value)) : IR::U32(value);
}
}
WriteU32Pair(inst.dst, result);
if (inst.dst.kind == Decoder::OperandKind::Sgpr) {
const auto dst = static_cast<IR::ScalarReg>(inst.dst.reg);
ir.SetThreadBitScalarReg(dst, invocation_result);
ir.SetScalarMaskTag(dst, mask_valid);
if (is_exec_or_vcc(inst.dst) || is_exec_or_vcc(inst.src0) ||
(inst.src_count > 1u && is_exec_or_vcc(inst.src1))) {
WriteMask(inst.dst, invocation_result, result, true);
} else {
// Descriptor tracking reasons about each scalar word independently.
auto mask_valid = ReadMaskValid(inst.src0);
if (inst.src_count > 1u) {
mask_valid = ir.LogicalAnd(mask_valid, ReadMaskValid(inst.src1));
}
WriteU32Pair(inst.dst, result);
if (inst.dst.kind == Decoder::OperandKind::Sgpr) {
const auto dst = static_cast<IR::ScalarReg>(inst.dst.reg);
ir.SetThreadBitScalarReg(dst, invocation_result);
ir.SetScalarMaskTag(dst, mask_valid);
}
}
ir.SetScc(ir.INotEqual(ir.BitwiseOr(result[0], result[1]), IR::U32(IR::Value(0u))));
}
@@ -293,7 +290,8 @@ void Translator::V_MAD_U64_U32(const Decoder::Instruction& inst) {
if (inst.dst2.kind != Decoder::OperandKind::Null &&
inst.dst2.kind != Decoder::OperandKind::Unknown) {
// The 32x32 product fits in 64 bits, so only the addition can carry.
WriteMask(inst.dst2, IR::U1(ir.Emit(IR::ValueOpcode::ULessThan64, {result, product})));
const auto carry = IR::U1(ir.Emit(IR::ValueOpcode::ULessThan64, {result, product}));
WriteMask(inst.dst2, carry, VectorMask(carry));
}
}
@@ -473,7 +471,7 @@ void Translator::S_BITCMP_B32(const Decoder::Instruction& inst, bool expected) {
const auto offset = ir.BitwiseAnd(ReadU32(inst.src1), IR::U32(IR::Value(31u)));
const auto bit =
IR::U32(ir.Emit(IR::ValueOpcode::BitFieldUExtract, {value, offset, IR::Value(1u)}));
WriteCompareResult(inst.dst, ir.IEqual(bit, IR::U32(IR::Value(expected ? 1u : 0u))));
ir.SetScc(ir.IEqual(bit, IR::U32(IR::Value(expected ? 1u : 0u))));
}
void Translator::S_BITCMP_B64(const Decoder::Instruction& inst, bool expected) {
@@ -484,7 +482,7 @@ void Translator::S_BITCMP_B64(const Decoder::Instruction& inst, bool expected) {
ir.Select(ir.ULessThan(offset, IR::U32(IR::Value(32u))), value[0], value[1]);
const auto bit =
IR::U32(ir.Emit(IR::ValueOpcode::BitFieldUExtract, {word, word_bit, IR::Value(1u)}));
WriteCompareResult(inst.dst, ir.IEqual(bit, IR::U32(IR::Value(expected ? 1u : 0u))));
ir.SetScc(ir.IEqual(bit, IR::U32(IR::Value(expected ? 1u : 0u))));
}
void Translator::V_ALIGN_B32(const Decoder::Instruction& inst, bool byte_offset) {
@@ -863,9 +863,10 @@ void Translator::DS_WRITE2(const Decoder::Instruction& inst) {
void Translator::DS_APPEND_CONSUME(const Decoder::Instruction& inst, IR::ValueOpcode opcode) {
const auto memory = MemoryInfoFromDecoded(inst);
WriteOperand(inst.dst, ir.Emit(opcode,
{ReadU32(MemorySourceAt(inst, 0)), ir.GetExec()},
AddMemoryInfo(memory, inst.pc)));
const auto exec = ExecMask();
WriteOperand(inst.dst,
ir.Emit(opcode, {ReadU32(MemorySourceAt(inst, 0)), exec[0], exec[1], ir.GetExec()},
AddMemoryInfo(memory, inst.pc)));
}
void Translator::DS_ADDTID(const Decoder::Instruction& inst, bool write) {
@@ -901,10 +902,7 @@ void Translator::DS_ORDERED_COUNT(const Decoder::Instruction& inst) {
const auto packer = ir.BitwiseAnd(m0, IR::U32(IR::Value(0xfu)));
address = ir.IAdd(address, ir.ShiftLeftLogical(packer, IR::U32(IR::Value(2u))));
}
const auto exec = ir.GetExec();
const auto ballot = ir.Emit(IR::ValueOpcode::Ballot, {exec});
const auto low = ir.CompositeExtract(ballot, 0u);
const auto high = ir.CompositeExtract(ballot, 1u);
const auto [low, high] = ExecMask();
const auto active = ir.INotEqual(ir.BitwiseOr(low, high), IR::U32(IR::Value(0u)));
const auto first = ir.Select(
ir.INotEqual(low, IR::U32(IR::Value(0u))),
@@ -73,43 +73,41 @@ void Translator::EmitScalar(const Decoder::Instruction& inst) {
case O::S_CMP_EQ_U32:
case O::S_CMP_EQ_I32:
EmitIntegerCompare(inst, IR::ValueOpcode::IEqual32, IR::Type::U32, true, false);
EmitIntegerCompare(inst, IR::ValueOpcode::IEqual32, IR::Type::U32, true);
return;
case O::S_CMP_LG_U32:
case O::S_CMP_LG_I32:
EmitIntegerCompare(inst, IR::ValueOpcode::INotEqual32, IR::Type::U32, true, false);
EmitIntegerCompare(inst, IR::ValueOpcode::INotEqual32, IR::Type::U32, true);
return;
case O::S_CMP_GT_U32:
EmitIntegerCompare(inst, IR::ValueOpcode::UGreaterThan32, IR::Type::U32, true, false);
EmitIntegerCompare(inst, IR::ValueOpcode::UGreaterThan32, IR::Type::U32, true);
return;
case O::S_CMP_GE_U32:
EmitIntegerCompare(inst, IR::ValueOpcode::UGreaterThanEqual32, IR::Type::U32, true,
false);
EmitIntegerCompare(inst, IR::ValueOpcode::UGreaterThanEqual32, IR::Type::U32, true);
return;
case O::S_CMP_LT_U32:
EmitIntegerCompare(inst, IR::ValueOpcode::ULessThan32, IR::Type::U32, true, false);
EmitIntegerCompare(inst, IR::ValueOpcode::ULessThan32, IR::Type::U32, true);
return;
case O::S_CMP_LE_U32:
EmitIntegerCompare(inst, IR::ValueOpcode::ULessThanEqual32, IR::Type::U32, true, false);
EmitIntegerCompare(inst, IR::ValueOpcode::ULessThanEqual32, IR::Type::U32, true);
return;
case O::S_CMP_GT_I32:
EmitIntegerCompare(inst, IR::ValueOpcode::SGreaterThan32, IR::Type::U32, true, false);
EmitIntegerCompare(inst, IR::ValueOpcode::SGreaterThan32, IR::Type::U32, true);
return;
case O::S_CMP_GE_I32:
EmitIntegerCompare(inst, IR::ValueOpcode::SGreaterThanEqual32, IR::Type::U32, true,
false);
EmitIntegerCompare(inst, IR::ValueOpcode::SGreaterThanEqual32, IR::Type::U32, true);
return;
case O::S_CMP_LT_I32:
EmitIntegerCompare(inst, IR::ValueOpcode::SLessThan32, IR::Type::U32, true, false);
EmitIntegerCompare(inst, IR::ValueOpcode::SLessThan32, IR::Type::U32, true);
return;
case O::S_CMP_LE_I32:
EmitIntegerCompare(inst, IR::ValueOpcode::SLessThanEqual32, IR::Type::U32, true, false);
EmitIntegerCompare(inst, IR::ValueOpcode::SLessThanEqual32, IR::Type::U32, true);
return;
case O::S_CMP_EQ_U64:
EmitIntegerCompare(inst, IR::ValueOpcode::IEqual64, IR::Type::U64, true, false);
EmitIntegerCompare(inst, IR::ValueOpcode::IEqual64, IR::Type::U64, true);
return;
case O::S_CMP_LG_U64:
EmitIntegerCompare(inst, IR::ValueOpcode::INotEqual64, IR::Type::U64, true, false);
EmitIntegerCompare(inst, IR::ValueOpcode::INotEqual64, IR::Type::U64, true);
return;
case O::S_AND_B64:
@@ -129,6 +129,20 @@ std::array<IR::U32, 2> Translator::BallotMask(IR::U1 value) {
program.wave_size == 64u ? ir.CompositeExtract(mask, 1) : IR::U32(IR::Value(0u))};
}
std::array<IR::U32, 2> Translator::ExecMask() {
return {ir.GetExecLo(), program.wave_size == 64u ? ir.GetExecHi() : IR::U32(IR::Value(0u))};
}
// Pixel EXEC words exclude the helper invocations that the per-lane EXEC keeps enabled.
std::array<IR::U32, 2> Translator::VectorMask(IR::U1 value) {
auto mask = BallotMask(value);
if (program.stage == ShaderType::Pixel) {
const auto exec = ExecMask();
mask = {ir.BitwiseAnd(mask[0], exec[0]), ir.BitwiseAnd(mask[1], exec[1])};
}
return mask;
}
IR::U32 Translator::ReadRawU32(const Decoder::Operand& operand) {
switch (operand.kind) {
case Decoder::OperandKind::LiteralConstant:
@@ -138,6 +152,8 @@ IR::U32 Translator::ReadRawU32(const Decoder::Operand& operand) {
case Decoder::OperandKind::SharedBase:
case Decoder::OperandKind::PrivateBase:
case Decoder::OperandKind::PopsExitingWaveId: return IR::U32(IR::Value(0u));
case Decoder::OperandKind::SharedLimit:
case Decoder::OperandKind::PrivateLimit: return IR::U32(IR::Value(0xffffffffu));
case Decoder::OperandKind::Sgpr:
return ir.GetScalarReg(static_cast<IR::ScalarReg>(operand.reg));
case Decoder::OperandKind::Vgpr:
@@ -388,21 +404,9 @@ void Translator::WriteOperand(const Decoder::Operand& operand, IR::Value value)
switch (operand.kind) {
case Decoder::OperandKind::Scc: ir.SetScc(IR::U1(value)); return;
case Decoder::OperandKind::ExecLo:
case Decoder::OperandKind::ExecHi: {
const auto mask = BallotMask(IR::U1(value));
ir.SetExec(IR::U1(value));
ir.SetExecLo(mask[0]);
ir.SetExecHi(mask[1]);
return;
}
case Decoder::OperandKind::ExecHi:
case Decoder::OperandKind::VccLo:
case Decoder::OperandKind::VccHi: {
const auto mask = BallotMask(IR::U1(value));
ir.SetVcc(IR::U1(value));
ir.SetVccLo(mask[0]);
ir.SetVccHi(mask[1]);
return;
}
case Decoder::OperandKind::VccHi: EXIT("lane masks are written through WriteMask");
default:
WriteRawU32(operand, ir.Select(IR::U1(value), IR::U32(IR::Value(1u)),
IR::U32(IR::Value(0u))));
@@ -471,17 +475,17 @@ IR::U32 Translator::ReadU32(const Decoder::Operand& operand) {
}
std::array<IR::U32, 2> Translator::ReadU32Pair(const Decoder::Operand& operand) {
if (operand.kind == Decoder::OperandKind::PrivateBase ||
operand.kind == Decoder::OperandKind::SharedBase) {
return {IR::U32(IR::Value(0u)), IR::U32(IR::Value(
operand.kind == Decoder::OperandKind::PrivateBase ? Decoder::PrivateApertureHigh
: Decoder::SharedApertureHigh))};
}
if (operand.kind == Decoder::OperandKind::ExecLo) {
return {ir.GetExecLo(), ir.GetExecHi()};
}
if (operand.kind == Decoder::OperandKind::VccLo) {
return {ir.GetVccLo(), ir.GetVccHi()};
// Each aperture limit is its base plus 0xffffffff.
switch (operand.kind) {
case Decoder::OperandKind::SharedBase:
case Decoder::OperandKind::SharedLimit:
return {ReadRawU32(operand), IR::U32(IR::Value(Decoder::SharedApertureHigh))};
case Decoder::OperandKind::PrivateBase:
case Decoder::OperandKind::PrivateLimit:
return {ReadRawU32(operand), IR::U32(IR::Value(Decoder::PrivateApertureHigh))};
case Decoder::OperandKind::ExecLo: return {ir.GetExecLo(), ir.GetExecHi()};
case Decoder::OperandKind::VccLo: return {ir.GetVccLo(), ir.GetVccHi()};
default: break;
}
if (operand.kind == Decoder::OperandKind::FloatInlineConstant) {
const auto bits =
@@ -639,7 +643,7 @@ IR::U1 Translator::ReadMask(const Decoder::Operand& operand) {
}
case Decoder::OperandKind::ExecLo:
case Decoder::OperandKind::ExecHi: return ir.GetExec();
case Decoder::OperandKind::VccLo:
case Decoder::OperandKind::VccLo: return ir.GetVcc();
case Decoder::OperandKind::VccHi:
return program.wave_size == 32u
? ThreadBit({ReadRawU32(operand), IR::U32(IR::Value(0u))})
@@ -677,9 +681,8 @@ IR::U1 Translator::ReadMaskValid(const Decoder::Operand& operand) {
}
}
std::array<IR::U32, 2> Translator::WriteMask(const Decoder::Operand& operand, IR::U1 value,
bool write_64) {
const auto mask = BallotMask(value);
void Translator::WriteMask(const Decoder::Operand& operand, IR::U1 value,
const std::array<IR::U32, 2>& mask, bool write_64) {
switch (operand.kind) {
case Decoder::OperandKind::Sgpr: {
const auto reg = static_cast<IR::ScalarReg>(operand.reg);
@@ -700,43 +703,43 @@ std::array<IR::U32, 2> Translator::WriteMask(const Decoder::Operand& operand, IR
ir.SetThreadBitScalarReg(high, IR::U1(IR::Value(false)));
ir.SetScalarMaskTag(high, IR::U1(IR::Value(false)));
}
return mask;
return;
}
case Decoder::OperandKind::ExecLo:
case Decoder::OperandKind::ExecHi: {
ir.SetExec(value);
ir.SetExecLo(mask[0]);
ir.SetExecHi(mask[1]);
return mask;
return;
}
case Decoder::OperandKind::VccLo:
case Decoder::OperandKind::VccHi: {
if (!write_64 && program.wave_size == 32u) {
if (operand.kind == Decoder::OperandKind::VccHi && !write_64 &&
program.wave_size == 32u) {
WriteRawU32(operand, mask[0]);
return mask;
return;
}
ir.SetVcc(value);
ir.SetVccLo(mask[0]);
ir.SetVccHi(mask[1]);
return mask;
if (write_64 || program.wave_size == 64u) {
ir.SetVccHi(mask[1]);
}
return;
}
case Decoder::OperandKind::Scc: ir.SetScc(value); return mask;
case Decoder::OperandKind::Scc: ir.SetScc(value); return;
default:
WriteRawU32(operand, mask[0]);
WriteRawU32(OffsetOperand(operand, 1), mask[1]);
return mask;
return;
}
}
void Translator::WriteCompareResult(const Decoder::Operand& operand, IR::U1 value) {
if (operand.kind == Decoder::OperandKind::Scc) {
WriteOperand(operand, value);
return;
}
void Translator::WriteVectorMask(const Decoder::Operand& operand, IR::U1 value) {
// VALU instructions only write lanes enabled by the incoming EXEC mask. This is
// especially important for V_CMPX: replacing EXEC with the ungated comparison would
// reactivate lanes disabled by an enclosing divergent region.
WriteMask(operand, ir.LogicalAnd(ir.GetExec(), value));
const auto active = ir.LogicalAnd(ir.GetExec(), value);
WriteMask(operand, active, VectorMask(active));
}
void Translator::AddBranchCondition(const CFG::Graph& graph, const CFG::BasicBlock& source,
@@ -1153,7 +1156,10 @@ IR::Program TranslateProgram(const Decoder::Program& decoded, const CFG::Graph&
}
}
entry_ir.SetExec(initial_exec);
const auto initial_mask = entry_ir.Emit(IR::ValueOpcode::Ballot, {initial_exec});
// Pixel EXEC words start with the live (non-helper) lanes; helpers keep the per-lane EXEC.
const auto initial_mask = options.stage == ShaderType::Pixel
? entry_ir.Emit(IR::ValueOpcode::LiveMask)
: entry_ir.Emit(IR::ValueOpcode::Ballot, {initial_exec});
const auto initial_lo = entry_ir.CompositeExtract(initial_mask, 0);
const auto initial_hi = options.wave_size == 64u ? entry_ir.CompositeExtract(initial_mask, 1)
: IR::U32(IR::Value(0u));
@@ -1238,12 +1244,12 @@ IR::Program TranslateProgram(const Decoder::Program& decoded, const CFG::Graph&
entry_ir.BitwiseOr(wave_info, entry_ir.BitwiseOr(entry_ir.ShiftLeftLogical(
primitive_count, u32(8)),
vertex_count)));
// Instance IDs exclude the start instance, which shaders add from user data.
if (mesh.fast_launch) {
// Fast launch broadcasts the subgroup's base vertex and instance to every lane.
entry_ir.SetVectorReg(static_cast<IR::VectorReg>(5), entry_ir.IAdd(draw(1), group));
entry_ir.SetVectorReg(
static_cast<IR::VectorReg>(6),
entry_ir.IAdd(draw(2), builtin(IR::StageInputKind::WorkgroupId, 1)));
entry_ir.SetVectorReg(static_cast<IR::VectorReg>(6),
builtin(IR::StageInputKind::WorkgroupId, 1));
} else {
// GS adjacency addresses local ES records in LDS. Fans retain the draw's
// center in every subgroup; strip winding follows the global primitive.
@@ -1271,14 +1277,14 @@ IR::Program TranslateProgram(const Decoder::Program& decoded, const CFG::Graph&
entry_ir.ShiftLeftLogical(second, u32(18))));
entry_ir.SetVectorReg(static_cast<IR::VectorReg>(1),
entry_ir.ShiftLeftLogical(third, u32(2)));
const auto index_bytes = draw(3);
const auto index_bytes = draw(2);
const auto indexed = entry_ir.INotEqual(index_bytes, u32(0));
const auto index_low = draw(4);
const auto index_low = draw(3);
const auto byte_offset = entry_ir.IAdd(entry_ir.BitwiseAnd(index_low, u32(3)),
entry_ir.IMul(input_vertex, index_bytes));
const auto index_resource = entry_ir.Emit(
IR::ValueOpcode::GetAddressResource,
{entry_ir.BitwiseAnd(index_low, u32(~3u)), draw(5)});
{entry_ir.BitwiseAnd(index_low, u32(~3u)), draw(4)});
const auto memory_index = static_cast<uint32_t>(result.memory_info.size());
result.memory_info.push_back({.kind = IR::ResourceKind::Global});
const auto packed_index = entry_ir.Emit(
@@ -1292,9 +1298,8 @@ IR::Program TranslateProgram(const Decoder::Program& decoded, const CFG::Graph&
entry_ir.IMul(index_bytes, u32(8))}));
entry_ir.SetVectorReg(static_cast<IR::VectorReg>(5),
entry_ir.IAdd(draw(1), entry_ir.Select(indexed, index, input_vertex)));
entry_ir.SetVectorReg(
static_cast<IR::VectorReg>(8),
entry_ir.IAdd(draw(2), builtin(IR::StageInputKind::WorkgroupId, 1)));
entry_ir.SetVectorReg(static_cast<IR::VectorReg>(8),
builtin(IR::StageInputKind::WorkgroupId, 1));
}
} else if (options.stage == ShaderType::Local) {
entry_ir.SetScalarReg(static_cast<IR::ScalarReg>(3), IR::U32(IR::Value(64u)));
@@ -27,6 +27,8 @@ private:
Decoder::Operand ScalarDestinationOperand(const Decoder::Operand& operand, uint32_t offset);
Decoder::Operand PlainOperand(const Decoder::Operand& operand);
std::array<IR::U32, 2> BallotMask(IR::U1 value);
std::array<IR::U32, 2> ExecMask();
std::array<IR::U32, 2> VectorMask(IR::U1 value);
IR::U32 ReadRawU32(const Decoder::Operand& operand);
IR::U32 ReadScalarCode(uint32_t code);
IR::U32 ApplyBitSourceModifiers(const Decoder::Operand& operand, IR::U32 value);
@@ -52,9 +54,9 @@ private:
IR::U32 ConditionBit(const Decoder::Operand& operand);
IR::U1 ReadMask(const Decoder::Operand& operand);
IR::U1 ReadMaskValid(const Decoder::Operand& operand);
std::array<IR::U32, 2> WriteMask(const Decoder::Operand& operand, IR::U1 value,
bool write_64 = false);
void WriteCompareResult(const Decoder::Operand& operand, IR::U1 value);
void WriteMask(const Decoder::Operand& operand, IR::U1 value,
const std::array<IR::U32, 2>& mask, bool write_64 = false);
void WriteVectorMask(const Decoder::Operand& operand, IR::U1 value);
IR::MemoryFlags AddMemoryInfo(const IR::MemoryInfo& memory, uint32_t pc);
IR::ExportFlags AddExportInfo(const Decoder::Instruction& inst);
@@ -114,17 +116,16 @@ private:
IR::F32 SelectF32(IR::U1 condition, IR::F32 true_value, IR::F32 false_value);
IR::U32 PackU16Lanes(IR::U32 low, IR::U32 high);
void EmitCompareResult(const Decoder::Instruction& inst, IR::U1 value, bool scalar, bool cmpx);
void EmitCompareConstant(const Decoder::Instruction& inst, bool value, bool scalar, bool cmpx);
IR::U32 Saturate16(IR::U32 value, bool signed_value);
void EmitCompareResult(const Decoder::Instruction& inst, IR::U1 value, bool scalar);
void EmitCompareConstant(const Decoder::Instruction& inst, bool value, bool scalar);
void EmitIntegerCompare(const Decoder::Instruction& inst, IR::ValueOpcode opcode, IR::Type type,
bool scalar, bool cmpx, bool swap_operands = false);
bool scalar, bool swap_operands = false);
void EmitInteger16Compare(const Decoder::Instruction& inst, IR::ValueOpcode opcode,
bool signed_value, bool cmpx);
void EmitFloatCompare(const Decoder::Instruction& inst, IR::ValueOpcode opcode, bool half,
bool cmpx);
void EmitFloatOrderedCompare(const Decoder::Instruction& inst, bool ordered, bool cmpx,
bool is64 = false);
void EmitFloatClassCompare(const Decoder::Instruction& inst, bool cmpx, bool half = false);
bool signed_value);
void EmitFloatCompare(const Decoder::Instruction& inst, IR::ValueOpcode opcode, bool half);
void EmitFloatOrderedCompare(const Decoder::Instruction& inst, bool ordered, bool is64 = false);
void EmitFloatClassCompare(const Decoder::Instruction& inst, bool half = false);
void V_CVT_F32_UBYTE(const Decoder::Instruction& inst, uint32_t byte_index);
void V_CVT_F32_U32(const Decoder::Instruction& inst);
void V_CVT_F32_I32(const Decoder::Instruction& inst);
@@ -141,6 +142,7 @@ private:
void V_CVT_PKRTZ_F16_F32(const Decoder::Instruction& inst);
void V_CVT_PKNORM_F32(const Decoder::Instruction& inst, IR::ValueOpcode opcode);
void V_CVT_PK_U8_F32(const Decoder::Instruction& inst);
void V_CVT_PK_16_32(const Decoder::Instruction& inst, bool signed_value);
void V_PACK_B32_F16(const Decoder::Instruction& inst);
void PackedFloat16(const Decoder::Instruction& inst, IR::ValueOpcode opcode, bool accumulator,
bool quiet_snan);
@@ -151,6 +153,7 @@ private:
void Float16Ternary(const Decoder::Instruction& inst, IR::ValueOpcode opcode, bool accumulator,
bool mix);
void FloatUnary(const Decoder::Instruction& inst, IR::ValueOpcode opcode);
void V_FRACT(const Decoder::Instruction& inst, IR::Type type);
void FloatBinary(const Decoder::Instruction& inst, IR::ValueOpcode opcode, bool reverse);
void FloatTernary(const Decoder::Instruction& inst, IR::ValueOpcode opcode, bool accumulator,
bool mix);
@@ -25,444 +25,310 @@ void Translator::EmitVector(const Decoder::Instruction& inst) {
case O::V_PERMLANEX16_B32: V_PERMLANE16_B32(inst, true); return;
case O::V_CMP_F_I32:
case O::V_CMP_F_U32: EmitCompareConstant(inst, false, false, false); return;
case O::V_CMP_F_U32: EmitCompareConstant(inst, false, false); return;
case O::V_CMP_T_I32:
case O::V_CMP_T_U32: EmitCompareConstant(inst, true, false, false); return;
case O::V_CMP_T_U32: EmitCompareConstant(inst, true, false); return;
case O::V_CMP_EQ_U32:
case O::V_CMP_EQ_I32:
EmitIntegerCompare(inst, IR::ValueOpcode::IEqual32, IR::Type::U32, false, false);
return;
case O::V_CMPX_EQ_U32:
case O::V_CMPX_EQ_I32:
EmitIntegerCompare(inst, IR::ValueOpcode::IEqual32, IR::Type::U32, false, true);
EmitIntegerCompare(inst, IR::ValueOpcode::IEqual32, IR::Type::U32, false);
return;
case O::V_CMP_NE_U32:
case O::V_CMP_NE_I32:
EmitIntegerCompare(inst, IR::ValueOpcode::INotEqual32, IR::Type::U32, false, false);
return;
case O::V_CMPX_NE_U32:
case O::V_CMPX_NE_I32:
EmitIntegerCompare(inst, IR::ValueOpcode::INotEqual32, IR::Type::U32, false, true);
EmitIntegerCompare(inst, IR::ValueOpcode::INotEqual32, IR::Type::U32, false);
return;
case O::V_CMP_GT_U32:
EmitIntegerCompare(inst, IR::ValueOpcode::UGreaterThan32, IR::Type::U32, false, false);
return;
case O::V_CMPX_GT_U32:
EmitIntegerCompare(inst, IR::ValueOpcode::UGreaterThan32, IR::Type::U32, false, true);
EmitIntegerCompare(inst, IR::ValueOpcode::UGreaterThan32, IR::Type::U32, false);
return;
case O::V_CMP_GE_U32:
EmitIntegerCompare(inst, IR::ValueOpcode::UGreaterThanEqual32, IR::Type::U32, false,
false);
return;
case O::V_CMPX_GE_U32:
EmitIntegerCompare(inst, IR::ValueOpcode::UGreaterThanEqual32, IR::Type::U32, false,
true);
EmitIntegerCompare(inst, IR::ValueOpcode::UGreaterThanEqual32, IR::Type::U32, false);
return;
case O::V_CMP_LT_U32:
EmitIntegerCompare(inst, IR::ValueOpcode::ULessThan32, IR::Type::U32, false, false);
return;
case O::V_CMPX_LT_U32:
EmitIntegerCompare(inst, IR::ValueOpcode::ULessThan32, IR::Type::U32, false, true);
EmitIntegerCompare(inst, IR::ValueOpcode::ULessThan32, IR::Type::U32, false);
return;
case O::V_CMP_LE_U32:
EmitIntegerCompare(inst, IR::ValueOpcode::ULessThanEqual32, IR::Type::U32, false,
false);
return;
case O::V_CMPX_LE_U32:
EmitIntegerCompare(inst, IR::ValueOpcode::ULessThanEqual32, IR::Type::U32, false, true);
EmitIntegerCompare(inst, IR::ValueOpcode::ULessThanEqual32, IR::Type::U32, false);
return;
case O::V_CMP_GT_I32:
EmitIntegerCompare(inst, IR::ValueOpcode::SGreaterThan32, IR::Type::U32, false, false);
return;
case O::V_CMPX_GT_I32:
EmitIntegerCompare(inst, IR::ValueOpcode::SGreaterThan32, IR::Type::U32, false, true);
EmitIntegerCompare(inst, IR::ValueOpcode::SGreaterThan32, IR::Type::U32, false);
return;
case O::V_CMP_GE_I32:
EmitIntegerCompare(inst, IR::ValueOpcode::SGreaterThanEqual32, IR::Type::U32, false,
false);
return;
case O::V_CMPX_GE_I32:
EmitIntegerCompare(inst, IR::ValueOpcode::SGreaterThanEqual32, IR::Type::U32, false,
true);
EmitIntegerCompare(inst, IR::ValueOpcode::SGreaterThanEqual32, IR::Type::U32, false);
return;
case O::V_CMP_LT_I32:
EmitIntegerCompare(inst, IR::ValueOpcode::SLessThan32, IR::Type::U32, false, false);
return;
case O::V_CMPX_LT_I32:
EmitIntegerCompare(inst, IR::ValueOpcode::SLessThan32, IR::Type::U32, false, true);
EmitIntegerCompare(inst, IR::ValueOpcode::SLessThan32, IR::Type::U32, false);
return;
case O::V_CMP_LE_I32:
EmitIntegerCompare(inst, IR::ValueOpcode::SLessThanEqual32, IR::Type::U32, false,
false);
return;
case O::V_CMPX_LE_I32:
EmitIntegerCompare(inst, IR::ValueOpcode::SLessThanEqual32, IR::Type::U32, false, true);
EmitIntegerCompare(inst, IR::ValueOpcode::SLessThanEqual32, IR::Type::U32, false);
return;
case O::V_CMP_F_I64:
case O::V_CMP_F_U64: EmitCompareConstant(inst, false, false, false); return;
case O::V_CMP_T_I64:
case O::V_CMP_T_U64: EmitCompareConstant(inst, true, false, false); return;
case O::V_CMP_F_U64:
case O::V_CMPX_F_I64:
case O::V_CMPX_F_U64: EmitCompareConstant(inst, false, false, true); return;
case O::V_CMPX_F_U64: EmitCompareConstant(inst, false, false); return;
case O::V_CMP_T_I64:
case O::V_CMP_T_U64:
case O::V_CMPX_T_I64:
case O::V_CMPX_T_U64: EmitCompareConstant(inst, true, false, true); return;
case O::V_CMPX_T_U64: EmitCompareConstant(inst, true, false); return;
case O::V_CMP_EQ_I64:
case O::V_CMP_EQ_U64:
EmitIntegerCompare(inst, IR::ValueOpcode::IEqual64, IR::Type::U64, false, false);
return;
case O::V_CMPX_EQ_I64:
case O::V_CMPX_EQ_U64:
EmitIntegerCompare(inst, IR::ValueOpcode::IEqual64, IR::Type::U64, false, true);
EmitIntegerCompare(inst, IR::ValueOpcode::IEqual64, IR::Type::U64, false);
return;
case O::V_CMP_LT_U64:
EmitIntegerCompare(inst, IR::ValueOpcode::ULessThan64, IR::Type::U64, false, false);
return;
case O::V_CMPX_LT_U64:
EmitIntegerCompare(inst, IR::ValueOpcode::ULessThan64, IR::Type::U64, false, true);
EmitIntegerCompare(inst, IR::ValueOpcode::ULessThan64, IR::Type::U64, false);
return;
case O::V_CMP_GT_U64:
EmitIntegerCompare(inst, IR::ValueOpcode::UGreaterThan64, IR::Type::U64, false, false);
return;
case O::V_CMPX_GT_U64:
EmitIntegerCompare(inst, IR::ValueOpcode::UGreaterThan64, IR::Type::U64, false, true);
EmitIntegerCompare(inst, IR::ValueOpcode::UGreaterThan64, IR::Type::U64, false);
return;
case O::V_CMP_NE_I64:
case O::V_CMP_NE_U64:
EmitIntegerCompare(inst, IR::ValueOpcode::INotEqual64, IR::Type::U64, false, false);
return;
case O::V_CMPX_NE_I64:
case O::V_CMPX_NE_U64:
EmitIntegerCompare(inst, IR::ValueOpcode::INotEqual64, IR::Type::U64, false, true);
EmitIntegerCompare(inst, IR::ValueOpcode::INotEqual64, IR::Type::U64, false);
return;
case O::V_CMP_LT_I64:
EmitIntegerCompare(inst, IR::ValueOpcode::SLessThan64, IR::Type::U64, false, false);
return;
case O::V_CMPX_LT_I64:
EmitIntegerCompare(inst, IR::ValueOpcode::SLessThan64, IR::Type::U64, false, true);
EmitIntegerCompare(inst, IR::ValueOpcode::SLessThan64, IR::Type::U64, false);
return;
case O::V_CMP_LE_I64:
EmitIntegerCompare(inst, IR::ValueOpcode::SLessThanEqual64, IR::Type::U64, false, false);
return;
case O::V_CMPX_LE_I64:
EmitIntegerCompare(inst, IR::ValueOpcode::SLessThanEqual64, IR::Type::U64, false, true);
EmitIntegerCompare(inst, IR::ValueOpcode::SLessThanEqual64, IR::Type::U64, false);
return;
// The IR has no signed 64-bit greater-than: a > b is b < a, and a >= b is b <= a.
case O::V_CMP_GT_I64:
EmitIntegerCompare(inst, IR::ValueOpcode::SLessThan64, IR::Type::U64, false, false,
true);
return;
case O::V_CMPX_GT_I64:
EmitIntegerCompare(inst, IR::ValueOpcode::SLessThan64, IR::Type::U64, false, true,
true);
EmitIntegerCompare(inst, IR::ValueOpcode::SLessThan64, IR::Type::U64, false, true);
return;
case O::V_CMP_GE_I64:
EmitIntegerCompare(inst, IR::ValueOpcode::SLessThanEqual64, IR::Type::U64, false, false,
true);
return;
case O::V_CMPX_GE_I64:
EmitIntegerCompare(inst, IR::ValueOpcode::SLessThanEqual64, IR::Type::U64, false, true,
true);
EmitIntegerCompare(inst, IR::ValueOpcode::SLessThanEqual64, IR::Type::U64, false, true);
return;
case O::V_CMP_LE_U64:
EmitIntegerCompare(inst, IR::ValueOpcode::ULessThanEqual64, IR::Type::U64, false, false);
return;
case O::V_CMPX_LE_U64:
EmitIntegerCompare(inst, IR::ValueOpcode::ULessThanEqual64, IR::Type::U64, false, true);
EmitIntegerCompare(inst, IR::ValueOpcode::ULessThanEqual64, IR::Type::U64, false);
return;
case O::V_CMP_GE_U64:
EmitIntegerCompare(inst, IR::ValueOpcode::UGreaterThanEqual64, IR::Type::U64, false, false);
return;
case O::V_CMPX_GE_U64:
EmitIntegerCompare(inst, IR::ValueOpcode::UGreaterThanEqual64, IR::Type::U64, false, true);
EmitIntegerCompare(inst, IR::ValueOpcode::UGreaterThanEqual64, IR::Type::U64, false);
return;
case O::V_CMP_EQ_U16:
EmitInteger16Compare(inst, IR::ValueOpcode::IEqual32, false, false);
case O::V_CMPX_EQ_U16:
EmitInteger16Compare(inst, IR::ValueOpcode::IEqual32, false);
return;
case O::V_CMP_EQ_I16:
EmitInteger16Compare(inst, IR::ValueOpcode::IEqual32, true, false);
return;
case O::V_CMPX_EQ_I16:
EmitInteger16Compare(inst, IR::ValueOpcode::IEqual32, true, true);
EmitInteger16Compare(inst, IR::ValueOpcode::IEqual32, true);
return;
case O::V_CMP_NE_U16:
EmitInteger16Compare(inst, IR::ValueOpcode::INotEqual32, false, false);
return;
case O::V_CMPX_NE_U16:
EmitInteger16Compare(inst, IR::ValueOpcode::INotEqual32, false, true);
EmitInteger16Compare(inst, IR::ValueOpcode::INotEqual32, false);
return;
case O::V_CMP_NE_I16:
EmitInteger16Compare(inst, IR::ValueOpcode::INotEqual32, true, false);
return;
case O::V_CMPX_NE_I16:
EmitInteger16Compare(inst, IR::ValueOpcode::INotEqual32, true, true);
EmitInteger16Compare(inst, IR::ValueOpcode::INotEqual32, true);
return;
case O::V_CMP_GT_U16:
EmitInteger16Compare(inst, IR::ValueOpcode::UGreaterThan32, false, false);
return;
case O::V_CMPX_GT_U16:
EmitInteger16Compare(inst, IR::ValueOpcode::UGreaterThan32, false, true);
EmitInteger16Compare(inst, IR::ValueOpcode::UGreaterThan32, false);
return;
case O::V_CMP_GE_U16:
EmitInteger16Compare(inst, IR::ValueOpcode::UGreaterThanEqual32, false, false);
EmitInteger16Compare(inst, IR::ValueOpcode::UGreaterThanEqual32, false);
return;
case O::V_CMP_LT_U16:
EmitInteger16Compare(inst, IR::ValueOpcode::ULessThan32, false, false);
return;
case O::V_CMPX_LT_U16:
EmitInteger16Compare(inst, IR::ValueOpcode::ULessThan32, false, true);
return;
case O::V_CMPX_EQ_U16:
EmitInteger16Compare(inst, IR::ValueOpcode::IEqual32, false, true);
EmitInteger16Compare(inst, IR::ValueOpcode::ULessThan32, false);
return;
case O::V_CMP_LE_U16:
EmitInteger16Compare(inst, IR::ValueOpcode::ULessThanEqual32, false, false);
EmitInteger16Compare(inst, IR::ValueOpcode::ULessThanEqual32, false);
return;
case O::V_CMP_GT_I16:
EmitInteger16Compare(inst, IR::ValueOpcode::SGreaterThan32, true, false);
return;
case O::V_CMPX_GT_I16:
EmitInteger16Compare(inst, IR::ValueOpcode::SGreaterThan32, true, true);
EmitInteger16Compare(inst, IR::ValueOpcode::SGreaterThan32, true);
return;
case O::V_CMP_GE_I16:
EmitInteger16Compare(inst, IR::ValueOpcode::SGreaterThanEqual32, true, false);
return;
case O::V_CMPX_GE_I16:
EmitInteger16Compare(inst, IR::ValueOpcode::SGreaterThanEqual32, true, true);
EmitInteger16Compare(inst, IR::ValueOpcode::SGreaterThanEqual32, true);
return;
case O::V_CMP_LT_I16:
EmitInteger16Compare(inst, IR::ValueOpcode::SLessThan32, true, false);
return;
case O::V_CMPX_LT_I16:
EmitInteger16Compare(inst, IR::ValueOpcode::SLessThan32, true, true);
EmitInteger16Compare(inst, IR::ValueOpcode::SLessThan32, true);
return;
case O::V_CMP_LE_I16:
EmitInteger16Compare(inst, IR::ValueOpcode::SLessThanEqual32, true, false);
return;
case O::V_CMPX_LE_I16:
EmitInteger16Compare(inst, IR::ValueOpcode::SLessThanEqual32, true, true);
EmitInteger16Compare(inst, IR::ValueOpcode::SLessThanEqual32, true);
return;
case O::V_CMP_F_F32: EmitCompareConstant(inst, false, false, false); return;
case O::V_CMP_TRU_F32: EmitCompareConstant(inst, true, false, false); return;
case O::V_CMP_F_F32: EmitCompareConstant(inst, false, false); return;
case O::V_CMP_TRU_F32: EmitCompareConstant(inst, true, false); return;
case O::V_CMP_EQ_F32:
EmitFloatCompare(inst, IR::ValueOpcode::FPOrdEqual32, false, false);
case O::V_CMPX_EQ_F32:
EmitFloatCompare(inst, IR::ValueOpcode::FPOrdEqual32, false);
return;
case O::V_CMP_EQ_F64:
EmitFloatCompare(inst, IR::ValueOpcode::FPOrdEqual64, false, false);
case O::V_CMPX_EQ_F64:
EmitFloatCompare(inst, IR::ValueOpcode::FPOrdEqual64, false);
return;
case O::V_CMP_LE_F64:
EmitFloatCompare(inst, IR::ValueOpcode::FPOrdLessThanEqual64, false, false);
return;
case O::V_CMPX_LE_F64:
EmitFloatCompare(inst, IR::ValueOpcode::FPOrdLessThanEqual64, false, true);
return;
case O::V_CMPX_GE_F64:
EmitFloatCompare(inst, IR::ValueOpcode::FPOrdGreaterThanEqual64, false, true);
return;
case O::V_CMP_F_F64: EmitCompareConstant(inst, false, false, false); return;
case O::V_CMP_TRU_F64: EmitCompareConstant(inst, true, false, false); return;
case O::V_CMPX_F_F64: EmitCompareConstant(inst, false, false, true); return;
case O::V_CMPX_TRU_F64: EmitCompareConstant(inst, true, false, true); return;
case O::V_CMP_LT_F64:
EmitFloatCompare(inst, IR::ValueOpcode::FPOrdLessThan64, false, false);
return;
case O::V_CMPX_LT_F64:
EmitFloatCompare(inst, IR::ValueOpcode::FPOrdLessThan64, false, true);
return;
case O::V_CMPX_EQ_F64:
EmitFloatCompare(inst, IR::ValueOpcode::FPOrdEqual64, false, true);
return;
case O::V_CMP_GT_F64:
EmitFloatCompare(inst, IR::ValueOpcode::FPOrdGreaterThan64, false, false);
return;
case O::V_CMPX_GT_F64:
EmitFloatCompare(inst, IR::ValueOpcode::FPOrdGreaterThan64, false, true);
return;
case O::V_CMP_LG_F64:
EmitFloatCompare(inst, IR::ValueOpcode::FPOrdNotEqual64, false, false);
return;
case O::V_CMPX_LG_F64:
EmitFloatCompare(inst, IR::ValueOpcode::FPOrdNotEqual64, false, true);
EmitFloatCompare(inst, IR::ValueOpcode::FPOrdLessThanEqual64, false);
return;
case O::V_CMP_GE_F64:
EmitFloatCompare(inst, IR::ValueOpcode::FPOrdGreaterThanEqual64, false, false);
case O::V_CMPX_GE_F64:
EmitFloatCompare(inst, IR::ValueOpcode::FPOrdGreaterThanEqual64, false);
return;
case O::V_CMP_F_F64:
case O::V_CMPX_F_F64: EmitCompareConstant(inst, false, false); return;
case O::V_CMP_TRU_F64:
case O::V_CMPX_TRU_F64: EmitCompareConstant(inst, true, false); return;
case O::V_CMP_LT_F64:
case O::V_CMPX_LT_F64:
EmitFloatCompare(inst, IR::ValueOpcode::FPOrdLessThan64, false);
return;
case O::V_CMP_GT_F64:
case O::V_CMPX_GT_F64:
EmitFloatCompare(inst, IR::ValueOpcode::FPOrdGreaterThan64, false);
return;
case O::V_CMP_LG_F64:
case O::V_CMPX_LG_F64:
EmitFloatCompare(inst, IR::ValueOpcode::FPOrdNotEqual64, false);
return;
case O::V_CMP_NLG_F64:
EmitFloatCompare(inst, IR::ValueOpcode::FPUnordEqual64, false, false);
return;
case O::V_CMPX_NLG_F64:
EmitFloatCompare(inst, IR::ValueOpcode::FPUnordEqual64, false, true);
EmitFloatCompare(inst, IR::ValueOpcode::FPUnordEqual64, false);
return;
case O::V_CMP_NEQ_F64:
EmitFloatCompare(inst, IR::ValueOpcode::FPUnordNotEqual64, false, false);
return;
case O::V_CMPX_NEQ_F64:
EmitFloatCompare(inst, IR::ValueOpcode::FPUnordNotEqual64, false, true);
EmitFloatCompare(inst, IR::ValueOpcode::FPUnordNotEqual64, false);
return;
case O::V_CMP_NLE_F64:
EmitFloatCompare(inst, IR::ValueOpcode::FPUnordGreaterThan64, false, false);
return;
case O::V_CMPX_NLE_F64:
EmitFloatCompare(inst, IR::ValueOpcode::FPUnordGreaterThan64, false, true);
EmitFloatCompare(inst, IR::ValueOpcode::FPUnordGreaterThan64, false);
return;
case O::V_CMP_NLT_F64:
EmitFloatCompare(inst, IR::ValueOpcode::FPUnordGreaterThanEqual64, false, false);
return;
case O::V_CMPX_NLT_F64:
EmitFloatCompare(inst, IR::ValueOpcode::FPUnordGreaterThanEqual64, false, true);
EmitFloatCompare(inst, IR::ValueOpcode::FPUnordGreaterThanEqual64, false);
return;
case O::V_CMP_NGE_F64:
EmitFloatCompare(inst, IR::ValueOpcode::FPUnordLessThan64, false, false);
return;
case O::V_CMPX_NGE_F64:
EmitFloatCompare(inst, IR::ValueOpcode::FPUnordLessThan64, false, true);
EmitFloatCompare(inst, IR::ValueOpcode::FPUnordLessThan64, false);
return;
case O::V_CMP_NGT_F64:
EmitFloatCompare(inst, IR::ValueOpcode::FPUnordLessThanEqual64, false, false);
return;
case O::V_CMPX_NGT_F64:
EmitFloatCompare(inst, IR::ValueOpcode::FPUnordLessThanEqual64, false, true);
return;
case O::V_CMP_O_F64: EmitFloatOrderedCompare(inst, true, false, true); return;
case O::V_CMP_U_F64: EmitFloatOrderedCompare(inst, false, false, true); return;
case O::V_CMPX_O_F64: EmitFloatOrderedCompare(inst, true, true, true); return;
case O::V_CMPX_U_F64: EmitFloatOrderedCompare(inst, false, true, true); return;
case O::V_CMPX_EQ_F32:
EmitFloatCompare(inst, IR::ValueOpcode::FPOrdEqual32, false, true);
EmitFloatCompare(inst, IR::ValueOpcode::FPUnordLessThanEqual64, false);
return;
case O::V_CMP_O_F64:
case O::V_CMPX_O_F64: EmitFloatOrderedCompare(inst, true, true); return;
case O::V_CMP_U_F64:
case O::V_CMPX_U_F64: EmitFloatOrderedCompare(inst, false, true); return;
case O::V_CMP_LG_F32:
EmitFloatCompare(inst, IR::ValueOpcode::FPOrdNotEqual32, false, false);
return;
case O::V_CMPX_LG_F32:
EmitFloatCompare(inst, IR::ValueOpcode::FPOrdNotEqual32, false, true);
EmitFloatCompare(inst, IR::ValueOpcode::FPOrdNotEqual32, false);
return;
case O::V_CMP_GT_F32:
EmitFloatCompare(inst, IR::ValueOpcode::FPOrdGreaterThan32, false, false);
return;
case O::V_CMPX_GT_F32:
EmitFloatCompare(inst, IR::ValueOpcode::FPOrdGreaterThan32, false, true);
EmitFloatCompare(inst, IR::ValueOpcode::FPOrdGreaterThan32, false);
return;
case O::V_CMP_GE_F32:
EmitFloatCompare(inst, IR::ValueOpcode::FPOrdGreaterThanEqual32, false, false);
return;
case O::V_CMPX_GE_F32:
EmitFloatCompare(inst, IR::ValueOpcode::FPOrdGreaterThanEqual32, false, true);
EmitFloatCompare(inst, IR::ValueOpcode::FPOrdGreaterThanEqual32, false);
return;
case O::V_CMP_LT_F32:
EmitFloatCompare(inst, IR::ValueOpcode::FPOrdLessThan32, false, false);
return;
case O::V_CMPX_LT_F32:
EmitFloatCompare(inst, IR::ValueOpcode::FPOrdLessThan32, false, true);
EmitFloatCompare(inst, IR::ValueOpcode::FPOrdLessThan32, false);
return;
case O::V_CMP_LE_F32:
EmitFloatCompare(inst, IR::ValueOpcode::FPOrdLessThanEqual32, false, false);
return;
case O::V_CMPX_LE_F32:
EmitFloatCompare(inst, IR::ValueOpcode::FPOrdLessThanEqual32, false, true);
EmitFloatCompare(inst, IR::ValueOpcode::FPOrdLessThanEqual32, false);
return;
case O::V_CMP_NLG_F32:
EmitFloatCompare(inst, IR::ValueOpcode::FPUnordEqual32, false, false);
return;
case O::V_CMPX_NLG_F32:
EmitFloatCompare(inst, IR::ValueOpcode::FPUnordEqual32, false, true);
EmitFloatCompare(inst, IR::ValueOpcode::FPUnordEqual32, false);
return;
case O::V_CMP_NEQ_F32:
EmitFloatCompare(inst, IR::ValueOpcode::FPUnordNotEqual32, false, false);
return;
case O::V_CMPX_NEQ_F32:
EmitFloatCompare(inst, IR::ValueOpcode::FPUnordNotEqual32, false, true);
EmitFloatCompare(inst, IR::ValueOpcode::FPUnordNotEqual32, false);
return;
case O::V_CMP_NLE_F32:
EmitFloatCompare(inst, IR::ValueOpcode::FPUnordGreaterThan32, false, false);
return;
case O::V_CMPX_NLE_F32:
EmitFloatCompare(inst, IR::ValueOpcode::FPUnordGreaterThan32, false, true);
EmitFloatCompare(inst, IR::ValueOpcode::FPUnordGreaterThan32, false);
return;
case O::V_CMP_NLT_F32:
EmitFloatCompare(inst, IR::ValueOpcode::FPUnordGreaterThanEqual32, false, false);
return;
case O::V_CMPX_NLT_F32:
EmitFloatCompare(inst, IR::ValueOpcode::FPUnordGreaterThanEqual32, false, true);
EmitFloatCompare(inst, IR::ValueOpcode::FPUnordGreaterThanEqual32, false);
return;
case O::V_CMP_NGE_F32:
EmitFloatCompare(inst, IR::ValueOpcode::FPUnordLessThan32, false, false);
return;
case O::V_CMPX_NGE_F32:
EmitFloatCompare(inst, IR::ValueOpcode::FPUnordLessThan32, false, true);
EmitFloatCompare(inst, IR::ValueOpcode::FPUnordLessThan32, false);
return;
case O::V_CMP_NGT_F32:
EmitFloatCompare(inst, IR::ValueOpcode::FPUnordLessThanEqual32, false, false);
return;
case O::V_CMPX_NGT_F32:
EmitFloatCompare(inst, IR::ValueOpcode::FPUnordLessThanEqual32, false, true);
EmitFloatCompare(inst, IR::ValueOpcode::FPUnordLessThanEqual32, false);
return;
case O::V_CMP_EQ_F16:
EmitFloatCompare(inst, IR::ValueOpcode::FPOrdEqual32, true, false);
return;
case O::V_CMPX_EQ_F16:
EmitFloatCompare(inst, IR::ValueOpcode::FPOrdEqual32, true, true);
EmitFloatCompare(inst, IR::ValueOpcode::FPOrdEqual32, true);
return;
case O::V_CMP_LG_F16:
EmitFloatCompare(inst, IR::ValueOpcode::FPOrdNotEqual32, true, false);
EmitFloatCompare(inst, IR::ValueOpcode::FPOrdNotEqual32, true);
return;
case O::V_CMP_GT_F16:
EmitFloatCompare(inst, IR::ValueOpcode::FPOrdGreaterThan32, true, false);
return;
case O::V_CMPX_GT_F16:
EmitFloatCompare(inst, IR::ValueOpcode::FPOrdGreaterThan32, true, true);
EmitFloatCompare(inst, IR::ValueOpcode::FPOrdGreaterThan32, true);
return;
case O::V_CMP_GE_F16:
EmitFloatCompare(inst, IR::ValueOpcode::FPOrdGreaterThanEqual32, true, false);
return;
case O::V_CMPX_GE_F16:
EmitFloatCompare(inst, IR::ValueOpcode::FPOrdGreaterThanEqual32, true, true);
EmitFloatCompare(inst, IR::ValueOpcode::FPOrdGreaterThanEqual32, true);
return;
case O::V_CMP_LT_F16:
EmitFloatCompare(inst, IR::ValueOpcode::FPOrdLessThan32, true, false);
return;
case O::V_CMPX_LT_F16:
EmitFloatCompare(inst, IR::ValueOpcode::FPOrdLessThan32, true, true);
EmitFloatCompare(inst, IR::ValueOpcode::FPOrdLessThan32, true);
return;
case O::V_CMP_LE_F16:
EmitFloatCompare(inst, IR::ValueOpcode::FPOrdLessThanEqual32, true, false);
return;
case O::V_CMPX_LE_F16:
EmitFloatCompare(inst, IR::ValueOpcode::FPOrdLessThanEqual32, true, true);
EmitFloatCompare(inst, IR::ValueOpcode::FPOrdLessThanEqual32, true);
return;
case O::V_CMP_NGE_F16:
EmitFloatCompare(inst, IR::ValueOpcode::FPUnordLessThan32, true, false);
EmitFloatCompare(inst, IR::ValueOpcode::FPUnordLessThan32, true);
return;
case O::V_CMP_NGT_F16:
EmitFloatCompare(inst, IR::ValueOpcode::FPUnordLessThanEqual32, true, false);
return;
case O::V_CMPX_NGT_F16:
EmitFloatCompare(inst, IR::ValueOpcode::FPUnordLessThanEqual32, true, true);
EmitFloatCompare(inst, IR::ValueOpcode::FPUnordLessThanEqual32, true);
return;
case O::V_CMPX_NLE_F16:
EmitFloatCompare(inst, IR::ValueOpcode::FPUnordGreaterThan32, true, true);
EmitFloatCompare(inst, IR::ValueOpcode::FPUnordGreaterThan32, true);
return;
case O::V_CMP_NEQ_F16:
EmitFloatCompare(inst, IR::ValueOpcode::FPUnordNotEqual32, true, false);
return;
case O::V_CMPX_NEQ_F16:
EmitFloatCompare(inst, IR::ValueOpcode::FPUnordNotEqual32, true, true);
EmitFloatCompare(inst, IR::ValueOpcode::FPUnordNotEqual32, true);
return;
case O::V_CMP_NLT_F16:
EmitFloatCompare(inst, IR::ValueOpcode::FPUnordGreaterThanEqual32, true, false);
return;
case O::V_CMPX_NLT_F16:
EmitFloatCompare(inst, IR::ValueOpcode::FPUnordGreaterThanEqual32, true, true);
EmitFloatCompare(inst, IR::ValueOpcode::FPUnordGreaterThanEqual32, true);
return;
case O::V_CMP_O_F32: EmitFloatOrderedCompare(inst, true, false); return;
case O::V_CMP_U_F32: EmitFloatOrderedCompare(inst, false, false); return;
case O::V_CMPX_O_F32: EmitFloatOrderedCompare(inst, true, true); return;
case O::V_CMPX_U_F32: EmitFloatOrderedCompare(inst, false, true); return;
case O::V_CMP_CLASS_F32: EmitFloatClassCompare(inst, false); return;
case O::V_CMPX_CLASS_F32: EmitFloatClassCompare(inst, true); return;
case O::V_CMP_CLASS_F16: EmitFloatClassCompare(inst, false, true); return;
case O::V_CMPX_CLASS_F16: EmitFloatClassCompare(inst, true, true); return;
case O::V_CMP_O_F32:
case O::V_CMPX_O_F32: EmitFloatOrderedCompare(inst, true); return;
case O::V_CMP_U_F32:
case O::V_CMPX_U_F32: EmitFloatOrderedCompare(inst, false); return;
case O::V_CMP_CLASS_F32:
case O::V_CMPX_CLASS_F32: EmitFloatClassCompare(inst); return;
case O::V_CMP_CLASS_F16:
case O::V_CMPX_CLASS_F16: EmitFloatClassCompare(inst, true); return;
case O::V_CVT_F32_UBYTE0: V_CVT_F32_UBYTE(inst, 0); return;
case O::V_CVT_F32_UBYTE1: V_CVT_F32_UBYTE(inst, 1); return;
@@ -495,8 +361,8 @@ void Translator::EmitVector(const Decoder::Instruction& inst) {
return;
case O::V_CVT_PK_U8_F32: V_CVT_PK_U8_F32(inst); return;
case O::V_PACK_B32_F16: V_PACK_B32_F16(inst); return;
case O::V_CVT_PK_U16_U32:
case O::V_CVT_PK_I16_I32: return PackB16(inst, false, false);
case O::V_CVT_PK_U16_U32: return V_CVT_PK_16_32(inst, false);
case O::V_CVT_PK_I16_I32: return V_CVT_PK_16_32(inst, true);
case O::V_LSHLREV_B16:
return Integer16Shift(inst, IR::ValueOpcode::ShiftLeftLogical32, false);
@@ -563,7 +429,7 @@ void Translator::EmitVector(const Decoder::Instruction& inst) {
case O::V_CEIL_F16: return Float16Unary(inst, IR::ValueOpcode::FPCeil32, false);
case O::V_TRUNC_F16: return Float16Unary(inst, IR::ValueOpcode::FPTrunc32, false);
case O::V_RNDNE_F16: return Float16Unary(inst, IR::ValueOpcode::FPRoundEven32, false);
case O::V_FRACT_F16: return Float16Unary(inst, IR::ValueOpcode::FPFract32, false);
case O::V_FRACT_F16: return V_FRACT(inst, IR::Type::F16);
case O::V_SIN_F16: return Float16Trig(inst, IR::ValueOpcode::FPSin);
case O::V_COS_F16: return Float16Trig(inst, IR::ValueOpcode::FPCos);
case O::V_MIN3_F16: return Float16Ternary(inst, IR::ValueOpcode::FPMinTri32, false, false);
@@ -574,8 +440,8 @@ void Translator::EmitVector(const Decoder::Instruction& inst) {
case O::V_RCP_F32: return FloatUnary(inst, IR::ValueOpcode::FPRecip32);
case O::V_RCP_F64: return FloatUnary(inst, IR::ValueOpcode::FPRecip64);
case O::V_RCP_IFLAG_F32: return FloatUnary(inst, IR::ValueOpcode::FPRecipIFlag32);
case O::V_FRACT_F32: return FloatUnary(inst, IR::ValueOpcode::FPFract32);
case O::V_FRACT_F64: return FloatUnary(inst, IR::ValueOpcode::FPFract64);
case O::V_FRACT_F32: return V_FRACT(inst, IR::Type::F32);
case O::V_FRACT_F64: return V_FRACT(inst, IR::Type::F64);
case O::V_TRUNC_F32: return FloatUnary(inst, IR::ValueOpcode::FPTrunc32);
case O::V_TRUNC_F64: return FloatUnary(inst, IR::ValueOpcode::FPTrunc64);
case O::V_CEIL_F32: return FloatUnary(inst, IR::ValueOpcode::FPCeil32);
@@ -0,0 +1,181 @@
#ifndef EMULATOR_INCLUDE_EMULATOR_GRAPHICS_SHADER_RECOMPILER_SERIALIZE_H_
#define EMULATOR_INCLUDE_EMULATOR_GRAPHICS_SHADER_RECOMPILER_SERIALIZE_H_
#include "common/assert.h"
#include "graphics/shader/recompiler/backend/spirv/SpirvEmitter.h"
#include "graphics/shader/recompiler/ir/ShaderIR.h"
#include "graphics/shader/recompiler/ir/passes/ResourceMaterialization.h"
#include "graphics/shader/rectListShader.h"
#include <algorithm>
#include <bit>
#include <optional>
#include <span>
#include <string>
#include <tuple>
#include <type_traits>
#include <unordered_map>
#include <vector>
// Shader cache records. Io(io, value) appends value to a Writer or reads it back from a Reader
// field by field, so padding never reaches the bytes; instructions travel as plan-local indices.
namespace Libs::Graphics::ShaderRecompiler::IR {
struct Writer {
std::vector<uint8_t> bytes;
std::unordered_map<const Inst*, uint32_t> index; // Instructions that values may refer to.
void Raw(const void* data, size_t size) {
const auto* begin = static_cast<const uint8_t*>(data);
bytes.insert(bytes.end(), begin, begin + size);
}
};
struct Reader {
std::span<const uint8_t> bytes;
std::vector<Inst*> insts; // Instructions by index.
bool ok = true; // False once a read ran past the end or was invalid.
void Raw(void* data, size_t size) {
ok = ok && size <= bytes.size();
if (!ok) return;
std::copy_n(bytes.data(), size, static_cast<uint8_t*>(data));
bytes = bytes.subspan(size);
}
// Every element takes at least one byte, which bounds counts read from damaged records.
uint32_t Count(uint32_t count) {
ok = ok && count <= bytes.size();
return ok ? count : 0u;
}
};
template <class S> constexpr bool Reading = std::is_same_v<S, Reader>;
template <class T, template <class...> class Of> constexpr bool IsA = false;
template <template <class...> class Of, class... A> constexpr bool IsA<Of<A...>, Of> = true;
template <class T, class U> concept Is = std::is_same_v<std::remove_const_t<T>, U>;
template <class S, class T> void Io(S& io, T& value);
template <class S, class... T> void IoAll(S& io, T&&... values) {
(Io(io, values), ...);
}
// The value's Type, Opaque for an instruction, then the instruction index or the zero-extended
// immediate.
inline void IoValue(Writer& writer, const Value& value) {
auto type = Type::Opaque;
uint64_t bits = 0;
if (const auto* inst = value.TryInstruction()) {
bits = writer.index.at(inst);
} else {
type = value.GetType();
switch (type) {
case Type::Void: break;
case Type::ScalarReg: bits = RegIndex(value.ScalarRegister()); break;
case Type::VectorReg: bits = RegIndex(value.VectorRegister()); break;
case Type::U1: bits = value.U1(); break;
case Type::U8: bits = value.U8(); break;
case Type::U16: bits = value.U16(); break;
case Type::U32: bits = value.U32(); break;
case Type::U64: bits = value.U64(); break;
case Type::F16: bits = value.F16Bits(); break;
case Type::F32: bits = std::bit_cast<uint32_t>(value.F32Value()); break;
default: EXIT("value type %u has no cache encoding\n", static_cast<uint32_t>(type));
}
}
IoAll(writer, type, bits);
}
inline void IoValue(Reader& reader, Value& value) {
auto type = Type::Void;
uint64_t bits = 0;
IoAll(reader, type, bits);
switch (type) {
case Type::Void: value = {}; break;
case Type::Opaque:
reader.ok = reader.ok && bits < reader.insts.size();
if (reader.ok) value = Value(reader.insts[bits]);
break;
case Type::ScalarReg: value = Value(static_cast<ScalarReg>(bits)); break;
case Type::VectorReg: value = Value(static_cast<VectorReg>(bits)); break;
case Type::U1: value = Value(bits != 0u); break;
case Type::U8: value = Value(static_cast<uint8_t>(bits)); break;
case Type::U16: value = Value(static_cast<uint16_t>(bits)); break;
case Type::U32: value = Value(static_cast<uint32_t>(bits)); break;
case Type::U64: value = Value(bits); break;
case Type::F16: value = Value::F16(static_cast<uint16_t>(bits)); break;
case Type::F32: value = Value::F32(std::bit_cast<float>(static_cast<uint32_t>(bits))); break;
default: reader.ok = false; break;
}
}
// Scalars and standard containers; structs have their own overloads below.
template <class S, class T> void Io(S& io, T& value) {
using U = std::remove_const_t<T>;
static_assert(!std::is_pointer_v<U>, "pointers do not persist");
if constexpr (std::is_arithmetic_v<U> || std::is_enum_v<U>) {
io.Raw(&value, sizeof(U));
} else if constexpr (std::is_same_v<U, Value>) {
IoValue(io, value);
} else if constexpr (IsA<U, std::vector> || IsA<U, std::basic_string>) {
auto count = static_cast<uint32_t>(value.size());
Io(io, count);
if constexpr (Reading<S>) value.resize(io.Count(count));
using E = typename U::value_type;
if constexpr (std::is_arithmetic_v<E> || std::is_enum_v<E>) {
io.Raw(value.data(), value.size() * sizeof(E));
} else {
for (auto& element: value) Io(io, element);
}
} else if constexpr (IsA<U, std::optional>) {
bool has = value.has_value();
Io(io, has);
if constexpr (Reading<S>) {
if (has) value = typename U::value_type {}; // Not emplace(): clang rejects nested aggregates.
else value.reset();
}
if (has) Io(io, *value);
} else {
static_assert(requires { std::tuple_size<U>::value; }, "struct without an Io overload");
std::apply([&](auto&... elements) { IoAll(io, elements...); }, value); // std::array, std::pair
}
}
// Binds every field, so that a field added later fails to compile until it is listed.
#define KYTY_IO_FIELDS(Struct, ...) \
template <class S, Is<Struct> T> void Io(S& io, T& object) { \
auto& [__VA_ARGS__] = object; \
IoAll(io, __VA_ARGS__); \
}
KYTY_IO_FIELDS(MemoryInfo, 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)
KYTY_IO_FIELDS(BufferResource, a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s)
KYTY_IO_FIELDS(ImageResource, a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, t, u, v, w)
KYTY_IO_FIELDS(SamplerResource, a, b, c, d, e, f, g, h, i, j, k)
KYTY_IO_FIELDS(SampledResourcePair, a, b, c)
KYTY_IO_FIELDS(StageInput, a, b, c, d, e)
KYTY_IO_FIELDS(StageOutput, a, b, c, d)
KYTY_IO_FIELDS(ShaderInfo, a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, t, u, v, w, x, y, z,
A, B, C, D, E, F, G, H, I, J, K)
KYTY_IO_FIELDS(DescriptorSource::IndirectDescriptor::SelectorRead::CountedRows, a, b)
KYTY_IO_FIELDS(DescriptorSource::IndirectDescriptor::SelectorRead, a, b, c, d, e, f, g, h, i, j)
KYTY_IO_FIELDS(DescriptorSource::IndirectDescriptor::PointerTable, a, b, c, d)
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, a, b, c)
KYTY_IO_FIELDS(SrtRead, a, b)
KYTY_IO_FIELDS(ResourceBlock, a, b, c, d)
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)
KYTY_IO_FIELDS(ResourceSpecialization::Image, a, b, c, d, e, f, g, h, i, j, k)
KYTY_IO_FIELDS(ResourceSpecialization::Sampler, a, b, c)
KYTY_IO_FIELDS(ResourceSpecialization, a, b, c)
KYTY_IO_FIELDS(Bindings, a, b, c, d, e)
KYTY_IO_FIELDS(CompiledShaderInfo, a, b, c, d, e, f, g, h, i, j)
KYTY_IO_FIELDS(Spirv::Profile, a, b, c)
KYTY_IO_FIELDS(ShaderTessellationInputInfo, a, b, c, d, e, f, g, h, i)
KYTY_IO_FIELDS(RectListParameter, a, b, c)
#undef KYTY_IO_FIELDS
} // namespace Libs::Graphics::ShaderRecompiler::IR
#endif /* EMULATOR_INCLUDE_EMULATOR_GRAPHICS_SHADER_RECOMPILER_SERIALIZE_H_ */
+9 -3
View File
@@ -365,7 +365,7 @@ static_assert(static_cast<uint32_t>(DescriptorBindingKind::Count) == 58u);
struct PushData {
static constexpr uint32_t DwordCount = 32;
static constexpr uint32_t MeshDrawDwordCount = 6;
static constexpr uint32_t MeshDrawDwordCount = 5;
static constexpr uint32_t NoStart = UINT32_MAX;
std::array<uint32_t, DwordCount> dwords {};
@@ -495,10 +495,16 @@ struct Bindings {
uint32_t dispatch_thread_dword = PushData::NoStart;
uint32_t memory_offset_dword = 0;
[[nodiscard]] uint32_t ShaderDataDwords() const {
// Packed 8-bit binding offsets, then each binding's bound range in bytes: OpArrayLength
// reads 0 for ranges of 2 GiB or more on NVIDIA.
[[nodiscard]] uint32_t MemorySizeDword() const {
return memory_offset_dword +
(descriptor_counts[static_cast<size_t>(DescriptorBindingKind::Buffers)] + 3u) / 4u;
}
[[nodiscard]] uint32_t ShaderDataDwords() const {
return MemorySizeDword() +
descriptor_counts[static_cast<size_t>(DescriptorBindingKind::Buffers)];
}
[[nodiscard]] bool UsesPushData() const {
return push_data_start_dword != PushData::NoStart;
}
@@ -541,6 +547,7 @@ struct ShaderInfo {
bool subgroup_shuffle = false;
bool subgroup_arithmetic = false;
bool subgroup_local_invocation_id = false;
bool helper_invocation = false;
bool compute_derivatives = false;
bool image_gather_extended = false;
bool function_lds = false;
@@ -655,7 +662,6 @@ struct UniformFillPlan {
// condition and fill values without translated blocks, plus reusable evaluation scratch.
struct ResourcePlan {
struct SelectorKeys {
uint32_t source = UINT32_MAX;
std::vector<uint32_t> values;
bool resolved = false;
};
@@ -96,6 +96,7 @@ VALUE_OPCODE(WqmU64, U64, U64)
// Core value operations used to expand architectural side effects.
VALUE_OPCODE(SelectU1, U1, U1, U1, U1)
VALUE_OPCODE(SelectF32, F32, U1, F32, F32)
VALUE_OPCODE(SelectF64, F64, U1, F64, F64)
VALUE_OPCODE(IAdd32, U32, U32, U32)
VALUE_OPCODE(IAdd64, U64, U64, U64)
VALUE_OPCODE(ISub32, U32, U32, U32)
@@ -218,11 +219,13 @@ VALUE_OPCODE(FPFract64, F64, F64)
VALUE_OPCODE(LaneId, U32)
VALUE_OPCODE(Ballot, U32x4, U1)
VALUE_OPCODE(LiveMask, U32x4)
VALUE_OPCODE(ReadFirstLane, U32, U32, U1)
VALUE_OPCODE(ReadLane, U32, U32, U32)
VALUE_OPCODE(SubgroupUMinU32, U32, U32)
VALUE_OPCODE(SubgroupUMaxU32, U32, U32)
VALUE_OPCODE(WriteLane, U32, U32, U32, U32)
VALUE_OPCODE(Permlane16U32, U32, U32, U32, U32, U1)
VALUE_OPCODE(Permlane16U32, U32, U32, U32, U32, U1, U32)
VALUE_OPCODE(PermuteU32, U32, U32, U32, U1)
VALUE_OPCODE(BpermuteU32, U32, U32, U32, U1)
@@ -308,8 +311,8 @@ VALUE_OPCODE(SharedAtomicOr32, U32, U32, U32, U1)
VALUE_OPCODE(SharedAtomicOr64, Void, U32, U64, U1)
VALUE_OPCODE(SharedAtomicXor32, U32, U32, U32, U1)
VALUE_OPCODE(SharedAtomicMaskedOr32, Void, U32, U32, U32, U1)
VALUE_OPCODE(DataAppend, U32, U32, U1)
VALUE_OPCODE(DataConsume, U32, U32, U1)
VALUE_OPCODE(DataAppend, U32, U32, U32, U32, U1)
VALUE_OPCODE(DataConsume, U32, U32, U32, U32, U1)
VALUE_OPCODE(SwizzleU32, U32, U32, U32, U1)
VALUE_OPCODE(ImageQueryDimensions, U32x4, ImageResource, ImageAddress)
@@ -167,6 +167,16 @@ bool ReplaceBinaryIdentity(Inst& inst, Type type, uint64_t identity) {
return false;
}
// Whether one operand is the other's negation.
bool IsComplementPair(const Inst& inst, ValueOpcode negation) {
for (uint32_t arg = 0; arg < 2u; ++arg) {
const auto* negate = Arg(inst, arg).TryInstruction();
if (negate != nullptr && negate->GetOpcode() == negation &&
Arg(*negate, 0) == Arg(inst, arg ^ 1u)) return true;
}
return false;
}
bool FoldSelect(Inst& inst) {
const auto condition = Arg(inst, 0);
const auto true_value = Arg(inst, 1);
@@ -626,8 +636,9 @@ void FoldInstruction(Block& block, Block::iterator instruction,
case ValueOpcode::BitwiseXor32:
if (!FoldU32(inst, [opcode = inst.GetOpcode()](uint32_t a, uint32_t b) {
return opcode == ValueOpcode::BitwiseOr32 ? a | b : a ^ b;
})) {
ReplaceBinaryIdentity(inst, Type::U32, 0u);
}) && !ReplaceBinaryIdentity(inst, Type::U32, 0u) &&
IsComplementPair(inst, ValueOpcode::BitwiseNot32)) {
Replace(inst, Value(UINT32_MAX));
}
return;
case ValueOpcode::BitwiseNot32: {
@@ -802,6 +813,8 @@ void FoldInstruction(Block& block, Block::iterator instruction,
Replace(inst, lhs.U1() ? lhs : rhs);
} else if (IsImmediate(rhs, Type::U1)) {
Replace(inst, rhs.U1() ? rhs : lhs);
} else if (IsComplementPair(inst, ValueOpcode::LogicalNot)) {
Replace(inst, Value(true));
} else {
const auto* left = lhs.TryInstruction();
const auto* right = rhs.TryInstruction();
@@ -892,8 +905,10 @@ private:
if (inst.GetOpcode() != ValueOpcode::CompositeExtractU32x4 ||
!Immediate(Arg(inst, 1), 0u)) return {};
const auto* source = Arg(inst, 0).TryInstruction();
return source != nullptr && source->GetOpcode() == ValueOpcode::Ballot
? Arg(*source, 0) : Value {};
if (source == nullptr) return {};
// Helper lanes keep the per-lane EXEC, so the live mask projects to true.
if (source->GetOpcode() == ValueOpcode::LiveMask) return Value(true);
return source->GetOpcode() == ValueOpcode::Ballot ? Arg(*source, 0) : Value {};
}
bool CanProject(Value value) {
@@ -99,89 +99,66 @@ Value GetRealValue(PhiMap& phi_map, Value source, uint32_t lane, uint32_t wave_s
return Value(&phi);
}
// A full-EXEC prefix scan with both half-wave endpoint reads is a reduction.
// A full-EXEC prefix scan with both half-wave endpoint reads is a reduction. Wave64 first joins
// the two rows of each half; a wave32 half is one row.
// Fragment subgroups may have no physical invocation at either endpoint.
class UMinReductionMatcher {
class WaveReductionMatcher {
public:
explicit UMinReductionMatcher(Block* block): block(block) {}
WaveReductionMatcher(const Block* block, ValueOpcode operation)
: block(block), operation(operation) {}
Value Match(const Inst& minimum) {
const auto* low = Get(minimum.Arg(0), ValueOpcode::ReadLane);
const auto* high = Get(minimum.Arg(1), ValueOpcode::ReadLane);
Value Match(const Inst& reduction, uint32_t wave_size) const {
const auto* low = Get(reduction.Arg(0), ValueOpcode::ReadLane);
const auto* high = Get(reduction.Arg(1), ValueOpcode::ReadLane);
if (low == nullptr || high == nullptr || low->Arg(0).Resolve() != high->Arg(0).Resolve()) {
return {};
}
const auto lane = [](Value value, uint32_t expected) {
const auto lane = [&](Value value, uint32_t expected) {
value = value.Resolve();
return value.IsImmediate() && value.GetType() == Type::U32 &&
value.U32() % 64u == expected;
value.U32() % wave_size == expected;
};
if (!((lane(low->Arg(1), 31) && lane(high->Arg(1), 63)) ||
(lane(low->Arg(1), 63) && lane(high->Arg(1), 31)))) {
const auto half = wave_size / 2u;
if (!((lane(low->Arg(1), half - 1u) && lane(high->Arg(1), wave_size - 1u)) ||
(lane(low->Arg(1), wave_size - 1u) && lane(high->Arg(1), half - 1u)))) {
return {};
}
const auto* joined = Get(Unwrap(low->Arg(0)), ValueOpcode::UMin32);
if (wave_size == 32u) return MatchScan(low->Arg(0));
const auto* joined = Get(low->Arg(0), operation);
if (joined == nullptr) return {};
for (size_t index = 0; index < 2; index++) {
auto attempt = *this;
const auto* cross = attempt.Get(attempt.Unwrap(joined->Arg(index)), ValueOpcode::Permlane16U32);
const auto* cross = Get(joined->Arg(index), ValueOpcode::Permlane16U32);
if (cross == nullptr) continue;
const auto flags = cross->Flags<PermlaneFlags>();
auto prefix = attempt.Unwrap(joined->Arg(1 - index));
const auto flags = cross->Flags<PermlaneFlags>();
const auto prefix = joined->Arg(1 - index).Resolve();
if (!flags.x16 || flags.fetch_inactive || !flags.bound_control ||
cross->Arg(1).Resolve() != Value(UINT32_MAX) ||
cross->Arg(2).Resolve() != Value(UINT32_MAX) ||
!attempt.FullExec(cross->Arg(3)) || attempt.Unwrap(cross->Arg(0)) != prefix) {
cross->Arg(2).Resolve() != Value(UINT32_MAX) || !FullExec(cross->Arg(3)) ||
cross->Arg(0).Resolve() != prefix) {
continue;
}
for (const uint32_t shift: {8u, 4u, 2u, 1u}) {
prefix = attempt.MatchRow(prefix, shift);
}
prefix = attempt.Unwrap(prefix);
if (prefix.IsEmpty() || attempt.saved_exec.IsEmpty()) return prefix;
const auto* seed = attempt.Get(prefix, ValueOpcode::SelectU32);
if (seed != nullptr && seed->Arg(2).Resolve() == Value(UINT32_MAX)) {
const auto predicate = seed->Arg(0).Resolve();
const auto* subset = attempt.Get(predicate, ValueOpcode::LogicalAnd);
if (predicate == attempt.saved_exec ||
(subset != nullptr && (subset->Arg(0).Resolve() == attempt.saved_exec ||
subset->Arg(1).Resolve() == attempt.saved_exec))) {
return prefix;
}
}
if (const auto seed = MatchScan(prefix); !seed.IsEmpty()) return seed;
}
return {};
}
private:
// The row scan's input.
Value MatchScan(Value prefix) const {
for (const uint32_t shift: {8u, 4u, 2u, 1u}) {
prefix = MatchRow(prefix, shift);
}
return prefix;
}
Inst* Get(Value value, ValueOpcode opcode) const {
auto* inst = value.Resolve().TryInstruction();
return inst != nullptr && inst->Parent() == block && inst->GetOpcode() == opcode
? inst : nullptr;
}
bool FullExec(Value predicate) {
predicate = predicate.Resolve();
if (predicate == Value(true)) return true;
const auto* either = Get(predicate, ValueOpcode::LogicalOr);
if (either == nullptr) return false;
for (size_t index = 0; index < 2; index++) {
const auto* negate = Get(either->Arg(index), ValueOpcode::LogicalNot);
if (negate == nullptr || negate->Arg(0).Resolve() != either->Arg(1 - index).Resolve()) continue;
const auto original = negate->Arg(0).Resolve();
if (saved_exec.IsEmpty()) saved_exec = original;
return saved_exec == original;
}
return false;
}
Value Unwrap(Value value) {
while (const auto* select = Get(value, ValueOpcode::SelectU32)) {
if (!FullExec(select->Arg(0))) break;
value = select->Arg(1);
}
return value.Resolve();
}
// Constant propagation folds the x | ~x EXEC of s_orn2_saveexec to true.
static bool FullExec(Value predicate) { return predicate.Resolve() == Value(true); }
static bool IsRowShift(const Inst& inst, uint32_t shift) {
const auto flags = inst.Flags<DppMoveFlags>();
@@ -189,22 +166,24 @@ private:
flags.bank_mask == 0xf && !flags.fetch_inactive && !flags.bound_control && !flags.dpp8;
}
Value MatchRow(Value value, uint32_t shift) {
const auto* update = Get(Unwrap(value), ValueOpcode::DppUpdateU32);
Value MatchRow(Value value, uint32_t shift) const {
const auto* update = Get(value, ValueOpcode::DppUpdateU32);
if (update == nullptr || !IsRowShift(*update, shift) || !FullExec(update->Arg(2))) return {};
const auto* minimum = Get(update->Arg(0), ValueOpcode::UMin32);
if (minimum == nullptr) return {};
const auto* combined = Get(update->Arg(0), operation);
if (combined == nullptr) return {};
for (size_t index = 0; index < 2; index++) {
const auto* move = Get(minimum->Arg(index), ValueOpcode::DppMoveU32);
const auto* move = Get(combined->Arg(index), ValueOpcode::DppMoveU32);
if (move == nullptr || !IsRowShift(*move, shift) || !FullExec(move->Arg(1))) continue;
const auto previous = Unwrap(minimum->Arg(1 - index));
if (Unwrap(move->Arg(0)) == previous && Unwrap(update->Arg(1)) == previous) return previous;
const auto previous = combined->Arg(1 - index).Resolve();
if (move->Arg(0).Resolve() == previous && update->Arg(1).Resolve() == previous) {
return previous;
}
}
return {};
}
Block* block;
Value saved_exec;
const Block* block;
ValueOpcode operation;
};
} // namespace
@@ -217,18 +196,22 @@ ReadLaneStats EliminateReadLane(Program& program, uint32_t wave_size) {
for (auto* block: program.blocks) {
for (auto& inst: *block) {
if (wave_size == 64u && inst.GetOpcode() == ValueOpcode::UMin32) {
const auto seed = UMinReductionMatcher(block).Match(inst);
const auto opcode = inst.GetOpcode();
if (opcode == ValueOpcode::UMin32 || opcode == ValueOpcode::UMax32) {
const auto seed = WaveReductionMatcher(block, opcode).Match(inst, wave_size);
if (!seed.IsEmpty()) {
const auto position = std::find_if(block->begin(), block->end(),
[&](const Inst& candidate) { return &candidate == &inst; });
auto& reduction = *block->PrependNewInst(position, ValueOpcode::SubgroupUMinU32, {seed});
const auto reduce = opcode == ValueOpcode::UMin32
? ValueOpcode::SubgroupUMinU32
: ValueOpcode::SubgroupUMaxU32;
auto& reduction = *block->PrependNewInst(position, reduce, {seed});
inst.ReplaceUsesWith(Value(&reduction));
stats.rewritten_reductions++;
}
continue;
}
if (inst.GetOpcode() != ValueOpcode::ReadLane) {
if (opcode != ValueOpcode::ReadLane) {
continue;
}
const auto selector = inst.Arg(1).Resolve();
@@ -3,6 +3,7 @@
#include "common/assert.h"
#include "graphics/guest_gpu/gpu_format.h"
#include "graphics/shader/recompiler/BufferFormat.h"
#include "graphics/shader/recompiler/ir/Serialize.h"
#include "graphics/shader/recompiler/ir/ShaderIR.h"
#include "graphics/shader/shaderBindings.h"
@@ -1311,6 +1312,17 @@ static UniformFillPlan AnalyzeUniformFill(const Program& program) {
return result;
}
// State an extracted or loaded plan derives from its fields: one key set per selector group, which
// extraction numbers densely in order of first use (false when they are not), and the walker.
static bool DerivePlanState(ResourcePlan& plan) {
for (const auto group: plan.selector_groups) {
if (group == plan.selector_keys.size()) plan.selector_keys.emplace_back();
if (group != UINT32_MAX && group >= plan.selector_keys.size()) return false;
}
if (plan.srt_plan_complete && plan.resource_tracking_complete) CompileSrtWalker(plan);
return true;
}
ResourcePlan ExtractResourcePlan(const Program& program) {
ResourcePlan plan;
plan.stage = program.stage;
@@ -1391,17 +1403,18 @@ ResourcePlan ExtractResourcePlan(const Program& program) {
}
// Destination tables share keys when their complete selector domains agree.
// Resolve those domains lazily at dispatch time, without repeated guest reads.
std::vector<uint32_t> group_sources; // The first source of each group.
plan.selector_groups.resize(plan.descriptor_sources.size(), UINT32_MAX);
for (uint32_t source = 0; source < plan.descriptor_sources.size(); ++source) {
const auto& descriptor = program.descriptor_sources[source].indirect_descriptor;
if (!descriptor || !descriptor->selector) continue;
const auto& a = *descriptor;
uint32_t group = 0;
for (; group < plan.selector_keys.size(); ++group) {
const auto& b = *program.descriptor_sources[plan.selector_keys[group].source].indirect_descriptor;
for (; group < group_sources.size(); ++group) {
const auto& b = *program.descriptor_sources[group_sources[group]].indirect_descriptor;
if (EquivalentSelectorDomain(program, a, b)) break;
}
if (group == plan.selector_keys.size()) plan.selector_keys.push_back({.source = source});
if (group == group_sources.size()) group_sources.push_back(source);
plan.selector_groups[source] = group;
}
plan.srt_reads.reserve(program.srt_reads.size());
@@ -1491,7 +1504,7 @@ ResourcePlan ExtractResourcePlan(const Program& program) {
capture_indirect_reads || capture_control_reads || !plan.source_reads.empty();
if (capture_indirect_reads || !plan.source_reads.empty())
plan.resource_tracking_complete &= !program.has_address_writes;
if (plan.srt_plan_complete && plan.resource_tracking_complete) CompileSrtWalker(plan);
DerivePlanState(plan);
return plan;
}
@@ -1986,4 +1999,86 @@ void ApplyResourceSpecialization(Program& program, const ResourceSpecialization&
image_remap.Apply(images);
}
namespace {
using ControlPhis = decltype(ResourcePlan::control_phi_predecessors);
// Instructions come first, so that any value can refer to them by index, then their arguments.
// Plan Phis have no blocks; a control Phi lists the plan blocks of its arguments after them.
void IoValues(Writer& writer, const std::list<Inst>& values, const ControlPhis& phis) {
IoAll(writer, static_cast<uint32_t>(values.size()));
for (const auto& inst: values) {
writer.index.emplace(&inst, static_cast<uint32_t>(writer.index.size()));
IoAll(writer, inst.GetOpcode(), inst.Flags<uint64_t>());
}
for (const auto& inst: values) {
if (inst.GetOpcode() != ValueOpcode::Phi) {
for (size_t index = 0; index < inst.NumArgs(); ++index) IoAll(writer, inst.Arg(index));
continue;
}
std::vector<Value> args;
for (size_t index = 0; index < inst.NumArgs(); ++index) {
EXIT_IF(inst.PhiBlock(index) != nullptr);
args.push_back(inst.Arg(index));
}
const auto found = phis.find(&inst);
IoAll(writer, args, found != phis.end() ? found->second : std::vector<uint32_t> {});
}
}
void IoValues(Reader& reader, std::list<Inst>& values, ControlPhis& phis) {
uint32_t count = 0;
Io(reader, count);
count = reader.Count(count);
for (uint32_t index = 0; index < count && reader.ok; ++index) {
ValueOpcode opcode {};
uint64_t flags = 0;
IoAll(reader, opcode, flags);
reader.ok = reader.ok && opcode < ValueOpcode::Count;
if (reader.ok) reader.insts.push_back(&values.emplace_back(opcode, flags));
}
for (auto* inst: reader.insts) {
if (inst->GetOpcode() != ValueOpcode::Phi) {
for (size_t index = 0; index < inst->NumArgs(); ++index) {
Value arg;
Io(reader, arg);
inst->SetArg(index, arg);
}
continue;
}
std::vector<Value> args;
std::vector<uint32_t> predecessors;
IoAll(reader, args, predecessors);
for (const auto arg: args) inst->AddPhiOperand(nullptr, arg);
if (!predecessors.empty()) phis.emplace(inst, std::move(predecessors));
}
}
// Binds every plan field: one added later fails to compile here until it is stored or derived.
template <class S, class P> void IoPlan(S& io, P& plan) {
auto& [stage, shader_hash, user_data_base, user_data_count, values, memory_info, descriptor_sources,
selector_groups, control_flow, control_phis, srt_reads, source_reads, capture_specialization_reads,
srt_plan_complete, resource_tracking_complete, info, uniform_fill,
// Derived on load.
srt_code, srt_user_data, srt_captures, srt_scratch, material_keys, selector_keys] = plan;
IoValues(io, values, control_phis);
IoAll(io, stage, shader_hash, user_data_base, user_data_count, memory_info, descriptor_sources,
selector_groups, control_flow, srt_reads, source_reads, capture_specialization_reads,
srt_plan_complete, resource_tracking_complete, info, uniform_fill);
}
} // namespace
std::vector<uint8_t> SaveResourcePlan(const ResourcePlan& plan) {
Writer writer;
IoPlan(writer, plan);
return std::move(writer.bytes);
}
bool LoadResourcePlan(std::span<const uint8_t> bytes, ResourcePlan& plan) {
Reader reader {bytes};
IoPlan(reader, plan);
return reader.ok && reader.bytes.empty() && DerivePlanState(plan);
}
} // namespace Libs::Graphics::ShaderRecompiler::IR
@@ -53,6 +53,11 @@ struct ResourceSpecialization {
// its values and is independent of the translated shader CFG.
ResourcePlan ExtractResourcePlan(const Program& program);
// Shader cache record of a plan; loading it into a default-constructed plan rebuilds the walker
// and scratch state like extraction. A failed load leaves a plan that must not be used.
std::vector<uint8_t> SaveResourcePlan(const ResourcePlan& plan);
bool LoadResourcePlan(std::span<const uint8_t> bytes, ResourcePlan& plan);
// Refreshes cached resources and specialization in place. A failed refresh must not be used.
bool MaterializeResources(const ResourcePlan& program, const SrtRuntime& runtime,
ResourceSnapshot& snapshot, ResourceSpecialization& specialization);
@@ -366,7 +366,6 @@ void Visit(Program& program, ShaderStageInputInfo input_info, InputUsage& inputs
info.function_lds |= kind == ResourceKind::Lds &&
program.stage != ShaderType::Compute && program.stage != ShaderType::Mesh;
if (shared_access == SharedAccess::Append || shared_access == SharedAccess::Consume) {
info.subgroup_ballot = true;
info.subgroup_shuffle = true;
info.subgroup_local_invocation_id = true;
}
@@ -517,7 +516,9 @@ void Visit(Program& program, ShaderStageInputInfo input_info, InputUsage& inputs
inst.Flags<CFG::BranchCondition>() != CFG::BranchCondition::ScalarInstruction;
break;
case ValueOpcode::Ballot: info.subgroup_ballot = true; break;
case ValueOpcode::SubgroupUMinU32: info.subgroup_arithmetic = true; break;
case ValueOpcode::LiveMask: info.subgroup_ballot = info.helper_invocation = true; break;
case ValueOpcode::SubgroupUMinU32:
case ValueOpcode::SubgroupUMaxU32: info.subgroup_arithmetic = true; break;
case ValueOpcode::DppMoveU32:
case ValueOpcode::ReadFirstLane:
case ValueOpcode::ReadLane: {
@@ -576,7 +577,7 @@ void CollectShaderInfo(Program& program, ShaderStageInputInfo input_info) {
info.has_bitwise_xor = info.uses_lds = info.uses_gds = info.uses_flattened_srt =
info.uses_dispatch_threads = info.uses_swizzle = false;
info.bvh = info.subgroup_ballot = info.subgroup_vote = info.subgroup_barrier =
info.subgroup_shuffle = info.subgroup_arithmetic =
info.subgroup_shuffle = info.subgroup_arithmetic = info.helper_invocation =
info.subgroup_local_invocation_id = info.compute_derivatives = info.image_gather_extended =
info.function_lds = info.function_scratch = info.pixel_valid_mask = info.buffer_int64_atomics =
info.buffer_u8 = info.buffer_u16 = info.shared_int64_atomics = info.coherent_buffers = info.float64 = false;
+59 -69
View File
@@ -18,45 +18,9 @@ using ShaderRecompiler::Spirv::Builder;
constexpr uint32_t SpirvVersion15 = 0x00010500u;
struct Parameter {
uint32_t input_location = 0;
uint32_t output_location = 0;
bool flat = false;
};
std::vector<Parameter> GetParameters(const ShaderVertexInputInfo& vertex_info,
const ShaderPixelInputInfo* pixel_info) {
if (pixel_info == nullptr) {
return {};
}
EXIT_IF(pixel_info->input_num > ShaderVertexInputInfo::RES_MAX);
EXIT_IF(pixel_info->stage.program == nullptr);
std::vector<uint32_t> active_inputs;
for (const auto& input: pixel_info->stage.program->info.inputs) {
if (input.kind == ShaderRecompiler::IR::StageInputKind::Parameter) {
active_inputs.push_back(input.location);
}
}
std::vector<Parameter> parameters;
std::array<bool, 32> output_locations {};
for (const auto input: active_inputs) {
const auto input_location = ShaderPixelParameterMappedLocation(*pixel_info, input);
const auto output_location = ShaderPixelParameterLocation(*pixel_info, active_inputs, input);
if ((vertex_info.stage.program->param_export_mask & (1u << input_location)) != 0 &&
!output_locations[output_location]) {
parameters.push_back({input_location, output_location,
ShaderPixelParameterIsFlat(*pixel_info, input)});
output_locations[output_location] = true;
}
}
return parameters;
}
class RectListEmitter {
public:
RectListEmitter(const std::vector<Parameter>& parameters_, spv::ExecutionModel model)
RectListEmitter(const std::vector<RectListParameter>& parameters_, spv::ExecutionModel model)
: parameters(parameters_) {
builder.AddMemoryModel(spv::AddressingModelLogical, spv::MemoryModelGLSL450);
@@ -315,42 +279,68 @@ private:
Result(spv::OpFAdd, vec4_float_type, p1, p2));
}
Builder builder {SpirvVersion15};
const std::vector<Parameter>& parameters;
std::vector<uint32_t> interfaces;
std::vector<uint32_t> inputs;
std::vector<uint32_t> outputs;
uint32_t main = 0;
uint32_t void_type = 0;
uint32_t bool_type = 0;
uint32_t uint_type = 0;
uint32_t int_type = 0;
uint32_t float_type = 0;
uint32_t vec2_bool_type = 0;
uint32_t vec2_float_type = 0;
uint32_t vec3_float_type = 0;
uint32_t vec4_float_type = 0;
uint32_t function_type = 0;
uint32_t per_vertex_type = 0;
uint32_t ptr_input_float = 0;
uint32_t ptr_input_vec4_float = 0;
uint32_t ptr_output_float = 0;
uint32_t ptr_output_vec4_float = 0;
uint32_t gl_in = 0;
uint32_t gl_out = 0;
uint32_t tess_inner = 0;
uint32_t tess_outer = 0;
uint32_t tess_coord = 0;
uint32_t invocation_id = 0;
Builder builder {SpirvVersion15};
const std::vector<RectListParameter>& parameters;
std::vector<uint32_t> interfaces;
std::vector<uint32_t> inputs;
std::vector<uint32_t> outputs;
uint32_t main = 0;
uint32_t void_type = 0;
uint32_t bool_type = 0;
uint32_t uint_type = 0;
uint32_t int_type = 0;
uint32_t float_type = 0;
uint32_t vec2_bool_type = 0;
uint32_t vec2_float_type = 0;
uint32_t vec3_float_type = 0;
uint32_t vec4_float_type = 0;
uint32_t function_type = 0;
uint32_t per_vertex_type = 0;
uint32_t ptr_input_float = 0;
uint32_t ptr_input_vec4_float = 0;
uint32_t ptr_output_float = 0;
uint32_t ptr_output_vec4_float = 0;
uint32_t gl_in = 0;
uint32_t gl_out = 0;
uint32_t tess_inner = 0;
uint32_t tess_outer = 0;
uint32_t tess_coord = 0;
uint32_t invocation_id = 0;
};
} // namespace
RectListShaders BuildRectListShaders(const ShaderVertexInputInfo& vertex_info,
const ShaderPixelInputInfo* pixel_info) {
const auto parameters = GetParameters(vertex_info, pixel_info);
RectListEmitter control(parameters, spv::ExecutionModelTessellationControl);
RectListEmitter evaluation(parameters, spv::ExecutionModelTessellationEvaluation);
std::vector<RectListParameter> RectListParameters(
const ShaderPixelInputInfo& pixel_info, const ShaderRecompiler::IR::CompiledShaderInfo& program) {
EXIT_IF(pixel_info.input_num > ShaderVertexInputInfo::RES_MAX);
std::vector<uint32_t> active_inputs;
for (const auto& input: program.info.inputs) {
if (input.kind == ShaderRecompiler::IR::StageInputKind::Parameter) {
active_inputs.push_back(input.location);
}
}
std::vector<RectListParameter> parameters;
for (const auto input: active_inputs) {
parameters.push_back({ShaderPixelParameterMappedLocation(pixel_info, input),
ShaderPixelParameterLocation(pixel_info, active_inputs, input),
ShaderPixelParameterIsFlat(pixel_info, input)});
}
return parameters;
}
RectListShaders BuildRectListShaders(std::span<const RectListParameter> parameters,
uint32_t param_export_mask) {
std::vector<RectListParameter> exported;
std::array<bool, 32> output_locations {};
for (const auto& parameter: parameters) {
if ((param_export_mask & (1u << parameter.input_location)) != 0 &&
!output_locations[parameter.output_location]) {
exported.push_back(parameter);
output_locations[parameter.output_location] = true;
}
}
RectListEmitter control(exported, spv::ExecutionModelTessellationControl);
RectListEmitter evaluation(exported, spv::ExecutionModelTessellationEvaluation);
return {control.EmitControl(), evaluation.EmitEvaluation()};
}
+18 -3
View File
@@ -2,20 +2,35 @@
#define EMULATOR_SRC_GRAPHICS_SHADER_RECTLISTSHADER_H_
#include <cstdint>
#include <span>
#include <vector>
namespace Libs::Graphics {
struct ShaderPixelInputInfo;
struct ShaderVertexInputInfo;
namespace ShaderRecompiler::IR {
struct CompiledShaderInfo;
} // namespace ShaderRecompiler::IR
// A pixel parameter the generated stages forward from vertex export input_location.
struct RectListParameter {
uint32_t input_location = 0;
uint32_t output_location = 0;
bool flat = false;
};
struct RectListShaders {
std::vector<uint32_t> control;
std::vector<uint32_t> evaluation;
};
RectListShaders BuildRectListShaders(const ShaderVertexInputInfo& vertex_info,
const ShaderPixelInputInfo* pixel_info);
// Every parameter a rectangle-list pixel program reads.
std::vector<RectListParameter> RectListParameters(
const ShaderPixelInputInfo& pixel_info, const ShaderRecompiler::IR::CompiledShaderInfo& program);
// Forwards the parameters the vertex stage exports, one per output location.
RectListShaders BuildRectListShaders(std::span<const RectListParameter> parameters,
uint32_t param_export_mask);
} // namespace Libs::Graphics
+3 -16
View File
@@ -11,7 +11,6 @@
#include "graphics/guest_gpu/hardwareContext.h"
#include "graphics/host_gpu/renderer/renderContext.h"
#include "graphics/shader/recompiler/ShaderRecompiler.h"
#include "graphics/shader/recompiler/Tessellation.h"
#include "graphics/shader/recompiler/frontend/decode/ShaderDecoder.h"
#include "graphics/shader/shaderCompiler.h"
#include "graphics/shader/shaderVertexMetadata.h"
@@ -53,8 +52,6 @@ struct ShaderMapEntry {
static std::unique_ptr<std::unordered_map<uint64_t, ShaderMapEntry>> g_shader_map;
static std::mutex g_shader_map_mutex;
static std::unordered_map<uint64_t, ShaderTessellationInputInfo> g_tess_layouts;
static std::mutex g_tess_layouts_mutex;
void ShaderInit() {
EXIT_IF(g_shader_map != nullptr);
@@ -493,7 +490,9 @@ void BuildStageStaticKey(const ShaderPixelInputInfo& info, std::vector<uint32_t>
key.push_back(static_cast<uint32_t>(info.object_primitive_id));
key.push_back(static_cast<uint32_t>(info.alpha_blend_source));
key.insert(key.end(), std::begin(info.target_output_mode), std::end(info.target_output_mode));
key.push_back(info.target_packed_unorm_mask | (uint32_t {info.target_round_mode} << 8u));
key.push_back(info.target_packed_unorm_mask | (uint32_t {info.target_round_mode} << 8u) |
(uint32_t {info.target_uint_mask} << 16u) |
(uint32_t {info.target_sint_mask} << 24u));
const auto mask = info.target_shader_mask;
key.push_back((mask | (mask >> 1u) | (mask >> 2u) | (mask >> 3u)) & 0x11111111u);
for (uint32_t base = 0; base < info.target_export_mapping.size(); base += 4u) {
@@ -662,18 +661,6 @@ PrepareTessellationPrograms(const HW::VertexShaderInfo& regs, const HW::Context&
};
EXIT_IF(tess.input_control_points == 0 || tess.input_control_points > 32 ||
tess.output_control_points == 0 || tess.output_control_points > 32);
// Reflection decodes both programs; the key also covers the VGT fields cached in tess.
const uint64_t key[] = {local_hash, control_hash,
uint64_t {sh.m_vgtLsHsConfig & 0xfff00u} << 32u | sh.m_vgtTfParam};
{
std::scoped_lock lock(g_tess_layouts_mutex);
auto [entry, inserted] = g_tess_layouts.try_emplace(XXH3_64bits(key, sizeof(key)), tess);
if (inserted) {
ShaderRecompiler::AnalyzeTessellationPrograms(params[0].code, params[1].code,
entry->second);
}
tess = entry->second;
}
for (auto& stage: input_info) {
stage.tess = tess;
}
+7 -9
View File
@@ -34,19 +34,14 @@ enum class ShaderType {
TessellationEvaluation
};
namespace ShaderRecompiler::IR {
struct CompiledShaderInfo;
} // namespace ShaderRecompiler::IR
struct ShaderProgram;
struct ShaderModule;
struct ShaderStageRuntime {
const ShaderRecompiler::IR::CompiledShaderInfo* program = nullptr;
const ShaderRecompiler::IR::ResourceSnapshot* resources = nullptr;
const ShaderProgram* handle = nullptr;
const ShaderModule* module = nullptr;
const ShaderRecompiler::IR::ResourceSnapshot* resources = nullptr;
[[nodiscard]] explicit operator bool() const {
return program != nullptr && resources != nullptr;
return module != nullptr && resources != nullptr;
}
};
@@ -186,6 +181,9 @@ struct ShaderPixelInputInfo {
uint32_t target_shader_mask = UINT32_MAX;
std::array<Prospero::ColorComponentMapping, 8> target_export_mapping = {};
uint8_t target_packed_unorm_mask = 0;
// Exports to UINT and SINT targets, which take integer outputs of the same signedness.
uint8_t target_uint_mask = 0;
uint8_t target_sint_mask = 0;
// CB round mode per converted export: zero adds half an LSB, one truncates.
uint8_t target_round_mode = 0;
uint32_t scratch_size_dwords = 0;
+1
View File
@@ -32,6 +32,7 @@ void BuildStageStaticKey(const ShaderComputeInputInfo& input_info, std::vector<u
ShaderParams PrepareProgram(const HW::VertexShaderInfo& regs, const HW::Context& context,
const HW::UserConfig& user_config, ShaderVertexInputInfo& input_info);
// Each stage's tess holds the VGT fields; the caller adds the layout reflected from the programs.
std::array<ShaderParams, 3>
PrepareTessellationPrograms(const HW::VertexShaderInfo& regs, const HW::Context& context,
std::array<ShaderVertexInputInfo, 3>& input_info);
@@ -141,6 +141,7 @@
<property name="value"><number>60</number></property>
</widget>
</item>
<!--
<item row="3" column="0">
<widget class="QLabel" name="label_shader_optimization">
<property name="text"><string>Shader optimization:</string></property>
@@ -151,6 +152,7 @@
<property name="toolTip"><string>Optimize shaders for code size or performance</string></property>
</widget>
</item>
-->
<item row="4" column="0">
<widget class="QLabel" name="label_gpu">
<property name="text"><string>GPU:</string></property>
@@ -191,6 +193,12 @@
<property name="text"><string>Enable HDR display support</string></property>
</widget>
</item>
<item row="10" column="1">
<widget class="QCheckBox" name="checkBox_shader_cache">
<property name="toolTip"><string>Save compiled shaders and pipelines per game and load them at startup.</string></property>
<property name="text"><string>Cache shaders</string></property>
</widget>
</item>
</layout>
</widget>
</item>
+21 -8
View File
@@ -4,26 +4,39 @@
#include <QDialog>
#include <QIcon>
#include <QString>
#include <QStringList>
#include <functional>
class Configuration;
class QLabel;
class QLineEdit;
class QListWidget;
class QPushButton;
class AdditionalContentDialog final: public QDialog {
public:
AdditionalContentDialog(const Configuration& game, const QString& folder,
QWidget* parent = nullptr);
// entries are the game's user add-ons, "LABEL" or "LABEL=DIR"; save receives every edit.
AdditionalContentDialog(const Configuration& game, const QString& folder, QStringList entries,
std::function<void(const QStringList&)> save,
QWidget* parent = nullptr);
private:
void Scan();
void Add();
void Remove();
QString m_title_id;
QString m_folder;
QIcon m_game_icon;
QLabel* m_status = nullptr;
QListWidget* m_list = nullptr;
QPushButton* m_open = nullptr;
QString m_title_id;
QString m_folder;
QStringList m_entries;
std::function<void(const QStringList&)> m_save;
QIcon m_game_icon;
QLabel* m_status = nullptr;
QListWidget* m_list = nullptr;
QPushButton* m_open = nullptr;
QPushButton* m_remove = nullptr;
QLineEdit* m_label_edit = nullptr;
QLineEdit* m_dir_edit = nullptr;
};
#endif // ADDITIONAL_CONTENT_DIALOG_H
+13 -10
View File
@@ -102,16 +102,17 @@ public:
Configuration() = default;
QString name;
QString title_id; /* Serial / title id from sce_sys/param.json */
QString gameVersion; /* appVersion / contentVersion from sce_sys/param.json */
QString firmwareVer; /* requiredSystemSoftwareVersion from sce_sys/param.json */
QString basedir; /* Game base directory */
QString game_path; /* Launcher-unique game path */
QString addons_dir; /* Global setting; not saved per game. */
bool custom_settings = false;
GameStatus game_status = GameStatus::Unknown;
QString game_comment;
QString name;
QString title_id; /* Serial / title id from sce_sys/param.json */
QString gameVersion; /* appVersion / contentVersion from sce_sys/param.json */
QString firmwareVer; /* requiredSystemSoftwareVersion from sce_sys/param.json */
QString basedir; /* Game base directory */
QString game_path; /* Launcher-unique game path */
QString addons_dir; /* Global setting; not saved per game. */
QStringList addcont; /* User add-ons ("LABEL" or "LABEL=DIR"), saved by title ID. */
bool custom_settings = false;
GameStatus game_status = GameStatus::Unknown;
QString game_comment;
// Controller preferences always come from the global configuration.
ControllerSettings controller;
@@ -130,6 +131,7 @@ public:
bool sync_raw_image_buffers = false;
bool tessellation_enabled = false;
bool occlusion_queries_enabled = false;
bool shader_cache_enabled = true;
bool trophy_enabled = true;
int trophy_duration_seconds = 6;
bool perf_overlay_enabled = false;
@@ -171,6 +173,7 @@ public:
sync_raw_image_buffers = other.sync_raw_image_buffers;
tessellation_enabled = other.tessellation_enabled;
occlusion_queries_enabled = other.occlusion_queries_enabled;
shader_cache_enabled = other.shader_cache_enabled;
trophy_enabled = other.trophy_enabled;
trophy_duration_seconds = other.trophy_duration_seconds;
perf_overlay_enabled = other.perf_overlay_enabled;
@@ -87,6 +87,7 @@ private:
QString m_runtime_directory;
QStringList m_game_dirs;
QString m_addons_dir;
QMap<QString, QStringList> m_addcont; // User add-ons by title ID.
Configuration m_global_info;
QMap<QString, Configuration*> m_custom_infos;
CompatibilityDatabase* m_compatibility = nullptr;
+75 -6
View File
@@ -7,7 +7,10 @@
#include <QDesktopServices>
#include <QDialogButtonBox>
#include <QDir>
#include <QFileDialog>
#include <QHBoxLayout>
#include <QLabel>
#include <QLineEdit>
#include <QListWidget>
#include <QPixmap>
#include <QPushButton>
@@ -16,8 +19,11 @@
#include <QVBoxLayout>
AdditionalContentDialog::AdditionalContentDialog(const Configuration& game, const QString& folder,
QWidget* parent)
: QDialog(parent), m_title_id(game.title_id.trimmed().toUpper()), m_folder(folder) {
QStringList entries,
std::function<void(const QStringList&)> save,
QWidget* parent)
: QDialog(parent), m_title_id(game.title_id.trimmed().toUpper()), m_folder(folder),
m_entries(std::move(entries)), m_save(std::move(save)) {
setAttribute(Qt::WA_DeleteOnClose);
setWindowTitle(tr("Additional Content - %1").arg(game.name));
resize(720, 480);
@@ -39,6 +45,28 @@ AdditionalContentDialog::AdditionalContentDialog(const Configuration& game, cons
m_list->setVerticalScrollMode(QAbstractItemView::ScrollPerPixel);
layout->addWidget(m_list, 1);
auto* add_layout = new QHBoxLayout;
m_label_edit = new QLineEdit(this);
m_label_edit->setPlaceholderText(tr("Label or content ID"));
m_dir_edit = new QLineEdit(this);
m_dir_edit->setPlaceholderText(tr("Add-on data folder (empty for a license only)"));
auto* browse = new QPushButton(tr("Browse..."), this);
auto* add = new QPushButton(tr("Add"), this);
add->setDefault(true);
add_layout->addWidget(m_label_edit, 1);
add_layout->addWidget(m_dir_edit, 2);
add_layout->addWidget(browse);
add_layout->addWidget(add);
layout->addLayout(add_layout);
connect(browse, &QPushButton::clicked, this, [this]() {
const auto dir =
QFileDialog::getExistingDirectory(this, tr("Add-on data folder"), m_dir_edit->text());
if (!dir.isEmpty()) {
m_dir_edit->setText(QDir::toNativeSeparators(dir));
}
});
connect(add, &QPushButton::clicked, this, &AdditionalContentDialog::Add);
m_status = new QLabel(this);
m_status->setTextFormat(Qt::PlainText);
m_status->setWordWrap(true);
@@ -47,10 +75,15 @@ AdditionalContentDialog::AdditionalContentDialog(const Configuration& game, cons
auto* buttons = new QDialogButtonBox(QDialogButtonBox::Close, this);
auto* refresh = buttons->addButton(tr("Refresh"), QDialogButtonBox::ActionRole);
m_open = buttons->addButton(tr("Open folder"), QDialogButtonBox::ActionRole);
m_remove = buttons->addButton(tr("Remove"), QDialogButtonBox::ActionRole);
layout->addWidget(buttons);
connect(refresh, &QPushButton::clicked, this, &AdditionalContentDialog::Scan);
connect(m_open, &QPushButton::clicked, this,
[this]() { QDesktopServices::openUrl(QUrl::fromLocalFile(m_folder)); });
connect(m_remove, &QPushButton::clicked, this, &AdditionalContentDialog::Remove);
connect(m_list, &QListWidget::currentItemChanged, this, [this](const QListWidgetItem* item) {
m_remove->setEnabled(item != nullptr && item->data(Qt::UserRole).isValid());
});
connect(buttons, &QDialogButtonBox::rejected, this, &QDialog::close);
Scan();
@@ -73,18 +106,54 @@ void AdditionalContentDialog::Scan() {
tr("%1\nInstalled").arg(title), m_list);
item->setToolTip(QString::fromStdString(entry.content_id));
}
for (const auto& entry: m_entries) {
const auto label = entry.section('=', 0, 0);
const auto dir = entry.section('=', 1);
auto text = tr("%1\nLicense only").arg(label);
if (!dir.isEmpty()) {
text = (QDir(dir).exists() ? tr("%1\nData: %2") : tr("%1\nData folder not found: %2"))
.arg(label, QDir::toNativeSeparators(dir));
}
auto* item = new QListWidgetItem(m_game_icon, text, m_list);
item->setData(Qt::UserRole, entry);
}
m_remove->setEnabled(false);
if (m_folder.isEmpty()) {
if (m_list->count() != 0) {
m_status->setText(tr("%n add-on(s)", nullptr, m_list->count()));
} else if (m_folder.isEmpty()) {
m_status->setText(tr("No add-ons folder is set. Open Global settings and set the "
"Add-ons folder to the location of your extracted add-on packages."));
} else if (!folder_exists) {
m_status->setText(tr("The add-ons folder does not exist:\n%1\nCheck the path in Global "
"settings.")
.arg(QDir::toNativeSeparators(m_folder)));
} else if (m_list->count() == 0) {
} else {
m_status->setText(
tr("No add-ons found for this game in:\n%1").arg(QDir::toNativeSeparators(m_folder)));
} else {
m_status->setText(tr("%n installed add-on(s)", nullptr, m_list->count()));
}
}
void AdditionalContentDialog::Add() {
// A content ID ends with its entitlement label.
const auto label = m_label_edit->text().trimmed().section('-', -1);
const auto dir = m_dir_edit->text().trimmed();
const auto entry = dir.isEmpty() ? label : label + '=' + QDir(dir).absolutePath();
if (!Common::ParseAddContent(entry.toStdString())) {
m_status->setText(tr("Enter a 16-character entitlement label (A-Z, 0-9) and an existing "
"data folder, or no folder for a license only."));
return;
}
m_entries.removeIf([&](const QString& e) { return e.section('=', 0, 0) == label; });
m_entries.append(entry);
m_save(m_entries);
m_label_edit->clear();
m_dir_edit->clear();
Scan();
}
void AdditionalContentDialog::Remove() {
m_entries.removeOne(m_list->currentItem()->data(Qt::UserRole).toString());
m_save(m_entries);
Scan();
}
+3 -1
View File
@@ -17,6 +17,7 @@ QVariantMap Configuration::GameSettings() const {
KYTY_CFG_SET(sync_raw_image_buffers);
KYTY_CFG_SET(tessellation_enabled);
KYTY_CFG_SET(occlusion_queries_enabled);
KYTY_CFG_SET(shader_cache_enabled);
KYTY_CFG_SET(trophy_enabled);
KYTY_CFG_SET(trophy_duration_seconds);
KYTY_CFG_SET(perf_overlay_enabled);
@@ -68,7 +69,8 @@ void Configuration::ReadGameSettingsValues(const Settings& s) {
KYTY_CFG_GET(sync_raw_image_buffers);
KYTY_CFG_GET(tessellation_enabled);
KYTY_CFG_GET(occlusion_queries_enabled);
trophy_enabled = s.value("trophy_enabled", trophy_enabled).toBool();
shader_cache_enabled = s.value("shader_cache_enabled", shader_cache_enabled).toBool();
trophy_enabled = s.value("trophy_enabled", trophy_enabled).toBool();
KYTY_CFG_GET(trophy_duration_seconds);
KYTY_CFG_GET(perf_overlay_enabled);
KYTY_CFG_GET(skip_notice_screen);
+5 -3
View File
@@ -290,6 +290,7 @@ void ConfigurationEditDialog::Init(const Configuration& info) {
m_ui->checkBox_sync_raw_image_buffers->setChecked(info.sync_raw_image_buffers);
m_ui->checkBox_tessellation->setChecked(info.tessellation_enabled);
m_ui->checkBox_occlusion_queries->setChecked(info.occlusion_queries_enabled);
m_ui->checkBox_shader_cache->setChecked(info.shader_cache_enabled);
m_ui->checkBox_skip_notice_screen->setChecked(info.skip_notice_screen);
m_ui->checkBox_network_connected->setChecked(info.network_connected);
m_ui->checkBox_trophy_notifications->setChecked(info.trophy_enabled);
@@ -314,7 +315,7 @@ void ConfigurationEditDialog::Init(const Configuration& info) {
#else
m_ui->checkBox_red_zone_protection->setVisible(false);
#endif
ListInit(m_ui->comboBox_shader_optimization_type, info.shader_optimization_type);
// ListInit(m_ui->comboBox_shader_optimization_type, info.shader_optimization_type);
ListInit(m_ui->comboBox_shader_log_direction, info.shader_log_direction);
m_ui->lineEdit_shader_log_folder->setText(info.shader_log_folder);
m_ui->lineEdit_shader_log_folder->setEnabled(info.shader_log_direction ==
@@ -485,6 +486,7 @@ static void UpdateInfo(Configuration& info, Ui::ConfigurationEditDialog& ui, boo
info.sync_raw_image_buffers = ui.checkBox_sync_raw_image_buffers->isChecked();
info.tessellation_enabled = ui.checkBox_tessellation->isChecked();
info.occlusion_queries_enabled = ui.checkBox_occlusion_queries->isChecked();
info.shader_cache_enabled = ui.checkBox_shader_cache->isChecked();
info.skip_notice_screen = ui.checkBox_skip_notice_screen->isChecked();
info.network_connected = ui.checkBox_network_connected->isChecked();
info.trophy_enabled = ui.checkBox_trophy_notifications->isChecked();
@@ -499,8 +501,8 @@ static void UpdateInfo(Configuration& info, Ui::ConfigurationEditDialog& ui, boo
#if defined(_WIN32)
info.red_zone_protection_enabled = ui.checkBox_red_zone_protection->isChecked();
#endif
info.shader_optimization_type = TextToEnum<Configuration::ShaderOptimizationType>(
ui.comboBox_shader_optimization_type->currentText());
// info.shader_optimization_type = TextToEnum<Configuration::ShaderOptimizationType>(
// ui.comboBox_shader_optimization_type->currentText());
info.shader_log_direction = TextToEnum<Configuration::LogDirection>(
ui.comboBox_shader_log_direction->currentText());
info.shader_log_folder = ui.lineEdit_shader_log_folder->text();
+29 -2
View File
@@ -79,6 +79,7 @@ constexpr char CONF_LAUNCHER[] = "Launcher";
constexpr char CONF_GAME_DIR[] = "game_dir";
constexpr char CONF_GAME_DIRS[] = "game_dirs";
constexpr char CONF_ADDONS_DIR[] = "addons_dir";
constexpr char CONF_ADDCONT[] = "AdditionalContent";
constexpr char CONF_GLOBAL[] = "GlobalConfiguration";
constexpr char SAVE_DATA_DIR[] = "_SaveData";
@@ -348,6 +349,13 @@ void ConfigurationListWidget::WriteSettings() {
s->remove(CONF_GAME_DIR);
s->endGroup();
s->remove(CONF_ADDCONT);
s->beginGroup(CONF_ADDCONT);
for (auto it = m_addcont.constBegin(); it != m_addcont.constEnd(); ++it) {
s->setValue(it.key(), it.value());
}
s->endGroup();
s->remove(CONF_GLOBAL);
s->beginGroup(CONF_GLOBAL);
m_global_info.WriteSettings(s.get());
@@ -395,6 +403,13 @@ void ConfigurationListWidget::ReadSettings() {
m_addons_dir = NormalizeGameDirectory(s->value(CONF_ADDONS_DIR).toString());
s->endGroup();
m_addcont.clear();
s->beginGroup(CONF_ADDCONT);
for (const auto& title_id: s->childKeys()) {
m_addcont.insert(title_id, SettingsStringList(s->value(title_id)));
}
s->endGroup();
s->beginGroup(CONF_GLOBAL);
if (!s->childKeys().isEmpty()) {
m_global_info.ReadSettings(s.get());
@@ -458,6 +473,7 @@ ConfigurationListWidget::CreateConfiguration(const ConfigurationItem& item) cons
}
info->host_input_mapping = m_global_info.host_input_mapping;
info->addons_dir = m_addons_dir;
info->addcont = m_addcont.value(item.GetInfo().title_id.trimmed().toUpper());
return info;
}
@@ -1273,11 +1289,22 @@ void ConfigurationListWidget::ShowContextMenu(QTreeWidgetItem* witem) {
connect(action_addcont, &QAction::triggered, this,
[this, item = QPointer<ConfigurationItem>(item)]() {
if (item != nullptr) {
auto* dialog = new AdditionalContentDialog(item->GetInfo(), m_addons_dir, this);
const auto title_id = item->GetInfo().title_id.trimmed().toUpper();
auto* dialog = new AdditionalContentDialog(
item->GetInfo(), m_addons_dir, m_addcont.value(title_id),
[this, title_id](const QStringList& entries) {
if (entries.isEmpty()) {
m_addcont.remove(title_id);
} else {
m_addcont.insert(title_id, entries);
}
WriteSettings();
},
this);
dialog->show();
}
});
action_addcont->setEnabled(item != nullptr);
action_addcont->setEnabled(item != nullptr && !item->GetInfo().title_id.trimmed().isEmpty());
QAction* action_compress =
menu.addAction(tr("Compress to ZAR..."), this, SLOT(compress_game()));
QAction* action_remove_save_data =
+4
View File
@@ -276,6 +276,7 @@ static QStringList CreateEmulatorArgs(const Configuration& info) {
AddFlag(args, info.network_connected, "--network-connected");
AddFlag(args, info.tessellation_enabled, "--tessellation");
AddFlag(args, info.occlusion_queries_enabled, "--occlusion-queries");
AddFlag(args, info.shader_cache_enabled, "--shader-cache");
args << "--vblank-frequency" << QString::number(info.vblank_frequency);
args << "--console-language" << QString::number(info.console_language);
AddFlag(args, info.vulkan_validation_enabled, "--vulkan-validation");
@@ -305,6 +306,9 @@ static QStringList CreateEmulatorArgs(const Configuration& info) {
if (!info.addons_dir.isEmpty()) {
args << "--addcont-dir" << info.addons_dir;
}
for (const auto& entry: info.addcont) {
args << "--addcont" << entry;
}
const auto patch_plan = Cheats::PlanPath(info.title_id);
if (QFileInfo::exists(patch_plan)) {
+7
View File
@@ -96,6 +96,13 @@ int KYTY_SYSV_ABI AcmBatchJobNotification(AcmBatchInfo* batch_info) {
return OK;
}
int KYTY_SYSV_ABI AcmBatchJobPriority(AcmBatchInfo* batch_info, uint32_t priority) {
PRINT_NAME();
(void)batch_info;
(void)priority;
return OK;
}
int KYTY_SYSV_ABI AcmConvReverbSharedInput(AcmBatchInfo* batch_info, uint32_t block_count, void* in,
uint32_t count, const void* const ir[],
const float* gain, void* const out[]) {
+1
View File
@@ -24,6 +24,7 @@ int KYTY_SYSV_ABI AcmBatchStartBuffers(AcmContextId context, uint32_t batch_info
AcmBatchError* batch_error, AcmBatchId* batch);
int KYTY_SYSV_ABI AcmBatchWait(AcmContextId context, AcmBatchId batch, uint32_t timeout);
int KYTY_SYSV_ABI AcmBatchJobNotification(AcmBatchInfo* batch_info);
int KYTY_SYSV_ABI AcmBatchJobPriority(AcmBatchInfo* batch_info, uint32_t priority);
int KYTY_SYSV_ABI AcmConvReverbSharedInput(AcmBatchInfo* batch_info, uint32_t block_count, void* in,
uint32_t count, const void* const ir[],
const float* gain, void* const out[]);
+2
View File
@@ -658,6 +658,8 @@ constexpr int SAVE_DATA_ERROR_BUSY_FOR_SAVING = -2137063402
constexpr int SAVE_DATA_ERROR_LIMITATION_OVER = -2137063401; /* 0x809F0017 */
constexpr int SAVE_DATA_ERROR_EVENT_BUSY = -2137063400; /* 0x809F0018 */
constexpr int SAVE_DATA_ERROR_PARAMSFO_TRANSFER_TITLE_ID_NOT_FOUND = -2137063399; /* 0x809F0019 */
constexpr int SAVE_DATA_ERROR_NO_NEED_CONVERT = -2137063394; /* 0x809F001E */
constexpr int SAVE_DATA_ERROR_BLOCKS_FEWER_THAN_SRC = -2137063392; /* 0x809F0020 */
} // namespace SaveData
+1
View File
@@ -579,6 +579,7 @@ LIB_DEFINE(InitAudio_1_Acm) {
LIB_FUNC("8fe55ktlNVo", Acm::AcmBatchStartBuffers);
LIB_FUNC("RLN3gRlXJLE", Acm::AcmBatchWait);
LIB_FUNC("r7z5YQFZo+U", Acm::AcmBatchJobNotification);
LIB_FUNC("GTfJ64qFCPw", Acm::AcmBatchJobPriority);
LIB_FUNC("u70oWo92SYQ", Acm::AcmConvReverbSharedInput);
LIB_FUNC("9nLbWmRDpa8", Acm::AcmConvReverbSharedIr);
LIB_FUNC("KovqaFbmtsM", Acm::AcmFft);
+3 -3
View File
@@ -771,10 +771,10 @@ LIB_DEFINE(InitLibcInternal_1) {
LIB_FUNC("Q3VBxCXhUHs", LibcInternal::memcpy);
LIB_FUNC("+P6FRGH4LfA", LibcInternal::memmove);
LIB_FUNC("DfivPArhucg", LibcInternal::memcmp);
LIB_FUNC("aesyjrHVWy4", LibcInternal::strcmp);
LIB_FUNC("Ovb2dSJOAuE", LibcInternal::strncmp);
LIB_FUNC("Ovb2dSJOAuE", LibcInternal::strcmp);
LIB_FUNC("aesyjrHVWy4", LibcInternal::strncmp);
LIB_FUNC("j4ViWNHEgww", LibcInternal::strlen);
LIB_FUNC("5Xa2ACNECdo", LibcInternal::strcpy);
LIB_FUNC("kiZSXIWd9vg", LibcInternal::strcpy);
LIB_FUNC("6sJWiWSRuqk", LibcInternal::strncpy);
LIB_FUNC("Ls4tzzhimqQ", LibcInternal::strcat);
LIB_FUNC("ob5xAW4ln-0", LibcInternal::strchr);
+2 -16
View File
@@ -1370,22 +1370,8 @@ static void KYTY_SYSV_ABI KernelRtldSetApplicationHeapAPI(void* api[]) {
[[maybe_unused]] auto* heap_posix_memalign = api[6];
}
static int64_t KYTY_SYSV_ABI write(int d, const char* str, int64_t size) {
// PRINT_NAME();
EXIT_NOT_IMPLEMENTED(d < 0);
if (Network::Net::IsSocket(d)) {
return Network::Net::Send(d, str, static_cast<uint64_t>(size), 0);
}
if (d > 2) {
return POSIX_N_CALL(FileSystem::KernelWrite(d, str, size));
}
int size_int = static_cast<int>(size);
return size_int;
static int64_t KYTY_SYSV_ABI write(int d, const void* buf, size_t nbytes) {
return POSIX_N_CALL(FileSystem::KernelWrite(d, buf, nbytes));
}
static int KYTY_SYSV_ABI open(const char* path, int flags, int mode) {
+4
View File
@@ -21,6 +21,9 @@ namespace Libs {
namespace LibNpEntitlementAccess {
void InitNet_1_NpEntitlementAccess(Loader::SymbolDatabase* s);
}
namespace LibNpEntitlementAccessPft {
void InitNet_1_NpEntitlementAccessPft(Loader::SymbolDatabase* s);
}
namespace LibNpTrophy2 {
void InitNet_1_NpTrophy2(Loader::SymbolDatabase* s);
}
@@ -2846,6 +2849,7 @@ LIB_DEFINE(InitNet_1) {
LibNpManager::InitNet_1_NpManager(s);
LibNpSessionSignaling::InitNet_1_NpSessionSignaling(s);
LibNpEntitlementAccess::InitNet_1_NpEntitlementAccess(s);
LibNpEntitlementAccessPft::InitNet_1_NpEntitlementAccessPft(s);
LibNpAuth::InitNet_1_NpAuth(s);
LibNpTrophy2::InitNet_1_NpTrophy2(s);
LibNpUniversalDataSystem::InitNet_1_NpUniversalDataSystem(s);
+25 -2
View File
@@ -172,8 +172,9 @@ MakeAddcontInfo(const Loader::AddContentEntry& entry) {
NpEntitlementAccessAddcontEntitlementInfo info {};
std::memcpy(info.entitlement_label.data, entry.entitlement_label.data(),
entry.entitlement_label.size());
info.package_type = 2; // Additional content with extra data.
info.download_status = 4; // Installed.
const bool extra_data = !entry.mount_dir.empty();
info.package_type = extra_data ? 2 : 3; // PSAC (with extra data) or PSAL (license only).
info.download_status = extra_data ? 4 : 0; // INSTALLED or NO_EXTRA_DATA.
return info;
}
@@ -512,4 +513,26 @@ LIB_DEFINE(InitNet_1_NpEntitlementAccess) {
} // namespace LibNpEntitlementAccess
namespace LibNpEntitlementAccessPft {
LIB_VERSION("NpEntitlementAccessPft", 1, "NpEntitlementAccess", 1, 1);
// Unknown function; return state 2 for the FC27 investigation.
static int KYTY_SYSV_ABI NpEntitlementAccessPftUnknowneDXKe9FndlE(uint32_t* state) {
PRINT_NAME();
if (state == nullptr) {
return LibNpEntitlementAccess::NP_ENTITLEMENT_ACCESS_ERROR_PARAMETER;
}
*state = 2;
return 0;
}
LIB_DEFINE(InitNet_1_NpEntitlementAccessPft) {
LIB_FUNC("eDXKe9FndlE", NpEntitlementAccessPftUnknowneDXKe9FndlE);
}
} // namespace LibNpEntitlementAccessPft
} // namespace Libs
+72 -9
View File
@@ -231,6 +231,15 @@ struct SaveDataDelete {
int32_t pad2;
};
struct SaveDataConvert {
int32_t user_id;
int32_t pad;
const SceSaveDataDirName* src_dir_name;
const SceSaveDataDirName* dst_dir_name;
uint64_t dst_blocks;
uint8_t reserved[24];
};
struct SaveDataEvent {
uint32_t type;
int32_t error_code;
@@ -309,14 +318,20 @@ static std::filesystem::path save_user_directory(std::string_view title_id, int3
return directory / title_id;
}
static std::filesystem::path save_directory(std::string_view title_id, std::string_view dir_name,
int32_t user_id) {
static bool is_memory_dir_name(std::string_view dir_name) {
for (uint32_t slot = 0; slot < 4; slot++) {
if (dir_name == memory_dir_name(slot)) {
return std::filesystem::path(SAVE_DATA_DIR) / title_id / dir_name /
std::to_string(user_id);
return true;
}
}
return false;
}
static std::filesystem::path save_directory(std::string_view title_id, std::string_view dir_name,
int32_t user_id) {
if (is_memory_dir_name(dir_name)) {
return std::filesystem::path(SAVE_DATA_DIR) / title_id / dir_name / std::to_string(user_id);
}
return save_user_directory(title_id, user_id) / dir_name;
}
@@ -1150,13 +1165,9 @@ int KYTY_SYSV_ABI SaveDataSyncSaveDataMemory(const SaveDataMemorySync* sync_para
return OK;
}
int KYTY_SYSV_ABI SaveDataGetEventResult(const void* event_param, SaveDataEvent* event) {
int KYTY_SYSV_ABI SaveDataGetEventResult(const void* /*event_param*/, SaveDataEvent* event) {
PRINT_NAME();
LOGF("\t event_param = 0x%016" PRIx64 "\n"
"\t event = 0x%016" PRIx64 "\n",
reinterpret_cast<uint64_t>(event_param), reinterpret_cast<uint64_t>(event));
if (event == nullptr) {
return SAVE_DATA_ERROR_PARAMETER;
}
@@ -1180,6 +1191,56 @@ int KYTY_SYSV_ABI SaveDataGetEventResult(const void* event_param, SaveDataEvent*
return OK;
}
int KYTY_SYSV_ABI SaveDataConvert(const SaveDataConvert* convert) {
PRINT_NAME();
const auto valid_name = [](const SceSaveDataDirName* name) {
return valid_path_component(name->data) && !is_memory_dir_name(name->data);
};
if (convert == nullptr || convert->src_dir_name == nullptr ||
!valid_name(convert->src_dir_name) ||
(convert->dst_dir_name != nullptr && !valid_name(convert->dst_dir_name)) ||
convert->dst_blocks < SAVE_DATA_BLOCKS_MIN || convert->dst_blocks > SAVE_DATA_BLOCKS_MAX) {
return SAVE_DATA_ERROR_PARAMETER;
}
if (convert->user_id < 0) {
return SAVE_DATA_ERROR_INVALID_LOGIN_USER;
}
LOGF("\t user_id = %" PRId32 "\n"
"\t src_dir_name = %s\n"
"\t dst_dir_name = %s\n"
"\t dst_blocks = %" PRIu64 "\n",
convert->user_id, convert->src_dir_name->data,
convert->dst_dir_name != nullptr ? convert->dst_dir_name->data : "<none>",
convert->dst_blocks);
Common::LockGuard lock(g_mount_mutex);
const auto source =
save_directory(get_title_id(), convert->src_dir_name->data, convert->user_id);
if (!Common::File::IsDirectoryExisting(source)) {
return SAVE_DATA_ERROR_NOT_FOUND;
}
if (g_mount_slots.FindAvailable(source) == SaveDataMountSlots::BUSY) {
return SAVE_DATA_ERROR_BUSY;
}
if (convert->dst_dir_name != nullptr &&
Common::File::IsDirectoryExisting(
save_directory(get_title_id(), convert->dst_dir_name->data, convert->user_id))) {
return SAVE_DATA_ERROR_EXISTS;
}
uint64_t blocks = 0;
if (read_save_blocks(source, &blocks) == OK) {
if (convert->dst_blocks == blocks) {
return SAVE_DATA_ERROR_NO_NEED_CONVERT;
}
if (convert->dst_blocks < blocks) {
return SAVE_DATA_ERROR_BLOCKS_FEWER_THAN_SRC;
}
}
EXIT("save data conversion of %s is not implemented\n", convert->src_dir_name->data);
}
int KYTY_SYSV_ABI SaveDataBackup(const SaveDataBackup* backup) {
PRINT_NAME();
@@ -1313,6 +1374,7 @@ LIB_DEFINE(InitSaveDataNative_1) {
LIB_FUNC("cduy9v4YmT4", ::Libs::SaveData::SaveDataSetSaveDataMemory2);
LIB_FUNC("wiT9jeC7xPw", ::Libs::SaveData::SaveDataSyncSaveDataMemory);
LIB_FUNC("j8xKtiFj0SY", ::Libs::SaveData::SaveDataGetEventResult);
LIB_FUNC("2mfSRGdshtk", ::Libs::SaveData::SaveDataConvert);
LIB_FUNC("WAzWTZm1H+I", ::Libs::SaveData::SaveDataTransferringMount);
LIB_FUNC("RjMlsR8EXrw", ::Libs::SaveData::SaveDataTransferringMount);
LIB_FUNC("z1JA8-iJt3k", ::Libs::SaveData::SaveDataBackup);
@@ -1343,6 +1405,7 @@ LIB_DEFINE(InitSaveData_1) {
LIB_FUNC("cduy9v4YmT4", SaveData::SaveDataSetSaveDataMemory2);
LIB_FUNC("wiT9jeC7xPw", SaveData::SaveDataSyncSaveDataMemory);
LIB_FUNC("j8xKtiFj0SY", SaveData::SaveDataGetEventResult);
LIB_FUNC("2mfSRGdshtk", SaveData::SaveDataConvert);
LIB_FUNC("WAzWTZm1H+I", SaveData::SaveDataTransferringMount);
LIB_FUNC("RjMlsR8EXrw", SaveData::SaveDataTransferringMount);
LIB_FUNC("z1JA8-iJt3k", SaveData::SaveDataBackup);
+17 -6
View File
@@ -10,10 +10,24 @@ namespace Loader {
static std::vector<AddContentEntry> g_add_content;
void AddContentLoad(const std::filesystem::path& dir, const std::string& title_id) {
static auto FindLabel(std::string_view label) {
return std::find_if(g_add_content.begin(), g_add_content.end(),
[&](const auto& entry) { return entry.entitlement_label == label; });
}
void AddContentLoad(const std::filesystem::path& dir, const std::vector<AddContentEntry>& user,
const std::string& title_id) {
g_add_content = Common::FindAddContent(dir, title_id);
for (const auto& entry: user) {
const auto it = FindLabel(entry.entitlement_label);
if (it == g_add_content.end()) {
g_add_content.push_back(entry);
} else {
*it = entry;
}
}
for (const auto& entry: g_add_content) {
LOGF("AddContent: registered %s -> %s\n", entry.content_id.c_str(),
LOGF("AddContent: registered %s -> %s\n", entry.entitlement_label.c_str(),
Common::PathToString(entry.mount_dir).c_str());
}
}
@@ -26,10 +40,7 @@ const AddContentEntry* AddContentFind(const char* label, size_t label_size) {
if (label == nullptr || label_size == 0) {
return nullptr;
}
const std::string_view value(label, std::find(label, label + label_size, '\0') - label);
const auto it =
std::find_if(g_add_content.begin(), g_add_content.end(),
[&](const auto& entry) { return entry.entitlement_label == value; });
const auto it = FindLabel({label, std::find(label, label + label_size, '\0')});
return it == g_add_content.end() ? nullptr : &*it;
}
+3 -1
View File
@@ -9,7 +9,9 @@ namespace Loader {
using AddContentEntry = Common::AddContentEntry;
void AddContentLoad(const std::filesystem::path& dir, const std::string& title_id);
// User entries replace detected packages with the same label.
void AddContentLoad(const std::filesystem::path& dir, const std::vector<AddContentEntry>& user,
const std::string& title_id);
const std::vector<AddContentEntry>& AddContentGetAll();
const AddContentEntry* AddContentFind(const char* label, size_t label_size);
+13
View File
@@ -48,6 +48,9 @@ static void PrintUsage() {
::printf(" --game <dir|elf|zar> Game directory, ELF, or ZArchive to load.\n");
::printf(" --game-patch <json> ETAHen cheat file.\n");
::printf(" --addcont-dir <dir> Add-ons (DLC) folder (default: none).\n");
::printf(
" --addcont <label>[=<dir>] Add-on (DLC) by entitlement label with its data in\n"
" <dir>, or license only without <dir>. Repeatable.\n");
::printf(" --screen-width <num> Window width. Default: 1280.\n");
::printf(" --screen-height <num> Window height. Default: 720.\n");
::printf(
@@ -77,6 +80,7 @@ static void PrintUsage() {
::printf(" --gpu-assisted-validation Bounds-check shader accesses on the GPU.\n"
" Implies --vulkan-validation; very slow.\n");
::printf(" --shader-validation Enable shader validation.\n");
::printf(" --shader-cache Keep compiled shaders and pipelines between boots.\n");
::printf(" --tessellation Draw tessellation patches; skipped by default.\n");
::printf(" --occlusion-queries Count occlusion query samples on the GPU.\n");
::printf(" --shader-optimization-type <value> None, Size, or Performance.\n");
@@ -202,6 +206,7 @@ static constexpr Flag FLAGS[] = {
{"--vulkan-validation", &ConfigOptions::vulkan_validation_enabled},
{"--gpu-assisted-validation", &ConfigOptions::gpu_assisted_validation_enabled},
{"--shader-validation", &ConfigOptions::shader_validation_enabled},
{"--shader-cache", &ConfigOptions::shader_cache_enabled},
{"--tessellation", &ConfigOptions::tessellation_enabled},
{"--occlusion-queries", &ConfigOptions::occlusion_queries_enabled},
{"--command-buffer-dump", &ConfigOptions::command_buffer_dump_enabled},
@@ -303,6 +308,14 @@ static bool ParseArgs(int argc, char* argv[], RunOptions& options, bool& show_he
return false;
}
options.addcont_dir = Common::PathFromUtf8(Common::FixFilenameSlash(value));
} else if (arg == "--addcont") {
auto entry = Common::ParseAddContent(value);
if (!entry) {
::printf("--addcont needs <16-character A-Z/0-9 label>[=<existing folder>]: %s\n",
value.c_str());
return false;
}
options.addcont.push_back(std::move(*entry));
} else if (arg == "--screen-width") {
if (!ParseUint32(value, options.config.screen_width) ||
options.config.screen_width == 0) {
+35 -3
View File
@@ -506,9 +506,9 @@ void TestAddContent(const std::filesystem::path& root) {
write(folder / "duplicate" / "sce_sys" / "param.json", param("PPSA11111", label(1)));
write(folder / label(0) / "payload.txt", "DLC payload");
Loader::AddContentLoad(folder, "");
Loader::AddContentLoad(folder, {}, "");
Check(Loader::AddContentGetAll().empty(), "unknown title never exposes unrelated add-ons");
Loader::AddContentLoad(folder, "PPSA11111");
Loader::AddContentLoad(folder, {}, "PPSA11111");
Check(Loader::AddContentGetAll().size() == 65,
"scan filters title before deduplication and ignores invalid/non-package metadata");
@@ -603,7 +603,39 @@ void TestAddContent(const std::filesystem::path& root) {
for (size_t i = 1; i < mounts.size(); ++i) {
Check(unmount(&mounts[i]) == OK, "unmount fixture");
}
Loader::AddContentLoad(folder / "missing", "PPSA11111");
const auto user_dir = root / "user-add-on";
write(user_dir / "payload.txt", "user payload");
const auto user_arg = label(0) + "=" + Common::PathToString(user_dir);
Check(!Common::ParseAddContent("SHORT") && !Common::ParseAddContent("lowercase0000000") &&
!Common::ParseAddContent("LICENSE0000000001") &&
!Common::ParseAddContent(user_arg + "/missing"),
"user add-on needs a 16-character A-Z/0-9 label and an existing folder");
const auto licensed = Common::ParseAddContent("LICENSE000000001");
const auto replaced = Common::ParseAddContent(user_arg);
const auto duplicate =
Common::ParseAddContent("LICENSE000000001=" + Common::PathToString(user_dir));
Check(licensed && replaced && duplicate, "user add-ons parse with and without extra data");
Loader::AddContentLoad(folder, {*duplicate, *licensed, *replaced}, "PPSA11111");
Check(list(0, nullptr, 0, &count) == OK && count == 69,
"user add-ons replace detected labels and append new ones");
Label user {};
std::memcpy(user.data, "LICENSE000000001", 17);
Check(get(0, &user, &single) == OK && single.package_type == 3 && single.download_status == 0 &&
mount(0, &user, &point) == static_cast<int>(0x80d90005u),
"last entry of a label wins and a license-only add-on has no extra data to mount");
Check(mount(0, &info[3].label, &point) == OK, "user add-on mounts its extra data");
const std::string user_file = std::string(point.data) + "/payload.txt";
const int user_fd = FileSystem::KernelOpen(user_file.c_str(), 0, 0);
std::array<char, 12> user_payload {};
Check(user_fd >= 3 &&
FileSystem::KernelRead(user_fd, user_payload.data(), user_payload.size()) ==
user_payload.size() &&
std::string_view(user_payload.data(), user_payload.size()) == "user payload" &&
FileSystem::KernelClose(user_fd) == OK && unmount(&point) == OK,
"user add-on replaces the detected folder of its label");
Loader::AddContentLoad(folder / "missing", {}, "PPSA11111");
Check(list(0, info.data(), 4, &count) == OK && count == 3,
"missing add-ons folder retains GTA V entitlements");
for (size_t i = 0; i < existing.size(); ++i) {
+81 -21
View File
@@ -1,3 +1,4 @@
#include "graphics/shader/recompiler/ir/Serialize.h"
#include "graphics/shader/recompiler/ir/ShaderIR.h"
#include "graphics/shader/recompiler/ir/passes/ResourceMaterialization.h"
#include "SrtWalkerTestMemory.h"
@@ -17,6 +18,22 @@ void Check(bool value, const char *text) {
}
}
// Every extracted plan below is the one a restarted game loads from the shader cache: exactly
// its bytes load, and the loaded plan saves back to them.
Libs::Graphics::ShaderRecompiler::IR::ResourcePlan
Extract(const Libs::Graphics::ShaderRecompiler::IR::Program &program) {
using namespace Libs::Graphics::ShaderRecompiler::IR;
const auto bytes = SaveResourcePlan(ExtractResourcePlan(program));
auto extended = bytes;
extended.push_back(0u);
ResourcePlan loaded, truncated, padded;
Check(LoadResourcePlan(bytes, loaded) && SaveResourcePlan(loaded) == bytes &&
!LoadResourcePlan(std::span(bytes).first(bytes.size() - 1u), truncated) &&
!LoadResourcePlan(extended, padded),
"resource plan did not round-trip through its exact cache bytes");
return loaded;
}
bool RejectSpecializationRead(void *userdata, uint64_t, std::span<uint32_t>) {
++*static_cast<uint32_t *>(userdata);
return false;
@@ -92,7 +109,7 @@ Libs::Graphics::ShaderRecompiler::IR::ResourcePlan UnbasedFlatPlan() {
program.resource_tracking_complete = true;
AddValueBlock(program);
program.info.uses_dma = true;
return ExtractResourcePlan(program);
return Extract(program);
}
Libs::Graphics::ShaderRecompiler::IR::ResourcePlan UserDataBufferPlan() {
@@ -113,7 +130,7 @@ Libs::Graphics::ShaderRecompiler::IR::ResourcePlan UserDataBufferPlan() {
source.dword_count = 4;
program.descriptor_sources.push_back(source);
program.info.buffers.push_back({.source = 0});
return ExtractResourcePlan(program);
return Extract(program);
}
Libs::Graphics::ShaderRecompiler::IR::Program MixedSamplerProgram() {
@@ -171,7 +188,7 @@ Libs::Graphics::ShaderRecompiler::IR::Program MixedSamplerProgram() {
void TestWalkerLoadsBypassSpecializationReader() {
using namespace Libs::Graphics::ShaderRecompiler::IR;
const uint32_t dword = 0x12345678;
auto plan = ExtractResourcePlan(SrtProgram(reinterpret_cast<uint64_t>(&dword)));
auto plan = Extract(SrtProgram(reinterpret_cast<uint64_t>(&dword)));
uint32_t specialization_reads = 0;
const SrtRuntime runtime{.userdata = &specialization_reads,
.read_specialization_memory =
@@ -223,7 +240,7 @@ void TestNativeNestedSrtRefresh() {
leaf->Arg(0).Instruction()->SetArg(1, pointer_high);
leaf->SetArg(1, Value(7u));
read(leaf->Arg(0), 11u);
auto plan = ExtractResourcePlan(program);
auto plan = Extract(program);
Check(plan.srt_code != nullptr, "nested user-data tree did not compile an x86 walker");
ResourceSnapshot snapshot;
ResourceSpecialization specialization;
@@ -268,7 +285,7 @@ void TestNativeNestedSrtRefresh() {
program.srt_reads.resize(1);
for (const int32_t offset : {-1, 7}) {
program.memory_info[0].offset = static_cast<uint32_t>(offset);
auto bounded = ExtractResourcePlan(program);
auto bounded = Extract(program);
auto base = pointer_words(second.data() + 1);
Check(bounded.srt_code != nullptr &&
MaterializeResources(bounded, {.user_data = base}, snapshot, specialization) &&
@@ -358,7 +375,7 @@ void TestSrtAliasesShareReads() {
DescriptorSource indirect;
indirect.indirect_descriptor.emplace(DescriptorSource::IndirectDescriptor{}).sources = {0, 1};
program.descriptor_sources.push_back(indirect);
auto plan = ExtractResourcePlan(program);
auto plan = Extract(program);
for (const auto &inst : plan.value_storage) {
Check(inst.GetOpcode() != ValueOpcode::ReadConst &&
inst.GetOpcode() != ValueOpcode::GetSrtResource,
@@ -431,7 +448,7 @@ void TestUniformVectorDescriptorRead() {
ResourceSpecialization specialization;
for (const bool written : {false, true}) {
program.info.buffers[0].written = written;
auto plan = ExtractResourcePlan(program);
auto plan = Extract(program);
Check(plan.control_flow.empty() && plan.capture_specialization_reads,
"vector descriptor read depended on incidental control-flow capture");
reads = {};
@@ -453,7 +470,7 @@ void TestUniformVectorDescriptorRead() {
auto &guarded_lane = block.AppendNewInst(ValueOpcode::ReadFirstLane, {Value(&guarded), Value(&active)});
program.descriptor_sources.push_back({.dwords = {Value(&guarded_lane)}, .dword_count = 1});
{
auto plan = ExtractResourcePlan(program);
auto plan = Extract(program);
uint32_t value = 0;
reads = {};
Check(EvaluateSrtValue(plan, runtime, plan.descriptor_sources.back().dwords[0], value) &&
@@ -466,7 +483,7 @@ void TestUniformVectorDescriptorRead() {
reads.count = 0;
uint32_t result = 99;
{
auto plan = ExtractResourcePlan(program);
auto plan = Extract(program);
Check(EvaluateSrtValue(plan, runtime, plan.descriptor_sources.back().dwords[0], result) &&
result == 0 && reads.count == 0,
"literal false EXEC read vector memory");
@@ -475,7 +492,7 @@ void TestUniformVectorDescriptorRead() {
handle.SetArg(3, Value(0x204u));
program.descriptor_sources.back().dwords[0] = Value(&count);
{
auto plan = ExtractResourcePlan(program);
auto plan = Extract(program);
Check(EvaluateSrtValue(plan, runtime, plan.descriptor_sources.back().dwords[0], result) &&
result == 0 && reads.count == 0,
"invalid vector buffer format read memory");
@@ -564,7 +581,7 @@ void TestWrittenDescriptorPredicateReads(bool memory_condition) {
store_block.AppendNewInst(ValueOpcode::StoreBufferU32,
{Value(&output), Value(0u), Value(0u), Value(0u), Value(1u), Value(true)})
.SetFlags(MemoryFlags{.index = 1});
auto plan = ExtractResourcePlan(program);
auto plan = Extract(program);
Check(plan.capture_specialization_reads == memory_condition && !plan.control_flow.empty(),
"conditional writable descriptor used the wrong memory dependency policy");
struct Reads { uint32_t count = 0; bool readable = false; } reads;
@@ -634,7 +651,7 @@ void TestSharedControlPredicates() {
program.blocks[i]->terminator.true_block = program.blocks[i == 0 ? 1 : 3];
program.blocks[i]->terminator.false_block = program.blocks[2];
}
auto plan = ExtractResourcePlan(program);
auto plan = Extract(program);
Check(plan.value_storage.size() <= 2u * depth + 12u &&
plan.control_flow.size() == 4 &&
plan.control_flow[0].condition == plan.control_flow[1].condition &&
@@ -706,7 +723,7 @@ void TestNestedExecPredicatesReuseReads() {
use.AppendNewInst(ValueOpcode::LoadBufferU32,
{buffer, Value(0u), Value(0u), Value(0u), Value(true)})
.SetFlags(MemoryFlags{.index = 1});
auto plan = ExtractResourcePlan(program);
auto plan = Extract(program);
struct Reads { uint32_t value = 0; bool readable = true; uint32_t count = 0; } reads;
FakeGuestMemory fake(+[](void *data, uint64_t address, std::span<uint32_t> words) {
auto &reads = *static_cast<Reads *>(data);
@@ -769,7 +786,7 @@ void TestFiniteImageRefreshReusesScalarReads() {
.resource_class = ImageResourceClass::Sampled,
.numeric_class = Libs::Graphics::Prospero::TextureNumericClass::Float,
.dimension = Libs::Graphics::ShaderRecompiler::Decoder::ImageDimension::Dim2D});
auto plan = ExtractResourcePlan(program);
auto plan = Extract(program);
struct Reads {
std::array<uint32_t, 3> words{0x100u, 0x200u, 0x200u};
std::array<uint32_t, 3> counts{};
@@ -912,7 +929,7 @@ void TestEmptyTraversalSkipsUnmappedDescriptor(bool independent_mask, bool count
{source.dwords[0], source.dwords[1], source.dwords[2], source.dwords[3]});
emit(use, ValueOpcode::LoadBufferU32, {buffer, Value(0u), Value(0u), Value(0u), Value(true)})
.Instruction()->SetFlags(MemoryFlags{.index = buffer_index});
auto plan = ExtractResourcePlan(program);
auto plan = Extract(program);
struct Reads { uint32_t count = 0; bool readable = true; uint32_t descriptors = 0; } reads;
FakeGuestMemory fake(+[](void *data, uint64_t address, std::span<uint32_t> words) {
constexpr std::array<uint32_t, 4> descriptor{0x8000u, 1u << 16u, 16u,
@@ -994,7 +1011,7 @@ void TestCyclicControlPredicatesRemainUnknown() {
branch(entry, u, left, right);
branch(left, a, right, exit);
branch(right, b, left, exit);
auto plan = ExtractResourcePlan(program);
auto plan = Extract(program);
uint32_t value = 99;
Check(!EvaluateSrtValue(plan, {}, plan.control_flow[1].condition, value) &&
!EvaluateSrtValue(plan, {}, plan.control_flow[2].condition, value),
@@ -1047,7 +1064,7 @@ void TestBackEdgeReachesPhiPredecessor() {
program.srt_reads.push_back({Value(&load(exit, 0u)), 0});
exit.AppendNewInst(ValueOpcode::ReadConst,
{Value(&exit.AppendNewInst(ValueOpcode::GetSrtResource)), Value(0u)});
auto plan = ExtractResourcePlan(program);
auto plan = Extract(program);
Check(plan.control_flow.size() == 4 && plan.control_phi_predecessors.size() == 1,
"back-edge plan lost its control flow or Phi");
plan.capture_specialization_reads = true;
@@ -1134,7 +1151,7 @@ void TestEmptyScalarBufferSpecialization() {
return fixture;
};
auto fixture = make_fixture();
auto plan = ExtractResourcePlan(fixture.program);
auto plan = Extract(fixture.program);
std::array<uint32_t, 4> descriptor{0x1000u, 8u << 16u, 0u, 3u << 28u};
uint32_t reads = 0u;
const SrtRuntime runtime{.user_data = descriptor, .userdata = &reads,
@@ -1191,7 +1208,7 @@ void TestEmptyScalarBufferSpecialization() {
void TestMixedSamplerVariantsShareRuntimeDescriptor() {
using namespace Libs::Graphics::ShaderRecompiler::IR;
auto program = MixedSamplerProgram();
auto plan = ExtractResourcePlan(program);
auto plan = Extract(program);
std::array<uint32_t, 2> user_data{0x11111111u, 0x22222222u};
const SrtRuntime runtime{.user_data = user_data};
ResourceSnapshot snapshot;
@@ -1239,7 +1256,7 @@ void TestSignedStorageSpecialization() {
program.descriptor_sources.push_back(source);
program.info.images.push_back({.source = 0u, .resource_class = ImageResourceClass::Storage,
.dimension = Libs::Graphics::ShaderRecompiler::Decoder::ImageDimension::Dim2D, .written = true});
auto plan = ExtractResourcePlan(program);
auto plan = Extract(program);
std::array<uint32_t, 1> data{};
const SrtRuntime runtime{.user_data = data};
ResourceSnapshot snapshot;
@@ -1317,7 +1334,7 @@ void TestIndirectImageSamplerClasses(bool mixed) {
program.descriptor_sources.push_back(root);
program.info.samplers.resize(1);
program.info.sampled_pairs.resize(1);
auto plan = ExtractResourcePlan(program);
auto plan = Extract(program);
std::array<uint32_t, 1> user_data{0x11111111u};
uint32_t reads = 0;
const SrtRuntime runtime{.user_data = user_data, .userdata = &reads,
@@ -1354,6 +1371,48 @@ void TestIndirectImageSamplerClasses(bool mixed) {
}
}
void TestShaderCacheRecords() {
using namespace Libs::Graphics::ShaderRecompiler::IR;
// Equivalent selector domains share one key group, which a load rebuilds.
Program selectors;
AddValueBlock(selectors);
for (const uint32_t stride : {4u, 8u, 4u}) {
DescriptorSource source;
source.indirect_descriptor = DescriptorSource::IndirectDescriptor{
.selector = DescriptorSource::IndirectDescriptor::SelectorRead{.stride = stride}};
selectors.descriptor_sources.push_back(source);
}
selectors.descriptor_sources.emplace_back();
const auto plan = Extract(selectors);
Check(plan.selector_groups == std::vector<uint32_t>{0u, 1u, 0u, UINT32_MAX} &&
plan.selector_keys.size() == 2u,
"loaded plan lost its selector key groups");
const std::array values{Value(), Value(ScalarReg(3)), Value(VectorReg(5)), Value(true),
Value(uint8_t{7}), Value(uint16_t{9}), Value(11u), Value(uint64_t{1} << 40u),
Value::F16(0x3c00u), Value::F32(-2.5f)};
Writer writer;
Io(writer, values);
Reader reader{writer.bytes};
std::array<Value, values.size()> loaded;
Io(reader, loaded);
Check(reader.ok && reader.bytes.empty() && loaded == values,
"immediate values changed through the shader cache");
// Damaged records: a missing instruction, a type that is no value type, an oversized count and
// an unknown opcode.
for (const uint8_t type : {1u, 3u}) {
const std::array<uint8_t, 12> bytes{type};
Reader damaged{bytes};
Value value;
Io(damaged, value);
Check(!damaged.ok, "damaged value was accepted");
}
ResourcePlan oversized, unknown;
Check(!LoadResourcePlan(std::array<uint8_t, 4>{0xffu, 0xffu, 0xffu, 0xffu}, oversized) &&
!LoadResourcePlan(std::array<uint8_t, 14>{1u, 0u, 0u, 0u, 0xffu, 0xffu}, unknown),
"damaged resource plan was accepted");
}
} // namespace
namespace Common {
@@ -1393,6 +1452,7 @@ int main() {
TestMixedSamplerVariantsShareRuntimeDescriptor();
TestSignedStorageSpecialization();
for (const bool mixed : {false, true}) TestIndirectImageSamplerClasses(mixed);
TestShaderCacheRecords();
std::puts("ResourceMaterializationTests: all cases passed");
return 0;
}
+33
View File
@@ -396,6 +396,38 @@ void TestClassicSavePaths() {
CHECK(Search(1) == names);
}
void TestConvert() {
Reset("CONVERT");
const auto source = DirName("save");
const auto memory = DirName("sce_sdmemory");
struct SaveDataConvert convert {};
convert.user_id = 1;
convert.src_dir_name = &memory;
convert.dst_blocks = 96;
CHECK(SaveDataConvert(&convert) == SAVE_DATA_ERROR_PARAMETER);
// A missing source fails at once.
convert.src_dir_name = &source;
CHECK(SaveDataConvert(&convert) == SAVE_DATA_ERROR_NOT_FOUND);
struct SaveDataMount3 mount {};
mount.user_id = 1;
mount.dir_name = &source;
mount.mount_mode = 4;
mount.blocks = 96;
SaveDataMountResult mounted {};
CHECK(SaveDataMount3(&mount, &mounted) == OK);
CHECK(SaveDataConvert(&convert) == SAVE_DATA_ERROR_BUSY);
CHECK(SaveDataUmount2(0, &mounted.mount_point) == OK);
CHECK(SaveDataConvert(&convert) == SAVE_DATA_ERROR_NO_NEED_CONVERT);
convert.dst_blocks = 95;
CHECK(SaveDataConvert(&convert) == SAVE_DATA_ERROR_BLOCKS_FEWER_THAN_SRC);
convert.dst_dir_name = &source;
CHECK(SaveDataConvert(&convert) == SAVE_DATA_ERROR_EXISTS);
for (const uint64_t blocks: {47u, 16385u}) {
convert.dst_blocks = blocks;
CHECK(SaveDataConvert(&convert) == SAVE_DATA_ERROR_PARAMETER);
}
}
void TestGuestSaveIsolation() {
Reset("MULTIUSER");
const auto name = DirName("slot");
@@ -670,6 +702,7 @@ int main(int argc, char** argv) {
TestIsolationAndSync();
TestFailedWritePreservesSave();
TestClassicSavePaths();
TestConvert();
TestGuestSaveIsolation();
TestSaveAllocations();
TestClassicSaveParams();
+48 -40
View File
@@ -628,42 +628,45 @@ void TestReadLaneElimination() {
"dynamic-lane read was rewritten unsafely");
}
void TestCompleteUMinReduction() {
void TestCompleteWaveReduction() {
enum class Case {
Masked, Subset, Literal, LiteralMasked, LiteralMove, Wave32,
UnrelatedSeed, UnmaskedSymbolic, WrongNeutral, RowMask, BankMask,
Masked, ScalarExec, Wave32, Wave32Rows, Wave32Maximum, RowMask, BankMask,
Shift, FetchInactive, BoundControl, Dpp8, MoveExec, UpdateExec,
Previous, DppSource, CrossExec, CrossRow, CrossSelectorLow,
CrossSelectorHigh, CrossFetch, CrossBound, CrossSource, FinalExec,
Endpoint, EndpointSource, DifferentWitness, CrossBlock, WrongOperation
Endpoint, EndpointSource, CrossBlock, WrongOperation
};
for (const auto test : {Case::Masked, Case::Subset, Case::Literal,
Case::LiteralMasked, Case::LiteralMove, Case::Wave32, Case::UnrelatedSeed,
Case::UnmaskedSymbolic, Case::WrongNeutral, Case::RowMask, Case::BankMask,
for (const auto test : {Case::Masked, Case::ScalarExec, Case::Wave32,
Case::Wave32Rows, Case::Wave32Maximum, Case::RowMask, Case::BankMask,
Case::Shift, Case::FetchInactive, Case::BoundControl, Case::Dpp8,
Case::MoveExec, Case::UpdateExec, Case::Previous, Case::DppSource,
Case::CrossExec, Case::CrossRow, Case::CrossSelectorLow,
Case::CrossSelectorHigh, Case::CrossFetch, Case::CrossBound,
Case::CrossSource, Case::FinalExec, Case::Endpoint, Case::EndpointSource,
Case::DifferentWitness, Case::CrossBlock, Case::WrongOperation}) {
Case::CrossBlock, Case::WrongOperation}) {
Fixture fixture(test == Case::CrossBlock ? 2 : 1);
const auto source = fixture.Emit(ValueOpcode::LaneId);
const auto mask = fixture.Emit(ValueOpcode::ULessThan32, {source, Value(19u)});
const auto other_mask = fixture.Emit(ValueOpcode::IEqual32, {source, Value(7u)});
const auto negated = fixture.Emit(ValueOpcode::LogicalNot, {mask});
const bool literal = test == Case::Literal || test == Case::LiteralMasked;
const auto enabled = literal ? Value(true) :
fixture.Emit(ValueOpcode::LogicalOr, {negated, mask});
auto seed_mask = test == Case::UnrelatedSeed ? other_mask : mask;
if (test == Case::Subset)
seed_mask = fixture.Emit(ValueOpcode::LogicalAnd, {other_mask, mask});
const auto seed = test == Case::Literal || test == Case::UnmaskedSymbolic ? source :
fixture.Emit(ValueOpcode::SelectU32,
{seed_mask, source, Value(test == Case::WrongNeutral ? 0u : UINT32_MAX)});
auto enabled = fixture.Emit(ValueOpcode::LogicalOr, {negated, mask});
if (test == Case::ScalarExec) {
// s_orn2_saveexec of a mask that is not a ballot, read back as a lane bit.
const auto exec = fixture.Emit(ValueOpcode::GetUserData, {Value(static_cast<ScalarReg>(2))});
const auto full = fixture.Emit(ValueOpcode::BitwiseOr32,
{fixture.Emit(ValueOpcode::BitwiseNot32, {exec}), exec});
const auto bit = fixture.Emit(ValueOpcode::BitwiseAnd32, {source, Value(31u)});
const auto shifted = fixture.Emit(ValueOpcode::ShiftRightLogical32, {full, bit});
enabled = fixture.Emit(ValueOpcode::INotEqual32,
{fixture.Emit(ValueOpcode::BitwiseAnd32, {shifted, Value(1u)}), Value(0u)});
}
// A UMax scan seeds lanes outside EXEC with zero.
const bool maximum = test == Case::Wave32Maximum;
const uint32_t neutral = maximum ? 0u : UINT32_MAX;
const auto seed = fixture.Emit(ValueOpcode::SelectU32, {mask, source, Value(neutral)});
auto prefix = fixture.Emit(ValueOpcode::SelectU32, {enabled, seed, source});
Value preserved;
const auto operation = test == Case::WrongOperation ? ValueOpcode::BitwiseOr32 :
ValueOpcode::UMin32;
auto operation = maximum ? ValueOpcode::UMax32 : ValueOpcode::UMin32;
if (test == Case::WrongOperation) operation = ValueOpcode::BitwiseOr32;
for (const uint32_t shift : {1u, 2u, 4u, 8u}) {
DppMoveFlags flags{.control = 0x110u + shift};
if (shift == 2) {
@@ -675,8 +678,7 @@ void TestCompleteUMinReduction() {
if (test == Case::Dpp8) flags.dpp8 = true;
}
const auto move = fixture.Emit(ValueOpcode::DppMoveU32,
{test == Case::DppSource ? source : prefix,
test == Case::MoveExec ? mask : test == Case::LiteralMove ? Value(true) : enabled});
{test == Case::DppSource ? source : prefix, test == Case::MoveExec ? mask : enabled});
move.Instruction()->SetFlags(flags);
const auto combined = fixture.Emit(operation, {move, prefix});
const auto update = fixture.Emit(ValueOpcode::DppUpdateU32,
@@ -686,15 +688,12 @@ void TestCompleteUMinReduction() {
prefix = update;
if (shift == 2) preserved = fixture.Emit(ValueOpcode::ReferenceU32, {prefix});
}
auto cross_exec = test == Case::CrossExec ? mask : enabled;
if (test == Case::DifferentWitness) {
const auto other_not = fixture.Emit(ValueOpcode::LogicalNot, {other_mask});
cross_exec = fixture.Emit(ValueOpcode::LogicalOr, {other_mask, other_not});
}
const auto cross_exec = test == Case::CrossExec ? mask : enabled;
const auto cross = fixture.Emit(ValueOpcode::Permlane16U32,
{test == Case::CrossSource ? source : prefix,
Value(test == Case::CrossSelectorLow ? 0xfffffff0u : UINT32_MAX),
Value(test == Case::CrossSelectorHigh ? 0xfffffff0u : UINT32_MAX), cross_exec});
Value(test == Case::CrossSelectorHigh ? 0xfffffff0u : UINT32_MAX), cross_exec,
Value(0u)});
cross.Instruction()->SetFlags(PermlaneFlags{
.x16 = test != Case::CrossRow, .fetch_inactive = test == Case::CrossFetch,
.bound_control = test != Case::CrossBound});
@@ -703,10 +702,16 @@ void TestCompleteUMinReduction() {
const auto final = fixture.Emit(ValueOpcode::SelectU32,
{test == Case::FinalExec ? mask : enabled, combined, prefix});
const uint32_t result_block = test == Case::CrossBlock ? 1u : 0u;
// Wave32 reads each half's row endpoint from the scan itself.
const bool rows = test == Case::Wave32Rows || test == Case::ScalarExec || maximum;
const auto endpoints = rows ? prefix : final;
uint32_t low_lane = test == Case::Endpoint ? 30u : 31u;
if (rows) low_lane = 15u;
const auto low = fixture.Emit(ValueOpcode::ReadLane,
{final, Value(test == Case::Endpoint ? 30u : 31u)}, 0, result_block);
{endpoints, Value(low_lane)}, 0, result_block);
const auto high = fixture.Emit(ValueOpcode::ReadLane,
{test == Case::EndpointSource ? prefix : final, Value(63u)}, 0, result_block);
{test == Case::EndpointSource ? prefix : endpoints, Value(rows ? 31u : 63u)}, 0,
result_block);
const auto result = fixture.Emit(operation, {high, low}, 0, result_block);
const auto use = fixture.Emit(ValueOpcode::ReferenceU32, {result}, 0, result_block);
using Terminator = Libs::Graphics::ShaderRecompiler::CFG::TerminatorKind;
@@ -717,16 +722,19 @@ void TestCompleteUMinReduction() {
fixture.BlockAt().terminator.true_block = &fixture.BlockAt(1);
}
ValidateProgram(fixture.program, true);
const auto stats = EliminateReadLane(fixture.program, test == Case::Wave32 ? 32u : 64u);
const bool matches = test == Case::Masked || test == Case::Subset ||
test == Case::Literal || test == Case::LiteralMasked || test == Case::LiteralMove;
// The pipeline folds full-EXEC predicates to true before eliminating reads.
const uint32_t wave_size = test == Case::Wave32 || rows ? 32u : 64u;
ConstantPropagationPass(fixture.program.blocks, wave_size);
const auto stats = EliminateReadLane(fixture.program, wave_size);
const bool matches = test == Case::Masked || rows;
Check(stats.rewritten_reductions == (matches ? 1u : 0u) && stats.rewritten_reads == 0,
("UMin reduction proof failed case " + std::to_string(static_cast<unsigned>(test))).c_str());
("wave reduction proof failed case " + std::to_string(static_cast<unsigned>(test))).c_str());
if (matches) {
const auto *reduction = use.Instruction()->Arg(0).ResolveInstruction();
Check(reduction->GetOpcode() == ValueOpcode::SubgroupUMinU32 &&
Check(reduction->GetOpcode() ==
(maximum ? ValueOpcode::SubgroupUMaxU32 : ValueOpcode::SubgroupUMinU32) &&
reduction->Arg(0).Resolve() == seed.Resolve(),
"UMin reduction changed the neutral-masked input");
"wave reduction changed the neutral-masked input");
RemoveIdentities(fixture.program);
EliminateDeadCode(fixture.program.blocks);
uint32_t updates = 0;
@@ -734,14 +742,14 @@ void TestCompleteUMinReduction() {
updates += inst.GetOpcode() == ValueOpcode::DppUpdateU32;
Check(inst.GetOpcode() != ValueOpcode::ReadLane &&
inst.GetOpcode() != ValueOpcode::Permlane16U32,
"UMin reduction retained unused endpoint or cross-row operations");
"wave reduction retained unused endpoint or cross-row operations");
}
Check(updates == 2 && preserved.Instruction()->Arg(0).Instruction()->HasUses(),
"UMin reduction destroyed an independently consumed prefix or kept its dead suffix");
"wave reduction destroyed an independently consumed prefix or kept its dead suffix");
ValidateProgram(fixture.program, true);
} else {
Check(use.Instruction()->Arg(0).Resolve() == result,
"rejected UMin proof changed its original result");
"rejected wave reduction proof changed its original result");
}
}
}
@@ -883,7 +891,7 @@ int main() {
TestSharedIntegerRuntimeDependencies();
TestConstantBufferBounds();
TestReadLaneElimination();
TestCompleteUMinReduction();
TestCompleteWaveReduction();
TestOptimizationPipeline();
TestControlFlowValueSurvivesReadLaneFolding();
TestDeadPhiCyclesAndPlanningRoots();
File diff suppressed because it is too large. Load diff
+138 -29
View File
@@ -8,6 +8,7 @@
#include "graphics/host_gpu/renderer/image/imageView.h"
#include "graphics/host_gpu/renderer/image/textureCommon.h"
#include "graphics/host_gpu/renderer/pipeline/blendMapping.h"
#include "graphics/host_gpu/renderer/pipeline/pipelineCache.h"
#include "graphics/host_gpu/renderer/pipeline/shaderResourceBarrier.h"
#include "graphics/shader/recompiler/ShaderRecompiler.h"
#include "graphics/shader/recompiler/backend/spirv/SpirvEmitter.h"
@@ -163,10 +164,10 @@ void CompilePixelRuntime(const ShaderParams &params,
options.input_info.pixel = &input_info;
auto result = RecompileForTest(params.code, options);
static std::deque<ShaderRecompiler::IR::CompiledShaderInfo> programs;
static std::deque<ShaderModule> modules;
static std::deque<ShaderRecompiler::IR::ResourceSnapshot> resources;
programs.push_back(std::move(result.program).TakeCompiledInfo());
input_info.stage.program = &programs.back();
modules.push_back({.info = std::move(result.program).TakeCompiledInfo()});
input_info.stage.module = &modules.back();
resources.push_back(std::move(result.resources));
input_info.stage.resources = &resources.back();
}
@@ -1157,7 +1158,7 @@ void TestNativeShaderResourceDependencies() {
set_buffer(0, 0x1000, 16, 3);
set_buffer(1, 0x1800, 4, 7);
set_buffer(2, 0x2000, 0, 64);
const ShaderRecompiler::IR::CompiledShaderInfo compiled{
const ShaderModule compiled{.info = {
.stage = program.stage,
.shader_hash = program.shader_hash,
.wave_size = program.wave_size,
@@ -1166,8 +1167,8 @@ void TestNativeShaderResourceDependencies() {
.scratch_dwords = program.scratch_dwords,
.info = program.info,
.bindings = program.bindings,
};
ShaderStageRuntime runtime{.program = &compiled, .resources = &resources};
}};
ShaderStageRuntime runtime{.module = &compiled, .resources = &resources};
Check(HasShaderBufferWrites(runtime),
"graphics/compute write predicate lost nonempty written buffers");
set_buffer(0, 0, 16, 3);
@@ -1846,6 +1847,31 @@ void TestShaderStageBarriers() {
"SPIR-V barrier does not use workgroup acquire-release memory semantics");
CheckSpirvBinaryValidates(result.spirv);
// s_barrier also acquires other waves' buffer and image stores.
const auto acquires = [](std::span<const uint32_t> code,
const ShaderRecompiler::CompileOptions &options) {
const auto result = RecompileForTest(code, options);
CheckSpirvBinaryValidates(result.spirv);
// AcquireRelease | UniformMemory | WorkgroupMemory | ImageMemory.
return DisassembleSpirvBinary(result.spirv).find(
"OpControlBarrier %uint_2 %uint_2 %uint_2376") != std::string::npos;
};
const uint32_t buffer_store[] = {
EncodeMubuf0(0x1c, 0, false), EncodeMubuf1(1, 0, 0), // buffer_store_dword
EncodeSopp(0x0a), EncodeSopp(0x01),
};
Check(acquires(buffer_store, MakeCompileOptions(ShaderType::Compute)),
"s_barrier after buffer stores does not acquire buffer memory");
const uint32_t image_store[] = {
EncodeMimg0(0x08, 0xf), EncodeMimg1(20, 0, 0, 4), // image_store
EncodeSopp(0x0a), EncodeSopp(0x01),
};
const auto image_user_data = ImageTestUserData();
auto image_options = MakeCompileOptions(ShaderType::Compute);
image_options.user_data = image_user_data;
Check(acquires(image_store, image_options),
"s_barrier after image stores does not acquire image memory");
const auto vertex_result =
RecompileForTest(shader, MakeCompileOptions(ShaderType::Vertex));
CheckSpirvBinaryValidates(vertex_result.spirv);
@@ -7463,7 +7489,7 @@ void TestNewShaderRecompilerMubufFormatTranslation() {
CheckSpirvBinaryValidates(result.spirv);
}
void TestNewShaderRecompilerFormattedStoreUsesRuntimeArrayLengthOnly() {
void TestNewShaderRecompilerFormattedStoreUsesRuntimeBindingSizeOnly() {
const uint32_t shader[] = {
EncodeMubuf0(0x04),
EncodeMubuf1(0, 0, 1), // buffer_store_format_x
@@ -7490,8 +7516,9 @@ void TestNewShaderRecompilerFormattedStoreUsesRuntimeArrayLengthOnly() {
CheckSpirvBinaryValidates(result.spirv);
const auto source = DisassembleSpirvBinary(result.spirv);
Check((source.find("OpArrayLength") != std::string::npos),
"formatted store SPIR-V lacks runtime storage-buffer bounds check");
Check(source.find("OpULessThan") != std::string::npos &&
source.find("OpArrayLength") == std::string::npos,
"formatted store SPIR-V lacks a bounds check against its pushed binding size");
Check(
!SpirvSourceHasInstructionUsing(source, "OpULessThan", "%uint_5"),
"formatted store SPIR-V baked descriptor NumRecords into a store guard");
@@ -9552,6 +9579,28 @@ void TestNativeConstantSamplerSelection() {
CheckSpirvBinaryValidates(result.spirv);
}
void TestPackedZeroHighSamplerTableKey() {
// s_pack_ll_b32_b16 with a zero high lane zero-extends a GPU-selected table key, so the
// sampler table stays bounded to 64Ki records.
const uint32_t shader[] = {
EncodeMubuf0(0x0c, 0, true), EncodeMubuf1(1, 3, 0), EncodeSopp(0x0c, 0),
EncodeVop1(0x02, 28, 257), EncodeSop2(0x32, 29, 28, 128),
EncodeSop2(0x1e, 30, 29, 132),
EncodeSmem0(0x0a, 24, 4), 30u << 25u, EncodeSopp(0x0c, 0),
EncodeMimg0(0x20, 0x1), EncodeMimg1(0, 4, 6, 2),
EncodeExp0(0x00, 0x1), EncodeExp1(0, 0, 0, 0),
EncodeSopp(0x01),
};
auto options = MakeCompileOptions(ShaderType::Pixel);
options.user_data = ImageTestUserData();
const auto translated = ShaderRecompiler::TranslateProgram(shader, options);
const auto plan = ShaderRecompiler::IR::ExtractResourcePlan(translated.program);
const auto& indirect =
plan.descriptor_sources[plan.info.samplers.at(0).source].indirect_descriptor;
Check(indirect && indirect->table_stride == 16u && indirect->table_maximum == 0xffffu,
"a zero-extended sampler table key lost its 16-bit bound");
}
void TestBoundedMaterialBufferStores(bool scalar_key) {
// Captured batch search and descriptor waterfall, with deformation arithmetic removed.
const uint32_t shader[] = {
@@ -10615,19 +10664,18 @@ void TestNewShaderRecompilerBufferLoadsGuardedByExec() {
const auto source = DisassembleSpirvBinary(result.spirv);
const auto exec_branch =
source.find("OpBranchConditional", 0);
const auto array_length =
source.find("OpArrayLength", 0);
const auto bounds_branch = source.find("OpBranchConditional", array_length);
const auto bounds = source.find("OpULessThan", 0);
const auto bounds_branch = source.find("OpBranchConditional", bounds);
const auto element_access = source.find("OpAccessChain %_ptr_StorageBuffer_uint", 0);
Check(exec_branch != std::string::npos,
"buffer load SPIR-V lacks EXEC guard branch");
Check(array_length != std::string::npos,
"buffer load SPIR-V lacks storage buffer array-length bounds check");
Check(bounds != std::string::npos,
"buffer load SPIR-V lacks storage buffer bounds check");
Check(bounds_branch != std::string::npos,
"buffer load SPIR-V lacks storage buffer bounds branch");
Check(element_access != std::string::npos,
"buffer load SPIR-V lacks storage element access");
Check(exec_branch < array_length,
Check(exec_branch < bounds,
"buffer load bounds check was emitted outside EXEC guard");
Check(bounds_branch < element_access,
"buffer load storage element pointer was formed before bounds guard");
@@ -10660,13 +10708,12 @@ void TestNewShaderRecompilerBufferAtomicsGuardedByBounds() {
CheckSpirvBinaryValidates(result.spirv);
const auto source = DisassembleSpirvBinary(result.spirv);
const auto array_length =
source.find("OpArrayLength", 0);
const auto bounds_branch = source.find("OpBranchConditional", array_length);
const auto bounds = source.find("OpULessThan", 0);
const auto bounds_branch = source.find("OpBranchConditional", bounds);
const auto atomic = source.find(test.spirv, 0);
const auto memory_barrier =
source.find("OpMemoryBarrier", atomic);
Check(array_length != std::string::npos &&
Check(bounds != std::string::npos &&
bounds_branch != std::string::npos &&
atomic != std::string::npos &&
memory_barrier != std::string::npos,
@@ -10715,14 +10762,13 @@ void TestCapturedBufferAtomicsX2() {
"64-bit buffer atomic storage view does not use eight-byte elements");
Check(CountSourceOccurrences(source, "Aliased") == 2u,
"both storage-buffer views must declare that they alias");
const auto array_length =
source.find("OpArrayLength", 0);
const auto bounds_branch = source.find("OpBranchConditional", array_length);
const auto bounds = source.find("OpULessThan", 0);
const auto bounds_branch = source.find("OpBranchConditional", bounds);
const auto atomic =
source.find(test.spirv_name, 0);
const auto memory_barrier =
source.find("OpMemoryBarrier", atomic);
Check(array_length != std::string::npos &&
Check(bounds != std::string::npos &&
bounds_branch != std::string::npos &&
atomic != std::string::npos && bounds_branch < atomic,
"64-bit buffer atomic was not guarded by storage-buffer bounds");
@@ -11001,6 +11047,18 @@ void TestMemoryFedScalarLeafCall() {
ExpectFatal([&] { (void)PrepareShaderSource(multiple, options); },
"a native return pair was accepted as a later call target");
#endif
// A lane read and a wave32 lane mask write one SGPR, so neither s13 writer defines s14.
for (const auto& [wave_size, writer] : {std::pair{64u, 0x360u}, std::pair{32u, 0xc2u}}) {
const uint32_t neighbor[] = {EncodeVop3Word0(writer, 13), EncodeVop3Word1(256, 128, 0),
EncodeSop1(0x21, 14, 14), EncodeSopp(0x01)};
auto neighbor_options = options;
neighbor_options.wave_size = wave_size;
const auto neighbor_source = PrepareShaderSource(neighbor, neighbor_options);
const auto* target = neighbor_source.calls.front().target[0].TryInstruction();
Check(target != nullptr && target->GetOpcode() == IR::ValueOpcode::GetUserData &&
IR::RegIndex(target->Arg(0).ScalarRegister()) == 14u,
"a single-SGPR vector write was taken as the next SGPR's call-target definition");
}
}
void TestNewShaderRecompilerSetpcBranch() {
@@ -11729,7 +11787,7 @@ void TestMeshInputAssembly() {
options.user_data_count = 0;
options.input_info.vertex = &input;
auto program = Frontend::TranslateProgram(decoded, graph, options);
const uint32_t draw[] = {test.count, test.base_vertex, 7, test.width,
const uint32_t draw[] = {test.count, test.base_vertex, test.width,
test.address_low, 0x12};
Inst *load = nullptr;
for (auto &inst : *program.blocks.front()) {
@@ -11774,7 +11832,7 @@ void TestMeshInputAssembly() {
Check(sgprs[2] == test.group_info && sgprs[3] == test.wave_info &&
vgprs[0] == ((test.first << 2) | (test.second << 18)) &&
vgprs[1] == test.third * 4 && vgprs[5] == test.vertex_id &&
vgprs[test.fast_launch ? 6 : 8] == 9u,
vgprs[test.fast_launch ? 6 : 8] == 2u,
"mesh prolog changed input assembly, wave counts, vertex ID, or instance ID");
}
}
@@ -13601,6 +13659,34 @@ void TestNewShaderRecompilerPerInvocationMasksWithoutMirrors() {
}
}
// The wave32 live mask projects to the entry predicate, and VCC keeps its compare predicate
// after WQM.
const uint32_t live_mask_shader[] = {
EncodeSop1(0x03, 0, 126), // s_mov_b32 s0, exec_lo
EncodeSop2(0x0e, 126, 126, 0), // s_and_b32 exec_lo, exec_lo, s0
EncodeVop1(0x01, 0, 242), // v_mov_b32 v0, 1.0
EncodeExp0(0x00, 0x1), EncodeExp1(0, 0, 0, 0),
EncodeSopp(0x01),
};
const uint32_t wqm_vcc_shader[] = {
EncodeSop1(0x09, 126, 126), // s_wqm_b32 exec_lo, exec_lo
EncodeVop2(0x23, 0, 193, 1), // v_mbcnt_lo_u32_b32 v0, -1, v1
EncodeVopc(0xc1, 129, 0), // v_cmp_lt_u32 vcc_lo, 1, v0
EncodeVop2(0x01, 2, 128, 0), // v_cndmask_b32 v2, 0, v0, vcc_lo
EncodeExp0(0x00, 0x1), EncodeExp1(2, 2, 2, 2),
EncodeSopp(0x01),
};
auto wave32_pixel_options = MakeCompileOptions(ShaderType::Pixel);
wave32_pixel_options.wave_size = 32u;
for (const std::span<const uint32_t> shader : {std::span<const uint32_t>(live_mask_shader),
std::span<const uint32_t>(wqm_vcc_shader)}) {
const auto pixel_result = RecompileForTest(shader, wave32_pixel_options);
CheckSpirvBinaryValidates(pixel_result.spirv);
Check(DisassembleSpirvBinary(pixel_result.spirv).find("OpGroupNonUniformBallot") ==
std::string::npos,
"wave32 pixel mask rebuilt a known predicate from a ballot");
}
const uint32_t local_shader[] = {
EncodeVopc(0xc1, 5 + 256, 8), // v_cmp_lt_u32 vcc, v5, v8
EncodeSop2(0x0f, 2, 126, 106), // s_and_b64 s[2:3], exec, vcc
@@ -13715,6 +13801,7 @@ void TestNewShaderRecompilerExpPixelOutputs() {
ShaderPixelInputInfo uint16_info;
uint16_info.target_output_mode[0] = 7;
uint16_info.target_uint_mask = 1;
options.input_info.pixel = &uint16_info;
auto uint16_result = RecompileForTest(shader, options);
const auto uint16_source = DisassembleSpirvBinary(uint16_result.spirv);
@@ -13729,6 +13816,18 @@ void TestNewShaderRecompilerExpPixelOutputs() {
"compressed UINT16 MRT export was incorrectly decoded as FP16");
CheckSpirvBinaryValidates(uint16_result.spirv);
ShaderPixelInputInfo sint16_info;
sint16_info.target_output_mode[0] = 8;
sint16_info.target_sint_mask = 1;
options.input_info.pixel = &sint16_info;
auto sint16_result = RecompileForTest(shader, options);
const auto sint16_source = DisassembleSpirvBinary(sint16_result.spirv);
Check(sint16_source.find("OpVariable %_ptr_Output_v4int Output") != std::string::npos &&
CountSourceOccurrences(sint16_source, "OpBitFieldSExtract") == 4u &&
!SpirvContainsExtInst(sint16_result.spirv, 62),
"compressed SINT16 MRT export did not sign-extend into a signed output");
CheckSpirvBinaryValidates(sint16_result.spirv);
ShaderPixelInputInfo unorm16_info;
unorm16_info.target_output_mode[0] = 5;
options.input_info.pixel = &unorm16_info;
@@ -13756,6 +13855,7 @@ void TestNewShaderRecompilerExpPixelOutputs() {
ShaderPixelInputInfo uint_info;
uint_info.target_output_mode[0] = 7;
uint_info.target_uint_mask = 1;
options.input_info.pixel = &uint_info;
auto partial_uint_result =
RecompileForTest(partial_shader, options);
@@ -13782,6 +13882,15 @@ void TestNewShaderRecompilerExpPixelOutputs() {
"compressed UINT16 BA-only export did not read and unpack VSRC1");
CheckSpirvBinaryValidates(compressed_ba_result.spirv);
// 32-bit exports keep their raw bits in a UINT target's integer output.
uint_info.target_output_mode[0] = 9;
auto raw_uint_result = RecompileForTest(partial_shader, options);
const auto raw_uint_source = DisassembleSpirvBinary(raw_uint_result.spirv);
Check(raw_uint_source.find("OpVariable %_ptr_Output_v4uint Output") != std::string::npos &&
raw_uint_source.find("OpBitcast %v4float") == std::string::npos,
"32-bit MRT export to a UINT target did not use an unsigned integer output");
CheckSpirvBinaryValidates(raw_uint_result.spirv);
unorm16_info.target_output_mode[0] = 5;
options.input_info.pixel = &unorm16_info;
auto unorm16_ba_result =
@@ -15925,8 +16034,8 @@ void TestPixelProgramCacheBindingIdentity() {
first_params.hash == XXH3_64bits(shader.data(), shader.size_bytes()) &&
first_params.hash == second_params.hash &&
first_key == MakeStageStaticKey(second_info) &&
first_info.stage.program != nullptr &&
second_info.stage.program != nullptr,
first_info.stage.module != nullptr &&
second_info.stage.module != nullptr,
"pixel program matching did not preserve an identical source/static identity");
return std::pair {first_params.hash, first_key};
};
@@ -16542,7 +16651,6 @@ void TestNewShaderRecompilerSpirvSizeBaselines() {
.variables = 2,
.loads = 9,
.stores = 4,
.array_lengths = 2,
.phis = 5,
.labels = 34,
.selection_merges = 10,
@@ -16743,7 +16851,7 @@ int main() {
TestNewShaderRecompilerNativeWideBufferIr();
TestNewShaderRecompilerScalarB64LaneTranslation();
TestNewShaderRecompilerMubufFormatTranslation();
TestNewShaderRecompilerFormattedStoreUsesRuntimeArrayLengthOnly();
TestNewShaderRecompilerFormattedStoreUsesRuntimeBindingSizeOnly();
TestNewShaderRecompilerTypedBufferTranslation();
TestNewShaderRecompilerDsReadWrite2Translation();
TestNewShaderRecompilerDsWideAndAtomicTranslation();
@@ -16814,6 +16922,7 @@ int main() {
TestNativeGuardedSamplerSource(false);
TestNativeGuardedSamplerSource(true);
TestNativeConstantSamplerSelection();
TestPackedZeroHighSamplerTableKey();
for (const bool scalar_key : {false, true}) TestBoundedMaterialBufferStores(scalar_key);
TestNativeDescriptorProvenanceKeepsGpuSelection();
TestCfgSiblingSharedExit();