Initializes Slint gui (#1084)
Some checks failed
Development Build / Build (push) Has been cancelled
Development Build / Update PRs (push) Has been cancelled

Co-authored-by: tompro <tomas.prochazka@apertia.cz>
This commit is contained in:
SuchAFuriousDeath 2024-11-11 05:16:48 +01:00 committed by GitHub
parent 9aef904d24
commit 296cc3e502
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
41 changed files with 1751 additions and 1370 deletions

View File

@ -15,7 +15,7 @@ on:
type: string
required: true
env:
CMAKE_BUILD_PARALLEL_LEVEL: '3'
CMAKE_BUILD_PARALLEL_LEVEL: "3"
jobs:
build:
name: ${{ inputs.name }}
@ -25,6 +25,10 @@ jobs:
QT_URL_BASE: https://download.qt.io/online/qtsdkrepository/mac_x64/desktop/qt6_672/qt.qt6.672.clang_64/6.7.2-0-202406110330qtbase-MacOS-MacOS_13-Clang-MacOS-MacOS_13-X86_64-ARM64.7z
QT_URL_SVG: https://download.qt.io/online/qtsdkrepository/mac_x64/desktop/qt6_672/qt.qt6.672.clang_64/6.7.2-0-202406110330qtsvg-MacOS-MacOS_13-Clang-MacOS-MacOS_13-X86_64-ARM64.7z
steps:
- name: Set up Python 3.12 (the latest version that is able to build skia-bindings)
uses: actions/setup-python@v4
with:
python-version: "3.12"
- name: Checkout source
uses: actions/checkout@v4
- name: Generate cache keys

26
.vscode/launch.json vendored
View File

@ -1,7 +1,7 @@
{
"configurations": [
{
"name": "GUI",
"name": "GUI (Qt)",
"type": "lldb",
"request": "launch",
"args": [
@ -22,6 +22,24 @@
"program": "${workspaceFolder}/build/gui/obliteration.app/Contents/MacOS/obliteration"
}
},
{
"name": "GUI (Slint)",
"type": "lldb",
"request": "launch",
"cargo": {
"args": [
"build",
"--manifest-path",
"${workspaceFolder}/gui/Cargo.toml",
"--features",
"slint"
],
"filter": {
"kind": "bin"
}
},
"cwd": "${workspaceFolder}"
},
{
"name": "Kernel",
"type": "lldb",
@ -31,10 +49,10 @@
"target create ${workspaceFolder}/build/obkrnl",
"target modules load --file ${workspaceFolder}/build/obkrnl -s 0xffffffff82200000"
],
"processCreateCommands": [
"gdb-remote 1234"
]
"processCreateCommands": ["gdb-remote 1234"]
}
],
"version": "2.0.0"
}

View File

@ -41,7 +41,7 @@ else()
endif()
add_cargo(MANIFEST Cargo.toml)
add_crate(gui)
add_crate(gui LIBRARY ARGS --features "qt")
add_crate(obkrnl
TOOLCHAIN ${kernel_toolchain}
VENDOR "unknown"

View File

@ -178,6 +178,9 @@ function(add_crate crate)
if(${kind} STREQUAL "staticlib")
add_library(${crate} STATIC IMPORTED)
list(APPEND build_args "--lib")
if(WIN32)
set(debug_artifact "${debug_outputs}/${crate}.lib")
set(release_artifact "${release_outputs}/${crate}.lib")

View File

@ -66,6 +66,10 @@ target_link_libraries(obliteration PRIVATE Qt6::Svg Qt6::Widgets)
target_link_libraries(obliteration PRIVATE Threads::Threads)
target_link_libraries(obliteration PRIVATE gui)
if(WIN32 OR (UNIX AND NOT APPLE))
target_link_libraries(obliteration PRIVATE Vulkan::Vulkan)
endif()
if(WIN32)
target_link_libraries(obliteration PRIVATE bcrypt imm32 ntdll setupapi userenv version winhvplatform winmm ws2_32)
elseif(APPLE)

View File

@ -5,6 +5,20 @@ edition = "2021"
[lib]
crate-type = ["staticlib"]
required-features = ["qt"]
[[bin]]
name = "obliteration"
path = "src/main.rs"
required-features = ["slint"]
[features]
slint = [
"dep:slint",
"dep:clap",
"dep:raw-window-handle",
]
qt = []
[dependencies]
bitfield-struct = "0.8.0"
@ -26,9 +40,18 @@ aarch64 = { path = "../arch/aarch64" }
[target.'cfg(target_arch = "x86_64")'.dependencies]
x86-64 = { path = "../arch/x86-64" }
raw-window-handle = { version = "0.6", optional = true }
clap = { version = "4.5.20", features = ["derive"], optional = true }
[dependencies.slint]
git = "https://github.com/slint-ui/slint"
rev = "875ca075fb5b2dfe4c3ab0a499d5759412fc1395"
features = ["compat-1-2", "std", "accessibility", "raw-window-handle-06", "backend-winit", "renderer-skia"]
default-features = false
optional = true
[target.'cfg(not(target_os = "macos"))'.dependencies]
ash = "0.38.0"
ash = { version = "0.38.0", features = ["linked", "std"], default-features = false }
[target.'cfg(windows)'.dependencies.windows-sys]
version = "0.52.0"
@ -49,3 +72,4 @@ objc = "0.2.7"
[build-dependencies]
cbindgen = "0.26.0"
slint-build = { git = "https://github.com/slint-ui/slint", rev = "875ca075fb5b2dfe4c3ab0a499d5759412fc1395" }

View File

@ -1,4 +1,5 @@
use cbindgen::{Builder, Config, Language, Style};
use slint_build::CompilerConfiguration;
use std::path::PathBuf;
const LINUX_INCLUDE: &str = r#"
@ -8,6 +9,25 @@ const LINUX_INCLUDE: &str = r#"
"#;
fn main() {
if std::env::var("CARGO_FEATURE_SLINT").is_ok_and(|var| var == "1") {
build_bin();
}
if std::env::var("CARGO_FEATURE_QT").is_ok_and(|var| var == "1") {
build_lib();
}
}
fn build_bin() {
// Compile main
slint_build::compile_with_config(
"slint/main.slint",
CompilerConfiguration::new().with_style(String::from("fluent-dark")),
)
.unwrap();
}
fn build_lib() {
let root = PathBuf::from(std::env::var("CARGO_MANIFEST_DIR").unwrap());
let mut conf = Config::default();
let mut buf = String::new();

View File

@ -166,7 +166,7 @@ int main(int argc, char *argv[])
QMessageBox::critical(
nullptr,
"Error",
"No any Vulkan device supports Vulkan 1.3.");
"No Vulkan device supports Vulkan 1.3.");
return 1;
}

63
gui/slint/main.slint Normal file
View File

@ -0,0 +1,63 @@
import { Game } from "structs.slint";
import { Tabs } from "main/tabs.slint";
import { Menu } from "main/menu.slint";
import { Actions } from "main/actions.slint";
export component MainWindow inherits Window {
preferred-width: 1920px;
preferred-height: 1080px;
icon: @image-url("../resources/obliteration-icon.png");
title: "Obliteration";
in property <[Game]> games: [];
in property <[string]> profiles: [];
in property <[string]> devices: [];
pure callback start_game(int);
pure callback clear_log();
pure callback get_log_text() -> string;
pure callback open_new_issue_link();
pure callback install_pkg();
pure callback open_system_folder();
pure callback quit();
VerticalLayout {
Menu {
background: root.background;
popup_width: root.width / 2;
popup_height: root.height / 2;
popup_x: root.width / 4;
popup_y: root.height / 4;
quit => { quit(); }
open_new_issue_link => { open_new_issue_link(); }
install_pkg => { install_pkg(); }
open_system_folder => { open_system_folder(); }
}
Tabs {
devices: devices;
games: games;
select_game(index) => { }
}
Actions {
profiles: profiles;
start_game => {
start_game(0)
}
}
}
}
export component Screen inherits Window {}
export component ErrorDialog inherits Window {
in property <string> message;
Text {
padding: 10px;
text: message;
}
}

View File

@ -0,0 +1,29 @@
import { ComboBox, Button } from "std-widgets.slint";
export component Actions {
in property <[string]> profiles: [];
pure callback start_game();
HorizontalLayout {
alignment: stretch;
spacing: 5px;
ComboBox {
model: profiles;
horizontal-stretch: 1;
}
Button {
text: "Start";
horizontal-stretch: 0;
icon: @image-url("../../resources/play.svg");
clicked => { start_game(); }
}
Button {
text: "Save";
horizontal-stretch: 0;
icon: @image-url("../../resources/content-save.svg");
}
}
}

178
gui/slint/main/menu.slint Normal file
View File

@ -0,0 +1,178 @@
import { Button, StandardButton, AboutSlint } from "std-widgets.slint";
export component Menu {
in property <brush> background;
in property <length> popup_width;
in property <length> popup_height;
in property <length> popup_x;
in property <length> popup_y;
pure callback quit();
pure callback open_new_issue_link();
pure callback open_system_folder();
pure callback install_pkg();
install_pkg_dialog := PopupWindow {
width: popup_width;
height: popup_height;
x: popup_x;
y: popup_y;
Dialog {
Text {
text: "This is a dialog box";
}
StandardButton { kind: ok; }
StandardButton { kind: cancel; }
}
}
about_dialog := PopupWindow {
width: popup_width;
height: popup_height;
x: popup_x;
y: popup_y;
Rectangle {
Dialog {
Text {
text: "Obliteration is a free and open-source software for playing your PlayStation 4 titles on PC.";
}
StandardButton { kind: ok; }
}
}
}
about_slint := PopupWindow {
width: popup_width;
height: popup_height;
x: popup_x;
y: popup_y;
Rectangle {
Dialog {
AboutSlint {}
StandardButton { kind: ok; }
}
}
}
logs := PopupWindow {
width: popup_width;
height: popup_height;
x: popup_x;
y: popup_y;
Rectangle {
Text {
text: "Obliteration is a free and open-source software for playing your PlayStation 4 titles on PC.";
}
}
}
HorizontalLayout {
alignment: start;
spacing: 5px;
padding-left: 5px;
VerticalLayout {
TouchArea {
clicked => { files_popup.show(); }
Rectangle {
Text {
text: "File";
}
}
}
files_popup := PopupWindow {
y: parent.y + parent.height;
Rectangle {
background: background;
VerticalLayout {
spacing: 0px;
Button {
text: "Install PKG";
clicked => { install_pkg(); }
}
Button {
text: "Open System Folder";
clicked => { open_system_folder(); }
}
Button {
text: "Quit";
clicked => { quit() }
}
}
}
}
}
VerticalLayout {
TouchArea {
clicked => { view_popup.show(); }
Rectangle {
Text {
text: "View";
}
}
}
view_popup := PopupWindow {
y: parent.y + parent.height;
Rectangle {
background: background;
VerticalLayout {
spacing: 0px;
Button {
text: "Logs";
clicked => { logs.show(); }
}
}
}
}
}
VerticalLayout {
TouchArea {
clicked => { help_popup.show(); }
Rectangle {
Text {
text: "Help";
}
}
}
help_popup := PopupWindow {
y: parent.y + parent.height;
Rectangle {
background: background;
VerticalLayout {
spacing: 0px;
Button {
text: "Report an Issue";
clicked => { open_new_issue_link(); }
}
Button {
text: "About Slint";
clicked => { about_slint.show(); }
}
Button {
text: "About Obliteration";
clicked => { about_dialog.show(); }
}
}
}
}
}
}
}

43
gui/slint/main/tabs.slint Normal file
View File

@ -0,0 +1,43 @@
import { TabWidget } from "std-widgets.slint";
import { DisplayTab } from "tabs/display.slint";
import { CpuTab } from "tabs/cpu.slint";
import { GamesTab } from "tabs/games.slint";
import { Game } from "../structs.slint";
export component Tabs {
in property <[string]> devices: [];
in property <[Game]> games: [];
pure callback select_game(int);
TabWidget {
Tab {
title: "Display";
//icon: @image-url("resources/darkmode/card-text-outline.svg");
VerticalLayout {
alignment: start;
DisplayTab { devices: devices; }
}
}
Tab {
title: "CPU";
//icon: @image-url("resources/darkmode/card-text-outline.svg");
VerticalLayout {
alignment: start;
CpuTab {}
}
}
Tab {
title: "Games";
//icon: @image-url("resources/darkmode/view-comfy.png");
VerticalLayout {
alignment: start;
GamesTab { games: games; }
}
}
}
}

View File

@ -0,0 +1,49 @@
import { Button, Slider, LineEdit } from "std-widgets.slint";
export component CpuTab {
HorizontalLayout {
VerticalLayout {
padding: 10px;
vertical-stretch: 0;
alignment: start;
Text {
text: "Count";
}
Slider {
value: 8;
minimum: 1;
maximum: 16;
}
Text {
text: "Changing this value to other than 8 may crash the game.";
}
}
VerticalLayout {
padding: 10px;
vertical-stretch: 0;
alignment: start;
Text {
text: "Debug";
}
HorizontalLayout {
spacing: 5px;
Text {
text: "Listen address";
vertical-alignment: center;
}
LineEdit {
text: "127.0.0.1";
max-height: 30px;
}
Button {
text: "Start";
}
}
Text {
text: "Specify a TCP address to listen for a debugger. The kernel will wait for a debugger to connect before start.";
}
}
}
}

View File

@ -0,0 +1,37 @@
import { ComboBox } from "std-widgets.slint";
export component DisplayTab {
in property <[string]> devices: [];
HorizontalLayout {
padding: 10px;
spacing: 10px;
VerticalLayout {
vertical-stretch: 0;
spacing: 10px;
alignment: start;
Text {
text: "Device";
}
ComboBox {
model: devices;
}
}
VerticalLayout {
VerticalLayout {
vertical-stretch: 0;
spacing: 10px;
alignment: start;
Text {
text: "Resolution";
}
ComboBox {
model: ["1280×720", "1920×1080", "3840×2160"];
current-value: "1920×1080";
}
}
}
}
}

View File

@ -0,0 +1,57 @@
import { Game } from "../../structs.slint";
export component GamesTab {
in-out property <[Game]> games: [];
VerticalLayout {
alignment: start;
HorizontalLayout {
alignment: stretch;
height: 30px;
Text {
text: "Name";
horizontal-alignment: center;
}
Text {
text: "ID";
width: 100px;
horizontal-alignment: center;
}
}
for game[i] in games: HorizontalLayout {
alignment: stretch;
HorizontalLayout {
alignment: stretch;
height: 30px;
Rectangle {
width: 15px;
Text {
text: i;
horizontal-alignment: center;
vertical-alignment: center;
}
}
Image {
source: game.icon;
width: 30px;
}
Text {
padding-left: 10px;
text: game.name;
vertical-alignment: center;
}
}
Text {
text: game.id;
vertical-alignment: center;
width: 80px;
}
}
}
}

6
gui/slint/structs.slint Normal file
View File

@ -0,0 +1,6 @@
export struct Game {
id: string,
name: string,
dir: string,
icon: image,
}

25
gui/src/args.rs Normal file
View File

@ -0,0 +1,25 @@
use clap::Parser;
use std::net::SocketAddrV4;
use std::path::Path;
#[derive(Debug, Parser)]
pub(crate) struct CliArgs {
#[arg(long, help = "Immediate launch the VMM in debug mode.")]
debug: Option<SocketAddrV4>,
#[arg(
long,
help = "Use the kernel image at the specified path instead of the default one."
)]
kernel: Option<Box<Path>>,
}
impl CliArgs {
pub fn debug_addr(&self) -> Option<SocketAddrV4> {
self.debug
}
pub fn kernel_path(&self) -> Option<&Path> {
self.kernel.as_deref()
}
}

View File

@ -2,11 +2,6 @@
use std::io::{Error, ErrorKind, Read, Write};
use std::net::TcpStream;
#[no_mangle]
pub unsafe extern "C" fn debug_client_free(d: *mut DebugClient) {
drop(Box::from_raw(d));
}
/// Encapsulate a debugger connection.
pub struct DebugClient {
sock: TcpStream,
@ -75,13 +70,10 @@ impl gdbstub::conn::Connection for DebugClient {
while !buf.is_empty() {
let written = match Write::write(&mut self.sock, buf) {
Ok(v) => v,
Err(e) => {
if matches!(e.kind(), ErrorKind::Interrupted | ErrorKind::WouldBlock) {
continue;
} else {
return Err(e);
}
Err(e) if matches!(e.kind(), ErrorKind::Interrupted | ErrorKind::WouldBlock) => {
continue;
}
Err(e) => return Err(e),
};
if written == 0 {

66
gui/src/debug/ffi.rs Normal file
View File

@ -0,0 +1,66 @@
use super::{DebugClient, DebugServer};
use crate::error::RustError;
use std::ffi::{c_char, CStr};
use std::ptr::null_mut;
#[no_mangle]
pub unsafe extern "C" fn debug_server_start(
addr: *const c_char,
err: *mut *mut RustError,
) -> *mut DebugServer {
// Get address.
let addr = match CStr::from_ptr(addr).to_str() {
Ok(v) => v,
Err(_) => {
*err = RustError::new("the specified address is not UTF-8").into_c();
return null_mut();
}
};
// Start server.
match DebugServer::new(addr) {
Ok(server) => Box::into_raw(Box::new(server)),
Err(e) => {
*err = RustError::wrap(e).into_c();
null_mut()
}
}
}
#[no_mangle]
pub unsafe extern "C" fn debug_server_free(s: *mut DebugServer) {
drop(Box::from_raw(s));
}
#[no_mangle]
pub unsafe extern "C" fn debug_server_addr(s: *mut DebugServer) -> *const c_char {
(*s).addr.as_ptr()
}
#[no_mangle]
pub unsafe extern "C" fn debug_server_socket(s: *mut DebugServer) -> isize {
#[cfg(unix)]
return std::os::fd::AsRawFd::as_raw_fd(&(*s).sock) as _;
#[cfg(windows)]
return std::os::windows::io::AsRawSocket::as_raw_socket(&(*s).sock) as _;
}
#[no_mangle]
pub unsafe extern "C" fn debug_server_accept(
s: *mut DebugServer,
err: *mut *mut RustError,
) -> *mut DebugClient {
match (*s).accept() {
Ok(client) => Box::into_raw(Box::new(client)),
Err(e) => {
*err = RustError::wrap(e).into_c();
null_mut()
}
}
}
#[no_mangle]
pub unsafe extern "C" fn debug_client_free(d: *mut DebugClient) {
drop(Box::from_raw(d));
}

View File

@ -1,84 +1,45 @@
// SPDX-License-Identifier: MIT OR Apache-2.0
pub use self::client::*;
use crate::error::RustError;
use std::ffi::{c_char, CStr, CString};
use std::net::TcpListener;
use std::ptr::null_mut;
use std::ffi::CString;
use std::net::{TcpListener, ToSocketAddrs};
use thiserror::Error;
mod client;
#[no_mangle]
pub unsafe extern "C" fn debug_server_start(
addr: *const c_char,
err: *mut *mut RustError,
) -> *mut DebugServer {
// Get address.
let addr = match CStr::from_ptr(addr).to_str() {
Ok(v) => v,
Err(_) => {
*err = RustError::new("the specified address is not UTF-8").into_c();
return null_mut();
}
};
// Start server.
let sock = match TcpListener::bind(addr) {
Ok(v) => v,
Err(e) => {
*err = RustError::with_source("couldn't bind to the specified address", e).into_c();
return null_mut();
}
};
// Get effective address to let the user know.
let addr = match sock.local_addr() {
Ok(v) => CString::new(v.to_string()).unwrap(),
Err(e) => {
*err = RustError::with_source("couldn't get server address", e).into_c();
return null_mut();
}
};
// Return server object.
Box::into_raw(Box::new(DebugServer { addr, sock }))
}
#[no_mangle]
pub unsafe extern "C" fn debug_server_free(s: *mut DebugServer) {
drop(Box::from_raw(s));
}
#[no_mangle]
pub unsafe extern "C" fn debug_server_addr(s: *mut DebugServer) -> *const c_char {
(*s).addr.as_ptr()
}
#[no_mangle]
pub unsafe extern "C" fn debug_server_socket(s: *mut DebugServer) -> isize {
#[cfg(unix)]
return std::os::fd::AsRawFd::as_raw_fd(&(*s).sock) as _;
#[cfg(windows)]
return std::os::windows::io::AsRawSocket::as_raw_socket(&(*s).sock) as _;
}
#[no_mangle]
pub unsafe extern "C" fn debug_server_accept(
s: *mut DebugServer,
err: *mut *mut RustError,
) -> *mut DebugClient {
match (*s).sock.accept() {
Ok((sock, _)) => Box::into_raw(Box::new(DebugClient::new(sock))),
Err(e) => {
*err = RustError::wrap(e).into_c();
null_mut()
}
}
}
#[cfg(feature = "qt")]
mod ffi;
/// TCP listener to accept a debugger connection.
pub struct DebugServer {
addr: CString,
sock: TcpListener,
}
impl DebugServer {
pub fn new(addr: impl ToSocketAddrs) -> Result<Self, StartDebugServerError> {
let sock = TcpListener::bind(addr).map_err(StartDebugServerError::BindFailed)?;
let addr = sock
.local_addr()
.map_err(StartDebugServerError::GetAddrFailed)?;
Ok(Self {
addr: CString::new(addr.to_string()).unwrap(),
sock,
})
}
pub fn accept(&self) -> std::io::Result<DebugClient> {
let (sock, _) = self.sock.accept()?;
Ok(DebugClient::new(sock))
}
}
#[derive(Debug, Error)]
pub enum StartDebugServerError {
#[error("couldn't bind to the specified address")]
BindFailed(#[source] std::io::Error),
#[error("couldn't get server address")]
GetAddrFailed(#[source] std::io::Error),
}

12
gui/src/error/ffi.rs Normal file
View File

@ -0,0 +1,12 @@
use super::RustError;
use std::ffi::c_char;
#[no_mangle]
pub unsafe extern "C" fn error_free(e: *mut RustError) {
drop(Box::from_raw(e));
}
#[no_mangle]
pub unsafe extern "C" fn error_message(e: *const RustError) -> *const c_char {
(*e).0.as_ptr()
}

View File

@ -1,17 +1,10 @@
// SPDX-License-Identifier: MIT OR Apache-2.0
use std::error::Error;
use std::ffi::{c_char, CString};
use std::ffi::CString;
use std::fmt::{Display, Write};
#[no_mangle]
pub unsafe extern "C" fn error_free(e: *mut RustError) {
drop(Box::from_raw(e));
}
#[no_mangle]
pub unsafe extern "C" fn error_message(e: *const RustError) -> *const c_char {
(*e).0.as_ptr()
}
#[cfg(feature = "qt")]
mod ffi;
/// Error object managed by Rust side.
pub struct RustError(CString);

View File

@ -11,6 +11,7 @@ mod string;
mod system;
mod vmm;
#[cfg(feature = "qt")]
#[no_mangle]
pub unsafe extern "C-unwind" fn set_panic_hook(
cx: *mut c_void,

177
gui/src/main.rs Normal file
View File

@ -0,0 +1,177 @@
use args::CliArgs;
use clap::Parser;
use debug::DebugServer;
use slint::{ComponentHandle, ModelExt, ModelRc, SharedString, VecModel};
use std::process::{ExitCode, Termination};
use thiserror::Error;
mod args;
mod debug;
mod error;
mod param;
mod pkg;
mod profile;
#[cfg(unix)]
mod rlim;
mod screen;
mod string;
mod system;
mod ui;
mod vmm;
fn main() -> AppExit {
let res = run();
AppExit::from(res)
}
fn run() -> Result<(), ApplicationError> {
#[cfg(unix)]
if let Err(e) = rlim::set_rlimit_nofile() {
ui::ErrorDialog::new()
.and_then(|error_dialog| {
error_dialog.set_message(SharedString::from(format!(
"Error setting rlimit: {}",
full_error_reason(e)
)));
error_dialog.run()
})
.inspect_err(|e| eprintln!("Error displaying error dialog: {e}"))
.unwrap();
}
let args = CliArgs::try_parse().map_err(ApplicationError::ParseArgs)?;
if let Some(debug_addr) = args.debug_addr() {
let debug_server = DebugServer::new(debug_addr)
.map_err(|e| ApplicationError::StartDebugServer(e, debug_addr))?;
let debug_client = debug_server
.accept()
.map_err(ApplicationError::CreateDebugClient)?;
}
let app = App::new()?;
app.run()?;
Ok(())
}
struct App {
main_window: ui::MainWindow,
games: ModelRc<ui::Game>,
profiles: ModelRc<SharedString>,
}
impl App {
fn new() -> Result<Self, ApplicationError> {
let main_window = ui::MainWindow::new().map_err(ApplicationError::CreateMainWindow)?;
let games = ModelRc::new(VecModel::from(Vec::new()));
main_window.set_games(games.clone());
let profiles = ModelRc::new(
VecModel::from(vec![profile::Profile::default()])
.map(|p| SharedString::from(String::from(p.name().to_string_lossy()))),
);
main_window.set_profiles(profiles.clone());
main_window.on_start_game(|_index| {
let screen = ui::Screen::new().unwrap();
screen.show().unwrap();
});
Ok(Self {
main_window,
games,
profiles,
})
}
fn run(&self) -> Result<(), ApplicationError> {
self.main_window
.run()
.map_err(ApplicationError::RunMainWindow)
}
}
fn full_error_reason<T>(e: T) -> String
where
T: std::error::Error,
{
use std::fmt::Write;
let mut msg = format!("{e}");
let mut src = e.source();
while let Some(e) = src {
write!(&mut msg, " -> {e}").unwrap();
src = e.source();
}
msg
}
pub enum AppExit {
Ok,
Err(ApplicationError),
}
impl Termination for AppExit {
fn report(self) -> ExitCode {
match self {
AppExit::Ok => ExitCode::SUCCESS,
AppExit::Err(e) => {
ui::ErrorDialog::new()
.and_then(|error_dialog| {
error_dialog.set_message(SharedString::from(format!(
"Error running application: {}",
full_error_reason(e)
)));
error_dialog.run()
})
.inspect_err(|e| eprintln!("Error displaying error dialog: {e}"))
.unwrap();
ExitCode::FAILURE
}
}
}
}
impl From<Result<(), ApplicationError>> for AppExit {
fn from(v: Result<(), ApplicationError>) -> Self {
match v {
Ok(_) => AppExit::Ok,
Err(e) => AppExit::Err(e),
}
}
}
#[derive(Debug, Error)]
pub enum ApplicationError {
#[error(transparent)]
ParseArgs(clap::Error),
#[error("failed to start debug server on {1}")]
StartDebugServer(
#[source] debug::StartDebugServerError,
std::net::SocketAddrV4,
),
#[error("failed to accept debug client")]
CreateDebugClient(#[source] std::io::Error),
#[error("failed to create main window")]
CreateMainWindow(#[source] slint::PlatformError),
#[error("failed to run main window")]
RunMainWindow(#[source] slint::PlatformError),
}

58
gui/src/param/ffi.rs Normal file
View File

@ -0,0 +1,58 @@
use crate::error::RustError;
use crate::string::strdup;
use param::Param;
use std::ffi::{c_char, CStr};
use std::ptr::null_mut;
#[no_mangle]
pub unsafe extern "C" fn param_open(file: *const c_char, error: *mut *mut RustError) -> *mut Param {
let path = CStr::from_ptr(file).to_str().unwrap();
match Param::open(path) {
Ok(param) => Box::into_raw(Box::new(param)),
Err(e) => {
*error = RustError::wrap(e).into_c();
null_mut()
}
}
}
#[no_mangle]
pub unsafe extern "C" fn param_close(p: *mut Param) {
drop(Box::from_raw(p));
}
#[no_mangle]
pub unsafe extern "C" fn param_app_ver_get(p: *const Param) -> *mut c_char {
(*p).app_ver().map(strdup).unwrap_or(null_mut())
}
#[no_mangle]
pub unsafe extern "C" fn param_category_get(p: *const Param) -> *mut c_char {
strdup((*p).category())
}
#[no_mangle]
pub unsafe extern "C" fn param_content_id_get(p: *const Param) -> *mut c_char {
strdup((*p).content_id())
}
#[no_mangle]
pub unsafe extern "C" fn param_short_content_id_get(p: *const Param) -> *mut c_char {
strdup((*p).shortcontent_id())
}
#[no_mangle]
pub unsafe extern "C" fn param_title_get(p: *const Param) -> *mut c_char {
(*p).title().map(strdup).unwrap_or(null_mut())
}
#[no_mangle]
pub unsafe extern "C" fn param_title_id_get(p: *const Param) -> *mut c_char {
strdup((*p).title_id())
}
#[no_mangle]
pub unsafe extern "C" fn param_version_get(p: *const Param) -> *mut c_char {
(*p).version().map(strdup).unwrap_or(null_mut())
}

View File

@ -1,69 +1,2 @@
use crate::error::RustError;
use crate::string::strdup;
use param::Param;
use std::ffi::{c_char, CStr};
use std::fs::File;
use std::ptr::null_mut;
#[no_mangle]
pub unsafe extern "C" fn param_open(file: *const c_char, error: *mut *mut RustError) -> *mut Param {
// Open file.
let file = match File::open(CStr::from_ptr(file).to_str().unwrap()) {
Ok(v) => v,
Err(e) => {
*error = RustError::with_source("couldn't open the specified file", e).into_c();
return null_mut();
}
};
// Parse.
let param = match Param::read(file) {
Ok(v) => v,
Err(e) => {
*error = RustError::with_source("couldn't read the specified file", e).into_c();
return null_mut();
}
};
Box::into_raw(param.into())
}
#[no_mangle]
pub unsafe extern "C" fn param_close(p: *mut Param) {
drop(Box::from_raw(p));
}
#[no_mangle]
pub unsafe extern "C" fn param_app_ver_get(p: *const Param) -> *mut c_char {
(*p).app_ver().map(strdup).unwrap_or(null_mut())
}
#[no_mangle]
pub unsafe extern "C" fn param_category_get(p: *const Param) -> *mut c_char {
strdup((*p).category())
}
#[no_mangle]
pub unsafe extern "C" fn param_content_id_get(p: *const Param) -> *mut c_char {
strdup((*p).content_id())
}
#[no_mangle]
pub unsafe extern "C" fn param_short_content_id_get(p: *const Param) -> *mut c_char {
strdup((*p).shortcontent_id())
}
#[no_mangle]
pub unsafe extern "C" fn param_title_get(p: *const Param) -> *mut c_char {
(*p).title().map(strdup).unwrap_or(null_mut())
}
#[no_mangle]
pub unsafe extern "C" fn param_title_id_get(p: *const Param) -> *mut c_char {
strdup((*p).title_id())
}
#[no_mangle]
pub unsafe extern "C" fn param_version_get(p: *const Param) -> *mut c_char {
(*p).version().map(strdup).unwrap_or(null_mut())
}
#[cfg(feature = "qt")]
mod ffi;

58
gui/src/pkg/ffi.rs Normal file
View File

@ -0,0 +1,58 @@
use super::ExtractProgress;
use crate::error::RustError;
use param::Param;
use pkg::Pkg;
use std::ffi::{c_char, c_void, CStr};
use std::path::Path;
use std::ptr::null_mut;
#[no_mangle]
pub unsafe extern "C" fn pkg_open(file: *const c_char, error: *mut *mut RustError) -> *mut Pkg {
let path = CStr::from_ptr(file).to_str().unwrap();
match Pkg::open(path) {
Ok(pkg) => Box::into_raw(Box::new(pkg)),
Err(e) => {
*error = RustError::wrap(e).into_c();
null_mut()
}
}
}
#[no_mangle]
pub unsafe extern "C" fn pkg_close(pkg: *mut Pkg) {
drop(Box::from_raw(pkg));
}
#[no_mangle]
pub unsafe extern "C" fn pkg_get_param(pkg: *const Pkg, error: *mut *mut RustError) -> *mut Param {
match (*pkg).get_param() {
Ok(param) => Box::into_raw(Box::new(param)),
Err(e) => {
*error = RustError::wrap(e).into_c();
null_mut()
}
}
}
#[no_mangle]
pub unsafe extern "C" fn pkg_extract(
pkg: *const Pkg,
dir: *const c_char,
status: extern "C" fn(*const c_char, usize, u64, u64, *mut c_void),
ud: *mut c_void,
) -> *mut RustError {
let root: &Path = CStr::from_ptr(dir).to_str().unwrap().as_ref();
let progress = ExtractProgress {
status,
ud,
root,
total: 0,
progress: 0,
};
match (*pkg).extract(root, progress) {
Ok(_) => null_mut(),
Err(e) => RustError::wrap(e).into_c(),
}
}

View File

@ -1,64 +1,11 @@
use crate::error::RustError;
use humansize::{SizeFormatter, DECIMAL};
use param::Param;
use pkg::{Pkg, PkgProgress};
use std::ffi::{c_char, c_void, CStr, CString};
use pkg::PkgProgress;
use std::ffi::{c_char, c_void, CString};
use std::path::Path;
use std::ptr::{null, null_mut};
use std::ptr::null;
#[no_mangle]
pub unsafe extern "C" fn pkg_open(file: *const c_char, error: *mut *mut RustError) -> *mut Pkg {
let path = CStr::from_ptr(file);
let pkg = match Pkg::open(path.to_str().unwrap()) {
Ok(v) => Box::new(v),
Err(e) => {
*error = RustError::wrap(e).into_c();
return null_mut();
}
};
Box::into_raw(pkg)
}
#[no_mangle]
pub unsafe extern "C" fn pkg_close(pkg: *mut Pkg) {
drop(Box::from_raw(pkg));
}
#[no_mangle]
pub unsafe extern "C" fn pkg_get_param(pkg: *const Pkg, error: *mut *mut RustError) -> *mut Param {
let param = match (*pkg).get_param() {
Ok(v) => Box::new(v),
Err(e) => {
*error = RustError::wrap(e).into_c();
return null_mut();
}
};
Box::into_raw(param)
}
#[no_mangle]
pub unsafe extern "C" fn pkg_extract(
pkg: *const Pkg,
dir: *const c_char,
status: extern "C" fn(*const c_char, usize, u64, u64, *mut c_void),
ud: *mut c_void,
) -> *mut RustError {
let root: &Path = CStr::from_ptr(dir).to_str().unwrap().as_ref();
let progress = ExtractProgress {
status,
ud,
root,
total: 0,
progress: 0,
};
match (*pkg).extract(root, progress) {
Ok(_) => null_mut(),
Err(e) => RustError::wrap(e).into_c(),
}
}
#[cfg(feature = "qt")]
mod ffi;
struct ExtractProgress<'a> {
status: extern "C" fn(*const c_char, usize, u64, u64, *mut c_void),
@ -87,7 +34,7 @@ impl<'a> PkgProgress for ExtractProgress<'a> {
let total = total.try_into().unwrap();
(self.status)(
b"Entries extraction completed\0".as_ptr().cast(), // https://github.com/mozilla/cbindgen/issues/927
b"Entries extraction completed\0".as_ptr().cast(),
0,
total,
total,
@ -128,7 +75,7 @@ impl<'a> PkgProgress for ExtractProgress<'a> {
fn pfs_completed(&mut self) {
(self.status)(
b"PFS extraction completed\0".as_ptr().cast(), // https://github.com/mozilla/cbindgen/issues/927
b"PFS extraction completed\0".as_ptr().cast(),
0,
self.total,
self.total,

77
gui/src/profile/ffi.rs Normal file
View File

@ -0,0 +1,77 @@
use super::{DisplayResolution, Profile};
use crate::error::RustError;
use crate::string::strdup;
use std::ffi::{c_char, CStr};
use std::path::Path;
use std::ptr::null_mut;
#[no_mangle]
pub unsafe extern "C" fn profile_new(name: *const c_char) -> *mut Profile {
Box::into_raw(Box::new(Profile {
name: CStr::from_ptr(name).to_owned(),
..Default::default()
}))
}
#[no_mangle]
pub unsafe extern "C" fn profile_load(
path: *const c_char,
err: *mut *mut RustError,
) -> *mut Profile {
// Check if path UTF-8.
let root = match CStr::from_ptr(path).to_str() {
Ok(v) => Path::new(v),
Err(_) => {
*err = RustError::new("the specified path is not UTF-8").into_c();
return null_mut();
}
};
match Profile::load(root) {
Ok(v) => Box::into_raw(Box::new(v)),
Err(e) => {
*err = RustError::wrap(e).into_c();
null_mut()
}
}
}
#[no_mangle]
pub unsafe extern "C" fn profile_free(p: *mut Profile) {
drop(Box::from_raw(p));
}
#[no_mangle]
pub unsafe extern "C" fn profile_id(p: *const Profile) -> *mut c_char {
strdup((*p).id.to_string())
}
#[no_mangle]
pub unsafe extern "C" fn profile_name(p: *const Profile) -> *const c_char {
(*p).name.as_ptr()
}
#[no_mangle]
pub unsafe extern "C" fn profile_display_resolution(p: *const Profile) -> DisplayResolution {
(*p).display_resolution
}
#[no_mangle]
pub unsafe extern "C" fn profile_set_display_resolution(p: *mut Profile, v: DisplayResolution) {
(*p).display_resolution = v;
}
#[no_mangle]
pub unsafe extern "C" fn profile_save(p: *const Profile, path: *const c_char) -> *mut RustError {
// Check if path UTF-8.
let root = match CStr::from_ptr(path).to_str() {
Ok(v) => Path::new(v),
Err(_) => return RustError::new("the specified path is not UTF-8").into_c(),
};
match (*p).save(root) {
Ok(_) => null_mut(),
Err(e) => RustError::wrap(e).into_c(),
}
}

View File

@ -1,120 +1,18 @@
use crate::error::RustError;
use crate::string::strdup;
use obconf::Config;
use serde::{Deserialize, Serialize};
use std::ffi::{c_char, CStr, CString};
use std::ffi::{CStr, CString};
use std::fs::File;
use std::num::NonZero;
use std::path::Path;
use std::ptr::null_mut;
use std::time::SystemTime;
use thiserror::Error;
use uuid::Uuid;
#[no_mangle]
pub unsafe extern "C" fn profile_new(name: *const c_char) -> *mut Profile {
Box::into_raw(Box::new(Profile {
name: CStr::from_ptr(name).to_owned(),
..Default::default()
}))
}
#[no_mangle]
pub unsafe extern "C" fn profile_load(
path: *const c_char,
err: *mut *mut RustError,
) -> *mut Profile {
// Check if path UTF-8.
let root = match CStr::from_ptr(path).to_str() {
Ok(v) => Path::new(v),
Err(_) => {
*err = RustError::new("the specified path is not UTF-8").into_c();
return null_mut();
}
};
// Open profile.bin.
let path = root.join("profile.bin");
let file = match File::open(&path) {
Ok(v) => v,
Err(e) => {
*err = RustError::with_source(format_args!("couldn't open {}", path.display()), e)
.into_c();
return null_mut();
}
};
// Load profile.bin.
let p = match ciborium::from_reader(file) {
Ok(v) => v,
Err(e) => {
*err = RustError::with_source(format_args!("couldn't load {}", path.display()), e)
.into_c();
return null_mut();
}
};
Box::into_raw(Box::new(p))
}
#[no_mangle]
pub unsafe extern "C" fn profile_free(p: *mut Profile) {
drop(Box::from_raw(p));
}
#[no_mangle]
pub unsafe extern "C" fn profile_id(p: *const Profile) -> *mut c_char {
strdup((*p).id.to_string())
}
#[no_mangle]
pub unsafe extern "C" fn profile_name(p: *const Profile) -> *const c_char {
(*p).name.as_ptr()
}
#[no_mangle]
pub unsafe extern "C" fn profile_display_resolution(p: *const Profile) -> DisplayResolution {
(*p).display_resolution
}
#[no_mangle]
pub unsafe extern "C" fn profile_set_display_resolution(p: *mut Profile, v: DisplayResolution) {
(*p).display_resolution = v;
}
#[no_mangle]
pub unsafe extern "C" fn profile_save(p: *const Profile, path: *const c_char) -> *mut RustError {
// Check if path UTF-8.
let root = match CStr::from_ptr(path).to_str() {
Ok(v) => Path::new(v),
Err(_) => return RustError::new("the specified path is not UTF-8").into_c(),
};
// Create a directory.
if let Err(e) = std::fs::create_dir_all(root) {
return RustError::with_source("couldn't create the specified path", e).into_c();
}
// Create profile.bin.
let path = root.join("profile.bin");
let file = match File::create(&path) {
Ok(v) => v,
Err(e) => {
return RustError::with_source(format_args!("couldn't create {}", path.display()), e)
.into_c()
}
};
// Write profile.bin.
if let Err(e) = ciborium::into_writer(&*p, file) {
return RustError::with_source(format_args!("couldn't write {}", path.display()), e)
.into_c();
}
null_mut()
}
#[cfg(feature = "qt")]
mod ffi;
/// Contains settings to launch the kernel.
#[derive(Deserialize, Serialize)]
#[derive(Clone, Deserialize, Serialize)]
#[serde(default)]
pub struct Profile {
id: Uuid,
@ -125,6 +23,33 @@ pub struct Profile {
}
impl Profile {
pub fn load(path: impl AsRef<Path>) -> Result<Self, LoadError> {
let path = path.as_ref().join("profile.bin");
let file = File::open(&path).map_err(LoadError::Open)?;
let profile = ciborium::from_reader(file).map_err(LoadError::Load)?;
Ok(profile)
}
pub fn save(&self, path: impl AsRef<Path>) -> Result<(), SaveError> {
let path = path.as_ref();
std::fs::create_dir_all(&path).map_err(SaveError::CreateDir)?;
let path = path.join("profile.bin");
let file = File::create(&path).map_err(SaveError::CreateFile)?;
ciborium::into_writer(self, file).map_err(SaveError::WriteFile)?;
Ok(())
}
pub fn name(&self) -> &CStr {
&self.name
}
pub fn kernel_config(&self) -> &Config {
&self.kernel_config
}
@ -155,3 +80,24 @@ pub enum DisplayResolution {
/// 3840×2160.
UltraHd,
}
#[derive(Debug, Error)]
pub enum LoadError {
#[error("couldn't open the profile file")]
Open(#[source] std::io::Error),
#[error("couldn't load the profile file")]
Load(#[source] ciborium::de::Error<std::io::Error>),
}
#[derive(Debug, Error)]
pub enum SaveError {
#[error("couldn't create the directory")]
CreateDir(#[source] std::io::Error),
#[error("couldn't create the profile file")]
CreateFile(#[source] std::io::Error),
#[error("couldn't write the profile file")]
WriteFile(#[source] ciborium::ser::Error<std::io::Error>),
}

34
gui/src/rlim.rs Normal file
View File

@ -0,0 +1,34 @@
use std::mem::MaybeUninit;
use thiserror::Error;
pub(crate) fn set_rlimit_nofile() -> Result<(), RlimitError> {
let mut rlim = MaybeUninit::uninit();
let ret = unsafe { libc::getrlimit(libc::RLIMIT_NOFILE, rlim.as_mut_ptr()) };
match ret {
0 => {
let mut rlim = unsafe { rlim.assume_init() };
if rlim.rlim_cur < rlim.rlim_max {
rlim.rlim_cur = rlim.rlim_max;
if unsafe { libc::setrlimit(libc::RLIMIT_NOFILE, &rlim) } != 0 {
return Err(RlimitError::SetRlimitFailed(std::io::Error::last_os_error()));
}
}
Ok(())
}
_ => Err(RlimitError::GetRlimitFailed(std::io::Error::last_os_error())),
}
}
#[derive(Debug, Error)]
pub(crate) enum RlimitError {
#[error("failed to get RLIMIT_NOFILE")]
GetRlimitFailed(#[source] std::io::Error),
#[error("failed to set RLIMIT_NOFILE")]
SetRlimitFailed(#[source] std::io::Error),
}

View File

@ -22,7 +22,7 @@ pub struct Metal {
}
impl Metal {
pub fn new(screen: &VmmScreen) -> Result<Self, MetalError> {
pub fn from_screen(screen: &VmmScreen) -> Result<Self, MetalError> {
// Get Metal device.
let device = match Device::system_default() {
Some(v) => v,

View File

@ -12,6 +12,12 @@ pub type Default = self::engine::Vulkan;
#[cfg(target_os = "macos")]
pub type Default = self::engine::Metal;
#[cfg(not(target_os = "macos"))]
pub type ScreenError = self::engine::VulkanError;
#[cfg(target_os = "macos")]
pub type ScreenError = self::engine::MetalError;
/// Encapsulates a platform-specific surface for drawing a VM screen.
pub trait Screen: 'static {
type Buffer: ScreenBuffer;

View File

@ -1,186 +0,0 @@
use ash::vk::{
AllocationCallbacks, Device, DeviceCreateInfo, ExtensionProperties, ExternalBufferProperties,
ExternalFenceProperties, ExternalSemaphoreProperties, Format, FormatProperties,
FormatProperties2, ImageCreateFlags, ImageFormatProperties, ImageFormatProperties2,
ImageTiling, ImageType, ImageUsageFlags, Instance, LayerProperties, PFN_vkVoidFunction,
PhysicalDevice, PhysicalDeviceExternalBufferInfo, PhysicalDeviceExternalFenceInfo,
PhysicalDeviceExternalSemaphoreInfo, PhysicalDeviceFeatures, PhysicalDeviceFeatures2,
PhysicalDeviceGroupProperties, PhysicalDeviceImageFormatInfo2, PhysicalDeviceMemoryProperties,
PhysicalDeviceMemoryProperties2, PhysicalDeviceProperties, PhysicalDeviceProperties2,
PhysicalDeviceSparseImageFormatInfo2, PhysicalDeviceToolProperties, QueueFamilyProperties,
QueueFamilyProperties2, Result, SampleCountFlags, SparseImageFormatProperties,
SparseImageFormatProperties2,
};
use std::ffi::c_char;
extern "system" {
#[link_name = "vmm_vk_create_device"]
pub fn create_device(
physical_device: PhysicalDevice,
p_create_info: *const DeviceCreateInfo<'_>,
p_allocator: *const AllocationCallbacks<'_>,
p_device: *mut Device,
) -> Result;
#[link_name = "vmm_vk_enumerate_device_extension_properties"]
pub fn enumerate_device_extension_properties(
physical_device: PhysicalDevice,
p_layer_name: *const c_char,
p_property_count: *mut u32,
p_properties: *mut ExtensionProperties,
) -> Result;
#[link_name = "vmm_vk_enumerate_device_layer_properties"]
pub fn enumerate_device_layer_properties(
physical_device: PhysicalDevice,
p_property_count: *mut u32,
p_properties: *mut LayerProperties,
) -> Result;
#[link_name = "vmm_vk_enumerate_physical_device_groups"]
pub fn enumerate_physical_device_groups(
instance: Instance,
p_physical_device_group_count: *mut u32,
p_physical_device_group_properties: *mut PhysicalDeviceGroupProperties<'_>,
) -> Result;
#[link_name = "vmm_vk_enumerate_physical_devices"]
pub fn enumerate_physical_devices(
instance: Instance,
p_physical_device_count: *mut u32,
p_physical_devices: *mut PhysicalDevice,
) -> Result;
#[link_name = "vmm_vk_get_device_proc_addr"]
pub fn get_device_proc_addr(device: Device, p_name: *const c_char) -> PFN_vkVoidFunction;
#[link_name = "vmm_vk_get_physical_device_external_buffer_properties"]
pub fn get_physical_device_external_buffer_properties(
physical_device: PhysicalDevice,
p_external_buffer_info: *const PhysicalDeviceExternalBufferInfo<'_>,
p_external_buffer_properties: *mut ExternalBufferProperties<'_>,
);
#[link_name = "vmm_vk_get_physical_device_external_fence_properties"]
pub fn get_physical_device_external_fence_properties(
physical_device: PhysicalDevice,
p_external_fence_info: *const PhysicalDeviceExternalFenceInfo<'_>,
p_external_fence_properties: *mut ExternalFenceProperties<'_>,
);
#[link_name = "vmm_vk_get_physical_device_external_semaphore_properties"]
pub fn get_physical_device_external_semaphore_properties(
physical_device: PhysicalDevice,
p_external_semaphore_info: *const PhysicalDeviceExternalSemaphoreInfo<'_>,
p_external_semaphore_properties: *mut ExternalSemaphoreProperties<'_>,
);
#[link_name = "vmm_vk_get_physical_device_features"]
pub fn get_physical_device_features(
physical_device: PhysicalDevice,
p_features: *mut PhysicalDeviceFeatures,
);
#[link_name = "vmm_vk_get_physical_device_features2"]
pub fn get_physical_device_features2(
physical_device: PhysicalDevice,
p_features: *mut PhysicalDeviceFeatures2<'_>,
);
#[link_name = "vmm_vk_get_physical_device_format_properties"]
pub fn get_physical_device_format_properties(
physical_device: PhysicalDevice,
format: Format,
p_format_properties: *mut FormatProperties,
);
#[link_name = "vmm_vk_get_physical_device_format_properties2"]
pub fn get_physical_device_format_properties2(
physical_device: PhysicalDevice,
format: Format,
p_format_properties: *mut FormatProperties2<'_>,
);
#[link_name = "vmm_vk_get_physical_device_image_format_properties"]
pub fn get_physical_device_image_format_properties(
physical_device: PhysicalDevice,
format: Format,
ty: ImageType,
tiling: ImageTiling,
usage: ImageUsageFlags,
flags: ImageCreateFlags,
p_image_format_properties: *mut ImageFormatProperties,
) -> Result;
#[link_name = "vmm_vk_get_physical_device_image_format_properties2"]
pub fn get_physical_device_image_format_properties2(
physical_device: PhysicalDevice,
p_image_format_info: *const PhysicalDeviceImageFormatInfo2<'_>,
p_image_format_properties: *mut ImageFormatProperties2<'_>,
) -> Result;
#[link_name = "vmm_vk_get_physical_device_memory_properties"]
pub fn get_physical_device_memory_properties(
physical_device: PhysicalDevice,
p_memory_properties: *mut PhysicalDeviceMemoryProperties,
);
#[link_name = "vmm_vk_get_physical_device_memory_properties2"]
pub fn get_physical_device_memory_properties2(
physical_device: PhysicalDevice,
p_memory_properties: *mut PhysicalDeviceMemoryProperties2<'_>,
);
#[link_name = "vmm_vk_get_physical_device_properties"]
pub fn get_physical_device_properties(
physical_device: PhysicalDevice,
p_properties: *mut PhysicalDeviceProperties,
);
#[link_name = "vmm_vk_get_physical_device_properties2"]
pub fn get_physical_device_properties2(
physical_device: PhysicalDevice,
p_properties: *mut PhysicalDeviceProperties2<'_>,
);
#[link_name = "vmm_vk_get_physical_device_queue_family_properties"]
pub fn get_physical_device_queue_family_properties(
physical_device: PhysicalDevice,
p_queue_family_property_count: *mut u32,
p_queue_family_properties: *mut QueueFamilyProperties,
);
#[link_name = "vmm_vk_get_physical_device_queue_family_properties2"]
pub fn get_physical_device_queue_family_properties2(
physical_device: PhysicalDevice,
p_queue_family_property_count: *mut u32,
p_queue_family_properties: *mut QueueFamilyProperties2<'_>,
);
#[link_name = "vmm_vk_get_physical_device_sparse_image_format_properties"]
pub fn get_physical_device_sparse_image_format_properties(
physical_device: PhysicalDevice,
format: Format,
ty: ImageType,
samples: SampleCountFlags,
usage: ImageUsageFlags,
tiling: ImageTiling,
p_property_count: *mut u32,
p_properties: *mut SparseImageFormatProperties,
);
#[link_name = "vmm_vk_get_physical_device_sparse_image_format_properties2"]
pub fn get_physical_device_sparse_image_format_properties2(
physical_device: PhysicalDevice,
p_format_info: *const PhysicalDeviceSparseImageFormatInfo2<'_>,
p_property_count: *mut u32,
p_properties: *mut SparseImageFormatProperties2<'_>,
);
#[link_name = "vmm_vk_get_physical_device_tool_properties"]
pub fn get_physical_device_tool_properties(
physical_device: PhysicalDevice,
p_tool_count: *mut u32,
p_tool_properties: *mut PhysicalDeviceToolProperties<'_>,
) -> Result;
}

View File

@ -1,28 +1,13 @@
// SPDX-License-Identifier: MIT OR Apache-2.0
use self::buffer::VulkanBuffer;
use self::ffi::{
create_device, enumerate_device_extension_properties, enumerate_device_layer_properties,
enumerate_physical_device_groups, enumerate_physical_devices, get_device_proc_addr,
get_physical_device_external_buffer_properties, get_physical_device_external_fence_properties,
get_physical_device_external_semaphore_properties, get_physical_device_features,
get_physical_device_features2, get_physical_device_format_properties,
get_physical_device_format_properties2, get_physical_device_image_format_properties,
get_physical_device_image_format_properties2, get_physical_device_memory_properties,
get_physical_device_memory_properties2, get_physical_device_properties,
get_physical_device_properties2, get_physical_device_queue_family_properties,
get_physical_device_queue_family_properties2,
get_physical_device_sparse_image_format_properties,
get_physical_device_sparse_image_format_properties2, get_physical_device_tool_properties,
};
use super::{Screen, ScreenBuffer};
use crate::vmm::VmmScreen;
use ash::vk::{DeviceCreateInfo, DeviceQueueCreateInfo, Handle, QueueFlags};
use ash::{Device, Instance, InstanceFnV1_0, InstanceFnV1_1, InstanceFnV1_3};
use ash::Device;
use std::sync::Arc;
use thiserror::Error;
mod buffer;
mod ffi;
/// Implementation of [`Screen`] using Vulkan.
pub struct Vulkan {
@ -31,44 +16,15 @@ pub struct Vulkan {
}
impl Vulkan {
pub fn new(screen: &VmmScreen) -> Result<Self, VulkanError> {
// Wrap VkInstance.
let instance = screen.vk_instance.try_into().unwrap();
let instance = ash::vk::Instance::from_raw(instance);
let instance = Instance::from_parts_1_3(
instance,
InstanceFnV1_0 {
destroy_instance: Self::destroy_instance,
enumerate_physical_devices,
get_physical_device_features,
get_physical_device_format_properties,
get_physical_device_image_format_properties,
get_physical_device_properties,
get_physical_device_queue_family_properties,
get_physical_device_memory_properties,
get_device_proc_addr,
create_device,
enumerate_device_extension_properties,
enumerate_device_layer_properties,
get_physical_device_sparse_image_format_properties,
},
InstanceFnV1_1 {
enumerate_physical_device_groups,
get_physical_device_features2,
get_physical_device_properties2,
get_physical_device_format_properties2,
get_physical_device_image_format_properties2,
get_physical_device_queue_family_properties2,
get_physical_device_memory_properties2,
get_physical_device_sparse_image_format_properties2,
get_physical_device_external_buffer_properties,
get_physical_device_external_fence_properties,
get_physical_device_external_semaphore_properties,
},
InstanceFnV1_3 {
get_physical_device_tool_properties,
},
);
pub fn from_screen(screen: &VmmScreen) -> Result<Self, VulkanError> {
let entry = ash::Entry::linked();
let instance = unsafe {
ash::Instance::load(
entry.static_fn(),
ash::vk::Instance::from_raw(screen.vk_instance.try_into().unwrap()),
)
};
// Wrap VkPhysicalDevice.
let physical = screen.vk_device.try_into().unwrap();
@ -76,32 +32,28 @@ impl Vulkan {
// Setup VkDeviceQueueCreateInfo.
let queue = unsafe { instance.get_physical_device_queue_family_properties(physical) }
.iter()
.into_iter()
.position(|p| p.queue_flags.contains(QueueFlags::GRAPHICS))
.unwrap();
let queues = [DeviceQueueCreateInfo::default()
.queue_family_index(queue.try_into().unwrap())
.queue_priorities(&[1.0])];
.ok_or(VulkanError::NoQueue)?;
let queue = queue
.try_into()
.map_err(|_| VulkanError::QueueOutOfBounds(queue))?;
let queues = DeviceQueueCreateInfo::default()
.queue_family_index(queue)
.queue_priorities(&[1.0]);
// Create logical device.
let device = DeviceCreateInfo::default().queue_create_infos(&queues);
let device = match unsafe { instance.create_device(physical, &device, None) } {
Ok(v) => v,
Err(e) => return Err(VulkanError::CreateDeviceFailed(e)),
};
let device = DeviceCreateInfo::default().queue_create_infos(std::slice::from_ref(&queues));
let device = unsafe { instance.create_device(physical, &device, None) }
.map_err(VulkanError::CreateDeviceFailed)?;
Ok(Self {
buffer: Arc::new(VulkanBuffer::new()),
device,
})
}
unsafe extern "system" fn destroy_instance(
_: ash::vk::Instance,
_: *const ash::vk::AllocationCallbacks<'_>,
) {
unimplemented!()
}
}
impl Drop for Vulkan {
@ -127,6 +79,12 @@ impl Screen for Vulkan {
/// Represents an error when [`Vulkan::new()`] fails.
#[derive(Debug, Error)]
pub enum VulkanError {
#[error("couldn't find suitable queue")]
NoQueue,
#[error("queue index #{0} out of bounds")]
QueueOutOfBounds(usize),
#[error("couldn't create a logical device")]
CreateDeviceFailed(#[source] ash::vk::Result),
}

2
gui/src/ui.rs Normal file
View File

@ -0,0 +1,2 @@
// This macro includes the generated Rust code from .slint files
slint::include_modules!();

139
gui/src/vmm/ffi.rs Normal file
View File

@ -0,0 +1,139 @@
use super::{DebugResult, KernelStop, Vmm, VmmEvent, VmmScreen};
use crate::debug::DebugClient;
use crate::error::RustError;
use crate::profile::Profile;
use crate::screen::Screen;
use gdbstub::common::Signal;
use gdbstub::stub::state_machine::GdbStubStateMachine;
use gdbstub::stub::MultiThreadStopReason;
use std::ffi::{c_char, c_void, CStr};
use std::ptr::null_mut;
use std::sync::atomic::Ordering;
#[no_mangle]
pub unsafe extern "C" fn vmm_start(
kernel: *const c_char,
screen: *const VmmScreen,
profile: *const Profile,
debugger: *mut DebugClient,
event: unsafe extern "C" fn(*const VmmEvent, *mut c_void),
cx: *mut c_void,
err: *mut *mut RustError,
) -> *mut Vmm {
// Consume the debugger now to prevent memory leak in case of error.
let debugger = if debugger.is_null() {
None
} else {
Some(*Box::from_raw(debugger))
};
// Check if path UTF-8.
let path = match CStr::from_ptr(kernel).to_str() {
Ok(v) => v,
Err(_) => {
*err = RustError::new("path of the kernel is not UTF-8").into_c();
return null_mut();
}
};
let screen = unsafe { &*screen };
let profile = unsafe { &*profile };
match Vmm::new(path, screen, profile, debugger, event, cx) {
Ok(vmm) => Box::into_raw(Box::new(vmm)),
Err(e) => {
*err = RustError::wrap(e).into_c();
null_mut()
}
}
}
#[no_mangle]
pub unsafe extern "C" fn vmm_free(vmm: *mut Vmm) {
drop(Box::from_raw(vmm));
}
#[no_mangle]
pub unsafe extern "C" fn vmm_draw(vmm: *mut Vmm) -> *mut RustError {
match (*vmm).screen.update() {
Ok(_) => null_mut(),
Err(e) => RustError::wrap(e).into_c(),
}
}
#[no_mangle]
pub unsafe extern "C" fn vmm_dispatch_debug(vmm: *mut Vmm, stop: *mut KernelStop) -> DebugResult {
// Consume stop reason now to prevent memory leak.
let vmm = &mut *vmm;
let mut stop = if stop.is_null() {
None
} else {
Some(Box::from_raw(stop).0)
};
loop {
// Check current state.
let r = match vmm.gdb.take().unwrap() {
GdbStubStateMachine::Idle(s) => match super::debug::dispatch_idle(&mut vmm.cpu, s) {
Ok(Ok(v)) => Ok(v),
Ok(Err(v)) => {
// No pending data from the debugger.
vmm.gdb = Some(v.into());
return DebugResult::Ok;
}
Err(e) => Err(e),
},
GdbStubStateMachine::Running(s) => {
match super::debug::dispatch_running(&mut vmm.cpu, s, stop.take()) {
Ok(Ok(v)) => Ok(v),
Ok(Err(v)) => {
// No pending data from the debugger.
vmm.gdb = Some(v.into());
return DebugResult::Ok;
}
Err(e) => Err(e),
}
}
GdbStubStateMachine::CtrlCInterrupt(s) => {
vmm.cpu.lock();
s.interrupt_handled(
&mut vmm.cpu,
Some(MultiThreadStopReason::Signal(Signal::SIGINT)),
)
.map_err(|e| RustError::with_source("couldn't handle CTRL+C from a debugger", e))
}
GdbStubStateMachine::Disconnected(_) => return DebugResult::Disconnected,
};
match r {
Ok(v) => vmm.gdb = Some(v),
Err(e) => return DebugResult::Error { reason: e.into_c() },
}
}
}
#[no_mangle]
pub unsafe extern "C" fn vmm_debug_socket(vmm: *mut Vmm) -> isize {
let s = match &mut (*vmm).gdb {
Some(v) => v,
None => return -1,
};
match s {
GdbStubStateMachine::Idle(s) => s.borrow_conn().socket() as _,
GdbStubStateMachine::Running(s) => s.borrow_conn().socket() as _,
GdbStubStateMachine::CtrlCInterrupt(s) => s.borrow_conn().socket() as _,
GdbStubStateMachine::Disconnected(s) => s.borrow_conn().socket() as _,
}
}
#[no_mangle]
pub unsafe extern "C" fn vmm_shutdown(vmm: *mut Vmm) {
(*vmm).shutdown.store(true, Ordering::Relaxed);
}
#[no_mangle]
pub unsafe extern "C" fn vmm_shutting_down(vmm: *mut Vmm) -> bool {
(*vmm).shutdown.load(Ordering::Relaxed)
}

View File

@ -24,6 +24,15 @@ pub type Default = self::os::Hvf;
#[cfg(target_os = "windows")]
pub type Default = self::os::Whp;
#[cfg(target_os = "linux")]
pub type HypervisorError = self::os::KvmError;
#[cfg(target_os = "macos")]
pub type HypervisorError = self::os::HvfError;
#[cfg(target_os = "windows")]
pub type HypervisorError = self::os::WhpError;
/// Underlying hypervisor (e.g. KVM on Linux).
pub trait Hypervisor: Send + Sync + 'static {
type Mapper: RamMapper;

View File

@ -10,16 +10,17 @@ use crate::debug::DebugClient;
use crate::error::RustError;
use crate::profile::Profile;
use crate::screen::Screen;
use gdbstub::common::Signal;
use cpu::GdbError;
use gdbstub::stub::state_machine::GdbStubStateMachine;
use gdbstub::stub::MultiThreadStopReason;
use gdbstub::stub::{GdbStubError, MultiThreadStopReason};
use kernel::{KernelError, ProgramHeaderError};
use obconf::{BootEnv, ConsoleType, Vm};
use std::cmp::max;
use std::error::Error;
use std::ffi::{c_char, c_void, CStr};
use std::io::Read;
use std::num::NonZero;
use std::ptr::null_mut;
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use thiserror::Error;
@ -29,532 +30,13 @@ use thiserror::Error;
mod arch;
mod cpu;
mod debug;
#[cfg(feature = "qt")]
mod ffi;
mod hv;
mod hw;
mod kernel;
mod ram;
#[no_mangle]
pub unsafe extern "C" fn vmm_start(
kernel: *const c_char,
screen: *const VmmScreen,
profile: *const Profile,
debugger: *mut DebugClient,
event: unsafe extern "C" fn(*const VmmEvent, *mut c_void),
cx: *mut c_void,
err: *mut *mut RustError,
) -> *mut Vmm {
// Consume the debugger now to prevent memory leak in case of error.
let debugger = match debugger.is_null() {
true => None,
false => Some(Box::from_raw(debugger)),
};
// Check if path UTF-8.
let path = match CStr::from_ptr(kernel).to_str() {
Ok(v) => v,
Err(_) => {
*err = RustError::new("path of the kernel is not UTF-8").into_c();
return null_mut();
}
};
// Open kernel image.
let mut file = match Kernel::open(path) {
Ok(v) => v,
Err(e) => {
*err = RustError::with_source(format_args!("couldn't open {path}"), e).into_c();
return null_mut();
}
};
// Get program header enumerator.
let hdrs = match file.program_headers() {
Ok(v) => v,
Err(e) => {
*err = RustError::with_source(
format_args!("couldn't start enumerating program headers of {path}"),
e,
)
.into_c();
return null_mut();
}
};
// Parse program headers.
let mut segments = Vec::new();
let mut dynamic = None;
let mut note = None;
for (index, item) in hdrs.enumerate() {
// Check if success.
let hdr = match item {
Ok(v) => v,
Err(e) => {
*err = RustError::with_source(
format_args!("couldn't read program header #{index} on {path}"),
e,
)
.into_c();
return null_mut();
}
};
// Process the header.
match hdr.p_type {
PT_LOAD => {
if hdr.p_filesz > TryInto::<u64>::try_into(hdr.p_memsz).unwrap() {
*err =
RustError::new(format!("invalid p_filesz on on PT_LOAD {index}")).into_c();
return null_mut();
}
segments.push(hdr);
}
PT_DYNAMIC => {
if dynamic.is_some() {
*err = RustError::new("multiple PT_DYNAMIC is not supported").into_c();
return null_mut();
}
dynamic = Some(hdr);
}
PT_NOTE => {
if note.is_some() {
*err = RustError::new("multiple PT_NOTE is not supported").into_c();
return null_mut();
}
note = Some(hdr);
}
PT_PHDR | PT_GNU_EH_FRAME | PT_GNU_STACK | PT_GNU_RELRO => {}
v => {
*err = RustError::new(format!("unknown p_type {v} on program header {index}"))
.into_c();
return null_mut();
}
}
}
segments.sort_unstable_by_key(|i| i.p_vaddr);
// Make sure the first PT_LOAD includes the ELF header.
match segments.first() {
Some(hdr) => {
if hdr.p_offset != 0 {
*err = RustError::new("the first PT_LOAD does not includes ELF header").into_c();
return null_mut();
}
}
None => {
*err = RustError::new("no any PT_LOAD on the kernel").into_c();
return null_mut();
}
}
// Check if PT_DYNAMIC exists.
let dynamic = match dynamic {
Some(v) => v,
None => {
*err = RustError::new("no PT_DYNAMIC segment on the kernel").into_c();
return null_mut();
}
};
// Check if PT_NOTE exists.
let note = match note {
Some(v) => v,
None => {
*err = RustError::new("no PT_NOTE segment on the kernel").into_c();
return null_mut();
}
};
// Seek to PT_NOTE.
let mut data = match file.segment_data(&note) {
Ok(v) => v,
Err(e) => {
*err = RustError::with_source(format_args!("couldn't seek to PT_NOTE on {path}"), e)
.into_c();
return null_mut();
}
};
// Parse PT_NOTE.
let mut vm_page_size = None;
for i in 0.. {
// Check remaining data.
if data.limit() == 0 {
break;
}
// Read note header.
let mut buf = [0u8; 4 * 3];
if let Err(e) = data.read_exact(&mut buf) {
*err = RustError::with_source(format_args!("couldn't read kernel note #{i} header"), e)
.into_c();
return null_mut();
}
// Parse note header.
let nlen: usize = u32::from_ne_bytes(buf[..4].try_into().unwrap())
.try_into()
.unwrap();
let dlen: usize = u32::from_ne_bytes(buf[4..8].try_into().unwrap())
.try_into()
.unwrap();
let ty = u32::from_ne_bytes(buf[8..].try_into().unwrap());
if nlen > 0xff {
*err = RustError::new(format!("name on kernel note #{i} is too large")).into_c();
return null_mut();
}
if dlen > 0xff {
*err = RustError::new(format!("description on kernel note #{i} is too large")).into_c();
return null_mut();
}
// Read note name + description.
let nalign = nlen.next_multiple_of(4);
let mut buf = vec![0u8; nalign + dlen];
if let Err(e) = data.read_exact(&mut buf) {
*err = RustError::with_source(format_args!("couldn't read kernel note #{i} data"), e)
.into_c();
return null_mut();
}
// Check name.
let name = match CStr::from_bytes_until_nul(&buf) {
Ok(v) if v.to_bytes_with_nul().len() == nlen => v,
_ => {
*err = RustError::new(format!("kernel note #{i} has invalid name")).into_c();
return null_mut();
}
};
if name.to_bytes() != b"obkrnl" {
continue;
}
// Parse description.
match ty {
0 => {
if vm_page_size.is_some() {
*err = RustError::new(format!("kernel note #{i} is duplicated")).into_c();
return null_mut();
}
vm_page_size = buf[nalign..]
.try_into()
.map(usize::from_ne_bytes)
.ok()
.and_then(NonZero::new)
.filter(|v| v.is_power_of_two());
if vm_page_size.is_none() {
*err =
RustError::new(format!("invalid description on kernel note #{i}")).into_c();
return null_mut();
}
}
v => {
*err = RustError::new(format!("unknown type {v} on kernel note #{i}")).into_c();
return null_mut();
}
}
}
// Check if page size exists.
let vm_page_size = match vm_page_size {
Some(v) => v,
None => {
*err = RustError::new("no page size in kernel note").into_c();
return null_mut();
}
};
// Get page size on the host.
let host_page_size = match get_page_size() {
Ok(v) => v,
Err(e) => {
*err = RustError::with_source("couldn't get host page size", e).into_c();
return null_mut();
}
};
// Get kernel memory size.
let mut len = 0;
for hdr in &segments {
if hdr.p_vaddr < len {
*err = RustError::new(format!(
"PT_LOAD at {:#x} is overlapped with the previous PT_LOAD",
hdr.p_vaddr
))
.into_c();
return null_mut();
}
len = match hdr.p_vaddr.checked_add(hdr.p_memsz) {
Some(v) => v,
None => {
*err = RustError::new(format!("invalid p_memsz on PT_LOAD at {:#x}", hdr.p_vaddr))
.into_c();
return null_mut();
}
};
}
// Round kernel memory size.
let block_size = max(vm_page_size, host_page_size);
let len = match len {
0 => {
*err = RustError::new("the kernel has PT_LOAD with zero length").into_c();
return null_mut();
}
v => match v.checked_next_multiple_of(block_size.get()) {
Some(v) => NonZero::new_unchecked(v),
None => {
*err = RustError::new("total size of PT_LOAD is too large").into_c();
return null_mut();
}
},
};
// Setup virtual devices.
let ram = NonZero::new(1024 * 1024 * 1024 * 8).unwrap();
let event = VmmEventHandler { fp: event, cx };
let devices = Arc::new(setup_devices(ram.get(), block_size, event));
// Setup hypervisor.
let mut hv = match self::hv::new(8, ram, block_size, debugger.is_some()) {
Ok(v) => v,
Err(e) => {
*err = RustError::with_source("couldn't setup a hypervisor", e).into_c();
return null_mut();
}
};
// Map the kernel.
let feats = hv.cpu_features().clone();
let mut ram = RamBuilder::new(hv.ram_mut());
let kern = match ram.alloc_kernel(len) {
Ok(v) => v,
Err(e) => {
*err = RustError::with_source("couldn't allocate RAM for the kernel", e).into_c();
return null_mut();
}
};
for hdr in &segments {
// Seek to segment data.
let mut data = match file.segment_data(hdr) {
Ok(v) => v,
Err(e) => {
*err = RustError::with_source(
format_args!("couldn't seek to offset {}", hdr.p_offset),
e,
)
.into_c();
return null_mut();
}
};
// Read segment data.
let mut seg = &mut kern[hdr.p_vaddr..(hdr.p_vaddr + hdr.p_memsz)];
match std::io::copy(&mut data, &mut seg) {
Ok(v) => {
if v != hdr.p_filesz {
*err = RustError::new(format!("{path} is incomplete")).into_c();
return null_mut();
}
}
Err(e) => {
*err = RustError::with_source(
format_args!("couldn't read kernet at offset {}", hdr.p_offset),
e,
)
.into_c();
return null_mut();
}
}
}
// Allocate stack.
if let Err(e) = ram.alloc_stack(NonZero::new(1024 * 1024 * 2).unwrap()) {
*err = RustError::with_source("couldn't allocate RAM for stack", e).into_c();
return null_mut();
}
// Allocate arguments.
let env = BootEnv::Vm(Vm {
vmm: devices.vmm().addr(),
console: devices.console().addr(),
host_page_size,
});
if let Err(e) = ram.alloc_args(env, (*profile).kernel_config().clone()) {
*err = RustError::with_source("couldn't allocate RAM for arguments", e).into_c();
return null_mut();
}
// Build RAM.
let map = match ram.build(&feats, vm_page_size, &devices, dynamic) {
Ok(v) => v,
Err(e) => {
*err = RustError::with_source("couldn't build RAM", e).into_c();
return null_mut();
}
};
// Setup screen.
let screen = match crate::screen::Default::new(&*screen) {
Ok(v) => v,
Err(e) => {
*err = RustError::with_source("couldn't setup a screen", e).into_c();
return null_mut();
}
};
// Setup CPU manager.
let shutdown = Arc::new(AtomicBool::new(false));
let mut cpu = CpuManager::new(
Arc::new(hv),
screen.buffer().clone(),
devices,
event,
shutdown.clone(),
);
// Setup GDB stub.
let gdb = match debugger
.map(|client| {
gdbstub::stub::GdbStub::new(*client)
.run_state_machine(&mut cpu)
.map_err(|e| RustError::with_source("couldn't setup a GDB stub", e))
})
.transpose()
{
Ok(v) => v,
Err(e) => {
*err = e.into_c();
return null_mut();
}
};
// Spawn main CPU.
cpu.spawn(map.kern_vaddr + file.entry(), Some(map), gdb.is_some());
// Create VMM.
let vmm = Vmm {
cpu,
screen,
gdb,
shutdown,
};
Box::into_raw(vmm.into())
}
#[no_mangle]
pub unsafe extern "C" fn vmm_free(vmm: *mut Vmm) {
drop(Box::from_raw(vmm));
}
#[no_mangle]
pub unsafe extern "C" fn vmm_draw(vmm: *mut Vmm) -> *mut RustError {
match (*vmm).screen.update() {
Ok(_) => null_mut(),
Err(e) => RustError::wrap(e).into_c(),
}
}
#[no_mangle]
pub unsafe extern "C" fn vmm_dispatch_debug(vmm: *mut Vmm, stop: *mut KernelStop) -> DebugResult {
// Consume stop reason now to prevent memory leak.
let vmm = &mut *vmm;
let mut stop = match stop.is_null() {
true => None,
false => Some(Box::from_raw(stop).0),
};
loop {
// Check current state.
let r = match vmm.gdb.take().unwrap() {
GdbStubStateMachine::Idle(s) => match self::debug::dispatch_idle(&mut vmm.cpu, s) {
Ok(Ok(v)) => Ok(v),
Ok(Err(v)) => {
// No pending data from the debugger.
vmm.gdb = Some(v.into());
return DebugResult::Ok;
}
Err(e) => Err(e),
},
GdbStubStateMachine::Running(s) => {
match self::debug::dispatch_running(&mut vmm.cpu, s, stop.take()) {
Ok(Ok(v)) => Ok(v),
Ok(Err(v)) => {
// No pending data from the debugger.
vmm.gdb = Some(v.into());
return DebugResult::Ok;
}
Err(e) => Err(e),
}
}
GdbStubStateMachine::CtrlCInterrupt(s) => {
vmm.cpu.lock();
s.interrupt_handled(
&mut vmm.cpu,
Some(MultiThreadStopReason::Signal(Signal::SIGINT)),
)
.map_err(|e| RustError::with_source("couldn't handle CTRL+C from a debugger", e))
}
GdbStubStateMachine::Disconnected(_) => return DebugResult::Disconnected,
};
match r {
Ok(v) => vmm.gdb = Some(v),
Err(e) => return DebugResult::Error { reason: e.into_c() },
}
}
}
#[no_mangle]
pub unsafe extern "C" fn vmm_debug_socket(vmm: *mut Vmm) -> isize {
let s = match &mut (*vmm).gdb {
Some(v) => v,
None => return -1,
};
match s {
GdbStubStateMachine::Idle(s) => s.borrow_conn().socket() as _,
GdbStubStateMachine::Running(s) => s.borrow_conn().socket() as _,
GdbStubStateMachine::CtrlCInterrupt(s) => s.borrow_conn().socket() as _,
GdbStubStateMachine::Disconnected(s) => s.borrow_conn().socket() as _,
}
}
#[no_mangle]
pub unsafe extern "C" fn vmm_shutdown(vmm: *mut Vmm) {
(*vmm).shutdown.store(true, Ordering::Relaxed);
}
#[no_mangle]
pub unsafe extern "C" fn vmm_shutting_down(vmm: *mut Vmm) -> bool {
(*vmm).shutdown.load(Ordering::Relaxed)
}
#[cfg(unix)]
fn get_page_size() -> Result<NonZero<usize>, std::io::Error> {
let v = unsafe { libc::sysconf(libc::_SC_PAGE_SIZE) };
@ -591,6 +73,283 @@ pub struct Vmm {
shutdown: Arc<AtomicBool>,
}
impl Vmm {
pub fn new(
kernel_path: impl AsRef<Path>,
screen: &VmmScreen,
profile: &Profile,
debugger: Option<DebugClient>,
event_handler: unsafe extern "C" fn(*const VmmEvent, *mut c_void),
cx: *mut c_void,
) -> Result<Self, VmmError> {
let path = kernel_path.as_ref();
// Open kernel image.
let mut kernel_img =
Kernel::open(path).map_err(|e| VmmError::OpenKernel(e, path.to_path_buf()))?;
// Get program header enumerator.
let hdrs = kernel_img
.program_headers()
.map_err(|e| VmmError::EnumerateProgramHeaders(e, path.to_path_buf()))?;
// Parse program headers.
let mut segments = Vec::new();
let mut dynamic = None;
let mut note = None;
for (index, item) in hdrs.enumerate() {
// Check if success.
let hdr =
item.map_err(|e| VmmError::ReadProgramHeader(e, index, path.to_path_buf()))?;
// Process the header.
match hdr.p_type {
PT_LOAD => {
if hdr.p_filesz > u64::try_from(hdr.p_memsz).unwrap() {
return Err(VmmError::InvalidFilesz(index));
}
segments.push(hdr);
}
PT_DYNAMIC => {
if dynamic.is_some() {
return Err(VmmError::MultipleDynamic);
}
dynamic = Some(hdr);
}
PT_NOTE => {
if note.is_some() {
return Err(VmmError::MultipleNote);
}
note = Some(hdr);
}
PT_PHDR | PT_GNU_EH_FRAME | PT_GNU_STACK | PT_GNU_RELRO => {}
v => return Err(VmmError::UnknownProgramHeaderType(v, index)),
}
}
segments.sort_unstable_by_key(|i| i.p_vaddr);
// Make sure the first PT_LOAD includes the ELF header.
let hdr = segments.first().ok_or(VmmError::NoLoadSegment)?;
if hdr.p_offset != 0 {
return Err(VmmError::ElfHeaderNotInFirstLoadSegment);
}
// Check if PT_DYNAMIC exists.
let dynamic = dynamic.ok_or(VmmError::NoDynamicSegment)?;
// Check if PT_NOTE exists.
let note = note.ok_or(VmmError::NoNoteSegment)?;
// Seek to PT_NOTE.
let mut data: std::io::Take<&mut std::fs::File> = kernel_img
.segment_data(&note)
.map_err(|e| VmmError::SeekToNote(e, path.to_path_buf()))?;
// Parse PT_NOTE.
let mut vm_page_size = None;
for i in 0u32.. {
// Check remaining data.
if data.limit() == 0 {
break;
}
// Read note header.
let mut buf = [0u8; 4 * 3];
data.read_exact(&mut buf)
.map_err(|e| VmmError::ReadKernelNote(e, i))?;
// Parse note header.
let nlen: usize = u32::from_ne_bytes(buf[..4].try_into().unwrap())
.try_into()
.unwrap();
let dlen: usize = u32::from_ne_bytes(buf[4..8].try_into().unwrap())
.try_into()
.unwrap();
let ty = u32::from_ne_bytes(buf[8..].try_into().unwrap());
if nlen > 0xff {
return Err(VmmError::NoteNameTooLarge(i));
}
if dlen > 0xff {
return Err(VmmError::InvalidNoteDescription(i));
}
// Read note name + description.
let nalign = nlen.next_multiple_of(4);
let mut buf = vec![0u8; nalign + dlen];
data.read_exact(&mut buf)
.map_err(|e| VmmError::ReadKernelNoteData(e, i))?;
// Check name.
let name = match CStr::from_bytes_until_nul(&buf) {
Ok(v) if v.to_bytes_with_nul().len() == nlen => v,
_ => return Err(VmmError::InvalidNoteName(i)),
};
if name.to_bytes() != b"obkrnl" {
continue;
}
// Parse description.
match ty {
0 => {
if vm_page_size.is_some() {
return Err(VmmError::DuplicateKernelNote(i));
}
vm_page_size = buf[nalign..]
.try_into()
.map(usize::from_ne_bytes)
.ok()
.and_then(NonZero::new)
.filter(|v| v.is_power_of_two());
if vm_page_size.is_none() {
return Err(VmmError::InvalidNoteDescription(i));
}
}
v => return Err(VmmError::UnknownKernelNoteType(v, i)),
}
}
// Check if page size exists.
let vm_page_size = vm_page_size.ok_or(VmmError::NoPageSizeInKernelNote)?;
// Get page size on the host.
let host_page_size = get_page_size().map_err(VmmError::GetHostPageSize)?;
// Get kernel memory size.
let mut len = 0;
for hdr in &segments {
if hdr.p_vaddr < len {
return Err(VmmError::OverlappedLoadSegment(hdr.p_vaddr));
}
len = hdr
.p_vaddr
.checked_add(hdr.p_memsz)
.ok_or(VmmError::InvalidPmemsz(hdr.p_vaddr))?;
}
// Round kernel memory size.
let block_size = max(vm_page_size, host_page_size);
let len = NonZero::new(len)
.ok_or(VmmError::ZeroLengthLoadSegment)?
.get()
.checked_next_multiple_of(block_size.get())
.ok_or(VmmError::TotalSizeTooLarge)?;
// Setup RAM.
let ram_size = NonZero::new(1024 * 1024 * 1024 * 8).unwrap();
// Setup virtual devices.
let event = VmmEventHandler {
event: event_handler,
cx,
};
let devices = Arc::new(setup_devices(ram_size.get(), block_size, event));
// Setup hypervisor.
let mut hv = unsafe { self::hv::new(8, ram_size, block_size, debugger.is_some()) }
.map_err(VmmError::SetupHypervisor)?;
// Map the kernel.
let feats = hv.cpu_features().clone();
let mut ram = RamBuilder::new(hv.ram_mut());
let kern = ram
.alloc_kernel(NonZero::new(len).unwrap())
.map_err(VmmError::AllocateRamForKernel)?;
for hdr in &segments {
// Seek to segment data.
let mut data = kernel_img
.segment_data(hdr)
.map_err(VmmError::SeekToOffset)?;
// Read segment data.
let mut seg = &mut kern[hdr.p_vaddr..(hdr.p_vaddr + hdr.p_memsz)];
match std::io::copy(&mut data, &mut seg) {
Ok(v) => {
if v != hdr.p_filesz {
return Err(VmmError::IncompleteKernel(path.to_path_buf()));
}
}
Err(e) => return Err(VmmError::ReadKernel(e, hdr.p_offset)),
}
}
// Allocate stack.
ram.alloc_stack(NonZero::new(1024 * 1024 * 2).unwrap())
.map_err(VmmError::AllocateRamForStack)?;
// Allocate arguments.
let env = BootEnv::Vm(Vm {
vmm: devices.vmm().addr(),
console: devices.console().addr(),
host_page_size,
});
ram.alloc_args(env, profile.kernel_config().clone())
.map_err(VmmError::AllocateRamForArgs)?;
// Build RAM.
let map = ram
.build(&feats, vm_page_size, &devices, dynamic)
.map_err(VmmError::BuildRam)?;
// Setup screen.
let screen = crate::screen::Default::from_screen(screen).map_err(VmmError::SetupScreen)?;
// Setup CPU manager.
let shutdown = Arc::new(AtomicBool::new(false));
let mut cpu_manager = CpuManager::new(
Arc::new(hv),
screen.buffer().clone(),
devices,
event,
shutdown.clone(),
);
// Setup GDB stub.
let gdb = debugger
.map(|client| {
gdbstub::stub::GdbStub::new(client)
.run_state_machine(&mut cpu_manager)
.map_err(VmmError::SetupGdbStub)
})
.transpose()?;
// Spawn main CPU.
cpu_manager.spawn(
map.kern_vaddr + kernel_img.entry(),
Some(map),
gdb.is_some(),
);
// Create VMM.
let vmm = Vmm {
cpu: cpu_manager,
screen,
gdb,
shutdown,
};
Ok(vmm)
}
}
impl Drop for Vmm {
fn drop(&mut self) {
// Set shutdown flag before dropping the other fields so their background thread can stop
@ -615,13 +374,13 @@ pub struct VmmScreen {
/// Encapsulates a function to handle VMM events.
#[derive(Clone, Copy)]
struct VmmEventHandler {
fp: unsafe extern "C" fn(*const VmmEvent, *mut c_void),
event: unsafe extern "C" fn(*const VmmEvent, *mut c_void),
cx: *mut c_void,
}
impl VmmEventHandler {
unsafe fn invoke(self, e: VmmEvent) {
(self.fp)(&e, self.cx)
(self.event)(&e, self.cx);
}
}
@ -684,6 +443,116 @@ pub enum DebugResult {
Error { reason: *mut RustError },
}
#[derive(Debug, Error)]
pub enum VmmError {
#[error("couldn't open kernel path {1}")]
OpenKernel(#[source] KernelError, PathBuf),
#[error("couldn't start enumerating program headers of {1}")]
EnumerateProgramHeaders(#[source] std::io::Error, PathBuf),
#[error("couldn't read program header #{1} on {2}")]
ReadProgramHeader(#[source] ProgramHeaderError, usize, PathBuf),
#[error("invalid p_filesz on on PT_LOAD {0}")]
InvalidFilesz(usize),
#[error("multiple PT_DYNAMIC is not supported")]
MultipleDynamic,
#[error("multiple PT_NOTE is not supported")]
MultipleNote,
#[error("unknown p_type {0} on program header {1}")]
UnknownProgramHeaderType(u32, usize),
#[error("the first PT_LOAD does not include ELF header")]
ElfHeaderNotInFirstLoadSegment,
#[error("no PT_LOAD on the kernel")]
NoLoadSegment,
#[error("no PT_DYNAMIC on the kernel")]
NoDynamicSegment,
#[error("no PT_NOTE on the kernel")]
NoNoteSegment,
#[error("couldn't seek to PT_NOTE on {1}")]
SeekToNote(#[source] std::io::Error, PathBuf),
#[error("couldn't read kernel note #{1}")]
ReadKernelNote(#[source] std::io::Error, u32),
#[error("name on kernel note #{0} is too large")]
NoteNameTooLarge(u32),
#[error("invalid description on kernel note #{0}")]
InvalidNoteDescription(u32),
#[error("couldn't read kernel note #{1} data")]
ReadKernelNoteData(#[source] std::io::Error, u32),
#[error("kernel note #{0} has invalid name")]
InvalidNoteName(u32),
#[error("kernel note #{0} is duplicated")]
DuplicateKernelNote(u32),
#[error("unknown type {0} on kernel note #{1}")]
UnknownKernelNoteType(u32, u32),
#[error("no page size in kernel note")]
NoPageSizeInKernelNote,
#[error("couldn't get host page size")]
GetHostPageSize(#[source] std::io::Error),
#[error("PT_LOAD at {0:#} is overlapped with the previous PT_LOAD")]
OverlappedLoadSegment(usize),
#[error("invalid p_memsz on PT_LOAD at {0:#}")]
InvalidPmemsz(usize),
#[error("the kernel has PT_LOAD with zero length")]
ZeroLengthLoadSegment,
#[error("total size of PT_LOAD is too large")]
TotalSizeTooLarge,
#[error("couldn't setup a hypervisor")]
SetupHypervisor(#[source] hv::HypervisorError),
#[error("couldn't allocate RAM for the kernel")]
AllocateRamForKernel(#[source] hv::RamError),
#[error("couldn't seek to offset")]
SeekToOffset(#[source] std::io::Error),
#[error("{0} is incomplete")]
IncompleteKernel(PathBuf),
#[error("couldn't read kernel at offset {1}")]
ReadKernel(#[source] std::io::Error, u64),
#[error("couldn't allocate RAM for stack")]
AllocateRamForStack(#[source] hv::RamError),
#[error("couldn't allocate RAM for arguments")]
AllocateRamForArgs(#[source] hv::RamError),
#[error("couldn't build RAM")]
BuildRam(#[source] ram::RamBuilderError),
#[error("couldn't setup a screen")]
SetupScreen(#[source] crate::screen::ScreenError),
#[error("couldn't setup a GDB stub")]
SetupGdbStub(
#[source] GdbStubError<GdbError, <DebugClient as gdbstub::conn::Connection>::Error>,
),
}
/// Represents an error when [`main_cpu()`] fails to reach event loop.
#[derive(Debug, Error)]
enum MainCpuError {

View File

@ -3,251 +3,3 @@
#include <QVulkanFunctions>
QVulkanFunctions *vkFunctions;
extern "C" VkResult vmm_vk_enumerate_physical_devices(
VkInstance instance,
uint32_t *p_physical_device_count,
VkPhysicalDevice *p_physical_devices)
{
return vkFunctions->vkEnumeratePhysicalDevices(
instance,
p_physical_device_count,
p_physical_devices);
}
extern "C" void vmm_vk_get_physical_device_features(
VkPhysicalDevice physical_device,
VkPhysicalDeviceFeatures *p_features)
{
vkFunctions->vkGetPhysicalDeviceFeatures(physical_device, p_features);
}
extern "C" void vmm_vk_get_physical_device_format_properties(
VkPhysicalDevice physical_device,
VkFormat format,
VkFormatProperties *p_format_properties)
{
vkFunctions->vkGetPhysicalDeviceFormatProperties(physical_device, format, p_format_properties);
}
extern "C" VkResult vmm_vk_get_physical_device_image_format_properties(
VkPhysicalDevice physical_device,
VkFormat format,
VkImageType ty,
VkImageTiling tiling,
VkImageUsageFlags usage,
VkImageCreateFlags flags,
VkImageFormatProperties *p_image_format_properties)
{
return vkFunctions->vkGetPhysicalDeviceImageFormatProperties(
physical_device,
format,
ty,
tiling,
usage,
flags,
p_image_format_properties);
}
extern "C" void vmm_vk_get_physical_device_properties(
VkPhysicalDevice physical_device,
VkPhysicalDeviceProperties *p_properties)
{
vkFunctions->vkGetPhysicalDeviceProperties(physical_device, p_properties);
}
extern "C" void vmm_vk_get_physical_device_queue_family_properties(
VkPhysicalDevice physical_device,
uint32_t *p_queue_family_property_count,
VkQueueFamilyProperties *p_queue_family_properties)
{
vkFunctions->vkGetPhysicalDeviceQueueFamilyProperties(
physical_device,
p_queue_family_property_count,
p_queue_family_properties);
}
extern "C" void vmm_vk_get_physical_device_memory_properties(
VkPhysicalDevice physical_device,
VkPhysicalDeviceMemoryProperties *p_memory_properties)
{
vkFunctions->vkGetPhysicalDeviceMemoryProperties(physical_device, p_memory_properties);
}
extern "C" PFN_vkVoidFunction vmm_vk_get_device_proc_addr(VkDevice device, const char *p_name)
{
return vkFunctions->vkGetDeviceProcAddr(device, p_name);
}
extern "C" VkResult vmm_vk_create_device(
VkPhysicalDevice physical_device,
const VkDeviceCreateInfo *p_create_info,
const VkAllocationCallbacks *p_allocator,
VkDevice *p_device)
{
return vkFunctions->vkCreateDevice(physical_device, p_create_info, p_allocator, p_device);
}
extern "C" VkResult vmm_vk_enumerate_device_extension_properties(
VkPhysicalDevice physical_device,
const char *p_layer_name,
uint32_t *p_property_count,
VkExtensionProperties *p_properties)
{
return vkFunctions->vkEnumerateDeviceExtensionProperties(
physical_device,
p_layer_name,
p_property_count,
p_properties);
}
extern "C" VkResult vmm_vk_enumerate_device_layer_properties(
VkPhysicalDevice physical_device,
uint32_t *p_property_count,
VkLayerProperties *p_properties)
{
return vkFunctions->vkEnumerateDeviceLayerProperties(
physical_device,
p_property_count,
p_properties);
}
extern "C" void vmm_vk_get_physical_device_sparse_image_format_properties(
VkPhysicalDevice physical_device,
VkFormat format,
VkImageType ty,
VkSampleCountFlagBits samples,
VkImageUsageFlags usage,
VkImageTiling tiling,
uint32_t *p_property_count,
VkSparseImageFormatProperties *p_properties)
{
vkFunctions->vkGetPhysicalDeviceSparseImageFormatProperties(
physical_device,
format,
ty,
samples,
usage,
tiling,
p_property_count,
p_properties);
}
extern "C" VkResult vmm_vk_enumerate_physical_device_groups(
VkInstance instance,
uint32_t *p_physical_device_group_count,
VkPhysicalDeviceGroupProperties *p_physical_device_group_properties)
{
return vkFunctions->vkEnumeratePhysicalDeviceGroups(
instance,
p_physical_device_group_count,
p_physical_device_group_properties);
}
extern "C" void vmm_vk_get_physical_device_features2(
VkPhysicalDevice physical_device,
VkPhysicalDeviceFeatures2 *p_features)
{
vkFunctions->vkGetPhysicalDeviceFeatures2(physical_device, p_features);
}
extern "C" void vmm_vk_get_physical_device_properties2(
VkPhysicalDevice physical_device,
VkPhysicalDeviceProperties2 *p_properties)
{
vkFunctions->vkGetPhysicalDeviceProperties2(physical_device, p_properties);
}
extern "C" void vmm_vk_get_physical_device_format_properties2(
VkPhysicalDevice physical_device,
VkFormat format,
VkFormatProperties2 *p_format_properties)
{
vkFunctions->vkGetPhysicalDeviceFormatProperties2(physical_device, format, p_format_properties);
}
extern "C" VkResult vmm_vk_get_physical_device_image_format_properties2(
VkPhysicalDevice physical_device,
const VkPhysicalDeviceImageFormatInfo2 *p_image_format_info,
VkImageFormatProperties2 *p_image_format_properties)
{
return vkFunctions->vkGetPhysicalDeviceImageFormatProperties2(
physical_device,
p_image_format_info,
p_image_format_properties);
}
extern "C" void vmm_vk_get_physical_device_queue_family_properties2(
VkPhysicalDevice physical_device,
uint32_t *p_queue_family_property_count,
VkQueueFamilyProperties2 *p_queue_family_properties)
{
vkFunctions->vkGetPhysicalDeviceQueueFamilyProperties2(
physical_device,
p_queue_family_property_count,
p_queue_family_properties);
}
extern "C" void vmm_vk_get_physical_device_memory_properties2(
VkPhysicalDevice physical_device,
VkPhysicalDeviceMemoryProperties2 *p_memory_properties)
{
vkFunctions->vkGetPhysicalDeviceMemoryProperties2(physical_device, p_memory_properties);
}
extern "C" void vmm_vk_get_physical_device_sparse_image_format_properties2(
VkPhysicalDevice physical_device,
const VkPhysicalDeviceSparseImageFormatInfo2 *p_format_info,
uint32_t *p_property_count,
VkSparseImageFormatProperties2 *p_properties)
{
vkFunctions->vkGetPhysicalDeviceSparseImageFormatProperties2(
physical_device,
p_format_info,
p_property_count,
p_properties);
}
extern "C" void vmm_vk_get_physical_device_external_buffer_properties(
VkPhysicalDevice physical_device,
const VkPhysicalDeviceExternalBufferInfo *p_external_buffer_info,
VkExternalBufferProperties *p_external_buffer_properties)
{
vkFunctions->vkGetPhysicalDeviceExternalBufferProperties(
physical_device,
p_external_buffer_info,
p_external_buffer_properties);
}
extern "C" void vmm_vk_get_physical_device_external_fence_properties(
VkPhysicalDevice physical_device,
const VkPhysicalDeviceExternalFenceInfo *p_external_fence_info,
VkExternalFenceProperties *p_external_fence_properties)
{
vkFunctions->vkGetPhysicalDeviceExternalFenceProperties(
physical_device,
p_external_fence_info,
p_external_fence_properties);
}
extern "C" void vmm_vk_get_physical_device_external_semaphore_properties(
VkPhysicalDevice physical_device,
const VkPhysicalDeviceExternalSemaphoreInfo *p_external_semaphore_info,
VkExternalSemaphoreProperties *p_external_semaphore_properties)
{
vkFunctions->vkGetPhysicalDeviceExternalSemaphoreProperties(
physical_device,
p_external_semaphore_info,
p_external_semaphore_properties);
}
extern "C" VkResult vmm_vk_get_physical_device_tool_properties(
VkPhysicalDevice physical_device,
uint32_t *p_tool_count,
VkPhysicalDeviceToolProperties *p_tool_properties)
{
return vkFunctions->vkGetPhysicalDeviceToolProperties(
physical_device,
p_tool_count,
p_tool_properties);
}

View File

@ -1,5 +1,7 @@
use byteorder::{ByteOrder, BE, LE};
use std::fs::File;
use std::io::{ErrorKind, Read, Seek, SeekFrom};
use std::path::Path;
use thiserror::Error;
/// A loaded param.sfo.
@ -16,6 +18,12 @@ pub struct Param {
}
impl Param {
pub fn open(path: impl AsRef<Path>) -> Result<Self, OpenError> {
let file = File::open(path).map_err(OpenError::OpenFailed)?;
Self::read(file).map_err(OpenError::ReadFailed)
}
pub fn read<R: Read + Seek>(mut raw: R) -> Result<Self, ReadError> {
// Seek to the beginning.
if let Err(e) = raw.seek(SeekFrom::Start(0)) {
@ -238,6 +246,15 @@ impl Param {
}
}
#[derive(Debug, Error)]
pub enum OpenError {
#[error("couldn't open the file")]
OpenFailed(#[source] std::io::Error),
#[error("couldn't read the file")]
ReadFailed(#[source] ReadError),
}
/// Errors for reading param.sfo.
#[derive(Debug, Error)]
pub enum ReadError {