diff --git a/lib/logic/display.dart b/lib/logic/display.dart index d4fa12dc..7f8e969b 100644 --- a/lib/logic/display.dart +++ b/lib/logic/display.dart @@ -1,4 +1,5 @@ import 'dart:async'; +import 'dart:collection'; import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; @@ -35,10 +36,37 @@ class DisplayManager extends ChangeNotifier { case 'map': toplevel.sync(); break; + case 'commit': + toplevel.notifyListeners(); + break; } break; case 'notifyToplevel': - print(call.arguments); + final server = find(call.arguments['name']); + if (server == null) break; + + final toplevel = server._toplevels.firstWhere((item) => item.id == call.arguments['id']); + + switch (call.arguments['propName']) { + case 'appId': + toplevel.appId = call.arguments['propValue']; + toplevel.notifyListeners(); + break; + case 'title': + toplevel.title = call.arguments['propValue']; + toplevel.notifyListeners(); + break; + case 'texture': + toplevel.texture = call.arguments['propValue']; + toplevel.notifyListeners(); + break; + case 'parent': + toplevel._parent = call.arguments['propValue']; + toplevel.notifyListeners(); + break; + default: + throw MissingPluginException(); + } break; default: throw MissingPluginException(); @@ -86,6 +114,7 @@ class DisplayServer extends ChangeNotifier { final String name; List _toplevels = []; + UnmodifiableListView get toplevels => UnmodifiableListView(_toplevels); Future stop() async { await DisplayManager.channel.invokeMethod('stop', name); @@ -109,6 +138,7 @@ class DisplayServerToplevel extends ChangeNotifier { String? appId; String? title; + int? texture; int? _parent; DisplayServerToplevel? get parent { @@ -124,7 +154,17 @@ class DisplayServerToplevel extends ChangeNotifier { appId = data['appId']; title = data['title']; + texture = data['texture']; _parent = data['parent']; notifyListeners(); } + + Future setSize(int width, int height) async { + await DisplayManager.channel.invokeMethod('setToplevelSize', { + 'name': _server.name, + 'id': id, + 'width': width, + 'height': height, + }); + } } diff --git a/lib/views/desktop.dart b/lib/views/desktop.dart index 732ddae3..93a9de9b 100644 --- a/lib/views/desktop.dart +++ b/lib/views/desktop.dart @@ -11,6 +11,7 @@ import '../logic/wallpaper.dart'; import '../widgets/system_layout.dart'; import '../widgets/system_navbar.dart'; +import '../widgets/toplevel.dart'; class DesktopView extends StatefulWidget { const DesktopView({ @@ -110,8 +111,23 @@ class _DesktopViewState extends State { image: getWallpaper( path: (Breakpoints.small.isActive(context) ? widget.mobileWallpaper : widget.desktopWallpaper) ?? widget.wallpaper, fallback: AssetImage('assets/wallpaper/${Breakpoints.small.isActive(context) ? 'mobile' : 'desktop'}/default.jpg'), - ), + ) ), + child: _displayServer != null + ? ChangeNotifierProvider( + create: (_) => _displayServer!, + child: Consumer( + builder: (context, server, _) => + Stack( + children: server.toplevels.map( + (toplevel) => + Toplevel( + toplevel: toplevel, + ) + ).toList(), + ), + ), + ) : null, ), bottomNavigationBar: Breakpoints.small.isActive(context) ? SystemNavbar() : null, diff --git a/lib/widgets/toplevel.dart b/lib/widgets/toplevel.dart new file mode 100644 index 00000000..ecc7dea7 --- /dev/null +++ b/lib/widgets/toplevel.dart @@ -0,0 +1,48 @@ +import 'package:flutter/scheduler.dart'; +import 'package:libtokyo_flutter/libtokyo.dart' hide ColorScheme; +import 'package:libtokyo/libtokyo.dart' hide TokyoApp; +import 'package:provider/provider.dart'; + +import '../logic/display.dart'; + +class Toplevel extends StatefulWidget { + const Toplevel({ + super.key, + required this.toplevel, + }); + + final DisplayServerToplevel toplevel; + + @override + State createState() => _ToplevelState(); +} + +class _ToplevelState extends State { + GlobalKey key = GlobalKey(); + + @override + void initState() { + super.initState(); + + SchedulerBinding.instance.addPostFrameCallback((_) { + if (key.currentContext != null && widget.toplevel.texture != null) { + final box = key.currentContext!.findRenderObject() as RenderBox; + widget.toplevel.setSize(box.size.width.toInt(), box.size.height.toInt()); + } + }); + } + + @override + Widget build(BuildContext context) => + ChangeNotifierProvider( + key: key, + create: (_) => widget.toplevel, + child: Consumer( + builder: (context, toplevel, _) => + toplevel.texture == null + ? SizedBox() : Texture( + textureId: toplevel.texture!, + ), + ), + ); +} diff --git a/linux/CMakeLists.txt b/linux/CMakeLists.txt index 92c410a0..1f730b61 100644 --- a/linux/CMakeLists.txt +++ b/linux/CMakeLists.txt @@ -121,6 +121,7 @@ add_executable(${BINARY_NAME} "channels/display/backend/dummy.c" "channels/display/backend/wayland.c" "channels/display/backend.c" + "channels/display/pixel-format.c" "channels/display/texture.c" "channels/display/toplevel.c" "channels/display.c" diff --git a/linux/channels/display.c b/linux/channels/display.c index 824e5ed9..22d4d2f2 100644 --- a/linux/channels/display.c +++ b/linux/channels/display.c @@ -200,7 +200,35 @@ static void method_call_handler(FlMethodChannel* channel, FlMethodCall* method_c fl_value_set(value, fl_value_new_string("parent"), fl_value_new_null()); } + if (toplevel->texture != NULL) { + fl_value_set(value, fl_value_new_string("texture"), fl_value_new_int((uintptr_t)FL_TEXTURE(toplevel->texture))); + } else { + fl_value_set(value, fl_value_new_string("texture"), fl_value_new_null()); + } + response = FL_METHOD_RESPONSE(fl_method_success_response_new(value)); + } else if (strcmp(fl_method_call_get_name(method_call), "setToplevelSize") == 0) { + FlValue* args = fl_method_call_get_args(method_call); + const gchar* name = fl_value_get_string(fl_value_lookup_string(args, "name")); + int id = fl_value_get_int(fl_value_lookup_string(args, "id")); + + if (!g_hash_table_contains(self->displays, name)) { + fl_method_call_respond_error(method_call, "Linux", "Display server does not exist", NULL, NULL); + return; + } + + DisplayChannelDisplay* disp = g_hash_table_lookup(self->displays, name); + + if (!g_hash_table_contains(disp->toplevels, &id)) { + fl_method_call_respond_error(method_call, "Linux", "Toplevel does not exist", NULL, NULL); + return; + } + + DisplayChannelToplevel* toplevel = g_hash_table_lookup(disp->toplevels, &id); + g_assert(toplevel->id == id); + + wlr_xdg_toplevel_set_size(toplevel->xdg, fl_value_get_int(fl_value_lookup_string(args, "width")), fl_value_get_int(fl_value_lookup_string(args, "height"))); + response = FL_METHOD_RESPONSE(fl_method_success_response_new(NULL)); } else { response = FL_METHOD_RESPONSE(fl_method_not_implemented_response_new()); } diff --git a/linux/channels/display/pixel-format.c b/linux/channels/display/pixel-format.c new file mode 100644 index 00000000..49d15839 --- /dev/null +++ b/linux/channels/display/pixel-format.c @@ -0,0 +1,279 @@ +#include +#include +#include +#include +#include + +#include "pixel-format.h" + +static const struct wlr_gles2_pixel_format gles2_formats[] = { + { + .drm_format = DRM_FORMAT_ARGB8888, + .gl_format = GL_BGRA_EXT, + .gl_type = GL_UNSIGNED_BYTE, + }, + { + .drm_format = DRM_FORMAT_XRGB8888, + .gl_format = GL_BGRA_EXT, + .gl_type = GL_UNSIGNED_BYTE, + }, + { + .drm_format = DRM_FORMAT_XBGR8888, + .gl_format = GL_RGBA, + .gl_type = GL_UNSIGNED_BYTE, + }, + { + .drm_format = DRM_FORMAT_ABGR8888, + .gl_format = GL_RGBA, + .gl_type = GL_UNSIGNED_BYTE, + }, + { + .drm_format = DRM_FORMAT_BGR888, + .gl_format = GL_RGB, + .gl_type = GL_UNSIGNED_BYTE, + }, +#if WLR_LITTLE_ENDIAN + { + .drm_format = DRM_FORMAT_RGBX4444, + .gl_format = GL_RGBA, + .gl_type = GL_UNSIGNED_SHORT_4_4_4_4, + }, + { + .drm_format = DRM_FORMAT_RGBA4444, + .gl_format = GL_RGBA, + .gl_type = GL_UNSIGNED_SHORT_4_4_4_4, + }, + { + .drm_format = DRM_FORMAT_RGBX5551, + .gl_format = GL_RGBA, + .gl_type = GL_UNSIGNED_SHORT_5_5_5_1, + }, + { + .drm_format = DRM_FORMAT_RGBA5551, + .gl_format = GL_RGBA, + .gl_type = GL_UNSIGNED_SHORT_5_5_5_1, + }, + { + .drm_format = DRM_FORMAT_RGB565, + .gl_format = GL_RGB, + .gl_type = GL_UNSIGNED_SHORT_5_6_5, + }, + { + .drm_format = DRM_FORMAT_XBGR2101010, + .gl_format = GL_RGBA, + .gl_type = GL_UNSIGNED_INT_2_10_10_10_REV_EXT, + }, + { + .drm_format = DRM_FORMAT_ABGR2101010, + .gl_format = GL_RGBA, + .gl_type = GL_UNSIGNED_INT_2_10_10_10_REV_EXT, + }, + { + .drm_format = DRM_FORMAT_XBGR16161616F, + .gl_format = GL_RGBA, + .gl_type = GL_HALF_FLOAT_OES, + }, + { + .drm_format = DRM_FORMAT_ABGR16161616F, + .gl_format = GL_RGBA, + .gl_type = GL_HALF_FLOAT_OES, + }, + { + .drm_format = DRM_FORMAT_XBGR16161616, + .gl_internalformat = GL_RGBA16_EXT, + .gl_format = GL_RGBA, + .gl_type = GL_UNSIGNED_SHORT, + }, + { + .drm_format = DRM_FORMAT_ABGR16161616, + .gl_internalformat = GL_RGBA16_EXT, + .gl_format = GL_RGBA, + .gl_type = GL_UNSIGNED_SHORT, + }, +#endif +}; + +static const struct wlr_pixel_format_info pixel_format_info[] = { + { + .drm_format = DRM_FORMAT_XRGB8888, + .bytes_per_block = 4, + }, + { + .drm_format = DRM_FORMAT_ARGB8888, + .opaque_substitute = DRM_FORMAT_XRGB8888, + .bytes_per_block = 4, + .has_alpha = true, + }, + { + .drm_format = DRM_FORMAT_XBGR8888, + .bytes_per_block = 4, + }, + { + .drm_format = DRM_FORMAT_ABGR8888, + .opaque_substitute = DRM_FORMAT_XBGR8888, + .bytes_per_block = 4, + .has_alpha = true, + }, + { + .drm_format = DRM_FORMAT_RGBX8888, + .bytes_per_block = 4, + }, + { + .drm_format = DRM_FORMAT_RGBA8888, + .opaque_substitute = DRM_FORMAT_RGBX8888, + .bytes_per_block = 4, + .has_alpha = true, + }, + { + .drm_format = DRM_FORMAT_BGRX8888, + .bytes_per_block = 4, + }, + { + .drm_format = DRM_FORMAT_BGRA8888, + .opaque_substitute = DRM_FORMAT_BGRX8888, + .bytes_per_block = 4, + .has_alpha = true, + }, + { + .drm_format = DRM_FORMAT_R8, + .bytes_per_block = 1, + }, + { + .drm_format = DRM_FORMAT_GR88, + .bytes_per_block = 2, + }, + { + .drm_format = DRM_FORMAT_RGB888, + .bytes_per_block = 3, + }, + { + .drm_format = DRM_FORMAT_BGR888, + .bytes_per_block = 3, + }, + { + .drm_format = DRM_FORMAT_RGBX4444, + .bytes_per_block = 2, + }, + { + .drm_format = DRM_FORMAT_RGBA4444, + .opaque_substitute = DRM_FORMAT_RGBX4444, + .bytes_per_block = 2, + .has_alpha = true, + }, + { + .drm_format = DRM_FORMAT_BGRX4444, + .bytes_per_block = 2, + }, + { + .drm_format = DRM_FORMAT_BGRA4444, + .opaque_substitute = DRM_FORMAT_BGRX4444, + .bytes_per_block = 2, + .has_alpha = true, + }, + { + .drm_format = DRM_FORMAT_RGBX5551, + .bytes_per_block = 2, + }, + { + .drm_format = DRM_FORMAT_RGBA5551, + .opaque_substitute = DRM_FORMAT_RGBX5551, + .bytes_per_block = 2, + .has_alpha = true, + }, + { + .drm_format = DRM_FORMAT_BGRX5551, + .bytes_per_block = 2, + }, + { + .drm_format = DRM_FORMAT_BGRA5551, + .opaque_substitute = DRM_FORMAT_BGRX5551, + .bytes_per_block = 2, + .has_alpha = true, + }, + { + .drm_format = DRM_FORMAT_XRGB1555, + .bytes_per_block = 2, + }, + { + .drm_format = DRM_FORMAT_ARGB1555, + .opaque_substitute = DRM_FORMAT_XRGB1555, + .bytes_per_block = 2, + .has_alpha = true, + }, + { + .drm_format = DRM_FORMAT_RGB565, + .bytes_per_block = 2, + }, + { + .drm_format = DRM_FORMAT_BGR565, + .bytes_per_block = 2, + }, + { + .drm_format = DRM_FORMAT_XRGB2101010, + .bytes_per_block = 4, + }, + { + .drm_format = DRM_FORMAT_ARGB2101010, + .opaque_substitute = DRM_FORMAT_XRGB2101010, + .bytes_per_block = 4, + .has_alpha = true, + }, + { + .drm_format = DRM_FORMAT_XBGR2101010, + .bytes_per_block = 4, + }, + { + .drm_format = DRM_FORMAT_ABGR2101010, + .opaque_substitute = DRM_FORMAT_XBGR2101010, + .bytes_per_block = 4, + .has_alpha = true, + }, + { + .drm_format = DRM_FORMAT_XBGR16161616F, + .bytes_per_block = 8, + }, + { + .drm_format = DRM_FORMAT_ABGR16161616F, + .opaque_substitute = DRM_FORMAT_XBGR16161616F, + .bytes_per_block = 8, + .has_alpha = true, + }, + { + .drm_format = DRM_FORMAT_XBGR16161616, + .bytes_per_block = 8, + }, + { + .drm_format = DRM_FORMAT_ABGR16161616, + .opaque_substitute = DRM_FORMAT_XBGR16161616, + .bytes_per_block = 8, + .has_alpha = true, + }, + { + .drm_format = DRM_FORMAT_YVYU, + .bytes_per_block = 4, + .block_width = 2, + .block_height = 1, + }, + { + .drm_format = DRM_FORMAT_VYUY, + .bytes_per_block = 4, + .block_width = 2, + .block_height = 1, + }, +}; + +static const size_t pixel_format_info_size = sizeof(pixel_format_info) / sizeof(pixel_format_info[0]); + +const struct wlr_gles2_pixel_format* get_gles2_format_from_drm(uint32_t fmt) { + for (size_t i = 0; i < sizeof(gles2_formats) / sizeof(*gles2_formats); ++i) { + if (gles2_formats[i].drm_format == fmt) return &gles2_formats[i]; + } + return NULL; +} + +const struct wlr_pixel_format_info* drm_get_pixel_format_info(uint32_t fmt) { + for (size_t i = 0; i < pixel_format_info_size; ++i) { + if (pixel_format_info[i].drm_format == fmt) return &pixel_format_info[i]; + } + return NULL; +} diff --git a/linux/channels/display/pixel-format.h b/linux/channels/display/pixel-format.h new file mode 100644 index 00000000..57f367e7 --- /dev/null +++ b/linux/channels/display/pixel-format.h @@ -0,0 +1,34 @@ +#pragma once + +#include +#include +#include +#include + +struct wlr_gles2_pixel_format { + uint32_t drm_format; + // optional field, if empty then internalformat = format + GLint gl_internalformat; + GLint gl_format, gl_type; + bool has_alpha; +}; + +struct wlr_pixel_format_info { + uint32_t drm_format; + + /* Equivalent of the format if it has an alpha channel, + * DRM_FORMAT_INVALID (0) if NA + */ + uint32_t opaque_substitute; + + /* Bytes per block (including padding) */ + uint32_t bytes_per_block; + /* Size of a block in pixels (zero for 1×1) */ + uint32_t block_width, block_height; + + /* True if the format has an alpha channel */ + bool has_alpha; +}; + +const struct wlr_gles2_pixel_format* get_gles2_format_from_drm(uint32_t fmt); +const struct wlr_pixel_format_info* drm_get_pixel_format_info(uint32_t fmt); diff --git a/linux/channels/display/texture.c b/linux/channels/display/texture.c index be964fae..fd9e2132 100644 --- a/linux/channels/display/texture.c +++ b/linux/channels/display/texture.c @@ -1,4 +1,5 @@ #include +#include "pixel-format.h" #include "texture.h" typedef struct _DisplayChannelTexturePrivate { @@ -85,27 +86,45 @@ static void display_channel_texture_init(DisplayChannelTexture* self) {} DisplayChannelTexture* display_channel_texture_new(GdkGLContext* gl_context, struct wlr_buffer* buffer) { DisplayChannelTexture* self = DISPLAY_CHANNEL_TEXTURE(g_object_new(display_channel_texture_get_type(), "gl-context", gl_context, NULL)); g_return_val_if_fail(self != NULL, NULL); + + DisplayChannelTexturePrivate* priv = display_channel_texture_get_instance_private(self); + gdk_gl_context_make_current(priv->gl_context); + glGenTextures(1, &priv->name); + gdk_gl_context_clear_current(); + display_channel_texture_update(self, buffer); return self; } void display_channel_texture_update(DisplayChannelTexture* self, struct wlr_buffer* buffer) { - struct wlr_dmabuf_attributes dmabuf_attribs; - struct wlr_shm_attributes shm_attribs; - if (wlr_buffer_get_dmabuf(buffer, &dmabuf_attribs)) { - g_message("%p", &dmabuf_attribs); - - // TODO: import using GBM into a texture - } else if (wlr_buffer_get_shm(buffer, &shm_attribs)) { - g_message("%p", &shm_attribs); - - // TODO: read shm into a GL texture - } else { - size_t stride = 0; - uint32_t fmt = 0; - void* fbdata = NULL; - wlr_buffer_begin_data_ptr_access(buffer, WLR_BUFFER_DATA_PTR_ACCESS_READ, &fbdata, &fmt, &stride); - // TODO: render out fbdata - wlr_buffer_end_data_ptr_access(buffer); - } + DisplayChannelTexturePrivate* priv = display_channel_texture_get_instance_private(self); + gdk_gl_context_make_current(priv->gl_context); + + size_t stride = 0; + uint32_t fmt = 0; + void* data = NULL; + wlr_buffer_begin_data_ptr_access(buffer, WLR_BUFFER_DATA_PTR_ACCESS_READ, &data, &fmt, &stride); + + const struct wlr_gles2_pixel_format* gles2_fmt = get_gles2_format_from_drm(fmt); + const struct wlr_pixel_format_info* drm_fmt = drm_get_pixel_format_info(fmt); + + glBindTexture(GL_TEXTURE_2D, priv->name); + + priv->width = buffer->width; + priv->height = buffer->height; + + GLint internal_format = gles2_fmt->gl_internalformat; + if (!internal_format) internal_format = gles2_fmt->gl_format; + + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); + glPixelStorei(GL_UNPACK_ROW_LENGTH_EXT, stride / drm_fmt->bytes_per_block); + glTexImage2D(GL_TEXTURE_2D, 0, internal_format, priv->width, priv->height, 0, gles2_fmt->gl_format, gles2_fmt->gl_type, data); + glPixelStorei(GL_UNPACK_ROW_LENGTH_EXT, 0); + + glBindTexture(GL_TEXTURE_2D, 0); + + wlr_buffer_end_data_ptr_access(buffer); + + gdk_gl_context_clear_current(); } diff --git a/linux/channels/display/toplevel.c b/linux/channels/display/toplevel.c index 119780e1..301c8ebd 100644 --- a/linux/channels/display/toplevel.c +++ b/linux/channels/display/toplevel.c @@ -98,18 +98,24 @@ static void xdg_toplevel_commit(struct wl_listener* listener, void* data) { FlEngine* engine = fl_view_get_engine(app->view); FlTextureRegistrar* tex_reg = fl_engine_get_texture_registrar(engine); - if (self->texture == NULL) { + bool is_new = self->texture == NULL; + + if (is_new) { GError* error = NULL; GdkGLContext* ctx = gdk_window_create_gl_context(win, &error); - gdk_gl_context_make_current(ctx); self->texture = display_channel_texture_new(ctx, buffer); - gdk_gl_context_clear_current(); fl_texture_registrar_register_texture(tex_reg, FL_TEXTURE(self->texture)); } else { display_channel_texture_update(self->texture, buffer); } fl_texture_registrar_mark_texture_frame_available(tex_reg, FL_TEXTURE(self->texture)); + + if (is_new) { + xdg_toplevel_emit_prop(self, "texture", fl_value_new_int((uintptr_t)FL_TEXTURE(self->texture))); + } + + xdg_toplevel_emit_request(self, "commit"); } static void xdg_toplevel_request_maximize(struct wl_listener* listener, void* data) {