Files
libretroadmin 0785467fdc Vulkan: a frame's descriptor sets and buffers allocate nothing
A descriptor set is made with vk_alloc_desc_set(), nothing allocated but
the set (vulkan.hpp's allocateDescriptorSetsUnique() made a std::vector
for each), and one retired while frames may still use it is given back
through the command pool's deferred list, CommandPool::DeferFree(), not
held in an object made for it. A drawer's main and vertex uniform
buffers are made with the drawer, not when it first draws: a render to
a texture may come long after the game started. Seven sites come off
the list of allocations during play; a site is allowed now by any of
its first three of the core's functions, so that a function inlined or
not names it the same.

vk_i686.sh compiles the Vulkan renderers for 32-bit Windows, as the
buildbot builds them, where a Vulkan handle is an integer and the
wrappers do not take one unasked; CI runs it.
2026-11-10 02:13:54 +01:00

387 lines
18 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 <algorithm>
#include <memory>
#include "../buffer.h"
#include "../texture.h"
#include "../commandpool.h"
#include "rend/oit_size.h"
#include "rend/oit_bands.h"
class OITBuffers
{
public:
/* The fragments are spread over this many storage buffers when the
* device cannot bind them as one (the shaders' PixelBuffer[]) */
static constexpr u32 Bindings = 4;
/* Render passes of a frame whose fragments are counted (OITDrawer::Draw()) */
static constexpr u32 Counts = 64;
// Frame slots counted: the command pool's, which are the swap chain's
static constexpr u32 CountSlots = OIT_BANDS_SLOTS;
/* The fragment buffer for a picture of @width x @height: rend/oit_size.h,
* no more than the device can bind. One storage buffer is no larger
* than the device can allocate in one piece, nor than a shader can be
* given as one (Mesa's software Vulkan stops at 128 MB, and a buffer
* bound past the limit drew nothing at all). */
vk::DeviceSize PlannedSize(int width, int height) const
{
const u32 kib = VulkanContext::Instance()->GetVramBudget().available_kib();
if (compact)
return oit_fallback_buffer_bytes((u32)std::max(width, 0), (u32)std::max(height, 0), kib, (u64)BindingLimit() * Bindings);
return oit_buffer_bytes((u32)std::max(width, 0), (u32)std::max(height, 0), kib, (u64)BindingLimit() * Bindings);
}
/* The compute renderer draws the screen's frames: the buffer is for
* the frames it leaves to the per-pixel renderer alone
* (oit_fallback_buffer_bytes()). Before the renderer is set up, or
* made again for a size: never while a game runs. */
void SetCompact(bool on)
{
compact = on;
plannedSize = 0;
}
/* With SSAA and the HDR output: an image of each pixel's channel sums,
* which the screen's last pass stores (OIT_FINAL_FRAG_S1_SUMS) for the
* encode to take the exact average from (VulkanContext::HdrEncode()).
* Before the renderer is set up, like SetCompact(), or made again for
* a size; made at the next Init(). */
void SetSums(bool on)
{
sums = on;
}
bool Sums() const { return sums; }
// (null without)
vk::ImageView GetSumsView() const { return sumsAttachment ? sumsAttachment->GetImageView() : vk::ImageView(); }
/* The screen's size: the fragment buffer is made for it, at the next
* Init(). Renders to a texture larger than the screen are drawn in
* more bands rather than the buffer made again while a game runs. */
void SetScreenSize(int width, int height)
{
plannedSize = PlannedSize(width, height);
}
void Init(int width, int height, CommandPool *commandPool)
{
const VulkanContext *context = VulkanContext::Instance();
if (!descSetLayout)
{
// Descriptor set and pipeline layout
vk::DescriptorSetLayoutBinding descSetLayoutBindings[] = {
{ 0, vk::DescriptorType::eStorageBuffer, Bindings, vk::ShaderStageFlagBits::eFragment }, // pixel buffer
{ 1, vk::DescriptorType::eStorageBuffer, 1, vk::ShaderStageFlagBits::eFragment }, // pixel counter
{ 2, vk::DescriptorType::eStorageImage, 1, vk::ShaderStageFlagBits::eFragment }, // a-buffer pointers
// (SSAA's channel sums, for the HDR output: written only with SetSums(), read by its shader alone)
{ 3, vk::DescriptorType::eStorageImage, 1, vk::ShaderStageFlagBits::eFragment },
};
descSetLayout = context->GetDevice().createDescriptorSetLayoutUnique(
vk::DescriptorSetLayoutCreateInfo(vk::DescriptorSetLayoutCreateFlags(), ARRAY_SIZE(descSetLayoutBindings), descSetLayoutBindings));
}
// (the renderer's set-up: the layout only, the buffers are for the screen's size)
if (width <= 0 || height <= 0)
return;
bool remake = false;
if (plannedSize == 0)
plannedSize = PlannedSize(width, height);
if (pixelBuffers[0] == nullptr || plannedSize != bufferSize)
{
MakePixelBuffers(commandPool);
remake = true;
}
if (!counts)
{
// (read by the CPU once a frame's fence has passed: see ChooseBands())
counts = std::unique_ptr<BufferData>(new BufferData(CountSlots * Counts * sizeof(u32), vk::BufferUsageFlagBits::eTransferDst));
std::fill(std::begin(written), std::end(written), 0);
}
if (width > maxWidth || height > maxHeight || !abufferPointerAttachment || sums != (sumsAttachment != nullptr))
{
maxWidth = std::max(maxWidth, width);
maxHeight = std::max(maxHeight, height);
/* Frames already handed to the GPU may still be using the image and
* the descriptor set that names it. Neither is touched: the image is
* retired, to be destroyed once those frames are done, and so is the
* descriptor set, with a new one written in its place below.
*
* This used to wait for the frontend's frames, which are not the
* ones in question - the core submits its own - and then rewrote
* the descriptor set those were using. */
if (abufferPointerAttachment)
commandPool->DeferDelete(std::move(abufferPointerAttachment));
abufferPointerAttachment = std::unique_ptr<FramebufferAttachment>(
new FramebufferAttachment(context->GetPhysicalDevice(), context->GetDevice()));
// (cleared when it is first used: see OnNewFrame)
abufferPointerAttachment->Init(maxWidth, maxHeight, vk::Format::eR32Uint,
vk::ImageUsageFlagBits::eStorage | vk::ImageUsageFlagBits::eTransferDst);
abufferPointerTransitionNeeded = true;
if (sumsAttachment)
commandPool->DeferDelete(std::move(sumsAttachment));
if (sums)
{
sumsAttachment = std::unique_ptr<FramebufferAttachment>(
new FramebufferAttachment(context->GetPhysicalDevice(), context->GetDevice()));
sumsAttachment->Init(maxWidth, maxHeight, vk::Format::eR16G16B16A16Uint,
vk::ImageUsageFlagBits::eStorage | vk::ImageUsageFlagBits::eSampled, false);
}
firstFrameAfterInit = true;
remake = true;
}
if (!remake)
return;
if (descSet)
commandPool->DeferFree(std::move(descSet), context->GetDescriptorPool());
descSet = vk_alloc_desc_set(context->GetDevice(), context->GetDescriptorPool(), descSetLayout.get());
/* (on the stack: nothing allocated for them) */
vk::WriteDescriptorSet writeDescriptorSets[4];
u32 writes = 0;
// (the buffers not needed are given the first again: the shaders never go to them)
vk::DescriptorBufferInfo pixelBufferInfo[Bindings];
for (u32 i = 0; i < Bindings; i++)
pixelBufferInfo[i] = vk::DescriptorBufferInfo(*pixelBuffers[i < bindings ? i : 0]->buffer, 0, bindingSize);
writeDescriptorSets[writes++] = (vk::WriteDescriptorSet(*descSet, 0, 0, Bindings, vk::DescriptorType::eStorageBuffer, nullptr, pixelBufferInfo, nullptr));
vk::DescriptorBufferInfo pixelCounterBufferInfo(*pixelCounter->buffer, 0, 2 * sizeof(u32));
writeDescriptorSets[writes++] = (vk::WriteDescriptorSet(*descSet, 1, 0, 1, vk::DescriptorType::eStorageBuffer, nullptr, &pixelCounterBufferInfo, nullptr));
vk::DescriptorImageInfo pointerImageInfo(vk::Sampler(), abufferPointerAttachment->GetImageView(), vk::ImageLayout::eGeneral);
writeDescriptorSets[writes++] = (vk::WriteDescriptorSet(*descSet, 2, 0, 1, vk::DescriptorType::eStorageImage, &pointerImageInfo, nullptr, nullptr));
vk::DescriptorImageInfo sumsImageInfo(vk::Sampler(), sumsAttachment ? sumsAttachment->GetImageView() : vk::ImageView(), vk::ImageLayout::eGeneral);
if (sumsAttachment)
writeDescriptorSets[writes++] = (vk::WriteDescriptorSet(*descSet, 3, 0, 1, vk::DescriptorType::eStorageImage, &sumsImageInfo, nullptr, nullptr));
context->GetDevice().updateDescriptorSets(writes, writeDescriptorSets, 0, nullptr);
}
void BindDescriptorSet(vk::CommandBuffer cmdBuffer, vk::PipelineLayout pipelineLayout, u32 firstSet)
{
cmdBuffer.bindDescriptorSets(vk::PipelineBindPoint::eGraphics, pipelineLayout, firstSet, 1, &descSet.get(), 0, nullptr);
}
void OnNewFrame(vk::CommandBuffer commandBuffer)
{
if (sumsAttachment)
{
/* The sums: in the general layout from the first; after that,
* the frame before's encode done reading them before this
* frame's last pass stores them again */
vk::ImageSubresourceRange range(vk::ImageAspectFlagBits::eColor, 0, 1, 0, 1);
vk::ImageMemoryBarrier barrier(vk::AccessFlags(), vk::AccessFlagBits::eShaderWrite,
abufferPointerTransitionNeeded ? vk::ImageLayout::eUndefined : vk::ImageLayout::eGeneral, vk::ImageLayout::eGeneral,
VK_QUEUE_FAMILY_IGNORED, VK_QUEUE_FAMILY_IGNORED, sumsAttachment->GetImage(), range);
commandBuffer.pipelineBarrier(vk::PipelineStageFlagBits::eFragmentShader, vk::PipelineStageFlagBits::eFragmentShader, {}, nullptr, nullptr,
barrier);
}
if (abufferPointerTransitionNeeded)
{
abufferPointerTransitionNeeded = false;
vk::ImageSubresourceRange imageSubresourceRange(vk::ImageAspectFlagBits::eColor, 0, 1, 0, 1);
// (made ready for the clear below, which is what uses it next)
vk::ImageMemoryBarrier imageMemoryBarrier(vk::AccessFlags(), vk::AccessFlagBits::eTransferWrite,
vk::ImageLayout::eUndefined, vk::ImageLayout::eGeneral, VK_QUEUE_FAMILY_IGNORED, VK_QUEUE_FAMILY_IGNORED,
abufferPointerAttachment->GetImage(), imageSubresourceRange);
commandBuffer.pipelineBarrier(vk::PipelineStageFlagBits::eTopOfPipe, vk::PipelineStageFlagBits::eTransfer, {}, nullptr, nullptr,
imageMemoryBarrier);
/* The lists start empty. The image is new and holds nothing
* that means anything; every frame leaves the lists empty for
* the next, and this does it for the first. */
const vk::ClearColorValue endOfList(std::array<uint32_t, 4>{ 0xffffffffu, 0xffffffffu, 0xffffffffu, 0xffffffffu });
commandBuffer.clearColorImage(abufferPointerAttachment->GetImage(), vk::ImageLayout::eGeneral, endOfList, imageSubresourceRange);
vk::ImageMemoryBarrier cleared(vk::AccessFlagBits::eTransferWrite, vk::AccessFlagBits::eShaderRead | vk::AccessFlagBits::eShaderWrite,
vk::ImageLayout::eGeneral, vk::ImageLayout::eGeneral, VK_QUEUE_FAMILY_IGNORED, VK_QUEUE_FAMILY_IGNORED,
abufferPointerAttachment->GetImage(), imageSubresourceRange);
commandBuffer.pipelineBarrier(vk::PipelineStageFlagBits::eTransfer, vk::PipelineStageFlagBits::eFragmentShader, {}, nullptr, nullptr,
cleared);
}
else
{
firstFrameAfterInit = false;
}
}
void ResetPixelCounter(vk::CommandBuffer commandBuffer)
{
/* The render passes and frames before this must be done counting... */
vk::BufferMemoryBarrier barrier(vk::AccessFlagBits::eShaderWrite, vk::AccessFlagBits::eTransferWrite,
VK_QUEUE_FAMILY_IGNORED, VK_QUEUE_FAMILY_IGNORED, *pixelCounter->buffer, 0, VK_WHOLE_SIZE);
commandBuffer.pipelineBarrier(vk::PipelineStageFlagBits::eFragmentShader, vk::PipelineStageFlagBits::eTransfer,
{}, nullptr, barrier, nullptr);
// (the count back at nothing, and after it how many fragments there is room for)
vk::BufferCopy copy(0, 0, 2 * sizeof(u32));
commandBuffer.copyBuffer(*pixelCounterReset->buffer, *pixelCounter->buffer, 1, &copy);
/* ...and the next render pass's shaders must see it back at nothing. (From upstream.) */
barrier.srcAccessMask = vk::AccessFlagBits::eTransferWrite;
barrier.dstAccessMask = vk::AccessFlagBits::eShaderRead | vk::AccessFlagBits::eShaderWrite;
commandBuffer.pipelineBarrier(vk::PipelineStageFlagBits::eTransfer, vk::PipelineStageFlagBits::eFragmentShader,
{}, nullptr, barrier, nullptr);
}
/* How many fragments the render pass just ended asked for, those that
* found no room too (the shaders count them all), copied out for the
* CPU to read when this frame slot comes round again: see ChooseBands().
* Nothing waits for it. */
void CountPass(vk::CommandBuffer commandBuffer)
{
if (countSlot >= CountSlots || written[countSlot] >= Counts)
return;
vk::BufferMemoryBarrier barrier(vk::AccessFlagBits::eShaderWrite, vk::AccessFlagBits::eTransferRead,
VK_QUEUE_FAMILY_IGNORED, VK_QUEUE_FAMILY_IGNORED, *pixelCounter->buffer, 0, VK_WHOLE_SIZE);
commandBuffer.pipelineBarrier(vk::PipelineStageFlagBits::eFragmentShader, vk::PipelineStageFlagBits::eTransfer,
{}, nullptr, barrier, nullptr);
const vk::DeviceSize offset = (countSlot * Counts + written[countSlot]) * sizeof(u32);
vk::BufferCopy copy(0, offset, sizeof(u32));
commandBuffer.copyBuffer(*pixelCounter->buffer, *counts->buffer, 1, &copy);
// (and the counter is not reset before it has been read)
vk::BufferMemoryBarrier toHost(vk::AccessFlagBits::eTransferWrite, vk::AccessFlagBits::eHostRead,
VK_QUEUE_FAMILY_IGNORED, VK_QUEUE_FAMILY_IGNORED, *counts->buffer, offset, sizeof(u32));
commandBuffer.pipelineBarrier(vk::PipelineStageFlagBits::eTransfer, vk::PipelineStageFlagBits::eTransfer | vk::PipelineStageFlagBits::eHost,
{}, nullptr, toHost, nullptr);
written[countSlot]++;
}
/* The bands a frame about to be drawn in frame slot @slot is drawn in,
* a render to a texture's if @rtt (rend/oit_bands.h), the slot's fence
* waited for by the command pool: what the frame drawn there before
* counted goes to the bands of its kind, whichever drawer drew it */
u32 ChooseBands(int slot, bool rtt)
{
countSlot = slot >= 0 && slot < (int)CountSlots ? (u32)slot : CountSlots;
if (countSlot == CountSlots)
return bandState.bands[rtt ? 1 : 0];
if (written[countSlot] != 0)
{
const u32 *count = (const u32 *)counts->MapMemory() + countSlot * Counts;
const u32 kind = bandState.rtt[countSlot];
u32 fragments = 0;
for (u32 i = 0; i < written[countSlot]; i++)
fragments = std::max(fragments, count[i]);
if (oit_bands_count(&bandState, countSlot, fragments, capacity))
NOTICE_LOG(RENDERER, "Vulkan per-pixel renderer: %u translucent fragments in a band of %u%s, room for %u: %u band%s",
fragments, std::max(bandState.drawn_in[countSlot], 1u), kind ? " (a render to a texture)" : "",
capacity, bandState.bands[kind], bandState.bands[kind] > 1 ? "s" : "");
}
written[countSlot] = 0;
return oit_bands_begin(&bandState, countSlot, rtt);
}
// ...and the bands this one is drawn in
void SetBands(u32 bands)
{
if (countSlot < CountSlots)
bandState.drawn_in[countSlot] = bands;
}
vk::DeviceSize GetSize() const { return bufferSize; }
void Term()
{
for (auto& buffer : pixelBuffers)
buffer.reset();
bufferSize = 0;
pixelCounter.reset();
pixelCounterReset.reset();
counts.reset();
abufferPointerAttachment.reset();
sumsAttachment.reset();
descSet.reset();
maxWidth = maxHeight = 0;
oit_bands_init(&bandState);
}
vk::DescriptorSetLayout GetDescriptorSetLayout() const { return *descSetLayout; }
bool isFirstFrameAfterInit() const { return firstFrameAfterInit; }
private:
static oit_bands_t MakeBandState()
{
oit_bands_t b;
oit_bands_init(&b);
return b;
}
static vk::DeviceSize BindingLimit()
{
const VulkanContext *context = VulkanContext::Instance();
return std::min<vk::DeviceSize>(context->GetMaxMemoryAllocationSize(), context->GetMaxStorageBufferRange())
& ~(vk::DeviceSize)(OIT_FRAGMENT_BYTES - 1);
}
/* The fragment buffer, of plannedSize: in as few storage buffers as
* the device allows, all of one size. The ones it replaces are retired
* like the pointer image, and so is the counter's reset value, which
* says how many fragments there is room for. */
void MakePixelBuffers(CommandPool *commandPool)
{
const vk::DeviceSize limit = BindingLimit();
bindings = (u32)std::min<vk::DeviceSize>((plannedSize + limit - 1) / limit, Bindings);
bindingSize = (plannedSize / bindings) & ~(vk::DeviceSize)(OIT_FRAGMENT_BYTES - 1);
bufferSize = plannedSize;
capacity = (u32)std::min<vk::DeviceSize>(bindingSize / OIT_FRAGMENT_BYTES * bindings, 0xffffffffu);
for (u32 i = 0; i < Bindings; i++)
{
if (pixelBuffers[i])
commandPool->DeferDelete(std::move(pixelBuffers[i]));
if (i < bindings)
pixelBuffers[i] = std::unique_ptr<BufferData>(new BufferData(bindingSize,
vk::BufferUsageFlagBits::eStorageBuffer, vk::MemoryPropertyFlagBits::eDeviceLocal));
}
if (!pixelCounter)
pixelCounter = std::unique_ptr<BufferData>(new BufferData(2 * sizeof(u32),
vk::BufferUsageFlagBits::eStorageBuffer | vk::BufferUsageFlagBits::eTransferDst | vk::BufferUsageFlagBits::eTransferSrc,
vk::MemoryPropertyFlagBits::eDeviceLocal));
if (pixelCounterReset)
commandPool->DeferDelete(std::move(pixelCounterReset));
pixelCounterReset = std::unique_ptr<BufferData>(new BufferData(2 * sizeof(u32), vk::BufferUsageFlagBits::eTransferSrc));
const u32 reset[2] = { 0, capacity };
pixelCounterReset->upload(sizeof(reset), reset);
NOTICE_LOG(RENDERER, "Vulkan per-pixel renderer: fragment buffer of %u MB in %u storage buffer%s (room for %u fragments)",
(u32)(bufferSize >> 20), bindings, bindings > 1 ? "s" : "", capacity);
}
vk::UniqueDescriptorSet descSet;
vk::UniqueDescriptorSetLayout descSetLayout;
std::unique_ptr<BufferData> pixelBuffers[Bindings];
bool compact = false;
vk::DeviceSize plannedSize = 0;
vk::DeviceSize bufferSize = 0;
vk::DeviceSize bindingSize = 0;
u32 bindings = 0;
u32 capacity = 0;
std::unique_ptr<BufferData> pixelCounter;
std::unique_ptr<BufferData> pixelCounterReset;
std::unique_ptr<FramebufferAttachment> abufferPointerAttachment;
std::unique_ptr<FramebufferAttachment> sumsAttachment;
bool sums = false;
bool abufferPointerTransitionNeeded = false;
bool firstFrameAfterInit = false;
int maxWidth = 0;
int maxHeight = 0;
// the counts copied out, Counts for each frame slot; CPU side
std::unique_ptr<BufferData> counts;
u32 written[CountSlots] = {};
u32 countSlot = CountSlots;
// the bands of the screen's frames and of renders to a texture, and what each slot's was drawn in
oit_bands_t bandState = MakeBandState();
};