Files
SpartanPort/tests/test_cache_storage.cpp
Mahdi Akbari Zarkesh c22329f13b Fix(GPU): ported texture, GPU and shader cache fixes (#1124)
EN-US

- Fixed texture cache image references moving when the cache grows, out-of-range slot IDs, per-dimension extent checks, view clamping, padding-only overlaps and in-flight sampler destruction
- Fixed depth and colour buffer copies to copy every shared mip and layer
- Fixed scheduler null chunks at 0x20000, exit 23 from oversized recording data and stale idle signals releasing new submissions
- Fixed IRQ heap corruption at startup and saved packet sizes before running packets
- Fixed shader cache loss: queued writes drained at exit, partial loads rejected, damaged records skipped
- Fixed draws whose pipelines the driver refuses, speculative specialization exceptions and null user-data tables at area loads
- Updated texture pressure to start at 60% of the budget on cards below 8 GB, unblocked evictions behind retained images and kept GPU-only images below the critical mark
- Added tests for the slot vector, subresource extents, IRQ controller, record data, cache storage, cache flush, serdes and shader user data

---

PT-BR

- Corrigidos no cache de texturas: referências de imagem que mudavam de lugar quando o cache crescia, IDs de slot fora do intervalo, checagem de tamanho por dimensão, recorte de views, sobreposição só de padding e destruição de samplers em uso
- Corrigidas as cópias de buffers de depth e color para copiar todos os mips e layers compartilhados
- Corrigidos no scheduler: chunks nulos em 0x20000, exit 23 com dados de gravação grandes demais e sinais de ocioso antigos liberando submissões novas
- Corrigida a corrupção do heap de IRQ na inicialização, e o tamanho dos pacotes passou a ser salvo antes de executá-los
- Corrigida a perda do cache de shaders: gravações na fila esvaziadas ao sair, cargas parciais rejeitadas e registros danificados pulados
- Corrigidos os draws com pipeline recusado pelo driver, as exceções de especialização especulativa e as tabelas de user data nulas na troca de área
- Atualizada a pressão de texturas para começar em 60% do orçamento em placas com menos de 8 GB, desbloqueadas as remoções atrás de imagens retidas e mantidas as imagens só da GPU abaixo da marca crítica
- Adicionados testes de slot vector, subresource extents, controlador de IRQ, record data, cache storage, flush do cache, serdes e shader user data
2026-11-01 21:31:31 -03:00

172 lines
6.7 KiB
C++

// SPDX-License-Identifier: GPL-2.0-or-later
// Run: out/gpu/cache-storage-test <dir> (a new directory that the test creates and removes).
// Exercise the production IO worker without a window, guest process or Vulkan device.
#include <atomic>
#include <barrier>
#include <cassert>
#include <chrono>
#include <cstdio>
#include <filesystem>
#include <fstream>
#include <future>
#include <thread>
#include <vector>
#include "../gpu/spartanportgpu.h"
// Include the implementation so optimized builds need not export its internal template writer.
#include "../gpu/shadps4/video_core/cache_storage.cpp"
// Mirror the production metadata shim without starting spartanportgpu_init's window/device.
namespace Core {
class Emulator {
public:
static void FillElfInfo(const SpartanGpuConfig& config) {
auto& info = Common::ElfInfo::Instance();
info.initialized = true;
info.game_serial = config.serial ? config.serial : "UNKNOWN";
info.title = config.title ? config.title : "";
info.sdk_ver = config.sdk_version;
info.psf_attributes.raw = config.psf_attributes;
}
};
} // namespace Core
int main(int argc, char** argv) {
assert(argc == 2); // Supply a new, test-owned directory inside the workspace.
const auto root = std::filesystem::absolute(argv[1]);
assert(!std::filesystem::exists(root));
std::filesystem::create_directories(root);
// Window close and renderer startup can make the first database access concurrently.
std::vector<Storage::DataBase*> databases(8);
std::barrier start{8};
std::vector<std::thread> readers;
for (unsigned i = 0; i < databases.size(); ++i) {
readers.emplace_back([&, i] {
start.arrive_and_wait();
databases[i] = &Storage::DataBase::Instance();
});
}
for (auto& reader : readers) {
reader.join();
}
for (auto* database : databases) {
assert(database == databases.front());
}
const auto save = [&](unsigned i) {
assert(Storage::WriteVector(Storage::BlobType::ShaderBinary, root / std::to_string(i),
std::vector<u32>{i, i + 1}));
};
const auto verify = [&](unsigned count) {
for (unsigned i = 0; i < count; ++i) {
const auto path = root / (std::to_string(i) + ".spv");
assert(std::filesystem::file_size(path) == 2 * sizeof(u32));
std::ifstream file(path, std::ios::binary);
u32 words[2]{};
file.read(reinterpret_cast<char*>(words), sizeof(words));
assert(file && words[0] == i && words[1] == i + 1);
assert(!std::filesystem::exists(path.string() + ".tmp"));
}
};
{
std::jthread worker{Storage::ProcessIO};
std::vector<std::thread> producers;
for (unsigned t = 0; t < 4; ++t) {
producers.emplace_back([&, t] {
for (unsigned i = 0; i < 100; ++i) {
save(t * 100 + i);
}
});
}
for (auto& producer : producers) {
producer.join();
}
} // Request stop immediately; every accepted write must finish before the join returns.
verify(400);
// Even a stop requested before the worker starts must drain its accepted backlog.
for (unsigned i = 400; i < 500; ++i) {
save(i);
}
std::stop_source stopped;
stopped.request_stop();
std::thread worker{[&] { Storage::ProcessIO(stopped.get_token()); }};
worker.join();
verify(500);
// Existing cache words still load; a miss must discard the caller's previous contents.
std::vector<u32> words{99};
auto path = root / "1";
Storage::LoadVector(Storage::BlobType::ShaderBinary, path, words);
assert((words == std::vector<u32>{1, 2}));
path = root / "absent";
Storage::LoadVector(Storage::BlobType::ShaderBinary, path, words);
assert(words.empty());
// Reject both an incomplete first word and a complete word followed by stray bytes.
for (const size_t bytes : {3, 5}) {
path = root / "bad.spv";
{
std::ofstream file(path, std::ios::binary);
file << std::string(bytes, 'x');
assert(file);
}
words = {99};
Storage::LoadVector(Storage::BlobType::ShaderBinary, path, words);
assert(words.empty());
std::vector<u8> raw{99};
Storage::LoadVector(Storage::BlobType::ShaderBinary, path, raw);
assert(raw == std::vector<u8>(bytes, 'x'));
}
path = root / "empty.spv";
std::ofstream{path, std::ios::binary}.close();
words = {99};
Storage::LoadVector(Storage::BlobType::ShaderBinary, path, words);
assert(words.empty());
// A cache root replaced by a file disables caching without throwing, then permits a retry.
Core::Emulator::FillElfInfo(SpartanGpuConfig{.serial = "TEST00000"});
using namespace Common::FS;
const auto original_cache = GetUserPath(PathType::CacheDir);
const auto cache_root = root / "cache";
std::filesystem::create_directory(cache_root);
SetUserPath(PathType::CacheDir, cache_root);
assert(std::filesystem::remove(cache_root));
std::ofstream{cache_root} << "blocked";
auto& cache = Storage::DataBase::Instance();
cache.Open();
assert(!cache.IsOpened());
assert(!cache.Save(Storage::BlobType::ShaderBinary, "blocked", std::vector<u32>{1}));
assert(std::filesystem::is_regular_file(cache_root));
assert(std::filesystem::remove(cache_root));
std::filesystem::create_directory(cache_root);
cache.Open();
assert(cache.IsOpened());
assert(std::filesystem::is_directory(cache_root / "TEST00000"));
// Flush waits while a write is queued or running and returns once the worker is idle.
std::promise<void> release;
std::atomic<bool> ran{false};
{
std::scoped_lock lock{m_request};
req_queue.emplace([&ran, gate = release.get_future().share()] {
gate.wait();
ran = true;
});
}
request_cv.notify_one();
assert(!cache.Flush(std::chrono::milliseconds(50)));
release.set_value();
assert(cache.Flush(std::chrono::seconds(5)) && ran);
// A saved blob is on disk when Flush returns; a closed cache has nothing to flush.
assert(cache.Save(Storage::BlobType::ShaderBinary, "flushed", std::vector<u32>{7}));
assert(cache.Flush(std::chrono::seconds(5)));
assert(std::filesystem::file_size(cache_root / "TEST00000" / "flushed.spv") == sizeof(u32));
cache.Close();
assert(cache.Flush(std::chrono::seconds(5)));
SetUserPath(PathType::CacheDir, original_cache);
std::filesystem::remove_all(root); // Only the directory this test creates above.
std::puts("Cache storage: concurrent writes, stop drain, flush and load hardening PASS");
}