-
Notifications
You must be signed in to change notification settings - Fork 1
3 Rendering a window
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:

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.