fix(agc): guest access to a multisampled color target's memory throws
A multisampled target's samples are only on the host, so its guest memory never holds them: a CPU read through GuestMemory::Read/ReadCommitted, a DMA_DATA copy from it and a texture or storage image lookup at its address throw instead of returning bytes that were never written there. Asked by oneandonlydean; the test reads a resolved-only target's memory each way. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
b64da7caf7
commit
d109f3966f
|
|
@ -183,6 +183,7 @@ unsigned long long CodeOffset(const void* address);
|
|||
// every read and store through this interface calls first. Code that reads guest memory directly
|
||||
// calls FlushGpuWrites itself.
|
||||
void SetFlushHook(void (*hook)(std::uint64_t address, std::size_t bytes));
|
||||
void SetReadGuard(void (*guard)(std::uint64_t address, std::size_t bytes));
|
||||
void FlushGpuWrites(std::uint64_t address, std::size_t bytes);
|
||||
// Moves with every ForgetPages call (the libc invalidator: memory unmapped or re-registered): odd
|
||||
// while the call stores its page states, moved past it after, so a reader that loads an even
|
||||
|
|
|
|||
|
|
@ -61,6 +61,7 @@ bool Driver::preparePacketMemory(const Submission& submission, QueueState& queue
|
|||
}
|
||||
}
|
||||
if (opcode == 0x50 && packet.size() >= 7) {
|
||||
if (Pm4::DmaSource(packet) == 0 || Pm4::DmaSource(packet) == 3) Graphics::RequireNoMultisampleTarget(packet[2] | (static_cast<std::uint64_t>(packet[3]) << 32u), packet[6] & 0x3ffffffu, "a DMA copy");
|
||||
if (Pm4::DmaSource(packet) == 2 && Pm4::DmaDestination(packet) != 1) {
|
||||
const auto address = packet[4] | (static_cast<std::uint64_t>(packet[5]) << 32u);
|
||||
Graphics::NoteDepthMetadataFill(address, packet[6] & 0x3ffffffu, packet[2]);
|
||||
|
|
|
|||
|
|
@ -1751,12 +1751,19 @@ void SetFlushHook(void (*hook)(std::uint64_t, std::size_t)) {
|
|||
flushHook.store(hook, std::memory_order_release);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
void FlushGpuWrites(std::uint64_t address, std::size_t bytes) {
|
||||
if (const auto hook = flushHook.load(std::memory_order_acquire)) hook(address, bytes);
|
||||
}
|
||||
|
||||
void Read(std::uint64_t address, std::span<std::byte> destination, std::size_t alignment) {
|
||||
if (destination.empty()) return;
|
||||
if (const auto guard = readGuard.load(std::memory_order_acquire)) guard(address, destination.size());
|
||||
FlushGpuWrites(address, destination.size());
|
||||
const TimedAccess timed(CounterRead, destination.size());
|
||||
CountReadCaller(__builtin_return_address(0));
|
||||
|
|
@ -1773,6 +1780,7 @@ void Read(std::uint64_t address, std::span<std::byte> destination, std::size_t a
|
|||
|
||||
void ReadCommitted(std::uint64_t address, std::span<std::byte> destination) {
|
||||
if (destination.empty()) return;
|
||||
if (const auto guard = readGuard.load(std::memory_order_acquire)) guard(address, destination.size());
|
||||
FlushGpuWrites(address, destination.size());
|
||||
const TimedAccess timed(CounterRead, destination.size());
|
||||
CountReadCaller(__builtin_return_address(0));
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@ VkImageView MultisampleTargetView(const Context& context, const ColorTarget& tar
|
|||
void ResolveMultisampleTarget(const Context& context, const ColorTarget& source, StorageTexture& destination, VkImageView destinationView);
|
||||
void NoteColorMetadataFill(std::uint64_t address, std::size_t bytes, std::uint32_t pattern);
|
||||
void ClearMultisampleTargets(VkDevice device);
|
||||
void RequireNoMultisampleTarget(std::uint64_t address, std::size_t bytes, const char* access);
|
||||
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
#include <shared_mutex>
|
||||
#include "prx/libSceAgcDriver/Graphics/include/MultisampleTarget.hpp"
|
||||
#include "prx/libSceAgcDriver/Graphics/include/Pipeline.hpp"
|
||||
#include "prx/libSceAgcDriver/Graphics/include/Recorder.hpp"
|
||||
|
|
@ -165,6 +166,41 @@ private:
|
|||
}
|
||||
};
|
||||
|
||||
struct GuardedRange {
|
||||
VkDevice device;
|
||||
std::uint64_t address;
|
||||
std::uint64_t bytes;
|
||||
std::uint32_t samples;
|
||||
};
|
||||
|
||||
std::shared_mutex& guardedMutex() {
|
||||
static std::shared_mutex mutex;
|
||||
return mutex;
|
||||
}
|
||||
|
||||
std::vector<GuardedRange>& guarded() {
|
||||
static std::vector<GuardedRange> ranges;
|
||||
return ranges;
|
||||
}
|
||||
|
||||
std::atomic<bool>& anyGuarded() {
|
||||
static std::atomic<bool> any{false};
|
||||
return any;
|
||||
}
|
||||
|
||||
void guardReads(std::uint64_t address, std::size_t bytes) {
|
||||
RequireNoMultisampleTarget(address, bytes, "a CPU read");
|
||||
}
|
||||
|
||||
void guard(const Context& context, const ColorTarget& target) {
|
||||
const auto bytes = std::max<std::uint64_t>(target.bytes, static_cast<std::uint64_t>(target.extent.width) * target.extent.height * target.elementBytes * target.samples);
|
||||
std::unique_lock lock(guardedMutex());
|
||||
guarded().push_back({context.device, target.address, bytes, target.samples});
|
||||
anyGuarded().store(true, std::memory_order_release);
|
||||
static std::once_flag installed;
|
||||
std::call_once(installed, [] { GuestMemory::SetReadGuard(&guardReads); });
|
||||
}
|
||||
|
||||
std::mutex& targetsMutex() {
|
||||
static std::mutex mutex;
|
||||
return mutex;
|
||||
|
|
@ -183,6 +219,7 @@ MultisampleTarget& acquire(const Context& context, const ColorTarget& use) {
|
|||
return *target;
|
||||
}
|
||||
targets().push_back(std::make_unique<MultisampleTarget>(context, use));
|
||||
guard(context, use);
|
||||
auto& created = *targets().back();
|
||||
if (use.cmaskFastClear) created.cmask.SeedKeys(CurrentDccKeys(use.cmaskAddress, static_cast<std::uint64_t>(use.cmaskBytes) * 256u));
|
||||
created.ApplyFastClear(use);
|
||||
|
|
@ -276,6 +313,20 @@ void NoteColorMetadataFill(std::uint64_t address, std::size_t bytes, std::uint32
|
|||
void ClearMultisampleTargets(VkDevice device) {
|
||||
std::lock_guard lock(targetsMutex());
|
||||
std::erase_if(targets(), [&](const auto& target) { return target->context.device == device; });
|
||||
std::unique_lock guardLock(guardedMutex());
|
||||
std::erase_if(guarded(), [&](const auto& range) { return range.device == device; });
|
||||
anyGuarded().store(!guarded().empty(), std::memory_order_release);
|
||||
}
|
||||
|
||||
void RequireNoMultisampleTarget(std::uint64_t address, std::size_t bytes, const char* access) {
|
||||
if (bytes == 0 || !anyGuarded().load(std::memory_order_acquire)) return;
|
||||
std::shared_lock lock(guardedMutex());
|
||||
for (const auto& range : guarded()) {
|
||||
if (address >= range.address + range.bytes || range.address >= address + bytes) continue;
|
||||
char text[256];
|
||||
std::snprintf(text, sizeof(text), "AGC graphics: %s of 0x%llx+0x%zx overlaps multisampled color target 0x%llx+0x%llx (%u samples), whose samples are only on the host: guest access to it is not implemented", access, static_cast<unsigned long long>(address), bytes, static_cast<unsigned long long>(range.address), static_cast<unsigned long long>(range.bytes), range.samples);
|
||||
throw std::runtime_error(text);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
#include "prx/libSceAgcDriver/Execution/include/PerformanceTimer.hpp"
|
||||
#include "prx/libSceAgcDriver/Execution/include/CaptureTrace.hpp"
|
||||
#include "ThreadOwned.hpp"
|
||||
#include "prx/libSceAgcDriver/Graphics/include/MultisampleTarget.hpp"
|
||||
#include "prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp"
|
||||
#include "prx/libSceAgcDriver/Graphics/include/PlaceholderImages.hpp"
|
||||
#include "prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp"
|
||||
|
|
@ -513,6 +514,7 @@ std::uint64_t sampledBudget(const Context& context, TextureCache& cache) {
|
|||
// `guestBytes` is the surface size when the caller described the surface already (0: described here).
|
||||
std::shared_ptr<Texture> cachedTexture(const Context& context, std::span<const std::uint32_t> words, const GuestTextureResource& resource, VkComponentMapping components, std::uint64_t guestBytes = 0, bool depthCompare = false) {
|
||||
CaptureTrace::Log("sampled-lookup address=%llx width=%u height=%u dcc=%llx", static_cast<unsigned long long>(resource.baseAddress), resource.width, resource.height, static_cast<unsigned long long>(resource.dccAddress));
|
||||
RequireNoMultisampleTarget(resource.baseAddress, guestBytes != 0 ? static_cast<std::size_t>(guestBytes) : 1u, "a texture read");
|
||||
if (auto* recorder = Recorder::Active(); recorder != nullptr) recorder->BoundKeptBytes();
|
||||
if (auto depth = DepthSurfaceTexture(context, words, resource, components)) {
|
||||
logLookup({depth.get(), resource, 0, DccKeys::Uncompressed, 0, nullptr, true});
|
||||
|
|
@ -831,6 +833,7 @@ GuestTextureResource StorageSurface(const Context& context, const GuestTextureRe
|
|||
|
||||
// `guestBytes` is the surface size when the caller described the surface already (0: described here).
|
||||
std::shared_ptr<StorageTexture> cachedStorageTexture(const Context& context, std::span<const std::uint32_t> words, const GuestTextureResource& viewed, std::uint32_t mip, std::uint64_t guestBytes) {
|
||||
RequireNoMultisampleTarget(viewed.baseAddress, guestBytes != 0 ? static_cast<std::size_t>(guestBytes) : 1u, "a storage image access");
|
||||
if (DepthSurfaceAt(viewed.baseAddress)) {
|
||||
char text[112];
|
||||
std::snprintf(text, sizeof(text), "AGC graphics: storage image access to depth/stencil surface 0x%llx is not implemented", static_cast<unsigned long long>(viewed.baseAddress));
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@
|
|||
#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp"
|
||||
#include "prx/libSceAgcDriver/Graphics/include/DccMetadata.hpp"
|
||||
#include "prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp"
|
||||
#include "prx/libSceAgcDriver/Graphics/include/MultisampleTarget.hpp"
|
||||
#include "prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp"
|
||||
#include "prx/libSceAgcDriver/Graphics/include/Resources.hpp"
|
||||
#include "prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp"
|
||||
|
|
@ -38,6 +39,7 @@
|
|||
#else
|
||||
#include <sys/mman.h>
|
||||
#endif
|
||||
#include <functional>
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <atomic>
|
||||
|
|
@ -4312,6 +4314,108 @@ void dataRefreshTests(const Device& device, Recorder& recorder) {
|
|||
recorder.Sync();
|
||||
}
|
||||
|
||||
void multisampleGuestAccessTests(const Device& device, Recorder& recorder) {
|
||||
const auto& base = device.GetContext();
|
||||
if ((base.limits.framebufferColorSampleCounts & VK_SAMPLE_COUNT_4_BIT) == 0 || !device.NullDescriptors()) {
|
||||
std::cout << "4x color targets or nullDescriptor unavailable: multisampled target guest access not tested\n";
|
||||
return;
|
||||
}
|
||||
constexpr std::uint32_t side = 64;
|
||||
constexpr std::size_t targetBytes = side * side * 4u;
|
||||
constexpr std::size_t bytes = 4u * 65536u;
|
||||
#ifdef _WIN32
|
||||
void* block = VirtualAlloc(nullptr, bytes, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE);
|
||||
#else
|
||||
void* block = std::aligned_alloc(65536, bytes);
|
||||
#endif
|
||||
Require(block != nullptr, "cannot allocate the multisampled target block");
|
||||
std::memset(block, 0, bytes);
|
||||
const auto address = reinterpret_cast<std::uint64_t>(block);
|
||||
{
|
||||
GuestAllocations::Mutation mutation;
|
||||
mutation.Add(block, bytes, true, true, true);
|
||||
}
|
||||
struct Unregister {
|
||||
const Context& context;
|
||||
void* block;
|
||||
~Unregister() {
|
||||
ClearMultisampleTargets(context.device);
|
||||
ClearCachedTextures(context.device);
|
||||
{
|
||||
GuestAllocations::Mutation mutation;
|
||||
mutation.Remove(block);
|
||||
}
|
||||
#ifdef _WIN32
|
||||
VirtualFree(block, 0, MEM_RELEASE);
|
||||
#else
|
||||
std::free(block);
|
||||
#endif
|
||||
}
|
||||
} unregister{base, block};
|
||||
TextureDetiler detiler(base);
|
||||
auto context = base;
|
||||
context.detiler = &detiler;
|
||||
context.nullDescriptors = true;
|
||||
TextureCache cache(context);
|
||||
context.textureCache = &cache;
|
||||
const auto refused = [](const std::function<void()>& access, const std::string& expected, const std::string& what) {
|
||||
std::string message;
|
||||
try {
|
||||
access();
|
||||
} catch (const std::exception& error) {
|
||||
message = error.what();
|
||||
}
|
||||
Require(message.find(expected) != std::string::npos && message.find("multisampled color target") != std::string::npos, what + " of a resolved-only multisampled target's memory did not throw: " + message);
|
||||
};
|
||||
ColorTarget target{};
|
||||
target.address = address;
|
||||
target.surfaceAddress = address;
|
||||
target.extent = {side, side};
|
||||
target.surfaceExtent = target.extent;
|
||||
target.format = VK_FORMAT_R8G8B8A8_UNORM;
|
||||
target.bytes = targetBytes;
|
||||
target.samples = 4;
|
||||
{
|
||||
std::lock_guard gpu(GpuMutex());
|
||||
Require(MultisampleTargetView(context, target) != VK_NULL_HANDLE, "the multisampled target has no view");
|
||||
recorder.Submit();
|
||||
device.WaitQueue();
|
||||
recorder.Sync();
|
||||
}
|
||||
std::array<std::byte, 64> texels{};
|
||||
refused([&] { AgcDriver::GuestMemory::Read(address + 256u, texels); }, "a CPU read", "a CPU read");
|
||||
refused([&] { RequireNoMultisampleTarget(address, 64u, "a DMA copy"); }, "a DMA copy", "a DMA copy");
|
||||
ShaderRecompiler::RecompileResult program;
|
||||
ShaderRecompiler::DescriptorBinding binding;
|
||||
binding.kind = ShaderRecompiler::DescriptorKind::SampledImage;
|
||||
binding.role = ShaderRecompiler::DescriptorRole::GuestImages;
|
||||
binding.descriptorSet = 0;
|
||||
binding.binding = ShaderRecompiler::RuntimeAbi::FirstImageBinding;
|
||||
binding.count = 1;
|
||||
binding.imageShape = ShaderRecompiler::DescriptorImageShape::Image2D;
|
||||
binding.imageSamplers = {0};
|
||||
binding.guestDescriptor = {
|
||||
static_cast<std::uint32_t>(address >> 8u),
|
||||
static_cast<std::uint32_t>((address >> 40u) & 0xffu) | (56u << 20u) | (((side - 1u) & 3u) << 30u),
|
||||
((side - 1u) >> 2u) | ((side - 1u) << 14u),
|
||||
0xfacu | (9u << 28u),
|
||||
0u,
|
||||
0u,
|
||||
0u,
|
||||
0u,
|
||||
};
|
||||
program.bindings.push_back(binding);
|
||||
const CompiledShader compute{ShaderRecompiler::ShaderStage::Compute, &program, 0};
|
||||
refused([&] {
|
||||
std::lock_guard gpu(GpuMutex());
|
||||
ShaderResources resources(context, compute);
|
||||
}, "a texture read", "a texture read");
|
||||
AgcDriver::GuestMemory::Read(address + 2u * 65536u, texels);
|
||||
ClearMultisampleTargets(context.device);
|
||||
AgcDriver::GuestMemory::Read(address + 256u, texels);
|
||||
std::cout << "Multisampled target guest access tests passed\n";
|
||||
}
|
||||
|
||||
void depthSurfaceProofTests(const Device& device, Recorder& recorder) {
|
||||
const auto& base = device.GetContext();
|
||||
if (!device.NullDescriptors()) {
|
||||
|
|
@ -5153,6 +5257,7 @@ int main(int argc, char** argv) {
|
|||
resourceReadTests(device, recorder);
|
||||
readWrittenStagingTests(device, recorder);
|
||||
depthSurfaceProofTests(device, recorder);
|
||||
multisampleGuestAccessTests(device, recorder);
|
||||
misalignedSnapshotTests(device, recorder);
|
||||
misalignedRegionTests(device, recorder);
|
||||
drawSnapshotReuseTests(device, recorder);
|
||||
|
|
|
|||
Loading…
Reference in New Issue