From 8c43f8aa333b09304babd976f7616b46cf02216c Mon Sep 17 00:00:00 2001 From: Norman Feske Date: Wed, 23 Oct 2024 19:37:28 +0200 Subject: [PATCH] sculpt_manager: interactive framebuffer settings This patch add a configuration dialog in the intel_fb node of the component graph. The dialog displays a list of present displays labeled after their respecive connectors. A mode can be selected for each connector when clicking on the connector entry. In-between the entries there are two buttons. The connect button is toggle that defines whether the two adjacent entries are mirrored. It is enabled by default so that all new connectors participate in the mirroring. By untoggling the last enabled connect button, the entry below the button becomes a discrete (non-mirrored) display. A swap button allows for changing the order of the list, which has to effects. First, the resolution of the very first entry defines the size for mirrored display. So be changing the order of mirrored displays, one can pick the preferred screen size. Second, the order of discrete displays defines the layout of the panorama from left to right. (the panorama config is not part of this commit though) Note that there is currently no safety net against locking oneself out of all displays. E.g., one can make Sculpt unusable by manually disabling each display, or by selecting modes not properly handled by the connected monitor. In the future, we may add a confirm button with a timeout to roll back such unfortunate settings. Fixes #5286 --- repos/gems/src/app/phone_manager/main.cc | 28 +- repos/gems/src/app/sculpt_manager/driver/fb.h | 41 ++ repos/gems/src/app/sculpt_manager/drivers.cc | 12 +- repos/gems/src/app/sculpt_manager/drivers.h | 11 +- repos/gems/src/app/sculpt_manager/graph.cc | 4 + repos/gems/src/app/sculpt_manager/graph.h | 14 +- repos/gems/src/app/sculpt_manager/main.cc | 108 ++++- .../src/app/sculpt_manager/model/fb_config.h | 401 ++++++++++++++++++ .../app/sculpt_manager/model/fb_connectors.h | 278 ++++++++++++ .../src/app/sculpt_manager/view/fb_widget.h | 187 ++++++++ 10 files changed, 1056 insertions(+), 28 deletions(-) create mode 100644 repos/gems/src/app/sculpt_manager/model/fb_config.h create mode 100644 repos/gems/src/app/sculpt_manager/model/fb_connectors.h create mode 100644 repos/gems/src/app/sculpt_manager/view/fb_widget.h diff --git a/repos/gems/src/app/phone_manager/main.cc b/repos/gems/src/app/phone_manager/main.cc index a70cc48ea7..8d74ab0351 100644 --- a/repos/gems/src/app/phone_manager/main.cc +++ b/repos/gems/src/app/phone_manager/main.cc @@ -811,8 +811,8 @@ struct Sculpt::Main : Input_event_handler, Conditional_widget _graph { Id { "graph" }, _runtime_state, _cached_runtime_config, _storage._storage_devices, - _storage._selected_target, _storage._ram_fs_state, - _popup.state, _deploy._children }; + _storage._selected_target, _storage._ram_fs_state, _fb_connectors, + _fb_config, _popup.state, _deploy._children }; Conditional_widget _network_widget { Conditional_widget::Attr { .centered = true }, @@ -1994,6 +1994,30 @@ struct Sculpt::Main : Input_event_handler, } + /********************************** + ** Display driver configuration ** + **********************************/ + + Fb_connectors _fb_connectors { }; + + Fb_config _fb_config { }; + + /** + * Fb_driver::Action interface + */ + void fb_connectors_changed() override { } + + /** + * Fb_widget::Action interface + */ + void select_fb_mode (Fb_connectors::Name const &, + Fb_connectors::Connector::Mode::Id const &) override { } + void disable_fb_connector (Fb_connectors::Name const &) override { } + void toggle_fb_merge_discrete(Fb_connectors::Name const &) override { } + void swap_fb_connector (Fb_connectors::Name const &) override { } + void fb_brightness (Fb_connectors::Name const &, unsigned) override { } + + /******************* ** Runtime graph ** *******************/ diff --git a/repos/gems/src/app/sculpt_manager/driver/fb.h b/repos/gems/src/app/sculpt_manager/driver/fb.h index 6a4ab96542..94c60fe394 100644 --- a/repos/gems/src/app/sculpt_manager/driver/fb.h +++ b/repos/gems/src/app/sculpt_manager/driver/fb.h @@ -14,17 +14,44 @@ #ifndef _DRIVER__FB_H_ #define _DRIVER__FB_H_ +#include + namespace Sculpt { struct Fb_driver; } struct Sculpt::Fb_driver : private Noncopyable { + using Fb_name = String<16>; + + struct Action : Interface + { + virtual void fb_connectors_changed() = 0; + }; + + Env &_env; + Action &_action; + Constructible _intel_gpu { }, _intel_fb { }, _vesa_fb { }, _boot_fb { }, _soc_fb { }; + Constructible> _connectors { }; + + void _handle_connectors(Xml_node const &) { _action.fb_connectors_changed(); } + + Fb_name _fb_name() const + { + return _intel_fb.constructed() ? "intel_fb" + : _vesa_fb .constructed() ? "vesa_fb" + : _boot_fb .constructed() ? "boot_fb" + : _soc_fb .constructed() ? "fb" + : ""; + } + + Fb_driver(Env &env, Action &action) : _env(env), _action(action) { } + void gen_start_nodes(Xml_generator &xml) const { auto gen_capture_route = [&] (Xml_generator &xml) @@ -123,6 +150,8 @@ struct Sculpt::Fb_driver : private Noncopyable bool const use_vesa = !use_intel_fb && !suspending && board_info.detected.vga && !use_boot_fb; + Fb_name const orig_fb_name = _fb_name(); + _intel_gpu.conditional(use_intel_gpu, registry, "intel_gpu", Priority::MULTIMEDIA, Ram_quota { 32*1024*1024 }, Cap_quota { 1400 }); @@ -148,6 +177,12 @@ struct Sculpt::Fb_driver : private Noncopyable Boot_fb::with_mode(platform, [&] (Boot_fb::Mode mode) { _boot_fb.construct(registry, "boot_fb", Priority::MULTIMEDIA, mode.ram_quota(), Cap_quota { 100 }); }); + + if (orig_fb_name != _fb_name()) { + Session_label label { "report -> runtime/", _fb_name(), "/connectors" }; + _connectors.conditional((label.length() > 1), _env, label, + *this, &Fb_driver::_handle_connectors); + } } static bool suspend_supported(Board_info const &board_info) @@ -156,6 +191,12 @@ struct Sculpt::Fb_driver : private Noncopyable return board_info.detected.intel_gfx && !board_info.options.suppress.intel_gpu; } + + void with_connectors(auto const &fn) const + { + if (_connectors.constructed()) + _connectors->with_xml(fn); + } }; #endif /* _DRIVER__FB_H_ */ diff --git a/repos/gems/src/app/sculpt_manager/drivers.cc b/repos/gems/src/app/sculpt_manager/drivers.cc index 58cd1e3b98..352cf777c9 100644 --- a/repos/gems/src/app/sculpt_manager/drivers.cc +++ b/repos/gems/src/app/sculpt_manager/drivers.cc @@ -100,7 +100,7 @@ class Sculpt::Drivers::Instance : Noncopyable, Ps2_driver _ps2_driver { }; Touch_driver _touch_driver { }; - Fb_driver _fb_driver { }; + Fb_driver _fb_driver { _env, _action }; Usb_driver _usb_driver { _env, *this, *this }; Ahci_driver _ahci_driver { _env, *this }; Nvme_driver _nvme_driver { _env, *this }; @@ -168,7 +168,8 @@ class Sculpt::Drivers::Instance : Noncopyable, } void with(With_board_info::Callback const &fn) const { fn(_board_info); } - void with(With_platform_info::Callback const &fn) const { fn(_platform.xml()); } + void with_platform_info(With_xml::Callback const &fn) const { fn(_platform.xml()); } + void with_fb_connectors(With_xml::Callback const &fn) const { _fb_driver.with_connectors(fn); } bool suspend_supported() const { @@ -202,9 +203,10 @@ Sculpt::Drivers::Drivers(Env &env, Children &children, Info const &info, Action _instance(_construct_instance(env, children, info, action)) { } -void Drivers::_with(With_storage_devices::Callback const &fn) const { _instance.with(fn); } -void Drivers::_with(With_board_info::Callback const &fn) const { _instance.with(fn); } -void Drivers::_with(With_platform_info::Callback const &fn) const { _instance.with(fn); } +void Drivers::_with(With_storage_devices::Callback const &fn) const { _instance.with(fn); } +void Drivers::_with(With_board_info::Callback const &fn) const { _instance.with(fn); } +void Drivers::_with_platform_info(With_xml::Callback const &fn) const { _instance.with_platform_info(fn); } +void Drivers::_with_fb_connectors(With_xml::Callback const &fn) const { _instance.with_fb_connectors(fn); } void Drivers::update_usb () { _instance.update_usb(); } void Drivers::update_soc (Board_info::Soc soc) { _instance.update_soc(soc); } diff --git a/repos/gems/src/app/sculpt_manager/drivers.h b/repos/gems/src/app/sculpt_manager/drivers.h index 09bea3fa25..17b2853a42 100644 --- a/repos/gems/src/app/sculpt_manager/drivers.h +++ b/repos/gems/src/app/sculpt_manager/drivers.h @@ -18,6 +18,7 @@ #include #include #include +#include namespace Sculpt { struct Drivers; } @@ -26,7 +27,7 @@ class Sculpt::Drivers : Noncopyable { public: - struct Action : Interface + struct Action : virtual Fb_driver::Action { virtual void handle_device_plug_unplug() = 0; }; @@ -53,11 +54,12 @@ class Sculpt::Drivers : Noncopyable using With_storage_devices = With; using With_board_info = With; - using With_platform_info = With; + using With_xml = With; void _with(With_storage_devices::Callback const &) const; void _with(With_board_info::Callback const &) const; - void _with(With_platform_info::Callback const &) const; + void _with_platform_info(With_xml::Callback const &) const; + void _with_fb_connectors(With_xml::Callback const &) const; public: @@ -71,7 +73,8 @@ class Sculpt::Drivers : Noncopyable void with_storage_devices(auto const &fn) const { _with(With_storage_devices::Fn { fn }); } void with_board_info (auto const &fn) const { _with(With_board_info::Fn { fn }); } - void with_platform_info (auto const &fn) const { _with(With_platform_info::Fn { fn }); } + void with_platform_info (auto const &fn) const { _with_platform_info(With_xml::Fn { fn }); } + void with_fb_connectors (auto const &fn) const { _with_fb_connectors(With_xml::Fn { fn }); } /* true if hardware is suspend/resume capable */ bool suspend_supported() const; diff --git a/repos/gems/src/app/sculpt_manager/graph.cc b/repos/gems/src/app/sculpt_manager/graph.cc index d90f52a24a..c9c86fa674 100644 --- a/repos/gems/src/app/sculpt_manager/graph.cc +++ b/repos/gems/src/app/sculpt_manager/graph.cc @@ -100,6 +100,9 @@ void Graph::_view_selected_node_content(Scope &s, if (name == "ram_fs") s.widget(_ram_fs_widget, _selected_target, _ram_fs_state); + if (name == "intel_fb") + s.widget(_fb_widget, _fb_connectors, _fb_config); + String<100> const ram (Capacity{info.assigned_ram - info.avail_ram}, " / ", Capacity{info.assigned_ram}), @@ -257,6 +260,7 @@ void Graph::click(Clicked_at const &at, Action &action) }); _ram_fs_widget .propagate(at, _selected_target, action); + _fb_widget .propagate(at, _fb_connectors, action); _ahci_devices_widget.propagate(at, action); _nvme_devices_widget.propagate(at, action); _mmc_devices_widget .propagate(at, action); diff --git a/repos/gems/src/app/sculpt_manager/graph.h b/repos/gems/src/app/sculpt_manager/graph.h index d9a850e84a..5bd5c6cb33 100644 --- a/repos/gems/src/app/sculpt_manager/graph.h +++ b/repos/gems/src/app/sculpt_manager/graph.h @@ -25,6 +25,7 @@ #include #include #include +#include #include #include #include @@ -42,6 +43,8 @@ struct Sculpt::Graph : Widget Storage_devices const &_storage_devices; Storage_target const &_selected_target; Ram_fs_state const &_ram_fs_state; + Fb_connectors const &_fb_connectors; + Fb_config const &_fb_config; Popup::State const &_popup_state; Depot_deploy::Children const &_deploy_children; @@ -50,6 +53,9 @@ struct Sculpt::Graph : Widget Hosted _ram_fs_widget { Id { "ram_fs" } }; + Hosted + _fb_widget { Id { "fb" } }; + Hosted _remove { Id { "Remove" } }, _restart { Id { "Restart" } }; @@ -81,18 +87,22 @@ struct Sculpt::Graph : Widget Storage_devices const &storage_devices, Storage_target const &selected_target, Ram_fs_state const &ram_fs_state, + Fb_connectors const &fb_connectors, + Fb_config const &fb_config, Popup::State const &popup_state, Depot_deploy::Children const &deploy_children) : _runtime_state(runtime_state), _runtime_config(runtime_config), _storage_devices(storage_devices), _selected_target(selected_target), - _ram_fs_state(ram_fs_state), _popup_state(popup_state), + _ram_fs_state(ram_fs_state), _fb_connectors(fb_connectors), + _fb_config(fb_config), _popup_state(popup_state), _deploy_children(deploy_children) { } void view(Scope &) const; - struct Action : virtual Storage_device_widget::Action + struct Action : virtual Storage_device_widget::Action, + virtual Fb_widget::Action { virtual void remove_deployed_component(Start_name const &) = 0; virtual void restart_deployed_component(Start_name const &) = 0; diff --git a/repos/gems/src/app/sculpt_manager/main.cc b/repos/gems/src/app/sculpt_manager/main.cc index f32d48b67a..35d453d990 100644 --- a/repos/gems/src/app/sculpt_manager/main.cc +++ b/repos/gems/src/app/sculpt_manager/main.cc @@ -40,6 +40,7 @@ #include #include #include +#include #include #include #include @@ -1561,18 +1562,6 @@ struct Sculpt::Main : Input_event_handler, _cached_runtime_config, _file_browser_state, *this }; - Managed_config
_fb_config { - _env, "config", "fb", *this, &Main::_handle_fb_config }; - - void _handle_fb_config(Xml_node const &node) - { - _fb_config.generate([&] (Xml_generator &xml) { - xml.attribute("system", "yes"); - copy_attributes(xml, node); - node.for_each_sub_node([&] (Xml_node const &sub_node) { - copy_node(xml, sub_node, { 5 }); }); }); - } - void _update_window_layout(Xml_node const &, Xml_node const &); void _update_window_layout() @@ -1616,6 +1605,96 @@ struct Sculpt::Main : Input_event_handler, Signal_handler
_wheel_handler { _env.ep(), *this, &Main::_update_window_layout }; + /********************************** + ** Display driver configuration ** + **********************************/ + + Fb_connectors _fb_connectors { }; + + Rom_handler
_manual_fb_handler { _env, "config -> fb", *this, &Main::_handle_manual_fb }; + + Expanding_reporter _managed_fb_reporter { _env, "config", "fb_config"}; + + Fb_config _fb_config { }; + + void _generate_fb_config() + { + _managed_fb_reporter.generate([&] (Xml_generator &xml) { + _fb_config.generate_managed_fb(xml); }); + } + + void _handle_manual_fb(Xml_node const &node) + { + log("_handle_manual_fb: ", node); + + _fb_config = { }; + _fb_config.import_manual_config(node); + _fb_config.apply_connectors(_fb_connectors); + + _generate_fb_config(); + } + + /** + * Fb_driver::Action interface + */ + void fb_connectors_changed() override + { + _drivers.with_fb_connectors([&] (Xml_node const &node) { + if (_fb_connectors.update(_heap, node).progress) { + _fb_config.apply_connectors(_fb_connectors); + _generate_fb_config(); + } + }); + _graph_view.refresh(); + } + + /** + * Fb_widget::Action interface + */ + void select_fb_mode(Fb_connectors::Name const &conn, + Fb_connectors::Connector::Mode::Id const &mode) override + { + _fb_config.select_fb_mode(conn, mode, _fb_connectors); + _generate_fb_config(); + } + + /** + * Fb_widget::Action interface + */ + void disable_fb_connector(Fb_connectors::Name const &conn) override + { + _fb_config.disable_connector(conn); + _generate_fb_config(); + } + + /** + * Fb_widget::Action interface + */ + void toggle_fb_merge_discrete(Fb_connectors::Name const &conn) override + { + _fb_config.toggle_merge_discrete(conn); + _generate_fb_config(); + } + + /** + * Fb_widget::Action interface + */ + void swap_fb_connector(Fb_connectors::Name const &conn) override + { + _fb_config.swap_connector(conn); + _generate_fb_config(); + } + + /** + * Fb_widget::Action interface + */ + void fb_brightness(Fb_connectors::Name const &conn, unsigned percent) override + { + _fb_config.brightness(conn, percent); + _generate_fb_config(); + } + + /******************* ** Runtime graph ** *******************/ @@ -1623,8 +1702,8 @@ struct Sculpt::Main : Input_event_handler, Popup _popup { }; Graph _graph { _runtime_state, _cached_runtime_config, _storage._storage_devices, - _storage._selected_target, _storage._ram_fs_state, - _popup.state, _deploy._children }; + _storage._selected_target, _storage._ram_fs_state, _fb_connectors, + _fb_config, _popup.state, _deploy._children }; struct Graph_dialog : Dialog::Top_level_dialog { @@ -1655,7 +1734,6 @@ struct Sculpt::Main : Input_event_handler, _gui.input.sigh(_input_handler); _gui.info_sigh(_gui_mode_handler); _handle_gui_mode(); - _fb_config.trigger_update(); /* * Generate initial configurations diff --git a/repos/gems/src/app/sculpt_manager/model/fb_config.h b/repos/gems/src/app/sculpt_manager/model/fb_config.h new file mode 100644 index 0000000000..ce8f249b95 --- /dev/null +++ b/repos/gems/src/app/sculpt_manager/model/fb_config.h @@ -0,0 +1,401 @@ +/* + * \brief Model for the framebuffer driver configuration + * \author Norman Feske + * \date 2024-10-23 + */ + +/* + * Copyright (C) 2024 Genode Labs GmbH + * + * This file is part of the Genode OS framework, which is distributed + * under the terms of the GNU Affero General Public License version 3. + */ + +#ifndef _MODEL__FB_CONFIG_H_ +#define _MODEL__FB_CONFIG_H_ + +#include + +namespace Sculpt { struct Fb_config; }; + + +struct Sculpt::Fb_config +{ + struct Entry + { + using Name = Fb_connectors::Name; + using Mode_id = Fb_connectors::Connector::Mode::Id; + using Mode_attr = Fb_connectors::Connector::Mode::Attr; + using Brightness = Fb_connectors::Brightness; + + bool defined; + bool present; /* false if imported from config but yet unused */ + Name name; + Mode_id mode_id; + Mode_attr mode_attr; + Brightness brightness; + + static Entry from_connector(Fb_connectors::Connector const &connector) + { + Mode_attr mode_attr { }; + Mode_id mode_id { }; + connector.with_used_mode([&] (Fb_connectors::Connector::Mode const &mode) { + mode_attr = mode.attr; + mode_id = mode.id; }); + + return { .defined = true, + .present = true, + .name = connector.name, + .mode_id = mode_id, + .mode_attr = mode_attr, + .brightness = connector.brightness }; + } + + static Entry from_manual_xml(Xml_node const &node) + { + return { .defined = true, + .present = false, + .name = node.attribute_value("name", Name()), + .mode_id = node.attribute_value("mode", Mode_id()), + .mode_attr = Mode_attr::from_xml(node), + .brightness = Brightness::from_xml(node) }; + } + + void generate(Xml_generator &xml) const + { + if (!defined) + return; + + xml.node("connector", [&] { + xml.attribute("name", name); + if (mode_attr.px.valid()) { + xml.attribute("width", mode_attr.px.w); + xml.attribute("height", mode_attr.px.h); + if (mode_attr.hz) + xml.attribute("hz", mode_attr.hz); + if (brightness.defined) + xml.attribute("brightness", brightness.percent); + if (mode_id.length() > 1) + xml.attribute("mode", mode_id); + } else { + xml.attribute("enabled", "no"); + } + }); + } + + bool smaller_than(Entry const &other) const + { + return mode_attr.px.count() < other.mode_attr.px.count(); + } + }; + + static constexpr unsigned MAX_ENTRIES = 16; + Entry _entries[MAX_ENTRIES]; + + struct Manual_attr + { + Area max_px; /* upper bound of framebuffer allocation */ + Area px; /* for vesa_fb */ + + static Manual_attr from_xml(Xml_node const &node) + { + return { .max_px = { .w = node.attribute_value("max_width", 0u), + .h = node.attribute_value("max_height", 0u) }, + .px = Area::from_xml(node) }; + } + + void generate(Xml_generator &xml) const + { + if (max_px.w) xml.attribute("max_width", max_px.w); + if (max_px.h) xml.attribute("max_height", max_px.h); + if (px.w) xml.attribute("width", px.w); + if (px.h) xml.attribute("height", px.h); + } + }; + + Manual_attr _manual_attr { }; + + unsigned _num_merged = 0; + + bool _known(Fb_connectors::Connector const &connector) + { + for (Entry const &entry : _entries) + if (entry.name == connector.name) + return true; + return false; + } + + void _with_known(Fb_connectors::Connector const &connector, auto const &fn) + { + for (Entry &entry : _entries) + if (entry.name == connector.name) + fn(entry); + } + + void _insert_at(unsigned at, Entry const &entry) + { + if (at >= MAX_ENTRIES) { + warning("maximum number of ", MAX_ENTRIES, " fb config entries exeeded"); + return; + } + + for (unsigned i = MAX_ENTRIES - 1; i > at; i--) + _entries[i] = _entries[i - 1]; + + _entries[at] = entry; + } + + /* + * A new merged connector such that the smallest mode stays in front + */ + void _add_unknown_merged(Entry const &new_entry) + { + unsigned at = 0; + for (; at < _num_merged && _entries[at].smaller_than(new_entry); at++); + + _insert_at(at, new_entry); + + if (_num_merged < MAX_ENTRIES) + _num_merged++; + } + + void _add_unknown_discrete(Entry const &new_entry) + { + unsigned at = 0; + for (; at < MAX_ENTRIES && _entries[at].defined; at++); + + _insert_at(at, new_entry); + } + + void import_manual_config(Xml_node const &config) + { + _manual_attr = Manual_attr::from_xml(config); + + unsigned count = 0; + + auto add_connectors = [&] (Xml_node const &node) + { + node.for_each_sub_node("connector", [&] (Xml_node const &node) { + Entry const e = Entry::from_manual_xml(node); + if (!_known(e.name) && count < MAX_ENTRIES) { + _entries[count] = e; + count++; + } + }); + }; + + /* import merged nodes */ + config.with_optional_sub_node("merge", [&] (Xml_node const &merge) { + add_connectors(merge); }); + + _num_merged = count; + + /* import discrete nodes */ + add_connectors(config); + } + + void apply_connectors(Fb_connectors const &connectors) + { + /* apply information for connectors known from the manual config */ + connectors.for_each([&] (Fb_connectors::Connector const &conn) { + _with_known(conn.name, [&] (Entry &e) { + + if (e.present) /* apply config only once */ + return; + + if (!e.mode_attr.px.valid()) { /* switched off by config */ + e.mode_id = { }; + e.mode_attr = { }; + e.present = true; + return; + } + + conn.with_matching_mode(e.mode_id, e.mode_attr, + [&] (Fb_connectors::Connector::Mode const &mode) { + e.mode_id = mode.id; + e.mode_attr = mode.attr; + e.present = true; }); + }); + }); + + connectors._merged.for_each([&] (Fb_connectors::Connector const &conn) { + if (!_known(conn.name)) + _add_unknown_merged(Entry::from_connector(conn)); }); + + connectors._discrete.for_each([&] (Fb_connectors::Connector const &conn) { + if (!_known(conn.name)) + _add_unknown_discrete(Entry::from_connector(conn)); }); + } + + void _with_entry(Entry::Name const &name, auto const &fn) + { + for (Entry &entry : _entries) + if (entry.name == name) + fn(entry); + } + + void select_fb_mode(Fb_connectors::Name const &conn, + Fb_connectors::Connector::Mode::Id const &mode_id, + Fb_connectors const &connectors) + { + connectors.with_mode_attr(conn, mode_id, [&] (Entry::Mode_attr const &attr) { + _with_entry(conn, [&] (Entry &entry) { + entry.mode_attr = attr; + entry.mode_id = mode_id; }); }); + } + + void disable_connector(Fb_connectors::Name const &conn) + { + _with_entry(conn, [&] (Entry &entry) { + entry.mode_attr = { }; }); + } + + void brightness(Fb_connectors::Name const &conn, unsigned percent) + { + _with_entry(conn, [&] (Entry &entry) { + entry.brightness.percent = percent; }); + } + + void _with_idx(Fb_connectors::Name const &conn, auto const &fn) const + { + for (unsigned i = 0; i < MAX_ENTRIES; i++) + if (_entries[i].name == conn && _entries[i].defined) { + fn(i); + return; + } + } + + void _swap_entries(unsigned i, unsigned j) + { + Entry tmp = _entries[i]; + _entries[i] = _entries[j]; + _entries[j] = tmp; + } + + /** + * Swap connector with next present predecessor + */ + void swap_connector(Fb_connectors::Name const &conn) + { + _with_idx(conn, [&] (unsigned const conn_idx) { + + if (conn_idx < 1) /* first entry cannot have a predecessor */ + return; + + /* search present predecessor */ + unsigned prev_idx = conn_idx - 1; + while (prev_idx > 0 && !_entries[prev_idx].present) + prev_idx--; + + _swap_entries(conn_idx, prev_idx); + }); + } + + void toggle_merge_discrete(Fb_connectors::Name const &conn) + { + _with_idx(conn, [&] (unsigned const idx) { + + if (idx < _num_merged) { + + /* + * Turn merged entry into discrete entry. + * + * There may be (non-present) merge entries following idx. + * Bubble up the entry so that it becomes the last merge + * entry before turning it into the first discrete entry by + * decreasing '_num_merged'. + */ + if (_num_merged > 0) { + for (unsigned i = idx; i < _num_merged - 1; i++) + _swap_entries(i, i + 1); + _num_merged--; + } + } else { + + /* + * Turn discrete entry into merged entry + */ + if (_num_merged < MAX_ENTRIES) { + for (unsigned i = idx; i > _num_merged; i--) + _swap_entries(i, i - 1); + _num_merged++; + } + } + }); + } + + struct Merge_info { Entry::Name name; Area px; }; + + void _with_merge_info(auto const &fn) const + { + Merge_info info { }; + + /* merged screen size and name corresponds to first enabled connector */ + for (unsigned i = 0; i < _num_merged; i++) { + info = { .name = _entries[i].name, + .px = _entries[i].mode_attr.px }; + if (info.px.valid() && _entries[i].present) + break; + } + + /* if all merged connectors are switched of, use name of first one */ + if (!info.px.valid()) { + for (unsigned i = 0; i < _num_merged; i++) { + info = { .name = _entries[i].name, + .px = _entries[i].mode_attr.px }; + if (_entries[i].present) + break; + } + } + + if (info.name.length() > 1) + fn(info); + }; + + void _gen_merge_node(Xml_generator &xml) const + { + _with_merge_info([&] (Merge_info const &info) { + xml.node("merge", [&] { + xml.attribute("width", info.px.w); + xml.attribute("height", info.px.h); + xml.attribute("name", info.name); + + for (unsigned i = 0; i < _num_merged; i++) + _entries[i].generate(xml); + }); + }); + } + + void generate_managed_fb(Xml_generator &xml) const + { + _manual_attr.generate(xml); + + xml.attribute("system", "yes"); /* for screen blanking on suspend */ + + xml.node("report", [&] { xml.attribute("connectors", "yes"); }); + + _gen_merge_node(xml); + + /* nodes for discrete connectors */ + for (unsigned i = _num_merged; i < MAX_ENTRIES; i++) + _entries[i].generate(xml); + } + + void for_each_present_connector(Fb_connectors const &connectors, auto const &fn) const + { + for (Entry const &entry : _entries) + if (entry.defined && entry.present) + connectors.with_connector(entry.name, fn); + } + + unsigned num_present_merged() const + { + unsigned count = 0; + for (unsigned i = 0; i < _num_merged; i++) + if (_entries[i].defined && _entries[i].present) + count++; + return count; + } +}; + +#endif /* _MODEL__FB_CONFIG_H_ */ diff --git a/repos/gems/src/app/sculpt_manager/model/fb_connectors.h b/repos/gems/src/app/sculpt_manager/model/fb_connectors.h new file mode 100644 index 0000000000..ed8b3a0081 --- /dev/null +++ b/repos/gems/src/app/sculpt_manager/model/fb_connectors.h @@ -0,0 +1,278 @@ +/* + * \brief Representation of connectors reported by the framebuffer driver + * \author Norman Feske + * \date 2024-10-23 + */ + +/* + * Copyright (C) 2024 Genode Labs GmbH + * + * This file is part of the Genode OS framework, which is distributed + * under the terms of the GNU Affero General Public License version 3. + */ + +#ifndef _MODEL__FB_CONNECTORS_H_ +#define _MODEL__FB_CONNECTORS_H_ + +#include + +namespace Sculpt { struct Fb_connectors; }; + + +struct Sculpt::Fb_connectors : private Noncopyable +{ + using Area = Gui::Area; + using Name = String<16>; + + struct Connector; + using Connectors = List_model; + + struct Brightness + { + bool defined; + unsigned percent; + + bool operator != (Brightness const &other) const + { + return defined != other.defined || percent != other.percent; + } + + static Brightness from_xml(Xml_node const &node) + { + return { .defined = node.has_attribute("brightness"), + .percent = node.attribute_value("brightness", 0u) }; + } + }; + + struct Connector : Connectors::Element + { + Name const name; + Area mm { }; + Brightness brightness { }; + + struct Mode; + using Modes = List_model; + + Modes _modes { }; + + struct Mode : Modes::Element + { + using Id = String<16>; + + Id const id; + + struct Attr + { + Name name; + Area px; + Area mm; + bool used; + unsigned hz; + + bool operator != (Attr const &other) const + { + return name != other.name + || px != other.px + || mm != other.mm + || used != other.used + || hz != other.hz; + } + + static Attr from_xml(Xml_node const &node) + { + return { + .name = node.attribute_value("name", Name()), + .px = { .w = node.attribute_value("width", 0u), + .h = node.attribute_value("height", 0u) }, + .mm = { .w = node.attribute_value("width_mm", 0u), + .h = node.attribute_value("height_mm", 0u) }, + .used = node.attribute_value("used", false), + .hz = node.attribute_value("hz", 0u) + }; + } + }; + + Attr attr { }; + + Mode(Id id) : id(id) { }; + + static Id id_from_xml(Xml_node const &node) + { + return node.attribute_value("id", Mode::Id()); + } + + bool matches(Xml_node const &node) const + { + return id_from_xml(node) == id; + } + + static bool type_matches(Xml_node const &node) + { + return node.has_type("mode"); + } + + bool has_resolution(Area px) const { return attr.px == px; } + bool has_hz (unsigned hz) const { return attr.hz == hz; } + }; + + Connector(Name const &name) : name(name) { } + + void _with_mode(auto const &match_fn, auto const &fn) const + { + bool found = false; + _modes.for_each([&] (Mode const &mode) { + if (!found && match_fn(mode.attr)) { + fn(mode); + found = true; } }); + } + + void with_used_mode(auto const &fn) const + { + _with_mode([&] (Mode::Attr const &attr) { return attr.used; }, fn); + } + + void with_matching_mode(Mode::Id const &preferred_id, + Mode::Attr const &attr, auto const &fn) const + { + auto matches_resolution_and_id = [&] (Mode const &mode) + { + return mode.has_resolution(attr.px) && (mode.id == preferred_id); + }; + + auto matches_resolution_and_hz = [&] (Mode const &mode) + { + return mode.has_resolution(attr.px) && mode.has_hz(attr.hz); + }; + + auto matches_resolution = [&] (Mode const &mode) + { + return mode.has_resolution(attr.px); + }; + + bool matched = false; + auto with_match_once = [&] (auto const &matches_fn, auto const &fn) + { + if (!matched) + _modes.for_each([&] (Mode const &mode) { + if (matches_fn(mode)) { + fn(mode); + matched = true; } }); + }; + + with_match_once(matches_resolution_and_id, fn); + with_match_once(matches_resolution_and_hz, fn); + with_match_once(matches_resolution, fn); + + if (!matched) + with_used_mode([&] (Mode const &used) { + fn(used); + matched = true; }); + } + + bool update(Allocator &alloc, Xml_node const &node) + { + Area const orig_mm = mm; + Brightness const orig_brightness = brightness; + + mm.w = node.attribute_value("width_mm", 0u); + mm.h = node.attribute_value("height_mm", 0u); + brightness = Brightness::from_xml(node); + + bool progress = (orig_mm != mm || orig_brightness != brightness); + + _modes.update_from_xml(node, + [&] (Xml_node const &node) -> Mode & { + progress = true; + return *new (alloc) Mode(Mode::id_from_xml(node)); + }, + [&] (Mode &mode) { + progress = true; + destroy(alloc, &mode); + }, + [&] (Mode &mode, Xml_node const &node) { + Mode::Attr const orig_attr = mode.attr; + mode.attr = Mode::Attr::from_xml(node); + progress |= (orig_attr != mode.attr); + } + ); + + return progress; + } + + bool matches(Xml_node const &node) const + { + return node.attribute_value("name", Name()) == name; + } + + static bool type_matches(Xml_node const &node) + { + return node.has_type("connector") + && node.attribute_value("connected", false); + } + }; + + Connectors _merged { }; + Connectors _discrete { }; + + [[nodiscard]] Progress update(Allocator &alloc, Xml_node const &connectors) + { + bool progress = false; + + auto update = [&] (Connectors &model, Xml_node const &node) + { + model.update_from_xml(node, + [&] (Xml_node const &node) -> Connector & { + progress = true; + return *new (alloc) Connector(node.attribute_value("name", Name())); + }, + [&] (Connector &conn) { + progress = true; + conn.update(alloc, Xml_node("")); + destroy(alloc, &conn); + }, + [&] (Connector &conn, Xml_node const &node) { + progress |= conn.update(alloc, node); + }); + }; + + update(_discrete, connectors); + + connectors.with_sub_node("merge", + [&] (Xml_node const &merge) { update(_merged, merge); }, + [&] { update(_merged, Xml_node("")); }); + + return { progress }; + } + + static unsigned _count(Connectors const &connectors) + { + unsigned count = 0; + connectors.for_each([&] (Connector const &) { count++; }); + return count; + } + + unsigned num_merged() const { return _count(_merged); } + + void for_each(auto const &fn) const + { + _merged .for_each(fn); + _discrete.for_each(fn); + } + + void with_connector(Name const &conn_name, auto const &fn) const + { + for_each([&] (Connector const &connector) { + if (connector.name == conn_name) + fn(connector); }); + } + + void with_mode_attr(Name const &conn_name, Connector::Mode::Id const &id, auto const &fn) const + { + with_connector(conn_name, [&] (Connector const &connector) { + connector._modes.for_each([&] (Connector::Mode const &mode) { + if (mode.id == id) + fn(mode.attr); }); }); + } +}; + +#endif /* _MODEL__FB_CONNECTORS_H_ */ diff --git a/repos/gems/src/app/sculpt_manager/view/fb_widget.h b/repos/gems/src/app/sculpt_manager/view/fb_widget.h new file mode 100644 index 0000000000..6b1de2815f --- /dev/null +++ b/repos/gems/src/app/sculpt_manager/view/fb_widget.h @@ -0,0 +1,187 @@ +/* + * \brief Framebuffer settings + * \author Norman Feske + * \date 2024-10-23 + */ + +/* + * Copyright (C) 2024 Genode Labs GmbH + * + * This file is part of the Genode OS framework, which is distributed + * under the terms of the GNU Affero General Public License version 3. + */ + +#ifndef _VIEW__FB_WIDGET_H_ +#define _VIEW__FB_WIDGET_H_ + +#include +#include + +namespace Sculpt { struct Fb_widget; } + +struct Sculpt::Fb_widget : Widget +{ + using Connector = Fb_connectors::Connector; + using Mode = Connector::Mode; + using Hosted_choice = Hosted>; + using Mode_radio = Hosted>; + + struct Action : Interface + { + virtual void select_fb_mode(Fb_connectors::Name const &, Mode::Id const &) = 0; + virtual void disable_fb_connector(Fb_connectors::Name const &) = 0; + virtual void toggle_fb_merge_discrete(Fb_connectors::Name const &) = 0; + virtual void swap_fb_connector(Fb_connectors::Name const &) = 0; + virtual void fb_brightness(Fb_connectors::Name const &, unsigned) = 0; + }; + + Fb_connectors::Name _selected_connector { }; + + struct Bar : Widget + { + void view(Scope &s, unsigned const percent) const + { + for (unsigned i = 0; i < 10; i++) { + s.sub_scope