fix(shader): write cache files of 2 GiB and more in chunks

On Windows (MinGW libstdc++) one std::ofstream::write of 2 GiB or more
never returns: the whole temporary file is written but WriteFileAtomically
hangs, the entry is never replaced and the temporary file stays behind.
A Vulkan pipeline cache of 2.1 GB hung every save and left a 2 GB .tmp
per run. Write in chunks of at most 1 GiB.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XBXM4pgkMHYEtqN1EQni2P
This commit is contained in:
thibautandClaude Opus 5.5 committed 2026-10-17 14:48:47 +02:00
1 parent c9e6c45de9
commit a03ccc6be1
3 files changed
+74 -1

No files matched your search

@@ -1592,6 +1592,11 @@ if(BUILD_TESTING)
if(WIN32) if(WIN32)
set_tests_properties(agc_driver_partially_resident_pending_stores PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>") set_tests_properties(agc_driver_partially_resident_pending_stores PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
endif() endif()
add_test(NAME agc_shader_disk_cache_large_file COMMAND agc_shader_disk_cache_tests --large-file)
set_tests_properties(agc_shader_disk_cache_large_file PROPERTIES SKIP_RETURN_CODE 77 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}" TIMEOUT 240)
if(WIN32)
set_tests_properties(agc_shader_disk_cache_large_file PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
endif()
if(WIN32) if(WIN32)
add_test(NAME agc_driver_untracked_snapshot_hint COMMAND agc_driver_partially_resident_snapshot_tests --untracked-hint) add_test(NAME agc_driver_untracked_snapshot_hint COMMAND agc_driver_partially_resident_snapshot_tests --untracked-hint)
set_tests_properties(agc_driver_untracked_snapshot_hint PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" SKIP_RETURN_CODE 77 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}" TIMEOUT 30 set_tests_properties(agc_driver_untracked_snapshot_hint PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" SKIP_RETURN_CODE 77 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}" TIMEOUT 30
@@ -17,7 +17,9 @@
#include <cstdio> #include <cstdio>
#include <cstdlib> #include <cstdlib>
#include <cstring> #include <cstring>
#include <exception>
#include <filesystem> #include <filesystem>
#include <fstream>
#include <functional> #include <functional>
#include <future> #include <future>
#include <iostream> #include <iostream>
@@ -29,6 +31,7 @@
#include <stdexcept> #include <stdexcept>
#include <string> #include <string>
#include <string_view> #include <string_view>
#include <thread>
#include <vector> #include <vector>
#ifdef _WIN32 #ifdef _WIN32
#ifndef NOMINMAX #ifndef NOMINMAX
@@ -730,6 +733,64 @@ void verifyFloatModeSerialization() {
require(back.request.context.floatMode == mode, "the float mode did not survive serialization"); require(back.request.context.floatMode == mode, "the float mode did not survive serialization");
} }
int runLargeFileWrite() {
constexpr std::size_t size = (std::size_t{2} << 30u) + 4096u;
const auto directory = std::filesystem::temp_directory_path() / "aps5-shader-disk-cache-large-file-test";
std::error_code error;
std::filesystem::remove_all(directory, error);
std::filesystem::create_directories(directory, error);
const auto space = std::filesystem::space(directory, error);
if (error || space.available < size + (std::uintmax_t{1} << 30u)) {
std::cout << "skipping: not enough free disk space for a file of more than 2 GiB\n";
return 77;
}
std::vector<std::byte> bytes;
try {
bytes.resize(size);
} catch (const std::bad_alloc&) {
std::cout << "skipping: cannot allocate a buffer of more than 2 GiB\n";
return 77;
}
const std::array<std::size_t, 6> samples{0u, (std::size_t{1} << 30u) - 1u, std::size_t{1} << 30u, (std::size_t{2} << 30u) - 1u, std::size_t{2} << 30u, size - 1u};
for (std::size_t index = 0; index < samples.size(); ++index) bytes[samples[index]] = static_cast<std::byte>(0xa0u + index);
const auto path = directory / "pipelines.bin";
auto written = std::make_shared<std::promise<bool>>();
auto result = written->get_future();
std::thread([written, &bytes, path] {
try {
written->set_value(WriteFileAtomically(path, bytes));
} catch (...) {
written->set_exception(std::current_exception());
}
}).detach();
if (result.wait_for(std::chrono::seconds(120)) != std::future_status::ready) {
std::cerr << "WriteFileAtomically did not return within 120 s for a file of more than 2 GiB\n";
std::cerr.flush();
std::_Exit(1);
}
require(result.get(), "cannot write a cache file of more than 2 GiB");
require(std::filesystem::file_size(path) == size, "the cache file of more than 2 GiB has the wrong size");
{
std::ifstream file(path, std::ios::binary);
require(static_cast<bool>(file), "cannot open the cache file of more than 2 GiB");
for (std::size_t index = 0; index < samples.size(); ++index) {
file.seekg(static_cast<std::streamoff>(samples[index]));
char value = 0;
require(file.get(value) && static_cast<std::byte>(value) == bytes[samples[index]], "the cache file of more than 2 GiB differs at byte " + std::to_string(samples[index]));
}
}
std::size_t entries = 0;
for (const auto& entry : std::filesystem::directory_iterator(directory)) {
static_cast<void>(entry);
++entries;
}
require(entries == 1u, "the cache file of more than 2 GiB left a temporary file behind");
std::filesystem::remove_all(directory, error);
require(!error, "cannot remove the large file test directory: " + error.message());
std::cout << "large cache file write test passed\n";
return 0;
}
void verifyAtomicReplacement(const std::filesystem::path& directory) { void verifyAtomicReplacement(const std::filesystem::path& directory) {
struct Reader { struct Reader {
#ifdef _WIN32 #ifdef _WIN32
@@ -2124,6 +2185,7 @@ int main(int argc, char** argv) {
std::cout << "artifact codec tests passed\n"; std::cout << "artifact codec tests passed\n";
return 0; return 0;
} }
if (argc == 2 && std::string_view(argv[1]) == "--large-file") return runLargeFileWrite();
if (argc == 2 && std::string_view(argv[1]) == "--load") return runLoadingProcess(); if (argc == 2 && std::string_view(argv[1]) == "--load") return runLoadingProcess();
if (argc == 2 && std::string_view(argv[1]) == "--load-prebuilt") return runPrebuiltLoadingProcess(); if (argc == 2 && std::string_view(argv[1]) == "--load-prebuilt") return runPrebuiltLoadingProcess();
if (argc == 2 && std::string_view(argv[1]) == "--no-failure-memo") return runWithoutFailureMemo(); if (argc == 2 && std::string_view(argv[1]) == "--no-failure-memo") return runWithoutFailureMemo();
@@ -1,6 +1,7 @@
#ifndef CORE_SHADER_RECOMPILER_SHADERCACHEDIRECTORY_HPP #ifndef CORE_SHADER_RECOMPILER_SHADERCACHEDIRECTORY_HPP
#define CORE_SHADER_RECOMPILER_SHADERCACHEDIRECTORY_HPP #define CORE_SHADER_RECOMPILER_SHADERCACHEDIRECTORY_HPP
#include <algorithm>
#include <atomic> #include <atomic>
#include <chrono> #include <chrono>
#include <cstddef> #include <cstddef>
@@ -56,7 +57,12 @@ inline bool WriteFileAtomically(const std::filesystem::path& path, std::span<con
{ {
std::ofstream file(temporary, std::ios::binary | std::ios::trunc); std::ofstream file(temporary, std::ios::binary | std::ios::trunc);
if (!file) return false; if (!file) return false;
file.write(reinterpret_cast<const char*>(bytes.data()), static_cast<std::streamsize>(bytes.size())); constexpr std::size_t largestWrite = std::size_t{1} << 30u;
for (std::size_t offset = 0; offset < bytes.size() && file;) {
const auto chunk = std::min(bytes.size() - offset, largestWrite);
file.write(reinterpret_cast<const char*>(bytes.data() + offset), static_cast<std::streamsize>(chunk));
offset += chunk;
}
file.close(); file.close();
written = !file.fail(); written = !file.fail();
} }