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:
Michon 2026-10-11 16:37:12 +02:00 committed by Adrià Franch
parent b64da7caf7
commit d109f3966f
7 changed files with 170 additions and 0 deletions

View File

@ -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

View File

@ -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]);

View File

@ -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));

View File

@ -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);
}

View File

@ -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);
}
}
}

View File

@ -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));

View File

@ -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);