mirror of
https://github.com/boykopovar/AnyPS5.git
synced 2026-10-18 23:07:07 -05:00
Merge pull request #6215 from Ondra97/codex/fix-msaa-retired-target-lifetime
fix(agc): reclaim retired multisample color targets
This commit is contained in:
9 files changed
+666
-26
No files matched your search
@@ -56,6 +56,8 @@ bool MultisampledFillClearColor(const ColorTarget& target, std::uint32_t fill, V
|
||||
bool TakeMultisampledClear(MultisampledCmask& cmask, MultisampledColorFill& colorFill, const ColorTarget& use, VkClearColorValue& clear);
|
||||
std::shared_ptr<Texture> MultisampleTargetTexture(const Context& context, const GuestTextureResource& resource, VkComponentMapping components, VkImageViewType viewType = VK_IMAGE_VIEW_TYPE_2D);
|
||||
VkImageView MultisampleTargetView(const Context& context, const ColorTarget& target, VkImage* image = nullptr);
|
||||
VkImageView MultisampleTargetView(const Context& context, const ColorTarget& target, std::shared_ptr<void>& owner, VkImage* image = nullptr);
|
||||
void KeepMultisampleTarget_nid_no_patch(const std::shared_ptr<void>& owner);
|
||||
void MaterializeMultisampleTargetClear(const Context& context, const ColorTarget& target);
|
||||
VkImageView MultisampleTargetStorageView(const Context& context, const GuestTextureResource& resource);
|
||||
bool MultisampleTargetStorable(const Context& context, const ColorTarget& target);
|
||||
|
||||
@@ -47,7 +47,7 @@ public:
|
||||
Pipeline(const Pipeline&) = delete;
|
||||
Pipeline& operator=(const Pipeline&) = delete;
|
||||
VkPipelineLayout Layout() const;
|
||||
std::shared_ptr<Framebuffer> AcquireFramebuffer(std::span<const VkImageView> targets, std::span<const std::shared_ptr<StorageTexture>> owners, VkExtent2D extent, std::uint32_t layers = 1, std::span<const std::uint8_t> persistent = {});
|
||||
std::shared_ptr<Framebuffer> AcquireFramebuffer(std::span<const VkImageView> targets, std::span<const std::shared_ptr<void>> owners, VkExtent2D extent, std::uint32_t layers = 1);
|
||||
// Begins the render pass on the framebuffer, binds the pipeline and sets its dynamic state.
|
||||
void Begin(VkCommandBuffer commands, const Framebuffer& framebuffer, VkExtent2D extent, const State& state) const;
|
||||
// The same inside a render pass another pipeline of the same attachments began (compatible by
|
||||
@@ -65,7 +65,7 @@ public:
|
||||
private:
|
||||
struct CachedFramebuffer {
|
||||
std::vector<VkImageView> views;
|
||||
std::vector<std::weak_ptr<StorageTexture>> owners;
|
||||
std::vector<std::weak_ptr<void>> owners;
|
||||
std::vector<bool> owned;
|
||||
VkExtent2D extent;
|
||||
std::uint32_t layers = 1;
|
||||
|
||||
@@ -1601,6 +1601,7 @@ struct RecordedDraw {
|
||||
std::span<const VkImageView> targetViews;
|
||||
std::vector<std::shared_ptr<StorageTexture>> targets;
|
||||
std::vector<std::uint32_t> targetSlices;
|
||||
std::vector<std::shared_ptr<void>> multisampleTargets;
|
||||
std::vector<std::shared_ptr<StorageTexture>> proxies;
|
||||
std::vector<VkImage> multisampleImages;
|
||||
const IndirectRecord* indirect = nullptr;
|
||||
@@ -1815,6 +1816,7 @@ void recordDraw(const Context& context, const State& state, const Pm4::DrawParam
|
||||
const PassAccess passAccess{passReads, resources.GpuWrites(), passImages, passAttachments, unknownMemory, unknownWrites};
|
||||
const bool fenced = state.depth.has_value() || !record.proxies.empty();
|
||||
const auto start = recorder->StartDrawPass(passKey, capture || readsTarget || gpuIndirect || meshIndirect, fenced, passAccess);
|
||||
for (const auto& target : record.multisampleTargets) KeepMultisampleTarget_nid_no_patch(target);
|
||||
const bool continued = start.continued;
|
||||
outcome.passContinued = continued;
|
||||
outcome.passBegun = !continued;
|
||||
@@ -2030,6 +2032,7 @@ void Draw(const Context& context, const State& state, const Pm4::DrawParameters&
|
||||
std::unique_ptr<RenderTarget> target;
|
||||
bool multisampled = false;
|
||||
VkImage multisampleImage = VK_NULL_HANDLE;
|
||||
std::shared_ptr<void> multisampleOwner;
|
||||
// APS5_DUMP_TARGETS: the rendered linear pixels, read back for target_<address>_<n>.raw.
|
||||
std::unique_ptr<Buffer> dump;
|
||||
};
|
||||
@@ -2046,7 +2049,7 @@ void Draw(const Context& context, const State& state, const Pm4::DrawParameters&
|
||||
if (color.samples > 1) {
|
||||
binding.multisampled = true;
|
||||
timer.phase(PhaseSetup);
|
||||
targetViews.push_back(MultisampleTargetView(context, color, &binding.multisampleImage));
|
||||
targetViews.push_back(MultisampleTargetView(context, color, binding.multisampleOwner, &binding.multisampleImage));
|
||||
timer.phase(PhaseReadTarget);
|
||||
continue;
|
||||
}
|
||||
@@ -2306,13 +2309,13 @@ void Draw(const Context& context, const State& state, const Pm4::DrawParameters&
|
||||
// reused with the pipeline; a per-draw RenderTarget gets a framebuffer of its own.
|
||||
std::vector<std::shared_ptr<StorageTexture>> owners;
|
||||
owners.reserve(targets.size());
|
||||
std::vector<std::uint8_t> persistent;
|
||||
persistent.reserve(targets.size());
|
||||
std::vector<std::shared_ptr<void>> framebufferOwners;
|
||||
framebufferOwners.reserve(targets.size());
|
||||
for (const auto& binding : targets) {
|
||||
owners.push_back(binding.resident);
|
||||
persistent.push_back(binding.multisampled ? 1u : 0u);
|
||||
framebufferOwners.push_back(binding.multisampled ? binding.multisampleOwner : binding.resident);
|
||||
}
|
||||
auto framebuffer = pipeline->AcquireFramebuffer(targetViews, owners, state.renderExtent, state.renderLayers, persistent);
|
||||
auto framebuffer = pipeline->AcquireFramebuffer(targetViews, framebufferOwners, state.renderExtent, state.renderLayers);
|
||||
timer.phase(PhasePipeline);
|
||||
APS5_LOG_CHARS_OUT_DEBUG("Pipeline created");
|
||||
if (lean) {
|
||||
@@ -2327,7 +2330,10 @@ void Draw(const Context& context, const State& state, const Pm4::DrawParameters&
|
||||
for (const auto& binding : targets) record.targetSlices.push_back(binding.color.arraySlice);
|
||||
for (const auto& binding : targets) {
|
||||
if (binding.proxied) record.proxies.push_back(binding.resident);
|
||||
if (binding.multisampled) record.multisampleImages.push_back(binding.multisampleImage);
|
||||
if (binding.multisampled) {
|
||||
record.multisampleImages.push_back(binding.multisampleImage);
|
||||
record.multisampleTargets.push_back(binding.multisampleOwner);
|
||||
}
|
||||
}
|
||||
record.indirect = args != nullptr ? &indirect : nullptr;
|
||||
record.listed = listed;
|
||||
@@ -2710,7 +2716,10 @@ DrawRecipeOutcome DrawWithRecipe(const Context& context, const State& state, con
|
||||
auto pipeline = recipe.pipeline.lock();
|
||||
if (pipeline == nullptr) return miss(DrawRecipeMiss::ObjectsGone);
|
||||
auto framebuffer = recipe.framebuffer.lock();
|
||||
if (framebuffer == nullptr) framebuffer = pipeline->AcquireFramebuffer(recipe.targetViews, targets, state.renderExtent, state.renderLayers);
|
||||
if (framebuffer == nullptr) {
|
||||
const std::vector<std::shared_ptr<void>> framebufferOwners(targets.begin(), targets.end());
|
||||
framebuffer = pipeline->AcquireFramebuffer(recipe.targetViews, framebufferOwners, state.renderExtent, state.renderLayers);
|
||||
}
|
||||
timer.phase(PhasePipeline);
|
||||
const auto recordStart = profile ? std::chrono::steady_clock::now() : std::chrono::steady_clock::time_point{};
|
||||
RecordedDraw record;
|
||||
|
||||
@@ -53,13 +53,14 @@ VkClearColorValue fastClearColor(const ColorTarget& target) {
|
||||
template<typename Record>
|
||||
void recordCommands(const Context& context, Record&& record) {
|
||||
auto* recorder = Recorder::Active();
|
||||
if (recorder != nullptr && recorder->Device() != context.device) recorder = nullptr;
|
||||
std::unique_ptr<CommandBatch> batch;
|
||||
if (recorder == nullptr) batch = std::make_unique<CommandBatch>(context);
|
||||
record(recorder != nullptr ? recorder->Commands() : batch->Handle(), recorder);
|
||||
if (batch) batch->SubmitAndWait();
|
||||
}
|
||||
|
||||
class MultisampleTarget {
|
||||
class MultisampleTarget : public std::enable_shared_from_this<MultisampleTarget> {
|
||||
public:
|
||||
MultisampleTarget(const Context& context, const ColorTarget& target) : context(context), target(target), cmask(target.cmaskAddress, target.cmaskBytes), colorFill(target) {
|
||||
this->context.bufferPool.reset();
|
||||
@@ -88,6 +89,7 @@ public:
|
||||
image = created.image;
|
||||
memory = created.memory;
|
||||
view = created.view;
|
||||
allocationBytes = created.bytes;
|
||||
recordCommands(context, [&](VkCommandBuffer commands, Recorder* recorder) {
|
||||
recordToGeneral(commands, image);
|
||||
const VkImageSubresourceRange range{VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1};
|
||||
@@ -108,6 +110,7 @@ public:
|
||||
VkImage image = VK_NULL_HANDLE;
|
||||
VkDeviceMemory memory = VK_NULL_HANDLE;
|
||||
VkImageView view = VK_NULL_HANDLE;
|
||||
VkDeviceSize bytes = 0;
|
||||
};
|
||||
|
||||
Allocation allocate(VkImageUsageFlags imageUsage, VkImageCreateFlags flags) {
|
||||
@@ -130,6 +133,7 @@ public:
|
||||
context.Function<PFN_vkGetImageMemoryRequirements>("vkGetImageMemoryRequirements")(context.device, created.image, &requirements);
|
||||
VkMemoryAllocateInfo allocation{VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO};
|
||||
allocation.allocationSize = requirements.size;
|
||||
created.bytes = requirements.size;
|
||||
allocation.memoryTypeIndex = context.MemoryType(requirements.memoryTypeBits, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT);
|
||||
created.memory = AllocateImageMemory(context, allocation, requirements.memoryTypeBits, "vkAllocateMemory multisampled target");
|
||||
Check(context.Function<PFN_vkBindImageMemory>("vkBindImageMemory")(context.device, created.image, created.memory, 0), "vkBindImageMemory multisampled target");
|
||||
@@ -177,19 +181,29 @@ public:
|
||||
context.Function<PFN_vkCmdCopyImage>("vkCmdCopyImage")(recorder.Commands(), source, VK_IMAGE_LAYOUT_GENERAL, storable.image, VK_IMAGE_LAYOUT_GENERAL, 1, ®ion);
|
||||
RecordMemoryBarrier(context, recorder.Commands(), VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_COLOR_ATTACHMENT_READ_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT | VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_TRANSFER_WRITE_BIT);
|
||||
Recorder::CountBarriers(Recorder::CommandClass::Draw, 3);
|
||||
for (const auto& [key, texture] : textures) recorder.Keep(texture);
|
||||
for (auto& [key, texture] : textures) retiredTextures.push_back(std::move(texture));
|
||||
textures.clear();
|
||||
retired.push_back({image, memory, view});
|
||||
image = storable.image;
|
||||
memory = storable.memory;
|
||||
view = storable.view;
|
||||
usage |= VK_IMAGE_USAGE_STORAGE_BIT;
|
||||
allocationBytes += storable.bytes;
|
||||
recorder.Keep(shared_from_this(), static_cast<std::size_t>(storable.bytes));
|
||||
}
|
||||
|
||||
void Keep() {
|
||||
if (auto* recorder = Recorder::Active(); recorder != nullptr && recorder->Device() == context.device) {
|
||||
recorder->Keep(shared_from_this());
|
||||
recorder->KeepBytes(this, allocationBytes);
|
||||
}
|
||||
}
|
||||
|
||||
void ApplyFastClear(const ColorTarget& use) {
|
||||
checkDccCurrent(use);
|
||||
VkClearColorValue clear{};
|
||||
if (!TakeMultisampledClear(cmask, colorFill, use, clear)) return;
|
||||
Keep();
|
||||
recordCommands(context, [&](VkCommandBuffer commands, Recorder* recorder) {
|
||||
RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_COLOR_ATTACHMENT_READ_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT, VK_ACCESS_TRANSFER_WRITE_BIT);
|
||||
const VkImageSubresourceRange range{VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1};
|
||||
@@ -215,7 +229,8 @@ public:
|
||||
Require(htileRefusal.empty(), htileRefusal);
|
||||
Require(cmask.Keys() != DccKeys::Clear0000, "a storage image access to a multisampled color target whose CMASK fast clear no draw has applied is not implemented");
|
||||
auto* recording = Recorder::Active();
|
||||
Require(recording != nullptr, "a multisampled color target is only written by recorded dispatches");
|
||||
Require(recording != nullptr && recording->Device() == context.device, "a multisampled color target is only written by recorded dispatches on its device");
|
||||
Keep();
|
||||
makeStorable(*recording);
|
||||
ApplyFastClear(target);
|
||||
if (storageView == VK_NULL_HANDLE) {
|
||||
@@ -243,7 +258,9 @@ public:
|
||||
Require(cmask.Refusal().empty(), cmask.Refusal());
|
||||
Require(htileRefusal.empty(), htileRefusal);
|
||||
Require(cmask.Keys() != DccKeys::Clear0000, "sampling a multisampled color target whose CMASK fast clear no draw has applied is not implemented");
|
||||
Require(Recorder::Active() != nullptr, "a multisampled color target is only sampled by recorded draws and dispatches");
|
||||
auto* recording = Recorder::Active();
|
||||
Require(recording != nullptr && recording->Device() == context.device, "a multisampled color target is only sampled by recorded draws and dispatches on its device");
|
||||
Keep();
|
||||
SettleStorageWrites();
|
||||
ApplyFastClear(target);
|
||||
recordCommands(context, [&](VkCommandBuffer commands, Recorder*) {
|
||||
@@ -251,10 +268,9 @@ public:
|
||||
Recorder::CountBarriers(Recorder::CommandClass::Draw, 1);
|
||||
});
|
||||
const std::array<std::uint32_t, 5> key{static_cast<std::uint32_t>(components.r), static_cast<std::uint32_t>(components.g), static_cast<std::uint32_t>(components.b), static_cast<std::uint32_t>(components.a), static_cast<std::uint32_t>(viewType)};
|
||||
if (const auto found = textures.find(key); found != textures.end()) return found->second;
|
||||
auto texture = std::make_shared<Texture>(context, image, target.format, VK_IMAGE_ASPECT_COLOR_BIT, components, viewType);
|
||||
textures.emplace(key, texture);
|
||||
return texture;
|
||||
auto& texture = textures[key];
|
||||
if (texture == nullptr) texture = std::make_shared<Texture>(context, image, target.format, VK_IMAGE_ASPECT_COLOR_BIT, components, viewType);
|
||||
return {shared_from_this(), texture.get()};
|
||||
}
|
||||
|
||||
VkRenderPass ResolvePass() {
|
||||
@@ -313,7 +329,9 @@ private:
|
||||
std::vector<Allocation> retired;
|
||||
VkImageView storageView = VK_NULL_HANDLE;
|
||||
bool storageWritten = false;
|
||||
VkDeviceSize allocationBytes = 0;
|
||||
std::map<std::array<std::uint32_t, 5>, std::shared_ptr<Texture>> textures;
|
||||
std::vector<std::shared_ptr<Texture>> retiredTextures;
|
||||
std::vector<GuestMemory::UnchangedQuery> dccRanges;
|
||||
|
||||
VkClearColorValue initialDccClear(const ColorTarget& use) {
|
||||
@@ -366,6 +384,7 @@ private:
|
||||
|
||||
void release() noexcept {
|
||||
textures.clear();
|
||||
retiredTextures.clear();
|
||||
if (resolvePass) context.Function<PFN_vkDestroyRenderPass>("vkDestroyRenderPass")(context.device, resolvePass, nullptr);
|
||||
if (storageView) context.Function<PFN_vkDestroyImageView>("vkDestroyImageView")(context.device, storageView, nullptr);
|
||||
destroy({image, memory, view});
|
||||
@@ -448,12 +467,17 @@ std::mutex& targetsMutex() {
|
||||
return mutex;
|
||||
}
|
||||
|
||||
std::vector<std::unique_ptr<MultisampleTarget>>& targets() {
|
||||
static std::vector<std::unique_ptr<MultisampleTarget>> list;
|
||||
std::vector<std::shared_ptr<MultisampleTarget>>& targets() {
|
||||
static std::vector<std::shared_ptr<MultisampleTarget>> list;
|
||||
return list;
|
||||
}
|
||||
|
||||
void discardRetiredTargets() {
|
||||
std::erase_if(targets(), [](const auto& target) { return !target->active->load(std::memory_order_acquire); });
|
||||
}
|
||||
|
||||
MultisampleTarget& acquire(const Context& context, const ColorTarget& use) {
|
||||
discardRetiredTargets();
|
||||
for (const auto& target : targets()) {
|
||||
if (!target->active->load(std::memory_order_acquire)) continue;
|
||||
if (target->context.device != context.device || !sameTarget(target->target, use)) continue;
|
||||
@@ -462,15 +486,19 @@ MultisampleTarget& acquire(const Context& context, const ColorTarget& use) {
|
||||
target->cmask = MultisampledCmask(use.cmaskAddress, use.cmaskBytes);
|
||||
}
|
||||
target->cmask.CheckAddress(use.cmaskAddress);
|
||||
target->Keep();
|
||||
target->SettleStorageWrites();
|
||||
target->ApplyFastClear(use);
|
||||
return *target;
|
||||
}
|
||||
targets().push_back(std::make_unique<MultisampleTarget>(context, use));
|
||||
auto target = std::make_shared<MultisampleTarget>(context, use);
|
||||
target->Keep();
|
||||
targets().push_back(std::move(target));
|
||||
auto& created = *targets().back();
|
||||
guard(context, use, created.active, created.metadataValid);
|
||||
if (use.cmaskFastClear && use.dccAddress == 0) created.cmask.SeedKeys(CurrentDccKeys(use.cmaskAddress, static_cast<std::uint64_t>(use.cmaskBytes) * 256u));
|
||||
created.ApplyFastClear(use);
|
||||
created.Keep();
|
||||
return created;
|
||||
}
|
||||
|
||||
@@ -537,7 +565,9 @@ MultisampleTarget& createFilled(const Context& context, const GuestTextureResour
|
||||
}
|
||||
VkClearColorValue clear{};
|
||||
if (!MultisampledFillClearColor(use, value, clear)) refuse(std::string("whose color memory ") + range + " holds " + held + ", which has no exact clear value in its format");
|
||||
targets().push_back(std::make_unique<MultisampleTarget>(context, use));
|
||||
auto target = std::make_shared<MultisampleTarget>(context, use);
|
||||
target->Keep();
|
||||
targets().push_back(std::move(target));
|
||||
auto& created = *targets().back();
|
||||
guard(context, use, created.active, created.metadataValid);
|
||||
created.colorFill.NoteFill(use.address, static_cast<std::size_t>(bytes), value);
|
||||
@@ -716,6 +746,18 @@ VkImageView MultisampleTargetView(const Context& context, const ColorTarget& tar
|
||||
return acquired.view;
|
||||
}
|
||||
|
||||
VkImageView MultisampleTargetView(const Context& context, const ColorTarget& target, std::shared_ptr<void>& owner, VkImage* image) {
|
||||
std::lock_guard lock(targetsMutex());
|
||||
auto& acquired = acquire(context, target);
|
||||
owner = acquired.shared_from_this();
|
||||
if (image != nullptr) *image = acquired.image;
|
||||
return acquired.view;
|
||||
}
|
||||
|
||||
void KeepMultisampleTarget_nid_no_patch(const std::shared_ptr<void>& owner) {
|
||||
std::static_pointer_cast<MultisampleTarget>(owner)->Keep();
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
bool redBlueSwapped(VkFormat rendered, VkFormat sampled) {
|
||||
@@ -737,6 +779,7 @@ VkComponentMapping swapRedBlue(VkComponentMapping components) {
|
||||
|
||||
std::shared_ptr<Texture> MultisampleTargetTexture(const Context& context, const GuestTextureResource& resource, VkComponentMapping components, VkImageViewType viewType) {
|
||||
std::lock_guard lock(targetsMutex());
|
||||
discardRetiredTargets();
|
||||
const auto format = ResolveTextureFormat(resource.format);
|
||||
const MultisampleTarget* other = nullptr;
|
||||
for (const auto& target : targets()) {
|
||||
@@ -764,6 +807,7 @@ void MaterializeMultisampleTargetClear(const Context& context, const ColorTarget
|
||||
|
||||
VkImageView MultisampleTargetStorageView(const Context& context, const GuestTextureResource& resource) {
|
||||
std::lock_guard lock(targetsMutex());
|
||||
discardRetiredTargets();
|
||||
const auto format = ResolveTextureFormat(resource.format);
|
||||
const MultisampleTarget* other = nullptr;
|
||||
for (const auto& target : targets()) {
|
||||
@@ -792,6 +836,7 @@ bool MultisampleTargetStorable(const Context& context, const ColorTarget& target
|
||||
void ResolveMultisampleTarget(const Context& context, const ColorTarget& source, StorageTexture& destination, VkImageView destinationView) {
|
||||
std::lock_guard lock(targetsMutex());
|
||||
auto& target = acquire(context, source);
|
||||
target.Keep();
|
||||
const std::array<VkImageView, 2> views{target.view, destinationView};
|
||||
auto framebuffer = std::make_shared<Framebuffer>(context, target.ResolvePass(), views, source.extent);
|
||||
recordCommands(context, [&](VkCommandBuffer commands, Recorder* recorder) {
|
||||
|
||||
@@ -630,10 +630,10 @@ VkPipelineLayout Pipeline::Layout() const {
|
||||
return layout;
|
||||
}
|
||||
|
||||
std::shared_ptr<Framebuffer> Pipeline::AcquireFramebuffer(std::span<const VkImageView> targets, std::span<const std::shared_ptr<StorageTexture>> owners, VkExtent2D extent, std::uint32_t layers, std::span<const std::uint8_t> persistent) {
|
||||
std::shared_ptr<Framebuffer> Pipeline::AcquireFramebuffer(std::span<const VkImageView> targets, std::span<const std::shared_ptr<void>> owners, VkExtent2D extent, std::uint32_t layers) {
|
||||
Require(targets.size() == attachments && owners.size() == colorAttachments, "render targets do not match the pipeline's attachments");
|
||||
bool resident = true;
|
||||
for (std::size_t i = 0; i < owners.size(); ++i) resident = resident && (owners[i] != nullptr || (i < persistent.size() && persistent[i] != 0));
|
||||
for (const auto& owner : owners) resident = resident && owner != nullptr;
|
||||
if (resident) {
|
||||
// Entries whose views are gone can never match again and go as soon as no recorded draw holds
|
||||
// them (Kept keeps its framebuffer until the batch completes).
|
||||
|
||||
@@ -6299,6 +6299,67 @@ void pipelineLibraryClearWaitsForBuildTests() {
|
||||
Require(mock.live == 0, "clearing during a library build leaked Vulkan objects");
|
||||
}
|
||||
|
||||
void framebufferOwnerTests() {
|
||||
using namespace AgcDriver::Graphics;
|
||||
mock = MockVulkan{};
|
||||
auto context = mockContext();
|
||||
context.graphicsPipelineLibrary = true;
|
||||
context.pipelineLinkTimeOptimization = false;
|
||||
context.limits.maxColorAttachments = 1;
|
||||
context.limits.maxFramebufferWidth = 4;
|
||||
context.limits.maxFramebufferHeight = 4;
|
||||
context.limits.maxFramebufferLayers = 1;
|
||||
context.deviceProc = +[](VkDevice device, const char* name) -> PFN_vkVoidFunction {
|
||||
if (std::strcmp(name, "vkCreateGraphicsPipelines") == 0) {
|
||||
return reinterpret_cast<PFN_vkVoidFunction>(+[](VkDevice, VkPipelineCache, std::uint32_t count, const VkGraphicsPipelineCreateInfo*, const VkAllocationCallbacks*, VkPipeline* pipeline) -> VkResult {
|
||||
Require(count == 1, "framebuffer owner test expects one graphics pipeline");
|
||||
*pipeline = makeHandle<VkPipeline>();
|
||||
++mock.live;
|
||||
return VK_SUCCESS;
|
||||
});
|
||||
}
|
||||
return mockProc(device, name);
|
||||
};
|
||||
{
|
||||
State state{};
|
||||
state.hasColorTarget = true;
|
||||
state.samples = 2;
|
||||
state.colors.resize(1);
|
||||
state.colors[0].format = VK_FORMAT_R8G8B8A8_UNORM;
|
||||
state.colors[0].samples = 2;
|
||||
state.blends.resize(1);
|
||||
ShaderRecompiler::RecompileResult vertex, fragment;
|
||||
vertex.spirv = makeModule({});
|
||||
fragment.spirv = makeModule({.fragment = true});
|
||||
const std::array<CompiledShader, 2> shaders{{{ShaderRecompiler::ShaderStage::Vertex, &vertex, 0}, {ShaderRecompiler::ShaderStage::Fragment, &fragment, PipelinePushSlotBytes}}};
|
||||
Pipeline pipeline(context, state, VertexInputLayout{}, PipelineSetLayouts{}, shaders);
|
||||
const std::array<VkImageView, 1> views{makeHandle<VkImageView>()};
|
||||
std::array<std::shared_ptr<void>, 1> owners{std::make_shared<int>(1)};
|
||||
const std::weak_ptr<void> firstOwner = owners[0];
|
||||
auto first = pipeline.AcquireFramebuffer(views, owners, {4, 4});
|
||||
auto repeated = pipeline.AcquireFramebuffer(views, owners, {4, 4});
|
||||
Require(first == repeated, "a live framebuffer owner did not reuse its framebuffer");
|
||||
auto retainedOwner = owners[0];
|
||||
owners[0] = std::make_shared<int>(2);
|
||||
auto distinct = pipeline.AcquireFramebuffer(views, owners, {4, 4});
|
||||
Require(distinct != first, "identical image-view handles reused another live owner's framebuffer");
|
||||
retainedOwner.reset();
|
||||
Require(firstOwner.expired(), "the framebuffer cache retained its image owner");
|
||||
const std::weak_ptr<Framebuffer> retired = first;
|
||||
auto recycled = pipeline.AcquireFramebuffer(views, owners, {4, 4});
|
||||
Require(recycled == distinct && recycled != first, "an expired owner matched a recycled image-view handle");
|
||||
first.reset();
|
||||
repeated.reset();
|
||||
recycled = pipeline.AcquireFramebuffer(views, owners, {4, 4});
|
||||
Require(retired.expired(), "the framebuffer cache retained an unused framebuffer after its owner expired");
|
||||
const std::weak_ptr<void> lastOwner = owners[0];
|
||||
owners[0].reset();
|
||||
Require(lastOwner.expired(), "a cached framebuffer kept the final image owner alive");
|
||||
}
|
||||
ClearPipelineLibraries(context.device);
|
||||
Require(mock.live == 0, "framebuffer owner tests leaked Vulkan objects");
|
||||
}
|
||||
|
||||
void barycentricEmulationTests() {
|
||||
using namespace ShaderRecompiler;
|
||||
using namespace AgcDriver::Graphics;
|
||||
@@ -9232,6 +9293,7 @@ int main(int argc, char** argv) {
|
||||
retainedPixelStageTests();
|
||||
pipelineLibrarySetTests();
|
||||
pipelineLibraryClearWaitsForBuildTests();
|
||||
framebufferOwnerTests();
|
||||
hdrComponentSwapTests();
|
||||
depthMaintenanceTests();
|
||||
hardwareScreenOffsetTests();
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp"
|
||||
#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp"
|
||||
#include "prx/libSceAgcDriver/Graphics/include/MultisampleTarget.hpp"
|
||||
#include "prx/libSceAgcDriver/Graphics/include/Recorder.hpp"
|
||||
#include "prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp"
|
||||
#include "prx/libSceAgcDriver/Graphics/include/State.hpp"
|
||||
#include "prx/libSceAgcDriver/Submit/include/Dcb.hpp"
|
||||
@@ -23,6 +24,7 @@
|
||||
#include <deque>
|
||||
#include <initializer_list>
|
||||
#include <iostream>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <span>
|
||||
#include <stdexcept>
|
||||
@@ -154,6 +156,10 @@ bool FillsTakenAsMultisampledClearsAreNotStored(AgcDriver::VulkanDevice& device)
|
||||
VkDevice handle;
|
||||
~ClearTargets() {
|
||||
std::lock_guard lock(AgcDriver::GuestMemory::GpuMutex());
|
||||
if (auto* active = AgcDriver::Graphics::Recorder::Active(); active != nullptr && active->Device() != handle) {
|
||||
active->Sync();
|
||||
active->FlushDeferredReleases();
|
||||
}
|
||||
device.SubmitRecorded(true);
|
||||
device.WaitIdle();
|
||||
AgcDriver::Graphics::ClearMultisampleTargets(handle);
|
||||
@@ -179,7 +185,12 @@ bool FillsTakenAsMultisampledClearsAreNotStored(AgcDriver::VulkanDevice& device)
|
||||
Require(AgcDriver::Graphics::MultisampledColorBytes(target) == BlockBytes, "the 4x 64x64 RGBA8 target does not take one 64 KiB block");
|
||||
{
|
||||
std::lock_guard lock(AgcDriver::GuestMemory::GpuMutex());
|
||||
Require(AgcDriver::Graphics::MultisampleTargetView(context, target) != VK_NULL_HANDLE, "the multisampled target has no view");
|
||||
auto* active = AgcDriver::Graphics::Recorder::Active();
|
||||
Require(active != nullptr && active->Device() != context.device, "the fill test requires a second device's active recorder");
|
||||
const auto keptBytes = active->OpenKeptBytes();
|
||||
std::shared_ptr<void> owner;
|
||||
Require(AgcDriver::Graphics::MultisampleTargetView(context, target, owner) != VK_NULL_HANDLE && owner != nullptr, "the multisampled target has no view");
|
||||
Require(active->OpenKeptBytes() == keptBytes, "the multisampled target was charged to another device's recorder");
|
||||
device.SubmitRecorded(true);
|
||||
device.WaitIdle();
|
||||
}
|
||||
@@ -205,7 +216,11 @@ bool FillsTakenAsMultisampledClearsAreNotStored(AgcDriver::VulkanDevice& device)
|
||||
srgb.format = VK_FORMAT_R8G8B8A8_SRGB;
|
||||
{
|
||||
std::lock_guard lock(AgcDriver::GuestMemory::GpuMutex());
|
||||
auto* active = AgcDriver::Graphics::Recorder::Active();
|
||||
Require(active != nullptr && active->Device() != context.device, "the sRGB fill test requires a second device's active recorder");
|
||||
const auto keptBytes = active->OpenKeptBytes();
|
||||
Require(AgcDriver::Graphics::MultisampleTargetView(context, srgb) != VK_NULL_HANDLE, "the sRGB multisampled target has no view");
|
||||
Require(active->OpenKeptBytes() == keptBytes, "the sRGB multisampled target was charged to another device's recorder");
|
||||
device.SubmitRecorded(true);
|
||||
device.WaitIdle();
|
||||
}
|
||||
@@ -226,6 +241,17 @@ bool FillsTakenAsMultisampledClearsAreNotStored(AgcDriver::VulkanDevice& device)
|
||||
DispatchFill(code, color.Address(), BlockBytes, 0);
|
||||
Settle(code, sentinel);
|
||||
Require(color.Holds(std::byte{}, 0, BlockBytes), "a compute fill over a retired target's color memory was not stored");
|
||||
{
|
||||
std::lock_guard lock(AgcDriver::GuestMemory::GpuMutex());
|
||||
std::shared_ptr<void> owner;
|
||||
Require(AgcDriver::Graphics::MultisampleTargetView(context, target, owner) != VK_NULL_HANDLE && owner != nullptr, "the replacement multisampled target has no view");
|
||||
const std::weak_ptr<void> targetOwner = owner;
|
||||
owner.reset();
|
||||
device.SubmitRecorded(true);
|
||||
device.WaitIdle();
|
||||
AgcDriver::Graphics::ClearMultisampleTargets(context.device);
|
||||
Require(targetOwner.expired(), "another device's recorder retained a multisampled target after its device's work completed");
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,496 @@
|
||||
#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp"
|
||||
#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp"
|
||||
#include "prx/libSceAgcDriver/Graphics/include/ColorTargetLayout.hpp"
|
||||
#include "prx/libSceAgcDriver/Graphics/include/MultisampleTarget.hpp"
|
||||
#include "prx/libSceAgcDriver/Graphics/include/Recorder.hpp"
|
||||
#include "prx/libSceAgcDriver/Graphics/include/Texture.hpp"
|
||||
#include "prx/libc/include/GuestAllocations.hpp"
|
||||
#include "VulkanTestDevice.hpp"
|
||||
#ifdef _WIN32
|
||||
#ifndef NOMINMAX
|
||||
#define NOMINMAX
|
||||
#endif
|
||||
#include <windows.h>
|
||||
#else
|
||||
#include <sys/mman.h>
|
||||
#endif
|
||||
#include <chrono>
|
||||
#include <condition_variable>
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <iostream>
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace AgcDriver {
|
||||
|
||||
struct VulkanDeviceTestAccess {
|
||||
static Graphics::Context ContextOf(const VulkanDevice& device) { return device.graphicsContext(); }
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
using namespace AgcDriver::Graphics;
|
||||
|
||||
constexpr std::size_t BlockBytes = 1u << 20u;
|
||||
constexpr std::uint32_t Side = 256;
|
||||
|
||||
struct Image {
|
||||
std::uint64_t generation;
|
||||
VkDeviceMemory memory;
|
||||
};
|
||||
|
||||
std::mutex countsMutex;
|
||||
std::condition_variable countsChanged;
|
||||
std::map<VkImage, Image> images;
|
||||
std::map<VkImageView, VkImage> views;
|
||||
std::map<VkDeviceMemory, VkDeviceSize> allocations;
|
||||
std::vector<std::pair<VkImage, std::uint64_t>> pendingImages;
|
||||
std::uint64_t created = 0, destroyed = 0;
|
||||
PFN_vkGetDeviceProcAddr realProc = nullptr;
|
||||
PFN_vkAllocateMemory realAllocate = nullptr;
|
||||
PFN_vkFreeMemory realFree = nullptr;
|
||||
PFN_vkCreateImage realCreateImage = nullptr;
|
||||
PFN_vkDestroyImage realDestroyImage = nullptr;
|
||||
PFN_vkBindImageMemory realBindImageMemory = nullptr;
|
||||
PFN_vkCreateImageView realCreateImageView = nullptr;
|
||||
PFN_vkDestroyImageView realDestroyImageView = nullptr;
|
||||
PFN_vkCmdClearColorImage realClearColorImage = nullptr;
|
||||
PFN_vkCmdCopyImage realCopyImage = nullptr;
|
||||
|
||||
[[noreturn]] void FailPendingImage() {
|
||||
std::fputs("an unsubmitted MSAA image would be destroyed; stopping without submitting it\n", stderr);
|
||||
std::fflush(stderr);
|
||||
#ifdef _WIN32
|
||||
TerminateProcess(GetCurrentProcess(), 1);
|
||||
#endif
|
||||
std::_Exit(1);
|
||||
}
|
||||
|
||||
VKAPI_ATTR VkResult VKAPI_CALL CountAllocate(VkDevice device, const VkMemoryAllocateInfo* info, const VkAllocationCallbacks* callbacks, VkDeviceMemory* memory) {
|
||||
std::lock_guard lock(countsMutex);
|
||||
const auto result = realAllocate(device, info, callbacks, memory);
|
||||
if (result == VK_SUCCESS) allocations.emplace(*memory, info->allocationSize);
|
||||
return result;
|
||||
}
|
||||
|
||||
VKAPI_ATTR void VKAPI_CALL CountFree(VkDevice device, VkDeviceMemory memory, const VkAllocationCallbacks* callbacks) {
|
||||
{
|
||||
std::lock_guard lock(countsMutex);
|
||||
realFree(device, memory, callbacks);
|
||||
allocations.erase(memory);
|
||||
}
|
||||
countsChanged.notify_all();
|
||||
}
|
||||
|
||||
VKAPI_ATTR VkResult VKAPI_CALL CountCreateImage(VkDevice device, const VkImageCreateInfo* info, const VkAllocationCallbacks* callbacks, VkImage* image) {
|
||||
std::lock_guard lock(countsMutex);
|
||||
const auto result = realCreateImage(device, info, callbacks, image);
|
||||
if (result == VK_SUCCESS) images.emplace(*image, Image{++created, VK_NULL_HANDLE});
|
||||
return result;
|
||||
}
|
||||
|
||||
VKAPI_ATTR void VKAPI_CALL CountDestroyImage(VkDevice device, VkImage image, const VkAllocationCallbacks* callbacks) {
|
||||
{
|
||||
std::lock_guard lock(countsMutex);
|
||||
for (const auto& [pending, generation] : pendingImages) {
|
||||
if (pending == image && images.at(image).generation == generation) FailPendingImage();
|
||||
}
|
||||
realDestroyImage(device, image, callbacks);
|
||||
images.erase(image);
|
||||
++destroyed;
|
||||
}
|
||||
countsChanged.notify_all();
|
||||
}
|
||||
|
||||
VKAPI_ATTR VkResult VKAPI_CALL CountBindImageMemory(VkDevice device, VkImage image, VkDeviceMemory memory, VkDeviceSize offset) {
|
||||
std::lock_guard lock(countsMutex);
|
||||
const auto result = realBindImageMemory(device, image, memory, offset);
|
||||
if (result == VK_SUCCESS) images.at(image).memory = memory;
|
||||
return result;
|
||||
}
|
||||
|
||||
VKAPI_ATTR VkResult VKAPI_CALL CountCreateImageView(VkDevice device, const VkImageViewCreateInfo* info, const VkAllocationCallbacks* callbacks, VkImageView* view) {
|
||||
std::lock_guard lock(countsMutex);
|
||||
const auto result = realCreateImageView(device, info, callbacks, view);
|
||||
if (result == VK_SUCCESS) views.emplace(*view, info->image);
|
||||
return result;
|
||||
}
|
||||
|
||||
VKAPI_ATTR void VKAPI_CALL CountDestroyImageView(VkDevice device, VkImageView view, const VkAllocationCallbacks* callbacks) {
|
||||
std::lock_guard lock(countsMutex);
|
||||
realDestroyImageView(device, view, callbacks);
|
||||
views.erase(view);
|
||||
}
|
||||
|
||||
VKAPI_ATTR void VKAPI_CALL CountClearColorImage(VkCommandBuffer commands, VkImage image, VkImageLayout layout, const VkClearColorValue* clear, std::uint32_t count, const VkImageSubresourceRange* ranges) {
|
||||
{
|
||||
std::lock_guard lock(countsMutex);
|
||||
pendingImages.emplace_back(image, images.at(image).generation);
|
||||
}
|
||||
realClearColorImage(commands, image, layout, clear, count, ranges);
|
||||
}
|
||||
|
||||
VKAPI_ATTR void VKAPI_CALL CountCopyImage(VkCommandBuffer commands, VkImage source, VkImageLayout sourceLayout, VkImage destination, VkImageLayout destinationLayout, std::uint32_t count, const VkImageCopy* regions) {
|
||||
{
|
||||
std::lock_guard lock(countsMutex);
|
||||
pendingImages.emplace_back(source, images.at(source).generation);
|
||||
pendingImages.emplace_back(destination, images.at(destination).generation);
|
||||
}
|
||||
realCopyImage(commands, source, sourceLayout, destination, destinationLayout, count, regions);
|
||||
}
|
||||
|
||||
VKAPI_ATTR PFN_vkVoidFunction VKAPI_CALL CountingDeviceProc(VkDevice device, const char* name) {
|
||||
const auto function = realProc(device, name);
|
||||
if (function == nullptr) return nullptr;
|
||||
if (std::strcmp(name, "vkAllocateMemory") == 0) return reinterpret_cast<PFN_vkVoidFunction>(CountAllocate);
|
||||
if (std::strcmp(name, "vkFreeMemory") == 0) return reinterpret_cast<PFN_vkVoidFunction>(CountFree);
|
||||
if (std::strcmp(name, "vkCreateImage") == 0) return reinterpret_cast<PFN_vkVoidFunction>(CountCreateImage);
|
||||
if (std::strcmp(name, "vkDestroyImage") == 0) return reinterpret_cast<PFN_vkVoidFunction>(CountDestroyImage);
|
||||
if (std::strcmp(name, "vkBindImageMemory") == 0) return reinterpret_cast<PFN_vkVoidFunction>(CountBindImageMemory);
|
||||
if (std::strcmp(name, "vkCreateImageView") == 0) return reinterpret_cast<PFN_vkVoidFunction>(CountCreateImageView);
|
||||
if (std::strcmp(name, "vkDestroyImageView") == 0) return reinterpret_cast<PFN_vkVoidFunction>(CountDestroyImageView);
|
||||
if (std::strcmp(name, "vkCmdClearColorImage") == 0) return reinterpret_cast<PFN_vkVoidFunction>(CountClearColorImage);
|
||||
if (std::strcmp(name, "vkCmdCopyImage") == 0) return reinterpret_cast<PFN_vkVoidFunction>(CountCopyImage);
|
||||
return function;
|
||||
}
|
||||
|
||||
void CheckPendingImages() {
|
||||
std::lock_guard lock(countsMutex);
|
||||
for (const auto& [image, generation] : pendingImages) {
|
||||
const auto found = images.find(image);
|
||||
if (found == images.end() || found->second.generation != generation) {
|
||||
FailPendingImage();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Image ImageFor(VkImageView view, VkImage* image = nullptr) {
|
||||
std::lock_guard lock(countsMutex);
|
||||
const auto handle = views.at(view);
|
||||
if (image) *image = handle;
|
||||
return images.at(handle);
|
||||
}
|
||||
|
||||
bool Alive(VkImage image, const Image& expected) {
|
||||
std::lock_guard lock(countsMutex);
|
||||
const auto found = images.find(image);
|
||||
return found != images.end() && found->second.generation == expected.generation && allocations.contains(expected.memory);
|
||||
}
|
||||
|
||||
VkDeviceSize AllocationBytesOf(const Image& image) {
|
||||
std::lock_guard lock(countsMutex);
|
||||
return allocations.at(image.memory);
|
||||
}
|
||||
|
||||
void AwaitImages(std::size_t limit, const std::string& what) {
|
||||
std::unique_lock lock(countsMutex);
|
||||
const bool released = countsChanged.wait_for(lock, std::chrono::seconds(2), [&] { return images.size() <= limit && allocations.size() <= limit; });
|
||||
Require(released, what + ": " + std::to_string(images.size()) + " images and " + std::to_string(allocations.size()) + " allocations remain");
|
||||
}
|
||||
|
||||
class GuestBlock {
|
||||
public:
|
||||
GuestBlock() {
|
||||
#ifdef _WIN32
|
||||
data = VirtualAlloc(nullptr, BlockBytes, MEM_RESERVE, PAGE_NOACCESS);
|
||||
Require(data != nullptr, "cannot reserve the MSAA guest block");
|
||||
#else
|
||||
data = mmap(nullptr, BlockBytes, PROT_NONE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
|
||||
Require(data != MAP_FAILED, "cannot reserve the MSAA guest block");
|
||||
#endif
|
||||
Map();
|
||||
}
|
||||
|
||||
~GuestBlock() {
|
||||
if (mapped) Unmap();
|
||||
#ifdef _WIN32
|
||||
VirtualFree(data, 0, MEM_RELEASE);
|
||||
#else
|
||||
munmap(data, BlockBytes);
|
||||
#endif
|
||||
}
|
||||
|
||||
GuestBlock(const GuestBlock&) = delete;
|
||||
GuestBlock& operator=(const GuestBlock&) = delete;
|
||||
|
||||
void Map() {
|
||||
Require(!mapped, "the MSAA guest block is already mapped");
|
||||
#ifdef _WIN32
|
||||
Require(VirtualAlloc(data, BlockBytes, MEM_COMMIT, PAGE_READWRITE) == data, "cannot commit the MSAA guest block");
|
||||
#else
|
||||
Require(mmap(data, BlockBytes, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS | MAP_FIXED, -1, 0) == data, "cannot map the MSAA guest block");
|
||||
#endif
|
||||
GuestAllocations::Mutation().Add(data, BlockBytes, true, true, true);
|
||||
mapped = true;
|
||||
}
|
||||
|
||||
void Unmap() {
|
||||
Require(mapped, "the MSAA guest block is not mapped");
|
||||
GuestAllocations::Mutation().Unmap(data, BlockBytes, [&](const void* begin, std::size_t bytes, const void*, bool last) {
|
||||
Require(begin == data && bytes == BlockBytes && last, "the MSAA allocation was not released as one range");
|
||||
#ifdef _WIN32
|
||||
Require(VirtualFree(data, BlockBytes, MEM_DECOMMIT) != 0, "cannot decommit the MSAA guest block");
|
||||
#else
|
||||
Require(mmap(data, BlockBytes, PROT_NONE, MAP_PRIVATE | MAP_ANONYMOUS | MAP_FIXED, -1, 0) == data, "cannot unmap the MSAA guest block");
|
||||
#endif
|
||||
});
|
||||
mapped = false;
|
||||
RequireNoMultisampleTarget(Address(), BlockBytes, "the released guest range");
|
||||
}
|
||||
|
||||
std::uint64_t Address() const { return reinterpret_cast<std::uintptr_t>(data); }
|
||||
|
||||
private:
|
||||
void* data = nullptr;
|
||||
bool mapped = false;
|
||||
};
|
||||
|
||||
ColorTarget Target(const GuestBlock& block) {
|
||||
ColorTarget target{};
|
||||
target.address = target.surfaceAddress = block.Address();
|
||||
target.extent = target.surfaceExtent = {Side, Side};
|
||||
target.format = VK_FORMAT_R8G8B8A8_UNORM;
|
||||
target.elementBytes = 4;
|
||||
target.samples = 4;
|
||||
target.tileMode = ColorTileMode::RenderTarget;
|
||||
target.bytes = ColorTargetLayout(Side, Side, target.tileMode, target.elementBytes).Bytes();
|
||||
Require(MultisampledColorBytes(target) == BlockBytes, "the MSAA guest footprint differs from its allocation");
|
||||
return target;
|
||||
}
|
||||
|
||||
GuestTextureResource Resource(const GuestBlock& block) {
|
||||
GuestTextureResource resource{};
|
||||
resource.baseAddress = block.Address();
|
||||
resource.width = resource.height = Side;
|
||||
resource.mipCount = resource.allocatedMipCount = 1;
|
||||
resource.dimension = TextureDimension::k2DMsaa;
|
||||
resource.tileMode = TextureTileMode::kR64KBX;
|
||||
resource.format = 56;
|
||||
resource.samples = 4;
|
||||
return resource;
|
||||
}
|
||||
|
||||
class Run {
|
||||
public:
|
||||
explicit Run(AgcDriver::VulkanDevice& device) : device(device), context(AgcDriver::VulkanDeviceTestAccess::ContextOf(device)) {
|
||||
realProc = context.deviceProc;
|
||||
realAllocate = context.Function<PFN_vkAllocateMemory>("vkAllocateMemory");
|
||||
realFree = context.Function<PFN_vkFreeMemory>("vkFreeMemory");
|
||||
realCreateImage = context.Function<PFN_vkCreateImage>("vkCreateImage");
|
||||
realDestroyImage = context.Function<PFN_vkDestroyImage>("vkDestroyImage");
|
||||
realBindImageMemory = context.Function<PFN_vkBindImageMemory>("vkBindImageMemory");
|
||||
realCreateImageView = context.Function<PFN_vkCreateImageView>("vkCreateImageView");
|
||||
realDestroyImageView = context.Function<PFN_vkDestroyImageView>("vkDestroyImageView");
|
||||
realClearColorImage = context.Function<PFN_vkCmdClearColorImage>("vkCmdClearColorImage");
|
||||
realCopyImage = context.Function<PFN_vkCmdCopyImage>("vkCmdCopyImage");
|
||||
context.deviceProc = CountingDeviceProc;
|
||||
context.functions = nullptr;
|
||||
}
|
||||
|
||||
~Run() { Clear(); }
|
||||
|
||||
void Sync() {
|
||||
std::lock_guard lock(AgcDriver::GuestMemory::GpuMutex());
|
||||
CheckPendingImages();
|
||||
{
|
||||
std::lock_guard countsLock(countsMutex);
|
||||
pendingImages.clear();
|
||||
}
|
||||
device.SubmitRecorded(true);
|
||||
device.WaitIdle();
|
||||
context.recorder->FlushDeferredReleases();
|
||||
}
|
||||
|
||||
void Clear() {
|
||||
Sync();
|
||||
std::lock_guard lock(AgcDriver::GuestMemory::GpuMutex());
|
||||
ClearMultisampleTargets(context.device);
|
||||
}
|
||||
|
||||
void IdleRemaps() {
|
||||
GuestBlock block;
|
||||
const auto target = Target(block);
|
||||
for (unsigned cycle = 0; cycle < 32; ++cycle) {
|
||||
{
|
||||
std::lock_guard lock(AgcDriver::GuestMemory::GpuMutex());
|
||||
Require(MultisampleTargetView(context, target) != VK_NULL_HANDLE, "an idle MSAA target has no view");
|
||||
}
|
||||
Sync();
|
||||
block.Unmap();
|
||||
if (cycle != 31) block.Map();
|
||||
}
|
||||
{
|
||||
std::lock_guard lock(countsMutex);
|
||||
std::printf("32 idle MSAA remaps: created=%llu destroyed=%llu live=%zu\n", static_cast<unsigned long long>(created), static_cast<unsigned long long>(destroyed), images.size());
|
||||
}
|
||||
AwaitImages(1, "unmapped idle MSAA targets accumulate");
|
||||
Clear();
|
||||
AwaitImages(0, "clearing idle MSAA targets did not release their allocations");
|
||||
}
|
||||
|
||||
void RecordedRemap() {
|
||||
GuestBlock block;
|
||||
const auto target = Target(block);
|
||||
VkImage oldImage = VK_NULL_HANDLE;
|
||||
Image old{};
|
||||
{
|
||||
std::lock_guard lock(AgcDriver::GuestMemory::GpuMutex());
|
||||
const auto keptBefore = context.recorder->OpenKeptBytes();
|
||||
std::shared_ptr<void> lookupOwner;
|
||||
VkImage lookupImage = VK_NULL_HANDLE;
|
||||
old = ImageFor(MultisampleTargetView(context, target, lookupOwner, &lookupImage), &oldImage);
|
||||
Require(lookupOwner != nullptr && lookupImage == oldImage, "the MSAA lookup did not return its owner and hazard image together");
|
||||
Require(context.recorder->Recording(), "MSAA initialization was not recorded");
|
||||
const auto kept = context.recorder->OpenKeptBytes();
|
||||
Require(kept >= keptBefore + AllocationBytesOf(old), "the pending MSAA allocation is missing from the recorder budget");
|
||||
Require(ImageFor(MultisampleTargetView(context, target)).generation == old.generation, "the repeated MSAA lookup changed its owner");
|
||||
Require(context.recorder->OpenKeptBytes() == kept, "a repeated MSAA lookup double-counts its allocation in one batch");
|
||||
}
|
||||
block.Unmap();
|
||||
block.Map();
|
||||
{
|
||||
std::lock_guard lock(AgcDriver::GuestMemory::GpuMutex());
|
||||
const auto keptBefore = context.recorder->OpenKeptBytes();
|
||||
const auto replacement = ImageFor(MultisampleTargetView(context, target));
|
||||
CheckPendingImages();
|
||||
Require(replacement.generation != old.generation && Alive(oldImage, old), "a pending MSAA clear lost its image during remap");
|
||||
Require(context.recorder->OpenKeptBytes() >= keptBefore + AllocationBytesOf(replacement), "the replacement MSAA allocation is missing from the recorder budget");
|
||||
}
|
||||
Sync();
|
||||
AwaitImages(1, "the completed retired MSAA target was not released");
|
||||
Require(!Alive(oldImage, old), "the retired recorded MSAA allocation remains live");
|
||||
block.Unmap();
|
||||
Clear();
|
||||
AwaitImages(0, "recorded MSAA targets were not fully released");
|
||||
}
|
||||
|
||||
void SampledRemap() {
|
||||
GuestBlock block;
|
||||
const auto target = Target(block);
|
||||
std::shared_ptr<Texture> held;
|
||||
VkImage oldImage = VK_NULL_HANDLE;
|
||||
Image old{};
|
||||
{
|
||||
std::lock_guard lock(AgcDriver::GuestMemory::GpuMutex());
|
||||
MultisampleTargetView(context, target);
|
||||
held = MultisampleTargetTexture(context, Resource(block), {});
|
||||
old = ImageFor(held->View(), &oldImage);
|
||||
}
|
||||
const std::weak_ptr<Texture> lifetime = held;
|
||||
Sync();
|
||||
block.Unmap();
|
||||
block.Map();
|
||||
{
|
||||
std::lock_guard lock(AgcDriver::GuestMemory::GpuMutex());
|
||||
const auto replacement = ImageFor(MultisampleTargetView(context, target));
|
||||
CheckPendingImages();
|
||||
Require(replacement.generation != old.generation, "a remapped sampled MSAA target reused its retired owner");
|
||||
Require(Alive(oldImage, old) && ImageFor(held->View()).generation == old.generation, "a held sampled MSAA view lost its parent allocation");
|
||||
context.recorder->Keep(held);
|
||||
held.reset();
|
||||
Require(!lifetime.expired() && Alive(oldImage, old), "a recorded sampled MSAA view lost its last alias");
|
||||
}
|
||||
Sync();
|
||||
AwaitImages(1, "a sampled MSAA owner or cached view forms a retention cycle");
|
||||
Require(lifetime.expired() && !Alive(oldImage, old), "a retired sampled MSAA parent survives its last alias and batch");
|
||||
block.Unmap();
|
||||
Clear();
|
||||
AwaitImages(0, "sampled MSAA targets were not fully released");
|
||||
}
|
||||
|
||||
void StorageRemap() {
|
||||
GuestBlock block;
|
||||
const auto target = Target(block);
|
||||
const auto resource = Resource(block);
|
||||
std::shared_ptr<Texture> held;
|
||||
VkImageView sampledView = VK_NULL_HANDLE, storageView = VK_NULL_HANDLE;
|
||||
VkImage oldImage = VK_NULL_HANDLE, promotedImage = VK_NULL_HANDLE;
|
||||
Image old{}, promoted{};
|
||||
VkDeviceSize totalBytes = 0;
|
||||
{
|
||||
std::lock_guard lock(AgcDriver::GuestMemory::GpuMutex());
|
||||
MultisampleTargetView(context, target);
|
||||
held = MultisampleTargetTexture(context, resource, {});
|
||||
sampledView = held->View();
|
||||
old = ImageFor(sampledView, &oldImage);
|
||||
}
|
||||
const std::weak_ptr<Texture> lifetime = held;
|
||||
Sync();
|
||||
{
|
||||
std::lock_guard lock(AgcDriver::GuestMemory::GpuMutex());
|
||||
const auto keptBefore = context.recorder->OpenKeptBytes();
|
||||
storageView = MultisampleTargetStorageView(context, resource);
|
||||
promoted = ImageFor(storageView, &promotedImage);
|
||||
Require(promoted.generation != old.generation && Alive(oldImage, old), "MSAA storage promotion did not retain its source image");
|
||||
Require(ImageFor(sampledView).generation == old.generation, "MSAA storage promotion destroyed a held sampled view");
|
||||
Require(held->View() == sampledView, "MSAA storage promotion changed a held sampled alias");
|
||||
totalBytes = AllocationBytesOf(old) + AllocationBytesOf(promoted);
|
||||
const auto kept = context.recorder->OpenKeptBytes();
|
||||
Require(kept == keptBefore + totalBytes, "MSAA storage promotion did not account for exactly both allocations");
|
||||
Require(MultisampleTargetStorageView(context, resource) == storageView, "a repeated MSAA storage lookup changed its view");
|
||||
Require(context.recorder->OpenKeptBytes() == kept, "a repeated MSAA storage lookup double-counts its allocations");
|
||||
}
|
||||
Sync();
|
||||
{
|
||||
std::lock_guard lock(AgcDriver::GuestMemory::GpuMutex());
|
||||
Require(Alive(oldImage, old) && ImageFor(sampledView).generation == old.generation, "a held sampled MSAA view did not survive storage promotion completion");
|
||||
Require(held->View() == sampledView, "a held sampled MSAA alias changed after promotion completion");
|
||||
held.reset();
|
||||
const auto keptBefore = context.recorder->OpenKeptBytes();
|
||||
Require(MultisampleTargetStorageView(context, resource) == storageView, "a later MSAA storage lookup changed its promoted image");
|
||||
const auto kept = context.recorder->OpenKeptBytes();
|
||||
Require(kept == keptBefore + totalBytes, "an already-storable MSAA target is missing allocations from the new batch budget");
|
||||
Require(MultisampleTargetStorageView(context, resource) == storageView, "a repeated promoted MSAA lookup changed its view");
|
||||
Require(context.recorder->OpenKeptBytes() == kept, "an already-storable MSAA target double-counts its allocations in one batch");
|
||||
}
|
||||
block.Unmap();
|
||||
block.Map();
|
||||
{
|
||||
std::lock_guard lock(AgcDriver::GuestMemory::GpuMutex());
|
||||
const auto replacement = ImageFor(MultisampleTargetView(context, target));
|
||||
CheckPendingImages();
|
||||
Require(replacement.generation != promoted.generation, "a remapped MSAA target reused its promoted storage owner");
|
||||
Require(!lifetime.expired() && Alive(oldImage, old) && Alive(promotedImage, promoted), "a pending MSAA storage use lost its promoted or retired allocation");
|
||||
}
|
||||
Sync();
|
||||
AwaitImages(1, "the completed promoted MSAA owner was not released");
|
||||
Require(lifetime.expired() && !Alive(oldImage, old) && !Alive(promotedImage, promoted), "a retired MSAA storage owner retained its allocations after completion");
|
||||
block.Unmap();
|
||||
Clear();
|
||||
AwaitImages(0, "promoted MSAA targets were not fully released");
|
||||
}
|
||||
|
||||
private:
|
||||
AgcDriver::VulkanDevice& device;
|
||||
Context context;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
int main() {
|
||||
try {
|
||||
const auto device = OpenVulkanTestDevice();
|
||||
if (!device) return VulkanTestSkipped;
|
||||
const auto context = AgcDriver::VulkanDeviceTestAccess::ContextOf(*device);
|
||||
Require((context.limits.framebufferColorSampleCounts & VK_SAMPLE_COUNT_4_BIT) != 0, "the retirement fixture needs 4x color targets");
|
||||
Run run(*device);
|
||||
run.IdleRemaps();
|
||||
run.RecordedRemap();
|
||||
run.SampledRemap();
|
||||
run.StorageRemap();
|
||||
std::puts("retired MSAA allocations stay bounded, recorded clears and storage promotions retain their images, and sampled aliases release their parents after the last batch");
|
||||
return 0;
|
||||
} catch (const std::exception& error) {
|
||||
std::cerr << error.what() << '\n';
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
@@ -53,7 +53,7 @@
|
||||
|
||||
- [Initial stencil state](../../../core/libs/prx/libSceAgcDriver/Execution/include/QueueState.hpp) uses zero for `DB_STENCIL_CONTROL`, `DB_STENCILREFMASK` and `DB_STENCILREFMASK_BF` (context `0x10b`-`0x10d`), including after `CLEAR_STATE`. The public V0, V4, V5 and V7-V11 [register-default tables](../../../core/libs/prx/libSceAgc/Command/include/RegisterDefaultsData.hpp) supply these values. Linux's [GFX10 clear-state table](https://github.com/torvalds/linux/blob/master/drivers/gpu/drm/amd/amdgpu/clearstate_gfx10.h) agrees on the control register but uses `0x01000000` for the reference masks; the console reset values have not been measured. Planet Coaster (PPSA01736 v01.017) enables stencil tests without writing the control register.
|
||||
- [Type-0 and type-1 texture descriptors](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestTextureResource.cpp) (`TYPE` = 0 or 1 in dword 3) with a nonzero base are bound as null images, like a zero base. TYPE 0 comes from a gfx1036 (RDNA2) measurement in issue #4139 (image sample, load and resinfo return zero, no fault). TYPE 1 comes from a gfx1010 (RDNA1) measurement in #4139 and #5828 (`image_sample_lz`, `image_sample_b`, `image_load` and `image_get_resinfo` return zero, no fault) with format 22 only; TYPE 1 is not measured on RDNA2, with other formats or with depth-compare reads (`image_sample_c*`). Neither type was measured on the console's gfx1013.
|
||||
- [Multisampled color targets](../../../core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp) lose their guest-read guards and cannot be reused after a successful allocation removal or unmap covers their full guarded range. Retired Vulkan images remain allocated until device teardown to preserve outstanding references. Partial unmaps keep the guard; partial allocation reuse and guest access to live multisampled data remain unsupported.
|
||||
- [Multisampled color targets](../../../core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp) lose their guest-read guards and cannot be reused after a successful allocation removal or unmap covers their full guarded range. The next target, sampled or storage lookup removes retired cache owners; recorded work and sampled views keep their images alive until their last reference is released. Partial unmaps keep the guard; partial allocation reuse and guest access to live multisampled data remain unsupported.
|
||||
- [Stencil reference export replay](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp) on devices without `VK_EXT_shader_stencil_export` supports at most sixteen values proved from constant, select and phi exports, with an export proved on every kept path. It records instances, then their triangles, then the reference classes of each triangle. One pipeline filters fragments at shader termination against a dynamic push value before late stencil and depth tests. The [Vulkan stencil-reference replacement rule](https://docs.vulkan.org/refpages/latest/refpages/source/FragStencilRefEXT.html) discards bits above the attachment's stencil width; supported stencil attachments have eight bits. Numeric subgroup observations throw except bounded own-lane mask plumbing and group control with kept-lane and derivative-helper coverage proved. WQM inputs must exclude helper contributions, since [non-quad subgroup operations may treat helpers as inactive](https://docs.vulkan.org/glslext/latest/glslext/khr/GL_KHR_shader_subgroup.html). Readonly guarded SRT reads may report internal host faults; these draws wait and check faults before publishing results, and loops throw. Indirect draws, restart, strips, fans, tessellation, mesh, multisampling, early tests, ordered pixel execution, guest shader memory writes, other fault-producing memory access, clocks, primitive ID inputs, attachment feedback and pipeline face/channel splits throw. An exhausted stage push range throws. Equivalence to console ordering has not been measured on console hardware.
|
||||
|
||||
- [32-bit depth-bits reads of guest memory](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp) (libSceAgcDriver): a sampled 32_UINT or 32_SINT T# with a depth swizzle mode that no live depth surface matches reads the guest bytes through an R32_SFLOAT image, as the recompiler's depth-bits read expects, and the shader casts the fetched value back to the stored dword. On RX 6700 XT (gfx1031, AMD Windows driver 32.0.21045.5002) the read keeps float denormals, the sign bit alone and quiet and signaling NaN payloads bit for bit. A host that canonicalizes NaNs or flushes denormals on a 32-bit float fetch would change those dwords, and other drivers are not measured. A live depth surface whose extent or base level differs from the T# is not used; the read takes the guest bytes, as the 32-bit float and 16-bit views already do.
|
||||
@@ -129,7 +129,7 @@
|
||||
- [Multisampled color fills](../../../core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp) (libSceAgcDriver) - a multisampled target's samples are on the host, so a fill of its color memory is applied at its next use: a uniform fill over the whole of it clears the host image to that value, and a pending CMASK fast clear takes precedence over the fill and over a fill that would throw. Its color memory is a single mip and slice of addrlib's block dimensions for its sample count (ComputeThinBlockDimension). A fill over part of it, a fill reaching the start of a target whose footprint is not computed (linear), and a fill value with no exact clear value in the format (sRGB channels other than 0 and 1) throw at the next use. A fill before the target's first use is not seen (the host image starts at 0), nor are copies or shader writes into its color memory. A uniform compute fill over exactly the whole color memory with an exact clear value is not stored into guest memory, since only the host image holds the samples. A target is retired only when its whole guarded range is unmapped or its allocation removed, so if a title reuses that memory for other data while it stays mapped, a read the guard does not check (a buffer read through a V# or a pointer) sees the bytes from before the fill.
|
||||
- [Multisampled texture no draw rendered to](../../../core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp) (libSceAgcDriver) - sampling a multisampled texture whose surface no draw rendered to creates the host target from the T# and clears it to the color a single 32-bit value repeated over the whole footprint encodes, padding up to whole tile blocks included. Inferred, not measured on the console: that the samples of such a surface are the guest color memory, as a title that clears its canvas on the CPU (ps5-opengl `ps5_clear_msaa4_color`) relies on. Memory that is not one repeated value, tile modes other than Standard4KB, Standard64KB and RenderTarget, unmapped memory and values with no exact clear color throw. CPU writes to the surface after its host target exists are not seen by the host.
|
||||
- [Multisampled targets in CB passes](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - a CB eliminate or DCC pass over a multisampled target throws, since its texels are on the host and only a CB resolve and an FMASK decompress (`CB_COLOR_CONTROL` mode 5) are modeled. The FMASK decompress only applies the pending CMASK fast clear to the host image; it requires a CMASK address, and the target's DCC and FMASK contents are not interpreted. DCC over a multisampled target is accepted only in that pass and throws everywhere else. A CB resolve into a fast-clear destination throws: the resolve writes the destination's texels but not its CMASK, which would still read as cleared.
|
||||
- [Multisampled target guest access](../../../core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp) (libSceAgcDriver) - the memory a multisampled color target throws on (CPU read, index buffer read, fixed-function vertex buffer read, DMA copy, texture or storage image read) uses fragment-aware 64 KiB render-target blocks. [AMD Addrlib's GFX10 thin-block rule](https://github.com/GPUOpen-Drivers/pal/blob/c5e800072a32f68b6ccc4422936d96167c6e0728/src/core/imported/addrlib/src/core/addrlib2.cpp#L1680-L1712) gives the block width and height from element bytes and fragment count; its [surface-size calculation](https://github.com/GPUOpen-Drivers/pal/blob/c5e800072a32f68b6ccc4422936d96167c6e0728/src/core/imported/addrlib/src/gfx10/gfx10addrlib.cpp#L4000-L4005) rounds both extents to those blocks and multiplies by element bytes and fragments. Thus a 1920x1080 4x RGBA8 surface occupies 0x1fe0000 bytes, leaving the next allocation at +0x2000000 outside the guard. The 2x and 8x block orientations differ from treating the fragments as a larger element. Console address-layout equivalence has not been measured. Other tile modes retain the conservative `max(CB bytes * samples, width * height * element bytes * samples)` bound, which may still reject a distinct allocation. Targets are kept per device for the device's lifetime, one per surface address, format, extent and sample count, and are not evicted.
|
||||
- [Multisampled target guest access](../../../core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp) (libSceAgcDriver) - the memory a multisampled color target throws on (CPU read, index buffer read, fixed-function vertex buffer read, DMA copy, texture or storage image read) uses fragment-aware 64 KiB render-target blocks. [AMD Addrlib's GFX10 thin-block rule](https://github.com/GPUOpen-Drivers/pal/blob/c5e800072a32f68b6ccc4422936d96167c6e0728/src/core/imported/addrlib/src/core/addrlib2.cpp#L1680-L1712) gives the block width and height from element bytes and fragment count; its [surface-size calculation](https://github.com/GPUOpen-Drivers/pal/blob/c5e800072a32f68b6ccc4422936d96167c6e0728/src/core/imported/addrlib/src/gfx10/gfx10addrlib.cpp#L4000-L4005) rounds both extents to those blocks and multiplies by element bytes and fragments. Thus a 1920x1080 4x RGBA8 surface occupies 0x1fe0000 bytes, leaving the next allocation at +0x2000000 outside the guard. The 2x and 8x block orientations differ from treating the fragments as a larger element. Console address-layout equivalence has not been measured. Other tile modes retain the conservative `max(CB bytes * samples, width * height * element bytes * samples)` bound, which may still reject a distinct allocation. Live targets remain cached per device, surface address, format, extent and sample count. Retired cache owners are removed on the next target, sampled or storage lookup; outstanding recorded work and sampled views retain their images.
|
||||
- [CB resolve sample count](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - a draw with CB_COLOR_CONTROL in resolve mode is a CB resolve when either PA_SC_AA_CONFIG or CB_COLOR0_ATTRIB.NUM_SAMPLES is multisampled, and takes its sample count from CB0. Inferred, not from documentation: PPSA36082 resolves its 4x targets with no sample count in PA_SC_AA_CONFIG, and radeonsi sets PA_SC_AA_CONFIG to 0 for a multisampled framebuffer when rasterizer multisampling and line smoothing are off.
|
||||
- [Custom MSAA sample locations](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) support a uniform 1x1 pattern for 2, 4 or 8 samples through VK_EXT_sample_locations. Patterns varying across the guest's 2x2 pixel grid use a 2x2 sample location grid, anchored at the framebuffer origin like the guest's while the window offset is zero, on devices whose `maxSampleLocationGridSize` for the sample count is a multiple of 2x2, and throw on others. The RTX 5070 reports 2x4 for 2 samples, 2x2 for 4 and 1x2 for 8; an AMD Radeon integrated GPU reports 2x2 for each. Devices without the required sample count, four subpixel bits or the full guest coordinate range refuse custom patterns. A multisampled rasterizer drawing into one-sample color targets, and a one-sample depth target if any, with MSAA_ENABLE set, no alpha-to-coverage and no coverage pixel input is drawn with one sample at sample 0's location in each pixel of the grid, as the targets keep only sample 0's coverage. Without PS_ITER_SAMPLE such a draw also refuses centroid inputs, and keeps PERSP_SAMPLE, LINEAR_SAMPLE and ANCILLARY: a pixel shaded once is taken to get sample 0's barycentrics and sample index 0. With PS_ITER_SAMPLE only sample 0's invocation reaches the targets, so its sample and centroid barycentrics are taken to be sample 0's and the draw is shaded once. Both are inferred, not measured. ANCILLARY with POS_X_FLOAT or POS_Y_FLOAT throws, since reading the sample index makes the host shade per sample, which moves FragCoord from the pixel center to sample 0's location. By the same Vulkan rule, PERSP_CENTER and LINEAR_CENTER inputs on this path are interpolated at sample 0's location on the host, where the console uses the pixel center; one-sample depth images are created with `SAMPLE_LOCATIONS_COMPATIBLE_DEPTH` when the device supports it, as multisampled ones already were; this is how VK_AMD_mixed_attachment_samples defines the case, and it is not measured on PS5 hardware. Other mixed sample counts throw. Depth images remain in GENERAL between draws, and render passes cannot continue across pattern changes. Guest offsets retain the existing decoder's signed four-bit, sixteenth-pixel interpretation; PS5 coverage and interpolation equivalence have not been measured on hardware.
|
||||
- [Single-sample scan into a multisampled depth target](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - a draw with `PA_SC_AA_CONFIG.MSAA_NUM_SAMPLES` = 0 and `PA_SC_MODE_CNTL_0.MSAA_ENABLE` clear into a multisampled depth target rasterizes the depth target's sample count with every sample at the pixel center, as a single-sample scan into a multisampled color target already does. radeonsi programs this `MSAA_ENABLE` and `PA_SC_AA_CONFIG` with the depth buffer's own sample count when GL multisampling is disabled on a multisampled framebuffer ([L1066-L1068](https://github.com/openbsd/xenocara/blob/1b39033df8d928ec247ca64448c4a63c72302db7/lib/mesa/src/gallium/drivers/radeonsi/si_state.c#L1066-L1068), [L3485-L3512](https://github.com/openbsd/xenocara/blob/1b39033df8d928ec247ca64448c4a63c72302db7/lib/mesa/src/gallium/drivers/radeonsi/si_state.c#L3485-L3512)), although its comment lists Z samples as at most the coverage samples ([L3439](https://github.com/openbsd/xenocara/blob/1b39033df8d928ec247ca64448c4a63c72302db7/lib/mesa/src/gallium/drivers/radeonsi/si_state.c#L3439)) and it sets `DB_EQAA.MAX_ANCHOR_SAMPLES` to the Z sample count, where PPSA18216 sets `DB_EQAA` to 0. GL then lets every sample take the single-sample depth ([L185-L190](https://github.com/KhronosGroup/OpenGL-Registry/blob/6af574a14089ccfee87efe230ebcdd8742859813/extensions/ARB/ARB_multisample.txt#L185-L190)) and the fragment be treated as in multisampling with full coverage ([L422-L425](https://github.com/KhronosGroup/OpenGL-Registry/blob/6af574a14089ccfee87efe230ebcdd8742859813/extensions/ARB/ARB_multisample.txt#L422-L425)), which is what this draws: each depth and stencil sample is tested and written with the depth at the pixel center. GL also lets an implementation test only a centermost sample and set every sample from it ([L425-L434](https://github.com/KhronosGroup/OpenGL-Registry/blob/6af574a14089ccfee87efe230ebcdd8742859813/extensions/ARB/ARB_multisample.txt#L425-L434)), which differs when a pixel's stored samples differ; which one the console does, whether it instead evaluates the depth plane at each sample position, and how its occlusion queries count such a draw, is not measured on hardware. Points, lines, alpha-to-coverage, `DB_EQAA` Z interpolation or overrasterization, a pixel program reading the sample index, a sample-mask export and a single-sample color target next to the multisampled depth target throw
|
||||
|
||||
Reference in new issue
Block a user