mirror of
https://github.com/ayozetr/bloodborne_linux.git
synced 2026-11-08 17:03:48 -10:00
The presenter's frame fence and the swapchain acquire were waited for without a limit, so a GPU that never finished a frame, or a compositor that kept every image, froze the game with nothing from them in the log (the Linux fork's #5: frozen loading screens, GPU at 0 %, NVIDIA Xid 109). They are now waited for in 10 s slices: past the first one the log says which wait it is, once, with the GPU breadcrumbs for a frame, and again when it does finish. The GPU watchdog already did this for submitted work.
880 lines
40 KiB
C++
880 lines
40 KiB
C++
// SPDX-FileCopyrightText: Copyright 2026 bbport contributors
|
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
#include "render/presenter.h"
|
|
#include "shim/bb_env.h"
|
|
|
|
#include <algorithm>
|
|
#include <atomic>
|
|
#include <chrono>
|
|
#include <thread>
|
|
#include <cstdio>
|
|
#include <cstdlib>
|
|
#include <limits>
|
|
|
|
#include <vk_mem_alloc.h>
|
|
|
|
#include "bbport_game_menu.h"
|
|
#include "bbport_overlay.h"
|
|
#include "bbport_timeline.h"
|
|
#include "bbport_toggles.h"
|
|
#include "core/emulator_settings.h"
|
|
#include "sdl_window.h"
|
|
#include "textures/image.h"
|
|
#include "textures/texture_cache.h"
|
|
#include "bbport_settings.h"
|
|
#include "render/vk_breadcrumbs.h"
|
|
#include "render/vk_low_latency.h"
|
|
#include "upscale/vk_frame_generation.h"
|
|
#include "upscale/vk_temporal_upscaler.h"
|
|
#include "render/rasterizer.h"
|
|
#include "vulkan/platform.h"
|
|
|
|
namespace Vulkan {
|
|
|
|
namespace {
|
|
|
|
constexpr vk::ImageSubresourceRange ColorRange{vk::ImageAspectFlagBits::eColor, 0, 1, 0, 1};
|
|
|
|
/// How the display buffer is read: its format as a view (the 10-bit sRGB one has no sRGB
|
|
/// Vulkan format: it is decoded in the encode pass).
|
|
/// The shown frame's aspect (bbgpu_frame_aspect): any thread.
|
|
std::atomic<float> shown_aspect{16.0f / 9.0f};
|
|
|
|
vk::Format FrameViewFormat(BbVideoOut::PixelFormat format) {
|
|
switch (format) {
|
|
case BbVideoOut::PixelFormat::A8B8G8R8Srgb:
|
|
return vk::Format::eR8G8B8A8Srgb;
|
|
case BbVideoOut::PixelFormat::A8R8G8B8Srgb:
|
|
return vk::Format::eB8G8R8A8Srgb;
|
|
case BbVideoOut::PixelFormat::A2R10G10B10:
|
|
case BbVideoOut::PixelFormat::A2R10G10B10Srgb:
|
|
case BbVideoOut::PixelFormat::A2R10G10B10Bt2020Pq:
|
|
return vk::Format::eA2R10G10B10UnormPack32;
|
|
default:
|
|
UNREACHABLE_MSG("display buffer format {:#x}", u32(format));
|
|
}
|
|
}
|
|
|
|
/// `frame` fitted into `target` keeping its aspect, centered.
|
|
vk::Rect2D Letterbox(vk::Extent2D frame, vk::Extent2D target) {
|
|
const float frame_aspect = float(frame.width) / float(frame.height);
|
|
const float target_aspect = float(target.width) / float(target.height);
|
|
vk::Extent2D size = target;
|
|
if (frame_aspect > target_aspect) {
|
|
size.height = u32(float(target.width) / frame_aspect);
|
|
} else {
|
|
size.width = u32(float(target.height) * frame_aspect);
|
|
}
|
|
return {{s32(target.width - size.width) / 2, s32(target.height - size.height) / 2}, size};
|
|
}
|
|
|
|
/// BB_FRAMES_AHEAD (default 1): how many guest frames the command processor thread may run
|
|
/// ahead of the GPU (0: unbounded).
|
|
u32 FramesAhead() {
|
|
static const u32 value = [] {
|
|
const char* env = bb_env("BB_FRAMES_AHEAD");
|
|
return env ? u32(std::max(0, std::atoi(env))) : 1u;
|
|
}();
|
|
return value;
|
|
}
|
|
|
|
// bbport (Linux fork, #5): in 10 s slices. Past the first one the log says so, once per wait,
|
|
// with the GPU breadcrumbs (which command it has not finished), and again when it does finish:
|
|
// waited for without a limit, a GPU that never finished a frame froze the game with nothing in
|
|
// the log (frozen loading screens, GPU at 0 %, NVIDIA Xid 109).
|
|
void WaitFence(vk::Device device, vk::Fence fence) {
|
|
bool told = false;
|
|
while (true) {
|
|
const vk::Result result = device.waitForFences(fence, true, 10'000'000'000ull);
|
|
if (result == vk::Result::eSuccess) {
|
|
if (told) {
|
|
std::printf("Presenter: the frame finished after all\n");
|
|
std::fflush(stdout);
|
|
}
|
|
return;
|
|
}
|
|
if (result != vk::Result::eTimeout) {
|
|
Check<"wait for a frame">(result);
|
|
}
|
|
if (!told) {
|
|
told = true;
|
|
std::printf("Presenter: a frame not finished after 10 s: the GPU is stuck on it\n");
|
|
std::fflush(stdout);
|
|
Breadcrumbs::ReportStuck("a frame the presenter waits for");
|
|
}
|
|
}
|
|
}
|
|
|
|
} // namespace
|
|
|
|
Presenter::Presenter(Frontend::WindowSDL& window_, BbCp::CommandProcessor* cp_)
|
|
: window{window_},
|
|
instance{window, EmulatorSettings.GetGpuId(), EmulatorSettings.IsVkValidationEnabled(),
|
|
EmulatorSettings.IsVkCrashDiagnosticEnabled()},
|
|
cp{cp_}, draw_scheduler{instance, true}, present_scheduler{instance},
|
|
swapchain{instance, window}, runtime{instance, draw_scheduler},
|
|
rasterizer{std::make_unique<Rasterizer>(instance, draw_scheduler, runtime, cp)},
|
|
texture_cache{rasterizer->GetTextureCache()},
|
|
encode_pass{instance.GetDevice(), swapchain.GetSurfaceFormat().format} {
|
|
const vk::Device device = instance.GetDevice();
|
|
const u32 count = swapchain.GetImageCount();
|
|
frames.resize(count);
|
|
for (u32 i = 0; i < count; ++i) {
|
|
frames[i].id = u8(i);
|
|
frames[i].present_done = Check<"create a frame fence">(
|
|
device.createFence({.flags = vk::FenceCreateFlagBits::eSignaled}));
|
|
free_frames.push_back(&frames[i]);
|
|
}
|
|
BbOverlay::Init(instance, swapchain.GetSurfaceFormat().format, count);
|
|
// Low latency (NVIDIA Reflex): the draw scheduler's submissions are the game's frames.
|
|
LowLatency::Get().Initialize(instance);
|
|
draw_scheduler.SetRendersFrames(true);
|
|
}
|
|
|
|
Presenter::~Presenter() {
|
|
draw_scheduler.Finish();
|
|
present_scheduler.Finish();
|
|
const vk::Device device = instance.GetDevice();
|
|
for (auto& frame : frames) {
|
|
device.destroyImageView(frame.image_view);
|
|
if (frame.image) {
|
|
vmaDestroyImage(instance.GetAllocator(), frame.image, frame.allocation);
|
|
}
|
|
DestroyInterpolated(frame);
|
|
device.destroyFence(frame.present_done);
|
|
}
|
|
}
|
|
|
|
void Presenter::DestroyInterpolated(Frame& frame) {
|
|
const vk::Device device = instance.GetDevice();
|
|
for (u32 i = 0; i < Frame::MaxInterp; ++i) {
|
|
device.destroyImageView(frame.interp_view[i]);
|
|
if (frame.interp_image[i]) {
|
|
vmaDestroyImage(instance.GetAllocator(), frame.interp_image[i], frame.interp_allocation[i]);
|
|
}
|
|
frame.interp_image[i] = vk::Image{};
|
|
frame.interp_view[i] = vk::ImageView{};
|
|
frame.interp_allocation[i] = {};
|
|
}
|
|
frame.interp_count = 0;
|
|
}
|
|
|
|
void Presenter::SetHDR(bool enable) {
|
|
if (!IsHDRSupported()) {
|
|
return;
|
|
}
|
|
if (swapchain.GetHDR() != enable) {
|
|
// Changing HDR recreates the swapchain and waits for the device: exclude queue submits.
|
|
std::scoped_lock lock{Scheduler::submit_mutex};
|
|
swapchain.SetHDR(enable);
|
|
}
|
|
hdr.store(enable, std::memory_order_relaxed);
|
|
}
|
|
|
|
VideoCore::Image& Presenter::RegisterVideoOutSurface(
|
|
const BbVideoOut::BufferAttributeGroup& attribute, VAddr cpu_address) {
|
|
auto desc = VideoCore::TextureCache::ImageDesc{attribute, cpu_address};
|
|
const auto image_id = texture_cache.FindImage(desc);
|
|
auto& image = texture_cache.GetImage(image_id);
|
|
image.usage.vo_surface = 1u;
|
|
return image;
|
|
}
|
|
|
|
void Presenter::RecreateFrame(Frame& frame, u32 width, u32 height) {
|
|
const vk::Device device = instance.GetDevice();
|
|
device.destroyImageView(frame.image_view);
|
|
if (frame.image) {
|
|
vmaDestroyImage(instance.GetAllocator(), frame.image, frame.allocation);
|
|
}
|
|
const vk::Format format = swapchain.GetSurfaceFormat().format;
|
|
const VkImageCreateInfo image_info = vk::ImageCreateInfo{
|
|
.imageType = vk::ImageType::e2D,
|
|
.format = format,
|
|
.extent = {width, height, 1},
|
|
.mipLevels = 1,
|
|
.arrayLayers = 1,
|
|
.samples = vk::SampleCountFlagBits::e1,
|
|
.usage = vk::ImageUsageFlagBits::eColorAttachment | vk::ImageUsageFlagBits::eTransferSrc,
|
|
};
|
|
const VmaAllocationCreateInfo alloc_info{
|
|
.flags = VMA_ALLOCATION_CREATE_WITHIN_BUDGET_BIT,
|
|
.usage = VMA_MEMORY_USAGE_AUTO_PREFER_DEVICE,
|
|
};
|
|
VkImage image{};
|
|
const VkResult result = vmaCreateImage(instance.GetAllocator(), &image_info, &alloc_info,
|
|
&image, &frame.allocation, nullptr);
|
|
Check<"allocate a frame image">(vk::Result(result));
|
|
frame.image = image;
|
|
frame.image_view = Check<"create a frame view">(device.createImageView({
|
|
.image = frame.image,
|
|
.viewType = vk::ImageViewType::e2D,
|
|
.format = format,
|
|
.subresourceRange = ColorRange,
|
|
}));
|
|
SetObjectName(device, frame.image, "frame {}", frame.id);
|
|
// Frame generation's frames (made with the first ones generated: most sessions never need
|
|
// them).
|
|
DestroyInterpolated(frame);
|
|
frame.width = width;
|
|
frame.height = height;
|
|
frame.is_hdr = swapchain.GetHDR();
|
|
}
|
|
|
|
Frame* Presenter::GetRenderFrame() {
|
|
Frame* frame;
|
|
BbStats::WaitTimer timer{BbStats::frame_pool_wait_ns};
|
|
{
|
|
std::unique_lock lock{free_mutex};
|
|
free_cv.wait(lock, [this] { return !free_frames.empty(); });
|
|
frame = free_frames.front();
|
|
free_frames.pop_front();
|
|
}
|
|
// Its last present has finished: the image, its FSR slot and the fence are free.
|
|
WaitFence(instance.GetDevice(), frame->present_done);
|
|
const u32 width = expected_width.load(std::memory_order_relaxed);
|
|
const u32 height = expected_height.load(std::memory_order_relaxed);
|
|
if (frame->width != width || frame->height != height ||
|
|
frame->is_hdr != swapchain.GetHDR()) {
|
|
RecreateFrame(*frame, width, height);
|
|
}
|
|
return frame;
|
|
}
|
|
|
|
void Presenter::ReleaseFrame(Frame* frame) {
|
|
if (frame == &blank_frame) {
|
|
return;
|
|
}
|
|
{
|
|
std::scoped_lock lock{free_mutex};
|
|
free_frames.push_back(frame);
|
|
}
|
|
free_cv.notify_one();
|
|
}
|
|
|
|
Frame* Presenter::PrepareFrame(const BbVideoOut::BufferAttributeGroup& attribute,
|
|
VAddr cpu_address) {
|
|
BbTimeline::Note(BbTimeline::PipeTask, 4, draw_scheduler.CurrentTick());
|
|
// Scaled upscaler presets: the output-size display buffer drawn by the port.
|
|
TemporalUpscaler::Display display{};
|
|
auto& upscaler = rasterizer->GetUpscaler();
|
|
const bool upscaled = upscaler.DisplayOverride(cpu_address, display);
|
|
VideoCore::ImageId image_id{};
|
|
if (!upscaled) {
|
|
auto desc = VideoCore::TextureCache::ImageDesc{attribute, cpu_address};
|
|
image_id = texture_cache.FindImage(desc);
|
|
texture_cache.UpdateImage(image_id);
|
|
}
|
|
|
|
Frame* frame = GetRenderFrame();
|
|
|
|
// Recorded like draws, on the recording threads: CommandBuffer() would first wait for them
|
|
// to record everything handed over before and then submit from this thread.
|
|
draw_scheduler.EndRendering();
|
|
|
|
VideoCore::ImageViewInfo view_info{};
|
|
view_info.format = FrameViewFormat(attribute.attrib.pixel_format);
|
|
// The frame is opaque: alpha would blend it with the overlay.
|
|
view_info.mapping.a = vk::ComponentSwizzle::eOne;
|
|
vk::ImageView source{};
|
|
vk::Extent2D source_size{};
|
|
if (upscaled) {
|
|
const vk::Device device = instance.GetDevice();
|
|
source = Check(device.createImageView({
|
|
.image = display.image,
|
|
.viewType = vk::ImageViewType::e2D,
|
|
.format = view_info.format,
|
|
.components = view_info.mapping,
|
|
.subresourceRange = ColorRange,
|
|
}));
|
|
draw_scheduler.DeferOperation([device, source] { device.destroyImageView(source); });
|
|
source_size = vk::Extent2D{display.width, display.height};
|
|
draw_scheduler.Record([image = display.image](vk::CommandBuffer cmdbuf) {
|
|
const vk::ImageMemoryBarrier2 to_read{
|
|
.srcStageMask = vk::PipelineStageFlagBits2::eColorAttachmentOutput,
|
|
.srcAccessMask = vk::AccessFlagBits2::eColorAttachmentWrite,
|
|
.dstStageMask = vk::PipelineStageFlagBits2::eAllCommands,
|
|
.dstAccessMask = vk::AccessFlagBits2::eShaderRead,
|
|
.oldLayout = vk::ImageLayout::eGeneral,
|
|
.newLayout = vk::ImageLayout::eShaderReadOnlyOptimal,
|
|
.image = image,
|
|
.subresourceRange = ColorRange,
|
|
};
|
|
cmdbuf.pipelineBarrier2({.imageMemoryBarrierCount = 1, .pImageMemoryBarriers = &to_read});
|
|
});
|
|
} else {
|
|
auto& image = texture_cache.GetImage(image_id);
|
|
source = *image.FindView(view_info).image_view;
|
|
source_size = vk::Extent2D{image.info.size.width, image.info.size.height};
|
|
// BB_PRESENT_DUMP_TRIGGER for the game's own display buffer too.
|
|
if (upscaler.PresentDumpDue()) {
|
|
runtime.Transit(&image, vk::ImageLayout::eGeneral, vk::PipelineStageFlagBits2::eTransfer,
|
|
vk::AccessFlagBits2::eTransferRead);
|
|
runtime.FlushBarriers();
|
|
upscaler.DumpPresented(image.GetImage(), source_size.width, source_size.height,
|
|
view_info.format);
|
|
}
|
|
runtime.Transit(&image, vk::ImageLayout::eShaderReadOnlyOptimal,
|
|
vk::PipelineStageFlagBits2::eFragmentShader |
|
|
vk::PipelineStageFlagBits2::eComputeShader,
|
|
vk::AccessFlagBits2::eShaderRead);
|
|
runtime.FlushBarriers();
|
|
}
|
|
expected_ratio.store(float(source_size.width) / float(source_size.height),
|
|
std::memory_order_relaxed);
|
|
shown_aspect.store(float(source_size.width) / float(source_size.height),
|
|
std::memory_order_relaxed);
|
|
|
|
const EncodePass::Settings encode{
|
|
.gamma = gamma.load(std::memory_order_relaxed),
|
|
.hdr = hdr.load(std::memory_order_relaxed),
|
|
.srgb_input = attribute.attrib.pixel_format == BbVideoOut::PixelFormat::A2R10G10B10Srgb,
|
|
// Sharpening without an upscaler (the upscalers sharpen their own output).
|
|
.sharpen = BbSettings::Get().upscaler == BbSettings::UpscalerOff && BbSettings::Get().sharpen
|
|
? std::clamp(BbSettings::Get().sharpness.load(), 0.0f, 2.0f)
|
|
: 0.0f,
|
|
};
|
|
// Numbered here, in command order; marked where the commands are recorded.
|
|
const u32 stream = draw_scheduler.CrumbStream();
|
|
const u32 crumb = Breadcrumbs::Enabled()
|
|
? Breadcrumbs::Note(stream, {.name = "presenter encode"})
|
|
: 0;
|
|
// The encode pass holds no state: frames of different submissions may be recorded on two
|
|
// threads at once.
|
|
draw_scheduler.Record([this, frame, source, encode, stream, crumb](vk::CommandBuffer cmdbuf) {
|
|
if (crumb) {
|
|
Breadcrumbs::Mark(cmdbuf, stream, crumb, false);
|
|
}
|
|
const vk::Extent2D size{frame->width, frame->height};
|
|
// The frame's last reader (the present blit) is done: the fence was waited for.
|
|
const vk::ImageMemoryBarrier2 to_attachment{
|
|
.srcStageMask = vk::PipelineStageFlagBits2::eColorAttachmentOutput,
|
|
.dstStageMask = vk::PipelineStageFlagBits2::eColorAttachmentOutput,
|
|
.dstAccessMask = vk::AccessFlagBits2::eColorAttachmentWrite,
|
|
.oldLayout = vk::ImageLayout::eUndefined,
|
|
.newLayout = vk::ImageLayout::eColorAttachmentOptimal,
|
|
.image = frame->image,
|
|
.subresourceRange = ColorRange,
|
|
};
|
|
cmdbuf.pipelineBarrier2({.imageMemoryBarrierCount = 1, .pImageMemoryBarriers = &to_attachment});
|
|
encode_pass.Render(cmdbuf, source, frame->image_view, size, encode);
|
|
// Read by the present blit (or a dump) in the general layout, after a semaphore wait.
|
|
const vk::ImageMemoryBarrier2 to_general{
|
|
.srcStageMask = vk::PipelineStageFlagBits2::eColorAttachmentOutput,
|
|
.srcAccessMask = vk::AccessFlagBits2::eColorAttachmentWrite,
|
|
.oldLayout = vk::ImageLayout::eColorAttachmentOptimal,
|
|
.newLayout = vk::ImageLayout::eGeneral,
|
|
.image = frame->image,
|
|
.subresourceRange = ColorRange,
|
|
};
|
|
cmdbuf.pipelineBarrier2({.imageMemoryBarrierCount = 1, .pImageMemoryBarriers = &to_general});
|
|
if (crumb) {
|
|
Breadcrumbs::Mark(cmdbuf, stream, crumb, true);
|
|
}
|
|
});
|
|
|
|
// Frame generation: the frames generated before this one, encoded the same way.
|
|
frame->interp_count = 0;
|
|
frame->interp_async = {};
|
|
if (upscaled && upscaler.GetFrameGeneration()) {
|
|
std::array<FrameGeneration::Output, FrameGeneration::MaxGenerated> interp{};
|
|
const u32 generated = upscaler.GetFrameGeneration()->TakeOutputs(cpu_address, interp);
|
|
static_assert(FrameGeneration::MaxGenerated <= Frame::MaxInterp);
|
|
for (u32 i = 0; i < generated; ++i) {
|
|
if (interp[i].wait_semaphore) {
|
|
// Asynchronous: encoded when shown, once the compute queue made it; one of
|
|
// another size is given back unshown.
|
|
if (interp[i].width == display.width && interp[i].height == display.height) {
|
|
frame->interp_async[frame->interp_count++] = interp[i];
|
|
frame->interp_encode = encode;
|
|
frame->interp_view_format = view_info.format;
|
|
} else {
|
|
upscaler.GetFrameGeneration()->ReleaseOutput(interp[i].target, interp[i].frame,
|
|
{}, 0);
|
|
}
|
|
continue;
|
|
}
|
|
if (interp[i].width == display.width && interp[i].height == display.height) {
|
|
upscaler.GetFrameGeneration()->ApplyDisplayLut(interp[i]);
|
|
// BB_FG_DUMP_TRIGGER=<file> (diagnostics): the real frame's display image and
|
|
// the generated one of three frames, raw, to compare them byte for byte.
|
|
static const char* fg_dump = bb_env("BB_FG_DUMP_TRIGGER");
|
|
static int fg_dump_left = 0;
|
|
if (fg_dump && fg_dump_left == 0 && std::remove(fg_dump) == 0) {
|
|
fg_dump_left = 3;
|
|
}
|
|
if (fg_dump_left > 0) {
|
|
--fg_dump_left;
|
|
upscaler.DumpPresented(display.image, display.width, display.height, display.format);
|
|
upscaler.DumpPresented(interp[i].image, interp[i].width, interp[i].height, interp[i].format);
|
|
}
|
|
EncodeInterpolated(frame, frame->interp_count, interp[i].image, interp[i].format,
|
|
interp[i].width, interp[i].height, view_info.format, encode);
|
|
}
|
|
}
|
|
}
|
|
|
|
BbStats::frame_number.fetch_add(1, std::memory_order_relaxed);
|
|
frame->ready_semaphore = draw_scheduler.GetWorkSemaphore()->Handle();
|
|
frame->ready_tick = draw_scheduler.CurrentTick();
|
|
SubmitInfo info{};
|
|
draw_scheduler.NoteFrameEndSubmission();
|
|
draw_scheduler.Flush(info);
|
|
frame->present_id = LowLatency::Get().RenderSubmitEnd(); // the frame's last submission
|
|
BbTimeline::Note(BbTimeline::PipeTask, 5, frame->ready_tick);
|
|
|
|
// The command processor thread runs at most FramesAhead() guest frames ahead of the GPU: it
|
|
// waits here for the frame that many flips back. When the GPU is the bottleneck it finishes
|
|
// frames at an even rate; unbounded, the thread ran ahead and then blocked wherever a
|
|
// resource ran out, so flips (and the guest's frame timing) came in bursts.
|
|
if (const u32 ahead = FramesAhead()) {
|
|
recent_frame_ticks.push_back(frame->ready_tick);
|
|
if (recent_frame_ticks.size() > ahead) {
|
|
const u64 tick = recent_frame_ticks.front();
|
|
recent_frame_ticks.pop_front();
|
|
BbStats::WaitTimer timer{BbStats::present_wait_ns};
|
|
draw_scheduler.Wait(tick);
|
|
}
|
|
}
|
|
return frame;
|
|
}
|
|
|
|
Frame* Presenter::PrepareBlankFrame(bool present_thread) {
|
|
// Presented as the black bars alone: nothing to render. A blank flip of the guest still
|
|
// ends its submission, as any flip does: the guest may wait for work recorded before it.
|
|
if (!present_thread) {
|
|
draw_scheduler.EndRendering();
|
|
SubmitInfo info{};
|
|
draw_scheduler.Flush(info);
|
|
}
|
|
return &blank_frame;
|
|
}
|
|
|
|
void Presenter::WaitRendered(const Frame* frame) {
|
|
if (!frame || !frame->ready_semaphore) {
|
|
return;
|
|
}
|
|
const vk::SemaphoreWaitInfo info{
|
|
.semaphoreCount = 1,
|
|
.pSemaphores = &frame->ready_semaphore,
|
|
.pValues = &frame->ready_tick,
|
|
};
|
|
// A lost device ends the wait (the presenter reports it); a timeout waits again.
|
|
while (instance.GetDevice().waitSemaphores(&info, 1'000'000'000) == vk::Result::eTimeout) {
|
|
}
|
|
}
|
|
|
|
void Presenter::SetExpectedSize(u32 width, u32 height) {
|
|
if (width == 0 || height == 0) {
|
|
return; // minimised: keep the last size (a 0x0 frame image cannot be made, issue #31)
|
|
}
|
|
const float ratio = expected_ratio.load(std::memory_order_relaxed);
|
|
if (float(width) / float(height) > ratio) {
|
|
width = u32(float(height) * ratio);
|
|
} else {
|
|
height = u32(float(width) / ratio);
|
|
}
|
|
expected_width.store(width, std::memory_order_relaxed);
|
|
expected_height.store(height, std::memory_order_relaxed);
|
|
}
|
|
|
|
void Presenter::EncodeInterpolated(Frame* frame, u32 index, vk::Image image, vk::Format format,
|
|
u32 width, u32 height, vk::Format display_view_format,
|
|
const EncodePass::Settings& encode,
|
|
vk::CommandBuffer present_cmdbuf) {
|
|
const vk::Device device = instance.GetDevice();
|
|
if (index >= Frame::MaxInterp) {
|
|
return;
|
|
}
|
|
vk::Image& interp_image = frame->interp_image[index];
|
|
vk::ImageView& interp_view = frame->interp_view[index];
|
|
if (!interp_image) {
|
|
const VkImageCreateInfo image_info = vk::ImageCreateInfo{
|
|
.imageType = vk::ImageType::e2D,
|
|
.format = swapchain.GetSurfaceFormat().format,
|
|
.extent = {frame->width, frame->height, 1},
|
|
.mipLevels = 1,
|
|
.arrayLayers = 1,
|
|
.samples = vk::SampleCountFlagBits::e1,
|
|
.usage = vk::ImageUsageFlagBits::eColorAttachment | vk::ImageUsageFlagBits::eTransferSrc,
|
|
};
|
|
const VmaAllocationCreateInfo alloc_info{
|
|
.flags = VMA_ALLOCATION_CREATE_WITHIN_BUDGET_BIT,
|
|
.usage = VMA_MEMORY_USAGE_AUTO_PREFER_DEVICE,
|
|
};
|
|
VkImage created{};
|
|
if (vmaCreateImage(instance.GetAllocator(), &image_info, &alloc_info, &created,
|
|
&frame->interp_allocation[index], nullptr) != VK_SUCCESS) {
|
|
return;
|
|
}
|
|
interp_image = created;
|
|
interp_view = Check<"create a frame view">(device.createImageView({
|
|
.image = interp_image,
|
|
.viewType = vk::ImageViewType::e2D,
|
|
.format = swapchain.GetSurfaceFormat().format,
|
|
.subresourceRange = ColorRange,
|
|
}));
|
|
}
|
|
// Read as the display buffer is, with its view format: the game's display pass copies the
|
|
// scene image's bytes as they are into a BGRA buffer, so the scene image (R8G8B8A8 by its
|
|
// format) holds them in that order too; its own format swapped red and blue. 8-bit images are
|
|
// made mutable (vk_frame_generation), 32-bit formats view each other.
|
|
const bool rgba8 = format == vk::Format::eR8G8B8A8Unorm || format == vk::Format::eB8G8R8A8Unorm;
|
|
const bool display8 = display_view_format == vk::Format::eB8G8R8A8Srgb ||
|
|
display_view_format == vk::Format::eR8G8B8A8Srgb;
|
|
const vk::Format view_format = rgba8 && display8 ? display_view_format : format;
|
|
vk::ComponentMapping mapping{};
|
|
mapping.a = vk::ComponentSwizzle::eOne;
|
|
const vk::ImageView source = Check(device.createImageView({
|
|
.image = image,
|
|
.viewType = vk::ImageViewType::e2D,
|
|
.format = view_format,
|
|
.components = mapping,
|
|
.subresourceRange = ColorRange,
|
|
}));
|
|
// Asynchronous frame generation: into the present thread's command buffer.
|
|
Scheduler& scheduler = present_cmdbuf ? present_scheduler : draw_scheduler;
|
|
scheduler.DeferOperation([device, source] { device.destroyImageView(source); });
|
|
const auto record = [this, frame, image, source, encode, interp_image,
|
|
interp_view](vk::CommandBuffer cmdbuf) {
|
|
const std::array<vk::ImageMemoryBarrier2, 2> before{{
|
|
{
|
|
.srcStageMask = vk::PipelineStageFlagBits2::eAllCommands,
|
|
.srcAccessMask = vk::AccessFlagBits2::eMemoryWrite,
|
|
.dstStageMask = vk::PipelineStageFlagBits2::eFragmentShader,
|
|
.dstAccessMask = vk::AccessFlagBits2::eShaderRead,
|
|
.oldLayout = vk::ImageLayout::eGeneral,
|
|
.newLayout = vk::ImageLayout::eShaderReadOnlyOptimal,
|
|
.image = image,
|
|
.subresourceRange = ColorRange,
|
|
},
|
|
{
|
|
.srcStageMask = vk::PipelineStageFlagBits2::eColorAttachmentOutput,
|
|
.dstStageMask = vk::PipelineStageFlagBits2::eColorAttachmentOutput,
|
|
.dstAccessMask = vk::AccessFlagBits2::eColorAttachmentWrite,
|
|
.oldLayout = vk::ImageLayout::eUndefined,
|
|
.newLayout = vk::ImageLayout::eColorAttachmentOptimal,
|
|
.image = interp_image,
|
|
.subresourceRange = ColorRange,
|
|
},
|
|
}};
|
|
cmdbuf.pipelineBarrier2({.imageMemoryBarrierCount = 2, .pImageMemoryBarriers = before.data()});
|
|
encode_pass.Render(cmdbuf, source, interp_view, {frame->width, frame->height}, encode);
|
|
const std::array<vk::ImageMemoryBarrier2, 2> after{{
|
|
{
|
|
.srcStageMask = vk::PipelineStageFlagBits2::eFragmentShader,
|
|
.srcAccessMask = vk::AccessFlagBits2::eShaderRead,
|
|
.dstStageMask = vk::PipelineStageFlagBits2::eAllCommands,
|
|
.oldLayout = vk::ImageLayout::eShaderReadOnlyOptimal,
|
|
.newLayout = vk::ImageLayout::eGeneral,
|
|
.image = image,
|
|
.subresourceRange = ColorRange,
|
|
},
|
|
{
|
|
.srcStageMask = vk::PipelineStageFlagBits2::eColorAttachmentOutput,
|
|
.srcAccessMask = vk::AccessFlagBits2::eColorAttachmentWrite,
|
|
.oldLayout = vk::ImageLayout::eColorAttachmentOptimal,
|
|
.newLayout = vk::ImageLayout::eGeneral,
|
|
.image = interp_image,
|
|
.subresourceRange = ColorRange,
|
|
},
|
|
}};
|
|
cmdbuf.pipelineBarrier2({.imageMemoryBarrierCount = 2, .pImageMemoryBarriers = after.data()});
|
|
};
|
|
if (present_cmdbuf) {
|
|
record(present_cmdbuf);
|
|
} else {
|
|
draw_scheduler.Record(record);
|
|
}
|
|
frame->interp_count = std::max(frame->interp_count, index + 1);
|
|
}
|
|
|
|
void Presenter::RecordPresent(vk::CommandBuffer cmdbuf, const Frame* frame, vk::Image target,
|
|
vk::Extent2D extent, vk::Image source) {
|
|
const vk::Rect2D rect =
|
|
frame ? Letterbox({frame->width, frame->height}, extent) : vk::Rect2D{{0, 0}, {0, 0}};
|
|
const bool bars = rect.extent != extent;
|
|
const vk::ImageMemoryBarrier2 to_transfer{
|
|
.srcStageMask = vk::PipelineStageFlagBits2::eAllCommands,
|
|
.dstStageMask = vk::PipelineStageFlagBits2::eTransfer,
|
|
.dstAccessMask = vk::AccessFlagBits2::eTransferWrite,
|
|
.oldLayout = vk::ImageLayout::eUndefined,
|
|
.newLayout = vk::ImageLayout::eTransferDstOptimal,
|
|
.image = target,
|
|
.subresourceRange = ColorRange,
|
|
};
|
|
cmdbuf.pipelineBarrier2({.imageMemoryBarrierCount = 1, .pImageMemoryBarriers = &to_transfer});
|
|
if (bars) {
|
|
const vk::ClearColorValue black{std::array{0.0f, 0.0f, 0.0f, 1.0f}};
|
|
cmdbuf.clearColorImage(target, vk::ImageLayout::eTransferDstOptimal, black, ColorRange);
|
|
}
|
|
if (frame) {
|
|
if (bars) {
|
|
const vk::MemoryBarrier2 cleared{
|
|
.srcStageMask = vk::PipelineStageFlagBits2::eClear,
|
|
.srcAccessMask = vk::AccessFlagBits2::eTransferWrite,
|
|
.dstStageMask = vk::PipelineStageFlagBits2::eBlit,
|
|
.dstAccessMask = vk::AccessFlagBits2::eTransferWrite,
|
|
};
|
|
cmdbuf.pipelineBarrier2({.memoryBarrierCount = 1, .pMemoryBarriers = &cleared});
|
|
}
|
|
const vk::ImageSubresourceLayers layers{vk::ImageAspectFlagBits::eColor, 0, 0, 1};
|
|
const vk::ImageBlit region{
|
|
.srcSubresource = layers,
|
|
.srcOffsets = std::array{vk::Offset3D{0, 0, 0},
|
|
vk::Offset3D{s32(frame->width), s32(frame->height), 1}},
|
|
.dstSubresource = layers,
|
|
.dstOffsets = std::array{vk::Offset3D{rect.offset.x, rect.offset.y, 0},
|
|
vk::Offset3D{rect.offset.x + s32(rect.extent.width),
|
|
rect.offset.y + s32(rect.extent.height), 1}},
|
|
};
|
|
cmdbuf.blitImage(source ? source : frame->image, vk::ImageLayout::eGeneral, target,
|
|
vk::ImageLayout::eTransferDstOptimal, region, vk::Filter::eLinear);
|
|
}
|
|
|
|
// The port's menu and FPS counter over the frame, at display resolution.
|
|
BbGameMenu::Poll();
|
|
const bool overlay = BbOverlay::Visible();
|
|
const vk::ImageMemoryBarrier2 after_transfer{
|
|
.srcStageMask = vk::PipelineStageFlagBits2::eAllTransfer,
|
|
.srcAccessMask = vk::AccessFlagBits2::eTransferWrite,
|
|
.dstStageMask = overlay ? vk::PipelineStageFlagBits2::eColorAttachmentOutput
|
|
: vk::PipelineStageFlagBits2::eNone,
|
|
.dstAccessMask = overlay ? vk::AccessFlagBits2::eColorAttachmentRead |
|
|
vk::AccessFlagBits2::eColorAttachmentWrite
|
|
: vk::AccessFlagBits2::eNone,
|
|
.oldLayout = vk::ImageLayout::eTransferDstOptimal,
|
|
.newLayout = overlay ? vk::ImageLayout::eColorAttachmentOptimal
|
|
: vk::ImageLayout::ePresentSrcKHR,
|
|
.image = target,
|
|
.subresourceRange = ColorRange,
|
|
};
|
|
cmdbuf.pipelineBarrier2({.imageMemoryBarrierCount = 1, .pImageMemoryBarriers = &after_transfer});
|
|
if (overlay) {
|
|
BbOverlay::Render(cmdbuf, swapchain.ImageView(), extent);
|
|
const vk::ImageMemoryBarrier2 to_present{
|
|
.srcStageMask = vk::PipelineStageFlagBits2::eColorAttachmentOutput,
|
|
.srcAccessMask = vk::AccessFlagBits2::eColorAttachmentWrite,
|
|
.oldLayout = vk::ImageLayout::eColorAttachmentOptimal,
|
|
.newLayout = vk::ImageLayout::ePresentSrcKHR,
|
|
.image = target,
|
|
.subresourceRange = ColorRange,
|
|
};
|
|
cmdbuf.pipelineBarrier2({.imageMemoryBarrierCount = 1, .pImageMemoryBarriers = &to_present});
|
|
}
|
|
}
|
|
|
|
void Presenter::Present(Frame* frame, bool is_reusing_frame, bool) {
|
|
BbTimeline::Note(BbTimeline::PresentBegin);
|
|
const auto release = [&] {
|
|
if (!is_reusing_frame) {
|
|
ReleaseFrame(frame);
|
|
}
|
|
};
|
|
const bool blank = frame == &blank_frame;
|
|
const u32 generated = blank ? 0 : std::min(frame->interp_count, Frame::MaxInterp);
|
|
if (!blank) {
|
|
LowLatency::Get().SetGeneratedPresents(generated != 0);
|
|
}
|
|
LowLatency::Get().ApplySettings(); // the menu's mode to the driver (cheap when unchanged)
|
|
// Frame generation: the generated frames first, evenly spaced over the interval between
|
|
// real frames (smoothed), the real one last.
|
|
using Clock = std::chrono::steady_clock;
|
|
static Clock::time_point last_real{};
|
|
static double interval_ms = 16.667;
|
|
const auto now = Clock::now();
|
|
if (last_real.time_since_epoch().count() != 0) {
|
|
const double ms = std::chrono::duration<double, std::milli>(now - last_real).count();
|
|
if (ms > 2.0 && ms < 100.0) {
|
|
interval_ms = interval_ms * 0.9 + ms * 0.1;
|
|
}
|
|
}
|
|
last_real = now;
|
|
// BB_FRAME_GEN_ONLY=1 (diagnostics): only the generated frames are shown.
|
|
static const bool only_interp = bb_env("BB_FRAME_GEN_ONLY") != nullptr;
|
|
if (!blank) {
|
|
frame->interp_count = 0;
|
|
// The FPS counter: the game's own rate and the frames generated for each of its frames.
|
|
auto& settings = BbSettings::Get();
|
|
settings.game_fps.store(float(1000.0 / interval_ms), std::memory_order_relaxed);
|
|
settings.frame_gen_now.store(int(generated), std::memory_order_relaxed);
|
|
}
|
|
if (only_interp && generated && frame->interp_image[0] && !frame->interp_async[0].wait_semaphore) {
|
|
PresentImage(frame, frame->interp_image[0], true);
|
|
release();
|
|
return;
|
|
}
|
|
for (u32 i = 0; i < generated; ++i) {
|
|
// Asynchronous: made on the compute queue. Waited for here, on the CPU, so the graphics
|
|
// queue never waits for the compute one (the next frame's work runs beside it).
|
|
const FrameGeneration::Output* made =
|
|
frame->interp_async[i].wait_semaphore ? &frame->interp_async[i] : nullptr;
|
|
if (made) {
|
|
const vk::Semaphore semaphore = made->wait_semaphore;
|
|
const u64 value = made->wait_value;
|
|
BbStats::WaitTimer timer{BbStats::present_wait_ns};
|
|
if (instance.GetDevice().waitSemaphores(
|
|
{.semaphoreCount = 1, .pSemaphores = &semaphore, .pValues = &value},
|
|
250'000'000) != vk::Result::eSuccess) {
|
|
rasterizer->GetUpscaler().GetFrameGeneration()->ReleaseOutput(made->target, made->frame, {}, 0);
|
|
continue; // not made in time: the real frame only
|
|
}
|
|
} else if (!frame->interp_image[i]) {
|
|
break;
|
|
}
|
|
static std::atomic<u64> shown{0};
|
|
if (const u64 n = ++shown; n == 1 || n % 1000 == 0) {
|
|
std::printf("Frame generation: %llu generated frames shown\n", (unsigned long long)n);
|
|
}
|
|
LowLatency::Get().PresentStart(frame->present_id, true);
|
|
const bool presented = PresentImage(frame, made ? vk::Image{} : frame->interp_image[i], false, made, i);
|
|
LowLatency::Get().PresentEnd(frame->present_id, true);
|
|
if (presented) {
|
|
const double at_ms = interval_ms * double(i + 1) / double(generated + 1);
|
|
std::this_thread::sleep_until(now + std::chrono::duration_cast<Clock::duration>(
|
|
std::chrono::duration<double, std::milli>(
|
|
std::clamp(at_ms, 1.0, 50.0))));
|
|
}
|
|
}
|
|
LowLatency::Get().PresentStart(blank ? 0 : frame->present_id, false);
|
|
PresentImage(frame, {}, true);
|
|
LowLatency::Get().PresentEnd(blank ? 0 : frame->present_id, false);
|
|
release();
|
|
}
|
|
|
|
bool Presenter::PresentImage(Frame* frame, vk::Image source, bool last,
|
|
const FrameGeneration::Output* generated, u32 index) {
|
|
// An asynchronous generated frame goes back to the frame generation however this ends: with
|
|
// the submission that read it, or unread.
|
|
u64 generated_tick = 0;
|
|
struct GiveBack {
|
|
Presenter& self;
|
|
const FrameGeneration::Output* output;
|
|
const u64& tick;
|
|
~GiveBack() {
|
|
if (output) {
|
|
self.rasterizer->GetUpscaler().GetFrameGeneration()->ReleaseOutput(
|
|
output->target, output->frame,
|
|
tick ? self.present_scheduler.GetWorkSemaphore()->Handle() : vk::Semaphore{}, tick);
|
|
}
|
|
}
|
|
} give_back{*this, generated, generated_tick};
|
|
const u32 width = u32(window.GetWidth());
|
|
const u32 height = u32(window.GetHeight());
|
|
// Nothing to present to while the window is minimised (a 0x0 surface, issue #31).
|
|
if (width == 0 || height == 0) {
|
|
return false;
|
|
}
|
|
// Recreate waits for the device. Recording threads must not submit to its queues meanwhile
|
|
// (#211); the recreation after a failed present below already uses this same mutex.
|
|
const auto recreate = [&] {
|
|
std::scoped_lock lock{Scheduler::submit_mutex};
|
|
swapchain.Recreate(width, height);
|
|
};
|
|
// A resized window (or one minimised at the last try) needs a new swapchain.
|
|
if (width != swapchain.GetWidth() || height != swapchain.GetHeight() ||
|
|
!swapchain.IsPresentable()) {
|
|
recreate();
|
|
if (!swapchain.IsPresentable()) {
|
|
return false;
|
|
}
|
|
}
|
|
BbTimeline::Note(BbTimeline::AcquireBegin);
|
|
const bool acquired = swapchain.AcquireNextImage();
|
|
BbTimeline::Note(BbTimeline::AcquireEnd);
|
|
if (!acquired) {
|
|
recreate();
|
|
if (!swapchain.AcquireNextImage()) {
|
|
// Resized faster than the swapchain can follow: this frame is skipped.
|
|
LOG_WARNING(Render_Vulkan, "Skipping a frame: no swapchain image");
|
|
return false;
|
|
}
|
|
}
|
|
|
|
const bool blank = frame == &blank_frame;
|
|
const vk::Extent2D extent = swapchain.GetExtent();
|
|
SetExpectedSize(extent.width, extent.height);
|
|
// Reset here, not when the frame is taken: a frame skipped above keeps its fence signaled.
|
|
if (!blank && last) {
|
|
Check<"reset a frame fence">(instance.GetDevice().resetFences(frame->present_done));
|
|
}
|
|
|
|
const auto cmdbuf = present_scheduler.CommandBuffer();
|
|
if (generated) {
|
|
// Its LUT and encoding (PrepareFrame does both for the real frame and, synchronous, for
|
|
// the generated ones in the draw stream).
|
|
rasterizer->GetUpscaler().GetFrameGeneration()->RecordDisplayLut(cmdbuf, *generated);
|
|
EncodeInterpolated(frame, index, generated->image, generated->format, generated->width,
|
|
generated->height, frame->interp_view_format, frame->interp_encode,
|
|
cmdbuf);
|
|
source = frame->interp_image[index];
|
|
}
|
|
if (!blank && last) {
|
|
DumpFinalFrameIfDue(instance, present_scheduler, cmdbuf, frame->image, frame->width,
|
|
frame->height, swapchain.GetSurfaceFormat().format);
|
|
}
|
|
// BB_SEQUENCE_DUMP_TRIGGER=<file> (frame generation checks): the next 16 images shown, the
|
|
// interpolated ones ("interp") and the game's ("real") in the order they are presented.
|
|
{
|
|
static const char* trigger = bb_env("BB_SEQUENCE_DUMP_TRIGGER");
|
|
static int left = 0, index = 0;
|
|
// BB_SEQUENCE_DUMP_COUNT: more images per trigger (rare artifacts).
|
|
static const int count = [] {
|
|
const char* env = bb_env("BB_SEQUENCE_DUMP_COUNT");
|
|
return env ? std::max(1, std::atoi(env)) : 16;
|
|
}();
|
|
if (trigger && left == 0 && std::remove(trigger) == 0) {
|
|
left = count;
|
|
if (!bb_env("BB_SEQUENCE_DUMP_COUNT")) index = 0; // batches: numbered on
|
|
}
|
|
if (left > 0 && !blank) {
|
|
--left;
|
|
DumpPresented(instance, present_scheduler, cmdbuf, source ? source : frame->image,
|
|
frame->width, frame->height, swapchain.GetSurfaceFormat().format, index++,
|
|
source ? "interp" : "real");
|
|
}
|
|
}
|
|
RecordPresent(cmdbuf, blank ? nullptr : frame, swapchain.Image(), extent, source);
|
|
|
|
// The frame's submission goes out from a recording thread (BB_ASYNC_SUBMIT); the present
|
|
// one, waiting for it on the same queue, must not get there first.
|
|
if (!blank && frame->ready_semaphore == draw_scheduler.GetWorkSemaphore()->Handle()) {
|
|
BbTimeline::Note(BbTimeline::WaitSubmitBegin);
|
|
draw_scheduler.WaitSubmitted(frame->ready_tick);
|
|
BbTimeline::Note(BbTimeline::WaitSubmitEnd);
|
|
}
|
|
SubmitInfo info{};
|
|
info.AddWait(swapchain.GetImageAcquiredSemaphore());
|
|
if (!blank) {
|
|
info.AddWait(frame->ready_semaphore, frame->ready_tick);
|
|
if (last) {
|
|
info.AddSignal(frame->present_done);
|
|
}
|
|
}
|
|
if (generated) {
|
|
// Reached already (Present waited on the CPU): the compute queue's writes made visible.
|
|
info.AddWait(generated->wait_semaphore, generated->wait_value);
|
|
}
|
|
info.AddSignal(swapchain.GetPresentReadySemaphore());
|
|
const u64 present_id = !blank && last && !source ? frame->present_id : 0;
|
|
info.present_id = present_id; // low latency: the frame's own present
|
|
if (generated) {
|
|
generated_tick = present_scheduler.CurrentTick();
|
|
}
|
|
present_scheduler.Flush(info);
|
|
{
|
|
std::scoped_lock lock{Scheduler::submit_mutex};
|
|
BbStats::WaitTimer timer{BbStats::present_call_ns};
|
|
if (!swapchain.Present(present_id)) {
|
|
swapchain.Recreate(width, height);
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
} // namespace Vulkan
|
|
|
|
extern "C" float bbgpu_frame_aspect(void) {
|
|
return Vulkan::shown_aspect.load(std::memory_order_relaxed);
|
|
}
|