Files
Xenia-Canary/src/xenia/ui/vulkan/vulkan_presenter.h
Triang3l fe3f0f26e4 [UI] Image post-processing and full presentation/window rework
[GPU] Add FXAA post-processing
[UI] Add FidelityFX FSR and CAS post-processing
[UI] Add blue noise dithering from 10bpc to 8bpc
[GPU] Apply the DC PWL gamma ramp closer to the spec, supporting fully white color
[UI] Allow the GPU CP thread to present on the host directly, bypassing the UI thread OS paint event
[UI] Allow variable refresh rate (or tearing)
[UI] Present the newest frame (restart) on DXGI
[UI] Replace GraphicsContext with a far more advanced Presenter with more coherent surface connection and UI overlay state management
[UI] Connect presentation to windows via the Surface class, not native window handles
[Vulkan] Switch to simpler Vulkan setup with no instance/device separation due to interdependencies and to pass fewer objects around
[Vulkan] Lower the minimum required Vulkan version to 1.0
[UI/GPU] Various cleanup, mainly ComPtr usage
[UI] Support per-monitor DPI awareness v2 on Windows
[UI] DPI-scale Dear ImGui
[UI] Replace the remaining non-detachable window delegates with unified window event and input listeners
[UI] Allow listeners to safely destroy or close the window, and to register/unregister listeners without use-after-free and the ABA problem
[UI] Explicit Z ordering of input listeners and UI overlays, top-down for input, bottom-up for drawing
[UI] Add explicit window lifecycle phases
[UI] Replace Window virtual functions with explicit desired state, its application, actual state, its feedback
[UI] GTK: Apply the initial size to the drawing area
[UI] Limit internal UI frame rate to that of the monitor
[UI] Hide the cursor using a timer instead of polling due to no repeated UI thread paints with GPU CP thread presentation, and only within the window
2022-01-29 13:22:03 +03:00

519 lines
20 KiB
C++

/**
******************************************************************************
* Xenia : Xbox 360 Emulator Research Project *
******************************************************************************
* Copyright 2022 Ben Vanik. All rights reserved. *
* Released under the BSD license - see LICENSE in the root for more details. *
******************************************************************************
*/
#ifndef XENIA_UI_VULKAN_VULKAN_PRESENTER_H_
#define XENIA_UI_VULKAN_VULKAN_PRESENTER_H_
#include <algorithm>
#include <array>
#include <cstddef>
#include <cstdint>
#include <memory>
#include <utility>
#include <vector>
#include "xenia/base/assert.h"
#include "xenia/base/platform.h"
#include "xenia/ui/presenter.h"
#include "xenia/ui/surface.h"
#include "xenia/ui/vulkan/vulkan_provider.h"
#include "xenia/ui/vulkan/vulkan_submission_tracker.h"
namespace xe {
namespace ui {
namespace vulkan {
class VulkanUIDrawContext final : public UIDrawContext {
public:
VulkanUIDrawContext(Presenter& presenter, uint32_t render_target_width,
uint32_t render_target_height,
VkCommandBuffer draw_command_buffer,
uint64_t submission_index_current,
uint64_t submission_index_completed,
VkRenderPass render_pass, VkFormat render_pass_format)
: UIDrawContext(presenter, render_target_width, render_target_height),
draw_command_buffer_(draw_command_buffer),
submission_index_current_(submission_index_current),
submission_index_completed_(submission_index_completed),
render_pass_(render_pass),
render_pass_format_(render_pass_format) {}
VkCommandBuffer draw_command_buffer() const { return draw_command_buffer_; }
uint64_t submission_index_current() const {
return submission_index_current_;
}
uint64_t submission_index_completed() const {
return submission_index_completed_;
}
VkRenderPass render_pass() const { return render_pass_; }
VkFormat render_pass_format() const { return render_pass_format_; }
private:
VkCommandBuffer draw_command_buffer_;
uint64_t submission_index_current_;
uint64_t submission_index_completed_;
// Has 1 subpass with a single render_pass_format_ attachment.
VkRenderPass render_pass_;
VkFormat render_pass_format_;
};
class VulkanPresenter final : public Presenter {
public:
// Maximum number of different guest output image versions still potentially
// considered alive that may be given to the refresher - this many instances
// of dependent objects (such as framebuffers) may need to be kept by the
// refresher across invocations (due to multiple-buffering of guest output
// images inside the presenter, different versions may be given even every
// invocation), to avoid recreation of dependent objects every frame.
static constexpr size_t kMaxActiveGuestOutputImageVersions =
kGuestOutputMailboxSize;
static constexpr VkFormat kGuestOutputFormat =
VK_FORMAT_A2B10G10R10_UNORM_PACK32;
// The guest output is expected to be acquired and released in this state by
// the refresher. The exception is the first write to the current guest output
// image - in this case, a barrier is only needed from
// VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT without access. Also if the image is
// being refreshed for the first time, it's in VK_IMAGE_LAYOUT_UNDEFINED (but
// it's safe, and preferred, to transition it from VK_IMAGE_LAYOUT_UNDEFINED
// when writing to it in general).
static constexpr VkPipelineStageFlagBits kGuestOutputInternalStageMask =
VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT;
static constexpr VkAccessFlags kGuestOutputInternalAccessMask =
VK_ACCESS_SHADER_READ_BIT;
static constexpr VkImageLayout kGuestOutputInternalLayout =
VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
// The callback must use the graphics and compute queue 0 of the provider.
class VulkanGuestOutputRefreshContext final
: public GuestOutputRefreshContext {
public:
VulkanGuestOutputRefreshContext(bool& is_8bpc_out_ref, VkImage image,
VkImageView image_view,
uint64_t image_version,
bool image_ever_written_previously)
: GuestOutputRefreshContext(is_8bpc_out_ref),
image_(image),
image_view_(image_view),
image_version_(image_version),
image_ever_written_previously_(image_ever_written_previously) {}
// The format is kGuestOutputFormat.
// Supports usage as a color attachment and as a sampled image, as well as
// transfer source (but the reason of that is guest output capturing).
VkImage image() const { return image_; }
VkImageView image_view() const { return image_view_; }
uint64_t image_version() const { return image_version_; }
// Whether a proper barrier must be done to acquire the image.
bool image_ever_written_previously() const {
return image_ever_written_previously_;
}
private:
VkImage image_;
VkImageView image_view_;
uint64_t image_version_;
bool image_ever_written_previously_;
};
static std::unique_ptr<VulkanPresenter> Create(
HostGpuLossCallback host_gpu_loss_callback, VulkanProvider& provider) {
auto presenter = std::unique_ptr<VulkanPresenter>(
new VulkanPresenter(host_gpu_loss_callback, provider));
if (!presenter->InitializeSurfaceIndependent()) {
return nullptr;
}
return presenter;
}
~VulkanPresenter();
VulkanProvider& provider() const { return provider_; }
static Surface::TypeFlags GetSurfaceTypesSupportedByInstance(
const VulkanProvider::InstanceExtensions& instance_extensions) {
if (!instance_extensions.khr_surface) {
return 0;
}
Surface::TypeFlags type_flags = 0;
#if XE_PLATFORM_ANDROID
if (instance_extensions.khr_android_surface) {
type_flags |= Surface::kTypeFlag_AndroidNativeWindow;
}
#elif XE_PLATFORM_GNU_LINUX
if (instance_extensions.khr_xcb_surface) {
type_flags |= Surface::kTypeFlag_XcbWindow;
}
#elif XE_PLATFORM_WIN32
if (instance_extensions.khr_win32_surface) {
type_flags |= Surface::kTypeFlag_Win32Hwnd;
}
#endif
return type_flags;
}
Surface::TypeFlags GetSupportedSurfaceTypes() const override;
bool CaptureGuestOutput(RawImage& image_out) override;
void AwaitUISubmissionCompletionFromUIThread(uint64_t submission_index) {
ui_submission_tracker_.AwaitSubmissionCompletion(submission_index);
}
VkCommandBuffer AcquireUISetupCommandBufferFromUIThread();
protected:
SurfacePaintConnectResult ConnectOrReconnectPaintingToSurfaceFromUIThread(
Surface& new_surface, uint32_t new_surface_width,
uint32_t new_surface_height, bool was_paintable,
bool& is_vsync_implicit_out) override;
void DisconnectPaintingFromSurfaceFromUIThreadImpl() override;
bool RefreshGuestOutputImpl(
uint32_t mailbox_index, uint32_t frontbuffer_width,
uint32_t frontbuffer_height,
std::function<bool(GuestOutputRefreshContext& context)> refresher,
bool& is_8bpc_out_ref) override;
PaintResult PaintAndPresentImpl(bool execute_ui_drawers) override;
private:
// Usable for both the guest output image itself and for intermediate images.
class GuestOutputImage {
public:
static std::unique_ptr<GuestOutputImage> Create(
const VulkanProvider& provider, uint32_t width, uint32_t height) {
assert_not_zero(width);
assert_not_zero(height);
auto image = std::unique_ptr<GuestOutputImage>(
new GuestOutputImage(provider, width, height));
if (!image->Initialize()) {
return nullptr;
}
return std::move(image);
}
GuestOutputImage(const GuestOutputImage& image) = delete;
GuestOutputImage& operator=(const GuestOutputImage& image) = delete;
~GuestOutputImage();
const VkExtent2D& extent() const { return extent_; }
VkImage image() const { return image_; }
VkDeviceMemory memory() const { return memory_; }
VkImageView view() const { return view_; }
private:
GuestOutputImage(const VulkanProvider& provider, uint32_t width,
uint32_t height)
: provider_(provider) {
extent_.width = width;
extent_.height = height;
}
bool Initialize();
const VulkanProvider& provider_;
VkExtent2D extent_;
VkImage image_ = VK_NULL_HANDLE;
VkDeviceMemory memory_ = VK_NULL_HANDLE;
VkImageView view_ = VK_NULL_HANDLE;
};
struct GuestOutputImageInstance {
// Refresher-side reference (painting has its own references for the purpose
// of destruction only after painting is done on the GPU).
std::shared_ptr<GuestOutputImage> image;
uint64_t version = UINT64_MAX;
uint64_t last_refresher_submission = 0;
// For choosing the barrier stage and access mask and layout depending on
// whether the image has previously been written. If an image is active
// after a refresh, it can be assumed that this is true.
bool ever_successfully_refreshed = false;
void SetToNewImage(const std::shared_ptr<GuestOutputImage>& new_image,
uint64_t new_version) {
image = new_image;
version = new_version;
last_refresher_submission = 0;
ever_successfully_refreshed = false;
}
};
struct GuestOutputPaintRectangleConstants {
union {
struct {
float x;
float y;
};
float offset[2];
};
union {
struct {
float width;
float height;
};
float size[2];
};
};
enum GuestOutputPaintPipelineLayoutIndex : size_t {
kGuestOutputPaintPipelineLayoutIndexBilinear,
kGuestOutputPaintPipelineLayoutIndexCasSharpen,
kGuestOutputPaintPipelineLayoutIndexCasResample,
kGuestOutputPaintPipelineLayoutIndexFsrEasu,
kGuestOutputPaintPipelineLayoutIndexFsrRcas,
kGuestOutputPaintPipelineLayoutCount,
};
static constexpr GuestOutputPaintPipelineLayoutIndex
GetGuestOutputPaintPipelineLayoutIndex(GuestOutputPaintEffect effect) {
switch (effect) {
case GuestOutputPaintEffect::kBilinear:
case GuestOutputPaintEffect::kBilinearDither:
return kGuestOutputPaintPipelineLayoutIndexBilinear;
case GuestOutputPaintEffect::kCasSharpen:
case GuestOutputPaintEffect::kCasSharpenDither:
return kGuestOutputPaintPipelineLayoutIndexCasSharpen;
case GuestOutputPaintEffect::kCasResample:
case GuestOutputPaintEffect::kCasResampleDither:
return kGuestOutputPaintPipelineLayoutIndexCasResample;
case GuestOutputPaintEffect::kFsrEasu:
return kGuestOutputPaintPipelineLayoutIndexFsrEasu;
case GuestOutputPaintEffect::kFsrRcas:
case GuestOutputPaintEffect::kFsrRcasDither:
return kGuestOutputPaintPipelineLayoutIndexFsrRcas;
default:
assert_unhandled_case(effect);
return kGuestOutputPaintPipelineLayoutCount;
}
}
struct PaintContext {
class Submission {
public:
static std::unique_ptr<Submission> Create(
const VulkanProvider& provider) {
auto submission = std::unique_ptr<Submission>(new Submission(provider));
if (!submission->Initialize()) {
return nullptr;
}
return submission;
}
Submission(const Submission& submission) = delete;
Submission& operator=(const Submission& submission) = delete;
~Submission();
VkSemaphore acquire_semaphore() const { return acquire_semaphore_; }
VkSemaphore present_semaphore() const { return present_semaphore_; }
VkCommandPool draw_command_pool() const { return draw_command_pool_; }
VkCommandBuffer draw_command_buffer() const {
return draw_command_buffer_;
}
private:
explicit Submission(const VulkanProvider& provider)
: provider_(provider) {}
bool Initialize();
const VulkanProvider& provider_;
VkSemaphore acquire_semaphore_ = VK_NULL_HANDLE;
VkSemaphore present_semaphore_ = VK_NULL_HANDLE;
VkCommandPool draw_command_pool_ = VK_NULL_HANDLE;
VkCommandBuffer draw_command_buffer_ = VK_NULL_HANDLE;
};
static constexpr uint32_t kSubmissionCount = 3;
struct GuestOutputPaintPipeline {
// Created on initialization.
VkPipeline intermediate_pipeline = VK_NULL_HANDLE;
// Created during guest output painting (after awaiting the last guest
// output paint if outdated and needs to be recreated), when needed, for
// the up-to-date render pass that draws to the swapchain with the actual
// image format.
VkPipeline swapchain_pipeline = VK_NULL_HANDLE;
VkFormat swapchain_format = VK_FORMAT_UNDEFINED;
};
enum GuestOutputDescriptorSet : uint32_t {
kGuestOutputDescriptorSetGuestOutput0Sampled,
kGuestOutputDescriptorSetIntermediate0Sampled =
kGuestOutputDescriptorSetGuestOutput0Sampled +
kGuestOutputMailboxSize,
kGuestOutputDescriptorSetCount =
kGuestOutputDescriptorSetIntermediate0Sampled +
kMaxGuestOutputPaintEffects - 1,
};
struct UISetupCommandBuffer {
UISetupCommandBuffer(VkCommandPool command_pool,
VkCommandBuffer command_buffer,
uint64_t last_usage_submission_index = 0)
: command_pool(command_pool),
command_buffer(command_buffer),
last_usage_submission_index(last_usage_submission_index) {}
VkCommandPool command_pool;
VkCommandBuffer command_buffer;
uint64_t last_usage_submission_index;
};
struct SwapchainFramebuffer {
SwapchainFramebuffer(VkImageView image_view, VkFramebuffer framebuffer)
: image_view(image_view), framebuffer(framebuffer) {}
VkImageView image_view;
VkFramebuffer framebuffer;
};
explicit PaintContext(VulkanProvider& provider)
: provider(provider), submission_tracker(provider) {}
PaintContext(const PaintContext& paint_context) = delete;
PaintContext& operator=(const PaintContext& paint_context) = delete;
// The old swapchain, if passed, should be assumed to be retired after this
// call (though it may fail before the vkCreateSwapchainKHR that will
// technically retire it, so it will be in an undefined state), and needs to
// be destroyed externally no matter what the result is.
static VkSwapchainKHR CreateSwapchainForVulkanSurface(
const VulkanProvider& provider, VkSurfaceKHR surface, uint32_t width,
uint32_t height, VkSwapchainKHR old_swapchain,
uint32_t& present_queue_family_out, VkFormat& image_format_out,
VkExtent2D& image_extent_out, bool& is_fifo_out,
bool& ui_surface_unusable_out);
// Destroys the swapchain and its derivatives, nulls `swapchain` and returns
// the original swapchain object, if it existed, for use as oldSwapchain if
// needed and for destruction.
VkSwapchainKHR PrepareForSwapchainRetirement();
// May be called from the destructor of the presenter.
void DestroySwapchainAndVulkanSurface();
// Connection-indepedent.
const VulkanProvider& provider;
std::array<std::unique_ptr<PaintContext::Submission>, kSubmissionCount>
submissions;
VulkanSubmissionTracker submission_tracker;
std::array<GuestOutputPaintPipeline, size_t(GuestOutputPaintEffect::kCount)>
guest_output_paint_pipelines;
VkDescriptorPool guest_output_descriptor_pool = VK_NULL_HANDLE;
// Descriptors are updated while painting if they're out of date.
VkDescriptorSet
guest_output_descriptor_sets[kGuestOutputDescriptorSetCount];
// Refreshed and cleaned up during guest output painting. The first is the
// paint submission index in which the guest output image (and its
// descriptors) was last used, the second is the reference to the image,
// which may be null. The indices are not mailbox indices here, rather, if
// the reference is not in this array yet, the most outdated reference, if
// needed, is replaced with the new one, awaiting the usage completion of
// the last paint usage.
std::array<std::pair<uint64_t, std::shared_ptr<GuestOutputImage>>,
kGuestOutputMailboxSize>
guest_output_image_paint_refs;
// The latest submission index at which any guest output image was drawn.
uint64_t guest_output_image_paint_last_submission = 0;
// Current intermediate images for guest output painting, refreshed when
// painting guest output.
std::array<std::unique_ptr<GuestOutputImage>,
kMaxGuestOutputPaintEffects - 1>
guest_output_intermediate_images;
// Created and destroyed alongside the images. UNORM only.
std::array<VkFramebuffer, kMaxGuestOutputPaintEffects - 1>
guest_output_intermediate_framebuffers = {};
uint64_t guest_output_intermediate_image_last_submission = 0;
// Command buffers optionally executed before the draw command buffer,
// outside the painting render pass.
std::vector<UISetupCommandBuffer> ui_setup_command_buffers;
size_t ui_setup_command_buffer_current_index = SIZE_MAX;
// Connection-specific.
// May be reused between connections if the format stays the same.
VkRenderPass swapchain_render_pass = VK_NULL_HANDLE;
VkFormat swapchain_render_pass_format = VK_FORMAT_UNDEFINED;
bool swapchain_render_pass_clear_load_op = false;
VkSurfaceKHR vulkan_surface = VK_NULL_HANDLE;
uint32_t present_queue_family = UINT32_MAX;
VkSwapchainKHR swapchain = VK_NULL_HANDLE;
VkExtent2D swapchain_extent = {};
bool swapchain_is_fifo = false;
std::vector<VkImage> swapchain_images;
std::vector<SwapchainFramebuffer> swapchain_framebuffers;
};
explicit VulkanPresenter(HostGpuLossCallback host_gpu_loss_callback,
VulkanProvider& provider)
: Presenter(host_gpu_loss_callback),
provider_(provider),
guest_output_image_refresher_submission_tracker_(provider),
ui_submission_tracker_(provider),
paint_context_(provider) {}
bool InitializeSurfaceIndependent();
[[nodiscard]] VkPipeline CreateGuestOutputPaintPipeline(
GuestOutputPaintEffect effect, VkRenderPass render_pass);
VulkanProvider& provider_;
// Static objects for guest output presentation, used only when painting the
// main target (can be destroyed only after awaiting main target usage
// completion).
VkDescriptorSetLayout guest_output_paint_image_descriptor_set_layout_ =
VK_NULL_HANDLE;
std::array<VkPipelineLayout, kGuestOutputPaintPipelineLayoutCount>
guest_output_paint_pipeline_layouts_ = {};
VkShaderModule guest_output_paint_vs_ = VK_NULL_HANDLE;
std::array<VkShaderModule, size_t(GuestOutputPaintEffect::kCount)>
guest_output_paint_fs_ = {};
// Not compatible with the swapchain render pass even if the format is the
// same due to different dependencies (this is shader read > color
// attachment > shader read).
VkRenderPass guest_output_intermediate_render_pass_ = VK_NULL_HANDLE;
// Value monotonically increased every time a new guest output image is
// initialized, for recreation of dependent objects such as framebuffers in
// the refreshers - saving and comparing the handle in the refresher is not
// enough as Create > Destroy > Create may result in the same handle for
// actually different objects without the refresher being aware of the
// destruction.
uint64_t guest_output_image_next_version_ = 0;
std::array<GuestOutputImageInstance, kGuestOutputMailboxSize>
guest_output_images_;
VulkanSubmissionTracker guest_output_image_refresher_submission_tracker_;
// UI submission tracker with the submission index that can be given to UI
// drawers (accessible from the UI thread only, at any time).
VulkanSubmissionTracker ui_submission_tracker_;
// Accessible only by painting and by surface connection lifetime management
// (ConnectOrReconnectPaintingToSurfaceFromUIThread,
// DisconnectPaintingFromSurfaceFromUIThreadImpl) by the thread doing it, as
// well as by presenter initialization and shutdown.
PaintContext paint_context_;
};
} // namespace vulkan
} // namespace ui
} // namespace xe
#endif // XENIA_UI_D3D12_D3D12_PRESENTER_H_