Build clib (#893)

* Build clib

* more

* more

* more

* linker

* more

* more

* more

* more

* more

* more
This commit is contained in:
Logan McNaughton
2026-05-15 13:18:48 +02:00
committed by GitHub
parent 3a301c517d
commit c8f485cd93
6 changed files with 863 additions and 401 deletions
-8
View File
@@ -211,12 +211,6 @@ jobs:
mv target/${{ matrix.target }}/release/gopher64 target/${{ matrix.target }}/release/gopher64-cli
cargo bundle --release --target=${{ matrix.target }} --format osx
cp target/${{ matrix.target }}/release/gopher64-cli target/${{ matrix.target }}/release/bundle/osx/Gopher64.app/Contents/MacOS/gopher64-cli
install_name_tool -change /opt/homebrew/opt/freetype/lib/libfreetype.6.dylib \
@executable_path/../Frameworks/libfreetype.6.dylib target/${{ matrix.target }}/release/bundle/osx/Gopher64.app/Contents/MacOS/gopher64
install_name_tool -change /opt/homebrew/opt/freetype/lib/libfreetype.6.dylib \
@executable_path/../Frameworks/libfreetype.6.dylib target/${{ matrix.target }}/release/bundle/osx/Gopher64.app/Contents/MacOS/gopher64-cli
install_name_tool -change /opt/homebrew/opt/libpng/lib/libpng16.16.dylib \
@executable_path/../Frameworks/libpng16.16.dylib target/${{ matrix.target }}/release/bundle/osx/Gopher64.app/Contents/Frameworks/libfreetype.6.dylib
env:
NETPLAY_ID: ${{ secrets.NETPLAY_ID }}
MACOSX_DEPLOYMENT_TARGET: "15.0"
@@ -233,8 +227,6 @@ jobs:
- name: Sign Bundle Release
if: ${{ github.ref_type == 'tag' }}
run: |
codesign -f --timestamp --options runtime -s "${{ secrets.APPLE_DEVELOPER_ID }}" target/${{ matrix.target }}/release/bundle/osx/Gopher64.app/Contents/Frameworks/libpng16.16.dylib
codesign -f --timestamp --options runtime -s "${{ secrets.APPLE_DEVELOPER_ID }}" target/${{ matrix.target }}/release/bundle/osx/Gopher64.app/Contents/Frameworks/libfreetype.6.dylib
codesign -f --timestamp --options runtime -s "${{ secrets.APPLE_DEVELOPER_ID }}" target/${{ matrix.target }}/release/bundle/osx/Gopher64.app/Contents/Frameworks/libMoltenVK.dylib
codesign -f --entitlements data/macos/entitlements_child.plist --timestamp --options runtime -s "${{ secrets.APPLE_DEVELOPER_ID }}" target/${{ matrix.target }}/release/bundle/osx/Gopher64.app/Contents/MacOS/gopher64-cli
codesign -f --entitlements data/macos/entitlements_prod.plist --timestamp --options runtime -s "${{ secrets.APPLE_DEVELOPER_ID }}" target/${{ matrix.target }}/release/bundle/osx/Gopher64.app
Generated
+539 -71
View File
File diff suppressed because it is too large Load Diff
+6 -3
View File
@@ -8,6 +8,9 @@ rust-version = "1.95.0"
default = ["gui"]
gui = ["dep:rfd", "dep:slint", "dep:slint-build"]
[lib]
crate-type = ["cdylib", "rlib"]
[dependencies]
dirs = "6.0"
zip = "8.0"
@@ -16,11 +19,11 @@ chrono = "0.4"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
serde-big-array = "0.5"
slint = { version = "1.16", default-features = false, features = ["compat-1-2", "std", "unstable-winit-030", "backend-winit", "renderer-skia", "accessibility"], optional = true }
slint = { version = "1.16", default-features = false, features = ["compat-1-2", "std", "unstable-winit-030", "backend-winit", "renderer-femtovg-wgpu", "accessibility"], optional = true }
open = "5.3"
sha2 = "0.11"
sdl3-sys = { version = "0.6", features = ["build-from-source-static"] }
sdl3-ttf-sys = { version = "0.6", features = ["build-from-source-static", "no-sdlttf-harfbuzz", "no-sdlttf-plutosvg"] }
sdl3-ttf-sys = { version = "0.6", features = ["build-static-vendored", "no-sdlttf-harfbuzz", "no-sdlttf-plutosvg"] }
rfd = {version = "0.17", optional = true }
tokio = {version = "1.46", features = ["rt-multi-thread", "macros", "fs", "process"] }
spin_sleep = "1.3"
@@ -60,5 +63,5 @@ name = "Gopher64"
identifier = "io.github.gopher64.gopher64"
category = "public.app-category.games"
icon = ["data/icon/gopher64_128x128.png","data/icon/gopher64_256x256.png","data/icon/gopher64_512x512.png"]
osx_frameworks = ["/opt/homebrew/opt/molten-vk/lib/libMoltenVK.dylib","/opt/homebrew/opt/freetype/lib/libfreetype.6.dylib","/opt/homebrew/opt/libpng/lib/libpng16.16.dylib"]
osx_frameworks = ["/opt/homebrew/opt/molten-vk/lib/libMoltenVK.dylib"]
osx_minimum_system_version = "15.0"
-6
View File
@@ -135,9 +135,6 @@ fn main() {
.compile()
.unwrap();
} else if os == "macos" {
println!("cargo:rustc-link-search=native=/opt/homebrew/opt/freetype/lib");
println!("cargo:rustc-link-lib=dylib=freetype");
let output = std::process::Command::new("clang")
.args(["--print-runtime-dir"])
.output()
@@ -148,9 +145,6 @@ fn main() {
println!("cargo:rustc-link-search=native={}", runtime_dir);
println!("cargo:rustc-link-lib=static=clang_rt.osx");
}
if cfg!(not(feature = "gui")) && os == "linux" {
println!("cargo:rustc-link-lib=dylib=freetype");
}
volk_build.flag("-flto=thin");
rdp_build.flag("-flto=thin");
+317
View File
@@ -0,0 +1,317 @@
#![deny(warnings)]
mod cheats;
mod device;
mod netplay;
mod retroachievements;
mod savestates;
mod ui;
use clap::Parser;
use std::io::Error;
/// N64 emulator
#[derive(Parser, Debug)]
#[command(author, version=env!("GIT_DESCRIBE"), about, long_about = None, arg_required_else_help = if cfg!(feature = "gui") { false } else { true })]
struct Args {
game: Option<String>,
#[arg(short, long)]
fullscreen: bool,
#[arg(long)]
overclock: Option<bool>,
#[arg(long)]
disable_expansion_pak: Option<bool>,
#[arg(long, value_name = "CHEATS_FILE", hide = true)]
cheats: Option<String>,
#[arg(long, value_name = "NETPLAY_PEER_ADDR", hide = true)]
netplay_peer_addr: Option<String>,
#[arg(long, value_name = "NETPLAY_PLAYER_NUMBER", hide = true)]
netplay_player_number: Option<u8>,
#[arg(
long,
value_name = "PROFILE_NAME",
help = "Create a new input profile (keyboard/gamepad mappings)"
)]
configure_input_profile: Option<String>,
#[arg(long, help = "Use DirectInput when configuring a new input profile")]
use_dinput: bool,
#[arg(
long,
value_name = "DEADZONE_PERCENTAGE",
help = "Used along with --configure-input-profile to set the deadzone for analog sticks"
)]
deadzone: Option<i32>,
#[arg(
long,
value_name = "PROFILE_NAME",
help = "Must also specify --port. Used to bind a previously created profile to a port"
)]
bind_input_profile: Option<String>,
#[arg(
long,
help = "Lists connected controllers which can be used in --assign-controller"
)]
list_controllers: bool,
#[arg(
long,
value_name = "CONTROLLER_NUMBER",
help = "Must also specify --port. Used to assign a controller listed in --list-controllers to a port"
)]
assign_controller: Option<i32>,
#[arg(
long,
value_name = "PORT",
help = "Valid values: 1-4. To be used alongside --bind-input-profile and --assign-controller"
)]
port: Option<usize>,
#[arg(
long,
help = "Clear all input profile bindings and controller assignments"
)]
clear_input_bindings: bool,
#[arg(
long,
value_name = "SLOT",
help = "Load savestate from slot 0-9 when starting the game"
)]
load_state: Option<u32>,
#[arg(
long = "ra-username",
value_name = "USERNAME",
help = "Username for RetroAchievements"
)]
ra_username: Option<String>,
#[arg(
long = "ra-token",
value_name = "TOKEN",
help = "Token for RetroAchievements",
hide = true
)]
ra_token: Option<String>,
#[arg(
long = "ra-password",
value_name = "PASSWORD",
help = "Password for RetroAchievements"
)]
ra_password: Option<String>,
#[arg(
long = "ra-hardcore",
help = "Enable Hardcore mode for RetroAchievements"
)]
ra_hardcore: bool,
#[arg(
long = "ra-challenge",
help = "Enable Challenge Indicators for RetroAchievements"
)]
ra_challenge: bool,
#[arg(
long = "ra-leaderboard",
help = "Enable Leaderboard Trackers for RetroAchievements"
)]
ra_leaderboard: bool,
#[arg(long = "discord-rich-presence", help = "Enable Discord Rich Presence")]
discord_rich_presence: bool,
}
pub async fn run() -> std::io::Result<()> {
let dirs = ui::get_dirs();
std::fs::create_dir_all(dirs.config_dir)?;
std::fs::create_dir_all(dirs.cache_dir)?;
std::fs::create_dir_all(dirs.data_dir.join("saves"))?;
std::fs::create_dir_all(dirs.data_dir.join("states"))?;
ui::sdl_hints();
let args = Args::parse();
if let Some(game) = args.game {
let file_path = std::path::Path::new(&game);
let Some(rom_contents) = device::get_rom_contents(file_path) else {
return Err(Error::other(format!(
"Could not read ROM file: {}",
file_path.display()
)));
};
if let Some(slot) = args.load_state
&& slot > 9
{
return Err(Error::other("Savestate slot must be between 0 and 9"));
}
let mut device = device::Device::new();
device.ui.config.recent_roms.retain(|x| *x != game);
device.ui.config.recent_roms.insert(0, game);
device.ui.config.recent_roms.truncate(5);
if args.fullscreen {
device.ui.video.fullscreen = true;
} else {
device.ui.video.fullscreen = device.ui.config.video.fullscreen;
}
let overclock = args
.overclock
.unwrap_or(device.ui.config.emulation.overclock);
let disable_expansion_pak = args
.disable_expansion_pak
.unwrap_or(device.ui.config.emulation.disable_expansion_pak);
let cheats = if let Some(cheats_file) = args.cheats
&& let Ok(data) = std::fs::read(cheats_file)
&& let Ok(cheats) = serde_json::from_slice(&data)
{
cheats
} else {
let game_crc = ui::storage::get_game_crc(&rom_contents);
ui::config::Cheats::new()
.cheats
.get(&game_crc)
.cloned()
.unwrap_or_default()
};
let mut shutdown_tx = None;
let mut usb_handle = None;
if let Some(peer_addr) = args.netplay_peer_addr
&& let Some(player_number) = args.netplay_player_number
{
device.netplay = Some(netplay::init(peer_addr.parse().unwrap(), player_number));
} else {
for i in 0..4 {
if device.ui.config.input.transfer_pak[i]
&& !device.ui.config.input.gb_rom_path[i].is_empty()
&& !device.ui.config.input.gb_ram_path[i].is_empty()
&& let Ok(rom) = std::fs::read(&device.ui.config.input.gb_rom_path[i])
&& let Ok(ram) = std::fs::read(&device.ui.config.input.gb_ram_path[i])
{
device::controller::gbcart::init(&mut device.transferpaks[i].cart, &rom, &ram);
}
}
if device.ui.config.emulation.usb {
(shutdown_tx, usb_handle, device.ui.usb) = ui::usb::init();
}
if let Some(username) = args.ra_username {
retroachievements::init_client(
if cfg!(ra_hardcore_enabled) {
args.ra_hardcore
} else {
false
},
args.ra_challenge,
args.ra_leaderboard,
);
let (tx, rx) = tokio::sync::oneshot::channel::<bool>();
if let Some(password) = args.ra_password {
retroachievements::login_user(username, password, tx);
} else if let Some(token) = args.ra_token {
retroachievements::login_token_user(username, token, tx);
} else {
tx.send(false).unwrap();
}
rx.await.unwrap();
}
}
let (discord_watch_tx, discord_handle) = retroachievements::load_game(
&rom_contents,
rom_contents.len(),
args.discord_rich_presence,
)
.await;
device::run_game(
&mut device,
&rom_contents,
ui::GameSettings {
overclock,
disable_expansion_pak,
cheats,
load_savestate_slot: args.load_state,
},
);
retroachievements::shutdown_client(discord_watch_tx, discord_handle).await;
if device.netplay.is_some() {
netplay::close(&mut device);
} else {
for i in 0..4 {
if device.ui.config.input.transfer_pak[i]
&& !device.ui.config.input.gb_ram_path[i].is_empty()
&& !device.transferpaks[i].cart.ram.is_empty()
{
device::controller::gbcart::save(
&mut device.transferpaks[i].cart,
device.vi.elapsed_time as i64,
&device.ui.config.input.gb_ram_path[i],
);
}
}
}
if let Some(shutdown_tx) = &shutdown_tx {
ui::usb::close(shutdown_tx, usb_handle).await;
}
} else if std::env::args().count() > 1 {
let mut config = ui::config::Config::new();
if let Some(profile) = args.configure_input_profile {
ui::input::configure_input_profile(
&mut config,
profile,
args.use_dinput,
args.deadzone.unwrap_or(ui::input::DEADZONE_DEFAULT),
);
ui::sdl_close();
return Ok(());
} else {
if args.clear_input_bindings {
ui::input::clear_bindings(&mut config);
return Ok(());
}
if let Some(port) = args.port
&& !(1..=4).contains(&port)
{
return Err(Error::other("Port must be between 1 and 4"));
}
ui::sdl_init(sdl3_sys::init::SDL_INIT_GAMEPAD);
if args.list_controllers {
let controllers = ui::input::get_controller_names();
for (i, controller) in controllers.iter().enumerate() {
println!("Controller {i}: {controller}");
}
} else {
if let Some(assign_controller) = args.assign_controller {
let Some(port) = args.port else {
ui::sdl_close();
return Err(Error::other("Must specify port number"));
};
ui::input::assign_controller(&mut config, assign_controller - 1, port);
}
if let Some(profile) = args.bind_input_profile {
let Some(port) = args.port else {
ui::sdl_close();
return Err(Error::other("Must specify port number"));
};
ui::input::bind_input_profile(&mut config, profile, port);
}
}
}
} else {
#[cfg(feature = "gui")]
{
retroachievements::init_client(false, false, false);
ui::gui::app_window();
retroachievements::shutdown_client(None, None).await;
}
}
ui::sdl_close();
Ok(())
}
+1 -313
View File
@@ -3,319 +3,7 @@
#[global_allocator]
static GLOBAL: mimalloc::MiMalloc = mimalloc::MiMalloc;
mod cheats;
mod device;
mod netplay;
mod retroachievements;
mod savestates;
mod ui;
use clap::Parser;
use std::io::Error;
/// N64 emulator
#[derive(Parser, Debug)]
#[command(author, version=env!("GIT_DESCRIBE"), about, long_about = None, arg_required_else_help = if cfg!(feature = "gui") { false } else { true })]
struct Args {
game: Option<String>,
#[arg(short, long)]
fullscreen: bool,
#[arg(long)]
overclock: Option<bool>,
#[arg(long)]
disable_expansion_pak: Option<bool>,
#[arg(long, value_name = "CHEATS_FILE", hide = true)]
cheats: Option<String>,
#[arg(long, value_name = "NETPLAY_PEER_ADDR", hide = true)]
netplay_peer_addr: Option<String>,
#[arg(long, value_name = "NETPLAY_PLAYER_NUMBER", hide = true)]
netplay_player_number: Option<u8>,
#[arg(
long,
value_name = "PROFILE_NAME",
help = "Create a new input profile (keyboard/gamepad mappings)"
)]
configure_input_profile: Option<String>,
#[arg(long, help = "Use DirectInput when configuring a new input profile")]
use_dinput: bool,
#[arg(
long,
value_name = "DEADZONE_PERCENTAGE",
help = "Used along with --configure-input-profile to set the deadzone for analog sticks"
)]
deadzone: Option<i32>,
#[arg(
long,
value_name = "PROFILE_NAME",
help = "Must also specify --port. Used to bind a previously created profile to a port"
)]
bind_input_profile: Option<String>,
#[arg(
long,
help = "Lists connected controllers which can be used in --assign-controller"
)]
list_controllers: bool,
#[arg(
long,
value_name = "CONTROLLER_NUMBER",
help = "Must also specify --port. Used to assign a controller listed in --list-controllers to a port"
)]
assign_controller: Option<i32>,
#[arg(
long,
value_name = "PORT",
help = "Valid values: 1-4. To be used alongside --bind-input-profile and --assign-controller"
)]
port: Option<usize>,
#[arg(
long,
help = "Clear all input profile bindings and controller assignments"
)]
clear_input_bindings: bool,
#[arg(
long,
value_name = "SLOT",
help = "Load savestate from slot 0-9 when starting the game"
)]
load_state: Option<u32>,
#[arg(
long = "ra-username",
value_name = "USERNAME",
help = "Username for RetroAchievements"
)]
ra_username: Option<String>,
#[arg(
long = "ra-token",
value_name = "TOKEN",
help = "Token for RetroAchievements",
hide = true
)]
ra_token: Option<String>,
#[arg(
long = "ra-password",
value_name = "PASSWORD",
help = "Password for RetroAchievements"
)]
ra_password: Option<String>,
#[arg(
long = "ra-hardcore",
help = "Enable Hardcore mode for RetroAchievements"
)]
ra_hardcore: bool,
#[arg(
long = "ra-challenge",
help = "Enable Challenge Indicators for RetroAchievements"
)]
ra_challenge: bool,
#[arg(
long = "ra-leaderboard",
help = "Enable Leaderboard Trackers for RetroAchievements"
)]
ra_leaderboard: bool,
#[arg(long = "discord-rich-presence", help = "Enable Discord Rich Presence")]
discord_rich_presence: bool,
}
#[tokio::main(worker_threads = 4)]
async fn main() -> std::io::Result<()> {
let dirs = ui::get_dirs();
std::fs::create_dir_all(dirs.config_dir)?;
std::fs::create_dir_all(dirs.cache_dir)?;
std::fs::create_dir_all(dirs.data_dir.join("saves"))?;
std::fs::create_dir_all(dirs.data_dir.join("states"))?;
ui::sdl_hints();
let args = Args::parse();
if let Some(game) = args.game {
let file_path = std::path::Path::new(&game);
let Some(rom_contents) = device::get_rom_contents(file_path) else {
return Err(Error::other(format!(
"Could not read ROM file: {}",
file_path.display()
)));
};
if let Some(slot) = args.load_state
&& slot > 9
{
return Err(Error::other("Savestate slot must be between 0 and 9"));
}
let mut device = device::Device::new();
device.ui.config.recent_roms.retain(|x| *x != game);
device.ui.config.recent_roms.insert(0, game);
device.ui.config.recent_roms.truncate(5);
if args.fullscreen {
device.ui.video.fullscreen = true;
} else {
device.ui.video.fullscreen = device.ui.config.video.fullscreen;
}
let overclock = args
.overclock
.unwrap_or(device.ui.config.emulation.overclock);
let disable_expansion_pak = args
.disable_expansion_pak
.unwrap_or(device.ui.config.emulation.disable_expansion_pak);
let cheats = if let Some(cheats_file) = args.cheats
&& let Ok(data) = std::fs::read(cheats_file)
&& let Ok(cheats) = serde_json::from_slice(&data)
{
cheats
} else {
let game_crc = ui::storage::get_game_crc(&rom_contents);
ui::config::Cheats::new()
.cheats
.get(&game_crc)
.cloned()
.unwrap_or_default()
};
let mut shutdown_tx = None;
let mut usb_handle = None;
if let Some(peer_addr) = args.netplay_peer_addr
&& let Some(player_number) = args.netplay_player_number
{
device.netplay = Some(netplay::init(peer_addr.parse().unwrap(), player_number));
} else {
for i in 0..4 {
if device.ui.config.input.transfer_pak[i]
&& !device.ui.config.input.gb_rom_path[i].is_empty()
&& !device.ui.config.input.gb_ram_path[i].is_empty()
&& let Ok(rom) = std::fs::read(&device.ui.config.input.gb_rom_path[i])
&& let Ok(ram) = std::fs::read(&device.ui.config.input.gb_ram_path[i])
{
device::controller::gbcart::init(&mut device.transferpaks[i].cart, &rom, &ram);
}
}
if device.ui.config.emulation.usb {
(shutdown_tx, usb_handle, device.ui.usb) = ui::usb::init();
}
if let Some(username) = args.ra_username {
retroachievements::init_client(
if cfg!(ra_hardcore_enabled) {
args.ra_hardcore
} else {
false
},
args.ra_challenge,
args.ra_leaderboard,
);
let (tx, rx) = tokio::sync::oneshot::channel::<bool>();
if let Some(password) = args.ra_password {
retroachievements::login_user(username, password, tx);
} else if let Some(token) = args.ra_token {
retroachievements::login_token_user(username, token, tx);
} else {
tx.send(false).unwrap();
}
rx.await.unwrap();
}
}
let (discord_watch_tx, discord_handle) = retroachievements::load_game(
&rom_contents,
rom_contents.len(),
args.discord_rich_presence,
)
.await;
device::run_game(
&mut device,
&rom_contents,
ui::GameSettings {
overclock,
disable_expansion_pak,
cheats,
load_savestate_slot: args.load_state,
},
);
retroachievements::shutdown_client(discord_watch_tx, discord_handle).await;
if device.netplay.is_some() {
netplay::close(&mut device);
} else {
for i in 0..4 {
if device.ui.config.input.transfer_pak[i]
&& !device.ui.config.input.gb_ram_path[i].is_empty()
&& !device.transferpaks[i].cart.ram.is_empty()
{
device::controller::gbcart::save(
&mut device.transferpaks[i].cart,
device.vi.elapsed_time as i64,
&device.ui.config.input.gb_ram_path[i],
);
}
}
}
if let Some(shutdown_tx) = &shutdown_tx {
ui::usb::close(shutdown_tx, usb_handle).await;
}
} else if std::env::args().count() > 1 {
let mut config = ui::config::Config::new();
if let Some(profile) = args.configure_input_profile {
ui::input::configure_input_profile(
&mut config,
profile,
args.use_dinput,
args.deadzone.unwrap_or(ui::input::DEADZONE_DEFAULT),
);
ui::sdl_close();
return Ok(());
} else {
if args.clear_input_bindings {
ui::input::clear_bindings(&mut config);
return Ok(());
}
if let Some(port) = args.port
&& !(1..=4).contains(&port)
{
return Err(Error::other("Port must be between 1 and 4"));
}
ui::sdl_init(sdl3_sys::init::SDL_INIT_GAMEPAD);
if args.list_controllers {
let controllers = ui::input::get_controller_names();
for (i, controller) in controllers.iter().enumerate() {
println!("Controller {i}: {controller}");
}
} else {
if let Some(assign_controller) = args.assign_controller {
let Some(port) = args.port else {
ui::sdl_close();
return Err(Error::other("Must specify port number"));
};
ui::input::assign_controller(&mut config, assign_controller - 1, port);
}
if let Some(profile) = args.bind_input_profile {
let Some(port) = args.port else {
ui::sdl_close();
return Err(Error::other("Must specify port number"));
};
ui::input::bind_input_profile(&mut config, profile, port);
}
}
}
} else {
#[cfg(feature = "gui")]
{
retroachievements::init_client(false, false, false);
ui::gui::app_window();
retroachievements::shutdown_client(None, None).await;
}
}
ui::sdl_close();
Ok(())
gopher64::run().await
}