init
This commit is contained in:
commit
cc9f6b58e1
21 changed files with 1538 additions and 0 deletions
1
.cache/uefi-rs
Submodule
1
.cache/uefi-rs
Submodule
|
|
@ -0,0 +1 @@
|
|||
Subproject commit 565b1aad2b6a79a11c67e419879475ac7414f6fe
|
||||
9
.cargo/config.toml
Normal file
9
.cargo/config.toml
Normal 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
21
.gitignore
vendored
Normal 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
228
Cargo.lock
generated
Normal 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
26
Cargo.toml
Normal 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
80
README.md
Normal 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
37
ai/state.json
Normal 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
BIN
assets/demo_audio.pdm
Normal file
Binary file not shown.
BIN
assets/demo_video.baa
Normal file
BIN
assets/demo_video.baa
Normal file
Binary file not shown.
102
flake.lock
generated
Normal file
102
flake.lock
generated
Normal 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
97
flake.nix
Normal 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
214
src/app/assets.rs
Normal 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
64
src/app/audio/mod.rs
Normal 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
79
src/app/audio/speaker.rs
Normal 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
151
src/app/graphics.rs
Normal 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
36
src/app/mod.rs
Normal 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
137
src/app/player.rs
Normal 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
71
src/app/timing.rs
Normal 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
20
src/main.rs
Normal 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
35
test.patch
Normal 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
130
tools/process_badapple.py
Executable 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")
|
||||
Loading…
Add table
Add a link
Reference in a new issue