HostDisplay: Add method to render screenshots at window size

This commit is contained in:
Connor McLaughlin 2021-03-06 02:11:17 +10:00
parent 757bef7b6d
commit 2aea58d056
8 changed files with 279 additions and 33 deletions

View file

@ -613,3 +613,40 @@ bool HostDisplay::WriteDisplayTextureToBuffer(std::vector<u32>* buffer, u32 resi
return true;
}
bool HostDisplay::WriteScreenshotToFile(std::string filename, bool compress_on_thread /*= false*/)
{
const u32 width = m_window_info.surface_width;
const u32 height = m_window_info.surface_height;
if (width == 0 || height == 0)
return false;
std::vector<u32> pixels;
u32 pixels_stride;
HostDisplayPixelFormat pixels_format;
if (!RenderScreenshot(width, height, &pixels, &pixels_stride, &pixels_format))
{
Log_ErrorPrintf("Failed to render %ux%u screenshot", width, height);
return false;
}
auto fp = FileSystem::OpenManagedCFile(filename.c_str(), "wb");
if (!fp)
{
Log_ErrorPrintf("Can't open file '%s': errno %d", filename.c_str(), errno);
return false;
}
const RenderAPI api = GetRenderAPI();
const bool flip_y = (api == RenderAPI::OpenGL || api == RenderAPI::OpenGLES);
if (!compress_on_thread)
{
return CompressAndWriteTextureToFile(width, height, std::move(filename), std::move(fp), true, flip_y, width, height,
std::move(pixels), pixels_stride, pixels_format);
}
std::thread compress_thread(CompressAndWriteTextureToFile, width, height, std::move(filename), std::move(fp), true,
flip_y, width, height, std::move(pixels), pixels_stride, pixels_format);
compress_thread.detach();
return true;
}

View file

@ -115,6 +115,10 @@ public:
/// Returns false if the window was completely occluded.
virtual bool Render() = 0;
/// Renders the display with postprocessing to the specified image.
virtual bool RenderScreenshot(u32 width, u32 height, std::vector<u32>* out_pixels, u32* out_stride,
HostDisplayPixelFormat* out_format) = 0;
virtual void SetVSync(bool enabled) = 0;
#ifdef WITH_IMGUI
@ -232,6 +236,9 @@ public:
bool WriteDisplayTextureToBuffer(std::vector<u32>* buffer, u32 resize_width = 0, u32 resize_height = 0,
bool clear_alpha = true);
/// Helper function to save screenshot to PNG.
bool WriteScreenshotToFile(std::string filename, bool compress_on_thread = false);
protected:
ALWAYS_INLINE bool HasSoftwareCursor() const { return static_cast<bool>(m_cursor_texture); }
ALWAYS_INLINE bool HasDisplayTexture() const { return (m_display_texture_handle != nullptr); }

View file

@ -709,6 +709,59 @@ bool D3D11HostDisplay::Render()
return true;
}
bool D3D11HostDisplay::RenderScreenshot(u32 width, u32 height, std::vector<u32>* out_pixels, u32* out_stride,
HostDisplayPixelFormat* out_format)
{
static constexpr DXGI_FORMAT format = DXGI_FORMAT_R8G8B8A8_UNORM;
static constexpr HostDisplayPixelFormat hdformat = HostDisplayPixelFormat::RGBA8;
D3D11::Texture render_texture;
if (!render_texture.Create(m_device.Get(), width, height, 1, 1, format, D3D11_BIND_RENDER_TARGET) ||
!m_readback_staging_texture.EnsureSize(m_context.Get(), width, height, format, false))
{
return false;
}
static constexpr std::array<float, 4> clear_color = {};
m_context->ClearRenderTargetView(render_texture.GetD3DRTV(), clear_color.data());
m_context->OMSetRenderTargets(1, render_texture.GetD3DRTVArray(), nullptr);
if (HasDisplayTexture())
{
const auto [left, top, draw_width, draw_height] = CalculateDrawRect(width, height, 0);
if (!m_post_processing_chain.IsEmpty())
{
ApplyPostProcessingChain(render_texture.GetD3DRTV(), left, top, draw_width, draw_height, m_display_texture_handle,
m_display_texture_width, m_display_texture_height, m_display_texture_view_x,
m_display_texture_view_y, m_display_texture_view_width, m_display_texture_view_height,
width, height);
}
else
{
RenderDisplay(left, top, draw_width, draw_height, m_display_texture_handle, m_display_texture_width,
m_display_texture_height, m_display_texture_view_x, m_display_texture_view_y,
m_display_texture_view_width, m_display_texture_view_height, m_display_linear_filtering);
}
}
m_context->OMSetRenderTargets(0, nullptr, nullptr);
m_readback_staging_texture.CopyFromTexture(m_context.Get(), render_texture, 0, 0, 0, 0, 0, width, height);
if (!m_readback_staging_texture.Map(m_context.Get(), false))
return false;
const u32 stride = sizeof(u32) * width;
out_pixels->resize(width * height);
*out_stride = stride;
*out_format = hdformat;
m_readback_staging_texture.ReadPixels<u32>(0, 0, width, height, stride, out_pixels->data());
m_readback_staging_texture.Unmap(m_context.Get());
return true;
}
void D3D11HostDisplay::RenderImGui()
{
ImGui::Render();
@ -726,7 +779,8 @@ void D3D11HostDisplay::RenderDisplay()
{
ApplyPostProcessingChain(m_swap_chain_rtv.Get(), left, top, width, height, m_display_texture_handle,
m_display_texture_width, m_display_texture_height, m_display_texture_view_x,
m_display_texture_view_y, m_display_texture_view_width, m_display_texture_view_height);
m_display_texture_view_y, m_display_texture_view_width, m_display_texture_view_height,
GetWindowWidth(), GetWindowHeight());
return;
}
@ -971,11 +1025,12 @@ bool D3D11HostDisplay::CheckPostProcessingRenderTargets(u32 target_width, u32 ta
void D3D11HostDisplay::ApplyPostProcessingChain(ID3D11RenderTargetView* final_target, s32 final_left, s32 final_top,
s32 final_width, s32 final_height, void* texture_handle,
u32 texture_width, s32 texture_height, s32 texture_view_x,
s32 texture_view_y, s32 texture_view_width, s32 texture_view_height)
s32 texture_view_y, s32 texture_view_width, s32 texture_view_height,
u32 target_width, u32 target_height)
{
static constexpr std::array<float, 4> clear_color = {0.0f, 0.0f, 0.0f, 1.0f};
if (!CheckPostProcessingRenderTargets(GetWindowWidth(), GetWindowHeight()))
if (!CheckPostProcessingRenderTargets(target_width, target_height))
{
RenderDisplay(final_left, final_top, final_width, final_height, texture_handle, texture_width, texture_height,
texture_view_x, texture_view_y, texture_view_width, texture_view_height, m_display_linear_filtering);

View file

@ -68,6 +68,8 @@ public:
virtual void SetVSync(bool enabled) override;
virtual bool Render() override;
virtual bool RenderScreenshot(u32 width, u32 height, std::vector<u32>* out_pixels, u32* out_stride,
HostDisplayPixelFormat* out_format) override;
static AdapterAndModeList StaticGetAdapterAndModeList();
@ -107,7 +109,7 @@ protected:
void ApplyPostProcessingChain(ID3D11RenderTargetView* final_target, s32 final_left, s32 final_top, s32 final_width,
s32 final_height, void* texture_handle, u32 texture_width, s32 texture_height,
s32 texture_view_x, s32 texture_view_y, s32 texture_view_width,
s32 texture_view_height);
s32 texture_view_height, u32 target_width, u32 target_height);
ComPtr<ID3D11Device> m_device;
ComPtr<ID3D11DeviceContext> m_context;

View file

@ -749,7 +749,6 @@ bool OpenGLHostDisplay::Render()
return false;
}
glDisable(GL_SCISSOR_TEST);
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0);
glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);
@ -765,6 +764,46 @@ bool OpenGLHostDisplay::Render()
return true;
}
bool OpenGLHostDisplay::RenderScreenshot(u32 width, u32 height, std::vector<u32>* out_pixels, u32* out_stride,
HostDisplayPixelFormat* out_format)
{
GL::Texture texture;
if (!texture.Create(width, height, 1, GL_RGBA8, GL_RGBA, GL_UNSIGNED_BYTE, nullptr) || !texture.CreateFramebuffer())
return false;
texture.BindFramebuffer(GL_FRAMEBUFFER);
glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);
if (HasDisplayTexture())
{
const auto [left, top, draw_width, draw_height] = CalculateDrawRect(width, height, 0);
if (!m_post_processing_chain.IsEmpty())
{
ApplyPostProcessingChain(texture.GetGLFramebufferID(), left, height - top - draw_height, draw_width, draw_height,
m_display_texture_handle, m_display_texture_width, m_display_texture_height,
m_display_texture_view_x, m_display_texture_view_y, m_display_texture_view_width,
m_display_texture_view_height, width, height);
}
else
{
RenderDisplay(left, height - top - draw_height, draw_width, draw_height, m_display_texture_handle,
m_display_texture_width, m_display_texture_height, m_display_texture_view_x,
m_display_texture_view_y, m_display_texture_view_width, m_display_texture_view_height,
m_display_linear_filtering);
}
}
out_pixels->resize(width * height);
*out_stride = sizeof(u32) * width;
*out_format = HostDisplayPixelFormat::RGBA8;
glReadPixels(0, 0, width, height, GL_RGBA, GL_UNSIGNED_BYTE, out_pixels->data());
glBindFramebuffer(GL_FRAMEBUFFER, 0);
return true;
}
void OpenGLHostDisplay::RenderImGui()
{
ImGui::Render();
@ -783,7 +822,8 @@ void OpenGLHostDisplay::RenderDisplay()
{
ApplyPostProcessingChain(0, left, GetWindowHeight() - top - height, width, height, m_display_texture_handle,
m_display_texture_width, m_display_texture_height, m_display_texture_view_x,
m_display_texture_view_y, m_display_texture_view_width, m_display_texture_view_height);
m_display_texture_view_y, m_display_texture_view_width, m_display_texture_view_height,
GetWindowWidth(), GetWindowHeight());
return;
}
@ -995,11 +1035,12 @@ bool OpenGLHostDisplay::CheckPostProcessingRenderTargets(u32 target_width, u32 t
void OpenGLHostDisplay::ApplyPostProcessingChain(GLuint final_target, s32 final_left, s32 final_top, s32 final_width,
s32 final_height, void* texture_handle, u32 texture_width,
s32 texture_height, s32 texture_view_x, s32 texture_view_y,
s32 texture_view_width, s32 texture_view_height)
s32 texture_view_width, s32 texture_view_height, u32 target_width,
u32 target_height)
{
if (!CheckPostProcessingRenderTargets(GetWindowWidth(), GetWindowHeight()))
if (!CheckPostProcessingRenderTargets(target_width, target_height))
{
RenderDisplay(final_left, GetWindowHeight() - final_top - final_height, final_width, final_height, texture_handle,
RenderDisplay(final_left, target_height - final_top - final_height, final_width, final_height, texture_handle,
texture_width, texture_height, texture_view_x, texture_view_y, texture_view_width,
texture_view_height, m_display_linear_filtering);
return;
@ -1008,7 +1049,7 @@ void OpenGLHostDisplay::ApplyPostProcessingChain(GLuint final_target, s32 final_
// downsample/upsample - use same viewport for remainder
m_post_processing_input_texture.BindFramebuffer(GL_DRAW_FRAMEBUFFER);
glClear(GL_COLOR_BUFFER_BIT);
RenderDisplay(final_left, GetWindowHeight() - final_top - final_height, final_width, final_height, texture_handle,
RenderDisplay(final_left, target_height - final_top - final_height, final_width, final_height, texture_handle,
texture_width, texture_height, texture_view_x, texture_view_y, texture_view_width, texture_view_height,
m_display_linear_filtering);

View file

@ -32,8 +32,10 @@ public:
virtual bool HasRenderDevice() const override;
virtual bool HasRenderSurface() const override;
virtual bool CreateRenderDevice(const WindowInfo& wi, std::string_view adapter_name, bool debug_device, bool threaded_presentation) override;
virtual bool InitializeRenderDevice(std::string_view shader_cache_directory, bool debug_device, bool threaded_presentation) override;
virtual bool CreateRenderDevice(const WindowInfo& wi, std::string_view adapter_name, bool debug_device,
bool threaded_presentation) override;
virtual bool InitializeRenderDevice(std::string_view shader_cache_directory, bool debug_device,
bool threaded_presentation) override;
virtual void DestroyRenderDevice() override;
virtual bool MakeRenderContextCurrent() override;
@ -65,6 +67,8 @@ public:
virtual void SetVSync(bool enabled) override;
virtual bool Render() override;
virtual bool RenderScreenshot(u32 width, u32 height, std::vector<u32>* out_pixels, u32* out_stride,
HostDisplayPixelFormat* out_format) override;
protected:
const char* GetGLSLVersionString() const;
@ -99,7 +103,8 @@ protected:
bool CheckPostProcessingRenderTargets(u32 target_width, u32 target_height);
void ApplyPostProcessingChain(GLuint final_target, s32 final_left, s32 final_top, s32 final_width, s32 final_height,
void* texture_handle, u32 texture_width, s32 texture_height, s32 texture_view_x,
s32 texture_view_y, s32 texture_view_width, s32 texture_view_height);
s32 texture_view_y, s32 texture_view_width, s32 texture_view_height, u32 target_width,
u32 target_height);
std::unique_ptr<GL::Context> m_gl_context;

View file

@ -640,14 +640,107 @@ bool VulkanHostDisplay::Render()
return true;
}
void VulkanHostDisplay::BeginSwapChainRenderPass(VkFramebuffer framebuffer)
bool VulkanHostDisplay::RenderScreenshot(u32 width, u32 height, std::vector<u32>* out_pixels, u32* out_stride,
HostDisplayPixelFormat* out_format)
{
// in theory we could do this without a swap chain, but postprocessing assumes it for now...
if (!m_swap_chain)
return false;
const VkFormat format = m_swap_chain ? m_swap_chain->GetTextureFormat() : VK_FORMAT_R8G8B8A8_UNORM;
switch (format)
{
case VK_FORMAT_R8G8B8A8_UNORM:
case VK_FORMAT_R8G8B8A8_SRGB:
*out_format = HostDisplayPixelFormat::RGBA8;
*out_stride = sizeof(u32) * width;
out_pixels->resize(width * height);
break;
case VK_FORMAT_B8G8R8A8_UNORM:
case VK_FORMAT_B8G8R8A8_SRGB:
*out_format = HostDisplayPixelFormat::BGRA8;
*out_stride = sizeof(u32) * width;
out_pixels->resize(width * height);
break;
case VK_FORMAT_A1R5G5B5_UNORM_PACK16:
*out_format = HostDisplayPixelFormat::RGBA5551;
*out_stride = sizeof(u16) * width;
out_pixels->resize(((width * height) + 1) / 2);
break;
case VK_FORMAT_R5G6B5_UNORM_PACK16:
*out_format = HostDisplayPixelFormat::RGB565;
*out_stride = sizeof(u16) * width;
out_pixels->resize(((width * height) + 1) / 2);
break;
default:
Log_ErrorPrintf("Unhandled swap chain pixel format %u", static_cast<unsigned>(format));
break;
}
Vulkan::Texture tex;
Vulkan::StagingTexture staging_tex;
if (!tex.Create(width, height, 1, 1, format, VK_SAMPLE_COUNT_1_BIT, VK_IMAGE_VIEW_TYPE_2D, VK_IMAGE_TILING_OPTIMAL,
VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT) ||
!staging_tex.Create(Vulkan::StagingBuffer::Type::Readback, format, width, height))
{
return false;
}
const VkRenderPass rp =
m_swap_chain ?
m_swap_chain->GetClearRenderPass() :
g_vulkan_context->GetRenderPass(format, VK_FORMAT_UNDEFINED, VK_SAMPLE_COUNT_1_BIT, VK_ATTACHMENT_LOAD_OP_CLEAR);
if (!rp)
return false;
const VkFramebuffer fb = tex.CreateFramebuffer(rp);
if (!fb)
return false;
tex.TransitionToLayout(g_vulkan_context->GetCurrentCommandBuffer(), VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL);
const auto [left, top, draw_width, draw_height] =
CalculateDrawRect(GetWindowWidth(), GetWindowHeight(), m_display_top_margin);
if (!m_post_processing_chain.IsEmpty())
{
ApplyPostProcessingChain(fb, left, top, draw_width, draw_height, m_display_texture_handle, m_display_texture_width,
m_display_texture_height, m_display_texture_view_x, m_display_texture_view_y,
m_display_texture_view_width, m_display_texture_view_height, width, height);
}
else
{
BeginSwapChainRenderPass(fb, width, height);
RenderDisplay(left, top, draw_width, draw_height, m_display_texture_handle, m_display_texture_width,
m_display_texture_height, m_display_texture_view_x, m_display_texture_view_y,
m_display_texture_view_width, m_display_texture_view_height, m_display_linear_filtering);
}
vkCmdEndRenderPass(g_vulkan_context->GetCurrentCommandBuffer());
tex.TransitionToLayout(g_vulkan_context->GetCurrentCommandBuffer(), VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL);
staging_tex.CopyFromTexture(tex, 0, 0, 0, 0, 0, 0, width, height);
staging_tex.ReadTexels(0, 0, width, height, out_pixels->data(), *out_stride);
// destroying these immediately should be safe since nothing's going to access them, and it's not part of the command
// stream
vkDestroyFramebuffer(g_vulkan_context->GetDevice(), fb, nullptr);
staging_tex.Destroy(false);
tex.Destroy(false);
return true;
}
void VulkanHostDisplay::BeginSwapChainRenderPass(VkFramebuffer framebuffer, u32 width, u32 height)
{
const VkClearValue clear_value = {{{0.0f, 0.0f, 0.0f, 1.0f}}};
const VkRenderPassBeginInfo rp = {VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO,
nullptr,
m_swap_chain->GetClearRenderPass(),
framebuffer,
{{0, 0}, {m_swap_chain->GetWidth(), m_swap_chain->GetHeight()}},
{{0, 0}, {width, height}},
1u,
&clear_value};
vkCmdBeginRenderPass(g_vulkan_context->GetCurrentCommandBuffer(), &rp, VK_SUBPASS_CONTENTS_INLINE);
@ -657,7 +750,8 @@ void VulkanHostDisplay::RenderDisplay()
{
if (!HasDisplayTexture())
{
BeginSwapChainRenderPass(m_swap_chain->GetCurrentFramebuffer());
BeginSwapChainRenderPass(m_swap_chain->GetCurrentFramebuffer(), m_swap_chain->GetWidth(),
m_swap_chain->GetHeight());
return;
}
@ -665,13 +759,14 @@ void VulkanHostDisplay::RenderDisplay()
if (!m_post_processing_chain.IsEmpty())
{
ApplyPostProcessingChain(left, top, width, height, m_display_texture_handle, m_display_texture_width,
m_display_texture_height, m_display_texture_view_x, m_display_texture_view_y,
m_display_texture_view_width, m_display_texture_view_height);
ApplyPostProcessingChain(m_swap_chain->GetCurrentFramebuffer(), left, top, width, height, m_display_texture_handle,
m_display_texture_width, m_display_texture_height, m_display_texture_view_x,
m_display_texture_view_y, m_display_texture_view_width, m_display_texture_view_height,
m_swap_chain->GetWidth(), m_swap_chain->GetHeight());
return;
}
BeginSwapChainRenderPass(m_swap_chain->GetCurrentFramebuffer());
BeginSwapChainRenderPass(m_swap_chain->GetCurrentFramebuffer(), m_swap_chain->GetWidth(), m_swap_chain->GetHeight());
RenderDisplay(left, top, width, height, m_display_texture_handle, m_display_texture_width, m_display_texture_height,
m_display_texture_view_x, m_display_texture_view_y, m_display_texture_view_width,
m_display_texture_view_height, m_display_linear_filtering);
@ -946,14 +1041,15 @@ bool VulkanHostDisplay::CheckPostProcessingRenderTargets(u32 target_width, u32 t
return true;
}
void VulkanHostDisplay::ApplyPostProcessingChain(s32 final_left, s32 final_top, s32 final_width, s32 final_height,
void* texture_handle, u32 texture_width, s32 texture_height,
s32 texture_view_x, s32 texture_view_y, s32 texture_view_width,
s32 texture_view_height)
void VulkanHostDisplay::ApplyPostProcessingChain(VkFramebuffer target_fb, s32 final_left, s32 final_top,
s32 final_width, s32 final_height, void* texture_handle,
u32 texture_width, s32 texture_height, s32 texture_view_x,
s32 texture_view_y, s32 texture_view_width, s32 texture_view_height,
u32 target_width, u32 target_height)
{
if (!CheckPostProcessingRenderTargets(m_swap_chain->GetWidth(), m_swap_chain->GetHeight()))
if (!CheckPostProcessingRenderTargets(target_width, target_height))
{
BeginSwapChainRenderPass(m_swap_chain->GetCurrentFramebuffer());
BeginSwapChainRenderPass(target_fb, target_width, target_height);
RenderDisplay(final_left, final_top, final_width, final_height, texture_handle, texture_width, texture_height,
texture_view_x, texture_view_y, texture_view_width, texture_view_height, m_display_linear_filtering);
return;
@ -962,7 +1058,7 @@ void VulkanHostDisplay::ApplyPostProcessingChain(s32 final_left, s32 final_top,
// downsample/upsample - use same viewport for remainder
VkCommandBuffer cmdbuffer = g_vulkan_context->GetCurrentCommandBuffer();
m_post_processing_input_texture.TransitionToLayout(cmdbuffer, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL);
BeginSwapChainRenderPass(m_post_processing_input_framebuffer);
BeginSwapChainRenderPass(m_post_processing_input_framebuffer, target_width, target_height);
RenderDisplay(final_left, final_top, final_width, final_height, texture_handle, texture_width, texture_height,
texture_view_x, texture_view_y, texture_view_width, texture_view_height, m_display_linear_filtering);
vkCmdEndRenderPass(cmdbuffer);
@ -983,11 +1079,11 @@ void VulkanHostDisplay::ApplyPostProcessingChain(s32 final_left, s32 final_top,
if (i != final_stage)
{
pps.output_texture.TransitionToLayout(cmdbuffer, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL);
BeginSwapChainRenderPass(pps.output_framebuffer);
BeginSwapChainRenderPass(pps.output_framebuffer, target_width, target_height);
}
else
{
BeginSwapChainRenderPass(m_swap_chain->GetCurrentFramebuffer());
BeginSwapChainRenderPass(target_fb, target_width, target_height);
}
const bool use_push_constants = m_post_processing_chain.GetShaderStage(i).UsePushConstants();

View file

@ -64,6 +64,8 @@ public:
virtual void SetVSync(bool enabled) override;
virtual bool Render() override;
virtual bool RenderScreenshot(u32 width, u32 height, std::vector<u32>* out_pixels, u32* out_stride,
HostDisplayPixelFormat* out_format) override;
static AdapterAndModeList StaticGetAdapterAndModeList(const WindowInfo* wi);
@ -89,9 +91,10 @@ protected:
};
bool CheckPostProcessingRenderTargets(u32 target_width, u32 target_height);
void ApplyPostProcessingChain(s32 final_left, s32 final_top, s32 final_width, s32 final_height, void* texture_handle,
u32 texture_width, s32 texture_height, s32 texture_view_x, s32 texture_view_y,
s32 texture_view_width, s32 texture_view_height);
void ApplyPostProcessingChain(VkFramebuffer target_fb, s32 final_left, s32 final_top, s32 final_width,
s32 final_height, void* texture_handle, u32 texture_width, s32 texture_height,
s32 texture_view_x, s32 texture_view_y, s32 texture_view_width, s32 texture_view_height,
u32 target_width, u32 target_height);
// Can be overridden by frontends.
virtual VkRenderPass GetRenderPassForDisplay() const;
@ -103,7 +106,7 @@ protected:
virtual void DestroyImGuiContext() override;
virtual bool UpdateImGuiFontTexture() override;
void BeginSwapChainRenderPass(VkFramebuffer framebuffer);
void BeginSwapChainRenderPass(VkFramebuffer framebuffer, u32 width, u32 height);
void RenderDisplay();
void RenderImGui();
void RenderSoftwareCursor();