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
This commit is contained in:
Norman Feske 2024-10-23 19:37:28 +02:00 committed by Christian Helmuth
parent 8d76eebf93
commit 8c43f8aa33
10 changed files with 1056 additions and 28 deletions

View File

@ -811,8 +811,8 @@ struct Sculpt::Main : Input_event_handler,
Conditional_widget<Graph> Conditional_widget<Graph>
_graph { Id { "graph" }, _graph { Id { "graph" },
_runtime_state, _cached_runtime_config, _storage._storage_devices, _runtime_state, _cached_runtime_config, _storage._storage_devices,
_storage._selected_target, _storage._ram_fs_state, _storage._selected_target, _storage._ram_fs_state, _fb_connectors,
_popup.state, _deploy._children }; _fb_config, _popup.state, _deploy._children };
Conditional_widget<Network_widget> Conditional_widget<Network_widget>
_network_widget { Conditional_widget<Network_widget>::Attr { .centered = true }, _network_widget { Conditional_widget<Network_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 ** ** Runtime graph **
*******************/ *******************/

View File

@ -14,17 +14,44 @@
#ifndef _DRIVER__FB_H_ #ifndef _DRIVER__FB_H_
#define _DRIVER__FB_H_ #define _DRIVER__FB_H_
#include <i2c_session/i2c_session.h>
namespace Sculpt { struct Fb_driver; } namespace Sculpt { struct Fb_driver; }
struct Sculpt::Fb_driver : private Noncopyable 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<Child_state> _intel_gpu { }, Constructible<Child_state> _intel_gpu { },
_intel_fb { }, _intel_fb { },
_vesa_fb { }, _vesa_fb { },
_boot_fb { }, _boot_fb { },
_soc_fb { }; _soc_fb { };
Constructible<Rom_handler<Fb_driver>> _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 void gen_start_nodes(Xml_generator &xml) const
{ {
auto gen_capture_route = [&] (Xml_generator &xml) 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 && bool const use_vesa = !use_intel_fb && !suspending &&
board_info.detected.vga && !use_boot_fb; board_info.detected.vga && !use_boot_fb;
Fb_name const orig_fb_name = _fb_name();
_intel_gpu.conditional(use_intel_gpu, _intel_gpu.conditional(use_intel_gpu,
registry, "intel_gpu", Priority::MULTIMEDIA, registry, "intel_gpu", Priority::MULTIMEDIA,
Ram_quota { 32*1024*1024 }, Cap_quota { 1400 }); 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::with_mode(platform, [&] (Boot_fb::Mode mode) {
_boot_fb.construct(registry, "boot_fb", Priority::MULTIMEDIA, _boot_fb.construct(registry, "boot_fb", Priority::MULTIMEDIA,
mode.ram_quota(), Cap_quota { 100 }); }); 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) 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 return board_info.detected.intel_gfx
&& !board_info.options.suppress.intel_gpu; && !board_info.options.suppress.intel_gpu;
} }
void with_connectors(auto const &fn) const
{
if (_connectors.constructed())
_connectors->with_xml(fn);
}
}; };
#endif /* _DRIVER__FB_H_ */ #endif /* _DRIVER__FB_H_ */

View File

@ -100,7 +100,7 @@ class Sculpt::Drivers::Instance : Noncopyable,
Ps2_driver _ps2_driver { }; Ps2_driver _ps2_driver { };
Touch_driver _touch_driver { }; Touch_driver _touch_driver { };
Fb_driver _fb_driver { }; Fb_driver _fb_driver { _env, _action };
Usb_driver _usb_driver { _env, *this, *this }; Usb_driver _usb_driver { _env, *this, *this };
Ahci_driver _ahci_driver { _env, *this }; Ahci_driver _ahci_driver { _env, *this };
Nvme_driver _nvme_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_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 bool suspend_supported() const
{ {
@ -204,7 +205,8 @@ Sculpt::Drivers::Drivers(Env &env, Children &children, Info const &info, Action
void Drivers::_with(With_storage_devices::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(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_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_usb () { _instance.update_usb(); }
void Drivers::update_soc (Board_info::Soc soc) { _instance.update_soc(soc); } void Drivers::update_soc (Board_info::Soc soc) { _instance.update_soc(soc); }

View File

@ -18,6 +18,7 @@
#include <xml.h> #include <xml.h>
#include <model/child_state.h> #include <model/child_state.h>
#include <model/board_info.h> #include <model/board_info.h>
#include <driver/fb.h>
namespace Sculpt { struct Drivers; } namespace Sculpt { struct Drivers; }
@ -26,7 +27,7 @@ class Sculpt::Drivers : Noncopyable
{ {
public: public:
struct Action : Interface struct Action : virtual Fb_driver::Action
{ {
virtual void handle_device_plug_unplug() = 0; virtual void handle_device_plug_unplug() = 0;
}; };
@ -53,11 +54,12 @@ class Sculpt::Drivers : Noncopyable
using With_storage_devices = With<Storage_devices const &>; using With_storage_devices = With<Storage_devices const &>;
using With_board_info = With<Board_info const &>; using With_board_info = With<Board_info const &>;
using With_platform_info = With<Xml_node const &>; using With_xml = With<Xml_node const &>;
void _with(With_storage_devices::Callback const &) const; void _with(With_storage_devices::Callback const &) const;
void _with(With_board_info::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: 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_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_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 */ /* true if hardware is suspend/resume capable */
bool suspend_supported() const; bool suspend_supported() const;

View File

@ -100,6 +100,9 @@ void Graph::_view_selected_node_content(Scope<Depgraph, Frame, Vbox> &s,
if (name == "ram_fs") if (name == "ram_fs")
s.widget(_ram_fs_widget, _selected_target, _ram_fs_state); 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 String<100> const
ram (Capacity{info.assigned_ram - info.avail_ram}, " / ", ram (Capacity{info.assigned_ram - info.avail_ram}, " / ",
Capacity{info.assigned_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); _ram_fs_widget .propagate(at, _selected_target, action);
_fb_widget .propagate(at, _fb_connectors, action);
_ahci_devices_widget.propagate(at, action); _ahci_devices_widget.propagate(at, action);
_nvme_devices_widget.propagate(at, action); _nvme_devices_widget.propagate(at, action);
_mmc_devices_widget .propagate(at, action); _mmc_devices_widget .propagate(at, action);

View File

@ -25,6 +25,7 @@
#include <types.h> #include <types.h>
#include <view/storage_widget.h> #include <view/storage_widget.h>
#include <view/ram_fs_widget.h> #include <view/ram_fs_widget.h>
#include <view/fb_widget.h>
#include <model/capacity.h> #include <model/capacity.h>
#include <model/popup.h> #include <model/popup.h>
#include <model/runtime_config.h> #include <model/runtime_config.h>
@ -42,6 +43,8 @@ struct Sculpt::Graph : Widget<Depgraph>
Storage_devices const &_storage_devices; Storage_devices const &_storage_devices;
Storage_target const &_selected_target; Storage_target const &_selected_target;
Ram_fs_state const &_ram_fs_state; Ram_fs_state const &_ram_fs_state;
Fb_connectors const &_fb_connectors;
Fb_config const &_fb_config;
Popup::State const &_popup_state; Popup::State const &_popup_state;
Depot_deploy::Children const &_deploy_children; Depot_deploy::Children const &_deploy_children;
@ -50,6 +53,9 @@ struct Sculpt::Graph : Widget<Depgraph>
Hosted<Depgraph, Frame, Vbox, Ram_fs_widget> Hosted<Depgraph, Frame, Vbox, Ram_fs_widget>
_ram_fs_widget { Id { "ram_fs" } }; _ram_fs_widget { Id { "ram_fs" } };
Hosted<Depgraph, Frame, Vbox, Fb_widget>
_fb_widget { Id { "fb" } };
Hosted<Depgraph, Frame, Vbox, Frame, Hbox, Deferred_action_button> Hosted<Depgraph, Frame, Vbox, Frame, Hbox, Deferred_action_button>
_remove { Id { "Remove" } }, _remove { Id { "Remove" } },
_restart { Id { "Restart" } }; _restart { Id { "Restart" } };
@ -81,18 +87,22 @@ struct Sculpt::Graph : Widget<Depgraph>
Storage_devices const &storage_devices, Storage_devices const &storage_devices,
Storage_target const &selected_target, Storage_target const &selected_target,
Ram_fs_state const &ram_fs_state, Ram_fs_state const &ram_fs_state,
Fb_connectors const &fb_connectors,
Fb_config const &fb_config,
Popup::State const &popup_state, Popup::State const &popup_state,
Depot_deploy::Children const &deploy_children) Depot_deploy::Children const &deploy_children)
: :
_runtime_state(runtime_state), _runtime_config(runtime_config), _runtime_state(runtime_state), _runtime_config(runtime_config),
_storage_devices(storage_devices), _selected_target(selected_target), _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) _deploy_children(deploy_children)
{ } { }
void view(Scope<Depgraph> &) const; void view(Scope<Depgraph> &) 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 remove_deployed_component(Start_name const &) = 0;
virtual void restart_deployed_component(Start_name const &) = 0; virtual void restart_deployed_component(Start_name const &) = 0;

View File

@ -40,6 +40,7 @@
#include <model/presets.h> #include <model/presets.h>
#include <model/screensaver.h> #include <model/screensaver.h>
#include <model/system_state.h> #include <model/system_state.h>
#include <model/fb_config.h>
#include <view/download_status_widget.h> #include <view/download_status_widget.h>
#include <view/popup_dialog.h> #include <view/popup_dialog.h>
#include <view/panel_dialog.h> #include <view/panel_dialog.h>
@ -1561,18 +1562,6 @@ struct Sculpt::Main : Input_event_handler,
_cached_runtime_config, _cached_runtime_config,
_file_browser_state, *this }; _file_browser_state, *this };
Managed_config<Main> _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(Xml_node const &, Xml_node const &);
void _update_window_layout() void _update_window_layout()
@ -1616,6 +1605,96 @@ struct Sculpt::Main : Input_event_handler,
Signal_handler<Main> _wheel_handler { _env.ep(), *this, &Main::_update_window_layout }; Signal_handler<Main> _wheel_handler { _env.ep(), *this, &Main::_update_window_layout };
/**********************************
** Display driver configuration **
**********************************/
Fb_connectors _fb_connectors { };
Rom_handler<Main> _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 ** ** Runtime graph **
*******************/ *******************/
@ -1623,8 +1702,8 @@ struct Sculpt::Main : Input_event_handler,
Popup _popup { }; Popup _popup { };
Graph _graph { _runtime_state, _cached_runtime_config, _storage._storage_devices, Graph _graph { _runtime_state, _cached_runtime_config, _storage._storage_devices,
_storage._selected_target, _storage._ram_fs_state, _storage._selected_target, _storage._ram_fs_state, _fb_connectors,
_popup.state, _deploy._children }; _fb_config, _popup.state, _deploy._children };
struct Graph_dialog : Dialog::Top_level_dialog struct Graph_dialog : Dialog::Top_level_dialog
{ {
@ -1655,7 +1734,6 @@ struct Sculpt::Main : Input_event_handler,
_gui.input.sigh(_input_handler); _gui.input.sigh(_input_handler);
_gui.info_sigh(_gui_mode_handler); _gui.info_sigh(_gui_mode_handler);
_handle_gui_mode(); _handle_gui_mode();
_fb_config.trigger_update();
/* /*
* Generate initial configurations * Generate initial configurations

View File

@ -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 <model/fb_connectors.h>
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_ */

View File

@ -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 <types.h>
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<Connector>;
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<Mode>;
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("<empty/>"));
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("<merge/>")); });
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_ */

View File

@ -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 <view/dialog.h>
#include <model/fb_config.h>
namespace Sculpt { struct Fb_widget; }
struct Sculpt::Fb_widget : Widget<Vbox>
{
using Connector = Fb_connectors::Connector;
using Mode = Connector::Mode;
using Hosted_choice = Hosted<Vbox, Choice<Mode::Id>>;
using Mode_radio = Hosted<Radio_select_button<Mode::Id>>;
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<Right_floating_hbox>
{
void view(Scope<Right_floating_hbox> &s, unsigned const percent) const
{
for (unsigned i = 0; i < 10; i++) {
s.sub_scope<Button>(Id { i }, [&] (Scope<Right_floating_hbox, Button> &s) {
if (s.hovered()) s.attribute("hovered", "yes");
if (i*10 <= percent)
s.attribute("selected", "yes");
else
s.attribute("style", "unimportant");
s.sub_scope<Float>([&] (auto &) { });
});
}
}
void click(Clicked_at const &at, auto const &fn)
{
Id const id = at.matching_id<Right_floating_hbox, Button>();
unsigned value = 0;
if (!ascii_to(id.value.string(), value))
return;
unsigned const percent = max(10u, min(100u, value*10 + 9));
fn(percent);
}
};
using Hosted_brightness = Hosted<Bar>;
void view(Scope<Vbox> &s, Fb_connectors const &connectors, Fb_config const &config) const
{
auto view_connector = [&] (Connector const &conn)
{
Hosted_choice choice { Id { conn.name }, conn.name };
Mode::Id selected_mode { "off" };
conn._modes.for_each([&] (Mode const &mode) {
if (mode.attr.used)
selected_mode = mode.id; });
s.widget(choice,
Hosted_choice::Attr {
.left_ex = 12, .right_ex = 28,
.unfolded = _selected_connector,
.selected_item = selected_mode
},
[&] (Hosted_choice::Sub_scope &s) {
if (conn.brightness.defined) {
Hosted_brightness brightness { Id { "brightness" } };
s.widget(brightness, conn.brightness.percent);
}
conn._modes.for_each([&] (Mode const &mode) {
String<32> text { mode.attr.name };
if (mode.attr.hz)
text = { text, " (", mode.attr.hz, " Hz)" };
s.widget(Mode_radio { Id { mode.id }, mode.id },
selected_mode, text);
});
s.widget(Mode_radio { Id { "off" }, "off" }, selected_mode, "off");
});
};
unsigned const num_merged = config.num_present_merged();
auto view_controls = [&] (Scope<Vbox> &s, unsigned const count, Id const &id)
{
if (count <= 1)
return;
s.sub_scope<Float>([&] (Scope<Vbox, Float> &s) {
s.sub_scope<Hbox>(id, [&] (Scope<Vbox, Float, Hbox> &s) {
s.sub_scope<Button>(Id { "equal" }, [&] (Scope<Vbox, Float, Hbox, Button> &s) {
if (count <= num_merged)
s.attribute("selected", "yes");
if (count == num_merged || count == num_merged + 1) {
if (s.hovered() && !s.dragged())
s.attribute("hovered", "yes");
} else {
s.attribute("style", "unimportant");
}
s.sub_scope<Label>("=");
});
s.sub_scope<Button>(Id { "swap" }, [&] (Scope<Vbox, Float, Hbox, Button> &s) {
s.sub_scope<Label>("Swap");
if (s.hovered() && !s.dragged())
s.attribute("hovered", "yes");
if (s.hovered() && s.dragged())
s.attribute("selected", "yes");
});
});
});
};
unsigned count = 0;
config.for_each_present_connector(connectors, [&] (Connector const &conn) {
count++;
view_controls(s, count, Id { conn.name });
view_connector(conn);
});
}
void click(Clicked_at const &at, Fb_connectors const &connectors, Action &action)
{
auto click_connector = [&] (Connector const &conn)
{
Hosted_choice choice { Id { conn.name }, conn.name };
choice.propagate(at, _selected_connector,
[&] { _selected_connector = { }; },
[&] (Clicked_at const &at) {
Id const id = at.matching_id<Mode_radio>();
if (id.value == "brightness") {
Hosted_brightness brightness { Id { "brightness" } };
brightness.propagate(at, [&] (unsigned percent) {
action.fb_brightness(conn.name, percent); });
} else if (id.value == "off")
action.disable_fb_connector(conn.name);
else if (id.valid())
action.select_fb_mode(conn.name, id);
});
};
connectors._merged.for_each([&] (Connector const &conn) {
click_connector(conn); });
connectors._discrete.for_each([&] (Connector const &conn) {
click_connector(conn); });
/* operation buttons */
{
Id const conn = at.matching_id<Vbox, Float, Hbox>();
Id const op = at.matching_id<Vbox, Float, Hbox, Button>();
if (op.value == "equal") action.toggle_fb_merge_discrete(conn.value);
if (op.value == "swap") action.swap_fb_connector(conn.value);
}
}
};
#endif /* _VIEW__FB_WIDGET_H_ */