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.
1829 lines
102 KiB
C++
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, ®ion);
|
|
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, ®ion);
|
|
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, ®ion);
|
|
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, ®ion);
|
|
|
|
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;
|
|
};
|
|
}
|