Merge pull request #120 from oneandonlydean/fix/agc-untracked-target-results

fix(agc): keep render target results where the write tracker cannot prove memory fresh
This commit is contained in:
boykopovar 2026-09-30 18:09:58 +03:00 committed by GitHub
commit 03fa5a146e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 131 additions and 4 deletions

View File

@ -1098,7 +1098,7 @@ bool StorageTexture::Refresh() {
// clear code make every result dead on hardware too. Without a generation (no tracking) a
// unit's stamps say nothing, so it is stored.
const auto droppable = [&](std::uint32_t unit) {
if (IsDccClear(keys)) return true;
if (keysChanged && IsDccClear(keys)) return true;
return layerGeneration[unit] != 0 && unit < stampedBlocks.size() && stampedBlocks[unit] != 0;
};
// A clear code -> uncompressed flip on an image with results pending: an unstamped pending
@ -1140,6 +1140,7 @@ bool StorageTexture::Refresh() {
stored[layer] = true;
anyStored = true;
}
if (anyStored && !keysChanged && descriptor.dccAddress != 0 && IsDccClear(uploadedKeys)) stored.assign(trackedLayers, true);
if (pendingResults) {
static std::atomic<int> reports{0};
if (reports.fetch_add(1) < 8) std::fprintf(stderr, "[gpu] storage image 0x%llx: guest memory changed while GPU results were pending; keeping the CPU's blocks\n", static_cast<unsigned long long>(descriptor.baseAddress));

View File

@ -4,6 +4,7 @@
#include "prx/libSceAgcDriver/Graphics/include/Resources.hpp"
#include "prx/libSceAgcDriver/Graphics/include/Texture.hpp"
#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp"
#include "prx/libc/include/GuestArena.hpp"
#include <algorithm>
#include <array>
#include <atomic>
@ -14,6 +15,7 @@
#include <map>
#include <mutex>
#include <stdexcept>
#include <utility>
namespace AgcDriver::Graphics {
@ -416,8 +418,8 @@ bool evictOne(Shadows& registry) {
}
bool UnitShadowEnabled() {
static const bool disabled = std::getenv("APS5_NO_UNIT_SHADOW") != nullptr;
return !disabled;
static const bool enabled = std::getenv("APS5_NO_UNIT_SHADOW") == nullptr && GuestArena::GuestArenaWriteWatched_nid_postfix();
return enabled;
}
bool ShadowVerify() {
@ -517,6 +519,13 @@ VkDeviceSize SlabOffset(const HostImport& import, const ShadowSlab& slab, std::u
std::optional<ShadowDestination> ShadowDestinationFor(const Context& context, const HostImport& import, std::uint64_t begin, std::uint64_t end) {
if (!UnitShadowEnabled() || end <= begin || begin < import.base || end > import.base + import.bytes) return std::nullopt;
static const auto arena = [] {
std::uintptr_t base = 0;
std::size_t size = 0;
GuestArena::GuestArenaRange_nid_postfix(&base, &size);
return std::pair<std::uint64_t, std::uint64_t>{base, size};
}();
if (arena.second == 0 || begin < arena.first || end > arena.first + arena.second) return std::nullopt;
auto& registry = Registry();
std::shared_ptr<UnitShadow> shadow;
std::size_t slabIndex = 0;

View File

@ -4,6 +4,8 @@
#include "prx/libSceAgcDriver/Graphics/include/Resources.hpp"
#include "prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp"
#include "prx/libSceAgcDriver/Graphics/include/Shaders.hpp"
#include "prx/libSceAgcDriver/Graphics/include/Texture.hpp"
#include "prx/libSceAgcDriver/Graphics/include/TextureDetiler.hpp"
#include "prx/libSceAgcDriver/Graphics/include/UnitShadow.hpp"
#include "prx/libSceAgcDriver/Execution/include/BdaFeatures.hpp"
#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp"
@ -22,6 +24,7 @@
#include <cstdlib>
#include <cstring>
#include <iostream>
#include <memory>
#include <mutex>
#include <string>
#include <thread>
@ -106,6 +109,7 @@ public:
function<PFN_vkGetPhysicalDeviceProperties>("vkGetPhysicalDeviceProperties")(context.physical, &properties);
context.limits = properties.limits;
context.bufferDeviceAddress = true;
context.formatProperties = function<PFN_vkGetPhysicalDeviceFormatProperties>("vkGetPhysicalDeviceFormatProperties");
context.Function<PFN_vkGetDeviceQueue>("vkGetDeviceQueue")(context.device, family, 0, &context.queue);
VkCommandPoolCreateInfo pool{VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO};
pool.queueFamilyIndex = family;
@ -709,7 +713,7 @@ void unitShadowTests(const Device& device, Recorder& recorder) {
const auto& context = device.GetContext();
if (!UnitShadowEnabled()) {
Require(!AnyShadowedOverlaps(0x10000, 16) && PublishShadow(0x10000, 16, PublishScope::Whole, PublishReason::Hook) == 0, "unit shadows are off but not inert");
std::cout << "unit shadows off (APS5_NO_UNIT_SHADOW): primitives inert\n";
std::cout << "unit shadows off (APS5_NO_UNIT_SHADOW, or an arena without write watching): primitives inert\n";
return;
}
if (context.hostImportAlignment == 0 || !GuestArena::GuestArenaAvailable_nid_postfix() || !GuestArena::GuestArenaWriteWatched_nid_postfix()) {
@ -893,6 +897,118 @@ void unitShadowTests(const Device& device, Recorder& recorder) {
else Require(words[128 * unit] == 0x77, "the eviction before the retire did not publish unit 128");
}
void storageRefreshTests(const Device& device, Recorder& recorder) {
const auto& base = device.GetContext();
if (base.hostImportAlignment == 0) {
std::cout << "host imports unavailable: storage refresh not tested\n";
return;
}
constexpr std::uint32_t side = 256;
constexpr std::size_t surfaceBytes = side * side * 4;
constexpr std::size_t keyCount = surfaceBytes / 256;
constexpr std::size_t bytes = surfaceBytes + 65536;
#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 storage refresh block");
auto* texels = static_cast<std::uint8_t*>(block);
auto* keys = texels + surfaceBytes;
std::memset(texels, 0x55, surfaceBytes);
std::memset(keys, 0x00, keyCount);
const auto address = reinterpret_cast<std::uint64_t>(block);
{
GuestAllocations::Mutation mutation;
mutation.Add(block, bytes, true, true);
}
struct Unregister {
const Context& context;
void* block;
std::uint64_t address;
~Unregister() {
{
GuestAllocations::Mutation mutation;
mutation.Remove(block);
}
HostImportFor(context, address, bytes);
}
} unregister{base, block, address};
const auto* import = HostImportFor(base, address, bytes);
if (import == nullptr) {
std::cout << "host import of the storage refresh block refused: storage refresh not tested\n";
return;
}
Require(!ShadowDestinationFor(base, *import, address, address + 65536).has_value(), "a unit shadow was offered for memory the write tracker does not watch");
if (!GuestArena::GuestArenaWriteWatched_nid_postfix()) Require(!UnitShadowEnabled(), "unit shadows are on without write watching");
TextureDetiler detiler(base);
auto context = base;
context.detiler = &detiler;
GuestTextureResource resource{};
resource.baseAddress = address;
resource.width = side;
resource.height = side;
resource.mipCount = 1;
resource.tileMode = TextureTileMode::kR64KBX;
resource.dimension = TextureDimension::k2D;
resource.format = 56;
resource.dstSelX = 4;
resource.dstSelY = 5;
resource.dstSelZ = 6;
resource.dstSelW = 7;
resource.dccAddress = address + surfaceBytes;
Require(DescribeSurface(resource).guestBytes == surfaceBytes, "the test surface has an unexpected size");
DccKeyProof proof;
Require(ProvedClearKeys(resource, surfaceBytes, proof) == DccKeys::Clear0000, "0x00 keys are not a 0000 clear");
{
auto image = std::make_shared<StorageTexture>(context, detiler, resource, 0);
const VkImageSubresourceRange range{VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1};
const auto draw = [&](VkClearColorValue value) {
const auto commands = recorder.Commands();
recorder.Keep(image);
RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_TRANSFER_WRITE_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT, VK_ACCESS_TRANSFER_WRITE_BIT);
context.Function<PFN_vkCmdClearColorImage>("vkCmdClearColorImage")(commands, image->Image(), VK_IMAGE_LAYOUT_GENERAL, &value, 1, &range);
RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_TRANSFER_READ_BIT | VK_ACCESS_TRANSFER_WRITE_BIT);
image->MarkDirty();
};
const auto holds = [&](std::array<std::uint8_t, 4> texel) {
Buffer readback(context, surfaceBytes, VK_BUFFER_USAGE_TRANSFER_DST_BIT);
const auto commands = recorder.Commands();
RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_TRANSFER_WRITE_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT, VK_ACCESS_TRANSFER_READ_BIT);
VkBufferImageCopy copy{};
copy.imageSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1};
copy.imageExtent = {side, side, 1};
context.Function<PFN_vkCmdCopyImageToBuffer>("vkCmdCopyImageToBuffer")(commands, image->Image(), VK_IMAGE_LAYOUT_GENERAL, readback.Handle(), 1, &copy);
RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_HOST_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_HOST_READ_BIT);
recorder.Submit();
device.WaitQueue();
recorder.Sync();
const auto pixels = readback.Bytes();
for (std::size_t i = 0; i < pixels.size(); ++i) {
if (std::to_integer<std::uint8_t>(pixels[i]) != texel[i % 4]) return false;
}
return true;
};
const auto memoryHolds = [&](std::array<std::uint8_t, 4> texel) {
for (std::size_t i = 0; i < surfaceBytes; ++i) {
if (texels[i] != texel[i % 4]) return false;
}
return true;
};
Require(holds({0, 0, 0, 0}), "a surface under 0000 clear keys was not cleared");
draw({{1.0f, 0.0f, 0.0f, 1.0f}});
image->Refresh();
Require(holds({255, 0, 0, 255}), "a refresh under the image's own clear keys dropped the draw's results");
Require(memoryHolds({255, 0, 0, 255}), "the refresh's store of the results did not reach guest memory");
draw({{0.0f, 1.0f, 0.0f, 1.0f}});
std::memset(keys, 0x40, keyCount);
image->Refresh();
Require(holds({0, 0, 0, 255}), "new 0001 clear keys did not clear the image");
Require(memoryHolds({255, 0, 0, 255}), "results dead under new clear keys were stored");
}
recorder.Sync();
}
// The data word positions of a dispatch-cache variant (Driver.cpp's data-only hits): leaves
// located among the runs' words, aliased, unaligned, out-of-run and mismatched ones skipped.
void dataWordPositionsTests() {
@ -972,6 +1088,7 @@ int main() {
resourceReadTests(device, recorder);
storeRunTests(device, recorder);
unitShadowTests(device, recorder);
storageRefreshTests(device, recorder);
dataWordPositionsTests();
dataRefreshTests(device, recorder);
std::cout << "Recorder read tracking and label tests passed\n";