Skip to content

Commit e17ebba

Browse files
KeyboardDannidarksylinc
authored andcommitted
Add CPU/GPU sync mode (renderer low latency)
Add CPU_GPU_SYNC_AUTO Remove redundant calls to get_ticks_usec() Fixes an unrelated bug when NAVIGATION_2D_DISABLED or NAVIGATION_3D_DISABLED are defined Co-authored-by: Danni <[email protected]> Co-authored-by: Matias N. Goldberg <[email protected]> Add waitable swapchain WIP
1 parent 19bb187 commit e17ebba

37 files changed

+897
-57
lines changed

core/config/project_settings.cpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1641,6 +1641,7 @@ ProjectSettings::ProjectSettings() {
16411641

16421642
GLOBAL_DEF_RST(PropertyInfo(Variant::INT, "rendering/rendering_device/vsync/frame_queue_size", PROPERTY_HINT_RANGE, "2,3,1"), 2);
16431643
GLOBAL_DEF_RST(PropertyInfo(Variant::INT, "rendering/rendering_device/vsync/swapchain_image_count", PROPERTY_HINT_RANGE, "2,4,1"), 3);
1644+
GLOBAL_DEF(PropertyInfo(Variant::INT, "rendering/rendering_device/vsync/latency_mode", PROPERTY_HINT_ENUM, "low,medium,high_throughput"), 0);
16441645
GLOBAL_DEF(PropertyInfo(Variant::INT, "rendering/rendering_device/staging_buffer/block_size_kb", PROPERTY_HINT_RANGE, "4,2048,1,or_greater"), 256);
16451646
GLOBAL_DEF(PropertyInfo(Variant::INT, "rendering/rendering_device/staging_buffer/max_size_mb", PROPERTY_HINT_RANGE, "1,1024,1,or_greater"), 128);
16461647
GLOBAL_DEF(PropertyInfo(Variant::INT, "rendering/rendering_device/staging_buffer/texture_upload_region_size_px", PROPERTY_HINT_RANGE, "1,256,1,or_greater"), 64);

doc/classes/Performance.xml

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -299,7 +299,34 @@
299299
<constant name="NAVIGATION_3D_OBSTACLE_COUNT" value="58" enum="Monitor">
300300
Number of active navigation obstacles in the [NavigationServer3D].
301301
</constant>
302-
<constant name="MONITOR_MAX" value="59" enum="Monitor">
302+
<constant name="FRAME_PACING_TOTAL_TIME" value="59" enum="Monitor">
303+
Value used by Godot when PACING_METHOD_SEQUENTIAL_SYNC is available and no other better latency-reduction method is available. to determine whether we should be in [constant RenderingServer.CPU_GPU_SYNC_PARALLEL] or in [constant RenderingServer.CPU_GPU_SYNC_SEQUENTIAL] mode. It is the sum of CPU Time + GPU Time. If the value is consistently high enough, Godot will determine to use PARALLEL, otherwise it will prefer SEQUENTIAL.
304+
[b]Note:[/b] this value attempts to be bereft of any additional time caused from waiting for V-Sync, therefore it will not match any other timing value (e.g. actual FPS, time taken by physics, etc). It is an estimation of how long the system would take if CPU and GPU were to be processing a frame serially, without the added delay of waiting for V-Sync.
305+
[b]Note:[/b] When using these monitors, it's best to set the Editor to a simple view like the Script tab to avoid the 2D/3D view from consuming system resources that could interfere with readings. Or better yet, run the Editor profiler in another machine.
306+
</constant>
307+
<constant name="FRAME_PACING_CPU_TIME" value="60" enum="Monitor">
308+
How long CPU took to process the frame, bereft of waiting delays caused by V-Sync. This value is an approximation and might not match any other timing value. If this value is added to GPU Time, you get Total Time. Useful to know where to focus optimization efforts.
309+
</constant>
310+
<constant name="FRAME_PACING_GPU_TIME" value="61" enum="Monitor">
311+
How long GPU took to process the frame, bereft of waiting delays caused by V-Sync. This value is an approximation and will not match any other timing value. If this value is added to CPU Time, you get Total Time. Useful to know where to focus optimization efforts.
312+
</constant>
313+
<constant name="FRAME_PACING_EVALUATED_SYNC_MODE" value="62" enum="Monitor">
314+
The mode decided by Godot that we should be in for each frame based on Total Time. "1" means we should be in [constant RenderingServer.CPU_GPU_SYNC_PARALLEL], "2" means we should be in [constant RenderingServer.CPU_GPU_SYNC_SEQUENTIAL].
315+
[b]Note:[/b] This value is not the actual mode Godot is in, because the decision is averaged over time to prevent Godot from constantly switching back and forth between PARALLEL and SEQUENTIAL (which would cause visible stutters). Ideally this should be a perfect flat line of either 1s or 2s. If you see the game going back and forth between 1 and 2, then the system is not fast enough for a smooth low-latency experience; or the game should be optimized further until it is.
316+
</constant>
317+
<constant name="FRAME_PACING_ACTUAL_SYNC_MODE" value="63" enum="Monitor">
318+
The [b]actual[/b] mode the game currently is. "1" means we are in [constant RenderingServer.CPU_GPU_SYNC_PARALLEL], "2" means we are in [constant RenderingServer.CPU_GPU_SYNC_SEQUENTIAL].
319+
[b]Note:[/b] This value should be as flat as possible. Every time it switches between "1" and "2", the game may suffer a small stutter.
320+
[b]Note:[/b] This value is ignored if PACING_METHOD_WAITABLE_SWAPCHAIN is available; or if [method RenderingDevice.get_latency_mode] is equal or higher than [constant RenderingDevice.LATENCY_MODE_MEDIUM]
321+
</constant>
322+
<constant name="FRAME_PACING_MISSED_HARD_TARGET" value="64" enum="Monitor">
323+
The number of frames where the "Total Time" has exceeded the monitor's refresh rate or the max FPS (whichever is lower). This does not necessarily mean the game has missed a V-Blank (if the game is running in [constant RenderingServer.CPU_GPU_SYNC_PARALLEL], then total frame time should be lower than the sum of CPU Time + GPU Time; thus in practice the app may not have missed any V-Blank) but it indicates V-Blanks would've been missed if executing in [constant RenderingServer.CPU_GPU_SYNC_SEQUENTIAL]. The value is expressed in thousands.
324+
For example one missed Hard Target will be shown as 1000. Two missed Hard Targets will be shown as 2000. This value decreases quickly over time. Missed Hard Targets weight heavily on Godot deciding to switch to PARALLEL to avoid degrading the experience further.
325+
[b]Note:[/b] While in PARALLEL mode, this counter is always reset to 0 each new frame, thus while [constant FRAME_PACING_ACTUAL_SYNC_MODE] is 1, this value will be either 0 or 1000, where a flat 1000 line means the game is always failing to reach the target framerate.
326+
[b]Note:[/b] Spikes in missed hard targets almost always means very visible stutter and thus should be avoided at all costs during gameplay. This value should be kept at 0 at all times. If the system isn't fast enough to keep the target framerate, this value should always be 1000 to keep pacing consistent.
327+
[b]Note:[/b] Periodically failing this metric means you should optimize your content to run faster, avoid spikes, or increase [member ProjectSettings.rendering/rendering_device/vsync/latency_mode] to a higher latency mode.
328+
</constant>
329+
<constant name="MONITOR_MAX" value="65" enum="Monitor">
303330
Represents the size of the [enum Monitor] enum.
304331
</constant>
305332
</constants>

doc/classes/ProjectSettings.xml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3211,6 +3211,12 @@
32113211
Try the [url=https://darksylinc.github.io/vsync_simulator/]V-Sync Simulator[/url], an interactive interface that simulates presentation to better understand how it is affected by different variables under various conditions.
32123212
[b]Note:[/b] This property is only read when the project starts. There is currently no way to change this value at run-time.
32133213
</member>
3214+
<member name="rendering/rendering_device/vsync/latency_mode" type="int" setter="" getter="" default="1">
3215+
Sets the default latency mode. Lower is better for input-to-display latency, but it will sacrifice FPS (frames per second) in return.
3216+
This setting can be changed at runtime via [method RenderingDevice.set_latency_mode]. See documentation for [constant RenderingDevice.LATENCY_MODE_LOW], [constant RenderingDevice.LATENCY_MODE_MEDIUM], and [constant RenderingDevice.LATENCY_MODE_HIGH_THROUGHPUT] for what each individual setting entails.
3217+
[b]Note:[/b] The setting [constant RenderingDevice.LATENCY_MODE_LOW_EXTREME] is not available through this property as it is strongly ill-advised to ship with this value as the default.
3218+
[b]Note:[/b] This property may be overridden with the [code]--latency-mode[/code] command-line argument. When this argument is used, this project setting is ignored.
3219+
</member>
32143220
<member name="rendering/rendering_device/vsync/swapchain_image_count" type="int" setter="" getter="" default="3">
32153221
The number of images the swapchain will consist of (back buffers + front buffer).
32163222
[code]2[/code] corresponds to double-buffering and [code]3[/code] to triple-buffering.

doc/classes/RenderingDevice.xml

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -646,6 +646,11 @@
646646
Returns the frame count kept by the graphics API. Higher values result in higher input lag, but with more consistent throughput. For the main [RenderingDevice], frames are cycled (usually 3 with triple-buffered V-Sync enabled). However, local [RenderingDevice]s only have 1 frame.
647647
</description>
648648
</method>
649+
<method name="get_latency_mode" qualifiers="const">
650+
<return type="int" enum="RenderingDevice.LatencyMode" />
651+
<description>
652+
</description>
653+
</method>
649654
<method name="get_memory_usage" qualifiers="const">
650655
<return type="int" />
651656
<param index="0" name="type" type="int" enum="RenderingDevice.MemoryType" />
@@ -782,6 +787,12 @@
782787
[b]Note:[/b] Only the main [RenderingDevice] returned by [method RenderingServer.get_rendering_device] has a width. If called on a local [RenderingDevice], this method prints an error and returns [constant INVALID_ID].
783788
</description>
784789
</method>
790+
<method name="set_latency_mode">
791+
<return type="void" />
792+
<param index="0" name="p_latency_mode" type="int" enum="RenderingDevice.LatencyMode" />
793+
<description>
794+
</description>
795+
</method>
785796
<method name="set_resource_name">
786797
<return type="void" />
787798
<param index="0" name="id" type="RID" />
@@ -2730,5 +2741,27 @@
27302741
<constant name="DRAW_IGNORE_ALL" value="720640" enum="DrawFlags" is_bitfield="true">
27312742
Ignore the previous contents of all attachments.
27322743
</constant>
2744+
<constant name="LATENCY_MODE_LOW_EXTREME" value="0" enum="LatencyMode">
2745+
Godot is willing to sacrifice a considerable amount of FPS (frames per second) to achieve the lowest possible latency.
2746+
It's generally recommended to use [constant LATENCY_MODE_LOW] instead, as the FPS cost tends to be too high.
2747+
It is strongly recommended this setting should only be set by the end user in user settings, and not be shipped by default.
2748+
[b]Note:[/b] Actually receiving low latency is not guaranteed, as it depends on various factors such as system speed, scene complexity and driver support.
2749+
[b]Note:[/b] Consider using [constant DisplayServer.VSYNC_ADAPTIVE] to reduce jitter and stutter while in this mode.
2750+
</constant>
2751+
<constant name="LATENCY_MODE_LOW" value="1" enum="LatencyMode">
2752+
Godot is willing to sacrifice some amount of FPS (frames per second) to achieve a generably enjoyable and acceptable low latency experience.
2753+
This is the recommended setting.
2754+
[b]Note:[/b] Actually receiving low latency is not guaranteed, as it depends on various factors such as system speed, scene complexity and driver support.
2755+
[b]Note:[/b] Consider using [constant DisplayServer.VSYNC_ADAPTIVE] to reduce jitter and stutter while in this mode.
2756+
</constant>
2757+
<constant name="LATENCY_MODE_MEDIUM" value="2" enum="LatencyMode">
2758+
Godot is not willing to sacrifice much FPS (frames per second), but still maintaining a decent amount of latency.
2759+
This setting is best for slow systems, or scenes that are too complex to run at decent FPS in lower latency modes.
2760+
It's also useful as a workaround if the user is experiencing pacing (jitter, stutter) problems with lower latency settings.
2761+
</constant>
2762+
<constant name="LATENCY_MODE_HIGH_THROUGHPUT" value="3" enum="LatencyMode">
2763+
Godot will prefer maximizing FPS (frames per second), with no consideration for latency.
2764+
This setting is ideal for apps that have no user interaction, like servers or headless processes.
2765+
</constant>
27332766
</constants>
27342767
</class>

doc/classes/RenderingServer.xml

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1592,6 +1592,12 @@
15921592
Tries to free an object in the RenderingServer. To avoid memory leaks, this should be called after using an object as memory management does not occur automatically when using RenderingServer directly.
15931593
</description>
15941594
</method>
1595+
<method name="get_actual_cpu_gpu_sync_mode" qualifiers="const">
1596+
<return type="int" enum="RenderingServer.CPUGPUSyncMode" />
1597+
<description>
1598+
See [constant Performance.FRAME_PACING_ACTUAL_SYNC_MODE].
1599+
</description>
1600+
</method>
15951601
<method name="get_current_rendering_driver_name" qualifiers="const">
15961602
<return type="String" />
15971603
<description>
@@ -5870,6 +5876,16 @@
58705876
<constant name="GLOBAL_VAR_TYPE_MAX" value="29" enum="GlobalShaderParameterType">
58715877
Represents the size of the [enum GlobalShaderParameterType] enum.
58725878
</constant>
5879+
<constant name="CPU_GPU_SYNC_PARALLEL" value="0" enum="CPUGPUSyncMode">
5880+
Indicates the renderer is prioritizing higher framerate by allowing the CPU to queue up additional frames before they're rendered by the GPU. This allows the CPU and GPU to work in tandem, improving the framerate and framepacing in complex scenes at the expense of input latency. This default setting is suitable for most 3D applications, especially on mobile and lower-performance desktop hardware.
5881+
[b]Note:[/b] This is part of a fallback mechanism to reduce latency when PACING_METHOD_WAITABLE_SWAPCHAIN is not available.
5882+
</constant>
5883+
<constant name="CPU_GPU_SYNC_SEQUENTIAL" value="1" enum="CPUGPUSyncMode">
5884+
Indicates the renderer is prioritizing lower display latency by severely limiting how far the CPU is allowed to get ahead of the GPU when queuing frames. This can greatly help with input lag, at the cost of significantly reduced framerate in most scenes. This setting is useful for games and applications with simple graphics where responsive input is important. Your results may vary based on platform, drivers, and scene contents.
5885+
[b]Note:[/b] This is part of a fallback mechanism to reduce latency when PACING_METHOD_WAITABLE_SWAPCHAIN is not available.
5886+
[b]Note:[/b] Important FPS drops are expected while in this mode. It prioritizes low latency over framerate.
5887+
[b]Note:[/b] Stutter can be reduced if using [constant DisplayServer.VSYNC_ADAPTIVE]. But it risks always degenerating to [constant DisplayServer.VSYNC_DISABLED] if the system is too slow.
5888+
</constant>
58735889
<constant name="RENDERING_INFO_TOTAL_OBJECTS_IN_FRAME" value="0" enum="RenderingInfo">
58745890
Number of objects rendered in the current 3D scene. This varies depending on camera position and rotation.
58755891
</constant>

drivers/d3d12/rendering_device_driver_d3d12.cpp

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2510,6 +2510,11 @@ void RenderingDeviceDriverD3D12::_swap_chain_release_buffers(SwapChain *p_swap_c
25102510
p_swap_chain->render_targets.clear();
25112511
p_swap_chain->render_targets_info.clear();
25122512

2513+
if (p_swap_chain->waitable_object) {
2514+
CloseHandle(p_swap_chain->waitable_object);
2515+
p_swap_chain->waitable_object = nullptr;
2516+
}
2517+
25132518
for (RDD::FramebufferID framebuffer : p_swap_chain->framebuffers) {
25142519
framebuffer_free(framebuffer);
25152520
}
@@ -2567,6 +2572,7 @@ Error RenderingDeviceDriverD3D12::swap_chain_resize(CommandQueueID p_cmd_queue,
25672572
case DisplayServer::VSYNC_ENABLED: {
25682573
sync_interval = 1;
25692574
present_flags = 0;
2575+
creation_flags = DXGI_SWAP_CHAIN_FLAG_FRAME_LATENCY_WAITABLE_OBJECT;
25702576
} break;
25712577
case DisplayServer::VSYNC_DISABLED: {
25722578
sync_interval = 0;
@@ -2577,6 +2583,7 @@ Error RenderingDeviceDriverD3D12::swap_chain_resize(CommandQueueID p_cmd_queue,
25772583
default:
25782584
sync_interval = 1;
25792585
present_flags = 0;
2586+
creation_flags = DXGI_SWAP_CHAIN_FLAG_FRAME_LATENCY_WAITABLE_OBJECT;
25802587
break;
25812588
}
25822589

@@ -2619,6 +2626,11 @@ Error RenderingDeviceDriverD3D12::swap_chain_resize(CommandQueueID p_cmd_queue,
26192626
ERR_FAIL_COND_V(!SUCCEEDED(res), ERR_CANT_CREATE);
26202627
}
26212628

2629+
if (creation_flags & DXGI_SWAP_CHAIN_FLAG_FRAME_LATENCY_WAITABLE_OBJECT) {
2630+
swap_chain->d3d_swap_chain->SetMaximumFrameLatency(UINT(frames.size()));
2631+
swap_chain->waitable_object = swap_chain->d3d_swap_chain->GetFrameLatencyWaitableObject();
2632+
}
2633+
26222634
if (surface->composition_device.Get() == nullptr) {
26232635
using PFN_DCompositionCreateDevice = HRESULT(WINAPI *)(IDXGIDevice *, REFIID, void **);
26242636
PFN_DCompositionCreateDevice pfn_DCompositionCreateDevice = (PFN_DCompositionCreateDevice)(void *)GetProcAddress(context_driver->lib_dcomp, "DCompositionCreateDevice");
@@ -2735,6 +2747,49 @@ void RenderingDeviceDriverD3D12::swap_chain_free(SwapChainID p_swap_chain) {
27352747
memdelete(swap_chain);
27362748
}
27372749

2750+
Error RenderingDeviceDriverD3D12::swap_chain_wait_for_present(DisplayServer::WindowID p_window, SwapChainID p_swap_chain, uint32_t p_max_frame_delay) {
2751+
SwapChain *swap_chain = (SwapChain *)(p_swap_chain.id);
2752+
if (swap_chain->waitable_object != NULL) {
2753+
UINT timeout = 1000u;
2754+
2755+
HRESULT res;
2756+
2757+
{
2758+
UINT current_frame_latency = 0u;
2759+
res = swap_chain->d3d_swap_chain->GetMaximumFrameLatency(&current_frame_latency);
2760+
2761+
ERR_FAIL_COND_V_MSG(!SUCCEEDED(res), FAILED, "GetMaximumFrameLatency failed with error " + vformat("0x%08ux", (uint64_t)res) + ".");
2762+
2763+
if (p_max_frame_delay != current_frame_latency) {
2764+
swap_chain->d3d_swap_chain->SetMaximumFrameLatency(UINT(p_max_frame_delay));
2765+
}
2766+
}
2767+
2768+
do {
2769+
res = WaitForSingleObjectEx(swap_chain->waitable_object, timeout, FALSE);
2770+
} while (res == WAIT_IO_COMPLETION);
2771+
2772+
if (res == WAIT_TIMEOUT) {
2773+
ERR_FAIL_COND_V_MSG(!SUCCEEDED(res), ERR_TIMEOUT, "swap_chain_wait_for_present timeout exceeded.");
2774+
} else if (res == (HRESULT)WAIT_FAILED) {
2775+
DWORD error = GetLastError();
2776+
ERR_FAIL_COND_V_MSG(!SUCCEEDED(res), FAILED, "WaitForSingleObjectEx failed with error " + vformat("0x%08ux", (uint64_t)error) + ".");
2777+
} else if (res != WAIT_OBJECT_0) {
2778+
ERR_FAIL_COND_V_MSG(!SUCCEEDED(res), FAILED, "WaitForSingleObjectEx returned " + vformat("0x%08ux", (uint64_t)res) + ".");
2779+
}
2780+
return OK;
2781+
} else {
2782+
return ERR_UNAVAILABLE;
2783+
}
2784+
}
2785+
2786+
BitField<RDD::PacingMethod> RenderingDeviceDriverD3D12::get_available_pacing_methods() const {
2787+
BitField<PacingMethod> methods = 0;
2788+
methods.set_flag(PACING_METHOD_SEQUENTIAL_SYNC);
2789+
methods.set_flag(PACING_METHOD_WAITABLE_SWAPCHAIN);
2790+
return methods;
2791+
}
2792+
27382793
/*********************/
27392794
/**** FRAMEBUFFER ****/
27402795
/*********************/

drivers/d3d12/rendering_device_driver_d3d12.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -468,6 +468,7 @@ class RenderingDeviceDriverD3D12 : public RenderingDeviceDriver {
468468

469469
struct SwapChain {
470470
ComPtr<IDXGISwapChain3> d3d_swap_chain;
471+
HANDLE waitable_object;
471472
RenderingContextDriver::SurfaceID surface = RenderingContextDriver::SurfaceID();
472473
UINT present_flags = 0;
473474
UINT sync_interval = 1;
@@ -489,6 +490,9 @@ class RenderingDeviceDriverD3D12 : public RenderingDeviceDriver {
489490
virtual RenderPassID swap_chain_get_render_pass(SwapChainID p_swap_chain) override;
490491
virtual DataFormat swap_chain_get_format(SwapChainID p_swap_chain) override;
491492
virtual void swap_chain_free(SwapChainID p_swap_chain) override;
493+
virtual Error swap_chain_wait_for_present(DisplayServer::WindowID p_window, SwapChainID p_swap_chain, uint32_t p_max_frame_delay) override final;
494+
495+
virtual BitField<PacingMethod> get_available_pacing_methods() const override final;
492496

493497
/*********************/
494498
/**** FRAMEBUFFER ****/

drivers/gles3/storage/utilities.cpp

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -335,6 +335,10 @@ void Utilities::capture_timestamp(const String &p_name) {
335335
frames[frame].timestamp_count++;
336336
}
337337

338+
void Utilities::capture_timestamps_sync_mode_auto_end() {
339+
// Not implemented for OpenGL.
340+
}
341+
338342
void Utilities::_capture_timestamps_begin() {
339343
// frame is incremented at the end of the frame so this gives us the queries for frame - 2. By then they should be ready.
340344
if (frames[frame].timestamp_count) {

drivers/gles3/storage/utilities.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,7 @@ class Utilities : public RendererUtilities {
201201

202202
virtual void capture_timestamps_begin() override;
203203
virtual void capture_timestamp(const String &p_name) override;
204+
virtual void capture_timestamps_sync_mode_auto_end() override;
204205
virtual uint32_t get_captured_timestamps_count() const override;
205206
virtual uint64_t get_captured_timestamps_frame() const override;
206207
virtual uint64_t get_captured_timestamp_gpu_time(uint32_t p_index) const override;

drivers/metal/rendering_device_driver_metal.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -224,6 +224,9 @@ class API_AVAILABLE(macos(11.0), ios(14.0), tvos(14.0)) RenderingDeviceDriverMet
224224
virtual DataFormat swap_chain_get_format(SwapChainID p_swap_chain) override final;
225225
virtual void swap_chain_set_max_fps(SwapChainID p_swap_chain, int p_max_fps) override final;
226226
virtual void swap_chain_free(SwapChainID p_swap_chain) override final;
227+
virtual Error swap_chain_wait_for_present(DisplayServer::WindowID p_window, SwapChainID p_swap_chain, uint32_t p_max_frame_delay) override final;
228+
229+
virtual BitField<PacingMethod> get_available_pacing_methods() const override final;
227230

228231
#pragma mark - Frame Buffer
229232

0 commit comments

Comments
 (0)