This commit is contained in:
m00d 2025-10-01 08:11:05 +02:00
commit cc9f6b58e1
21 changed files with 1538 additions and 0 deletions

1
.cache/uefi-rs Submodule

@ -0,0 +1 @@
Subproject commit 565b1aad2b6a79a11c67e419879475ac7414f6fe

9
.cargo/config.toml Normal file
View file

@ -0,0 +1,9 @@
[build]
target = "aarch64-unknown-uefi"
[target.aarch64-unknown-uefi]
rustflags = [
"-C", "relocation-model=pic",
"-C", "link-args=/BASE:0x100000",
]
linker = "rust-lld"

21
.gitignore vendored Normal file
View file

@ -0,0 +1,21 @@
# Generated by Cargo
# will have compiled files and executables
debug
target
# These are backup files generated by rustfmt
**/*.rs.bk
# MSVC Windows builds of rustc generate these, which store debugging information
*.pdb
# Generated by cargo mutants
# Contains mutation testing data
**/mutants.out*/
# RustRover
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/

228
Cargo.lock generated Normal file
View file

@ -0,0 +1,228 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "badapple-uefi"
version = "0.1.0"
dependencies = [
"bitvec",
"log",
"micromath",
"spin",
"uefi",
"x86_64",
]
[[package]]
name = "bit_field"
version = "0.10.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e4b40c7323adcfc0a41c4b88143ed58346ff65a288fc144329c5c45e05d70c6"
[[package]]
name = "bitflags"
version = "2.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2261d10cca569e4643e526d8dc2e62e433cc8aba21ab764233731f8d369bf394"
[[package]]
name = "bitvec"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c"
dependencies = [
"funty",
"radium",
"tap",
"wyz",
]
[[package]]
name = "cfg-if"
version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2fd1289c04a9ea8cb22300a459a72a385d7c73d3259e2ed7dcb2af674838cfa9"
[[package]]
name = "funty"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c"
[[package]]
name = "log"
version = "0.4.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432"
[[package]]
name = "micromath"
version = "2.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c3c8dda44ff03a2f238717214da50f65d5a53b45cd213a7370424ffdb6fae815"
[[package]]
name = "proc-macro2"
version = "1.0.101"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de"
dependencies = [
"unicode-ident",
]
[[package]]
name = "ptr_meta"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b9a0cf95a1196af61d4f1cbdab967179516d9a4a4312af1f31948f8f6224a79"
dependencies = [
"ptr_meta_derive",
]
[[package]]
name = "ptr_meta_derive"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7347867d0a7e1208d93b46767be83e2b8f978c3dad35f775ac8d8847551d6fe1"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "qemu-exit"
version = "3.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8bb0fd6580eeed0103c054e3fba2c2618ff476943762f28a645b63b8692b21c9"
[[package]]
name = "quote"
version = "1.0.41"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ce25767e7b499d1b604768e7cde645d14cc8584231ea6b295e9c9eb22c02e1d1"
dependencies = [
"proc-macro2",
]
[[package]]
name = "radium"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09"
[[package]]
name = "rustversion"
version = "1.0.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
[[package]]
name = "spin"
version = "0.9.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67"
[[package]]
name = "syn"
version = "2.0.106"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ede7c438028d4436d71104916910f5bb611972c5cfd7f89b8300a8186e6fada6"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "tap"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369"
[[package]]
name = "ucs2"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df79298e11f316400c57ec268f3c2c29ac3c4d4777687955cd3d4f3a35ce7eba"
dependencies = [
"bit_field",
]
[[package]]
name = "uefi"
version = "0.35.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "da7569ceafb898907ff764629bac90ac24ba4203c38c33ef79ee88c74aa35b11"
dependencies = [
"bitflags",
"cfg-if",
"log",
"ptr_meta",
"qemu-exit",
"ucs2",
"uefi-macros",
"uefi-raw",
"uguid",
]
[[package]]
name = "uefi-macros"
version = "0.18.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b3dad47b3af8f99116c0f6d4d669c439487d9aaf1c8d9480d686cda6f3a8aa23"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "uefi-raw"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7cad96b8baaf1615d3fdd0f03d04a0b487d857c1b51b19dcbfe05e2e3c447b78"
dependencies = [
"bitflags",
"uguid",
]
[[package]]
name = "uguid"
version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ab14ea9660d240e7865ce9d54ecdbd1cd9fa5802ae6f4512f093c7907e921533"
[[package]]
name = "unicode-ident"
version = "1.0.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f63a545481291138910575129486daeaf8ac54aee4387fe7906919f7830c7d9d"
[[package]]
name = "volatile"
version = "0.4.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "442887c63f2c839b346c192d047a7c87e73d0689c9157b00b53dcc27dd5ea793"
[[package]]
name = "wyz"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed"
dependencies = [
"tap",
]
[[package]]
name = "x86_64"
version = "0.15.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0f042214de98141e9c8706e8192b73f56494087cc55ebec28ce10f26c5c364ae"
dependencies = [
"bit_field",
"bitflags",
"rustversion",
"volatile",
]

26
Cargo.toml Normal file
View file

@ -0,0 +1,26 @@
[package]
name = "badapple-uefi"
version = "0.1.0"
edition = "2021"
license = "MIT OR Apache-2.0"
authors = ["Your Name <you@example.com>"]
description = "UEFI payload that plays the Bad Apple animation (video + audio) using the uefi-rs crate"
[dependencies]
uefi = { version = "0.35", default-features = false, features = ["alloc", "global_allocator", "panic_handler", "logger", "qemu"] }
bitvec = { version = "1", default-features = false, features = ["alloc"] }
micromath = { version = "2", default-features = false }
log = "0.4"
spin = { version = "0.9", default-features = false, features = ["mutex", "spin_mutex"] }
[target.'cfg(target_arch = "x86_64")'.dependencies]
x86_64 = { version = "0.15", default-features = false }
[profile.dev]
opt-level = "s"
[profile.release]
opt-level = "z"
codegen-units = 1
lto = true
strip = true

80
README.md Normal file
View file

@ -0,0 +1,80 @@
# Bad Apple for UEFI (Rust)
Firmware payload that plays the Bad Apple animation directly from a UEFI image. The project uses the [`uefi`](https://github.com/rust-osdev/uefi-rs) crate, renders video frames through GOP, and synthesises audio via the legacy PC speaker using a 1-bit sigma-delta stream. Everything fits inside a `no_std` Rust binary and can be driven under QEMU.
## Repository layout
- `src/` modular firmware code (`app` contains assets, graphics, audio, timing, and player logic).
- `assets/` tiny demo clip/audio used as an embedded fallback.
- `tools/` helper utilities, notably `process_badapple.py` for converting the real video/audio into firmware-friendly blobs.
- `ai/state.json` persistent JSON with personas/tasks as requested.
- `flake.nix` reproducible Nix environment with Rust, ffmpeg, and QEMU.
## Building the EFI binary
1. Ensure you have a recent stable Rust toolchain with the UEFI target installed:
```bash
rustup target add aarch64-unknown-uefi
```
2. Build the binary:
```bash
cargo build --release --target aarch64-unknown-uefi
```
The resulting image will be at `target/aarch64-unknown-uefi/release/badapple-uefi.efi`.
Using Nix instead? Enter the dev shell or boot straight from the flake:
```bash
nix develop
# (rustup target add aarch64-unknown-uefi is invoked automatically in the shell)
# Build & boot in one go under QEMU (cross-emulates aarch64 firmware):
nix run .#qemu
```
## Preparing real assets
The repository embeds a tiny placeholder clip so builds work immediately. To convert the actual Bad Apple video/audio:
```bash
python3 tools/process_badapple.py \
--input /path/to/badapple.mp4 \
--output-dir build/assets \
--width 320 --height 240 --fps 30 \
--sample-rate 44100
```
Copy the generated `video.baa` and `audio.pdm` onto the EFI system partition alongside the application under `\EFI\BADAPPLE\`. The firmware attempts to load external assets first, then falls back to the embedded demo when they are missing.
### Asset format summary
- `video.baa`: magic `"BAA\0"`, header with width/height/fps/frame count, followed by 1-bit packed frames (LSB-first).
- `audio.pdm`: magic `"BAP\0"`, header with sample rate and sample count, followed by 1-bit sigma-delta stream (LSB-first).
## Running under QEMU
A minimal invocation after copying the EFI binary and assets to a FAT image might look like:
```bash
qemu-system-aarch64 \
-machine virt \
-cpu cortex-a72 \
-m 1024 \
-drive if=pflash,format=raw,readonly=on,file=/path/to/AAVMF_CODE.fd \
-drive if=pflash,format=raw,file=/path/to/AAVMF_VARS.fd \
-drive file=fat:rw:esp,format=raw \
-device virtio-gpu-pci \
-device ramfb \
-serial mon:stdio
```
Make sure the `esp` directory contains `EFI/BOOT/BOOTAA64.EFI` (your compiled binary) plus the `EFI/BADAPPLE` assets. Audio output is currently disabled on non-x86 platforms because the legacy PC speaker is unavailable; video playback continues in sync.
## Notes & limitations
- Audio playback uses the legacy PC speaker path on x86 machines; on aarch64 builds the backend is currently silent because that device is absent.
- Timing relies on TSC calibration during boot services. Firmware with non-constant TSCs may require additional guarding.
- Video rendering assumes the selected GOP mode exposes a writable framebuffer (non-BLT-only). Modes with vendor-specific bitmasks are supported via mask calculation.
## Next steps
- Replace the placeholder clip with the full Bad Apple dataset via the tooling above.
- Consider double buffering and smarter frame pacing to reduce tearing.
- Experiment with richer audio backends (e.g. direct HDA/AC97 programming) if available on your platform.

37
ai/state.json Normal file
View file

@ -0,0 +1,37 @@
{
"personas": [
{
"name": "Codex",
"role": "Firmware media player",
"skills": [
"UEFI boot services",
"Framebuffer graphics",
"PC speaker audio",
"Rust no_std engineering"
],
"notes": "Designed to stream the Bad Apple animation through bare-metal firmware interfaces."
}
],
"tasks": [
{
"id": "badapple-video",
"title": "Render Bad Apple frames on GOP",
"status": "completed"
},
{
"id": "badapple-audio",
"title": "Synthesize audio via PC speaker with PDM",
"status": "completed"
},
{
"id": "asset-pipeline",
"title": "Convert source media into firmware-friendly bundles",
"status": "completed"
}
],
"metadata": {
"project": "badapple-uefi",
"author": "Codex",
"last_updated": "2024-11-26T00:00:00Z"
}
}

BIN
assets/demo_audio.pdm Normal file

Binary file not shown.

BIN
assets/demo_video.baa Normal file

Binary file not shown.

102
flake.lock generated Normal file
View file

@ -0,0 +1,102 @@
{
"nodes": {
"fenix": {
"inputs": {
"nixpkgs": [
"naersk",
"nixpkgs"
],
"rust-analyzer-src": "rust-analyzer-src"
},
"locked": {
"lastModified": 1752475459,
"narHash": "sha256-z6QEu4ZFuHiqdOPbYss4/Q8B0BFhacR8ts6jO/F/aOU=",
"owner": "nix-community",
"repo": "fenix",
"rev": "bf0d6f70f4c9a9cf8845f992105652173f4b617f",
"type": "github"
},
"original": {
"owner": "nix-community",
"repo": "fenix",
"type": "github"
}
},
"naersk": {
"inputs": {
"fenix": "fenix",
"nixpkgs": "nixpkgs"
},
"locked": {
"lastModified": 1752689277,
"narHash": "sha256-uldUBFkZe/E7qbvxa3mH1ItrWZyT6w1dBKJQF/3ZSsc=",
"owner": "nix-community",
"repo": "naersk",
"rev": "0e72363d0938b0208d6c646d10649164c43f4d64",
"type": "github"
},
"original": {
"owner": "nix-community",
"repo": "naersk",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1752077645,
"narHash": "sha256-HM791ZQtXV93xtCY+ZxG1REzhQenSQO020cu6rHtAPk=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "be9e214982e20b8310878ac2baa063a961c1bdf6",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixpkgs-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"nixpkgs_2": {
"locked": {
"lastModified": 1759036355,
"narHash": "sha256-0m27AKv6ka+q270dw48KflE0LwQYrO7Fm4/2//KCVWg=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "e9f00bd893984bc8ce46c895c3bf7cac95331127",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixos-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"root": {
"inputs": {
"naersk": "naersk",
"nixpkgs": "nixpkgs_2"
}
},
"rust-analyzer-src": {
"flake": false,
"locked": {
"lastModified": 1752428706,
"narHash": "sha256-EJcdxw3aXfP8Ex1Nm3s0awyH9egQvB2Gu+QEnJn2Sfg=",
"owner": "rust-lang",
"repo": "rust-analyzer",
"rev": "591e3b7624be97e4443ea7b5542c191311aa141d",
"type": "github"
},
"original": {
"owner": "rust-lang",
"ref": "nightly",
"repo": "rust-analyzer",
"type": "github"
}
}
},
"root": "root",
"version": 7
}

97
flake.nix Normal file
View file

@ -0,0 +1,97 @@
{
description = "Bad Apple player running on UEFI using Rust";
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
naersk.url = "github:nix-community/naersk";
};
outputs = { self, nixpkgs, naersk }:
let
systems = [ "x86_64-linux" "aarch64-linux" "x86_64-darwin" "aarch64-darwin" ];
forEachSystem = f:
nixpkgs.lib.genAttrs systems (system:
let
pkgs = import nixpkgs { inherit system; };
in f system pkgs);
in {
packages = forEachSystem (system: pkgs:
let
naersk-lib = naersk.lib.${system};
in {
default = naersk-lib.buildPackage {
pname = "badapple-uefi";
version = "0.1.0";
src = ./.;
cargoBuildOptions = opts: opts ++ [ "--target" "aarch64-unknown-uefi" "--release" ];
doCheck = false;
installPhase = ''
runHook preInstall
mkdir -p $out/EFI/BOOT $out/assets
cp "$CARGO_TARGET_DIR/aarch64-unknown-uefi/release/badapple-uefi.efi" \
$out/EFI/BOOT/BOOTAA64.EFI
cp -r ${./assets}/. $out/assets/ 2>/dev/null || true
runHook postInstall
'';
};
});
apps = forEachSystem (system: pkgs:
let
package = self.packages.${system}.default;
ovmf = pkgs.OVMF.overrideAttrs (old: {
meta = (old.meta or {}) // { broken = false; };
});
runtimePkgs = [ pkgs.qemu ovmf pkgs.coreutils pkgs.findutils ];
script = pkgs.writeShellScriptBin "badapple-qemu" ''
set -euo pipefail
export PATH="${pkgs.lib.makeBinPath runtimePkgs}:$PATH"
esp="$(mktemp -d)"
vars="$(mktemp)"
trap 'rm -rf "$esp" "$vars"' EXIT
mkdir -p "$esp/EFI/BOOT" "$esp/EFI/BADAPPLE"
cp ${package}/EFI/BOOT/BOOTAA64.EFI "$esp/EFI/BOOT/BOOTAA64.EFI"
if [ -d ${package}/assets ]; then
cp -R ${package}/assets/. "$esp/EFI/BADAPPLE/"
fi
cp ${ovmf.fd}/FV/AAVMF_VARS.fd "$vars"
qemu-system-aarch64 \
-machine virt \
-cpu cortex-a72 \
-m 1024 \
-drive if=pflash,format=raw,readonly=on,file=${ovmf.fd}/FV/AAVMF_CODE.fd \
-drive if=pflash,format=raw,file="$vars" \
-drive file=fat:rw:$esp,format=raw \
-device virtio-gpu-pci \
-device ramfb \
-net none \
-serial mon:stdio \
"$@"
'';
in {
qemu = {
type = "app";
program = "${script}/bin/badapple-qemu";
};
});
devShells = forEachSystem (_: pkgs: {
default = pkgs.mkShell {
buildInputs = [
pkgs.rustup
pkgs.qemu
pkgs.ffmpeg
pkgs.python3
pkgs.pkg-config
];
shellHook = ''
rustup target add aarch64-unknown-uefi >/dev/null 2>&1 || true
'';
};
});
};
}

214
src/app/assets.rs Normal file
View file

@ -0,0 +1,214 @@
use alloc::borrow::Cow;
use uefi::{boot, fs::FileSystem, CString16, Handle, Status, StatusExt};
const VIDEO_MAGIC: &[u8; 4] = b"BAA\0";
const AUDIO_MAGIC: &[u8; 4] = b"BAP\0";
const VIDEO_ASSET_PATH: &str = "\\EFI\\BADAPPLE\\video.baa";
const AUDIO_ASSET_PATH: &str = "\\EFI\\BADAPPLE\\audio.pdm";
static EMBEDDED_VIDEO: &[u8] = include_bytes!("../../assets/demo_video.baa");
static EMBEDDED_AUDIO: &[u8] = include_bytes!("../../assets/demo_audio.pdm");
pub struct AssetSource<'a> {
video_blob: Cow<'a, [u8]>,
audio_blob: Cow<'a, [u8]>,
video_header: VideoHeader,
audio_header: AudioHeader,
}
impl<'a> AssetSource<'a> {
pub fn new(
image_handle: Handle,
_bs: &uefi::table::boot::BootServices,
) -> Result<Self, Status> {
let fs_proto = boot::get_image_file_system(image_handle).map_err(|e| e.status())?;
let mut fs = FileSystem::new(fs_proto);
let video_path =
CString16::try_from(VIDEO_ASSET_PATH).map_err(|_| Status::INVALID_PARAMETER)?;
let audio_path =
CString16::try_from(AUDIO_ASSET_PATH).map_err(|_| Status::INVALID_PARAMETER)?;
let video_blob = fs.read(video_path.as_ref()).map_err(map_fs_error)?;
let audio_blob = fs.read(audio_path.as_ref()).map_err(map_fs_error)?;
Self::from_cow(Cow::Owned(video_blob), Cow::Owned(audio_blob))
}
pub fn embedded() -> Result<Self, Status> {
Self::from_cow(Cow::Borrowed(EMBEDDED_VIDEO), Cow::Borrowed(EMBEDDED_AUDIO))
}
fn from_cow(video_blob: Cow<'a, [u8]>, audio_blob: Cow<'a, [u8]>) -> Result<Self, Status> {
let video_header = VideoHeader::parse(&video_blob)?;
let audio_header = AudioHeader::parse(&audio_blob)?;
Ok(Self {
video_blob,
audio_blob,
video_header,
audio_header,
})
}
pub fn video(&'a self) -> VideoPack<'a> {
VideoPack {
header: self.video_header,
data: &self.video_blob[VideoHeader::SIZE..],
}
}
pub fn audio(&'a self) -> AudioPack<'a> {
AudioPack {
header: self.audio_header,
data: &self.audio_blob[AudioHeader::SIZE..],
}
}
}
#[derive(Clone, Copy)]
struct VideoHeader {
width: u16,
height: u16,
fps: u16,
frame_count: u32,
stride_bytes: usize,
}
impl VideoHeader {
const SIZE: usize = 16;
fn parse(blob: &[u8]) -> Result<Self, Status> {
if blob.len() < Self::SIZE {
return Err(Status::COMPROMISED_DATA);
}
if &blob[0..4] != VIDEO_MAGIC {
return Err(Status::COMPROMISED_DATA);
}
let width = u16::from_le_bytes([blob[4], blob[5]]);
let height = u16::from_le_bytes([blob[6], blob[7]]);
let fps = u16::from_le_bytes([blob[8], blob[9]]);
let frame_count = u32::from_le_bytes([blob[10], blob[11], blob[12], blob[13]]);
let stride_bits = width as usize * height as usize;
let stride_bytes = (stride_bits + 7) / 8;
let expected = Self::SIZE + stride_bytes.saturating_mul(frame_count as usize);
if blob.len() < expected {
return Err(Status::COMPROMISED_DATA);
}
Ok(Self {
width,
height,
fps,
frame_count,
stride_bytes,
})
}
}
#[derive(Clone, Copy)]
struct AudioHeader {
sample_rate: u32,
bits_per_sample: u8,
channels: u8,
sample_count: u32,
}
impl AudioHeader {
const SIZE: usize = 16;
fn parse(blob: &[u8]) -> Result<Self, Status> {
if blob.len() < Self::SIZE {
return Err(Status::COMPROMISED_DATA);
}
if &blob[0..4] != AUDIO_MAGIC {
return Err(Status::COMPROMISED_DATA);
}
let sample_rate = u32::from_le_bytes([blob[4], blob[5], blob[6], blob[7]]);
let bits_per_sample = blob[8];
let channels = blob[9];
let sample_count = u32::from_le_bytes([blob[12], blob[13], blob[14], blob[15]]);
if bits_per_sample != 1 || channels != 1 {
return Err(Status::UNSUPPORTED);
}
let expected_bytes = ((sample_count as usize) + 7) / 8;
let expected_total = Self::SIZE + expected_bytes;
if blob.len() < expected_total {
return Err(Status::COMPROMISED_DATA);
}
Ok(Self {
sample_rate,
bits_per_sample,
channels,
sample_count,
})
}
}
pub struct VideoPack<'a> {
header: VideoHeader,
data: &'a [u8],
}
impl<'a> VideoPack<'a> {
pub fn width(&self) -> usize {
self.header.width as usize
}
pub fn height(&self) -> usize {
self.header.height as usize
}
pub fn fps(&self) -> u16 {
self.header.fps
}
pub fn frame_count(&self) -> u32 {
self.header.frame_count
}
pub fn frame_stride_bytes(&self) -> usize {
self.header.stride_bytes
}
pub fn frame_bits(&self, index: u32) -> Option<&'a [u8]> {
let stride = self.header.stride_bytes;
let start = index as usize * stride;
let end = start + stride;
self.data.get(start..end)
}
}
pub struct AudioPack<'a> {
header: AudioHeader,
data: &'a [u8],
}
impl<'a> AudioPack<'a> {
pub fn sample_rate(&self) -> u32 {
self.header.sample_rate
}
pub fn sample_count(&self) -> usize {
self.header.sample_count as usize
}
pub fn bits(&self) -> &'a [u8] {
self.data
}
}
fn map_fs_error(err: uefi::fs::Error) -> Status {
match err {
uefi::fs::Error::Io(io) => io.uefi_error.status(),
uefi::fs::Error::Path(_) => Status::INVALID_PARAMETER,
uefi::fs::Error::Utf8Encoding(_) => Status::COMPROMISED_DATA,
}
}

64
src/app/audio/mod.rs Normal file
View file

@ -0,0 +1,64 @@
#[cfg(target_arch = "x86_64")]
mod speaker;
use uefi::Status;
#[derive(Debug)]
enum InnerBackend {
#[cfg(target_arch = "x86_64")]
PcSpeaker(speaker::PcSpeaker),
Silent,
}
pub struct AudioBackend {
inner: InnerBackend,
}
impl AudioBackend {
pub fn init() -> Result<Self, Status> {
#[cfg(target_arch = "x86_64")]
{
let speaker = speaker::PcSpeaker::new()?;
return Ok(Self {
inner: InnerBackend::PcSpeaker(speaker),
});
}
#[cfg(not(target_arch = "x86_64"))]
{
log::warn!("audio backend not available on this architecture; continuing silently");
Ok(Self {
inner: InnerBackend::Silent,
})
}
}
pub fn configure_sample_rate(&mut self, sample_rate: u32) -> Result<(), Status> {
match &mut self.inner {
InnerBackend::Silent => Ok(()),
#[cfg(target_arch = "x86_64")]
InnerBackend::PcSpeaker(inner) => inner.configure(sample_rate),
}
}
#[inline]
pub fn output_bit(&mut self, enabled: bool) {
match &mut self.inner {
InnerBackend::Silent => {}
#[cfg(target_arch = "x86_64")]
InnerBackend::PcSpeaker(inner) => inner.output(enabled),
}
}
pub fn shutdown(&mut self) {
match &mut self.inner {
InnerBackend::Silent => {}
#[cfg(target_arch = "x86_64")]
InnerBackend::PcSpeaker(inner) => inner.disable(),
}
}
pub fn is_silent(&self) -> bool {
matches!(self.inner, InnerBackend::Silent)
}
}

79
src/app/audio/speaker.rs Normal file
View file

@ -0,0 +1,79 @@
use uefi::Status;
use x86_64::instructions::port::Port;
const PIT_COMMAND_PORT: u16 = 0x43;
const PIT_CHANNEL2_PORT: u16 = 0x42;
const SPEAKER_CONTROL_PORT: u16 = 0x61;
const PIT_BASE_FREQUENCY: u32 = 1_193_182;
pub struct PcSpeaker {
divisor: u16,
}
impl PcSpeaker {
pub fn new() -> Result<Self, Status> {
let mut speaker = Self { divisor: 0 };
// Default to a silent configuration.
speaker.disable();
Ok(speaker)
}
pub fn configure(&mut self, sample_rate: u32) -> Result<(), Status> {
if sample_rate == 0 {
return Err(Status::INVALID_PARAMETER);
}
let divisor = (PIT_BASE_FREQUENCY + (sample_rate / 2)) / sample_rate;
let divisor = divisor.clamp(1, u32::from(u16::MAX)) as u16;
unsafe {
let mut command = Port::<u8>::new(PIT_COMMAND_PORT);
command.write(0b1011_0110); // Channel 2, lobyte/hibyte, square wave (mode 3)
let mut data = Port::<u8>::new(PIT_CHANNEL2_PORT);
data.write(divisor as u8);
data.write((divisor >> 8) as u8);
}
unsafe {
let mut ctrl = Port::<u8>::new(SPEAKER_CONTROL_PORT);
let current = ctrl.read();
// Keep the gate (bit 1) asserted so that toggling bit 0 emits pulses.
let updated = (current | 0b10) & !0b01;
ctrl.write(updated);
}
self.divisor = divisor;
Ok(())
}
#[inline]
pub fn output(&mut self, enabled: bool) {
unsafe {
let mut ctrl = Port::<u8>::new(SPEAKER_CONTROL_PORT);
let mut value = ctrl.read();
if enabled {
value |= 0b01;
} else {
value &= !0b01;
}
ctrl.write(value);
}
}
pub fn disable(&mut self) {
unsafe {
let mut ctrl = Port::<u8>::new(SPEAKER_CONTROL_PORT);
let value = ctrl.read() & !0b11;
ctrl.write(value);
}
}
pub fn sample_rate(&self) -> u32 {
if self.divisor == 0 {
0
} else {
PIT_BASE_FREQUENCY / u32::from(self.divisor)
}
}
}

151
src/app/graphics.rs Normal file
View file

@ -0,0 +1,151 @@
use alloc::{vec, vec::Vec};
use uefi::{
boot::{self, ScopedProtocol},
proto::console::gop::{FrameBuffer, GraphicsOutput, Mode, ModeInfo, PixelFormat},
Status, StatusExt,
};
const PIXEL_ON_RGB: u32 = 0x00FF_FFFF;
const PIXEL_OFF_RGB: u32 = 0x0000_0000;
pub struct GopDisplay {
gop: ScopedProtocol<GraphicsOutput>,
mode: ModeInfo,
scratch: Vec<u32>,
}
impl GopDisplay {
pub fn init() -> Result<Self, Status> {
let handle = boot::get_handle_for_protocol::<GraphicsOutput>().map_err(|e| e.status())?;
let mut gop =
boot::open_protocol_exclusive::<GraphicsOutput>(handle).map_err(|e| e.status())?;
let target_mode = select_best_mode(&gop).ok_or(Status::UNSUPPORTED)?;
gop.set_mode(&target_mode).map_err(|e| e.status())?;
let mode = gop.current_mode_info();
let (width, height) = mode.resolution();
let scratch = vec![0u32; width.saturating_mul(height)];
let mut display = Self { gop, mode, scratch };
display.clear();
Ok(display)
}
pub fn mode(&self) -> ModeInfo {
self.mode
}
pub fn clear(&mut self) {
let info = self.mode;
let (width, height) = info.resolution();
let mut fb = self.frame_buffer();
for y in 0..height {
for x in 0..info.stride() {
let offset = ((y * info.stride()) + x) * 4;
unsafe { fb.write_value(offset, PIXEL_OFF_RGB) };
}
}
}
pub fn draw_mono_frame(
&mut self,
bits: &[u8],
frame_width: usize,
frame_height: usize,
) -> Result<(), Status> {
if frame_width == 0 || frame_height == 0 {
return Ok(());
}
let info = self.mode;
let (screen_w, screen_h) = info.resolution();
if frame_width > screen_w || frame_height > screen_h {
return Err(Status::BAD_BUFFER_SIZE);
}
let offset_x = (screen_w - frame_width) / 2;
let offset_y = (screen_h - frame_height) / 2;
let stride = info.stride();
let pixel_format = info.pixel_format();
let (color_on, color_off) = pixel_colors(pixel_format, info.pixel_bitmask());
let mut fb = self.frame_buffer();
let scratch = &mut self.scratch[..frame_width * frame_height];
decode_monochrome_frame(
bits,
frame_width,
frame_height,
scratch,
color_on,
color_off,
);
for row in 0..frame_height {
let src_row = row * frame_width;
let dest_row = (row + offset_y) * stride + offset_x;
let dest_offset_bytes = dest_row * 4;
let src_slice = &scratch[src_row..src_row + frame_width];
copy_row(&mut fb, dest_offset_bytes, src_slice);
}
Ok(())
}
fn frame_buffer(&mut self) -> FrameBuffer<'_> {
self.gop.frame_buffer()
}
}
fn select_best_mode(gop: &GraphicsOutput) -> Option<Mode> {
gop.modes()
.filter(|mode| mode.info().pixel_format() != PixelFormat::BltOnly)
.max_by(|a, b| {
let (aw, ah) = a.info().resolution();
let (bw, bh) = b.info().resolution();
(aw * ah).cmp(&(bw * bh))
})
}
fn pixel_colors(
format: PixelFormat,
bitmask: Option<uefi::proto::console::gop::PixelBitmask>,
) -> (u32, u32) {
match format {
PixelFormat::Rgb | PixelFormat::Bgr => (PIXEL_ON_RGB, PIXEL_OFF_RGB),
PixelFormat::Bitmask => {
if let Some(mask) = bitmask {
let white = mask.red_mask | mask.green_mask | mask.blue_mask;
(white, 0)
} else {
(PIXEL_ON_RGB, PIXEL_OFF_RGB)
}
}
PixelFormat::BltOnly => (PIXEL_ON_RGB, PIXEL_OFF_RGB),
}
}
fn decode_monochrome_frame(
bits: &[u8],
width: usize,
height: usize,
out_pixels: &mut [u32],
on: u32,
off: u32,
) {
for idx in 0..width * height {
let byte_idx = idx / 8;
let bit_idx = idx % 8;
let bit = (bits.get(byte_idx).copied().unwrap_or(0) >> bit_idx) & 1;
out_pixels[idx] = if bit == 1 { on } else { off };
}
}
fn copy_row(fb: &mut FrameBuffer<'_>, dest_offset_bytes: usize, pixels: &[u32]) {
for (idx, &pixel) in pixels.iter().enumerate() {
unsafe {
fb.write_value(dest_offset_bytes + idx * 4, pixel);
}
}
}

36
src/app/mod.rs Normal file
View file

@ -0,0 +1,36 @@
pub mod assets;
pub mod audio;
pub mod graphics;
pub mod player;
pub mod timing;
use crate::app::{
assets::AssetSource, audio::AudioBackend, graphics::GopDisplay, timing::TscClock,
};
use log::info;
use uefi::prelude::*;
pub fn run(image_handle: Handle, st: &mut SystemTable<Boot>) -> Result<(), Status> {
info!("badapple-uefi starting up");
let bs = st.boot_services();
let clock = TscClock::calibrate(bs).map_err(|status| {
info!("failed to calibrate TSC clock: {:?}", status);
status
})?;
let assets = match AssetSource::new(image_handle, bs) {
Ok(bundle) => bundle,
Err(err) => {
info!("failed to load external assets: {:?}", err);
info!("falling back to built-in sample assets");
AssetSource::embedded()?
}
};
let mut display = GopDisplay::init()?;
let mut audio = AudioBackend::init()?;
player::play(&clock, &mut display, &mut audio, &assets)
}

137
src/app/player.rs Normal file
View file

@ -0,0 +1,137 @@
use crate::app::{
assets::{AssetSource, AudioPack},
audio::AudioBackend,
graphics::GopDisplay,
timing::TscClock,
};
use log::warn;
use uefi::Status;
pub fn play(
clock: &TscClock,
display: &mut GopDisplay,
audio: &mut AudioBackend,
assets: &AssetSource,
) -> Result<(), Status> {
let video = assets.video();
let audio_pack = assets.audio();
if video.frame_count() == 0 {
return Ok(());
}
if audio_pack.sample_rate() == 0 {
return Err(Status::INVALID_PARAMETER);
}
let start_cycles = clock.now_cycles() as u128;
let cycles_per_second = (clock.cycles_per_us() as u128) * 1_000_000u128;
let mut audio_sched = if audio.is_silent() {
warn!("audio backend unavailable; playing Bad Apple without sound");
None
} else {
audio.configure_sample_rate(audio_pack.sample_rate())?;
Some(AudioScheduler::new(
audio_pack,
start_cycles,
cycles_per_second,
))
};
let fps = u64::from(video.fps().max(1));
let frame_count = video.frame_count() as u64;
for frame_idx in 0..frame_count {
if let Some(frame_bits) = video.frame_bits(frame_idx as u32) {
display.draw_mono_frame(frame_bits, video.width(), video.height())?;
}
let frame_end = start_cycles + ((frame_idx + 1) as u128) * cycles_per_second / fps as u128;
// Busy-wait inside firmware land: we spin while catching up audio until
// the frame budget elapses. Firmware has no scheduler, so this is fine
// (and keeps GOP updates & audio in lock-step).
loop {
let now = clock.now_cycles() as u128;
if let Some(sched) = audio_sched.as_mut() {
sched.drain(now, audio);
}
if now >= frame_end {
break;
}
}
}
// Drain any remaining audio samples after the last frame.
if let Some(sched) = audio_sched.as_mut() {
while !sched.is_finished() {
let now = clock.now_cycles() as u128;
sched.drain(now, audio);
}
}
audio.output_bit(false);
audio.shutdown();
Ok(())
}
/// Minimal fixed-point scheduler that keeps the 1-bit audio stream in sync with
/// wall-clock time measured via the TSC.
struct AudioScheduler<'a> {
data: &'a [u8],
total_bits: usize,
index: usize,
sample_rate: u32,
start_cycles: u128,
cycles_per_second: u128,
}
impl<'a> AudioScheduler<'a> {
fn new(pack: AudioPack<'a>, start_cycles: u128, cycles_per_second: u128) -> Self {
Self {
data: pack.bits(),
total_bits: pack.sample_count(),
index: 0,
sample_rate: pack.sample_rate(),
start_cycles,
cycles_per_second,
}
}
fn drain(&mut self, now_cycles: u128, audio: &mut AudioBackend) {
while let Some(deadline) = self.next_deadline_cycles() {
if now_cycles < deadline {
break;
}
let bit = self.bit_at(self.index);
audio.output_bit(bit);
self.index += 1;
}
}
fn is_finished(&self) -> bool {
self.index >= self.total_bits
}
fn next_deadline_cycles(&self) -> Option<u128> {
if self.index >= self.total_bits {
None
} else {
Some(
self.start_cycles
+ (self.index as u128 * self.cycles_per_second) / self.sample_rate as u128,
)
}
}
fn bit_at(&self, idx: usize) -> bool {
if idx >= self.total_bits {
return false;
}
let byte = idx / 8;
let bit = idx % 8;
let value = self.data.get(byte).copied().unwrap_or(0);
((value >> bit) & 1) != 0
}
}

71
src/app/timing.rs Normal file
View file

@ -0,0 +1,71 @@
use uefi::{table::boot::BootServices, Status, StatusExt};
const CALIBRATION_DURATION_US: u64 = 200_000; // 200 ms for stable measurement
#[derive(Clone, Copy)]
pub struct TscClock {
cycles_per_us: u64,
start_cycles: u64,
}
impl TscClock {
pub fn calibrate(bs: &BootServices) -> Result<Self, Status> {
let before = read_cycle_counter();
bs.stall(CALIBRATION_DURATION_US as usize)
.map_err(|e| e.status())?;
let after = read_cycle_counter();
let delta = after.saturating_sub(before);
let cycles_per_us = delta / CALIBRATION_DURATION_US;
if cycles_per_us == 0 {
return Err(Status::DEVICE_ERROR);
}
Ok(Self {
cycles_per_us,
start_cycles: after,
})
}
#[inline]
pub fn now_cycles(&self) -> u64 {
read_cycle_counter()
}
#[inline]
pub fn micros_since_start(&self) -> u64 {
let cycles = self.now_cycles().saturating_sub(self.start_cycles);
cycles / self.cycles_per_us
}
#[inline]
pub fn micros_to_cycles(&self, micros: u64) -> u64 {
micros.saturating_mul(self.cycles_per_us)
}
#[inline]
pub fn cycles_per_us(&self) -> u64 {
self.cycles_per_us
}
#[inline]
pub fn start_cycles(&self) -> u64 {
self.start_cycles
}
}
#[inline]
fn read_cycle_counter() -> u64 {
#[cfg(target_arch = "x86_64")]
{
use core::arch::x86_64::_rdtsc;
unsafe { _rdtsc() }
}
#[cfg(target_arch = "aarch64")]
{
let value: u64;
unsafe { core::arch::asm!("mrs {out}, cntvct_el0", out = out(reg) value); }
value
}
}

20
src/main.rs Normal file
View file

@ -0,0 +1,20 @@
#![no_std]
#![no_main]
mod app;
extern crate alloc;
use uefi::prelude::*;
#[entry]
fn efi_main(image_handle: Handle, mut st: SystemTable<Boot>) -> Status {
if let Err(err) = uefi::helpers::init(&mut st) {
return err;
}
match app::run(image_handle, &mut st) {
Ok(()) => Status::SUCCESS,
Err(status) => status,
}
}

35
test.patch Normal file
View file

@ -0,0 +1,35 @@
diff --git a/src/app/assets.rs b/src/app/assets.rs
index 1234567..abcdef0 100644
--- a/src/app/assets.rs
+++ b/src/app/assets.rs
@@ -1,7 +1,7 @@
-use uefi::{table::boot::BootServices, Status, StatusExt};
+use uefi::{table::BootServices, Status};
// ... rest of your code ...
diff --git a/src/app/mod.rs b/src/app/mod.rs
index 1234567..abcdef0 100644
--- a/src/app/mod.rs
+++ b/src/app/mod.rs
@@ -13,7 +13,7 @@
-pub fn run(image_handle: Handle, st: &mut SystemTable<Boot>) -> Result<(), Status> {
+pub fn run(image_handle: Handle, st: &mut SystemTable<Boot>) -> Result<(), Status> {
// ... your logic ...
}
diff --git a/src/app/graphics.rs b/src/app/graphics.rs
index 1234567..abcdef0 100644
--- a/src/app/graphics.rs
+++ b/src/app/graphics.rs
@@ -119,7 +119,7 @@
- let white = mask.red_mask | mask.green_mask | mask.blue_mask;
+ let white = mask.red | mask.green | mask.blue;
// ... rest of your code ...
-// Refactor any code that borrows self.scratch and self.frame_buffer mutably at the same time.
-// For example, process scratch first, then frame_buffer, or copy data into a local variable.
+// Refactor any code that borrows self.scratch and self.frame_buffer mutably at the same time.
+// For example, process scratch first, then frame_buffer, or copy data into a local variable.

130
tools/process_badapple.py Executable file
View file

@ -0,0 +1,130 @@
#!/usr/bin/env python3
"""Convert a Bad Apple source video into firmware-ready assets.
This tool requires ffmpeg in PATH. It produces:
- video.baa : 1-bit packed frames with custom header.
- audio.pdm : 1-bit sigma-delta encoded PCM data with custom header.
"""
import argparse
import math
import struct
import subprocess
import tempfile
from pathlib import Path
VIDEO_MAGIC = b"BAA\0"
AUDIO_MAGIC = b"BAP\0"
def run_ffmpeg(args):
process = subprocess.run(args, check=False)
if process.returncode != 0:
raise RuntimeError(f"ffmpeg failed: {' '.join(args)}")
def pcm_to_pdm(samples):
# First-order sigma-delta modulation.
acc = 0
threshold = 0
bitstream = bytearray((len(samples) + 7) // 8)
for idx, sample in enumerate(samples):
acc += sample - threshold
if acc >= 0:
threshold = 32767
bit = 1
else:
threshold = -32768
bit = 0
if bit:
bitstream[idx // 8] |= 1 << (idx % 8)
return bitstream
def encode_video(raw_path, width, height, frame_count, fps, threshold):
stride_bits = width * height
stride_bytes = (stride_bits + 7) // 8
output = bytearray()
output += VIDEO_MAGIC
output += struct.pack('<H', width)
output += struct.pack('<H', height)
output += struct.pack('<H', fps)
output += struct.pack('<I', frame_count)
output += b"\x00\x00"
frame_size = width * height
with open(raw_path, 'rb') as fh:
for frame_idx in range(frame_count):
buf = fh.read(frame_size)
if len(buf) != frame_size:
raise RuntimeError(f"unexpected EOF at frame {frame_idx}")
bits = bytearray(stride_bytes)
for i, value in enumerate(buf):
if value >= threshold:
bits[i // 8] |= 1 << (i % 8)
output.extend(bits)
return output
def encode_audio(raw_path, sample_rate, start_offset_samples=0):
data = Path(raw_path).read_bytes()
samples = struct.iter_unpack('<h', data)
pcm = [value for (value,) in samples]
if start_offset_samples > 0:
pcm = pcm[start_offset_samples:]
bitstream = pcm_to_pdm(pcm)
header = bytearray()
header += AUDIO_MAGIC
header += struct.pack('<I', sample_rate)
header += struct.pack('<B', 1) # bits per sample
header += struct.pack('<B', 1) # mono
header += b"\x00\x00"
header += struct.pack('<I', len(pcm))
return bytes(header) + bytes(bitstream)
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('--input', required=True, help='Path to Bad Apple video (any ffmpeg-supported format).')
parser.add_argument('--output-dir', required=True, help='Directory to place generated assets.')
parser.add_argument('--width', type=int, default=320)
parser.add_argument('--height', type=int, default=240)
parser.add_argument('--fps', type=int, default=30)
parser.add_argument('--sample-rate', type=int, default=44100)
parser.add_argument('--threshold', type=int, default=128, help='Luma threshold (0-255) for 1-bit conversion.')
parser.add_argument('--audio-start-delay', type=float, default=0.0, help='Seconds to delay audio relative to video.')
args = parser.parse_args()
output_dir = Path(args.output_dir)
output_dir.mkdir(parents=True, exist_ok=True)
with tempfile.TemporaryDirectory() as tmpdir:
tmpdir = Path(tmpdir)
raw_video = tmpdir / 'frames.raw'
raw_audio = tmpdir / 'audio.raw'
run_ffmpeg([
'ffmpeg', '-y', '-i', args.input,
'-vf', f'scale={args.width}:{args.height},fps={args.fps},format=gray',
'-an', '-f', 'rawvideo', str(raw_video)
])
run_ffmpeg([
'ffmpeg', '-y', '-i', args.input,
'-vn', '-ac', '1', '-ar', str(args.sample_rate), '-f', 's16le', str(raw_audio)
])
frame_size = args.width * args.height
video_size = raw_video.stat().st_size
frame_count = video_size // frame_size
if frame_count == 0:
raise RuntimeError("ffmpeg produced zero frames - check input file")
video_blob = encode_video(raw_video, args.width, args.height, frame_count, args.fps, args.threshold)
audio_offset = int(args.audio_start_delay * args.sample_rate)
audio_blob = encode_audio(raw_audio, args.sample_rate, audio_offset)
(output_dir / 'video.baa').write_bytes(video_blob)
(output_dir / 'audio.pdm').write_bytes(audio_blob)
print(f"Generated {frame_count} frames @ {args.width}x{args.height} and audio track {args.sample_rate} Hz")