diff --git a/README.md b/README.md index d8c79042..9dd3e818 100644 --- a/README.md +++ b/README.md @@ -384,6 +384,11 @@ OPTIONS: If no hz value is given, the highest possible refreshrate will be used. + --dummy-display Simulate a display. Useful for running apps + without a display attached. + --dummy-display-size "width,height" The width & height of the dummy display + in pixels. + -h, --help Show this help and exit. EXAMPLES: diff --git a/src/flutter-pi.c b/src/flutter-pi.c index 82dd20a6..ff3185d4 100644 --- a/src/flutter-pi.c +++ b/src/flutter-pi.c @@ -136,6 +136,11 @@ OPTIONS:\n\ --videomode widthxheight@hz Uses an output videomode that satisfies the argument.\n\ If no hz value is given, the highest possible refreshrate\n\ will be used.\n\ +\n\ + --dummy-display Simulate a display. Useful for running apps\n\ + without a display attached.\n\ + --dummy-display-size \"width,height\" The width & height of the dummy display\n\ + in pixels.\n\ \n\ -h, --help Show this help and exit.\n\ \n\ @@ -1848,7 +1853,7 @@ struct cmd_args { int rotation; bool has_physical_dimensions; - int width_mm, height_mm; + struct vec2i physical_dimensions; bool has_pixel_format; enum pixfmt pixel_format; @@ -1864,6 +1869,9 @@ struct cmd_args { bool use_vulkan; char *desired_videomode; + + bool dummy_display; + struct vec2i dummy_display_size; }; static struct flutter_paths *setup_paths(enum flutter_runtime_mode runtime_mode, const char *app_bundle_path) { @@ -1877,10 +1885,22 @@ static struct flutter_paths *setup_paths(enum flutter_runtime_mode runtime_mode, #endif } +static bool parse_vec2i(const char *str, struct vec2i *out) { + int ok; + + ok = sscanf(str, "%d,%d", &out->x, &out->y); + if (ok != 2) { + return false; + } + + return true; +} + static bool parse_cmd_args(int argc, char **argv, struct cmd_args *result_out) { bool finished_parsing_options; int runtime_mode_int = FLUTTER_RUNTIME_MODE_DEBUG; int vulkan_int = false; + int dummy_display_int = 0; int longopt_index = 0; int opt, ok; @@ -1894,6 +1914,8 @@ static bool parse_cmd_args(int argc, char **argv, struct cmd_args *result_out) { { "pixelformat", required_argument, NULL, 'p' }, { "vulkan", no_argument, &vulkan_int, true }, { "videomode", required_argument, NULL, 'v' }, + { "dummy-display", no_argument, &dummy_display_int, 1 }, + { "dummy-display-size", required_argument, NULL, 's' }, { 0, 0, 0, 0 }, }; @@ -1960,18 +1982,12 @@ static bool parse_cmd_args(int argc, char **argv, struct cmd_args *result_out) { break; case 'd':; - unsigned int width_mm, height_mm; - - ok = sscanf(optarg, "%u,%u", &width_mm, &height_mm); - if (ok != 2) { - LOG_ERROR("ERROR: Invalid argument for --dimensions passed.\n%s", usage); + ok = parse_vec2i(optarg, &result_out->physical_dimensions); + if (!ok) { + LOG_ERROR("ERROR: Invalid argument for --dimensions passed.\n"); return false; } - result_out->width_mm = width_mm; - result_out->height_mm = height_mm; - result_out->has_physical_dimensions = true; - break; case 'p': @@ -2004,6 +2020,15 @@ static bool parse_cmd_args(int argc, char **argv, struct cmd_args *result_out) { result_out->desired_videomode = vmode_dup; break; + case 's':; // --dummy-display-size + ok = parse_vec2i(optarg, &result_out->dummy_display_size); + if (!ok) { + LOG_ERROR("ERROR: Invalid argument for --dummy-display-size passed.\n"); + return false; + } + + break; + case 'h': printf("%s", usage); return false; case '?': @@ -2038,6 +2063,8 @@ static bool parse_cmd_args(int argc, char **argv, struct cmd_args *result_out) { #endif result_out->use_vulkan = vulkan_int; + result_out->dummy_display = !!dummy_display_int; + return true; } @@ -2311,6 +2338,11 @@ struct flutterpi *flutterpi_new_from_args(int argc, char **argv) { desired_videomode = cmd_args.desired_videomode; + if (bundle_path == NULL) { + LOG_ERROR("ERROR: Bundle path does not exist.\n"); + goto fail_free_cmd_args; + } + paths = setup_paths(runtime_mode, bundle_path); if (paths == NULL) { goto fail_free_cmd_args; @@ -2449,29 +2481,44 @@ struct flutterpi *flutterpi_new_from_args(int argc, char **argv) { goto fail_unref_scheduler; } - window = kms_window_new( - // clang-format off - tracer, - scheduler, - renderer_type, - gl_renderer, - vk_renderer, - cmd_args.has_rotation, - cmd_args.rotation == 0 ? PLANE_TRANSFORM_ROTATE_0 : - cmd_args.rotation == 90 ? PLANE_TRANSFORM_ROTATE_90 : - cmd_args.rotation == 180 ? PLANE_TRANSFORM_ROTATE_180 : - cmd_args.rotation == 270 ? PLANE_TRANSFORM_ROTATE_270 : - (assert(0 && "invalid rotation"), PLANE_TRANSFORM_ROTATE_0), - cmd_args.has_orientation, cmd_args.orientation, - cmd_args.has_physical_dimensions, cmd_args.width_mm, cmd_args.height_mm, - cmd_args.has_pixel_format, cmd_args.pixel_format, - drmdev, - desired_videomode - // clang-format on - ); - if (window == NULL) { - LOG_ERROR("Couldn't create KMS window.\n"); - goto fail_unref_renderer; + if (cmd_args.dummy_display) { + window = dummy_window_new( + tracer, + scheduler, + renderer_type, + gl_renderer, + vk_renderer, + cmd_args.dummy_display_size, + cmd_args.has_physical_dimensions, + cmd_args.physical_dimensions.x, + cmd_args.physical_dimensions.y, + 60.0 + ); + } else { + window = kms_window_new( + // clang-format off + tracer, + scheduler, + renderer_type, + gl_renderer, + vk_renderer, + cmd_args.has_rotation, + cmd_args.rotation == 0 ? PLANE_TRANSFORM_ROTATE_0 : + cmd_args.rotation == 90 ? PLANE_TRANSFORM_ROTATE_90 : + cmd_args.rotation == 180 ? PLANE_TRANSFORM_ROTATE_180 : + cmd_args.rotation == 270 ? PLANE_TRANSFORM_ROTATE_270 : + (assert(0 && "invalid rotation"), PLANE_TRANSFORM_ROTATE_0), + cmd_args.has_orientation, cmd_args.orientation, + cmd_args.has_physical_dimensions, cmd_args.physical_dimensions.x, cmd_args.physical_dimensions.y, + cmd_args.has_pixel_format, cmd_args.pixel_format, + drmdev, + desired_videomode + // clang-format on + ); + if (window == NULL) { + LOG_ERROR("Couldn't create KMS window.\n"); + goto fail_unref_renderer; + } } compositor = compositor_new(tracer, window); diff --git a/src/util/geometry.h b/src/util/geometry.h index 11481244..d983d9a8 100644 --- a/src/util/geometry.h +++ b/src/util/geometry.h @@ -45,6 +45,10 @@ struct vec2i { #define VEC2I(_x, _y) ((struct vec2i){ .x = (_x), .y = (_y) }) +ATTR_CONST static inline struct vec2i vec2f_round_to_integer(struct vec2f a) { + return VEC2I((int) round(a.x), (int) round(a.y)); +} + ATTR_CONST static inline struct vec2i vec2i_add(struct vec2i a, struct vec2i b) { return VEC2I(a.x + b.x, a.y + b.y); } @@ -130,10 +134,8 @@ ATTR_CONST static inline struct quad get_quad(const struct aa_rect rect) { ATTR_CONST static inline bool quad_is_axis_aligned(const struct quad quad) { struct aa_rect aa = quad_get_aa_bounding_rect(quad); - return vec2f_equals(quad.top_left, aa_rect_top_left(aa)) && - vec2f_equals(quad.top_right, aa_rect_top_right(aa)) && - vec2f_equals(quad.bottom_left, aa_rect_bottom_left(aa)) && - vec2f_equals(quad.bottom_right, aa_rect_bottom_right(aa)); + return vec2f_equals(quad.top_left, aa_rect_top_left(aa)) && vec2f_equals(quad.top_right, aa_rect_top_right(aa)) && + vec2f_equals(quad.bottom_left, aa_rect_bottom_left(aa)) && vec2f_equals(quad.bottom_right, aa_rect_bottom_right(aa)); } struct mat3f { diff --git a/src/window.c b/src/window.c index e23addb9..4d81ff57 100644 --- a/src/window.c +++ b/src/window.c @@ -231,8 +231,15 @@ struct window { EGLSurface (*get_egl_surface)(struct window *window); #endif - int (*set_cursor_locked - )(struct window *window, bool has_enabled, bool enabled, bool has_kind, enum pointer_kind kind, bool has_pos, struct vec2i pos); + int (*set_cursor_locked)( + struct window *window, + bool has_enabled, + bool enabled, + bool has_kind, + enum pointer_kind kind, + bool has_pos, + struct vec2i pos + ); void (*deinit)(struct window *window); }; @@ -473,7 +480,7 @@ int window_get_next_vblank(struct window *window, uint64_t *next_vblank_ns_out) #ifdef HAVE_EGL_GLES2 bool window_has_egl_surface(struct window *window) { - return window->egl_surface; + return window->has_egl_surface(window); } EGLSurface window_get_egl_surface(struct window *window) { @@ -1574,3 +1581,224 @@ static int kms_window_set_cursor_locked( window->cursor_enabled = enabled; return 0; } + +static int dummy_window_push_composition(struct window *window, struct fl_layer_composition *composition); +static struct render_surface *dummy_window_get_render_surface_internal(struct window *window, bool has_size, UNUSED struct vec2i size); +static struct render_surface *dummy_window_get_render_surface(struct window *window, struct vec2i size); + +#ifdef HAVE_EGL_GLES2 +static bool dummy_window_has_egl_surface(struct window *window); +static EGLSurface dummy_window_get_egl_surface(struct window *window); +#endif + +static void dummy_window_deinit(struct window *window); +static int dummy_window_set_cursor_locked( + // clang-format off + struct window *window, + bool has_enabled, bool enabled, + bool has_kind, enum pointer_kind kind, + bool has_pos, struct vec2i pos + // clang-format on +); + +MUST_CHECK struct window *dummy_window_new( + // clang-format off + struct tracer *tracer, + struct frame_scheduler *scheduler, + enum renderer_type renderer_type, + struct gl_renderer *gl_renderer, + struct vk_renderer *vk_renderer, + struct vec2i size, + bool has_explicit_dimensions, int width_mm, int height_mm, + double refresh_rate + // clang-format on +) { + struct window *window; + + window = malloc(sizeof *window); + if (window == NULL) { + return NULL; + } + + window_init( + // clang-format off + window, + tracer, + scheduler, + false, PLANE_TRANSFORM_NONE, + false, kLandscapeLeft, + size.x, size.y, + has_explicit_dimensions, width_mm, height_mm, + refresh_rate, + false, PIXFMT_RGB565 + // clang-format on + ); + + window->renderer_type = renderer_type; + if (gl_renderer != NULL) { +#ifdef HAVE_EGL_GLES2 + window->gl_renderer = gl_renderer_ref(gl_renderer); +#else + UNREACHABLE(); +#endif + } + if (vk_renderer != NULL) { +#ifdef HAVE_VULKAN + window->vk_renderer = vk_renderer_ref(vk_renderer); +#else + UNREACHABLE(); +#endif + } else { + window->vk_renderer = NULL; + } + window->push_composition = dummy_window_push_composition; + window->get_render_surface = dummy_window_get_render_surface; +#ifdef HAVE_EGL_GLES2 + window->has_egl_surface = dummy_window_has_egl_surface; + window->get_egl_surface = dummy_window_get_egl_surface; +#endif + window->deinit = dummy_window_deinit; + window->set_cursor_locked = dummy_window_set_cursor_locked; + return window; +} + +static int dummy_window_push_composition(struct window *window, struct fl_layer_composition *composition) { + window_lock(window); + + /// TODO: Maybe allow to export the layer composition as an image, for testing purposes. + (void) composition; + + window_unlock(window); + + return 0; +} + +static struct render_surface *dummy_window_get_render_surface_internal(struct window *window, bool has_size, UNUSED struct vec2i size) { + struct render_surface *render_surface; + + ASSERT_NOT_NULL(window); + + if (!has_size) { + size = vec2f_round_to_integer(window->view_size); + } + + if (window->render_surface != NULL) { + return window->render_surface; + } + + if (window->renderer_type == kOpenGL_RendererType) { + // opengl +#ifdef HAVE_EGL_GLES2 + // EGL_NO_CONFIG_KHR is defined by EGL_KHR_no_config_context. + #ifndef EGL_KHR_no_config_context + #error "EGL header definitions for extension EGL_KHR_no_config_context are required." + #endif + + struct egl_gbm_render_surface *egl_surface = egl_gbm_render_surface_new_with_egl_config( + window->tracer, + size, + gl_renderer_get_gbm_device(window->gl_renderer), + window->gl_renderer, + window->has_forced_pixel_format ? window->forced_pixel_format : PIXFMT_ARGB8888, + EGL_NO_CONFIG_KHR, + NULL, + 0 + ); + if (egl_surface == NULL) { + LOG_ERROR("Couldn't create EGL GBM rendering surface.\n"); + render_surface = NULL; + } else { + render_surface = CAST_RENDER_SURFACE(egl_surface); + } + +#else + UNREACHABLE(); +#endif + } else { + ASSUME(window->renderer_type == kVulkan_RendererType); + + // vulkan +#ifdef HAVE_VULKAN + UNIMPLEMENTED(); +#else + UNREACHABLE(); +#endif + } + + window->render_surface = render_surface; + return render_surface; +} + +static struct render_surface *dummy_window_get_render_surface(struct window *window, struct vec2i size) { + ASSERT_NOT_NULL(window); + return dummy_window_get_render_surface_internal(window, true, size); +} + +#ifdef HAVE_EGL_GLES2 +static bool dummy_window_has_egl_surface(struct window *window) { + ASSERT_NOT_NULL(window); + + if (window->renderer_type == kOpenGL_RendererType) { + return window->render_surface != NULL; + } else { + return false; + } +} + +static EGLSurface dummy_window_get_egl_surface(struct window *window) { + ASSERT_NOT_NULL(window); + + if (window->renderer_type == kOpenGL_RendererType) { + struct render_surface *render_surface = dummy_window_get_render_surface_internal(window, false, VEC2I(0, 0)); + return egl_gbm_render_surface_get_egl_surface(CAST_EGL_GBM_RENDER_SURFACE(render_surface)); + } else { + return EGL_NO_SURFACE; + } +} +#endif + +static void dummy_window_deinit(struct window *window) { + ASSERT_NOT_NULL(window); + + if (window->render_surface != NULL) { + surface_unref(CAST_SURFACE(window->render_surface)); + } + + if (window->gl_renderer != NULL) { +#ifdef HAVE_EGL_GLES2 + gl_renderer_unref(window->gl_renderer); +#else + UNREACHABLE(); +#endif + } + + if (window->vk_renderer != NULL) { +#ifdef HAVE_VULKAN + vk_renderer_unref(window->vk_renderer); +#else + UNREACHABLE(); +#endif + } + + window_deinit(window); +} + +static int dummy_window_set_cursor_locked( + // clang-format off + struct window *window, + bool has_enabled, bool enabled, + bool has_kind, enum pointer_kind kind, + bool has_pos, struct vec2i pos + // clang-format on +) { + ASSERT_NOT_NULL(window); + + (void) has_enabled; + (void) enabled; + (void) has_kind; + (void) kind; + (void) has_pos; + (void) pos; + + return 0; +} diff --git a/src/window.h b/src/window.h index ed7616f4..2efd0196 100644 --- a/src/window.h +++ b/src/window.h @@ -69,6 +69,34 @@ struct window *kms_window_new( // clang-format on ); +/** + * Creates a new dummy window. + * + * @param tracer The tracer object. + * @param scheduler The frame scheduler object. + * @param renderer_type The type of renderer. + * @param gl_renderer The GL renderer object. + * @param vk_renderer The Vulkan renderer object. + * @param size The size of the window. + * @param has_explicit_dimensions Indicates if the window has explicit dimensions. + * @param width_mm The width of the window in millimeters. + * @param height_mm The height of the window in millimeters. + * @param refresh_rate The refresh rate of the window. + * @return A pointer to the newly created window. + */ +MUST_CHECK struct window *dummy_window_new( + struct tracer *tracer, + struct frame_scheduler *scheduler, + enum renderer_type renderer_type, + struct gl_renderer *gl_renderer, + struct vk_renderer *vk_renderer, + struct vec2i size, + bool has_explicit_dimensions, + int width_mm, + int height_mm, + double refresh_rate +); + /** * @brief Push a new flutter composition to the window, outputting a new frame. *