mirror of
https://github.com/KytyPS5/KytyPS5.git
synced 2026-10-28 00:12:40 +00:00
Compare commits
28
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
83f8bfd3c9 | ||
|
|
dd5bc26c64 | ||
|
|
72903bfce6 | ||
|
|
59d8256b36 | ||
|
|
9a20c6f738 | ||
|
|
67c89e0915 | ||
|
|
5d8381e0f3 | ||
|
|
e864cb20bd | ||
|
|
40aaab482e | ||
|
|
ec8b75b244 | ||
|
|
9210264d47 | ||
|
|
66e21a6245 | ||
|
|
87e49282b8 | ||
|
|
1816004357 | ||
|
|
bfc377627a | ||
|
|
4b651f2ba7 | ||
|
|
8c01d8e849 | ||
|
|
e0c5ae090c | ||
|
|
906d25568f | ||
|
|
8291cfeb04 | ||
|
|
dbc7b8b2ff | ||
|
|
269e411ef2 | ||
|
|
170c107779 | ||
|
|
8dc0d56b7e | ||
|
|
d7856753e0 | ||
|
|
bdf48dcf3b | ||
|
|
f356fa3730 | ||
|
|
07f6420273 |
No files matched your search
+6
-1
@@ -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"
|
||||
|
||||
@@ -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
|
||||
@@ -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_ */
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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);
|
||||
|
||||
@@ -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
|
||||
@@ -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
@@ -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
@@ -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
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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_ */
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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_ */
|
||||
@@ -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()};
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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,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
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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 = ⌖
|
||||
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
@@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
@@ -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(),
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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 ®isters = 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_
|
||||
@@ -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
@@ -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 ¶ms,
|
||||
|
||||
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,
|
||||
|
||||
Reference in new issue
Block a user