mirror of
https://github.com/KytyPS5/KytyPS5.git
synced 2026-10-27 07:52:39 +00:00
Compare commits
9
Commits
6aaa533296
...
8291cfeb04
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8291cfeb04 | ||
|
|
dbc7b8b2ff | ||
|
|
269e411ef2 | ||
|
|
170c107779 | ||
|
|
8dc0d56b7e | ||
|
|
d7856753e0 | ||
|
|
bdf48dcf3b | ||
|
|
f356fa3730 | ||
|
|
07f6420273 |
No files matched your search
@@ -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"
|
||||
|
||||
+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);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -944,11 +936,10 @@ void RenderExecutor::CommitBindings(CommandBuffer& buffer,
|
||||
}
|
||||
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");
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -9,10 +9,9 @@
|
||||
#include "common/threads.h"
|
||||
#include "graphics/guest_gpu/gpu_format.h"
|
||||
#include "graphics/guest_gpu/hardwareContext.h"
|
||||
#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/imageView.h"
|
||||
#include "graphics/host_gpu/renderer/image/textureCommon.h"
|
||||
#include "graphics/host_gpu/renderer/pipeline/blendMapping.h"
|
||||
#include "graphics/host_gpu/renderer/render.h"
|
||||
#include "graphics/host_gpu/renderer/renderContext.h"
|
||||
@@ -332,33 +331,47 @@ struct PipelineCache::ProgramCache {
|
||||
template <typename InputInfo>
|
||||
ShaderProgram Get(const ShaderParams& params, InputInfo& input_info,
|
||||
uint32_t& push_data_cursor) {
|
||||
ShaderType stage;
|
||||
if constexpr (std::is_same_v<InputInfo, ShaderVertexInputInfo>) {
|
||||
stage = input_info.logical_stage;
|
||||
} else if constexpr (std::is_same_v<InputInfo, ShaderPixelInputInfo>) {
|
||||
stage = ShaderType::Pixel;
|
||||
} else {
|
||||
static_assert(std::is_same_v<InputInfo, ShaderComputeInputInfo>);
|
||||
stage = ShaderType::Compute;
|
||||
}
|
||||
|
||||
const auto user_data = std::span(params.user_data).first(params.user_data_count);
|
||||
lookup_key.stage = stage;
|
||||
lookup_key.hash = params.hash;
|
||||
lookup_key.user_data_count = params.user_data_count;
|
||||
lookup_key.code_size = static_cast<uint32_t>(params.code.size());
|
||||
BuildStageStaticKey(input_info, lookup_key.static_state);
|
||||
auto entry = programs.find(lookup_key);
|
||||
ShaderRecompiler::IR::SrtRuntime runtime {
|
||||
ShaderRecompiler::IR::SrtRuntime runtime {
|
||||
.user_data = user_data,
|
||||
.shader_base = params.Base(),
|
||||
.read_specialization_memory = ReadShaderGuestMemory,
|
||||
.image_heap = image_heap,
|
||||
};
|
||||
if constexpr (std::is_same_v<InputInfo, ShaderComputeInputInfo>) {
|
||||
runtime.workgroup_counts = input_info.workgroup_counts;
|
||||
ShaderRecompiler::CompileOptions options {.wave_size = input_info.wave_size};
|
||||
if constexpr (std::is_same_v<InputInfo, ShaderVertexInputInfo>) {
|
||||
options.stage = input_info.logical_stage;
|
||||
options.input_info.vertex = &input_info;
|
||||
options.user_data_base = 8;
|
||||
if (options.stage == ShaderType::Mesh ||
|
||||
options.stage == ShaderType::TessellationControl) {
|
||||
options.user_data_base = 0;
|
||||
options.wave_size =
|
||||
options.stage == ShaderType::Mesh ? input_info.mesh.wave_size : 64u;
|
||||
}
|
||||
} else if constexpr (std::is_same_v<InputInfo, ShaderPixelInputInfo>) {
|
||||
options.stage = ShaderType::Pixel;
|
||||
options.input_info.pixel = &input_info;
|
||||
} else {
|
||||
static_assert(std::is_same_v<InputInfo, ShaderComputeInputInfo>);
|
||||
options.stage = ShaderType::Compute;
|
||||
options.input_info.compute = &input_info;
|
||||
runtime.workgroup_counts = input_info.workgroup_counts;
|
||||
runtime.indirect_args_address = input_info.indirect_args_address;
|
||||
}
|
||||
lookup_key.stage = options.stage;
|
||||
lookup_key.hash = params.hash;
|
||||
lookup_key.user_data_count = params.user_data_count;
|
||||
lookup_key.code_size = static_cast<uint32_t>(params.code.size());
|
||||
BuildStageStaticKey(input_info, lookup_key.static_state);
|
||||
auto entry = programs.find(lookup_key);
|
||||
const auto bind = [&](const Permutation& permutation) {
|
||||
input_info.stage = {.program = &permutation.program,
|
||||
.resources = &entry->second.resources,
|
||||
.handle = &permutation.handle};
|
||||
permutation.program.bindings.AdvancePushData(push_data_cursor);
|
||||
return permutation.handle;
|
||||
};
|
||||
bool source_changed = false;
|
||||
if (entry != programs.end()) {
|
||||
auto& cached = entry->second;
|
||||
@@ -370,24 +383,12 @@ struct PipelineCache::ProgramCache {
|
||||
} else {
|
||||
permutation = cached.FindPermutation<false>(runtime, push_data_cursor);
|
||||
}
|
||||
if (permutation != nullptr) {
|
||||
input_info.stage = {.program = &permutation->program, .resources = &entry->second.resources};
|
||||
permutation->program.bindings.AdvancePushData(push_data_cursor);
|
||||
return permutation->handle;
|
||||
}
|
||||
if (permutation != nullptr) return bind(*permutation);
|
||||
}
|
||||
|
||||
ShaderStageInputInfo stage_input {};
|
||||
if constexpr (std::is_same_v<InputInfo, ShaderVertexInputInfo>) {
|
||||
stage_input.vertex = &input_info;
|
||||
} else if constexpr (std::is_same_v<InputInfo, ShaderPixelInputInfo>) {
|
||||
stage_input.pixel = &input_info;
|
||||
} else {
|
||||
stage_input.compute = &input_info;
|
||||
}
|
||||
const char* label = nullptr;
|
||||
const char* stage_name = nullptr;
|
||||
switch (stage) {
|
||||
switch (options.stage) {
|
||||
case ShaderType::Vertex: label = "ShaderRecompiler VS"; stage_name = "vs"; break;
|
||||
case ShaderType::Mesh: label = "ShaderRecompiler MS"; stage_name = "ms"; break;
|
||||
case ShaderType::Local: label = "ShaderRecompiler LS"; stage_name = "ls"; break;
|
||||
@@ -397,8 +398,6 @@ struct PipelineCache::ProgramCache {
|
||||
case ShaderType::Compute: label = "ShaderRecompiler CS"; stage_name = "cs"; break;
|
||||
default: EXIT("invalid pipeline shader stage\n");
|
||||
}
|
||||
ShaderRecompiler::CompileOptions options;
|
||||
options.stage = stage;
|
||||
options.profile = &profile;
|
||||
options.shader_hash = params.hash;
|
||||
options.user_data = user_data;
|
||||
@@ -407,18 +406,6 @@ struct PipelineCache::ProgramCache {
|
||||
options.early_dump = options.dump_ir;
|
||||
options.emit_debug_info = Config::GraphicsDebugDumpEnabled();
|
||||
options.dump_label = label;
|
||||
options.input_info = stage_input;
|
||||
|
||||
if constexpr (std::is_same_v<InputInfo, ShaderVertexInputInfo>) {
|
||||
options.user_data_base = 8;
|
||||
options.wave_size = input_info.wave_size;
|
||||
if (stage == ShaderType::Mesh || stage == ShaderType::TessellationControl) {
|
||||
options.user_data_base = 0;
|
||||
options.wave_size = stage == ShaderType::Mesh ? input_info.mesh.wave_size : 64u;
|
||||
}
|
||||
} else {
|
||||
options.wave_size = input_info.wave_size;
|
||||
}
|
||||
DumpShaderOriginal(stage_name, options.shader_hash, params.code);
|
||||
ShaderRecompiler::ShaderSource source;
|
||||
if (entry == programs.end()) source = ShaderRecompiler::PrepareShaderSource(params.code, options);
|
||||
@@ -453,17 +440,12 @@ struct PipelineCache::ProgramCache {
|
||||
} else if (source_changed) {
|
||||
entry->second.resource_plan = ShaderRecompiler::IR::ExtractResourcePlan(translated.program);
|
||||
if (const auto* permutation = entry->second.FindPermutation<true>(runtime, push_data_cursor)) {
|
||||
input_info.stage = {.program = &permutation->program, .resources = &entry->second.resources};
|
||||
permutation->program.bindings.AdvancePushData(push_data_cursor);
|
||||
return permutation->handle;
|
||||
return bind(*permutation);
|
||||
}
|
||||
}
|
||||
entry->second.permutations.push_back(CompilePermutation(
|
||||
stage_name, options, std::move(translated), entry->second.specialization, push_data_cursor,
|
||||
entry->second.FunctionCode()));
|
||||
const auto& permutation = entry->second.permutations.back();
|
||||
input_info.stage = {.program = &permutation.program, .resources = &entry->second.resources};
|
||||
permutation.program.bindings.AdvancePushData(push_data_cursor);
|
||||
|
||||
std::array<size_t, static_cast<size_t>(ShaderType::TessellationEvaluation) + 1> counts {};
|
||||
for (const auto& [key, source]: programs) {
|
||||
@@ -478,7 +460,7 @@ struct PipelineCache::ProgramCache {
|
||||
counts[static_cast<size_t>(ShaderType::Local)],
|
||||
counts[static_cast<size_t>(ShaderType::TessellationControl)],
|
||||
counts[static_cast<size_t>(ShaderType::TessellationEvaluation)]);
|
||||
return permutation.handle;
|
||||
return bind(entry->second.permutations.back());
|
||||
}
|
||||
|
||||
ProgramCache(GraphicContext& graphics, ShaderRecompiler::IR::ImageHeapSource* image_heap)
|
||||
@@ -521,9 +503,9 @@ PipelineCache::~PipelineCache() {
|
||||
auto destroy = [this](const auto& pipelines) {
|
||||
for (const auto& [key, pipeline]: pipelines) {
|
||||
(void)key;
|
||||
m_graphics.device.destroyPipeline(pipeline->pipeline, nullptr);
|
||||
m_graphics.device.destroyPipelineLayout(pipeline->pipeline_layout, nullptr);
|
||||
m_graphics.device.destroyDescriptorSetLayout(pipeline->descriptor_set_layout, nullptr);
|
||||
m_graphics.device.destroyPipeline(pipeline.pipeline, nullptr);
|
||||
m_graphics.device.destroyPipelineLayout(pipeline.pipeline_layout, nullptr);
|
||||
m_graphics.device.destroyDescriptorSetLayout(pipeline.descriptor_set_layout, nullptr);
|
||||
}
|
||||
};
|
||||
destroy(m_graphics_pipelines);
|
||||
@@ -681,11 +663,34 @@ void PipelineCache::Save() {
|
||||
m_driver_cache_dirty = false;
|
||||
}
|
||||
|
||||
PipelineCache::GraphicsPrograms PipelineCache::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) {
|
||||
void PipelineCache::GetGraphicsPrograms(const CommandBuffer& command, DrawRenderState& state) {
|
||||
const auto& context = command.GetRegisters();
|
||||
const auto& user_config = command.GetUserConfig();
|
||||
const auto& sh = context.GetShaderRegisters();
|
||||
const auto& vertex_regs = command.GetShaders().GetVs();
|
||||
const auto& pixel_regs = command.GetShaders().GetPs();
|
||||
auto& vertex_info = state.vertex_info;
|
||||
auto& pixel_info = state.ps_input_info;
|
||||
const bool object_primitive_id = context.HasObjectPrimitiveIdOutput();
|
||||
const auto write_mask = context.GetRenderTargetMask() & sh.m_cbShaderMask;
|
||||
uint32_t color_output_mask = object_primitive_id ? 1u << 7u : 0;
|
||||
for (uint32_t index = 0; index < RENDER_COLOR_ATTACHMENTS_MAX; index++) {
|
||||
const auto slot = ShaderPixelExportTarget(sh.m_cbShaderMask, index);
|
||||
if (slot >= RENDER_COLOR_ATTACHMENTS_MAX) {
|
||||
break;
|
||||
}
|
||||
if (sh.target_output_mode[index] != 0 && render_target_mask_slot(write_mask, slot) != 0) {
|
||||
color_output_mask |= 1u << slot;
|
||||
}
|
||||
}
|
||||
const auto& db = sh.db_shader_control;
|
||||
// Without color outputs, a pixel shader still runs for its depth or coverage side effects.
|
||||
const bool pixel_active =
|
||||
pixel_regs.ps_regs.data_addr != 0 &&
|
||||
(color_output_mask != 0 || 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.AlphaToMask());
|
||||
state.ps_active = pixel_active;
|
||||
const bool tess_active = user_config.GetPrimType() == Prospero::PrimitiveType::kPatch;
|
||||
std::array<ShaderParams, 3> vertex_params;
|
||||
if (tess_active) {
|
||||
@@ -694,7 +699,6 @@ PipelineCache::GraphicsPrograms PipelineCache::GetGraphicsPrograms(
|
||||
vertex_params[0] = PrepareProgram(vertex_regs, context, user_config, vertex_info[0]);
|
||||
}
|
||||
const bool mesh_active = vertex_info[0].logical_stage == ShaderType::Mesh;
|
||||
const bool object_primitive_id = context.HasObjectPrimitiveIdOutput();
|
||||
if (object_primitive_id) {
|
||||
// CxObjectIdMode: register source plus a 32-bit pipelined primitive-ID addition.
|
||||
EXIT_NOT_IMPLEMENTED(sh.m_paClObjprimIdCntl != 2u || mesh_active || tess_active ||
|
||||
@@ -725,6 +729,17 @@ PipelineCache::GraphicsPrograms PipelineCache::GetGraphicsPrograms(
|
||||
}
|
||||
ShaderParams pixel_params;
|
||||
if (pixel_active) {
|
||||
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 = context.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;
|
||||
}
|
||||
}
|
||||
pixel_params = PrepareProgram(pixel_regs, sh, target_export_mapping, pixel_info);
|
||||
pixel_info.object_primitive_id = object_primitive_id;
|
||||
for (uint32_t export_index = 0; export_index < 8u; export_index++) {
|
||||
@@ -816,21 +831,25 @@ PipelineCache::GraphicsPrograms PipelineCache::GetGraphicsPrograms(
|
||||
}
|
||||
uint32_t push_data_cursor = ShaderRecompiler::IR::PushData::GraphicsDrawDwords(
|
||||
mesh_active, object_primitive_id, last_stage.user_clip_planes);
|
||||
GraphicsPrograms result;
|
||||
if (pixel_active) {
|
||||
result.pixel = m_program_cache->Get(pixel_params, pixel_info, push_data_cursor);
|
||||
m_program_cache->Get(pixel_params, pixel_info, push_data_cursor);
|
||||
for (const auto& output: pixel_info.stage.program->info.outputs) {
|
||||
if (output.kind == ShaderRecompiler::IR::StageOutputKind::Mrt ||
|
||||
output.kind == ShaderRecompiler::IR::StageOutputKind::ObjectPrimitiveId) {
|
||||
state.mrt_mask |= 1u << output.location;
|
||||
}
|
||||
}
|
||||
state.mrt_mask &= color_output_mask;
|
||||
}
|
||||
for (uint32_t i = 0; i < (tess_active ? 3u : 1u); i++) {
|
||||
result.vertex[i] = m_program_cache->Get(vertex_params[i], vertex_info[i], push_data_cursor);
|
||||
m_program_cache->Get(vertex_params[i], vertex_info[i], push_data_cursor);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
ShaderProgram PipelineCache::GetComputeProgram(const HW::ComputeShaderInfo& regs,
|
||||
const HW::ShaderRegisters& sh,
|
||||
ShaderComputeInputInfo& input_info) {
|
||||
input_info.host_subgroup_size = m_graphics.SupportsComputeWave64() ? 64u : 32u;
|
||||
const auto params = PrepareProgram(regs, sh, input_info);
|
||||
const auto params = PrepareProgram(regs, input_info);
|
||||
input_info.lds_storage = input_info.lds_size_dwords * 4u >
|
||||
m_graphics.GetPhysicalDeviceProperties().limits.maxComputeSharedMemorySize;
|
||||
uint32_t push_data_cursor = 0;
|
||||
@@ -841,32 +860,32 @@ bool PipelineStaticParameters::operator==(const PipelineStaticParameters& other)
|
||||
return std::memcmp(this, &other, sizeof(*this)) == 0;
|
||||
}
|
||||
|
||||
PipelineCache::Pipeline& PipelineCache::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) {
|
||||
PipelineCache::Pipeline& PipelineCache::GetGraphicsPipeline(const CommandBuffer& command,
|
||||
const DrawRenderState& state) {
|
||||
const auto* colors = state.color_info;
|
||||
const auto& depth = state.depth_info;
|
||||
const auto vertex_info = state.VertexStages();
|
||||
const auto& ps_input_info = state.ps_input_info;
|
||||
const auto& vs_input_info = vertex_info.front();
|
||||
const auto& vertex_program = programs.vertex[0];
|
||||
const auto& pixel_program = programs.pixel;
|
||||
const auto* vertex_program = vs_input_info.stage.handle;
|
||||
const auto* pixel_program = state.ps_input_info.stage.handle;
|
||||
KYTY_PROFILER_BLOCK("PipelineCache::CreatePipeline(Gfx)", profiler::colors::DeepOrangeA200);
|
||||
|
||||
EXIT_IF(colors.size() > RENDER_COLOR_ATTACHMENTS_MAX);
|
||||
EXIT_IF(!vertex_program);
|
||||
const bool ps_active = ps_input_info != nullptr;
|
||||
EXIT_IF(ps_active && !pixel_program);
|
||||
const auto color_count = static_cast<uint32_t>(colors.size());
|
||||
EXIT_IF(vertex_program == nullptr);
|
||||
const bool ps_active = state.ps_active;
|
||||
EXIT_IF(ps_active && pixel_program == nullptr);
|
||||
const auto color_count = state.color_count;
|
||||
|
||||
auto& ctx = command.GetRegisters();
|
||||
|
||||
const HW::ModeControl& mc = ctx.GetModeControl();
|
||||
|
||||
const auto vs_id = vertex_program.id;
|
||||
const auto ps_id = ps_active ? pixel_program.id : 0;
|
||||
const auto vs_id = vertex_program->id;
|
||||
const auto ps_id = ps_active ? pixel_program->id : 0;
|
||||
|
||||
GraphicsPipelineKey key {};
|
||||
for (uint32_t i = 0; i < programs.vertex.size(); i++) {
|
||||
key.vertex_shader_ids[i] = programs.vertex[i].id;
|
||||
for (uint32_t i = 0; i < vertex_info.size(); i++) {
|
||||
key.vertex_shader_ids[i] = vertex_info[i].stage.handle->id;
|
||||
}
|
||||
key.ps_shader_id = ps_id;
|
||||
auto& static_params = key.static_params;
|
||||
@@ -893,8 +912,8 @@ PipelineCache::Pipeline& PipelineCache::GetGraphicsPipeline(
|
||||
const auto& rt = ctx.GetRenderTarget(colors[i].target_slot);
|
||||
const auto& bc = ctx.GetBlendControl(colors[i].target_slot);
|
||||
auto alpha_source = ShaderAlphaBlendSource::None;
|
||||
if (slot == 0 && ps_input_info != nullptr) {
|
||||
alpha_source = ps_input_info->alpha_blend_source;
|
||||
if (slot == 0 && ps_active) {
|
||||
alpha_source = ps_input_info.alpha_blend_source;
|
||||
}
|
||||
static_params.blend_enable[slot] = bc.enable && !rt.info.blend_bypass;
|
||||
if (static_params.blend_enable[slot] && alpha_source == ShaderAlphaBlendSource::None &&
|
||||
@@ -964,7 +983,7 @@ PipelineCache::Pipeline& PipelineCache::GetGraphicsPipeline(
|
||||
EXIT_IF(attachment_samples == 0 ||
|
||||
vulkan_sample_count(attachment_samples) == vk::SampleCountFlagBits {});
|
||||
|
||||
if (ps_active && depth.depth_test_enable && ps_input_info->ps_execute_on_noop) {
|
||||
if (ps_active && depth.depth_test_enable && ps_input_info.ps_execute_on_noop) {
|
||||
static std::atomic<uint32_t> log_count {0};
|
||||
if (log_count.fetch_add(1, std::memory_order_relaxed) < 16) {
|
||||
LOGF("Pipeline: temporary: accepting EXEC_ON_NOOP with depth test enabled\n");
|
||||
@@ -974,11 +993,11 @@ PipelineCache::Pipeline& PipelineCache::GetGraphicsPipeline(
|
||||
const auto& clip_control = ctx.GetClipControl();
|
||||
static_params.negative_one_to_one = !clip_control.dx_clip_space;
|
||||
static_params.depth_clip_enable = clip_control.IsZClipEnabled();
|
||||
static_params.topology = topology;
|
||||
static_params.primitive_restart_enable = primitive_restart_enable;
|
||||
static_params.topology = state.topology;
|
||||
static_params.primitive_restart_enable = state.primitive_restart_enable;
|
||||
static_params.samples = attachment_samples;
|
||||
static_params.sample_shading_enable =
|
||||
ps_active && attachment_samples > 1 && ps_input_info->ps_sample_shading;
|
||||
ps_active && attachment_samples > 1 && ps_input_info.ps_sample_shading;
|
||||
if (static_params.sample_shading_enable && !m_graphics.sample_rate_shading_enabled) {
|
||||
EXIT("Pipeline: sample-rate shading is required but unsupported by the host\n");
|
||||
}
|
||||
@@ -1014,73 +1033,67 @@ PipelineCache::Pipeline& PipelineCache::GetGraphicsPipeline(
|
||||
}
|
||||
}
|
||||
|
||||
if (auto iter = m_graphics_pipelines.find(key); iter != m_graphics_pipelines.end()) {
|
||||
return *iter->second;
|
||||
auto [iter, inserted] = m_graphics_pipelines.try_emplace(key);
|
||||
auto& pipeline = iter->second;
|
||||
if (!inserted) {
|
||||
return pipeline;
|
||||
}
|
||||
|
||||
if (graphics_debug_dump_enabled()) {
|
||||
ShaderDbgDumpInputInfo(vs_input_info);
|
||||
if (ps_active) {
|
||||
ShaderDbgDumpInputInfo(*ps_input_info);
|
||||
ShaderDbgDumpInputInfo(ps_input_info);
|
||||
}
|
||||
LOGF("PipelineTrace: shader modules VS=%" PRIu64 " module=%p PS=%" PRIu64 " module=%p\n",
|
||||
vs_id, static_cast<void*>(vertex_program.module), ps_id,
|
||||
static_cast<void*>(pixel_program.module));
|
||||
vs_id, static_cast<void*>(vertex_program->module), ps_id,
|
||||
ps_active ? static_cast<void*>(pixel_program->module) : nullptr);
|
||||
}
|
||||
|
||||
auto cached = std::make_unique<Pipeline>();
|
||||
LogPipelineTrace("CreatePipelineInternal begin", vs_id, ps_id);
|
||||
{
|
||||
Common::DebugCounters::ScopedTimer timer(Common::DebugCounters::Counter::PipelinesCreated,
|
||||
Common::DebugCounters::Counter::PipelineCreateNs,
|
||||
Common::DebugCounters::Peak::PipelineCreateNs);
|
||||
CreatePipelineInternal(m_graphics, *cached, rendering, key.vertex_input, vertex_info,
|
||||
ps_input_info, programs, static_params, m_driver_cache);
|
||||
CreatePipelineInternal(m_graphics, pipeline, rendering, key.vertex_input, state,
|
||||
static_params, m_driver_cache);
|
||||
}
|
||||
LogPipelineTrace("CreatePipelineInternal done", vs_id, ps_id);
|
||||
|
||||
EXIT_NOT_IMPLEMENTED(cached->pipeline == nullptr);
|
||||
EXIT_NOT_IMPLEMENTED(cached->pipeline_layout == nullptr);
|
||||
|
||||
auto [iter, inserted] = m_graphics_pipelines.emplace(std::move(key), std::move(cached));
|
||||
EXIT_IF(!inserted);
|
||||
EXIT_NOT_IMPLEMENTED(pipeline.pipeline == nullptr);
|
||||
EXIT_NOT_IMPLEMENTED(pipeline.pipeline_layout == nullptr);
|
||||
CheckpointDriverCache();
|
||||
|
||||
return *iter->second;
|
||||
return pipeline;
|
||||
}
|
||||
|
||||
PipelineCache::Pipeline&
|
||||
PipelineCache::GetComputePipeline(const ShaderComputeInputInfo& input_info,
|
||||
const ShaderProgram& compute_program) {
|
||||
PipelineCache::Pipeline& PipelineCache::GetComputePipeline(const ShaderComputeInputInfo& input_info) {
|
||||
KYTY_PROFILER_BLOCK("PipelineCache::CreatePipeline(Compute)", profiler::colors::RedA100);
|
||||
|
||||
EXIT_IF(!compute_program);
|
||||
EXIT_IF(input_info.stage.handle == nullptr);
|
||||
const auto& compute_program = *input_info.stage.handle;
|
||||
|
||||
if (auto iter = m_compute_pipelines.find(compute_program.id);
|
||||
iter != m_compute_pipelines.end()) {
|
||||
return *iter->second;
|
||||
auto [iter, inserted] = m_compute_pipelines.try_emplace(compute_program.id);
|
||||
auto& pipeline = iter->second;
|
||||
if (!inserted) {
|
||||
return pipeline;
|
||||
}
|
||||
|
||||
if (graphics_debug_dump_enabled()) {
|
||||
ShaderDbgDumpInputInfo(input_info);
|
||||
}
|
||||
|
||||
auto cached = std::make_unique<Pipeline>();
|
||||
{
|
||||
Common::DebugCounters::ScopedTimer timer(Common::DebugCounters::Counter::PipelinesCreated,
|
||||
Common::DebugCounters::Counter::PipelineCreateNs,
|
||||
Common::DebugCounters::Peak::PipelineCreateNs);
|
||||
CreatePipelineInternal(m_graphics, *cached, input_info, compute_program.module,
|
||||
CreatePipelineInternal(m_graphics, pipeline, input_info, compute_program.module,
|
||||
m_driver_cache);
|
||||
}
|
||||
|
||||
EXIT_NOT_IMPLEMENTED(cached->pipeline == nullptr);
|
||||
EXIT_NOT_IMPLEMENTED(cached->pipeline_layout == nullptr);
|
||||
|
||||
auto [iter, inserted] = m_compute_pipelines.emplace(compute_program.id, std::move(cached));
|
||||
EXIT_IF(!inserted);
|
||||
EXIT_NOT_IMPLEMENTED(pipeline.pipeline == nullptr);
|
||||
EXIT_NOT_IMPLEMENTED(pipeline.pipeline_layout == nullptr);
|
||||
CheckpointDriverCache();
|
||||
|
||||
return *iter->second;
|
||||
return pipeline;
|
||||
}
|
||||
} // namespace Libs::Graphics
|
||||
@@ -4,6 +4,8 @@
|
||||
#include "common/abi.h"
|
||||
#include "common/assert.h"
|
||||
#include "common/common.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/shader.h"
|
||||
@@ -19,17 +21,8 @@
|
||||
namespace Libs::Graphics {
|
||||
|
||||
struct GraphicContext;
|
||||
struct RenderColorInfo;
|
||||
struct RenderDepthInfo;
|
||||
class CommandBuffer;
|
||||
|
||||
namespace HW {
|
||||
class Context;
|
||||
class Shader;
|
||||
class UserConfig;
|
||||
struct ComputeShaderInfo;
|
||||
} // namespace HW
|
||||
|
||||
namespace ShaderRecompiler::IR {
|
||||
class ImageHeapSource;
|
||||
} // namespace ShaderRecompiler::IR
|
||||
@@ -105,6 +98,24 @@ struct ShaderProgram {
|
||||
explicit operator bool() const { return id != 0 && module != nullptr; }
|
||||
};
|
||||
|
||||
// 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};
|
||||
}
|
||||
};
|
||||
|
||||
// The owning renderer serializes access, including saves while the GPU is running.
|
||||
class PipelineCache {
|
||||
public:
|
||||
@@ -121,32 +132,14 @@ public:
|
||||
bool uses_image_heap = false;
|
||||
};
|
||||
|
||||
struct GraphicsPrograms {
|
||||
std::array<ShaderProgram, 3> vertex;
|
||||
ShaderProgram pixel;
|
||||
|
||||
[[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);
|
||||
// Reads the shaders and registers bound in `command` into the state's shader stages.
|
||||
void GetGraphicsPrograms(const CommandBuffer& command, DrawRenderState& state);
|
||||
// An empty program skips the dispatch.
|
||||
ShaderProgram GetComputeProgram(const HW::ComputeShaderInfo& regs,
|
||||
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;
|
||||
@@ -182,9 +175,8 @@ private:
|
||||
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;
|
||||
|
||||
void InitializeDriverCache();
|
||||
void CheckpointDriverCache();
|
||||
@@ -192,13 +184,11 @@ private:
|
||||
|
||||
void LogPipelineTrace(const char* phase, uint64_t vertex_program_id, uint64_t pixel_program_id);
|
||||
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 PipelineRenderingState& rendering,
|
||||
const PipelineVertexInputState& vertex_input,
|
||||
const DrawRenderState& state,
|
||||
const PipelineStaticParameters& static_params,
|
||||
vk::PipelineCache driver_cache);
|
||||
void CreatePipelineInternal(GraphicContext& graphics, PipelineCache::Pipeline& pipeline,
|
||||
const ShaderComputeInputInfo& input_info,
|
||||
vk::ShaderModule compute_module, vk::PipelineCache driver_cache);
|
||||
|
||||
@@ -242,22 +242,20 @@ 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 PipelineRenderingState& rendering,
|
||||
const PipelineVertexInputState& vertex_input,
|
||||
const DrawRenderState& state,
|
||||
const PipelineStaticParameters& static_params,
|
||||
vk::PipelineCache driver_cache) {
|
||||
const auto vertex_info = state.VertexStages();
|
||||
const auto& vs_input_info = vertex_info.front();
|
||||
const auto& vertex_program = programs.vertex[0];
|
||||
const auto& pixel_program = programs.pixel;
|
||||
const auto* ps_input_info = state.ps_active ? &state.ps_input_info : nullptr;
|
||||
const auto& vertex_program = *vs_input_info.stage.handle;
|
||||
const auto* pixel_program = state.ps_input_info.stage.handle;
|
||||
const bool tessellation = vertex_info.size() == 3;
|
||||
const bool ps_active = ps_input_info != nullptr;
|
||||
EXIT_IF(!vertex_program || (ps_active && !pixel_program));
|
||||
const bool ps_active = state.ps_active;
|
||||
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;
|
||||
EXIT_NOT_IMPLEMENTED(mesh && !graphics.mesh_shader_enabled);
|
||||
const bool rect_list =
|
||||
@@ -267,8 +265,7 @@ void CreatePipelineInternal(GraphicContext& graphics, PipelineCache::Pipeline& p
|
||||
vk::ShaderModule tess_eval_shader_module = nullptr;
|
||||
|
||||
if (rect_list) {
|
||||
const auto shaders =
|
||||
BuildRectListShaders(vs_input_info, ps_active ? ps_input_info : nullptr);
|
||||
const auto shaders = BuildRectListShaders(vs_input_info, ps_input_info);
|
||||
tess_control_shader_module = CompileSPV(shaders.control, graphics.device);
|
||||
if (graphics_debug_dump_enabled()) {
|
||||
LOGF("PipelineTrace: vkCreateShaderModule RectList TCS done module=%p\n",
|
||||
@@ -290,7 +287,7 @@ void CreatePipelineInternal(GraphicContext& graphics, PipelineCache::Pipeline& p
|
||||
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,
|
||||
.module = vertex_info[i].stage.handle->module,
|
||||
.pName = "main"};
|
||||
}
|
||||
if (rect_list) {
|
||||
@@ -305,7 +302,7 @@ void CreatePipelineInternal(GraphicContext& graphics, PipelineCache::Pipeline& p
|
||||
}
|
||||
if (ps_active) {
|
||||
shader_stages[shader_stage_count++] = {.stage = vk::ShaderStageFlagBits::eFragment,
|
||||
.module = pixel_program.module,
|
||||
.module = pixel_program->module,
|
||||
.pName = "main"};
|
||||
}
|
||||
|
||||
@@ -477,7 +474,6 @@ void CreatePipelineInternal(GraphicContext& graphics, PipelineCache::Pipeline& p
|
||||
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;
|
||||
@@ -498,7 +494,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);
|
||||
vertex_program.id, ps_active ? pixel_program->id : 0, 1u);
|
||||
}
|
||||
auto result = graphics.device.createPipelineLayout(&pipeline_layout_info, nullptr,
|
||||
&pipeline.pipeline_layout);
|
||||
@@ -573,7 +569,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,
|
||||
vertex_program.id, ps_active ? pixel_program->id : 0,
|
||||
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);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,9 +310,7 @@ 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;
|
||||
@@ -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,11 +497,9 @@ 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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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"
|
||||
@@ -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,15 +1046,14 @@ 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 auto vertex_stages = state.VertexStages();
|
||||
const bool mesh_active = state.vertex_info[0].stage.program->stage == ShaderType::Mesh;
|
||||
uint32_t mesh_groups = 0;
|
||||
if (mesh_active) {
|
||||
@@ -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,
|
||||
@@ -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;
|
||||
@@ -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();
|
||||
|
||||
@@ -584,10 +584,13 @@ 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
|
||||
@@ -617,12 +620,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 +655,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 +685,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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -663,19 +660,6 @@ struct ResourcePlan {
|
||||
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 +679,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;
|
||||
|
||||
@@ -186,35 +186,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 +280,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 +334,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 +363,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 +379,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 +433,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 +454,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 +512,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 +575,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 +629,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 +741,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 +758,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 +797,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))
|
||||
@@ -1380,16 +1350,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 +1378,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 =
|
||||
@@ -1523,57 +1489,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);
|
||||
if (plan.srt_plan_complete && plan.resource_tracking_complete) CompileSrtWalker(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 +1507,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 +1531,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 +1572,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 +1582,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 +1632,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 +1649,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 +1670,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());
|
||||
|
||||
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_ */
|
||||
@@ -40,9 +40,15 @@
|
||||
|
||||
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;
|
||||
@@ -64,8 +70,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 +270,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 +286,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 +385,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;
|
||||
@@ -684,10 +691,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;
|
||||
@@ -49,9 +38,12 @@ namespace ShaderRecompiler::IR {
|
||||
struct CompiledShaderInfo;
|
||||
} // namespace ShaderRecompiler::IR
|
||||
|
||||
struct ShaderProgram;
|
||||
|
||||
struct ShaderStageRuntime {
|
||||
const ShaderRecompiler::IR::CompiledShaderInfo* program = nullptr;
|
||||
const ShaderRecompiler::IR::ResourceSnapshot* resources = nullptr;
|
||||
const ShaderProgram* handle = nullptr;
|
||||
|
||||
[[nodiscard]] explicit operator bool() const {
|
||||
return program != nullptr && resources != nullptr;
|
||||
|
||||
@@ -39,8 +39,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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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,6 @@
|
||||
#include "graphics/shader/recompiler/ir/ShaderIR.h"
|
||||
#include "graphics/shader/recompiler/ir/passes/ResourceMaterialization.h"
|
||||
#include "SrtWalkerTestMemory.h"
|
||||
|
||||
#include <bit>
|
||||
#include <cstdio>
|
||||
@@ -32,6 +33,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;
|
||||
@@ -149,7 +168,7 @@ 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)));
|
||||
@@ -162,10 +181,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() {
|
||||
@@ -209,17 +228,6 @@ void TestNativeNestedSrtRefresh() {
|
||||
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 +235,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,12 +258,11 @@ 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);
|
||||
@@ -266,17 +279,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 +330,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];
|
||||
@@ -362,64 +364,27 @@ void TestSrtAliasesRetainReadPolicy() {
|
||||
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 +418,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,
|
||||
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 = ExtractResourcePlan(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 = ExtractResourcePlan(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 = ExtractResourcePlan(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 +495,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 +504,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 +541,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)});
|
||||
@@ -595,35 +567,25 @@ void TestWrittenDescriptorPredicateReads(bool memory_condition) {
|
||||
auto plan = ExtractResourcePlan(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>>{}),
|
||||
@@ -680,16 +642,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}),
|
||||
@@ -748,34 +707,27 @@ void TestNestedExecPredicatesReuseReads() {
|
||||
{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) {
|
||||
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");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -821,16 +773,8 @@ void TestFiniteImageRefreshReusesScalarReads() {
|
||||
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 +782,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 +802,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");
|
||||
@@ -968,36 +913,35 @@ void TestEmptyTraversalSkipsUnmappedDescriptor(bool independent_mask, bool count
|
||||
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) {
|
||||
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 +949,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");
|
||||
@@ -1051,16 +995,89 @@ void TestCyclicControlPredicatesRemainUnknown() {
|
||||
branch(left, a, right, exit);
|
||||
branch(right, b, left, exit);
|
||||
auto plan = ExtractResourcePlan(program);
|
||||
SrtWalker walker(plan, {});
|
||||
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 = ExtractResourcePlan(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 {
|
||||
@@ -1120,8 +1137,9 @@ void TestEmptyScalarBufferSpecialization() {
|
||||
auto plan = ExtractResourcePlan(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}) {
|
||||
@@ -1353,10 +1371,10 @@ void DbgExit(int) { std::abort(); }
|
||||
} // namespace Common
|
||||
|
||||
int main() {
|
||||
TestMappedSrtUsesDirectReaderByDefault();
|
||||
TestWalkerLoadsBypassSpecializationReader();
|
||||
TestNativeNestedSrtRefresh();
|
||||
TestIntegerRuntimeValueFollowsSrtReads();
|
||||
TestSrtAliasesRetainReadPolicy();
|
||||
TestSrtAliasesShareReads();
|
||||
TestUniformVectorDescriptorRead();
|
||||
TestExactReciprocalDescriptorArithmetic();
|
||||
TestUnbasedFlatCacheHitMaterializes();
|
||||
@@ -1370,6 +1388,7 @@ int main() {
|
||||
TestEmptyTraversalSkipsUnmappedDescriptor(independent_mask);
|
||||
TestEmptyTraversalSkipsUnmappedDescriptor(false, true);
|
||||
TestCyclicControlPredicatesRemainUnknown();
|
||||
TestBackEdgeReachesPhiPredecessor();
|
||||
TestEmptyScalarBufferSpecialization();
|
||||
TestMixedSamplerVariantsShareRuntimeDescriptor();
|
||||
TestSignedStorageSpecialization();
|
||||
|
||||
+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");
|
||||
}
|
||||
}
|
||||
@@ -831,8 +841,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");
|
||||
}
|
||||
|
||||
|
||||
@@ -60,6 +60,7 @@
|
||||
#include "libs/errno.h"
|
||||
#include "loader/symbolDatabase.h"
|
||||
#include "spirv-tools/libspirv.hpp"
|
||||
#include "SrtWalkerTestMemory.h"
|
||||
|
||||
#if __has_include("graphics/host_gpu/renderer/renderTargetBarriers.h")
|
||||
#error "legacy render-target barrier API must remain deleted"
|
||||
@@ -193,6 +194,8 @@ struct BufferCacheTestAccess {
|
||||
return cache.SynchronizeBufferFromImage(buffer, address, size);
|
||||
}
|
||||
|
||||
static std::mutex &BdaSyncMutex(BufferCache &cache) { return cache.m_bda_sync_mutex; }
|
||||
|
||||
static BufferId PageOwner(const BufferCache &cache, uint64_t address) {
|
||||
const auto *owner = cache.m_page_table.Find(
|
||||
address >> BufferCache::PageTable::kPageBits);
|
||||
@@ -328,32 +331,6 @@ struct TextureCacheTestAccess {
|
||||
return count;
|
||||
}
|
||||
|
||||
static void AddPageOwner(TextureCache &cache, uint64_t address, ImageId id) {
|
||||
std::lock_guard lock(cache.m_lock);
|
||||
cache
|
||||
.m_image_page_table[static_cast<size_t>(
|
||||
address >> TextureCache::ImagePageTable::kPageBits)]
|
||||
.push_back(id);
|
||||
}
|
||||
|
||||
static bool RemovePageOwner(TextureCache &cache, uint64_t address,
|
||||
ImageId id) {
|
||||
std::lock_guard lock(cache.m_lock);
|
||||
auto *owners = cache.m_image_page_table.Find(static_cast<size_t>(
|
||||
address >> TextureCache::ImagePageTable::kPageBits));
|
||||
return owners != nullptr && owners->Erase(id);
|
||||
}
|
||||
|
||||
static void SetQueryEpoch(TextureCache &cache, uint32_t epoch) {
|
||||
std::lock_guard lock(cache.m_lock);
|
||||
cache.m_image_query_epoch = epoch;
|
||||
}
|
||||
|
||||
static uint32_t QueryEpoch(TextureCache &cache) {
|
||||
std::lock_guard lock(cache.m_lock);
|
||||
return cache.m_image_query_epoch;
|
||||
}
|
||||
|
||||
static ImageId InsertImage(TextureCache &cache, const ImageInfo &info) {
|
||||
std::lock_guard lock(cache.m_lock);
|
||||
return cache.InsertImage(info);
|
||||
@@ -1722,15 +1699,17 @@ CompiledShader CompileCase(const TestCase &test, u32 host_subgroup_size = 64) {
|
||||
const ShaderRecompiler::IR::SrtRuntime runtime{
|
||||
.user_data = options.user_data,
|
||||
.shader_base = reinterpret_cast<uint64_t>(test.code.data()),
|
||||
.read_memory = ReadTestMemory,
|
||||
.userdata = const_cast<std::vector<u32> *>(&test.initial),
|
||||
.read_specialization_memory = ReadTestMemory,
|
||||
.workgroup_counts = workgroup_counts,
|
||||
};
|
||||
Require(test.name, "resource materialization",
|
||||
ShaderRecompiler::IR::MaterializeResources(
|
||||
resource_plan, runtime, resources, specialization),
|
||||
"translated resources could not be materialized");
|
||||
{
|
||||
FakeGuestMemory fake(ReadTestMemory, runtime.userdata);
|
||||
Require(test.name, "resource materialization",
|
||||
ShaderRecompiler::IR::MaterializeResources(
|
||||
resource_plan, runtime, resources, specialization),
|
||||
"translated resources could not be materialized");
|
||||
}
|
||||
auto result = ShaderRecompiler::CompileProgram(
|
||||
std::move(translated), options, specialization);
|
||||
for (const auto &[text, expected] : test.decoded_counts) {
|
||||
@@ -4338,15 +4317,6 @@ public:
|
||||
BufferCacheTestAccess::PageOwner(
|
||||
cache, index_begin + 2 * index_page) == index_bridge,
|
||||
"bridging acquisition did not publish its merged Buffer range");
|
||||
scheduler.Finish();
|
||||
constexpr uint64_t recycled_offset = index_offset + 4 * index_page;
|
||||
const auto recycled = cache.FindBuffer(base + recycled_offset + 0x100,
|
||||
sizeof(uint32_t));
|
||||
Require(name, "slot generation reuse",
|
||||
(recycled.index == index_left.index && recycled != index_left) ||
|
||||
(recycled.index == index_right.index &&
|
||||
recycled != index_right),
|
||||
"retired Buffer slot was not reused with a new generation");
|
||||
const auto [resolved_left, resolved_left_offset] = cache.ObtainBuffer(
|
||||
index_begin + 0x100, sizeof(uint32_t), true, false, index_left);
|
||||
const auto [resolved_right, resolved_right_offset] =
|
||||
@@ -4357,6 +4327,12 @@ public:
|
||||
resolved_right == resolved_left &&
|
||||
resolved_left_offset != resolved_right_offset,
|
||||
"saved descriptor IDs did not resolve through the merged owner");
|
||||
scheduler.Finish();
|
||||
constexpr uint64_t recycled_offset = index_offset + 4 * index_page;
|
||||
const auto recycled = cache.FindBuffer(base + recycled_offset + 0x100,
|
||||
sizeof(uint32_t));
|
||||
Require(name, "slot reuse", recycled == index_left || recycled == index_right,
|
||||
"retired Buffer slot was not reused");
|
||||
|
||||
constexpr uint64_t stream_page = BufferCache::CACHING_PAGESIZE;
|
||||
constexpr uint64_t stream_leap = stream_page * 128;
|
||||
@@ -5460,6 +5436,28 @@ public:
|
||||
Require(name, "unmapped descriptor", !textures.IsRegionRegistered(texture(23), 0x1000u),
|
||||
"a descriptor over unmapped memory was resolved");
|
||||
|
||||
// An idle view keeps the element of a freed image. Once a new image takes its slot,
|
||||
// reactivating the view must resolve the descriptor again.
|
||||
write(30, descriptor(30, Format::k8_8_8_8UNorm));
|
||||
Require(name, "idle element", acquire(unfiltered), "the view was declined");
|
||||
draw();
|
||||
const auto erased = ImageHeapTestAccess::ImageOf(heap, idle.slots[240]);
|
||||
textures.UnmapMemory(texture(30), 0x1000u);
|
||||
Require(name, "replacement", acquire(sampled), "the view was declined");
|
||||
draw();
|
||||
write(31, descriptor(31, Format::k8_8_8_8UNorm));
|
||||
Require(name, "slot reuse", acquire(sampled), "the view was declined");
|
||||
draw();
|
||||
Require(name, "reused slot",
|
||||
erased && ImageHeapTestAccess::ImageOf(heap, filtered.slots[248]) == erased,
|
||||
"the next image did not take the freed image's slot");
|
||||
Require(name, "reactivated idle view", acquire(unfiltered), "the view was declined");
|
||||
draw();
|
||||
Require(name, "reused slot element",
|
||||
ImageHeapTestAccess::ImageOf(heap, idle.slots[240]) ==
|
||||
ImageHeapTestAccess::ImageOf(heap, filtered.slots[240]),
|
||||
"a reactivated view kept an element whose image slot holds a new image");
|
||||
|
||||
heap.UnmapMemory(table, table_size);
|
||||
Require(name, "unmapped table",
|
||||
ImageHeapTestAccess::MemoryCount(heap) == 0u &&
|
||||
@@ -5596,10 +5594,18 @@ public:
|
||||
constexpr const char *name = "BdaSyncUploads";
|
||||
constexpr auto page = BufferCache::CACHING_PAGESIZE;
|
||||
EnsureRuntimeContext();
|
||||
const auto base = LibKernel::Memory::AllocateRuntimeMemory(
|
||||
0, 3 * page, Common::VirtualMemory::Mode::ReadWrite, "bda_sync_uploads");
|
||||
Require(name, "allocation", base != 0, "guest allocation failed");
|
||||
auto *words = reinterpret_cast<u32 *>(base);
|
||||
// Direct memory, so readbacks can publish GPU-dirty bytes.
|
||||
int64_t direct = -1;
|
||||
void *mapped = nullptr;
|
||||
Require(name, "allocation",
|
||||
LibKernel::Memory::KernelAllocateDirectMemory(
|
||||
0, LibKernel::Memory::KernelGetDirectMemorySize(), 3 * page, page, 0, &direct) ==
|
||||
0 &&
|
||||
LibKernel::Memory::KernelMapDirectMemory(&mapped, 3 * page, 0x3, 0, direct, page) ==
|
||||
0,
|
||||
"guest allocation failed");
|
||||
const auto base = reinterpret_cast<uint64_t>(mapped);
|
||||
auto *words = static_cast<u32 *>(mapped);
|
||||
std::fill_n(words, 3 * page / sizeof(u32), 0x12345678u);
|
||||
RenderContext context(m_runtime_context);
|
||||
auto &scheduler = context.GetCommandScheduler();
|
||||
@@ -5608,6 +5614,7 @@ public:
|
||||
HW::UserConfig user_config{};
|
||||
HW::Shader shaders{};
|
||||
scheduler.Begin(registers, user_config, shaders);
|
||||
context.InitializeGpu(nullptr);
|
||||
context.MapMemory(base, page);
|
||||
context.MapMemory(base + 2 * page, page);
|
||||
const auto id = cache.FindBuffer(base, 3 * page);
|
||||
@@ -5650,6 +5657,34 @@ public:
|
||||
context.PrepareBda();
|
||||
snapshot(2 * page, 5);
|
||||
|
||||
// A write fault on GPU-dirty memory flushes it on the GPU thread, which must queue the page
|
||||
// itself: the faulting thread can resume only after later commands.
|
||||
(void)cache.ObtainBuffer(base + 20, sizeof(u32), true, false);
|
||||
std::unique_lock queue_lock(BufferCacheTestAccess::BdaSyncMutex(cache));
|
||||
std::jthread fault([&] { cache.InvalidateMemory(base + 20, sizeof(u32)); });
|
||||
while (!cache.IsRegionCpuModified(base, TRACKER_PAGE_SIZE)) {
|
||||
std::this_thread::yield();
|
||||
}
|
||||
std::binary_semaphore later_command{0};
|
||||
context.GetGpu().SendCommand([&] { later_command.release(); });
|
||||
Require(name, "flush queue order",
|
||||
!later_command.try_acquire_for(std::chrono::milliseconds(100)),
|
||||
"a GPU command ran before the write-fault flush queued its page");
|
||||
queue_lock.unlock();
|
||||
fault.join();
|
||||
later_command.acquire();
|
||||
|
||||
// Garbage collection untracks a clean buffer; its replacement queues the range on Register.
|
||||
BufferCacheTestAccess::SetGarbageCollectionThresholds(
|
||||
cache, 0, std::numeric_limits<uint64_t>::max());
|
||||
cache.RunGarbageCollector();
|
||||
Require(name, "clean eviction", !cache.IsRegionRegistered(base, 3 * page),
|
||||
"garbage collection kept the clean buffer");
|
||||
(void)cache.FindBuffer(base, 3 * page);
|
||||
context.PrepareBda();
|
||||
Require(name, "replacement upload", !cache.IsRegionCpuModified(base, 3 * page),
|
||||
"a buffer replacing an evicted one did not upload its untracked pages");
|
||||
|
||||
vk::BufferMemoryBarrier barrier{};
|
||||
barrier.srcAccessMask = vk::AccessFlagBits::eTransferWrite;
|
||||
barrier.dstAccessMask = vk::AccessFlagBits::eHostRead;
|
||||
@@ -5666,7 +5701,9 @@ public:
|
||||
context.UnmapMemory(base, 3 * page);
|
||||
scheduler.Finish();
|
||||
DestroyBuffer(&readback);
|
||||
Require(name, "release", LibKernel::Memory::FreeGuestMemory(base, 3 * page),
|
||||
Require(name, "release",
|
||||
LibKernel::Memory::KernelMunmap(base, 3 * page) == 0 &&
|
||||
LibKernel::Memory::KernelReleaseDirectMemory(direct, 3 * page) == 0,
|
||||
"guest allocation release failed");
|
||||
std::printf("[gpu] %-32s ok\n", name);
|
||||
}
|
||||
@@ -6016,23 +6053,6 @@ public:
|
||||
large_owner) == 0,
|
||||
"large image unregister left coarse memberships behind");
|
||||
|
||||
const ImageId stale{first.index, first.generation + 1};
|
||||
TextureCacheTestAccess::AddPageOwner(texture_cache, base, stale);
|
||||
const auto stale_filtered = TextureCacheTestAccess::FindImages(
|
||||
texture_cache, base, sizeof(initial), false);
|
||||
Require(name, "stale page owner filtering",
|
||||
IsOnlyImage(stale_filtered, first) &&
|
||||
TextureCacheTestAccess::RemovePageOwner(texture_cache, base,
|
||||
stale),
|
||||
"a stale generation escaped the direct page-owner lookup");
|
||||
|
||||
TextureCacheTestAccess::SetQueryEpoch(texture_cache, UINT32_MAX);
|
||||
const auto wrap_results = TextureCacheTestAccess::FindImages(
|
||||
texture_cache, base, sizeof(initial), false);
|
||||
Require(name, "page-owner query epoch wrap",
|
||||
IsOnlyImage(wrap_results, first) &&
|
||||
TextureCacheTestAccess::QueryEpoch(texture_cache) == 1,
|
||||
"query deduplication failed while wrapping its epoch");
|
||||
|
||||
auto &image = texture_cache.GetImage(first);
|
||||
constexpr uint32_t final_sampled_value = 0x88776655u;
|
||||
@@ -10963,7 +10983,7 @@ public:
|
||||
shaders.SetCsUserSgpr(12 + i, output.fields[i]);
|
||||
ShaderComputeInputInfo input{};
|
||||
(void)context.GetPipelineCache().GetComputeProgram(
|
||||
shaders.GetCs(), processor.GetCtx().GetShaderRegisters(), input);
|
||||
shaders.GetCs(), input);
|
||||
PreparedBindings bindings;
|
||||
executor.PrepareBindings(input.stage, bindings);
|
||||
executor.RebindImages(bindings);
|
||||
@@ -11078,7 +11098,7 @@ public:
|
||||
}
|
||||
ShaderComputeInputInfo input{};
|
||||
const auto program = context.GetPipelineCache().GetComputeProgram(
|
||||
shaders.GetCs(), processor.GetCtx().GetShaderRegisters(), input);
|
||||
shaders.GetCs(), input);
|
||||
if (original_id == 0) original_id = program.id;
|
||||
Require(name, "exact function identity",
|
||||
(program.id == original_id) == (test.value == 41),
|
||||
@@ -11170,7 +11190,7 @@ public:
|
||||
LibKernel::Memory::WriteBacking(output, actual.data(), wave_size * 4);
|
||||
ShaderComputeInputInfo input{};
|
||||
const auto program = context.GetPipelineCache().GetComputeProgram(
|
||||
shaders.GetCs(), processor.GetCtx().GetShaderRegisters(), input);
|
||||
shaders.GetCs(), input);
|
||||
if (phase == 1) original_vector_id = program.id;
|
||||
if (phase >= 2)
|
||||
Require(name, "ordered helper identity", (program.id == original_vector_id) == (phase == 3),
|
||||
@@ -11194,7 +11214,7 @@ public:
|
||||
};
|
||||
const ShaderRecompiler::IR::SrtRuntime runtime{
|
||||
.user_data = user_data, .shader_base = reinterpret_cast<uint64_t>(caller.data()),
|
||||
.read_memory = read, .read_specialization_memory = read};
|
||||
.read_specialization_memory = read};
|
||||
auto source = ShaderRecompiler::PrepareShaderSource(caller, options);
|
||||
auto translated = ShaderRecompiler::TranslateProgram(
|
||||
ShaderRecompiler::RefreshShaderSource(source, runtime), options);
|
||||
@@ -11713,7 +11733,7 @@ public:
|
||||
const auto clean_tick = scheduler.CurrentTick();
|
||||
ShaderComputeInputInfo input{};
|
||||
const auto program = context.GetPipelineCache().GetComputeProgram(
|
||||
shaders.GetCs(), processor.GetCtx().GetShaderRegisters(), input);
|
||||
shaders.GetCs(), input);
|
||||
if (previous_program.id != 0) {
|
||||
Require(name, "count permutation reuse", program.id == previous_program.id,
|
||||
"a changing GPU count compiled a new shader permutation");
|
||||
@@ -11795,7 +11815,7 @@ public:
|
||||
input.workgroup_counts[0] = input.workgroup_counts[1] = 1;
|
||||
input.workgroup_counts[2] = test.groups;
|
||||
const auto program = context.GetPipelineCache().GetComputeProgram(
|
||||
shaders.GetCs(), processor.GetCtx().GetShaderRegisters(), input);
|
||||
shaders.GetCs(), input);
|
||||
Require(name, "workgroup table refresh and coalescing",
|
||||
input.workgroup_counts[2] == test.groups &&
|
||||
input.stage.program->info.images.size() == 1 &&
|
||||
@@ -11813,7 +11833,7 @@ public:
|
||||
const auto *image_storage = snapshot->images.data();
|
||||
const auto flattened_capacity = snapshot->flattened_srt.capacity();
|
||||
const auto repeated = context.GetPipelineCache().GetComputeProgram(
|
||||
shaders.GetCs(), processor.GetCtx().GetShaderRegisters(), input);
|
||||
shaders.GetCs(), input);
|
||||
Require(name, "workgroup table cache-hit storage reuse",
|
||||
repeated.id == program.id && input.stage.resources == snapshot &&
|
||||
snapshot->images.data() == image_storage &&
|
||||
@@ -12052,7 +12072,7 @@ public:
|
||||
input.workgroup_counts[0] = material_groups;
|
||||
input.workgroup_counts[1] = input.workgroup_counts[2] = 1u;
|
||||
(void)context.GetPipelineCache().GetComputeProgram(
|
||||
shaders.GetCs(), processor.GetCtx().GetShaderRegisters(), input);
|
||||
shaders.GetCs(), input);
|
||||
const auto &info = input.stage.program->info;
|
||||
const auto root = std::ranges::find_if(info.buffers, [](const auto &buffer) {
|
||||
return buffer.indirect_resources.size() == material_rows;
|
||||
@@ -16965,9 +16985,7 @@ public:
|
||||
scheduler.EndRendering();
|
||||
RenderExecutorTestAccess::ResetBindings(executor);
|
||||
Require(name, "draw-scoped target reset",
|
||||
TextureCacheTestAccess::Owner(texture_cache,
|
||||
target_subresource_id) == nullptr &&
|
||||
!texture_cache.GetImage(target_parent_id).binding.is_target &&
|
||||
!texture_cache.GetImage(target_parent_id).binding.is_target &&
|
||||
!texture_cache.GetImage(depth_id).binding.is_target,
|
||||
"RenderExecutor retained target binding state after reset");
|
||||
|
||||
@@ -17117,12 +17135,7 @@ public:
|
||||
stencil_compute.stage.resources = &stencil_snapshot;
|
||||
Require(name, "stencil fill materialization",
|
||||
ShaderRecompiler::IR::MaterializeResources(stencil_plan,
|
||||
{.user_data = stencil_userdata, .userdata = &stencil_byte,
|
||||
.read_specialization_memory = +[](void *data, uint64_t address, std::span<uint32_t> words) {
|
||||
if (address != reinterpret_cast<uint64_t>(data) || words.size() != 1u) return false;
|
||||
words[0] = *static_cast<uint32_t *>(data);
|
||||
return true;
|
||||
}}, stencil_snapshot, stencil_specialization),
|
||||
{.user_data = stencil_userdata}, stencil_snapshot, stencil_specialization),
|
||||
"captured stencil shader could not resolve its clear byte");
|
||||
ShaderRecompiler::IR::ApplyResourceSpecialization(stencil_translated.program,
|
||||
stencil_specialization);
|
||||
@@ -18352,10 +18365,12 @@ public:
|
||||
mode.polymode_back_ptype = back;
|
||||
mode.provoking_vtx_last = provoking_last;
|
||||
registers.SetModeControl(mode);
|
||||
return context.GetPipelineCache().GetGraphicsPipeline(
|
||||
std::span{&color, 1u}, depth, std::span{&vertex, 1u}, scheduler.Current(), &pixel,
|
||||
topology, false,
|
||||
PipelineCache::GraphicsPrograms{{vertex_shader}, pixel_shader});
|
||||
DrawRenderState state{.depth_info = depth, .color_info = {color}, .color_count = 1,
|
||||
.topology = topology, .ps_active = true,
|
||||
.vertex_info = {vertex}, .ps_input_info = pixel};
|
||||
state.vertex_info[0].stage.handle = &vertex_shader;
|
||||
state.ps_input_info.stage.handle = &pixel_shader;
|
||||
return context.GetPipelineCache().GetGraphicsPipeline(scheduler.Current(), state);
|
||||
};
|
||||
auto &filled = pipeline(true, 2, 2);
|
||||
const auto draw = [&](const PipelineCache::Pipeline &selected, uint32_t vertex_count = 3,
|
||||
@@ -18706,17 +18721,50 @@ public:
|
||||
ShaderMapUserData(pixel_address,
|
||||
{.type = Prospero::ShaderBinaryType::kPs,
|
||||
.code_size_bytes = static_cast<uint32_t>(native_pixel.size() * sizeof(u32))});
|
||||
HW::VertexShaderInfo native_vertex_regs{};
|
||||
native_vertex_regs.es_regs.data_addr = vertex_address;
|
||||
HW::PixelShaderInfo native_pixel_regs{};
|
||||
native_pixel_regs.ps_regs.data_addr = pixel_address;
|
||||
shaders.SetEsShaderBase(vertex_address);
|
||||
shaders.SetPsShaderBase(pixel_address);
|
||||
const auto owned_vertex_shader = vertex_shader;
|
||||
const auto owned_pixel_shader = pixel_shader;
|
||||
depth.stencil_test_enable = false;
|
||||
registers.SetViewportTransformControl(0x300);
|
||||
user_config.SetPrimitiveType(Prospero::PrimitiveType::kTriList);
|
||||
std::array<Prospero::ColorComponentMapping, 8> export_mapping{};
|
||||
std::array<ShaderVertexInputInfo, 3> native_vertex_info{};
|
||||
// The cache reads the bound shaders, the color outputs and the export mapping itself.
|
||||
const auto refresh = [&] {
|
||||
DrawRenderState state{};
|
||||
context.GetPipelineCache().GetGraphicsPrograms(scheduler.Current(), state);
|
||||
return state;
|
||||
};
|
||||
const auto native_programs = [&] {
|
||||
const auto native = refresh();
|
||||
vertex = native.vertex_info[0];
|
||||
pixel = native.ps_input_info;
|
||||
vertex_shader = *vertex.stage.handle;
|
||||
pixel_shader = *pixel.stage.handle;
|
||||
};
|
||||
const auto silent = refresh();
|
||||
registers.SetDepthShaderControl({.shader_kill_enable = true});
|
||||
const auto killing = refresh();
|
||||
registers.SetDepthShaderControl({.shader_z_export_enable = true});
|
||||
const auto depth_writing = refresh();
|
||||
registers.SetDepthShaderControl({});
|
||||
registers.SetTargetOutputMode(0, 4);
|
||||
const auto writing = refresh();
|
||||
// Slot 1 takes color output, but the pixel program exports only MRT0.
|
||||
registers.SetRenderTargetMask(0xff);
|
||||
registers.SetShaderMask(0xff);
|
||||
registers.SetTargetOutputMode(1, 4);
|
||||
const auto partial = refresh();
|
||||
registers.SetTargetOutputMode(1, 0);
|
||||
registers.SetShaderMask(0xf);
|
||||
registers.SetRenderTargetMask(0xf);
|
||||
Require(name, "pixel stage activity",
|
||||
!silent.ps_active && !silent.ps_input_info.stage && silent.mrt_mask == 0 &&
|
||||
killing.ps_active && killing.ps_input_info.stage && killing.mrt_mask == 0 &&
|
||||
depth_writing.ps_active && depth_writing.mrt_mask == 0 &&
|
||||
writing.ps_active && writing.mrt_mask == 1u && partial.mrt_mask == 1u &&
|
||||
writing.VertexStages().size() == 1 &&
|
||||
writing.vertex_info[0].stage.handle != nullptr,
|
||||
"pixel activity or its MRT mask ignored the color outputs or depth side effects");
|
||||
std::array<uint64_t, 2> wave_ids{};
|
||||
std::array<std::vector<u32>, 2> wave_keys;
|
||||
struct PixelWaveCase { uint32_t control, width, color; };
|
||||
@@ -18724,9 +18772,7 @@ public:
|
||||
PixelWaveCase{0x8008, 32, 0x3e800000},
|
||||
PixelWaveCase{0x0008, 64, 0}}) {
|
||||
registers.SetPsInControl(wave.control);
|
||||
const auto programs = context.GetPipelineCache().GetGraphicsPrograms(
|
||||
native_vertex_regs, native_pixel_regs, registers.GetShaderRegisters(),
|
||||
registers, user_config, export_mapping, true, native_vertex_info, pixel);
|
||||
native_programs();
|
||||
Require(name, "pixel wave metadata propagation",
|
||||
pixel.input_num == 8 && pixel.stage.program->wave_size == wave.width,
|
||||
"SPI_PS_IN_CONTROL width did not reach the compiled pixel program");
|
||||
@@ -18735,14 +18781,11 @@ public:
|
||||
BuildStageStaticKey(pixel, key);
|
||||
if (wave_ids[index] != 0) {
|
||||
Require(name, "pixel wave cache reuse",
|
||||
programs.pixel.id == wave_ids[index] && key == wave_keys[index],
|
||||
pixel_shader.id == wave_ids[index] && key == wave_keys[index],
|
||||
"restoring wave64 did not reuse its original program");
|
||||
}
|
||||
wave_ids[index] = programs.pixel.id;
|
||||
wave_ids[index] = pixel_shader.id;
|
||||
wave_keys[index] = std::move(key);
|
||||
vertex_shader = programs.vertex[0];
|
||||
pixel_shader = programs.pixel;
|
||||
vertex = native_vertex_info[0];
|
||||
draw(pipeline(true, 2, 2));
|
||||
const auto wave_pixels = read_color();
|
||||
for (size_t component = 0; component < wave_pixels.size(); component++) {
|
||||
@@ -18778,8 +18821,8 @@ public:
|
||||
*(position + 1) = EncodeExp1(3, 4, 0, 9);
|
||||
return code;
|
||||
}();
|
||||
native_vertex_regs.es_regs.data_addr = reinterpret_cast<uint64_t>(waterfall_vertex.data());
|
||||
ShaderMapUserData(native_vertex_regs.es_regs.data_addr,
|
||||
shaders.SetEsShaderBase(reinterpret_cast<uint64_t>(waterfall_vertex.data()));
|
||||
ShaderMapUserData(shaders.GetVs().es_regs.data_addr,
|
||||
{.type = Prospero::ShaderBinaryType::kGs, .user_data = &native_user_data,
|
||||
.code_size_bytes = static_cast<u32>(waterfall_vertex.size() * sizeof(u32))});
|
||||
registers.SetPsInControl(0x8008);
|
||||
@@ -18787,12 +18830,7 @@ public:
|
||||
for (const u32 wave : {32u, 64u}) {
|
||||
registers.SetShaderStages((saved_stages & ~0x00400000u) |
|
||||
(wave == 32u ? 0x00400000u : 0u));
|
||||
const auto programs = context.GetPipelineCache().GetGraphicsPrograms(
|
||||
native_vertex_regs, native_pixel_regs, registers.GetShaderRegisters(),
|
||||
registers, user_config, export_mapping, true, native_vertex_info, pixel);
|
||||
vertex_shader = programs.vertex[0];
|
||||
pixel_shader = programs.pixel;
|
||||
vertex = native_vertex_info[0];
|
||||
native_programs();
|
||||
Require(name, "native NGG waterfall wave",
|
||||
vertex.stage.program->stage == ShaderType::Vertex &&
|
||||
vertex.stage.program->wave_size == wave,
|
||||
@@ -18807,7 +18845,7 @@ public:
|
||||
}
|
||||
}
|
||||
registers.SetShaderStages(saved_stages);
|
||||
native_vertex_regs.es_regs.data_addr = vertex_address;
|
||||
shaders.SetEsShaderBase(vertex_address);
|
||||
|
||||
// Reversed RGBA targets must use logical Sa for RGB, with a separate
|
||||
// zero or unit source factor for separate alpha attenuation or accumulation.
|
||||
@@ -18822,8 +18860,8 @@ public:
|
||||
AppendEnd(&code);
|
||||
return code;
|
||||
}();
|
||||
native_pixel_regs.ps_regs.data_addr = reinterpret_cast<uint64_t>(blend_pixel.data());
|
||||
ShaderMapUserData(native_pixel_regs.ps_regs.data_addr,
|
||||
shaders.SetPsShaderBase(reinterpret_cast<uint64_t>(blend_pixel.data()));
|
||||
ShaderMapUserData(shaders.GetPs().ps_regs.data_addr,
|
||||
{.type = Prospero::ShaderBinaryType::kPs,
|
||||
.code_size_bytes = static_cast<uint32_t>(blend_pixel.size() * sizeof(u32))});
|
||||
const auto saved_blend = registers.GetBlendControl(0);
|
||||
@@ -18832,8 +18870,13 @@ public:
|
||||
registers.SetTargetOutputMode(0, 4);
|
||||
auto blend_target_info = saved_target_info;
|
||||
blend_target_info.blend_bypass = false;
|
||||
// A reversed RGBA32F target exports through the ABGR mapping.
|
||||
blend_target_info.format = Prospero::ChannelLayout::k32_32_32_32;
|
||||
blend_target_info.channel_type = Prospero::ChannelType::kFloat;
|
||||
blend_target_info.channel_order = Prospero::ChannelOrder::kReversed;
|
||||
registers.SetColorInfo(0, blend_target_info);
|
||||
export_mapping[0] = color.export_mapping = Prospero::ColorMappingAbgr;
|
||||
registers.SetColorBase(0, {.addr = depth_address + 0x10000});
|
||||
color.export_mapping = Prospero::ColorMappingAbgr;
|
||||
HW::BlendControl alpha_blend{};
|
||||
alpha_blend.enable = true;
|
||||
alpha_blend.color_srcblend = static_cast<uint8_t>(Prospero::BlendFactor::kSrcAlpha);
|
||||
@@ -18856,22 +18899,18 @@ public:
|
||||
alpha_blend.separate_alpha_blend = test.separate;
|
||||
alpha_blend.alpha_srcblend = static_cast<uint8_t>(test.alpha_source);
|
||||
registers.SetBlendControl(0, alpha_blend);
|
||||
const auto programs = context.GetPipelineCache().GetGraphicsPrograms(
|
||||
native_vertex_regs, native_pixel_regs, registers.GetShaderRegisters(),
|
||||
registers, user_config, export_mapping, true, native_vertex_info, pixel);
|
||||
native_programs();
|
||||
Require(name, "logical alpha blend program", pixel.alpha_blend_source == test.mode &&
|
||||
pixel.dual_source_blending,
|
||||
pixel.dual_source_blending &&
|
||||
pixel.target_export_mapping[0] == Prospero::ColorMappingAbgr,
|
||||
"the separate alpha equation did not reach the compiled pixel program");
|
||||
vertex_shader = programs.vertex[0];
|
||||
pixel_shader = programs.pixel;
|
||||
vertex = native_vertex_info[0];
|
||||
auto &selected = pipeline(true, 2, 2);
|
||||
if (test.mode == ShaderAlphaBlendSource::SourceAlpha) {
|
||||
Require(name, "equal blend equation cache reuse",
|
||||
broadcast_program == 0 || (programs.pixel.id == broadcast_program &&
|
||||
broadcast_program == 0 || (pixel_shader.id == broadcast_program &&
|
||||
selected.pipeline == broadcast_pipeline),
|
||||
"equivalent separate alpha state created another shader or pipeline");
|
||||
broadcast_program = programs.pixel.id;
|
||||
broadcast_program = pixel_shader.id;
|
||||
broadcast_pipeline = selected.pipeline;
|
||||
}
|
||||
constexpr std::array<float, 4> destination{0.3f, 0.3f, 0.1f, 0.2f};
|
||||
@@ -18885,9 +18924,10 @@ public:
|
||||
"reversed blending changed logical RGB or the separate alpha equation");
|
||||
}
|
||||
}
|
||||
native_pixel_regs.ps_regs.data_addr = pixel_address;
|
||||
export_mapping[0] = color.export_mapping = {};
|
||||
shaders.SetPsShaderBase(pixel_address);
|
||||
color.export_mapping = {};
|
||||
registers.SetBlendControl(0, saved_blend);
|
||||
registers.SetColorBase(0, {});
|
||||
registers.SetColorInfo(0, saved_target_info);
|
||||
registers.SetTargetOutputMode(0, saved_output_mode);
|
||||
|
||||
@@ -18926,17 +18966,12 @@ public:
|
||||
user_config.SetPrimitiveType(Prospero::PrimitiveType::kTriStrip);
|
||||
for (size_t i = 0; i < position_shaders.size(); i++) {
|
||||
const auto &code = position_shaders[i];
|
||||
native_vertex_regs.es_regs.data_addr = reinterpret_cast<uint64_t>(code.data());
|
||||
ShaderMapUserData(native_vertex_regs.es_regs.data_addr,
|
||||
shaders.SetEsShaderBase(reinterpret_cast<uint64_t>(code.data()));
|
||||
ShaderMapUserData(shaders.GetVs().es_regs.data_addr,
|
||||
{.type = Prospero::ShaderBinaryType::kGs,
|
||||
.user_data = &native_user_data,
|
||||
.code_size_bytes = static_cast<uint32_t>(code.size() * sizeof(u32))});
|
||||
const auto programs = context.GetPipelineCache().GetGraphicsPrograms(
|
||||
native_vertex_regs, native_pixel_regs, registers.GetShaderRegisters(),
|
||||
registers, user_config, export_mapping, true, native_vertex_info, pixel);
|
||||
vertex_shader = programs.vertex[0];
|
||||
pixel_shader = programs.pixel;
|
||||
vertex = native_vertex_info[0];
|
||||
native_programs();
|
||||
draw(pipeline(true, 2, 2, false, false, vk::PrimitiveTopology::eTriangleStrip), 4);
|
||||
const auto pixels = read_color();
|
||||
bool second_triangle = false;
|
||||
@@ -19473,11 +19508,7 @@ public:
|
||||
shaders.SetGsShaderResource2({});
|
||||
shaders.SetPsShaderBase(ps_address);
|
||||
user_config.SetPrimitiveType(Prospero::PrimitiveType::kTriList);
|
||||
native_vertex_regs.es_regs.data_addr = vertex_address;
|
||||
native_pixel_regs.ps_regs.data_addr = ps_address;
|
||||
const auto programs = context.GetPipelineCache().GetGraphicsPrograms(
|
||||
native_vertex_regs, native_pixel_regs, registers.GetShaderRegisters(),
|
||||
registers, user_config, export_mapping, true, native_vertex_info, pixel);
|
||||
native_programs();
|
||||
Require(name, "packed format follows export ordinal",
|
||||
pixel.target_output_mode[2] == 9 && pixel.wave_size == test.wave &&
|
||||
pixel.target_packed_unorm_mask == 4u &&
|
||||
@@ -19485,9 +19516,9 @@ public:
|
||||
"packed conversion or rounding contaminated another MRT or followed a physical slot");
|
||||
if (test.slot == 2) {
|
||||
auto &id = test.truncate ? truncate_program : half_up_program;
|
||||
Require(name, "packed rounding program reuse", id == 0 || id == programs.pixel.id,
|
||||
Require(name, "packed rounding program reuse", id == 0 || id == pixel_shader.id,
|
||||
"restoring half-up rounding failed to reuse its program");
|
||||
id = programs.pixel.id;
|
||||
id = pixel_shader.id;
|
||||
}
|
||||
RenderColorInfo packed_target{}, float_target{};
|
||||
RenderExecutorTestAccess::ResolveRenderColorTarget(
|
||||
@@ -39482,25 +39513,25 @@ void CheckRuntimeBufferRecords(VulkanHarness &vulkan) {
|
||||
options.input_info.compute = &test.compute_info;
|
||||
auto translated = ShaderRecompiler::TranslateProgram(test.code, options);
|
||||
auto plan = ExtractResourcePlan(translated.program);
|
||||
struct CountMemory { u32 value = 72; u32 reads = 0; bool clean = true; } memory;
|
||||
SrtRuntime runtime{.user_data = test.user_data, .userdata = &memory};
|
||||
struct CountMemory { u32 value = 72; u32 reads = 0; bool readable = true; } memory;
|
||||
ResourceSnapshot snapshot;
|
||||
ResourceSpecialization specialization;
|
||||
Require(test.name, "strict reader required",
|
||||
!MaterializeResources(plan, runtime, snapshot, specialization),
|
||||
"vector descriptor input bypassed the clean-memory reader");
|
||||
runtime.read_specialization_memory = [](void *data, uint64_t address, std::span<u32> words) {
|
||||
auto &memory = *static_cast<CountMemory *>(data);
|
||||
++memory.reads;
|
||||
if (!memory.clean || address != count_address || words.size() != 1) return false;
|
||||
words[0] = memory.value;
|
||||
return true;
|
||||
const auto materialize = [&] {
|
||||
FakeGuestMemory fake(+[](void *data, uint64_t address, std::span<u32> words) {
|
||||
auto &memory = *static_cast<CountMemory *>(data);
|
||||
++memory.reads;
|
||||
if (!memory.readable || address != count_address) return false;
|
||||
words[0] = memory.value;
|
||||
return true;
|
||||
}, &memory);
|
||||
return MaterializeResources(plan, {.user_data = test.user_data}, snapshot, specialization);
|
||||
};
|
||||
memory.clean = false;
|
||||
Require(test.name, "dirty input rejected",
|
||||
!MaterializeResources(plan, runtime, snapshot, specialization),
|
||||
"GPU-dirty count was accepted for host descriptor evaluation");
|
||||
memory.clean = true;
|
||||
// An unreadable count reads as zero, which leaves no records.
|
||||
memory.readable = false;
|
||||
Require(test.name, "unreadable input",
|
||||
materialize() && memory.reads == 1 && snapshot.buffers[2].dwords[2] == 0,
|
||||
"unreadable count did not read as zero");
|
||||
memory.readable = true;
|
||||
ResourceSpecialization initial_specialization;
|
||||
// Reuse the same resource plan and compiled shader as the count and coverage change.
|
||||
for (const u32 count : {0u, 72u, 512u, 8192u, 262144u}) {
|
||||
@@ -39509,7 +39540,7 @@ void CheckRuntimeBufferRecords(VulkanHarness &vulkan) {
|
||||
const u32 chunk = count < 512 ? 64 : count < 8192 ? 128 : count < 262144 ? 256 : 512;
|
||||
const u32 records = (count + chunk - 1) / chunk;
|
||||
Require(test.name, "cached descriptor refresh",
|
||||
MaterializeResources(plan, runtime, snapshot, specialization) && memory.reads == 1 &&
|
||||
materialize() && memory.reads == 1 &&
|
||||
snapshot.buffers[2].dwords[2] == records &&
|
||||
snapshot.specialization_reads ==
|
||||
std::vector<std::pair<uint64_t, uint64_t>>{{count_address, 4}},
|
||||
@@ -44505,13 +44536,13 @@ void CheckComputeLdsLimit(VulkanHarness &vulkan) {
|
||||
uint16_t{112}, uint16_t{128}, uint16_t{128}}) {
|
||||
regs.cs_regs.lds_size = units;
|
||||
ShaderComputeInputInfo input{};
|
||||
const auto program = cache.GetComputeProgram(regs, {}, input);
|
||||
const auto program = cache.GetComputeProgram(regs, input);
|
||||
Require(name, "guest allocation and device backing",
|
||||
input.lds_size_dwords == units * 128u &&
|
||||
input.lds_storage == (units > limit_units),
|
||||
"compute LDS was truncated or chose backing that exceeds the device limit");
|
||||
Require(name, "pipeline creation",
|
||||
cache.GetComputePipeline(input, program).pipeline != nullptr,
|
||||
cache.GetComputePipeline(input).pipeline != nullptr,
|
||||
"full-size LDS shader did not create a Vulkan compute pipeline");
|
||||
if (previous.id != 0) {
|
||||
Require(name, "guest-size cache identity",
|
||||
@@ -44557,7 +44588,6 @@ void CheckPs5GameExampleImageClearRuntimeShape() {
|
||||
compute.thread_ids_num = 1;
|
||||
compute.workgroup_register = 8;
|
||||
u32 scalar_clear = 0x40404040u;
|
||||
bool clean_scalar = false;
|
||||
|
||||
const auto Compile = [&](const std::vector<u32> &code) {
|
||||
ShaderRecompiler::CompileOptions options;
|
||||
@@ -44575,13 +44605,6 @@ void CheckPs5GameExampleImageClearRuntimeShape() {
|
||||
const ShaderRecompiler::IR::SrtRuntime runtime{
|
||||
.user_data = options.user_data,
|
||||
.shader_base = reinterpret_cast<uint64_t>(code.data()),
|
||||
.userdata = &scalar_clear,
|
||||
.read_specialization_memory = clean_scalar
|
||||
? +[](void *data, uint64_t address, std::span<uint32_t> words) {
|
||||
if (address != reinterpret_cast<uint64_t>(data) || words.size() != 1u) return false;
|
||||
words[0] = *static_cast<uint32_t *>(data);
|
||||
return true;
|
||||
} : nullptr,
|
||||
};
|
||||
Require("Ps5GameExampleImageClear", "resource materialization",
|
||||
ShaderRecompiler::IR::MaterializeResources(
|
||||
@@ -44739,7 +44762,7 @@ void CheckPs5GameExampleImageClearRuntimeShape() {
|
||||
scalar_code.push_back(EncodeMubuf0(0x04u, 0, true, false));
|
||||
scalar_code.push_back(EncodeMubuf1(1, 0, 0));
|
||||
AppendEnd(&scalar_code);
|
||||
clean_scalar = true;
|
||||
// The scalar input is a walker load, so it needs no specialization memory reader.
|
||||
auto loaded = Compile(scalar_code);
|
||||
runtime_resources = loaded;
|
||||
Require("Ps5GameExampleImageClear", "scalar-loaded fill",
|
||||
@@ -44747,12 +44770,6 @@ void CheckPs5GameExampleImageClearRuntimeShape() {
|
||||
packed_clear, size) &&
|
||||
packed_clear == scalar_clear && descriptor.Base48() == 0x200000u,
|
||||
"GTA3's scalar input was confused with its destination");
|
||||
clean_scalar = false;
|
||||
runtime_resources = Compile(scalar_code);
|
||||
Require("Ps5GameExampleImageClear", "unavailable clean scalar",
|
||||
!ResolveComputeBufferFill(compute, 1, 1, 1, 0x41u, descriptor,
|
||||
packed_clear, size),
|
||||
"a scalar without a clean reader was replaced by a fill");
|
||||
std::printf("[host] %-32s ok\n", "Ps5GameExampleImageClear");
|
||||
}
|
||||
|
||||
@@ -47219,8 +47236,9 @@ void CheckImageSamplerSpecialization() {
|
||||
auto plan = ExtractResourcePlan(translated.program);
|
||||
ResourceSnapshot snapshot;
|
||||
ResourceSpecialization specialization;
|
||||
const SrtRuntime runtime{.user_data = user_data, .read_memory = ReadTestMemory,
|
||||
.userdata = &memory, .read_specialization_memory = ReadTestMemory};
|
||||
const SrtRuntime runtime{.user_data = user_data, .userdata = &memory,
|
||||
.read_specialization_memory = ReadTestMemory};
|
||||
FakeGuestMemory fake(ReadTestMemory, &memory);
|
||||
Require(name, "bounded native material plan",
|
||||
plan.capture_specialization_reads && !translated.program.has_address_writes &&
|
||||
MaterializeResources(plan, runtime, snapshot, specialization) &&
|
||||
@@ -47310,10 +47328,8 @@ void CheckResourcePlanHandoff() {
|
||||
auto plan = ExtractResourcePlan(translated.program);
|
||||
ResourceSnapshot snapshot;
|
||||
ResourceSpecialization specialization;
|
||||
const SrtRuntime runtime{.user_data = user_data,
|
||||
.read_memory = ReadTestMemory,
|
||||
.userdata = &memory,
|
||||
.read_specialization_memory = ReadTestMemory};
|
||||
const SrtRuntime runtime{.user_data = user_data};
|
||||
FakeGuestMemory fake(ReadTestMemory, &memory);
|
||||
Require(name, "initial descriptors",
|
||||
MaterializeResources(plan, runtime, snapshot, specialization) &&
|
||||
snapshot.buffers.size() == 1 && snapshot.buffers[0].dwords[0] == 0x1000u,
|
||||
@@ -48106,37 +48122,6 @@ void CheckDepthTargetFootprints() {
|
||||
std::printf("[host] %-32s ok\n", "DepthTargetFootprints");
|
||||
}
|
||||
|
||||
struct SlotLifetimeProbe {
|
||||
explicit SlotLifetimeProbe(bool *destroyed) : destroyed(destroyed) {
|
||||
if (destroyed == nullptr || *destroyed) {
|
||||
EXIT("fence-lifetime probe has invalid construction state\n");
|
||||
}
|
||||
}
|
||||
~SlotLifetimeProbe() { *destroyed = true; }
|
||||
|
||||
bool *destroyed = nullptr;
|
||||
};
|
||||
|
||||
void CheckSlotVectorLifetime() {
|
||||
bool destroyed = false;
|
||||
Common::SlotVector<SlotLifetimeProbe> slots;
|
||||
const auto first = slots.insert(&destroyed);
|
||||
Require("SlotVectorLifetime", "inserted",
|
||||
slots.is_allocated(first) && slots.try_get(first) != nullptr &&
|
||||
!destroyed,
|
||||
"slot insertion did not expose its cache-owned object");
|
||||
slots.erase(first);
|
||||
const bool first_destroyed = destroyed;
|
||||
destroyed = false;
|
||||
const auto second = slots.insert(&destroyed);
|
||||
Require("SlotVectorLifetime", "generation",
|
||||
first_destroyed && !destroyed && first.index == second.index &&
|
||||
first != second && !slots.is_allocated(first) &&
|
||||
slots.is_allocated(second),
|
||||
"reused slot accepted a stale cache resource id");
|
||||
std::printf("[host] %-32s ok\n", "SlotVectorLifetime");
|
||||
}
|
||||
|
||||
void CheckEmbeddedFetchLaneSpill() {
|
||||
std::vector<u32> code;
|
||||
code.push_back(EncodeSMovB32(0, InlineU32(0)));
|
||||
@@ -51273,7 +51258,6 @@ int main(int argc, char **argv) {
|
||||
CheckDepthFeedbackAspects();
|
||||
CheckDynamicRenderingState();
|
||||
CheckDepthTargetFootprints();
|
||||
CheckSlotVectorLifetime();
|
||||
#else
|
||||
if (argc != 1) {
|
||||
std::fprintf(stderr, "unknown test selector: %s\n", argv[1]);
|
||||
|
||||
@@ -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);
|
||||
|
||||
+114
-149
@@ -24,13 +24,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 +135,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 +143,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(
|
||||
@@ -1588,11 +1585,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 +1906,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 +5407,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 +9223,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 +9283,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 +9359,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 +9405,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 +9507,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 +9615,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 +9700,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 +10817,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 +10862,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 +10873,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 +10964,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 +10987,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 +11136,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 +11284,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 +13822,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 +13839,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 +14487,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 +14552,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 +14589,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 +14603,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 +14803,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 +14829,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 +14891,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 +14943,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 +15022,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 +15030,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 +15065,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 +15103,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 +15185,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 +15254,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 +15296,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 +15323,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 +15351,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 +15376,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 +15411,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 +15788,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 +15859,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);
|
||||
@@ -16007,7 +15972,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 +15981,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 +15991,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