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:
parent
87911b3bb5
commit
8a69fefe13
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
};
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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, ©);
|
||||
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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
|
@ -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
|
||||
|
||||
}
|
||||
|
|
@ -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;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
};
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
};
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
};
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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);
|
||||
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
|
|
|
|||
|
|
@ -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
|
|
@ -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, ©);
|
||||
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, ©);
|
||||
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);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -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");
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
|
@ -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});
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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, ®ion);
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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
|
|
@ -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});
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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");
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
Loading…
Reference in New Issue