Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions cmake/RosettaGraphics.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ add_library(libSceAgcDriver SHARED EXCLUDE_FROM_ALL
${rosettaAgcDriver}/Execution/Metal/MetalDriver.mm
${rosettaAgcDriver}/Execution/Metal/MetalPacketExecution.mm
${rosettaAgcDriver}/Execution/Metal/DirectDriver.mm
${rosettaAgcDriver}/Execution/Metal/AgcFrameCapture.mm
${rosettaAgcDriver}/Graphics/src/ComputeStageInfo.cpp
${rosettaAgcDriver}/Graphics/src/State.cpp
${rosettaAgcDriver}/Graphics/src/SurfaceMetadata.cpp
Expand Down Expand Up @@ -132,6 +133,21 @@ add_custom_command(
VERBATIM)
add_custom_target(rosetta_metallib DEPENDS ${PATCHED_DIR}/AnyPS5Utilities.metallib)

# Frame-capture replay benchmark (tools/agc-replay): replays a frame captured with
# ANYPS5_AGC_CAPTURE through this libSceAgcDriver, under Rosetta like the game.
add_executable(anyps5_agc_replay ${CMAKE_SOURCE_DIR}/tools/agc-replay/AgcReplay.mm)
target_compile_options(anyps5_agc_replay PRIVATE -fobjc-arc)
target_compile_features(anyps5_agc_replay PRIVATE cxx_std_20)
target_compile_definitions(anyps5_agc_replay PRIVATE ANYPS5_METAL_GUEST_REPLAY=1 ANYPS5_METAL_BACKEND=1 ANYPS5_ROSETTA_DIRECT=1
ANYPS5_AGC_REPLAY_METALLIB="${PATCHED_DIR}/AnyPS5Utilities.metallib")
target_include_directories(anyps5_agc_replay PRIVATE ${ANYPS5_ROSETTA_LIBS_ROOT} ${ANYPS5_VULKAN_HEADERS_INCLUDE_DIR}
$<TARGET_PROPERTY:anyps5_rosetta_shader_recompiler,INTERFACE_INCLUDE_DIRECTORIES>)
target_link_libraries(anyps5_agc_replay PRIVATE libSceAgcDriver libkernel libc "-framework Metal" "-framework Foundation"
"-framework CoreGraphics" "-framework ImageIO")
set_target_properties(anyps5_agc_replay PROPERTIES CXX_EXTENSIONS OFF BUILD_RPATH ${UNPATCHED_DIR}
RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/rosetta/bin)
add_dependencies(anyps5_agc_replay rosetta_metallib)

function(anyps5_rosetta_graphics_tests)
if(NOT ANYPS5_ROSETTA_BUILD_TESTING)
return()
Expand Down
90 changes: 90 additions & 0 deletions core/libs/prx/libSceAgcDriver/Execution/Metal/AgcCaptureLogic.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
#pragma once

// Pure bookkeeping shared by the AGC frame capture and the replay tool (no Metal, no I/O), so the
// two rules below can be tested on their own.

#include <algorithm>
#include <cstddef>
#include <cstdint>
#include <vector>

namespace AgcDriver::Metal::AgcCapture {

// Which presents belong to the recorded frames, and when the capture is complete. Present k scans
// out flip k, so the presents of the recorded flips are target .. target + frames - 1. A present
// in that window without a buffer (a blanked output) still takes its slot: it has no Display
// record, but it counts towards completion, otherwise the End record would never be written.
class PresentTracker {
public:
struct Step {
bool inWindow = false;
bool recordDisplay = false; // the present has a buffer to record
std::uint32_t frame = 0;
bool complete = false; // every frame of the window has been presented
};

// `armed` is false while the capture is not recording: the present is only counted.
Step Observe(std::uint64_t target, std::uint64_t frames, bool armed, bool hasBuffer) {
++presents_;
Step step;
if (!armed || presents_ < target || presents_ >= target + frames) return step;
step.inWindow = true;
step.recordDisplay = hasBuffer;
step.frame = static_cast<std::uint32_t>(presents_ - target);
step.complete = ++seen_ == frames;
return step;
}
std::uint64_t Presents() const { return presents_; }

private:
std::uint64_t presents_ = 0;
std::uint64_t seen_ = 0;
};

// The guest ranges of a capture over its submissions. A range recorded while the frame was running
// exists only from the submission that introduced it (`activation`, an index into the capture's
// global submission order; 0 for the pre-frame snapshot), and replaces an earlier range it overlaps.
class RangeTimeline {
public:
void Add(std::uint64_t guest, std::uint64_t bytes, std::size_t activation) { ranges_.push_back({guest, bytes, activation}); }

// Indices (in Add order) of the ranges that exist when `submission` runs.
std::vector<std::size_t> LiveAt(std::size_t submission) const {
std::vector<std::size_t> live;
for (std::size_t i = 0; i < ranges_.size(); ++i) {
if (ranges_[i].activation > submission) continue;
bool replaced = false;
for (std::size_t j = 0; j < ranges_.size() && !replaced; ++j)
replaced = j != i && ranges_[j].activation <= submission && Replaces(j, i);
if (!replaced) live.push_back(i);
}
return live;
}

// The first submission after `submission` at which the set of live ranges changes, or npos.
std::size_t NextChange(std::size_t submission) const {
std::size_t next = npos;
for (const auto& range : ranges_)
if (range.activation > submission) next = std::min(next, range.activation);
return next;
}

static constexpr std::size_t npos = ~std::size_t{0};

private:
struct Entry {
std::uint64_t guest;
std::uint64_t bytes;
std::size_t activation;
};
// Whether range j (activated no later than the submission asked about) takes the place of range i.
bool Replaces(std::size_t j, std::size_t i) const {
const auto& a = ranges_[j];
const auto& b = ranges_[i];
const bool overlap = a.guest < b.guest + b.bytes && b.guest < a.guest + a.bytes;
return overlap && (a.activation > b.activation || (a.activation == b.activation && j > i));
}
std::vector<Entry> ranges_;
};

}
157 changes: 157 additions & 0 deletions core/libs/prx/libSceAgcDriver/Execution/Metal/AgcFrameCapture.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
#pragma once

// Frame capture and replay for the Metal AGC driver (fork-only; tools/agc-replay).
//
// Capture (off unless ANYPS5_AGC_CAPTURE=<dir> is set): after the queue workers have executed
// flip ANYPS5_AGC_CAPTURE_FLIP - 1 (default 600; ANYPS5_AGC_CAPTURE_AFTER_S=<seconds> captures the
// first flip executed after that long since the driver loaded instead), the worker that ran that flip drains the GPU and
// writes <dir>/frame.agc:
// - every borrowed guest range the driver sees (GPU-visible guest memory plus its staging range)
// and the contents of its resident, non-zero 1 MiB chunks, compressed: the pre-frame state;
// - the queue state (registers, constant RAM, index and indirect bases, predication) of each
// queue at its first recorded submission;
// - every submission the workers execute until flip N + ANYPS5_AGC_CAPTURE_FRAMES - 1, in
// execution order: queue, command address and words, the shader registry it was submitted
// with, and the 64 KiB guest pages that changed since the previous submission (taken just
// before it executes, with the GPU drained), each with the hash of its previous contents;
// - for each captured flip, the display buffer the next present scanned out and its pixels
// (the frame the game showed), also written as <dir>/game-<frame>.png.
// The hooks sit at the submission boundary (queue worker loop) and in Present; they never touch
// the per-draw path. Capturing stalls the game for the snapshot (seconds) once.
//
// Replay (anyps5_agc_replay) maps the ranges at their guest addresses, submits the recorded
// command buffers through the same driver, and applies each recorded page delta from the worker
// just before that submission executes: a page is written only while the replay's copy still
// equals the game's previous contents, so pages the replayed GPU work wrote keep the replay's
// bytes (the delta carries CPU writes the guest made between submissions).
//
// File format (little-endian, version 1): "AGCFRAME", u32 version, u32 compression algorithm
// (libcompression constant), then records {u32 tag, u32 reserved, u64 payload bytes, payload}.

#include "prx/libSceAgcDriver/Execution/include/Driver/Queues/Submission.hpp"
#include "prx/libSceAgcDriver/Execution/include/DisplayBuffer.hpp"
#include "prx/libSceAgcDriver/Execution/include/NativeGuestMemory.hpp"
#include "prx/libSceAgcDriver/Execution/include/QueueState.hpp"
#include <cstddef>
#include <cstdint>
#include <cstring>
#include <filesystem>
#include <functional>
#include <mutex>
#include <span>
#include <stdexcept>
#include <string>
#include <vector>

namespace AgcDriver::Metal::AgcCapture {

inline constexpr char Magic[8] = {'A', 'G', 'C', 'F', 'R', 'A', 'M', 'E'};
inline constexpr std::uint32_t Version = 1;
inline constexpr std::uint64_t ChunkBytes = std::uint64_t{1} << 20;
inline constexpr std::uint64_t PageBytes = std::uint64_t{64} << 10;

enum class Tag : std::uint32_t {
Range = 1, // u64 guest, u64 bytes, u32 writable, u32 0
Chunk = 2, // u64 guest, u32 raw bytes, u32 stored bytes (== raw: uncompressed), u64 hash, data
Shader = 3, // u32 id, u32 type, u64 code address, u64 header address, u32 code words, u32 header bytes, code, header
Registry = 4, // u32 id, u32 entries, entries x {u64 key, u32 shader id}
QueueState = 5, // u32 queue, serialized QueueState
Submit = 6, // u32 queue, u32 registry id, u64 command address, u32 words, u32 pages, u32 frame, u32 0,
// words x u32, pages x {u64 guest, u32 raw, u32 stored, u64 previous hash, u64 hash, data}
Display = 7, // u32 frame, u32 0, DisplayBuffer fields, u64 pixel hash, u32 raw, u32 stored, data
End = 8, // u32 frames, u32 submissions
};

// FNV-style 4-lane hash; the same function hashes chunks, pages and display pixels.
inline std::uint64_t Hash(const void* data, std::size_t bytes) {
const auto* p = static_cast<const unsigned char*>(data);
std::uint64_t lanes[4] = {0x9e3779b97f4a7c15ull, 0xc2b2ae3d27d4eb4full, 0x165667b19e3779f9ull, 0x27d4eb2f165667c5ull};
std::size_t i = 0;
for (; i + 32 <= bytes; i += 32) {
for (int lane = 0; lane < 4; ++lane) {
std::uint64_t word;
std::memcpy(&word, p + i + 8 * lane, 8);
lanes[lane] = (lanes[lane] ^ word) * 0x100000001b3ull;
}
}
std::uint64_t h = lanes[0] ^ (lanes[1] << 1) ^ (lanes[2] << 2) ^ (lanes[3] << 3) ^ bytes;
for (; i < bytes; ++i) h = (h ^ p[i]) * 0x100000001b3ull;
h ^= h >> 33;
h *= 0xff51afd7ed558ccdull;
h ^= h >> 33;
return h;
}

class Writer {
public:
std::vector<std::byte> bytes;
template <class T> void Put(const T& value) {
const auto* p = reinterpret_cast<const std::byte*>(&value);
bytes.insert(bytes.end(), p, p + sizeof(T));
}
void PutBytes(const void* data, std::size_t size) {
const auto* p = static_cast<const std::byte*>(data);
bytes.insert(bytes.end(), p, p + size);
}
};

class Reader {
public:
Reader(const std::byte* data, std::size_t size) : data(data), size(size) {}
template <class T> T Get() {
T value;
std::memcpy(&value, Take(sizeof(T)), sizeof(T));
return value;
}
const std::byte* Take(std::size_t count) {
if (count > size - offset) throw std::runtime_error("agc capture: truncated record");
const auto* p = data + offset;
offset += count;
return p;
}
[[nodiscard]] bool Done() const { return offset == size; }

private:
const std::byte* data;
std::size_t size;
std::size_t offset = 0;
};

void WriteQueueState(Writer& out, const QueueState& state);
QueueState ReadQueueState(Reader& in);

// Compresses with the capture's algorithm; returns raw bytes when compression does not help.
std::vector<std::byte> Compress(const void* data, std::size_t bytes);
// Decompresses `stored` bytes into exactly `raw` bytes at `target` (a copy when stored == raw).
void Decompress(const std::byte* stored, std::size_t storedBytes, void* target, std::size_t raw);

// Writes width x height BGRA8 pixels (scanout layout) as a PNG.
bool WriteBgraPng(const std::filesystem::path& path, const std::vector<std::byte>& pixels, std::uint32_t width, std::uint32_t height);

// What a hook needs from the driver: the worker's ranges, the GPU lock and the drain
// (CompletePriorGpuWorkAndCopyBack, to call with the GPU lock held).
struct Context {
std::span<const NativeGuestMemory::BorrowedRange> ranges;
std::recursive_mutex& gpuMutex;
std::function<void()> drain;
};

// Whether a capture or a replay is installed (cheap; the hooks return at once otherwise).
bool Active();
// Queue worker, before and after ExecuteSubmission (not holding the GPU lock).
void BeforeExecute(const DriverDetail::Submission& submission, const QueueState& state, const Context& context);
void AfterExecute(const DriverDetail::Submission& submission, const Context& context);
// MetalDriver::Present with the GPU lock held; `pixels` are the decoded display pixels when read.
void Presented(const DisplayBuffer* buffer, const std::vector<std::byte>* pixels, const Context& context);

// Replay: called on the worker before each submission of `queue` executes, in queue order.
using ReplayHook = std::function<void(std::uint32_t queue, const Context& context)>;
void InstallReplay(ReplayHook hook);

// GPU busy time (union of command buffer GPU intervals) while enabled; MetalDevice records it.
void EnableGpuBusy(bool enabled);
bool GpuBusyEnabled();
void RecordGpuBusy(double gpuStart, double gpuEnd);
double TakeGpuBusyMs();

}
Loading