Files
OpenRA3/src/vulkan/ra3.vulkan.sdl.cppm
T
EnderTheCoder 70f35d8382 render map objects: decode compiled W3DMesh art, skin and draw over terrain
Read the buildings and props a map places from its compiled art (the
uncompressed worldbuilder stream + embedded DDS textures):

- ra3.models: BAB asset-stream parser (lazy slices), W3DMesh + D3DHierarchy
  decode, DDS (DXT1/3/5 + uncompressed) decode, per-mesh bone remap and
  bind-pose single-joint skinning, flattened to one world-space triangle soup.
- ra3.map: parse the ObjectsList chunk into (type, x, y, z, angle).
- ra3.terrain: render3d rasterises the scene over the raymarched terrain with
  a z-buffer; gpu_terrain carries the scene for the GPU backends.
- ra3.vulkan: second pipeline + depth attachment, terrain.frag writes
  gl_FragDepth, and a small depth bias keeps ground decals from z-fighting.
- objects.fx shaders (compiled to SPIR-V), embedded like scene/terrain.

Only opaque parts are drawn: FX-light billboards (DefaultW3D.fx / BasicW3D.fx)
and damage-fill shells (BuildingsGenericDamageFill.fx) are skipped, since the
latter paint the wrecked interior (e.g. orange CBBuilding_Wood) over the shell.

Ground-decal meshes with no diffuse role and the Road templates themselves are
still not drawn.
2026-09-29 18:08:11 +08:00

1829 lines
102 KiB
C++

module;
#include <volk.h>
#include <SDL3/SDL.h>
#include <SDL3/SDL_vulkan.h>
#include <cstdio>
#include "shaders_embedded.hpp"
export module ra3.vulkan;
import std;
export import ra3.core;
import ra3.render;
import ra3.terrain;
import ra3.client;
/**
* A Vulkan presentation backend for the engine's rendered scene.
*
* Red Alert 3 shipped a Direct3D 9 renderer; OpenRA3 replaces the graphics
* stack with Vulkan while keeping the platform/window layer on SDL3. This is one
* implementation of `ra3::client::display`: the interactive loops live in the
* base class, so only the low-level primitives (`init` / `present` /
* `poll_event` / `window_size` / `key_down` / `present_terrain` / `shutdown`)
* are backend-specific.
*
* The image pipeline is deliberately small: one graphics pipeline draws a
* fullscreen triangle sampling a software-rendered `ra3::render::image`. The
* heightfield raymarcher is a second pipeline behind `present_terrain`. A thin
* RHI/render-graph will replace the hand-rolled pipelines as materials, skybox
* and HUD grow.
*/
export namespace ra3::vulkan {
namespace detail {
[[nodiscard]] inline auto check(VkResult result, std::string_view what) -> bool {
if (result == VK_SUCCESS) return true;
std::cerr << "ra3.vulkan: " << what << " failed (VkResult " << static_cast<int>(result) << ")\n";
return false;
}
}
/**
* The Vulkan device, swapchain and pipelines, plus the SDL window and
* surface they render to.
*/
class vulkan_display final : public ra3::client::display {
public:
vulkan_display() = default;
vulkan_display(const vulkan_display &) = delete;
auto operator=(const vulkan_display &) -> vulkan_display & = delete;
~vulkan_display() override { this->cleanup(); }
/**
* Create the window, Vulkan device and swapchain.
*
* @return false when Vulkan or a display is unavailable; the caller
* should then fall back to another display.
*/
[[nodiscard]] auto init(const ra3::client::display_options &options) -> bool override {
if (volkInitialize() != VK_SUCCESS) return false;
if (!SDL_Init(SDL_INIT_VIDEO)) return false;
this->fps_limit_ = options.fps_limit;
window_ = SDL_CreateWindow(options.title.c_str(), options.width, options.height,
SDL_WINDOW_VULKAN | SDL_WINDOW_RESIZABLE | (options.fullscreen ? SDL_WINDOW_FULLSCREEN : 0));
if (window_ == nullptr) return false;
SDL_RaiseWindow(window_); // take input focus so the wheel zooms without a click
Uint32 extension_count = 0;
const char *const *extensions = SDL_Vulkan_GetInstanceExtensions(&extension_count);
if (extensions == nullptr) return false;
VkApplicationInfo app{};
app.sType = VK_STRUCTURE_TYPE_APPLICATION_INFO;
app.pApplicationName = "OpenRA3";
app.apiVersion = VK_API_VERSION_1_1;
VkInstanceCreateInfo instance_info{};
instance_info.sType = VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO;
instance_info.pApplicationInfo = &app;
instance_info.enabledExtensionCount = extension_count;
instance_info.ppEnabledExtensionNames = extensions;
if (!detail::check(vkCreateInstance(&instance_info, nullptr, &instance_), "vkCreateInstance")) return false;
volkLoadInstance(instance_);
if (!SDL_Vulkan_CreateSurface(window_, instance_, nullptr, &surface_)) return false;
if (!this->pick_device()) return false;
if (!this->create_device()) return false;
if (!this->create_swapchain()) return false;
if (!this->create_render_pass()) return false;
if (!this->create_framebuffers()) return false;
if (!this->create_pipeline()) return false;
if (!this->create_commands()) return false;
if (!this->create_sync()) return false;
SDL_StartTextInput(window_);
return true;
}
/** Upload (or replace) the scene texture. */
[[nodiscard]] auto upload(const ra3::render::image &scene) -> bool {
if (scene.empty()) return false;
const auto width = scene.width();
const auto height = scene.height();
const VkDeviceSize bytes = static_cast<VkDeviceSize>(width) * height * 4U;
// Fast path: same size -> just refresh the pixels into the existing
// image. Recreating the image/view/sampler/descriptor every frame is
// what made the menu (which repaints often) stutter and flicker.
if (texture_ != VK_NULL_HANDLE && width == texture_width_ && height == texture_height_) {
VkBuffer staging = VK_NULL_HANDLE;
VkDeviceMemory staging_memory = VK_NULL_HANDLE;
VkBufferCreateInfo buffer_info{};
buffer_info.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO;
buffer_info.size = bytes;
buffer_info.usage = VK_BUFFER_USAGE_TRANSFER_SRC_BIT;
if (!detail::check(vkCreateBuffer(device_, &buffer_info, nullptr, &staging), "vkCreateBuffer(scene)")) return false;
VkMemoryRequirements br{};
vkGetBufferMemoryRequirements(device_, staging, &br);
VkMemoryAllocateInfo ba{};
ba.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO;
ba.allocationSize = br.size;
ba.memoryTypeIndex = this->memory_type(br.memoryTypeBits, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT);
if (!detail::check(vkAllocateMemory(device_, &ba, nullptr, &staging_memory), "vkAllocateMemory(scene)")) {
vkDestroyBuffer(device_, staging, nullptr);
return false;
}
vkBindBufferMemory(device_, staging, staging_memory, 0);
void *mapped = nullptr;
vkMapMemory(device_, staging_memory, 0, bytes, 0, &mapped);
std::memcpy(mapped, scene.data(), static_cast<std::size_t>(bytes));
vkUnmapMemory(device_, staging_memory);
this->immediate([&](VkCommandBuffer cmd) {
this->transition(cmd, texture_, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL);
VkBufferImageCopy region{};
region.imageSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0U, 0U, 1U};
region.imageExtent = {width, height, 1U};
vkCmdCopyBufferToImage(cmd, staging, texture_, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1U, &region);
this->transition(cmd, texture_, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL);
});
vkDestroyBuffer(device_, staging, nullptr);
vkFreeMemory(device_, staging_memory, nullptr);
return true;
}
this->destroy_texture();
VkBufferCreateInfo buffer_info{};
buffer_info.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO;
buffer_info.size = bytes;
buffer_info.usage = VK_BUFFER_USAGE_TRANSFER_SRC_BIT;
buffer_info.sharingMode = VK_SHARING_MODE_EXCLUSIVE;
VkBuffer staging = VK_NULL_HANDLE;
VkDeviceMemory staging_memory = VK_NULL_HANDLE;
if (!detail::check(vkCreateBuffer(device_, &buffer_info, nullptr, &staging), "vkCreateBuffer")) return false;
VkMemoryRequirements requirements{};
vkGetBufferMemoryRequirements(device_, staging, &requirements);
VkMemoryAllocateInfo allocate{};
allocate.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO;
allocate.allocationSize = requirements.size;
allocate.memoryTypeIndex = this->memory_type(requirements.memoryTypeBits, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT);
if (!detail::check(vkAllocateMemory(device_, &allocate, nullptr, &staging_memory), "vkAllocateMemory(staging)")) return false;
if (!detail::check(vkBindBufferMemory(device_, staging, staging_memory, 0), "vkBindBufferMemory")) return false;
void *mapped = nullptr;
if (!detail::check(vkMapMemory(device_, staging_memory, 0, bytes, 0, &mapped), "vkMapMemory")) return false;
std::memcpy(mapped, scene.data(), static_cast<std::size_t>(bytes));
vkUnmapMemory(device_, staging_memory);
// VK_FORMAT_B8G8R8A8_UNORM matches the software image's 0xAARRGGBB.
VkImageCreateInfo image_info{};
image_info.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO;
image_info.imageType = VK_IMAGE_TYPE_2D;
image_info.extent = {width, height, 1U};
image_info.mipLevels = 1U;
image_info.arrayLayers = 1U;
image_info.format = VK_FORMAT_B8G8R8A8_UNORM;
image_info.tiling = VK_IMAGE_TILING_OPTIMAL;
image_info.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
image_info.usage = VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_SAMPLED_BIT;
image_info.samples = VK_SAMPLE_COUNT_1_BIT;
image_info.sharingMode = VK_SHARING_MODE_EXCLUSIVE;
if (!detail::check(vkCreateImage(device_, &image_info, nullptr, &texture_), "vkCreateImage")) return false;
VkMemoryRequirements image_requirements{};
vkGetImageMemoryRequirements(device_, texture_, &image_requirements);
VkMemoryAllocateInfo image_allocate{};
image_allocate.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO;
image_allocate.allocationSize = image_requirements.size;
image_allocate.memoryTypeIndex = this->memory_type(image_requirements.memoryTypeBits, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT);
if (!detail::check(vkAllocateMemory(device_, &image_allocate, nullptr, &texture_memory_), "vkAllocateMemory(texture)")) return false;
if (!detail::check(vkBindImageMemory(device_, texture_, texture_memory_, 0), "vkBindImageMemory")) return false;
this->immediate([&](VkCommandBuffer cmd) {
this->transition(cmd, texture_, VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL);
VkBufferImageCopy region{};
region.imageSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0U, 0U, 1U};
region.imageExtent = {width, height, 1U};
vkCmdCopyBufferToImage(cmd, staging, texture_, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1U, &region);
this->transition(cmd, texture_, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL);
});
vkDestroyBuffer(device_, staging, nullptr);
vkFreeMemory(device_, staging_memory, nullptr);
VkImageViewCreateInfo view_info{};
view_info.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO;
view_info.image = texture_;
view_info.viewType = VK_IMAGE_VIEW_TYPE_2D;
view_info.format = VK_FORMAT_B8G8R8A8_UNORM;
view_info.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0U, 1U, 0U, 1U};
if (!detail::check(vkCreateImageView(device_, &view_info, nullptr, &texture_view_), "vkCreateImageView")) return false;
VkSamplerCreateInfo sampler_info{};
sampler_info.sType = VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO;
sampler_info.magFilter = VK_FILTER_NEAREST;
sampler_info.minFilter = VK_FILTER_NEAREST;
sampler_info.addressModeU = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE;
sampler_info.addressModeV = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE;
sampler_info.addressModeW = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE;
sampler_info.maxLod = 1.0F;
if (!detail::check(vkCreateSampler(device_, &sampler_info, nullptr, &sampler_), "vkCreateSampler")) return false;
VkDescriptorImageInfo descriptor_image{};
descriptor_image.imageLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
descriptor_image.imageView = texture_view_;
descriptor_image.sampler = sampler_;
VkWriteDescriptorSet write{};
write.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET;
write.dstSet = descriptor_set_;
write.dstBinding = 0U;
write.descriptorCount = 1U;
write.descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
write.pImageInfo = &descriptor_image;
vkUpdateDescriptorSets(device_, 1U, &write, 0U, nullptr);
texture_width_ = width;
texture_height_ = height;
return true;
}
/**
* Interactive 3D camera loop: the viewer owns the camera and asks
* `provider` to re-render whenever it moves.
*
* Controls: wheel / +/- move the camera closer or farther (camera
* height, not an image scale); WASD or the arrows and screen-edge pushes
* pan the target; left-drag rotates yaw/pitch; Q/E yaw; Esc quits.
*/
[[nodiscard]] auto present(const ra3::render::image &frame, const ra3::render::view_rect &dest, bool changed) -> bool override {
if (frame.empty()) return false;
if (changed || texture_ == VK_NULL_HANDLE || frame.width() != texture_width_ || frame.height() != texture_height_) {
if (!this->upload(frame)) return false;
}
int w = 0;
int h = 0;
SDL_GetWindowSizeInPixels(window_, &w, &h);
return this->draw(dest, static_cast<float>(w), static_cast<float>(h));
}
[[nodiscard]] auto poll_event(ra3::render::ui_event &out) -> bool override {
SDL_Event event;
while (SDL_PollEvent(&event)) {
switch (event.type) {
case SDL_EVENT_QUIT:
out = {};
out.type = ra3::render::ui_event_type::quit;
return true;
case SDL_EVENT_KEY_DOWN:
if (const auto key = map_key(event.key.key); key != ra3::render::ui_key::none) {
out = {};
out.type = ra3::render::ui_event_type::key;
out.key = key;
return true;
}
break;
case SDL_EVENT_TEXT_INPUT:
if (event.text.text[0] != '\0') {
out = {};
out.type = ra3::render::ui_event_type::text;
out.character = event.text.text[0];
return true;
}
break;
case SDL_EVENT_MOUSE_MOTION:
out = {};
out.type = ra3::render::ui_event_type::mouse_move;
out.x = event.motion.x;
out.y = event.motion.y;
out.dx = event.motion.xrel;
out.dy = event.motion.yrel;
out.left = (event.motion.state & SDL_BUTTON_LMASK) != 0U;
return true;
case SDL_EVENT_MOUSE_BUTTON_DOWN:
out = {};
out.type = ra3::render::ui_event_type::mouse_button;
out.x = event.button.x;
out.y = event.button.y;
out.left = event.button.button == SDL_BUTTON_LEFT;
return true;
case SDL_EVENT_MOUSE_WHEEL:
out = {};
out.type = ra3::render::ui_event_type::wheel;
out.wheel = event.wheel.y;
return true;
default:
break;
}
}
return false;
}
[[nodiscard]] auto window_size() const -> std::pair<int, int> override {
int w = 0;
int h = 0;
SDL_GetWindowSizeInPixels(window_, &w, &h);
return {w, h};
}
[[nodiscard]] auto key_down(ra3::render::ui_key key) const -> bool override {
const bool *keys = SDL_GetKeyboardState(nullptr);
if (keys == nullptr) return false;
switch (key) {
case ra3::render::ui_key::up: return keys[SDL_SCANCODE_W] || keys[SDL_SCANCODE_UP];
case ra3::render::ui_key::down: return keys[SDL_SCANCODE_S] || keys[SDL_SCANCODE_DOWN];
case ra3::render::ui_key::left: return keys[SDL_SCANCODE_A] || keys[SDL_SCANCODE_LEFT];
case ra3::render::ui_key::right: return keys[SDL_SCANCODE_D] || keys[SDL_SCANCODE_RIGHT];
default: return false;
}
}
[[nodiscard]] auto supports_terrain() const -> bool override { return true; }
[[nodiscard]] auto present_terrain(const ra3::terrain::gpu_terrain &terrain, const ra3::render::camera3d &camera, float time_s,
const ra3::client::terrain_overlay &overlay) -> bool override {
if (!terrain_ready_) {
if (!this->create_terrain_pipeline()) return false;
if (!this->create_terrain_images(terrain)) return false;
if (!this->create_terrain_descriptors()) return false;
if (!terrain.objects.empty()) {
if (!this->create_object_pipeline()) return false;
if (!this->create_object_buffers(terrain)) return false;
if (!this->create_object_descriptors()) return false;
objects_ready_ = true;
}
terrain_ready_ = true;
}
if (overlay.label_changed) this->update_overlay(overlay_label_, overlay_label_set_, overlay_label_w_, overlay_label_h_, overlay.label);
if (overlay.minimap_changed) this->update_overlay(overlay_minimap_, overlay_minimap_set_, overlay_minimap_w_, overlay_minimap_h_, overlay.minimap);
int w = 0;
int h = 0;
SDL_GetWindowSizeInPixels(window_, &w, &h);
const auto aspect = static_cast<float>(w) / static_cast<float>(std::max(1, h));
return this->draw_terrain(terrain, camera, time_s, aspect);
}
auto shutdown() -> void override { this->cleanup(); }
[[nodiscard]] auto name() const -> std::string_view override { return "vulkan"; }
private:
static constexpr uint32_t frames_in_flight = 2U;
[[nodiscard]] static auto map_key(SDL_Keycode key) -> ra3::render::ui_key {
using ra3::render::ui_key;
switch (key) {
case SDLK_UP: return ui_key::up;
case SDLK_DOWN: return ui_key::down;
case SDLK_LEFT: return ui_key::left;
case SDLK_RIGHT: return ui_key::right;
case SDLK_PAGEUP: return ui_key::page_up;
case SDLK_PAGEDOWN: return ui_key::page_down;
case SDLK_RETURN:
case SDLK_KP_ENTER: return ui_key::confirm;
case SDLK_ESCAPE: return ui_key::cancel;
case SDLK_TAB: return ui_key::tab;
case SDLK_BACKSPACE: return ui_key::backspace;
default: return ui_key::none;
}
}
struct gpu_image {
VkImage image = VK_NULL_HANDLE;
VkDeviceMemory memory = VK_NULL_HANDLE;
VkImageView view = VK_NULL_HANDLE;
VkSampler sampler = VK_NULL_HANDLE;
uint32_t mips = 1U;
uint32_t width = 0U;
uint32_t height = 0U;
};
/**
* Upload an overlay into a **persistent** fixed-size texture (created
* once, then only its pixels are refreshed). Reusing the image and its
* descriptor set avoids per-frame texture churn (which caused stutter)
* and the descriptor-pool growth (which caused corruption).
*/
auto update_overlay(gpu_image &target, VkDescriptorSet &set, uint32_t &out_w, uint32_t &out_h, const ra3::render::image &source) -> void {
if (source.empty()) {
out_w = 0;
out_h = 0;
return;
}
const auto w = source.width();
const auto h = source.height();
out_w = w;
out_h = h;
if (target.image == VK_NULL_HANDLE) {
const auto bytes = static_cast<VkDeviceSize>(w) * h * 4U;
if (!this->make_gpu_image(target, w, h, VK_FORMAT_B8G8R8A8_UNORM, source.data(), bytes, 1U, VK_FILTER_NEAREST, VK_SAMPLER_MIPMAP_MODE_NEAREST))
return;
VkDescriptorSetAllocateInfo allocate{};
allocate.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO;
allocate.descriptorPool = descriptor_pool_;
allocate.descriptorSetCount = 1U;
allocate.pSetLayouts = &descriptor_layout_;
if (vkAllocateDescriptorSets(device_, &allocate, &set) != VK_SUCCESS) return;
VkDescriptorImageInfo info{};
info.imageLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
info.imageView = target.view;
info.sampler = target.sampler;
VkWriteDescriptorSet write{};
write.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET;
write.dstSet = set;
write.dstBinding = 0U;
write.descriptorCount = 1U;
write.descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
write.pImageInfo = &info;
vkUpdateDescriptorSets(device_, 1U, &write, 0U, nullptr);
return; // the creation copy already holds the pixels
}
if (w > target.width || h > target.height) return; // fixed size, ignore growth
const auto bytes = static_cast<VkDeviceSize>(w) * h * 4U;
VkBuffer staging = VK_NULL_HANDLE;
VkDeviceMemory memory = VK_NULL_HANDLE;
VkBufferCreateInfo buffer_info{};
buffer_info.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO;
buffer_info.size = bytes;
buffer_info.usage = VK_BUFFER_USAGE_TRANSFER_SRC_BIT;
if (!detail::check(vkCreateBuffer(device_, &buffer_info, nullptr, &staging), "vkCreateBuffer(overlay)")) return;
VkMemoryRequirements req{};
vkGetBufferMemoryRequirements(device_, staging, &req);
VkMemoryAllocateInfo alloc{};
alloc.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO;
alloc.allocationSize = req.size;
alloc.memoryTypeIndex = this->memory_type(req.memoryTypeBits, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT);
if (!detail::check(vkAllocateMemory(device_, &alloc, nullptr, &memory), "vkAllocateMemory(overlay)")) {
vkDestroyBuffer(device_, staging, nullptr);
return;
}
vkBindBufferMemory(device_, staging, memory, 0);
void *mapped = nullptr;
vkMapMemory(device_, memory, 0, bytes, 0, &mapped);
std::memcpy(mapped, source.data(), static_cast<std::size_t>(bytes));
vkUnmapMemory(device_, memory);
this->immediate([&](VkCommandBuffer cmd) {
this->barrier_mip(cmd, target.image, 0U, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_ACCESS_SHADER_READ_BIT,
VK_ACCESS_TRANSFER_WRITE_BIT);
VkBufferImageCopy region{};
region.imageSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0U, 0U, 1U};
region.imageExtent = {w, h, 1U};
vkCmdCopyBufferToImage(cmd, staging, target.image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1U, &region);
this->barrier_mip(cmd, target.image, 0U, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, VK_ACCESS_TRANSFER_WRITE_BIT,
VK_ACCESS_SHADER_READ_BIT);
});
vkDestroyBuffer(device_, staging, nullptr);
vkFreeMemory(device_, memory, nullptr);
}
auto barrier_mip(VkCommandBuffer cmd, VkImage image, uint32_t level, VkImageLayout from, VkImageLayout to, VkAccessFlags src_access,
VkAccessFlags dst_access, uint32_t layer_count = 1U) -> void {
VkImageMemoryBarrier barrier{};
barrier.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER;
barrier.oldLayout = from;
barrier.newLayout = to;
barrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
barrier.image = image;
barrier.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, level, 1U, 0U, layer_count};
barrier.srcAccessMask = src_access;
barrier.dstAccessMask = dst_access;
vkCmdPipelineBarrier(cmd, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, 0U, 0U, nullptr, 0U, nullptr, 1U, &barrier);
}
[[nodiscard]] auto make_gpu_image(gpu_image &out, uint32_t w, uint32_t h, VkFormat format, const void *data, VkDeviceSize bytes, uint32_t mips,
VkFilter filter, VkSamplerMipmapMode mip_mode, uint32_t array_layers = 1U,
VkSamplerAddressMode address = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE) -> bool {
out.mips = std::max(1U, mips);
const auto layers = std::max(1U, array_layers);
out.width = w;
out.height = h;
VkImageCreateInfo image_info{};
image_info.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO;
image_info.imageType = VK_IMAGE_TYPE_2D;
image_info.extent = {w, h, 1U};
image_info.mipLevels = out.mips;
image_info.arrayLayers = layers;
image_info.format = format;
image_info.tiling = VK_IMAGE_TILING_OPTIMAL;
image_info.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
image_info.usage = VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT | VK_IMAGE_USAGE_SAMPLED_BIT;
image_info.samples = VK_SAMPLE_COUNT_1_BIT;
image_info.sharingMode = VK_SHARING_MODE_EXCLUSIVE;
if (!detail::check(vkCreateImage(device_, &image_info, nullptr, &out.image), "vkCreateImage(terrain)")) return false;
VkMemoryRequirements requirements{};
vkGetImageMemoryRequirements(device_, out.image, &requirements);
VkMemoryAllocateInfo allocate{};
allocate.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO;
allocate.allocationSize = requirements.size;
allocate.memoryTypeIndex = this->memory_type(requirements.memoryTypeBits, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT);
if (!detail::check(vkAllocateMemory(device_, &allocate, nullptr, &out.memory), "vkAllocateMemory(terrain)")) return false;
if (!detail::check(vkBindImageMemory(device_, out.image, out.memory, 0), "vkBindImageMemory")) return false;
VkBuffer staging = VK_NULL_HANDLE;
VkDeviceMemory staging_memory = VK_NULL_HANDLE;
if (bytes > 0U) {
VkBufferCreateInfo buffer_info{};
buffer_info.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO;
buffer_info.size = bytes;
buffer_info.usage = VK_BUFFER_USAGE_TRANSFER_SRC_BIT;
buffer_info.sharingMode = VK_SHARING_MODE_EXCLUSIVE;
if (!detail::check(vkCreateBuffer(device_, &buffer_info, nullptr, &staging), "vkCreateBuffer(terrain)")) return false;
VkMemoryRequirements br{};
vkGetBufferMemoryRequirements(device_, staging, &br);
VkMemoryAllocateInfo ba{};
ba.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO;
ba.allocationSize = br.size;
ba.memoryTypeIndex = this->memory_type(br.memoryTypeBits, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT);
if (!detail::check(vkAllocateMemory(device_, &ba, nullptr, &staging_memory), "vkAllocateMemory(staging terrain)")) return false;
if (!detail::check(vkBindBufferMemory(device_, staging, staging_memory, 0), "vkBindBufferMemory")) return false;
void *mapped = nullptr;
if (!detail::check(vkMapMemory(device_, staging_memory, 0, bytes, 0, &mapped), "vkMapMemory")) return false;
std::memcpy(mapped, data, static_cast<std::size_t>(bytes));
vkUnmapMemory(device_, staging_memory);
}
this->immediate([&](VkCommandBuffer cmd) {
for (uint32_t level = 0; level < out.mips; ++level) {
this->barrier_mip(cmd, out.image, level, VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 0U, VK_ACCESS_TRANSFER_WRITE_BIT, layers);
}
VkBufferImageCopy region{};
region.imageSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0U, 0U, layers};
region.imageExtent = {w, h, 1U};
vkCmdCopyBufferToImage(cmd, staging, out.image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1U, &region);
int32_t mw = static_cast<int32_t>(w);
int32_t mh = static_cast<int32_t>(h);
for (uint32_t level = 1; level < out.mips; ++level) {
this->barrier_mip(cmd, out.image, level - 1U, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, VK_ACCESS_TRANSFER_WRITE_BIT,
VK_ACCESS_TRANSFER_READ_BIT, layers);
const auto nw = std::max(1, mw / 2);
const auto nh = std::max(1, mh / 2);
for (uint32_t layer = 0; layer < layers; ++layer) {
VkImageBlit blit{};
blit.srcSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, level - 1U, layer, 1U};
blit.srcOffsets[1] = {mw, mh, 1};
blit.dstSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, level, layer, 1U};
blit.dstOffsets[1] = {nw, nh, 1};
vkCmdBlitImage(cmd, out.image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, out.image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1U, &blit, VK_FILTER_LINEAR);
}
mw = nw;
mh = nh;
}
for (uint32_t level = 0; level < out.mips; ++level) {
const auto from = level + 1U < out.mips ? VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL : VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL;
const auto src_access = level + 1U < out.mips ? VK_ACCESS_TRANSFER_READ_BIT : VK_ACCESS_TRANSFER_WRITE_BIT;
this->barrier_mip(cmd, out.image, level, from, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, src_access, VK_ACCESS_SHADER_READ_BIT, layers);
}
});
if (staging != VK_NULL_HANDLE) vkDestroyBuffer(device_, staging, nullptr);
if (staging_memory != VK_NULL_HANDLE) vkFreeMemory(device_, staging_memory, nullptr);
VkImageViewCreateInfo view_info{};
view_info.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO;
view_info.image = out.image;
view_info.viewType = layers > 1U ? VK_IMAGE_VIEW_TYPE_2D_ARRAY : VK_IMAGE_VIEW_TYPE_2D;
view_info.format = format;
view_info.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0U, out.mips, 0U, layers};
if (!detail::check(vkCreateImageView(device_, &view_info, nullptr, &out.view), "vkCreateImageView(terrain)")) return false;
VkSamplerCreateInfo sampler_info{};
sampler_info.sType = VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO;
sampler_info.magFilter = filter;
sampler_info.minFilter = filter;
sampler_info.mipmapMode = mip_mode;
sampler_info.addressModeU = address;
sampler_info.addressModeV = address;
sampler_info.addressModeW = address;
sampler_info.maxLod = static_cast<float>(out.mips);
return detail::check(vkCreateSampler(device_, &sampler_info, nullptr, &out.sampler), "vkCreateSampler(terrain)");
}
[[nodiscard]] auto create_terrain_images(const ra3::terrain::gpu_terrain &terrain) -> bool {
const auto height_bytes = static_cast<VkDeviceSize>(terrain.width) * terrain.height * 2U;
const auto cell_bytes = static_cast<VkDeviceSize>(terrain.width) * terrain.height * 8U;
const auto layer_bytes = static_cast<VkDeviceSize>(terrain.layer_count) * terrain.layer_size * terrain.layer_size * 4U;
// No mip chain: the array mips were the source of the corrupted
// patches, and the raymarcher samples close to 1:1 anyway.
const auto mips = 1U;
if (!this->make_gpu_image(height_texture_, terrain.width, terrain.height, VK_FORMAT_R16_UNORM, terrain.heights.data(), height_bytes, 1U, VK_FILTER_NEAREST,
VK_SAMPLER_MIPMAP_MODE_NEAREST))
return false;
if (!this->make_gpu_image(tile_texture_, terrain.width, terrain.height, VK_FORMAT_R16G16B16A16_UNORM, terrain.cell_data.data(), cell_bytes, 1U,
VK_FILTER_NEAREST, VK_SAMPLER_MIPMAP_MODE_NEAREST))
return false;
return this->make_gpu_image(atlas_texture_, terrain.layer_size, terrain.layer_size, VK_FORMAT_B8G8R8A8_UNORM, terrain.layers.data(), layer_bytes,
mips, VK_FILTER_LINEAR, VK_SAMPLER_MIPMAP_MODE_LINEAR, terrain.layer_count, VK_SAMPLER_ADDRESS_MODE_REPEAT);
}
[[nodiscard]] auto create_terrain_pipeline() -> bool {
VkDescriptorSetLayoutBinding bindings[3]{};
for (uint32_t i = 0; i < 3U; ++i) {
bindings[i].binding = i;
bindings[i].descriptorCount = 1U;
bindings[i].descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
bindings[i].stageFlags = VK_SHADER_STAGE_FRAGMENT_BIT;
}
VkDescriptorSetLayoutCreateInfo layout_info{};
layout_info.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO;
layout_info.bindingCount = 3U;
layout_info.pBindings = bindings;
if (!detail::check(vkCreateDescriptorSetLayout(device_, &layout_info, nullptr, &terrain_layout_), "vkCreateDescriptorSetLayout(terrain)")) return false;
VkPushConstantRange push_range{};
push_range.stageFlags = VK_SHADER_STAGE_FRAGMENT_BIT;
push_range.size = sizeof(float) * 20U;
VkPipelineLayoutCreateInfo pipeline_layout_info{};
pipeline_layout_info.sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO;
pipeline_layout_info.setLayoutCount = 1U;
pipeline_layout_info.pSetLayouts = &terrain_layout_;
pipeline_layout_info.pushConstantRangeCount = 1U;
pipeline_layout_info.pPushConstantRanges = &push_range;
if (!detail::check(vkCreatePipelineLayout(device_, &pipeline_layout_info, nullptr, &terrain_pipeline_layout_), "vkCreatePipelineLayout(terrain)"))
return false;
VkShaderModule vertex = VK_NULL_HANDLE;
VkShaderModule fragment = VK_NULL_HANDLE;
if (!this->make_shader_module(ra3_shaders::terrain_vert_spv, ra3_shaders::terrain_vert_spv_words, vertex)) return false;
if (!this->make_shader_module(ra3_shaders::terrain_frag_spv, ra3_shaders::terrain_frag_spv_words, fragment)) return false;
VkPipelineShaderStageCreateInfo stages[2]{};
stages[0].sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO;
stages[0].stage = VK_SHADER_STAGE_VERTEX_BIT;
stages[0].module = vertex;
stages[0].pName = "main";
stages[1].sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO;
stages[1].stage = VK_SHADER_STAGE_FRAGMENT_BIT;
stages[1].module = fragment;
stages[1].pName = "main";
VkPipelineVertexInputStateCreateInfo vertex_input{};
vertex_input.sType = VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO;
VkPipelineInputAssemblyStateCreateInfo assembly{};
assembly.sType = VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO;
assembly.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST;
VkPipelineViewportStateCreateInfo viewport{};
viewport.sType = VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO;
viewport.viewportCount = 1U;
viewport.scissorCount = 1U;
VkPipelineRasterizationStateCreateInfo raster{};
raster.sType = VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO;
raster.polygonMode = VK_POLYGON_MODE_FILL;
raster.cullMode = VK_CULL_MODE_NONE;
raster.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE;
raster.lineWidth = 1.0F;
VkPipelineMultisampleStateCreateInfo multisample{};
multisample.sType = VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO;
multisample.rasterizationSamples = VK_SAMPLE_COUNT_1_BIT;
VkPipelineDepthStencilStateCreateInfo depth_stencil{};
depth_stencil.sType = VK_STRUCTURE_TYPE_PIPELINE_DEPTH_STENCIL_STATE_CREATE_INFO;
depth_stencil.depthTestEnable = VK_TRUE;
depth_stencil.depthWriteEnable = VK_TRUE;
depth_stencil.depthCompareOp = VK_COMPARE_OP_LESS_OR_EQUAL;
VkPipelineColorBlendAttachmentState blend_attachment{};
blend_attachment.colorWriteMask = VK_COLOR_COMPONENT_R_BIT | VK_COLOR_COMPONENT_G_BIT | VK_COLOR_COMPONENT_B_BIT | VK_COLOR_COMPONENT_A_BIT;
VkPipelineColorBlendStateCreateInfo blend{};
blend.sType = VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO;
blend.attachmentCount = 1U;
blend.pAttachments = &blend_attachment;
const VkDynamicState dynamic_states[] = {VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR};
VkPipelineDynamicStateCreateInfo dynamic{};
dynamic.sType = VK_STRUCTURE_TYPE_PIPELINE_DYNAMIC_STATE_CREATE_INFO;
dynamic.dynamicStateCount = 2U;
dynamic.pDynamicStates = dynamic_states;
VkGraphicsPipelineCreateInfo pipeline_info{};
pipeline_info.sType = VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO;
pipeline_info.stageCount = 2U;
pipeline_info.pStages = stages;
pipeline_info.pVertexInputState = &vertex_input;
pipeline_info.pInputAssemblyState = &assembly;
pipeline_info.pViewportState = &viewport;
pipeline_info.pRasterizationState = &raster;
pipeline_info.pMultisampleState = &multisample;
pipeline_info.pDepthStencilState = &depth_stencil;
pipeline_info.pColorBlendState = &blend;
pipeline_info.pDynamicState = &dynamic;
pipeline_info.layout = terrain_pipeline_layout_;
pipeline_info.renderPass = render_pass_;
pipeline_info.subpass = 0U;
const auto created = detail::check(vkCreateGraphicsPipelines(device_, VK_NULL_HANDLE, 1U, &pipeline_info, nullptr, &terrain_pipeline_),
"vkCreateGraphicsPipelines(terrain)");
vkDestroyShaderModule(device_, vertex, nullptr);
vkDestroyShaderModule(device_, fragment, nullptr);
return created;
}
[[nodiscard]] auto create_terrain_descriptors() -> bool {
VkDescriptorPoolSize pool_size{VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, 3U};
VkDescriptorPoolCreateInfo pool_info{};
pool_info.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO;
pool_info.maxSets = 1U;
pool_info.poolSizeCount = 1U;
pool_info.pPoolSizes = &pool_size;
if (!detail::check(vkCreateDescriptorPool(device_, &pool_info, nullptr, &terrain_pool_), "vkCreateDescriptorPool(terrain)")) return false;
VkDescriptorSetAllocateInfo set_info{};
set_info.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO;
set_info.descriptorPool = terrain_pool_;
set_info.descriptorSetCount = 1U;
set_info.pSetLayouts = &terrain_layout_;
if (!detail::check(vkAllocateDescriptorSets(device_, &set_info, &terrain_set_), "vkAllocateDescriptorSets(terrain)")) return false;
VkDescriptorImageInfo infos[3]{};
const gpu_image *images[3] = {&height_texture_, &tile_texture_, &atlas_texture_};
VkWriteDescriptorSet writes[3]{};
for (uint32_t i = 0; i < 3U; ++i) {
infos[i].imageLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
infos[i].imageView = images[i]->view;
infos[i].sampler = images[i]->sampler;
writes[i].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET;
writes[i].dstSet = terrain_set_;
writes[i].dstBinding = i;
writes[i].descriptorCount = 1U;
writes[i].descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
writes[i].pImageInfo = &infos[i];
}
vkUpdateDescriptorSets(device_, 3U, writes, 0U, nullptr);
return true;
}
/** Host-visible vertex/index buffer, filled once (the map's geometry is static). */
[[nodiscard]] auto make_host_buffer(const void *data, VkDeviceSize bytes, VkBufferUsageFlags usage, VkBuffer &buffer,
VkDeviceMemory &memory) -> bool {
VkBufferCreateInfo info{};
info.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO;
info.size = bytes;
info.usage = usage;
info.sharingMode = VK_SHARING_MODE_EXCLUSIVE;
if (!detail::check(vkCreateBuffer(device_, &info, nullptr, &buffer), "vkCreateBuffer(object)")) return false;
VkMemoryRequirements requirements{};
vkGetBufferMemoryRequirements(device_, buffer, &requirements);
VkMemoryAllocateInfo allocate{};
allocate.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO;
allocate.allocationSize = requirements.size;
allocate.memoryTypeIndex = this->memory_type(requirements.memoryTypeBits, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT);
if (!detail::check(vkAllocateMemory(device_, &allocate, nullptr, &memory), "vkAllocateMemory(object)")) return false;
if (!detail::check(vkBindBufferMemory(device_, buffer, memory, 0), "vkBindBufferMemory(object)")) return false;
void *mapped = nullptr;
if (!detail::check(vkMapMemory(device_, memory, 0, bytes, 0, &mapped), "vkMapMemory(object)")) return false;
std::memcpy(mapped, data, static_cast<std::size_t>(bytes));
vkUnmapMemory(device_, memory);
return true;
}
/** Upload the static-map geometry and its texture array. */
[[nodiscard]] auto create_object_buffers(const ra3::terrain::gpu_terrain &terrain) -> bool {
const auto &scene = terrain.objects;
if (scene.empty()) return true;
const auto vertex_bytes = static_cast<VkDeviceSize>(scene.vertices.size()) * sizeof(ra3::models::vertex);
const auto index_bytes = static_cast<VkDeviceSize>(scene.indices.size()) * sizeof(uint32_t);
if (!this->make_host_buffer(scene.vertices.data(), vertex_bytes, VK_BUFFER_USAGE_VERTEX_BUFFER_BIT, object_vertex_buffer_, object_vertex_memory_)) return false;
if (!this->make_host_buffer(scene.indices.data(), index_bytes, VK_BUFFER_USAGE_INDEX_BUFFER_BIT, object_index_buffer_, object_index_memory_)) return false;
object_index_count_ = static_cast<uint32_t>(scene.indices.size());
const auto size = std::max(1U, scene.texture_size);
const auto layers = std::max<size_t>(1U, scene.textures.size());
std::vector<uint32_t> pixels(static_cast<size_t>(size) * size * layers, 0xFFFFFFFFU);
for (size_t layer = 0; layer < scene.textures.size(); ++layer) {
const auto &texture = scene.textures[layer];
if (texture.empty()) continue;
// The scene already resized every texture to `texture_size`.
for (uint32_t y = 0; y < std::min(size, texture.height()); ++y) {
for (uint32_t x = 0; x < std::min(size, texture.width()); ++x) {
pixels[(static_cast<size_t>(layer) * size + y) * size + x] = texture.data()[static_cast<size_t>(y) * texture.width() + x];
}
}
}
return this->make_gpu_image(object_texture_, size, size, VK_FORMAT_B8G8R8A8_UNORM, pixels.data(),
static_cast<VkDeviceSize>(pixels.size()) * 4U, this->mip_count(size), VK_FILTER_LINEAR, VK_SAMPLER_MIPMAP_MODE_LINEAR,
static_cast<uint32_t>(layers), VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE);
}
/** Full mip chain length for a square texture. */
[[nodiscard]] static auto mip_count(uint32_t size) -> uint32_t {
uint32_t mips = 1U;
while (size > 1U) {
size >>= 1U;
++mips;
}
return mips;
}
[[nodiscard]] auto create_object_pipeline() -> bool {
VkPushConstantRange push_range{};
push_range.stageFlags = VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT;
push_range.size = sizeof(float) * 20U;
VkPipelineLayoutCreateInfo layout_info{};
layout_info.sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO;
layout_info.setLayoutCount = 1U;
layout_info.pSetLayouts = &descriptor_layout_; // reused: one combined image sampler
layout_info.pushConstantRangeCount = 1U;
layout_info.pPushConstantRanges = &push_range;
if (!detail::check(vkCreatePipelineLayout(device_, &layout_info, nullptr, &object_pipeline_layout_), "vkCreatePipelineLayout(object)")) return false;
VkShaderModule vertex = VK_NULL_HANDLE;
VkShaderModule fragment = VK_NULL_HANDLE;
if (!this->make_shader_module(ra3_shaders::object_vert_spv, ra3_shaders::object_vert_spv_words, vertex)) return false;
if (!this->make_shader_module(ra3_shaders::object_frag_spv, ra3_shaders::object_frag_spv_words, fragment)) return false;
VkPipelineShaderStageCreateInfo stages[2]{};
stages[0].sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO;
stages[0].stage = VK_SHADER_STAGE_VERTEX_BIT;
stages[0].module = vertex;
stages[0].pName = "main";
stages[1].sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO;
stages[1].stage = VK_SHADER_STAGE_FRAGMENT_BIT;
stages[1].module = fragment;
stages[1].pName = "main";
VkVertexInputBindingDescription binding{};
binding.binding = 0U;
binding.stride = sizeof(ra3::models::vertex);
binding.inputRate = VK_VERTEX_INPUT_RATE_VERTEX;
VkVertexInputAttributeDescription attributes[4]{};
attributes[0] = {0U, 0U, VK_FORMAT_R32G32B32_SFLOAT, 0U};
attributes[1] = {1U, 0U, VK_FORMAT_R32G32B32_SFLOAT, 12U};
attributes[2] = {2U, 0U, VK_FORMAT_R32G32_SFLOAT, 24U};
attributes[3] = {3U, 0U, VK_FORMAT_R32_SFLOAT, 32U};
static_assert(sizeof(ra3::models::vertex) == 36U, "object vertex layout changed");
VkPipelineVertexInputStateCreateInfo vertex_input{};
vertex_input.sType = VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO;
vertex_input.vertexBindingDescriptionCount = 1U;
vertex_input.pVertexBindingDescriptions = &binding;
vertex_input.vertexAttributeDescriptionCount = 4U;
vertex_input.pVertexAttributeDescriptions = attributes;
VkPipelineInputAssemblyStateCreateInfo assembly{};
assembly.sType = VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO;
assembly.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST;
VkPipelineViewportStateCreateInfo viewport{};
viewport.sType = VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO;
viewport.viewportCount = 1U;
viewport.scissorCount = 1U;
VkPipelineRasterizationStateCreateInfo raster{};
raster.sType = VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO;
raster.polygonMode = VK_POLYGON_MODE_FILL;
raster.cullMode = VK_CULL_MODE_NONE;
raster.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE;
raster.lineWidth = 1.0F;
// Ground decals (sidewalks/roads/deck pieces) sit exactly on the
// terrain; a small negative depth bias keeps them from z-fighting.
raster.depthBiasEnable = VK_TRUE;
raster.depthBiasConstantFactor = -1.0F;
raster.depthBiasSlopeFactor = -1.0F;
VkPipelineMultisampleStateCreateInfo multisample{};
multisample.sType = VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO;
multisample.rasterizationSamples = VK_SAMPLE_COUNT_1_BIT;
VkPipelineDepthStencilStateCreateInfo depth_stencil{};
depth_stencil.sType = VK_STRUCTURE_TYPE_PIPELINE_DEPTH_STENCIL_STATE_CREATE_INFO;
depth_stencil.depthTestEnable = VK_TRUE;
depth_stencil.depthWriteEnable = VK_TRUE;
depth_stencil.depthCompareOp = VK_COMPARE_OP_LESS_OR_EQUAL;
VkPipelineColorBlendAttachmentState blend_attachment{};
blend_attachment.colorWriteMask = VK_COLOR_COMPONENT_R_BIT | VK_COLOR_COMPONENT_G_BIT | VK_COLOR_COMPONENT_B_BIT | VK_COLOR_COMPONENT_A_BIT;
blend_attachment.blendEnable = VK_FALSE;
VkPipelineColorBlendStateCreateInfo blend{};
blend.sType = VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO;
blend.attachmentCount = 1U;
blend.pAttachments = &blend_attachment;
const VkDynamicState dynamic_states[] = {VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR};
VkPipelineDynamicStateCreateInfo dynamic{};
dynamic.sType = VK_STRUCTURE_TYPE_PIPELINE_DYNAMIC_STATE_CREATE_INFO;
dynamic.dynamicStateCount = 2U;
dynamic.pDynamicStates = dynamic_states;
VkGraphicsPipelineCreateInfo pipeline_info{};
pipeline_info.sType = VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO;
pipeline_info.stageCount = 2U;
pipeline_info.pStages = stages;
pipeline_info.pVertexInputState = &vertex_input;
pipeline_info.pInputAssemblyState = &assembly;
pipeline_info.pViewportState = &viewport;
pipeline_info.pRasterizationState = &raster;
pipeline_info.pMultisampleState = &multisample;
pipeline_info.pDepthStencilState = &depth_stencil;
pipeline_info.pColorBlendState = &blend;
pipeline_info.pDynamicState = &dynamic;
pipeline_info.layout = object_pipeline_layout_;
pipeline_info.renderPass = render_pass_;
pipeline_info.subpass = 0U;
const auto created = detail::check(vkCreateGraphicsPipelines(device_, VK_NULL_HANDLE, 1U, &pipeline_info, nullptr, &object_pipeline_),
"vkCreateGraphicsPipelines(object)");
vkDestroyShaderModule(device_, vertex, nullptr);
vkDestroyShaderModule(device_, fragment, nullptr);
return created;
}
[[nodiscard]] auto create_object_descriptors() -> bool {
if (object_texture_.view == VK_NULL_HANDLE) return false;
VkDescriptorSetAllocateInfo set_info{};
set_info.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO;
set_info.descriptorPool = descriptor_pool_;
set_info.descriptorSetCount = 1U;
set_info.pSetLayouts = &descriptor_layout_;
if (!detail::check(vkAllocateDescriptorSets(device_, &set_info, &object_set_), "vkAllocateDescriptorSets(object)")) return false;
VkDescriptorImageInfo info{};
info.imageLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
info.imageView = object_texture_.view;
info.sampler = object_texture_.sampler;
VkWriteDescriptorSet write{};
write.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET;
write.dstSet = object_set_;
write.dstBinding = 0U;
write.descriptorCount = 1U;
write.descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
write.pImageInfo = &info;
vkUpdateDescriptorSets(device_, 1U, &write, 0U, nullptr);
return true;
}
[[nodiscard]] auto draw_terrain(const ra3::terrain::gpu_terrain &terrain, const ra3::render::camera3d &camera, float time_s, float aspect) -> bool {
vkWaitForFences(device_, 1U, &in_flight_[current_frame_], VK_TRUE, UINT64_MAX);
uint32_t image_index = 0;
const auto acquire = vkAcquireNextImageKHR(device_, swapchain_, UINT64_MAX, image_available_[current_frame_], VK_NULL_HANDLE, &image_index);
if (acquire == VK_ERROR_OUT_OF_DATE_KHR) return this->recreate_swapchain();
if (acquire != VK_SUCCESS && acquire != VK_SUBOPTIMAL_KHR) return false;
vkResetFences(device_, 1U, &in_flight_[current_frame_]);
const auto cmd = command_buffers_[current_frame_];
vkResetCommandBuffer(cmd, 0U);
VkCommandBufferBeginInfo begin{};
begin.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO;
vkBeginCommandBuffer(cmd, &begin);
VkClearValue clear{};
clear.color = {{0.45F, 0.55F, 0.70F, 1.0F}};
VkClearValue depth_clear{};
depth_clear.depthStencil = {1.0F, 0U};
const VkClearValue clears[2] = {clear, depth_clear};
VkRenderPassBeginInfo render_pass_begin{};
render_pass_begin.sType = VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO;
render_pass_begin.renderPass = render_pass_;
render_pass_begin.framebuffer = framebuffers_[image_index];
render_pass_begin.renderArea = {{0U, 0U}, swapchain_extent_};
render_pass_begin.clearValueCount = 2U;
render_pass_begin.pClearValues = clears;
vkCmdBeginRenderPass(cmd, &render_pass_begin, VK_SUBPASS_CONTENTS_INLINE);
VkViewport viewport{0.0F, 0.0F, static_cast<float>(swapchain_extent_.width), static_cast<float>(swapchain_extent_.height), 0.0F, 1.0F};
VkRect2D scissor{{0U, 0U}, swapchain_extent_};
vkCmdSetViewport(cmd, 0U, 1U, &viewport);
vkCmdSetScissor(cmd, 0U, 1U, &scissor);
vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, terrain_pipeline_);
vkCmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, terrain_pipeline_layout_, 0U, 1U, &terrain_set_, 0U, nullptr);
const float push[20] = {camera.target_x,
camera.target_y,
camera.yaw,
camera.height,
camera.pitch,
camera.fov,
terrain.water_z,
terrain.has_water ? 1.0F : 0.0F,
0.45F,
0.35F,
0.82F,
0.38F,
static_cast<float>(terrain.width),
static_cast<float>(terrain.height),
0.0F,
terrain.z_scale,
time_s,
0.0F,
terrain.cell_span,
aspect};
vkCmdPushConstants(cmd, terrain_pipeline_layout_, VK_SHADER_STAGE_FRAGMENT_BIT, 0U, sizeof(push), push);
vkCmdDraw(cmd, 3U, 1U, 0U, 0U);
// Static-map models (buildings/props) depth-test against the terrain.
if (objects_ready_ && object_index_count_ > 0U) {
const auto z_scale = terrain.z_scale;
const auto world_w = static_cast<float>(terrain.width) * 10.0F;
const auto world_h = static_cast<float>(terrain.height) * 10.0F;
const auto sample = [&](float wx, float wy) -> float {
if (wx < 0.0F || wy < 0.0F || wx >= world_w || wy >= world_h) return -1.0e9F;
const auto cx = std::min(terrain.width - 1U, static_cast<uint32_t>(wx / 10.0F));
const auto cy = std::min(terrain.height - 1U, static_cast<uint32_t>((world_h - wy) / 10.0F));
return static_cast<float>(terrain.heights[static_cast<size_t>(cy) * terrain.width + cx]) * z_scale;
};
float target_z = sample(camera.target_x, camera.target_y);
if (target_z < -1.0e8F) target_z = 0.0F;
const auto pitch = std::clamp(camera.pitch, 0.15F, 1.45F);
const auto fov = std::clamp(camera.fov, 0.3F, 1.4F);
const auto cp = std::cos(pitch);
const float fwd[3] = {cp * std::sin(camera.yaw), cp * std::cos(camera.yaw), -std::sin(pitch)};
float right[3] = {fwd[1], -fwd[0], 0.0F};
const auto rl = std::sqrt(right[0] * right[0] + right[1] * right[1]);
right[0] /= rl;
right[1] /= rl;
float up[3] = {right[1] * fwd[2], -right[0] * fwd[2], right[0] * fwd[1] - right[1] * fwd[0]};
const auto ul = std::sqrt(up[0] * up[0] + up[1] * up[1] + up[2] * up[2]);
up[0] /= ul;
up[1] /= ul;
up[2] /= ul;
const auto dist = camera.height / std::sin(pitch);
const float cam[3] = {camera.target_x - fwd[0] * dist, camera.target_y - fwd[1] * dist, target_z + camera.height - fwd[2] * dist};
const auto th = std::tan(fov * 0.5F);
const float obj_push[20] = {cam[0], cam[1], cam[2], 0.0F,
fwd[0], fwd[1], fwd[2], 0.0F,
right[0], right[1], right[2], th,
up[0], up[1], up[2], th * aspect,
0.45F, 0.35F, 0.82F, 0.38F};
vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, object_pipeline_);
vkCmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, object_pipeline_layout_, 0U, 1U, &object_set_, 0U, nullptr);
vkCmdPushConstants(cmd, object_pipeline_layout_, VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT, 0U, sizeof(obj_push), obj_push);
const VkDeviceSize offset = 0U;
vkCmdBindVertexBuffers(cmd, 0U, 1U, &object_vertex_buffer_, &offset);
vkCmdBindIndexBuffer(cmd, object_index_buffer_, 0U, VK_INDEX_TYPE_UINT32);
vkCmdDrawIndexed(cmd, object_index_count_, 1U, 0U, 0, 0U);
}
// Overlays (top-left FPS label, bottom-right minimap) use the image
// pipeline with alpha blending, drawn into the same render pass.
const auto draw_overlay = [&](const gpu_image &overlay, VkDescriptorSet set, uint32_t w, uint32_t h, float x, float y) {
if (set == VK_NULL_HANDLE || overlay.image == VK_NULL_HANDLE || w == 0U || h == 0U) return;
vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline_);
vkCmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline_layout_, 0U, 1U, &set, 0U, nullptr);
const float rect[4] = {x / static_cast<float>(swapchain_extent_.width), y / static_cast<float>(swapchain_extent_.height),
static_cast<float>(w) / static_cast<float>(swapchain_extent_.width),
static_cast<float>(h) / static_cast<float>(swapchain_extent_.height)};
vkCmdPushConstants(cmd, pipeline_layout_, VK_SHADER_STAGE_VERTEX_BIT, 0U, sizeof(rect), rect);
vkCmdDraw(cmd, 3U, 1U, 0U, 0U);
};
const float margin = 12.0F;
draw_overlay(overlay_label_, overlay_label_set_, overlay_label_w_, overlay_label_h_, margin, margin);
// Minimap lives in the top-right corner.
draw_overlay(overlay_minimap_, overlay_minimap_set_, overlay_minimap_w_, overlay_minimap_h_,
static_cast<float>(swapchain_extent_.width) - static_cast<float>(overlay_minimap_w_) - margin, margin);
vkCmdEndRenderPass(cmd);
vkEndCommandBuffer(cmd);
const VkPipelineStageFlags wait_stage = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT;
VkSubmitInfo submit{};
submit.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO;
submit.waitSemaphoreCount = 1U;
submit.pWaitSemaphores = &image_available_[current_frame_];
submit.pWaitDstStageMask = &wait_stage;
submit.commandBufferCount = 1U;
submit.pCommandBuffers = &cmd;
submit.signalSemaphoreCount = 1U;
submit.pSignalSemaphores = &render_finished_[current_frame_];
if (!detail::check(vkQueueSubmit(queue_, 1U, &submit, in_flight_[current_frame_]), "vkQueueSubmit(terrain)")) return false;
VkPresentInfoKHR present{};
present.sType = VK_STRUCTURE_TYPE_PRESENT_INFO_KHR;
present.waitSemaphoreCount = 1U;
present.pWaitSemaphores = &render_finished_[current_frame_];
present.swapchainCount = 1U;
present.pSwapchains = &swapchain_;
present.pImageIndices = &image_index;
const auto result = vkQueuePresentKHR(queue_, &present);
if (result == VK_ERROR_OUT_OF_DATE_KHR || result == VK_SUBOPTIMAL_KHR) {
if (!this->recreate_swapchain()) return false;
} else if (result != VK_SUCCESS) {
return false;
}
current_frame_ = (current_frame_ + 1U) % frames_in_flight;
return true;
}
auto destroy_terrain() -> void {
for (gpu_image *image: {&height_texture_, &tile_texture_, &atlas_texture_, &overlay_label_, &overlay_minimap_}) {
if (image->sampler != VK_NULL_HANDLE) vkDestroySampler(device_, image->sampler, nullptr);
if (image->view != VK_NULL_HANDLE) vkDestroyImageView(device_, image->view, nullptr);
if (image->image != VK_NULL_HANDLE) vkDestroyImage(device_, image->image, nullptr);
if (image->memory != VK_NULL_HANDLE) vkFreeMemory(device_, image->memory, nullptr);
*image = {};
}
if (terrain_pool_ != VK_NULL_HANDLE) vkDestroyDescriptorPool(device_, terrain_pool_, nullptr);
if (terrain_pipeline_ != VK_NULL_HANDLE) vkDestroyPipeline(device_, terrain_pipeline_, nullptr);
if (terrain_pipeline_layout_ != VK_NULL_HANDLE) vkDestroyPipelineLayout(device_, terrain_pipeline_layout_, nullptr);
if (terrain_layout_ != VK_NULL_HANDLE) vkDestroyDescriptorSetLayout(device_, terrain_layout_, nullptr);
terrain_pool_ = VK_NULL_HANDLE;
terrain_pipeline_ = VK_NULL_HANDLE;
terrain_pipeline_layout_ = VK_NULL_HANDLE;
terrain_layout_ = VK_NULL_HANDLE;
terrain_set_ = VK_NULL_HANDLE;
overlay_label_set_ = VK_NULL_HANDLE;
overlay_minimap_set_ = VK_NULL_HANDLE;
if (object_vertex_buffer_ != VK_NULL_HANDLE) vkDestroyBuffer(device_, object_vertex_buffer_, nullptr);
if (object_index_buffer_ != VK_NULL_HANDLE) vkDestroyBuffer(device_, object_index_buffer_, nullptr);
if (object_vertex_memory_ != VK_NULL_HANDLE) vkFreeMemory(device_, object_vertex_memory_, nullptr);
if (object_index_memory_ != VK_NULL_HANDLE) vkFreeMemory(device_, object_index_memory_, nullptr);
if (object_texture_.sampler != VK_NULL_HANDLE) vkDestroySampler(device_, object_texture_.sampler, nullptr);
if (object_texture_.view != VK_NULL_HANDLE) vkDestroyImageView(device_, object_texture_.view, nullptr);
if (object_texture_.image != VK_NULL_HANDLE) vkDestroyImage(device_, object_texture_.image, nullptr);
if (object_texture_.memory != VK_NULL_HANDLE) vkFreeMemory(device_, object_texture_.memory, nullptr);
if (object_pipeline_ != VK_NULL_HANDLE) vkDestroyPipeline(device_, object_pipeline_, nullptr);
if (object_pipeline_layout_ != VK_NULL_HANDLE) vkDestroyPipelineLayout(device_, object_pipeline_layout_, nullptr);
object_vertex_buffer_ = VK_NULL_HANDLE;
object_index_buffer_ = VK_NULL_HANDLE;
object_vertex_memory_ = VK_NULL_HANDLE;
object_index_memory_ = VK_NULL_HANDLE;
object_texture_ = {};
object_pipeline_ = VK_NULL_HANDLE;
object_pipeline_layout_ = VK_NULL_HANDLE;
object_set_ = VK_NULL_HANDLE;
object_index_count_ = 0U;
objects_ready_ = false;
}
// ---- device selection ------------------------------------------------
[[nodiscard]] auto pick_device() -> bool {
uint32_t count = 0;
vkEnumeratePhysicalDevices(instance_, &count, nullptr);
if (count == 0U) return false;
std::vector<VkPhysicalDevice> devices(count);
vkEnumeratePhysicalDevices(instance_, &count, devices.data());
for (const auto device: devices) {
uint32_t family_count = 0;
vkGetPhysicalDeviceQueueFamilyProperties(device, &family_count, nullptr);
std::vector<VkQueueFamilyProperties> families(family_count);
vkGetPhysicalDeviceQueueFamilyProperties(device, &family_count, families.data());
bool has_graphics = false;
bool has_present = false;
for (uint32_t i = 0; i < family_count; ++i) {
if ((families[i].queueFlags & VK_QUEUE_GRAPHICS_BIT) != 0U) has_graphics = true;
VkBool32 present = VK_FALSE;
vkGetPhysicalDeviceSurfaceSupportKHR(device, i, surface_, &present);
if (present == VK_TRUE) has_present = true;
}
if (has_graphics && has_present) {
physical_ = device;
return true;
}
}
return false;
}
[[nodiscard]] auto create_device() -> bool {
uint32_t family_count = 0;
vkGetPhysicalDeviceQueueFamilyProperties(physical_, &family_count, nullptr);
std::vector<VkQueueFamilyProperties> families(family_count);
vkGetPhysicalDeviceQueueFamilyProperties(physical_, &family_count, families.data());
queue_family_ = UINT32_MAX;
for (uint32_t i = 0; i < family_count; ++i) {
if ((families[i].queueFlags & VK_QUEUE_GRAPHICS_BIT) == 0U) continue;
VkBool32 present = VK_FALSE;
vkGetPhysicalDeviceSurfaceSupportKHR(physical_, i, surface_, &present);
if (present == VK_TRUE) {
queue_family_ = i;
break;
}
}
if (queue_family_ == UINT32_MAX) return false;
const float priority = 1.0F;
VkDeviceQueueCreateInfo queue_info{};
queue_info.sType = VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO;
queue_info.queueFamilyIndex = queue_family_;
queue_info.queueCount = 1U;
queue_info.pQueuePriorities = &priority;
const char *extensions[] = {VK_KHR_SWAPCHAIN_EXTENSION_NAME};
VkDeviceCreateInfo device_info{};
device_info.sType = VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO;
device_info.queueCreateInfoCount = 1U;
device_info.pQueueCreateInfos = &queue_info;
device_info.enabledExtensionCount = 1U;
device_info.ppEnabledExtensionNames = extensions;
if (!detail::check(vkCreateDevice(physical_, &device_info, nullptr, &device_), "vkCreateDevice")) return false;
volkLoadDevice(device_);
vkGetDeviceQueue(device_, queue_family_, 0U, &queue_);
return true;
}
[[nodiscard]] auto memory_type(uint32_t bits, VkMemoryPropertyFlags properties) const -> uint32_t {
VkPhysicalDeviceMemoryProperties memory{};
vkGetPhysicalDeviceMemoryProperties(physical_, &memory);
for (uint32_t i = 0; i < memory.memoryTypeCount; ++i) {
if ((bits & (1U << i)) != 0U && (memory.memoryTypes[i].propertyFlags & properties) == properties) return i;
}
return 0U;
}
// ---- swapchain -------------------------------------------------------
[[nodiscard]] auto create_swapchain() -> bool {
VkSurfaceCapabilitiesKHR caps{};
vkGetPhysicalDeviceSurfaceCapabilitiesKHR(physical_, surface_, &caps);
uint32_t format_count = 0;
vkGetPhysicalDeviceSurfaceFormatsKHR(physical_, surface_, &format_count, nullptr);
std::vector<VkSurfaceFormatKHR> formats(format_count);
vkGetPhysicalDeviceSurfaceFormatsKHR(physical_, surface_, &format_count, formats.data());
VkSurfaceFormatKHR chosen_format{};
if (formats.empty()) {
chosen_format = {VK_FORMAT_B8G8R8A8_UNORM, VK_COLOR_SPACE_SRGB_NONLINEAR_KHR};
} else {
chosen_format = formats[0];
}
for (const auto &candidate: formats) {
if (candidate.format == VK_FORMAT_B8G8R8A8_UNORM && candidate.colorSpace == VK_COLOR_SPACE_SRGB_NONLINEAR_KHR) chosen_format = candidate;
}
VkExtent2D extent = caps.currentExtent;
if (extent.width == UINT32_MAX) {
int width = 0;
int height = 0;
SDL_GetWindowSizeInPixels(window_, &width, &height);
extent.width = std::clamp(static_cast<uint32_t>(width), caps.minImageExtent.width, caps.maxImageExtent.width);
extent.height = std::clamp(static_cast<uint32_t>(height), caps.minImageExtent.height, caps.maxImageExtent.height);
}
uint32_t image_count = caps.minImageCount + 1U;
if (caps.maxImageCount > 0U && image_count > caps.maxImageCount) image_count = caps.maxImageCount;
VkSwapchainCreateInfoKHR swapchain_info{};
swapchain_info.sType = VK_STRUCTURE_TYPE_SWAPCHAIN_CREATE_INFO_KHR;
swapchain_info.surface = surface_;
swapchain_info.minImageCount = image_count;
swapchain_info.imageFormat = chosen_format.format;
swapchain_info.imageColorSpace = chosen_format.colorSpace;
swapchain_info.imageExtent = extent;
swapchain_info.imageArrayLayers = 1U;
swapchain_info.imageUsage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT;
swapchain_info.imageSharingMode = VK_SHARING_MODE_EXCLUSIVE;
swapchain_info.preTransform = caps.currentTransform;
swapchain_info.compositeAlpha = VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR;
swapchain_info.presentMode = this->fps_limit_ == 0 ? VK_PRESENT_MODE_FIFO_KHR : VK_PRESENT_MODE_IMMEDIATE_KHR;
swapchain_info.clipped = VK_TRUE;
if (!detail::check(vkCreateSwapchainKHR(device_, &swapchain_info, nullptr, &swapchain_), "vkCreateSwapchainKHR")) return false;
uint32_t actual = 0;
vkGetSwapchainImagesKHR(device_, swapchain_, &actual, nullptr);
swapchain_images_.resize(actual);
vkGetSwapchainImagesKHR(device_, swapchain_, &actual, swapchain_images_.data());
swapchain_format_ = chosen_format.format;
swapchain_extent_ = extent;
swapchain_views_.resize(actual);
for (uint32_t i = 0; i < actual; ++i) {
VkImageViewCreateInfo view_info{};
view_info.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO;
view_info.image = swapchain_images_[i];
view_info.viewType = VK_IMAGE_VIEW_TYPE_2D;
view_info.format = swapchain_format_;
view_info.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0U, 1U, 0U, 1U};
if (!detail::check(vkCreateImageView(device_, &view_info, nullptr, &swapchain_views_[i]), "vkCreateImageView(swapchain)")) return false;
}
framebuffers_.resize(actual);
return true;
}
/** Create one color+depth framebuffer per swapchain image (needs `render_pass_`). */
[[nodiscard]] auto create_framebuffers() -> bool {
this->create_depth_resources();
for (size_t i = 0; i < framebuffers_.size(); ++i) {
VkImageView attachments[] = {swapchain_views_[i], depth_views_[i]};
VkFramebufferCreateInfo framebuffer_info{};
framebuffer_info.sType = VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO;
framebuffer_info.renderPass = render_pass_;
framebuffer_info.attachmentCount = 2U;
framebuffer_info.pAttachments = attachments;
framebuffer_info.width = swapchain_extent_.width;
framebuffer_info.height = swapchain_extent_.height;
framebuffer_info.layers = 1U;
if (!detail::check(vkCreateFramebuffer(device_, &framebuffer_info, nullptr, &framebuffers_[i]), "vkCreateFramebuffer")) return false;
}
return true;
}
/** Allocate a depth image + view per swapchain image (static-map models depth-test against the terrain). */
auto create_depth_resources() -> void {
this->destroy_depth_resources();
depth_images_.resize(swapchain_images_.size());
depth_memories_.resize(swapchain_images_.size());
depth_views_.resize(swapchain_images_.size());
for (size_t i = 0; i < swapchain_images_.size(); ++i) {
VkImageCreateInfo image_info{};
image_info.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO;
image_info.imageType = VK_IMAGE_TYPE_2D;
image_info.extent = {swapchain_extent_.width, swapchain_extent_.height, 1U};
image_info.mipLevels = 1U;
image_info.arrayLayers = 1U;
image_info.format = VK_FORMAT_D32_SFLOAT;
image_info.tiling = VK_IMAGE_TILING_OPTIMAL;
image_info.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
image_info.usage = VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT;
image_info.samples = VK_SAMPLE_COUNT_1_BIT;
image_info.sharingMode = VK_SHARING_MODE_EXCLUSIVE;
if (vkCreateImage(device_, &image_info, nullptr, &depth_images_[i]) != VK_SUCCESS) return;
VkMemoryRequirements requirements{};
vkGetImageMemoryRequirements(device_, depth_images_[i], &requirements);
VkMemoryAllocateInfo allocate{};
allocate.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO;
allocate.allocationSize = requirements.size;
allocate.memoryTypeIndex = this->memory_type(requirements.memoryTypeBits, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT);
if (vkAllocateMemory(device_, &allocate, nullptr, &depth_memories_[i]) != VK_SUCCESS) return;
vkBindImageMemory(device_, depth_images_[i], depth_memories_[i], 0);
VkImageViewCreateInfo view_info{};
view_info.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO;
view_info.image = depth_images_[i];
view_info.viewType = VK_IMAGE_VIEW_TYPE_2D;
view_info.format = VK_FORMAT_D32_SFLOAT;
view_info.subresourceRange = {VK_IMAGE_ASPECT_DEPTH_BIT, 0U, 1U, 0U, 1U};
if (vkCreateImageView(device_, &view_info, nullptr, &depth_views_[i]) != VK_SUCCESS) return;
}
}
auto destroy_depth_resources() -> void {
if (device_ == VK_NULL_HANDLE) return;
for (const auto view: depth_views_) vkDestroyImageView(device_, view, nullptr);
for (const auto image: depth_images_) vkDestroyImage(device_, image, nullptr);
for (const auto memory: depth_memories_) vkFreeMemory(device_, memory, nullptr);
depth_views_.clear();
depth_images_.clear();
depth_memories_.clear();
}
auto cleanup_swapchain() -> void {
for (const auto framebuffer: framebuffers_) vkDestroyFramebuffer(device_, framebuffer, nullptr);
this->destroy_depth_resources();
for (const auto view: swapchain_views_) vkDestroyImageView(device_, view, nullptr);
framebuffers_.clear();
swapchain_views_.clear();
swapchain_images_.clear();
if (swapchain_ != VK_NULL_HANDLE) vkDestroySwapchainKHR(device_, swapchain_, nullptr);
swapchain_ = VK_NULL_HANDLE;
}
[[nodiscard]] auto recreate_swapchain() -> bool {
int width = 0;
int height = 0;
SDL_GetWindowSizeInPixels(window_, &width, &height);
if (width == 0 || height == 0) return false;
vkDeviceWaitIdle(device_);
this->cleanup_swapchain();
return this->create_swapchain() && this->create_framebuffers();
}
// ---- pipeline --------------------------------------------------------
[[nodiscard]] auto create_render_pass() -> bool {
VkAttachmentDescription color{};
color.format = swapchain_format_;
color.samples = VK_SAMPLE_COUNT_1_BIT;
color.loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR;
color.storeOp = VK_ATTACHMENT_STORE_OP_STORE;
color.stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE;
color.stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE;
color.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
color.finalLayout = VK_IMAGE_LAYOUT_PRESENT_SRC_KHR;
VkAttachmentReference color_ref{0U, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL};
VkAttachmentDescription depth{};
depth.format = VK_FORMAT_D32_SFLOAT;
depth.samples = VK_SAMPLE_COUNT_1_BIT;
depth.loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR;
depth.storeOp = VK_ATTACHMENT_STORE_OP_DONT_CARE;
depth.stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE;
depth.stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE;
depth.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
depth.finalLayout = VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL;
VkAttachmentReference depth_ref{1U, VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL};
VkSubpassDescription subpass{};
subpass.pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS;
subpass.colorAttachmentCount = 1U;
subpass.pColorAttachments = &color_ref;
subpass.pDepthStencilAttachment = &depth_ref;
VkSubpassDependency dependency{};
dependency.srcSubpass = VK_SUBPASS_EXTERNAL;
dependency.dstSubpass = 0U;
dependency.srcStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT | VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT;
dependency.dstStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT | VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT;
dependency.dstAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT | VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT;
const VkAttachmentDescription attachments[] = {color, depth};
VkRenderPassCreateInfo render_pass_info{};
render_pass_info.sType = VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO;
render_pass_info.attachmentCount = 2U;
render_pass_info.pAttachments = attachments;
render_pass_info.subpassCount = 1U;
render_pass_info.pSubpasses = &subpass;
render_pass_info.dependencyCount = 1U;
render_pass_info.pDependencies = &dependency;
return detail::check(vkCreateRenderPass(device_, &render_pass_info, nullptr, &render_pass_), "vkCreateRenderPass");
}
[[nodiscard]] auto make_shader_module(const uint32_t *code, std::size_t words, VkShaderModule &out) -> bool {
VkShaderModuleCreateInfo info{};
info.sType = VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO;
info.codeSize = words * sizeof(uint32_t);
info.pCode = code;
return detail::check(vkCreateShaderModule(device_, &info, nullptr, &out), "vkCreateShaderModule");
}
[[nodiscard]] auto create_pipeline() -> bool {
VkDescriptorSetLayoutBinding binding{};
binding.binding = 0U;
binding.descriptorCount = 1U;
binding.descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
binding.stageFlags = VK_SHADER_STAGE_FRAGMENT_BIT;
VkDescriptorSetLayoutCreateInfo layout_info{};
layout_info.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO;
layout_info.bindingCount = 1U;
layout_info.pBindings = &binding;
if (!detail::check(vkCreateDescriptorSetLayout(device_, &layout_info, nullptr, &descriptor_layout_), "vkCreateDescriptorSetLayout")) return false;
VkPushConstantRange push_range{};
push_range.stageFlags = VK_SHADER_STAGE_VERTEX_BIT;
push_range.size = sizeof(float) * 4U;
VkPipelineLayoutCreateInfo pipeline_layout_info{};
pipeline_layout_info.sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO;
pipeline_layout_info.setLayoutCount = 1U;
pipeline_layout_info.pSetLayouts = &descriptor_layout_;
pipeline_layout_info.pushConstantRangeCount = 1U;
pipeline_layout_info.pPushConstantRanges = &push_range;
if (!detail::check(vkCreatePipelineLayout(device_, &pipeline_layout_info, nullptr, &pipeline_layout_), "vkCreatePipelineLayout")) return false;
VkShaderModule vertex = VK_NULL_HANDLE;
VkShaderModule fragment = VK_NULL_HANDLE;
if (!this->make_shader_module(ra3_shaders::scene_vert_spv, ra3_shaders::scene_vert_spv_words, vertex)) return false;
if (!this->make_shader_module(ra3_shaders::scene_frag_spv, ra3_shaders::scene_frag_spv_words, fragment)) return false;
VkPipelineShaderStageCreateInfo stages[2]{};
stages[0].sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO;
stages[0].stage = VK_SHADER_STAGE_VERTEX_BIT;
stages[0].module = vertex;
stages[0].pName = "main";
stages[1].sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO;
stages[1].stage = VK_SHADER_STAGE_FRAGMENT_BIT;
stages[1].module = fragment;
stages[1].pName = "main";
VkPipelineVertexInputStateCreateInfo vertex_input{};
vertex_input.sType = VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO;
VkPipelineInputAssemblyStateCreateInfo assembly{};
assembly.sType = VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO;
assembly.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST;
VkPipelineViewportStateCreateInfo viewport{};
viewport.sType = VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO;
viewport.viewportCount = 1U;
viewport.scissorCount = 1U;
VkPipelineRasterizationStateCreateInfo raster{};
raster.sType = VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO;
raster.polygonMode = VK_POLYGON_MODE_FILL;
raster.cullMode = VK_CULL_MODE_NONE;
raster.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE;
raster.lineWidth = 1.0F;
VkPipelineMultisampleStateCreateInfo multisample{};
multisample.sType = VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO;
multisample.rasterizationSamples = VK_SAMPLE_COUNT_1_BIT;
VkPipelineColorBlendAttachmentState blend_attachment{};
blend_attachment.colorWriteMask = VK_COLOR_COMPONENT_R_BIT | VK_COLOR_COMPONENT_G_BIT | VK_COLOR_COMPONENT_B_BIT | VK_COLOR_COMPONENT_A_BIT;
blend_attachment.blendEnable = VK_TRUE;
blend_attachment.srcColorBlendFactor = VK_BLEND_FACTOR_SRC_ALPHA;
blend_attachment.dstColorBlendFactor = VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA;
blend_attachment.colorBlendOp = VK_BLEND_OP_ADD;
blend_attachment.srcAlphaBlendFactor = VK_BLEND_FACTOR_ONE;
blend_attachment.dstAlphaBlendFactor = VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA;
blend_attachment.alphaBlendOp = VK_BLEND_OP_ADD;
VkPipelineColorBlendStateCreateInfo blend{};
blend.sType = VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO;
blend.attachmentCount = 1U;
blend.pAttachments = &blend_attachment;
const VkDynamicState dynamic_states[] = {VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR};
VkPipelineDynamicStateCreateInfo dynamic{};
dynamic.sType = VK_STRUCTURE_TYPE_PIPELINE_DYNAMIC_STATE_CREATE_INFO;
dynamic.dynamicStateCount = 2U;
dynamic.pDynamicStates = dynamic_states;
VkGraphicsPipelineCreateInfo pipeline_info{};
pipeline_info.sType = VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO;
pipeline_info.stageCount = 2U;
pipeline_info.pStages = stages;
pipeline_info.pVertexInputState = &vertex_input;
pipeline_info.pInputAssemblyState = &assembly;
pipeline_info.pViewportState = &viewport;
pipeline_info.pRasterizationState = &raster;
pipeline_info.pMultisampleState = &multisample;
pipeline_info.pColorBlendState = &blend;
pipeline_info.pDynamicState = &dynamic;
pipeline_info.layout = pipeline_layout_;
pipeline_info.renderPass = render_pass_;
pipeline_info.subpass = 0U;
const auto created = detail::check(vkCreateGraphicsPipelines(device_, VK_NULL_HANDLE, 1U, &pipeline_info, nullptr, &pipeline_), "vkCreateGraphicsPipelines");
vkDestroyShaderModule(device_, vertex, nullptr);
vkDestroyShaderModule(device_, fragment, nullptr);
return created;
}
// ---- commands, sync, descriptor pool ---------------------------------
[[nodiscard]] auto create_commands() -> bool {
VkCommandPoolCreateInfo pool_info{};
pool_info.sType = VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO;
pool_info.flags = VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT;
pool_info.queueFamilyIndex = queue_family_;
if (!detail::check(vkCreateCommandPool(device_, &pool_info, nullptr, &command_pool_), "vkCreateCommandPool")) return false;
VkCommandBufferAllocateInfo allocate{};
allocate.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO;
allocate.commandPool = command_pool_;
allocate.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY;
allocate.commandBufferCount = frames_in_flight;
command_buffers_.resize(frames_in_flight);
if (!detail::check(vkAllocateCommandBuffers(device_, &allocate, command_buffers_.data()), "vkAllocateCommandBuffers")) return false;
VkDescriptorPoolSize pool_size{VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, 8U};
VkDescriptorPoolCreateInfo descriptor_pool_info{};
descriptor_pool_info.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO;
descriptor_pool_info.maxSets = 8U;
descriptor_pool_info.poolSizeCount = 1U;
descriptor_pool_info.pPoolSizes = &pool_size;
if (!detail::check(vkCreateDescriptorPool(device_, &descriptor_pool_info, nullptr, &descriptor_pool_), "vkCreateDescriptorPool")) return false;
VkDescriptorSetAllocateInfo set_info{};
set_info.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO;
set_info.descriptorPool = descriptor_pool_;
set_info.descriptorSetCount = 1U;
set_info.pSetLayouts = &descriptor_layout_;
return detail::check(vkAllocateDescriptorSets(device_, &set_info, &descriptor_set_), "vkAllocateDescriptorSets");
}
[[nodiscard]] auto create_sync() -> bool {
VkSemaphoreCreateInfo semaphore_info{};
semaphore_info.sType = VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO;
VkFenceCreateInfo fence_info{};
fence_info.sType = VK_STRUCTURE_TYPE_FENCE_CREATE_INFO;
fence_info.flags = VK_FENCE_CREATE_SIGNALED_BIT;
image_available_.resize(frames_in_flight);
render_finished_.resize(frames_in_flight);
in_flight_.resize(frames_in_flight);
for (uint32_t i = 0; i < frames_in_flight; ++i) {
if (!detail::check(vkCreateSemaphore(device_, &semaphore_info, nullptr, &image_available_[i]), "vkCreateSemaphore")) return false;
if (!detail::check(vkCreateSemaphore(device_, &semaphore_info, nullptr, &render_finished_[i]), "vkCreateSemaphore")) return false;
if (!detail::check(vkCreateFence(device_, &fence_info, nullptr, &in_flight_[i]), "vkCreateFence")) return false;
}
return true;
}
// ---- helpers ---------------------------------------------------------
/** Run `record` on a one-shot command buffer and wait for it. */
template<typename Fn>
auto immediate(Fn &&record) -> void {
VkCommandBufferAllocateInfo allocate{};
allocate.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO;
allocate.commandPool = command_pool_;
allocate.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY;
allocate.commandBufferCount = 1U;
VkCommandBuffer cmd = VK_NULL_HANDLE;
vkAllocateCommandBuffers(device_, &allocate, &cmd);
VkCommandBufferBeginInfo begin{};
begin.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO;
begin.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT;
vkBeginCommandBuffer(cmd, &begin);
record(cmd);
vkEndCommandBuffer(cmd);
VkSubmitInfo submit{};
submit.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO;
submit.commandBufferCount = 1U;
submit.pCommandBuffers = &cmd;
vkQueueSubmit(queue_, 1U, &submit, VK_NULL_HANDLE);
vkQueueWaitIdle(queue_);
vkFreeCommandBuffers(device_, command_pool_, 1U, &cmd);
}
static auto transition(VkCommandBuffer cmd, VkImage image, VkImageLayout from, VkImageLayout to) -> void {
VkImageMemoryBarrier barrier{};
barrier.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER;
barrier.oldLayout = from;
barrier.newLayout = to;
barrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
barrier.image = image;
barrier.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0U, 1U, 0U, 1U};
barrier.srcAccessMask = (to == VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL) ? 0U : VK_ACCESS_TRANSFER_WRITE_BIT;
barrier.dstAccessMask = (to == VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL) ? VK_ACCESS_TRANSFER_WRITE_BIT : VK_ACCESS_SHADER_READ_BIT;
vkCmdPipelineBarrier(cmd, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0U, 0U, nullptr, 0U, nullptr, 1U, &barrier);
}
/** Record, submit and present one frame. Returns false when the loop should stop. */
[[nodiscard]] auto draw(const ra3::render::view_rect &rect, float window_w, float window_h) -> bool {
vkWaitForFences(device_, 1U, &in_flight_[current_frame_], VK_TRUE, UINT64_MAX);
uint32_t image_index = 0;
const auto acquire = vkAcquireNextImageKHR(device_, swapchain_, UINT64_MAX, image_available_[current_frame_], VK_NULL_HANDLE, &image_index);
if (acquire == VK_ERROR_OUT_OF_DATE_KHR) return this->recreate_swapchain();
if (acquire != VK_SUCCESS && acquire != VK_SUBOPTIMAL_KHR) return false;
vkResetFences(device_, 1U, &in_flight_[current_frame_]);
const auto cmd = command_buffers_[current_frame_];
vkResetCommandBuffer(cmd, 0U);
VkCommandBufferBeginInfo begin{};
begin.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO;
vkBeginCommandBuffer(cmd, &begin);
VkClearValue clear{};
clear.color = {{0.05F, 0.06F, 0.08F, 1.0F}};
VkClearValue depth_clear{};
depth_clear.depthStencil = {1.0F, 0U};
const VkClearValue clears[2] = {clear, depth_clear};
VkRenderPassBeginInfo render_pass_begin{};
render_pass_begin.sType = VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO;
render_pass_begin.renderPass = render_pass_;
render_pass_begin.framebuffer = framebuffers_[image_index];
render_pass_begin.renderArea = {{0U, 0U}, swapchain_extent_};
render_pass_begin.clearValueCount = 2U;
render_pass_begin.pClearValues = clears;
vkCmdBeginRenderPass(cmd, &render_pass_begin, VK_SUBPASS_CONTENTS_INLINE);
VkViewport viewport{0.0F, 0.0F, static_cast<float>(swapchain_extent_.width), static_cast<float>(swapchain_extent_.height), 0.0F, 1.0F};
VkRect2D scissor{{0U, 0U}, swapchain_extent_};
vkCmdSetViewport(cmd, 0U, 1U, &viewport);
vkCmdSetScissor(cmd, 0U, 1U, &scissor);
vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline_);
vkCmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline_layout_, 0U, 1U, &descriptor_set_, 0U, nullptr);
// The vertex shader works in window-normalized coordinates (0..1), so
// convert the pixel rect the camera produced.
const float push[4] = {rect.x / window_w, rect.y / window_h, rect.w / window_w, rect.h / window_h};
vkCmdPushConstants(cmd, pipeline_layout_, VK_SHADER_STAGE_VERTEX_BIT, 0U, sizeof(push), push);
vkCmdDraw(cmd, 3U, 1U, 0U, 0U);
vkCmdEndRenderPass(cmd);
vkEndCommandBuffer(cmd);
const VkPipelineStageFlags wait_stage = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT;
VkSubmitInfo submit{};
submit.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO;
submit.waitSemaphoreCount = 1U;
submit.pWaitSemaphores = &image_available_[current_frame_];
submit.pWaitDstStageMask = &wait_stage;
submit.commandBufferCount = 1U;
submit.pCommandBuffers = &cmd;
submit.signalSemaphoreCount = 1U;
submit.pSignalSemaphores = &render_finished_[current_frame_];
if (!detail::check(vkQueueSubmit(queue_, 1U, &submit, in_flight_[current_frame_]), "vkQueueSubmit")) return false;
VkPresentInfoKHR present{};
present.sType = VK_STRUCTURE_TYPE_PRESENT_INFO_KHR;
present.waitSemaphoreCount = 1U;
present.pWaitSemaphores = &render_finished_[current_frame_];
present.swapchainCount = 1U;
present.pSwapchains = &swapchain_;
present.pImageIndices = &image_index;
const auto result = vkQueuePresentKHR(queue_, &present);
if (result == VK_ERROR_OUT_OF_DATE_KHR || result == VK_SUBOPTIMAL_KHR) {
if (!this->recreate_swapchain()) return false;
} else if (result != VK_SUCCESS) {
return false;
}
current_frame_ = (current_frame_ + 1U) % frames_in_flight;
return true;
}
auto destroy_texture() -> void {
if (device_ == VK_NULL_HANDLE) {
sampler_ = VK_NULL_HANDLE;
texture_view_ = VK_NULL_HANDLE;
texture_ = VK_NULL_HANDLE;
texture_memory_ = VK_NULL_HANDLE;
texture_width_ = 0;
texture_height_ = 0;
return;
}
if (sampler_ != VK_NULL_HANDLE) vkDestroySampler(device_, sampler_, nullptr);
if (texture_view_ != VK_NULL_HANDLE) vkDestroyImageView(device_, texture_view_, nullptr);
if (texture_ != VK_NULL_HANDLE) vkDestroyImage(device_, texture_, nullptr);
if (texture_memory_ != VK_NULL_HANDLE) vkFreeMemory(device_, texture_memory_, nullptr);
sampler_ = VK_NULL_HANDLE;
texture_view_ = VK_NULL_HANDLE;
texture_ = VK_NULL_HANDLE;
texture_memory_ = VK_NULL_HANDLE;
texture_width_ = 0;
texture_height_ = 0;
}
auto cleanup() -> void {
if (device_ != VK_NULL_HANDLE) {
vkDeviceWaitIdle(device_);
this->destroy_terrain();
terrain_ready_ = false;
this->destroy_texture();
for (const auto semaphore: image_available_) vkDestroySemaphore(device_, semaphore, nullptr);
for (const auto semaphore: render_finished_) vkDestroySemaphore(device_, semaphore, nullptr);
for (const auto fence: in_flight_) vkDestroyFence(device_, fence, nullptr);
image_available_.clear();
render_finished_.clear();
in_flight_.clear();
if (descriptor_pool_ != VK_NULL_HANDLE) vkDestroyDescriptorPool(device_, descriptor_pool_, nullptr);
if (pipeline_ != VK_NULL_HANDLE) vkDestroyPipeline(device_, pipeline_, nullptr);
if (pipeline_layout_ != VK_NULL_HANDLE) vkDestroyPipelineLayout(device_, pipeline_layout_, nullptr);
if (descriptor_layout_ != VK_NULL_HANDLE) vkDestroyDescriptorSetLayout(device_, descriptor_layout_, nullptr);
if (command_pool_ != VK_NULL_HANDLE) vkDestroyCommandPool(device_, command_pool_, nullptr);
if (render_pass_ != VK_NULL_HANDLE) vkDestroyRenderPass(device_, render_pass_, nullptr);
this->cleanup_swapchain();
vkDestroyDevice(device_, nullptr);
}
if (surface_ != VK_NULL_HANDLE) vkDestroySurfaceKHR(instance_, surface_, nullptr);
if (instance_ != VK_NULL_HANDLE) vkDestroyInstance(instance_, nullptr);
if (window_ != nullptr) SDL_DestroyWindow(window_);
SDL_Quit();
device_ = VK_NULL_HANDLE;
instance_ = VK_NULL_HANDLE;
surface_ = VK_NULL_HANDLE;
window_ = nullptr;
render_pass_ = VK_NULL_HANDLE;
command_pool_ = VK_NULL_HANDLE;
descriptor_layout_ = VK_NULL_HANDLE;
pipeline_layout_ = VK_NULL_HANDLE;
pipeline_ = VK_NULL_HANDLE;
descriptor_pool_ = VK_NULL_HANDLE;
}
SDL_Window *window_ = nullptr;
VkInstance instance_ = VK_NULL_HANDLE;
VkSurfaceKHR surface_ = VK_NULL_HANDLE;
VkPhysicalDevice physical_ = VK_NULL_HANDLE;
VkDevice device_ = VK_NULL_HANDLE;
VkQueue queue_ = VK_NULL_HANDLE;
uint32_t queue_family_ = UINT32_MAX;
VkSwapchainKHR swapchain_ = VK_NULL_HANDLE;
VkFormat swapchain_format_ = VK_FORMAT_UNDEFINED;
VkExtent2D swapchain_extent_{};
std::vector<VkImage> swapchain_images_;
std::vector<VkImageView> swapchain_views_;
std::vector<VkFramebuffer> framebuffers_;
std::vector<VkImage> depth_images_;
std::vector<VkDeviceMemory> depth_memories_;
std::vector<VkImageView> depth_views_;
VkRenderPass render_pass_ = VK_NULL_HANDLE;
VkDescriptorSetLayout descriptor_layout_ = VK_NULL_HANDLE;
VkPipelineLayout pipeline_layout_ = VK_NULL_HANDLE;
VkPipeline pipeline_ = VK_NULL_HANDLE;
VkCommandPool command_pool_ = VK_NULL_HANDLE;
std::vector<VkCommandBuffer> command_buffers_;
VkDescriptorPool descriptor_pool_ = VK_NULL_HANDLE;
VkDescriptorSet descriptor_set_ = VK_NULL_HANDLE;
VkImage texture_ = VK_NULL_HANDLE;
VkDeviceMemory texture_memory_ = VK_NULL_HANDLE;
VkImageView texture_view_ = VK_NULL_HANDLE;
VkSampler sampler_ = VK_NULL_HANDLE;
ra3::core::uint32 texture_width_ = 0;
ra3::core::uint32 texture_height_ = 0;
bool terrain_ready_ = false;
gpu_image height_texture_;
gpu_image tile_texture_;
gpu_image atlas_texture_;
gpu_image overlay_label_;
gpu_image overlay_minimap_;
VkDescriptorSet overlay_label_set_ = VK_NULL_HANDLE;
VkDescriptorSet overlay_minimap_set_ = VK_NULL_HANDLE;
uint32_t overlay_label_w_ = 0U;
uint32_t overlay_label_h_ = 0U;
uint32_t overlay_minimap_w_ = 0U;
uint32_t overlay_minimap_h_ = 0U;
VkDescriptorSetLayout terrain_layout_ = VK_NULL_HANDLE;
VkPipelineLayout terrain_pipeline_layout_ = VK_NULL_HANDLE;
VkPipeline terrain_pipeline_ = VK_NULL_HANDLE;
VkDescriptorPool terrain_pool_ = VK_NULL_HANDLE;
VkDescriptorSet terrain_set_ = VK_NULL_HANDLE;
VkBuffer object_vertex_buffer_ = VK_NULL_HANDLE;
VkDeviceMemory object_vertex_memory_ = VK_NULL_HANDLE;
VkBuffer object_index_buffer_ = VK_NULL_HANDLE;
VkDeviceMemory object_index_memory_ = VK_NULL_HANDLE;
uint32_t object_index_count_ = 0U;
gpu_image object_texture_;
VkPipelineLayout object_pipeline_layout_ = VK_NULL_HANDLE;
VkPipeline object_pipeline_ = VK_NULL_HANDLE;
VkDescriptorSet object_set_ = VK_NULL_HANDLE;
bool objects_ready_ = false;
std::vector<VkSemaphore> image_available_;
std::vector<VkSemaphore> render_finished_;
std::vector<VkFence> in_flight_;
uint32_t current_frame_ = 0U;
};
}