Skip to content

Commit aaa0e2f

Browse files
committed
Add Swappy & Pre-Transformed Swapchain
- Adds Swappy for Android for stable frame pacing - Implements pre-transformed Swapchain so that Godot's compositor is in charge of rotating the screen instead of Android's compositor (performance optimization for phones that don't have HW rotator) ============================ The work was performed by collaboration of TheForge and Google. I am merely splitting it up into smaller PRs and cleaning it up. Changes from original PR: - Removed "display/window/frame_pacing/android/target_frame_rate" option to use Engine::get_max_fps instead. - Target framerate can be changed at runtime using Engine::set_max_fps. - Swappy is enabled by default. - Added documentation. - enable_auto_swap setting is replaced with swappy_mode.
1 parent 92e51fc commit aaa0e2f

23 files changed

+1064
-14
lines changed

.github/workflows/android_builds.yml

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,19 +24,19 @@ jobs:
2424
cache-name: android-editor
2525
target: editor
2626
tests: false
27-
sconsflags: arch=arm64 production=yes
27+
sconsflags: arch=arm64 production=yes swappy=yes
2828

2929
- name: Template arm32 (target=template_release, arch=arm32)
3030
cache-name: android-template-arm32
3131
target: template_release
3232
tests: false
33-
sconsflags: arch=arm32
33+
sconsflags: arch=arm32 swappy=yes
3434

3535
- name: Template arm64 (target=template_release, arch=arm64)
3636
cache-name: android-template-arm64
3737
target: template_release
3838
tests: false
39-
sconsflags: arch=arm64
39+
sconsflags: arch=arm64 swappy=yes
4040

4141
steps:
4242
- name: Checkout
@@ -59,6 +59,17 @@ jobs:
5959
- name: Setup Python and SCons
6060
uses: ./.github/actions/godot-deps
6161

62+
- name: Download pre-built Android Swappy Frame Pacing Library
63+
uses: dsaltares/[email protected]
64+
with:
65+
repo: darksylinc/godot-swappy
66+
version: tags/v2023.3.0.0
67+
file: godot-swappy.7z
68+
target: swappy/godot-swappy.7z
69+
70+
- name: Extract pre-built Android Swappy Frame Pacing Library
71+
run: 7za x -y swappy/godot-swappy.7z -o${{github.workspace}}/thirdparty/swappy-frame-pacing
72+
6273
- name: Compilation
6374
uses: ./.github/actions/godot-build
6475
with:

SConstruct

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -229,6 +229,7 @@ opts.Add(BoolVariable("use_volk", "Use the volk library to load the Vulkan loade
229229
opts.Add(BoolVariable("disable_exceptions", "Force disabling exception handling code", True))
230230
opts.Add("custom_modules", "A list of comma-separated directory paths containing custom modules to build.", "")
231231
opts.Add(BoolVariable("custom_modules_recursive", "Detect custom modules recursively for each specified path.", True))
232+
opts.Add(BoolVariable("swappy", "Use Swappy Frame Pacing Library in Android builds.", False))
232233

233234
# Advanced options
234235
opts.Add(
@@ -611,6 +612,8 @@ if env["dev_mode"]:
611612
if env["production"]:
612613
env["use_static_cpp"] = methods.get_cmdline_bool("use_static_cpp", True)
613614
env["debug_symbols"] = methods.get_cmdline_bool("debug_symbols", False)
615+
if platform_arg == "android":
616+
env["swappy"] = methods.get_cmdline_bool("swappy", True)
614617
# LTO "auto" means we handle the preferred option in each platform detect.py.
615618
env["lto"] = ARGUMENTS.get("lto", "auto")
616619

core/config/engine.cpp

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
#include "core/license.gen.h"
3737
#include "core/variant/typed_array.h"
3838
#include "core/version.h"
39+
#include "servers/rendering/rendering_device.h"
3940

4041
void Engine::set_physics_ticks_per_second(int p_ips) {
4142
ERR_FAIL_COND_MSG(p_ips <= 0, "Engine iterations per second must be greater than 0.");
@@ -68,6 +69,11 @@ double Engine::get_physics_jitter_fix() const {
6869

6970
void Engine::set_max_fps(int p_fps) {
7071
_max_fps = p_fps > 0 ? p_fps : 0;
72+
73+
RenderingDevice *rd = RenderingDevice::get_singleton();
74+
if (rd) {
75+
rd->_set_max_fps(_max_fps);
76+
}
7177
}
7278

7379
int Engine::get_max_fps() const {

core/config/project_settings.cpp

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1485,6 +1485,10 @@ ProjectSettings::ProjectSettings() {
14851485
GLOBAL_DEF("display/window/subwindows/embed_subwindows", true);
14861486
// Keep the enum values in sync with the `DisplayServer::VSyncMode` enum.
14871487
custom_prop_info["display/window/vsync/vsync_mode"] = PropertyInfo(Variant::INT, "display/window/vsync/vsync_mode", PROPERTY_HINT_ENUM, "Disabled,Enabled,Adaptive,Mailbox");
1488+
1489+
GLOBAL_DEF("display/window/frame_pacing/android/enable_frame_pacing", true);
1490+
GLOBAL_DEF(PropertyInfo(Variant::INT, "display/window/frame_pacing/android/swappy_mode", PROPERTY_HINT_ENUM, "pipeline_forced_on,auto_fps_pipeline_forced_on,auto_fps_auto_pipeline"), 2);
1491+
14881492
custom_prop_info["rendering/driver/threads/thread_model"] = PropertyInfo(Variant::INT, "rendering/driver/threads/thread_model", PROPERTY_HINT_ENUM, "Single-Unsafe,Single-Safe,Multi-Threaded");
14891493
GLOBAL_DEF("physics/2d/run_on_separate_thread", false);
14901494
GLOBAL_DEF("physics/3d/run_on_separate_thread", false);

doc/classes/ProjectSettings.xml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -816,6 +816,17 @@
816816
<member name="display/window/energy_saving/keep_screen_on" type="bool" setter="" getter="" default="true">
817817
If [code]true[/code], keeps the screen on (even in case of inactivity), so the screensaver does not take over. Works on desktop and mobile platforms.
818818
</member>
819+
<member name="display/window/frame_pacing/android/enable_frame_pacing" type="bool" setter="" getter="" default="true">
820+
Enable Swappy for stable frame pacing on Android. Highly recommended.
821+
[b]Note:[/b] This option will be forced off when using OpenXR.
822+
</member>
823+
<member name="display/window/frame_pacing/android/swappy_mode" type="int" setter="" getter="" default="2">
824+
Swappy mode to use. The options are:
825+
- pipeline_forced_on: Try to honor [member Engine.max_fps]. Pipelining is always on. This is the same behavior as Desktop PC.
826+
- auto_fps_pipeline_forced_on: Autocalculate max fps. Actual max_fps will be between 0 and [member Engine.max_fps]. While this sounds convenient, beware that Swappy will often downgrade max fps until it finds something that can be met and sustained. That means if your game runs between 40fps and 60fps on a 60hz screen, after some time Swappy will downgrade max fps so that the game renders at perfect 30fps.
827+
- auto_fps_auto_pipeline: Same as auto_fps_pipeline_forced_on, but if Swappy detects that rendering is very fast (e.g. it takes &lt; 8ms to render on a 60hz screen) Swappy will disable pipelining to minimize input latency. This is the default.
828+
[b]Note:[/b] If [member Engine.max_fps] is 0, actual max_fps will considered as to be the screen's refresh rate (often 60hz, 90hz or 120hz depending on device model and OS settings).
829+
</member>
819830
<member name="display/window/handheld/orientation" type="int" setter="" getter="" default="0">
820831
The default screen orientation to use on mobile devices. See [enum DisplayServer.ScreenOrientation] for possible values.
821832
[b]Note:[/b] When set to a portrait orientation, this project setting does not flip the project resolution's width and height automatically. Instead, you have to set [member display/window/size/viewport_width] and [member display/window/size/viewport_height] accordingly.

drivers/vulkan/rendering_device_driver_vulkan.cpp

Lines changed: 143 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,16 @@
3535
#include "thirdparty/misc/smolv.h"
3636
#include "vulkan_hooks.h"
3737

38+
#if defined(ANDROID_ENABLED)
39+
#include "platform/android/java_godot_wrapper.h"
40+
#include "platform/android/os_android.h"
41+
#include "platform/android/thread_jandroid.h"
42+
#endif
43+
44+
#if defined(SWAPPY_FRAME_PACING_ENABLED)
45+
#include "thirdparty/swappy-frame-pacing/swappyVk.h"
46+
#endif
47+
3848
#define ARRAY_SIZE(a) (sizeof(a) / sizeof(a[0]))
3949

4050
#define PRINT_NATIVE_COMMANDS 0
@@ -533,6 +543,37 @@ Error RenderingDeviceDriverVulkan::_initialize_device_extensions() {
533543
err = vkEnumerateDeviceExtensionProperties(physical_device, nullptr, &device_extension_count, device_extensions.ptr());
534544
ERR_FAIL_COND_V(err != VK_SUCCESS, ERR_CANT_CREATE);
535545

546+
#if defined(SWAPPY_FRAME_PACING_ENABLED)
547+
if (swappy_frame_pacer_enable) {
548+
char **swappy_required_extensions;
549+
uint32_t swappy_required_extensions_count = 0;
550+
// Determine number of extensions required by Swappy frame pacer.
551+
SwappyVk_determineDeviceExtensions(physical_device, device_extension_count, device_extensions.ptr(), &swappy_required_extensions_count, nullptr);
552+
553+
if (swappy_required_extensions_count < device_extension_count) {
554+
// Determine the actual extensions.
555+
swappy_required_extensions = (char **)malloc(swappy_required_extensions_count * sizeof(char *));
556+
char *pRequiredExtensionsData = (char *)malloc(swappy_required_extensions_count * (VK_MAX_EXTENSION_NAME_SIZE + 1));
557+
for (uint32_t i = 0; i < swappy_required_extensions_count; i++) {
558+
swappy_required_extensions[i] = &pRequiredExtensionsData[i * (VK_MAX_EXTENSION_NAME_SIZE + 1)];
559+
}
560+
SwappyVk_determineDeviceExtensions(physical_device, device_extension_count,
561+
device_extensions.ptr(), &swappy_required_extensions_count, swappy_required_extensions);
562+
563+
// Enable extensions requested by Swappy.
564+
for (uint32_t i = 0; i < swappy_required_extensions_count; i++) {
565+
CharString extension_name(swappy_required_extensions[i]);
566+
if (requested_device_extensions.has(extension_name)) {
567+
enabled_device_extension_names.insert(extension_name);
568+
}
569+
}
570+
571+
free(pRequiredExtensionsData);
572+
free(swappy_required_extensions);
573+
}
574+
}
575+
#endif
576+
536577
#ifdef DEV_ENABLED
537578
for (uint32_t i = 0; i < device_extension_count; i++) {
538579
print_verbose(String("VULKAN: Found device extension ") + String::utf8(device_extensions[i].extensionName));
@@ -1371,6 +1412,18 @@ Error RenderingDeviceDriverVulkan::initialize(uint32_t p_device_index, uint32_t
13711412
max_descriptor_sets_per_pool = GLOBAL_GET("rendering/rendering_device/vulkan/max_descriptors_per_pool");
13721413
breadcrumb_buffer = buffer_create(sizeof(uint32_t), BufferUsageBits::BUFFER_USAGE_TRANSFER_TO_BIT, MemoryAllocationType::MEMORY_ALLOCATION_TYPE_CPU);
13731414

1415+
#if defined(SWAPPY_FRAME_PACING_ENABLED)
1416+
swappy_frame_pacer_enable = GLOBAL_GET("display/window/frame_pacing/android/enable_frame_pacing");
1417+
swappy_mode = GLOBAL_GET("display/window/frame_pacing/android/swappy_mode");
1418+
1419+
if (VulkanHooks::get_singleton() != nullptr) {
1420+
// Hooks control device creation & possibly presentation
1421+
// (e.g. OpenXR) thus it's too risky to use Swappy.
1422+
swappy_frame_pacer_enable = false;
1423+
OS::get_singleton()->print("VulkanHooks detected (e.g. OpenXR): Force-disabling Swappy Frame Pacing.\n");
1424+
}
1425+
#endif
1426+
13741427
return OK;
13751428
}
13761429

@@ -2356,6 +2409,14 @@ RDD::CommandQueueID RenderingDeviceDriverVulkan::command_queue_create(CommandQue
23562409

23572410
ERR_FAIL_COND_V_MSG(picked_queue_index >= queue_family.size(), CommandQueueID(), "A queue in the picked family could not be found.");
23582411

2412+
#if defined(SWAPPY_FRAME_PACING_ENABLED)
2413+
if (swappy_frame_pacer_enable) {
2414+
VkQueue selected_queue;
2415+
vkGetDeviceQueue(vk_device, family_index, picked_queue_index, &selected_queue);
2416+
SwappyVk_setQueueFamilyIndex(vk_device, selected_queue, family_index);
2417+
}
2418+
#endif
2419+
23592420
// Create the virtual queue.
23602421
CommandQueue *command_queue = memnew(CommandQueue);
23612422
command_queue->queue_family = family_index;
@@ -2501,7 +2562,16 @@ Error RenderingDeviceDriverVulkan::command_queue_execute_and_present(CommandQueu
25012562
present_info.pResults = results.ptr();
25022563

25032564
device_queue.submit_mutex.lock();
2565+
#if defined(SWAPPY_FRAME_PACING_ENABLED)
2566+
if (swappy_frame_pacer_enable) {
2567+
err = SwappyVk_queuePresent(device_queue.queue, &present_info);
2568+
} else {
2569+
err = device_functions.QueuePresentKHR(device_queue.queue, &present_info);
2570+
}
2571+
#else
25042572
err = device_functions.QueuePresentKHR(device_queue.queue, &present_info);
2573+
#endif
2574+
25052575
device_queue.submit_mutex.unlock();
25062576

25072577
// Set the index to an invalid value. If any of the swap chains returned out of date, indicate it should be resized the next time it's acquired.
@@ -2681,6 +2751,14 @@ void RenderingDeviceDriverVulkan::_swap_chain_release(SwapChain *swap_chain) {
26812751
swap_chain->framebuffers.clear();
26822752

26832753
if (swap_chain->vk_swapchain != VK_NULL_HANDLE) {
2754+
#if defined(SWAPPY_FRAME_PACING_ENABLED)
2755+
if (swappy_frame_pacer_enable) {
2756+
// Swappy has a bug where the ANativeWindow will be leaked if we call
2757+
// SwappyVk_destroySwapchain, so we must release it by hand.
2758+
SwappyVk_setWindow(vk_device, swap_chain->vk_swapchain, nullptr);
2759+
SwappyVk_destroySwapchain(vk_device, swap_chain->vk_swapchain);
2760+
}
2761+
#endif
26842762
device_functions.DestroySwapchainKHR(vk_device, swap_chain->vk_swapchain, VKC::get_allocation_callbacks(VK_OBJECT_TYPE_SWAPCHAIN_KHR));
26852763
swap_chain->vk_swapchain = VK_NULL_HANDLE;
26862764
}
@@ -2797,6 +2875,20 @@ Error RenderingDeviceDriverVulkan::swap_chain_resize(CommandQueueID p_cmd_queue,
27972875
VkResult err = functions.GetPhysicalDeviceSurfaceCapabilitiesKHR(physical_device, surface->vk_surface, &surface_capabilities);
27982876
ERR_FAIL_COND_V(err != VK_SUCCESS, ERR_CANT_CREATE);
27992877

2878+
// No swapchain yet, this is the first time we're creating it.
2879+
if (!swap_chain->vk_swapchain) {
2880+
uint32_t width = surface_capabilities.currentExtent.width;
2881+
uint32_t height = surface_capabilities.currentExtent.height;
2882+
if (surface_capabilities.currentTransform & VK_SURFACE_TRANSFORM_ROTATE_90_BIT_KHR ||
2883+
surface_capabilities.currentTransform & VK_SURFACE_TRANSFORM_ROTATE_270_BIT_KHR) {
2884+
// Swap to get identity width and height.
2885+
surface_capabilities.currentExtent.height = width;
2886+
surface_capabilities.currentExtent.width = height;
2887+
}
2888+
2889+
native_display_size = surface_capabilities.currentExtent;
2890+
}
2891+
28002892
VkExtent2D extent;
28012893
if (surface_capabilities.currentExtent.width == 0xFFFFFFFF) {
28022894
// The current extent is currently undefined, so the current surface width and height will be clamped to the surface's capabilities.
@@ -2863,15 +2955,8 @@ Error RenderingDeviceDriverVulkan::swap_chain_resize(CommandQueueID p_cmd_queue,
28632955
desired_swapchain_images = MIN(desired_swapchain_images, surface_capabilities.maxImageCount);
28642956
}
28652957

2866-
// Prefer identity transform if it's supported, use the current transform otherwise.
2867-
// This behavior is intended as Godot does not supported native rotation in platforms that use these bits.
28682958
// Refer to the comment in command_queue_present() for more details.
2869-
VkSurfaceTransformFlagBitsKHR surface_transform_bits;
2870-
if (surface_capabilities.supportedTransforms & VK_SURFACE_TRANSFORM_IDENTITY_BIT_KHR) {
2871-
surface_transform_bits = VK_SURFACE_TRANSFORM_IDENTITY_BIT_KHR;
2872-
} else {
2873-
surface_transform_bits = surface_capabilities.currentTransform;
2874-
}
2959+
VkSurfaceTransformFlagBitsKHR surface_transform_bits = surface_capabilities.currentTransform;
28752960

28762961
VkCompositeAlphaFlagBitsKHR composite_alpha = VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR;
28772962
if (OS::get_singleton()->is_layered_allowed() || !(surface_capabilities.supportedCompositeAlpha & composite_alpha)) {
@@ -2898,7 +2983,7 @@ Error RenderingDeviceDriverVulkan::swap_chain_resize(CommandQueueID p_cmd_queue,
28982983
swap_create_info.minImageCount = desired_swapchain_images;
28992984
swap_create_info.imageFormat = swap_chain->format;
29002985
swap_create_info.imageColorSpace = swap_chain->color_space;
2901-
swap_create_info.imageExtent = extent;
2986+
swap_create_info.imageExtent = native_display_size;
29022987
swap_create_info.imageArrayLayers = 1;
29032988
swap_create_info.imageUsage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT;
29042989
swap_create_info.imageSharingMode = VK_SHARING_MODE_EXCLUSIVE;
@@ -2909,6 +2994,39 @@ Error RenderingDeviceDriverVulkan::swap_chain_resize(CommandQueueID p_cmd_queue,
29092994
err = device_functions.CreateSwapchainKHR(vk_device, &swap_create_info, VKC::get_allocation_callbacks(VK_OBJECT_TYPE_SWAPCHAIN_KHR), &swap_chain->vk_swapchain);
29102995
ERR_FAIL_COND_V(err != VK_SUCCESS, ERR_CANT_CREATE);
29112996

2997+
#if defined(SWAPPY_FRAME_PACING_ENABLED)
2998+
if (swappy_frame_pacer_enable) {
2999+
const double max_fps = Engine::get_singleton()->get_max_fps();
3000+
const uint64_t max_time = max_fps > 0 ? uint64_t((1000.0 * 1000.0 * 1000.0) / max_fps) : 0;
3001+
3002+
SwappyVk_initAndGetRefreshCycleDuration(get_jni_env(), static_cast<OS_Android *>(OS::get_singleton())->get_godot_java()->get_activity(), physical_device,
3003+
vk_device, swap_chain->vk_swapchain, &swap_chain->refresh_duration);
3004+
SwappyVk_setWindow(vk_device, swap_chain->vk_swapchain, static_cast<OS_Android *>(OS::get_singleton())->get_native_window());
3005+
SwappyVk_setSwapIntervalNS(vk_device, swap_chain->vk_swapchain, MAX(swap_chain->refresh_duration, max_time));
3006+
3007+
enum SwappyModes {
3008+
PIPELINE_FORCED_ON,
3009+
AUTO_FPS_PIPELINE_FORCED_ON,
3010+
AUTO_FPS_AUTO_PIPELINE,
3011+
};
3012+
3013+
switch (swappy_mode) {
3014+
case PIPELINE_FORCED_ON:
3015+
SwappyVk_setAutoSwapInterval(true);
3016+
SwappyVk_setAutoPipelineMode(true);
3017+
break;
3018+
case AUTO_FPS_PIPELINE_FORCED_ON:
3019+
SwappyVk_setAutoSwapInterval(true);
3020+
SwappyVk_setAutoPipelineMode(false);
3021+
break;
3022+
case AUTO_FPS_AUTO_PIPELINE:
3023+
SwappyVk_setAutoSwapInterval(false);
3024+
SwappyVk_setAutoPipelineMode(false);
3025+
break;
3026+
}
3027+
}
3028+
#endif
3029+
29123030
uint32_t image_count = 0;
29133031
err = device_functions.GetSwapchainImagesKHR(vk_device, swap_chain->vk_swapchain, &image_count, nullptr);
29143032
ERR_FAIL_COND_V(err != VK_SUCCESS, ERR_CANT_CREATE);
@@ -3049,6 +3167,22 @@ RDD::DataFormat RenderingDeviceDriverVulkan::swap_chain_get_format(SwapChainID p
30493167
}
30503168
}
30513169

3170+
void RenderingDeviceDriverVulkan::swap_chain_set_max_fps(SwapChainID p_swap_chain, int p_max_fps) {
3171+
DEV_ASSERT(p_swap_chain.id != 0);
3172+
3173+
#ifdef SWAPPY_FRAME_PACING_ENABLED
3174+
if (!swappy_frame_pacer_enable) {
3175+
return;
3176+
}
3177+
3178+
SwapChain *swap_chain = (SwapChain *)(p_swap_chain.id);
3179+
if (swap_chain->vk_swapchain != VK_NULL_HANDLE) {
3180+
const uint64_t max_time = p_max_fps > 0 ? uint64_t((1000.0 * 1000.0 * 1000.0) / p_max_fps) : 0;
3181+
SwappyVk_setSwapIntervalNS(vk_device, swap_chain->vk_swapchain, MAX(swap_chain->refresh_duration, max_time));
3182+
}
3183+
#endif
3184+
}
3185+
30523186
void RenderingDeviceDriverVulkan::swap_chain_free(SwapChainID p_swap_chain) {
30533187
DEV_ASSERT(p_swap_chain.id != 0);
30543188

drivers/vulkan/rendering_device_driver_vulkan.h

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,11 @@ class RenderingDeviceDriverVulkan : public RenderingDeviceDriver {
142142
bool device_fault_support = false;
143143
#if defined(VK_TRACK_DEVICE_MEMORY)
144144
bool device_memory_report_support = false;
145+
#endif
146+
#if defined(SWAPPY_FRAME_PACING_ENABLED)
147+
// Swappy frame pacer for Android.
148+
bool swappy_frame_pacer_enable = false;
149+
uint8_t swappy_mode = 2; // See default value for display/window/frame_pacing/android/swappy_mode.
145150
#endif
146151
DeviceFunctions device_functions;
147152

@@ -350,16 +355,21 @@ class RenderingDeviceDriverVulkan : public RenderingDeviceDriver {
350355
LocalVector<uint32_t> command_queues_acquired_semaphores;
351356
RenderPassID render_pass;
352357
uint32_t image_index = 0;
358+
#ifdef ANDROID_ENABLED
359+
uint64_t refresh_duration = 0;
360+
#endif
353361
};
354362

355363
void _swap_chain_release(SwapChain *p_swap_chain);
364+
VkExtent2D native_display_size;
356365

357366
public:
358367
virtual SwapChainID swap_chain_create(RenderingContextDriver::SurfaceID p_surface) override final;
359368
virtual Error swap_chain_resize(CommandQueueID p_cmd_queue, SwapChainID p_swap_chain, uint32_t p_desired_framebuffer_count) override final;
360369
virtual FramebufferID swap_chain_acquire_framebuffer(CommandQueueID p_cmd_queue, SwapChainID p_swap_chain, bool &r_resize_required) override final;
361370
virtual RenderPassID swap_chain_get_render_pass(SwapChainID p_swap_chain) override final;
362371
virtual DataFormat swap_chain_get_format(SwapChainID p_swap_chain) override final;
372+
virtual void swap_chain_set_max_fps(SwapChainID p_swap_chain, int p_max_fps) override final;
363373
virtual void swap_chain_free(SwapChainID p_swap_chain) override final;
364374

365375
/*********************/

0 commit comments

Comments
 (0)