style: drop the branch's code comments per CONVENTIONS.md

Only the lines this branch added lose their comments; the commits before
keep them, with the reasoning behind each change, for whoever digs into
that code later.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
STL1te 2026-09-29 21:25:29 +03:00
parent 119560206d
commit 6b36c050fe
127 changed files with 8 additions and 574 deletions

View File

@ -34,10 +34,8 @@ set(ANYPS5_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS}")
set(CMAKE_EXE_LINKER_FLAGS "")
add_subdirectory(3rdparty/SDL2)
set(CMAKE_EXE_LINKER_FLAGS "${ANYPS5_EXE_LINKER_FLAGS}")
# Prebuilt FFmpeg (AJM's MP3 decoder), downloaded for the submodule's commit as in Kyty.
set(ARCHITECTURE x86_64)
add_subdirectory(3rdparty/ffmpeg-core)
# The system libraries its pkg-config files name, which ffmpeg-core adds only for MSVC.
target_link_libraries(ffmpeg INTERFACE bcrypt ole32 user32)
set(FT_DISABLE_ZLIB ON CACHE BOOL "" FORCE)

View File

@ -17,11 +17,11 @@ std::uint32_t* APS5_VABI sceAgcCbSetShRegisterRangeDirect(CommandBuffer* buf, st
}
uint32_t APS5_VABI sceAgcCbSetShRegisterRangeDirectGetSize(uint32_t num_values) {
return (num_values + 2u) * 4u; // header + offset + values
return (num_values + 2u) * 4u;
}
std::uint32_t APS5_VABI sceAgcCbSetUcRegisterRangeDirectGetSize(std::uint32_t numValues) {
return (numValues + 2u) * 4u; // header + offset + values
return (numValues + 2u) * 4u;
}
std::uint32_t* APS5_VABI sceAgcCbSetShRegistersDirect(CommandBuffer* buf, const volatile ShaderRegister* regs, std::uint32_t numRegs) {

View File

@ -15,7 +15,7 @@ std::uint32_t* APS5_VABI sceAgcCbNop_nid_postfix(CommandBuffer* buf, std::uint32
}
uint32_t APS5_VABI sceAgcCbNopGetSize(uint32_t size_in_dwords) {
return size_in_dwords * 4u; // a NOP packet is exactly size_in_dwords dwords including its header
return size_in_dwords * 4u;
}
std::uint32_t APS5_VABI sceAgcCbQueueEndOfPipeActionGetSize() {

View File

@ -7,9 +7,7 @@ std::uint32_t* WriteJump(CommandBuffer* buffer, std::uint8_t chain, std::uint8_t
CheckBits(chain, 1, function);
CheckBits(cachePolicy, 3, function);
CheckBits(sizeInDwords, 0xfffffu, function);
// Titles reserve a jump with a null target and write the address into the packet later.
Require((guestAddress & 3u) == 0, function, "misaligned jump target");
// INDIRECT_BUFFER: address, then size in bits 19:0, chain (no return) in bit 20, cache policy in 29:28.
return Emit(buffer, 0x3fu, {static_cast<std::uint32_t>(guestAddress) & ~3u, static_cast<std::uint32_t>(guestAddress >> 32u), 0x0f200000u | (static_cast<std::uint32_t>(cachePolicy) << 28u) | (static_cast<std::uint32_t>(chain) << 20u) | sizeInDwords}, function);
}

View File

@ -32,7 +32,7 @@ std::uint32_t* APS5_VABI sceAgcDcbDrawIndexOffset(CommandBuffer* buf, std::uint3
}
uint32_t APS5_VABI sceAgcDcbDrawIndexOffsetGetSize(void) {
return 20; // DRAW_INDEX_OFFSET_2: header + 4 dwords
return 20;
}
uint32_t* APS5_VABI sceAgcDcbDrawIndexIndirect(CommandBuffer* buf, uint32_t data_offset_in_bytes, uint64_t modifier) {

View File

@ -14,7 +14,7 @@ std::uint32_t* APS5_VABI sceAgcDcbSetIndexBuffer(CommandBuffer* buf, std::uint64
}
std::uint32_t APS5_VABI sceAgcDcbSetIndexBufferGetSize() {
return 12; // INDEX_BASE: header + 2 dwords
return 12;
}
std::uint32_t* APS5_VABI sceAgcDcbSetIndexCount(CommandBuffer* buf, std::uint32_t indexCount) {
@ -32,11 +32,9 @@ std::uint32_t* APS5_VABI sceAgcDcbSetIndexSize(CommandBuffer* buf, std::uint8_t
}
std::uint32_t APS5_VABI sceAgcDcbSetIndexSizeGetSize() {
return 12; // SET_UCONFIG_REG_INDEX: header + 2 dwords
return 12;
}
// Unnamed: sceAgcDcbSetIndexSize with the per-instance object id flag (VGT_INDEX_TYPE bit 14), as
// Kyty and SharpEmu build it.
APS5_EXPORT("-KRzWekV120", sceAgcUnknown__MKRzWekV120);
std::uint32_t* APS5_VABI sceAgcUnknown__MKRzWekV120(CommandBuffer* buf, std::uint8_t indexSize, std::uint8_t cachePolicy, std::uint8_t perInstanceObjectId) {
Agc::Command::CheckBits(perInstanceObjectId, 1, __func__);

View File

@ -42,7 +42,7 @@ std::uint32_t* APS5_VABI sceAgcDcbEventWrite(CommandBuffer* buf, std::uint8_t ev
}
std::uint64_t APS5_VABI sceAgcDcbEventWriteGetSize(std::uint8_t eventType) {
return (eventType & 0xfeu) == 0x38u ? 16 : 8; // EVENT_WRITE, plus the address for the 0x38/0x39 events
return (eventType & 0xfeu) == 0x38u ? 16 : 8;
}
std::uint32_t* APS5_VABI sceAgcDcbStallCommandBufferParser(CommandBuffer* buf) {

View File

@ -77,7 +77,6 @@ int APS5_VABI sceAgcUnknownFuseShaderHalves(Shader* fused_result, const Shader*
const auto& frontRsrc2 = FindRegister(front->sh_registers, frontCount, isGs ? SPI_SHADER_PGM_RSRC2_GS : SPI_SHADER_PGM_RSRC2_HS);
auto& fusedRsrc1 = FindRegister(fused, fusedCount, isGs ? SPI_SHADER_PGM_RSRC1_GS : SPI_SHADER_PGM_RSRC1_HS);
auto& fusedRsrc2 = FindRegister(fused, fusedCount, isGs ? SPI_SHADER_PGM_RSRC2_GS : SPI_SHADER_PGM_RSRC2_HS);
// Both halves run in one wave, so VGPRs are the larger of the two and shared VGPRs are reallocated (after Kyty).
const std::uint32_t frontVgprs = ((frontRsrc1.value & 0x3fu) + 1u) * 4u;
const std::uint32_t backVgprs = ((fusedRsrc1.value & 0x3fu) + 1u) * 4u;
const std::uint32_t frontTotal = frontVgprs + (frontRsrc2.value >> 28u) * 8u;
@ -94,7 +93,6 @@ int APS5_VABI sceAgcUnknownFuseShaderHalves(Shader* fused_result, const Shader*
MergeMax(fusedRsrc1, frontRsrc1, 28, 0x3u);
}
fusedRsrc2.value = (fusedRsrc2.value & 0xf7ffffc1u) | (frontRsrc2.value & 0x0800003eu);
// The fused program starts at the front half; its address goes in the ES/LS program registers.
const std::uint32_t lo = isGs ? SPI_SHADER_PGM_LO_ES : SPI_SHADER_PGM_LO_LS;
auto& loRegister = FindRegister(fused, fusedCount, lo);
auto& hiRegister = FindRegister(fused, fusedCount, lo + 1u);

View File

@ -9,7 +9,6 @@
namespace {
// INDIRECT_BUFFER as sceAgcDcbJump writes it: [1..2] target, [3] size in 19:0, chain in 20, cache policy in 29:28.
void PatchJump(std::uint32_t* cmd, const volatile std::uint32_t* target, std::uint32_t sizeInDwords, const char* function) {
Agc::Command::ValidatePacket(cmd, 0x3fu, 4, function);
const auto address = reinterpret_cast<std::uintptr_t>(target);
@ -29,7 +28,6 @@ int APS5_VABI sceAgcJumpPatchSetTarget(uint32_t* cmd, const volatile uint32_t* t
return 0;
}
// Named after Kyty's reading: a jump patch that also sets the cache policy.
APS5_EXPORT("Ikfdt-rIqCE", sceAgcUnknown_Ikfdt_MrIqCE);
int APS5_VABI sceAgcUnknown_Ikfdt_MrIqCE(uint32_t* cmd, uint64_t cache_policy, const volatile uint32_t* target, uint32_t size_in_dwords) {
Agc::Command::CheckBits(cache_policy, 3u, __func__);

View File

@ -31,7 +31,6 @@ int APS5_VABI sceAgcQueueEndOfPipeActionPatchAddress(std::uint32_t* cmd, const v
}
int APS5_VABI sceAgcQueueEndOfPipeActionPatchData(uint32_t* cmd, uint64_t data) {
// RELEASE_MEM as built by sceAgcCbReleaseMem: [2] data select << 29, [5..6] data.
Agc::Command::ValidatePacket(cmd, 0x49u, 8, __func__);
if ((cmd[2] >> 29u) == 1u) Agc::Command::CheckBits(data, 0xffffffffu, __func__);
cmd[5] = static_cast<std::uint32_t>(data);

View File

@ -10,8 +10,6 @@
namespace {
// The valid bit (31) releases a queue stalled on the REWIND; the release store publishes the commands
// the title wrote behind it before setting the bit.
int SetRewindState(std::uint32_t* cmd, std::uint8_t state, const char* function) {
Agc::Command::ValidatePacket(cmd, 0x59u, 2, function);
Agc::Command::CheckBits(state, 1, function);

View File

@ -40,8 +40,6 @@ class IVideoOutput {
public:
virtual ~IVideoOutput() = default;
virtual std::shared_ptr<IFlipRequest> Reserve(const FlipInfo& info) = 0;
// Blocks a submission carrying a flip until the output's flip queue has room, as the system
// holds back a submitter that runs ahead of the display. Called without the driver's lock.
virtual void WaitForFlipRoom() {}
virtual std::shared_ptr<IRenderingWait> CaptureRenderingWait(std::uint32_t index) {
throw std::runtime_error("VideoOut: rendering waits are unsupported by this output");

View File

@ -203,8 +203,6 @@ struct Submission {
// submission trusts only labels the recorder noted with a newer stamp (see Recorder::NoteLabel).
std::uint64_t received = 0;
std::chrono::steady_clock::time_point enqueuedAt{};
// The guest commands behind a REWIND (commands stop at it): the title writes them after submitting
// and marks the REWIND valid, so they are read only then (Driver::executeRewindTail).
const std::uint32_t* rewindTail = nullptr;
std::size_t rewindWords = 0;
};
@ -528,17 +526,12 @@ private:
public:
// Copies a submission's commands, stopping after the first REWIND: what follows it is read when the
// REWIND becomes valid. INDIRECT_BUFFER packets are followed as the CP follows them: a chained one
// (bit 20 of its size dword) replaces the rest of its buffer with the target, a call inserts the
// target and continues after the packet.
static void copyCommands(Submission& submission, const std::uint32_t* guest, std::size_t words) {
submission.commands.clear();
std::size_t budget = std::size_t{1} << 26u;
copySegment(submission, guest, words, budget);
}
// Appends one command segment; true when a REWIND ended the copy.
static bool copySegment(Submission& submission, const std::uint32_t* guest, std::size_t words, std::size_t& budget) {
require(words <= budget, "command buffer jumps exceed the copy limit (a jump loop?)");
budget -= words;
@ -547,7 +540,6 @@ public:
if (Pm4::FillerPacket(header)) { submission.commands.push_back(header); ++cursor; continue; }
const auto count = (header & 0xc0000000u) == 0xc0000000u ? Pm4::PacketWords(header) : words - cursor;
if ((header & 0xc0000000u) != 0xc0000000u || count > words - cursor) {
// Malformed: copied as is for validate to report with its context.
submission.commands.insert(submission.commands.end(), guest + cursor, guest + words);
return false;
}
@ -574,8 +566,6 @@ public:
return false;
}
// Holds the submitting thread while an output named by a flip in the submission has a full flip
// queue (mutex not held: the presenter needs the driver to finish flips).
void waitForFlipRoom(const Submission& submission) {
for (std::size_t cursor = 0; cursor < submission.commands.size(); cursor += Pm4::PacketWords(submission.commands[cursor])) {
if (submission.commands[cursor] != FlipPacketHeader) continue;
@ -590,7 +580,6 @@ public:
}
}
// Takes the flip reservations and rendering waits the submission's packets name (mutex held).
void reserveOutputs(Submission& submission) {
for (std::size_t cursor = 0; cursor < submission.commands.size();) {
const auto* words = submission.commands.data() + cursor;
@ -613,8 +602,6 @@ public:
}
}
// REWIND stalls the queue until the CPU marks it valid (sceAgcRewindPatchSetRewindState); the
// commands behind it are copied from guest memory only then and run as a continuation.
void executeRewindTail(const Submission& stalled) {
std::atomic_ref<std::uint32_t> control(*const_cast<std::uint32_t*>(stalled.rewindTail - 1));
while ((control.load(std::memory_order_acquire) & 0x80000000u) == 0) {
@ -3250,8 +3237,6 @@ private:
pendingDispatchPhases().outcome = DispatchOutcome::CopyHle;
return;
}
// USE_THREAD_DIMENSIONS that is no whole number of groups: the host launches whole groups,
// the partial-group variant retires the threads past the size.
if (indirectArguments == 0 && (packet[4] & 0x20u) != 0) {
const std::array<std::uint32_t, 3> threads{packet[1], packet[2], packet[3]};
for (std::uint32_t axis = 0; axis < 3; ++axis) {
@ -3322,7 +3307,6 @@ private:
};
mix(address);
mix(packet[4] & 0x8000u);
// The partial-group size is shader data of the compiled result.
for (const auto threads : compute.partialThreads) mix(threads);
for (const auto word : userData) mix(word);
// Only the shader registers the request reads (thread counts and RSRC1/2; the program
@ -3997,9 +3981,6 @@ private:
if (!drawParameters.indirect && !drawParameters.indexed && (drawParameters.indexCount == 0 || drawParameters.instanceCount == 0)) return DrawVerdict::Nothing;
static const bool traceIndirect = std::getenv("APS5_TRACE_INDIRECT_DRAWS") != nullptr;
if (traceIndirect && drawParameters.indirect) std::fprintf(stderr, "[draw] indirect packet %s args 0x%llx count %u reached\n", Pm4::Name(packet[0]).c_str(), static_cast<unsigned long long>(drawParameters.indirect->arguments), drawParameters.indirect->count);
// A draw without color writes or a pixel shader draws nothing, unless it tests depth or
// stencil against a bound surface: such depth-only draws need a pipeline without a
// fragment stage, which is not implemented.
{
const auto targetMask = queue.context.find(0x8e);
const auto shaderMask = queue.context.find(0x8f);
@ -5156,13 +5137,6 @@ private:
return true;
}
// PIXEL_PIPE_STAT_DUMP, drained like a label: every draw before it completed, so the recorder's
// sample count is the DB counter at this point. The PS5 layout interleaves a begin and an end
// counter per DB (16 of them, 16 bytes apart; a query's begin dump writes the first of each
// pair, its end dump the second): the count goes to the first DB and the others stay at zero,
// all with bit 63 marking the result ready.
// ponytail: a GPU drain per dump; accumulate on the GPU behind the batch if titles dump many
// per frame.
void dumpSampleCounters(std::uint64_t address) {
std::uint64_t samples = 0;
{

View File

@ -1365,8 +1365,6 @@ void WriteChanged(std::uint64_t address, std::span<const std::byte> current, std
std::size_t lastChanged = 0;
for (std::size_t at = 0; at < size; at += block) {
if (!differs(at)) continue;
// Only the bytes the GPU changed: the rest of the block holds the snapshot, and storing it
// would roll back what the CPU wrote there since (a title lost an object's vtable pointer).
const auto blockEnd = std::min(at + block, size);
for (std::size_t run = at; run < blockEnd;) {
if (current[run] == original[run]) { ++run; continue; }

View File

@ -313,7 +313,6 @@ void Validate(std::span<const std::uint32_t> packet, std::uint32_t queue) {
require(eventIndex == 0 || eventIndex == 7, "invalid cache-flush event index");
break;
case 0x39:
// PIXEL_PIPE_STAT_DUMP: the occlusion counters (see Driver::dumpSampleCounters).
graphics();
size(4);
require(eventIndex == 1, "invalid occlusion counter dump event index");
@ -354,7 +353,6 @@ void Validate(std::span<const std::uint32_t> packet, std::uint32_t queue) {
break;
}
case 0x59:
// REWIND: bit 31 is the valid flag the CPU sets; the driver waits for it (see Driver).
size(2);
require((packet[1] & 0x7fffffffu) == 0, "unsupported REWIND payload bits");
break;

View File

@ -218,7 +218,6 @@ struct VulkanDevice::State {
std::map<std::uint64_t, std::shared_ptr<ComputePipelineObjects>> computePipelines;
std::unique_ptr<Graphics::GpuColorTransfer> colorTransfer;
std::shared_ptr<Graphics::BufferPool> bufferPool;
// Bound for null V#s (Context::emptyBuffer).
std::unique_ptr<Graphics::Buffer> emptyBuffer;
std::unique_ptr<Graphics::TextureCache> textureCache;
std::unique_ptr<Graphics::PipelineCache> pipelineCache;
@ -493,7 +492,6 @@ struct VulkanDevice::State {
// Cached graphics pipelines (with their framebuffers, modules, render passes and layouts)
// belong to this device and must be destroyed while it lives.
Graphics::ClearCachedPipelines(device);
// Their framebuffers were the last users of the depth surfaces' views.
Graphics::ClearDepthSurfaces(device);
{
std::lock_guard pipelines(computePipelinesMutex);
@ -780,7 +778,6 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window) : state(std::make_u
// PA_CL_CLIP_CNTL near/far clip disable maps to depth clamping.
enabled.depthClamp = available.depthClamp;
state->depthClamp = enabled.depthClamp == VK_TRUE;
// PIXEL_PIPE_STAT_DUMP reports sample counts (see Recorder::CountSamples).
enabled.occlusionQueryPrecise = available.occlusionQueryPrecise;
state->occlusionQueryPrecise = enabled.occlusionQueryPrecise == VK_TRUE;
// Recompiled storage-image access declares no format (the guest descriptor decides it).
@ -789,7 +786,6 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window) : state(std::make_u
// Gathers with non-constant offsets (ImageGatherExtended).
enabled.shaderImageGatherExtended = available.shaderImageGatherExtended;
if (enabled.shaderImageGatherExtended) state->capabilities.push_back(spv::CapabilityImageGatherExtended);
// Sampling with an LOD clamp (image_sample_*_cl) as the MinLod image operand.
enabled.shaderResourceMinLod = available.shaderResourceMinLod;
if (enabled.shaderResourceMinLod) state->capabilities.push_back(spv::CapabilityMinLod);
if (enabled.shaderStorageImageWriteWithoutFormat) state->capabilities.push_back(spv::CapabilityStorageImageWriteWithoutFormat);
@ -1702,8 +1698,6 @@ bool NoResidentPresent() {
// Debug aid: APS5_DUMP_FRAMES=<n> saves the first n presented display buffers as frame_<index>.bmp,
// read back by the blit's own submission and written after its fence (RetireSlot), or with
// APS5_NO_GPU_DUMP=1 decoded from the tiled guest buffer on the CPU, as before.
// APS5_DUMP_FRAMES_EVERY=<k> dumps every k-th presented frame (up to APS5_DUMP_FRAMES of them)
// instead of the first ones, to reach screens far into a run.
struct FrameDumps {
int limit;
bool cpu;
@ -2309,11 +2303,6 @@ bool TraceDispatchIo() {
return traceIo;
}
// Debug aid: APS5_WATCH_MEMORY=<hex address>:<hex bytes>[,<hex address>:<hex bytes>...] (with
// APS5_SYNC_DISPATCH=1) keeps the last 64 contents of the ranges, each with the dispatch that left it,
// and writes them to watch_<n>_<program>.bin (the ranges back to back, in order) when the loop guard
// first trips: the dispatch that corrupted a structure a looping shader walks is the one whose
// snapshot first shows the damage.
void WatchMemory(std::uint64_t programAddress) {
struct Range {
std::uint64_t address = 0;
@ -2341,8 +2330,6 @@ void WatchMemory(std::uint64_t programAddress) {
static bool written = false;
std::lock_guard lock(mutex);
if (written) return;
// The heap base differs between runs above bit 36: the first of those bases each range is
// mapped at, once all are.
static std::vector<std::uint64_t> addresses;
if (addresses.empty()) {
std::vector<std::uint64_t> found;

View File

@ -5,7 +5,6 @@
namespace AgcDriver::Graphics {
// APS5_LOOP_GUARD: whether a dispatch has reported a loop that ran past the guard.
bool LoopGuardTripped();
class BdaResources {
@ -41,8 +40,6 @@ private:
// 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).
// The pages stores through the table noted in the fault buffer (BdaAbi::WrittenPageSlots) are
// marked GPU-written, and the set emptied; CheckFault does it first.
void markWrittenPages() const;
std::shared_ptr<Buffer> table;
std::unique_ptr<Buffer> fault;

View File

@ -74,7 +74,6 @@ inline std::uint64_t& DeviceProcLookups() {
return count;
}
// The size of Context::emptyBuffer.
inline constexpr std::size_t EmptyBufferBytes = 16;
struct Context {
@ -119,10 +118,7 @@ struct Context {
bool drawIndirectFirstInstance = false;
bool multiDrawIndirect = false;
bool drawIndirectCount = false;
// Occlusion queries count exact samples (occlusionQueryPrecise), not just any-passed.
bool occlusionQueryPrecise = false;
// A small storage buffer bound for a null V# (no records): the recompiler specializes such a
// buffer as empty (BufferResource::empty), so shaders never access it.
VkBuffer emptyBuffer = VK_NULL_HANDLE;
// The device's list of recorded dispatches whose copied written buffers await a CPU write-back
// (VulkanDevice's State::copiedWriters; the draw counterpart is DrawCopiedWriters): an indirect

View File

@ -11,20 +11,9 @@ namespace AgcDriver::Graphics {
class Texture;
// The attachment view of the device's image for a depth/stencil target, made on first use. The
// image lives on the GPU only, always in the general layout: its guest surface (tiled, HTILE
// compressed) is never read or written, so a new image starts at the target's clear values, and
// shaders sample the image itself (DepthSurfaceTexture). Images stay until the device goes
// (ClearDepthSurfaces), so their views are stable for cached framebuffers.
// ponytail: never evicted, one image per distinct surface; evict (and keep recorded ones alive) if
// titles churn through many.
VkImageView DepthSurfaceView(const Context& context, const DepthTarget& target);
void ClearDepthSurfaces(VkDevice device);
// Whether the depth or stencil plane of a surface drawn with starts at `address`.
bool DepthSurfaceAt(std::uint64_t address);
// The texture sampling the depth or stencil plane that starts at the descriptor's base address,
// or null when no surface drawn with on this device has a plane there. The plane is read in place,
// so the descriptor must describe it (its format, extent, one level and slice); anything else throws.
std::shared_ptr<Texture> DepthSurfaceTexture(const Context& context, std::span<const std::uint32_t> words, const GuestTextureResource& resource, VkComponentMapping components);
}

View File

@ -124,8 +124,6 @@ struct AddressSpaceStats {
};
AddressSpaceStats AddressSpaceCounters();
// The image and heap mirrors (see GuestBufferMemory.cpp): heap mirrors held and their bytes, then
// cumulative: mirrors made, 64 KiB blocks copied into them after their fill, heap mirrors read again.
struct MirrorStats {
std::uint64_t heapMirrors = 0;
std::uint64_t heapBytes = 0;
@ -135,16 +133,12 @@ struct MirrorStats {
};
MirrorStats MirrorCounters();
// A registered range an address-based build copies instead of serving in place: its committed bytes
// and why no import or mirror serves it.
struct AddressCopy {
std::uint64_t begin;
std::uint64_t end;
std::uint64_t committed;
const char* reason;
};
// The FATAL message for copies whose committed bytes exceed `limit` (APS5_ADDRESS_COPY_MAX_MIB),
// naming the largest ones; empty within the limit.
std::string AddressCopyOverflow(std::vector<AddressCopy> copies, std::uint64_t limit);
class GuestBufferMemory {
@ -187,9 +181,6 @@ public:
void Upload(bool addressable);
void UploadPrepare(bool addressable);
void UploadFinish(bool addressable);
// A view off minStorageBufferOffsetAlignment in its GPU owner (a view inside a merged or
// imported range) is bound from the aligned offset below it; `adjustment` is the difference,
// which the shader adds to every access (RecompileResult::memoryOffsetDword).
VkDescriptorBufferInfo Descriptor(std::uint64_t address, std::size_t bytes, std::uint32_t& adjustment) const;
std::vector<ShaderRecompiler::BdaAbi::Range> AddressRanges() const;
// The BDA table of the cached address space when it serves this upload alone (an address-based
@ -325,8 +316,6 @@ private:
// 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);
// Keeps the bytes of every merged written range a writable heap mirror serves, as uploaded: the
// mirror keeps no shadow, and write-back stores only what the GPU changed from them.
void takeHeapReferences();
Context context;
bool stagingAllowed = false;
@ -342,7 +331,6 @@ private:
// 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;
// By the begin of the merged written range (see takeHeapReferences).
std::vector<std::pair<std::uint64_t, std::vector<std::byte>>> heapReferences;
// UploadPrepare ran (regions are frozen); `uploaded` once UploadFinish ran.
bool prepared = false;

View File

@ -40,18 +40,12 @@ public:
Pipeline(const Pipeline&) = delete;
Pipeline& operator=(const Pipeline&) = delete;
VkPipelineLayout Layout() const;
// The framebuffer of these attachment views: the color views, then the depth surface's when the
// pipeline has one. A resident target's view (`owners[i]` set) is stable while its
// StorageTexture lives, and a depth surface's while its device does, so framebuffers made of
// resident views are kept in the pipeline and reused when every owner is still the same
// object; any other set is built per call.
std::shared_ptr<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;
// The same inside a render pass another pipeline of the same attachments began (compatible by
// construction: the attachment formats alone decide).
void Continue(VkCommandBuffer commands, const VkViewport& viewport, const VkRect2D& scissor) const;
// An assembled block (AssemblePushConstants, then ShaderResources::PatchPushConstants).
void PushConstants(VkCommandBuffer commands, VkShaderStageFlags stages, std::span<const std::byte, PipelinePushConstantBytes> bytes) const;
// Forgets the Vulkan objects without destroying them: for entries of a device that is already gone.
void Abandon() noexcept;

View File

@ -165,9 +165,6 @@ public:
void FinishUpTo(std::uint64_t serial);
// Submits and waits for every batch, running completions in order.
void Sync();
// Sample counting for PIXEL_PIPE_STAT_DUMP: once on (the first dump), every batch counts the
// samples its draws pass in an occlusion query begun outside any render pass, and adds them to
// SamplesPassed when it completes. Under the mutex; an open batch starts counting at once.
void CountSamples();
static std::uint64_t SamplesPassed();
// Waits only for the batches up to the newest one that writes the range (submitting the open
@ -491,7 +488,6 @@ private:
std::uint32_t queue = 0xffffffffu;
std::chrono::steady_clock::time_point submittedAt{};
VkQueryPool queries = VK_NULL_HANDLE;
// The occlusion query counting this batch's samples (see CountSamples).
VkQueryPool samples = VK_NULL_HANDLE;
std::vector<std::uint64_t> timedKeys;
std::vector<std::uint64_t> timedBytes;

View File

@ -185,8 +185,6 @@ public:
// compiled shader's descriptors. A recipe hit refreshes the template iff the two differ.
std::uint64_t DataWordsHash() const { return dataWordsHash; }
static std::uint64_t DataWordsHash(const CompiledShader& shader);
// Stores the byte offsets of the guest buffers bound below their views (GuestBufferMemory::
// Descriptor) into a push constant block assembled from the shaders this object was built for.
void PatchPushConstants(std::span<std::byte, PipelinePushConstantBytes> bytes) const {
for (const auto& [position, adjustment] : pushPatches) bytes[position] = static_cast<std::byte>(adjustment);
}
@ -242,9 +240,6 @@ private:
bool written = true;
// Data buffers: the words the buffer holds once the recorded work ran (see RefreshData).
std::vector<std::uint32_t> dataWords;
// Guest buffers: how far below the view the descriptor starts (GuestBufferMemory::
// Descriptor), and where in the push constant block the shader reads that (-1: the
// shader data is a buffer).
std::uint32_t adjustment = 0;
std::int32_t pushByte = -1;
};
@ -410,7 +405,6 @@ private:
// FNV-1a offset basis: the hash of no data buffers (DataWordsHash).
std::uint64_t dataWordsHash = 14695981039346656037ull;
void rehashDataWords();
// The nonzero guest buffer adjustments by push constant byte (PatchPushConstants).
std::vector<std::pair<std::uint32_t, std::uint32_t>> pushPatches;
BuildTiming timing;
// Build state carried from stage A to stage B: the bindings in plan order, the image bindings

View File

@ -40,10 +40,6 @@ struct ColorTarget {
// DCC metadata of a compressed target (CB_COLOR_INFO DCC_ENABLE), or 0 (see DccMetadata.hpp).
std::uint64_t dccAddress = 0;
bool dccAlphaOnMsb = false;
// A view of one mip of a mipmapped surface (CB_COLOR_VIEW MIP_LEVEL, CB_COLOR_ATTRIB2 MAX_MIP):
// the surface's base, mip 0 extent and mip count, the rendered mip, and whether it lies in the
// packed mip tail (which shares one block with the smaller mips, so it is only rendered through
// the resident image of the whole chain). `address`, `extent` and `bytes` are the mip's.
std::uint64_t surfaceAddress = 0;
VkExtent2D surfaceExtent{};
std::uint32_t mipCount = 1;
@ -51,22 +47,17 @@ struct ColorTarget {
bool mipTail = false;
};
// The depth/stencil surface a draw with depth or stencil tests renders with (DB_Z_INFO,
// DB_STENCIL_INFO and their bases). Its image lives on the GPU only (DepthSurface.hpp).
struct DepthTarget {
std::uint64_t address;
// The stencil plane, 0 without one.
std::uint64_t stencilAddress;
VkExtent2D extent;
VkFormat format;
// DB_DEPTH_CLEAR / DB_STENCIL_CLEAR: what a new image starts as.
float clearDepth;
std::uint8_t clearStencil;
};
struct State {
ShaderStages stages;
// Set when the draw tests depth or stencil against a bound surface; the tests below are off otherwise.
std::optional<DepthTarget> depth;
bool depthTest = false;
bool depthWrite = false;
@ -94,7 +85,6 @@ struct State {
ShaderStages DecodeShaderStages(const QueueState& queue);
State DecodeState(const QueueState& queue);
// The component mapping of each MRT slot's export (identity past the written slots).
std::array<std::uint8_t, 8> ExportMappings(const State& state);
// The message DecodeState (or the pixel stage decode after it) would throw for the register rules
// this precheck covers, evaluated without exceptions before the draw is decoded; empty when they
@ -132,11 +122,6 @@ struct DrawKeyRange {
std::uint32_t count;
};
inline constexpr std::array<DrawKeyRange, 44> DrawKeyRegisters{{
// DB_RENDER_CONTROL, DB_DEPTH_VIEW, DB_DEPTH_SIZE_XY, DB_STENCIL_CLEAR/DB_DEPTH_CLEAR with
// PA_SC_SCREEN_SCISSOR, DB_Z_INFO .. DB_STENCIL_WRITE_BASE and their high words; the window
// offset/scissor and clip rect, the edge rule, the hardware screen offset, CB_TARGET_MASK/
// CB_SHADER_MASK, the generic and viewport 0 scissors, the viewport 0 depth clamp, the blend
// constants, DB_STENCIL_CONTROL/DB_STENCILREFMASK(_BF), the viewport 0 transform.
{RegisterBank::Context, 0x000, 1}, {RegisterBank::Context, 0x002, 1}, {RegisterBank::Context, 0x007, 1}, {RegisterBank::Context, 0x00a, 4}, {RegisterBank::Context, 0x010, 6}, {RegisterBank::Context, 0x01a, 4},
{RegisterBank::Context, 0x080, 4}, {RegisterBank::Context, 0x08c, 4}, {RegisterBank::Context, 0x090, 2}, {RegisterBank::Context, 0x094, 2}, {RegisterBank::Context, 0x0b4, 2}, {RegisterBank::Context, 0x105, 4}, {RegisterBank::Context, 0x10b, 3}, {RegisterBank::Context, 0x10f, 6},
// SPI_PS_INPUT_CNTL_0..31, SPI_PS_INPUT_ENA/ADDR, SPI_PS_IN_CONTROL, SPI_SHADER_POS/Z/COL_FORMAT,

View File

@ -57,8 +57,6 @@ public:
// 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);
// A view of one plane (aspect) of a depth surface's image, which stays in the general layout;
// the surface (DepthSurface.hpp) owns the image and its views.
Texture(const Context& context, VkImage depthImage, VkFormat depthFormat, VkImageAspectFlags aspect, VkComponentMapping components);
static bool CanCopyFrom(const StorageTexture& source, const GuestTextureResource& descriptor);
~Texture();

View File

@ -255,9 +255,6 @@ void BdaResources::CheckFault() const {
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);
if (report.reason == ShaderRecompiler::BdaAbi::FaultReason::Permission && table != nullptr) {
// Only a writable range imported in place is writable through the table (see
// GuestBufferMemory::addressRange): a store into a range served by a mirror or a copy would
// not reach guest memory, so it faults. Name the range and how it is served.
const auto bytes = table->Bytes();
ShaderRecompiler::BdaAbi::Header header{};
std::memcpy(&header, bytes.data(), sizeof(header));
@ -276,10 +273,6 @@ void BdaResources::CheckFault() const {
namespace AgcDriver::Graphics {
// ponytail: the pages are marked once the work completed, so a CPU read of them before that (a
// capture or texture upload of later work in the same batch) is not made to wait for the stores,
// as descriptor-bound writes are (Recorder::NotePendingWrites); note the V#s' ranges at record
// time if that shows up.
void BdaResources::markWrittenPages() const {
namespace Abi = ShaderRecompiler::BdaAbi;
auto* words = reinterpret_cast<std::uint32_t*>(fault->Bytes().data());

View File

@ -33,7 +33,6 @@ ColorTargetLayout::ColorTargetLayout(std::uint32_t width, std::uint32_t height,
std::uint32_t paddedHeight = height;
switch (mode) {
case ColorTileMode::Linear: {
// Rows are padded to 256 bytes, addrlib's linear pitch alignment (linear textures use the same).
const auto pitchAlignment = 256u / bytesPerElement;
pitch = (width + pitchAlignment - 1u) / pitchAlignment * pitchAlignment;
break;

View File

@ -52,8 +52,6 @@ public:
viewInfo.format = target.format;
viewInfo.subresourceRange = {aspects, 0, 1, 0, 1};
Check(context.Function<PFN_vkCreateImageView>("vkCreateImageView")(context.device, &viewInfo, nullptr, &view), "vkCreateImageView depth");
// Cleared in the general layout it keeps from then on, recorded ahead of the draw (a draw's
// pass begins behind a barrier from every earlier transfer write).
auto* recorder = Recorder::Active();
std::unique_ptr<CommandBatch> batch;
if (recorder == nullptr) batch = std::make_unique<CommandBatch>(context);
@ -82,7 +80,6 @@ public:
DepthSurface(const DepthSurface&) = delete;
DepthSurface& operator=(const DepthSurface&) = delete;
// The texture sampling the plane at the descriptor's base address, one per descriptor.
std::shared_ptr<Texture> Sampled(std::span<const std::uint32_t> words, const GuestTextureResource& resource, VkComponentMapping components) {
std::array<std::uint32_t, 12> key{};
std::copy_n(words.begin(), std::min<std::size_t>(words.size(), 8), key.begin());
@ -95,8 +92,6 @@ public:
const bool d16 = target.format == VK_FORMAT_D16_UNORM || target.format == VK_FORMAT_D16_UNORM_S8_UINT;
const auto expected = stencil ? VK_FORMAT_R8_UINT : d16 ? VK_FORMAT_R16_UNORM : VK_FORMAT_R32_SFLOAT;
const auto format = ResolveTextureFormat(resource.format);
// A 32-bit integer read of a D32 plane takes the float view: the shader keeps the bits
// (ShaderRecompiler::IsDepthBitsTexture).
const bool depthBits = !stencil && !d16 && words.size() >= 4 && ShaderRecompiler::IsDepthBitsTexture(words[1], words[3]);
if ((format != expected && !depthBits) || resource.dimension != TextureDimension::k2D || resource.width != target.extent.width || resource.height != target.extent.height || resource.baseLevel != 0 || resource.lastLevel != 0 || resource.baseArray != 0) {
char text[448];
@ -139,7 +134,6 @@ std::mutex& surfacesMutex() {
return mutex;
}
// Never destroyed with the statics: the images belong to a device that may be gone by then.
std::vector<std::unique_ptr<DepthSurface>>& surfaces() {
static auto* list = new std::vector<std::unique_ptr<DepthSurface>>();
return *list;
@ -164,7 +158,6 @@ void ClearDepthSurfaces(VkDevice device) {
std::shared_ptr<Texture> DepthSurfaceTexture(const Context& context, std::span<const std::uint32_t> words, const GuestTextureResource& resource, VkComponentMapping components) {
std::lock_guard lock(surfacesMutex());
const auto& list = surfaces();
// The newest surface made at the address.
const auto found = std::find_if(list.rbegin(), list.rend(), [&](const auto& surface) {
return surface->context.device == context.device && (surface->target.address == resource.baseAddress || (surface->target.stencilAddress != 0 && surface->target.stencilAddress == resource.baseAddress));
});

View File

@ -42,7 +42,6 @@ std::uint32_t GuestFormatFor(VkFormat format, std::uint32_t elementBytes) {
// The color buffer as a single-mip 2D surface descriptor (tile mode SW_64KB_R_X).
GuestTextureResource SurfaceForTarget(const ColorTarget& color) {
Require(color.tileMode == ColorTileMode::RenderTarget, "only 64 KiB tiled color targets are resident");
// A mipmapped target is its whole chain; the attachment view picks the mip.
const bool chain = color.mipCount > 1;
GuestTextureResource surface{};
surface.baseAddress = chain ? color.surfaceAddress : color.address;
@ -503,7 +502,6 @@ void CheckBufferAliases(std::span<const CompiledShader> shaders, const ColorTarg
const ShaderRecompiler::ShaderBufferResource descriptor{{words[offset], words[offset + 1], words[offset + 2], words[offset + 3]}};
const auto address = descriptor.Base48();
const auto size = descriptor.GetSize();
// A null V# binds nothing (ShaderResources::addGuestBuffer).
if (size == 0 || address == 0) continue;
Require(!overlap(address, size, target.address, target.bytes), "shader buffer aliases the render target");
Require(!overlap(address, size, indexAddress, indexBytes), "writable shader buffer aliases the index buffer");
@ -1140,8 +1138,6 @@ void recordDraw(const Context& context, const State& state, const Pm4::DrawParam
if (continued) {
record.pipeline->Continue(commands, state.viewport, state.scissor);
} else {
// Depth surfaces stay in the general layout too; an earlier pass's depth writes (the recorder's
// trailing barrier after a pass does not name them) are made visible here.
const VkMemoryBarrier before{VK_STRUCTURE_TYPE_MEMORY_BARRIER, nullptr, VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_TRANSFER_WRITE_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT | VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT, VK_ACCESS_COLOR_ATTACHMENT_READ_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT | VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_READ_BIT | VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT | VK_ACCESS_INDEX_READ_BIT | VK_ACCESS_VERTEX_ATTRIBUTE_READ_BIT | VK_ACCESS_UNIFORM_READ_BIT | VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT};
context.Resolved(&DeviceFunctions::cmdPipelineBarrier, "vkCmdPipelineBarrier")(commands, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT | VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT | VK_PIPELINE_STAGE_LATE_FRAGMENT_TESTS_BIT | VK_PIPELINE_STAGE_VERTEX_INPUT_BIT | inputs.shaderStages, 0, 1, &before, 0, nullptr, 0, nullptr);
countBarrier(1);
@ -1331,7 +1327,6 @@ void Draw(const Context& context, const State& state, const Pm4::DrawParameters&
binding.target = std::make_unique<RenderTarget>(context, color, state.blends[index].blendEnable != 0);
targetViews.push_back(binding.target->View());
}
// The depth surface is the last attachment (Pipeline::AcquireFramebuffer).
if (state.depth) targetViews.push_back(DepthSurfaceView(context, *state.depth));
timer.phase(PhasePrepare);
const bool recordDraws = RecordDraws();
@ -1477,7 +1472,6 @@ void Draw(const Context& context, const State& state, const Pm4::DrawParameters&
// only a template the resource cache serves under this content key (reusable: no lease,
// no copied writes, every direct region import- or mirror-served) of a direct draw, so a
// hit's proof is the template's ProveCurrent and nothing needs completion work.
// ponytail: none for depth draws (the recipe replays color targets only); add the depth view when they need the hit path.
if (recipeOut != nullptr && DrawRecipes() && cacheable && !outcome.waited && args == nullptr && resources->Reusable() && !state.depth) {
auto recipe = std::make_shared<DrawRecipe>();
recipe->device = context.device;

View File

@ -36,19 +36,11 @@ namespace AgcDriver::Graphics {
// mprotect, which replaces the registered Range object, and a mirror whose Range is gone is rebuilt.
// Writable ranges keep a shadow of the guest bytes and copy the 64 KiB blocks that differ on each use.
// A mirror never holds the Range shared_ptr: a pinned range makes the guest's mprotect spin and fail.
// Heap mirrors serve the readable registered heap ranges the host imports did not take (past
// APS5_HOST_IMPORT_MIB, or refused by the driver), which each address-based build copied instead.
// They keep no shadow: the arena's write tracking says which 64 KiB blocks changed since the mirror
// took them, and those blocks are read again. A writable one is written back per build from the
// bytes its written ranges held at upload (takeHeapReferences); stores through the BDA table fault
// there, as in a copy.
struct ImageMirror {
std::uint64_t base = 0;
std::uint64_t bytes = 0;
bool writable = false;
bool heap = false;
// Heap mirrors only: per tracker block of the range (block 0 holds `base`), the generation
// collected before the block was last read into the buffer.
std::vector<std::uint64_t> generations;
std::weak_ptr<const GuestAllocations::Range> range;
std::shared_ptr<Buffer> buffer;
@ -316,12 +308,9 @@ struct ImageMirrors {
VkDevice device = VK_NULL_HANDLE;
std::map<std::uint64_t, std::shared_ptr<ImageMirror>> entries;
std::set<std::uint64_t> failed;
// Bytes of the heap mirrors in `entries` (against APS5_HEAP_MIRROR_MIB).
std::uint64_t heapBytes = 0;
// APS5_PROFILE_DRAW counters, reported every 10 s as [buffers] image mirrors.
std::uint64_t builds = 0;
// Heap mirrors read again because their range changed, and heap ranges left to copies because
// the arena does not track their writes.
std::uint64_t heapRefills = 0;
std::uint64_t heapUnwatched = 0;
std::uint64_t subranges = 0;
@ -377,7 +366,6 @@ bool mirrorsEnabled() {
return !disabled;
}
// The heap mirrors' limit: past it the process stops rather than copying gigabytes per dispatch.
std::uint64_t heapMirrorBudget() {
static const std::uint64_t bytes = [] {
const char* value = std::getenv("APS5_HEAP_MIRROR_MIB");
@ -401,14 +389,10 @@ struct RefreshBlock {
ImageMirror* mirror;
std::uint64_t address;
std::size_t length;
// Heap mirrors: the generation the block's copy makes current, stored only once the copy ran,
// so a build that throws before its copies leaves the block stale in the tracker's eyes too.
std::uint64_t generation = 0;
};
// Compares a block's guest bytes with the mirror's shadow and copies a changed block into the device
// buffer and the shadow (a heap mirror's block is known to have changed and is copied). Blocks are
// distinct, so blocks of one refresh can be compared in parallel.
bool compareBlock(const RefreshBlock& block) {
const auto offset = static_cast<std::size_t>(block.address - block.mirror->base);
const auto* guest = reinterpret_cast<const std::byte*>(block.address);
@ -520,9 +504,6 @@ private:
// completes first, so its results reach the guest and the shadow before the compare and no batch
// writes a block being replaced; GPU reads of the mirror by earlier recorded work (an address-based
// dispatch whose lease is released at completion) see the newer bytes, as they would on hardware.
// prepareRange does it for [address, address + bytes), prepareRefresh for a writable image mirror and
// returns whether there is anything to compare. Heap mirrors are prepared a run of adjacent ranges at
// a time, before their write tracking is asked what changed (refreshHeapMirrors).
void prepareRange(std::uint64_t address, std::uint64_t bytes) {
// The compare reads the pages directly; a mirror is only reused while its Range exists, but a
// lease of a build in flight can keep a replaced Range alive past a protection change.
@ -574,11 +555,6 @@ void refreshMirror(ImageMirror& mirror, std::uint64_t address, std::uint64_t byt
compareBlocks(blocks);
}
// Appends to `blocks` the blocks of one build's heap mirrors that a CPU or driver store changed
// since the mirrors took them. The mirrors of adjacent ranges are prepared and collected as one run: a
// collect walks the run's page tables at a fixed cost of microseconds, which paid per mirror (thousands
// on a story-mode load) was most of an address-based build. The copies follow and store the run's
// collected generation (see RefreshBlock).
void refreshHeapMirrors(std::vector<ImageMirror*>& mirrors, std::vector<RefreshBlock>& blocks) {
constexpr std::uint64_t block = 65536;
std::sort(mirrors.begin(), mirrors.end(), [](const ImageMirror* left, const ImageMirror* right) { return left->base < right->base; });
@ -613,9 +589,6 @@ void refreshHeapMirrors(std::vector<ImageMirror*>& mirrors, std::vector<RefreshB
// The mirror for a leased image range: the existing one, or a new one when none exists or the
// registered Range object changed (a protection change). Null when the range cannot be mirrored. An
// existing writable mirror's blocks are appended to `blocks` for the caller's one compare of every
// mirror of the build (prepareRefresh ran); the caller refreshes an existing heap mirror with the
// build's others (refreshHeapMirrors); a new mirror is filled here. A heap mirror past APS5_HEAP_MIRROR_MIB, or whose buffer the driver
// refuses, stops the process.
std::shared_ptr<ImageMirror> acquireMirror(const Context& context, const std::shared_ptr<const GuestAllocations::Range>& range, std::vector<RefreshBlock>& blocks, bool heap) {
auto& state = Mirrors();
std::shared_ptr<ImageMirror> mirror;
@ -635,10 +608,6 @@ std::shared_ptr<ImageMirror> acquireMirror(const Context& context, const std::sh
if (!heap && prepareRefresh(*mirror, range->address, range->bytes)) appendBlocks(blocks, *mirror, range->address, range->bytes);
return mirror;
}
// The registered flags were taken from the pages; an image range that disagrees is copied by this
// build. A heap mirror needs readable pages only: a writable range may hold read-only pages (a
// direct memory page mapped at several addresses is read-only in every view), whose bytes the GPU
// reads like the others and whose write-back storeChanged refuses.
if (!GuestMemory::Accessible(reinterpret_cast<const void*>(range->address), range->bytes, range->writable && !heap)) return nullptr;
mirror = std::make_shared<ImageMirror>();
mirror->base = range->address;
@ -647,7 +616,6 @@ std::shared_ptr<ImageMirror> acquireMirror(const Context& context, const std::sh
mirror->heap = heap;
mirror->range = range;
if (heap) {
// Pending GPU results reach the range before the collect, so the fill below is current.
prepareRefresh(*mirror, range->address, range->bytes);
const auto generation = GuestMemory::CollectWrites(range->address, range->bytes);
constexpr std::uint64_t block = 65536;
@ -687,9 +655,6 @@ std::shared_ptr<ImageMirror> acquireMirror(const Context& context, const std::sh
return mirror;
}
// The image mirror containing [begin, end) whose registered range still exists, or null. A mirror
// whose Range was replaced is only rebuilt by the next address-based build; until then the range is
// copied. Heap mirrors are not given out: only an address-based build refreshes them.
std::shared_ptr<ImageMirror> findMirror(const Context& context, std::uint64_t begin, std::uint64_t end) {
auto& state = Mirrors();
std::lock_guard lock(state.mutex);
@ -1002,7 +967,6 @@ void GuestBufferMemory::AcquireRegistered() {
// APS5_NO_SORTED_SNAPSHOT_LOOKUP=1: AddSnapshot scans the regions as before instead of searching.
static const bool sortedLookup = std::getenv("APS5_NO_SORTED_SNAPSHOT_LOOKUP") == nullptr;
std::vector<RefreshBlock> blocks;
// The build's heap mirrors, refreshed together before the blocks are copied.
std::vector<ImageMirror*> heaps;
bool mirrored = false;
auto& spaces = Spaces();
@ -1060,7 +1024,6 @@ void GuestBufferMemory::AcquireRegistered() {
// The import lock is taken per range: a mirror's preparation may wait for recorded work, which
// must not happen under it.
regions.reserve(lease.size());
// The ranges left to copies, with why no import or mirror serves them (the copy limit below).
std::vector<AddressCopy> copies;
for (const auto& range : lease) {
if (!range->readable) continue;
@ -1081,8 +1044,6 @@ void GuestBufferMemory::AcquireRegistered() {
continue;
}
if (mirrorsEnabled()) {
// Main image and heaps: served by a persistent mirror (see ImageMirror); the writable
// image ones are compared with guest memory below, all at once.
region.mirror = acquireMirror(context, range, blocks, range->releasable);
lap(timing.mirrorsUs, at);
if (region.mirror != nullptr) {
@ -1099,13 +1060,10 @@ void GuestBufferMemory::AcquireRegistered() {
committed = 0;
for (const auto& [first, last] : copied.backed) committed += last - first;
}
// A heap range with readable pages is mirrored unless the arena does not track its writes.
const char* reason = !mirrorsEnabled() ? "mirrors disabled by APS5_NO_LEASE_MIRROR" : copied.sparse ? "unreadable pages" : !range->releasable ? "image mirror refused" : "outside the write-watched arena";
copies.push_back({copied.begin, copied.end, committed, reason});
}
regionsSorted = sortedLookup;
// The ranges left to copies are copied by every build of this space; past the limit the
// process stops rather than copying them per dispatch.
static const std::uint64_t copyLimit = [] {
const char* value = std::getenv("APS5_ADDRESS_COPY_MAX_MIB");
return (value ? std::strtoull(value, nullptr, 10) : 64ull) << 20u;
@ -1189,7 +1147,6 @@ void GuestBufferMemory::addCopiedRange(const CopiedRange& range) {
// cannot write them, so that equals a snapshot taken now without the extra copy. Reserved heaps
// are registered whole while the guest commits pages on demand: committed pages only.
region.hostBacked = !range.writable;
// Readable pages are copied in, a writable range's read-only ones included (see storeChanged).
auto committed = GuestMemory::DescribeCommitted(range.begin, range.end - range.begin);
if (!committed.whole) {
region.sparse = true;
@ -1269,8 +1226,6 @@ void GuestBufferMemory::addDescriptorRegion(std::uint64_t address, std::size_t b
auto committed = GuestMemory::DescribeCommitted(address, bytes);
if (!committed.whole) {
// A GPU heap bound whole while the guest commits its pages on demand, or a descriptor left
// pointing at unmapped memory: only readable pages are copied in and stored back (writable
// ones only, see storeChanged). Shaders must not touch the rest, which reads as zeros.
region.sparse = true;
region.backed = std::move(committed.ranges);
}
@ -2110,7 +2065,6 @@ VkDescriptorBufferInfo GuestBufferMemory::Descriptor(std::uint64_t address, std:
const auto base = region.direct != nullptr ? region.direct->base : region.mirror != nullptr ? region.mirror->base : region.begin;
const auto offset = address - base;
Require(context.limits.minStorageBufferOffsetAlignment != 0, "no storage buffer offset alignment");
// At most 255 (the alignment is at most 256); the shader adds it in DWORDs.
adjustment = static_cast<std::uint32_t>(offset % context.limits.minStorageBufferOffsetAlignment);
Require(adjustment % 4 == 0, "guest buffer view off the storage buffer offset alignment is not DWORD aligned");
Require(bytes + adjustment <= context.limits.maxStorageBufferRange, "guest buffer view exceeds descriptor range limit");
@ -2122,9 +2076,6 @@ ShaderRecompiler::BdaAbi::Range GuestBufferMemory::addressRange(const Region& re
Require(region.buffer != nullptr || region.direct != nullptr || region.mirror != nullptr, "incomplete guest GPU upload");
const auto address = region.direct != nullptr ? region.direct->address + (region.begin - region.direct->base) : region.mirror != nullptr ? region.mirror->buffer->DeviceAddress() + (region.begin - region.mirror->base) : region.buffer->DeviceAddress();
Require(region.end - region.begin <= std::numeric_limits<std::uint64_t>::max() - address, "GPU address range overflow");
// Stores through the table (GPU-selected V#s) land in guest memory only where the range is the
// allocation itself: a writable one served in place by its import. Mirrors and copies would
// drop them, so a store there faults.
const auto permissions = ShaderRecompiler::BdaAbi::Read | (region.direct != nullptr && region.writable ? ShaderRecompiler::BdaAbi::Write : 0u);
return {region.begin, region.end, address, permissions, 0};
}
@ -2178,10 +2129,6 @@ void GuestBufferMemory::MarkDirectWrites() const {
namespace {
// Stores what the GPU changed in [address, address + current.size()) against `original`. Only the
// writable pages are stored: a read-only page of a writable range (a direct memory page mapped at
// several addresses is read-only in every view, aliased writes are not implemented) must come back
// unchanged, else the write-back fails rather than dropping the GPU's store.
void storeChanged(std::uint64_t address, std::span<const std::byte> current, std::span<const std::byte> original) {
const auto writable = GuestMemory::DescribeCommitted(address, current.size(), true);
if (writable.whole) {
@ -2206,7 +2153,6 @@ void storeChanged(std::uint64_t address, std::span<const std::byte> current, std
if (cursor < address + current.size()) unchanged(cursor, address + current.size());
}
// The written ranges sorted, overlapping ones merged.
std::vector<std::pair<std::uint64_t, std::uint64_t>> mergeWrites(std::vector<std::pair<std::uint64_t, std::uint64_t>> writes) {
std::sort(writes.begin(), writes.end());
std::vector<std::pair<std::uint64_t, std::uint64_t>> merged;
@ -2257,8 +2203,6 @@ void GuestBufferMemory::WriteBack() {
const auto length = static_cast<std::size_t>(end - begin);
const auto current = mirror.buffer->Bytes().subspan(offset, length);
if (mirror.heap) {
// The build's reference instead of a shadow; the stores stamp the blocks, and the
// next refresh takes them back from guest memory.
const auto reference = std::find_if(heapReferences.begin(), heapReferences.end(), [&](const auto& entry) { return entry.first == begin; });
Require(reference != heapReferences.end() && reference->second.size() == length, "heap mirror write-back has no reference");
storeChanged(begin, current, reference->second);
@ -2327,8 +2271,6 @@ std::optional<std::vector<std::pair<std::uint64_t, std::uint64_t>>> GuestBufferM
}
for (const auto& region : regions) {
// A read-only mirror is as fixed as an import: its bytes and VkBuffer never change while its
// Range exists (see ImageMirrorSerial). A writable one is refreshed and written back per build,
// a heap one refreshed per build.
// A staged region is keyed by its import like one bound in place: the import is the source
// of its copy-in and the destination of its copy-back, both recorded per use.
const bool fixedMirror = region.mirror != nullptr && !region.mirror->writable && !region.mirror->heap;

View File

@ -103,7 +103,6 @@ GuestTextureResource DecodeTextureResource(std::span<const std::uint32_t> words)
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");
// DCC block sizes only shape compressed blocks, and surfaces are read as stored (below).
static_cast<void>(maxUncompBlkSize);
static_cast<void>(maxCompBlkSize);
// Surfaces are always written uncompressed here (render targets and storage images bypass DCC), so

View File

@ -105,7 +105,6 @@ Pipeline::Pipeline(const Context& context, const State& state, const VertexInput
subpass.pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS;
subpass.colorAttachmentCount = static_cast<std::uint32_t>(references.size());
subpass.pColorAttachments = references.empty() ? nullptr : references.data();
// The depth surface stays in the general layout (DepthSurface.hpp).
const VkAttachmentReference depthReference{static_cast<std::uint32_t>(colors.size()), VK_IMAGE_LAYOUT_GENERAL};
if (state.depth) {
VkAttachmentDescription depth{};

View File

@ -264,10 +264,7 @@ std::shared_ptr<StorageTexture> sampledStorageSource(const Context& context, con
// `guestBytes` is the surface size when the caller described the surface already (0: described here).
std::shared_ptr<Texture> cachedTexture(const Context& context, std::span<const std::uint32_t> words, const GuestTextureResource& resource, VkComponentMapping components, std::uint64_t guestBytes = 0) {
CaptureTrace::Log("sampled-lookup address=%llx width=%u height=%u dcc=%llx", static_cast<unsigned long long>(resource.baseAddress), resource.width, resource.height, static_cast<unsigned long long>(resource.dccAddress));
// A depth surface's contents live in its GPU image; its guest memory is stale.
if (auto depth = DepthSurfaceTexture(context, words, resource, components)) return depth;
// The recompiler reads such a T# through a float view (IsDepthBitsTexture), which only a depth
// surface provides.
if (words.size() >= 4 && ShaderRecompiler::IsDepthBitsTexture(words[1], words[3])) {
char text[160];
std::snprintf(text, sizeof(text), "AGC graphics: 32-bit integer read of the depth-layout texture 0x%llx, which is no depth surface drawn with, is not implemented", static_cast<unsigned long long>(resource.baseAddress));
@ -829,7 +826,6 @@ void ShaderResources::buildPrepare(std::span<const CompiledShader> shaders, cons
const bool written = element >= binding.bufferWritten.size() || binding.bufferWritten[element];
const bool atomic = element < binding.bufferAtomic.size() && binding.bufferAtomic[element];
const auto index = addGuestBuffer(std::span<const std::uint32_t>(binding.guestDescriptor).subspan(static_cast<std::size_t>(element) * 4, 4), target, indexAddress, indexBytes, written, atomic);
// The element's byte offset in the shader data (RecompileResult::memoryOffsetDword).
const auto& push = shader.program->pushConstants;
if (!push.empty()) {
const auto position = shader.program->memoryOffsetDword * 4u + element;
@ -2029,7 +2025,6 @@ std::size_t ShaderResources::addGuestBuffer(std::span<const std::uint32_t> words
const auto address = descriptor.Base48();
const auto byteSize = descriptor.GetSize();
if (byteSize == 0 || address == 0) {
// A null V#: every access is out of bounds in the shader (BufferResource::empty).
allocations.push_back({0, EmptyBufferBytes, false, nullptr, ShaderRecompiler::DescriptorRole::GuestBuffers, false});
return allocations.size() - 1;
}

View File

@ -79,17 +79,12 @@ bool IgnoreDepthTest() {
return ignore;
}
// Whether DB_Z_INFO or DB_STENCIL_INFO names a plane (an absent register names none).
bool depthSurfaceBound(const Registers& cx) {
const auto z = find(cx, 0x010);
const auto stencil = find(cx, 0x011);
return (z != cx.end() && (z->second & 3u) != 0) || (stencil != cx.end() && (stencil->second & 1u) != 0);
}
// One face of the stencil state (the compare function, DB_STENCIL_CONTROL's three operations and
// DB_STENCILREFMASK) as Vulkan's. AMD's compare functions are in Vulkan's order. Vulkan has one
// reference per face for the test and every replacement, so a replacement by another value
// (REPLACE_OP writes STENCILOPVAL, ONES 0xff) is taken where the bits it writes are not also compared.
VkStencilOpState stencilFace(std::uint32_t compare, std::uint32_t ops, std::uint32_t refMask, bool readOnly) {
VkStencilOpState face{};
face.compareOp = static_cast<VkCompareOp>(compare);
@ -98,8 +93,6 @@ VkStencilOpState stencilFace(std::uint32_t compare, std::uint32_t ops, std::uint
const auto test = refMask & 0xffu;
const auto opValue = refMask >> 24u;
auto reference = test;
// The reference bits already spoken for: the compared ones (none for NEVER and ALWAYS), then
// the replaced ones.
auto fixed = compare == 0 || compare == 7 ? 0u : face.compareMask;
const auto convert = [&](std::uint32_t op) {
if (face.writeMask == 0) return VK_STENCIL_OP_KEEP;
@ -123,7 +116,6 @@ VkStencilOpState stencilFace(std::uint32_t compare, std::uint32_t ops, std::uint
case 6:
case 8:
case 9:
// ADD/SUB add STENCILOPVAL; Vulkan's increments add 1.
if (opValue != 1) {
std::snprintf(detail, sizeof(detail), "AGC graphics: stencil add/subtract of 0x%02x is unsupported", opValue);
throw std::runtime_error(detail);
@ -142,15 +134,10 @@ VkStencilOpState stencilFace(std::uint32_t compare, std::uint32_t ops, std::uint
return face;
}
// The depth/stencil surface and tests of a draw that tests depth or stencil with a plane bound.
void decodeDepth(const Registers& cx, std::uint32_t depthControl, State& result) {
// DEPTH/STENCIL_COMPRESS_DISABLE (bits 5, 6) and the ordering hints from bit 13 on do not change
// what is drawn.
zero(cx, 0x000, 0x00001f9fu, "depth/stencil clear, copy, resummarize or decompress draws (DB_RENDER_CONTROL)");
const auto view = read(cx, 0x002);
// Z_READ_ONLY and STENCIL_READ_ONLY (bits 24, 25) turn the writes off.
zero(cx, 0x002, ~0x03000000u, "depth array slices or mips (DB_DEPTH_VIEW)");
// NUM_SAMPLES, PARTIALLY_RESIDENT, MAXMIP; the rest is tiling and compression the host image has none of.
zero(cx, 0x010, 0x000f100cu, "multisampled, partially resident or mipmapped depth (DB_Z_INFO)");
zero(cx, 0x011, 0x00001000u, "partially resident stencil (DB_STENCIL_INFO)");
const auto zFormat = read(cx, 0x010) & 3u;
@ -171,7 +158,6 @@ void decodeDepth(const Registers& cx, std::uint32_t depthControl, State& result)
Require(!stencil || stencilReadOnly || base(0x015, 0x01d) == depth.stencilAddress, "stencil read and written at different addresses is unsupported");
const auto size = read(cx, 0x007);
depth.extent = {(size & 0x3fffu) + 1u, ((size >> 16u) & 0x3fffu) + 1u};
// A stencil-only surface gets an unused depth plane.
depth.format = zFormat == 1 ? (stencil ? VK_FORMAT_D16_UNORM_S8_UINT : VK_FORMAT_D16_UNORM) : (stencil ? VK_FORMAT_D32_SFLOAT_S8_UINT : VK_FORMAT_D32_SFLOAT);
depth.clearDepth = readFloat(cx, 0x00b);
depth.clearStencil = static_cast<std::uint8_t>(read(cx, 0x00a) & 0xffu);
@ -183,13 +169,10 @@ void decodeDepth(const Registers& cx, std::uint32_t depthControl, State& result)
if (result.stencilTest) {
const auto ops = read(cx, 0x10b);
result.stencilFront = stencilFace((depthControl >> 8u) & 7u, ops, read(cx, 0x10c), stencilReadOnly);
// BACKFACE_ENABLE gives back faces their own function, operations and masks.
result.stencilBack = (depthControl & 0x80u) != 0 ? stencilFace((depthControl >> 20u) & 7u, ops >> 12u, read(cx, 0x10d), stencilReadOnly) : result.stencilFront;
}
}
// DB_DEPTH_CONTROL without a bound surface: depth and stencil tests whose function is ALWAYS and
// that write no depth cannot change color output, so they are accepted (stencil writes go nowhere).
bool depthPassThrough(std::uint32_t depthControl) {
const bool stencil = (depthControl & 1u) != 0;
const bool depth = (depthControl & 2u) != 0;
@ -238,7 +221,6 @@ VkBlendOp blendOp(std::uint32_t value) {
struct DecodedColorFormat {
VkFormat format;
std::uint32_t elementBytes;
// The exported component each attachment component stores, two bits each (0xe4 is identity).
std::uint8_t componentMapping = 0xe4u;
};
@ -251,8 +233,6 @@ DecodedColorFormat DecodeColorFormat(std::uint32_t format, std::uint32_t number,
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;
// A one-component format stores the exported component COMP_SWAP names: R, G, B or A (an A8
// target is COLOR_8 with SWAP_ALT_REV).
const auto single = [&](VkFormat vkFormat, std::uint32_t bytes) { return DecodedColorFormat{vkFormat, bytes, static_cast<std::uint8_t>((0xe4u & ~3u) | swap)}; };
if (swap > 1 && format != 1 && format != 2 && format != 4) return fail();
switch (format) {
@ -497,8 +477,6 @@ State DecodeState(const QueueState& queue) {
for (std::uint32_t slot = 0; slot < slotCount; ++slot) {
// Export formats only matter for the targets the draw writes.
const auto slotExport = (exportFormat >> (4u * slot)) & 0xfu;
// FP16, UNORM16 and SNORM16 exports are unpacked to floats by the shader; 32_R, 32_GR, 32_AR and
// 32_ABGR are written as is, the components a format leaves out reading as 0 (alpha 1).
if (slotExport == 0 || slotExport == 7 || slotExport == 8 || slotExport > 9) throw std::runtime_error("AGC graphics: color export format " + std::to_string(slotExport) + " is unsupported");
const auto stride = slot * 0xfu;
ColorTarget color{};
@ -509,12 +487,8 @@ State DecodeState(const QueueState& queue) {
const auto format = (info >> 2u) & 0x1fu;
const auto decoded = DecodeColorFormat(format, number, swap);
// ROUND_MODE (bit 18) only affects unorm rounding. With DCC_ENABLE (bit 28) the target is written
// uncompressed and only its fast-clear keys matter (see DccMetadata.hpp). BLEND_BYPASS (bit 16) is
// checked against enabled blending below.
if ((info & ~(0x00039f7cu | 0x00040000u | 0x10000000u)) != 0) throw std::runtime_error("AGC graphics: color compression, DCC, endian conversion, nonstandard rounding or color optimization is unsupported (CB_COLOR_INFO 0x" + [&] { char text[16]; std::snprintf(text, sizeof(text), "%08x", info); return std::string(text); }() + ")");
Require((info & 0x8000u) != 0 || number == 7 || number == 4 || number == 5, "unclamped normalized color is unsupported");
// CB_COLOR_VIEW (GFX10: SLICE_START 0-12, SLICE_MAX 13-25, MIP_LEVEL 26-29): MIP_LEVEL selects
// the rendered mip; array slices are not modeled.
const auto view = read(cx, 0x31b + stride);
Require((view & ~0x3c000000u) == 0, "color array views are unsupported");
const auto viewMip = (view >> 26u) & 0xfu;
@ -608,8 +582,6 @@ State DecodeState(const QueueState& queue) {
APS5_LOG_OUT_DEBUG("Blend %u control=0x%x", slot, blend);
Require((blend & 0x0000e000u) == 0, "reserved blend control bits");
VkPipelineColorBlendAttachmentState state{};
// CB_TARGET_MASK names exported components; attachment component c stores exported component
// mapping[c], so its write bit and, when it stores alpha, its blend come from that component.
const auto mapping = result.colors[slot].componentMapping;
const auto exportedMask = (targetMask >> (4u * slot)) & 0xfu;
for (std::uint32_t component = 0; component < 4; ++component) {
@ -665,7 +637,6 @@ std::string DrawRejection(const QueueState& queue, bool indexed) {
if (auto reason = indexed ? nonzero(queue.userConfig, 0x24b, ~0u, "primitive restart (GE_MULTI_PRIM_IB_RESET_EN)") : std::string(); !reason.empty()) return reason;
if (auto reason = nonzero(cx, 0x207, ~LayerExports, "clip distances, layer, viewport or auxiliary vertex exports"); !reason.empty()) return reason;
if (value(cx, 0x200, word)) {
// Tests against a bound surface are decoded (and their limits checked) by DecodeState.
const bool surface = (word & 3u) != 0 && depthSurfaceBound(cx);
if (!surface && !depthPassThrough(word) && !IgnoreDepthTest() && (word & DepthControlMask) != 0) return zeroMessage(0x200, word, "depth, stencil or conditional color writes");
if (auto reason = require((word & 0xc0000008u) == 0, "depth bounds or depth-conditional color writes are unsupported"); !reason.empty()) return reason;

View File

@ -291,8 +291,6 @@ ThickLayout ComputeThickLayout(TextureTileMode tileMode, std::uint32_t format, s
SurfaceGeometry DescribeSurface(const GuestTextureResource& descriptor) {
SurfaceGeometry geometry;
if (descriptor.dimension == TextureDimension::k3D && (descriptor.tileMode == TextureTileMode::kD64KBX || descriptor.tileMode == TextureTileMode::kR64KBX)) {
// addrlib keeps display and rotated swizzles thin for 3D (only Z and standard ones are thick):
// each depth slice is laid out as an array slice, its index XORed in through the equation.
Require(descriptor.mipCount == 1, "mipmapped 3D textures are not implemented");
const auto depth = descriptor.depthOrLastArray + 1u;
geometry.mips = ComputeMipLayout(descriptor.tileMode, descriptor.format, descriptor.width, descriptor.height, 1u);

View File

@ -8,8 +8,6 @@
extern "C" {
// Tessellation runs as Vulkan tessellation stages, which keep hull-shader outputs and tessellation
// factors on the host GPU: the off-chip hull memory and the tessellation-factor ring are never read.
int APS5_VABI sceAgcDriverSetHsOffchipParam(uint64_t value0, uint64_t value1, uint64_t value2) {
(void)value0;
(void)value1;

View File

@ -38,8 +38,6 @@ void RunBdaContractTests() {
auto& block = program.CreateBlock();
block.AppendInstruction(&program.CreateValue(IrOpcode::Barrier, IrType::Void));
program.BlockOrder().push_back(&block);
// A program with workgroup barriers is accepted: its faulting reads return zero instead of
// ending the invocation, so every invocation still reaches the barriers.
ValidateBdaTarget(program, target);
AgcDriver::Graphics::Require(!BdaInvocationsMayStop(program), "a BDA program with barriers must keep its invocations running");
program.Resources().stage = IrShaderStage::TessellationControl;
@ -57,6 +55,5 @@ void RunBdaContractTests() {
auto invalid = encoded;
invalid[0] = invalid[0] == 'A' ? 'B' : 'A';
reject([&] { static_cast<void>(serializer.Deserialize(invalid)); }, "request signature");
// The signature followed by serialization version 99, in base64.
reject([&] { static_cast<void>(serializer.Deserialize("NVNQQWMAAAA=")); }, "serialization version");
}

View File

@ -9,7 +9,6 @@
#include <iostream>
#include <vector>
// Resources.cpp submits and reaps through the active recorder; these tests record nothing.
namespace AgcDriver::Graphics {
Recorder* Recorder::Active() { return nullptr; }
void Recorder::Submit() { Require(false, "the device tests have no recorder"); }

View File

@ -35,12 +35,7 @@ ShaderRecompiler::DescriptorBinding binding(Role role, std::uint32_t slot) {
return {ShaderRecompiler::DescriptorKind::StorageBuffer, role, 0, slot, 1, {}, false};
}
// Heap ranges that no import serves are mirrored. A read-only one: the build binds the mirror, the
// next build with the range unchanged copies nothing into it, and the blocks a CPU write changed are
// read again. A writable one: a descriptor's store reaches guest memory at write-back, and only the
// bytes the GPU changed (a CPU store made while the GPU works stays).
void heapMirrorTests(const Context& context, const BdaTestAccess& access) {
// Heap mirrors refresh from the arena's write tracking; without it they are never made.
Require(GuestArena::GuestArenaAvailable_nid_postfix() && GuestArena::GuestArenaWriteWatched_nid_postfix(), "the heap mirror tests need the write-watched guest arena");
constexpr std::size_t bytes = 2 * 65536;
void* block = GuestArena::GuestArenaAllocate_nid_postfix(bytes, 65536);
@ -56,8 +51,6 @@ void heapMirrorTests(const Context& context, const BdaTestAccess& access) {
else GuestAllocations::GuestAllocationsRemove_nid_postfix(mutation, block);
GuestAllocations::GuestAllocationsEnd_nid_postfix(mutation);
};
// An address-based build that writes 32 bytes at `written` (nothing for 0), with `gpu` run
// between its upload and its write-back; returns the device address serving the range.
const auto build = [&](std::uint64_t written, const std::function<void(GuestBufferMemory&)>& gpu) {
GuestBufferMemory leased(context);
leased.AcquireRegistered();
@ -71,7 +64,6 @@ void heapMirrorTests(const Context& context, const BdaTestAccess& access) {
leased.WriteBack();
return device;
};
// An address-based build sweeps the mirrors whose ranges are gone (an earlier test's too).
const auto sweep = [&] {
GuestBufferMemory leased(context);
leased.AcquireRegistered();
@ -113,8 +105,6 @@ void heapMirrorTests(const Context& context, const BdaTestAccess& access) {
Require(MirrorCounters().heapMirrors == before.heapMirrors, "the writable heap mirror outlived its range");
#ifdef _WIN32
// A build that throws after queuing a mirror's changed blocks (another mirror's range became
// inaccessible) must not mark those blocks current: the next build copies them.
{
constexpr std::size_t half = 2 * 65536;
void* pair = GuestArena::GuestArenaAllocate_nid_postfix(2 * half, 65536);
@ -145,7 +135,6 @@ void heapMirrorTests(const Context& context, const BdaTestAccess& access) {
const auto device = firstDevice();
first[5] = 0x66;
DWORD previous = 0;
// A protection change outside the registry, as a heap decommit is: the page cache is told.
Require(VirtualProtect(second, 65536, PAGE_NOACCESS, &previous) != 0, "cannot protect the second heap range");
GuestAllocations::GuestAllocationsInvalidate_nid_postfix(reinterpret_cast<std::uintptr_t>(second), 65536);
bool threw = false;
@ -166,10 +155,6 @@ void heapMirrorTests(const Context& context, const BdaTestAccess& access) {
}
#endif
#ifdef _WIN32
// A writable range with a read-only page (a direct memory page mapped at several addresses is
// read-only in every view): the mirror holds the page's bytes, a store next to it is written
// back, and a GPU change inside it fails the write-back instead of being dropped. A descriptor's
// copy outside an address-based build holds the page's bytes too.
{
constexpr std::size_t size = 2 * 65536;
void* raw = GuestArena::GuestArenaAllocate_nid_postfix(size, 65536);
@ -304,7 +289,6 @@ void RunBdaResourceTests(const Context& context, const BdaTestAccess& access) {
Require(range.begin == snapshots[0].address && range.end == range.begin + source.size(), "64-bit guest address was truncated");
const auto fault = access.bytes(access.descriptor(5).buffer);
for (const auto byte : fault) Require(byte == std::byte{}, "fault buffer was not initialized");
// A store into a read-only table range names the range and why stores cannot reach it.
const ShaderRecompiler::BdaAbi::Fault denied{ShaderRecompiler::BdaAbi::FaultState::Ready, ShaderRecompiler::BdaAbi::FaultReason::Permission, snapshots[0].address + 4, 4, 0, 0x88, 0};
std::memcpy(fault.data(), &denied, sizeof(denied));
reject([&] { resources.WriteBack(); }, "read-only in the BDA table");
@ -372,7 +356,6 @@ void RunBdaResourceTests(const Context& context, const BdaTestAccess& access) {
if (dropped.enabled) Require(dropped.waiterDrops == after.waiterDrops + 1 && LeaseCounters().cacheDrops == dropped.waiterDrops, "the free did not drop the cached address space");
}
heapMirrorTests(context, access);
// The copies left to an address-based build: nothing within the limit, the largest first past it.
Require(AddressCopyOverflow({{0x1000, 0x3000, 0x2000, "uncommitted pages"}}, 0x2000).empty(), "copies within the limit were refused");
const auto copies = AddressCopyOverflow({{0x1000, 0x2000, 0x1000, "not mirrored"}, {0x10000, 0x30000, 0x18000, "uncommitted pages"}}, 0x2000);
Require(!copies.empty() && copies.find("0x10000+0x20000 (0.1 MiB committed, uncommitted pages)") < copies.find("0x1000+0x1000"), "the copy limit does not name the largest copy first");

View File

@ -8,7 +8,6 @@
struct BdaTestAccess {
std::function<std::span<std::byte>(VkBuffer)> bytes;
std::function<VkDescriptorBufferInfo(std::uint32_t)> descriptor;
// The bytes of the buffer behind a device address, from that address on.
std::function<std::span<std::byte>(VkDeviceAddress)> addressBytes;
};

View File

@ -59,7 +59,6 @@ void RunColorTargetLayoutTests() {
}
reject([&] { layout.Detile(std::span(tiled).first(4), restored); });
reject([&] { layout.Tile(std::span(linear).first(4), tiled); });
// Aligned by hand: a PE section cannot ask for 64 KiB alignment.
static std::vector<std::byte> storage(2 * 65536);
const std::span guest(reinterpret_cast<std::byte*>((reinterpret_cast<std::uintptr_t>(storage.data()) + 0xffffu) & ~std::uintptr_t{0xffffu}), 65536);
std::fill(guest.begin(), guest.end(), std::byte{0x6b});

View File

@ -1,5 +1,3 @@
// A DPP instruction's row and bank masks gate its VGPR write, not only its source read: the rows a
// mask leaves out keep their value (a scan adding the other row's total into the odd rows only).
#include "ControlFlow/GraphBuilder.hpp"
#include "ControlFlow/Structurizer.hpp"
#include "RdnaDecoder/RdnaInstructionDecoder.hpp"
@ -12,7 +10,6 @@
using namespace ShaderRecompiler;
int main() {
// v_add_nc_u32_dpp v1, v0, v1 quad_perm:[0,1,2,3] row_mask:0xa bank_mask:0xf; s_endpgm
const std::vector<std::uint32_t> code{0x4a0202fau, 0xaf00e400u, 0xbf810000u};
try {
const auto decoded = RdnaInstructionDecoder{}.Decode(code);

View File

@ -99,7 +99,6 @@ void stateTests() {
queue.context[0x91] = 0x30020;
state = AgcDriver::Graphics::DecodeState(queue);
Require(state.scissor.offset.x == 3 && state.scissor.offset.y == 1 && state.scissor.extent.width == 29 && state.scissor.extent.height == 2, "scissor intersection changed");
// DCC_ENABLE: the target is written uncompressed, its keys at CB_COLOR0_DCC_BASE.
queue.context[0x31c] |= 0x10000000;
queue.context[0x325] = 0x1234;
Require(AgcDriver::Graphics::DecodeState(queue).color.dccAddress == 0x123400, "DCC key address decode changed");
@ -110,11 +109,9 @@ void stateTests() {
queue.context[0x3b8] |= 5u << 14u;
expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "unsupported color tile mode");
queue = makeState();
// A linear width off the 256-byte pitch alignment is padded (rows of 64 texels here).
queue.context[0x3b0] = (62u << 14u) | 3u;
Require(AgcDriver::Graphics::DecodeState(queue).color.bytes == 64u * 4u * 4u, "padded linear pitch changed");
queue = makeState();
// Slot 1 written by the shader needs an export format.
queue.context[0x8e] = 0xff;
queue.context[0x8f] = 0xff;
expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "color export format 0");
@ -238,8 +235,6 @@ void DisabledColorTests() {
expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "missing register");
}
// Depth/stencil tests against a bound surface: Scaleform's mask pass (stencil ALWAYS, add 1) and
// content pass (stencil EQUAL), as a Scaleform menu draws them.
void DepthStencilTests() {
auto queue = makeState();
queue.context[0x000] = 0;
@ -270,35 +265,29 @@ void DepthStencilTests() {
const auto& front = state.stencilFront;
Require(front.compareOp == VK_COMPARE_OP_ALWAYS && front.passOp == VK_STENCIL_OP_INCREMENT_AND_CLAMP && front.failOp == VK_STENCIL_OP_KEEP && front.reference == 1 && front.writeMask == 0xff, "stencil mask pass changed");
Require(std::memcmp(&state.stencilBack, &front, sizeof(front)) == 0, "back faces without BACKFACE_ENABLE must use the front state");
// The content pass: EQUAL to 2, nothing written.
queue.context[0x10b] = 0;
queue.context[0x10c] = 0x01ffff02;
queue.context[0x200] = 0x00200211;
state = AgcDriver::Graphics::DecodeState(queue);
Require(state.stencilFront.compareOp == VK_COMPARE_OP_EQUAL && state.stencilFront.reference == 2 && state.stencilFront.passOp == VK_STENCIL_OP_KEEP, "stencil content pass changed");
// Clearing to 0 with REPLACE_TEST under ALWAYS.
queue.context[0x10b] = 0x00030030;
queue.context[0x10c] = 0x01ffff00;
queue.context[0x200] = 0x00700771;
state = AgcDriver::Graphics::DecodeState(queue);
Require(state.stencilFront.passOp == VK_STENCIL_OP_REPLACE && state.stencilFront.reference == 0 && std::memcmp(&state.stencilBack, &state.stencilFront, sizeof(state.stencilFront)) == 0, "stencil clear pass changed");
// BACKFACE_ENABLE: back faces get STENCILFUNC_BF, the _BF operations and DB_STENCILREFMASK_BF.
queue.context[0x200] = 0x007007f1;
state = AgcDriver::Graphics::DecodeState(queue);
Require(state.stencilBack.compareOp == VK_COMPARE_OP_ALWAYS && state.stencilBack.passOp == VK_STENCIL_OP_KEEP && state.stencilBack.writeMask == 0 && state.stencilBack.reference == 1, "back-face stencil state changed");
// REPLACE_OP writing a value the test compares differently has no single Vulkan reference.
queue.context[0x10b] = 0x40;
queue.context[0x10c] = 0x05ffff02;
queue.context[0x200] = 0x00200211;
expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "stencil replacement");
// Adding anything but 1 has no Vulkan operation.
queue.context[0x10b] = 0x50;
queue.context[0x200] = 0x00700711;
expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "stencil add/subtract");
queue.context[0x10b] = 0;
queue.context[0x000] = 1;
expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "DB_RENDER_CONTROL");
// CB_COLOR_VIEW's mip level is bits 26-29.
queue = makeState();
queue.context[0x31b] = 1u << 26u;
expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "mip exceeds");
@ -334,7 +323,6 @@ void DepthClipTests() {
expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "inverted viewport depth clamp");
queue.context[0xb4] = 0;
for (std::uint32_t bit = 0; bit < 32; ++bit) {
// DX_CLIP_SPACE_DEF (19) and ZCLIP_NEAR/FAR_DISABLE (26, 27, depth clamping) are decoded.
if (bit == 19 || bit == 26 || bit == 27) continue;
queue.context[0x204] = 1u << bit;
expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "PA_CL_CLIP_CNTL");
@ -666,7 +654,6 @@ void expectSingleFailure(const ShaderRecompiler::DescriptorBinding& binding, std
expectResourceFailure(vertex, fragment, reason);
}
// A binding the resources accept (building them throws nothing), leaving no Vulkan object behind.
void expectSingleAccepted(const ShaderRecompiler::DescriptorBinding& binding, std::string_view what) {
ShaderRecompiler::RecompileResult vertex;
ShaderRecompiler::RecompileResult fragment;
@ -802,8 +789,6 @@ void resourceTests() {
mutate(binding);
return binding;
};
// Image and sampler bindings are implemented: a buffer's four-DWORD V# is no T#, and the mock
// context has no sampler slots, so a sampler binding exceeds its per-stage limit.
expectSingleFailure(changed([](auto& binding) { binding.role = Role::GuestImages; binding.kind = Kind::SampledImage; }), "guest texture descriptor must contain 8 dwords");
expectSingleFailure(changed([](auto& binding) { binding.role = Role::GuestImages; binding.kind = Kind::StorageImage; }), "guest storage image descriptors must contain 8 dwords");
expectSingleFailure(changed([](auto& binding) { binding.role = Role::GuestSamplers; binding.kind = Kind::Sampler; }), "shader sampler descriptors exceed per-stage limits");
@ -825,8 +810,6 @@ void resourceTests() {
expectSingleFailure(changed([](auto& binding) { binding.guestDescriptor[3] |= 0x40000000u; }), "unsupported type");
expectSingleFailure(changed([](auto& binding) { binding.guestDescriptor[1] |= 0x3fffu << 16u; binding.guestDescriptor[2] = 0xffffffffu; }), "descriptor range limit");
expectSingleFailure(changed([](auto& binding) { binding.guestDescriptor[2] = 8192; }), "descriptor range limit");
// A V# left pointing at unmapped memory binds its committed pages only (none here); the rest reads
// as zeros (GuestBufferMemory::addDescriptorRegion), so it is not rejected.
expectSingleAccepted(changed([](auto& binding) { binding.guestDescriptor = vsharp(reinterpret_cast<const void*>(0x1000), 8); }), "an unmapped V#");
expectSingleFailure(changed([&](auto& binding) { binding.guestDescriptor = vsharp(reinterpret_cast<const void*>(state.color.address), 64); }), "aliases the render target");
expectSingleAccepted(changed([](auto& binding) { binding.count = 3; binding.guestDescriptor = join(join(vsharp(guestFirst.data(), 16), vsharp(guestSecond.data(), 32)), vsharp(reinterpret_cast<const void*>(0x1000), 8)); }), "an unmapped V# element");
@ -1051,8 +1034,6 @@ void rectListTests() {
expectFailure([&] { static_cast<void>(BuildRectListShaders(vertex, fragment, target)); }, "per-vertex interpolation");
fragment.fragmentParameters[0].perVertex = false;
vertex.parameterExports.clear();
// A fragment input the vertex shader does not export is built as a missing parameter (see
// RectListShaders.cpp), not rejected.
auto unexported = BuildRectListShaders(vertex, fragment, target);
Require(!unexported.control.spirv.empty() && !unexported.evaluation.spirv.empty(), "rect-list shaders with an unexported parameter are empty");
fragment.fragmentParameters.clear();
@ -1234,8 +1215,6 @@ void validationTests() {
}
int main() {
// The guest allocation tests expect mutations of leased ranges to be refused; they need not wait
// the default minute for each refusal. Set before the registry first reads it.
_putenv_s("APS5_PIN_WAIT_MS", "200");
try {
{
@ -1273,7 +1252,6 @@ int main() {
mock = MockVulkan{};
auto bdaContext = mockContext();
bdaContext.bufferDeviceAddress = true;
// The fault buffer (with its written-page slots) is larger than the mock's 4 KiB default.
bdaContext.limits.maxStorageBufferRange = 1u << 27;
RunBdaResourceTests(bdaContext, {
[](VkBuffer buffer) -> std::span<std::byte> { return mock.memories.at(mock.bufferMemory.at(buffer)); },
@ -1284,7 +1262,6 @@ int main() {
throw std::runtime_error("missing BDA test descriptor");
},
[](VkDeviceAddress address) {
// The inverse of mockGetBufferDeviceAddress.
const auto offset = address - 0x100000000000ULL;
const auto buffer = reinterpret_cast<VkBuffer>(offset / 0x10000);
return std::span<std::byte>(mock.memories.at(mock.bufferMemory.at(buffer))).subspan(offset % 0x10000);

View File

@ -24,7 +24,6 @@ struct Fields {
std::uint32_t filterMode = 0;
bool disableDegamma = false;
std::uint32_t minLodRaw = 0;
// MAX_LOD is 12 bits of unsigned 4.8 fixed point: 0xc00 is LOD 12.
std::uint32_t maxLodRaw = 0xc00;
std::uint32_t perfMip = 0;
std::uint32_t perfZ = 0;

View File

@ -114,8 +114,6 @@ void RunGuestTextureResourceTests() {
tileModes.tileModeRaw = 0x09;
Require(DecodeTextureResource(pack(tileModes)).tileMode == TextureTileMode::kStandard64KB, "tile mode 0x09 must decode to standard 64KB");
tileModes.tileModeRaw = 0x1b;
// An XOR swizzle folds a pipe/bank XOR into the low base bits; only 64 KiB aligned bases are
// modeled, so the fixture's unaligned base is refused and an aligned one decodes.
rejectFields(tileModes, "pipe/bank XOR base");
tileModes.base40 = 0x120000ull;
Require(DecodeTextureResource(pack(tileModes)).tileMode == TextureTileMode::RenderTarget64KB, "tile mode 0x1b must decode to render target 64KB");
@ -220,14 +218,11 @@ void RunGuestTextureResourceTests() {
badMsaa.msaaDepth = true;
rejectFields(badMsaa, "MSAA");
// DCC block size overrides only matter to DCC metadata, which is not read; they decode.
Fields blockSize = base;
blockSize.maxUncompBlkSize = 1;
blockSize.maxCompBlkSize = 1;
Require(DecodeTextureResource(pack(blockSize)).baseAddress == DecodeTextureResource(pack(base)).baseAddress, "DCC block size overrides changed texture storage");
// DCC metadata decodes: its address only with metadata compression on, and where alpha sits; the
// pipe alignment, write compression and color transform flags do not change what a read returns.
Fields meta = base;
meta.metaPipeAligned = true;
meta.writeCompress = true;
@ -250,7 +245,6 @@ void RunGuestTextureResourceTests() {
badLevels.maxMip = 1;
rejectFields(badLevels, "base mip level past its last mip level");
// A view may stop before the surface's last level: the surface keeps its mip count.
Fields partialMips = base;
partialMips.lastLevel = 1;
partialMips.maxMip = 2;

View File

@ -343,8 +343,6 @@ void testMemorySynchronization() {
}
void testWriteChangedKeepsUntouchedBytes() {
// Write-back stores only the bytes the GPU changed: its other bytes are the snapshot, and the
// CPU may have written them since.
alignas(256) static std::uint8_t guest[256];
std::memset(guest, 0, sizeof(guest));
std::vector<std::byte> original(sizeof(guest)), current(sizeof(guest));

View File

@ -1,5 +1,3 @@
// A selection in a loop with an arm that continues and another target it shares with an enclosing
// selection: its merge is where the other arm goes, so splitting the shared merge terminates.
#include "ControlFlow/Structurizer.hpp"
#include <cstdio>
#include <exception>
@ -9,8 +7,6 @@
using namespace ShaderRecompiler;
int main() {
// 0 -> 1 (loop header) -> 2; 2: 5 | 3; 3: 5 | 4; 4 -> 7 (continue); 5: 6 (exit) | 7; 6 returns;
// 7 -> 1. Around the back edge every path from 4 passes 5, so 5 post-dominates both selections.
const std::vector<std::vector<std::uint32_t>> successors{{1}, {2}, {5, 3}, {5, 4}, {7}, {6, 7}, {}, {1}};
ControlFlowGraph graph;
graph.entryBlock = 0;

View File

@ -88,15 +88,11 @@ void RunTextureTilingTests() {
{
const auto mips = ComputeMipLayout(TextureTileMode::RenderTarget64KB, 56, 257, 129, 1);
Require(mips[0].blocksPerRow == 3 && mips[0].tiledSize == 393216, "render target surfaces must pad to complete 128 by 128 blocks for 32-bit pixels");
// The detiler writes linear rows at the padded pitch (three 128-texel blocks), so the linear
// region spans whole padded rows.
Require(mips[0].pitchBytes == 1536 && mips[0].linearSize == 1536u * 129u, "detiled render target rows must span the padded block width");
Require(ComputeSurfaceSize(mips, 6) == 2359296, "render target cube faces must retain the padded guest slice stride");
}
for (const auto format : std::array<std::uint32_t, 5>{1, 7, 56, 71, 77}) {
const auto mips = ComputeMipLayout(TextureTileMode::RenderTarget64KB, format, 1024, 513, 11);
// Linear offsets follow the tiled ones (the smallest regular level first), with the tail levels
// after the chain, so the ranges are checked for overlap in address order, not level order.
std::vector<std::pair<std::uint64_t, std::uint64_t>> ranges;
bool tailSeen = false;
for (const auto& mip : mips) {
@ -113,11 +109,8 @@ void RunTextureTilingTests() {
for (std::size_t index = 1; index < ranges.size(); ++index) Require(ranges[index].first >= ranges[index - 1].second, "detiled mip levels must occupy separate ranges");
Require(tailSeen && !mips.front().tail, "render target mip chains must cover both regular blocks and mip tails");
}
// Block-compressed formats are laid out in render target tiling too: a 64 by 64 texel level is one
// guest 64KB block.
const auto compressed = ComputeMipLayout(TextureTileMode::RenderTarget64KB, 169, 64, 64, 1);
Require(compressed.size() == 1 && compressed[0].tiledSize == 65536 && compressed[0].linearSize != 0, "block compressed render target layout is wrong");
// Formats 128, 129 and 132 are laid out in render target tiling as well.
Require(ComputeMipLayout(TextureTileMode::RenderTarget64KB, 132, 64, 64, 1).size() == 1, "format 132 render target layout is missing");
reject([] { ComputeMipLayout(TextureTileMode::RenderTarget64KB, 74, 64, 64, 1); }, "unsupported bytes per element");

View File

@ -80,7 +80,6 @@ bool Replay(const char* path) {
const auto result = ShaderRecompiler::Recompile(request.request);
std::printf(" recompiled: %zu SPIR-V words\n", result.spirv.size());
if (g_spirv) {
// For spirv-val: <request>.spv in the working directory.
std::string name(path);
name = name.substr(name.find_last_of("/\\") + 1) + ".spv";
std::ofstream(name, std::ios::binary).write(reinterpret_cast<const char*>(result.spirv.data()), static_cast<std::streamsize>(result.spirv.size() * sizeof(result.spirv[0])));

View File

@ -97,7 +97,6 @@ struct Instance {
std::uint64_t totalDecodedSamples = 0;
SidebandGaplessDecode gapless{};
bool flagsReported = false;
// MP3: the FFmpeg decoder (it keeps the bit reservoir between frames) and the last frame's shape.
AVCodecContext* mp3 = nullptr;
std::uint32_t mp3Channels = 0;
std::uint32_t mp3SampleRate = 0;
@ -246,7 +245,6 @@ std::vector<std::uint8_t> JoinInputs(const JobHeader& job, const AjmBuffer* inpu
return input;
}
// The job's output buffers, filled in order.
struct PcmOutputs {
const AjmBuffer* buffers;
std::uint32_t count;
@ -275,12 +273,10 @@ struct PcmOutputs {
}
};
// Instance flags bits 7..9 select the PCM encoding: 0 16-bit, 1 32-bit integer, 2 float.
std::uint32_t PcmEncoding(const Instance& instance) {
return static_cast<std::uint32_t>((instance.flags >> 7u) & 7u);
}
// Emits one decoded frame of interleaved PCM after the instance's gapless skip and up to its sample limit.
void EmitFrame(Instance& instance, PcmOutputs& outputs, const std::uint8_t* pcm, std::size_t samples, std::size_t channels, std::size_t sampleBytes) {
std::size_t first = 0;
std::size_t count = samples;
@ -298,7 +294,6 @@ void EmitFrame(Instance& instance, PcmOutputs& outputs, const std::uint8_t* pcm,
instance.totalDecodedSamples += count;
}
// The sidebands a run job asked for, in the SDK's order after its result.
void WriteRunSideband(const JobHeader& job, const Instance& instance, std::int32_t result, std::size_t consumed, std::size_t produced, std::uint32_t frames, const SidebandFormat& format) {
auto* sideband = static_cast<std::uint8_t*>(job.sideband);
std::size_t offset = 0;
@ -321,8 +316,6 @@ void WriteRunSideband(const JobHeader& job, const Instance& instance, std::int32
}
}
// Decodes AT9 frames from the concatenated inputs into the concatenated outputs in the instance's PCM
// encoding.
void RunAt9(Instance& instance, const JobHeader& job, const AjmBuffer* inputs, const AjmBuffer* outputs) {
const auto input = JoinInputs(job, inputs);
PcmOutputs pcmOutputs(outputs, job.outputCount);
@ -398,8 +391,6 @@ void RunAt9(Instance& instance, const JobHeader& job, const AjmBuffer* inputs, c
WriteRunSideband(job, instance, result, consumed, pcmOutputs.produced, frames, SidebandFormat{static_cast<std::uint32_t>(channels), ChannelMask(channels), static_cast<std::uint32_t>(instance.info.samplingRate), encoding, 0, 0});
}
// An MPEG audio Layer III frame header: the frame's bytes and PCM shape; false when the bytes do not
// start one (free-format bitrates included).
struct Mp3Frame {
std::size_t bytes;
std::uint32_t channels;
@ -437,8 +428,6 @@ void OpenMp3(Instance& instance) {
if (!instance.mp3 || avcodec_open2(instance.mp3, codec, nullptr) < 0) throw std::runtime_error("AJM: cannot open the FFmpeg MP3 decoder");
}
// Decodes whole MP3 frames from the concatenated inputs. Like AT9 superframes, a frame is decoded only
// once all its bytes are present: a trailing partial frame stays unconsumed as partial input.
void RunMp3(Instance& instance, const JobHeader& job, const AjmBuffer* inputs, const AjmBuffer* outputs) {
const auto input = JoinInputs(job, inputs);
PcmOutputs pcmOutputs(outputs, job.outputCount);
@ -467,7 +456,6 @@ void RunMp3(Instance& instance, const JobHeader& job, const AjmBuffer* inputs, c
if (frames == 0) result |= AJM_RESULT_NOT_ENOUGH_ROOM;
break;
}
// Not reference counted: the decoder copies the frame into a padded buffer of its own.
packet->data = const_cast<std::uint8_t*>(input.data() + consumed);
packet->size = static_cast<int>(frame.bytes);
if (avcodec_send_packet(instance.mp3, packet.get()) < 0) {
@ -620,7 +608,6 @@ int APS5_VABI sceAjmInstanceCreate(uint32_t context, uint32_t codec, uint64_t fl
auto created = std::make_unique<Instance>();
created->codec = codec;
created->flags = flags;
// MP3 has no codec parameters: the stream's frame headers carry them, so it runs without an initialize job.
if (codec == CODEC_MP3) {
OpenMp3(*created);
created->initialized = true;

View File

@ -17,7 +17,6 @@ static constexpr char TEMPORARY_MOUNT_POINT[] = "/temp0";
static constexpr char DOWNLOAD_MOUNT_POINT[] = "/download0";
static constexpr uint32_t TEMPORARY_DATA_OPTION_FORMAT = 1;
// Temporary data lives in <root>/temp0 on the host and survives until the title formats it.
static std::filesystem::path TemporaryDirectory(const AppContentMountPoint* mount_point) {
if (!mount_point || std::strncmp(mount_point->data, TEMPORARY_MOUNT_POINT, sizeof(mount_point->data)) != 0) APS5_INVALID_ARG_EX;
return ResolvePath_nid_no_patch(TEMPORARY_MOUNT_POINT);
@ -62,7 +61,6 @@ int APS5_VABI sceAppContentDownloadDataGetAvailableSpaceKb(const AppContentMount
if (!mount_point || std::strncmp(mount_point->data, DOWNLOAD_MOUNT_POINT, sizeof(mount_point->data)) != 0) APS5_INVALID_ARG_EX;
const std::uint64_t quotaKb = GetAppDownloadDataSizeMiB_nid_postfix() * 1024u;
if (quotaKb == 0) throw std::logic_error(std::string(__func__) + ": the title declares no download data");
// The system mounts the title's download data before boot; on the host it is <root>/download0.
const auto directory = ResolvePath_nid_no_patch(DOWNLOAD_MOUNT_POINT);
std::filesystem::create_directories(directory);
std::uint64_t usedKb = 0;

View File

@ -331,7 +331,6 @@ int APS5_VABI sceAudioOutOpen(int userId, int type, int index, std::uint32_t len
if (type != PORT_TYPE_VIBRATION) {
openDevice(port);
}
// APS5_TRACE_AUDIOOUT=1 reports the ports a title opens.
static const bool trace = std::getenv("APS5_TRACE_AUDIOOUT") != nullptr;
if (trace) std::fprintf(stderr, "[audioout] port %d: type %d, %u samples at %u Hz, format %d, device %u\n", i + 1, type, len, freq, static_cast<int>(format), static_cast<unsigned>(port.device));
return i + 1;

View File

@ -46,12 +46,8 @@ struct AudioOut2Port {
std::uint32_t dataFormat = 0;
std::uint32_t samplingFreq = 0;
std::uint32_t flags = 0;
// Channel count and sample type decoded from dataFormat: float samples, or 16-bit integers.
std::uint32_t channels = 0;
bool int16 = false;
// The PCM buffer (one grain, interleaved) the title last handed over through the data attribute.
// It is guest memory the title rewrites every tick, so it is read when the context mixes, not
// when it is set.
const void* data = nullptr;
float volume[AUDIO_OUT2_PORT_CHANNELS_MAX] = {1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f};
std::uint64_t dataSets = 0;

View File

@ -22,9 +22,6 @@ static constexpr std::uint64_t ATTRIBUTE_TRACE_EVERY = 2000;
static constexpr std::uint32_t ATTRIBUTE_DATA = 0;
static constexpr std::uint32_t ATTRIBUTE_VOLUME = 1;
// data_format bits 8..11 carry the channel count, bits 0..6 the sample type (0 float, 1 16-bit
// integer) and bit 7 the standard 8-channel order, as Kyty decodes it: the titles open 0x100 (mono
// object ports), 0x200 (stereo), 0x880 (7.1 bed) and 0x201 (a Bink movie's sound).
static constexpr std::uint32_t FORMAT_CHANNELS_SHIFT = 8;
static constexpr std::uint32_t FORMAT_CHANNELS_MASK = 0xFu;
static constexpr std::uint32_t FORMAT_TYPE_MASK = 0x7Fu;

View File

@ -10,7 +10,6 @@
namespace {
// Layout of the params titles pass to sceAudioOut2SpeakerArrayCreate.
struct GuestVbapParams {
const AudioOut2Position* positions;
std::uint32_t numSpeakers;
@ -24,7 +23,6 @@ constexpr std::uint32_t MaxSpeakers = 32;
constexpr std::uint32_t AmbisonicsChannelFlag = 0x40;
constexpr std::uint32_t MaxAmbisonicsOrder = 5;
// Lives in the memory the title allocates from sceAudioOut2GetSpeakerArrayMemorySize; the handle points at it.
struct SpeakerArray {
std::uint32_t magic;
std::uint32_t numSpeakers;
@ -38,7 +36,6 @@ SpeakerArray& Array(AudioOut2SpeakerArrayHandle handle) {
return *array;
}
// Real SN3D spherical harmonic for ACN index `acn` (no Condon-Shortley phase), frame x front, y left, z up.
float Sn3d(std::uint32_t acn, const AudioOut2Position& direction) {
const int n = static_cast<int>(std::sqrt(static_cast<double>(acn)));
const int m = static_cast<int>(acn) - n * n - n;
@ -87,12 +84,9 @@ int APS5_VABI sceAudioOut2SpeakerArrayDestroy(AudioOut2SpeakerArrayHandle handle
return 0;
}
// Basic sampling decoder: speaker gain = sum over channels of Y(speaker) / numSpeakers, so a stereo pair decodes
// first order as two cardioids. Orders the layout cannot resolve (2D rule: 2N+1 speakers) get zero.
int APS5_VABI sceAudioOut2GetSpeakerArrayAmbisonicsCoefficients(AudioOut2SpeakerArrayHandle handle, uint32_t ambisonics_channel, float* coefficients, uint32_t num_coefficients) {
const auto& array = Array(handle);
if (coefficients == nullptr || num_coefficients != array.numSpeakers) APS5_INVALID_ARG_EX;
// ponytail: only the 0x40-flagged encoding seen so far (assumed ACN/SN3D); other encodings throw until seen.
const std::uint32_t acn = ambisonics_channel & ~AmbisonicsChannelFlag;
if ((ambisonics_channel & AmbisonicsChannelFlag) == 0 || acn >= (MaxAmbisonicsOrder + 1) * (MaxAmbisonicsOrder + 1)) {
throw std::invalid_argument("unsupported ambisonics channel " + std::to_string(ambisonics_channel));

View File

@ -7,7 +7,6 @@
#include <string>
namespace {
// Deletes exported content; the lifecycle is honest, deletion is not implemented.
std::atomic<bool> g_initialized{false};
}

View File

@ -7,7 +7,6 @@
#include <string>
namespace {
// Exports to the system gallery; the lifecycle is honest, exports themselves are not implemented.
std::atomic<bool> g_initialized{false};
}

View File

@ -7,7 +7,6 @@
#include <string>
namespace {
// Searches the system content library; the lifecycle is honest, searches are not implemented.
std::atomic<bool> g_initialized{false};
}

View File

@ -6,7 +6,6 @@
extern "C" {
int APS5_VABI sceGameLiveStreamingInitialize(size_t heap_size) {
// Nothing is ever broadcast; the library only has to accept its lifecycle.
if (heap_size == 0) APS5_INVALID_ARG_EX;
return 0;
}

View File

@ -9,8 +9,6 @@
namespace {
// The host keyboard drives the virtual pad (docs/INPUT_MAPPING.md), so no USB keyboard is attached:
// opening the keyboard succeeds, as on a console with none plugged in, and it never reports events.
std::mutex g_keyboardMutex;
std::set<int32_t> g_openKeyboards;
@ -99,7 +97,6 @@ int APS5_VABI sceImeSetTextGeometry(TextAreaMode mode, const TextGeometry* geome
int APS5_VABI sceImeUpdate(EventHandler handler) {
if (!handler) APS5_INVALID_ARG_EX;
// Only keyboard events could be pending (sceImeOpen is not implemented), and none are generated.
return 0;
}

View File

@ -6,7 +6,6 @@
#include "prx/libc/include/General.hpp"
namespace {
// Common dialog status: 0 none, 1 initialized, 2 running, 3 finished. Nothing is ever opened here, so it never runs.
std::atomic<int> g_status{0};
}

View File

@ -14,7 +14,6 @@
#include <windows.h>
#endif
// The base game is the full SKU; the add-ons the account owns come from anyps5-entitlements.ini (below).
static constexpr int SCE_NP_ENTITLEMENT_ACCESS_ERROR_PARAMETER = static_cast<int>(0x80558003);
static constexpr int SCE_NP_ENTITLEMENT_ACCESS_ERROR_NOT_FOUND = static_cast<int>(0x80558007);
static constexpr uint32_t SKU_FLAG_FULL = 3;
@ -33,10 +32,6 @@ std::filesystem::path EntitlementsPath() {
#endif
}
// The add-on entitlements the account owns: one unified entitlement label per line of
// anyps5-entitlements.ini next to the executable (ANYPS5_ENTITLEMENTS names another file), '#' and ';'
// starting comments. Without the file the account owns no add-on. They are reported as installed
// license-only add-ons (package type 3, download status 4), as Kyty reports them.
const std::vector<NpEntitlementAccessAddcontEntitlementInfo>& OwnedAddons() {
static const auto owned = [] {
std::vector<NpEntitlementAccessAddcontEntitlementInfo> addons;

View File

@ -128,28 +128,24 @@ void APS5_VABI sceNpRegisterGamePresenceCallback(void* callback, void* userdata)
int APS5_VABI sceNpRegisterNpReachabilityStateCallback(void* callback, void* userdata) {
(void)userdata;
// The signed-out state never changes, so registered callbacks never fire.
if (!callback) return SCE_NP_ERROR_INVALID_ARGUMENT;
return 0;
}
int APS5_VABI sceNpRegisterPlusEventCallback(void* callback, void* userdata) {
(void)userdata;
// The signed-out state never changes, so registered callbacks never fire.
if (!callback) return SCE_NP_ERROR_INVALID_ARGUMENT;
return 0;
}
int APS5_VABI sceNpRegisterPremiumEventCallback(void* callback, void* userdata) {
(void)userdata;
// The signed-out state never changes, so registered callbacks never fire.
if (!callback) return SCE_NP_ERROR_INVALID_ARGUMENT;
return 0;
}
int APS5_VABI sceNpRegisterStateCallback(void* callback, void* userdata) {
(void)userdata;
// The signed-out state never changes, so registered callbacks never fire.
if (!callback) return SCE_NP_ERROR_INVALID_ARGUMENT;
return 0;
}

View File

@ -5,7 +5,6 @@
#include "SceTypes.hpp"
#include "prx/libc/include/General.hpp"
// A PC has no Remote Play: the library initializes and every user stays disconnected.
constexpr int REMOTEPLAY_CONNECTION_STATUS_DISCONNECT = 0;
extern "C" {

View File

@ -6,7 +6,6 @@
extern "C" {
int APS5_VABI sceSharePlayInitialize(void* heap, size_t heap_size) {
// No Share Play session can ever start; the library only has to accept its lifecycle.
if (heap == nullptr || heap_size == 0) APS5_INVALID_ARG_EX;
return 0;
}

View File

@ -85,8 +85,6 @@ public:
if (queue->stopping || cfg->shutdownToken.stop_requested()) throw ProcessShutdown{};
std::lock_guard lock(cfg->mutex);
checkConfig(*cfg);
// The limit was waited for by WaitForFlipRoom before the driver took its lock; concurrent
// submitters may overshoot it by their own flips.
if (info.index >= 0) {
request->buffer = cfg->buffers[info.index];
require(request->buffer.Occupied(), "flip buffer is not registered");
@ -507,7 +505,6 @@ void VideoOutDriver::presentLoop(std::stop_token token) {
}
if (current) {
current.reset();
// processFlip released the flip's reservation: a submitter may be waiting for room.
flipQueue->changed.notify_all();
}
}

View File

@ -10,8 +10,6 @@
#include <vector>
namespace {
// Recording metadata the title sets for the system recorder (titles, permission levels, guard
// areas); kept as given. Nothing records, so recording itself is not implemented.
std::mutex g_infoMutex;
std::map<int32_t, std::vector<unsigned char>> g_info;
}

View File

@ -9,8 +9,6 @@
#include "SceTypes.hpp"
#include "prx/libc/include/General.hpp"
// A console with no headset and no voice peers: the library runs its lifecycle and keeps ports and
// their connections, but no voice ever arrives, so output ports stay empty.
namespace {
constexpr std::int32_t PortInDevice = 0;
@ -70,7 +68,6 @@ int APS5_VABI sceVoiceInit(VoiceInitParam* param, int32_t version) {
}
int APS5_VABI sceVoiceSetThreadsParams(void* params) {
// Thread priorities and affinities for the voice threads; this library runs none.
if (params == nullptr) APS5_INVALID_ARG_EX;
return 0;
}
@ -176,8 +173,6 @@ int APS5_VABI sceVoiceGetPortInfo(uint32_t port_id, VoicePortInfo* info) {
*info = VoicePortInfo{};
info->port_type = port.type;
info->state = PortStateReady;
// Titles pace voice by frame (a title divides by the frame length it derives from this).
// ponytail: the codec's real frame layout is unknown; 20 ms frames at the port's bitrate.
if (port.bitrate != 0) info->frame_size = (port.bitrate * VoiceFrameMs + 7999u) / 8000u;
return 0;
}
@ -196,7 +191,6 @@ int APS5_VABI sceVoiceReadFromOPort(uint32_t output_port_id, void* data, uint32_
auto& voice = State();
std::lock_guard lock(voice.mutex);
if (IsInput(RequirePort(voice, output_port_id, __func__).type)) Fail(__func__, "port is not an output port");
// Inputs are the absent microphone or WriteToIPort, which is not implemented: nothing to read.
*size = 0;
return 0;
}

View File

@ -6,7 +6,6 @@
#include "prx/libc/include/General.hpp"
namespace {
// Common dialog status: 0 none, 1 initialized, 2 running, 3 finished. Nothing is ever opened here, so it never runs.
std::atomic<int> g_status{0};
}

View File

@ -5,7 +5,6 @@
#include <cstdint>
// Guest virtual memory is placed inside one reserved arena below the PS5 application map limit
// (0xFC_0000_0000), in ascending first-fit order from the system-managed area like the PS5 kernel. Guest code indexes tables by
// absolute address and breaks on host addresses outside that range.
namespace GuestArena {

View File

@ -202,7 +202,6 @@ inline int FormatWindows(char* buffer, size_t size, const char* format, const vo
AppendUtf8(utf8, static_cast<char16_t>(args.Next<unsigned int>()));
output.Value(spec + 's', utf8.c_str());
} else if (conversion == 's' && (length.empty() || length == "h")) {
// FreeBSD's printf, the console's, prints a null string as "(null)".
const char* value = args.Next<const char*>();
if (!value) value = "(null)";
output.Value(spec + 's', value);

View File

@ -18,7 +18,6 @@
namespace {
// Guest long is 64-bit; MinGW's is 32-bit, so integer "l" conversions need "ll" on the host.
std::string HostLength(const std::string& length, char conversion) {
#ifdef _WIN32
if (length == "l" && std::strchr("diouxXn", conversion) != nullptr) return "ll";
@ -28,8 +27,6 @@ std::string HostLength(const std::string& length, char conversion) {
return length;
}
// Guest scanf, one directive at a time through the host sscanf, so the guest arguments can come from any
// va_list flavor. secure adds the _s rules: %s, %c and %[ take a buffer capacity after the pointer.
template <typename NextPointer, typename NextCapacity>
int ScanGuest(const char* buffer, const char* format, bool secure, NextPointer nextPointer, NextCapacity nextCapacity) {
if (buffer == nullptr || format == nullptr) return EOF;

View File

@ -53,7 +53,6 @@ std::optional<std::filesystem::path> ResolveAlias(const std::string& guestPath)
struct WorkingDirectory {
std::mutex mutex;
const std::filesystem::path root = std::filesystem::canonical(std::filesystem::current_path());
// A PS5 process starts in /app0, so titles open game files by relative path.
std::filesystem::path current = std::filesystem::is_directory(root / "app0") ? root / "app0" : root;
};
WorkingDirectory& Directories() { static WorkingDirectory state; return state; }

View File

@ -37,8 +37,6 @@ std::atomic<std::uint64_t> generation{1};
std::atomic<void (*)(std::uintptr_t, std::size_t)> invalidator{nullptr};
std::atomic<bool (*)()> pinWaiter{nullptr};
// How long a mutation of a leased range waits for the lease before it is refused:
// APS5_PIN_WAIT_MS, 60 s by default (tests that expect the refusal set it short).
std::chrono::milliseconds pinWait() {
static const std::chrono::milliseconds value{[] {
const char* text = std::getenv("APS5_PIN_WAIT_MS");
@ -210,9 +208,6 @@ void GuestAllocationsRequireUnpinned_nid_postfix(void* mutation, const void* poi
// this registry, so waiting with the lock held would deadlock them. The scan restarts after the
// lock is retaken, since another mutation may have run meanwhile. Without a waiter the lease is
// dropped by another thread (a synchronous draw or dispatch), so yielding suffices. A waiter
// round that finished nothing (the holder is not recorded GPU work) yields: the holder may be a
// driver thread inside a long Vulkan call (importing host memory takes a lease for its whole
// length), so every wait is bounded by the deadline only.
auto* state = static_cast<MutationState*>(mutation);
const auto deadline = std::chrono::steady_clock::now() + pinWait();
int syncedRounds = 0;

View File

@ -15,10 +15,6 @@
namespace GuestArena {
namespace {
// Mappings without an address go to the system-managed area from 0x2_0000_0000 first, as in Kyty and
// shadPS4; the application area from 0x10_0000_0000 is left to fixed mappings until that fills (titles
// map their virtual memory pages at a fixed window from 0x10_0000_0000). The system-reserved range
// between the two is never handed out.
constexpr std::uintptr_t PreferredBase = 0x0000000200000000ull;
constexpr std::uintptr_t SystemReservedStart = 0x00000007FFFFC000ull;
constexpr std::uintptr_t SystemReservedEnd = 0x0000001000000000ull;

View File

@ -98,7 +98,6 @@ void APS5_VABI srand_nid_postfix(unsigned int seed) {
}
// Dinkumware float classification codes returned by _FDtest and _FExp.
constexpr short FloatDenorm = -2;
constexpr short FloatFinite = -1;
constexpr short FloatInfinite = 1;
@ -116,7 +115,6 @@ short APS5_VABI _FDtest_nid_postfix(const float* x) {
}
}
// Stores y * e^(*x) * 2^eoff in *x and classifies the result as 0 (zero or underflow), infinite, or finite.
short APS5_VABI _FExp_nid_postfix(float* x, float y, short eoff) {
*x = static_cast<float>(std::ldexp(static_cast<double>(y) * std::exp(static_cast<double>(*x)), eoff));
if (*x == 0.0f) return 0;

View File

@ -104,7 +104,6 @@ unsigned int APS5_VABI _ZSt14_Random_devicev_nid_postfix() {
throw std::runtime_error("_Throw_Cpp_error: C++ thread error " + std::to_string(code));
}
// The title's std::error_category object carries its own Dinkumware vtable and string ABI.
const void* APS5_VABI _ZSt16generic_categoryv_nid_postfix() {
NotImplemented_nid_no_patch(__func__);
return nullptr;

View File

@ -51,7 +51,6 @@ int APS5_VABI sprintf_s_nid_postfix(char* buffer, size_t size, const char* forma
std::abort();
}
// Dinkumware lconv as the title's libc lays it out: monetary strings, CHAR_MAX monetary flags, then numeric and message strings.
struct GuestLconv {
const char* monetary[7];
char monetaryFlags[14];

View File

@ -22,7 +22,6 @@ AppTitle GetAppTitle_nid_postfix();
AppTitleId GetAppTitleId_nid_postfix();
bool HasAppIcon_nid_postfix();
AppIconData GetAppIconData_nid_postfix();
// The download data quota param.json declares, in MiB (0 when none).
std::uint64_t GetAppDownloadDataSizeMiB_nid_postfix();
}

View File

@ -8,7 +8,6 @@
struct ParsedParamJson {
std::string title;
std::string titleId;
// The download data quota in MiB; 0 when the title declares none.
std::uint64_t downloadDataSizeMiB = 0;
};

View File

@ -187,7 +187,6 @@ int APS5_VABI sceKernelAprResolveFilepathsToIdsAndFileSizes(const char** paths,
return 0;
}
// The prefix is prepended to every path; titles pass "" with absolute /app0 paths.
int APS5_VABI sceKernelAprResolveFilepathsWithPrefixToIdsAndFileSizes(const char* prefix, const char** paths, uint32_t count, uint32_t* ids, uint64_t* sizes, uint32_t* error_index) {
if (!prefix || !paths || !ids) return _fail(GUEST_EINVAL);
for (uint32_t index = 0; index < count; ++index) {
@ -223,8 +222,6 @@ int APS5_VABI sceKernelAprSubmitCommandBuffer(const Apr::CommandBufferObject* bu
return 0;
}
// Command buffers run synchronously, so a submission is complete (result 0) before its id is returned
// and waiting on it has nothing left to do.
int APS5_VABI sceKernelAprSubmitCommandBufferAndGetId(const Apr::CommandBufferObject* buffer, uint32_t priority, uint32_t* id) {
if (!id) return _fail(GUEST_EINVAL);
const int result = sceKernelAprSubmitCommandBuffer(buffer, priority);

View File

@ -219,17 +219,12 @@ void Trace(const char* format, ...) {
va_end(args);
}
// Direct memory keeps its contents across mappings: titles unmap physical pages and map them again at
// another address (titles move their pools that way) and read back what they wrote through the old one.
// The contents move by copying at unmap and map; a shared section with views (as in Kyty) would alias
// them for real but loses the write watching of the guest arena.
struct DirectMapping {
std::uintptr_t end;
std::uint64_t phys;
};
std::mutex g_directLock;
std::map<std::uintptr_t, DirectMapping> g_directMappings;
// Contents of unmapped direct memory by physical page; all-zero pages are left out.
std::map<std::uint64_t, std::unique_ptr<unsigned char[]>> g_physPages;
void ProtectOrThrow(std::uintptr_t address, std::size_t len, int nativeProt) {
@ -248,13 +243,8 @@ void SaveContents(std::uintptr_t address, std::size_t len, std::uint64_t phys) {
}
}
// A physical page mapped at more than one address has the same contents in every view, and every
// view lacks write access while it is shared (g_shared).
// ponytail: a write to a shared page faults; handing the page to the writing view on the fault, with
// the GPU driver's imports of the other views, is the upgrade if a title writes through an alias.
std::set<std::uintptr_t> g_shared;
// The views of physPage in g_directMappings.
std::vector<std::uintptr_t> Views(std::uint64_t physPage) {
std::vector<std::uintptr_t> views;
for (const auto& [base, mapping] : g_directMappings) {
@ -273,9 +263,6 @@ int SharedProtection(int nativeProt) {
return nativeProt & ~PROT_WRITE;
}
// Takes the shared page out of its sharing; the views left keep sharing it, or the last one gets its
// own protection back. The views must still hold the same bytes: a write that reached one of them
// (the GPU writes through its imports of guest memory) would have been lost to the others.
void Unshare(std::uintptr_t page, std::uint64_t physPage) {
const auto others = Views(physPage);
if (others.empty()) throw std::runtime_error("shared direct memory page has no other view");
@ -293,7 +280,6 @@ void Unshare(std::uintptr_t page, std::uint64_t physPage) {
ProtectOrThrow(others.front(), PS5_PAGE_SIZE, last ? prot : SharedProtection(prot));
}
// Takes [start, end) out of the direct mappings; with save its bytes stay as the contents of its pages.
void EraseMappings(std::uintptr_t start, std::uintptr_t end, bool save) {
auto it = g_directMappings.lower_bound(start);
if (it != g_directMappings.begin() && std::prev(it)->second.end > start) --it;
@ -317,7 +303,6 @@ void EraseMappings(std::uintptr_t start, std::uintptr_t end, bool save) {
}
}
// Records a fresh mapping of phys at address (zero-filled, already protected nativeProt) and brings its contents back.
void AddMapping(std::uintptr_t address, std::size_t len, std::uint64_t phys, int nativeProt) {
std::vector<std::pair<std::size_t, std::uintptr_t>> shared;
for (const auto& [base, mapping] : g_directMappings) {
@ -355,7 +340,6 @@ bool RemapFixedIntoRegistered(GuestAllocations::Mutation& mutation, void* addr,
const auto address = reinterpret_cast<std::uintptr_t>(addr);
std::lock_guard lock(g_directLock);
EraseMappings(address, address + len, true);
// The range shows the physical pages' contents now, not what was mapped there before.
#ifdef _WIN32
if (physStart >= 0 && !VirtualFree(addr, len, MEM_DECOMMIT)) throw std::system_error(static_cast<int>(GetLastError()), std::system_category(), "VirtualFree decommit failed");
if (!VirtualAlloc(addr, len, MEM_COMMIT, WinProtFromPosix(nativeProtection))) throw std::system_error(static_cast<int>(GetLastError()), std::system_category(), "VirtualAlloc commit failed");
@ -443,8 +427,6 @@ void* MapAligned(void* addr, size_t len, int prot, int flags, size_t alignment)
return aligned;
}
// SCE protections as the title set them: the host page protection keeps only the CPU bits, but titles
// read the GPU and AMPR bits back (a streamer reads straight into memory the APR may write).
struct ProtectedRange {
std::uintptr_t end;
int prot;

View File

@ -11,9 +11,7 @@ static constexpr size_t PS5_PAGE_SIZE = 0x4000;
int DirectMemoryAlloc(int64_t searchStart, int64_t searchEnd, size_t len, size_t alignment, int memoryType, int64_t* physOut);
void DirectMemoryFree(int64_t start, size_t len);
// Drops what the released physical range held and stops its live mappings from saving into it.
void ForgetDirectMemory(int64_t start, size_t len);
// Finds the allocation containing offset, or with findNext the first one above it; false when there is none.
bool DirectMemoryFind(int64_t offset, bool findNext, int64_t* start, int64_t* end, int* memoryType);
size_t DirectMemoryFreeRun(uint64_t offset, uint64_t limit);
int DoMapDirect(void** addr, size_t len, int prot, int flags, int64_t physStart, size_t alignment);
@ -21,7 +19,6 @@ int DoMapAnon(void** addr, size_t len, int prot, int flags);
int DoMprotect(const void* addr, size_t len, int prot);
int DoMunmap(void* addr, size_t len);
int DoReserveVirtual(void** addr, size_t len, size_t alignment);
// The SCE protection the title last mapped or protected addr with (GPU and AMPR bits included); false when unknown.
bool GuestProtection(uintptr_t addr, int* prot);
#endif

View File

@ -279,7 +279,6 @@ int APS5_VABI sceKernelMtypeprotect(const void* addr, size_t len, int type, int
return 0;
}
// The mapping that contains addr, as sceKernelVirtualQuery reports it without FIND_NEXT.
int APS5_VABI sceKernelQueryMemoryProtection(void* addr, void** start, void** end, int* prot) {
VirtualQueryInfo info{};
const int result = sceKernelVirtualQuery(addr, 0, &info, sizeof(info));

View File

@ -6,7 +6,6 @@
int DirectMemoryAlloc(int64_t searchStart, int64_t searchEnd, size_t len, size_t alignment, int memoryType, int64_t* physOut);
void DirectMemoryFree(int64_t start, size_t len);
// Finds the allocation containing offset, or with findNext the first one above it; false when there is none.
bool DirectMemoryFind(int64_t offset, bool findNext, int64_t* start, int64_t* end, int* memoryType);
#endif

View File

@ -4,15 +4,11 @@
#include <filesystem>
#include <optional>
// Windows cannot open a directory as a file descriptor, so a guest open() of a directory gets a
// descriptor for NUL that is tracked here with its path and a getdents cursor.
namespace File {
int OpenDirectoryDescriptor(const std::filesystem::path& path);
std::optional<std::filesystem::path> DirectoryDescriptorPath(int fd);
void ForgetDirectoryDescriptor(int fd);
// Fills FreeBSD dirent records { u32 fileno; u16 reclen; u8 type; u8 namlen; char name[]; } (4-byte aligned).
// Returns the bytes written, or -1 when fd is not a directory descriptor.
int ReadDirectoryDescriptor(int fd, char* buf, int nbytes);
}

View File

@ -47,9 +47,6 @@ static int NativeFlock(int descriptor, int operation) {
if (operation & 4) flags |= LOCKFILE_FAIL_IMMEDIATELY;
return ::LockFileEx(handle, flags, 0, MAXDWORD, MAXDWORD, &overlapped) ? 0 : -1;
}
// Positioned I/O through OVERLAPPED is atomic, unlike seek + read, which races when streaming threads share a
// descriptor. ponytail: on a synchronous handle it still moves the file pointer to offset + n, which POSIX pread
// does not; a mixed read()/pread() user on one descriptor would notice.
static std::int64_t NativePositioned(int descriptor, void* buf, std::size_t nbytes, std::int64_t offset, bool write) {
if (nbytes > static_cast<std::size_t>(std::numeric_limits<DWORD>::max())) {
throw std::runtime_error("NativePositioned: nbytes exceeds platform limit");

View File

@ -9,9 +9,6 @@
#include "../../Time/include/Time.hpp"
#include "Common.hpp"
// Dinkumware C11 thread support as the title's own libc.prx implements it: each _Mtx_t/_Cnd_t is a
// pointer-sized slot holding the sce object, and results are _Thrd_* codes.
extern "C" {
int APS5_VABI scePthreadCreate(Pthread* thread, const PthreadAttr* attr, PthreadEntry entry, void* arg, const char* name);
int APS5_VABI scePthreadJoin(Pthread thread, void** retval);
@ -37,7 +34,6 @@ int initResult(int result) {
return result == 0 ? ThrdSuccess : result == SceEnomem ? ThrdNomem : ThrdError;
}
// std::_Pad as laid out by the title's libc; _Go() is the first vtable slot.
struct GuestPad {
unsigned (APS5_VABI* const* vtable)(GuestPad*);
PthreadCond cond;
@ -101,7 +97,6 @@ int APS5_VABI _Cnd_broadcast_nid_postfix(PthreadCond* cond) {
return ThrdSuccess;
}
// xtime has the KernelTimespec layout and is an absolute gettimeofday deadline.
int APS5_VABI _Cnd_timedwait_nid_postfix(PthreadCond* cond, PthreadMutex* mutex, const KernelTimespec* deadline) {
KernelUseconds usec = 0;
if (!PosixThread::RelativeMicroseconds(GuestRealtimeClock, deadline, &usec)) throw std::runtime_error("_Cnd_timedwait: invalid xtime");
@ -120,7 +115,6 @@ Pthread APS5_VABI _Thrd_id_nid_postfix() {
return scePthreadSelf();
}
// Microseconds since the epoch, not the 100 ns ticks of other Dinkumware ports.
std::int64_t APS5_VABI _Xtime_get_ticks_nid_postfix() {
KernelTimeval now{};
gettimeofday_nid_postfix(&now, nullptr);

View File

@ -57,7 +57,6 @@ int APS5_VABI scePthreadAttrSetstacksize(PthreadAttr* attr, std::size_t stacksiz
if (!attr || !*attr) throw std::runtime_error(std::string(__func__) + ": null attr");
if (stacksize < 16384) throw std::runtime_error(std::string(__func__) + ": too small");
#ifdef _WIN32
// Like libthr, any size is kept as given; thread creation rounds it up to whole pages.
if (stacksize > std::numeric_limits<unsigned>::max() - 0xffffu)
throw std::runtime_error(std::string(__func__) + ": stack size exceeds the Windows limit");
#endif

View File

@ -228,7 +228,7 @@ int APS5_VABI bind_nid_postfix(int descriptor, const void* address, std::uint32_
int APS5_VABI shutdown_nid_postfix(int descriptor, int how) {
const auto socket = Lookup(descriptor);
if (!socket) return Fail(9);
if (how < 0 || how > 2) return Fail(22); // SHUT_RD/SHUT_WR/SHUT_RDWR share values with the host constants
if (how < 0 || how > 2) return Fail(22);
return ::shutdown(socket->value, how) ? Fail(NativeError()) : 0;
}
int APS5_VABI getsockname_nid_postfix(int descriptor, void* address, std::uint32_t* length) {

View File

@ -24,9 +24,6 @@ int APS5_VABI sceCoredumpUnregisterCoredumpHandler(void) {
return 0;
}
// A libc that fails to throw (no handler: the exception is uncaught) reports it here right before
// std::terminate, after its own stderr line; the console keeps it for the core dump, we print it.
// The second argument is 0x400 from the libc that calls it; its meaning is not known.
int APS5_VABI sceKernelDebugWriteCppExceptionInfo(const void* exception, uint64_t unknown, const char* typeName, const char* what) {
(void)unknown;
std::fprintf(stderr, "[coredump] uncaught C++ exception %p of type %s%s%s\n", exception, typeName ? typeName : "(unknown)", what ? ", what(): " : "", what ? what : "");

View File

@ -62,7 +62,6 @@ static void CheckNamedAndHintedMappings() {
Require(sceKernelMunmap(first, length) == 0);
}
// Direct memory contents follow the physical pages: unmapped and mapped again elsewhere, they read back.
static void CheckDirectMemoryFollowsPhysicalPages() {
constexpr std::size_t page = 0x4000;
std::int64_t phys = 0;

View File

@ -35,7 +35,6 @@ std::vector<InstructionMatch> InstructionScanner::ScanCodeSection(
try {
length = decoder.Decode(cursor, available);
} catch (const CodegenException& e) {
// Zero padding at the segment tail decodes as `add [rax], al` pairs; an odd tail leaves one truncated byte.
if (std::all_of(cursor, cursor + available, [](const std::uint8_t b) { return b == 0; })) break;
throw CodegenException(std::string("Cannot decode instruction: ") + e.what(), codeSectionOffset + i);
}

View File

@ -45,10 +45,6 @@ static_assert(offsetof(Range, deviceAddress) == 16 && offsetof(Range, permission
static_assert(std::is_standard_layout_v<Fault> && std::is_trivially_copyable_v<Fault> && sizeof(Fault) == 32);
static_assert(offsetof(Fault, address) == 8 && offsetof(Fault, instruction) == 24);
// After the Fault record, the fault buffer holds the pages the shader stored to through the table
// (Write permission), for the host to mark GPU-written once the work completed: an overflow word,
// then an open-addressed set of page number + 1 (0 is free), probed WrittenPageProbes slots from a
// multiplicative hash. A store that finds no slot sets the overflow word.
inline constexpr std::uint32_t WrittenPageShift = 12;
inline constexpr std::uint32_t WrittenPageSlots = 4096;
inline constexpr std::uint32_t WrittenPageProbes = 8;

View File

@ -670,7 +670,6 @@ std::string RequestSerializer::Serialize(const RecompileRequest& request) const
writeGraphicsCompileContext(writer, *request.graphics);
}
writer.WriteBool(request.useCache);
// Version 3: a compute request's partial-group size.
if (request.context.compute.has_value()) {
for (const std::uint32_t value : request.context.compute->partialThreads) {
writer.WriteU32(value);

View File

@ -288,7 +288,6 @@ bool canReachBefore(const ControlFlowGraph& graph, std::uint32_t start, std::uin
return false;
}
// Whether `start` reaches `target` without passing the loop's continue or merge block.
bool reachesWithinIteration(const ControlFlowGraph& graph, const NaturalLoop& loop, std::uint32_t start, std::uint32_t target) {
std::vector<std::uint32_t> pending = {start};
std::vector<bool> visited(graph.blocks.size(), false);
@ -345,9 +344,6 @@ std::uint32_t findSelectionMerge(const ControlFlowGraph& graph, const BasicBlock
if (isLoopControlGateway(graph, *loop, falseTarget) && graph.Dominates(block.id, falseTarget) && isInsideLoopConstruct(graph, *loop, trueTarget)) {
return falseTarget;
}
// An arm that reaches the post-dominator only in a later iteration (around the back edge)
// leaves the selection by continuing; the merge is where the other arm goes. Splitting the
// post-dominator off would leave it where it is, and the split repeats without end.
if (globalMerge != InvalidControlFlowId) {
const bool trueJoins = reachesWithinIteration(graph, *loop, trueTarget, globalMerge);
const bool falseJoins = reachesWithinIteration(graph, *loop, falseTarget, globalMerge);

View File

@ -49,8 +49,6 @@ struct IrBindingLayout {
std::uint32_t pushDataStartDword = PushData::NoStart;
std::uint32_t memoryOffsetDword = 0;
std::uint32_t memoryOffsetCount = 0;
// Three dwords after the memory offsets hold the partial-group dispatch size (ShaderInfo::
// dispatchThreadLimit).
bool dispatchThreadLimit = false;
std::vector<std::uint32_t> userDataRegisters;
std::vector<IrDescriptorBinding> descriptors;

View File

@ -45,11 +45,7 @@ struct MemoryInfo {
bool idxen = false;
bool offen = false;
bool planningOnly = false;
// A buffer load or store with GLC or DLC: it bypasses the caches another workgroup's stores
// may be stale in (a look-back polling a flag another workgroup publishes).
bool coherent = false;
// A buffer access whose V# the program computes on the GPU (loaded at a runtime offset, say),
// so no binding can hold it: the shader decodes the V# and goes through the BDA page table.
bool gpuDescriptor = false;
bool operator==(const MemoryInfo& other) const = default;

View File

@ -25,10 +25,6 @@ struct BufferResource {
bool atomic = false;
bool formatted = false;
bool scalar = false;
// Specialized: the V# has no records or is at address 0 (a null V#), so every access is out of
// bounds: loads read zero (or the format's default), stores and atomics are dropped. For no
// records that is the hardware's behavior; at address 0 an access would fault on hardware, so a
// working title never reaches such a slot.
bool empty = false;
bool operator==(const BufferResource& other) const = default;
@ -54,8 +50,6 @@ struct ImageResource {
bool depthCompare = false;
bool cube = false;
bool r128 = false;
// Specialized: a 32-bit integer read of a depth plane (IsDepthBitsTexture), bound as the float
// depth view; the texel bits are the result, the swizzle's constants integers.
bool depthBits = false;
std::uint32_t indirectRoot = NoIndirectImage;
std::uint32_t indirectMappingOffset = 0;

View File

@ -32,10 +32,7 @@ struct ShaderInfo {
bool instanceOffsetConflict = false;
bool hasBitwiseXor = false;
bool usesDma = false;
// Some access stores through the BDA page table (MemoryInfo::gpuDescriptor): the pages it
// writes are noted in the fault buffer (BdaAbi::WrittenPageSlots).
bool bdaWrites = false;
// The entry reads the partial-group dispatch size (StageInputKind::DispatchThreadLimit).
bool dispatchThreadLimit = false;
bool operator==(const ShaderInfo& other) const = default;

Some files were not shown because too many files have changed in this diff Show More