Last active
June 21, 2023 08:52
-
-
Save ardera/60af806830cc80f4a61451b6c333cb50 to your computer and use it in GitHub Desktop.
minimal embedder reproducing the flutter 2.2 embedder vsync issue. Compile with `-lsystemd` and `-lflutter_engine`. The asset bundle of the default flutter create app should be inside `/tmp/hello_world`, the `icudtl.dat` inside `/usr/lib/`.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#define _GNU_SOURCE | |
#include <assert.h> | |
#include <stdlib.h> | |
#include <stdio.h> | |
#include <unistd.h> | |
#include <string.h> | |
#include <time.h> | |
#include <pthread.h> | |
#include <errno.h> | |
#include <sys/eventfd.h> | |
#include <systemd/sd-event.h> | |
#include "flutter_embedder.h" | |
// The thread executing any platform tasks. | |
pthread_t platform_task_thread; | |
// The mutex for accessing the platform task loop. | |
pthread_mutex_t platform_task_loop_mutex = PTHREAD_MUTEX_INITIALIZER; | |
// Our platform task event loop. | |
sd_event *platform_task_loop; | |
// A linux eventfd to wakeup the sd_event loop. | |
// Needed because sd_event does not work with multithreading. | |
int wakeup_fd; | |
// The flutter engine. Initialized inside [main]. | |
FlutterEngine engine; | |
// ring buffer of the batons that we're requested by the vsync callback. | |
intptr_t vsync_batons[16]; | |
// the index of the first vsync baton inside [vsync_batons]. | |
size_t start_vsync_batons = 0; | |
// the index after the last vsync baton. | |
size_t end_vsync_batons = 0; | |
// Whether we already sent a pointer event with phase == kAdd to flutter. | |
// We need to do that for every multitouch slot before we can emit input events. | |
bool added_touch_device = false; | |
sd_event_source *fake_touch_down; | |
sd_event_source *fake_touch_up; | |
static inline bool has_vsync_batons() { | |
return start_vsync_batons != end_vsync_batons; | |
} | |
static inline void add_vsync_baton(intptr_t baton) { | |
assert(((end_vsync_batons + 1) & 0xF) != start_vsync_batons); | |
vsync_batons[end_vsync_batons] = baton; | |
end_vsync_batons = (end_vsync_batons + 1) & 0xF; | |
} | |
static inline intptr_t peek_vsync_baton() { | |
assert(has_vsync_batons()); | |
return vsync_batons[start_vsync_batons]; | |
} | |
static inline void pop_vsync_baton() { | |
assert(has_vsync_batons()); | |
start_vsync_batons = (start_vsync_batons + 1) & 0xF; | |
} | |
static bool runs_platform_tasks_on_current_thread() { | |
return pthread_equal(platform_task_thread, pthread_self()); | |
} | |
static bool on_runs_platform_tasks_on_current_thread(void *userdata) { | |
return runs_platform_tasks_on_current_thread(); | |
} | |
static int on_wakeup_main_loop(sd_event_source *s, int fd, uint32_t revents, void *userdata) { | |
uint8_t buffer[8]; | |
int ok; | |
ok = read(fd, buffer, 8); | |
assert(ok >= 0); | |
return 0; | |
} | |
static int on_execute_platform_task(sd_event_source *s, uint64_t usec, void *userdata) { | |
FlutterEngineResult engine_result; | |
printf("on_execute_platform_task(task: %"PRIu64")\n", ((FlutterTask *)userdata)->task); | |
engine_result = FlutterEngineRunTask(engine, (FlutterTask *) userdata); | |
assert(engine_result == kSuccess); | |
} | |
static void on_post_platform_task( | |
FlutterTask task, | |
uint64_t target_time_nanos, | |
void* userdata | |
) { | |
FlutterTask *allocated; | |
int ok; | |
printf("on_post_platform_task(task: %"PRIu64", target_time_nanos: %"PRIu64")\n", task.task, target_time_nanos); | |
allocated = malloc(sizeof *allocated); | |
*allocated = task; | |
if (!runs_platform_tasks_on_current_thread()) { | |
pthread_mutex_lock(&platform_task_loop_mutex); | |
} | |
ok = sd_event_add_time( | |
platform_task_loop, | |
NULL, | |
CLOCK_MONOTONIC, | |
target_time_nanos / 1000ull, | |
1, | |
on_execute_platform_task, | |
allocated | |
); | |
assert(ok >= 0); | |
if (!runs_platform_tasks_on_current_thread()) { | |
ok = write(wakeup_fd, (uint8_t[8]) {0, 0, 0, 0, 0, 0, 0, 1}, 8); | |
assert(ok >= 0); | |
pthread_mutex_unlock(&platform_task_loop_mutex); | |
} | |
} | |
static void post_platform_task( | |
int (*cb)(sd_event_source *s, void *userdata), | |
void *userdata | |
) { | |
int ok; | |
if (!runs_platform_tasks_on_current_thread()) { | |
pthread_mutex_lock(&platform_task_loop_mutex); | |
} | |
ok = sd_event_add_defer( | |
platform_task_loop, | |
NULL, | |
cb, | |
userdata | |
); | |
assert(ok >= 0); | |
if (!runs_platform_tasks_on_current_thread()) { | |
ok = write(wakeup_fd, (uint8_t[8]) {0, 0, 0, 0, 0, 0, 0, 1}, 8); | |
assert(ok >= 0); | |
pthread_mutex_unlock(&platform_task_loop_mutex); | |
} | |
} | |
static int on_frame_complete( | |
sd_event_source *s, | |
void *userdata | |
) { | |
printf("on_frame_complete\n"); | |
pop_vsync_baton(); | |
// if a frame was requested after that, | |
// signal it now | |
if (has_vsync_batons()) { | |
uint64_t time = FlutterEngineGetCurrentTime(); | |
printf("FlutterEngineOnVsync(baton: %p)\n", (void*) peek_vsync_baton()); | |
FlutterEngineResult engine_result = FlutterEngineOnVsync( | |
engine, | |
peek_vsync_baton(), | |
time, | |
time + 1000000000ull / 60 | |
); | |
assert(engine_result); | |
} | |
} | |
static bool on_present( | |
void *userdata, | |
const void* allocation, | |
size_t row_bytes, | |
size_t height | |
) { | |
printf("present\n"); | |
post_platform_task( | |
on_frame_complete, | |
NULL | |
); | |
return true; | |
} | |
static int on_execute_request_frame( | |
sd_event_source *s, | |
void *userdata | |
) { | |
FlutterEngineResult engine_result; | |
intptr_t baton; | |
bool reply_immediately; | |
baton = (intptr_t) userdata; | |
printf("on_execute_request_frame(baton: %p)\n", (void*) baton); | |
// if no frame is requested right now, we can immediately reply | |
// to the frame request | |
reply_immediately = !has_vsync_batons(); | |
add_vsync_baton(baton); | |
if (reply_immediately) { | |
uint64_t time = FlutterEngineGetCurrentTime(); | |
printf("FlutterEngineOnVsync(baton: %p)\n", (void*) baton); | |
engine_result = FlutterEngineOnVsync( | |
engine, | |
baton, | |
time, | |
time + 1000000000ull / 60 | |
); | |
assert(engine_result == kSuccess); | |
} | |
} | |
static void on_request_frame( | |
void* userdata, | |
intptr_t baton | |
) { | |
int ok; | |
printf("on_request_frame(baton: %p)\n", (void*) baton); | |
post_platform_task( | |
on_execute_request_frame, | |
(void*) baton | |
); | |
} | |
/** | |
* Simulate touching the "Increase" button of the default flutter create app. | |
* We simulate a touch down event, then 100ms later a touch up event, then 2000ms later a touch down again, etc | |
*/ | |
static int on_do_fake_touch_down(sd_event_source *s, uint64_t usec, void *userdata) { | |
FlutterEngineResult engine_result; | |
int ok; | |
if (!added_touch_device) { | |
engine_result = FlutterEngineSendPointerEvent( | |
engine, | |
&(FlutterPointerEvent) { | |
.struct_size = sizeof(FlutterPointerEvent), | |
.phase = kAdd, | |
.timestamp = usec, | |
.x = 0, | |
.y = 0, | |
.device = 0, | |
.signal_kind = kFlutterPointerSignalKindNone, | |
.scroll_delta_x = 0, | |
.scroll_delta_y = 0, | |
.device_kind = kFlutterPointerDeviceKindTouch, | |
.buttons = 0 | |
}, | |
1 | |
); | |
assert(engine_result == kSuccess); | |
} | |
engine_result = FlutterEngineSendPointerEvent( | |
engine, | |
&(FlutterPointerEvent) { | |
.struct_size = sizeof(FlutterPointerEvent), | |
.phase = kDown, | |
.timestamp = usec, | |
.x = 775, | |
.y = 455, | |
.device = 0, | |
.signal_kind = kFlutterPointerSignalKindNone, | |
.scroll_delta_x = 0, | |
.scroll_delta_y = 0, | |
.device_kind = kFlutterPointerDeviceKindTouch, | |
.buttons = 0 | |
}, | |
1 | |
); | |
assert(engine_result == kSuccess); | |
ok = sd_event_source_set_time( | |
fake_touch_up, | |
usec + 100000 /* touch up 100ms later */ | |
); | |
assert(ok >= 0); | |
ok = sd_event_source_set_enabled( | |
fake_touch_up, | |
SD_EVENT_ONESHOT | |
); | |
assert(ok >= 0); | |
} | |
static int on_do_fake_touch_up(sd_event_source *s, uint64_t usec, void *userdata) { | |
FlutterEngineResult engine_result; | |
int ok; | |
engine_result = FlutterEngineSendPointerEvent( | |
engine, | |
&(FlutterPointerEvent) { | |
.struct_size = sizeof(FlutterPointerEvent), | |
.phase = kUp, | |
.timestamp = usec, | |
.x = 775, | |
.y = 455, | |
.device = 0, | |
.signal_kind = kFlutterPointerSignalKindNone, | |
.scroll_delta_x = 0, | |
.scroll_delta_y = 0, | |
.device_kind = kFlutterPointerDeviceKindTouch, | |
.buttons = 0 | |
}, | |
1 | |
); | |
assert(engine_result == kSuccess); | |
ok = sd_event_source_set_time( | |
fake_touch_down, | |
usec + 2000000 /* touch down 2sec later */ | |
); | |
assert(ok >= 0); | |
ok = sd_event_source_set_enabled( | |
fake_touch_down, | |
SD_EVENT_ONESHOT | |
); | |
assert(ok >= 0); | |
} | |
int main(int argc, char **argv) { | |
FlutterEngineResult engine_result; | |
FlutterProjectArgs project_args; | |
int ok, evloop_fd; | |
// initialize some global sate | |
platform_task_thread = pthread_self(); | |
wakeup_fd = eventfd(0, EFD_CLOEXEC | EFD_NONBLOCK); | |
assert(wakeup_fd >= 0); | |
ok = sd_event_new(&platform_task_loop); | |
assert(ok >= 0); | |
ok = sd_event_add_io( | |
platform_task_loop, | |
NULL, | |
wakeup_fd, | |
EPOLLIN, | |
on_wakeup_main_loop, | |
NULL | |
); | |
assert(ok >= 0); | |
evloop_fd = sd_event_get_fd(platform_task_loop); | |
assert(evloop_fd >= 0); | |
// add our touch faker | |
ok = sd_event_add_time( | |
platform_task_loop, | |
&fake_touch_down, | |
CLOCK_MONOTONIC, | |
FlutterEngineGetCurrentTime()/1000 + 5000000, | |
1000000, | |
on_do_fake_touch_down, | |
NULL | |
); | |
assert(ok >= 0); | |
// this will be setup correctly inside on_do_fake_touch_down, | |
// just add it with some default values here | |
ok = sd_event_add_time( | |
platform_task_loop, | |
&fake_touch_up, | |
CLOCK_MONOTONIC, | |
0, | |
1000000, | |
on_do_fake_touch_up, | |
NULL | |
); | |
assert(ok >= 0); | |
// will be enabled inside on_do_fake_touch_down | |
sd_event_source_set_enabled(fake_touch_up, SD_EVENT_OFF); | |
// initialize project args | |
memset(&project_args, 0, sizeof(project_args)); | |
project_args.struct_size = sizeof(project_args); | |
project_args.assets_path = "/tmp/hello_world"; | |
project_args.icu_data_path = "/usr/lib/icudtl.dat"; | |
project_args.vsync_callback = on_request_frame; | |
project_args.custom_task_runners = &(FlutterCustomTaskRunners) { | |
.struct_size = sizeof(FlutterCustomTaskRunners), | |
.platform_task_runner = &(FlutterTaskRunnerDescription) { | |
.struct_size = sizeof(FlutterTaskRunnerDescription), | |
.post_task_callback = on_post_platform_task, | |
.runs_task_on_current_thread_callback = on_runs_platform_tasks_on_current_thread, | |
.user_data = NULL, | |
.identifier = 0 | |
}, | |
.render_task_runner = NULL | |
}; | |
// start up the engine | |
engine_result = FlutterEngineRun( | |
FLUTTER_ENGINE_VERSION, | |
&(FlutterRendererConfig) { | |
.type = kSoftware, | |
.software = { | |
.struct_size = sizeof(FlutterSoftwareRendererConfig), | |
.surface_present_callback = on_present | |
} | |
}, | |
&project_args, | |
NULL, | |
&engine | |
); | |
assert(engine_result == kSuccess); | |
// send our initial window metrics. | |
// this are the default raspberry pi 7" touchscreen window metrics | |
engine_result = FlutterEngineSendWindowMetricsEvent( | |
engine, | |
&(FlutterWindowMetricsEvent) { | |
.struct_size = sizeof(FlutterWindowMetricsEvent), | |
.width = 800, | |
.height = 480, | |
.pixel_ratio = 1.35, | |
.left = 0, | |
.top = 0, | |
} | |
); | |
assert(engine_result == kSuccess); | |
// sadly sd_event is single threaded, so we need this mess | |
{ | |
fd_set fds; | |
int state; | |
FD_ZERO(&fds); | |
FD_SET(evloop_fd, &fds); | |
const fd_set const_fds = fds; | |
pthread_mutex_lock(&platform_task_loop_mutex); | |
do { | |
state = sd_event_get_state(platform_task_loop); | |
switch (state) { | |
case SD_EVENT_INITIAL: | |
ok = sd_event_prepare(platform_task_loop); | |
assert(ok >= 0); | |
break; | |
case SD_EVENT_ARMED: | |
pthread_mutex_unlock(&platform_task_loop_mutex); | |
do { | |
fds = const_fds; | |
ok = select(evloop_fd + 1, &fds, &fds, &fds, NULL); | |
assert((ok >= 0) || (errno != EINTR)); | |
} while ((ok < 0) && (errno == EINTR)); | |
pthread_mutex_lock(&platform_task_loop_mutex); | |
ok = sd_event_wait(platform_task_loop, 0); | |
assert(ok >= 0); | |
break; | |
case SD_EVENT_PENDING: | |
ok = sd_event_dispatch(platform_task_loop); | |
assert(ok >= 0); | |
break; | |
case SD_EVENT_FINISHED: | |
break; | |
default: | |
abort(); | |
} | |
} while (state != SD_EVENT_FINISHED); | |
pthread_mutex_unlock(&platform_task_loop_mutex); | |
pthread_mutex_destroy(&platform_task_loop_mutex); | |
sd_event_unrefp(&platform_task_loop); | |
} | |
return EXIT_SUCCESS; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment