Merge pull request #192 from MichonGoddijn231849/perf/validation-failure-memo

perf(agc): remember shader validation failures and reject repeats without throwing
This commit is contained in:
boykopovar 2026-09-30 22:12:58 +03:00 committed by GitHub
commit a67b644ff6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 59 additions and 7 deletions

View File

@ -273,6 +273,7 @@ public:
// `recipe`, when given, receives the DrawRecipe a recorded, cacheable, reusable, direct draw
// built for its draw-cache entry (design_cpu_final M8); null otherwise.
void Draw(const Graphics::State& graphics, const Pm4::DrawParameters& draw, std::span<const Graphics::CompiledShader> shaders, std::span<const Graphics::GuestMemorySnapshot> snapshots = {}, std::shared_ptr<const DrawRecipe>* recipe = nullptr);
std::optional<std::string> KnownDrawRejection(const Graphics::State& graphics, std::span<const Graphics::CompiledShader> shaders) const;
// A draw-cache hit recorded from its recipe (Graphics::DrawWithRecipe), under the mutex after
// the packet's labels; Rebuild when the recipe's device is not this one or DrawWithRecipe
// missed (nothing recorded: the caller runs Draw with the hit's stages and re-attaches).

View File

@ -4866,6 +4866,10 @@ private:
fold(*programResults[0], direct);
if (graphics.rectList && patched.contains(0)) buildRectList();
snapshot();
if (auto known = localDevice->KnownDrawRejection(graphics, stages)) {
rejected = std::move(*known);
return DrawVerdict::Rejected;
}
lockForDraw();
noteDrawWriters(stages, submission.queue);
phase(DrawRowVectors);
@ -4891,6 +4895,12 @@ private:
recipe = findDrawRecipe(drawKey, recipeStages);
if (recipe == nullptr) VulkanDevice::NoteDrawRecipeMiss(VulkanDevice::DrawRecipePrecheck::NoRecipe);
}
if (recipe == nullptr) {
if (auto known = localDevice->KnownDrawRejection(graphics, stages)) {
rejected = std::move(*known);
return DrawVerdict::Rejected;
}
}
lockForDraw();
noteDrawWriters(stages, submission.queue);
phase(DrawRowVectors);

View File

@ -2196,6 +2196,10 @@ VulkanDevice::IndirectDrawSupport VulkanDevice::DrawIndirectSupport() const {
return {state->drawIndirectFirstInstance, state->multiDrawIndirect, state->drawIndirectCount};
}
std::optional<std::string> VulkanDevice::KnownDrawRejection(const Graphics::State& graphics, std::span<const Graphics::CompiledShader> shaders) const {
return Graphics::KnownValidationFailure(graphicsContext(), shaders, graphics);
}
void VulkanDevice::Draw(const Graphics::State& graphics, const Pm4::DrawParameters& draw, std::span<const Graphics::CompiledShader> shaders, std::span<const Graphics::GuestMemorySnapshot> snapshots, std::shared_ptr<const DrawRecipe>* recipe) {
// Two stdout lines per draw cost ~1.3 ms per frame of the queue-0 worker (part of it under the
// GPU mutex); APS5_TRACE_DRAWS=1 restores them.

View File

@ -1,3 +1,5 @@
#include <string>
#include <optional>
#ifndef CORE_LIBS_PRX_LIBSCEAGCDRIVER_GRAPHICS_INCLUDE_DRAW_HPP
#define CORE_LIBS_PRX_LIBSCEAGCDRIVER_GRAPHICS_INCLUDE_DRAW_HPP
@ -13,6 +15,7 @@ namespace AgcDriver::Graphics {
// built for its draw-cache entry (design_cpu_final M8; null otherwise, and always under
// APS5_NO_DRAW_RECIPE=1).
void Draw(const Context& context, const State& state, const Pm4::DrawParameters& draw, std::span<const CompiledShader> shaders, std::span<const GuestMemorySnapshot> snapshots = {}, std::shared_ptr<const DrawRecipe>* recipe = nullptr);
std::optional<std::string> KnownValidationFailure(const Context& context, std::span<const CompiledShader> shaders, const State& state);
std::array<std::uint32_t, 4> MeshIndexBufferDescriptor(const Pm4::DrawParameters& draw, std::uint64_t unreadAddress);

View File

@ -369,12 +369,9 @@ void countCache(std::uint64_t DrawProfile::*counter) {
// fragment results the key already names (the pipeline key treats them the same way). `memoized`
// says whether the memo applied, `hit` whether it answered. APS5_NO_VALIDATE_CACHE=1 validates
// every draw.
std::set<std::uint32_t> CachedFragmentOutputs(const Context& context, std::span<const CompiledShader> shaders, const State& state, bool& memoized, bool& hit) {
bool ValidationKey(const Context& context, std::span<const CompiledShader> shaders, const State& state, std::vector<std::uint64_t>& key) {
using Stage = ShaderRecompiler::ShaderStage;
static const bool disabled = std::getenv("APS5_NO_VALIDATE_CACHE") != nullptr;
memoized = false;
hit = false;
std::vector<std::uint64_t> key;
const auto add = [&](auto value) { key.push_back(static_cast<std::uint64_t>(value)); };
bool keyed = !disabled;
if (keyed) {
@ -435,19 +432,47 @@ std::set<std::uint32_t> CachedFragmentOutputs(const Context& context, std::span<
add(tessellation.outputTopology);
}
}
return keyed;
}
std::mutex& validationMutex() {
static std::mutex mutex;
return mutex;
}
std::map<std::vector<std::uint64_t>, std::string>& validationFailures() {
static std::map<std::vector<std::uint64_t>, std::string> failures;
return failures;
}
std::set<std::uint32_t> CachedFragmentOutputs(const Context& context, std::span<const CompiledShader> shaders, const State& state, bool& memoized, bool& hit) {
memoized = false;
hit = false;
std::vector<std::uint64_t> key;
const bool keyed = ValidationKey(context, shaders, state, key);
static std::map<std::vector<std::uint64_t>, std::set<std::uint32_t>> memo;
if (keyed) {
memoized = true;
std::lock_guard lock(mutex);
std::lock_guard lock(validationMutex());
if (const auto found = memo.find(key); found != memo.end()) {
hit = true;
return found->second;
}
}
auto outputs = ValidateShaders(shaders, state, context.subgroup, context.fragmentShaderBarycentric, context.descriptorIndexing);
std::set<std::uint32_t> outputs;
try {
outputs = ValidateShaders(shaders, state, context.subgroup, context.fragmentShaderBarycentric, context.descriptorIndexing);
} catch (const std::exception& error) {
if (keyed) {
std::lock_guard lock(validationMutex());
auto& failures = validationFailures();
if (failures.size() >= 1024) failures.clear();
failures.emplace(std::move(key), error.what());
}
throw;
}
if (keyed) {
std::lock_guard lock(mutex);
std::lock_guard lock(validationMutex());
// A handful of configurations recur; a runaway key space is dropped wholesale.
if (memo.size() >= 1024) memo.clear();
memo.emplace(std::move(key), outputs);
@ -1238,6 +1263,15 @@ bool RecordDraws() {
}
std::optional<std::string> KnownValidationFailure(const Context& context, std::span<const CompiledShader> shaders, const State& state) {
std::vector<std::uint64_t> key;
if (!ValidationKey(context, shaders, state, key)) return std::nullopt;
std::lock_guard lock(validationMutex());
const auto& failures = validationFailures();
if (const auto found = failures.find(key); found != failures.end()) return found->second;
return std::nullopt;
}
void Draw(const Context& context, const State& state, const Pm4::DrawParameters& draw, std::span<const CompiledShader> shaders, std::span<const GuestMemorySnapshot> snapshots, std::shared_ptr<const DrawRecipe>* recipeOut) {
PerformanceTimer timing("Graphics.Draw");
// APS5_PROFILE_DRAW prints the time of each phase of the draw (microseconds) and the [draws] totals.