mirror of
https://github.com/libretro/parallel-dc.git
synced 2026-11-09 17:28:17 -10:00
The read back of a render to a texture runs in the SIGSEGV handler, and made its batches there the first time and grew a copy's buffer and shrink image there for a larger render. The batches are made with the renderer now (vk_rtt_init()), their command buffers allocated in place, and every copy's room is grown when a render is watched (copies_reserve()), on the render thread. The dynarec's write to a page of code discards the page's blocks straight from its list, without copying it, into a list of discards reserved at bm_Init(); a state loaded over the code (bm_LoadRam(), every frame with run-ahead) does the same. vk_alloc_sites.py fails a site reached from the core's signal handler whatever the allowlist says, and the allowlist loses the four the handler had, taking the copies' growth as vk_rtt_watch.
418 lines
14 KiB
C++
418 lines
14 KiB
C++
#define PVR_REGS_FOR_RENDERER // see hw/pvr/pvr_regs.h
|
|
/* See rtt_read.h. */
|
|
#include "rtt_read.h"
|
|
#include "rend/rtt_watch.h"
|
|
#include "vulkan.h"
|
|
#include "texture.h"
|
|
#include "commandpool.h"
|
|
#include "hw/pvr/pvr_regs.h"
|
|
#include <algorithm>
|
|
#include <memory>
|
|
|
|
static CommandPool read_pool; /* vk_read_picture()'s */
|
|
static bool read_pool_ready;
|
|
|
|
/* Copies of pictures off the card, each into memory this side reads: begun
|
|
* ahead of the read where rtt_watch says which (vk_rtt_prefetch()), or by
|
|
* the read itself. A batch of them is one submit with one fence. The read
|
|
* runs in the signal handler, where nothing may be allocated: the batches
|
|
* are made with the renderer (vk_rtt_init()), and every copy's buffer is
|
|
* grown to a render's size when it is watched (copies_reserve()). */
|
|
#define RTT_COPIES 18
|
|
#define RTT_BATCHES 4
|
|
|
|
struct RttBatch
|
|
{
|
|
vk::UniqueCommandPool pool;
|
|
vk::CommandBuffer cmd; /* the pool's: freed with it */
|
|
vk::UniqueFence fence;
|
|
bool busy; /* submitted, its fence not seen signalled */
|
|
};
|
|
|
|
struct RttCopy
|
|
{
|
|
u32 serial; /* the render's (RttWatch::serial), 0 if none */
|
|
uintptr_t tex;
|
|
u32 w, h;
|
|
int batch; /* the batch that writes it */
|
|
u64 stamp; /* when it was last begun, for the oldest */
|
|
std::unique_ptr<BufferData> buffer; /* grown, never shrunk */
|
|
std::unique_ptr<FramebufferAttachment> shrink; /* the console's size, for a finer picture */
|
|
};
|
|
|
|
static RttBatch batches[RTT_BATCHES];
|
|
static RttCopy copies[RTT_COPIES];
|
|
static bool copies_ready;
|
|
static u64 copy_stamp;
|
|
/* what every copy has room for: the console's pixels, and a shrink image */
|
|
static vk::DeviceSize copies_bytes;
|
|
static u32 shrink_w, shrink_h;
|
|
|
|
static bool batch_done(RttBatch& b)
|
|
{
|
|
if (b.busy && VulkanContext::Instance()->GetDevice().getFenceStatus(*b.fence) == vk::Result::eSuccess)
|
|
b.busy = false;
|
|
return !b.busy;
|
|
}
|
|
|
|
static void copies_init(void)
|
|
{
|
|
if (copies_ready)
|
|
return;
|
|
vk::Device device = VulkanContext::Instance()->GetDevice();
|
|
for (RttBatch& b : batches)
|
|
{
|
|
b.pool = device.createCommandPoolUnique(vk::CommandPoolCreateInfo(vk::CommandPoolCreateFlagBits::eTransient
|
|
| vk::CommandPoolCreateFlagBits::eResetCommandBuffer, VulkanContext::Instance()->GetGraphicsQueueFamilyIndex()));
|
|
const vk::CommandBufferAllocateInfo info(*b.pool, vk::CommandBufferLevel::ePrimary, 1);
|
|
(void)device.allocateCommandBuffers(&info, &b.cmd);
|
|
b.fence = device.createFenceUnique(vk::FenceCreateInfo());
|
|
b.busy = false;
|
|
}
|
|
for (RttCopy& c : copies)
|
|
{
|
|
c.serial = 0;
|
|
c.batch = -1;
|
|
}
|
|
copies_bytes = 0;
|
|
shrink_w = shrink_h = 0;
|
|
copies_ready = true;
|
|
}
|
|
|
|
/* Every copy's room grown, where a render to be watched needs more: its
|
|
* buffer for w by h pixels, and its shrink image, where the render is
|
|
* finer than the console's (@scale above 1) */
|
|
static void copies_reserve(u32 w, u32 h, u32 scale)
|
|
{
|
|
VulkanContext *context = VulkanContext::Instance();
|
|
const vk::DeviceSize bytes = (vk::DeviceSize)w * h * 4;
|
|
const bool shrink = scale > 1 && (w > shrink_w || h > shrink_h);
|
|
|
|
if (bytes <= copies_bytes && !shrink)
|
|
return;
|
|
/* (one being written by a batch on the card keeps its old room until
|
|
* it is done: every batch waited for, rarely - a larger render) */
|
|
for (RttBatch& b : batches)
|
|
if (b.busy)
|
|
{
|
|
context->GetDevice().waitForFences(1, &b.fence.get(), true, UINT64_MAX);
|
|
b.busy = false;
|
|
}
|
|
if (bytes > copies_bytes)
|
|
copies_bytes = bytes;
|
|
if (shrink)
|
|
{
|
|
shrink_w = std::max(shrink_w, w);
|
|
shrink_h = std::max(shrink_h, h);
|
|
}
|
|
for (RttCopy& c : copies)
|
|
{
|
|
if (!c.buffer || c.buffer->bufferSize < copies_bytes)
|
|
c.buffer = std::unique_ptr<BufferData>(new BufferData(copies_bytes, vk::BufferUsageFlagBits::eTransferDst));
|
|
if (shrink && (!c.shrink || c.shrink->getExtent().width < shrink_w || c.shrink->getExtent().height < shrink_h))
|
|
{
|
|
c.shrink = std::unique_ptr<FramebufferAttachment>(new FramebufferAttachment(context->GetPhysicalDevice(), context->GetDevice()));
|
|
/* (sampled: its view, made with it, needs a use a view is for) */
|
|
c.shrink->Init(shrink_w, shrink_h, vk::Format::eR8G8B8A8Unorm, vk::ImageUsageFlagBits::eTransferSrc
|
|
| vk::ImageUsageFlagBits::eTransferDst | vk::ImageUsageFlagBits::eSampled, false);
|
|
}
|
|
/* (what it held was copied into the room it had: gone) */
|
|
c.serial = 0;
|
|
}
|
|
}
|
|
|
|
/* A batch free to record into: none if every one is still on the card
|
|
* and @wait is false; with @wait, the oldest waited for. */
|
|
static int batch_take(bool wait)
|
|
{
|
|
for (int i = 0; i < RTT_BATCHES; i++)
|
|
if (batch_done(batches[i]))
|
|
return i;
|
|
if (!wait)
|
|
return -1;
|
|
vk::Device device = VulkanContext::Instance()->GetDevice();
|
|
device.waitForFences(1, &batches[0].fence.get(), true, UINT64_MAX);
|
|
batches[0].busy = false;
|
|
return 0;
|
|
}
|
|
|
|
static RttCopy *copy_of(const RttWatch *watch)
|
|
{
|
|
for (RttCopy& c : copies)
|
|
if (c.serial == watch->serial && c.serial != 0 && c.tex == watch->tex)
|
|
return &c;
|
|
return nullptr;
|
|
}
|
|
|
|
/* One to begin a copy in: one that holds none, or the one begun longest
|
|
* ago - never one a batch on the card is still writing, nor one begun
|
|
* after @since (in the batch being recorded). */
|
|
static RttCopy *copy_free(u64 since)
|
|
{
|
|
RttCopy *oldest = nullptr;
|
|
|
|
for (RttCopy& c : copies)
|
|
{
|
|
if (c.batch >= 0 && !batch_done(batches[c.batch]))
|
|
continue;
|
|
if (c.serial != 0 && c.stamp > since)
|
|
continue;
|
|
if (c.serial == 0)
|
|
return &c;
|
|
if (oldest == nullptr || c.stamp < oldest->stamp)
|
|
oldest = &c;
|
|
}
|
|
return oldest;
|
|
}
|
|
|
|
/* The picture as the console would have it, w by h, R G B A, copied into
|
|
* c's buffer by @cmd: from the texture at the clip; or made the console's
|
|
* size on the card first, filtered, where it is finer. */
|
|
static void record_copy(vk::CommandBuffer cmd, const RttWatch *watch, RttCopy& c)
|
|
{
|
|
Texture *texture = (Texture *)watch->tex;
|
|
VulkanContext *context = VulkanContext::Instance();
|
|
const u32 w = watch->w, h = watch->h, scale = watch->scale;
|
|
const s32 x0 = (s32)(watch->x0 * (scale > 1 ? scale : 1)), y0 = (s32)(watch->y0 * (scale > 1 ? scale : 1));
|
|
const vk::Format format = vk::Format::eR8G8B8A8Unorm;
|
|
const vk::ImageSubresourceLayers layers(vk::ImageAspectFlagBits::eColor, 0, 0, 1);
|
|
|
|
/* (its room made when the render was watched: copies_reserve()) */
|
|
(void)context;
|
|
vk::Image source = texture->GetImage();
|
|
setImageLayout(cmd, source, format, 1, vk::ImageLayout::eShaderReadOnlyOptimal, vk::ImageLayout::eTransferSrcOptimal);
|
|
if (scale > 1)
|
|
{
|
|
const std::array<vk::Offset3D, 2> from = { vk::Offset3D(x0, y0, 0), vk::Offset3D(x0 + w * scale, y0 + h * scale, 1) };
|
|
const std::array<vk::Offset3D, 2> to = { vk::Offset3D(0, 0, 0), vk::Offset3D(w, h, 1) };
|
|
|
|
setImageLayout(cmd, c.shrink->GetImage(), format, 1, vk::ImageLayout::eUndefined, vk::ImageLayout::eTransferDstOptimal);
|
|
cmd.blitImage(source, vk::ImageLayout::eTransferSrcOptimal, c.shrink->GetImage(), vk::ImageLayout::eTransferDstOptimal,
|
|
vk::ImageBlit(layers, from, layers, to), vk::Filter::eLinear);
|
|
setImageLayout(cmd, c.shrink->GetImage(), format, 1, vk::ImageLayout::eTransferDstOptimal, vk::ImageLayout::eTransferSrcOptimal);
|
|
source = c.shrink->GetImage();
|
|
}
|
|
/* (from the console-sized image at 0; from the texture at the clip) */
|
|
const vk::Offset3D at = scale > 1 ? vk::Offset3D(0, 0, 0) : vk::Offset3D(x0, y0, 0);
|
|
cmd.copyImageToBuffer(source, vk::ImageLayout::eTransferSrcOptimal, *c.buffer->buffer,
|
|
vk::BufferImageCopy(0, w, h, layers, at, vk::Extent3D(w, h, 1)));
|
|
setImageLayout(cmd, texture->GetImage(), format, 1, vk::ImageLayout::eTransferSrcOptimal, vk::ImageLayout::eShaderReadOnlyOptimal);
|
|
c.serial = watch->serial;
|
|
c.tex = watch->tex;
|
|
c.w = w;
|
|
c.h = h;
|
|
c.stamp = ++copy_stamp;
|
|
}
|
|
|
|
/* Copies of @watches recorded into batch @b and submitted, those that have
|
|
* one already left alone; false if there was nothing to copy. */
|
|
static bool batch_copies(int b, const RttWatch *const *watches, unsigned count)
|
|
{
|
|
RttBatch& batch = batches[b];
|
|
const u64 since = copy_stamp;
|
|
bool begun = false;
|
|
|
|
for (unsigned i = 0; i < count; i++)
|
|
{
|
|
if (copy_of(watches[i]) != nullptr)
|
|
continue;
|
|
RttCopy *c = copy_free(since);
|
|
if (c == nullptr)
|
|
break;
|
|
if (!begun)
|
|
{
|
|
batch.cmd.reset(vk::CommandBufferResetFlags());
|
|
batch.cmd.begin(vk::CommandBufferBeginInfo(vk::CommandBufferUsageFlagBits::eOneTimeSubmit));
|
|
begun = true;
|
|
}
|
|
record_copy(batch.cmd, watches[i], *c);
|
|
c->batch = b;
|
|
}
|
|
if (!begun)
|
|
return false;
|
|
/* (one barrier for every buffer: the host reads them after the fence) */
|
|
const vk::MemoryBarrier barrier(vk::AccessFlagBits::eTransferWrite, vk::AccessFlagBits::eHostRead);
|
|
batch.cmd.pipelineBarrier(vk::PipelineStageFlagBits::eTransfer, vk::PipelineStageFlagBits::eHost, {}, barrier, nullptr, nullptr);
|
|
batch.cmd.end();
|
|
VulkanContext::Instance()->GetDevice().resetFences(1, &batch.fence.get());
|
|
VulkanContext::Instance()->SubmitCommandBuffers(1, &batch.cmd, *batch.fence);
|
|
batch.busy = true;
|
|
return true;
|
|
}
|
|
|
|
/* rtt_watch's: begun now, not waited for; nothing if no batch is free */
|
|
static void vk_rtt_prefetch(const RttWatch *const *watches, unsigned count)
|
|
{
|
|
const int b = batch_take(false);
|
|
if (b >= 0)
|
|
batch_copies(b, watches, count);
|
|
}
|
|
|
|
/* The picture, from its copy - begun ahead as often as not, and done by
|
|
* now - or from one begun here and waited for. */
|
|
static void vk_rtt_read(const RttWatch *watch, u8 *scratch, RttWatchTake take)
|
|
{
|
|
RttCopy *c = copy_of(watch);
|
|
if (c == nullptr)
|
|
{
|
|
if (!batch_copies(batch_take(true), &watch, 1))
|
|
{
|
|
/* every copy is being written: all of them waited for */
|
|
vk::Device device = VulkanContext::Instance()->GetDevice();
|
|
for (RttBatch& b : batches)
|
|
if (b.busy)
|
|
{
|
|
device.waitForFences(1, &b.fence.get(), true, UINT64_MAX);
|
|
b.busy = false;
|
|
}
|
|
batch_copies(0, &watch, 1);
|
|
}
|
|
c = copy_of(watch);
|
|
}
|
|
RttBatch& batch = batches[c->batch];
|
|
if (batch.busy)
|
|
{
|
|
VulkanContext::Instance()->GetDevice().waitForFences(1, &batch.fence.get(), true, UINT64_MAX);
|
|
batch.busy = false;
|
|
}
|
|
const BufferData *data = c->buffer.get();
|
|
/* Read where the copy put it, with the host's cache in front of it (as
|
|
* the heap prefers for what is read back); memory without is slow to
|
|
* read a byte at a time, and is copied out first. */
|
|
if (data->allocation.PropertyFlags() & VK_MEMORY_PROPERTY_HOST_CACHED_BIT)
|
|
take(watch, (const u8 *)data->allocation.MapMemory(), (size_t)c->w * 4);
|
|
else
|
|
{
|
|
data->download(c->w * c->h * 4, scratch);
|
|
take(watch, scratch, (size_t)c->w * 4);
|
|
}
|
|
c->serial = 0;
|
|
}
|
|
|
|
static void copies_term(void)
|
|
{
|
|
if (!copies_ready)
|
|
return;
|
|
vk::Device device = VulkanContext::Instance()->GetDevice();
|
|
for (RttBatch& b : batches)
|
|
{
|
|
if (b.busy)
|
|
device.waitForFences(1, &b.fence.get(), true, UINT64_MAX);
|
|
b.busy = false;
|
|
b.cmd = nullptr;
|
|
b.pool.reset();
|
|
b.fence.reset();
|
|
}
|
|
for (RttCopy& c : copies)
|
|
{
|
|
c.buffer.reset();
|
|
c.shrink.reset();
|
|
c.serial = 0;
|
|
c.batch = -1;
|
|
}
|
|
copies_ready = false;
|
|
}
|
|
|
|
void vk_read_picture(vk::Image image, vk::Format format, u32 width, u32 height, u8 *rgba)
|
|
{
|
|
VulkanContext *context = VulkanContext::Instance();
|
|
const vk::ImageSubresourceLayers layers(vk::ImageAspectFlagBits::eColor, 0, 0, 1);
|
|
BufferData buffer((vk::DeviceSize)width * height * 4, vk::BufferUsageFlagBits::eTransferDst);
|
|
|
|
read_pool.Init();
|
|
read_pool_ready = true;
|
|
read_pool.BeginFrame();
|
|
vk::CommandBuffer cmd = read_pool.Allocate();
|
|
cmd.begin(vk::CommandBufferBeginInfo(vk::CommandBufferUsageFlagBits::eOneTimeSubmit));
|
|
setImageLayout(cmd, image, format, 1, vk::ImageLayout::eShaderReadOnlyOptimal, vk::ImageLayout::eTransferSrcOptimal);
|
|
cmd.copyImageToBuffer(image, vk::ImageLayout::eTransferSrcOptimal, *buffer.buffer,
|
|
vk::BufferImageCopy(0, width, height, layers, vk::Offset3D(0, 0, 0), vk::Extent3D(width, height, 1)));
|
|
vk::BufferMemoryBarrier barrier(vk::AccessFlagBits::eTransferWrite, vk::AccessFlagBits::eHostRead,
|
|
VK_QUEUE_FAMILY_IGNORED, VK_QUEUE_FAMILY_IGNORED, *buffer.buffer, 0, VK_WHOLE_SIZE);
|
|
cmd.pipelineBarrier(vk::PipelineStageFlagBits::eTransfer, vk::PipelineStageFlagBits::eHost, {}, nullptr, barrier, nullptr);
|
|
setImageLayout(cmd, image, format, 1, vk::ImageLayout::eTransferSrcOptimal, vk::ImageLayout::eShaderReadOnlyOptimal);
|
|
cmd.end();
|
|
read_pool.EndFrame();
|
|
|
|
vk::Fence fence = read_pool.GetCurrentFence();
|
|
context->GetDevice().waitForFences(1, &fence, true, UINT64_MAX);
|
|
buffer.download(width * height * 4, rgba);
|
|
|
|
if (format == vk::Format::eB8G8R8A8Unorm || format == vk::Format::eB8G8R8A8Srgb)
|
|
for (u32 i = 0; i < width * height; i++)
|
|
{
|
|
const u8 blue = rgba[i * 4];
|
|
rgba[i * 4] = rgba[i * 4 + 2];
|
|
rgba[i * 4 + 2] = blue;
|
|
}
|
|
}
|
|
|
|
/* The texture cache's, never this module's to delete. */
|
|
static void vk_rtt_release(uintptr_t)
|
|
{
|
|
}
|
|
|
|
static const RttWatchBackend vk_rtt_backend = { vk_rtt_read, vk_rtt_release, vk_rtt_prefetch };
|
|
|
|
/* What the render covers of video memory, as EndRenderPass() has it: the
|
|
* clip's pixels, from (x0, y0) of the frame at addr, which moves to the
|
|
* first of them. */
|
|
static void covered(u32& addr, u32 x0, u32 y0, u32& width, u32 height, u32& stride, u32& bytes)
|
|
{
|
|
stride = FB_W_LINESTRIDE.stride * 8;
|
|
if (stride == 0)
|
|
stride = (x0 + width) * 2;
|
|
else if ((x0 + width) * 2 > stride)
|
|
// Happens for Virtua Tennis
|
|
width = x0 * 2 >= stride ? 0 : stride / 2 - x0;
|
|
addr += y0 * stride + x0 * 2;
|
|
bytes = width == 0 || height == 0 ? 0 : stride * (height - 1) + width * 2;
|
|
}
|
|
|
|
void vk_rtt_supersede(u32 addr, u32 x0, u32 y0, u32 width, u32 height)
|
|
{
|
|
u32 stride, bytes;
|
|
|
|
covered(addr, x0, y0, width, height, stride, bytes);
|
|
if (bytes != 0)
|
|
rtt_watch_supersede(addr, bytes);
|
|
}
|
|
|
|
void vk_rtt_watch(Texture *texture, u32 addr, u32 x0, u32 y0, u32 width, u32 height)
|
|
{
|
|
RttWatch watch = {};
|
|
|
|
covered(addr, x0, y0, width, height, watch.stride, watch.bytes);
|
|
watch.addr = addr;
|
|
watch.w = width;
|
|
watch.h = height;
|
|
watch.x0 = x0;
|
|
watch.y0 = y0;
|
|
watch.packmode = FB_W_CTRL.fb_packmode;
|
|
watch.kval_bit = (FB_W_CTRL.fb_kval & 0x80) << 8;
|
|
watch.alpha_threshold = FB_W_CTRL.fb_alpha_threshold;
|
|
watch.dither = FB_W_CTRL.fb_dither;
|
|
watch.tex = (uintptr_t)texture;
|
|
watch.target = texture;
|
|
watch.scale = settings.rend.RenderToTextureUpscale > 1 ? settings.rend.RenderToTextureUpscale : 1;
|
|
if (watch.bytes == 0)
|
|
return;
|
|
copies_init();
|
|
copies_reserve(watch.w, watch.h, watch.scale);
|
|
rtt_watch_add(&watch, &vk_rtt_backend);
|
|
}
|
|
|
|
void vk_rtt_init(void)
|
|
{
|
|
copies_init();
|
|
}
|
|
|
|
void vk_rtt_term(void)
|
|
{
|
|
rtt_watch_term();
|
|
copies_term();
|
|
if (read_pool_ready)
|
|
read_pool.Term();
|
|
read_pool_ready = false;
|
|
}
|