-
Notifications
You must be signed in to change notification settings - Fork 1
1 Hello wlroots
During this series of articles, the compositor we're building will live on GitHub: Wayland McWayface. Each article in this series will be presented as a breakdown of a single commit between zero and a fully functional Wayland compositor. The commit for this article is 2dcd101. I'm only going to explain the important parts - I suggest you review the entire commit separately.
Let's get started. First, I'm going to define a struct for holding our compositor's state:
+type Server struct {
+ display wlroots.Display
+}Note: mcw is short for McWayface. We'll be using this acronym throughout the article series. We'll set one of these aside and initialize the Wayland display for it:
func main() {
+ server := new(Server)
+ server.display = wlroots.NewDisplay()
}The Wayland display gives us a number of things, but for now all we care about is the event loop. This event loop is deeply integrated into wlroots, and is used for things like dispatching signals across the application, being notified when data is available on various file descriptors, and so on.
Next, we need to create the backend:
type Server struct {
display wlroots.Display
+ backend wlroots.Backend
}The Backend is our first wlroots concept. The backend is responsible for abstracting the low level input and output implementations from you. Each backend can generate zero or more input devices (such as mice, keyboards, etc) and zero or more output devices (such as monitors on your desk). Backends have nothing to do with Wayland - their purpose is to help you with the other APIs you need to use as a Wayland compositor. There are various backends with various purposes:
- The drm backend utilizes the Linux DRM subsystem to render directly to your physical displays.
- The libinput backend utilizes libinput to enumerate and control physical input devices.
- The wayland backend creates "outputs" as windows on another running Wayland compositors, allowing you to nest compositors. Useful for debugging.
- The x11 backend is similar to the Wayland backend, but opens an x11 window on an x11 server rather than a Wayland window on a Wayland server.
Another important backend is the multi backend, which allows you to initialize several backends at once and aggregate their input and output devices. This is necessary, for example, to utilize both drm and libinput simultaneously.
There is a function to automatically create an appropriate backend based on the user environment:
func main() {
server := new(Server)
server.display = wlroots.NewDisplay()
+ server.backend = wlroots.NewBackend(server.display)We can now start the backend and enter the Wayland event loop:
+ // start the backend
+ err := server.backend.Start()
+ if err != nil {
+ panic(err)
+ }
+
+ // and run the display
+ server.display.Run()
+}If you run your compositor at this point, you should see the backend start up and... do nothing. It'll open a window if you run from a running Wayland or X11 server. If you run it on DRM, it'll probably do very little and you won't even be able to switch to another TTY to kill it.
In order to render something, we need to know about the outputs we can render on. The backend provides a wl_signal that notifies us when it gets a new output. This will happen on startup and as any outputs are hotplugged at runtime.
In go, we can react to that signal by adding a function that gets called if a new output gets added:
func main() {
server := new(Server)
server.outputs = make([]*Output, 0)
server.display = wlroots.NewDisplay()
server.backend = wlroots.NewBackend(server.display)
+ server.backend.OnNewOutput(server.newOuput)And this is the server method that gets called:
+func (s *Server) newOuput(output wlroots.Output) {
+ out := &Output{
+ wlrOutput: output,
+ lastFrame: time.Now(),
+ color: [4]float32{1, 0, 0, 1},
+ }
+
+ // set the output mode to the last mode (only if there are modes)
+ // The last mode is usually the largest at the highest refresh rate
+ modes := out.wlrOutput.Modes()
+ if len(modes) > 0 {
+ out.wlrOutput.SetMode(modes[len(modes)-1])
+ }
+
+ s.outputs = append(s.outputs, out)
+}This function adds the output to a list of outputs in the server. We save the wlroots.Output in a struct together with the color of the output and the time the last frame was drawn. The OutputMode describes the resolution and the frame rate of the output. Modes are not supported by all backends, but DRM needs them. The backends that don't support modes (for example the X11 and Wayland backends) ignore the set mode.
type Server struct {
display wlroots.Display
backend wlroots.Backend
+ outputs []*Output
}
+type Output struct {
+ wlrOutput wlroots.Output
+ lastFrame time.Time
+
+ color [4]float32
+}This will be the structure we use to store any state we have for this output that is specific to our compositor's needs.
We could use this now, but it would leak memory. We also need to handle output removal, with a signal provided by wlr_output. In go, we react to an output removal with a function:
func (s *Server) newOuput(output wlroots.Output) {
out := &Output{
wlrOutput: output,
lastFrame: time.Now(),
color: [4]float32{1, 0, 0, 1},
}
+ out.wlrOutput.OnDestroy(s.destroyOutput)And this is the function we specified:
+func (s *Server) destroyOutput(output wlroots.Output) {
+ for i, out := range s.outputs {
+ if out.wlrOutput.Name() == output.Name() {
+ // delete the output from the list
+ s.outputs = append(s.outputs[:i], s.outputs[i+1:]...)
+ break
+ }
+ }
+}This one should be pretty self-explanatory.
So, we now have a reference to the output. However, we are still not rendering anything - if you run the compositor again you'll notice the same behavior. In order to render things, we have to listen for the frame signal. Depending on the selected mode, the output can only receive new frames at a certain rate. We keep track of this for you in wlroots, and emit the frame signal when it's time to draw a new frame.
Of course we can react to this frame signal. Here's how to specify a function that gets called:
func (s *Server) newOuput(output wlroots.Output) {
out := &Output{
wlrOutput: output,
lastFrame: time.Now(),
color: [4]float32{1, 0, 0, 1},
}
out.wlrOutput.OnDestroy(s.destroyOutput)
+ out.wlrOutput.OnFrame(s.drawFrame)...and here's the server method specified above:
+func (s *Server) drawFrame(output wlroots.Output) {
+ // search for our version of the output
+ var mcwOut *Output
+ for _, out := range s.outputs {
+ if out.wlrOutput.Name() == output.Name() {
+ mcwOut = out
+ break
+ }
+ }
+ // check if we haven't found it
+ if mcwOut == nil {
+ return
+ }In order to render anything here, we need to first obtain a Renderer. We can obtain one from the backend:
func (s *Server) drawFrame(output wlroots.Output) {
+ renderer := s.backend.Renderer()
// search for our version of the output
var mcwOut *Output
for _, out := range s.outputs {
if out.wlrOutput.Name() == output.Name() {We can now take advantage of this renderer to draw something on the output.
+ width, height := output.EffectiveResolution()
+
+ // try to make the current output the current OpenGL context
+ _, err := output.MakeCurrent()
+ if err != nil {
+ panic("Could not change OpenGL context!")
+ }
+
+ renderer.Begin(output, width, height)
+ renderer.Clear(&wlroots.Color{
+ A: mcwOut.color[3],
+ R: mcwOut.color[0],
+ B: mcwOut.color[1],
+ G: mcwOut.color[2],
+ })
+
+ output.SwapBuffers()
+ renderer.End()
}Calling output.MakeCurrent will set the current OpenGL context to the
current output so we can use OpenGL calls to modify it. After, we call
renderer.Begin to configure some sane OpenGL defaults for us.
At this point we can start rendering. We'll expand more on what you can
do with wlroots.Renderer later, but for now we'll be satisified with
setting the output to a specific color.
When we're done rendering, we call output.SwapBuffers to swap the
output's front and back buffers, committing what we've rendered to the
actual screen. We call renderer.End to clean up the OpenGL context.
Running our compositor now should show you a solid red screen.
The output will currently have the color red because we specified that color when initializing that output. Because this is boring and because we don't currently see if the output gets updated, lets make the color change over time:
// check if we haven't found it
if mcwOut == nil {
return
}
+ // calculate a color based on the time difference from the last frame
+ now := time.Now()
+ delta := now.Sub(mcwOut.lastFrame)
+ mcwOut.lastFrame = now
+ deltaS := float32(delta.Seconds())
+
+ for i := 0; i < 3; i++ {
+ // get the index of the next color
+ next := i + 1
+ if next == 3 {
+ next = 0
+ }
+
+ // if the next color is 0, increase
+ if mcwOut.color[next] == 0 {
+ mcwOut.color[i] += deltaS
+ if mcwOut.color[i] >= 1 {
+ mcwOut.color[next] = deltaS
+ mcwOut.color[i] = 1
+ }
+ } else { // decrease
+ mcwOut.color[i] -= deltaS
+ if mcwOut.color[i] <= 0 {
+ mcwOut.color[i] = 0
+ }
+ }
+ }
+ // end fancy color generationThis color changing code may be a bit hacky, but it should work. So running the compositor now, we should see the output changing color over time.