diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 00000000..487e7668 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,4 @@ +*target/ +.github/ +docs/ +*Dockerfile \ No newline at end of file diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 8ddd55bb..af366b49 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -56,4 +56,8 @@ jobs: run: cargo make libretro_desktop - name: Build libretro android - run: cargo make libretro_android \ No newline at end of file + run: cargo make libretro_android + + - name: Build nintendo switch + if: ${{ matrix.os == 'ubuntu-latest' }} # This fails on windows since it does not have docker installed + run: cargo make nx \ No newline at end of file diff --git a/.gitignore b/.gitignore index aa0054a4..44062012 100644 --- a/.gitignore +++ b/.gitignore @@ -40,9 +40,6 @@ bld/ # Uncomment if you have tasks that create the project's static files in wwwroot #wwwroot/ -#vscode -.vscode/ - # Visual Studio 2017 auto generated files Generated\ Files/ diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 00000000..f32e13bf --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,13 @@ +{ + "C_Cpp.clang_format_fallbackStyle": "WebKit", + "C_Cpp.default.includePath": [ + "${DEVKITPRO}\\libnx\\include", + "${DEVKITPRO}\\devkitA64\\aarch64-none-elf\\include", + "${DEVKITPRO}\\devkitA64\\lib\\gcc\\aarch64-none-elf\\15.1.0\\include" + ], + "rust-analyzer.cargo.extraEnv": { + "RPI": "4" + }, + "rust-analyzer.cargo.allTargets": false, + "rust-analyzer.check.workspace": true, +} \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 49641a93..dfb3f1c2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -846,6 +846,12 @@ version = "0.2.155" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" +[[package]] +name = "libm" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de" + [[package]] name = "libretro-sys" version = "0.1.1" @@ -881,7 +887,7 @@ checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c" [[package]] name = "magenboy_common" -version = "4.1.0" +version = "4.2.0" dependencies = [ "cfg-if", "chrono", @@ -889,13 +895,14 @@ dependencies = [ "crossbeam-channel", "fast_image_resize", "fern", + "libm", "log", "magenboy_core", ] [[package]] name = "magenboy_core" -version = "4.1.0" +version = "4.2.0" dependencies = [ "cfg-if", "criterion", @@ -907,7 +914,7 @@ dependencies = [ [[package]] name = "magenboy_libretro" -version = "4.1.0" +version = "4.2.0" dependencies = [ "libretro-sys", "log", @@ -915,9 +922,18 @@ dependencies = [ "magenboy_core", ] +[[package]] +name = "magenboy_nx" +version = "4.2.0" +dependencies = [ + "log", + "magenboy_common", + "magenboy_core", +] + [[package]] name = "magenboy_rpi" -version = "4.1.0" +version = "4.2.0" dependencies = [ "arrayvec", "bitfield-struct", @@ -933,7 +949,7 @@ dependencies = [ [[package]] name = "magenboy_sdl" -version = "4.1.0" +version = "4.2.0" dependencies = [ "cfg-if", "crossbeam-channel", diff --git a/Cargo.toml b/Cargo.toml index 832b4ae1..a444aa79 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,11 +6,12 @@ members = [ "core", "rpi", "common", - "libretro" + "libretro", + "nx" ] [workspace.package] -version = "4.1.0" +version = "4.2.0" authors = ["alloncm "] rust-version = "1.73" # cause of cargo-ndk used to build for android platform edition = "2021" diff --git a/Makefile.toml b/Makefile.toml index 34d6c899..0543769c 100644 --- a/Makefile.toml +++ b/Makefile.toml @@ -45,7 +45,7 @@ install_crate_args=["--locked", "--version", "0.2.5"] command = "cross" args = ["build", "--release", "--target", "armv7-unknown-linux-gnueabihf", "--bin", "rpios","--no-default-features", "--features", "os"] -[tasks.pre-rpibm] +[tasks.nightly-install] command = "rustup" args = ["toolchain", "install", "--profile", "minimal", "--no-self-update", "${nightly_version}"] @@ -55,7 +55,7 @@ install_crate = "cargo-binutils" install_crate_args=["--locked", "--version", "0.3.6"] command = "rust-objcopy" args = ["target/armv7a-none-eabihf/release/baremetal", "-O", "binary", "kernel7.img"] -dependencies = ["pre-rpibm", "build_rpi_baremetal","install_llvm_tools"] +dependencies = ["nightly-install", "build_rpi_baremetal","install_llvm_tools"] [tasks.build_rpi_baremetal] toolchain = "${nightly_version}" @@ -85,4 +85,8 @@ dependencies = ["add_android_target"] [tasks.add_android_target] command = "rustup" -args = ["target", "add", "aarch64-linux-android"] \ No newline at end of file +args = ["target", "add", "aarch64-linux-android"] + +[tasks.nx] +command = "docker" +args = ["build", "--progress=plain", ".", "--file", "nx/Dockerfile", "--target", "export", "--output=.", "--build-arg", "NIGHTLY=${nightly_version}"] \ No newline at end of file diff --git a/README.md b/README.md index 8a0833b9..d29e74b9 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ Install `cargo-make` ```sh cargo install cargo-make ``` -verify you have docker or podman installed +Verify you have cmake and docker or podman installed ### Desktop @@ -73,6 +73,10 @@ rustup component add llvm-tools-preview See - [LibretroDocs](docs/Libretro.md) +### Nintendo Switch + +See - [NxDocs](docs/Nx.md) + ## Running ### Desktop diff --git a/common/Cargo.toml b/common/Cargo.toml index 86b45b61..3ccffae5 100644 --- a/common/Cargo.toml +++ b/common/Cargo.toml @@ -10,13 +10,15 @@ edition.workspace = true magenboy_core = {path = "../core"} log = "0.4" cfg-if = "1" +libm = "0.2.15" crossbeam-channel = {version = "0.5", optional = true} fern = {version = "0.6", optional = true} chrono = {version = "0.4", optional = true} [features] -std = ["chrono", "fern", "crossbeam-channel"] +std = ["chrono", "fern", "crossbeam-channel", "alloc"] dbg = ["std"] +alloc = [] [dev-dependencies] criterion = "0.3" diff --git a/common/src/audio/audio_resampler.rs b/common/src/audio/audio_resampler.rs index 22cb39e8..6ed046a0 100644 --- a/common/src/audio/audio_resampler.rs +++ b/common/src/audio/audio_resampler.rs @@ -1,3 +1,5 @@ +use alloc::{string::String, vec::Vec}; + use magenboy_core::apu::audio_device::{Sample, AudioDevice, StereoSample, BUFFER_SIZE}; pub trait AudioResampler{ diff --git a/common/src/audio/manual_audio_resampler.rs b/common/src/audio/manual_audio_resampler.rs index db38be64..e11fda80 100644 --- a/common/src/audio/manual_audio_resampler.rs +++ b/common/src/audio/manual_audio_resampler.rs @@ -1,6 +1,8 @@ +use alloc::vec::Vec; + use magenboy_core::apu::audio_device::{BUFFER_SIZE, StereoSample}; -use super::audio_resampler::AudioResampler; +use super::audio_resampler::AudioResampler; pub struct ManualAudioResampler{ to_skip:u32, @@ -17,9 +19,11 @@ impl AudioResampler for ManualAudioResampler{ // Calling round in order to get the nearest integer and resample as precise as possible let div = original_frequency as f32 / target_frequency as f32; - let lower_to_skip = div.floor() as u32; - let upper_to_skip = div.ceil() as u32; - let mut reminder = div.fract(); + // Sicne we dont have many f32 methods without std we are implementing them ourself + let lower_to_skip = libm::floorf(div) as u32; + let upper_to_skip = libm::ceilf(div) as u32; + let mut reminder = div - (div as u32 as f32); // Acts as f32::fracts (since inputs are unsigned) + let (to_skip, alt_to_skip) = if reminder < 0.5{ (lower_to_skip, upper_to_skip) } @@ -29,7 +33,7 @@ impl AudioResampler for ManualAudioResampler{ }; if lower_to_skip == 0{ - std::panic!("target freqency is too high: {}", target_frequency); + core::panic!("target freqency is too high: {}", target_frequency); } ManualAudioResampler{ diff --git a/common/src/lib.rs b/common/src/lib.rs index 0090a992..545fe05c 100644 --- a/common/src/lib.rs +++ b/common/src/lib.rs @@ -4,18 +4,24 @@ cfg_if::cfg_if!{ if #[cfg(feature = "std")] { pub mod mbc_handler; pub mod mpmc_gfx_device; pub mod logging; + pub mod initialization; + pub use initialization::*; +}} + +cfg_if::cfg_if!{ if #[cfg(feature = "alloc")] { + extern crate alloc; + pub mod audio{ mod audio_resampler; mod manual_audio_resampler; pub use audio_resampler::*; pub use manual_audio_resampler::*; } - pub mod initialization; - pub use initialization::*; }} pub mod menu; pub mod joypad_menu; pub mod interpolation; +pub mod synchronization; pub const VERSION:&str = env!("MAGENBOY_VERSION"); \ No newline at end of file diff --git a/common/src/menu.rs b/common/src/menu.rs index 9969456b..a982fb75 100644 --- a/common/src/menu.rs +++ b/common/src/menu.rs @@ -4,6 +4,8 @@ pub struct MenuOption>{ pub prompt:S } +#[repr(u32)] +#[derive(Debug, Clone, Copy)] pub enum EmulatorMenuOption{ Resume, Restart, diff --git a/rpi/src/syncronization.rs b/common/src/synchronization.rs similarity index 100% rename from rpi/src/syncronization.rs rename to common/src/synchronization.rs diff --git a/core/src/utils/static_allocator.rs b/core/src/utils/static_allocator.rs index fd819f99..2e4bd127 100644 --- a/core/src/utils/static_allocator.rs +++ b/core/src/utils/static_allocator.rs @@ -37,20 +37,22 @@ impl StaticAllocator{ } pub fn alloc(&mut self, layout: Layout) -> NonNull { - let allocation_address = self.buffer_ptr as usize + self.allocation_size; - let aligned_address = Self::align_address(allocation_address, layout.align); - self.allocation_size += layout.size + (aligned_address - allocation_address); - - if self.allocation_size > self.buffer_size{ + if self.allocation_size + layout.align + layout.size > self.buffer_size { core::panic!("Allocation failed, allocator is out of static memory, pool size: {}, allocation req: {}", self.buffer_size, layout.size); } + let allocation_address = unsafe{self.buffer_ptr.add(self.allocation_size)}; + let aligned_address = Self::align_address(allocation_address, layout.align); + self.allocation_size += layout.size + (unsafe{aligned_address.offset_from(allocation_address)} as usize); + return NonNull::new(aligned_address as *mut u8).expect("Null ptr detected"); } - fn align_address(address:usize, alignment:usize)->usize{ - let reminder = address % alignment; - return if reminder != 0 {address - reminder + alignment} else {address}; + fn align_address(address:*mut u8, alignment:usize)->*mut u8 { + let reminder = address as usize % alignment; + return if reminder != 0 { + unsafe{address.sub(reminder).add(alignment)} + } else {address}; } } diff --git a/docs/Nx.md b/docs/Nx.md new file mode 100644 index 00000000..12d1f1fe --- /dev/null +++ b/docs/Nx.md @@ -0,0 +1,45 @@ +# NX (Nintendo Switch) + +## Setup your Switch + +I have used this [guide](https://switch.hacks.guide/user_guide/getting_started.html) to setup my Switch to be able to run homebrew apps + +## Build + +Verify `docker` or `podman` are installed as the build process use the devkitpro image to get reliable access to the tools. + +Run: +```sh +cargo make nx +``` + +## Install + +Copy the `magenboy.nro` to the switch or upload it with nxlink (needs to be installed with the devkitpro toolchain) + +nxlink command after pressing `Y` in the homebrew app: + +```sh +nxlink -s path_to_magenboy.nro +``` + +if it fails verify the PC can ping the switch IP and if it can add `-a ip_address` to the command flags. + +## Run + +The app expect to find ROMS in a folder called `roms` in the path if the app. + +### Usage + +| Joypad | Joycon | +| ---------- | ----------- | +| A | A or X | +| B | B or Y | +| Start | + | +| Select | - | +| Dpad Up | Up arrow | +| Dpad Down | Down arrow | +| Dpad Left | Left arrow | +| Dpad Right | Right arrow | + +The menu can be opened by pressing `L` + `R` \ No newline at end of file diff --git a/nx/Cargo.toml b/nx/Cargo.toml new file mode 100644 index 00000000..62e78057 --- /dev/null +++ b/nx/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "magenboy_nx" +edition.workspace = true +version.workspace = true +authors.workspace = true +rust-version.workspace = true + +[lib] +crate-type = ["staticlib"] + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +magenboy_core = { path = "../core", features = ["apu"] } +magenboy_common = { path = "../common", features = ["alloc"] } +log = "0.4" \ No newline at end of file diff --git a/nx/Dockerfile b/nx/Dockerfile new file mode 100644 index 00000000..7f133fc4 --- /dev/null +++ b/nx/Dockerfile @@ -0,0 +1,35 @@ +# We are installing the Rust toolchain so the version does not matter +FROM rust:latest AS builder + +# Nightly version - entered as a build argument +ARG NIGHTLY + +RUN rustup toolchain install ${NIGHTLY} +RUN rustup +${NIGHTLY} component add rust-src + +WORKDIR /magenboy + +COPY . . + +RUN cargo +${NIGHTLY} build --release --package magenboy_nx --target aarch64-nintendo-switch-freestanding \ + -Z build-std=core,compiler_builtins,alloc -Z build-std-features=compiler-builtins-mem + +FROM devkitpro/devkita64 AS final + +WORKDIR /magenboy_nx + +COPY nx/Makefile ./ +COPY nx/src ./src + +# Copy the built Rust library from the builder stage +COPY --from=builder /magenboy/target/ target/ + +# Needs to be run as the same RUN statement since the shell session is not shared between RUN statements +# Without this the Makefile will not be able to find the version and authors +RUN export VERSION=$(cat target/aarch64-nintendo-switch-freestanding/release/version.txt) && \ + export AUTHORS=$(cat target/aarch64-nintendo-switch-freestanding/release/authors.txt) && \ + make --always-make + +# Export the built files using a scratch image, this is the best practice for multi-stage builds +FROM scratch AS export +COPY --from=final /magenboy_nx/build/ /target/nx \ No newline at end of file diff --git a/nx/Makefile b/nx/Makefile new file mode 100644 index 00000000..cbf2bd4a --- /dev/null +++ b/nx/Makefile @@ -0,0 +1,225 @@ +#--------------------------------------------------------------------------------- +.SUFFIXES: +#--------------------------------------------------------------------------------- + +ifeq ($(strip $(DEVKITPRO)),) +$(error "Please set DEVKITPRO in your environment. export DEVKITPRO=/devkitpro") +endif + +TOPDIR ?= $(CURDIR) +include $(DEVKITPRO)/libnx/switch_rules + +#--------------------------------------------------------------------------------- +# TARGET is the name of the output +# BUILD is the directory where object files & intermediate files will be placed +# SOURCES is a list of directories containing source code +# DATA is a list of directories containing data files +# INCLUDES is a list of directories containing header files +# ROMFS is the directory containing data to be added to RomFS, relative to the Makefile (Optional) +# +# NO_ICON: if set to anything, do not use icon. +# NO_NACP: if set to anything, no .nacp file is generated. +# APP_TITLE is the name of the app stored in the .nacp file (Optional) +# APP_AUTHOR is the author of the app stored in the .nacp file (Optional) +# APP_VERSION is the version of the app stored in the .nacp file (Optional) +# APP_TITLEID is the titleID of the app stored in the .nacp file (Optional) +# ICON is the filename of the icon (.jpg), relative to the project folder. +# If not set, it attempts to use one of the following (in this order): +# - .jpg +# - icon.jpg +# - /default_icon.jpg +# +# CONFIG_JSON is the filename of the NPDM config file (.json), relative to the project folder. +# If not set, it attempts to use one of the following (in this order): +# - .json +# - config.json +# If a JSON file is provided or autodetected, an ExeFS PFS0 (.nsp) is built instead +# of a homebrew executable (.nro). This is intended to be used for sysmodules. +# NACP building is skipped as well. +#--------------------------------------------------------------------------------- +BUILD := build +SOURCES := src +DATA := data +INCLUDES := include +TARGET := $(BUILD)/magenboy +APP_VERSION := ${VERSION} +APP_AUTHOR := ${AUTHORS} +#ROMFS := romfs + +#--------------------------------------------------------------------------------- +# options for code generation +#--------------------------------------------------------------------------------- +ARCH := -march=armv8-a+crc+crypto -mtune=cortex-a57 -mtp=soft -fPIE + +CFLAGS := -g -Wall -O2 -ffunction-sections \ + $(ARCH) $(DEFINES) + +CFLAGS += $(INCLUDE) -D__SWITCH__ + +CXXFLAGS := $(CFLAGS) -fno-rtti -fno-exceptions + +ASFLAGS := -g $(ARCH) +LDFLAGS = -specs=$(DEVKITPRO)/libnx/switch.specs -g $(ARCH) -Wl,-Map,$(notdir $*.map) + +LIBS := -lnx -lmagenboy_nx + +#--------------------------------------------------------------------------------- +# list of directories containing libraries, this must be the top level containing +# include and lib +#--------------------------------------------------------------------------------- +LIBDIRS := $(PORTLIBS) $(LIBNX) + + +#--------------------------------------------------------------------------------- +# no real need to edit anything past this point unless you need to add additional +# rules for different file extensions +#--------------------------------------------------------------------------------- +ifneq ($(BUILD),$(notdir $(CURDIR))) +#--------------------------------------------------------------------------------- + +export OUTPUT := $(CURDIR)/$(TARGET) +export TOPDIR := $(CURDIR) + +export VPATH := $(foreach dir,$(SOURCES),$(CURDIR)/$(dir)) \ + $(foreach dir,$(DATA),$(CURDIR)/$(dir)) + +export DEPSDIR := $(CURDIR)/$(BUILD) + +CFILES := $(foreach dir,$(SOURCES),$(notdir $(wildcard $(dir)/*.c))) +CPPFILES := $(foreach dir,$(SOURCES),$(notdir $(wildcard $(dir)/*.cpp))) +SFILES := $(foreach dir,$(SOURCES),$(notdir $(wildcard $(dir)/*.s))) +BINFILES := $(foreach dir,$(DATA),$(notdir $(wildcard $(dir)/*.*))) + +#--------------------------------------------------------------------------------- +# use CXX for linking C++ projects, CC for standard C +#--------------------------------------------------------------------------------- +ifeq ($(strip $(CPPFILES)),) +#--------------------------------------------------------------------------------- + export LD := $(CC) +#--------------------------------------------------------------------------------- +else +#--------------------------------------------------------------------------------- + export LD := $(CXX) +#--------------------------------------------------------------------------------- +endif +#--------------------------------------------------------------------------------- + +export OFILES_BIN := $(addsuffix .o,$(BINFILES)) +export OFILES_SRC := $(CPPFILES:.cpp=.o) $(CFILES:.c=.o) $(SFILES:.s=.o) +export OFILES := $(OFILES_BIN) $(OFILES_SRC) +export HFILES_BIN := $(addsuffix .h,$(subst .,_,$(BINFILES))) + +export INCLUDE := $(foreach dir,$(INCLUDES),-I$(CURDIR)/$(dir)) \ + $(foreach dir,$(LIBDIRS),-I$(dir)/include) \ + -I$(CURDIR)/$(BUILD) + +export LIBPATHS := $(foreach dir,$(LIBDIRS),-L$(dir)/lib) \ + -L$(abspath target/aarch64-nintendo-switch-freestanding/release) + +ifeq ($(strip $(CONFIG_JSON)),) + jsons := $(wildcard *.json) + ifneq (,$(findstring $(TARGET).json,$(jsons))) + export APP_JSON := $(TOPDIR)/$(TARGET).json + else + ifneq (,$(findstring config.json,$(jsons))) + export APP_JSON := $(TOPDIR)/config.json + endif + endif +else + export APP_JSON := $(TOPDIR)/$(CONFIG_JSON) +endif + +ifeq ($(strip $(ICON)),) + icons := $(wildcard *.jpg) + ifneq (,$(findstring $(TARGET).jpg,$(icons))) + export APP_ICON := $(TOPDIR)/$(TARGET).jpg + else + ifneq (,$(findstring icon.jpg,$(icons))) + export APP_ICON := $(TOPDIR)/icon.jpg + endif + endif +else + export APP_ICON := $(TOPDIR)/$(ICON) +endif + +ifeq ($(strip $(NO_ICON)),) + export NROFLAGS += --icon=$(APP_ICON) +endif + +ifeq ($(strip $(NO_NACP)),) + export NROFLAGS += --nacp=$(CURDIR)/$(TARGET).nacp +endif + +ifneq ($(APP_TITLEID),) + export NACPFLAGS += --titleid=$(APP_TITLEID) +endif + +ifneq ($(ROMFS),) + export NROFLAGS += --romfsdir=$(CURDIR)/$(ROMFS) +endif + +.PHONY: $(BUILD) clean all + +#--------------------------------------------------------------------------------- +all: $(BUILD) + +$(BUILD): + @[ -d $@ ] || mkdir -p $@ + @$(MAKE) --no-print-directory -C $(BUILD) -f $(CURDIR)/Makefile + +#--------------------------------------------------------------------------------- +clean: + @echo clean ... +ifeq ($(strip $(APP_JSON)),) + @rm -fr $(BUILD) $(TARGET).nro $(TARGET).nacp $(TARGET).elf +else + @rm -fr $(BUILD) $(TARGET).nsp $(TARGET).nso $(TARGET).npdm $(TARGET).elf +endif + + +#--------------------------------------------------------------------------------- +else +.PHONY: all + +DEPENDS := $(OFILES:.o=.d) + +#--------------------------------------------------------------------------------- +# main targets +#--------------------------------------------------------------------------------- +ifeq ($(strip $(APP_JSON)),) + +all : $(OUTPUT).nro + +ifeq ($(strip $(NO_NACP)),) +$(OUTPUT).nro : $(OUTPUT).elf $(OUTPUT).nacp +else +$(OUTPUT).nro : $(OUTPUT).elf +endif + +else + +all : $(OUTPUT).nsp + +$(OUTPUT).nsp : $(OUTPUT).nso $(OUTPUT).npdm + +$(OUTPUT).nso : $(OUTPUT).elf + +endif + +$(OUTPUT).elf : $(OFILES) + +$(OFILES_SRC) : $(HFILES_BIN) + +#--------------------------------------------------------------------------------- +# you need a rule like this for each extension you use as binary data +#--------------------------------------------------------------------------------- +%.bin.o %_bin.h : %.bin +#--------------------------------------------------------------------------------- + @echo $(notdir $<) + @$(bin2o) + +-include $(DEPENDS) + +#--------------------------------------------------------------------------------------- +endif +#--------------------------------------------------------------------------------------- diff --git a/nx/build.rs b/nx/build.rs new file mode 100644 index 00000000..5bfbaccd --- /dev/null +++ b/nx/build.rs @@ -0,0 +1,12 @@ +// Export values for the C application to use +fn main() { + let version = env!("CARGO_PKG_VERSION"); + let authors = env!("CARGO_PKG_AUTHORS"); + let out_dir = std::env::var("OUT_DIR").unwrap(); + let mut out_vars_path = std::path::Path::new(&out_dir).to_path_buf(); + out_vars_path.pop(); + out_vars_path.pop(); + out_vars_path.pop(); + std::fs::write(out_vars_path.join("version.txt"), version).expect("Unable to write version file"); + std::fs::write(out_vars_path.join("authors.txt"), authors).expect("Unable to write authors file"); +} \ No newline at end of file diff --git a/nx/src/allocator.rs b/nx/src/allocator.rs new file mode 100644 index 00000000..c1c9244e --- /dev/null +++ b/nx/src/allocator.rs @@ -0,0 +1,21 @@ +use core::{alloc::GlobalAlloc, ffi::c_void}; + +// Link malloc and free from libnx +extern "C" { + pub fn malloc(size: usize) -> *mut c_void; + pub fn free(ptr: *mut c_void); +} + +pub struct NxAllocator; + +// Currently ignoring the layout, hope this will be fine lol +unsafe impl GlobalAlloc for NxAllocator{ + unsafe fn alloc(&self, layout: core::alloc::Layout) -> *mut u8 { + let ptr = malloc(layout.size()); + return ptr as *mut u8; + } + + unsafe fn dealloc(&self, ptr: *mut u8, _layout: core::alloc::Layout) { + free(ptr as *mut c_void); + } +} \ No newline at end of file diff --git a/nx/src/devices.rs b/nx/src/devices.rs new file mode 100644 index 00000000..cae271c7 --- /dev/null +++ b/nx/src/devices.rs @@ -0,0 +1,83 @@ +use core::ffi::c_int; + +use magenboy_common::{audio::{ManualAudioResampler, AudioResampler}, joypad_menu::MenuJoypadProvider}; +use magenboy_core::{AudioDevice, GfxDevice, self, JoypadProvider, keypad::button::Button}; + +pub type JoypadProviderCallback = unsafe extern "C" fn() -> u64; +pub type PollJoypadProviderCallback = unsafe extern "C" fn() -> u64; + +// Copied from libnx definitions +const fn bit(index: u64) -> u64 { 1 << index } +const JOYCON_A: u64 = bit(0); +const JOYCON_B: u64 = bit(1); +const JOYCON_X: u64 = bit(2); +const JOYCON_Y: u64 = bit(3); +const JOYCON_PLUS: u64 = bit(10); +const JOYCON_MINUS: u64 = bit(11); +const JOYCON_LEFT: u64 = bit(12); +const JOYCON_UP: u64 = bit(13); +const JOYCON_RIGHT: u64 = bit(14); +const JOYCON_DOWN: u64 = bit(15); + +pub(crate) struct NxJoypadProvider{ + pub provider_cb: JoypadProviderCallback, + pub poll_cb: PollJoypadProviderCallback +} + +impl NxJoypadProvider{ + fn update_state(joypad: &mut magenboy_core::keypad::joypad::Joypad, joycons_state: u64) { + joypad.buttons[Button::A as usize] = (joycons_state & JOYCON_A) != 0 || (joycons_state & JOYCON_X) != 0; + joypad.buttons[Button::B as usize] = (joycons_state & JOYCON_B) != 0 || (joycons_state & JOYCON_Y) != 0; + joypad.buttons[Button::Start as usize] = (joycons_state & JOYCON_PLUS) != 0; + joypad.buttons[Button::Select as usize] = (joycons_state & JOYCON_MINUS) != 0; + joypad.buttons[Button::Up as usize] = (joycons_state & JOYCON_UP) != 0; + joypad.buttons[Button::Down as usize] = (joycons_state & JOYCON_DOWN) != 0; + joypad.buttons[Button::Right as usize] = (joycons_state & JOYCON_RIGHT) != 0; + joypad.buttons[Button::Left as usize] = (joycons_state & JOYCON_LEFT) != 0; + } +} + +impl JoypadProvider for NxJoypadProvider { + fn provide(&mut self, joypad: &mut magenboy_core::keypad::joypad::Joypad) { + let joycons_state = unsafe{(self.provider_cb)()}; + Self::update_state(joypad, joycons_state); + } +} + +impl MenuJoypadProvider for NxJoypadProvider { + fn poll(&mut self, joypad:&mut magenboy_core::keypad::joypad::Joypad) { + let joycon_state = unsafe{(self.poll_cb)()}; + Self::update_state(joypad, joycon_state); + } +} + +pub type GfxDeviceCallback = unsafe extern "C" fn(buffer:*const u16) -> (); + +pub(crate) struct NxGfxDevice{ + pub cb: GfxDeviceCallback, + pub turbo: u32, + pub frame_counter: u32, +} + +impl GfxDevice for NxGfxDevice{ + fn swap_buffer(&mut self, buffer:&[magenboy_core::Pixel; magenboy_core::ppu::gb_ppu::SCREEN_HEIGHT * magenboy_core::ppu::gb_ppu::SCREEN_WIDTH]) { + if self.frame_counter % self.turbo == 0{ + unsafe{(self.cb)(buffer.as_ptr())}; + } + self.frame_counter = (self.frame_counter + 1) % self.turbo; + } +} + +pub type AudioDeviceCallback = unsafe extern "C" fn(buffer:*const magenboy_core::apu::audio_device::StereoSample, size:c_int) -> (); + +pub(crate) struct NxAudioDevice{ + pub cb: AudioDeviceCallback, + pub resampler: ManualAudioResampler, +} + +impl AudioDevice for NxAudioDevice{ + fn push_buffer(&mut self, buffer:&[magenboy_core::apu::audio_device::StereoSample; magenboy_core::apu::audio_device::BUFFER_SIZE]) { + let resampled = self.resampler.resample(buffer); + unsafe{(self.cb)(resampled.as_ptr(), (resampled.len() * 2) as c_int)}; + } +} diff --git a/nx/src/lib.rs b/nx/src/lib.rs new file mode 100644 index 00000000..9a807015 --- /dev/null +++ b/nx/src/lib.rs @@ -0,0 +1,154 @@ +#![no_std] + +extern crate alloc; + +mod logging; +mod devices; +mod allocator; + +use core::{ffi::{c_char, c_ulonglong, c_void, CStr}, panic}; +use alloc::{vec::Vec, boxed::Box, string::String}; + +use magenboy_common::{audio::*, joypad_menu::{joypad_gfx_menu::GfxDeviceMenuRenderer, JoypadMenu}, menu::{MenuOption, GAME_MENU_OPTIONS}, VERSION}; +use magenboy_core::{machine, GameBoy, Mode, GB_FREQUENCY}; + +use devices::*; +use logging::{LogCallback, NxLogger}; + +const TURBO: u32 = 2; + +struct NxGbContext<'a>{ + gb: GameBoy<'a, NxJoypadProvider, NxAudioDevice, NxGfxDevice>, + sram_fat_pointer: (*mut u8, usize) +} + +#[global_allocator] +static ALLOCATOR: allocator::NxAllocator = allocator::NxAllocator{}; + +#[panic_handler] +fn panic_handler(info: &panic::PanicInfo) -> ! { + log::error!("Panic: {}", info); + loop{} +} + +// Exported C interface for nx + +#[no_mangle] +pub unsafe extern "C" fn magenboy_init_logger(log_cb: LogCallback) { + // SAFETY: log_cb is a valid c function pointer + NxLogger::init(log::LevelFilter::Debug, log_cb); +} + +/// SAFETY: rom size must be the size of rom +#[no_mangle] +pub unsafe extern "C" fn magenboy_init(rom: *const c_char, rom_size: c_ulonglong, gfx_cb: GfxDeviceCallback, joypad_cb: JoypadProviderCallback, + poll_joypad_cb: PollJoypadProviderCallback, audio_cb:AudioDeviceCallback) -> *mut c_void { + + let rom:&[u8] = unsafe{ core::slice::from_raw_parts(rom as *const u8, rom_size as usize) }; + let mbc = machine::mbc_initializer::initialize_mbc(&rom, None); + + let mode = mbc.detect_preferred_mode(); + log::info!("Detected mode: {}", >::into(mode)); + + let sram_fat_pointer = (mbc.get_ram().as_mut_ptr(), mbc.get_ram().len()); + + // Initialize the GameBoy instance + let gameboy = GameBoy::new_with_mode( + mbc, + NxJoypadProvider{provider_cb: joypad_cb, poll_cb: poll_joypad_cb}, + NxAudioDevice{cb: audio_cb, resampler: ManualAudioResampler::new(GB_FREQUENCY * TURBO, 48000)}, + NxGfxDevice {cb: gfx_cb, turbo: TURBO, frame_counter: 0}, + mode, + ); + + let ctx = NxGbContext {gb: gameboy, sram_fat_pointer }; + + // Allocate on static memory + let gameboy = Box::new(ctx); + log::info!("Initialized MagenBoy successfully"); + return Box::into_raw(gameboy) as *mut c_void; +} + +#[no_mangle] +pub unsafe extern "C" fn magenboy_deinit(ctx: *mut c_void) { + // SAFETY: ctx is a valid pointer to a GameBoy instance + if ctx.is_null() { + log::warn!("Attempted to deinitialize MagenBoy with a null context pointer"); + return; + } + + let _ = unsafe { Box::from_raw(ctx as *mut NxGbContext) }; // Drop the Box to deallocate memory + log::info!("MagenBoy deinitialized successfully"); +} + + +#[no_mangle] +pub unsafe extern "C" fn magenboy_menu_trigger(gfx_cb: GfxDeviceCallback, joypad_cb: JoypadProviderCallback, poll_joypad_cb: PollJoypadProviderCallback, + roms: *const *const c_char, roms_count: u32) -> *const c_char { + + log::info!("Starting ROM menu"); + + // SAFETY: roms is a valid c strings array + let roms: Vec> = unsafe { + let mut roms_vec = Vec::with_capacity(roms_count as usize); + for i in 0..roms_count { + let rom_name = *(roms.add(i as usize)); + let c_str = CStr::from_ptr(rom_name as *mut c_char); + roms_vec.push(MenuOption{value: c_str, prompt: filename_from_path(c_str.to_str().unwrap())}); + } + roms_vec + }; + + let selection = render_menu(gfx_cb, joypad_cb, poll_joypad_cb, &roms, "Choose ROM menu"); + + return selection.as_ptr(); +} + +#[no_mangle] +pub unsafe extern "C" fn magenboy_pause_trigger(gfx_cb: GfxDeviceCallback, joypad_cb: JoypadProviderCallback, poll_joypad_cb: PollJoypadProviderCallback) -> u32 { + + log::info!("Starting pause menu"); + let header: String = alloc::format!("Magenboy {VERSION}"); + let selection= render_menu(gfx_cb, joypad_cb, poll_joypad_cb, &GAME_MENU_OPTIONS, header.as_str()); + return *selection as u32; +} + +fn render_menu<'a, T>(gfx_cb: GfxDeviceCallback, joypad_cb: JoypadProviderCallback, poll_joypad_cb: PollJoypadProviderCallback, options: &'a [MenuOption], header: &'a str) -> &'a T { + let mut gfx_device = NxGfxDevice {cb: gfx_cb, turbo: 1, frame_counter: 0}; + let menu_renderer = GfxDeviceMenuRenderer::new(&mut gfx_device); + let mut provider = NxJoypadProvider{provider_cb: joypad_cb, poll_cb: poll_joypad_cb}; + let mut menu = JoypadMenu::new(&options, header, menu_renderer); + return menu.get_menu_selection(&mut provider); +} + +/// SAFETY: ctx is a valid pointer to a GameBoy instance +#[no_mangle] +pub unsafe extern "C" fn magenboy_cycle_frame(ctx: *mut c_void) { + // SAFETY: ctx is a valid pointer to a GameBoy instance + unsafe { + (*(ctx as *mut NxGbContext)).gb.cycle_frame() + } +} + +#[no_mangle] +pub unsafe extern "C" fn magenboy_get_dimensions(width: *mut u32, height: *mut u32) { + // SAFETY: width and height are valid pointers to uint32_t + unsafe { + *width = magenboy_core::ppu::gb_ppu::SCREEN_WIDTH as u32; + *height = magenboy_core::ppu::gb_ppu::SCREEN_HEIGHT as u32; + } +} + +#[no_mangle] +pub unsafe extern "C" fn magenboy_get_sram(ctx: *mut c_void, ptr: *mut *mut u8, size: *mut usize){ + let sram_fat_ptr = (*(ctx as *mut NxGbContext)).sram_fat_pointer; + *ptr = sram_fat_ptr.0; + *size = sram_fat_ptr.1; +} + +fn filename_from_path(path: &str) -> &str { + match path.rfind(|c| c == '/') { + Some(pos) => &path[pos + 1..], + None => path, + } +} \ No newline at end of file diff --git a/nx/src/logging.rs b/nx/src/logging.rs new file mode 100644 index 00000000..936809ba --- /dev/null +++ b/nx/src/logging.rs @@ -0,0 +1,48 @@ +use core::{ffi::{c_char, c_int}, fmt::Write}; + +use magenboy_common::synchronization::Mutex; + +use log::Log; + +pub type LogCallback = extern "C" fn(*const c_char, len: c_int) -> (); + +struct NxLogCallback{ + cb:LogCallback +} + +impl Write for NxLogCallback{ + fn write_str(&mut self, s: &str) -> core::fmt::Result { + (self.cb)(s.as_ptr() as *const c_char, s.len() as c_int); + + return core::fmt::Result::Ok(()); + } +} + +pub struct NxLogger{ + log_cb: Mutex +} + + +impl NxLogger{ + pub fn init(max_log_level:log::LevelFilter, log_cb: LogCallback){ + static mut LOGGER: Option = None; + unsafe{ + LOGGER = Some(NxLogger{log_cb: Mutex::new(NxLogCallback{cb: log_cb})}); + log::set_logger(LOGGER.as_ref().unwrap()).expect("Failed to set logger"); + } + log::set_max_level(max_log_level); + } +} + +impl Log for NxLogger{ + fn enabled(&self, metadata: &log::Metadata) -> bool { + return metadata.level() <= log::max_level(); + } + + fn log(&self, record: &log::Record) { + if !self.enabled(record.metadata()) { return } + self.log_cb.lock(|d|d.write_fmt(format_args!("{} - {}\r\n", record.level(), record.args())).unwrap()); + } + + fn flush(&self) {} +} \ No newline at end of file diff --git a/nx/src/magenboy.h b/nx/src/magenboy.h new file mode 100644 index 00000000..8b61e3fa --- /dev/null +++ b/nx/src/magenboy.h @@ -0,0 +1,47 @@ +#ifndef MAGENBOY_H +#define MAGENBOY_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include + +// Define a callback type for logging. +// Adjust the signature as needed. +typedef void (*LogCallback)(const char* message, int len); +typedef void (*GfxDeviceCallback)(const uint16_t* buffer); +typedef uint64_t (*JoypadDeviceCallback)(); +typedef uint64_t (*PollJoypadDeviceCallback)(); +typedef void (*AudioDeviceCallback)(const int16_t* buffer, int size); + +void magenboy_init_logger(LogCallback log_cb); + +// Initialize the GameBoy instance. +// rom: pointer to ROM data +// rom_size: size of ROM data in bytes +// Returns: a pointer to the statically allocated GameBoy instance. +void* magenboy_init(const uint8_t* rom, uint64_t rom_size, GfxDeviceCallback gfx_cb, JoypadDeviceCallback joypad_cb, PollJoypadDeviceCallback poll_cb, + AudioDeviceCallback audio_cb); + +void magenboy_deinit(void* ctx); + +const char* magenboy_menu_trigger(GfxDeviceCallback gfx_cb, JoypadDeviceCallback joypad_cb, PollJoypadDeviceCallback poll_cb, const char** roms, uint32_t roms_count); + +const uint32_t magenboy_pause_trigger(GfxDeviceCallback gfx_cb, JoypadDeviceCallback joypad_cb, PollJoypadDeviceCallback poll_cb); + +// Cycle a frame for the given GameBoy instance. +// ctx: pointer to a GameBoy instance returned by magenboy_init. +void magenboy_cycle_frame(void* ctx); + +// Get the GB display dimensions. +void magenboy_get_dimensions(uint32_t* width, uint32_t* height); + +void magenboy_get_sram(void* ctx, uint8_t** sram_buffer, size_t* sram_size); + +#ifdef __cplusplus +} +#endif + +#endif // MAGENBOY_H \ No newline at end of file diff --git a/nx/src/main.c b/nx/src/main.c new file mode 100644 index 00000000..12d71524 --- /dev/null +++ b/nx/src/main.c @@ -0,0 +1,430 @@ +#include +#include +#include +#include +#include +#include +#include + +// Include the main libnx system header, for Switch development +#include + +// Include magenboy header +#include "magenboy.h" + +#define MIN(X, Y) (((X) < (Y)) ? (X) : (Y)) + +static void log_cb(const char* message, int len) { + fwrite(message, 1, len, stdout); +} + +static long read_rom_buffer(const char* path, u8** out_rom_buffer) { + long return_value = -1; + *out_rom_buffer = NULL; + + FILE* file = fopen(path, "rb"); + if (!file) { + perror("Failed to open ROM file"); + return return_value; + } + + if (fseek(file, 0, SEEK_END) != 0) { + perror("Failed to seek to end of ROM file"); + goto exit_file; + } + long size = ftell(file); + rewind(file); + + *out_rom_buffer = (u8*)malloc(size); + if (!out_rom_buffer) { + perror("Failed to allocate memory for ROM"); + goto exit_file; + } + + if (fread(*out_rom_buffer, 1, size, file) != size) { + perror("Failed to read ROM file"); + free(*out_rom_buffer); + *out_rom_buffer = NULL; + } + + return_value = size; + +exit_file: + fclose(file); + return return_value; +} + +static Framebuffer fb; + +static void render_buffer_cb(const uint16_t* buffer) { + u32 stride; + uint16_t* framebuffer = (uint16_t*)framebufferBegin(&fb, &stride); + stride /= sizeof(uint16_t); + + u32 gb_width, gb_height; + magenboy_get_dimensions(&gb_width, &gb_height); + + u32 frame_initial_width = (stride - gb_width) / 2; + + for (int y = 0; y < gb_height; y++) { + uint16_t* dest = framebuffer + (y * stride) + frame_initial_width; + const uint16_t* src = buffer + (y * gb_width); + memcpy(dest, src, gb_width * sizeof(uint16_t)); + } + + framebufferEnd(&fb); +} + +static PadState pad; + +static uint64_t get_joycon_state() { + padUpdate(&pad); + return padGetButtons(&pad); +} + +static uint64_t poll_until_joycon_pressed() { + while (1) { + padUpdate(&pad); + u64 buttons = padGetButtonsDown(&pad); + if (buttons != 0) { + return buttons; + } + + svcSleepThread(10000000ULL); // 100ms in nanoseconds + } +} + +#define SAMPLERATE (48000) +#define CHANNEL_COUNT (2) +#define BYTES_PER_SAMPLE (sizeof(int16_t)) +// For sone reason multplying by 40 makes the best audio latency (60 for example makes audoutWaitPlayFinish to block for a long time) +// causing frame drops and audio glitches +#define AUDIO_DATA_SIZE ((SAMPLERATE * CHANNEL_COUNT * BYTES_PER_SAMPLE) / (40)) + +// buffer for audio must be aligned to 0x1000 bytes +#define BUFFER_ALIGNMENT (0x1000) +#define AUDIO_BUFFER_SIZE ((AUDIO_DATA_SIZE + (BUFFER_ALIGNMENT - 1)) & ~(BUFFER_ALIGNMENT - 1)) /*Aligned buffer size*/ + +static int16_t* audio_work_buffer; +static int audio_work_data_offset = 0; +static int16_t* audio_io_buffer; + +static void audio_device_cb(const int16_t* buffer, int size) { + int transfer_size = MIN(size, (AUDIO_DATA_SIZE / BYTES_PER_SAMPLE) - audio_work_data_offset); + memcpy(audio_work_buffer + audio_work_data_offset, buffer, transfer_size * BYTES_PER_SAMPLE); + audio_work_data_offset += transfer_size; + + if (audio_work_data_offset >= (AUDIO_DATA_SIZE / BYTES_PER_SAMPLE)) { + audio_work_data_offset = 0; + + // wait for last buffer to finish playing + AudioOutBuffer* released_buffer = NULL; + u32 count = 0; + audoutWaitPlayFinish(&released_buffer, &count, UINT64_MAX); + + // Copy data to buffer + if (released_buffer) { + memcpy(released_buffer->buffer, audio_work_buffer, released_buffer->data_size); + } + + // Submit new samples + audoutAppendAudioOutBuffer(released_buffer); + } + + if (transfer_size < size) { + int remaining_size = size - transfer_size; + memcpy(audio_work_buffer + audio_work_data_offset, buffer + transfer_size, remaining_size * BYTES_PER_SAMPLE); + audio_work_data_offset += remaining_size; + } +} + +static int intiailzie_audio_buffers() { + audio_work_buffer = aligned_alloc(BUFFER_ALIGNMENT, AUDIO_BUFFER_SIZE); + if (audio_work_buffer == NULL) { + printf("Failed to allocate audio work buffer.\n"); + return -1; + } + audio_io_buffer = aligned_alloc(BUFFER_ALIGNMENT, AUDIO_BUFFER_SIZE); + if (audio_io_buffer == NULL) { + printf("Failed to allocate audio io buffer.\n"); + free(audio_work_buffer); + return -1; + } + + memset(audio_work_buffer, 0, AUDIO_BUFFER_SIZE); + memset(audio_work_buffer, 0, AUDIO_BUFFER_SIZE); + + return 0; +} + +static void get_timespec(struct timespec* ts) { + clock_gettime(CLOCK_MONOTONIC, ts); +} + +static int has_gb_extension(const char* filename) { + const char* ext = strrchr(filename, '.'); + if (ext && (strcmp(ext, ".gb") == 0 || strcmp(ext, ".gbc") == 0)) { + return 1; + } + return 0; +} + +static int read_dir_filenames(const char* directory_path, char** file_list, size_t max_filename_size, size_t max_files) { + struct dirent* entry; + DIR* dir = opendir(directory_path); + + if (dir == NULL) { + perror("Failed to open directory"); + return -1; + } + + printf("Files in directory '%s':\n", directory_path); + int counter = 0; + while ((entry = readdir(dir)) != NULL) { + if (has_gb_extension(entry->d_name) != 0) { + printf("%s\n", entry->d_name); + + if (counter < max_files) { + snprintf(file_list[counter], max_filename_size, "%s/%s", directory_path, entry->d_name); + counter++; + } else { + printf("Maximum number of files reached.\n"); + break; + } + } + } + + closedir(dir); + return counter; +} + +#define MAX_ROMS (30) +#define MAX_FILENAME_SIZE (300) + +static int try_load_sram(const char* filepath, u8** sram_buffer, size_t* sram_size) { + int status = 0; + char sram_path[MAX_FILENAME_SIZE]; + snprintf(sram_path, sizeof(sram_path), "%s.sram", filepath); + + FILE* file = fopen(sram_path, "rb"); + if (!file) { + perror("Failed to open SRAM file"); + return -1; + } + + fseek(file, 0, SEEK_END); + *sram_size = ftell(file); + rewind(file); + + *sram_buffer = (u8*)malloc(*sram_size); + if (!*sram_buffer) { + perror("Failed to allocate memory for SRAM"); + status = -1; + goto exit; + } + + if (fread(*sram_buffer, 1, *sram_size, file) != *sram_size) { + perror("Failed to read SRAM file"); + free(*sram_buffer); + status = -1; + goto exit; + } + +exit: + fclose(file); + return status; +} + +static void save_sram(const char* filepath, const u8* sram_buffer, size_t sram_size) { + char sram_path[MAX_FILENAME_SIZE]; + snprintf(sram_path, sizeof(sram_path), "%s.sram", filepath); + + FILE* file = fopen(sram_path, "wb"); + if (!file) { + perror("Failed to open SRAM file for writing"); + return; + } + + if (fwrite(sram_buffer, 1, sram_size, file) != sram_size) { + perror("Failed to write SRAM data"); + } + + fclose(file); +} + +int main(int argc, char* argv[]) { + if (socketInitializeDefault() != 0) { + printf("Failed to initialize socket driver.\n"); + return -1; + } + int nxlink_fd = nxlinkStdio(); + if (nxlink_fd < 0) { + printf("Failed to initialize NXLink: %d.\n", errno); + socketExit(); + } + + // Configure our supported input layout: a single player with standard controller styles + padConfigureInput(1, HidNpadStyleSet_NpadStandard); + + // Initialize the default gamepad (which reads handheld mode inputs as well as the first connected controller) + padInitializeDefault(&pad); + + // Retrieve the default window + NWindow* win = nwindowGetDefault(); + + u32 win_width, win_height; + if (R_FAILED(nwindowGetDimensions(win, &win_width, &win_height))) { + printf("Failed to get window dimensions.\n"); + goto scoket_exit; + } + + u32 gb_wifth, gb_height; + magenboy_get_dimensions(&gb_wifth, &gb_height); + + // Adjusting the framebuffer width to match the window width in order to let the switch scale the image + float width_scale_ratio = (float)win_height / (float)gb_height; + u32 frame_width = (u32)(gb_wifth * (float)win_width / (float)(gb_wifth * width_scale_ratio)); + + // Initialize the framebuffer + if (R_FAILED(framebufferCreate(&fb, win, frame_width, gb_height, PIXEL_FORMAT_RGB_565, 2))) { + printf("Failed to create framebuffer.\n"); + goto link_exit; + } + + if (R_FAILED(framebufferMakeLinear(&fb))) { + printf("Failed to make framebuffer linear.\n"); + goto fb_exit; + } + + if (intiailzie_audio_buffers() != 0) { + printf("Failed to initialize audio.\n"); + goto fb_exit; + } + if (R_FAILED(audoutInitialize())) { + printf("Failed to initialize audio.\n"); + goto audio_buffers_exit; + } + if (R_FAILED(audoutStartAudioOut())) { + printf("Failed to start audio.\n"); + goto audio_exit; + } + + // Initialize the audio output buffer + + AudioOutBuffer audio_out_buffer = { + .buffer = audio_io_buffer, + .buffer_size = AUDIO_BUFFER_SIZE, + .data_size = AUDIO_DATA_SIZE, + .data_offset = 0, + .next = NULL, + }; + + audoutAppendAudioOutBuffer(&audio_out_buffer); + + magenboy_init_logger(log_cb); + + // Asks the user to select a ROM file + char** roms = malloc(MAX_ROMS * sizeof(char*)); + for (int i = 0; i < MAX_ROMS; i++) { + roms[i] = malloc(MAX_FILENAME_SIZE); + } + +restart: + int count = read_dir_filenames("roms", roms, MAX_FILENAME_SIZE, MAX_ROMS); + + const char* filepath = magenboy_menu_trigger(render_buffer_cb, get_joycon_state, poll_until_joycon_pressed, (const char**)roms, count); + if (filepath == NULL) { + printf("Failed to trigger ROM menu.\n"); + goto fb_exit; + } + + // Read a rom file + u8* rom_buffer = NULL; + long file_size = read_rom_buffer(filepath, &rom_buffer); + if (file_size < 0) { + printf("Failed to read ROM file.\n"); + goto fb_exit; + } + + u8* found_sram_buffer = NULL; + size_t found_sram_size = 0; + int found_sram = try_load_sram(filepath, &found_sram_buffer, &found_sram_size); + + void* ctx = magenboy_init(rom_buffer, file_size, render_buffer_cb, get_joycon_state, poll_until_joycon_pressed, audio_device_cb); + + u8* sram_buffer = NULL; + size_t sram_size = 0; + magenboy_get_sram(ctx, &sram_buffer, &sram_size); + + if (found_sram == 0 && sram_size == found_sram_size) { + memcpy(sram_buffer, found_sram_buffer, sram_size); + printf("Loaded SRAM from file: %s.sram\n", filepath); + } + + // FPS measurement variables + struct timespec start_time, end_time; + int frame_count = 0; + double elapsed_time = 0.0; + + get_timespec(&start_time); + + // Main loop + while (appletMainLoop()) { + // No need to update as the joypad called is polling the state + u64 kDown = padGetButtons(&pad); + if ((kDown & HidNpadButton_L) != 0 && (kDown & HidNpadButton_R) != 0) { + int shutdown = 0; + switch (magenboy_pause_trigger(render_buffer_cb, get_joycon_state, poll_until_joycon_pressed)) { + case 0: // Resume + break; + case 1: // Restart + printf("Restarting\n"); + goto restart; + case 2: // Shutdon + printf("Shutting down\n"); + shutdown = 1; + break; + } + if (shutdown) { + break; // Exit the main loop + } + } + + magenboy_cycle_frame(ctx); + + // FPS calculation + frame_count++; + get_timespec(&end_time); + elapsed_time = (end_time.tv_sec - start_time.tv_sec) + (end_time.tv_nsec - start_time.tv_nsec) / 1e9; + + // Print FPS every second + if (elapsed_time >= 1.0) { + printf("FPS: %d\n", frame_count); + frame_count = 0; + get_timespec(&start_time); + } + } + + save_sram(filepath, sram_buffer, sram_size); + + // Deinitialize and clean up resources + magenboy_deinit(ctx); + free(rom_buffer); + audoutStopAudioOut(); +audio_exit: + audoutExit(); +audio_buffers_exit: + free(audio_work_buffer); + free(audio_io_buffer); +fb_exit: + framebufferClose(&fb); +link_exit: + if (nxlink_fd > 0) { + close(nxlink_fd); + } +scoket_exit: + if (nxlink_fd > 0) { + socketExit(); + } + return 0; +} diff --git a/rpi/src/bin/baremetal/logging.rs b/rpi/src/bin/baremetal/logging.rs index 514dfbd3..68f8d858 100644 --- a/rpi/src/bin/baremetal/logging.rs +++ b/rpi/src/bin/baremetal/logging.rs @@ -1,6 +1,7 @@ use core::fmt::Write; -use magenboy_rpi::{peripherals::{MiniUart, PERIPHERALS}, syncronization::Mutex}; +use magenboy_rpi::peripherals::{MiniUart, PERIPHERALS}; +use magenboy_common::synchronization::Mutex; use log::{Record, Metadata, Log, LevelFilter}; diff --git a/rpi/src/lib.rs b/rpi/src/lib.rs index 25dd797d..b9dbe227 100644 --- a/rpi/src/lib.rs +++ b/rpi/src/lib.rs @@ -7,7 +7,6 @@ pub mod configuration; pub mod peripherals; pub mod drivers; cfg_if::cfg_if!{ if #[cfg(feature = "bm")]{ - pub mod syncronization; pub mod delay; }} diff --git a/rpi/src/peripherals/gpio.rs b/rpi/src/peripherals/gpio.rs index 83908fec..2904408e 100644 --- a/rpi/src/peripherals/gpio.rs +++ b/rpi/src/peripherals/gpio.rs @@ -25,7 +25,9 @@ pub enum Trigger{ #[cfg(feature = "bm")] pub mod no_std_impl{ - use crate::{syncronization::Mutex, peripherals::utils::{compile_time_size_assert, MmioReg32, get_static_peripheral, memory_barrier, BulkWrite}}; + use magenboy_common::synchronization::Mutex; + + use crate::peripherals::utils::{compile_time_size_assert, MmioReg32, get_static_peripheral, memory_barrier, BulkWrite}; use super::*; #[repr(C,align(4))]