diff --git a/core/config/project_settings.cpp b/core/config/project_settings.cpp index 565a97342571..73bf7fb1edaa 100644 --- a/core/config/project_settings.cpp +++ b/core/config/project_settings.cpp @@ -1620,6 +1620,7 @@ ProjectSettings::ProjectSettings() { GLOBAL_DEF_RST(PropertyInfo(Variant::INT, "rendering/rendering_device/vsync/frame_queue_size", PROPERTY_HINT_RANGE, "2,3,1"), 2); GLOBAL_DEF_RST(PropertyInfo(Variant::INT, "rendering/rendering_device/vsync/swapchain_image_count", PROPERTY_HINT_RANGE, "2,4,1"), 3); + GLOBAL_DEF(PropertyInfo(Variant::BOOL, "rendering/rendering_device/vsync/wait_for_present"), true); GLOBAL_DEF(PropertyInfo(Variant::INT, "rendering/rendering_device/staging_buffer/block_size_kb", PROPERTY_HINT_RANGE, "4,2048,1,or_greater"), 256); GLOBAL_DEF(PropertyInfo(Variant::INT, "rendering/rendering_device/staging_buffer/max_size_mb", PROPERTY_HINT_RANGE, "1,1024,1,or_greater"), 128); GLOBAL_DEF(PropertyInfo(Variant::INT, "rendering/rendering_device/staging_buffer/texture_upload_region_size_px", PROPERTY_HINT_RANGE, "1,256,1,or_greater"), 64); diff --git a/doc/classes/ProjectSettings.xml b/doc/classes/ProjectSettings.xml index 863fef324281..78c7ed4a9870 100644 --- a/doc/classes/ProjectSettings.xml +++ b/doc/classes/ProjectSettings.xml @@ -3205,6 +3205,10 @@ [b]Note:[/b] This property is only read when the project starts. There is currently no way to change this value at run-time. [b]Note:[/b] Some platforms may restrict the actual value. + + Instructs Godot to use Waitable Swapchains when supported. Normally Godot allows the GPU to get [member rendering/rendering_device/vsync/frame_queue_size] ahead of the GPU. Assuming frame_queue_size = 2, Godot normally starts working on frame N+2 with the CPU when the GPU is done working on frame N. This setting makes Godot start frame N+2 when frame N is [i]done presenting[/i]. + This gap between when the GPU finishes its work and when that work actually appears on screen can increase latency. [b]Enabling this setting thus reduces latency[/b] at the cost of some framerate (framerate may decrease because the CPU has to wait more time doing nothing). + The number of descriptors per pool. Godot's Vulkan backend uses linear pools for descriptors that will be created and destroyed within a single frame. Instead of destroying every single descriptor every frame, they all can be destroyed at once by resetting the pool they belong to. A larger number is more efficient up to a limit, after that it will only waste RAM (maximum efficiency is achieved when there is no more than 1 pool per frame). A small number could end up with one pool per descriptor, which negatively impacts performance. diff --git a/doc/classes/RenderingDevice.xml b/doc/classes/RenderingDevice.xml index 67a265698d6d..ea6de04410fb 100644 --- a/doc/classes/RenderingDevice.xml +++ b/doc/classes/RenderingDevice.xml @@ -680,6 +680,11 @@ This is only used by Vulkan in debug builds. Godot must also be started with the [code]--extra-gpu-memory-tracking[/code] [url=$DOCS_URL/tutorials/editor/command_line_tutorial.html]command line argument[/url]. + + + + + @@ -792,6 +797,13 @@ [b]Note:[/b] Resource names are only set when the engine runs in verbose mode ([method OS.is_stdout_verbose] = [code]true[/code]), or when using an engine build compiled with the [code]dev_mode=yes[/code] SCons option. The graphics driver must also support the [code]VK_EXT_DEBUG_UTILS_EXTENSION_NAME[/code] Vulkan extension for named resources to work. + + + + + See [member ProjectSettings.rendering/rendering_device/vsync/wait_for_present]. + + diff --git a/drivers/d3d12/rendering_device_driver_d3d12.cpp b/drivers/d3d12/rendering_device_driver_d3d12.cpp index b964cb85b6fd..aef92c351f62 100644 --- a/drivers/d3d12/rendering_device_driver_d3d12.cpp +++ b/drivers/d3d12/rendering_device_driver_d3d12.cpp @@ -2400,6 +2400,11 @@ void RenderingDeviceDriverD3D12::_swap_chain_release_buffers(SwapChain *p_swap_c p_swap_chain->render_targets.clear(); p_swap_chain->render_targets_info.clear(); + if (p_swap_chain->waitable_object) { + CloseHandle(p_swap_chain->waitable_object); + p_swap_chain->waitable_object = nullptr; + } + for (RDD::FramebufferID framebuffer : p_swap_chain->framebuffers) { framebuffer_free(framebuffer); } @@ -2457,6 +2462,7 @@ Error RenderingDeviceDriverD3D12::swap_chain_resize(CommandQueueID p_cmd_queue, case DisplayServer::VSYNC_ENABLED: { sync_interval = 1; present_flags = 0; + creation_flags = DXGI_SWAP_CHAIN_FLAG_FRAME_LATENCY_WAITABLE_OBJECT; } break; case DisplayServer::VSYNC_DISABLED: { sync_interval = 0; @@ -2467,6 +2473,7 @@ Error RenderingDeviceDriverD3D12::swap_chain_resize(CommandQueueID p_cmd_queue, default: sync_interval = 1; present_flags = 0; + creation_flags = DXGI_SWAP_CHAIN_FLAG_FRAME_LATENCY_WAITABLE_OBJECT; break; } @@ -2509,6 +2516,11 @@ Error RenderingDeviceDriverD3D12::swap_chain_resize(CommandQueueID p_cmd_queue, ERR_FAIL_COND_V(!SUCCEEDED(res), ERR_CANT_CREATE); } + if (creation_flags & DXGI_SWAP_CHAIN_FLAG_FRAME_LATENCY_WAITABLE_OBJECT) { + swap_chain->d3d_swap_chain->SetMaximumFrameLatency(UINT(frames.size())); + swap_chain->waitable_object = swap_chain->d3d_swap_chain->GetFrameLatencyWaitableObject(); + } + if (surface->composition_device.Get() == nullptr) { using PFN_DCompositionCreateDevice = HRESULT(WINAPI *)(IDXGIDevice *, REFIID, void **); PFN_DCompositionCreateDevice pfn_DCompositionCreateDevice = (PFN_DCompositionCreateDevice)(void *)GetProcAddress(context_driver->lib_dcomp, "DCompositionCreateDevice"); @@ -2625,6 +2637,42 @@ void RenderingDeviceDriverD3D12::swap_chain_free(SwapChainID p_swap_chain) { memdelete(swap_chain); } +Error RenderingDeviceDriverD3D12::swap_chain_wait_for_present(SwapChainID p_swap_chain, uint32_t p_max_frame_delay) { + SwapChain *swap_chain = (SwapChain *)(p_swap_chain.id); + if (swap_chain->waitable_object != NULL) { + UINT timeout = 1000u; + + HRESULT res; + + { + UINT current_frame_latency = 0u; + res = swap_chain->d3d_swap_chain->GetMaximumFrameLatency(¤t_frame_latency); + + ERR_FAIL_COND_V_MSG(!SUCCEEDED(res), FAILED, "GetMaximumFrameLatency failed with error " + vformat("0x%08ux", (uint64_t)res) + "."); + + if (p_max_frame_delay != current_frame_latency) { + swap_chain->d3d_swap_chain->SetMaximumFrameLatency(UINT(p_max_frame_delay)); + } + } + + do { + res = WaitForSingleObjectEx(swap_chain->waitable_object, timeout, FALSE); + } while (res == WAIT_IO_COMPLETION); + + if (res == WAIT_TIMEOUT) { + ERR_FAIL_COND_V_MSG(!SUCCEEDED(res), ERR_TIMEOUT, "swap_chain_wait_for_present timeout exceeded."); + } else if (res == (HRESULT)WAIT_FAILED) { + DWORD error = GetLastError(); + ERR_FAIL_COND_V_MSG(!SUCCEEDED(res), FAILED, "WaitForSingleObjectEx failed with error " + vformat("0x%08ux", (uint64_t)error) + "."); + } else if (res != WAIT_OBJECT_0) { + ERR_FAIL_COND_V_MSG(!SUCCEEDED(res), FAILED, "WaitForSingleObjectEx returned " + vformat("0x%08ux", (uint64_t)res) + "."); + } + return OK; + } else { + return ERR_UNAVAILABLE; + } +} + /*********************/ /**** FRAMEBUFFER ****/ /*********************/ diff --git a/drivers/d3d12/rendering_device_driver_d3d12.h b/drivers/d3d12/rendering_device_driver_d3d12.h index bc5ccc012350..301a483a3926 100644 --- a/drivers/d3d12/rendering_device_driver_d3d12.h +++ b/drivers/d3d12/rendering_device_driver_d3d12.h @@ -465,6 +465,7 @@ class RenderingDeviceDriverD3D12 : public RenderingDeviceDriver { struct SwapChain { ComPtr d3d_swap_chain; + HANDLE waitable_object; RenderingContextDriver::SurfaceID surface = RenderingContextDriver::SurfaceID(); UINT present_flags = 0; UINT sync_interval = 1; @@ -486,6 +487,7 @@ class RenderingDeviceDriverD3D12 : public RenderingDeviceDriver { virtual RenderPassID swap_chain_get_render_pass(SwapChainID p_swap_chain) override; virtual DataFormat swap_chain_get_format(SwapChainID p_swap_chain) override; virtual void swap_chain_free(SwapChainID p_swap_chain) override; + virtual Error swap_chain_wait_for_present(SwapChainID p_swap_chain, uint32_t p_max_frame_delay) override final; /*********************/ /**** FRAMEBUFFER ****/ diff --git a/drivers/metal/rendering_device_driver_metal.h b/drivers/metal/rendering_device_driver_metal.h index b4e5b6e7a37f..a5025ec36219 100644 --- a/drivers/metal/rendering_device_driver_metal.h +++ b/drivers/metal/rendering_device_driver_metal.h @@ -224,6 +224,7 @@ class API_AVAILABLE(macos(11.0), ios(14.0), tvos(14.0)) RenderingDeviceDriverMet virtual DataFormat swap_chain_get_format(SwapChainID p_swap_chain) override final; virtual void swap_chain_set_max_fps(SwapChainID p_swap_chain, int p_max_fps) override final; virtual void swap_chain_free(SwapChainID p_swap_chain) override final; + virtual Error swap_chain_wait_for_present(SwapChainID p_swap_chain, uint32_t p_max_frame_delay) override final; #pragma mark - Frame Buffer diff --git a/drivers/metal/rendering_device_driver_metal.mm b/drivers/metal/rendering_device_driver_metal.mm index cdae0f93331a..b015dcbea5fc 100644 --- a/drivers/metal/rendering_device_driver_metal.mm +++ b/drivers/metal/rendering_device_driver_metal.mm @@ -1052,6 +1052,10 @@ static const API_AVAILABLE(macos(11.0), ios(14.0), tvos(14.0)) MTLSamplerBorderC memdelete(swap_chain); } +Error RenderingDeviceDriverMetal::swap_chain_wait_for_present(SwapChainID p_swap_chain, uint32_t p_max_frame_delay) { + return ERR_UNAVAILABLE; +} + #pragma mark - Frame buffer RDD::FramebufferID RenderingDeviceDriverMetal::framebuffer_create(RenderPassID p_render_pass, VectorView p_attachments, uint32_t p_width, uint32_t p_height) { diff --git a/drivers/vulkan/rendering_device_driver_vulkan.cpp b/drivers/vulkan/rendering_device_driver_vulkan.cpp index b62d39d18f3c..1f35d19fb73e 100644 --- a/drivers/vulkan/rendering_device_driver_vulkan.cpp +++ b/drivers/vulkan/rendering_device_driver_vulkan.cpp @@ -531,6 +531,8 @@ Error RenderingDeviceDriverVulkan::_initialize_device_extensions() { _register_requested_device_extension(VK_EXT_ASTC_DECODE_MODE_EXTENSION_NAME, false); _register_requested_device_extension(VK_KHR_BUFFER_DEVICE_ADDRESS_EXTENSION_NAME, false); _register_requested_device_extension(VK_EXT_TEXTURE_COMPRESSION_ASTC_HDR_EXTENSION_NAME, false); + _register_requested_device_extension(VK_KHR_PRESENT_ID_EXTENSION_NAME, false); + _register_requested_device_extension(VK_KHR_PRESENT_WAIT_EXTENSION_NAME, false); // We don't actually use this extension, but some runtime components on some platforms // can and will fill the validation layers with useless info otherwise if not enabled. @@ -758,6 +760,8 @@ Error RenderingDeviceDriverVulkan::_check_device_capabilities() { VkPhysicalDevice16BitStorageFeaturesKHR storage_feature = {}; VkPhysicalDeviceMultiviewFeatures multiview_features = {}; VkPhysicalDevicePipelineCreationCacheControlFeatures pipeline_cache_control_features = {}; + VkPhysicalDevicePresentIdFeaturesKHR present_id_features = {}; + VkPhysicalDevicePresentWaitFeaturesKHR present_wait_features = {}; const bool use_1_2_features = physical_device_properties.apiVersion >= VK_API_VERSION_1_2; if (use_1_2_features) { @@ -807,6 +811,18 @@ Error RenderingDeviceDriverVulkan::_check_device_capabilities() { next_features = &pipeline_cache_control_features; } + if (enabled_device_extension_names.has(VK_KHR_PRESENT_ID_EXTENSION_NAME)) { + present_id_features.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PRESENT_ID_FEATURES_KHR; + present_id_features.pNext = next_features; + next_features = &present_id_features; + } + + if (enabled_device_extension_names.has(VK_KHR_PRESENT_WAIT_EXTENSION_NAME)) { + present_wait_features.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PRESENT_WAIT_FEATURES_KHR; + present_wait_features.pNext = next_features; + next_features = &present_wait_features; + } + VkPhysicalDeviceFeatures2 device_features_2 = {}; device_features_2.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2; device_features_2.pNext = next_features; @@ -866,6 +882,10 @@ Error RenderingDeviceDriverVulkan::_check_device_capabilities() { pipeline_cache_control_support = pipeline_cache_control_features.pipelineCreationCacheControl; } + if (enabled_device_extension_names.has(VK_KHR_PRESENT_ID_EXTENSION_NAME) && enabled_device_extension_names.has(VK_KHR_PRESENT_WAIT_EXTENSION_NAME)) { + waitable_swapchain_support = present_id_features.presentId && present_wait_features.presentWait; + } + if (enabled_device_extension_names.has(VK_EXT_DEVICE_FAULT_EXTENSION_NAME)) { device_fault_support = true; } @@ -1103,6 +1123,19 @@ Error RenderingDeviceDriverVulkan::_initialize_device(const LocalVector swapchains; thread_local LocalVector image_indices; thread_local LocalVector results; +#if !defined(SWAPPY_FRAME_PACING_ENABLED) + thread_local LocalVector present_ids; +#endif swapchains.clear(); image_indices.clear(); @@ -2789,6 +2828,18 @@ Error RenderingDeviceDriverVulkan::command_queue_execute_and_present(CommandQueu err = device_functions.QueuePresentKHR(device_queue.queue, &present_info); } #else + VkPresentIdKHR present_id = {}; + if (waitable_swapchain_support) { + present_ids.resize(swapchains.size()); + ++current_present_id; + for (uint64_t &id : present_ids) { + id = current_present_id; + } + present_id.sType = VK_STRUCTURE_TYPE_PRESENT_ID_KHR; + present_id.pPresentIds = present_ids.ptr(); + present_id.swapchainCount = present_ids.size(); + present_info.pNext = &present_id; + } err = device_functions.QueuePresentKHR(device_queue.queue, &present_info); #endif @@ -3488,6 +3539,28 @@ void RenderingDeviceDriverVulkan::swap_chain_free(SwapChainID p_swap_chain) { memdelete(swap_chain); } +Error RenderingDeviceDriverVulkan::swap_chain_wait_for_present(SwapChainID p_swap_chain, uint32_t p_max_frame_delay) { + if (!waitable_swapchain_support) { + return ERR_UNAVAILABLE; + } + + if (current_present_id <= p_max_frame_delay) { + return OK; + } + + SwapChain *swap_chain = (SwapChain *)(p_swap_chain.id); + + constexpr uint64_t wait_timeout = 100'000'000; + VkResult err = device_functions.WaitForPresentKHR(vk_device, swap_chain->vk_swapchain, current_present_id - p_max_frame_delay, wait_timeout); + + if (err == VK_TIMEOUT) { + ERR_FAIL_COND_V_MSG(err, ERR_TIMEOUT, "vkWaitForPresentKHR timeout exceeded."); + } else if (err != VK_SUCCESS && err != VK_SUBOPTIMAL_KHR) { + ERR_FAIL_COND_V_MSG(err, FAILED, "vkWaitForPresentKHR failed with error " + itos(err) + "."); + } + return OK; +} + /*********************/ /**** FRAMEBUFFER ****/ /*********************/ diff --git a/drivers/vulkan/rendering_device_driver_vulkan.h b/drivers/vulkan/rendering_device_driver_vulkan.h index d3a7bd3a9a2d..bcdfc2a6d02c 100644 --- a/drivers/vulkan/rendering_device_driver_vulkan.h +++ b/drivers/vulkan/rendering_device_driver_vulkan.h @@ -98,6 +98,7 @@ class RenderingDeviceDriverVulkan : public RenderingDeviceDriver { PFN_vkQueuePresentKHR QueuePresentKHR = nullptr; PFN_vkCreateRenderPass2KHR CreateRenderPass2KHR = nullptr; PFN_vkCmdEndRenderPass2KHR EndRenderPass2KHR = nullptr; + PFN_vkWaitForPresentKHR WaitForPresentKHR = nullptr; // Debug marker extensions. PFN_vkCmdDebugMarkerBeginEXT CmdDebugMarkerBeginEXT = nullptr; @@ -115,6 +116,7 @@ class RenderingDeviceDriverVulkan : public RenderingDeviceDriver { RenderingContextDriverVulkan *context_driver = nullptr; RenderingContextDriver::Device context_device = {}; uint32_t frame_count = 1; + uint64_t current_present_id = 0ul; VkPhysicalDevice physical_device = VK_NULL_HANDLE; VkPhysicalDeviceProperties physical_device_properties = {}; VkPhysicalDeviceFeatures physical_device_features = {}; @@ -141,6 +143,7 @@ class RenderingDeviceDriverVulkan : public RenderingDeviceDriver { bool swappy_frame_pacer_enable = false; uint8_t swappy_mode = 2; // See default value for display/window/frame_pacing/android/swappy_mode. #endif + bool waitable_swapchain_support = false; DeviceFunctions device_functions; void _register_requested_device_extension(const CharString &p_extension_name, bool p_required); @@ -383,6 +386,7 @@ class RenderingDeviceDriverVulkan : public RenderingDeviceDriver { virtual DataFormat swap_chain_get_format(SwapChainID p_swap_chain) override final; virtual void swap_chain_set_max_fps(SwapChainID p_swap_chain, int p_max_fps) override final; virtual void swap_chain_free(SwapChainID p_swap_chain) override final; + virtual Error swap_chain_wait_for_present(SwapChainID p_swap_chain, uint32_t p_max_frame_delay) override final; private: /*********************/ diff --git a/editor/editor_node.cpp b/editor/editor_node.cpp index 05af591d84bc..073fc24851fa 100644 --- a/editor/editor_node.cpp +++ b/editor/editor_node.cpp @@ -415,6 +415,9 @@ void EditorNode::_update_from_settings() { } _update_title(); + const bool wait_for_present = GLOBAL_GET("rendering/rendering_device/vsync/wait_for_present"); + RD::get_singleton()->set_wait_for_present(wait_for_present); + int current_filter = GLOBAL_GET("rendering/textures/canvas_textures/default_texture_filter"); if (current_filter != scene_root->get_default_canvas_item_texture_filter()) { Viewport::DefaultCanvasItemTextureFilter tf = (Viewport::DefaultCanvasItemTextureFilter)current_filter; diff --git a/main/main.cpp b/main/main.cpp index 0b8655a624d2..26605ec9fd33 100644 --- a/main/main.cpp +++ b/main/main.cpp @@ -4605,6 +4605,12 @@ static uint64_t navigation_process_max = 0; bool Main::iteration() { iterating++; + if (RD::get_singleton()) { + // We must do this right before input polling (i.e. DisplayServer**::process_events()). + // But we also must do this outside of timing measurements, so this is the 2nd best place. + RD::get_singleton()->_wait_for_present(); + } + const uint64_t ticks = OS::get_singleton()->get_ticks_usec(); Engine::get_singleton()->_frame_ticks = ticks; main_timer_sync.set_cpu_ticks_usec(ticks); diff --git a/servers/rendering/rendering_device.cpp b/servers/rendering/rendering_device.cpp index 1e25fc872102..4c74446340cb 100644 --- a/servers/rendering/rendering_device.cpp +++ b/servers/rendering/rendering_device.cpp @@ -6352,6 +6352,25 @@ void RenderingDevice::_free_pending_resources(int p_frame) { } } +void RenderingDevice::set_wait_for_present(bool p_wait_for_present) { + wait_for_present = p_wait_for_present; +} + +bool RenderingDevice::get_wait_for_present() const { + return wait_for_present; +} + +void RenderingDevice::_wait_for_present() { + if (!wait_for_present) { + return; + } + HashMap::ConstIterator it = screen_swap_chains.find(DisplayServer::MAIN_WINDOW_ID); + if (it != screen_swap_chains.end()) { + const uint32_t max_frame_delay = frames.size(); + driver->swap_chain_wait_for_present(it->value, max_frame_delay); + } +} + uint32_t RenderingDevice::get_frame_delay() const { return frames.size(); } @@ -6674,6 +6693,8 @@ Error RenderingDevice::initialize(RenderingContextDriver *p_context, DisplayServ frames.resize(frame_count); max_timestamp_query_elements = GLOBAL_GET("debug/settings/profiler/max_timestamp_query_elements"); + wait_for_present = GLOBAL_GET("rendering/rendering_device/vsync/wait_for_present"); + device = context->device_get(device_index); err = driver->initialize(device_index, frame_count); ERR_FAIL_COND_V_MSG(err != OK, FAILED, "Failed to initialize driver for device."); @@ -7425,6 +7446,8 @@ void RenderingDevice::_bind_methods() { ClassDB::bind_method(D_METHOD("has_feature", "feature"), &RenderingDevice::has_feature); ClassDB::bind_method(D_METHOD("limit_get", "limit"), &RenderingDevice::limit_get); + ClassDB::bind_method(D_METHOD("set_wait_for_present", "wait_for_present"), &RenderingDevice::set_wait_for_present); + ClassDB::bind_method(D_METHOD("get_wait_for_present"), &RenderingDevice::get_wait_for_present); ClassDB::bind_method(D_METHOD("get_frame_delay"), &RenderingDevice::get_frame_delay); ClassDB::bind_method(D_METHOD("submit"), &RenderingDevice::submit); ClassDB::bind_method(D_METHOD("sync"), &RenderingDevice::sync); diff --git a/servers/rendering/rendering_device.h b/servers/rendering/rendering_device.h index 5eb89538ae61..3ad141018edb 100644 --- a/servers/rendering/rendering_device.h +++ b/servers/rendering/rendering_device.h @@ -194,6 +194,9 @@ class RenderingDevice : public RenderingDeviceCommons { Error _buffer_initialize(Buffer *p_buffer, const uint8_t *p_data, size_t p_data_size, uint32_t p_required_align = 32); void update_perf_report(); + + bool wait_for_present = false; + // Flag for batching descriptor sets. bool descriptor_set_batching = true; // When true, the final draw call that copies our offscreen result into the Swapchain is put into its @@ -1623,6 +1626,11 @@ class RenderingDevice : public RenderingDeviceCommons { void swap_buffers(bool p_present); + void set_wait_for_present(bool p_wait_for_present); + bool get_wait_for_present() const; + + void _wait_for_present(); + uint32_t get_frame_delay() const; void submit(); diff --git a/servers/rendering/rendering_device_driver.h b/servers/rendering/rendering_device_driver.h index 31517ea58204..af0b5104f86f 100644 --- a/servers/rendering/rendering_device_driver.h +++ b/servers/rendering/rendering_device_driver.h @@ -484,6 +484,8 @@ class RenderingDeviceDriver : public RenderingDeviceCommons { // Wait until all rendering associated to the swap chain is finished before deleting it. virtual void swap_chain_free(SwapChainID p_swap_chain) = 0; + virtual Error swap_chain_wait_for_present(SwapChainID p_swap_chain, uint32_t p_max_frame_delay) = 0; + /*********************/ /**** FRAMEBUFFER ****/ /*********************/