Commit 4f2e8a6b authored by Keith Packard's avatar Keith Packard Committed by Jakob Bornecrantz
Browse files

vulkan: Add VK_GOOGLE_display_timing extension (x11+display, anv+radv) [v10]



This adds support for the VK_GOOGLE_display timing extension, which
provides two things:

 1) Detailed information about when frames are displayed, including
    slack time between GPU execution and display frame.

 2) Absolute time control over swapchain queue processing. This allows
    the application to request frames be displayed at specific
    absolute times, using the same timebase as that provided in vblank
    events.

Support for this extension has been implemented for the x11 and
display backends; adding support to other backends should be
reasonable straightforward for one familiar with those systems and
should not require any additional device-specific code.

v2:
	Adjust GOOGLE_display_timing earliest value.  The
	earliestPresentTime for an image cannot be before the previous
	image was displayed, or even a frame later (in FIFO mode).

	Make GOOGLE_display_timing use render completed time.  Switch
	from VK_PIPELINE_TOP_OF_PIPE_BIT to
	VK_PIPELINE_STAGE_ALL_COMMANDS_BIT so that the time reported
	to applications as the end of rendering reflects the latest
	possible value to ensure that applications don't underestimate
	the amount of work done in the frame.

v3:
	Adopt Jason Ekstrand's coding conventions.  Declare variables
	at first use, eliminate extra whitespace between types and
	names. Wrap lines to 80 columns.
Suggested-by: Jason Ekstrand's avatarJason Ekstrand <jason.ekstrand@intel.com>

v4:
	Adapt to changes in MESA_query_timestamp extension

v5:
	Squash core bits and anv/radv wrappers into a single patch
Suggested-by: Jason Ekstrand's avatarJason Ekstrand <jason.ekstrand@intel.com>

v6:
	Switch from MESA_query_timestamp to EXT_calibrated_timestamps

v7:
	Ensure we target frame no earlier than desired. This means
	rounding the target frame up, rather than selecting the
	nearest one.
Suggested-by: Michel Dänzer's avatarMichel Dänzer <michel@daenzer.net>

v8:
	Re-order display_timing in anv_extensions.py. That code
	now requires extensions in alphabetical order.

	Rename wsi_mark_time to wsi_present_complete to make
	the functionality clearer.

v9:
	Adopt to upstream changes to anv and radv extensions.

v10:
	Remove GetPhysicalDevicePropertie usage.
Suggested-by: Lionel Landwerlin's avatarLionel Landwerlin <lionel.g.landwerlin@intel.com>
Signed-off-by: Keith Packard's avatarKeith Packard <keithp@keithp.com>
Signed-off-by: Jakob Bornecrantz's avatarJakob Bornecrantz <jakob@collabora.com>
parent 8a3dbf1c
Pipeline #273576 waiting for manual action with stages
in 17 seconds
......@@ -501,6 +501,7 @@ radv_physical_device_get_supported_extensions(const struct radv_physical_device
.ANDROID_native_buffer = device->rad_info.has_syncobj_wait_for_submit,
#endif
.GOOGLE_decorate_string = true,
.GOOGLE_display_timing = true,
.GOOGLE_hlsl_functionality1 = true,
.GOOGLE_user_type = true,
.NV_compute_shader_derivatives = true,
......
......@@ -347,3 +347,36 @@ VkResult radv_GetPhysicalDevicePresentRectanglesKHR(
surface,
pRectCount, pRects);
}
/* VK_GOOGLE_display_timing */
VkResult
radv_GetRefreshCycleDurationGOOGLE(
VkDevice _device,
VkSwapchainKHR swapchain,
VkRefreshCycleDurationGOOGLE *pDisplayTimingProperties)
{
RADV_FROM_HANDLE(radv_device, device, _device);
struct radv_physical_device *pdevice = device->physical_device;
return wsi_common_get_refresh_cycle_duration(&pdevice->wsi_device,
_device,
swapchain,
pDisplayTimingProperties);
}
VkResult
radv_GetPastPresentationTimingGOOGLE(VkDevice _device,
VkSwapchainKHR swapchain,
uint32_t *pPresentationTimingCount,
VkPastPresentationTimingGOOGLE
*pPresentationTimings)
{
RADV_FROM_HANDLE(radv_device, device, _device);
struct radv_physical_device *pdevice = device->physical_device;
return wsi_common_get_past_presentation_timing(&pdevice->wsi_device,
_device,
swapchain,
pPresentationTimingCount,
pPresentationTimings);
}
......@@ -306,6 +306,7 @@ get_device_extensions(const struct anv_physical_device *device,
.ANDROID_native_buffer = true,
#endif
.GOOGLE_decorate_string = true,
.GOOGLE_display_timing = true,
.GOOGLE_hlsl_functionality1 = true,
.GOOGLE_user_type = true,
.INTEL_performance_query = device->perf &&
......
......@@ -393,3 +393,34 @@ VkResult anv_GetPhysicalDevicePresentRectanglesKHR(
surface,
pRectCount, pRects);
}
/* VK_GOOGLE_display_timing */
VkResult
anv_GetRefreshCycleDurationGOOGLE(VkDevice _device,
VkSwapchainKHR swapchain,
VkRefreshCycleDurationGOOGLE
*pDisplayTimingProperties)
{
ANV_FROM_HANDLE(anv_device, device, _device);
return wsi_common_get_refresh_cycle_duration(&device->physical->wsi_device,
_device,
swapchain,
pDisplayTimingProperties);
}
VkResult
anv_GetPastPresentationTimingGOOGLE(VkDevice _device,
VkSwapchainKHR swapchain,
uint32_t *pPresentationTimingCount,
VkPastPresentationTimingGOOGLE
*pPresentationTimings)
{
ANV_FROM_HANDLE(anv_device, device, _device);
return wsi_common_get_past_presentation_timing(&device->physical->wsi_device,
_device,
swapchain,
pPresentationTimingCount,
pPresentationTimings);
}
......@@ -31,6 +31,7 @@
#include <time.h>
#include <stdlib.h>
#include <stdio.h>
#include <math.h>
VkResult
wsi_device_init(struct wsi_device *wsi,
......@@ -64,12 +65,14 @@ wsi_device_init(struct wsi_device *wsi,
};
GetPhysicalDeviceProperties2(pdevice, &pdp2);
wsi->timestamp_period = pdp2.properties.limits.timestampPeriod;
wsi->maxImageDimension2D = pdp2.properties.limits.maxImageDimension2D;
wsi->override_present_mode = VK_PRESENT_MODE_MAX_ENUM_KHR;
GetPhysicalDeviceMemoryProperties(pdevice, &wsi->memory_props);
GetPhysicalDeviceQueueFamilyProperties(pdevice, &wsi->queue_family_count, NULL);
#define WSI_GET_CB(func) \
wsi->func = (PFN_vk##func)proc_addr(pdevice, "vk" #func)
WSI_GET_CB(AllocateMemory);
......@@ -78,14 +81,18 @@ wsi_device_init(struct wsi_device *wsi,
WSI_GET_CB(BindImageMemory);
WSI_GET_CB(BeginCommandBuffer);
WSI_GET_CB(CmdCopyImageToBuffer);
WSI_GET_CB(CmdResetQueryPool);
WSI_GET_CB(CmdWriteTimestamp);
WSI_GET_CB(CreateBuffer);
WSI_GET_CB(CreateCommandPool);
WSI_GET_CB(CreateFence);
WSI_GET_CB(CreateImage);
WSI_GET_CB(CreateQueryPool);
WSI_GET_CB(DestroyBuffer);
WSI_GET_CB(DestroyCommandPool);
WSI_GET_CB(DestroyFence);
WSI_GET_CB(DestroyImage);
WSI_GET_CB(DestroyQueryPool);
WSI_GET_CB(EndCommandBuffer);
WSI_GET_CB(FreeMemory);
WSI_GET_CB(FreeCommandBuffers);
......@@ -98,8 +105,11 @@ wsi_device_init(struct wsi_device *wsi,
WSI_GET_CB(GetPhysicalDeviceFormatProperties);
WSI_GET_CB(GetPhysicalDeviceFormatProperties2KHR);
WSI_GET_CB(GetPhysicalDeviceImageFormatProperties2);
WSI_GET_CB(GetPhysicalDeviceQueueFamilyProperties);
WSI_GET_CB(GetQueryPoolResults);
WSI_GET_CB(ResetFences);
WSI_GET_CB(QueueSubmit);
WSI_GET_CB(GetCalibratedTimestampsEXT);
WSI_GET_CB(WaitForFences);
WSI_GET_CB(MapMemory);
WSI_GET_CB(UnmapMemory);
......@@ -191,6 +201,8 @@ wsi_swapchain_init(const struct wsi_device *wsi,
chain->device = device;
chain->alloc = *pAllocator;
chain->use_prime_blit = false;
chain->timing_insert = 0;
chain->timing_count = 0;
chain->cmd_pools =
vk_zalloc(pAllocator, sizeof(VkCommandPool) * wsi->queue_family_count, 8,
......@@ -290,6 +302,63 @@ wsi_swapchain_finish(struct wsi_swapchain *chain)
vk_object_base_finish(&chain->base);
}
VkResult
wsi_image_init_timestamp(const struct wsi_swapchain *chain,
struct wsi_image *image)
{
const struct wsi_device *wsi = chain->wsi;
VkResult result;
/* Set up command buffer to get timestamp info */
result = wsi->CreateQueryPool(
chain->device,
&(const VkQueryPoolCreateInfo) {
.sType = VK_STRUCTURE_TYPE_QUERY_POOL_CREATE_INFO,
.queryType = VK_QUERY_TYPE_TIMESTAMP,
.queryCount = 1,
},
NULL,
&image->query_pool);
if (result != VK_SUCCESS)
goto fail;
result = wsi->AllocateCommandBuffers(
chain->device,
&(const VkCommandBufferAllocateInfo) {
.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO,
.pNext = NULL,
.commandPool = chain->cmd_pools[0],
.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY,
.commandBufferCount = 1,
},
&image->timestamp_buffer);
if (result != VK_SUCCESS)
goto fail;
wsi->BeginCommandBuffer(
image->timestamp_buffer,
&(VkCommandBufferBeginInfo) {
.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO,
.flags = 0
});
wsi->CmdResetQueryPool(image->timestamp_buffer,
image->query_pool,
0, 1);
wsi->CmdWriteTimestamp(image->timestamp_buffer,
VK_PIPELINE_STAGE_ALL_COMMANDS_BIT,
image->query_pool,
0);
wsi->EndCommandBuffer(image->timestamp_buffer);
return VK_SUCCESS;
fail:
return result;
}
void
wsi_destroy_image(const struct wsi_swapchain *chain,
struct wsi_image *image)
......@@ -549,6 +618,128 @@ wsi_common_acquire_next_image2(const struct wsi_device *wsi,
return result;
}
static struct wsi_timing *
wsi_get_timing(struct wsi_swapchain *chain, uint32_t i)
{
uint32_t j = WSI_TIMING_HISTORY + chain->timing_insert -
chain->timing_count + i;
if (j >= WSI_TIMING_HISTORY)
j -= WSI_TIMING_HISTORY;
return &chain->timing[j];
}
static struct wsi_timing *
wsi_next_timing(struct wsi_swapchain *chain, int image_index)
{
uint32_t j = chain->timing_insert;
++chain->timing_insert;
if (chain->timing_insert >= WSI_TIMING_HISTORY)
chain->timing_insert = 0;
if (chain->timing_count < WSI_TIMING_HISTORY)
++chain->timing_count;
struct wsi_timing *timing = &chain->timing[j];
memset(timing, '\0', sizeof (*timing));
return timing;
}
void
wsi_present_complete(struct wsi_swapchain *swapchain,
struct wsi_image *image,
uint64_t ust,
uint64_t msc)
{
const struct wsi_device *wsi = swapchain->wsi;
struct wsi_timing *timing = image->timing;
if (!timing)
return;
uint64_t render_timestamp;
VkResult result = wsi->GetQueryPoolResults(
swapchain->device, image->query_pool,
0, 1, sizeof(render_timestamp), &render_timestamp,
sizeof (uint64_t),
VK_QUERY_RESULT_64_BIT|VK_QUERY_RESULT_WAIT_BIT);
if (result != VK_SUCCESS)
return;
static const VkCalibratedTimestampInfoEXT timestampInfo[2] = {
{
.sType = VK_STRUCTURE_TYPE_CALIBRATED_TIMESTAMP_INFO_EXT,
.pNext = NULL,
.timeDomain = VK_TIME_DOMAIN_DEVICE_EXT,
},
{
.sType = VK_STRUCTURE_TYPE_CALIBRATED_TIMESTAMP_INFO_EXT,
.pNext = NULL,
.timeDomain = VK_TIME_DOMAIN_CLOCK_MONOTONIC_EXT,
},
};
uint64_t timestamps[2];
uint64_t maxDeviation;
result = wsi->GetCalibratedTimestampsEXT(swapchain->device,
2,
timestampInfo,
timestamps,
&maxDeviation);
if (result != VK_SUCCESS)
return;
uint64_t current_gpu_timestamp = timestamps[0];
uint64_t current_time = timestamps[1];
VkRefreshCycleDurationGOOGLE display_timings;
swapchain->get_refresh_cycle_duration(swapchain, &display_timings);
uint64_t refresh_duration = display_timings.refreshDuration;
/* When did drawing complete (in nsec) */
int64_t since_render = (int64_t) floor ((double) (current_gpu_timestamp - render_timestamp) *
(double) wsi->timestamp_period + 0.5);
uint64_t render_time = current_time - since_render;
if (render_time > ust)
render_time = ust;
uint64_t render_frames = (ust - render_time) / refresh_duration;
uint64_t earliest_time = ust - render_frames * refresh_duration;
/* Use the presentation mode to figure out when the image could have been
* displayed. It couldn't have been displayed before the previous image, so
* use that as a lower bound. If we're in FIFO mode, then it couldn't have
* been displayed before one frame *after* the previous image
*/
uint64_t possible_frame = swapchain->frame_ust;
switch (swapchain->present_mode) {
case VK_PRESENT_MODE_FIFO_KHR:
case VK_PRESENT_MODE_FIFO_RELAXED_KHR:
possible_frame += refresh_duration;
break;
default:
break;
}
if (earliest_time < possible_frame)
earliest_time = possible_frame;
if (earliest_time > ust)
earliest_time = ust;
timing->timing.actualPresentTime = ust;
timing->timing.earliestPresentTime = earliest_time;
timing->timing.presentMargin = earliest_time - render_time;
timing->complete = true;
swapchain->frame_msc = msc;
swapchain->frame_ust = ust;
}
VkResult
wsi_common_queue_present(const struct wsi_device *wsi,
VkDevice device,
......@@ -560,11 +751,14 @@ wsi_common_queue_present(const struct wsi_device *wsi,
const VkPresentRegionsKHR *regions =
vk_find_struct_const(pPresentInfo->pNext, PRESENT_REGIONS_KHR);
const VkPresentTimesInfoGOOGLE *present_times_info =
vk_find_struct_const(pPresentInfo->pNext, PRESENT_TIMES_INFO_GOOGLE);
for (uint32_t i = 0; i < pPresentInfo->swapchainCount; i++) {
VK_FROM_HANDLE(wsi_swapchain, swapchain, pPresentInfo->pSwapchains[i]);
uint32_t image_index = pPresentInfo->pImageIndices[i];
VkResult result;
struct wsi_timing *timing = NULL;
if (swapchain->fences[image_index] == VK_NULL_HANDLE) {
const VkFenceCreateInfo fence_info = {
......@@ -599,9 +793,12 @@ wsi_common_queue_present(const struct wsi_device *wsi,
.memory = image->memory,
};
VkCommandBuffer submit_buffers[2];
VkSubmitInfo submit_info = {
.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO,
.pNext = &mem_signal,
.pCommandBuffers = submit_buffers,
.commandBufferCount = 0
};
VkPipelineStageFlags *stage_flags = NULL;
......@@ -632,10 +829,47 @@ wsi_common_queue_present(const struct wsi_device *wsi,
/* If we are using prime blits, we need to perform the blit now. The
* command buffer is attached to the image.
*/
submit_info.commandBufferCount = 1;
submit_info.pCommandBuffers =
&image->prime.blit_cmd_buffers[queue_family_index];
mem_signal.memory = image->prime.memory;
submit_buffers[submit_info.commandBufferCount++] =
image->prime.blit_cmd_buffers[queue_family_index];
}
/* Set up GOOGLE_display_timing bits */
if (present_times_info &&
present_times_info->pTimes != NULL &&
i < present_times_info->swapchainCount)
{
const VkPresentTimeGOOGLE *present_time =
&present_times_info->pTimes[i];
timing = wsi_next_timing(swapchain, pPresentInfo->pImageIndices[i]);
timing->timing.presentID = present_time->presentID;
timing->timing.desiredPresentTime = present_time->desiredPresentTime;
timing->target_msc = 0;
image->timing = timing;
if (present_time->desiredPresentTime != 0)
{
int64_t delta_nsec = (int64_t) (present_time->desiredPresentTime -
swapchain->frame_ust);
/* Set the target msc only if it's no more than two seconds from
* now, and not stale
*/
if (0 <= delta_nsec && delta_nsec <= 2000000000ul) {
VkRefreshCycleDurationGOOGLE refresh_timing;
swapchain->get_refresh_cycle_duration(swapchain,
&refresh_timing);
int64_t refresh = (int64_t) refresh_timing.refreshDuration;
int64_t frames = (delta_nsec + refresh - 1) / refresh;
timing->target_msc = swapchain->frame_msc + frames;
}
}
submit_buffers[submit_info.commandBufferCount++] =
image->timestamp_buffer;
}
result = wsi->QueueSubmit(queue, 1, &submit_info, swapchain->fences[image_index]);
......@@ -673,3 +907,52 @@ wsi_common_get_current_time(void)
{
return os_time_get_nano();
}
VkResult
wsi_common_get_refresh_cycle_duration(
const struct wsi_device *wsi,
VkDevice device_h,
VkSwapchainKHR _swapchain,
VkRefreshCycleDurationGOOGLE *pDisplayTimingProperties)
{
VK_FROM_HANDLE(wsi_swapchain, swapchain, _swapchain);
if (!swapchain->get_refresh_cycle_duration)
return VK_ERROR_EXTENSION_NOT_PRESENT;
return swapchain->get_refresh_cycle_duration(swapchain,
pDisplayTimingProperties);
}
VkResult
wsi_common_get_past_presentation_timing(
const struct wsi_device *wsi,
VkDevice device_h,
VkSwapchainKHR _swapchain,
uint32_t *count,
VkPastPresentationTimingGOOGLE *timings)
{
VK_FROM_HANDLE(wsi_swapchain, swapchain, _swapchain);
uint32_t timing_count_requested = *count;
uint32_t timing_count_available = 0;
/* Count the number of completed entries, copy */
for (uint32_t t = 0; t < swapchain->timing_count; t++) {
struct wsi_timing *timing = wsi_get_timing(swapchain, t);
if (timing->complete && !timing->consumed) {
if (timings && timing_count_available < timing_count_requested) {
timings[timing_count_available] = timing->timing;
timing->consumed = true;
}
timing_count_available++;
}
}
*count = timing_count_available;
if (timing_count_available > timing_count_requested && timings != NULL)
return VK_INCOMPLETE;
return VK_SUCCESS;
}
......@@ -88,6 +88,7 @@ struct wsi_device {
VkPhysicalDevice pdevice;
VkPhysicalDeviceMemoryProperties memory_props;
uint32_t queue_family_count;
float timestamp_period;
VkPhysicalDevicePCIBusInfoPropertiesEXT pci_bus_info;
......@@ -157,14 +158,18 @@ struct wsi_device {
WSI_CB(BindImageMemory);
WSI_CB(BeginCommandBuffer);
WSI_CB(CmdCopyImageToBuffer);
WSI_CB(CmdResetQueryPool);
WSI_CB(CmdWriteTimestamp);
WSI_CB(CreateBuffer);
WSI_CB(CreateCommandPool);
WSI_CB(CreateFence);
WSI_CB(CreateImage);
WSI_CB(CreateQueryPool);
WSI_CB(DestroyBuffer);
WSI_CB(DestroyCommandPool);
WSI_CB(DestroyFence);
WSI_CB(DestroyImage);
WSI_CB(DestroyQueryPool);
WSI_CB(EndCommandBuffer);
WSI_CB(FreeMemory);
WSI_CB(FreeCommandBuffers);
......@@ -176,8 +181,11 @@ struct wsi_device {
WSI_CB(GetPhysicalDeviceFormatProperties);
WSI_CB(GetPhysicalDeviceFormatProperties2KHR);
WSI_CB(GetPhysicalDeviceImageFormatProperties2);
WSI_CB(GetPhysicalDeviceQueueFamilyProperties);
WSI_CB(GetQueryPoolResults);
WSI_CB(ResetFences);
WSI_CB(QueueSubmit);
WSI_CB(GetCalibratedTimestampsEXT);
WSI_CB(WaitForFences);
WSI_CB(MapMemory);
WSI_CB(UnmapMemory);
......@@ -298,4 +306,27 @@ wsi_common_queue_present(const struct wsi_device *wsi,
uint64_t
wsi_common_get_current_time(void);
VkResult
wsi_common_convert_timestamp(const struct wsi_device *wsi,
VkDevice device_h,
VkSurfaceKHR surface_h,
uint64_t monotonic_timestamp,
uint64_t *surface_timestamp);
/* VK_GOOGLE_display_timing */
VkResult
wsi_common_get_refresh_cycle_duration(const struct wsi_device *wsi,
VkDevice device_h,
VkSwapchainKHR swapchain,
VkRefreshCycleDurationGOOGLE
*pDisplayTimingProperties);
VkResult
wsi_common_get_past_presentation_timing(const struct wsi_device *wsi,
VkDevice device_h,
VkSwapchainKHR swapchain,
uint32_t *pPresentationTimingCount,
VkPastPresentationTimingGOOGLE
*pPresentationTimings);
#endif
......@@ -76,6 +76,8 @@ typedef struct wsi_display_connector {
char *name;
bool connected;
bool active;
uint64_t last_frame;
uint64_t last_nsec;
struct list_head display_modes;
wsi_display_mode *current_mode;
drmModeModeInfo current_drm_mode;
......@@ -110,6 +112,7 @@ struct wsi_display {
enum wsi_image_state {
WSI_IMAGE_IDLE,
WSI_IMAGE_DRAWING,
WSI_IMAGE_WAITING,
WSI_IMAGE_QUEUED,
WSI_IMAGE_FLIPPING,
WSI_IMAGE_DISPLAYING
......@@ -119,6 +122,7 @@ struct wsi_display_image {
struct wsi_image base;
struct wsi_display_swapchain *chain;
enum wsi_image_state state;
struct wsi_display_fence *fence;
uint32_t fb_id;
uint32_t buffer[4];
uint64_t flip_sequence;
......@@ -139,6 +143,7 @@ struct wsi_display_fence {
bool destroyed;
uint32_t syncobj; /* syncobj to signal on event */
uint64_t sequence;
struct wsi_display_image *image;
};
static uint64_t fence_sequence;
......@@ -1045,6 +1050,7 @@ wsi_display_image_init(VkDevice device_h,
image->chain = chain;
image->state = WSI_IMAGE_IDLE;
image->fence = NULL;
image->fb_id = 0;
int ret = drmModeAddFB2(wsi->fd,
......@@ -1136,6 +1142,11 @@ wsi_display_idle_old_displaying(struct wsi_display_image *active_image)
static VkResult
_wsi_display_queue_next(struct wsi_swapchain *drv_chain);
static uint64_t widen_32_to_64(uint32_t narrow, uint64_t near)
{
return near + (int32_t) (narrow - near);
}
static void
wsi_display_page_flip_handler2(int fd,
unsigned int frame,
......@@ -1146,17 +1157,38 @@ wsi_display_page_flip_handler2(int fd,
{
struct wsi_display_image *image = data;
struct wsi_display_swapchain *chain = image->chain;
VkIcdSurfaceDisplay *surface = chain->surface;
wsi_display_mode *display_mode =
wsi_display_mode_from_handle(surface->displayMode);
wsi_display_connector *connector = display_mode->connector;
uint64_t nsec = (uint64_t) sec * 1000000000ull + (uint64_t) usec * 1000;
wsi_display_debug("image %ld displayed at %d\n",
image - &(image->chain->images[0]), frame);
/* Don't let time go backwards because this function has lower resolution
* than ktime */
if (nsec < connector->last_nsec)
nsec = connector->last_nsec;
image->state = WSI_IMAGE_DISPLAYING;
uint64_t frame64 = widen_32_to_64(frame, connector->last_frame);
connector->last_frame = frame64;