Screenshots

Signed-off-by: Slendi <slendi@socopon.com>
This commit is contained in:
2026-01-11 11:00:33 +02:00
parent 7978606a52
commit 447114e38d
434 changed files with 112456 additions and 230 deletions

View File

@@ -2,9 +2,11 @@
#include <algorithm>
#include <cerrno>
#include <chrono>
#include <cmath>
#include <cstdint>
#include <fcntl.h>
#include <format>
#include <iostream>
#include <numbers>
#include <optional>
@@ -12,6 +14,23 @@
#include <stdexcept>
#include <unistd.h>
#if defined(__clang__)
# pragma clang diagnostic push
# pragma clang diagnostic ignored "-Wreserved-identifier"
# pragma clang diagnostic ignored "-Wimplicit-fallthrough"
# pragma clang diagnostic ignored "-Wcast-qual"
# pragma clang diagnostic ignored "-Wmissing-field-initializers"
# pragma clang diagnostic ignored "-Wused-but-marked-unused"
# pragma clang diagnostic ignored "-Wmissing-prototypes"
# pragma clang diagnostic ignored "-Wextra-semi-stmt"
# pragma clang diagnostic ignored "-Wimplicit-int-conversion"
#endif
#define STB_IMAGE_WRITE_IMPLEMENTATION
#include "../thirdparty/stb/stb_image_write.h"
#if defined(__clang__)
# pragma clang diagnostic pop
#endif
#include <SDL3/SDL_events.h>
#include <SDL3/SDL_init.h>
#include <SDL3/SDL_mouse.h>
@@ -428,9 +447,7 @@ auto Application::run() -> void
uint64_t last { 0 };
float fps { 0.0f };
while (m_running) {
#if defined(TRACY_ENABLE)
ZoneScopedN("Frame");
#endif
GZoneScopedN("Frame");
uint64_t now { SDL_GetTicks() };
uint64_t dt { now - last };
float dt_seconds { static_cast<float>(dt) / 1000.0f };
@@ -439,233 +456,246 @@ auto Application::run() -> void
if (dt > 0)
fps = 1000.0f / (float)dt;
process_libinput_events();
{
GZoneScopedN("Input");
process_libinput_events();
while (SDL_PollEvent(&e)) {
bool forward_to_imgui { false };
if (e.type == SDL_EVENT_QUIT) {
m_running = false;
} else if (e.type == SDL_EVENT_WINDOW_RESIZED) {
int width {}, height {};
SDL_GetWindowSize(m_window, &width, &height);
m_renderer->resize(static_cast<uint32_t>(width),
static_cast<uint32_t>(height));
clamp_mouse_to_window(width, height);
forward_to_imgui = true;
} else if (e.type == SDL_EVENT_MOUSE_MOTION) {
m_mouse_x = e.motion.x;
m_mouse_y = e.motion.y;
m_mouse_dx = e.motion.xrel;
m_mouse_dy = e.motion.yrel;
forward_to_imgui = true;
} else if (e.type == SDL_EVENT_MOUSE_BUTTON_DOWN
|| e.type == SDL_EVENT_MOUSE_BUTTON_UP) {
m_mouse_x = e.button.x;
m_mouse_y = e.button.y;
forward_to_imgui = true;
} else if (e.type == SDL_EVENT_MOUSE_WHEEL) {
m_mouse_x = e.wheel.mouse_x;
m_mouse_y = e.wheel.mouse_y;
forward_to_imgui = true;
while (SDL_PollEvent(&e)) {
bool forward_to_imgui { false };
if (e.type == SDL_EVENT_QUIT) {
m_running = false;
} else if (e.type == SDL_EVENT_WINDOW_RESIZED) {
int width {}, height {};
SDL_GetWindowSize(m_window, &width, &height);
m_renderer->resize(static_cast<uint32_t>(width),
static_cast<uint32_t>(height));
clamp_mouse_to_window(width, height);
forward_to_imgui = true;
} else if (e.type == SDL_EVENT_MOUSE_MOTION) {
m_mouse_x = e.motion.x;
m_mouse_y = e.motion.y;
m_mouse_dx = e.motion.xrel;
m_mouse_dy = e.motion.yrel;
forward_to_imgui = true;
} else if (e.type == SDL_EVENT_MOUSE_BUTTON_DOWN
|| e.type == SDL_EVENT_MOUSE_BUTTON_UP) {
m_mouse_x = e.button.x;
m_mouse_y = e.button.y;
forward_to_imgui = true;
} else if (e.type == SDL_EVENT_MOUSE_WHEEL) {
m_mouse_x = e.wheel.mouse_x;
m_mouse_y = e.wheel.mouse_y;
forward_to_imgui = true;
}
if (forward_to_imgui)
ImGui_ImplSDL3_ProcessEvent(&e);
}
}
{
GZoneScopedN("CameraUpdate");
auto const target_offset { m_camera.target - m_camera.position };
auto const target_distance { target_offset.magnitude() };
auto const target_polar { PolarCoordinate::from_vec3(
target_offset) };
// Keep cursor angles in sync with externally-updated targets so
// view aligns to the target direction. Preserve radius when the
// target sits on top of the camera to avoid collapsing it.
if (target_distance > 0.0f) {
m_cursor.r = target_distance;
m_cursor.theta = target_polar.theta;
m_cursor.phi = target_polar.phi;
}
if (forward_to_imgui)
ImGui_ImplSDL3_ProcessEvent(&e);
bool rotated_this_frame { false };
if (mouse_captured()) {
constexpr float phi_epsilon { smath::deg(5.0f) };
m_cursor.theta
+= static_cast<float>(m_mouse_dx) * m_mouse_sensitivity;
m_cursor.phi
+= static_cast<float>(m_mouse_dy) * m_mouse_sensitivity;
m_cursor.phi = std::clamp(m_cursor.phi, phi_epsilon,
std::numbers::pi_v<float> - phi_epsilon);
rotated_this_frame = (m_mouse_dx != 0.0 || m_mouse_dy != 0.0);
}
auto look_dir { m_cursor.to_vec3().normalized_safe() };
if (!rotated_this_frame && target_distance > 0.0f) {
look_dir = target_offset.normalized_safe();
}
if (look_dir.magnitude() == 0.0f)
look_dir = smath::Vec3 { 0.0f, 0.0f, -1.0f };
smath::Vec3 const world_up { 0.0f, 1.0f, 0.0f };
auto right { look_dir.cross(world_up).normalized_safe() };
if (right.magnitude() == 0.0f)
right = smath::Vec3 { 1.0f, 0.0f, 0.0f };
auto camera_up { right.cross(look_dir).normalized_safe() };
if (camera_up.magnitude() == 0.0f)
camera_up = world_up;
smath::Vec3 move_dir {};
if (is_key_pressed(KEY_W))
move_dir += look_dir;
if (is_key_pressed(KEY_S))
move_dir -= look_dir;
if (is_key_pressed(KEY_D))
move_dir += right;
if (is_key_pressed(KEY_A))
move_dir -= right;
if (is_key_pressed(KEY_SPACE))
move_dir += world_up;
if (is_key_pressed(KEY_LEFTSHIFT))
move_dir -= world_up;
if (move_dir.magnitude() > 0.0f) {
constexpr float move_speed { 10.0f };
move_dir = move_dir.normalized_safe();
m_camera.position += move_dir * (move_speed * dt_seconds);
}
if (!m_show_imgui) {
m_camera.up = camera_up;
auto const distance = target_distance > 0.0f
? target_distance
: std::max(1.0f, m_cursor.r);
m_camera.target = m_camera.position + look_dir * distance;
}
m_mouse_dx = 0.0;
m_mouse_dy = 0.0;
}
auto const target_offset { m_camera.target - m_camera.position };
auto const target_distance { target_offset.magnitude() };
auto const target_polar { PolarCoordinate::from_vec3(target_offset) };
// Keep cursor angles in sync with externally-updated targets so view
// aligns to the target direction. Preserve radius when the target sits
// on top of the camera to avoid collapsing it.
if (target_distance > 0.0f) {
m_cursor.r = target_distance;
m_cursor.theta = target_polar.theta;
m_cursor.phi = target_polar.phi;
}
{
GZoneScopedN("ImGui");
ImGui_ImplSDL3_NewFrame();
ImGui_ImplVulkan_NewFrame();
bool rotated_this_frame { false };
if (mouse_captured()) {
constexpr float phi_epsilon { smath::deg(5.0f) };
ImGui::NewFrame();
m_cursor.theta
+= static_cast<float>(m_mouse_dx) * m_mouse_sensitivity;
m_cursor.phi
+= static_cast<float>(m_mouse_dy) * m_mouse_sensitivity;
m_cursor.phi = std::clamp(m_cursor.phi, phi_epsilon,
std::numbers::pi_v<float> - phi_epsilon);
rotated_this_frame = (m_mouse_dx != 0.0 || m_mouse_dy != 0.0);
}
auto look_dir { m_cursor.to_vec3().normalized_safe() };
if (!rotated_this_frame && target_distance > 0.0f) {
look_dir = target_offset.normalized_safe();
}
if (look_dir.magnitude() == 0.0f)
look_dir = smath::Vec3 { 0.0f, 0.0f, -1.0f };
smath::Vec3 const world_up { 0.0f, 1.0f, 0.0f };
auto right { look_dir.cross(world_up).normalized_safe() };
if (right.magnitude() == 0.0f)
right = smath::Vec3 { 1.0f, 0.0f, 0.0f };
auto camera_up { right.cross(look_dir).normalized_safe() };
if (camera_up.magnitude() == 0.0f)
camera_up = world_up;
smath::Vec3 move_dir {};
if (is_key_pressed(KEY_W))
move_dir += look_dir;
if (is_key_pressed(KEY_S))
move_dir -= look_dir;
if (is_key_pressed(KEY_D))
move_dir += right;
if (is_key_pressed(KEY_A))
move_dir -= right;
if (is_key_pressed(KEY_SPACE))
move_dir += world_up;
if (is_key_pressed(KEY_LEFTSHIFT))
move_dir -= world_up;
if (move_dir.magnitude() > 0.0f) {
constexpr float move_speed { 10.0f };
move_dir = move_dir.normalized_safe();
m_camera.position += move_dir * (move_speed * dt_seconds);
}
if (!m_show_imgui) {
m_camera.up = camera_up;
auto const distance = target_distance > 0.0f
? target_distance
: std::max(1.0f, m_cursor.r);
m_camera.target = m_camera.position + look_dir * distance;
}
m_mouse_dx = 0.0;
m_mouse_dy = 0.0;
ImGui_ImplSDL3_NewFrame();
ImGui_ImplVulkan_NewFrame();
ImGui::NewFrame();
ImGui::SetNextWindowSize({ 300, 100 });
ImGui::SetNextWindowPos({ 0, 0 });
ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4 { 0, 0, 0, 0.5f });
bool debug_open { ImGui::Begin("Debug Info", nullptr,
ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize) };
if (debug_open) {
ImGui::Text("%s", std::format("FPS: {:.2f}", fps).c_str());
ImGui::Text("%s",
std::format("Cam pos: ({:.2f}, {:.2f}, {:.2f})",
m_camera.position.x(), m_camera.position.y(),
m_camera.position.z())
.c_str());
ImGui::Text("%s",
std::format("Cam tgt: ({:.2f}, {:.2f}, {:.2f})",
m_camera.target.x(), m_camera.target.y(),
m_camera.target.z())
.c_str());
ImGui::Text("%s",
std::format("Cam up: ({:.2f}, {:.2f}, {:.2f})",
m_camera.up.x(), m_camera.up.y(), m_camera.up.z())
.c_str());
ImGui::Text("%s",
std::format("Cursor r/theta/phi: {:.2f}, {:.2f}, {:.2f}",
m_cursor.r, m_cursor.theta, m_cursor.phi)
.c_str());
}
ImGui::End();
ImGui::PopStyleColor();
if (m_show_imgui) {
ImGui::SetNextWindowCollapsed(true, ImGuiCond_Once);
ImGui::ShowDemoWindow();
ImGui::SetNextWindowSize({ 300, -1 }, ImGuiCond_Once);
bool fun_menu_open { ImGui::Begin("Fun menu") };
if (fun_menu_open) {
static std::array<char const *, 4> const aa_items {
"None",
"MSAA 2X",
"MSAA 4X",
"MSAA 8X",
};
int selected_item {
static_cast<int>(m_renderer->antialiasing()),
};
if (ImGui::Combo("Antialiasing", &selected_item,
aa_items.data(), aa_items.size())) {
m_renderer->set_antialiasing(
static_cast<VulkanRenderer::AntiAliasingKind>(
selected_item));
}
if (ImGui::CollapsingHeader("Camera",
ImGuiTreeNodeFlags_Framed
| ImGuiTreeNodeFlags_SpanAvailWidth
| ImGuiTreeNodeFlags_DefaultOpen)) {
auto const camera_offset { m_camera.target
- m_camera.position };
auto const camera_distance { camera_offset.magnitude() };
auto const camera_direction {
camera_offset.normalized_safe()
};
ImGui::SliderFloat("Mouse sensitivity",
&m_mouse_sensitivity, 0.0001f, 0.01f, "%.4f");
constexpr float position_step { 0.05f };
constexpr float target_step { 0.05f };
bool position_changed { ImGui::DragFloat3(
"Pos", m_camera.position.data(), position_step) };
bool target_changed { ImGui::DragFloat3(
"Target", m_camera.target.data(), target_step) };
ImGui::DragFloat3("Up", m_camera.up.data());
if (position_changed && !target_changed) {
auto offset { m_cursor.to_vec3() };
auto const preserve_distance { camera_distance > 0.0f
? camera_distance
: offset.magnitude() };
if (offset.magnitude() == 0.0f
&& camera_direction.magnitude() > 0.0f) {
offset = camera_direction * preserve_distance;
}
if (offset.magnitude() > 0.0f) {
m_camera.target = m_camera.position + offset;
}
}
if (target_changed && !position_changed) {
auto const new_offset { m_camera.target
- m_camera.position };
auto new_direction { new_offset.normalized_safe() };
auto const preserve_distance { camera_distance > 0.0f
? camera_distance
: new_offset.magnitude() };
if (new_direction.magnitude() == 0.0f) {
new_direction = camera_direction;
}
if (new_direction.magnitude() > 0.0f
&& preserve_distance > 0.0f) {
m_camera.position = m_camera.target
- new_direction * preserve_distance;
}
}
}
ImGui::SetNextWindowSize({ 300, 100 });
ImGui::SetNextWindowPos({ 0, 0 });
ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4 { 0, 0, 0, 0.5f });
bool debug_open { ImGui::Begin("Debug Info", nullptr,
ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize) };
if (debug_open) {
ImGui::Text("%s", std::format("FPS: {:.2f}", fps).c_str());
ImGui::Text("%s",
std::format("Cam pos: ({:.2f}, {:.2f}, {:.2f})",
m_camera.position.x(), m_camera.position.y(),
m_camera.position.z())
.c_str());
ImGui::Text("%s",
std::format("Cam tgt: ({:.2f}, {:.2f}, {:.2f})",
m_camera.target.x(), m_camera.target.y(),
m_camera.target.z())
.c_str());
ImGui::Text("%s",
std::format("Cam up: ({:.2f}, {:.2f}, {:.2f})",
m_camera.up.x(), m_camera.up.y(), m_camera.up.z())
.c_str());
ImGui::Text("%s",
std::format("Cursor r/theta/phi: {:.2f}, {:.2f}, {:.2f}",
m_cursor.r, m_cursor.theta, m_cursor.phi)
.c_str());
}
ImGui::End();
ImGui::PopStyleColor();
if (m_show_imgui) {
ImGui::SetNextWindowCollapsed(true, ImGuiCond_Once);
ImGui::ShowDemoWindow();
ImGui::SetNextWindowSize({ 300, -1 }, ImGuiCond_Once);
bool fun_menu_open { ImGui::Begin("Fun menu") };
if (fun_menu_open) {
static std::array<char const *, 4> const aa_items {
"None",
"MSAA 2X",
"MSAA 4X",
"MSAA 8X",
};
int selected_item {
static_cast<int>(m_renderer->antialiasing()),
};
if (ImGui::Combo("Antialiasing", &selected_item,
aa_items.data(), aa_items.size())) {
m_renderer->set_antialiasing(
static_cast<VulkanRenderer::AntiAliasingKind>(
selected_item));
}
if (ImGui::CollapsingHeader("Camera",
ImGuiTreeNodeFlags_Framed
| ImGuiTreeNodeFlags_SpanAvailWidth
| ImGuiTreeNodeFlags_DefaultOpen)) {
auto const camera_offset { m_camera.target
- m_camera.position };
auto const camera_distance {
camera_offset.magnitude()
};
auto const camera_direction {
camera_offset.normalized_safe()
};
ImGui::SliderFloat("Mouse sensitivity",
&m_mouse_sensitivity, 0.0001f, 0.01f, "%.4f");
constexpr float position_step { 0.05f };
constexpr float target_step { 0.05f };
bool position_changed { ImGui::DragFloat3(
"Pos", m_camera.position.data(), position_step) };
bool target_changed { ImGui::DragFloat3(
"Target", m_camera.target.data(), target_step) };
ImGui::DragFloat3("Up", m_camera.up.data());
if (position_changed && !target_changed) {
auto offset { m_cursor.to_vec3() };
auto const preserve_distance {
camera_distance > 0.0f ? camera_distance
: offset.magnitude()
};
if (offset.magnitude() == 0.0f
&& camera_direction.magnitude() > 0.0f) {
offset = camera_direction * preserve_distance;
}
if (offset.magnitude() > 0.0f) {
m_camera.target = m_camera.position + offset;
}
}
if (target_changed && !position_changed) {
auto const new_offset { m_camera.target
- m_camera.position };
auto new_direction { new_offset.normalized_safe() };
auto const preserve_distance {
camera_distance > 0.0f ? camera_distance
: new_offset.magnitude()
};
if (new_direction.magnitude() == 0.0f) {
new_direction = camera_direction;
}
if (new_direction.magnitude() > 0.0f
&& preserve_distance > 0.0f) {
m_camera.position = m_camera.target
- new_direction * preserve_distance;
}
}
}
}
ImGui::End();
}
ImGui::Render();
}
ImGui::Render();
m_renderer->render([&](VulkanRenderer::GL &gl) {
#if defined(TRACY_ENABLE)
ZoneScopedN("Render");
#endif
GZoneScopedN("Render");
auto view { smath::matrix_look_at(
m_camera.position, m_camera.target, m_camera.up) };
auto const draw_extent = m_renderer->draw_extent();
@@ -821,6 +851,34 @@ auto Application::handle_keyboard_event(libinput_event_keyboard *event) -> void
mouse_captured(!new_show_imgui);
}
if (pressed && key == KEY_F12) {
auto screenshot { std::optional<VulkanRenderer::ScreenshotPixels> {} };
if (m_renderer) {
screenshot = m_renderer->get_screenshot_pixels();
}
if (!screenshot) {
m_logger.warn("Screenshot not ready");
return;
}
auto const extent { screenshot->extent };
auto const stride { static_cast<int>(extent.width * 4) };
auto const index { m_screenshot_index++ };
auto const now { std::chrono::system_clock::now() };
auto filename { std::format(
"screenshot_{:%Y%m%d_%H%M%S}_{:04}.png", now, index) };
int const result { stbi_write_png(filename.c_str(),
static_cast<int>(extent.width), static_cast<int>(extent.height), 4,
screenshot->pixels.data(), stride) };
if (result == 0) {
m_logger.err("Failed to write screenshot {}", filename);
} else {
m_logger.info("Saved screenshot {}", filename);
}
}
if (auto imgui_key { linux_key_to_imgui(key) }) {
if (m_show_imgui)
ImGui::GetIO().AddKeyEvent(*imgui_key, pressed);

View File

@@ -1,6 +1,7 @@
#pragma once
#include <array>
#include <cstdint>
#include <memory>
#include <SDL3/SDL_video.h>
@@ -48,6 +49,7 @@ private:
bool m_mouse_captured { false };
bool m_show_imgui { false };
int m_ctrl_pressed_count { 0 };
std::uint32_t m_screenshot_index { 0 };
double m_mouse_x { 0.0 };
double m_mouse_y { 0.0 };

View File

@@ -1,6 +1,8 @@
#pragma once
#include <cmath>
#include <cstdint>
#include <vector>
#include <smath.hpp>
#include <vk_mem_alloc.h>
@@ -33,6 +35,15 @@ struct FrameData {
DeletionQueue deletion_queue;
DescriptorAllocatorGrowable frame_descriptors;
AllocatedBuffer frame_image_buffer {};
vk::Extent2D frame_image_extent {};
std::vector<std::uint8_t> frame_image_rgba;
bool frame_image_ready { false };
bool tracy_frame_ready { false };
AllocatedBuffer screenshot_buffer {};
vk::Extent2D screenshot_extent {};
std::vector<std::uint8_t> screenshot_rgba;
bool screenshot_ready { false };
};
struct Vertex {

View File

@@ -36,6 +36,14 @@ template<typename F> privDefer<F> defer_func(F f) { return privDefer<F>(f); }
} \
} while (0)
#if defined(TRACY_ENABLE)
# define GZoneScopedN(name) ZoneScopedN(name)
#else
# define GZoneScopedN(name) \
do { \
} while (0)
#endif
namespace vkutil {
auto transition_image(vk::CommandBuffer cmd, vk::Image image,

View File

@@ -1,11 +1,14 @@
#define VULKAN_HPP_DEFAULT_DISPATCH_LOADER_DYNAMIC_STORAGE
#include "VulkanRenderer.h"
#include <algorithm>
#include <cassert>
#include <cmath>
#include <cstdint>
#include <cstring>
#include <format>
#include <iostream>
#include <limits>
#include <optional>
#include <print>
#include <stdexcept>
@@ -18,12 +21,15 @@
#include <imgui_impl_sdl3.h>
#include <imgui_impl_vulkan.h>
#if defined(TRACY_ENABLE)
# include <tracy/Tracy.hpp>
#endif
#include "DescriptorLayoutBuilder.h"
#include "DescriptorWriter.h"
#include "GraphicsPipelineBuilder.h"
#include "Util.h"
VULKAN_HPP_DEFAULT_DISPATCH_LOADER_DYNAMIC_STORAGE
VULKAN_HPP_DEFAULT_DISPATCH_LOADER_DYNAMIC_STORAGE;
namespace Lunar {
@@ -633,6 +639,11 @@ VulkanRenderer::~VulkanRenderer()
m_vk.default_sampler_linear.reset();
m_vk.default_sampler_nearest.reset();
if (m_latest_screenshot) {
destroy_image(*m_latest_screenshot);
m_latest_screenshot.reset();
}
destroy_swapchain();
destroy_draw_image();
destroy_msaa_color_image();
@@ -1369,6 +1380,10 @@ auto VulkanRenderer::render(std::function<void(GL &)> const &record) -> void
m_device.waitForFences(frame.render_fence.get(), true, 1'000'000'000));
frame.deletion_queue.flush();
frame.frame_descriptors.clear_pools(m_vkb.dev.device);
emit_frame_screenshot(frame);
#if defined(TRACY_ENABLE)
emit_tracy_frame_image(frame);
#endif
auto raw_fence = static_cast<VkFence>(frame.render_fence.get());
VK_CHECK(m_logger, vkResetFences(m_vkb.dev.device, 1, &raw_fence));
@@ -1438,9 +1453,92 @@ auto VulkanRenderer::render(std::function<void(GL &)> const &record) -> void
draw_imgui(cmd, m_vk.swapchain_image_views.at(swapchain_image_idx).get());
vkutil::transition_image(cmd, m_vk.swapchain_images[swapchain_image_idx],
vkutil::transition_image(cmd, m_vk.swapchain_images.at(swapchain_image_idx),
vk::ImageLayout::eColorAttachmentOptimal,
vk::ImageLayout::ePresentSrcKHR);
vk::ImageLayout::eTransferSrcOptimal);
if (frame.screenshot_buffer.buffer) {
vk::BufferImageCopy screenshot_copy {};
screenshot_copy.imageSubresource.aspectMask
= vk::ImageAspectFlagBits::eColor;
screenshot_copy.imageSubresource.mipLevel = 0;
screenshot_copy.imageSubresource.baseArrayLayer = 0;
screenshot_copy.imageSubresource.layerCount = 1;
screenshot_copy.imageExtent
= vk::Extent3D { m_vk.swapchain_extent.width,
m_vk.swapchain_extent.height, 1 };
cmd.copyImageToBuffer(m_vk.swapchain_images.at(swapchain_image_idx),
vk::ImageLayout::eTransferSrcOptimal,
frame.screenshot_buffer.buffer, screenshot_copy);
frame.screenshot_ready = true;
} else {
frame.screenshot_ready = false;
}
#if defined(TRACY_ENABLE)
constexpr std::uint64_t tracy_frame_stride { 10 };
bool const tracy_capture { TracyIsConnected
&& (m_vk.frame_number % tracy_frame_stride == 0) };
frame.tracy_frame_ready = false;
frame.frame_image_ready = false;
if (tracy_capture && frame.frame_image_buffer.buffer
&& m_vk.tracy_capture_image.image) {
vkutil::transition_image(cmd, m_vk.tracy_capture_image.image,
m_vk.tracy_capture_image_layout,
vk::ImageLayout::eTransferDstOptimal);
m_vk.tracy_capture_image_layout = vk::ImageLayout::eTransferDstOptimal;
vk::ImageBlit blit_region {};
blit_region.srcSubresource.aspectMask = vk::ImageAspectFlagBits::eColor;
blit_region.srcSubresource.mipLevel = 0;
blit_region.srcSubresource.baseArrayLayer = 0;
blit_region.srcSubresource.layerCount = 1;
blit_region.srcOffsets[0] = vk::Offset3D { 0, 0, 0 };
blit_region.srcOffsets[1]
= vk::Offset3D { static_cast<int32_t>(m_vk.swapchain_extent.width),
static_cast<int32_t>(m_vk.swapchain_extent.height), 1 };
blit_region.dstSubresource.aspectMask = vk::ImageAspectFlagBits::eColor;
blit_region.dstSubresource.mipLevel = 0;
blit_region.dstSubresource.baseArrayLayer = 0;
blit_region.dstSubresource.layerCount = 1;
blit_region.dstOffsets[0] = vk::Offset3D { 0, 0, 0 };
blit_region.dstOffsets[1] = vk::Offset3D {
static_cast<int32_t>(m_vk.tracy_capture_extent.width),
static_cast<int32_t>(m_vk.tracy_capture_extent.height), 1
};
cmd.blitImage(m_vk.swapchain_images.at(swapchain_image_idx),
vk::ImageLayout::eTransferSrcOptimal,
m_vk.tracy_capture_image.image,
vk::ImageLayout::eTransferDstOptimal, blit_region,
vk::Filter::eLinear);
vkutil::transition_image(cmd, m_vk.tracy_capture_image.image,
m_vk.tracy_capture_image_layout,
vk::ImageLayout::eTransferSrcOptimal);
m_vk.tracy_capture_image_layout = vk::ImageLayout::eTransferSrcOptimal;
vk::BufferImageCopy copy_region {};
copy_region.imageSubresource.aspectMask
= vk::ImageAspectFlagBits::eColor;
copy_region.imageSubresource.mipLevel = 0;
copy_region.imageSubresource.baseArrayLayer = 0;
copy_region.imageSubresource.layerCount = 1;
copy_region.imageExtent
= vk::Extent3D { m_vk.tracy_capture_extent.width,
m_vk.tracy_capture_extent.height, 1 };
cmd.copyImageToBuffer(m_vk.tracy_capture_image.image,
vk::ImageLayout::eTransferSrcOptimal,
frame.frame_image_buffer.buffer, copy_region);
frame.frame_image_ready = true;
frame.tracy_frame_ready = true;
}
#endif
vkutil::transition_image(cmd, m_vk.swapchain_images.at(swapchain_image_idx),
vk::ImageLayout::eTransferSrcOptimal, vk::ImageLayout::ePresentSrcKHR);
cmd.end();
@@ -1503,7 +1601,9 @@ auto VulkanRenderer::create_swapchain(uint32_t width, uint32_t height) -> void
})
.set_desired_present_mode(VK_PRESENT_MODE_FIFO_KHR)
.set_desired_extent(width, height)
.add_image_usage_flags(VK_IMAGE_USAGE_TRANSFER_DST_BIT)
.set_image_usage_flags(VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT
| VK_IMAGE_USAGE_TRANSFER_DST_BIT
| VK_IMAGE_USAGE_TRANSFER_SRC_BIT)
.build() };
if (!swapchain_ret) {
std::println(std::cerr, "Failed to create swapchain. Error: {}",
@@ -1536,6 +1636,10 @@ auto VulkanRenderer::create_swapchain(uint32_t width, uint32_t height) -> void
for (auto &semaphore : m_vk.present_semaphores) {
semaphore = m_device.createSemaphoreUnique(semaphore_ci);
}
ensure_screenshot_buffers(m_vk.swapchain_extent);
#if defined(TRACY_ENABLE)
ensure_tracy_frame_buffers(m_vk.swapchain_extent);
#endif
}
auto VulkanRenderer::create_draw_image(uint32_t width, uint32_t height) -> void
@@ -1652,6 +1756,11 @@ auto VulkanRenderer::destroy_swapchain() -> void
if (!m_vk.swapchain)
return;
destroy_screenshot_buffers();
#if defined(TRACY_ENABLE)
destroy_tracy_frame_buffers();
#endif
m_vk.present_semaphores.clear();
m_device.destroySwapchainKHR(m_vk.swapchain);
@@ -1662,11 +1771,292 @@ auto VulkanRenderer::destroy_swapchain() -> void
m_vk.swapchain_extent = vk::Extent2D { 0, 0 };
}
auto VulkanRenderer::create_image(vk::Extent3D size, vk::Format format,
auto VulkanRenderer::ensure_screenshot_buffers(vk::Extent2D extent) -> void
{
if (extent.width == 0 || extent.height == 0) {
return;
}
auto const byte_count { static_cast<size_t>(extent.width)
* static_cast<size_t>(extent.height) * 4 };
for (auto &frame : m_vk.frames) {
auto const same_extent { frame.screenshot_buffer.buffer
&& frame.screenshot_extent.width == extent.width
&& frame.screenshot_extent.height == extent.height };
if (!same_extent && frame.screenshot_buffer.buffer) {
destroy_buffer(frame.screenshot_buffer);
frame.screenshot_buffer = AllocatedBuffer {};
}
if (!same_extent) {
frame.screenshot_buffer = create_buffer(byte_count,
vk::BufferUsageFlagBits::eTransferDst,
VMA_MEMORY_USAGE_GPU_TO_CPU);
frame.screenshot_extent = extent;
frame.screenshot_rgba.resize(byte_count);
}
frame.screenshot_ready = false;
}
}
auto VulkanRenderer::destroy_screenshot_buffers() -> void
{
for (auto &frame : m_vk.frames) {
if (frame.screenshot_buffer.buffer) {
destroy_buffer(frame.screenshot_buffer);
frame.screenshot_buffer = AllocatedBuffer {};
}
frame.screenshot_extent = vk::Extent2D {};
frame.screenshot_rgba.clear();
frame.screenshot_ready = false;
}
m_latest_screenshot_pixels.clear();
m_latest_screenshot_extent = vk::Extent2D {};
if (m_latest_screenshot) {
destroy_image(*m_latest_screenshot);
m_latest_screenshot.reset();
}
}
auto VulkanRenderer::emit_frame_screenshot(FrameData &frame) -> void
{
if (!frame.screenshot_ready) {
return;
}
auto const extent { frame.screenshot_extent };
if (extent.width == 0 || extent.height == 0) {
return;
}
auto const byte_count { static_cast<size_t>(extent.width)
* static_cast<size_t>(extent.height) * 4 };
if (!frame.screenshot_buffer.buffer) {
return;
}
VmaAllocationInfo info {};
vmaGetAllocationInfo(
m_vk.allocator, frame.screenshot_buffer.allocation, &info);
void *mapped { info.pMappedData };
bool mapped_here { false };
if (!mapped) {
auto const map_result { vmaMapMemory(
m_vk.allocator, frame.screenshot_buffer.allocation, &mapped) };
if (map_result != VK_SUCCESS) {
return;
}
mapped_here = true;
}
auto *source { static_cast<std::uint8_t *>(mapped) };
auto &destination { frame.screenshot_rgba };
if (destination.size() != byte_count) {
destination.resize(byte_count);
}
for (size_t i = 0; i < byte_count; i += 4) {
destination[i] = source[i + 2];
destination[i + 1] = source[i + 1];
destination[i + 2] = source[i];
destination[i + 3] = source[i + 3];
}
auto const screenshot_flags { vk::ImageUsageFlagBits::eSampled };
auto const screenshot_extent { vk::Extent3D {
extent.width, extent.height, 1 } };
if (m_latest_screenshot) {
destroy_image(*m_latest_screenshot);
m_latest_screenshot.reset();
}
m_latest_screenshot = create_image(destination.data(), screenshot_extent,
vk::Format::eR8G8B8A8Unorm, screenshot_flags);
m_latest_screenshot_pixels = destination;
m_latest_screenshot_extent = extent;
if (mapped_here) {
vmaUnmapMemory(m_vk.allocator, frame.screenshot_buffer.allocation);
}
frame.screenshot_ready = false;
}
#if defined(TRACY_ENABLE)
namespace {
[[nodiscard]] auto tracy_capture_extent(vk::Extent2D extent) -> vk::Extent2D
{
constexpr uint32_t max_width { 320 };
constexpr uint32_t max_height { 180 };
auto width { std::min(extent.width, max_width) };
auto height { std::min(extent.height, max_height) };
width -= width % 4;
height -= height % 4;
return vk::Extent2D { width, height };
}
} // namespace
auto VulkanRenderer::ensure_tracy_frame_buffers(vk::Extent2D extent) -> void
{
auto const capture_extent { tracy_capture_extent(extent) };
if (capture_extent.width == 0 || capture_extent.height == 0) {
return;
}
auto const byte_count { static_cast<size_t>(capture_extent.width)
* static_cast<size_t>(capture_extent.height) * 4 };
if (m_vk.tracy_capture_extent.width != capture_extent.width
|| m_vk.tracy_capture_extent.height != capture_extent.height) {
if (m_vk.tracy_capture_image.image) {
destroy_image(m_vk.tracy_capture_image);
}
auto const flags { vk::ImageUsageFlagBits::eTransferDst
| vk::ImageUsageFlagBits::eTransferSrc
| vk::ImageUsageFlagBits::eColorAttachment
| vk::ImageUsageFlagBits::eSampled };
auto const capture_size { vk::Extent3D {
capture_extent.width, capture_extent.height, 1 } };
m_vk.tracy_capture_image = create_image_no_view(
capture_size, m_vk.swapchain_image_format, flags);
m_vk.tracy_capture_image_layout = vk::ImageLayout::eUndefined;
m_vk.tracy_capture_extent = capture_extent;
}
for (auto &frame : m_vk.frames) {
auto const same_extent { frame.frame_image_buffer.buffer
&& frame.frame_image_extent.width == capture_extent.width
&& frame.frame_image_extent.height == capture_extent.height };
if (!same_extent && frame.frame_image_buffer.buffer) {
destroy_buffer(frame.frame_image_buffer);
frame.frame_image_buffer = AllocatedBuffer {};
}
if (!same_extent) {
frame.frame_image_buffer = create_buffer(byte_count,
vk::BufferUsageFlagBits::eTransferDst,
VMA_MEMORY_USAGE_GPU_TO_CPU);
frame.frame_image_extent = capture_extent;
frame.frame_image_rgba.resize(byte_count);
}
frame.frame_image_ready = false;
frame.tracy_frame_ready = false;
}
}
auto VulkanRenderer::destroy_tracy_frame_buffers() -> void
{
for (auto &frame : m_vk.frames) {
if (frame.frame_image_buffer.buffer) {
destroy_buffer(frame.frame_image_buffer);
frame.frame_image_buffer = AllocatedBuffer {};
}
frame.frame_image_extent = vk::Extent2D {};
frame.frame_image_rgba.clear();
frame.frame_image_ready = false;
frame.tracy_frame_ready = false;
}
if (m_vk.tracy_capture_image.image) {
destroy_image(m_vk.tracy_capture_image);
m_vk.tracy_capture_image = AllocatedImage {};
}
m_vk.tracy_capture_image_layout = vk::ImageLayout::eUndefined;
m_vk.tracy_capture_extent = vk::Extent2D {};
}
auto VulkanRenderer::emit_tracy_frame_image(FrameData &frame) -> void
{
if (!frame.frame_image_ready) {
return;
}
auto const extent { frame.frame_image_extent };
if (extent.width == 0 || extent.height == 0) {
return;
}
if (extent.width % 4 != 0 || extent.height % 4 != 0) {
return;
}
if (extent.width > std::numeric_limits<std::uint16_t>::max()
|| extent.height > std::numeric_limits<std::uint16_t>::max()) {
return;
}
auto const byte_count { static_cast<size_t>(extent.width)
* static_cast<size_t>(extent.height) * 4 };
if (!frame.frame_image_buffer.buffer) {
return;
}
VmaAllocationInfo info {};
vmaGetAllocationInfo(
m_vk.allocator, frame.frame_image_buffer.allocation, &info);
void *mapped { info.pMappedData };
bool mapped_here { false };
if (!mapped) {
auto const map_result { vmaMapMemory(
m_vk.allocator, frame.frame_image_buffer.allocation, &mapped) };
if (map_result != VK_SUCCESS) {
return;
}
mapped_here = true;
}
auto *source { static_cast<std::uint8_t *>(mapped) };
auto &destination { frame.frame_image_rgba };
if (destination.size() != byte_count) {
destination.resize(byte_count);
}
for (size_t i = 0; i < byte_count; i += 4) {
destination[i] = source[i + 2];
destination[i + 1] = source[i + 1];
destination[i + 2] = source[i];
destination[i + 3] = source[i + 3];
}
if (!frame.tracy_frame_ready || !TracyIsConnected) {
frame.frame_image_ready = false;
frame.tracy_frame_ready = false;
if (mapped_here) {
vmaUnmapMemory(m_vk.allocator, frame.frame_image_buffer.allocation);
}
return;
}
auto const frame_offset { static_cast<std::uint8_t>(
m_vk.frames.size() - 1) };
FrameImage(destination.data(), static_cast<std::uint16_t>(extent.width),
static_cast<std::uint16_t>(extent.height), frame_offset, false);
if (mapped_here) {
vmaUnmapMemory(m_vk.allocator, frame.frame_image_buffer.allocation);
}
frame.frame_image_ready = false;
frame.tracy_frame_ready = false;
}
#endif
auto VulkanRenderer::create_image_no_view(vk::Extent3D size, vk::Format format,
vk::ImageUsageFlags flags, vk::SampleCountFlagBits samples, bool mipmapped)
-> AllocatedImage
{
AllocatedImage new_image;
AllocatedImage new_image {};
new_image.format = format;
new_image.extent = size;
@@ -1688,6 +2078,16 @@ auto VulkanRenderer::create_image(vk::Extent3D size, vk::Format format,
reinterpret_cast<VkImage *>(&new_image.image),
&new_image.allocation, nullptr));
return new_image;
}
auto VulkanRenderer::create_image(vk::Extent3D size, vk::Format format,
vk::ImageUsageFlags flags, vk::SampleCountFlagBits samples, bool mipmapped)
-> AllocatedImage
{
AllocatedImage new_image { create_image_no_view(
size, format, flags, samples, mipmapped) };
vk::ImageAspectFlags aspect_flag { vk::ImageAspectFlagBits::eColor };
if (format == vk::Format::eD32Sfloat) {
aspect_flag = vk::ImageAspectFlagBits::eDepth;

View File

@@ -1,9 +1,11 @@
#pragma once
#include <array>
#include <cstdint>
#include <functional>
#include <mutex>
#include <optional>
#include <span>
#include <variant>
#include <vector>
@@ -30,14 +32,13 @@ struct GPUDrawPushConstants {
constexpr unsigned FRAME_OVERLAP = 2;
struct VulkanRenderer {
enum class AntiAliasingKind {
NONE,
MSAA_2X,
MSAA_4X,
MSAA_8X,
struct ScreenshotPixels {
std::span<std::uint8_t const> pixels;
vk::Extent2D extent;
};
struct GL {
enum class GeometryKind {
Triangles,
TriangleStrip,
@@ -116,6 +117,13 @@ struct VulkanRenderer {
std::vector<uint32_t> m_indices;
};
enum class AntiAliasingKind {
NONE,
MSAA_2X,
MSAA_4X,
MSAA_8X,
};
VulkanRenderer(SDL_Window *window, Logger &logger);
~VulkanRenderer();
@@ -160,11 +168,33 @@ struct VulkanRenderer {
auto mesh_pipeline() -> Pipeline & { return m_vk.mesh_pipeline; }
auto triangle_pipeline() -> Pipeline & { return m_vk.triangle_pipeline; }
auto gl_api() -> GL & { return gl; }
auto get_screenshot() const -> std::optional<AllocatedImage>
{
return m_latest_screenshot;
}
auto get_screenshot_pixels() const
-> std::optional<VulkanRenderer::ScreenshotPixels>
{
if (m_latest_screenshot_pixels.empty()
|| m_latest_screenshot_extent.width == 0
|| m_latest_screenshot_extent.height == 0) {
return {};
}
auto const span { std::span<std::uint8_t const> {
m_latest_screenshot_pixels.data(),
m_latest_screenshot_pixels.size() } };
return ScreenshotPixels { span, m_latest_screenshot_extent };
}
auto logger() const -> Logger & { return m_logger; }
GL gl;
std::optional<AllocatedImage> m_latest_screenshot {};
std::vector<std::uint8_t> m_latest_screenshot_pixels {};
vk::Extent2D m_latest_screenshot_extent {};
private:
struct RenderCommand {
struct SetAntiAliasing {
@@ -197,10 +227,22 @@ private:
auto destroy_msaa_color_image() -> void;
auto recreate_swapchain(uint32_t width, uint32_t height) -> void;
auto destroy_swapchain() -> void;
auto ensure_screenshot_buffers(vk::Extent2D extent) -> void;
auto destroy_screenshot_buffers() -> void;
auto emit_frame_screenshot(FrameData &frame) -> void;
#if defined(TRACY_ENABLE)
auto ensure_tracy_frame_buffers(vk::Extent2D extent) -> void;
auto destroy_tracy_frame_buffers() -> void;
auto emit_tracy_frame_image(FrameData &frame) -> void;
#endif
auto create_image(vk::Extent3D size, vk::Format format,
vk::ImageUsageFlags flags,
vk::SampleCountFlagBits samples = vk::SampleCountFlagBits::e1,
bool mipmapped = false) -> AllocatedImage;
auto create_image_no_view(vk::Extent3D size, vk::Format format,
vk::ImageUsageFlags flags,
vk::SampleCountFlagBits samples = vk::SampleCountFlagBits::e1,
bool mipmapped = false) -> AllocatedImage;
auto create_image(void const *data, vk::Extent3D size, vk::Format format,
vk::ImageUsageFlags flags, bool mipmapped = false) -> AllocatedImage;
auto destroy_image(AllocatedImage const &img) -> void;
@@ -248,6 +290,13 @@ private:
vk::ImageLayout msaa_color_image_layout { vk::ImageLayout::eUndefined };
AllocatedImage depth_image {};
vk::ImageLayout depth_image_layout { vk::ImageLayout::eUndefined };
#if defined(TRACY_ENABLE)
AllocatedImage tracy_capture_image {};
vk::ImageLayout tracy_capture_image_layout {
vk::ImageLayout::eUndefined
};
vk::Extent2D tracy_capture_extent {};
#endif
vk::Extent2D draw_extent {};
AntiAliasingKind antialiasing_kind { AntiAliasingKind::NONE };
vk::SampleCountFlagBits msaa_samples { vk::SampleCountFlagBits::e1 };