Files
ayozetr 7c4a740338 Presenter: say so when a frame or a swapchain image does not come for 10 s
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.
2026-11-01 14:35:48 +00:00

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);
}