Files
Richard Stokes e36e9df2dc vk oit: fix synchronization hazards between render passes and frames
The OIT color and depth/stencil attachments, a-buffers and pixel counter
are shared by consecutive render passes and by all frames in flight, but
the render pass had no external dependency covering them, and the pixel
counter reset wasn't synchronized with the fragment shaders using it.

With MoltenVK using Metal 3 argument buffers and residency sets (macOS 15+),
Metal no longer tracks hazards on resources accessed through argument
buffers, so input attachment reads and storage buffer accesses of a frame
may race with the attachment clears/loads and a-buffer writes of the next
one. This results in sporadic one-frame glitches with per-pixel alpha
sorting.

Issue #2495
2026-09-28 13:49:06 +02:00

151 lines
6.2 KiB
C++

/*
Created on: Nov 12, 2019
Copyright 2019 flyinghead
This file is part of Flycast.
Flycast is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 2 of the License, or
(at your option) any later version.
Flycast is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Flycast. If not, see <https://www.gnu.org/licenses/>.
*/
#pragma once
#include "../buffer.h"
#include "../texture.h"
#include <cinttypes>
#include <memory>
class OITBuffers
{
public:
void Init(int width, int height)
{
const VulkanContext *context = VulkanContext::Instance();
if (width <= maxWidth && height <= maxHeight)
return;
maxWidth = std::max(maxWidth, width);
maxHeight = std::max(maxHeight, height);
if (!pixelCounter)
{
pixelCounter = std::make_unique<BufferData>(4,
vk::BufferUsageFlagBits::eStorageBuffer | vk::BufferUsageFlagBits::eTransferDst, vk::MemoryPropertyFlagBits::eDeviceLocal);
pixelCounterReset = std::make_unique<BufferData>(4, vk::BufferUsageFlagBits::eTransferSrc);
const int zero = 0;
pixelCounterReset->upload(sizeof(zero), &zero);
}
// We need to wait until this buffer is not used before deleting it
context->WaitIdle();
abufferPointer.reset();
abufferPointer = std::make_unique<BufferData>(maxWidth * maxHeight * sizeof(int),
vk::BufferUsageFlagBits::eStorageBuffer, vk::MemoryPropertyFlagBits::eDeviceLocal);
firstFrameAfterInit = true;
}
void updateDescriptorSet(vk::DescriptorSet descSet, std::vector<vk::WriteDescriptorSet>& writeDescSets)
{
static vk::DescriptorBufferInfo pixelBufferInfo({}, 0, vk::WholeSize);
pixelBufferInfo.buffer = *pixelBuffer->buffer;
/**
* The intention behind setting the range to 1 byte in BDA mode is to make it easier to spot any missing or invalid USE_BDA defines in shader sources.
* If USE_BDA is missing or invalid, the shader will access PixelBuffer as an SSBO, and if it's this tiny, it should result in obvious graphical glitches.
*/
pixelBufferInfo.range = pixelBufferAddress ? 1 : VK_WHOLE_SIZE;
writeDescSets.emplace_back(descSet, 7, 0, vk::DescriptorType::eStorageBuffer, nullptr, pixelBufferInfo);
static vk::DescriptorBufferInfo pixelCounterBufferInfo({}, 0, 4);
pixelCounterBufferInfo.buffer = *pixelCounter->buffer;
writeDescSets.emplace_back(descSet, 8, 0, vk::DescriptorType::eStorageBuffer, nullptr, pixelCounterBufferInfo);
static vk::DescriptorBufferInfo abufferPointerInfo({}, 0, vk::WholeSize);
abufferPointerInfo.buffer = *abufferPointer->buffer;
writeDescSets.emplace_back(descSet, 9, 0, vk::DescriptorType::eStorageBuffer, nullptr, abufferPointerInfo);
}
void OnNewFrame(vk::CommandBuffer commandBuffer)
{
firstFrameAfterInit = false;
if (pixelBufferSize != config::PixelBufferSize)
{
pixelBufferSize = config::PixelBufferSize;
VulkanContext::Instance()->WaitIdle();
makePixelBuffer();
}
}
void ResetPixelCounter(vk::CommandBuffer commandBuffer)
{
// Previous render passes and frames in flight must be done with the counter
vk::BufferMemoryBarrier barrier(vk::AccessFlagBits::eShaderWrite, vk::AccessFlagBits::eTransferWrite,
vk::QueueFamilyIgnored, vk::QueueFamilyIgnored, *pixelCounter->buffer, 0, vk::WholeSize);
commandBuffer.pipelineBarrier(vk::PipelineStageFlagBits::eFragmentShader, vk::PipelineStageFlagBits::eTransfer,
{}, nullptr, barrier, nullptr);
vk::BufferCopy copy(0, 0, sizeof(int));
commandBuffer.copyBuffer(*pixelCounterReset->buffer, *pixelCounter->buffer, copy);
// The reset must be visible to the fragment shaders of the next render pass
barrier.srcAccessMask = vk::AccessFlagBits::eTransferWrite;
barrier.dstAccessMask = vk::AccessFlagBits::eShaderRead | vk::AccessFlagBits::eShaderWrite;
commandBuffer.pipelineBarrier(vk::PipelineStageFlagBits::eTransfer, vk::PipelineStageFlagBits::eFragmentShader,
{}, nullptr, barrier, nullptr);
}
void Term()
{
pixelBufferAddress = 0;
pixelBuffer.reset();
pixelCounter.reset();
pixelCounterReset.reset();
abufferPointer.reset();
}
bool isFirstFrameAfterInit() const { return firstFrameAfterInit; }
vk::DeviceAddress getPixelBufferAddress() const { return pixelBufferAddress; }
vk::DeviceSize getPixelBufferSize() const
{
return pixelBuffer ? pixelBuffer->bufferSize : 0;
}
private:
std::unique_ptr<BufferData> pixelBuffer;
std::unique_ptr<BufferData> pixelCounter;
std::unique_ptr<BufferData> pixelCounterReset;
std::unique_ptr<BufferData> abufferPointer;
bool firstFrameAfterInit = false;
int maxWidth = 0;
int maxHeight = 0;
int64_t pixelBufferSize = 0;
vk::DeviceAddress pixelBufferAddress = 0;
void makePixelBuffer() {
const VulkanContext *context = VulkanContext::Instance();
const u32 maxStorageBufferRange = context->GetMaxStorageBufferRange();
vk::DeviceSize allocSize = std::min<vk::DeviceSize>(pixelBufferSize, context->GetMaxMemoryAllocationSize());
vk::BufferUsageFlags usage = vk::BufferUsageFlagBits::eStorageBuffer;
if (allocSize > maxStorageBufferRange) {
if (context->SupportsBufferDeviceAddress()) {
NOTICE_LOG(RENDERER, "PixelBuffer allocSize %" PRIu64 " > maxStorageBufferRange %lu; will use BDA", allocSize, (unsigned long)maxStorageBufferRange);
usage |= vk::BufferUsageFlagBits::eShaderDeviceAddressKHR;
} else {
NOTICE_LOG(RENDERER, "PixelBuffer allocSize %" PRIu64 " > maxStorageBufferRange %lu; capping allocSize, as the GPU doesn't support BDA", allocSize, (unsigned long)maxStorageBufferRange);
allocSize = maxStorageBufferRange;
}
}
pixelBuffer = std::make_unique<BufferData>(allocSize, usage, vk::MemoryPropertyFlagBits::eDeviceLocal);
pixelBufferAddress =
(usage & vk::BufferUsageFlagBits::eShaderDeviceAddressKHR)
? context->GetDevice().getBufferAddress(vk::BufferDeviceAddressInfo{*pixelBuffer->buffer}) : 0;
DEBUG_LOG(RENDERER, "PixelBuffer allocated. size %" PRIu64 " address %" PRIu64 "", allocSize, pixelBufferAddress);
}
};