Skip to content

Instantly share code, notes, and snippets.

@ardera
Last active June 21, 2023 08:52
Show Gist options
  • Save ardera/60af806830cc80f4a61451b6c333cb50 to your computer and use it in GitHub Desktop.
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/`.
#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