perf(agc): batch GPU work in a recorder with host-imported guest memory

Replaces the synchronous per-command execution model, and with it the
upstream render-target, pipeline-cache, tile-buffer, batched-draw and
memory-ownership commits (5a280ce, 7ed5a0b, 46c799d, 598caac, 40df528),
whose files are removed.

- Recorder: one open command buffer per device; dispatches, resident
  draws, uploads, write-backs, labels, fills and copies record into it;
  timeline-semaphore serials let drains wait without the GPU mutex;
  completions run in order at reap.
- one worker thread per PM4 queue; labels and stores written by the GPU
  into the host import; GPU-side DISPATCH_INDIRECT; the buffer-fill
  kernel replaced by vkCmdFillBuffer; a dispatch cache validated by
  write watch; filler packets tolerated; rejected submissions dumped.
- guest memory: page-state cache, 64 KiB write generations, host imports
  with VK_EXT_external_memory_host, persistent mirrors of the executable
  image, GPU copies of misaligned sub-ranges.
- textures: shader-written images stay resident with deferred
  write-back (adjacent-generation rule for packed video planes), sampled
  views of storage images, DCC key handling, addrlib swizzle equations,
  bidirectional detiler.
- draws: MRT, more formats, dynamic viewport/scissor, cached pipelines,
  resident 64 KiB targets, draws recorded into the recorder.
- ShaderResources: two-stage build, descriptor and sampler caches, a
  content-keyed resource cache; presentation from the resident image.
- a label deferred behind a completion is no longer re-stored by the
  CPU at reap time unless a write-back overwrote it.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0177gthrWEBBfBu5xJDXgBfo
This commit is contained in:
devilismyfriend 2026-09-25 16:27:49 -07:00
parent 87911b3bb5
commit 8a69fefe13
68 changed files with 14987 additions and 1461 deletions

View File

@ -47,14 +47,15 @@ add_custom_command(
)
add_library(libSceAgcDriver SHARED EXCLUDE_FROM_ALL
${CMAKE_CURRENT_SOURCE_DIR}/Unimplemented.cpp
${CMAKE_CURRENT_SOURCE_DIR}/Execution/src/DisplayBuffer.cpp
${CMAKE_CURRENT_SOURCE_DIR}/Execution/src/Driver.cpp
${CMAKE_CURRENT_SOURCE_DIR}/Execution/src/WorkerSampler.cpp
${CMAKE_CURRENT_SOURCE_DIR}/Execution/src/GuestMemory.cpp
${CMAKE_CURRENT_SOURCE_DIR}/Execution/src/ShaderMemory.cpp
${CMAKE_CURRENT_SOURCE_DIR}/Execution/src/BdaFeatures.cpp
${CMAKE_CURRENT_SOURCE_DIR}/Execution/src/Pm4.cpp
${CMAKE_CURRENT_SOURCE_DIR}/Execution/src/VulkanDevice.cpp
${CMAKE_CURRENT_SOURCE_DIR}/Execution/src/SwapchainState.cpp
${CMAKE_CURRENT_SOURCE_DIR}/Execution/src/AspectFit.cpp
${CMAKE_CURRENT_SOURCE_DIR}/Execution/src/PresentationScaler.cpp
${CMAKE_CURRENT_SOURCE_DIR}/Graphics/src/Draw.cpp
@ -62,6 +63,7 @@ add_library(libSceAgcDriver SHARED EXCLUDE_FROM_ALL
${CMAKE_CURRENT_SOURCE_DIR}/Graphics/src/ColorTargetTransfer.cpp
${CMAKE_CURRENT_SOURCE_DIR}/Graphics/src/Pipeline.cpp
${CMAKE_CURRENT_SOURCE_DIR}/Graphics/src/Resources.cpp
${CMAKE_CURRENT_SOURCE_DIR}/Graphics/src/Recorder.cpp
${CMAKE_CURRENT_SOURCE_DIR}/Graphics/src/ShaderInputState.cpp
${CMAKE_CURRENT_SOURCE_DIR}/Graphics/src/ShaderResources.cpp
${CMAKE_CURRENT_SOURCE_DIR}/Graphics/src/ShaderAddressResources.cpp
@ -73,6 +75,7 @@ add_library(libSceAgcDriver SHARED EXCLUDE_FROM_ALL
${CMAKE_CURRENT_SOURCE_DIR}/Graphics/src/GuestTextureResource.cpp
${CMAKE_CURRENT_SOURCE_DIR}/Graphics/src/GuestSamplerResource.cpp
${CMAKE_CURRENT_SOURCE_DIR}/Graphics/src/TextureFormat.cpp
${CMAKE_CURRENT_SOURCE_DIR}/Graphics/src/DccMetadata.cpp
${CMAKE_CURRENT_SOURCE_DIR}/Graphics/src/TextureTiling.cpp
${CMAKE_CURRENT_SOURCE_DIR}/Graphics/src/TextureDetiler.cpp
${CMAKE_CURRENT_SOURCE_DIR}/Graphics/src/Texture.cpp
@ -208,6 +211,7 @@ add_executable(agc_driver_graphics_tests EXCLUDE_FROM_ALL
Graphics/src/GuestTextureResource.cpp
Graphics/src/GuestSamplerResource.cpp
Graphics/src/TextureFormat.cpp
Graphics/src/DccMetadata.cpp
Graphics/src/TextureTiling.cpp
Graphics/src/TextureDetiler.cpp
Graphics/src/Texture.cpp
@ -282,7 +286,6 @@ if(AGC_BUILD_VISUAL_TEST)
add_executable(agc_driver_visual_test EXCLUDE_FROM_ALL
tests/Visual.cpp
Execution/src/VulkanDevice.cpp
Execution/src/SwapchainState.cpp
Execution/src/AspectFit.cpp
Execution/src/PresentationScaler.cpp
Execution/src/GuestMemory.cpp
@ -301,6 +304,7 @@ if(AGC_BUILD_VISUAL_TEST)
Graphics/src/GuestTextureResource.cpp
Graphics/src/GuestSamplerResource.cpp
Graphics/src/TextureFormat.cpp
Graphics/src/DccMetadata.cpp
Graphics/src/TextureTiling.cpp
Graphics/src/TextureDetiler.cpp
Graphics/src/Texture.cpp
@ -363,3 +367,14 @@ if(AGC_BUILD_VISUAL_TEST)
endif()
include(PostCompile.cmake)
# Replays shader_*.req files the driver writes when a compute dispatch cannot be recompiled.
add_executable(agc_shader_replay EXCLUDE_FROM_ALL
tools/ShaderReplay.cpp
)
target_include_directories(agc_shader_replay PRIVATE ${LIBS_INCLUDE_DIR})
target_link_libraries(agc_shader_replay PRIVATE shader_recompiler_agc_driver libc)
if(ANYPS5_ENABLE_SPIRV_TOOLS)
target_link_libraries(agc_shader_replay PRIVATE SPIRV-Tools-static)
endif()
set_target_properties(agc_shader_replay PROPERTIES CXX_EXTENSIONS OFF)
configure_windows_unwind(agc_shader_replay)

View File

@ -1,16 +1,169 @@
#ifndef CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_GUESTMEMORY_HPP
#define CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_GUESTMEMORY_HPP
#include <atomic>
#include <cstddef>
#include <cstdint>
#include <mutex>
#include <span>
#include <utility>
#include <vector>
namespace AgcDriver::GuestMemory {
void CheckRange(const void* pointer, std::size_t bytes, std::size_t alignment, bool writable = false);
void CheckGpuRange(const void* pointer, std::size_t bytes, std::size_t alignment, bool writable = false);
// CheckRange without throwing: whether the whole range is mapped with the requested access.
bool Accessible(const void* pointer, std::size_t bytes, bool writable = false);
// The accessible parts [begin, end) of a range, in address order. GPU heaps are often bound whole while
// the guest commits their pages on demand, so a binding can cover reserved but uncommitted pages.
std::vector<std::pair<std::uint64_t, std::uint64_t>> CommittedRanges(std::uint64_t address, std::size_t bytes, bool writable = false);
// Both facts from one walk of the page tables: the accessible parts of a range and whether they cover
// all of it (`whole`, what Accessible answers). Reserved pages are queried each time, so asking twice
// costs two VirtualQuery per run.
struct Commitment {
std::vector<std::pair<std::uint64_t, std::uint64_t>> ranges;
bool whole = false;
};
Commitment DescribeCommitted(std::uint64_t address, std::size_t bytes, bool writable = false);
void Read(std::uint64_t address, std::span<std::byte> destination, std::size_t alignment = 1);
// Variants for resources in heaps the guest commits on demand (partially resident textures, GPU
// scratch heaps): uncommitted pages read as zeros, are not compared, and are never stored.
void ReadCommitted(std::uint64_t address, std::span<std::byte> destination);
bool EqualsCommitted(std::uint64_t address, std::span<const std::byte> bytes);
void WriteChangedCommitted(std::uint64_t address, std::span<const std::byte> current, std::span<const std::byte> original);
void Write(std::uint64_t address, std::span<const std::byte> source, std::size_t alignment = 1);
// Stores the parts of `current` that differ from `original` (the guest bytes the GPU started from), so
// guest writes made meanwhile to untouched bytes survive. Compares in 256-byte blocks.
void WriteChanged(std::uint64_t address, std::span<const std::byte> current, std::span<const std::byte> original);
// Write tracking. The guest arena is reserved with page write watching, so CollectWrites learns which
// pages of a range the CPU wrote since they were last collected, stamps their 64 KiB blocks with a new
// generation and returns it; a snapshot of the range taken after that call is current while
// UnchangedSince(range, that generation) holds. Every validation collects its own range first, so
// resources sharing pages see each other's writes. GPU writes into host-imported memory bypass the
// page tables and are reported with MarkWritten. Without write watching CollectWrites returns 0 and
// UnchangedSince is always false, so callers fall back to comparing bytes.
std::uint64_t CollectWrites(std::uint64_t address, std::size_t bytes);
bool UnchangedSince(std::uint64_t address, std::size_t bytes, std::uint64_t generation);
void MarkWritten(std::uint64_t address, std::size_t bytes);
// Collect epoch: within one epoch a range already collected is not walked again, CollectWrites
// returns the current generation instead. A guest write landing between two collects of the same
// epoch is seen by the next epoch, which is the ordering real hardware gives a CPU write made while a
// submission runs. The driver's own writes (Write, WriteChanged, MarkWritten) stamp their blocks, so
// they are never hidden. APS5_NO_COLLECT_MEMO=1 disables the memo. CollectWritesUncached always walks:
// for decisions about which blocks the CPU wrote while a GPU result was pending (a stale answer there
// stores old texels over reused memory).
// The epoch is per queue worker: BumpCollectEpoch advances the calling thread's epoch to a value no
// other thread ever holds (one worker's walk never satisfies another's check) at the hardware
// ordering points of its queue: the start of a submission, a WAIT_REG_MEM the CPU may have satisfied
// (at entry, polled, timed out, or by another queue's recorded label; a label this queue recorded
// itself orders no CPU write), a device drain, and before reaping completions outside a submission.
// A thread that never bumps (the presenter, game threads inside the flush hook) gets a fresh epoch
// for every collect: nothing is reused without an ordering point. APS5_PACKET_EPOCH=1 makes the
// workers bump before every packet as before. CollectEpochBumps counts the bumps ([guestmem] line).
void BumpCollectEpoch();
std::uint64_t CollectEpochBumps();
std::uint64_t CollectWritesUncached(std::uint64_t address, std::size_t bytes);
// Serializes device work: draws, dispatches, presentation and the deferred write-backs below. The
// mutex is recursive; it is wrapped so every acquisition (std::lock_guard at any site) measures how
// long it waited: the graphics worker's frame is mostly spent waiting for this lock behind the
// compute workers, and that wait has to stay visible. Under APS5_PROFILE_DRAW each thread reports its
// waits in a [lock] line every 10 s (APS5_NO_LOCK_PROFILE=1 turns the timing off); a queue worker tags
// its thread with TagGpuLockThread so the line names the queue.
class GpuMutexType {
public:
void lock();
bool try_lock();
void unlock();
// Whether the calling thread holds the mutex (at any depth): the APS5_ASSERT_GPU_LOCK checks
// (AssertGpuLockHeld) and code that must know whether it runs inside the device lock.
bool HeldByThisThread() const;
// The calling thread's recursion depth (0 when it does not hold the mutex): 1 means its
// current acquisition is the outermost, so code that releases the mutex around a wait
// (Recorder::SyncThrough for the flush hook) gives up no caller's hold.
std::uint32_t DepthOnThisThread() const;
private:
void acquired();
std::recursive_mutex mutex;
// The holder's thread token and recursion depth, written by the holder only; another thread
// reads the owner just to see that it is not itself.
std::atomic<const void*> owner{nullptr};
std::uint32_t depth = 0;
};
GpuMutexType& GpuMutex();
// Called on the thread that just gave up its outermost hold, right after the mutex was released:
// work the hold deferred to run without it (the recorder's release of the objects completed batches
// kept). One hook, set once; it must not take GpuMutex itself.
void SetGpuUnlockHook(void (*hook)());
// Debug aid (APS5_ASSERT_GPU_LOCK=1): aborts with a [lock] line naming `where` when the calling
// thread does not hold GpuMutex. Recording, submits, command batches and the detiler's pools must
// only be reached under it (a resource build's stage A runs without it, see ShaderResources).
void AssertGpuLockHeld(const char* where);
void TagGpuLockThread(std::uint32_t queue);
// The queue tag of the calling thread (0xffffffff when untagged), for reports that name the thread.
std::uint32_t GpuLockThreadTag();
// Lock sites (APS5_PROFILE_DRAW): a caller names the site of its next lock() so the [lock] line
// splits acquisitions and waits per site (whose holds queue 0 waits behind is then measurable). The
// tag is consumed by that acquisition, so a nested lock inside the flush hook is attributed to the
// hook, and an acquisition nobody tagged counts as "other". Try-only acquisitions are not counted
// as acquisitions; the hold they start is timed under "try". Holds are timed per site as well (the
// outermost acquisition of a thread until its unlock; APS5_NO_HOLD_PROFILE=1 turns that off), so
// the line shows who holds the mutex, not only who waits for it.
enum class GpuLockSite : std::uint8_t { Other = 0, Dispatch, Indirect, Draw, Hook, Wait, Label, Flush, Present, Fill, Try, Count };
void TagGpuLockSite(GpuLockSite site);
// A code address (a return address) as an offset into its module, in the form of the [guestmem]
// callers (symbolized with nm against the driver's image). Takes the loader lock: for reports only.
unsigned long long CodeOffset(const void* address);
// Deferred GPU writes. The graphics layer keeps the results of storage images on the GPU until guest
// memory is read; it registers a hook that stores every pending result overlapping a range, which
// every read and store through this interface calls first. Code that reads guest memory directly
// calls FlushGpuWrites itself.
void SetFlushHook(void (*hook)(std::uint64_t address, std::size_t bytes));
void FlushGpuWrites(std::uint64_t address, std::size_t bytes);
// Attribution of guest memory accesses (the [hooksync] line in Recorder.cpp and the read-site
// counts of the [guestmem] line, APS5_PROFILE_DRAW): a sync the flush hook makes for an access has
// to be charged to the PM4 packet and the driver code that made the access. A queue worker names
// the packet it executes at the top of its packet loop (SetCurrentPacket; opcode 0xffff is the
// flip); a thread that runs no packets (the presenter, game threads) reads back NoPacket. The main
// read paths name themselves with a ReadSiteScope; an access made by none of them is attributed by
// its return addresses instead (CaptureCallerOffsets).
struct PacketTag {
std::uint32_t opcode;
std::uint32_t queue;
};
constexpr std::uint32_t NoPacket = 0xfffffffeu;
void SetCurrentPacket(std::uint32_t opcode, std::uint32_t queue);
PacketTag CurrentPacket();
// Store: a CPU write (Write/WriteChanged) rather than a read; a hook sync made for one orders a
// write after a pending GPU write, so whether the bytes change says nothing about its necessity.
// MirrorRefresh: an address-based build bringing an image mirror up to date with guest memory
// (GuestBufferMemory.cpp: the flush before a writable mirror's compare, a new mirror's fill).
// Every enumerator needs its name in ReadSiteName's table (GuestMemory.cpp).
enum class ReadSite : std::uint8_t { Unknown = 0, Capture, DispatchCache, TextureCompare, TextureRead, BufferUpload, IndexBuffer, VertexBuffer, Registers, IndirectArguments, Wait, Label, Scanout, Store, MirrorRefresh, Count };
const char* ReadSiteName(ReadSite site);
// Sets the calling thread's read site and returns the previous one (nested scopes restore it).
ReadSite SetReadSite(ReadSite site);
ReadSite CurrentReadSite();
class ReadSiteScope {
public:
explicit ReadSiteScope(ReadSite site) : previous(SetReadSite(site)) {}
~ReadSiteScope() { SetReadSite(previous); }
ReadSiteScope(const ReadSiteScope&) = delete;
ReadSiteScope& operator=(const ReadSiteScope&) = delete;
private:
ReadSite previous;
};
// Return addresses above the caller as offsets into the driver's module (the same form as the
// [guestmem] callers, symbolized with nm against the driver's image; an address outside the module
// is written as 0), newest first, skipping `skip` frames above the caller; returns how many were
// written. Stack unwinding, so only for rare, profiled events.
std::size_t CaptureCallerOffsets(std::span<unsigned long long> frames, unsigned skip);
}

View File

@ -3,6 +3,9 @@
#include "prx/libSceAgcDriver/Execution/include/QueueState.hpp"
#include "prx/libSceAgcDriver/Execution/include/Pm4Opcodes.hpp"
#include <cstddef>
#include <vector>
#include <optional>
#include <array>
#include <span>
#include <string>
@ -21,11 +24,59 @@ struct DrawParameters {
};
std::string Name(std::uint32_t header);
// A PM4 type-2 packet is a one-dword filler (command-buffer padding); type 3 and type 0 carry a
// dword count in bits 29:16. Type 1 is undefined.
inline bool FillerPacket(std::uint32_t header) { return (header >> 30u) == 2u; }
inline std::size_t PacketWords(std::uint32_t header) { return FillerPacket(header) ? 1u : static_cast<std::size_t>((header >> 16u) & 0x3fffu) + 2u; }
std::string_view UnsupportedReason(std::uint32_t header);
void Validate(std::span<const std::uint32_t> packet, std::uint32_t queue);
void Execute(std::span<const std::uint32_t> packet, QueueState& queue);
bool AccessesMemory(std::uint32_t header);
// Whether an ACQUIRE_MEM packet asks only for GPU cache actions (no CPU-visible memory
// synchronization): such a packet needs a pipeline barrier, not a device drain.
bool UsesGpuCacheBarrier(std::span<const std::uint32_t> packet);
bool IsTagMarker(std::span<const std::uint32_t> packet);
bool WaitSatisfied(std::span<const std::uint32_t> packet);
// WaitSatisfied for a polling loop: the caller has validated the address once, so the value is read
// directly instead of through the checked guest memory path.
bool WaitSatisfiedUnchecked(std::span<const std::uint32_t> packet);
// Whether a WAIT_REG_MEM's compare holds for `value` (the 4 or 8 bytes a label the recorder still
// holds will store), without reading memory.
bool WaitComparesValue(std::span<const std::uint32_t> packet, std::uint64_t value);
// A label write (RELEASE_MEM with a data select, WRITE_DATA to memory): the destination and the
// bytes it stores, so the write can be recorded on the GPU behind the work it signals. Packets
// without a memory destination decode to nothing. No allocation per label (tens of thousands per
// second): WRITE_DATA's bytes are a view of the packet (valid while the packet is), a RELEASE_MEM
// value is held inline.
struct LabelWrite {
std::uint64_t address;
std::span<const std::byte> packetBytes;
std::array<std::byte, 8> inlineBytes{};
std::size_t inlineSize = 0;
std::span<const std::byte> Bytes() const { return inlineSize != 0 ? std::span<const std::byte>(inlineBytes).first(inlineSize) : packetBytes; }
};
std::optional<LabelWrite> DecodeLabelWrite(std::span<const std::uint32_t> packet);
// A memory store the CPU can resolve before the GPU runs it (COPY_DATA and DMA_DATA to memory,
// DUMP_CONST_RAM): the destination and the bytes it stores, so the driver can record the store on
// the GPU like a label instead of draining the device and storing on the CPU. Immediate and constant
// RAM bytes are known at once; a memory source is read here through the checked guest memory path
// (which waits only for recorded GPU work that writes the source). Nothing when the packet is not
// such a store, when it stores more than `limit` bytes, or when its destination or size is not a
// multiple of 4 (the GPU store needs both; the source is then not read: the caller keeps its CPU
// path). A store of zero bytes resolves with empty bytes.
struct StoreWrite {
std::uint64_t address;
std::span<const std::byte> viewBytes;
std::vector<std::byte> ownedBytes;
std::span<const std::byte> Bytes() const { return ownedBytes.empty() ? viewBytes : std::span<const std::byte>(ownedBytes); }
};
std::optional<StoreWrite> ResolveStore(std::span<const std::uint32_t> packet, const QueueState& queue, std::size_t limit);
// A DISPATCH_INDIRECT's arguments: the guest address of its three group-count dwords (no memory
// access, so the GPU can read them in place: VulkanDevice::DispatchIndirect), the DISPATCH_DIRECT
// packet made by reading them there (through the checked guest memory path, which waits for
// recorded GPU work that writes them), and both steps in one for the packet.
std::uint64_t DispatchArgumentAddress(std::span<const std::uint32_t> packet, const QueueState& queue);
std::array<std::uint32_t, 5> ReadDispatchArguments(std::uint64_t arguments, std::uint32_t initiator);
std::array<std::uint32_t, 5> ResolveDispatch(std::span<const std::uint32_t> packet, const QueueState& queue);
DrawParameters ResolveDraw(std::span<const std::uint32_t> packet, const QueueState& queue);

View File

@ -18,7 +18,18 @@ public:
void EnsureSourceImage(std::uint32_t width, std::uint32_t height);
void RecordUpload(VkCommandBuffer commands, VkBuffer uploadBuffer);
void RecordImage(VkCommandBuffer commands, VkImage image);
// Fills the source image from another image of the same extent (any blittable format, layout
// unchanged), leaving the source image ready to blit or read back.
void RecordBlitInto(VkCommandBuffer commands, VkImage image, VkImageLayout layout, VkFilter filter);
// Copies the source image's texels (tightly packed rows of its format) into a host-readable buffer
// and makes them visible to the host once the submission completes.
void RecordReadback(VkCommandBuffer commands, VkBuffer destination);
void RecordBlit(VkCommandBuffer commands, VkImage destinationImage, std::uint32_t destinationWidth, std::uint32_t destinationHeight);
// The aspect-fitting blit of RecordBlit from any image: presenting a resident render target needs
// no copy into the source image first.
static void RecordBlitFrom(const Graphics::Context& context, VkCommandBuffer commands, VkImage image, VkImageLayout layout, std::uint32_t width, std::uint32_t height, VkFilter filter, VkImage destinationImage, std::uint32_t destinationWidth, std::uint32_t destinationHeight);
std::uint32_t SourceWidth() const { return sourceWidth; }
std::uint32_t SourceHeight() const { return sourceHeight; }
private:
void release() noexcept;

View File

@ -2,19 +2,45 @@
#define CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_SHADERMEMORY_HPP
#include "Recompiler.hpp"
#include <array>
#include <bitset>
#include <cstdint>
#include <map>
#include <memory>
#include <span>
#include <vector>
namespace AgcDriver {
// Records the guest words a shader's resource analysis reads, as the memory regions its recompile
// request carries. Guest memory is fetched a 4 KiB page at a time; the regions report exactly the
// dwords read, so cache keys built from them do not change with unrelated bytes nearby.
class ShaderMemory {
public:
explicit ShaderMemory(std::span<const ShaderRecompiler::MemoryRegion> initial);
void Capture(const ShaderRecompiler::RecompileRequest& request);
// Returns what the capture resolved (plan, snapshot, specialization) for
// ShaderRecompiler::Recompile(request, capture), which then skips its own materialization. One
// result per call: the draw path captures several stages on one ShaderMemory.
std::shared_ptr<const ShaderRecompiler::ResourceCapture> Capture(const ShaderRecompiler::RecompileRequest& request);
[[nodiscard]] std::vector<ShaderRecompiler::MemoryRegion> Regions() const;
private:
static constexpr std::size_t PageBytes = 4096;
static constexpr std::size_t PageWords = PageBytes / sizeof(std::uint32_t);
struct Page {
std::array<std::uint32_t, PageWords> words{};
std::bitset<PageWords> valid;
std::bitset<PageWords> read;
};
static bool read(void* context, std::uint64_t address, std::uint32_t* value);
std::map<std::uint64_t, std::vector<std::byte>> regions;
Page& page(std::uint64_t base);
// Regions given at construction (the registered shader's code and header), referenced as given:
// the caller keeps them alive for as long as the capture is used.
std::map<std::uint64_t, std::span<const std::byte>> initial;
std::map<std::uint64_t, Page> pages;
};
}

View File

@ -8,9 +8,17 @@
#include "prx/libSceAgcDriver/Execution/include/DisplayBuffer.hpp"
#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp"
#include <memory>
#include <optional>
namespace AgcDriver {
namespace Graphics {
class StorageTexture;
}
// Stage A of a dispatch's resource build (see VulkanDevice::PrepareDispatch); opaque to callers.
struct PreparedDispatch;
class VulkanDevice {
public:
explicit VulkanDevice(const PresentationWindow* window = nullptr);
@ -18,23 +26,99 @@ public:
VulkanDevice(const VulkanDevice&) = delete;
VulkanDevice& operator=(const VulkanDevice&) = delete;
ShaderRecompiler::SpirvTarget Target() const;
// Drains everything under the caller's GpuMutex: recorder Sync plus vkDeviceWaitIdle. For suspend,
// resize, device replacement, CPU fill fallback and APS5_DRAIN_ALL; the packet-loop drains use the
// three-step form below so the GPU wait happens without the mutex.
void WaitIdle();
void WaitDraws();
void AcquireGpuMemory();
// Sends recorded work to the GPU without waiting for it. With `reapFirst` it first retires batches
// that already finished, so the in-flight list stays short (APS5_NO_OPPORTUNISTIC_REAP=1 skips
// that). A reap runs completion actions, and a write-back can wait for a later batch under the
// mutex through the flush hook, so a thread submitting on another queue's behalf (a WAIT_REG_MEM
// poller, a compute worker's between-packet flush) passes false: it must only submit, never wait.
void SubmitRecorded(bool reapFirst = true);
// Unlocked drain: SubmitAndEpoch (under the mutex) returns the serial covering every recorded
// batch (0: nothing in flight); WaitRecorded(serial) waits for it WITHOUT the mutex (the caller
// holds a shared_ptr to this device); ReapRecorded(serial) (under the mutex) runs the completions
// of the batches up to it. CanWaitUnlocked is false without timeline semaphores: use WaitIdle.
bool CanWaitUnlocked() const;
std::uint64_t SubmitAndEpoch();
void WaitRecorded(std::uint64_t serial);
void ReapRecorded(std::uint64_t serial);
// Completes the batches that already finished (non-blocking); under the mutex.
void ReapRecorded();
// Records a store of `bytes` at a guest address behind the recorded work, so the value appears
// once that work completed; the batch is submitted by the queue worker's flush rules (or at once
// with APS5_LABEL_SUBMIT_NOW=1). `stamp` is the record-order stamp and `queue` the recording
// queue for the recorder's pending-label table. Returns 0 when recorded on the GPU, 5 when kept
// as a completion action behind pending write-backs and 6 when kept as one because the memory is
// not host-imported so the GPU cannot store it (both land when their batch is reaped), else why
// the CPU must write it:
// 1 nothing recorded (the write is already ordered), 2 write-backs pending and 3 memory not
// imported (both only with APS5_DRAIN_COMPLETION_LABELS=1), 4 unsuitable size or alignment.
// `reapFirst` retires finished batches before the checks; a caller recording a group of labels
// under one lock passes it for the first label only.
int WriteLabelOnGpu(std::uint64_t address, std::span<const std::byte> bytes, std::uint64_t stamp, std::uint32_t queue, bool reapFirst = true);
// Pending-label table lookup and open-batch overlap test for WAIT_REG_MEM (see Recorder).
std::optional<std::uint64_t> PendingLabel(std::uint64_t address, std::size_t bytes, std::uint64_t afterStamp, std::uint32_t& queue) const;
bool OpenWriteOverlaps(std::uint64_t address, std::size_t bytes) const;
// Fills [address, address + bytes) of host-imported guest memory with a repeating 16-byte pattern,
// recorded behind the open batch; false when the range is not imported (the caller stores it).
// Recorded GPU stores over the range are ordered before the fill by its barrier; finished
// batches are retired first, and it waits only for stores a batch's completion makes on the
// CPU (a copied buffer's write-back, a deferred label). Debug aid: APS5_FILL_SYNC=1 waits for
// every recorded store over the range.
bool FillBuffer(std::uint64_t address, std::size_t bytes, std::span<const std::uint32_t, 4> pattern);
void ResolveMemory(std::uint64_t address, std::size_t bytes, bool writable);
void* Window() const;
void Resize(std::uint32_t width, std::uint32_t height);
bool Presentable() const;
void PresentClear(std::uint32_t width, std::uint32_t height, bool opaque);
// A presentation is four steps so the presenter holds GuestMemory::GpuMutex only while it touches
// the queue: AcquireImage takes the next swapchain image (no mutex needed: the swapchain and its
// fences are the presenter's own, and in FIFO mode this is where a frame waits for the vblank);
// PresentClear/PresentDisplayBuffer record and submit the frame (under the mutex) and return
// whether one is in flight; FinishPresent waits for its render fence (no mutex); QueuePresent
// hands the image to the swapchain (under the mutex again). AcquireImage returns false when the
// swapchain is out of date (the frame is dropped; the next Resize recreates it); a present without
// a prior AcquireImage acquires itself. PresentPixels does all steps itself.
bool AcquireImage();
bool PresentClear(std::uint32_t width, std::uint32_t height, bool opaque);
void PresentPixels(std::uint32_t width, std::uint32_t height, std::span<const std::byte> pixels);
void PresentDisplayBuffer(const DisplayBuffer& buffer);
void Dispatch(const ShaderRecompiler::RecompileResult& shader, std::uint32_t x, std::uint32_t y, std::uint32_t z, std::span<const Graphics::GuestMemorySnapshot> snapshots = {});
bool PresentDisplayBuffer(const DisplayBuffer& buffer);
void FinishPresent();
void QueuePresent();
// Stage A of a dispatch's resource build, run WITHOUT GuestMemory::GpuMutex before Dispatch or
// DispatchIndirect: the binding plan, data buffers, imports already serving the guest buffers,
// read-only copies and the descriptor set (see ShaderResources). The dispatch completes it
// under the mutex (stage B: texture lookups, the rest of the upload, descriptor writes) and
// records. Null when nothing is prepared: the resource cache may serve the dispatch (its
// Revalidate stays under the mutex), or APS5_LOCKED_BUILD=1 keeps the whole build under it as
// before. `shader` and `snapshots` must outlive the dispatch.
std::shared_ptr<PreparedDispatch> PrepareDispatch(const ShaderRecompiler::RecompileResult& shader, std::span<const Graphics::GuestMemorySnapshot> snapshots);
void Dispatch(const ShaderRecompiler::RecompileResult& shader, std::uint32_t x, std::uint32_t y, std::uint32_t z, std::span<const Graphics::GuestMemorySnapshot> snapshots = {}, std::uint64_t programAddress = 0, std::shared_ptr<PreparedDispatch> prepared = nullptr);
// A dispatch whose group counts are the three dwords at `arguments` in guest memory
// (DISPATCH_INDIRECT): the GPU reads them in place from the host import, ordered after everything
// recorded before, so the CPU never waits for the shader that wrote them. When the GPU could not
// see the current bytes the counts are read on the CPU instead (through the flush hook, as the
// driver resolved every indirect dispatch before) and the dispatch is recorded as a direct one:
// cpuReason 1 storage-image results were pending over them, 2 a recorded dispatch writes them
// through a copied buffer (its CPU write-back lands only when the batch is reaped) or a label
// over those dwords is pending, 3 the memory is not host-imported; 0 when recorded GPU-side. argumentReadMs is
// what that CPU read (its sync) took. The device limit on group counts is not checked GPU-side.
// Debug aid: APS5_NO_GPU_INDIRECT=1 (in the driver) keeps every indirect dispatch on the CPU path.
struct IndirectOutcome {
int cpuReason;
double argumentReadMs;
};
IndirectOutcome DispatchIndirect(const ShaderRecompiler::RecompileResult& shader, std::uint64_t arguments, std::span<const Graphics::GuestMemorySnapshot> snapshots = {}, std::uint64_t programAddress = 0, std::shared_ptr<PreparedDispatch> prepared = nullptr);
void Draw(const Graphics::State& graphics, const Pm4::DrawParameters& draw, std::span<const Graphics::CompiledShader> shaders, std::span<const Graphics::GuestMemorySnapshot> snapshots = {});
void EnqueueDraw(const Graphics::State& graphics, const Pm4::DrawParameters& draw, std::span<const Graphics::CompiledShader> shaders, std::span<const Graphics::GuestMemorySnapshot> snapshots = {});
private:
Graphics::Context graphicsContext() const;
void present(std::uint32_t width, std::uint32_t height, bool opaque, std::span<const std::byte> pixels, const DisplayBuffer* display = nullptr);
// Body of Dispatch and DispatchIndirect: `arguments` 0 dispatches x, y, z groups.
IndirectOutcome dispatch(const ShaderRecompiler::RecompileResult& shader, std::uint32_t x, std::uint32_t y, std::uint32_t z, std::uint64_t arguments, std::span<const Graphics::GuestMemorySnapshot> snapshots, std::uint64_t programAddress, std::shared_ptr<PreparedDispatch> prepared);
bool present(std::uint32_t width, std::uint32_t height, bool opaque, std::span<const std::byte> pixels, const DisplayBuffer* display = nullptr, const std::shared_ptr<Graphics::StorageTexture>& resident = nullptr, VkFilter residentFilter = VK_FILTER_LINEAR, bool dumpFrame = false);
void writeFrameDump();
struct State;
std::unique_ptr<State> state;
};

View File

@ -0,0 +1,11 @@
#ifndef CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_WORKERSAMPLER_HPP
#define CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_WORKERSAMPLER_HPP
namespace AgcDriver {
// Starts sampling the calling thread when APS5_SAMPLE_WORKER names an output file.
void StartWorkerSampler();
}
#endif

View File

@ -1,6 +1,9 @@
#include "prx/libSceAgcDriver/Execution/include/DisplayBuffer.hpp"
#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp"
#include "prx/libSceAgcDriver/Execution/include/PerformanceTimer.hpp"
#include <chrono>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <limits>
#include <stdexcept>
@ -12,6 +15,15 @@ void require(bool condition, const char* reason) {
if (!condition) throw std::runtime_error(reason);
}
// Bit 56 selects the 10-bit (A2B10G10R10) variant of the format.
constexpr std::uint64_t PixelFormatUnormBit = 0x0100000000000000ull;
constexpr std::uint64_t PixelFormatB8G8R8A8 = 0x8000000000000000ull;
constexpr std::uint64_t PixelFormatR8G8B8A8 = 0x8000000022000000ull;
std::uint64_t baseFormat(std::uint64_t pixelFormat) {
return pixelFormat & ~PixelFormatUnormBit;
}
std::uint32_t tileOffset(std::uint32_t x, std::uint32_t y) {
return ((y << 4u) & 0x0070u) ^ ((y << 5u) & 0x0f00u) ^ ((y << 9u) & 0x1000u) ^ ((y << 8u) & 0x4000u) ^ ((x << 2u) & 0x000cu) ^ ((x << 5u) & 0x0380u) ^ ((x << 4u) & 0x0400u) ^ ((x << 6u) & 0x0800u) ^ ((x << 9u) & 0xa000u);
}
@ -20,7 +32,11 @@ std::uint32_t tileOffset(std::uint32_t x, std::uint32_t y) {
std::size_t DisplayBufferSize(const DisplayBuffer& buffer) {
require(buffer.width != 0 && buffer.height != 0 && buffer.width <= 16384 && buffer.height <= 16384, "VideoOut: invalid display buffer dimensions");
require(buffer.pixelFormat == 0x8000000000000000ull || buffer.pixelFormat == 0x8000000022000000ull, "VideoOut: unsupported display pixel format");
if (baseFormat(buffer.pixelFormat) != PixelFormatB8G8R8A8 && baseFormat(buffer.pixelFormat) != PixelFormatR8G8B8A8) {
char message[96];
std::snprintf(message, sizeof(message), "VideoOut: unsupported display pixel format 0x%016llx", static_cast<unsigned long long>(buffer.pixelFormat));
throw std::runtime_error(message);
}
require(buffer.address != 0 && (buffer.address & 65535u) == 0, "VideoOut: display buffer requires 64 KiB alignment");
const auto size = static_cast<std::uint64_t>((buffer.width + 127u) / 128u) * ((buffer.height + 127u) / 128u) * 65536u;
require(size <= std::numeric_limits<std::size_t>::max() && size <= std::numeric_limits<std::uintptr_t>::max() - buffer.address, "VideoOut: display buffer range overflow");
@ -32,12 +48,27 @@ std::vector<std::byte> DecodeDisplayBuffer(const DisplayBuffer& buffer, std::spa
require(source.size() == DisplayBufferSize(buffer), "VideoOut: invalid tiled display buffer size");
std::vector<std::byte> pixels(static_cast<std::size_t>(buffer.width) * buffer.height * 4);
const auto blocksPerRow = (buffer.width + 127u) / 128u;
const bool rgba = buffer.pixelFormat == 0x8000000022000000ull;
const bool rgba = baseFormat(buffer.pixelFormat) == PixelFormatR8G8B8A8;
// Bit 56 marks the 10-bit variant: A2B10G10R10 with red in the low bits (the scanout draws render
// COLOR_2_10_10_10 into it). Presented as 8-bit BGRA by dropping the low two bits of each channel.
const bool tenBit = (buffer.pixelFormat & PixelFormatUnormBit) != 0;
timing.Mark("validate_allocate");
for (std::uint32_t y = 0; y < buffer.height; ++y) {
for (std::uint32_t x = 0; x < buffer.width; ++x) {
const auto tiled = (static_cast<std::size_t>(y / 128u) * blocksPerRow + x / 128u) * 65536u + tileOffset(x, y);
const auto linear = (static_cast<std::size_t>(y) * buffer.width + x) * 4;
if (tenBit) {
std::uint32_t word = 0;
std::memcpy(&word, source.data() + tiled, 4);
const auto red = static_cast<std::byte>((word >> 2u) & 0xffu);
const auto green = static_cast<std::byte>((word >> 12u) & 0xffu);
const auto blue = static_cast<std::byte>((word >> 22u) & 0xffu);
pixels[linear] = rgba ? blue : red;
pixels[linear + 1] = green;
pixels[linear + 2] = rgba ? red : blue;
pixels[linear + 3] = std::byte{0xff};
continue;
}
pixels[linear] = source[tiled + (rgba ? 2 : 0)];
pixels[linear + 1] = source[tiled + 1];
pixels[linear + 2] = source[tiled + (rgba ? 0 : 2)];
@ -50,6 +81,12 @@ std::vector<std::byte> DecodeDisplayBuffer(const DisplayBuffer& buffer, std::spa
std::vector<std::byte> ReadDisplayBuffer(const DisplayBuffer& buffer) {
const auto size = DisplayBufferSize(buffer);
static const bool traceGpu = std::getenv("APS5_TRACE_GPU") != nullptr;
if (traceGpu) {
static const auto start = std::chrono::steady_clock::now();
std::fprintf(stderr, "[gpu] scanout display buffer 0x%llx %ux%u format 0x%016llx at %.1f s\n", static_cast<unsigned long long>(buffer.address), buffer.width, buffer.height, static_cast<unsigned long long>(buffer.pixelFormat), std::chrono::duration<double>(std::chrono::steady_clock::now() - start).count());
}
GuestMemory::FlushGpuWrites(buffer.address, size);
GuestMemory::CheckRange(reinterpret_cast<const void*>(buffer.address), size, 65536);
const auto* source = reinterpret_cast<const std::byte*>(buffer.address);
return DecodeDisplayBuffer(buffer, {source, size});

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -2,14 +2,23 @@
#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp"
#include "prx/libc/include/General.hpp"
#include <algorithm>
#include <thread>
#include <chrono>
#include <cstdio>
#include <cstring>
#include <limits>
#include <stdexcept>
#include <string>
namespace AgcDriver::Pm4 {
namespace {
std::string ToHex(std::uint32_t value) {
char buffer[16];
std::snprintf(buffer, sizeof(buffer), "%x", value);
return buffer;
}
void require(bool condition, const char* reason) {
if (!condition) throw std::runtime_error(reason);
}
@ -25,6 +34,13 @@ std::uint32_t registerOffset(std::uint32_t value) {
return offset;
}
// Debug aid: APS5_TRACE_CONTEXT_STATE logs context-state operations and the context registers
// written while a context is pushed. Read once: getenv scans the environment under a lock.
bool TraceContextState() {
static const bool value = std::getenv("APS5_TRACE_CONTEXT_STATE") != nullptr;
return value;
}
Registers& registersFor(QueueState& queue, std::uint32_t opcode) {
if (opcode == 0x69 || opcode == 0x9f) return queue.context;
if (opcode == 0x76 || opcode == 0x63) return queue.shader;
@ -32,7 +48,10 @@ Registers& registersFor(QueueState& queue, std::uint32_t opcode) {
}
void writeRegister(QueueState& queue, std::uint32_t opcode, std::uint32_t offset, std::uint32_t value) {
if ((opcode == 0x69 || opcode == 0x9f) && (offset == 0x8e || offset == 0x8f || offset == 0x318 || offset == 0x31b || offset == 0x31c || offset == 0x31d || offset == 0x390 || offset == 0x3b0 || offset == 0x3b8))
APS5_LOG_OUT_DEBUG("CONTEXT WRITE opcode=0x%x offset=0x%x value=0x%x", opcode, offset, value);
registersFor(queue, opcode).insert_or_assign(offset, value);
if (TraceContextState() && (opcode == 0x69 || opcode == 0x9f) && ((offset >= 0x318 && offset < 0x318 + 8 * 0xf && (offset - 0x318) % 0xf == 0) || offset == 0x8e)) std::fprintf(stderr, "[context] write %x = %08x (0x%x)\n", offset, value, opcode);
if ((opcode == 0x64 || opcode == 0x79 || opcode == 0x7a) && offset == 0x243) queue.indexType = value & 3u;
}
@ -48,9 +67,9 @@ std::uint32_t dmaDestination(std::span<const std::uint32_t> packet) {
return ((packet[1] >> 20u) & 3u) | ((packet[6] >> 25u) & 4u) | ((packet[6] >> 26u) & 8u);
}
void copyMemory(std::uint64_t source, std::uint64_t destination, std::size_t bytes, bool immediate) {
if (bytes == 0) return;
GuestMemory::CheckRange(reinterpret_cast<void*>(destination), bytes, 1, true);
// The bytes a COPY_DATA or DMA_DATA stores: an immediate is a repeating 32-bit pattern, a memory
// source is read through the checked path (it waits for recorded GPU work that writes it).
std::vector<std::byte> copySource(std::uint64_t source, std::size_t bytes, bool immediate) {
std::vector<std::byte> data(bytes);
if (immediate) {
const auto value = static_cast<std::uint32_t>(source);
@ -58,12 +77,19 @@ void copyMemory(std::uint64_t source, std::uint64_t destination, std::size_t byt
} else {
GuestMemory::Read(source, data);
}
GuestMemory::Write(destination, data);
return data;
}
void copyMemory(std::uint64_t source, std::uint64_t destination, std::size_t bytes, bool immediate) {
if (bytes == 0) return;
GuestMemory::CheckRange(reinterpret_cast<void*>(destination), bytes, 1, true);
GuestMemory::Write(destination, copySource(source, bytes, immediate));
}
}
std::string Name(std::uint32_t header) {
if (FillerPacket(header)) return "PM4_FILLER";
const auto opcode = (header >> 8u) & 0xffu;
if (opcode == 0x10 && (header & 0xfcu) != 0) {
switch ((header >> 2u) & 0x3fu) {
@ -107,11 +133,13 @@ std::string_view UnsupportedReason(std::uint32_t header) {
case 0x20: return "GPU query predication is not implemented";
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 0x39: case 0x3c: case 0x59: case 0x93:
case 0x3c: case 0x93: return {};
case 0x39: case 0x59:
return "cooperative command-queue waits are not implemented";
case 0x84: case 0x85: case 0x86: case 0x88:
return "separate CE/DE execution and counter synchronization are not implemented";
case 0x43: case 0x47: case 0x48: case 0x49:
case 0x49: return {};
case 0x43: case 0x47: case 0x48:
return "guest cache actions, GPU events and interrupt delivery are not implemented";
case 0x8e: return "GPU LOD statistics are not implemented; synthetic results are forbidden";
case 0x28: case 0x41: case 0x68: case 0x78:
@ -121,8 +149,13 @@ std::string_view UnsupportedReason(std::uint32_t header) {
}
void Validate(std::span<const std::uint32_t> packet, std::uint32_t queue) {
require(packet.size() >= 2, "truncated PM4 header or payload");
require(!packet.empty(), "truncated PM4 header");
const auto header = packet[0];
if (FillerPacket(header)) {
require(packet.size() == 1, "invalid PM4 filler size");
return;
}
require(packet.size() >= 2, "truncated PM4 header or payload");
require((header & 0xc0000000u) == 0xc0000000u, "unsupported PM4 packet type");
require(packet.size() == ((header >> 16u) & 0x3fffu) + 2u, "invalid PM4 packet size");
const auto opcode = (header >> 8u) & 0xffu;
@ -153,7 +186,13 @@ void Validate(std::span<const std::uint32_t> packet, std::uint32_t queue) {
}
return;
}
require((header & 0xffu) == 0 || (opcode == 0x11 && (header & 0xffu) == 2), "PM4 header flags are not implemented");
// Header bit 1 selects the compute shader type and bit 2 (register writes) resets the filter CAM;
// neither changes what the packet writes. Predication (bit 0) is not implemented.
const auto flags = header & 0xffu;
const bool registerWrite = opcode == 0x69 || opcode == 0x76 || opcode == 0x79 || opcode == 0x7a;
auto allowedFlags = opcode == 0x11 ? 2u : registerWrite ? 6u : opcode == 0x3c || opcode == 0x93 ? 2u : 0u;
if (IsTagMarker(packet)) allowedFlags |= 1u;
if ((flags & ~allowedFlags) != 0) throw std::runtime_error("PM4 header flags 0x" + ToHex(flags) + " are not implemented");
switch (opcode) {
case 0x11:
size(4);
@ -175,10 +214,10 @@ void Validate(std::span<const std::uint32_t> packet, std::uint32_t queue) {
require(packet[3] <= packet[1], "index count exceeds maximum index size");
require((packet[4] & ~0x20u) == 0, "unsupported indexed draw flags");
break;
case 0x15: size(5); require((packet[4] & ~0x8000u) == 0x41u, "dispatch modifiers are not implemented"); break;
case 0x15: size(5); if ((packet[4] & ~0x8020u) != 0x41u) throw std::runtime_error("dispatch modifiers 0x" + ToHex(packet[4]) + " are not implemented"); break;
case 0x16:
require(packet.size() == 3 || packet.size() == 4, "invalid indirect dispatch size");
require((packet.back() & ~0x8000u) == 0x41u, "indirect dispatch modifiers are not implemented");
require((packet.back() & ~0x8020u) == 0x41u, "indirect dispatch modifiers are not implemented");
break;
case 0x42: size(2); require(packet[1] == 0, "unsupported PFP_SYNC_ME payload"); break;
case 0x46: {
@ -222,6 +261,7 @@ void Validate(std::span<const std::uint32_t> packet, std::uint32_t queue) {
require((packet[1] & 3u) == 0 && packet[3] == 0x80000000u && packet[4] <= 0x3fffu, "unsupported indirect-register address or control fields");
break;
case 0x69: case 0x76: case 0x79: case 0x7a: {
if (IsTagMarker(packet)) break;
if (opcode != 0x76) graphics();
require(packet.size() >= 3, "register packet has no values");
if (opcode == 0x7a) require((packet[1] & 0xf0000000u) == 0 || (packet.size() == 3 && packet[1] == 0x20000243u), "indexed register bank selection is not implemented");
@ -229,6 +269,20 @@ void Validate(std::span<const std::uint32_t> packet, std::uint32_t queue) {
require(packet.size() - 2 <= 0x10000u - offset, "register range overflow");
break;
}
case 0x3c: case 0x93: {
size(opcode == 0x3c ? 7 : 9);
require((packet[1] & 0x10u) != 0, "register-space WAIT_REG_MEM is not implemented");
require((packet[1] & 7u) <= 6u, "invalid WAIT_REG_MEM compare function");
require((packet[2] & (opcode == 0x3c ? 3u : 7u)) == 0, "misaligned WAIT_REG_MEM address");
break;
}
case 0x49: {
size(8);
const auto dataSelect = packet[2] >> 29u;
require(dataSelect <= 3, "GDS RELEASE_MEM data is not implemented");
require(dataSelect == 0 || address(packet[3], packet[4]) == 0 || (packet[3] & (dataSelect == 1 ? 3u : 7u)) == 0, "misaligned RELEASE_MEM destination");
break;
}
case 0x81:
graphics();
require(packet[1] <= 0xbffcu && (packet[1] & 3u) == 0 && packet.size() - 2 <= 0x3000u - packet[1] / 4u, "constant RAM write range overflow or misalignment");
@ -266,6 +320,111 @@ void Validate(std::span<const std::uint32_t> packet, std::uint32_t queue) {
}
}
bool IsTagMarker(std::span<const std::uint32_t> packet) {
// libSceAgc brackets waits and draws with predicated SET_UCONFIG_REG writes of a tag value to
// 0x342; they carry no register state.
return packet.size() >= 3 && ((packet[0] >> 8u) & 0xffu) == 0x79u && (packet[0] & 1u) != 0 && packet[1] == 0x342u;
}
namespace {
bool waitCompares(std::span<const std::uint32_t> packet, bool wide, std::uint64_t value) {
const std::uint64_t reference = wide ? address(packet[4], packet[5]) : packet[4];
const std::uint64_t mask = wide ? address(packet[6], packet[7]) : packet[5];
const auto masked = value & mask;
switch (packet[1] & 7u) {
case 0: return true;
case 1: return masked < reference;
case 2: return masked <= reference;
case 3: return masked == reference;
case 4: return masked != reference;
case 5: return masked >= reference;
case 6: return masked > reference;
default: return false;
}
}
}
bool WaitSatisfied(std::span<const std::uint32_t> packet) {
const bool wide = ((packet[0] >> 8u) & 0xffu) == 0x93u;
const auto source = address(packet[2], packet[3]);
std::uint64_t value = 0;
// Named for the [hooksync] attribution (the read goes through the flush hook).
const GuestMemory::ReadSiteScope site(GuestMemory::ReadSite::Wait);
GuestMemory::Read(source, std::as_writable_bytes(std::span(&value, 1)).first(wide ? 8 : 4), wide ? 8 : 4);
return waitCompares(packet, wide, value);
}
bool WaitSatisfiedUnchecked(std::span<const std::uint32_t> packet) {
const bool wide = ((packet[0] >> 8u) & 0xffu) == 0x93u;
const auto source = address(packet[2], packet[3]);
const std::uint64_t value = wide ? *reinterpret_cast<const volatile std::uint64_t*>(source) : *reinterpret_cast<const volatile std::uint32_t*>(source);
return waitCompares(packet, wide, value);
}
bool WaitComparesValue(std::span<const std::uint32_t> packet, std::uint64_t value) {
const bool wide = ((packet[0] >> 8u) & 0xffu) == 0x93u;
return waitCompares(packet, wide, value);
}
std::optional<LabelWrite> DecodeLabelWrite(std::span<const std::uint32_t> packet) {
const auto opcode = (packet[0] >> 8u) & 0xffu;
if (opcode == 0x49 && packet.size() >= 7) {
const auto dataSelect = packet[2] >> 29u;
const auto destination = address(packet[3], packet[4]);
if (dataSelect == 0 || dataSelect > 3) return std::nullopt;
if (destination == 0) return std::nullopt;
std::uint64_t value = address(packet[5], packet[6]);
if (dataSelect == 3) value = static_cast<std::uint64_t>(std::chrono::duration_cast<std::chrono::nanoseconds>(std::chrono::steady_clock::now().time_since_epoch()).count() / 10);
LabelWrite label{destination, {}};
label.inlineSize = dataSelect == 1 ? 4 : 8;
std::memcpy(label.inlineBytes.data(), &value, label.inlineSize);
return label;
}
if (opcode == 0x37 && packet.size() > 4) {
const auto destination = address(packet[2], packet[3]);
if (destination == 0) return std::nullopt;
// WR_ONE_ADDR stores every value at the same address: the last one remains.
const auto words = (packet[1] & 0x10000u) != 0 ? packet.last(1) : packet.subspan(4);
return LabelWrite{destination, std::as_bytes(words)};
}
return std::nullopt;
}
std::optional<StoreWrite> ResolveStore(std::span<const std::uint32_t> packet, const QueueState& queue, std::size_t limit) {
// The same decode as Execute's cases below (a packet Validate accepted): a store that is not
// resolved here still runs there. The size and alignment tests precede any memory read: a store
// the GPU path would reject anyway must not read (and sync for) its source here and again there.
const auto fits = [limit](std::uint64_t destination, std::size_t bytes) { return bytes <= limit && destination % 4 == 0 && bytes % 4 == 0; };
switch ((packet[0] >> 8u) & 0xffu) {
case 0x40: {
if (packet.size() < 6) return std::nullopt;
const auto source = ((packet[1] & 0xfu) << 1u) | ((packet[1] >> 30u) & 1u);
const std::size_t bytes = (packet[1] & 0x10000u) != 0 ? 8 : 4;
const auto destination = address(packet[4], packet[5]);
if (!fits(destination, bytes)) return std::nullopt;
return StoreWrite{destination, {}, copySource(address(packet[2], packet[3]), bytes, source >= 10)};
}
case 0x50: {
if (packet.size() < 7) return std::nullopt;
const std::size_t bytes = packet[6] & 0x3ffffffu;
const auto destination = address(packet[4], packet[5]);
if (!fits(destination, bytes)) return std::nullopt;
return StoreWrite{destination, {}, copySource(address(packet[2], packet[3]), bytes, dmaSource(packet) == 2)};
}
case 0x83: {
if (packet.size() < 5 || packet[1] / 4 > queue.constantRam.size() || packet[2] > queue.constantRam.size() - packet[1] / 4) return std::nullopt;
const auto words = std::span<const std::uint32_t>(queue.constantRam).subspan(packet[1] / 4, packet[2]);
if (words.size_bytes() > limit) return std::nullopt;
// A view of the queue's constant RAM: valid until the next CONST_RAM write on this queue,
// which is a later packet of the same worker.
return StoreWrite{address(packet[3], packet[4]), std::as_bytes(words), {}};
}
default: return std::nullopt;
}
}
bool UsesGpuCacheBarrier(std::span<const std::uint32_t> packet) {
require(!packet.empty() && ((packet[0] >> 8u) & 0xffu) == 0x58, "cache barrier requires ACQUIRE_MEM");
Validate(packet, 0);
@ -274,24 +433,30 @@ bool UsesGpuCacheBarrier(std::span<const std::uint32_t> packet) {
bool AccessesMemory(std::uint32_t header) {
switch ((header >> 8u) & 0xffu) {
case 0x16: case 0x2d: case 0x35: case 0x37: case 0x40: case 0x50: case 0x63: case 0x64: case 0x83: case 0x9f: return true;
case 0x3c: case 0x93: case 0x49: case 0x16: case 0x2d: case 0x35: case 0x37: case 0x40: case 0x50: case 0x63: case 0x64: case 0x83: case 0x9f: return true;
default: return false;
}
}
std::array<std::uint32_t, 5> ResolveDispatch(std::span<const std::uint32_t> packet, const QueueState& queue) {
std::uint64_t source = 0;
if (packet.size() == 4) source = address(packet[1], packet[2]);
else {
require(queue.dispatchIndirectBase != 0, "indirect dispatch base has not been set");
require(packet[1] <= std::numeric_limits<std::uint64_t>::max() - queue.dispatchIndirectBase, "indirect dispatch address overflow");
source = queue.dispatchIndirectBase + packet[1];
}
std::array<std::uint32_t, 5> result{0xc0031500u, 0, 0, 0, packet.back()};
GuestMemory::Read(source, std::as_writable_bytes(std::span(result).subspan(1, 3)), 4);
std::uint64_t DispatchArgumentAddress(std::span<const std::uint32_t> packet, const QueueState& queue) {
if (packet.size() == 4) return address(packet[1], packet[2]);
require(queue.dispatchIndirectBase != 0, "indirect dispatch base has not been set");
require(packet[1] <= std::numeric_limits<std::uint64_t>::max() - queue.dispatchIndirectBase, "indirect dispatch address overflow");
return queue.dispatchIndirectBase + packet[1];
}
std::array<std::uint32_t, 5> ReadDispatchArguments(std::uint64_t arguments, std::uint32_t initiator) {
std::array<std::uint32_t, 5> result{0xc0031500u, 0, 0, 0, initiator};
// Named for the [hooksync] attribution (the read goes through the flush hook).
const GuestMemory::ReadSiteScope site(GuestMemory::ReadSite::IndirectArguments);
GuestMemory::Read(arguments, std::as_writable_bytes(std::span(result).subspan(1, 3)), 4);
return result;
}
std::array<std::uint32_t, 5> ResolveDispatch(std::span<const std::uint32_t> packet, const QueueState& queue) {
return ReadDispatchArguments(DispatchArgumentAddress(packet, queue), packet.back());
}
DrawParameters ResolveDraw(std::span<const std::uint32_t> packet, const QueueState& queue) {
Validate(packet, 0);
if (((packet[0] >> 8u) & 0xffu) == 0x2d) {
@ -311,6 +476,7 @@ DrawParameters ResolveDraw(std::span<const std::uint32_t> packet, const QueueSta
const auto bytes = static_cast<std::uint64_t>(packet[3]) * indexSize;
require(bytes <= std::numeric_limits<std::size_t>::max(), "index range size overflow");
GuestMemory::CheckRange(reinterpret_cast<const void*>(address), static_cast<std::size_t>(bytes), indexSize);
APS5_LOG_OUT_DEBUG("ResolveDraw context targetMask=0x%x shaderMask=0x%x indexCount=%u indexType=%u instances=%u", queue.context.contains(0x8e) ? queue.context.at(0x8e) : 0u, queue.context.contains(0x8f) ? queue.context.at(0x8f) : 0u, packet[3], queue.indexType, queue.instanceCount);
return {address, packet[3], indexSize, queue.instanceCount, packet[4]};
}
@ -327,6 +493,8 @@ void Execute(std::span<const std::uint32_t> packet, QueueState& queue) {
queue.markers.pop_back();
return;
case 0x1a:
if (TraceContextState()) std::fprintf(stderr, "[context] op %u: %zu registers, saved %d, cb0 %x info %x mask %x\n", packet[1], queue.context.size(), queue.savedContext.has_value() ? 1 : 0, queue.context.contains(0x318) ? queue.context.at(0x318) : 0u, queue.context.contains(0x31c) ? queue.context.at(0x31c) : 0u, queue.context.contains(0x8e) ? queue.context.at(0x8e) : 0u);
APS5_LOG_OUT_DEBUG("CONTEXT_STATE operation=%u targetMaskBefore=0x%x shaderMaskBefore=0x%x", packet[1], queue.context.contains(0x8e) ? queue.context.at(0x8e) : 0u, queue.context.contains(0x8f) ? queue.context.at(0x8f) : 0u);
switch (packet[1]) {
case 0: queue.ClearContext(); break;
case 1: case 3:
@ -340,6 +508,7 @@ void Execute(std::span<const std::uint32_t> packet, QueueState& queue) {
queue.savedContext.reset();
break;
}
APS5_LOG_OUT_DEBUG("CONTEXT_STATE done operation=%u targetMaskAfter=0x%x shaderMaskAfter=0x%x", packet[1], queue.context.contains(0x8e) ? queue.context.at(0x8e) : 0u, queue.context.contains(0x8f) ? queue.context.at(0x8f) : 0u);
return;
default: throw std::runtime_error("custom packet requires driver execution");
}
@ -347,6 +516,7 @@ void Execute(std::span<const std::uint32_t> packet, QueueState& queue) {
((packet[0] & 2u) == 0 ? queue.drawIndirectBase : queue.dispatchIndirectBase) = address(packet[2], packet[3]);
return;
case 0x12:
APS5_LOG_OUT_DEBUG("CLEAR_STATE targetMaskBefore=0x%x shaderMaskBefore=0x%x", queue.context.contains(0x8e) ? queue.context.at(0x8e) : 0u, queue.context.contains(0x8f) ? queue.context.at(0x8f) : 0u);
queue.ClearContext(); return;
case 0x13: queue.indexBufferSize = packet[1]; return;
case 0x26: queue.indexBase = address(packet[1], packet[2]); return;
@ -354,12 +524,47 @@ void Execute(std::span<const std::uint32_t> packet, QueueState& queue) {
case 0x2f: queue.instanceCount = packet[1]; return;
case 0x63: case 0x64: case 0x9f: {
std::vector<std::uint32_t> pairs(static_cast<std::size_t>(packet[4]) * 2);
// Named for the [hooksync] attribution (the read goes through the flush hook).
const GuestMemory::ReadSiteScope site(GuestMemory::ReadSite::Registers);
GuestMemory::Read(address(packet[1], packet[2]), std::as_writable_bytes(std::span(pairs)), 4);
for (std::size_t i = 0; i < pairs.size(); i += 2) registerOffset(pairs[i]);
for (std::size_t i = 0; i < pairs.size(); i += 2) writeRegister(queue, opcode, registerOffset(pairs[i]), pairs[i + 1]);
if (queue.savedContext.has_value() && TraceContextState()) {
std::fprintf(stderr, "[context] indirect 0x%x:", opcode);
for (std::size_t i = 0; i < pairs.size(); i += 2) std::fprintf(stderr, " %x", registerOffset(pairs[i]));
std::fprintf(stderr, "\n");
}
return;
}
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();
bool warned = false;
while (!WaitSatisfied(packet)) {
std::this_thread::sleep_for(std::chrono::microseconds(50));
if (!warned && std::chrono::steady_clock::now() - start > std::chrono::seconds(5)) {
warned = true;
std::fprintf(stderr, "[gpu] WAIT_REG_MEM at 0x%llx still waiting after 5s (function %u ref 0x%x mask 0x%x)\n",
static_cast<unsigned long long>(address(packet[2], packet[3])), packet[1] & 7u, packet[4], packet[5]);
}
}
return;
}
case 0x49: {
// Earlier work has drained by the time the driver reaches this packet, so the end-of-pipe
// write can happen immediately. Interrupt delivery is not modeled.
const auto dataSelect = packet[2] >> 29u;
const auto destination = address(packet[3], packet[4]);
if (dataSelect == 0 || destination == 0) return;
std::uint64_t value = address(packet[5], packet[6]);
if (dataSelect == 3) {
value = static_cast<std::uint64_t>(std::chrono::duration_cast<std::chrono::nanoseconds>(std::chrono::steady_clock::now().time_since_epoch()).count() / 10);
}
GuestMemory::Write(destination, std::as_bytes(std::span(&value, 1)).first(dataSelect == 1 ? 4 : 8), dataSelect == 1 ? 4 : 8);
return;
}
case 0x69: case 0x76: case 0x79: case 0x7a: {
if (IsTagMarker(packet)) return;
const auto offset = registerOffset(packet[1]);
for (std::size_t i = 2; i < packet.size(); ++i) writeRegister(queue, opcode, offset + static_cast<std::uint32_t>(i - 2), packet[i]);
return;

View File

@ -1,5 +1,6 @@
#include "prx/libSceAgcDriver/Execution/include/PresentationScaler.hpp"
#include "prx/libSceAgcDriver/Execution/include/AspectFit.hpp"
#include "prx/libSceAgcDriver/Graphics/include/Resources.hpp"
namespace AgcDriver {
@ -77,17 +78,56 @@ void PresentationScaler::RecordUpload(VkCommandBuffer commands, VkBuffer uploadB
pipelineBarrier(commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, nullptr, 0, nullptr, 1, &barrier);
}
void PresentationScaler::RecordBlitInto(VkCommandBuffer commands, VkImage image, VkImageLayout layout, VkFilter filter) {
Graphics::Require(sourceImage != VK_NULL_HANDLE && image != VK_NULL_HANDLE, "presentation image is unavailable");
const auto pipelineBarrier = context.Function<PFN_vkCmdPipelineBarrier>("vkCmdPipelineBarrier");
VkImageMemoryBarrier barrier{VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER};
barrier.dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
barrier.oldLayout = VK_IMAGE_LAYOUT_UNDEFINED;
barrier.newLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL;
barrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
barrier.image = sourceImage;
barrier.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1};
pipelineBarrier(commands, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, nullptr, 0, nullptr, 1, &barrier);
VkImageBlit blit{};
blit.srcSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1};
blit.srcOffsets[1] = {static_cast<std::int32_t>(sourceWidth), static_cast<std::int32_t>(sourceHeight), 1};
blit.dstSubresource = blit.srcSubresource;
blit.dstOffsets[1] = blit.srcOffsets[1];
context.Function<PFN_vkCmdBlitImage>("vkCmdBlitImage")(commands, image, layout, sourceImage, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, &blit, filter);
barrier.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
barrier.dstAccessMask = VK_ACCESS_TRANSFER_READ_BIT;
barrier.oldLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL;
barrier.newLayout = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL;
pipelineBarrier(commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, nullptr, 0, nullptr, 1, &barrier);
}
void PresentationScaler::RecordReadback(VkCommandBuffer commands, VkBuffer destination) {
Graphics::Require(sourceImage != VK_NULL_HANDLE && destination != VK_NULL_HANDLE, "presentation readback is unavailable");
VkBufferImageCopy copy{};
copy.imageSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1};
copy.imageExtent = {sourceWidth, sourceHeight, 1u};
context.Function<PFN_vkCmdCopyImageToBuffer>("vkCmdCopyImageToBuffer")(commands, sourceImage, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, destination, 1, &copy);
Graphics::RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_HOST_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_HOST_READ_BIT);
}
void PresentationScaler::RecordBlit(VkCommandBuffer commands, VkImage destinationImage, std::uint32_t destinationWidth, std::uint32_t destinationHeight) {
Graphics::Require(sourceImage != VK_NULL_HANDLE, "presentation source image has not been created");
const auto rect = ComputeContainRect_nid_postfix(sourceWidth, sourceHeight, destinationWidth, destinationHeight);
RecordBlitFrom(context, commands, sourceImage, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, sourceWidth, sourceHeight, VK_FILTER_LINEAR, destinationImage, destinationWidth, destinationHeight);
}
void PresentationScaler::RecordBlitFrom(const Graphics::Context& context, VkCommandBuffer commands, VkImage image, VkImageLayout layout, std::uint32_t width, std::uint32_t height, VkFilter filter, VkImage destinationImage, std::uint32_t destinationWidth, std::uint32_t destinationHeight) {
Graphics::Require(image != VK_NULL_HANDLE && width != 0 && height != 0, "presentation blit source is unavailable");
const auto rect = ComputeContainRect_nid_postfix(width, height, destinationWidth, destinationHeight);
VkImageBlit blit{};
blit.srcSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1};
blit.srcOffsets[0] = {0, 0, 0};
blit.srcOffsets[1] = {static_cast<std::int32_t>(sourceWidth), static_cast<std::int32_t>(sourceHeight), 1};
blit.srcOffsets[1] = {static_cast<std::int32_t>(width), static_cast<std::int32_t>(height), 1};
blit.dstSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1};
blit.dstOffsets[0] = {rect.x, rect.y, 0};
blit.dstOffsets[1] = {rect.x + static_cast<std::int32_t>(rect.width), rect.y + static_cast<std::int32_t>(rect.height), 1};
context.Function<PFN_vkCmdBlitImage>("vkCmdBlitImage")(commands, sourceImage, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, destinationImage, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, &blit, VK_FILTER_LINEAR);
context.Function<PFN_vkCmdBlitImage>("vkCmdBlitImage")(commands, image, layout, destinationImage, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, &blit, filter);
}
}

View File

@ -3,68 +3,128 @@
#include "Optimization/RequestMemoryView.hpp"
#include "Optimization/ResourceMaterializer.hpp"
#include "Optimization/ResourceProgram.hpp"
#include <atomic>
#include <chrono>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <iterator>
#include <limits>
#include <stdexcept>
#include <utility>
namespace AgcDriver {
namespace {
ShaderMemory::ShaderMemory(std::span<const ShaderRecompiler::MemoryRegion> initial) {
const ShaderRecompiler::RequestMemoryView validated(initial);
for (const auto& region : initial) {
regions.emplace(region.guestAddress, std::vector<std::byte>(region.bytes.begin(), region.bytes.end()));
struct CaptureProfile {
std::atomic<std::uint64_t> captures{0};
std::atomic<std::uint64_t> reads{0};
std::atomic<std::uint64_t> pages{0};
// The whole CaptureResources call: the plan lookup and the materialization are one call.
std::atomic<std::uint64_t> captureNanoseconds{0};
};
CaptureProfile& CaptureTotals() {
static CaptureProfile profile;
return profile;
}
}
ShaderMemory::ShaderMemory(std::span<const ShaderRecompiler::MemoryRegion> regions) {
const ShaderRecompiler::RequestMemoryView validated(regions);
for (const auto& region : regions) {
initial.emplace(region.guestAddress, region.bytes);
}
}
ShaderMemory::Page& ShaderMemory::page(std::uint64_t base) {
const auto found = pages.find(base);
if (found != pages.end()) return found->second;
auto& page = pages[base];
++CaptureTotals().pages;
// A page is either mapped whole or read word by word where the guest mapped less than a page.
if (GuestMemory::Accessible(reinterpret_cast<const void*>(base), PageBytes)) {
GuestMemory::Read(base, std::as_writable_bytes(std::span(page.words)), sizeof(std::uint32_t));
page.valid.set();
}
return page;
}
bool ShaderMemory::read(void* context, std::uint64_t address, std::uint32_t* value) {
auto& self = *static_cast<ShaderMemory*>(context);
if (address % sizeof(*value) != 0 || address > std::numeric_limits<std::uint64_t>::max() - sizeof(*value)) {
throw std::runtime_error("AGC driver: invalid shader memory read address");
}
const auto next = self.regions.upper_bound(address);
if (next != self.regions.begin()) {
const auto previous = std::prev(next);
const auto offset = address - previous->first;
if (offset < previous->second.size()) {
if (previous->second.size() - offset < sizeof(*value)) {
throw std::runtime_error("AGC driver: shader memory read crosses a snapshot boundary");
++CaptureTotals().reads;
if (!self.initial.empty()) {
const auto next = self.initial.upper_bound(address);
if (next != self.initial.begin()) {
const auto previous = std::prev(next);
const auto offset = address - previous->first;
if (offset < previous->second.size()) {
if (previous->second.size() - offset < sizeof(*value)) throw std::runtime_error("AGC driver: shader memory read crosses a snapshot boundary");
std::memcpy(value, previous->second.data() + offset, sizeof(*value));
return true;
}
std::memcpy(value, previous->second.data() + offset, sizeof(*value));
return true;
}
if (next != self.initial.end() && next->first - address < sizeof(*value)) throw std::runtime_error("AGC driver: shader memory read overlaps a snapshot boundary");
}
if (next != self.regions.end() && next->first - address < sizeof(*value)) {
throw std::runtime_error("AGC driver: shader memory read overlaps a snapshot boundary");
auto& page = self.page(address & ~static_cast<std::uint64_t>(PageBytes - 1));
const auto index = static_cast<std::size_t>((address % PageBytes) / sizeof(*value));
if (!page.valid.test(index)) {
std::uint32_t word = 0;
GuestMemory::Read(address, std::as_writable_bytes(std::span(&word, 1)), alignof(std::uint32_t));
page.words[index] = word;
page.valid.set(index);
}
std::vector<std::byte> bytes(sizeof(*value));
GuestMemory::Read(address, bytes, alignof(std::uint32_t));
std::memcpy(value, bytes.data(), sizeof(*value));
self.regions.emplace(address, std::move(bytes));
page.read.set(index);
*value = page.words[index];
return true;
}
void ShaderMemory::Capture(const ShaderRecompiler::RecompileRequest& request) {
const auto plan = ShaderRecompiler::GetResourcePlan(request);
constexpr ShaderRecompiler::ResourceMaterializer materializer;
std::shared_ptr<const ShaderRecompiler::ResourceCapture> ShaderMemory::Capture(const ShaderRecompiler::RecompileRequest& request) {
// The capture's word and page reads (through `read`) are attributed to it ([hooksync], [guestmem]).
const GuestMemory::ReadSiteScope site(GuestMemory::ReadSite::Capture);
const auto started = std::chrono::steady_clock::now();
ShaderRecompiler::SrtRuntime runtime;
runtime.userData = request.context.userData;
runtime.shaderBase = request.shader.codeAddress;
runtime.userContext = this;
runtime.readMemory = &read;
runtime.readSpecializationMemory = &read;
ShaderRecompiler::ResourceSnapshot snapshot;
ShaderRecompiler::ResourceSpecialization specialization;
materializer.Materialize(*plan, runtime, snapshot, specialization);
auto capture = ShaderRecompiler::CaptureResources(request, runtime);
static const bool profile = std::getenv("APS5_PROFILE_DRAW") != nullptr;
auto& totals = CaptureTotals();
if (profile) {
const auto done = std::chrono::steady_clock::now();
totals.captureNanoseconds += static_cast<std::uint64_t>(std::chrono::duration_cast<std::chrono::nanoseconds>(done - started).count());
std::uint64_t initialBytes = 0;
for (const auto& [address, bytes] : initial) initialBytes += bytes.size();
if (++totals.captures % 500 == 0) std::fprintf(stderr, "[capture] %llu captures: %llu word reads, %llu pages fetched, capture (plan lookup + materialize) %.1f s, %llu KiB registered code this capture\n", static_cast<unsigned long long>(totals.captures.load()), static_cast<unsigned long long>(totals.reads.load()), static_cast<unsigned long long>(totals.pages.load()), totals.captureNanoseconds.load() / 1e9, static_cast<unsigned long long>(initialBytes / 1024));
}
return capture;
}
std::vector<ShaderRecompiler::MemoryRegion> ShaderMemory::Regions() const {
std::vector<ShaderRecompiler::MemoryRegion> result;
result.reserve(regions.size());
for (const auto& [address, bytes] : regions) {
result.push_back({address, bytes});
result.reserve(initial.size() + pages.size());
auto next = initial.begin();
// Both maps are ordered by address and never overlap, so a merge keeps the result sorted.
for (const auto& [base, page] : pages) {
while (next != initial.end() && next->first < base) {
result.push_back({next->first, next->second});
++next;
}
for (std::size_t index = 0; index < PageWords;) {
if (!page.read.test(index)) {
++index;
continue;
}
const auto first = index;
while (index < PageWords && page.read.test(index)) ++index;
result.push_back({base + first * sizeof(std::uint32_t), std::as_bytes(std::span(page.words).subspan(first, index - first))});
}
}
for (; next != initial.end(); ++next) result.push_back({next->first, next->second});
return result;
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,181 @@
#include "prx/libSceAgcDriver/Execution/include/WorkerSampler.hpp"
#include <cstdio>
#include <cstdlib>
#include <cstring>
#ifdef _WIN32
#include <windows.h>
#include <tlhelp32.h>
#include <algorithm>
#include <atomic>
#include <chrono>
#include <map>
#include <mutex>
#include <string>
#include <thread>
#include <vector>
#endif
namespace AgcDriver {
#ifdef _WIN32
namespace {
// Debug aid: APS5_SAMPLE_WORKER=<file> samples the calling thread every millisecond and, every 20 s,
// writes "module+offset self inclusive" lines to <file> for offline symbolization with nm.
struct Sampler {
HANDLE target = nullptr;
std::string path;
std::mutex mutex;
std::map<std::uint64_t, std::pair<std::uint64_t, std::uint64_t>> counts;
std::uint64_t samples = 0;
void sample() {
CONTEXT context{};
context.ContextFlags = CONTEXT_FULL;
if (SuspendThread(target) == static_cast<DWORD>(-1)) return;
const bool captured = GetThreadContext(target, &context) != 0;
std::vector<std::uint64_t> frames;
if (captured) {
// Unwinding reads the target's stack while it is suspended; frames without unwind data
// (JIT/guest code) end the walk.
for (int depth = 0; depth < 12 && context.Rip != 0; ++depth) {
frames.push_back(context.Rip);
DWORD64 imageBase = 0;
auto* entry = RtlLookupFunctionEntry(context.Rip, &imageBase, nullptr);
if (entry == nullptr) break;
PVOID handler = nullptr;
DWORD64 establisher = 0;
RtlVirtualUnwind(UNW_FLAG_NHANDLER, imageBase, context.Rip, entry, &context, &handler, &establisher, nullptr);
}
}
ResumeThread(target);
std::lock_guard lock(mutex);
++samples;
for (std::size_t i = 0; i < frames.size(); ++i) {
auto& count = counts[frames[i]];
if (i == 0) ++count.first;
++count.second;
}
}
void write() {
std::lock_guard lock(mutex);
std::FILE* file = std::fopen(path.c_str(), "w");
if (file == nullptr) return;
std::fprintf(file, "# %llu samples\n", static_cast<unsigned long long>(samples));
for (const auto& [address, count] : counts) {
HMODULE module = nullptr;
char name[MAX_PATH] = "?";
if (GetModuleHandleExA(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT, reinterpret_cast<LPCSTR>(address), &module)) GetModuleFileNameA(module, name, sizeof(name));
const char* base = std::strrchr(name, '\\');
std::fprintf(file, "%s 0x%llx %llu %llu\n", base ? base + 1 : name, static_cast<unsigned long long>(address - reinterpret_cast<std::uint64_t>(module)), static_cast<unsigned long long>(count.first), static_cast<unsigned long long>(count.second));
}
std::fclose(file);
}
};
// Debug aid: APS5_SAMPLE_THREADS=<file> samples every thread in the process every 2 ms and, every
// 20 s, writes each busy thread's hottest frames ("module+offset self inclusive") to <file>.
struct ProcessSampler {
std::string path;
DWORD self = 0;
std::map<DWORD, Sampler> threads;
std::uint64_t rounds = 0;
std::chrono::steady_clock::time_point enumerated{};
// The system-wide thread snapshot is slow (tens of ms); the thread list is refreshed every 5 s and
// the rounds in between only sample the known threads.
void enumerate() {
const HANDLE snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, 0);
if (snapshot == INVALID_HANDLE_VALUE) return;
THREADENTRY32 entry{};
entry.dwSize = sizeof(entry);
const DWORD process = GetCurrentProcessId();
for (BOOL more = Thread32First(snapshot, &entry); more; more = Thread32Next(snapshot, &entry)) {
if (entry.th32OwnerProcessID != process || entry.th32ThreadID == self) continue;
auto& thread = threads[entry.th32ThreadID];
if (thread.target == nullptr) thread.target = OpenThread(THREAD_SUSPEND_RESUME | THREAD_GET_CONTEXT | THREAD_QUERY_INFORMATION, FALSE, entry.th32ThreadID);
}
CloseHandle(snapshot);
enumerated = std::chrono::steady_clock::now();
}
void sample() {
if (threads.empty() || std::chrono::steady_clock::now() - enumerated > std::chrono::seconds(5)) enumerate();
for (auto& [id, thread] : threads) {
if (thread.target != nullptr) thread.sample();
}
++rounds;
}
void write() {
std::FILE* file = std::fopen(path.c_str(), "w");
if (file == nullptr) return;
std::fprintf(file, "# %llu rounds\n", static_cast<unsigned long long>(rounds));
for (auto& [id, thread] : threads) {
std::lock_guard lock(thread.mutex);
std::vector<std::pair<std::uint64_t, std::pair<std::uint64_t, std::uint64_t>>> hot(thread.counts.begin(), thread.counts.end());
std::sort(hot.begin(), hot.end(), [](const auto& a, const auto& b) { return a.second.second > b.second.second; });
std::uint64_t leaves = 0;
for (const auto& [address, count] : hot) leaves += count.first;
if (leaves == 0) continue;
std::fprintf(file, "thread %lu samples %llu\n", static_cast<unsigned long>(id), static_cast<unsigned long long>(thread.samples));
for (std::size_t i = 0; i < hot.size() && i < 40; ++i) {
const auto address = hot[i].first;
HMODULE module = nullptr;
char name[MAX_PATH] = "?";
if (GetModuleHandleExA(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT, reinterpret_cast<LPCSTR>(address), &module)) GetModuleFileNameA(module, name, sizeof(name));
const char* base = std::strrchr(name, '\\');
std::fprintf(file, " %s 0x%llx %llu %llu\n", base ? base + 1 : name, static_cast<unsigned long long>(address - reinterpret_cast<std::uint64_t>(module)), static_cast<unsigned long long>(hot[i].second.first), static_cast<unsigned long long>(hot[i].second.second));
}
}
std::fclose(file);
}
};
void StartProcessSampler() {
const char* path = std::getenv("APS5_SAMPLE_THREADS");
if (path == nullptr) return;
std::thread([path = std::string(path)] {
auto* sampler = new ProcessSampler();
sampler->path = path;
sampler->self = GetCurrentThreadId();
auto flushed = std::chrono::steady_clock::now();
for (;;) {
std::this_thread::sleep_for(std::chrono::milliseconds(2));
sampler->sample();
if (std::chrono::steady_clock::now() - flushed > std::chrono::seconds(20)) {
sampler->write();
flushed = std::chrono::steady_clock::now();
}
}
}).detach();
}
}
void StartWorkerSampler() {
StartProcessSampler();
const char* path = std::getenv("APS5_SAMPLE_WORKER");
if (path == nullptr) return;
auto* sampler = new Sampler();
sampler->path = path;
DuplicateHandle(GetCurrentProcess(), GetCurrentThread(), GetCurrentProcess(), &sampler->target, THREAD_ALL_ACCESS, FALSE, 0);
std::thread([sampler] {
auto flushed = std::chrono::steady_clock::now();
for (;;) {
std::this_thread::sleep_for(std::chrono::milliseconds(1));
sampler->sample();
if (std::chrono::steady_clock::now() - flushed > std::chrono::seconds(20)) {
sampler->write();
flushed = std::chrono::steady_clock::now();
}
}
}).detach();
}
#else
void StartWorkerSampler() {}
#endif
}

View File

@ -12,9 +12,20 @@ public:
VkDescriptorBufferInfo Table() const;
VkDescriptorBufferInfo Fault() const;
void CheckFault() const;
// APS5_PROFILE_DRAW: page tables served from the per-device cache and built anew (cumulative),
// and how many recent tables the cache holds right now.
struct TableCacheStats {
std::uint64_t hits = 0;
std::uint64_t misses = 0;
std::size_t held = 0;
};
static TableCacheStats TableCacheCounters();
private:
std::unique_ptr<Buffer> table;
// The page table is read-only to the shader, so consecutive builds mapping the same ranges to the
// same device addresses share one buffer while one of them is alive (see the table cache in
// BdaResources.cpp, which refers to it weakly).
std::shared_ptr<Buffer> table;
std::unique_ptr<Buffer> fault;
std::size_t tableBytes = 0;
};

View File

@ -2,10 +2,11 @@
#define CORE_LIBS_PRX_LIBSCEAGCDRIVER_GRAPHICS_INCLUDE_BUFFERPOOL_HPP
#include "prx/libSceAgcDriver/Graphics/include/Context.hpp"
#include <array>
#include <cstdint>
#include <memory>
#include <mutex>
#include <optional>
#include <vector>
namespace AgcDriver::Graphics {
@ -15,46 +16,80 @@ struct BufferAllocation {
void* mapping;
VkDeviceAddress address;
VkDeviceSize allocationBytes;
// Size the VkBuffer was created with (see BufferPool::Capacity), not the size a user asked for.
std::size_t bytes;
VkBufferUsageFlags usage;
VkMemoryPropertyFlags properties = VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT;
};
// Released buffer allocations kept for reuse, since creating, binding and mapping one costs tens of
// microseconds and a build makes several. Small requests are served by size class (the next power of
// two), so the many differently sized data and copied-region buffers of consecutive builds hit;
// requests of a MiB and more keep their exact size (the multi-MiB registered-range snapshots repeat
// exactly, and rounding them would waste pinned host memory). Retained allocations are evicted least
// recently used under a byte budget and a slot count, so a burst of small buffers between two large
// builds no longer sweeps the large ones out. Reuse is safe because a Buffer is only released once
// the GPU work using it completed (kept until the batch fence, or after a CommandBatch wait).
// APS5_NO_BUFFER_CLASSES=1 matches exact sizes only, as before; APS5_BUFFER_POOL_SLOTS=64 restores
// the old slot count (the bound applies to each tier below, so twice that many slots in all).
//
// The size classes and the exact-size allocations are retained in two tiers with a budget each:
// the multi-MiB texture scratch buffers that come back from a batch (GPU-direct uploads and
// write-backs of storage images) filled the one budget, so every one returned evicted hundreds of
// the 256-byte copied-region buffers to fit, and those then missed on every build (81869 misses
// and 80850 evictions per run, each a Vulkan create or destroy under the pool mutex, 3.5 s per
// run). APS5_BUFFER_POOL_SHARED=1 keeps one tier as before.
class BufferPool {
public:
explicit BufferPool(const Context& context);
~BufferPool();
BufferPool(const BufferPool&) = delete;
BufferPool& operator=(const BufferPool&) = delete;
// The size a buffer for `bytes` is created with: its size class, or `bytes` itself when large.
static std::size_t Capacity(std::size_t bytes);
std::optional<BufferAllocation> Take(std::size_t bytes, VkBufferUsageFlags usage, VkMemoryPropertyFlags properties = VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT);
void Put(const BufferAllocation& allocation) noexcept;
private:
static constexpr std::size_t capacity = 1024;
static constexpr std::size_t bucketCount = 256;
static constexpr std::size_t none = capacity;
struct Slot {
std::optional<BufferAllocation> allocation;
std::size_t next = none;
std::size_t older = none;
std::size_t newer = none;
BufferAllocation allocation;
std::uint64_t lastUse;
};
static std::size_t bucket(std::size_t bytes, VkBufferUsageFlags usage, VkMemoryPropertyFlags properties) noexcept;
BufferAllocation remove(std::size_t index) noexcept;
// One retention tier: its slots, their bytes, the byte budget they are evicted under and its
// counters (APS5_PROFILE_DRAW, reported every 10 s from Take).
struct Tier {
std::vector<Slot> free;
VkDeviceSize retainedBytes = 0;
VkDeviceSize budget = 0;
std::uint64_t hits = 0;
std::uint64_t misses = 0;
std::uint64_t evictions = 0;
};
// The tier a buffer of `capacity` is retained in (the large one for everything when shared).
Tier& tierFor(std::size_t capacity);
void destroy(const BufferAllocation& allocation) noexcept;
// Moves the tier's least recently used slot to `evicted`; the caller destroys those after
// releasing the mutex, so builds taking buffers on other threads do not wait behind the
// Vulkan destroy calls. Nothing changes when the vector cannot grow.
void evictOldest(Tier& tier, std::vector<BufferAllocation>& evicted);
// The retained-slot bound of each tier (APS5_BUFFER_POOL_SLOTS, default `defaultSlots`), read once.
static std::size_t MaxSlots();
VkDevice device;
PFN_vkUnmapMemory unmap;
PFN_vkDestroyBuffer destroyBuffer;
PFN_vkFreeMemory freeMemory;
std::mutex mutex;
std::array<Slot, capacity> slots;
std::array<std::size_t, bucketCount> buckets;
std::array<std::size_t, capacity> freeSlots;
std::size_t freeCount = capacity;
std::size_t oldest = none;
std::size_t newest = none;
VkDeviceSize retainedBytes = 0;
// Not `small`/`large`: <rpcndr.h> (via <windows.h>) defines `small` as a macro.
Tier smallTier;
Tier largeTier;
std::uint64_t clock = 0;
static constexpr VkDeviceSize budget = 512ull * 1024 * 1024;
// The small tier's own budget (512 slots of at most half a MiB each): pinned host memory the
// large tier's budget does not count.
static constexpr VkDeviceSize smallBudget = 64ull * 1024 * 1024;
// Requests of this size and more keep their exact size and go to the large tier.
static constexpr std::size_t classLimit = std::size_t{1} << 20u;
static constexpr std::size_t defaultSlots = 512;
};
std::shared_ptr<BufferPool> GetBufferPool(const Context& context);

View File

@ -4,6 +4,7 @@
#include <cstddef>
#include <cstdint>
#include <span>
#include <vector>
namespace AgcDriver::Graphics {
@ -16,10 +17,11 @@ ColorTileMode DecodeColorTileMode(std::uint32_t attrib3);
class ColorTargetLayout {
public:
ColorTargetLayout(std::uint32_t width, std::uint32_t height, ColorTileMode mode);
ColorTargetLayout(std::uint32_t width, std::uint32_t height, ColorTileMode mode, std::uint32_t bytesPerElement = 4);
std::size_t Bytes() const { return bytes; }
std::size_t LinearBytes() const { return static_cast<std::size_t>(width) * height * 4u; }
std::size_t LinearBytes() const { return static_cast<std::size_t>(width) * height * elementBytes; }
std::size_t Alignment() const { return mode == ColorTileMode::Linear ? 256u : 65536u; }
std::uint32_t BlocksPerRow() const { return pitch / blockWidth; }
std::size_t Offset(std::uint32_t x, std::uint32_t y) const;
void Detile(std::span<const std::byte> source, std::span<std::byte> destination) const;
void Tile(std::span<const std::byte> source, std::span<std::byte> destination) const;
@ -31,6 +33,12 @@ private:
std::uint32_t pitch;
ColorTileMode mode;
std::size_t bytes;
std::uint32_t elementBytes;
std::uint32_t blockWidth = 1;
std::uint32_t blockHeight = 1;
// The XOR swizzle is linear over GF(2), so a block offset is xOffsets[x] ^ yOffsets[y].
std::vector<std::uint32_t> xOffsets;
std::vector<std::uint32_t> yOffsets;
};
}

View File

@ -19,6 +19,7 @@ class TextureCache;
class RenderCache;
class DrawQueue;
class GraphicsPipelineCache;
class Recorder;
class DescriptorCache;
class SamplerCache;
@ -59,11 +60,18 @@ struct Context {
mutable std::shared_ptr<BufferPool> bufferPool;
TextureCache* textureCache = nullptr;
VkPipelineCache pipelineCache = VK_NULL_HANDLE;
bool depthClamp = false;
// Nonzero when VK_EXT_external_memory_host is enabled: the required host pointer alignment.
VkDeviceSize hostImportAlignment = 0;
RenderCache* renderCache = nullptr;
DrawQueue* drawQueue = nullptr;
GraphicsPipelineCache* graphicsPipelines = nullptr;
mutable std::shared_ptr<DescriptorCache> descriptorCache;
mutable std::shared_ptr<SamplerCache> samplerCache;
// Batches GPU work across guest commands (see Recorder); null before the device finished setup.
Recorder* recorder = nullptr;
// Per-device caches of the immutable descriptor objects a ShaderResources build needs (set
// layouts, descriptor pools, samplers); null (tests) means every build makes and destroys its own.
DescriptorCache* descriptorCache = nullptr;
SamplerCache* samplerCache = nullptr;
template<typename TFunction>
TFunction Function(const char* name) const {

View File

@ -0,0 +1,53 @@
#ifndef CORE_LIBS_PRX_LIBSCEAGCDRIVER_GRAPHICS_INCLUDE_DCCMETADATA_HPP
#define CORE_LIBS_PRX_LIBSCEAGCDRIVER_GRAPHICS_INCLUDE_DCCMETADATA_HPP
#ifndef VK_NO_PROTOTYPES
#define VK_NO_PROTOTYPES
#endif
#include <vulkan/vulkan.h>
#include "prx/libSceAgcDriver/Graphics/include/GuestTextureResource.hpp"
#include <cstddef>
#include <cstdint>
#include <span>
namespace AgcDriver::Graphics {
struct Context;
// Delta color compression keeps one key byte per 256 bytes of a color surface. Surfaces are written
// uncompressed here, so the keys that matter are the fast-clear codes a title writes into the metadata
// (the surface then reads as a constant whatever its texels hold) and "uncompressed", which the driver
// stores after it writes a surface so later reads see the texels.
enum class DccKeys { Uncompressed, Clear0000, Clear0001, Clear1110, Clear1111, ClearRegister, Mixed, Unreadable };
const char* DccKeysName(DccKeys keys);
// The keys covering a surface of `surfaceBytes`, when they all agree.
DccKeys ReadDccKeys(std::uint64_t metaAddress, std::uint64_t surfaceBytes);
bool IsDccClear(DccKeys keys);
// Stores "uncompressed" keys over the surface's metadata on the CPU (a guest memory write: it waits
// for recorded GPU work that writes the keys first).
void MarkDccUncompressed(std::uint64_t metaAddress, std::uint64_t surfaceBytes);
// The same after a write-back recorded under GuestMemory::GpuMutex: when the keys are in host-imported
// memory and a recorder is active, the store is a fill recorded into the open batch (ordered behind
// the title's key-writing kernels like the write-back itself, nothing waits on the CPU) and the range
// reads as uncompressed from ReadDccKeys while that batch is pending; otherwise the CPU store above.
// APS5_CPU_DCC_KEYS=1 always stores on the CPU.
void MarkDccUncompressed(const Context& context, std::uint64_t metaAddress, std::uint64_t surfaceBytes);
// Fills `bytes` with the texel a 0000/0001/1110/1111 clear code stands for ("1" is 1.0 or the integer
// maximum; the alpha channel is the last one in memory when alphaOnMsb, else the first, and 3-channel
// formats have none). False when the format has no encoding here.
bool FillDccClear(VkFormat format, DccKeys keys, bool alphaOnMsb, std::span<std::byte> bytes);
// Where a color target's metadata puts alpha: the last channel in memory unless the component swap is
// reversed (single-channel formats: only with the alternate reversed swap).
bool DccAlphaOnMsb(VkFormat format, std::uint32_t componentSwap);
// The keys of a texture's DCC surface when they fast-clear it to a value encodable here, else
// Uncompressed (the texels are read as stored; other keys are reported once).
DccKeys TextureClearKeys(const GuestTextureResource& resource, std::uint64_t guestBytes);
// A surface's texels as a read sees them: the guest bytes, or the clear value of fast-cleared keys.
void ReadTextureSurface(const GuestTextureResource& resource, DccKeys keys, std::span<std::byte> bytes);
}
#endif

View File

@ -3,11 +3,28 @@
#include "prx/libSceAgcDriver/Graphics/include/Pipeline.hpp"
#include "prx/libSceAgcDriver/Execution/include/Pm4.hpp"
#include <memory>
#include <vector>
namespace AgcDriver::Graphics {
void Draw(const Context& context, const State& state, const Pm4::DrawParameters& draw, std::span<const CompiledShader> shaders, std::span<const GuestMemorySnapshot> snapshots = {});
// Recorded draws whose written guest buffers were copied (their results reach guest memory by a CPU
// write-back when their batch completes), listed until that write-back ran. It is the draw
// counterpart of VulkanDevice's State::copiedWriters for dispatches, which VulkanDevice.cpp consults
// in two places: DispatchIndirect (an indirect dispatch whose arguments such a writer produced reads
// them on the CPU, behind the write-back, instead of from the host import on the GPU) and the fill
// HLE (a vkCmdFillBuffer over a range a listed write-back will land on must wait for it, or the
// write-back overwrites the fill). Draws are listed here only under APS5_RECORD_COPIED_DRAWS=1,
// which is safe only once BOTH of those checks also test this list, i.e. each extends its
// `std::any_of(state->copiedWriters ...)` with
// `|| std::any_of(Graphics::DrawCopiedWriters()->begin(), Graphics::DrawCopiedWriters()->end(), [&](const auto& writer) { return writer->WritesOverlap(<its range>); })`;
// cleaner still, State::copiedWriters can become this list (one registry for both producers), after
// which the switch can turn into APS5_NO_RECORD_COPIED_DRAWS. Until then such draws are recorded and
// waited for at once. Read and written under GuestMemory::GpuMutex, like the device's list.
std::shared_ptr<std::vector<std::shared_ptr<ShaderResources>>> DrawCopiedWriters();
}
#endif

View File

@ -13,18 +13,15 @@ public:
GpuColorTransfer(const GpuColorTransfer&) = delete;
GpuColorTransfer& operator=(const GpuColorTransfer&) = delete;
void Upload(std::uint64_t address, std::uint32_t width, std::uint32_t height, ColorTileMode mode);
void Detile(VkCommandBuffer commands, bool swapRedBlue = false);
void Detile(VkCommandBuffer commands, bool swapRedBlue = false, bool tenBit = false);
void Tile(VkCommandBuffer commands);
void WriteBack(std::uint64_t address);
void WriteBackTracked(std::uint64_t address);
bool MatchesGuest(std::uint64_t address);
VkBuffer LinearBuffer() const;
RenderTarget& Target(const ColorTarget& color, bool blending);
private:
void writeBack(std::uint64_t address, bool tracked);
void prepare(std::uint32_t width, std::uint32_t height, ColorTileMode mode);
void convert(VkCommandBuffer commands, bool toTiled, bool swapRedBlue);
void convert(VkCommandBuffer commands, bool toTiled, bool swapRedBlue, bool tenBit = false);
void release() noexcept;
Context context;
VkDescriptorSetLayout descriptorLayout = VK_NULL_HANDLE;
@ -32,10 +29,10 @@ private:
VkDescriptorSet descriptorSet = VK_NULL_HANDLE;
VkPipelineLayout pipelineLayout = VK_NULL_HANDLE;
VkPipeline pipeline = VK_NULL_HANDLE;
// Guest bytes in host memory, and the device-local copies the conversion shader works on.
std::unique_ptr<Buffer> tiled;
std::unique_ptr<Buffer> linear;
std::unique_ptr<Buffer> readback;
std::unique_ptr<Buffer> upload;
std::unique_ptr<DeviceBuffer> tiledDevice;
std::unique_ptr<DeviceBuffer> linear;
std::uint32_t width = 0;
std::uint32_t height = 0;
ColorTileMode mode = ColorTileMode::Linear;

View File

@ -1,30 +0,0 @@
#ifndef CORE_LIBS_PRX_LIBSCEAGCDRIVER_GRAPHICS_INCLUDE_GRAPHICSPIPELINECACHE_HPP
#define CORE_LIBS_PRX_LIBSCEAGCDRIVER_GRAPHICS_INCLUDE_GRAPHICSPIPELINECACHE_HPP
#include "prx/libSceAgcDriver/Graphics/include/Pipeline.hpp"
#include "prx/libSceAgcDriver/Graphics/include/RenderCache.hpp"
#include <list>
#include <map>
#include <string>
namespace AgcDriver::Graphics {
class GraphicsPipelineCache {
public:
explicit GraphicsPipelineCache(const Context& context) : context(context) {}
std::shared_ptr<Pipeline> Get(const State& state, const std::shared_ptr<ResidentColor>& target, const ShaderResources& resources, std::span<const CompiledShader> shaders);
private:
struct Entry {
std::string key;
std::shared_ptr<ResidentColor> target;
std::shared_ptr<Pipeline> pipeline;
};
Context context;
std::list<Entry> entries;
std::map<std::string, std::list<Entry>::iterator> lookup;
};
}
#endif

View File

@ -5,6 +5,8 @@
#include "BdaAbi.hpp"
#include "prx/libc/include/GuestAllocations.hpp"
#include <memory>
#include <optional>
#include <utility>
#include <vector>
namespace AgcDriver::Graphics {
@ -14,17 +16,118 @@ struct GuestMemorySnapshot {
std::span<const std::byte> bytes;
};
// A guest allocation imported with VK_EXT_external_memory_host, usable by the GPU in place.
struct HostImport {
std::uint64_t base;
std::uint64_t bytes;
VkBuffer buffer;
VkDeviceMemory memory;
VkDeviceAddress address;
// Identity for the life of this import (see HostImportSerial); 0 until first asked for.
std::uint64_t serial = 0;
};
// The host import of the registered allocation containing [address, address + bytes), made on demand
// (alignment and budget permitting), or null. Bytes at `address` are at `address - import->base` in
// the import's buffer.
const HostImport* HostImportFor(const Context& context, std::uint64_t address, std::size_t bytes);
// Whether an existing import covers [address, address + bytes), without reconciling the imports
// with the registry or making one (HostImportFor may take a registry lease): a hint for choices
// made outside the device lock (a sampled texture's path, a dispatch's pre-sync); the path taken
// re-checks with HostImportFor when it binds the import.
bool HostImportCovers(const Context& context, std::uint64_t address, std::size_t bytes);
// A persistent device copy of one registered range of the main guest image (which cannot be host
// imported); see GuestBufferMemory.cpp.
struct ImageMirror;
class Recorder;
// Identity of the import serving [address, address + bytes): a serial unique for the life of one
// import, 0 when no import serves the range. An equal serial later means the same VkBuffer still
// backs the range (a re-import gets a new serial), which is what a descriptor set written against it
// needs to stay valid (see the ShaderResources cache). With `reconcile`, imports whose registered
// range changed are dropped first, as an upload does; without it (right after an upload, before the
// work using the import is recorded) the imports are left as they are.
std::uint64_t HostImportSerial(const Context& context, std::uint64_t address, std::size_t bytes, bool reconcile);
// Identity of the read-only image mirror serving [address, address + bytes) (the counterpart of
// HostImportSerial for main-image ranges, which no import serves): a serial unique for the life of
// one mirror with its top bit set, 0 when no read-only mirror whose registered range still exists
// covers the range. A read-only mirror's bytes and VkBuffer never change, so a descriptor set written
// against it stays valid while the serial is unchanged.
std::uint64_t ImageMirrorSerial(const Context& context, std::uint64_t address, std::size_t bytes);
// Deferred lease release (see GuestBufferMemory.cpp). An address-based build leases every readable
// registered allocation until its write-back, which runs when its batch completed; the guest's
// unmap/mprotect/free of a leased allocation waits for that through the registry's pin waiter, which
// submits the recorder and finishes its batches up to the newest one recorded with a lease. SyncLeaseWork
// says whether the build's work must instead be synced as soon as it is recorded
// (APS5_SYNC_LEASE_DISPATCH=1, the behaviour before deferral). The driver and the draw path count
// their choice with CountLeaseOutcome right after the lease-holding resources were kept by the open
// batch: `batchSerial` is that batch's serial (Recorder::Submissions() + 1 under the device lock), 0
// when synced, and the waiter targets the newest of them. Under APS5_PROFILE_DRAW CountLeaseOutcome
// prints the [address-sync] leases line every 10 s from LeaseCounters (cumulative).
bool SyncLeaseWork();
void CountLeaseOutcome(bool synced, std::uint64_t batchSerial);
struct LeaseStats {
std::uint64_t deferred = 0;
std::uint64_t synced = 0;
// Pin-contention waits made by guest threads (the waiter calls), how many of them finished the
// newest lease batch, how many had to drain the whole recorder instead (no lease batch noted),
// and their total time.
std::uint64_t contentionWaits = 0;
std::uint64_t contentionSyncs = 0;
std::uint64_t contentionDrains = 0;
double contentionMs = 0;
};
LeaseStats LeaseCounters();
class GuestBufferMemory {
public:
explicit GuestBufferMemory(const Context& context);
void AcquireRegistered();
// A guest range bound through a descriptor. Both read live guest memory at upload and bind the
// same way (a storage buffer); AddWritable also notes the range in Writes(), so it gets the
// write-back's reference copy, a write-back, the recorder's pending-write note and the
// direct-write marks. AddReadable is for an element the shader is proved never to store to
// (DescriptorBinding::bufferWritten): the CPU never has to wait for it.
void AddWritable(std::uint64_t address, std::size_t bytes);
void AddReadable(std::uint64_t address, std::size_t bytes);
void AddSnapshot(const GuestMemorySnapshot& snapshot);
// Upload is the two stages below back to back. UploadPrepare needs no device lock: it merges the
// regions, binds the image mirrors and host imports that already serve them (an import pointer is
// re-checked against the registry epoch later) and copies read-only regions. UploadFinish runs
// under GuestMemory::GpuMutex: it reconciles and makes imports (retiring one hands its buffer to
// the recorder), refreshes writable mirrors and flushes pending results (both may wait for
// recorded work) and copies the regions a descriptor may write, so the window between that copy
// and the recorder's pending-write note stays closed.
void Upload(bool addressable);
void UploadPrepare(bool addressable);
void UploadFinish(bool addressable);
VkDescriptorBufferInfo Descriptor(std::uint64_t address, std::size_t bytes) const;
std::vector<ShaderRecompiler::BdaAbi::Range> AddressRanges() const;
void WriteBack();
bool WritesOverlap(std::uint64_t address, std::size_t bytes) const;
// Guest ranges the shader may write through descriptors, as [begin, end).
const std::vector<std::pair<std::uint64_t, std::uint64_t>>& Writes() const { return writes; }
// Whether any written range lives in a copied buffer, so a write-back must run once the GPU is done.
bool HasCopiedWrites() const;
// Reports writes into host-imported memory (made by the GPU in place, or copied back into it by
// RecordCopyBacks) to the write tracking now.
void MarkDirectWrites() const;
// Records, into the recorder's open batch, the copy of every written sub-range of a region the
// GPU copied out of a host import (see Region::gpuCopy) back into the import: what the shader
// wrote lands in guest memory by the GPU, ordered after the recorded work, so the region needs
// no CPU write-back (HasCopiedWrites no longer counts it) and the caller notes the ranges as
// pending writes and marks them as MarkDirectWrites does. Called right after the work using the
// regions was recorded (ShaderResources::MarkGpuWrites), under GuestMemory::GpuMutex. A use that
// never calls it (a synchronous draw) stores the staging bytes from the CPU in WriteBack.
void RecordCopyBacks(Recorder& recorder);
// Whether registered allocations are pinned until write-back (address-based shaders).
bool HoldsLease() const { return !lease.empty(); }
// Every uploaded region as [begin, end) when all of them are served by host imports (nothing was
// copied, so the upload can serve a later identical build), else nothing.
std::optional<std::vector<std::pair<std::uint64_t, std::uint64_t>>> DirectRegions() const;
private:
struct Region {
@ -32,14 +135,66 @@ private:
std::uint64_t end;
bool writable;
std::vector<std::byte> snapshot;
std::unique_ptr<Buffer> buffer;
// Shared so a recorded GPU copy into it (see gpuCopy) can keep it in the batch itself, before
// the caller keeps the whole resources: a throw between the two would otherwise return it to
// the pool under an unsubmitted copy command.
std::shared_ptr<Buffer> buffer;
// Guest bytes as uploaded; write-back only stores bytes the GPU changed.
std::vector<std::byte> uploaded {};
// Registered allocation that is imported: its bytes are read from live guest memory, not a snapshot.
bool hostBacked = false;
// Set when the region is served by an imported allocation; nothing is copied or written back.
const HostImport* direct = nullptr;
// Set when the region covers uncommitted pages: only the `backed` parts (possibly none) are guest
// memory; the rest reads as zeros and is never stored.
bool sparse = false;
std::vector<std::pair<std::uint64_t, std::uint64_t>> backed {};
// Set when the region is served by an image mirror: nothing is copied; writable mirrors are
// written back by comparing with the mirror's shadow. Kept alive here for recorded work.
std::shared_ptr<ImageMirror> mirror {};
// The mirror is a descriptor sub-range one found by UploadPrepare (not a lease mirror), which
// UploadFinish confirms still has its Range before the region binds it.
bool subrangeMirror = false;
// Set by UploadPrepare when UploadFinish still has device-lock work for the region: a copy a
// descriptor writes, a writable mirror's refresh, an import to reconcile or make, or the
// pending-results flush of an import it took.
bool pending = false;
// The region lies inside a host import but is misaligned for binding in place: `buffer` is
// filled by a vkCmdCopyBuffer from the import recorded into the batch (no CPU read, so no
// flush-hook wait), from `copySource` (the import's VkBuffer, alive until the batch
// completed, with the guest address of its first byte). `copiedBack` once RecordCopyBacks
// recorded the written sub-ranges' copies back into the import.
bool gpuCopy = false;
VkBuffer copySource = VK_NULL_HANDLE;
std::uint64_t copySourceBase = 0;
bool copiedBack = false;
};
void validate(std::uint64_t address, std::size_t bytes) const;
// The region of a descriptor-bound range (AddWritable/AddReadable), committed pages only.
void addDescriptorRegion(std::uint64_t address, std::size_t bytes);
// Gives a region a buffer of its own with its bytes (guest memory for host-backed and writable
// ranges, plus the write-back's reference copy for a range a descriptor writes; else its snapshot).
void copyRegion(Region& region, bool addressable);
// Whether a region inside a host import that cannot be bound in place is copied by the GPU
// instead of the CPU (see Region::gpuCopy): live guest bytes, not sparse, at most the size
// APS5_GPU_COPY_MAX_KIB allows, and APS5_CPU_COPIES unset.
bool gpuCopyEligible(const Region& region) const;
// Records the import-to-buffer copies of the given gpuCopy regions into the open batch, with
// the barriers that order them after earlier recorded writes and before the shaders reading them.
void recordGpuCopies(std::span<Region* const> copies, bool addressable);
Context context;
GuestAllocations::Lease lease;
// Import registry epoch when `direct` pointers were taken at acquire time; they are reused while
// no import was destroyed since.
std::uint64_t importsEpoch = 0;
std::vector<Region> regions;
// Whether `regions` is in ascending address order (true right after AcquireRegistered, whose
// regions follow the registry's order), so AddSnapshot can search instead of scanning.
bool regionsSorted = false;
std::vector<std::pair<std::uint64_t, std::uint64_t>> writes;
// UploadPrepare ran (regions are frozen); `uploaded` once UploadFinish ran.
bool prepared = false;
bool uploaded = false;
bool committed = false;
};

View File

@ -12,14 +12,32 @@ enum class TextureTileMode {
kStandard256B,
kStandard4KB,
kStandard64KB,
RenderTarget64KB
// 64 KiB XOR swizzles; their address equations live in TextureSwizzleEquations.hpp.
kZ64KBX,
kS64KBX,
kD64KBX,
kR64KBX,
// Upstream's name for SW_64KB_R_X (tile mode 0x1b).
RenderTarget64KB = kR64KBX
};
// The hardware SW_MODE of an XOR swizzle tile mode, or 0 for the modes addressed without an equation table.
constexpr std::uint32_t XorSwizzleMode(TextureTileMode mode) {
switch (mode) {
case TextureTileMode::kZ64KBX: return 24u;
case TextureTileMode::kS64KBX: return 25u;
case TextureTileMode::kD64KBX: return 26u;
case TextureTileMode::kR64KBX: return 27u;
default: return 0u;
}
}
enum class TextureDimension {
k1D,
k2D,
k2DArray,
kCube
kCube,
k3D
};
struct GuestTextureResource {
@ -37,6 +55,11 @@ struct GuestTextureResource {
std::uint8_t dstSelY;
std::uint8_t dstSelZ;
std::uint8_t dstSelW;
// Last mip level the view exposes; the surface itself holds mipCount levels.
std::uint32_t lastLevel = 0;
// DCC metadata of a compressed surface, or 0 (see DccMetadata.hpp).
std::uint64_t dccAddress = 0;
bool dccAlphaOnMsb = false;
};
GuestTextureResource DecodeTextureResource(std::span<const std::uint32_t> words);

View File

@ -2,31 +2,86 @@
#define CORE_LIBS_PRX_LIBSCEAGCDRIVER_GRAPHICS_INCLUDE_PIPELINE_HPP
#include "prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp"
#include <set>
namespace AgcDriver::Graphics {
struct VertexInputLayout;
// The attachments of one render pass instance. Work recorded with it references the handle until
// the batch completes, so a recorded draw keeps the object (Recorder::Keep) like its pipeline.
class Framebuffer {
public:
Framebuffer(const Context& context, VkRenderPass renderPass, std::span<const VkImageView> targets, VkExtent2D extent);
~Framebuffer();
Framebuffer(const Framebuffer&) = delete;
Framebuffer& operator=(const Framebuffer&) = delete;
VkFramebuffer Handle() const { return framebuffer; }
// Forgets the handle without destroying it (the device it belongs to is already gone).
void Abandon() noexcept;
private:
Context context;
VkFramebuffer framebuffer = VK_NULL_HANDLE;
};
// The shader modules, layout, render pass and VkPipeline of one draw configuration. Viewport and
// scissor are dynamic state set at Begin, so pipelines are shared by draws that differ only there
// (see CachedPipeline).
class Pipeline {
public:
Pipeline(const Context& context, const State& state, const RenderTarget* target, const ShaderResources& resources, std::span<const CompiledShader> shaders);
// `state` carries the blend states with the outputs the pixel shader lacks already masked and
// `vertexInput` the layout of the draw's vertex descriptors; the shaders were validated by the
// caller (ValidateShaders).
Pipeline(const Context& context, const State& state, const VertexInputLayout& vertexInput, const ShaderResources& resources, std::span<const CompiledShader> shaders);
~Pipeline();
Pipeline(const Pipeline&) = delete;
Pipeline& operator=(const Pipeline&) = delete;
VkPipelineLayout Layout() const;
void Begin(VkCommandBuffer commands, VkExtent2D extent) 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.
std::shared_ptr<Framebuffer> AcquireFramebuffer(std::span<const VkImageView> targets, std::span<const std::shared_ptr<StorageTexture>> 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;
void PushConstants(VkCommandBuffer commands, std::span<const CompiledShader> shaders) const;
// Forgets the Vulkan objects without destroying them: for entries of a device that is already gone.
void Abandon() noexcept;
private:
struct CachedFramebuffer {
std::vector<VkImageView> views;
std::vector<std::weak_ptr<StorageTexture>> owners;
VkExtent2D extent;
std::shared_ptr<Framebuffer> framebuffer;
};
void release() noexcept;
Context context;
std::vector<VkShaderModule> _modules;
VkPipelineLayout layout = VK_NULL_HANDLE;
VkRenderPass renderPass = VK_NULL_HANDLE;
VkFramebuffer framebuffer = VK_NULL_HANDLE;
VkPipeline pipeline = VK_NULL_HANDLE;
std::size_t attachments = 0;
std::vector<CachedFramebuffer> framebuffers;
};
// The pipeline for this configuration, shared across draws and frames: keyed by the stages' variant
// ids (the rect-list tessellation pair by the vertex and fragment ids that generate it), the vertex
// input layout, the descriptor set layout key, push constant stages and every pipeline state field.
// A configuration whose stage has no variant id gets a private pipeline. Entries are LRU-bounded and
// only evicted once no recorded draw holds them. Debug aid: APS5_NO_PIPELINE_CACHE=1 builds one per
// draw as before.
std::shared_ptr<Pipeline> CachedPipeline(const Context& context, const State& state, const VertexInputLayout& vertexInput, const ShaderResources& resources, std::span<const CompiledShader> shaders);
// Destroys the cached pipelines of a device; to be called before the device goes away. Without it,
// entries of a gone device are recognised by their buffer pool (made and reset with the device, so
// it tells device instances apart when the loader reuses a VkDevice handle) and forgotten unused.
void ClearCachedPipelines(VkDevice device);
// Device limit checks of the viewport, which is dynamic state and so no longer checked by Pipeline.
void ValidateViewport(const Context& context, const VkViewport& viewport);
void ValidateShaderPair(const ShaderRecompiler::RecompileResult& vertex, const ShaderRecompiler::RecompileResult& fragment);
void ValidateShaders(std::span<const CompiledShader> shaders, const State& state, const VkPhysicalDeviceSubgroupProperties& subgroup, bool fragmentShaderBarycentric);
// Returns the color attachment locations the pixel shader writes.
std::set<std::uint32_t> ValidateShaders(std::span<const CompiledShader> shaders, const State& state, const VkPhysicalDeviceSubgroupProperties& subgroup, bool fragmentShaderBarycentric);
}

View File

@ -0,0 +1,262 @@
#ifndef CORE_LIBS_PRX_LIBSCEAGCDRIVER_GRAPHICS_INCLUDE_RECORDER_HPP
#define CORE_LIBS_PRX_LIBSCEAGCDRIVER_GRAPHICS_INCLUDE_RECORDER_HPP
#include "prx/libSceAgcDriver/Graphics/include/Context.hpp"
#include <chrono>
#include <cstddef>
#include <cstdint>
#include <array>
#include <deque>
#include <mutex>
#include <functional>
#include <memory>
#include <optional>
#include <span>
#include <unordered_map>
#include <utility>
#include <vector>
namespace AgcDriver::Graphics {
// Accumulates GPU work across guest commands so the CPU does not wait for each one. Dispatches and the
// copies that feed them record into one open batch; Submit sends it to the queue without waiting and
// Sync waits for every batch, then runs its completion actions (write-backs) in order. Objects handed
// to Keep live until the batch that recorded them completed. Guest memory ranges the recorded work
// will write are noted, so a CPU access to such a range (the GuestMemory flush hook) syncs first.
// Every call happens under GuestMemory::GpuMutex, except WaitSerial and the static lock-free readers
// below. The noted ranges are also published as an immutable snapshot (rebuilt under the mutex at
// every note and after a batch's completions ran) that the flush hook reads without the mutex, so
// accesses overlapping nothing never wait behind device work.
//
// Submissions are numbered (serials) and, when the device has timeline semaphores, every Submit
// signals a timeline semaphore with its serial: a thread can then wait for "everything submitted
// up to serial S" WITHOUT the mutex (WaitSerial), because the monotonic value cannot be reset or
// recycled the way the pooled batch fences are, and afterwards run the completions under the mutex
// (FinishUpTo). Without timeline semaphores callers use the locked Sync as before.
class Recorder {
public:
// `timelineSemaphores`: the device enabled VK_KHR_timeline_semaphore (WaitSerial is usable).
explicit Recorder(const Context& context, bool timelineSemaphores = false);
~Recorder();
Recorder(const Recorder&) = delete;
Recorder& operator=(const Recorder&) = delete;
// The open batch's command buffer, starting a batch when none is open.
VkCommandBuffer Commands();
bool Recording() const { return open != nullptr; }
bool Idle() const { return open == nullptr && inFlight.empty(); }
// Whether recorded work still has completion actions (write-backs the CPU must see) to run.
bool HasCompletions() const;
// The object lives at least until the batch open now completed; it is then destroyed AFTER the
// finishing thread released GuestMemory::GpuMutex, on a release thread of its own (never under
// a hold, see Recorder.cpp ReleaseDeferredKeeps; the finishing thread destroys it itself when
// the thread's queue is full, APS5_RELEASE_QUEUE_MAX batches, or with APS5_RELEASE_ON_UNLOCK=1;
// APS5_RELEASE_UNDER_LOCK=1 destroys it in the reap as before), so its destructor must need
// neither the mutex nor the recorder nor a particular thread. ~Recorder joins the release
// thread and waits for every release in progress before the device goes.
void Keep(std::shared_ptr<void> object);
void OnComplete(std::function<void()> action);
void NotePendingWrite(std::uint64_t address, std::size_t bytes);
// Notes several [begin, end) ranges and publishes the snapshot once (a dispatch writes many buffers).
void NotePendingWrites(std::span<const std::pair<std::uint64_t, std::uint64_t>> ranges);
bool PendingWriteOverlaps(std::uint64_t address, std::size_t bytes) const;
// Whether the OPEN (unsubmitted) batch writes the range: a wait on such a range must submit it.
bool OpenWriteOverlaps(std::uint64_t address, std::size_t bytes) const;
// What SyncThrough(address, bytes) would wait for, for the [hooksync] attribution: the newest
// batch whose noted range overlaps the access (the open batch counts first, with the serial it
// will get), whether that batch's fence has already signaled (its GPU work is done, so the
// access's bytes are already final; never for the open batch), that noted range, and how many
// batches the wait finishes. Nullopt: no overlap.
struct PendingWriteInfo {
std::uint64_t serial;
bool open;
bool signaled;
std::uint64_t rangeBegin;
std::uint64_t rangeEnd;
std::size_t batchesToFinish;
};
std::optional<PendingWriteInfo> DescribePendingWrite(std::uint64_t address, std::size_t bytes) const;
// Ends and submits the open batch without waiting.
void Submit();
// Submits the open batch and returns the serial of the newest submitted batch (0 when nothing was
// ever submitted); WaitSerial(serial) then covers all recorded work.
std::uint64_t SubmitAndEpoch();
bool HasTimeline() const { return timeline != VK_NULL_HANDLE; }
// Waits until every batch up to `serial` completed on the GPU. Called WITHOUT GuestMemory::GpuMutex:
// it touches only the immutable timeline semaphore (the caller keeps the device alive). Requires
// HasTimeline(); the debug switch APS5_NO_TIMELINE makes the device create the recorder without one.
void WaitSerial(std::uint64_t serial);
// Under the mutex: finishes (completions, release) the in-flight batches up to `serial`, from the
// front only; tolerates batches another thread finished meanwhile. Later batches stay in flight.
void FinishUpTo(std::uint64_t serial);
// Submits and waits for every batch, running completions in order.
void Sync();
// Waits only for the batches up to the newest one that writes the range (submitting the open
// batch when it is that one); later batches stay in flight. Fences of one queue signal in
// submission order, so completions still run in order. Debug aid: APS5_NO_SYNC_THROUGH=1 syncs all.
// `waitUnlocked` (the flush hook): when the caller's GpuMutex acquisition is the outermost one
// on this thread and the device has a timeline, the GPU wait runs WITHOUT the mutex (released
// and retaken here; the caller must hold nothing else that orders after it) and only the
// completions run under it, so other queues do not queue behind a CPU read's wait. Inside a
// batch's completion action no wait is made at all: every batch still in flight was recorded
// after the completing one, and its store is that batch's in-order write (see Recorder.cpp).
void SyncThrough(std::uint64_t address, std::size_t bytes, bool waitUnlocked = false);
// Completes batches that already finished; returns whether nothing is in flight.
bool Reap();
std::uint64_t Submissions() const { return submissions; }
// Pending-label table. A GPU label (RELEASE_MEM/WRITE_DATA recorded as a store into the batch)
// is noted per 4-byte dword with the value it will store and a record-order stamp (the driver's
// event serial, taken when the label is recorded). A WAIT_REG_MEM whose submission was received
// (stamped) BEFORE the label was recorded may take the value from the table instead of waiting
// for the GPU: any CPU store the game ordered before that submit call precedes the label in
// program order exactly as on hardware, and later recorded work follows the label in queue
// order. Entries recorded before the submission (the previous frame's label at the same address)
// are never trusted. Entries leave the table with their batch (a memory bound only).
void NoteLabel(std::uint64_t address, std::span<const std::byte> bytes, std::uint64_t stamp, std::uint32_t queue);
// The 4- or 8-byte value the table holds for `address` when every dword is present with a stamp
// newer than `afterStamp`; `queue` receives the recording queue of the first dword.
std::optional<std::uint64_t> PendingLabel(std::uint64_t address, std::size_t bytes, std::uint64_t afterStamp, std::uint32_t& queue) const;
std::size_t PendingLabels() const { return labels.size(); }
// Whether any tracked label dword lies inside [address, address + bytes): a range query for
// large ranges (a fill of megabytes), where a per-dword lookup would not do.
bool PendingLabelIn(std::uint64_t address, std::size_t bytes) const;
// PendingLabel of the active recorder WITHOUT GuestMemory::GpuMutex: the table has a small mutex
// of its own (every mutation holds both), so a WAIT_REG_MEM consults it without queueing behind
// device work. Nothing is done under the table mutex but the lookup (it never takes the GPU mutex).
static std::optional<std::uint64_t> LookupLabel(std::uint64_t address, std::size_t bytes, std::uint64_t afterStamp, std::uint32_t& queue);
// Whether the lock-free pending-write snapshot (open, in-flight and finishing batches) overlaps
// the range: false means no recorded work writes it, so a wait on it has nothing to submit.
static bool SnapshotWriteOverlaps(std::uint64_t address, std::size_t bytes);
// A label behind pending completions (CPU write-backs the label must not precede): stored to
// guest memory by a completion action appended to the open batch, or to the newest in-flight
// batch when none is open (no empty submission), so it runs after every earlier write-back in
// submission order. The range is noted as a pending write of that batch so CPU reads through the
// flush hook still sync, and the label enters the table like a GPU one.
// `storedOnGpu`: the same bytes were also recorded into the open batch (a host import), so the
// completion re-stores them only when a CPU write-back noted by NoteWrittenBack overlapped the
// range since; the plain store of a label the GPU has no view of always runs. An unconditional
// completion store would land on memory the game may have reused by then (a stale label value
// over a fresh command buffer). Debug aid: APS5_LABEL_STORE_ALWAYS=1 stores unconditionally.
void AfterCompletions(std::uint64_t address, std::span<const std::byte> bytes, std::uint64_t stamp, std::uint32_t queue, bool storedOnGpu);
// A CPU store of GPU results into guest memory (GuestBufferMemory::WriteBack): recorded so a
// completion label store can tell whether its bytes were overwritten.
static void NoteWrittenBack(std::uint64_t address, std::size_t bytes);
// Lock-free readers for the queue workers' packet loops and WAIT_REG_MEM polls (values of the
// active recorder; a torn-down recorder never publishes):
// when the open batch received its first label (nullopt: none pending), so the label's
// submission can be bounded (APS5_LABEL_FLUSH_US) without taking the mutex to look;
static std::optional<std::chrono::steady_clock::time_point> PendingLabelSince();
// bumped by every note of a pending write into the open batch, so a poller learns that the
// producer may have recorded the label it waits for;
static std::uint64_t WriteGeneration();
// labels waiting in completion actions (the CPU must reap their batch for them to land);
static std::uint64_t PendingCompletionLabels();
// dispatches and draws recorded since the last submit (the driver counts them: CountRecordedWork),
// so a batch is submitted after a bounded amount of work (APS5_BATCH_CAP).
static std::uint64_t RecordedWorkSinceSubmit();
static void CountRecordedWork();
// The recorder of the current device, for code that only has guest addresses (the flush hook)
// and for helpers that must not recycle resources the recorded work still uses.
static Recorder* Active();
void Activate();
// APS5_PROFILE_DRAW: syncs by source (0 device idle, 1 CPU access to pending writes, 2 CPU read
// after a recorded store, 3 address-based dispatch, 4 other). Contract: the call announces the
// source for the Sync/SyncThrough this thread makes NEXT, which consumes it (a sync without a
// preceding CountSync counts as "other"); call it right before the sync and never without one,
// or the announcement attributes an unrelated later wait. `site` names the call site for the
// [recorder] "top sync sites" table (a return address, printed as a module offset like the
// [guestmem] callers): a caller that syncs on behalf of others (VulkanDevice::WaitIdle) passes
// its own return address, so the table names the driver site, not the wrapper; without it the
// return address of the Sync/SyncThrough call itself is taken, so an unannounced sync still
// names its real caller.
static void CountSync(int source, const void* site = nullptr);
// Names only the site for the sync or drain this thread makes next (its source stays as
// announced, or as the drain counts it): for a wrapper (VulkanDevice::ReapRecorded) whose
// FinishUpTo counts the drain itself, so a CountSync there would count it twice.
static void AnnounceSyncSite(const void* site);
// APS5_PROFILE_DRAW: the calling thread's fence and timeline waits so far, in milliseconds (0
// when not profiling): a caller reads it around a span of its own work to learn how much of
// that span waited for the GPU (a resource build's nested flush-hook waits, a draw's).
static double ThreadWaitedMs();
// APS5_PROFILE_GPU=1: GPU time of recorded work by key (a guest program address), from timestamp
// queries around each timed range; the totals per key are reported every 10 s. Begin returns the
// range index to pass to End, or NoTiming when timing is off or the batch's queries are used up.
static constexpr std::uint32_t NoTiming = 0xffffffffu;
std::uint32_t BeginGpuTiming(std::uint64_t key);
void EndGpuTiming(std::uint32_t index);
private:
struct Batch {
VkCommandBuffer commands = VK_NULL_HANDLE;
VkFence fence = VK_NULL_HANDLE;
std::vector<std::shared_ptr<void>> kept;
std::vector<std::function<void()>> completions;
std::vector<std::pair<std::uint64_t, std::uint64_t>> writes;
// Submission number (1-based): identifies a batch after its allocation may have been reused.
std::uint64_t serial = 0;
bool submitted = false;
VkQueryPool queries = VK_NULL_HANDLE;
std::vector<std::uint64_t> timedKeys;
// Dword addresses this batch noted in the label table (removed when it finishes).
std::vector<std::uint64_t> labelDwords;
// Labels stored by this batch's completion actions (see AfterCompletions); subtracted from
// the lock-free pending count when the batch finishes, whether or not its completions ran.
std::uint32_t completionLabelCount = 0;
};
struct LabelEntry {
std::uint32_t value;
std::uint32_t queue;
std::uint64_t stamp;
const Batch* batch;
};
void readGpuTiming(Batch& batch);
// The unlocked wait of SyncThrough(waitUnlocked): the target batch (submitting the open one when
// it is the target), the timeline wait with the mutex released, then the completions up to it.
// Returns false when the locked path must run instead (no timeline, nested acquisition, off).
bool syncThroughUnlocked(std::uint64_t address, std::uint64_t end, int source, const void* site);
// `source` is the CountSync source the wait is attributed to.
void finish(std::unique_ptr<Batch> batch, bool wait, int source);
void release(Batch& batch) noexcept;
static bool overlaps(const Batch& batch, std::uint64_t address, std::uint64_t end);
// Rebuilds the lock-free snapshot of pending writes from open, inFlight and finishing.
void publishPendingWrites() const;
// Appends one range to the open batch; returns whether the snapshot must be rebuilt for it.
bool noteWrite(std::uint64_t address, std::size_t bytes);
// Appends one range to `batch` (open or in flight) and publishes the snapshot if needed.
void noteWriteOn(Batch& batch, std::uint64_t address, std::size_t bytes);
void noteLabelOn(Batch& batch, std::uint64_t address, std::span<const std::byte> bytes, std::uint64_t stamp, std::uint32_t queue);
// Whether a write-back noted after `sequence` overlapped [begin, end); true when the ring no
// longer reaches back to `sequence` (conservative: the store runs as before).
bool writtenBackSince(std::uint64_t sequence, std::uint64_t begin, std::uint64_t end);
std::mutex writtenBackMutex;
std::deque<std::array<std::uint64_t, 3>> writtenBack;
std::uint64_t writtenBackSequence = 0;
// PendingLabel without the table mutex (the caller holds it, or the GPU mutex).
std::optional<std::uint64_t> lookupLabel(std::uint64_t address, std::size_t bytes, std::uint64_t afterStamp, std::uint32_t& queue) const;
Context context;
VkSemaphore timeline = VK_NULL_HANDLE;
// Identity for a thread that released the mutex around a wait (syncThroughUnlocked): the
// recorder may have been torn down meanwhile, so it is looked up by this id, not by pointer.
std::uint64_t id = 0;
// Mutated only under both GuestMemory::GpuMutex and the table mutex (Recorder.cpp), so a holder
// of either reads it consistently.
std::unordered_map<std::uint64_t, LabelEntry> labels;
std::unique_ptr<Batch> open;
std::deque<std::unique_ptr<Batch>> inFlight;
// Batches popped from inFlight whose completions have not run yet: their writes stay in the
// snapshot (a rebuild from inside a completion must not drop them) until finish returns.
std::vector<const Batch*> finishing;
std::uint64_t submissions = 0;
// Command buffers and fences of completed batches, reused by later ones (hundreds of batches per
// frame would otherwise allocate and free their objects each time).
std::vector<std::pair<VkCommandBuffer, VkFence>> spare;
};
}
#endif

View File

@ -26,13 +26,44 @@ private:
VkDeviceMemory memory = VK_NULL_HANDLE;
void* mapping = nullptr;
std::size_t size;
// The VkBuffer's own size, `size` rounded up to its pool class (see BufferPool::Capacity).
std::size_t capacity;
VkDeviceSize allocationBytes = 0;
VkBufferUsageFlags usage;
VkMemoryPropertyFlags properties;
bool reusable = false;
std::shared_ptr<BufferPool> cache;
};
// Device-local scratch memory for GPU-side layout conversion. The detiler reads and writes scattered
// elements, which crawls across PCIe, so guest bytes move between host and device buffers with DMA
// copies and are only swizzled in video memory.
class DeviceBuffer {
public:
DeviceBuffer(const Context& context, std::size_t size, VkBufferUsageFlags usage);
~DeviceBuffer();
DeviceBuffer(const DeviceBuffer&) = delete;
DeviceBuffer& operator=(const DeviceBuffer&) = delete;
VkBuffer Handle() const;
std::size_t Size() const;
private:
void release() noexcept;
Context context;
VkBuffer buffer = VK_NULL_HANDLE;
VkDeviceMemory memory = VK_NULL_HANDLE;
std::size_t size;
std::size_t capacity;
VkDeviceSize allocationBytes = 0;
VkBufferUsageFlags usage;
std::shared_ptr<BufferPool> cache;
};
// Records a whole-range buffer copy.
void CopyBuffer(const Context& context, VkCommandBuffer commands, VkBuffer source, VkDeviceSize sourceOffset, VkBuffer destination, VkDeviceSize destinationOffset, VkDeviceSize bytes);
// Records a global memory barrier.
void RecordMemoryBarrier(const Context& context, VkCommandBuffer commands, VkPipelineStageFlags sourceStage, VkPipelineStageFlags destinationStage, VkAccessFlags sourceAccess, VkAccessFlags destinationAccess);
class RenderTarget {
public:
RenderTarget(const Context& context, const ColorTarget& target, bool blending);
@ -60,8 +91,6 @@ public:
void SubmitAndWait();
void Submit();
void Wait();
bool IsComplete();
void Reset();
private:
void release() noexcept;

View File

@ -4,8 +4,11 @@
#include "prx/libSceAgcDriver/Graphics/include/Context.hpp"
#include "prx/libSceAgcDriver/Graphics/include/GuestSamplerResource.hpp"
#include <array>
#include <cstdint>
#include <map>
#include <memory>
#include <mutex>
#include <span>
namespace AgcDriver::Graphics {
@ -21,18 +24,36 @@ public:
private:
void release() noexcept;
VkDevice device;
PFN_vkDestroySampler destroySampler;
Context context;
VkSampler sampler = VK_NULL_HANDLE;
};
// One VkSampler per distinct S# (its 4 words plus the shader's depth-compare use): samplers are
// immutable and DecodeSamplerResource is a pure function of the words, so equal keys mean equal
// samplers and nothing ever invalidates an entry. Holders keep a shared_ptr, so an entry evicted from
// the LRU-capped cache lives on while a descriptor set in flight references it. One cache per device.
// APS5_NO_SAMPLER_CACHE=1 creates a sampler per binding as before.
class SamplerCache {
public:
std::shared_ptr<Sampler> Get(const Context& context, std::span<const std::uint32_t> words, const GuestSamplerResource& descriptor);
explicit SamplerCache(std::size_t capacity = 1024);
SamplerCache(const SamplerCache&) = delete;
SamplerCache& operator=(const SamplerCache&) = delete;
std::shared_ptr<Sampler> Get(const Context& context, std::span<const std::uint32_t> words, bool compareEnable);
// APS5_PROFILE_DRAW counters: lookups served by an existing sampler, and samplers created.
std::uint64_t Hits() const { return hits; }
std::uint64_t Misses() const { return misses; }
private:
struct Entry {
std::shared_ptr<Sampler> sampler;
std::uint64_t lastUse;
};
std::mutex mutex;
std::map<std::array<std::uint32_t, 5>, std::shared_ptr<Sampler>> entries;
std::map<std::array<std::uint32_t, 5>, Entry> entries;
std::uint64_t clock = 0;
std::size_t capacity;
std::uint64_t hits = 0;
std::uint64_t misses = 0;
};
}

View File

@ -5,36 +5,166 @@
#include "prx/libSceAgcDriver/Graphics/include/BdaResources.hpp"
#include "prx/libSceAgcDriver/Graphics/include/Texture.hpp"
#include "prx/libSceAgcDriver/Graphics/include/Sampler.hpp"
#include "prx/libSceAgcDriver/Graphics/include/DescriptorCache.hpp"
#include "Recompiler.hpp"
#include "prx/libSceAgcDriver/Graphics/include/Shaders.hpp"
#include <array>
#include <chrono>
#include <list>
#include <map>
#include <memory>
#include <mutex>
#include <string>
#include <unordered_map>
#include <vector>
namespace AgcDriver::Graphics {
class Recorder;
// The cached storage image of a surface (render targets use it as their resident image); brought up
// to date with guest memory before it is returned.
std::shared_ptr<StorageTexture> CachedStorageSurface(const Context& context, const GuestTextureResource& resource);
// Defined in Texture.cpp beside the pending-results registry, for the fast Revalidate below: whether
// a storage image other than `except` has results pending in [address, address + bytes).
bool PendingStorageOverlaps(std::uint64_t address, std::size_t bytes, const StorageTexture* except);
// Per-device descriptor objects shared by ShaderResources builds: set layouts by their binding list
// (immutable; kept until the device is torn down, which Vulkan allows even for pipeline layouts made
// from them) and a chain of descriptor pools that sets are freed back to, so a build creates and
// destroys no layout or pool of its own. A set lives exactly as long as its ShaderResources, which
// its batch keeps until the GPU completed. The cache locks itself: stage A of a dispatch build
// (ShaderResources::buildPrepare, from VulkanDevice::PrepareDispatch) takes layouts and sets from it
// without GuestMemory::GpuMutex, while other threads free and update sets of the same pools under
// that lock (Vulkan synchronizes the pool for allocate/free and the set for update, so that is
// legal). APS5_NO_LAYOUT_CACHE=1 / APS5_NO_POOL_CACHE=1 restore the per-build objects.
class DescriptorCache {
public:
explicit DescriptorCache(const Context& context);
~DescriptorCache();
DescriptorCache(const DescriptorCache&) = delete;
DescriptorCache& operator=(const DescriptorCache&) = delete;
// The layout for `key` (binding, type, count, stage flags per binding, as ShaderResources builds
// it from `bindings`), created on first use.
VkDescriptorSetLayout Layout(std::span<const std::uint32_t> key, std::span<const VkDescriptorSetLayoutBinding> bindings);
struct SetAllocation {
VkDescriptorSet set = VK_NULL_HANDLE;
VkDescriptorPool pool = VK_NULL_HANDLE;
};
// A set of `layout` needing `sizes` descriptors from the pool chain, opening a pool when no pool
// has room; a null set when the needs exceed what one chain pool holds (the caller then makes a
// dedicated pool, as before).
SetAllocation Allocate(VkDescriptorSetLayout layout, std::span<const VkDescriptorPoolSize> sizes);
void Free(const SetAllocation& allocation) noexcept;
// APS5_PROFILE_DRAW counters: layouts served from the map / created, sets allocated, pools opened.
struct Stats {
std::uint64_t layoutHits = 0;
std::uint64_t layoutMisses = 0;
std::uint64_t sets = 0;
std::uint64_t pools = 0;
};
Stats Counters() const;
private:
Context context;
PFN_vkDestroyDescriptorSetLayout destroyLayout;
PFN_vkDestroyDescriptorPool destroyPool;
PFN_vkFreeDescriptorSets freeSets;
mutable std::mutex mutex;
std::map<std::vector<std::uint32_t>, VkDescriptorSetLayout> layouts;
std::vector<VkDescriptorPool> pools;
Stats stats;
};
class ShaderResources {
public:
ShaderResources(const Context& context, const ShaderRecompiler::RecompileResult& vertex, const ShaderRecompiler::RecompileResult& fragment, const ColorTarget& target, std::uint64_t indexAddress, std::size_t indexBytes);
ShaderResources(const Context& context, std::span<const CompiledShader> shaders, const ColorTarget& target, std::uint64_t indexAddress, std::size_t indexBytes, std::span<const GuestMemorySnapshot> snapshots = {});
ShaderResources(const Context& context, const CompiledShader& compute, std::span<const GuestMemorySnapshot> snapshots = {});
// Two-stage build for dispatches (see build): with `deferred` the constructor runs stage A only,
// which needs no device lock, and Complete() runs stage B under GuestMemory::GpuMutex; until
// then no other member may be used. `compute` and `snapshots` must outlive Complete(). An
// address-based shader (BDA tables) builds entirely in Complete(): its lease acquisition
// reconciles imports and refreshes mirrors, which needs the lock.
ShaderResources(const Context& context, const CompiledShader& compute, std::span<const GuestMemorySnapshot> snapshots, bool deferred);
void Complete();
bool Completed() const { return completed; }
~ShaderResources();
ShaderResources(const ShaderResources&) = delete;
ShaderResources& operator=(const ShaderResources&) = delete;
VkDescriptorSetLayout Layout() const;
void Bind(VkCommandBuffer commands, VkPipelineBindPoint bindPoint, VkPipelineLayout layout) const;
void WriteBack();
// Deferred completion: MarkGpuWrites registers the results the recorded work leaves on the GPU
// (storage images stay there; buffer ranges are noted so CPU reads wait); WriteBackBuffers runs
// once the work completed.
void MarkGpuWrites(Recorder& recorder);
void WriteBackBuffers();
// Whether WriteBackBuffers has anything the CPU must see (copied written buffers, BDA faults).
bool NeedsCompletion() const { return bda != nullptr || guestMemory.HasCopiedWrites(); }
// Whether a written buffer was copied (its results reach guest memory by the CPU write-back).
bool HasCopiedWrites() const { return guestMemory.HasCopiedWrites(); }
bool HoldsLease() const { return guestMemory.HoldsLease(); }
bool WritesOverlap(std::uint64_t address, std::size_t bytes) const { return guestMemory.WritesOverlap(address, bytes); }
const std::vector<std::uint32_t>& LayoutKey() const { return layoutKey; }
// Debug aid: each bound guest resource with the fraction of sampled bytes that are nonzero.
std::string Describe() const;
// Reuse across dispatches of identical content (see the resource cache in VulkanDevice): the
// key is everything a compute build consumes from its compiled shader (variant, and per binding
// kind, role, slot, count, guest descriptor words, written images, depth-compare samplers); push
// constants are not part of it because every dispatch pushes its own. Reusable says whether this
// object can serve a later dispatch with the same key: nothing needs completion work and every
// guest buffer is read in place through a host import. Revalidate brings a reusable object up to
// date for another use, or says it cannot be reused any more (a buffer's import went away, or a
// texture or storage image the set references is no longer the one its memory maps to).
static std::vector<std::uint32_t> ContentKey(const CompiledShader& shader);
bool Reusable() const { return reusable; }
// `shaders` are the stages the object was built from, in build order (a recorded draw's vertex
// and fragment stages, or one compute stage): their bindings are walked like the build did.
bool Revalidate(std::span<const CompiledShader> shaders);
bool Revalidate(const CompiledShader& shader) { return Revalidate(std::span<const CompiledShader>(&shader, 1)); }
// APS5_PROFILE_DRAW: the build's sub-phase times in milliseconds (zero when not profiling), so
// the draw path can total them separately from dispatches. bindingsMs covers the binding plan
// (stage A) and the image lookups (stage B); prepareMs/completeMs are the two stages' totals.
struct BuildTiming {
double bindingsMs = 0;
double uploadMs = 0;
double descriptorsMs = 0;
double prepareMs = 0;
double completeMs = 0;
};
const BuildTiming& Timing() const { return timing; }
// The image surfaces (address, bytes) whose stage-B lookup reads guest memory on the CPU and so
// would wait, under the device lock, for recorded work writing them: a surface without a host
// import, or a sampled one that stays a CPU snapshot (block compressed, or the storage-view path
// is off). VulkanDevice::PrepareDispatch waits for that work before the lock (the pre-sync).
// Usable right after the deferred constructor, for address-based builds too (it decodes the
// compiled shader's bindings itself when stage A has not run), and on a completed object from
// the resource cache (its Revalidate repeats the same lookups), without the device lock.
std::vector<std::pair<std::uint64_t, std::uint64_t>> PresyncSurfaces() const;
private:
struct DescribedRange {
const char* kind;
std::uint64_t address;
std::uint64_t bytes;
std::uint32_t width = 0;
std::uint32_t height = 0;
std::uint32_t format = 0;
int tileMode = 0;
std::uint64_t dccAddress = 0;
};
std::vector<DescribedRange> describedRanges;
struct Allocation {
std::uint64_t address;
std::size_t size;
bool guest;
std::unique_ptr<Buffer> buffer;
ShaderRecompiler::DescriptorRole role = ShaderRecompiler::DescriptorRole::ShaderData;
// Guest buffers: whether the shader may store to the range (see addGuestBuffer).
bool written = true;
};
struct Binding {
@ -43,13 +173,92 @@ private:
std::vector<std::size_t> imageAllocations;
};
// A host-imported buffer region the set reads in place, with its import's identity at build time.
struct DirectRegion {
std::uint64_t begin;
std::uint64_t end;
std::uint64_t serial;
};
// The build is two stages. A (buildPrepare, no device lock): the binding plan with its layout
// entries, samplers, data buffers, the upload's prepare stage (imports already serving the
// regions, read-only copies), the set layout and the descriptor set. B (buildComplete, under
// GuestMemory::GpuMutex): the texture and storage image lookups (they refresh, upload and flush
// through the recorder), the rest of the upload, the BDA objects, the descriptor writes and the
// reusability record. build runs both.
void build(std::span<const CompiledShader> shaders, const ColorTarget* target, std::uint64_t indexAddress, std::size_t indexBytes);
std::size_t addGuestBuffer(std::span<const std::uint32_t> words, const ColorTarget* target, std::uint64_t indexAddress, std::size_t indexBytes);
void buildPrepare(std::span<const CompiledShader> shaders, const ColorTarget* target, std::uint64_t indexAddress, std::size_t indexBytes);
void buildComplete();
// APS5_PROFILE_DRAW: closes the current sub-phase of the build into the [resources] totals.
double phase(const char* name);
// `written` is the element's DescriptorBinding::bufferWritten: a read-only element binds the
// same way but is left out of the write set (no write-back, no pending-write note).
std::size_t addGuestBuffer(std::span<const std::uint32_t> words, const ColorTarget* target, std::uint64_t indexAddress, std::size_t indexBytes, bool written);
std::size_t addDataBuffer(std::span<const std::uint32_t> words);
void addImageBinding(const ShaderRecompiler::DescriptorBinding& binding, VkShaderStageFlags flags, std::vector<Binding>& bindings);
// Stage A: the layout entry of an image binding (samplers are taken at once, the sampler cache
// locks itself); stage B looks the sampled textures and storage images up (resolveImageBinding).
void addImageBinding(const ShaderRecompiler::DescriptorBinding& binding, VkShaderStageFlags flags);
// Stage A: one record per planned image element (ImageRecord), in plan order: the decoded
// descriptor and its surface size (computed once for the build), the write watch walked so stage
// B's collects are memo hits (APS5_NO_PRECOLLECT=1 skips the pass entirely), and for a sampled
// element the cache entry it maps to, taken now so stage B needs only the fastRevalidate
// predicate under the lock (APS5_NO_STAGE_A_IMAGES=1 leaves every element to the full lookup).
// Whether it ran.
bool precollectImages();
struct ImageRecord {
bool sampled = false;
// The descriptor decoded and its surface described; false leaves the element to stage B's
// own decode (which reports the error).
bool decoded = false;
std::array<std::uint32_t, 8> words{};
GuestTextureResource resource{};
std::uint64_t guestBytes = 0;
VkComponentMapping components{};
// Sampled elements: the keys read in stage A and the collect made BEFORE stage B's checks
// (the generation the entry moves to when they pass).
DccKeys keys = DccKeys::Uncompressed;
std::uint64_t generation = 0;
// The cache entry's object, storage source, keys and generation as of stage A (texture null:
// no entry, the full lookup makes one).
std::shared_ptr<Texture> texture;
std::shared_ptr<StorageTexture> source;
DccKeys entryKeys = DccKeys::Uncompressed;
std::uint64_t entryGeneration = 0;
};
std::vector<ImageRecord> imageRecords;
// The next record resolveImageBinding consumes (records follow the deferredImages order).
std::size_t nextImageRecord = 0;
// Stage B: the record's texture when the fastRevalidate predicate proves it current under the
// lock and the cache still holds it; null sends the element to cachedTexture.
std::shared_ptr<Texture> fastTexture(const ImageRecord& record);
void resolveImageBinding(const ShaderRecompiler::DescriptorBinding& binding, Binding& item);
void forgetDeferredInputs();
void release() noexcept;
void prepareAddressBindings(std::span<const CompiledShader> shaders, std::span<const GuestMemorySnapshot> snapshots);
VkDescriptorBufferInfo descriptor(const Allocation& allocation) const;
void noteReusable();
void reportDescriptorCaches() const;
// What a sampled texture was proved current against when the build (or the last full Revalidate)
// looked it up, so the next Revalidate can repeat the proof from write stamps and the DCC keys
// alone instead of repeating the lookup (see fastRevalidate). One record per textures[] entry; an
// element without a record always takes the full lookup. Storage images need no record: their
// proof comes from the image itself (its own surface and generation).
struct ValidatedSurface {
GuestTextureResource resource{};
std::uint64_t bytes = 0;
// Keys read at the lookup (a snapshot texture holds its clear texels while they are unchanged).
DccKeys keys = DccKeys::Uncompressed;
// Snapshot textures: write generation the content was known current at, always from a
// collect issued before the checks that proved it.
std::uint64_t generation = 0;
// The collect of the check in progress, moved into `generation` once every element passed.
std::uint64_t collected = 0;
// Storage-sourced textures: the image the view follows (kept alive by the texture).
const StorageTexture* source = nullptr;
bool valid = false;
};
void captureValidation();
bool fastRevalidate();
Context context;
std::vector<std::uint32_t> layoutKey;
GuestBufferMemory guestMemory;
@ -57,13 +266,93 @@ private:
bool usesBda = false;
bool usesFaultBuffer = false;
VkDescriptorSetLayout _layout = VK_NULL_HANDLE;
// Whether the layout is this object's own (no cache) and destroyed with it.
bool ownsLayout = false;
VkDescriptorSet _set = VK_NULL_HANDLE;
std::unique_ptr<DescriptorAllocation> descriptors;
// Dedicated pool of this object's set (no cache, or a set too large for a cache pool).
VkDescriptorPool pool = VK_NULL_HANDLE;
// The cache pool the set was allocated from, freed back to it on release.
VkDescriptorPool cachePool = VK_NULL_HANDLE;
std::vector<Allocation> allocations;
// Guest buffer elements bound read-only: each use of this object skips that many pending-write
// notes (counted in MarkGpuWrites for the [buffers] line).
std::size_t readOnlyBuffers = 0;
std::vector<std::shared_ptr<Texture>> textures;
std::vector<std::shared_ptr<StorageTexture>> storageTextures;
std::vector<std::uint32_t> storageMips;
std::vector<bool> storageWritten;
std::vector<std::shared_ptr<Sampler>> samplers;
bool reusable = false;
std::vector<DirectRegion> directRegions;
std::vector<ValidatedSurface> validatedTextures;
BuildTiming timing;
// Build state carried from stage A to stage B: the bindings in plan order, the image bindings
// still to look up (index into `bindings`; the DescriptorBinding lives in the compiled shader),
// the descriptor counts the set was sized for, and the compute stage of a deferred build.
std::vector<Binding> bindings;
struct DeferredImages {
const ShaderRecompiler::DescriptorBinding* binding;
std::size_t index;
};
std::vector<DeferredImages> deferredImages;
std::uint64_t storageBuffers = 0;
std::uint32_t plannedSampledImages = 0;
std::uint32_t plannedStorageImages = 0;
// The compute constructor's shader and captured regions: the caller's objects, valid only until
// the build (Complete() for a deferred one) is done, and reset then (forgetDeferredInputs).
CompiledShader deferredCompute{};
std::span<const GuestMemorySnapshot> deferredSnapshots;
// The deferred build is address-based: everything runs in Complete().
bool lockedBuild = false;
// Stage A runs without the device lock (a deferred, non-address-based build).
bool unlockedPrepare = false;
bool completed = false;
std::chrono::steady_clock::time_point phaseStart;
};
// Built ShaderResources by the content the build consumed (see ShaderResources::ContentKey), so a
// dispatch or recorded draw repeating an earlier one binds the earlier descriptor set instead of
// building one (import lookups, sampler and descriptor objects, guest buffer uploads). Only reusable
// objects (nothing to complete, every buffer read in place) are kept, and an entry serves a later
// use only after ShaderResources::Revalidate brought it up to date. Entries hold their object alive;
// the recorder keeps it as well while its batch runs. One LRU bound covers dispatch and draw entries
// together (APS5_RESOURCE_CACHE_ENTRIES, default 1024); the map has its own mutex and locks itself:
// VulkanDevice::PrepareDispatch probes it without GuestMemory::GpuMutex (a hit there is revalidated
// under the lock by the dispatch; inserts and Revalidate stay under it). Compute keys start with the
// stage, draw keys with a marker word, so the two never meet, and both name the device handle: the driver replaces
// the headless device with the windowed one while workers may still hold the old one, so a replaced
// device's entries (whose descriptor sets and pooled buffers belong to it) are unreachable by key
// and dropped by that device's Clear(). APS5_NO_RESOURCE_CACHE=1 (dispatches) and
// APS5_NO_DRAW_RESOURCE_CACHE=1 (draws) build every object as before; the device clears the cache at
// teardown before its descriptor caches go.
class ResourceCache {
public:
using Key = std::vector<std::uint32_t>;
std::shared_ptr<ShaderResources> Find(const Key& key);
void Insert(const Key& key, std::shared_ptr<ShaderResources> resources);
void Remove(const Key& key);
void Clear();
std::size_t Size() const;
private:
void erase(const Key& key);
void noteMiss(const Key& key);
struct KeyHash {
std::size_t operator()(const Key& key) const noexcept {
std::uint64_t hash = 14695981039346656037ull;
for (const auto word : key) hash = (hash ^ word) * 1099511628211ull;
return static_cast<std::size_t>(hash);
}
};
mutable std::mutex mutex;
std::list<std::pair<Key, std::shared_ptr<ShaderResources>>> entries;
std::unordered_map<Key, decltype(entries)::iterator, KeyHash> index;
};
// The process-wide cache the device and the draw path share (keys name the device; a device clears
// it when it goes). Never destroyed: entries belong to the device, not to static teardown.
ResourceCache& SharedResourceCache();
}
#endif

View File

@ -4,6 +4,7 @@
#include "prx/libSceAgcDriver/Graphics/include/Context.hpp"
#include "prx/libSceAgcDriver/Graphics/include/ColorTargetLayout.hpp"
#include "prx/libSceAgcDriver/Execution/include/QueueState.hpp"
#include <vector>
#include <array>
#include <cstddef>
#include <cstdint>
@ -34,17 +35,25 @@ struct ColorTarget {
std::size_t bytes;
std::uint8_t componentMapping;
ColorTileMode tileMode = ColorTileMode::Linear;
std::uint32_t elementBytes = 4;
// DCC metadata of a compressed target (CB_COLOR_INFO DCC_ENABLE), or 0 (see DccMetadata.hpp).
std::uint64_t dccAddress = 0;
bool dccAlphaOnMsb = false;
};
struct State {
ShaderStages stages;
// MRT slot 0; `colors`/`blends` hold every written slot, attachment i being slot i.
ColorTarget color;
std::vector<ColorTarget> colors;
std::vector<VkPipelineColorBlendAttachmentState> blends;
bool hasColorTarget;
bool rectList = false;
VkExtent2D renderExtent;
VkPrimitiveTopology topology;
VkViewport viewport;
bool negativeOneToOne;
bool depthClamp = false;
VkRect2D scissor;
VkCullModeFlags cullMode;
VkFrontFace frontFace;

View File

@ -2,35 +2,174 @@
#define CORE_LIBS_PRX_LIBSCEAGCDRIVER_GRAPHICS_INCLUDE_TEXTURE_HPP
#include "prx/libSceAgcDriver/Graphics/include/Context.hpp"
#include "prx/libSceAgcDriver/Graphics/include/DccMetadata.hpp"
#include "prx/libSceAgcDriver/Graphics/include/GuestTextureResource.hpp"
#include "prx/libSceAgcDriver/Graphics/include/TextureDetiler.hpp"
#include "prx/libSceAgcDriver/Graphics/include/TextureTiling.hpp"
#include <cstddef>
#include <map>
#include <memory>
#include <vector>
namespace AgcDriver::Graphics {
class ResidentColor;
class CommandBatch;
class StorageTexture;
// The Vulkan format storage images of a guest format use (sRGB formats store as their UNORM form).
// Throws when the format has no storage form; the query is cached per format (a lookup cost a
// vkGetPhysicalDeviceFormatProperties call before).
VkFormat StorageFormatForGuest(const Context& context, std::uint32_t guestFormat);
// Whether StorageFormatForGuest would succeed, without throwing (unknown guest formats included).
bool StorageFormatAvailable(const Context& context, std::uint32_t guestFormat);
// A sampled texture's own VkImage with its memory, shared with the recorder while a recorded upload
// still writes it (see the snapshot constructor), so the texture may go before the batch completes.
struct OwnedImage {
OwnedImage(const Context& context, VkImage image, VkDeviceMemory memory) : context(context), image(image), memory(memory) {}
OwnedImage(const OwnedImage&) = delete;
OwnedImage& operator=(const OwnedImage&) = delete;
~OwnedImage() {
if (image) context.Function<PFN_vkDestroyImage>("vkDestroyImage")(context.device, image, nullptr);
if (memory) context.Function<PFN_vkFreeMemory>("vkFreeMemory")(context.device, memory, nullptr);
}
Context context;
VkImage image;
VkDeviceMemory memory;
};
class Texture {
public:
Texture(const Context& context, TextureDetiler& detiler, const GuestTextureResource& descriptor, VkComponentMapping components, std::span<const std::byte> snapshot);
Texture(const Context& context, const std::shared_ptr<ResidentColor>& source, const GuestTextureResource& descriptor, VkComponentMapping components);
// A view of a storage image's own VkImage: the sampled texture follows the image's content, so a
// compute pass writing it and the next pass sampling it share one image and copy nothing.
// CanCopyFrom says whether the two descriptors address the same surface compatibly.
Texture(const Context& context, const std::shared_ptr<StorageTexture>& source, const GuestTextureResource& descriptor, VkComponentMapping components);
static bool CanCopyFrom(const StorageTexture& source, const GuestTextureResource& descriptor);
~Texture();
Texture(const Texture&) = delete;
Texture& operator=(const Texture&) = delete;
VkImageView View() const;
// The layout the image is kept in while sampled.
VkImageLayout Layout() const { return layout; }
VkDeviceSize AllocationBytes() const { return allocationBytes; }
// Whether this texture is a view of a storage image (no snapshot of its own).
bool ViewsStorageImage() const { return storageSource != nullptr; }
private:
void release() noexcept;
// Held by value: cached textures outlive the Context of the draw that created them.
Context context;
// The snapshot constructor's image (null for a view of a storage image); `owned` frees it.
VkImage image = VK_NULL_HANDLE;
std::shared_ptr<OwnedImage> owned;
VkImageView view = VK_NULL_HANDLE;
VkImageLayout layout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
VkDeviceSize allocationBytes = 0;
std::shared_ptr<ResidentColor> source;
std::shared_ptr<StorageTexture> storageSource;
std::unique_ptr<CommandBatch> upload;
};
// A guest texture a shader writes through a storage image. It is uploaded like a sampled texture;
// after the GPU work completes its results are stored to guest memory (retiled, changed bytes only),
// either at once (WriteBack) or deferred: MarkDirty keeps them on the GPU until something reads that
// memory (FlushPending, through the GuestMemory flush hook), the image is refreshed after a CPU write,
// or it leaves the cache. Dispatches reusing the image meanwhile skip the round trip entirely.
class StorageTexture : public std::enable_shared_from_this<StorageTexture> {
public:
StorageTexture(const Context& context, TextureDetiler& detiler, const GuestTextureResource& descriptor, std::uint32_t mipLevel);
~StorageTexture();
StorageTexture(const StorageTexture&) = delete;
StorageTexture& operator=(const StorageTexture&) = delete;
VkImageView View() const;
// The image holds every mip of the surface; one storage view per written mip is made on demand,
// so successive mip writes of a chain share one image and one write-back.
VkImageView View(std::uint32_t mip);
// 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);
void WriteBack();
// Deferred write-back (APS5_EAGER_WRITEBACK=1 stores at once instead).
void MarkDirty();
void Flush();
// Stores every pending image overlapping the range, except `except`; returns whether any was.
// Stores into host-imported memory are recorded (not waited for): a CPU reader syncs afterwards.
static bool FlushPending(std::uint64_t address, std::size_t bytes, const StorageTexture* except = nullptr, const char* reason = "memory access");
static void FlushAllPending(const char* reason);
// The pending image whose surface starts at `address` and covers at least `bytes` (a mip chain
// contains a descriptor of its first mips), if any; the caller decides whether the geometry fits
// (Texture::CanCopyFrom, ResidentPresentable).
static std::shared_ptr<StorageTexture> FindPending(std::uint64_t address, std::uint64_t bytes);
VkImage Image() const { return image; }
const GuestTextureResource& Descriptor() const { return descriptor; }
std::uint32_t ImageLayers() const { return geometry.imageLayers; }
std::uint32_t ImageDepth() const { return geometry.imageDepth; }
// Content version: advances when the image is re-uploaded or a shader wrote it. Together with
// Generation (the write generation guest memory was last known to match the content at) it
// validates textures copied from this image.
std::uint64_t Version() const { return version; }
std::uint64_t Generation() const { return generation; }
// Brings the image up to date with guest memory before another use; returns whether its content
// was still current (nothing uploaded).
// Keeps the image current with guest memory (see GuestMemory::CollectWrites).
bool Refresh();
std::uint64_t GuestBytes() const;
private:
std::vector<VkBufferImageCopy> CopyRegions() const;
void upload();
// Stores the image to guest memory; with `skipWrittenSince` set, 64 KiB blocks the CPU wrote since
// that write generation keep the CPU's bytes.
void writeBack(std::uint64_t skipWrittenSince);
// Pending images whose surface shares a 64 KiB write-stamp block with this one without
// overlapping it, and whose memory is unchanged since their generation (see writeBack), with
// the generation seen at the check.
struct Adjacent {
std::shared_ptr<StorageTexture> texture;
std::uint64_t generation;
};
std::vector<Adjacent> adjacentPendingUnchanged() const;
// Advances the entries still at their seen generation to `now`, the generation this write-back's
// stamps of blocks firstBlock..lastBlock (64 KiB) are below, unless the neighbour's other blocks
// were stamped since the check.
static void advanceAdjacent(const std::vector<Adjacent>& adjacent, std::uint64_t now, std::uint64_t firstBlock, std::uint64_t lastBlock);
bool overlaps(std::uint64_t address, std::size_t bytes) const;
VkImageView createView(std::uint32_t mip) const;
void release() noexcept;
Context context;
TextureDetiler& detiler;
GuestTextureResource descriptor;
std::vector<TileMipLayout> mips;
std::uint32_t arrayLayers = 1;
std::uint64_t guestBytes = 0;
std::uint64_t sliceLinearBytes = 0;
SurfaceGeometry geometry;
std::vector<std::byte> original;
// DCC keys the image content was uploaded under: a fast-cleared surface starts as its clear value.
DccKeys uploadedKeys = DccKeys::Uncompressed;
// Write generation `original` is known current at.
std::uint64_t generation = 0;
VkImage image = VK_NULL_HANDLE;
VkDeviceMemory memory = VK_NULL_HANDLE;
VkImageView view = VK_NULL_HANDLE;
VkDeviceSize allocationBytes = 0;
std::shared_ptr<ResidentColor> source;
std::unique_ptr<CommandBatch> upload;
std::uint32_t defaultMip = 0;
std::map<std::uint32_t, VkImageView> extraViews;
bool attachable = false;
std::map<VkFormat, VkImageView> attachmentViews;
VkFormat storageFormat = VK_FORMAT_UNDEFINED;
// Results are on the GPU only (guarded by the pending-write registry lock).
bool dirty = false;
std::uint64_t version = 0;
// `original` holds the guest bytes; false after a GPU-side clear, which never read them.
bool originalValid = true;
};
}

View File

@ -1,6 +1,7 @@
#ifndef CORE_LIBS_PRX_LIBSCEAGCDRIVER_GRAPHICS_INCLUDE_TEXTUREDETILER_HPP
#define CORE_LIBS_PRX_LIBSCEAGCDRIVER_GRAPHICS_INCLUDE_TEXTUREDETILER_HPP
#include <array>
#include "prx/libSceAgcDriver/Graphics/include/Context.hpp"
#include "prx/libSceAgcDriver/Graphics/include/GuestTextureResource.hpp"
#include "prx/libSceAgcDriver/Graphics/include/TextureTiling.hpp"
@ -17,11 +18,14 @@ namespace AgcDriver::Graphics {
TextureDetiler(const TextureDetiler&) = delete;
TextureDetiler& operator=(const TextureDetiler&) = delete;
void Dispatch(VkCommandBuffer commands, TextureTileMode tileMode, std::uint32_t elementBytes, VkBuffer source, std::uint64_t sourceOffset, VkBuffer destination, std::uint64_t destinationOffset, const TileMipLayout& layout, std::uint32_t arrayLayer);
// Detiles `source` (tiled) into `destination` (linear), or with `retile` writes linear `source`
// into tiled `destination`; offsets always refer to the respective buffers.
void Dispatch(VkCommandBuffer commands, TextureTileMode tileMode, std::uint32_t elementBytes, VkBuffer source, std::uint64_t sourceOffset, VkBuffer destination, std::uint64_t destinationOffset, const TileMipLayout& layout, bool retile = false, std::uint32_t slice = 0, bool thick = false);
// Recycles the descriptor sets of the previous batch; call before recording a new command batch.
void BeginBatch();
private:
VkPipeline pipeline(TextureTileMode tileMode, std::uint32_t elementBytes);
VkPipeline pipeline(TextureTileMode tileMode, std::uint32_t elementBytes, bool retile, bool thick);
void release() noexcept;
VkDescriptorSet allocateSet();

View File

@ -0,0 +1,63 @@
#ifndef CORE_LIBS_PRX_LIBSCEAGCDRIVER_GRAPHICS_INCLUDE_TEXTURESWIZZLEEQUATIONS_HPP
#define CORE_LIBS_PRX_LIBSCEAGCDRIVER_GRAPHICS_INCLUDE_TEXTURESWIZZLEEQUATIONS_HPP
#include <array>
#include <cstdint>
namespace AgcDriver::Graphics {
// Address equations of the 64 KiB XOR swizzle modes (SW_64KB_Z_X, S_X, D_X, R_X) for the PS5 GPU, a
// Navi1x-class part with 16 pipes. Generated from AMD addrlib's GFX10 non-RB+ swizzle pattern tables
// (the 32 bpp R_X entry reproduces the scanout layout in ColorTargetLayout).
// Each entry gives, for byte-address bit 0..15 inside a block, the element-coordinate bits XORed into it:
// bits 0-11 select x bits, bits 12-23 select y bits and bits 24-31 select slice (or depth) bits.
struct TextureSwizzleEquation {
std::uint32_t swizzleMode;
std::uint32_t elementBytes;
std::array<std::uint32_t, 16> bits;
};
inline constexpr TextureSwizzleEquation kTextureSwizzleEquations[] = {
{24u, 1u, {0x00000001u, 0x00001000u, 0x00000002u, 0x00002000u, 0x00000004u, 0x00004000u, 0x00000008u, 0x00010000u, 0x08008008u, 0x04010010u, 0x02020040u, 0x01040020u, 0x00040000u, 0x00000040u, 0x00080000u, 0x00000080u}},
{24u, 2u, {0x00000000u, 0x00000001u, 0x00001000u, 0x00000002u, 0x00002000u, 0x00000004u, 0x00004000u, 0x00000008u, 0x08008008u, 0x04010010u, 0x02020040u, 0x01040020u, 0x00010000u, 0x00000040u, 0x00040000u, 0x00000080u}},
{24u, 4u, {0x00000000u, 0x00000000u, 0x00000001u, 0x00001000u, 0x00000002u, 0x00002000u, 0x00000004u, 0x00004000u, 0x08008008u, 0x04010010u, 0x02020040u, 0x01040020u, 0x00008000u, 0x00000010u, 0x00040000u, 0x00000040u}},
{24u, 8u, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000001u, 0x00001000u, 0x00000002u, 0x00002000u, 0x00000004u, 0x08008008u, 0x04010010u, 0x02020040u, 0x01040020u, 0x00004000u, 0x00000008u, 0x00010000u, 0x00000040u}},
{24u, 16u, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000001u, 0x00001000u, 0x00000002u, 0x00002000u, 0x08008008u, 0x04010010u, 0x02020040u, 0x01040020u, 0x00004000u, 0x00000004u, 0x00008000u, 0x00000010u}},
{25u, 1u, {0x00000001u, 0x00000002u, 0x00000004u, 0x00000008u, 0x00001000u, 0x00002000u, 0x00004000u, 0x00008000u, 0x08010080u, 0x04080010u, 0x02020040u, 0x01040020u, 0x00040000u, 0x00000040u, 0x00080000u, 0x00000080u}},
{25u, 2u, {0x00000000u, 0x00000001u, 0x00000002u, 0x00000004u, 0x00001000u, 0x00002000u, 0x00004000u, 0x00000008u, 0x08008080u, 0x04040010u, 0x02010040u, 0x01020020u, 0x00020000u, 0x00000040u, 0x00040000u, 0x00000080u}},
{25u, 4u, {0x00000000u, 0x00000000u, 0x00000001u, 0x00000002u, 0x00001000u, 0x00002000u, 0x00004000u, 0x00000004u, 0x08008040u, 0x04040008u, 0x02010020u, 0x01020010u, 0x00020000u, 0x00000020u, 0x00040000u, 0x00000040u}},
{25u, 8u, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000001u, 0x00001000u, 0x00002000u, 0x00000002u, 0x00000004u, 0x08004040u, 0x04020008u, 0x02008020u, 0x01010010u, 0x00010000u, 0x00000020u, 0x00020000u, 0x00000040u}},
{25u, 16u, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00001000u, 0x00002000u, 0x00000001u, 0x00000002u, 0x08004020u, 0x04020004u, 0x02008010u, 0x01010008u, 0x00010000u, 0x00000010u, 0x00020000u, 0x00000020u}},
{26u, 1u, {0x00000001u, 0x00000002u, 0x00000004u, 0x00002000u, 0x00001000u, 0x00004000u, 0x00000008u, 0x00008000u, 0x08010080u, 0x04080010u, 0x02020040u, 0x01040020u, 0x00040000u, 0x00000040u, 0x00080000u, 0x00000080u}},
{26u, 2u, {0x00000000u, 0x00000001u, 0x00000002u, 0x00000004u, 0x00001000u, 0x00002000u, 0x00004000u, 0x00000008u, 0x08008080u, 0x04040010u, 0x02010040u, 0x01020020u, 0x00020000u, 0x00000040u, 0x00040000u, 0x00000080u}},
{26u, 4u, {0x00000000u, 0x00000000u, 0x00000001u, 0x00000002u, 0x00001000u, 0x00002000u, 0x00004000u, 0x00000004u, 0x08008040u, 0x04040008u, 0x02010020u, 0x01020010u, 0x00020000u, 0x00000020u, 0x00040000u, 0x00000040u}},
{26u, 8u, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000001u, 0x00001000u, 0x00000002u, 0x00000004u, 0x00002000u, 0x08004040u, 0x04020008u, 0x02008020u, 0x01010010u, 0x00010000u, 0x00000020u, 0x00020000u, 0x00000040u}},
{26u, 16u, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000001u, 0x00001000u, 0x00000002u, 0x00002000u, 0x08004020u, 0x04020004u, 0x02008010u, 0x01010008u, 0x00010000u, 0x00000010u, 0x00020000u, 0x00000020u}},
{27u, 1u, {0x00000001u, 0x00000002u, 0x00000004u, 0x00002000u, 0x00001000u, 0x00004000u, 0x00000008u, 0x00010000u, 0x08008008u, 0x04010010u, 0x02020040u, 0x01040020u, 0x00040000u, 0x00000040u, 0x00080000u, 0x00000080u}},
{27u, 2u, {0x00000000u, 0x00000001u, 0x00000002u, 0x00000004u, 0x00001000u, 0x00002000u, 0x00004000u, 0x00000008u, 0x08008008u, 0x04010010u, 0x02020040u, 0x01040020u, 0x00010000u, 0x00000040u, 0x00040000u, 0x00000080u}},
{27u, 4u, {0x00000000u, 0x00000000u, 0x00000001u, 0x00000002u, 0x00001000u, 0x00002000u, 0x00004000u, 0x00000004u, 0x08008008u, 0x04010010u, 0x02020040u, 0x01040020u, 0x00008000u, 0x00000010u, 0x00040000u, 0x00000040u}},
{27u, 8u, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000001u, 0x00001000u, 0x00000002u, 0x00000004u, 0x00002000u, 0x08008008u, 0x04010010u, 0x02020040u, 0x01040020u, 0x00004000u, 0x00000008u, 0x00010000u, 0x00000040u}},
{27u, 16u, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000001u, 0x00001000u, 0x00000002u, 0x00002000u, 0x08008008u, 0x04010010u, 0x02020040u, 0x01040020u, 0x00004000u, 0x00000004u, 0x00008000u, 0x00000010u}},
// Thick (3D) standard swizzles SW_4KB_S and SW_64KB_S use the S3 patterns, keyed 0x100 | SW_MODE.
{0x105u, 1u, {0x00000001u, 0x00000002u, 0x01000000u, 0x00001000u, 0x02000000u, 0x00002000u, 0x00000004u, 0x04000000u, 0x00004000u, 0x00000008u, 0x08000000u, 0x00008000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}},
{0x105u, 2u, {0x00000000u, 0x00000001u, 0x01000000u, 0x00001000u, 0x02000000u, 0x00002000u, 0x00000002u, 0x04000000u, 0x00004000u, 0x00000004u, 0x08000000u, 0x00008000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}},
{0x105u, 4u, {0x00000000u, 0x00000000u, 0x00000001u, 0x00001000u, 0x01000000u, 0x00002000u, 0x00000002u, 0x02000000u, 0x00004000u, 0x00000004u, 0x04000000u, 0x00008000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}},
{0x105u, 8u, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000001u, 0x01000000u, 0x00001000u, 0x00000002u, 0x02000000u, 0x00002000u, 0x00000004u, 0x04000000u, 0x00004000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}},
{0x105u, 16u, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x01000000u, 0x00001000u, 0x00000001u, 0x02000000u, 0x00002000u, 0x00000002u, 0x04000000u, 0x00004000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}},
{0x109u, 1u, {0x00000001u, 0x00000002u, 0x01000000u, 0x00001000u, 0x02000000u, 0x00002000u, 0x00000004u, 0x04000000u, 0x00004000u, 0x00000008u, 0x08000000u, 0x00008000u, 0x00000010u, 0x10000000u, 0x00010000u, 0x00000020u}},
{0x109u, 2u, {0x00000000u, 0x00000001u, 0x01000000u, 0x00001000u, 0x02000000u, 0x00002000u, 0x00000002u, 0x04000000u, 0x00004000u, 0x00000004u, 0x08000000u, 0x00008000u, 0x00000008u, 0x10000000u, 0x00010000u, 0x00000010u}},
{0x109u, 4u, {0x00000000u, 0x00000000u, 0x00000001u, 0x00001000u, 0x01000000u, 0x00002000u, 0x00000002u, 0x02000000u, 0x00004000u, 0x00000004u, 0x04000000u, 0x00008000u, 0x00000008u, 0x08000000u, 0x00010000u, 0x00000010u}},
{0x109u, 8u, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000001u, 0x01000000u, 0x00001000u, 0x00000002u, 0x02000000u, 0x00002000u, 0x00000004u, 0x04000000u, 0x00004000u, 0x00000008u, 0x08000000u, 0x00008000u, 0x00000010u}},
{0x109u, 16u, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x01000000u, 0x00001000u, 0x00000001u, 0x02000000u, 0x00002000u, 0x00000002u, 0x04000000u, 0x00004000u, 0x00000004u, 0x08000000u, 0x00008000u, 0x00000008u}},
};
inline const TextureSwizzleEquation* FindTextureSwizzleEquation(std::uint32_t swizzleMode, std::uint32_t elementBytes) {
for (const auto& equation : kTextureSwizzleEquations) {
if (equation.swizzleMode == swizzleMode && equation.elementBytes == elementBytes) return &equation;
}
return nullptr;
}
}
#endif

View File

@ -2,6 +2,7 @@
#define CORE_LIBS_PRX_LIBSCEAGCDRIVER_GRAPHICS_INCLUDE_TEXTURETILING_HPP
#include "prx/libSceAgcDriver/Graphics/include/GuestTextureResource.hpp"
#include <array>
#include <cstdint>
#include <vector>
@ -23,6 +24,43 @@ struct TileMipLayout {
std::vector<TileMipLayout> ComputeMipLayout(TextureTileMode tileMode, std::uint32_t format, std::uint32_t width, std::uint32_t height, std::uint32_t mipCount);
std::uint64_t ComputeSurfaceSize(const std::vector<TileMipLayout>& mips, std::uint32_t arrayLayers);
// Mip chain of an uncompressed surface given its element size (color targets have no texture format).
std::vector<TileMipLayout> ComputeElementMipLayout(TextureTileMode tileMode, std::uint32_t bytesPerElement, std::uint32_t width, std::uint32_t height, std::uint32_t mipCount);
// Thick (3D) SW_4KB_S / SW_64KB_S blocks span several depth slices: {width, height, depth} in elements.
std::array<std::uint32_t, 3> ThickBlockExtent(TextureTileMode tileMode, std::uint32_t bytesPerElement);
// Single-mip 3D surface. Each depth slice z is detiled from slab z / blockDepth (slabBytes apart) with the
// swizzle's slice input set to z, into linear slices sliceLinearBytes apart.
struct ThickLayout {
TileMipLayout mip;
std::uint32_t depth;
std::uint32_t blockDepth;
std::uint64_t slabBytes;
std::uint64_t sliceLinearBytes;
std::uint64_t guestBytes;
};
ThickLayout ComputeThickLayout(TextureTileMode tileMode, std::uint32_t format, std::uint32_t width, std::uint32_t height, std::uint32_t depth);
// Guest surface as the texture upload/write-back loops walk it: array layers of thin surfaces, or the
// depth slices of a volume (which Vulkan holds as one layer with extent depth).
struct SurfaceGeometry {
std::vector<TileMipLayout> mips;
std::uint32_t layers = 1;
std::uint32_t imageLayers = 1;
std::uint32_t imageDepth = 1;
std::uint64_t guestBytes = 0;
std::uint64_t sliceLinearBytes = 0;
bool thick = false;
std::uint32_t blockDepth = 1;
std::uint64_t layerBytes = 0;
std::uint64_t GuestLayerOffset(std::uint32_t layer) const { return thick ? static_cast<std::uint64_t>(layer / blockDepth) * layerBytes : static_cast<std::uint64_t>(layer) * layerBytes; }
std::uint64_t LinearLayerOffset(std::uint32_t layer) const { return static_cast<std::uint64_t>(layer) * sliceLinearBytes; }
std::uint32_t CopyLayer(std::uint32_t layer) const { return imageDepth > 1 ? 0u : layer; }
std::int32_t CopyDepth(std::uint32_t layer) const { return imageDepth > 1 ? static_cast<std::int32_t>(layer) : 0; }
};
SurfaceGeometry DescribeSurface(const GuestTextureResource& descriptor);
}

View File

@ -22,6 +22,8 @@ void main() {
tiledIndex = ((y / 128u) * parameters.blocksPerRow + x / 128u) * 16384u + offset / 4u;
}
uint pixel = (parameters.mode & 1u) != 0u ? linear[linearIndex] : tiled[tiledIndex];
// Mode 8: A2B10G10R10 source (red in the low bits) presented as 8-bit RGBA.
if ((parameters.mode & 8u) != 0u) pixel = ((pixel >> 2u) & 0xffu) | (((pixel >> 12u) & 0xffu) << 8u) | (((pixel >> 22u) & 0xffu) << 16u) | 0xff000000u;
if ((parameters.mode & 2u) != 0u) pixel = (pixel & 0xff00ff00u) | ((pixel & 0xffu) << 16u) | ((pixel >> 16u) & 0xffu);
if ((parameters.mode & 1u) != 0u) tiled[tiledIndex] = pixel;
else linear[linearIndex] = pixel;

View File

@ -3,6 +3,29 @@
layout(constant_id = 0) const uint ELEMENT_BYTES = 4u;
layout(constant_id = 1) const uint BLOCK_BYTES = 256u;
layout(constant_id = 2) const uint TILE_FAMILY = 0u;
// 0: tiled input -> linear output (detile); 1: linear input -> tiled output (retile).
layout(constant_id = 3) const uint RETILE = 0u;
// TILE_FAMILY 2: XOR swizzle given per byte-address bit as masks of x (bits 0-11), y (12-23) and
// slice (24-31) element-coordinate bits.
layout(constant_id = 4) const uint EQ0 = 0u;
layout(constant_id = 5) const uint EQ1 = 0u;
layout(constant_id = 6) const uint EQ2 = 0u;
layout(constant_id = 7) const uint EQ3 = 0u;
layout(constant_id = 8) const uint EQ4 = 0u;
layout(constant_id = 9) const uint EQ5 = 0u;
layout(constant_id = 10) const uint EQ6 = 0u;
layout(constant_id = 11) const uint EQ7 = 0u;
layout(constant_id = 12) const uint EQ8 = 0u;
layout(constant_id = 13) const uint EQ9 = 0u;
layout(constant_id = 14) const uint EQ10 = 0u;
layout(constant_id = 15) const uint EQ11 = 0u;
layout(constant_id = 16) const uint EQ12 = 0u;
layout(constant_id = 17) const uint EQ13 = 0u;
layout(constant_id = 18) const uint EQ14 = 0u;
layout(constant_id = 19) const uint EQ15 = 0u;
// Nonzero: block extent in elements (thick 3D blocks are not the thin 2D shape derived below).
layout(constant_id = 20) const uint BLOCK_WIDTH = 0u;
layout(constant_id = 21) const uint BLOCK_HEIGHT = 0u;
layout(local_size_x = 8, local_size_y = 8) in;
@ -20,9 +43,23 @@ layout(push_constant) uniform Push {
uint tailX;
uint tailY;
uint elementBytes;
uint arrayLayer;
uint slice;
} params;
uint equationBit(uint mask, uvec2 p) {
uint selected = (p.x & (mask & 0xfffu)) ^ ((p.y << 12) & (mask & 0xfff000u)) ^ ((params.slice << 24) & (mask & 0xff000000u));
return bitCount(selected) & 1u;
}
uint equationOffset(uvec2 p) {
const uint eq[16] = uint[16](EQ0, EQ1, EQ2, EQ3, EQ4, EQ5, EQ6, EQ7, EQ8, EQ9, EQ10, EQ11, EQ12, EQ13, EQ14, EQ15);
uint offset = 0u;
for (uint bit = 0u; bit < 16u; ++bit) {
offset |= equationBit(eq[bit], p) << bit;
}
return offset;
}
uint standardOffset(uint x, uint y) {
switch (ELEMENT_BYTES) {
case 1u:
@ -67,76 +104,9 @@ uint standard64Extra(uint x, uint y) {
}
uint renderTargetOffset(uint x, uint y) {
uint offset = 0u;
switch (ELEMENT_BYTES) {
case 1u:
offset ^= (y << 2u) & 0x0008u;
offset ^= (y << 4u) & 0x0010u;
offset ^= (y << 3u) & 0x00a0u;
offset ^= (y << 5u) & 0x0f00u;
offset ^= (y << 6u) & 0x1000u;
offset ^= (y << 7u) & 0x4000u;
offset ^= x & 0x0007u;
offset ^= (x << 3u) & 0x0040u;
offset ^= (x << 5u) & 0x0300u;
offset ^= (x << 4u) & 0x0400u;
offset ^= (x << 6u) & 0x0800u;
offset ^= (x << 7u) & 0x2000u;
offset ^= (x << 8u) & 0x8000u;
break;
case 2u:
offset ^= (y << 4u) & 0x0070u;
offset ^= (y << 5u) & 0x0f00u;
offset ^= (y << 8u) & 0x5000u;
offset ^= (x << 1u) & 0x000eu;
offset ^= (x << 4u) & 0x0480u;
offset ^= (x << 5u) & 0x0300u;
offset ^= (x << 6u) & 0x0800u;
offset ^= (x << 7u) & 0x2000u;
offset ^= (x << 8u) & 0x8000u;
break;
case 4u:
offset ^= (y << 4u) & 0x0070u;
offset ^= (y << 5u) & 0x0f00u;
offset ^= (y << 9u) & 0x1000u;
offset ^= (y << 8u) & 0x4000u;
offset ^= (x << 2u) & 0x000cu;
offset ^= (x << 5u) & 0x0380u;
offset ^= (x << 4u) & 0x0400u;
offset ^= (x << 6u) & 0x0800u;
offset ^= (x << 9u) & 0xa000u;
break;
case 8u:
offset ^= (y << 4u) & 0x0010u;
offset ^= (y << 6u) & 0x0080u;
offset ^= (y << 5u) & 0x0f00u;
offset ^= (y << 10u) & 0x5000u;
offset ^= (x << 3u) & 0x0008u;
offset ^= (x << 4u) & 0x0460u;
offset ^= (x << 5u) & 0x0300u;
offset ^= (x << 6u) & 0x0800u;
offset ^= (x << 10u) & 0x2000u;
offset ^= (x << 9u) & 0x8000u;
break;
case 16u:
offset ^= (x << 4u) & 0x0410u;
offset ^= (x << 5u) & 0x0340u;
offset ^= (x << 6u) & 0x0800u;
offset ^= (x << 11u) & 0xa000u;
offset ^= (y << 5u) & 0x0f20u;
offset ^= (y << 6u) & 0x0080u;
offset ^= (y << 10u) & 0x1000u;
offset ^= (y << 11u) & 0x4000u;
break;
}
uint z = params.arrayLayer;
return offset ^ ((z & 8u) << 5u) ^ ((z & 4u) << 7u) ^ ((z & 2u) << 9u) ^ ((z & 1u) << 11u);
}
uint blockOffset(uvec2 p) {
if (TILE_FAMILY == 2u) {
return renderTargetOffset(p.x, p.y);
return equationOffset(p);
}
uint offset = standardOffset(p.x, p.y);
if (BLOCK_BYTES > 4096u) {
@ -146,6 +116,9 @@ uint blockOffset(uvec2 p) {
}
uvec2 blockExtent() {
if (BLOCK_WIDTH != 0u) {
return uvec2(BLOCK_WIDTH, BLOCK_HEIGHT);
}
uint width;
if (BLOCK_BYTES <= 256u) {
width = ELEMENT_BYTES <= 2u ? 16u : (ELEMENT_BYTES <= 8u ? 8u : 4u);
@ -197,6 +170,10 @@ void main() {
uint blockIndex = block.y * params.blocksPerRow + block.x;
uint tiledOffset = blockIndex * BLOCK_BYTES + blockOffset(swizzle);
copyElement(params.srcBase + tiledOffset, params.dstBase + linearOffset);
if (RETILE != 0u) {
copyElement(params.srcBase + linearOffset, params.dstBase + tiledOffset);
} else {
copyElement(params.srcBase + tiledOffset, params.dstBase + linearOffset);
}
}

View File

@ -1,11 +1,90 @@
#include "prx/libSceAgcDriver/Graphics/include/BdaResources.hpp"
#include <algorithm>
#include <bit>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <limits>
#include <list>
#include <mutex>
#include <sstream>
namespace AgcDriver::Graphics {
namespace {
// The page tables built per device, reused by a build whose ranges (guest ranges, device
// addresses, permissions) equal one of them: consecutive address-based builds map the same registered
// set through the same imports and mirrors, so the ~1200-entry table repeats. The shader only reads
// the table (its lookup keeps its state in function variables), and a batch in flight keeps its
// BdaResources, so sharing the buffer is safe. The cache holds the tables weakly: they never outlive
// the builds that own them (a static owner would destroy the buffer after its device, at exit or on
// device replacement, and would fail the tests' leak check), and with deferred lease release the
// previous builds are still in flight when the next one asks, which is what makes the hit rate.
// One table was enough while every build repeated the last one exactly; now the misaligned
// descriptor sub-ranges the GPU copies out of imports sit in pool staging buffers whose device
// addresses cycle through the few buffers the batches in flight leave free, and the three
// address-based programs of one queue interleave with the lease draws, so a table differs from the
// previous one and comes back a few builds later (69% misses in the wave-8 run, 4% before the GPU
// copies). Hence several recent tables, most recently used first, matched by a word hash before
// the byte compare. APS5_BDA_TABLE_CACHE_ENTRIES sets how many (default 8; 1 is the previous
// behaviour); APS5_NO_BDA_TABLE_CACHE=1 builds every table.
struct TableEntry {
std::uint64_t hash;
std::vector<ShaderRecompiler::BdaAbi::Range> ranges;
std::weak_ptr<Buffer> buffer;
};
struct TableCache {
std::mutex mutex;
VkDevice device = VK_NULL_HANDLE;
std::list<TableEntry> entries;
std::uint64_t hits = 0;
std::uint64_t misses = 0;
};
TableCache& Tables() {
static TableCache cache;
return cache;
}
bool tableCacheEnabled() {
static const bool disabled = std::getenv("APS5_NO_BDA_TABLE_CACHE") != nullptr;
return !disabled;
}
std::size_t tableCacheEntries() {
static const std::size_t entries = [] {
const char* value = std::getenv("APS5_BDA_TABLE_CACHE_ENTRIES");
const auto parsed = value != nullptr ? std::strtoull(value, nullptr, 10) : 8ull;
return static_cast<std::size_t>(std::clamp<unsigned long long>(parsed, 1, 64));
}();
return entries;
}
bool sameRanges(const std::vector<ShaderRecompiler::BdaAbi::Range>& left, const std::vector<ShaderRecompiler::BdaAbi::Range>& right) {
return left.size() == right.size() && (left.empty() || std::memcmp(left.data(), right.data(), left.size() * sizeof(left.front())) == 0);
}
// A 64-bit mix of the table's words: a table is ~1200 ranges of four words, and comparing the bytes
// against every held table would cost as much as building it.
std::uint64_t hashRanges(const std::vector<ShaderRecompiler::BdaAbi::Range>& ranges) {
static_assert(sizeof(ShaderRecompiler::BdaAbi::Range) % sizeof(std::uint64_t) == 0);
std::uint64_t hash = 0x9e3779b97f4a7c15ull ^ ranges.size();
for (const auto& range : ranges) {
std::uint64_t words[sizeof(range) / sizeof(std::uint64_t)];
std::memcpy(words, &range, sizeof(range));
for (const auto word : words) {
hash ^= word;
hash *= 0xff51afd7ed558ccdull;
hash ^= hash >> 33u;
}
}
return hash;
}
}
BdaResources::BdaResources(const Context& context) {
static_assert(std::endian::native == std::endian::little);
Require(sizeof(ShaderRecompiler::BdaAbi::Fault) <= context.limits.maxStorageBufferRange, "BDA fault buffer exceeds storage buffer range limit");
@ -14,15 +93,67 @@ BdaResources::BdaResources(const Context& context) {
}
BdaResources::BdaResources(const Context& context, const GuestBufferMemory& memory) : BdaResources(context) {
const auto ranges = memory.AddressRanges();
auto ranges = memory.AddressRanges();
Require(ranges.size() <= std::numeric_limits<std::uint32_t>::max(), "BDA table range count overflow");
Require(ranges.size() <= (std::numeric_limits<std::size_t>::max() - sizeof(ShaderRecompiler::BdaAbi::Header)) / sizeof(ShaderRecompiler::BdaAbi::Range), "BDA table size overflow");
tableBytes = sizeof(ShaderRecompiler::BdaAbi::Header) + ranges.size() * sizeof(ShaderRecompiler::BdaAbi::Range);
Require(tableBytes <= context.limits.maxStorageBufferRange && sizeof(ShaderRecompiler::BdaAbi::Fault) <= context.limits.maxStorageBufferRange, "BDA descriptors exceed storage buffer range limit");
table = std::make_unique<Buffer>(context, tableBytes, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT);
auto& cache = Tables();
const auto hash = tableCacheEnabled() ? hashRanges(ranges) : 0;
if (tableCacheEnabled()) {
std::lock_guard lock(cache.mutex);
// Another device's tables are not this one's (their buffers are dead or foreign).
if (cache.device != context.device) {
cache.entries.clear();
cache.device = context.device;
}
for (auto it = cache.entries.begin(); it != cache.entries.end();) {
// Compare the hash before taking a strong reference: locking a non-matching entry's
// buffer could make this thread its last owner (a concurrent reap releasing it) and
// run the pool release under the cache mutex; expired() takes no reference.
if (it->hash != hash) {
if (it->buffer.expired()) {
it = cache.entries.erase(it);
} else {
++it;
}
continue;
}
auto shared = it->buffer.lock();
if (shared == nullptr) {
// Every build that owned the table is gone, and its buffer with it.
it = cache.entries.erase(it);
continue;
}
if (sameRanges(it->ranges, ranges)) {
++cache.hits;
table = std::move(shared);
cache.entries.splice(cache.entries.begin(), cache.entries, it);
return;
}
++it;
}
}
table = std::make_shared<Buffer>(context, tableBytes, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT);
const ShaderRecompiler::BdaAbi::Header header{ShaderRecompiler::BdaAbi::Version, static_cast<std::uint32_t>(ranges.size()), sizeof(ShaderRecompiler::BdaAbi::Range), 0};
std::memcpy(table->Bytes().data(), &header, sizeof(header));
if (!ranges.empty()) std::memcpy(table->Bytes().data() + sizeof(header), ranges.data(), ranges.size() * sizeof(ranges.front()));
if (tableCacheEnabled()) {
std::lock_guard lock(cache.mutex);
++cache.misses;
if (cache.device != context.device) {
cache.entries.clear();
cache.device = context.device;
}
cache.entries.push_front({hash, std::move(ranges), table});
while (cache.entries.size() > tableCacheEntries()) cache.entries.pop_back();
}
}
BdaResources::TableCacheStats BdaResources::TableCacheCounters() {
auto& cache = Tables();
std::lock_guard lock(cache.mutex);
return {cache.hits, cache.misses, cache.entries.size()};
}
VkDescriptorBufferInfo BdaResources::Table() const {
@ -43,6 +174,12 @@ void BdaResources::CheckFault() const {
}
Require(report.state == ShaderRecompiler::BdaAbi::FaultState::Ready && report.reserved == 0, "incomplete or invalid BDA fault record");
Require(report.reason != ShaderRecompiler::BdaAbi::FaultReason::InvalidRectangle, "rect-list requires finite nondegenerate axis-aligned positions with equal positive W");
if (report.reason == ShaderRecompiler::BdaAbi::FaultReason::LoopLimit) {
// APS5_LOOP_GUARD: the shader left a loop that ran past the guard; the dispatch result is kept.
std::fprintf(stderr, "[gpu] loop guard: the loop exit at pc 0x%x ran past %u evaluations\n", report.instruction, report.bytes);
std::memset(fault->Bytes().data(), 0, fault->Bytes().size());
return;
}
std::ostringstream message;
message << "BDA access failed: address=0x" << std::hex << report.address << " instruction=0x" << report.instruction << std::dec << " bytes=" << report.bytes << " stage=" << report.stage << " reason=" << static_cast<std::uint32_t>(report.reason);
throw std::runtime_error(message.str());

View File

@ -1,74 +1,118 @@
#include "prx/libSceAgcDriver/Graphics/include/BufferPool.hpp"
#include <algorithm>
#include <bit>
#include <chrono>
#include <cstdio>
#include <cstdlib>
namespace AgcDriver::Graphics {
namespace {
// APS5_BUFFER_POOL_SHARED=1: one tier for every size, as before the split.
bool sharedTiers() {
static const bool shared = std::getenv("APS5_BUFFER_POOL_SHARED") != nullptr;
return shared;
}
}
BufferPool::BufferPool(const Context& context) : device(context.device), unmap(context.Function<PFN_vkUnmapMemory>("vkUnmapMemory")), destroyBuffer(context.Function<PFN_vkDestroyBuffer>("vkDestroyBuffer")), freeMemory(context.Function<PFN_vkFreeMemory>("vkFreeMemory")) {
buckets.fill(none);
for (std::size_t index = 0; index < capacity; ++index) freeSlots[index] = index;
smallTier.budget = smallBudget;
largeTier.budget = budget;
}
BufferPool::~BufferPool() {
for (const auto& slot : slots) {
if (slot.allocation) destroy(*slot.allocation);
}
}
std::size_t BufferPool::bucket(std::size_t bytes, VkBufferUsageFlags usage, VkMemoryPropertyFlags properties) noexcept {
auto hash = bytes ^ (bytes >> 16u);
hash = hash * 16777619u ^ usage;
hash = hash * 16777619u ^ properties;
return (hash ^ (hash >> 8u) ^ (hash >> 16u)) % bucketCount;
}
BufferAllocation BufferPool::remove(std::size_t index) noexcept {
auto& slot = slots[index];
const auto allocation = *slot.allocation;
auto* link = &buckets[bucket(allocation.bytes, allocation.usage, allocation.properties)];
while (*link != index) link = &slots[*link].next;
*link = slot.next;
if (slot.older != none) slots[slot.older].newer = slot.newer;
else oldest = slot.newer;
if (slot.newer != none) slots[slot.newer].older = slot.older;
else newest = slot.older;
retainedBytes -= allocation.allocationBytes;
slot = Slot{};
freeSlots[freeCount++] = index;
return allocation;
for (const auto& slot : smallTier.free) destroy(slot.allocation);
for (const auto& slot : largeTier.free) destroy(slot.allocation);
}
void BufferPool::destroy(const BufferAllocation& allocation) noexcept {
// Device-local allocations (see DeviceBuffer) are never mapped.
if (allocation.mapping != nullptr) unmap(device, allocation.memory);
destroyBuffer(device, allocation.buffer, nullptr);
freeMemory(device, allocation.memory, nullptr);
}
std::size_t BufferPool::Capacity(std::size_t bytes) {
static const bool exact = std::getenv("APS5_NO_BUFFER_CLASSES") != nullptr;
constexpr std::size_t smallest = 256;
if (exact || bytes >= classLimit) return bytes;
return std::max(smallest, std::bit_ceil(bytes));
}
BufferPool::Tier& BufferPool::tierFor(std::size_t capacity) {
return !sharedTiers() && capacity < classLimit ? smallTier : largeTier;
}
std::optional<BufferAllocation> BufferPool::Take(std::size_t bytes, VkBufferUsageFlags usage, VkMemoryPropertyFlags properties) {
static const bool profile = std::getenv("APS5_PROFILE_DRAW") != nullptr;
const auto capacity = Capacity(bytes);
std::lock_guard lock(mutex);
for (auto index = buckets[bucket(bytes, usage, properties)]; index != none; index = slots[index].next) {
const auto& allocation = *slots[index].allocation;
if (allocation.bytes == bytes && allocation.usage == usage && allocation.properties == properties) return remove(index);
++clock;
if (profile) {
static auto lastReport = std::chrono::steady_clock::now();
const auto now = std::chrono::steady_clock::now();
if (now - lastReport > std::chrono::seconds(10)) {
lastReport = now;
std::fprintf(stderr, "[bufferpool] small: %llu hits, %llu misses, %llu evictions, %zu retained (%.0f MiB); large: %llu hits, %llu misses, %llu evictions, %zu retained (%.0f MiB)%s\n", static_cast<unsigned long long>(smallTier.hits), static_cast<unsigned long long>(smallTier.misses), static_cast<unsigned long long>(smallTier.evictions), smallTier.free.size(), smallTier.retainedBytes / 1048576.0, static_cast<unsigned long long>(largeTier.hits), static_cast<unsigned long long>(largeTier.misses), static_cast<unsigned long long>(largeTier.evictions), largeTier.free.size(), largeTier.retainedBytes / 1048576.0, sharedTiers() ? " (shared)" : "");
}
}
auto& tier = tierFor(capacity);
for (auto it = tier.free.begin(); it != tier.free.end(); ++it) {
const auto& allocation = it->allocation;
if (allocation.bytes != capacity || allocation.usage != usage || allocation.properties != properties) continue;
auto result = allocation;
tier.retainedBytes -= allocation.allocationBytes;
*it = std::move(tier.free.back());
tier.free.pop_back();
++tier.hits;
return result;
}
++tier.misses;
return std::nullopt;
}
void BufferPool::evictOldest(Tier& tier, std::vector<BufferAllocation>& evicted) {
const auto oldest = std::min_element(tier.free.begin(), tier.free.end(), [](const Slot& left, const Slot& right) { return left.lastUse < right.lastUse; });
// First: a throw here leaves the slot retained and counted.
evicted.push_back(oldest->allocation);
tier.retainedBytes -= oldest->allocation.allocationBytes;
*oldest = std::move(tier.free.back());
tier.free.pop_back();
++tier.evictions;
}
std::size_t BufferPool::MaxSlots() {
// APS5_BUFFER_POOL_SLOTS=<n> bounds the retained allocations; 64 is the capacity the pool had
// before least-recently-used retention, for A/B runs.
static const std::size_t slots = [] {
const char* value = std::getenv("APS5_BUFFER_POOL_SLOTS");
const auto parsed = value != nullptr ? std::strtoull(value, nullptr, 10) : 0;
return parsed != 0 ? static_cast<std::size_t>(parsed) : defaultSlots;
}();
return slots;
}
void BufferPool::Put(const BufferAllocation& allocation) noexcept {
std::lock_guard lock(mutex);
if (allocation.allocationBytes > budget) {
// Evicted allocations are destroyed after the mutex is released (see evictOldest). The vector
// may throw on growth; a Put that cannot retain simply destroys, as noexcept requires.
std::vector<BufferAllocation> evicted;
try {
std::lock_guard lock(mutex);
auto& tier = tierFor(allocation.bytes);
if (allocation.allocationBytes > tier.budget) {
evicted.push_back(allocation);
} else {
const auto maxSlots = MaxSlots();
while (!tier.free.empty() && (tier.retainedBytes + allocation.allocationBytes > tier.budget || tier.free.size() >= maxSlots)) evictOldest(tier, evicted);
tier.free.push_back({allocation, ++clock});
tier.retainedBytes += allocation.allocationBytes;
}
} catch (...) {
destroy(allocation);
return;
}
while (retainedBytes > budget - allocation.allocationBytes || freeCount == 0) destroy(remove(oldest));
const auto index = freeSlots[--freeCount];
auto& slot = slots[index];
auto& head = buckets[bucket(allocation.bytes, allocation.usage, allocation.properties)];
slot.allocation = allocation;
slot.next = head;
slot.older = newest;
if (newest != none) slots[newest].newer = index;
else oldest = index;
newest = index;
head = index;
retainedBytes += allocation.allocationBytes;
for (const auto& gone : evicted) destroy(gone);
}
std::shared_ptr<BufferPool> GetBufferPool(const Context& context) {

View File

@ -1,4 +1,6 @@
#include "prx/libSceAgcDriver/Graphics/include/ColorTargetLayout.hpp"
#include "prx/libSceAgcDriver/Graphics/include/TextureSwizzleEquations.hpp"
#include <bit>
#include <cstring>
#include <limits>
#include <stdexcept>
@ -10,9 +12,8 @@ void require(bool condition, const char* reason) {
if (!condition) throw std::runtime_error(reason);
}
std::uint32_t blockOffset(std::uint32_t x, std::uint32_t y) {
return ((y << 4u) & 0x0070u) ^ ((y << 5u) & 0x0f00u) ^ ((y << 9u) & 0x1000u) ^ ((y << 8u) & 0x4000u)
^ ((x << 2u) & 0x000cu) ^ ((x << 5u) & 0x0380u) ^ ((x << 4u) & 0x0400u) ^ ((x << 6u) & 0x0800u) ^ ((x << 9u) & 0xa000u);
std::uint32_t parity(std::uint32_t value) {
return static_cast<std::uint32_t>(std::popcount(value)) & 1u;
}
}
@ -26,28 +27,48 @@ ColorTileMode DecodeColorTileMode(std::uint32_t attrib3) {
return static_cast<ColorTileMode>(mode);
}
ColorTargetLayout::ColorTargetLayout(std::uint32_t width, std::uint32_t height, ColorTileMode mode) : width(width), height(height), pitch(width), mode(mode), bytes(0) {
ColorTargetLayout::ColorTargetLayout(std::uint32_t width, std::uint32_t height, ColorTileMode mode, std::uint32_t bytesPerElement) : width(width), height(height), pitch(width), mode(mode), bytes(0), elementBytes(bytesPerElement) {
require(width != 0 && height != 0 && width <= 16384 && height <= 16384, "AGC graphics: invalid color surface extent");
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 % 64u == 0, "AGC graphics: linear surface pitch requires a width aligned to 256 bytes");
require(width * bytesPerElement % 256u == 0, "AGC graphics: linear surface pitch requires a width aligned to 256 bytes");
break;
case ColorTileMode::RenderTarget:
pitch = (width + 127u) & ~127u;
paddedHeight = (height + 127u) & ~127u;
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::uint32_t>(std::countr_zero(bytesPerElement));
blockWidth = 1u << ((log2Elements + 1u) / 2u);
blockHeight = 1u << (log2Elements / 2u);
pitch = (width + blockWidth - 1u) / blockWidth * blockWidth;
paddedHeight = (height + blockHeight - 1u) / blockHeight * blockHeight;
const auto* equation = FindTextureSwizzleEquation(27u, bytesPerElement);
require(equation != nullptr, "AGC graphics: no SW_64KB_R_X equation for the color element size");
xOffsets.resize(blockWidth);
yOffsets.resize(blockHeight);
for (std::uint32_t x = 0; x < blockWidth; ++x) {
std::uint32_t offset = 0;
for (std::uint32_t bit = 0; bit < 16u; ++bit) offset |= parity(x & equation->bits[bit] & 0xfffu) << bit;
xOffsets[x] = offset;
}
for (std::uint32_t y = 0; y < blockHeight; ++y) {
std::uint32_t offset = 0;
for (std::uint32_t bit = 0; bit < 16u; ++bit) offset |= parity((y << 12u) & equation->bits[bit] & 0xfff000u) << bit;
yOffsets[y] = offset;
}
break;
}
default: throw std::runtime_error("AGC graphics: unsupported color tile mode");
}
const auto size = static_cast<std::uint64_t>(pitch) * paddedHeight * 4u;
const auto size = static_cast<std::uint64_t>(pitch) * paddedHeight * bytesPerElement;
require(size <= std::numeric_limits<std::size_t>::max(), "AGC graphics: color surface size overflow");
bytes = static_cast<std::size_t>(size);
}
std::size_t ColorTargetLayout::offset(std::uint32_t x, std::uint32_t y) const {
if (mode == ColorTileMode::Linear) return (static_cast<std::size_t>(y) * pitch + x) * 4u;
const auto block = static_cast<std::size_t>(y / 128u) * (pitch / 128u) + x / 128u;
return block * 65536u + blockOffset(x, y);
if (mode == ColorTileMode::Linear) return (static_cast<std::size_t>(y) * pitch + x) * elementBytes;
const auto block = static_cast<std::size_t>(y / blockHeight) * (pitch / blockWidth) + x / blockWidth;
return block * 65536u + (xOffsets[x % blockWidth] ^ yOffsets[y % blockHeight]);
}
std::size_t ColorTargetLayout::Offset(std::uint32_t x, std::uint32_t y) const {
@ -58,18 +79,16 @@ std::size_t ColorTargetLayout::Offset(std::uint32_t x, std::uint32_t y) const {
void ColorTargetLayout::Detile(std::span<const std::byte> source, std::span<std::byte> destination) const {
require(source.size() == Bytes() && destination.size() == LinearBytes(), "AGC graphics: color detile buffer size mismatch");
for (std::uint32_t y = 0; y < height; ++y) {
for (std::uint32_t x = 0; x < width; ++x) {
std::memcpy(destination.data() + (static_cast<std::size_t>(y) * width + x) * 4u, source.data() + offset(x, y), 4);
}
auto* row = destination.data() + static_cast<std::size_t>(y) * width * elementBytes;
for (std::uint32_t x = 0; x < width; ++x) std::memcpy(row + static_cast<std::size_t>(x) * elementBytes, source.data() + offset(x, y), elementBytes);
}
}
void ColorTargetLayout::Tile(std::span<const std::byte> source, std::span<std::byte> destination) const {
require(source.size() == LinearBytes() && destination.size() == Bytes(), "AGC graphics: color tile buffer size mismatch");
for (std::uint32_t y = 0; y < height; ++y) {
for (std::uint32_t x = 0; x < width; ++x) {
std::memcpy(destination.data() + offset(x, y), source.data() + (static_cast<std::size_t>(y) * width + x) * 4u, 4);
}
const auto* row = source.data() + static_cast<std::size_t>(y) * width * elementBytes;
for (std::uint32_t x = 0; x < width; ++x) std::memcpy(destination.data() + offset(x, y), row + static_cast<std::size_t>(x) * elementBytes, elementBytes);
}
}

View File

@ -7,7 +7,7 @@ namespace AgcDriver::Graphics {
void ReadColorTarget(const ColorTarget& target, std::span<std::byte> destination) {
PerformanceTimer timing("ColorTarget.Read");
const ColorTargetLayout layout(target.extent.width, target.extent.height, target.tileMode);
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) {
GuestMemory::Read(target.address, destination, layout.Alignment());
@ -23,7 +23,7 @@ void ReadColorTarget(const ColorTarget& target, std::span<std::byte> destination
void WriteColorTarget(const ColorTarget& target, std::span<const std::byte> source) {
PerformanceTimer timing("ColorTarget.Write");
const ColorTargetLayout layout(target.extent.width, target.extent.height, target.tileMode);
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) {
GuestMemory::Write(target.address, source, layout.Alignment());

View File

@ -0,0 +1,433 @@
#include "prx/libSceAgcDriver/Graphics/include/DccMetadata.hpp"
#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp"
#include "prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp"
#include "prx/libSceAgcDriver/Graphics/include/Recorder.hpp"
#include "prx/libSceAgcDriver/Graphics/include/Resources.hpp"
#include "prx/libSceAgcDriver/Graphics/include/TextureFormat.hpp"
#include <algorithm>
#include <atomic>
#include <chrono>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <memory>
#include <mutex>
#include <optional>
#include <set>
#include <utility>
#include <vector>
namespace AgcDriver::Graphics {
namespace {
constexpr std::uint64_t KeyBytes = 256;
// Whether every key in [keys, keys + count) equals `first`. Eight keys per compare: the scan runs
// on every texture and storage validation, and an all-0xff (uncompressed) surface's keys are read
// to the end each time (132 KiB for a 4K RGBA8 target). APS5_NO_DCC_WORD_SCAN=1 restores the byte loop.
bool AllKeysEqual(const std::uint8_t* keys, std::size_t count, std::uint8_t first) {
static const bool byteScan = std::getenv("APS5_NO_DCC_WORD_SCAN") != nullptr;
if (byteScan) return std::all_of(keys, keys + count, [&](std::uint8_t key) { return key == first; });
const std::uint64_t pattern = 0x0101010101010101ull * first;
std::size_t i = 0;
for (; i + 8 <= count; i += 8) {
std::uint64_t word;
std::memcpy(&word, keys + i, sizeof(word));
if (word != pattern) return false;
}
for (; i < count; ++i) {
if (keys[i] != first) return false;
}
return true;
}
// APS5_PROFILE_DRAW: key scans, their bytes and time, printed as [dcc] every 10 s, with the hits of
// the GPU-store memo below and the "uncompressed" key stores by where they were made. Flush syncs
// stay 0 (no key read syncs the recorder); the field is kept so the line's shape does not change.
struct ScanProfile {
std::atomic<std::uint64_t> scans{0};
std::atomic<std::uint64_t> bytes{0};
std::atomic<std::uint64_t> nanoseconds{0};
std::atomic<std::uint64_t> memoHits{0};
std::atomic<std::uint64_t> flushSyncs{0};
std::atomic<std::uint64_t> gpuStores{0};
// GPU stores whose range had a 4-byte unaligned head or tail (copied instead of filled).
std::atomic<std::uint64_t> gpuSplitStores{0};
std::atomic<std::uint64_t> cpuStores{0};
std::atomic<std::int64_t> lastReport{0};
};
ScanProfile& Scans() {
static ScanProfile profile;
return profile;
}
bool ScanProfileEnabled() {
static const bool profile = std::getenv("APS5_PROFILE_DRAW") != nullptr;
return profile;
}
void ReportScans(std::chrono::steady_clock::time_point now) {
auto& profile = Scans();
const auto nowMs = std::chrono::duration_cast<std::chrono::milliseconds>(now.time_since_epoch()).count();
auto last = profile.lastReport.load();
// The first event starts the period rather than reporting an empty one.
if (last == 0) {
profile.lastReport.compare_exchange_strong(last, nowMs);
return;
}
if (nowMs - last < 10000 || !profile.lastReport.compare_exchange_strong(last, nowMs)) return;
std::fprintf(stderr, "[dcc] %llu scans, %.1f MiB scanned, %llu memo hits, %llu flush syncs, %.1f ms; uncompressed keys stored: %llu on the GPU (%llu with an unaligned head or tail), %llu on the CPU\n", static_cast<unsigned long long>(profile.scans.load()), profile.bytes.load() / 1048576.0, static_cast<unsigned long long>(profile.memoHits.load()), static_cast<unsigned long long>(profile.flushSyncs.load()), profile.nanoseconds.load() / 1e6, static_cast<unsigned long long>(profile.gpuStores.load()), static_cast<unsigned long long>(profile.gpuSplitStores.load()), static_cast<unsigned long long>(profile.cpuStores.load()));
}
void CountScan(std::size_t bytes, std::chrono::steady_clock::time_point start) {
auto& profile = Scans();
const auto now = std::chrono::steady_clock::now();
profile.scans.fetch_add(1, std::memory_order_relaxed);
profile.bytes.fetch_add(bytes, std::memory_order_relaxed);
profile.nanoseconds.fetch_add(static_cast<std::uint64_t>(std::chrono::duration_cast<std::chrono::nanoseconds>(now - start).count()), std::memory_order_relaxed);
ReportScans(now);
}
void CountStore(std::atomic<std::uint64_t>& counter) {
if (!ScanProfileEnabled()) return;
counter.fetch_add(1, std::memory_order_relaxed);
ReportScans(std::chrono::steady_clock::now());
}
// "Uncompressed" keys the driver stored on the GPU (MarkDccUncompressed's recorded fill) whose batch
// may not have run yet: the bytes ReadDccKeys scans would still show the keys from before the store
// (a fast-clear code), and a storage image whose write-back made the store would be re-uploaded as
// cleared. While recorded work that writes the range is still running, the range reads as
// uncompressed from here; the bytes are trusted again only once the newest recorded writer of the
// range has signaled its fence (batches of the recorder's queue finish in order, so the fill and
// every writer up to that one have landed and the bytes are final, whichever batch wrote last).
// Dropping an entry on a later batch's serial alone would not do: until the fill's batch runs the
// bytes PREDATE the fill (the previous clear code), and a later writer that is not a same-code clear
// (a decompress kernel, a partial key write, a false overlap from an unrelated V#) would make the
// next Refresh compare that stale code against the uploaded keys. The lookup runs with or without
// GuestMemory::GpuMutex (a resource build's stage A reads keys unlocked, its stage B re-reads them
// under the lock): unlocked it can only ask the lock-free snapshot whether the range is written at
// all. Entries belong to one recorder: when the active recorder changes (device replacement; a new
// recorder can reuse the old one's address) they are all dropped. Its mutex is a leaf (nothing is
// taken under it).
struct GpuKeyStore {
std::uint64_t begin;
std::uint64_t end;
};
struct KeyStoreMemo {
std::mutex mutex;
std::vector<GpuKeyStore> entries;
const Recorder* recorder = nullptr;
};
KeyStoreMemo& Memo() {
static KeyStoreMemo memo;
return memo;
}
// Under memo.mutex: the active recorder, with the entries of any other recorder dropped.
Recorder* MemoRecorder(KeyStoreMemo& memo) {
auto* active = Recorder::Active();
if (active != memo.recorder) {
memo.entries.clear();
memo.recorder = active;
}
return active;
}
void ForgetStores(std::uint64_t begin, std::uint64_t end) {
auto& memo = Memo();
std::lock_guard lock(memo.mutex);
std::erase_if(memo.entries, [&](const GpuKeyStore& store) { return begin < store.end && store.begin < end; });
}
// Uncompressed when a GPU store still pending covers the whole range, else nullopt (the bytes decide).
std::optional<DccKeys> MemoizedKeys(std::uint64_t begin, std::uint64_t end) {
auto& memo = Memo();
std::lock_guard lock(memo.mutex);
auto* recorder = MemoRecorder(memo);
for (auto it = memo.entries.begin(); it != memo.entries.end(); ++it) {
if (it->begin > begin || it->end < end) continue;
const auto bytes = static_cast<std::size_t>(it->end - it->begin);
bool pending = false;
if (recorder != nullptr) {
if (GuestMemory::GpuMutex().HeldByThisThread()) {
// The open batch is never signaled (also with APS5_NO_SYNC_THROUGH, where the info
// carries the open flag with the newest in-flight batch's fence state).
const auto info = recorder->DescribePendingWrite(it->begin, bytes);
pending = info.has_value() && !info->signaled;
} else {
pending = Recorder::SnapshotWriteOverlaps(it->begin, bytes);
}
}
if (!pending) {
memo.entries.erase(it);
return std::nullopt;
}
if (ScanProfileEnabled()) Scans().memoHits.fetch_add(1, std::memory_order_relaxed);
return DccKeys::Uncompressed;
}
return std::nullopt;
}
// The 0xff keys as a fill recorded into the active recorder's open batch, into the range's host
// import (the WriteLabelOnGpu / FillBuffer pattern): ordered behind every earlier recorded read or
// write of the range (the title's DCC clear or decompress kernel storing keys through a V#, which the
// CPU store had to wait for) and visible to the work recorded after it and to the host. vkCmdFillBuffer
// needs a 4-byte aligned offset and size; an unaligned head or tail is copied from a kept 4-byte
// buffer of 0xff instead. Under GuestMemory::GpuMutex (it records). False when the keys cannot be
// stored this way (memory the GPU has no view of): the caller stores them on the CPU.
bool StoreUncompressedOnGpu(const Context& context, Recorder& recorder, std::uint64_t begin, std::size_t count) {
const auto* import = HostImportFor(context, begin, count);
if (import == nullptr) return false;
const VkDeviceSize first = begin - import->base;
const VkDeviceSize last = first + count;
const VkDeviceSize fillBegin = std::min((first + 3) & ~VkDeviceSize{3}, last);
const VkDeviceSize fillEnd = std::max(last & ~VkDeviceSize{3}, fillBegin);
// Everything that can throw (the seed's allocation, the batch's command buffer) comes before the
// first recorded command: a fill left in the batch without its note and memo entry would be an
// unordered write of the keys.
std::shared_ptr<Buffer> seed;
if (fillBegin > first || last > fillEnd) {
seed = std::make_shared<Buffer>(context, 4, VK_BUFFER_USAGE_TRANSFER_SRC_BIT);
std::memset(seed->Bytes().data(), 0xff, seed->Bytes().size());
recorder.Keep(seed);
}
const auto commands = recorder.Commands();
RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_TRANSFER_WRITE_BIT | VK_ACCESS_HOST_WRITE_BIT, VK_ACCESS_TRANSFER_WRITE_BIT | VK_ACCESS_TRANSFER_READ_BIT);
if (fillEnd > fillBegin) context.Function<PFN_vkCmdFillBuffer>("vkCmdFillBuffer")(commands, import->buffer, fillBegin, fillEnd - fillBegin, 0xffffffffu);
if (seed != nullptr) {
VkBufferCopy copies[2];
std::uint32_t copyCount = 0;
if (fillBegin > first) copies[copyCount++] = {0, first, fillBegin - first};
if (last > fillEnd) copies[copyCount++] = {0, fillEnd, last - fillEnd};
context.Function<PFN_vkCmdCopyBuffer>("vkCmdCopyBuffer")(commands, seed->Handle(), import->buffer, copyCount, copies);
CountStore(Scans().gpuSplitStores);
}
RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT | VK_PIPELINE_STAGE_HOST_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_TRANSFER_READ_BIT | VK_ACCESS_HOST_READ_BIT);
recorder.NotePendingWrite(begin, count);
GuestMemory::MarkWritten(begin, count);
// After the note: an unlocked memo lookup drops an entry the snapshot does not cover yet. The
// entry is kept only for the active recorder (the one the flush hook and the lookups consult).
{
auto& memo = Memo();
std::lock_guard lock(memo.mutex);
const auto end = begin + count;
std::erase_if(memo.entries, [&](const GpuKeyStore& store) { return begin < store.end && store.begin < end; });
if (MemoRecorder(memo) == &recorder) memo.entries.push_back({begin, end});
}
CountStore(Scans().gpuStores);
return true;
}
void StoreUncompressedOnCpu(std::uint64_t begin, std::size_t count) {
// The write waits for recorded work over the range (the flush hook), so the bytes are current after.
ForgetStores(begin, begin + count);
const std::vector<std::byte> uncompressed(count, std::byte{0xff});
GuestMemory::Write(begin, uncompressed);
CountStore(Scans().cpuStores);
}
// Whether the keys already read as uncompressed: scans the same bytes as ReadDccKeys, so it counts
// as a scan too.
bool AlreadyUncompressed(std::uint64_t metaAddress, std::size_t count) {
const auto* keys = reinterpret_cast<const std::uint8_t*>(metaAddress);
const auto start = ScanProfileEnabled() ? std::chrono::steady_clock::now() : std::chrono::steady_clock::time_point{};
const bool uncompressed = AllKeysEqual(keys, count, 0xff);
if (ScanProfileEnabled()) CountScan(count, start);
return uncompressed;
}
bool CpuKeysOnly() {
static const bool cpuKeys = std::getenv("APS5_CPU_DCC_KEYS") != nullptr;
return cpuKeys;
}
enum class Kind { Unorm, Snorm, Uint, Sint, Float };
struct Layout {
std::uint32_t channels;
std::uint32_t bits[4];
Kind kind;
};
bool LayoutFor(VkFormat format, Layout& layout) {
switch (format) {
case VK_FORMAT_R8_UNORM: case VK_FORMAT_R8_SRGB: layout = {1, {8}, Kind::Unorm}; return true;
case VK_FORMAT_R8_UINT: layout = {1, {8}, Kind::Uint}; return true;
case VK_FORMAT_R16_UNORM: layout = {1, {16}, Kind::Unorm}; return true;
case VK_FORMAT_R16_SNORM: layout = {1, {16}, Kind::Snorm}; return true;
case VK_FORMAT_R16_UINT: layout = {1, {16}, Kind::Uint}; return true;
case VK_FORMAT_R16_SINT: layout = {1, {16}, Kind::Sint}; return true;
case VK_FORMAT_R16_SFLOAT: layout = {1, {16}, Kind::Float}; return true;
case VK_FORMAT_R8G8_UNORM: case VK_FORMAT_R8G8_SRGB: layout = {2, {8, 8}, Kind::Unorm}; return true;
case VK_FORMAT_R8G8_SNORM: layout = {2, {8, 8}, Kind::Snorm}; return true;
case VK_FORMAT_R8G8_UINT: layout = {2, {8, 8}, Kind::Uint}; return true;
case VK_FORMAT_R8G8_SINT: layout = {2, {8, 8}, Kind::Sint}; return true;
case VK_FORMAT_R32_UINT: layout = {1, {32}, Kind::Uint}; return true;
case VK_FORMAT_R32_SINT: layout = {1, {32}, Kind::Sint}; return true;
case VK_FORMAT_R32_SFLOAT: layout = {1, {32}, Kind::Float}; return true;
case VK_FORMAT_R16G16_UNORM: layout = {2, {16, 16}, Kind::Unorm}; return true;
case VK_FORMAT_R16G16_SNORM: layout = {2, {16, 16}, Kind::Snorm}; return true;
case VK_FORMAT_R16G16_UINT: layout = {2, {16, 16}, Kind::Uint}; return true;
case VK_FORMAT_R16G16_SINT: layout = {2, {16, 16}, Kind::Sint}; return true;
case VK_FORMAT_R16G16_SFLOAT: layout = {2, {16, 16}, Kind::Float}; return true;
case VK_FORMAT_A2B10G10R10_UNORM_PACK32: layout = {4, {10, 10, 10, 2}, Kind::Unorm}; return true;
case VK_FORMAT_A2B10G10R10_UINT_PACK32: layout = {4, {10, 10, 10, 2}, Kind::Uint}; return true;
case VK_FORMAT_R8G8B8A8_UNORM: case VK_FORMAT_R8G8B8A8_SRGB: case VK_FORMAT_B8G8R8A8_UNORM: case VK_FORMAT_B8G8R8A8_SRGB: layout = {4, {8, 8, 8, 8}, Kind::Unorm}; return true;
case VK_FORMAT_R8G8B8A8_SNORM: layout = {4, {8, 8, 8, 8}, Kind::Snorm}; return true;
case VK_FORMAT_R8G8B8A8_UINT: layout = {4, {8, 8, 8, 8}, Kind::Uint}; return true;
case VK_FORMAT_R8G8B8A8_SINT: layout = {4, {8, 8, 8, 8}, Kind::Sint}; return true;
case VK_FORMAT_R32G32_UINT: layout = {2, {32, 32}, Kind::Uint}; return true;
case VK_FORMAT_R32G32_SINT: layout = {2, {32, 32}, Kind::Sint}; return true;
case VK_FORMAT_R32G32_SFLOAT: layout = {2, {32, 32}, Kind::Float}; return true;
case VK_FORMAT_R16G16B16A16_UNORM: layout = {4, {16, 16, 16, 16}, Kind::Unorm}; return true;
case VK_FORMAT_R16G16B16A16_SNORM: layout = {4, {16, 16, 16, 16}, Kind::Snorm}; return true;
case VK_FORMAT_R16G16B16A16_UINT: layout = {4, {16, 16, 16, 16}, Kind::Uint}; return true;
case VK_FORMAT_R16G16B16A16_SINT: layout = {4, {16, 16, 16, 16}, Kind::Sint}; return true;
case VK_FORMAT_R16G16B16A16_SFLOAT: layout = {4, {16, 16, 16, 16}, Kind::Float}; return true;
case VK_FORMAT_R32G32B32A32_UINT: layout = {4, {32, 32, 32, 32}, Kind::Uint}; return true;
case VK_FORMAT_R32G32B32A32_SINT: layout = {4, {32, 32, 32, 32}, Kind::Sint}; return true;
case VK_FORMAT_R32G32B32A32_SFLOAT: layout = {4, {32, 32, 32, 32}, Kind::Float}; return true;
default: return false;
}
}
// "1" in a channel of this kind and width.
std::uint64_t One(Kind kind, std::uint32_t bits) {
switch (kind) {
case Kind::Unorm: case Kind::Uint: return bits >= 64 ? ~0ull : (1ull << bits) - 1u;
case Kind::Snorm: case Kind::Sint: return (1ull << (bits - 1u)) - 1u;
case Kind::Float: return bits == 16 ? 0x3c00u : 0x3f800000u;
}
return 0;
}
}
const char* DccKeysName(DccKeys keys) {
switch (keys) {
case DccKeys::Uncompressed: return "uncompressed";
case DccKeys::Clear0000: return "0000";
case DccKeys::Clear0001: return "0001";
case DccKeys::Clear1110: return "1110";
case DccKeys::Clear1111: return "1111";
case DccKeys::ClearRegister: return "register";
case DccKeys::Mixed: return "mixed";
case DccKeys::Unreadable: return "unreadable";
}
return "?";
}
DccKeys ReadDccKeys(std::uint64_t metaAddress, std::uint64_t surfaceBytes) {
const auto count = static_cast<std::size_t>(surfaceBytes / KeyBytes);
if (metaAddress == 0 || count == 0 || !GuestMemory::Accessible(reinterpret_cast<const void*>(metaAddress), count)) return DccKeys::Unreadable;
// A driver store still pending on the GPU is not in the bytes yet.
if (const auto memoized = MemoizedKeys(metaAddress, metaAddress + count)) return *memoized;
const auto* keys = reinterpret_cast<const std::uint8_t*>(metaAddress);
const auto first = keys[0];
const auto start = ScanProfileEnabled() ? std::chrono::steady_clock::now() : std::chrono::steady_clock::time_point{};
const bool uniform = AllKeysEqual(keys + 1, count - 1, first);
if (ScanProfileEnabled()) CountScan(count, start);
if (!uniform) return DccKeys::Mixed;
switch (first) {
case 0x00: return DccKeys::Clear0000;
case 0x40: return DccKeys::Clear0001;
case 0x80: return DccKeys::Clear1110;
case 0xc0: return DccKeys::Clear1111;
case 0x20: return DccKeys::ClearRegister;
case 0xff: return DccKeys::Uncompressed;
default: return DccKeys::Mixed;
}
}
bool IsDccClear(DccKeys keys) {
return keys == DccKeys::Clear0000 || keys == DccKeys::Clear0001 || keys == DccKeys::Clear1110 || keys == DccKeys::Clear1111 || keys == DccKeys::ClearRegister;
}
void MarkDccUncompressed(std::uint64_t metaAddress, std::uint64_t surfaceBytes) {
const auto count = static_cast<std::size_t>(surfaceBytes / KeyBytes);
if (metaAddress == 0 || count == 0 || !GuestMemory::Accessible(reinterpret_cast<const void*>(metaAddress), count, true)) return;
// The bytes are trusted only when no recorded work writes them (lock-free, with or without
// GuestMemory::GpuMutex); otherwise the store runs and waits through the flush hook. A pending
// GPU store of the driver's own is no reason to skip: a title kernel recorded into the same
// batch after it may write clear keys, and the memo cannot order inside a batch.
if (!Recorder::SnapshotWriteOverlaps(metaAddress, count) && AlreadyUncompressed(metaAddress, count)) return;
StoreUncompressedOnCpu(metaAddress, count);
}
void MarkDccUncompressed(const Context& context, std::uint64_t metaAddress, std::uint64_t surfaceBytes) {
const auto count = static_cast<std::size_t>(surfaceBytes / KeyBytes);
if (metaAddress == 0 || count == 0 || !GuestMemory::Accessible(reinterpret_cast<const void*>(metaAddress), count, true)) return;
auto* recorder = CpuKeysOnly() || !GuestMemory::GpuMutex().HeldByThisThread() ? nullptr : Recorder::Active();
// The bytes are scanned only when no recorded work writes them: a pending clear kernel's keys are
// not in memory yet, so the bytes could read as uncompressed while the kernel will store a clear
// code, and the range is stored without looking (the fill lands after the kernel). That includes
// a pending GPU store of the driver's own (the memo): a title kernel recorded into the same batch
// after it may write clear keys, and nothing orders inside a batch, so a fresh fill is recorded
// (idempotent, no wait) rather than trusting the earlier one.
const bool pending = recorder != nullptr && recorder->PendingWriteOverlaps(metaAddress, count);
if (!pending && AlreadyUncompressed(metaAddress, count)) return;
if (recorder != nullptr && StoreUncompressedOnGpu(context, *recorder, metaAddress, count)) return;
StoreUncompressedOnCpu(metaAddress, count);
}
bool FillDccClear(VkFormat format, DccKeys keys, bool alphaOnMsb, std::span<std::byte> bytes) {
if (keys == DccKeys::Clear0000) {
std::fill(bytes.begin(), bytes.end(), std::byte{0});
return true;
}
Layout layout{};
if (!IsDccClear(keys) || keys == DccKeys::ClearRegister || !LayoutFor(format, layout)) return false;
const bool color = keys == DccKeys::Clear1110 || keys == DccKeys::Clear1111;
const bool alpha = keys == DccKeys::Clear0001 || keys == DccKeys::Clear1111;
const int alphaChannel = layout.channels == 3 ? -1 : alphaOnMsb ? static_cast<int>(layout.channels) - 1 : 0;
std::uint32_t elementBits = 0;
for (std::uint32_t channel = 0; channel < layout.channels; ++channel) elementBits += layout.bits[channel];
std::vector<std::byte> element(elementBits / 8u);
std::uint32_t bit = 0;
for (std::uint32_t channel = 0; channel < layout.channels; ++channel) {
const bool set = static_cast<int>(channel) == alphaChannel ? alpha : color;
const auto value = set ? One(layout.kind, layout.bits[channel]) : 0u;
for (std::uint32_t i = 0; i < layout.bits[channel]; ++i, ++bit) {
if (((value >> i) & 1u) != 0) element[bit / 8u] |= static_cast<std::byte>(1u << (bit % 8u));
}
}
for (std::size_t offset = 0; offset + element.size() <= bytes.size(); offset += element.size()) std::memcpy(bytes.data() + offset, element.data(), element.size());
return true;
}
bool DccAlphaOnMsb(VkFormat format, std::uint32_t componentSwap) {
Layout layout{};
const auto channels = LayoutFor(format, layout) ? layout.channels : 4u;
constexpr std::uint32_t standardReversed = 2;
constexpr std::uint32_t alternateReversed = 3;
if (channels == 1) return componentSwap == alternateReversed;
return componentSwap != standardReversed && componentSwap != alternateReversed;
}
DccKeys TextureClearKeys(const GuestTextureResource& resource, std::uint64_t guestBytes) {
if (resource.dccAddress == 0) return DccKeys::Uncompressed;
const auto keys = ReadDccKeys(resource.dccAddress, guestBytes);
if (keys == DccKeys::Uncompressed) return keys;
std::byte probe[16]{};
if (!IsDccClear(keys) || !FillDccClear(ResolveTextureFormat(resource.format), keys, resource.dccAlphaOnMsb, std::span(probe, std::min<std::size_t>(sizeof(probe), BytesPerElement(resource.format))))) {
static std::mutex reportedMutex;
static std::set<std::pair<std::uint64_t, int>> reported;
std::lock_guard lock(reportedMutex);
if (reported.size() < 32 && reported.insert({resource.baseAddress, static_cast<int>(keys)}).second) std::fprintf(stderr, "[gpu] texture 0x%llx (format %u) has %s DCC keys at 0x%llx; its texels are read as stored\n", static_cast<unsigned long long>(resource.baseAddress), resource.format, DccKeysName(keys), static_cast<unsigned long long>(resource.dccAddress));
return DccKeys::Uncompressed;
}
return keys;
}
void ReadTextureSurface(const GuestTextureResource& resource, DccKeys keys, std::span<std::byte> bytes) {
// Named for the [hooksync] attribution: the read goes through the flush hook.
const GuestMemory::ReadSiteScope site(GuestMemory::ReadSite::TextureRead);
if (keys == DccKeys::Uncompressed) GuestMemory::ReadCommitted(resource.baseAddress, bytes);
else FillDccClear(ResolveTextureFormat(resource.format), keys, resource.dccAlphaOnMsb, bytes);
}
}

File diff suppressed because it is too large Load Diff

View File

@ -2,8 +2,6 @@
#include "prx/libSceAgcDriver/Graphics/shaders/ColorTransfer_spv.h"
#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp"
#include <array>
#include <cstring>
#include "prx/libSceAgcDriver/Execution/include/PerformanceTimer.hpp"
namespace AgcDriver::Graphics {
@ -72,11 +70,11 @@ void GpuColorTransfer::prepare(std::uint32_t newWidth, std::uint32_t newHeight,
Require(layout.Bytes() <= context.limits.maxStorageBufferRange && layout.LinearBytes() <= context.limits.maxStorageBufferRange, "color transfer exceeds storage buffer limits");
Require((newWidth + 7u) / 8u <= context.limits.maxComputeWorkGroupCount[0] && (newHeight + 7u) / 8u <= context.limits.maxComputeWorkGroupCount[1], "color transfer exceeds workgroup limits");
if (tiled && width == newWidth && height == newHeight && mode == newMode) return;
auto newTiled = std::make_unique<Buffer>(context, layout.Bytes(), VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_SRC_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT);
auto newLinear = std::make_unique<Buffer>(context, layout.LinearBytes(), VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_SRC_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT);
auto newReadback = std::make_unique<Buffer>(context, layout.Bytes(), VK_BUFFER_USAGE_TRANSFER_DST_BIT, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_CACHED_BIT);
auto newUpload = std::make_unique<Buffer>(context, layout.Bytes(), VK_BUFFER_USAGE_TRANSFER_SRC_BIT);
const std::array<VkDescriptorBufferInfo, 2> buffers{{{newTiled->Handle(), 0, layout.Bytes()}, {newLinear->Handle(), 0, layout.LinearBytes()}}};
constexpr VkBufferUsageFlags copies = VK_BUFFER_USAGE_TRANSFER_SRC_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT;
auto newTiled = std::make_unique<Buffer>(context, layout.Bytes(), copies);
auto newTiledDevice = std::make_unique<DeviceBuffer>(context, layout.Bytes(), copies | VK_BUFFER_USAGE_STORAGE_BUFFER_BIT);
auto newLinear = std::make_unique<DeviceBuffer>(context, layout.LinearBytes(), copies | VK_BUFFER_USAGE_STORAGE_BUFFER_BIT);
const std::array<VkDescriptorBufferInfo, 2> buffers{{{newTiledDevice->Handle(), 0, layout.Bytes()}, {newLinear->Handle(), 0, layout.LinearBytes()}}};
std::array<VkWriteDescriptorSet, 2> writes{};
for (std::uint32_t i = 0; i < writes.size(); ++i) {
writes[i].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET;
@ -88,31 +86,30 @@ void GpuColorTransfer::prepare(std::uint32_t newWidth, std::uint32_t newHeight,
}
context.Function<PFN_vkUpdateDescriptorSets>("vkUpdateDescriptorSets")(context.device, static_cast<std::uint32_t>(writes.size()), writes.data(), 0, nullptr);
tiled = std::move(newTiled);
tiledDevice = std::move(newTiledDevice);
linear = std::move(newLinear);
readback = std::move(newReadback);
upload = std::move(newUpload);
width = newWidth;
height = newHeight;
mode = newMode;
}
void GpuColorTransfer::Upload(std::uint64_t address, std::uint32_t newWidth, std::uint32_t newHeight, ColorTileMode newMode) {
PerformanceTimer timing("ColorTransfer.Upload");
prepare(newWidth, newHeight, newMode);
timing.Mark("prepare");
const ColorTargetLayout layout(width, height, mode);
GuestMemory::Read(address, upload->Bytes(), layout.Alignment());
timing.Mark("guest_read", layout.Bytes());
GuestMemory::Read(address, tiled->Bytes(), layout.Alignment());
}
void GpuColorTransfer::convert(VkCommandBuffer commands, bool toTiled, bool swapRedBlue) {
Require(commands != VK_NULL_HANDLE && tiled && linear, "color transfer is not prepared");
void GpuColorTransfer::convert(VkCommandBuffer commands, bool toTiled, bool swapRedBlue, bool tenBit) {
Require(commands != VK_NULL_HANDLE && tiled && tiledDevice && linear, "color transfer is not prepared");
// The guest bytes move to video memory with one DMA copy; the shader's scattered accesses stay local.
RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_HOST_BIT | VK_PIPELINE_STAGE_TRANSFER_BIT | VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_HOST_WRITE_BIT | VK_ACCESS_TRANSFER_WRITE_BIT | VK_ACCESS_TRANSFER_READ_BIT | VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT, VK_ACCESS_TRANSFER_READ_BIT | VK_ACCESS_TRANSFER_WRITE_BIT);
CopyBuffer(context, commands, tiled->Handle(), 0, tiledDevice->Handle(), 0, tiledDevice->Size());
VkMemoryBarrier before{VK_STRUCTURE_TYPE_MEMORY_BARRIER};
before.srcAccessMask = VK_ACCESS_HOST_WRITE_BIT | VK_ACCESS_TRANSFER_WRITE_BIT | VK_ACCESS_TRANSFER_READ_BIT | VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT;
before.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT | VK_ACCESS_TRANSFER_READ_BIT | VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT;
before.dstAccessMask = VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT;
const auto barrier = context.Function<PFN_vkCmdPipelineBarrier>("vkCmdPipelineBarrier");
barrier(commands, VK_PIPELINE_STAGE_HOST_BIT | VK_PIPELINE_STAGE_TRANSFER_BIT | VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, 0, 1, &before, 0, nullptr, 0, nullptr);
const std::array<std::uint32_t, 4> push{width, height, (width + 127u) / 128u, (toTiled ? 1u : 0u) | (swapRedBlue ? 2u : 0u) | (mode == ColorTileMode::RenderTarget ? 4u : 0u)};
barrier(commands, VK_PIPELINE_STAGE_TRANSFER_BIT | VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, 0, 1, &before, 0, nullptr, 0, nullptr);
const std::array<std::uint32_t, 4> push{width, height, (width + 127u) / 128u, (toTiled ? 1u : 0u) | (swapRedBlue ? 2u : 0u) | (mode == ColorTileMode::RenderTarget ? 4u : 0u) | (tenBit ? 8u : 0u)};
context.Function<PFN_vkCmdBindPipeline>("vkCmdBindPipeline")(commands, VK_PIPELINE_BIND_POINT_COMPUTE, pipeline);
context.Function<PFN_vkCmdBindDescriptorSets>("vkCmdBindDescriptorSets")(commands, VK_PIPELINE_BIND_POINT_COMPUTE, pipelineLayout, 0, 1, &descriptorSet, 0, nullptr);
context.Function<PFN_vkCmdPushConstants>("vkCmdPushConstants")(commands, pipelineLayout, VK_SHADER_STAGE_COMPUTE_BIT, 0, sizeof(push), push.data());
@ -121,32 +118,24 @@ void GpuColorTransfer::convert(VkCommandBuffer commands, bool toTiled, bool swap
after.srcAccessMask = VK_ACCESS_SHADER_WRITE_BIT;
after.dstAccessMask = VK_ACCESS_TRANSFER_READ_BIT;
barrier(commands, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 1, &after, 0, nullptr, 0, nullptr);
if (toTiled) {
CopyBuffer(context, commands, tiledDevice->Handle(), 0, tiled->Handle(), 0, tiledDevice->Size());
RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_HOST_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_HOST_READ_BIT);
}
}
void GpuColorTransfer::Detile(VkCommandBuffer commands, bool swapRedBlue) {
Require(commands != VK_NULL_HANDLE && upload && tiled, "color upload is not prepared");
VkMemoryBarrier before{VK_STRUCTURE_TYPE_MEMORY_BARRIER};
before.srcAccessMask = VK_ACCESS_HOST_WRITE_BIT | VK_ACCESS_MEMORY_READ_BIT | VK_ACCESS_MEMORY_WRITE_BIT;
before.dstAccessMask = VK_ACCESS_TRANSFER_READ_BIT | VK_ACCESS_TRANSFER_WRITE_BIT;
context.Function<PFN_vkCmdPipelineBarrier>("vkCmdPipelineBarrier")(commands, VK_PIPELINE_STAGE_HOST_BIT | VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 1, &before, 0, nullptr, 0, nullptr);
const VkBufferCopy copy{0, 0, upload->Bytes().size()};
context.Function<PFN_vkCmdCopyBuffer>("vkCmdCopyBuffer")(commands, upload->Handle(), tiled->Handle(), 1, &copy);
convert(commands, false, swapRedBlue);
void GpuColorTransfer::Detile(VkCommandBuffer commands, bool swapRedBlue, bool tenBit) {
convert(commands, false, swapRedBlue, tenBit);
}
void GpuColorTransfer::Tile(VkCommandBuffer commands) {
convert(commands, true, false);
VkMemoryBarrier before{VK_STRUCTURE_TYPE_MEMORY_BARRIER};
before.srcAccessMask = VK_ACCESS_SHADER_WRITE_BIT;
before.dstAccessMask = VK_ACCESS_TRANSFER_READ_BIT;
const auto barrier = context.Function<PFN_vkCmdPipelineBarrier>("vkCmdPipelineBarrier");
barrier(commands, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 1, &before, 0, nullptr, 0, nullptr);
const VkBufferCopy copy{0, 0, readback->Bytes().size()};
context.Function<PFN_vkCmdCopyBuffer>("vkCmdCopyBuffer")(commands, tiled->Handle(), readback->Handle(), 1, &copy);
VkMemoryBarrier after{VK_STRUCTURE_TYPE_MEMORY_BARRIER};
after.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
after.dstAccessMask = VK_ACCESS_HOST_READ_BIT;
barrier(commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_HOST_BIT, 0, 1, &after, 0, nullptr, 0, nullptr);
}
void GpuColorTransfer::WriteBack(std::uint64_t address) {
Require(tiled != nullptr, "color transfer is not prepared for writeback");
const ColorTargetLayout layout(width, height, mode);
GuestMemory::Write(address, tiled->Bytes(), layout.Alignment());
}
VkBuffer GpuColorTransfer::LinearBuffer() const {
@ -154,13 +143,6 @@ VkBuffer GpuColorTransfer::LinearBuffer() const {
return linear->Handle();
}
bool GpuColorTransfer::MatchesGuest(std::uint64_t address) {
Require(readback != nullptr, "color readback is unavailable");
const auto bytes = readback->Bytes();
GuestMemory::CheckRange(reinterpret_cast<const void*>(address), bytes.size(), 1);
return std::memcmp(reinterpret_cast<const void*>(address), bytes.data(), bytes.size()) == 0;
}
RenderTarget& GpuColorTransfer::Target(const ColorTarget& color, bool blending) {
if (!target || targetExtent.width != color.extent.width || targetExtent.height != color.extent.height || targetFormat != color.format || targetBlending != blending) {
auto replacement = std::make_unique<RenderTarget>(context, color, blending);

View File

@ -1,138 +0,0 @@
#include "prx/libSceAgcDriver/Graphics/include/GraphicsPipelineCache.hpp"
#include "prx/libSceAgcDriver/Graphics/include/VertexInput.hpp"
#include "prx/libSceAgcDriver/Execution/include/PerformanceTimer.hpp"
#include <type_traits>
#include <algorithm>
#include <iterator>
namespace AgcDriver::Graphics {
namespace {
template<typename TValue>
void append(std::string& key, const TValue& value) {
static_assert(std::is_trivially_copyable_v<TValue>);
key.append(reinterpret_cast<const char*>(&value), sizeof(value));
}
std::string makeKey(const Context& context, const State& state, const std::shared_ptr<ResidentColor>& target, const ShaderResources& resources, std::span<const CompiledShader> shaders) {
PerformanceTimer timing("Graphics.PipelineKey");
std::string key;
key.reserve(512);
append(key, target ? target->Target().View() : VK_NULL_HANDLE);
append(key, state.hasColorTarget);
append(key, state.rectList);
append(key, state.renderExtent.width);
append(key, state.renderExtent.height);
append(key, state.topology);
append(key, state.viewport.x);
append(key, state.viewport.y);
append(key, state.viewport.width);
append(key, state.viewport.height);
append(key, state.viewport.minDepth);
append(key, state.viewport.maxDepth);
append(key, state.negativeOneToOne);
append(key, state.scissor.offset.x);
append(key, state.scissor.offset.y);
append(key, state.scissor.extent.width);
append(key, state.scissor.extent.height);
append(key, state.cullMode);
append(key, state.frontFace);
append(key, state.blend.blendEnable);
append(key, state.blend.srcColorBlendFactor);
append(key, state.blend.dstColorBlendFactor);
append(key, state.blend.colorBlendOp);
append(key, state.blend.srcAlphaBlendFactor);
append(key, state.blend.dstAlphaBlendFactor);
append(key, state.blend.alphaBlendOp);
append(key, state.blend.colorWriteMask);
for (const auto value : state.blendConstants) append(key, value);
append(key, state.stages.mesh.has_value());
append(key, state.stages.tessellation.has_value());
if (state.stages.mesh) {
const auto& mesh = *state.stages.mesh;
append(key, mesh.inputPrimitive);
append(key, mesh.primitivesPerGroup);
append(key, mesh.verticesPerGroup);
append(key, mesh.maxVertices);
append(key, mesh.maxPrimitives);
append(key, mesh.threadsPerGroup);
append(key, mesh.ldsSizeDwords);
append(key, mesh.provokingVertex);
}
if (state.stages.tessellation) {
const auto& tessellation = *state.stages.tessellation;
append(key, tessellation.inputControlPoints);
append(key, tessellation.outputControlPoints);
append(key, tessellation.domain);
append(key, tessellation.partitioning);
append(key, tessellation.outputTopology);
}
timing.Mark("state");
const auto input = BuildVertexInputLayout(context, shaders.front().program->vertexAttributes);
timing.Mark("vertex_layout");
append(key, input.bindings.size());
for (const auto& binding : input.bindings) {
append(key, binding.binding);
append(key, binding.stride);
append(key, binding.inputRate);
}
append(key, input.attributes.size());
for (const auto& attribute : input.attributes) {
append(key, attribute.location);
append(key, attribute.binding);
append(key, attribute.format);
append(key, attribute.offset);
}
append(key, resources.LayoutKey().size());
const auto layout = std::span(resources.LayoutKey());
if (!layout.empty()) key.append(reinterpret_cast<const char*>(layout.data()), layout.size_bytes());
append(key, PushConstantStages(shaders));
append(key, shaders.size());
timing.Mark("resources");
std::uint64_t shaderBytes = 0;
for (const auto& shader : shaders) {
append(key, shader.stage);
append(key, shader.program->spirv.size());
const auto bytes = std::as_bytes(std::span(shader.program->spirv));
if (!bytes.empty()) key.append(reinterpret_cast<const char*>(bytes.data()), bytes.size());
shaderBytes += bytes.size();
}
timing.Mark("shader_code", shaderBytes);
return key;
}
}
std::shared_ptr<Pipeline> GraphicsPipelineCache::Get(const State& state, const std::shared_ptr<ResidentColor>& target, const ShaderResources& resources, std::span<const CompiledShader> shaders) {
PerformanceTimer timing("Graphics.PipelineCache");
auto key = makeKey(context, state, target, resources, shaders);
timing.Mark("key");
const auto found = lookup.find(key);
if (found != lookup.end()) {
const auto it = found->second;
auto pipeline = it->pipeline;
entries.splice(entries.end(), entries, it);
timing.Mark("hit");
return pipeline;
}
timing.Mark("miss");
auto pipeline = std::make_shared<Pipeline>(context, state, target ? &target->Target() : nullptr, resources, shaders);
timing.Mark("create");
entries.push_back({std::move(key), target, pipeline});
try {
const auto it = std::prev(entries.end());
Require(lookup.emplace(it->key, it).second, "duplicate graphics pipeline cache key");
} catch (...) {
entries.pop_back();
throw;
}
while (entries.size() > 128) {
const auto it = std::find_if(entries.begin(), entries.end(), [](const auto& entry) { return entry.pipeline.use_count() == 1; });
if (it == entries.end()) break;
lookup.erase(it->key);
entries.erase(it);
}
return pipeline;
}
}

View File

@ -75,14 +75,17 @@ GuestSamplerResource DecodeSamplerResource(std::span<const std::uint32_t> words)
const auto borderColorType = (words[3] >> 30u) & 0x3u;
Require(!forceUnormCoords, "guest sampler descriptor uses unnormalized coordinates which are not implemented");
Require(anisoThreshold == 0, "guest sampler descriptor uses an anisotropy threshold override which is not implemented");
Require(!forceSrgb, "guest sampler descriptor forces sRGB decoding which is not implemented");
Require(anisoBias == 0, "guest sampler descriptor uses an anisotropy bias which is not implemented");
Require(!truncCoord, "guest sampler descriptor uses coordinate truncation which is not implemented");
// TRUNC_COORD picks point-sampled texels by truncation instead of rounding, and the perf fields
// trade mip/depth precision for speed; Vulkan's nearest filtering already floors, so these only
// move texel selection by half a texel at most and are accepted as is. ANISO_THRESHOLD and
// ANISO_BIAS only tune how many anisotropic taps the hardware takes; the host filter picks its own.
static_cast<void>(truncCoord);
static_cast<void>(anisoThreshold);
static_cast<void>(anisoBias);
Require(!disableCubeWrap, "guest sampler descriptor disables seamless cube filtering which is not implemented");
Require(filterMode == 0, "guest sampler descriptor uses a reduction filter mode which is not implemented");
Require(!disableDegamma, "guest sampler descriptor disables degamma which is not implemented");
Require(perfMip == 0 && perfZ == 0, "guest sampler descriptor uses performance counters which are not implemented");
Require(lodBiasSec == 0, "guest sampler descriptor uses a secondary LOD bias which is not implemented");
Require(!pointPreclamp, "guest sampler descriptor uses point preclamping which is not implemented");
Require(!anisoOverride, "guest sampler descriptor uses an anisotropy override which is not implemented");

View File

@ -1,5 +1,7 @@
#include <cstdio>
#include "prx/libSceAgcDriver/Graphics/include/GuestTextureResource.hpp"
#include "prx/libSceAgcDriver/Graphics/include/Context.hpp"
#include <algorithm>
#include <stdexcept>
#include <string>
@ -17,7 +19,10 @@ TextureTileMode resolveTileMode(std::uint32_t raw) {
case 0x01: return TextureTileMode::kStandard256B;
case 0x05: return TextureTileMode::kStandard4KB;
case 0x09: return TextureTileMode::kStandard64KB;
case 0x1b: return TextureTileMode::RenderTarget64KB;
case 0x18: return TextureTileMode::kZ64KBX;
case 0x19: return TextureTileMode::kS64KBX;
case 0x1a: return TextureTileMode::kD64KBX;
case 0x1b: return TextureTileMode::kR64KBX;
default: throw std::runtime_error("AGC graphics: guest texture descriptor uses an unsupported tile mode " + std::to_string(raw));
}
}
@ -26,6 +31,7 @@ TextureDimension resolveDimension(std::uint32_t raw) {
switch (raw) {
case 8: return TextureDimension::k1D;
case 9: return TextureDimension::k2D;
case 10: return TextureDimension::k3D;
case 11: return TextureDimension::kCube;
case 13: return TextureDimension::k2DArray;
default: throw std::runtime_error("AGC graphics: guest texture descriptor uses an unsupported image type " + std::to_string(raw));
@ -51,7 +57,7 @@ GuestTextureResource DecodeTextureResource(std::span<const std::uint32_t> words)
const auto dstSelZ = (words[3] >> 6u) & 0x7u;
const auto dstSelW = (words[3] >> 9u) & 0x7u;
const auto baseLevel = (words[3] >> 12u) & 0xfu;
const auto lastLevel = (words[3] >> 16u) & 0xfu;
auto lastLevel = (words[3] >> 16u) & 0xfu;
const auto tileModeRaw = (words[3] >> 20u) & 0x1fu;
const auto bcSwizzle = (words[3] >> 25u) & 0x7u;
const auto typeRaw = (words[3] >> 28u) & 0xfu;
@ -83,21 +89,54 @@ GuestTextureResource DecodeTextureResource(std::span<const std::uint32_t> words)
requireValidDstSel(dstSelW);
Require(minLod == 0, "guest texture descriptor uses a nonzero minimum LOD clamp which is not implemented");
Require(minLodWarn == 0, "guest texture descriptor uses a minimum LOD warning threshold which is not implemented");
Require(mipStatsCntId == 0 && !mipStatsCntEn, "guest texture descriptor uses mip statistics counters which are not implemented");
// The LOD warning threshold and mip statistics counters are texture-streaming feedback: the GPU
// reports which mips were wanted. Nothing is reported back here, which only affects what the title
// chooses to stream, not what this access returns.
if (minLodWarn != 0 || mipStatsCntId != 0 || mipStatsCntEn) {
static bool reported = false;
if (!reported) {
reported = true;
std::fprintf(stderr, "[gpu] texture streaming feedback (LOD warning / mip statistics) is not reported\n");
}
}
Require(!cornerSample, "guest texture descriptor uses corner sampling which is not implemented");
Require(!prtDefColor, "guest texture descriptor uses a partially resident default color which is not implemented");
Require(arrayPitch == 0, "guest texture descriptor uses a nonzero array pitch which is not implemented");
Require(!msaaDepth, "guest texture descriptor uses MSAA which is not implemented");
Require(maxUncompBlkSize == 0 && maxCompBlkSize == 0, "guest texture descriptor uses DCC block size overrides which are not implemented");
Require(!metaPipeAligned && !writeCompress && !metaCompress && !dccAlphaPos && !dccColorTransf && metaAddr == 0, "guest texture descriptor uses metadata compression which is not implemented");
// DCC block sizes only apply with metadata compression, which is rejected above.
static_cast<void>(maxUncompBlkSize);
static_cast<void>(maxCompBlkSize);
// Surfaces are always written uncompressed here (render targets and storage images bypass DCC), so
// only the fast-clear keys in DCC metadata change what a read returns (see DccMetadata.hpp).
static_cast<void>(metaPipeAligned);
static_cast<void>(writeCompress);
if (dccColorTransf) {
static bool reported = false;
if (!reported) {
reported = true;
std::fprintf(stderr, "[gpu] texture DCC color transform is ignored (word6=0x%08x word7=0x%08x)\n", words[6], words[7]);
}
}
Require(bcSwizzle == 0, "guest texture descriptor uses a BC swizzle which is not implemented");
Require(baseLevel <= lastLevel, "guest texture descriptor has a base mip level past its last mip level");
Require(lastLevel == maxMip, "guest texture descriptor must expose every mip level down to the last one");
const auto tileMode = resolveTileMode(tileModeRaw);
const auto dimension = resolveDimension(typeRaw);
const auto describe = [&] { return " (format " + std::to_string(format) + ", " + std::to_string(width) + "x" + std::to_string(height) + "x" + std::to_string(depth + 1) + ", type " + std::to_string(typeRaw) + ", tile " + std::to_string(tileModeRaw) + ", mips " + std::to_string(baseLevel) + ".." + std::to_string(lastLevel) + " of " + std::to_string(maxMip + 1) + ")"; };
TextureTileMode tileMode;
TextureDimension dimension;
try {
tileMode = resolveTileMode(tileModeRaw);
dimension = resolveDimension(typeRaw);
} catch (const std::exception& error) {
throw std::runtime_error(std::string(error.what()) + describe());
}
Require(baseLevel <= maxMip, "guest texture descriptor starts past the surface's last mip level" + describe());
// Views may name levels past MAX_MIP (a 512x512 view through 1x1 over a 9-level surface); the
// hardware never addresses them, so the view ends at the surface's last level.
lastLevel = std::min(lastLevel, maxMip);
// XOR swizzles fold a pipe/bank XOR into the low address bits; only unmodified 64 KiB bases are modeled.
Require(XorSwizzleMode(tileMode) == 0 || (baseAddress & 0xffffu) == 0, "guest texture descriptor combines an XOR swizzle with a pipe/bank XOR base which is not implemented");
switch (dimension) {
case TextureDimension::k1D:
@ -106,6 +145,10 @@ GuestTextureResource DecodeTextureResource(std::span<const std::uint32_t> words)
case TextureDimension::k2D:
Require(depth == 0 && baseArray == 0, "guest 2D texture descriptor has a nonzero depth or base array");
break;
case TextureDimension::k3D:
// word4 DEPTH holds the depth minus one; BASE_ARRAY has no meaning for volumes.
Require(baseArray == 0, "guest 3D texture descriptor has a nonzero base array");
break;
case TextureDimension::k2DArray:
Require(baseArray <= depth, "guest 2D array texture descriptor has a base array past its last array slice");
break;
@ -124,6 +167,7 @@ GuestTextureResource DecodeTextureResource(std::span<const std::uint32_t> words)
result.baseArray = baseArray;
result.mipCount = maxMip + 1u;
result.baseLevel = baseLevel;
result.lastLevel = lastLevel;
result.tileMode = tileMode;
result.dimension = dimension;
result.format = format;
@ -131,6 +175,8 @@ GuestTextureResource DecodeTextureResource(std::span<const std::uint32_t> words)
result.dstSelY = static_cast<std::uint8_t>(dstSelY);
result.dstSelZ = static_cast<std::uint8_t>(dstSelZ);
result.dstSelW = static_cast<std::uint8_t>(dstSelW);
result.dccAddress = metaCompress ? metaAddr << 8u : 0u;
result.dccAlphaOnMsb = dccAlphaPos;
return result;
}
@ -138,9 +184,9 @@ bool MatchesGuestDimension(ShaderRecompiler::DescriptorImageShape shape, Texture
switch (shape) {
case ShaderRecompiler::DescriptorImageShape::Image1D: return dimension == TextureDimension::k1D;
case ShaderRecompiler::DescriptorImageShape::Image2D: return dimension == TextureDimension::k2D;
case ShaderRecompiler::DescriptorImageShape::Image2DArray: return dimension == TextureDimension::k2DArray;
case ShaderRecompiler::DescriptorImageShape::Image2DArray: return dimension == TextureDimension::k2DArray || dimension == TextureDimension::kCube;
case ShaderRecompiler::DescriptorImageShape::ImageCube: return dimension == TextureDimension::kCube;
case ShaderRecompiler::DescriptorImageShape::Image3D: return false;
case ShaderRecompiler::DescriptorImageShape::Image3D: return dimension == TextureDimension::k3D;
}
throw std::runtime_error("AGC graphics: MatchesGuestDimension encountered an unknown descriptor image shape");
}

View File

@ -1,16 +1,53 @@
#include "prx/libSceAgcDriver/Graphics/include/Pipeline.hpp"
#include "prx/libSceAgcDriver/Graphics/include/VertexInput.hpp"
#include <algorithm>
#include <array>
#include <chrono>
#include <cmath>
#include <cstdio>
#include <cstdlib>
#include <list>
#include <mutex>
#include <type_traits>
#include <unordered_map>
#include <vector>
namespace AgcDriver::Graphics {
Pipeline::Pipeline(const Context& context, const State& state, const RenderTarget* target, const ShaderResources& resources, std::span<const CompiledShader> shaders) : context(context), _modules(shaders.size()) {
Require((target != nullptr) == state.hasColorTarget, "render target does not match decoded color state");
Require(state.renderExtent.width != 0 && state.renderExtent.height != 0 && state.renderExtent.width <= context.limits.maxFramebufferWidth && state.renderExtent.height <= context.limits.maxFramebufferHeight, "framebuffer extent exceeds device limits");
Framebuffer::Framebuffer(const Context& context, VkRenderPass renderPass, std::span<const VkImageView> targets, VkExtent2D extent) : context(context) {
// Cached objects outlive their device's teardown; they must not keep its buffer pool alive past it.
this->context.bufferPool.reset();
Require(extent.width != 0 && extent.height != 0 && extent.width <= context.limits.maxFramebufferWidth && extent.height <= context.limits.maxFramebufferHeight, "framebuffer extent exceeds device limits");
VkFramebufferCreateInfo framebufferInfo{VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO};
framebufferInfo.renderPass = renderPass;
framebufferInfo.attachmentCount = static_cast<std::uint32_t>(targets.size());
framebufferInfo.pAttachments = targets.empty() ? nullptr : targets.data();
framebufferInfo.width = extent.width;
framebufferInfo.height = extent.height;
framebufferInfo.layers = 1;
Check(context.Function<PFN_vkCreateFramebuffer>("vkCreateFramebuffer")(context.device, &framebufferInfo, nullptr, &framebuffer), "vkCreateFramebuffer");
}
Framebuffer::~Framebuffer() {
if (framebuffer) context.Function<PFN_vkDestroyFramebuffer>("vkDestroyFramebuffer")(context.device, framebuffer, nullptr);
}
void ValidateViewport(const Context& context, const VkViewport& viewport) {
Require(std::isfinite(viewport.minDepth) && std::isfinite(viewport.maxDepth), "non-finite viewport depth range");
Require(context.depthRangeUnrestricted || (viewport.minDepth >= 0 && viewport.minDepth <= 1 && viewport.maxDepth >= 0 && viewport.maxDepth <= 1), "viewport depth outside [0, 1] requires VK_EXT_depth_range_unrestricted");
Require(std::isfinite(viewport.x) && std::isfinite(viewport.y) && std::isfinite(viewport.width) && std::isfinite(viewport.height), "viewport arithmetic overflow");
Require(viewport.width <= context.limits.maxViewportDimensions[0] && std::abs(viewport.height) <= context.limits.maxViewportDimensions[1], "viewport dimensions exceed device limits");
Require(viewport.x >= context.limits.viewportBoundsRange[0] && viewport.x + viewport.width <= context.limits.viewportBoundsRange[1], "viewport X exceeds device bounds");
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<const CompiledShader> shaders) : context(context), _modules(shaders.size()), attachments(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();
Require(state.blends.size() == state.colors.size(), "blend states do not match decoded color state");
Require(state.colors.size() <= context.limits.maxColorAttachments, "color targets exceed device attachment limits");
Require(state.hasColorTarget || (context.limits.framebufferNoAttachmentsSampleCounts & VK_SAMPLE_COUNT_1_BIT) != 0, "device does not support single-sample rendering without attachments");
ValidateShaders(shaders, state, context.subgroup, context.fragmentShaderBarycentric);
Require(!state.negativeOneToOne || context.depthClipControl, "negative-one-to-one depth clipping requires VK_EXT_depth_clip_control with depthClipControl enabled");
if (state.rectList) Require(context.tessellationShader && context.limits.maxTessellationPatchSize >= 4, "rect-list requires tessellation with four output control points");
if (state.stages.tessellation) {
@ -23,13 +60,6 @@ Pipeline::Pipeline(const Context& context, const State& state, const RenderTarge
Require(mesh.threadsPerGroup <= context.meshLimits.maxMeshWorkGroupInvocations && mesh.threadsPerGroup <= context.meshLimits.maxMeshWorkGroupSize[0], "mesh workgroup exceeds device limits");
Require(mesh.maxVertices <= context.meshLimits.maxMeshOutputVertices && mesh.maxPrimitives <= context.meshLimits.maxMeshOutputPrimitives && static_cast<std::uint64_t>(mesh.ldsSizeDwords) * 4 <= context.meshLimits.maxMeshSharedMemorySize, "mesh output or LDS exceeds device limits");
}
const auto& viewport = state.viewport;
Require(std::isfinite(viewport.minDepth) && std::isfinite(viewport.maxDepth), "non-finite viewport depth range");
Require(context.depthRangeUnrestricted || (viewport.minDepth >= 0 && viewport.minDepth <= 1 && viewport.maxDepth >= 0 && viewport.maxDepth <= 1), "viewport depth outside [0, 1] requires VK_EXT_depth_range_unrestricted");
Require(std::isfinite(viewport.x) && std::isfinite(viewport.y) && std::isfinite(viewport.width) && std::isfinite(viewport.height), "viewport arithmetic overflow");
Require(viewport.width <= context.limits.maxViewportDimensions[0] && std::abs(viewport.height) <= context.limits.maxViewportDimensions[1], "viewport dimensions exceed device limits");
Require(viewport.x >= context.limits.viewportBoundsRange[0] && viewport.x + viewport.width <= context.limits.viewportBoundsRange[1], "viewport X exceeds device bounds");
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");
const auto pushStages = PushConstantStages(shaders);
Require(pushStages == 0 || context.limits.maxPushConstantsSize >= PipelinePushConstantBytes, "graphics push constant range exceeds device limit");
try {
@ -46,6 +76,8 @@ Pipeline::Pipeline(const Context& context, const State& state, const RenderTarge
stages[i].module = _modules[i];
stages[i].pName = "main";
}
// A descriptor set layout with the same bindings as this one is compatible with the pipeline
// layout, so later draws bind their own ShaderResources' set under it.
const auto setLayout = resources.Layout();
const VkPushConstantRange push{pushStages, 0, PipelinePushConstantBytes};
VkPipelineLayoutCreateInfo layoutInfo{VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO};
@ -54,61 +86,61 @@ Pipeline::Pipeline(const Context& context, const State& state, const RenderTarge
layoutInfo.pushConstantRangeCount = pushStages != 0 ? 1 : 0;
layoutInfo.pPushConstantRanges = pushStages != 0 ? &push : nullptr;
Check(context.Function<PFN_vkCreatePipelineLayout>("vkCreatePipelineLayout")(context.device, &layoutInfo, nullptr, &layout), "vkCreatePipelineLayout graphics");
VkAttachmentDescription color{};
color.format = state.color.format;
color.samples = VK_SAMPLE_COUNT_1_BIT;
color.loadOp = VK_ATTACHMENT_LOAD_OP_LOAD;
color.storeOp = VK_ATTACHMENT_STORE_OP_STORE;
color.stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE;
color.stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE;
color.initialLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;
color.finalLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;
VkAttachmentReference reference{0, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL};
std::vector<VkAttachmentDescription> colors;
std::vector<VkAttachmentReference> references;
for (std::uint32_t index = 0; index < state.colors.size(); ++index) {
VkAttachmentDescription color{};
color.format = state.colors[index].format;
color.samples = VK_SAMPLE_COUNT_1_BIT;
color.loadOp = VK_ATTACHMENT_LOAD_OP_LOAD;
color.storeOp = VK_ATTACHMENT_STORE_OP_STORE;
color.stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE;
color.stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE;
color.initialLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;
color.finalLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;
colors.push_back(color);
references.push_back({index, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL});
}
VkSubpassDescription subpass{};
subpass.pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS;
subpass.colorAttachmentCount = state.hasColorTarget ? 1 : 0;
subpass.pColorAttachments = state.hasColorTarget ? &reference : nullptr;
subpass.colorAttachmentCount = static_cast<std::uint32_t>(references.size());
subpass.pColorAttachments = references.empty() ? nullptr : references.data();
VkRenderPassCreateInfo passInfo{VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO};
passInfo.attachmentCount = state.hasColorTarget ? 1 : 0;
passInfo.pAttachments = state.hasColorTarget ? &color : nullptr;
passInfo.attachmentCount = static_cast<std::uint32_t>(colors.size());
passInfo.pAttachments = colors.empty() ? nullptr : colors.data();
passInfo.subpassCount = 1;
passInfo.pSubpasses = &subpass;
Check(context.Function<PFN_vkCreateRenderPass>("vkCreateRenderPass")(context.device, &passInfo, nullptr, &renderPass), "vkCreateRenderPass");
const auto view = target ? target->View() : VK_NULL_HANDLE;
VkFramebufferCreateInfo framebufferInfo{VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO};
framebufferInfo.renderPass = renderPass;
framebufferInfo.attachmentCount = state.hasColorTarget ? 1 : 0;
framebufferInfo.pAttachments = state.hasColorTarget ? &view : nullptr;
framebufferInfo.width = state.renderExtent.width;
framebufferInfo.height = state.renderExtent.height;
framebufferInfo.layers = 1;
Check(context.Function<PFN_vkCreateFramebuffer>("vkCreateFramebuffer")(context.device, &framebufferInfo, nullptr, &framebuffer), "vkCreateFramebuffer");
VkPipelineVertexInputStateCreateInfo input{VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO};
const auto vertexInput = BuildVertexInputLayout(context, shaders.front().program->vertexAttributes);
input.vertexBindingDescriptionCount = static_cast<std::uint32_t>(vertexInput.bindings.size());
input.pVertexBindingDescriptions = vertexInput.bindings.data();
input.vertexAttributeDescriptionCount = static_cast<std::uint32_t>(vertexInput.attributes.size());
input.pVertexAttributeDescriptions = vertexInput.attributes.data();
VkPipelineInputAssemblyStateCreateInfo assembly{VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO};
assembly.topology = state.topology;
// Viewport and scissor are set per draw (Begin), so they do not multiply pipelines; the depth
// clip control stays baked in.
VkPipelineViewportStateCreateInfo viewports{VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO};
VkPipelineViewportDepthClipControlCreateInfoEXT depthClip{VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_DEPTH_CLIP_CONTROL_CREATE_INFO_EXT};
depthClip.negativeOneToOne = state.negativeOneToOne;
if (state.negativeOneToOne) viewports.pNext = &depthClip;
viewports.viewportCount = 1;
viewports.pViewports = &state.viewport;
viewports.scissorCount = 1;
viewports.pScissors = &state.scissor;
const std::array<VkDynamicState, 2> dynamicStates{VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR};
VkPipelineDynamicStateCreateInfo dynamic{VK_STRUCTURE_TYPE_PIPELINE_DYNAMIC_STATE_CREATE_INFO};
dynamic.dynamicStateCount = static_cast<std::uint32_t>(dynamicStates.size());
dynamic.pDynamicStates = dynamicStates.data();
VkPipelineRasterizationStateCreateInfo raster{VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO};
raster.polygonMode = VK_POLYGON_MODE_FILL;
raster.depthClampEnable = state.depthClamp && context.depthClamp ? VK_TRUE : VK_FALSE;
raster.cullMode = state.cullMode;
raster.frontFace = state.frontFace;
raster.lineWidth = 1;
VkPipelineMultisampleStateCreateInfo samples{VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO};
samples.rasterizationSamples = VK_SAMPLE_COUNT_1_BIT;
VkPipelineColorBlendStateCreateInfo blend{VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO};
blend.attachmentCount = state.hasColorTarget ? 1 : 0;
blend.pAttachments = state.hasColorTarget ? &state.blend : nullptr;
blend.attachmentCount = static_cast<std::uint32_t>(state.blends.size());
blend.pAttachments = state.blends.empty() ? nullptr : state.blends.data();
std::copy(state.blendConstants.begin(), state.blendConstants.end(), blend.blendConstants);
VkGraphicsPipelineCreateInfo pipelineInfo{VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO};
pipelineInfo.stageCount = static_cast<std::uint32_t>(stages.size());
@ -124,6 +156,7 @@ Pipeline::Pipeline(const Context& context, const State& state, const RenderTarge
pipelineInfo.pRasterizationState = &raster;
pipelineInfo.pMultisampleState = &samples;
pipelineInfo.pColorBlendState = &blend;
pipelineInfo.pDynamicState = &dynamic;
pipelineInfo.layout = layout;
pipelineInfo.renderPass = renderPass;
Check(context.Function<PFN_vkCreateGraphicsPipelines>("vkCreateGraphicsPipelines")(context.device, context.pipelineCache, 1, &pipelineInfo, nullptr, &pipeline), "vkCreateGraphicsPipelines");
@ -138,26 +171,83 @@ Pipeline::~Pipeline() {
}
void Pipeline::release() noexcept {
framebuffers.clear();
if (pipeline) context.Function<PFN_vkDestroyPipeline>("vkDestroyPipeline")(context.device, pipeline, nullptr);
if (framebuffer) context.Function<PFN_vkDestroyFramebuffer>("vkDestroyFramebuffer")(context.device, framebuffer, nullptr);
if (renderPass) context.Function<PFN_vkDestroyRenderPass>("vkDestroyRenderPass")(context.device, renderPass, nullptr);
if (layout) context.Function<PFN_vkDestroyPipelineLayout>("vkDestroyPipelineLayout")(context.device, layout, nullptr);
for (auto module : _modules) {
if (module) context.Function<PFN_vkDestroyShaderModule>("vkDestroyShaderModule")(context.device, module, nullptr);
}
pipeline = VK_NULL_HANDLE;
renderPass = VK_NULL_HANDLE;
layout = VK_NULL_HANDLE;
_modules.clear();
}
void Pipeline::Abandon() noexcept {
for (auto& entry : framebuffers) entry.framebuffer->Abandon();
framebuffers.clear();
pipeline = VK_NULL_HANDLE;
renderPass = VK_NULL_HANDLE;
layout = VK_NULL_HANDLE;
_modules.clear();
}
void Framebuffer::Abandon() noexcept {
framebuffer = VK_NULL_HANDLE;
}
VkPipelineLayout Pipeline::Layout() const {
return layout;
}
void Pipeline::Begin(VkCommandBuffer commands, VkExtent2D extent) const {
std::shared_ptr<Framebuffer> Pipeline::AcquireFramebuffer(std::span<const VkImageView> targets, std::span<const std::shared_ptr<StorageTexture>> owners, VkExtent2D extent) {
Require(targets.size() == attachments && targets.size() == owners.size(), "render targets do not match the pipeline's color 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
// them (Kept keeps its framebuffer until the batch completes).
std::erase_if(framebuffers, [](const CachedFramebuffer& entry) {
return entry.framebuffer.use_count() == 1 && std::any_of(entry.owners.begin(), entry.owners.end(), [](const auto& owner) { return owner.expired(); });
});
for (auto it = framebuffers.begin(); it != framebuffers.end(); ++it) {
if (it->extent.width != extent.width || it->extent.height != extent.height || !std::equal(it->views.begin(), it->views.end(), targets.begin(), targets.end())) continue;
// View handles are recycled once a StorageTexture is destroyed, so the owners must be the
// very objects the views were made for.
bool same = true;
for (std::size_t i = 0; i < owners.size() && same; ++i) same = it->owners[i].lock().get() == owners[i].get();
if (!same) continue;
std::rotate(it, std::next(it), framebuffers.end());
return framebuffers.back().framebuffer;
}
}
auto framebuffer = std::make_shared<Framebuffer>(context, renderPass, targets, extent);
if (!resident) return framebuffer;
// Beyond the bound the least recently used unreferenced entry goes.
constexpr std::size_t bound = 8;
while (framebuffers.size() >= bound) {
const auto victim = std::find_if(framebuffers.begin(), framebuffers.end(), [](const CachedFramebuffer& entry) { return entry.framebuffer.use_count() == 1; });
if (victim == framebuffers.end()) break;
framebuffers.erase(victim);
}
CachedFramebuffer entry;
entry.views.assign(targets.begin(), targets.end());
entry.owners.assign(owners.begin(), owners.end());
entry.extent = extent;
entry.framebuffer = framebuffer;
framebuffers.push_back(std::move(entry));
return framebuffer;
}
void Pipeline::Begin(VkCommandBuffer commands, const Framebuffer& framebuffer, VkExtent2D extent, const VkViewport& viewport, const VkRect2D& scissor) const {
VkRenderPassBeginInfo begin{VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO};
begin.renderPass = renderPass;
begin.framebuffer = framebuffer;
begin.framebuffer = framebuffer.Handle();
begin.renderArea = {{0, 0}, extent};
context.Function<PFN_vkCmdBeginRenderPass>("vkCmdBeginRenderPass")(commands, &begin, VK_SUBPASS_CONTENTS_INLINE);
context.Function<PFN_vkCmdBindPipeline>("vkCmdBindPipeline")(commands, VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline);
context.Function<PFN_vkCmdSetViewport>("vkCmdSetViewport")(commands, 0, 1, &viewport);
context.Function<PFN_vkCmdSetScissor>("vkCmdSetScissor")(commands, 0, 1, &scissor);
}
void Pipeline::PushConstants(VkCommandBuffer commands, std::span<const CompiledShader> shaders) const {
@ -167,4 +257,211 @@ void Pipeline::PushConstants(VkCommandBuffer commands, std::span<const CompiledS
context.Function<PFN_vkCmdPushConstants>("vkCmdPushConstants")(commands, layout, stages, 0, PipelinePushConstantBytes, bytes.data());
}
namespace {
template<typename TValue>
void append(std::vector<std::byte>& key, const TValue& value) {
static_assert(std::is_trivially_copyable_v<TValue>);
const auto bytes = std::as_bytes(std::span(&value, 1));
key.insert(key.end(), bytes.begin(), bytes.end());
}
// Everything the Pipeline objects are built from, or empty when a stage's result has no variant id
// (the recompiler could not identify it, so nothing else may share its pipeline). The rect-list
// control and evaluation stages are generated from the vertex and fragment results, which the key
// already names, so they carry no id of their own.
std::vector<std::byte> pipelineKey(const Context& context, const State& state, const VertexInputLayout& input, const ShaderResources& resources, std::span<const CompiledShader> shaders) {
using Stage = ShaderRecompiler::ShaderStage;
std::vector<std::byte> key;
append(key, context.device);
append(key, shaders.size());
for (const auto& shader : shaders) {
Require(shader.program != nullptr, "missing compiled shader");
const bool generated = state.rectList && (shader.stage == Stage::TessellationControl || shader.stage == Stage::TessellationEvaluation);
if (!generated && shader.program->variantId == 0) return {};
append(key, shader.stage);
append(key, generated ? std::uint64_t{0} : shader.program->variantId);
// Where the stage's push constants sit in the block (AssemblePushConstants).
append(key, shader.pushConstantOffset);
}
append(key, PushConstantStages(shaders));
append(key, input.bindings.size());
for (const auto& binding : input.bindings) {
append(key, binding.binding);
append(key, binding.stride);
append(key, binding.inputRate);
}
append(key, input.attributes.size());
for (const auto& attribute : input.attributes) {
append(key, attribute.location);
append(key, attribute.binding);
append(key, attribute.format);
append(key, attribute.offset);
}
append(key, resources.LayoutKey().size());
for (const auto word : resources.LayoutKey()) append(key, word);
append(key, state.hasColorTarget);
append(key, state.rectList);
append(key, state.topology);
append(key, state.cullMode);
append(key, state.frontFace);
append(key, state.negativeOneToOne);
append(key, state.depthClamp && context.depthClamp);
append(key, state.blends.size());
for (const auto& blend : state.blends) append(key, blend);
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.stages.mesh.has_value());
if (state.stages.mesh) {
const auto& mesh = *state.stages.mesh;
append(key, mesh.inputPrimitive);
append(key, mesh.primitivesPerGroup);
append(key, mesh.verticesPerGroup);
append(key, mesh.maxVertices);
append(key, mesh.maxPrimitives);
append(key, mesh.threadsPerGroup);
append(key, mesh.ldsSizeDwords);
append(key, mesh.provokingVertex);
}
append(key, state.stages.tessellation.has_value());
if (state.stages.tessellation) {
const auto& tessellation = *state.stages.tessellation;
append(key, tessellation.inputControlPoints);
append(key, tessellation.outputControlPoints);
append(key, tessellation.domain);
append(key, tessellation.partitioning);
append(key, tessellation.outputTopology);
}
return key;
}
std::uint64_t hashKey(const std::vector<std::byte>& key) {
std::uint64_t hash = 14695981039346656037ull;
for (const auto byte : key) {
hash ^= static_cast<std::uint8_t>(byte);
hash *= 1099511628211ull;
}
return hash;
}
struct PipelineStore {
struct Entry {
VkDevice device;
// The device's buffer pool at insertion: it is made and reset with the device, so it tells
// the device instance apart from a later one the loader gave the same handle value.
std::weak_ptr<BufferPool> pool;
std::uint64_t hash;
std::vector<std::byte> key;
std::shared_ptr<Pipeline> pipeline;
};
std::mutex mutex;
// Least recently used first.
std::list<Entry> entries;
std::unordered_map<std::uint64_t, std::list<Entry>::iterator> index;
std::uint64_t hits = 0;
std::uint64_t misses = 0;
std::uint64_t uncached = 0;
std::uint64_t evicted = 0;
std::chrono::steady_clock::time_point lastReport = std::chrono::steady_clock::now();
};
// Never destroyed: the pipelines belong to a device that may already be gone when statics die, and
// the device's teardown (ClearCachedPipelines) is the place to destroy them.
PipelineStore& Pipelines() {
static auto* store = new PipelineStore();
return *store;
}
// Whether the entry's objects belong to the device the context names. A context without a buffer
// pool (tests) is identified by the handle alone.
bool alive(const PipelineStore::Entry& entry, const Context& context) {
if (entry.device != context.device) return false;
return context.bufferPool == nullptr || entry.pool.lock() == context.bufferPool;
}
// Drops an entry of a device that is gone: its objects went with the device, so they are forgotten,
// not destroyed.
std::list<PipelineStore::Entry>::iterator abandon(PipelineStore& store, std::list<PipelineStore::Entry>::iterator it) {
it->pipeline->Abandon();
store.index.erase(it->hash);
return store.entries.erase(it);
}
void reportPipelines(PipelineStore& store) {
static const bool profile = std::getenv("APS5_PROFILE_DRAW") != nullptr;
if (!profile) return;
const auto now = std::chrono::steady_clock::now();
if (now - store.lastReport < std::chrono::seconds(10)) return;
store.lastReport = now;
const auto lookups = store.hits + store.misses + store.uncached;
std::fprintf(stderr, "[pipecache] %llu lookups over 10 s: %llu hits (%.0f%%), %llu misses, %llu private (no variant id), %llu evicted, %zu cached\n", static_cast<unsigned long long>(lookups), static_cast<unsigned long long>(store.hits), lookups != 0 ? 100.0 * static_cast<double>(store.hits) / static_cast<double>(lookups) : 0.0, static_cast<unsigned long long>(store.misses), static_cast<unsigned long long>(store.uncached), static_cast<unsigned long long>(store.evicted), store.entries.size());
store.hits = store.misses = store.uncached = store.evicted = 0;
}
}
std::shared_ptr<Pipeline> CachedPipeline(const Context& context, const State& state, const VertexInputLayout& vertexInput, const ShaderResources& resources, std::span<const CompiledShader> shaders) {
static const bool disabled = std::getenv("APS5_NO_PIPELINE_CACHE") != nullptr;
if (disabled) return std::make_shared<Pipeline>(context, state, vertexInput, resources, shaders);
auto& store = Pipelines();
std::lock_guard lock(store.mutex);
reportPipelines(store);
const auto key = pipelineKey(context, state, vertexInput, resources, shaders);
if (key.empty()) {
++store.uncached;
return std::make_shared<Pipeline>(context, state, vertexInput, resources, shaders);
}
const auto hash = hashKey(key);
if (const auto found = store.index.find(hash); found != store.index.end()) {
const auto it = found->second;
if (it->key == key) {
// The key names the device handle, which the loader may reuse for a device created after
// this one was destroyed without ClearCachedPipelines: such an entry is a miss.
if (alive(*it, context)) {
++store.hits;
store.entries.splice(store.entries.end(), store.entries, it);
return it->pipeline;
}
abandon(store, it);
} else {
// A different configuration with the same hash keeps the resident entry; this one stays private.
++store.uncached;
return std::make_shared<Pipeline>(context, state, vertexInput, resources, shaders);
}
}
++store.misses;
// Entries of another device belong to one the driver replaced (it does so under the GpuMutex
// before any draw reaches the new device), whose objects went with it: forget them.
for (auto it = store.entries.begin(); it != store.entries.end();) {
it = alive(*it, context) ? std::next(it) : abandon(store, it);
}
auto pipeline = std::make_shared<Pipeline>(context, state, vertexInput, resources, shaders);
store.entries.push_back({context.device, context.bufferPool, hash, key, pipeline});
store.index[hash] = std::prev(store.entries.end());
constexpr std::size_t bound = 256;
while (store.entries.size() > bound) {
// Only an entry no recorded draw still holds may go (Kept keeps its shared_ptr until the fence).
const auto victim = std::find_if(store.entries.begin(), store.entries.end(), [](const PipelineStore::Entry& entry) { return entry.pipeline.use_count() == 1; });
if (victim == store.entries.end()) break;
store.index.erase(victim->hash);
store.entries.erase(victim);
++store.evicted;
}
return pipeline;
}
void ClearCachedPipelines(VkDevice device) {
auto& store = Pipelines();
std::lock_guard lock(store.mutex);
for (auto it = store.entries.begin(); it != store.entries.end();) {
if (it->device != device) {
++it;
continue;
}
store.index.erase(it->hash);
it = store.entries.erase(it);
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -1,21 +0,0 @@
#include "prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp"
#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp"
namespace AgcDriver::Graphics {
void GuestBufferMemory::AcquireRegistered() {
Require(!uploaded && regions.empty() && lease.empty(), "guest allocation lease must precede resource registration");
lease = GuestAllocations::GuestAllocationsAcquire_nid_postfix();
regions.reserve(lease.size());
for (const auto& range : lease) {
validate(range->address, range->bytes);
std::vector<std::byte> snapshot;
if (!range->writable) {
snapshot.resize(range->bytes);
GuestMemory::Read(range->address, snapshot);
}
regions.push_back({range->address, range->address + range->bytes, range->writable, std::move(snapshot), nullptr});
}
}
}

View File

@ -1,11 +1,13 @@
#include "prx/libSceAgcDriver/Graphics/include/Resources.hpp"
#include "prx/libSceAgcDriver/Graphics/include/BufferPool.hpp"
#include "prx/libSceAgcDriver/Graphics/include/Recorder.hpp"
#include "prx/libSceAgcDriver/Execution/include/PerformanceTimer.hpp"
#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp"
#include <exception>
namespace AgcDriver::Graphics {
Buffer::Buffer(const Context& context, std::size_t size, VkBufferUsageFlags usage, VkMemoryPropertyFlags properties) : context(context), size(size), usage(usage), properties(properties) {
Buffer::Buffer(const Context& context, std::size_t size, VkBufferUsageFlags usage, VkMemoryPropertyFlags properties) : context(context), size(size), capacity(BufferPool::Capacity(size)), usage(usage), properties(properties) {
Require(size != 0, "zero-sized GPU buffer");
const bool addressable = (usage & VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT) != 0;
Require(!addressable || context.bufferDeviceAddress, "buffer device address is not enabled");
@ -16,12 +18,11 @@ Buffer::Buffer(const Context& context, std::size_t size, VkBufferUsageFlags usag
mapping = allocation->mapping;
deviceAddress = allocation->address;
allocationBytes = allocation->allocationBytes;
reusable = true;
return;
}
try {
VkBufferCreateInfo info{VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO};
info.size = size;
info.size = capacity;
info.usage = usage;
info.sharingMode = VK_SHARING_MODE_EXCLUSIVE;
Check(context.Function<PFN_vkCreateBuffer>("vkCreateBuffer")(context.device, &info, nullptr, &buffer), "vkCreateBuffer");
@ -32,12 +33,22 @@ Buffer::Buffer(const Context& context, std::size_t size, VkBufferUsageFlags usag
if (addressable) allocation.pNext = &flags;
allocation.allocationSize = requirements.size;
allocationBytes = requirements.size;
allocation.memoryTypeIndex = context.MemoryType(requirements.memoryTypeBits, properties);
// The CPU reads most of these buffers back (write-back, diffs), which is very slow from
// write-combined memory, so the default host properties prefer cached host memory.
constexpr VkMemoryPropertyFlags hostDefault = VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT;
if (properties == hostDefault) {
try {
allocation.memoryTypeIndex = context.MemoryType(requirements.memoryTypeBits, hostDefault | VK_MEMORY_PROPERTY_HOST_CACHED_BIT);
} catch (const std::runtime_error&) {
allocation.memoryTypeIndex = context.MemoryType(requirements.memoryTypeBits, hostDefault);
}
} else {
allocation.memoryTypeIndex = context.MemoryType(requirements.memoryTypeBits, properties);
}
Check(context.Function<PFN_vkAllocateMemory>("vkAllocateMemory")(context.device, &allocation, nullptr, &memory), "vkAllocateMemory buffer");
Check(context.Function<PFN_vkBindBufferMemory>("vkBindBufferMemory")(context.device, buffer, memory, 0), "vkBindBufferMemory");
initializeAddress(usage);
if ((properties & VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT) != 0) Check(context.Function<PFN_vkMapMemory>("vkMapMemory")(context.device, memory, 0, VK_WHOLE_SIZE, 0, &mapping), "vkMapMemory");
reusable = true;
Check(context.Function<PFN_vkMapMemory>("vkMapMemory")(context.device, memory, 0, VK_WHOLE_SIZE, 0, &mapping), "vkMapMemory");
} catch (...) {
release();
throw;
@ -49,8 +60,8 @@ Buffer::~Buffer() {
}
void Buffer::release() noexcept {
if (reusable && buffer && memory && cache) {
cache->Put({buffer, memory, mapping, deviceAddress, allocationBytes, size, usage, properties});
if (mapping && buffer && memory && cache) {
cache->Put({buffer, memory, mapping, deviceAddress, allocationBytes, capacity, usage, properties});
return;
}
if (mapping) context.Function<PFN_vkUnmapMemory>("vkUnmapMemory")(context.device, memory);
@ -63,18 +74,76 @@ VkBuffer Buffer::Handle() const {
}
std::span<std::byte> Buffer::Bytes() {
Require(mapping != nullptr, "GPU-only buffer has no CPU mapping");
return {static_cast<std::byte*>(mapping), size};
}
void Buffer::Invalidate() {
Require(mapping != nullptr, "cannot invalidate an unmapped GPU buffer");
VkMappedMemoryRange range{VK_STRUCTURE_TYPE_MAPPED_MEMORY_RANGE};
range.memory = memory;
range.size = VK_WHOLE_SIZE;
Check(context.Function<PFN_vkInvalidateMappedMemoryRanges>("vkInvalidateMappedMemoryRanges")(context.device, 1, &range), "vkInvalidateMappedMemoryRanges");
}
DeviceBuffer::DeviceBuffer(const Context& context, std::size_t size, VkBufferUsageFlags usage) : context(context), size(size), capacity(BufferPool::Capacity(size)), usage(usage) {
Require(size != 0, "zero-sized device buffer");
cache = GetBufferPool(context);
if (const auto allocation = cache->Take(size, usage, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT)) {
buffer = allocation->buffer;
memory = allocation->memory;
allocationBytes = allocation->allocationBytes;
return;
}
try {
VkBufferCreateInfo info{VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO};
info.size = capacity;
info.usage = usage;
info.sharingMode = VK_SHARING_MODE_EXCLUSIVE;
Check(context.Function<PFN_vkCreateBuffer>("vkCreateBuffer")(context.device, &info, nullptr, &buffer), "vkCreateBuffer device");
VkMemoryRequirements requirements{};
context.Function<PFN_vkGetBufferMemoryRequirements>("vkGetBufferMemoryRequirements")(context.device, buffer, &requirements);
VkMemoryAllocateInfo allocation{VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO};
allocation.allocationSize = requirements.size;
allocationBytes = requirements.size;
allocation.memoryTypeIndex = context.MemoryType(requirements.memoryTypeBits, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT);
Check(context.Function<PFN_vkAllocateMemory>("vkAllocateMemory")(context.device, &allocation, nullptr, &memory), "vkAllocateMemory device buffer");
Check(context.Function<PFN_vkBindBufferMemory>("vkBindBufferMemory")(context.device, buffer, memory, 0), "vkBindBufferMemory device");
} catch (...) {
release();
throw;
}
}
DeviceBuffer::~DeviceBuffer() {
release();
}
void DeviceBuffer::release() noexcept {
if (buffer && memory && cache) {
cache->Put({buffer, memory, nullptr, 0, allocationBytes, capacity, usage, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT});
return;
}
if (buffer) context.Function<PFN_vkDestroyBuffer>("vkDestroyBuffer")(context.device, buffer, nullptr);
if (memory) context.Function<PFN_vkFreeMemory>("vkFreeMemory")(context.device, memory, nullptr);
}
VkBuffer DeviceBuffer::Handle() const {
return buffer;
}
std::size_t DeviceBuffer::Size() const {
return size;
}
void CopyBuffer(const Context& context, VkCommandBuffer commands, VkBuffer source, VkDeviceSize sourceOffset, VkBuffer destination, VkDeviceSize destinationOffset, VkDeviceSize bytes) {
const VkBufferCopy region{sourceOffset, destinationOffset, bytes};
context.Function<PFN_vkCmdCopyBuffer>("vkCmdCopyBuffer")(commands, source, destination, 1, &region);
}
void RecordMemoryBarrier(const Context& context, VkCommandBuffer commands, VkPipelineStageFlags sourceStage, VkPipelineStageFlags destinationStage, VkAccessFlags sourceAccess, VkAccessFlags destinationAccess) {
const VkMemoryBarrier barrier{VK_STRUCTURE_TYPE_MEMORY_BARRIER, nullptr, sourceAccess, destinationAccess};
context.Function<PFN_vkCmdPipelineBarrier>("vkCmdPipelineBarrier")(commands, sourceStage, destinationStage, 0, 1, &barrier, 0, nullptr, 0, nullptr);
}
RenderTarget::RenderTarget(const Context& context, const ColorTarget& target, bool blending) : context(context) {
VkFormatProperties properties{};
context.formatProperties(context.physical, target.format, &properties);
@ -136,6 +205,8 @@ VkImageView RenderTarget::View() const {
}
CommandBatch::CommandBatch(const Context& context) : context(context) {
// Records into the device's one command pool and submits to its queue: device-lock work only.
GuestMemory::AssertGpuLockHeld("CommandBatch");
try {
VkCommandBufferAllocateInfo allocation{VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO};
allocation.commandPool = context.pool;
@ -147,6 +218,8 @@ CommandBatch::CommandBatch(const Context& context) : context(context) {
VkCommandBufferBeginInfo begin{VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO};
begin.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT;
Check(context.Function<PFN_vkBeginCommandBuffer>("vkBeginCommandBuffer")(commands, &begin), "vkBeginCommandBuffer");
// Work recorded so far goes first in queue order, so this batch sees its results.
if (auto* recorder = Recorder::Active(); recorder != nullptr && recorder->Recording()) recorder->Submit();
} catch (...) {
release();
throw;
@ -176,16 +249,6 @@ void CommandBatch::SubmitAndWait() {
Wait();
}
void CommandBatch::Reset() {
Require(submitted && !pending, "command batch must complete before reuse");
Check(context.Function<PFN_vkResetFences>("vkResetFences")(context.device, 1, &fence), "vkResetFences graphics");
Check(context.Function<PFN_vkResetCommandBuffer>("vkResetCommandBuffer")(commands, 0), "vkResetCommandBuffer graphics");
VkCommandBufferBeginInfo begin{VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO};
begin.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT;
Check(context.Function<PFN_vkBeginCommandBuffer>("vkBeginCommandBuffer")(commands, &begin), "vkBeginCommandBuffer graphics");
submitted = false;
}
void CommandBatch::Submit() {
PerformanceTimer timing("Graphics.Submit");
Require(!submitted, "command batch has already been submitted");
@ -208,6 +271,8 @@ void CommandBatch::Wait() {
timing.Mark("fence_wait");
if (result == VK_SUCCESS || result == VK_ERROR_DEVICE_LOST) pending = false;
Check(result, "vkWaitForFences graphics");
// Recorded batches preceded this one, so their completions (write-backs) can run now.
if (auto* recorder = Recorder::Active()) recorder->Reap();
}
}

View File

@ -1,8 +1,10 @@
#include "prx/libSceAgcDriver/Graphics/include/Sampler.hpp"
#include <algorithm>
#include <cstdlib>
namespace AgcDriver::Graphics {
Sampler::Sampler(const Context& context, const GuestSamplerResource& descriptor) : device(context.device), destroySampler(context.Function<PFN_vkDestroySampler>("vkDestroySampler")) {
Sampler::Sampler(const Context& context, const GuestSamplerResource& descriptor) : context(context) {
Require(!descriptor.anisotropyEnable || context.samplerAnisotropy, "guest sampler descriptor requests anisotropic filtering which the device does not support");
Require(descriptor.maxAnisotropy <= context.limits.maxSamplerAnisotropy, "guest sampler descriptor requests an anisotropy ratio beyond the device limit");
Require(descriptor.lodBias >= -context.limits.maxSamplerLodBias && descriptor.lodBias <= context.limits.maxSamplerLodBias, "guest sampler descriptor requests a LOD bias beyond the device limit");
@ -31,22 +33,36 @@ namespace AgcDriver::Graphics {
}
void Sampler::release() noexcept {
if (sampler) destroySampler(device, sampler, nullptr);
if (sampler) context.Function<PFN_vkDestroySampler>("vkDestroySampler")(context.device, sampler, nullptr);
}
VkSampler Sampler::Handle() const {
return sampler;
}
std::shared_ptr<Sampler> SamplerCache::Get(const Context& context, std::span<const std::uint32_t> words, const GuestSamplerResource& descriptor) {
Require(words.size() == 4, "sampler cache descriptor must contain four DWORDs");
const std::array<std::uint32_t, 5> key{words[0], words[1], words[2], words[3], descriptor.compareEnable ? 1u : 0u};
SamplerCache::SamplerCache(std::size_t capacity) : capacity(std::max<std::size_t>(capacity, 1)) {}
std::shared_ptr<Sampler> SamplerCache::Get(const Context& context, std::span<const std::uint32_t> words, bool compareEnable) {
Require(words.size() == 4, "guest sampler descriptor must contain 4 dwords");
const std::array<std::uint32_t, 5> key{words[0], words[1], words[2], words[3], compareEnable ? 1u : 0u};
std::lock_guard lock(mutex);
const auto found = entries.find(key);
if (found != entries.end()) return found->second;
auto sampler = std::make_shared<Sampler>(context, descriptor);
if (entries.size() >= 256) entries.erase(entries.begin());
entries.emplace(key, sampler);
++clock;
if (const auto found = entries.find(key); found != entries.end()) {
++hits;
found->second.lastUse = clock;
return found->second.sampler;
}
++misses;
auto resource = DecodeSamplerResource(words);
resource.compareEnable = compareEnable;
auto sampler = std::make_shared<Sampler>(context, resource);
// The cap keeps live samplers well below the device's limit (NVIDIA: ~4000); a set in flight
// still holds the evicted sampler through its own shared_ptr.
while (entries.size() >= capacity) {
const auto oldest = std::min_element(entries.begin(), entries.end(), [](const auto& left, const auto& right) { return left.second.lastUse < right.second.lastUse; });
entries.erase(oldest);
}
entries.emplace(key, Entry{sampler, clock});
return sampler;
}

View File

@ -2,7 +2,10 @@
#include "SceShaders.hpp"
#include "prx/libSceAgc/Shader/include/ShaderConstants.hpp"
#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp"
#include <algorithm>
#include <array>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <stdexcept>
#include <string>
@ -25,7 +28,9 @@ constexpr std::uint32_t spiShaderColFormat = 0x1C5;
std::uint32_t read(const Registers& registers, std::uint32_t offset) {
const auto it = registers.find(offset);
if (it == registers.end()) {
throw std::runtime_error("AGC graphics: missing register at DWORD 0x" + std::to_string(offset));
char text[64];
std::snprintf(text, sizeof(text), "AGC graphics: missing register at DWORD 0x%x", offset);
throw std::runtime_error(text);
}
return it->second;
}
@ -65,9 +70,12 @@ ShaderRecompiler::ShaderComputeStageInfo DecodeComputeStageInfo(const Registers&
if ((rsrc2 & 0x1u) != 0) {
throw std::runtime_error("AGC graphics: COMPUTE_PGM_RSRC2.SCRATCH_EN is unsupported");
}
// Debug aid: APS5_LDS_SLACK=<dwords> grows every dispatch's LDS allocation by that much, to tell
// whether a program depends on addresses past its declared allocation.
static const std::uint32_t ldsSlack = [] { const char* text = std::getenv("APS5_LDS_SLACK"); return text ? static_cast<std::uint32_t>(std::strtoul(text, nullptr, 0)) : 0u; }();
return ShaderRecompiler::ShaderComputeStageInfo{
{numThreadX, numThreadY, numThreadZ},
((rsrc2 >> 15u) & 0x1FFu) * 128u,
std::min(((rsrc2 >> 15u) & 0x1FFu) * 128u + ldsSlack, 16384u),
{((rsrc2 >> 7u) & 0x1u) != 0, ((rsrc2 >> 8u) & 0x1u) != 0, ((rsrc2 >> 9u) & 0x1u) != 0},
((rsrc2 >> 10u) & 0x1u) != 0,
((rsrc2 >> 11u) & 0x3u) + 1u
@ -92,12 +100,8 @@ ShaderRecompiler::ShaderPixelStageInfo DecodePixelStageInfo(const Registers& con
interpolatorSettings[i] = read(context, spiPsInputCntl0 + i);
}
const auto shaderControl = read(context, dbShaderControl);
if (((shaderControl >> 9u) & 0x1u) != 0) {
throw std::runtime_error("AGC graphics: DB_SHADER_CONTROL.DUAL_EXPORT_ENABLE is unsupported");
}
if (((shaderControl >> 11u) & 0x1u) != 0) {
throw std::runtime_error("AGC graphics: DB_SHADER_CONTROL.ALPHA_TO_MASK_DISABLE is unsupported");
}
// Bits 9 and 11 are EXEC_ON_HIER_FAIL and ALPHA_TO_MASK_DISABLE on GFX10; neither changes what the
// recompiled pixel shader computes.
if (((shaderControl >> 13u) & 0x3u) != 0) {
throw std::runtime_error("AGC graphics: DB_SHADER_CONTROL.CONSERVATIVE_Z_EXPORT is unsupported");
}

File diff suppressed because it is too large Load Diff

View File

@ -1,7 +1,6 @@
#include "BdaAbi.hpp"
#include "prx/libSceAgcDriver/Graphics/include/Pipeline.hpp"
#include "prx/libSceAgcDriver/Graphics/include/VertexInput.hpp"
#include "prx/libSceAgcDriver/Execution/include/PerformanceTimer.hpp"
#include <spirv/unified1/spirv.hpp>
#include <algorithm>
#include <map>
@ -9,8 +8,6 @@
#include <set>
#include <string>
#include <vector>
#include <memory>
#include <type_traits>
namespace AgcDriver::Graphics {
namespace {
@ -205,13 +202,26 @@ Module Inspect(const CompiledShader& compiled, const State& state, const VkPhysi
mesh &&
capability == spv::CapabilityMeshShadingEXT;
// Enabled unconditionally or by the device setup in VulkanDevice.
const bool isFeatureCapability =
capability == spv::CapabilityImageGatherExtended ||
capability == spv::CapabilityImageQuery ||
capability == spv::CapabilityStorageImageWriteWithoutFormat ||
capability == spv::CapabilityStorageImageReadWithoutFormat ||
capability == spv::CapabilityInt64 ||
capability == spv::CapabilityInt16 ||
capability == spv::CapabilityFloat16 ||
capability == spv::CapabilityStorageBuffer8BitAccess ||
capability == spv::CapabilityPhysicalStorageBufferAddresses;
Require(
isBaseCapability ||
isBarycentricCapability ||
isSubgroupCapability ||
isBdaCapability ||
isTessellationCapability ||
isMeshCapability,
isMeshCapability ||
isFeatureCapability,
std::string("SPIR-V requires unsupported device capability ") +
std::to_string(static_cast<std::uint32_t>(capability)));
@ -242,11 +252,10 @@ Module Inspect(const CompiledShader& compiled, const State& state, const VkPhysi
case spv::OpSpecConstantOp:
throw std::runtime_error("AGC graphics: unsupported SPIR-V extension, grouped decoration or specialization constant");
case spv::OpMemoryModel: {
const auto addressingModel = shader.bdaAbiVersion == ShaderRecompiler::BdaAbi::Version && !rectListControl
? spv::AddressingModelPhysicalStorageBuffer64
: spv::AddressingModelLogical;
// Address-based shaders use PhysicalStorageBuffer64; everything else (including the
// generated rect-list stages) is Logical.
Require(count == 3, "invalid SPIR-V OpMemoryModel word count: " + std::to_string(count));
Require(instruction[1] == addressingModel, "unsupported SPIR-V addressing model: " + std::to_string(instruction[1]));
Require(instruction[1] == spv::AddressingModelPhysicalStorageBuffer64 || instruction[1] == spv::AddressingModelLogical, "unsupported SPIR-V addressing model: " + std::to_string(instruction[1]));
Require(instruction[2] == spv::MemoryModelGLSL450, "unsupported SPIR-V memory model: " + std::to_string(instruction[2]));
++memoryModels;
break;
@ -417,7 +426,6 @@ Module Inspect(const CompiledShader& compiled, const State& state, const VkPhysi
const auto binding = std::find_if(shader.bindings.begin(), shader.bindings.end(), [&](const auto& item) { return item.descriptorSet == key.first && item.binding == key.second; });
Require(binding != shader.bindings.end(), "SPIR-V resource is absent from recompiler binding metadata");
Require(binding->kind == ShaderRecompiler::DescriptorKind::Sampler || binding->kind == ShaderRecompiler::DescriptorKind::SampledImage || binding->kind == ShaderRecompiler::DescriptorKind::StorageImage, "SPIR-V descriptor type disagrees with recompiler binding metadata");
Require(binding->kind != ShaderRecompiler::DescriptorKind::StorageImage, "storage image resources are not implemented");
} else {
Require(variable.storage == spv::StorageClassStorageBuffer && decoration.set && decoration.binding, "unsupported or unbound shader resource");
const auto key = std::make_pair(*decoration.set, *decoration.binding);
@ -466,77 +474,7 @@ Module Inspect(const CompiledShader& compiled, const State& state, const VkPhysi
}
namespace {
struct ValidatedInterface {
std::map<std::uint32_t, std::string> inputs;
std::map<std::uint32_t, std::string> outputs;
};
std::shared_ptr<const ValidatedInterface> inspectCached(const CompiledShader& compiled, const State& state, const VkPhysicalDeviceSubgroupProperties& subgroup, bool fragmentShaderBarycentric) {
PerformanceTimer timing("Graphics.ShaderValidation");
Require(compiled.program != nullptr, "missing compiled shader");
const auto& shader = *compiled.program;
std::string key;
const auto append = [&]<typename TValue>(const TValue& value) {
static_assert(std::is_trivially_copyable_v<TValue>);
key.append(reinterpret_cast<const char*>(&value), sizeof(value));
};
append(compiled.stage);
append(state.rectList);
append(state.stages.mesh.has_value());
if (state.stages.mesh) {
append(state.stages.mesh->threadsPerGroup);
append(state.stages.mesh->maxVertices);
append(state.stages.mesh->maxPrimitives);
}
append(state.stages.tessellation.has_value());
if (state.stages.tessellation) {
append(state.stages.tessellation->inputControlPoints);
append(state.stages.tessellation->outputControlPoints);
}
append(subgroup.supportedStages);
append(subgroup.supportedOperations);
append(fragmentShaderBarycentric);
append(shader.bdaAbiVersion);
append(shader.pushConstants.empty());
append(shader.bindings.size());
for (const auto& binding : shader.bindings) {
append(binding.kind);
append(binding.role);
append(binding.descriptorSet);
append(binding.binding);
append(binding.count);
append(binding.readOnly);
append(binding.guestDescriptor.empty());
}
append(shader.vertexAttributes.size());
for (const auto& attribute : shader.vertexAttributes) {
append(attribute.location);
append(attribute.components);
append(attribute.resource.fields[3]);
}
append(shader.spirv.size());
const auto code = std::as_bytes(std::span(shader.spirv));
if (!code.empty()) key.append(reinterpret_cast<const char*>(code.data()), code.size());
timing.Mark("key", key.size());
static thread_local std::map<std::string, std::shared_ptr<const ValidatedInterface>> cache;
const auto found = cache.find(key);
if (found != cache.end()) {
timing.Mark("hit");
return found->second;
}
auto module = Inspect(compiled, state, subgroup, fragmentShaderBarycentric);
auto result = std::make_shared<const ValidatedInterface>(ValidatedInterface{std::move(module.inputs), std::move(module.outputs)});
if (cache.size() >= 128) cache.clear();
cache.emplace(std::move(key), result);
timing.Mark("inspect");
return result;
}
}
void ValidateShaders(std::span<const CompiledShader> shaders, const State& state, const VkPhysicalDeviceSubgroupProperties& subgroup, bool fragmentShaderBarycentric) {
std::set<std::uint32_t> ValidateShaders(std::span<const CompiledShader> shaders, const State& state, const VkPhysicalDeviceSubgroupProperties& subgroup, bool fragmentShaderBarycentric) {
using Stage = ShaderRecompiler::ShaderStage;
const bool tessellation = state.stages.path == ShaderPath::Tessellation;
const bool mesh = state.stages.path == ShaderPath::Geometry;
@ -546,22 +484,30 @@ void ValidateShaders(std::span<const CompiledShader> shaders, const State& state
Require(shaders.size() == (tessellation || state.rectList ? 4u : 2u), "incorrect graphics stage count");
const std::array<Stage, 4> tessStages{Stage::Local, Stage::TessellationControl, Stage::TessellationEvaluation, Stage::Fragment};
static_cast<void>(AssemblePushConstants(shaders));
std::shared_ptr<const ValidatedInterface> previous;
Module previous;
for (std::size_t i = 0; i < shaders.size(); ++i) {
const auto expected = state.rectList ? (i == 0 ? Stage::Vertex : tessStages[i]) : tessellation ? tessStages[i] : i == 1 ? Stage::Fragment : state.stages.path == ShaderPath::Geometry ? Stage::Mesh : Stage::Vertex;
Require(shaders[i].program != nullptr, "missing compiled shader");
Require(shaders[i].stage == expected, "graphics stage order disagrees");
for (const auto& binding : shaders[i].program->bindings) Require(binding.descriptorSet == 0, "graphics resource uses a descriptor set other than zero");
const auto current = inspectCached(shaders[i], state, subgroup, fragmentShaderBarycentric);
const auto current = Inspect(shaders[i], state, subgroup, fragmentShaderBarycentric);
if (i != 0) {
for (const auto& [location, signature] : current->inputs) {
const auto output = previous->outputs.find(location);
Require(output != previous->outputs.end() && output->second == signature, "graphics interfaces disagree at location " + std::to_string(location));
for (const auto& [location, signature] : current.inputs) {
const auto output = previous.outputs.find(location);
Require(output != previous.outputs.end() && output->second == signature, "graphics interfaces disagree at location " + std::to_string(location));
}
}
previous = current;
}
Require(previous->outputs.size() == 1 && previous->outputs.contains(0) && previous->outputs.at(0) == "vertex:f32x4", "fragment shader must export one float4 color at location zero");
// One float4 color per MRT attachment. A pixel shader may also export no color at all when it
// writes storage images or buffers instead; its attachments are then left untouched.
const auto attachments = std::max<std::size_t>(state.colors.size(), 1u);
std::set<std::uint32_t> locations;
for (const auto& [location, signature] : previous.outputs) {
Require(location < attachments && signature == "vertex:f32x4", "fragment shader must export float4 colors at locations below the attachment count");
locations.insert(location);
}
return locations;
}
void ValidateShaderPair(const ShaderRecompiler::RecompileResult& vertex, const ShaderRecompiler::RecompileResult& fragment) {

View File

@ -1,4 +1,7 @@
#include <cstdio>
#include "prx/libSceAgcDriver/Graphics/include/State.hpp"
#include "prx/libSceAgcDriver/Graphics/include/TextureTiling.hpp"
#include "prx/libSceAgcDriver/Graphics/include/DccMetadata.hpp"
#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp"
#include "prx/libc/include/General.hpp"
#include <algorithm>
@ -27,7 +30,12 @@ float readFloat(const Registers& registers, std::uint32_t offset) {
}
void zero(const Registers& registers, std::uint32_t offset, std::uint32_t mask, const char* name, const char* bank = "context") {
Require((read(registers, offset, bank) & mask) == 0, std::string(name) + " is unsupported");
const auto value = read(registers, offset, bank);
if ((value & mask) != 0) {
char detail[64];
std::snprintf(detail, sizeof(detail), " (register 0x%x = 0x%08x)", offset, value);
throw std::runtime_error(std::string(name) + " is unsupported" + detail);
}
}
VkBlendFactor blendFactor(std::uint32_t value) {
@ -62,6 +70,82 @@ VkBlendOp blendOp(std::uint32_t value) {
}
}
struct DecodedColorFormat {
VkFormat format;
std::uint32_t elementBytes;
};
// 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 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();
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};
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};
return fail();
case 3:
if (swap != 0) return fail();
if (number == unorm) return {VK_FORMAT_R8G8_UNORM, 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};
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 == uint) return {VK_FORMAT_R16G16_UINT, 4};
return fail();
case 6:
// COLOR_10_11_11: red in the low 11 bits, the Vulkan B10G11R11 packing.
if (swap != 0 || number != floating) return fail();
return {VK_FORMAT_B10G11R11_UFLOAT_PACK32, 4};
case 9:
// COLOR_2_10_10_10 keeps red in the low bits, the Vulkan A2B10G10R10 packing.
if (number != unorm) return fail();
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 == srgb) return {alternate ? VK_FORMAT_B8G8R8A8_SRGB : VK_FORMAT_R8G8B8A8_SRGB, 4};
return fail();
case 11:
if (swap != 0) return fail();
if (number == floating) return {VK_FORMAT_R32G32_SFLOAT, 8};
if (number == uint) return {VK_FORMAT_R32G32_UINT, 8};
return fail();
case 12:
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 == uint) return {VK_FORMAT_R16G16B16A16_UINT, 8};
return fail();
case 14:
if (swap != 0) return fail();
if (number == floating) return {VK_FORMAT_R32G32B32A32_SFLOAT, 16};
if (number == uint) return {VK_FORMAT_R32G32B32A32_UINT, 16};
return fail();
default:
return fail();
}
}
void intersect(VkRect2D& result, const Registers& registers, std::uint32_t offset, bool screen) {
const auto tl = read(registers, offset);
const auto br = read(registers, offset + 1);
@ -95,6 +179,7 @@ ShaderStages DecodeShaderStages(const QueueState& queue) {
validate(!tessellation || !geometry, "combined tessellation and geometry is unsupported by the reference path");
const auto path = tessellation ? ShaderPath::Tessellation : geometry ? ShaderPath::Geometry : ShaderPath::Vertex;
ShaderStages result{path, value, (value & 0x00400000u) != 0 ? 32u : 64u, (read(queue.context, 0x1b6) & 0x8000u) != 0 ? 32u : 64u, {}, {}};
APS5_LOG_OUT_DEBUG("DecodeShaderStages value=0x%x primitive=%u path=%u vertexWave=%u", value, primitive, static_cast<unsigned>(result.path), result.vertexWaveSize);
if (path == ShaderPath::Vertex) {
validate((value & 0x2000u) != 0, "legacy vertex routing without PRIMGEN_EN is unsupported");
validate((value & ~0x02402010u) == 0, "unsupported vertex routing, scheduling or wave-ID state");
@ -132,6 +217,7 @@ State DecodeState(const QueueState& queue) {
State result{};
result.stages = DecodeShaderStages(queue);
const auto primitive = read(queue.userConfig, 0x242, "user-config");
APS5_LOG_OUT_DEBUG("DecodeState primitive=%u path=%u vertexWave=%u", primitive, static_cast<unsigned>(result.stages.path), result.stages.vertexWaveSize);
switch (primitive) {
case 1: Require(result.stages.mesh.has_value(), "point-list vertex rendering requires point-size output support"); result.topology = VK_PRIMITIVE_TOPOLOGY_POINT_LIST; break;
case 2: result.topology = VK_PRIMITIVE_TOPOLOGY_LINE_LIST; break;
@ -147,10 +233,42 @@ State DecodeState(const QueueState& queue) {
case 6: result.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP; break;
default: throw std::runtime_error("AGC graphics: unsupported primitive type " + std::to_string(primitive));
}
APS5_LOG_OUT_DEBUG("Topology=%u", static_cast<unsigned>(result.topology));
zero(queue.userConfig, 0x24b, ~0u, "primitive restart (GE_MULTI_PRIM_IB_RESET_EN)", "user-config");
zero(cx, 0x207, ~0u, "clip distances, layer, viewport or auxiliary vertex exports");
zero(cx, 0x200, ~0x007007f0u, "depth, stencil or conditional color writes");
zero(cx, 0x203, ~0x00009870u, "depth export, shader coverage or ordered fragment execution");
// Render target index, viewport index and the misc export vector that carries them are accepted but
// not routed: color targets are single-layer, so layered draws land in layer 0.
constexpr std::uint32_t layerExports = (1u << 18u) | (1u << 19u) | (1u << 21u) | (1u << 24u);
if ((read(cx, 0x207, "context") & layerExports) != 0) {
static bool reported = false;
if (!reported) {
reported = true;
std::fprintf(stderr, "[gpu] layer/viewport index vertex exports are ignored (PA_CL_VS_OUT_CNTL=0x%08x)\n", read(cx, 0x207, "context"));
}
}
zero(cx, 0x207, ~layerExports, "clip distances, layer, viewport or auxiliary vertex exports");
// 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).
{
const auto depthControl = read(cx, 0x200, "context");
const bool stencil = (depthControl & 1u) != 0;
const bool depth = (depthControl & 2u) != 0;
const bool depthWrite = (depthControl & 4u) != 0;
const bool passThroughDepth = !depth || (!depthWrite && ((depthControl >> 4u) & 7u) == 7u);
const bool passThroughStencil = !stencil || (((depthControl >> 8u) & 7u) == 7u && ((depthControl & 0x80u) == 0 || ((depthControl >> 20u) & 7u) == 7u));
if ((stencil || depth) && passThroughDepth && passThroughStencil) {
static bool reported = false;
if (!reported) {
reported = true;
std::fprintf(stderr, "[gpu] always-pass depth/stencil state is rendered without a depth target (DB_DEPTH_CONTROL=0x%08x)\n", depthControl);
}
} else {
zero(cx, 0x200, ~0x007007f0u, "depth, stencil or conditional color writes");
}
Require((depthControl & 0xc0000008u) == 0, "depth bounds or depth-conditional color writes are unsupported");
}
// EXEC_ON_HIER_FAIL / EXEC_ON_NOOP / EXEC_IF_OVERLAPPED (bits 9, 10, 17) only force the pixel shader
// to run, which it always does here.
zero(cx, 0x203, ~(0x00009870u | 0x00020600u), "depth export, shader coverage or ordered fragment execution");
zero(cx, 0x2dc, ~0x0001ff00u, "alpha-to-coverage");
zero(cx, 0x2f8, ~0u, "multisampling or coverage conversion");
zero(cx, 0x292, ~2u, "scan conversion mode");
@ -168,53 +286,120 @@ State DecodeState(const QueueState& queue) {
message << "AGC graphics: PA_CL_VTE_CNTL=0x" << std::hex << viewportControl << ": expected 0x43f for homogeneous positions and all viewport transforms; pre-divided coordinates, reciprocal W or disabled transforms are unsupported";
throw std::runtime_error(message.str());
}
zero(cx, 0x204, ~0x80000u, "unsupported PA_CL_CLIP_CNTL flags");
// Bits 26/27 (ZCLIP_NEAR/FAR_DISABLE) become depth clamping; bit 19 selects the [0, 1] clip space.
zero(cx, 0x204, ~(0x80000u | 0x0c000000u), "unsupported PA_CL_CLIP_CNTL flags");
result.depthClamp = (read(cx, 0x204) & 0x0c000000u) != 0;
result.negativeOneToOne = (read(cx, 0x204) & 0x80000u) == 0;
const auto raster = read(cx, 0x205);
Require((raster & ~0x7u) == 0 || (raster & ~0x7u) == 0x240u, "polygon mode, depth bias, provoking vertex or nonstandard rasterization is unsupported");
// Bits 5-10 give the front/back polygon type (2 = filled triangles), which POLY_MODE (bit 3) turns on
// explicitly; KEEP_TOGETHER_ENABLE (bit 24) only affects primitive distribution across the chip.
const auto rasterMode = raster & ~0x7u & ~(1u << 24u);
Require(rasterMode == 0 || rasterMode == 0x240u || rasterMode == 0x248u, "polygon mode, depth bias, provoking vertex or nonstandard rasterization is unsupported");
result.cullMode = ((raster & 1u) != 0 ? VK_CULL_MODE_FRONT_BIT : 0u) | ((raster & 2u) != 0 ? VK_CULL_MODE_BACK_BIT : 0u);
if (result.rectList) result.cullMode = VK_CULL_MODE_NONE;
result.frontFace = (raster & 4u) != 0 ? VK_FRONT_FACE_CLOCKWISE : VK_FRONT_FACE_COUNTER_CLOCKWISE;
const auto targetMask = read(cx, 0x8e);
APS5_LOG_OUT_DEBUG("Raster=0x%x cullMode=0x%x frontFace=%u negativeOneToOne=%u", raster, static_cast<unsigned>(result.cullMode), static_cast<unsigned>(result.frontFace), result.negativeOneToOne ? 1u : 0u);
const auto shaderMask = read(cx, 0x8f);
if ((targetMask & ~0xfu) != 0 || (shaderMask & ~0xfu) != 0) {
std::ostringstream message;
message << "AGC graphics: only color target zero is supported: CB_TARGET_MASK=0x" << std::hex << targetMask << ", CB_SHADER_MASK=0x" << shaderMask;
throw std::runtime_error(message.str());
// Channels of targets the pixel shader does not export are never written, so the target mask only
// matters where the shader exports.
const auto targetMask = read(cx, 0x8e) & shaderMask;
APS5_LOG_OUT_DEBUG("CB_TARGET_MASK=0x%x CB_SHADER_MASK=0x%x", targetMask, shaderMask);
// MRT slots 0..n-1 become attachments 0..n-1; a slot between written slots must be written too.
std::uint32_t slotCount = 0;
for (std::uint32_t slot = 0; slot < 8; ++slot) {
if (((targetMask >> (4u * slot)) & 0xfu) != 0) slotCount = slot + 1;
}
result.hasColorTarget = targetMask != 0;
Require(!result.hasColorTarget || shaderMask == 0xfu, "partial shader color exports are unsupported");
Require(read(cx, 0x202) == 0xcc0010u, "only normal color rendering with copy ROP is supported");
for (std::uint32_t slot = 0; slot < slotCount; ++slot) {
if (((targetMask >> (4u * slot)) & 0xfu) == 0) {
std::ostringstream message;
message << "AGC graphics: color targets with gaps are unsupported: CB_TARGET_MASK=0x" << std::hex << targetMask << ", CB_SHADER_MASK=0x" << shaderMask;
throw std::runtime_error(message.str());
}
}
result.hasColorTarget = slotCount != 0;
APS5_LOG_OUT_DEBUG("hasColorTarget=%u slots=%u", result.hasColorTarget ? 1u : 0u, slotCount);
// CB_COLOR_CONTROL mode 0 disables color writes, which only matters when a target is written.
Require(read(cx, 0x202) == 0xcc0010u || (!result.hasColorTarget && (read(cx, 0x202) & ~0x70u) == 0xcc0000u), "only normal color rendering with copy ROP is supported");
zero(cx, 0x1c4, ~0u, "depth or sample-mask export");
const auto exportFormat = read(cx, 0x1c5);
Require(exportFormat == 4 || exportFormat == 9, "only FP16_ABGR or 32_ABGR color export is supported");
Require(read(cx, 0x1c3) == 4, "additional position exports are unsupported");
if (result.hasColorTarget) {
const auto info = read(cx, 0x31c);
APS5_LOG_OUT_DEBUG("Export format=%u", exportFormat);
// SPI_SHADER_POS_FORMAT: POS0 must be a 4-component position; later vectors carry the misc/clip
// exports that PA_CL_VS_OUT_CNTL validation above already limits to ignored layer/viewport data.
Require((read(cx, 0x1c3) & 0xfu) == 4, "additional position exports are unsupported");
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");
const auto stride = slot * 0xfu;
ColorTarget color{};
const auto info = read(cx, 0x31c + stride);
const auto number = (info >> 8u) & 7u;
const auto swap = (info >> 11u) & 3u;
Require(((info >> 2u) & 0x1fu) == 10 && (number == 0 || number == 6) && swap <= 1, "unsupported color format or component order");
Require((info & ~0x00029f7cu) == 0, "color compression, DCC, endian conversion, nonstandard rounding or color optimization is unsupported");
Require((info & 0x8000u) != 0, "unclamped normalized color is unsupported");
zero(cx, 0x31b, ~0u, "color mip or array view");
zero(cx, 0x31d, ~0u, "color samples, fragments or destination alpha override");
const auto attrib2 = read(cx, 0x3b0);
Require((attrib2 >> 28u) == 0, "mipmapped render targets are unsupported");
const auto attrib3 = read(cx, 0x3b8);
result.color.tileMode = DecodeColorTileMode(attrib3);
result.color.extent = {((attrib2 >> 14u) & 0x3fffu) + 1u, (attrib2 & 0x3fffu) + 1u};
const ColorTargetLayout colorLayout(result.color.extent.width, result.color.extent.height, result.color.tileMode);
const auto high = read(cx, 0x390);
APS5_LOG_OUT_DEBUG("Color %u info=0x%x number=%u swap=%u", slot, info, number, swap);
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.
const auto view = read(cx, 0x31b + stride);
Require((view & ~0x0f000000u) == 0, "color array views are unsupported");
const auto viewMip = (view >> 24u) & 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;
Require(viewMip <= maxMip, "color view mip exceeds the surface");
const auto attrib3 = read(cx, 0x3b8 + slot);
color.tileMode = DecodeColorTileMode(attrib3);
color.extent = {((attrib2 >> 14u) & 0x3fffu) + 1u, (attrib2 & 0x3fffu) + 1u};
color.elementBytes = decoded.elementBytes;
std::uint64_t mipOffset = 0;
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.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");
result.color.address = (static_cast<std::uint64_t>(high) << 40u) | (static_cast<std::uint64_t>(read(cx, 0x318)) << 8u);
result.color.bytes = colorLayout.Bytes();
GuestMemory::CheckGpuRange(reinterpret_cast<const void*>(result.color.address), result.color.bytes, colorLayout.Alignment(), true);
result.color.format = swap == 0 ? (number == 0 ? VK_FORMAT_R8G8B8A8_UNORM : VK_FORMAT_R8G8B8A8_SRGB) : (number == 0 ? VK_FORMAT_B8G8R8A8_UNORM : VK_FORMAT_B8G8R8A8_SRGB);
result.color.componentMapping = 0xe4u;
result.renderExtent = result.color.extent;
color.address = ((static_cast<std::uint64_t>(high) << 40u) | (static_cast<std::uint64_t>(read(cx, 0x318 + stride)) << 8u)) + mipOffset;
color.bytes = colorLayout.Bytes();
GuestMemory::CheckRange(reinterpret_cast<const void*>(color.address), color.bytes, colorLayout.Alignment(), true);
color.format = decoded.format;
color.componentMapping = 0xe4u;
if ((info & 0x10000000u) != 0) {
if (maxMip == 0) {
const auto dccHigh = cx.find(0x3a8 + slot);
color.dccAddress = ((dccHigh == cx.end() ? 0ull : static_cast<std::uint64_t>(dccHigh->second & 0xffu)) << 40u) | (static_cast<std::uint64_t>(read(cx, 0x325 + stride)) << 8u);
color.dccAlphaOnMsb = DccAlphaOnMsb(color.format, swap);
} else {
static bool reported = false;
if (!reported) {
reported = true;
std::fprintf(stderr, "[gpu] DCC keys of mipmapped color targets are ignored\n");
}
}
}
APS5_LOG_OUT_DEBUG("Color %u address=0x%llx extent=%ux%u bytes=%llu VkFormat=%u", slot, static_cast<unsigned long long>(color.address), color.extent.width, color.extent.height, static_cast<unsigned long long>(color.bytes), static_cast<unsigned>(color.format));
if (slot == 0) {
result.renderExtent = color.extent;
} else {
result.renderExtent = {std::min(result.renderExtent.width, color.extent.width), std::min(result.renderExtent.height, color.extent.height)};
}
result.colors.push_back(color);
}
if (result.hasColorTarget) {
result.color = result.colors.front();
} else {
const auto screenBottomRight = read(cx, 0xd);
APS5_LOG_OUT_DEBUG("No color target, screen BR register=0x%x", screenBottomRight);
result.renderExtent = {screenBottomRight & 0xffffu, screenBottomRight >> 16u};
APS5_LOG_OUT_DEBUG("Render extent from screen=%ux%u", result.renderExtent.width, result.renderExtent.height);
Require(result.renderExtent.width != 0 && result.renderExtent.height != 0, "empty framebuffer extent for a draw without color writes");
}
const auto xs = readFloat(cx, 0x10f);
@ -225,6 +410,7 @@ State DecodeState(const QueueState& queue) {
const auto zo = readFloat(cx, 0x114);
const auto minDepth = result.negativeOneToOne ? zo - zs : zo;
const auto maxDepth = zo + zs;
APS5_LOG_OUT_DEBUG("Viewport transform scale=(%f,%f,%f) offset=(%f,%f,%f) depth=(%f,%f)", xs, ys, zs, xo, yo, zo, minDepth, maxDepth);
if (!(xs > 0 && ys != 0 && std::isfinite(minDepth) && std::isfinite(maxDepth))) {
std::ostringstream message;
message << "AGC graphics: unsupported viewport transform: scale=(" << xs << ", " << ys << ", " << zs << "), offset=(" << xo << ", " << yo << ", " << zo << "), depth=(" << minDepth << ", " << maxDepth << "), negativeOneToOne=" << result.negativeOneToOne;
@ -232,28 +418,35 @@ State DecodeState(const QueueState& queue) {
}
Require(readFloat(cx, 0xb4) <= readFloat(cx, 0xb5), "inverted viewport depth clamp bounds");
result.viewport = {xo - xs, yo - ys, 2 * xs, 2 * ys, minDepth, maxDepth};
APS5_LOG_OUT_DEBUG("Viewport x=%f y=%f w=%f h=%f minDepth=%f maxDepth=%f", result.viewport.x, result.viewport.y, result.viewport.width, result.viewport.height, result.viewport.minDepth, result.viewport.maxDepth);
result.scissor = {{0, 0}, result.renderExtent};
intersect(result.scissor, cx, 0xc, true);
intersect(result.scissor, cx, 0x81, false);
intersect(result.scissor, cx, 0x90, false);
if ((read(cx, 0x292) & 2u) != 0) intersect(result.scissor, cx, 0x94, false);
if (result.hasColorTarget) {
const auto blend = read(cx, 0x1e0);
APS5_LOG_OUT_DEBUG("Scissor offset=(%d,%d) extent=%ux%u", result.scissor.offset.x, result.scissor.offset.y, result.scissor.extent.width, result.scissor.extent.height);
for (std::uint32_t slot = 0; slot < result.colors.size(); ++slot) {
const auto blend = read(cx, 0x1e0 + slot);
APS5_LOG_OUT_DEBUG("Blend %u control=0x%x", slot, blend);
Require((blend & 0x0000e000u) == 0, "reserved blend control bits");
result.blend.colorWriteMask = targetMask;
result.blend.blendEnable = (blend >> 30u) & 1u;
if (result.blend.blendEnable) {
Require((read(cx, 0x31c) & 0x10000u) == 0, "blend bypass conflicts with enabled blending");
result.blend.srcColorBlendFactor = blendFactor(blend & 0x1fu);
result.blend.dstColorBlendFactor = blendFactor((blend >> 8u) & 0x1fu);
result.blend.colorBlendOp = blendOp((blend >> 5u) & 7u);
VkPipelineColorBlendAttachmentState state{};
state.colorWriteMask = (targetMask >> (4u * slot)) & 0xfu;
state.blendEnable = (blend >> 30u) & 1u;
if (state.blendEnable) {
Require((read(cx, 0x31c + slot * 0xfu) & 0x10000u) == 0, "blend bypass conflicts with enabled blending");
state.srcColorBlendFactor = blendFactor(blend & 0x1fu);
state.dstColorBlendFactor = blendFactor((blend >> 8u) & 0x1fu);
state.colorBlendOp = blendOp((blend >> 5u) & 7u);
const auto alpha = (blend & 0x20000000u) != 0 ? blend >> 16u : blend;
result.blend.srcAlphaBlendFactor = blendFactor(alpha & 0x1fu);
result.blend.dstAlphaBlendFactor = blendFactor((alpha >> 8u) & 0x1fu);
result.blend.alphaBlendOp = blendOp((alpha >> 5u) & 7u);
state.srcAlphaBlendFactor = blendFactor(alpha & 0x1fu);
state.dstAlphaBlendFactor = blendFactor((alpha >> 8u) & 0x1fu);
state.alphaBlendOp = blendOp((alpha >> 5u) & 7u);
for (std::uint32_t i = 0; i < 4; ++i) result.blendConstants[i] = readFloat(cx, 0x105 + i);
}
result.blends.push_back(state);
}
if (!result.blends.empty()) result.blend = result.blends.front();
APS5_LOG_OUT_DEBUG("DecodeState done colorTarget=%u render=%ux%u topology=%u", result.hasColorTarget ? 1u : 0u, result.renderExtent.width, result.renderExtent.height, static_cast<unsigned>(result.topology));
return result;
}

File diff suppressed because it is too large Load Diff

View File

@ -1,8 +1,6 @@
#include "prx/libSceAgcDriver/Graphics/include/TextureCache.hpp"
#include "prx/libSceAgcDriver/Graphics/include/TextureTiling.hpp"
#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp"
#include "prx/libSceAgcDriver/Graphics/include/RenderCache.hpp"
#include "prx/libSceAgcDriver/Graphics/include/TextureFormat.hpp"
#include <algorithm>
#include <cstring>
#include <limits>
@ -29,26 +27,9 @@ std::shared_ptr<Texture> TextureCache::Get(std::span<const std::uint32_t> words,
trim();
std::array<std::uint32_t, 8> key;
std::copy(words.begin(), words.end(), key.begin());
auto source = context.renderCache ? context.renderCache->Find(resource.baseAddress) : nullptr;
if (source) {
const auto& color = source->Description();
const auto compatibleTiling = (color.tileMode == ColorTileMode::RenderTarget && resource.tileMode == TextureTileMode::RenderTarget64KB) || (color.tileMode == ColorTileMode::Linear && resource.tileMode == TextureTileMode::kLinear);
if (!compatibleTiling || resource.width != color.extent.width || resource.height != color.extent.height || resource.dimension != TextureDimension::k2D || resource.mipCount != 1 || resource.baseLevel != 0 || resource.baseArray != 0 || IsBlockCompressed(resource.format) || BytesPerElement(resource.format) != 4) source.reset();
}
for (auto it = entries.begin(); it != entries.end(); ++it) {
if (it->descriptor != key) continue;
if (source || it->generation != 0) {
if (source && it->source.lock() == source && it->generation == source->Generation()) {
auto result = it->texture;
entries.splice(entries.end(), entries, it);
return result;
}
retainedBytes -= it->snapshot.size() + it->texture->AllocationBytes();
entries.erase(it);
break;
}
GuestMemory::CheckRange(reinterpret_cast<const void*>(resource.baseAddress), it->snapshot.size(), 1);
if (std::memcmp(reinterpret_cast<const void*>(resource.baseAddress), it->snapshot.data(), it->snapshot.size()) == 0) {
if (GuestMemory::EqualsCommitted(resource.baseAddress, it->snapshot)) {
auto result = it->texture;
entries.splice(entries.end(), entries, it);
return result;
@ -57,19 +38,12 @@ std::shared_ptr<Texture> TextureCache::Get(std::span<const std::uint32_t> words,
entries.erase(it);
break;
}
if (source) {
auto texture = std::make_shared<Texture>(context, source, resource, components);
entries.push_back({key, {}, texture, source, source->Generation()});
retainedBytes += texture->AllocationBytes();
trim();
return texture;
}
const auto mips = ComputeMipLayout(resource.tileMode, resource.format, resource.width, resource.height, resource.mipCount);
const auto layers = resource.dimension == TextureDimension::k2DArray || resource.dimension == TextureDimension::kCube ? resource.depthOrLastArray + 1u : 1u;
const auto bytes = ComputeSurfaceSize(mips, layers);
Require(bytes != 0 && bytes <= std::numeric_limits<std::size_t>::max(), "texture cache surface size overflow");
std::vector<std::byte> snapshot(static_cast<std::size_t>(bytes));
GuestMemory::Read(resource.baseAddress, snapshot, 1);
GuestMemory::ReadCommitted(resource.baseAddress, snapshot);
auto texture = std::make_shared<Texture>(context, *context.detiler, resource, components, snapshot);
const auto retained = snapshot.size() + texture->AllocationBytes();
entries.push_back({key, std::move(snapshot), texture});

View File

@ -1,4 +1,6 @@
#include "prx/libSceAgcDriver/Graphics/include/TextureDetiler.hpp"
#include "prx/libSceAgcDriver/Graphics/include/TextureSwizzleEquations.hpp"
#include "prx/libSceAgcDriver/Graphics/include/TextureTiling.hpp"
#include "prx/libSceAgcDriver/Graphics/shaders/TextureDetile_spv.h"
#include <algorithm>
#include <array>
@ -22,7 +24,7 @@ struct Push {
std::uint32_t tailX;
std::uint32_t tailY;
std::uint32_t elementBytes;
std::uint32_t arrayLayer;
std::uint32_t slice;
};
std::uint32_t BlockBytesFor(TextureTileMode tileMode) {
@ -30,14 +32,17 @@ std::uint32_t BlockBytesFor(TextureTileMode tileMode) {
case TextureTileMode::kLinear: return 0u;
case TextureTileMode::kStandard256B: return 256u;
case TextureTileMode::kStandard4KB: return 4096u;
case TextureTileMode::RenderTarget64KB:
case TextureTileMode::kStandard64KB: return 65536u;
case TextureTileMode::kStandard64KB:
case TextureTileMode::kZ64KBX:
case TextureTileMode::kS64KBX:
case TextureTileMode::kD64KBX:
case TextureTileMode::kR64KBX: return 65536u;
}
throw std::runtime_error("AGC graphics: TextureDetiler encountered an unknown tile mode");
}
std::uint32_t PipelineKey(TextureTileMode tileMode, std::uint32_t elementBytes) {
return (static_cast<std::uint32_t>(tileMode) << 8) | elementBytes;
std::uint32_t PipelineKey(TextureTileMode tileMode, std::uint32_t elementBytes, bool retile, bool thick) {
return (thick ? 1u << 17 : 0u) | (retile ? 1u << 16 : 0u) | (static_cast<std::uint32_t>(tileMode) << 8) | elementBytes;
}
}
@ -87,19 +92,38 @@ void TextureDetiler::release() noexcept {
if (descriptorLayout) context.Function<PFN_vkDestroyDescriptorSetLayout>("vkDestroyDescriptorSetLayout")(context.device, descriptorLayout, nullptr);
}
VkPipeline TextureDetiler::pipeline(TextureTileMode tileMode, std::uint32_t elementBytes) {
VkPipeline TextureDetiler::pipeline(TextureTileMode tileMode, std::uint32_t elementBytes, bool retile, bool thick) {
Require(std::has_single_bit(elementBytes) && elementBytes <= 16u, "unsupported element size for texture detiling");
const auto key = PipelineKey(tileMode, elementBytes);
const auto key = PipelineKey(tileMode, elementBytes, retile, thick);
for (const auto& entry : pipelines) {
if (entry.first == key) return entry.second;
}
const std::uint32_t values[3] = {elementBytes, BlockBytesFor(tileMode), tileMode == TextureTileMode::kLinear ? 0u : (tileMode == TextureTileMode::RenderTarget64KB ? 2u : 1u)};
const VkSpecializationMapEntry entries[3] = {{0, 0, 4}, {1, 4, 4}, {2, 8, 4}};
// Constants 0-3 select element size, block size, addressing family and direction; 4-19 carry the
// per-bit XOR equation for the equation family (2); 20-21 override the block extent in elements.
std::array<std::uint32_t, 22> values{elementBytes, BlockBytesFor(tileMode), tileMode == TextureTileMode::kLinear ? 0u : 1u, retile ? 1u : 0u};
if (thick) {
const auto thickMode = tileMode == TextureTileMode::kStandard4KB ? 0x105u : tileMode == TextureTileMode::kStandard64KB ? 0x109u : 0u;
Require(thickMode != 0, "3D textures are only detiled from SW_4KB_S or SW_64KB_S");
const auto* equation = FindTextureSwizzleEquation(thickMode, elementBytes);
Require(equation != nullptr, "no thick swizzle equation for the element size");
values[2] = 2u;
std::copy(equation->bits.begin(), equation->bits.end(), values.begin() + 4);
const auto extent = ThickBlockExtent(tileMode, elementBytes);
values[20] = extent[0];
values[21] = extent[1];
} else if (const auto mode = XorSwizzleMode(tileMode); mode != 0) {
const auto* equation = FindTextureSwizzleEquation(mode, elementBytes);
Require(equation != nullptr, "no swizzle equation for tile mode " + std::to_string(mode) + " at " + std::to_string(elementBytes) + " bytes per element");
values[2] = 2u;
std::copy(equation->bits.begin(), equation->bits.end(), values.begin() + 4);
}
std::array<VkSpecializationMapEntry, 22> entries{};
for (std::uint32_t index = 0; index < entries.size(); ++index) entries[index] = {index, index * 4u, 4};
VkSpecializationInfo specialization{};
specialization.mapEntryCount = 3;
specialization.pMapEntries = entries;
specialization.mapEntryCount = static_cast<std::uint32_t>(entries.size());
specialization.pMapEntries = entries.data();
specialization.dataSize = sizeof(values);
specialization.pData = values;
specialization.pData = values.data();
VkPipelineShaderStageCreateInfo stage{VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO};
stage.stage = VK_SHADER_STAGE_COMPUTE_BIT;
stage.module = module;
@ -114,20 +138,20 @@ VkPipeline TextureDetiler::pipeline(TextureTileMode tileMode, std::uint32_t elem
return result;
}
void TextureDetiler::Dispatch(VkCommandBuffer commands, TextureTileMode tileMode, std::uint32_t elementBytes, VkBuffer source, std::uint64_t sourceOffset, VkBuffer destination, std::uint64_t destinationOffset, const TileMipLayout& layout, std::uint32_t arrayLayer) {
void TextureDetiler::Dispatch(VkCommandBuffer commands, TextureTileMode tileMode, std::uint32_t elementBytes, VkBuffer source, std::uint64_t sourceOffset, VkBuffer destination, std::uint64_t destinationOffset, const TileMipLayout& layout, bool retile, std::uint32_t slice, bool thick) {
Require(commands != VK_NULL_HANDLE, "texture detiling requires an active command buffer");
Require(source != VK_NULL_HANDLE && destination != VK_NULL_HANDLE, "texture detiling requires source and destination buffers");
Require(layout.width != 0 && layout.height != 0, "texture detiling requires a non-empty mip layout");
Require(layout.tiledSize != 0 && layout.linearSize != 0, "texture detiling requires a non-empty mip layout");
const auto target = pipeline(tileMode, elementBytes);
const auto target = pipeline(tileMode, elementBytes, retile, thick);
const auto alignment = std::max<VkDeviceSize>(context.limits.minStorageBufferOffsetAlignment, 4);
const auto sourceDescriptorOffset = sourceOffset - sourceOffset % alignment;
const auto destinationDescriptorOffset = destinationOffset - destinationOffset % alignment;
const auto sourceBase = sourceOffset - sourceDescriptorOffset;
const auto destinationBase = destinationOffset - destinationDescriptorOffset;
Require(sourceBase <= UINT32_MAX && destinationBase <= UINT32_MAX, "texture detiling buffer offset exceeds addressable range");
const auto sourceRange = (sourceBase + layout.tiledSize + 3) / 4 * 4;
const auto destinationRange = (destinationBase + layout.linearSize + 3) / 4 * 4;
const auto sourceRange = (sourceBase + (retile ? layout.linearSize : layout.tiledSize) + 3) / 4 * 4;
const auto destinationRange = (destinationBase + (retile ? layout.tiledSize : layout.linearSize) + 3) / 4 * 4;
Require(sourceRange <= context.limits.maxStorageBufferRange && destinationRange <= context.limits.maxStorageBufferRange, "texture detiling buffer range exceeds device limits");
const auto set = allocateSet();
const VkDescriptorBufferInfo sourceInfo{source, sourceDescriptorOffset, sourceRange};
@ -159,7 +183,7 @@ void TextureDetiler::Dispatch(VkCommandBuffer commands, TextureTileMode tileMode
push.tailX = layout.tailX;
push.tailY = layout.tailY;
push.elementBytes = elementBytes;
push.arrayLayer = arrayLayer;
push.slice = slice;
context.Function<PFN_vkCmdPushConstants>("vkCmdPushConstants")(commands, pipelineLayout, VK_SHADER_STAGE_COMPUTE_BIT, 0, sizeof(Push), &push);
const auto groupsX = (layout.width + 7u) / 8u;
const auto groupsY = (layout.height + 7u) / 8u;

View File

@ -1,8 +1,14 @@
#include "prx/libSceAgcDriver/Graphics/include/Recorder.hpp"
#include "prx/libSceAgcDriver/Graphics/include/TextureDetiler.hpp"
#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp"
namespace AgcDriver::Graphics {
void TextureDetiler::BeginBatch() {
GuestMemory::AssertGpuLockHeld("TextureDetiler::BeginBatch");
// Sets of recorded work that has not completed stay allocated; the pools are recycled once the
// recorder is idle again.
if (const auto* recorder = Recorder::Active(); recorder != nullptr && !recorder->Idle()) return;
for (const auto pool : descriptorPools) {
Check(context.Function<PFN_vkResetDescriptorPool>("vkResetDescriptorPool")(context.device, pool, 0), "vkResetDescriptorPool texture detiler");
}

View File

@ -45,8 +45,11 @@ BlockLayout GetBlockLayout(TextureTileMode tileMode, std::uint32_t bytesPerEleme
case TextureTileMode::kLinear: throw std::runtime_error("AGC graphics: GetBlockLayout does not apply to linear tiling");
case TextureTileMode::kStandard256B: return {256u, 1u << kLog2BlockThin256B[index].width, 1u << kLog2BlockThin256B[index].height};
case TextureTileMode::kStandard4KB: return {4096u, 1u << kLog2BlockThin4KB[index].width, 1u << kLog2BlockThin4KB[index].height};
case TextureTileMode::RenderTarget64KB:
case TextureTileMode::kStandard64KB: return {65536u, 1u << kLog2BlockThin64KB[index].width, 1u << kLog2BlockThin64KB[index].height};
case TextureTileMode::kStandard64KB:
case TextureTileMode::kZ64KBX:
case TextureTileMode::kS64KBX:
case TextureTileMode::kD64KBX:
case TextureTileMode::kR64KBX: return {65536u, 1u << kLog2BlockThin64KB[index].width, 1u << kLog2BlockThin64KB[index].height};
}
throw std::runtime_error("AGC graphics: GetBlockLayout encountered an unknown tile mode");
}
@ -92,8 +95,11 @@ bool GetMipTailLayout(TextureTileMode tileMode, const BlockLayout& block, std::u
case TextureTileMode::kStandard4KB:
out = MakeMipTailLayout(kMipTailThin4KB[index], block.blockWidth >> 1u, block.blockHeight);
return true;
case TextureTileMode::RenderTarget64KB:
case TextureTileMode::kStandard64KB:
case TextureTileMode::kZ64KBX:
case TextureTileMode::kS64KBX:
case TextureTileMode::kD64KBX:
case TextureTileMode::kR64KBX:
out = MakeMipTailLayout(kMipTailThin64KB[index], block.blockWidth >> 1u, block.blockHeight);
return true;
}
@ -167,8 +173,10 @@ std::vector<TileMipLayout> ComputeTiledMipLayout(TextureTileMode tileMode, std::
const auto paddedWidth = AlignUp(std::max(ShiftCeil(elementsWidth0, level), 1u), block.blockWidth);
const auto paddedHeight = AlignUp(std::max(ShiftCeil(elementsHeight0, level), 1u), block.blockHeight);
mip.blocksPerRow = paddedWidth / block.blockWidth;
mip.pitchBytes = paddedWidth * bytesPerElement;
mip.tiledSize = static_cast<std::uint64_t>(paddedWidth) * paddedHeight * bytesPerElement;
mip.linearSize = static_cast<std::uint64_t>(mip.width) * mip.height * bytesPerElement;
// The detiler writes linear rows at pitchBytes, so the region spans whole padded rows.
mip.linearSize = static_cast<std::uint64_t>(mip.pitchBytes) * mip.height;
mip.tail = false;
mip.tailX = 0;
mip.tailY = 0;
@ -182,8 +190,9 @@ std::vector<TileMipLayout> ComputeTiledMipLayout(TextureTileMode tileMode, std::
mip.width = TexelLevelDimension(width, level, texelWidth);
mip.height = TexelLevelDimension(height, level, texelHeight);
mip.blocksPerRow = 1u;
mip.pitchBytes = block.blockWidth * bytesPerElement;
mip.tiledSize = block.blockSize;
mip.linearSize = static_cast<std::uint64_t>(mip.width) * mip.height * bytesPerElement;
mip.linearSize = static_cast<std::uint64_t>(mip.pitchBytes) * mip.height;
mip.tail = true;
mip.tailX = tail.locations[level - firstTailLevel].x;
mip.tailY = tail.locations[level - firstTailLevel].y;
@ -192,10 +201,17 @@ std::vector<TileMipLayout> ComputeTiledMipLayout(TextureTileMode tileMode, std::
std::uint64_t offset = firstTailLevel < mipCount ? block.blockSize : 0;
for (auto level = firstTailLevel; level-- > 0;) {
mips[level].tiledOffset = offset;
mips[level].linearOffset = offset;
offset += mips[level].tiledSize;
}
// Tail mips share one tiled block but each needs its own linear region, placed after the chain.
std::uint64_t linearCursor = 0;
for (auto level = std::uint32_t{0}; level < firstTailLevel; ++level) linearCursor = std::max(linearCursor, mips[level].linearOffset + mips[level].linearSize);
linearCursor = std::max(linearCursor, offset);
for (auto level = firstTailLevel; level < mipCount; ++level) {
mips[level].tiledOffset = 0;
mips[level].linearOffset = linearCursor;
linearCursor += mips[level].linearSize;
}
Require(offset == blockSliceSize, "tiled texture mip chain geometry is inconsistent");
@ -203,15 +219,6 @@ std::vector<TileMipLayout> ComputeTiledMipLayout(TextureTileMode tileMode, std::
Require(mip.width != 0 && mip.height != 0, "computed a zero-sized tiled texture mip level");
Require(mip.tiledSize != 0 && mip.linearSize != 0, "computed a zero-sized tiled texture mip level");
}
std::uint64_t linearOffset = 0;
for (auto& mip : mips) {
const auto alignment = std::max(bytesPerElement, 4u);
linearOffset = (linearOffset + alignment - 1u) / alignment * alignment;
mip.linearOffset = linearOffset;
mip.pitchBytes = mip.width * bytesPerElement;
mip.linearSize = (static_cast<std::uint64_t>(mip.pitchBytes) * mip.height + alignment - 1u) / alignment * alignment;
linearOffset += mip.linearSize;
}
return mips;
}
@ -225,14 +232,88 @@ std::vector<TileMipLayout> ComputeMipLayout(TextureTileMode tileMode, std::uint3
const auto texelWidth = BlockWidth(format);
const auto texelHeight = BlockHeight(format);
if (tileMode == TextureTileMode::RenderTarget64KB) {
Require(!IsBlockCompressed(format), "render target tiling does not support block compressed formats");
Require(format != 128 && format != 129 && format != 132, "texture format does not support render target tiling");
}
if (tileMode == TextureTileMode::kLinear) return ComputeLinearMipLayout(bytesPerElement, texelWidth, texelHeight, width, height, mipCount);
return ComputeTiledMipLayout(tileMode, bytesPerElement, texelWidth, texelHeight, width, height, mipCount);
}
std::array<std::uint32_t, 3> ThickBlockExtent(TextureTileMode tileMode, std::uint32_t bytesPerElement) {
// addrlib Block4K_Log2_3d / Block64K_Log2_3d.
constexpr std::uint8_t thick4KB[5][3] = {{4, 4, 4}, {3, 4, 4}, {3, 4, 3}, {3, 3, 3}, {2, 3, 3}};
constexpr std::uint8_t thick64KB[5][3] = {{6, 5, 5}, {5, 5, 5}, {5, 5, 4}, {5, 4, 4}, {4, 4, 4}};
Require(std::has_single_bit(bytesPerElement) && bytesPerElement <= 16u, "unsupported bytes per element for thick texture geometry");
const auto index = static_cast<std::size_t>(std::countr_zero(bytesPerElement));
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");
}
}
ThickLayout ComputeThickLayout(TextureTileMode tileMode, std::uint32_t format, std::uint32_t width, std::uint32_t height, std::uint32_t depth) {
Require(width != 0 && height != 0 && depth != 0, "cannot compute layout for a zero-sized 3D texture");
const auto bytesPerElement = BytesPerElement(format);
Require(BlockWidth(format) == 1u && BlockHeight(format) == 1u, "block-compressed 3D textures are not implemented");
ThickLayout result{};
result.depth = depth;
auto& mip = result.mip;
mip.width = width;
mip.height = height;
mip.tail = false;
if (tileMode == TextureTileMode::kLinear) {
const auto paddedWidth = AlignUp(width, CalcLinearBlockWidth(bytesPerElement));
mip.blocksPerRow = paddedWidth;
mip.pitchBytes = paddedWidth * bytesPerElement;
mip.tiledSize = static_cast<std::uint64_t>(mip.pitchBytes) * height;
result.blockDepth = 1;
result.slabBytes = mip.tiledSize;
} else {
const auto block = ThickBlockExtent(tileMode, bytesPerElement);
const auto paddedWidth = AlignUp(width, block[0]);
const auto paddedHeight = AlignUp(height, block[1]);
mip.blocksPerRow = paddedWidth / block[0];
mip.pitchBytes = paddedWidth * bytesPerElement;
result.blockDepth = block[2];
result.slabBytes = static_cast<std::uint64_t>(mip.blocksPerRow) * (paddedHeight / block[1]) * (tileMode == TextureTileMode::kStandard4KB ? 4096u : 65536u);
mip.tiledSize = result.slabBytes;
}
mip.linearSize = static_cast<std::uint64_t>(mip.pitchBytes) * height;
result.sliceLinearBytes = mip.linearSize;
result.guestBytes = result.slabBytes * ((depth + result.blockDepth - 1u) / result.blockDepth);
return result;
}
SurfaceGeometry DescribeSurface(const GuestTextureResource& descriptor) {
SurfaceGeometry geometry;
if (descriptor.dimension == TextureDimension::k3D) {
Require(descriptor.mipCount == 1, "mipmapped 3D textures are not implemented");
const auto depth = descriptor.depthOrLastArray + 1u;
const auto thick = ComputeThickLayout(descriptor.tileMode, descriptor.format, descriptor.width, descriptor.height, depth);
geometry.mips = {thick.mip};
geometry.layers = depth;
geometry.imageDepth = depth;
geometry.guestBytes = thick.guestBytes;
geometry.sliceLinearBytes = thick.sliceLinearBytes;
geometry.thick = descriptor.tileMode != TextureTileMode::kLinear;
geometry.blockDepth = thick.blockDepth;
geometry.layerBytes = thick.slabBytes;
return geometry;
}
geometry.mips = ComputeMipLayout(descriptor.tileMode, descriptor.format, descriptor.width, descriptor.height, descriptor.mipCount);
const bool layered = descriptor.dimension == TextureDimension::k2DArray || descriptor.dimension == TextureDimension::kCube;
geometry.layers = layered ? descriptor.depthOrLastArray + 1u : 1u;
geometry.imageLayers = geometry.layers;
geometry.guestBytes = ComputeSurfaceSize(geometry.mips, geometry.layers);
geometry.layerBytes = geometry.guestBytes / geometry.layers;
for (const auto& mip : geometry.mips) geometry.sliceLinearBytes = std::max(geometry.sliceLinearBytes, mip.linearOffset + mip.linearSize);
return geometry;
}
std::vector<TileMipLayout> ComputeElementMipLayout(TextureTileMode tileMode, std::uint32_t bytesPerElement, std::uint32_t width, std::uint32_t height, std::uint32_t mipCount) {
Require(width != 0 && height != 0 && mipCount != 0 && mipCount <= 16u, "invalid surface mip chain");
if (tileMode == TextureTileMode::kLinear) return ComputeLinearMipLayout(bytesPerElement, 1u, 1u, width, height, mipCount);
return ComputeTiledMipLayout(tileMode, bytesPerElement, 1u, 1u, width, height, mipCount);
}
std::uint64_t ComputeSurfaceSize(const std::vector<TileMipLayout>& mips, std::uint32_t arrayLayers) {
Require(!mips.empty(), "cannot compute surface size for an empty mip chain");
Require(arrayLayers != 0, "cannot compute surface size for zero array layers");

View File

@ -12,25 +12,19 @@ add_custom_command(
foreach(agcTarget IN ITEMS libSceAgcDriver agc_driver_visual_test agc_driver_graphics_tests agc_driver_bda_device_tests)
if(TARGET ${agcTarget})
target_sources(${agcTarget} PRIVATE Graphics/src/BufferPool.cpp Graphics/src/CommandCompletion.cpp)
endif()
endforeach()
foreach(agcTarget IN ITEMS libSceAgcDriver agc_driver_visual_test agc_driver_graphics_tests agc_driver_bda_device_tests)
if(TARGET ${agcTarget})
target_sources(${agcTarget} PRIVATE Execution/src/GuestGpuRange.cpp)
target_sources(${agcTarget} PRIVATE Graphics/src/BufferPool.cpp)
endif()
endforeach()
foreach(agcTarget IN ITEMS libSceAgcDriver agc_driver_visual_test agc_driver_graphics_tests)
if(TARGET ${agcTarget})
target_sources(${agcTarget} PRIVATE Graphics/src/TextureDetilerDescriptors.cpp Graphics/src/TextureCache.cpp Graphics/src/RegisteredGpuMemory.cpp)
target_sources(${agcTarget} PRIVATE Graphics/src/TextureDetilerDescriptors.cpp Graphics/src/TextureCache.cpp)
endif()
endforeach()
foreach(agcTarget IN ITEMS libSceAgcDriver agc_driver_visual_test agc_driver_graphics_tests agc_driver_bda_device_tests)
if(TARGET ${agcTarget})
target_sources(${agcTarget} PRIVATE Graphics/src/GpuColorTransfer.cpp Graphics/src/ColorMemoryWriteback.cpp ${agcColorTransferHeader})
target_sources(${agcTarget} PRIVATE Graphics/src/GpuColorTransfer.cpp ${agcColorTransferHeader})
endif()
endforeach()
@ -41,14 +35,6 @@ if(TARGET agc_driver_visual_test)
target_sources(agc_driver_visual_test PRIVATE Execution/src/DisplayBuffer.cpp)
endif()
foreach(agcTarget IN ITEMS libSceAgcDriver agc_driver_visual_test agc_driver_graphics_tests)
if(TARGET ${agcTarget})
target_sources(${agcTarget} PRIVATE Graphics/src/RenderCache.cpp Graphics/src/RenderMemoryOwnership.cpp Graphics/src/DrawQueue.cpp Graphics/src/DrawCompletion.cpp Graphics/src/RenderTexture.cpp Graphics/src/DescriptorCache.cpp)
endif()
endforeach()
foreach(agcTarget IN ITEMS libSceAgcDriver agc_driver_visual_test)
if(TARGET ${agcTarget})
target_sources(${agcTarget} PRIVATE Execution/src/PresentationImage.cpp Execution/src/DeviceSynchronization.cpp Graphics/src/GraphicsPipelineCache.cpp)
endif()
endforeach()
# Upstream's GPU-resident draw path (RenderCache, DrawQueue, RenderTexture, PresentationImage,
# GraphicsPipelineCache) targets the single-target Pipeline/GpuColorTransfer API; the multi-target
# draw path in Draw.cpp does not use it yet, so those sources stay out of the build.

View File

@ -5,16 +5,26 @@
#include "SceTypes.hpp"
#include "prx/libc/include/General.hpp"
// Resource registration feeds GPU debugging tools, none of which are attached; the game treats this code as benign.
static constexpr int SCE_AGC_ERROR_RESOURCE_REGISTRATION_UNAVAILABLE = static_cast<int>(0x8A6C9018);
extern "C" {
int APS5_VABI sceAgcDriverRegisterOwner(void) {
NotImplemented_nid_no_patch(__func__);
return 0;
int APS5_VABI sceAgcDriverRegisterOwner(uint32_t* owner_handle, const char* name) {
(void)owner_handle;
(void)name;
return SCE_AGC_ERROR_RESOURCE_REGISTRATION_UNAVAILABLE;
}
int APS5_VABI sceAgcDriverRegisterResource(void) {
NotImplemented_nid_no_patch(__func__);
return 0;
int APS5_VABI sceAgcDriverRegisterResource(uint32_t* resource_handle, uint32_t owner_handle, const void* memory, size_t size, const char* name, uint32_t type, uint64_t user_data) {
(void)resource_handle;
(void)owner_handle;
(void)memory;
(void)size;
(void)name;
(void)type;
(void)user_data;
return SCE_AGC_ERROR_RESOURCE_REGISTRATION_UNAVAILABLE;
}
int APS5_VABI sceAgcDriverRegisterWorkloadStream(uint32_t stream_id, const void* stream) {
@ -25,14 +35,13 @@ int APS5_VABI sceAgcDriverRegisterWorkloadStream(uint32_t stream_id, const void*
}
int APS5_VABI sceAgcDriverUnregisterOwnerAndResources(uint32_t owner_handle) {
(void)owner_handle;
NotImplemented_nid_no_patch(__func__);
return 0;
(void)owner_handle;
return SCE_AGC_ERROR_RESOURCE_REGISTRATION_UNAVAILABLE;
}
int APS5_VABI sceAgcDriverUnregisterResource(void) {
NotImplemented_nid_no_patch(__func__);
return 0;
int APS5_VABI sceAgcDriverUnregisterResource(uint32_t resource_handle) {
(void)resource_handle;
return SCE_AGC_ERROR_RESOURCE_REGISTRATION_UNAVAILABLE;
}
}

View File

@ -0,0 +1,112 @@
#include <cstdint>
#include <cstddef>
#include "SceTypes.hpp"
#include "prx/libc/include/General.hpp"
extern "C" {
int APS5_VABI sceAgcDriverFindResourcesPublic() {
NotImplemented_nid_no_patch(__func__);
return 0;
}
int APS5_VABI sceAgcDriverGetDefaultOwner() {
NotImplemented_nid_no_patch(__func__);
return 0;
}
int APS5_VABI sceAgcDriverGetOwnerName() {
NotImplemented_nid_no_patch(__func__);
return 0;
}
int APS5_VABI sceAgcDriverGetResourceBaseAddressAndSizeInBytes() {
NotImplemented_nid_no_patch(__func__);
return 0;
}
int APS5_VABI sceAgcDriverGetResourceName() {
NotImplemented_nid_no_patch(__func__);
return 0;
}
int APS5_VABI sceAgcDriverGetResourceRegistrationMaxNameLength() {
NotImplemented_nid_no_patch(__func__);
return 0;
}
int APS5_VABI sceAgcDriverGetResourceShaderGuid() {
NotImplemented_nid_no_patch(__func__);
return 0;
}
int APS5_VABI sceAgcDriverGetResourceType() {
NotImplemented_nid_no_patch(__func__);
return 0;
}
int APS5_VABI sceAgcDriverGetResourceUserData() {
NotImplemented_nid_no_patch(__func__);
return 0;
}
int APS5_VABI sceAgcDriverGetShaderDebuggingStatus() {
NotImplemented_nid_no_patch(__func__);
return 0;
}
int APS5_VABI sceAgcDriverRegisterDefaultOwner(uint32_t* owner_handle) {
(void)owner_handle;
return static_cast<int>(0x8A6C9018);
}
int APS5_VABI sceAgcDriverRegisterGdsResource() {
NotImplemented_nid_no_patch(__func__);
return 0;
}
int APS5_VABI sceAgcDriverSetResourceUserData() {
NotImplemented_nid_no_patch(__func__);
return 0;
}
int APS5_VABI sceAgcDriverSetValidationErrorOutputFrequency() {
NotImplemented_nid_no_patch(__func__);
return 0;
}
int APS5_VABI sceAgcDriverUnregisterAllResourcesForOwner() {
NotImplemented_nid_no_patch(__func__);
return 0;
}
int APS5_VABI sceAgcDriverUnregisterWorkloadStream() {
NotImplemented_nid_no_patch(__func__);
return 0;
}
APS5_EXPORT("+iAOE3jCnkk", sceAgcDriverUnknown__PiAOE3jCnkk);
int APS5_VABI sceAgcDriverUnknown__PiAOE3jCnkk() {
NotImplemented_nid_no_patch(__func__);
return 0;
}
APS5_EXPORT("ICaGtkEIXTk", sceAgcDriverUnknown_ICaGtkEIXTk);
int APS5_VABI sceAgcDriverUnknown_ICaGtkEIXTk() {
NotImplemented_nid_no_patch(__func__);
return 0;
}
APS5_EXPORT("WHIOMbb+iIU", sceAgcDriverUnknown_WHIOMbb_PiIU);
int APS5_VABI sceAgcDriverUnknown_WHIOMbb_PiIU() {
NotImplemented_nid_no_patch(__func__);
return 0;
}
APS5_EXPORT("chJWZcNSzjk", sceAgcDriverUnknown_chJWZcNSzjk);
int APS5_VABI sceAgcDriverUnknown_chJWZcNSzjk() {
NotImplemented_nid_no_patch(__func__);
return 0;
}
}

View File

@ -149,21 +149,17 @@ void RunGuestSamplerResourceTests() {
badUnorm.forceUnormCoords = true;
rejectFields(badUnorm, "unnormalized coordinates");
Fields badThreshold = base;
badThreshold.anisoThreshold = 1;
rejectFields(badThreshold, "anisotropy threshold override");
Fields badSrgb = base;
badSrgb.forceSrgb = true;
rejectFields(badSrgb, "forces sRGB decoding");
Fields badAnisoBias = base;
badAnisoBias.anisoBias = 1;
rejectFields(badAnisoBias, "anisotropy bias");
Fields badTrunc = base;
badTrunc.truncCoord = true;
rejectFields(badTrunc, "coordinate truncation");
Fields truncated = base;
truncated.truncCoord = true;
truncated.perfMip = 1;
truncated.perfZ = 1;
truncated.anisoThreshold = 1;
truncated.anisoBias = 1;
static_cast<void>(DecodeSamplerResource(pack(truncated)));
Fields badCubeWrap = base;
badCubeWrap.disableCubeWrap = true;
@ -177,13 +173,6 @@ void RunGuestSamplerResourceTests() {
badDegamma.disableDegamma = true;
rejectFields(badDegamma, "disables degamma");
Fields badPerf = base;
badPerf.perfMip = 1;
rejectFields(badPerf, "performance counters");
badPerf = base;
badPerf.perfZ = 1;
rejectFields(badPerf, "performance counters");
Fields badLodBiasSec = base;
badLodBiasSec.lodBiasSec = 1;
rejectFields(badLodBiasSec, "secondary LOD bias");

View File

@ -179,9 +179,12 @@ void RunGuestTextureResourceTests() {
badMinLod.minLod = 1;
rejectFields(badMinLod, "nonzero minimum LOD clamp");
Fields badMinLodWarn = base;
badMinLodWarn.minLodWarn = 1;
rejectFields(badMinLodWarn, "minimum LOD warning threshold");
// Streaming feedback fields decode; nothing is reported back.
Fields feedback = base;
feedback.minLodWarn = 1;
feedback.mipStatsCntId = 1;
feedback.mipStatsCntEn = true;
static_cast<void>(DecodeTextureResource(pack(feedback)));
const auto unmodulated = DecodeTextureResource(pack(base));
for (std::uint32_t perfMod = 0; perfMod < 8; ++perfMod) {
@ -196,13 +199,6 @@ void RunGuestTextureResourceTests() {
rejectFields(modulated, "corner sampling");
}
Fields badMipStats = base;
badMipStats.mipStatsCntId = 1;
rejectFields(badMipStats, "mip statistics counters");
badMipStats = base;
badMipStats.mipStatsCntEn = true;
rejectFields(badMipStats, "mip statistics counters");
Fields badCorner = base;
badCorner.cornerSample = true;
rejectFields(badCorner, "corner sampling");

View File

@ -0,0 +1,125 @@
// agc_shader_replay <shader_*.req>...: replays compute recompile requests the driver saved with
// APS5_DUMP_SHADERS=1, running the same resource analysis and recompile steps without the game.
#include "ControlFlow/RequestSerializer.hpp"
#include "Optimization/ResourceMaterializer.hpp"
#include "Optimization/ResourceProgram.hpp"
#include "Recompiler.hpp"
#include "RdnaDecoder/RdnaInstructionDecoder.hpp"
#if ANYPS5_ENABLE_SPIRV_TOOLS
#include "spirv-tools/libspirv.hpp"
#endif
#include <cstdio>
#include <exception>
#include <fstream>
#include <sstream>
#include <string>
namespace {
std::string ReadText(const char* path) {
std::ifstream file(path, std::ios::binary);
std::ostringstream stream;
stream << file.rdbuf();
auto text = stream.str();
while (!text.empty() && (text.back() == '\n' || text.back() == '\r' || text.back() == ' ')) text.pop_back();
return text;
}
// The error text without the serialized request that Recompile appends.
std::string FirstLine(const std::string& text) {
const auto request = text.find("\nRecompileRequest:");
auto shown = request == std::string::npos ? text : text.substr(0, request);
if (shown.size() > 2000) shown.resize(2000);
return shown;
}
bool g_disassemble = false;
bool g_assembly = false;
bool g_memory = false;
bool Replay(const char* path) {
const auto request = ShaderRecompiler::RequestSerializer{}.Deserialize(ReadText(path));
std::printf("%s: %zu code words, %zu user data, %zu memory regions, wave%u\n", path, request.request.shader.code.size(), request.request.context.userData.size(), request.request.context.memory.size(), request.request.context.waveSize);
if (request.request.context.compute.has_value()) {
const auto& compute = *request.request.context.compute;
std::printf(" compute: threads %ux%ux%u, lds %u dwords, group ids %d%d%d, tg size %d, thread id components %u\n", compute.numThreads[0], compute.numThreads[1], compute.numThreads[2], compute.ldsSizeDwords, compute.groupIdEnable[0], compute.groupIdEnable[1], compute.groupIdEnable[2], compute.tgSizeEnable, compute.threadIdComponentCount);
}
if (g_memory) {
// --mem: the captured inputs. User data words are printed; each memory region is written to
// mem_<code address>_<guest address>.bin next to the request for inspection with other tools.
const auto& context = request.request.context;
for (std::size_t i = 0; i < context.userData.size(); ++i) std::printf(" user[%zu] = 0x%08x\n", i, context.userData[i]);
for (const auto& region : context.memory) {
char name[96];
std::snprintf(name, sizeof(name), "mem_%llx_%llx.bin", static_cast<unsigned long long>(request.request.shader.codeAddress), static_cast<unsigned long long>(region.guestAddress));
if (std::FILE* file = std::fopen(name, "wb")) {
std::fwrite(region.bytes.data(), 1, region.bytes.size(), file);
std::fclose(file);
}
std::printf(" region 0x%llx + 0x%zx -> %s\n", static_cast<unsigned long long>(region.guestAddress), region.bytes.size(), name);
}
}
if (g_assembly) {
const auto program = ShaderRecompiler::RdnaInstructionDecoder{}.Decode(request.request.shader.code);
// Raw first words carry what the text omits (branch offsets, waitcnt fields).
for (const auto& instruction : program.instructions) std::printf("raw=%08x %s\n", instruction.rawWords[0], ShaderRecompiler::RdnaInstructionToString(instruction).c_str());
}
try {
auto program = ShaderRecompiler::PrepareResourceProgram(request.request);
constexpr ShaderRecompiler::ResourceMaterializer materializer;
static_cast<void>(materializer.ExtractPlan(program));
} catch (const std::exception& error) {
std::printf(" resource analysis failed: %s\n", FirstLine(error.what()).c_str());
return false;
}
try {
const auto result = ShaderRecompiler::Recompile(request.request);
std::printf(" recompiled: %zu SPIR-V words\n", result.spirv.size());
#if ANYPS5_ENABLE_SPIRV_TOOLS
if (g_disassemble) {
spvtools::SpirvTools tools(SPV_ENV_VULKAN_1_2);
std::string text;
if (tools.Disassemble(result.spirv, &text, SPV_BINARY_TO_TEXT_OPTION_FRIENDLY_NAMES)) std::printf("%s\n", text.c_str());
}
#else
if (g_disassemble) std::printf(" (--dis needs ANYPS5_ENABLE_SPIRV_TOOLS=ON)\n");
#endif
return true;
} catch (const std::exception& error) {
std::printf(" recompile failed: %s\n", FirstLine(error.what()).c_str());
return false;
}
}
}
int main(int argc, char** argv) {
if (argc < 2) {
std::fprintf(stderr, "usage: agc_shader_replay [--dis] <shader.req>...\n the driver writes shader_<address>.req files when APS5_DUMP_SHADERS is set\n");
return 2;
}
int failures = 0;
for (int i = 1; i < argc; ++i) {
if (std::string(argv[i]) == "--dis") {
g_disassemble = true;
continue;
}
if (std::string(argv[i]) == "--asm") {
g_assembly = true;
continue;
}
if (std::string(argv[i]) == "--mem") {
g_memory = true;
continue;
}
try {
if (!Replay(argv[i])) ++failures;
} catch (const std::exception& error) {
std::printf("%s: could not load request: %s\n", argv[i], FirstLine(error.what()).c_str());
++failures;
}
}
return failures == 0 ? 0 : 1;
}