Skip to content

3 Rendering a window

Lukas edited this page Mar 23, 2019 · 5 revisions

The commit that this article dissects is def9b1b.

The first thing we have to do in order to render windows is establish the compositor. The wl_compositor global is used by clients to allocate wlroots.Surfaces, to which they attach wl_buffers. These surfaces are just a generic mechanism for sharing buffers of pixels with compositors, and don't carry an implicit role, such as "application window" or "panel".

wlroots provides an implementation of wl_compositor. Let's set aside a reference for it:

 type Server struct {
     display wlroots.Display
     backend wlroots.Backend
     
     seat       wlroots.Seat
+    compositor wlroots.Compositor
     
     outputs []*Output
 }

Then rig it up:

 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)
     server.backend.Renderer().InitDisplay(server.display)
     
     // configure seat
     server.seat = wlroots.NewSeat(server.display, "seat0")
     
+    server.compositor = wlroots.NewCompositor(server.display, server.backend.Renderer())

If we run mcwayface now and check out the globals with weston-info, we'll see a wl_compositor and wl_subcompositor have appeared:

interface: 'wl_drm', version: 2, name: 3
interface: 'wl_seat', version: 6, name: 4
        name: seat0
        capabilities:
interface: 'wl_compositor', version: 4, name: 5
interface: 'wl_subcompositor', version: 1, name: 6
interface: 'wl_output', version: 3, name: 7
        x: 0, y: 0, scale: 1,
        physical_width: 0 mm, physical_height: 0 mm,
        make: 'wayland', model: 'wayland',
        subpixel_orientation: unknown, output_transform: normal,
        mode:
                width: 630 px, height: 1056 px, refresh: 0.000 Hz,
                flags: current

You get a wl_subcompositor for free with the wlroots wl_compositor implementation. Subcompositors will be discussed later. Speaking of things we'll discuss later, add this:

     // configure seat
     server.seat = wlroots.NewSeat(server.display, "seat0")
     
     server.compositor = wlroots.NewCompositor(server.display, server.backend.Renderer())
     
+    wlroots.NewXDGShell(server.display)

Remember that I said earlier that surfaces are just globs of pixels with no role? xdg_shell is something that can give surfaces a role. We'll talk about it more in the next article. After adding this, many clients will be able to connect to your compositor and spawn a window. However, without adding anything else, these windows will never be shown on-screen. You have to render them!

Something that distinguishes wlroots from libraries like wlc and libweston is that wlroots does not do any rendering for you. This gives you a lot of flexibility to render surfaces any way you like. The clients just gave you a pile of pixels, what you do with them is up to you - maybe you're making a desktop compositor, or maybe you want to draw them on an Android-style app switcher, or perhaps your compositor arranges windows in VR - all of this is possible with wlroots.

We keep track of surfaces using the wlroots.XDGShell instance we created earlier:

 type Server struct {
     display wlroots.Display
     backend wlroots.Backend
     
     seat       wlroots.Seat
     compositor wlroots.Compositor
+    xdgShell   wlroots.XDGShell
     
     outputs  []*Output
+    surfaces []wlroots.XDGSurface
 }

And in the main function:

     // configure seat
     server.seat = wlroots.NewSeat(server.display, "seat0")
     
     server.compositor = wlroots.NewCompositor(server.display, server.backend.Renderer())
     
~    server.xdgShell = wlroots.NewXDGShell(server.display)
+    server.xdgShell.OnNewSurface(server.handleNewSurface)

The server method handleNewSurface now gets called every time the XDGShell adds a new surface. In it, we just have to add the surface to our list (and specify a function to call if it gets destroyed):

+func (s *Server) handleNewSurface(surface wlroots.XDGSurface) {
+    surface.OnDestroy(s.handleSurfaceDestroy)
+    s.surfaces = append(s.surfaces, surface)
+}

Additionaly, if a surface gets destroyed, we'll wan't to remove it from our list:

+func (s *Server) handleSurfaceDestroy(surface wlroots.XDGSurface) {
+    for i, sb := range s.surfaces {
+        if surface == sb {
+            s.surfaces = append(s.surfaces[:i], s.surfaces[i+1:]...)
+        }
+    }
+}

Things are about to get complicated, so let's start with the easy part: in the Server.drawFrame method, we have to get a reference to every wlroots.Surface we want to render. So let's iterate over every surface our wlroots.Compositor is keeping track of:

     // 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],
     })
     
+    for _, surface := range s.surfaces {
+        // TODO Render
+    }
     
     output.SwapBuffers()
     renderer.End()
 }

wlroots might make you do the rendering yourself, but some tools are provided to help you write compositors with simple rendering requirements: wlroots.Renderer. We've already touched on this a little bit, but now we're going to use it for real. A little bit of OpenGL knowledge is required here. If you're a complete novice with OpenGL, I can recommend this tutorial to help you out. Since you're in a hurry, we'll do a quick crash course on the concepts necessary to utilize wlr_renderer. If you get lost, just skip to the next diff and treat it as magic incantations that make your windows appear.

We have a pile of pixels, and we want to put it on the screen. We can do this with a shader. If you're using wlr_renderer (and mcwayface will be), shaders are provided for you. To use our shaders, we feed them a texture (the pile of pixels) and a matrix. If we treat every pixel coordinate on our surface as a vector from (0, 0); top left, to (1, 1); bottom right, our goal is to produce a matrix that we can multiply a vector by to find the final coordinates on-screen for the pixel to be drawn to. We must project pixel coordinates from this 0-1 system to the coordinates of our desired rectangle on screen.

There's gotcha here, however: the coordinates on-screen also go from 0 to 1, instead of, for example, 0-1920 and 0-1080. To project coordinates like "put my 640x480 window at coordinates 100,100" to screen coordinates, we use an orthographic projection matrix. I know that sounds scary, but don't worry - wlroots does all of the work for you. Your wlroots.Output already has a suitable matrix called TransformMatrix, which incorporates into it the current resolution, scale factor, and rotation of your screen.

Okay, hopefully you're still with me. This sounds a bit complicated, but the manifestation of all of this nonsense is fairly straightforward. wlroots provides some tools to make it easy for you. First, we have to prepare a wlroots.Box that represents (in output coordinates) where we want the surface to show up.

     for _, surface := range s.surfaces {
-        //TODO Render
+        surf := surface.Surface()
+        state := surf.CurrentState()
+        
+        renderBox := wlroots.Box{
+            X:      20,
+            Y:      20,
+            Width:  state.Width(),
+            Height: state.Height(),
+        }
     }

Now, here's the great part: all of that fancy math I was just talking about can be done with a single helper method provided by wlroots: Matrix.ProjectBox.

     for _, surface := range s.surfaces {
     
         surf := surface.Surface()
         state := surf.CurrentState()
         
         renderBox := &wlroots.Box{
             X:      20,
             Y:      20,
             Width:  state.Width(),
             Height: state.Height(),
         }
+        
+        matrix := wlroots.Matrix{}
+        transformMatrix := output.TransformMatrix()
+        matrix.ProjectBox(renderBox, state.Transform(), 0, &transformMatrix)
     }

This a box you want to project, some other stuff that isn't important right now, and the projection you want to use - in this case, we just use the one provided by wlroots.Output.

The reason we make you understand and perform these steps is because it's entirely possible that you'll want to do them differently in the future. This is only the simplest case, but remember that wlroots is designed for every case. Now that we've obtained this matrix, we can finally render the surface:

       for _, surface := range s.surfaces {
       
           surf := surface.Surface()
           state := surf.CurrentState()
           
           renderBox := &wlroots.Box{
               X:      20,
               Y:      20,
               Width:  state.Width(),
               Height: state.Height(),
           }
           
           matrix := &wlroots.Matrix{}
           transformMatrix := output.TransformMatrix()
           matrix.ProjectBox(renderBox, state.Transform(), 0, &transformMatrix)
+          
+          renderer.RenderTextureWithMatrix(surf.Texture(), matrix, 1)
+          surf.SendFrameDone(time.Now())
       }

We also throw in a Surface.SendFrameDone for good measure, which lets the client know that we're done with it so they can send another frame. We're done! Run mcwayface now, then the following command:

$ WAYLAND_DISPLAY=wayland-1 termite -e htop

To see the following image:

screenshot

Run any other clients you like - many of them will work!

We used a bit of a hack today by simply rendering all of the surfaces the XDGShell informed us about. In practice, we're going to need to extend our XDGShell support (and add some other shells, too) to do this properly. We'll cover this in the next chapter.

Clone this wiki locally