Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

On implementing output profiles for wayland. #3645

Closed
1 task done
oddlama opened this issue Jun 20, 2022 · 16 comments
Closed
1 task done

On implementing output profiles for wayland. #3645

oddlama opened this issue Jun 20, 2022 · 16 comments

Comments

@oddlama
Copy link
Contributor

oddlama commented Jun 20, 2022

The issue:

Currently, an external program (kanshi) is needed to configure displays on qtile with wayland, which should technically be a part of the compositor. I would love to provide a PR for this, I've already successfully tested some of the ideas I have. But before implementing anything of larger scale, I'd like to discuss my approach here to ensure we all agree on a reasonable solution.

Summary

For those unfamiliar with kanshi, it is a tool similar to autorandr on X11, but dead-simple: You first define several profiles. Each profile can assign configuration to outputs (enable, mode, position, scaling). Whenever outputs change, the best-matching profile is activated and outputs are configured accordingly. Sway has a similar built-in mechanism for output configuration.

Also closely related to this proposal is a feature that has already been asked for in #3389; Sway automatically scales HiDPI displays to 2 without requiring any configuration, and qtile could do the same for convenience. It's basically a default scaling that can be overwritten by using output profiles.

I've seen that qtile already has bindings to all necessary wayland protocol extensions that are necessary to replace kanshi completely, without much effort. So to solve both of this, I'd suggest the following:

  1. Implement output profiles similar to kanshi. See below for syntax discussion.
  2. Apply a simple default scaling to each display, similar to what sway does (see my comment on [FR] Auto adjust DPI by default like in Sway #3389), but only if the scaling was not specified in the matched profile. Sway's detection is also very basic (scaling is either 1 or 2), and could maybe be improved.

Open Problems

If I understand qtile's Screens correctly, they map to physical outputs in a 1:1 fashion. Yet I struggle to understand how you currently ensure that certain screens always map to specific outputs. Imagine a laptop with an optional second HDMI display. Depending on the output configuration, users may want the Bar on the internal screen (e.g. beamer connected) or on the external screen (docking station connected). How is this done currently?

This would be solved by not having a global screens = [...] configuration, but instead by assigning the screens in the matched profile. So I can have a bar on the internal screen in profile 1 and no bar in profile 2. This is IMO much more intuitive and more. (Or maybe I don't understand Screens in their current form).

I'm also not sure how to address mirroring and more complex configurations. See swaywm/sway#1666 for examples.

Profile Syntax

Similar to kanshi, I'd introduce a list of profiles which will be matched from top to bottom.

profiles = [
	# internal only
	Profile([
		Output(identifier="eDP-1", enable=True, scale=2)
		Output(identifier="LVDS", enable=False)
	]),
	# specific docking station
	Profile([
		Output(identifier="eDP-1", scale=2)
		Output(name="My specific home display name", scale=1.5, position=(1920, 0))
	]),
	# other hdmi outputs (probably beamers, matched after specific config above)
	Profile([
		Output(identifier="eDP-1", scale=2)
		Output(identifier="HDMI-1", position=(1920, 0))
	]),
	Profile([
		Output(match=r".*", scale=2)
	]),
]

Ideally, scale, position and other properties would allow dynamic configuration. But I'd regard that as a secondary issue for now. For example:

def on_configure_output(output):
	scale = calculate_scale(output.width, output.height)
	output.set_scale(scale)
	# output.set_position(...)

profiles = [
	Profile([
		Output(match=r"HDMI-1", scale=calculate_scale) # Allow passing lambda functions to specific properties
		Output(match=r".*", on_config=on_configure_output) # Or allow taking over the full config. Both might be desirable.
	]),
]

Additional thoughts

This change would definitely be wayland exclusive. I have no interest in implementing this for X11, and don't even think it's possible to do properly. Scaling support is almost nonexistent on X11 anyway, and the standard way to apply display configuration is xrandr.

Required:

  • I have searched past issues to see if this bug has already been reported.
@oddlama
Copy link
Contributor Author

oddlama commented Jun 20, 2022

I've looked a bit more at how screens are used in qtile internally. If I understand correctly, it works in several stages:

  1. You define a list of screens by default in your config.
  2. When physical outputs change, a hook is fired
  3. You register this hook in your config and then manually overwrite config.screens
  4. At the end of your registered hook you fire cmd_reconfigure_screens to have qtile relayout to the new screen config.

Please correct me if I'm wrong.

EDIT: From what I can tell, screens have to be configured in the order in which the physical outputs are enumerated by the backend. If not, they will refer to wrong outputs. Now I also understand why my displays sometimes did switch bar and position - the enumeration order of the physical devices was unstable. So I guess Output would be a more fitting name.

@m-col
Copy link
Member

m-col commented Jun 26, 2022

I'm open to the idea, but want to cover all other options (i.e. via external tools) before committing to have anything within the backend.

I've looked a bit more at how screens are used in qtile internally. If I understand correctly, it works in several stages:

The user doesn't need to manually do anything besides defining the number of Screens that they could need at any one time. If they set reconfigure_screens to True, then cmd_reconfigure_screens is called for them upon change of the output layout. I don't think config.screens would need changing if it is set up for all possible output configurations. The way that works could be documented better for sure, especially about how those screens map to outputs, and how the primary output plays into that. Looks like you figured that out per your edit.

With that being the case, is it possible to attain the desired setup for most reasonable output configurations that one might have through regular usage, just with careful ordering of config.screens and selection of the primary output?

@oddlama
Copy link
Contributor Author

oddlama commented Jun 26, 2022

I'm open to the idea, but want to cover all other options (i.e. via external tools) before committing to have anything within the backend.

A fair decision. I'm not sure how it would turn out in the end, but "profiles" really sounds like a lot more than it would actually be in terms of code. The crucial difference is to replace the ordered matching of outputs to screens with a function that assigns each output exactly one screen, but depending on the available outputs. It might even remove the need for the virtual_screens variable, except when you want to support combining screens.

The user doesn't need to manually do anything besides defining the number of Screens that they could need at any one time. [...]

Not initially i suppose. Yet I'm not sure whether this is sufficient to configure multiple different setups correctly. A lot of people plug in beamers temporarily, or dock their laptop at home. I would love to be able to have a different layout optimized to each scenario. Some common ones that come to mind are:

  • Only laptop: 1 Screen with bar optimized for laptop use
  • At home: 2 screens (laptop screen disabled, 2 external screens). Different bar & wallpaper setup.
  • At work: 2 screens (laptop screen, plus 1 widescreen external monitor). Bar only on external screen for readability, but I'd like to split the external screen into two virtual screens.
  • Temporarily connected beamer: 2 screens (laptop, beamer). Keep bar on internal screen, still in "laptop mode".
  • (Also, how would I mirror to a beamer?)

I don't think config.screens would need changing if it is set up for all possible output configurations.
[...] is it possible to attain the desired setup for most reasonable output configurations that one might have through regular usage, just with careful ordering of config.screens and selection of the primary output?

All of the scenarios I listed require only 2 screens to be defined - but each screen definition would contain different bars and maybe wallpapers. What is the intended way to do this with the current configuration?

The second problem arises when outputs are reordered. Plugging in my display port adapter makes my external display the first display, which is different from what I want when using a beamer.

The third problem that I see is splitting just the external work screen into two virtual sub-screens. I understand that defining virtual_screens completely disables classical screen definitions, so I'm not sure how to combine these.

Using profiles seems like a much more intuitive fit for this problem, all of this would become fairly trivial. The correct profile is matched automatically, and each output can define the screens that it needs. The widescreen monitor can define two virtual screens. Although I know I'm biased. Therefore, I'd like to know how qtile currently intends to solve these problems. Maybe there is a better way that I cannot see right now.

I'm currently missing some insights into how qtile is supposed to be used in that case, so I really depend on your expert opinion.

EDIT 1: added paragraph about reordering screens

@m-col
Copy link
Member

m-col commented Jun 26, 2022

Ah ok I think I missed the point a bit here. While we can specify control more 'low-level' settings of outputs individually with things like kanshi, the point is that the primary output may change depending on connected outputs and importantly we want to configure our Screens differently for each case. Gotcha.

If that is the case, would this really be Wayland exclusive? Some aspects will be (scaling), but the problem as posed above also applies to x11, right?

@oddlama
Copy link
Contributor Author

oddlama commented Jun 26, 2022

No not wayland exclusive, I misjudged that initially. I previously thought it would be hard to do for X11, but I noticed that qtile already has compatible abstractions over outputs in both backends.

The only thing that would be wayland exclusive is that output scalings would be ignored on X11. Pretty sure it's impossible to do properly there.

@m-col
Copy link
Member

m-col commented Jun 26, 2022

OK cool, I think that will make things easier to implement. I think you've made a pretty solid case so I'd be happy to have a solution in qtile :)

Currently screens has a one-directional logic where outputs exist in the backend, and screens is mapped onto those in the order the backend reports them. screens does not have any control over how the outputs themselves are configured. This feature provides a mechanism for control in the opposite direction, so that some configurable can control the output configuration. This logic, and that provided by screens (whether it is still provided by screens or something else), need to work together to get the desired result. Is your draft syntax in the OP intended to work alongside screens such that you define both screens and profiles and then profiles configures outputs, and then screens is mapped onto those in the usual manner?

I don't know how feasible it is but if we can continue to just use screens to use both features that might keep the API quite simple. E.g. passing some kind of matching parameter to each Screen that tells qtile which output that item can apply to. I appreciate that your proposal above separates the concerns of the two directions of control, but from a user-facing API perspective a screen is a screen is a screen, so keeping them together there might be more intuitive.

@oddlama
Copy link
Contributor Author

oddlama commented Jun 26, 2022

OK cool, I think that will make things easier to implement. I think you've made a pretty solid case so I'd be happy to have a solution in qtile :)

Great ❤️, I'll begin working on it when we've ironed out the details.

Currently screens has a one-directional logic where outputs exist in the backend, and screens is mapped onto those in the order the backend reports them. screens does not have any control over how the outputs themselves are configured. This feature provides a mechanism for control in the opposite direction, so that some configurable can control the output configuration. This logic, and that provided by screens (whether it is still provided by screens or something else), need to work together to get the desired result. Is your draft syntax in the OP intended to work alongside screens such that you define both screens and profiles and then profiles configures outputs, and then screens is mapped onto those in the usual manner?

I would add a parameter to the outputs in the draft syntax to specify a screen for each output. This would default to Screen(). I'm playing with the thought that this will be a list of screens, which would allow to subdivide screens into several virtual screens simply by passing more than one screen. Although I'd like to allow a widescreen to become several virtual screens while keeping the bar attached to the physical screen. Not sure how to represent that.

profiles = [
    # widescreen 3440x1440 example
    Profile([
        Output(identifier="eDP-1", subscreens=[
            Screen(x=0, y=0, w=1720, h=1440), Screen(x=1720, y=0, w=1720, h=1440)])
        # TODO: how to handle dynamic screen sizes for e.g. beamers? allow specifying percentages? or allow specifying a lambda?
        Output(identifier="LVDS", enable=False) # default: subscreens=[Screen()]
    ]),
]

I was thinking of reusing the Screen mostly as-is, but to more specifically have it refer to virtual screens. By default each output has one virtual screen fully covering it.

I don't know how feasible it is but if we can continue to just use screens to use both features that might keep the API quite simple. E.g. passing some kind of matching parameter to each Screen that tells qtile which output that item can apply to. I appreciate that your proposal above separates the concerns of the two directions of control, but from a user-facing API perspective a screen is a screen is a screen, so keeping them together there might be more intuitive.

I'm honestly struggling to see how this would integrate well with the existing API. Can you provide a short example how you'd expect to use this? Maybe that clears things up for me.

If screens match outputs instead of the other way around, I currently imagine it being quite the hassle to implement output profiles, as it would make all the logic backwards. You'd effectively have to duplicate the profiles logic into every screen.

Imagine a Screen which only applies to the internal laptop output if:

  • Nothing else is connected
  • Or a specific beamer is connected
  • Or we are docked at work (2 specific other displays connected)

Plus a second screen for the opposite case. (Have an internal bar in those cases, and none otherwise). Now
add the constraint that the wallpaper should be different at work...

To keep backwards-compatibility, we could definitely have the current screens variable create an implicit output profile that matches all outputs in-order, and assigns screen based on the current logic.

PS:

Just to clarify how the backend works: What would happen currently, if two outputs (backend outputs, not screens) were assigned overlapping positions? e.g. two outputs of same size, both positioned at 0,0. Will this currently mirror the rendered content?

@clotodex
Copy link

clotodex commented Oct 7, 2022

Regarding configuration I would propose an alternative:
We already have python as a powerful configuration language, so why introduce a difficult set of matchings with profiles etc.
What do you think about having a callback function that is passed a hardware state (raw outputs, kanshi like) and returns a list/map of screens. This way one could write a dynamic function that can also take other variables into account (e.g. if keyboard is connected or not, etc) and return an appropriate configuration. This allows users to be as creative and complex as they want while keeping a default very easy. What do you think?

@m-col
Copy link
Member

m-col commented Jan 13, 2023

What do you think about having a callback function that is passed a hardware state (raw outputs, kanshi like) and returns a list/map of screens. This way one could write a dynamic function that can also take other variables into account (e.g. if keyboard is connected or not, etc) and return an appropriate configuration. This allows users to be as creative and complex as they want while keeping a default very easy. What do you think?

I think such a behaviour wouldn't be incompatible with the original proposal. We have the screen_change hook which itself is used to reconfigure screens upon changes to the output setup in the backend. One could hook into that to read information about the outputs and set their state. But I think the original proposal would be a bit more plug-and-play for most users who aren't quite as interested or comfortable doing more advanced configuration. So both options might be good :)

As for this feature @oddlama, I think it would be easier (for me and perhaps others who might like a say in how this would work out) if we had a working draft. There are many bugs and tasks (like the wlroots 0.16 update) that I've directed my focus towards, but I also don't want this to get too lost. I'm hesitant to ask you to implement something that we might change, but I think it would be easier than discussing ideas without the code to represent them, which we might then change anyway once we get the implementation together. So it's up to you, but perhaps you'd be open to submitted a draft PR to discuss it further? It might even be helpful for us to merge a working solution but keep it experimental and/or undocumented until we and others use it for a while to get it to maturity. Thoughts?

@oddlama
Copy link
Contributor Author

oddlama commented Jan 14, 2023

But I think the original proposal would be a bit more plug-and-play for most users who aren't quite as interested or comfortable doing more advanced configuration. So both options might be good :)

If done correctly, we could have optional support for advanced matching and configuration directly in the profiles. I'm sure we can get manual logic to work with the original syntax while keeping the default plug-and-play behavior.

So it's up to you, but perhaps you'd be open to submitted a draft PR to discuss it further?

Definitely! In the meantime I've teamed up with @clotodex so maybe we can get something working. But due to some other stuff that I have to do until March I can't give any guarantees about the timeline at this point. I certainly haven't forgotten about this.

It might even be helpful for us to merge a working solution but keep it experimental and/or undocumented until we and others use it for a while to get it to maturity. Thoughts?

Sure thing! While there are a lot of things that prevent a "proper" implementation from the beginning (mostly stuff dealing with effective resolution, screens and fake screens), I could certainly do the matching part first which shouldn't be too invasive while already providing a huge benefit. This would mean an eventual update after that might break configs for the initial draft, but I guess as long as it's tagged as experimental this wouldn't be a problem.

Publishing an experimental feature might also help us to get some external feedback before finalizing anything.

@github-actions
Copy link

This issue is stale because it has been open 90 days with no activity. Remove stale label or comment or this will be closed in 30 days.

@clotodex
Copy link

@m-col or @elParaguayo please mark as non-stale :) This depends a lot on #3985 and we will continue working on it then. The design was already discussed and we are waiting with the implementation.

@github-actions
Copy link

This issue is stale because it has been open 90 days with no activity. Remove the status: stale label or comment, or this will be closed in 30 days.

@clotodex
Copy link

Will check it out when I find time, now that wlroots16 is merged

Copy link

This issue is stale because it has been open 90 days with no activity. Remove the status: stale label or comment, or this will be closed in 30 days.

@clotodex
Copy link

With wlroots16 this is not as crucial to a lot of bugs anymore.
However this is still unsolved for some (@oddlama)
Further this could be a neat solution for "GPU-workspaces". Since right now afaik there is no way to run two displays with different GPUs (internal and dedicated). Which is sadly crucial now with hardwired HDMI port laptops.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
Status: Done
Development

No branches or pull requests

4 participants