Compare commits

...
Author SHA1 Message Date
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
Mr. Big Britchesandnmzik 906d25568f launcher: option to close the terminal after a clean exit (#1869)
launcher: close the terminal after a clean exit and run several games at once

Co-authored-by: nmzik <Nmzik@mail.ru>
2026-10-24 20:06:45 +02:00
nmzik 8291cfeb04 shader: parse vertex metadata once when a shader is mapped 2026-10-24 05:44:46 +02:00
nmzik dbc7b8b2ff pipeline: look up draw programs and pipelines from the command buffer 2026-10-24 05:18:46 +02:00
nmzik 269e411ef2 graphics: index live cache slots directly 2026-10-24 04:26:16 +02:00
nmzik 170c107779 texture_cache: dedupe region queries with a picked flag 2026-10-24 04:03:01 +02:00
nmzik 8dc0d56b7e common: SlotVector without generations 2026-10-24 03:48:46 +02:00
nmzik d7856753e0 shader: walk every resource plan with the x86 SRT walker 2026-10-24 02:57:24 +02:00
nmzik bdf48dcf3b shader: complete faulting SRT walker loads as zero reads 2026-10-24 01:39:42 +02:00
nmzik f356fa3730 gpu: move the BDA sync queue from MemoryTracker into BufferCache 2026-10-24 01:05:19 +02:00
nmzik 07f6420273 memory: take the address-space lock shared for tracker protections 2026-10-23 23:53:51 +02:00
90 changed files with 4995 additions and 3706 deletions

No files matched your search

+6 -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)
@@ -451,6 +451,11 @@ add_executable(lru_cache_tests EXCLUDE_FROM_ALL
)
target_include_directories(lru_cache_tests PRIVATE ${inc_headers})
add_executable(slot_vector_tests EXCLUDE_FROM_ALL
"${KYTY_TESTS_DIR}/SlotVectorTests.cpp"
)
target_include_directories(slot_vector_tests PRIVATE ${inc_headers})
add_executable(ime_dialog_tests EXCLUDE_FROM_ALL
"${KYTY_TESTS_DIR}/ImeDialogTests.cpp"
"${KYTY_SOURCE_DIR}/libs/imeCommon.cpp"
+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();
+52 -52
View File
@@ -1,15 +1,15 @@
#ifndef EMULATOR_SRC_COMMON_SLOTVECTOR_H_
#define EMULATOR_SRC_COMMON_SLOTVECTOR_H_
#include "common/assert.h"
#include "common/abi.h"
#include "common/common.h"
#include <compare>
#include <cstdint>
#include <deque>
#include <functional>
#include <limits>
#include <optional>
#include <memory>
#include <new>
#include <numeric>
#include <utility>
#include <vector>
@@ -19,105 +19,105 @@ struct SlotId {
static constexpr uint32_t INVALID_INDEX = std::numeric_limits<uint32_t>::max();
constexpr SlotId() noexcept = default;
constexpr SlotId(uint32_t value) noexcept: index(value), generation(1) {}
constexpr SlotId(uint32_t value, uint32_t slot_generation) noexcept
: index(value), generation(slot_generation) {}
constexpr SlotId(uint32_t index) noexcept: index(index) {}
[[nodiscard]] constexpr explicit operator bool() const noexcept { return index != INVALID_INDEX; }
[[nodiscard]] constexpr explicit operator bool() const noexcept {
return index != INVALID_INDEX;
}
constexpr auto operator<=>(const SlotId&) const noexcept = default;
uint32_t index = INVALID_INDEX;
uint32_t generation = 0;
};
// Stable-address slot storage for cache resources that are intentionally non-movable.
// Fixed-size chunks keep the stored, non-movable resources in place. Occupancy is a byte per
// slot, not a bitset: a deferred erase runs while fault threads read other slots.
template <typename T>
class SlotVector {
static constexpr uint32_t ChunkSize = 256;
public:
SlotVector() = default;
KYTY_CLASS_NO_COPY(SlotVector);
~SlotVector() noexcept {
ForEach([](SlotId, T& value) { value.~T(); });
}
[[nodiscard]] T& operator[](SlotId id) noexcept {
EXIT_IF(!is_allocated(id));
return *m_values[id.index].value;
return m_chunks[id.index / ChunkSize][id.index % ChunkSize].object;
}
[[nodiscard]] const T& operator[](SlotId id) const noexcept {
EXIT_IF(!is_allocated(id));
return *m_values[id.index].value;
return m_chunks[id.index / ChunkSize][id.index % ChunkSize].object;
}
[[nodiscard]] T* try_get(SlotId id) noexcept {
return is_allocated(id) ? &*m_values[id.index].value : nullptr;
return is_allocated(id) ? &(*this)[id] : nullptr;
}
[[nodiscard]] const T* try_get(SlotId id) const noexcept {
return is_allocated(id) ? &*m_values[id.index].value : nullptr;
return is_allocated(id) ? &(*this)[id] : nullptr;
}
[[nodiscard]] bool is_allocated(SlotId id) const noexcept {
return id && id.index < m_values.size() &&
m_values[id.index].generation == id.generation && m_values[id.index].value.has_value();
return id.index < m_stored.size() && m_stored[id.index] != 0;
}
template <typename... Args>
[[nodiscard]] SlotId insert(Args&&... args) {
uint32_t index = 0;
if (m_free_list.empty()) {
index = static_cast<uint32_t>(m_values.size());
m_values.emplace_back();
m_values.back().value.emplace(std::forward<Args>(args)...);
} else {
index = m_free_list.back();
m_free_list.pop_back();
EXIT_IF(m_values[index].value.has_value());
m_values[index].value.emplace(std::forward<Args>(args)...);
Reserve();
}
++m_size;
return SlotId {index, m_values[index].generation};
const uint32_t index = m_free_list.back();
m_free_list.pop_back();
new (&(*this)[index]) T(std::forward<Args>(args)...);
m_stored[index] = 1;
return SlotId {index};
}
void erase(SlotId id) noexcept {
EXIT_IF(!is_allocated(id));
auto& slot = m_values[id.index];
slot.value.reset();
if (++slot.generation == 0) {
slot.generation = 1;
}
(*this)[id].~T();
m_stored[id.index] = 0;
m_free_list.push_back(id.index);
--m_size;
}
[[nodiscard]] size_t size() const noexcept { return m_size; }
[[nodiscard]] size_t capacity() const noexcept { return m_values.size(); }
template <typename F>
void ForEach(F&& fn) {
for (uint32_t index = 0; index < m_values.size(); ++index) {
if (m_values[index].value) {
fn(SlotId {index, m_values[index].generation}, *m_values[index].value);
for (uint32_t index = 0; index < m_stored.size(); ++index) {
if (m_stored[index] != 0) {
fn(SlotId {index}, (*this)[index]);
}
}
}
template <typename F>
void ForEach(F&& fn) const {
for (uint32_t index = 0; index < m_values.size(); ++index) {
if (m_values[index].value) {
fn(SlotId {index, m_values[index].generation}, *m_values[index].value);
for (uint32_t index = 0; index < m_stored.size(); ++index) {
if (m_stored[index] != 0) {
fn(SlotId {index}, (*this)[index]);
}
}
}
private:
struct Slot {
std::optional<T> value;
uint32_t generation = 1;
union Slot {
Slot() noexcept {}
~Slot() noexcept {}
T object;
};
std::deque<Slot> m_values;
std::vector<uint32_t> m_free_list;
size_t m_size = 0;
void Reserve() {
const auto first = static_cast<uint32_t>(m_stored.size());
m_chunks.push_back(std::make_unique<Slot[]>(ChunkSize));
m_stored.resize(first + ChunkSize);
m_free_list.resize(ChunkSize);
std::iota(m_free_list.rbegin(), m_free_list.rend(), first);
}
std::vector<std::unique_ptr<Slot[]>> m_chunks;
std::vector<uint8_t> m_stored;
std::vector<uint32_t> m_free_list;
};
} // namespace Common
@@ -125,7 +125,7 @@ private:
template <>
struct std::hash<Common::SlotId> {
[[nodiscard]] size_t operator()(Common::SlotId id) const noexcept {
return std::hash<uint64_t> {}((static_cast<uint64_t>(id.generation) << 32u) | id.index);
return std::hash<uint32_t> {}(id.index);
}
};
+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);
-15
View File
@@ -66,18 +66,6 @@ RegionManager* MemoryTracker::GetOrCreateRegion(uint64_t index) {
return ptr;
}
void MemoryTracker::QueueBdaSync(uint64_t vaddr, uint64_t size) {
const auto begin = Common::AlignDown(vaddr, TRACKER_PAGE_SIZE);
const auto end = Common::AlignUp(vaddr + size, TRACKER_PAGE_SIZE);
std::lock_guard lock(m_bda_sync_mutex);
m_bda_sync.Add(begin, end - begin);
}
RangeSet MemoryTracker::TakeBdaSync() {
std::lock_guard lock(m_bda_sync_mutex);
return std::exchange(m_bda_sync, RangeSet {});
}
bool MemoryTracker::IsRegionCpuModified(uint64_t vaddr, uint64_t size) {
CheckNotInUploadCallback();
return Iterate<true>(vaddr, size, [](RegionManager* manager, uint64_t offset, uint64_t bytes) {
@@ -100,7 +88,6 @@ void MemoryTracker::MarkRegionAsCpuModified(uint64_t vaddr, uint64_t size) {
std::scoped_lock lock(manager->lock);
manager->ChangeState<DirtySource::Cpu, true>(manager->GetCpuAddr() + offset, bytes);
});
QueueBdaSync(vaddr, size);
}
void MemoryTracker::MarkRegionAsGpuModified(uint64_t vaddr, uint64_t size) {
@@ -141,8 +128,6 @@ void MemoryTracker::UntrackMemory(uint64_t vaddr, uint64_t size) {
Iterate<false>(vaddr, size, [](RegionManager* manager, uint64_t offset, uint64_t bytes) {
manager->ChangeState<DirtySource::Cpu, true>(manager->GetCpuAddr() + offset, bytes);
});
locks.clear();
QueueBdaSync(vaddr, size);
}
} // namespace Libs::Graphics
-7
View File
@@ -29,10 +29,6 @@ public:
void MarkRegionAsGpuModified(uint64_t vaddr, uint64_t size);
void UnmarkRegionAsGpuModified(uint64_t vaddr, uint64_t size);
void UntrackMemory(uint64_t vaddr, uint64_t size);
// Queue after changing ownership; a concurrent drain must not consume an event
// before the dirty pages it describes become visible.
void QueueBdaSync(uint64_t vaddr, uint64_t size);
[[nodiscard]] RangeSet TakeBdaSync();
// Removes protection from a range and flushes GPU-owned data when required.
template <typename Flush>
void InvalidateRegion(uint64_t vaddr, uint64_t size, Flush&& on_flush) noexcept {
@@ -55,7 +51,6 @@ public:
on_flush();
}
});
QueueBdaSync(vaddr, size);
}
#if KYTY_BUILD == KYTY_BUILD_DEBUG
void ValidateGpuDirtyPages(const RangeSet& dirty, uint64_t vaddr, uint64_t size,
@@ -154,8 +149,6 @@ private:
std::unique_ptr<std::atomic<RegionManager*>[]> m_regions;
std::vector<std::unique_ptr<RegionManager>> m_region_storage;
std::mutex m_region_mutex;
std::mutex m_bda_sync_mutex;
RangeSet m_bda_sync;
PageManager& m_page_manager;
};
+13 -9
View File
@@ -127,7 +127,7 @@ void BufferCache::ChangeRegister(BufferId id) {
}
WriteDataBuffer(m_bda_pagetable_buffer, table_offset,
addresses.data(), addresses.size() * sizeof(vk::DeviceAddress));
m_memory_tracker.QueueBdaSync(buffer.CpuAddress(), buffer.Size());
QueueBdaSync(buffer.CpuAddress(), buffer.Size());
} else {
const auto found = m_buffers.find(buffer.CpuAddress());
EXIT_IF(found == m_buffers.end() || found->second != id);
@@ -299,6 +299,7 @@ void BufferCache::InvalidateMemory(uint64_t vaddr, uint64_t size) {
}
m_memory_tracker.InvalidateRegion(vaddr, size,
[this, vaddr, size] { ReadMemory(vaddr, size, true); });
QueueBdaSync(vaddr, size);
}
void BufferCache::ReadMemory(uint64_t vaddr, uint64_t size, bool is_write) {
@@ -318,6 +319,7 @@ void BufferCache::ReadMemory(uint64_t vaddr, uint64_t size, bool is_write) {
}
if (is_write) {
m_memory_tracker.MarkRegionAsCpuModified(vaddr, size);
QueueBdaSync(vaddr, size);
}
});
}
@@ -719,14 +721,16 @@ void BufferCache::ProcessFaultBuffer() {
m_fault_manager.ProcessFaultBuffer();
}
void BufferCache::SynchronizeBda(const RangeSet& mapped) {
// Take before walking: writes queued during synchronization remain pending.
const auto pending = m_memory_tracker.TakeBdaSync();
pending.ForEach([&](uint64_t begin, uint64_t end) {
mapped.ForEachInRange(begin, end - begin, [this](uint64_t start, uint64_t finish) {
SynchronizeBuffersInRange(start, finish - start);
});
});
void BufferCache::QueueBdaSync(uint64_t vaddr, uint64_t size) {
const auto begin = Common::AlignDown(vaddr, TRACKER_PAGE_SIZE);
const auto end = Common::AlignUp(vaddr + size, TRACKER_PAGE_SIZE);
std::lock_guard lock(m_bda_sync_mutex);
m_bda_sync.Add(begin, end - begin);
}
RangeSet BufferCache::TakeBdaSync() {
std::lock_guard lock(m_bda_sync_mutex);
return std::exchange(m_bda_sync, {});
}
void BufferCache::SynchronizeBuffersInRange(uint64_t vaddr, uint64_t size) {
+8 -9
View File
@@ -13,6 +13,7 @@
#include <array>
#include <map>
#include <mutex>
#include <span>
#include <utility>
#include <vector>
@@ -85,19 +86,16 @@ public:
void MarkColorMetadataChecked(uint64_t vaddr, uint64_t size, uint32_t keys);
[[nodiscard]] bool IsColorMetadataChecked(uint64_t vaddr, uint64_t size, uint32_t keys) const;
void ProcessFaultBuffer();
void SynchronizeBda(const RangeSet& mapped);
void QueueBdaSync(uint64_t vaddr, uint64_t size) {
m_memory_tracker.QueueBdaSync(vaddr, size);
}
void SynchronizeBuffersInRange(uint64_t vaddr, uint64_t size);
// Queue after the range's state change becomes visible, or a drain could consume it first.
void QueueBdaSync(uint64_t vaddr, uint64_t size);
[[nodiscard]] RangeSet TakeBdaSync();
void RunGarbageCollector();
private:
friend struct BufferCacheTestAccess;
bool IsBufferInvalid(BufferId id) const {
const auto* buffer = m_slot_buffers.try_get(id);
return buffer == nullptr || buffer->is_deleted;
}
bool IsBufferInvalid(BufferId id) const { return !id || m_slot_buffers[id].is_deleted; }
using BufferMap = std::map<uint64_t, BufferId>;
struct OverlapResult {
@@ -122,7 +120,6 @@ private:
void DeleteBuffer(BufferId id);
[[nodiscard]] bool SynchronizeBuffer(Buffer& buffer, uint64_t vaddr, uint64_t size,
bool is_written, bool is_texel_buffer);
void SynchronizeBuffersInRange(uint64_t vaddr, uint64_t size);
[[nodiscard]] vk::Buffer UploadCopies(Buffer& buffer, std::span<vk::BufferCopy> copies,
uint64_t total_size);
[[nodiscard]] bool SynchronizeBufferFromImage(Buffer& buffer, uint64_t vaddr, uint64_t size);
@@ -144,6 +141,8 @@ private:
// One per TileManager::ColorClearSlots key slot.
std::array<RangeSet, 8> m_checked_color_metadata;
MemoryTracker m_memory_tracker;
std::mutex m_bda_sync_mutex;
RangeSet m_bda_sync;
StreamBuffer m_staging_buffer;
StreamBuffer m_stream_buffer;
StreamBuffer m_download_buffer;
+52 -75
View File
@@ -251,24 +251,24 @@ void TextureCache::UnregisterImage(ImageId id) {
}
void TextureCache::DeleteImage(ImageId id) {
auto* image = m_slot_images.try_get(id);
if (image == nullptr || !image->registered) {
auto& image = m_slot_images[id];
if (!image.registered) {
return;
}
if (image->depth_id) {
std::erase(m_slot_images[image->depth_id].stencil_ids, id);
if (image.depth_id) {
std::erase(m_slot_images[image.depth_id].stencil_ids, id);
} else {
// Detach first: deleting each association also unlinks it from its depth owner.
for (const auto association: std::exchange(image->stencil_ids, {})) {
for (const auto association: std::exchange(image.stencil_ids, {})) {
FreeImage(association);
}
}
if (image->IsGpuModified()) {
if (image.IsGpuModified()) {
EXIT("TextureCache: deleting a GPU-modified image without resolving its contents\n");
}
m_download_images.erase(id);
if (image->info.metadata.kind == ImageMetadataKind::Htile) {
m_surface_metas.erase(image->info.metadata.range.address);
if (image.info.metadata.kind == ImageMetadataKind::Htile) {
m_surface_metas.erase(image.info.metadata.range.address);
}
UnregisterImage(id);
if (m_scheduler.Active()) {
@@ -429,12 +429,6 @@ TextureCache::ImageIds TextureCache::FindImagesInRegion(uint64_t address, uint64
return {};
}
uint32_t query_epoch = ++m_image_query_epoch;
if (query_epoch == 0) {
m_slot_images.ForEach([](ImageId, const Image& image) { image.query_epoch = 0; });
query_epoch = ++m_image_query_epoch;
}
ImageIds result;
ForEachPage(address, size, [&](uint64_t page) {
const auto* owners = m_image_page_table.Find(page);
@@ -442,19 +436,17 @@ TextureCache::ImageIds TextureCache::FindImagesInRegion(uint64_t address, uint64
return;
}
owners->ForEach([&](ImageId id) {
auto* image = m_slot_images.try_get(id);
if (image == nullptr) {
const auto& image = m_slot_images[id];
if (image.picked || !image.Overlaps(address, size, page_overlap)) {
return;
}
if (image->query_epoch == query_epoch) {
return;
}
image->query_epoch = query_epoch;
if (image->Overlaps(address, size, page_overlap)) {
result.push_back(id);
}
image.picked = true;
result.push_back(id);
});
});
for (const auto id: result) {
m_slot_images[id].picked = false;
}
return result;
}
@@ -787,15 +779,14 @@ void Resolve(TextureCache& cache, ImageId cached_id, ImageId merged_id) {
TextureCache::OverlapResult TextureCache::ResolveOverlap(const ImageInfo& requested,
BindingType binding, ImageId cached_id,
ImageId merged_id) {
auto owner = m_slot_images.try_get(cached_id);
if (owner == nullptr || !owner->registered) {
auto& cached = m_slot_images[cached_id];
if (!cached.registered) {
return {merged_id};
}
if (BcAlias::Matches(requested, owner->info) && !owner->IsCpuDirty()) {
if (BcAlias::Matches(requested, cached.info) && !cached.IsCpuDirty()) {
BcAlias::Resolve(*this, cached_id, merged_id);
return {merged_id};
}
auto& cached = *owner;
const auto current_tick = m_scheduler.CurrentTick();
const bool safe_to_delete =
current_tick - std::min(current_tick, cached.tick_accessed_last) > NumFramesBeforeRemoval &&
@@ -869,9 +860,8 @@ TextureCache::OverlapResult TextureCache::ResolveOverlap(const ImageInfo& reques
if (requested.pixel_format != cached.info.pixel_format ||
requested.data.size <= cached.info.data.size) {
const auto result_id = merged_id ? merged_id : cached_id;
const auto result = m_slot_images.try_get(result_id);
return {result != nullptr && ImageViewOps::FormatsCompatible(result->info.pixel_format,
requested.pixel_format)
return {ImageViewOps::FormatsCompatible(m_slot_images[result_id].info.pixel_format,
requested.pixel_format)
? result_id
: ImageId {}};
}
@@ -1218,9 +1208,8 @@ ImageId TextureCache::AssociateStencil(ImageId depth_id, GuestRange stencil) {
ImageId association {};
for (const auto id: FindImagesInRegion(stencil.address, stencil.size, false)) {
const auto owner = m_slot_images.try_get(id);
if (owner != nullptr && owner->info.data == stencil &&
owner->info.extent == depth.info.extent) {
const auto& owner = m_slot_images[id];
if (owner.info.data == stencil && owner.info.extent == depth.info.extent) {
association = id;
}
}
@@ -1315,10 +1304,9 @@ ImageId TextureCache::FindImage(ImageDesc& desc, bool exact_format) {
}
} else if (block_alias) {
for (const auto candidate: candidates) {
const auto* owner = m_slot_images.try_get(candidate);
if (candidate != result && owner != nullptr && owner->registered &&
!owner->IsCpuDirty() &&
BcAlias::Matches(m_slot_images[result].info, owner->info)) {
const auto& owner = m_slot_images[candidate];
if (candidate != result && owner.registered && !owner.IsCpuDirty() &&
BcAlias::Matches(m_slot_images[result].info, owner.info)) {
BcAlias::Resolve(*this, candidate, result);
}
}
@@ -1381,14 +1369,14 @@ ImageId TextureCache::FindImageFromRange(uint64_t address, uint64_t size, bool e
std::scoped_lock lock {m_lock};
ImageIds matches;
for (const auto id: FindImagesInRegion(address, size, false)) {
auto owner = m_slot_images.try_get(id);
if (owner == nullptr || owner->info.data.address != address) {
const auto* owner = &m_slot_images[id];
if (owner->info.data.address != address) {
continue;
}
if (ensure_valid && owner->depth_id) {
owner = m_slot_images.try_get(owner->depth_id);
owner = &m_slot_images[owner->depth_id];
}
if (owner == nullptr || (ensure_valid && !owner->SafeToDownload())) {
if (ensure_valid && !owner->SafeToDownload()) {
continue;
}
matches.push_back(id);
@@ -1405,11 +1393,8 @@ ImageId TextureCache::FindImageFromRange(uint64_t address, uint64_t size, bool e
}
}
}
if (selected && ensure_valid) {
const auto owner = m_slot_images.try_get(selected);
if (owner != nullptr && owner->depth_id) {
selected = owner->depth_id;
}
if (selected && ensure_valid && m_slot_images[selected].depth_id) {
selected = m_slot_images[selected].depth_id;
}
return selected;
}
@@ -1430,9 +1415,8 @@ vk::ImageView TextureCache::FindTexture(ImageId id, const ImageDesc& desc) {
desc.info.data.End() <= image.info.stencil.End()) {
for (const auto stencil_id:
FindImagesInRegion(image.info.stencil.address, image.info.stencil.size, false)) {
const auto* stencil = m_slot_images.try_get(stencil_id);
if (stencil != nullptr && stencil->depth_id == id &&
stencil->info.data == image.info.stencil) {
const auto& stencil = m_slot_images[stencil_id];
if (stencil.depth_id == id && stencil.info.data == image.info.stencil) {
RefreshImage(stencil_id);
break;
}
@@ -1543,27 +1527,24 @@ bool TextureCache::ClearImageFromBuffer(CommandBuffer& command, uint64_t address
vk::ImageAspectFlags aspect {};
ImageSubresourceRange subresources;
for (const auto id: FindImagesInRegion(address, size, false)) {
auto owner = m_slot_images.try_get(id);
if (owner == nullptr) {
continue;
}
const auto& owner = m_slot_images[id];
vk::ImageAspectFlags candidate {};
ImageId candidate_id = id;
ImageSubresourceRange candidate_subresources {
0, owner->info.resources.levels, 0, owner->info.TransferLayers()};
if (owner->depth_id && owner->info.data.address == address &&
owner->info.data.size == size) {
0, owner.info.resources.levels, 0, owner.info.TransferLayers()};
if (owner.depth_id && owner.info.data.address == address &&
owner.info.data.size == size) {
candidate = vk::ImageAspectFlagBits::eStencil;
candidate_id = owner->depth_id;
candidate_subresources = owner->stencil_subresources;
owner = m_slot_images.try_get(candidate_id);
if (owner == nullptr || owner->backing.image == nullptr || !owner->info.HasStencil()) {
candidate_id = owner.depth_id;
candidate_subresources = owner.stencil_subresources;
const auto& depth = m_slot_images[candidate_id];
if (depth.backing.image == nullptr || !depth.info.HasStencil()) {
continue;
}
} else if (!owner->depth_id && owner->info.data.address == address &&
owner->info.data.size == size) {
candidate = owner->info.IsDepth() ? vk::ImageAspectFlagBits::eDepth
: vk::ImageAspectFlagBits::eColor;
} else if (!owner.depth_id && owner.info.data.address == address &&
owner.info.data.size == size) {
candidate = owner.info.IsDepth() ? vk::ImageAspectFlagBits::eDepth
: vk::ImageAspectFlagBits::eColor;
}
if (!candidate) {
continue;
@@ -1904,23 +1885,20 @@ void TextureCache::InvalidateCpuAliases(uint64_t address, uint64_t size) {
const auto page_begin = Common::AlignDown(address, TRACKER_PAGE_SIZE);
const auto page_end = Common::AlignUp(address + size, TRACKER_PAGE_SIZE);
for (const auto id: FindImagesInRegion(address, size, true)) {
auto owner = m_slot_images.try_get(id);
if (owner == nullptr) {
continue;
}
if (owner->Overlaps(address, size)) {
owner->MarkCpuDirty();
auto& owner = m_slot_images[id];
if (owner.Overlaps(address, size)) {
owner.MarkCpuDirty();
UntrackImage(id);
continue;
}
const auto image_begin = owner->info.data.address;
const auto image_end = owner->info.data.End();
const auto image_begin = owner.info.data.address;
const auto image_end = owner.info.data.End();
if (page_end < image_end) {
UntrackImageHead(id);
} else if (image_begin < page_begin) {
UntrackImageTail(id);
} else {
MarkAsMaybeDirty(id, *owner);
MarkAsMaybeDirty(id, owner);
}
}
}
@@ -2060,8 +2038,7 @@ void TextureCache::ProcessDownloadImages() {
}
std::scoped_lock lock {m_lock};
for (const auto id: m_download_images) {
const auto owner = m_slot_images.try_get(id);
if (owner != nullptr && owner->registered && owner->IsGpuModified()) {
if (m_slot_images[id].IsGpuModified()) {
(void)DownloadImageMemory(id);
}
}
-1
View File
@@ -184,7 +184,6 @@ private:
uint64_t m_pressure_gc_memory = 1536ull * 1024 * 1024;
uint64_t m_critical_gc_memory = 3ull * 1024 * 1024 * 1024;
uint64_t m_gc_tick = 0;
mutable uint32_t m_image_query_epoch = 0;
bool m_readback_linear_images = false;
bool m_readback_tiled_images = false;
-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);
+4 -1
View File
@@ -139,14 +139,17 @@ public:
}
static inline std::atomic<uint64_t> heap_epoch {0};
static inline std::atomic<uint64_t> serials {0};
// Unique per image, unlike ImageId: freed slots are reused.
const uint64_t serial = serials.fetch_add(1u, std::memory_order_relaxed) + 1u;
ImageInfo info;
VulkanImage backing;
std::vector<CachedImageView> views;
ImageUsage usage;
ImageBinding binding;
bool registered = false;
mutable uint32_t query_epoch = 0;
mutable bool picked = false; // Set only while FindImagesInRegion collects it.
uint64_t track_addr = 0;
uint64_t track_addr_end = 0;
ImageId depth_id {};
@@ -672,9 +672,7 @@ void RenderExecutor::BindRenderTarget(ImageId id) {
void RenderExecutor::ResetBindings() {
for (const auto id: m_bound_images) {
if (auto* image = m_context.GetTextureCache().m_slot_images.try_get(id); image != nullptr) {
image->binding = {};
}
m_context.GetTextureCache().m_slot_images[id].binding = {};
}
m_bound_images.clear();
}
@@ -683,7 +681,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};
@@ -725,7 +723,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)]);
@@ -782,7 +780,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)]);
@@ -815,7 +813,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());
@@ -823,12 +821,9 @@ void RenderExecutor::RebindImages(PreparedBindings& prepared) {
// Acquire each view before a later overlapping descriptor can replace its image.
for (uint32_t i = 0; i < program.info.images.size(); i++) {
if (!images[i].image_id) continue;
const auto old_image = texture_cache.m_slot_images.try_get(images[i].image_id);
if (old_image == nullptr || !old_image->registered ||
old_image->binding.needs_rebind) {
if (old_image != nullptr) {
old_image->binding = {};
}
auto& old_image = texture_cache.m_slot_images[images[i].image_id];
if (!old_image.registered || old_image.binding.needs_rebind) {
old_image.binding = {};
images[i] = ResolveTexture(program.info.images[i], snapshot.images[i]);
BindImage(images[i].image_id,
images[i].desc.type == TextureCache::BindingType::Storage);
@@ -864,8 +859,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();
@@ -879,12 +874,9 @@ void RenderExecutor::PrepareGraphicsBindings(std::span<PreparedBindings* const>
auto& cache = m_context.GetTextureCache();
for (auto& target: colors) {
EXIT_IF(!target.image_id);
const auto old_image = cache.m_slot_images.try_get(target.image_id);
if (old_image == nullptr || !old_image->registered ||
old_image->binding.needs_rebind) {
if (old_image != nullptr) {
old_image->binding = {};
}
auto& old_image = cache.m_slot_images[target.image_id];
if (!old_image.registered || old_image.binding.needs_rebind) {
old_image.binding = {};
target.desc.view_info.base_level = target.guest_mip_level;
target.desc.view_info.base_layer = target.guest_array_layer;
target.image_id = cache.FindImage(target.desc);
@@ -917,7 +909,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);
@@ -938,17 +930,16 @@ 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");
}
}
for (const auto [address, size]: reads) {
for (const auto id: m_bound_images) {
const auto* image = m_context.GetTextureCache().m_slot_images.try_get(id);
if (image == nullptr ||
(!image->binding.shader_write && !image->binding.is_target)) continue;
for (const auto written: {image->info.data, image->info.stencil,
image->info.metadata.range}) {
const auto& image = m_context.GetTextureCache().m_slot_images[id];
if (!image.binding.shader_write && !image.binding.is_target) continue;
for (const auto written: {image.info.data, image.info.stencil,
image.info.metadata.range}) {
if (written.size != 0 && ImageRangeOverlaps(address, size,
written.address, written.size)) {
EXIT("scalar resource reads overlap an image or attachment write\n");
@@ -956,7 +947,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];
@@ -982,7 +973,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);
@@ -350,12 +350,17 @@ void ImageHeap::Release(View& view, uint32_t entry) {
}
void ImageHeap::Unreference(Element& element) {
if (auto* image = m_context.GetTextureCache().m_slot_images.try_get(element.binding.image_id)) {
if (auto* image = ImageOf(element)) {
image->heap_refs.fetch_sub(1u, std::memory_order_relaxed);
image->tick_accessed_last = m_context.GetCommandScheduler().CurrentTick();
}
}
Image* ImageHeap::ImageOf(const Element& element) const {
auto* image = m_context.GetTextureCache().m_slot_images.try_get(element.binding.image_id);
return image != nullptr && image->serial == element.serial ? image : nullptr;
}
uint32_t ImageHeap::Allocate() {
auto& scheduler = m_context.GetCommandScheduler();
while (!m_retired.empty() && scheduler.IsFree(m_retired.front().first)) {
@@ -393,7 +398,7 @@ void ImageHeap::Rebind(RenderExecutor& executor) {
for (const auto& [value, index]: view->elements) {
const auto& binding = m_elements[index].binding;
if (!binding.image_id) continue;
const auto* image = textures.m_slot_images.try_get(binding.image_id);
const auto* image = ImageOf(m_elements[index]);
if (image == nullptr || !image->registered || image->depth_id ||
image->binding.needs_rebind) {
mark(index);
@@ -434,6 +439,7 @@ void ImageHeap::Rebind(RenderExecutor& executor) {
if (binding.image_id) {
auto& image = textures.GetImage(binding.image_id);
image.heap_refs.fetch_add(1u, std::memory_order_relaxed);
m_elements[index].serial = image.serial;
binding.layout = image.ReadLayout();
binding.image_view = textures.FindTexture(binding.image_id, binding.desc);
if (binding.desc.info.metadata.kind == ImageMetadataKind::Dcc) {
@@ -471,8 +477,7 @@ void ImageHeap::WriteDescriptors() {
void ImageHeap::Commit(CommandBuffer& buffer, vk::PipelineBindPoint bind_point,
vk::PipelineLayout layout) {
KYTY_PROFILER_FUNCTION();
auto& textures = m_context.GetTextureCache();
const auto epoch = Image::heap_epoch.load(std::memory_order_relaxed);
const auto epoch = Image::heap_epoch.load(std::memory_order_relaxed);
for (auto* view: m_active) {
if (view->dirty_begin != view->dirty_end) {
const auto slots = std::span(view->slots).subspan(view->dirty_begin,
@@ -493,7 +498,7 @@ void ImageHeap::Commit(CommandBuffer& buffer, vk::PipelineBindPoint bind_point,
// this draw's discovery replaced lives until the tick ends, and its slots still select it.
std::erase_if(view->away, [&](uint32_t index) {
const auto& element = m_elements[index];
auto* image = textures.m_slot_images.try_get(element.binding.image_id);
auto* image = ImageOf(element);
if (element.refs == 0u || image == nullptr) return true;
if (image->binding.is_target || image->binding.shader_write ||
image->binding.force_general) {
@@ -57,9 +57,10 @@ private:
struct Element {
DescriptorValue value;
TextureBinding binding;
uint32_t kind = 0;
uint32_t refs = 0; // Slots of the owning view that select it.
bool stale = false;
uint32_t kind = 0;
uint32_t refs = 0; // Slots of the owning view that select it.
bool stale = false;
uint64_t serial = 0; // Image::serial of binding.image_id.
};
struct Memory;
@@ -109,6 +110,7 @@ private:
uint32_t Intern(View& view, uint32_t kind, const DescriptorValue& value);
void Release(View& view, uint32_t entry);
void Unreference(Element& element);
Image* ImageOf(const Element& element) const;
uint32_t Allocate();
void Retire(View& view);
void WriteDescriptors();
File diff suppressed because it is too large. Load diff
@@ -4,42 +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 RenderColorInfo;
struct RenderDepthInfo;
struct ShaderParams;
class CommandBuffer;
namespace HW {
class Context;
class Shader;
class UserConfig;
struct ComputeShaderInfo;
} // namespace HW
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;
@@ -58,57 +73,108 @@ 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.
struct DrawRenderState {
RenderDepthInfo depth_info;
RenderColorInfo color_info[RENDER_COLOR_ATTACHMENTS_MAX] = {};
uint32_t color_count = 0;
// Color targets the pixel program writes.
uint32_t mrt_mask = 0;
vk::PrimitiveTopology topology = vk::PrimitiveTopology::ePointList;
bool primitive_restart_enable = false;
bool ps_active = false;
std::array<ShaderVertexInputInfo, 3> vertex_info;
ShaderPixelInputInfo ps_input_info;
[[nodiscard]] std::span<const ShaderVertexInputInfo> VertexStages() const {
return {vertex_info.data(), vertex_info[1].stage ? 3u : 1u};
}
};
// 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();
@@ -121,87 +187,106 @@ public:
bool uses_image_heap = false;
};
struct GraphicsPrograms {
std::array<ShaderProgram, 3> vertex;
ShaderProgram pixel;
// Reads the shaders and registers bound in `command` into the state's shader stages.
void GetGraphicsPrograms(const CommandBuffer& command, DrawRenderState& state);
// Null skips the dispatch.
const ShaderModule* GetComputeProgram(const HW::ComputeShaderInfo& regs,
ShaderComputeInputInfo& input_info);
[[nodiscard]] uint32_t VertexStageCount() const { return vertex[1] ? 3u : 1u; }
};
GraphicsPrograms
GetGraphicsPrograms(const HW::VertexShaderInfo& vertex_regs,
const HW::PixelShaderInfo& pixel_regs, const HW::ShaderRegisters& sh,
const HW::Context& context, const HW::UserConfig& user_config,
std::span<const Prospero::ColorComponentMapping, 8> target_export_mapping,
bool pixel_active, std::array<ShaderVertexInputInfo, 3>& vertex_info,
ShaderPixelInputInfo& pixel_info);
ShaderProgram GetComputeProgram(const HW::ComputeShaderInfo& regs,
const HW::ShaderRegisters& sh,
ShaderComputeInputInfo& input_info);
Pipeline& GetGraphicsPipeline(std::span<const RenderColorInfo> colors,
const RenderDepthInfo& depth,
std::span<const ShaderVertexInputInfo> vertex_info,
CommandBuffer& command, const ShaderPixelInputInfo* ps_input_info,
vk::PrimitiveTopology topology, bool primitive_restart_enable,
const GraphicsPrograms& programs);
Pipeline& GetComputePipeline(const ShaderComputeInputInfo& input_info,
const ShaderProgram& compute_program);
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, std::unique_ptr<Pipeline>, GraphicsPipelineKeyHash>
m_graphics_pipelines;
std::unordered_map<uint64_t, std::unique_ptr<Pipeline>> m_compute_pipelines;
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,
std::span<const ShaderVertexInputInfo> vertex_info,
const ShaderPixelInputInfo* ps_input_info,
const PipelineCache::GraphicsPrograms& programs,
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,33 +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,
std::span<const ShaderVertexInputInfo> vertex_info,
const ShaderPixelInputInfo* ps_input_info,
const PipelineCache::GraphicsPrograms& programs,
const PipelineStaticParameters& static_params,
vk::PipelineCache driver_cache) {
const auto& vs_input_info = vertex_info.front();
const auto& vertex_program = programs.vertex[0];
const auto& pixel_program = programs.pixel;
const bool tessellation = vertex_info.size() == 3;
const bool ps_active = ps_input_info != nullptr;
EXIT_IF(!vertex_program || (ps_active && !pixel_program));
const bool with_depth = rendering.depth_format != vk::Format::eUndefined ||
rendering.stencil_format != vk::Format::eUndefined;
EXIT_IF(!vs_input_info.stage);
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_active ? ps_input_info : nullptr);
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",
@@ -287,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 = programs.vertex[i].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 =
@@ -303,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] {};
@@ -323,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 {};
@@ -470,17 +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) {
EXIT_IF(!ps_input_info->stage);
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,
@@ -498,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);
@@ -508,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 {
@@ -555,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;
@@ -573,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);
@@ -586,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;
@@ -616,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};
@@ -632,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;
@@ -652,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
+2 -8
View File
@@ -23,14 +23,9 @@ class Shader;
} // namespace HW
struct GraphicContext;
struct ShaderBufferResource;
struct ShaderComputeInputInfo;
struct RenderDepthInfo;
struct RenderColorInfo;
struct DrawCallInfo;
struct DrawEmitInfo;
struct DrawIndexBufferSource;
struct DrawRenderState;
class RenderContext;
class CommandScheduler;
struct RenderExecutorTestAccess;
@@ -198,9 +193,8 @@ private:
uint32_t render_target_slice_offset,
DrawRenderState& state);
void ExecutePreparedDraw(uint64_t submit_id, CommandBuffer& buffer, const DrawCallInfo& draw,
DrawRenderState& state, vk::PrimitiveTopology topology,
const DrawEmitInfo& emit, const DrawIndexBufferSource& index_source,
bool primitive_restart_enable);
DrawRenderState& state, const DrawEmitInfo& emit,
const DrawIndexBufferSource& index_source);
[[nodiscard]] RenderState AcquireRenderTargets(CommandBuffer& buffer, RenderColorInfo* colors,
uint32_t color_count, RenderDepthInfo& depth,
vk::ImageAspectFlags& feedback_aspects,
@@ -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;
}
@@ -248,7 +248,6 @@ void RenderExecutor::DispatchDirect(uint64_t submit_id, CommandBuffer& buffer,
uint32_t thread_group_z, uint32_t mode, bool async_compute) {
EXIT_IF(buffer.IsInvalid());
m_context.GetCommandScheduler().PopPendingOperations();
auto& ctx = buffer.GetRegisters();
auto& sh_ctx = buffer.GetShaders();
buffer.SetDebugInfo(static_cast<uint32_t>(CommandBufferDebugOp::DispatchDirect), submit_id,
@@ -293,7 +292,6 @@ void RenderExecutor::DispatchDirect(uint64_t submit_id, CommandBuffer& buffer,
}
const auto& cs_regs = sh_ctx.GetCs();
const auto& sh_regs = ctx.GetShaderRegisters();
ShaderComputeInputInfo input_info {};
input_info.async_compute = async_compute;
@@ -312,16 +310,14 @@ void RenderExecutor::DispatchDirect(uint64_t submit_id, CommandBuffer& buffer,
}
}
m_context.GetImageHeap().BeginDraw();
const auto compute_program =
m_context.GetPipelineCache().GetComputeProgram(cs_regs, sh_regs, input_info);
if (!compute_program) return;
if (!m_context.GetPipelineCache().GetComputeProgram(cs_regs, input_info)) return;
if (use_thread_dimensions) {
input_info.dispatch_threads_num[0] = thread_group_x;
input_info.dispatch_threads_num[1] = thread_group_y;
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) ||
@@ -351,8 +347,7 @@ void RenderExecutor::DispatchDirect(uint64_t submit_id, CommandBuffer& buffer,
}
buffer.EndRendering();
auto& pipeline =
m_context.GetPipelineCache().GetComputePipeline(input_info, compute_program);
auto& pipeline = m_context.GetPipelineCache().GetComputePipeline(input_info);
auto& bindings = m_compute_bindings;
PrepareBindings(input_info.stage, bindings);
if (program.bindings.dispatch_thread_dword != ShaderRecompiler::IR::PushData::NoStart) {
@@ -502,16 +497,14 @@ void RenderExecutor::DispatchIndirect(uint64_t submit_id, CommandBuffer& buffer,
input_info.async_compute = async_compute;
input_info.indirect_args_address = args_addr;
m_context.GetImageHeap().BeginDraw();
const auto compute_program = m_context.GetPipelineCache().GetComputeProgram(
cs_regs, buffer.GetRegisters().GetShaderRegisters(), input_info);
if (!compute_program) return;
if (!m_context.GetPipelineCache().GetComputeProgram(cs_regs, input_info)) return;
buffer.EndRendering();
auto& pipeline = m_context.GetPipelineCache().GetComputePipeline(input_info, compute_program);
auto& pipeline = m_context.GetPipelineCache().GetComputePipeline(input_info);
auto& bindings = m_compute_bindings;
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) {
@@ -134,7 +134,11 @@ void RenderContext::PrepareBda() {
m_bda_logged = true;
}
std::shared_lock lock(m_mapped_ranges_mutex);
m_buffer_cache.SynchronizeBda(m_mapped_ranges);
m_buffer_cache.TakeBdaSync().ForEach([this](uint64_t begin, uint64_t end) {
m_mapped_ranges.ForEachInRange(begin, end - begin, [this](uint64_t start, uint64_t finish) {
m_buffer_cache.SynchronizeBuffersInRange(start, finish - start);
});
});
m_fault_process_pending = true;
}
@@ -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);
+30 -114
View File
@@ -17,7 +17,6 @@
#include "graphics/host_gpu/renderer/colorRenderTarget.h"
#include "graphics/host_gpu/renderer/debug.h"
#include "graphics/host_gpu/renderer/depthRenderTarget.h"
#include "graphics/host_gpu/renderer/image/textureCommon.h"
#include "graphics/host_gpu/renderer/pipeline/pipelineCache.h"
#include "graphics/host_gpu/renderer/pipeline/shaderResourceBarrier.h"
#include "graphics/host_gpu/renderer/render.h"
@@ -58,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)) {
@@ -198,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;
@@ -321,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;
@@ -433,22 +432,6 @@ static bool DrawHasValidVertexShader(const HW::Shader& sh_ctx) {
return vs.es_regs.data_addr != 0;
}
static bool PixelShaderHasDepthOrCoverageSideEffects(const HW::ShaderRegisters& sh_regs) {
const auto& db = sh_regs.db_shader_control;
return db.shader_kill_enable || db.shader_z_export_enable || db.shader_mask_export_enable ||
db.shader_dual_export_enable || db.shader_execute_on_noop || sh_regs.AlphaToMask();
}
struct DrawRenderState {
RenderDepthInfo depth_info;
RenderColorInfo color_info[RENDER_COLOR_ATTACHMENTS_MAX] = {};
uint32_t color_count = 0;
bool ps_active = true;
std::array<ShaderVertexInputInfo, 3> vertex_info;
ShaderPixelInputInfo ps_input_info;
PipelineCache::GraphicsPrograms programs;
};
struct DrawCallInfo {
CommandBufferDebugOp debug_op = CommandBufferDebugOp::DrawIndex;
uint32_t index_count = 0;
@@ -474,9 +457,8 @@ RenderState RenderExecutor::AcquireRenderTargets(CommandBuffer& buffer, RenderCo
for (uint32_t i = 0; i < color_count; i++) {
auto& target = colors[i];
EXIT_IF(!target.image_id);
const auto owner = cache.m_slot_images.try_get(target.image_id);
if (owner == nullptr || (!owner->registered && !owner->info.data.Empty()) ||
owner->binding.needs_rebind) {
const auto& owner = cache.m_slot_images[target.image_id];
if ((!owner.registered && !owner.info.data.Empty()) || owner.binding.needs_rebind) {
EXIT("color target changed after render-state discovery\n");
}
const auto image_view = cache.FindRenderTarget(target.image_id, target.desc);
@@ -502,8 +484,8 @@ RenderState RenderExecutor::AcquireRenderTargets(CommandBuffer& buffer, RenderCo
attachment.image_layout = layout;
}
if (depth.image_id) {
const auto owner = cache.m_slot_images.try_get(depth.image_id);
if (owner == nullptr || !owner->registered || owner->binding.needs_rebind) {
const auto& owner = cache.m_slot_images[depth.image_id];
if (!owner.registered || owner.binding.needs_rebind) {
EXIT("depth target changed after render-state discovery\n");
}
const auto image_view = cache.FindDepthTarget(depth.image_id, depth.desc);
@@ -589,23 +571,6 @@ RenderState RenderExecutor::AcquireRenderTargets(CommandBuffer& buffer, RenderCo
return state;
}
static uint32_t DrawColorOutputMask(const HW::Context& ctx) {
const auto& sh_regs = ctx.GetShaderRegisters();
const auto write_mask = ctx.GetRenderTargetMask() & sh_regs.m_cbShaderMask;
uint32_t output_mask = ctx.HasObjectPrimitiveIdOutput() ? 1u << 7u : 0;
for (uint32_t index = 0; index < RENDER_COLOR_ATTACHMENTS_MAX; index++) {
const auto slot = ShaderPixelExportTarget(sh_regs.m_cbShaderMask, index);
if (slot >= RENDER_COLOR_ATTACHMENTS_MAX) {
break;
}
if (sh_regs.target_output_mode[index] != 0 &&
render_target_mask_slot(write_mask, slot) != 0) {
output_mask |= 1u << slot;
}
}
return output_mask;
}
enum class CbColorMode : uint8_t {
Disable = 0,
Normal = 1,
@@ -870,62 +835,19 @@ static bool ResolvePrimitiveRestart(const CommandBuffer& buffer,
return false;
}
static void RefreshShaders(CommandBuffer& buffer, const DrawCallInfo& draw,
uint32_t color_output_mask, DrawRenderState& state) {
auto& ctx = buffer.GetRegisters();
auto& sh_ctx = buffer.GetShaders();
const auto& vertex_shader_info = sh_ctx.GetVs();
const auto& pixel_shader_info = sh_ctx.GetPs();
const auto& shader_regs = ctx.GetShaderRegisters();
state.programs = {};
state.ps_input_info = {};
std::array<Prospero::ColorComponentMapping, RENDER_COLOR_ATTACHMENTS_MAX>
target_export_mapping {};
for (uint32_t slot = 0; slot < RENDER_COLOR_ATTACHMENTS_MAX; slot++) {
const auto& rt = ctx.GetRenderTarget(slot);
if ((color_output_mask & (1u << slot)) != 0 && rt.base.addr != 0) {
target_export_mapping[slot] =
TextureGetRenderTargetFormat(rt.info.format, rt.info.channel_type,
rt.info.channel_order)
.export_mapping;
}
}
auto& pipeline_cache = buffer.GetContext().GetPipelineCache();
buffer.GetContext().GetImageHeap().BeginDraw();
if (draw.IsIndexed()) {
LogDrawPhase(draw.Name(), "GetGraphicsPrograms");
}
state.programs = pipeline_cache.GetGraphicsPrograms(
vertex_shader_info, pixel_shader_info, shader_regs, ctx, buffer.GetUserConfig(),
target_export_mapping, state.ps_active, state.vertex_info, state.ps_input_info);
}
bool RenderExecutor::PrepareDrawRenderState(CommandBuffer& buffer, const DrawCallInfo& draw,
uint32_t render_target_slice_offset,
DrawRenderState& state) {
const auto& shader_regs = buffer.GetRegisters().GetShaderRegisters();
const auto color_output_mask = DrawColorOutputMask(buffer.GetRegisters());
state.ps_active = buffer.GetShaders().GetPs().ps_regs.data_addr != 0 &&
(color_output_mask != 0 ||
PixelShaderHasDepthOrCoverageSideEffects(shader_regs));
RefreshShaders(buffer, draw, color_output_mask, state);
uint32_t mrt_mask = 0;
if (state.ps_active) {
for (const auto& output: state.ps_input_info.stage.program->info.outputs) {
if (output.kind == ShaderRecompiler::IR::StageOutputKind::Mrt ||
output.kind == ShaderRecompiler::IR::StageOutputKind::ObjectPrimitiveId) {
mrt_mask |= 1u << output.location;
}
}
m_context.GetImageHeap().BeginDraw();
if (draw.IsIndexed()) {
LogDrawPhase(draw.Name(), "GetGraphicsPrograms");
}
mrt_mask &= color_output_mask;
m_context.GetPipelineCache().GetGraphicsPrograms(buffer, state);
if (draw.IsIndexed()) {
LogDrawPhase(draw.Name(), "ResolveRenderColorTarget");
}
for (uint32_t slot = 0; slot < RENDER_COLOR_ATTACHMENTS_MAX; slot++) {
if ((mrt_mask & (1u << slot)) != 0) {
if ((state.mrt_mask & (1u << slot)) != 0) {
ResolveRenderColorTarget(buffer, state.color_info[state.color_count],
render_target_slice_offset, slot);
if (state.color_info[state.color_count].image_id) {
@@ -1124,16 +1046,15 @@ bool RenderExecutor::CopyDepthToColor(CommandBuffer& buffer, const DrawCallInfo&
void RenderExecutor::ExecutePreparedDraw(uint64_t submit_id, CommandBuffer& buffer,
const DrawCallInfo& draw, DrawRenderState& state,
vk::PrimitiveTopology topology, const DrawEmitInfo& emit,
const DrawIndexBufferSource& index_source,
bool primitive_restart_enable) {
auto& ucfg = buffer.GetUserConfig();
const DrawEmitInfo& emit,
const DrawIndexBufferSource& index_source) {
auto& ucfg = buffer.GetUserConfig();
const bool primitive_restart_enable = state.primitive_restart_enable;
if (CopyDepthToColor(buffer, draw, emit, state)) {
return;
}
const auto vertex_stages =
std::span {state.vertex_info.data(), state.programs.VertexStageCount()};
const bool mesh_active = state.vertex_info[0].stage.program->stage == ShaderType::Mesh;
const auto vertex_stages = state.VertexStages();
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;
@@ -1203,10 +1124,7 @@ void RenderExecutor::ExecutePreparedDraw(uint64_t submit_id, CommandBuffer& buff
if (draw.IsIndexed()) {
LogDrawPhase(draw.Name(), "CreatePipeline");
}
auto& pipeline = m_context.GetPipelineCache().GetGraphicsPipeline(
std::span {state.color_info, state.color_count}, state.depth_info, vertex_stages, buffer,
state.ps_active ? &state.ps_input_info : nullptr, topology, primitive_restart_enable,
state.programs);
auto& pipeline = m_context.GetPipelineCache().GetGraphicsPipeline(buffer, state);
vk::ImageAspectFlags feedback_aspects;
const auto rendering =
AcquireRenderTargets(buffer, state.color_info, state.color_count, state.depth_info,
@@ -1323,7 +1241,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;
}
@@ -1388,8 +1306,8 @@ bool RenderExecutor::DrawIndex(uint64_t submit_id, CommandBuffer& buffer,
hw_check(buffer);
vk::PrimitiveTopology topology = vk::PrimitiveTopology::ePointList;
if (!GetDrawTopology(ucfg, topology)) {
DrawRenderState state {};
if (!GetDrawTopology(ucfg, state.topology)) {
return true;
}
@@ -1410,7 +1328,7 @@ bool RenderExecutor::DrawIndex(uint64_t submit_id, CommandBuffer& buffer,
default: EXIT("unknown index_type_and_size: %u\n", args.index_type_and_size);
}
index_source.size = static_cast<uint64_t>(args.index_count) * index_source.guest_element_size;
const bool primitive_restart = ResolvePrimitiveRestart(buffer, index_source);
state.primitive_restart_enable = ResolvePrimitiveRestart(buffer, index_source);
std::vector<uint16_t> expanded_indices;
if (index_source.guest_element_size == 1) {
@@ -1418,7 +1336,8 @@ bool RenderExecutor::DrawIndex(uint64_t submit_id, CommandBuffer& buffer,
const auto* src = static_cast<const uint8_t*>(args.index_addr);
expanded_indices.resize(args.index_count);
for (uint32_t i = 0; i < args.index_count; i++) {
expanded_indices[i] = primitive_restart && src[i] == 0xffu ? 0xffffu : src[i];
expanded_indices[i] =
state.primitive_restart_enable && src[i] == 0xffu ? 0xffffu : src[i];
}
index_source.host_data = expanded_indices.data();
index_source.size = expanded_indices.size() * sizeof(uint16_t);
@@ -1426,7 +1345,6 @@ bool RenderExecutor::DrawIndex(uint64_t submit_id, CommandBuffer& buffer,
const DrawCallInfo draw {CommandBufferDebugOp::DrawIndex, args.index_count,
args.instance_count, args.first_instance};
DrawRenderState state {};
if (!PrepareDrawRenderState(buffer, draw, args.render_target_slice_offset, state)) {
ResetBindings();
return true;
@@ -1437,8 +1355,7 @@ bool RenderExecutor::DrawIndex(uint64_t submit_id, CommandBuffer& buffer,
const auto emit = ResolveDrawEmit(ucfg, state.vertex_info[0], args.offset_source,
args.base_vertex, args.first_instance, args.indirect);
if (emit) {
ExecutePreparedDraw(submit_id, buffer, draw, state, topology, *emit, index_source,
primitive_restart);
ExecutePreparedDraw(submit_id, buffer, draw, state, *emit, index_source);
}
ResetBindings();
return emit.has_value();
@@ -1494,12 +1411,11 @@ bool RenderExecutor::DrawAuto(uint64_t submit_id, CommandBuffer& buffer, const D
const DrawCallInfo draw {CommandBufferDebugOp::DrawIndexAuto,
args.vertex_count, args.instance_count, args.first_instance};
vk::PrimitiveTopology topology = vk::PrimitiveTopology::ePointList;
if (!GetDrawTopology(ucfg, topology)) {
DrawRenderState state {};
if (!GetDrawTopology(ucfg, state.topology)) {
ResetBindings();
return true;
}
DrawRenderState state {};
if (!PrepareDrawRenderState(buffer, draw, args.render_target_slice_offset, state)) {
ResetBindings();
return true;
@@ -1507,7 +1423,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: "
@@ -1524,7 +1440,7 @@ bool RenderExecutor::DrawAuto(uint64_t submit_id, CommandBuffer& buffer, const D
static_cast<int32_t>(args.first_vertex), args.first_instance,
args.indirect);
if (emit) {
ExecutePreparedDraw(submit_id, buffer, draw, state, topology, *emit, {}, false);
ExecutePreparedDraw(submit_id, buffer, draw, state, *emit, {});
}
ResetBindings();
return emit.has_value();
@@ -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()),
@@ -584,14 +567,30 @@ void PrepareCallTargets(ShaderSource& source, const CompileOptions& options) {
for (auto& inst: block) {
inst.SetParent(nullptr);
if (inst.GetOpcode() == ValueOpcode::LoadAddressU32 || inst.GetOpcode() == ValueOpcode::ReadConstBuffer)
inst.SetFlags(SrtReadFlags {.index = inst.Flags<MemoryFlags>().index});
inst.SetFlags(MemoryFlags {.index = inst.Flags<MemoryFlags>().index});
}
query.value_storage.splice(query.value_storage.end(), block.Instructions());
source.call_targets = std::move(static_cast<ResourcePlan&>(query));
std::vector<Value> targets;
for (const auto& site: source.calls) targets.insert(targets.end(), site.target.begin(), site.target.end());
CompileSrtWalker(source.call_targets, targets);
}
} // 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;
@@ -617,12 +616,18 @@ const Decoder::Program& RefreshShaderSource(ShaderSource& source, const IR::SrtR
EXIT_IF(source.unsupported_call_pc != UINT32_MAX);
auto& reads = source.reads;
reads.clear();
IR::SrtReadCapture capture(runtime, reads);
const auto clean = IR::CleanRuntime(capture.ObservedRuntime());
IR::SrtWalker walker(source.call_targets, clean);
std::vector<uint32_t> flat;
const bool walked = IR::WalkSrt(source.call_targets, runtime, flat, &reads);
const auto read_code = [&](uint64_t address, std::span<uint32_t> words) {
if (runtime.read_specialization_memory == nullptr ||
!runtime.read_specialization_memory(runtime.userdata, address, words)) return false;
reads.emplace_back(address, words.size_bytes());
return true;
};
for (auto& site: source.calls) {
uint32_t low = 0, high = 0;
if (!walker.Evaluate(site.target[0], low) || !walker.Evaluate(site.target[1], high))
if (!walked || !IR::WalkedValue(source.call_targets, site.target[0], low) ||
!IR::WalkedValue(source.call_targets, site.target[1], high))
EXIT("shader call at pc 0x%08x has an unavailable scalar target",
source.decoded.instructions[site.instruction].pc);
// SWAPPC reads the old aliased pair and ignores the target's low two bits.
@@ -646,7 +651,7 @@ const Decoder::Program& RefreshShaderSource(ShaderSource& source, const IR::SrtR
break;
}
linked.observed_function.resize(function.size);
if (!clean.read_memory(clean.userdata, source.calls[i].address, linked.observed_function) ||
if (!read_code(source.calls[i].address, linked.observed_function) ||
!std::ranges::equal(expected, linked.observed_function)) {
unchanged = false;
break;
@@ -676,8 +681,7 @@ const Decoder::Program& RefreshShaderSource(ShaderSource& source, const IR::SrtR
uint32_t count = 1;
Decoder::Instruction inst;
for (;;) {
if (!clean.read_memory(clean.userdata, address + uint64_t {index} * sizeof(uint32_t),
std::span(words).first(count)))
if (!read_code(address + uint64_t {index} * sizeof(uint32_t), std::span(words).first(count)))
EXIT("shader call at pc 0x%08x cannot read leaf code", call.pc);
inst = {};
Decoder::DecodeInstruction(words, 0u, inst);
@@ -756,6 +760,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>(
@@ -765,27 +770,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;
@@ -795,12 +798,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());
}
@@ -813,7 +816,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()));
}
}
@@ -826,12 +829,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] {
@@ -842,8 +845,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();
@@ -857,10 +861,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;
@@ -877,6 +879,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.
@@ -923,19 +926,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;
@@ -69,6 +67,7 @@ struct ShaderSource {
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));
}
@@ -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;
}
@@ -816,27 +843,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 +875,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) {
@@ -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)
@@ -531,6 +531,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);
@@ -472,6 +472,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));
@@ -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,6 +123,12 @@ 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)));
@@ -140,4 +139,12 @@ IR::U32 Translator::PackU16Lanes(IR::U32 low, IR::U32 high) {
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);
}
@@ -114,6 +114,7 @@ private:
IR::F32 SelectF32(IR::U1 condition, IR::F32 true_value, IR::F32 false_value);
IR::U32 PackU16Lanes(IR::U32 low, IR::U32 high);
IR::U32 Saturate16(IR::U32 value, bool signed_value);
void EmitCompareResult(const Decoder::Instruction& inst, IR::U1 value, bool scalar, bool cmpx);
void EmitCompareConstant(const Decoder::Instruction& inst, bool value, bool scalar, bool cmpx);
void EmitIntegerCompare(const Decoder::Instruction& inst, IR::ValueOpcode opcode, IR::Type type,
@@ -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);
@@ -495,8 +495,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 +563,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 +574,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)
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_ */
+5 -25
View File
@@ -14,7 +14,6 @@
#include <array>
#include <bit>
#include <deque>
#include <list>
#include <memory>
#include <optional>
@@ -28,8 +27,6 @@ class CodeGenerator;
namespace Libs::Graphics::ShaderRecompiler::IR {
class SrtWalker;
// Finite descriptor domains also bound the arithmetic used to prove their indices.
constexpr uint32_t MaxIndirectDescriptorProbes = 65536u;
@@ -658,24 +655,10 @@ 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;
};
struct EvaluationContext {
struct Entry {
uint64_t value = 0;
uint64_t generation = 0;
};
std::vector<Entry> values;
uint64_t generation = 0;
const SrtWalker* session = nullptr;
Value active_mask;
uint8_t read_policy = 0;
};
ResourcePlan();
~ResourcePlan();
@@ -695,21 +678,18 @@ struct ResourcePlan {
std::vector<ResourceBlock> control_flow;
std::unordered_map<const Inst*, std::vector<uint32_t>> control_phi_predecessors;
std::vector<SrtRead> srt_reads;
bool requires_specialization_memory = false;
std::vector<std::pair<uint64_t, uint64_t>> source_reads;
bool capture_specialization_reads = false;
bool srt_plan_complete = false;
bool resource_tracking_complete = false;
ShaderInfo info;
UniformFillPlan uniform_fill;
// x86 walker over the retained SRT, descriptor and condition values.
std::unique_ptr<Xbyak::CodeGenerator> srt_code;
// GPU-thread scratch for nested clean/EXEC memos, activity and material keys.
mutable std::deque<EvaluationContext> evaluation_contexts;
mutable uint32_t evaluation_value_count = 0;
mutable std::vector<uint8_t> active_sources;
mutable std::vector<uint8_t> visited_blocks;
mutable std::vector<uint8_t> reachable_blocks;
mutable std::vector<uint32_t> pending_blocks;
uint32_t srt_user_data = 0; // User-data DWORDs the walker reads.
uint32_t srt_captures = 0; // Scratch offset of loaded addresses.
// GPU-thread scratch for walker results and material keys.
mutable std::vector<uint64_t> srt_scratch;
mutable std::vector<uint32_t> material_keys;
mutable std::vector<SelectorKeys> selector_keys;
};
+4 -8
View File
@@ -123,13 +123,9 @@ public:
[[nodiscard]] Block* PhiBlock(size_t index) const;
[[nodiscard]] Block* Parent() const;
[[nodiscard]] const std::vector<Use>& Uses() const;
// Runtime indices belong to the resource plan that owns this instruction.
[[nodiscard]] uint32_t EvaluationIndex(uint32_t& count) const {
if (evaluation_index == UINT32_MAX) {
evaluation_index = count++;
}
return evaluation_index;
}
// SRT walker output index, assigned by the resource plan that owns this instruction.
[[nodiscard]] uint32_t WalkOutput() const { return walk_output; }
void SetWalkOutput(uint32_t index) const { walk_output = index; }
// Backend definitions belong to one emission. Wave64 emulation needs one per half.
[[nodiscard]] uint32_t Definition(uint32_t half) const { return definitions[half]; }
void SetDefinition(uint32_t half, uint32_t definition) const {
@@ -171,7 +167,7 @@ private:
ValueOpcode opcode;
uint8_t num_args;
bool live = false;
mutable uint32_t evaluation_index = UINT32_MAX;
mutable uint32_t walk_output = UINT32_MAX;
mutable std::array<uint32_t, 2> definitions {};
uint64_t flags;
Block* parent = nullptr;
@@ -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)
@@ -222,7 +223,7 @@ VALUE_OPCODE(ReadFirstLane, U32, U32, U1)
VALUE_OPCODE(ReadLane, U32, U32, U32)
VALUE_OPCODE(SubgroupUMinU32, 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)
@@ -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"
@@ -186,35 +187,6 @@ const DescriptorSource* Source(const ResourcePlan& program, uint32_t source) {
return &program.descriptor_sources[source];
}
void MarkCleanReads(const std::unordered_set<const Inst*>& planned_reads,
const DescriptorSource* source, Value extra = {}) {
if (source == nullptr && extra.IsEmpty()) {
return;
}
std::vector<Value> pending;
if (source != nullptr) {
pending.assign(source->dwords.begin(), source->dwords.begin() + source->dword_count);
}
if (!extra.IsEmpty()) pending.push_back(extra);
std::vector<const Inst*> visited;
while (!pending.empty()) {
auto* inst = pending.back().Resolve().TryInstruction();
pending.pop_back();
if (inst == nullptr || std::ranges::find(visited, inst) != visited.end()) {
continue;
}
visited.push_back(inst);
if (planned_reads.contains(inst)) {
auto flags = inst->Flags<SrtReadFlags>();
flags.clean = 1u;
inst->SetFlags(flags);
}
for (size_t arg = 0; arg < inst->NumArgs(); arg++) {
pending.push_back(inst->Arg(arg));
}
}
}
bool ReadTable(uint64_t base, uint64_t size, uint64_t offset,
const SrtRuntime& runtime, std::span<uint32_t> words) {
const auto count = std::min<uint64_t>(words.size(), offset < size ? (size - offset) / 4u : 0u);
@@ -309,22 +281,22 @@ bool CanReadIndexedSelector(const ShaderBufferResource& material, uint32_t offse
uint64_t {UINT32_MAX} + 1u;
}
bool EvaluateIndirectCount(const DescriptorSource::IndirectDescriptor& indirect,
const SrtRuntime& runtime, SrtWalker& clean, uint32_t& count) {
bool EvaluateIndirectCount(const ResourcePlan& program, const DescriptorSource::IndirectDescriptor& indirect,
const SrtRuntime& runtime, uint32_t& count) {
if (indirect.workgroup_axis != UINT32_MAX) {
if (runtime.workgroup_counts.size() != 3u || indirect.workgroup_axis >= 3u) return false;
count = std::ranges::find(runtime.workgroup_counts, 0u) != runtime.workgroup_counts.end()
? 0u : runtime.workgroup_counts[indirect.workgroup_axis];
return true;
}
if (!clean.Evaluate(indirect.key_count, count)) return false;
if (!WalkedValue(program, indirect.key_count, count)) return false;
if (indirect.signed_count && std::bit_cast<int32_t>(count) < 0) count = 0;
return true;
}
bool ResolveSelectorKeys(const DescriptorSource::IndirectDescriptor& indirect,
bool ResolveSelectorKeys(const ResourcePlan& program, const DescriptorSource::IndirectDescriptor& indirect,
const DescriptorValue& material_value, const SrtRuntime& runtime,
SrtWalker& clean, std::vector<uint32_t>& keys) {
std::vector<uint32_t>& keys) {
const auto* selector = &*indirect.selector;
keys.clear();
const auto read_selector = [&](uint64_t base, uint64_t size, uint64_t offset,
@@ -363,7 +335,7 @@ bool ResolveSelectorKeys(const DescriptorSource::IndirectDescriptor& indirect,
const auto& rows = *selector->rows;
DescriptorValue count_value;
ShaderBufferResource counts, material;
if (!clean.EvaluateDescriptor(rows.source, count_value) ||
if (!WalkedDescriptor(program, rows.source, count_value) ||
!DecodeBufferDescriptor(count_value, counts) ||
!DecodeBufferDescriptor(material_value, material) || selector->stride != 4u ||
selector->offset != 0u || rows.stride == 0u || (rows.stride & 3u) != 0u ||
@@ -392,7 +364,7 @@ bool ResolveSelectorKeys(const DescriptorSource::IndirectDescriptor& indirect,
if (candidate_end - first > MaxIndirectDescriptorProbes) break;
end = candidate_end;
}
// Coalesce small gaps, but interpret only selected prefixes. The strict
// Coalesce small gaps, but interpret only selected prefixes. The specialization
// reader captures the entire fetched span for writable-alias rejection.
batch.resize(end - first);
if (!ReadScalarTable(material.Base48(), material.GetSize(), first * 4u, runtime, batch))
@@ -408,8 +380,8 @@ bool ResolveSelectorKeys(const DescriptorSource::IndirectDescriptor& indirect,
std::array<uint32_t, 4> masks {};
uint32_t first = 0, count = 0;
if (!DecodeBufferDescriptor(material_value, material) ||
!clean.Evaluate(indirect.selector_first, first) ||
!EvaluateIndirectCount(indirect, runtime, clean, count) || count != masks.size() * 32u ||
!WalkedValue(program, indirect.selector_first, first) ||
!EvaluateIndirectCount(program, indirect, runtime, count) || count != masks.size() * 32u ||
selector->stride != sizeof(masks) || selector->offset != 0u ||
material.Type() != 0u || material.Stride() != sizeof(masks) ||
material.SwizzleEnabled() || material.AddTid() || material.OutOfBounds() != 0u ||
@@ -462,8 +434,8 @@ bool ResolveSelectorKeys(const DescriptorSource::IndirectDescriptor& indirect,
base = material.Base48();
size = material.GetSize();
}
if (!clean.Evaluate(indirect.selector_first, first) ||
!EvaluateIndirectCount(indirect, runtime, clean, count) || count > MaxIndirectDescriptorProbes)
if (!WalkedValue(program, indirect.selector_first, first) ||
!EvaluateIndirectCount(program, indirect, runtime, count) || count > MaxIndirectDescriptorProbes)
return false;
// SMEM uses its instruction's byte stride; V# stride only supplies its bounds.
const auto step = selector->stride != 0u ? selector->stride : material.Stride();
@@ -483,8 +455,8 @@ bool ResolveSelectorKeys(const DescriptorSource::IndirectDescriptor& indirect,
uint32_t mask = 0;
uint32_t count = 0;
if (material_value.dword_count != 2u ||
!clean.Evaluate(indirect.selector_mask, mask) ||
!EvaluateIndirectCount(indirect, runtime, clean, count) || count == 0u || count > 32u) {
!WalkedValue(program, indirect.selector_mask, mask) ||
!EvaluateIndirectCount(program, indirect, runtime, count) || count == 0u || count > 32u) {
return false;
}
if (count < 32u) mask &= (1u << count) - 1u;
@@ -541,8 +513,7 @@ template <typename Specialization, typename Normalize>
bool MaterializeIndirectDescriptor(const ResourcePlan& program,
uint32_t source_index,
uint32_t resource_index, uint32_t dword_count, uint32_t read_dwords,
const SrtRuntime& runtime, SrtWalker& clean,
ResourceSnapshot& snapshot,
const SrtRuntime& runtime, ResourceSnapshot& snapshot,
std::vector<DescriptorValue>& descriptors,
std::vector<Specialization>& specializations,
Normalize&& normalize) {
@@ -605,8 +576,8 @@ bool MaterializeIndirectDescriptor(const ResourcePlan& p
if (selector == nullptr) keys.clear();
DescriptorValue material_value;
if ((selector != nullptr && (byte_keys || !selector_keys->resolved) &&
!clean.EvaluateDescriptor(selector->source, material_value)) ||
!clean.EvaluateDescriptor(indirect.table_source, table_value)) {
!WalkedDescriptor(program, selector->source, material_value)) ||
!WalkedDescriptor(program, indirect.table_source, table_value)) {
return false;
}
if (table_value.dword_count == 2u) {
@@ -659,7 +630,7 @@ bool MaterializeIndirectDescriptor(const ResourcePlan& p
: uint64_t {1} << 32u;
if (!indirect.key_count.IsEmpty()) {
uint32_t key_count = 0u;
if (!EvaluateIndirectCount(indirect, runtime, clean, key_count)) return false;
if (!EvaluateIndirectCount(program, indirect, runtime, key_count)) return false;
domain = std::min(domain, uint64_t {key_count});
}
if (indexed_pointer &&
@@ -771,8 +742,8 @@ bool MaterializeIndirectDescriptor(const ResourcePlan& p
if (selector == nullptr) {
uint32_t key_count = 0;
uint32_t first_key = 0;
if (!EvaluateIndirectCount(indirect, runtime, clean, key_count)) return false;
if (!indirect.selector_first.IsEmpty() && !clean.Evaluate(indirect.selector_first, first_key))
if (!EvaluateIndirectCount(program, indirect, runtime, key_count)) return false;
if (!indirect.selector_first.IsEmpty() && !WalkedValue(program, indirect.selector_first, first_key))
return false;
if (indirect.table_stride == 0u || key_count > MaxIndirectDescriptorProbes ||
(indirect.selector_first.IsEmpty() && key_count != 0u &&
@@ -788,7 +759,7 @@ bool MaterializeIndirectDescriptor(const ResourcePlan& p
} else {
if (!indirect.selector_mask.IsEmpty() && table_value.dword_count != 2u) return false;
if (!selector_keys->resolved) {
if (!ResolveSelectorKeys(indirect, material_value, runtime, clean, keys)) return false;
if (!ResolveSelectorKeys(program, indirect, material_value, runtime, keys)) return false;
selector_keys->resolved = true;
}
}
@@ -827,7 +798,7 @@ bool MaterializeIndirectDescriptor(const ResourcePlan& p
std::span(candidate.dwords).first(read_dwords))) {
return read_failed("table descriptor read", key, 0u, table_base, table_offset, descriptor_bytes);
}
} else if (!clean.EvaluateDescriptor(sources[entry], candidate)) {
} else if (!WalkedDescriptor(program, sources[entry], candidate)) {
return false;
}
if (!normalize(candidate))
@@ -1341,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;
@@ -1380,16 +1362,13 @@ ResourcePlan ExtractResourcePlan(const Program& program) {
if (const auto found = cloned.find(source); found != cloned.end()) {
return Value(found->second);
}
if (source->GetOpcode() == ValueOpcode::LoadBufferU32) {
plan.requires_specialization_memory = true;
plan.capture_specialization_reads = true;
}
if (source->GetOpcode() == ValueOpcode::LoadBufferU32) plan.capture_specialization_reads = true;
auto& target =
plan.value_storage.emplace_back(source->GetOpcode(), source->Flags<uint64_t>());
if (source->GetOpcode() == ValueOpcode::LoadAddressU32 ||
source->GetOpcode() == ValueOpcode::ReadConstBuffer ||
source->GetOpcode() == ValueOpcode::LoadBufferU32) {
target.SetFlags(SrtReadFlags {.index = source->Flags<MemoryFlags>().index});
target.SetFlags(MemoryFlags {.index = source->Flags<MemoryFlags>().index});
}
cloned.emplace(source, &target);
if (source->GetOpcode() == ValueOpcode::Phi) {
@@ -1411,7 +1390,6 @@ ResourcePlan ExtractResourcePlan(const Program& program) {
target.dword_count = source.dword_count;
target.indirect_descriptor = source.indirect_descriptor;
if (target.indirect_descriptor.has_value()) {
plan.requires_specialization_memory = true;
capture_indirect_reads = true;
target.indirect_descriptor->key_count = Clone(target.indirect_descriptor->key_count);
target.indirect_descriptor->selector_first =
@@ -1425,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());
@@ -1523,57 +1502,16 @@ ResourcePlan ExtractResourcePlan(const Program& program) {
plan.uniform_fill.element_count = Clone(plan.uniform_fill.element_count);
plan.capture_specialization_reads |=
capture_indirect_reads || capture_control_reads || !plan.source_reads.empty();
if (plan.capture_specialization_reads) {
// Every former flat alias now targets its retained raw read. Mark those reads
// on the normalized graph, so discarded Phi/EXEC dependencies stay discarded.
std::unordered_set<const Inst*> planned_reads;
for (const auto& read: plan.srt_reads) {
auto* inst = read.value.ResolveInstruction();
EXIT_IF(inst == nullptr || (inst->GetOpcode() != ValueOpcode::LoadAddressU32 &&
inst->GetOpcode() != ValueOpcode::ReadConstBuffer));
planned_reads.insert(inst);
}
for (const auto& source: plan.descriptor_sources) {
if (!source.indirect_descriptor.has_value()) continue;
const auto& indirect = *source.indirect_descriptor;
if (indirect.selector) {
MarkCleanReads(planned_reads, Source(plan, indirect.selector->source));
if (indirect.selector->rows)
MarkCleanReads(planned_reads, Source(plan, indirect.selector->rows->source));
}
MarkCleanReads(planned_reads, nullptr, indirect.selector_mask);
MarkCleanReads(planned_reads, nullptr, indirect.selector_first);
MarkCleanReads(planned_reads, nullptr, indirect.key_count);
if (indirect.sources.empty()) {
MarkCleanReads(planned_reads, Source(plan, indirect.table_source));
} else {
for (const auto candidate: indirect.sources) {
MarkCleanReads(planned_reads, Source(plan, candidate));
}
}
}
// Clean writable addresses prove that shader writes cannot overlap captured reads.
for (const auto& buffer: plan.info.buffers) {
if (buffer.written) MarkCleanReads(planned_reads, Source(plan, buffer.source));
}
for (const auto& image: plan.info.images) {
if (image.written) MarkCleanReads(planned_reads, Source(plan, image.source));
}
}
if (capture_indirect_reads || !plan.source_reads.empty())
plan.resource_tracking_complete &= !program.has_address_writes;
CompileSrtWalker(plan);
DerivePlanState(plan);
return plan;
}
bool MaterializeResources(const ResourcePlan& program, const SrtRuntime& runtime,
ResourceSnapshot& snapshot, ResourceSpecialization& specialization) {
for (auto& keys: program.selector_keys) keys.resolved = false;
if (!program.resource_tracking_complete ||
((program.requires_specialization_memory || !program.source_reads.empty()) &&
runtime.read_specialization_memory == nullptr)) {
return false;
}
if (!program.resource_tracking_complete) return false;
const auto uses_workgroup_count = [](const DescriptorSource& source) {
return source.indirect_descriptor && source.indirect_descriptor->workgroup_axis != UINT32_MAX;
};
@@ -1582,20 +1520,16 @@ bool MaterializeResources(const ResourcePlan& program, const SrtRuntime& runtime
std::ranges::any_of(program.descriptor_sources, uses_workgroup_count));
auto& reads = snapshot.specialization_reads;
reads = program.source_reads;
SrtReadCapture capture {runtime, reads};
auto observed = capture_reads ? capture.ObservedRuntime() : runtime;
SrtWalker clean(program, CleanRuntime(observed));
SrtWalker walker(program, observed,
capture_reads || program.requires_specialization_memory ? &clean : nullptr);
if (!walker.RefreshFlatBuffer(snapshot.flattened_srt)) {
if (!WalkSrt(program, runtime, snapshot.flattened_srt, capture_reads ? &reads : nullptr)) {
return SpecializationFail(fmt::format("SRT refresh complete={} reads={} user_data={}",
program.srt_plan_complete, program.srt_reads.size(), runtime.user_data.size()));
}
const auto active = std::span<const uint8_t>(program.active_sources);
SrtReadCapture capture {runtime, reads};
auto observed = capture_reads ? capture.ObservedRuntime() : runtime;
std::array<uint32_t, 3> workgroup_counts {};
if (runtime.indirect_args_address != 0u) {
for (uint32_t source = 0; source < program.descriptor_sources.size(); ++source) {
if ((!active.empty() && !active[source]) ||
if (!WalkedActive(program, source) ||
!uses_workgroup_count(program.descriptor_sources[source])) continue;
if ((runtime.indirect_args_address & 3u) != 0u ||
!ReadTable(runtime.indirect_args_address, sizeof(workgroup_counts), 0u,
@@ -1610,24 +1544,24 @@ bool MaterializeResources(const ResourcePlan& program, const SrtRuntime& runtime
std::array<uint32_t, 4> stored {};
bool uniform_fill = words != 0;
if (uniform_fill && !fill.pattern_size.IsEmpty())
uniform_fill = clean.Evaluate(fill.pattern_size, words) && words >= 1 && words <= 4;
uniform_fill = WalkedValue(program, fill.pattern_size, words) && words >= 1 && words <= 4;
uint32_t elements = 0;
if (uniform_fill && !fill.element_count.IsEmpty())
uniform_fill = clean.Evaluate(fill.element_count, elements) && elements != 0;
uniform_fill = WalkedValue(program, fill.element_count, elements) && elements != 0;
for (uint32_t i = 0; i < words && uniform_fill; ++i) {
uniform_fill = clean.Evaluate(fill.values[i], stored[i]) && stored[i] == stored[0];
uniform_fill = WalkedValue(program, fill.values[i], stored[i]) && stored[i] == stored[0];
}
if (uniform_fill) {
snapshot.uniform_fill = fill.fill;
snapshot.uniform_fill.value = stored[0];
snapshot.uniform_fill.bounded_elements = elements;
}
const auto evaluate = [&](uint32_t source, DescriptorValue& value, bool written = false) {
const auto evaluate = [&](uint32_t source, DescriptorValue& value) {
if (source >= program.descriptor_sources.size()) {
return false;
}
if (active.empty() || active[source]) {
return (capture_reads && written ? clean : walker).EvaluateDescriptor(source, value) ||
if (WalkedActive(program, source)) {
return WalkedDescriptor(program, source, value) ||
SpecializationFail(fmt::format("descriptor evaluation source={}", source));
}
value = {};
@@ -1651,9 +1585,9 @@ bool MaterializeResources(const ResourcePlan& program, const SrtRuntime& runtime
if (source == nullptr) return false;
if (source->indirect_descriptor.has_value()) {
snapshot.buffers[i] = {.dword_count = 4u};
if (active.empty() || active[base.source]) {
if (WalkedActive(program, base.source)) {
if (!MaterializeIndirectDescriptor(
program, base.source, i, 4u, 4u, observed, clean, snapshot,
program, base.source, i, 4u, 4u, observed, snapshot,
snapshot.buffers, specialization.buffers,
[&](DescriptorValue& value) {
return NormalizeIndirectBuffer(
@@ -1661,7 +1595,7 @@ bool MaterializeResources(const ResourcePlan& program, const SrtRuntime& runtime
}))
return resource_failed("buffer", i, base.source, snapshot.buffers.size());
}
} else if (!evaluate(base.source, snapshot.buffers[i], base.written)) {
} else if (!evaluate(base.source, snapshot.buffers[i])) {
return false;
}
}
@@ -1711,13 +1645,13 @@ bool MaterializeResources(const ResourcePlan& program, const SrtRuntime& runtime
}
if (source->indirect_descriptor.has_value()) {
snapshot.images[i] = {.dword_count = 8u};
if (!active.empty() && !active[image.source]) {
if (!WalkedActive(program, image.source)) {
continue;
}
const auto& indirect = *source->indirect_descriptor;
const bool bounded_table = IsBoundedDescriptorTable(program, indirect);
if (!MaterializeIndirectDescriptor(
program, image.source, i, 8u, image.r128 ? 4u : 8u, observed, clean, snapshot, snapshot.images,
program, image.source, i, 8u, image.r128 ? 4u : 8u, observed, snapshot, snapshot.images,
specialization.images, [&](DescriptorValue& value) {
// A broad heap also contains resources for other typed image operations.
if (NullImageDescriptor(value) || !ValidImageDescriptor(value, image.r128) ||
@@ -1728,7 +1662,7 @@ bool MaterializeResources(const ResourcePlan& program, const SrtRuntime& runtime
return resource_failed("image", i, image.source, snapshot.images.size());
}
} else {
if (!evaluate(image.source, snapshot.images[i], image.written)) {
if (!evaluate(image.source, snapshot.images[i])) {
return false;
}
if (!ValidImageDescriptor(snapshot.images[i], image.r128)) {
@@ -1749,8 +1683,8 @@ bool MaterializeResources(const ResourcePlan& program, const SrtRuntime& runtime
const auto* indirect = descriptor->indirect_descriptor ? &*descriptor->indirect_descriptor : nullptr;
if (indirect != nullptr && indirect->sources.empty()) {
snapshot.samplers[i] = {.dword_count = 4u};
if ((active.empty() || active[source]) && !MaterializeIndirectDescriptor(
program, source, i, 4u, 4u, observed, clean, snapshot, snapshot.samplers,
if (WalkedActive(program, source) && !MaterializeIndirectDescriptor(
program, source, i, 4u, 4u, observed, snapshot, snapshot.samplers,
specialization.samplers,
[](DescriptorValue&) { return true; }))
return resource_failed("sampler", i, source, snapshot.samplers.size());
@@ -2065,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);
File diff suppressed because it is too large. Load diff
@@ -8,8 +8,6 @@
namespace Libs::Graphics::ShaderRecompiler::IR {
class Value;
using SrtMemoryReader = bool (*)(void* userdata, uint64_t address, std::span<uint32_t> values);
// Image descriptors at base + first + k * step, k < count, within size bytes that a sampled
@@ -42,7 +40,6 @@ protected:
struct SrtRuntime {
std::span<const uint32_t> user_data;
uint64_t shader_base = 0;
SrtMemoryReader read_memory = nullptr;
void* userdata = nullptr;
SrtMemoryReader read_specialization_memory = nullptr;
std::span<const uint32_t> workgroup_counts;
@@ -57,20 +54,12 @@ public:
SrtRuntime ObservedRuntime();
private:
static bool ReadStrict(void* userdata, uint64_t address, std::span<uint32_t> values);
static bool ReadOrdinary(void* userdata, uint64_t address, std::span<uint32_t> values);
static bool Read(void* userdata, uint64_t address, std::span<uint32_t> values);
SrtRuntime m_source;
std::vector<std::pair<uint64_t, uint64_t>>& m_ranges;
};
// Retained reads no longer need the guest instruction PC. A clean read evaluates
// its address and bounds through the strict reader as well as the final DWORD.
struct SrtReadFlags {
uint32_t index = 0;
uint32_t clean = 0;
};
enum class RuntimeValueType { Any, Integer, ImmutableInteger };
bool ValidateRuntimeValue(const ResourcePlan& program, Value value,
@@ -78,9 +67,20 @@ bool ValidateRuntimeValue(const ResourcePlan& program, Value value,
// Uniform across a compute wave, including values derived from its workgroup ID.
// This does not imply that the host can evaluate the value.
bool ValidateWaveUniformValue(const ResourcePlan& program, Value value);
void CompileSrtWalker(ResourcePlan& program);
// Uses the strict reader for values that affect shader specialization.
SrtRuntime CleanRuntime(SrtRuntime runtime);
// Compiles the plan's x86 walker. Without values it walks the plan's resources; otherwise it
// only evaluates the values, with every block reachable.
void CompileSrtWalker(ResourcePlan& plan, std::span<const Value> values = {});
// Runs the walker. flat receives the scalar reads of reachable blocks and reads, when non-null,
// one DWORD range per guest load. False when a reachable read is unknown.
bool WalkSrt(const ResourcePlan& plan, const SrtRuntime& runtime, std::vector<uint32_t>& flat,
std::vector<std::pair<uint64_t, uint64_t>>* reads);
// Results of the last walk; false when unknown.
bool WalkedValue(const ResourcePlan& plan, Value value, uint32_t& result);
bool WalkedDescriptor(const ResourcePlan& plan, uint32_t source, DescriptorValue& result);
bool WalkedActive(const ResourcePlan& plan, uint32_t source);
// Length of the guest load at rip if it belongs to the walker running on this thread, else 0.
// The exception handler completes such a load as a zero read.
uint32_t SrtWalkerLoadLength(uint64_t rip);
// Nonnegative affine coefficients: element zero is constant, remaining elements are
// caller-selected leaves. Runtime range proofs must additionally exclude U32 wrap.
@@ -124,43 +124,6 @@ std::optional<std::array<uint64_t, Size>> AffineIndex(
return left;
}
// One memoized evaluation session shared by the entire shader resource refresh.
class SrtWalker {
public:
SrtWalker(const ResourcePlan& program, const SrtRuntime& runtime,
SrtWalker* clean_evaluator = nullptr);
~SrtWalker();
SrtWalker(const SrtWalker&) = delete;
SrtWalker& operator=(const SrtWalker&) = delete;
bool Evaluate(Value value, uint32_t& result);
bool EvaluateDescriptor(uint32_t source, DescriptorValue& result);
// Refreshes reachable scalar reads and active descriptor sources in one walk.
bool RefreshFlatBuffer(std::vector<uint32_t>& flat);
private:
SrtWalker(SrtWalker& parent, Value active_mask, bool clean = false,
SrtWalker* clean_evaluator = nullptr);
ResourcePlan::EvaluationContext& AcquireContext();
void ResetUnknowns();
static float Float32(uint64_t bits);
bool EvaluateWide(Value value, uint64_t& result);
bool Arg(const Inst& inst, size_t index, uint64_t& result);
bool EvaluatePhi(const Inst& inst, uint64_t& result);
bool EvaluateExtract(const Inst& inst, uint64_t& result);
bool EvaluateRawRead(const Inst& inst, uint64_t& result);
bool EvaluateInst(const Inst& inst, uint64_t& result);
const ResourcePlan& m_program;
SrtRuntime m_runtime;
SrtWalker* m_clean_evaluator = nullptr;
Value m_active_mask;
const std::vector<uint8_t>* m_reachable_blocks = nullptr;
const SrtWalker* m_session;
uint8_t m_read_policy;
ResourcePlan::EvaluationContext& m_context;
};
} // namespace Libs::Graphics::ShaderRecompiler::IR
#endif /* EMULATOR_INCLUDE_EMULATOR_GRAPHICS_SHADER_RECOMPILER_SRTWALKER_H_ */
+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
+19 -28
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"
@@ -40,15 +39,19 @@
namespace Libs::Graphics {
// A mapped shader and its vertex metadata, parsed once when it is mapped.
struct ShaderMapData: ShaderMappedData {
ShaderVertexMetadata vertex;
const char* vertex_error = nullptr;
};
struct ShaderMapEntry {
ShaderMappedData data;
uint64_t hash;
ShaderMapData data;
uint64_t hash;
};
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);
@@ -64,8 +67,11 @@ void ShaderMapUserData(uint64_t addr, const ShaderMappedData& data) {
addr, data.code_size_bytes);
}
const auto hash = XXH3_64bits(reinterpret_cast<const void*>(addr), data.code_size_bytes);
ShaderMapData mapped {data};
mapped.vertex_error =
ShaderReadVertexMetadata(data, HW::UserSgprInfo::SGPRS_MAX, mapped.vertex);
std::scoped_lock lock(g_shader_map_mutex);
(*g_shader_map)[addr] = {data, hash};
(*g_shader_map)[addr] = {mapped, hash};
}
static ShaderMapEntry ShaderGetMappedData(uint64_t addr, const char* label) {
@@ -261,8 +267,8 @@ static uint32_t ShaderCalcPsSystemInputBase(const HW::ShaderRegisters& regs) {
static bool ShaderGetStaticVertexInputInfo(uint64_t shader_addr, const HW::UserSgprInfo& user_sgpr,
uint32_t user_sgpr_num, const HW::ShaderRegisters& sh,
const ShaderMappedData& data,
ShaderVertexInputInfo& info) {
const ShaderMapData& data,
ShaderVertexInputInfo& info) {
KYTY_PROFILER_FUNCTION();
info = {};
@@ -277,13 +283,12 @@ static bool ShaderGetStaticVertexInputInfo(uint64_t shader_addr, const HW::UserS
" user_sgpr_num=%u\n",
shader_addr, user_sgpr_num);
}
ShaderVertexMetadata metadata;
std::string metadata_error;
if (!ShaderReadVertexMetadata(data, HW::UserSgprInfo::SGPRS_MAX, metadata, &metadata_error)) {
if (data.vertex_error != nullptr) {
LOGF("ShaderGetVertexInputInfo(): invalid AGC metadata shader=0x%016" PRIx64 ": %s\n",
shader_addr, metadata_error.c_str());
shader_addr, data.vertex_error);
return false;
}
const auto& metadata = data.vertex;
if (metadata.vertex_attrib_reg >= 0) {
info.fetch_external = false;
@@ -377,7 +382,6 @@ static void ShaderGetStaticInputInfoPS(
}
static void ShaderGetStaticInputInfoCS(const HW::ComputeShaderInfo& regs,
const HW::ShaderRegisters& /*sh*/,
const ShaderMappedData& data, ShaderComputeInputInfo& info) {
const bool async_compute = info.async_compute;
const bool dispatch_thread_dimensions = info.dispatch_thread_dimensions;
@@ -655,18 +659,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;
}
@@ -684,10 +676,9 @@ ShaderParams PrepareProgram(
std::span<const uint32_t>(regs.ps_user_sgpr.value, regs.ps_regs.rsrc2.user_sgpr), data);
}
ShaderParams PrepareProgram(const HW::ComputeShaderInfo& regs, const HW::ShaderRegisters& sh,
ShaderComputeInputInfo& info) {
ShaderParams PrepareProgram(const HW::ComputeShaderInfo& regs, ShaderComputeInputInfo& info) {
const auto [data, hash] = ShaderGetMappedData(regs.cs_regs.data_addr, "ShaderGetInputInfoCS():");
ShaderGetStaticInputInfoCS(regs, sh, data, info);
ShaderGetStaticInputInfoCS(regs, data, info);
return GetShaderParams(
regs.cs_regs.data_addr, hash,
std::span<const uint32_t>(regs.cs_user_sgpr.value, regs.cs_regs.user_sgpr), data);
+4 -17
View File
@@ -15,17 +15,6 @@
namespace Libs::Graphics {
namespace ShaderError {
[[nodiscard]] inline bool Fail(std::string* error, std::string_view message) {
if (error != nullptr) {
error->assign(message.data(), message.size());
}
return false;
}
} // namespace ShaderError
namespace HW {
struct VertexShaderInfo;
struct PixelShaderInfo;
@@ -45,16 +34,14 @@ enum class ShaderType {
TessellationEvaluation
};
namespace ShaderRecompiler::IR {
struct CompiledShaderInfo;
} // namespace ShaderRecompiler::IR
struct ShaderModule;
struct ShaderStageRuntime {
const ShaderRecompiler::IR::CompiledShaderInfo* program = nullptr;
const ShaderRecompiler::IR::ResourceSnapshot* resources = 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;
}
};
+2 -2
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);
@@ -39,8 +40,7 @@ ShaderParams PrepareProgram(
const HW::PixelShaderInfo& regs, const HW::ShaderRegisters& sh,
std::span<const Prospero::ColorComponentMapping, 8> target_export_mapping,
ShaderPixelInputInfo& input_info);
ShaderParams PrepareProgram(const HW::ComputeShaderInfo& regs, const HW::ShaderRegisters& sh,
ShaderComputeInputInfo& input_info);
ShaderParams PrepareProgram(const HW::ComputeShaderInfo& regs, ShaderComputeInputInfo& input_info);
} // namespace Libs::Graphics
+13 -14
View File
@@ -5,10 +5,10 @@
namespace Libs::Graphics {
bool ShaderReadVertexMetadata(const ShaderMappedData& data, uint32_t max_user_sgprs,
ShaderVertexMetadata& metadata, std::string* error) {
const char* ShaderReadVertexMetadata(const ShaderMappedData& data, uint32_t max_user_sgprs,
ShaderVertexMetadata& metadata) {
if (data.user_data == nullptr) {
return ShaderError::Fail(error, "missing AGC user-data header");
return "missing AGC user-data header";
}
ShaderUserData user_data {};
@@ -17,8 +17,7 @@ bool ShaderReadVertexMetadata(const ShaderMappedData& data, uint32_t max_user_sg
constexpr uint32_t DirectResourceCount =
static_cast<uint32_t>(AgcDirectResourceType::Last) + 1u;
if (user_data.direct_resource_count > DirectResourceCount) {
return ShaderError::Fail(error,
"AGC direct-resource count exceeds the known resource domain");
return "AGC direct-resource count exceeds the known resource domain";
}
std::array<uint16_t, DirectResourceCount> direct_offsets {};
@@ -26,7 +25,7 @@ bool ShaderReadVertexMetadata(const ShaderMappedData& data, uint32_t max_user_sg
static_cast<uint64_t>(user_data.direct_resource_count) * sizeof(uint16_t);
if (direct_size != 0) {
if (user_data.direct_resource_offset == nullptr) {
return ShaderError::Fail(error, "missing AGC direct-resource offsets");
return "missing AGC direct-resource offsets";
}
std::memcpy(direct_offsets.data(), user_data.direct_resource_offset, direct_size);
}
@@ -47,33 +46,33 @@ bool ShaderReadVertexMetadata(const ShaderMappedData& data, uint32_t max_user_sg
}
if (next.vertex_attrib_reg >= 0 && next.vertex_buffer_reg < 0) {
return ShaderError::Fail(error, "vertex attribute table requires a vertex buffer table");
return "vertex attribute table requires a vertex buffer table";
}
if (next.vertex_buffer_reg < 0) {
metadata = next;
return true;
return nullptr;
}
if (static_cast<uint32_t>(next.vertex_buffer_reg) + 1u >= max_user_sgprs) {
return ShaderError::Fail(error, "vertex table pointer exceeds the user-SGPR domain");
return "vertex table pointer exceeds the user-SGPR domain";
}
if (next.vertex_attrib_reg < 0) {
metadata = next;
return true;
return nullptr;
}
if (static_cast<uint32_t>(next.vertex_attrib_reg) + 1u >= max_user_sgprs) {
return ShaderError::Fail(error, "vertex table pointer exceeds the user-SGPR domain");
return "vertex table pointer exceeds the user-SGPR domain";
}
if (data.num_input_semantics == 0 ||
data.num_input_semantics > ShaderVertexInputInfo::RES_MAX) {
return ShaderError::Fail(error, "vertex semantic count is outside the supported domain");
return "vertex semantic count is outside the supported domain";
}
if (data.input_semantics == nullptr) {
return ShaderError::Fail(error, "missing vertex input semantics");
return "missing vertex input semantics";
}
next.input_semantics = {data.input_semantics, data.num_input_semantics};
metadata = next;
return true;
return nullptr;
}
} // namespace Libs::Graphics
+4 -4
View File
@@ -4,7 +4,6 @@
#include "graphics/shader/shader.h"
#include <span>
#include <string>
namespace Libs::Graphics {
@@ -14,9 +13,10 @@ struct ShaderVertexMetadata {
std::span<const ShaderSemantic> input_semantics;
};
// Validates the vertex metadata and returns a view into the shader header.
bool ShaderReadVertexMetadata(const ShaderMappedData& data, uint32_t max_user_sgprs,
ShaderVertexMetadata& metadata, std::string* error);
// Reads the vertex metadata as a view into the shader header. Returns nullptr if it is
// valid, otherwise why it was rejected.
const char* ShaderReadVertexMetadata(const ShaderMappedData& data, uint32_t max_user_sgprs,
ShaderVertexMetadata& metadata);
} // namespace Libs::Graphics
+2 -2
View File
@@ -820,12 +820,12 @@ public:
return ProtectMappedUnlocked(vaddr, size, mode);
}
// Tracker protections only read m_mapped; the page tracker orders them per page.
bool ProtectTransient(uint64_t vaddr, uint64_t size, VirtualMemory::Mode mode) {
if (size == 0 || UINT64_MAX - vaddr < size) {
return false;
}
std::lock_guard lock(m_mutex);
std::shared_lock lock(m_mutex);
return ProtectMappedUnlocked(vaddr, size, mode);
}
@@ -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>
+7
View File
@@ -112,6 +112,13 @@
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="close_terminal">
<property name="text">
<string>Close terminal automatically</string>
</property>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="themeLayout">
<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;
+1 -1
View File
@@ -22,7 +22,7 @@ public:
explicit MainDialog(QWidget* parent = nullptr);
~MainDialog() override = default;
void RunInterpreter(QProcess* process, const Configuration& info);
void RunInterpreter(QProcess* process, const Configuration& info, bool close_terminal);
static void WriteSettings(QSettings& s);
static void ReadSettings(QSettings& s);
+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 =
+81 -113
View File
@@ -17,7 +17,6 @@
#include <QDir>
#include <QFile>
#include <QFileInfo>
#include <QIODevice>
#include <QLabel>
#include <QMessageBox>
#include <QObject>
@@ -28,7 +27,6 @@
#include <QSettings>
#include <QStringList>
#include <QStyleHints>
#include <QTextStream>
#include <QVariant>
#include <QtCore>
@@ -47,23 +45,17 @@
class QWidget;
#if defined(_WIN32)
constexpr char EMULATOR_EXE[] = "kyty_emulator.exe";
constexpr char EMULATOR_EXE[] = "kyty_emulator.exe";
constexpr char CMD_EXE[] = "cmd.exe";
constexpr DWORD CMD_X_CHARS = 175;
constexpr DWORD CMD_Y_CHARS = 1000;
#else
constexpr char EMULATOR_EXE[] = "kyty_emulator";
#endif
#if defined(_WIN32)
constexpr char CMD_EXE[] = "cmd.exe";
#elif defined(__linux__)
constexpr char KYTY_BASH_FILE[] = "kyty_run.sh";
#endif
#if defined(_WIN32)
constexpr DWORD CMD_X_CHARS = 175;
constexpr DWORD CMD_Y_CHARS = 1000;
#endif
constexpr char SETTINGS_MAIN_DIALOG[] = "MainDialog";
constexpr char SETTINGS_MAIN_LAST_GEOMETRY[] = "geometry";
constexpr char SETTINGS_CHECK_UPDATES[] = "check_updates_on_startup";
constexpr char SETTINGS_CLOSE_TERMINAL[] = "close_terminal";
constexpr char SETTINGS_THEME[] = "theme";
class MainDialogPrivate: public QObject {
@@ -89,6 +81,7 @@ public:
private:
static QByteArray g_last_geometry;
static bool g_check_updates_on_startup;
static bool g_close_terminal;
static int g_theme;
static void ApplyTheme(int theme) {
@@ -108,13 +101,12 @@ private:
TitleSound m_title_sound;
QString m_interpreter;
QProcess m_process;
QPointer<ConfigurationItem> m_running_item;
int m_running_games = 0;
};
QByteArray MainDialogPrivate::g_last_geometry;
bool MainDialogPrivate::g_check_updates_on_startup = true;
bool MainDialogPrivate::g_close_terminal = false;
int MainDialogPrivate::g_theme = 0;
MainDialog::MainDialog(QWidget* parent): QDialog(parent), m_p(new MainDialogPrivate(this)) {
@@ -122,7 +114,6 @@ MainDialog::MainDialog(QWidget* parent): QDialog(parent), m_p(new MainDialogPriv
}
MainDialogPrivate::~MainDialogPrivate() {
m_process.disconnect(this);
delete m_ui;
}
@@ -134,10 +125,14 @@ void MainDialogPrivate::Setup(MainDialog* main_dialog) {
m_main_dialog = main_dialog;
m_update_checker = new UpdateChecker(main_dialog);
m_ui->check_updates_on_startup->setChecked(g_check_updates_on_startup);
m_ui->close_terminal->setChecked(g_close_terminal);
m_ui->theme_combo->setCurrentIndex(g_theme);
ApplyTheme(g_theme);
m_ui->check_updates_link->setVisible(UpdateChecker::IsSupported());
m_ui->check_updates_on_startup->setVisible(UpdateChecker::IsSupported());
#if defined(__APPLE__)
m_ui->close_terminal->setVisible(false); // The emulator runs without a terminal.
#endif
main_dialog->setWindowFlags(Qt::Dialog /*| Qt::MSWindowsFixedSizeDialogHint*/);
@@ -146,13 +141,12 @@ void MainDialogPrivate::Setup(MainDialog* main_dialog) {
connect(m_ui->widget, &ConfigurationListWidget::Select, this, [this]() {
Update();
const auto* item = m_ui->widget->GetSelectedItem();
m_title_sound.SetTitle(m_process.state() == QProcess::NotRunning && item != nullptr
? item->GetInfo().basedir
: QString());
m_title_sound.SetTitle(m_running_games == 0 && item != nullptr ? item->GetInfo().basedir
: QString());
});
connect(m_ui->widget, &ConfigurationListWidget::PreviewControllerColor, this,
[this](const QString& color) {
if (m_process.state() == QProcess::NotRunning) {
if (m_running_games == 0) {
m_controller_preview.SetColor(color);
}
});
@@ -166,6 +160,10 @@ void MainDialogPrivate::Setup(MainDialog* main_dialog) {
g_check_updates_on_startup = checked;
m_ui->widget->WriteSettings();
});
connect(m_ui->close_terminal, &QCheckBox::toggled, this, [this](bool checked) {
g_close_terminal = checked;
m_ui->widget->WriteSettings();
});
connect(m_ui->theme_combo, &QComboBox::currentIndexChanged, this, [this](int index) {
g_theme = index;
ApplyTheme(index);
@@ -176,16 +174,6 @@ void MainDialogPrivate::Setup(MainDialog* main_dialog) {
m_ui->widget->WriteSettings();
});
connect(&m_process, &QProcess::stateChanged, this, [this](QProcess::ProcessState state) {
m_update_checker->SetGameRunning(state != QProcess::NotRunning);
if (state == QProcess::NotRunning) {
if (m_running_item != nullptr) {
m_running_item->SetRunning(false);
}
Update();
}
});
m_ui->label_settings_file->setText(tr("Settings file: ") + m_ui->widget->GetSettingsFile());
m_main_dialog->restoreGeometry(g_last_geometry);
@@ -288,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");
@@ -317,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)) {
@@ -327,32 +319,14 @@ static QStringList CreateEmulatorArgs(const Configuration& info) {
}
#ifdef __linux__
static QString BashQuote(QString value) {
value.replace('\'', "'\\''");
return QStringLiteral("'") + value + QStringLiteral("'");
}
static bool CreateBashScript(const QString& interpreter, const QStringList& args,
const QString& file_name) {
QFile file(file_name);
if (file.open(QIODevice::WriteOnly | QIODevice::Text)) {
QTextStream s(&file);
s << "#!/bin/bash\n";
s << BashQuote(interpreter);
for (const auto& arg: args) {
s << " " << BashQuote(arg);
}
s << "\n";
s << "echo Press any key...\n";
s << "read -n1\n";
file.close();
return file.setPermissions(file.permissions() | QFile::ExeUser | QFile::ExeOwner |
QFile::ExeGroup);
}
return false;
// Runs the command in bash, then waits for a key (with close_on_clean_exit, only after an error).
// The command goes in as positional parameters ("$@"), so it needs no quoting.
static QStringList BashCommand(const QString& interpreter, const QStringList& args,
bool close_on_clean_exit) {
const QString run = close_on_clean_exit ? "\"$@\" && exit" : "\"$@\"";
return QStringList {"bash", "-c", run + "; echo Press any key...; read -n1", "kyty",
interpreter} +
args;
}
// Find a terminal and its command separator.
@@ -416,28 +390,23 @@ static bool FindTerminal(QString* program, QStringList* prefix) {
#endif
#if defined(_WIN32)
// Quote one token for cmd.exe so paths with spaces survive /K parsing.
static QString WinCmdQuote(QString value) {
value.replace(QLatin1Char('"'), QStringLiteral("\\\""));
return QLatin1Char('"') + value + QLatin1Char('"');
}
static QString BuildWinCmdKCommand(const QString& interpreter, const QStringList& args) {
QString command = WinCmdQuote(QDir::toNativeSeparators(interpreter));
for (const auto& arg: args) {
command += QLatin1Char(' ');
command += WinCmdQuote(arg);
}
return command;
// cmd /K arguments that keep the console open (with close_on_clean_exit, only after an error).
// A quote is written as "" rather than \" so cmd's quote count stays even and operators inside
// arguments stay quoted.
static QString CmdCommand(const QString& interpreter, const QStringList& args,
bool close_on_clean_exit) {
static const QRegularExpression quote_or_end(QStringLiteral(R"((\\*)("|\z))"));
// The UCRT reads 2n backslashes before a quote as n.
auto quoted = QStringList {interpreter} + args;
quoted.replaceInStrings(quote_or_end, QStringLiteral(R"(\1\1\2\2)"));
return QStringLiteral("/K \"\"") + quoted.join(QStringLiteral("\" \"")) +
(close_on_clean_exit ? "\" && exit\"" : "\"\"");
}
#endif
void MainDialog::RunInterpreter(QProcess* process, const Configuration& info) {
void MainDialog::RunInterpreter(QProcess* process, const Configuration& info, bool close_terminal) {
const auto& interpreter = m_p->GetInterpreter();
QFileInfo f(interpreter);
auto dir = f.absoluteDir();
auto args = CreateEmulatorArgs(info);
if (args.isEmpty()) {
QMessageBox::critical(this, tr("Error"), tr("Invalid emulator configuration"));
@@ -445,40 +414,24 @@ void MainDialog::RunInterpreter(QProcess* process, const Configuration& info) {
return;
}
#ifdef __linux__
auto bash_file_name = dir.filePath(KYTY_BASH_FILE);
if (!CreateBashScript(interpreter, args, bash_file_name)) {
QMessageBox::critical(this, tr("Error"), tr("Can't create file:\n") + bash_file_name);
QApplication::quit();
return;
}
{
QString terminal;
QStringList terminal_prefix;
// Pass the script as a file argument (not bash -c) so paths with spaces work.
if (FindTerminal(&terminal, &terminal_prefix)) {
process->setProgram(terminal);
process->setArguments(terminal_prefix + QStringList {"bash", bash_file_name});
} else {
// Run without a terminal as a fallback.
process->setProgram(QStringLiteral("bash"));
process->setArguments({bash_file_name});
}
}
#elif defined(_WIN32)
{
// Use nativeArguments so Qt does not re-quote the /K command string.
process->setProgram(CMD_EXE);
process->setArguments({});
process->setNativeArguments(QStringLiteral("/K \"") +
BuildWinCmdKCommand(interpreter, args) + QLatin1Char('"'));
}
#else
// Without a terminal, the emulator runs directly.
process->setProgram(interpreter);
process->setArguments(args);
#ifdef __linux__
QString terminal;
QStringList terminal_prefix;
if (FindTerminal(&terminal, &terminal_prefix)) {
process->setProgram(terminal);
process->setArguments(terminal_prefix + BashCommand(interpreter, args, close_terminal));
}
#elif defined(_WIN32)
// Use nativeArguments so Qt does not re-quote the /K command string.
process->setProgram(CMD_EXE);
process->setArguments({});
process->setNativeArguments(
CmdCommand(QDir::toNativeSeparators(interpreter), args, close_terminal));
#endif
process->setWorkingDirectory(dir.path());
process->setWorkingDirectory(QFileInfo(interpreter).absolutePath());
#if defined(_WIN32)
process->setCreateProcessArgumentsModifier([](QProcess::CreateProcessArguments* args) {
args->flags |= static_cast<uint32_t>(CREATE_NEW_CONSOLE);
@@ -525,6 +478,7 @@ void MainDialogPrivate::WriteSettings(QSettings& s) {
s.setValue(SETTINGS_MAIN_LAST_GEOMETRY, g_last_geometry);
}
s.setValue(SETTINGS_CHECK_UPDATES, g_check_updates_on_startup);
s.setValue(SETTINGS_CLOSE_TERMINAL, g_close_terminal);
s.setValue(SETTINGS_THEME, g_theme);
s.endGroup();
@@ -535,6 +489,7 @@ void MainDialogPrivate::ReadSettings(QSettings& s) {
g_last_geometry = s.value(SETTINGS_MAIN_LAST_GEOMETRY, g_last_geometry).toByteArray();
g_check_updates_on_startup = s.value(SETTINGS_CHECK_UPDATES, true).toBool();
g_close_terminal = s.value(SETTINGS_CLOSE_TERMINAL, false).toBool();
g_theme = s.value(SETTINGS_THEME, 0).toInt();
if (g_theme < 0 || g_theme > 2) {
g_theme = 0;
@@ -547,17 +502,30 @@ void MainDialogPrivate::Run() {
if (m_update_checker->IsInstalling()) {
return;
}
m_running_item = m_ui->widget->GetSelectedItem();
if (m_running_item == nullptr) {
QPointer<ConfigurationItem> item = m_ui->widget->GetSelectedItem();
if (item == nullptr) {
return;
}
m_running_item->SetRunning(true);
item->SetRunning(true);
m_running_games++;
m_controller_preview.SetSuspended(true);
m_title_sound.SetTitle({});
auto info = m_ui->widget->CreateConfiguration(*m_running_item);
m_main_dialog->RunInterpreter(&m_process, *info);
auto* process = new QProcess(this);
connect(process, &QProcess::stateChanged, this,
[this, process, item](QProcess::ProcessState state) {
if (state == QProcess::NotRunning) {
if (item != nullptr) {
item->SetRunning(false);
}
m_running_games--;
process->deleteLater();
Update();
}
});
auto info = m_ui->widget->CreateConfiguration(*item);
m_main_dialog->RunInterpreter(process, *info, g_close_terminal);
Update();
}
@@ -565,8 +533,7 @@ void MainDialogPrivate::Run() {
void MainDialogPrivate::Update() {
const auto* item = m_ui->widget->GetSelectedItem();
bool run_enabled = (m_process.state() == QProcess::NotRunning && item != nullptr &&
!m_update_checker->IsInstalling());
bool run_enabled = item != nullptr && !item->IsRunning() && !m_update_checker->IsInstalling();
if (run_enabled) {
const auto& info = item->GetInfo();
@@ -575,7 +542,8 @@ void MainDialogPrivate::Update() {
}
m_ui->widget->SetRunEnabled(run_enabled);
if (m_process.state() != QProcess::NotRunning) {
m_update_checker->SetGameRunning(m_running_games > 0);
if (m_running_games > 0) {
m_controller_preview.SetSuspended(true);
return;
}
+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) {
+3 -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;
}
+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);
+2 -1
View File
@@ -149,7 +149,8 @@ int LoadExec(const char* path, const char* const argv[]) {
}
CloseHandle(process.hThread);
CloseHandle(process.hProcess);
std::quick_exit(0);
// Nonzero, or the launcher's "cmd /K ... && exit" closes the console the new process shares.
std::quick_exit(1);
#else
std::ifstream command("/proc/self/cmdline", std::ios::binary);
std::vector<std::string> host_arguments;
+6
View File
@@ -14,6 +14,7 @@
#include "common/threads.h"
#include "common/virtualMemory.h"
#include "graphics/host_gpu/pageManager.h"
#include "graphics/shader/recompiler/ir/passes/SrtWalker.h"
#include "kernel/fileSystem.h"
#include "kernel/memory.h"
#include "kernel/pthread.h"
@@ -672,6 +673,11 @@ static bool KytyExceptionHandler(const Common::HostException::ExceptionInfo& exc
Common::DebugCounters::Add(Common::DebugCounters::Counter::GpuFaults);
return true;
}
if (const auto length =
Libs::Graphics::ShaderRecompiler::IR::SrtWalkerLoadLength(info->exception_address)) {
Loader::X64InstructionEmulator::SkipLoad(info->native_context, length);
return true;
}
}
// Report whatever guest context can be read safely before terminating: which guest thread
// faulted, the register file, the faulting code bytes and the top of its stack.
+6
View File
@@ -822,4 +822,10 @@ bool TryEmulate(void* native_context) {
TryEmulateCpuExtensions(context);
}
void SkipLoad(void* native_context, size_t length) {
Context context {static_cast<decltype(Context::native)>(native_context)};
context.Gpr(0) = 0;
context.Advance(length);
}
} // namespace Loader::X64InstructionEmulator
+4
View File
@@ -1,9 +1,13 @@
#ifndef KYTY_LOADER_X64_INSTRUCTION_EMULATOR_H_
#define KYTY_LOADER_X64_INSTRUCTION_EMULATOR_H_
#include <cstddef>
namespace Loader::X64InstructionEmulator {
[[nodiscard]] bool TryEmulate(void* native_context);
// Completes a faulting load into EAX as a zero read and resumes after it.
void SkipLoad(void* native_context, size_t length);
} // namespace Loader::X64InstructionEmulator
+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) {
-80
View File
@@ -213,12 +213,6 @@ struct ProtectionCall {
std::vector<ProtectionCall> g_protection_log;
std::mutex g_protection_log_mutex;
struct ProtectionGate {
std::binary_semaphore entered{0};
std::binary_semaphore resume{0};
};
ProtectionGate *g_protection_gate = nullptr;
void ResetProtectionLog() {
g_protection_calls = 0;
g_protection_log.clear();
@@ -226,10 +220,6 @@ void ResetProtectionLog() {
bool ProtectAddressSpace(uint64_t vaddr, uint64_t size,
Common::VirtualMemory::Mode mode) {
if (g_protection_gate != nullptr && mode == Common::VirtualMemory::Mode::ReadWrite) {
g_protection_gate->entered.release();
g_protection_gate->resume.acquire();
}
uint32_t protection = PAGE_NOACCESS;
if (mode == Common::VirtualMemory::Mode::Read) {
protection = PAGE_READONLY;
@@ -417,74 +407,6 @@ void TestCpuDirtyUpload() {
Release(memory);
}
void TestBdaSyncDirtiness() {
constexpr auto page = Libs::Graphics::TRACKER_PAGE_SIZE;
TrackerHarness harness;
auto &tracker = harness.tracker;
auto *memory = Allocate(harness.page_manager, 3);
const auto address = reinterpret_cast<uint64_t>(memory);
Check(tracker.TakeBdaSync().Empty(), "new tracker queued unowned memory");
tracker.QueueBdaSync(address, 3 * page);
Check(tracker.IsRegionCpuModified(address, page), "new region was clean");
auto pending = tracker.TakeBdaSync();
Check(pending.Contains(address, 3 * page) && !pending.Intersects(address + 3 * page, page),
"mapped or registered range did not bound initial BDA synchronization");
tracker.ForEachUploadRange(address, 3 * page, false,
[](uint64_t, uint64_t) noexcept {}, []() noexcept {});
Check(tracker.TakeBdaSync().Empty(), "clean upload queued redundant BDA work");
tracker.MarkRegionAsCpuModified(address + 17, 1);
pending = tracker.TakeBdaSync();
Check(pending.Contains(address, page) && !pending.Intersects(address + page, page) &&
tracker.TakeBdaSync().Empty(),
"byte dirtiness did not drain exactly its whole tracker page");
// A write after Take belongs to the next drain, even when its page was just taken.
tracker.MarkRegionAsCpuModified(address + 31, 1);
Check(tracker.TakeBdaSync().Contains(address, page), "re-dirty event was lost");
tracker.ForEachUploadRange(address, page, true,
[](uint64_t, uint64_t) noexcept {}, []() noexcept {});
Check(tracker.TakeBdaSync().Empty(), "GPU ownership queued an unnecessary upload");
tracker.InvalidateRegion(address + 7, 1, [&] {
tracker.UnmarkRegionAsGpuModified(address, page);
tracker.MarkRegionAsCpuModified(address, page);
});
Check(tracker.TakeBdaSync().Contains(address, page),
"GPU-to-CPU invalidation did not queue BDA synchronization");
tracker.InvalidateRegion(address + page + 7, 1, [] {});
Check(tracker.TakeBdaSync().Contains(address + page, page),
"CPU invalidation did not queue BDA synchronization");
tracker.UntrackMemory(address, 3 * page);
Check(tracker.TakeBdaSync().Contains(address, 3 * page),
"untracked memory did not queue its newly dirty pages");
Release(memory);
}
void TestBdaSyncConcurrentDrain() {
constexpr auto page = Libs::Graphics::TRACKER_PAGE_SIZE;
TrackerHarness harness;
auto &tracker = harness.tracker;
auto *memory = Allocate(harness.page_manager, 1);
const auto address = reinterpret_cast<uint64_t>(memory);
tracker.ForEachUploadRange(address, page, false,
[](uint64_t, uint64_t) noexcept {}, []() noexcept {});
(void)tracker.TakeBdaSync();
ProtectionGate gate;
g_protection_gate = &gate;
std::jthread writer([&] { tracker.MarkRegionAsCpuModified(address + 17, 1); });
gate.entered.acquire();
// Ownership is still changing. Publishing its event before this point would let
// a drain consume it before the writer has finished releasing page protection.
Check(tracker.TakeBdaSync().Empty(), "BDA event preceded its state transition");
gate.resume.release();
writer.join();
g_protection_gate = nullptr;
Check(tracker.TakeBdaSync().Contains(address, page) &&
tracker.IsRegionCpuModified(address, page),
"concurrent drain lost a completed CPU dirty transition");
tracker.UntrackMemory(address, page);
Release(memory);
}
void TestCleanUploadPreservesOwnership() {
constexpr auto page_size = Libs::Graphics::TRACKER_PAGE_SIZE;
constexpr auto region_size = Libs::Graphics::TRACKER_REGION_SIZE;
@@ -1280,8 +1202,6 @@ int main(int argc, char **argv) {
TestQueriesDoNotRequireMappedOwnership();
TestConcurrentRegionPublication();
TestCpuDirtyUpload();
TestBdaSyncDirtiness();
TestBdaSyncConcurrentDrain();
TestCleanUploadPreservesOwnership();
TestRangeInvalidation();
TestGpuReacquisitionAfterInvalidation();
+298 -219
View File
@@ -1,5 +1,7 @@
#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"
#include <bit>
#include <cstdio>
@@ -16,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;
@@ -32,6 +50,24 @@ AddValueBlock(Libs::Graphics::ShaderRecompiler::IR::Program &program) {
return *result;
}
// Lists one extra source per block and walks the plan: each source's activity is its block's reachability.
std::vector<uint8_t> ReachedBlocks(Libs::Graphics::ShaderRecompiler::IR::ResourcePlan &plan,
const Libs::Graphics::ShaderRecompiler::IR::SrtRuntime &runtime) {
using namespace Libs::Graphics::ShaderRecompiler::IR;
const auto first = static_cast<uint32_t>(plan.descriptor_sources.size());
for (uint32_t block = 0; block < plan.control_flow.size(); ++block) {
plan.descriptor_sources.emplace_back();
plan.control_flow[block].sources.push_back(first + block);
}
CompileSrtWalker(plan);
std::vector<uint32_t> flat;
Check(WalkSrt(plan, runtime, flat, nullptr), "reachability walk failed");
std::vector<uint8_t> reached;
for (uint32_t block = 0; block < plan.control_flow.size(); ++block)
reached.push_back(WalkedActive(plan, first + block));
return reached;
}
Libs::Graphics::ShaderRecompiler::IR::Program SrtProgram(uint64_t address) {
using namespace Libs::Graphics::ShaderRecompiler::IR;
Program program;
@@ -73,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() {
@@ -94,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() {
@@ -149,10 +185,10 @@ Libs::Graphics::ShaderRecompiler::IR::Program MixedSamplerProgram() {
return program;
}
void TestMappedSrtUsesDirectReaderByDefault() {
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 =
@@ -162,10 +198,10 @@ void TestMappedSrtUsesDirectReaderByDefault() {
Check(MaterializeResources(plan, runtime, snapshot, specialization),
"mapped SRT stage materialization failed");
Check(specialization_reads == 0,
"ordinary SRT read used the specialization reader");
"walker load used the specialization reader");
Check(snapshot.flattened_srt.size() == 1 &&
snapshot.flattened_srt[0] == dword,
"cache rematerialization did not use the direct reader by default");
"walker did not load the mapped SRT DWORD");
}
void TestNativeNestedSrtRefresh() {
@@ -204,22 +240,11 @@ 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;
const SrtRuntime runtime{.user_data = user_data};
{
SrtWalker walker(plan, runtime);
Check(walker.RefreshFlatBuffer(snapshot.flattened_srt),
"native SRT refresh failed");
const auto loaded = first[1];
first[1] ^= 0xffffffffu;
uint32_t value = 0;
Check(walker.Evaluate(plan.srt_reads[0].value, value) && value == loaded,
"descriptor evaluation reloaded a value already copied by the native walker");
first[1] = loaded;
}
for (const auto *data : {&first, &second}) {
table = pointer_words(data->data());
Check(MaterializeResources(plan, runtime, snapshot, specialization) &&
@@ -227,14 +252,20 @@ void TestNativeNestedSrtRefresh() {
std::vector<uint32_t>{(*data)[1], table[0], table[1], (*data)[2]},
"native SRT refresh lost pointer masking, alignment, or current values");
}
{
// Unmapped tables and the null pointer they yield read as zero.
FakeGuestMemory unmapped;
const std::array<uint32_t, 2> unmapped_table{0x1000u, 0u};
Check(MaterializeResources(plan, {.user_data = unmapped_table}, snapshot, specialization) &&
snapshot.flattened_srt == std::vector<uint32_t>(4, 0u),
"faulting walker loads of a table and a null pointer did not read zero");
}
Check(!MaterializeResources(plan, {.user_data = std::span(user_data).first(1)},
snapshot, specialization),
"native SRT walk accepted truncated user data");
const std::array<uint32_t, 2> fake_user_data{0x1003u, 0xabcd0000u};
uint32_t reads = 0;
const SrtRuntime callback_runtime{
.user_data = fake_user_data,
.read_memory = +[](void *data, uint64_t address, std::span<uint32_t> words) {
FakeGuestMemory fake(+[](void *data, uint64_t address, std::span<uint32_t> words) {
++*static_cast<uint32_t *>(data);
switch (address) {
case 0x1000u: words[0] = 0x2003u; break;
@@ -244,18 +275,17 @@ void TestNativeNestedSrtRefresh() {
default: return false;
}
return true;
},
.userdata = &reads};
Check(MaterializeResources(plan, callback_runtime, snapshot, specialization) && reads == 4 &&
}, &reads);
Check(MaterializeResources(plan, {.user_data = fake_user_data}, snapshot, specialization) && reads == 4 &&
snapshot.flattened_srt ==
std::vector<uint32_t>{0x1234u, 0x2003u, 0xabcd0000u, 0x5678u},
"native-eligible SRT bypassed its callback or repeated nested pointer reads");
"native SRT walk repeated nested pointer reads");
leaf->SetArg(0, root);
leaf->SetArg(1, Value(0u));
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) &&
@@ -266,17 +296,6 @@ void TestNativeNestedSrtRefresh() {
Check(!MaterializeResources(bounded, {.user_data = base}, snapshot, specialization),
"native SRT accepted address underflow or 48-bit overflow");
}
high.Instruction()->SetArg(0, Value(ScalarReg(6)));
Check(ExtractResourcePlan(program).srt_code == nullptr,
"native SRT accepted nonadjacent user-data pointer words");
high.Instruction()->SetArg(0, Value(ScalarReg(5)));
program.memory_info[0].offset = 0;
program.srt_reads.push_back({pointer_low, 1});
program.srt_reads.push_back({pointer_high, 2});
leaf->SetArg(0, emit(ValueOpcode::GetAddressResource, {pointer_low, pointer_high}));
pointer_high.Instruction()->SetArg(1, Value(8u));
Check(ExtractResourcePlan(program).srt_code == nullptr,
"native SRT accepted nonadjacent nested pointer words");
}
void TestIntegerRuntimeValueFollowsSrtReads() {
@@ -328,7 +347,7 @@ void TestIntegerRuntimeValueFollowsSrtReads() {
"cyclic SRT read-first-lane dependency was accepted");
}
void TestSrtAliasesRetainReadPolicy() {
void TestSrtAliasesShareReads() {
using namespace Libs::Graphics::ShaderRecompiler::IR;
auto program = SrtProgram(0x1000u);
auto &block = *program.blocks[0];
@@ -356,70 +375,33 @@ void TestSrtAliasesRetainReadPolicy() {
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,
"retained plan still contains flattened-read aliases");
}
Check(plan.descriptor_sources[0].dwords[0] == plan.srt_reads[0].value &&
plan.srt_reads[0].value.Instruction()->Flags<SrtReadFlags>().clean == 1u &&
plan.srt_reads[1].value.Instruction()->Flags<SrtReadFlags>().clean == 0u &&
plan.srt_reads[2].value.Instruction()->Flags<SrtReadFlags>().clean == 1u,
"alias normalization lost read identity or retained discarded EXEC provenance");
Check(plan.srt_code == nullptr,
"captured strict reads compiled an unused x86 walker");
struct Reads { uint32_t strict = 0; uint32_t ordinary = 0; bool dirty = false; } reads;
const SrtRuntime runtime{
.read_memory = +[](void *data, uint64_t address, std::span<uint32_t> words) {
++static_cast<Reads *>(data)->ordinary;
if (address == 0x2000u) words[0] = 0x2222u;
else if (address == 0x3000u) words[0] = 0x8000u;
else if (address == 0x8000u) words[0] = 0xeeeeu;
else return false;
return true;
},
.userdata = &reads,
.read_specialization_memory = +[](void *data, uint64_t address,
std::span<uint32_t> words) {
Check(plan.descriptor_sources[0].dwords[0] == plan.srt_reads[0].value,
"alias normalization lost read identity");
struct Reads { uint32_t count = 0; bool dirty = false; } reads;
FakeGuestMemory fake(+[](void *data, uint64_t address, std::span<uint32_t> words) {
auto &reads = *static_cast<Reads *>(data);
++reads.strict;
if (reads.dirty) return false;
if (address == 0x3000u) words[0] = 0x1000u;
else if (address == 0x1000u) words[0] = 0x1111u;
++reads.count;
if (address == 0x1000u) words[0] = 0x1111u;
else if (address == 0x2000u) words[0] = 0x2222u;
else if (address == 0x3000u && !reads.dirty) words[0] = 0x1000u;
else return false;
return true;
}};
}, &reads);
std::vector<uint32_t> flat;
DescriptorValue descriptor;
{
SrtWalker clean(plan, CleanRuntime(runtime));
SrtWalker walker(plan, runtime, &clean);
Check(walker.RefreshFlatBuffer(flat) &&
flat == std::vector<uint32_t>{0x1111u, 0x2222u, 0x1000u} &&
walker.EvaluateDescriptor(0, descriptor) && descriptor.dwords[0] == 0x1111u &&
reads.strict == 2 && reads.ordinary == 1,
"normalized aliases changed nested strict reads or repeated a shared read");
}
Check(!SrtWalker(plan, runtime).RefreshFlatBuffer(flat),
"strict flat read accepted a missing clean evaluator");
auto no_reader = runtime;
no_reader.read_specialization_memory = nullptr;
{
SrtWalker clean(plan, CleanRuntime(no_reader));
Check(!SrtWalker(plan, no_reader, &clean).RefreshFlatBuffer(flat),
"strict flat read accepted a missing strict reader");
}
Check(SrtWalker(plan, runtime).EvaluateDescriptor(0, descriptor) &&
descriptor.dwords[0] == 0xeeeeu && reads.strict == 2 && reads.ordinary == 3,
"direct descriptor evaluation without a clean evaluator changed read domains");
reads.dirty = true;
{
SrtWalker clean(plan, CleanRuntime(runtime));
Check(!SrtWalker(plan, runtime, &clean).RefreshFlatBuffer(flat) &&
reads.strict == 3 && reads.ordinary == 3,
"dirty strict pointer fell back to an ordinary read");
}
Check(WalkSrt(plan, {}, flat, nullptr) && flat == std::vector<uint32_t>{0x1111u, 0x2222u, 0x1000u} &&
reads.count == 3,
"normalized aliases changed nested reads or repeated a shared read");
// An unreadable pointer reads as zero, and so does the null table it yields.
reads = {.dirty = true};
Check(WalkSrt(plan, {}, flat, nullptr) && flat == std::vector<uint32_t>{0u, 0x2222u, 0u} &&
reads.count == 3,
"unreadable pointer did not read as zero");
}
void TestUniformVectorDescriptorRead() {
@@ -453,59 +435,66 @@ void TestUniformVectorDescriptorRead() {
program.descriptor_sources.push_back(source);
program.info.buffers.push_back({.source = 0, .written = true});
Check(ValidateRuntimeValue(program, Value(&count)), "uniform DWORD count was rejected");
struct Reads { uint32_t value = 72; uint32_t strict = 0; uint32_t ordinary = 0; bool clean = true; } reads;
const SrtRuntime runtime{
.read_memory = [](void *data, uint64_t, std::span<uint32_t> words) {
++static_cast<Reads *>(data)->ordinary;
words[0] = 999;
return true;
},
.userdata = &reads,
.read_specialization_memory = [](void *data, uint64_t address, std::span<uint32_t> words) {
struct Reads { uint32_t value = 72; uint32_t count = 0; bool readable = true; } reads;
FakeGuestMemory fake([](void *data, uint64_t address, std::span<uint32_t> words) {
auto &reads = *static_cast<Reads *>(data);
++reads.strict;
if (!reads.clean || address != 0x1000u || words.size() != 1) return false;
++reads.count;
if (!reads.readable || address != 0x1000u || words.size() != 1) return false;
words[0] = reads.value;
return true;
}};
}, &reads);
const SrtRuntime runtime;
ResourceSnapshot snapshot;
ResourceSpecialization specialization;
for (const bool written : {false, true}) {
program.info.buffers[0].written = written;
auto plan = ExtractResourcePlan(program);
Check(plan.control_flow.empty() && plan.requires_specialization_memory &&
plan.capture_specialization_reads,
auto plan = Extract(program);
Check(plan.control_flow.empty() && plan.capture_specialization_reads,
"vector descriptor read depended on incidental control-flow capture");
reads = {};
Check(MaterializeResources(plan, runtime, snapshot, specialization) &&
snapshot.buffers[0].dwords[2] == 145 && reads.strict == 1 && reads.ordinary == 0 &&
snapshot.buffers[0].dwords[2] == 145 && reads.count == 1 &&
snapshot.specialization_reads ==
std::vector<std::pair<uint64_t, uint64_t>>{{0x1000u, 4u}},
"vector descriptor input was not read and captured exactly once");
reads.clean = false;
reads.strict = 0;
Check(!MaterializeResources(plan, runtime, snapshot, specialization) &&
reads.strict == 1 && reads.ordinary == 0,
"dirty vector descriptor input fell back to an ordinary memory read");
// An unreadable input reads as zero, so the count selects 0 + 1.
reads = {.readable = false};
Check(MaterializeResources(plan, runtime, snapshot, specialization) &&
snapshot.buffers[0].dwords[2] == 1 && reads.count == 1,
"unreadable vector descriptor input did not read as zero");
}
// A read guarded by its EXEC mask depends on that mask, so it cannot reuse the unmasked read.
auto &guarded = block.AppendNewInst(ValueOpcode::LoadBufferU32,
{Value(&handle), Value(0u), Value(0u), Value(0u), Value(&active)});
guarded.SetFlags(MemoryFlags{.index = 0});
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 = Extract(program);
uint32_t value = 0;
reads = {};
Check(EvaluateSrtValue(plan, runtime, plan.descriptor_sources.back().dwords[0], value) &&
value == 72u && reads.count == 1,
"EXEC-guarded vector read lost its mask");
}
count.SetArg(4, Value(false));
auto &inactive = block.AppendNewInst(ValueOpcode::ReadFirstLane, {Value(&count), Value(false)});
program.descriptor_sources.push_back({.dwords = {Value(&inactive)}, .dword_count = 1});
reads.strict = 0;
reads.count = 0;
uint32_t result = 99;
{
const auto plan = ExtractResourcePlan(program);
Check(SrtWalker(plan, runtime).Evaluate(plan.descriptor_sources.back().dwords[0], result) &&
result == 0 && reads.strict == 0 && reads.ordinary == 0,
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");
}
count.SetArg(4, Value(true));
handle.SetArg(3, Value(0x204u));
program.descriptor_sources.back().dwords[0] = Value(&count);
{
const auto plan = ExtractResourcePlan(program);
Check(SrtWalker(plan, runtime).Evaluate(plan.descriptor_sources.back().dwords[0], result) &&
result == 0 && reads.strict == 0 && reads.ordinary == 0,
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");
}
handle.SetArg(3, Value(0x16204u));
@@ -523,7 +512,7 @@ void TestExactReciprocalDescriptorArithmetic() {
std::bit_cast<float>(253u << 23u)}) {
auto &reciprocal = block.AppendNewInst(ValueOpcode::FPRecipIFlag32, {Value::F32(divisor)});
uint32_t result = 0;
Check(SrtWalker(program, {}).Evaluate(Value(&reciprocal), result) &&
Check(EvaluateSrtValue(program, {}, Value(&reciprocal), result) &&
result == std::bit_cast<uint32_t>(1.f / divisor),
"power-of-two reciprocal was not exact");
}
@@ -532,7 +521,7 @@ void TestExactReciprocalDescriptorArithmetic() {
std::bit_cast<float>(254u << 23u)}) {
auto &reciprocal = block.AppendNewInst(ValueOpcode::FPRecipIFlag32, {Value::F32(divisor)});
uint32_t result = 0;
Check(!SrtWalker(program, {}).Evaluate(Value(&reciprocal), result),
Check(!EvaluateSrtValue(program, {}, Value(&reciprocal), result),
"unsupported reciprocal rounding or exceptional input was accepted");
}
}
@@ -569,7 +558,7 @@ void TestWrittenDescriptorPredicateReads(bool memory_condition) {
source.dword_count = 4;
program.descriptor_sources.push_back(source);
program.info.buffers.push_back({.source = 0, .written = true});
// Only memory-derived host decisions need clean, disjoint descriptor reads.
// Only memory-derived host decisions capture their descriptor reads.
auto &condition = block.AppendNewInst(ValueOpcode::IEqual32,
{memory_condition ? Value(&read) : Value(&offset),
Value(memory_condition ? 0x8000u : 4u)});
@@ -592,38 +581,28 @@ 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 ordinary = 0; uint32_t strict = 0; bool clean = false; } reads;
struct Reads { uint32_t count = 0; bool readable = false; } reads;
const std::array<uint32_t, 1> user_data{4u};
const SrtRuntime runtime{
.user_data = user_data,
.read_memory = +[](void *data, uint64_t, std::span<uint32_t> words) {
++static_cast<Reads *>(data)->ordinary;
words[0] = 0x8000u;
return true;
},
.userdata = &reads,
.read_specialization_memory = +[](void *data, uint64_t address, std::span<uint32_t> words) {
FakeGuestMemory fake(+[](void *data, uint64_t address, std::span<uint32_t> words) {
auto &reads = *static_cast<Reads *>(data);
++reads.strict;
if (!reads.clean || address != 0x1004u) return false;
++reads.count;
if (!reads.readable || address != 0x1004u) return false;
words[0] = 0x8000u;
return true;
}};
}, &reads);
ResourceSnapshot snapshot;
ResourceSpecialization specialization;
for (const bool clean : {false, true}) {
reads = {.clean = clean};
// A failed predicate explores both edges; its dirty read also rejects the writable descriptor.
const bool expected = !memory_condition || clean;
Check(MaterializeResources(plan, runtime, snapshot, specialization) == expected &&
reads.ordinary == (memory_condition ? 0u : 1u) &&
reads.strict == (memory_condition ? 1u : 0u),
"writable descriptor changed reader policy or bypassed a dirty memory predicate");
if (!expected) continue;
Check(snapshot.buffers[0].dwords[0] == 0x8000u &&
for (const bool readable : {false, true}) {
reads = {.readable = readable};
// The predicate and the descriptor share one read. An unreadable one is zero, which
// makes a memory predicate skip the store block and leave its descriptor inactive.
Check(MaterializeResources(plan, {.user_data = user_data}, snapshot, specialization) &&
reads.count == 1,
"writable descriptor repeated or skipped its shared read");
Check(snapshot.buffers[0].dwords[0] == (readable ? 0x8000u : 0u) &&
snapshot.specialization_reads == (memory_condition
? std::vector<std::pair<uint64_t, uint64_t>>{{0x1004u, 4u}}
: std::vector<std::pair<uint64_t, uint64_t>>{}),
@@ -672,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 &&
@@ -680,16 +659,13 @@ void TestSharedControlPredicates() {
"shared predicates expanded into trees or retained varying dependencies");
for (const uint32_t flag : {0u, 1u}) {
const std::array<uint32_t, 1> user_data{flag};
SrtWalker walker(plan, {.user_data = user_data});
uint32_t result = 99;
const bool conjunction = operation == ValueOpcode::LogicalAnd;
const bool known = conjunction ? flag == 0 : flag != 0;
Check(walker.Evaluate(plan.control_flow[0].condition, result) == known &&
Check(EvaluateSrtValue(plan, {.user_data = user_data}, plan.control_flow[0].condition, result) == known &&
(!known || result == uint32_t{conjunction}),
"shared predicate changed its uniform factor or cached runtime values");
std::vector<uint32_t> flat;
Check(walker.RefreshFlatBuffer(flat) &&
plan.visited_blocks == (known
Check(ReachedBlocks(plan, {.user_data = user_data}) == (known
? (conjunction ? std::vector<uint8_t>{1, 1, 0, 1}
: std::vector<uint8_t>{1, 0, 1, 0})
: std::vector<uint8_t>{1, 1, 1, 1}),
@@ -747,35 +723,28 @@ void TestNestedExecPredicatesReuseReads() {
use.AppendNewInst(ValueOpcode::LoadBufferU32,
{buffer, Value(0u), Value(0u), Value(0u), Value(true)})
.SetFlags(MemoryFlags{.index = 1});
auto plan = ExtractResourcePlan(program);
struct Reads { uint32_t value = 0; bool clean = true; uint32_t strict = 0, ordinary = 0; } reads;
const SrtRuntime runtime{
.read_memory = +[](void *data, uint64_t, std::span<uint32_t>) {
++static_cast<Reads *>(data)->ordinary;
return false;
},
.userdata = &reads,
.read_specialization_memory = +[](void *data, uint64_t address, std::span<uint32_t> words) {
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);
++reads.strict;
Check(reads.strict <= depth + 1,
"shared EXEC predicates repeated scalar reads beyond their distinct masks");
if (!reads.clean || address != 0x1000u || words.size() != 1) return false;
++reads.count;
if (!reads.readable || address != 0x1000u || words.size() != 1) return false;
words[0] = reads.value;
return true;
}};
}, &reads);
ResourceSnapshot snapshot;
ResourceSpecialization specialization;
for (const auto state : {Reads{64u, true}, Reads{0u, true}, Reads{0u, false}, Reads{64u, true}}) {
for (const auto state : {Reads{64u, true}, Reads{0u, true}, Reads{64u, false}, Reads{64u, true}}) {
reads = state;
Check(MaterializeResources(plan, runtime, snapshot, specialization),
Check(MaterializeResources(plan, {}, snapshot, specialization),
"nested EXEC predicates rejected a valid resource snapshot");
const bool inactive = reads.clean && reads.value == 0;
// An unreadable count reads as zero.
const bool inactive = !reads.readable || reads.value == 0;
Check(snapshot.buffers[0].dwords[0] == (inactive ? 0u : 0x8000u),
"nested EXEC predicates lost an unknown branch or retained stale runtime facts");
Check(reads.strict > 0 && reads.ordinary == 0 &&
snapshot.specialization_reads.size() <= depth + 1,
"shared EXEC predicates repeated scalar reads beyond their distinct masks");
Check(reads.count == 1 && snapshot.specialization_reads ==
std::vector<std::pair<uint64_t, uint64_t>>{{0x1000u, 4u}},
"EXEC contexts repeated a mask-independent scalar read");
}
}
@@ -817,20 +786,12 @@ 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{};
uint32_t ordinary = 0;
} reads;
const SrtRuntime runtime{
.read_memory = +[](void *data, uint64_t, std::span<uint32_t>) {
++static_cast<Reads *>(data)->ordinary;
return false;
},
.userdata = &reads,
.read_specialization_memory = +[](void *data, uint64_t address,
std::span<uint32_t> words) {
FakeGuestMemory fake(+[](void *data, uint64_t address, std::span<uint32_t> words) {
if (words.size() != 1 || address < 0x1000u || address >= 0x100cu ||
(address & 3u) != 0) return false;
auto &reads = *static_cast<Reads *>(data);
@@ -838,7 +799,8 @@ void TestFiniteImageRefreshReusesScalarReads() {
++reads.counts[index];
words[0] = reads.words[index];
return true;
}};
}, &reads);
const SrtRuntime runtime;
ResourceSnapshot snapshot;
ResourceSpecialization specialization;
const auto capacities = [&] {
@@ -857,15 +819,15 @@ void TestFiniteImageRefreshReusesScalarReads() {
};
Check(MaterializeResources(plan, runtime, snapshot, specialization),
"finite image materialization failed");
Check(reads.ordinary == 0 && reads.counts == std::array<uint32_t, 3>{1, 1, 1} &&
Check(reads.counts == std::array<uint32_t, 3>{1, 1, 1} &&
snapshot.specialization_reads.size() == 3,
"finite image candidates repeated scalar reads or bypassed clean provenance");
"finite image candidates repeated or skipped scalar reads");
check_mapping({0, 1, 1, 1});
const auto warm_capacities = capacities();
reads.words = {0x300u, 0x300u, 0x400u};
Check(MaterializeResources(plan, runtime, snapshot, specialization),
"finite image refresh failed after descriptor changes");
Check(reads.ordinary == 0 && reads.counts == std::array<uint32_t, 3>{2, 2, 2} &&
Check(reads.counts == std::array<uint32_t, 3>{2, 2, 2} &&
snapshot.specialization_reads.size() == 3 &&
snapshot.images[0].dwords[0] == 0x300u && snapshot.images[1].dwords[0] == 0x400u,
"finite image refresh retained old scalar values or repeated reads");
@@ -967,37 +929,36 @@ 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);
struct Reads { uint32_t count = 0; bool clean = true; uint32_t descriptors = 0; } reads;
const SrtRuntime runtime{
.read_memory = +[](void *data, uint64_t address, std::span<uint32_t> words) {
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,
(3u << 28u) | 0x4dfacu};
auto &reads = *static_cast<Reads *>(data);
if (address == 0x1000u && reads.readable) {
words[0] = reads.count;
return true;
}
if (address < 0x2000u || address >= 0x2010u || address % 4u || words.size() != 1)
return false;
++static_cast<Reads *>(data)->descriptors;
++reads.descriptors;
words[0] = descriptor[(address - 0x2000u) / 4u];
return true;
},
.userdata = &reads,
.read_specialization_memory = +[](void *data, uint64_t address, std::span<uint32_t> words) {
const auto &reads = *static_cast<Reads *>(data);
if (!reads.clean || address != 0x1000u || words.size() != 1) return false;
words[0] = reads.count;
return true;
}};
}, &reads);
const SrtRuntime runtime;
ResourceSnapshot snapshot;
ResourceSpecialization specialization;
for (const auto state : {Reads{0u, true}, Reads{4u, true}, Reads{0u, false}, Reads{0u, true}}) {
for (const auto state : {Reads{0u, true}, Reads{4u, true}, Reads{4u, false}, Reads{0u, true}}) {
reads = state;
if (!counted_loop) {
uint32_t condition = 99;
Check(!SrtWalker(plan, runtime).Evaluate(plan.control_flow[3].condition, condition),
Check(!EvaluateSrtValue(plan, runtime, plan.control_flow[3].condition, condition),
"direct merge evaluation used missing or previous runtime reachability");
}
Check(MaterializeResources(plan, runtime, snapshot, specialization),
"partial traversal proof rejected a valid resource snapshot");
const bool inactive = !independent_mask && reads.clean && reads.count == 0u;
// An unreadable count reads as zero.
const bool inactive = !independent_mask && (!reads.readable || reads.count == 0u);
Check(snapshot.buffers[0].dwords[0] == (inactive ? 0u : 0x8000u),
"empty traversal kept its descriptor or an uncertain traversal lost it");
Check(specialization.buffers[0].zero_stride_oob == inactive,
@@ -1005,7 +966,7 @@ void TestEmptyTraversalSkipsUnmappedDescriptor(bool independent_mask, bool count
Check(reads.descriptors == (inactive ? 0u : 4u),
"inactive traversal read its descriptor or an uncertain traversal skipped it");
if (inactive) {
Check(!snapshot.specialization_reads.empty(), "empty traversal lost its clean scalar read");
Check(!snapshot.specialization_reads.empty(), "empty traversal lost its captured scalar read");
for (const auto &read : snapshot.specialization_reads)
Check(read == std::pair<uint64_t, uint64_t>{0x1000u, 4u},
"empty traversal proof captured an unrelated memory range");
@@ -1050,17 +1011,90 @@ void TestCyclicControlPredicatesRemainUnknown() {
branch(entry, u, left, right);
branch(left, a, right, exit);
branch(right, b, left, exit);
auto plan = ExtractResourcePlan(program);
SrtWalker walker(plan, {});
auto plan = Extract(program);
uint32_t value = 99;
Check(!walker.Evaluate(plan.control_flow[1].condition, value) &&
!walker.Evaluate(plan.control_flow[2].condition, value),
Check(!EvaluateSrtValue(plan, {}, plan.control_flow[1].condition, value) &&
!EvaluateSrtValue(plan, {}, plan.control_flow[2].condition, value),
"cyclic varying control acquired an unsupported uniform value");
std::vector<uint32_t> flat;
Check(walker.RefreshFlatBuffer(flat) && plan.visited_blocks == std::vector<uint8_t>({1, 1, 1, 1}),
Check(ReachedBlocks(plan, {}) == std::vector<uint8_t>({1, 1, 1, 1}),
"cyclic control lost a possible branch during reachability");
}
void TestBackEdgeReachesPhiPredecessor() {
using namespace Libs::Graphics::ShaderRecompiler::IR;
using Libs::Graphics::ShaderRecompiler::CFG::TerminatorKind;
Program program;
program.stage = Libs::Graphics::ShaderType::Compute;
program.user_data_count = 3;
program.srt_plan_complete = program.resource_tracking_complete = true;
for (uint32_t i = 0; i < 4; ++i) AddValueBlock(program);
auto &entry = *program.blocks[0];
auto &header = *program.blocks[1];
auto &body = *program.blocks[2];
auto &exit = *program.blocks[3];
const auto branch = [](Block &block, Block &target, Value condition = {}, Block *other = nullptr) {
block.terminator.kind = other ? TerminatorKind::ConditionalBranch : TerminatorKind::Branch;
block.terminator.true_block = &target;
block.terminator.false_block = other;
block.condition = condition;
block.AddBranch(&target);
if (other) block.AddBranch(other);
};
// header: p = Phi(entry: 0, body: 1); table[1] == p enters body, which returns to header. The
// first pass evaluates p and loads table[1] before body is reached; the walk must revisit both,
// capture the load once and reach exit, which loads table[0].
program.memory_info.push_back({.kind = ResourceKind::ScalarAddress});
const auto table = Value(&entry.AppendNewInst(ValueOpcode::GetAddressResource,
{Value(&entry.AppendNewInst(ValueOpcode::GetUserData, {Value(ScalarReg(0))})),
Value(&entry.AppendNewInst(ValueOpcode::GetUserData, {Value(ScalarReg(1))}))}));
const auto load = [&](Block &block, uint32_t offset) -> Inst & {
auto &read = block.AppendNewInst(ValueOpcode::LoadAddressU32, {table, Value(offset), Value(0u), Value(true)});
read.SetFlags(MemoryFlags{.index = 0});
return read;
};
auto &p = header.AppendNewInst(ValueOpcode::Phi);
p.SetFlags(Type::U32);
p.AddPhiOperand(&entry, Value(0u));
p.AddPhiOperand(&body, Value(1u));
branch(entry, header);
branch(header, body, Value(&header.AppendNewInst(ValueOpcode::IEqual32, {Value(&load(header, 4u)), Value(&p)})),
&exit);
branch(body, header);
exit.terminator.kind = TerminatorKind::Return;
program.srt_reads.push_back({Value(&load(exit, 0u)), 0});
exit.AppendNewInst(ValueOpcode::ReadConst,
{Value(&exit.AppendNewInst(ValueOpcode::GetSrtResource)), Value(0u)});
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;
const std::array<uint32_t, 2> user_data{0x1000u, 0u};
ResourceSnapshot snapshot;
ResourceSpecialization specialization;
{
// The table is unmapped: its loads read zero instead of faulting the walk.
FakeGuestMemory unmapped;
Check(MaterializeResources(plan, {.user_data = user_data}, snapshot, specialization) &&
snapshot.flattened_srt == std::vector<uint32_t>{0u},
"faulting load in a reachable block did not read zero");
}
for (uint32_t selected : {0u, 1u}) {
FakeGuestMemory fake(+[](void *userdata, uint64_t address, std::span<uint32_t> words) {
words[0] = address == 0x1000u ? 0x5678u : *static_cast<uint32_t *>(userdata);
return address == 0x1000u || address == 0x1004u;
}, &selected);
Check(MaterializeResources(plan, {.user_data = user_data}, snapshot, specialization) &&
snapshot.flattened_srt == std::vector<uint32_t>{0x5678u},
"back edge to an evaluated Phi left its exit unreachable");
Check(snapshot.specialization_reads ==
std::vector<std::pair<uint64_t, uint64_t>>{{0x1004u, 4u}, {0x1000u, 4u}},
"Phi restart kept the loads of the abandoned pass");
Check(ReachedBlocks(plan, {.user_data = user_data}) ==
(selected == 0u ? std::vector<uint8_t>{1, 1, 1, 1} : std::vector<uint8_t>{1, 1, 0, 1}),
"Phi restart followed a rejected edge or missed a reachable one");
}
}
void TestEmptyScalarBufferSpecialization() {
using namespace Libs::Graphics::ShaderRecompiler::IR;
struct Fixture {
@@ -1117,11 +1151,12 @@ 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, .read_memory = RejectSpecializationRead,
.userdata = &reads, .read_specialization_memory = RejectSpecializationRead};
const SrtRuntime runtime{.user_data = descriptor, .userdata = &reads,
.read_specialization_memory = RejectSpecializationRead};
FakeGuestMemory fake(RejectSpecializationRead, &reads);
ResourceSnapshot snapshot;
ResourceSpecialization specialization, empty, populated;
for (const uint32_t records : {0u, 1u, 0x20000000u, 0u}) {
@@ -1173,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;
@@ -1221,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;
@@ -1299,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,
@@ -1336,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 {
@@ -1353,10 +1430,10 @@ void DbgExit(int) { std::abort(); }
} // namespace Common
int main() {
TestMappedSrtUsesDirectReaderByDefault();
TestWalkerLoadsBypassSpecializationReader();
TestNativeNestedSrtRefresh();
TestIntegerRuntimeValueFollowsSrtReads();
TestSrtAliasesRetainReadPolicy();
TestSrtAliasesShareReads();
TestUniformVectorDescriptorRead();
TestExactReciprocalDescriptorArithmetic();
TestUnbasedFlatCacheHitMaterializes();
@@ -1370,10 +1447,12 @@ int main() {
TestEmptyTraversalSkipsUnmappedDescriptor(independent_mask);
TestEmptyTraversalSkipsUnmappedDescriptor(false, true);
TestCyclicControlPredicatesRemainUnknown();
TestBackEdgeReachesPhiPredecessor();
TestEmptyScalarBufferSpecialization();
TestMixedSamplerVariantsShareRuntimeDescriptor();
TestSignedStorageSpecialization();
for (const bool mixed : {false, true}) TestIndirectImageSamplerClasses(mixed);
TestShaderCacheRecords();
std::puts("ResourceMaterializationTests: all cases passed");
return 0;
}
+152 -154
View File
@@ -5,7 +5,7 @@
#include "graphics/shader/recompiler/ir/passes/ResourceMaterialization.h"
#include "graphics/shader/recompiler/ir/passes/ResourceTracking.h"
#include "graphics/shader/recompiler/ir/passes/ShaderInfoCollection.h"
#include "graphics/shader/recompiler/ir/passes/SrtWalker.h"
#include "SrtWalkerTestMemory.h"
#include <array>
#include <bit>
@@ -212,6 +212,12 @@ bool ReadLinearTestMemory(void *userdata, uint64_t address, std::span<uint32_t>
return true;
}
// Serves walker loads from the memory a test runtime currently points at.
bool ReadRuntimeMemory(void *userdata, uint64_t address, std::span<uint32_t> values) {
const auto &runtime = *static_cast<const SrtRuntime *>(userdata);
return runtime.read_specialization_memory(runtime.userdata, address, values);
}
std::unique_ptr<Fixture>
MakeIndirectImageFixture(bool malformed, uint32_t table_stride = 32, uint32_t table_offset = 0,
uint32_t table_shader_offset = 0, bool nested = false,
@@ -424,9 +430,8 @@ void TestInvariantIndirectImageMaterialization(bool nested) {
const auto handle = std::ranges::find_if(*wrap->block, [](const Inst &inst) {
return inst.GetOpcode() == ValueOpcode::GetImageResource;
});
SrtWalker walker(wrap->program, runtime);
uint32_t offset = 0;
Check(walker.Evaluate(handle->Arg(0), offset) && offset == expected,
Check(EvaluateSrtValue(wrap->program, runtime, handle->Arg(0), offset) && offset == expected,
"GPU descriptor offset projection lost U32 wrap or scalar immediate carry");
}
auto malformed = MakeIndirectImageFixture(true);
@@ -624,6 +629,7 @@ void TestBoundedImageViewEligibility() {
plan.descriptor_sources.push_back(source);
}
plan.descriptor_sources[image_source].indirect_descriptor->sources = std::move(selected);
CompileSrtWalker(plan);
Check(!MaterializeResources(plan, runtime, snapshot, specialization),
"explicitly selected incompatible descriptor was silently normalized to null");
}
@@ -873,9 +879,8 @@ void TestWaterfallImageTable() {
std::array{UINT32_MAX, 0x5204u, UINT32_MAX}, std::array{0u, 0u, UINT32_MAX}}) {
data[4] = index;
data[3] = format;
SrtWalker walker(fixture->program, runtime);
uint32_t offset = 0;
Check(walker.Evaluate(handle->Arg(0), offset) && offset == expected,
Check(EvaluateSrtValue(fixture->program, runtime, handle->Arg(0), offset) && offset == expected,
"VMEM table projection lost record bounds, format disable or multiplication carry");
}
auto disabled = make(true, false);
@@ -1113,7 +1118,7 @@ void TestNestedPointerImageTable() {
memory.fail_address = 0x1800u;
*at(table) = 0u;
Check(!MaterializeResources(plan, runtime, snapshot, specialization),
"unreadable null pointer row bypassed the strict pointee reader");
"unreadable null pointer row bypassed the specialization reader");
*at(table) = objects;
auto capped = make(4u);
@@ -1381,7 +1386,7 @@ void TestIndexedPointerImageTable() {
*at(table) = 0u;
*at(table + 4u) = 0u;
Check(!MaterializeResources(plan, runtime, snapshot, specialization),
"explicit null pointer bypassed the strict child reader");
"explicit null pointer bypassed the specialization reader");
auto reversed = make(Case::ReverseSigned);
reversed->PlanAndTrack();
for (const auto variant : {Case::MissingParent, Case::WrongParent, Case::MissingChild,
@@ -1398,8 +1403,9 @@ void TestIndexedPointerImageTable() {
const auto counted_plan = ExtractResourcePlan(counted->program);
constexpr uint32_t count_address = 0x1800u;
std::array<uint32_t, 6> counted_data{table, 1u | (stride << 16u), 2u, 0x5204u, count_address, 1u};
const SrtRuntime counted_runtime{.user_data = counted_data, .read_memory = ReadLinearTestMemory, .userdata = &memory,
const SrtRuntime counted_runtime{.user_data = counted_data, .userdata = &memory,
.read_specialization_memory = ReadLinearTestMemory};
FakeGuestMemory fake(ReadLinearTestMemory, &memory);
*at(table) = objects;
*at(table + 4u) = 0xbeef0001u;
*at(count_address) = *at(count_address + 4u) = 2u;
@@ -1431,9 +1437,11 @@ void TestIndexedPointerImageTable() {
Check(!MaterializeResources(counted_plan, counted_runtime, snapshot, specialization) && memory.watched_reads == 0u,
"runtime parent count exceeding V# records was clamped or read candidates");
*at(count_address) = 2u;
// An unreadable parent count reads as zero: an empty domain without candidate reads.
memory.fail_address = high + count_address;
Check(!MaterializeResources(counted_plan, counted_runtime, snapshot, specialization) && memory.watched_reads == 0u,
"unreadable parent count was ignored before candidate reads");
Check(MaterializeResources(counted_plan, counted_runtime, snapshot, specialization) &&
snapshot.images.size() == 1u && memory.watched_reads == 0u,
"unreadable parent count did not read as an empty domain");
memory.fail_address = UINT64_MAX;
for (const auto variant : {Case::MissingCount, Case::DifferentCount, Case::UnprovenMask}) {
auto unproved = make(variant);
@@ -1780,9 +1788,9 @@ void TestGuardedDirectImageTable() {
};
fill_table(memory, table);
std::array<uint32_t, 2> user_data{0x1000u, 0u};
SrtRuntime runtime{.user_data = user_data, .read_memory = ReadLinearTestMemory,
.userdata = &memory,
SrtRuntime runtime{.user_data = user_data, .userdata = &memory,
.read_specialization_memory = ReadLinearTestMemory};
FakeGuestMemory fake(ReadRuntimeMemory, &runtime);
ResourceSnapshot snapshot;
ResourceSpecialization specialization;
auto packed = make_plan(Guard::Plain, 0u, true);
@@ -2352,10 +2360,9 @@ void TestBoundedComputeImageLoop() {
(static_cast<uint32_t>(Libs::Graphics::Prospero::ImageType::kColor2D) << 28u);
}
std::array<uint32_t, 3> user_data{0x1800u, 0u, 2u};
SrtRuntime runtime{.user_data = user_data,
.read_memory = ReadLinearTestMemory,
.userdata = &memory,
SrtRuntime runtime{.user_data = user_data, .userdata = &memory,
.read_specialization_memory = ReadLinearTestMemory};
FakeGuestMemory fake(ReadLinearTestMemory, &memory);
ResourceSnapshot snapshot;
ResourceSpecialization specialization;
for (const auto post : {PostTest::Signed, PostTest::Unsigned}) {
@@ -2668,8 +2675,8 @@ void TestDescendingImageRange() {
std::array<uint32_t, 4> user_data {0x1000u, 0u, 0u, UINT32_MAX};
ResourceSnapshot snapshot;
ResourceSpecialization specialization;
const SrtRuntime runtime {.user_data = user_data, .read_memory = ReadLinearTestMemory,
.userdata = &memory, .read_specialization_memory = ReadLinearTestMemory};
const SrtRuntime runtime {.user_data = user_data, .userdata = &memory,
.read_specialization_memory = ReadLinearTestMemory};
for (const auto count : {0u, 2u, 0u}) {
user_data[2] = count;
Check(MaterializeResources(plan, runtime, snapshot, specialization) && snapshot.images.size() == 2u,
@@ -3176,7 +3183,7 @@ void TestUniformizedMaterialImageKeys() {
make_plan(Variant::FirstLane);
make_plan(Variant::FirstLaneDirect);
plan = make_plan(Variant::FirstLaneAndNot);
Check(plan.requires_specialization_memory &&
Check(plan.capture_specialization_reads &&
plan.descriptor_sources[plan.info.images[0].source]
.indirect_descriptor->selector_mask.Resolve().TryInstruction() != nullptr,
"uniformized image mask did not survive extraction");
@@ -3207,10 +3214,8 @@ void TestUniformizedMaterialImageKeys() {
user_data[9] = 16u << 16u;
user_data[10] = 1u;
memory.fail_address = base + 0xc00u + 3u * 0x90u;
const SrtRuntime runtime{
.user_data = user_data, .read_memory = ReadLinearTestMemory,
.userdata = &memory,
.read_specialization_memory = ReadLinearTestMemory};
const SrtRuntime runtime{.user_data = user_data, .userdata = &memory,
.read_specialization_memory = ReadLinearTestMemory};
ResourceSnapshot snapshot;
ResourceSpecialization specialization;
Check(MaterializeResources(plan, runtime, snapshot, specialization) &&
@@ -3388,7 +3393,7 @@ void TestIndexedMaterialImageKeys() {
if (shared) {
owner_key.ResolveInstruction()->ReplaceUsesWith(Value(12994u));
uint32_t offset = 0u;
Check(SrtWalker(f.program, {}).Evaluate(shared_image.ResolveInstruction()->Arg(0), offset) &&
Check(EvaluateSrtValue(f.program, {}, shared_image.ResolveInstruction()->Arg(0), offset) &&
offset == 12994u * 32u,
"inherited indexed selector changed GPU byte offsets into raw material keys");
}
@@ -3811,10 +3816,9 @@ void TestUniformScalarBufferImage() {
memory.words.begin() + (0x1010u + index * 48u) / 4u);
memory.words[(0x1010u + index * 48u) / 4u] += index;
}
SrtRuntime runtime{.user_data = user_data,
.read_memory = ReadLinearTestMemory,
.userdata = &memory,
SrtRuntime runtime{.user_data = user_data, .userdata = &memory,
.read_specialization_memory = ReadLinearTestMemory};
FakeGuestMemory fake(ReadLinearTestMemory, &memory);
ResourceSnapshot snapshot;
ResourceSpecialization specialization;
const auto materializes = [&](uint32_t address) {
@@ -3827,9 +3831,10 @@ void TestUniformScalarBufferImage() {
memory.words[0x44u / 4u] = 0u;
memory.words[0x1010u / 4u] = 0x30u;
Check(materializes(0x30u), "changed scalar table memory reused the previous image descriptor");
// An unreadable key reads as zero and selects the first record instead of the second.
memory.words[0x44u / 4u] = 1u;
memory.fail_address = 0x1044u;
Check(!MaterializeResources(plan, runtime, snapshot, specialization),
"unavailable nested image table was accepted");
Check(materializes(0x30u), "unavailable nested image key did not read as zero");
}
void TestCompactScalarImageTable() {
@@ -4258,8 +4263,8 @@ void TestScalarBufferSelectorDomain() {
(static_cast<uint32_t>(Libs::Graphics::Prospero::ImageType::kColor2D) << 28u);
}
std::array<uint32_t, 6> user_data {0x1000u, 0u, 180u, 0u, 0x2000u, 0u};
const SrtRuntime runtime {.user_data = user_data, .read_memory = ReadLinearTestMemory,
.userdata = &memory, .read_specialization_memory = ReadLinearTestMemory};
const SrtRuntime runtime {.user_data = user_data, .userdata = &memory,
.read_specialization_memory = ReadLinearTestMemory};
ResourceSnapshot snapshot;
ResourceSpecialization specialization;
memory.watched_address = 0x1000u + 144u;
@@ -4314,8 +4319,8 @@ void TestScalarBufferSelectorDomain() {
user_data[1] = 2u << 16u;
user_data[2] = UINT32_MAX;
Check(MaterializeResources(plan,
{.user_data = user_data, .read_memory = read_sparse, .userdata = &sparse,
.read_specialization_memory = read_sparse}, snapshot, specialization) &&
{.user_data = user_data, .userdata = &sparse, .read_specialization_memory = read_sparse},
snapshot, specialization) &&
sparse.offsets == std::vector<uint64_t>{144u, 0x80000090u} &&
snapshot.images.size() == 2u,
"large V# bounds extended the native U32 domain or invented an OOB zero key");
@@ -4644,7 +4649,8 @@ void TestCountedRowImageSelector() {
at(MaterialBase + 256u) = 0x200u;
at(MaterialBase + 260u) = static_cast<uint32_t>(Libs::Graphics::Prospero::BufferFormat::k8SScaled) << 20u;
at(MaterialBase + 268u) = at(MaterialBase + 196u);
std::array<uint32_t, 12> user_data{KeyBase, 4u << 16u, 64u, 0x14004u,
// The walker needs every user-data DWORD the plan reads, including the zero s12-s14.
std::array<uint32_t, 15> user_data{KeyBase, 4u << 16u, 64u, 0x14004u,
CountBase, 4u << 16u, 4u, 0x14004u, MaterialBase, 396u << 16u, 4u, 0x14004u};
const SrtRuntime runtime{.user_data = user_data, .userdata = &memory,
.read_specialization_memory = ReadLinearTestMemory};
@@ -5002,8 +5008,9 @@ void TestSharedAppendSelectorBounds() {
(static_cast<uint32_t>(Libs::Graphics::Prospero::ImageType::kColor2D) << 28u);
}
std::array<uint32_t, 9> user_data {0x1000u, 160u << 16u, 8u, 0u, 0x2000u, 0u, 0x1800u, 0u, 0u};
const SrtRuntime runtime {.user_data = user_data, .read_memory = ReadLinearTestMemory,
.userdata = &memory, .read_specialization_memory = ReadLinearTestMemory};
SrtRuntime runtime {.user_data = user_data, .userdata = &memory,
.read_specialization_memory = ReadLinearTestMemory};
FakeGuestMemory fake(ReadRuntimeMemory, &runtime);
ResourceSnapshot snapshot;
ResourceSpecialization specialization;
memory.fail_address = memory.watched_address = 0x1090u;
@@ -5085,9 +5092,8 @@ void TestSharedAppendSelectorBounds() {
user_data[2] = 65536u;
user_data[4] = 0x50000000u;
user_data[6] = 0x40000000u;
Check(MaterializeResources(plan,
{.user_data = user_data, .read_memory = read_boundary, .userdata = &boundary,
.read_specialization_memory = read_boundary}, snapshot, specialization) &&
runtime = {.user_data = user_data, .userdata = &boundary, .read_specialization_memory = read_boundary};
Check(MaterializeResources(plan, runtime, snapshot, specialization) &&
boundary.keys_read == 65536u && snapshot.images.size() == 1u &&
snapshot.images[0].dwords[0] == 0x101u,
"shared append materialization rejected its exact nonwrapping probe limit");
@@ -5103,9 +5109,7 @@ void TestSharedAppendSelectorBounds() {
for (const auto& test : overlaps) {
boundary.counts = test.counts;
boundary.keys_read = 0u;
Check(MaterializeResources(overlap_plan,
{.user_data = user_data, .read_memory = read_boundary, .userdata = &boundary,
.read_specialization_memory = read_boundary}, snapshot, specialization) &&
Check(MaterializeResources(overlap_plan, runtime, snapshot, specialization) &&
boundary.keys_read == test.expected[layout] && snapshot.images.size() == 1u &&
snapshot.images[0].dwords[0] == (test.expected[layout] != 0u ? 0x101u : 0u),
"shared append bounds lost overlap, included a touching list or retained stale keys");
@@ -5119,7 +5123,6 @@ void TestComputeBufferFill() {
bool conditional = false;
bool shifted = false;
bool extra_store = false;
bool clean = false;
bool branch = false;
};
const auto Run = [](Options options) {
@@ -5188,17 +5191,13 @@ void TestComputeBufferFill() {
words[0] = memory.words[0];
return true;
};
Check(MaterializeResources(
plan,
{.user_data = userdata,
.read_memory = Read,
.userdata = &memory,
.read_specialization_memory = options.clean ? Read : nullptr},
snapshot, specialization),
"fill fixture did not materialize");
const auto materialize = [&](SrtMemoryReader reader) {
FakeGuestMemory fake(reader, &memory);
return MaterializeResources(plan, {.user_data = userdata}, snapshot, specialization);
};
Check(materialize(Read), "fill fixture did not materialize");
const bool expected = !options.conditional && !options.shifted &&
!options.extra_store && !options.branch &&
(!options.scalar || options.clean);
!options.extra_store && !options.branch;
Check((snapshot.uniform_fill.words != 0) == expected,
"fill proof accepted an unsafe store or missed the real GTA3 clear");
if (expected) {
@@ -5207,21 +5206,19 @@ void TestComputeBufferFill() {
snapshot.uniform_fill.value ==
(options.scalar ? 0x40404040u : 0u),
"fill proof lost address coverage or the actual stored scalar");
if (options.scalar && options.clean) {
Check(MaterializeResources(plan,
{.user_data = userdata, .read_memory = Read, .userdata = &memory},
snapshot, specialization) && snapshot.uniform_fill.words == 0,
"an unavailable clean value retained a previous uniform fill");
if (options.scalar) {
Check(materialize(nullptr) && snapshot.uniform_fill.words == 1 &&
snapshot.uniform_fill.value == 0u,
"an unreadable fill value retained a previous uniform fill");
}
}
};
Run({});
Run({.scalar = true, .clean = true});
Run({.scalar = true});
Run({.conditional = true, .clean = true});
Run({.shifted = true, .clean = true});
Run({.extra_store = true, .clean = true});
Run({.clean = true, .branch = true});
Run({.conditional = true});
Run({.shifted = true});
Run({.extra_store = true});
Run({.branch = true});
}
void TestDenseBufferTracking() {
@@ -5332,12 +5329,12 @@ void TestRuntimeUnsignedMinDescriptor() {
SrtRuntime runtime{.user_data = user_data};
DescriptorValue value;
const auto source = fixture.program.info.buffers[0].source;
Check(SrtWalker(plan, runtime).EvaluateDescriptor(source, value) &&
Check(EvaluateSrtDescriptor(plan, runtime, source, value) &&
value.dwords[3] == 0x100u,
"runtime descriptor unsigned minimum did not clamp its first operand");
user_data[0] = 0x80u;
Check(
SrtWalker(plan, runtime).EvaluateDescriptor(source, value) &&
EvaluateSrtDescriptor(plan, runtime, source, value) &&
value.dwords[3] == 0x80u,
"runtime descriptor unsigned minimum did not preserve its first operand");
}
@@ -5488,12 +5485,12 @@ void TestSampleAdjustSamplerScratch() {
std::array<uint32_t, 4> user_data{4u, 1u, 2u, 0x80000abcu};
SrtRuntime runtime{.user_data = user_data};
DescriptorValue descriptor;
Check(SrtWalker(plan, runtime).EvaluateDescriptor(source, descriptor) &&
Check(EvaluateSrtDescriptor(plan, runtime, source, descriptor) &&
descriptor.dwords[3] == 0x80000abcu,
"SampleAdjust canonicalization lost sampler border fields");
const auto shift_source = fixture.program.info.samplers[
shift_sampler.Instruction()->Flags<uint32_t>()].source;
Check(SrtWalker(plan, runtime).EvaluateDescriptor(shift_source, descriptor) &&
Check(EvaluateSrtDescriptor(plan, runtime, shift_source, descriptor) &&
std::ranges::equal(std::span(descriptor.dwords).first(4),
std::array{0x36u, 0xfff000u, 0x02500000u, 0u}),
"SampleAdjust shift-only scratch was not reduced to its zero border payload");
@@ -5601,11 +5598,11 @@ void TestFmaskLoadSpecialization() {
"FMASK load did not lower to a value vector");
result = vector->Arg(0);
uint32_t value = 0;
Check(SrtWalker(fixture.program, {.user_data = user_data}).Evaluate(result, value) &&
Check(EvaluateSrtValue(fixture.program, {.user_data = user_data}, result, value) &&
value == 0x76543210u,
"FMASK load did not return the native sample-to-fragment mapping");
user_data[8] = 1;
Check(SrtWalker(fixture.program, {.user_data = user_data}).Evaluate(result, value) &&
Check(EvaluateSrtValue(fixture.program, {.user_data = user_data}, result, value) &&
value == 0u,
"inactive FMASK load did not preserve the execution mask");
ShaderComputeInputInfo compute{};
@@ -5849,16 +5846,14 @@ void TestSrtFlatteningAndRuntimeMemoization() {
std::array<uint32_t, 2> user_data{0x1000u, 0u};
TestMemory memory;
memory.words[1] = 0xdeadbeefu;
SrtRuntime runtime{.user_data = user_data,
.read_memory = ReadTestMemory,
.userdata = &memory};
SrtRuntime runtime{.user_data = user_data};
FakeGuestMemory fake(ReadTestMemory, &memory);
auto plan = ExtractResourcePlan(fixture.program);
DescriptorValue descriptor;
std::vector<uint32_t> flat;
const uint32_t request = fixture.program.info.buffers[0].source;
const auto refresh = [&](const ResourcePlan& plan) {
SrtWalker walker(plan, runtime);
return walker.EvaluateDescriptor(request, descriptor) && walker.RefreshFlatBuffer(flat);
return WalkSrt(plan, runtime, flat, nullptr) && WalkedDescriptor(plan, request, descriptor);
};
Check(refresh(plan), "typed runtime source evaluation failed");
Check(descriptor.dwords[0] == 0xdeadbeefu &&
@@ -5873,10 +5868,11 @@ void TestSrtFlatteningAndRuntimeMemoization() {
memory.reads = 0;
memory.fail_after = 0;
Check(!refresh(plan), "unavailable scalar memory was accepted");
Check(refresh(plan) && descriptor.dwords[0] == 0u && flat == std::vector<uint32_t>{0u},
"unavailable scalar memory did not read as zero");
memory.fail_after = UINT32_MAX;
Check(refresh(plan) && descriptor.dwords[0] == 0x12345678u && memory.reads == 1,
"failed runtime evaluation left a value marked as visiting");
"a zero read left a stale value in the next walk");
auto moved = std::move(plan);
memory.reads = 0;
@@ -5924,18 +5920,17 @@ void TestScalarDataReadControlFlow() {
{address, Value(0u), Value(0u), Value(true)}, fixture.AddMemory(scalar, 4), use);
fixture.PlanAndTrack();
auto plan = ExtractResourcePlan(fixture.program);
Check(plan.srt_reads.size() == 1 && !fixture.program.info.uses_dma &&
bool(plan.srt_code) == (comparison == ValueOpcode::Void),
"scalar data lost flattening or conditional reads entered the unconditional JIT");
Check(plan.srt_reads.size() == 1 && !fixture.program.info.uses_dma && plan.srt_code,
"scalar data lost flattening or its walker");
uint32_t reads = 0;
std::array<uint32_t, 2> user_data {};
SrtRuntime runtime {.user_data = user_data,
.read_memory = +[](void* data, uint64_t address, std::span<uint32_t> words) {
SrtRuntime runtime {.user_data = user_data};
FakeGuestMemory fake(+[](void* data, uint64_t address, std::span<uint32_t> words) {
++*static_cast<uint32_t*>(data);
if (address == 0) return false;
words[0] = 42;
return true;
}, .userdata = &reads};
}, &reads);
ResourceSnapshot snapshot;
ResourceSpecialization specialization;
for (const uint64_t pointer : {uint64_t{0}, uint64_t{0x1000}, uint64_t{1} << 32u}) {
@@ -5977,11 +5972,10 @@ void TestDynamicSrtReadRemainsExplicit() {
std::array<uint32_t, 3> user_data{0x1000u, 0u, 4u};
TestMemory memory;
memory.words[1] = 0xabcdef01u;
SrtRuntime runtime{.user_data = user_data,
.read_memory = ReadTestMemory,
.userdata = &memory};
SrtRuntime runtime{.user_data = user_data};
FakeGuestMemory fake(ReadTestMemory, &memory);
DescriptorValue value;
Check(SrtWalker(plan, runtime).EvaluateDescriptor(fixture.program.info.buffers[0].source, value) &&
Check(EvaluateSrtDescriptor(plan, runtime, fixture.program.info.buffers[0].source, value) &&
value.dwords[0] == 0xabcdef01u && memory.reads == 1,
"dynamic typed scalar descriptor source was not evaluated");
@@ -6091,8 +6085,9 @@ void TestBoundedFormattedBufferStore() {
memory.words[word + 3u] = static_cast<uint32_t>(Prospero::BufferFormat::k32UInt) << 12u | 4u;
}
std::array<uint32_t, 2> user_data{static_cast<uint32_t>(memory.base), 0u};
SrtRuntime runtime{.user_data = user_data, .read_memory = ReadLinearTestMemory,
.userdata = &memory, .read_specialization_memory = ReadLinearTestMemory};
SrtRuntime runtime{.user_data = user_data, .userdata = &memory,
.read_specialization_memory = ReadLinearTestMemory};
FakeGuestMemory fake(ReadLinearTestMemory, &memory);
ResourceSnapshot snapshot;
ResourceSpecialization specialization;
const auto materialize = [&] { return MaterializeResources(plan, runtime, snapshot, specialization); };
@@ -6316,8 +6311,9 @@ void TestBoundedBufferRecords(uint32_t d32_components) {
}
std::array<uint32_t, 6> user_data{uint32_t(memory.base) + table_offset, 8u << 16u,
4u * 192u / 8u, 0x5204u, uint32_t(memory.base), 0u};
SrtRuntime runtime{.user_data = user_data, .read_memory = ReadLinearTestMemory,
.userdata = &memory, .read_specialization_memory = ReadLinearTestMemory};
SrtRuntime runtime{.user_data = user_data, .userdata = &memory,
.read_specialization_memory = ReadLinearTestMemory};
FakeGuestMemory fake(ReadLinearTestMemory, &memory);
ResourceSnapshot snapshot;
ResourceSpecialization specialization;
const auto materialize = [&] { return MaterializeResources(plan, runtime, snapshot, specialization); };
@@ -6498,8 +6494,8 @@ void TestGuardedWaterfallBufferRecords() {
std::copy(poison.begin(), poison.end(), memory.words.begin() + (table_offset + 480u) / 4u);
std::array<uint32_t, 8> user_data{uint32_t(memory.base) + table_offset, 488u << 16u,
2u, 0x5204u, 0x9000u, 4u << 16u, 32u, 0x5204u};
SrtRuntime runtime{.user_data = user_data, .read_memory = ReadLinearTestMemory,
.userdata = &memory, .read_specialization_memory = ReadLinearTestMemory};
SrtRuntime runtime{.user_data = user_data, .userdata = &memory,
.read_specialization_memory = ReadLinearTestMemory};
ResourceSnapshot snapshot;
ResourceSpecialization specialization;
for (const auto guard : {Guard::Exact, Guard::Reversed, Guard::Greater, Guard::GreaterEqual,
@@ -6617,8 +6613,8 @@ void TestGpuSelectedBufferRecords() {
// SMEM aligns the base and dynamic offset independently; its V# format is ignored.
std::array<uint32_t, 8> user_data{uint32_t(memory.base) + table_offset + 3u, 0u, 0u, 0u,
uint32_t(memory.fail_address), 12u << 16u, 129600u, 0x5204u};
SrtRuntime runtime{.user_data = user_data, .read_memory = ReadLinearTestMemory,
.userdata = &memory, .read_specialization_memory = ReadLinearTestMemory};
SrtRuntime runtime{.user_data = user_data, .userdata = &memory,
.read_specialization_memory = ReadLinearTestMemory};
ResourceSnapshot snapshot;
ResourceSpecialization specialization;
const auto materialize = [&] { return MaterializeResources(plan, runtime, snapshot, specialization); };
@@ -6709,8 +6705,8 @@ void TestWorkgroupSelectedBufferRecords() {
std::array<uint32_t, 8> user_data{uint32_t(memory.base) + table, 192u << 16u, 2u, 0x5204u,
uint32_t(memory.base) + selector, 12u << 16u, MaxIndirectDescriptorProbes, 0x5204u};
memory.watched_address = memory.base;
SrtRuntime runtime{.user_data = user_data, .read_memory = ReadLinearTestMemory,
.userdata = &memory, .read_specialization_memory = ReadLinearTestMemory,
SrtRuntime runtime{.user_data = user_data, .userdata = &memory,
.read_specialization_memory = ReadLinearTestMemory,
.indirect_args_address = memory.base};
ResourceSnapshot snapshot;
ResourceSpecialization specialization;
@@ -6938,8 +6934,9 @@ void TestGpuSelectedFormattedRecordStore() {
memory.fail_address = memory.base + 0x2000u;
std::array<uint32_t, 8> user_data{uint32_t(memory.base) + table_offset, 592u << 16u, 6u, 0x5204u,
uint32_t(memory.fail_address), 4u << 16u, 64u, 0x5204u};
SrtRuntime runtime{.user_data = user_data, .read_memory = ReadLinearTestMemory,
.userdata = &memory, .read_specialization_memory = ReadLinearTestMemory};
SrtRuntime runtime{.user_data = user_data, .userdata = &memory,
.read_specialization_memory = ReadLinearTestMemory};
FakeGuestMemory fake(ReadRuntimeMemory, &runtime);
ResourceSnapshot snapshot;
ResourceSpecialization specialization;
const auto materialize = [&] { return MaterializeResources(plan, runtime, snapshot, specialization); };
@@ -7127,9 +7124,8 @@ void TestGpuSelectedFormattedRecordStore() {
std::array<uint32_t, 8> prefix_data{uint32_t(prefix_memory.base) + table_offset - 48u, 0u, 0u, 0u,
uint32_t(prefix_memory.base) + selector_offset, 8u << 16u, 12u, 0x5204u};
std::array counts{2u, 1u, 1u};
SrtRuntime prefix_runtime{.user_data = prefix_data, .read_memory = read_prefix,
.userdata = &prefix_memory, .read_specialization_memory = read_prefix,
.workgroup_counts = counts};
runtime = {.user_data = prefix_data, .userdata = &prefix_memory,
.read_specialization_memory = read_prefix, .workgroup_counts = counts};
prefix_memory.watched_address = prefix_memory.base + selector_offset + 4u;
for (const auto prefix : {Prefix::Forward, Prefix::Reversed}) {
const auto selected = make(16u, 16u, false, true, prefix);
@@ -7143,7 +7139,7 @@ void TestGpuSelectedFormattedRecordStore() {
const auto refresh = [&] {
prefix_memory.watched_reads = 0u;
prefix_memory.read_attempts = 0u;
return MaterializeResources(selected_plan, prefix_runtime, snapshot, specialization);
return MaterializeResources(selected_plan, runtime, snapshot, specialization);
};
for (const uint32_t first : {0u, 2u, 4u, 0u}) {
prefix_memory.words[first_offset / 4u] = first;
@@ -7155,9 +7151,12 @@ void TestGpuSelectedFormattedRecordStore() {
Check(std::ranges::find(snapshot.specialization_reads, read) != snapshot.specialization_reads.end(),
"runtime first or selected prefix escaped specialization read capture");
}
// An unreadable runtime first reads as zero.
prefix_memory.words[first_offset / 4u] = 4u;
prefix_memory.fail_address = prefix_memory.base + first_offset;
Check(!refresh() && prefix_memory.read_attempts == 1u,
"unreadable runtime first reached the selector payload");
Check(refresh() && snapshot.buffers.size() == selected_plan.info.buffers.size() + 1u &&
snapshot.buffers[wide_root].dwords[0] == 0x100000u,
"unreadable runtime first did not read as zero");
prefix_memory.fail_address = UINT64_MAX;
prefix_memory.words[first_offset / 4u] = 6u;
Check(!refresh(), "an actually selected invalid numeric descriptor was silently omitted");
@@ -7257,8 +7256,8 @@ void TestFiniteBufferSelection() {
}
memory.fail_address = memory.base + 32u + 5u * 16u;
const std::array<uint32_t, 2> user_data{static_cast<uint32_t>(memory.base), 0u};
SrtRuntime runtime{.user_data = user_data, .read_memory = ReadLinearTestMemory,
.userdata = &memory, .read_specialization_memory = ReadLinearTestMemory};
SrtRuntime runtime{.user_data = user_data, .userdata = &memory,
.read_specialization_memory = ReadLinearTestMemory};
ResourceSnapshot snapshot;
ResourceSpecialization specialization;
Check(MaterializeResources(plan, runtime, snapshot, specialization) && snapshot.buffers.size() == 5u,
@@ -7339,8 +7338,8 @@ void TestPackedScalarBufferSelection() {
// No descriptor exists after key 18; broad enumeration would read this address.
memory.fail_address = memory.base + 0x170u;
const std::array<uint32_t, 2> user_data{static_cast<uint32_t>(memory.base), 0u};
const SrtRuntime runtime{.user_data = user_data, .read_memory = ReadLinearTestMemory,
.userdata = &memory, .read_specialization_memory = ReadLinearTestMemory};
const SrtRuntime runtime{.user_data = user_data, .userdata = &memory,
.read_specialization_memory = ReadLinearTestMemory};
ResourceSnapshot snapshot;
ResourceSpecialization specialization;
const auto materialize = [&] { return MaterializeResources(plan, runtime, snapshot, specialization); };
@@ -7497,38 +7496,27 @@ void TestConditionalSamplerPhi() {
for (uint32_t word = 448 / 4; word < (480 + 16) / 4; ++word) {
memory.words[word] = 0x400u + word;
}
const SrtRuntime runtime{.read_memory = ReadLinearTestMemory,
.userdata = &memory,
.read_specialization_memory =
ReadLinearTestMemory};
FakeGuestMemory fake(ReadLinearTestMemory, &memory);
// Sampler selection compares a signed value against zero.
for (const auto flag : {-1, 0, 1, INT32_MIN, INT32_MAX}) {
memory.words[(0x1000 + 196) / 4] = std::bit_cast<uint32_t>(flag);
const uint32_t first = (flag < 0) != reverse ? 480 / 4 : 448 / 4;
memory.fail_address = 0x1000 + (first == 448 / 4 ? 480u : 448u);
DescriptorValue selected;
SrtWalker clean(plan, CleanRuntime(runtime));
Check(SrtWalker(plan, runtime, &clean).EvaluateDescriptor(source, selected),
Check(EvaluateSrtDescriptor(plan, {}, source, selected),
"conditional sampler did not survive detached plan lifetime");
for (uint32_t word = 0; word < 4; ++word) {
Check(selected.dwords[word] == memory.words[first + word],
"conditional sampler chose the wrong incoming descriptor");
}
}
DescriptorValue selected;
auto no_clean_reader = runtime;
no_clean_reader.read_specialization_memory = nullptr;
{
SrtWalker clean(plan, CleanRuntime(no_clean_reader));
Check(!SrtWalker(plan, no_clean_reader, &clean).EvaluateDescriptor(source, selected),
"conditional sampler used unchecked memory for its predicate");
}
// An unreadable negative predicate reads as zero and selects the nonnegative edge.
memory.words[(0x1000 + 196) / 4] = UINT32_MAX;
memory.fail_address = 0x2000 + 196;
{
SrtWalker clean(plan, CleanRuntime(runtime));
Check(!SrtWalker(plan, runtime, &clean).EvaluateDescriptor(source, selected),
"conditional sampler ignored unavailable coherent predicate memory");
}
DescriptorValue selected;
Check(EvaluateSrtDescriptor(plan, {}, source, selected) &&
selected.dwords[0] == memory.words[reverse ? 480 / 4 : 448 / 4],
"conditional sampler did not read unavailable predicate memory as zero");
}
}
CheckFatal([&] { ConditionalSamplerPlan(diamond, false, false, true); },
@@ -7592,10 +7580,9 @@ void TestFiniteImagePhiCycle() {
"finite image key did not preserve the loop-carried selection");
std::array<uint32_t, 16> data;
for (uint32_t i = 0; i < data.size(); ++i) data[i] = 100u + i;
SrtWalker walker(plan, SrtRuntime{.user_data = data});
for (uint32_t candidate = 0; candidate < 2; ++candidate) {
DescriptorValue descriptor;
Check(walker.EvaluateDescriptor(finite->sources[candidate], descriptor),
Check(EvaluateSrtDescriptor(plan, {.user_data = data}, finite->sources[candidate], descriptor),
"finite image candidate is not host-readable");
for (uint32_t word = 0; word < 8; ++word)
Check(descriptor.dwords[word] == data[candidate * 8 + word],
@@ -7666,8 +7653,8 @@ void TestPackedDescriptorWaterfall() {
memory.words[word + 1u] = 0x00fff000u;
memory.words[word + 2u] = (row & 1u) ? 0x0a500000u : 0u;
}
const SrtRuntime runtime{.user_data = user_data, .read_memory = ReadLinearTestMemory,
.userdata = &memory, .read_specialization_memory = ReadLinearTestMemory};
const SrtRuntime runtime{.user_data = user_data, .userdata = &memory,
.read_specialization_memory = ReadLinearTestMemory};
ResourceSnapshot snapshot;
ResourceSpecialization specialization;
Check(MaterializeResources(plan, runtime, snapshot, specialization) &&
@@ -7795,8 +7782,9 @@ void TestFiniteImageWaterfall() {
(uint32_t(Libs::Graphics::Prospero::ImageType::kColor2D) << 28u);
}
std::array<uint32_t, 4> user_data{uint32_t(memory.base) + 256u, 8u << 16u, 60u, 0x5204u};
SrtRuntime runtime{.user_data = user_data, .read_memory = ReadLinearTestMemory,
.userdata = &memory, .read_specialization_memory = ReadLinearTestMemory};
SrtRuntime runtime{.user_data = user_data, .userdata = &memory,
.read_specialization_memory = ReadLinearTestMemory};
FakeGuestMemory fake(ReadLinearTestMemory, &memory);
ResourceSnapshot snapshot;
ResourceSpecialization specialization, populated;
for (const uint32_t count : {0u, 60u, 0u, 60u}) {
@@ -8048,7 +8036,7 @@ void TestLoopCycleEnteredThroughRuntimeValue() {
const std::array<uint32_t, 1> user_data{0x4000u};
DescriptorValue descriptor;
Check(fixture.program.info.buffers.size() == 1u &&
SrtWalker(plan, {.user_data = user_data}).EvaluateDescriptor(
EvaluateSrtDescriptor(plan, {.user_data = user_data},
fixture.program.info.buffers[0].source, descriptor) &&
descriptor.dwords[0] == user_data[0],
"runtime-rooted invariant loop lost its buffer source");
@@ -8079,7 +8067,7 @@ void TestInvariantLoopPhi() {
std::array<uint32_t, 1> user_data{0x12345678u};
SrtRuntime runtime{.user_data = user_data};
DescriptorValue descriptor;
Check(SrtWalker(plan, runtime).EvaluateDescriptor(fixture.program.info.buffers[0].source, descriptor) &&
Check(EvaluateSrtDescriptor(plan, runtime, fixture.program.info.buffers[0].source, descriptor) &&
descriptor.dwords[0] == user_data[0],
"loop-invariant descriptor phi was not evaluated through typed SSA");
}
@@ -8196,8 +8184,7 @@ void TestBoundedRelativeRegisterWrites() {
const std::array<uint32_t, 3> user_data{0x1000u, 0u, 72u};
SrtRuntime runtime{.user_data = user_data};
DescriptorValue descriptor;
Check(SrtWalker(plan, runtime).EvaluateDescriptor(
fixture.program.info.buffers[0].source, descriptor) &&
Check(EvaluateSrtDescriptor(plan, runtime, fixture.program.info.buffers[0].source, descriptor) &&
descriptor.dwords[2] == 72u,
"impossible relative writes left a false descriptor dependency");
}
@@ -8412,6 +8399,7 @@ void TestConditionalBufferMaterialization() {
TestMemory memory;
SrtRuntime runtime{.user_data = user_data, .userdata = &memory,
.read_specialization_memory = ReadTestMemory};
FakeGuestMemory fake(ReadTestMemory, &memory);
ResourceSnapshot snapshot;
ResourceSpecialization specialization;
Check(MaterializeResources(plan, runtime, snapshot, specialization) &&
@@ -8422,12 +8410,11 @@ void TestConditionalBufferMaterialization() {
Check(snapshot.user_data == std::vector<uint32_t>(user_data.begin(), user_data.end()),
"resource reachability changed native shader user data");
// The walker needs every user-data DWORD it may read, even for an untaken branch.
runtime.user_data = std::span(user_data).first(4);
Check(MaterializeResources(plan, runtime, snapshot, specialization),
"untaken branch evaluated its unavailable descriptor");
memory.words[0] = 1;
Check(!MaterializeResources(plan, runtime, snapshot, specialization),
"taken branch accepted an unavailable descriptor");
"truncated user data was accepted");
memory.words[0] = 1;
runtime.user_data = user_data;
const auto CheckActive = [&] {
@@ -8438,9 +8425,13 @@ void TestConditionalBufferMaterialization() {
"potentially executed buffer descriptor was discarded");
};
CheckActive();
memory.words[0] = 0;
// An unreadable control word reads as zero and skips the branch.
memory.fail_after = memory.reads;
CheckActive();
Check(MaterializeResources(plan, runtime, snapshot, specialization) &&
snapshot.buffers.size() == 2 && snapshot.buffers[1].dwords == std::array<uint32_t, 8>{},
"unreadable scalar control word did not read as zero");
// Walker loads do not depend on the specialization reader.
memory.fail_after = UINT32_MAX;
runtime.read_specialization_memory = nullptr;
CheckActive();
}
@@ -8515,8 +8506,9 @@ void TestGuardedScalarDescriptorReads() {
return ReadLinearTestMemory(&memory.data, address, words);
};
const std::array<uint32_t, 5> user_data{0x1000u, 0u, 64u, 0u, 0x1040u};
const SrtRuntime runtime{.user_data = user_data, .read_memory = Read,
.userdata = &memory, .read_specialization_memory = Read};
const SrtRuntime runtime{.user_data = user_data, .userdata = &memory,
.read_specialization_memory = Read};
FakeGuestMemory fake(Read, &memory);
memory.data.words[0xa8 / 4] = 0x2000u;
memory.data.words[0xb0 / 4] = 4u;
ResourceSnapshot snapshot;
@@ -8526,8 +8518,9 @@ void TestGuardedScalarDescriptorReads() {
std::ranges::all_of(snapshot.flattened_srt, [](auto word) { return word == 0; }),
"disabled feature speculatively dereferenced its null BVH table");
memory.data.words[7] = 1;
Check(!MaterializeResources(plan, runtime, snapshot, specialization) && memory.null_reads == 1,
"active feature accepted an unreadable BVH table");
Check(MaterializeResources(plan, runtime, snapshot, specialization) && memory.null_reads == 1 &&
std::ranges::all_of(snapshot.flattened_srt, [](auto word) { return word == 0; }),
"active feature did not read its unreadable null BVH table as zero");
}
memory.data.words[0x40 / 4] = 0x1080u;
memory.data.watched_address = 0x10a8u;
@@ -8557,19 +8550,24 @@ void TestConservativeBufferReachability() {
TestMemory memory;
const SrtRuntime runtime{.user_data = user_data, .userdata = &memory,
.read_specialization_memory = ReadTestMemory};
FakeGuestMemory fake(ReadTestMemory, &memory);
for (const auto use : {ConditionalBufferUse::Shared, ConditionalBufferUse::Loop,
ConditionalBufferUse::Writable}) {
auto plan = ConditionalBufferPlan(use);
ResourceSnapshot snapshot;
ResourceSpecialization specialization;
std::array<uint32_t, 4> expected{};
if (use != ConditionalBufferUse::Writable)
std::copy_n(user_data.begin() + 4, expected.size(), expected.begin());
Check(MaterializeResources(plan, runtime, snapshot, specialization) &&
snapshot.buffers.size() == 2 &&
std::equal(expected.begin(), expected.end(),
snapshot.buffers[1].dwords.begin()),
"shared or loop-dependent resource was pruned, or inactive scalar branch retained");
// With a zero control word, the loop body's only other predecessor is itself.
for (const uint32_t flag : {0u, 1u}) {
memory.words[0] = flag;
std::array<uint32_t, 4> expected{};
if (use == ConditionalBufferUse::Shared || flag != 0u)
std::copy_n(user_data.begin() + 4, expected.size(), expected.begin());
Check(MaterializeResources(plan, runtime, snapshot, specialization) &&
snapshot.buffers.size() == 2 &&
std::equal(expected.begin(), expected.end(),
snapshot.buffers[1].dwords.begin()),
"shared or entered loop resource was pruned, or an untaken branch or loop retained");
}
}
}
+51 -39
View File
@@ -4,7 +4,7 @@
#include "graphics/shader/recompiler/ir/passes/ReadLaneElimination.h"
#include "graphics/shader/recompiler/ir/passes/ResourceTracking.h"
#include "graphics/shader/recompiler/ir/passes/ResourceMaterialization.h"
#include "graphics/shader/recompiler/ir/passes/SrtWalker.h"
#include "SrtWalkerTestMemory.h"
#include <cstdlib>
#include <iostream>
@@ -95,6 +95,16 @@ bool ReadMemory(void *userdata, uint64_t address, std::span<uint32_t> values) {
return true;
}
// Evaluates the DWORDs of a descriptor source that no resource consumes.
bool EvaluateSource(ResourcePlan &plan, const SrtRuntime &runtime, uint32_t source,
DescriptorValue &result) {
const auto &words = plan.descriptor_sources[source];
result = {.dword_count = words.dword_count};
for (uint32_t word = 0; word < words.dword_count; ++word)
if (!EvaluateSrtValue(plan, runtime, words.dwords[word], result.dwords[word])) return false;
return true;
}
Value Address(Fixture &fixture, Value low, Value high, uint32_t block = 0) {
return fixture.Emit(ValueOpcode::GetAddressResource, {low, high}, 0, block);
}
@@ -132,9 +142,9 @@ void TestImmediateFlatteningAndGvn() {
"flattened raw read was not kept as a planning-only root");
TestMemory memory_image{{{0x1020u, 0xfeedbeefu}}};
SrtRuntime runtime{.read_memory = ReadMemory, .userdata = &memory_image};
FakeGuestMemory fake(ReadMemory, &memory_image);
std::vector<uint32_t> flat;
Check(SrtWalker(ExtractResourcePlan(fixture.program), runtime).RefreshFlatBuffer(flat), "flattened SRT walk failed");
Check(WalkSrt(ExtractResourcePlan(fixture.program), {}, flat, nullptr), "flattened SRT walk failed");
Check(flat == std::vector<uint32_t>{0xfeedbeefu} && memory_image.reads == 1,
"flattened SRT did not evaluate its canonical read once");
}
@@ -148,9 +158,9 @@ void TestRawScalarComponentAlignment() {
fixture.Plan();
TestMemory memory_image{{{0x1000u, 0x12345678u}}};
SrtRuntime runtime{.read_memory = ReadMemory, .userdata = &memory_image};
FakeGuestMemory fake(ReadMemory, &memory_image);
std::vector<uint32_t> flat;
Check(SrtWalker(ExtractResourcePlan(fixture.program), runtime).RefreshFlatBuffer(flat), "raw scalar SRT walk failed");
Check(WalkSrt(ExtractResourcePlan(fixture.program), {}, flat, nullptr), "raw scalar SRT walk failed");
Check(
flat == std::vector<uint32_t>{0x12345678u} && memory_image.reads == 1,
"raw scalar base, immediate, and offset were not aligned independently");
@@ -203,9 +213,9 @@ void TestNestedSrtWalk() {
fixture.Plan();
TestMemory memory_image{{{0x1000u, 0x2000u}, {0x2000u, 0xabcdef01u}}};
SrtRuntime runtime{.read_memory = ReadMemory, .userdata = &memory_image};
FakeGuestMemory fake(ReadMemory, &memory_image);
std::vector<uint32_t> flat;
Check(SrtWalker(ExtractResourcePlan(fixture.program), runtime).RefreshFlatBuffer(flat), "nested SRT walk failed");
Check(WalkSrt(ExtractResourcePlan(fixture.program), {}, flat, nullptr), "nested SRT walk failed");
Check(flat == std::vector<uint32_t>({0x2000u, 0xabcdef01u}),
"nested typed SRT reads were not evaluated in dependency order");
}
@@ -227,8 +237,9 @@ void TestShaderBaseAndUserData() {
const std::array user_data{0x20u};
SrtRuntime runtime{.user_data = user_data,
.shader_base = 0x12345678abcdef00ull};
auto plan = ExtractResourcePlan(fixture.program);
DescriptorValue result;
Check(SrtWalker(ExtractResourcePlan(fixture.program), runtime).EvaluateDescriptor(0, result),
Check(EvaluateSource(plan, runtime, 0, result),
"shader-relative descriptor evaluation failed");
Check(result.dword_count == 3 && result.dwords[0] == 0xabcdef00u &&
result.dwords[1] == 0x12345678u && result.dwords[2] == 0x24u,
@@ -249,8 +260,9 @@ void TestCarryAndBitFields() {
fixture.program.descriptor_sources.push_back(
{.dwords = {low, high, inserted, sign}, .dword_count = 4});
auto plan = ExtractResourcePlan(fixture.program);
DescriptorValue result;
Check(SrtWalker(ExtractResourcePlan(fixture.program), {}).EvaluateDescriptor(0, result),
Check(EvaluateSource(plan, {}, 0, result),
"carry and bit-field descriptor evaluation failed");
Check(result.dwords[0] == 1u && result.dwords[1] == 1u &&
result.dwords[2] == 0x89abcdefu && result.dwords[3] == 0xffffffffu,
@@ -273,12 +285,12 @@ void TestInvariantAndDivergentPhi() {
fixture.program.descriptor_sources.push_back(
{.dwords = {Value(&divergent)}, .dword_count = 1});
const auto plan = ExtractResourcePlan(fixture.program);
auto plan = ExtractResourcePlan(fixture.program);
DescriptorValue result;
Check(SrtWalker(plan, {}).EvaluateDescriptor(0, result) &&
Check(EvaluateSource(plan, {}, 0, result) &&
result.dwords[0] == 7u,
"loop-invariant typed phi was rejected");
Check(!SrtWalker(plan, {}).EvaluateDescriptor(1, result),
Check(!EvaluateSource(plan, {}, 1, result),
"divergent phi was accepted");
}
@@ -294,7 +306,7 @@ void TestOperandMutationLifecycle() {
source->ReplaceUsesWith(Value(0xaau));
uint32_t result = 0;
Check(source->UseCount() == 0 &&
SrtWalker(fixture.program, {}).Evaluate(inserted, result) && result == 0x3au,
EvaluateSrtValue(fixture.program, {}, inserted, result) && result == 0x3au,
"inline operand replacement changed the evaluated value");
for (const auto opcode : {ValueOpcode::GetImageResource, ValueOpcode::MakeImageAddress}) {
@@ -333,7 +345,7 @@ void TestOperandMutationLifecycle() {
const auto sum = fixture.Emit(ValueOpcode::IAdd32, {Value(&phi), Value(5u)}, 0, 5);
phi.ReplaceUsesWith(Value(6u));
Check(source->UseCount() == 0 && phi.NumPhiBlocks() == 0 && phi.NumArgs() == 1 &&
SrtWalker(fixture.program, {}).Evaluate(sum, result) && result == 11,
EvaluateSrtValue(fixture.program, {}, sum, result) && result == 11,
"Phi replacement retained incoming uses or changed the consumer value");
}
@@ -374,8 +386,9 @@ void TestRuntime64BitDescriptorOps() {
fixture.program.descriptor_sources.push_back(
{.dwords = {low, high}, .dword_count = 2});
auto plan = ExtractResourcePlan(fixture.program);
DescriptorValue result;
Check(SrtWalker(ExtractResourcePlan(fixture.program), {}).EvaluateDescriptor(0, result) &&
Check(EvaluateSource(plan, {}, 0, result) &&
result.dwords[0] == 0xabcdu && result.dwords[1] == 0x1235u,
"64-bit typed descriptor arithmetic evaluation is incorrect");
}
@@ -448,13 +461,13 @@ void TestUniformFirstLaneSamplerLod() {
std::array<uint32_t, 7> user_data{};
user_data[6] = 3u;
SrtRuntime runtime{.user_data = user_data};
const auto plan = ExtractResourcePlan(fixture.program);
auto plan = ExtractResourcePlan(fixture.program);
DescriptorValue result;
Check(SrtWalker(plan, runtime).EvaluateDescriptor(0, result) &&
Check(EvaluateSource(plan, runtime, 0, result) &&
result.dwords[0] == 0x00300300u,
"uniform sampler LOD evaluated incorrectly");
user_data[6] = 20u;
Check(SrtWalker(plan, runtime).EvaluateDescriptor(0, result) &&
Check(EvaluateSource(plan, runtime, 0, result) &&
result.dwords[0] == 0x00ffffffu,
"uniform sampler LOD clamp evaluated incorrectly");
}
@@ -489,13 +502,12 @@ void TestFloatComparisonDescriptorInputs() {
fixture.program.descriptor_sources.push_back(
{.dwords = {low, high}, .dword_count = 2});
const auto plan = ExtractResourcePlan(fixture.program);
auto plan = ExtractResourcePlan(fixture.program);
for (size_t index = 0; index < inputs.size(); index++) {
const auto &input = inputs[index];
const std::array user_data{input.bits};
DescriptorValue result;
Check(SrtWalker(plan, {.user_data = user_data})
.EvaluateDescriptor(0, result) &&
Check(EvaluateSource(plan, {.user_data = user_data}, 0, result) &&
result.dwords[0] == (flush && index < 4 ? 1 : input.less_equal) &&
result.dwords[1] == (flush && index < 4 ? 1 : input.greater_equal),
"descriptor comparison disagrees with native FP32 input mode");
@@ -551,7 +563,6 @@ void TestConstantBufferBounds() {
uint32_t immediate;
uint32_t stride;
uint32_t records;
bool valid;
uint32_t expected;
uint32_t reads;
};
@@ -565,17 +576,17 @@ void TestConstantBufferBounds() {
++probe.attempts;
return ReadMemory(&probe.memory, address, values);
};
// PS5 scalar-buffer loads return zero per out-of-range DWORD; an unreadable
// in-range address remains a failed memory read.
for (const auto &test : {Case{12u, 0u, 0u, 16u, true, 0xa5a5a5a5u, 1u},
Case{3u, 1u, 0u, 16u, true, 0x12345678u, 1u},
Case{0xfffffffcu, 4u, 0u, 16u, true, 0u, 0u},
Case{16u, 0u, 0u, 16u, true, 0u, 0u},
Case{12u, 0u, 0u, 14u, true, 0u, 0u},
Case{0u, 0u, 0u, 0u, true, 0u, 0u},
Case{8u, 0u, 0u, 16u, false, 0u, 1u},
Case{12u, 0u, 48u, 32768u, true, 0xa5a5a5a5u, 1u},
Case{0x6339c0u, 16u, 48u, 32768u, true, 0u, 0u}}) {
// PS5 scalar-buffer loads return zero per out-of-range DWORD without an access;
// an unreadable in-range address is accessed and reads as zero too.
for (const auto &test : {Case{12u, 0u, 0u, 16u, 0xa5a5a5a5u, 1u},
Case{3u, 1u, 0u, 16u, 0x12345678u, 1u},
Case{0xfffffffcu, 4u, 0u, 16u, 0u, 0u},
Case{16u, 0u, 0u, 16u, 0u, 0u},
Case{12u, 0u, 0u, 14u, 0u, 0u},
Case{0u, 0u, 0u, 0u, 0u, 0u},
Case{8u, 0u, 0u, 16u, 0u, 1u},
Case{12u, 0u, 48u, 32768u, 0xa5a5a5a5u, 1u},
Case{0x6339c0u, 16u, 48u, 32768u, 0u, 0u}}) {
Fixture fixture;
const auto memory = fixture.AddMemory(ResourceKind::ScalarBuffer, test.immediate);
const auto buffer =
@@ -588,12 +599,11 @@ void TestConstantBufferBounds() {
fixture.Plan();
MemoryProbe memory_image;
SrtRuntime runtime{.read_memory = read_memory, .userdata = &memory_image};
FakeGuestMemory fake(read_memory, &memory_image);
std::vector<uint32_t> flat;
Check(SrtWalker(ExtractResourcePlan(fixture.program), runtime).RefreshFlatBuffer(flat) == test.valid,
Check(WalkSrt(ExtractResourcePlan(fixture.program), {}, flat, nullptr),
"constant-buffer walk misaligned or wrapped its offset components");
Check((!test.valid || flat == std::vector<uint32_t>{test.expected}) &&
memory_image.attempts == test.reads,
Check(flat == std::vector<uint32_t>{test.expected} && memory_image.attempts == test.reads,
"constant-buffer walk read the wrong word or accessed an out-of-bounds address");
}
}
@@ -684,7 +694,8 @@ void TestCompleteUMinReduction() {
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});
@@ -831,8 +842,9 @@ void TestUndefinedRuntimeValueFails() {
fixture.program.descriptor_sources.push_back(
{.dwords = {undef}, .dword_count = 1});
auto plan = ExtractResourcePlan(fixture.program);
DescriptorValue result;
Check(!SrtWalker(ExtractResourcePlan(fixture.program), {}).EvaluateDescriptor(0, result),
Check(!EvaluateSource(plan, {}, 0, result),
"undefined typed descriptor source was accepted");
}
File diff suppressed because it is too large. Load diff
+3 -6
View File
@@ -40,9 +40,7 @@ void CheckRejected(const ShaderMappedData& data, const char* text) {
output.vertex_buffer_reg = 37;
output.vertex_attrib_reg = 41;
output.input_semantics = prior_semantics;
std::string error;
Check(!ShaderReadVertexMetadata(data, 64, output, &error), text);
Check(!error.empty(), "metadata rejection omitted its diagnostic");
Check(ShaderReadVertexMetadata(data, 64, output) != nullptr, text);
Check(output.vertex_buffer_reg == 37 && output.vertex_attrib_reg == 41 &&
output.input_semantics.data() == prior_semantics.data() &&
output.input_semantics.size() == prior_semantics.size(),
@@ -52,8 +50,7 @@ void CheckRejected(const ShaderMappedData& data, const char* text) {
void TestValidAndInvalidMetadata() {
Fixture fixture;
ShaderVertexMetadata output;
std::string error;
Check(ShaderReadVertexMetadata(fixture.mapped, 64, output, &error),
Check(ShaderReadVertexMetadata(fixture.mapped, 64, output) == nullptr,
"valid AGC vertex metadata was rejected");
Check(output.vertex_buffer_reg == 2 && output.vertex_attrib_reg == 4 &&
output.input_semantics.size() == 1,
@@ -64,7 +61,7 @@ void TestValidAndInvalidMetadata() {
AGC_ILLEGAL_DIRECT_OFFSET;
buffer_only.mapped.input_semantics = nullptr;
buffer_only.mapped.num_input_semantics = 0;
Check(ShaderReadVertexMetadata(buffer_only.mapped, 64, output, &error),
Check(ShaderReadVertexMetadata(buffer_only.mapped, 64, output) == nullptr,
"vertex-buffer-only AGC metadata was rejected");
Check(output.vertex_buffer_reg == 2 && output.vertex_attrib_reg == -1 &&
output.input_semantics.empty(),
+103
View File
@@ -0,0 +1,103 @@
#include "common/slotVector.h"
#include <cstdint>
#include <cstdio>
#include <cstdlib>
#include <utility>
#include <vector>
namespace {
void Check(bool value, const char *message) {
if (!value) {
std::fprintf(stderr, "SlotVectorTests: failed: %s\n", message);
std::abort();
}
}
// Non-movable like the cached Image/Buffer; counts live objects.
struct Probe {
Probe(int *live, uint32_t value) : live(live), value(value) { ++*live; }
~Probe() { --*live; }
KYTY_CLASS_NO_COPY(Probe);
int *live;
uint32_t value;
};
static_assert(sizeof(Common::SlotId) == sizeof(uint32_t));
void TestInsertEraseReuse() {
int live = 0;
Common::SlotVector<Probe> slots;
Check(!slots.is_allocated(Common::SlotId {}) && slots.try_get(Common::SlotId {}) == nullptr &&
!slots.is_allocated(Common::SlotId {0}),
"an empty vector reported an allocated slot");
const auto a = slots.insert(&live, 0u);
const auto b = slots.insert(&live, 1u);
const auto c = slots.insert(&live, 2u);
Check(a.index == 0 && b.index == 1 && c.index == 2 && live == 3,
"fresh slots were not handed out in order");
Check(slots.is_allocated(b) && slots.try_get(b) == &slots[b] && slots[b].value == 1,
"an inserted slot did not expose its object");
Check(!slots.is_allocated(Common::SlotId {3}), "an unused slot reported an object");
slots.erase(b);
slots.erase(a);
Check(live == 1 && !slots.is_allocated(a) && !slots.is_allocated(b) &&
slots.try_get(b) == nullptr && slots.is_allocated(c),
"erase did not destroy and free exactly its slot");
// Freed indices are reused most recently freed first, with the same plain id.
const auto reused_a = slots.insert(&live, 0u);
const auto reused_b = slots.insert(&live, 1u);
const auto d = slots.insert(&live, 3u);
Check(reused_a == a && reused_b == b && d.index == 3 && live == 4,
"freed slots were not reused in LIFO order");
Check(slots[reused_a].value == 0 && slots[reused_b].value == 1,
"a reused slot did not expose its new object");
}
void TestForEachAndStability() {
int live = 0;
{
Common::SlotVector<Probe> slots;
const auto first = slots.insert(&live, 0u);
Probe *const first_address = &slots[first];
// Cross several chunks; earlier objects must keep their address.
std::vector<Common::SlotId> ids {first};
for (uint32_t i = 1; i < 1000; ++i) {
ids.push_back(slots.insert(&live, i));
}
Check(&slots[first] == first_address && live == 1000,
"growing the vector moved a stored object");
std::vector<uint32_t> expected;
for (uint32_t i = 0; i < 1000; ++i) {
if (i % 3 == 0) {
slots.erase(ids[i]);
} else {
expected.push_back(i);
}
}
std::vector<uint32_t> visited;
std::as_const(slots).ForEach([&](Common::SlotId id, const Probe &probe) {
Check(probe.value == id.index, "ForEach passed an id that does not match its object");
visited.push_back(id.index);
});
Check(visited == expected && live == static_cast<int>(expected.size()),
"ForEach did not visit exactly the live slots in index order");
}
Check(live == 0, "destroying the vector did not destroy every live object exactly once");
}
} // namespace
int main() {
TestInsertEraseReuse();
TestForEachAndStability();
std::puts("SlotVectorTests: all cases passed");
return 0;
}
+113
View File
@@ -0,0 +1,113 @@
#ifndef KYTY_TESTS_SRTWALKERTESTMEMORY_H_
#define KYTY_TESTS_SRTWALKERTESTMEMORY_H_
#include "graphics/shader/recompiler/ir/passes/SrtWalker.h"
#include <xbyak/xbyak.h>
#include <csignal>
#include <cstdlib>
#include <utility>
#include <vector>
#if defined(__linux__)
#include <ucontext.h>
#elif defined(_WIN32)
#include <windows.h>
#endif
// Serves SRT walker loads from fake guest addresses: a walker load that faults completes with
// the test reader's DWORD, or with zero like the emulator when the reader fails or is null.
class FakeGuestMemory {
public:
explicit FakeGuestMemory(Libs::Graphics::ShaderRecompiler::IR::SrtMemoryReader reader = nullptr,
void *userdata = nullptr) {
Current() = {reader, userdata};
#if defined(__linux__)
struct sigaction action {};
action.sa_sigaction = Handle;
action.sa_flags = SA_SIGINFO;
sigaction(SIGSEGV, &action, &m_previous);
#elif defined(_WIN32)
m_handler = AddVectoredExceptionHandler(1, Handle);
#endif
}
~FakeGuestMemory() {
#if defined(__linux__)
sigaction(SIGSEGV, &m_previous, nullptr);
#elif defined(_WIN32)
RemoveVectoredExceptionHandler(m_handler);
#endif
Current() = {};
}
FakeGuestMemory(const FakeGuestMemory &) = delete;
FakeGuestMemory &operator=(const FakeGuestMemory &) = delete;
private:
using Reader = std::pair<Libs::Graphics::ShaderRecompiler::IR::SrtMemoryReader, void *>;
static Reader &Current() {
static thread_local Reader reader;
return reader;
}
static uint32_t Read(uint64_t address) {
uint32_t word = 0;
const auto [reader, userdata] = Current();
return reader != nullptr && reader(userdata, address, {&word, 1}) ? word : 0u;
}
#if defined(__linux__)
static void Handle(int, siginfo_t *, void *context) {
auto &registers = static_cast<ucontext_t *>(context)->uc_mcontext.gregs;
const auto length =
Libs::Graphics::ShaderRecompiler::IR::SrtWalkerLoadLength(registers[REG_RIP]);
if (length == 0) std::abort();
registers[REG_RAX] = Read(registers[REG_R10]);
registers[REG_RIP] += length;
}
struct sigaction m_previous {};
#elif defined(_WIN32)
static LONG CALLBACK Handle(EXCEPTION_POINTERS *info) {
auto &context = *info->ContextRecord;
const auto length = Libs::Graphics::ShaderRecompiler::IR::SrtWalkerLoadLength(context.Rip);
if (info->ExceptionRecord->ExceptionCode != EXCEPTION_ACCESS_VIOLATION || length == 0)
return EXCEPTION_CONTINUE_SEARCH;
context.Rax = Read(context.R10);
context.Rip += length;
return EXCEPTION_CONTINUE_EXECUTION;
}
void *m_handler = nullptr;
#endif
};
// Evaluates one value through a walker compiled for it, keeping the plan's resource walker.
inline bool EvaluateSrtValue(Libs::Graphics::ShaderRecompiler::IR::ResourcePlan &plan,
const Libs::Graphics::ShaderRecompiler::IR::SrtRuntime &runtime,
Libs::Graphics::ShaderRecompiler::IR::Value value, uint32_t &result) {
using namespace Libs::Graphics::ShaderRecompiler::IR;
const auto *inst = value.Resolve().TryInstruction();
const auto output = inst != nullptr ? inst->WalkOutput() : UINT32_MAX;
auto code = std::move(plan.srt_code);
auto scratch = std::move(plan.srt_scratch);
const auto user_data = plan.srt_user_data;
const auto captures = plan.srt_captures;
CompileSrtWalker(plan, {&value, 1});
std::vector<uint32_t> flat;
const bool known = WalkSrt(plan, runtime, flat, nullptr) && WalkedValue(plan, value, result);
plan.srt_code = std::move(code);
plan.srt_scratch = std::move(scratch);
plan.srt_user_data = user_data;
plan.srt_captures = captures;
if (inst != nullptr) inst->SetWalkOutput(output);
return known;
}
// Walks the plan's resources and reads back one descriptor.
inline bool EvaluateSrtDescriptor(const Libs::Graphics::ShaderRecompiler::IR::ResourcePlan &plan,
const Libs::Graphics::ShaderRecompiler::IR::SrtRuntime &runtime,
uint32_t source,
Libs::Graphics::ShaderRecompiler::IR::DescriptorValue &result) {
using namespace Libs::Graphics::ShaderRecompiler::IR;
std::vector<uint32_t> flat;
return WalkSrt(plan, runtime, flat, nullptr) && WalkedDescriptor(plan, source, result);
}
#endif // KYTY_TESTS_SRTWALKERTESTMEMORY_H_
+40
View File
@@ -720,6 +720,44 @@ void TestBackingReadExcludesWritesAndUnmap() {
std::printf("[host] %-48s ok\n", test);
}
void TestTrackerProtectSharesBackingReads() {
using namespace Libs::LibKernel::Memory;
const char* test = "TrackerProtectSharesBackingReads";
const auto base = MapNamedFlexible(test, SceKernelPageSize, SceKernelProtCpuRw,
"tracker_protect_shares_reads");
std::counting_semaphore<2> entered {0};
std::counting_semaphore<2> resume {0};
g_backing_reads_entered = &entered;
g_continue_backing_reads = &resume;
TestSetBackingReadCallback(ParkBackingRead);
uint64_t value = 0;
std::thread reader([&] { CopySparseMemory(base, &value, sizeof(value)); });
const bool reader_parked = entered.try_acquire_for(std::chrono::seconds(5));
std::counting_semaphore<2> done {0};
bool tracker_ok = false;
int guest_result = OK;
std::thread protector([&] {
tracker_ok = ProtectGuestHostMemory(base, SceKernelPageSize,
Common::VirtualMemory::Mode::ReadWrite);
done.release();
guest_result =
KernelMprotect(reinterpret_cast<void*>(base), SceKernelPageSize, SceKernelProtCpuRw);
done.release();
});
const bool shared = done.try_acquire_for(std::chrono::seconds(5));
const bool excluded = !done.try_acquire_for(std::chrono::milliseconds(100));
resume.release();
reader.join();
protector.join();
TestSetBackingReadCallback(nullptr);
CheckOk(test, KernelMunmap(base, SceKernelPageSize), "KernelMunmap");
Check(test, reader_parked && shared && tracker_ok,
"tracker protection waited for a backing read");
Check(test, excluded && guest_result == OK,
"guest mprotect completed while a backing read held its lock");
std::printf("[host] %-48s ok\n", test);
}
void TestGuestAddressSpaceHasNoFixedFallback() {
const char* test = "GuestAddressSpaceHasNoFixedFallback";
const auto unowned_address = reinterpret_cast<void*>(0x10000);
@@ -4894,6 +4932,7 @@ int main(int argc, char** argv) {
if (argc == 2 && std::strcmp(argv[1], "--backing-transfers-only") == 0) {
RunTest(TestConcurrentBackingReads);
RunTest(TestBackingReadExcludesWritesAndUnmap);
RunTest(TestTrackerProtectSharesBackingReads);
RunTest(TestSparseBackingReadPreservesResidency);
RunTest(TestCopySparseMemoryMappings);
RunTest(TestSparseReadDuringDirectCommit);
@@ -4953,6 +4992,7 @@ int main(int argc, char** argv) {
RunTest(TestSparseReadDuringDirectCommit);
RunTest(TestConcurrentBackingReads);
RunTest(TestBackingReadExcludesWritesAndUnmap);
RunTest(TestTrackerProtectSharesBackingReads);
RunTest(TestGuestAddressSpaceHasNoFixedFallback);
RunTest(TestGuestFreeRangeSearchDoesNotUnderflow);
RunTest(TestFlexibleMemoryCapacityIsBootFixed);
+123 -157
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"
@@ -24,13 +25,13 @@
#include "graphics/shader/recompiler/ir/passes/ResourceTracking.h"
#include "graphics/shader/recompiler/ir/passes/ResourceMaterialization.h"
#include "graphics/shader/recompiler/ir/passes/ShaderInfoCollection.h"
#include "graphics/shader/recompiler/ir/passes/SrtWalker.h"
#include "graphics/shader/recompiler/ir/passes/SsaRewrite.h"
#include "graphics/shader/shader.h"
#include "graphics/shader/shaderCompiler.h"
#include "libs/agc.h"
#include "spirv-tools/libspirv.hpp"
#include "xxhash.h"
#include "SrtWalkerTestMemory.h"
#include <algorithm>
#include <array>
@@ -135,8 +136,7 @@ struct TestCompileResult {
TestCompileResult RecompileForTest(
std::span<const uint32_t> code,
const ShaderRecompiler::CompileOptions &options,
ShaderRecompiler::IR::SrtMemoryReader read_memory = nullptr,
void *read_memory_data = nullptr, uint32_t push_data_start_dword = 0) {
uint32_t push_data_start_dword = 0) {
auto translated = ShaderRecompiler::TranslateProgram(code, options);
auto plan = ShaderRecompiler::IR::ExtractResourcePlan(translated.program);
ShaderRecompiler::IR::ResourceSnapshot resources;
@@ -144,8 +144,6 @@ TestCompileResult RecompileForTest(
const ShaderRecompiler::IR::SrtRuntime runtime{
.user_data = options.user_data,
.shader_base = reinterpret_cast<uint64_t>(code.data()),
.read_memory = read_memory,
.userdata = read_memory_data,
.read_specialization_memory = ReadHostTestMemory,
};
Check(ShaderRecompiler::IR::MaterializeResources(
@@ -166,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();
}
@@ -1160,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,
@@ -1169,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);
@@ -1588,11 +1586,6 @@ void SetImageTestFormat(std::array<uint32_t, 64> *data, uint32_t srsrc,
(*data)[format_dword] = static_cast<uint32_t>(format) << 20u;
}
bool ReadZeroTestMemory(void *, uint64_t, std::span<uint32_t> values) {
std::ranges::fill(values, 0u);
return true;
}
constexpr uint32_t EncodeVop2(uint32_t opcode, uint32_t dst, uint32_t src0,
uint32_t src1) {
return ((opcode & 0x3fu) << 25u) | ((dst & 0xffu) << 17u) |
@@ -1914,6 +1907,13 @@ void TestVertexBufferGrouping() {
input.resources[2].Base48() == input.buffers[0].addr,
"a later lower-address attribute or changed table left stale vertex offsets");
}
// The vertex table metadata is parsed when the shader is mapped.
user_data.direct_resource_count = 0;
(void)PrepareProgram(regs, HW::Context{}, HW::UserConfig{}, input);
Check(input.fetch_attrib_reg == 2, "a draw reread the vertex metadata of a mapped shader");
ShaderMapUserData(regs.es_regs.data_addr, mapped);
(void)PrepareProgram(regs, HW::Context{}, HW::UserConfig{}, input);
Check(!input.fetch_embedded, "remapping a shader kept its previous vertex metadata");
}
void TestNggVertexEntryState() {
@@ -5408,7 +5408,8 @@ void TestNewShaderRecompilerMemoryFamilyTranslation() {
options.dump_ir = true;
options.user_data = user_data;
auto result = RecompileForTest(shader, options, ReadZeroTestMemory);
FakeGuestMemory zero_memory;
auto result = RecompileForTest(shader, options);
Check((result.decoded_dump.find("s_load_dword") != std::string::npos),
"new decoder did not decode SMEM dword load");
Check((result.decoded_dump.find("s_buffer_load_dword") != std::string::npos),
@@ -9223,7 +9224,7 @@ void TestSharedReturnPreservesDescriptorDominance() {
user_data[29] = static_cast<uint32_t>(address >> 32u);
auto options = MakeCompileOptions(ShaderType::Pixel);
options.user_data = user_data;
auto result = RecompileForTest(shader, options, ReadHostTestMemory);
auto result = RecompileForTest(shader, options);
Check(!result.program.dispatcher_fallback &&
result.resources.buffers.size() == 1u &&
result.resources.buffers[0].dwords[0] == 0x2000u,
@@ -9283,7 +9284,7 @@ void TestNestedSelectionPreservesDescriptorSources() {
auto options = MakeCompileOptions(ShaderType::Compute);
options.user_data_base = 8;
options.user_data = user_data;
const auto result = RecompileForTest(shader, options, ReadHostTestMemory);
const auto result = RecompileForTest(shader, options);
Check(!result.program.dispatcher_fallback && result.resources.buffers.size() == 4u,
"nested descriptor selection lost its structured resource bindings");
for (size_t i = 0; i < result.program.info.buffers.size(); ++i) {
@@ -9359,7 +9360,7 @@ void TestSharedExitPreservesNativeDescriptorSources() {
user_data[2] = arm;
auto options = MakeCompileOptions(ShaderType::Pixel);
options.user_data = user_data;
const auto result = RecompileForTest(shader, options, ReadHostTestMemory);
const auto result = RecompileForTest(shader, options);
Check(!result.program.dispatcher_fallback && result.resources.buffers.size() == 2u,
"shared-exit descriptor selection lost its structured resource bindings");
for (size_t i = 0; i < result.program.info.buffers.size(); ++i) {
@@ -9405,7 +9406,7 @@ void TestNativeScalarReadDescriptorPlanning() {
user_data[8] = mode;
auto options = MakeCompileOptions(ShaderType::Pixel);
options.user_data = user_data;
const auto result = RecompileForTest(shader, options, ReadHostTestMemory);
const auto result = RecompileForTest(shader, options);
const auto expected = mode == 2u ? table : std::array<uint32_t, 4>{};
Check(!result.program.dispatcher_fallback && result.resources.buffers.size() == 1u &&
std::ranges::equal(result.resources.flattened_srt, expected),
@@ -9507,7 +9508,7 @@ void TestNativeGuardedSamplerSource(bool inline_constants) {
auto options = MakeCompileOptions(ShaderType::Pixel);
options.user_data_base = 8;
options.user_data = user_data;
const auto result = RecompileForTest(code, options, ReadHostTestMemory);
const auto result = RecompileForTest(code, options);
const auto expected = mode == 2u ? sampler : std::array<uint32_t, 4>{};
Check(!result.program.dispatcher_fallback && result.resources.samplers.size() == 1u &&
result.resources.samplers[0].dword_count == expected.size() &&
@@ -9615,8 +9616,7 @@ void TestBoundedMaterialBufferStores(bool scalar_key) {
ShaderRecompiler::IR::ResourceSnapshot snapshot;
ShaderRecompiler::IR::ResourceSpecialization specialization;
const ShaderRecompiler::IR::SrtRuntime runtime{
.user_data = user_data, .read_memory = ReadHostTestMemory,
.read_specialization_memory = ReadHostTestMemory};
.user_data = user_data, .read_specialization_memory = ReadHostTestMemory};
const auto materialize = [&] {
return ShaderRecompiler::IR::MaterializeResources(plan, runtime, snapshot, specialization);
};
@@ -9701,7 +9701,7 @@ void TestNativeDescriptorProvenanceKeepsGpuSelection() {
options.input_info.compute = &input_info;
options.user_data = user_data;
for (const auto &shader : {selected, partial, loop}) {
const auto result = RecompileForTest(shader, options, ReadHostTestMemory);
const auto result = RecompileForTest(shader, options);
Check(result.program.info.uses_dma && result.program.info.buffers.size() == 1u &&
result.program.info.buffers[0].written,
"GPU-selected scalar descriptor was materialized as a host binding");
@@ -10818,11 +10818,10 @@ void TestGraphicsComputeGetreg() {
ShaderMappedData mapped{};
mapped.code_size_bytes = sizeof(shader);
ShaderMapUserData(regs.cs_regs.data_addr, mapped);
HW::ShaderRegisters sh{};
ShaderComputeInputInfo graphics{}, async{};
async.async_compute = true;
(void)PrepareProgram(regs, sh, graphics);
(void)PrepareProgram(regs, sh, async);
(void)PrepareProgram(regs, graphics);
(void)PrepareProgram(regs, async);
Check(!graphics.async_compute && async.async_compute &&
MakeStageStaticKey(graphics) != MakeStageStaticKey(async) &&
MakeStageStaticKey(graphics).size() == MakeStageStaticKey(async).size(),
@@ -10864,7 +10863,7 @@ void TestMemoryFedScalarLeafCall() {
struct Memory {
std::array<uint32_t, 128> words{};
uint64_t end = base + 0x10c;
bool clean = true;
bool readable = true;
} memory;
std::copy_n(std::array{0xe0040060u, 0x0010000fu, 8u, 0x5204u}.begin(),
4, memory.words.begin());
@@ -10875,13 +10874,14 @@ void TestMemoryFedScalarLeafCall() {
memory.words[66] = EncodeSop1(0x21, 125, 14);
const auto read = [](void *data, uint64_t address, std::span<uint32_t> out) {
const auto& m = *static_cast<Memory*>(data);
if (!m.clean || address < base || address > m.end || out.size_bytes() > m.end - address)
if (!m.readable || address < base || address > m.end || out.size_bytes() > m.end - address)
return false;
std::memcpy(out.data(), reinterpret_cast<const uint8_t*>(m.words.data()) + address - base,
out.size_bytes());
return true;
};
IR::SrtRuntime runtime{.read_memory = read, .userdata = &memory, .read_specialization_memory = read};
FakeGuestMemory fake(read, &memory);
IR::SrtRuntime runtime{.userdata = &memory, .read_specialization_memory = read};
const auto options = MakeCompileOptions(ShaderType::Compute);
const uint32_t gpu_callable[] = {EncodeVop1(0x02, 4, 256),
EncodeSmem0(0x09, 14, 0), 4u << 25u, EncodeSop1(0x21, 14, 14), EncodeSopp(0x01)};
@@ -10965,14 +10965,14 @@ void TestMemoryFedScalarLeafCall() {
Check(source.revision == 3 && source.reads.back().second == 8,
"shorter exact-boundary leaf was not decoded after a failed old-span read");
#if KYTY_PLATFORM != KYTY_PLATFORM_WINDOWS
memory.clean = false;
memory.readable = false;
ExpectFatal([&] { (void)RefreshShaderSource(source, runtime); },
"unreadable call dependencies reused stale linked code");
memory.clean = true;
auto ordinary = runtime;
ordinary.read_specialization_memory = nullptr;
ExpectFatal([&] { (void)RefreshShaderSource(source, ordinary); },
"call target query accepted an ordinary non-strict memory reader");
memory.readable = true;
auto no_reader = runtime;
no_reader.read_specialization_memory = nullptr;
ExpectFatal([&] { (void)RefreshShaderSource(source, no_reader); },
"leaf code was linked without a specialization memory reader");
std::vector<uint32_t> escaping(std::begin(shader), std::end(shader) - 1);
escaping.insert(escaping.end(), {EncodeVop1(0x01, 1, 14),
EncodeMubuf0(0x1c, 0, false), EncodeMubuf1(1, 0, 0), EncodeSopp(0x01)});
@@ -10988,9 +10988,10 @@ void TestMemoryFedScalarLeafCall() {
EncodeSMovB32(15, 129), EncodeSop2(0x0f, 14, 128, 128),
EncodeSop1(0x21, 14, 14), EncodeSopp(0x01)};
auto pair_source = PrepareShaderSource(high_writer, options);
IR::SrtWalker pair_walker(pair_source.call_targets, runtime);
std::vector<uint32_t> flat;
uint32_t high = 1;
Check(pair_walker.Evaluate(pair_source.calls.front().target[1], high) && high == 0,
Check(IR::WalkSrt(pair_source.call_targets, runtime, flat, nullptr) &&
IR::WalkedValue(pair_source.call_targets, pair_source.calls.front().target[1], high) && high == 0,
"scalar pair producer used a stale preceding high-half definition");
const uint32_t joined[] = {
EncodeSopp(0x04, 2), EncodeSMovB32(14, 129), EncodeSopp(0x02, 1),
@@ -11136,7 +11137,7 @@ void TestMeshExportStorage() {
options.back_code = split ? std::span{back} : std::span<const uint32_t>{};
const auto result = RecompileForTest(
split ? std::span{front} : std::span<const uint32_t>{monolithic},
options, nullptr, nullptr, push_data_start);
options, push_data_start);
CheckSpirvBinaryValidates(result.spirv);
const auto &layout = result.program.bindings;
Check(layout.UsesPushData() == (push_data_start == PushData::MeshDrawDwordCount) &&
@@ -11284,13 +11285,11 @@ void TestMergedShaderUserDataSnapshot() {
const auto plan = IR::ExtractResourcePlan(program);
for (const auto *params : {&first, &second}) {
const auto user_data = std::span(params->user_data).first(params->user_data_count);
const IR::SrtRuntime runtime{.user_data = user_data,
.read_memory = ReadHostTestMemory};
IR::SrtWalker walker(plan, runtime);
const IR::SrtRuntime runtime{.user_data = user_data};
IR::DescriptorValue front_descriptor, back_descriptor;
const auto &table = params == &first ? first_table : second_table;
Check(walker.EvaluateDescriptor(program.info.buffers[0].source, front_descriptor) &&
walker.EvaluateDescriptor(program.info.buffers[1].source, back_descriptor) &&
Check(EvaluateSrtDescriptor(plan, runtime, program.info.buffers[0].source, front_descriptor) &&
EvaluateSrtDescriptor(plan, runtime, program.info.buffers[1].source, back_descriptor) &&
std::equal(user_data.begin() + 8, user_data.end(),
front_descriptor.dwords.begin()) &&
std::equal(table.begin(), table.end(), back_descriptor.dwords.begin()),
@@ -13824,7 +13823,7 @@ void TestAutomaticObjectPrimitiveId() {
PushData::GraphicsDrawDwords(true, false) == PushData::MeshDrawDwordCount,
"graphics draw data reserved an incorrect prefix");
for (const uint32_t start : {prefix, PushData::DwordCount}) {
const auto result = RecompileForTest(shader, options, nullptr, nullptr, start);
const auto result = RecompileForTest(shader, options, start);
Check(result.program.bindings.UsesPushData() == (start == prefix),
"object ID changed shader-data push/spill selection");
Check(std::ranges::any_of(result.program.info.outputs, [](const StageOutput& output) {
@@ -13841,7 +13840,7 @@ void TestAutomaticObjectPrimitiveId() {
}
#if KYTY_PLATFORM != KYTY_PLATFORM_WINDOWS
pixel.target_shader_mask = 0xf0000000u;
ExpectFatal([&] { (void)RecompileForTest(shader, options, nullptr, nullptr, prefix); },
ExpectFatal([&] { (void)RecompileForTest(shader, options, prefix); },
"automatic RT7 silently overlapped an explicit MRT7 export");
#endif
}
@@ -14489,33 +14488,6 @@ void CheckFlattenedReadSlots(const ShaderRecompiler::IR::Program &program,
}
}
bool ReadSrtHostDword(void *, uint64_t address, std::span<uint32_t> values) {
if (address == 0 || values.empty()) {
return false;
}
std::memcpy(values.data(), reinterpret_cast<const void *>(address), values.size_bytes());
return true;
}
struct SrtHostRange {
const uint32_t *data;
size_t count;
};
bool ReadSrtHostRangeDword(void *userdata, uint64_t address, std::span<uint32_t> values) {
const auto *range = static_cast<const SrtHostRange *>(userdata);
if (range == nullptr || range->data == nullptr || range->count == 0 || values.empty()) {
return false;
}
const auto base = reinterpret_cast<uint64_t>(range->data);
const auto size = range->count * sizeof(uint32_t);
if (address < base || values.size_bytes() > size || address - base > size - values.size_bytes()) {
return false;
}
std::memcpy(values.data(), reinterpret_cast<const void *>(address), values.size_bytes());
return true;
}
void TestTypedDescriptorRealWideMoveTranslation() {
const uint32_t shader[] = {
EncodeSop1(0x04, 0, 20), // s_mov_b64 s[0:1], s[20:21]
@@ -14581,7 +14553,7 @@ void TestComputeImageFill() {
options.input_info.compute = &compute;
options.user_data = userdata;
enum class Mutation { None, Offset, WrongAxis, VaryingValue, Predicate, ExtraStore, A16 };
const auto Run = [&](Mutation mutation, bool clean = true) {
const auto Run = [&](Mutation mutation) {
auto translated = ShaderRecompiler::TranslateProgram(shader, options);
auto &program = translated.program;
for (auto *block : program.blocks) {
@@ -14618,13 +14590,11 @@ void TestComputeImageFill() {
auto plan = ExtractResourcePlan(program);
ResourceSnapshot snapshot;
ResourceSpecialization specialization;
Check(MaterializeResources(plan,
{.user_data = userdata, .read_memory = ReadHostTestMemory,
.read_specialization_memory = clean ? ReadHostTestMemory : nullptr},
snapshot, specialization),
// The fill value is a walker load, so it needs no specialization memory reader.
Check(MaterializeResources(plan, {.user_data = userdata}, snapshot, specialization),
"captured stencil clear did not materialize");
const auto &fill = snapshot.uniform_fill;
const bool expected = mutation == Mutation::None && clean;
const bool expected = mutation == Mutation::None;
Check((fill.kind == UniformFillKind::Image) == expected,
"image fill proof accepted an unsafe store or missed the captured clear");
if (expected) {
@@ -14634,7 +14604,6 @@ void TestComputeImageFill() {
}
};
Run(Mutation::None);
Run(Mutation::None, false);
for (const auto mutation : {Mutation::Offset, Mutation::WrongAxis, Mutation::VaryingValue,
Mutation::Predicate, Mutation::ExtraStore, Mutation::A16})
Run(mutation);
@@ -14835,27 +14804,22 @@ void TestGpuProducedWritableDescriptor() {
"native GPU-built writable descriptor acquired an unnecessary alias proof");
struct Reads {
std::array<uint32_t, 3> header{0x26c0u, 0u, 151u};
uint32_t ordinary = 0;
uint32_t strict = 0;
uint32_t count = 0;
} reads;
const SrtRuntime runtime{
.user_data = user_data,
.read_memory = +[](void *data, uint64_t address, std::span<uint32_t> words) {
if (words.size() != 1) return false;
FakeGuestMemory fake(+[](void *data, uint64_t address, std::span<uint32_t> words) {
auto &reads = *static_cast<Reads *>(data);
const uint32_t index = address == 0x300315028ull ? 0u
: address == 0x30031502cull ? 1u
: address == 0x300315074ull ? 2u : 3u;
if (index == 3u) return false;
++reads.ordinary;
uint32_t index = 0;
switch (address) {
case 0x300315028ull: index = 0u; break;
case 0x30031502cull: index = 1u; break;
case 0x300315074ull: index = 2u; break;
default: return false;
}
++reads.count;
words[0] = reads.header[index];
return true;
},
.userdata = &reads,
.read_specialization_memory = +[](void *data, uint64_t, std::span<uint32_t>) {
++static_cast<Reads *>(data)->strict;
return false; // Current GPU bytes are available only through the ordinary reader.
}};
}, &reads);
const SrtRuntime runtime{.user_data = user_data};
ResourceSnapshot snapshot;
ResourceSpecialization specialization;
const auto check = [&](uint32_t low, uint32_t high, uint32_t count, uint32_t reads_expected) {
@@ -14866,8 +14830,7 @@ void TestGpuProducedWritableDescriptor() {
snapshot.buffers[0].dwords[1] == high &&
snapshot.buffers[0].dwords[2] == count &&
snapshot.buffers[0].dwords[3] == 0x16204u &&
snapshot.specialization_reads.empty() && reads.strict == 0 &&
reads.ordinary == reads_expected,
snapshot.specialization_reads.empty() && reads.count == reads_expected,
"GPU-produced descriptor lost current scalar data or repeated its reads");
};
check(0x003176c0u, 0x00800003u, 151u, 3u);
@@ -14929,25 +14892,29 @@ void TestUniformSelectedWritableDescriptor() {
auto plan = ExtractResourcePlan(translated.program);
Check(plan.info.buffers.size() == 1 && plan.info.buffers[0].written,
"uniform address selection lost its writable buffer");
const auto read = +[](void *data, uint64_t address, std::span<uint32_t> words) {
const bool alternate = *static_cast<uint32_t *>(data) == 0;
if (words.size() != 1) return false;
// An unavailable read would complete as zero, so the reader counts them.
struct Memory { const uint32_t *flag; uint32_t unavailable = 0; } memory{&user_data[2]};
FakeGuestMemory fake(+[](void *data, uint64_t address, std::span<uint32_t> words) {
auto &memory = *static_cast<Memory *>(data);
const bool alternate = *memory.flag == 0;
bool available = false;
switch (address) {
case 0x1040: words[0] = 8u; return alternate;
case 0x1044: words[0] = 1u; return alternate;
case 0x1080: words[0] = 3u; return !alternate;
case 0x109c: words[0] = 2u; return !alternate;
case 0x1084: words[0] = 10u; return true;
default: return false;
case 0x1040: words[0] = 8u; available = alternate; break;
case 0x1044: words[0] = 1u; available = alternate; break;
case 0x1080: words[0] = 3u; available = !alternate; break;
case 0x109c: words[0] = 2u; available = !alternate; break;
case 0x1084: words[0] = 10u; available = true; break;
default: break;
}
};
const SrtRuntime runtime{.user_data = user_data, .read_memory = read,
.userdata = &user_data[2], .read_specialization_memory = read};
memory.unavailable += !available;
return available;
}, &memory);
const SrtRuntime runtime{.user_data = user_data};
ResourceSnapshot snapshot;
ResourceSpecialization specialization;
for (const uint32_t flag : {1u, 0u, 1u}) {
user_data[2] = flag;
Check(MaterializeResources(plan, runtime, snapshot, specialization),
Check(MaterializeResources(plan, runtime, snapshot, specialization) && memory.unavailable == 0,
"uniform selection evaluated an unavailable untaken scalar source");
const auto &descriptor = snapshot.buffers[0].dwords;
Check(descriptor[0] == (flag ? 0xe0u : 4u) &&
@@ -14977,18 +14944,16 @@ void TestUniformSelectedDescriptorLoadAddress() {
Check(plan.srt_reads.size() == 4u && plan.info.buffers.size() == 1u &&
plan.info.buffers[0].written && !plan.capture_specialization_reads,
"conditional scalar address duplicated or detached descriptor reads");
const auto read = +[](void *data, uint64_t address, std::span<uint32_t> words) {
FakeGuestMemory fake(+[](void *data, uint64_t address, std::span<uint32_t> words) {
const auto flag = *static_cast<uint32_t *>(data);
const uint64_t base = flag ? 0x2000u : 0x1000u;
const std::array<uint32_t, 4> descriptor{0x3000u + flag * 0x1000u,
4u << 16u, 16u, 0x16204u};
if (words.size() != 1u || address < base || address >= base + 16u ||
(address & 3u) != 0u) return false;
if (address < base || address >= base + 16u || (address & 3u) != 0u) return false;
words[0] = descriptor[(address - base) / 4u];
return true;
};
const SrtRuntime runtime{.user_data = user_data, .read_memory = read,
.userdata = &user_data[0]};
}, &user_data[0]);
const SrtRuntime runtime{.user_data = user_data};
ResourceSnapshot snapshot;
ResourceSpecialization specialization;
for (const uint32_t flag : {0u, 1u, 0u}) {
@@ -15058,7 +15023,7 @@ void TestBoundedScalarMaterialImageKeys() {
uint32_t stride = 160;
uint32_t records = 409;
uint64_t unavailable = 0;
bool clean = true;
bool readable = true;
bool mixed = false;
bool same_key = false;
uint32_t key_reads = 0;
@@ -15066,7 +15031,7 @@ void TestBoundedScalarMaterialImageKeys() {
} memory;
const auto read = +[](void *data, uint64_t address, std::span<uint32_t> words) {
auto &m = *static_cast<Memory *>(data);
if (!m.clean || (m.unavailable >= address &&
if (!m.readable || (m.unavailable >= address &&
m.unavailable - address < words.size_bytes())) return false;
if (words.size() == 1 && address >= 0x10a0u && address < 0x10c0u) {
const uint32_t descriptors[] = {0x2000u, m.stride << 16u, m.records, 0x5204u,
@@ -15101,6 +15066,7 @@ void TestBoundedScalarMaterialImageKeys() {
}
return false;
};
FakeGuestMemory fake(read, &memory);
const SrtRuntime runtime{.user_data = user_data, .userdata = &memory,
.read_specialization_memory = read};
ResourceSnapshot snapshot;
@@ -15138,9 +15104,11 @@ void TestBoundedScalarMaterialImageKeys() {
Check(MaterializeResources(plan, runtime, snapshot, specialization) &&
memory.key_reads == 0 && memory.images == 0 && snapshot.images.size() == 2,
"empty scalar material loop retained image probes");
memory = {.clean = false};
Check(!MaterializeResources(plan, runtime, snapshot, specialization),
"dirty scalar material proof was accepted");
// Unreadable descriptor and count reads are zero, which leaves the loop empty.
memory = {.readable = false};
Check(MaterializeResources(plan, runtime, snapshot, specialization) &&
memory.key_reads == 0 && memory.images == 0 && snapshot.images.size() == 2,
"unreadable scalar material count did not read as zero");
for (const uint64_t unavailable : {0x2094u, 0x211cu}) {
memory = {.unavailable = unavailable};
Check(!MaterializeResources(plan, runtime, snapshot, specialization),
@@ -15218,12 +15186,14 @@ void TestTypedDescriptorRealCarryAndScalarLoads() {
std::array<uint32_t, 64> carry_user_data{};
const uint64_t shader_base = 0x00000012fffffffbull;
const uint64_t expected_pc = shader_base + 5u;
ShaderRecompiler::IR::SrtRuntime carry_runtime{carry_user_data, shader_base,
nullptr, nullptr};
ShaderRecompiler::IR::DescriptorValue carry_value;
Check(ShaderRecompiler::IR::SrtWalker(ShaderRecompiler::IR::ExtractResourcePlan(carry_ir), carry_runtime).EvaluateDescriptor(carry_source_index, carry_value) &&
carry_value.dwords[0] == static_cast<uint32_t>(expected_pc) &&
carry_value.dwords[1] == static_cast<uint32_t>(expected_pc >> 32u),
const ShaderRecompiler::IR::SrtRuntime carry_runtime{carry_user_data, shader_base};
auto carry_plan = ShaderRecompiler::IR::ExtractResourcePlan(carry_ir);
const auto &carry_words = carry_plan.descriptor_sources[carry_source_index].dwords;
uint32_t carry_low = 0, carry_high = 0;
Check(EvaluateSrtValue(carry_plan, carry_runtime, carry_words[0], carry_low) &&
EvaluateSrtValue(carry_plan, carry_runtime, carry_words[1], carry_high) &&
carry_low == static_cast<uint32_t>(expected_pc) &&
carry_high == static_cast<uint32_t>(expected_pc >> 32u),
"S_GETPC shader-base or add/addc carry evaluation was incorrect");
const uint32_t load_shader[] = {
@@ -15285,19 +15255,21 @@ void TestTypedDescriptorRealCarryAndScalarLoads() {
BuildTypedPlan(inline_sampler_shader,
static_cast<uint32_t>(std::size(inline_sampler_shader)),
inline_sampler_ir);
// The walk also reads the sampled image's user-data descriptor.
auto user_data = ImageTestUserData();
ShaderRecompiler::IR::DescriptorValue sampler;
ShaderRecompiler::IR::SrtRuntime runtime;
const ShaderRecompiler::IR::SrtRuntime runtime{user_data};
Check(inline_sampler_ir.info.samplers.size() == 1 &&
TypedDescriptorSource(inline_sampler_ir,
inline_sampler_ir.info.samplers[0].source) !=
nullptr &&
ShaderRecompiler::IR::SrtWalker(ShaderRecompiler::IR::ExtractResourcePlan(inline_sampler_ir), runtime).EvaluateDescriptor(inline_sampler_ir.info.samplers[0].source, sampler) &&
EvaluateSrtDescriptor(ShaderRecompiler::IR::ExtractResourcePlan(inline_sampler_ir), runtime,
inline_sampler_ir.info.samplers[0].source, sampler) &&
sampler.dwords[0] == 0 && sampler.dwords[1] == 0x00fff000u &&
sampler.dwords[2] == 0x09500000u && sampler.dwords[3] == 0,
"real inline sampler construction was unresolved or evaluated "
"incorrectly");
auto user_data = ImageTestUserData();
auto options = MakeCompileOptions(ShaderType::Pixel);
options.user_data = user_data;
auto result = RecompileForTest(inline_sampler_shader, options);
@@ -15325,9 +15297,8 @@ void TestSrtWalkerRealSmemTranslation() {
user_data[8] = static_cast<uint32_t>(address);
user_data[9] = static_cast<uint32_t>(address >> 32u);
std::vector<uint32_t> flat;
const ShaderRecompiler::IR::SrtRuntime runtime{user_data, 0, ReadSrtHostDword,
nullptr};
const auto walked = ShaderRecompiler::IR::SrtWalker(ShaderRecompiler::IR::ExtractResourcePlan(ir), runtime).RefreshFlatBuffer(flat);
const ShaderRecompiler::IR::SrtRuntime runtime{user_data};
const auto walked = ShaderRecompiler::IR::WalkSrt(ShaderRecompiler::IR::ExtractResourcePlan(ir), runtime, flat, nullptr);
Check(walked, "SRT walk failed");
Check(flat.size() == table.size() &&
std::equal(flat.begin(), flat.end(), table.begin()),
@@ -15353,10 +15324,9 @@ void TestSrtWalkerVccBaseTranslation() {
const auto address = reinterpret_cast<uint64_t>(table.data());
user_data[27] = static_cast<uint32_t>(address);
user_data[28] = static_cast<uint32_t>(address >> 32u);
const ShaderRecompiler::IR::SrtRuntime runtime{user_data, 0, ReadSrtHostDword,
nullptr};
const ShaderRecompiler::IR::SrtRuntime runtime{user_data};
std::vector<uint32_t> flat;
Check(ShaderRecompiler::IR::SrtWalker(ShaderRecompiler::IR::ExtractResourcePlan(ir), runtime).RefreshFlatBuffer(flat), "SRT walk failed");
Check(ShaderRecompiler::IR::WalkSrt(ShaderRecompiler::IR::ExtractResourcePlan(ir), runtime, flat, nullptr), "SRT walk failed");
Check(flat.size() == table.size() &&
std::equal(flat.begin(), flat.end(), table.begin()),
"typed SSA lost an SMEM base copied through VCC");
@@ -15382,17 +15352,16 @@ void TestSrtWalkerRealSBufferTranslation() {
user_data[9] = static_cast<uint32_t>(address >> 32u);
user_data[10] = sizeof(table);
std::vector<uint32_t> flat;
const ShaderRecompiler::IR::SrtRuntime runtime{user_data, 0, ReadSrtHostDword,
nullptr};
const ShaderRecompiler::IR::SrtRuntime runtime{user_data};
const auto plan = ShaderRecompiler::IR::ExtractResourcePlan(ir);
const auto walked = ShaderRecompiler::IR::SrtWalker(plan, runtime).RefreshFlatBuffer(flat);
const auto walked = ShaderRecompiler::IR::WalkSrt(plan, runtime, flat, nullptr);
Check(walked, "SRT walk failed");
Check(flat.size() == 4 &&
std::equal(flat.begin(), flat.end(), table.begin()),
"real S_BUFFER_LOAD walk did not align offset components independently");
user_data[10] = 3 * sizeof(uint32_t);
const auto bounds_walked = ShaderRecompiler::IR::SrtWalker(plan, runtime).RefreshFlatBuffer(flat);
const auto bounds_walked = ShaderRecompiler::IR::WalkSrt(plan, runtime, flat, nullptr);
Check(bounds_walked && flat == std::vector<uint32_t>{table[0], table[1], table[2], 0u},
"real S_BUFFER_LOAD walk did not zero only the out-of-bounds DWORD");
CheckFlattenedReadSlots(
@@ -15408,7 +15377,7 @@ void TestSrtWalkerRealSBufferTranslation() {
static_cast<uint32_t>(std::size(negative_shader)),
negative_ir);
user_data[10] = sizeof(table);
Check(!ShaderRecompiler::IR::SrtWalker(ShaderRecompiler::IR::ExtractResourcePlan(negative_ir), runtime).RefreshFlatBuffer(flat),
Check(!ShaderRecompiler::IR::WalkSrt(ShaderRecompiler::IR::ExtractResourcePlan(negative_ir), runtime, flat, nullptr),
"real S_BUFFER_LOAD walk accepted a negative immediate");
}
@@ -15443,11 +15412,9 @@ void TestScalarMemorySourcesCapturedBeforeWrites() {
if (opcode == 0x0a) {
user_data[base + 2u] = sizeof(table);
}
SrtHostRange range{table.data(), table.size()};
const ShaderRecompiler::IR::SrtRuntime runtime{
user_data, 0, ReadSrtHostRangeDword, &range};
const ShaderRecompiler::IR::SrtRuntime runtime{user_data};
std::vector<uint32_t> flat;
Check(ShaderRecompiler::IR::SrtWalker(ShaderRecompiler::IR::ExtractResourcePlan(ir), runtime).RefreshFlatBuffer(flat), "SRT walk failed");
Check(ShaderRecompiler::IR::WalkSrt(ShaderRecompiler::IR::ExtractResourcePlan(ir), runtime, flat, nullptr), "SRT walk failed");
Check(flat.size() == table.size() &&
std::equal(flat.begin(), flat.end(), table.begin()),
"overlapping scalar-memory load did not capture its sources before "
@@ -15822,13 +15789,12 @@ void TestComputeLdsAllocationIdentity() {
ShaderMappedData mapped{};
mapped.code_size_bytes = sizeof(shader);
ShaderMapUserData(regs.cs_regs.data_addr, mapped);
HW::ShaderRegisters sh{};
const auto compile = [&](uint16_t encoded_lds_size,
uint32_t expected_dwords) {
regs.cs_regs.lds_size = encoded_lds_size;
ShaderComputeInputInfo input_info{};
const auto params = PrepareProgram(regs, sh, input_info);
const auto params = PrepareProgram(regs, input_info);
Check(input_info.lds_size_dwords == expected_dwords,
"COMPUTE_PGM_RSRC2 LDS allocation units were not decoded");
auto options = MakeCompileOptions(ShaderType::Compute);
@@ -15894,7 +15860,7 @@ void TestComputeLdsAllocationIdentity() {
ShaderMapUserData(regs.cs_regs.data_addr, mapped);
regs.cs_regs.lds_size = decode_lds_field(lds_896_rsrc2);
ShaderComputeInputInfo scratch_info{};
const auto scratch_params = PrepareProgram(regs, sh, scratch_info);
const auto scratch_params = PrepareProgram(regs, scratch_info);
Check(scratch_info.scratch_size_dwords == 7,
"AGC per-thread scratch size was not propagated");
auto scratch_options = MakeCompileOptions(ShaderType::Compute);
@@ -15960,8 +15926,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};
};
@@ -16007,7 +15973,7 @@ void TestGraphicsPushConstantPlacement() {
constexpr uint32_t PushDataStart = 7;
auto placed =
RecompileForTest(shader, options, nullptr, nullptr, PushDataStart);
RecompileForTest(shader, options, PushDataStart);
Check(placed.program.bindings.UsesPushData() &&
placed.program.bindings.push_data_start_dword == PushDataStart &&
placed.program.bindings.descriptor_counts[static_cast<size_t>(BindingKind::ShaderData)] == 0,
@@ -16016,7 +15982,7 @@ void TestGraphicsPushConstantPlacement() {
"SPIR-V push-data block did not use the canonical zero base");
CheckSpirvBinaryValidates(placed.spirv);
auto exact = RecompileForTest(shader, options, nullptr, nullptr, 28);
auto exact = RecompileForTest(shader, options, 28);
Check(exact.program.bindings.UsesPushData() &&
exact.program.bindings.push_data_start_dword == 28 &&
exact.program.bindings.ShaderDataDwords() == 4,
@@ -16026,8 +15992,8 @@ void TestGraphicsPushConstantPlacement() {
Check(cursor == 32, "exact-fit shader data advanced the cursor incorrectly");
CheckSpirvBinaryValidates(exact.spirv);
auto spill = RecompileForTest(shader, options, nullptr, nullptr, 29);
auto full = RecompileForTest(shader, options, nullptr, nullptr, 32);
auto spill = RecompileForTest(shader, options, 29);
auto full = RecompileForTest(shader, options, 32);
Check(!spill.program.bindings.UsesPushData() &&
spill.program.bindings.descriptor_counts[static_cast<size_t>(BindingKind::ShaderData)] == 1 &&
spill.program.bindings == full.program.bindings && spill.spirv == full.spirv,