Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Swappy & Pre-Transformed Swapchain #96439

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 14 additions & 3 deletions .github/workflows/android_builds.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,19 +24,19 @@ jobs:
cache-name: android-editor
target: editor
tests: false
sconsflags: arch=arm64 production=yes
sconsflags: arch=arm64 production=yes swappy=yes

- name: Template arm32 (target=template_release, arch=arm32)
cache-name: android-template-arm32
target: template_release
tests: false
sconsflags: arch=arm32
sconsflags: arch=arm32 swappy=yes

- name: Template arm64 (target=template_release, arch=arm64)
cache-name: android-template-arm64
target: template_release
tests: false
sconsflags: arch=arm64
sconsflags: arch=arm64 swappy=yes

steps:
- name: Checkout
Expand All @@ -59,6 +59,17 @@ jobs:
- name: Setup Python and SCons
uses: ./.github/actions/godot-deps

- name: Download pre-built Android Swappy Frame Pacing Library
uses: dsaltares/[email protected]
with:
repo: darksylinc/godot-swappy
version: tags/v2023.3.0.0
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

any ideas how often the library is updated?

I'm thinking we may need to update this at least once per release similarly to how we update the other Android dependencies.

file: godot-swappy.7z
target: swappy/godot-swappy.7z

- name: Extract pre-built Android Swappy Frame Pacing Library
run: 7za x -y swappy/godot-swappy.7z -o${{github.workspace}}/thirdparty/swappy-frame-pacing

- name: Compilation
uses: ./.github/actions/godot-build
with:
Expand Down
3 changes: 3 additions & 0 deletions SConstruct
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,7 @@ opts.Add(BoolVariable("use_volk", "Use the volk library to load the Vulkan loade
opts.Add(BoolVariable("disable_exceptions", "Force disabling exception handling code", True))
opts.Add("custom_modules", "A list of comma-separated directory paths containing custom modules to build.", "")
opts.Add(BoolVariable("custom_modules_recursive", "Detect custom modules recursively for each specified path.", True))
opts.Add(BoolVariable("swappy", "Use Swappy Frame Pacing Library in Android builds.", False))

# Advanced options
opts.Add(
Expand Down Expand Up @@ -611,6 +612,8 @@ if env["dev_mode"]:
if env["production"]:
env["use_static_cpp"] = methods.get_cmdline_bool("use_static_cpp", True)
env["debug_symbols"] = methods.get_cmdline_bool("debug_symbols", False)
if platform_arg == "android":
env["swappy"] = methods.get_cmdline_bool("swappy", True)
# LTO "auto" means we handle the preferred option in each platform detect.py.
env["lto"] = ARGUMENTS.get("lto", "auto")

Expand Down
6 changes: 6 additions & 0 deletions core/config/engine.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
#include "core/license.gen.h"
#include "core/variant/typed_array.h"
#include "core/version.h"
#include "servers/rendering/rendering_device.h"

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

void Engine::set_max_fps(int p_fps) {
_max_fps = p_fps > 0 ? p_fps : 0;

RenderingDevice *rd = RenderingDevice::get_singleton();
if (rd) {
rd->_set_max_fps(_max_fps);
}
}

int Engine::get_max_fps() const {
Expand Down
4 changes: 4 additions & 0 deletions core/config/project_settings.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1485,6 +1485,10 @@ ProjectSettings::ProjectSettings() {
GLOBAL_DEF("display/window/subwindows/embed_subwindows", true);
// Keep the enum values in sync with the `DisplayServer::VSyncMode` enum.
custom_prop_info["display/window/vsync/vsync_mode"] = PropertyInfo(Variant::INT, "display/window/vsync/vsync_mode", PROPERTY_HINT_ENUM, "Disabled,Enabled,Adaptive,Mailbox");

GLOBAL_DEF("display/window/frame_pacing/android/enable_frame_pacing", true);
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);
clayjohn marked this conversation as resolved.
Show resolved Hide resolved
clayjohn marked this conversation as resolved.
Show resolved Hide resolved

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");
GLOBAL_DEF("physics/2d/run_on_separate_thread", false);
GLOBAL_DEF("physics/3d/run_on_separate_thread", false);
Expand Down
11 changes: 11 additions & 0 deletions doc/classes/ProjectSettings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -816,6 +816,17 @@
<member name="display/window/energy_saving/keep_screen_on" type="bool" setter="" getter="" default="true">
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.
</member>
<member name="display/window/frame_pacing/android/enable_frame_pacing" type="bool" setter="" getter="" default="true">
Enable Swappy for stable frame pacing on Android. Highly recommended.
darksylinc marked this conversation as resolved.
Show resolved Hide resolved
[b]Note:[/b] This option will be forced off when using OpenXR.
</member>
<member name="display/window/frame_pacing/android/swappy_mode" type="int" setter="" getter="" default="2">
Swappy mode to use. The options are:
- pipeline_forced_on: Try to honor [member Engine.max_fps]. Pipelining is always on. This is the same behavior as Desktop PC.
- 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.
- 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.
[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).
</member>
<member name="display/window/handheld/orientation" type="int" setter="" getter="" default="0">
The default screen orientation to use on mobile devices. See [enum DisplayServer.ScreenOrientation] for possible values.
[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.
Expand Down
152 changes: 143 additions & 9 deletions drivers/vulkan/rendering_device_driver_vulkan.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,16 @@
#include "thirdparty/misc/smolv.h"
#include "vulkan_hooks.h"

#if defined(ANDROID_ENABLED)
#include "platform/android/java_godot_wrapper.h"
#include "platform/android/os_android.h"
#include "platform/android/thread_jandroid.h"
#endif

#if defined(SWAPPY_FRAME_PACING_ENABLED)
#include "thirdparty/swappy-frame-pacing/swappyVk.h"
#endif

#define ARRAY_SIZE(a) (sizeof(a) / sizeof(a[0]))

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

#if defined(SWAPPY_FRAME_PACING_ENABLED)
if (swappy_frame_pacer_enable) {
char **swappy_required_extensions;
uint32_t swappy_required_extensions_count = 0;
// Determine number of extensions required by Swappy frame pacer.
SwappyVk_determineDeviceExtensions(physical_device, device_extension_count, device_extensions.ptr(), &swappy_required_extensions_count, nullptr);

if (swappy_required_extensions_count < device_extension_count) {
// Determine the actual extensions.
swappy_required_extensions = (char **)malloc(swappy_required_extensions_count * sizeof(char *));
char *pRequiredExtensionsData = (char *)malloc(swappy_required_extensions_count * (VK_MAX_EXTENSION_NAME_SIZE + 1));
for (uint32_t i = 0; i < swappy_required_extensions_count; i++) {
swappy_required_extensions[i] = &pRequiredExtensionsData[i * (VK_MAX_EXTENSION_NAME_SIZE + 1)];
}
SwappyVk_determineDeviceExtensions(physical_device, device_extension_count,
device_extensions.ptr(), &swappy_required_extensions_count, swappy_required_extensions);

// Enable extensions requested by Swappy.
for (uint32_t i = 0; i < swappy_required_extensions_count; i++) {
CharString extension_name(swappy_required_extensions[i]);
if (requested_device_extensions.has(extension_name)) {
enabled_device_extension_names.insert(extension_name);
}
}

free(pRequiredExtensionsData);
free(swappy_required_extensions);
}
}
#endif

#ifdef DEV_ENABLED
for (uint32_t i = 0; i < device_extension_count; i++) {
print_verbose(String("VULKAN: Found device extension ") + String::utf8(device_extensions[i].extensionName));
Expand Down Expand Up @@ -1371,6 +1412,18 @@ Error RenderingDeviceDriverVulkan::initialize(uint32_t p_device_index, uint32_t
max_descriptor_sets_per_pool = GLOBAL_GET("rendering/rendering_device/vulkan/max_descriptors_per_pool");
breadcrumb_buffer = buffer_create(sizeof(uint32_t), BufferUsageBits::BUFFER_USAGE_TRANSFER_TO_BIT, MemoryAllocationType::MEMORY_ALLOCATION_TYPE_CPU);

#if defined(SWAPPY_FRAME_PACING_ENABLED)
swappy_frame_pacer_enable = GLOBAL_GET("display/window/frame_pacing/android/enable_frame_pacing");
swappy_mode = GLOBAL_GET("display/window/frame_pacing/android/swappy_mode");

if (VulkanHooks::get_singleton() != nullptr) {
// Hooks control device creation & possibly presentation
// (e.g. OpenXR) thus it's too risky to use Swappy.
swappy_frame_pacer_enable = false;
OS::get_singleton()->print("VulkanHooks detected (e.g. OpenXR): Force-disabling Swappy Frame Pacing.\n");
}
#endif

return OK;
}

Expand Down Expand Up @@ -2356,6 +2409,14 @@ RDD::CommandQueueID RenderingDeviceDriverVulkan::command_queue_create(CommandQue

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

#if defined(SWAPPY_FRAME_PACING_ENABLED)
if (swappy_frame_pacer_enable) {
VkQueue selected_queue;
vkGetDeviceQueue(vk_device, family_index, picked_queue_index, &selected_queue);
SwappyVk_setQueueFamilyIndex(vk_device, selected_queue, family_index);
}
#endif

// Create the virtual queue.
CommandQueue *command_queue = memnew(CommandQueue);
command_queue->queue_family = family_index;
Expand Down Expand Up @@ -2501,7 +2562,16 @@ Error RenderingDeviceDriverVulkan::command_queue_execute_and_present(CommandQueu
present_info.pResults = results.ptr();

device_queue.submit_mutex.lock();
#if defined(SWAPPY_FRAME_PACING_ENABLED)
if (swappy_frame_pacer_enable) {
err = SwappyVk_queuePresent(device_queue.queue, &present_info);
} else {
err = device_functions.QueuePresentKHR(device_queue.queue, &present_info);
}
#else
err = device_functions.QueuePresentKHR(device_queue.queue, &present_info);
#endif

device_queue.submit_mutex.unlock();

// 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.
Expand Down Expand Up @@ -2681,6 +2751,14 @@ void RenderingDeviceDriverVulkan::_swap_chain_release(SwapChain *swap_chain) {
swap_chain->framebuffers.clear();

if (swap_chain->vk_swapchain != VK_NULL_HANDLE) {
#if defined(SWAPPY_FRAME_PACING_ENABLED)
if (swappy_frame_pacer_enable) {
// Swappy has a bug where the ANativeWindow will be leaked if we call
clayjohn marked this conversation as resolved.
Show resolved Hide resolved
// SwappyVk_destroySwapchain, so we must release it by hand.
SwappyVk_setWindow(vk_device, swap_chain->vk_swapchain, nullptr);
SwappyVk_destroySwapchain(vk_device, swap_chain->vk_swapchain);
}
#endif
device_functions.DestroySwapchainKHR(vk_device, swap_chain->vk_swapchain, VKC::get_allocation_callbacks(VK_OBJECT_TYPE_SWAPCHAIN_KHR));
swap_chain->vk_swapchain = VK_NULL_HANDLE;
}
Expand Down Expand Up @@ -2797,6 +2875,20 @@ Error RenderingDeviceDriverVulkan::swap_chain_resize(CommandQueueID p_cmd_queue,
VkResult err = functions.GetPhysicalDeviceSurfaceCapabilitiesKHR(physical_device, surface->vk_surface, &surface_capabilities);
ERR_FAIL_COND_V(err != VK_SUCCESS, ERR_CANT_CREATE);

// No swapchain yet, this is the first time we're creating it.
if (!swap_chain->vk_swapchain) {
uint32_t width = surface_capabilities.currentExtent.width;
uint32_t height = surface_capabilities.currentExtent.height;
if (surface_capabilities.currentTransform & VK_SURFACE_TRANSFORM_ROTATE_90_BIT_KHR ||
surface_capabilities.currentTransform & VK_SURFACE_TRANSFORM_ROTATE_270_BIT_KHR) {
// Swap to get identity width and height.
surface_capabilities.currentExtent.height = width;
surface_capabilities.currentExtent.width = height;
}

native_display_size = surface_capabilities.currentExtent;
}

VkExtent2D extent;
if (surface_capabilities.currentExtent.width == 0xFFFFFFFF) {
// The current extent is currently undefined, so the current surface width and height will be clamped to the surface's capabilities.
Expand Down Expand Up @@ -2863,15 +2955,8 @@ Error RenderingDeviceDriverVulkan::swap_chain_resize(CommandQueueID p_cmd_queue,
desired_swapchain_images = MIN(desired_swapchain_images, surface_capabilities.maxImageCount);
}

// Prefer identity transform if it's supported, use the current transform otherwise.
// This behavior is intended as Godot does not supported native rotation in platforms that use these bits.
// Refer to the comment in command_queue_present() for more details.
VkSurfaceTransformFlagBitsKHR surface_transform_bits;
if (surface_capabilities.supportedTransforms & VK_SURFACE_TRANSFORM_IDENTITY_BIT_KHR) {
surface_transform_bits = VK_SURFACE_TRANSFORM_IDENTITY_BIT_KHR;
} else {
surface_transform_bits = surface_capabilities.currentTransform;
}
VkSurfaceTransformFlagBitsKHR surface_transform_bits = surface_capabilities.currentTransform;

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

#if defined(SWAPPY_FRAME_PACING_ENABLED)
if (swappy_frame_pacer_enable) {
const double max_fps = Engine::get_singleton()->get_max_fps();
const uint64_t max_time = max_fps > 0 ? uint64_t((1000.0 * 1000.0 * 1000.0) / max_fps) : 0;

SwappyVk_initAndGetRefreshCycleDuration(get_jni_env(), static_cast<OS_Android *>(OS::get_singleton())->get_godot_java()->get_activity(), physical_device,
vk_device, swap_chain->vk_swapchain, &swap_chain->refresh_duration);
SwappyVk_setWindow(vk_device, swap_chain->vk_swapchain, static_cast<OS_Android *>(OS::get_singleton())->get_native_window());
SwappyVk_setSwapIntervalNS(vk_device, swap_chain->vk_swapchain, MAX(swap_chain->refresh_duration, max_time));

enum SwappyModes {
PIPELINE_FORCED_ON,
AUTO_FPS_PIPELINE_FORCED_ON,
AUTO_FPS_AUTO_PIPELINE,
};

switch (swappy_mode) {
case PIPELINE_FORCED_ON:
SwappyVk_setAutoSwapInterval(true);
SwappyVk_setAutoPipelineMode(true);
break;
case AUTO_FPS_PIPELINE_FORCED_ON:
SwappyVk_setAutoSwapInterval(true);
SwappyVk_setAutoPipelineMode(false);
break;
case AUTO_FPS_AUTO_PIPELINE:
SwappyVk_setAutoSwapInterval(false);
SwappyVk_setAutoPipelineMode(false);
break;
}
}
#endif

uint32_t image_count = 0;
err = device_functions.GetSwapchainImagesKHR(vk_device, swap_chain->vk_swapchain, &image_count, nullptr);
ERR_FAIL_COND_V(err != VK_SUCCESS, ERR_CANT_CREATE);
Expand Down Expand Up @@ -3049,6 +3167,22 @@ RDD::DataFormat RenderingDeviceDriverVulkan::swap_chain_get_format(SwapChainID p
}
}

void RenderingDeviceDriverVulkan::swap_chain_set_max_fps(SwapChainID p_swap_chain, int p_max_fps) {
DEV_ASSERT(p_swap_chain.id != 0);

#ifdef SWAPPY_FRAME_PACING_ENABLED
if (!swappy_frame_pacer_enable) {
return;
}

SwapChain *swap_chain = (SwapChain *)(p_swap_chain.id);
if (swap_chain->vk_swapchain != VK_NULL_HANDLE) {
const uint64_t max_time = p_max_fps > 0 ? uint64_t((1000.0 * 1000.0 * 1000.0) / p_max_fps) : 0;
SwappyVk_setSwapIntervalNS(vk_device, swap_chain->vk_swapchain, MAX(swap_chain->refresh_duration, max_time));
}
#endif
}

void RenderingDeviceDriverVulkan::swap_chain_free(SwapChainID p_swap_chain) {
DEV_ASSERT(p_swap_chain.id != 0);

Expand Down
10 changes: 10 additions & 0 deletions drivers/vulkan/rendering_device_driver_vulkan.h
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,11 @@ class RenderingDeviceDriverVulkan : public RenderingDeviceDriver {
bool device_fault_support = false;
#if defined(VK_TRACK_DEVICE_MEMORY)
bool device_memory_report_support = false;
#endif
#if defined(SWAPPY_FRAME_PACING_ENABLED)
// Swappy frame pacer for Android.
bool swappy_frame_pacer_enable = false;
uint8_t swappy_mode = 2; // See default value for display/window/frame_pacing/android/swappy_mode.
#endif
DeviceFunctions device_functions;

Expand Down Expand Up @@ -350,16 +355,21 @@ class RenderingDeviceDriverVulkan : public RenderingDeviceDriver {
LocalVector<uint32_t> command_queues_acquired_semaphores;
RenderPassID render_pass;
uint32_t image_index = 0;
#ifdef ANDROID_ENABLED
uint64_t refresh_duration = 0;
#endif
};

void _swap_chain_release(SwapChain *p_swap_chain);
VkExtent2D native_display_size;

public:
virtual SwapChainID swap_chain_create(RenderingContextDriver::SurfaceID p_surface) override final;
virtual Error swap_chain_resize(CommandQueueID p_cmd_queue, SwapChainID p_swap_chain, uint32_t p_desired_framebuffer_count) override final;
virtual FramebufferID swap_chain_acquire_framebuffer(CommandQueueID p_cmd_queue, SwapChainID p_swap_chain, bool &r_resize_required) override final;
virtual RenderPassID swap_chain_get_render_pass(SwapChainID p_swap_chain) override final;
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;

/*********************/
Expand Down
Loading
Loading