Building a UEFI Shell Graphical Breakout Game From Scratch With AI: Workflow, Pitfalls, and Engineering Lessons
This article documents an experiment in developing a standalone graphical breakout game for the UEFI Shell from zero with AI assistance. It explains why UEFI application development is a demanding testbed for AI coding, how to set up the build scaffolding, how to use the Graphics Output Protocol, how to handle input and animation, and how to render Chinese text through a custom bitmap font. It also summarizes common failure modes, practical collaboration rules, and the engineering controls needed to make AI-generated low-level firmware UI code reliable.
1. Why a Bare-Metal Graphical Program Is a Good Test for AI Coding
There are two common views on whether AI-assisted programming can enter low-level development. One view is that hardware programming, register timing, and firmware protocol stacks lack public corpus material and executable feedback, so AI struggles. The other view is that local knowledge bases and engineering rule constraints can fill the gap. Both views have merit, but the real difference is not the corpus. It is whether the feedback loop is closed.
BIOS/UEFI development is special. There is no operating system, no standard library, no process isolation, and an out-of-bounds write can hang the machine directly. The feedback is also slow: after changing one line of code, you must complete the build, copy the medium, reboot, and enter the firmware shell before seeing the result. This makes it an ideal scenario for testing AI coding capability. It requires cross-file consistency, precise specification knowledge, and has no runtime safety net that can automatically “help it run.”
The rules of this experiment were deliberately strict:
- The Windows environment had only one commercial C/C++ compiler toolchain installed, with no additional helper tools.
- The local codebase was empty. No code was manually written in advance.
- No expert-level implementation guidance was provided. Only requirements and acceptance criteria were given.
The goal was to run a graphical brick-breaking game under the UEFI Shell and support Chinese-language display. In other words, the result had to be a standalone executable module that could be copied to a USB drive, inserted into a machine, and run directly from the firmware shell.
Breakout was not chosen randomly. It has a clear structure and complete logic, while covering five core capabilities: graphics rendering, real-time input, collision detection, state management, and resource packaging. Similar comparison tests had been done on web and mobile platforms before. The difficulty increased sharply in a firmware environment for one reason only: the entire supporting system is missing.
2. First Define the Capability Boundary: What the UEFI Environment Lacks
Without clarifying the boundary first, it is impossible to evaluate AI performance afterward. The following table compares capabilities that are readily available in a normal runtime with the reality of a UEFI application runtime:
| Readily available in a normal environment | Reality in a UEFI application runtime |
|---|---|
| Standard C library functions such as memory allocation, strings, and formatted output | Not available. Must use framework libraries such as BaseMemoryLib, MemoryAllocationLib, and PrintLib instead. |
| Operating system graphics APIs or window systems | Only the Graphics Output Protocol and text output protocol are available. |
| Keyboard event callbacks and an input subsystem | Can only wait for or read key events from simple text input and extended input protocols. |
| Millisecond sleep and multithreading | Only busy wait such as Stall or event timer callbacks. There is no preemptive scheduling. |
| Arbitrary file access | Requires the Simple File System protocol and depends on firmware having recognized and mounted the medium. |
| Direct Chinese character printing | Depends on the font built into the firmware. Most implementations only contain Latin glyphs. |
The conclusion is clear: every capability must be explicitly handled by locating a protocol, checking its version, and calling its functions. There is no implicit runtime. This also explains why AI can easily make mistakes in this environment, but can also be corrected easily, because each step has a specification text and compiler error to compare against.
3. The First Foundation: Make the Build System Run First
The first batch of AI-generated artifacts should be the project scaffolding, not the game logic. A minimal loadable module needs three files working together:
- A package declaration, DEC, exports GUIDs, PCDs, and library classes.
- A platform description, DSC, describes the target environment, library mappings, and build options.
- A module description, INF, describes the entry point, source files, and dependencies of a single module.
The INF file is the most error-prone part:
[Defines]INF_VERSION = 0x00010005BASE_NAME = BrickGameFILE_GUID = 6f1a1e3c-9a2b-4d7e-8c11-3f0a5b7d9e21 # Must be generated manually and be globally uniqueMODULE_TYPE = UEFI_APPLICATIONVERSION_STRING = 1.0ENTRY_POINT = BrickGameMain[Sources]BrickGame.cBrickGame.hFontBitmap16.h[Packages]MdePkg/MdePkg.dec[LibraryClasses]UefiApplicationEntryPointUefiLibUefiBootServicesTableLibUefiRuntimeServicesTableLibBaseMemoryLibMemoryAllocationLibPrintLibDebugLib[Protocols]gEfiGraphicsOutputProtocolGuidgEfiSimpleTextInputExProtocolGuid
Three high-frequency errors are worth recording separately. They almost always appear in the first version generated by AI:
Incorrect entry point usage. When using
UefiApplicationEntryPoint, the entry signature should be(EFI_HANDLE ImageHandle, EFI_SYSTEM_TABLE *SystemTable). If the standard entry is declared directly, or the signature does not matchEFI_MAIN, compilation may pass but loading will fail.Missing library classes. If
AllocatePoolis called butMemoryAllocationLibis not declared, orUnicodeSPrintis used withoutPrintLib, the linker reports unresolved symbols. The model often misjudges this as a code syntax problem and changes the implementation instead of fixing the dependency declaration.The module is not included in the platform description. If the INF file is correct but the module is not referenced in the Components section of the DSC file, the build may succeed but no product will be found.
The build system itself has more traps than the code. One early failure was caused by copying a reference framework directory in a “copy and paste” manner, which caused missing submodule content and confused incremental build state. The symptom was that the build script reported missing tools, while the real cause was an incomplete directory. The correct approach is to clone with recursive submodule fetching and preserve the original directory structure. Another failure occurred when the build script was executed from an editor’s integrated terminal. The compiler toolchain environment variables and prefix mappings were not loaded correctly, repeatedly causing “compiler not found” errors. Starting the build from an independent developer command line solved it in one attempt. On Windows, path length limits also need attention. Placing the project in a shallow directory can avoid many unexplained failures.
4. The Graphics Layer: Connecting to GOP Correctly
Graphics capability is the first real technical barrier of the project. The core process is locating the protocol, enumerating modes, setting a mode, and performing block transfers:
EFI_STATUS Status;UINT32 Pixel;UINTN Index, MaxMode;Status = gBS->LocateProtocol (&gEfiGraphicsOutputProtocolGuid, NULL, (VOID **)&mGop);if (EFI_ERROR (Status)) {return Status;}MaxMode = mGop->Mode->MaxMode;for (Index = 0; Index < MaxMode; Index++) {EFI_GRAPHICS_MODE_INFORMATION *Info;UINTN InfoSize;Status = mGop->QueryMode (mGop, Index, &InfoSize, &Info);if (EFI_ERROR (Status)) {continue;}if ((Info->HorizontalResolution >= 1024) && (Info->VerticalResolution >= 768)) {Status = mGop->SetMode (mGop, Index);FreePool (Info);break; // Note: Info allocated by QueryMode must be freed, otherwise repeated switching leaks memory}FreePool (Info);}
Drawing should always use block transfer. Filling color uses EfiBltFill:
Pixel = 0x00E0E0E0; // 0x00RRGGBBmGop->Blt (mGop, &Pixel, EfiBltFill, 0, 0, X, Y, Width, Height, 0);
Three key details are often missed in the first AI-generated version:
The pixel format is not unique. Most implementations use
PixelRedGreenBlueReserved8BitPerPixelorPixelBlueGreenRedReserved8BitPerPixel, where red and blue are reversed. There is also aPixelBitmaskform that requires conversion through masks. Hardcoding0x00RRGGBBcan cause red and blue to be swapped on some platforms.Do not write directly to the frame buffer. Some platforms report frame buffers as write-combining or unreadable. Direct writes can cause tearing, flickering, or even reading undefined data. The safe approach is to use block transfer, or to render a complete frame into a readable off-screen buffer and then move it in one transfer.
Stride is not equal to width. The scanline stride is given by
Mode->Info->PixelsPerScanLine. Calculating offsets by width can cause the entire image to be misaligned at some resolutions.
5. The Interaction Layer: Key Input, Timing, and the Main Loop
Input depends on the simple text input protocol. Arrow keys are scan codes and must be mapped separately:
STATICEFI_STATUSGameWaitKey (OUT EFI_INPUT_KEY *Key){UINTN Index;EFI_STATUS Status;Status = gBS->WaitForEvent (1, &gST->ConIn->WaitForKey, &Index);if (EFI_ERROR (Status)) {return Status;}return gST->ConIn->ReadKeyStroke (gST->ConIn, Key);}
The main loop needs to decouple event waiting from animation frame rate. Calling WaitForEvent directly blocks execution and leaves the ball frozen in mid-air. Pure busy-wait polling consumes the CPU and makes input feel unstable. A workable compromise is to use a fixed frame interval as the rhythm. Within each frame, read the input queue non-blockingly. If no key is available, yield the time slice through a short busy wait or a millisecond-level event timer. Paddle movement, ball integration, collision detection, score management, and life management can be placed in the same frame function. The state machine should include at least four states: title, playing, paused, and game over.
6. Chinese Display: From Question-Mark Boxes to Real Text
This is the part that best reflects low-level engineering constraints. When the firmware text output protocol renders a string, it uses the font built into the firmware. Most implementations only include Latin and symbol glyphs. Chinese characters are either displayed as boxes or discarded. This means Chinese display cannot rely on the shell’s printing capability. The module must carry its own glyph data.
The standard approach is to generate a bitmap font offline and compile it into the module. Take a set of commonly used characters and symbols, generate bitmaps at 16×16 or 12×12, export them as a C array header file, and at runtime use Unicode character - offset to look up the table and draw the bits point by point to the frame buffer. This is a task AI can fully handle. It is a typical “specification input, deterministic output” task. Given the font table and format convention, the accuracy of generated code is high, and errors can be located immediately through a screenshot.
The Chinese comments themselves failed to compile. After Chinese comments appeared in the source code, the build failed. The root cause was a mismatch between source encoding and the compiler’s default code page. Non-UTF-8 Chinese bytes were interpreted using the local code page, multi-byte sequences were broken, string literals were polluted, and warnings were treated as errors. There are two ways to handle this. One is to explicitly add UTF-8 source character set and execution character set options for the corresponding toolchain in the platform description. The other is to require all comments in source code to use ASCII. Places that truly need Chinese text should be placed in independent resource header files and carried as arrays, avoiding encoding ambiguity.
7. Iteration Rhythm: From “It Runs” to “It Is Playable”
Stage 1: Close the Loop
The goal is simple: the compiled product can be mounted by a USB drive, executed in the shell, clear the screen, and draw a rectangle in the center of the screen. The value of this stage is not the game itself, but verifying that the “build-copy-run-observe” chain works. It also helps AI become familiar with the form of error feedback.
Stage 2: Enhance Functionality
Add paddle movement, ball velocity vectors, brick matrices, collision bouncing, scoring, lives, and restart logic. AI is most efficient at this stage. The logic is clear, locally verifiable, and once requirements are described clearly, large amounts of output can be generated.
Stage 3: Attack Visual and Display Issues
Add color layers, borders, state transitions, and dirty rectangle refresh to eliminate flickering. Finally, complete self-drawn Chinese bitmap rendering. This stage requires more iterations for AI. Resolution differences, pixel format differences, scanline stride, and glyph baseline alignment only show up on real firmware.
8. Turning “Zero Guidance” Into an Executable Collaboration Method
Under strict rules, the real factors for success are not model capability, but specification quality and feedback quality. Six practical rules can be reused directly:
Write acceptance criteria before writing code. For example: “The title screen must appear within 5 seconds at a minimum resolution of 1024×768,” “Arrow key response delay must not exceed one frame,” and “After exit, return to the shell without memory leak warnings.” The more specific the acceptance criteria, the fewer rework rounds AI needs.
Deliver in rounds, with one closed loop per round. Do not generate a complete project at once. Each round should solve only one verifiable goal, preventing errors from spreading across multiple files and becoming impossible to locate.
Provide context proactively. Fix the key specification chapters, INF/DSC templates, and project conventions as rule files inside the repository. Without protocol definitions, the model may fabricate field names from memory, which is the most common source of errors in low-level scenarios.
Feed error messages back as they are. Do not merely say “compilation failed.” Provide the complete error output, virtual machine firmware logs, and serial debugging information to the model. Locating low-level problems almost entirely depends on the original error text.
Build a regression environment. Use virtual machines with public UEFI reference firmware for fast regression, avoiding physical machine reboots every time. This can compress the iteration cycle from minutes to seconds and brings the most significant benefit to AI collaboration.
Make builds reproducible. Fix toolchain versions and dependencies. Archive products, compilation logs, and screenshots together. If a cloud pipeline is used, object storage can store build artifacts and logs, container platforms can host the build environment, and log services can retain each compilation output, so that the input and output of each iteration are traceable.
9. Common Failure Modes
| Symptom | Common root cause | Handling direction |
|---|---|---|
| Module crashes immediately after loading or shows a black screen | Graphics mode was not set successfully, or an inaccessible frame buffer was written | Check the return value of SetMode and switch to block transfer |
| Colors have red and blue swapped | Wrong pixel format assumption | Detect format at runtime and convert through bit masks if necessary |
| Image is globally misaligned or skewed | Scanline stride was calculated by width | Use PixelsPerScanLine to calculate offsets |
| Keys have no response | Waiting on the wrong wait handle, or the event is not registered | Use the WaitForKey event from the input protocol |
| Chinese characters appear as boxes or question marks | The built-in firmware font lacks glyph data | Carry a custom bitmap font and render manually |
| Source code containing Chinese characters fails to compile | Source encoding and execution character set do not match | Explicitly specify character set options, or use ASCII comments |
| Build succeeds but no product is found | The module was not added to the platform description | Add the Components section |
| Behavior becomes abnormal after rapid mode switching | Query results were not freed during each mode enumeration | Add the missing FreePool calls |
10. Capability Boundaries and Risk Control
AI has clear advantages in low-level development: generating engineering scaffolding and build descriptions, producing protocol boilerplate code, interpreting error messages, making cross-file consistency changes such as synchronizing all call sites after an interface change, and completing documentation and comments. These are exactly the parts that are most time-consuming for humans and most prone to low-level mistakes.
At the same time, its limitations must be acknowledged. Hardware timing and electrical characteristics, behavior differences between platforms, undisclosed implementation details, and firmware version compatibility problems cannot be derived through code reasoning alone. They can only be converged through actual testing. Therefore, risk control should be placed on engineering constraints rather than model self-awareness. Submit and record change intent round by round. Manually review critical paths such as memory writes, protocol pointer usage, and mode switching. Lock dependency versions. Separate physical machine verification from virtual machine regression. Physical machines should only run versions that have passed regression.
11. Conclusion
Under this strict setup, going from zero to a runnable demo and then to a finished product with Chinese display required no manually written business code. The main effort went into requirement description, acceptance criteria formulation, and error feedback. The conclusion can be summarized in three points:
Feasibility is established, but only if AI is treated as an “executor” rather than a “designer.” Decisions such as interface selection, state machine structure, and resource format must be defined clearly by humans first.
Efficiency improvement mainly comes from process reconstruction, not from code generation speed. The real gains come from specification-driven requirements, structured error feedback, and automated validation environments. Compressing the feedback cycle of each iteration is what amplifies AI’s value.
Verification cost does not disappear. It only shifts. In low-level scenarios, there is no automatic safety net. Regression environments and human review must be built, otherwise the high generation speed of AI will directly turn into exploding debugging time.
BIOS programming can be roughly divided into creative development and platform porting. The former depends on design constraints, while the latter depends on specification details and existing implementation references. Both fit the division of labor where humans define specifications, AI implements, and machines verify. The reusable delivery checklist is: interface and acceptance criteria documents, build scaffolding templates, virtual machine regression scripts, error symptom comparison tables, and font/resource generation scripts. Once these five items are accumulated, the startup cost of the next bare-metal project will be significantly lower than the first one.
