[KongchangAI]
· 3 min read· 1,960 words

From ADB to the Quake Engine: What '90s Game Engines Teach Us About Extreme Optimization

From ADB to the Quake Engine: What '90s Game Engines Teach Us About Extreme Optimization

From Quake assembly optimization to the ADB rewrite, understanding the underlying machine matters more than trusting abstraction layers.

Based on Fabien Sanglard's talk on the Android Developers Backstage podcast, this article connects lessons from '90s game engine optimization with the refactoring of today's Android toolchain. It retraces the ADB Wi-Fi fix: the team hand-wrote MDNS in 2,000 lines of Rust, replacing ~900,000 lines of Open Screen + Abseil dependencies, boosting usage from 7% to 20%. Using Michael Abrash's assembly-level optimization of the Quake engine, it shows why compilers can't reproduce certain manual optimizations. It also exposes legacy issues inside ADB, including a one-byte out-of-bounds write caught by ASAN. Finally, it discusses the boundaries of LLMs in coding and debugging: great for "drywall code," but core components still need line-by-line understanding.

From ADB to the Quake Engine: What '90s Game Engines Teach Us About Extreme Optimization

A Thought Sparked by Lunch

On a recent episode of the Android Developers Backstage podcast, Fabien Sanglard, a core member of the Android Studio and ADB teams, shared a particularly memorable experience. At QuakeCon, he had lunch with legendary developers John Carmack, John Romero, Tom Hall, and Adrian Carmack, and the conversation turned to the assembly-level optimizations Michael Abrash had performed on the Quake engine—single-handedly doubling Quake's frame rate. Without those optimizations, Quake might never have shipped at all.

That conversation made Fabien reconsider the ADB refactoring work he was doing at the time. The extreme optimization mindset from 30 years ago and the engineering problems facing today's Android toolchain turn out to have striking similarities.

According to discussions among compiler experts on Hacker News, even today modern compilers still cannot reproduce the level of optimization Abrash achieved by hand back then. This raises a key question: as abstraction layers grow ever thicker, have we lost our understanding of the underlying machine?

The Long Road to Fixing ADB Wi-Fi

Why ADB Wi-Fi Used to Be a Terrible Experience

ADB Wi-Fi usage jumped from 7%-8% to 20%, and behind that jump was Fabien's team's thorough overhaul of the entire stack. The problems lay in every link of the MDNS (multicast DNS) chain: on the device side, the code used an Apple Bonjour reference implementation that hadn't been updated in over a decade, ran with root privileges, and leaked memory; on the workstation side, two different libraries were mixed together—the Bonjour ported from the phone side on Mac, and the Open Screen implementation from the Chromecast open-source project on Linux and Windows.

But Open Screen was designed for Chromecast: devices that are always on and always online, so it never implemented active network probing. The result: you close your laptop to go to a meeting, come back and reopen it, and ADB can't find your phone—because nobody sends a probe packet, and the phone doesn't proactively broadcast itself either.

every time network is involved

MDNS (multicast DNS) is a zero-configuration network protocol that allows devices to discover services on a local network without relying on a central DNS server, using the multicast address (224.0.0.251:5353 for IPv4). Each device can either actively send a query ("Who is _adb._tcp.local?") or proactively broadcast its own service information ("I am adb-xxx._adb._tcp.local, port 5555"). ADB Wi-Fi relies on this to let workstations automatically find phones. The problem is that MDNS discovery is event-driven: when the network topology changes (e.g., a laptop wakes up and rejoins), someone must send a new probe or response packet to refresh the cache. Open Screen was designed for the always-online, network-stable Chromecast scenario and omitted this active probing logic, so once the network state changes, the workstation can no longer find the device.

Replacing a Million Lines of Dependencies with 2,000 Lines of Code

The fix was stunning. On the device side, Fabien's team redirected MDNS registration to SD Manager (Java-based), which the Wi-Fi team had built in-house, completely eliminating the duplicate registration and memory leak issues. On the workstation side, Fabien made a bold decision: write an MDNS implementation directly in Rust by hand.

The contrast is stark—Open Screen plus its dependency Abseil totaled roughly 900,000 lines of code, while the new Rust implementation is only 2,000 lines. As Fabien explained on the podcast, because UDP multicast allows shared sockets, they could cover all platforms with a single unified implementation, no longer needing to maintain a separate one for Mac.

Here's a lesson worth pondering for developers: reusing code is a good thing, but you have to understand what you're reusing. Fabien compared library dependencies to a credit card—sometimes you're pressed for time and have to swipe it, but the key is to eventually pay it off. Either rewrite the small portion you actually need, or keep updating instead of forking.

What Compilers Can't Do: The Quake Lesson

Why Manual Optimization Still Matters

Back to Quake. The optimizations Abrash performed at the assembly level doubled the frame rate, and that's not something better hardware or a compiler could have solved. On the podcast, Fabien pointed out that modern compilers are constrained by the various semantic guarantees defined in language specifications and cannot make certain aggressive optimizations. But when you deeply understand your own application domain and know which semantic constraints don't apply in your scenario, you can make optimizations the compiler can't.

He observed a classic example in his own Kotlin Explorer tool: in Kotlin, a companion object used as a static member, but at the actual call site the VM must verify whether the object was properly initialized and whether it needs to trigger the initialization flow—these checks are unnecessary most of the time, but the compiler can't omit them because the language specification requires them.

The reason compilers cannot optimize due to language specification constraints comes down to the concept of "observable behavior." Take C/C++ for example: the standard mandates that certain operation orders, initialization timings, and memory visibility must be executed per the semantics, even when the programmer knows they could be omitted in a specific scenario. For example, in Kotlin/JVM, companion object initialization must satisfy class-loading-time thread safety and lazy semantics, so the VM must insert initialization-state checks on every access. The compiler cannot "cleverly" skip these checks because once skipped, the result could be observed to violate the specification in another piece of code. Abrash could manually skip these checks at the assembly level because he had the full runtime context of Quake and knew which constraints would never be triggered in this scenario. This kind of "domain-knowledge-driven boundary-pushing optimization" is something general-purpose compilers find fundamentally difficult to replicate.

Old Tech's New Life

Fabien mentioned an interesting example: shadow maps—a technique that renders the scene from another viewpoint and stores the result to generate shadows—appeared in '90s engines, but was too expensive to be practical at the time. Today, it's a standard feature of every modern game.

Another striking comparison: according to a piece of news mentioned on the podcast, the weather app on a popular operating system apparently consumes 600MB to 1.6GB of memory. As software grows ever more bloated, the careful resource-conscious programming techniques of the '90s seem all the more relevant.

to test some things

Inside ADB: From "Drywall" to "Load-Bearing Walls"

The Debt of Hardware Abstraction Layers

ADB's internals are full of legacy issues. Fabien found a piece of code where two threads communicated through a pipe, and after digging deeper discovered—it was actually the same thread talking to itself. He asked a colleague if it could simply be deleted, and the reply was "give it a try." Similar problems abound: ADB scans the entire USB device tree every second to detect device connections, whereas the standard approach should be to use Netlink to receive kernel event notifications.

Even more extreme is the USB driver on Windows: because the build system twenty years ago didn't support Windows, a developer compiled a 32-bit x86 static library on their own machine and dropped it directly into the source tree. That precompiled library is still being linked today, and the original source can no longer be rebuilt.

Fortunately, ADB can now use LLMs to help rewrite this USB driver. Fabien proposed an important conceptual distinction: "drywall code" versus "load-bearing wall code". One-off experiments or rapid prototypes are fine to generate with LLMs, just like drywall you can use freely; but the core components that support the system must be load-bearing walls, requiring line-by-line understanding.

Netlink is a bidirectional communication mechanism between user space and kernel space provided by the Linux kernel, widely used for event notifications such as network configuration, routing table changes, and device hotplug. Compared to ADB's current approach of polling the entire USB device tree every second, Netlink's NETLINK_KOBJECT_UEVENT socket can proactively push device attach/remove events detected by the kernel, with lower latency and near-zero CPU overhead. The problem with polling isn't just wasted resources—it also incurs considerable syscall overhead when there are many devices, and has a detection latency of up to one second. This kind of "polling instead of event notification" code is very common in legacy systems, often because the development environment at the time didn't support more modern IPC mechanisms, or because the developers didn't know about the interfaces the kernel provided.

The Cost of a Single Byte

After compiling ADB with ASAN (AddressSanitizer), Fabien found it crashed after just one minute of running—every time it received a packet from the network, there was a one-byte out-of-bounds write to the buffer. This kind of memory bug is extremely subtle in C/C++ projects, but under ASAN's detection it has nowhere to hide. After fixing it, the first thing he did was rewrite the MDNS-related modules.

ASAN (AddressSanitizer) is a compile-time instrumentation tool developed by Google that detects memory errors such as buffer overflows, use-after-free, and stack overflows at runtime. Its principle is to add "redzones" around each allocated memory region and maintain shadow memory to track which bytes are accessible. When the program reads or writes memory, the instrumented code first queries the shadow memory, and on an out-of-bounds access immediately reports an error and prints the full call stack. For C/C++ projects, ASAN's extra memory overhead is about 2-3x and performance overhead about 2x, but compared to the subtle bugs it can catch during testing, that cost is usually well worth it. The "one-byte out-of-bounds write on every packet received" mentioned in the article is exactly the kind ASAN excels at finding: the error itself may not crash immediately, but over long-running operation it quietly corrupts the heap layout, leading to random crashes that are hard to reproduce.

Debuggers and LLMs: A Match Not Yet Found

The Android debugger stack spans Android Studio (GDI), a device-side Agent (communicating with ART via JVMDI), and the ART runtime. Fabien's team is using JetBrains' 10,000+ debugger test cases to validate behavior on ART, pinpointing differences one by one and fixing them.

One typical bug is quite instructive: after a temporary object was created in an expression, ART's garbage collection was too aggressive and collected the object before it was used. Another issue was that ART used a differently implemented hash table, causing class creation order to be inconsistent with the JVM. These problems fundamentally stem from the debugger relying on undocumented JVM behavior.

Interestingly, Fabien admitted they hit a bottleneck when trying to combine LLMs with the debugger. LLMs always tend to read the source code directly—even if you deliberately give it only a JAR file, it will decompile it first and then get the source. So far they haven't found a scenario where an LLM plus a debugger is clearly better than an LLM just reading code. Still, Romain suggested a possible direction: when a bug lies in a third-party library you can't modify, or when you need to save tokens, the debugger's precise information retrieval capability could become an advantage.

too busy building ADB

Final Thoughts

Fabien is currently writing his third book—an analysis of the Quake engine. The first two delved into the engine implementations of Wolfenstein 3D and Doom respectively, but they went beyond the games themselves—covering low-level knowledge such as DOS, the 386/486 architecture, and VGA graphics. His chapter explaining how floating-point numbers work was called by Romain "the best explanation of floating-point I've ever read."

From Quake to ADB, engineering practice spanning thirty years points to the same truth: understand the machine your code runs on, rather than blindly trusting abstraction layers. Whether it's assembly-level optimization, MDNS protocol implementation, or USB driver rewriting, this principle always holds.

#ADB Wi-Fi#MDNS#Quake engine#Michael Abrash#Rust#ASAN#LLM code generation#Android toolchain
Share:

Related articles