Wonders and Mysteries of the M1

I’m starting 2021 with a very nice laptop upgrade.

I made the mistake of using my 2018 MacBook Pro in the proximity of my 1½-year-old son, and amidst his toy-flinging the poor laptop’s screen got smashed. Only 80% of it works. Well, the thing was due for service anyway due its glitchy keyboard. But in a fortuitous turn of events, the new M1 MacBook Air that I ordered several weeks ago got delivered on the very same day.

It is a such a nice little computer. The M1 seems as powerful as people have been extolling and overall it feels great to use — running cool and fast. Having a physical function key row is a nice bonus. While I never hated the touch bar, it didn’t seem all that useful: what good is a small display in the keyboard that you never look at? Its value always seemed geared toward novice users who don’t know keyboard shortcuts.

Of course, getting Doomsday up and running was among the first things I did on it. I now have a native M1 build of Doomsday 3 that seems to work well enough. It took me three days after receiving the computer to resolve the immediate issues.

Debugger is buggered

Perhaps the most annoying issue is that the debugger is having real trouble running via IDEs. All is not lost, though, since I am able to run lldb from the command line and it appears to work fine, but both Xcode and Qt Creator are not having any luck launching it. LLDB terminates immediately after starting.

This may have something to do with the Big Sur’s more strict codesigning requirements for native arm64 code. It appears that all binaries, even those that were just built on the local machine, must be codesigned before they can be run. This happens mostly automatically with ad-hoc signatures, but unfortunately any changes to the binary after linking will invalidate the signature. Doomsday’s build scripts do a bunch of this kind of actions for various purposes. I’ll need to clean this up or apply some sort of re-signing as the last step.

The issue with Qt Creator is most likely some kind of conflict between it being x86_64 code running under Rosetta, while lldb is a native arm64 binary. I’ll likely need to switch IDEs for a while, but this doesn’t seem too bad since Xcode has evolved pretty nicely in recent years.

The case of the missing textures

There’s a bug report about many world textures being missing on the M1. I managed to fix it just now, but this one was a bit strange. I suspect that there is a bug in the GLSL compiler of the M1 OpenGL driver.

The problem was in a shader that does batched drawing of primitives. This makes the legacy graphics code run a bit faster, as it doesn’t have to issue a separate draw call for each used texture, but can bind multiple textures at once and use them all for a larger sequence of primitives. This is very useful for example when drawing the game menus, where each letter is stored in a separate OpenGL texture. (Using an atlas to reduce the number of textures would be an obvious improvement here, but it would require deep changes in the legacy code that is on its way out anyway.)

It seemed that only the first bound texture in the batch was successfully used. All the others would just come up as plain white. I noticed an error message in the log:

UNSUPPORTED (log once): POSSIBLE ISSUE: unit 0 GLD_TEXTURE_INDEX_2D is unloadable and bound to sampler type (Float) – using zero texture because texture unloadable

This certainly seemed to be somehow relevant to the issue. But texture unit 0 is the only one that did work, so what was that about? I spent a few hours poking around the texture binding code, trying different variations and making sure that everything was actually being bound appropriately. But as far as I could see, it was working as it should.

Next I took a closer look at the shader itself. The texture samplers were stored in an array, which is a bit of a special case in GLSL, particularly when it comes to OpenGL 3. All accesses to the sampler array must use compile-time fixed indices. I was using a switch statement to branch out uses of every possible index, and this has been working fine on various OpenGL drivers so far. The shader was responding correctly to if statements, though, so eventually I decided to replace the switch statements with a series of ifs. Functionally these are supposed to be equivalent, but to my surprise it fixed the problem!

Clipped world

One more issue remained in the M1 build. One half of the world was being clipped regardless of the current viewing direction. I immediately suspected a problem with integer-based angle calculations because sometimes there are CPU architecture specific differences in how signed/unsigned integers and floating-point values interact.

The culprit was indeed a cast from a floating-point angle to a 16-bit unsigned integer. On arm64, I have to first cast the floating-point angle to a signed integer, and then to 16-bit unsigned to make it work as expected.

Onwards

The M1 fixes will be heading to the 2.3.1 patch release.

I’m still working on getting the autobuilder up and running for 3.0. Progress has been made: I can now run the platform_release.py script on Windows, too, and it succeeds in creating a build. It looks like I need to change the Windows installer system, though, because I doubt WiX will be happy to run under MSYS. WiX is a bit too complicated anyway so a simpler installer wouldn’t hurt.