mirror of
https://github.com/boykopovar/AnyPS5.git
synced 2026-10-22 16:43:05 -05:00
fix(graphics): bypass vertex layout cache churn
Preserve current null-descriptor and format-component semantics, keep live validation per draw, and leave empty or bypassed layouts on the value-owned path. Add differential, concurrency, and isolated timing coverage. Written with AI assistance.
This commit is contained in:
1 parent
1efbb87145
commit
02e58e9936
4 files changed
+238
-78
No files matched your search
@@ -78,7 +78,7 @@ struct DrawRecipe {
|
||||
std::vector<std::weak_ptr<Graphics::StorageTexture>> targets;
|
||||
std::vector<VkImageView> targetViews;
|
||||
std::uint64_t passKey = 0;
|
||||
std::shared_ptr<const Graphics::VertexInputLayout> vertexInput;
|
||||
Graphics::VertexInputLayout vertexInput;
|
||||
std::array<std::byte, Graphics::PipelinePushConstantBytes> pushBytes{};
|
||||
VkShaderStageFlags pushStages = 0;
|
||||
std::optional<Graphics::State> masked;
|
||||
|
||||
@@ -3,12 +3,13 @@
|
||||
|
||||
#include "prx/libSceAgcDriver/Graphics/include/Shaders.hpp"
|
||||
#include <algorithm>
|
||||
#include <atomic>
|
||||
#include <list>
|
||||
#include <mutex>
|
||||
#include <unordered_map>
|
||||
#include <cstring>
|
||||
#include <limits>
|
||||
#include <optional>
|
||||
#include <list>
|
||||
#include <unordered_map>
|
||||
#include <mutex>
|
||||
#include <set>
|
||||
#include <span>
|
||||
#include <utility>
|
||||
@@ -108,18 +109,8 @@ inline std::string VertexAttributeSignature(const ShaderRecompiler::VertexAttrib
|
||||
struct VertexInputLayout {
|
||||
std::vector<VkVertexInputBindingDescription> bindings;
|
||||
std::vector<VkVertexInputAttributeDescription> attributes;
|
||||
std::vector<std::uint32_t> alignments;
|
||||
};
|
||||
|
||||
inline void ValidateVertexAddresses(std::span<const ShaderRecompiler::VertexAttribute> attributes, const VertexInputLayout& layout) {
|
||||
Require(attributes.size() == layout.alignments.size(), "vertex input layout count mismatch");
|
||||
for (std::size_t i = 0; i < attributes.size(); ++i) {
|
||||
const auto& fields = attributes[i].resource.fields;
|
||||
const auto address = fields[0] | (static_cast<std::uint64_t>(fields[1] & 0xffffu) << 32u);
|
||||
Require(address != 0 && address % layout.alignments[i] == 0, "unaligned vertex buffer");
|
||||
}
|
||||
}
|
||||
|
||||
inline VertexInputLayout BuildVertexInputLayout(const Context& context, std::span<const ShaderRecompiler::VertexAttribute> attributes) {
|
||||
Require(attributes.size() <= context.limits.maxVertexInputBindings && attributes.size() <= context.limits.maxVertexInputAttributes, "vertex input count exceeds device limits");
|
||||
VertexInputLayout result;
|
||||
@@ -141,52 +132,28 @@ inline VertexInputLayout BuildVertexInputLayout(const Context& context, std::spa
|
||||
const auto binding = static_cast<std::uint32_t>(result.bindings.size());
|
||||
result.bindings.push_back({binding, stride, attribute.fetchIndex == 0 ? VK_VERTEX_INPUT_RATE_VERTEX : VK_VERTEX_INPUT_RATE_INSTANCE});
|
||||
result.attributes.push_back({attribute.location, binding, format.format, 0});
|
||||
result.alignments.push_back(format.alignment);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
inline void ValidateVertexAddresses(std::span<const ShaderRecompiler::VertexAttribute> attributes) {
|
||||
for (const auto& attribute : attributes) {
|
||||
if (NullVertexDescriptor(attribute)) continue;
|
||||
const auto& fields = attribute.resource.fields;
|
||||
const auto address = fields[0] | (static_cast<std::uint64_t>(fields[1] & 0xffffu) << 32u);
|
||||
if (address == 0 || address % DecodeVertexFormat(attribute).alignment != 0) Require(false, "unaligned vertex buffer");
|
||||
}
|
||||
}
|
||||
|
||||
class VertexInputCache {
|
||||
public:
|
||||
explicit VertexInputCache(const Context& context, std::size_t capacity = 1024) : context(context), capacity(capacity) {
|
||||
Require(capacity != 0, "vertex input cache capacity is zero");
|
||||
}
|
||||
|
||||
std::shared_ptr<const VertexInputLayout> Get(std::uint64_t variant, std::span<const ShaderRecompiler::VertexAttribute> attributes) {
|
||||
if (attributes.empty()) return empty;
|
||||
if (variant == 0) return std::make_shared<const VertexInputLayout>(BuildVertexInputLayout(context, attributes));
|
||||
std::lock_guard lock(mutex);
|
||||
const auto found = entries.find(variant);
|
||||
if (found != entries.end() && sameShape(attributes, found->second.shape)) {
|
||||
ValidateVertexAddresses(attributes, *found->second.layout);
|
||||
order.splice(order.begin(), order, found->second.order);
|
||||
return found->second.layout;
|
||||
}
|
||||
auto layout = std::make_shared<const VertexInputLayout>(BuildVertexInputLayout(context, attributes));
|
||||
std::vector<ShaderRecompiler::VertexAttribute> shape(attributes.begin(), attributes.end());
|
||||
for (auto& attribute : shape) {
|
||||
attribute.resource.fields[0] = 0;
|
||||
attribute.resource.fields[1] &= 0xffff0000u;
|
||||
attribute.resource.fields[2] = 0;
|
||||
}
|
||||
if (found != entries.end()) {
|
||||
found->second.shape = std::move(shape);
|
||||
found->second.layout = layout;
|
||||
order.splice(order.begin(), order, found->second.order);
|
||||
return layout;
|
||||
}
|
||||
order.push_front(variant);
|
||||
try {
|
||||
entries.emplace(variant, Entry{layout, std::move(shape), order.begin()});
|
||||
} catch (...) {
|
||||
order.pop_front();
|
||||
throw;
|
||||
}
|
||||
if (entries.size() > capacity) {
|
||||
entries.erase(order.back());
|
||||
order.pop_back();
|
||||
}
|
||||
return layout;
|
||||
std::shared_ptr<const VertexInputLayout> TryGet(std::uint64_t variant, std::span<const ShaderRecompiler::VertexAttribute> attributes) {
|
||||
if (attributes.empty() || variant == 0 || bypassed(variant)) return nullptr;
|
||||
return get(variant, attributes);
|
||||
}
|
||||
|
||||
std::size_t Size() const {
|
||||
@@ -195,12 +162,55 @@ public:
|
||||
}
|
||||
|
||||
private:
|
||||
std::shared_ptr<const VertexInputLayout> get(std::uint64_t variant, std::span<const ShaderRecompiler::VertexAttribute> attributes) {
|
||||
std::lock_guard lock(mutex);
|
||||
const auto found = entries.find(variant);
|
||||
if (found != entries.end()) {
|
||||
if (!sameShape(attributes, found->second.shape)) {
|
||||
bypass(variant);
|
||||
order.erase(found->second.order);
|
||||
entries.erase(found);
|
||||
return nullptr;
|
||||
}
|
||||
ValidateVertexAddresses(attributes);
|
||||
order.splice(order.begin(), order, found->second.order);
|
||||
return found->second.layout;
|
||||
}
|
||||
if (bypassed(variant)) return nullptr;
|
||||
auto layout = std::make_shared<const VertexInputLayout>(BuildVertexInputLayout(context, attributes));
|
||||
std::vector<ShaderRecompiler::VertexAttribute> shape(attributes.begin(), attributes.end());
|
||||
order.push_front(variant);
|
||||
try {
|
||||
entries.emplace(variant, Entry{layout, std::move(shape), order.begin()});
|
||||
} catch (...) {
|
||||
order.pop_front();
|
||||
throw;
|
||||
}
|
||||
if (entries.size() > capacity) {
|
||||
bypass(order.back());
|
||||
entries.erase(order.back());
|
||||
order.pop_back();
|
||||
}
|
||||
return layout;
|
||||
}
|
||||
|
||||
static bool sameShape(std::span<const ShaderRecompiler::VertexAttribute> a, std::span<const ShaderRecompiler::VertexAttribute> b) {
|
||||
return std::equal(a.begin(), a.end(), b.begin(), b.end(), [](const auto& x, const auto& y) {
|
||||
return x.location == y.location && x.components == y.components && x.fetchIndex == y.fetchIndex &&
|
||||
(x.resource.fields[1] & 0xffff0000u) == y.resource.fields[1] && x.resource.fields[3] == y.resource.fields[3];
|
||||
return x.location == y.location && x.components == y.components && x.formatComponents == y.formatComponents && x.fetchIndex == y.fetchIndex &&
|
||||
(x.resource.fields[1] & 0xffff0000u) == (y.resource.fields[1] & 0xffff0000u) && x.resource.fields[3] == y.resource.fields[3] && NullVertexDescriptor(x) == NullVertexDescriptor(y);
|
||||
});
|
||||
}
|
||||
static std::size_t bypassIndex(std::uint64_t variant) {
|
||||
return static_cast<std::size_t>((variant ^ (variant >> 32u)) & 65535u);
|
||||
}
|
||||
bool bypassed(std::uint64_t variant) const {
|
||||
const auto index = bypassIndex(variant);
|
||||
return (bypassBits[index / 64u].load(std::memory_order_relaxed) & (std::uint64_t{1} << (index % 64u))) != 0;
|
||||
}
|
||||
void bypass(std::uint64_t variant) {
|
||||
const auto index = bypassIndex(variant);
|
||||
bypassBits[index / 64u].fetch_or(std::uint64_t{1} << (index % 64u), std::memory_order_relaxed);
|
||||
}
|
||||
struct Entry {
|
||||
std::shared_ptr<const VertexInputLayout> layout;
|
||||
std::vector<ShaderRecompiler::VertexAttribute> shape;
|
||||
@@ -208,7 +218,7 @@ private:
|
||||
};
|
||||
Context context;
|
||||
std::size_t capacity;
|
||||
const std::shared_ptr<const VertexInputLayout> empty = std::make_shared<const VertexInputLayout>();
|
||||
std::array<std::atomic<std::uint64_t>, 1024> bypassBits{};
|
||||
mutable std::mutex mutex;
|
||||
std::list<std::uint64_t> order;
|
||||
std::unordered_map<std::uint64_t, Entry> entries;
|
||||
|
||||
@@ -970,7 +970,8 @@ struct DrawInputs {
|
||||
VkBuffer indexHandle = VK_NULL_HANDLE;
|
||||
VkDeviceSize indexOffset = 0;
|
||||
std::uint32_t maxIndex = 0;
|
||||
std::shared_ptr<const VertexInputLayout> vertexInput;
|
||||
VertexInputLayout vertexInput;
|
||||
std::shared_ptr<const VertexInputLayout> retainedVertexInput;
|
||||
std::vector<std::shared_ptr<Buffer>> vertexBuffers;
|
||||
std::vector<VkBuffer> vertexHandles;
|
||||
std::vector<VkDeviceSize> vertexOffsets;
|
||||
@@ -979,6 +980,7 @@ struct DrawInputs {
|
||||
VkPipelineStageFlags shaderStages = 0;
|
||||
std::uint32_t meshGroups = 0;
|
||||
std::vector<std::uint32_t> restartTable;
|
||||
const VertexInputLayout& VertexLayout() const { return retainedVertexInput != nullptr ? *retainedVertexInput : vertexInput; }
|
||||
};
|
||||
|
||||
std::shared_ptr<Buffer> zeroVertexBuffer(const Context& context, const ShaderRecompiler::VertexAttribute& attribute) {
|
||||
@@ -1119,11 +1121,15 @@ DrawInputs prepareDrawInputs(const Context& context, const State& state, const P
|
||||
APS5_LOG_CHARS_OUT_DEBUG("Index validation OK");
|
||||
const auto& attributes = shaders.front().program->vertexAttributes;
|
||||
// Validates the vertex descriptors; the layout also keys and builds the pipeline.
|
||||
if (recipe != nullptr) {
|
||||
inputs.vertexInput = recipe->vertexInput;
|
||||
ValidateVertexAddresses(attributes, *inputs.vertexInput);
|
||||
} else if (context.vertexInputs != nullptr) inputs.vertexInput = context.vertexInputs->Get(shaders.front().program->variantId, attributes);
|
||||
else inputs.vertexInput = std::make_shared<const VertexInputLayout>(BuildVertexInputLayout(context, attributes));
|
||||
if (!attributes.empty()) {
|
||||
if (recipe != nullptr) {
|
||||
inputs.vertexInput = recipe->vertexInput;
|
||||
ValidateVertexAddresses(attributes);
|
||||
} else {
|
||||
if (context.vertexInputs != nullptr) inputs.retainedVertexInput = context.vertexInputs->TryGet(shaders.front().program->variantId, attributes);
|
||||
if (inputs.retainedVertexInput == nullptr) inputs.vertexInput = BuildVertexInputLayout(context, attributes);
|
||||
}
|
||||
}
|
||||
inputs.vertexOffsets.assign(attributes.size(), 0);
|
||||
// An indexed draw's vertex offset moves every fetch: the copy must reach the last one.
|
||||
if (draw.indexed) {
|
||||
@@ -2122,7 +2128,7 @@ void Draw(const Context& context, const State& state, const Pm4::DrawParameters&
|
||||
const auto built = resources->SetLayouts();
|
||||
Require(described.layouts.main == built.main && described.layouts.fragment == built.fragment && described.layouts.split == built.split && std::ranges::equal(described.mainKey, built.mainKey) && std::ranges::equal(described.fragmentKey, built.fragmentKey), "set layouts described from the shaders differ from the draw's");
|
||||
}
|
||||
auto pipeline = CachedPipeline(context, pipelineState, *inputs.vertexInput, *resources, shaders, lean ? VK_IMAGE_LAYOUT_GENERAL : VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL);
|
||||
auto pipeline = CachedPipeline(context, pipelineState, inputs.VertexLayout(), *resources, shaders, lean ? VK_IMAGE_LAYOUT_GENERAL : VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL);
|
||||
// Resident targets' views are stable while their storage image lives, so the framebuffer is
|
||||
// reused with the pipeline; a per-draw RenderTarget gets a framebuffer of its own.
|
||||
std::vector<std::shared_ptr<StorageTexture>> owners;
|
||||
@@ -2168,7 +2174,7 @@ void Draw(const Context& context, const State& state, const Pm4::DrawParameters&
|
||||
for (const auto& owner : owners) recipe->targets.emplace_back(owner);
|
||||
recipe->targetViews = targetViews;
|
||||
recipe->passKey = DrawRenderPassKey(context, state, targetViews);
|
||||
recipe->vertexInput = inputs.vertexInput;
|
||||
recipe->vertexInput = inputs.VertexLayout();
|
||||
recipe->pushStages = PushConstantStages(shaders);
|
||||
if (recipe->pushStages != 0) {
|
||||
recipe->pushBytes = AssemblePushConstants(shaders);
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
#include "prx/libSceAgcDriver/Graphics/include/VertexInput.hpp"
|
||||
#include <atomic>
|
||||
#include <barrier>
|
||||
#include <chrono>
|
||||
#include <ctime>
|
||||
#include <string_view>
|
||||
#include <iostream>
|
||||
#include <random>
|
||||
#include <thread>
|
||||
@@ -33,7 +36,7 @@ VertexAttribute attribute(std::uint32_t location = 0) {
|
||||
}
|
||||
|
||||
void same(const VertexInputLayout& a, const VertexInputLayout& b) {
|
||||
Require(a.bindings.size() == b.bindings.size() && a.attributes.size() == b.attributes.size() && a.alignments == b.alignments, "layout sizes differ");
|
||||
Require(a.bindings.size() == b.bindings.size() && a.attributes.size() == b.attributes.size(), "layout sizes differ");
|
||||
for (std::size_t i = 0; i < a.bindings.size(); ++i) {
|
||||
const auto& x = a.bindings[i];
|
||||
const auto& y = b.bindings[i];
|
||||
@@ -44,6 +47,11 @@ void same(const VertexInputLayout& a, const VertexInputLayout& b) {
|
||||
}
|
||||
}
|
||||
|
||||
std::shared_ptr<const VertexInputLayout> get(VertexInputCache& cache, const Context& ctx, std::uint64_t variant, std::span<const VertexAttribute> attributes) {
|
||||
auto result = cache.TryGet(variant, attributes);
|
||||
return result != nullptr ? result : std::make_shared<const VertexInputLayout>(BuildVertexInputLayout(ctx, attributes));
|
||||
}
|
||||
|
||||
template<class TAction> void fails(TAction&& fn) {
|
||||
bool failed = false;
|
||||
try { fn(); } catch (const std::runtime_error&) { failed = true; }
|
||||
@@ -54,19 +62,19 @@ void run() {
|
||||
auto ctx = context();
|
||||
VertexInputCache cache(ctx, 2);
|
||||
std::array attributes{attribute(0), attribute(1)};
|
||||
auto first = cache.Get(1, attributes);
|
||||
auto first = get(cache, ctx, 1, attributes);
|
||||
const auto count = queries.load();
|
||||
attributes[0].resource.fields[0] += 256;
|
||||
attributes[0].resource.fields[1] |= 0x12u;
|
||||
attributes[0].resource.fields[2] = 17;
|
||||
Require(cache.Get(1, attributes) == first && queries == count, "live addresses or counts rebuilt the layout");
|
||||
Require(get(cache, ctx, 1, attributes) == first && queries == count, "live addresses or counts rebuilt the layout");
|
||||
auto invalid = attributes;
|
||||
invalid[0].resource.fields[0] += 1;
|
||||
fails([&] { cache.Get(1, invalid); });
|
||||
fails([&] { get(cache, ctx, 1, invalid); });
|
||||
invalid = attributes;
|
||||
invalid[0].resource.fields[0] = 0;
|
||||
invalid[0].resource.fields[1] &= 0xffff0000u;
|
||||
fails([&] { cache.Get(1, invalid); });
|
||||
fails([&] { get(cache, ctx, 1, invalid); });
|
||||
for (unsigned field = 0; field < 8; ++field) {
|
||||
invalid = attributes;
|
||||
switch (field) {
|
||||
@@ -79,25 +87,26 @@ void run() {
|
||||
case 6: invalid[0].resource.fields[1] = 4096u << 16u; break;
|
||||
case 7: invalid[0].resource.fields[3] = 5u << 12u; break;
|
||||
}
|
||||
fails([&] { cache.Get(1, invalid); });
|
||||
fails([&] { get(cache, ctx, 1, invalid); });
|
||||
fails([&] { BuildVertexInputLayout(ctx, invalid); });
|
||||
}
|
||||
attributes[0].resource.fields[1] = 16u << 16u;
|
||||
auto second = cache.Get(2, attributes);
|
||||
auto second = get(cache, ctx, 2, attributes);
|
||||
Require(first != second, "stride change reused a layout");
|
||||
attributes[0].fetchIndex = 1;
|
||||
auto third = cache.Get(3, attributes);
|
||||
auto third = get(cache, ctx, 3, attributes);
|
||||
Require(third != second && cache.Size() == 2 && first->bindings[0].stride == 32, "eviction or retained lifetime failed");
|
||||
attributes[0] = attribute(0);
|
||||
Require(cache.Get(1, attributes) != first, "least recently used layout survived eviction");
|
||||
Require(cache.Get(0, attributes) != cache.Get(0, attributes), "unidentified variant was cached");
|
||||
Require(get(cache, ctx, 1, attributes) != first, "least recently used layout survived eviction");
|
||||
Require(get(cache, ctx, 0, attributes) != get(cache, ctx, 0, attributes), "unidentified variant was cached");
|
||||
VertexInputCache other(ctx);
|
||||
Require(other.Get(1, attributes) != cache.Get(1, attributes), "device caches shared ownership");
|
||||
Require(get(other, ctx, 1, attributes) != get(cache, ctx, 1, attributes), "device caches shared ownership");
|
||||
auto restricted = ctx;
|
||||
restricted.limits.maxVertexInputBindings = 1;
|
||||
VertexInputCache restrictedCache(restricted);
|
||||
fails([&] { restrictedCache.Get(1, attributes); });
|
||||
Require(cache.Get(1, {})->attributes.empty(), "empty layout failed");
|
||||
fails([&] { get(restrictedCache, restricted, 1, attributes); });
|
||||
Require(get(cache, ctx, 1, {})->attributes.empty(), "empty layout failed");
|
||||
VertexInputCache generated(ctx, 16);
|
||||
std::mt19937 random(719);
|
||||
for (unsigned i = 0; i < 10000; ++i) {
|
||||
std::vector<VertexAttribute> input;
|
||||
@@ -114,9 +123,73 @@ void run() {
|
||||
input.push_back(a);
|
||||
}
|
||||
const auto expected = BuildVertexInputLayout(ctx, input);
|
||||
same(*cache.Get(1 + i % 16, input), expected);
|
||||
same(*get(generated, ctx, 1 + i, input), expected);
|
||||
for (auto& a : input) a.resource.fields[0] += 256;
|
||||
same(*cache.Get(1 + i % 16, input), BuildVertexInputLayout(ctx, input));
|
||||
same(*get(generated, ctx, 1 + i, input), BuildVertexInputLayout(ctx, input));
|
||||
}
|
||||
{
|
||||
VertexInputCache live(ctx);
|
||||
std::array input{attribute()};
|
||||
const auto stored = live.TryGet(100, input);
|
||||
for (const auto records : {0u, 1u, 128u}) {
|
||||
input[0].resource.fields[2] = records;
|
||||
Require(live.TryGet(100, input) == stored, "record count rebuilt a vertex layout");
|
||||
Require(VertexBufferReadSize(input[0], 5, 1) == 176u, "record count changed the direct fetch extent");
|
||||
if (records != 0) Require(VertexBufferExtent(input[0]) == 32u * records, "cached layout froze the indirect descriptor extent");
|
||||
else fails([&] { VertexBufferExtent(input[0]); });
|
||||
}
|
||||
input[0].resource.fields[1] = 0;
|
||||
input[0].resource.fields[3] |= 2u << 28u;
|
||||
input[0].resource.fields[2] = 0;
|
||||
const auto zeroStride = live.TryGet(101, input);
|
||||
Require(VertexFetchOutOfRange(input[0]), "zero records did not select zero fill");
|
||||
input[0].resource.fields[2] = 64;
|
||||
Require(live.TryGet(101, input) == zeroStride && !VertexFetchOutOfRange(input[0]) && VertexBufferExtent(input[0]) == 64, "cached zero-stride layout froze the live zero-fill decision");
|
||||
input[0].resource.fields[2] = 0;
|
||||
Require(live.TryGet(101, input) == zeroStride && VertexFetchOutOfRange(input[0]), "record count zero reused a live fetch");
|
||||
input[0] = attribute();
|
||||
input[0].formatComponents = 1;
|
||||
const auto narrow = live.TryGet(102, input);
|
||||
input[0].formatComponents = 2;
|
||||
const auto wide = get(live, ctx, 102, input);
|
||||
Require(narrow->attributes[0].format == VK_FORMAT_R32_SFLOAT && wide->attributes[0].format == VK_FORMAT_R32G32_SFLOAT, "format component count reused the wrong vertex layout");
|
||||
input[0] = {};
|
||||
input[0].components = 4;
|
||||
input[0].formatComponents = 1;
|
||||
const auto nullLayout = live.TryGet(103, input);
|
||||
Require(live.TryGet(103, input) == nullLayout && nullLayout->attributes[0].format == VK_FORMAT_R32G32B32A32_SFLOAT, "null vertex descriptor was rejected or narrowed on reuse");
|
||||
input[0].resource.fields[2] = 1;
|
||||
fails([&] { get(live, ctx, 103, input); });
|
||||
input[0] = attribute();
|
||||
input[0].resource.fields[3] |= 2u << 28u;
|
||||
input[0].resource.fields[2] = 6;
|
||||
const auto bounded = live.TryGet(104, input);
|
||||
Require(VertexBufferReadSize(input[0], 5, 1) == 176, "bounded vertex fetch failed");
|
||||
input[0].resource.fields[2] = 5;
|
||||
Require(live.TryGet(104, input) == bounded, "bounded record count rebuilt the layout");
|
||||
fails([&] { VertexBufferReadSize(input[0], 5, 1); });
|
||||
}
|
||||
{
|
||||
VertexInputCache thrashing(ctx, 2);
|
||||
std::array input{attribute()};
|
||||
const auto kept = thrashing.TryGet(1, input);
|
||||
thrashing.TryGet(2, input);
|
||||
thrashing.TryGet(3, input);
|
||||
const auto queryCount = queries.load();
|
||||
for (unsigned i = 0; i < 1000; ++i) Require(thrashing.TryGet(1, input) == nullptr, "an evicted variant kept re-entering the cache");
|
||||
Require(queries == queryCount && thrashing.Size() == 2 && kept->bindings[0].stride == 32, "bypassed variant rebuilt or invalidated a retained layout");
|
||||
Require(thrashing.TryGet(65537, input) == nullptr, "bypass hash collision was not conservative");
|
||||
same(*get(thrashing, ctx, 65537, input), BuildVertexInputLayout(ctx, input));
|
||||
input[0].resource.fields[0] = 1;
|
||||
fails([&] { get(thrashing, ctx, 1, input); });
|
||||
Require(thrashing.TryGet(0, input) == nullptr && thrashing.TryGet(2, {}) == nullptr, "empty or unidentified layout entered the cache");
|
||||
input[0] = attribute();
|
||||
input[0].resource.fields[1] = 16u << 16u;
|
||||
Require(thrashing.TryGet(2, input) == nullptr, "a changing variant shape stayed cached");
|
||||
const auto afterChange = queries.load();
|
||||
input[0].resource.fields[1] = 32u << 16u;
|
||||
for (unsigned i = 0; i < 1000; ++i) Require(thrashing.TryGet(2, input) == nullptr, "a changing variant shape kept taking the cache path");
|
||||
Require(queries == afterChange, "changing shape rebuilt in the cache");
|
||||
}
|
||||
VertexInputCache concurrent(ctx);
|
||||
std::array<std::shared_ptr<const VertexInputLayout>, 8> results;
|
||||
@@ -124,16 +197,87 @@ void run() {
|
||||
std::vector<std::thread> threads;
|
||||
for (unsigned i = 0; i < results.size(); ++i) threads.emplace_back([&, i] {
|
||||
ready.arrive_and_wait();
|
||||
results[i] = concurrent.Get(1, attributes);
|
||||
results[i] = concurrent.TryGet(1, attributes);
|
||||
});
|
||||
for (auto& thread : threads) thread.join();
|
||||
for (const auto& result : results) Require(result == results[0], "concurrent cache identity differs");
|
||||
VertexInputCache evicting(ctx, 4);
|
||||
const auto expected = BuildVertexInputLayout(ctx, attributes);
|
||||
threads.clear();
|
||||
for (unsigned i = 0; i < 8; ++i) threads.emplace_back([&, i] {
|
||||
ready.arrive_and_wait();
|
||||
for (unsigned call = 0; call < 1000; ++call) same(*get(evicting, ctx, 1 + (call + i * 17) % 64, attributes), expected);
|
||||
});
|
||||
for (auto& thread : threads) thread.join();
|
||||
Require(evicting.Size() <= 4, "concurrent evictions exceeded the layout budget");
|
||||
}
|
||||
|
||||
template<bool TCached> void benchmark() {
|
||||
auto ctx = context();
|
||||
ctx.formatProperties = [](VkPhysicalDevice, VkFormat, VkFormatProperties* properties) {
|
||||
*properties = {};
|
||||
properties->bufferFeatures = VK_FORMAT_FEATURE_VERTEX_BUFFER_BIT;
|
||||
};
|
||||
for (const unsigned count : {0u, 1u, 4u, 8u}) {
|
||||
for (const auto scenario : {"reuse-1", "reuse-64", "churn-2048", "evicted", "shape-churn"}) {
|
||||
VertexInputCache cache(ctx);
|
||||
std::vector<VertexAttribute> input;
|
||||
for (unsigned i = 0; i < count; ++i) input.push_back(attribute(i));
|
||||
const std::string_view name(scenario);
|
||||
const unsigned variants = name == "reuse-64" ? 64u : name == "churn-2048" ? 2048u : name == "evicted" ? 1024u : 1u;
|
||||
if (name == "evicted") for (unsigned variant = 1; variant <= 2048; ++variant) cache.TryGet(variant, input);
|
||||
std::vector<double> samples;
|
||||
std::vector<double> wallSamples;
|
||||
std::uint64_t checksum = 0;
|
||||
unsigned hits = 0;
|
||||
constexpr unsigned calls = 32768;
|
||||
for (unsigned pass = 0; pass < 10; ++pass) {
|
||||
hits = 0;
|
||||
const auto start = std::chrono::steady_clock::now();
|
||||
const auto cpuStart = std::clock();
|
||||
for (unsigned call = 0; call < calls; ++call) {
|
||||
if constexpr (TCached) {
|
||||
if (input.empty()) continue;
|
||||
}
|
||||
if (name == "shape-churn" && !input.empty()) input[0].resource.fields[1] = (32u + (call % 2u) * 16u) << 16u;
|
||||
const auto variant = 1u + call % variants;
|
||||
VertexInputLayout owned;
|
||||
std::shared_ptr<const VertexInputLayout> retained;
|
||||
if constexpr (TCached) {
|
||||
if (!input.empty()) retained = cache.TryGet(variant, input);
|
||||
}
|
||||
if (retained == nullptr) owned = BuildVertexInputLayout(ctx, input);
|
||||
const auto& layout = retained != nullptr ? *retained : owned;
|
||||
hits += retained != nullptr;
|
||||
checksum += layout.bindings.size();
|
||||
for (const auto& binding : layout.bindings) checksum += binding.binding + binding.stride + binding.inputRate;
|
||||
for (const auto& attribute : layout.attributes) checksum += attribute.location + attribute.binding + attribute.format + attribute.offset;
|
||||
}
|
||||
const auto cpuElapsed = static_cast<double>(std::clock() - cpuStart) * 1e9 / CLOCKS_PER_SEC / calls;
|
||||
const auto wallElapsed = std::chrono::duration<double, std::nano>(std::chrono::steady_clock::now() - start).count() / calls;
|
||||
if (pass != 0) {
|
||||
samples.push_back(cpuElapsed);
|
||||
wallSamples.push_back(wallElapsed);
|
||||
}
|
||||
}
|
||||
std::sort(samples.begin(), samples.end());
|
||||
std::sort(wallSamples.begin(), wallSamples.end());
|
||||
std::cout << count << ' ' << name << ": " << samples[4] << " ns/call, wall " << wallSamples[4] << " ns/call, hits " << hits << '/' << calls << ", checksum " << checksum << '\n';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
int main() {
|
||||
int main(int argc, char** argv) {
|
||||
try {
|
||||
if (argc == 3 && std::string_view(argv[1]) == "--benchmark-vertex-input") {
|
||||
const std::string_view mode(argv[2]);
|
||||
Require(mode == "baseline" || mode == "cache", "unknown vertex benchmark mode");
|
||||
if (mode == "cache") benchmark<true>();
|
||||
else benchmark<false>();
|
||||
return 0;
|
||||
}
|
||||
run();
|
||||
std::cout << "Vertex input cache tests passed\n";
|
||||
} catch (const std::exception& error) {
|
||||
|
||||
Reference in new issue
Block a user