citra/src/video_core/renderer_vulkan/renderer_vulkan.cpp

987 lines
40 KiB
C++

// Copyright 2023 Citra Emulator Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
#include "common/assert.h"
#include "common/logging/log.h"
#include "common/microprofile.h"
#include "common/settings.h"
#include "common/texture.h"
#include "core/core.h"
#include "core/frontend/emu_window.h"
#include "core/hw/gpu.h"
#include "core/hw/hw.h"
#include "core/hw/lcd.h"
#include "video_core/renderer_vulkan/renderer_vulkan.h"
#include "video_core/renderer_vulkan/vk_shader_util.h"
#include "video_core/host_shaders/vulkan_present_anaglyph_frag_spv.h"
#include "video_core/host_shaders/vulkan_present_frag_spv.h"
#include "video_core/host_shaders/vulkan_present_interlaced_frag_spv.h"
#include "video_core/host_shaders/vulkan_present_vert_spv.h"
#include "vulkan/vulkan_format_traits.hpp"
#include <vk_mem_alloc.h>
MICROPROFILE_DEFINE(Vulkan_RenderFrame, "Vulkan", "Render Frame", MP_RGB(128, 128, 64));
namespace Vulkan {
struct ScreenRectVertex {
ScreenRectVertex() = default;
ScreenRectVertex(float x, float y, float u, float v)
: position{Common::MakeVec(x, y)}, tex_coord{Common::MakeVec(u, v)} {}
Common::Vec2f position;
Common::Vec2f tex_coord;
};
constexpr u32 VERTEX_BUFFER_SIZE = sizeof(ScreenRectVertex) * 8192;
constexpr std::array<f32, 4 * 4> MakeOrthographicMatrix(u32 width, u32 height) {
// clang-format off
return { 2.f / width, 0.f, 0.f, -1.f,
0.f, 2.f / height, 0.f, -1.f,
0.f, 0.f, 1.f, 0.f,
0.f, 0.f, 0.f, 1.f};
// clang-format on
}
constexpr static std::array<vk::DescriptorSetLayoutBinding, 1> PRESENT_BINDINGS = {{
{0, vk::DescriptorType::eCombinedImageSampler, 3, vk::ShaderStageFlagBits::eFragment},
}};
RendererVulkan::RendererVulkan(Core::System& system, Frontend::EmuWindow& window,
Frontend::EmuWindow* secondary_window)
: RendererBase{system, window, secondary_window}, memory{system.Memory()},
instance{system.TelemetrySession(), window, Settings::values.physical_device.GetValue()},
scheduler{instance, renderpass_cache}, renderpass_cache{instance, scheduler}, pool{instance},
main_window{window, instance, scheduler},
vertex_buffer{instance, scheduler, vk::BufferUsageFlagBits::eVertexBuffer,
VERTEX_BUFFER_SIZE},
rasterizer{memory,
system.CustomTexManager(),
*this,
render_window,
instance,
scheduler,
pool,
renderpass_cache,
main_window.ImageCount()},
present_set_provider{instance, pool, PRESENT_BINDINGS} {
CompileShaders();
BuildLayouts();
BuildPipelines();
if (secondary_window) {
second_window = std::make_unique<PresentWindow>(*secondary_window, instance, scheduler);
}
}
RendererVulkan::~RendererVulkan() {
vk::Device device = instance.GetDevice();
scheduler.Finish();
device.waitIdle();
device.destroyShaderModule(present_vertex_shader);
for (u32 i = 0; i < PRESENT_PIPELINES; i++) {
device.destroyPipeline(present_pipelines[i]);
device.destroyShaderModule(present_shaders[i]);
}
for (auto& sampler : present_samplers) {
device.destroySampler(sampler);
}
for (auto& info : screen_infos) {
device.destroyImageView(info.texture.image_view);
vmaDestroyImage(instance.GetAllocator(), info.texture.image, info.texture.allocation);
}
}
void RendererVulkan::Sync() {
rasterizer.SyncEntireState();
}
void RendererVulkan::PrepareRendertarget() {
for (u32 i = 0; i < 3; i++) {
const u32 fb_id = i == 2 ? 1 : 0;
const auto& framebuffer = GPU::g_regs.framebuffer_config[fb_id];
// Main LCD (0): 0x1ED02204, Sub LCD (1): 0x1ED02A04
u32 lcd_color_addr =
(fb_id == 0) ? LCD_REG_INDEX(color_fill_top) : LCD_REG_INDEX(color_fill_bottom);
lcd_color_addr = HW::VADDR_LCD + 4 * lcd_color_addr;
LCD::Regs::ColorFill color_fill{0};
LCD::Read(color_fill.raw, lcd_color_addr);
if (color_fill.is_enabled) {
LoadColorToActiveVkTexture(color_fill.color_r, color_fill.color_g, color_fill.color_b,
screen_infos[i].texture);
} else {
TextureInfo& texture = screen_infos[i].texture;
if (texture.width != framebuffer.width || texture.height != framebuffer.height ||
texture.format != framebuffer.color_format) {
// Reallocate texture if the framebuffer size has changed.
// This is expected to not happen very often and hence should not be a
// performance problem.
ConfigureFramebufferTexture(texture, framebuffer);
}
LoadFBToScreenInfo(framebuffer, screen_infos[i], i == 1);
// Resize the texture in case the framebuffer size has changed
texture.width = framebuffer.width;
texture.height = framebuffer.height;
}
}
}
void RendererVulkan::PrepareDraw(Frame* frame, const Layout::FramebufferLayout& layout) {
const auto sampler = present_samplers[!Settings::values.filter_mode.GetValue()];
std::transform(screen_infos.begin(), screen_infos.end(), present_textures.begin(),
[&](auto& info) {
return DescriptorData{vk::DescriptorImageInfo{sampler, info.image_view,
vk::ImageLayout::eGeneral}};
});
const auto descriptor_set = present_set_provider.Acquire(present_textures);
renderpass_cache.EndRendering();
scheduler.Record([this, layout, frame, descriptor_set, renderpass = main_window.Renderpass(),
index = current_pipeline](vk::CommandBuffer cmdbuf) {
const vk::Viewport viewport = {
.x = 0.0f,
.y = 0.0f,
.width = static_cast<float>(layout.width),
.height = static_cast<float>(layout.height),
.minDepth = 0.0f,
.maxDepth = 1.0f,
};
const vk::Rect2D scissor = {
.offset = {0, 0},
.extent = {layout.width, layout.height},
};
cmdbuf.setViewport(0, viewport);
cmdbuf.setScissor(0, scissor);
const vk::ClearValue clear{.color = clear_color};
const vk::PipelineLayout layout{*present_pipeline_layout};
const vk::RenderPassBeginInfo renderpass_begin_info = {
.renderPass = renderpass,
.framebuffer = frame->framebuffer,
.renderArea =
vk::Rect2D{
.offset = {0, 0},
.extent = {frame->width, frame->height},
},
.clearValueCount = 1,
.pClearValues = &clear,
};
cmdbuf.beginRenderPass(renderpass_begin_info, vk::SubpassContents::eInline);
cmdbuf.bindPipeline(vk::PipelineBindPoint::eGraphics, present_pipelines[index]);
cmdbuf.bindDescriptorSets(vk::PipelineBindPoint::eGraphics, layout, 0, descriptor_set, {});
});
}
void RendererVulkan::RenderToWindow(PresentWindow& window, const Layout::FramebufferLayout& layout,
bool flipped) {
Frame* frame = window.GetRenderFrame();
if (layout.width != frame->width || layout.height != frame->height) {
window.WaitPresent();
scheduler.Finish();
window.RecreateFrame(frame, layout.width, layout.height);
}
DrawScreens(frame, layout, flipped);
scheduler.Flush(frame->render_ready);
window.Present(frame);
}
void RendererVulkan::LoadFBToScreenInfo(const GPU::Regs::FramebufferConfig& framebuffer,
ScreenInfo& screen_info, bool right_eye) {
if (framebuffer.address_right1 == 0 || framebuffer.address_right2 == 0) {
right_eye = false;
}
const PAddr framebuffer_addr =
framebuffer.active_fb == 0
? (right_eye ? framebuffer.address_right1 : framebuffer.address_left1)
: (right_eye ? framebuffer.address_right2 : framebuffer.address_left2);
LOG_TRACE(Render_Vulkan, "0x{:08x} bytes from 0x{:08x}({}x{}), fmt {:x}",
framebuffer.stride * framebuffer.height, framebuffer_addr, framebuffer.width.Value(),
framebuffer.height.Value(), framebuffer.format);
const int bpp = GPU::Regs::BytesPerPixel(framebuffer.color_format);
const std::size_t pixel_stride = framebuffer.stride / bpp;
ASSERT(pixel_stride * bpp == framebuffer.stride);
ASSERT(pixel_stride % 4 == 0);
if (!rasterizer.AccelerateDisplay(framebuffer, framebuffer_addr, static_cast<u32>(pixel_stride),
screen_info)) {
// Reset the screen info's display texture to its own permanent texture
screen_info.image_view = screen_info.texture.image_view;
screen_info.texcoords = {0.f, 0.f, 1.f, 1.f};
ASSERT(false);
}
}
void RendererVulkan::CompileShaders() {
vk::Device device = instance.GetDevice();
present_vertex_shader = CompileSPV(VULKAN_PRESENT_VERT_SPV, device);
present_shaders[0] = CompileSPV(VULKAN_PRESENT_FRAG_SPV, device);
present_shaders[1] = CompileSPV(VULKAN_PRESENT_ANAGLYPH_FRAG_SPV, device);
present_shaders[2] = CompileSPV(VULKAN_PRESENT_INTERLACED_FRAG_SPV, device);
auto properties = instance.GetPhysicalDevice().getProperties();
for (std::size_t i = 0; i < present_samplers.size(); i++) {
const vk::Filter filter_mode = i == 0 ? vk::Filter::eLinear : vk::Filter::eNearest;
const vk::SamplerCreateInfo sampler_info = {
.magFilter = filter_mode,
.minFilter = filter_mode,
.mipmapMode = vk::SamplerMipmapMode::eLinear,
.addressModeU = vk::SamplerAddressMode::eClampToEdge,
.addressModeV = vk::SamplerAddressMode::eClampToEdge,
.anisotropyEnable = instance.IsAnisotropicFilteringSupported(),
.maxAnisotropy = properties.limits.maxSamplerAnisotropy,
.compareEnable = false,
.compareOp = vk::CompareOp::eAlways,
.borderColor = vk::BorderColor::eIntOpaqueBlack,
.unnormalizedCoordinates = false,
};
present_samplers[i] = device.createSampler(sampler_info);
}
}
void RendererVulkan::BuildLayouts() {
const vk::PushConstantRange push_range = {
.stageFlags = vk::ShaderStageFlagBits::eVertex | vk::ShaderStageFlagBits::eFragment,
.offset = 0,
.size = sizeof(PresentUniformData),
};
const auto descriptor_set_layout = present_set_provider.Layout();
const vk::PipelineLayoutCreateInfo layout_info = {
.setLayoutCount = 1,
.pSetLayouts = &descriptor_set_layout,
.pushConstantRangeCount = 1,
.pPushConstantRanges = &push_range,
};
present_pipeline_layout = instance.GetDevice().createPipelineLayoutUnique(layout_info);
}
void RendererVulkan::BuildPipelines() {
const vk::VertexInputBindingDescription binding = {
.binding = 0,
.stride = sizeof(ScreenRectVertex),
.inputRate = vk::VertexInputRate::eVertex,
};
const std::array attributes = {
vk::VertexInputAttributeDescription{
.location = 0,
.binding = 0,
.format = vk::Format::eR32G32Sfloat,
.offset = offsetof(ScreenRectVertex, position),
},
vk::VertexInputAttributeDescription{
.location = 1,
.binding = 0,
.format = vk::Format::eR32G32Sfloat,
.offset = offsetof(ScreenRectVertex, tex_coord),
},
};
const vk::PipelineVertexInputStateCreateInfo vertex_input_info = {
.vertexBindingDescriptionCount = 1,
.pVertexBindingDescriptions = &binding,
.vertexAttributeDescriptionCount = static_cast<u32>(attributes.size()),
.pVertexAttributeDescriptions = attributes.data(),
};
const vk::PipelineInputAssemblyStateCreateInfo input_assembly = {
.topology = vk::PrimitiveTopology::eTriangleStrip,
.primitiveRestartEnable = false,
};
const vk::PipelineRasterizationStateCreateInfo raster_state = {
.depthClampEnable = false,
.rasterizerDiscardEnable = false,
.cullMode = vk::CullModeFlagBits::eNone,
.frontFace = vk::FrontFace::eClockwise,
.depthBiasEnable = false,
.lineWidth = 1.0f,
};
const vk::PipelineMultisampleStateCreateInfo multisampling = {
.rasterizationSamples = vk::SampleCountFlagBits::e1,
.sampleShadingEnable = false,
};
const vk::PipelineColorBlendAttachmentState colorblend_attachment = {
.blendEnable = false,
.colorWriteMask = vk::ColorComponentFlagBits::eR | vk::ColorComponentFlagBits::eG |
vk::ColorComponentFlagBits::eB | vk::ColorComponentFlagBits::eA,
};
const vk::PipelineColorBlendStateCreateInfo color_blending = {
.logicOpEnable = false,
.attachmentCount = 1,
.pAttachments = &colorblend_attachment,
.blendConstants = std::array{1.0f, 1.0f, 1.0f, 1.0f},
};
const vk::Viewport placeholder_viewport = vk::Viewport{0.0f, 0.0f, 1.0f, 1.0f, 0.0f, 1.0f};
const vk::Rect2D placeholder_scissor = vk::Rect2D{{0, 0}, {1, 1}};
const vk::PipelineViewportStateCreateInfo viewport_info = {
.viewportCount = 1,
.pViewports = &placeholder_viewport,
.scissorCount = 1,
.pScissors = &placeholder_scissor,
};
const std::array dynamic_states = {
vk::DynamicState::eViewport,
vk::DynamicState::eScissor,
};
const vk::PipelineDynamicStateCreateInfo dynamic_info = {
.dynamicStateCount = static_cast<u32>(dynamic_states.size()),
.pDynamicStates = dynamic_states.data(),
};
const vk::PipelineDepthStencilStateCreateInfo depth_info = {
.depthTestEnable = false,
.depthWriteEnable = false,
.depthCompareOp = vk::CompareOp::eAlways,
.depthBoundsTestEnable = false,
.stencilTestEnable = false,
};
for (u32 i = 0; i < PRESENT_PIPELINES; i++) {
const std::array shader_stages = {
vk::PipelineShaderStageCreateInfo{
.stage = vk::ShaderStageFlagBits::eVertex,
.module = present_vertex_shader,
.pName = "main",
},
vk::PipelineShaderStageCreateInfo{
.stage = vk::ShaderStageFlagBits::eFragment,
.module = present_shaders[i],
.pName = "main",
},
};
const vk::GraphicsPipelineCreateInfo pipeline_info = {
.stageCount = static_cast<u32>(shader_stages.size()),
.pStages = shader_stages.data(),
.pVertexInputState = &vertex_input_info,
.pInputAssemblyState = &input_assembly,
.pViewportState = &viewport_info,
.pRasterizationState = &raster_state,
.pMultisampleState = &multisampling,
.pDepthStencilState = &depth_info,
.pColorBlendState = &color_blending,
.pDynamicState = &dynamic_info,
.layout = *present_pipeline_layout,
.renderPass = main_window.Renderpass(),
};
const auto [result, pipeline] =
instance.GetDevice().createGraphicsPipeline({}, pipeline_info);
ASSERT_MSG(result == vk::Result::eSuccess, "Unable to build present pipelines");
present_pipelines[i] = pipeline;
}
}
void RendererVulkan::ConfigureFramebufferTexture(TextureInfo& texture,
const GPU::Regs::FramebufferConfig& framebuffer) {
vk::Device device = instance.GetDevice();
if (texture.image_view) {
device.destroyImageView(texture.image_view);
}
if (texture.image) {
vmaDestroyImage(instance.GetAllocator(), texture.image, texture.allocation);
}
const VideoCore::PixelFormat pixel_format =
VideoCore::PixelFormatFromGPUPixelFormat(framebuffer.color_format);
const vk::Format format = instance.GetTraits(pixel_format).native;
const vk::ImageCreateInfo image_info = {
.imageType = vk::ImageType::e2D,
.format = format,
.extent = {framebuffer.width, framebuffer.height, 1},
.mipLevels = 1,
.arrayLayers = 1,
.samples = vk::SampleCountFlagBits::e1,
.usage = vk::ImageUsageFlagBits::eSampled,
};
const VmaAllocationCreateInfo alloc_info = {
.flags = VMA_ALLOCATION_CREATE_WITHIN_BUDGET_BIT,
.usage = VMA_MEMORY_USAGE_AUTO_PREFER_DEVICE,
.requiredFlags = 0,
.preferredFlags = 0,
.pool = VK_NULL_HANDLE,
.pUserData = nullptr,
};
VkImage unsafe_image{};
VkImageCreateInfo unsafe_image_info = static_cast<VkImageCreateInfo>(image_info);
VkResult result = vmaCreateImage(instance.GetAllocator(), &unsafe_image_info, &alloc_info,
&unsafe_image, &texture.allocation, nullptr);
if (result != VK_SUCCESS) [[unlikely]] {
LOG_CRITICAL(Render_Vulkan, "Failed allocating texture with error {}", result);
UNREACHABLE();
}
texture.image = vk::Image{unsafe_image};
const vk::ImageViewCreateInfo view_info = {
.image = texture.image,
.viewType = vk::ImageViewType::e2D,
.format = format,
.subresourceRange{
.aspectMask = vk::ImageAspectFlagBits::eColor,
.baseMipLevel = 0,
.levelCount = 1,
.baseArrayLayer = 0,
.layerCount = 1,
},
};
texture.image_view = device.createImageView(view_info);
texture.width = framebuffer.width;
texture.height = framebuffer.height;
texture.format = framebuffer.color_format;
}
void RendererVulkan::LoadColorToActiveVkTexture(u8 color_r, u8 color_g, u8 color_b,
const TextureInfo& texture) {
const vk::ClearColorValue clear_color = {
.float32 =
std::array{
color_r / 255.0f,
color_g / 255.0f,
color_b / 255.0f,
1.0f,
},
};
renderpass_cache.EndRendering();
scheduler.Record([image = texture.image, clear_color](vk::CommandBuffer cmdbuf) {
const vk::ImageSubresourceRange range = {
.aspectMask = vk::ImageAspectFlagBits::eColor,
.baseMipLevel = 0,
.levelCount = VK_REMAINING_MIP_LEVELS,
.baseArrayLayer = 0,
.layerCount = VK_REMAINING_ARRAY_LAYERS,
};
const vk::ImageMemoryBarrier pre_barrier = {
.srcAccessMask = vk::AccessFlagBits::eShaderRead | vk::AccessFlagBits::eTransferRead,
.dstAccessMask = vk::AccessFlagBits::eTransferWrite,
.oldLayout = vk::ImageLayout::eGeneral,
.newLayout = vk::ImageLayout::eTransferDstOptimal,
.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED,
.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED,
.image = image,
.subresourceRange = range,
};
const vk::ImageMemoryBarrier post_barrier = {
.srcAccessMask = vk::AccessFlagBits::eTransferWrite,
.dstAccessMask = vk::AccessFlagBits::eShaderRead | vk::AccessFlagBits::eTransferRead,
.oldLayout = vk::ImageLayout::eTransferDstOptimal,
.newLayout = vk::ImageLayout::eGeneral,
.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED,
.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED,
.image = image,
.subresourceRange = range,
};
cmdbuf.pipelineBarrier(vk::PipelineStageFlagBits::eFragmentShader,
vk::PipelineStageFlagBits::eTransfer,
vk::DependencyFlagBits::eByRegion, {}, {}, pre_barrier);
cmdbuf.clearColorImage(image, vk::ImageLayout::eTransferDstOptimal, clear_color, range);
cmdbuf.pipelineBarrier(vk::PipelineStageFlagBits::eTransfer,
vk::PipelineStageFlagBits::eFragmentShader,
vk::DependencyFlagBits::eByRegion, {}, {}, post_barrier);
});
}
void RendererVulkan::ReloadPipeline() {
const Settings::StereoRenderOption render_3d = Settings::values.render_3d.GetValue();
switch (render_3d) {
case Settings::StereoRenderOption::Anaglyph:
current_pipeline = 1;
break;
case Settings::StereoRenderOption::Interlaced:
case Settings::StereoRenderOption::ReverseInterlaced:
current_pipeline = 2;
draw_info.reverse_interlaced = render_3d == Settings::StereoRenderOption::ReverseInterlaced;
break;
default:
current_pipeline = 0;
break;
}
}
void RendererVulkan::DrawSingleScreen(u32 screen_id, float x, float y, float w, float h,
Layout::DisplayOrientation orientation) {
const ScreenInfo& screen_info = screen_infos[screen_id];
const auto& texcoords = screen_info.texcoords;
std::array<ScreenRectVertex, 4> vertices;
switch (orientation) {
case Layout::DisplayOrientation::Landscape:
vertices = {{
ScreenRectVertex(x, y, texcoords.bottom, texcoords.left),
ScreenRectVertex(x + w, y, texcoords.bottom, texcoords.right),
ScreenRectVertex(x, y + h, texcoords.top, texcoords.left),
ScreenRectVertex(x + w, y + h, texcoords.top, texcoords.right),
}};
break;
case Layout::DisplayOrientation::Portrait:
vertices = {{
ScreenRectVertex(x, y, texcoords.bottom, texcoords.right),
ScreenRectVertex(x + w, y, texcoords.top, texcoords.right),
ScreenRectVertex(x, y + h, texcoords.bottom, texcoords.left),
ScreenRectVertex(x + w, y + h, texcoords.top, texcoords.left),
}};
std::swap(h, w);
break;
case Layout::DisplayOrientation::LandscapeFlipped:
vertices = {{
ScreenRectVertex(x, y, texcoords.top, texcoords.right),
ScreenRectVertex(x + w, y, texcoords.top, texcoords.left),
ScreenRectVertex(x, y + h, texcoords.bottom, texcoords.right),
ScreenRectVertex(x + w, y + h, texcoords.bottom, texcoords.left),
}};
break;
case Layout::DisplayOrientation::PortraitFlipped:
vertices = {{
ScreenRectVertex(x, y, texcoords.top, texcoords.left),
ScreenRectVertex(x + w, y, texcoords.bottom, texcoords.left),
ScreenRectVertex(x, y + h, texcoords.top, texcoords.right),
ScreenRectVertex(x + w, y + h, texcoords.bottom, texcoords.right),
}};
std::swap(h, w);
break;
default:
LOG_ERROR(Render_Vulkan, "Unknown DisplayOrientation: {}", orientation);
break;
}
const u64 size = sizeof(ScreenRectVertex) * vertices.size();
auto [data, offset, invalidate] = vertex_buffer.Map(size, 16);
std::memcpy(data, vertices.data(), size);
vertex_buffer.Commit(size);
const u32 scale_factor = GetResolutionScaleFactor();
draw_info.i_resolution =
Common::MakeVec(static_cast<f32>(screen_info.texture.width * scale_factor),
static_cast<f32>(screen_info.texture.height * scale_factor),
1.0f / static_cast<f32>(screen_info.texture.width * scale_factor),
1.0f / static_cast<f32>(screen_info.texture.height * scale_factor));
draw_info.o_resolution = Common::MakeVec(h, w, 1.0f / h, 1.0f / w);
draw_info.screen_id_l = screen_id;
scheduler.Record([this, offset = offset, info = draw_info](vk::CommandBuffer cmdbuf) {
const u32 first_vertex = static_cast<u32>(offset) / sizeof(ScreenRectVertex);
cmdbuf.pushConstants(*present_pipeline_layout,
vk::ShaderStageFlagBits::eFragment | vk::ShaderStageFlagBits::eVertex,
0, sizeof(info), &info);
cmdbuf.bindVertexBuffers(0, vertex_buffer.Handle(), {0});
cmdbuf.draw(4, 1, first_vertex, 0);
});
}
void RendererVulkan::DrawSingleScreenStereo(u32 screen_id_l, u32 screen_id_r, float x, float y,
float w, float h,
Layout::DisplayOrientation orientation) {
const ScreenInfo& screen_info_l = screen_infos[screen_id_l];
const auto& texcoords = screen_info_l.texcoords;
std::array<ScreenRectVertex, 4> vertices;
switch (orientation) {
case Layout::DisplayOrientation::Landscape:
vertices = {{
ScreenRectVertex(x, y, texcoords.bottom, texcoords.left),
ScreenRectVertex(x + w, y, texcoords.bottom, texcoords.right),
ScreenRectVertex(x, y + h, texcoords.top, texcoords.left),
ScreenRectVertex(x + w, y + h, texcoords.top, texcoords.right),
}};
break;
case Layout::DisplayOrientation::Portrait:
vertices = {{
ScreenRectVertex(x, y, texcoords.bottom, texcoords.right),
ScreenRectVertex(x + w, y, texcoords.top, texcoords.right),
ScreenRectVertex(x, y + h, texcoords.bottom, texcoords.left),
ScreenRectVertex(x + w, y + h, texcoords.top, texcoords.left),
}};
std::swap(h, w);
break;
case Layout::DisplayOrientation::LandscapeFlipped:
vertices = {{
ScreenRectVertex(x, y, texcoords.top, texcoords.right),
ScreenRectVertex(x + w, y, texcoords.top, texcoords.left),
ScreenRectVertex(x, y + h, texcoords.bottom, texcoords.right),
ScreenRectVertex(x + w, y + h, texcoords.bottom, texcoords.left),
}};
break;
case Layout::DisplayOrientation::PortraitFlipped:
vertices = {{
ScreenRectVertex(x, y, texcoords.top, texcoords.left),
ScreenRectVertex(x + w, y, texcoords.bottom, texcoords.left),
ScreenRectVertex(x, y + h, texcoords.top, texcoords.right),
ScreenRectVertex(x + w, y + h, texcoords.bottom, texcoords.right),
}};
std::swap(h, w);
break;
default:
LOG_ERROR(Render_Vulkan, "Unknown DisplayOrientation: {}", orientation);
break;
}
const u64 size = sizeof(ScreenRectVertex) * vertices.size();
auto [data, offset, invalidate] = vertex_buffer.Map(size, 16);
std::memcpy(data, vertices.data(), size);
vertex_buffer.Commit(size);
const u32 scale_factor = GetResolutionScaleFactor();
draw_info.i_resolution =
Common::MakeVec(static_cast<f32>(screen_info_l.texture.width * scale_factor),
static_cast<f32>(screen_info_l.texture.height * scale_factor),
1.0f / static_cast<f32>(screen_info_l.texture.width * scale_factor),
1.0f / static_cast<f32>(screen_info_l.texture.height * scale_factor));
draw_info.o_resolution = Common::MakeVec(h, w, 1.0f / h, 1.0f / w);
draw_info.screen_id_l = screen_id_l;
draw_info.screen_id_r = screen_id_r;
scheduler.Record([this, offset = offset, info = draw_info](vk::CommandBuffer cmdbuf) {
const u32 first_vertex = static_cast<u32>(offset) / sizeof(ScreenRectVertex);
cmdbuf.pushConstants(*present_pipeline_layout,
vk::ShaderStageFlagBits::eFragment | vk::ShaderStageFlagBits::eVertex,
0, sizeof(info), &info);
cmdbuf.bindVertexBuffers(0, vertex_buffer.Handle(), {0});
cmdbuf.draw(4, 1, first_vertex, 0);
});
}
void RendererVulkan::DrawTopScreen(const Layout::FramebufferLayout& layout,
const Common::Rectangle<u32>& top_screen) {
if (!layout.top_screen_enabled) {
return;
}
const float top_screen_left = static_cast<float>(top_screen.left);
const float top_screen_top = static_cast<float>(top_screen.top);
const float top_screen_width = static_cast<float>(top_screen.GetWidth());
const float top_screen_height = static_cast<float>(top_screen.GetHeight());
const auto orientation = layout.is_rotated ? Layout::DisplayOrientation::Landscape
: Layout::DisplayOrientation::Portrait;
switch (Settings::values.render_3d.GetValue()) {
case Settings::StereoRenderOption::Off: {
const int eye = static_cast<int>(Settings::values.mono_render_option.GetValue());
DrawSingleScreen(eye, top_screen_left, top_screen_top, top_screen_width, top_screen_height,
orientation);
break;
}
case Settings::StereoRenderOption::SideBySide: {
DrawSingleScreen(0, top_screen_left / 2, top_screen_top, top_screen_width / 2,
top_screen_height, orientation);
draw_info.layer = 1;
DrawSingleScreen(1, static_cast<float>((top_screen_left / 2) + (layout.width / 2)),
top_screen_top, top_screen_width / 2, top_screen_height, orientation);
break;
}
case Settings::StereoRenderOption::CardboardVR: {
DrawSingleScreen(0, top_screen_left, top_screen_top, top_screen_width, top_screen_height,
orientation);
draw_info.layer = 1;
DrawSingleScreen(
1, static_cast<float>(layout.cardboard.top_screen_right_eye + (layout.width / 2)),
top_screen_top, top_screen_width, top_screen_height, orientation);
break;
}
case Settings::StereoRenderOption::Anaglyph:
case Settings::StereoRenderOption::Interlaced:
case Settings::StereoRenderOption::ReverseInterlaced: {
DrawSingleScreenStereo(0, 1, top_screen_left, top_screen_top, top_screen_width,
top_screen_height, orientation);
break;
}
}
}
void RendererVulkan::DrawBottomScreen(const Layout::FramebufferLayout& layout,
const Common::Rectangle<u32>& bottom_screen) {
if (!layout.bottom_screen_enabled) {
return;
}
const float bottom_screen_left = static_cast<float>(bottom_screen.left);
const float bottom_screen_top = static_cast<float>(bottom_screen.top);
const float bottom_screen_width = static_cast<float>(bottom_screen.GetWidth());
const float bottom_screen_height = static_cast<float>(bottom_screen.GetHeight());
const auto orientation = layout.is_rotated ? Layout::DisplayOrientation::Landscape
: Layout::DisplayOrientation::Portrait;
switch (Settings::values.render_3d.GetValue()) {
case Settings::StereoRenderOption::Off: {
DrawSingleScreen(2, bottom_screen_left, bottom_screen_top, bottom_screen_width,
bottom_screen_height, orientation);
break;
}
case Settings::StereoRenderOption::SideBySide: {
DrawSingleScreen(2, bottom_screen_left / 2, bottom_screen_top, bottom_screen_width / 2,
bottom_screen_height, orientation);
draw_info.layer = 1;
DrawSingleScreen(2, static_cast<float>((bottom_screen_left / 2) + (layout.width / 2)),
bottom_screen_top, bottom_screen_width / 2, bottom_screen_height,
orientation);
break;
}
case Settings::StereoRenderOption::CardboardVR: {
DrawSingleScreen(2, bottom_screen_left, bottom_screen_top, bottom_screen_width,
bottom_screen_height, orientation);
draw_info.layer = 1;
DrawSingleScreen(
2, static_cast<float>(layout.cardboard.bottom_screen_right_eye + (layout.width / 2)),
bottom_screen_top, bottom_screen_width, bottom_screen_height, orientation);
break;
}
case Settings::StereoRenderOption::Anaglyph:
case Settings::StereoRenderOption::Interlaced:
case Settings::StereoRenderOption::ReverseInterlaced: {
DrawSingleScreenStereo(2, 2, bottom_screen_left, bottom_screen_top, bottom_screen_width,
bottom_screen_height, orientation);
break;
}
}
}
void RendererVulkan::DrawScreens(Frame* frame, const Layout::FramebufferLayout& layout,
bool flipped) {
if (settings.bg_color_update_requested.exchange(false)) {
clear_color.float32[0] = Settings::values.bg_red.GetValue();
clear_color.float32[1] = Settings::values.bg_green.GetValue();
clear_color.float32[2] = Settings::values.bg_blue.GetValue();
}
if (settings.shader_update_requested.exchange(false)) {
ReloadPipeline();
}
PrepareDraw(frame, layout);
const auto& top_screen = layout.top_screen;
const auto& bottom_screen = layout.bottom_screen;
draw_info.modelview = MakeOrthographicMatrix(layout.width, layout.height);
draw_info.layer = 0;
if (!Settings::values.swap_screen.GetValue()) {
DrawTopScreen(layout, top_screen);
draw_info.layer = 0;
DrawBottomScreen(layout, bottom_screen);
} else {
DrawBottomScreen(layout, bottom_screen);
draw_info.layer = 0;
DrawTopScreen(layout, top_screen);
}
if (layout.additional_screen_enabled) {
const auto& additional_screen = layout.additional_screen;
if (!Settings::values.swap_screen.GetValue()) {
DrawTopScreen(layout, additional_screen);
} else {
DrawBottomScreen(layout, additional_screen);
}
}
scheduler.Record([image = frame->image](vk::CommandBuffer cmdbuf) {
const vk::ImageMemoryBarrier render_barrier = {
.srcAccessMask = vk::AccessFlagBits::eColorAttachmentWrite,
.dstAccessMask = vk::AccessFlagBits::eTransferRead,
.oldLayout = vk::ImageLayout::eTransferSrcOptimal,
.newLayout = vk::ImageLayout::eTransferSrcOptimal,
.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED,
.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED,
.image = image,
.subresourceRange{
.aspectMask = vk::ImageAspectFlagBits::eColor,
.baseMipLevel = 0,
.levelCount = 1,
.baseArrayLayer = 0,
.layerCount = VK_REMAINING_ARRAY_LAYERS,
},
};
cmdbuf.endRenderPass();
cmdbuf.pipelineBarrier(vk::PipelineStageFlagBits::eColorAttachmentOutput,
vk::PipelineStageFlagBits::eTransfer,
vk::DependencyFlagBits::eByRegion, {}, {}, render_barrier);
});
}
void RendererVulkan::SwapBuffers() {
const Layout::FramebufferLayout& layout = render_window.GetFramebufferLayout();
PrepareRendertarget();
RenderScreenshot();
RenderToWindow(main_window, layout, false);
#ifndef ANDROID
if (Settings::values.layout_option.GetValue() == Settings::LayoutOption::SeparateWindows) {
ASSERT(secondary_window);
const auto& secondary_layout = secondary_window->GetFramebufferLayout();
if (!second_window) {
second_window = std::make_unique<PresentWindow>(*secondary_window, instance, scheduler);
}
RenderToWindow(*second_window, secondary_layout, false);
secondary_window->PollEvents();
}
#endif
rasterizer.TickFrame();
EndFrame();
}
void RendererVulkan::RenderScreenshot() {
if (!settings.screenshot_requested.exchange(false)) {
return;
}
const Layout::FramebufferLayout layout{settings.screenshot_framebuffer_layout};
const u32 width = layout.width;
const u32 height = layout.height;
const vk::BufferCreateInfo staging_buffer_info = {
.size = width * height * 4,
.usage = vk::BufferUsageFlagBits::eTransferDst,
};
const VmaAllocationCreateInfo alloc_create_info = {
.flags = VMA_ALLOCATION_CREATE_WITHIN_BUDGET_BIT | VMA_ALLOCATION_CREATE_MAPPED_BIT |
VMA_ALLOCATION_CREATE_HOST_ACCESS_RANDOM_BIT,
.usage = VMA_MEMORY_USAGE_AUTO_PREFER_HOST,
.requiredFlags = 0,
.preferredFlags = 0,
.pool = VK_NULL_HANDLE,
.pUserData = nullptr,
};
VkBuffer unsafe_buffer{};
VmaAllocation allocation{};
VmaAllocationInfo alloc_info;
VkBufferCreateInfo unsafe_buffer_info = static_cast<VkBufferCreateInfo>(staging_buffer_info);
VkResult result = vmaCreateBuffer(instance.GetAllocator(), &unsafe_buffer_info,
&alloc_create_info, &unsafe_buffer, &allocation, &alloc_info);
if (result != VK_SUCCESS) [[unlikely]] {
LOG_CRITICAL(Render_Vulkan, "Failed allocating texture with error {}", result);
UNREACHABLE();
}
vk::Buffer staging_buffer{unsafe_buffer};
Frame frame{};
main_window.RecreateFrame(&frame, width, height);
DrawScreens(&frame, layout, false);
scheduler.Record(
[width, height, source_image = frame.image, staging_buffer](vk::CommandBuffer cmdbuf) {
const vk::ImageMemoryBarrier read_barrier = {
.srcAccessMask = vk::AccessFlagBits::eMemoryWrite,
.dstAccessMask = vk::AccessFlagBits::eTransferRead,
.oldLayout = vk::ImageLayout::eTransferSrcOptimal,
.newLayout = vk::ImageLayout::eTransferSrcOptimal,
.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED,
.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED,
.image = source_image,
.subresourceRange{
.aspectMask = vk::ImageAspectFlagBits::eColor,
.baseMipLevel = 0,
.levelCount = VK_REMAINING_MIP_LEVELS,
.baseArrayLayer = 0,
.layerCount = VK_REMAINING_ARRAY_LAYERS,
},
};
const vk::ImageMemoryBarrier write_barrier = {
.srcAccessMask = vk::AccessFlagBits::eTransferRead,
.dstAccessMask = vk::AccessFlagBits::eMemoryWrite,
.oldLayout = vk::ImageLayout::eTransferSrcOptimal,
.newLayout = vk::ImageLayout::eTransferSrcOptimal,
.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED,
.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED,
.image = source_image,
.subresourceRange{
.aspectMask = vk::ImageAspectFlagBits::eColor,
.baseMipLevel = 0,
.levelCount = VK_REMAINING_MIP_LEVELS,
.baseArrayLayer = 0,
.layerCount = VK_REMAINING_ARRAY_LAYERS,
},
};
static constexpr vk::MemoryBarrier memory_write_barrier = {
.srcAccessMask = vk::AccessFlagBits::eMemoryWrite,
.dstAccessMask = vk::AccessFlagBits::eMemoryRead | vk::AccessFlagBits::eMemoryWrite,
};
const vk::BufferImageCopy image_copy = {
.bufferOffset = 0,
.bufferRowLength = 0,
.bufferImageHeight = 0,
.imageSubresource =
{
.aspectMask = vk::ImageAspectFlagBits::eColor,
.mipLevel = 0,
.baseArrayLayer = 0,
.layerCount = 1,
},
.imageOffset = {0, 0, 0},
.imageExtent = {width, height, 1},
};
cmdbuf.pipelineBarrier(vk::PipelineStageFlagBits::eAllCommands,
vk::PipelineStageFlagBits::eTransfer,
vk::DependencyFlagBits::eByRegion, {}, {}, read_barrier);
cmdbuf.copyImageToBuffer(source_image, vk::ImageLayout::eTransferSrcOptimal,
staging_buffer, image_copy);
cmdbuf.pipelineBarrier(
vk::PipelineStageFlagBits::eTransfer, vk::PipelineStageFlagBits::eAllCommands,
vk::DependencyFlagBits::eByRegion, memory_write_barrier, {}, write_barrier);
});
// Ensure the copy is fully completed before saving the screenshot
scheduler.Finish();
const vk::Device device = instance.GetDevice();
// Copy backing image data to the QImage screenshot buffer
std::memcpy(settings.screenshot_bits, alloc_info.pMappedData, staging_buffer_info.size);
// Destroy allocated resources
vmaDestroyBuffer(instance.GetAllocator(), unsafe_buffer, allocation);
vmaDestroyImage(instance.GetAllocator(), frame.image, frame.allocation);
device.destroyFramebuffer(frame.framebuffer);
device.destroyImageView(frame.image_view);
settings.screenshot_complete_callback(false);
}
} // namespace Vulkan