Fry3DX 0.4: an easy 3D modeller

Shapes with live settings, shape editing, knife and hole tools, drawing
shapes onto other objects, combining objects (add / subtract / overlap),
copies, thickness, painting with stickers and layers, a Layout tab, a
Print tab with checks, packed .fry files with autosave, export to GLB,
glTF, FBX, OBJ, 3MF, STL and PLY, folders in Layers, its own crash
screen, and a website for GitHub Pages in docs/.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NMSrd5oe1GpUD7E6d825KP
This commit is contained in:
Nexoniarz 2026-10-19 21:18:35 +02:00
commit f3e8eca87b
135 changed files with 23423 additions and 0 deletions

View file

@ -0,0 +1,356 @@
#define VMA_IMPLEMENTATION
#define VMA_STATIC_VULKAN_FUNCTIONS 0
#define VMA_DYNAMIC_VULKAN_FUNCTIONS 1
#include "rhi/vulkan/vk_device.h"
#include "core/log.h"
#include "platform/window.h"
#include <SDL3/SDL_vulkan.h>
#include <VkBootstrap.h>
#include <cstdlib>
#include <cstring>
namespace keel::vk {
void check(VkResult r, const char* expr, const char* file, int line) {
if (r == VK_SUCCESS) return;
log::error("{} failed with VkResult {} ({}:{})", expr, int(r), file, line);
std::abort();
}
void transitionImage(VkCommandBuffer cmd, VkImage image, VkImageLayout from, VkImageLayout to, VkImageAspectFlags aspect) {
VkImageMemoryBarrier2 barrier{VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER_2};
// Coarse but correct for now; per-pass barriers come with the render graph.
barrier.srcStageMask = VK_PIPELINE_STAGE_2_ALL_COMMANDS_BIT;
barrier.srcAccessMask = VK_ACCESS_2_MEMORY_WRITE_BIT;
barrier.dstStageMask = VK_PIPELINE_STAGE_2_ALL_COMMANDS_BIT;
barrier.dstAccessMask = VK_ACCESS_2_MEMORY_WRITE_BIT | VK_ACCESS_2_MEMORY_READ_BIT;
barrier.oldLayout = from;
barrier.newLayout = to;
barrier.image = image;
barrier.subresourceRange = {aspect, 0, VK_REMAINING_MIP_LEVELS, 0, VK_REMAINING_ARRAY_LAYERS};
VkDependencyInfo dep{VK_STRUCTURE_TYPE_DEPENDENCY_INFO};
dep.imageMemoryBarrierCount = 1;
dep.pImageMemoryBarriers = &barrier;
vkCmdPipelineBarrier2(cmd, &dep);
}
static VKAPI_ATTR VkBool32 VKAPI_CALL debugCallback(VkDebugUtilsMessageSeverityFlagBitsEXT severity,
VkDebugUtilsMessageTypeFlagsEXT,
const VkDebugUtilsMessengerCallbackDataEXT* data, void*) {
if (severity & VK_DEBUG_UTILS_MESSAGE_SEVERITY_ERROR_BIT_EXT) log::error("[vulkan] {}", data->pMessage);
else if (severity & VK_DEBUG_UTILS_MESSAGE_SEVERITY_WARNING_BIT_EXT) log::warn("[vulkan] {}", data->pMessage);
return VK_FALSE;
}
bool Device::init(const Window& window, bool validation) {
window_ = &window;
if (volkInitialize() != VK_SUCCESS) {
log::error("No Vulkan loader found - is a Vulkan driver installed?");
return false;
}
Uint32 extCount = 0;
const char* const* sdlExts = SDL_Vulkan_GetInstanceExtensions(&extCount);
vkb::InstanceBuilder ib(vkGetInstanceProcAddr);
ib.set_app_name("Fry3DX").require_api_version(1, 3, 0).enable_extensions(extCount, sdlExts);
if (validation) ib.request_validation_layers().set_debug_callback(debugCallback);
auto inst = ib.build();
if (!inst) {
log::error("Vulkan instance: {}", inst.error().message());
return false;
}
instance_ = inst->instance;
messenger_ = inst->debug_messenger;
volkLoadInstance(instance_);
if (!SDL_Vulkan_CreateSurface(window.handle(), instance_, nullptr, &surface_)) {
log::error("SDL_Vulkan_CreateSurface: {}", SDL_GetError());
return false;
}
VkPhysicalDeviceVulkan13Features f13{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_VULKAN_1_3_FEATURES};
f13.dynamicRendering = VK_TRUE;
f13.synchronization2 = VK_TRUE;
f13.shaderDemoteToHelperInvocation = VK_TRUE; // `discard` compiles to demote in Vulkan 1.3 SPIR-V
VkPhysicalDeviceVulkan12Features f12{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_VULKAN_1_2_FEATURES};
f12.bufferDeviceAddress = VK_TRUE;
f12.descriptorIndexing = VK_TRUE;
f12.runtimeDescriptorArray = VK_TRUE;
f12.descriptorBindingPartiallyBound = VK_TRUE;
f12.descriptorBindingSampledImageUpdateAfterBind = VK_TRUE;
f12.shaderSampledImageArrayNonUniformIndexing = VK_TRUE;
VkPhysicalDeviceFeatures f10{};
f10.samplerAnisotropy = VK_TRUE;
f10.independentBlend = VK_TRUE; // (the G-buffer pass blends its attachments differently)
auto phys = vkb::PhysicalDeviceSelector(*inst)
.set_surface(surface_)
.set_minimum_version(1, 3)
.set_required_features_13(f13)
.set_required_features_12(f12)
.set_required_features(f10)
.prefer_gpu_device_type(vkb::PreferredDeviceType::discrete)
.select();
if (!phys) {
log::error("No suitable GPU (needs Vulkan 1.3): {}", phys.error().message());
return false;
}
vkb::PhysicalDevice pd = phys.value();
physical_ = pd.physical_device;
log::info("GPU: {}", pd.properties.deviceName);
// Ray queries where the GPU has them (not needed: what uses them is skipped without).
VkPhysicalDeviceAccelerationStructureFeaturesKHR asf{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_ACCELERATION_STRUCTURE_FEATURES_KHR};
asf.accelerationStructure = VK_TRUE;
VkPhysicalDeviceRayQueryFeaturesKHR rqf{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_RAY_QUERY_FEATURES_KHR};
rqf.rayQuery = VK_TRUE;
const char* rt[] = {VK_KHR_ACCELERATION_STRUCTURE_EXTENSION_NAME, VK_KHR_RAY_QUERY_EXTENSION_NAME, VK_KHR_DEFERRED_HOST_OPERATIONS_EXTENSION_NAME};
rayQuery_ = pd.enable_extensions_if_present(3, rt) && pd.enable_extension_features_if_present(asf) && pd.enable_extension_features_if_present(rqf);
log::info("Ray queries: {}", rayQuery_ ? "yes" : "no");
memoryBudget_ = pd.enable_extension_if_present(VK_EXT_MEMORY_BUDGET_EXTENSION_NAME); // (real VRAM use, for the debug overlay)
gpuName_ = pd.properties.deviceName;
VkPhysicalDeviceDriverProperties drv{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_DRIVER_PROPERTIES};
VkPhysicalDeviceProperties2 props2{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PROPERTIES_2, &drv};
vkGetPhysicalDeviceProperties2(pd.physical_device, &props2);
gpuDriver_ = std::string(drv.driverName) + (drv.driverInfo[0] ? std::string(" ") + drv.driverInfo : std::string());
auto dev = vkb::DeviceBuilder(pd).build();
if (!dev) {
log::error("Vulkan device: {}", dev.error().message());
return false;
}
device_ = dev->device;
volkLoadDevice(device_);
queue_ = dev->get_queue(vkb::QueueType::graphics).value();
queueFamily_ = dev->get_queue_index(vkb::QueueType::graphics).value();
VmaVulkanFunctions fns{};
fns.vkGetInstanceProcAddr = vkGetInstanceProcAddr;
fns.vkGetDeviceProcAddr = vkGetDeviceProcAddr;
VmaAllocatorCreateInfo aci{};
aci.flags = VMA_ALLOCATOR_CREATE_BUFFER_DEVICE_ADDRESS_BIT | (memoryBudget_ ? VMA_ALLOCATOR_CREATE_EXT_MEMORY_BUDGET_BIT : 0);
aci.vulkanApiVersion = VK_API_VERSION_1_3;
aci.instance = instance_;
aci.physicalDevice = physical_;
aci.device = device_;
aci.pVulkanFunctions = &fns;
VK_CHECK(vmaCreateAllocator(&aci, &allocator_));
for (Frame& f : frames_) {
VkCommandPoolCreateInfo pci{VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO};
pci.flags = VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT;
pci.queueFamilyIndex = queueFamily_;
VK_CHECK(vkCreateCommandPool(device_, &pci, nullptr, &f.pool));
VkCommandBufferAllocateInfo ai{VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO};
ai.commandPool = f.pool;
ai.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY;
ai.commandBufferCount = 1;
VK_CHECK(vkAllocateCommandBuffers(device_, &ai, &f.cmd));
VkSemaphoreCreateInfo sci{VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO};
VK_CHECK(vkCreateSemaphore(device_, &sci, nullptr, &f.acquired));
VkFenceCreateInfo fci{VK_STRUCTURE_TYPE_FENCE_CREATE_INFO};
fci.flags = VK_FENCE_CREATE_SIGNALED_BIT;
VK_CHECK(vkCreateFence(device_, &fci, nullptr, &f.inFlight));
}
VkCommandPoolCreateInfo pci{VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO};
pci.flags = VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT;
pci.queueFamilyIndex = queueFamily_;
VK_CHECK(vkCreateCommandPool(device_, &pci, nullptr, &immPool_));
VkCommandBufferAllocateInfo ai{VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO};
ai.commandPool = immPool_;
ai.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY;
ai.commandBufferCount = 1;
VK_CHECK(vkAllocateCommandBuffers(device_, &ai, &immCmd_));
VkFenceCreateInfo fci{VK_STRUCTURE_TYPE_FENCE_CREATE_INFO};
VK_CHECK(vkCreateFence(device_, &fci, nullptr, &immFence_));
return createSwapchain();
}
void Device::immediateSubmit(const std::function<void(VkCommandBuffer)>& record) {
VK_CHECK(vkResetCommandBuffer(immCmd_, 0));
VkCommandBufferBeginInfo bi{VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO};
bi.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT;
VK_CHECK(vkBeginCommandBuffer(immCmd_, &bi));
record(immCmd_);
VK_CHECK(vkEndCommandBuffer(immCmd_));
VkCommandBufferSubmitInfo cbi{VK_STRUCTURE_TYPE_COMMAND_BUFFER_SUBMIT_INFO};
cbi.commandBuffer = immCmd_;
VkSubmitInfo2 si{VK_STRUCTURE_TYPE_SUBMIT_INFO_2};
si.commandBufferInfoCount = 1;
si.pCommandBufferInfos = &cbi;
VK_CHECK(vkQueueSubmit2(queue_, 1, &si, immFence_));
VK_CHECK(vkWaitForFences(device_, 1, &immFence_, VK_TRUE, UINT64_MAX));
VK_CHECK(vkResetFences(device_, 1, &immFence_));
}
Buffer Device::createBuffer(VkDeviceSize size, VkBufferUsageFlags usage, bool hostVisible, bool readback) {
VkBufferCreateInfo bci{VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO};
bci.size = size;
bci.usage = usage | VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT;
VmaAllocationCreateInfo aci{};
aci.usage = VMA_MEMORY_USAGE_AUTO;
if (hostVisible)
aci.flags = (readback ? VMA_ALLOCATION_CREATE_HOST_ACCESS_RANDOM_BIT : VMA_ALLOCATION_CREATE_HOST_ACCESS_SEQUENTIAL_WRITE_BIT) | VMA_ALLOCATION_CREATE_MAPPED_BIT;
Buffer b;
VmaAllocationInfo info{};
VK_CHECK(vmaCreateBuffer(allocator_, &bci, &aci, &b.buffer, &b.alloc, &info));
b.mapped = info.pMappedData;
VkBufferDeviceAddressInfo dai{VK_STRUCTURE_TYPE_BUFFER_DEVICE_ADDRESS_INFO};
dai.buffer = b.buffer;
b.address = vkGetBufferDeviceAddress(device_, &dai);
return b;
}
void Device::destroyBuffer(Buffer& b) {
if (b.buffer) vmaDestroyBuffer(allocator_, b.buffer, b.alloc);
b = {};
}
Buffer Device::uploadBuffer(const void* data, VkDeviceSize size, VkBufferUsageFlags usage) {
Buffer staging = createBuffer(size, VK_BUFFER_USAGE_TRANSFER_SRC_BIT, true);
std::memcpy(staging.mapped, data, size);
Buffer b = createBuffer(size, usage | VK_BUFFER_USAGE_TRANSFER_DST_BIT, false);
immediateSubmit([&](VkCommandBuffer cmd) {
VkBufferCopy copy{0, 0, size};
vkCmdCopyBuffer(cmd, staging.buffer, b.buffer, 1, &copy);
});
destroyBuffer(staging);
return b;
}
bool Device::createSwapchain() {
uint32_t w, h;
window_->pixelSize(w, h);
if (w == 0 || h == 0) return false;
vkDeviceWaitIdle(device_);
VkSwapchainKHR old = swapchain_;
auto sc = vkb::SwapchainBuilder(physical_, device_, surface_)
.set_desired_format({VK_FORMAT_B8G8R8A8_UNORM, VK_COLOR_SPACE_SRGB_NONLINEAR_KHR})
.set_desired_present_mode(vsync_ ? VK_PRESENT_MODE_FIFO_KHR : VK_PRESENT_MODE_MAILBOX_KHR)
.add_fallback_present_mode(vsync_ ? VK_PRESENT_MODE_FIFO_KHR : VK_PRESENT_MODE_IMMEDIATE_KHR)
.set_desired_extent(w, h)
.add_image_usage_flags(VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT)
.set_old_swapchain(old)
.build();
if (!sc) {
log::error("Swapchain: {}", sc.error().message());
return false;
}
destroySwapchainViews();
if (old) vkDestroySwapchainKHR(device_, old, nullptr);
swapchain_ = sc->swapchain;
swapchainFormat_ = sc->image_format;
extent_ = sc->extent;
images_ = sc->get_images().value();
views_ = sc->get_image_views().value();
renderDone_.resize(images_.size());
for (VkSemaphore& s : renderDone_) {
VkSemaphoreCreateInfo sci{VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO};
VK_CHECK(vkCreateSemaphore(device_, &sci, nullptr, &s));
}
swapchainDirty_ = false;
return true;
}
void Device::destroySwapchainViews() {
for (VkImageView v : views_) vkDestroyImageView(device_, v, nullptr);
for (VkSemaphore s : renderDone_) vkDestroySemaphore(device_, s, nullptr);
views_.clear();
renderDone_.clear();
}
VkCommandBuffer Device::beginFrame() {
if (swapchainDirty_ && !createSwapchain()) return nullptr;
Frame& f = frames_[frameIndex_];
VK_CHECK(vkWaitForFences(device_, 1, &f.inFlight, VK_TRUE, UINT64_MAX));
VkResult r = vkAcquireNextImageKHR(device_, swapchain_, UINT64_MAX, f.acquired, VK_NULL_HANDLE, &imageIndex_);
if (r == VK_ERROR_OUT_OF_DATE_KHR) {
swapchainDirty_ = true;
return nullptr;
}
if (r != VK_SUBOPTIMAL_KHR) VK_CHECK(r);
VK_CHECK(vkResetFences(device_, 1, &f.inFlight));
VK_CHECK(vkResetCommandBuffer(f.cmd, 0));
VkCommandBufferBeginInfo bi{VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO};
bi.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT;
VK_CHECK(vkBeginCommandBuffer(f.cmd, &bi));
transitionImage(f.cmd, images_[imageIndex_], VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL);
return f.cmd;
}
void Device::endFrame() {
Frame& f = frames_[frameIndex_];
transitionImage(f.cmd, images_[imageIndex_], VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, VK_IMAGE_LAYOUT_PRESENT_SRC_KHR);
VK_CHECK(vkEndCommandBuffer(f.cmd));
VkSemaphoreSubmitInfo wait{VK_STRUCTURE_TYPE_SEMAPHORE_SUBMIT_INFO};
wait.semaphore = f.acquired;
wait.stageMask = VK_PIPELINE_STAGE_2_COLOR_ATTACHMENT_OUTPUT_BIT;
VkSemaphoreSubmitInfo signal{VK_STRUCTURE_TYPE_SEMAPHORE_SUBMIT_INFO};
signal.semaphore = renderDone_[imageIndex_];
signal.stageMask = VK_PIPELINE_STAGE_2_ALL_COMMANDS_BIT;
VkCommandBufferSubmitInfo cbi{VK_STRUCTURE_TYPE_COMMAND_BUFFER_SUBMIT_INFO};
cbi.commandBuffer = f.cmd;
VkSubmitInfo2 si{VK_STRUCTURE_TYPE_SUBMIT_INFO_2};
si.waitSemaphoreInfoCount = 1;
si.pWaitSemaphoreInfos = &wait;
si.commandBufferInfoCount = 1;
si.pCommandBufferInfos = &cbi;
si.signalSemaphoreInfoCount = 1;
si.pSignalSemaphoreInfos = &signal;
VK_CHECK(vkQueueSubmit2(queue_, 1, &si, f.inFlight));
VkPresentInfoKHR pi{VK_STRUCTURE_TYPE_PRESENT_INFO_KHR};
pi.waitSemaphoreCount = 1;
pi.pWaitSemaphores = &renderDone_[imageIndex_];
pi.swapchainCount = 1;
pi.pSwapchains = &swapchain_;
pi.pImageIndices = &imageIndex_;
VkResult r = vkQueuePresentKHR(queue_, &pi);
if (r == VK_ERROR_OUT_OF_DATE_KHR || r == VK_SUBOPTIMAL_KHR) swapchainDirty_ = true;
else VK_CHECK(r);
frameIndex_ = (frameIndex_ + 1) % kFramesInFlight;
}
void Device::shutdown() {
if (!device_) return;
vkDeviceWaitIdle(device_);
for (Frame& f : frames_) {
vkDestroyCommandPool(device_, f.pool, nullptr);
vkDestroySemaphore(device_, f.acquired, nullptr);
vkDestroyFence(device_, f.inFlight, nullptr);
}
vkDestroyCommandPool(device_, immPool_, nullptr);
vkDestroyFence(device_, immFence_, nullptr);
destroySwapchainViews();
vkDestroySwapchainKHR(device_, swapchain_, nullptr);
vmaDestroyAllocator(allocator_);
vkDestroyDevice(device_, nullptr);
vkDestroySurfaceKHR(instance_, surface_, nullptr);
if (messenger_) vkDestroyDebugUtilsMessengerEXT(instance_, messenger_, nullptr);
vkDestroyInstance(instance_, nullptr);
device_ = VK_NULL_HANDLE;
}
void Device::vram(uint64_t& total, uint64_t& used, uint64_t& ours) const {
total = used = ours = 0;
if (!allocator_) return;
const VkPhysicalDeviceMemoryProperties* mem = nullptr;
vmaGetMemoryProperties(allocator_, &mem);
VmaBudget budgets[VK_MAX_MEMORY_HEAPS];
vmaGetHeapBudgets(allocator_, budgets);
for (uint32_t h = 0; h < mem->memoryHeapCount; ++h) {
if (!(mem->memoryHeaps[h].flags & VK_MEMORY_HEAP_DEVICE_LOCAL_BIT)) continue;
total += mem->memoryHeaps[h].size;
used += budgets[h].usage; // (the whole process's, with VK_EXT_memory_budget; VMA's own blocks without)
ours += budgets[h].statistics.blockBytes;
}
}
} // namespace keel::vk