From 2476bfda5645a1b970be951de5cab35c6d29b460 Mon Sep 17 00:00:00 2001 From: STL1te Date: Tue, 29 Sep 2026 07:54:04 +0300 Subject: [PATCH] feat(agc-driver): depth/stencil and mip-chain targets, more color formats - Depth/stencil: draws that test depth or stencil against a bound surface render with a GPU-only depth/stencil attachment (DepthSurface), cleared to DB_DEPTH_CLEAR/DB_STENCIL_CLEAR when made. Z test/write/function and per face the stencil function, operations and masks (BACKFACE_ENABLE); replacements by another value where Vulkan's single reference can carry them. Clear and copy draws, add/sub other than 1, array or mip views and MSAA throw, and texture reads of a surface's memory are refused. Scaleform menus mask with the stencil (REPLACE 0, ADD_CLAMP, EQUAL); their masked content was skipped before. - Mipmapped color targets render through the resident image of the whole chain with a view of the mip, so mips in the packed tail are drawn too; CB_COLOR_VIEW's mip level is read from bits 26-29 (GFX10). - Color: SNORM targets, UNORM16/SNORM16 exports, one-component formats honouring COMP_SWAP (A8), 32_R/32_GR/32_AR exports, linear rows padded to 256 bytes, BLEND_BYPASS. - INDIRECT_BUFFER jumps are followed when copying a submission; thin 3D textures in display and rotated XOR swizzles; only the bytes the GPU changed are written back; resource registration. - VideoOut: a flip stalls the GPU queue while the flip queue is full and holds the submitter back. - APS5_DUMP_FRAMES_EVERY dumps every k-th presented frame. - The graphics test target builds again (missing sources, a hand-aligned 64 KiB buffer, stale expectations updated) and gains DepthStencilTests. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 6 + .../Execution/include/VideoOutput.hpp | 3 + .../libSceAgcDriver/Execution/src/Driver.cpp | 155 ++++++++++--- .../Execution/src/GuestMemory.cpp | 15 +- .../prx/libSceAgcDriver/Execution/src/Pm4.cpp | 15 +- .../Execution/src/VulkanDevice.cpp | 17 +- .../Graphics/include/DepthSurface.hpp | 23 ++ .../Graphics/include/Pipeline.hpp | 9 +- .../Graphics/include/ShaderInputState.hpp | 2 +- .../Graphics/include/State.hpp | 45 +++- .../Graphics/include/Texture.hpp | 4 +- .../Graphics/src/ColorTargetLayout.cpp | 7 +- .../Graphics/src/ColorTargetTransfer.cpp | 4 +- .../Graphics/src/DepthSurface.cpp | 130 +++++++++++ .../prx/libSceAgcDriver/Graphics/src/Draw.cpp | 33 ++- .../Graphics/src/GuestTextureResource.cpp | 7 + .../libSceAgcDriver/Graphics/src/Pipeline.cpp | 37 +++- .../Graphics/src/ShaderInputState.cpp | 9 +- .../libSceAgcDriver/Graphics/src/State.cpp | 206 +++++++++++++++--- .../libSceAgcDriver/Graphics/src/Texture.cpp | 9 +- .../Graphics/src/TextureTiling.cpp | 16 +- .../Resource/src/Lifecycle.cpp | 7 +- .../Resource/src/Registration.cpp | 1 - .../State/src/Configuration.cpp | 8 +- .../tests/ColorTargetLayout.cpp | 12 +- .../prx/libSceAgcDriver/tests/Graphics.cpp | 85 +++++++- core/libs/prx/libSceAgcDriver/tests/Pm4.cpp | 13 ++ .../prx/libSceVideoOut/src/VideoOutDriver.cpp | 31 +-- 28 files changed, 781 insertions(+), 128 deletions(-) create mode 100644 core/libs/prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp create mode 100644 core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index e97930867..fb36e364d 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -59,6 +59,7 @@ add_library(libSceAgcDriver SHARED EXCLUDE_FROM_ALL ${CMAKE_CURRENT_SOURCE_DIR}/Execution/src/AspectFit.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Execution/src/PresentationScaler.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Graphics/src/Draw.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/Graphics/src/DepthSurface.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Graphics/src/ColorTargetLayout.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Graphics/src/ColorTargetTransfer.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Graphics/src/Pipeline.cpp @@ -219,6 +220,7 @@ add_executable(agc_driver_graphics_tests EXCLUDE_FROM_ALL tests/GuestSamplerResource.cpp tests/TextureDetiler.cpp Graphics/src/Draw.cpp + Graphics/src/DepthSurface.cpp Graphics/src/Pipeline.cpp Graphics/src/State.cpp Graphics/src/ColorTargetLayout.cpp @@ -239,8 +241,11 @@ add_executable(agc_driver_graphics_tests EXCLUDE_FROM_ALL Graphics/src/Texture.cpp Graphics/src/UnitShadow.cpp Graphics/src/Sampler.cpp + Graphics/src/Recorder.cpp + Graphics/src/ShaderInputState.cpp ${agcTextureDetileShaderHeader} Execution/src/GuestMemory.cpp + Execution/src/Pm4.cpp ) target_include_directories(agc_driver_graphics_tests PRIVATE @@ -327,6 +332,7 @@ if(AGC_BUILD_VISUAL_TEST) Execution/src/GuestMemory.cpp Execution/src/BdaFeatures.cpp Graphics/src/Draw.cpp + Graphics/src/DepthSurface.cpp Graphics/src/ColorTargetLayout.cpp Graphics/src/ColorTargetTransfer.cpp Graphics/src/Pipeline.cpp diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/VideoOutput.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/VideoOutput.hpp index 79f96d8c1..8669b4859 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/VideoOutput.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/VideoOutput.hpp @@ -40,6 +40,9 @@ class IVideoOutput { public: virtual ~IVideoOutput() = default; virtual std::shared_ptr Reserve(const FlipInfo& info) = 0; + // Blocks a submission carrying a flip until the output's flip queue has room, as the system + // holds back a submitter that runs ahead of the display. Called without the driver's lock. + virtual void WaitForFlipRoom() {} virtual std::shared_ptr CaptureRenderingWait(std::uint32_t index) { throw std::runtime_error("VideoOut: rendering waits are unsupported by this output"); } diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver.cpp index c34a57bfa..55bd2b36e 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver.cpp @@ -203,6 +203,10 @@ struct Submission { // submission trusts only labels the recorder noted with a newer stamp (see Recorder::NoteLabel). std::uint64_t received = 0; std::chrono::steady_clock::time_point enqueuedAt{}; + // The guest commands behind a REWIND (commands stop at it): the title writes them after submitting + // and marks the REWIND valid, so they are read only then (Driver::executeRewindTail). + const std::uint32_t* rewindTail = nullptr; + std::size_t rewindWords = 0; }; std::uint32_t readRegister(const Registers& registers, std::uint32_t offset) { @@ -524,6 +528,117 @@ private: public: + // Copies a submission's commands, stopping after the first REWIND: what follows it is read when the + // REWIND becomes valid. INDIRECT_BUFFER packets are followed as the CP follows them: a chained one + // (bit 20 of its size dword) replaces the rest of its buffer with the target, a call inserts the + // target and continues after the packet. + static void copyCommands(Submission& submission, const std::uint32_t* guest, std::size_t words) { + submission.commands.clear(); + std::size_t budget = std::size_t{1} << 26u; + copySegment(submission, guest, words, budget); + } + + // Appends one command segment; true when a REWIND ended the copy. + static bool copySegment(Submission& submission, const std::uint32_t* guest, std::size_t words, std::size_t& budget) { + require(words <= budget, "command buffer jumps exceed the copy limit (a jump loop?)"); + budget -= words; + for (std::size_t cursor = 0; cursor < words;) { + const auto header = guest[cursor]; + if (Pm4::FillerPacket(header)) { submission.commands.push_back(header); ++cursor; continue; } + const auto count = (header & 0xc0000000u) == 0xc0000000u ? Pm4::PacketWords(header) : words - cursor; + if ((header & 0xc0000000u) != 0xc0000000u || count > words - cursor) { + // Malformed: copied as is for validate to report with its context. + submission.commands.insert(submission.commands.end(), guest + cursor, guest + words); + return false; + } + const auto opcode = (header >> 8u) & 0xffu; + if (opcode == 0x3fu) { + require(count == 4, "invalid INDIRECT_BUFFER size"); + const auto* target = reinterpret_cast(static_cast(guest[cursor + 1] & ~3u) | (static_cast(guest[cursor + 2] & 0xffffu) << 32u)); + const std::size_t targetWords = guest[cursor + 3] & 0xfffffu; + const bool chain = (guest[cursor + 3] & (1u << 20u)) != 0; + GuestMemory::CheckRange(target, targetWords * sizeof(std::uint32_t), alignof(std::uint32_t)); + if (copySegment(submission, target, targetWords, budget)) return true; + if (chain) return false; + cursor += count; + continue; + } + submission.commands.insert(submission.commands.end(), guest + cursor, guest + cursor + count); + cursor += count; + if (opcode == 0x59u) { + submission.rewindTail = guest + cursor; + submission.rewindWords = words - cursor; + return true; + } + } + return false; + } + + // Holds the submitting thread while an output named by a flip in the submission has a full flip + // queue (mutex not held: the presenter needs the driver to finish flips). + void waitForFlipRoom(const Submission& submission) { + for (std::size_t cursor = 0; cursor < submission.commands.size(); cursor += Pm4::PacketWords(submission.commands[cursor])) { + if (submission.commands[cursor] != FlipPacketHeader) continue; + std::shared_ptr output; + { + std::lock_guard lock(mutex); + const auto found = outputs.find(submission.commands[cursor + 1]); + require(found != outputs.end(), "flip references an unregistered video output"); + output = found->second; + } + output->WaitForFlipRoom(); + } + } + + // Takes the flip reservations and rendering waits the submission's packets name (mutex held). + void reserveOutputs(Submission& submission) { + for (std::size_t cursor = 0; cursor < submission.commands.size();) { + const auto* words = submission.commands.data() + cursor; + if (words[0] == RenderingWaitPacketHeader) { + const auto output = outputs.find(words[1]); + require(output != outputs.end(), "rendering wait references an unregistered video output"); + auto wait = output->second->CaptureRenderingWait(words[2]); + require(wait != nullptr, "video output returned a null rendering wait"); + submission.renderingWaits.emplace(cursor, std::move(wait)); + } + if (words[0] == FlipPacketHeader) { + const auto output = outputs.find(words[1]); + require(output != outputs.end(), "flip references an unregistered video output"); + const FlipInfo info{words[1], std::bit_cast(words[2]), words[3], std::bit_cast(static_cast(words[4]) | (static_cast(words[5]) << 32u))}; + auto request = output->second->Reserve(info); + require(request != nullptr, "video output returned a null flip reservation"); + submission.flips.emplace(cursor, std::move(request)); + } + cursor += Pm4::PacketWords(words[0]); + } + } + + // REWIND stalls the queue until the CPU marks it valid (sceAgcRewindPatchSetRewindState); the + // commands behind it are copied from guest memory only then and run as a continuation. + void executeRewindTail(const Submission& stalled) { + std::atomic_ref control(*const_cast(stalled.rewindTail - 1)); + while ((control.load(std::memory_order_acquire) & 0x80000000u) == 0) { + CheckFailure(); + checkStopping(); + PollSleep(); + } + Submission tail{}; + tail.queue = stalled.queue; + copyCommands(tail, stalled.rewindTail, stalled.rewindWords); + validate(tail.commands, tail.queue, stalled.rewindTail); + waitForFlipRoom(tail); + { + std::lock_guard lock(mutex); + rethrowFailure(); + checkStopping(); + reserveOutputs(tail); + tail.shaders = shaders; + tail.serial = stalled.serial; + tail.received = ++eventSerial; + } + execute(tail); + } + void Submit(const Packet* packet, std::uint32_t queue) { CheckFailure(); require(queue == 0 || (queue >= 0x20 && queue < 0x58), "unsupported compute queue"); @@ -538,10 +653,11 @@ public: if (descriptor.dw_num != 0) { require(descriptor.dw_num <= std::numeric_limits::max() / sizeof(std::uint32_t), "command size overflow"); GuestMemory::CheckRange(descriptor.addr, static_cast(descriptor.dw_num) * sizeof(std::uint32_t), alignof(std::uint32_t)); - submission.commands.assign(descriptor.addr, descriptor.addr + descriptor.dw_num); + copyCommands(submission, descriptor.addr, descriptor.dw_num); } const auto copied = profile ? std::chrono::steady_clock::now() : start; validate(submission.commands, queue, descriptor.addr); + waitForFlipRoom(submission); static const bool trace = std::getenv("APS5_TRACE_GPU") != nullptr; if (trace) std::fprintf(stderr, "[gpu] %.1f submit queue=0x%x dwords=%zu at %p\n", TraceMs(), queue, submission.commands.size(), static_cast(descriptor.addr)); const auto validated = profile ? std::chrono::steady_clock::now() : start; @@ -550,25 +666,7 @@ public: rethrowFailure(); checkStopping(); require(accepted != std::numeric_limits::max(), "submission serial overflow"); - for (std::size_t cursor = 0; cursor < submission.commands.size();) { - const auto* words = submission.commands.data() + cursor; - if (words[0] == RenderingWaitPacketHeader) { - const auto output = outputs.find(words[1]); - require(output != outputs.end(), "rendering wait references an unregistered video output"); - auto wait = output->second->CaptureRenderingWait(words[2]); - require(wait != nullptr, "video output returned a null rendering wait"); - submission.renderingWaits.emplace(cursor, std::move(wait)); - } - if (words[0] == FlipPacketHeader) { - const auto output = outputs.find(words[1]); - require(output != outputs.end(), "flip references an unregistered video output"); - const FlipInfo info{words[1], std::bit_cast(words[2]), words[3], std::bit_cast(static_cast(words[4]) | (static_cast(words[5]) << 32u))}; - auto request = output->second->Reserve(info); - require(request != nullptr, "video output returned a null flip reservation"); - submission.flips.emplace(cursor, std::move(request)); - } - cursor += Pm4::PacketWords(words[0]); - } + reserveOutputs(submission); submission.shaders = shaders; submission.serial = accepted + 1; // Every CPU store the game made before this call precedes the stamp; labels recorded @@ -3889,13 +3987,19 @@ private: if (!drawParameters.indirect && !drawParameters.indexed && (drawParameters.indexCount == 0 || drawParameters.instanceCount == 0)) return DrawVerdict::Nothing; static const bool traceIndirect = std::getenv("APS5_TRACE_INDIRECT_DRAWS") != nullptr; if (traceIndirect && drawParameters.indirect) std::fprintf(stderr, "[draw] indirect packet %s args 0x%llx count %u reached\n", Pm4::Name(packet[0]).c_str(), static_cast(drawParameters.indirect->arguments), drawParameters.indirect->count); - // Depth/stencil-only passes (no color writes, no pixel shader) have no effect without depth - // targets, which are not emulated. + // A draw without color writes or a pixel shader draws nothing, unless it tests depth or + // stencil against a bound surface: such depth-only draws need a pipeline without a + // fragment stage, which is not implemented. { const auto targetMask = queue.context.find(0x8e); const auto shaderMask = queue.context.find(0x8f); const bool colorWrites = targetMask != queue.context.end() && shaderMask != queue.context.end() && (targetMask->second & shaderMask->second) != 0; - if (!colorWrites && !queue.shader.contains(0x8)) return DrawVerdict::Nothing; + if (!colorWrites && !queue.shader.contains(0x8)) { + const auto word = [&](std::uint32_t offset) { const auto it = queue.context.find(offset); return it == queue.context.end() ? 0u : it->second; }; + if ((word(0x200) & 3u) == 0 || ((word(0x010) & 3u) == 0 && (word(0x011) & 1u) == 0)) return DrawVerdict::Nothing; + rejected = "AGC graphics: depth/stencil-only draws without a pixel shader are not implemented"; + return DrawVerdict::Rejected; + } } if (drawParameters.indexed) { const auto restart = queue.userConfig.find(0x24b); @@ -4057,7 +4161,7 @@ private: } append(0x008, 1, Stage::Fragment, 0x00b, 0x00c, Role::Fragment); programs.back().firstUserSgpr = 0; - product->pixel = Graphics::DecodePixelStageInfo(queue.context, graphics.hasColorTarget, graphics.color.componentMapping); + product->pixel = Graphics::DecodePixelStageInfo(queue.context, Graphics::ExportMappings(graphics)); return product; }; if (decode == nullptr || VerifyDrawRecipe()) { @@ -6090,7 +6194,7 @@ private: static const bool submitAtEnd = std::getenv("APS5_SUBMIT_AT_END") != nullptr; if (!deferredLabels().labels.empty() || Graphics::Recorder::PendingLabelSince().has_value() || Graphics::Recorder::RecordedWorkSinceSubmit() != 0) { auto& costs = submissionCosts(submission.queue); - if (!submitAtEnd && submission.queue == 0 && workerQueued() != nullptr && workerQueued()->load(std::memory_order_acquire) != 0) { + if (!submitAtEnd && submission.rewindTail == nullptr && submission.queue == 0 && workerQueued() != nullptr && workerQueued()->load(std::memory_order_acquire) != 0) { ++costs.endSkipped; return; } @@ -6102,6 +6206,7 @@ private: recordDeferredLabels(localDevice.get(), submission.queue); if (localDevice != nullptr) localDevice->SubmitRecorded(submission.queue == 0); } + if (submission.rewindTail != nullptr) executeRewindTail(submission); } // The calling worker's pending count (QueueWorker::queued), set by run(); null off a worker. diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp index 561ef3b50..c6f588ffe 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp @@ -1365,9 +1365,18 @@ void WriteChanged(std::uint64_t address, std::span current, std std::size_t lastChanged = 0; for (std::size_t at = 0; at < size; at += block) { if (!differs(at)) continue; - std::memcpy(destination + at, current.data() + at, std::min(block, size - at)); - firstChanged = std::min(firstChanged, at); - lastChanged = std::min(at + block, size); + // Only the bytes the GPU changed: the rest of the block holds the snapshot, and storing it + // would roll back what the CPU wrote there since (a title lost an object's vtable pointer). + const auto blockEnd = std::min(at + block, size); + for (std::size_t run = at; run < blockEnd;) { + if (current[run] == original[run]) { ++run; continue; } + auto runEnd = run + 1; + while (runEnd < blockEnd && current[runEnd] != original[runEnd]) ++runEnd; + std::memcpy(destination + run, current.data() + run, runEnd - run); + firstChanged = std::min(firstChanged, run); + lastChanged = std::max(lastChanged, runEnd); + run = runEnd; + } } // Stamped like a GPU write: a collect memoized for this packet would not see the page fault. if (firstChanged < lastChanged) MarkWritten(address + firstChanged, lastChanged - firstChanged); diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp index ff230342a..ea62413b1 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp @@ -30,7 +30,11 @@ std::uint64_t address(std::uint32_t low, std::uint32_t high) { std::uint32_t registerOffset(std::uint32_t value) { require(value != 0xffffffffu, "indirect register sentinel semantics are not implemented"); const auto offset = value & ~0x70000000u; - require(offset <= 0xffffu, "extended register semantics are not implemented"); + if (offset > 0xffffu) { + char what[80]; + std::snprintf(what, sizeof(what), "extended register semantics are not implemented (offset dword 0x%08x)", value); + throw std::runtime_error(what); + } return offset; } @@ -175,8 +179,9 @@ std::string_view UnsupportedReason(std::uint32_t header) { case 0x22: return "conditional command execution and conditional flip reservation are not implemented"; case 0x33: case 0x3f: return "nested command buffers, branching and nested flip reservation are not implemented"; case 0x3c: case 0x93: return {}; - case 0x39: case 0x59: + case 0x39: return "cooperative command-queue waits are not implemented"; + case 0x59: return {}; case 0x84: case 0x85: case 0x86: case 0x88: return "separate CE/DE execution and counter synchronization are not implemented"; case 0x49: return {}; @@ -341,6 +346,11 @@ void Validate(std::span packet, std::uint32_t queue) { require(packet.size() - 2 <= 0x10000u - offset, "register range overflow"); break; } + case 0x59: + // REWIND: bit 31 is the valid flag the CPU sets; the driver waits for it (see Driver). + size(2); + require((packet[1] & 0x7fffffffu) == 0, "unsupported REWIND payload bits"); + break; case 0x3c: case 0x93: { size(opcode == 0x3c ? 7 : 9); require((packet[1] & 0x10u) != 0, "register-space WAIT_REG_MEM is not implemented"); @@ -688,6 +698,7 @@ void Execute(std::span packet, QueueState& queue) { } return; } + case 0x59: break; case 0x3c: case 0x93: { // The waited-on value is written by the CPU or another queue; poll it like the CP would. const auto start = std::chrono::steady_clock::now(); diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp index 83bd0b373..9aa13dfb5 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp @@ -4,6 +4,7 @@ #include "prx/libSceAgcDriver/Execution/include/PerformanceTimer.hpp" #include "prx/libSceAgcDriver/Execution/include/BdaFeatures.hpp" #include "prx/libSceAgcDriver/Execution/include/PresentationScaler.hpp" +#include "prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp" #include "prx/libSceAgcDriver/Graphics/include/TextureDetiler.hpp" #include "prx/libSceAgcDriver/Graphics/include/GpuColorTransfer.hpp" #include "prx/libSceAgcDriver/Graphics/include/BufferPool.hpp" @@ -489,6 +490,8 @@ struct VulkanDevice::State { // Cached graphics pipelines (with their framebuffers, modules, render passes and layouts) // belong to this device and must be destroyed while it lives. Graphics::ClearCachedPipelines(device); + // Their framebuffers were the last users of the depth surfaces' views. + Graphics::ClearDepthSurfaces(device); { std::lock_guard pipelines(computePipelinesMutex); computePipelines.clear(); @@ -779,6 +782,9 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window) : state(std::make_u // Gathers with non-constant offsets (ImageGatherExtended). enabled.shaderImageGatherExtended = available.shaderImageGatherExtended; if (enabled.shaderImageGatherExtended) state->capabilities.push_back(spv::CapabilityImageGatherExtended); + // Sampling with an LOD clamp (image_sample_*_cl) as the MinLod image operand. + enabled.shaderResourceMinLod = available.shaderResourceMinLod; + if (enabled.shaderResourceMinLod) state->capabilities.push_back(spv::CapabilityMinLod); if (enabled.shaderStorageImageWriteWithoutFormat) state->capabilities.push_back(spv::CapabilityStorageImageWriteWithoutFormat); if (enabled.shaderStorageImageReadWithoutFormat) state->capabilities.push_back(spv::CapabilityStorageImageReadWithoutFormat); // Bindless image tables index an image array with a wave-uniform runtime slot. @@ -1688,17 +1694,24 @@ bool NoResidentPresent() { // Debug aid: APS5_DUMP_FRAMES= saves the first n presented display buffers as frame_.bmp, // read back by the blit's own submission and written after its fence (RetireSlot), or with // APS5_NO_GPU_DUMP=1 decoded from the tiled guest buffer on the CPU, as before. +// APS5_DUMP_FRAMES_EVERY= dumps every k-th presented frame (up to APS5_DUMP_FRAMES of them) +// instead of the first ones, to reach screens far into a run. struct FrameDumps { int limit; bool cpu; + int every = 1; int dumped = 0; + std::uint64_t presents = 0; }; FrameDumps& Dumps() { static FrameDumps dumps{[] { const char* value = std::getenv("APS5_DUMP_FRAMES"); return value ? std::atoi(value) : 0; - }(), std::getenv("APS5_NO_GPU_DUMP") != nullptr}; + }(), std::getenv("APS5_NO_GPU_DUMP") != nullptr, [] { + const char* value = std::getenv("APS5_DUMP_FRAMES_EVERY"); + return value ? std::max(std::atoi(value), 1) : 1; + }()}; return dumps; } @@ -1742,7 +1755,7 @@ bool VulkanDevice::PresentDisplayBuffer(const DisplayBuffer& buffer) { } auto& dumps = Dumps(); bool dumpFrame = false; - if (dumps.dumped < dumps.limit) { + if (dumps.dumped < dumps.limit && ++dumps.presents % static_cast(dumps.every) == 0) { if (dumps.cpu) { const auto full = ReadDisplayBuffer(buffer); WriteFrameBmp(dumps.dumped++, buffer.width, buffer.height, full, DumpScale()); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp new file mode 100644 index 000000000..424a80d85 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp @@ -0,0 +1,23 @@ +#ifndef CORE_LIBS_PRX_LIBSCEAGCDRIVER_GRAPHICS_INCLUDE_DEPTHSURFACE_HPP +#define CORE_LIBS_PRX_LIBSCEAGCDRIVER_GRAPHICS_INCLUDE_DEPTHSURFACE_HPP + +#include "prx/libSceAgcDriver/Graphics/include/State.hpp" +#include + +namespace AgcDriver::Graphics { + +// The attachment view of the device's image for a depth/stencil target, made on first use. The +// image lives on the GPU only, always in the general layout: its guest surface (tiled, HTILE +// compressed) is never read or written, so a new image starts at the target's clear values, and +// texture reads of the surface are refused (DepthSurfaceAt). Images stay until the device goes +// (ClearDepthSurfaces), so their views are stable for cached framebuffers. +// ponytail: never evicted, one image per distinct surface; evict (and keep recorded ones alive) if +// titles churn through many. +VkImageView DepthSurfaceView(const Context& context, const DepthTarget& target); +void ClearDepthSurfaces(VkDevice device); +// Whether the depth or stencil plane of a surface drawn with starts at `address`. +bool DepthSurfaceAt(std::uint64_t address); + +} + +#endif diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/Pipeline.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/Pipeline.hpp index efa0a9514..ef98ccc0f 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/Pipeline.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/Pipeline.hpp @@ -40,9 +40,11 @@ public: Pipeline(const Pipeline&) = delete; Pipeline& operator=(const Pipeline&) = delete; VkPipelineLayout Layout() const; - // The framebuffer of these attachment views. A resident target's view (`owners[i]` set) is stable - // while its StorageTexture lives, so framebuffers made of resident views are kept in the pipeline - // and reused when every owner is still the same object; any other set is built per call. + // The framebuffer of these attachment views: the color views, then the depth surface's when the + // pipeline has one. A resident target's view (`owners[i]` set) is stable while its + // StorageTexture lives, and a depth surface's while its device does, so framebuffers made of + // resident views are kept in the pipeline and reused when every owner is still the same + // object; any other set is built per call. std::shared_ptr AcquireFramebuffer(std::span targets, std::span> owners, VkExtent2D extent); // Begins the render pass on the framebuffer, binds the pipeline and sets viewport and scissor. void Begin(VkCommandBuffer commands, const Framebuffer& framebuffer, VkExtent2D extent, const VkViewport& viewport, const VkRect2D& scissor) const; @@ -69,6 +71,7 @@ private: VkRenderPass renderPass = VK_NULL_HANDLE; VkPipeline pipeline = VK_NULL_HANDLE; std::size_t attachments = 0; + std::size_t colorAttachments = 0; std::vector framebuffers; }; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderInputState.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderInputState.hpp index 540d73af7..df16329a9 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderInputState.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderInputState.hpp @@ -10,7 +10,7 @@ namespace AgcDriver::Graphics { -ShaderRecompiler::ShaderPixelStageInfo DecodePixelStageInfo(const Registers& context, bool hasColorTarget, std::uint8_t colorComponentMapping); +ShaderRecompiler::ShaderPixelStageInfo DecodePixelStageInfo(const Registers& context, const std::array& exportMappings); ShaderRecompiler::ShaderComputeStageInfo DecodeComputeStageInfo(const Registers& shader); // A guest range DecodeVertexStageInfo read (an attribute word, a vertex V#) with the bytes as read: // the draw cache validates them by value with the stage's capture (design_cpu_final rule RD). diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/State.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/State.hpp index fd90957c3..c4e32973a 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/State.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/State.hpp @@ -40,10 +40,40 @@ struct ColorTarget { // DCC metadata of a compressed target (CB_COLOR_INFO DCC_ENABLE), or 0 (see DccMetadata.hpp). std::uint64_t dccAddress = 0; bool dccAlphaOnMsb = false; + // A view of one mip of a mipmapped surface (CB_COLOR_VIEW MIP_LEVEL, CB_COLOR_ATTRIB2 MAX_MIP): + // the surface's base, mip 0 extent and mip count, the rendered mip, and whether it lies in the + // packed mip tail (which shares one block with the smaller mips, so it is only rendered through + // the resident image of the whole chain). `address`, `extent` and `bytes` are the mip's. + std::uint64_t surfaceAddress = 0; + VkExtent2D surfaceExtent{}; + std::uint32_t mipCount = 1; + std::uint32_t mip = 0; + bool mipTail = false; +}; + +// The depth/stencil surface a draw with depth or stencil tests renders with (DB_Z_INFO, +// DB_STENCIL_INFO and their bases). Its image lives on the GPU only (DepthSurface.hpp). +struct DepthTarget { + std::uint64_t address; + // The stencil plane, 0 without one. + std::uint64_t stencilAddress; + VkExtent2D extent; + VkFormat format; + // DB_DEPTH_CLEAR / DB_STENCIL_CLEAR: what a new image starts as. + float clearDepth; + std::uint8_t clearStencil; }; struct State { ShaderStages stages; + // Set when the draw tests depth or stencil against a bound surface; the tests below are off otherwise. + std::optional depth; + bool depthTest = false; + bool depthWrite = false; + VkCompareOp depthCompare = VK_COMPARE_OP_ALWAYS; + bool stencilTest = false; + VkStencilOpState stencilFront{}; + VkStencilOpState stencilBack{}; // MRT slot 0; `colors`/`blends` hold every written slot, attachment i being slot i. ColorTarget color; std::vector colors; @@ -64,6 +94,8 @@ struct State { ShaderStages DecodeShaderStages(const QueueState& queue); State DecodeState(const QueueState& queue); +// The component mapping of each MRT slot's export (identity past the written slots). +std::array ExportMappings(const State& state); // The message DecodeState (or the pixel stage decode after it) would throw for the register rules // this precheck covers, evaluated without exceptions before the draw is decoded; empty when they // pass (DecodeState still checks everything). A register a rule needs that is absent is no verdict. @@ -99,11 +131,14 @@ struct DrawKeyRange { std::uint32_t first; std::uint32_t count; }; -inline constexpr std::array DrawKeyRegisters{{ - // PA_SC_SCREEN_SCISSOR, the window offset/scissor and clip rect, the edge rule, the hardware - // screen offset, CB_TARGET_MASK/CB_SHADER_MASK, the generic and viewport 0 scissors, the - // viewport 0 depth clamp, the blend constants, the viewport 0 transform. - {RegisterBank::Context, 0x00c, 2}, {RegisterBank::Context, 0x080, 4}, {RegisterBank::Context, 0x08c, 4}, {RegisterBank::Context, 0x090, 2}, {RegisterBank::Context, 0x094, 2}, {RegisterBank::Context, 0x0b4, 2}, {RegisterBank::Context, 0x105, 4}, {RegisterBank::Context, 0x10f, 6}, +inline constexpr std::array DrawKeyRegisters{{ + // DB_RENDER_CONTROL, DB_DEPTH_VIEW, DB_DEPTH_SIZE_XY, DB_STENCIL_CLEAR/DB_DEPTH_CLEAR with + // PA_SC_SCREEN_SCISSOR, DB_Z_INFO .. DB_STENCIL_WRITE_BASE and their high words; the window + // offset/scissor and clip rect, the edge rule, the hardware screen offset, CB_TARGET_MASK/ + // CB_SHADER_MASK, the generic and viewport 0 scissors, the viewport 0 depth clamp, the blend + // constants, DB_STENCIL_CONTROL/DB_STENCILREFMASK(_BF), the viewport 0 transform. + {RegisterBank::Context, 0x000, 1}, {RegisterBank::Context, 0x002, 1}, {RegisterBank::Context, 0x007, 1}, {RegisterBank::Context, 0x00a, 4}, {RegisterBank::Context, 0x010, 6}, {RegisterBank::Context, 0x01a, 4}, + {RegisterBank::Context, 0x080, 4}, {RegisterBank::Context, 0x08c, 4}, {RegisterBank::Context, 0x090, 2}, {RegisterBank::Context, 0x094, 2}, {RegisterBank::Context, 0x0b4, 2}, {RegisterBank::Context, 0x105, 4}, {RegisterBank::Context, 0x10b, 3}, {RegisterBank::Context, 0x10f, 6}, // SPI_PS_INPUT_CNTL_0..31, SPI_PS_INPUT_ENA/ADDR, SPI_PS_IN_CONTROL, SPI_SHADER_POS/Z/COL_FORMAT, // CB_BLEND0..7_CONTROL, GE_MAX_OUTPUT_PER_SUBGROUP. {RegisterBank::Context, 0x191, 32}, {RegisterBank::Context, 0x1b3, 2}, {RegisterBank::Context, 0x1b6, 1}, {RegisterBank::Context, 0x1c3, 3}, {RegisterBank::Context, 0x1e0, 8}, {RegisterBank::Context, 0x1ff, 1}, diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp index 3e42c0c42..ff073f026 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp @@ -111,7 +111,7 @@ public: // Render targets live in the same images: draws attach mip 0 through a view of the color // buffer's format and mark the image dirty like a storage write. bool Attachable() const { return attachable; } - VkImageView AttachmentView(VkFormat format); + VkImageView AttachmentView(VkFormat format, std::uint32_t mip = 0); void WriteBack(); // Deferred write-back (APS5_EAGER_WRITEBACK=1 stores at once instead). void MarkDirty(); @@ -403,7 +403,7 @@ private: std::uint32_t defaultMip = 0; std::map extraViews; bool attachable = false; - std::map attachmentViews; + std::map, VkImageView> attachmentViews; VkFormat storageFormat = VK_FORMAT_UNDEFINED; // Results are on the GPU only (guarded by the pending-write registry lock). bool dirty = false; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/ColorTargetLayout.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/ColorTargetLayout.cpp index f9d4c41e9..1af888ca6 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ColorTargetLayout.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ColorTargetLayout.cpp @@ -32,9 +32,12 @@ ColorTargetLayout::ColorTargetLayout(std::uint32_t width, std::uint32_t height, require(std::has_single_bit(bytesPerElement) && bytesPerElement <= 16u, "AGC graphics: unsupported color element size"); std::uint32_t paddedHeight = height; switch (mode) { - case ColorTileMode::Linear: - require(width * bytesPerElement % 256u == 0, "AGC graphics: linear surface pitch requires a width aligned to 256 bytes"); + case ColorTileMode::Linear: { + // Rows are padded to 256 bytes, addrlib's linear pitch alignment (linear textures use the same). + const auto pitchAlignment = 256u / bytesPerElement; + pitch = (width + pitchAlignment - 1u) / pitchAlignment * pitchAlignment; break; + } case ColorTileMode::RenderTarget: { // SW_64KB_R_X: 64 KiB blocks of 2^(16 - log2(bpe)) elements, wider than tall for odd powers. const auto log2Elements = 16u - static_cast(std::countr_zero(bytesPerElement)); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/ColorTargetTransfer.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/ColorTargetTransfer.cpp index 9d9d7d346..36690adc4 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ColorTargetTransfer.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ColorTargetTransfer.cpp @@ -9,7 +9,7 @@ void ReadColorTarget(const ColorTarget& target, std::span destination PerformanceTimer timing("ColorTarget.Read"); const ColorTargetLayout layout(target.extent.width, target.extent.height, target.tileMode, target.elementBytes); Require(target.bytes == layout.Bytes() && destination.size() == layout.LinearBytes(), "color target transfer size mismatch"); - if (target.tileMode == ColorTileMode::Linear) { + if (target.tileMode == ColorTileMode::Linear && layout.Bytes() == layout.LinearBytes()) { GuestMemory::Read(target.address, destination, layout.Alignment()); return; } @@ -25,7 +25,7 @@ void WriteColorTarget(const ColorTarget& target, std::span sour PerformanceTimer timing("ColorTarget.Write"); const ColorTargetLayout layout(target.extent.width, target.extent.height, target.tileMode, target.elementBytes); Require(target.bytes == layout.Bytes() && source.size() == layout.LinearBytes(), "color target transfer size mismatch"); - if (target.tileMode == ColorTileMode::Linear) { + if (target.tileMode == ColorTileMode::Linear && layout.Bytes() == layout.LinearBytes()) { GuestMemory::Write(target.address, source, layout.Alignment()); return; } diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp new file mode 100644 index 000000000..ddb073359 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp @@ -0,0 +1,130 @@ +#include "prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Recorder.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Resources.hpp" +#include +#include +#include +#include + +namespace AgcDriver::Graphics { +namespace { + +class DepthSurface { +public: + DepthSurface(const Context& context, const DepthTarget& target) : context(context), target(target) { + this->context.bufferPool.reset(); + VkFormatProperties properties{}; + context.formatProperties(context.physical, target.format, &properties); + Require((properties.optimalTilingFeatures & VK_FORMAT_FEATURE_DEPTH_STENCIL_ATTACHMENT_BIT) != 0, "depth/stencil format " + std::to_string(target.format) + " cannot be an attachment on this device"); + Require(target.extent.width <= context.limits.maxFramebufferWidth && target.extent.height <= context.limits.maxFramebufferHeight, "depth target exceeds framebuffer limits"); + const VkImageAspectFlags aspects = VK_IMAGE_ASPECT_DEPTH_BIT | (target.stencilAddress != 0 ? VK_IMAGE_ASPECT_STENCIL_BIT : 0u); + try { + VkImageCreateInfo info{VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO}; + info.imageType = VK_IMAGE_TYPE_2D; + info.format = target.format; + info.extent = {target.extent.width, target.extent.height, 1}; + info.mipLevels = 1; + info.arrayLayers = 1; + info.samples = VK_SAMPLE_COUNT_1_BIT; + info.tiling = VK_IMAGE_TILING_OPTIMAL; + info.usage = VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT; + info.sharingMode = VK_SHARING_MODE_EXCLUSIVE; + info.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; + Check(context.Function("vkCreateImage")(context.device, &info, nullptr, &image), "vkCreateImage depth"); + VkMemoryRequirements requirements{}; + context.Function("vkGetImageMemoryRequirements")(context.device, image, &requirements); + VkMemoryAllocateInfo allocation{VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO}; + allocation.allocationSize = requirements.size; + allocation.memoryTypeIndex = context.MemoryType(requirements.memoryTypeBits, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT); + Check(context.Function("vkAllocateMemory")(context.device, &allocation, nullptr, &memory), "vkAllocateMemory depth target"); + Check(context.Function("vkBindImageMemory")(context.device, image, memory, 0), "vkBindImageMemory depth"); + VkImageViewCreateInfo viewInfo{VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO}; + viewInfo.image = image; + viewInfo.viewType = VK_IMAGE_VIEW_TYPE_2D; + viewInfo.format = target.format; + viewInfo.subresourceRange = {aspects, 0, 1, 0, 1}; + Check(context.Function("vkCreateImageView")(context.device, &viewInfo, nullptr, &view), "vkCreateImageView depth"); + // Cleared in the general layout it keeps from then on, recorded ahead of the draw (a draw's + // pass begins behind a barrier from every earlier transfer write). + auto* recorder = Recorder::Active(); + std::unique_ptr batch; + if (recorder == nullptr) batch = std::make_unique(context); + const auto commands = recorder != nullptr ? recorder->Commands() : batch->Handle(); + VkImageMemoryBarrier toGeneral{VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER}; + toGeneral.dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; + toGeneral.oldLayout = VK_IMAGE_LAYOUT_UNDEFINED; + toGeneral.newLayout = VK_IMAGE_LAYOUT_GENERAL; + toGeneral.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + toGeneral.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + toGeneral.image = image; + toGeneral.subresourceRange = viewInfo.subresourceRange; + const auto barrier = context.Resolved(&DeviceFunctions::cmdPipelineBarrier, "vkCmdPipelineBarrier"); + barrier(commands, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, nullptr, 0, nullptr, 1, &toGeneral); + const VkClearDepthStencilValue clear{target.clearDepth, target.clearStencil}; + context.Function("vkCmdClearDepthStencilImage")(commands, image, VK_IMAGE_LAYOUT_GENERAL, &clear, 1, &toGeneral.subresourceRange); + RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT | VK_PIPELINE_STAGE_LATE_FRAGMENT_TESTS_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_READ_BIT | VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT); + if (batch) batch->SubmitAndWait(); + else Recorder::CountBarriers(Recorder::CommandClass::Draw, 2); + } catch (...) { + release(); + throw; + } + } + ~DepthSurface() { release(); } + DepthSurface(const DepthSurface&) = delete; + DepthSurface& operator=(const DepthSurface&) = delete; + + const Context context; + const DepthTarget target; + VkImage image = VK_NULL_HANDLE; + VkImageView view = VK_NULL_HANDLE; + VkDeviceMemory memory = VK_NULL_HANDLE; + +private: + void release() noexcept { + if (view) context.Function("vkDestroyImageView")(context.device, view, nullptr); + if (image) context.Function("vkDestroyImage")(context.device, image, nullptr); + if (memory) context.Function("vkFreeMemory")(context.device, memory, nullptr); + view = VK_NULL_HANDLE; + image = VK_NULL_HANDLE; + memory = VK_NULL_HANDLE; + } +}; + +bool sameSurface(const DepthTarget& a, const DepthTarget& b) { + return a.address == b.address && a.stencilAddress == b.stencilAddress && a.extent.width == b.extent.width && a.extent.height == b.extent.height && a.format == b.format; +} + +std::mutex& surfacesMutex() { + static std::mutex mutex; + return mutex; +} + +// Never destroyed with the statics: the images belong to a device that may be gone by then. +std::vector>& surfaces() { + static auto* list = new std::vector>(); + return *list; +} + +} + +VkImageView DepthSurfaceView(const Context& context, const DepthTarget& target) { + std::lock_guard lock(surfacesMutex()); + for (const auto& surface : surfaces()) { + if (surface->context.device == context.device && sameSurface(surface->target, target)) return surface->view; + } + surfaces().push_back(std::make_unique(context, target)); + return surfaces().back()->view; +} + +void ClearDepthSurfaces(VkDevice device) { + std::lock_guard lock(surfacesMutex()); + std::erase_if(surfaces(), [&](const auto& surface) { return surface->context.device == device; }); +} + +bool DepthSurfaceAt(std::uint64_t address) { + std::lock_guard lock(surfacesMutex()); + return std::any_of(surfaces().begin(), surfaces().end(), [&](const auto& surface) { return surface->target.address == address || surface->target.stencilAddress == address; }); +} + +} diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp index 73e9dfa33..e6b9ae5c5 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp @@ -4,6 +4,7 @@ #include "prx/libSceAgcDriver/Graphics/include/VertexInput.hpp" #include "prx/libSceAgcDriver/Graphics/include/TextureDetiler.hpp" #include "prx/libSceAgcDriver/Graphics/include/DccMetadata.hpp" +#include "prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp" #include "prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp" #include "prx/libSceAgcDriver/Graphics/include/Recorder.hpp" #include "prx/libSceAgcDriver/Graphics/include/TextureFormat.hpp" @@ -41,15 +42,17 @@ std::uint32_t GuestFormatFor(VkFormat format, std::uint32_t elementBytes) { // The color buffer as a single-mip 2D surface descriptor (tile mode SW_64KB_R_X). GuestTextureResource SurfaceForTarget(const ColorTarget& color) { Require(color.tileMode == ColorTileMode::RenderTarget, "only 64 KiB tiled color targets are resident"); + // A mipmapped target is its whole chain; the attachment view picks the mip. + const bool chain = color.mipCount > 1; GuestTextureResource surface{}; - surface.baseAddress = color.address; - surface.width = color.extent.width; - surface.height = color.extent.height; + surface.baseAddress = chain ? color.surfaceAddress : color.address; + surface.width = chain ? color.surfaceExtent.width : color.extent.width; + surface.height = chain ? color.surfaceExtent.height : color.extent.height; surface.depthOrLastArray = 0; surface.baseArray = 0; - surface.mipCount = 1; + surface.mipCount = color.mipCount; surface.baseLevel = 0; - surface.lastLevel = 0; + surface.lastLevel = color.mipCount - 1; surface.tileMode = TextureTileMode::kR64KBX; surface.dimension = TextureDimension::k2D; surface.format = GuestFormatFor(color.format, color.elementBytes); @@ -1135,8 +1138,10 @@ void recordDraw(const Context& context, const State& state, const Pm4::DrawParam if (continued) { record.pipeline->Continue(commands, state.viewport, state.scissor); } else { - const VkMemoryBarrier before{VK_STRUCTURE_TYPE_MEMORY_BARRIER, nullptr, VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_TRANSFER_WRITE_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT, VK_ACCESS_COLOR_ATTACHMENT_READ_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT | VK_ACCESS_INDEX_READ_BIT | VK_ACCESS_VERTEX_ATTRIBUTE_READ_BIT | VK_ACCESS_UNIFORM_READ_BIT | VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT}; - context.Resolved(&DeviceFunctions::cmdPipelineBarrier, "vkCmdPipelineBarrier")(commands, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT | VK_PIPELINE_STAGE_VERTEX_INPUT_BIT | inputs.shaderStages, 0, 1, &before, 0, nullptr, 0, nullptr); + // Depth surfaces stay in the general layout too; an earlier pass's depth writes (the recorder's + // trailing barrier after a pass does not name them) are made visible here. + const VkMemoryBarrier before{VK_STRUCTURE_TYPE_MEMORY_BARRIER, nullptr, VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_TRANSFER_WRITE_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT | VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT, VK_ACCESS_COLOR_ATTACHMENT_READ_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT | VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_READ_BIT | VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT | VK_ACCESS_INDEX_READ_BIT | VK_ACCESS_VERTEX_ATTRIBUTE_READ_BIT | VK_ACCESS_UNIFORM_READ_BIT | VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT}; + context.Resolved(&DeviceFunctions::cmdPipelineBarrier, "vkCmdPipelineBarrier")(commands, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT | VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT | VK_PIPELINE_STAGE_LATE_FRAGMENT_TESTS_BIT | VK_PIPELINE_STAGE_VERTEX_INPUT_BIT | inputs.shaderStages, 0, 1, &before, 0, nullptr, 0, nullptr); countBarrier(1); APS5_LOG_CHARS_OUT_DEBUG("Upload barrier recorded"); if (gpuIndirect) rewritten = recordIndirectArguments(context, commands, recorder, true, *record.indirect, scratch, argumentBuffer, argumentOffset, countBarrier); @@ -1281,15 +1286,16 @@ void Draw(const Context& context, const State& state, const Pm4::DrawParameters& binding.resident = refreshResidentTarget(state, color, outcome, profile, [&] { auto resident = CachedStorageSurface(context, SurfaceForTarget(color)); Require(resident->Attachable(), "storage format cannot be a color attachment"); - Require(resident->GuestBytes() == colorLayout.Bytes(), "resident image layout differs from the color layout"); + Require(color.mipCount > 1 || resident->GuestBytes() == colorLayout.Bytes(), "resident image layout differs from the color layout"); return resident; }); } if (binding.resident != nullptr) { timer.phase(PhaseReadTarget); - targetViews.push_back(binding.resident->AttachmentView(color.format)); + targetViews.push_back(binding.resident->AttachmentView(color.format, color.mip)); continue; } + Require(!color.mipTail, "rendering into a packed mip tail needs the resident image of its surface"); if (binding.gpuTiling) { binding.mip = ColorTargetMip(color, colorLayout); binding.tiled = std::make_unique(context, colorLayout.Bytes(), copies); @@ -1319,6 +1325,8 @@ void Draw(const Context& context, const State& state, const Pm4::DrawParameters& binding.target = std::make_unique(context, color, state.blends[index].blendEnable != 0); targetViews.push_back(binding.target->View()); } + // The depth surface is the last attachment (Pipeline::AcquireFramebuffer). + if (state.depth) targetViews.push_back(DepthSurfaceView(context, *state.depth)); timer.phase(PhasePrepare); const bool recordDraws = RecordDraws(); auto* recorder = Recorder::Active(); @@ -1463,7 +1471,8 @@ void Draw(const Context& context, const State& state, const Pm4::DrawParameters& // only a template the resource cache serves under this content key (reusable: no lease, // no copied writes, every direct region import- or mirror-served) of a direct draw, so a // hit's proof is the template's ProveCurrent and nothing needs completion work. - if (recipeOut != nullptr && DrawRecipes() && cacheable && !outcome.waited && args == nullptr && resources->Reusable()) { + // ponytail: none for depth draws (the recipe replays color targets only); add the depth view when they need the hit path. + if (recipeOut != nullptr && DrawRecipes() && cacheable && !outcome.waited && args == nullptr && resources->Reusable() && !state.depth) { auto recipe = std::make_shared(); recipe->device = context.device; recipe->templateRef = resources; @@ -1536,6 +1545,10 @@ void Draw(const Context& context, const State& state, const Pm4::DrawParameters& upload.dstAccessMask = VK_ACCESS_TRANSFER_READ_BIT | VK_ACCESS_INDEX_READ_BIT | VK_ACCESS_VERTEX_ATTRIBUTE_READ_BIT | VK_ACCESS_UNIFORM_READ_BIT | VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT; context.Resolved(&DeviceFunctions::cmdPipelineBarrier, "vkCmdPipelineBarrier")(commands, VK_PIPELINE_STAGE_HOST_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT | VK_PIPELINE_STAGE_VERTEX_INPUT_BIT | inputs.shaderStages, 0, 1, &upload, 0, nullptr, 0, nullptr); countBarrier(); + if (state.depth) { + RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT | VK_PIPELINE_STAGE_LATE_FRAGMENT_TESTS_BIT, VK_ACCESS_TRANSFER_WRITE_BIT | VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT, VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_READ_BIT | VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT); + countBarrier(); + } APS5_LOG_CHARS_OUT_DEBUG("Upload barrier recorded"); if (gpuIndirect) rewritten = recordIndirectArguments(context, commands, recorder, recorded, indirect, scratch, argumentBuffer, argumentOffset, countBarrier); for (auto& binding : targets) { diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestTextureResource.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestTextureResource.cpp index 3cd364eae..cf4db9175 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestTextureResource.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestTextureResource.cpp @@ -1,6 +1,7 @@ #include #include "prx/libSceAgcDriver/Graphics/include/GuestTextureResource.hpp" #include "prx/libSceAgcDriver/Graphics/include/Context.hpp" +#include "prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp" #include #include #include @@ -46,6 +47,12 @@ GuestTextureResource DecodeTextureResource(std::span words) const auto base40 = (static_cast(words[0]) | (static_cast(words[1]) << 32u)) & 0xffffffffffull; const auto baseAddress = base40 << 8u; Require(baseAddress != 0, "guest texture descriptor has a null base address"); + // A depth surface's contents live on the GPU only; its guest memory is stale. + if (DepthSurfaceAt(baseAddress)) { + char text[96]; + std::snprintf(text, sizeof(text), "AGC graphics: sampling depth/stencil surface 0x%llx is not implemented", static_cast(baseAddress)); + throw std::runtime_error(text); + } const auto minLod = (words[1] >> 8u) & 0xfffu; const auto format = (words[1] >> 20u) & 0x1ffu; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp index af75014dd..c06f4471b 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp @@ -41,7 +41,7 @@ void ValidateViewport(const Context& context, const VkViewport& viewport) { Require(std::min(viewport.y, viewport.y + viewport.height) >= context.limits.viewportBoundsRange[0] && std::max(viewport.y, viewport.y + viewport.height) <= context.limits.viewportBoundsRange[1], "viewport Y exceeds device bounds"); } -Pipeline::Pipeline(const Context& context, const State& state, const VertexInputLayout& vertexInput, const ShaderResources& resources, std::span shaders, VkImageLayout attachmentLayout) : context(context), _modules(shaders.size()), attachments(state.colors.size()) { +Pipeline::Pipeline(const Context& context, const State& state, const VertexInputLayout& vertexInput, const ShaderResources& resources, std::span shaders, VkImageLayout attachmentLayout) : context(context), _modules(shaders.size()), attachments(state.colors.size() + (state.depth ? 1u : 0u)), colorAttachments(state.colors.size()) { // A cached pipeline may outlive its device's teardown (see ClearCachedPipelines); it must not keep // the buffer pool, which is reset with the device, alive past it. this->context.bufferPool.reset(); @@ -105,6 +105,21 @@ Pipeline::Pipeline(const Context& context, const State& state, const VertexInput subpass.pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS; subpass.colorAttachmentCount = static_cast(references.size()); subpass.pColorAttachments = references.empty() ? nullptr : references.data(); + // The depth surface stays in the general layout (DepthSurface.hpp). + const VkAttachmentReference depthReference{static_cast(colors.size()), VK_IMAGE_LAYOUT_GENERAL}; + if (state.depth) { + VkAttachmentDescription depth{}; + depth.format = state.depth->format; + depth.samples = VK_SAMPLE_COUNT_1_BIT; + depth.loadOp = VK_ATTACHMENT_LOAD_OP_LOAD; + depth.storeOp = VK_ATTACHMENT_STORE_OP_STORE; + depth.stencilLoadOp = VK_ATTACHMENT_LOAD_OP_LOAD; + depth.stencilStoreOp = VK_ATTACHMENT_STORE_OP_STORE; + depth.initialLayout = VK_IMAGE_LAYOUT_GENERAL; + depth.finalLayout = VK_IMAGE_LAYOUT_GENERAL; + colors.push_back(depth); + subpass.pDepthStencilAttachment = &depthReference; + } VkRenderPassCreateInfo passInfo{VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO}; passInfo.attachmentCount = static_cast(colors.size()); passInfo.pAttachments = colors.empty() ? nullptr : colors.data(); @@ -138,6 +153,13 @@ Pipeline::Pipeline(const Context& context, const State& state, const VertexInput raster.lineWidth = 1; VkPipelineMultisampleStateCreateInfo samples{VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO}; samples.rasterizationSamples = VK_SAMPLE_COUNT_1_BIT; + VkPipelineDepthStencilStateCreateInfo depthStencil{VK_STRUCTURE_TYPE_PIPELINE_DEPTH_STENCIL_STATE_CREATE_INFO}; + depthStencil.depthTestEnable = state.depthTest; + depthStencil.depthWriteEnable = state.depthWrite; + depthStencil.depthCompareOp = state.depthCompare; + depthStencil.stencilTestEnable = state.stencilTest; + depthStencil.front = state.stencilFront; + depthStencil.back = state.stencilBack; VkPipelineColorBlendStateCreateInfo blend{VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO}; blend.attachmentCount = static_cast(state.blends.size()); blend.pAttachments = state.blends.empty() ? nullptr : state.blends.data(); @@ -155,6 +177,7 @@ Pipeline::Pipeline(const Context& context, const State& state, const VertexInput pipelineInfo.pViewportState = &viewports; pipelineInfo.pRasterizationState = &raster; pipelineInfo.pMultisampleState = &samples; + pipelineInfo.pDepthStencilState = state.depth ? &depthStencil : nullptr; pipelineInfo.pColorBlendState = &blend; pipelineInfo.pDynamicState = &dynamic; pipelineInfo.layout = layout; @@ -202,7 +225,7 @@ VkPipelineLayout Pipeline::Layout() const { } std::shared_ptr Pipeline::AcquireFramebuffer(std::span targets, std::span> owners, VkExtent2D extent) { - Require(targets.size() == attachments && targets.size() == owners.size(), "render targets do not match the pipeline's color attachments"); + Require(targets.size() == attachments && owners.size() == colorAttachments, "render targets do not match the pipeline's attachments"); const bool resident = std::all_of(owners.begin(), owners.end(), [](const auto& owner) { return owner != nullptr; }); if (resident) { // Entries whose views are gone can never match again and go as soon as no recorded draw holds @@ -322,6 +345,16 @@ std::vector pipelineKey(const Context& context, const State& state, c for (const auto value : state.blendConstants) append(key, value); append(key, state.colors.size()); for (const auto& color : state.colors) append(key, color.format); + append(key, state.depth.has_value()); + if (state.depth) { + append(key, state.depth->format); + append(key, state.depthTest); + append(key, state.depthWrite); + append(key, state.depthCompare); + append(key, state.stencilTest); + append(key, state.stencilFront); + append(key, state.stencilBack); + } append(key, state.stages.mesh.has_value()); if (state.stages.mesh) { const auto& mesh = *state.stages.mesh; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderInputState.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderInputState.cpp index e69fe0fb8..a45d6ee4b 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderInputState.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderInputState.cpp @@ -84,7 +84,7 @@ ShaderRecompiler::ShaderComputeStageInfo DecodeComputeStageInfo(const Registers& }; } -ShaderRecompiler::ShaderPixelStageInfo DecodePixelStageInfo(const Registers& context, bool hasColorTarget, std::uint8_t colorComponentMapping) { +ShaderRecompiler::ShaderPixelStageInfo DecodePixelStageInfo(const Registers& context, const std::array& exportMappings) { const auto inControl = read(context, spiPsInControl, RegisterBank::Context); const auto inputNum = inControl & 0x3Fu; if (inputNum > 32u) { @@ -117,11 +117,6 @@ ShaderRecompiler::ShaderPixelStageInfo DecodePixelStageInfo(const Registers& con const bool depthExportEnable = (shaderControl & 0x1u) != 0; const bool sampleMaskExportEnable = ((shaderControl >> 8u) & 0x1u) != 0; const auto zOrder = (shaderControl >> 4u) & 0x3u; - std::array targetExportMapping{}; - targetExportMapping.fill(0xe4u); - if (hasColorTarget) { - targetExportMapping[0] = colorComponentMapping; - } return ShaderRecompiler::ShaderPixelStageInfo{ inputNum, interpolatorSettings, @@ -142,7 +137,7 @@ ShaderRecompiler::ShaderPixelStageInfo DecodePixelStageInfo(const Registers& con zOrder == 1u && !pixelKillEnable && !depthExportEnable && !sampleMaskExportEnable, ((shaderControl >> 10u) & 0x1u) != 0, targetOutputMode, - targetExportMapping + exportMappings }; } diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp index dcc5a608e..9c771a7ec 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp @@ -79,8 +79,117 @@ bool IgnoreDepthTest() { return ignore; } -// DB_DEPTH_CONTROL: depth and stencil tests whose function is ALWAYS and that write no depth cannot -// change color output, so they are accepted without a depth target (stencil writes are dropped). +// Whether DB_Z_INFO or DB_STENCIL_INFO names a plane (an absent register names none). +bool depthSurfaceBound(const Registers& cx) { + const auto z = find(cx, 0x010); + const auto stencil = find(cx, 0x011); + return (z != cx.end() && (z->second & 3u) != 0) || (stencil != cx.end() && (stencil->second & 1u) != 0); +} + +// One face of the stencil state (the compare function, DB_STENCIL_CONTROL's three operations and +// DB_STENCILREFMASK) as Vulkan's. AMD's compare functions are in Vulkan's order. Vulkan has one +// reference per face for the test and every replacement, so a replacement by another value +// (REPLACE_OP writes STENCILOPVAL, ONES 0xff) is taken where the bits it writes are not also compared. +VkStencilOpState stencilFace(std::uint32_t compare, std::uint32_t ops, std::uint32_t refMask, bool readOnly) { + VkStencilOpState face{}; + face.compareOp = static_cast(compare); + face.compareMask = (refMask >> 8u) & 0xffu; + face.writeMask = readOnly ? 0u : (refMask >> 16u) & 0xffu; + const auto test = refMask & 0xffu; + const auto opValue = refMask >> 24u; + auto reference = test; + // The reference bits already spoken for: the compared ones (none for NEVER and ALWAYS), then + // the replaced ones. + auto fixed = compare == 0 || compare == 7 ? 0u : face.compareMask; + const auto convert = [&](std::uint32_t op) { + if (face.writeMask == 0) return VK_STENCIL_OP_KEEP; + char detail[96]; + switch (op) { + case 0: return VK_STENCIL_OP_KEEP; + case 1: return VK_STENCIL_OP_ZERO; + case 2: + case 3: + case 4: { + const auto value = op == 2 ? 0xffu : op == 3 ? test : opValue; + if (((reference ^ value) & fixed & face.writeMask) != 0) { + std::snprintf(detail, sizeof(detail), "AGC graphics: stencil replacement 0x%02x against test value 0x%02x (masks 0x%02x/0x%02x) is unsupported", value, test, face.compareMask, face.writeMask); + throw std::runtime_error(detail); + } + reference = (reference & ~face.writeMask) | (value & face.writeMask); + fixed |= face.writeMask; + return VK_STENCIL_OP_REPLACE; + } + case 5: + case 6: + case 8: + case 9: + // ADD/SUB add STENCILOPVAL; Vulkan's increments add 1. + if (opValue != 1) { + std::snprintf(detail, sizeof(detail), "AGC graphics: stencil add/subtract of 0x%02x is unsupported", opValue); + throw std::runtime_error(detail); + } + return op == 5 ? VK_STENCIL_OP_INCREMENT_AND_CLAMP : op == 6 ? VK_STENCIL_OP_DECREMENT_AND_CLAMP : op == 8 ? VK_STENCIL_OP_INCREMENT_AND_WRAP : VK_STENCIL_OP_DECREMENT_AND_WRAP; + case 7: return VK_STENCIL_OP_INVERT; + default: + std::snprintf(detail, sizeof(detail), "AGC graphics: stencil operation %u is unsupported", op); + throw std::runtime_error(detail); + } + }; + face.failOp = convert(ops & 0xfu); + face.passOp = convert((ops >> 4u) & 0xfu); + face.depthFailOp = convert((ops >> 8u) & 0xfu); + face.reference = reference; + return face; +} + +// The depth/stencil surface and tests of a draw that tests depth or stencil with a plane bound. +void decodeDepth(const Registers& cx, std::uint32_t depthControl, State& result) { + // DEPTH/STENCIL_COMPRESS_DISABLE (bits 5, 6) and the ordering hints from bit 13 on do not change + // what is drawn. + zero(cx, 0x000, 0x00001f9fu, "depth/stencil clear, copy, resummarize or decompress draws (DB_RENDER_CONTROL)"); + const auto view = read(cx, 0x002); + // Z_READ_ONLY and STENCIL_READ_ONLY (bits 24, 25) turn the writes off. + zero(cx, 0x002, ~0x03000000u, "depth array slices or mips (DB_DEPTH_VIEW)"); + // NUM_SAMPLES, PARTIALLY_RESIDENT, MAXMIP; the rest is tiling and compression the host image has none of. + zero(cx, 0x010, 0x000f100cu, "multisampled, partially resident or mipmapped depth (DB_Z_INFO)"); + zero(cx, 0x011, 0x00001000u, "partially resident stencil (DB_STENCIL_INFO)"); + const auto zFormat = read(cx, 0x010) & 3u; + const bool stencil = (read(cx, 0x011) & 1u) != 0; + Require(zFormat != 2, "Z_24 depth is unsupported"); + Require(zFormat != 0 || (depthControl & 2u) == 0, "depth test without a depth plane"); + Require(stencil || (depthControl & 1u) == 0, "stencil test without a stencil plane"); + const auto base = [&](std::uint32_t low, std::uint32_t highOffset) { + const auto high = find(cx, highOffset); + return (high == cx.end() ? 0ull : static_cast(high->second & 0xffu) << 40u) | (static_cast(read(cx, low)) << 8u); + }; + const bool depthReadOnly = (view & 0x01000000u) != 0; + const bool stencilReadOnly = (view & 0x02000000u) != 0; + DepthTarget depth{}; + depth.address = zFormat != 0 ? base(0x012, 0x01a) : 0; + depth.stencilAddress = stencil ? base(0x013, 0x01b) : 0; + Require(zFormat == 0 || depthReadOnly || base(0x014, 0x01c) == depth.address, "depth read and written at different addresses is unsupported"); + Require(!stencil || stencilReadOnly || base(0x015, 0x01d) == depth.stencilAddress, "stencil read and written at different addresses is unsupported"); + const auto size = read(cx, 0x007); + depth.extent = {(size & 0x3fffu) + 1u, ((size >> 16u) & 0x3fffu) + 1u}; + // A stencil-only surface gets an unused depth plane. + depth.format = zFormat == 1 ? (stencil ? VK_FORMAT_D16_UNORM_S8_UINT : VK_FORMAT_D16_UNORM) : (stencil ? VK_FORMAT_D32_SFLOAT_S8_UINT : VK_FORMAT_D32_SFLOAT); + depth.clearDepth = readFloat(cx, 0x00b); + depth.clearStencil = static_cast(read(cx, 0x00a) & 0xffu); + result.depth = depth; + result.depthTest = (depthControl & 2u) != 0; + result.depthWrite = result.depthTest && (depthControl & 4u) != 0 && !depthReadOnly; + result.depthCompare = static_cast((depthControl >> 4u) & 7u); + result.stencilTest = (depthControl & 1u) != 0; + if (result.stencilTest) { + const auto ops = read(cx, 0x10b); + result.stencilFront = stencilFace((depthControl >> 8u) & 7u, ops, read(cx, 0x10c), stencilReadOnly); + // BACKFACE_ENABLE gives back faces their own function, operations and masks. + result.stencilBack = (depthControl & 0x80u) != 0 ? stencilFace((depthControl >> 20u) & 7u, ops >> 12u, read(cx, 0x10d), stencilReadOnly) : result.stencilFront; + } +} + +// DB_DEPTH_CONTROL without a bound surface: depth and stencil tests whose function is ALWAYS and +// that write no depth cannot change color output, so they are accepted (stencil writes go nowhere). bool depthPassThrough(std::uint32_t depthControl) { const bool stencil = (depthControl & 1u) != 0; const bool depth = (depthControl & 2u) != 0; @@ -129,44 +238,50 @@ VkBlendOp blendOp(std::uint32_t value) { struct DecodedColorFormat { VkFormat format; std::uint32_t elementBytes; + // The exported component each attachment component stores, two bits each (0xe4 is identity). + std::uint8_t componentMapping = 0xe4u; }; // CB_COLOR_INFO FORMAT / NUMBER_TYPE / COMP_SWAP to a Vulkan attachment format. AMD formats list // components from the least significant bits, as Vulkan's non-packed formats do. DecodedColorFormat DecodeColorFormat(std::uint32_t format, std::uint32_t number, std::uint32_t swap) { - constexpr std::uint32_t unorm = 0, sint = 5, srgb = 6, floating = 7; + constexpr std::uint32_t unorm = 0, snorm = 1, sint = 5, srgb = 6, floating = 7; constexpr std::uint32_t uint = 4; const auto fail = [&]() -> DecodedColorFormat { throw std::runtime_error("AGC graphics: unsupported color format " + std::to_string(format) + " number type " + std::to_string(number) + " component swap " + std::to_string(swap)); }; const bool alternate = swap == 1; - if (swap > 1) return fail(); + // A one-component format stores the exported component COMP_SWAP names: R, G, B or A (an A8 + // target is COLOR_8 with SWAP_ALT_REV). + const auto single = [&](VkFormat vkFormat, std::uint32_t bytes) { return DecodedColorFormat{vkFormat, bytes, static_cast((0xe4u & ~3u) | swap)}; }; + if (swap > 1 && format != 1 && format != 2 && format != 4) return fail(); switch (format) { case 1: - if (swap != 0) return fail(); - if (number == unorm) return {VK_FORMAT_R8_UNORM, 1}; - if (number == uint) return {VK_FORMAT_R8_UINT, 1}; + if (number == unorm) return single(VK_FORMAT_R8_UNORM, 1); + if (number == snorm) return single(VK_FORMAT_R8_SNORM, 1); + if (number == uint) return single(VK_FORMAT_R8_UINT, 1); return fail(); case 2: - if (swap != 0) return fail(); - if (number == unorm) return {VK_FORMAT_R16_UNORM, 2}; - if (number == floating) return {VK_FORMAT_R16_SFLOAT, 2}; - if (number == uint) return {VK_FORMAT_R16_UINT, 2}; + if (number == unorm) return single(VK_FORMAT_R16_UNORM, 2); + if (number == snorm) return single(VK_FORMAT_R16_SNORM, 2); + if (number == floating) return single(VK_FORMAT_R16_SFLOAT, 2); + if (number == uint) return single(VK_FORMAT_R16_UINT, 2); return fail(); case 3: if (swap != 0) return fail(); if (number == unorm) return {VK_FORMAT_R8G8_UNORM, 2}; + if (number == snorm) return {VK_FORMAT_R8G8_SNORM, 2}; return fail(); case 4: - if (swap != 0) return fail(); - if (number == floating) return {VK_FORMAT_R32_SFLOAT, 4}; - if (number == uint) return {VK_FORMAT_R32_UINT, 4}; - if (number == sint) return {VK_FORMAT_R32_SINT, 4}; + if (number == floating) return single(VK_FORMAT_R32_SFLOAT, 4); + if (number == uint) return single(VK_FORMAT_R32_UINT, 4); + if (number == sint) return single(VK_FORMAT_R32_SINT, 4); return fail(); case 5: if (swap != 0) return fail(); if (number == floating) return {VK_FORMAT_R16G16_SFLOAT, 4}; if (number == unorm) return {VK_FORMAT_R16G16_UNORM, 4}; + if (number == snorm) return {VK_FORMAT_R16G16_SNORM, 4}; if (number == uint) return {VK_FORMAT_R16G16_UINT, 4}; return fail(); case 6: @@ -179,6 +294,7 @@ DecodedColorFormat DecodeColorFormat(std::uint32_t format, std::uint32_t number, return {alternate ? VK_FORMAT_A2R10G10B10_UNORM_PACK32 : VK_FORMAT_A2B10G10R10_UNORM_PACK32, 4}; case 10: if (number == unorm) return {alternate ? VK_FORMAT_B8G8R8A8_UNORM : VK_FORMAT_R8G8B8A8_UNORM, 4}; + if (number == snorm) return {alternate ? VK_FORMAT_B8G8R8A8_SNORM : VK_FORMAT_R8G8B8A8_SNORM, 4}; if (number == srgb) return {alternate ? VK_FORMAT_B8G8R8A8_SRGB : VK_FORMAT_R8G8B8A8_SRGB, 4}; return fail(); case 11: @@ -190,6 +306,7 @@ DecodedColorFormat DecodeColorFormat(std::uint32_t format, std::uint32_t number, if (swap != 0) return fail(); if (number == floating) return {VK_FORMAT_R16G16B16A16_SFLOAT, 8}; if (number == unorm) return {VK_FORMAT_R16G16B16A16_UNORM, 8}; + if (number == snorm) return {VK_FORMAT_R16G16B16A16_SNORM, 8}; if (number == uint) return {VK_FORMAT_R16G16B16A16_UINT, 8}; return fail(); case 14: @@ -304,7 +421,9 @@ State DecodeState(const QueueState& queue) { zero(cx, 0x207, ~LayerExports, "clip distances, layer, viewport or auxiliary vertex exports"); { const auto depthControl = read(cx, 0x200); - if (depthPassThrough(depthControl)) { + if ((depthControl & 3u) != 0 && depthSurfaceBound(cx)) { + decodeDepth(cx, depthControl, result); + } else if (depthPassThrough(depthControl)) { static bool reported = false; if (!reported) { reported = true; @@ -378,7 +497,9 @@ State DecodeState(const QueueState& queue) { for (std::uint32_t slot = 0; slot < slotCount; ++slot) { // Export formats only matter for the targets the draw writes. const auto slotExport = (exportFormat >> (4u * slot)) & 0xfu; - Require(slotExport == 4 || slotExport == 9, "only FP16_ABGR or 32_ABGR color export is supported"); + // FP16, UNORM16 and SNORM16 exports are unpacked to floats by the shader; 32_R, 32_GR, 32_AR and + // 32_ABGR are written as is, the components a format leaves out reading as 0 (alpha 1). + if (slotExport == 0 || slotExport == 7 || slotExport == 8 || slotExport > 9) throw std::runtime_error("AGC graphics: color export format " + std::to_string(slotExport) + " is unsupported"); const auto stride = slot * 0xfu; ColorTarget color{}; const auto info = read(cx, 0x31c + stride); @@ -388,13 +509,15 @@ State DecodeState(const QueueState& queue) { const auto format = (info >> 2u) & 0x1fu; const auto decoded = DecodeColorFormat(format, number, swap); // ROUND_MODE (bit 18) only affects unorm rounding. With DCC_ENABLE (bit 28) the target is written - // uncompressed and only its fast-clear keys matter (see DccMetadata.hpp). - Require((info & ~(0x00029f7cu | 0x00040000u | 0x10000000u)) == 0, "color compression, DCC, endian conversion, nonstandard rounding or color optimization is unsupported"); - Require((info & 0x8000u) != 0 || number == 7, "unclamped normalized color is unsupported"); - // CB_COLOR_VIEW: MIP_LEVEL (bits 24-27) selects the rendered mip; array slices are not modeled. + // uncompressed and only its fast-clear keys matter (see DccMetadata.hpp). BLEND_BYPASS (bit 16) is + // checked against enabled blending below. + if ((info & ~(0x00039f7cu | 0x00040000u | 0x10000000u)) != 0) throw std::runtime_error("AGC graphics: color compression, DCC, endian conversion, nonstandard rounding or color optimization is unsupported (CB_COLOR_INFO 0x" + [&] { char text[16]; std::snprintf(text, sizeof(text), "%08x", info); return std::string(text); }() + ")"); + Require((info & 0x8000u) != 0 || number == 7 || number == 4 || number == 5, "unclamped normalized color is unsupported"); + // CB_COLOR_VIEW (GFX10: SLICE_START 0-12, SLICE_MAX 13-25, MIP_LEVEL 26-29): MIP_LEVEL selects + // the rendered mip; array slices are not modeled. const auto view = read(cx, 0x31b + stride); - Require((view & ~0x0f000000u) == 0, "color array views are unsupported"); - const auto viewMip = (view >> 24u) & 0xfu; + Require((view & ~0x3c000000u) == 0, "color array views are unsupported"); + const auto viewMip = (view >> 26u) & 0xfu; zero(cx, 0x31d + stride, ~0u, "color samples, fragments or destination alpha override"); const auto attrib2 = read(cx, 0x3b0 + slot); const auto maxMip = attrib2 >> 28u; @@ -404,22 +527,26 @@ State DecodeState(const QueueState& queue) { color.extent = {((attrib2 >> 14u) & 0x3fffu) + 1u, (attrib2 & 0x3fffu) + 1u}; color.elementBytes = decoded.elementBytes; std::uint64_t mipOffset = 0; + color.surfaceExtent = color.extent; + color.mipCount = maxMip + 1u; + color.mip = viewMip; if (maxMip != 0) { // A mipmapped surface is addressed like a texture; the view renders into one mip of it. const auto mips = ComputeElementMipLayout(color.tileMode == ColorTileMode::Linear ? TextureTileMode::kLinear : TextureTileMode::kR64KBX, color.elementBytes, color.extent.width, color.extent.height, maxMip + 1u); const auto& mip = mips.at(viewMip); - Require(!mip.tail, "rendering into a packed mip tail is unsupported"); mipOffset = mip.tiledOffset; + color.mipTail = mip.tail; color.extent = {mip.width, mip.height}; } const ColorTargetLayout colorLayout(color.extent.width, color.extent.height, color.tileMode, color.elementBytes); const auto high = read(cx, 0x390 + slot); Require((high & ~0xffu) == 0, "invalid color address extension"); - color.address = ((static_cast(high) << 40u) | (static_cast(read(cx, 0x318 + stride)) << 8u)) + mipOffset; + color.surfaceAddress = (static_cast(high) << 40u) | (static_cast(read(cx, 0x318 + stride)) << 8u); + color.address = color.surfaceAddress + mipOffset; color.bytes = colorLayout.Bytes(); GuestMemory::CheckRange(reinterpret_cast(color.address), color.bytes, colorLayout.Alignment(), true); color.format = decoded.format; - color.componentMapping = 0xe4u; + color.componentMapping = decoded.componentMapping; if ((info & 0x10000000u) != 0) { if (maxMip == 0) { const auto dccHigh = find(cx, 0x3a8 + slot); @@ -443,6 +570,9 @@ State DecodeState(const QueueState& queue) { } if (result.hasColorTarget) { result.color = result.colors.front(); + if (result.depth) result.renderExtent = {std::min(result.renderExtent.width, result.depth->extent.width), std::min(result.renderExtent.height, result.depth->extent.height)}; + } else if (result.depth) { + result.renderExtent = result.depth->extent; } else { const auto screenBottomRight = read(cx, 0xd); APS5_LOG_OUT_DEBUG("No color target, screen BR register=0x%x", screenBottomRight); @@ -478,7 +608,13 @@ State DecodeState(const QueueState& queue) { APS5_LOG_OUT_DEBUG("Blend %u control=0x%x", slot, blend); Require((blend & 0x0000e000u) == 0, "reserved blend control bits"); VkPipelineColorBlendAttachmentState state{}; - state.colorWriteMask = (targetMask >> (4u * slot)) & 0xfu; + // CB_TARGET_MASK names exported components; attachment component c stores exported component + // mapping[c], so its write bit and, when it stores alpha, its blend come from that component. + const auto mapping = result.colors[slot].componentMapping; + const auto exportedMask = (targetMask >> (4u * slot)) & 0xfu; + for (std::uint32_t component = 0; component < 4; ++component) { + if (((exportedMask >> ((mapping >> (2u * component)) & 3u)) & 1u) != 0) state.colorWriteMask |= 1u << component; + } state.blendEnable = (blend >> 30u) & 1u; if (state.blendEnable) { Require((read(cx, 0x31c + slot * 0xfu) & 0x10000u) == 0, "blend bypass conflicts with enabled blending"); @@ -489,6 +625,11 @@ State DecodeState(const QueueState& queue) { state.srcAlphaBlendFactor = blendFactor(alpha & 0x1fu); state.dstAlphaBlendFactor = blendFactor((alpha >> 8u) & 0x1fu); state.alphaBlendOp = blendOp((alpha >> 5u) & 7u); + if ((mapping & 3u) == 3u) { + state.srcColorBlendFactor = state.srcAlphaBlendFactor; + state.dstColorBlendFactor = state.dstAlphaBlendFactor; + state.colorBlendOp = state.alphaBlendOp; + } for (std::uint32_t i = 0; i < 4; ++i) result.blendConstants[i] = readFloat(cx, 0x105 + i); } result.blends.push_back(state); @@ -498,6 +639,13 @@ State DecodeState(const QueueState& queue) { return result; } +std::array ExportMappings(const State& state) { + std::array mappings{}; + mappings.fill(0xe4u); + for (std::size_t slot = 0; slot < state.colors.size() && slot < mappings.size(); ++slot) mappings[slot] = state.colors[slot].componentMapping; + return mappings; +} + std::string DrawRejection(const QueueState& queue, bool indexed) { const auto& cx = queue.context; // A register a rule needs that is absent gives no verdict here: DecodeState reports it. @@ -517,7 +665,9 @@ std::string DrawRejection(const QueueState& queue, bool indexed) { if (auto reason = indexed ? nonzero(queue.userConfig, 0x24b, ~0u, "primitive restart (GE_MULTI_PRIM_IB_RESET_EN)") : std::string(); !reason.empty()) return reason; if (auto reason = nonzero(cx, 0x207, ~LayerExports, "clip distances, layer, viewport or auxiliary vertex exports"); !reason.empty()) return reason; if (value(cx, 0x200, word)) { - if (!depthPassThrough(word) && !IgnoreDepthTest() && (word & DepthControlMask) != 0) return zeroMessage(0x200, word, "depth, stencil or conditional color writes"); + // Tests against a bound surface are decoded (and their limits checked) by DecodeState. + const bool surface = (word & 3u) != 0 && depthSurfaceBound(cx); + if (!surface && !depthPassThrough(word) && !IgnoreDepthTest() && (word & DepthControlMask) != 0) return zeroMessage(0x200, word, "depth, stencil or conditional color writes"); if (auto reason = require((word & 0xc0000008u) == 0, "depth bounds or depth-conditional color writes are unsupported"); !reason.empty()) return reason; } if (auto reason = nonzero(cx, 0x203, ShaderControlMask, "depth export, shader coverage or ordered fragment execution"); !reason.empty()) return reason; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp index 74f55c1a8..7f5008dd6 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp @@ -866,9 +866,10 @@ VkImageView StorageTexture::createView(std::uint32_t mip) const { return created; } -VkImageView StorageTexture::AttachmentView(VkFormat format) { +VkImageView StorageTexture::AttachmentView(VkFormat format, std::uint32_t mip) { Require(attachable, "storage image cannot be a color attachment"); - const auto found = attachmentViews.find(format); + Require(mip < descriptor.mipCount, "attachment mip exceeds the storage image"); + const auto found = attachmentViews.find({format, mip}); if (found != attachmentViews.end()) return found->second; VkImageViewUsageCreateInfo usage{VK_STRUCTURE_TYPE_IMAGE_VIEW_USAGE_CREATE_INFO}; usage.usage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT; @@ -878,10 +879,10 @@ VkImageView StorageTexture::AttachmentView(VkFormat format) { viewInfo.viewType = VK_IMAGE_VIEW_TYPE_2D; viewInfo.format = format; viewInfo.components = {VK_COMPONENT_SWIZZLE_IDENTITY, VK_COMPONENT_SWIZZLE_IDENTITY, VK_COMPONENT_SWIZZLE_IDENTITY, VK_COMPONENT_SWIZZLE_IDENTITY}; - viewInfo.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1u, descriptor.baseArray, 1u}; + viewInfo.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, mip, 1u, descriptor.baseArray, 1u}; VkImageView created = VK_NULL_HANDLE; Check(context.Function("vkCreateImageView")(context.device, &viewInfo, nullptr, &created), "vkCreateImageView attachment"); - attachmentViews.emplace(format, created); + attachmentViews.emplace(std::pair{format, mip}, created); return created; } diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/TextureTiling.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/TextureTiling.cpp index 49af94ea1..48506c345 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/TextureTiling.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/TextureTiling.cpp @@ -1,6 +1,7 @@ #include "prx/libSceAgcDriver/Graphics/include/TextureTiling.hpp" #include "prx/libSceAgcDriver/Graphics/include/Context.hpp" #include "prx/libSceAgcDriver/Graphics/include/TextureFormat.hpp" +#include #include #include #include @@ -245,7 +246,7 @@ std::array ThickBlockExtent(TextureTileMode tileMode, std::uin switch (tileMode) { case TextureTileMode::kStandard4KB: return {1u << thick4KB[index][0], 1u << thick4KB[index][1], 1u << thick4KB[index][2]}; case TextureTileMode::kStandard64KB: return {1u << thick64KB[index][0], 1u << thick64KB[index][1], 1u << thick64KB[index][2]}; - default: throw std::runtime_error("AGC graphics: 3D textures are only supported linear or in SW_4KB_S / SW_64KB_S"); + default: throw std::runtime_error("AGC graphics: 3D textures are only supported linear or in SW_4KB_S / SW_64KB_S, not in XOR swizzle " + std::to_string(XorSwizzleMode(tileMode)) + " at " + std::to_string(bytesPerElement) + " bytes per element"); } } @@ -289,6 +290,19 @@ ThickLayout ComputeThickLayout(TextureTileMode tileMode, std::uint32_t format, s SurfaceGeometry DescribeSurface(const GuestTextureResource& descriptor) { SurfaceGeometry geometry; + if (descriptor.dimension == TextureDimension::k3D && (descriptor.tileMode == TextureTileMode::kD64KBX || descriptor.tileMode == TextureTileMode::kR64KBX)) { + // addrlib keeps display and rotated swizzles thin for 3D (only Z and standard ones are thick): + // each depth slice is laid out as an array slice, its index XORed in through the equation. + Require(descriptor.mipCount == 1, "mipmapped 3D textures are not implemented"); + const auto depth = descriptor.depthOrLastArray + 1u; + geometry.mips = ComputeMipLayout(descriptor.tileMode, descriptor.format, descriptor.width, descriptor.height, 1u); + geometry.layers = depth; + geometry.imageDepth = depth; + geometry.guestBytes = ComputeSurfaceSize(geometry.mips, depth); + geometry.layerBytes = geometry.guestBytes / depth; + for (const auto& mip : geometry.mips) geometry.sliceLinearBytes = std::max(geometry.sliceLinearBytes, mip.linearOffset + mip.linearSize); + return geometry; + } if (descriptor.dimension == TextureDimension::k3D) { Require(descriptor.mipCount == 1, "mipmapped 3D textures are not implemented"); const auto depth = descriptor.depthOrLastArray + 1u; diff --git a/core/libs/prx/libSceAgcDriver/Resource/src/Lifecycle.cpp b/core/libs/prx/libSceAgcDriver/Resource/src/Lifecycle.cpp index 8ff5e49b4..78757ea71 100644 --- a/core/libs/prx/libSceAgcDriver/Resource/src/Lifecycle.cpp +++ b/core/libs/prx/libSceAgcDriver/Resource/src/Lifecycle.cpp @@ -5,16 +5,17 @@ #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" +static constexpr uint32_t AGC_EINVAL = 0x80020065u; + extern "C" { uint32_t APS5_VABI sceAgcDriverInitResourceRegistration(void) { - NotImplemented_nid_no_patch(__func__); return 0; } uint32_t APS5_VABI sceAgcDriverQueryResourceRegistrationUserMemoryRequirements(uint64_t* size_in_bytes) { - (void)size_in_bytes; - NotImplemented_nid_no_patch(__func__); + if (!size_in_bytes) return AGC_EINVAL; + *size_in_bytes = 0x4000ULL; return 0; } diff --git a/core/libs/prx/libSceAgcDriver/Resource/src/Registration.cpp b/core/libs/prx/libSceAgcDriver/Resource/src/Registration.cpp index 4de64c2dd..7aec5262e 100644 --- a/core/libs/prx/libSceAgcDriver/Resource/src/Registration.cpp +++ b/core/libs/prx/libSceAgcDriver/Resource/src/Registration.cpp @@ -30,7 +30,6 @@ int APS5_VABI sceAgcDriverRegisterResource(uint32_t* resource_handle, uint32_t o int APS5_VABI sceAgcDriverRegisterWorkloadStream(uint32_t stream_id, const void* stream) { (void)stream_id; (void)stream; - NotImplemented_nid_no_patch(__func__); return 0; } diff --git a/core/libs/prx/libSceAgcDriver/State/src/Configuration.cpp b/core/libs/prx/libSceAgcDriver/State/src/Configuration.cpp index fa1ae944e..08c81ffb4 100644 --- a/core/libs/prx/libSceAgcDriver/State/src/Configuration.cpp +++ b/core/libs/prx/libSceAgcDriver/State/src/Configuration.cpp @@ -2,23 +2,23 @@ #include #include +#include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" extern "C" { +// Tessellation runs as Vulkan tessellation stages, which keep hull-shader outputs and tessellation +// factors on the host GPU: the off-chip hull memory and the tessellation-factor ring are never read. int APS5_VABI sceAgcDriverSetHsOffchipParam(uint64_t value0, uint64_t value1, uint64_t value2) { (void)value0; (void)value1; (void)value2; - NotImplemented_nid_no_patch(__func__); return 0; } int APS5_VABI sceAgcDriverSetTFRing(const volatile void* base, uint32_t size) { - (void)base; - (void)size; - NotImplemented_nid_no_patch(__func__); + if (base == nullptr || size == 0) throw std::invalid_argument("sceAgcDriverSetTFRing: empty ring"); return 0; } diff --git a/core/libs/prx/libSceAgcDriver/tests/ColorTargetLayout.cpp b/core/libs/prx/libSceAgcDriver/tests/ColorTargetLayout.cpp index 1c96fbee9..5b3c912d5 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ColorTargetLayout.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ColorTargetLayout.cpp @@ -1,7 +1,10 @@ #include "BdaTests.hpp" #include "prx/libSceAgcDriver/Graphics/include/ColorTargetTransfer.hpp" +#include #include +#include #include +#include #include namespace { @@ -25,7 +28,8 @@ void RunColorTargetLayoutTests() { reject([] { DecodeColorTileMode(0xcdc6c000); }); reject([] { DecodeColorTileMode(0x09014000); }); reject([] { ColorTargetLayout(0, 1, ColorTileMode::RenderTarget); }); - reject([] { ColorTargetLayout(63, 1, ColorTileMode::Linear); }); + const ColorTargetLayout padded(63, 2, ColorTileMode::Linear); + Require(padded.Bytes() == 512 && padded.LinearBytes() == 504 && padded.Offset(0, 1) == 256, "linear rows are not padded to 256 bytes"); const ColorTargetLayout screen(3840, 2160, ColorTileMode::RenderTarget); Require(screen.Bytes() == 33423360 && screen.LinearBytes() == 33177600 && screen.Alignment() == 65536, "4K color backing layout is incorrect"); const ColorTargetLayout layout(257, 129, ColorTileMode::RenderTarget); @@ -55,8 +59,10 @@ void RunColorTargetLayoutTests() { } reject([&] { layout.Detile(std::span(tiled).first(4), restored); }); reject([&] { layout.Tile(std::span(linear).first(4), tiled); }); - alignas(65536) static std::array guest{}; - guest.fill(std::byte{0x6b}); + // Aligned by hand: a PE section cannot ask for 64 KiB alignment. + static std::vector storage(2 * 65536); + const std::span guest(reinterpret_cast((reinterpret_cast(storage.data()) + 0xffffu) & ~std::uintptr_t{0xffffu}), 65536); + std::fill(guest.begin(), guest.end(), std::byte{0x6b}); ColorTarget target{reinterpret_cast(guest.data()), {2, 2}, VK_FORMAT_R8G8B8A8_UNORM, guest.size(), 0xe4, ColorTileMode::RenderTarget}; std::array pixels{}; pixels.fill(std::byte{0x32}); diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index dacf921c2..d636c3d97 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -99,8 +99,10 @@ void stateTests() { queue.context[0x91] = 0x30020; state = AgcDriver::Graphics::DecodeState(queue); Require(state.scissor.offset.x == 3 && state.scissor.offset.y == 1 && state.scissor.extent.width == 29 && state.scissor.extent.height == 2, "scissor intersection changed"); + // DCC_ENABLE: the target is written uncompressed, its keys at CB_COLOR0_DCC_BASE. queue.context[0x31c] |= 0x10000000; - expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "DCC"); + queue.context[0x325] = 0x1234; + Require(AgcDriver::Graphics::DecodeState(queue).color.dccAddress == 0x123400, "DCC key address decode changed"); queue = makeState(); queue.context.erase(0x3b8); expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "missing register"); @@ -108,11 +110,14 @@ void stateTests() { queue.context[0x3b8] |= 5u << 14u; expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "unsupported color tile mode"); queue = makeState(); + // A linear width off the 256-byte pitch alignment is padded (rows of 64 texels here). queue.context[0x3b0] = (62u << 14u) | 3u; - expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "pitch"); + Require(AgcDriver::Graphics::DecodeState(queue).color.bytes == 64u * 4u * 4u, "padded linear pitch changed"); queue = makeState(); + // Slot 1 written by the shader needs an export format. queue.context[0x8e] = 0xff; - expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "target zero"); + queue.context[0x8f] = 0xff; + expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "color export format 0"); queue = makeState(); queue.context[0x200] = 2; expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "depth"); @@ -128,7 +133,7 @@ void stateTests() { queue.context[0x1b4] = 2; state = AgcDriver::Graphics::DecodeState(queue); Require(AgcDriver::Graphics::DrawRejection(queue, true).empty(), "precheck rejected the reference state"); - static_cast(AgcDriver::Graphics::DecodePixelStageInfo(queue.context, state.hasColorTarget, state.color.componentMapping)); + static_cast(AgcDriver::Graphics::DecodePixelStageInfo(queue.context, AgcDriver::Graphics::ExportMappings(state))); AgcDriver::Graphics::RegisterReadLog() = nullptr; Require(!log.empty(), "the register facade recorded nothing"); for (const auto read : log) Require(AgcDriver::Graphics::DrawKeyCovers(read), "DrawKeyRegisters lacks a register the decoders read: " + std::string(AgcDriver::Graphics::RegisterBankName(read.bank)) + " " + std::to_string(read.offset)); @@ -233,6 +238,74 @@ void DisabledColorTests() { expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "missing register"); } +// Depth/stencil tests against a bound surface: Scaleform's mask pass (stencil ALWAYS, add 1) and +// content pass (stencil EQUAL), as a Scaleform menu draws them. +void DepthStencilTests() { + auto queue = makeState(); + queue.context[0x000] = 0; + queue.context[0x002] = 0; + queue.context[0x007] = (1u << 16u) | 3u; + queue.context[0x00a] = 7; + queue.context[0x00b] = 0; + queue.context[0x010] = 0x22900983; + queue.context[0x011] = 0x20000181; + for (const auto offset : {0x012u, 0x014u}) queue.context[offset] = 0x100; + for (const auto offset : {0x013u, 0x015u}) queue.context[offset] = 0x200; + queue.context[0x10b] = 0x00050050; + queue.context[0x10c] = 0x01ffff01; + queue.context[0x10d] = 0x01000001; + queue.context[0x200] = 0x00700711; + std::vector log; + AgcDriver::Graphics::RegisterReadLog() = &log; + auto state = AgcDriver::Graphics::DecodeState(queue); + AgcDriver::Graphics::RegisterReadLog() = nullptr; + for (const auto read : log) Require(AgcDriver::Graphics::DrawKeyCovers(read), "DrawKeyRegisters lacks a depth register the decoder reads: " + std::to_string(read.offset)); + queue.context[0x1b3] = 2; + queue.context[0x1b4] = 2; + const auto rejection = AgcDriver::Graphics::DrawRejection(queue, false); + Require(rejection.empty(), "precheck rejected a stencil draw with a surface: " + rejection); + Require(state.depth && state.depth->address == 0x10000 && state.depth->stencilAddress == 0x20000 && state.depth->format == VK_FORMAT_D32_SFLOAT_S8_UINT && state.depth->clearStencil == 7, "depth surface decode changed"); + Require(state.renderExtent.width == 4 && state.renderExtent.height == 2, "render extent ignores the depth surface"); + Require(!state.depthTest && !state.depthWrite && state.stencilTest, "depth/stencil enables changed"); + const auto& front = state.stencilFront; + Require(front.compareOp == VK_COMPARE_OP_ALWAYS && front.passOp == VK_STENCIL_OP_INCREMENT_AND_CLAMP && front.failOp == VK_STENCIL_OP_KEEP && front.reference == 1 && front.writeMask == 0xff, "stencil mask pass changed"); + Require(std::memcmp(&state.stencilBack, &front, sizeof(front)) == 0, "back faces without BACKFACE_ENABLE must use the front state"); + // The content pass: EQUAL to 2, nothing written. + queue.context[0x10b] = 0; + queue.context[0x10c] = 0x01ffff02; + queue.context[0x200] = 0x00200211; + state = AgcDriver::Graphics::DecodeState(queue); + Require(state.stencilFront.compareOp == VK_COMPARE_OP_EQUAL && state.stencilFront.reference == 2 && state.stencilFront.passOp == VK_STENCIL_OP_KEEP, "stencil content pass changed"); + // Clearing to 0 with REPLACE_TEST under ALWAYS. + queue.context[0x10b] = 0x00030030; + queue.context[0x10c] = 0x01ffff00; + queue.context[0x200] = 0x00700771; + state = AgcDriver::Graphics::DecodeState(queue); + Require(state.stencilFront.passOp == VK_STENCIL_OP_REPLACE && state.stencilFront.reference == 0 && std::memcmp(&state.stencilBack, &state.stencilFront, sizeof(state.stencilFront)) == 0, "stencil clear pass changed"); + // BACKFACE_ENABLE: back faces get STENCILFUNC_BF, the _BF operations and DB_STENCILREFMASK_BF. + queue.context[0x200] = 0x007007f1; + state = AgcDriver::Graphics::DecodeState(queue); + Require(state.stencilBack.compareOp == VK_COMPARE_OP_ALWAYS && state.stencilBack.passOp == VK_STENCIL_OP_KEEP && state.stencilBack.writeMask == 0 && state.stencilBack.reference == 1, "back-face stencil state changed"); + // REPLACE_OP writing a value the test compares differently has no single Vulkan reference. + queue.context[0x10b] = 0x40; + queue.context[0x10c] = 0x05ffff02; + queue.context[0x200] = 0x00200211; + expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "stencil replacement"); + // Adding anything but 1 has no Vulkan operation. + queue.context[0x10b] = 0x50; + queue.context[0x200] = 0x00700711; + expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "stencil add/subtract"); + queue.context[0x10b] = 0; + queue.context[0x000] = 1; + expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "DB_RENDER_CONTROL"); + // CB_COLOR_VIEW's mip level is bits 26-29. + queue = makeState(); + queue.context[0x31b] = 1u << 26u; + expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "mip exceeds"); + queue.context[0x31b] = 1u << 13u; + expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "array views"); +} + void DepthClipTests() { auto queue = makeState(); const auto direct = AgcDriver::Graphics::DecodeState(queue); @@ -261,7 +334,8 @@ void DepthClipTests() { expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "inverted viewport depth clamp"); queue.context[0xb4] = 0; for (std::uint32_t bit = 0; bit < 32; ++bit) { - if (bit == 19) continue; + // DX_CLIP_SPACE_DEF (19) and ZCLIP_NEAR/FAR_DISABLE (26, 27, depth clamping) are decoded. + if (bit == 19 || bit == 26 || bit == 27) continue; queue.context[0x204] = 1u << bit; expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "PA_CL_CLIP_CNTL"); } @@ -1168,6 +1242,7 @@ int main() { stateTests(); hardwareScreenOffsetTests(); DepthClipTests(); + DepthStencilTests(); DisabledColorTests(); ShaderStageTests(); InitialContextTests(); diff --git a/core/libs/prx/libSceAgcDriver/tests/Pm4.cpp b/core/libs/prx/libSceAgcDriver/tests/Pm4.cpp index 34198c47b..5723d6ec8 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Pm4.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Pm4.cpp @@ -342,6 +342,18 @@ void testMemorySynchronization() { check(memory.destination == 42, "failed synchronization changed the destination"); } +void testWriteChangedKeepsUntouchedBytes() { + // Write-back stores only the bytes the GPU changed: its other bytes are the snapshot, and the + // CPU may have written them since. + alignas(256) static std::uint8_t guest[256]; + std::memset(guest, 0, sizeof(guest)); + std::vector original(sizeof(guest)), current(sizeof(guest)); + current[3] = std::byte{7}; + guest[100] = 0x55; + AgcDriver::GuestMemory::WriteChanged(reinterpret_cast(guest), current, original); + check(guest[3] == 7 && guest[100] == 0x55, "write-back rolled back a byte the GPU did not change"); +} + void testEventWrite() { for (const auto eventType : {0x07u, 0x0fu, 0x10u}) { AgcDriver::Pm4::Validate(makePacket(0x46, {0x400u | eventType}), 0); @@ -481,6 +493,7 @@ int main(int argc, char** argv) { return 0; } testCatalog(); + testWriteChangedKeepsUntouchedBytes(); testRegisters(); testContextAndBases(); testIndexedDraw(); diff --git a/core/libs/prx/libSceVideoOut/src/VideoOutDriver.cpp b/core/libs/prx/libSceVideoOut/src/VideoOutDriver.cpp index 26d2bdb67..45104221d 100644 --- a/core/libs/prx/libSceVideoOut/src/VideoOutDriver.cpp +++ b/core/libs/prx/libSceVideoOut/src/VideoOutDriver.cpp @@ -59,6 +59,16 @@ public: return std::make_shared(cfg, index, cfg->bufferReuse[index].Capture()); } + void WaitForFlipRoom() override { + std::unique_lock queueLock(queue->mutex); + const bool room = queue->changed.wait_for(queueLock, std::chrono::seconds(60), [&] { + return queue->failure || queue->stopping || cfg->shutdownToken.stop_requested() || queue->reservations.load() < VIDEO_OUT_FLIP_QUEUE_CAPACITY; + }); + if (queue->failure) std::rethrow_exception(queue->failure); + if (queue->stopping || cfg->shutdownToken.stop_requested()) throw ProcessShutdown{}; + require(room, "flip queue stayed full for 60 s: the presenter is not completing flips"); + } + std::shared_ptr Reserve(const AgcDriver::FlipInfo& info) override { // All flip modes are presented at the next vsync. require(info.mode >= VIDEO_OUT_FLIP_MODE_VSYNC && info.mode <= 6, "unsupported flip mode"); @@ -70,23 +80,13 @@ public: request->outputHandle = info.handle; request->flipMode = static_cast(info.mode); request->flipArg = info.argument; - // Debug aid: APS5_FLIP_QUEUE_WAIT=1 waits for room instead of failing the flip, so a - // presenter stall keeps the process alive for a debugger (reported every 5 s). - static const bool waitWhenFull = std::getenv("APS5_FLIP_QUEUE_WAIT") != nullptr; - if (waitWhenFull) { - int waited = 0; - while (queue->reservations.load() >= VIDEO_OUT_FLIP_QUEUE_CAPACITY) { - if (cfg->shutdownToken.stop_requested()) throw ProcessShutdown{}; - std::this_thread::sleep_for(std::chrono::milliseconds(10)); - if (++waited % 500 == 0) std::fprintf(stderr, "[flip] queue full for %d s; waiting (APS5_FLIP_QUEUE_WAIT)\n", waited / 100); - } - } std::lock_guard queueLock(queue->mutex); if (queue->failure) std::rethrow_exception(queue->failure); if (queue->stopping || cfg->shutdownToken.stop_requested()) throw ProcessShutdown{}; std::lock_guard lock(cfg->mutex); checkConfig(*cfg); - require(queue->reservations.load() < VIDEO_OUT_FLIP_QUEUE_CAPACITY, "flip queue full"); + // The limit was waited for by WaitForFlipRoom before the driver took its lock; concurrent + // submitters may overshoot it by their own flips. if (info.index >= 0) { request->buffer = cfg->buffers[info.index]; require(request->buffer.Occupied(), "flip buffer is not registered"); @@ -479,6 +479,7 @@ void VideoOutDriver::presentLoop(std::stop_token token) { flipQueue->requests.pop_front(); } } + SDL_Event event; while (SDL_PollEvent(&event)) { if (event.type == SDL_QUIT) { @@ -504,7 +505,11 @@ void VideoOutDriver::presentLoop(std::stop_token token) { } current->timing->Print(current->outputHandle, current->index, current->flipArg, finished, interval); } - current.reset(); + if (current) { + current.reset(); + // processFlip released the flip's reservation: a submitter may be waiting for room. + flipQueue->changed.notify_all(); + } } } catch (const ProcessShutdown&) { } catch (const std::exception& error) {