Summer of Pinephone: Week 2

I received my Pinephone near the end of last week, so this week was entirely spent getting oasis up and running on it.

Rather than building a system image from scratch — I should probably write a script to do this in the long term — I started with a postmarketOS image. I replaced the rootfs with an oasis rootfs, built a static kernel based on megi's kernel (essentially using sed "s/=m/=y/" on the config), and modified the postmarketOS u-boot script to suit this new setup. To aid in debugging my broken u-boot script, I used the Pinephone's serial interface which can be accessed by toggling the 6th DIP switch, and connecting to the headphone jack. Unfortunately I had to sacrifice an audio cable and do some sketchy wiring using alligator clips, but after compiling USB serial support into my laptop's kernel, I had a functioning if fragile serial connection to the phone. After spending some time screwing around with the u-boot script, I had oasis booting.

At this point, I would be able to get wifi and an ssh connection going, so I could ditch the serial interface. oasis uses perp for managing services, so enabling ssh was as simple as toggling the sticky bit on. For wifi, I had to write my own services for perp, but this was pretty straightforward. The only challenge was learning some idiosyncracies of the rc shell's syntax, which all of the oasis services are written in. I could have used ksh as the shell language, but I figure if I'm using oasis, I might as well do it the oasis way.

Given that I intend for my phone interface to be graphical, a natural next step was getting a graphical environment going, in this case velox. Disappointingly, it did not work out of the box. It wasn't printing error messages either, so I resorted to annotating it with printfs (something that is only practical because of oasis's build system), and found that the issue was with swc's DRM initialization. Specifically, the following in drm_initialize caused it to fail:

if (drmSetClientCap(swc.drm->fd, DRM_CLIENT_CAP_UNIVERSAL_PLANES, 1) < 0) {
	ERROR("Could not enable DRM universal planes\n");
	goto error1;
}

This error occured when it used card0 as the primary card, but I noticed that there were two cards in /sys/class/drm. So I tried hardcoding it to use card1. It made it a little bit farther, but failed on the following lines:

drm.path = drmGetRenderDeviceNameFromFd(swc.drm->fd);
if (!drm.path) {
	ERROR("Could not determine render node path\n");
	goto error1;
}

After several hours of alternating between reading about DRM, banging my head against a wall and comtemplating calling it a day (having done nothing), I found the information that I needed to make sense of this. On desktop GPUs, it is the graphics card that does both hardware accelerated rendering and display output (scan out). This explains why on a desktop you plug monitors directly into the graphics card, rather than the motherboard. On many ARM devices it seems, it is different. The tasks of hardware accelerated rendering and scan out can be split into different devices. On the Pinephone, the Mali 400 GPU does the rendering, while the Display Engine on the A64 chip does scan out. So card0 is the GPU, and card1 is the Display Engine. This explains why card0 doesn't have the universal planes capability, and card1 doesn't have an associated render node. I also assume that this wasn't taken into account when swc was built, because it wasn't developed for this environment. To fix this, I added a check that ensures that the primary device used by swc has the universal planes capability. I also made it just use a device with the path /dev/dri/renderD* as the render node, the way the article above recommends.

With this fixed, velox seemed to work. After enabling hotplugd, I could use the keyboard to spawn terminal windows.

The last thing that I worked on this week was touch support in swc. I implemented basic touch support based on the existing pointer and keyboard input devices. Initially, libinput was not sending touch events to swc and I couldn't figure out why. I enabled debug output for libinput, and found that it wasn't detecting the touchscreen as a supported device. It turns out udev handles detecting supported devices under normal circumstances, but oasis doesn't use udev. So libinput was modified to detect devices based on the events that a device supports. Here is the relevant code with my humble addition:

if (major(st.st_rdev) == INPUT_MAJOR)
	tags |= EVDEV_UDEV_TAG_INPUT;
if (libevdev_has_event_code(evdev, EV_KEY, KEY_ENTER))
	tags |= EVDEV_UDEV_TAG_KEYBOARD;
if (libevdev_has_event_code(evdev, EV_REL, REL_X) &&
    libevdev_has_event_code(evdev, EV_REL, REL_Y) &&
    libevdev_has_event_code(evdev, EV_KEY, BTN_MOUSE))
	tags |= EVDEV_UDEV_TAG_MOUSE;
if (libevdev_has_event_code(evdev, EV_ABS, ABS_X) &&
    libevdev_has_event_code(evdev, EV_ABS, ABS_Y)) {
	if (libevdev_has_event_code(evdev, EV_KEY, BTN_TOOL_FINGER) &&
	    !libevdev_has_event_code(evdev, EV_KEY, BTN_TOOL_PEN)) {
		tags |= EVDEV_UDEV_TAG_TOUCHPAD;
	} else if (libevdev_has_event_code(evdev, EV_KEY, BTN_MOUSE)) {
		tags |= EVDEV_UDEV_TAG_MOUSE;
	} else {
		/* TODO account for tablets */
		tags |= EVDEV_UDEV_TAG_TOUCHSCREEN;
	}
}

Again, because oasis wasn't designed for phones, the touchscreen wasn't supported out of the box. That last else clause was my addition, and it seems to work properly, as swc now received touch events. Using the new touch support, I added basic window switching, but I still haven't implemented forwarding touch events to clients. That's for this week. Here's a demonstration of touch-based window switching:

Finally, I want to touch on my workflow. For most of the week, I have been pulling the battery and SD card out of the phone, inserting it in a micro-to-SD converter, inserting it into my laptop, mounting it, and doing a git pull from the repo containing the rootfs. Then I would perform the same sequence but backward to update the phone. Even typing it is painful. A few days ago, I set it up so that I can update without the SD card being removed from the phone at all. To do this, I use git daemon to run a git server on my laptop, and added this git server as a remote on the phone's root repository. Then all it takes is

# cd /
# git pull laptop pinephone-root

and I can reboot into the updated system without worrying about touching the SD card. There is no other Linux system — as far as I'm aware — where this is so easy. It makes development way easier.