mirror of
https://github.com/boykopovar/AnyPS5.git
synced 2026-10-18 23:07:07 -05:00
fix(libs): flush pending GPU results before file writes
This commit is contained in:
1 parent
fc623324a2
commit
a64d038154
10 files changed
+337
-4
No files matched your search
@@ -1678,6 +1678,8 @@ target_link_libraries(agc_driver_gpu_map_on_fiber_tests PRIVATE libSceFiber)
|
||||
target_link_libraries(agc_driver_rect_list_corner_tests PRIVATE shader_recompiler_agc_driver)
|
||||
target_link_libraries(agc_driver_queue_zero_late_interrupt_tests PRIVATE libkernel)
|
||||
|
||||
target_link_libraries(agc_driver_storage_file_write_tests PRIVATE libkernel)
|
||||
|
||||
target_link_libraries(agc_driver_preparation_scope_settle_tests PRIVATE libSceAgc)
|
||||
|
||||
target_include_directories(agc_driver_alpha_to_coverage_tests PRIVATE $<TARGET_PROPERTY:shader_recompiler_agc_driver,INTERFACE_INCLUDE_DIRECTORIES>)
|
||||
@@ -1860,6 +1862,7 @@ if(BUILD_TESTING)
|
||||
endif()
|
||||
endforeach()
|
||||
set_tests_properties(agc_driver_pixel_implicit_derivatives PROPERTIES TIMEOUT 30)
|
||||
set_tests_properties(agc_driver_storage_file_write PROPERTIES TIMEOUT 30 ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1;APS5_SUSPEND_DRAIN=1")
|
||||
add_test(NAME agc_driver_partially_resident_pending_stores COMMAND agc_driver_partially_resident_snapshot_tests --pending-stores)
|
||||
set_tests_properties(agc_driver_partially_resident_pending_stores PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" SKIP_RETURN_CODE 77 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}" TIMEOUT 30)
|
||||
if(WIN32)
|
||||
|
||||
@@ -2201,16 +2201,22 @@ unsigned long long CodeOffset(const void* address) {
|
||||
|
||||
namespace {
|
||||
std::atomic<void (*)(std::uint64_t, std::size_t)> flushHook{nullptr};
|
||||
std::atomic<void (*)(std::uint64_t, std::size_t)> readGuard{nullptr};
|
||||
|
||||
void PrepareHostRead(std::uintptr_t address, std::size_t bytes) {
|
||||
if (const auto guard = readGuard.load(std::memory_order_acquire)) guard(address, bytes);
|
||||
FlushGpuWrites(address, bytes);
|
||||
}
|
||||
}
|
||||
|
||||
void SetFlushHook(void (*hook)(std::uint64_t, std::size_t)) {
|
||||
flushHook.store(hook, std::memory_order_release);
|
||||
GuestArena::GuestArenaSetHostReadHook_nid_no_patch(&PrepareHostRead);
|
||||
}
|
||||
|
||||
std::atomic<void (*)(std::uint64_t, std::size_t)> readGuard{nullptr};
|
||||
|
||||
void SetReadGuard(void (*guard)(std::uint64_t address, std::size_t bytes)) {
|
||||
readGuard.store(guard, std::memory_order_release);
|
||||
GuestArena::GuestArenaSetHostReadHook_nid_no_patch(&PrepareHostRead);
|
||||
}
|
||||
|
||||
void FlushGpuWrites(std::uint64_t address, std::size_t bytes) {
|
||||
|
||||
@@ -0,0 +1,293 @@
|
||||
#include "prx/libSceAgcDriver/Execution/include/Driver.hpp"
|
||||
#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp"
|
||||
#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp"
|
||||
#include "prx/libSceAgcDriver/Graphics/include/Texture.hpp"
|
||||
#include "prx/libc/include/GuestAllocations.hpp"
|
||||
#include "prx/libc/include/GuestArena.hpp"
|
||||
#include "prx/libc/include/GuestWriteWatch.hpp"
|
||||
#include "prx/libkernel/File/include/FileFlags.hpp"
|
||||
#include "VulkanTestDevice.hpp"
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <chrono>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <iostream>
|
||||
#include <limits>
|
||||
#include <span>
|
||||
#include <string>
|
||||
#include <stdexcept>
|
||||
#include <vector>
|
||||
#ifdef _WIN32
|
||||
#ifndef NOMINMAX
|
||||
#define NOMINMAX
|
||||
#endif
|
||||
#include <windows.h>
|
||||
#else
|
||||
#include <sys/mman.h>
|
||||
#endif
|
||||
|
||||
class FileStream;
|
||||
struct GuestIovec { void* base; std::size_t length; };
|
||||
|
||||
extern "C" {
|
||||
int APS5_VABI sceKernelOpen(const char*, int, std::uint16_t);
|
||||
int APS5_VABI sceKernelClose(int);
|
||||
std::int64_t APS5_VABI sceKernelWrite(int, const void*, std::size_t);
|
||||
std::int64_t APS5_VABI sceKernelPwrite(int, const void*, std::size_t, std::int64_t);
|
||||
std::int64_t APS5_VABI sceKernelWritev(int, const GuestIovec*, int);
|
||||
std::int64_t APS5_VABI sceKernelPwritev(int, const GuestIovec*, int, std::int64_t);
|
||||
std::int64_t APS5_VABI write_nid_postfix(int, const char*, std::int64_t);
|
||||
std::int64_t APS5_VABI _write_nid_postfix(int, const void*, std::size_t);
|
||||
std::int64_t APS5_VABI pwrite_nid_disambig1_nid_postfix(int, const void*, std::size_t, std::int64_t);
|
||||
std::int64_t APS5_VABI writev_nid_postfix(int, const GuestIovec*, int);
|
||||
std::int64_t APS5_VABI pwritev_nid_postfix(int, const GuestIovec*, int, std::int64_t);
|
||||
int APS5_VABI sceKernelAioSubmitWriteCommands(KernelAioRwRequest*, std::int32_t, std::int32_t, std::int32_t*);
|
||||
int APS5_VABI sceKernelAioSubmitWriteCommandsMultiple(KernelAioRwRequest*, std::int32_t, std::int32_t, std::int32_t*);
|
||||
int APS5_VABI sceKernelAioDeleteRequest(std::int32_t, std::int32_t*);
|
||||
FileStream* APS5_VABI fopen_nid_postfix(const char*, const char*);
|
||||
std::size_t APS5_VABI fwrite_nid_postfix(const void*, std::size_t, std::size_t, FileStream*);
|
||||
int APS5_VABI fclose_nid_postfix(FileStream*);
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
using AgcDriver::Graphics::Require;
|
||||
constexpr std::uint32_t Width = 128;
|
||||
constexpr std::uint32_t Height = 120;
|
||||
constexpr std::size_t SurfaceBytes = 0x20000;
|
||||
constexpr std::size_t SourceOffset = 13;
|
||||
constexpr std::size_t TransferBytes = SurfaceBytes - 37;
|
||||
constexpr std::int64_t FileOffset = 17;
|
||||
alignas(256) constexpr std::array<std::uint32_t, 11> WriteCode{
|
||||
0xd7460004u, 0x0401060eu, 0xd7460005u, 0x0405060fu, 0x7e100d04u, 0x7e120d05u,
|
||||
0x7e140c0du, 0x7e1602f2u, 0xf0200f08u, 0x00000804u, 0xbf810000u,
|
||||
};
|
||||
|
||||
class GuestSurface {
|
||||
public:
|
||||
GuestSurface() {
|
||||
#ifdef _WIN32
|
||||
block = static_cast<std::uint8_t*>(GuestArena::GuestArenaAllocate_nid_postfix(SurfaceBytes, SurfaceBytes));
|
||||
GuestArena::GuestArenaCommit_nid_postfix(block, SurfaceBytes, PAGE_READWRITE, SurfaceBytes);
|
||||
#else
|
||||
void* mapped = mmap(nullptr, SurfaceBytes * 2, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
|
||||
Require(mapped != MAP_FAILED, "file write: cannot allocate the guest surface");
|
||||
const auto begin = reinterpret_cast<std::uintptr_t>(mapped);
|
||||
const auto aligned = (begin + SurfaceBytes - 1) & ~(std::uintptr_t{SurfaceBytes - 1});
|
||||
if (aligned != begin) munmap(mapped, aligned - begin);
|
||||
const auto tail = begin + SurfaceBytes * 2 - aligned - SurfaceBytes;
|
||||
if (tail != 0) munmap(reinterpret_cast<void*>(aligned + SurfaceBytes), tail);
|
||||
block = reinterpret_cast<std::uint8_t*>(aligned);
|
||||
if (AgcDriver::GuestMemory::WriteWatched()) GuestWriteWatch::GuestWriteWatchRegister_nid_postfix(block, SurfaceBytes);
|
||||
#endif
|
||||
GuestAllocations::Mutation mutation;
|
||||
mutation.Add(block, SurfaceBytes, true, true, true);
|
||||
}
|
||||
|
||||
~GuestSurface() {
|
||||
{
|
||||
GuestAllocations::Mutation mutation;
|
||||
mutation.Remove(block);
|
||||
}
|
||||
#ifdef _WIN32
|
||||
GuestArena::GuestArenaReset_nid_postfix(block, SurfaceBytes);
|
||||
GuestArena::GuestArenaRelease_nid_postfix(block, SurfaceBytes);
|
||||
#else
|
||||
munmap(block, SurfaceBytes);
|
||||
if (AgcDriver::GuestMemory::WriteWatched()) GuestWriteWatch::GuestWriteWatchUnregister_nid_postfix(block, SurfaceBytes);
|
||||
#endif
|
||||
}
|
||||
|
||||
GuestSurface(const GuestSurface&) = delete;
|
||||
GuestSurface& operator=(const GuestSurface&) = delete;
|
||||
std::uint8_t* Data() { return block; }
|
||||
|
||||
private:
|
||||
std::uint8_t* block = nullptr;
|
||||
};
|
||||
|
||||
void Produce(std::uint8_t* texels) {
|
||||
std::memset(texels, 0, SurfaceBytes);
|
||||
const auto address = reinterpret_cast<std::uintptr_t>(texels);
|
||||
const std::array<std::uint32_t, 14> data{
|
||||
static_cast<std::uint32_t>(address >> 8u),
|
||||
static_cast<std::uint32_t>((address >> 40u) & 0xffu) | (71u << 20u) | (((Width - 1u) & 3u) << 30u),
|
||||
((Width - 1u) >> 2u) | ((Height - 1u) << 14u),
|
||||
0xfacu | (0x09u << 20u) | (9u << 28u),
|
||||
0u, 0u, 0u, 0u, 0u, 0u, 0u, 0u, 0u, 7u,
|
||||
};
|
||||
std::vector<std::uint32_t> words;
|
||||
const auto registers = [&](std::uint32_t first, std::span<const std::uint32_t> values) {
|
||||
words.push_back(0xc0007600u | (static_cast<std::uint32_t>(values.size()) << 16u));
|
||||
words.push_back(first);
|
||||
words.insert(words.end(), values.begin(), values.end());
|
||||
};
|
||||
const std::array<std::uint32_t, 3> shape{8, 8, 1};
|
||||
registers(0x207, shape);
|
||||
const auto code = reinterpret_cast<std::uintptr_t>(WriteCode.data());
|
||||
const std::array<std::uint32_t, 2> program{static_cast<std::uint32_t>(code >> 8u), static_cast<std::uint32_t>(code >> 40u)};
|
||||
registers(0x20c, program);
|
||||
const std::array<std::uint32_t, 1> resource{(static_cast<std::uint32_t>(data.size()) << 1u) | (7u << 7u) | (1u << 11u)};
|
||||
registers(0x213, resource);
|
||||
registers(0x240, data);
|
||||
words.insert(words.end(), {0xc0031500u, Width / 8u, Height / 8u, 1u, 0x41u});
|
||||
Packet packet{words.data(), static_cast<std::uint32_t>(words.size()), 0, {}};
|
||||
AgcDriver::Submit(&packet, 0);
|
||||
AgcDriverSuspendPoint_nid_postfix();
|
||||
AgcDriverWaitIdle_nid_postfix();
|
||||
Require(AgcDriver::Graphics::StorageTexture::AnyPending(), "file write: the GPU producer left no pending image");
|
||||
}
|
||||
|
||||
class OutputFile {
|
||||
public:
|
||||
OutputFile() : name("anyps5-storage-file-write-" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count()) + ".bin") {}
|
||||
~OutputFile() {
|
||||
std::error_code error;
|
||||
std::filesystem::remove(name, error);
|
||||
}
|
||||
const std::string& Name() const { return name; }
|
||||
int Open() const {
|
||||
const auto descriptor = sceKernelOpen(name.c_str(), SCE_KERNEL_O_WRONLY | SCE_KERNEL_O_CREAT | SCE_KERNEL_O_TRUNC, 0600);
|
||||
Require(descriptor >= 0, "file write: cannot open the output");
|
||||
return descriptor;
|
||||
}
|
||||
std::vector<std::byte> Read(std::size_t offset) const {
|
||||
std::ifstream file(name, std::ios::binary | std::ios::ate);
|
||||
Require(file.tellg() == static_cast<std::streamoff>(offset + TransferBytes), "file write: incorrect output length");
|
||||
file.seekg(static_cast<std::streamoff>(offset));
|
||||
std::vector<std::byte> result(TransferBytes);
|
||||
Require(static_cast<bool>(file.read(reinterpret_cast<char*>(result.data()), result.size())), "file write: short output readback");
|
||||
return result;
|
||||
}
|
||||
|
||||
private:
|
||||
std::string name;
|
||||
};
|
||||
|
||||
enum class Api { Write, PosixWrite, CrtWrite, Pwrite, PosixPwrite, Writev, PosixWritev, Pwritev, PosixPwritev, Aio, AioMultiple, Fwrite };
|
||||
|
||||
bool CheckWrite(Api api, const char* name, GuestSurface& surface) {
|
||||
Produce(surface.Data());
|
||||
OutputFile file;
|
||||
auto* source = surface.Data() + SourceOffset;
|
||||
const std::array<GuestIovec, 4> vectors{{{source, 111}, {nullptr, 0}, {source + 111, 65537}, {source + 65648, TransferBytes - 65648}}};
|
||||
const bool positioned = api == Api::Pwrite || api == Api::PosixPwrite || api == Api::Pwritev || api == Api::PosixPwritev || api == Api::Aio || api == Api::AioMultiple;
|
||||
if (api == Api::Fwrite) {
|
||||
auto* stream = fopen_nid_postfix(file.Name().c_str(), "wb");
|
||||
Require(stream != nullptr, "file write: cannot open the stdio output");
|
||||
Require(fwrite_nid_postfix(source, 0, 7, stream) == 0, "file write: zero-sized fwrite failed");
|
||||
Require(fwrite_nid_postfix(source, 5, 0, stream) == 0, "file write: zero-count fwrite failed");
|
||||
bool overflow = false;
|
||||
try { fwrite_nid_postfix(source, std::numeric_limits<std::size_t>::max(), 2, stream); }
|
||||
catch (const std::overflow_error&) { overflow = true; }
|
||||
Require(overflow, "file write: fwrite byte count overflow was accepted");
|
||||
Require(AgcDriver::Graphics::StorageTexture::FindPending(reinterpret_cast<std::uintptr_t>(surface.Data()), SurfaceBytes) != nullptr, "file write: an empty or overflowing fwrite flushed its source");
|
||||
const auto written = fwrite_nid_postfix(source, 5, TransferBytes / 5, stream);
|
||||
Require(fclose_nid_postfix(stream) == 0, "file write: cannot close the stdio output");
|
||||
Require(written == TransferBytes / 5, "file write: fwrite returned a short element count");
|
||||
} else {
|
||||
const auto descriptor = file.Open();
|
||||
if (api == Api::Write) {
|
||||
Require(sceKernelWrite(descriptor, source, 0) == 0, "file write: zero length failed");
|
||||
Require(sceKernelPwrite(descriptor, nullptr, 0, 0) == 0, "file write: zero-length pwrite failed");
|
||||
Require(sceKernelWritev(descriptor, nullptr, 0) == 0, "file write: empty writev failed");
|
||||
Require(sceKernelPwrite(descriptor, source, 1, -1) == static_cast<std::int64_t>(static_cast<int>(0x80020016u)), "file write: negative offset was accepted");
|
||||
Require(AgcDriver::Graphics::StorageTexture::FindPending(reinterpret_cast<std::uintptr_t>(surface.Data()), SurfaceBytes) != nullptr, "file write: an empty or invalid write flushed its source");
|
||||
const char cpu = 'x';
|
||||
Require(sceKernelWrite(descriptor, &cpu, 1) == 1, "file write: unrelated CPU write failed");
|
||||
Require(AgcDriver::Graphics::StorageTexture::FindPending(reinterpret_cast<std::uintptr_t>(surface.Data()), SurfaceBytes) != nullptr, "file write: an unrelated source flushed the GPU image");
|
||||
Require(sceKernelClose(descriptor) == 0, "file write: cannot close the scope-control output");
|
||||
}
|
||||
const auto output = api == Api::Write ? file.Open() : descriptor;
|
||||
std::int64_t written = -1;
|
||||
switch (api) {
|
||||
case Api::Write: written = sceKernelWrite(output, source, TransferBytes); break;
|
||||
case Api::PosixWrite: written = write_nid_postfix(output, reinterpret_cast<const char*>(source), TransferBytes); break;
|
||||
case Api::CrtWrite: written = _write_nid_postfix(output, source, TransferBytes); break;
|
||||
case Api::Pwrite: written = sceKernelPwrite(output, source, TransferBytes, FileOffset); break;
|
||||
case Api::PosixPwrite: written = pwrite_nid_disambig1_nid_postfix(output, source, TransferBytes, FileOffset); break;
|
||||
case Api::Writev: written = sceKernelWritev(output, vectors.data(), vectors.size()); break;
|
||||
case Api::PosixWritev: written = writev_nid_postfix(output, vectors.data(), vectors.size()); break;
|
||||
case Api::Pwritev: written = sceKernelPwritev(output, vectors.data(), vectors.size(), FileOffset); break;
|
||||
case Api::PosixPwritev: written = pwritev_nid_postfix(output, vectors.data(), vectors.size(), FileOffset); break;
|
||||
case Api::Aio:
|
||||
case Api::AioMultiple: {
|
||||
KernelAioResult result{};
|
||||
KernelAioRwRequest request{FileOffset, TransferBytes, source, &result, output};
|
||||
std::int32_t id = 0;
|
||||
Require((api == Api::Aio ? sceKernelAioSubmitWriteCommands(&request, 1, 0, &id) : sceKernelAioSubmitWriteCommandsMultiple(&request, 1, 0, &id)) == 0, "file write: AIO submission failed");
|
||||
std::int32_t removed = -1;
|
||||
Require(sceKernelAioDeleteRequest(id, &removed) == 0 && removed == 0, "file write: AIO deletion failed");
|
||||
Require(result.state == 3, "file write: AIO did not complete");
|
||||
written = result.return_value;
|
||||
break;
|
||||
}
|
||||
case Api::Fwrite: break;
|
||||
}
|
||||
Require(sceKernelClose(output) == 0, "file write: cannot close the output");
|
||||
Require(written == TransferBytes, "file write: the transfer returned a short byte count");
|
||||
}
|
||||
const auto stored = file.Read(positioned ? FileOffset : 0);
|
||||
std::vector<std::byte> expected(TransferBytes);
|
||||
AgcDriver::GuestMemory::Read(reinterpret_cast<std::uintptr_t>(source), expected);
|
||||
Require(std::any_of(expected.begin(), expected.end(), [](std::byte value) { return value != std::byte{}; }), "file write: the GPU producer wrote only zeros");
|
||||
std::size_t differences = 0;
|
||||
for (std::size_t index = 0; index < stored.size(); ++index) differences += stored[index] != expected[index];
|
||||
std::printf("%s: %zu incorrect file bytes out of %zu\n", name, differences, stored.size());
|
||||
return differences == 0;
|
||||
}
|
||||
|
||||
void CheckShutdownAndGuard() {
|
||||
OutputFile file;
|
||||
std::vector<std::byte> source(TransferBytes, std::byte{0x5a});
|
||||
AgcDriver::GuestMemory::SetFlushHook(nullptr);
|
||||
AgcDriver::GuestMemory::SetReadGuard([](std::uint64_t, std::size_t) { throw std::runtime_error("file source read guard"); });
|
||||
const auto descriptor = file.Open();
|
||||
bool refused = false;
|
||||
try { sceKernelWrite(descriptor, source.data(), source.size()); }
|
||||
catch (const std::runtime_error& error) { refused = std::string(error.what()) == "file source read guard"; }
|
||||
AgcDriver::GuestMemory::SetReadGuard(nullptr);
|
||||
Require(refused && std::filesystem::file_size(file.Name()) == 0, "file write: source read guard was bypassed before the file changed");
|
||||
Require(sceKernelWrite(descriptor, source.data(), source.size()) == static_cast<std::int64_t>(source.size()), "file write: ordinary source failed after shutdown and hook reset");
|
||||
Require(sceKernelClose(descriptor) == 0, "file write: cannot close the shutdown output");
|
||||
Require(file.Read(0) == source, "file write: ordinary output changed after shutdown and hook reset");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
int main() {
|
||||
try {
|
||||
{
|
||||
const auto device = OpenVulkanTestDevice();
|
||||
if (!device) return VulkanTestSkipped;
|
||||
}
|
||||
GuestSurface surface;
|
||||
GuestSurface unrelated;
|
||||
Produce(unrelated.Data());
|
||||
bool correct = true;
|
||||
const std::array<std::pair<Api, const char*>, 12> cases{{
|
||||
{Api::Write, "sceKernelWrite"}, {Api::PosixWrite, "write"}, {Api::CrtWrite, "_write"},
|
||||
{Api::Pwrite, "sceKernelPwrite"}, {Api::PosixPwrite, "pwrite"},
|
||||
{Api::Writev, "sceKernelWritev"}, {Api::PosixWritev, "writev"},
|
||||
{Api::Pwritev, "sceKernelPwritev"}, {Api::PosixPwritev, "pwritev"},
|
||||
{Api::Aio, "AIO"}, {Api::AioMultiple, "AIO multiple"}, {Api::Fwrite, "fwrite"},
|
||||
}};
|
||||
for (const auto& [api, name] : cases) {
|
||||
correct = CheckWrite(api, name, surface) && correct;
|
||||
Require(AgcDriver::Graphics::StorageTexture::FindPending(reinterpret_cast<std::uintptr_t>(unrelated.Data()), SurfaceBytes) != nullptr, "file write: an unrelated pending GPU image was flushed");
|
||||
}
|
||||
Require(correct, "file write: pending GPU results were not stored before the host read");
|
||||
std::vector<std::byte> unused(SurfaceBytes);
|
||||
AgcDriver::GuestMemory::Read(reinterpret_cast<std::uintptr_t>(unrelated.Data()), unused);
|
||||
AgcDriverShutdown_nid_postfix();
|
||||
CheckShutdownAndGuard();
|
||||
return 0;
|
||||
} catch (const std::exception& error) {
|
||||
std::cerr << error.what() << '\n';
|
||||
AgcDriverShutdown_nid_postfix();
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
@@ -56,6 +56,8 @@ bool GuestArenaSharedBacking_nid_postfix(std::uintptr_t address, std::size_t byt
|
||||
#endif
|
||||
bool GuestArenaBeginHostWrite_nid_postfix(void* pointer, std::size_t bytes);
|
||||
void GuestArenaEndHostWrite_nid_postfix(void* pointer, std::size_t bytes);
|
||||
void GuestArenaSetHostReadHook_nid_no_patch(void (*hook)(std::uintptr_t address, std::size_t bytes));
|
||||
void GuestArenaPrepareHostRead_nid_no_patch(const void* pointer, std::size_t bytes);
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -419,6 +419,10 @@ size_t APS5_VABI fwrite_nid_postfix(const void* buffer, size_t size, size_t coun
|
||||
auto* handle = GetNativeStream(stream);
|
||||
if (size == 0 || count == 0) return 0;
|
||||
if (!buffer) throw std::runtime_error("fwrite: null buffer");
|
||||
if (count > std::numeric_limits<std::size_t>::max() / size) throw std::overflow_error("fwrite: buffer size overflow");
|
||||
const auto bytes = size * count;
|
||||
if (bytes > std::numeric_limits<std::uintptr_t>::max() - reinterpret_cast<std::uintptr_t>(buffer)) throw std::overflow_error("fwrite: buffer address overflow");
|
||||
GuestArena::GuestArenaPrepareHostRead_nid_no_patch(buffer, bytes);
|
||||
const auto result = std::fwrite(buffer, size, count, handle);
|
||||
stream->SyncStatus();
|
||||
return result;
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
#endif
|
||||
#include <cstdio>
|
||||
#include <iterator>
|
||||
#include <limits>
|
||||
#include <map>
|
||||
#include <mutex>
|
||||
#include <stdexcept>
|
||||
@@ -31,6 +32,8 @@
|
||||
namespace GuestArena {
|
||||
namespace {
|
||||
|
||||
std::atomic<void (*)(std::uintptr_t, std::size_t)> hostReadHook{nullptr};
|
||||
|
||||
#ifdef _WIN32
|
||||
std::atomic<std::uint64_t> commitGeneration{1};
|
||||
std::atomic<void (*)(std::uintptr_t, std::size_t, std::uint64_t)> privateMappingObserver{nullptr};
|
||||
@@ -536,6 +539,16 @@ void GuestArenaSetAccessObserver_nid_postfix(void (*callback)(std::uintptr_t add
|
||||
accessObserver.store(callback, std::memory_order_release);
|
||||
}
|
||||
|
||||
void GuestArenaSetHostReadHook_nid_no_patch(void (*hook)(std::uintptr_t, std::size_t)) {
|
||||
hostReadHook.store(hook, std::memory_order_release);
|
||||
}
|
||||
|
||||
void GuestArenaPrepareHostRead_nid_no_patch(const void* pointer, std::size_t bytes) {
|
||||
const auto address = reinterpret_cast<std::uintptr_t>(pointer);
|
||||
if (address == 0 || bytes == 0 || bytes > std::numeric_limits<std::uintptr_t>::max() - address) return;
|
||||
if (const auto hook = hostReadHook.load(std::memory_order_acquire)) hook(address, bytes);
|
||||
}
|
||||
|
||||
bool GuestArenaBeginHostWrite_nid_postfix(void* pointer, std::size_t bytes) {
|
||||
#ifdef _WIN32
|
||||
return WindowsMappings::Get().BeginHostWrite(reinterpret_cast<std::uintptr_t>(pointer), bytes);
|
||||
|
||||
@@ -80,11 +80,12 @@ std::int64_t NativePread(std::int32_t fd, void* buf, std::size_t nbyte, std::int
|
||||
}
|
||||
|
||||
std::int64_t NativePwrite(std::int32_t fd, const void* buf, std::size_t nbyte, std::int64_t offset) {
|
||||
#ifdef _WIN32
|
||||
if (offset < 0) {
|
||||
errno = EINVAL;
|
||||
return -1;
|
||||
}
|
||||
GuestArena::GuestArenaPrepareHostRead_nid_no_patch(buf, nbyte);
|
||||
#ifdef _WIN32
|
||||
return NativePositioned_nid_no_patch(fd, const_cast<void*>(buf), nbyte, offset, true);
|
||||
#else
|
||||
const auto result = File::PwriteSharedMemory_nid_no_patch(fd, buf, nbyte, offset);
|
||||
|
||||
@@ -314,6 +314,7 @@ std::int64_t APS5_VABI sceKernelWrite(int d, const void* buf, std::size_t nbytes
|
||||
const char empty = 0;
|
||||
return sceKernelWrite(d, &empty, 0);
|
||||
}
|
||||
GuestArena::GuestArenaPrepareHostRead_nid_no_patch(buf, nbytes);
|
||||
auto n = NativeWrite(d, buf, nbytes);
|
||||
if (n < 0) return SceIoErrorFromErrno(errno);
|
||||
return static_cast<std::int64_t>(n);
|
||||
|
||||
@@ -900,6 +900,7 @@ int64_t APS5_VABI pwrite_nid_disambig1_nid_postfix(int d, const void* buf, size_
|
||||
if (buf == nullptr && nbytes != 0) return PosixFailure(GUEST_EFAULT);
|
||||
if (offset < 0) return PosixFailure(GUEST_EINVAL);
|
||||
if (FailPosixPositionedGuestSocket(d)) return -1;
|
||||
GuestArena::GuestArenaPrepareHostRead_nid_no_patch(buf, nbytes);
|
||||
char empty = 0;
|
||||
const auto result = NativePwrite(d, buf == nullptr ? &empty : buf, nbytes, offset);
|
||||
return result < 0 ? PosixFailure(SceErrorFromErrno(errno) & 0xffff) : result;
|
||||
@@ -1132,6 +1133,10 @@ static bool OpenIovecs(const KernelIovec* iov, int iovcnt, std::deque<GuestArena
|
||||
return true;
|
||||
}
|
||||
|
||||
static void PrepareWriteIovecs(const KernelIovec* iov, int iovcnt) {
|
||||
for (int i = 0; i < iovcnt; ++i) GuestArena::GuestArenaPrepareHostRead_nid_no_patch(iov[i].base, iov[i].length);
|
||||
}
|
||||
|
||||
#ifdef _WIN32
|
||||
|
||||
int64_t APS5_VABI sceKernelPread(int d, void* buf, size_t nbytes, int64_t offset) {
|
||||
@@ -1151,6 +1156,7 @@ int64_t APS5_VABI sceKernelPwrite(int d, const void* buf, size_t nbytes, int64_t
|
||||
if (buf == nullptr && nbytes != 0) return SceErrorFromErrno(GUEST_EFAULT);
|
||||
if (offset < 0) return SceErrorFromErrno(GUEST_EINVAL);
|
||||
if (const auto error = PositionedGuestSocketError(d); error != 0) return error;
|
||||
GuestArena::GuestArenaPrepareHostRead_nid_no_patch(buf, nbytes);
|
||||
char empty = 0;
|
||||
const auto result = NativePwrite(d, nbytes == 0 && buf == nullptr ? &empty : buf, nbytes, offset);
|
||||
return result < 0 ? SceErrorFromErrno(errno) : result;
|
||||
@@ -1176,6 +1182,7 @@ static std::int64_t TransferIovecs(int d, const KernelIovec* iov, int iovcnt, co
|
||||
: GuestSockets::Readv(d, reinterpret_cast<const GuestSockets::Iovec*>(iov), iovcnt);
|
||||
return result < 0 ? SceKernelError(*__error_nid_postfix()) : result;
|
||||
}
|
||||
if (write) PrepareWriteIovecs(iov, iovcnt);
|
||||
if (!write && File::IsRandomDevice(d)) return ReadRandomIovecs(d, iov, iovcnt);
|
||||
if (!write) {
|
||||
if (const auto archived = File::ReadArchivedIovecs(d, iov, iovcnt, offset)) return *archived;
|
||||
@@ -1247,6 +1254,7 @@ int64_t APS5_VABI sceKernelPwrite(int d, const void* buf, size_t nbytes, int64_t
|
||||
if (buf == nullptr && nbytes != 0) return SceErrorFromErrno(GUEST_EFAULT);
|
||||
if (offset < 0) return SceErrorFromErrno(GUEST_EINVAL);
|
||||
if (const auto error = PositionedGuestSocketError(d); error != 0) return error;
|
||||
GuestArena::GuestArenaPrepareHostRead_nid_no_patch(buf, nbytes);
|
||||
const auto result = NativePwrite(d, buf, nbytes, offset);
|
||||
return result < 0 ? SceErrorFromErrno(errno) : result;
|
||||
}
|
||||
@@ -1271,6 +1279,7 @@ int64_t APS5_VABI sceKernelWritev(int d, const KernelIovec* iov, int iovcnt) {
|
||||
const auto result = GuestSockets::Writev(d, reinterpret_cast<const GuestSockets::Iovec*>(iov), iovcnt);
|
||||
return result < 0 ? SceKernelError(*__error_nid_postfix()) : result;
|
||||
}
|
||||
PrepareWriteIovecs(iov, iovcnt);
|
||||
const auto sharedResult = File::WritevSharedMemory_nid_no_patch(d, NativeIovecs(iov), iovcnt);
|
||||
if (sharedResult != -2) return sharedResult < 0 ? SceErrorFromErrno(errno) : sharedResult;
|
||||
const auto result = static_cast<std::int64_t>(::writev(d, NativeIovecs(iov), iovcnt));
|
||||
@@ -1293,6 +1302,7 @@ int64_t APS5_VABI sceKernelPwritev(int d, const KernelIovec* iov, int iovcnt, in
|
||||
if (const int error = CheckIovecs(iov, iovcnt)) return error;
|
||||
if (offset < 0) return SceErrorFromErrno(GUEST_EINVAL);
|
||||
if (const auto error = PositionedGuestSocketError(d); error != 0) return error;
|
||||
PrepareWriteIovecs(iov, iovcnt);
|
||||
const auto sharedResult = File::PwritevSharedMemory_nid_no_patch(d, NativeIovecs(iov), iovcnt, offset);
|
||||
if (sharedResult != -2) return sharedResult < 0 ? SceErrorFromErrno(errno) : sharedResult;
|
||||
const auto result = static_cast<std::int64_t>(::pwritev(d, NativeIovecs(iov), iovcnt, static_cast<off_t>(offset)));
|
||||
|
||||
@@ -84,7 +84,7 @@
|
||||
- AMD image descriptors with `TYPE=0` are treated as null for read-only image slots even when the base and other words are nonzero: `image_load` on a gfx1035 with such a T# returns 0,0,0,0, and the same descriptors were seen in direct reads in PPSA01341 on an RTX 4070. Writable and atomic image slots and entries of an image table keep faulting because there is no hardware measurement for them, and the reserved types 2-7 also fault. `TYPE=1` is treated as null in the same read-only slots: gfx1035 reads it as zeros too, and gfx1010 returns zeros for `image_sample_lz`, `image_sample_b`, `image_load` and `image_get_resinfo` with format 22 (#5828); it is not measured on RDNA2, with other formats or with depth-compare reads (`image_sample_c*`). gfx1013 behavior is unverified
|
||||
- On a device without VK_EXT_robustness2's `nullDescriptor`, such as MoltenVK, a null image descriptor in a typed heap is a [placeholder image](../../../core/libs/prx/libSceAgcDriver/Graphics/src/PlaceholderImages.cpp) of that heap's type, 1×1 and cleared to zero, which every null descriptor of the heap shares. Reads give zeros, as `nullDescriptor` allows, but stores and atomics to it are kept, so a later read of a null storage image can return them, and a depth comparison compares against depth 0. A heap whose placeholder the device cannot create, for example 1D or 3D depth, 64-bit atomics without 64-bit image atomics, or multisampled storage without `shaderStorageImageMultisample`, throws when a null descriptor reaches it.
|
||||
- [Draw input snapshots](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp) (libSceAgcDriver): `PrepareDrawBindings` caps read-only input buffer snapshots at 64 MiB (overridable via `APS5_DRAW_SNAPSHOT_MAX_MIB`). Larger inputs (streaming heaps, bindless pools) or draws exceeding the cumulative budget skip the per-draw snapshot and bind in place, avoiding multi-gigabyte CPU copy stalls (such as Ratchet & Clank: Rift Apart PPSA01474 stalling at frame ~380 copying 36 GB every 5 s). If the title rewrites those large ranges on the CPU before GPU completion, the in-place binding observes the modification, as verified by `drawSnapshotCapTests`.
|
||||
- [Pending storage-image results](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp) (libSceAgcDriver) are stored when something reads their memory, not when the GPU work that wrote them completes. A title that waits for that work, frees the image and reuses its memory can have the new data overwritten by the late store (#1970). A title's own CPU reads are not seen either, so a title that reads the results after waiting for a label reads stale memory. `APS5_STORE_AT_FLIP=1` stores every pending result at each flip and `APS5_STORE_AT_LABEL=1` before each label the GPU writes; both store every pending image, not only the ones the waited-for work wrote, and neither is on by default.
|
||||
- [Pending storage-image results](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp) (libSceAgcDriver) are stored when something reads their memory, not when the GPU work that wrote them completes. A title that waits for that work, frees the image and reuses its memory can have the new data overwritten by the late store (#1970). A title's own CPU reads are not seen either, so a title that reads the results after waiting for a label reads stale memory. Explicit-size file writes (`write`, `pwrite`, `writev`, `pwritev`, AIO and `fwrite`) synchronize their source ranges through the [host-read hook](../../../core/libs/prx/libc/src/GuestArena.cpp); other host readers are not covered. `APS5_STORE_AT_FLIP=1` stores every pending result at each flip and `APS5_STORE_AT_LABEL=1` before each label the GPU writes; both store every pending image, not only the ones the waited-for work wrote, and neither is on by default.
|
||||
- [Pending storage-image results over memory outside the write watch](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp) (libSceAgcDriver) leave to the CPU the 64 KiB blocks whose bytes differ from the ones saved at the upload, the fill, the image copy, the previous store or the moment their range left the watch (`Unwatch`, or a host import that does not stay watched), and the blocks the watch saw the CPU write before that moment. A store made on the GPU saves its bytes through a host-visible copy that becomes the saved bytes when its batch completes; a store or a refresh that needs them earlier waits for that batch. A unit without a write generation saves the guest bytes when results land in it outside the watch. One whose range leaves the watch while its results are pending, and ranges that leave the watch through a failed collect, have no saved bytes; storing such results throws.
|
||||
- Windows host-pointer imports of the guest mapping itself are compared instead of write-watched, except writable imports of private `MEM_WRITE_WATCH` memory when the import probe keeps the write watch and a settle queue exists: those of 1 MiB or more stay watched, and smaller ones return to the watch once their settle has completed, unless a storage image overlaps them (see Pages under an import window): a driver can mark imported pages dirty after the synchronous import window. Shared memory imported through a separate writable alias retains protection-based tracking on the guest views; driver access to the import alias does not disarm those views. [GuestMemory](../../../core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp) excludes every overlapping 64 KiB tracker block while its backing is retained. A full block replaced by fresh private backing regains tracking and gets a new generation; protection changes, partial replacements and shared remappings do not restore it. Excluded blocks keep their write-watch marks as a hint only (`UntrackedHint`): a storage image or a sampled texture snapshot over them compares its bytes again after a mark in its range, a driver store, a mapping change or a recorded GPU write instead of at every collect epoch or lookup, and a mark the driver makes without a CPU store costs one comparison. This preserves CPU edits through byte comparisons but limits snapshot reuse on excluded blocks to draws of one collect epoch and disables unit shadows for them; restoring tracking on import retirement without replacing the backing remains unimplemented.
|
||||
- [Pages under an import window](../../../core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp) (libSceAgcDriver) whose write protection was resolved while a host import that leaves its range watched was made are compared with the bytes they held before the import and count as stored over only when those differ. A CPU store of the bytes already there within the window cannot be told apart from an import that resolves the write protection without storing, as the NVIDIA driver (610.57.04) does for every imported page, so such a store does not win over pending results. Where host-pointer and dma-buf imports differ in whether they stay watched, a failed host-pointer import and the dma-buf import after it are two windows, and the pages the failed one resolved count as stored over. On Windows, the NVIDIA driver (617.42) marks every page of a host import of private `MEM_WRITE_WATCH` memory written, asynchronously, until a GPU use of the import completes; a 4-byte read of it on the settle queue ends that even while main-queue work that used the import first is still running, and an earlier import of the same pages whose first use runs after that read marks none of them (`agc_import_settle_first_use`, `agc_import_settle_earlier_import`). Such an import stays watched only after a 4-byte read of it on the settle queue has completed inside the window. Each one waits for that read, which keeps its window open for one round trip on the settle queue, so a same-value CPU store from another thread in that time counts as unchanged too. An import made where no settle queue exists is compared instead. An import of under 1 MiB is not settled inside its window, because the settle blocks the importing thread and a title that imports a 64 KiB buffer around every submission lost about 19 % of its frame rate to that wait (Dripl1x, #6060). It is compared until its read, submitted in a batch by a later import lookup under `GpuMutex` while the imports match the registry, has completed; a later lookup then returns its whole blocks to the write watch, each stamped as a CPU store, so whatever was derived from them while they were compared reads as changed once. Its blocks stay compared while storage results are pending over them, and those of an import retired before that stay compared.
|
||||
|
||||
Reference in new issue
Block a user