Skip to content

Commit

Permalink
Add Swappy & Pre-Transformed Swapchain
Browse files Browse the repository at this point in the history
- 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.
  • Loading branch information
darksylinc committed Sep 1, 2024
1 parent 61598c5 commit 0faaac2
Show file tree
Hide file tree
Showing 25 changed files with 1,191 additions and 15 deletions.
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 @@ -1492,6 +1492,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);

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
10 changes: 10 additions & 0 deletions doc/classes/ProjectSettings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -816,6 +816,16 @@
<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.
</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.
Note: 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
143 changes: 134 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,13 @@
#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"
#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 @@ -532,6 +539,38 @@ 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(ANDROID_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 && 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 @@ -1370,6 +1409,11 @@ 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(ANDROID_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");
#endif

return OK;
}

Expand Down Expand Up @@ -2344,6 +2388,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(ANDROID_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 @@ -2489,7 +2541,16 @@ Error RenderingDeviceDriverVulkan::command_queue_execute_and_present(CommandQueu
present_info.pResults = results.ptr();

device_queue.submit_mutex.lock();
#if defined(ANDROID_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 @@ -2669,6 +2730,14 @@ void RenderingDeviceDriverVulkan::_swap_chain_release(SwapChain *swap_chain) {
swap_chain->framebuffers.clear();

if (swap_chain->vk_swapchain != VK_NULL_HANDLE) {
#if defined(ANDROID_ENABLED)
if (swappy_frame_pacer_enable) {
// Swappy has a bug where the ANativeWindow will be leaked if we call
// 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 @@ -2785,6 +2854,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 @@ -2851,15 +2934,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 @@ -2886,7 +2962,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 @@ -2897,6 +2973,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(ANDROID_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 @@ -3037,6 +3146,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 ANDROID_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 @@ -146,6 +146,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(ANDROID_ENABLED)
// Swappy frame pacer for Android
bool swappy_frame_pacer_enable = false;
uint8_t swappy_mode = true;
#endif
DeviceFunctions device_functions;

Expand Down Expand Up @@ -351,16 +356,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
52 changes: 52 additions & 0 deletions main/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -4619,3 +4619,55 @@ void Main::cleanup(bool p_force) {

OS::get_singleton()->finalize_core();
}

Main::ThermalState Main::thermal_state = Main::THERMAL_STATE_NONE;

const char *Main::get_thermal_state_string(Main::ThermalState p_thermalState) {
switch (p_thermalState) {
case THERMAL_STATE_NOT_SUPPORTED:
return "NotSupported";
case THERMAL_STATE_ERROR:
return "Error";
case THERMAL_STATE_NONE:
return "None";
case THERMAL_STATE_LIGHT:
return "Light";
case THERMAL_STATE_MODERATE:
return "Moderate";
case THERMAL_STATE_SEVERE:
return "Severe";
case THERMAL_STATE_CRITICAL:
return "Critical";
case THERMAL_STATE_EMERGENCY:
return "Emergency";
case THERMAL_STATE_SHUTDOWN:
return "Shutdown";
default:
return "Invalid";
}
}

void Main::update_thermal_state(ThermalState p_thermalState) {
DEV_ASSERT(p_thermalState >= THERMAL_STATE_MIN && p_thermalState < THERMAL_STATE_MAX);
const char *state_string = get_thermal_state_string(p_thermalState);
OS::get_singleton()->print("Thermal state changed : %s (%d)", state_string, p_thermalState);

if (p_thermalState > THERMAL_STATE_MODERATE) {
const String error_msg = "Thermal state is " + String(state_string);
OS::get_singleton()->alert(error_msg);
}
thermal_state = p_thermalState;
}

Main::ThermalState Main::get_thermal_state() {
return thermal_state;
}

float Main::get_thermal_headroom(int p_forecast_seconds) {
#if defined(ANDROID_ENABLED)
float GodotGetThermalHeadroom(int);
return GodotGetThermalHeadroom(p_forecast_seconds);
#else
return -1.0f;
#endif
}
22 changes: 22 additions & 0 deletions main/main.h
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,28 @@ class Main {
static const Vector<String> &get_forwardable_cli_arguments(CLIScope p_scope);
#endif

// Thermal state support
enum ThermalState {
THERMAL_STATE_MIN = -2,
THERMAL_STATE_NOT_SUPPORTED = THERMAL_STATE_MIN, // Current platform/device/build doesn't support ThermalState queries
THERMAL_STATE_ERROR = -1, // error acquiring thermal state

THERMAL_STATE_NONE = 0, // no state (perhaps device doesn't support it)
THERMAL_STATE_LIGHT = 1, // iOS Nominal
THERMAL_STATE_MODERATE = 2, // iOS Fair
THERMAL_STATE_SEVERE = 3, // iOS Serious
THERMAL_STATE_CRITICAL = 4, // iOS Critical
THERMAL_STATE_EMERGENCY = 5,
THERMAL_STATE_SHUTDOWN = 6,

THERMAL_STATE_MAX,
};
static ThermalState thermal_state;
static const char *get_thermal_state_string(ThermalState p_thermalState);
static void update_thermal_state(ThermalState p_thermalState);
static ThermalState get_thermal_state();
static float get_thermal_headroom(int p_forecast_seconds);

static int test_entrypoint(int argc, char *argv[], bool &tests_need_run);
static Error setup(const char *execpath, int argc, char *argv[], bool p_second_phase = true);
static Error setup2(bool p_show_boot_logo = true); // The thread calling setup2() will effectively become the main thread.
Expand Down
Loading

0 comments on commit 0faaac2

Please sign in to comment.