Developing a Graphical Game in UEFI Shell: Evaluating AI Coding Assistants

    This article uses a bare-metal "Breakout in UEFI Shell" experiment as a testbed to probe how far AI coding assistants can go when building a graphical application before any operating system has taken over the machine. It breaks down the key layers of low-level graphics development, including framebuffer rendering, keyboard input, Chinese font glyph rendering, physics and collision, memory and lifecycle management, and resource packaging, and then maps out where coding assistants tend to excel and where they commonly fail. It also offers practical guidance for using them more effectively and a standardized framework for comparing different assistants on measurable criteria.

    After the CPU powers on and before the operating system takes control of the machine, the firmware stage offers a demanding test for AI coding assistants: can they build a graphical Breakout game in this “bare-metal” environment, and how far can they actually get? Using a “Breakout in UEFI Shell” experiment as an example, this article breaks down the critical steps of low-level graphics development, outlines the capability boundaries and common mistakes of coding assistants, and suggests more scientific ways to use and evaluate them.

    1. Why Breakout Is a Good Testbed

    Breakout has long been a litmus test for programming ability: the rules are simple, yet it covers a surprisingly complete set of elements. To do it well under UEFI Shell, you have to solve several problems at once:

    • Graphics rendering: set the resolution and pixel format, and draw the ball, paddle, bricks, and on-screen text;
    • Human-computer interaction: read the arrow keys, confirm key, and exit key in real time;
    • Physics and collision: ball speed, bounce angle, paddle hit position, brick detection, and tunneling prevention;
    • Resource and data management: pack a Chinese font, allocate and reclaim memory, and persist data;
    • Robustness: run for a long time without crashing or screen corruption, and exit cleanly at any moment.

    For anyone who has only ever worked inside high-level language runtimes and mature graphics frameworks, most of these are provided “for free” by the system. Once you enter the firmware stage, though, every layer has to be built from scratch. That makes this kind of project ideal for testing whether a coding assistant can construct a low-level application from zero.

    2. Environment Anatomy: What UEFI Shell Actually Provides

    The boot flow defined by the UEFI (Unified Extensible Firmware Interface) specification is generally divided into three phases:

    1. DXE phase: each driver initializes hardware and establishes the global system table and boot services table;
    2. BDS phase: boot options are attempted one by one in boot order;
    3. After boot: once ExitBootServices is called, most boot services are no longer available.

    The UEFI Shell itself is just an ordinary UEFI application. The reason it can access disks and read or write files is that the corresponding file system and block device drivers were loaded during the boot phase. In a minimal firmware emulation environment, these drivers may not exist. For development and testing, placing the executable in the firmware’s built-in EFI partition, or mounting a virtual disk in an emulated environment, is often more stable than relying on a USB drive.

    (Throughout this article, programs with the .efi extension that are loaded directly by the firmware are referred to collectively as “EFI applications.”)

    For a graphical game running in the Shell, the available capabilities roughly break down as follows:

    Capability Primary firmware-side interface Difference from ordinary OS development
    Graphics output Graphics Output Protocol: mode query/set, pixel block transfer No window system; you operate the framebuffer directly
    Keyboard input Simple Text Input Protocol: wait for key events, read keys You handle event waiting, arrow-key scan codes, and key repeat yourself
    Memory management Boot service allocate/free interfaces No mature memory library beyond “free”; you manage lifecycles yourself
    Text display Simple Text Output Protocol Mostly an emulated terminal; fine for debug output, unsuitable for game visuals
    Character encoding Firmware widely uses UCS-2 (an implementation of UTF-16) Not UTF-8; Chinese text handling must be adapted

    At its core, graphics development in a UEFI Shell environment is “drawing pixels directly onto the framebuffer”: there is no window, no event-loop framework, no resource loader, and no standard input/output stream. These absences are the main source of difficulty, and they also make this an excellent scenario for testing a coding assistant’s understanding of the environment.

    3. Key Implementation Areas

    3.1 Graphics Initialization: From “Text Mode” to the “Pixel World”

    The UEFI Graphics Output Protocol (GOP) typically exposes the screen as a linear framebuffer: each frame is a contiguous block of pixel data in memory, with the color format decided by the firmware (commonly RGBX or BGRX, 4 bytes per pixel). There are two paths to updating the game screen:

    • Option 1: draw an entire frame in an off-screen buffer, then transfer it to video memory in one shot. The advantage is that it avoids screen tearing and keeps the logic simple;
    • Option 2: partial updates that transfer only the changed rectangular regions, suitable for simple scenes.

    Drawing a solid rectangle is essentially a loop:

    1. // Example: a linear framebuffer with 4 bytes per pixel and XRGB color format
    2. for (UINT32 row = 0; row < h; row++) {
    3. UINT32 *dst = base + (y + row) * stride + x;
    4. for (UINT32 col = 0; col < w; col++) {
    5. dst[col] = color; // write one 32-bit color value per pixel
    6. }
    7. }

    Writing pixels looks simple, but it hides some classic pitfalls of low-level development:

    • The screen width is not necessarily equal to the “number of pixels per row” (the stride). Many emulated environments allocate the framebuffer at a default resolution, so even when the actual resolution you set is smaller, you must still compute offsets based on the stride;
    • The pixel format varies by firmware. If you don’t split the color components according to the actual format, you’ll end up with red and blue swapped;
    • Some emulated environments support only a limited set of resolutions. When a mode set fails, you should fall back to the current mode and keep running, rather than exiting outright.

    These factors directly affect details such as how the ball’s bounce trajectory is rendered.

    3.2 Input Handling: From Blocking Waits to Non-Blocking Polling

    There are two common paths to reading keys:

    • Wait for key events in a blocking manner, so the thread suspends when there is no input;
    • Query key events in a non-blocking manner, driven periodically by a timer.

    For a real-time game, non-blocking polling with a fixed time step is usually the safer choice: the main loop advances the game state at fixed intervals and reads the key state once per cycle, which avoids stuttering and frame-rate-dependent speed. If the underlying layer only offers a blocking key-read, you can wrap it into a “queryable key state” that the upper layer polls uniformly.

    One thing to watch for in particular: arrow keys are “extended scan codes,” encoded differently from ordinary characters like space and enter, so the input logic must handle them separately.

    3.3 Chinese Fonts: Turning “Chinese Characters” into Pixels

    Chinese glyphs typically use a 16×16 bitmap, with each character occupying 32 bytes. If you write them in source code as a two-dimensional array, a 3,500-character common-character set would produce about 110,000 lines of code, far exceeding the single-pass output limit of most models. Two approaches are more common in practice:

    • Pack the glyphs into the application as binary resources, then look them up by code point at runtime;
    • Package only the Chinese characters actually used in the interface (a subset font) to compress the size further.

    Text rendering also has to deal with the width difference between half-width and full-width characters: one Chinese character is as wide as two English characters, which must be factored into layout and centering calculations. This logic is fiddly and heavily dependent on an understanding of character encoding, and it is one of the areas where a coding assistant is most likely to go wrong by making assumptions.

    3.4 Physics and Collision: Details Decide the Feel

    The core feel of Breakout is concentrated in the paddle’s bounce logic:

    • Uniform reflection feels too mechanical: when the ball bounces straight back off the paddle, it tends to get stuck bouncing in place. A more common approach changes the reflection angle based on where the ball hits the paddle, letting the player control the ball’s path by controlling the contact point;
    • Velocity components and maximum angle: the post-bounce velocity components are usually determined by the hit offset and the maximum bounce angle together, while the overall speed magnitude stays constant;
    • Tunneling prevention: at high ball speeds, the single-frame displacement may exceed a brick’s thickness, causing the ball to “pass through” the brick. Common remedies are to split each frame’s motion into several sub-steps, or to use swept collision detection.

    These physics details have no single standard, and the feel varies widely between implementations. Coding assistants often deliver a version that “runs but isn’t fun to play,” so developers have to tune parameters repeatedly or even redesign the mechanics.

    3.5 Memory and Lifecycle: No OS to Back You Up

    The firmware environment has no process isolation, and unlike a normal OS, it has no unified protection against invalid memory access. Every allocation must have a clear point at which it is freed: the paddle, ball, brick matrix, and font buffer should all be allocated once, managed centrally, and checked for allocation failure. Once problems like use-after-free or writing past the framebuffer bounds appear, the result is usually not a tidy error message but an outright reboot or screen corruption.

    3.6 Resource Packaging and the Verification Loop

    An EFI application must be compiled and linked into an executable image format the firmware recognizes, then placed in an EFI boot directory, or pointed to directly by a boot option. Verification typically involves: compile check → boot in an emulated environment → boot on real hardware or a virtual machine → observe during extended running.

    Compared with desktop or web development, verification of UEFI applications is far more costly: there are no mature graphical breakpoint debugging tools, and error information is limited. This makes “compile errors” the primary feedback signal, and it also makes a coding assistant’s performance more dependent on the quality of the environmental feedback it receives.

    4. How AI Coding Assistants Perform in UEFI Projects

    Drawing on practice with projects like this, coding assistants tend to be “strong on boilerplate, weak on environmental details, poor at physics tuning.”

    4.1 Clear Strengths

    • Fast scaffolding: structured code such as the boot entry point, protocol lookup, and the basic render loop usually comes together in one pass;
    • Familiarity with common protocol patterns: they handle general patterns like boot service table calls, event waiting, and device paths well;
    • Willingness to explain principles: they can walk through protocol structures and the meaning of parameters section by section, which genuinely helps developers understand the low level;
    • Good at diagnosing compile issues: when you paste back errors like missing headers or mismatched struct definitions verbatim, they can usually pinpoint the cause accurately.

    4.2 Frequent Mistakes

    • Environmental hallucinations: this is the most common problem. Some output looks professional but calls helper functions that don’t exist in the environment, or assumes the resolution, pixel format, and stride based on experience;
    • Inability to self-verify: without compile and run feedback, the generated code may “look reasonable” yet fail to run;
    • Making assumptions about Chinese text: they default to UTF-8, ignore the UCS-2 encoding widely used in firmware, and tend to oversimplify the font solution;
    • Weak physics tuning: parameter values are mostly “experience-based defaults,” and it’s hard to get a version that feels right in one shot;
    • Context forgetting: in long tasks they easily overlook previously agreed constraints, or give mutually conflicting instructions across turns.

    4.3 Six Practices That Make Coding Assistants More Reliable

    • Create a project context file: in the project root, write down the target environment, compiler, target architecture, available protocols, resolution and pixel-format constraints, build commands, and acceptance criteria, and require the assistant to follow it in every turn;
    • Drive iteration with real feedback: always attach the complete compile errors and run logs, instead of letting the assistant “guess from memory”;
    • Advance in layers: proceed in stages—“minimal runnable program → graphics rendering → input interaction → Chinese font → game logic → packaging and verification”—with a verifiable milestone at each stage;
    • Forbid speculation explicitly: require the assistant to produce a “to-be-verified” list when it is unsure, rather than inventing interfaces;
    • Hand the physics to numerical tools: parameters like bounce angles and speed curves can be simulated offline with a script, then tuned by feel;
    • Maintain a self-check list: before each delivery, ask it to verify memory release, out-of-bounds risks, error branches, and long-run stability.

    5. Evaluation Framework: How to Compare Different Coding Assistants Scientifically

    5.1 Fix the Experimental Variables

    • Use the same initial task description and context file;
    • Use the same toolchain, target environment (firmware emulator or virtual machine), and acceptance script;
    • Fix the maximum number of iteration rounds (for example, no more than N per task);
    • Have the same person perform acceptance against a unified standard.

    5.2 Quantitative Metrics

    Dimension How it is observed What it indicates
    First-round compile rate Proportion of first-round outputs that pass the compiler check Familiarity with the environment and interfaces
    Runs-without-intervention rate Proportion that run successfully directly in the acceptance environment Best reflects “out-of-the-box” capability
    Average iteration rounds Number of interactions needed to reach acceptance criteria Reflects correction efficiency
    Manual edit volume Proportion of lines changed by hand out of the total code Reflects how much work is actually saved
    Defect-type distribution Compile errors, runtime crashes, logic errors, Chinese display errors Helps locate weak areas
    Long-run stability Whether crashes or memory anomalies occur after 30+ minutes of continuous running Reflects low-level engineering quality
    Documentation and maintainability Comment completeness, clarity of layering Affects the cost of later extension

    5.3 Evaluation Principles

    • Blind evaluation: hide the model name before acceptance to avoid preconceptions;
    • Multi-run sampling: run the same task several times, and describe stability with the mean and variance rather than a single result;
    • Distinguish “compiles” from “runs”: these are the two criteria most easily confused;
    • Pay attention to the long tail: what truly separates the tools is often the hardest 10%.

    6. Model Selection: Balancing Capability and Cost

    In real use of AI coding assistants, “high-end models” and “lightweight options” each have their place:

    • High-end models: suited to high-value-density tasks such as requirements clarification, architecture design, protocol selection, and diagnosing difficult defects;
    • Lightweight models: suited to high-frequency, low-complexity tasks such as boilerplate generation, formatting, adding comments, and fixing simple compile errors;
    • Hybrid strategy: have a strong model handle design and key implementation first, then let a lightweight model take over day-to-day coding—usually the better overall cost.

    The deciding criterion shouldn’t be “which model is more expensive,” but “on the current task, who can deliver a verifiable result in the fewest iteration rounds.”

    7. Conclusion: The Tool Is an Amplifier, Not a Replacement

    The biggest takeaway from this experiment is that in low-level development, the value of a coding assistant lies not in “writing it all for you,” but in “helping you build, diagnose, and verify faster.” When environmental feedback is scarce and the cost of errors is high, human judgment, an understanding of low-level principles, and control over acceptance criteria become more important, not less.

    For teams that want to evaluate coding assistants, the recommendation is to use a “small but complete” project like this as a fixed testbed: it can expose differences across several dimensions—environmental understanding, error recovery, context retention, and documentation ability—within a day or two, and it is far more convincing than a vague “write me a feature.”

    Standardize the evaluation method, layer the tasks, and make the feedback real—that is the right way to make AI a true “engineering amplifier.”