mirror of
https://github.com/slendidev/lunar.git
synced 2025-12-13 03:49:51 +02:00
Compare commits
3 Commits
be453697f0
...
a1061c873a
| Author | SHA1 | Date | |
|---|---|---|---|
| a1061c873a | |||
| 872ce63358 | |||
| f8aa685b8a |
@@ -46,4 +46,13 @@ struct GPUMeshBuffers {
|
||||
VkDeviceAddress vertex_buffer_address;
|
||||
};
|
||||
|
||||
struct GPUSceneData {
|
||||
smath::Mat4 view;
|
||||
smath::Mat4 proj;
|
||||
smath::Mat4 viewport;
|
||||
smath::Vec4 ambient_color;
|
||||
smath::Vec4 sunlight_direction;
|
||||
smath::Vec4 sunlight_color;
|
||||
};
|
||||
|
||||
} // namespace Lunar
|
||||
|
||||
@@ -94,6 +94,9 @@ auto VulkanRenderer::immediate_submit(
|
||||
|
||||
VK_CHECK(m_logger,
|
||||
vkWaitForFences(m_vkb.dev, 1, &m_vk.imm_fence, true, 9999999999));
|
||||
|
||||
m_vk.get_current_frame().deletion_queue.flush();
|
||||
m_vk.get_current_frame().frame_descriptors.clear_pools(m_vkb.dev);
|
||||
}
|
||||
|
||||
auto VulkanRenderer::vk_init() -> void
|
||||
@@ -327,6 +330,32 @@ auto VulkanRenderer::descriptors_init() -> void
|
||||
vkDestroyDescriptorSetLayout(
|
||||
m_vkb.dev, m_vk.draw_image_descriptor_layout, nullptr);
|
||||
});
|
||||
|
||||
for (unsigned int i = 0; i < FRAME_OVERLAP; i++) {
|
||||
std::vector<DescriptorAllocatorGrowable::PoolSizeRatio> frame_sizes = {
|
||||
{ VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, 3 },
|
||||
{ VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 3 },
|
||||
{ VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, 3 },
|
||||
{ VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, 4 },
|
||||
};
|
||||
|
||||
m_vk.frames[i].frame_descriptors = DescriptorAllocatorGrowable {};
|
||||
m_vk.frames[i].frame_descriptors.init(m_vkb.dev, 1000, frame_sizes);
|
||||
|
||||
m_vk.deletion_queue.emplace([&, i]() {
|
||||
m_vk.frames[i].frame_descriptors.destroy_pools(m_vkb.dev);
|
||||
});
|
||||
}
|
||||
|
||||
m_vk.gpu_scene_data_descriptor_layout
|
||||
= DescriptorLayoutBuilder()
|
||||
.add_binding(0, VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER)
|
||||
.build(m_logger, m_vkb.dev,
|
||||
VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT);
|
||||
m_vk.deletion_queue.emplace([&]() {
|
||||
vkDestroyDescriptorSetLayout(
|
||||
m_vkb.dev, m_vk.gpu_scene_data_descriptor_layout, nullptr);
|
||||
});
|
||||
}
|
||||
|
||||
auto VulkanRenderer::pipelines_init() -> void
|
||||
@@ -603,6 +632,59 @@ auto VulkanRenderer::default_data_init() -> void
|
||||
destroy_buffer(m_vk.rectangle.index_buffer);
|
||||
destroy_buffer(m_vk.rectangle.vertex_buffer);
|
||||
});
|
||||
|
||||
{
|
||||
// Solid color images
|
||||
auto const white = smath::pack_unorm4x8(smath::Vec4 { 1, 1, 1, 1 });
|
||||
m_vk.white_image = create_image(&white, VkExtent3D { 1, 1, 1 },
|
||||
VK_FORMAT_R8G8B8A8_UNORM, VK_IMAGE_USAGE_SAMPLED_BIT);
|
||||
|
||||
auto const black = smath::pack_unorm4x8(smath::Vec4 { 0, 0, 0, 1 });
|
||||
m_vk.black_image = create_image(&black, VkExtent3D { 1, 1, 1 },
|
||||
VK_FORMAT_R8G8B8A8_UNORM, VK_IMAGE_USAGE_SAMPLED_BIT);
|
||||
|
||||
auto const gray
|
||||
= smath::pack_unorm4x8(smath::Vec4 { 0.6f, 0.6f, 0.6f, 1 });
|
||||
m_vk.gray_image = create_image(&gray, VkExtent3D { 1, 1, 1 },
|
||||
VK_FORMAT_R8G8B8A8_UNORM, VK_IMAGE_USAGE_SAMPLED_BIT);
|
||||
|
||||
// Error checkerboard image
|
||||
auto const magenta = smath::pack_unorm4x8(smath::Vec4 { 1, 0, 1, 1 });
|
||||
std::array<uint32_t, 16 * 16> checkerboard;
|
||||
for (int x = 0; x < 16; x++) {
|
||||
for (int y = 0; y < 16; y++) {
|
||||
checkerboard[y * 16 + x]
|
||||
= ((x % 2) ^ (y % 2)) ? magenta : black;
|
||||
}
|
||||
}
|
||||
m_vk.error_image
|
||||
= create_image(checkerboard.data(), VkExtent3D { 16, 16, 1 },
|
||||
VK_FORMAT_R8G8B8A8_UNORM, VK_IMAGE_USAGE_SAMPLED_BIT);
|
||||
}
|
||||
|
||||
VkSamplerCreateInfo sampler_ci {};
|
||||
sampler_ci.sType = VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO;
|
||||
sampler_ci.pNext = nullptr;
|
||||
|
||||
sampler_ci.magFilter = VK_FILTER_NEAREST;
|
||||
sampler_ci.minFilter = VK_FILTER_NEAREST;
|
||||
vkCreateSampler(
|
||||
m_vkb.dev, &sampler_ci, nullptr, &m_vk.default_sampler_nearest);
|
||||
|
||||
sampler_ci.magFilter = VK_FILTER_LINEAR;
|
||||
sampler_ci.minFilter = VK_FILTER_LINEAR;
|
||||
vkCreateSampler(
|
||||
m_vkb.dev, &sampler_ci, nullptr, &m_vk.default_sampler_linear);
|
||||
|
||||
m_vk.deletion_queue.emplace([&]() {
|
||||
vkDestroySampler(m_vkb.dev, m_vk.default_sampler_linear, nullptr);
|
||||
vkDestroySampler(m_vkb.dev, m_vk.default_sampler_nearest, nullptr);
|
||||
|
||||
destroy_image(m_vk.error_image);
|
||||
destroy_image(m_vk.gray_image);
|
||||
destroy_image(m_vk.black_image);
|
||||
destroy_image(m_vk.white_image);
|
||||
});
|
||||
}
|
||||
|
||||
auto VulkanRenderer::render() -> void
|
||||
@@ -737,6 +819,40 @@ auto VulkanRenderer::draw_background(VkCommandBuffer cmd) -> void
|
||||
|
||||
auto VulkanRenderer::draw_geometry(VkCommandBuffer cmd) -> void
|
||||
{
|
||||
auto gpu_scene_data_buffer { create_buffer(sizeof(GPUSceneData),
|
||||
VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT, VMA_MEMORY_USAGE_CPU_TO_GPU) };
|
||||
m_vk.get_current_frame().deletion_queue.emplace(
|
||||
[=, this]() { destroy_buffer(gpu_scene_data_buffer); });
|
||||
|
||||
VmaAllocationInfo info {};
|
||||
vmaGetAllocationInfo(
|
||||
m_vk.allocator, gpu_scene_data_buffer.allocation, &info);
|
||||
|
||||
GPUSceneData *scene_uniform_data
|
||||
= reinterpret_cast<GPUSceneData *>(info.pMappedData);
|
||||
if (!scene_uniform_data) {
|
||||
VkResult res = vmaMapMemory(m_vk.allocator,
|
||||
gpu_scene_data_buffer.allocation, (void **)&scene_uniform_data);
|
||||
assert(res == VK_SUCCESS);
|
||||
}
|
||||
defer({
|
||||
if (info.pMappedData == nullptr) {
|
||||
vmaUnmapMemory(m_vk.allocator, gpu_scene_data_buffer.allocation);
|
||||
}
|
||||
});
|
||||
|
||||
*scene_uniform_data = m_vk.scene_data;
|
||||
|
||||
auto const global_desc {
|
||||
m_vk.get_current_frame().frame_descriptors.allocate(
|
||||
m_logger, m_vkb.dev, m_vk.gpu_scene_data_descriptor_layout)
|
||||
};
|
||||
|
||||
DescriptorWriter writer;
|
||||
writer.write_buffer(0, gpu_scene_data_buffer.buffer, sizeof(GPUSceneData),
|
||||
0, VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER);
|
||||
writer.update_set(m_vkb.dev, global_desc);
|
||||
|
||||
auto color_att { vkinit::attachment_info(m_vk.draw_image.image_view,
|
||||
nullptr, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL) };
|
||||
auto depth_att { vkinit::depth_attachment_info(m_vk.depth_image.image_view,
|
||||
@@ -776,8 +892,6 @@ auto VulkanRenderer::draw_geometry(VkCommandBuffer cmd) -> void
|
||||
scissor.extent = m_vk.draw_extent;
|
||||
vkCmdSetScissor(cmd, 0, 1, &scissor);
|
||||
|
||||
// vkCmdDraw(cmd, 3, 1, 0, 0);
|
||||
|
||||
vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, m_vk.mesh_pipeline);
|
||||
|
||||
GPUDrawPushConstants push_constants;
|
||||
@@ -868,60 +982,22 @@ auto VulkanRenderer::create_draw_image(uint32_t width, uint32_t height) -> void
|
||||
{
|
||||
destroy_draw_image();
|
||||
|
||||
m_vk.draw_image.format = VK_FORMAT_R16G16B16A16_SFLOAT;
|
||||
m_vk.draw_image.extent = {
|
||||
width,
|
||||
height,
|
||||
1,
|
||||
};
|
||||
|
||||
VkImageCreateInfo rimg_ci { vkinit::image_create_info(
|
||||
m_vk.draw_image.format,
|
||||
VK_IMAGE_USAGE_TRANSFER_SRC_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT
|
||||
| VK_IMAGE_USAGE_STORAGE_BIT | VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT,
|
||||
m_vk.draw_image.extent) };
|
||||
VmaAllocationCreateInfo rimg_alloci {};
|
||||
rimg_alloci.usage = VMA_MEMORY_USAGE_GPU_ONLY;
|
||||
rimg_alloci.requiredFlags
|
||||
= VkMemoryPropertyFlags(VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT);
|
||||
|
||||
vmaCreateImage(m_vk.allocator, &rimg_ci, &rimg_alloci,
|
||||
&m_vk.draw_image.image, &m_vk.draw_image.allocation, nullptr);
|
||||
|
||||
VkImageViewCreateInfo rview_ci
|
||||
= vkinit::imageview_create_info(m_vk.draw_image.format,
|
||||
m_vk.draw_image.image, VK_IMAGE_ASPECT_COLOR_BIT);
|
||||
VK_CHECK(m_logger,
|
||||
vkCreateImageView(
|
||||
m_vkb.dev, &rview_ci, nullptr, &m_vk.draw_image.image_view));
|
||||
auto const flags { VK_IMAGE_USAGE_TRANSFER_SRC_BIT
|
||||
| VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_STORAGE_BIT
|
||||
| VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT };
|
||||
m_vk.draw_image = create_image(
|
||||
{ width, height, 1 }, VK_FORMAT_R16G16B16A16_SFLOAT, flags);
|
||||
}
|
||||
|
||||
auto VulkanRenderer::create_depth_image(uint32_t width, uint32_t height) -> void
|
||||
{
|
||||
destroy_depth_image();
|
||||
|
||||
m_vk.depth_image.format = VK_FORMAT_D32_SFLOAT;
|
||||
m_vk.depth_image.extent = { width, height, 1 };
|
||||
|
||||
VkImageCreateInfo rimg_ci { vkinit::image_create_info(
|
||||
m_vk.depth_image.format,
|
||||
VK_IMAGE_USAGE_TRANSFER_SRC_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT
|
||||
| VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT,
|
||||
m_vk.depth_image.extent) };
|
||||
VmaAllocationCreateInfo rimg_alloci {};
|
||||
rimg_alloci.usage = VMA_MEMORY_USAGE_GPU_ONLY;
|
||||
rimg_alloci.requiredFlags
|
||||
= VkMemoryPropertyFlags(VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT);
|
||||
|
||||
vmaCreateImage(m_vk.allocator, &rimg_ci, &rimg_alloci,
|
||||
&m_vk.depth_image.image, &m_vk.depth_image.allocation, nullptr);
|
||||
|
||||
VkImageViewCreateInfo rview_ci
|
||||
= vkinit::imageview_create_info(m_vk.depth_image.format,
|
||||
m_vk.depth_image.image, VK_IMAGE_ASPECT_DEPTH_BIT);
|
||||
VK_CHECK(m_logger,
|
||||
vkCreateImageView(
|
||||
m_vkb.dev, &rview_ci, nullptr, &m_vk.depth_image.image_view));
|
||||
auto const flags { VK_IMAGE_USAGE_TRANSFER_SRC_BIT
|
||||
| VK_IMAGE_USAGE_TRANSFER_DST_BIT
|
||||
| VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT };
|
||||
m_vk.depth_image
|
||||
= create_image({ width, height, 1 }, VK_FORMAT_D32_SFLOAT, flags);
|
||||
}
|
||||
|
||||
auto VulkanRenderer::destroy_depth_image() -> void
|
||||
@@ -1004,6 +1080,112 @@ auto VulkanRenderer::destroy_swapchain() -> void
|
||||
m_vk.swapchain_extent = { 0, 0 };
|
||||
}
|
||||
|
||||
auto VulkanRenderer::create_image(VkExtent3D size, VkFormat format,
|
||||
VkImageUsageFlags flags, bool mipmapped) -> AllocatedImage
|
||||
{
|
||||
AllocatedImage new_image;
|
||||
new_image.format = format;
|
||||
new_image.extent = size;
|
||||
|
||||
auto img_ci { vkinit::image_create_info(format, flags, size) };
|
||||
if (mipmapped) {
|
||||
img_ci.mipLevels = static_cast<uint32_t>(std::floor(
|
||||
std::log2(std::max(size.width, size.height))))
|
||||
+ 1;
|
||||
}
|
||||
|
||||
VmaAllocationCreateInfo alloc_ci {};
|
||||
alloc_ci.usage = VMA_MEMORY_USAGE_GPU_ONLY;
|
||||
alloc_ci.requiredFlags
|
||||
= VkMemoryPropertyFlags(VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT);
|
||||
|
||||
VK_CHECK(m_logger,
|
||||
vmaCreateImage(m_vk.allocator, &img_ci, &alloc_ci, &new_image.image,
|
||||
&new_image.allocation, nullptr));
|
||||
|
||||
VkImageAspectFlags aspect_flag { VK_IMAGE_ASPECT_COLOR_BIT };
|
||||
if (format == VK_FORMAT_D32_SFLOAT) {
|
||||
aspect_flag = VK_IMAGE_ASPECT_DEPTH_BIT;
|
||||
}
|
||||
|
||||
auto const view_ci { vkinit::imageview_create_info(
|
||||
format, new_image.image, aspect_flag) };
|
||||
VK_CHECK(m_logger,
|
||||
vkCreateImageView(m_vkb.dev, &view_ci, nullptr, &new_image.image_view));
|
||||
|
||||
return new_image;
|
||||
}
|
||||
|
||||
auto VulkanRenderer::create_image(void const *data, VkExtent3D size,
|
||||
VkFormat format, VkImageUsageFlags flags, bool mipmapped) -> AllocatedImage
|
||||
{
|
||||
size_t data_size {
|
||||
static_cast<uint32_t>(size.depth) * static_cast<uint32_t>(size.width)
|
||||
* static_cast<uint32_t>(size.height) * 4,
|
||||
};
|
||||
auto const upload_buffer {
|
||||
create_buffer(data_size, VK_BUFFER_USAGE_TRANSFER_SRC_BIT,
|
||||
VMA_MEMORY_USAGE_CPU_TO_GPU),
|
||||
};
|
||||
|
||||
VmaAllocationInfo info {};
|
||||
vmaGetAllocationInfo(m_vk.allocator, upload_buffer.allocation, &info);
|
||||
|
||||
void *mapped_data { reinterpret_cast<GPUSceneData *>(info.pMappedData) };
|
||||
bool mapped_here { false };
|
||||
if (!mapped_data) {
|
||||
VkResult res = vmaMapMemory(
|
||||
m_vk.allocator, upload_buffer.allocation, (void **)&mapped_data);
|
||||
assert(res == VK_SUCCESS);
|
||||
mapped_here = true;
|
||||
}
|
||||
|
||||
memcpy(mapped_data, data, data_size);
|
||||
|
||||
auto const new_image {
|
||||
create_image(size, format,
|
||||
flags | VK_IMAGE_USAGE_TRANSFER_DST_BIT
|
||||
| VK_IMAGE_USAGE_TRANSFER_SRC_BIT,
|
||||
mipmapped),
|
||||
};
|
||||
|
||||
immediate_submit([&](VkCommandBuffer cmd) {
|
||||
vkutil::transition_image(cmd, new_image.image,
|
||||
VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL);
|
||||
|
||||
VkBufferImageCopy copy_region {};
|
||||
copy_region.bufferOffset = 0;
|
||||
copy_region.bufferRowLength = 0;
|
||||
copy_region.bufferImageHeight = 0;
|
||||
|
||||
copy_region.imageSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
|
||||
copy_region.imageSubresource.mipLevel = 0;
|
||||
copy_region.imageSubresource.baseArrayLayer = 0;
|
||||
copy_region.imageSubresource.layerCount = 1;
|
||||
copy_region.imageExtent = size;
|
||||
|
||||
vkCmdCopyBufferToImage(cmd, upload_buffer.buffer, new_image.image,
|
||||
VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, ©_region);
|
||||
|
||||
vkutil::transition_image(cmd, new_image.image,
|
||||
VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
|
||||
VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL);
|
||||
});
|
||||
|
||||
if (mapped_here) {
|
||||
vmaUnmapMemory(m_vk.allocator, upload_buffer.allocation);
|
||||
}
|
||||
destroy_buffer(upload_buffer);
|
||||
|
||||
return new_image;
|
||||
}
|
||||
|
||||
auto VulkanRenderer::destroy_image(AllocatedImage const &img) -> void
|
||||
{
|
||||
vkDestroyImageView(m_vkb.dev, img.image_view, nullptr);
|
||||
vmaDestroyImage(m_vk.allocator, img.image, img.allocation);
|
||||
}
|
||||
|
||||
auto VulkanRenderer::create_buffer(size_t alloc_size, VkBufferUsageFlags usage,
|
||||
VmaMemoryUsage memory_usage) -> AllocatedBuffer
|
||||
{
|
||||
@@ -1031,7 +1213,7 @@ auto VulkanRenderer::create_buffer(size_t alloc_size, VkBufferUsageFlags usage,
|
||||
return buffer;
|
||||
}
|
||||
|
||||
auto VulkanRenderer::destroy_buffer(AllocatedBuffer &buffer) -> void
|
||||
auto VulkanRenderer::destroy_buffer(AllocatedBuffer const &buffer) -> void
|
||||
{
|
||||
vmaDestroyBuffer(m_vk.allocator, buffer.buffer, buffer.allocation);
|
||||
}
|
||||
@@ -1067,15 +1249,12 @@ auto VulkanRenderer::upload_mesh(
|
||||
vmaGetAllocationInfo(m_vk.allocator, staging.allocation, &info);
|
||||
|
||||
void *data = info.pMappedData;
|
||||
bool mapped_here { false };
|
||||
if (!data) {
|
||||
VkResult res = vmaMapMemory(m_vk.allocator, staging.allocation, &data);
|
||||
assert(res == VK_SUCCESS);
|
||||
mapped_here = true;
|
||||
}
|
||||
defer({
|
||||
if (info.pMappedData == nullptr) {
|
||||
vmaUnmapMemory(m_vk.allocator, staging.allocation);
|
||||
}
|
||||
});
|
||||
|
||||
memcpy(data, vertices.data(), vertex_buffer_size);
|
||||
memcpy(reinterpret_cast<void *>(
|
||||
@@ -1100,6 +1279,9 @@ auto VulkanRenderer::upload_mesh(
|
||||
&index_copy);
|
||||
});
|
||||
|
||||
if (mapped_here) {
|
||||
vmaUnmapMemory(m_vk.allocator, staging.allocation);
|
||||
}
|
||||
destroy_buffer(staging);
|
||||
|
||||
return new_surface;
|
||||
|
||||
@@ -63,10 +63,15 @@ private:
|
||||
auto destroy_depth_image() -> void;
|
||||
auto recreate_swapchain(uint32_t width, uint32_t height) -> void;
|
||||
auto destroy_swapchain() -> void;
|
||||
auto create_image(VkExtent3D size, VkFormat format, VkImageUsageFlags flags,
|
||||
bool mipmapped = false) -> AllocatedImage;
|
||||
auto create_image(void const *data, VkExtent3D size, VkFormat format,
|
||||
VkImageUsageFlags flags, bool mipmapped = false) -> AllocatedImage;
|
||||
auto destroy_image(AllocatedImage const &img) -> void;
|
||||
|
||||
auto create_buffer(size_t alloc_size, VkBufferUsageFlags usage,
|
||||
VmaMemoryUsage memory_usage) -> AllocatedBuffer;
|
||||
auto destroy_buffer(AllocatedBuffer &buffer) -> void;
|
||||
auto destroy_buffer(AllocatedBuffer const &buffer) -> void;
|
||||
|
||||
struct {
|
||||
vkb::Instance instance;
|
||||
@@ -104,6 +109,9 @@ private:
|
||||
VkDescriptorSet draw_image_descriptors;
|
||||
VkDescriptorSetLayout draw_image_descriptor_layout;
|
||||
|
||||
GPUSceneData scene_data {};
|
||||
VkDescriptorSetLayout gpu_scene_data_descriptor_layout;
|
||||
|
||||
VkPipeline gradient_pipeline {};
|
||||
VkPipelineLayout gradient_pipeline_layout {};
|
||||
|
||||
@@ -126,6 +134,14 @@ private:
|
||||
uint64_t frame_number { 0 };
|
||||
|
||||
std::vector<std::shared_ptr<Mesh>> test_meshes;
|
||||
|
||||
AllocatedImage white_image {};
|
||||
AllocatedImage black_image {};
|
||||
AllocatedImage gray_image {};
|
||||
AllocatedImage error_image {};
|
||||
|
||||
VkSampler default_sampler_linear;
|
||||
VkSampler default_sampler_nearest;
|
||||
} m_vk;
|
||||
|
||||
SDL_Window *m_window { nullptr };
|
||||
|
||||
2
thirdparty/smath
vendored
2
thirdparty/smath
vendored
Submodule thirdparty/smath updated: a5d669235e...1a42238a41
Reference in New Issue
Block a user