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