From 02e58e9936ca7703e9832cf18142c190129d9711 Mon Sep 17 00:00:00 2001 From: TibixDev Date: Mon, 12 Oct 2026 07:20:01 +0300 Subject: [PATCH] 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. --- .../Execution/include/Recipe.hpp | 2 +- .../Graphics/include/VertexInput.hpp | 114 ++++++----- .../prx/libSceAgcDriver/Graphics/src/Draw.cpp | 22 ++- .../prx/libSceAgcDriver/tests/VertexInput.cpp | 178 ++++++++++++++++-- 4 files changed, 238 insertions(+), 78 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/Recipe.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/Recipe.hpp index 8653aa9acf..9a4dc16db6 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/Recipe.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/Recipe.hpp @@ -78,7 +78,7 @@ struct DrawRecipe { std::vector> targets; std::vector targetViews; std::uint64_t passKey = 0; - std::shared_ptr vertexInput; + Graphics::VertexInputLayout vertexInput; std::array pushBytes{}; VkShaderStageFlags pushStages = 0; std::optional masked; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/VertexInput.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/VertexInput.hpp index 73457bdf02..1387258786 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/VertexInput.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/VertexInput.hpp @@ -3,12 +3,13 @@ #include "prx/libSceAgcDriver/Graphics/include/Shaders.hpp" #include +#include +#include +#include +#include #include #include #include -#include -#include -#include #include #include #include @@ -108,18 +109,8 @@ inline std::string VertexAttributeSignature(const ShaderRecompiler::VertexAttrib struct VertexInputLayout { std::vector bindings; std::vector attributes; - std::vector alignments; }; -inline void ValidateVertexAddresses(std::span 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(fields[1] & 0xffffu) << 32u); - Require(address != 0 && address % layout.alignments[i] == 0, "unaligned vertex buffer"); - } -} - inline VertexInputLayout BuildVertexInputLayout(const Context& context, std::span 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(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 attributes) { + for (const auto& attribute : attributes) { + if (NullVertexDescriptor(attribute)) continue; + const auto& fields = attribute.resource.fields; + const auto address = fields[0] | (static_cast(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 Get(std::uint64_t variant, std::span attributes) { - if (attributes.empty()) return empty; - if (variant == 0) return std::make_shared(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(BuildVertexInputLayout(context, attributes)); - std::vector 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 TryGet(std::uint64_t variant, std::span 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 get(std::uint64_t variant, std::span 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(BuildVertexInputLayout(context, attributes)); + std::vector 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 a, std::span 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((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 layout; std::vector shape; @@ -208,7 +218,7 @@ private: }; Context context; std::size_t capacity; - const std::shared_ptr empty = std::make_shared(); + std::array, 1024> bypassBits{}; mutable std::mutex mutex; std::list order; std::unordered_map entries; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp index 3d59511f09..51ec575146 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp @@ -970,7 +970,8 @@ struct DrawInputs { VkBuffer indexHandle = VK_NULL_HANDLE; VkDeviceSize indexOffset = 0; std::uint32_t maxIndex = 0; - std::shared_ptr vertexInput; + VertexInputLayout vertexInput; + std::shared_ptr retainedVertexInput; std::vector> vertexBuffers; std::vector vertexHandles; std::vector vertexOffsets; @@ -979,6 +980,7 @@ struct DrawInputs { VkPipelineStageFlags shaderStages = 0; std::uint32_t meshGroups = 0; std::vector restartTable; + const VertexInputLayout& VertexLayout() const { return retainedVertexInput != nullptr ? *retainedVertexInput : vertexInput; } }; std::shared_ptr 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(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> 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); diff --git a/core/libs/prx/libSceAgcDriver/tests/VertexInput.cpp b/core/libs/prx/libSceAgcDriver/tests/VertexInput.cpp index a95685655f..38ad2f43ea 100644 --- a/core/libs/prx/libSceAgcDriver/tests/VertexInput.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/VertexInput.cpp @@ -1,6 +1,9 @@ #include "prx/libSceAgcDriver/Graphics/include/VertexInput.hpp" #include #include +#include +#include +#include #include #include #include @@ -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 get(VertexInputCache& cache, const Context& ctx, std::uint64_t variant, std::span attributes) { + auto result = cache.TryGet(variant, attributes); + return result != nullptr ? result : std::make_shared(BuildVertexInputLayout(ctx, attributes)); +} + template 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 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, 8> results; @@ -124,16 +197,87 @@ void run() { std::vector 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 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 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 samples; + std::vector 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 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(std::clock() - cpuStart) * 1e9 / CLOCKS_PER_SEC / calls; + const auto wallElapsed = std::chrono::duration(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(); + else benchmark(); + return 0; + } run(); std::cout << "Vertex input cache tests passed\n"; } catch (const std::exception& error) {