#include "shader_contract.hpp" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace faset::render { namespace { void check(VkResult result, const char* action) { if (result != VK_SUCCESS) throw std::runtime_error(std::string(action) + " failed (Vulkan " + std::to_string(result) + ")"); } struct GpuVertex { float clip[4], world[3], normal[3], color[4], material[2], uv[2]; }; struct Push { Mat4 light_view_projection; std::array light_direction, eye; }; static_assert(sizeof(Push) == 96, "Slang FrameParameters layout"); std::array point(const Mat4& m, std::array p) { std::array o{}; for (int r = 0; r < 4; ++r) for (int c = 0; c < 4; ++c) o[r] += m[c * 4 + r] * p[c]; return o; } struct Buffer { VkBuffer handle{}; VkDeviceMemory memory{}; VkDeviceSize size{}, allocation_size{}; }; struct Image { VkImage handle{}; VkDeviceMemory memory{}; VkImageView view{}; VkImageLayout layout{VK_IMAGE_LAYOUT_UNDEFINED}; VkDeviceSize allocation_size{}; }; struct Batch { std::uint32_t first{}, count{}; const Texture* texture{}; std::array clip_rect{}; }; constexpr std::uint32_t shadow_size = 1024; } // namespace struct Renderer::Impl { RendererConfig config; SDL_Window* window{}; std::string offscreen_clipboard; bool sdl{}, close{}, dirty_swapchain{}; std::uint32_t width{}, height{}; VkInstance instance{}; VkDebugUtilsMessengerEXT messenger{}; VkSurfaceKHR surface{}; VkPhysicalDevice physical{}; VkDevice device{}; VkQueue queue{}; std::uint32_t queue_family{}; VkCommandPool pool{}; VkCommandBuffer command{}; VkFence fence{}; VkQueryPool timestamp_pool{}; float timestamp_period{}; std::uint32_t timestamp_bits{}; VkSemaphore acquired{}, present_ready{}; std::array shader_layouts{}; VkSwapchainKHR swapchain{}; VkFormat swap_format{}; VkExtent2D swap_extent{}; std::vector swap_images; std::vector swap_layouts; Image color, depth, shadow; Buffer vertices, readback; VkDescriptorSetLayout descriptor_layout{}; VkDescriptorPool descriptor_pool{}; VkSampler shadow_sampler{}, color_sampler{}; VkPipelineLayout pipeline_layout{}; VkPipeline pipeline{}, ui_pipeline{}, shadow_pipeline{}, sprite_pipeline{}; PFN_vkCmdBeginDebugUtilsLabelEXT begin_gpu_label{}; PFN_vkCmdEndDebugUtilsLabelEXT end_gpu_label{}; struct GpuTexture { Image image; VkDescriptorSet descriptor{}; std::shared_ptr source; std::uint64_t revision{}; }; std::unordered_map textures; std::shared_ptr white; std::vector last_pixels; FrameStats statistics; std::atomic validation_errors{}; ~Impl() { cleanup(); } static VKAPI_ATTR VkBool32 VKAPI_CALL debug(VkDebugUtilsMessageSeverityFlagBitsEXT severity, VkDebugUtilsMessageTypeFlagsEXT, const VkDebugUtilsMessengerCallbackDataEXT* data, void* user) { if (severity >= VK_DEBUG_UTILS_MESSAGE_SEVERITY_ERROR_BIT_EXT) static_cast(user)->validation_errors.fetch_add(1); if (severity >= VK_DEBUG_UTILS_MESSAGE_SEVERITY_WARNING_BIT_EXT) std::cerr << "[Vulkan] " << data->pMessage << '\n'; return VK_FALSE; } void destroy(Buffer& b) { if (device) { if (b.handle) vkDestroyBuffer(device, b.handle, nullptr); if (b.memory) vkFreeMemory(device, b.memory, nullptr); } b = {}; } void destroy(Image& i) { if (device) { if (i.view) vkDestroyImageView(device, i.view, nullptr); if (i.handle) vkDestroyImage(device, i.handle, nullptr); if (i.memory) vkFreeMemory(device, i.memory, nullptr); } i = {}; } void cleanup() { if (device) vkDeviceWaitIdle(device); for (auto& [_, texture] : textures) destroy(texture.image); destroy(vertices); destroy(readback); destroy(color); destroy(depth); destroy(shadow); if (device) { if (pipeline) vkDestroyPipeline(device, pipeline, nullptr); if (ui_pipeline) vkDestroyPipeline(device, ui_pipeline, nullptr); if (shadow_pipeline) vkDestroyPipeline(device, shadow_pipeline, nullptr); if (sprite_pipeline) vkDestroyPipeline(device, sprite_pipeline, nullptr); if (pipeline_layout) vkDestroyPipelineLayout(device, pipeline_layout, nullptr); if (descriptor_pool) vkDestroyDescriptorPool(device, descriptor_pool, nullptr); if (descriptor_layout) vkDestroyDescriptorSetLayout(device, descriptor_layout, nullptr); if (shadow_sampler) vkDestroySampler(device, shadow_sampler, nullptr); if (color_sampler) vkDestroySampler(device, color_sampler, nullptr); if (swapchain) vkDestroySwapchainKHR(device, swapchain, nullptr); if (timestamp_pool) vkDestroyQueryPool(device, timestamp_pool, nullptr); if (acquired) vkDestroySemaphore(device, acquired, nullptr); if (present_ready) vkDestroySemaphore(device, present_ready, nullptr); if (fence) vkDestroyFence(device, fence, nullptr); if (pool) vkDestroyCommandPool(device, pool, nullptr); vkDestroyDevice(device, nullptr); } if (surface) vkDestroySurfaceKHR(instance, surface, nullptr); if (messenger) { auto fn = reinterpret_cast( vkGetInstanceProcAddr(instance, "vkDestroyDebugUtilsMessengerEXT")); if (fn) fn(instance, messenger, nullptr); } if (instance) vkDestroyInstance(instance, nullptr); if (window) SDL_DestroyWindow(window); if (sdl) SDL_QuitSubSystem(SDL_INIT_VIDEO); } std::uint32_t memory_type(std::uint32_t bits, VkMemoryPropertyFlags properties, VkMemoryPropertyFlags preferred = 0) { VkPhysicalDeviceMemoryProperties p{}; vkGetPhysicalDeviceMemoryProperties(physical, &p); if (preferred) for (std::uint32_t i = 0; i < p.memoryTypeCount; ++i) if ((bits & (1u << i)) && (p.memoryTypes[i].propertyFlags & (properties | preferred)) == (properties | preferred)) return i; for (std::uint32_t i = 0; i < p.memoryTypeCount; ++i) if ((bits & (1u << i)) && (p.memoryTypes[i].propertyFlags & properties) == properties) return i; throw std::runtime_error("Required Vulkan memory type is unavailable"); } Buffer make_buffer(VkDeviceSize bytes, VkBufferUsageFlags usage, VkMemoryPropertyFlags properties, VkMemoryPropertyFlags preferred = 0) { Buffer b{}; b.size = bytes; VkBufferCreateInfo info{}; info.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO; info.size = bytes; info.usage = usage; info.sharingMode = VK_SHARING_MODE_EXCLUSIVE; check(vkCreateBuffer(device, &info, nullptr, &b.handle), "Create buffer"); try { VkMemoryRequirements req{}; vkGetBufferMemoryRequirements(device, b.handle, &req); VkMemoryAllocateInfo alloc{}; alloc.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO; alloc.allocationSize = req.size; alloc.memoryTypeIndex = memory_type(req.memoryTypeBits, properties, preferred); check(vkAllocateMemory(device, &alloc, nullptr, &b.memory), "Allocate buffer memory"); b.allocation_size = req.size; check(vkBindBufferMemory(device, b.handle, b.memory, 0), "Bind buffer memory"); } catch (...) { destroy(b); throw; } return b; } Image make_image(std::uint32_t w, std::uint32_t h, VkFormat format, VkImageUsageFlags usage, VkImageAspectFlags aspect) { Image image{}; VkImageCreateInfo info{}; info.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO; info.imageType = VK_IMAGE_TYPE_2D; info.format = format; info.extent = {w, h, 1}; info.mipLevels = 1; info.arrayLayers = 1; info.samples = VK_SAMPLE_COUNT_1_BIT; info.tiling = VK_IMAGE_TILING_OPTIMAL; info.usage = usage; info.sharingMode = VK_SHARING_MODE_EXCLUSIVE; check(vkCreateImage(device, &info, nullptr, &image.handle), "Create image"); try { VkMemoryRequirements req{}; vkGetImageMemoryRequirements(device, image.handle, &req); VkMemoryAllocateInfo alloc{}; alloc.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO; alloc.allocationSize = req.size; alloc.memoryTypeIndex = memory_type(req.memoryTypeBits, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT); check(vkAllocateMemory(device, &alloc, nullptr, &image.memory), "Allocate image memory"); image.allocation_size = req.size; check(vkBindImageMemory(device, image.handle, image.memory, 0), "Bind image memory"); VkImageViewCreateInfo view{}; view.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO; view.image = image.handle; view.viewType = VK_IMAGE_VIEW_TYPE_2D; view.format = format; view.subresourceRange = {aspect, 0, 1, 0, 1}; check(vkCreateImageView(device, &view, nullptr, &image.view), "Create image view"); } catch (...) { destroy(image); throw; } return image; } void transition(VkCommandBuffer cmd, VkImage image, VkImageLayout& before, VkImageLayout after, VkImageAspectFlags aspect) { // Conservative dependencies make the first single-queue backend auditable. VkImageMemoryBarrier2 barrier{}; barrier.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER_2; barrier.srcStageMask = before == VK_IMAGE_LAYOUT_UNDEFINED ? VK_PIPELINE_STAGE_2_NONE : VK_PIPELINE_STAGE_2_ALL_COMMANDS_BIT; barrier.srcAccessMask = before == VK_IMAGE_LAYOUT_UNDEFINED ? 0 : VK_ACCESS_2_MEMORY_WRITE_BIT; barrier.dstStageMask = VK_PIPELINE_STAGE_2_ALL_COMMANDS_BIT; barrier.dstAccessMask = VK_ACCESS_2_MEMORY_READ_BIT | VK_ACCESS_2_MEMORY_WRITE_BIT; barrier.oldLayout = before; barrier.newLayout = after; barrier.srcQueueFamilyIndex = barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; barrier.image = image; barrier.subresourceRange = {aspect, 0, 1, 0, 1}; VkDependencyInfo dependency{}; dependency.sType = VK_STRUCTURE_TYPE_DEPENDENCY_INFO; dependency.imageMemoryBarrierCount = 1; dependency.pImageMemoryBarriers = &barrier; vkCmdPipelineBarrier2(cmd, &dependency); before = after; } void transition(VkCommandBuffer cmd, Image& image, VkImageLayout after, VkImageAspectFlags aspect) { transition(cmd, image.handle, image.layout, after, aspect); } void begin() { check(vkResetCommandBuffer(command, 0), "Reset command buffer"); VkCommandBufferBeginInfo info{}; info.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO; info.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT; check(vkBeginCommandBuffer(command, &info), "Begin command buffer"); } void submit(bool present = false) { check(vkEndCommandBuffer(command), "End command buffer"); check(vkResetFences(device, 1, &fence), "Reset fence"); VkCommandBufferSubmitInfo cmd{}; cmd.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_SUBMIT_INFO; cmd.commandBuffer = command; VkSubmitInfo2 info{}; info.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO_2; info.commandBufferInfoCount = 1; info.pCommandBufferInfos = &cmd; VkSemaphoreSubmitInfo wait{}, signal{}; wait.sType = signal.sType = VK_STRUCTURE_TYPE_SEMAPHORE_SUBMIT_INFO; if (present) { wait.semaphore = acquired; wait.stageMask = VK_PIPELINE_STAGE_2_TRANSFER_BIT; signal.semaphore = present_ready; signal.stageMask = VK_PIPELINE_STAGE_2_ALL_COMMANDS_BIT; info.waitSemaphoreInfoCount = 1; info.pWaitSemaphoreInfos = &wait; info.signalSemaphoreInfoCount = 1; info.pSignalSemaphoreInfos = &signal; } check(vkQueueSubmit2(queue, 1, &info, fence), "Submit frame"); check(vkWaitForFences(device, 1, &fence, VK_TRUE, UINT64_MAX), "Wait frame fence"); } void initialize(const RendererConfig& c) { config = c; width = c.width; height = c.height; if (!width || !height) throw std::invalid_argument("Renderer dimensions must be nonzero"); std::vector extensions; if (!c.headless) { if (!SDL_InitSubSystem(SDL_INIT_VIDEO)) throw std::runtime_error(SDL_GetError()); sdl = true; window = SDL_CreateWindow( c.title.c_str(), static_cast(width), static_cast(height), SDL_WINDOW_VULKAN | SDL_WINDOW_RESIZABLE | SDL_WINDOW_HIGH_PIXEL_DENSITY); if (!window) throw std::runtime_error(SDL_GetError()); Uint32 count{}; auto names = SDL_Vulkan_GetInstanceExtensions(&count); if (!names) throw std::runtime_error(SDL_GetError()); extensions.assign(names, names + count); SDL_StartTextInput(window); } std::uint32_t count{}; check(vkEnumerateInstanceLayerProperties(&count, nullptr), "Enumerate layers"); std::vector layers(count); check(vkEnumerateInstanceLayerProperties(&count, layers.data()), "Enumerate layers"); check(vkEnumerateInstanceExtensionProperties(nullptr, &count, nullptr), "Enumerate instance extensions"); std::vector instance_extensions(count); check(vkEnumerateInstanceExtensionProperties(nullptr, &count, instance_extensions.data()), "Enumerate instance extensions"); const bool debug_utils = std::any_of(instance_extensions.begin(), instance_extensions.end(), [](const auto& p) { return std::strcmp(p.extensionName, VK_EXT_DEBUG_UTILS_EXTENSION_NAME) == 0; }); bool validation = c.validation && debug_utils && std::any_of(layers.begin(), layers.end(), [](auto& p) { return std::strcmp(p.layerName, "VK_LAYER_KHRONOS_validation") == 0; }); statistics.validation_enabled = validation; if (c.validation && !validation) std::cerr << "[Faset] Vulkan validation layer/debug-utils unavailable; validation " "disabled.\n"; if (debug_utils) extensions.push_back(VK_EXT_DEBUG_UTILS_EXTENSION_NAME); VkApplicationInfo app{}; app.sType = VK_STRUCTURE_TYPE_APPLICATION_INFO; app.pApplicationName = "Faset Engine"; app.apiVersion = VK_API_VERSION_1_3; VkDebugUtilsMessengerCreateInfoEXT debug_info{}; debug_info.sType = VK_STRUCTURE_TYPE_DEBUG_UTILS_MESSENGER_CREATE_INFO_EXT; debug_info.messageSeverity = VK_DEBUG_UTILS_MESSAGE_SEVERITY_WARNING_BIT_EXT | VK_DEBUG_UTILS_MESSAGE_SEVERITY_ERROR_BIT_EXT; debug_info.messageType = VK_DEBUG_UTILS_MESSAGE_TYPE_VALIDATION_BIT_EXT | VK_DEBUG_UTILS_MESSAGE_TYPE_PERFORMANCE_BIT_EXT | VK_DEBUG_UTILS_MESSAGE_TYPE_GENERAL_BIT_EXT; debug_info.pfnUserCallback = debug; debug_info.pUserData = this; const char* validation_name = "VK_LAYER_KHRONOS_validation"; VkInstanceCreateInfo info{}; info.sType = VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO; info.pApplicationInfo = &app; info.enabledExtensionCount = static_cast(extensions.size()); info.ppEnabledExtensionNames = extensions.data(); if (validation) { info.enabledLayerCount = 1; info.ppEnabledLayerNames = &validation_name; info.pNext = &debug_info; } check(vkCreateInstance(&info, nullptr, &instance), "Create Vulkan instance"); if (validation) { auto fn = reinterpret_cast( vkGetInstanceProcAddr(instance, "vkCreateDebugUtilsMessengerEXT")); if (fn) check(fn(instance, &debug_info, nullptr, &messenger), "Create validation messenger"); } if (window && !SDL_Vulkan_CreateSurface(window, instance, nullptr, &surface)) throw std::runtime_error(SDL_GetError()); check(vkEnumeratePhysicalDevices(instance, &count, nullptr), "Enumerate GPUs"); std::vector devices(count); check(vkEnumeratePhysicalDevices(instance, &count, devices.data()), "Enumerate GPUs"); int best = -1; for (auto gpu : devices) { VkPhysicalDeviceProperties properties{}; vkGetPhysicalDeviceProperties(gpu, &properties); if (properties.apiVersion < VK_API_VERSION_1_3) continue; VkPhysicalDeviceVulkan13Features f13{}; f13.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_VULKAN_1_3_FEATURES; VkPhysicalDeviceFeatures2 features{}; features.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2; features.pNext = &f13; vkGetPhysicalDeviceFeatures2(gpu, &features); if (!f13.synchronization2 || !f13.dynamicRendering) continue; VkFormatProperties color_props{}, depth_props{}; vkGetPhysicalDeviceFormatProperties(gpu, VK_FORMAT_R8G8B8A8_UNORM, &color_props); vkGetPhysicalDeviceFormatProperties(gpu, VK_FORMAT_D32_SFLOAT, &depth_props); if (!(color_props.optimalTilingFeatures & VK_FORMAT_FEATURE_COLOR_ATTACHMENT_BIT) || !(depth_props.optimalTilingFeatures & VK_FORMAT_FEATURE_DEPTH_STENCIL_ATTACHMENT_BIT) || !(depth_props.optimalTilingFeatures & VK_FORMAT_FEATURE_SAMPLED_IMAGE_BIT)) continue; std::uint32_t n{}; vkGetPhysicalDeviceQueueFamilyProperties(gpu, &n, nullptr); std::vector queues(n); vkGetPhysicalDeviceQueueFamilyProperties(gpu, &n, queues.data()); for (std::uint32_t i = 0; i < n; ++i) { VkBool32 supports = VK_TRUE; if (surface) check(vkGetPhysicalDeviceSurfaceSupportKHR(gpu, i, surface, &supports), "Query present support"); int score = properties.deviceType == VK_PHYSICAL_DEVICE_TYPE_DISCRETE_GPU ? 3 : properties.deviceType == VK_PHYSICAL_DEVICE_TYPE_INTEGRATED_GPU ? 2 : 1; if (supports && (queues[i].queueFlags & VK_QUEUE_GRAPHICS_BIT) && score > best) { best = score; physical = gpu; queue_family = i; statistics.device = properties.deviceName; timestamp_period = properties.limits.timestampPeriod; timestamp_bits = queues[i].timestampValidBits; } } } if (!physical) throw std::runtime_error("No Vulkan 1.3 device supports dynamic rendering, " "synchronization2 and required color/depth formats"); float priority = 1; VkDeviceQueueCreateInfo qi{}; qi.sType = VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO; qi.queueFamilyIndex = queue_family; qi.queueCount = 1; qi.pQueuePriorities = &priority; VkPhysicalDeviceVulkan13Features f13{}; f13.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_VULKAN_1_3_FEATURES; f13.synchronization2 = VK_TRUE; f13.dynamicRendering = VK_TRUE; VkDeviceCreateInfo di{}; di.sType = VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO; di.pNext = &f13; di.queueCreateInfoCount = 1; di.pQueueCreateInfos = &qi; const char* swap_extension = VK_KHR_SWAPCHAIN_EXTENSION_NAME; if (surface) { di.enabledExtensionCount = 1; di.ppEnabledExtensionNames = &swap_extension; } check(vkCreateDevice(physical, &di, nullptr, &device), "Create Vulkan device"); vkGetDeviceQueue(device, queue_family, 0, &queue); if (debug_utils) { begin_gpu_label = reinterpret_cast( vkGetDeviceProcAddr(device, "vkCmdBeginDebugUtilsLabelEXT")); end_gpu_label = reinterpret_cast( vkGetDeviceProcAddr(device, "vkCmdEndDebugUtilsLabelEXT")); } statistics.gpu_labels_enabled = begin_gpu_label && end_gpu_label; VkCommandPoolCreateInfo pi{}; pi.sType = VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO; pi.queueFamilyIndex = queue_family; pi.flags = VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT; check(vkCreateCommandPool(device, &pi, nullptr, &pool), "Create command pool"); VkCommandBufferAllocateInfo ai{}; ai.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO; ai.commandPool = pool; ai.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY; ai.commandBufferCount = 1; check(vkAllocateCommandBuffers(device, &ai, &command), "Allocate command buffer"); VkFenceCreateInfo fi{}; fi.sType = VK_STRUCTURE_TYPE_FENCE_CREATE_INFO; fi.flags = VK_FENCE_CREATE_SIGNALED_BIT; check(vkCreateFence(device, &fi, nullptr, &fence), "Create frame fence"); VkSemaphoreCreateInfo si{}; si.sType = VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO; check(vkCreateSemaphore(device, &si, nullptr, &acquired), "Create acquire semaphore"); check(vkCreateSemaphore(device, &si, nullptr, &present_ready), "Create present semaphore"); if (timestamp_bits) { VkQueryPoolCreateInfo query{}; query.sType = VK_STRUCTURE_TYPE_QUERY_POOL_CREATE_INFO; query.queryType = VK_QUERY_TYPE_TIMESTAMP; query.queryCount = 2; check(vkCreateQueryPool(device, &query, nullptr, ×tamp_pool), "Create GPU timestamp queries"); } shadow = make_image(shadow_size, shadow_size, VK_FORMAT_D32_SFLOAT, VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT | VK_IMAGE_USAGE_SAMPLED_BIT, VK_IMAGE_ASPECT_DEPTH_BIT); make_targets(); make_descriptors(); make_pipelines(); white = std::make_shared(); white->width = white->height = 1; white->rgba = {255, 255, 255, 255}; upload_texture(white); if (surface) make_swapchain(); } void make_targets() { check(vkDeviceWaitIdle(device), "Wait resize"); destroy(color); destroy(depth); destroy(readback); color = make_image(width, height, VK_FORMAT_R8G8B8A8_UNORM, VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT, VK_IMAGE_ASPECT_COLOR_BIT); depth = make_image(width, height, VK_FORMAT_D32_SFLOAT, VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT, VK_IMAGE_ASPECT_DEPTH_BIT); // CPU reads this allocation every frame. Prefer cached coherent memory when available. readback = make_buffer(VkDeviceSize(width) * height * 4, VK_BUFFER_USAGE_TRANSFER_DST_BIT, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT, VK_MEMORY_PROPERTY_HOST_CACHED_BIT); last_pixels.clear(); } void make_swapchain() { if (!surface) return; int w{}, h{}; SDL_GetWindowSizeInPixels(window, &w, &h); if (w <= 0 || h <= 0) return; check(vkDeviceWaitIdle(device), "Wait swapchain"); VkSurfaceCapabilitiesKHR caps{}; check(vkGetPhysicalDeviceSurfaceCapabilitiesKHR(physical, surface, &caps), "Read surface capabilities"); std::uint32_t count{}; check(vkGetPhysicalDeviceSurfaceFormatsKHR(physical, surface, &count, nullptr), "Read surface formats"); std::vector formats(count); check(vkGetPhysicalDeviceSurfaceFormatsKHR(physical, surface, &count, formats.data()), "Read surface formats"); if (formats.empty()) throw std::runtime_error("Window surface has no formats"); auto chosen = formats.front(); for (auto f : formats) if (f.format == VK_FORMAT_B8G8R8A8_UNORM && f.colorSpace == VK_COLOR_SPACE_SRGB_NONLINEAR_KHR) chosen = f; VkFormatProperties properties{}; vkGetPhysicalDeviceFormatProperties(physical, chosen.format, &properties); if (!(caps.supportedUsageFlags & VK_IMAGE_USAGE_TRANSFER_DST_BIT) || !(properties.optimalTilingFeatures & VK_FORMAT_FEATURE_BLIT_DST_BIT)) throw std::runtime_error("Window surface does not support transfer presentation"); swap_extent = caps.currentExtent; if (swap_extent.width == UINT32_MAX) swap_extent = {std::clamp(static_cast(w), caps.minImageExtent.width, caps.maxImageExtent.width), std::clamp(static_cast(h), caps.minImageExtent.height, caps.maxImageExtent.height)}; count = caps.minImageCount + 1; if (caps.maxImageCount) count = std::min(count, caps.maxImageCount); VkSwapchainCreateInfoKHR info{}; info.sType = VK_STRUCTURE_TYPE_SWAPCHAIN_CREATE_INFO_KHR; info.surface = surface; info.minImageCount = count; info.imageFormat = chosen.format; info.imageColorSpace = chosen.colorSpace; info.imageExtent = swap_extent; info.imageArrayLayers = 1; info.imageUsage = VK_IMAGE_USAGE_TRANSFER_DST_BIT; info.imageSharingMode = VK_SHARING_MODE_EXCLUSIVE; info.preTransform = caps.currentTransform; info.compositeAlpha = VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR; if (!(caps.supportedCompositeAlpha & info.compositeAlpha)) { for (auto a : {VK_COMPOSITE_ALPHA_PRE_MULTIPLIED_BIT_KHR, VK_COMPOSITE_ALPHA_POST_MULTIPLIED_BIT_KHR, VK_COMPOSITE_ALPHA_INHERIT_BIT_KHR}) if (caps.supportedCompositeAlpha & a) { info.compositeAlpha = a; break; } } info.presentMode = VK_PRESENT_MODE_FIFO_KHR; info.clipped = VK_TRUE; info.oldSwapchain = swapchain; VkSwapchainKHR next{}; check(vkCreateSwapchainKHR(device, &info, nullptr, &next), "Create swapchain"); if (swapchain) vkDestroySwapchainKHR(device, swapchain, nullptr); swapchain = next; swap_format = chosen.format; check(vkGetSwapchainImagesKHR(device, swapchain, &count, nullptr), "Get swapchain images"); swap_images.resize(count); check(vkGetSwapchainImagesKHR(device, swapchain, &count, swap_images.data()), "Get swapchain images"); swap_layouts.assign(count, VK_IMAGE_LAYOUT_UNDEFINED); dirty_swapchain = false; if (width != swap_extent.width || height != swap_extent.height) { width = swap_extent.width; height = swap_extent.height; make_targets(); } } void make_descriptors() { std::array bindings{}; for (std::uint32_t i = 0; i < 4; ++i) { bindings[i].binding = i; bindings[i].descriptorType = i % 2 ? VK_DESCRIPTOR_TYPE_SAMPLER : VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE; bindings[i].descriptorCount = 1; bindings[i].stageFlags = VK_SHADER_STAGE_FRAGMENT_BIT; } VkDescriptorSetLayoutCreateInfo li{}; li.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO; li.bindingCount = 4; li.pBindings = bindings.data(); check(vkCreateDescriptorSetLayout(device, &li, nullptr, &descriptor_layout), "Create descriptor layout"); VkDescriptorPoolSize sizes[] = {{VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE, 2048}, {VK_DESCRIPTOR_TYPE_SAMPLER, 2048}}; VkDescriptorPoolCreateInfo pi{}; pi.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO; pi.flags = VK_DESCRIPTOR_POOL_CREATE_FREE_DESCRIPTOR_SET_BIT; pi.maxSets = 1024; pi.poolSizeCount = 2; pi.pPoolSizes = sizes; check(vkCreateDescriptorPool(device, &pi, nullptr, &descriptor_pool), "Create descriptor pool"); VkSamplerCreateInfo si{}; si.sType = VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO; si.magFilter = si.minFilter = VK_FILTER_NEAREST; si.mipmapMode = VK_SAMPLER_MIPMAP_MODE_NEAREST; si.addressModeU = si.addressModeV = si.addressModeW = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE; si.maxLod = 0; check(vkCreateSampler(device, &si, nullptr, &shadow_sampler), "Create shadow sampler"); si.magFilter = si.minFilter = VK_FILTER_LINEAR; check(vkCreateSampler(device, &si, nullptr, &color_sampler), "Create color sampler"); } VkDescriptorSet upload_texture(std::shared_ptr source) { if (!source) source = white; if (!source || !source->width || !source->height || source->rgba.size() != std::size_t(source->width) * source->height * 4) throw std::invalid_argument("Texture requires width * height * 4 RGBA bytes"); auto found = textures.find(source.get()); if (found != textures.end() && found->second.revision == source->revision) return found->second.descriptor; check(vkDeviceWaitIdle(device), "Wait texture upload"); GpuTexture texture{}; texture.source = source; texture.revision = source->revision; texture.image = make_image(source->width, source->height, source->srgb ? VK_FORMAT_R8G8B8A8_SRGB : VK_FORMAT_R8G8B8A8_UNORM, VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_SAMPLED_BIT, VK_IMAGE_ASPECT_COLOR_BIT); Buffer staging{}; try { staging = make_buffer(source->rgba.size(), VK_BUFFER_USAGE_TRANSFER_SRC_BIT, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT); void* mapped{}; check(vkMapMemory(device, staging.memory, 0, staging.size, 0, &mapped), "Map texture staging"); std::memcpy(mapped, source->rgba.data(), source->rgba.size()); vkUnmapMemory(device, staging.memory); begin(); transition(command, texture.image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_ASPECT_COLOR_BIT); VkBufferImageCopy copy{}; copy.imageSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1}; copy.imageExtent = {source->width, source->height, 1}; vkCmdCopyBufferToImage(command, staging.handle, texture.image.handle, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, ©); transition(command, texture.image, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, VK_IMAGE_ASPECT_COLOR_BIT); submit(); destroy(staging); VkDescriptorSetAllocateInfo ai{}; ai.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO; ai.descriptorPool = descriptor_pool; ai.descriptorSetCount = 1; ai.pSetLayouts = &descriptor_layout; check(vkAllocateDescriptorSets(device, &ai, &texture.descriptor), "Allocate texture descriptor"); VkDescriptorImageInfo images[] = { {VK_NULL_HANDLE, shadow.view, VK_IMAGE_LAYOUT_DEPTH_READ_ONLY_OPTIMAL}, {shadow_sampler, VK_NULL_HANDLE, VK_IMAGE_LAYOUT_UNDEFINED}, {VK_NULL_HANDLE, texture.image.view, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL}, {color_sampler, VK_NULL_HANDLE, VK_IMAGE_LAYOUT_UNDEFINED}}; std::array writes{}; for (std::uint32_t i = 0; i < 4; ++i) { writes[i].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; writes[i].dstSet = texture.descriptor; writes[i].dstBinding = i; writes[i].descriptorCount = 1; writes[i].descriptorType = i % 2 ? VK_DESCRIPTOR_TYPE_SAMPLER : VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE; writes[i].pImageInfo = &images[i]; } vkUpdateDescriptorSets(device, 4, writes.data(), 0, nullptr); } catch (...) { destroy(staging); destroy(texture.image); throw; } if (found != textures.end()) { destroy(found->second.image); vkFreeDescriptorSets(device, descriptor_pool, 1, &found->second.descriptor); found->second = std::move(texture); return found->second.descriptor; } auto [inserted, _] = textures.emplace(source.get(), std::move(texture)); return inserted->second.descriptor; } std::filesystem::path shader_directory() const { if (!config.shader_directory.empty()) return config.shader_directory; std::vector roots; const char* base = SDL_GetBasePath(); if (base) roots.emplace_back(faset::path_from_utf8(base) / "shaders"); roots.emplace_back(std::filesystem::current_path() / "shaders"); roots.emplace_back(faset::path_from_utf8(FASET_SHADER_DIRECTORY)); for (const auto& root : roots) if (std::filesystem::is_regular_file(faset::native_io_path(root / "vertexMain.spv"))) return root; throw std::runtime_error("Compiled Slang shader bundle is missing"); } VkShaderModule shader(const detail::ShaderCode& code) { VkShaderModuleCreateInfo ci{}; ci.sType = VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO; ci.codeSize = code.words.size() * sizeof(std::uint32_t); ci.pCode = code.words.data(); VkShaderModule result{}; check(vkCreateShaderModule(device, &ci, nullptr, &result), "Create shader module"); return result; } void make_pipelines() { const auto shaders = detail::load_shader_bundle(shader_directory()); for (std::size_t i = 0; i < shaders.size(); ++i) if (!shader_layouts[i].empty() && shader_layouts[i] != shaders[i].layout_fingerprint) throw std::runtime_error( "Shader layout changed; the current pipeline was preserved"); VkPushConstantRange push{VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT, 0, sizeof(Push)}; VkPipelineLayoutCreateInfo li{}; li.sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO; li.setLayoutCount = 1; li.pSetLayouts = &descriptor_layout; li.pushConstantRangeCount = 1; li.pPushConstantRanges = &push; check(vkCreatePipelineLayout(device, &li, nullptr, &pipeline_layout), "Create pipeline layout"); VkShaderModule vertex{}, fragment{}, shadow_vertex{}; try { vertex = shader(shaders[0]); fragment = shader(shaders[1]); shadow_vertex = shader(shaders[2]); for (int mode = 0; mode < 4; ++mode) { bool shadow_pass = mode == 2, ui = mode == 1, sprite = mode == 3; 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 = shadow_pass ? shadow_vertex : 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{0, sizeof(GpuVertex), VK_VERTEX_INPUT_RATE_VERTEX}; VkVertexInputAttributeDescription attrs[] = { {0, 0, VK_FORMAT_R32G32B32A32_SFLOAT, offsetof(GpuVertex, clip)}, {1, 0, VK_FORMAT_R32G32B32_SFLOAT, offsetof(GpuVertex, world)}, {2, 0, VK_FORMAT_R32G32B32_SFLOAT, offsetof(GpuVertex, normal)}, {3, 0, VK_FORMAT_R32G32B32A32_SFLOAT, offsetof(GpuVertex, color)}, {4, 0, VK_FORMAT_R32G32_SFLOAT, offsetof(GpuVertex, material)}, {5, 0, VK_FORMAT_R32G32_SFLOAT, offsetof(GpuVertex, uv)}}; VkPipelineVertexInputStateCreateInfo vi{}; vi.sType = VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO; vi.vertexBindingDescriptionCount = 1; vi.pVertexBindingDescriptions = &binding; vi.vertexAttributeDescriptionCount = shadow_pass ? 1 : 6; vi.pVertexAttributeDescriptions = shadow_pass ? attrs + 1 : attrs; VkPipelineInputAssemblyStateCreateInfo ia{}; ia.sType = VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO; ia.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; VkPipelineViewportStateCreateInfo vp{}; vp.sType = VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO; vp.viewportCount = vp.scissorCount = 1; VkPipelineRasterizationStateCreateInfo rs{}; rs.sType = VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO; rs.polygonMode = VK_POLYGON_MODE_FILL; rs.cullMode = VK_CULL_MODE_NONE; rs.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE; rs.lineWidth = 1; rs.depthBiasEnable = shadow_pass; rs.depthBiasConstantFactor = 1.25f; rs.depthBiasSlopeFactor = 1.75f; VkPipelineMultisampleStateCreateInfo ms{}; ms.sType = VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO; ms.rasterizationSamples = VK_SAMPLE_COUNT_1_BIT; VkPipelineDepthStencilStateCreateInfo ds{}; ds.sType = VK_STRUCTURE_TYPE_PIPELINE_DEPTH_STENCIL_STATE_CREATE_INFO; ds.depthTestEnable = !ui; ds.depthWriteEnable = !ui && !sprite; ds.depthCompareOp = VK_COMPARE_OP_LESS_OR_EQUAL; VkPipelineColorBlendAttachmentState blend{}; blend.colorWriteMask = 15; blend.blendEnable = VK_TRUE; blend.srcColorBlendFactor = VK_BLEND_FACTOR_SRC_ALPHA; blend.dstColorBlendFactor = VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA; blend.colorBlendOp = VK_BLEND_OP_ADD; blend.srcAlphaBlendFactor = VK_BLEND_FACTOR_ONE; blend.dstAlphaBlendFactor = VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA; blend.alphaBlendOp = VK_BLEND_OP_ADD; VkPipelineColorBlendStateCreateInfo cb{}; cb.sType = VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO; cb.attachmentCount = shadow_pass ? 0 : 1; cb.pAttachments = &blend; VkDynamicState states[] = {VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR}; VkPipelineDynamicStateCreateInfo dynamic{}; dynamic.sType = VK_STRUCTURE_TYPE_PIPELINE_DYNAMIC_STATE_CREATE_INFO; dynamic.dynamicStateCount = 2; dynamic.pDynamicStates = states; VkFormat format = VK_FORMAT_R8G8B8A8_UNORM; VkPipelineRenderingCreateInfo rendering{}; rendering.sType = VK_STRUCTURE_TYPE_PIPELINE_RENDERING_CREATE_INFO; rendering.colorAttachmentCount = shadow_pass ? 0 : 1; rendering.pColorAttachmentFormats = &format; rendering.depthAttachmentFormat = VK_FORMAT_D32_SFLOAT; VkGraphicsPipelineCreateInfo pi{}; pi.sType = VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO; pi.pNext = &rendering; pi.stageCount = shadow_pass ? 1 : 2; pi.pStages = stages; pi.pVertexInputState = &vi; pi.pInputAssemblyState = &ia; pi.pViewportState = &vp; pi.pRasterizationState = &rs; pi.pMultisampleState = &ms; pi.pDepthStencilState = &ds; pi.pColorBlendState = &cb; pi.pDynamicState = &dynamic; pi.layout = pipeline_layout; auto* output = shadow_pass ? &shadow_pipeline : ui ? &ui_pipeline : sprite ? &sprite_pipeline : &pipeline; check(vkCreateGraphicsPipelines(device, VK_NULL_HANDLE, 1, &pi, nullptr, output), "Create graphics pipeline"); } } catch (...) { vkDestroyShaderModule(device, vertex, nullptr); vkDestroyShaderModule(device, fragment, nullptr); vkDestroyShaderModule(device, shadow_vertex, nullptr); throw; } for (std::size_t i = 0; i < shaders.size(); ++i) shader_layouts[i] = shaders[i].layout_fingerprint; vkDestroyShaderModule(device, vertex, nullptr); vkDestroyShaderModule(device, fragment, nullptr); vkDestroyShaderModule(device, shadow_vertex, nullptr); } GpuVertex gpu_vertex(const Vertex& v, const DrawItem& item, const Mat4& vp) { GpuVertex out{}; auto world = point(item.model, {v.position[0], v.position[1], v.position[2], 1}); auto clip = point(vp, world); std::copy(clip.begin(), clip.end(), out.clip); std::copy_n(world.begin(), 3, out.world); // Inverse-transpose 3x3, including nonuniform scale. Singular models have no valid normal. const auto& m = item.model; Vec3 a{m[0], m[1], m[2]}, b{m[4], m[5], m[6]}, c{m[8], m[9], m[10]}; auto cross = [](Vec3 x, Vec3 y) { return Vec3{x[1] * y[2] - x[2] * y[1], x[2] * y[0] - x[0] * y[2], x[0] * y[1] - x[1] * y[0]}; }; auto ca = cross(b, c), cb = cross(c, a), cc = cross(a, b); float determinant = a[0] * ca[0] + a[1] * ca[1] + a[2] * ca[2]; for (int i = 0; i < 3; ++i) out.normal[i] = determinant != 0 ? (ca[i] * v.normal[0] + cb[i] * v.normal[1] + cc[i] * v.normal[2]) * (determinant < 0 ? -1.f : 1.f) : 0; float normal_length = std::hypot(out.normal[0], out.normal[1], out.normal[2]); if (normal_length > 0) for (float& component : out.normal) component /= normal_length; for (int i = 0; i < 4; ++i) out.color[i] = item.color[i] * v.color[i]; out.material[0] = item.roughness; out.material[1] = item.metallic; out.uv[0] = v.uv[0]; out.uv[1] = v.uv[1]; return out; } bool outside(const std::vector& data, std::size_t start) const { for (int plane = 0; plane < 6; ++plane) { bool all = true; for (std::size_t i = start; i < data.size(); ++i) { auto& p = data[i].clip; float d = plane == 0 ? p[0] + p[3] : plane == 1 ? p[3] - p[0] : plane == 2 ? p[1] + p[3] : plane == 3 ? p[3] - p[1] : plane == 4 ? p[2] : p[3] - p[2]; if (d >= 0) { all = false; break; } } if (all) return true; } return false; } void quad(std::vector& data, const Quad& q) { const float xy[4][2] = {{q.x, q.y}, {q.x + q.width, q.y}, {q.x + q.width, q.y + q.height}, {q.x, q.y + q.height}}; const float uv[4][2] = {{q.uv_rect[0], q.uv_rect[1]}, {q.uv_rect[2], q.uv_rect[1]}, {q.uv_rect[2], q.uv_rect[3]}, {q.uv_rect[0], q.uv_rect[3]}}; for (auto i : {0, 1, 2, 0, 2, 3}) { GpuVertex v{}; v.clip[0] = xy[i][0] / float(width) * 2 - 1; v.clip[1] = xy[i][1] / float(height) * 2 - 1; v.clip[3] = 1; std::copy(q.color.begin(), q.color.end(), v.color); v.uv[0] = uv[i][0]; v.uv[1] = uv[i][1]; v.material[0] = q.texture && q.texture->srgb ? 1.f : 0.f; data.push_back(v); } } void draw_debug_text(std::vector& data, const Text& text) { // Small diagnostic alphabet only. The editor supplies shaped Unicode text as texture quads. static const std::unordered_map> glyphs = { {'A', {14, 17, 17, 31, 17, 17, 17}}, {'B', {30, 17, 17, 30, 17, 17, 30}}, {'C', {14, 17, 16, 16, 16, 17, 14}}, {'D', {30, 17, 17, 17, 17, 17, 30}}, {'E', {31, 16, 16, 30, 16, 16, 31}}, {'F', {31, 16, 16, 30, 16, 16, 16}}, {'G', {14, 17, 16, 23, 17, 17, 15}}, {'H', {17, 17, 17, 31, 17, 17, 17}}, {'I', {14, 4, 4, 4, 4, 4, 14}}, {'J', {7, 2, 2, 2, 18, 18, 12}}, {'K', {17, 18, 20, 24, 20, 18, 17}}, {'L', {16, 16, 16, 16, 16, 16, 31}}, {'M', {17, 27, 21, 21, 17, 17, 17}}, {'N', {17, 25, 21, 19, 17, 17, 17}}, {'O', {14, 17, 17, 17, 17, 17, 14}}, {'P', {30, 17, 17, 30, 16, 16, 16}}, {'Q', {14, 17, 17, 17, 21, 18, 13}}, {'R', {30, 17, 17, 30, 20, 18, 17}}, {'S', {15, 16, 16, 14, 1, 1, 30}}, {'T', {31, 4, 4, 4, 4, 4, 4}}, {'U', {17, 17, 17, 17, 17, 17, 14}}, {'V', {17, 17, 17, 17, 17, 10, 4}}, {'W', {17, 17, 17, 21, 21, 27, 17}}, {'X', {17, 17, 10, 4, 10, 17, 17}}, {'Y', {17, 17, 10, 4, 4, 4, 4}}, {'Z', {31, 1, 2, 4, 8, 16, 31}}, {'0', {14, 17, 19, 21, 25, 17, 14}}, {'1', {4, 12, 4, 4, 4, 4, 14}}, {'2', {14, 17, 1, 2, 4, 8, 31}}, {'3', {30, 1, 1, 14, 1, 1, 30}}, {'4', {2, 6, 10, 18, 31, 2, 2}}, {'5', {31, 16, 16, 30, 1, 1, 30}}, {'6', {14, 16, 16, 30, 17, 17, 14}}, {'7', {31, 1, 2, 4, 8, 8, 8}}, {'8', {14, 17, 17, 14, 17, 17, 14}}, {'9', {14, 17, 17, 15, 1, 1, 14}}, {'.', {0, 0, 0, 0, 0, 12, 12}}, {':', {0, 12, 12, 0, 12, 12, 0}}, {'-', {0, 0, 0, 31, 0, 0, 0}}, {'/', {1, 1, 2, 4, 8, 16, 16}}, {'_', {0, 0, 0, 0, 0, 0, 31}}, {'(', {2, 4, 8, 8, 8, 4, 2}}, {')', {8, 4, 2, 2, 2, 4, 8}}, {'+', {0, 4, 4, 31, 4, 4, 0}}, {'=', {0, 0, 31, 0, 31, 0, 0}}, {'[', {14, 8, 8, 8, 8, 8, 14}}, {']', {14, 2, 2, 2, 2, 2, 14}}, {'?', {14, 17, 1, 2, 4, 0, 4}}, {'!', {4, 4, 4, 4, 4, 0, 4}}}; float x = text.x, y = text.y, unit = text.size / 7; for (unsigned char c : text.value) { if (c == '\n') { x = text.x; y += text.size * 1.4f; continue; } if (c >= 'a' && c <= 'z') c -= 32; if (c != ' ') { auto it = glyphs.find(static_cast(c)); auto pattern = it == glyphs.end() ? std::array{31, 17, 17, 17, 17, 17, 31} : it->second; for (int row = 0; row < 7; ++row) for (int col = 0; col < 5; ++col) if (pattern[row] & (1 << (4 - col))) quad(data, {x + col * unit, y + row * unit, unit, unit, text.color}); } x += 6 * unit; } } void render(const Snapshot& snapshot) { auto start = std::chrono::steady_clock::now(); statistics.draw_calls = statistics.culled_meshes = statistics.gpu_label_count = 0; bool can_present = surface != VK_NULL_HANDLE; if (surface) { // A capture may render between normal event-loop iterations. Keep the window // system progressing without consuming events intended for the editor. SDL_PumpEvents(); int w{}, h{}; SDL_GetWindowSizeInPixels(window, &w, &h); can_present = w > 0 && h > 0 && !(SDL_GetWindowFlags(window) & (SDL_WINDOW_HIDDEN | SDL_WINDOW_MINIMIZED)); if (can_present && (dirty_swapchain || !swapchain)) make_swapchain(); } // Retire atlas/image resources no longer retained by a caller. for (auto it = textures.begin(); it != textures.end();) { if (it->first != white.get() && it->second.source.use_count() == 1) { destroy(it->second.image); vkFreeDescriptorSets(device, descriptor_pool, 1, &it->second.descriptor); it = textures.erase(it); } else ++it; } const VkDescriptorSet white_descriptor = upload_texture(white); for (const auto& q : snapshot.ui_quads) if (q.texture) upload_texture(q.texture); for (const auto& draw : snapshot.draws) if (draw.texture) upload_texture(draw.texture); for (const auto& sprite : snapshot.sprites) if (sprite.texture) upload_texture(sprite.texture); for (const auto& triangles : snapshot.ui_triangles) if (triangles.texture) upload_texture(triangles.texture); std::vector data; std::vector scene_batches, shadow_batches, sprite_batches, ui_batches; for (const auto& item : snapshot.draws) { if (!item.mesh) continue; auto first = data.size(); const auto& mesh = *item.mesh; auto emit = [&](std::uint32_t index) { if (index >= mesh.vertices.size()) throw std::out_of_range("Mesh index outside vertex range"); data.push_back(gpu_vertex(mesh.vertices[index], item, snapshot.view_projection)); }; if (mesh.indices.empty()) for (std::uint32_t i = 0; i < mesh.vertices.size(); ++i) emit(i); else for (auto i : mesh.indices) emit(i); auto count = static_cast(data.size() - first); if (count % 3) throw std::invalid_argument( "Mesh triangle vertex count must be divisible by three"); if (!count) continue; Batch batch{static_cast(first), count, item.texture ? item.texture.get() : white.get()}; if (item.cast_shadow) shadow_batches.push_back(batch); if (outside(data, first)) ++statistics.culled_meshes; else scene_batches.push_back(batch); } struct OrderedSprite { const Sprite* sprite; float depth; }; std::vector ordered_sprites; ordered_sprites.reserve(snapshot.sprites.size()); for (const auto& sprite : snapshot.sprites) { const auto clip = point(snapshot.view_projection, {sprite.position[0], sprite.position[1], sprite.position[2], 1}); const auto depth = clip[3] != 0 ? clip[2] / clip[3] : std::numeric_limits::infinity(); ordered_sprites.push_back( {&sprite, std::isfinite(depth) ? depth : std::numeric_limits::infinity()}); } std::stable_sort(ordered_sprites.begin(), ordered_sprites.end(), [](const auto& a, const auto& b) { if (a.sprite->layer != b.sprite->layer) return a.sprite->layer < b.sprite->layer; return a.depth > b.depth; }); for (const auto& ordered : ordered_sprites) { const auto& sprite = *ordered.sprite; auto first = static_cast(data.size()); float c = std::cos(sprite.rotation), s = std::sin(sprite.rotation); for (auto i : {0, 1, 2, 0, 2, 3}) { const float corners[4][2] = {{-.5f, -.5f}, {.5f, -.5f}, {.5f, .5f}, {-.5f, .5f}}; float x = corners[i][0] * sprite.size[0], y = corners[i][1] * sprite.size[1]; auto clip = point(snapshot.view_projection, {sprite.position[0] + c * x - s * y, sprite.position[1] + s * x + c * y, sprite.position[2], 1}); GpuVertex vertex{}; std::copy(clip.begin(), clip.end(), vertex.clip); std::copy(sprite.color.begin(), sprite.color.end(), vertex.color); vertex.uv[0] = corners[i][0] + .5f; vertex.uv[1] = .5f - corners[i][1]; vertex.material[0] = sprite.texture && sprite.texture->srgb ? 1.f : 0.f; data.push_back(vertex); } sprite_batches.push_back( {first, 6, sprite.texture ? sprite.texture.get() : white.get()}); } for (const auto& q : snapshot.ui_quads) { auto first = static_cast(data.size()); quad(data, q); const Texture* texture = q.texture ? q.texture.get() : white.get(); if (!ui_batches.empty() && ui_batches.back().texture == texture) ui_batches.back().count += 6; else ui_batches.push_back({first, 6, texture}); } auto text_first = static_cast(data.size()); for (const auto& text : snapshot.ui_text) draw_debug_text(data, text); if (data.size() > text_first) ui_batches.push_back( {text_first, static_cast(data.size() - text_first), white.get()}); for (const auto& triangles : snapshot.ui_triangles) { if (triangles.vertices.size() % 3 != 0) throw std::invalid_argument("UI triangle list must contain complete triangles"); const auto first = static_cast(data.size()); for (const auto& source : triangles.vertices) { GpuVertex vertex{}; vertex.clip[0] = source.position[0] / float(width) * 2 - 1; vertex.clip[1] = source.position[1] / float(height) * 2 - 1; vertex.clip[3] = 1; std::copy(source.color.begin(), source.color.end(), vertex.color); std::copy(source.uv.begin(), source.uv.end(), vertex.uv); vertex.material[0] = triangles.texture && triangles.texture->srgb ? 1.f : 0.f; data.push_back(vertex); } ui_batches.push_back({first, static_cast(triangles.vertices.size()), triangles.texture ? triangles.texture.get() : white.get(), triangles.clip_rect}); } statistics.vertices = static_cast(data.size()); auto byte_count = std::max(sizeof(GpuVertex), data.size() * sizeof(GpuVertex)); if (vertices.size < byte_count) { destroy(vertices); vertices = make_buffer(byte_count, VK_BUFFER_USAGE_VERTEX_BUFFER_BIT, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT); } void* mapped{}; check(vkMapMemory(device, vertices.memory, 0, vertices.size, 0, &mapped), "Map vertices"); if (!data.empty()) std::memcpy(mapped, data.data(), data.size() * sizeof(GpuVertex)); vkUnmapMemory(device, vertices.memory); Vec3 direction = snapshot.light_direction; float length = std::sqrt(direction[0] * direction[0] + direction[1] * direction[1] + direction[2] * direction[2]); if (length < 1e-5f) { direction = {-.5f, -1, -.3f}; length = std::sqrt(1.34f); } for (auto& v : direction) v /= length; Vec3 light_eye{-direction[0] * 30, -direction[1] * 30, -direction[2] * 30}; Vec3 light_up = std::abs(direction[1]) > .98f ? Vec3{0, 0, 1} : Vec3{0, 1, 0}; Push push{multiply(orthographic(-20, 20, -20, 20, .1f, 80), look_at(light_eye, {0, 0, 0}, light_up)), {direction[0], direction[1], direction[2], 0}, {snapshot.eye[0], snapshot.eye[1], snapshot.eye[2], 1}}; std::optional swap_index; if (can_present) { std::uint32_t index{}; // Compositors can withhold images while a window is occluded. Rendering and // editor capture must remain available even when presentation cannot advance. const auto result = vkAcquireNextImageKHR(device, swapchain, 0, acquired, VK_NULL_HANDLE, &index); if (result == VK_ERROR_OUT_OF_DATE_KHR) { dirty_swapchain = true; } else if (result == VK_SUCCESS || result == VK_SUBOPTIMAL_KHR) { swap_index = index; if (result == VK_SUBOPTIMAL_KHR) dirty_swapchain = true; } else if (result != VK_NOT_READY && result != VK_TIMEOUT) { check(result, "Acquire swapchain image"); } } begin(); if (timestamp_pool) { vkCmdResetQueryPool(command, timestamp_pool, 0, 2); vkCmdWriteTimestamp2(command, VK_PIPELINE_STAGE_2_TOP_OF_PIPE_BIT, timestamp_pool, 0); } VkDeviceSize offset{}; vkCmdBindVertexBuffers(command, 0, 1, &vertices.handle, &offset); vkCmdPushConstants(command, pipeline_layout, VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT, 0, sizeof(push), &push); auto set_viewport = [&](std::uint32_t w, std::uint32_t h) { VkViewport viewport{0, 0, float(w), float(h), 0, 1}; VkRect2D scissor{{0, 0}, {w, h}}; vkCmdSetViewport(command, 0, 1, &viewport); vkCmdSetScissor(command, 0, 1, &scissor); }; RenderGraph graph; auto add_pass = [&](std::string name, std::vector reads, std::vector writes, RenderGraph::Callback callback) { const auto label_name = name; graph.add(std::move(name), std::move(reads), std::move(writes), [this, label_name, callback = std::move(callback)] { if (statistics.gpu_labels_enabled) { VkDebugUtilsLabelEXT label{}; label.sType = VK_STRUCTURE_TYPE_DEBUG_UTILS_LABEL_EXT; label.pLabelName = label_name.c_str(); label.color[0] = .25f; label.color[1] = .65f; label.color[2] = .9f; label.color[3] = 1.f; begin_gpu_label(command, &label); ++statistics.gpu_label_count; } struct EndLabel { Impl& renderer; ~EndLabel() { if (renderer.statistics.gpu_labels_enabled) renderer.end_gpu_label(renderer.command); } } end{*this}; callback(); }); }; add_pass("ShadowMap", {}, {"shadow"}, [&] { transition(command, shadow, VK_IMAGE_LAYOUT_DEPTH_ATTACHMENT_OPTIMAL, VK_IMAGE_ASPECT_DEPTH_BIT); VkRenderingAttachmentInfo attachment{}; attachment.sType = VK_STRUCTURE_TYPE_RENDERING_ATTACHMENT_INFO; attachment.imageView = shadow.view; attachment.imageLayout = VK_IMAGE_LAYOUT_DEPTH_ATTACHMENT_OPTIMAL; attachment.loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR; attachment.storeOp = VK_ATTACHMENT_STORE_OP_STORE; attachment.clearValue.depthStencil = {1, 0}; VkRenderingInfo rendering{}; rendering.sType = VK_STRUCTURE_TYPE_RENDERING_INFO; rendering.renderArea = {{0, 0}, {shadow_size, shadow_size}}; rendering.layerCount = 1; rendering.pDepthAttachment = &attachment; vkCmdBeginRendering(command, &rendering); set_viewport(shadow_size, shadow_size); vkCmdBindPipeline(command, VK_PIPELINE_BIND_POINT_GRAPHICS, shadow_pipeline); vkCmdPushConstants(command, pipeline_layout, VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT, 0, sizeof(push), &push); for (auto batch : shadow_batches) { vkCmdDraw(command, batch.count, 1, batch.first, 0); ++statistics.draw_calls; } vkCmdEndRendering(command); transition(command, shadow, VK_IMAGE_LAYOUT_DEPTH_READ_ONLY_OPTIMAL, VK_IMAGE_ASPECT_DEPTH_BIT); }); add_pass("ForwardAndUI", {"shadow"}, {"color", "depth"}, [&] { transition(command, color, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, VK_IMAGE_ASPECT_COLOR_BIT); transition(command, depth, VK_IMAGE_LAYOUT_DEPTH_ATTACHMENT_OPTIMAL, VK_IMAGE_ASPECT_DEPTH_BIT); VkRenderingAttachmentInfo ca{}; ca.sType = VK_STRUCTURE_TYPE_RENDERING_ATTACHMENT_INFO; ca.imageView = color.view; ca.imageLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL; ca.loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR; ca.storeOp = VK_ATTACHMENT_STORE_OP_STORE; std::copy(snapshot.clear_color.begin(), snapshot.clear_color.end(), ca.clearValue.color.float32); VkRenderingAttachmentInfo da{}; da.sType = VK_STRUCTURE_TYPE_RENDERING_ATTACHMENT_INFO; da.imageView = depth.view; da.imageLayout = VK_IMAGE_LAYOUT_DEPTH_ATTACHMENT_OPTIMAL; da.loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR; da.storeOp = VK_ATTACHMENT_STORE_OP_DONT_CARE; da.clearValue.depthStencil = {1, 0}; VkRenderingInfo rendering{}; rendering.sType = VK_STRUCTURE_TYPE_RENDERING_INFO; rendering.renderArea = {{0, 0}, {width, height}}; rendering.layerCount = 1; rendering.colorAttachmentCount = 1; rendering.pColorAttachments = &ca; rendering.pDepthAttachment = &da; vkCmdBeginRendering(command, &rendering); set_viewport(width, height); if (snapshot.scene_rect[2] > 0 && snapshot.scene_rect[3] > 0) { auto r = snapshot.scene_rect; float x = std::clamp(r[0], 0.f, float(width)), y = std::clamp(r[1], 0.f, float(height)); float w = std::min(r[2], float(width) - x), h = std::min(r[3], float(height) - y); VkViewport viewport{x, y, w, h, 0, 1}; VkRect2D scissor{{static_cast(x), static_cast(y)}, {static_cast(w), static_cast(h)}}; vkCmdSetViewport(command, 0, 1, &viewport); vkCmdSetScissor(command, 0, 1, &scissor); } vkCmdBindPipeline(command, VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline); vkCmdPushConstants(command, pipeline_layout, VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT, 0, sizeof(push), &push); vkCmdBindDescriptorSets(command, VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline_layout, 0, 1, &white_descriptor, 0, nullptr); for (auto batch : scene_batches) { auto descriptor = textures.at(batch.texture).descriptor; vkCmdBindDescriptorSets(command, VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline_layout, 0, 1, &descriptor, 0, nullptr); vkCmdDraw(command, batch.count, 1, batch.first, 0); ++statistics.draw_calls; } vkCmdBindPipeline(command, VK_PIPELINE_BIND_POINT_GRAPHICS, sprite_pipeline); for (auto batch : sprite_batches) { auto descriptor = textures.at(batch.texture).descriptor; vkCmdBindDescriptorSets(command, VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline_layout, 0, 1, &descriptor, 0, nullptr); vkCmdDraw(command, batch.count, 1, batch.first, 0); ++statistics.draw_calls; } set_viewport(width, height); vkCmdBindPipeline(command, VK_PIPELINE_BIND_POINT_GRAPHICS, ui_pipeline); vkCmdPushConstants(command, pipeline_layout, VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT, 0, sizeof(push), &push); for (auto batch : ui_batches) { VkRect2D scissor{{0, 0}, {width, height}}; if (batch.clip_rect[2] > 0 && batch.clip_rect[3] > 0) { const auto& clip = batch.clip_rect; const float x = std::clamp(clip[0], 0.f, float(width)); const float y = std::clamp(clip[1], 0.f, float(height)); const float right = std::clamp(clip[0] + clip[2], x, float(width)); const float bottom = std::clamp(clip[1] + clip[3], y, float(height)); scissor.offset = {static_cast(x), static_cast(y)}; scissor.extent = {static_cast(right) - static_cast(x), static_cast(bottom) - static_cast(y)}; } if (!scissor.extent.width || !scissor.extent.height) continue; vkCmdSetScissor(command, 0, 1, &scissor); auto descriptor = textures.at(batch.texture).descriptor; vkCmdBindDescriptorSets(command, VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline_layout, 0, 1, &descriptor, 0, nullptr); vkCmdDraw(command, batch.count, 1, batch.first, 0); ++statistics.draw_calls; } vkCmdEndRendering(command); }); add_pass("Readback", {"color"}, {"capture"}, [&] { transition(command, color, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, VK_IMAGE_ASPECT_COLOR_BIT); VkBufferImageCopy copy{}; copy.imageSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1}; copy.imageExtent = {width, height, 1}; vkCmdCopyImageToBuffer(command, color.handle, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, readback.handle, 1, ©); }); if (swap_index) add_pass("Presentation", {"color"}, {"swapchain"}, [&] { auto index = *swap_index; transition(command, swap_images[index], swap_layouts[index], VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_ASPECT_COLOR_BIT); VkImageBlit blit{}; blit.srcSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1}; blit.srcOffsets[1] = {static_cast(width), static_cast(height), 1}; blit.dstSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1}; blit.dstOffsets[1] = {static_cast(swap_extent.width), static_cast(swap_extent.height), 1}; vkCmdBlitImage(command, color.handle, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, swap_images[index], VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, &blit, VK_FILTER_NEAREST); transition(command, swap_images[index], swap_layouts[index], VK_IMAGE_LAYOUT_PRESENT_SRC_KHR, VK_IMAGE_ASPECT_COLOR_BIT); }); graph.execute(); if (timestamp_pool) vkCmdWriteTimestamp2(command, VK_PIPELINE_STAGE_2_BOTTOM_OF_PIPE_BIT, timestamp_pool, 1); submit(swap_index.has_value()); if (timestamp_pool) { std::uint64_t stamps[2]{}; check(vkGetQueryPoolResults(device, timestamp_pool, 0, 2, sizeof(stamps), stamps, sizeof(std::uint64_t), VK_QUERY_RESULT_64_BIT | VK_QUERY_RESULT_WAIT_BIT), "Read GPU timestamps"); auto delta = stamps[1] - stamps[0]; if (timestamp_bits < 64) delta &= (std::uint64_t(1) << timestamp_bits) - 1; statistics.gpu_ms = double(delta) * timestamp_period / 1000000.0; } if (swap_index) { VkPresentInfoKHR present{}; present.sType = VK_STRUCTURE_TYPE_PRESENT_INFO_KHR; present.waitSemaphoreCount = 1; present.pWaitSemaphores = &present_ready; present.swapchainCount = 1; present.pSwapchains = &swapchain; present.pImageIndices = &*swap_index; auto result = vkQueuePresentKHR(queue, &present); if (result == VK_ERROR_OUT_OF_DATE_KHR || result == VK_SUBOPTIMAL_KHR) dirty_swapchain = true; else check(result, "Present frame"); check(vkQueueWaitIdle(queue), "Wait presentation"); } const auto readback_started = std::chrono::steady_clock::now(); last_pixels.resize(std::size_t(width) * height * 4); check(vkMapMemory(device, readback.memory, 0, readback.size, 0, &mapped), "Map captured frame"); std::memcpy(last_pixels.data(), mapped, last_pixels.size()); vkUnmapMemory(device, readback.memory); statistics.readback_cpu_ms = std::chrono::duration( std::chrono::steady_clock::now() - readback_started) .count(); ++statistics.frame; statistics.gpu_allocated_bytes = vertices.allocation_size + readback.allocation_size + color.allocation_size + depth.allocation_size + shadow.allocation_size; statistics.texture_count = static_cast(textures.size()); for (const auto& [_, texture] : textures) statistics.gpu_allocated_bytes += texture.image.allocation_size; statistics.validation_errors = validation_errors.load(); statistics.cpu_ms = std::chrono::duration(std::chrono::steady_clock::now() - start) .count(); } }; Renderer::Renderer(const RendererConfig& config) : impl_(std::make_unique()) { impl_->initialize(config); } Renderer::~Renderer() = default; Renderer::Renderer(Renderer&&) noexcept = default; Renderer& Renderer::operator=(Renderer&&) noexcept = default; void Renderer::render(const Snapshot& snapshot) { impl_->render(snapshot); } bool Renderer::reload_shaders(std::string& error) { auto& r = *impl_; check(vkDeviceWaitIdle(r.device), "Wait shader reload"); auto previous_layout = r.pipeline_layout; auto previous = r.pipeline; auto previous_ui = r.ui_pipeline; auto previous_shadow = r.shadow_pipeline; auto previous_sprite = r.sprite_pipeline; r.pipeline_layout = {}; r.pipeline = {}; r.ui_pipeline = {}; r.shadow_pipeline = {}; r.sprite_pipeline = {}; try { r.make_pipelines(); } catch (const std::exception& exception) { if (r.pipeline) vkDestroyPipeline(r.device, r.pipeline, nullptr); if (r.ui_pipeline) vkDestroyPipeline(r.device, r.ui_pipeline, nullptr); if (r.shadow_pipeline) vkDestroyPipeline(r.device, r.shadow_pipeline, nullptr); if (r.sprite_pipeline) vkDestroyPipeline(r.device, r.sprite_pipeline, nullptr); if (r.pipeline_layout) vkDestroyPipelineLayout(r.device, r.pipeline_layout, nullptr); r.pipeline_layout = previous_layout; r.pipeline = previous; r.ui_pipeline = previous_ui; r.shadow_pipeline = previous_shadow; r.sprite_pipeline = previous_sprite; error = exception.what(); return false; } vkDestroyPipeline(r.device, previous, nullptr); vkDestroyPipeline(r.device, previous_ui, nullptr); vkDestroyPipeline(r.device, previous_shadow, nullptr); vkDestroyPipeline(r.device, previous_sprite, nullptr); vkDestroyPipelineLayout(r.device, previous_layout, nullptr); error.clear(); return true; } void Renderer::resize(std::uint32_t w, std::uint32_t h) { if (!w || !h) return; if (impl_->window) { SDL_SetWindowSize(impl_->window, static_cast(w), static_cast(h)); impl_->dirty_swapchain = true; } else if (w != impl_->width || h != impl_->height) { impl_->width = w; impl_->height = h; impl_->make_targets(); } } std::uint32_t Renderer::width() const { return impl_->width; } std::uint32_t Renderer::height() const { return impl_->height; } float Renderer::display_scale() const { const float scale = impl_->window ? SDL_GetWindowDisplayScale(impl_->window) : 1.f; return std::isfinite(scale) && scale > 0.f ? scale : 1.f; } bool Renderer::should_close() const { return impl_->close; } const FrameStats& Renderer::stats() const { return impl_->statistics; } std::vector Renderer::pixels() const { return impl_->last_pixels; } void Renderer::capture(const std::filesystem::path& path) { if (impl_->last_pixels.empty()) throw std::runtime_error("Cannot capture before a completed frame"); std::ofstream out(faset::native_io_path(path), std::ios::binary); if (!out) throw std::runtime_error("Cannot write screenshot: " + faset::path_to_utf8(path)); out << "P6\n" << width() << ' ' << height() << "\n255\n"; for (std::size_t i = 0; i < impl_->last_pixels.size(); i += 4) out.write(reinterpret_cast(impl_->last_pixels.data() + i), 3); if (!out) throw std::runtime_error("Screenshot write failed"); } void Renderer::set_title(const std::string& title) { if (impl_->window) SDL_SetWindowTitle(impl_->window, title.c_str()); } void Renderer::set_text_input(bool enabled) { if (!impl_->window) return; if (enabled) SDL_StartTextInput(impl_->window); else SDL_StopTextInput(impl_->window); } void Renderer::set_text_input_area(float x, float y, float width, float height) { if (!impl_->window) return; int w{}, h{}, pw{}, ph{}; SDL_GetWindowSize(impl_->window, &w, &h); SDL_GetWindowSizeInPixels(impl_->window, &pw, &ph); float sx = pw > 0 ? float(w) / float(pw) : 1, sy = ph > 0 ? float(h) / float(ph) : 1; SDL_Rect rectangle{int(x * sx), int(y * sy), std::max(1, int(width * sx)), std::max(1, int(height * sy))}; if (!SDL_SetTextInputArea(impl_->window, &rectangle, 0)) throw std::runtime_error(SDL_GetError()); } void Renderer::set_clipboard(const std::string& text) { if (!impl_->window) { impl_->offscreen_clipboard = text; return; } if (!SDL_SetClipboardText(text.c_str())) throw std::runtime_error(SDL_GetError()); } std::string Renderer::clipboard() const { if (!impl_->window) return impl_->offscreen_clipboard; char* text = SDL_GetClipboardText(); if (!text) return {}; std::string result = text; SDL_free(text); return result; } std::vector Renderer::poll_events() { std::vector result; SDL_Event event{}; while (SDL_PollEvent(&event)) { Event item; bool emit = true; auto modifiers = SDL_GetModState(); item.control = (modifiers & SDL_KMOD_CTRL) != 0; item.shift = (modifiers & SDL_KMOD_SHIFT) != 0; item.alt = (modifiers & SDL_KMOD_ALT) != 0; switch (event.type) { case SDL_EVENT_QUIT: case SDL_EVENT_WINDOW_CLOSE_REQUESTED: item.type = Event::Type::Quit; impl_->close = true; break; case SDL_EVENT_WINDOW_RESIZED: case SDL_EVENT_WINDOW_PIXEL_SIZE_CHANGED: item.type = Event::Type::Resize; item.x = float(event.window.data1); item.y = float(event.window.data2); impl_->dirty_swapchain = true; break; case SDL_EVENT_WINDOW_FOCUS_GAINED: item.type = Event::Type::FocusGained; break; case SDL_EVENT_WINDOW_FOCUS_LOST: item.type = Event::Type::FocusLost; break; case SDL_EVENT_MOUSE_MOTION: item.type = Event::Type::MouseMove; item.x = event.motion.x; item.y = event.motion.y; break; case SDL_EVENT_MOUSE_BUTTON_DOWN: case SDL_EVENT_MOUSE_BUTTON_UP: item.type = event.type == SDL_EVENT_MOUSE_BUTTON_DOWN ? Event::Type::MouseDown : Event::Type::MouseUp; item.x = event.button.x; item.y = event.button.y; item.button = event.button.button; break; case SDL_EVENT_MOUSE_WHEEL: item.type = Event::Type::Wheel; item.x = event.wheel.x; item.y = event.wheel.y; break; case SDL_EVENT_KEY_DOWN: case SDL_EVENT_KEY_UP: item.type = event.type == SDL_EVENT_KEY_DOWN ? Event::Type::KeyDown : Event::Type::KeyUp; item.key = SDL_GetKeyName(event.key.key); item.repeat = event.key.repeat; break; case SDL_EVENT_TEXT_INPUT: item.type = Event::Type::TextInput; item.text = event.text.text; break; case SDL_EVENT_TEXT_EDITING: item.type = Event::Type::TextEditing; item.text = event.edit.text; item.edit_start = event.edit.start; item.edit_length = event.edit.length; break; default: emit = false; } // Rendering/UI coordinates use drawable pixels; SDL pointer events use logical window // units. if (impl_->window && (item.type == Event::Type::MouseMove || item.type == Event::Type::MouseDown || item.type == Event::Type::MouseUp)) { int w{}, h{}, pw{}, ph{}; SDL_GetWindowSize(impl_->window, &w, &h); SDL_GetWindowSizeInPixels(impl_->window, &pw, &ph); if (w > 0 && h > 0) { item.x *= float(pw) / float(w); item.y *= float(ph) / float(h); } } if (emit) result.push_back(std::move(item)); } return result; } } // namespace faset::render