From decdf07be024824a6ea28ca032e12a54e193c2a1 Mon Sep 17 00:00:00 2001 From: Jan Janssen Date: Sat, 8 Apr 2023 15:34:34 +0200 Subject: [PATCH] examples: Add uefi-demo --- .github/workflows/build_docs.yaml | 2 +- .github/workflows/ci.yaml | 15 +- Cargo.toml | 1 + examples/uefi-demo/Cargo.toml | 28 +++ examples/uefi-demo/README.md | 23 +++ examples/uefi-demo/build.rs | 12 ++ examples/uefi-demo/demo.slint | 114 ++++++++++++ examples/uefi-demo/main.rs | 282 ++++++++++++++++++++++++++++++ 8 files changed, 474 insertions(+), 3 deletions(-) create mode 100644 examples/uefi-demo/Cargo.toml create mode 100644 examples/uefi-demo/README.md create mode 100644 examples/uefi-demo/build.rs create mode 100644 examples/uefi-demo/demo.slint create mode 100644 examples/uefi-demo/main.rs diff --git a/.github/workflows/build_docs.yaml b/.github/workflows/build_docs.yaml index 9c2e1c2b9..c6c0da822 100644 --- a/.github/workflows/build_docs.yaml +++ b/.github/workflows/build_docs.yaml @@ -107,7 +107,7 @@ jobs: docs/tutorial/cpp/book/html docs/tutorial/node/book/html - name: "Check for docs warnings in internal crates" - run: cargo +nightly doc --workspace --no-deps --all-features --exclude slint-node --exclude mcu-board-support --exclude printerdemo_mcu --exclude carousel --exclude test-* --exclude plotter + run: cargo +nightly doc --workspace --no-deps --all-features --exclude slint-node --exclude mcu-board-support --exclude printerdemo_mcu --exclude carousel --exclude test-* --exclude plotter --exclude uefi-demo - name: Clean cache # Don't cache docs to avoid them including removed classes being published run: | rm -rf target/doc target/cppdocs target/slintdocs api/node/docs docs/tutorial/rust/book docs/tutorial/cpp/book docs/tutorial/node/book diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 1ed4230f5..b174db383 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -60,14 +60,14 @@ jobs: toolchain: ${{ matrix.rust_version }} key: x-v2-${{ steps.node-install.outputs.node-version }} # the cache key consists of a manually bumpable version and the node version, as the cached rustc artifacts contain linking information where to find node.lib, which is in a versioned directory. - name: Build - run: cargo build --verbose --all-features --workspace ${{ matrix.extra_args }} --exclude test-driver-cpp --exclude mcu-board-support --exclude printerdemo_mcu --exclude carousel # mcu backend requires nightly + run: cargo build --verbose --all-features --workspace ${{ matrix.extra_args }} --exclude test-driver-cpp --exclude mcu-board-support --exclude printerdemo_mcu --exclude carousel --exclude uefi-demo # mcu backend requires nightly - name: Run tests uses: actions-rs/cargo@v1 env: SLINT_CREATE_SCREENSHOTS: 1 with: command: test - args: --verbose --all-features --workspace ${{ matrix.extra_args }} --exclude test-driver-cpp --exclude mcu-board-support --exclude printerdemo_mcu --exclude carousel # mcu backend requires nightly + args: --verbose --all-features --workspace ${{ matrix.extra_args }} --exclude test-driver-cpp --exclude mcu-board-support --exclude printerdemo_mcu --exclude carousel --exclude uefi-demo # mcu backend requires nightly - name: Archive screenshots after failed tests if: ${{ failure() }} uses: actions/upload-artifact@v3 @@ -233,6 +233,17 @@ jobs: - name: Check run: cross check --target=armv7-unknown-linux-gnueabihf -p slint-cpp --no-default-features --features=testing,interpreter + uefi-demo: + runs-on: ubuntu-20.04 + steps: + - uses: actions/checkout@v3 + - uses: ./.github/actions/setup-rust + with: + toolchain: stable + target: x86_64-unknown-uefi + - name: Check + run: cargo check --target=x86_64-unknown-uefi -p uefi-demo + docs: uses: ./.github/workflows/build_docs.yaml diff --git a/Cargo.toml b/Cargo.toml index 7f988e82c..290c099cd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,6 +24,7 @@ members = [ 'examples/carousel/rust', 'examples/energy-monitor', 'examples/mcu-board-support', + 'examples/uefi-demo', 'helper_crates/const-field-offset', 'helper_crates/vtable', 'helper_crates/vtable/macro', diff --git a/examples/uefi-demo/Cargo.toml b/examples/uefi-demo/Cargo.toml new file mode 100644 index 000000000..e6b77b0aa --- /dev/null +++ b/examples/uefi-demo/Cargo.toml @@ -0,0 +1,28 @@ +# Copyright © SixtyFPS GmbH +# SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-commercial + +[package] +name = "uefi-demo" +version = "1.0.0" +edition = "2021" +license = "GPL-3.0-only OR LicenseRef-Slint-commercial" +build = "build.rs" +publish = false + +[[bin]] +path = "main.rs" +name = "uefi-demo" + +[dependencies] +uefi = "0.20.0" +uefi-services = "0.17.0" + +slint = { path = "../../api/rs/slint", default-features = false, features = [ + "compat-1-0", + "libm", + "log", + "unsafe-single-threaded", +] } + +[build-dependencies] +slint-build = { path = "../../api/rs/build" } diff --git a/examples/uefi-demo/README.md b/examples/uefi-demo/README.md new file mode 100644 index 000000000..c1d94bc8b --- /dev/null +++ b/examples/uefi-demo/README.md @@ -0,0 +1,23 @@ +# Slint UEFI demo + +This example demonstrates slint in a UEFI environment. + +To build this example a suitable UEFI rust target must be installed first: + +``` +rustup target install x86_64-unknown-uefi +``` + +To build, simply pass the `--package` and `--target` arguments to cargo: + +``` +cargo build --package uefi-demo --target x86_64-unknown-uefi +``` + +The produced UEFI binary can then either be tested on real hardware by booting +it like any other bootloader or directly with QEMU (the firmware location +varies by distro): + +``` +qemu-system-x86_64 -bios /usr/share/edk2-ovmf/x64/OVMF.fd -kernel target/x86_64-unknown-uefi/debug/uefi-demo.efi +``` diff --git a/examples/uefi-demo/build.rs b/examples/uefi-demo/build.rs new file mode 100644 index 000000000..763cd4887 --- /dev/null +++ b/examples/uefi-demo/build.rs @@ -0,0 +1,12 @@ +// Copyright © SixtyFPS GmbH +// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-commercial + +fn main() { + slint_build::compile_with_config( + "demo.slint", + slint_build::CompilerConfiguration::new() + .with_style("fluent-dark".to_owned()) + .embed_resources(slint_build::EmbedResourcesKind::EmbedForSoftwareRenderer), + ) + .unwrap(); +} diff --git a/examples/uefi-demo/demo.slint b/examples/uefi-demo/demo.slint new file mode 100644 index 000000000..b01142af1 --- /dev/null +++ b/examples/uefi-demo/demo.slint @@ -0,0 +1,114 @@ +// Copyright © SixtyFPS GmbH +// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-commercial + +import { + AboutSlint, Button, GridBox, HorizontalBox, LineEdit, Slider, + StandardButton, StandardListView, TabWidget, VerticalBox +} from "std-widgets.slint"; + +export component Demo inherits Window { + in property firmware-vendor; + in property firmware-version; + in property uefi-version; + in property secure-boot; + + default-font-size: 22px; + default-font-family: "Noto Sans"; + + TabWidget { + width: root.width; + height: root.height; + + Tab { + title: "Info"; + GridBox { + Row { Rectangle {} } + Row { + Text { + colspan: 2; + text: "slint UEFI Demo"; + horizontal-alignment: center; + font-size: 44px; + font-weight: 600; + } + } + Row { + Text { text: "Firmware vendor:"; horizontal-alignment: right; } + Text { text: firmware-vendor; } + } + Row { + Text { text: "Firmware version:"; horizontal-alignment: right; } + Text { text: firmware-version; } + } + Row { + Text { text: "UEFI version:"; horizontal-alignment: right; } + Text { text: uefi-version; } + } + Row { + Text { text: "Secure boot:"; horizontal-alignment: right; } + Text { text: secure-boot ? "enabled" : "disabled"; } + } + Row { + Text { text: "Resolution:"; horizontal-alignment: right; } + Text { text: "\{floor(root.width / 1px)}x\{floor(root.height / 1px)}"; } + } + Row { Rectangle {} } + } + } + + Tab { + title: "Widgets"; + VerticalBox { + enabler := Button { + checked: true; + checkable: true; + text: "Widgets enabled"; + } + LineEdit { + enabled: enabler.checked; + placeholder-text: "Edit Me!"; + } + Slider { + enabled: enabler.checked; + } + StandardListView { + vertical-stretch: 1; + enabled: enabler.checked; + model: [ + { text: "Abydos"}, { text: "Asuras" }, { text: "Athos" }, + { text: "Celestis" }, { text: "Chulak"}, { text: "Dakara"}, + { text: "Earth" }, { text: "Langara" }, { text: "Tollana"}, + ]; + } + HorizontalBox { + alignment: center; + StandardButton { enabled: enabler.checked; kind: ok; } + StandardButton { enabled: enabler.checked; kind: reset; } + StandardButton { enabled: enabler.checked; kind: abort; } + } + } + } + + Tab { + title: "V-Sync"; + Rectangle { + for color[index] in [ + #fff, #f00, #0f0, + #00f, #0ff, #ff0, + #f0f]: Rectangle { + y: 0; + height: parent.height; + x: (parent.width - self.width) * 0.5 * + (1 + 1.1 * sin(animation-tick() * (index + 1) / 17ms * 1deg)); + width: 25px + 100px * abs(sin(animation-tick() / 25ms * 1deg)); + background: color; + } + } + } + + Tab { + title: "About"; + AboutSlint {} + } + } +} diff --git a/examples/uefi-demo/main.rs b/examples/uefi-demo/main.rs new file mode 100644 index 000000000..bc298825a --- /dev/null +++ b/examples/uefi-demo/main.rs @@ -0,0 +1,282 @@ +// Copyright © SixtyFPS GmbH +// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-commercial + +#![no_main] +#![no_std] + +extern crate alloc; + +use alloc::{ + boxed::Box, + format, + rc::Rc, + string::{String, ToString}, +}; +use core::{slice, time::Duration}; +use slint::{platform::software_renderer, SharedString}; +use uefi::{prelude::*, proto::console::gop::BltPixel, Char16}; +use uefi_services::system_table; + +slint::include_modules!(); + +fn st() -> &'static mut SystemTable { + // SAFETY: uefi_services::init() is always called first in main() + // and we never operate outside boot services + unsafe { system_table().as_mut() } +} + +fn timer_tick() -> u64 { + #[cfg(target_arch = "x86")] + unsafe { + core::arch::x86::_rdtsc() + } + + #[cfg(target_arch = "x86_64")] + unsafe { + core::arch::x86_64::_rdtsc() + } + + #[cfg(target_arch = "aarch64")] + unsafe { + let mut ticks: u64; + core::arch::asm!("mrs {}, cntvct_el0", out(reg) ticks); + ticks + } +} + +fn timer_freq() -> u64 { + #[cfg(any(target_arch = "x86", target_arch = "x86_64"))] + { + let start = timer_tick(); + st().boot_services().stall(1000); + let end = timer_tick(); + (end - start) * 1000 + } + + #[cfg(target_arch = "aarch64")] + unsafe { + let mut freq: u64; + core::arch::asm!("mrs {}, cntfrq_el0", out(reg) freq); + freq + } +} + +fn get_key_press() -> Option { + use slint::platform::Key::*; + use uefi::proto::console::text::Key as UefiKey; + use uefi::proto::console::text::ScanCode as Scan; + + let nl = Char16::try_from('\r').unwrap(); + + match st().stdin().read_key() { + Err(_) | Ok(None) => None, + Ok(Some(UefiKey::Printable(key))) if key == nl => Some('\n'), + Ok(Some(UefiKey::Printable(key))) => Some(char::from(key)), + Ok(Some(UefiKey::Special(key))) => Some( + match key { + Scan::UP => UpArrow, + Scan::DOWN => DownArrow, + Scan::RIGHT => RightArrow, + Scan::LEFT => LeftArrow, + Scan::HOME => Home, + Scan::END => End, + Scan::INSERT => Insert, + Scan::DELETE => Delete, + Scan::PAGE_UP => PageUp, + Scan::PAGE_DOWN => PageDown, + Scan::ESCAPE => Escape, + Scan::FUNCTION_1 => F1, + Scan::FUNCTION_2 => F2, + Scan::FUNCTION_3 => F3, + Scan::FUNCTION_4 => F4, + Scan::FUNCTION_5 => F5, + Scan::FUNCTION_6 => F6, + Scan::FUNCTION_7 => F7, + Scan::FUNCTION_8 => F8, + Scan::FUNCTION_9 => F9, + Scan::FUNCTION_10 => F10, + Scan::FUNCTION_11 => F11, + Scan::FUNCTION_12 => F12, + Scan::FUNCTION_13 => F13, + Scan::FUNCTION_14 => F14, + Scan::FUNCTION_15 => F15, + Scan::FUNCTION_16 => F16, + Scan::FUNCTION_17 => F17, + Scan::FUNCTION_18 => F18, + Scan::FUNCTION_19 => F19, + Scan::FUNCTION_20 => F20, + Scan::FUNCTION_21 => F21, + Scan::FUNCTION_22 => F22, + Scan::FUNCTION_23 => F23, + Scan::FUNCTION_24 => F24, + _ => return None, + } + .into(), + ), + } +} + +fn wait_for_input(max_timeout: Option) { + use uefi::table::boot::*; + + let watchdog_timeout = Duration::from_secs(120); + let timeout = watchdog_timeout.min(max_timeout.unwrap_or(watchdog_timeout)); + + let bs = st().boot_services(); + + // SAFETY: The event is closed before returning from this function. + let timer = unsafe { bs.create_event(EventType::TIMER, Tpl::APPLICATION, None, None).unwrap() }; + bs.set_timer(&timer, TimerTrigger::Periodic((timeout.as_nanos() / 100) as u64)).unwrap(); + + bs.set_watchdog_timer(2 * watchdog_timeout.as_micros() as usize, 0x10000, None).unwrap(); + + { + // SAFETY: The cloned handles are only used to wait for further input events and + // are then immediately dropped. + let mut events = + unsafe { [st().stdin().wait_for_key_event().unsafe_clone(), timer.unsafe_clone()] }; + bs.wait_for_event(&mut events).unwrap(); + } + + bs.set_watchdog_timer(2 * watchdog_timeout.as_micros() as usize, 0x10000, None).unwrap(); + bs.close_event(timer).unwrap(); +} + +#[repr(transparent)] +#[derive(Clone, Copy)] +struct SlintBltPixel(BltPixel); + +impl software_renderer::TargetPixel for SlintBltPixel { + fn blend(&mut self, color: software_renderer::PremultipliedRgbaColor) { + let a = (u8::MAX - color.alpha) as u16; + self.0.red = (self.0.red as u16 * a / 255) as u8 + color.red; + self.0.green = (self.0.green as u16 * a / 255) as u8 + color.green; + self.0.blue = (self.0.blue as u16 * a / 255) as u8 + color.blue; + } + + fn from_rgb(red: u8, green: u8, blue: u8) -> Self { + SlintBltPixel(BltPixel::new(red, green, blue)) + } +} + +struct Platform { + window: Rc, + timer_freq: f64, + timer_start: f64, +} + +impl Default for Platform { + fn default() -> Self { + Self { + window: software_renderer::MinimalSoftwareWindow::new( + software_renderer::RepaintBufferType::ReusedBuffer, + ), + timer_freq: timer_freq() as f64, + timer_start: timer_tick() as f64, + } + } +} + +impl slint::platform::Platform for Platform { + fn create_window_adapter( + &self, + ) -> Result, slint::PlatformError> { + Ok(self.window.clone()) + } + + fn duration_since_start(&self) -> Duration { + Duration::from_secs_f64((timer_tick() as f64 - self.timer_start) / self.timer_freq) + } + + fn run_event_loop(&self) -> Result<(), slint::PlatformError> { + use uefi::{proto::console::gop::*, table::boot::*}; + + let bs = st().boot_services(); + + let gop_handle = bs.get_handle_for_protocol::().unwrap(); + + // SAFETY: uefi-rs wants us to use open_protocol_exclusive(), which will not work + // on real hardware. We can only hope that any other users of this + // handle/protocol behave and don't interfere with our uses of it. + let mut gop = unsafe { + bs.open_protocol::( + OpenProtocolParams { + handle: gop_handle, + agent: bs.image_handle(), + controller: None, + }, + OpenProtocolAttributes::GetProtocol, + ) + .unwrap() + }; + + let info = gop.current_mode_info(); + let mut fb = alloc::vec![SlintBltPixel(BltPixel::new(0, 0, 0)); info.resolution().0 * info.resolution().1]; + + self.window.set_size(slint::PhysicalSize::new( + info.resolution().0.try_into().unwrap(), + info.resolution().1.try_into().unwrap(), + )); + + loop { + slint::platform::update_timers_and_animations(); + + while let Some(key) = get_key_press() { + // EFI does not distinguish between pressed and released events. + let text = SharedString::from(key); + self.window.dispatch_event(slint::platform::WindowEvent::KeyPressed { + text: text.clone(), + }); + self.window.dispatch_event(slint::platform::WindowEvent::KeyReleased { text }); + } + + self.window.draw_if_needed(|renderer| { + renderer.render(&mut fb, info.resolution().0); + + // SAFETY: SlintBltPixel is a repr(transparent) BltPixel so it is safe to transform. + let blt_fb = + unsafe { slice::from_raw_parts(fb.as_ptr() as *const BltPixel, fb.len()) }; + + // We could let the software renderer draw to gop.frame_buffer() directly, but that + // requires dealing with different frame buffer formats. The blit buffer is easier to + // deal with and guaranteed to be available by the UEFI spec. This also reduces tearing + // by quite a bit. + gop.blt(BltOp::BufferToVideo { + buffer: blt_fb, + src: BltRegion::Full, + dest: (0, 0), + dims: info.resolution(), + }) + .unwrap(); + }); + + if !self.window.has_active_animations() { + wait_for_input(slint::platform::duration_until_next_timer_update()); + } + } + } +} + +#[entry] +fn main(_image_handle: Handle, mut st: SystemTable) -> Status { + uefi_services::init(&mut st).unwrap(); + + slint::platform::set_platform(Box::::default()).unwrap(); + + let ui = Demo::new().unwrap(); + + ui.set_firmware_vendor(String::from_utf16_lossy(st.firmware_vendor().to_u16_slice()).into()); + ui.set_firmware_version( + format!("{}.{:02}", st.firmware_revision() >> 16, st.firmware_revision() & 0xffff).into(), + ); + ui.set_uefi_version(st.uefi_revision().to_string().into()); + + let mut buf = [0u8; 1]; + let guid = uefi::table::runtime::VariableVendor::GLOBAL_VARIABLE; + let sb = st.runtime_services().get_variable(cstr16!("SecureBoot"), &guid, &mut buf); + ui.set_secure_boot(if sb.is_ok() { buf[0] == 1 } else { false }); + + ui.run().unwrap(); + + Status::SUCCESS +}