#include "shader_contract.hpp" #include #include #include #include #include #include #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]; float previous_clip[4], motion_valid{}; }; // The GPU path keeps local geometry separate from the instance table. All layouts // below are mirrored by gpu_scene.slang and checked by shader reflection tests. struct SceneVertex { float position[3], normal[3], color[4], uv[2]; }; static_assert(sizeof(SceneVertex) == 48); struct SceneInstance { Mat4 model; std::array normal0, normal1, normal2; std::array color, material; std::array center_extent, half_extent; std::array previous_center_extent, previous_half_extent; std::array metadata; Mat4 previous_model; }; static_assert(sizeof(SceneInstance) == 288 && offsetof(SceneInstance, previous_model) == 224); struct SceneView { Mat4 current_vp, previous_vp; std::array viewport, previous_viewport; std::array hzb_dimensions, previous_hzb_dimensions, flags; }; static_assert(sizeof(SceneView) == 208); struct SceneBin { std::uint32_t candidate_first, candidate_count, visible_base, capacity; }; struct SceneCandidate { std::uint32_t instance_id, bin_index, flags, unused; }; struct SceneIndirect { std::uint32_t vertex_count, instance_count, first_vertex, first_instance; }; static_assert(sizeof(SceneBin) == 16 && sizeof(SceneCandidate) == 16 && sizeof(SceneIndirect) == 16); struct Push { Mat4 light_view_projection; std::array light_direction, eye; }; static_assert(sizeof(Push) == 96, "Slang FrameParameters layout"); struct ScenePush { Push frame; std::array draw_info; }; static_assert(sizeof(ScenePush) == 112); struct LightingHeaderGpu { std::array counts{}; std::array sun_direction_intensity{}; std::array sun_color{}; std::array camera_forward_shadow_distance{}; std::array cascade_splits{}; }; struct LocalLightGpu { std::array position_range{}; std::array direction_cos_outer{}; std::array color_intensity{}; std::array cone_type_shadow_view{}; std::array reserved{}; }; struct ShadowViewGpu { Mat4 view_projection{identity}; std::array tile_scale_offset{}; std::array guarded_clamp{}; std::array bias_flags{}; }; struct LightTilePush { Mat4 view_projection; std::array viewport; std::array dimensions; }; static_assert(sizeof(LightTilePush) == 96); static_assert(sizeof(LightingHeaderGpu) == 80 && offsetof(LightingHeaderGpu, sun_direction_intensity) == 16 && offsetof(LightingHeaderGpu, sun_color) == 32 && offsetof(LightingHeaderGpu, camera_forward_shadow_distance) == 48 && offsetof(LightingHeaderGpu, cascade_splits) == 64); static_assert(sizeof(LocalLightGpu) == 80 && offsetof(LocalLightGpu, direction_cos_outer) == 16 && offsetof(LocalLightGpu, color_intensity) == 32 && offsetof(LocalLightGpu, cone_type_shadow_view) == 48 && offsetof(LocalLightGpu, reserved) == 64); static_assert(sizeof(ShadowViewGpu) == 112 && offsetof(ShadowViewGpu, tile_scale_offset) == 64 && offsetof(ShadowViewGpu, guarded_clamp) == 80 && offsetof(ShadowViewGpu, bias_flags) == 96); 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{}; std::vector mip_views; std::uint32_t width{}, height{}; std::uint32_t mip_levels{1}; VkImageLayout layout{VK_IMAGE_LAYOUT_UNDEFINED}; VkDeviceSize allocation_size{}; }; struct Batch { std::uint32_t first{}, count{}; const Texture* texture{}; std::array clip_rect{}; }; struct SceneDrawBin { const Mesh* mesh{}; const Texture* texture{}; SceneIndirect command{}; SceneBin range{}; }; struct PreparedScene { std::vector vertices; std::vector instances; std::vector candidates; std::vector bins; std::vector commands; std::vector textures; SceneView view{}; std::uint32_t candidate_count{}; }; float projected_pixels(const Bounds& bounds, const Mat4& view_projection, const std::array& viewport) { float min_x = std::numeric_limits::infinity(); float max_x = -min_x, min_y = min_x, max_y = -min_x; for (unsigned corner = 0; corner < 8; ++corner) { Vec3 p{corner & 1 ? bounds.max[0] : bounds.min[0], corner & 2 ? bounds.max[1] : bounds.min[1], corner & 4 ? bounds.max[2] : bounds.min[2]}; auto clip = point(view_projection, {p[0], p[1], p[2], 1}); if (!std::isfinite(clip[0]) || !std::isfinite(clip[1]) || !std::isfinite(clip[3]) || clip[3] <= 0) return std::numeric_limits::max(); const float x = (clip[0] / clip[3] * .5f + .5f) * viewport[2]; const float y = (clip[1] / clip[3] * .5f + .5f) * viewport[3]; min_x = std::min(min_x, x); max_x = std::max(max_x, x); min_y = std::min(min_y, y); max_y = std::max(max_y, y); } return std::max(max_x - min_x, max_y - min_y); } bool opaque_texture(const Texture* texture) { if (!texture) return true; for (std::size_t i = 3; i < texture->rgba.size(); i += 4) if (texture->rgba[i] != 255) return false; return true; } struct SceneResources { Buffer vertices, instances, candidates, bins, view; Buffer main_ids, post_ids, main_args, post_args, deferred_ids, deferred_count; // CPU-owned templates double as optional diagnostic readback destinations. Buffer main_args_stage, post_args_stage, deferred_count_stage; Image hzb[2]; VkDescriptorSetLayout graphics_layout{}, cull_layout{}, hzb_layout{}; VkDescriptorPool descriptor_pool{}; VkDescriptorSet graphics_main{}, graphics_post{}, cull_main{}, cull_post{}; std::vector hzb_sets[2]; VkPipelineLayout graphics_pipeline_layout{}, cull_pipeline_layout{}, hzb_pipeline_layout{}; VkPipeline graphics_pipeline{}, cull_pipeline{}, post_pipeline{}, hzb_pipeline{}; std::array shader_layouts{}; std::uint32_t hzb_mips{}, hzb_current{}; bool hzb_history_valid{}, available{}; bool hzb_supported{}, hzb_extent_supported{}; Mat4 previous_vp{identity}; Mat4 previous_projection{identity}; std::array previous_viewport{}; std::string previous_view_id; }; struct TemporalResources { Image scene_color, velocity, history_color[2], history_depth[2]; Buffer pixel_counts, pixel_counts_stage; VkDescriptorSetLayout resolve_layout{}, composite_layout{}; VkDescriptorPool descriptor_pool{}; VkDescriptorSet resolve_sets[2]{}, composite_sets[2]{}; VkPipelineLayout resolve_pipeline_layout{}, composite_pipeline_layout{}; VkPipeline resolve_pipeline{}, composite_pipeline{}; VkPipeline direct_pipeline{}, transparent_pipeline{}, sprite_pipeline{}, gpu_pipeline{}; std::array shader_layouts{}; std::array scene_shader_layouts{}; TemporalCapabilities capabilities{}; TemporalHistoryState history; Mat4 previous_jittered_vp{identity}; Mat4 previous_unjittered_vp{identity}; std::array previous_jitter{}; std::uint64_t shader_generation{1}; std::uint32_t internal_width{}, internal_height{}, completed_index{}; bool has_completed_image{}; }; constexpr std::uint32_t timestamp_capacity = 40; } // 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{}; std::uint32_t max_compute_groups_x{}, max_compute_groups_y{}, max_storage_buffer_range{}, max_image_dimension{}; bool independent_blend_supported{}; 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, local_shadow; std::uint32_t sun_shadow_size{}, local_shadow_size{}; Buffer vertices, readback; Buffer lighting_header, lighting_locals, lighting_views, light_tile_words, light_tile_readback; bool light_tiles_capable{}; SceneResources scene; TemporalResources temporal; InstanceTracker instance_tracker; std::unordered_map previous_lods; struct CachedBounds { std::weak_ptr owner; Bounds local; }; struct CachedOpacity { std::weak_ptr owner; std::uint64_t revision{}; bool opaque{}; }; std::unordered_map bounds_cache; std::unordered_map opacity_cache; VkDescriptorSetLayout descriptor_layout{}; VkDescriptorPool descriptor_pool{}; VkDescriptorSetLayout lighting_layout{}; VkDescriptorPool lighting_pool{}; VkDescriptorSet lighting_set{}; VkDescriptorSetLayout light_tile_layout{}; VkDescriptorPool light_tile_pool{}; VkDescriptorSet light_tile_set{}; VkPipelineLayout light_tile_pipeline_layout{}; VkPipeline light_tile_pipeline{}; VkSampler shadow_sampler{}, color_sampler{}; VkPipelineLayout pipeline_layout{}; VkPipeline pipeline{}, ui_pipeline{}, shadow_pipeline{}, sprite_pipeline{}, temporal_ui_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) { for (auto view : i.mip_views) vkDestroyImageView(device, view, nullptr); 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 destroy_scene_interfaces() { if (!device) return; for (auto* pipeline : {&scene.graphics_pipeline, &scene.cull_pipeline, &scene.post_pipeline, &scene.hzb_pipeline}) { if (*pipeline) vkDestroyPipeline(device, *pipeline, nullptr); *pipeline = {}; } for (auto* layout : {&scene.graphics_pipeline_layout, &scene.cull_pipeline_layout, &scene.hzb_pipeline_layout}) { if (*layout) vkDestroyPipelineLayout(device, *layout, nullptr); *layout = {}; } if (scene.descriptor_pool) vkDestroyDescriptorPool(device, scene.descriptor_pool, nullptr); scene.descriptor_pool = {}; for (auto* layout : {&scene.graphics_layout, &scene.cull_layout, &scene.hzb_layout}) { if (*layout) vkDestroyDescriptorSetLayout(device, *layout, nullptr); *layout = {}; } scene.graphics_main = scene.graphics_post = scene.cull_main = scene.cull_post = {}; for (auto& sets : scene.hzb_sets) sets.clear(); scene.shader_layouts = {}; } void destroy_temporal_interfaces() { if (!device) return; for (auto* pipeline : {&temporal.resolve_pipeline, &temporal.composite_pipeline, &temporal.direct_pipeline, &temporal.transparent_pipeline, &temporal.gpu_pipeline}) { if (*pipeline) vkDestroyPipeline(device, *pipeline, nullptr); *pipeline = {}; } for (auto* layout : {&temporal.resolve_pipeline_layout, &temporal.composite_pipeline_layout}) { if (*layout) vkDestroyPipelineLayout(device, *layout, nullptr); *layout = {}; } if (temporal.descriptor_pool) vkDestroyDescriptorPool(device, temporal.descriptor_pool, nullptr); temporal.descriptor_pool = {}; for (auto* layout : {&temporal.resolve_layout, &temporal.composite_layout}) { if (*layout) vkDestroyDescriptorSetLayout(device, *layout, nullptr); *layout = {}; } temporal.resolve_sets[0] = temporal.resolve_sets[1] = {}; temporal.composite_sets[0] = temporal.composite_sets[1] = {}; } void cleanup() { if (device) vkDeviceWaitIdle(device); for (auto& [_, texture] : textures) destroy(texture.image); destroy(scene.vertices); destroy(scene.instances); destroy(scene.candidates); destroy(scene.bins); destroy(scene.view); destroy(scene.main_ids); destroy(scene.post_ids); destroy(scene.main_args); destroy(scene.post_args); destroy(scene.deferred_ids); destroy(scene.deferred_count); destroy(scene.main_args_stage); destroy(scene.post_args_stage); destroy(scene.deferred_count_stage); destroy(scene.hzb[0]); destroy(scene.hzb[1]); destroy(temporal.scene_color); destroy(temporal.velocity); for (auto& image : temporal.history_color) destroy(image); for (auto& image : temporal.history_depth) destroy(image); destroy(temporal.pixel_counts); destroy(temporal.pixel_counts_stage); destroy(vertices); destroy(readback); destroy(lighting_header); destroy(lighting_locals); destroy(lighting_views); destroy(light_tile_words); destroy(light_tile_readback); destroy(color); destroy(depth); destroy(shadow); destroy(local_shadow); if (device) { destroy_temporal_interfaces(); destroy_scene_interfaces(); destroy_light_tile_interfaces(); 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 (temporal_ui_pipeline) vkDestroyPipeline(device, temporal_ui_pipeline, nullptr); if (pipeline_layout) vkDestroyPipelineLayout(device, pipeline_layout, nullptr); if (descriptor_pool) vkDestroyDescriptorPool(device, descriptor_pool, nullptr); if (lighting_pool) vkDestroyDescriptorPool(device, lighting_pool, nullptr); if (descriptor_layout) vkDestroyDescriptorSetLayout(device, descriptor_layout, nullptr); if (lighting_layout) vkDestroyDescriptorSetLayout(device, lighting_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, std::uint32_t mip_levels = 1) { Image image{}; image.width = w; image.height = h; image.mip_levels = mip_levels; 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 = mip_levels; 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, mip_levels, 0, 1}; check(vkCreateImageView(device, &view, nullptr, &image.view), "Create image view"); if (mip_levels > 1) { image.mip_views.reserve(mip_levels); for (std::uint32_t level = 0; level < mip_levels; ++level) { view.subresourceRange = {aspect, level, 1, 0, 1}; VkImageView mip_view{}; check(vkCreateImageView(device, &view, nullptr, &mip_view), "Create mip view"); image.mip_views.push_back(mip_view); } } } catch (...) { destroy(image); throw; } return image; } void transition(VkCommandBuffer cmd, VkImage image, VkImageLayout& before, VkImageLayout after, VkImageAspectFlags aspect, std::uint32_t mip_levels = 1) { // 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, mip_levels, 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, image.mip_levels); } 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"); (void)temporal_internal_extent(width, height, c.temporal_mode, c.render_scale); 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{}, hzb_props{}, velocity_props{}; vkGetPhysicalDeviceFormatProperties(gpu, VK_FORMAT_R8G8B8A8_UNORM, &color_props); vkGetPhysicalDeviceFormatProperties(gpu, VK_FORMAT_D32_SFLOAT, &depth_props); vkGetPhysicalDeviceFormatProperties(gpu, VK_FORMAT_R32_SFLOAT, &hzb_props); vkGetPhysicalDeviceFormatProperties(gpu, VK_FORMAT_R16G16B16A16_SFLOAT, &velocity_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; max_compute_groups_x = properties.limits.maxComputeWorkGroupCount[0]; max_compute_groups_y = properties.limits.maxComputeWorkGroupCount[1]; max_storage_buffer_range = properties.limits.maxStorageBufferRange; max_image_dimension = properties.limits.maxImageDimension2D; independent_blend_supported = features.features.independentBlend; light_tiles_capable = (queues[i].queueFlags & VK_QUEUE_COMPUTE_BIT) != 0 && properties.limits.maxComputeWorkGroupInvocations >= 64 && properties.limits.maxComputeWorkGroupSize[0] >= 64 && properties.limits.maxPerStageDescriptorStorageBuffers >= 4 && properties.limits.maxDescriptorSetStorageBuffers >= 4 && max_compute_groups_x > 0; scene.available = (queues[i].queueFlags & VK_QUEUE_COMPUTE_BIT) != 0 && properties.limits.maxPerStageDescriptorStorageBuffers >= 8 && properties.limits.maxDescriptorSetStorageBuffers >= 8 && properties.limits.maxComputeWorkGroupInvocations >= 64 && properties.limits.maxComputeWorkGroupSize[0] >= 64 && max_compute_groups_x > 0; scene.hzb_supported = scene.available && (hzb_props.optimalTilingFeatures & (VK_FORMAT_FEATURE_SAMPLED_IMAGE_BIT | VK_FORMAT_FEATURE_STORAGE_IMAGE_BIT | VK_FORMAT_FEATURE_TRANSFER_SRC_BIT | VK_FORMAT_FEATURE_TRANSFER_DST_BIT)) == (VK_FORMAT_FEATURE_SAMPLED_IMAGE_BIT | VK_FORMAT_FEATURE_STORAGE_IMAGE_BIT | VK_FORMAT_FEATURE_TRANSFER_SRC_BIT | VK_FORMAT_FEATURE_TRANSFER_DST_BIT); temporal.capabilities.compute = (queues[i].queueFlags & VK_QUEUE_COMPUTE_BIT) != 0 && properties.limits.maxComputeWorkGroupInvocations >= 64 && properties.limits.maxComputeWorkGroupSize[0] >= 8 && properties.limits.maxComputeWorkGroupSize[1] >= 8 && properties.limits.maxPerStageDescriptorSampledImages >= 5 && properties.limits.maxPerStageDescriptorStorageImages >= 2; const auto velocity_features = VK_FORMAT_FEATURE_COLOR_ATTACHMENT_BIT | VK_FORMAT_FEATURE_SAMPLED_IMAGE_BIT | VK_FORMAT_FEATURE_STORAGE_IMAGE_BIT; temporal.capabilities.formats = independent_blend_supported && (color_props.optimalTilingFeatures & VK_FORMAT_FEATURE_SAMPLED_IMAGE_BIT) && (velocity_props.optimalTilingFeatures & velocity_features) == velocity_features && (hzb_props.optimalTilingFeatures & (VK_FORMAT_FEATURE_SAMPLED_IMAGE_BIT | VK_FORMAT_FEATURE_STORAGE_IMAGE_BIT)) == (VK_FORMAT_FEATURE_SAMPLED_IMAGE_BIT | VK_FORMAT_FEATURE_STORAGE_IMAGE_BIT); temporal.capabilities.extent = width <= max_image_dimension && height <= max_image_dimension && (width + 7) / 8 <= max_compute_groups_x && (height + 7) / 8 <= max_compute_groups_y; } } } 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; VkPhysicalDeviceFeatures enabled_features{}; enabled_features.independentBlend = independent_blend_supported ? VK_TRUE : VK_FALSE; di.pEnabledFeatures = &enabled_features; 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 = timestamp_capacity; check(vkCreateQueryPool(device, &query, nullptr, ×tamp_pool), "Create GPU timestamp queries"); } const auto shadow_usage = VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT | VK_IMAGE_USAGE_SAMPLED_BIT; for (const auto size : {2048u, 1024u}) { if (size > max_image_dimension) continue; try { shadow = make_image(size, size, VK_FORMAT_D32_SFLOAT, shadow_usage, VK_IMAGE_ASPECT_DEPTH_BIT); sun_shadow_size = size; break; } catch (const std::exception&) { // Optional atlas allocation may fail; try the bounded half-size profile. } } if (!shadow.handle) shadow = make_image(1, 1, VK_FORMAT_D32_SFLOAT, shadow_usage, VK_IMAGE_ASPECT_DEPTH_BIT); for (const auto size : {2048u, 1024u}) { if (size > max_image_dimension) continue; try { local_shadow = make_image(size, size, VK_FORMAT_D32_SFLOAT, shadow_usage, VK_IMAGE_ASPECT_DEPTH_BIT); local_shadow_size = size; break; } catch (const std::exception&) { // Local shadows are optional; all affected lights remain unshadowed. } } make_targets(); light_tile_words = make_buffer(16, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT | VK_BUFFER_USAGE_TRANSFER_SRC_BIT, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT); make_descriptors(); if (light_tiles_capable) { try { make_light_tile_descriptors(); } catch (const std::exception&) { destroy_light_tile_interfaces(); light_tiles_capable = false; } } make_pipelines(); if (scene.available && (c.visibility_mode != VisibilityMode::Direct || c.temporal_mode != TemporalMode::Off)) make_scene_descriptors_and_pipelines(); make_temporal_interfaces_and_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); destroy(scene.hzb[0]); destroy(scene.hzb[1]); destroy(temporal.scene_color); destroy(temporal.velocity); for (auto& image : temporal.history_color) destroy(image); for (auto& image : temporal.history_depth) destroy(image); destroy(temporal.pixel_counts); destroy(temporal.pixel_counts_stage); temporal.has_completed_image = false; scene.hzb_history_valid = false; temporal.capabilities.extent = width <= max_image_dimension && height <= max_image_dimension && (width + 7) / 8 <= max_compute_groups_x && (height + 7) / 8 <= max_compute_groups_y; const auto effective = select_effective_temporal_mode(config.temporal_mode, temporal.capabilities); const auto internal = temporal_internal_extent(width, height, effective, effective == TemporalMode::Upscale ? config.render_scale : 1.f); temporal.internal_width = internal[0]; temporal.internal_height = internal[1]; 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(internal[0], internal[1], VK_FORMAT_D32_SFLOAT, VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT | VK_IMAGE_USAGE_SAMPLED_BIT, VK_IMAGE_ASPECT_DEPTH_BIT); if (effective != TemporalMode::Off) { temporal.scene_color = make_image(internal[0], internal[1], VK_FORMAT_R8G8B8A8_UNORM, VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_SAMPLED_BIT, VK_IMAGE_ASPECT_COLOR_BIT); temporal.velocity = make_image(internal[0], internal[1], VK_FORMAT_R16G16B16A16_SFLOAT, VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_SAMPLED_BIT, VK_IMAGE_ASPECT_COLOR_BIT); for (auto& image : temporal.history_color) image = make_image(width, height, VK_FORMAT_R16G16B16A16_SFLOAT, VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_STORAGE_BIT, VK_IMAGE_ASPECT_COLOR_BIT); for (auto& image : temporal.history_depth) image = make_image(width, height, VK_FORMAT_R32_SFLOAT, VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_STORAGE_BIT, VK_IMAGE_ASPECT_COLOR_BIT); temporal.pixel_counts = make_buffer(2 * sizeof(std::uint32_t), VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT | VK_BUFFER_USAGE_TRANSFER_SRC_BIT, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT); temporal.completed_index = 0; } const auto padded_width = std::bit_ceil(internal[0]); const auto padded_height = std::bit_ceil(internal[1]); scene.hzb_extent_supported = scene.hzb_supported && padded_width <= max_image_dimension && padded_height <= max_image_dimension && (padded_width + 7u) / 8u <= max_compute_groups_x; if (scene.hzb_extent_supported && config.visibility_mode == VisibilityMode::GpuOcclusion) { scene.hzb_mips = std::bit_width(std::max(padded_width, padded_height)); for (auto& pyramid : scene.hzb) pyramid = make_image(padded_width, padded_height, VK_FORMAT_R32_SFLOAT, VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_STORAGE_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT, VK_IMAGE_ASPECT_COLOR_BIT, scene.hzb_mips); scene.hzb_current = 0; } else { scene.hzb_mips = 0; } // 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(); if (scene.graphics_layout) refresh_scene_descriptors(); if (temporal.resolve_layout && effective != TemporalMode::Off) refresh_temporal_descriptors(); } void refresh_temporal_descriptors() { if (!temporal.resolve_layout || !temporal.scene_color.handle) return; if (temporal.descriptor_pool) vkDestroyDescriptorPool(device, temporal.descriptor_pool, nullptr); temporal.descriptor_pool = {}; const std::array sizes{{ {VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE, 12}, {VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, 4}, {VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 2}}}; VkDescriptorPoolCreateInfo pool_info{}; pool_info.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO; pool_info.maxSets = 4; pool_info.poolSizeCount = static_cast(sizes.size()); pool_info.pPoolSizes = sizes.data(); check(vkCreateDescriptorPool(device, &pool_info, nullptr, &temporal.descriptor_pool), "Create temporal descriptor pool"); const std::array layouts{ temporal.resolve_layout, temporal.resolve_layout, temporal.composite_layout, temporal.composite_layout}; VkDescriptorSetAllocateInfo allocation{}; allocation.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO; allocation.descriptorPool = temporal.descriptor_pool; allocation.descriptorSetCount = static_cast(layouts.size()); allocation.pSetLayouts = layouts.data(); std::array sets{}; check(vkAllocateDescriptorSets(device, &allocation, sets.data()), "Allocate temporal descriptors"); temporal.resolve_sets[0] = sets[0]; temporal.resolve_sets[1] = sets[1]; temporal.composite_sets[0] = sets[2]; temporal.composite_sets[1] = sets[3]; for (std::uint32_t next = 0; next < 2; ++next) { const std::array images{{ {VK_NULL_HANDLE, temporal.scene_color.view, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL}, {VK_NULL_HANDLE, depth.view, VK_IMAGE_LAYOUT_DEPTH_READ_ONLY_OPTIMAL}, {VK_NULL_HANDLE, temporal.velocity.view, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL}, {VK_NULL_HANDLE, temporal.history_color[1 - next].view, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL}, {VK_NULL_HANDLE, temporal.history_depth[1 - next].view, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL}, {VK_NULL_HANDLE, temporal.history_color[next].view, VK_IMAGE_LAYOUT_GENERAL}, {VK_NULL_HANDLE, temporal.history_depth[next].view, VK_IMAGE_LAYOUT_GENERAL}}}; std::array writes{}; for (std::uint32_t binding = 0; binding < 7; ++binding) { writes[binding].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; writes[binding].dstSet = temporal.resolve_sets[next]; writes[binding].dstBinding = binding; writes[binding].descriptorCount = 1; writes[binding].descriptorType = binding < 5 ? VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE : VK_DESCRIPTOR_TYPE_STORAGE_IMAGE; writes[binding].pImageInfo = &images[binding]; } const VkDescriptorBufferInfo count_buffer{temporal.pixel_counts.handle, 0, 2 * sizeof(std::uint32_t)}; writes[7].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; writes[7].dstSet = temporal.resolve_sets[next]; writes[7].dstBinding = 7; writes[7].descriptorCount = 1; writes[7].descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER; writes[7].pBufferInfo = &count_buffer; writes[8].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; writes[8].dstSet = temporal.composite_sets[next]; writes[8].dstBinding = 0; writes[8].descriptorCount = 1; writes[8].descriptorType = VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE; const VkDescriptorImageInfo composite_image{ VK_NULL_HANDLE, temporal.history_color[next].view, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL}; writes[8].pImageInfo = &composite_image; vkUpdateDescriptorSets(device, static_cast(writes.size()), writes.data(), 0, nullptr); } } 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"); std::array lighting_bindings{}; for (std::uint32_t i = 0; i < lighting_bindings.size(); ++i) lighting_bindings[i] = {i, i == 3 ? VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE : VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 1, VK_SHADER_STAGE_FRAGMENT_BIT, nullptr}; li.bindingCount = static_cast(lighting_bindings.size()); li.pBindings = lighting_bindings.data(); check(vkCreateDescriptorSetLayout(device, &li, nullptr, &lighting_layout), "Create lighting descriptor layout"); VkDescriptorPoolSize lighting_sizes[] = { {VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 4}, {VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE, 1}}; pi.flags = 0; pi.maxSets = 1; pi.poolSizeCount = 2; pi.pPoolSizes = lighting_sizes; check(vkCreateDescriptorPool(device, &pi, nullptr, &lighting_pool), "Create lighting descriptor pool"); VkDescriptorSetAllocateInfo lighting_allocation{}; lighting_allocation.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO; lighting_allocation.descriptorPool = lighting_pool; lighting_allocation.descriptorSetCount = 1; lighting_allocation.pSetLayouts = &lighting_layout; check(vkAllocateDescriptorSets(device, &lighting_allocation, &lighting_set), "Allocate lighting descriptors"); 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"); } void destroy_light_tile_interfaces() { if (!device) return; if (light_tile_pipeline) vkDestroyPipeline(device, light_tile_pipeline, nullptr); if (light_tile_pipeline_layout) vkDestroyPipelineLayout(device, light_tile_pipeline_layout, nullptr); if (light_tile_pool) vkDestroyDescriptorPool(device, light_tile_pool, nullptr); if (light_tile_layout) vkDestroyDescriptorSetLayout(device, light_tile_layout, nullptr); light_tile_pipeline = {}; light_tile_pipeline_layout = {}; light_tile_pool = {}; light_tile_layout = {}; light_tile_set = {}; } void make_light_tile_descriptors() { const std::array bindings{{ {0, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 1, VK_SHADER_STAGE_COMPUTE_BIT, nullptr}, {1, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 1, VK_SHADER_STAGE_COMPUTE_BIT, nullptr}}}; VkDescriptorSetLayoutCreateInfo layout{}; layout.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO; layout.bindingCount = static_cast(bindings.size()); layout.pBindings = bindings.data(); check(vkCreateDescriptorSetLayout(device, &layout, nullptr, &light_tile_layout), "Create light tile descriptor layout"); VkDescriptorPoolSize size{VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 2}; VkDescriptorPoolCreateInfo pool_info{}; pool_info.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO; pool_info.maxSets = 1; pool_info.poolSizeCount = 1; pool_info.pPoolSizes = &size; check(vkCreateDescriptorPool(device, &pool_info, nullptr, &light_tile_pool), "Create light tile descriptor pool"); VkDescriptorSetAllocateInfo allocation{}; allocation.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO; allocation.descriptorPool = light_tile_pool; allocation.descriptorSetCount = 1; allocation.pSetLayouts = &light_tile_layout; check(vkAllocateDescriptorSets(device, &allocation, &light_tile_set), "Allocate light tile descriptors"); VkPushConstantRange push{VK_SHADER_STAGE_COMPUTE_BIT, 0, sizeof(LightTilePush)}; VkPipelineLayoutCreateInfo pipeline{}; pipeline.sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO; pipeline.setLayoutCount = 1; pipeline.pSetLayouts = &light_tile_layout; pipeline.pushConstantRangeCount = 1; pipeline.pPushConstantRanges = &push; check(vkCreatePipelineLayout(device, &pipeline, nullptr, &light_tile_pipeline_layout), "Create light tile pipeline layout"); } 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; const std::array set_layouts{descriptor_layout, lighting_layout}; li.setLayoutCount = static_cast(set_layouts.size()); li.pSetLayouts = set_layouts.data(); 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 < 5; ++mode) { bool shadow_pass = mode == 2, ui = mode == 1 || mode == 4, sprite = mode == 3, temporal_ui = mode == 4; 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 = temporal_ui ? VK_FORMAT_UNDEFINED : 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 = temporal_ui ? &temporal_ui_pipeline : shadow_pass ? &shadow_pipeline : ui ? &ui_pipeline : sprite ? &sprite_pipeline : &pipeline; check(vkCreateGraphicsPipelines(device, VK_NULL_HANDLE, 1, &pi, nullptr, output), "Create graphics pipeline"); } if (light_tiles_capable) { VkShaderModule tile_shader{}; try { tile_shader = shader(shaders[3]); VkComputePipelineCreateInfo tile{}; tile.sType = VK_STRUCTURE_TYPE_COMPUTE_PIPELINE_CREATE_INFO; tile.stage.sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO; tile.stage.stage = VK_SHADER_STAGE_COMPUTE_BIT; tile.stage.module = tile_shader; tile.stage.pName = "main"; tile.layout = light_tile_pipeline_layout; check(vkCreateComputePipelines(device, VK_NULL_HANDLE, 1, &tile, nullptr, &light_tile_pipeline), "Create light tile compute pipeline"); } catch (const std::exception&) { // Optional acceleration: keep the validated forward renderer. if (light_tile_pipeline) vkDestroyPipeline(device, light_tile_pipeline, nullptr); light_tile_pipeline = {}; light_tiles_capable = false; } if (tile_shader) vkDestroyShaderModule(device, tile_shader, nullptr); } } 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); } void refresh_scene_descriptors() { if (!scene.graphics_layout) return; if (scene.descriptor_pool) vkDestroyDescriptorPool(device, scene.descriptor_pool, nullptr); scene.graphics_main = scene.graphics_post = scene.cull_main = scene.cull_post = {}; for (auto& sets : scene.hzb_sets) sets.clear(); const std::uint32_t hzb_sets = scene.hzb_mips * 2; const std::array sizes{{ {VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 64}, {VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE, 16 + hzb_sets}, {VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, hzb_sets}}}; VkDescriptorPoolCreateInfo pool{}; pool.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO; pool.maxSets = 4 + hzb_sets; pool.poolSizeCount = hzb_sets ? static_cast(sizes.size()) : 2; pool.pPoolSizes = sizes.data(); check(vkCreateDescriptorPool(device, &pool, nullptr, &scene.descriptor_pool), "Create GPU visibility descriptor pool"); const std::array layouts{ scene.graphics_layout, scene.graphics_layout, scene.cull_layout, scene.cull_layout}; VkDescriptorSetAllocateInfo allocation{}; allocation.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO; allocation.descriptorPool = scene.descriptor_pool; allocation.descriptorSetCount = 4; allocation.pSetLayouts = layouts.data(); std::array sets{}; check(vkAllocateDescriptorSets(device, &allocation, sets.data()), "Allocate GPU visibility descriptors"); scene.graphics_main = sets[0]; scene.graphics_post = sets[1]; scene.cull_main = sets[2]; scene.cull_post = sets[3]; if (!scene.hzb_mips) return; for (std::uint32_t image = 0; image < 2; ++image) { auto& target_sets = scene.hzb_sets[image]; target_sets.resize(scene.hzb_mips); std::vector mip_layouts(scene.hzb_mips, scene.hzb_layout); allocation.descriptorSetCount = scene.hzb_mips; allocation.pSetLayouts = mip_layouts.data(); check(vkAllocateDescriptorSets(device, &allocation, target_sets.data()), "Allocate HZB mip descriptors"); for (std::uint32_t mip = 0; mip < scene.hzb_mips; ++mip) { const auto source = mip == 0 ? depth.view : scene.hzb[image].mip_views[mip - 1]; const auto output = scene.hzb[image].mip_views.empty() ? scene.hzb[image].view : scene.hzb[image].mip_views[mip]; VkDescriptorImageInfo images[] = { {VK_NULL_HANDLE, source, mip == 0 ? VK_IMAGE_LAYOUT_DEPTH_READ_ONLY_OPTIMAL : VK_IMAGE_LAYOUT_GENERAL}, {VK_NULL_HANDLE, output, VK_IMAGE_LAYOUT_GENERAL}}; VkWriteDescriptorSet writes[2]{}; for (std::uint32_t binding = 0; binding < 2; ++binding) { writes[binding].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; writes[binding].dstSet = target_sets[mip]; writes[binding].dstBinding = binding; writes[binding].descriptorCount = 1; writes[binding].descriptorType = binding == 0 ? VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE : VK_DESCRIPTOR_TYPE_STORAGE_IMAGE; writes[binding].pImageInfo = &images[binding]; } vkUpdateDescriptorSets(device, 2, writes, 0, nullptr); } } } void make_scene_descriptors_and_pipelines() { std::array graphics{}; for (std::uint32_t i = 0; i < graphics.size(); ++i) graphics[i] = {i, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 1, VK_SHADER_STAGE_VERTEX_BIT, nullptr}; VkDescriptorSetLayoutCreateInfo layout{}; layout.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO; layout.bindingCount = static_cast(graphics.size()); layout.pBindings = graphics.data(); check(vkCreateDescriptorSetLayout(device, &layout, nullptr, &scene.graphics_layout), "Create GPU scene descriptor layout"); std::array cull{}; for (std::uint32_t i = 0; i < cull.size(); ++i) cull[i] = {i, i == 7 || i == 8 ? VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE : VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 1, VK_SHADER_STAGE_COMPUTE_BIT, nullptr}; layout.bindingCount = static_cast(cull.size()); layout.pBindings = cull.data(); check(vkCreateDescriptorSetLayout(device, &layout, nullptr, &scene.cull_layout), "Create GPU cull descriptor layout"); std::array hzb{{ {0, VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE, 1, VK_SHADER_STAGE_COMPUTE_BIT, nullptr}, {1, VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, 1, VK_SHADER_STAGE_COMPUTE_BIT, nullptr}}}; layout.bindingCount = static_cast(hzb.size()); layout.pBindings = hzb.data(); check(vkCreateDescriptorSetLayout(device, &layout, nullptr, &scene.hzb_layout), "Create HZB descriptor layout"); const std::array scene_layouts{ descriptor_layout, lighting_layout, scene.graphics_layout}; VkPushConstantRange graphics_push{VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT, 0, sizeof(ScenePush)}; VkPipelineLayoutCreateInfo pipeline_info{}; pipeline_info.sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO; pipeline_info.setLayoutCount = static_cast(scene_layouts.size()); pipeline_info.pSetLayouts = scene_layouts.data(); pipeline_info.pushConstantRangeCount = 1; pipeline_info.pPushConstantRanges = &graphics_push; check(vkCreatePipelineLayout(device, &pipeline_info, nullptr, &scene.graphics_pipeline_layout), "Create GPU scene pipeline layout"); VkPushConstantRange compute_push{VK_SHADER_STAGE_COMPUTE_BIT, 0, 16}; pipeline_info.setLayoutCount = 1; pipeline_info.pSetLayouts = &scene.cull_layout; pipeline_info.pPushConstantRanges = &compute_push; check(vkCreatePipelineLayout(device, &pipeline_info, nullptr, &scene.cull_pipeline_layout), "Create GPU cull pipeline layout"); pipeline_info.pSetLayouts = &scene.hzb_layout; check(vkCreatePipelineLayout(device, &pipeline_info, nullptr, &scene.hzb_pipeline_layout), "Create HZB pipeline layout"); make_scene_pipelines(); refresh_scene_descriptors(); } void make_scene_pipelines() { const auto gpu_shaders = detail::load_gpu_shader_bundle(shader_directory()); const auto baseline_shaders = detail::load_shader_bundle(shader_directory()); for (std::size_t i = 0; i < gpu_shaders.size(); ++i) if (!scene.shader_layouts[i].empty() && scene.shader_layouts[i] != gpu_shaders[i].layout_fingerprint) throw std::runtime_error("GPU shader layout changed; current pipeline preserved"); VkShaderModule vertex = shader(gpu_shaders[0]); VkShaderModule fragment = shader(baseline_shaders[1]); VkShaderModule main_cull = shader(gpu_shaders[2]); VkShaderModule hzb_shader = scene.hzb_supported ? shader(gpu_shaders[3]) : VK_NULL_HANDLE; VkShaderModule post_cull = shader(gpu_shaders[4]); try { VkPipelineShaderStageCreateInfo stages[2]{}; for (auto& stage : stages) stage.sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO; stages[0].stage = VK_SHADER_STAGE_VERTEX_BIT; stages[0].module = vertex; stages[0].pName = "main"; stages[1].stage = VK_SHADER_STAGE_FRAGMENT_BIT; stages[1].module = fragment; stages[1].pName = "main"; VkVertexInputBindingDescription binding{0, sizeof(SceneVertex), VK_VERTEX_INPUT_RATE_VERTEX}; const std::array attributes{{ {0, 0, VK_FORMAT_R32G32B32_SFLOAT, offsetof(SceneVertex, position)}, {1, 0, VK_FORMAT_R32G32B32_SFLOAT, offsetof(SceneVertex, normal)}, {2, 0, VK_FORMAT_R32G32B32A32_SFLOAT, offsetof(SceneVertex, color)}, {3, 0, VK_FORMAT_R32G32_SFLOAT, offsetof(SceneVertex, uv)}}}; VkPipelineVertexInputStateCreateInfo input{}; input.sType = VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO; input.vertexBindingDescriptionCount = 1; input.pVertexBindingDescriptions = &binding; input.vertexAttributeDescriptionCount = attributes.size(); input.pVertexAttributeDescriptions = attributes.data(); VkPipelineInputAssemblyStateCreateInfo assembly{}; assembly.sType = VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO; assembly.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; VkPipelineViewportStateCreateInfo viewport{}; viewport.sType = VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO; viewport.viewportCount = viewport.scissorCount = 1; VkPipelineRasterizationStateCreateInfo raster{}; raster.sType = VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO; raster.polygonMode = VK_POLYGON_MODE_FILL; raster.cullMode = VK_CULL_MODE_NONE; raster.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE; raster.lineWidth = 1; VkPipelineMultisampleStateCreateInfo samples{}; samples.sType = VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO; samples.rasterizationSamples = VK_SAMPLE_COUNT_1_BIT; VkPipelineDepthStencilStateCreateInfo depth_state{}; depth_state.sType = VK_STRUCTURE_TYPE_PIPELINE_DEPTH_STENCIL_STATE_CREATE_INFO; depth_state.depthTestEnable = VK_TRUE; depth_state.depthWriteEnable = VK_TRUE; depth_state.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.srcAlphaBlendFactor = VK_BLEND_FACTOR_ONE; blend.dstAlphaBlendFactor = VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA; VkPipelineColorBlendStateCreateInfo color_blend{}; color_blend.sType = VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO; color_blend.attachmentCount = 1; color_blend.pAttachments = &blend; const std::array dynamic_states{VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR}; VkPipelineDynamicStateCreateInfo dynamic{}; dynamic.sType = VK_STRUCTURE_TYPE_PIPELINE_DYNAMIC_STATE_CREATE_INFO; dynamic.dynamicStateCount = dynamic_states.size(); dynamic.pDynamicStates = dynamic_states.data(); const VkFormat color_format = VK_FORMAT_R8G8B8A8_UNORM; VkPipelineRenderingCreateInfo rendering{}; rendering.sType = VK_STRUCTURE_TYPE_PIPELINE_RENDERING_CREATE_INFO; rendering.colorAttachmentCount = 1; rendering.pColorAttachmentFormats = &color_format; rendering.depthAttachmentFormat = VK_FORMAT_D32_SFLOAT; VkGraphicsPipelineCreateInfo graphics_pipeline{}; graphics_pipeline.sType = VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO; graphics_pipeline.pNext = &rendering; graphics_pipeline.stageCount = 2; graphics_pipeline.pStages = stages; graphics_pipeline.pVertexInputState = &input; graphics_pipeline.pInputAssemblyState = &assembly; graphics_pipeline.pViewportState = &viewport; graphics_pipeline.pRasterizationState = &raster; graphics_pipeline.pMultisampleState = &samples; graphics_pipeline.pDepthStencilState = &depth_state; graphics_pipeline.pColorBlendState = &color_blend; graphics_pipeline.pDynamicState = &dynamic; graphics_pipeline.layout = scene.graphics_pipeline_layout; check(vkCreateGraphicsPipelines(device, VK_NULL_HANDLE, 1, &graphics_pipeline, nullptr, &scene.graphics_pipeline), "Create GPU scene pipeline"); auto make_compute = [&](VkShaderModule module, VkPipelineLayout layout, VkPipeline& output) { VkComputePipelineCreateInfo info{}; info.sType = VK_STRUCTURE_TYPE_COMPUTE_PIPELINE_CREATE_INFO; info.stage.sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO; info.stage.stage = VK_SHADER_STAGE_COMPUTE_BIT; info.stage.module = module; info.stage.pName = "main"; info.layout = layout; check(vkCreateComputePipelines(device, VK_NULL_HANDLE, 1, &info, nullptr, &output), "Create GPU visibility compute pipeline"); }; make_compute(main_cull, scene.cull_pipeline_layout, scene.cull_pipeline); make_compute(post_cull, scene.cull_pipeline_layout, scene.post_pipeline); if (scene.hzb_supported) make_compute(hzb_shader, scene.hzb_pipeline_layout, scene.hzb_pipeline); for (std::size_t i = 0; i < gpu_shaders.size(); ++i) scene.shader_layouts[i] = gpu_shaders[i].layout_fingerprint; } catch (...) { for (auto module : {vertex, fragment, main_cull, hzb_shader, post_cull}) vkDestroyShaderModule(device, module, nullptr); throw; } for (auto module : {vertex, fragment, main_cull, hzb_shader, post_cull}) vkDestroyShaderModule(device, module, nullptr); } void make_temporal_interfaces_and_pipelines() { // Validate the complete shader package even on devices that fall back to Off. const auto post = detail::load_temporal_shader_bundle(shader_directory()); const auto raster = detail::load_temporal_scene_shader_bundle(shader_directory()); for (std::size_t i = 0; i < post.size(); ++i) if (!temporal.shader_layouts[i].empty() && temporal.shader_layouts[i] != post[i].layout_fingerprint) throw std::runtime_error("Temporal shader layout changed; active pipeline preserved"); for (std::size_t i = 0; i < raster.size(); ++i) if (!temporal.scene_shader_layouts[i].empty() && temporal.scene_shader_layouts[i] != raster[i].layout_fingerprint) throw std::runtime_error("Temporal scene layout changed; active pipeline preserved"); if (!temporal.capabilities.compute || !temporal.capabilities.formats) return; if (!temporal.resolve_layout) { std::array bindings{}; for (std::uint32_t i = 0; i < bindings.size(); ++i) bindings[i] = {i, i < 5 ? VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE : i < 7 ? VK_DESCRIPTOR_TYPE_STORAGE_IMAGE : VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 1, VK_SHADER_STAGE_COMPUTE_BIT, nullptr}; VkDescriptorSetLayoutCreateInfo descriptor_info{}; descriptor_info.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO; descriptor_info.bindingCount = static_cast(bindings.size()); descriptor_info.pBindings = bindings.data(); check(vkCreateDescriptorSetLayout(device, &descriptor_info, nullptr, &temporal.resolve_layout), "Create temporal resolve descriptor layout"); bindings[0].stageFlags = VK_SHADER_STAGE_FRAGMENT_BIT; descriptor_info.bindingCount = 1; check(vkCreateDescriptorSetLayout(device, &descriptor_info, nullptr, &temporal.composite_layout), "Create temporal composite descriptor layout"); VkPushConstantRange push{VK_SHADER_STAGE_COMPUTE_BIT, 0, 80}; VkPipelineLayoutCreateInfo layout_info{}; layout_info.sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO; layout_info.setLayoutCount = 1; layout_info.pSetLayouts = &temporal.resolve_layout; layout_info.pushConstantRangeCount = 1; layout_info.pPushConstantRanges = &push; check(vkCreatePipelineLayout(device, &layout_info, nullptr, &temporal.resolve_pipeline_layout), "Create temporal resolve pipeline layout"); layout_info.pSetLayouts = &temporal.composite_layout; layout_info.pushConstantRangeCount = 0; check(vkCreatePipelineLayout(device, &layout_info, nullptr, &temporal.composite_pipeline_layout), "Create temporal composite pipeline layout"); } std::array modules{}; try { modules[0] = shader(post[0]); modules[1] = shader(post[1]); modules[2] = shader(post[2]); modules[3] = shader(raster[0]); modules[4] = shader(raster[1]); modules[5] = shader(raster[2]); VkComputePipelineCreateInfo compute{}; compute.sType = VK_STRUCTURE_TYPE_COMPUTE_PIPELINE_CREATE_INFO; compute.stage.sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO; compute.stage.stage = VK_SHADER_STAGE_COMPUTE_BIT; compute.stage.module = modules[0]; compute.stage.pName = "main"; compute.layout = temporal.resolve_pipeline_layout; check(vkCreateComputePipelines(device, VK_NULL_HANDLE, 1, &compute, nullptr, &temporal.resolve_pipeline), "Create temporal resolve pipeline"); const auto make_graphics = [&](VkShaderModule vertex, VkShaderModule fragment, VkPipelineLayout layout, bool gpu, bool composite, bool depth_write, VkPipeline& output) { std::array stages{}; for (auto& stage : stages) { stage.sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO; stage.pName = "main"; } stages[0].stage = VK_SHADER_STAGE_VERTEX_BIT; stages[0].module = vertex; stages[1].stage = VK_SHADER_STAGE_FRAGMENT_BIT; stages[1].module = fragment; const VkVertexInputBindingDescription binding{ 0, static_cast(gpu ? sizeof(SceneVertex) : sizeof(GpuVertex)), VK_VERTEX_INPUT_RATE_VERTEX}; const std::array direct_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)}, {6, 0, VK_FORMAT_R32G32B32A32_SFLOAT, offsetof(GpuVertex, previous_clip)}, {7, 0, VK_FORMAT_R32_SFLOAT, offsetof(GpuVertex, motion_valid)}}}; const std::array gpu_attrs{{ {0, 0, VK_FORMAT_R32G32B32_SFLOAT, offsetof(SceneVertex, position)}, {1, 0, VK_FORMAT_R32G32B32_SFLOAT, offsetof(SceneVertex, normal)}, {2, 0, VK_FORMAT_R32G32B32A32_SFLOAT, offsetof(SceneVertex, color)}, {3, 0, VK_FORMAT_R32G32_SFLOAT, offsetof(SceneVertex, uv)}}}; VkPipelineVertexInputStateCreateInfo input{}; input.sType = VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO; input.vertexBindingDescriptionCount = 1; input.pVertexBindingDescriptions = &binding; input.vertexAttributeDescriptionCount = composite ? 1 : gpu ? static_cast(gpu_attrs.size()) : static_cast(direct_attrs.size()); input.pVertexAttributeDescriptions = gpu ? gpu_attrs.data() : direct_attrs.data(); VkPipelineInputAssemblyStateCreateInfo assembly{}; assembly.sType = VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO; assembly.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; VkPipelineViewportStateCreateInfo viewport{}; viewport.sType = VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO; viewport.viewportCount = viewport.scissorCount = 1; VkPipelineRasterizationStateCreateInfo raster_state{}; raster_state.sType = VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO; raster_state.polygonMode = VK_POLYGON_MODE_FILL; raster_state.cullMode = VK_CULL_MODE_NONE; raster_state.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE; raster_state.lineWidth = 1; VkPipelineMultisampleStateCreateInfo samples{}; samples.sType = VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO; samples.rasterizationSamples = VK_SAMPLE_COUNT_1_BIT; VkPipelineDepthStencilStateCreateInfo depth_state{}; depth_state.sType = VK_STRUCTURE_TYPE_PIPELINE_DEPTH_STENCIL_STATE_CREATE_INFO; depth_state.depthTestEnable = !composite; depth_state.depthWriteEnable = !composite && depth_write; depth_state.depthCompareOp = VK_COMPARE_OP_LESS_OR_EQUAL; VkPipelineColorBlendAttachmentState color_blend{}; color_blend.colorWriteMask = 15; color_blend.blendEnable = !composite; color_blend.srcColorBlendFactor = VK_BLEND_FACTOR_SRC_ALPHA; color_blend.dstColorBlendFactor = VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA; color_blend.colorBlendOp = VK_BLEND_OP_ADD; color_blend.srcAlphaBlendFactor = VK_BLEND_FACTOR_ONE; color_blend.dstAlphaBlendFactor = VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA; color_blend.alphaBlendOp = VK_BLEND_OP_ADD; VkPipelineColorBlendAttachmentState velocity_blend{}; velocity_blend.colorWriteMask = 15; const std::array blends{ color_blend, velocity_blend}; VkPipelineColorBlendStateCreateInfo blend_state{}; blend_state.sType = VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO; blend_state.attachmentCount = composite ? 1 : 2; blend_state.pAttachments = blends.data(); const std::array states{VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR}; VkPipelineDynamicStateCreateInfo dynamic{}; dynamic.sType = VK_STRUCTURE_TYPE_PIPELINE_DYNAMIC_STATE_CREATE_INFO; dynamic.dynamicStateCount = static_cast(states.size()); dynamic.pDynamicStates = states.data(); const std::array formats{VK_FORMAT_R8G8B8A8_UNORM, VK_FORMAT_R16G16B16A16_SFLOAT}; VkPipelineRenderingCreateInfo rendering{}; rendering.sType = VK_STRUCTURE_TYPE_PIPELINE_RENDERING_CREATE_INFO; rendering.colorAttachmentCount = composite ? 1 : 2; rendering.pColorAttachmentFormats = formats.data(); rendering.depthAttachmentFormat = composite ? VK_FORMAT_UNDEFINED : VK_FORMAT_D32_SFLOAT; VkGraphicsPipelineCreateInfo info{}; info.sType = VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO; info.pNext = &rendering; info.stageCount = 2; info.pStages = stages.data(); info.pVertexInputState = &input; info.pInputAssemblyState = &assembly; info.pViewportState = &viewport; info.pRasterizationState = &raster_state; info.pMultisampleState = &samples; info.pDepthStencilState = &depth_state; info.pColorBlendState = &blend_state; info.pDynamicState = &dynamic; info.layout = layout; check(vkCreateGraphicsPipelines(device, VK_NULL_HANDLE, 1, &info, nullptr, &output), "Create temporal graphics pipeline"); }; make_graphics(modules[1], modules[2], temporal.composite_pipeline_layout, false, true, false, temporal.composite_pipeline); make_graphics(modules[3], modules[4], pipeline_layout, false, false, true, temporal.direct_pipeline); make_graphics(modules[3], modules[4], pipeline_layout, false, false, false, temporal.transparent_pipeline); if (scene.graphics_pipeline_layout) make_graphics(modules[5], modules[4], scene.graphics_pipeline_layout, true, false, true, temporal.gpu_pipeline); for (std::size_t i = 0; i < post.size(); ++i) temporal.shader_layouts[i] = post[i].layout_fingerprint; for (std::size_t i = 0; i < raster.size(); ++i) temporal.scene_shader_layouts[i] = raster[i].layout_fingerprint; } catch (...) { for (auto module : modules) if (module) vkDestroyShaderModule(device, module, nullptr); throw; } for (auto module : modules) vkDestroyShaderModule(device, module, nullptr); if (!temporal.descriptor_pool) refresh_temporal_descriptors(); } GpuVertex gpu_vertex(const Vertex& v, const DrawItem& item, const Mat4& vp, const Mat4* previous_model = nullptr, const Mat4* previous_vp = nullptr) { 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); if (previous_model && previous_vp) { const auto previous_world = point(*previous_model, {v.position[0], v.position[1], v.position[2], 1}); const auto previous_clip = point(*previous_vp, previous_world); std::copy(previous_clip.begin(), previous_clip.end(), out.previous_clip); out.motion_valid = 1.f; } 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 upload_scene_buffer(Buffer& buffer, const void* bytes, std::size_t count, VkBufferUsageFlags usage) { const VkDeviceSize required = std::max(16, count); if (required > max_storage_buffer_range) throw std::overflow_error("GPU scene buffer exceeds maxStorageBufferRange"); if (buffer.size < required) { destroy(buffer); buffer = make_buffer(required, usage | VK_BUFFER_USAGE_STORAGE_BUFFER_BIT, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT, VK_MEMORY_PROPERTY_HOST_CACHED_BIT); } if (count && bytes) { void* mapped{}; check(vkMapMemory(device, buffer.memory, 0, buffer.size, 0, &mapped), "Map GPU scene buffer"); std::memcpy(mapped, bytes, count); vkUnmapMemory(device, buffer.memory); } } void reserve_scene_output(Buffer& buffer, std::size_t count, VkBufferUsageFlags usage = 0) { const VkDeviceSize required = std::max(16, count); if (required > max_storage_buffer_range) throw std::overflow_error("GPU scene buffer exceeds maxStorageBufferRange"); if (buffer.size < required) { destroy(buffer); buffer = make_buffer(required, usage | VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT | VK_BUFFER_USAGE_TRANSFER_SRC_BIT, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT); } } template void upload_scene_vector(Buffer& buffer, const std::vector& values, VkBufferUsageFlags usage = 0) { upload_scene_buffer(buffer, values.data(), values.size() * sizeof(T), usage); } void update_lighting_descriptors() { const std::array buffers{{ {lighting_header.handle, 0, lighting_header.size}, {lighting_locals.handle, 0, lighting_locals.size}, {lighting_views.handle, 0, lighting_views.size}, {light_tile_words.handle, 0, light_tile_words.size}}}; const VkDescriptorImageInfo atlas{VK_NULL_HANDLE, local_shadow.handle ? local_shadow.view : shadow.view, VK_IMAGE_LAYOUT_DEPTH_READ_ONLY_OPTIMAL}; std::array writes{}; for (std::uint32_t i = 0; i < writes.size(); ++i) { writes[i].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; writes[i].dstSet = lighting_set; writes[i].dstBinding = i; writes[i].descriptorCount = 1; writes[i].descriptorType = i == 3 ? VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE : VK_DESCRIPTOR_TYPE_STORAGE_BUFFER; if (i == 3) writes[i].pImageInfo = &atlas; else writes[i].pBufferInfo = &buffers[i == 4 ? 3 : i]; } vkUpdateDescriptorSets(device, static_cast(writes.size()), writes.data(), 0, nullptr); } void update_light_tile_descriptors() { if (!light_tile_set) return; const std::array buffers{{ {lighting_locals.handle, 0, lighting_locals.size}, {light_tile_words.handle, 0, light_tile_words.size}}}; std::array writes{}; for (std::uint32_t i = 0; i < writes.size(); ++i) { writes[i].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; writes[i].dstSet = light_tile_set; writes[i].dstBinding = i; writes[i].descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER; writes[i].descriptorCount = 1; writes[i].pBufferInfo = &buffers[i]; } vkUpdateDescriptorSets(device, static_cast(writes.size()), writes.data(), 0, nullptr); } void update_scene_descriptors(bool occlusion) { auto write_buffers = [&](VkDescriptorSet set, std::span buffers, std::uint32_t first_binding) { std::vector infos(buffers.size()); std::vector writes(buffers.size()); for (std::size_t i = 0; i < buffers.size(); ++i) { infos[i] = {buffers[i]->handle, 0, buffers[i]->size}; writes[i].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; writes[i].dstSet = set; writes[i].dstBinding = first_binding + static_cast(i); writes[i].descriptorCount = 1; writes[i].descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER; writes[i].pBufferInfo = &infos[i]; } vkUpdateDescriptorSets(device, static_cast(writes.size()), writes.data(), 0, nullptr); }; const std::array main_graphics{&scene.instances, &scene.main_ids, &scene.view}; const std::array post_graphics{&scene.instances, &scene.post_ids, &scene.view}; write_buffers(scene.graphics_main, main_graphics, 0); write_buffers(scene.graphics_post, post_graphics, 0); const std::array main_cull{ &scene.instances, &scene.candidates, &scene.bins, &scene.main_ids, &scene.main_args, &scene.deferred_ids, &scene.deferred_count}; const std::array post_cull{ &scene.instances, &scene.candidates, &scene.bins, &scene.post_ids, &scene.post_args, &scene.deferred_ids, &scene.deferred_count}; write_buffers(scene.cull_main, main_cull, 0); write_buffers(scene.cull_post, post_cull, 0); for (auto set : {scene.cull_main, scene.cull_post}) { const Image* previous = occlusion ? &scene.hzb[1 - scene.hzb_current] : &shadow; const Image* current = occlusion ? &scene.hzb[scene.hzb_current] : &shadow; const VkImageLayout previous_layout = occlusion ? VK_IMAGE_LAYOUT_GENERAL : VK_IMAGE_LAYOUT_DEPTH_READ_ONLY_OPTIMAL; const VkImageLayout current_layout = previous == &shadow ? VK_IMAGE_LAYOUT_DEPTH_READ_ONLY_OPTIMAL : VK_IMAGE_LAYOUT_GENERAL; VkDescriptorImageInfo images[] = { {VK_NULL_HANDLE, previous->view, previous_layout}, {VK_NULL_HANDLE, current->view, current_layout}}; VkWriteDescriptorSet writes[2]{}; for (std::uint32_t i = 0; i < 2; ++i) { writes[i].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; writes[i].dstSet = set; writes[i].dstBinding = 7 + i; writes[i].descriptorCount = 1; writes[i].descriptorType = VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE; writes[i].pImageInfo = &images[i]; } vkUpdateDescriptorSets(device, 2, writes, 0, nullptr); const std::array view{&scene.view}; write_buffers(set, view, 9); } } void scene_barrier(VkPipelineStageFlags2 source_stages, VkAccessFlags2 source_access, VkPipelineStageFlags2 destination_stages, VkAccessFlags2 destination_access) { VkMemoryBarrier2 barrier{}; barrier.sType = VK_STRUCTURE_TYPE_MEMORY_BARRIER_2; barrier.srcStageMask = source_stages; barrier.srcAccessMask = source_access; barrier.dstStageMask = destination_stages; barrier.dstAccessMask = destination_access; VkDependencyInfo dependency{}; dependency.sType = VK_STRUCTURE_TYPE_DEPENDENCY_INFO; dependency.memoryBarrierCount = 1; dependency.pMemoryBarriers = &barrier; vkCmdPipelineBarrier2(command, &dependency); } Bounds world_bounds(const std::shared_ptr& mesh, const Mat4& model) { auto found = bounds_cache.find(mesh.get()); if (found == bounds_cache.end() || found->second.owner.lock() != mesh) { auto local = local_bounds(*mesh); found = bounds_cache.insert_or_assign(mesh.get(), CachedBounds{mesh, local}).first; } return transformed_bounds(found->second.local, model); } bool is_opaque(const std::shared_ptr& texture) { if (!texture) return true; auto found = opacity_cache.find(texture.get()); if (found == opacity_cache.end() || found->second.owner.lock() != texture || found->second.revision != texture->revision) { const bool opaque = opaque_texture(texture.get()); found = opacity_cache.insert_or_assign( texture.get(), CachedOpacity{texture, texture->revision, opaque}).first; } return found->second.opaque; } void render(const Snapshot& snapshot) { auto start = std::chrono::steady_clock::now(); statistics.draw_calls = statistics.culled_meshes = statistics.gpu_label_count = 0; statistics.submitted_local_lights = statistics.omitted_local_lights = 0; statistics.requested_sun_cascades = statistics.effective_sun_cascades = statistics.sun_shadow_caster_draws = 0; statistics.sun_shadow_atlas_bytes = sun_shadow_size ? shadow.allocation_size : 0; statistics.gpu_sun_shadow_ms = 0; statistics.requested_local_shadow_faces = statistics.local_shadow_faces = statistics.local_shadow_tiles = statistics.dropped_shadow_faces = statistics.dropped_point_shadow_faces = statistics.shadow_atlas_full_drops = statistics.shadow_caster_budget_drops = statistics.shadow_unavailable_drops = statistics.shadow_caster_draws = 0; statistics.local_shadow_atlas_bytes = local_shadow_size ? local_shadow.allocation_size : 0; statistics.gpu_local_shadow_ms = 0; statistics.gpu_light_tiles_ms = 0; statistics.light_tile_count = 0; statistics.light_tile_counts_valid = false; statistics.light_tile_candidate_count = statistics.light_tile_overflow_count = 0; statistics.effective_lighting_path = "forward"; statistics.gpu_bins = statistics.gpu_visible_instances = statistics.gpu_frustum_rejected = statistics.gpu_occlusion_deferred = statistics.gpu_post_visible = 0; statistics.lod_counts = {}; statistics.visibility_counters_valid = false; statistics.requested_temporal_mode = config.temporal_mode; statistics.temporal_history_valid = false; statistics.temporal_valid_motion_instances = 0; statistics.temporal_jitter = {}; statistics.temporal_counters_valid = false; statistics.temporal_accepted_pixels = statistics.temporal_rejected_pixels = 0; statistics.gpu_temporal_resolve_ms = statistics.gpu_temporal_composite_ms = statistics.gpu_ui_ms = 0; statistics.graph_passes.clear(); for (auto it = bounds_cache.begin(); it != bounds_cache.end();) it = it->second.owner.expired() ? bounds_cache.erase(it) : std::next(it); for (auto it = opacity_cache.begin(); it != opacity_cache.end();) it = it->second.owner.expired() ? opacity_cache.erase(it) : std::next(it); 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(); } // A swapchain resize can recreate temporal targets and destroy their // diagnostic staging buffer. Decide the actual paths and allocate the // readback only after that resource lifetime boundary. statistics.effective_temporal_mode = select_effective_temporal_mode( config.temporal_mode, temporal.capabilities); statistics.temporal_fallback_reason = temporal_fallback_reason( config.temporal_mode, temporal.capabilities); statistics.temporal_internal_width = temporal.internal_width; statistics.temporal_internal_height = temporal.internal_height; const bool temporal_active = statistics.effective_temporal_mode != TemporalMode::Off; statistics.requested_visibility_mode = config.visibility_mode; statistics.effective_visibility_mode = select_effective_visibility_mode( config.visibility_mode, scene.available, scene.hzb_supported && scene.hzb_mips); const bool gpu_active = statistics.effective_visibility_mode != VisibilityMode::Direct; const bool occlusion = statistics.effective_visibility_mode == VisibilityMode::GpuOcclusion; statistics.gpu_visibility_active = gpu_active; statistics.hzb_valid = false; bool collect_temporal_counts = temporal_active && config.temporal_diagnostics; if (collect_temporal_counts && !temporal.pixel_counts_stage.handle) { try { temporal.pixel_counts_stage = make_buffer(2 * sizeof(std::uint32_t), VK_BUFFER_USAGE_TRANSFER_DST_BIT, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT, VK_MEMORY_PROPERTY_HOST_CACHED_BIT); } catch (const std::exception&) { collect_temporal_counts = false; } } // 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::array scene_viewport{0, 0, float(width), float(height)}; if (snapshot.scene_rect[2] > 0 && snapshot.scene_rect[3] > 0) { const auto& rect = snapshot.scene_rect; const float x = std::clamp(rect[0], 0.f, float(width)); const float y = std::clamp(rect[1], 0.f, float(height)); scene_viewport = {x, y, std::max(0.f, std::min(rect[2], float(width) - x)), std::max(0.f, std::min(rect[3], float(height) - y))}; } const auto output_scene_viewport = scene_viewport; if (temporal_active) { const float sx = float(temporal.internal_width) / float(width); const float sy = float(temporal.internal_height) / float(height); scene_viewport = {scene_viewport[0] * sx, scene_viewport[1] * sy, scene_viewport[2] * sx, scene_viewport[3] * sy}; statistics.temporal_jitter = temporal_jitter( statistics.frame, std::max(1u, static_cast(std::ceil(scene_viewport[2]))), std::max(1u, static_cast(std::ceil(scene_viewport[3])))); } Mat4 raster_vp = snapshot.view_projection; if (temporal_active) for (int column = 0; column < 4; ++column) { raster_vp[column * 4] += statistics.temporal_jitter[0] * raster_vp[column * 4 + 3]; raster_vp[column * 4 + 1] += statistics.temporal_jitter[1] * raster_vp[column * 4 + 3]; } const auto raster_width = temporal_active ? temporal.internal_width : width; const auto raster_height = temporal_active ? temporal.internal_height : height; const std::string view_id = snapshot.view_id.empty() ? "default" : snapshot.view_id; TemporalHistoryKey temporal_key; temporal_key.view_id = view_id; temporal_key.output_width = width; temporal_key.output_height = height; temporal_key.internal_width = temporal.internal_width; temporal_key.internal_height = temporal.internal_height; temporal_key.scene_rect = output_scene_viewport; temporal_key.projection = snapshot.projection; temporal_key.view_projection = snapshot.view_projection; temporal_key.camera_eye = snapshot.eye; temporal_key.mode = statistics.effective_temporal_mode; temporal_key.render_scale = temporal_active ? config.render_scale : 1.f; temporal_key.shader_generation = temporal.shader_generation; temporal_key.camera_cut = snapshot.camera_cut; auto temporal_decision = temporal.history.prepare(temporal_key); if (temporal_active && temporal_decision.valid && !temporal.has_completed_image) temporal_decision = {false, TemporalResetReason::FirstFrame}; statistics.temporal_history_valid = temporal_active && temporal_decision.valid; statistics.temporal_reset_reason = temporal_active ? temporal_decision.reason : statistics.temporal_fallback_reason == TemporalFallbackReason::None ? temporal_decision.reason : TemporalResetReason::Unsupported; const bool history_compatible = occlusion && scene.hzb_history_valid && !snapshot.camera_cut && scene.previous_view_id == view_id && scene.previous_viewport == scene_viewport && scene.previous_projection == snapshot.projection; if (occlusion) scene.hzb_current = history_compatible ? 1 - scene.hzb_current : 0; PreparedScene gpu_frame; gpu_frame.view.current_vp = raster_vp; gpu_frame.view.previous_vp = scene.previous_vp; gpu_frame.view.viewport = scene_viewport; gpu_frame.view.previous_viewport = scene.previous_viewport; if (scene.hzb_mips) { const auto dimensions = std::array{ std::bit_ceil(temporal.internal_width), std::bit_ceil(temporal.internal_height), scene.hzb_mips, 0}; gpu_frame.view.hzb_dimensions = dimensions; gpu_frame.view.previous_hzb_dimensions = dimensions; } gpu_frame.view.flags[0] = history_compatible ? 1 : 0; statistics.hzb_valid = history_compatible; struct SelectedDraw { const DrawItem* source; std::shared_ptr mesh; bool gpu{}; bool opaque{}; bool motion_valid{}; Mat4 previous_model{identity}; }; std::vector selected_draws; selected_draws.reserve(snapshot.draws.size()); std::vector shadow_casters; shadow_casters.reserve(snapshot.draws.size()); struct BuildingBin { const Mesh* mesh{}; const Texture* texture{}; std::uint32_t first_vertex{}, vertex_count{}; std::vector instances; }; std::vector building_bins; std::unordered_map> mesh_ranges; std::unordered_map current_lods; for (std::size_t source_index = 0; source_index < snapshot.draws.size(); ++source_index) { const auto& item = snapshot.draws[source_index]; if (!item.mesh || item.mesh->vertices.empty()) continue; const auto source_bounds = world_bounds(item.mesh, item.model); if (item.cast_shadow) { if (source_index > UINT32_MAX) throw std::overflow_error("Shadow source draw index exceeds 32-bit capacity"); shadow_casters.push_back( {source_bounds, static_cast(source_index)}); } std::vector thresholds; std::vector available; std::shared_ptr selected_mesh = item.mesh; std::size_t lod = 0; if (!item.lod_meshes.empty()) { available.push_back(1); for (std::size_t level = 0; level < item.lod_meshes.size(); ++level) { thresholds.push_back(192.f / float(std::uint32_t(1) << std::min(level, 20))); available.push_back(item.lod_meshes[level] && !item.lod_meshes[level]->vertices.empty() ? 1 : 0); } auto previous = previous_lods.find(item.instance_key); lod = select_lod(projected_pixels(source_bounds, snapshot.view_projection, scene_viewport), previous != previous_lods.end() && !item.instance_key.empty() ? previous->second : SIZE_MAX, thresholds, .12f, available); if (lod) selected_mesh = item.lod_meshes[lod - 1]; } statistics.lod_counts[std::min(lod, 3)]++; if (!item.instance_key.empty()) current_lods[item.instance_key] = lod; const auto bounds = selected_mesh == item.mesh ? source_bounds : world_bounds(selected_mesh, item.model); InstanceUpdate previous{}; if (!item.instance_key.empty()) previous = instance_tracker.update(item.instance_key, selected_mesh, item.model, bounds, view_id); const bool opaque = item.color[3] >= 1.f && is_opaque(item.texture); const bool eligible = gpu_active && opaque; const bool motion_valid = temporal_active && temporal_decision.valid && previous.previous_valid && opaque; if (motion_valid) ++statistics.temporal_valid_motion_instances; selected_draws.push_back({&item, selected_mesh, eligible, opaque, motion_valid, previous.previous_valid ? previous.previous_model : item.model}); if (!eligible) continue; auto [range_it, inserted] = mesh_ranges.try_emplace(selected_mesh.get()); if (inserted) { const std::size_t first = gpu_frame.vertices.size(); auto emit = [&](std::uint32_t index) { if (index >= selected_mesh->vertices.size()) throw std::out_of_range("Mesh index outside vertex range"); const auto& v = selected_mesh->vertices[index]; SceneVertex gpu_vertex{}; std::copy(v.position.begin(), v.position.end(), gpu_vertex.position); std::copy(v.normal.begin(), v.normal.end(), gpu_vertex.normal); std::copy(v.color.begin(), v.color.end(), gpu_vertex.color); std::copy(v.uv.begin(), v.uv.end(), gpu_vertex.uv); gpu_frame.vertices.push_back(gpu_vertex); }; if (selected_mesh->indices.empty()) for (std::uint32_t i = 0; i < selected_mesh->vertices.size(); ++i) emit(i); else for (auto i : selected_mesh->indices) emit(i); const auto count = gpu_frame.vertices.size() - first; if (count % 3 || first > UINT32_MAX || count > UINT32_MAX) throw std::invalid_argument("GPU scene mesh must fit complete triangles"); range_it->second = {static_cast(first), static_cast(count)}; } if (!range_it->second.second) continue; auto bin_it = std::find_if(building_bins.begin(), building_bins.end(), [&](const BuildingBin& bin) { return bin.mesh == selected_mesh.get() && bin.texture == (item.texture ? item.texture.get() : white.get()); }); if (bin_it == building_bins.end()) { const auto [first, count] = range_it->second; building_bins.push_back({selected_mesh.get(), item.texture ? item.texture.get() : white.get(), first, count, {}}); bin_it = std::prev(building_bins.end()); } 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]}; }; const auto ca = cross(b, c), cb = cross(c, a), cc = cross(a, b); const float determinant = a[0] * ca[0] + a[1] * ca[1] + a[2] * ca[2]; const float sign = determinant < 0 ? -1.f : 1.f; SceneInstance instance{}; instance.model = m; for (int row = 0; row < 3; ++row) { auto& normal = row == 0 ? instance.normal0 : row == 1 ? instance.normal1 : instance.normal2; normal = {ca[row] * sign, cb[row] * sign, cc[row] * sign, 0}; } instance.color = item.color; instance.material = {item.roughness, item.metallic, 0, 0}; for (int axis = 0; axis < 3; ++axis) { instance.center_extent[axis] = (bounds.min[axis] + bounds.max[axis]) * .5f; instance.half_extent[axis] = (bounds.max[axis] - bounds.min[axis]) * .5f; instance.previous_center_extent[axis] = (previous.previous_bounds.min[axis] + previous.previous_bounds.max[axis]) * .5f; instance.previous_half_extent[axis] = (previous.previous_bounds.max[axis] - previous.previous_bounds.min[axis]) * .5f; } instance.metadata = gpu_instance_metadata(previous, history_compatible, temporal_decision.valid); instance.previous_model = previous.previous_valid ? previous.previous_model : item.model; if (gpu_frame.instances.size() >= UINT32_MAX) throw std::overflow_error("GPU scene instance capacity exceeded"); bin_it->instances.push_back(static_cast(gpu_frame.instances.size())); gpu_frame.instances.push_back(instance); } previous_lods = std::move(current_lods); for (const auto& bin : building_bins) { if (gpu_frame.candidates.size() > UINT32_MAX - bin.instances.size()) throw std::overflow_error("GPU scene candidate capacity exceeded"); const auto first_candidate = static_cast(gpu_frame.candidates.size()); const auto visible_base = first_candidate; const auto bin_index = static_cast(gpu_frame.bins.size()); for (auto id : bin.instances) gpu_frame.candidates.push_back({id, bin_index, 0, 0}); gpu_frame.bins.push_back({first_candidate, static_cast(bin.instances.size()), visible_base, static_cast(bin.instances.size())}); // gpuVertexMain/gpuShadowMain read raw Vulkan InstanceIndex. // Keep firstInstance at zero; visible_base is supplied separately. gpu_frame.commands.push_back({bin.vertex_count, 0, bin.first_vertex, 0}); gpu_frame.textures.push_back(bin.texture); } gpu_frame.candidate_count = static_cast(gpu_frame.candidates.size()); ShadowBudget shadow_budget; shadow_budget.sun_atlas_size = sun_shadow_size; shadow_budget.sun_atlas_available = sun_shadow_size != 0; shadow_budget.local_atlas_size = local_shadow_size; shadow_budget.local_atlas_available = local_shadow_size != 0; const auto shadow_plan = build_shadow_plan(snapshot, shadow_casters, shadow_budget); const bool sun_raster = sun_shadow_size && std::any_of(shadow_plan.sun_views.begin(), shadow_plan.sun_views.end(), [](const ShadowView& view) { return view.valid && !view.caster_indices.empty(); }); statistics.requested_sun_cascades = shadow_plan.requested_sun_cascades; statistics.effective_sun_cascades = sun_raster ? shadow_plan.effective_sun_cascades : 0; if (sun_raster) for (const auto& view : shadow_plan.sun_views) if (view.valid) statistics.sun_shadow_caster_draws += static_cast(view.caster_indices.size()); const bool local_raster = local_shadow_size && std::any_of(shadow_plan.local_views.begin(), shadow_plan.local_views.end(), [](const ShadowView& view) { return view.valid && !view.caster_indices.empty(); }); statistics.requested_local_shadow_faces = shadow_plan.local_faces_requested; statistics.local_shadow_tiles = shadow_plan.local_faces_used; statistics.local_shadow_faces = local_raster ? shadow_plan.local_faces_used : 0; statistics.dropped_shadow_faces = shadow_plan.dropped_local_faces; statistics.dropped_point_shadow_faces = shadow_plan.dropped_point_faces; for (const auto& assignment : shadow_plan.local_assignments) { if (assignment.valid || assignment.reason == ShadowDropReason::None) continue; const auto& light = snapshot.local_lights[assignment.source_index]; const auto faces = light.kind == LocalLight::Kind::Point ? 6u : 1u; if (assignment.reason == ShadowDropReason::TileBudget) statistics.shadow_atlas_full_drops += faces; else if (assignment.reason == ShadowDropReason::CasterBudget) statistics.shadow_caster_budget_drops += faces; else if (assignment.reason == ShadowDropReason::Unavailable) statistics.shadow_unavailable_drops += faces; } for (const auto& view : shadow_plan.sun_views) { if (view.reason == ShadowDropReason::CasterBudget) ++statistics.shadow_caster_budget_drops; else if (view.reason == ShadowDropReason::Unavailable) ++statistics.shadow_unavailable_drops; } statistics.shadow_caster_draws = statistics.sun_shadow_caster_draws; if (local_raster) for (const auto& view : shadow_plan.local_views) statistics.shadow_caster_draws += static_cast(view.caster_indices.size()); std::vector data; std::vector scene_batches, transparent_batches, sprite_batches, ui_batches; for (const auto& selected : selected_draws) { const auto& item = *selected.source; if (selected.gpu) continue; auto first = data.size(); const auto& mesh = *selected.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, raster_vp, selected.motion_valid ? &selected.previous_model : nullptr, selected.motion_valid ? &scene.previous_vp : nullptr)); }; 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 (outside(data, first)) ++statistics.culled_meshes; else if (!selected.opaque && (gpu_active || temporal_active)) transparent_batches.push_back(batch); 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(raster_vp, {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}); } const auto composite_first = static_cast(data.size()); if (temporal_active) for (const auto xy : {Vec2{-1, -1}, Vec2{3, -1}, Vec2{-1, 3}}) { GpuVertex vertex{}; vertex.clip[0] = xy[0]; vertex.clip[1] = xy[1]; vertex.clip[3] = 1; data.push_back(vertex); } std::vector shadow_batch_by_source(snapshot.draws.size()); if (sun_raster || local_raster) { std::vector required(snapshot.draws.size()); for (const auto& view : shadow_plan.sun_views) if (sun_raster && view.valid) for (const auto source : view.caster_indices) required.at(source) = 1; for (const auto& view : shadow_plan.local_views) if (local_raster && view.valid) for (const auto source : view.caster_indices) required.at(source) = 1; for (std::size_t source = 0; source < required.size(); ++source) { if (!required[source]) continue; const auto& item = snapshot.draws[source]; if (!item.mesh) continue; const auto first = data.size(); const auto& mesh = *item.mesh; // Source LOD 0, independent of camera/P2 LOD. auto emit = [&](std::uint32_t index) { if (index >= mesh.vertices.size()) throw std::out_of_range("Shadow 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 (const auto index : mesh.indices) emit(index); const auto count = data.size() - first; if (count % 3 || first > UINT32_MAX || count > UINT32_MAX) throw std::invalid_argument("Shadow mesh must fit complete triangles"); shadow_batch_by_source[source] = {static_cast(first), static_cast(count), white.get()}; } } statistics.vertices = static_cast(data.size() + gpu_frame.vertices.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); if (gpu_active) { statistics.gpu_bins = static_cast(gpu_frame.bins.size()); upload_scene_vector(scene.vertices, gpu_frame.vertices, VK_BUFFER_USAGE_VERTEX_BUFFER_BIT); upload_scene_vector(scene.instances, gpu_frame.instances); upload_scene_vector(scene.candidates, gpu_frame.candidates); upload_scene_vector(scene.bins, gpu_frame.bins); upload_scene_buffer(scene.view, &gpu_frame.view, sizeof(gpu_frame.view), 0); reserve_scene_output(scene.main_ids, gpu_frame.candidate_count * sizeof(std::uint32_t)); reserve_scene_output(scene.post_ids, gpu_frame.candidate_count * sizeof(std::uint32_t)); reserve_scene_output(scene.main_args, gpu_frame.commands.size() * sizeof(SceneIndirect), VK_BUFFER_USAGE_INDIRECT_BUFFER_BIT); reserve_scene_output(scene.post_args, gpu_frame.commands.size() * sizeof(SceneIndirect), VK_BUFFER_USAGE_INDIRECT_BUFFER_BIT); reserve_scene_output(scene.deferred_ids, gpu_frame.candidate_count * sizeof(std::uint32_t)); upload_scene_vector(scene.main_args_stage, gpu_frame.commands, VK_BUFFER_USAGE_TRANSFER_SRC_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT); upload_scene_vector(scene.post_args_stage, gpu_frame.commands, VK_BUFFER_USAGE_TRANSFER_SRC_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT); const std::uint32_t zero = 0; reserve_scene_output(scene.deferred_count, sizeof(zero)); upload_scene_buffer(scene.deferred_count_stage, &zero, sizeof(zero), VK_BUFFER_USAGE_TRANSFER_SRC_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT); update_scene_descriptors(occlusion); } std::optional sun = snapshot.sun; if (!sun && !snapshot.authored_lights_present && snapshot.local_lights.empty()) sun = SunLight{"legacy-sun", snapshot.light_direction, {1, 1, 1, 1}, 1, true}; Vec3 direction = sun ? sun->direction : snapshot.light_direction; float length = std::sqrt(direction[0] * direction[0] + direction[1] * direction[1] + direction[2] * direction[2]); if (!std::isfinite(length) || length < 1e-5f) { if (sun && sun->stable_id != "legacy-sun") throw std::invalid_argument("Authored sun direction must be finite and nonzero"); direction = {-.5f, -1, -.3f}; length = std::sqrt(1.34f); } for (auto& v : direction) v /= length; LightingHeaderGpu lighting{}; lighting.counts[1] = sun ? 1u : 0u; lighting.counts[2] = sun_raster ? 1u : 0u; lighting.sun_direction_intensity = {direction[0], direction[1], direction[2], sun ? sun->intensity : 0}; lighting.sun_color = sun ? sun->color : Color{0, 0, 0, 1}; if (sun && (!std::isfinite(sun->intensity) || sun->intensity < 0 || std::any_of(sun->color.begin(), sun->color.end(), [](float v) { return !std::isfinite(v) || v < 0; }))) throw std::invalid_argument("Authored sun radiance must be finite and nonnegative"); lighting.camera_forward_shadow_distance = {0, 0, -1, 80}; if (snapshot.camera_frustum) { const auto& view = snapshot.camera_frustum->view; lighting.camera_forward_shadow_distance = {-view[2], -view[6], -view[10], 80}; } lighting.counts[3] = static_cast(shadow_plan.sun_views.size()); std::vector gpu_shadow_views; gpu_shadow_views.reserve(std::max(1, shadow_plan.sun_views.size())); for (std::size_t i = 0; i < shadow_plan.sun_views.size(); ++i) { const auto& view = shadow_plan.sun_views[i]; ShadowViewGpu gpu{}; gpu.view_projection = view.view_projection; gpu.tile_scale_offset = view.atlas_scale_offset; gpu.guarded_clamp = view.guarded_clamp; gpu.bias_flags = {.0008f, .003f, sun_shadow_size ? 1.f / float(sun_shadow_size) : 0.f, sun_raster && view.valid ? 1.f : 0.f}; gpu_shadow_views.push_back(gpu); if (i < lighting.cascade_splits.size()) lighting.cascade_splits[i] = view.split_far; } for (const auto& view : shadow_plan.local_views) { ShadowViewGpu gpu{}; gpu.view_projection = view.view_projection; gpu.tile_scale_offset = view.atlas_scale_offset; gpu.guarded_clamp = view.guarded_clamp; gpu.bias_flags = {.0008f, .003f, local_shadow_size ? 1.f / float(local_shadow_size) : 0.f, local_raster && view.valid ? 1.f : 0.f}; gpu_shadow_views.push_back(gpu); } std::vector assignments(snapshot.local_lights.size()); for (const auto& assignment : shadow_plan.local_assignments) assignments.at(assignment.source_index) = &assignment; statistics.omitted_local_lights = shadow_plan.omitted_local_lights; std::vector gpu_lights; gpu_lights.reserve(shadow_plan.submitted_local_indices.size()); for (const auto source : shadow_plan.submitted_local_indices) { const auto& local = snapshot.local_lights[source]; auto spot_direction = local.direction; float spot_length = std::hypot(spot_direction[0], spot_direction[1], spot_direction[2]); if (!std::isfinite(spot_length) || spot_length < 1e-6f) { if (local.kind == LocalLight::Kind::Spot) throw std::invalid_argument("Spotlight direction must be finite and nonzero"); spot_direction = {0, 0, -1}; spot_length = 1; } for (auto& axis : spot_direction) axis /= spot_length; LocalLightGpu gpu{}; gpu.position_range = {local.position[0], local.position[1], local.position[2], local.range}; const bool spot = local.kind == LocalLight::Kind::Spot; gpu.direction_cos_outer = {spot_direction[0], spot_direction[1], spot_direction[2], spot ? std::cos(local.outer_angle) : 0.f}; gpu.color_intensity = {local.color[0], local.color[1], local.color[2], local.intensity}; gpu.cone_type_shadow_view = {spot ? std::cos(local.inner_angle) : 1.f, spot ? 1.f : 0.f, -1, 0}; const auto* assignment = assignments.at(source); if (local_raster && assignment && assignment->valid) { gpu.cone_type_shadow_view[2] = float( shadow_plan.sun_views.size() + assignment->first_view); gpu.cone_type_shadow_view[3] = float(assignment->face_count); } gpu_lights.push_back(gpu); } lighting.counts[0] = static_cast(gpu_lights.size()); statistics.submitted_local_lights = lighting.counts[0]; if (gpu_lights.empty()) gpu_lights.push_back({}); // Descriptors always point at a full initialized record. if (gpu_shadow_views.empty()) gpu_shadow_views.push_back({}); // Always bind an initialized record. upload_scene_buffer(lighting_header, &lighting, sizeof(lighting), 0); upload_scene_vector(lighting_locals, gpu_lights); upload_scene_vector(lighting_views, gpu_shadow_views); constexpr std::uint32_t light_tile_side = 16; constexpr std::uint32_t light_tile_stride_words = 66; constexpr std::uint32_t light_tile_capacity = 64; const std::uint32_t light_tiles_x = raster_width / light_tile_side + (raster_width % light_tile_side != 0); const std::uint32_t light_tiles_y = raster_height / light_tile_side + (raster_height % light_tile_side != 0); const std::uint64_t light_tile_count = std::uint64_t(light_tiles_x) * light_tiles_y; const std::uint64_t light_tile_bytes = (4u + light_tile_count * light_tile_stride_words) * sizeof(std::uint32_t); // The fixed 1080p reference scene has broad overlapping lights: 32 and // 64 nearly fill every tile, and 128 overflows every tile. Until a // validated runtime occupancy predictor exists, Auto keeps the measured // faster full scan. The explicit mode supports sparse-light projects. const bool requested_light_tiles = config.lighting_mode == LightingMode::Tiled; bool use_light_tiles = requested_light_tiles && lighting.counts[0] > 0 && light_tiles_capable && light_tile_pipeline && light_tile_set && std::all_of(scene_viewport.begin(), scene_viewport.end(), [](float value) { return std::isfinite(value); }) && scene_viewport[2] > 0 && scene_viewport[3] > 0 && light_tile_count <= std::numeric_limits::max() && light_tile_bytes <= max_storage_buffer_range && (light_tile_count + 63) / 64 <= max_compute_groups_x; if (use_light_tiles && light_tile_words.size < light_tile_bytes) { try { auto replacement = make_buffer(light_tile_bytes, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT | VK_BUFFER_USAGE_TRANSFER_SRC_BIT, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT); destroy(light_tile_words); light_tile_words = replacement; } catch (const std::exception&) { use_light_tiles = false; } } if (use_light_tiles) { statistics.effective_lighting_path = "tiled"; statistics.light_tile_count = static_cast(light_tile_count); } bool collect_light_tile_counts = use_light_tiles && config.visibility_diagnostics; if (collect_light_tile_counts && light_tile_readback.size < light_tile_bytes) { try { auto replacement = make_buffer(light_tile_bytes, VK_BUFFER_USAGE_TRANSFER_DST_BIT, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT, VK_MEMORY_PROPERTY_HOST_CACHED_BIT); destroy(light_tile_readback); light_tile_readback = replacement; } catch (const std::exception&) { collect_light_tile_counts = false; } } update_lighting_descriptors(); if (use_light_tiles) update_light_tile_descriptors(); 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{sun_raster && !shadow_plan.sun_views.empty() ? shadow_plan.sun_views.front().view_projection : 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(); std::uint32_t timestamp_cursor = 0; std::vector timestamp_labels; if (timestamp_pool) { vkCmdResetQueryPool(command, timestamp_pool, 0, timestamp_capacity); vkCmdWriteTimestamp2(command, VK_PIPELINE_STAGE_2_TOP_OF_PIPE_BIT, timestamp_pool, timestamp_cursor++); } 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); }; auto set_scene_viewport = [&] { VkViewport viewport{scene_viewport[0], scene_viewport[1], scene_viewport[2], scene_viewport[3], 0, 1}; VkRect2D scissor{}; if (temporal_active) { const auto left = static_cast(std::floor(scene_viewport[0])); const auto top = static_cast(std::floor(scene_viewport[1])); const auto right = static_cast(std::ceil( scene_viewport[0] + scene_viewport[2])); const auto bottom = static_cast(std::ceil( scene_viewport[1] + scene_viewport[3])); scissor = {{left, top}, {static_cast(std::max(0, right - left)), static_cast(std::max(0, bottom - top))}}; } else { scissor = {{static_cast(scene_viewport[0]), static_cast(scene_viewport[1])}, {static_cast(scene_viewport[2]), static_cast(scene_viewport[3])}}; } vkCmdSetViewport(command, 0, 1, &viewport); vkCmdSetScissor(command, 0, 1, &scissor); }; auto bind_material = [&](VkDescriptorSet material) { const std::array sets{material, lighting_set}; vkCmdBindDescriptorSets(command, VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline_layout, 0, static_cast(sets.size()), sets.data(), 0, nullptr); }; auto draw_transparent = [&] { if (transparent_batches.empty()) return; vkCmdBindVertexBuffers(command, 0, 1, &vertices.handle, &offset); vkCmdBindPipeline(command, VK_PIPELINE_BIND_POINT_GRAPHICS, temporal_active ? temporal.transparent_pipeline : pipeline); vkCmdPushConstants(command, pipeline_layout, VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT, 0, sizeof(push), &push); for (auto batch : transparent_batches) { auto descriptor = textures.at(batch.texture).descriptor; bind_material(descriptor); vkCmdDraw(command, batch.count, 1, batch.first, 0); ++statistics.draw_calls; } }; auto draw_sprites = [&] { vkCmdBindVertexBuffers(command, 0, 1, &vertices.handle, &offset); vkCmdBindPipeline(command, VK_PIPELINE_BIND_POINT_GRAPHICS, temporal_active ? temporal.transparent_pipeline : sprite_pipeline); vkCmdPushConstants(command, pipeline_layout, VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT, 0, sizeof(push), &push); for (auto batch : sprite_batches) { auto descriptor = textures.at(batch.texture).descriptor; bind_material(descriptor); vkCmdDraw(command, batch.count, 1, batch.first, 0); ++statistics.draw_calls; } }; auto draw_ui = [&] { vkCmdBindVertexBuffers(command, 0, 1, &vertices.handle, &offset); set_viewport(width, height); vkCmdBindPipeline(command, VK_PIPELINE_BIND_POINT_GRAPHICS, temporal_active ? temporal_ui_pipeline : ui_pipeline); 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; bind_material(descriptor); vkCmdDraw(command, batch.count, 1, batch.first, 0); ++statistics.draw_calls; } }; auto dispatch_scene = [&](VkPipeline compute_pipeline, VkDescriptorSet descriptor) { if (!gpu_frame.candidate_count) return; vkCmdBindPipeline(command, VK_PIPELINE_BIND_POINT_COMPUTE, compute_pipeline); vkCmdBindDescriptorSets(command, VK_PIPELINE_BIND_POINT_COMPUTE, scene.cull_pipeline_layout, 0, 1, &descriptor, 0, nullptr); const std::uint64_t max_threads = std::uint64_t(max_compute_groups_x) * 64; for (std::uint64_t base = 0; base < gpu_frame.candidate_count; base += max_threads) { const auto remaining = std::uint64_t(gpu_frame.candidate_count) - base; const auto groups = static_cast( std::min(max_compute_groups_x, (remaining + 63) / 64)); const std::array parameters{ gpu_frame.candidate_count, gpu_frame.candidate_count, static_cast(base), 0}; vkCmdPushConstants(command, scene.cull_pipeline_layout, VK_SHADER_STAGE_COMPUTE_BIT, 0, sizeof(parameters), parameters.data()); vkCmdDispatch(command, groups, 1, 1); } }; RenderGraph graph; if (temporal_active) graph.import("history_previous"); auto add_pass = [&](std::string name, std::vector reads, std::vector writes, RenderGraph::Callback callback) { const auto label_name = name; statistics.graph_passes.push_back(label_name); graph.add(std::move(name), std::move(reads), std::move(writes), [this, ×tamp_cursor, ×tamp_labels, 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(); if (timestamp_pool && timestamp_cursor < timestamp_capacity) { vkCmdWriteTimestamp2(command, VK_PIPELINE_STAGE_2_BOTTOM_OF_PIPE_BIT, timestamp_pool, timestamp_cursor++); timestamp_labels.push_back(label_name); } }); }; auto raster_shadow_atlas = [&](Image& atlas, std::uint32_t atlas_size, const std::vector& views) { transition(command, atlas, VK_IMAGE_LAYOUT_DEPTH_ATTACHMENT_OPTIMAL, VK_IMAGE_ASPECT_DEPTH_BIT); VkRenderingAttachmentInfo attachment{}; attachment.sType = VK_STRUCTURE_TYPE_RENDERING_ATTACHMENT_INFO; attachment.imageView = atlas.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}, {atlas_size, atlas_size}}; rendering.layerCount = 1; rendering.pDepthAttachment = &attachment; vkCmdBeginRendering(command, &rendering); vkCmdBindPipeline(command, VK_PIPELINE_BIND_POINT_GRAPHICS, shadow_pipeline); for (const auto& view : views) { if (!view.valid || view.caster_indices.empty()) continue; const auto guard = (view.tile_size - view.usable_size) / 2; const auto x = view.tile_origin_x + guard; const auto y = view.tile_origin_y + guard; const VkViewport viewport{float(x), float(y), float(view.usable_size), float(view.usable_size), 0, 1}; const VkRect2D scissor{{static_cast(x), static_cast(y)}, {view.usable_size, view.usable_size}}; vkCmdSetViewport(command, 0, 1, &viewport); vkCmdSetScissor(command, 0, 1, &scissor); auto view_push = push; view_push.light_view_projection = view.view_projection; vkCmdPushConstants(command, pipeline_layout, VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT, 0, sizeof(view_push), &view_push); for (const auto source : view.caster_indices) { const auto& batch = shadow_batch_by_source.at(source); if (!batch.count) continue; vkCmdDraw(command, batch.count, 1, batch.first, 0); ++statistics.draw_calls; } } vkCmdEndRendering(command); transition(command, atlas, VK_IMAGE_LAYOUT_DEPTH_READ_ONLY_OPTIMAL, VK_IMAGE_ASPECT_DEPTH_BIT); }; if (sun_raster) add_pass("SunShadowAtlas", {}, {"shadow"}, [&] { raster_shadow_atlas(shadow, sun_shadow_size, shadow_plan.sun_views); }); else add_pass("ShadowFallback", {}, {"shadow"}, [&] { // A bound descriptor still needs a matching image layout, even when // every graphics shader branch treats its shadow as unshadowed. transition(command, shadow, VK_IMAGE_LAYOUT_DEPTH_READ_ONLY_OPTIMAL, VK_IMAGE_ASPECT_DEPTH_BIT); }); if (local_raster) add_pass("LocalShadowAtlas", {}, {"local_shadow"}, [&] { raster_shadow_atlas(local_shadow, local_shadow_size, shadow_plan.local_views); }); else add_pass("LocalShadowFallback", {}, {"local_shadow"}, [&] { if (local_shadow.handle) transition(command, local_shadow, VK_IMAGE_LAYOUT_DEPTH_READ_ONLY_OPTIMAL, VK_IMAGE_ASPECT_DEPTH_BIT); }); if (use_light_tiles) add_pass("LightTileBuild", {}, {"light_tiles"}, [&] { scene_barrier(VK_PIPELINE_STAGE_2_HOST_BIT, VK_ACCESS_2_HOST_WRITE_BIT, VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT, VK_ACCESS_2_SHADER_STORAGE_READ_BIT); vkCmdBindPipeline(command, VK_PIPELINE_BIND_POINT_COMPUTE, light_tile_pipeline); vkCmdBindDescriptorSets(command, VK_PIPELINE_BIND_POINT_COMPUTE, light_tile_pipeline_layout, 0, 1, &light_tile_set, 0, nullptr); const LightTilePush tile_push{ raster_vp, scene_viewport, {light_tiles_x, light_tiles_y, lighting.counts[0], light_tile_capacity}}; vkCmdPushConstants(command, light_tile_pipeline_layout, VK_SHADER_STAGE_COMPUTE_BIT, 0, sizeof(tile_push), &tile_push); vkCmdDispatch(command, static_cast((light_tile_count + 63) / 64), 1, 1); scene_barrier(VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT, VK_ACCESS_2_SHADER_STORAGE_WRITE_BIT, VK_PIPELINE_STAGE_2_FRAGMENT_SHADER_BIT, VK_ACCESS_2_SHADER_STORAGE_READ_BIT); }); else add_pass("LightTileFallback", {}, {"light_tiles"}, [&] { vkCmdFillBuffer(command, light_tile_words.handle, 0, 16, 0); scene_barrier(VK_PIPELINE_STAGE_2_TRANSFER_BIT, VK_ACCESS_2_TRANSFER_WRITE_BIT, VK_PIPELINE_STAGE_2_FRAGMENT_SHADER_BIT, VK_ACCESS_2_SHADER_STORAGE_READ_BIT); }); if (gpu_active) add_pass("MainCull", {"shadow"}, {"main_indirect", "main_visible", "deferred_ids"}, [&] { scene_barrier(VK_PIPELINE_STAGE_2_HOST_BIT, VK_ACCESS_2_HOST_WRITE_BIT, VK_PIPELINE_STAGE_2_TRANSFER_BIT, VK_ACCESS_2_TRANSFER_READ_BIT); const VkDeviceSize command_bytes = gpu_frame.commands.size() * sizeof(SceneIndirect); if (command_bytes) { const VkBufferCopy copy{0, 0, command_bytes}; vkCmdCopyBuffer(command, scene.main_args_stage.handle, scene.main_args.handle, 1, ©); vkCmdCopyBuffer(command, scene.post_args_stage.handle, scene.post_args.handle, 1, ©); } const VkBufferCopy count_copy{0, 0, sizeof(std::uint32_t)}; vkCmdCopyBuffer(command, scene.deferred_count_stage.handle, scene.deferred_count.handle, 1, &count_copy); scene_barrier(VK_PIPELINE_STAGE_2_TRANSFER_BIT, VK_ACCESS_2_TRANSFER_WRITE_BIT, VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT | VK_PIPELINE_STAGE_2_DRAW_INDIRECT_BIT, VK_ACCESS_2_SHADER_STORAGE_READ_BIT | VK_ACCESS_2_SHADER_STORAGE_WRITE_BIT | VK_ACCESS_2_INDIRECT_COMMAND_READ_BIT); if (occlusion) { for (auto& pyramid : scene.hzb) { if (pyramid.layout == VK_IMAGE_LAYOUT_UNDEFINED) { transition(command, pyramid, VK_IMAGE_LAYOUT_GENERAL, VK_IMAGE_ASPECT_COLOR_BIT); VkClearColorValue far_depth{}; far_depth.float32[0] = 1.f; VkImageSubresourceRange range{VK_IMAGE_ASPECT_COLOR_BIT, 0, pyramid.mip_levels, 0, 1}; vkCmdClearColorImage(command, pyramid.handle, VK_IMAGE_LAYOUT_GENERAL, &far_depth, 1, &range); } } scene_barrier(VK_PIPELINE_STAGE_2_TRANSFER_BIT, VK_ACCESS_2_TRANSFER_WRITE_BIT, VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT, VK_ACCESS_2_SHADER_SAMPLED_READ_BIT); } scene_barrier(VK_PIPELINE_STAGE_2_HOST_BIT, VK_ACCESS_2_HOST_WRITE_BIT, VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT, VK_ACCESS_2_SHADER_STORAGE_READ_BIT | VK_ACCESS_2_SHADER_STORAGE_WRITE_BIT); if (gpu_frame.candidate_count) { dispatch_scene(scene.cull_pipeline, scene.cull_main); scene_barrier(VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT, VK_ACCESS_2_SHADER_STORAGE_WRITE_BIT, VK_PIPELINE_STAGE_2_DRAW_INDIRECT_BIT | VK_PIPELINE_STAGE_2_VERTEX_SHADER_BIT, VK_ACCESS_2_INDIRECT_COMMAND_READ_BIT | VK_ACCESS_2_SHADER_STORAGE_READ_BIT); } }); Image& raster_color = temporal_active ? temporal.scene_color : color; add_pass(occlusion || temporal_active ? "MainRaster" : "ForwardAndUI", gpu_active ? std::vector{"shadow", "local_shadow", "light_tiles", "main_indirect", "main_visible"} : std::vector{"shadow", "local_shadow", "light_tiles"}, temporal_active ? std::vector{"scene_color", "depth", "scene_velocity"} : std::vector{"color", "depth"}, [&] { transition(command, raster_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 = raster_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 = occlusion || temporal_active ? VK_ATTACHMENT_STORE_OP_STORE : VK_ATTACHMENT_STORE_OP_DONT_CARE; da.clearValue.depthStencil = {1, 0}; VkRenderingAttachmentInfo va{}; std::array color_attachments{ca, va}; if (temporal_active) { transition(command, temporal.velocity, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, VK_IMAGE_ASPECT_COLOR_BIT); va.sType = VK_STRUCTURE_TYPE_RENDERING_ATTACHMENT_INFO; va.imageView = temporal.velocity.view; va.imageLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL; va.loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR; va.storeOp = VK_ATTACHMENT_STORE_OP_STORE; color_attachments[1] = va; } VkRenderingInfo rendering{}; rendering.sType = VK_STRUCTURE_TYPE_RENDERING_INFO; rendering.renderArea = {{0, 0}, {raster_width, raster_height}}; rendering.layerCount = 1; rendering.colorAttachmentCount = temporal_active ? 2 : 1; rendering.pColorAttachments = color_attachments.data(); rendering.pDepthAttachment = &da; vkCmdBeginRendering(command, &rendering); set_scene_viewport(); vkCmdBindPipeline(command, VK_PIPELINE_BIND_POINT_GRAPHICS, temporal_active ? temporal.direct_pipeline : pipeline); vkCmdPushConstants(command, pipeline_layout, VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT, 0, sizeof(push), &push); bind_material(white_descriptor); if (gpu_active && !gpu_frame.bins.empty()) { VkDeviceSize scene_offset{}; vkCmdBindVertexBuffers(command, 0, 1, &scene.vertices.handle, &scene_offset); vkCmdBindPipeline(command, VK_PIPELINE_BIND_POINT_GRAPHICS, temporal_active ? temporal.gpu_pipeline : scene.graphics_pipeline); for (std::uint32_t bin = 0; bin < gpu_frame.bins.size(); ++bin) { auto descriptor = textures.at(gpu_frame.textures[bin]).descriptor; const std::array sets{descriptor, lighting_set, scene.graphics_main}; vkCmdBindDescriptorSets(command, VK_PIPELINE_BIND_POINT_GRAPHICS, scene.graphics_pipeline_layout, 0, sets.size(), sets.data(), 0, nullptr); ScenePush draw_push{push, {gpu_frame.bins[bin].visible_base, 0, 0, 0}}; vkCmdPushConstants(command, scene.graphics_pipeline_layout, VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT, 0, sizeof(draw_push), &draw_push); vkCmdDrawIndirect(command, scene.main_args.handle, VkDeviceSize(bin) * sizeof(SceneIndirect), 1, sizeof(SceneIndirect)); ++statistics.draw_calls; } vkCmdBindVertexBuffers(command, 0, 1, &vertices.handle, &offset); vkCmdBindPipeline(command, VK_PIPELINE_BIND_POINT_GRAPHICS, temporal_active ? temporal.direct_pipeline : pipeline); vkCmdPushConstants(command, pipeline_layout, VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT, 0, sizeof(push), &push); } for (auto batch : scene_batches) { auto descriptor = textures.at(batch.texture).descriptor; bind_material(descriptor); vkCmdDraw(command, batch.count, 1, batch.first, 0); ++statistics.draw_calls; } if (!occlusion) { draw_transparent(); draw_sprites(); if (!temporal_active) draw_ui(); } vkCmdEndRendering(command); }); if (occlusion) { add_pass("BuildCurrentHZB", {"depth"}, {"current_hzb"}, [&] { transition(command, depth, VK_IMAGE_LAYOUT_DEPTH_READ_ONLY_OPTIMAL, VK_IMAGE_ASPECT_DEPTH_BIT); auto& pyramid = scene.hzb[scene.hzb_current]; transition(command, pyramid, VK_IMAGE_LAYOUT_GENERAL, VK_IMAGE_ASPECT_COLOR_BIT); vkCmdBindPipeline(command, VK_PIPELINE_BIND_POINT_COMPUTE, scene.hzb_pipeline); const auto padded_width = std::bit_ceil(raster_width); const auto padded_height = std::bit_ceil(raster_height); for (std::uint32_t mip = 0; mip < scene.hzb_mips; ++mip) { const std::uint32_t source_width = mip == 0 ? raster_width : std::max(1u, padded_width >> (mip - 1)); const std::uint32_t source_height = mip == 0 ? raster_height : std::max(1u, padded_height >> (mip - 1)); const std::uint32_t output_width = std::max(1u, padded_width >> mip); const std::uint32_t output_height = std::max(1u, padded_height >> mip); const auto descriptor = scene.hzb_sets[scene.hzb_current][mip]; vkCmdBindDescriptorSets(command, VK_PIPELINE_BIND_POINT_COMPUTE, scene.hzb_pipeline_layout, 0, 1, &descriptor, 0, nullptr); const std::array dimensions{ source_width, source_height, output_width, output_height}; vkCmdPushConstants(command, scene.hzb_pipeline_layout, VK_SHADER_STAGE_COMPUTE_BIT, 0, sizeof(dimensions), dimensions.data()); vkCmdDispatch(command, (output_width + 7) / 8, (output_height + 7) / 8, 1); scene_barrier(VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT, VK_ACCESS_2_SHADER_STORAGE_WRITE_BIT, VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT, VK_ACCESS_2_SHADER_SAMPLED_READ_BIT); } }); add_pass("PostCull", {"deferred_ids", "current_hzb"}, {"post_indirect", "post_visible"}, [&] { scene_barrier(VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT, VK_ACCESS_2_SHADER_STORAGE_WRITE_BIT, VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT, VK_ACCESS_2_SHADER_STORAGE_READ_BIT | VK_ACCESS_2_SHADER_SAMPLED_READ_BIT); if (gpu_frame.candidate_count) { dispatch_scene(scene.post_pipeline, scene.cull_post); scene_barrier(VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT, VK_ACCESS_2_SHADER_STORAGE_WRITE_BIT, VK_PIPELINE_STAGE_2_DRAW_INDIRECT_BIT | VK_PIPELINE_STAGE_2_VERTEX_SHADER_BIT, VK_ACCESS_2_INDIRECT_COMMAND_READ_BIT | VK_ACCESS_2_SHADER_STORAGE_READ_BIT); } }); add_pass(temporal_active ? "PostRasterScene" : "PostRasterAndUI", {temporal_active ? "scene_color" : "color", "depth", "post_indirect", "post_visible"}, temporal_active ? std::vector{"scene_color", "depth", "scene_velocity"} : std::vector{"color", "depth"}, [&] { transition(command, raster_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 = raster_color.view; ca.imageLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL; ca.loadOp = VK_ATTACHMENT_LOAD_OP_LOAD; ca.storeOp = VK_ATTACHMENT_STORE_OP_STORE; 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_LOAD; da.storeOp = temporal_active ? VK_ATTACHMENT_STORE_OP_STORE : VK_ATTACHMENT_STORE_OP_DONT_CARE; VkRenderingAttachmentInfo va{}; std::array color_attachments{ca, va}; if (temporal_active) { transition(command, temporal.velocity, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, VK_IMAGE_ASPECT_COLOR_BIT); va.sType = VK_STRUCTURE_TYPE_RENDERING_ATTACHMENT_INFO; va.imageView = temporal.velocity.view; va.imageLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL; va.loadOp = VK_ATTACHMENT_LOAD_OP_LOAD; va.storeOp = VK_ATTACHMENT_STORE_OP_STORE; color_attachments[1] = va; } VkRenderingInfo rendering{}; rendering.sType = VK_STRUCTURE_TYPE_RENDERING_INFO; rendering.renderArea = {{0, 0}, {raster_width, raster_height}}; rendering.layerCount = 1; rendering.colorAttachmentCount = temporal_active ? 2 : 1; rendering.pColorAttachments = color_attachments.data(); rendering.pDepthAttachment = &da; vkCmdBeginRendering(command, &rendering); set_scene_viewport(); if (!gpu_frame.bins.empty()) { VkDeviceSize scene_offset{}; vkCmdBindVertexBuffers(command, 0, 1, &scene.vertices.handle, &scene_offset); vkCmdBindPipeline(command, VK_PIPELINE_BIND_POINT_GRAPHICS, temporal_active ? temporal.gpu_pipeline : scene.graphics_pipeline); for (std::uint32_t bin = 0; bin < gpu_frame.bins.size(); ++bin) { auto descriptor = textures.at(gpu_frame.textures[bin]).descriptor; const std::array sets{descriptor, lighting_set, scene.graphics_post}; vkCmdBindDescriptorSets(command, VK_PIPELINE_BIND_POINT_GRAPHICS, scene.graphics_pipeline_layout, 0, sets.size(), sets.data(), 0, nullptr); ScenePush draw_push{push, {gpu_frame.bins[bin].visible_base, 0, 0, 0}}; vkCmdPushConstants(command, scene.graphics_pipeline_layout, VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT, 0, sizeof(draw_push), &draw_push); vkCmdDrawIndirect(command, scene.post_args.handle, VkDeviceSize(bin) * sizeof(SceneIndirect), 1, sizeof(SceneIndirect)); ++statistics.draw_calls; } } draw_transparent(); draw_sprites(); if (!temporal_active) draw_ui(); vkCmdEndRendering(command); }); } const std::uint32_t next_history = temporal.has_completed_image ? 1 - temporal.completed_index : 0; if (temporal_active) { add_pass("TemporalResolve", {"scene_color", "depth", "scene_velocity", "history_previous"}, {"resolved_color", "resolved_depth"}, [&] { if (collect_temporal_counts) { vkCmdFillBuffer(command, temporal.pixel_counts.handle, 0, 2 * sizeof(std::uint32_t), 0); scene_barrier(VK_PIPELINE_STAGE_2_TRANSFER_BIT, VK_ACCESS_2_TRANSFER_WRITE_BIT, VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT, VK_ACCESS_2_SHADER_STORAGE_READ_BIT | VK_ACCESS_2_SHADER_STORAGE_WRITE_BIT); } transition(command, temporal.scene_color, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, VK_IMAGE_ASPECT_COLOR_BIT); transition(command, depth, VK_IMAGE_LAYOUT_DEPTH_READ_ONLY_OPTIMAL, VK_IMAGE_ASPECT_DEPTH_BIT); transition(command, temporal.velocity, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, VK_IMAGE_ASPECT_COLOR_BIT); transition(command, temporal.history_color[1 - next_history], VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, VK_IMAGE_ASPECT_COLOR_BIT); transition(command, temporal.history_depth[1 - next_history], VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, VK_IMAGE_ASPECT_COLOR_BIT); transition(command, temporal.history_color[next_history], VK_IMAGE_LAYOUT_GENERAL, VK_IMAGE_ASPECT_COLOR_BIT); transition(command, temporal.history_depth[next_history], VK_IMAGE_LAYOUT_GENERAL, VK_IMAGE_ASPECT_COLOR_BIT); struct ResolvePush { std::array dimensions; std::array output_rect, internal_rect; std::array flags; std::array jitter_motion; }; static_assert(sizeof(ResolvePush) == 80); const bool static_camera = statistics.temporal_history_valid && temporal.previous_unjittered_vp == snapshot.view_projection; ResolvePush parameters{{width, height, raster_width, raster_height}, output_scene_viewport, scene_viewport, {statistics.temporal_history_valid ? 1u : 0u, collect_temporal_counts ? 1u : 0u, 0, 0}, {(statistics.temporal_jitter[0] - temporal.previous_jitter[0]) * .5f, (statistics.temporal_jitter[1] - temporal.previous_jitter[1]) * .5f, static_camera ? 1.f : 0.f, 0.f}}; vkCmdBindPipeline(command, VK_PIPELINE_BIND_POINT_COMPUTE, temporal.resolve_pipeline); vkCmdBindDescriptorSets(command, VK_PIPELINE_BIND_POINT_COMPUTE, temporal.resolve_pipeline_layout, 0, 1, &temporal.resolve_sets[next_history], 0, nullptr); vkCmdPushConstants(command, temporal.resolve_pipeline_layout, VK_SHADER_STAGE_COMPUTE_BIT, 0, sizeof(parameters), ¶meters); vkCmdDispatch(command, (width + 7) / 8, (height + 7) / 8, 1); }); add_pass("TemporalComposite", {"resolved_color"}, {"color"}, [&] { transition(command, temporal.history_color[next_history], VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, VK_IMAGE_ASPECT_COLOR_BIT); transition(command, color, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, VK_IMAGE_ASPECT_COLOR_BIT); VkRenderingAttachmentInfo attachment{}; attachment.sType = VK_STRUCTURE_TYPE_RENDERING_ATTACHMENT_INFO; attachment.imageView = color.view; attachment.imageLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL; attachment.loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR; attachment.storeOp = VK_ATTACHMENT_STORE_OP_STORE; std::copy(snapshot.clear_color.begin(), snapshot.clear_color.end(), attachment.clearValue.color.float32); VkRenderingInfo rendering{}; rendering.sType = VK_STRUCTURE_TYPE_RENDERING_INFO; rendering.renderArea = {{0, 0}, {width, height}}; rendering.layerCount = 1; rendering.colorAttachmentCount = 1; rendering.pColorAttachments = &attachment; vkCmdBeginRendering(command, &rendering); set_viewport(width, height); vkCmdBindVertexBuffers(command, 0, 1, &vertices.handle, &offset); vkCmdBindPipeline(command, VK_PIPELINE_BIND_POINT_GRAPHICS, temporal.composite_pipeline); vkCmdBindDescriptorSets(command, VK_PIPELINE_BIND_POINT_GRAPHICS, temporal.composite_pipeline_layout, 0, 1, &temporal.composite_sets[next_history], 0, nullptr); vkCmdDraw(command, 3, 1, composite_first, 0); vkCmdEndRendering(command); }); add_pass("UI", {"color"}, {"color"}, [&] { if (ui_batches.empty()) return; transition(command, color, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, VK_IMAGE_ASPECT_COLOR_BIT); VkRenderingAttachmentInfo attachment{}; attachment.sType = VK_STRUCTURE_TYPE_RENDERING_ATTACHMENT_INFO; attachment.imageView = color.view; attachment.imageLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL; attachment.loadOp = VK_ATTACHMENT_LOAD_OP_LOAD; attachment.storeOp = VK_ATTACHMENT_STORE_OP_STORE; VkRenderingInfo rendering{}; rendering.sType = VK_STRUCTURE_TYPE_RENDERING_INFO; rendering.renderArea = {{0, 0}, {width, height}}; rendering.layerCount = 1; rendering.colorAttachmentCount = 1; rendering.pColorAttachments = &attachment; vkCmdBeginRendering(command, &rendering); draw_ui(); vkCmdEndRendering(command); }); } add_pass("Readback", collect_light_tile_counts ? std::vector{"color", "light_tiles"} : std::vector{"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 (gpu_active && config.visibility_diagnostics) { // The staging buffers were transfer sources at the start of // this frame; finish those reads before reusing them as copies' // destinations for diagnostic counters. scene_barrier(VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT | VK_PIPELINE_STAGE_2_TRANSFER_BIT, VK_ACCESS_2_SHADER_STORAGE_WRITE_BIT | VK_ACCESS_2_TRANSFER_READ_BIT, VK_PIPELINE_STAGE_2_TRANSFER_BIT, VK_ACCESS_2_TRANSFER_READ_BIT | VK_ACCESS_2_TRANSFER_WRITE_BIT); const VkDeviceSize command_bytes = gpu_frame.commands.size() * sizeof(SceneIndirect); if (command_bytes) { const VkBufferCopy command_copy{0, 0, command_bytes}; vkCmdCopyBuffer(command, scene.main_args.handle, scene.main_args_stage.handle, 1, &command_copy); if (occlusion) vkCmdCopyBuffer(command, scene.post_args.handle, scene.post_args_stage.handle, 1, &command_copy); } const VkBufferCopy count_copy{0, 0, sizeof(std::uint32_t)}; vkCmdCopyBuffer(command, scene.deferred_count.handle, scene.deferred_count_stage.handle, 1, &count_copy); scene_barrier(VK_PIPELINE_STAGE_2_TRANSFER_BIT, VK_ACCESS_2_TRANSFER_WRITE_BIT, VK_PIPELINE_STAGE_2_HOST_BIT, VK_ACCESS_2_HOST_READ_BIT); } if (collect_light_tile_counts) { scene_barrier(VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT, VK_ACCESS_2_SHADER_STORAGE_WRITE_BIT, VK_PIPELINE_STAGE_2_TRANSFER_BIT, VK_ACCESS_2_TRANSFER_READ_BIT); const VkBufferCopy tile_copy{0, 0, light_tile_bytes}; vkCmdCopyBuffer(command, light_tile_words.handle, light_tile_readback.handle, 1, &tile_copy); scene_barrier(VK_PIPELINE_STAGE_2_TRANSFER_BIT, VK_ACCESS_2_TRANSFER_WRITE_BIT, VK_PIPELINE_STAGE_2_HOST_BIT, VK_ACCESS_2_HOST_READ_BIT); } if (collect_temporal_counts) { scene_barrier(VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT, VK_ACCESS_2_SHADER_STORAGE_WRITE_BIT, VK_PIPELINE_STAGE_2_TRANSFER_BIT, VK_ACCESS_2_TRANSFER_READ_BIT); const VkBufferCopy count_copy{0, 0, 2 * sizeof(std::uint32_t)}; vkCmdCopyBuffer(command, temporal.pixel_counts.handle, temporal.pixel_counts_stage.handle, 1, &count_copy); scene_barrier(VK_PIPELINE_STAGE_2_TRANSFER_BIT, VK_ACCESS_2_TRANSFER_WRITE_BIT, VK_PIPELINE_STAGE_2_HOST_BIT, VK_ACCESS_2_HOST_READ_BIT); } }); 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(); submit(swap_index.has_value()); if (timestamp_pool) { std::array stamps{}; check(vkGetQueryPoolResults(device, timestamp_pool, 0, timestamp_cursor, timestamp_cursor * sizeof(std::uint64_t), stamps.data(), sizeof(std::uint64_t), VK_QUERY_RESULT_64_BIT | VK_QUERY_RESULT_WAIT_BIT), "Read GPU timestamps"); auto milliseconds = [&](std::uint64_t before, std::uint64_t after) { auto delta = after - before; if (timestamp_bits < 64) delta &= (std::uint64_t(1) << timestamp_bits) - 1; return double(delta) * timestamp_period / 1000000.0; }; statistics.gpu_ms = milliseconds(stamps[0], stamps[timestamp_cursor - 1]); statistics.gpu_main_cull_ms = statistics.gpu_main_raster_ms = statistics.gpu_hzb_ms = statistics.gpu_post_cull_ms = statistics.gpu_post_raster_ms = 0; for (std::size_t i = 0; i < timestamp_labels.size(); ++i) { const auto elapsed = milliseconds(stamps[i], stamps[i + 1]); const auto& label = timestamp_labels[i]; if (label == "MainCull") statistics.gpu_main_cull_ms = elapsed; else if (label == "SunShadowAtlas") statistics.gpu_sun_shadow_ms = elapsed; else if (label == "LocalShadowAtlas") statistics.gpu_local_shadow_ms = elapsed; else if (label == "LightTileBuild") statistics.gpu_light_tiles_ms = elapsed; else if (label == "MainRaster" || label == "ForwardAndUI") statistics.gpu_main_raster_ms = elapsed; else if (label == "BuildCurrentHZB") statistics.gpu_hzb_ms = elapsed; else if (label == "PostCull") statistics.gpu_post_cull_ms = elapsed; else if (label == "PostRasterAndUI" || label == "PostRasterScene") statistics.gpu_post_raster_ms = elapsed; else if (label == "TemporalResolve") statistics.gpu_temporal_resolve_ms = elapsed; else if (label == "TemporalComposite") statistics.gpu_temporal_composite_ms = elapsed; else if (label == "UI") statistics.gpu_ui_ms = elapsed; } } 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(); if (gpu_active && config.visibility_diagnostics) { void* counts{}; check(vkMapMemory(device, scene.main_args_stage.memory, 0, scene.main_args_stage.size, 0, &counts), "Read GPU scene indirect counts"); auto* commands = static_cast(counts); std::uint32_t main_visible{}; for (std::size_t i = 0; i < gpu_frame.commands.size(); ++i) main_visible += commands[i].instance_count; vkUnmapMemory(device, scene.main_args_stage.memory); check(vkMapMemory(device, scene.deferred_count_stage.memory, 0, scene.deferred_count_stage.size, 0, &counts), "Read GPU deferred count"); statistics.gpu_occlusion_deferred = *static_cast(counts); vkUnmapMemory(device, scene.deferred_count_stage.memory); if (occlusion && statistics.gpu_occlusion_deferred) { check(vkMapMemory(device, scene.post_args_stage.memory, 0, scene.post_args_stage.size, 0, &counts), "Read GPU post counts"); commands = static_cast(counts); for (std::size_t i = 0; i < gpu_frame.commands.size(); ++i) statistics.gpu_post_visible += commands[i].instance_count; vkUnmapMemory(device, scene.post_args_stage.memory); } statistics.gpu_visible_instances = main_visible + statistics.gpu_post_visible; statistics.gpu_frustum_rejected = gpu_frame.candidate_count - std::min(gpu_frame.candidate_count, main_visible + statistics.gpu_occlusion_deferred); statistics.culled_meshes += statistics.gpu_frustum_rejected; statistics.visibility_counters_valid = true; } if (collect_light_tile_counts) { void* mapped_tiles{}; check(vkMapMemory(device, light_tile_readback.memory, 0, light_tile_bytes, 0, &mapped_tiles), "Read light tile diagnostics"); const auto* words = static_cast(mapped_tiles); if (words[0] != light_tiles_x || words[1] != 1 || words[2] != light_tiles_y || words[3] != light_tile_capacity) { vkUnmapMemory(device, light_tile_readback.memory); throw std::runtime_error("Light tile diagnostic header is inconsistent"); } for (std::uint32_t tile = 0; tile < light_tile_count; ++tile) { const auto base = 4u + tile * light_tile_stride_words; if (words[base] > light_tile_capacity || words[base + 1] > 1) { vkUnmapMemory(device, light_tile_readback.memory); throw std::runtime_error("Light tile diagnostic record is invalid"); } statistics.light_tile_candidate_count += words[base]; statistics.light_tile_overflow_count += words[base + 1]; } vkUnmapMemory(device, light_tile_readback.memory); statistics.light_tile_counts_valid = true; } if (collect_temporal_counts) { void* mapped_counts{}; check(vkMapMemory(device, temporal.pixel_counts_stage.memory, 0, temporal.pixel_counts_stage.size, 0, &mapped_counts), "Read temporal pixel diagnostics"); const auto* words = static_cast(mapped_counts); statistics.temporal_accepted_pixels = words[0]; statistics.temporal_rejected_pixels = words[1]; vkUnmapMemory(device, temporal.pixel_counts_stage.memory); statistics.temporal_counters_valid = true; } instance_tracker.finish_frame(); scene.previous_vp = raster_vp; scene.previous_projection = snapshot.projection; scene.previous_viewport = scene_viewport; scene.previous_view_id = view_id; scene.hzb_history_valid = occlusion; temporal.previous_jittered_vp = raster_vp; temporal.previous_unjittered_vp = snapshot.view_projection; temporal.previous_jitter = statistics.temporal_jitter; temporal.history.complete(temporal_key); if (temporal_active) { temporal.completed_index = temporal.has_completed_image ? 1 - temporal.completed_index : 0; temporal.has_completed_image = true; } else { temporal.has_completed_image = false; } ++statistics.frame; statistics.gpu_allocated_bytes = vertices.allocation_size + readback.allocation_size + color.allocation_size + depth.allocation_size + shadow.allocation_size + local_shadow.allocation_size + lighting_header.allocation_size + lighting_locals.allocation_size + lighting_views.allocation_size + light_tile_words.allocation_size + light_tile_readback.allocation_size; statistics.texture_count = static_cast(textures.size()); for (const auto& [_, texture] : textures) statistics.gpu_allocated_bytes += texture.image.allocation_size; if (scene.available) { for (const Buffer* buffer : {&scene.vertices, &scene.instances, &scene.candidates, &scene.bins, &scene.view, &scene.main_ids, &scene.post_ids, &scene.main_args, &scene.post_args, &scene.deferred_ids, &scene.deferred_count, &scene.main_args_stage, &scene.post_args_stage, &scene.deferred_count_stage}) statistics.gpu_allocated_bytes += buffer->allocation_size; for (const auto& image : scene.hzb) statistics.gpu_allocated_bytes += image.allocation_size; } statistics.gpu_allocated_bytes += temporal.scene_color.allocation_size + temporal.velocity.allocation_size + temporal.pixel_counts.allocation_size + temporal.pixel_counts_stage.allocation_size; for (const auto& image : temporal.history_color) statistics.gpu_allocated_bytes += image.allocation_size; for (const auto& image : temporal.history_depth) statistics.gpu_allocated_bytes += 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; auto previous_temporal_ui = r.temporal_ui_pipeline; auto previous_light_tile = r.light_tile_pipeline; auto previous_light_tiles_capable = r.light_tiles_capable; auto previous_layout_fingerprints = r.shader_layouts; auto previous_gpu_fingerprints = r.scene.shader_layouts; auto previous_gpu = r.scene.graphics_pipeline; auto previous_cull = r.scene.cull_pipeline; auto previous_post = r.scene.post_pipeline; auto previous_hzb = r.scene.hzb_pipeline; const auto previous_temporal_layouts = r.temporal.shader_layouts; const auto previous_temporal_scene_layouts = r.temporal.scene_shader_layouts; const std::array previous_temporal_pipelines{ r.temporal.resolve_pipeline, r.temporal.composite_pipeline, r.temporal.direct_pipeline, r.temporal.transparent_pipeline, r.temporal.gpu_pipeline}; r.pipeline_layout = {}; r.pipeline = {}; r.ui_pipeline = {}; r.shadow_pipeline = {}; r.sprite_pipeline = {}; r.temporal_ui_pipeline = {}; r.light_tile_pipeline = {}; r.scene.graphics_pipeline = r.scene.cull_pipeline = r.scene.post_pipeline = r.scene.hzb_pipeline = {}; r.temporal.resolve_pipeline = r.temporal.composite_pipeline = r.temporal.direct_pipeline = r.temporal.transparent_pipeline = r.temporal.gpu_pipeline = {}; try { r.make_pipelines(); if (r.scene.graphics_pipeline_layout) r.make_scene_pipelines(); r.make_temporal_interfaces_and_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.temporal_ui_pipeline) vkDestroyPipeline(r.device, r.temporal_ui_pipeline, nullptr); if (r.light_tile_pipeline) vkDestroyPipeline(r.device, r.light_tile_pipeline, nullptr); if (r.pipeline_layout) vkDestroyPipelineLayout(r.device, r.pipeline_layout, nullptr); for (auto pipeline : {r.scene.graphics_pipeline, r.scene.cull_pipeline, r.scene.post_pipeline, r.scene.hzb_pipeline}) if (pipeline) vkDestroyPipeline(r.device, pipeline, nullptr); for (auto pipeline : {r.temporal.resolve_pipeline, r.temporal.composite_pipeline, r.temporal.direct_pipeline, r.temporal.transparent_pipeline, r.temporal.gpu_pipeline}) if (pipeline) vkDestroyPipeline(r.device, pipeline, nullptr); r.pipeline_layout = previous_layout; r.pipeline = previous; r.ui_pipeline = previous_ui; r.shadow_pipeline = previous_shadow; r.sprite_pipeline = previous_sprite; r.temporal_ui_pipeline = previous_temporal_ui; r.light_tile_pipeline = previous_light_tile; r.light_tiles_capable = previous_light_tiles_capable; r.scene.graphics_pipeline = previous_gpu; r.scene.cull_pipeline = previous_cull; r.scene.post_pipeline = previous_post; r.scene.hzb_pipeline = previous_hzb; r.temporal.resolve_pipeline = previous_temporal_pipelines[0]; r.temporal.composite_pipeline = previous_temporal_pipelines[1]; r.temporal.direct_pipeline = previous_temporal_pipelines[2]; r.temporal.transparent_pipeline = previous_temporal_pipelines[3]; r.temporal.gpu_pipeline = previous_temporal_pipelines[4]; r.shader_layouts = previous_layout_fingerprints; r.scene.shader_layouts = previous_gpu_fingerprints; r.temporal.shader_layouts = previous_temporal_layouts; r.temporal.scene_shader_layouts = previous_temporal_scene_layouts; 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); vkDestroyPipeline(r.device, previous_temporal_ui, nullptr); if (previous_light_tile) vkDestroyPipeline(r.device, previous_light_tile, nullptr); for (auto pipeline : {previous_gpu, previous_cull, previous_post, previous_hzb}) if (pipeline) vkDestroyPipeline(r.device, pipeline, nullptr); for (auto pipeline : previous_temporal_pipelines) if (pipeline) vkDestroyPipeline(r.device, pipeline, nullptr); vkDestroyPipelineLayout(r.device, previous_layout, nullptr); ++r.temporal.shader_generation; 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(); } } void Renderer::set_visibility_mode(VisibilityMode mode) { if (impl_->config.visibility_mode == mode) return; if (mode != VisibilityMode::Direct && impl_->scene.available && !impl_->scene.graphics_layout) { try { impl_->make_scene_descriptors_and_pipelines(); if (impl_->temporal.resolve_layout) { impl_->destroy_temporal_interfaces(); impl_->make_temporal_interfaces_and_pipelines(); } } catch (...) { impl_->destroy_scene_interfaces(); throw; } } impl_->config.visibility_mode = mode; if (mode == VisibilityMode::GpuOcclusion && impl_->scene.hzb_supported && !impl_->scene.hzb[0].handle) impl_->make_targets(); impl_->scene.hzb_history_valid = false; impl_->instance_tracker.invalidate_view(impl_->scene.previous_view_id); } VisibilityMode Renderer::visibility_mode() const { return impl_->config.visibility_mode; } void Renderer::set_temporal_mode(TemporalMode mode, float scale) { (void)temporal_internal_extent(impl_->width, impl_->height, mode, scale); if (impl_->config.temporal_mode == mode && impl_->config.render_scale == scale) return; impl_->config.temporal_mode = mode; impl_->config.render_scale = scale; impl_->make_targets(); } TemporalMode Renderer::temporal_mode() const { return impl_->config.temporal_mode; } float Renderer::render_scale() const { return impl_->config.render_scale; } void Renderer::set_visibility_diagnostics(bool enabled) { impl_->config.visibility_diagnostics = enabled; } void Renderer::set_temporal_diagnostics(bool enabled) { impl_->config.temporal_diagnostics = enabled; if (!enabled) impl_->destroy(impl_->temporal.pixel_counts_stage); } std::optional Renderer::hzb_debug_image(std::uint32_t mip) { auto& renderer = *impl_; if (!renderer.scene.hzb_history_valid || !renderer.scene.hzb_supported) return std::nullopt; if (mip >= renderer.scene.hzb_mips) throw std::out_of_range("HZB debug mip outside pyramid"); auto& pyramid = renderer.scene.hzb[renderer.scene.hzb_current]; const std::uint32_t w = std::max(1u, pyramid.width >> mip); const std::uint32_t h = std::max(1u, pyramid.height >> mip); auto staging = renderer.make_buffer(VkDeviceSize(w) * h * sizeof(float), VK_BUFFER_USAGE_TRANSFER_DST_BIT, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT, VK_MEMORY_PROPERTY_HOST_CACHED_BIT); try { renderer.begin(); renderer.transition(renderer.command, pyramid, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, VK_IMAGE_ASPECT_COLOR_BIT); VkBufferImageCopy copy{}; copy.imageSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, mip, 0, 1}; copy.imageExtent = {w, h, 1}; vkCmdCopyImageToBuffer(renderer.command, pyramid.handle, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, staging.handle, 1, ©); renderer.transition(renderer.command, pyramid, VK_IMAGE_LAYOUT_GENERAL, VK_IMAGE_ASPECT_COLOR_BIT); renderer.submit(); HzbDebugImage result; result.width = w; result.height = h; result.rgba.resize(std::size_t(w) * h * 4); void* mapped{}; check(vkMapMemory(renderer.device, staging.memory, 0, staging.size, 0, &mapped), "Map HZB debug image"); const auto* depth = static_cast(mapped); for (std::size_t i = 0; i < std::size_t(w) * h; ++i) { const float value = std::isfinite(depth[i]) ? std::clamp(depth[i], 0.f, 1.f) : 1.f; const auto gray = static_cast(std::lround(std::pow(value, 32.f) * 255)); result.rgba[4 * i] = result.rgba[4 * i + 1] = result.rgba[4 * i + 2] = gray; result.rgba[4 * i + 3] = 255; } vkUnmapMemory(renderer.device, staging.memory); renderer.destroy(staging); return result; } catch (...) { renderer.destroy(staging); throw; } } 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