diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index 029af5f1c8cb..8ba732e46ffa 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -2378,6 +2378,7 @@ if(BUILD_TESTING) set_tests_properties(agc_driver_pipelined_dispatch_item PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + set_tests_properties(agc_driver_queued_index_read_order PROPERTIES TIMEOUT 60) foreach(agcTest IN ITEMS agc_spirv_narrow_constant_store_tests agc_spirv13_image_tests agc_spirv_optimizer_tests agc_driver_constant_store_tests) if(NOT TARGET ${agcTest}) diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Draw/DrawPipeline.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Draw/DrawPipeline.hpp index ef6a3e2e7d5a..9ca8b03dd5e7 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Draw/DrawPipeline.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Draw/DrawPipeline.hpp @@ -32,7 +32,7 @@ class DrawPipeline { static void FollowEpoch(std::uint64_t token); static unsigned Committers(); - void Enqueue(Commit commit, std::vector writes, std::uint64_t labelAddress = 0, std::vector labelBytes = {}); + void Enqueue(Commit commit, std::vector writes, std::uint64_t labelAddress = 0, std::vector labelBytes = {}, std::vector reads = {}); void Drain(DrainReason reason, std::uint32_t opcode = 0x100); void Stop(); static std::uint64_t DrainRequestsByOpcode(std::uint32_t opcode); @@ -40,6 +40,8 @@ class DrawPipeline { std::size_t Outstanding() const { return outstanding.load(std::memory_order_acquire); } bool Overlaps(std::uint64_t address, std::size_t bytes); std::optional PendingLabel(std::uint64_t address, std::size_t bytes); + static bool HeldReads(std::uint64_t address, std::size_t bytes); + void ReleaseRunningReads(); private: struct Item { @@ -47,6 +49,7 @@ class DrawPipeline { std::vector writes; std::uint64_t labelAddress = 0; std::vector labelBytes; + std::vector reads; }; DrawPipeline() = default; void run(); diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Memory/DrawWriteRanges.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Memory/DrawWriteRanges.hpp index 85cc30100cc5..261fd2f6f549 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Memory/DrawWriteRanges.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Memory/DrawWriteRanges.hpp @@ -1,6 +1,7 @@ #ifndef CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_DRIVER_DRAWWRITERANGES_HPP #define CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_DRIVER_DRAWWRITERANGES_HPP +#include "prx/libSceAgcDriver/Execution/include/Pm4.hpp" #include "prx/libSceAgcDriver/Graphics/include/Shaders.hpp" #include "prx/libSceAgcDriver/Graphics/include/State.hpp" #include @@ -13,6 +14,7 @@ namespace AgcDriver::DriverDetail { std::vector> DrawWriteRanges(const Graphics::State& graphics, std::span stages, bool exact); std::vector> DispatchWriteRanges(const ShaderRecompiler::RecompileResult& compiled, bool& bounded); bool ExactDrawWrites(); +std::vector> QueuedDrawReadRanges(const Pm4::DrawParameters& draw, std::span stages); } diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/Draw.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/Draw.cpp index 7d11469870fd..be47da3113e0 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/Draw.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/Draw.cpp @@ -2,6 +2,7 @@ #include "prx/libSceAgcDriver/Execution/include/Driver/Diagnostics.hpp" #include "prx/libSceAgcDriver/Execution/include/Driver/Draw/DrawPipeline.hpp" #include "prx/libSceAgcDriver/Execution/include/Driver/Draw/DrawProgramTrace.hpp" +#include "prx/libSceAgcDriver/Execution/include/Driver/Memory/DrawWriteRanges.hpp" #include "prx/libSceAgcDriver/Execution/include/Driver/Synchronization/DeferredLabels.hpp" #include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" #include "prx/libSceAgcDriver/Execution/include/PerformanceTimer.hpp" @@ -602,13 +603,14 @@ DrawVerdict Driver::draw(QueueState& queue, std::span packe const auto commitQueue = submission.queue; const auto commitPacket = (packet[0] >> 8u) & 0xffu; const auto epoch = DrawPipeline::EpochToken().load(std::memory_order_relaxed); + auto reads = QueuedDrawReadRanges(drawParameters, stages); auto commit = [this, localDevice = std::move(localDevice), decode = std::move(decode), drawParameters, stages = std::move(stages), snapshots = std::move(snapshots), recipe = std::move(recipe), recipeStages = std::move(recipeStages), drawKey, commitQueue, commitPacket, epoch, programs = std::move(programs), results = std::move(results), memory = std::move(memory), linked = std::move(linked), stageCaptures = std::move(stageCaptures), matched = std::move(matched), matchedRegions = std::move(matchedRegions), fresh = std::move(fresh), decodeReads = std::move(decodeReads), shaderMemory = std::move(shaderMemory), entry = std::move(entry), dataEntry = std::move(dataEntry), compiledStages = std::move(compiledStages), stageData = std::move(stageData)] { DrawPipeline::FollowEpoch(epoch); GuestMemory::SetCurrentPacket(commitPacket, commitQueue); commitDraw(localDevice, commitQueue, decode->state, drawParameters, stages, snapshots, recipe, recipeStages, drawKey); }; auto held = std::make_shared(std::move(commit)); - DrawPipeline::Queue0().Enqueue([held] { (*held)(); }, std::move(writes)); + DrawPipeline::Queue0().Enqueue([held] { (*held)(); }, std::move(writes), 0, {}, std::move(reads)); phaseTiming.Phase(DrawRowLockWait); timing.Mark("draw_enqueued"); return drawn(); diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawPipeline.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawPipeline.cpp index 420ddb33a44c..78de2917372c 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawPipeline.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawPipeline.cpp @@ -22,6 +22,10 @@ std::uint64_t elapsedNs(std::chrono::steady_clock::time_point since) { return static_cast(std::chrono::duration_cast(std::chrono::steady_clock::now() - since).count()); } +std::atomic heldReadRanges{0}; +std::atomic heldReadQueries{0}; +std::atomic heldReadHits{0}; + } DrawPipeline& DrawPipeline::Queue0() { @@ -73,7 +77,7 @@ void DrawPipeline::rethrowFailure() { std::rethrow_exception(error); } -void DrawPipeline::Enqueue(Commit commit, std::vector writes, std::uint64_t labelAddress, std::vector labelBytes) { +void DrawPipeline::Enqueue(Commit commit, std::vector writes, std::uint64_t labelAddress, std::vector labelBytes, std::vector reads) { static const bool profile = std::getenv("APS5_PROFILE_DRAW") != nullptr; std::unique_lock lock(mutex); rethrowFailure(); @@ -93,7 +97,8 @@ void DrawPipeline::Enqueue(Commit commit, std::vector writes, std::uint64 } depthSum += items.size(); ++enqueued; - items.push_back({std::move(commit), std::move(writes), labelAddress, std::move(labelBytes)}); + heldReadRanges.fetch_add(reads.size(), std::memory_order_relaxed); + items.push_back({std::move(commit), std::move(writes), labelAddress, std::move(labelBytes), std::move(reads)}); outstanding.fetch_add(1, std::memory_order_release); if (!thread.joinable()) { thread = std::thread([this] { run(); }); @@ -126,7 +131,10 @@ void DrawPipeline::report(std::chrono::steady_clock::time_point now) { const auto perCommit = commits == 0 ? 0.0 : static_cast(commitNs) / 1e3 / static_cast(commits); const auto perDraw = enqueued == 0 ? 0.0 : static_cast(fullWaitNs) / 1e3 / static_cast(enqueued); const auto queued = enqueued == 0 ? 0.0 : static_cast(depthSum) / static_cast(enqueued); - std::fprintf(stderr, "[draw] pipelined (10 s, depth %zu): %llu commits at %.1f us each on the committer, %llu failed; %llu enqueued behind %.2f on average, waiting %.1f us per draw for room; drains:%s\n", Depth(), static_cast(commits), perCommit, static_cast(commitErrors), static_cast(enqueued), queued, perDraw, drainText.c_str()); + char reads[64]; + std::snprintf(reads, sizeof(reads), " %llu read ranges, %llu stores held", static_cast(heldReadRanges.load(std::memory_order_relaxed)), static_cast(heldReadHits.exchange(0, std::memory_order_relaxed))); + drainText += reads; + std::fprintf(stderr, "[draw] pipelined (10 s, depth %zu): %llu commits at %.1f us each on the committer, %llu failed; %llu enqueued behind %.2f on average, waiting %.1f us per draw for room;%s; drains:%s\n", Depth(), static_cast(commits), perCommit, static_cast(commitErrors), static_cast(enqueued), queued, perDraw, reads, drainText.c_str()); commits = commitNs = commitErrors = enqueued = fullWaitNs = depthSum = 0; drains = {}; drainWaitNs = {}; @@ -211,6 +219,27 @@ std::uint64_t DrawPipeline::DrainRequestsByOpcode(std::uint32_t opcode) { return Queue0().drainRequests[std::min(opcode, 0x100)].load(std::memory_order_relaxed); } +bool DrawPipeline::HeldReads(std::uint64_t address, std::size_t bytes) { + if (bytes == 0 || std::getenv("APS5_NO_QUEUED_DRAW_READ_ORDER") != nullptr) return false; + const auto end = address + bytes; + auto& pipeline = Queue0(); + std::lock_guard lock(pipeline.mutex); + heldReadQueries.fetch_add(1, std::memory_order_relaxed); + for (const auto& item : pipeline.items) { + for (const auto& [begin, limit] : item.reads) { + if (address >= limit || begin >= end) continue; + heldReadHits.fetch_add(1, std::memory_order_relaxed); + return true; + } + } + return false; +} + +void DrawPipeline::ReleaseRunningReads() { + std::lock_guard lock(mutex); + if (!items.empty()) items.front().reads.clear(); +} + std::optional DrawPipeline::PendingLabel(std::uint64_t address, std::size_t bytes) { if (bytes == 0 || bytes > 8) return std::nullopt; const auto end = address + bytes; diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Memory/WrittenBuffers.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Memory/WrittenBuffers.cpp index 47b88455e4ab..47e17bfdd876 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Memory/WrittenBuffers.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Memory/WrittenBuffers.cpp @@ -4,10 +4,13 @@ #include "prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp" #include "prx/libSceAgcDriver/Graphics/include/GuestTextureResource.hpp" #include "prx/libSceAgcDriver/Graphics/include/TextureTiling.hpp" +#include "prx/libSceAgcDriver/Graphics/include/VertexInput.hpp" #include "Optimization/ShaderStageInputInfo.hpp" #include #include #include +#include +#include namespace AgcDriver::DriverDetail { @@ -156,6 +159,40 @@ std::vector> Driver::drawWriteRanges(con return DrawWriteRanges(graphics, stages, ExactDrawWrites()); } +std::vector> QueuedDrawReadRanges(const Pm4::DrawParameters& draw, std::span stages) { + std::vector> ranges; + const auto add = [&](std::uint64_t begin, std::uint64_t bytes) { + if (begin == 0 || bytes == 0 || bytes > std::numeric_limits::max() - begin) return; + ranges.emplace_back(begin, begin + bytes); + }; + if (draw.indexed && draw.indexSize != 0 && draw.indexCount != 0) add(draw.indexAddress, static_cast(draw.indexCount) * draw.indexSize); + if (stages.empty()) return ranges; + for (const auto& stage : stages) { + if (stage.program == nullptr) continue; + for (const auto& binding : stage.program->bindings) { + if (binding.role != ShaderRecompiler::DescriptorRole::GuestBuffers || binding.guestDescriptor.size() < static_cast(binding.count) * 4u) continue; + for (std::uint32_t element = 0; element < binding.count; ++element) { + const auto* words = binding.guestDescriptor.data() + static_cast(element) * 4u; + const ShaderRecompiler::ShaderBufferResource descriptor{{words[0], words[1], words[2], words[3]}}; + add(descriptor.Base48(), descriptor.GetSize()); + } + } + } + if (stages.empty() || stages.front().program == nullptr) return ranges; + for (const auto& attribute : stages.front().program->vertexAttributes) { + if (Graphics::NullVertexDescriptor(attribute) || Graphics::VertexFetchOutOfRange(attribute)) continue; + const auto address = attribute.resource.fields[0] | (static_cast(attribute.resource.fields[1] & 0xffffu) << 32u); + std::uint64_t bytes = 0; + try { + bytes = Graphics::VertexBufferExtent(attribute); + } catch (const std::runtime_error&) { + continue; + } + add(address, bytes); + } + return ranges; +} + std::optional Driver::newestWriterLocked(std::uint64_t begin, std::uint64_t end) const { for (auto it = writtenBuffers.rbegin(); it != writtenBuffers.rend(); ++it) { if (begin < it->end && it->begin < end) return *it; diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketMemory.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketMemory.cpp index bb8a031b3819..aa5fb2a23dc7 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketMemory.cpp @@ -311,6 +311,7 @@ bool Driver::enqueueDmaPacket(std::span packet, std::uint32 const auto copy = Pm4::DecodeMemoryCopy(packet); if (!copy.has_value() || copy->destination % 4 != 0 || copy->bytes % 4 != 0 || !GuestMemory::Accessible(reinterpret_cast(copy->source), copy->bytes) || !GuestMemory::Accessible(reinterpret_cast(copy->destination), copy->bytes, true)) return false; DrawPipeline::Queue0().Enqueue([this, queue, copy = *copy] { + DrawPipeline::Queue0().ReleaseRunningReads(); GuestMemory::SetCurrentPacket(0x50, queue); constexpr std::size_t gpuStoreLimit = 65536; if (copy.bytes > gpuStoreLimit) { @@ -326,7 +327,7 @@ bool Driver::enqueueDmaPacket(std::span packet, std::uint32 std::vector bytes(copy.bytes); GuestMemory::Read(copy.source, bytes, 1); commitDmaStore(queue, copy.destination, bytes); - }, {{copy->destination, copy->destination + copy->bytes}}); + }, {{copy->destination, copy->destination + copy->bytes}}, 0, {}, {{copy->source, copy->source + copy->bytes}}); return true; } diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Synchronization/WaitMemory.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Synchronization/WaitMemory.cpp index ef7a88bb2f72..1363a3b1df63 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Synchronization/WaitMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Synchronization/WaitMemory.cpp @@ -327,6 +327,7 @@ void Driver::waitMemory(std::span packet, std::uint32_t que ~BlockedWait() { driver.noteWaitBlocked(queue, 0, false); } } blocked{*this, queue}; static const bool pauseSpin = std::getenv("APS5_NO_PAUSE_SPIN") == nullptr; + static const bool storeHeldResults = std::getenv("APS5_NO_QUEUED_DRAW_READ_ORDER") == nullptr; const auto spinLimit = queue == 0 ? std::chrono::microseconds(1500) : std::chrono::microseconds(100); const auto spinStart = std::chrono::steady_clock::now(); bool spinning = pauseSpin; @@ -350,6 +351,7 @@ void Driver::waitMemory(std::span packet, std::uint32_t que } CheckFailure(); if (pollService(!spinning)) return; + if (storeHeldResults && !spinning && (polls & 31u) == 0) GuestMemory::FlushGpuWrites(awaited, awaitedBytes); noteAwaitingTitle(queue, awaited); if (requireMemory && (polls & 15u) == 0) { std::unique_lock settleLock(GuestMemory::GpuMutex(), std::try_to_lock); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp index 9e15260b3e89..209907de3c0e 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp @@ -1,6 +1,7 @@ #include "prx/libSceAgcDriver/Execution/include/AsyncLog.hpp" #include "prx/libSceAgcDriver/Execution/include/ProfileOutput.hpp" #include "prx/libSceAgcDriver/Execution/include/CaptureTrace.hpp" +#include "prx/libSceAgcDriver/Execution/include/Driver/Draw/DrawPipeline.hpp" #include "prx/libSceAgcDriver/Graphics/include/Texture.hpp" #include "prx/libSceAgcDriver/Graphics/include/SlimMutex.hpp" #include "prx/libc/include/General.hpp" @@ -94,6 +95,7 @@ std::atomic partialUploads{0}, partialUploadBytes{0}, partialWrit // writeBackLayers), and pending units a DCC clear -> uncompressed key flip kept as the texels // (Refresh). std::atomic pretestSkipped{0}, unregisteredDropped{0}, keyFlipKept{0}; +std::atomic queuedReadHeld{0}; // Images stored by a FlushPending after a hook skip of theirs (AccessKeptByCpu), of which by the // hook for the read site of the last skip; images evictStale dropped (the [hooksync] and [storage] lines). std::atomic flushedAfterSkip{0}, flushedAfterSkipSameSite{0}, staleEvicted{0}; @@ -140,7 +142,7 @@ void reportStorageTraffic(StorageTraffic& traffic) { const auto take = [](std::atomic& counter) { return static_cast(counter.exchange(0, std::memory_order_relaxed)); }; const auto partialUploadCount = take(partialUploads), partialWriteBackCount = take(partialWriteBacks); const auto uploadMiB = take(partialUploadBytes) / 1048576.0, writeBackMiB = take(partialWriteBackBytes) / 1048576.0; - AgcDriver::ProfilePrint_nid_no_patch( "[storage] uploads by path (count/MiB, 10 s):%s; %llu write-backs GPU-direct, %llu stored nothing; block units: partial uploads %llu/%.1f, partial write-backs %llu/%.1f, %llu widened to every pending unit, units dropped %llu, superseded %llu, dropped unregistered %llu, kept over a key flip %llu%s, pretest skipped %llu, evicted stale %llu\n", line.c_str(), static_cast(traffic.directWriteBacks), take(emptyWriteBacks), partialUploadCount, uploadMiB, partialWriteBackCount, writeBackMiB, take(coalescedWriteBacks), take(unitsDropped), take(unitsSuperseded), take(unregisteredDropped), take(keyFlipKept), ShadowReport().c_str(), take(pretestSkipped), take(staleEvicted)); + AgcDriver::ProfilePrint_nid_no_patch( "[storage] uploads by path (count/MiB, 10 s):%s; %llu write-backs GPU-direct, %llu stored nothing; block units: partial uploads %llu/%.1f, partial write-backs %llu/%.1f, %llu widened to every pending unit, units dropped %llu, superseded %llu, dropped unregistered %llu, kept over a key flip %llu%s, pretest skipped %llu, held for a queued draw read %llu, evicted stale %llu\n", line.c_str(), static_cast(traffic.directWriteBacks), take(emptyWriteBacks), partialUploadCount, uploadMiB, partialWriteBackCount, writeBackMiB, take(coalescedWriteBacks), take(unitsDropped), take(unitsSuperseded), take(unregisteredDropped), take(keyFlipKept), ShadowReport().c_str(), take(pretestSkipped), take(queuedReadHeld), take(staleEvicted)); traffic.writeBacks.clear(); traffic.uploadReasons.clear(); traffic.directWriteBacks = 0; @@ -3640,6 +3642,10 @@ bool StorageTexture::pendingUnitInside(std::uint64_t address, std::size_t bytes) bool StorageTexture::FlushPending(std::uint64_t address, std::size_t bytes, const StorageTexture* except, const char* reason, PublishScope scope, bool* published) { if (published != nullptr) *published = false; + if (AgcDriver::DriverDetail::DrawPipeline::HeldReads(address, bytes)) { + queuedReadHeld.fetch_add(1, std::memory_order_relaxed); + return false; + } // The images stay alive across the scan: their last owner may be a batch's kept list, which // another thread releases outside the GPU mutex once the batch completed. std::vector> flush; diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/QueuedIndexReadOrder.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/QueuedIndexReadOrder.cpp new file mode 100644 index 000000000000..acc2b1617a78 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/QueuedIndexReadOrder.cpp @@ -0,0 +1,432 @@ +#include "prx/libSceAgcDriver/Execution/include/Driver.hpp" +#include "prx/libSceAgcDriver/Execution/include/Driver/Draw/DrawPipeline.hpp" +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Texture.hpp" +#include "prx/libSceAgcDriver/Submit/include/Dcb.hpp" +#include "prx/libc/include/GuestAllocations.hpp" +#include "prx/libc/include/GuestArena.hpp" +#include "prx/libc/include/GuestWriteWatch.hpp" +#include "VulkanTestDevice.hpp" +#ifdef _WIN32 +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#else +#include +#endif +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; + +constexpr std::uint32_t Width = 192; +constexpr std::uint32_t Height = 128; +constexpr std::uint32_t DistanceVector = 0x00400000u; +constexpr std::uint32_t SetContextReg = 0x69u; +constexpr std::uint32_t SetShReg = 0x76u; +constexpr std::uint32_t SetUconfigReg = 0x79u; +constexpr std::uint32_t DrawIndexOffset2Header = 0xc0033500u; + +constexpr std::size_t Block = 65536; +constexpr std::size_t PoolBytes = 2 * Block; +constexpr std::size_t ColorBytes = Width * Height * 4; +constexpr std::uint32_t FormatRgba16Float = 71; +constexpr std::uint32_t Type2D = 9; +constexpr std::uint32_t Standard64KB = 0x09; +constexpr std::uint32_t ImageWidth = 128; +constexpr std::uint32_t ImageHeight = 120; +constexpr std::uint8_t Sentinel = 0xc7; +constexpr std::array Indices{0, 1, 2}; +constexpr std::size_t VertexOffset = 0x2000; + +std::byte* ColorMemory = nullptr; +std::byte* Pool = nullptr; +std::byte* Copied = nullptr; + +alignas(256) constexpr std::array VertexCode{ + 0xe0382000, 0x80020005, 0xe0382010, 0x80020405, 0xbf8c3f70, + 0xf80000cf, 0x03020100, 0xf80008df, 0x07060504, 0xbf810000, +}; + +alignas(256) constexpr std::array PixelCode{ + 0x7e0002f2, 0x7e0202f2, 0x7e0402f2, 0x7e0602f2, 0xf800080f, 0x03020100, 0xbf810000, +}; + +alignas(256) constexpr std::array ImageWriteCode{ + 0xd7460004u, 0x0401060eu, 0xd7460005u, 0x0405060fu, 0x7e100d04u, 0x7e120d05u, + 0x7e140c0du, 0x7e1602f2u, 0xf0200f08u, 0x00000804u, 0xbf810000u, +}; + +struct Vertex { + std::array position; + std::array distances; +}; + +std::array Vertices{}; + +constexpr std::array PositionX{-3.0f, 1.0f, 1.0f}; +constexpr std::array PositionY{-1.0f, -1.0f, 3.0f}; +constexpr std::array Negative{-1.0f, -1.0f, -1.0f}; + +struct RegisteredShader { + Shader shader; + ShaderUserData userData; +}; +RegisteredShader VertexShader{}; +RegisteredShader PixelShader{}; + +const std::vector> ContextBaseline{ + {0x2d5, 0x2000}, + {0x1b6, 0}, {0x200, 0}, {0x203, 0x800}, + {0x2dc, 0xaa00}, {0x2f8, 0}, {0x292, 2}, {0x293, 0}, + {0x80, 0}, {0x8d, 0}, {0x83, 0xffff}, {0x8c, 0xa}, + {0x2f9, 0x2d}, {0x313, 0x6000}, {0x30e, 0xffffffff}, {0x30f, 0xffffffff}, + {0x206, 0x43f}, {0x204, 0x80000}, {0x205, 0x240}, + {0x8e, 0xf}, {0x8f, 0xf}, {0x202, 0xcc0010}, + {0x1c4, 0}, {0x1c5, 9}, {0x1c3, 4}, {0x31c, 0x28028}, + {0x31b, 0}, {0x31d, 0}, {0x3b8, 0x9000000}, {0x1e0, 0}, + {0xc, 0}, + {0x81, 0x80000000}, {0x90, 0x80000000}, {0x94, 0x80000000}, + {0xb4, 0}, {0xb5, std::bit_cast(1.0f)}, {0x114, 0}, {0x113, std::bit_cast(1.0f)}, +}; + +std::byte* AllocateWatched(std::size_t bytes) { +#ifdef _WIN32 + void* block = GuestArena::GuestArenaAllocate_nid_postfix(bytes, Block); + GuestArena::GuestArenaCommit_nid_postfix(block, bytes, PAGE_READWRITE, bytes); +#else + void* raw = mmap(nullptr, bytes + Block, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + if (raw == MAP_FAILED) throw std::runtime_error("queued index read: cannot map the watched block"); + const auto begin = reinterpret_cast(raw); + const auto aligned = (begin + Block - 1) & ~static_cast(Block - 1); + if (aligned != begin) munmap(raw, aligned - begin); + if (aligned + bytes != begin + bytes + Block) munmap(reinterpret_cast(aligned + bytes), begin + Block - aligned); + void* block = reinterpret_cast(aligned); + GuestWriteWatch::GuestWriteWatchRegister_nid_postfix(block, bytes); +#endif + return static_cast(block); +} + +void Register(RegisteredShader& registered, const void* code, std::uint32_t bytes, std::uint8_t type) { + registered.shader.file_header = 0x34333231u; + registered.shader.version = 0x18u; + registered.shader.user_data = ®istered.userData; + registered.shader.code = code; + registered.shader.header_size = sizeof(RegisteredShader); + registered.shader.shader_size = bytes; + registered.shader.type = type; + AgcDriverRegisterShader_nid_postfix(®istered.shader); +} + +std::array BufferDescriptor(const void* data, std::uint32_t stride, std::uint32_t count) { + const auto address = reinterpret_cast(data); + return {static_cast(address), static_cast((address >> 32u) & 0xffffu) | (stride << 16u), count, 0x01016facu}; +} + +void SetRegister(std::vector& words, std::uint32_t opcode, std::uint32_t offset, std::uint32_t value) { + words.push_back(0xc0010000u | (opcode << 8u)); + words.push_back(offset); + words.push_back(value); +} + +std::array ImageDescriptor(const void* texels) { + const auto address = reinterpret_cast(texels); + return { + static_cast(address >> 8u), + static_cast((address >> 40u) & 0xffu) | (FormatRgba16Float << 20u) | (((ImageWidth - 1u) & 3u) << 30u), + ((ImageWidth - 1u) >> 2u) | ((ImageHeight - 1u) << 14u), + 0xfacu | (Standard64KB << 20u) | (Type2D << 28u), + 0u, 0u, 0u, 0u, + }; +} + +std::vector ImageWrite() { + const auto descriptor = ImageDescriptor(Pool); + std::vector data(descriptor.begin(), descriptor.end()); + data.resize(14, 0u); + data[13] = 7u; + std::vector words; + const auto registers = [&](std::uint32_t first, std::span values) { + words.push_back(0xc0007600u | (static_cast(values.size()) << 16u)); + words.push_back(first); + words.insert(words.end(), values.begin(), values.end()); + }; + const std::array shape{8, 8, 1}; + registers(0x207, shape); + const auto code = reinterpret_cast(ImageWriteCode.data()); + const std::array program{static_cast(code >> 8u), static_cast(code >> 40u)}; + registers(0x20c, program); + const std::array resource{(static_cast(data.size()) << 1u) | (7u << 7u) | (1u << 11u)}; + registers(0x213, resource); + registers(0x240, data); + words.insert(words.end(), {0xc0031500u, shape[0], shape[1], shape[2], 0x41u}); + return words; +} + +std::vector Draw(const void* vertices, std::size_t count) { + const auto colorAddress = reinterpret_cast(ColorMemory); + const auto poolAddress = reinterpret_cast(Pool); + const auto vertexAddress = reinterpret_cast(VertexCode.data()); + const auto pixelAddress = reinterpret_cast(PixelCode.data()); + std::vector words; + for (const auto& [offset, value] : ContextBaseline) SetRegister(words, SetContextReg, offset, value); + SetRegister(words, SetContextReg, 0x207, DistanceVector); + SetRegister(words, SetContextReg, 0x318, static_cast(colorAddress >> 8u)); + SetRegister(words, SetContextReg, 0x390, static_cast(colorAddress >> 40u)); + SetRegister(words, SetContextReg, 0x3b0, ((Width - 1u) << 14u) | (Height - 1u)); + SetRegister(words, SetContextReg, 0x10f, std::bit_cast(Width / 2.0f)); + SetRegister(words, SetContextReg, 0x110, std::bit_cast(Width / 2.0f)); + SetRegister(words, SetContextReg, 0x111, std::bit_cast(-(Height / 2.0f))); + SetRegister(words, SetContextReg, 0x112, std::bit_cast(Height / 2.0f)); + const auto extent = (Height << 16u) | Width; + SetRegister(words, SetContextReg, 0xd, extent); + SetRegister(words, SetContextReg, 0x82, extent); + SetRegister(words, SetContextReg, 0x91, extent); + SetRegister(words, SetContextReg, 0x95, extent); + SetRegister(words, SetUconfigReg, 0x242, 4u); + SetRegister(words, SetShReg, 0x008, static_cast(pixelAddress >> 8u)); + SetRegister(words, SetShReg, 0x009, static_cast(pixelAddress >> 40u)); + SetRegister(words, SetShReg, 0x00b, 0u); + SetRegister(words, SetShReg, 0x0c8, static_cast(vertexAddress >> 8u)); + SetRegister(words, SetShReg, 0x0c9, static_cast(vertexAddress >> 40u)); + SetRegister(words, SetShReg, 0x08b, 4u << 1u); + const auto descriptor = BufferDescriptor(vertices, sizeof(Vertex), static_cast(count)); + for (std::uint32_t i = 0; i < descriptor.size(); ++i) SetRegister(words, SetShReg, 0x08c + i, descriptor[i]); + words.insert(words.end(), {0xc0012600u, static_cast(poolAddress), static_cast(poolAddress >> 32u)}); + words.insert(words.end(), {0xc0001300u, static_cast(Indices.size())}); + SetRegister(words, SetUconfigReg, 0x243, 0x401u); + words.insert(words.end(), {DrawIndexOffset2Header, static_cast(Indices.size()), 0u, static_cast(Indices.size()), 0u}); + return words; +} + +void Submit(const std::vector& words, const char* what) { + Packet packet{const_cast(words.data()), static_cast(words.size()), 0, {}}; + Require(sceAgcDriverSubmitDcb(&packet) == 0, std::string("queued index read: the submission failed for ") + what); +} + +std::vector CopyPool(std::size_t bytes) { + const auto source = reinterpret_cast(Pool); + const auto destination = reinterpret_cast(Copied); + return {0xc0055000u, 0u, static_cast(source), static_cast(source >> 32u), static_cast(destination), static_cast(destination >> 32u), static_cast(bytes)}; +} + +void RequireCopiedImage(std::size_t bytes, const char* phase) { + std::size_t differing = 0; + for (std::size_t i = 0; i < bytes; ++i) differing += Copied[i] != Pool[i] ? 1u : 0u; + Require(differing == 0, std::string("queued index read: ") + phase + ": " + std::to_string(differing) + " of " + std::to_string(bytes) + " copied bytes miss the image's results over the copy source"); +} + +std::vector WaitRegMem(std::uint64_t address, std::uint32_t reference) { + return {0xc0053c00u, 0x13u, static_cast(address), static_cast(address >> 32u), reference, 0xffffffffu, 0u}; +} + +bool IndicesIntact() { + return std::equal(Indices.begin(), Indices.end(), reinterpret_cast(Pool)); +} + +bool VerticesIntact() { + return std::memcmp(Pool + VertexOffset, Vertices.data(), sizeof(Vertex) * Vertices.size()) == 0; +} + +std::uint32_t PoolWord(std::size_t offset) { + return *reinterpret_cast(Pool + offset); +} + +void ResetPool() { + std::memset(Pool, Sentinel, PoolBytes); + std::memcpy(Pool, Indices.data(), Indices.size() * sizeof(std::uint32_t)); + std::memcpy(Pool + VertexOffset, Vertices.data(), sizeof(Vertex) * Vertices.size()); +} + +std::size_t WrittenPixels() { + std::vector colors(ColorBytes); + AgcDriver::GuestMemory::Read(reinterpret_cast(reinterpret_cast(ColorMemory)), colors); + std::size_t written = 0; + for (std::uint32_t pixel = 0; pixel < Width * Height; ++pixel) { + const auto value = std::to_integer(colors[pixel * 4u]); + Require(value == 0u || value == 255u, "queued index read: pixel " + std::to_string(pixel) + " stored " + std::to_string(value)); + written += value == 255u ? 1u : 0u; + } + return written; +} + +void RequireTriangle(const char* phase) { + const auto written = WrittenPixels(); + Require(written == Width * Height, std::string("queued index read: ") + phase + ": the draw left " + std::to_string(Width * Height - written) + " pixels cleared, expected the triangle its indices and vertices name"); +} + +void LandPool() { + const auto landBy = std::chrono::steady_clock::now() + std::chrono::seconds(5); + do { + AgcDriver::GuestMemory::FlushGpuWrites(reinterpret_cast(reinterpret_cast(Pool)), PoolBytes); + AgcDriverWaitIdle_nid_postfix(); + } while (IndicesIntact() && std::chrono::steady_clock::now() < landBy); + Require(!IndicesIntact(), "queued index read: the image's results never reached the memory they cover, so the run above proves nothing"); +} + +void WaitForWord(std::uint64_t address, std::uint32_t value, const char* what) { + const auto start = std::chrono::steady_clock::now(); + const auto deadline = start + std::chrono::seconds(10); + while (*reinterpret_cast(address) != value) { + if (std::chrono::steady_clock::now() >= deadline) { + char message[256]; + std::snprintf(message, sizeof(message), "queued index read: %s: 0x%08llx held 0x%08x after %.1f s, expected 0x%08x", what, static_cast(address), *reinterpret_cast(address), std::chrono::duration(std::chrono::steady_clock::now() - start).count(), value); + Require(false, message); + } + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + } +} + +std::atomic GateOpen{true}; + +void GateUntilQueued() { + AgcDriver::DriverDetail::DrawPipeline::Queue0().Enqueue([] { + const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(10); + while (AgcDriver::DriverDetail::DrawPipeline::QueuedItems() < 2 || !GateOpen.load()) { + if (std::chrono::steady_clock::now() >= deadline) return; + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + } + }, {}); +} + +void WaitForCommits() { + const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(10); + while (AgcDriver::DriverDetail::DrawPipeline::QueuedItems() != 0) { + Require(std::chrono::steady_clock::now() < deadline, "queued index read: the draw commit never ran"); + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + } +} + +void WaitForHeldRange(std::uint64_t address, std::size_t bytes, const char* what) { + const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(10); + while (!AgcDriver::DriverDetail::DrawPipeline::HeldReads(address, bytes) && std::chrono::steady_clock::now() < deadline) std::this_thread::sleep_for(std::chrono::milliseconds(1)); + const bool held = AgcDriver::DriverDetail::DrawPipeline::HeldReads(address, bytes); + GateOpen = true; + Require(held, std::string("queued index read: the queued draw holds no read of its ") + what + " range"); +} + +void CopyPendingImage(std::size_t bytes, const char* phase) { + const auto poolAddress = reinterpret_cast(reinterpret_cast(Pool)); + ResetPool(); + std::memset(Copied, 0, PoolBytes); + Submit(ImageWrite(), phase); + AgcDriverWaitIdle_nid_postfix(); + Require(AgcDriver::Graphics::StorageTexture::AnyPending(), std::string("queued index read: ") + phase + ": the image write left no pending result to order against"); + GateOpen = false; + GateUntilQueued(); + Submit(CopyPool(bytes), phase); + WaitForHeldRange(poolAddress, bytes, "copy source"); + WaitForCommits(); + AgcDriverWaitIdle_nid_postfix(); + LandPool(); + RequireCopiedImage(bytes, phase); +} + +} + +int main() { +#ifdef _WIN32 + _putenv_s("APS5_PIPELINED_DRAWS", "1"); + _putenv_s("APS5_PIPELINE_DMA", "1"); +#else + setenv("APS5_PIPELINED_DRAWS", "1", 1); + setenv("APS5_PIPELINE_DMA", "1", 1); +#endif + try { + { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + } + ColorMemory = AllocateWatched(2 * Block); + Pool = AllocateWatched(PoolBytes); + Copied = AllocateWatched(PoolBytes); + GuestAllocations::Mutation().Add(Pool, PoolBytes, true, true, true); + GuestAllocations::Mutation().Add(Copied, PoolBytes, true, true, true); + Register(VertexShader, VertexCode.data(), sizeof(VertexCode), 2); + Register(PixelShader, PixelCode.data(), sizeof(PixelCode), 1); + std::memset(Pool, Sentinel, PoolBytes); + std::memcpy(Pool, Indices.data(), Indices.size() * sizeof(std::uint32_t)); + for (std::size_t i = 0; i < Vertices.size(); ++i) Vertices[i] = {{PositionX[i], PositionY[i], 0.5f, 1.0f}, {Negative[i], Negative[i], Negative[i], 0.0f}}; + std::memcpy(Pool + VertexOffset, Vertices.data(), sizeof(Vertex) * Vertices.size()); + + const auto poolAddress = reinterpret_cast(reinterpret_cast(Pool)); + const auto vertexAddress = poolAddress + VertexOffset; + const std::array, 1> poolRange{{ + {poolAddress, static_cast(PoolBytes)}}}; + Submit(ImageWrite(), "the image write"); + AgcDriverWaitIdle_nid_postfix(); + Require(AgcDriver::Graphics::StorageTexture::AnyPending(), "queued index read: the image write left no pending result to order against"); + Require(IndicesIntact(), "queued index read: the image's results reached guest memory before any consumer asked"); + GateUntilQueued(); + std::fill_n(ColorMemory, ColorBytes, std::byte{0}); + Submit(Draw(Vertices.data(), Vertices.size()), "the indexed draw"); + WaitForCommits(); + Require(IndicesIntact(), "queued index read: the queued draw's commit stored the image's results over its index buffer"); + AgcDriverWaitIdle_nid_postfix(); + RequireTriangle("the index read"); + LandPool(); + + ResetPool(); + Submit(ImageWrite(), "the second image write"); + AgcDriverWaitIdle_nid_postfix(); + Require(AgcDriver::Graphics::StorageTexture::AnyPending(), "queued index read: the second image write left no pending result to order against"); + GateOpen = false; + GateUntilQueued(); + std::fill_n(ColorMemory, ColorBytes, std::byte{0}); + Submit(Draw(Pool + VertexOffset, Vertices.size()), "the draw whose vertices the image covers"); + WaitForHeldRange(vertexAddress, sizeof(Vertex) * Vertices.size(), "vertex"); + WaitForCommits(); + AgcDriverWaitIdle_nid_postfix(); + RequireTriangle("the vertex read"); + LandPool(); + + std::array texels{}; + for (std::size_t i = 0; i < texels.size(); ++i) texels[i] = PoolWord(i * sizeof(std::uint32_t)); + const auto sentinelWord = static_cast(Sentinel) * 0x01010101u; + std::size_t awaited = texels.size(); + for (std::size_t i = 0; i < texels.size(); ++i) { + if (texels[i] != Indices[i] && texels[i] != sentinelWord) { + awaited = i; + break; + } + } + Require(awaited != texels.size(), "queued index read: the image's first texels repeat the indices and the sentinel, so a wait on them proves nothing"); + const auto awaitedAddress = poolAddress + awaited * sizeof(std::uint32_t); + ResetPool(); + + Submit(ImageWrite(), "the third image write"); + AgcDriverWaitIdle_nid_postfix(); + Require(AgcDriver::Graphics::StorageTexture::AnyPending(), "queued index read: the third image write left no pending result to order against"); + std::fill_n(ColorMemory, ColorBytes, std::byte{0}); + Submit(Draw(Pool + VertexOffset, Vertices.size()), "the draw a wait behind it reads the held range of"); + Submit(WaitRegMem(awaitedAddress, texels[awaited]), "the wait over the held range"); + WaitForWord(awaitedAddress, texels[awaited], "the wait over the held range was not satisfied"); + AgcDriverWaitIdle_nid_postfix(); + RequireTriangle("the wait over the held range"); + Require(!VerticesIntact(), "queued index read: the image's results never reached the memory the wait covered"); + CopyPendingImage(4096, "the queued copy of the image"); + std::puts("queued index read vs. deferred storage write-back tests passed"); + AgcDriverShutdown_nid_postfix(); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + AgcDriverShutdown_nid_postfix(); + return 1; + } +} diff --git a/docs/dev/ENVIRONMENT.md b/docs/dev/ENVIRONMENT.md index 6c0cd7f81503..e148b5133db7 100644 --- a/docs/dev/ENVIRONMENT.md +++ b/docs/dev/ENVIRONMENT.md @@ -272,6 +272,7 @@ The code reads environment names through `getenv`, `IsEnvironmentSet`, `kibSetti | `APS5_NO_PRESENT_EPOCH` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp` | | keeps the presenter at collect epoch 0 instead of adopting the newest epoch a worker started | | `APS5_NO_PRESYNC` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp` | | disables the stage-A pre-sync of pending writes | | `APS5_NO_PROC_TABLE` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp` | | resolves the device's Vulkan entry points per call instead of filling the function table once | +| `APS5_NO_QUEUED_DRAW_READ_ORDER` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawPipeline.cpp` | | stops queued draws and DMA copies from holding back write-backs over the guest memory they read at commit | | `APS5_NO_QUEUED_LABELS` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Synchronization/DeferredLabels.cpp` | | disables the queued-label table | | `APS5_NO_RECORDED_CLEAR` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp` | | waits for a storage image's fast clear in a batch of its own instead of recording it | | `APS5_NO_RECORDER_SYNC_DRAWS` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp` | | gives the draws the recorder waits for their own batch instead of recording them with the wait | diff --git a/docs/dev/debt/libSceAgcDriver.md b/docs/dev/debt/libSceAgcDriver.md index e8afc7eb4bbd..ff966bf7dc05 100644 --- a/docs/dev/debt/libSceAgcDriver.md +++ b/docs/dev/debt/libSceAgcDriver.md @@ -140,7 +140,7 @@ - [Custom MSAA sample locations](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) support a uniform 1x1 pattern for 2, 4 or 8 samples through VK_EXT_sample_locations. Patterns varying across the guest's 2x2 pixel grid use a 2x2 sample location grid, anchored at the framebuffer origin like the guest's while the window offset is zero, on devices whose `maxSampleLocationGridSize` for the sample count is a multiple of 2x2. On other devices every pixel takes the pattern's pixel (0,0) locations (`PA_SC_AA_SAMPLE_LOCS_PIXEL_X0Y0_*`, its first registers and the corner the grid is anchored at), and the first such pipeline of each sample count logs `[gpu] no 2x2 sample location grid`. The RTX 5070 reports 2x4 for 2 samples, 2x2 for 4 and 1x2 for 8; an AMD Radeon integrated GPU reports 2x2 for each; an RTX 3070 Laptop GPU (driver 617.42) reports 1x1 for every count. With that stand-in the top-left pixel of every 2x2 quad matches the console and the other three use its positions. A pattern that only reorders one set of positions between pixels, as a 2x checkerboard pattern puts its samples at x = +4/16 and -4/16 and swaps them on odd rows, keeps every position and the number of samples a primitive covers in each pixel, so a resolve that averages a pixel's samples, or takes their minimum or maximum, sees the console's values. Sample i of an odd row then holds what the console stores in sample 1 - i: a pass that reads the samples by index, as checkerboard reconstruction does, exchanges the two half-pixels of odd rows, which can show as combing along vertical edges, and per-sample shading, sample-mask exports and coverage inputs see the swapped indices too. A pattern whose pixels hold different positions draws pixel (0,0)'s positions in each pixel whose own differ. A device's pipelines and render pass begins take the same stand-in for a given pattern, so a sample-location-compatible depth image is not drawn with one set of locations and transitioned or tested with another. A multisampled rasterizer drawing into one-sample color targets, and a one-sample depth target if any, with MSAA_ENABLE set, no alpha-to-coverage and no coverage pixel input is drawn with one sample at sample 0's location in each pixel of the grid, as the targets keep only sample 0's coverage. Without PS_ITER_SAMPLE such a draw also refuses centroid inputs, and keeps PERSP_SAMPLE, LINEAR_SAMPLE and ANCILLARY: a pixel shaded once is taken to get sample 0's barycentrics and sample index 0. With PS_ITER_SAMPLE only sample 0's invocation reaches the targets, so its sample and centroid barycentrics are taken to be sample 0's and the draw is shaded once. Both are inferred, not measured. The pixel shader runs once per pixel with sample number 0, so ANCILLARY reads sample 0 and POS_X_FLOAT and POS_Y_FLOAT keep the position `SPI_BARYC_CNTL` names. PERSP_CENTER and LINEAR_CENTER inputs on this path are interpolated at sample 0's location on the host, where the console uses the pixel center; one-sample depth images are created with `SAMPLE_LOCATIONS_COMPATIBLE_DEPTH` when the device supports it, as multisampled ones already were; this is how VK_AMD_mixed_attachment_samples defines the case, and it is not measured on PS5 hardware. Other mixed sample counts throw. Devices without the required sample count, four subpixel bits or the full guest coordinate range refuse custom patterns. Depth images remain in GENERAL between draws, and render passes cannot continue across pattern changes. Guest offsets retain the existing decoder's signed four-bit, sixteenth-pixel interpretation; PS5 coverage and interpolation equivalence have not been measured on hardware. - [Single-sample scan into a multisampled depth target](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - a draw with `PA_SC_AA_CONFIG.MSAA_NUM_SAMPLES` = 0 and `PA_SC_MODE_CNTL_0.MSAA_ENABLE` clear into a multisampled depth target rasterizes the depth target's sample count with every sample at the pixel center, as a single-sample scan into a multisampled color target already does. radeonsi programs this `MSAA_ENABLE` and `PA_SC_AA_CONFIG` with the depth buffer's own sample count when GL multisampling is disabled on a multisampled framebuffer ([L1066-L1068](https://github.com/openbsd/xenocara/blob/1b39033df8d928ec247ca64448c4a63c72302db7/lib/mesa/src/gallium/drivers/radeonsi/si_state.c#L1066-L1068), [L3485-L3512](https://github.com/openbsd/xenocara/blob/1b39033df8d928ec247ca64448c4a63c72302db7/lib/mesa/src/gallium/drivers/radeonsi/si_state.c#L3485-L3512)), although its comment lists Z samples as at most the coverage samples ([L3439](https://github.com/openbsd/xenocara/blob/1b39033df8d928ec247ca64448c4a63c72302db7/lib/mesa/src/gallium/drivers/radeonsi/si_state.c#L3439)) and it sets `DB_EQAA.MAX_ANCHOR_SAMPLES` to the Z sample count, where PPSA18216 sets `DB_EQAA` to 0. GL then lets every sample take the single-sample depth ([L185-L190](https://github.com/KhronosGroup/OpenGL-Registry/blob/6af574a14089ccfee87efe230ebcdd8742859813/extensions/ARB/ARB_multisample.txt#L185-L190)) and the fragment be treated as in multisampling with full coverage ([L422-L425](https://github.com/KhronosGroup/OpenGL-Registry/blob/6af574a14089ccfee87efe230ebcdd8742859813/extensions/ARB/ARB_multisample.txt#L422-L425)), which is what this draws: each depth and stencil sample is tested and written with the depth at the pixel center. GL also lets an implementation test only a centermost sample and set every sample from it ([L425-L434](https://github.com/KhronosGroup/OpenGL-Registry/blob/6af574a14089ccfee87efe230ebcdd8742859813/extensions/ARB/ARB_multisample.txt#L425-L434)), which differs when a pixel's stored samples differ; which one the console does, whether it instead evaluates the depth plane at each sample position, and how its occlusion queries count such a draw, is not measured on hardware. Points, lines, alpha-to-coverage, `DB_EQAA` Z interpolation or overrasterization, a pixel program reading the sample index, a sample-mask export and a single-sample color target next to the multisampled depth target throw - [DCC display buffers](../../../core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp) are presented only when all keys of the console's DCC extent agree on uncompressed or a fast-clear code, padding keys included; other keys, and register-clear keys over a pending image, throw. -- [Storage image write-back](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp) (libSceAgcDriver) stores the GPU results over a CPU-stamped range whose guest bytes equal the copy taken when the image's layers got their generation (images up to 1 MiB whose guest bytes were settled at that point, with no unit shadow, pending image results or recorded GPU store over them; other images keep the previous rule: the CPU's bytes win). A CPU store that rewrites the baseline value after the GPU changed it is not told apart from no store, and the GPU result wins there. The 1 MiB limit is a choice, not a console value. +- [Storage image write-back](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp) (libSceAgcDriver) stores the GPU results over a CPU-stamped range whose guest bytes equal the copy taken when the image's layers got their generation (images up to 1 MiB whose guest bytes were settled at that point, with no unit shadow, pending image results or recorded GPU store over them; other images keep the previous rule: the CPU's bytes win). A CPU store that rewrites the baseline value after the GPU changed it is not told apart from no store, and the GPU result wins there. The 1 MiB limit is a choice, not a console value. A range a queued draw commit still has to read is left pending whatever the stamps say ([`DrawPipeline::HeldReads`](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawPipeline.hpp)), so a title that recycles one allocation between an index buffer and a storage image sees the draw's own indices; the trade is a title that reads a surface as index data and means the image gets the pre-image bytes instead, since the reservation does not compare the command-stream positions of the draw and of the image's write. `APS5_NO_QUEUED_DRAW_READ_ORDER=1` restores the old behaviour, and the `[storage]` line counts the listings the hold turned away (`held for a queued draw read`) and the `[draw] pipelined` line the stores it held. The hold covers what the commit reads of guest memory: the index range, the vertex input ranges as their descriptor extents ([`QueuedDrawReadRanges`](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Memory/WrittenBuffers.cpp), since the bytes a draw fetches depend on the highest index, which only the commit knows) and the source of a queued DMA copy. Every other commit-time read goes through the flush hook, so it sees the bytes the hold protects. Two commit-time reads are outside the hold: the texels a register or cmask clear materialization reads from the draw's own colour target (through the hook, so ordered, but the hold does not cover a render target), and a CPU read whose store the hold turned away, which sees the pre-image bytes. Nothing retries a refused store: the image reaches guest memory with the next flush of that range (the queue worker's idle store, a `WAIT_REG_MEM` poll over it, a later read or write of it). - [16-bit float display buffers](../../../core/libs/prx/libSceAgcDriver/Execution/src/DisplayBuffer.cpp) (libSceAgcDriver, libSceVideoOut) - pixel formats `0xC001000622000000` and `0xC001000600000000` are taken as R16G16B16A16 and B16G16R16A16 float, eight bytes per texel, as [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/59183dbfa81ed7efdff6069177fed50de0552725/src/graphics/host_gpu/renderer/image/imageInfo.h) maps them and [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/VideoOut/VideoOutTypes.cs) names them (`Rgba16Float`, `Bgra16Float`); PPSA11386 registers 3840x2160 tiled buffers of the first. The tiled layout is assumed to be the 8-byte SW_64KB_R_X equation of `TextureSwizzleEquations.hpp` applied to full coordinates, as addrlib applies it, by analogy with the 32-bit scanout; `ColorTargetLayout` applies the equations to in-block coordinates, which for 8-byte texels differs in every odd 64-row block row (address bit 11 takes y bit 6) and for 16-byte texels also differs, so a float display buffer written through `WriteColorTarget` and presented from guest memory would show those rows misplaced. The values are assumed to be linear light and are encoded with the sRGB curve for the 8-bit output. Bits 32-39 of these formats are `0x06`, as in the 10-bit formats SharpProspero lists without sRGB transfer, while its sRGB formats have `0x00`, its 10-bit PQ formats `0x04` (with `0x07` in bits 40-47) and its float BT.2100 PQ formats `0x07`: they show only that these formats are not the sRGB ones. NaN and negative values give 0, values of 1 and above give 255, alpha is linear. Neither the layout nor the transfer is verified against console or title frames, and HDR output is not modelled. DCC metadata on a float buffer throws. - [BT.2100 PQ display buffers](../../../core/libs/prx/libSceAgcDriver/Execution/include/PqToSdr.hpp) (libSceAgcDriver, libSceVideoOut) - pixel formats `0x8100070422000000` and `0x8100070400000000` are taken as 10:10:10:2 texels with BT.2020 primaries and the SMPTE ST 2084 transfer, red or blue in the low bits, as [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/VideoOut/VideoOutTypes.cs) names them (`Rgb10A2Bt2100Pq`, `Bgr10A2Bt2100Pq`) and [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/59183dbfa81ed7efdff6069177fed50de0552725/src/graphics/host_gpu/renderer/image/imageInfo.h) maps the first; PPSA01317 registers display buffers of the first when its output reports HDR. With HDR output they are presented unchanged through an HDR10 swapchain (`VK_COLOR_SPACE_HDR10_ST2084_EXT`, 10-bit, with `VK_EXT_hdr_metadata`); otherwise they are converted to 8-bit sRGB: ST 2084 EOTF, BT.2020 to BT.709 primaries with negative components clipped, a luminance tone map and the sRGB encoding. The tone map is ours: min(203 nits, white level) is SDR white (the BT.2408 HDR reference white), luminance passes unchanged up to 0.75 of it and an extended Reinhard shoulder above reaches 1.0 at the white level, the peak sceSystemServiceGetHdrToneMapLuminance last reported to the title (or the current one when it never asked), since titles read it once. How a console shows such a buffer on an SDR display is unknown. Alpha is opaque. - [DCC key stores](../../../core/libs/prx/libSceAgcDriver/Graphics/src/DccMetadata.cpp) cover the console's DCC extent only for pipe-aligned DCC of single-level, single-layer 4-byte SW_64KB_R_X surfaces; other surfaces, and every texture and color target key read, cover one byte per 256 bytes.