swww icon indicating copy to clipboard operation
swww copied to clipboard

qoestion: how swww works?

Open sumit-modak opened this issue 6 months ago • 7 comments

I am trying to understand how swww works, but I am stuck with this module common::mmap. There is not much documentation, so please can you add a few. Thank you!

sumit-modak avatar Oct 10 '25 08:10 sumit-modak

Do you know what memory mapping is? More specifically, do you know how to share memory between two processes in UNIX, using shm and mmap? If not, then that code will be very hard to understand. I could document it better, for sure, but I believe you would be better off learning those concepts first. The code is actually not that complicated; in my opinion, the really nasty stuff in swww is in the serialization code.

LGFae avatar Oct 10 '25 13:10 LGFae

Hey, I just learned what mmap and shm is, but I am not very confident with my understanding. There might be a lot of missing parts. How did you learnt all these concepts? Can you link some resources?

ghost avatar Oct 11 '25 10:10 ghost

Unfortunately, I learned them largely by using them. In the beginning, I was using smithay as my wayland library, and it dealt with all these things for me. But, as I strived to use as little memory as possible, and making the daemon more efficient, I slowly abandoned libraries and wrote my own solutions to things. I do not know if there's any substitute to good old experience.


Here's a simple explanation:

Wayland requires us to share large buffers of memory for certain operations (for example, displaying a buffer on a surface). To do this, we share memory between the client and the server. Sharing memory requires we send a file descriptor through a Unix Socket. This file descriptor will refer to the shared memory buffer.

The shared memory file is created with either memfd_create, if we are on linux, or shm_open, if we are not. You can think of this as a "virtual file". It asks the operating system for a handle (a file descriptor), that will allow us to share memory between processes.

Now that we have the handle, we need to actually use it. First, we need to resize the file to our desired size using ftruncate. Then, finally, we can read its bytes with a mmap call. mmap will be using its MAP_SHARED flag, because we want our changes to be reflected across all processes that memory map the same shared memory file.

From here on out, we just do whatever we want with the underlying memory, and signal the wayland compositor when we are done. Because we are sharing memory, the changes are seen by all processes that are memory mapping the same file.

This should be enough for you to roughly grasps what's going on in common::mmap. Though, if you just learned these concepts, it might still not be that easy.

LGFae avatar Oct 11 '25 14:10 LGFae

You explained it well, but I have a few questions:

  1. Why do we need to resize the file with ftruncate. And how do we know what size we need because size may change over time.
  2. Correct me if I am wrong, memfd_create is used to create a buffer and shm_open is used to open that buffer when we don't have the OwnedFd?
  3. rustix::net::socket_with is used to create a socket that is used for ipc and the OwnedFd created by memfd_create is used for wl_buffers?

ghost avatar Oct 12 '25 04:10 ghost

Why do we need to resize the file with ftruncate. And how do we know what size we need because size may change over time.

When we create the shared memory file, it has size 0. A 'normal' file will resize itself when you call the write syscall with its file descriptor, but that would be inefficient here, since we do not wish to write anything to it; we just want it to have a certain size so we can mmap it all at once. ftruncate is how you can "resize" a file without calling write on it (note ftruncate could also shrink a file, but we do not do that here).

EDIT: regarding the size, the caller needs to know what size it needs. Note that in the Mmap::new function we demand a len parameter, and in the Mmap::remap function as well. We always truncate the file to whatever was asked, and so memory mapping that length is always safe. But if the caller does something weird, like mapping less memory than it needs, it's going to panic eventually with an out-of-bounds access.

Note that the wayland protocol disallows reducing the shared memory file's size. It can only increase. So the caller in the daemon only needs to worry about never decreasing the size. Look at the function daemon::wayland::BumpPool::grow. You will see that we panic if we try calling Mmap::remap with a smaller length than it already had.

Note we also use memory maps to share data between client and daemon. For that, we call remap to increase the size as needed. Look at common::ipc::ImageRequestBuilder::{extend,grow}.

Correct me if I am wrong, memfd_create is used to create a buffer and shm_open is used to open that buffer when we don't have the OwnedFd

No, you can think that memfd_create and shm_open do the same thing: they create the shared memory file and return its file descriptor. The main difference is memfd_create accepts anything as its name, while shm_open demands the file name to be unique. memfd_create is a linux-exclusive syscall.

This is why, if you look at the code, we only call memfd_create when we are on a linux machine, and when go for shm_open, we keep trying again and again with a different timestamp, because the file may already exist.

I believe because memfd_create is a linux exclusive, dedicated syscall for this operation, it could also have a more efficient kernel implementation, but that's just pure speculation on my part.

rustix::net::socket_with is used to create a socket that is used for ipc and the OwnedFd created by memfd_create is used for wl_buffers?

Yes. This is correct. After we create the shared memory file, we must send it to the process we want to share memory with through a UnixSocket.

LGFae avatar Oct 12 '25 17:10 LGFae

All good, I see that you have used waybackend but I don't have a good grasp of wayland yet. Can you say what you did here? Also, I found out that swww takes 3.2 MB RAM, that's impressive. Is waybackend behind all that?

ghost avatar Oct 13 '25 15:10 ghost

I see that you have used waybackend but I don't have a good grasp of wayland yet. You say what you did here?

waybackend is my own implementation of the wayland protocol, client-side. I originally designed it to be used specifically in swww, but have since also been using it in a few other personal projects (I have made a status bar and a basic launcher, you can find them here).

waybackend emphasizes low-level operations. We forego most of the things usual wayland implementations use to make their libraries more ergonomic. Most importantly, the wayland protocol request calls themselves are not type-checked, so you can do something nonsensical like ask for a keyboard handle from a pointer. This means you need to write the code while constantly consulting the wayland protocol documentation. Although, in my experience, once you get used to it it's not that bad, since the requests are all nicely divided into modules.

In any case, keep in mind that waybackend is significantly less battle-tested than either wayrs or smithay, potentially worse documented, and also potentially a bit of a pain to use.

Also, I found out that swww takes 3.2 MB RAM, that's impressive. Is waybackend behind all that?

And that's the payoff. To be clear, it's not just waybackend. After we are done displaying static images, we unmap their buffer memory. Many other wallpaper setters actually keep the file's contents in memory, either because it's easier or because that way they can easily resize it. We just keep the file's path instead, and when the output size change, we fire the client, who's then responsible for sending in a new image in the appropriate size.

That said, waybackend does play a big role here. smithay has had a few releases since I last used it, but it's API used to demand almost everything to be wrapped in an Arc, and used a lot of dynamic dispatch. Dynamic dispatch is a problem because it can prevent function inlining, and there are quite a few functions in the wayland protocol code that are very, very simple, meaning they would greatly benefit from inlining. Both smithay and wayrs seem to have been inspired by the canonical C libwayland implementation, and, from what I can tell, do not fully explore what a Rust implementation of the protocol could look like.

By that, I mean above all else the existence of callbacks. Because we have generics in Rust, we do not need to register callbacks. We do not need to store a pointer to a static str, and two full u32s for a single object, like wayrs does. That's 24 bytes per object (remember the &str is a fat pointer with an address and a length) while waybackend stores just 1: an enum variant.

This is because our backend is generic over an enum, that represents every wayland protocol we are going to be dealing with in the current application. swww's enum is declared here. You can find more examples in the repository I linked earlier with a status bar and a basic menu launcher. Because almost no application will ever have to deal with more than 255 interfaces, our ObjectManager can store just one byte per object most of the time (at worse it would store 2, still a big improvement over 24), and use its index to figure out what its wayland id number is. The implementation is here (note: we do allocation manually, so there's some unsafe code in there).

By doing it like this, dispatching the correct handler is literally just a match on an enum. Indeed, that's what our helper macro in waybackend does for you. No need for compiler-enforced dynamic dispatch, no need for complicated runtime schemes, no need for Arcs, just very straightforward in general.

Back when I was still using smithay, I had to write a bunch of "no-op" functions, because the smithay-client-toolkit didn't know which specific protocols my application was going to deal with. Once again, it's been a long time, so maybe they have improved this. Honestly, it could also be that I was just being stupid, as I was a much worse dev back then. In any case, that's not necessary here because we declare up-front all the protocols we will be interacting with.

As a result of all of this, the necessary data we need to keep track of is greatly diminished, and so waybackend can run with a quite low memory footprint. Right now, it does have the disadvantage that it will always ask for latest supported version of any protocol, though changing that would not be difficult, I just haven't had a use-case for it yet.


Honestly, I am obviously very biased, but I find waybackend actually easier to work with than smithay and wayrs, because it is much more predictable. There's very little magic behind it, and it gives me all the control I want.


I wrote this a little fast, sorry if it's a little confusing. I can clear up anything that ended up being difficult to understand.

LGFae avatar Oct 13 '25 17:10 LGFae