Browse Source

Merge branch 'master' of https://github.com/rust3ds/ctru-rs into feature/ir-user

pull/86/head
Andrea Ciliberti 1 year ago
parent
commit
6899f7026e
  1. 46
      .github/actions/setup/action.yml
  2. 63
      .github/workflows/ci.yml
  3. 1
      .gitignore
  4. 8
      Cargo.toml
  5. 28
      README.md
  6. 28
      ctru-rs/Cargo.toml
  7. 22
      ctru-rs/README.md
  8. 41
      ctru-rs/examples/audio-filters.rs
  9. 34
      ctru-rs/examples/buttons.rs
  10. 95
      ctru-rs/examples/camera-image.rs
  11. 26
      ctru-rs/examples/file-explorer.rs
  12. 2
      ctru-rs/examples/futures-basic.rs
  13. 2
      ctru-rs/examples/futures-tokio.rs
  14. 33
      ctru-rs/examples/gfx-3d-mode.rs
  15. 30
      ctru-rs/examples/gfx-bitmap.rs
  16. 14
      ctru-rs/examples/gfx-wide-mode.rs
  17. 57
      ctru-rs/examples/graphics-bitmap.rs
  18. 13
      ctru-rs/examples/hashmaps.rs
  19. 13
      ctru-rs/examples/hello-both-screens.rs
  20. 20
      ctru-rs/examples/hello-world.rs
  21. 17
      ctru-rs/examples/linear-memory.rs
  22. 29
      ctru-rs/examples/mii-selector.rs
  23. 52
      ctru-rs/examples/movement.rs
  24. 33
      ctru-rs/examples/network-sockets.rs
  25. 11
      ctru-rs/examples/output-3dslink.rs
  26. 22
      ctru-rs/examples/romfs.rs
  27. 27
      ctru-rs/examples/software-keyboard.rs
  28. 16
      ctru-rs/examples/system-configuration.rs
  29. 4
      ctru-rs/examples/thread-basic.rs
  30. 4
      ctru-rs/examples/thread-info.rs
  31. 4
      ctru-rs/examples/thread-locals.rs
  32. 14
      ctru-rs/examples/time-rtc.rs
  33. 58
      ctru-rs/examples/title-info.rs
  34. 20
      ctru-rs/examples/touch-screen.rs
  35. 263
      ctru-rs/src/applets/mii_selector.rs
  36. 10
      ctru-rs/src/applets/mod.rs
  37. 375
      ctru-rs/src/applets/swkbd.rs
  38. 326
      ctru-rs/src/console.rs
  39. 42
      ctru-rs/src/error.rs
  40. 87
      ctru-rs/src/lib.rs
  41. 21
      ctru-rs/src/linear.rs
  42. 176
      ctru-rs/src/mii.rs
  43. 154
      ctru-rs/src/os.rs
  44. 11
      ctru-rs/src/prelude.rs
  45. 154
      ctru-rs/src/services/am.rs
  46. 63
      ctru-rs/src/services/apt.rs
  47. 1105
      ctru-rs/src/services/cam.rs
  48. 166
      ctru-rs/src/services/cfgu.rs
  49. 200
      ctru-rs/src/services/fs.rs
  50. 302
      ctru-rs/src/services/gfx.rs
  51. 13
      ctru-rs/src/services/gspgpu.rs
  52. 488
      ctru-rs/src/services/hid.rs
  53. 15
      ctru-rs/src/services/mod.rs
  54. 493
      ctru-rs/src/services/ndsp/mod.rs
  55. 99
      ctru-rs/src/services/ndsp/wave.rs
  56. 105
      ctru-rs/src/services/ps.rs
  57. 42
      ctru-rs/src/services/reference.rs
  58. 61
      ctru-rs/src/services/romfs.rs
  59. 111
      ctru-rs/src/services/soc.rs
  60. 22
      ctru-rs/src/services/sslc.rs
  61. 73
      ctru-rs/src/test_runner.rs
  62. 15
      ctru-sys/Cargo.toml
  63. 33
      ctru-sys/README.md
  64. 57
      ctru-sys/bindgen.sh
  65. 119
      ctru-sys/build.rs
  66. 7
      ctru-sys/docstring-to-rustdoc/Cargo.toml
  67. 31
      ctru-sys/docstring-to-rustdoc/src/main.rs
  68. 22931
      ctru-sys/src/bindings.rs
  69. 19
      ctru-sys/src/lib.rs

46
.github/actions/setup/action.yml

@ -1,46 +0,0 @@
name: Setup
description: Set up CI environment for Rust + 3DS development
inputs:
toolchain:
description: The Rust toolchain to use for the steps
required: true
default: nightly
runs:
using: composite
steps:
# https://github.com/nektos/act/issues/917#issuecomment-1074421318
- if: ${{ env.ACT }}
shell: bash
name: Hack container for local development
run: |
curl -fsSL https://deb.nodesource.com/setup_lts.x | sudo -E bash -
sudo apt-get install -y nodejs
- name: Setup default Rust toolchain
# Use this helper action so we get matcher support
# https://github.com/actions-rust-lang/setup-rust-toolchain/pull/15
uses: actions-rust-lang/setup-rust-toolchain@v1
with:
components: clippy, rustfmt, rust-src
toolchain: ${{ inputs.toolchain }}
- name: Install build tools for host
shell: bash
run: sudo apt-get update && sudo apt-get install -y build-essential
- name: Install cargo-3ds
uses: actions-rs/cargo@v1
with:
command: install
# TODO: this should probably just be a released version from crates.io
# once cargo-3ds gets published somewhere...
args: >-
--git https://github.com/rust3ds/cargo-3ds
--rev 78a652fdfb01e2614a792d1a56b10c980ee1dae9
- name: Set PATH to include devkitARM
shell: bash
# For some reason devkitARM/bin is not part of the default PATH in the container
run: echo "${DEVKITARM}/bin" >> $GITHUB_PATH

63
.github/workflows/ci.yml

@ -9,19 +9,13 @@ on:
- master - master
workflow_dispatch: workflow_dispatch:
env:
# https://blog.rust-lang.org/2022/06/22/sparse-registry-testing.html
CARGO_UNSTABLE_SPARSE_REGISTRY: "true"
# actions-rust-lang/setup-rust-toolchain sets some default RUSTFLAGS
RUSTFLAGS: ""
jobs: jobs:
lint: lint:
strategy: strategy:
matrix: matrix:
toolchain: toolchain:
# Run against a "known good" nightly # Run against a "known good" nightly. Rustc version is 1 day behind the toolchain date
- nightly-2023-01-13 - nightly-2023-06-01
# Check for breakage on latest nightly # Check for breakage on latest nightly
- nightly - nightly
@ -33,11 +27,14 @@ jobs:
- name: Checkout branch - name: Checkout branch
uses: actions/checkout@v2 uses: actions/checkout@v2
- uses: ./.github/actions/setup - uses: rust3ds/test-runner/setup@v1
with: with:
toolchain: ${{ matrix.toolchain }} toolchain: ${{ matrix.toolchain }}
- name: Hide duplicate warnings from lint job # https://github.com/actions/runner/issues/504
# Removing the matchers won't keep the job from failing if there are errors,
# but will at least declutter pull request annotations (especially for warnings).
- name: Hide duplicate annotations from nightly
if: ${{ matrix.toolchain == 'nightly' }} if: ${{ matrix.toolchain == 'nightly' }}
run: | run: |
echo "::remove-matcher owner=clippy::" echo "::remove-matcher owner=clippy::"
@ -46,18 +43,17 @@ jobs:
- name: Check formatting - name: Check formatting
run: cargo fmt --all --verbose -- --check run: cargo fmt --all --verbose -- --check
- name: Cargo check - name: Cargo check ctru-sys (without tests)
run: cargo 3ds clippy --color=always --workspace --verbose --all-targets run: cargo 3ds clippy --package ctru-sys --color=always --verbose
# --deny=warnings would be nice, but can easily break CI for new clippy
# lints getting added. I'd also like to use Github's "inline warnings" - name: Cargo check ctru-rs (including tests)
# feature, but https://github.com/actions/runner/issues/2341 means we run: cargo 3ds clippy --package ctru-rs --color=always --verbose --all-targets
# can't have both that *and* colored output.
doctests: test:
strategy: strategy:
matrix: matrix:
toolchain: toolchain:
- nightly-2023-01-13 - nightly-2023-06-01
- nightly - nightly
continue-on-error: ${{ matrix.toolchain == 'nightly' }} continue-on-error: ${{ matrix.toolchain == 'nightly' }}
runs-on: ubuntu-latest runs-on: ubuntu-latest
@ -66,15 +62,36 @@ jobs:
- name: Checkout branch - name: Checkout branch
uses: actions/checkout@v2 uses: actions/checkout@v2
- uses: ./.github/actions/setup - uses: rust3ds/test-runner/setup@v1
with: with:
toolchain: ${{ matrix.toolchain }} toolchain: ${{ matrix.toolchain }}
- name: Hide duplicated warnings from lint job - name: Hide duplicated warnings from lint job
run: echo "::remove-matcher owner=clippy::" run: echo "::remove-matcher owner=clippy::"
- name: Build doc tests # This needs to be done separately from running the tests to ensure the
run: cargo 3ds test --doc --verbose # lib tests' .3dsx is built before the test is run (for romfs). We don't
# really have a good way to build the 3dsx in between the build + test,
# unless cargo-3ds actually runs them as separate commands. See
# https://github.com/rust3ds/cargo-3ds/issues/44 for more details
- name: Build lib and integration tests
run: cargo 3ds test --no-run --tests --package ctru-rs
- name: Run lib and integration tests
uses: rust3ds/test-runner/run-tests@v1
with:
args: --tests --package ctru-rs
# TODO: it would be nice to actually build 3dsx for examples/tests, etc. - name: Build and run doc tests
# and run it somehow, but exactly how remains to be seen. uses: rust3ds/test-runner/run-tests@v1
with:
args: --doc --package ctru-rs
- name: Upload citra logs and capture videos
uses: actions/upload-artifact@v3
if: success() || failure() # always run unless the workflow was cancelled
with:
name: citra-logs-${{ matrix.toolchain }}
path: |
target/armv6k-nintendo-3ds/debug/deps/*.txt
target/armv6k-nintendo-3ds/debug/deps/*.webm

1
.gitignore vendored

@ -4,3 +4,4 @@ Cargo.lock
# IDE files # IDE files
.idea .idea
.vscode

8
Cargo.toml

@ -1,6 +1,10 @@
[workspace] [workspace]
members = ["ctru-rs", "ctru-sys", "ctru-sys/docstring-to-rustdoc"] members = ["ctru-rs", "ctru-sys"]
default-members = ["ctru-rs", "ctru-sys"]
resolver = "2"
[patch.'https://github.com/rust3ds/ctru-rs'] [patch.'https://github.com/rust3ds/ctru-rs']
# Make sure all dependencies use the local ctru-sys package # Make sure all dependencies use the local packages. This is needed for things
# like pthread-3ds that rely on ctru-sys, and test-runner which relies on ctru-rs
ctru-rs = { path = "ctru-rs" }
ctru-sys = { path = "ctru-sys" } ctru-sys = { path = "ctru-sys" }

28
README.md

@ -1,33 +1,27 @@
# ctru-rs # ctru-rs
A Rust wrapper library for smealum's [ctrulib](https://github.com/smealum/ctrulib). This repository is home of the `ctru-rs` project, which aims to bring full control of the Nintendo 3DS console to homebrew developers using Rust.
## Structure ## Structure
This repository is organized as follows: This repository is organized as follows:
* `ctru-rs`: Safe, idiomatic wrapper around `ctru-sys` * [`ctru-rs`](./ctru-rs/) - Safe, idiomatic wrapper around [`ctru-sys`](./ctru-sys/).
* [`ctru-sys`](./ctru-sys/) - Low-level, unsafe bindings to [`libctru`](https://github.com/devkitPro/libctru).
* `ctru-sys`: Low-level, unsafe bindings to ctrulib. ## Getting Started
This crate's version changes according to the version of `libctru` Specific information about how to use the crates is present in the individual README for each package.
used to generate the bindings, with the following convention: Have a look at `ctru-rs`' [README.md](./ctru-rs/README.md) for a broad overview.
* `libctru` version `X.Y.Z-W`
* `ctru-sys` version `XY.Z.P+X.Y.Z-W`
where `P` is usually 0 but may be incremented for fixes in e.g.
binding generation, `libc` dependency bump, etc.
It may be possible to build this crate against a different version of `libctru`,
but you may encounter linker errors or ABI issues. A build-time Cargo warning
(displayed when built with `-vv`) will be issued if the build script detects
a mismatch or is unable to check the installed `libctru` version.
## Original version ## Original version
This project is based on the efforts the original authors: This project is based on the efforts of the original authors:
* [Eidolon](https://github.com/HybridEidolon) * [Eidolon](https://github.com/HybridEidolon)
* [FenrirWolf](https://github.com/FenrirWolf) * [FenrirWolf](https://github.com/FenrirWolf)
The old version is archived [here](https://github.com/rust3ds/ctru-rs-old). The old version is archived [here](https://github.com/rust3ds/ctru-rs-old).
## License
This project is distributed under the Zlib license.

28
ctru-rs/Cargo.toml

@ -1,11 +1,15 @@
[package] [package]
authors = ["Rust3DS Org", "Ronald Kinard <furyhunter600@gmail.com>"]
description = "A safe wrapper around smealum's ctrulib."
license = "Zlib"
name = "ctru-rs" name = "ctru-rs"
version = "0.7.1" version = "0.7.1"
authors = ["Rust3DS Org", "Ronald Kinard <furyhunter600@gmail.com>"]
description = "A safe wrapper around libctru"
repository = "https://github.com/rust3ds/ctru-rs"
keywords = ["3ds", "libctru"]
categories = ["os", "api-bindings", "hardware-support"]
exclude = ["examples"]
license = "Zlib"
edition = "2021" edition = "2021"
rust-version = "1.64" rust-version = "1.70"
[lib] [lib]
crate-type = ["rlib"] crate-type = ["rlib"]
@ -13,25 +17,26 @@ name = "ctru"
[dependencies] [dependencies]
cfg-if = "1.0" cfg-if = "1.0"
ctru-sys = { path = "../ctru-sys", version = "21.2" } ctru-sys = { path = "../ctru-sys", version = "22.2" }
const-zero = "0.1.0" const-zero = "0.1.0"
shim-3ds = { git = "https://github.com/rust3ds/shim-3ds.git" } shim-3ds = { git = "https://github.com/rust3ds/shim-3ds.git" }
pthread-3ds = { git = "https://github.com/rust3ds/pthread-3ds.git" } pthread-3ds = { git = "https://github.com/rust3ds/pthread-3ds.git" }
libc = "0.2.121" libc = "0.2.121"
bitflags = "1.0.0" bitflags = "2.3.3"
widestring = "0.2.2" widestring = "0.2.2"
[build-dependencies] [build-dependencies]
toml = "0.5" toml = "0.5"
[dev-dependencies] [dev-dependencies]
bytemuck = "1.12.3"
cfg-if = "1.0.0"
ferris-says = "0.2.1" ferris-says = "0.2.1"
futures = "0.3" futures = "0.3"
lewton = "0.10.2"
test-runner = { git = "https://github.com/rust3ds/test-runner.git" }
time = "0.3.7" time = "0.3.7"
tokio = { version = "1.16", features = ["rt", "time", "sync", "macros"] } tokio = { version = "1.16", features = ["rt", "time", "sync", "macros"] }
cfg-if = "1.0.0"
bytemuck = "1.12.3"
lewton = "0.10.2"
[features] [features]
default = ["romfs", "big-stack"] default = ["romfs", "big-stack"]
@ -45,6 +50,11 @@ std-threads = []
[package.metadata.cargo-3ds] [package.metadata.cargo-3ds]
romfs_dir = "examples/romfs" romfs_dir = "examples/romfs"
[package.metadata.docs.rs]
default-target = "armv6k-nintendo-3ds"
targets = []
cargo-args = ["-Z", "build-std"]
[[example]] [[example]]
name = "thread-basic" name = "thread-basic"
required-features = ["std-threads"] required-features = ["std-threads"]

22
ctru-rs/README.md

@ -0,0 +1,22 @@
# ctru-rs
Safe and idiomatic Rust wrapper around [`libctru`](https://github.com/devkitPro/libctru).
## Getting Started
Thoroughly read the [`ctru-rs` wiki](https://github.com/rust3ds/ctru-rs/wiki/Getting-Started) to meet the requirements
and to understand what it takes to develop homebrew software on the Nintendo 3DS family of consoles.
After that, you can simply add the crate as a dependency to your project and build your final binary by using [`cargo-3ds`](https://github.com/rust3ds/cargo-3ds)
or by manually compiling for the `armv6k-nintendo-3ds` target.
## Examples
Many examples to demonstrate the `ctru-rs` functionality are available in the [`examples`](./examples/) folder. Simply run them via
```bash
cargo 3ds run --example <example-name>
```
## License
This project is distributed under the Zlib license.

41
ctru-rs/examples/audio-filters.rs

@ -1,3 +1,7 @@
//! Audio Filters example.
//!
//! This example showcases basic audio functionality using [`Ndsp`].
#![feature(allocator_api)] #![feature(allocator_api)]
use std::f32::consts::PI; use std::f32::consts::PI;
@ -5,21 +9,22 @@ use std::f32::consts::PI;
use ctru::linear::LinearAllocator; use ctru::linear::LinearAllocator;
use ctru::prelude::*; use ctru::prelude::*;
use ctru::services::ndsp::{ use ctru::services::ndsp::{
wave::{Wave, WaveStatus}, wave::{Status, Wave},
AudioFormat, AudioMix, InterpolationType, Ndsp, OutputMode, AudioFormat, AudioMix, InterpolationType, Ndsp, OutputMode,
}; };
// Configuration for the NDSP process and channels.
const SAMPLE_RATE: usize = 22050; const SAMPLE_RATE: usize = 22050;
const SAMPLES_PER_BUF: usize = SAMPLE_RATE / 10; // 2205 const SAMPLES_PER_BUF: usize = SAMPLE_RATE / 10; // 2205
const BYTES_PER_SAMPLE: usize = AudioFormat::PCM16Stereo.size(); const BYTES_PER_SAMPLE: usize = AudioFormat::PCM16Stereo.size();
const AUDIO_WAVE_LENGTH: usize = SAMPLES_PER_BUF * BYTES_PER_SAMPLE; const AUDIO_WAVE_LENGTH: usize = SAMPLES_PER_BUF * BYTES_PER_SAMPLE;
// Note Frequencies // Note frequencies.
const NOTEFREQ: [f32; 7] = [220., 440., 880., 1760., 3520., 7040., 14080.]; const NOTEFREQ: [f32; 7] = [220., 440., 880., 1760., 3520., 7040., 14080.];
// The audio format is Stereo PCM16
// As such, a sample is made up of 2 "Mono" samples (2 * i16 = u32), one for each channel (left and right)
fn fill_buffer(audio_data: &mut [u8], frequency: f32) { fn fill_buffer(audio_data: &mut [u8], frequency: f32) {
// The audio format is Stereo PCM16.
// As such, a sample is made up of 2 "Mono" samples (2 * i16), one for each channel (left and right).
let formatted_data = bytemuck::cast_slice_mut::<_, [i16; 2]>(audio_data); let formatted_data = bytemuck::cast_slice_mut::<_, [i16; 2]>(audio_data);
for (i, chunk) in formatted_data.iter_mut().enumerate() { for (i, chunk) in formatted_data.iter_mut().enumerate() {
@ -35,8 +40,6 @@ fn fill_buffer(audio_data: &mut [u8], frequency: f32) {
} }
fn main() { fn main() {
ctru::use_panic_handler();
let gfx = Gfx::new().expect("Couldn't obtain GFX controller"); let gfx = Gfx::new().expect("Couldn't obtain GFX controller");
let mut hid = Hid::new().expect("Couldn't obtain HID controller"); let mut hid = Hid::new().expect("Couldn't obtain HID controller");
let apt = Apt::new().expect("Couldn't obtain APT controller"); let apt = Apt::new().expect("Couldn't obtain APT controller");
@ -44,8 +47,7 @@ fn main() {
let mut note: usize = 4; let mut note: usize = 4;
// Filters // Filter names to display.
let filter_names = [ let filter_names = [
"None", "None",
"Low-Pass", "Low-Pass",
@ -60,19 +62,26 @@ fn main() {
// We set up two wave buffers and alternate between the two, // We set up two wave buffers and alternate between the two,
// effectively streaming an infinitely long sine wave. // effectively streaming an infinitely long sine wave.
// We create a buffer on the LINEAR memory that will hold our audio data.
// It's necessary for the buffer to live on the LINEAR memory sector since it needs to be accessed by the DSP processor.
let mut audio_data1 = Box::new_in([0u8; AUDIO_WAVE_LENGTH], LinearAllocator); let mut audio_data1 = Box::new_in([0u8; AUDIO_WAVE_LENGTH], LinearAllocator);
// Fill the buffer with the first set of data. This simply writes a sine wave into the buffer.
fill_buffer(audio_data1.as_mut_slice(), NOTEFREQ[4]); fill_buffer(audio_data1.as_mut_slice(), NOTEFREQ[4]);
// Clone the original buffer to obtain an equal buffer on the LINEAR memory used for double buffering.
let audio_data2 = audio_data1.clone(); let audio_data2 = audio_data1.clone();
// Setup two wave info objects with the correct configuration and ownership of the audio data.
let mut wave_info1 = Wave::new(audio_data1, AudioFormat::PCM16Stereo, false); let mut wave_info1 = Wave::new(audio_data1, AudioFormat::PCM16Stereo, false);
let mut wave_info2 = Wave::new(audio_data2, AudioFormat::PCM16Stereo, false); let mut wave_info2 = Wave::new(audio_data2, AudioFormat::PCM16Stereo, false);
let mut ndsp = Ndsp::new().expect("Couldn't obtain NDSP controller"); // Setup the NDSP service and its configuration.
// This line isn't needed since the default NDSP configuration already sets the output mode to `Stereo` let mut ndsp = Ndsp::new().expect("Couldn't obtain NDSP controller");
ndsp.set_output_mode(OutputMode::Stereo); ndsp.set_output_mode(OutputMode::Stereo);
// Channel configuration. We use channel zero but any channel would do just fine.
let mut channel_zero = ndsp.channel(0).unwrap(); let mut channel_zero = ndsp.channel(0).unwrap();
channel_zero.set_interpolation(InterpolationType::Linear); channel_zero.set_interpolation(InterpolationType::Linear);
channel_zero.set_sample_rate(SAMPLE_RATE as f32); channel_zero.set_sample_rate(SAMPLE_RATE as f32);
@ -82,6 +91,7 @@ fn main() {
let mix = AudioMix::default(); let mix = AudioMix::default();
channel_zero.set_mix(&mix); channel_zero.set_mix(&mix);
// First set of queueing for the two buffers. The second one will only play after the first one has ended.
channel_zero.queue_wave(&mut wave_info1).unwrap(); channel_zero.queue_wave(&mut wave_info1).unwrap();
channel_zero.queue_wave(&mut wave_info2).unwrap(); channel_zero.queue_wave(&mut wave_info2).unwrap();
@ -93,6 +103,8 @@ fn main() {
filter_names[filter as usize] filter_names[filter as usize]
); );
println!("\x1b[29;16HPress Start to exit");
let mut altern = true; // true is wave_info1, false is wave_info2 let mut altern = true; // true is wave_info1, false is wave_info2
while apt.main_loop() { while apt.main_loop() {
@ -101,14 +113,16 @@ fn main() {
if keys_down.contains(KeyPad::START) { if keys_down.contains(KeyPad::START) {
break; break;
} // break in order to return to hbmenu }
// Note frequency controller using the buttons.
if keys_down.intersects(KeyPad::DOWN) { if keys_down.intersects(KeyPad::DOWN) {
note = note.saturating_sub(1); note = note.saturating_sub(1);
} else if keys_down.intersects(KeyPad::UP) { } else if keys_down.intersects(KeyPad::UP) {
note = std::cmp::min(note + 1, NOTEFREQ.len() - 1); note = std::cmp::min(note + 1, NOTEFREQ.len() - 1);
} }
// Filter controller using the buttons.
let mut update_params = false; let mut update_params = false;
if keys_down.intersects(KeyPad::LEFT) { if keys_down.intersects(KeyPad::LEFT) {
filter -= 1; filter -= 1;
@ -139,14 +153,16 @@ fn main() {
} }
} }
// Double buffer alternation depending on the one used.
let current: &mut Wave = if altern { let current: &mut Wave = if altern {
&mut wave_info1 &mut wave_info1
} else { } else {
&mut wave_info2 &mut wave_info2
}; };
// If the current buffer has finished playing, we can refill it with new data and re-queue it.
let status = current.status(); let status = current.status();
if let WaveStatus::Done = status { if let Status::Done = status {
fill_buffer(current.get_buffer_mut().unwrap(), NOTEFREQ[note]); fill_buffer(current.get_buffer_mut().unwrap(), NOTEFREQ[note]);
channel_zero.queue_wave(current).unwrap(); channel_zero.queue_wave(current).unwrap();
@ -154,7 +170,6 @@ fn main() {
altern = !altern; altern = !altern;
} }
//Wait for VBlank
gfx.wait_for_vblank(); gfx.wait_for_vblank();
} }
} }

34
ctru-rs/examples/buttons.rs

@ -1,8 +1,10 @@
//! Buttons example.
//!
//! This example showcases how to retrieve button inputs from the console's HID.
use ctru::prelude::*; use ctru::prelude::*;
fn main() { fn main() {
ctru::use_panic_handler();
let apt = Apt::new().unwrap(); let apt = Apt::new().unwrap();
let mut hid = Hid::new().unwrap(); let mut hid = Hid::new().unwrap();
let gfx = Gfx::new().unwrap(); let gfx = Gfx::new().unwrap();
@ -18,29 +20,35 @@ fn main() {
// Scan for user input on the current frame. // Scan for user input on the current frame.
hid.scan_input(); hid.scan_input();
// Get information about which keys were held down on this frame // Get information about which keys were held down on this frame.
let keys = hid.keys_held(); let keys = hid.keys_held();
// Print the status of the volume slider.
println!(
"\x1b[20;0HVolume slider: {} ",
hid.volume_slider()
);
// We only want to print when the keys we're holding now are different // We only want to print when the keys we're holding now are different
// from what they were on the previous frame // from what they were on the previous frame.
if keys != old_keys { if keys != old_keys {
// Clear the screen // Clear the screen.
console.clear(); console.clear();
// We print these again because we just cleared the screen above // We print these again because we just cleared the screen above.
println!("Hi there! Try pressing a button"); println!("Hi there! Try pressing a button");
println!("\x1b[29;16HPress Start to exit"); println!("\x1b[29;16HPress Start to exit");
// Move the cursor back to the top of the screen // Move the cursor back to the top of the screen.
println!("\x1b[3;0H"); println!("\x1b[3;0H");
// Print to the screen depending on which keys were held. // Print to the screen depending on which keys were held.
// //
// The .contains() method checks for all of the provided keys, // The `.contains()` method checks for all of the provided keys,
// and the .intersects() method checks for any of the provided keys. // and the `.intersects()` method checks for any of the provided keys.
// //
// You can also use the .bits() method to do direct comparisons on // You can also use the `.bits()` method to do direct comparisons on
// the underlying bits // the underlying bits.
if keys.contains(KeyPad::A) { if keys.contains(KeyPad::A) {
println!("You held A!"); println!("You held A!");
@ -54,13 +62,13 @@ fn main() {
if keys.intersects(KeyPad::L | KeyPad::R | KeyPad::ZL | KeyPad::ZR) { if keys.intersects(KeyPad::L | KeyPad::R | KeyPad::ZL | KeyPad::ZR) {
println!("You held a shoulder button!"); println!("You held a shoulder button!");
} }
if keys.intersects(KeyPad::START) { if keys.contains(KeyPad::START) {
println!("See ya!"); println!("See ya!");
break; break;
} }
} }
// Save our current key presses for the next frame // Save our current key presses for the next frame.
old_keys = keys; old_keys = keys;
gfx.wait_for_vblank(); gfx.wait_for_vblank();

95
ctru-rs/examples/camera-image.rs

@ -1,40 +1,39 @@
//! Camera image example.
//!
//! This example demonstrates how to use the built-in cameras to take a picture and display it to the screen.
use ctru::prelude::*; use ctru::prelude::*;
use ctru::services::cam::{Cam, Camera, OutputFormat, ShutterSound, ViewSize}; use ctru::services::cam::{
use ctru::services::gfx::{Flush, Screen, Swap}; Cam, Camera, OutputFormat, ShutterSound, Trimming, ViewSize, WhiteBalance,
};
use ctru::services::gfx::{Flush, Screen, Swap, TopScreen3D};
use ctru::services::gspgpu::FramebufferFormat; use ctru::services::gspgpu::FramebufferFormat;
use std::time::Duration; use std::time::Duration;
const WIDTH: usize = 400;
const HEIGHT: usize = 240;
// The screen size is the width and height multiplied by 2 (RGB565 store pixels in 2 bytes)
const BUF_SIZE: usize = WIDTH * HEIGHT * 2;
const WAIT_TIMEOUT: Duration = Duration::from_millis(300); const WAIT_TIMEOUT: Duration = Duration::from_millis(300);
fn main() { fn main() {
ctru::use_panic_handler();
let apt = Apt::new().expect("Failed to initialize Apt service."); let apt = Apt::new().expect("Failed to initialize Apt service.");
let mut hid = Hid::new().expect("Failed to initialize Hid service."); let mut hid = Hid::new().expect("Failed to initialize Hid service.");
let gfx = Gfx::new().expect("Failed to initialize GFX service."); let gfx = Gfx::new().expect("Failed to initialize GFX service.");
let mut top_screen = gfx.top_screen.borrow_mut(); gfx.top_screen.borrow_mut().set_double_buffering(true);
top_screen.set_double_buffering(true); gfx.top_screen
top_screen.set_framebuffer_format(FramebufferFormat::Rgb565); .borrow_mut()
.set_framebuffer_format(FramebufferFormat::Rgb565);
let _console = Console::new(gfx.bottom_screen.borrow_mut()); let mut top_screen_3d = TopScreen3D::from(&gfx.top_screen);
let mut keys_down; let _console = Console::new(gfx.bottom_screen.borrow_mut());
println!("Initializing camera"); println!("Initializing camera");
let mut cam = Cam::new().expect("Failed to initialize CAM service."); let mut cam = Cam::new().expect("Failed to initialize CAM service.");
// Camera setup.
let camera = &mut cam.both_outer_cams;
{ {
let camera = &mut cam.outer_right_cam;
camera camera
.set_view_size(ViewSize::TopLCD) .set_view_size(ViewSize::TopLCD)
.expect("Failed to set camera size"); .expect("Failed to set camera size");
@ -48,48 +47,69 @@ fn main() {
.set_auto_exposure(true) .set_auto_exposure(true)
.expect("Failed to enable auto exposure"); .expect("Failed to enable auto exposure");
camera camera
.set_auto_white_balance(true) .set_white_balance(WhiteBalance::Auto)
.expect("Failed to enable auto white balance"); .expect("Failed to enable auto white balance");
// This line has no effect on the camera since the photos are already shot with `TopLCD` size.
camera camera
.set_trimming(false) .set_trimming(Trimming::new_centered_with_view(ViewSize::TopLCD))
.expect("Failed to disable trimming"); .expect("Failed to enable trimming");
} }
let mut buf = vec![0u8; BUF_SIZE]; // We don't intend on making any other modifications to the camera, so this size should be enough.
let len = camera.final_byte_length();
let mut buf = vec![0u8; len];
println!("\nPress R to take a new picture"); println!("\nPress R to take a new picture");
println!("Press Start to exit to Homebrew Launcher"); println!("Press Start to exit");
while apt.main_loop() { while apt.main_loop() {
hid.scan_input(); hid.scan_input();
keys_down = hid.keys_down(); let keys_down = hid.keys_down();
if keys_down.contains(KeyPad::START) { if keys_down.contains(KeyPad::START) {
break; break;
} }
// If the user presses the R button.
if keys_down.contains(KeyPad::R) { if keys_down.contains(KeyPad::R) {
println!("Capturing new image"); println!("Capturing new image");
let camera = &mut cam.outer_right_cam; let camera = &mut cam.both_outer_cams;
// Take a picture and write it to the buffer.
camera camera
.take_picture( .take_picture(&mut buf, WAIT_TIMEOUT)
&mut buf,
WIDTH.try_into().unwrap(),
HEIGHT.try_into().unwrap(),
WAIT_TIMEOUT,
)
.expect("Failed to take picture"); .expect("Failed to take picture");
let (width, height) = camera.final_view_size();
// Play the normal shutter sound.
cam.play_shutter_sound(ShutterSound::Normal) cam.play_shutter_sound(ShutterSound::Normal)
.expect("Failed to play shutter sound"); .expect("Failed to play shutter sound");
rotate_image_to_screen(&buf, top_screen.raw_framebuffer().ptr, WIDTH, HEIGHT); {
let (mut left_side, mut right_side) = top_screen_3d.split_mut();
// Rotate the left image and correctly display it on the screen.
rotate_image_to_screen(
&buf,
left_side.raw_framebuffer().ptr,
width as usize,
height as usize,
);
// Rotate the right image and correctly display it on the screen.
rotate_image_to_screen(
&buf[len / 2..],
right_side.raw_framebuffer().ptr,
width as usize,
height as usize,
);
}
// We will only flush the "camera" screen, since the other screen is handled by `Console` // We will only flush and swap the "camera" screen, since the other screen is handled by the `Console`.
top_screen.flush_buffers(); top_screen_3d.flush_buffers();
top_screen.swap_buffers(); top_screen_3d.swap_buffers();
gfx.wait_for_vblank(); gfx.wait_for_vblank();
} }
@ -99,6 +119,7 @@ fn main() {
// The 3DS' screens are 2 vertical LCD panels rotated by 90 degrees. // The 3DS' screens are 2 vertical LCD panels rotated by 90 degrees.
// As such, we'll need to write a "vertical" image to the framebuffer to have it displayed properly. // As such, we'll need to write a "vertical" image to the framebuffer to have it displayed properly.
// This functions rotates an horizontal image by 90 degrees to the right. // This functions rotates an horizontal image by 90 degrees to the right.
// This function is only supposed to be used in this example. In a real world application, the program should use the GPU to draw to the screen.
fn rotate_image_to_screen(src: &[u8], framebuf: *mut u8, width: usize, height: usize) { fn rotate_image_to_screen(src: &[u8], framebuf: *mut u8, width: usize, height: usize) {
for j in 0..height { for j in 0..height {
for i in 0..width { for i in 0..width {
@ -115,10 +136,10 @@ fn rotate_image_to_screen(src: &[u8], framebuf: *mut u8, width: usize, height: u
let draw_index = (draw_x * height + draw_y) * 2; // This 2 stands for the number of bytes per pixel (16 bits) let draw_index = (draw_x * height + draw_y) * 2; // This 2 stands for the number of bytes per pixel (16 bits)
unsafe { unsafe {
// We'll work with pointers since the frambuffer is a raw pointer regardless. // We'll work with pointers since the framebuffer is a raw pointer regardless.
// The offsets are completely safe as long as the width and height are correct. // The offsets are completely safe as long as the width and height are correct.
let pixel_pointer = framebuf.offset(draw_index as isize); let pixel_pointer = framebuf.add(draw_index);
pixel_pointer.copy_from(src.as_ptr().offset(read_index as isize), 2); pixel_pointer.copy_from(src.as_ptr().add(read_index), 2);
} }
} }
} }

26
ctru-rs/examples/file-explorer.rs

@ -1,7 +1,9 @@
//! A file explorer which shows off using standard library file system APIs to //! File Explorer example.
//! read the SD card. //!
//! This (rather complex) example creates a working text-based file explorer which shows off using standard library file system APIs to
//! read the SD card and RomFS (if properly read via the `romfs:/` prefix).
use ctru::applets::swkbd::{Button, Swkbd}; use ctru::applets::swkbd::{Button, SoftwareKeyboard};
use ctru::prelude::*; use ctru::prelude::*;
use std::fs::DirEntry; use std::fs::DirEntry;
@ -9,12 +11,11 @@ use std::os::horizon::fs::MetadataExt;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
fn main() { fn main() {
ctru::use_panic_handler();
let apt = Apt::new().unwrap(); let apt = Apt::new().unwrap();
let mut hid = Hid::new().unwrap(); let mut hid = Hid::new().unwrap();
let gfx = Gfx::new().unwrap(); let gfx = Gfx::new().unwrap();
// Mount the RomFS if available.
#[cfg(all(feature = "romfs", romfs_exists))] #[cfg(all(feature = "romfs", romfs_exists))]
let _romfs = ctru::services::romfs::RomFS::new().unwrap(); let _romfs = ctru::services::romfs::RomFS::new().unwrap();
@ -50,6 +51,7 @@ impl<'a> FileExplorer<'a> {
fn run(&mut self) { fn run(&mut self) {
self.running = true; self.running = true;
// Print the file explorer commands.
self.print_menu(); self.print_menu();
while self.running && self.apt.main_loop() { while self.running && self.apt.main_loop() {
@ -62,8 +64,10 @@ impl<'a> FileExplorer<'a> {
self.path.pop(); self.path.pop();
self.console.clear(); self.console.clear();
self.print_menu(); self.print_menu();
// Open a directory/file to read.
} else if input.contains(KeyPad::A) { } else if input.contains(KeyPad::A) {
self.get_input_and_run(Self::set_next_path); self.get_input_and_run(Self::set_next_path);
// Open a specific path using the `SoftwareKeyboard` applet.
} else if input.contains(KeyPad::X) { } else if input.contains(KeyPad::X) {
self.get_input_and_run(Self::set_exact_path); self.get_input_and_run(Self::set_exact_path);
} }
@ -100,7 +104,7 @@ impl<'a> FileExplorer<'a> {
} }
}; };
println!("Start to exit, A to select an entry by number, B to go up a directory, X to set the path."); println!("Press Start to exit, A to select an entry by number, B to go up a directory, X to set the path.");
} }
fn print_dir_entries(&mut self) { fn print_dir_entries(&mut self) {
@ -137,7 +141,7 @@ impl<'a> FileExplorer<'a> {
} }
} }
/// Paginate output // Paginate output.
fn wait_for_page_down(&mut self) { fn wait_for_page_down(&mut self) {
println!("Press A to go to next page, or Start to exit"); println!("Press A to go to next page, or Start to exit");
@ -159,18 +163,18 @@ impl<'a> FileExplorer<'a> {
} }
fn get_input_and_run(&mut self, action: impl FnOnce(&mut Self, String)) { fn get_input_and_run(&mut self, action: impl FnOnce(&mut Self, String)) {
let mut keyboard = Swkbd::default(); let mut keyboard = SoftwareKeyboard::default();
match keyboard.get_string(2048) { match keyboard.get_string(2048) {
Ok((path, Button::Right)) => { Ok((path, Button::Right)) => {
// Clicked "OK" // Clicked "OK".
action(self, path); action(self, path);
} }
Ok((_, Button::Left)) => { Ok((_, Button::Left)) => {
// Clicked "Cancel" // Clicked "Cancel".
} }
Ok((_, Button::Middle)) => { Ok((_, Button::Middle)) => {
// This button wasn't shown // This button wasn't shown.
unreachable!() unreachable!()
} }
Err(e) => { Err(e) => {

2
ctru-rs/examples/futures-basic.rs

@ -13,8 +13,6 @@ use futures::StreamExt;
use std::os::horizon::thread::BuilderExt; use std::os::horizon::thread::BuilderExt;
fn main() { fn main() {
ctru::use_panic_handler();
let gfx = Gfx::new().expect("Couldn't obtain GFX controller"); let gfx = Gfx::new().expect("Couldn't obtain GFX controller");
let mut hid = Hid::new().expect("Couldn't obtain HID controller"); let mut hid = Hid::new().expect("Couldn't obtain HID controller");
let apt = Apt::new().expect("Couldn't obtain APT controller"); let apt = Apt::new().expect("Couldn't obtain APT controller");

2
ctru-rs/examples/futures-tokio.rs

@ -6,8 +6,6 @@ use std::os::horizon::thread::BuilderExt;
use std::time::Duration; use std::time::Duration;
fn main() { fn main() {
ctru::use_panic_handler();
let gfx = Gfx::new().expect("Couldn't obtain GFX controller"); let gfx = Gfx::new().expect("Couldn't obtain GFX controller");
let mut hid = Hid::new().expect("Couldn't obtain HID controller"); let mut hid = Hid::new().expect("Couldn't obtain HID controller");
let apt = Apt::new().expect("Couldn't obtain APT controller"); let apt = Apt::new().expect("Couldn't obtain APT controller");

33
ctru-rs/examples/gfx-3d-mode.rs

@ -1,23 +1,30 @@
//! 3D Graphics example.
//!
//! This example showcases 3D mode rendering (using the CPU).
//! In a normal application, all rendering should be handled via the GPU.
//!
//! See `gfx-bitmap.rs` for details on how the image is generated.
//!
//! # Warning
//!
//! This example uses 3D mode in a rather unnatural way, and should
//! probably not be viewed for too long or at all if you are photosensitive.
use ctru::prelude::*; use ctru::prelude::*;
use ctru::services::gfx::{Flush, Screen, Side, Swap, TopScreen3D}; use ctru::services::gfx::{Flush, Screen, Side, Swap, TopScreen3D};
/// See `graphics-bitmap.rs` for details on how the image is generated.
///
/// WARNING: this example uses 3D mode in a rather unnatural way, and should
/// probably not be viewed for too long or at all if you are photosensitive.
const IMAGE: &[u8] = include_bytes!("assets/ferris.rgb"); const IMAGE: &[u8] = include_bytes!("assets/ferris.rgb");
static ZERO: &[u8] = &[0; IMAGE.len()]; static ZERO: &[u8] = &[0; IMAGE.len()];
fn main() { fn main() {
ctru::use_panic_handler();
let gfx = Gfx::new().expect("Couldn't obtain GFX controller"); let gfx = Gfx::new().expect("Couldn't obtain GFX controller");
let mut hid = Hid::new().expect("Couldn't obtain HID controller"); let mut hid = Hid::new().expect("Couldn't obtain HID controller");
let apt = Apt::new().expect("Couldn't obtain APT controller"); let apt = Apt::new().expect("Couldn't obtain APT controller");
let _console = Console::new(gfx.bottom_screen.borrow_mut()); let _console = Console::new(gfx.bottom_screen.borrow_mut());
println!("Press Start to exit.\nPress A to switch sides (be sure to have 3D mode enabled)."); println!("Press A to switch sides.");
println!("Make sure to have set the 3D slider correctly");
println!("\x1b[29;12HPress Start to exit");
gfx.top_screen.borrow_mut().set_double_buffering(true); gfx.top_screen.borrow_mut().set_double_buffering(true);
@ -25,39 +32,40 @@ fn main() {
let mut current_side = Side::Left; let mut current_side = Side::Left;
// Main loop
while apt.main_loop() { while apt.main_loop() {
//Scan all the inputs. This should be done once for each frame
hid.scan_input(); hid.scan_input();
if hid.keys_down().contains(KeyPad::START) { if hid.keys_down().contains(KeyPad::START) {
break; break;
} }
// Split the TopScreen3D to get references to the two render surfaces.
let (mut left, mut right) = top_screen.split_mut(); let (mut left, mut right) = top_screen.split_mut();
let left_buf = left.raw_framebuffer(); let left_buf = left.raw_framebuffer();
let right_buf = right.raw_framebuffer(); let right_buf = right.raw_framebuffer();
// Clear both buffers every time, in case the user switches sides this loop // Clear both buffers every time, in case the user switches sides this loop.
unsafe { unsafe {
left_buf.ptr.copy_from(ZERO.as_ptr(), ZERO.len()); left_buf.ptr.copy_from(ZERO.as_ptr(), ZERO.len());
right_buf.ptr.copy_from(ZERO.as_ptr(), ZERO.len()); right_buf.ptr.copy_from(ZERO.as_ptr(), ZERO.len());
} }
if hid.keys_down().contains(KeyPad::A) { if hid.keys_down().contains(KeyPad::A) {
// flip which buffer we're writing to // Switch which buffer we're writing to.
current_side = match current_side { current_side = match current_side {
Side::Left => Side::Right, Side::Left => Side::Right,
Side::Right => Side::Left, Side::Right => Side::Left,
}; };
} }
// Obtain the framebuffer of the currently rendered side.
let buf = match current_side { let buf = match current_side {
Side::Left => left_buf.ptr, Side::Left => left_buf.ptr,
Side::Right => right_buf.ptr, Side::Right => right_buf.ptr,
}; };
// Render the image to the surface's buffer.
unsafe { unsafe {
buf.copy_from(IMAGE.as_ptr(), IMAGE.len()); buf.copy_from(IMAGE.as_ptr(), IMAGE.len());
} }
@ -67,7 +75,6 @@ fn main() {
top_screen.flush_buffers(); top_screen.flush_buffers();
top_screen.swap_buffers(); top_screen.swap_buffers();
//Wait for VBlank
gfx.wait_for_vblank(); gfx.wait_for_vblank();
} }
} }

30
ctru-rs/examples/gfx-bitmap.rs

@ -1,3 +1,6 @@
/// Bitmap Graphics example.
///
/// This example uses the CPU to render a simple bitmap image to the screen.
use ctru::prelude::*; use ctru::prelude::*;
use ctru::services::gfx::{Flush, Screen, Swap}; use ctru::services::gfx::{Flush, Screen, Swap};
@ -15,14 +18,13 @@ use ctru::services::gfx::{Flush, Screen, Swap};
static IMAGE: &[u8] = include_bytes!("assets/ferris.rgb"); static IMAGE: &[u8] = include_bytes!("assets/ferris.rgb");
fn main() { fn main() {
ctru::use_panic_handler();
let gfx = Gfx::new().expect("Couldn't obtain GFX controller"); let gfx = Gfx::new().expect("Couldn't obtain GFX controller");
let mut hid = Hid::new().expect("Couldn't obtain HID controller"); let mut hid = Hid::new().expect("Couldn't obtain HID controller");
let apt = Apt::new().expect("Couldn't obtain APT controller"); let apt = Apt::new().expect("Couldn't obtain APT controller");
let _console = Console::new(gfx.top_screen.borrow_mut()); let _console = Console::new(gfx.top_screen.borrow_mut());
println!("\x1b[21;4HPress Start to exit, or A to flip the image."); println!("\x1b[21;4HPress A to flip the image.");
println!("\x1b[29;16HPress Start to exit");
let mut bottom_screen = gfx.bottom_screen.borrow_mut(); let mut bottom_screen = gfx.bottom_screen.borrow_mut();
@ -37,38 +39,44 @@ fn main() {
let mut image_bytes = IMAGE; let mut image_bytes = IMAGE;
// Main loop // We assume the image is the correct size already, so we drop width + height.
let frame_buffer = bottom_screen.raw_framebuffer();
// We copy the image to the framebuffer.
unsafe {
frame_buffer
.ptr
.copy_from(image_bytes.as_ptr(), image_bytes.len());
}
while apt.main_loop() { while apt.main_loop() {
//Scan all the inputs. This should be done once for each frame
hid.scan_input(); hid.scan_input();
if hid.keys_down().contains(KeyPad::START) { if hid.keys_down().contains(KeyPad::START) {
break; break;
} }
// We assume the image is the correct size already, so we drop width + height.
let frame_buffer = bottom_screen.raw_framebuffer();
if hid.keys_down().contains(KeyPad::A) { if hid.keys_down().contains(KeyPad::A) {
image_bytes = if std::ptr::eq(image_bytes, IMAGE) { image_bytes = if std::ptr::eq(image_bytes, IMAGE) {
&flipped_image[..] &flipped_image[..]
} else { } else {
IMAGE IMAGE
}; };
}
// this copies more than necessary (once per frame) but it's fine... let frame_buffer = bottom_screen.raw_framebuffer();
// We render the newly switched image to the framebuffer.
unsafe { unsafe {
frame_buffer frame_buffer
.ptr .ptr
.copy_from(image_bytes.as_ptr(), image_bytes.len()); .copy_from(image_bytes.as_ptr(), image_bytes.len());
} }
}
// Flush framebuffers. Since we're not using double buffering, // Flush framebuffers. Since we're not using double buffering,
// this will render the pixels immediately // this will render the pixels immediately
bottom_screen.flush_buffers(); bottom_screen.flush_buffers();
//Wait for VBlank
gfx.wait_for_vblank(); gfx.wait_for_vblank();
} }
} }

14
ctru-rs/examples/gfx-wide-mode.rs

@ -1,14 +1,20 @@
//! Wide-Mode Graphics example.
//!
//! This example demonstrates the wide-mode capability of the top screen
//! which doubles the horizontal resolution of the screen by merging the 2 stereoscopic 3D sides.
//!
//! Beware, wide-mode doesn't work on Old 2DS consoles.
use ctru::prelude::*; use ctru::prelude::*;
fn main() { fn main() {
ctru::use_panic_handler();
let apt = Apt::new().unwrap(); let apt = Apt::new().unwrap();
let mut hid = Hid::new().unwrap(); let mut hid = Hid::new().unwrap();
let gfx = Gfx::new().unwrap(); let gfx = Gfx::new().unwrap();
let mut console = Console::new(gfx.top_screen.borrow_mut()); let mut console = Console::new(gfx.top_screen.borrow_mut());
println!("Press A to enable/disable wide screen mode."); println!("Press A to enable/disable wide screen mode.");
println!("\x1b[29;16HPress Start to exit");
while apt.main_loop() { while apt.main_loop() {
hid.scan_input(); hid.scan_input();
@ -17,14 +23,18 @@ fn main() {
break; break;
} }
// Since we can't set wide-mode while running the console (since that would break the currently displayed text),
// we need to rebuild the console each time we want to make the switch.
if hid.keys_down().contains(KeyPad::A) { if hid.keys_down().contains(KeyPad::A) {
drop(console); drop(console);
// Switch the state of the wide-mode.
let wide_mode = gfx.top_screen.borrow().is_wide(); let wide_mode = gfx.top_screen.borrow().is_wide();
gfx.top_screen.borrow_mut().set_wide_mode(!wide_mode); gfx.top_screen.borrow_mut().set_wide_mode(!wide_mode);
console = Console::new(gfx.top_screen.borrow_mut()); console = Console::new(gfx.top_screen.borrow_mut());
println!("Press A to enable/disable wide screen mode."); println!("Press A to enable/disable wide screen mode.");
println!("\x1b[29;16HPress Start to exit");
} }
gfx.wait_for_vblank(); gfx.wait_for_vblank();

57
ctru-rs/examples/graphics-bitmap.rs

@ -1,57 +0,0 @@
use ctru::prelude::*;
use ctru::services::gfx::{Flush, Screen, Swap};
/// Ferris image taken from <https://rustacean.net> and scaled down to 320x240px.
/// To regenerate the data, you will need to install `imagemagick` and run this
/// command from the `examples` directory:
///
/// ```sh
/// magick assets/ferris.png -channel-fx "red<=>blue" -rotate 90 assets/ferris.rgb
/// ```
///
/// This creates an image appropriate for the default frame buffer format of
/// [`Bgr8`](ctru::services::gspgpu::FramebufferFormat::Bgr8)
/// and rotates the image 90° to account for the portrait mode screen.
static IMAGE: &[u8] = include_bytes!("assets/ferris.rgb");
fn main() {
ctru::use_panic_handler();
let gfx = Gfx::new().expect("Couldn't obtain GFX controller");
let mut hid = Hid::new().expect("Couldn't obtain HID controller");
let apt = Apt::new().expect("Couldn't obtain APT controller");
let _console = Console::new(gfx.top_screen.borrow_mut());
println!("\x1b[21;16HPress Start to exit.");
let mut bottom_screen = gfx.bottom_screen.borrow_mut();
// We don't need double buffering in this example.
// In this way we can draw our image only once on screen.
bottom_screen.set_double_buffering(false);
// We assume the image is the correct size already, so we drop width + height.
let frame_buffer = bottom_screen.raw_framebuffer();
// Copy the image into the frame buffer
unsafe {
frame_buffer.ptr.copy_from(IMAGE.as_ptr(), IMAGE.len());
}
// Main loop
while apt.main_loop() {
//Scan all the inputs. This should be done once for each frame
hid.scan_input();
if hid.keys_down().contains(KeyPad::START) {
break;
}
// Flush and swap framebuffers
bottom_screen.flush_buffers();
bottom_screen.swap_buffers();
//Wait for VBlank
gfx.wait_for_vblank();
}
}

13
ctru-rs/examples/hashmaps.rs

@ -1,10 +1,14 @@
//! Hashmap example.
//!
//! This example showcases using Hashmaps on the 3DS console using the functionality implemented by the standard library.
//! While it may seem inappropriate for such a simple (and somewhat out-of-scope) example to be included here, it's important to note how
//! normally Hashmaps wouldn't work on the console, and are only capable to because of the internal implementations made by `ctru-rs`.
//!
//! As such, this example functions more closely to a test than a demonstration.
use ctru::prelude::*; use ctru::prelude::*;
fn main() { fn main() {
ctru::use_panic_handler();
// Initialize services
//
// HashMaps generate hashes thanks to the 3DS' cryptografically secure generator. // HashMaps generate hashes thanks to the 3DS' cryptografically secure generator.
// This generator is only active when activating the `PS` service. // This generator is only active when activating the `PS` service.
// This service is automatically initialized. // This service is automatically initialized.
@ -19,6 +23,7 @@ fn main() {
map.remove("A Key!"); map.remove("A Key!");
println!("{map:#?}"); println!("{map:#?}");
println!("\x1b[29;16HPress Start to exit");
while apt.main_loop() { while apt.main_loop() {
gfx.wait_for_vblank(); gfx.wait_for_vblank();

13
ctru-rs/examples/hello-both-screens.rs

@ -1,8 +1,10 @@
//! Hello World example using both screens.
//!
//! This is similar to the `hello-world` example, with the main difference of using 2 virtual `Console`s that can be alternated to print on both screens.
use ctru::prelude::*; use ctru::prelude::*;
fn main() { fn main() {
ctru::use_panic_handler();
let apt = Apt::new().unwrap(); let apt = Apt::new().unwrap();
let mut hid = Hid::new().unwrap(); let mut hid = Hid::new().unwrap();
let gfx = Gfx::new().unwrap(); let gfx = Gfx::new().unwrap();
@ -11,14 +13,15 @@ fn main() {
let top_screen = Console::new(gfx.top_screen.borrow_mut()); let top_screen = Console::new(gfx.top_screen.borrow_mut());
// Start a console on the bottom screen. // Start a console on the bottom screen.
// The most recently initialized console will be active by default // The most recently initialized console will be active by default.
let bottom_screen = Console::new(gfx.bottom_screen.borrow_mut()); let bottom_screen = Console::new(gfx.bottom_screen.borrow_mut());
// Let's print on the top screen first // Let's print on the top screen first.
// Since the bottom screen is currently selected (being created afterwards), it is required to select the top screen console first.
top_screen.select(); top_screen.select();
println!("This is the top screen! We have a lot of space up here!"); println!("This is the top screen! We have a lot of space up here!");
// Now let's print something on the bottom screen // Now let's print something on the bottom screen.
bottom_screen.select(); bottom_screen.select();
println!("\x1b[14;00HThis is the bottom screen."); println!("\x1b[14;00HThis is the bottom screen.");
println!("There's not as much space down here, but that's okay."); println!("There's not as much space down here, but that's okay.");

20
ctru-rs/examples/hello-world.rs

@ -1,15 +1,21 @@
use ctru::prelude::*; //! Hello World example.
//!
//! Simple "Hello World" application to showcase the basic setup needed for any user-oriented app to work.
use ctru::prelude::*;
use std::io::BufWriter; use std::io::BufWriter;
fn main() { fn main() {
ctru::use_panic_handler(); // Setup Graphics, Controller Inputs, Application runtime.
// These is standard setup any app would need.
let gfx = Gfx::new().expect("Couldn't obtain GFX controller"); let gfx = Gfx::new().expect("Couldn't obtain GFX controller");
let mut hid = Hid::new().expect("Couldn't obtain HID controller"); let mut hid = Hid::new().expect("Couldn't obtain HID controller");
let apt = Apt::new().expect("Couldn't obtain APT controller"); let apt = Apt::new().expect("Couldn't obtain APT controller");
// Create a Console to print our "Hello, World!" to.
let _console = Console::new(gfx.top_screen.borrow_mut()); let _console = Console::new(gfx.top_screen.borrow_mut());
// Snazzy message created via `ferris_says`.
let out = b"Hello fellow Rustaceans, I'm on the Nintendo 3DS!"; let out = b"Hello fellow Rustaceans, I'm on the Nintendo 3DS!";
let width = 24; let width = 24;
@ -21,16 +27,18 @@ fn main() {
String::from_utf8_lossy(&writer.into_inner().unwrap()) String::from_utf8_lossy(&writer.into_inner().unwrap())
); );
// Main loop println!("\x1b[29;16HPress Start to exit");
// Main application loop. This checks whether the app is normally running in the foreground.
while apt.main_loop() { while apt.main_loop() {
//Scan all the inputs. This should be done once for each frame // Scan all the controller inputs.
hid.scan_input(); hid.scan_input();
if hid.keys_down().contains(KeyPad::START) { if hid.keys_down().contains(KeyPad::START) {
break; break;
} }
//Wait for VBlank // Use VSync to cap the framerate at the same speed as the LCD screen's refresh rate (60 fps).
gfx.wait_for_vblank(); gfx.wait_for_vblank();
} }
} }

17
ctru-rs/examples/linear-memory.rs

@ -1,21 +1,27 @@
//! LINEAR memory example.
//!
//! This example showcases simple allocation on the LINEAR memory sector.
//! Using LINEAR memory is required when sending data to the GPU or DSP processor.
// You will need to activate this unstable feature to use custom allocators.
#![feature(allocator_api)] #![feature(allocator_api)]
use ctru::linear::LinearAllocator; use ctru::linear::LinearAllocator;
use ctru::prelude::*; use ctru::prelude::*;
fn main() { fn main() {
ctru::use_panic_handler();
let gfx = Gfx::new().expect("Couldn't obtain GFX controller"); let gfx = Gfx::new().expect("Couldn't obtain GFX controller");
let mut hid = Hid::new().expect("Couldn't obtain HID controller"); let mut hid = Hid::new().expect("Couldn't obtain HID controller");
let apt = Apt::new().expect("Couldn't obtain APT controller"); let apt = Apt::new().expect("Couldn't obtain APT controller");
let _console = Console::new(gfx.top_screen.borrow_mut()); let _console = Console::new(gfx.top_screen.borrow_mut());
// The `LinearAllocator` is always available for use.
// Luckily, we can always read how much memory is available to be allocated on it.
let linear_space_before = LinearAllocator::free_space(); let linear_space_before = LinearAllocator::free_space();
// Normal `Box` on the heap // Normal `Box` on the heap.
let heap_box = Box::new(1492); let heap_box = Box::new(1492);
// `Box` living on the linear memory sector // `Box` living on the LINEAR memory.
let linear_box: Box<i32, LinearAllocator> = Box::new_in(2022, LinearAllocator); let linear_box: Box<i32, LinearAllocator> = Box::new_in(2022, LinearAllocator);
println!("This value is from the heap: {heap_box}"); println!("This value is from the heap: {heap_box}");
@ -29,16 +35,13 @@ fn main() {
println!("\x1b[29;16HPress Start to exit"); println!("\x1b[29;16HPress Start to exit");
// Main loop
while apt.main_loop() { while apt.main_loop() {
//Scan all the inputs. This should be done once for each frame
hid.scan_input(); hid.scan_input();
if hid.keys_down().contains(KeyPad::START) { if hid.keys_down().contains(KeyPad::START) {
break; break;
} }
//Wait for VBlank
gfx.wait_for_vblank(); gfx.wait_for_vblank();
} }
} }

29
ctru-rs/examples/mii-selector.rs

@ -1,22 +1,28 @@
use ctru::applets::mii_selector::MiiSelector; //! Mii Selector example.
//!
//! This example showcases the use of the MiiSelector applet to obtain Mii data from the user's input.
use ctru::applets::mii_selector::{Error, MiiSelector, Options};
use ctru::prelude::*; use ctru::prelude::*;
fn main() { fn main() {
ctru::use_panic_handler();
let gfx = Gfx::new().expect("Couldn't obtain GFX controller"); let gfx = Gfx::new().expect("Couldn't obtain GFX controller");
let mut hid = Hid::new().expect("Couldn't obtain HID controller"); let mut hid = Hid::new().expect("Couldn't obtain HID controller");
let apt = Apt::new().expect("Couldn't obtain APT controller"); let apt = Apt::new().expect("Couldn't obtain APT controller");
let _console = Console::new(gfx.top_screen.borrow_mut()); let _console = Console::new(gfx.top_screen.borrow_mut());
// Setup the Mii Selector configuration.
let mut mii_selector = MiiSelector::new(); let mut mii_selector = MiiSelector::new();
// The Mii Selector window can be closed without selecting a Mii.
mii_selector.set_options(Options::ENABLE_CANCEL);
mii_selector.set_initial_index(3); mii_selector.set_initial_index(3);
mii_selector.blacklist_user_mii(0.into()); // The first user-made Mii cannot be used.
mii_selector.blocklist_user_mii(0.into());
mii_selector.set_title("Great Mii Selector!"); mii_selector.set_title("Great Mii Selector!");
let result = mii_selector.launch().unwrap(); // Launch the Mii Selector and use its result to print the selected Mii's information.
match mii_selector.launch() {
println!("Is Mii selected?: {:?}", result.is_mii_selected); Ok(result) => {
println!("Mii type: {:?}", result.mii_type); println!("Mii type: {:?}", result.mii_type);
println!("Name: {:?}", result.mii_data.name); println!("Name: {:?}", result.mii_data.name);
println!("Author: {:?}", result.mii_data.author_name); println!("Author: {:?}", result.mii_data.author_name);
@ -24,17 +30,20 @@ fn main() {
"Does the Mii have moles?: {:?}", "Does the Mii have moles?: {:?}",
result.mii_data.mole_details.is_enabled result.mii_data.mole_details.is_enabled
); );
}
Err(Error::InvalidChecksum) => println!("Corrupt Mii selected"),
Err(Error::NoMiiSelected) => println!("No Mii selected"),
}
println!("\x1b[29;16HPress Start to exit");
// Main loop
while apt.main_loop() { while apt.main_loop() {
//Scan all the inputs. This should be done once for each frame
hid.scan_input(); hid.scan_input();
if hid.keys_down().contains(KeyPad::START) { if hid.keys_down().contains(KeyPad::START) {
break; break;
} }
//Wait for VBlank
gfx.wait_for_vblank(); gfx.wait_for_vblank();
} }
} }

52
ctru-rs/examples/movement.rs

@ -0,0 +1,52 @@
//! Movement example.
//!
//! Simple application to showcase the use of the accelerometer and gyroscope.
use ctru::prelude::*;
fn main() {
let gfx = Gfx::new().expect("Couldn't obtain GFX controller");
let mut hid = Hid::new().expect("Couldn't obtain HID controller");
let apt = Apt::new().expect("Couldn't obtain APT controller");
let _console = Console::new(gfx.top_screen.borrow_mut());
println!("Move the console around!");
println!("\x1b[29;16HPress Start to exit");
// Activate the accelerometer and the gyroscope.
// Because of the complex nature of the movement sensors, they aren't activated by default with the `Hid` service.
// However, they can simply be turned on and off whenever necessary.
hid.set_accelerometer(true)
.expect("Couldn't activate accelerometer");
hid.set_gyroscope(true)
.expect("Couldn't activate gyroscope");
while apt.main_loop() {
// Scan all the controller inputs.
// Accelerometer and gyroscope require this step to update the readings.
hid.scan_input();
if hid.keys_down().contains(KeyPad::START) {
break;
}
// Be careful: reading without activating the sensors (as done before this loop) will result in a panic.
println!(
"\x1b[3;0HAcceleration: {:?} ",
<(i16, i16, i16)>::from(
hid.accelerometer_vector()
.expect("could not retrieve acceleration vector")
)
);
println!(
"\x1b[4;0HGyroscope angular rate: {:?} ",
Into::<(i16, i16, i16)>::into(
hid.gyroscope_rate()
.expect("could not retrieve angular rate")
)
);
gfx.wait_for_vblank();
}
}

33
ctru-rs/examples/network-sockets.rs

@ -1,3 +1,7 @@
//! Network Sockets example.
//!
//! This example showcases the use of network sockets via the `Soc` service and the standard library's implementations.
use ctru::prelude::*; use ctru::prelude::*;
use std::io::{self, Read, Write}; use std::io::{self, Read, Write};
@ -5,29 +9,36 @@ use std::net::{Shutdown, TcpListener};
use std::time::Duration; use std::time::Duration;
fn main() { fn main() {
ctru::use_panic_handler();
let gfx = Gfx::new().unwrap(); let gfx = Gfx::new().unwrap();
let _console = Console::new(gfx.top_screen.borrow_mut());
let mut hid = Hid::new().unwrap(); let mut hid = Hid::new().unwrap();
let apt = Apt::new().unwrap(); let apt = Apt::new().unwrap();
println!("\nlibctru sockets demo\n"); // Owning a living handle to the `Soc` service is required to use network functionalities.
let soc = Soc::new().unwrap(); let soc = Soc::new().unwrap();
// Listen on the standard HTTP port (80).
let server = TcpListener::bind("0.0.0.0:80").unwrap(); let server = TcpListener::bind("0.0.0.0:80").unwrap();
server.set_nonblocking(true).unwrap(); server.set_nonblocking(true).unwrap();
println!("Point your browser to http://{}/\n", soc.host_address()); let _bottom_console = Console::new(gfx.bottom_screen.borrow_mut());
println!("Point your browser at:\nhttp://{}/\n", soc.host_address());
println!("\x1b[29;12HPress Start to exit");
let _top_console = Console::new(gfx.top_screen.borrow_mut());
while apt.main_loop() { while apt.main_loop() {
gfx.wait_for_vblank(); hid.scan_input();
if hid.keys_down().contains(KeyPad::START) {
break;
};
// Receive any incoming connections.
match server.accept() { match server.accept() {
Ok((mut stream, socket_addr)) => { Ok((mut stream, socket_addr)) => {
println!("Got connection from {socket_addr}"); println!("Got connection from {socket_addr}");
// Print the HTTP request sent by the client (most likely, a web browser).
let mut buf = [0u8; 4096]; let mut buf = [0u8; 4096];
match stream.read(&mut buf) { match stream.read(&mut buf) {
Ok(_) => { Ok(_) => {
@ -43,15 +54,18 @@ fn main() {
} }
} }
// Return a HTML page with a simple "Hello World!".
let response = b"HTTP/1.1 200 OK\r\nContent-Type: text/html; charset=UTF-8\r\n\r\n<html><body>Hello world</body></html>\r\n"; let response = b"HTTP/1.1 200 OK\r\nContent-Type: text/html; charset=UTF-8\r\n\r\n<html><body>Hello world</body></html>\r\n";
if let Err(e) = stream.write(response) { if let Err(e) = stream.write(response) {
println!("Error writing http response: {e}"); println!("Error writing http response: {e}");
} }
// Shutdown the stream (depending on the web browser used to view the page, this might cause some issues).
stream.shutdown(Shutdown::Both).unwrap(); stream.shutdown(Shutdown::Both).unwrap();
} }
Err(e) => match e.kind() { Err(e) => match e.kind() {
// If the TCP socket would block execution, just try again.
std::io::ErrorKind::WouldBlock => {} std::io::ErrorKind::WouldBlock => {}
_ => { _ => {
println!("Error accepting connection: {e}"); println!("Error accepting connection: {e}");
@ -60,9 +74,6 @@ fn main() {
}, },
} }
hid.scan_input(); gfx.wait_for_vblank();
if hid.keys_down().contains(KeyPad::START) {
break;
};
} }
} }

11
ctru-rs/examples/output-3dslink.rs

@ -1,4 +1,6 @@
//! Use the `3dslink --server` option for redirecting output from the 3DS back //! Output redirection example.
//!
//! This example uses the `3dslink --server` option for redirecting output from the 3DS back
//! to the device that sent the executable. //! to the device that sent the executable.
//! //!
//! For now, `cargo 3ds run` does not support this flag, so to run this example //! For now, `cargo 3ds run` does not support this flag, so to run this example
@ -11,30 +13,27 @@
use ctru::prelude::*; use ctru::prelude::*;
fn main() { fn main() {
ctru::use_panic_handler();
let gfx = Gfx::new().expect("Couldn't obtain GFX controller"); let gfx = Gfx::new().expect("Couldn't obtain GFX controller");
let mut hid = Hid::new().expect("Couldn't obtain HID controller"); let mut hid = Hid::new().expect("Couldn't obtain HID controller");
let apt = Apt::new().expect("Couldn't obtain APT controller"); let apt = Apt::new().expect("Couldn't obtain APT controller");
// We need to use network sockets to send the data stream back.
let mut soc = Soc::new().expect("Couldn't obtain SOC controller"); let mut soc = Soc::new().expect("Couldn't obtain SOC controller");
// Set the output to be redirected to the `3dslink` server.
soc.redirect_to_3dslink(true, true) soc.redirect_to_3dslink(true, true)
.expect("unable to redirect stdout/err to 3dslink server"); .expect("unable to redirect stdout/err to 3dslink server");
println!("Hello 3dslink!"); println!("Hello 3dslink!");
eprintln!("Press Start on the device to disconnect and exit."); eprintln!("Press Start on the device to disconnect and exit.");
// Main loop
while apt.main_loop() { while apt.main_loop() {
//Scan all the inputs. This should be done once for each frame
hid.scan_input(); hid.scan_input();
if hid.keys_down().contains(KeyPad::START) { if hid.keys_down().contains(KeyPad::START) {
break; break;
} }
//Wait for VBlank
gfx.wait_for_vblank(); gfx.wait_for_vblank();
} }
} }

22
ctru-rs/examples/romfs.rs

@ -1,43 +1,45 @@
//! RomFS example.
//!
//! This example showcases the RomFS service and how to mount it to include a read-only filesystem within the application bundle.
use ctru::prelude::*; use ctru::prelude::*;
fn main() { fn main() {
ctru::use_panic_handler();
let gfx = Gfx::new().expect("Couldn't obtain GFX controller"); let gfx = Gfx::new().expect("Couldn't obtain GFX controller");
let mut hid = Hid::new().expect("Couldn't obtain HID controller"); let mut hid = Hid::new().expect("Couldn't obtain HID controller");
let apt = Apt::new().expect("Couldn't obtain APT controller"); let apt = Apt::new().expect("Couldn't obtain APT controller");
let _console = Console::new(gfx.top_screen.borrow_mut()); let _console = Console::new(gfx.top_screen.borrow_mut());
cfg_if::cfg_if! { cfg_if::cfg_if! {
// Run this code if RomFS are wanted and available // Run this code if RomFS are wanted and available.
// This never fails as `ctru-rs` examples inherit all of the `ctru` features, // This never fails as `ctru-rs` examples inherit all of the `ctru-rs` features,
// but it might if a normal user application wasn't setup correctly // but it might if a normal user application wasn't setup correctly.
if #[cfg(all(feature = "romfs", romfs_exists))] { if #[cfg(all(feature = "romfs", romfs_exists))] {
// Mount the romfs volume.
let _romfs = ctru::services::romfs::RomFS::new().unwrap(); let _romfs = ctru::services::romfs::RomFS::new().unwrap();
// Open a simple text file present in the RomFS volume.
// Remember to use the `romfs:/` prefix when working with `RomFS`.
let f = std::fs::read_to_string("romfs:/test-file.txt").unwrap(); let f = std::fs::read_to_string("romfs:/test-file.txt").unwrap();
println!("Contents of test-file.txt: \n{f}\n"); println!("Contents of test-file.txt: \n{f}\n");
let f = std::fs::read_to_string("romfs:/ファイル.txt").unwrap(); let f = std::fs::read_to_string("romfs:/ファイル.txt").unwrap();
// While RomFS supports UTF-16 file paths, `Console` doesn't... // While `RomFS` supports UTF-16 file paths, `Console` does not, so we'll use a placeholder for the text.
println!("Contents of [UTF-16 File]: \n{f}\n"); println!("Contents of [UTF-16 File]: \n{f}\n");
} else { } else {
println!("No RomFS was found, are you sure you included it?") println!("No RomFS was found, are you sure you included it?")
} }
} }
println!("\nPress START to exit"); println!("\x1b[29;16HPress Start to exit");
// Main loop
while apt.main_loop() { while apt.main_loop() {
//Scan all the inputs. This should be done once for each frame
hid.scan_input(); hid.scan_input();
if hid.keys_down().contains(KeyPad::START) { if hid.keys_down().contains(KeyPad::START) {
break; break;
} }
//Wait for VBlank
gfx.wait_for_vblank(); gfx.wait_for_vblank();
} }
} }

27
ctru-rs/examples/software-keyboard.rs

@ -1,29 +1,34 @@
use ctru::applets::swkbd::{Button, Swkbd}; //! Software Keyboard example.
//!
//! This example showcases the use of the Software Keyboard applet to receive text input from the user.
use ctru::applets::swkbd::{Button, SoftwareKeyboard};
use ctru::prelude::*; use ctru::prelude::*;
fn main() { fn main() {
ctru::use_panic_handler();
let apt = Apt::new().unwrap(); let apt = Apt::new().unwrap();
let mut hid = Hid::new().unwrap(); let mut hid = Hid::new().unwrap();
let gfx = Gfx::new().unwrap(); let gfx = Gfx::new().unwrap();
let _console = Console::new(gfx.top_screen.borrow_mut()); let _console = Console::new(gfx.top_screen.borrow_mut());
println!("Press A to enter some text or press Start to quit"); println!("Press A to enter some text or press Start to exit.");
while apt.main_loop() { while apt.main_loop() {
gfx.wait_for_vblank();
hid.scan_input(); hid.scan_input();
if hid.keys_down().contains(KeyPad::START) {
break;
}
// Check if the user request to write some input.
if hid.keys_down().contains(KeyPad::A) { if hid.keys_down().contains(KeyPad::A) {
// Prepares a software keyboard with two buttons: One to cancel input and one // Prepares a software keyboard with two buttons: One to cancel input and one
// to accept it. You can also use `Swkbd::new()` to launch the keyboard in different // to accept it. You can also use `SoftwareKeyboard::new()` to launch the keyboard in different
// configurations. // configurations.
let mut keyboard = Swkbd::default(); let mut keyboard = SoftwareKeyboard::default();
// Raise the software keyboard. You can perform different actions depending on which // Raise the software keyboard. You can perform different actions depending on which
// software button the user pressed // software button the user pressed.
match keyboard.get_string(2048) { match keyboard.get_string(2048) {
Ok((text, Button::Right)) => println!("You entered: {text}"), Ok((text, Button::Right)) => println!("You entered: {text}"),
Ok((_, Button::Left)) => println!("Cancelled"), Ok((_, Button::Left)) => println!("Cancelled"),
@ -32,8 +37,6 @@ fn main() {
} }
} }
if hid.keys_down().contains(KeyPad::START) { gfx.wait_for_vblank();
break;
}
} }
} }

16
ctru-rs/examples/system-configuration.rs

@ -1,29 +1,33 @@
//! System Configuration example.
//!
//! This example showcases the CFGU service to retrieve information about the console that the application is running on,
//! such as the model, region and used language.
use ctru::prelude::*; use ctru::prelude::*;
use ctru::services::cfgu::Cfgu; use ctru::services::cfgu::Cfgu;
fn main() { fn main() {
ctru::use_panic_handler();
let gfx = Gfx::new().expect("Couldn't obtain GFX controller"); let gfx = Gfx::new().expect("Couldn't obtain GFX controller");
let mut hid = Hid::new().expect("Couldn't obtain HID controller"); let mut hid = Hid::new().expect("Couldn't obtain HID controller");
let apt = Apt::new().expect("Couldn't obtain APT controller"); let apt = Apt::new().expect("Couldn't obtain APT controller");
let cfgu = Cfgu::new().expect("Couldn't obtain CFGU controller");
let _console = Console::new(gfx.top_screen.borrow_mut()); let _console = Console::new(gfx.top_screen.borrow_mut());
// Initialize the CFGU service to retrieve all wanted information.
let cfgu = Cfgu::new().expect("Couldn't obtain CFGU controller");
println!("\x1b[0;0HRegion: {:?}", cfgu.region().unwrap()); println!("\x1b[0;0HRegion: {:?}", cfgu.region().unwrap());
println!("\x1b[10;0HLanguage: {:?}", cfgu.language().unwrap()); println!("\x1b[10;0HLanguage: {:?}", cfgu.language().unwrap());
println!("\x1b[20;0HModel: {:?}", cfgu.model().unwrap()); println!("\x1b[20;0HModel: {:?}", cfgu.model().unwrap());
// Main loop println!("\x1b[29;16HPress Start to exit");
while apt.main_loop() { while apt.main_loop() {
//Scan all the inputs. This should be done once for each frame
hid.scan_input(); hid.scan_input();
if hid.keys_down().contains(KeyPad::START) { if hid.keys_down().contains(KeyPad::START) {
break; break;
} }
//Wait for VBlank
gfx.wait_for_vblank(); gfx.wait_for_vblank();
} }
} }

4
ctru-rs/examples/thread-basic.rs

@ -6,8 +6,6 @@ use std::os::horizon::thread::BuilderExt;
use std::time::Duration; use std::time::Duration;
fn main() { fn main() {
ctru::use_panic_handler();
let apt = Apt::new().unwrap(); let apt = Apt::new().unwrap();
let mut hid = Hid::new().unwrap(); let mut hid = Hid::new().unwrap();
let gfx = Gfx::new().unwrap(); let gfx = Gfx::new().unwrap();
@ -33,6 +31,8 @@ fn main() {
println!("Created thread {ix}"); println!("Created thread {ix}");
} }
println!("\x1b[29;16HPress Start to exit");
while apt.main_loop() { while apt.main_loop() {
gfx.wait_for_vblank(); gfx.wait_for_vblank();

4
ctru-rs/examples/thread-info.rs

@ -7,8 +7,6 @@ use ctru::prelude::*;
use std::os::horizon::thread::BuilderExt; use std::os::horizon::thread::BuilderExt;
fn main() { fn main() {
ctru::use_panic_handler();
let gfx = Gfx::new().expect("Couldn't obtain GFX controller"); let gfx = Gfx::new().expect("Couldn't obtain GFX controller");
let mut hid = Hid::new().expect("Couldn't obtain HID controller"); let mut hid = Hid::new().expect("Couldn't obtain HID controller");
let apt = Apt::new().expect("Couldn't obtain APT controller"); let apt = Apt::new().expect("Couldn't obtain APT controller");
@ -36,7 +34,7 @@ fn main() {
.unwrap(); .unwrap();
println!("sys thread exited"); println!("sys thread exited");
println!("\nPress Start to exit"); println!("\x1b[29;16HPress Start to exit");
while apt.main_loop() { while apt.main_loop() {
hid.scan_input(); hid.scan_input();

4
ctru-rs/examples/thread-locals.rs

@ -10,8 +10,6 @@ std::thread_local! {
} }
fn main() { fn main() {
ctru::use_panic_handler();
let gfx = Gfx::new().expect("Couldn't obtain GFX controller"); let gfx = Gfx::new().expect("Couldn't obtain GFX controller");
gfx.top_screen.borrow_mut().set_wide_mode(true); gfx.top_screen.borrow_mut().set_wide_mode(true);
let mut hid = Hid::new().expect("Couldn't obtain HID controller"); let mut hid = Hid::new().expect("Couldn't obtain HID controller");
@ -52,7 +50,7 @@ fn main() {
); );
}); });
println!("Press Start to exit"); println!("\x1b[29;16HPress Start to exit");
while apt.main_loop() { while apt.main_loop() {
hid.scan_input(); hid.scan_input();

14
ctru-rs/examples/time-rtc.rs

@ -1,19 +1,20 @@
//! Time Clock example.
//!
//! This example showcases how to retrieve the local time set in the console's configuration
//! using the implementations of the standard library.
use ctru::prelude::*; use ctru::prelude::*;
fn main() { fn main() {
ctru::use_panic_handler();
let gfx = Gfx::new().expect("Couldn't obtain GFX controller"); let gfx = Gfx::new().expect("Couldn't obtain GFX controller");
let mut hid = Hid::new().expect("Couldn't obtain HID controller"); let mut hid = Hid::new().expect("Couldn't obtain HID controller");
let apt = Apt::new().expect("Couldn't obtain APT controller"); let apt = Apt::new().expect("Couldn't obtain APT controller");
let _console = Console::new(gfx.top_screen.borrow_mut()); let _console = Console::new(gfx.top_screen.borrow_mut());
print!("\x1b[30;16HPress Start to exit."); println!("\x1b[29;16HPress Start to exit");
// Main loop
while apt.main_loop() { while apt.main_loop() {
// Scan all the inputs. This should be done once for each frame
hid.scan_input(); hid.scan_input();
if hid.keys_down().contains(KeyPad::START) { if hid.keys_down().contains(KeyPad::START) {
@ -21,7 +22,7 @@ fn main() {
} }
// Technically, this actually just gets local time and assumes it's UTC, // Technically, this actually just gets local time and assumes it's UTC,
// since the 3DS doesn't seem to support timezones... // since the 3DS doesn't seem to support timezones.
let cur_time = time::OffsetDateTime::now_utc(); let cur_time = time::OffsetDateTime::now_utc();
let hours = cur_time.hour(); let hours = cur_time.hour();
@ -36,7 +37,6 @@ fn main() {
println!("\x1b[1;1H{hours:0>2}:{minutes:0>2}:{seconds:0>2}"); println!("\x1b[1;1H{hours:0>2}:{minutes:0>2}:{seconds:0>2}");
println!("{weekday} {month} {day} {year}"); println!("{weekday} {month} {day} {year}");
//Wait for VBlank
gfx.wait_for_vblank(); gfx.wait_for_vblank();
} }
} }

58
ctru-rs/examples/title-info.rs

@ -1,27 +1,37 @@
//! Title Info example.
//!
//! This example showcases how to retrieve information about the titles installed on the console running the application
//! via the Application Manager (Am) service.
use ctru::prelude::*; use ctru::prelude::*;
use ctru::services::am::Am; use ctru::services::am::Am;
use ctru::services::fs::FsMediaType; use ctru::services::fs::FsMediaType;
fn main() { fn main() {
ctru::use_panic_handler();
let gfx = Gfx::new().expect("Couldn't obtain GFX controller"); let gfx = Gfx::new().expect("Couldn't obtain GFX controller");
let mut hid = Hid::new().expect("Couldn't obtain HID controller"); let mut hid = Hid::new().expect("Couldn't obtain HID controller");
let apt = Apt::new().expect("Couldn't obtain APT controller"); let apt = Apt::new().expect("Couldn't obtain APT controller");
let am = Am::new().expect("Couldn't obtain AM controller");
let top_screen = Console::new(gfx.top_screen.borrow_mut()); let top_screen = Console::new(gfx.top_screen.borrow_mut());
let bottom_screen = Console::new(gfx.bottom_screen.borrow_mut()); let bottom_screen = Console::new(gfx.bottom_screen.borrow_mut());
// Setup the AM service to retrieve the wanted information.
let am = Am::new().expect("Couldn't obtain AM controller");
// Amount of titles installed on the SD card.
let sd_count = am let sd_count = am
.title_count(FsMediaType::Sd) .title_count(FsMediaType::Sd)
.expect("Failed to get sd title count"); .expect("Failed to get sd title count");
// List of titles installed on the SD card.
let sd_list = am let sd_list = am
.title_list(FsMediaType::Sd) .title_list(FsMediaType::Sd)
.expect("Failed to get sd title list"); .expect("Failed to get sd title list");
// Amount of titles installed on the NAND storage.
let nand_count = am let nand_count = am
.title_count(FsMediaType::Nand) .title_count(FsMediaType::Nand)
.expect("Failed to get nand title count"); .expect("Failed to get nand title count");
// List of titles installed on the NAND storage.
let nand_list = am let nand_list = am
.title_list(FsMediaType::Nand) .title_list(FsMediaType::Nand)
.expect("Failed to get nand title list"); .expect("Failed to get nand title list");
@ -30,9 +40,7 @@ fn main() {
let mut refresh = true; let mut refresh = true;
let mut use_nand = false; let mut use_nand = false;
// Main loop
while apt.main_loop() { while apt.main_loop() {
//Scan all the inputs. This should be done once for each frame
hid.scan_input(); hid.scan_input();
if hid.keys_down().contains(KeyPad::START) { if hid.keys_down().contains(KeyPad::START) {
@ -48,23 +56,23 @@ fn main() {
if hid.keys_down().intersects(KeyPad::DOWN) { if hid.keys_down().intersects(KeyPad::DOWN) {
if offset + 1 < cur_list.len() { if offset + 1 < cur_list.len() {
offset = offset + 1; offset += 1;
refresh = true; refresh = true;
} }
} else if hid.keys_down().intersects(KeyPad::UP) { } else if hid.keys_down().intersects(KeyPad::UP) && offset > 0 {
if offset > 0 { offset -= 1;
offset = offset - 1;
refresh = true; refresh = true;
} }
}
// Render the title list via a scrollable text UI.
if refresh { if refresh {
let mut selected_title = cur_list.iter().skip(offset).next().unwrap(); let mut selected_title = cur_list.get(offset).unwrap();
// Clear top screen and write title ids to it
// Clear the top screen and write title IDs to it.
top_screen.select(); top_screen.select();
print!("\x1b[2J"); print!("\x1b[2J");
// Top screen seems to have only 30 rows // Top screen has 30 rows.
for (i, title) in cur_list.iter().skip(offset).take(29).enumerate() { for (i, title) in cur_list.iter().skip(offset).take(29).enumerate() {
if i == 0 { if i == 0 {
selected_title = title; selected_title = title;
@ -74,25 +82,18 @@ fn main() {
} }
} }
// Clear bottom screen and write properties of selected title to it // Clear the bottom screen and write the properties of selected title to it.
bottom_screen.select(); bottom_screen.select();
println!("\x1b[2J"); bottom_screen.clear();
// Move cursor to top left println!("Press Start to exit");
// Move cursor to top left.
println!("\x1b[1;1"); println!("\x1b[1;1");
match selected_title.title_info() { println!("Size: {} kB", selected_title.size() / 1024);
Ok(info) => { println!("Version: 0x{:x}", selected_title.version());
println!("Size: {} KB", info.size_bytes() / 1024); println!("Product code: \"{}\"", selected_title.product_code());
println!("Version: 0x{:x}", info.version());
}
Err(e) => println!("Failed to get title info: {}", e),
}
match selected_title.product_code() {
Ok(code) => println!("Product code: \"{code}\""),
Err(e) => println!("Failed to get product code: {}", e),
}
println!("\x1b[26;0HPress START to exit");
if use_nand { if use_nand {
println!("Press SELECT to choose SD Card"); println!("Press SELECT to choose SD Card");
println!("Current medium: NAND"); println!("Current medium: NAND");
@ -106,7 +107,6 @@ fn main() {
refresh = false; refresh = false;
} }
//Wait for VBlank
gfx.wait_for_vblank(); gfx.wait_for_vblank();
} }
} }

20
ctru-rs/examples/touch-screen.rs

@ -1,15 +1,17 @@
//! Touch Screen example.
//!
//! This example showcases how to retrieve the touch screen's touch information via the HID service.
use ctru::prelude::*; use ctru::prelude::*;
fn main() { fn main() {
ctru::use_panic_handler();
let gfx = Gfx::new().expect("Couldn't obtain GFX controller"); let gfx = Gfx::new().expect("Couldn't obtain GFX controller");
let mut hid = Hid::new().expect("Couldn't obtain HID controller"); let mut hid = Hid::new().expect("Couldn't obtain HID controller");
let apt = Apt::new().expect("Couldn't obtain APT controller"); let apt = Apt::new().expect("Couldn't obtain APT controller");
let console = Console::new(gfx.top_screen.borrow_mut()); let console = Console::new(gfx.top_screen.borrow_mut());
// We'll hold the previous touch position for comparison. // We'll save the previous touch position for comparison.
let mut old_touch: (u16, u16) = (0, 0); let mut old_touch: (u16, u16) = (0, 0);
println!("\x1b[29;16HPress Start to exit"); println!("\x1b[29;16HPress Start to exit");
@ -26,22 +28,22 @@ fn main() {
let touch: (u16, u16) = hid.touch_position(); let touch: (u16, u16) = hid.touch_position();
// We only want to print the position when it's different // We only want to print the position when it's different
// from what it was on the previous frame // from what it was on the previous frame.
if touch != old_touch { if touch != old_touch {
// Move the cursor back to the top of the screen and print the coordinates.
print!("\x1b[1;1HTouch Screen position: {:#?}", touch);
// Special case for when the user lifts the stylus/finger from the screen. // Special case for when the user lifts the stylus/finger from the screen.
// This is done to avoid some screen tearing. // This is done to avoid some screen tearing.
if touch == (0, 0) { if touch == (0, 0) {
console.clear(); console.clear();
// Print again because we just cleared the screen // Print again because we just cleared the screen.
println!("\x1b[29;16HPress Start to exit"); println!("\x1b[29;16HPress Start to exit");
} }
// Move the cursor back to the top of the screen and print the coordinates
print!("\x1b[1;1HTouch Screen position: {:#?}", touch);
} }
// Save our current touch position for the next frame // Save our current touch position for the next frame.
old_touch = touch; old_touch = touch;
gfx.wait_for_vblank(); gfx.wait_for_vblank();

263
ctru-rs/src/applets/mii_selector.rs

@ -1,73 +1,87 @@
//! Mii Selector applet //! Mii Selector applet.
//! //!
//! This module contains the methods to launch the Mii Selector. //! This applet opens a window which lets the player/user choose a Mii from the ones present on their console.
//! The selected Mii is readable as a [`Mii`].
use crate::mii::MiiData; use crate::mii::Mii;
use bitflags::bitflags; use bitflags::bitflags;
use std::ffi::CString; use std::{ffi::CString, fmt};
/// Index of a Mii used to configure some parameters of the Mii Selector /// Index of a Mii on the [`MiiSelector`] interface.
/// Can be either a single index, or _all_ Miis ///
/// See [`MiiSelector::allowlist_user_mii()`] and related functions for more information.
#[derive(Debug, Clone, Copy, Eq, PartialEq)] #[derive(Debug, Clone, Copy, Eq, PartialEq)]
pub enum Index { pub enum Index {
/// Specific Mii index.
///
/// # Notes
///
/// Indexes start at 0.
Index(u32), Index(u32),
/// All Miis.
All, All,
} }
/// The type of a Mii with their respective data /// The type of a Mii.
#[derive(Debug, Clone, Eq, PartialEq)] #[derive(Debug, Clone, Eq, PartialEq)]
pub enum MiiType { pub enum MiiType {
Guest { index: u32, name: String }, /// Guest Mii.
Guest {
/// Guest Mii index.
index: u32,
/// Guest Mii name.
name: String,
},
/// User-made Mii.
User, User,
} }
bitflags! { bitflags! {
/// Options for the Mii Selector /// Options to configure the [`MiiSelector`].
///
/// See [`MiiSelector::set_options()`] to learn how to use them.
#[derive(PartialEq, Eq, PartialOrd, Ord, Hash, Debug, Clone, Copy)]
pub struct Options: u32 { pub struct Options: u32 {
/// Show the cancel button /// Show the cancel button.
const MII_SELECTOR_CANCEL = ctru_sys::MIISELECTOR_CANCEL; const ENABLE_CANCEL = ctru_sys::MIISELECTOR_CANCEL;
/// Make guest Miis selectable /// Make guest Miis available to select.
const MII_SELECTOR_GUESTS = ctru_sys::MIISELECTOR_GUESTS; const ENABLE_GUESTS = ctru_sys::MIISELECTOR_GUESTS;
/// Show on the top screen /// Show the [`MiiSelector`] window on the top screen.
const MII_SELECTOR_TOP = ctru_sys::MIISELECTOR_TOP; const USE_TOP_SCREEN = ctru_sys::MIISELECTOR_TOP;
/// Start on the guest's page /// Start the [`MiiSelector`] on the guests' page. Requires [`Options::ENABLE_GUESTS`].
const MII_SELECTOR_GUEST_START = ctru_sys::MIISELECTOR_GUESTSTART; const START_WITH_GUESTS = ctru_sys::MIISELECTOR_GUESTSTART;
} }
} }
/// An instance of the Mii Selector /// Configuration structure to setup the Mii Selector applet.
/// #[doc(alias = "MiiSelectorConf")]
/// # Example
/// ```
/// use ctru::applets::mii_selector::MiiSelector;
///
/// let mut mii_selector = MiiSelector::new();
/// mii_selector.set_title("Example Mii selector");
///
/// let result = mii_selector.launch().unwrap();
/// ```
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct MiiSelector { pub struct MiiSelector {
config: Box<ctru_sys::MiiSelectorConf>, config: Box<ctru_sys::MiiSelectorConf>,
} }
/// Return value from a MiiSelector's launch /// Return value of a successful [`MiiSelector::launch()`].
#[non_exhaustive] #[non_exhaustive]
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct SelectionResult { pub struct Selection {
pub mii_data: MiiData, /// Data of the selected Mii.
pub is_mii_selected: bool, pub mii_data: Mii,
/// Type of the selected Mii.
pub mii_type: MiiType, pub mii_type: MiiType,
} }
/// Error type for the Mii selector /// Error returned by an unsuccessful [`MiiSelector::launch()`].
#[derive(Copy, Clone, Debug, Eq, PartialEq)] #[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub enum LaunchError { pub enum Error {
/// The selected Mii's data is corrupt.
InvalidChecksum, InvalidChecksum,
/// Either the user cancelled the selection (see [`Options::ENABLE_CANCEL`]) or no valid Miis were available to select.
NoMiiSelected,
} }
impl MiiSelector { impl MiiSelector {
/// Initializes a Mii Selector /// Initialize a new configuration for the Mii Selector applet.
#[doc(alias = "miiSelectorInit")]
pub fn new() -> Self { pub fn new() -> Self {
let mut config = Box::<ctru_sys::MiiSelectorConf>::default(); let mut config = Box::<ctru_sys::MiiSelectorConf>::default();
unsafe { unsafe {
@ -76,9 +90,22 @@ impl MiiSelector {
Self { config } Self { config }
} }
/// Set the title of the Mii Selector. /// Set the title of the Mii Selector window.
/// ///
/// This function would panic if the given ``&str`` contains NUL bytes. /// # Panics
/// This function will panic if the given `&str` contains NUL bytes.
///
/// # Example
///
/// ```no_run
/// # fn main() {
/// use ctru::applets::mii_selector::MiiSelector;
///
/// let mut mii_selector = MiiSelector::new();
/// mii_selector.set_title("Select a Mii!");
/// # }
/// ```
#[doc(alias = "miiSelectorSetTitle")]
pub fn set_title(&mut self, text: &str) { pub fn set_title(&mut self, text: &str) {
// This can only fail if the text contains NUL bytes in the string... which seems // This can only fail if the text contains NUL bytes in the string... which seems
// unlikely and is documented // unlikely and is documented
@ -88,13 +115,48 @@ impl MiiSelector {
} }
} }
/// Set the options of the Mii Selector /// Set the options of the Mii Selector.
///
/// This will overwrite any previously saved options. Use bitwise operations to set all your wanted options at once.
///
/// # Example
///
/// ```no_run
/// # fn main() {
/// use ctru::applets::mii_selector::{MiiSelector, Options};
/// let mut mii_selector = MiiSelector::new();
///
/// // Setup a `MiiSelector` that can be cancelled and that makes Guest Miis available to select.
/// let opts = Options::ENABLE_CANCEL & Options::ENABLE_GUESTS;
/// mii_selector.set_options(opts);
/// # }
/// ```
#[doc(alias = "miiSelectorSetOptions")]
pub fn set_options(&mut self, options: Options) { pub fn set_options(&mut self, options: Options) {
unsafe { ctru_sys::miiSelectorSetOptions(self.config.as_mut(), options.bits) } unsafe { ctru_sys::miiSelectorSetOptions(self.config.as_mut(), options.bits()) }
} }
/// Whitelist a guest Mii /// Allowlist a guest Mii based on its index.
pub fn whitelist_guest_mii(&mut self, mii_index: Index) { ///
/// # Notes
///
/// Guest Mii's won't be available regardless of their allowlist/blocklist state if the [`MiiSelector`] is run without setting [`Options::ENABLE_GUESTS`].
/// Look into [`MiiSelector::set_options()`] to see how to work with options.
///
/// # Example
///
/// ```no_run
/// # fn main() {
/// #
/// use ctru::applets::mii_selector::{Index, MiiSelector};
/// let mut mii_selector = MiiSelector::new();
///
/// // Allowlist the guest Mii at index 2.
/// mii_selector.allowlist_guest_mii(Index::Index(2));
/// # }
/// ```
#[doc(alias = "miiSelectorWhitelistGuestMii")]
pub fn allowlist_guest_mii(&mut self, mii_index: Index) {
let index = match mii_index { let index = match mii_index {
Index::Index(i) => i, Index::Index(i) => i,
Index::All => ctru_sys::MIISELECTOR_GUESTMII_SLOTS, Index::All => ctru_sys::MIISELECTOR_GUESTMII_SLOTS,
@ -103,8 +165,27 @@ impl MiiSelector {
unsafe { ctru_sys::miiSelectorWhitelistGuestMii(self.config.as_mut(), index) } unsafe { ctru_sys::miiSelectorWhitelistGuestMii(self.config.as_mut(), index) }
} }
/// Blacklist a guest Mii /// Blocklist a guest Mii based on its index.
pub fn blacklist_guest_mii(&mut self, mii_index: Index) { ///
/// # Notes
///
/// Guest Mii's won't be available regardless of their allowlist/blocklist state if the [`MiiSelector`] is run without setting [`Options::ENABLE_GUESTS`].
/// Look into [`MiiSelector::set_options()`] to see how to work with options.
///
/// # Example
///
/// ```no_run
/// # fn main() {
/// #
/// use ctru::applets::mii_selector::{Index, MiiSelector};
/// let mut mii_selector = MiiSelector::new();
///
/// // Blocklist the guest Mii at index 1 so that it cannot be selected.
/// mii_selector.blocklist_guest_mii(Index::Index(1));
/// # }
/// ```
#[doc(alias = "miiSelectorBlacklistGuestMii")]
pub fn blocklist_guest_mii(&mut self, mii_index: Index) {
let index = match mii_index { let index = match mii_index {
Index::Index(i) => i, Index::Index(i) => i,
Index::All => ctru_sys::MIISELECTOR_GUESTMII_SLOTS, Index::All => ctru_sys::MIISELECTOR_GUESTMII_SLOTS,
@ -113,8 +194,22 @@ impl MiiSelector {
unsafe { ctru_sys::miiSelectorBlacklistGuestMii(self.config.as_mut(), index) } unsafe { ctru_sys::miiSelectorBlacklistGuestMii(self.config.as_mut(), index) }
} }
/// Whitelist a user Mii /// Allowlist a user-created Mii based on its index.
pub fn whitelist_user_mii(&mut self, mii_index: Index) { ///
/// # Example
///
/// ```no_run
/// # fn main() {
/// #
/// use ctru::applets::mii_selector::{Index, MiiSelector};
/// let mut mii_selector = MiiSelector::new();
///
/// // Allowlist the user-created Mii at index 0.
/// mii_selector.allowlist_user_mii(Index::Index(0));
/// # }
/// ```
#[doc(alias = "miiSelectorWhitelistUserMii")]
pub fn allowlist_user_mii(&mut self, mii_index: Index) {
let index = match mii_index { let index = match mii_index {
Index::Index(i) => i, Index::Index(i) => i,
Index::All => ctru_sys::MIISELECTOR_USERMII_SLOTS, Index::All => ctru_sys::MIISELECTOR_USERMII_SLOTS,
@ -123,8 +218,22 @@ impl MiiSelector {
unsafe { ctru_sys::miiSelectorWhitelistUserMii(self.config.as_mut(), index) } unsafe { ctru_sys::miiSelectorWhitelistUserMii(self.config.as_mut(), index) }
} }
/// Blacklist a user Mii /// Blocklist a user-created Mii based on its index.
pub fn blacklist_user_mii(&mut self, mii_index: Index) { ///
/// # Example
///
/// ```no_run
/// # fn main() {
/// #
/// use ctru::applets::mii_selector::{Index, MiiSelector};
/// let mut mii_selector = MiiSelector::new();
///
/// // Blocklist all user-created Miis so that they cannot be selected.
/// mii_selector.blocklist_user_mii(Index::All);
/// # }
/// ```
#[doc(alias = "miiSelectorBlacklistUserMii")]
pub fn blocklist_user_mii(&mut self, mii_index: Index) {
let index = match mii_index { let index = match mii_index {
Index::Index(i) => i, Index::Index(i) => i,
Index::All => ctru_sys::MIISELECTOR_USERMII_SLOTS, Index::All => ctru_sys::MIISELECTOR_USERMII_SLOTS,
@ -133,24 +242,52 @@ impl MiiSelector {
unsafe { ctru_sys::miiSelectorBlacklistUserMii(self.config.as_mut(), index) } unsafe { ctru_sys::miiSelectorBlacklistUserMii(self.config.as_mut(), index) }
} }
/// Set where the cursor will be. /// Set where the GUI cursor will start at.
/// If there's no Mii at that index, the cursor will start at the Mii with the index 0 ///
/// If there's no Mii at that index, the cursor will start at the Mii with the index 0.
#[doc(alias = "miiSelectorSetInitialIndex")]
pub fn set_initial_index(&mut self, index: usize) { pub fn set_initial_index(&mut self, index: usize) {
// This function is static inline in libctru unsafe { ctru_sys::miiSelectorSetInitialIndex(self.config.as_mut(), index as u32) };
// https://github.com/devkitPro/libctru/blob/af5321c78ee5c72a55b526fd2ed0d95ca1c05af9/libctru/include/3ds/applets/miiselector.h#L155
self.config.initial_index = index as u32;
} }
/// Launch the Mii Selector. /// Launch the Mii Selector.
/// Returns an error when the checksum of the Mii is invalid. ///
pub fn launch(&mut self) -> Result<SelectionResult, LaunchError> { /// Depending on the configuration, the Mii Selector window will appear either on the bottom screen (default behaviour) or the top screen (see [`Options::USE_TOP_SCREEN`]).
///
/// TODO: UNSAFE OPERATION, LAUNCHING APPLETS REQUIRES GRAPHICS, WITHOUT AN ACTIVE GFX THIS WILL CAUSE A SEGMENTATION FAULT.
///
/// # Example
///
/// ```no_run
/// # use std::error::Error;
/// # fn main() -> Result<(), Box<dyn Error>> {
/// #
/// use ctru::applets::mii_selector::{MiiSelector, Options};
///
/// let mut mii_selector = MiiSelector::new();
/// mii_selector.set_title("Select a Mii!");
///
/// let opts = Options::ENABLE_CANCEL & Options::ENABLE_GUESTS;
/// mii_selector.set_options(opts);
///
/// let result = mii_selector.launch()?;
/// #
/// # Ok(())
/// # }
/// ```
#[doc(alias = "miiSelectorLaunch")]
pub fn launch(&mut self) -> Result<Selection, Error> {
let mut return_val = Box::<ctru_sys::MiiSelectorReturn>::default(); let mut return_val = Box::<ctru_sys::MiiSelectorReturn>::default();
unsafe { ctru_sys::miiSelectorLaunch(self.config.as_mut(), return_val.as_mut()) } unsafe { ctru_sys::miiSelectorLaunch(self.config.as_mut(), return_val.as_mut()) }
if return_val.no_mii_selected != 0 {
return Err(Error::NoMiiSelected);
}
if unsafe { ctru_sys::miiSelectorChecksumIsValid(return_val.as_mut()) } { if unsafe { ctru_sys::miiSelectorChecksumIsValid(return_val.as_mut()) } {
Ok((*return_val).into()) Ok((*return_val).into())
} else { } else {
Err(LaunchError::InvalidChecksum) Err(Error::InvalidChecksum)
} }
} }
} }
@ -161,14 +298,24 @@ impl Default for MiiSelector {
} }
} }
impl From<ctru_sys::MiiSelectorReturn> for SelectionResult { impl fmt::Display for Error {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::InvalidChecksum => write!(f, "selected mii has invalid checksum"),
Self::NoMiiSelected => write!(f, "no mii was selected"),
}
}
}
impl std::error::Error for Error {}
impl From<ctru_sys::MiiSelectorReturn> for Selection {
fn from(ret: ctru_sys::MiiSelectorReturn) -> Self { fn from(ret: ctru_sys::MiiSelectorReturn) -> Self {
let raw_mii_data = ret.mii; let raw_mii_data = ret.mii;
let mut guest_mii_name = ret.guest_mii_name; let mut guest_mii_name = ret.guest_mii_name;
SelectionResult { Selection {
mii_data: raw_mii_data.into(), mii_data: raw_mii_data.into(),
is_mii_selected: ret.no_mii_selected == 0,
mii_type: if ret.guest_mii_index != 0xFFFFFFFF { mii_type: if ret.guest_mii_index != 0xFFFFFFFF {
MiiType::Guest { MiiType::Guest {
index: ret.guest_mii_index, index: ret.guest_mii_index,

10
ctru-rs/src/applets/mod.rs

@ -1,2 +1,12 @@
//! System Applets.
//!
//! Applets are small integrated programs that the OS makes available to the developer to streamline commonly needed functionality.
//! Thanks to these integrations the developer can avoid wasting time re-implementing common features and instead use a more reliable base for their application.
//!
//! Unlike [services](crate::services), applets aren't accessed via a system subprocess (which would require obtaining a special handle at runtime).
//! Instead, the application builds a configuration storing the various parameters which is then used to "launch" the applet.
//!
//! Applets block execution of the thread that launches them as long as the user doesn't close the applet.
pub mod mii_selector; pub mod mii_selector;
pub mod swkbd; pub mod swkbd;

375
ctru-rs/src/applets/swkbd.rs

@ -1,106 +1,212 @@
//! Software Keyboard applet.
//!
//! This applet opens a virtual keyboard on the console's bottom screen which lets the user write UTF-16 valid text.
// TODO: Implement remaining functionality (password mode, filter callbacks, etc.). Also improve "max text length" API. Improve `number of buttons` API when creating a new SoftwareKeyboard.
// TODO: Split the Parental PIN lock operations into a different type.
#![doc(alias = "keyboard")]
use bitflags::bitflags; use bitflags::bitflags;
use ctru_sys::{ use ctru_sys::{
self, swkbdInit, swkbdInputText, swkbdSetButton, swkbdSetFeatures, swkbdSetHintText, SwkbdState, self, swkbdInit, swkbdInputText, swkbdSetButton, swkbdSetFeatures, swkbdSetHintText,
swkbdSetInitialText, SwkbdState,
}; };
use libc; use libc;
use std::fmt::Display;
use std::iter::once; use std::iter::once;
use std::str; use std::str;
/// An instance of the software keyboard. /// Configuration structure to setup the Software Keyboard applet.
#[doc(alias = "SwkbdState")]
#[derive(Clone)] #[derive(Clone)]
pub struct Swkbd { pub struct SoftwareKeyboard {
state: Box<SwkbdState>, state: Box<SwkbdState>,
} }
/// The kind of keyboard to be initialized. /// The type of keyboard used by the [`SoftwareKeyboard`].
/// ///
/// Normal is the full keyboard with several pages (QWERTY/accents/symbol/mobile) /// Can be set with [`SoftwareKeyboard::new()`]
/// Qwerty is a QWERTY-only keyboard. #[doc(alias = "SwkbdType")]
/// Numpad is a number pad.
/// Western is a text keyboard without japanese symbols (only applies to JPN systems). For other
/// systems it's the same as a Normal keyboard.
#[derive(Copy, Clone, Debug, PartialEq, Eq)] #[derive(Copy, Clone, Debug, PartialEq, Eq)]
#[repr(u32)] #[repr(u32)]
pub enum Kind { pub enum Kind {
/// Normal keyboard composed of several pages (QWERTY, accents, symbols, mobile).
Normal = ctru_sys::SWKBD_TYPE_NORMAL, Normal = ctru_sys::SWKBD_TYPE_NORMAL,
/// Only QWERTY keyboard.
Qwerty = ctru_sys::SWKBD_TYPE_QWERTY, Qwerty = ctru_sys::SWKBD_TYPE_QWERTY,
/// Only number pad.
Numpad = ctru_sys::SWKBD_TYPE_NUMPAD, Numpad = ctru_sys::SWKBD_TYPE_NUMPAD,
/// On JPN systems: a keyboard without japanese input capabilities.
///
/// On any other region: same as [`Normal`](Kind::Normal).
Western = ctru_sys::SWKBD_TYPE_WESTERN, Western = ctru_sys::SWKBD_TYPE_WESTERN,
} }
/// Represents which button the user pressed to close the software keyboard. /// Represents which button the user pressed to close the [`SoftwareKeyboard`].
///
/// Button text and behaviour can be customized with [`SoftwareKeyboard::configure_button()`].
#[doc(alias = "SwkbdButton")]
#[derive(Copy, Clone, Debug, PartialEq, Eq)] #[derive(Copy, Clone, Debug, PartialEq, Eq)]
#[repr(u32)] #[repr(u32)]
pub enum Button { pub enum Button {
/// Left button. Usually corresponds to "Cancel".
Left = ctru_sys::SWKBD_BUTTON_LEFT, Left = ctru_sys::SWKBD_BUTTON_LEFT,
/// Middle button. Usually corresponds to "I Forgot".
Middle = ctru_sys::SWKBD_BUTTON_MIDDLE, Middle = ctru_sys::SWKBD_BUTTON_MIDDLE,
/// Right button. Usually corresponds to "OK".
Right = ctru_sys::SWKBD_BUTTON_RIGHT, Right = ctru_sys::SWKBD_BUTTON_RIGHT,
} }
/// Error type for the software keyboard. /// Error returned by an unsuccessful [`SoftwareKeyboard::get_string()`].
#[doc(alias = "SwkbdResult")]
#[derive(Copy, Clone, Debug, PartialEq, Eq)] #[derive(Copy, Clone, Debug, PartialEq, Eq)]
#[repr(i32)] #[repr(i32)]
pub enum Error { pub enum Error {
InvalidInput = ctru_sys::SWKBD_INVALID_INPUT, /// Invalid parameters given to the [`SoftwareKeyboard`] configuration.
InvalidParameters = ctru_sys::SWKBD_INVALID_INPUT,
/// [`SoftwareKeyboard`] ran out of memory.
OutOfMem = ctru_sys::SWKBD_OUTOFMEM, OutOfMem = ctru_sys::SWKBD_OUTOFMEM,
/// Home button was pressed while [`SoftwareKeyboard`] was running.
HomePressed = ctru_sys::SWKBD_HOMEPRESSED, HomePressed = ctru_sys::SWKBD_HOMEPRESSED,
/// Reset button was pressed while [`SoftwareKeyboard`] was running.
ResetPressed = ctru_sys::SWKBD_RESETPRESSED, ResetPressed = ctru_sys::SWKBD_RESETPRESSED,
/// Power button was pressed while [`SoftwareKeyboard`] was running.
PowerPressed = ctru_sys::SWKBD_POWERPRESSED, PowerPressed = ctru_sys::SWKBD_POWERPRESSED,
/// The parental lock PIN was correct.
///
/// While this variant isn't *technically* considerable an error
/// the result of a Parental PIN operation won't return a string to the program, thus it's still exceptional behaviour.
ParentalOk = ctru_sys::SWKBD_PARENTAL_OK, ParentalOk = ctru_sys::SWKBD_PARENTAL_OK,
/// The parental lock PIN was incorrect.
ParentalFail = ctru_sys::SWKBD_PARENTAL_FAIL, ParentalFail = ctru_sys::SWKBD_PARENTAL_FAIL,
/// Input triggered the filter.
///
/// You can have a look at [`Filters`] to activate custom filters.
BannedInput = ctru_sys::SWKBD_BANNED_INPUT, BannedInput = ctru_sys::SWKBD_BANNED_INPUT,
} }
/// Restrictions on keyboard input /// Restrictions to enforce rules on the keyboard input.
///
/// See [`SoftwareKeyboard::set_validation()`]
#[doc(alias = "SwkbdValidInput")]
#[derive(Copy, Clone, Debug, PartialEq, Eq)] #[derive(Copy, Clone, Debug, PartialEq, Eq)]
#[repr(u32)] #[repr(u32)]
pub enum ValidInput { pub enum ValidInput {
/// All inputs are accepted.
Anything = ctru_sys::SWKBD_ANYTHING, Anything = ctru_sys::SWKBD_ANYTHING,
/// Empty inputs are not accepted.
NotEmpty = ctru_sys::SWKBD_NOTEMPTY, NotEmpty = ctru_sys::SWKBD_NOTEMPTY,
NotEmptyNotBlank = ctru_sys::SWKBD_NOTEMPTY_NOTBLANK, /// Blank inputs (consisting only of whitespaces) are not accepted.
NotBlank = ctru_sys::SWKBD_NOTBLANK, NotBlank = ctru_sys::SWKBD_NOTBLANK,
/// Neither empty inputs nor blank inputs are accepted.
NotEmptyNotBlank = ctru_sys::SWKBD_NOTEMPTY_NOTBLANK,
/// Input must have a fixed length. Maximum length can be specified with [`SoftwareKeyboard::set_max_text_len()`];
FixedLen = ctru_sys::SWKBD_FIXEDLEN, FixedLen = ctru_sys::SWKBD_FIXEDLEN,
} }
bitflags! { bitflags! {
/// Keyboard feature flags /// Special features that can be activated via [`SoftwareKeyboard::set_features()`].
#[derive(PartialEq, Eq, PartialOrd, Ord, Hash, Debug, Clone, Copy)]
pub struct Features: u32 { pub struct Features: u32 {
/// Parental PIN mode.
///
/// # Notes
///
/// Refer to [`Error::ParentalOk`] and [`Error::ParentalFail`] to check whether the Parental PIN lock was successfully opened.
const PARENTAL_PIN = ctru_sys::SWKBD_PARENTAL; const PARENTAL_PIN = ctru_sys::SWKBD_PARENTAL;
/// Darken top screen while the [`SoftwareKeyboard`] is active.
const DARKEN_TOP_SCREEN = ctru_sys::SWKBD_DARKEN_TOP_SCREEN; const DARKEN_TOP_SCREEN = ctru_sys::SWKBD_DARKEN_TOP_SCREEN;
/// Enable predictive input (necessary for Kanji on JPN consoles).
const PREDICTIVE_INPUT = ctru_sys::SWKBD_PREDICTIVE_INPUT; const PREDICTIVE_INPUT = ctru_sys::SWKBD_PREDICTIVE_INPUT;
/// Enable multiline input.
const MULTILINE = ctru_sys::SWKBD_MULTILINE; const MULTILINE = ctru_sys::SWKBD_MULTILINE;
/// Enable fixed-width mode.
const FIXED_WIDTH = ctru_sys::SWKBD_FIXED_WIDTH; const FIXED_WIDTH = ctru_sys::SWKBD_FIXED_WIDTH;
/// Allow the usage of the Home Button while the [`SoftwareKeyboard`] is running.
const ALLOW_HOME = ctru_sys::SWKBD_ALLOW_HOME; const ALLOW_HOME = ctru_sys::SWKBD_ALLOW_HOME;
/// Allow the usage of the Reset Button while the [`SoftwareKeyboard`] is running.
const ALLOW_RESET = ctru_sys::SWKBD_ALLOW_RESET; const ALLOW_RESET = ctru_sys::SWKBD_ALLOW_RESET;
/// Allow the usage of the Power Button while the [`SoftwareKeyboard`] is running.
const ALLOW_POWER = ctru_sys::SWKBD_ALLOW_POWER; const ALLOW_POWER = ctru_sys::SWKBD_ALLOW_POWER;
/// Default to the QWERTY page when the [`SoftwareKeyboard`] is shown.
const DEFAULT_QWERTY = ctru_sys::SWKBD_DEFAULT_QWERTY; const DEFAULT_QWERTY = ctru_sys::SWKBD_DEFAULT_QWERTY;
} }
/// Keyboard input filtering flags /// Availble filters to disallow some types of input for the [`SoftwareKeyboard`].
///
/// See [`SoftwareKeyboard::set_validation()`]
#[derive(PartialEq, Eq, PartialOrd, Ord, Hash, Debug, Clone, Copy)]
pub struct Filters: u32 { pub struct Filters: u32 {
/// Disallow the usage of numerical digits.
///
/// The maximum number of digits that are allowed to be used while this filter is active
/// can be configured with [`SoftwareKeyboard::set_max_digits()`] (default is 0).
const DIGITS = ctru_sys::SWKBD_FILTER_DIGITS; const DIGITS = ctru_sys::SWKBD_FILTER_DIGITS;
/// Disallow the usage of the "at" (@) sign.
const AT = ctru_sys::SWKBD_FILTER_AT; const AT = ctru_sys::SWKBD_FILTER_AT;
/// Disallow the usage of the "percent" (%) sign.
const PERCENT = ctru_sys::SWKBD_FILTER_PERCENT; const PERCENT = ctru_sys::SWKBD_FILTER_PERCENT;
/// Disallow the usage of the "backslash" (\) sign.
const BACKSLASH = ctru_sys::SWKBD_FILTER_BACKSLASH; const BACKSLASH = ctru_sys::SWKBD_FILTER_BACKSLASH;
/// Disallow the use of profanity via Nintendo's profanity filter.
const PROFANITY = ctru_sys::SWKBD_FILTER_PROFANITY; const PROFANITY = ctru_sys::SWKBD_FILTER_PROFANITY;
/// Use a custom callback in order to filter the input.
///
/// TODO: It's currently impossible to setup a custom filter callback.
const CALLBACK = ctru_sys::SWKBD_FILTER_CALLBACK; const CALLBACK = ctru_sys::SWKBD_FILTER_CALLBACK;
} }
} }
impl Swkbd { impl SoftwareKeyboard {
/// Initializes a software keyboard of the specified type and the chosen number of buttons /// Initialize a new configuration for the Software Keyboard applet depending on how many "exit" buttons are available to the user (1, 2 or 3).
/// (from 1-3). ///
/// # Example
///
/// ```
/// # let _runner = test_runner::GdbRunner::default();
/// # fn main() {
/// #
/// use ctru::applets::swkbd::{SoftwareKeyboard, Kind};
///
/// // Standard keyboard.
/// let keyboard = SoftwareKeyboard::new(Kind::Normal, 2);
///
/// // Numpad (with only the "confirm" button).
/// let keyboard = SoftwareKeyboard::new(Kind::Numpad, 1);
/// #
/// # }
#[doc(alias = "swkbdInit")]
pub fn new(keyboard_type: Kind, num_buttons: i32) -> Self { pub fn new(keyboard_type: Kind, num_buttons: i32) -> Self {
unsafe { unsafe {
let mut state = Box::<SwkbdState>::default(); let mut state = Box::<SwkbdState>::default();
swkbdInit(state.as_mut(), keyboard_type.into(), num_buttons, -1); swkbdInit(state.as_mut(), keyboard_type.into(), num_buttons, -1);
Swkbd { state } SoftwareKeyboard { state }
} }
} }
/// Gets input from this keyboard and appends it to the provided string. /// Launches the applet based on the given configuration and returns a string containing the text input.
///
/// # Notes
/// ///
/// The text received from the keyboard will be truncated if it is longer than `max_bytes`. /// The text received from the keyboard will be truncated if it is longer than `max_bytes`.
///
/// TODO: UNSAFE OPERATION, LAUNCHING APPLETS REQUIRES GRAPHICS, WITHOUT AN ACTIVE GFX THIS WILL CAUSE A SEGMENTATION FAULT.
///
/// # Example
///
/// ```
/// # let _runner = test_runner::GdbRunner::default();
/// # use std::error::Error;
/// # fn main() -> Result<(), Box<dyn Error>> {
/// #
/// use ctru::applets::swkbd::SoftwareKeyboard;
/// let mut keyboard = SoftwareKeyboard::default();
///
/// let (text, button) = keyboard.get_string(2048)?;
/// #
/// # Ok(())
/// # }
/// ```
#[doc(alias = "swkbdInputText")]
pub fn get_string(&mut self, max_bytes: usize) -> Result<(String, Button), Error> { pub fn get_string(&mut self, max_bytes: usize) -> Result<(String, Button), Error> {
// Unfortunately the libctru API doesn't really provide a way to get the exact length // Unfortunately the libctru API doesn't really provide a way to get the exact length
// of the string that it receieves from the software keyboard. Instead it expects you // of the string that it receieves from the software keyboard. Instead it expects you
@ -123,8 +229,31 @@ impl Swkbd {
/// Fills the provided buffer with a UTF-8 encoded, NUL-terminated sequence of bytes from /// Fills the provided buffer with a UTF-8 encoded, NUL-terminated sequence of bytes from
/// this software keyboard. /// this software keyboard.
/// ///
/// # Notes
///
/// If the buffer is too small to contain the entire sequence received from the keyboard, /// If the buffer is too small to contain the entire sequence received from the keyboard,
/// the output will be truncated but should still be well-formed UTF-8. /// the output will be truncated.
///
/// TODO: UNSAFE OPERATION, LAUNCHING APPLETS REQUIRES GRAPHICS, WITHOUT AN ACTIVE GFX THIS WILL CAUSE A SEGMENTATION FAULT.
///
/// # Example
///
/// ```
/// # let _runner = test_runner::GdbRunner::default();
/// # use std::error::Error;
/// # fn main() -> Result<(), Box<dyn Error>> {
/// #
/// use ctru::applets::swkbd::SoftwareKeyboard;
/// let mut keyboard = SoftwareKeyboard::default();
///
/// let mut buffer = vec![0; 100];
///
/// let button = keyboard.write_exact(&mut buffer)?;
/// #
/// # Ok(())
/// # }
/// ```
#[doc(alias = "swkbdInputText")]
pub fn write_exact(&mut self, buf: &mut [u8]) -> Result<Button, Error> { pub fn write_exact(&mut self, buf: &mut [u8]) -> Result<Button, Error> {
unsafe { unsafe {
match swkbdInputText(self.state.as_mut(), buf.as_mut_ptr(), buf.len()) { match swkbdInputText(self.state.as_mut(), buf.as_mut_ptr(), buf.len()) {
@ -137,25 +266,118 @@ impl Swkbd {
} }
} }
/// Sets special features for this keyboard /// Set special features for this keyboard.
///
/// # Example
///
/// ```
/// # let _runner = test_runner::GdbRunner::default();
/// # fn main() {
/// #
/// use ctru::applets::swkbd::{SoftwareKeyboard, Features};
///
/// let mut keyboard = SoftwareKeyboard::default();
///
/// let features = Features::DARKEN_TOP_SCREEN & Features::MULTILINE;
/// keyboard.set_features(features);
/// #
/// # }
#[doc(alias = "swkbdSetFeatures")]
pub fn set_features(&mut self, features: Features) { pub fn set_features(&mut self, features: Features) {
unsafe { swkbdSetFeatures(self.state.as_mut(), features.bits) } unsafe { swkbdSetFeatures(self.state.as_mut(), features.bits()) }
} }
/// Configures input validation for this keyboard /// Configure input validation for this keyboard.
///
/// # Example
///
/// ```
/// # let _runner = test_runner::GdbRunner::default();
/// # fn main() {
/// #
/// use ctru::applets::swkbd::{SoftwareKeyboard, ValidInput, Filters};
/// let mut keyboard = SoftwareKeyboard::default();
///
/// // Disallow empty or blank input.
/// let validation = ValidInput::NotEmptyNotBlank;
///
/// // Disallow the use of numerical digits and profanity.
/// let filters = Filters::DIGITS & Filters::PROFANITY;
/// keyboard.set_validation(validation, filters);
/// #
/// # }
pub fn set_validation(&mut self, validation: ValidInput, filters: Filters) { pub fn set_validation(&mut self, validation: ValidInput, filters: Filters) {
self.state.valid_input = validation.into(); self.state.valid_input = validation.into();
self.state.filter_flags = filters.bits; self.state.filter_flags = filters.bits();
} }
/// Configures the maximum number of digits that can be entered in the keyboard when the /// Configure the maximum number of digits that can be entered in the keyboard when the [`Filters::DIGITS`] flag is enabled.
/// `Filters::DIGITS` flag is enabled ///
/// # Example
///
/// ```
/// # let _runner = test_runner::GdbRunner::default();
/// # fn main() {
/// #
/// use ctru::applets::swkbd::{SoftwareKeyboard, ValidInput, Filters};
/// let mut keyboard = SoftwareKeyboard::default();
///
/// // Disallow empty or blank input.
/// let validation = ValidInput::NotEmptyNotBlank;
///
/// // Disallow the use of numerical digits. This filter is customizable thanks to `set_max_digits`.
/// let filters = Filters::DIGITS;
/// keyboard.set_validation(validation, filters);
///
/// // No more than 3 numbers are accepted.
/// keyboard.set_max_digits(3);
/// #
/// # }
pub fn set_max_digits(&mut self, digits: u16) { pub fn set_max_digits(&mut self, digits: u16) {
self.state.max_digits = digits; self.state.max_digits = digits;
} }
/// Sets the hint text for this software keyboard (that is, the help text that is displayed /// Set the initial text for this software keyboard.
/// when the textbox is empty) ///
/// The initial text is the text already written when you open the software keyboard.
///
/// # Example
///
/// ```
/// # let _runner = test_runner::GdbRunner::default();
/// # fn main() {
/// #
/// use ctru::applets::swkbd::SoftwareKeyboard;
/// let mut keyboard = SoftwareKeyboard::default();
///
/// keyboard.set_initial_text("Write here what you like!");
/// #
/// # }
#[doc(alias = "swkbdSetInitialText")]
pub fn set_initial_text(&mut self, text: &str) {
unsafe {
let nul_terminated: String = text.chars().chain(once('\0')).collect();
swkbdSetInitialText(self.state.as_mut(), nul_terminated.as_ptr());
}
}
/// Set the hint text for this software keyboard.
///
/// The hint text is the text shown in gray before any text gets written in the input box.
///
/// # Example
///
/// ```
/// # let _runner = test_runner::GdbRunner::default();
/// # fn main() {
/// #
/// use ctru::applets::swkbd::SoftwareKeyboard;
/// let mut keyboard = SoftwareKeyboard::default();
///
/// keyboard.set_hint_text("Write here what you like!");
/// #
/// # }
#[doc(alias = "swkbdSetHintText")]
pub fn set_hint_text(&mut self, text: &str) { pub fn set_hint_text(&mut self, text: &str) {
unsafe { unsafe {
let nul_terminated: String = text.chars().chain(once('\0')).collect(); let nul_terminated: String = text.chars().chain(once('\0')).collect();
@ -163,12 +385,33 @@ impl Swkbd {
} }
} }
/// Configures the look and behavior of a button for this keyboard. /// Configure the look and behavior of a button for this keyboard.
///
/// # Arguments
///
/// - `button` - the [`Button`] to be configured based on the position.
/// - `text` - the text displayed in the button.
/// - `submit` - whether pressing the button will accept the keyboard's input or discard it.
///
/// # Example
///
/// ```
/// # let _runner = test_runner::GdbRunner::default();
/// # fn main() {
/// #
/// use ctru::applets::swkbd::{SoftwareKeyboard, Button, Kind};
/// ///
/// `button` is the `Button` to be configured /// // We create a `SoftwareKeyboard` with left and right buttons.
/// `text` configures the display text for the button /// let mut keyboard = SoftwareKeyboard::new(Kind::Normal, 2);
/// `submit` configures whether pressing the button will accept the keyboard's input or ///
/// discard it. /// // Set the left button text to "Cancel" and pressing it will NOT return the user's input.
/// keyboard.configure_button(Button::Left, "Cancel", false);
///
/// // Set the right button text to "Ok" and pressing it will return the user's input.
/// keyboard.configure_button(Button::Right, "Ok", true);
/// #
/// # }
#[doc(alias = "swkbdSetButton")]
pub fn configure_button(&mut self, button: Button, text: &str, submit: bool) { pub fn configure_button(&mut self, button: Button, text: &str, submit: bool) {
unsafe { unsafe {
let nul_terminated: String = text.chars().chain(once('\0')).collect(); let nul_terminated: String = text.chars().chain(once('\0')).collect();
@ -181,19 +424,35 @@ impl Swkbd {
} }
} }
/// Configures the maximum number of UTF-16 code units that can be entered into the software /// Configure the maximum number of UTF-16 code units that can be entered into the software
/// keyboard. By default the limit is 0xFDE8 code units. /// keyboard. By default the limit is `65000` code units.
/// ///
/// Note that keyboard input is converted from UTF-16 to UTF-8 before being handed to Rust, /// # Notes
///
/// Keyboard input is converted from UTF-16 to UTF-8 before being handed to Rust,
/// so this code point limit does not necessarily equal the max number of UTF-8 code points /// so this code point limit does not necessarily equal the max number of UTF-8 code points
/// receivable by the `get_utf8` and `get_bytes` functions. /// receivable by [`SoftwareKeyboard::get_string()`] and [`SoftwareKeyboard::write_exact()`].
///
/// # Example
///
/// ```
/// # let _runner = test_runner::GdbRunner::default();
/// # fn main() {
/// #
/// use ctru::applets::swkbd::{SoftwareKeyboard, Button, Kind};
/// let mut keyboard = SoftwareKeyboard::default();
///
/// // Set the maximum text length to 18 UTF-16 code units.
/// keyboard.set_max_text_len(18);
/// #
/// # }
pub fn set_max_text_len(&mut self, len: u16) { pub fn set_max_text_len(&mut self, len: u16) {
self.state.max_text_len = len; self.state.max_text_len = len;
} }
fn parse_swkbd_error(&self) -> Error { fn parse_swkbd_error(&self) -> Error {
match self.state.result { match self.state.result {
ctru_sys::SWKBD_INVALID_INPUT => Error::InvalidInput, ctru_sys::SWKBD_INVALID_INPUT => Error::InvalidParameters,
ctru_sys::SWKBD_OUTOFMEM => Error::OutOfMem, ctru_sys::SWKBD_OUTOFMEM => Error::OutOfMem,
ctru_sys::SWKBD_HOMEPRESSED => Error::HomePressed, ctru_sys::SWKBD_HOMEPRESSED => Error::HomePressed,
ctru_sys::SWKBD_RESETPRESSED => Error::ResetPressed, ctru_sys::SWKBD_RESETPRESSED => Error::ResetPressed,
@ -206,11 +465,43 @@ impl Swkbd {
} }
} }
impl Default for Swkbd { /// Creates a new [`SoftwareKeyboard`] configuration set to using a [`Kind::Normal`] keyboard and 2 [`Button`]s.
impl Default for SoftwareKeyboard {
fn default() -> Self { fn default() -> Self {
Swkbd::new(Kind::Normal, 2) SoftwareKeyboard::new(Kind::Normal, 2)
}
}
impl Display for Error {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::InvalidParameters => write!(
f,
"software keyboard was configured with invalid parameters"
),
Self::OutOfMem => write!(f, "software keyboard ran out of memory"),
Self::HomePressed => {
write!(f, "home button pressed while software keyboard was running")
}
Self::ResetPressed => write!(
f,
"reset button pressed while software keyboard was running"
),
Self::PowerPressed => write!(
f,
"power button pressed while software keyboard was running"
),
Self::ParentalOk => write!(f, "parental lock pin was correct"),
Self::ParentalFail => write!(f, "parental lock pin was incorrect"),
Self::BannedInput => write!(
f,
"input given to the software keyboard triggered the active filters"
),
} }
} }
}
impl std::error::Error for Error {}
from_impl!(Kind, ctru_sys::SwkbdType); from_impl!(Kind, ctru_sys::SwkbdType);
from_impl!(Button, ctru_sys::SwkbdButton); from_impl!(Button, ctru_sys::SwkbdButton);

326
ctru-rs/src/console.rs

@ -1,3 +1,10 @@
//! Virtual text console.
//!
//! The [`Console`] works as a virtual shell that renders on screen all output of `stdout`. As such, it is useful as a basic interface to show info to the user,
//! such as in simple "Hello World" applications or more complex software that does not need much user interaction.
//!
//! Have a look at [`Soc::redirect_to_3dslink()`](crate::services::soc::Soc::redirect_to_3dslink) for a better alternative when debugging applications.
use std::cell::RefMut; use std::cell::RefMut;
use std::default::Default; use std::default::Default;
@ -7,31 +14,129 @@ use crate::services::gfx::Screen;
static mut EMPTY_CONSOLE: PrintConsole = unsafe { const_zero::const_zero!(PrintConsole) }; static mut EMPTY_CONSOLE: PrintConsole = unsafe { const_zero::const_zero!(PrintConsole) };
/// Error enum for generic errors within [`Console`].
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum Error {
/// The coordinate specified on the given axis exceeds the limits imposed by the [`Console`] window.
CoordinateOutOfBounds(Axis),
/// The size specified for the given dimension exceeds the limits imposed by the [`Console`] window.
DimensionOutOfBounds(Dimension),
}
/// 2D coordinate axes.
#[allow(missing_docs)]
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum Axis {
X,
Y,
}
/// 2D dimensions.
#[allow(missing_docs)]
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum Dimension {
Width,
Height,
}
/// Virtual text console.
///
/// [`Console`] lets the application redirect `stdout` and `stderr` to a simple text displayer on the 3DS screen.
/// This means that any text written to `stdout` and `stderr` (e.g. using `println!`, `eprintln!` or `dbg!`) will become visible in the area taken by the console.
///
/// # Notes
///
/// The [`Console`] will take full possession of the screen handed to it as long as it stays alive. It also supports some ANSI codes, such as text color and cursor positioning.
/// The [`Console`]'s window size will be:
/// - 40x30 on the [`BottomScreen`](crate::services::gfx::BottomScreen).
/// - 50x30 on the normal [`TopScreen`](crate::services::gfx::TopScreen).
/// - 100x30 on the [`TopScreen`](crate::services::gfx::TopScreen) when wide mode is enabled.
///
/// # Alternatives
///
/// If you'd like to see live standard output while running the application but cannot or do not want to show the text on the 3DS itself,
/// you can try using [`Soc::redirect_to_3dslink`](crate::services::soc::Soc::redirect_to_3dslink) while activating the `--server` flag for `3dslink` (also supported by `cargo-3ds`).
/// More info in the [`cargo-3ds` docs](https://github.com/rust3ds/cargo-3ds#running-executables).
#[doc(alias = "PrintConsole")]
pub struct Console<'screen> { pub struct Console<'screen> {
context: Box<PrintConsole>, context: Box<PrintConsole>,
_screen: RefMut<'screen, dyn Screen>, screen: RefMut<'screen, dyn Screen>,
} }
impl<'screen> Console<'screen> { impl<'screen> Console<'screen> {
/// Initialize a console on the chosen screen, overwriting whatever was on the screen /// Initialize a console on the chosen screen.
/// previously (including other consoles). The new console is automatically selected for
/// printing.
/// ///
/// # Notes /// # Notes
/// ///
/// [Console] automatically takes care of flushing and swapping buffers for its screen when printing. /// This operation overwrites whatever was on the screen before the initialization (including other [`Console`]s)
/// and changes the [`FramebufferFormat`](crate::services::gspgpu::FramebufferFormat) of the selected screen to better suit the [`Console`].
///
/// The new console is automatically selected for printing.
///
/// [`Console`] automatically takes care of flushing and swapping buffers for its screen when printing.
///
/// # Panics
///
/// If the [`Gfx`](crate::services::gfx::Gfx) service was initialised via [`Gfx::with_formats_vram()`](crate::services::gfx::Gfx::with_formats_vram)
/// this function will crash the program with an ARM exception.
///
/// # Example
///
/// ```
/// # let _runner = test_runner::GdbRunner::default();
/// # use std::error::Error;
/// # fn main() -> Result<(), Box<dyn Error>> {
/// #
/// use ctru::console::Console;
/// use ctru::services::gfx::Gfx;
///
/// // Initialize graphics (using framebuffers allocated on the HEAP).
/// let gfx = Gfx::new()?;
///
/// // Create a `Console` that takes control of the upper LCD screen.
/// let top_console = Console::new(gfx.top_screen.borrow_mut());
///
/// println!("I'm on the top screen!");
/// #
/// # Ok(())
/// # }
/// ```
#[doc(alias = "consoleInit")]
pub fn new(screen: RefMut<'screen, dyn Screen>) -> Self { pub fn new(screen: RefMut<'screen, dyn Screen>) -> Self {
let mut context = Box::<PrintConsole>::default(); let mut context = Box::<PrintConsole>::default();
unsafe { consoleInit(screen.as_raw(), context.as_mut()) }; unsafe { consoleInit(screen.as_raw(), context.as_mut()) };
Console { Console { context, screen }
context,
_screen: screen,
}
} }
/// Returns true if a valid Console to print on is selected /// Returns `true` if a valid [`Console`] to print on is currently selected.
///
/// # Notes
///
/// This function is used to check whether one of the two screens has an existing (and selected) [`Console`],
/// so that the program can be sure its output will be shown *somewhere*.
///
/// # Example
///
/// ```
/// # let _runner = test_runner::GdbRunner::default();
/// # use std::error::Error;
/// # fn main() -> Result<(), Box<dyn Error>> {
/// #
/// # use ctru::services::gfx::Gfx;
/// # // Initialize graphics.
/// # let gfx = Gfx::new()?;
/// #
/// use ctru::console::Console;
/// let top_console = Console::new(gfx.top_screen.borrow_mut());
///
/// // There is at least one selected `Console`.
/// assert!(Console::exists());
/// #
/// # Ok(())
/// # }
/// ```
pub fn exists() -> bool { pub fn exists() -> bool {
unsafe { unsafe {
let current_console = ctru_sys::consoleSelect(&mut EMPTY_CONSOLE); let current_console = ctru_sys::consoleSelect(&mut EMPTY_CONSOLE);
@ -44,28 +149,178 @@ impl<'screen> Console<'screen> {
} }
} }
/// Select this console as the current target for stdout /// Select this console as the current target for standard output.
///
/// # Notes
///
/// Any previously selected console will be unhooked and will not show the `stdout` and `stderr` output.
///
/// # Example
///
/// ```
/// # let _runner = test_runner::GdbRunner::default();
/// # use std::error::Error;
/// # fn main() -> Result<(), Box<dyn Error>> {
/// #
/// # use ctru::services::gfx::Gfx;
/// # let gfx = Gfx::new()?;
/// #
/// use ctru::console::Console;
///
/// // Create a `Console` that takes control of the upper LCD screen.
/// let top_console = Console::new(gfx.top_screen.borrow_mut());
///
/// // Create a `Console` that takes control of the lower LCD screen.
/// let bottom_console = Console::new(gfx.bottom_screen.borrow_mut());
///
/// // Remember that `Console::new` automatically selects the new `Console` for output.
/// println!("I'm on the bottom screen!");
///
/// top_console.select();
///
/// println!("Being on the upper screen is much better!");
/// #
/// # Ok(())
/// # }
/// ```
#[doc(alias = "consoleSelect")]
pub fn select(&self) { pub fn select(&self) {
unsafe { unsafe {
consoleSelect(self.context.as_ref() as *const _ as *mut _); consoleSelect(self.context.as_ref() as *const _ as *mut _);
} }
} }
/// Clears all text from the console /// Clear all text from the console.
#[doc(alias = "consoleClear")]
pub fn clear(&self) { pub fn clear(&self) {
unsafe { consoleClear() } unsafe { consoleClear() }
} }
/// Resizes the active console to fit in a smaller portion of the screen. /// Resize the console to fit in a smaller portion of the screen.
///
/// # Notes
/// ///
/// The first two arguments are the desired coordinates of the top-left corner /// The first two arguments are the desired coordinates of the top-left corner
/// of the console, and the second pair is the new width and height /// of the new window based on the row/column coordinates of a full-screen console.
/// The second pair is the new width and height.
///
/// # Example
///
/// ```
/// # use std::error::Error;
/// # fn main() -> Result<(), Box<dyn Error>> {
/// #
/// # use ctru::services::gfx::Gfx;
/// # let gfx = Gfx::new()?;
/// #
/// # use ctru::console::Console;
/// #
/// let mut top_console = Console::new(gfx.top_screen.borrow_mut());
/// top_console.set_window(10, 10, 16, 6);
/// ///
/// # Safety /// println!("I'm becoming claustrophobic in here!");
/// This function is unsafe because it does not validate that the input will produce /// #
/// a console that actually fits on the screen /// # Ok(())
pub unsafe fn set_window(&mut self, x: i32, y: i32, width: i32, height: i32) { /// # }
consoleSetWindow(self.context.as_mut(), x, y, width, height); /// ```
#[doc(alias = "consoleSetWindow")]
pub fn set_window(&mut self, x: u8, y: u8, width: u8, height: u8) -> Result<(), Error> {
let height_limit = 30;
let length_limit = self.max_width();
if x >= length_limit {
return Err(Error::CoordinateOutOfBounds(Axis::X));
}
if y >= height_limit {
return Err(Error::CoordinateOutOfBounds(Axis::Y));
}
if (x + width) > length_limit {
return Err(Error::DimensionOutOfBounds(Dimension::Width));
}
if (y + height) > height_limit {
return Err(Error::DimensionOutOfBounds(Dimension::Height));
}
unsafe {
consoleSetWindow(
self.context.as_mut(),
x.into(),
y.into(),
width.into(),
height.into(),
)
};
Ok(())
}
/// Reset the window's size to default parameters.
///
/// This can be used to undo the changes made by [`set_window()`](Console::set_window()).
///
/// # Example
///
/// ```
/// # use std::error::Error;
/// # fn main() -> Result<(), Box<dyn Error>> {
/// #
/// # use ctru::services::gfx::Gfx;
/// # let gfx = Gfx::new()?;
/// #
/// # use ctru::console::Console;
/// #
/// let mut top_console = Console::new(gfx.top_screen.borrow_mut());
/// top_console.set_window(15, 15, 8, 10);
///
/// println!("It's really jammed in here!");
///
/// top_console.reset_window();
///
/// println!("Phew, finally a breath of fresh air.");
/// #
/// # Ok(())
/// # }
/// ```
pub fn reset_window(&mut self) {
let width = self.max_width();
self.set_window(0, 0, width, 30).unwrap();
}
/// Returns this [`Console`]'s maximum character width depending on the screen used.
///
/// # Example
///
/// ```
/// # use std::error::Error;
/// # fn main() -> Result<(), Box<dyn Error>> {
/// #
/// # use ctru::services::gfx::Gfx;
/// # use ctru::console::Console;
/// #
/// let gfx = Gfx::new()?;
///
/// let top_console = Console::new(gfx.top_screen.borrow_mut());
///
/// // The maximum width for the top screen (without any alterations) is 50 characters.
/// assert_eq!(top_console.max_width(), 50);
/// #
/// # Ok(())
/// # }
/// ```
pub fn max_width(&self) -> u8 {
match self.screen.as_raw() {
ctru_sys::GFX_TOP => {
if unsafe { ctru_sys::gfxIsWide() } {
100
} else {
50
}
}
ctru_sys::GFX_BOTTOM => 40,
_ => unreachable!(),
}
} }
} }
@ -94,3 +349,36 @@ impl Drop for Console<'_> {
} }
} }
} }
impl std::fmt::Display for Axis {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::X => write!(f, "x"),
Self::Y => write!(f, "y"),
}
}
}
impl std::fmt::Display for Dimension {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Width => write!(f, "width"),
Self::Height => write!(f, "height"),
}
}
}
impl std::fmt::Display for Error {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::CoordinateOutOfBounds(a) => {
write!(f, "coordinate specified for the {a} axis is out of bounds")
}
Self::DimensionOutOfBounds(d) => {
write!(f, "size specified for the {d} is out of bounds")
}
}
}
}
impl std::error::Error for Error {}

42
ctru-rs/src/error.rs

@ -1,3 +1,7 @@
//! Error handling interface.
//!
//! This module holds the generic error and result types to interface with `ctru_sys` and the [`ctru-rs`](crate) safe wrapper.
use std::borrow::Cow; use std::borrow::Cow;
use std::error; use std::error;
use std::ffi::CStr; use std::ffi::CStr;
@ -6,8 +10,30 @@ use std::ops::{ControlFlow, FromResidual, Try};
use ctru_sys::result::{R_DESCRIPTION, R_LEVEL, R_MODULE, R_SUMMARY}; use ctru_sys::result::{R_DESCRIPTION, R_LEVEL, R_MODULE, R_SUMMARY};
/// Custom type alias for generic [`ctru-rs`](crate) operations.
///
/// This type is compatible with [`ctru_sys::Result`] codes.
pub type Result<T> = ::std::result::Result<T, Error>; pub type Result<T> = ::std::result::Result<T, Error>;
/// Validity checker of raw [`ctru_sys::Result`] codes.
///
/// This struct supports the "try" syntax (`?`) to convert to an [`Error::Os`].
///
/// # Example
///
/// ```
/// use ctru::error::{Result, ResultCode};
///
/// pub fn main() -> Result<()> {
/// # let _runner = test_runner::GdbRunner::default();
/// // We run an unsafe function which returns a `ctru_sys::Result`.
/// let result: ctru_sys::Result = unsafe { ctru_sys::hidInit() };
///
/// // The result code is parsed and any possible error gets returned by the function.
/// ResultCode(result)?;
/// Ok(())
/// }
/// ```
#[derive(Debug, Copy, Clone, PartialEq, PartialOrd, Eq, Ord)] #[derive(Debug, Copy, Clone, PartialEq, PartialOrd, Eq, Ord)]
#[repr(transparent)] #[repr(transparent)]
pub struct ResultCode(pub ctru_sys::Result); pub struct ResultCode(pub ctru_sys::Result);
@ -48,13 +74,20 @@ impl<T> FromResidual<Error> for Result<T> {
} }
} }
/// The error type returned by all libctru functions. /// The generic error enum returned by [`ctru-rs`](crate) functions.
///
/// This error enum supports parsing and displaying [`ctru_sys::Result`] codes.
#[non_exhaustive] #[non_exhaustive]
pub enum Error { pub enum Error {
/// Raw [`ctru_sys::Result`] codes.
Os(ctru_sys::Result), Os(ctru_sys::Result),
/// Generic [`libc`] errors.
Libc(String), Libc(String),
/// Requested service is already active and cannot be activated again.
ServiceAlreadyActive, ServiceAlreadyActive,
/// `stdout` is already being redirected.
OutputAlreadyRedirected, OutputAlreadyRedirected,
/// The buffer provided by the user to store some data is shorter than required.
BufferTooShort { BufferTooShort {
/// Length of the buffer provided by the user. /// Length of the buffer provided by the user.
provided: usize, provided: usize,
@ -64,8 +97,9 @@ pub enum Error {
} }
impl Error { impl Error {
/// Create an [`Error`] out of the last set value in `errno`. This can be used /// Create an [`Error`] out of the last set value in `errno`.
/// to get a human-readable error string from calls to `libc` functions. ///
/// This can be used to get a human-readable error string from calls to `libc` functions.
pub(crate) fn from_errno() -> Self { pub(crate) fn from_errno() -> Self {
let error_str = unsafe { let error_str = unsafe {
let errno = ctru_sys::errno(); let errno = ctru_sys::errno();
@ -126,6 +160,8 @@ impl fmt::Debug for Error {
impl fmt::Display for Error { impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self { match self {
// TODO: should we consider using ctru_sys::osStrError here as well?
// It might do some of the work for us or provide additional details
&Self::Os(err) => write!( &Self::Os(err) => write!(
f, f,
"libctru result code 0x{err:08X}: [{} {}] {}: {}", "libctru result code 0x{err:08X}: [{} {}] {}: {}",

87
ctru-rs/src/lib.rs

@ -1,18 +1,49 @@
//! Safe and idiomatic Rust wrapper around [`libctru`](https://github.com/devkitPro/libctru).
//!
//! # About
//!
//! This crate behaves as the main tool to access system-specific functionality on the Nintendo 3DS when developing homebrew software in Rust.
//! Thanks to it, developers can develop userland applications by accessing access the underlying system services and the console's hardware
//! (such as [HID devices](crate::services::hid), [network capabilities](crate::services::soc), [graphics](crate::services::gfx), [built-in cameras](crate::services::cam), etc.).
//!
//! Among these features, [`ctru-rs`](crate) also automatically includes functionality to properly integrate the Rust `std` with the console's operating system,
//! which the developer would otherwise need to implement manually.
//!
//! # Usage
//!
//! Thoroughly read the official [`ctru-rs` wiki](https://github.com/rust3ds/ctru-rs/wiki) which guides you through the setup needed to install the required toolchain and helpful tools.
//! After following the guide and understanding the many quirks of the Nintendo 3DS homebrew development environment, you can create a new project by including this crate as a dependency
//! in your `Cargo.toml` manifest and build your binaries either manually (for the `armv6k-nintendo-3ds` target) or via [`cargo-3ds`](https://github.com/rust3ds/cargo-3ds).
#![crate_type = "rlib"] #![crate_type = "rlib"]
#![crate_name = "ctru"] #![crate_name = "ctru"]
#![feature(test)] #![warn(missing_docs)]
#![feature(custom_test_frameworks)] #![feature(custom_test_frameworks)]
#![feature(try_trait_v2)] #![feature(try_trait_v2)]
#![feature(allocator_api)] #![feature(allocator_api)]
#![feature(nonnull_slice_from_raw_parts)] #![test_runner(test_runner::run_gdb)] // TODO: does this make sense to have configurable?
#![test_runner(test_runner::run)] #![doc(
html_favicon_url = "https://user-images.githubusercontent.com/11131775/225929072-2fa1741c-93ae-4b47-9bdf-af70f3d59910.png"
)]
#![doc(
html_logo_url = "https://user-images.githubusercontent.com/11131775/225929072-2fa1741c-93ae-4b47-9bdf-af70f3d59910.png"
)]
// Nothing is imported from these crates but their inclusion here assures correct linking of the missing implementations. // Nothing is imported from these crates but their inclusion here assures correct linking of the missing implementations.
extern crate pthread_3ds; extern crate pthread_3ds;
extern crate shim_3ds; extern crate shim_3ds;
/// Expanded stack size used to spawn the main thread by `libctru`.
///
/// It takes effect only if the `big-stack` feature is active. Otherwise, the default stack size should be ~32kB.
///
/// This value was chosen to support crate dependencies which expected more stack than provided. It's suggested to use less stack if possible.
#[no_mangle] #[no_mangle]
#[cfg(feature = "big-stack")] // When building lib tests, we don't want to redefine the same symbol twice,
// since ctru-rs is both the crate under test and a dev-dependency (non-test).
// We might also be able to use #[linkage] for similar effect, but this way
// works without depending on another unstable feature.
#[cfg(all(feature = "big-stack", not(test)))]
static __stacksize__: usize = 2 * 1024 * 1024; // 2MB static __stacksize__: usize = 2 * 1024 * 1024; // 2MB
macro_rules! from_impl { macro_rules! from_impl {
@ -25,59 +56,13 @@ macro_rules! from_impl {
}; };
} }
/// Activate the default panic handler.
///
/// With this implementation, the main thread will stop and try to print debug info to an available [console::Console].
/// In case it fails to find an active [console::Console] the program will just exit.
///
/// # Notes
///
/// When ´test´ is enabled, this function won't do anything, as it should be overridden by the ´test´ environment.
pub fn use_panic_handler() {
#[cfg(not(test))]
panic_hook_setup();
}
#[cfg(not(test))]
fn panic_hook_setup() {
use crate::services::hid::{Hid, KeyPad};
use std::panic::PanicInfo;
let main_thread = std::thread::current().id();
// Panic Hook setup
let default_hook = std::panic::take_hook();
let new_hook = Box::new(move |info: &PanicInfo| {
default_hook(info);
// Only for panics in the main thread
if main_thread == std::thread::current().id() && console::Console::exists() {
println!("\nPress SELECT to exit the software");
match Hid::new() {
Ok(mut hid) => loop {
hid.scan_input();
let keys = hid.keys_down();
if keys.contains(KeyPad::SELECT) {
break;
}
},
Err(e) => println!("Error while intializing Hid controller during panic: {e}"),
}
}
});
std::panic::set_hook(new_hook);
}
pub mod applets; pub mod applets;
pub mod console; pub mod console;
pub mod error; pub mod error;
pub mod linear; pub mod linear;
pub mod mii; pub mod mii;
pub mod os;
pub mod prelude; pub mod prelude;
pub mod services; pub mod services;
#[cfg(test)]
mod test_runner;
pub use crate::error::{Error, Result}; pub use crate::error::{Error, Result};

21
ctru-rs/src/linear.rs

@ -1,11 +1,12 @@
//! Linear memory allocator //! LINEAR memory allocator.
//! //!
//! Linear memory is a sector of the 3DS' RAM that binds virtual addresses exactly to the physical address. //! LINEAR memory is a sector of the 3DS' RAM that binds virtual addresses exactly to the physical address.
//! As such, it is used for fast and safe memory sharing between services (and is especially needed for GPU and DSP). //! As such, it is used for fast and safe memory sharing between different hardware components (such as the GPU and the DSP processor).
//! //!
//! Resources:<br> //! # Additional Resources
//! <https://github.com/devkitPro/libctru/blob/master/libctru/source/allocator/linear.cpp><br> //!
//! <https://www.3dbrew.org/wiki/Memory_layout> //! - <https://github.com/devkitPro/libctru/blob/master/libctru/source/allocator/linear.cpp>
//! - <https://www.3dbrew.org/wiki/Memory_layout>
use std::alloc::{AllocError, Allocator, Layout}; use std::alloc::{AllocError, Allocator, Layout};
use std::ptr::NonNull; use std::ptr::NonNull;
@ -15,19 +16,22 @@ use std::ptr::NonNull;
// Sadly the linear memory allocator included in `libctru` doesn't implement `linearRealloc` at the time of these additions, // Sadly the linear memory allocator included in `libctru` doesn't implement `linearRealloc` at the time of these additions,
// but the default fallback of the `std` will take care of that for us. // but the default fallback of the `std` will take care of that for us.
/// [`std::alloc::Allocator`] struct for LINEAR memory /// [`Allocator`] struct for LINEAR memory.
///
/// To use this struct the main crate must activate the `allocator_api` unstable feature. /// To use this struct the main crate must activate the `allocator_api` unstable feature.
#[derive(Copy, Clone, Default, Debug)] #[derive(Copy, Clone, Default, Debug)]
pub struct LinearAllocator; pub struct LinearAllocator;
impl LinearAllocator { impl LinearAllocator {
/// Returns the amount of free space left in the LINEAR sector /// Returns the amount of free space left in the LINEAR memory sector.
#[doc(alias = "linearSpaceFree")]
pub fn free_space() -> u32 { pub fn free_space() -> u32 {
unsafe { ctru_sys::linearSpaceFree() } unsafe { ctru_sys::linearSpaceFree() }
} }
} }
unsafe impl Allocator for LinearAllocator { unsafe impl Allocator for LinearAllocator {
#[doc(alias = "linearAlloc", alias = "linearMemAlign")]
fn allocate(&self, layout: Layout) -> Result<NonNull<[u8]>, AllocError> { fn allocate(&self, layout: Layout) -> Result<NonNull<[u8]>, AllocError> {
let pointer = unsafe { ctru_sys::linearMemAlign(layout.size(), layout.align()) }; let pointer = unsafe { ctru_sys::linearMemAlign(layout.size(), layout.align()) };
@ -36,6 +40,7 @@ unsafe impl Allocator for LinearAllocator {
.ok_or(AllocError) .ok_or(AllocError)
} }
#[doc(alias = "linearFree")]
unsafe fn deallocate(&self, ptr: NonNull<u8>, _layout: Layout) { unsafe fn deallocate(&self, ptr: NonNull<u8>, _layout: Layout) {
ctru_sys::linearFree(ptr.as_ptr().cast()); ctru_sys::linearFree(ptr.as_ptr().cast());
} }

176
ctru-rs/src/mii.rs

@ -1,213 +1,301 @@
//! Mii Data //! Mii data.
//! //!
//! This module contains the structs that represent all the data of a Mii. //! This module contains the structs that represent all the data of a Mii.
//! This data is given by the [``MiiSelector``](crate::applets::mii_selector::MiiSelector) //!
//! Have a look at the [`MiiSelector`](crate::applets::mii_selector::MiiSelector) applet to learn how to ask the user for a specific Mii.
/// Represents the region lock of the console /// Region lock of the Mii.
#[derive(Copy, Clone, Debug, Eq, PartialEq)] #[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub enum RegionLock { pub enum RegionLock {
/// No region-lock.
None, None,
/// Japan region-lock.
Japan, Japan,
/// USA region-lock.
USA, USA,
/// Europe region-lock.
Europe, Europe,
} }
/// Represent the charset of the console /// Charset of the Mii.
#[derive(Copy, Clone, Debug, Eq, PartialEq)] #[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub enum Charset { pub enum Charset {
/// Japan-USA-Europe unified charset.
JapanUSAEurope, JapanUSAEurope,
/// China charset.
China, China,
/// Korea charset.
Korea, Korea,
/// Taiwan charset.
Taiwan, Taiwan,
} }
/// Represents the options of the Mii /// Generic options of the Mii.
#[derive(Copy, Clone, Debug)] #[derive(Copy, Clone, Debug)]
pub struct MiiDataOptions { pub struct Options {
/// Whether it is allowed to copy the Mii.
pub is_copying_allowed: bool, pub is_copying_allowed: bool,
/// Whether the profanity flag is active.
pub is_profanity_flag_enabled: bool, pub is_profanity_flag_enabled: bool,
/// The Mii's active region-lock.
pub region_lock: RegionLock, pub region_lock: RegionLock,
/// The Mii's used charset.
pub charset: Charset, pub charset: Charset,
} }
/// Represents the position that the Mii has on the selector /// Positional Index that the Mii has on the [`MiiSelector`](crate::applets::mii_selector::MiiSelector) window.
#[derive(Copy, Clone, Debug)] #[derive(Copy, Clone, Debug)]
pub struct SelectorPosition { pub struct SelectorPosition {
/// Index of the page where the Mii is found.
pub page_index: u8, pub page_index: u8,
/// Index of the slot (relative to the page) where the Mii is found.
pub slot_index: u8, pub slot_index: u8,
} }
/// Represents the kind of origin console /// Console model from which the Mii originated.
#[derive(Copy, Clone, Debug, Eq, PartialEq)] #[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub enum OriginConsole { pub enum OriginConsole {
/// Nintendo Wii.
Wii, Wii,
/// Nintendo DSi.
DSi, DSi,
/// Both New 3DS and Old 3DS /// Nintendo 3DS.
///
/// This includes all consoles of the 3DS family (3DS, 2DS, and their respective "New" or "XL" variants).
N3DS, N3DS,
/// Nintendo Wii U/Switch.
WiiUSwitch, WiiUSwitch,
} }
/// Represents the identity of the origin console /// Identity of the origin console.
#[derive(Copy, Clone, Debug)] #[derive(Copy, Clone, Debug)]
pub struct ConsoleIdentity { pub struct ConsoleIdentity {
/// From which console the Mii originated from.
pub origin_console: OriginConsole, pub origin_console: OriginConsole,
} }
/// Represents the sex of the Mii /// Sex of the Mii.
#[derive(Copy, Clone, Debug, Eq, PartialEq)] #[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub enum MiiSex { pub enum Sex {
/// Male sex.
Male, Male,
/// Female sex.
Female, Female,
} }
/// Represents the details of the Mii /// Generic details of the Mii.
#[derive(Copy, Clone, Debug)] #[derive(Copy, Clone, Debug)]
pub struct Details { pub struct Details {
pub sex: MiiSex, /// Sex of the Mii.
pub sex: Sex,
/// Birthday month.
pub birthday_month: u8, pub birthday_month: u8,
/// Birthday day.
pub birthday_day: u8, pub birthday_day: u8,
/// Color of the Mii's shirt.
pub shirt_color: u8, pub shirt_color: u8,
/// Whether the Mii is a favorite.
pub is_favorite: bool, pub is_favorite: bool,
/// Whether the Mii can be shared.
pub is_sharing_enabled: bool,
} }
/// Represents the face style of the Mii /// Face style of the Mii.
#[derive(Copy, Clone, Debug)] #[derive(Copy, Clone, Debug)]
pub struct FaceStyle { pub struct FaceStyle {
pub is_sharing_enabled: bool, /// Face shape.
pub shape: u8, pub shape: u8,
/// Skin color.
pub skin_color: u8, pub skin_color: u8,
} }
/// Represents the face details of the Mii /// Face details of the Mii.
#[derive(Copy, Clone, Debug)] #[derive(Copy, Clone, Debug)]
pub struct FaceDetails { pub struct FaceDetails {
/// Face style.
pub style: FaceStyle, pub style: FaceStyle,
/// Wrinkles.
pub wrinkles: u8, pub wrinkles: u8,
/// Makeup.
pub makeup: u8, pub makeup: u8,
} }
/// Represents the hair details of the Mii /// Hair details of the Mii.
#[derive(Copy, Clone, Debug)] #[derive(Copy, Clone, Debug)]
pub struct HairDetails { pub struct HairDetails {
/// Hair style.
pub style: u8, pub style: u8,
/// Hair color.
pub color: u8, pub color: u8,
/// Whether the Mii's hair is flipped.
pub is_flipped: bool, pub is_flipped: bool,
} }
/// Represents the eye details of the Mii /// Eye details of the Mii.
#[derive(Copy, Clone, Debug)] #[derive(Copy, Clone, Debug)]
pub struct EyeDetails { pub struct EyeDetails {
/// Eye style.
pub style: u8, pub style: u8,
/// Eye color.
pub color: u8, pub color: u8,
/// Eye scale.
pub scale: u8, pub scale: u8,
/// Eye scale (y-axis).
pub y_scale: u8, pub y_scale: u8,
/// Eye rotation.
pub rotation: u8, pub rotation: u8,
/// Spacing between the eyes /// Spacing between the eyes.
pub x_spacing: u8, pub x_spacing: u8,
/// Eye height.
pub y_position: u8, pub y_position: u8,
} }
/// Represents the eyebrow details of the Mii /// Eyebrow details of the Mii.
#[derive(Copy, Clone, Debug)] #[derive(Copy, Clone, Debug)]
pub struct EyebrowDetails { pub struct EyebrowDetails {
/// Eyebrow style.
pub style: u8, pub style: u8,
/// Eyebrow color.
pub color: u8, pub color: u8,
/// Eyebrow scale.
pub scale: u8, pub scale: u8,
/// Eyebrow scale (y-axis).
pub y_scale: u8, pub y_scale: u8,
/// Eyebrow rotation.
pub rotation: u8, pub rotation: u8,
/// Spacing between the eyebrows /// Spacing between the eyebrows
pub x_spacing: u8, pub x_spacing: u8,
/// Eyebrow height.
pub y_position: u8, pub y_position: u8,
} }
/// Represents the details of the nose /// Nose details of the Mii.
#[derive(Copy, Clone, Debug)] #[derive(Copy, Clone, Debug)]
pub struct NoseDetails { pub struct NoseDetails {
/// Nose style.
pub style: u8, pub style: u8,
/// Nose scale.
pub scale: u8, pub scale: u8,
/// Nose height.
pub y_position: u8, pub y_position: u8,
} }
/// Represents the details of the mouth /// Mouth details of the Mii.
#[derive(Copy, Clone, Debug)] #[derive(Copy, Clone, Debug)]
pub struct MouthDetails { pub struct MouthDetails {
/// Mouth style.
pub style: u8, pub style: u8,
/// Mouth color.
pub color: u8, pub color: u8,
/// Mouth scale.
pub scale: u8, pub scale: u8,
/// Mouth scale (y-axis).
pub y_scale: u8, pub y_scale: u8,
/// Mouth height.
pub y_position: u8,
} }
/// Represents the details of the mustache /// Mustache details of the Mii.
#[derive(Copy, Clone, Debug)] #[derive(Copy, Clone, Debug)]
pub struct MustacheDetails { pub struct MustacheDetails {
pub mouth_y_position: u8, /// Mustache style.
pub mustache_style: u8, pub mustache_style: u8,
} }
/// Represents the details of the beard /// Beard details of the Mii.
#[derive(Copy, Clone, Debug)] #[derive(Copy, Clone, Debug)]
pub struct BeardDetails { pub struct BeardDetails {
/// Beard style
pub style: u8, pub style: u8,
/// Beard color.
pub color: u8, pub color: u8,
/// Beard scale.
pub scale: u8, pub scale: u8,
/// Beard height.
pub y_position: u8, pub y_position: u8,
} }
/// Represents the details of the glass /// Glasses details of the Mii.
#[derive(Copy, Clone, Debug)] #[derive(Copy, Clone, Debug)]
pub struct GlassDetails { pub struct GlassesDetails {
/// Glasses style.
pub style: u8, pub style: u8,
/// Glasses color.
pub color: u8, pub color: u8,
/// Glasses scale.
pub scale: u8, pub scale: u8,
/// Glasses height.
pub y_position: u8, pub y_position: u8,
} }
/// Represents the details of the mole /// Mole details of the Mii.
#[derive(Copy, Clone, Debug)] #[derive(Copy, Clone, Debug)]
pub struct MoleDetails { pub struct MoleDetails {
/// Whether the Mii has a mole.
pub is_enabled: bool, pub is_enabled: bool,
/// Mole scale.
pub scale: u8, pub scale: u8,
/// Mole position (x-axis).
pub x_position: u8, pub x_position: u8,
/// Mole position (y-axis).
pub y_position: u8, pub y_position: u8,
} }
/// Represents all the data of a Mii /// Full Mii data representation.
/// ///
/// Some values are not ordered _like_ the Mii Editor UI. The mapped values can be seen here: /// Some values are not ordered *like* the Mii Editor UI. The mapped values can be seen [here](https://www.3dbrew.org/wiki/Mii#Mapped_Editor_.3C-.3E_Hex_values).
/// <https://www.3dbrew.org/wiki/Mii#Mapped_Editor_.3C-.3E_Hex_values>
/// ///
/// This struct is returned by the [``MiiSelector``](crate::applets::mii_selector::MiiSelector) /// This struct can be retrieved by [`MiiSelector::launch()`](crate::applets::mii_selector::MiiSelector::launch).
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct MiiData { pub struct Mii {
pub options: MiiDataOptions, /// Mii options.
pub options: Options,
/// Position taken by the Mii on the Mii Selector screen.
pub selector_position: SelectorPosition, pub selector_position: SelectorPosition,
/// Console the Mii was created on.
pub console_identity: ConsoleIdentity, pub console_identity: ConsoleIdentity,
/// Unique system ID, not dependant on the MAC address /// Unique system ID, not dependant on the MAC address
pub system_id: [u8; 8], pub system_id: [u8; 8],
/// Console's MAC address.
pub mac_address: [u8; 6], pub mac_address: [u8; 6],
/// General information about the Mii.
pub details: Details, pub details: Details,
/// Mii name.
pub name: String, pub name: String,
/// Mii height.
pub height: u8, pub height: u8,
/// Mii width.
pub width: u8, pub width: u8,
/// Face details.
pub face_details: FaceDetails, pub face_details: FaceDetails,
/// Hair details.
pub hair_details: HairDetails, pub hair_details: HairDetails,
/// Eyes details.
pub eye_details: EyeDetails, pub eye_details: EyeDetails,
/// Eyebrow details.
pub eyebrow_details: EyebrowDetails, pub eyebrow_details: EyebrowDetails,
/// Nose details.
pub nose_details: NoseDetails, pub nose_details: NoseDetails,
/// Mouth details.
pub mouth_details: MouthDetails, pub mouth_details: MouthDetails,
/// Mustache details.
pub mustache_details: MustacheDetails, pub mustache_details: MustacheDetails,
/// Beard details.
pub beard_details: BeardDetails, pub beard_details: BeardDetails,
pub glass_details: GlassDetails, /// Glasses details.
pub glass_details: GlassesDetails,
/// Mole details.
pub mole_details: MoleDetails, pub mole_details: MoleDetails,
/// Name of the Mii's original author.
pub author_name: String, pub author_name: String,
} }
impl From<ctru_sys::MiiData> for MiiData { impl From<ctru_sys::MiiData> for Mii {
fn from(mii_data: ctru_sys::MiiData) -> Self { fn from(mii_data: ctru_sys::MiiData) -> Self {
let raw_mii_data = mii_data._bindgen_opaque_blob; let raw_mii_data = mii_data._bindgen_opaque_blob;
// Source for the representation and what each thing means: https://www.3dbrew.org/wiki/Mii // Source for the representation and what each thing means: https://www.3dbrew.org/wiki/Mii
@ -272,7 +360,7 @@ impl From<ctru_sys::MiiData> for MiiData {
let name = utf16_byte_pairs_to_string(raw_utf16_name); let name = utf16_byte_pairs_to_string(raw_utf16_name);
let author_name = utf16_byte_pairs_to_string(raw_utf16_author); let author_name = utf16_byte_pairs_to_string(raw_utf16_author);
let options = MiiDataOptions { let options = Options {
is_copying_allowed: raw_options[0], is_copying_allowed: raw_options[0],
is_profanity_flag_enabled: raw_options[1], is_profanity_flag_enabled: raw_options[1],
region_lock: { region_lock: {
@ -312,19 +400,19 @@ impl From<ctru_sys::MiiData> for MiiData {
let details = Details { let details = Details {
sex: { sex: {
match raw_details[0] { match raw_details[0] {
true => MiiSex::Female, true => Sex::Female,
false => MiiSex::Male, false => Sex::Male,
} }
}, },
birthday_month: partial_u8_bits_to_u8(&raw_details[1..=4]), birthday_month: partial_u8_bits_to_u8(&raw_details[1..=4]),
birthday_day: partial_u8_bits_to_u8(&raw_details[5..=9]), birthday_day: partial_u8_bits_to_u8(&raw_details[5..=9]),
shirt_color: partial_u8_bits_to_u8(&raw_details[10..=13]), shirt_color: partial_u8_bits_to_u8(&raw_details[10..=13]),
is_favorite: raw_details[14], is_favorite: raw_details[14],
is_sharing_enabled: !raw_face_style[0],
}; };
let face_details = FaceDetails { let face_details = FaceDetails {
style: FaceStyle { style: FaceStyle {
is_sharing_enabled: !raw_face_style[0],
shape: partial_u8_bits_to_u8(&raw_face_style[1..=4]), shape: partial_u8_bits_to_u8(&raw_face_style[1..=4]),
skin_color: partial_u8_bits_to_u8(&raw_face_style[5..=7]), skin_color: partial_u8_bits_to_u8(&raw_face_style[5..=7]),
}, },
@ -371,10 +459,10 @@ impl From<ctru_sys::MiiData> for MiiData {
color: partial_u8_bits_to_u8(&raw_mouth_details[6..=8]), color: partial_u8_bits_to_u8(&raw_mouth_details[6..=8]),
scale: partial_u8_bits_to_u8(&raw_mouth_details[9..=12]), scale: partial_u8_bits_to_u8(&raw_mouth_details[9..=12]),
y_scale: partial_u8_bits_to_u8(&raw_mouth_details[13..=15]), y_scale: partial_u8_bits_to_u8(&raw_mouth_details[13..=15]),
y_position: partial_u8_bits_to_u8(&raw_mustache_details[0..=4]),
}; };
let mustache_details = MustacheDetails { let mustache_details = MustacheDetails {
mouth_y_position: partial_u8_bits_to_u8(&raw_mustache_details[0..=4]),
mustache_style: partial_u8_bits_to_u8(&raw_mustache_details[5..=7]), mustache_style: partial_u8_bits_to_u8(&raw_mustache_details[5..=7]),
}; };
@ -385,7 +473,7 @@ impl From<ctru_sys::MiiData> for MiiData {
y_position: partial_u8_bits_to_u8(&raw_beard_details[10..=14]), y_position: partial_u8_bits_to_u8(&raw_beard_details[10..=14]),
}; };
let glass_details = GlassDetails { let glass_details = GlassesDetails {
style: partial_u8_bits_to_u8(&raw_glass_details[0..=3]), style: partial_u8_bits_to_u8(&raw_glass_details[0..=3]),
color: partial_u8_bits_to_u8(&raw_glass_details[4..=6]), color: partial_u8_bits_to_u8(&raw_glass_details[4..=6]),
scale: partial_u8_bits_to_u8(&raw_glass_details[7..=10]), scale: partial_u8_bits_to_u8(&raw_glass_details[7..=10]),
@ -399,7 +487,7 @@ impl From<ctru_sys::MiiData> for MiiData {
y_position: partial_u8_bits_to_u8(&raw_mole_details[10..=14]), y_position: partial_u8_bits_to_u8(&raw_mole_details[10..=14]),
}; };
MiiData { Mii {
options, options,
selector_position, selector_position,
console_identity, console_identity,

154
ctru-rs/src/os.rs

@ -0,0 +1,154 @@
//! Utilities to get information about the operating system and hardware state.
/// System version information. This struct is used for both kernel and firmware versions.
///
/// # Example
/// ```
/// # let _runner = test_runner::GdbRunner::default();
/// let firm_version = ctru::os::firm_version();
/// assert_ne!(firm_version.major(), 0);
///
/// let kernel_version = ctru::os::kernel_version();
/// assert_ne!(kernel_version.major(), 0);
/// ```
#[derive(Clone, Copy)]
pub struct Version(u32);
impl Version {
/// Pack a system version from its components
pub fn new(major: u8, minor: u8, revision: u8) -> Self {
let major = u32::from(major);
let minor = u32::from(minor);
let revision = u32::from(revision);
Self(major << 24 | minor << 16 | revision << 8)
}
/// Get the major version from a packed system version.
pub fn major(&self) -> u8 {
(self.0 >> 24).try_into().unwrap()
}
/// Get the minor version from a packed system version.
pub fn minor(&self) -> u8 {
(self.0 >> 16 & 0xFF).try_into().unwrap()
}
/// Get the revision from a packed system version.
pub fn revision(&self) -> u8 {
(self.0 >> 8 & 0xFF).try_into().unwrap()
}
}
/// Get the system's FIRM version.
pub fn firm_version() -> Version {
Version(unsafe { ctru_sys::osGetFirmVersion() })
}
/// Get the system's kernel version.
pub fn kernel_version() -> Version {
Version(unsafe { ctru_sys::osGetKernelVersion() })
}
// TODO: I can't seem to find good documentation on it, but we could probably
// define enums for firmware type (NATIVE_FIRM, SAFE_FIRM etc.) as well as
// application memory layout. Leaving those as future enhancements for now
/// A region of memory. Most applications will only use [`Application`](MemRegion::Application)
/// memory, but the other types can be used to query memory usage information.
/// See <https://www.3dbrew.org/wiki/Memory_layout#FCRAM_memory-regions_layout>
/// for more details on the different types of memory.
///
/// # Example
/// ```
/// # let _runner = test_runner::GdbRunner::default();
/// let all_memory = ctru::os::MemRegion::All;
///
/// assert!(all_memory.size() > 0);
/// assert!(all_memory.used() > 0);
/// assert!(all_memory.free() > 0);
/// ```
#[derive(Clone, Copy, Debug)]
#[non_exhaustive]
#[repr(u32)]
pub enum MemRegion {
/// All memory regions.
All = ctru_sys::MEMREGION_ALL,
/// APPLICATION memory.
Application = ctru_sys::MEMREGION_APPLICATION,
/// SYSTEM memory.
System = ctru_sys::MEMREGION_SYSTEM,
/// BASE memory.
Base = ctru_sys::MEMREGION_BASE,
}
impl MemRegion {
/// Get the total size of this memory region, in bytes.
pub fn size(&self) -> usize {
unsafe { ctru_sys::osGetMemRegionSize(*self as u32) }
.try_into()
.unwrap()
}
/// Get the number of bytes used within this memory region.
pub fn used(&self) -> usize {
unsafe { ctru_sys::osGetMemRegionUsed(*self as u32) }
.try_into()
.unwrap()
}
/// Get the number of bytes free within this memory region.
pub fn free(&self) -> usize {
unsafe { ctru_sys::osGetMemRegionFree(*self as u32) }
.try_into()
.unwrap()
}
}
/// WiFi signal strength. This enum's `u8` representation corresponds with
/// the number of bars displayed in the Home menu.
///
/// # Example
///
/// ```
/// let _runner = test_runner::GdbRunner::default();
/// let strength = ctru::os::WifiStrength::current();
/// assert!((strength as u8) < 4);
/// ```
#[derive(Clone, Copy, Debug)]
#[non_exhaustive]
#[repr(u8)]
pub enum WifiStrength {
/// This may indicate a very poor signal quality even worse than `Bad`,
/// or that no network is connected at all.
Disconnected = 0,
/// Poor signal strength.
Bad = 1,
/// Medium signal strength.
Decent = 2,
/// Good signal strength.
Good = 3,
}
impl WifiStrength {
/// Get the current WiFi signal strength.
pub fn current() -> Self {
match unsafe { ctru_sys::osGetWifiStrength() } {
0 => Self::Disconnected,
1 => Self::Bad,
2 => Self::Decent,
3 => Self::Good,
other => panic!("Got unexpected WiFi strength value {other}"),
}
}
}
/// Get the current value of the stereoscopic 3D slider on a scale from 0.0­­1.0.
pub fn current_3d_slider_state() -> f32 {
unsafe { ctru_sys::osGet3DSliderState() }
}
/// Whether or not a headset is currently plugged into the device.
pub fn is_headset_connected() -> bool {
unsafe { ctru_sys::osIsHeadsetConnected() }
}

11
ctru-rs/src/prelude.rs

@ -1,2 +1,11 @@
//! `use ctru::prelude::*;` to import common services, members and functions.
//!
//! Particularly useful when writing very small applications.
pub use crate::console::Console; pub use crate::console::Console;
pub use crate::services::{gfx::Gfx, hid::KeyPad, soc::Soc, Apt, Hid}; pub use crate::services::{
apt::Apt,
gfx::Gfx,
hid::{Hid, KeyPad},
soc::Soc,
};

154
ctru-rs/src/services/am.rs

@ -1,67 +1,79 @@
//! Application Manager service.
//!
//! As the name implies, the AM service manages installed applications. It can:
//! - Read the installed applications on the console and their information (depending on the install location).
//! - Install compatible applications to the console.
//!
//! TODO: [`ctru-rs`](crate) doesn't support installing or uninstalling titles yet.
#![doc(alias = "app")]
#![doc(alias = "manager")]
use crate::error::ResultCode; use crate::error::ResultCode;
use crate::services::fs::FsMediaType; use crate::services::fs::FsMediaType;
use std::marker::PhantomData; use std::marker::PhantomData;
use std::mem::MaybeUninit;
#[derive(Copy, Clone, Debug)]
#[repr(transparent)]
pub struct TitleInfo(ctru_sys::AM_TitleEntry);
impl TitleInfo {
pub fn id(&self) -> u64 {
self.0.titleID
}
pub fn size_bytes(&self) -> u64 {
self.0.size
}
pub fn version(&self) -> u16 {
self.0.version
}
}
/// General information about a specific title entry.
#[doc(alias = "AM_TitleEntry")]
pub struct Title<'a> { pub struct Title<'a> {
id: u64, id: u64,
mediatype: FsMediaType, mediatype: FsMediaType,
size: u64,
version: u16,
_am: PhantomData<&'a Am>, _am: PhantomData<&'a Am>,
} }
impl<'a> Title<'a> { impl<'a> Title<'a> {
/// Returns this title's ID.
pub fn id(&self) -> u64 { pub fn id(&self) -> u64 {
self.id self.id
} }
pub fn product_code(&self) -> crate::Result<String> { /// Returns this title's unique product code.
#[doc(alias = "AM_GetTitleProductCode")]
pub fn product_code(&self) -> String {
let mut buf: [u8; 16] = [0; 16]; let mut buf: [u8; 16] = [0; 16];
// This operation is safe as long as the title was correctly obtained via [`Am::title_list()`].
unsafe { unsafe {
ResultCode(ctru_sys::AM_GetTitleProductCode( let _ =
self.mediatype.into(), ctru_sys::AM_GetTitleProductCode(self.mediatype.into(), self.id, buf.as_mut_ptr());
self.id,
buf.as_mut_ptr(),
))?;
}
Ok(String::from_utf8_lossy(&buf).to_string())
} }
pub fn title_info(&self) -> crate::Result<TitleInfo> { String::from_utf8_lossy(&buf).to_string()
let mut info = MaybeUninit::zeroed(); }
unsafe {
ResultCode(ctru_sys::AM_GetTitleInfo(
self.mediatype.into(),
1,
&mut self.id.clone(),
info.as_mut_ptr() as _,
))?;
Ok(info.assume_init()) /// Returns the size of this title in bytes.
pub fn size(&self) -> u64 {
self.size
} }
/// Returns the installed version of this title.
pub fn version(&self) -> u16 {
self.version
} }
} }
/// Handle to the Application Manager service.
pub struct Am(()); pub struct Am(());
impl Am { impl Am {
/// Initialize a new service handle.
///
/// # Example
///
/// ```
/// # let _runner = test_runner::GdbRunner::default();
/// # use std::error::Error;
/// # fn main() -> Result<(), Box<dyn Error>> {
/// #
/// use ctru::services::am::Am;
///
/// let app_manager = Am::new()?;
/// #
/// # Ok(())
/// # }
/// ```
#[doc(alias = "amInit")]
pub fn new() -> crate::Result<Am> { pub fn new() -> crate::Result<Am> {
unsafe { unsafe {
ResultCode(ctru_sys::amInit())?; ResultCode(ctru_sys::amInit())?;
@ -69,6 +81,29 @@ impl Am {
} }
} }
/// Returns the amount of titles currently installed in a specific install location.
///
/// # Example
///
/// ```
/// # let _runner = test_runner::GdbRunner::default();
/// # use std::error::Error;
/// # fn main() -> Result<(), Box<dyn Error>> {
/// #
/// use ctru::services::am::Am;
/// use ctru::services::fs::FsMediaType;
/// let app_manager = Am::new()?;
///
/// // Number of titles installed on the Nand storage.
/// let nand_count = app_manager.title_count(FsMediaType::Nand);
///
/// // Number of apps installed on the SD card storage
/// let sd_count = app_manager.title_count(FsMediaType::Sd);
/// #
/// # Ok(())
/// # }
/// ```
#[doc(alias = "AM_GetTitleCount")]
pub fn title_count(&self, mediatype: FsMediaType) -> crate::Result<u32> { pub fn title_count(&self, mediatype: FsMediaType) -> crate::Result<u32> {
unsafe { unsafe {
let mut count = 0; let mut count = 0;
@ -77,10 +112,34 @@ impl Am {
} }
} }
/// Returns the list of titles installed in a specific install location.
///
/// # Example
///
/// ```
/// # let _runner = test_runner::GdbRunner::default();
/// # use std::error::Error;
/// # fn main() -> Result<(), Box<dyn Error>> {
/// #
/// use ctru::services::am::Am;
/// use ctru::services::fs::FsMediaType;
/// let app_manager = Am::new()?;
///
/// // Number of apps installed on the SD card storage
/// let sd_titles = app_manager.title_list(FsMediaType::Sd)?;
///
/// // Unique product code identifier of the 5th installed title.
/// let product_code = sd_titles[4].product_code();
/// #
/// # Ok(())
/// # }
/// ```
#[doc(alias = "AM_GetTitleList")]
pub fn title_list(&self, mediatype: FsMediaType) -> crate::Result<Vec<Title>> { pub fn title_list(&self, mediatype: FsMediaType) -> crate::Result<Vec<Title>> {
let count = self.title_count(mediatype)?; let count = self.title_count(mediatype)?;
let mut buf = vec![0; count as usize]; let mut buf = vec![0; count as usize];
let mut read_amount = 0; let mut read_amount = 0;
unsafe { unsafe {
ResultCode(ctru_sys::AM_GetTitleList( ResultCode(ctru_sys::AM_GetTitleList(
&mut read_amount, &mut read_amount,
@ -89,11 +148,27 @@ impl Am {
buf.as_mut_ptr(), buf.as_mut_ptr(),
))?; ))?;
} }
Ok(buf
let mut info: Vec<ctru_sys::AM_TitleEntry> = Vec::with_capacity(count as _);
unsafe {
ResultCode(ctru_sys::AM_GetTitleInfo(
mediatype.into(),
count,
buf.as_mut_ptr(),
info.as_mut_ptr() as _,
))?;
info.set_len(count as _);
};
Ok(info
.into_iter() .into_iter()
.map(|id| Title { .map(|title| Title {
id, id: title.titleID,
mediatype, mediatype,
size: title.size,
version: title.version,
_am: PhantomData, _am: PhantomData,
}) })
.collect()) .collect())
@ -101,6 +176,7 @@ impl Am {
} }
impl Drop for Am { impl Drop for Am {
#[doc(alias = "amExit")]
fn drop(&mut self) { fn drop(&mut self) {
unsafe { ctru_sys::amExit() }; unsafe { ctru_sys::amExit() };
} }

63
ctru-rs/src/services/apt.rs

@ -1,8 +1,34 @@
//! Applet service.
//!
//! The APT service handles integration with other applications,
//! including high-level OS features such as Sleep mode, the Home Menu and application switching.
//!
//! It also handles running applets, small programs made available by the OS to streamline specific functionality.
//! Those are implemented in the [`applets`](crate::applets) module.
use crate::error::ResultCode; use crate::error::ResultCode;
/// Handle to the Applet service.
pub struct Apt(()); pub struct Apt(());
impl Apt { impl Apt {
/// Initialize a new service handle.
///
/// # Example
///
/// ```
/// # let _runner = test_runner::GdbRunner::default();
/// # use std::error::Error;
/// # fn main() -> Result<(), Box<dyn Error>> {
/// #
/// use ctru::services::apt::Apt;
///
/// let apt = Apt::new()?;
/// #
/// # Ok(())
/// # }
/// ```
#[doc(alias = "aptInit")]
pub fn new() -> crate::Result<Apt> { pub fn new() -> crate::Result<Apt> {
unsafe { unsafe {
ResultCode(ctru_sys::aptInit())?; ResultCode(ctru_sys::aptInit())?;
@ -10,10 +36,46 @@ impl Apt {
} }
} }
/// Returns `true` if the application is running in the foreground as normal.
///
/// # Notes
///
/// This function is called as such since it automatically handles all checks for Home Menu switching, Sleep mode and other events that could take away control from the application.
/// For this reason, its main use is as the condition of a while loop that controls the main logic for your program.
///
/// # Example
///
/// ```
/// # let _runner = test_runner::GdbRunner::default();
/// use std::error::Error;
/// use ctru::services::apt::Apt;
///
/// // In a simple `main` function, the structure should be the following.
/// fn main() -> Result<(), Box<dyn Error>> {
///
/// let apt = Apt::new()?;
///
/// while apt.main_loop() {
/// // Main program logic should be written here.
/// }
///
/// // Optional clean-ups after running the application should be written after the main loop.
/// #
/// # Ok(())
/// # }
/// ```
#[doc(alias = "aptMainLoop")]
pub fn main_loop(&self) -> bool { pub fn main_loop(&self) -> bool {
unsafe { ctru_sys::aptMainLoop() } unsafe { ctru_sys::aptMainLoop() }
} }
/// Set (in percentage) the amount of time to lend to the application thread spawned on the syscore (core #1).
///
/// # Notes
///
/// It is necessary to set a time limit before spawning threads on the syscore.
/// The percentage value must be withing 5% and 89%, though it is suggested to use lower values (around 30-45%) to avoid slowing down the OS processes.
#[doc(alias = "APT_SetAppCpuTimeLimit")]
pub fn set_app_cpu_time_limit(&mut self, percent: u32) -> crate::Result<()> { pub fn set_app_cpu_time_limit(&mut self, percent: u32) -> crate::Result<()> {
unsafe { unsafe {
ResultCode(ctru_sys::APT_SetAppCpuTimeLimit(percent))?; ResultCode(ctru_sys::APT_SetAppCpuTimeLimit(percent))?;
@ -23,6 +85,7 @@ impl Apt {
} }
impl Drop for Apt { impl Drop for Apt {
#[doc(alias = "aptExit")]
fn drop(&mut self) { fn drop(&mut self) {
unsafe { ctru_sys::aptExit() }; unsafe { ctru_sys::aptExit() };
} }

1105
ctru-rs/src/services/cam.rs

File diff suppressed because it is too large Load Diff

166
ctru-rs/src/services/cfgu.rs

@ -1,72 +1,125 @@
//! Configuration service //! System Configuration service.
//! //!
//! This module contains basic methods to retrieve and change configuration from the console. //! This module contains basic methods to retrieve the console's system configuration.
#![doc(alias = "configuration")]
use crate::error::ResultCode; use crate::error::ResultCode;
/// Console region.
#[doc(alias = "CFG_Region")]
#[derive(Copy, Clone, Debug, PartialEq, Eq)] #[derive(Copy, Clone, Debug, PartialEq, Eq)]
#[repr(u32)] #[repr(u32)]
pub enum Region { pub enum Region {
/// Japan.
Japan = ctru_sys::CFG_REGION_JPN, Japan = ctru_sys::CFG_REGION_JPN,
/// USA.
USA = ctru_sys::CFG_REGION_USA, USA = ctru_sys::CFG_REGION_USA,
/// Europe.
Europe = ctru_sys::CFG_REGION_EUR, Europe = ctru_sys::CFG_REGION_EUR,
/// Australia.
Australia = ctru_sys::CFG_REGION_AUS, Australia = ctru_sys::CFG_REGION_AUS,
/// China.
China = ctru_sys::CFG_REGION_CHN, China = ctru_sys::CFG_REGION_CHN,
/// Korea.
Korea = ctru_sys::CFG_REGION_KOR, Korea = ctru_sys::CFG_REGION_KOR,
/// Taiwan.
Taiwan = ctru_sys::CFG_REGION_TWN, Taiwan = ctru_sys::CFG_REGION_TWN,
} }
/// Language set for the console's OS.
#[doc(alias = "CFG_Language")]
#[derive(Copy, Clone, Debug, PartialEq, Eq)] #[derive(Copy, Clone, Debug, PartialEq, Eq)]
#[repr(u32)] #[repr(u32)]
pub enum Language { pub enum Language {
/// Japanese.
Japanese = ctru_sys::CFG_LANGUAGE_JP, Japanese = ctru_sys::CFG_LANGUAGE_JP,
/// English.
English = ctru_sys::CFG_LANGUAGE_EN, English = ctru_sys::CFG_LANGUAGE_EN,
/// French.
French = ctru_sys::CFG_LANGUAGE_FR, French = ctru_sys::CFG_LANGUAGE_FR,
/// German.
German = ctru_sys::CFG_LANGUAGE_DE, German = ctru_sys::CFG_LANGUAGE_DE,
/// Italian.
Italian = ctru_sys::CFG_LANGUAGE_IT, Italian = ctru_sys::CFG_LANGUAGE_IT,
/// Spanish.
Spanish = ctru_sys::CFG_LANGUAGE_ES, Spanish = ctru_sys::CFG_LANGUAGE_ES,
SimplifiedChinese = ctru_sys::CFG_LANGUAGE_ZH, /// Korean.
Korean = ctru_sys::CFG_LANGUAGE_KO, Korean = ctru_sys::CFG_LANGUAGE_KO,
/// Dutch.
Dutch = ctru_sys::CFG_LANGUAGE_NL, Dutch = ctru_sys::CFG_LANGUAGE_NL,
/// Portuguese.
Portuguese = ctru_sys::CFG_LANGUAGE_PT, Portuguese = ctru_sys::CFG_LANGUAGE_PT,
/// Russian.
Russian = ctru_sys::CFG_LANGUAGE_RU, Russian = ctru_sys::CFG_LANGUAGE_RU,
/// Simplified Chinese.
SimplifiedChinese = ctru_sys::CFG_LANGUAGE_ZH,
/// Traditional Chinese.
TraditionalChinese = ctru_sys::CFG_LANGUAGE_TW, TraditionalChinese = ctru_sys::CFG_LANGUAGE_TW,
} }
/// Specific model of the console.
#[doc(alias = "CFG_SystemModel")]
#[derive(Copy, Clone, Debug, PartialEq, Eq)] #[derive(Copy, Clone, Debug, PartialEq, Eq)]
#[repr(u32)] #[repr(u32)]
pub enum SystemModel { pub enum SystemModel {
/// Old Nintendo 3DS.
Old3DS = ctru_sys::CFG_MODEL_3DS, Old3DS = ctru_sys::CFG_MODEL_3DS,
/// Old Nintendo 3DS XL.
Old3DSXL = ctru_sys::CFG_MODEL_3DSXL, Old3DSXL = ctru_sys::CFG_MODEL_3DSXL,
/// New Nintendo 3DS.
New3DS = ctru_sys::CFG_MODEL_N3DS, New3DS = ctru_sys::CFG_MODEL_N3DS,
/// Old Nintendo 2DS.
Old2DS = ctru_sys::CFG_MODEL_2DS, Old2DS = ctru_sys::CFG_MODEL_2DS,
/// New Nintendo 3DS XL.
New3DSXL = ctru_sys::CFG_MODEL_N3DSXL, New3DSXL = ctru_sys::CFG_MODEL_N3DSXL,
/// New Nintendo 2DS XL.
New2DSXL = ctru_sys::CFG_MODEL_N2DSXL, New2DSXL = ctru_sys::CFG_MODEL_N2DSXL,
} }
/// Represents the configuration service. No actions can be performed /// Handle to the System Configuration service.
/// until an instance of this struct is created.
///
/// The service exits when all instances of this struct go out of scope.
pub struct Cfgu(()); pub struct Cfgu(());
impl Cfgu { impl Cfgu {
/// Initializes the CFGU service. /// Initialize a new service handle.
/// ///
/// # Errors /// # Example
/// ///
/// This function will return Err if there was an error initializing the /// ```
/// CFGU service. /// # let _runner = test_runner::GdbRunner::default();
/// # use std::error::Error;
/// # fn main() -> Result<(), Box<dyn Error>> {
/// #
/// use ctru::services::cfgu::Cfgu;
/// ///
/// ctrulib services are reference counted, so this function may be called /// let cfgu = Cfgu::new()?;
/// as many times as desired and the service will not exit until all /// #
/// instances of Cfgu drop out of scope. /// # Ok(())
/// # }
/// ```
#[doc(alias = "cfguInit")]
pub fn new() -> crate::Result<Cfgu> { pub fn new() -> crate::Result<Cfgu> {
ResultCode(unsafe { ctru_sys::cfguInit() })?; ResultCode(unsafe { ctru_sys::cfguInit() })?;
Ok(Cfgu(())) Ok(Cfgu(()))
} }
/// Gets system region from secure info /// Returns the console's region from the system's secure info.
///
/// # Example
///
/// ```
/// # let _runner = test_runner::GdbRunner::default();
/// # use std::error::Error;
/// # fn main() -> Result<(), Box<dyn Error>> {
/// #
/// use ctru::services::cfgu::Cfgu;
/// let cfgu = Cfgu::new()?;
///
/// let region = cfgu.region()?;
/// #
/// # Ok(())
/// # }
/// ```
#[doc(alias = "CFGU_SecureInfoGetRegion")]
pub fn region(&self) -> crate::Result<Region> { pub fn region(&self) -> crate::Result<Region> {
let mut region: u8 = 0; let mut region: u8 = 0;
@ -74,7 +127,24 @@ impl Cfgu {
Ok(Region::try_from(region).unwrap()) Ok(Region::try_from(region).unwrap())
} }
/// Gets system's model /// Returns the console's model.
///
/// # Example
///
/// ```
/// # let _runner = test_runner::GdbRunner::default();
/// # use std::error::Error;
/// # fn main() -> Result<(), Box<dyn Error>> {
/// #
/// use ctru::services::cfgu::Cfgu;
/// let cfgu = Cfgu::new()?;
///
/// let model = cfgu.model()?;
/// #
/// # Ok(())
/// # }
/// ```
#[doc(alias = "CFGU_GetSystemModel")]
pub fn model(&self) -> crate::Result<SystemModel> { pub fn model(&self) -> crate::Result<SystemModel> {
let mut model: u8 = 0; let mut model: u8 = 0;
@ -82,7 +152,24 @@ impl Cfgu {
Ok(SystemModel::try_from(model).unwrap()) Ok(SystemModel::try_from(model).unwrap())
} }
/// Gets system's language /// Returns the system language set for the console.
///
/// # Example
///
/// ```
/// # let _runner = test_runner::GdbRunner::default();
/// # use std::error::Error;
/// # fn main() -> Result<(), Box<dyn Error>> {
/// #
/// use ctru::services::cfgu::Cfgu;
/// let cfgu = Cfgu::new()?;
///
/// let language = cfgu.language()?;
/// #
/// # Ok(())
/// # }
/// ```
#[doc(alias = "CFGU_GetSystemLanguage")]
pub fn language(&self) -> crate::Result<Language> { pub fn language(&self) -> crate::Result<Language> {
let mut language: u8 = 0; let mut language: u8 = 0;
@ -90,7 +177,26 @@ impl Cfgu {
Ok(Language::try_from(language).unwrap()) Ok(Language::try_from(language).unwrap())
} }
/// Checks if NFC is supported by the console /// Check if NFC is supported by the console.
///
/// # Example
///
/// ```
/// # let _runner = test_runner::GdbRunner::default();
/// # use std::error::Error;
/// # fn main() -> Result<(), Box<dyn Error>> {
/// #
/// use ctru::services::cfgu::Cfgu;
/// let cfgu = Cfgu::new()?;
///
/// if cfgu.is_nfc_supported()? {
/// println!("NFC is available!");
/// }
/// #
/// # Ok(())
/// # }
/// ```
#[doc(alias = "CFGU_IsNFCSupported")]
pub fn is_nfc_supported(&self) -> crate::Result<bool> { pub fn is_nfc_supported(&self) -> crate::Result<bool> {
let mut supported: bool = false; let mut supported: bool = false;
@ -98,7 +204,28 @@ impl Cfgu {
Ok(supported) Ok(supported)
} }
/// Check if the console is from the 2DS family (2DS, New2DS, New2DSXL) /// Check if the console is from the 2DS family ([`Old2DS`](SystemModel::Old2DS), [`New2DSXL`](SystemModel::New2DSXL)).
///
/// Useful to avoid stereoscopic 3D rendering when working with 2DS consoles.
///
/// # Example
///
/// ```
/// # let _runner = test_runner::GdbRunner::default();
/// # use std::error::Error;
/// # fn main() -> Result<(), Box<dyn Error>> {
/// #
/// use ctru::services::cfgu::Cfgu;
/// let cfgu = Cfgu::new()?;
///
/// if cfgu.is_2ds_family()? {
/// println!("Stereoscopic 3D is not supported.");
/// }
/// #
/// # Ok(())
/// # }
/// ```
#[doc(alias = "CFGU_GetModelNintendo2DS")]
pub fn is_2ds_family(&self) -> crate::Result<bool> { pub fn is_2ds_family(&self) -> crate::Result<bool> {
let mut is_2ds_family: u8 = 0; let mut is_2ds_family: u8 = 0;
@ -108,6 +235,7 @@ impl Cfgu {
} }
impl Drop for Cfgu { impl Drop for Cfgu {
#[doc(alias = "cfguExit")]
fn drop(&mut self) { fn drop(&mut self) {
unsafe { unsafe {
ctru_sys::cfguExit(); ctru_sys::cfguExit();

200
ctru-rs/src/services/fs.rs

@ -1,7 +1,9 @@
//! Filesystem service //! FileSystem service.
//! //!
//! This module contains basic methods to manipulate the contents of the 3DS's filesystem. //! This module contains basic methods to manipulate the contents of the 3DS's filesystem.
//! Only the SD card is currently supported. You should prefer using `std::fs`. //! Only the SD card is currently supported. You should prefer using `std::fs`.
// TODO: Refactor service to accomodate for various changes (such as SMDH support). Properly document the public API.
#![doc(alias = "filesystem")]
use bitflags::bitflags; use bitflags::bitflags;
use std::ffi::OsString; use std::ffi::OsString;
@ -17,70 +19,108 @@ use std::sync::Arc;
use widestring::{WideCStr, WideCString}; use widestring::{WideCStr, WideCString};
bitflags! { bitflags! {
#[derive(Default)] #[derive(Default, PartialEq, Eq, PartialOrd, Ord, Hash, Debug, Clone, Copy)]
struct FsOpen: u32 { struct FsOpen: u32 {
const FS_OPEN_READ = 1; const FS_OPEN_READ = ctru_sys::FS_OPEN_READ;
const FS_OPEN_WRITE = 2; const FS_OPEN_WRITE = ctru_sys::FS_OPEN_WRITE;
const FS_OPEN_CREATE = 4; const FS_OPEN_CREATE = ctru_sys::FS_OPEN_CREATE;
} }
#[derive(Default)] #[derive(Default, PartialEq, Eq, PartialOrd, Ord, Hash, Debug, Clone, Copy)]
struct FsWrite: u32 { struct FsWrite: u32 {
const FS_WRITE_FLUSH = 1; const FS_WRITE_FLUSH = ctru_sys::FS_WRITE_FLUSH;
const FS_WRITE_UPDATE_TIME = 256; const FS_WRITE_UPDATE_TIME = ctru_sys::FS_WRITE_UPDATE_TIME;
} }
#[derive(Default)] #[derive(Default, PartialEq, Eq, PartialOrd, Ord, Hash, Debug, Clone, Copy)]
struct FsAttribute: u32 { struct FsAttribute: u32 {
const FS_ATTRIBUTE_DIRECTORY = 1; const FS_ATTRIBUTE_DIRECTORY = ctru_sys::FS_ATTRIBUTE_DIRECTORY;
const FS_ATTRIBUTE_HIDDEN = 256; const FS_ATTRIBUTE_HIDDEN = ctru_sys::FS_ATTRIBUTE_HIDDEN;
const FS_ATTRIBUTE_ARCHIVE = 65536; const FS_ATTRIBUTE_ARCHIVE = ctru_sys::FS_ATTRIBUTE_ARCHIVE;
const FS_ATTRIBUTE_READ_ONLY = 16777216; const FS_ATTRIBUTE_READ_ONLY = ctru_sys::FS_ATTRIBUTE_READ_ONLY;
} }
} }
/// Media type used for storage.
#[doc(alias = "FS_MediaType")]
#[derive(Copy, Clone, Debug, PartialEq, Eq)] #[derive(Copy, Clone, Debug, PartialEq, Eq)]
#[repr(u32)] #[repr(u32)]
pub enum FsMediaType { pub enum FsMediaType {
/// Internal NAND memory.
Nand = ctru_sys::MEDIATYPE_NAND, Nand = ctru_sys::MEDIATYPE_NAND,
/// External SD card.
Sd = ctru_sys::MEDIATYPE_SD, Sd = ctru_sys::MEDIATYPE_SD,
/// Game Cartridge.
GameCard = ctru_sys::MEDIATYPE_GAME_CARD, GameCard = ctru_sys::MEDIATYPE_GAME_CARD,
} }
/// Kind of file path.
#[doc(alias = "FS_PathType")]
#[derive(Copy, Clone, Debug, PartialEq, Eq)] #[derive(Copy, Clone, Debug, PartialEq, Eq)]
#[repr(u32)] #[repr(u32)]
pub enum PathType { pub enum PathType {
/// Invalid path.
Invalid = ctru_sys::PATH_INVALID, Invalid = ctru_sys::PATH_INVALID,
/// Empty path.
Empty = ctru_sys::PATH_EMPTY, Empty = ctru_sys::PATH_EMPTY,
/// Binary path.
///
/// Its meaning differs depending on the Archive it is used on.
Binary = ctru_sys::PATH_BINARY, Binary = ctru_sys::PATH_BINARY,
/// ASCII path.
ASCII = ctru_sys::PATH_ASCII, ASCII = ctru_sys::PATH_ASCII,
/// UTF-16 path.
UTF16 = ctru_sys::PATH_UTF16, UTF16 = ctru_sys::PATH_UTF16,
} }
/// Index of the various usable data archives.
#[doc(alias = "FS_ArchiveID")]
#[derive(Copy, Clone, Debug, PartialEq, Eq)] #[derive(Copy, Clone, Debug, PartialEq, Eq)]
#[repr(u32)] #[repr(u32)]
pub enum ArchiveID { pub enum ArchiveID {
/// Read-Only Memory File System.
RomFS = ctru_sys::ARCHIVE_ROMFS, RomFS = ctru_sys::ARCHIVE_ROMFS,
/// Game save data.
Savedata = ctru_sys::ARCHIVE_SAVEDATA, Savedata = ctru_sys::ARCHIVE_SAVEDATA,
/// Game ext data.
Extdata = ctru_sys::ARCHIVE_EXTDATA, Extdata = ctru_sys::ARCHIVE_EXTDATA,
/// Shared ext data.
SharedExtdata = ctru_sys::ARCHIVE_SHARED_EXTDATA, SharedExtdata = ctru_sys::ARCHIVE_SHARED_EXTDATA,
/// System save data.
SystemSavedata = ctru_sys::ARCHIVE_SYSTEM_SAVEDATA, SystemSavedata = ctru_sys::ARCHIVE_SYSTEM_SAVEDATA,
/// SD card.
Sdmc = ctru_sys::ARCHIVE_SDMC, Sdmc = ctru_sys::ARCHIVE_SDMC,
/// SD card (write-only).
SdmcWriteOnly = ctru_sys::ARCHIVE_SDMC_WRITE_ONLY, SdmcWriteOnly = ctru_sys::ARCHIVE_SDMC_WRITE_ONLY,
/// BOSS ext data.
BossExtdata = ctru_sys::ARCHIVE_BOSS_EXTDATA, BossExtdata = ctru_sys::ARCHIVE_BOSS_EXTDATA,
/// Card SPI File System.
CardSpiFS = ctru_sys::ARCHIVE_CARD_SPIFS, CardSpiFS = ctru_sys::ARCHIVE_CARD_SPIFS,
/// Game ext data and BOSS data.
ExtDataAndBossExtdata = ctru_sys::ARCHIVE_EXTDATA_AND_BOSS_EXTDATA, ExtDataAndBossExtdata = ctru_sys::ARCHIVE_EXTDATA_AND_BOSS_EXTDATA,
/// System save data.
SystemSaveData2 = ctru_sys::ARCHIVE_SYSTEM_SAVEDATA2, SystemSaveData2 = ctru_sys::ARCHIVE_SYSTEM_SAVEDATA2,
/// Internal NAND (read-write).
NandRW = ctru_sys::ARCHIVE_NAND_RW, NandRW = ctru_sys::ARCHIVE_NAND_RW,
/// Internal NAND (read-only).
NandRO = ctru_sys::ARCHIVE_NAND_RO, NandRO = ctru_sys::ARCHIVE_NAND_RO,
/// Internal NAND (read-only write access).
NandROWriteAccess = ctru_sys::ARCHIVE_NAND_RO_WRITE_ACCESS, NandROWriteAccess = ctru_sys::ARCHIVE_NAND_RO_WRITE_ACCESS,
/// User save data and ExeFS/RomFS.
SaveDataAndContent = ctru_sys::ARCHIVE_SAVEDATA_AND_CONTENT, SaveDataAndContent = ctru_sys::ARCHIVE_SAVEDATA_AND_CONTENT,
/// User save data and ExeFS/RomFS (only ExeFS for fs:LDR).
SaveDataAndContent2 = ctru_sys::ARCHIVE_SAVEDATA_AND_CONTENT2, SaveDataAndContent2 = ctru_sys::ARCHIVE_SAVEDATA_AND_CONTENT2,
/// NAND CTR File System.
NandCtrFS = ctru_sys::ARCHIVE_NAND_CTR_FS, NandCtrFS = ctru_sys::ARCHIVE_NAND_CTR_FS,
/// TWL photo.
TwlPhoto = ctru_sys::ARCHIVE_TWL_PHOTO, TwlPhoto = ctru_sys::ARCHIVE_TWL_PHOTO,
/// NAND TWL File System.
NandTwlFS = ctru_sys::ARCHIVE_NAND_TWL_FS, NandTwlFS = ctru_sys::ARCHIVE_NAND_TWL_FS,
/// Game card save data.
GameCardSavedata = ctru_sys::ARCHIVE_GAMECARD_SAVEDATA, GameCardSavedata = ctru_sys::ARCHIVE_GAMECARD_SAVEDATA,
/// User save data.
UserSavedata = ctru_sys::ARCHIVE_USER_SAVEDATA, UserSavedata = ctru_sys::ARCHIVE_USER_SAVEDATA,
/// Demo save data.
DemoSavedata = ctru_sys::ARCHIVE_DEMO_SAVEDATA, DemoSavedata = ctru_sys::ARCHIVE_DEMO_SAVEDATA,
} }
@ -96,7 +136,8 @@ pub struct Fs(());
/// ///
/// # Examples /// # Examples
/// ///
/// ```no_run /// ```
/// # let _runner = test_runner::GdbRunner::default();
/// use ctru::services::fs::Fs; /// use ctru::services::fs::Fs;
/// ///
/// let mut fs = Fs::new().unwrap(); /// let mut fs = Fs::new().unwrap();
@ -118,12 +159,14 @@ pub struct Archive {
/// ///
/// Create a new file and write bytes to it: /// Create a new file and write bytes to it:
/// ///
/// ```no_run /// ```
/// # let _runner = test_runner::GdbRunner::default();
/// # use std::error::Error; /// # use std::error::Error;
/// # fn main() -> Result<(), Box<dyn Error>> { /// # fn main() -> Result<(), Box<dyn Error>> {
/// # /// #
/// use std::io::prelude::*; /// use std::io::prelude::*;
/// use ctru::services::fs::{Fs, File}; ///
/// use ctru::services::fs::{File, Fs};
/// ///
/// let mut fs = Fs::new()?; /// let mut fs = Fs::new()?;
/// let mut sdmc = fs.sdmc()?; /// let mut sdmc = fs.sdmc()?;
@ -134,12 +177,14 @@ pub struct Archive {
/// ///
/// Read the contents of a file into a `String`:: /// Read the contents of a file into a `String`::
/// ///
/// ```no_run /// ```
/// # let _runner = test_runner::GdbRunner::default();
/// # use std::error::Error; /// # use std::error::Error;
/// # fn main() -> Result<(), Box<dyn Error>> { /// # fn main() -> Result<(), Box<dyn Error>> {
/// # /// #
/// use std::io::prelude::*; /// use std::io::prelude::*;
/// use ctru::services::fs::{Fs, File}; ///
/// use ctru::services::fs::{File, Fs};
/// ///
/// let mut fs = Fs::new()?; /// let mut fs = Fs::new()?;
/// let mut sdmc = fs.sdmc()?; /// let mut sdmc = fs.sdmc()?;
@ -156,13 +201,15 @@ pub struct Archive {
/// It can be more efficient to read the contents of a file with a buffered /// It can be more efficient to read the contents of a file with a buffered
/// `Read`er. This can be accomplished with `BufReader<R>`: /// `Read`er. This can be accomplished with `BufReader<R>`:
/// ///
/// ```no_run /// ```
/// # let _runner = test_runner::GdbRunner::default();
/// # use std::error::Error; /// # use std::error::Error;
/// # fn main() -> Result<(), Box<dyn Error>> { /// # fn main() -> Result<(), Box<dyn Error>> {
/// # /// #
/// use std::io::BufReader;
/// use std::io::prelude::*; /// use std::io::prelude::*;
/// use ctru::services::fs::{Fs, File}; /// use std::io::BufReader;
///
/// use ctru::services::fs::{File, Fs};
/// ///
/// let mut fs = Fs::new()?; /// let mut fs = Fs::new()?;
/// let mut sdmc = fs.sdmc()?; /// let mut sdmc = fs.sdmc()?;
@ -183,56 +230,49 @@ pub struct File {
/// Metadata information about a file. /// Metadata information about a file.
/// ///
/// This structure is returned from the [`metadata`] function and /// This structure is returned from the [`File::metadata`] function and
/// represents known metadata about a file. /// represents known metadata about a file.
///
/// [`metadata`]: fn.metadata.html
pub struct Metadata { pub struct Metadata {
attributes: u32, attributes: u32,
size: u64, size: u64,
} }
/// Options and flags which can be used to configure how a [`File`] is opened. /// Options and flags which can be used to configure how a [`File`] is opened.
/// This builder exposes the ability to configure how a `File` is opened /// This builder exposes the ability to configure how a [`File`] is opened
/// and what operations are permitted on the open file. The [`File::open`] /// and what operations are permitted on the open file. The [`File::open`]
/// and [`File::create`] methods are aliases for commonly used options /// and [`File::create`] methods are aliases for commonly used options
/// using this builder. /// using this builder.
/// ///
/// [`File`]: struct.File.html /// Generally speaking, when using [`OpenOptions`], you'll first call [`OpenOptions::new`],
/// [`File::open`]: struct.File.html#method.open /// then chain calls to methods to set each option, then call [`OpenOptions::open`],
/// [`File::create`]: struct.File.html#method.create
///
/// Generally speaking, when using `OpenOptions`, you'll first call [`new()`],
/// then chain calls to methods to set each option, then call [`open()`],
/// passing the path of the file you're trying to open. /// passing the path of the file you're trying to open.
/// ///
/// It is required to also pass a reference to the [`Archive`] that the /// It is required to also pass a reference to the [`Archive`] that the
/// file lives in. /// file lives in.
/// ///
/// [`new()`]: struct.OpenOptions.html#method.new
/// [`open()`]: struct.OpenOptions.html#method.open
/// [`Archive`]: struct.Archive.html
///
/// # Examples /// # Examples
/// ///
/// Opening a file to read: /// Opening a file to read:
/// ///
/// ```no_run /// ```
/// # let _runner = test_runner::GdbRunner::default();
/// use ctru::services::fs::{Fs, OpenOptions}; /// use ctru::services::fs::{Fs, OpenOptions};
/// ///
/// let mut fs = Fs::new().unwrap(); /// let mut fs = Fs::new().unwrap();
/// let mut sdmc_archive = fs.sdmc().unwrap(); /// let mut sdmc_archive = fs.sdmc().unwrap();
/// let file = OpenOptions::new() /// let result = OpenOptions::new()
/// .read(true) /// .read(true)
/// .archive(&sdmc_archive) /// .archive(&sdmc_archive)
/// .open("foo.txt") /// .open("foo.txt");
/// .unwrap(); ///
/// assert!(result.is_err());
/// ``` /// ```
/// ///
/// Opening a file for both reading and writing, as well as creating it if it /// Opening a file for both reading and writing, as well as creating it if it
/// doesn't exist: /// doesn't exist:
/// ///
/// ```no_run /// ```
/// # let _runner = test_runner::GdbRunner::default();
/// use ctru::services::fs::{Fs, OpenOptions}; /// use ctru::services::fs::{Fs, OpenOptions};
/// ///
/// let mut fs = Fs::new().unwrap(); /// let mut fs = Fs::new().unwrap();
@ -242,7 +282,7 @@ pub struct Metadata {
/// .write(true) /// .write(true)
/// .create(true) /// .create(true)
/// .archive(&sdmc_archive) /// .archive(&sdmc_archive)
/// .open("foo.txt") /// .open("/foo.txt")
/// .unwrap(); /// .unwrap();
/// ``` /// ```
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
@ -257,14 +297,11 @@ pub struct OpenOptions {
/// Iterator over the entries in a directory. /// Iterator over the entries in a directory.
/// ///
/// This iterator is returned from the [`read_dir`] function of this module and /// This iterator is returned from the [`read_dir`] function and
/// will yield instances of `Result<DirEntry, i32>`. Through a [`DirEntry`] /// will yield instances of [`Result<DirEntry, i32>`]. Through a [`DirEntry`]
/// information like the entry's path and possibly other metadata can be /// information like the entry's path and possibly other metadata can be
/// learned. /// learned.
/// ///
/// [`read_dir`]: fn.read_dir.html
/// [`DirEntry`]: struct.DirEntry.html
///
/// # Errors /// # Errors
/// ///
/// This Result will return Err if there's some sort of intermittent IO error /// This Result will return Err if there's some sort of intermittent IO error
@ -277,8 +314,6 @@ pub struct ReadDir<'a> {
/// Entries returned by the [`ReadDir`] iterator. /// Entries returned by the [`ReadDir`] iterator.
/// ///
/// [`ReadDir`]: struct.ReadDir.html
///
/// An instance of `DirEntry` represents an entry inside of a directory on the /// An instance of `DirEntry` represents an entry inside of a directory on the
/// filesystem. Each entry can be inspected via methods to learn about the full /// filesystem. Each entry can be inspected via methods to learn about the full
/// path or possibly other metadata. /// path or possibly other metadata.
@ -297,7 +332,7 @@ unsafe impl Send for Dir {}
unsafe impl Sync for Dir {} unsafe impl Sync for Dir {}
impl Fs { impl Fs {
/// Initializes the FS service. /// Initialize a new service handle.
/// ///
/// # Errors /// # Errors
/// ///
@ -338,8 +373,6 @@ impl Fs {
impl Archive { impl Archive {
/// Retrieves an Archive's [`ArchiveID`] /// Retrieves an Archive's [`ArchiveID`]
///
/// [`ArchiveID`]: enum.ArchiveID.html
pub fn id(&self) -> ArchiveID { pub fn id(&self) -> ArchiveID {
self.id self.id
} }
@ -355,16 +388,16 @@ impl File {
/// This function will return an error if `path` does not already exit. /// This function will return an error if `path` does not already exit.
/// Other errors may also be returned accoridng to [`OpenOptions::open`] /// Other errors may also be returned accoridng to [`OpenOptions::open`]
/// ///
/// [`OpenOptions::open`]: struct.OpenOptions.html#method.open
///
/// # Examples /// # Examples
/// ///
/// ```no_run /// ```
/// use ctru::services::fs::{Fs, File}; /// # let _runner = test_runner::GdbRunner::default();
/// use ctru::services::fs::{File, Fs};
/// ///
/// let mut fs = Fs::new().unwrap(); /// let mut fs = Fs::new().unwrap();
/// let mut sdmc_archive = fs.sdmc().unwrap(); /// let mut sdmc_archive = fs.sdmc().unwrap();
/// let mut f = File::open(&sdmc_archive, "/foo.txt").unwrap(); /// // Non-existent file:
/// assert!(File::open(&sdmc_archive, "/foo.txt").is_err());
/// ``` /// ```
pub fn open<P: AsRef<Path>>(arch: &Archive, path: P) -> IoResult<File> { pub fn open<P: AsRef<Path>>(arch: &Archive, path: P) -> IoResult<File> {
OpenOptions::new() OpenOptions::new()
@ -382,20 +415,19 @@ impl File {
/// # Errors /// # Errors
/// ///
/// This function will return an error if `path` does not already exit. /// This function will return an error if `path` does not already exit.
/// Other errors may also be returned accoridng to [`OpenOptions::create`] /// Other errors may also be returned accoridng to [`OpenOptions::create`].
///
/// [`OpenOptions::create`]: struct.OpenOptions.html#method.create
/// ///
/// # Examples /// # Examples
/// ///
/// ```no_run /// ```
/// use ctru::services::fs::{Fs, File}; /// # let _runner = test_runner::GdbRunner::default();
/// use ctru::services::fs::{File, Fs};
/// ///
/// let mut fs = Fs::new().unwrap(); /// let mut fs = Fs::new().unwrap();
/// let mut sdmc_archive = fs.sdmc().unwrap(); /// let mut sdmc_archive = fs.sdmc().unwrap();
/// let mut f = File::create(&mut sdmc_archive, "/foo.txt").unwrap(); /// let mut f = File::create(&mut sdmc_archive, "/foo.txt").unwrap();
/// ``` /// ```
pub fn create<P: AsRef<Path>>(arch: &mut Archive, path: P) -> IoResult<File> { pub fn create<P: AsRef<Path>>(arch: &Archive, path: P) -> IoResult<File> {
OpenOptions::new() OpenOptions::new()
.write(true) .write(true)
.create(true) .create(true)
@ -412,6 +444,7 @@ impl File {
/// # Errors /// # Errors
/// ///
/// This function will return an error if the file is not opened for writing. /// This function will return an error if the file is not opened for writing.
#[doc(alias = "FSFILE_SetSize")]
pub fn set_len(&mut self, size: u64) -> IoResult<()> { pub fn set_len(&mut self, size: u64) -> IoResult<()> {
unsafe { unsafe {
let r = ctru_sys::FSFILE_SetSize(self.handle, size); let r = ctru_sys::FSFILE_SetSize(self.handle, size);
@ -447,6 +480,7 @@ impl File {
} }
} }
#[doc(alias = "FSFILE_Read")]
fn read(&mut self, buf: &mut [u8]) -> IoResult<usize> { fn read(&mut self, buf: &mut [u8]) -> IoResult<usize> {
unsafe { unsafe {
let mut n_read = 0; let mut n_read = 0;
@ -470,6 +504,7 @@ impl File {
unsafe { read_to_end_uninitialized(self, buf) } unsafe { read_to_end_uninitialized(self, buf) }
} }
#[doc(alias = "FSFILE_Write")]
fn write(&mut self, buf: &[u8]) -> IoResult<usize> { fn write(&mut self, buf: &[u8]) -> IoResult<usize> {
unsafe { unsafe {
let mut n_written = 0; let mut n_written = 0;
@ -521,7 +556,7 @@ impl OpenOptions {
Self::default() Self::default()
} }
/// Sets the option for read access. /// Set the option for read access.
/// ///
/// This option, when true, will indicate that the file should be /// This option, when true, will indicate that the file should be
/// `read`-able if opened. /// `read`-able if opened.
@ -530,7 +565,7 @@ impl OpenOptions {
self self
} }
/// Sets the option for write access. /// Set the option for write access.
/// ///
/// This option, when true, will indicate that the file should be /// This option, when true, will indicate that the file should be
/// `write`-able if opened. /// `write`-able if opened.
@ -542,7 +577,7 @@ impl OpenOptions {
self self
} }
/// Sets the option for the append mode. /// Set the option for the append mode.
/// ///
/// This option, when true, means that writes will append to a file instead /// This option, when true, means that writes will append to a file instead
/// of overwriting previous contents. Note that setting .write(true).append(true) /// of overwriting previous contents. Note that setting .write(true).append(true)
@ -554,7 +589,7 @@ impl OpenOptions {
self self
} }
/// Sets the option for truncating a previous file. /// Set the option for truncating a previous file.
/// ///
/// If a file is successfully opened with this option set it will truncate /// If a file is successfully opened with this option set it will truncate
/// the file to 0 length if it already exists. /// the file to 0 length if it already exists.
@ -565,7 +600,7 @@ impl OpenOptions {
self self
} }
/// Sets the option for creating a new file. /// Set the option for creating a new file.
/// ///
/// This option indicates whether a new file will be created /// This option indicates whether a new file will be created
/// if the file does not yet already /// if the file does not yet already
@ -577,7 +612,7 @@ impl OpenOptions {
self self
} }
/// Sets which archive the file is to be opened in. /// Set which archive the file is to be opened in.
/// ///
/// Failing to pass in an archive will result in the file failing to open. /// Failing to pass in an archive will result in the file failing to open.
pub fn archive(&mut self, archive: &Archive) -> &mut OpenOptions { pub fn archive(&mut self, archive: &Archive) -> &mut OpenOptions {
@ -597,8 +632,7 @@ impl OpenOptions {
/// to the `archive` method. /// to the `archive` method.
/// * Filesystem-level errors (full disk, etc). /// * Filesystem-level errors (full disk, etc).
/// * Invalid combinations of open options. /// * Invalid combinations of open options.
/// #[doc(alias = "FSUSER_OpenFile")]
/// [`Archive`]: struct.Archive.html
pub fn open<P: AsRef<Path>>(&mut self, path: P) -> IoResult<File> { pub fn open<P: AsRef<Path>>(&mut self, path: P) -> IoResult<File> {
self._open(path.as_ref(), self.open_flags()) self._open(path.as_ref(), self.open_flags())
} }
@ -717,7 +751,8 @@ impl<'a> DirEntry<'a> {
/// but is not limited to just these cases: /// but is not limited to just these cases:
/// ///
/// * User lacks permissions to create directory at `path` /// * User lacks permissions to create directory at `path`
pub fn create_dir<P: AsRef<Path>>(arch: &mut Archive, path: P) -> IoResult<()> { #[doc(alias = "FSUSER_CreateDirectory")]
pub fn create_dir<P: AsRef<Path>>(arch: &Archive, path: P) -> IoResult<()> {
unsafe { unsafe {
let path = to_utf16(path.as_ref()); let path = to_utf16(path.as_ref());
let fs_path = ctru_sys::fsMakePath(PathType::UTF16.into(), path.as_ptr() as _); let fs_path = ctru_sys::fsMakePath(PathType::UTF16.into(), path.as_ptr() as _);
@ -743,7 +778,8 @@ pub fn create_dir<P: AsRef<Path>>(arch: &mut Archive, path: P) -> IoResult<()> {
/// ///
/// * If any directory in the path specified by `path` does not already exist /// * If any directory in the path specified by `path` does not already exist
/// and it could not be created otherwise. /// and it could not be created otherwise.
pub fn create_dir_all<P: AsRef<Path>>(arch: &mut Archive, path: P) -> IoResult<()> { #[doc(alias = "FSUSER_CreateDirectory")]
pub fn create_dir_all<P: AsRef<Path>>(arch: &Archive, path: P) -> IoResult<()> {
let path = path.as_ref(); let path = path.as_ref();
let mut dir = PathBuf::new(); let mut dir = PathBuf::new();
let mut result = Ok(()); let mut result = Ok(());
@ -779,7 +815,8 @@ pub fn metadata<P: AsRef<Path>>(arch: &Archive, path: P) -> IoResult<Metadata> {
/// ///
/// * The user lacks permissions to remove the directory at the provided path. /// * The user lacks permissions to remove the directory at the provided path.
/// * The directory isn't empty. /// * The directory isn't empty.
pub fn remove_dir<P: AsRef<Path>>(arch: &mut Archive, path: P) -> IoResult<()> { #[doc(alias = "FSUSER_DeleteDirectory")]
pub fn remove_dir<P: AsRef<Path>>(arch: &Archive, path: P) -> IoResult<()> {
unsafe { unsafe {
let path = to_utf16(path.as_ref()); let path = to_utf16(path.as_ref());
let fs_path = ctru_sys::fsMakePath(PathType::UTF16.into(), path.as_ptr() as _); let fs_path = ctru_sys::fsMakePath(PathType::UTF16.into(), path.as_ptr() as _);
@ -797,7 +834,8 @@ pub fn remove_dir<P: AsRef<Path>>(arch: &mut Archive, path: P) -> IoResult<()> {
/// # Errors /// # Errors
/// ///
/// see `file::remove_file` and `fs::remove_dir` /// see `file::remove_file` and `fs::remove_dir`
pub fn remove_dir_all<P: AsRef<Path>>(arch: &mut Archive, path: P) -> IoResult<()> { #[doc(alias = "FSUSER_DeleteDirectoryRecursively")]
pub fn remove_dir_all<P: AsRef<Path>>(arch: &Archive, path: P) -> IoResult<()> {
unsafe { unsafe {
let path = to_utf16(path.as_ref()); let path = to_utf16(path.as_ref());
let fs_path = ctru_sys::fsMakePath(PathType::UTF16.into(), path.as_ptr() as _); let fs_path = ctru_sys::fsMakePath(PathType::UTF16.into(), path.as_ptr() as _);
@ -821,6 +859,7 @@ pub fn remove_dir_all<P: AsRef<Path>>(arch: &mut Archive, path: P) -> IoResult<(
/// * The provided path doesn't exist. /// * The provided path doesn't exist.
/// * The process lacks permissions to view the contents. /// * The process lacks permissions to view the contents.
/// * The path points at a non-directory file. /// * The path points at a non-directory file.
#[doc(alias = "FSUSER_OpenDirectory")]
pub fn read_dir<P: AsRef<Path>>(arch: &Archive, path: P) -> IoResult<ReadDir> { pub fn read_dir<P: AsRef<Path>>(arch: &Archive, path: P) -> IoResult<ReadDir> {
unsafe { unsafe {
let mut handle = 0; let mut handle = 0;
@ -849,7 +888,8 @@ pub fn read_dir<P: AsRef<Path>>(arch: &Archive, path: P) -> IoResult<ReadDir> {
/// ///
/// * path points to a directory. /// * path points to a directory.
/// * The user lacks permissions to remove the file. /// * The user lacks permissions to remove the file.
pub fn remove_file<P: AsRef<Path>>(arch: &mut Archive, path: P) -> IoResult<()> { #[doc(alias = "FSUSER_DeleteFile")]
pub fn remove_file<P: AsRef<Path>>(arch: &Archive, path: P) -> IoResult<()> {
unsafe { unsafe {
let path = to_utf16(path.as_ref()); let path = to_utf16(path.as_ref());
let fs_path = ctru_sys::fsMakePath(PathType::UTF16.into(), path.as_ptr() as _); let fs_path = ctru_sys::fsMakePath(PathType::UTF16.into(), path.as_ptr() as _);
@ -872,7 +912,8 @@ pub fn remove_file<P: AsRef<Path>>(arch: &mut Archive, path: P) -> IoResult<()>
/// ///
/// * from does not exist. /// * from does not exist.
/// * The user lacks permissions to view contents. /// * The user lacks permissions to view contents.
pub fn rename<P, Q>(arch: &mut Archive, from: P, to: Q) -> IoResult<()> #[doc(alias = "FSUSER_RenameFile", alias = "FSUSER_RenameDirectory")]
pub fn rename<P, Q>(arch: &Archive, from: P, to: Q) -> IoResult<()>
where where
P: AsRef<Path>, P: AsRef<Path>,
Q: AsRef<Path>, Q: AsRef<Path>,
@ -901,7 +942,6 @@ fn to_utf16(path: &Path) -> WideCString {
WideCString::from_str(path).unwrap() WideCString::from_str(path).unwrap()
} }
// Adapted from sys/windows/fs.rs in libstd
fn truncate_utf16_at_nul(v: &[u16]) -> &[u16] { fn truncate_utf16_at_nul(v: &[u16]) -> &[u16] {
match v.iter().position(|c| *c == 0) { match v.iter().position(|c| *c == 0) {
// don't include the 0 // don't include the 0
@ -910,8 +950,6 @@ fn truncate_utf16_at_nul(v: &[u16]) -> &[u16] {
} }
} }
// Copied from sys/common/io.rs in libstd
// Provides read_to_end functionality over an uninitialized buffer. // Provides read_to_end functionality over an uninitialized buffer.
// This function is unsafe because it calls the underlying // This function is unsafe because it calls the underlying
// read function with a slice into uninitialized memory. The default // read function with a slice into uninitialized memory. The default
@ -998,6 +1036,7 @@ impl Seek for File {
} }
impl Drop for Fs { impl Drop for Fs {
#[doc(alias = "fsExit")]
fn drop(&mut self) { fn drop(&mut self) {
unsafe { unsafe {
ctru_sys::fsExit(); ctru_sys::fsExit();
@ -1006,6 +1045,7 @@ impl Drop for Fs {
} }
impl Drop for Archive { impl Drop for Archive {
#[doc(alias = "FSUSER_CloseArchive")]
fn drop(&mut self) { fn drop(&mut self) {
unsafe { unsafe {
let _ = ctru_sys::FSUSER_CloseArchive(self.handle); let _ = ctru_sys::FSUSER_CloseArchive(self.handle);
@ -1014,6 +1054,7 @@ impl Drop for Archive {
} }
impl Drop for File { impl Drop for File {
#[doc(alias = "FSFILE_Close")]
fn drop(&mut self) { fn drop(&mut self) {
unsafe { unsafe {
let _ = ctru_sys::FSFILE_Close(self.handle); let _ = ctru_sys::FSFILE_Close(self.handle);
@ -1022,6 +1063,7 @@ impl Drop for File {
} }
impl Drop for Dir { impl Drop for Dir {
#[doc(alias = "FSDIR_Close")]
fn drop(&mut self) { fn drop(&mut self) {
unsafe { unsafe {
let _ = ctru_sys::FSDIR_Close(self.0); let _ = ctru_sys::FSDIR_Close(self.0);

302
ctru-rs/src/services/gfx.rs

@ -1,4 +1,8 @@
//! LCD screens manipulation helper //! Graphics service.
//!
//! The GFX service controls (in a somewhat high-level way) the console's LCD screens.
//! The screens are subordinate to the GFX service handle and can be used by only one borrower at a time.
#![doc(alias = "graphics")]
use std::cell::{Ref, RefCell, RefMut}; use std::cell::{Ref, RefCell, RefMut};
use std::marker::PhantomData; use std::marker::PhantomData;
@ -20,9 +24,12 @@ mod private {
impl Sealed for BottomScreen {} impl Sealed for BottomScreen {}
} }
/// Trait to handle common functionality for all screens.
///
/// This trait is implemented by the screen structs for working with frame buffers and /// This trait is implemented by the screen structs for working with frame buffers and
/// drawing to the screens. Graphics-related code can be made generic over this /// drawing to the screens. Graphics-related code can be made generic over this
/// trait to work with any of the given screens. /// trait to work with any of the given screens.
#[doc(alias = "gfxScreen_t")]
pub trait Screen: private::Sealed { pub trait Screen: private::Sealed {
/// Returns the `libctru` value for the Screen kind. /// Returns the `libctru` value for the Screen kind.
fn as_raw(&self) -> ctru_sys::gfxScreen_t; fn as_raw(&self) -> ctru_sys::gfxScreen_t;
@ -30,10 +37,17 @@ pub trait Screen: private::Sealed {
/// Returns the Screen side (left or right). /// Returns the Screen side (left or right).
fn side(&self) -> Side; fn side(&self) -> Side;
/// Returns a [`RawFrameBuffer`] for the screen. /// Returns a [`RawFrameBuffer`] for the screen (if the framebuffer was allocated on the HEAP).
///
/// # Notes
///
/// The pointer of the framebuffer returned by this function can change after each call
/// to this function if double buffering is enabled, so it's suggested to NOT save it for later use.
/// ///
/// Note that the pointer of the framebuffer returned by this function can /// # Panics
/// change after each call to this function if double buffering is enabled. ///
/// If the [`Gfx`] service was initialised via [`Gfx::with_formats_vram()`] this function will crash the program with an ARM exception.
#[doc(alias = "gfxGetFramebuffer")]
fn raw_framebuffer(&mut self) -> RawFrameBuffer { fn raw_framebuffer(&mut self) -> RawFrameBuffer {
let mut width: u16 = 0; let mut width: u16 = 0;
let mut height: u16 = 0; let mut height: u16 = 0;
@ -48,15 +62,8 @@ pub trait Screen: private::Sealed {
} }
} }
/// Sets whether to use double buffering. Enabled by default.
///
/// [`Swap::swap_buffers`] must be called after this function for the configuration
/// change to take effect.
fn set_double_buffering(&mut self, enabled: bool) {
unsafe { ctru_sys::gfxSetDoubleBuffering(self.as_raw(), enabled) }
}
/// Gets the framebuffer format. /// Gets the framebuffer format.
#[doc(alias = "gfxGetScreenFormat")]
fn framebuffer_format(&self) -> FramebufferFormat { fn framebuffer_format(&self) -> FramebufferFormat {
unsafe { ctru_sys::gfxGetScreenFormat(self.as_raw()) }.into() unsafe { ctru_sys::gfxGetScreenFormat(self.as_raw()) }.into()
} }
@ -65,40 +72,53 @@ pub trait Screen: private::Sealed {
/// ///
/// [`Swap::swap_buffers`] must be called after this method for the configuration /// [`Swap::swap_buffers`] must be called after this method for the configuration
/// change to take effect. /// change to take effect.
#[doc(alias = "gfxSetScreenFormat")]
fn set_framebuffer_format(&mut self, fmt: FramebufferFormat) { fn set_framebuffer_format(&mut self, fmt: FramebufferFormat) {
unsafe { ctru_sys::gfxSetScreenFormat(self.as_raw(), fmt.into()) } unsafe { ctru_sys::gfxSetScreenFormat(self.as_raw(), fmt.into()) }
} }
} }
/// The top screen. Mutable access to this struct is required to write to the top /// The top LCD screen.
/// screen's frame buffer. To enable 3D mode, it can be converted into a [`TopScreen3D`]. ///
/// Mutable access to this struct is required to write to the top screen's frame buffer.
///
/// To enable 3D mode, it can be converted into a [`TopScreen3D`].
pub struct TopScreen { pub struct TopScreen {
left: TopScreenLeft, left: TopScreenLeft,
right: TopScreenRight, right: TopScreenRight,
} }
/// The top LCD screen set in stereoscopic 3D mode.
///
/// A helper container for both sides of the top screen. Once the [`TopScreen`] is /// A helper container for both sides of the top screen. Once the [`TopScreen`] is
/// converted into this, 3D mode will be enabled until this struct is dropped. /// converted into this, 3D mode will be enabled until this struct is dropped.
pub struct TopScreen3D<'screen> { pub struct TopScreen3D<'screen> {
screen: &'screen RefCell<TopScreen>, screen: &'screen RefCell<TopScreen>,
} }
/// A screen that can have its frame buffers swapped, if double buffering is enabled. /// Trait for screens that can have its frame buffers swapped, when double buffering is enabled.
/// ///
/// This trait applies to all [`Screen`]s that have swappable frame buffers. /// This trait applies to all [`Screen`]s that have swappable frame buffers.
pub trait Swap: private::Sealed { pub trait Swap: private::Sealed {
/// Swaps the video buffers. /// Swaps the video buffers.
/// ///
/// If double buffering is disabled, "swapping" the buffers has the side effect /// Even if double buffering is disabled, "swapping" the buffers has the side effect
/// of committing any configuration changes to the buffers (e.g. [`set_wide_mode`], /// of committing any configuration changes to the buffers (e.g. [`TopScreen::set_wide_mode()`],
/// [`set_framebuffer_format`], [`set_double_buffering`]). /// [`Screen::set_framebuffer_format()`], [`Swap::set_double_buffering()`]), so it should still be used.
/// ///
/// This should be called once per frame at most. /// This should be called once per frame at most.
/// #[doc(alias = "gfxScreenSwapBuffers")]
/// [`set_wide_mode`]: TopScreen::set_wide_mode
/// [`set_framebuffer_format`]: Screen::set_framebuffer_format
/// [`set_double_buffering`]: Screen::set_double_buffering
fn swap_buffers(&mut self); fn swap_buffers(&mut self);
/// Set whether to use double buffering.
///
/// # Notes
///
/// Double buffering is enabled by default.
/// [`Swap::swap_buffers`] must be called after this function for the configuration
/// change to take effect.
#[doc(alias = "gfxSetDoubleBuffering")]
fn set_double_buffering(&mut self, enabled: bool);
} }
impl Swap for TopScreen3D<'_> { impl Swap for TopScreen3D<'_> {
@ -107,6 +127,10 @@ impl Swap for TopScreen3D<'_> {
ctru_sys::gfxScreenSwapBuffers(ctru_sys::GFX_TOP, true); ctru_sys::gfxScreenSwapBuffers(ctru_sys::GFX_TOP, true);
} }
} }
fn set_double_buffering(&mut self, enabled: bool) {
unsafe { ctru_sys::gfxSetDoubleBuffering(ctru_sys::GFX_TOP, enabled) }
}
} }
impl Swap for TopScreen { impl Swap for TopScreen {
@ -115,6 +139,10 @@ impl Swap for TopScreen {
ctru_sys::gfxScreenSwapBuffers(ctru_sys::GFX_TOP, false); ctru_sys::gfxScreenSwapBuffers(ctru_sys::GFX_TOP, false);
} }
} }
fn set_double_buffering(&mut self, enabled: bool) {
unsafe { ctru_sys::gfxSetDoubleBuffering(ctru_sys::GFX_TOP, enabled) }
}
} }
impl Swap for BottomScreen { impl Swap for BottomScreen {
@ -123,13 +151,20 @@ impl Swap for BottomScreen {
ctru_sys::gfxScreenSwapBuffers(ctru_sys::GFX_BOTTOM, false); ctru_sys::gfxScreenSwapBuffers(ctru_sys::GFX_BOTTOM, false);
} }
} }
fn set_double_buffering(&mut self, enabled: bool) {
unsafe { ctru_sys::gfxSetDoubleBuffering(ctru_sys::GFX_BOTTOM, enabled) }
}
} }
/// A screen with buffers that can be flushed. This trait applies to any [`Screen`] /// A screen with buffers that can be flushed.
/// that has data written to its frame buffer. ///
/// This trait applies to any [`Screen`] that has data written to its frame buffer.
pub trait Flush: private::Sealed { pub trait Flush: private::Sealed {
/// Flushes the video buffer(s) for this screen. Note that you must still call /// Flushes the video buffer(s) for this screen.
/// [`Swap::swap_buffers`] after this method for the buffer contents to be displayed. ///
/// Note that you must still call [`Swap::swap_buffers`] after this method for the buffer contents to be displayed.
#[doc(alias = "gfxFlushBuffers")]
fn flush_buffers(&mut self); fn flush_buffers(&mut self);
} }
@ -138,7 +173,7 @@ impl<S: Screen> Flush for S {
let framebuffer = self.raw_framebuffer(); let framebuffer = self.raw_framebuffer();
// Flush the data array. `self.raw_framebuffer` should get the correct parameters for all kinds of screens // Flush the data array. `self.raw_framebuffer` should get the correct parameters for all kinds of screens
unsafe { let _ = unsafe {
ctru_sys::GSPGPU_FlushDataCache( ctru_sys::GSPGPU_FlushDataCache(
framebuffer.ptr.cast(), framebuffer.ptr.cast(),
(framebuffer.height * framebuffer.width) as u32, (framebuffer.height * framebuffer.width) as u32,
@ -167,14 +202,16 @@ pub struct TopScreenLeft;
#[non_exhaustive] #[non_exhaustive]
pub struct TopScreenRight; pub struct TopScreenRight;
/// The bottom screen. Mutable access to this struct is required to write to the /// The bottom LCD screen.
/// bottom screen's frame buffer. ///
/// Mutable access to this struct is required to write to the bottom screen's frame buffer.
#[derive(Debug)] #[derive(Debug)]
#[non_exhaustive] #[non_exhaustive]
pub struct BottomScreen; pub struct BottomScreen;
/// Representation of a framebuffer for one [`Side`] of the top screen, or the /// Representation of a framebuffer for one [`Side`] of the top screen, or the entire bottom screen.
/// entire bottom screen. The inner pointer is only valid for one frame if double ///
/// The inner pointer is only valid for one frame if double
/// buffering is enabled. Data written to `ptr` will be rendered to the screen. /// buffering is enabled. Data written to `ptr` will be rendered to the screen.
#[derive(Debug)] #[derive(Debug)]
pub struct RawFrameBuffer<'screen> { pub struct RawFrameBuffer<'screen> {
@ -188,11 +225,12 @@ pub struct RawFrameBuffer<'screen> {
screen: PhantomData<&'screen mut dyn Screen>, screen: PhantomData<&'screen mut dyn Screen>,
} }
#[derive(Copy, Clone, Debug, PartialEq, Eq)] /// Side of the [`TopScreen`]'s framebuffer.
#[repr(u32)]
/// Side of top screen framebuffer
/// ///
/// The top screen of the 3DS can have two separate sets of framebuffers to support its 3D functionality /// The top screen of the 3DS can have two separate sets of framebuffers to support its 3D functionality
#[doc(alias = "gfx3dSide_t")]
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
#[repr(u32)]
pub enum Side { pub enum Side {
/// The left framebuffer. This framebuffer is also the one used when 3D is disabled /// The left framebuffer. This framebuffer is also the one used when 3D is disabled
Left = ctru_sys::GFX_LEFT, Left = ctru_sys::GFX_LEFT,
@ -200,40 +238,139 @@ pub enum Side {
Right = ctru_sys::GFX_RIGHT, Right = ctru_sys::GFX_RIGHT,
} }
/// A handle to libctru's gfx module. This module is a wrapper around the GSPGPU service that /// Handle to the GFX service.
/// provides helper functions and utilities for software rendering.
/// ///
/// The service exits when this struct is dropped. /// This service is a wrapper around the lower-level [GSPGPU](crate::services::gspgpu) service that
/// provides helper functions and utilities for software rendering.
pub struct Gfx { pub struct Gfx {
/// Top screen representation.
pub top_screen: RefCell<TopScreen>, pub top_screen: RefCell<TopScreen>,
/// Bottom screen representation.
pub bottom_screen: RefCell<BottomScreen>, pub bottom_screen: RefCell<BottomScreen>,
_service_handler: ServiceReference, _service_handler: ServiceReference,
} }
static GFX_ACTIVE: Mutex<usize> = Mutex::new(0); static GFX_ACTIVE: Mutex<()> = Mutex::new(());
impl Gfx { impl Gfx {
/// Creates a new [Gfx] instance with default init values /// Initialize a new default service handle.
/// It's the same as calling: ///
/// `Gfx::with_formats(FramebufferFormat::Bgr8, FramebufferFormat::Bgr8, false)` /// # Notes
///
/// The new `Gfx` instance will allocate the needed framebuffers in the CPU-GPU shared memory region (to ensure compatibiltiy with all possible uses of the `Gfx` service).
/// As such, it's the same as calling:
///
/// ```
/// # let _runner = test_runner::GdbRunner::default();
/// # use std::error::Error;
/// # fn main() -> Result<(), Box<dyn Error>> {
/// #
/// # use ctru::services::gfx::Gfx;
/// # use ctru::services::gspgpu::FramebufferFormat;
/// #
/// Gfx::with_formats_shared(FramebufferFormat::Bgr8, FramebufferFormat::Bgr8)?;
/// #
/// # Ok(())
/// # }
/// ```
///
/// Have a look at [`Gfx::with_formats_vram()`] if you aren't interested in manipulating the framebuffers using the CPU.
///
/// # Example
///
/// ```
/// # let _runner = test_runner::GdbRunner::default();
/// # use std::error::Error;
/// # fn main() -> Result<(), Box<dyn Error>> {
/// #
/// use ctru::services::gfx::Gfx;
///
/// let gfx = Gfx::new()?;
/// #
/// # Ok(())
/// # }
/// ```
#[doc(alias = "gfxInit")]
pub fn new() -> Result<Self> { pub fn new() -> Result<Self> {
Gfx::with_formats(FramebufferFormat::Bgr8, FramebufferFormat::Bgr8, false) Gfx::with_formats_shared(FramebufferFormat::Bgr8, FramebufferFormat::Bgr8)
} }
/// Initialize the Gfx module with the chosen framebuffer formats for the top and bottom /// Initialize a new service handle with the chosen framebuffer formats on the HEAP for the top and bottom screens.
/// screens ///
/// Use [`Gfx::new()`] instead of this function to initialize the module with default parameters
///
/// # Example
/// ///
/// Use `Gfx::new()` instead of this function to initialize the module with default parameters /// ```
pub fn with_formats( /// # let _runner = test_runner::GdbRunner::default();
/// # use std::error::Error;
/// # fn main() -> Result<(), Box<dyn Error>> {
/// #
/// use ctru::services::gfx::Gfx;
/// use ctru::services::gspgpu::FramebufferFormat;
///
/// // Top screen uses RGBA8, bottom screen uses RGB565.
/// // The screen buffers are allocated in the standard HEAP memory, and not in VRAM.
/// let gfx = Gfx::with_formats_shared(FramebufferFormat::Rgba8, FramebufferFormat::Rgb565)?;
/// #
/// # Ok(())
/// # }
/// ```
#[doc(alias = "gfxInit")]
pub fn with_formats_shared(
top_fb_fmt: FramebufferFormat, top_fb_fmt: FramebufferFormat,
bottom_fb_fmt: FramebufferFormat, bottom_fb_fmt: FramebufferFormat,
use_vram_buffers: bool, ) -> Result<Self> {
Self::with_configuration(top_fb_fmt, bottom_fb_fmt, false)
}
/// Initialize a new service handle with the chosen framebuffer formats on the VRAM for the top and bottom screens.
///
/// # Notes
///
/// Though unsafe to do so, it's suggested to use VRAM buffers when working exclusively with the GPU,
/// since they result in faster performance and less memory waste.
///
/// # Safety
///
/// By initializing the [`Gfx`] service as such, all functionality that relies on CPU manipulation of the framebuffers will
/// be completely unavailable (usually resulting in an ARM panic if wrongly used).
///
/// Usage of functionality such as [`Console`](crate::console::Console) and [`Screen::raw_framebuffer()`] will result in ARM exceptions.
///
/// # Example
///
/// ```
/// # use std::error::Error;
/// # fn main() -> Result<(), Box<dyn Error>> {
/// #
/// use ctru::services::{gfx::Gfx, gspgpu::FramebufferFormat};
///
/// // Top screen uses RGBA8, bottom screen uses RGB565.
/// // The screen buffers are allocated in the in VRAM, so they will NOT be accessible from the CPU.
/// let gfx = unsafe { Gfx::with_formats_vram(FramebufferFormat::Rgba8, FramebufferFormat::Rgb565)? };
/// #
/// # Ok(())
/// # }
/// ```
#[doc(alias = "gfxInit")]
pub unsafe fn with_formats_vram(
top_fb_fmt: FramebufferFormat,
bottom_fb_fmt: FramebufferFormat,
) -> Result<Self> {
Self::with_configuration(top_fb_fmt, bottom_fb_fmt, true)
}
// Internal function to handle the initialization of `Gfx`.
fn with_configuration(
top_fb_fmt: FramebufferFormat,
bottom_fb_fmt: FramebufferFormat,
vram_buffer: bool,
) -> Result<Self> { ) -> Result<Self> {
let handler = ServiceReference::new( let handler = ServiceReference::new(
&GFX_ACTIVE, &GFX_ACTIVE,
false,
|| unsafe { || unsafe {
ctru_sys::gfxInit(top_fb_fmt.into(), bottom_fb_fmt.into(), use_vram_buffers); ctru_sys::gfxInit(top_fb_fmt.into(), bottom_fb_fmt.into(), vram_buffer);
Ok(()) Ok(())
}, },
@ -247,9 +384,34 @@ impl Gfx {
}) })
} }
/// Waits for the vertical blank interrupt /// Waits for the vertical blank event.
/// ///
/// Use this to synchronize your application with the refresh rate of the LCD screens /// Use this to synchronize your application with the refresh rate of the LCD screens
///
/// # Example
///
/// ```
/// # let _runner = test_runner::GdbRunner::default();
/// # use std::error::Error;
/// # fn main() -> Result<(), Box<dyn Error>> {
/// #
/// use ctru::services::apt::Apt;
/// use ctru::services::gfx::Gfx;
/// let apt = Apt::new()?;
/// let gfx = Gfx::new()?;
///
/// // Simple main loop.
/// while apt.main_loop() {
/// // Main program logic
///
/// // Wait for the screens to refresh.
/// // This blocks the current thread to make it run at 60Hz.
/// gfx.wait_for_vblank();
/// }
/// #
/// # Ok(())
/// # }
/// ```
pub fn wait_for_vblank(&self) { pub fn wait_for_vblank(&self) {
gspgpu::wait_for_event(gspgpu::Event::VBlank0, true); gspgpu::wait_for_event(gspgpu::Event::VBlank0, true);
} }
@ -269,7 +431,36 @@ impl TopScreen3D<'_> {
} }
} }
/// Convert the [`TopScreen`] into a [`TopScreen3D`] and activate stereoscopic 3D.
///
/// # Example
///
/// ```
/// # let _runner = test_runner::GdbRunner::default();
/// # use std::error::Error;
/// # fn main() -> Result<(), Box<dyn Error>> {
/// #
/// use ctru::services::gfx::{Gfx, TopScreen, TopScreen3D};
/// let gfx = Gfx::new()?;
///
/// let mut top_screen = TopScreen3D::from(&gfx.top_screen);
///
/// let (left, right) = top_screen.split_mut();
///
/// // Rendering must be done twice for each side
/// // (with a slight variation in perspective to simulate the eye-to-eye distance).
/// render(left);
/// render(right);
/// #
/// # Ok(())
/// # }
/// #
/// # use ctru::services::gfx::Screen;
/// # use std::cell::RefMut;
/// # fn render(screen: RefMut<'_, dyn Screen>) {}
/// ```
impl<'screen> From<&'screen RefCell<TopScreen>> for TopScreen3D<'screen> { impl<'screen> From<&'screen RefCell<TopScreen>> for TopScreen3D<'screen> {
#[doc(alias = "gfxSet3D")]
fn from(top_screen: &'screen RefCell<TopScreen>) -> Self { fn from(top_screen: &'screen RefCell<TopScreen>) -> Self {
unsafe { unsafe {
ctru_sys::gfxSet3D(true); ctru_sys::gfxSet3D(true);
@ -297,8 +488,13 @@ impl TopScreen {
/// Enable or disable wide mode on the top screen. /// Enable or disable wide mode on the top screen.
/// ///
/// # Notes
///
/// [`Swap::swap_buffers`] must be called after this method for the configuration /// [`Swap::swap_buffers`] must be called after this method for the configuration
/// to take effect. /// to take effect.
///
/// Wide mode does NOT work on Old 2DS models (but still does on New 2DS XL models).
#[doc(alias = "gfxSetWide")]
pub fn set_wide_mode(&mut self, enable: bool) { pub fn set_wide_mode(&mut self, enable: bool) {
unsafe { unsafe {
ctru_sys::gfxSetWide(enable); ctru_sys::gfxSetWide(enable);
@ -306,6 +502,7 @@ impl TopScreen {
} }
/// Returns whether or not wide mode is enabled on the top screen. /// Returns whether or not wide mode is enabled on the top screen.
#[doc(alias = "gfxIsWide")]
pub fn is_wide(&self) -> bool { pub fn is_wide(&self) -> bool {
unsafe { ctru_sys::gfxIsWide() } unsafe { ctru_sys::gfxIsWide() }
} }
@ -362,7 +559,10 @@ mod tests {
#[test] #[test]
fn gfx_duplicate() { fn gfx_duplicate() {
// We don't need to build a `Gfx` because the test runner has one already // NOTE: this is expected to fail if using the console test runner, since
// that necessarily creates a Gfx as part of its test setup:
let _gfx = Gfx::new().unwrap();
assert!(matches!(Gfx::new(), Err(Error::ServiceAlreadyActive))); assert!(matches!(Gfx::new(), Err(Error::ServiceAlreadyActive)));
} }
} }

13
ctru-rs/src/services/gspgpu.rs

@ -1,18 +1,28 @@
//! GSPGPU service //! GSPGPU service
/// GSPGPU events that can be awaited.
#[doc(alias = "GSPGPU_Event")]
#[derive(Copy, Clone, Debug, PartialEq, Eq)] #[derive(Copy, Clone, Debug, PartialEq, Eq)]
#[repr(u32)] #[repr(u32)]
pub enum Event { pub enum Event {
/// Memory fill 1 completed.
Psc0 = ctru_sys::GSPGPU_EVENT_PSC0, Psc0 = ctru_sys::GSPGPU_EVENT_PSC0,
/// Memory fill 2 completed.
Psc1 = ctru_sys::GSPGPU_EVENT_PSC1, Psc1 = ctru_sys::GSPGPU_EVENT_PSC1,
/// Top screen VBlank.
VBlank0 = ctru_sys::GSPGPU_EVENT_VBlank0, VBlank0 = ctru_sys::GSPGPU_EVENT_VBlank0,
/// Bottom screen VBlank.
VBlank1 = ctru_sys::GSPGPU_EVENT_VBlank1, VBlank1 = ctru_sys::GSPGPU_EVENT_VBlank1,
/// Display transfer completed.
PPF = ctru_sys::GSPGPU_EVENT_PPF, PPF = ctru_sys::GSPGPU_EVENT_PPF,
/// Command list processing completed.
P3D = ctru_sys::GSPGPU_EVENT_P3D, P3D = ctru_sys::GSPGPU_EVENT_P3D,
/// Direct Memory Access requested.
DMA = ctru_sys::GSPGPU_EVENT_DMA, DMA = ctru_sys::GSPGPU_EVENT_DMA,
} }
/// Framebuffer formats supported by the 3DS #[doc(alias = "GSPGPU_FramebufferFormat")]
/// Framebuffer formats supported by the 3DS' screens.
#[derive(Copy, Clone, Debug, PartialEq, Eq)] #[derive(Copy, Clone, Debug, PartialEq, Eq)]
#[repr(u32)] #[repr(u32)]
pub enum FramebufferFormat { pub enum FramebufferFormat {
@ -45,6 +55,7 @@ impl FramebufferFormat {
/// Waits for a GSPGPU event to occur. /// Waits for a GSPGPU event to occur.
/// ///
/// `discard_current` determines whether to discard the current event and wait for the next event /// `discard_current` determines whether to discard the current event and wait for the next event
#[doc(alias = "gspWaitForEvent")]
pub fn wait_for_event(ev: Event, discard_current: bool) { pub fn wait_for_event(ev: Event, discard_current: bool) {
unsafe { unsafe {
ctru_sys::gspWaitForEvent(ev.into(), discard_current); ctru_sys::gspWaitForEvent(ev.into(), discard_current);

488
ctru-rs/src/services/hid.rs

@ -1,75 +1,218 @@
//! HID service //! Human Interface Device service.
//! //!
//! The HID service provides access to user input such as button presses, touch screen presses, //! The HID service provides read access to user input such as [button presses](Hid::keys_down), [touch screen presses](Hid::touch_position),
//! and circle pad information. It also provides information from the sound volume slider, //! and [circle pad information](Hid::circlepad_position). It also provides information from the [volume slider](Hid::volume_slider()),
//! the accelerometer, and the gyroscope. //! the [accelerometer](Hid::accelerometer_vector()), and the [gyroscope](Hid::gyroscope_rate()).
#![doc(alias = "input")]
#![doc(alias = "controller")]
#![doc(alias = "gamepad")]
use std::sync::Mutex;
use crate::error::ResultCode; use crate::error::ResultCode;
bitflags::bitflags! { use crate::services::ServiceReference;
/// A set of flags corresponding to the button and directional pad
/// inputs on the 3DS use bitflags::bitflags;
static HID_ACTIVE: Mutex<()> = Mutex::new(());
bitflags! {
/// A set of flags corresponding to the button and directional pad inputs present on the 3DS.
#[derive(PartialEq, Eq, PartialOrd, Ord, Hash, Debug, Clone, Copy)]
pub struct KeyPad: u32 { pub struct KeyPad: u32 {
/// A button.
const A = ctru_sys::KEY_A; const A = ctru_sys::KEY_A;
/// B button.
const B = ctru_sys::KEY_B; const B = ctru_sys::KEY_B;
/// Select button.
const SELECT = ctru_sys::KEY_SELECT; const SELECT = ctru_sys::KEY_SELECT;
/// Start button.
const START = ctru_sys::KEY_START; const START = ctru_sys::KEY_START;
/// D-Pad Right.
const DPAD_RIGHT = ctru_sys::KEY_DRIGHT; const DPAD_RIGHT = ctru_sys::KEY_DRIGHT;
/// D-Pad Left.
const DPAD_LEFT = ctru_sys::KEY_DLEFT; const DPAD_LEFT = ctru_sys::KEY_DLEFT;
/// D-Pad Up.
const DPAD_UP = ctru_sys::KEY_DUP; const DPAD_UP = ctru_sys::KEY_DUP;
/// D-Pad Down.
const DPAD_DOWN = ctru_sys::KEY_DDOWN; const DPAD_DOWN = ctru_sys::KEY_DDOWN;
/// R button.
const R = ctru_sys::KEY_R; const R = ctru_sys::KEY_R;
/// L button.
const L = ctru_sys::KEY_L; const L = ctru_sys::KEY_L;
/// X button.
const X = ctru_sys::KEY_X; const X = ctru_sys::KEY_X;
/// Y button.
const Y = ctru_sys::KEY_Y; const Y = ctru_sys::KEY_Y;
/// ZL button.
const ZL = ctru_sys::KEY_ZL; const ZL = ctru_sys::KEY_ZL;
/// ZR button.
const ZR = ctru_sys::KEY_ZR; const ZR = ctru_sys::KEY_ZR;
/// Touchscreen.
const TOUCH = ctru_sys::KEY_TOUCH; const TOUCH = ctru_sys::KEY_TOUCH;
/// C-Stick Right.
const CSTICK_RIGHT = ctru_sys::KEY_CSTICK_RIGHT; const CSTICK_RIGHT = ctru_sys::KEY_CSTICK_RIGHT;
/// C-Stick Left.
const CSTICK_LEFT = ctru_sys::KEY_CSTICK_LEFT; const CSTICK_LEFT = ctru_sys::KEY_CSTICK_LEFT;
/// C-Stick Up.
const CSTICK_UP = ctru_sys::KEY_CSTICK_UP; const CSTICK_UP = ctru_sys::KEY_CSTICK_UP;
/// C-Stick Down.
const CSTICK_DOWN = ctru_sys::KEY_CSTICK_DOWN; const CSTICK_DOWN = ctru_sys::KEY_CSTICK_DOWN;
/// CirclePad Right.
const CPAD_RIGHT = ctru_sys::KEY_CPAD_RIGHT; const CPAD_RIGHT = ctru_sys::KEY_CPAD_RIGHT;
/// CirclePad Left.
const CPAD_LEFT = ctru_sys::KEY_CPAD_LEFT; const CPAD_LEFT = ctru_sys::KEY_CPAD_LEFT;
/// CirclePad Up.
const CPAD_UP = ctru_sys::KEY_CPAD_UP; const CPAD_UP = ctru_sys::KEY_CPAD_UP;
/// CirclePad Down.
const CPAD_DOWN = ctru_sys::KEY_CPAD_DOWN; const CPAD_DOWN = ctru_sys::KEY_CPAD_DOWN;
// Convenience catch-all for the dpad and cpad
// Convenience catch-all for the D-Pad and the CirclePad
/// Direction Up (either D-Pad or CirclePad).
const UP = KeyPad::DPAD_UP.bits() | KeyPad::CPAD_UP.bits(); const UP = KeyPad::DPAD_UP.bits() | KeyPad::CPAD_UP.bits();
/// Direction Down (either D-Pad or CirclePad).
const DOWN = KeyPad::DPAD_DOWN.bits() | KeyPad::CPAD_DOWN.bits(); const DOWN = KeyPad::DPAD_DOWN.bits() | KeyPad::CPAD_DOWN.bits();
/// Direction Left (either D-Pad or CirclePad).
const LEFT = KeyPad::DPAD_LEFT.bits() | KeyPad::CPAD_LEFT.bits(); const LEFT = KeyPad::DPAD_LEFT.bits() | KeyPad::CPAD_LEFT.bits();
/// Direction Right (either D-Pad or CirclePad).
const RIGHT = KeyPad::DPAD_RIGHT.bits() | KeyPad::CPAD_RIGHT.bits(); const RIGHT = KeyPad::DPAD_RIGHT.bits() | KeyPad::CPAD_RIGHT.bits();
} }
} }
/// A reference-counted handle to the HID service. The service is closed /// Error enum for generic errors within the [`Hid`] service.
/// when all instances of this struct fall out of scope. #[non_exhaustive]
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum Error {
/// An attempt was made to access the accelerometer while disabled.
UnavailableAccelerometer,
/// An attempt was made to access the gyroscope while disabled.
UnavailableGyroscope,
}
/// Representation of the acceleration vector read by the accelerometer.
/// ///
/// This service requires no special permissions to use. /// Have a look at [`Hid::set_accelerometer()`] for more information.
pub struct Hid(()); #[allow(missing_docs)]
#[derive(Default, Copy, Clone, Debug, PartialEq, Eq)]
pub struct Acceleration {
x: i16,
y: i16,
z: i16,
}
/// Representation of the angular rate read by the gyroscope.
///
/// Have a look at [`Hid::set_gyroscope()`] for more information.
#[allow(missing_docs)]
#[derive(Default, Copy, Clone, Debug, PartialEq, Eq)]
pub struct AngularRate {
roll: i16,
pitch: i16,
yaw: i16,
}
/// Initializes the HID service. /// Handle to the HID service.
pub struct Hid {
active_accelerometer: bool,
active_gyroscope: bool,
_service_handler: ServiceReference,
}
impl Hid {
/// Initialize a new service handle.
/// ///
/// # Errors /// # Errors
/// ///
/// This function will return an error if the service was unable to be initialized. /// This function will return an error if the service was unable to be initialized.
/// Since this service requires no special or elevated permissions, errors are /// Since this service requires no special or elevated permissions, errors are rare in practice.
/// rare in practice. ///
impl Hid { /// # Example
///
/// ```
/// # let _runner = test_runner::GdbRunner::default();
/// # use std::error::Error;
/// # fn main() -> Result<(), Box<dyn Error>> {
/// #
/// use ctru::services::hid::Hid;
///
/// let hid = Hid::new()?;
/// #
/// # Ok(())
/// # }
/// ```
#[doc(alias = "hidInit")]
pub fn new() -> crate::Result<Hid> { pub fn new() -> crate::Result<Hid> {
unsafe { let handler = ServiceReference::new(
ResultCode(ctru_sys::hidInit())?; &HID_ACTIVE,
Ok(Hid(())) || {
} ResultCode(unsafe { ctru_sys::hidInit() })?;
Ok(())
},
|| unsafe {
let _ = ctru_sys::HIDUSER_DisableGyroscope();
let _ = ctru_sys::HIDUSER_DisableAccelerometer();
ctru_sys::hidExit();
},
)?;
Ok(Self {
active_accelerometer: false,
active_gyroscope: false,
_service_handler: handler,
})
} }
/// Scans the HID service for all user input occurring on the current /// Scan the HID service for all user input occurring on the current frame.
/// frame. This function should be called on every frame when polling ///
/// This function should be called on every frame when polling
/// for user input. /// for user input.
///
/// # Example
///
/// ```
/// # let _runner = test_runner::GdbRunner::default();
/// # use std::error::Error;
/// # fn main() -> Result<(), Box<dyn Error>> {
/// #
/// use ctru::services::hid::Hid;
/// let mut hid = Hid::new()?;
///
/// hid.scan_input();
/// #
/// # Ok(())
/// # }
/// ```
#[doc(alias = "hidScanInput")]
pub fn scan_input(&mut self) { pub fn scan_input(&mut self) {
unsafe { ctru_sys::hidScanInput() }; unsafe { ctru_sys::hidScanInput() };
} }
/// Returns a bitflag struct representing which buttons have just been pressed /// Returns a bitflag struct representing which buttons have just been pressed
/// on the current frame (and were not pressed on the previous frame). /// on the current frame (and were not pressed on the previous frame).
///
/// # Example
///
/// ```
/// # let _runner = test_runner::GdbRunner::default();
/// # use std::error::Error;
/// # fn main() -> Result<(), Box<dyn Error>> {
/// #
/// use ctru::services::hid::{Hid, KeyPad};
/// let mut hid = Hid::new()?;
///
/// hid.scan_input();
///
/// if hid.keys_down().contains(KeyPad::A) {
/// println!("You have pressed the A button!")
/// }
/// #
/// # Ok(())
/// # }
/// ```
#[doc(alias = "hidKeysDown")]
pub fn keys_down(&self) -> KeyPad { pub fn keys_down(&self) -> KeyPad {
unsafe { unsafe {
let keys = ctru_sys::hidKeysDown(); let keys = ctru_sys::hidKeysDown();
@ -79,6 +222,27 @@ impl Hid {
/// Returns a bitflag struct representing which buttons have been held down /// Returns a bitflag struct representing which buttons have been held down
/// during the current frame. /// during the current frame.
///
/// # Example
///
/// ```
/// # let _runner = test_runner::GdbRunner::default();
/// # use std::error::Error;
/// # fn main() -> Result<(), Box<dyn Error>> {
/// #
/// use ctru::services::hid::{Hid, KeyPad};
/// let mut hid = Hid::new()?;
///
/// hid.scan_input();
///
/// if hid.keys_held().contains(KeyPad::START) {
/// println!("You are holding the START button!")
/// }
/// #
/// # Ok(())
/// # }
/// ```
#[doc(alias = "hidKeysHeld")]
pub fn keys_held(&self) -> KeyPad { pub fn keys_held(&self) -> KeyPad {
unsafe { unsafe {
let keys = ctru_sys::hidKeysHeld(); let keys = ctru_sys::hidKeysHeld();
@ -88,6 +252,27 @@ impl Hid {
/// Returns a bitflag struct representing which buttons have just been released on /// Returns a bitflag struct representing which buttons have just been released on
/// the current frame. /// the current frame.
///
/// # Example
///
/// ```
/// # let _runner = test_runner::GdbRunner::default();
/// # use std::error::Error;
/// # fn main() -> Result<(), Box<dyn Error>> {
/// #
/// use ctru::services::hid::{Hid, KeyPad};
/// let mut hid = Hid::new()?;
///
/// hid.scan_input();
///
/// if hid.keys_held().contains(KeyPad::B) {
/// println!("You have released the B button!")
/// }
/// #
/// # Ok(())
/// # }
/// ```
#[doc(alias = "hidKeysUp")]
pub fn keys_up(&self) -> KeyPad { pub fn keys_up(&self) -> KeyPad {
unsafe { unsafe {
let keys = ctru_sys::hidKeysUp(); let keys = ctru_sys::hidKeysUp();
@ -100,12 +285,32 @@ impl Hid {
/// # Notes /// # Notes
/// ///
/// (0, 0) represents the top left corner of the screen. /// (0, 0) represents the top left corner of the screen.
pub fn touch_position(&mut self) -> (u16, u16) { ///
/// # Example
///
/// ```
/// # let _runner = test_runner::GdbRunner::default();
/// # use std::error::Error;
/// # fn main() -> Result<(), Box<dyn Error>> {
/// #
/// use ctru::services::hid::Hid;
/// let mut hid = Hid::new()?;
///
/// hid.scan_input();
///
/// let (touch_x, touch_y) = hid.touch_position();
/// #
/// # Ok(())
/// # }
/// ```
#[doc(alias = "hidTouchRead")]
pub fn touch_position(&self) -> (u16, u16) {
let mut res = ctru_sys::touchPosition { px: 0, py: 0 }; let mut res = ctru_sys::touchPosition { px: 0, py: 0 };
unsafe { unsafe {
ctru_sys::hidTouchRead(&mut res); ctru_sys::hidTouchRead(&mut res);
} }
(res.px, res.py) (res.px, res.py)
} }
@ -114,18 +319,247 @@ impl Hid {
/// # Notes /// # Notes
/// ///
/// (0, 0) represents the center of the circle pad. /// (0, 0) represents the center of the circle pad.
pub fn circlepad_position(&mut self) -> (i16, i16) { ///
/// # Example
///
/// ```
/// # let _runner = test_runner::GdbRunner::default();
/// # use std::error::Error;
/// # fn main() -> Result<(), Box<dyn Error>> {
/// #
/// use ctru::services::hid::Hid;
/// let mut hid = Hid::new()?;
///
/// hid.scan_input();
///
/// let (pad_x, pad_y) = hid.circlepad_position();
/// #
/// # Ok(())
/// # }
/// ```
#[doc(alias = "hidCircleRead")]
pub fn circlepad_position(&self) -> (i16, i16) {
let mut res = ctru_sys::circlePosition { dx: 0, dy: 0 }; let mut res = ctru_sys::circlePosition { dx: 0, dy: 0 };
unsafe { unsafe {
ctru_sys::hidCircleRead(&mut res); ctru_sys::hidCircleRead(&mut res);
} }
(res.dx, res.dy) (res.dx, res.dy)
} }
/// Returns the current volume slider position (between 0 and 1).
///
/// # Notes
///
/// The [`ndsp`](crate::services::ndsp) service automatically uses the volume slider's position to handle audio mixing.
/// As such this method should not be used to programmatically change the volume.
///
/// Its purpose is only to inform the program of the volume slider's position (e.g. checking if the user has muted the audio).
///
/// # Example
///
/// ```
/// # use std::error::Error;
/// # fn main() -> Result<(), Box<dyn Error>> {
/// #
/// use ctru::services::hid::Hid;
/// let mut hid = Hid::new()?;
///
/// hid.scan_input();
///
/// let volume = hid.volume_slider();
/// #
/// # Ok(())
/// # }
/// ```
#[doc(alias = "HIDUSER_GetSoundVolume")]
pub fn volume_slider(&self) -> f32 {
let mut slider = 0;
unsafe {
let _ = ctru_sys::HIDUSER_GetSoundVolume(&mut slider);
}
(slider as f32) / 63.
}
/// Activate/deactivate the console's acceleration sensor.
///
/// # Example
///
/// ```
/// # use std::error::Error;
/// # fn main() -> Result<(), Box<dyn Error>> {
/// #
/// use ctru::services::hid::Hid;
/// let mut hid = Hid::new()?;
///
/// // The accelerometer will start to register movements.
/// hid.set_accelerometer(true).unwrap();
/// #
/// # Ok(())
/// # }
/// ```
#[doc(alias = "HIDUSER_EnableAccelerometer")]
#[doc(alias = "HIDUSER_DisableAccelerometer")]
pub fn set_accelerometer(&mut self, enabled: bool) -> crate::Result<()> {
if enabled {
ResultCode(unsafe { ctru_sys::HIDUSER_EnableAccelerometer() })?;
} else {
ResultCode(unsafe { ctru_sys::HIDUSER_DisableAccelerometer() })?;
}
self.active_accelerometer = enabled;
Ok(())
}
/// Activate/deactivate the console's gyroscopic sensor.
///
/// # Example
///
/// ```
/// # use std::error::Error;
/// # fn main() -> Result<(), Box<dyn Error>> {
/// #
/// use ctru::services::hid::Hid;
/// let mut hid = Hid::new()?;
///
/// // The gyroscope will start to register positions.
/// hid.set_gyroscope(true).unwrap();
/// #
/// # Ok(())
/// # }
/// ```
#[doc(alias = "HIDUSER_EnableGyroscope")]
#[doc(alias = "HIDUSER_DisableGyroscope")]
pub fn set_gyroscope(&mut self, enabled: bool) -> crate::Result<()> {
if enabled {
ResultCode(unsafe { ctru_sys::HIDUSER_EnableGyroscope() })?;
} else {
ResultCode(unsafe { ctru_sys::HIDUSER_DisableGyroscope() })?;
}
self.active_gyroscope = enabled;
Ok(())
}
/// Returns the acceleration vector (x, y, z) registered by the accelerometer.
///
/// # Errors
///
/// This function will return an error if the accelerometer was not previously enabled.
/// Have a look at [`Hid::set_accelerometer()`] to enable the accelerometer.
///
/// # Example
///
/// ```
/// # use std::error::Error;
/// # fn main() -> Result<(), Box<dyn Error>> {
/// #
/// use ctru::services::hid::Hid;
/// let mut hid = Hid::new()?;
///
/// // The accelerometer will start to register movements.
/// hid.set_accelerometer(true).unwrap();
///
/// // It's necessary to run `scan_input()` to update the accelerometer's readings.
/// hid.scan_input();
///
/// // This call fails if the accelerometer was not previously enabled.
/// let acceleration = hid.accelerometer_vector()?;
/// #
/// # Ok(())
/// # }
/// ```
#[doc(alias = "hidAccelRead")]
pub fn accelerometer_vector(&self) -> Result<Acceleration, Error> {
if !self.active_accelerometer {
return Err(Error::UnavailableAccelerometer);
}
let mut res = ctru_sys::accelVector { x: 0, y: 0, z: 0 };
unsafe {
ctru_sys::hidAccelRead(&mut res);
}
Ok(Acceleration {
x: res.x,
y: res.y,
z: res.z,
})
}
/// Returns the angular rate registered by the gyroscope.
///
/// # Errors
///
/// This function returns an error if the gyroscope was not previously enabled.
/// Have a look at [`Hid::set_gyroscope()`].
///
/// # Example
///
/// ```
/// # use std::error::Error;
/// # fn main() -> Result<(), Box<dyn Error>> {
/// #
/// use ctru::services::hid::Hid;
/// let mut hid = Hid::new()?;
///
/// // The gyroscope will start to register positions.
/// hid.set_gyroscope(true).unwrap();
///
/// // It's necessary to run `scan_input()` to update the gyroscope's readings.
/// hid.scan_input();
///
/// // This call fails if the gyroscope was not previously enabled.
/// let angular_rate = hid.gyroscope_rate()?;
/// #
/// # Ok(())
/// # }
/// ```
#[doc(alias = "hidGyroRead")]
pub fn gyroscope_rate(&self) -> Result<AngularRate, Error> {
if !self.active_gyroscope {
return Err(Error::UnavailableGyroscope);
} }
impl Drop for Hid { let mut res = ctru_sys::angularRate { x: 0, y: 0, z: 0 };
fn drop(&mut self) {
unsafe { ctru_sys::hidExit() }; unsafe {
ctru_sys::hidGyroRead(&mut res);
}
Ok(AngularRate {
roll: res.x,
pitch: res.y,
yaw: res.z,
})
} }
} }
impl From<Acceleration> for (i16, i16, i16) {
fn from(value: Acceleration) -> (i16, i16, i16) {
(value.x, value.y, value.z)
}
}
impl From<AngularRate> for (i16, i16, i16) {
fn from(value: AngularRate) -> (i16, i16, i16) {
(value.roll, value.pitch, value.yaw)
}
}
impl std::fmt::Display for Error {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::UnavailableAccelerometer => write!(f, "tried using accelerometer while disabled"),
Self::UnavailableGyroscope => write!(f, "tried using gyroscope while disabled"),
}
}
}
impl std::error::Error for Error {}

15
ctru-rs/src/services/mod.rs

@ -1,9 +1,15 @@
//! System services used to handle system-specific functionalities. //! OS services used to handle system-specific functionality.
//! //!
//! Most of the 3DS console's functionalities (when writing homebrew) are locked behind services, //! Most of the 3DS console's functionalities (when writing user-land homebrew) are accessible via services,
//! which need to be initialized before accessing any particular feature. //! which need to be initialized before accessing any particular feature.
//! //!
//! Some include: button input, audio playback, graphics rendering, built-in cameras, etc. //! To ensure safety while using the underlying services, [`ctru-rs`](crate) leverages Rust's lifetime model.
//! After initializing the handle for a specific service (e.g. [`Apt`](apt::Apt)) the service will be accessible as long as there is at least one handle "alive".
//! As such, handles should be dropped *after* the use of a specific service. This is particularly important for services which are necessary for functionality
//! "outside" their associated methods, such as [`RomFS`](romfs::RomFS), which creates an accessible virtual filesystem, or [`Soc`](soc::Soc),
//! which enables all network communications via sockets.
//!
//! In [`ctru-rs`](crate) some services only allow a single handle to be created at a time, to ensure a safe and controlled environment.
pub mod am; pub mod am;
pub mod apt; pub mod apt;
@ -44,7 +50,4 @@ cfg_if::cfg_if! {
} }
} }
pub use self::apt::Apt;
pub use self::hid::Hid;
pub(crate) use self::reference::ServiceReference; pub(crate) use self::reference::ServiceReference;

493
ctru-rs/src/services/ndsp/mod.rs

@ -1,7 +1,21 @@
//! NDSP (Audio) service //! NDSP (Audio) service.
//!
//! The NDSP service is used to handle communications to the DSP processor present on the console's motherboard.
//! Thanks to the DSP processor the program can play sound effects and music on the console's built-in speakers or to any audio device
//! connected via the audio jack.
//!
//! To use NDSP audio, you will need to dump DSP firmware from a real 3DS using
//! something like [DSP1](https://www.gamebrew.org/wiki/DSP1_3DS).
//!
//! `libctru` expects to find it at `sdmc:/3ds/dspfirm.cdc` when initializing the NDSP service.
#![doc(alias = "audio")]
// As a result of requiring DSP firmware to initialize, all of the doctests in
// this module are `no_run`, since Citra doesn't provide a stub for the DSP firmware:
// https://github.com/citra-emu/citra/issues/6111
pub mod wave; pub mod wave;
use wave::{Wave, WaveStatus}; use wave::{Status, Wave};
use crate::error::ResultCode; use crate::error::ResultCode;
use crate::services::ServiceReference; use crate::services::ServiceReference;
@ -14,60 +28,101 @@ use std::sync::Mutex;
const NUMBER_OF_CHANNELS: u8 = 24; const NUMBER_OF_CHANNELS: u8 = 24;
/// Audio output mode.
#[doc(alias = "ndspOutputMode")]
#[derive(Copy, Clone, Debug, PartialEq, Eq)] #[derive(Copy, Clone, Debug, PartialEq, Eq)]
#[repr(u32)] #[repr(u32)]
pub enum OutputMode { pub enum OutputMode {
/// Single-Channel.
Mono = ctru_sys::NDSP_OUTPUT_MONO, Mono = ctru_sys::NDSP_OUTPUT_MONO,
/// Dual-Channel.
Stereo = ctru_sys::NDSP_OUTPUT_STEREO, Stereo = ctru_sys::NDSP_OUTPUT_STEREO,
/// Surround.
Surround = ctru_sys::NDSP_OUTPUT_SURROUND, Surround = ctru_sys::NDSP_OUTPUT_SURROUND,
} }
/// PCM formats supported by the audio engine.
#[derive(Copy, Clone, Debug, PartialEq, Eq)] #[derive(Copy, Clone, Debug, PartialEq, Eq)]
#[repr(u32)] #[repr(u32)]
pub enum AudioFormat { pub enum AudioFormat {
/// PCM 8bit single-channel.
PCM8Mono = ctru_sys::NDSP_FORMAT_MONO_PCM8, PCM8Mono = ctru_sys::NDSP_FORMAT_MONO_PCM8,
/// PCM 16bit single-channel.
PCM16Mono = ctru_sys::NDSP_FORMAT_MONO_PCM16, PCM16Mono = ctru_sys::NDSP_FORMAT_MONO_PCM16,
/// PCM 8bit interleaved dual-channel.
PCM8Stereo = ctru_sys::NDSP_FORMAT_STEREO_PCM8, PCM8Stereo = ctru_sys::NDSP_FORMAT_STEREO_PCM8,
/// PCM 16bit interleaved dual-channel.
PCM16Stereo = ctru_sys::NDSP_FORMAT_STEREO_PCM16, PCM16Stereo = ctru_sys::NDSP_FORMAT_STEREO_PCM16,
} }
/// Representation of volume mix for a channel. /// Representation of the volume mix for a channel.
#[derive(Copy, Clone, Debug, PartialEq)] #[derive(Copy, Clone, Debug, PartialEq)]
pub struct AudioMix { pub struct AudioMix {
raw: [f32; 12], raw: [f32; 12],
} }
/// Auxiliary Device index.
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
#[repr(usize)]
pub enum AuxDevice {
/// Aux device with index 0.
Zero = 0,
/// Aux device with index 1.
One = 1,
}
/// Interpolation used between audio frames.
#[doc(alias = "ndspInterpType")]
#[derive(Copy, Clone, Debug, PartialEq, Eq)] #[derive(Copy, Clone, Debug, PartialEq, Eq)]
#[repr(u32)] #[repr(u32)]
pub enum InterpolationType { pub enum InterpolationType {
/// Polyphase interpolation.
Polyphase = ctru_sys::NDSP_INTERP_POLYPHASE, Polyphase = ctru_sys::NDSP_INTERP_POLYPHASE,
/// Linear interpolation.
Linear = ctru_sys::NDSP_INTERP_LINEAR, Linear = ctru_sys::NDSP_INTERP_LINEAR,
/// No interpolation.
None = ctru_sys::NDSP_INTERP_NONE, None = ctru_sys::NDSP_INTERP_NONE,
} }
/// Errors returned by [`ndsp`](self) functions.
#[non_exhaustive]
#[derive(Copy, Clone, Debug, PartialEq, Eq)] #[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum NdspError { pub enum Error {
/// Channel ID /// Channel with the specified ID does not exist.
InvalidChannel(u8), InvalidChannel(u8),
/// Channel ID /// Channel with the specified ID is already being used.
ChannelAlreadyInUse(u8), ChannelAlreadyInUse(u8),
/// Channel ID /// The wave is already busy playing in the channel with the specified ID.
WaveBusy(u8), WaveBusy(u8),
/// Sample amount requested, Max sample amount /// The sample amount requested was larger than the maximum.
SampleCountOutOfBounds(usize, usize), SampleCountOutOfBounds(usize, usize),
} }
/// NDSP Channel representation.
///
/// There are 24 individual channels in total and each can play a different audio [`Wave`] simultaneuosly.
///
/// # Default
///
/// NDSP initialises all channels with default values on initialization, but the developer is supposed to change these values to correctly work with the service.
///
/// In particular:
/// - Default audio format is set to [`AudioFormat::PCM16Mono`].
/// - Default sample rate is set to 1 Hz.
/// - Default interpolation type is set to [`InterpolationType::Polyphase`].
/// - Default mix is set to [`AudioMix::default()`]
///
/// The handle to a channel can be retrieved with [`Ndsp::channel()`]
pub struct Channel<'ndsp> { pub struct Channel<'ndsp> {
id: u8, id: u8,
_rf: RefMut<'ndsp, ()>, // we don't need to hold any data _rf: RefMut<'ndsp, ()>, // we don't need to hold any data
} }
static NDSP_ACTIVE: Mutex<usize> = Mutex::new(0); static NDSP_ACTIVE: Mutex<()> = Mutex::new(());
/// Handler of the DSP service and DSP processor. /// Handle to the DSP service.
/// ///
/// This is the main struct to handle audio playback using the 3DS' speakers and headphone jack. /// Only one handle for this service can exist at a time.
/// Only one "instance" of this struct can exist at a time.
pub struct Ndsp { pub struct Ndsp {
_service_handler: ServiceReference, _service_handler: ServiceReference,
channel_flags: [RefCell<()>; NUMBER_OF_CHANNELS as usize], channel_flags: [RefCell<()>; NUMBER_OF_CHANNELS as usize],
@ -78,12 +133,27 @@ impl Ndsp {
/// ///
/// # Errors /// # Errors
/// ///
/// This function will return an error if an instance of the `Ndsp` struct already exists /// This function will return an error if an instance of the [`Ndsp`] struct already exists
/// or if there are any issues during initialization. /// or if there are any issues during initialization (for example, DSP firmware
/// cannot be found. See [module documentation](super::ndsp) for more details.).
///
/// # Example
///
/// ```no_run
/// # use std::error::Error;
/// # fn main() -> Result<(), Box<dyn Error>> {
/// #
/// use ctru::services::ndsp::Ndsp;
///
/// let ndsp = Ndsp::new()?;
/// #
/// # Ok(())
/// # }
/// ```
#[doc(alias = "ndspInit")]
pub fn new() -> crate::Result<Self> { pub fn new() -> crate::Result<Self> {
let _service_handler = ServiceReference::new( let _service_handler = ServiceReference::new(
&NDSP_ACTIVE, &NDSP_ACTIVE,
false,
|| { || {
ResultCode(unsafe { ctru_sys::ndspInit() })?; ResultCode(unsafe { ctru_sys::ndspInit() })?;
@ -105,7 +175,22 @@ impl Ndsp {
/// # Errors /// # Errors
/// ///
/// An error will be returned if the channel ID is not between 0 and 23 or if the specified channel is already being used. /// An error will be returned if the channel ID is not between 0 and 23 or if the specified channel is already being used.
pub fn channel(&self, id: u8) -> std::result::Result<Channel, NdspError> { ///
/// # Example
///
/// ```no_run
/// # use std::error::Error;
/// # fn main() -> Result<(), Box<dyn Error>> {
/// #
/// use ctru::services::ndsp::Ndsp;
/// let ndsp = Ndsp::new()?;
///
/// let channel_0 = ndsp.channel(0)?;
/// #
/// # Ok(())
/// # }
/// ```
pub fn channel(&self, id: u8) -> std::result::Result<Channel, Error> {
let in_bounds = self.channel_flags.get(id as usize); let in_bounds = self.channel_flags.get(id as usize);
match in_bounds { match in_bounds {
@ -113,41 +198,145 @@ impl Ndsp {
let flag = ref_cell.try_borrow_mut(); let flag = ref_cell.try_borrow_mut();
match flag { match flag {
Ok(_rf) => Ok(Channel { id, _rf }), Ok(_rf) => Ok(Channel { id, _rf }),
Err(_) => Err(NdspError::ChannelAlreadyInUse(id)), Err(_) => Err(Error::ChannelAlreadyInUse(id)),
} }
} }
None => Err(NdspError::InvalidChannel(id)), None => Err(Error::InvalidChannel(id)),
} }
} }
/// Set the audio output mode. Defaults to `OutputMode::Stereo`. /// Set the audio output mode. Defaults to [`OutputMode::Stereo`].
///
/// # Example
///
/// ```no_run
/// # use std::error::Error;
/// # fn main() -> Result<(), Box<dyn Error>> {
/// #
/// use ctru::services::ndsp::{Ndsp, OutputMode};
/// let mut ndsp = Ndsp::new()?;
///
/// // Use dual-channel output.
/// ndsp.set_output_mode(OutputMode::Stereo);
/// #
/// # Ok(())
/// # }
/// ```
#[doc(alias = "ndspSetOutputMode")]
pub fn set_output_mode(&mut self, mode: OutputMode) { pub fn set_output_mode(&mut self, mode: OutputMode) {
unsafe { ctru_sys::ndspSetOutputMode(mode.into()) }; unsafe { ctru_sys::ndspSetOutputMode(mode.into()) };
} }
} }
impl Channel<'_> { impl Channel<'_> {
/// Reset the channel /// Reset the channel (clear the queue and reset parameters).
///
/// # Example
///
/// ```no_run
/// # use std::error::Error;
/// # fn main() -> Result<(), Box<dyn Error>> {
/// #
/// use ctru::services::ndsp::Ndsp;
/// let ndsp = Ndsp::new()?;
/// let mut channel_0 = ndsp.channel(0)?;
///
/// channel_0.reset();
/// #
/// # Ok(())
/// # }
/// ```
#[doc(alias = "ndspChnReset")]
pub fn reset(&mut self) { pub fn reset(&mut self) {
unsafe { ctru_sys::ndspChnReset(self.id.into()) }; unsafe { ctru_sys::ndspChnReset(self.id.into()) };
} }
/// Initialize the channel's parameters /// Initialize the channel's parameters with default values.
pub fn init_parameters(&self) { ///
/// # Example
///
/// ```no_run
/// # use std::error::Error;
/// # fn main() -> Result<(), Box<dyn Error>> {
/// #
/// use ctru::services::ndsp::Ndsp;
/// let ndsp = Ndsp::new()?;
/// let mut channel_0 = ndsp.channel(0)?;
///
/// channel_0.init_parameters();
/// #
/// # Ok(())
/// # }
/// ```
#[doc(alias = "ndspChnInitParams")]
pub fn init_parameters(&mut self) {
unsafe { ctru_sys::ndspChnInitParams(self.id.into()) }; unsafe { ctru_sys::ndspChnInitParams(self.id.into()) };
} }
/// Returns whether the channel is playing any audio. /// Returns whether the channel is playing any audio.
///
/// # Example
///
/// ```no_run
/// # use std::error::Error;
/// # fn main() -> Result<(), Box<dyn Error>> {
/// #
/// use ctru::services::ndsp::Ndsp;
/// let ndsp = Ndsp::new()?;
/// let mut channel_0 = ndsp.channel(0)?;
///
/// // The channel is not playing any audio.
/// assert!(!channel_0.is_playing());
/// #
/// # Ok(())
/// # }
/// ```
#[doc(alias = "ndspChnIsPlaying")]
pub fn is_playing(&self) -> bool { pub fn is_playing(&self) -> bool {
unsafe { ctru_sys::ndspChnIsPlaying(self.id.into()) } unsafe { ctru_sys::ndspChnIsPlaying(self.id.into()) }
} }
/// Returns whether the channel's playback is currently paused. /// Returns whether the channel's playback is currently paused.
///
/// # Example
///
/// ```no_run
/// # use std::error::Error;
/// # fn main() -> Result<(), Box<dyn Error>> {
/// #
/// use ctru::services::ndsp::Ndsp;
/// let ndsp = Ndsp::new()?;
/// let mut channel_0 = ndsp.channel(0)?;
///
/// // The channel is not paused.
/// assert!(!channel_0.is_paused());
/// #
/// # Ok(())
/// # }
/// ```
#[doc(alias = "ndspChnIsPaused")]
pub fn is_paused(&self) -> bool { pub fn is_paused(&self) -> bool {
unsafe { ctru_sys::ndspChnIsPaused(self.id.into()) } unsafe { ctru_sys::ndspChnIsPaused(self.id.into()) }
} }
// Returns the channel's id /// Returns the channel's index.
///
/// # Example
///
/// ```no_run
/// # use std::error::Error;
/// # fn main() -> Result<(), Box<dyn Error>> {
/// #
/// use ctru::services::ndsp::Ndsp;
/// let ndsp = Ndsp::new()?;
/// let mut channel_0 = ndsp.channel(0)?;
///
/// // The channel's index is 0.
/// assert_eq!(channel_0.id(), 0);
/// #
/// # Ok(())
/// # }
/// ```
pub fn id(&self) -> u8 { pub fn id(&self) -> u8 {
self.id self.id
} }
@ -155,46 +344,161 @@ impl Channel<'_> {
/// Returns the index of the currently played sample. /// Returns the index of the currently played sample.
/// ///
/// Because of how fast this value changes, it should only be used as a rough estimate of the current progress. /// Because of how fast this value changes, it should only be used as a rough estimate of the current progress.
#[doc(alias = "ndspChnGetSamplePos")]
pub fn sample_position(&self) -> usize { pub fn sample_position(&self) -> usize {
(unsafe { ctru_sys::ndspChnGetSamplePos(self.id.into()) }) as usize (unsafe { ctru_sys::ndspChnGetSamplePos(self.id.into()) }) as usize
} }
/// Returns the channel's current wave sequence's id. /// Returns the channel's current wave sequence's id.
#[doc(alias = "ndspChnGetWaveBufSeq")]
pub fn wave_sequence_id(&self) -> u16 { pub fn wave_sequence_id(&self) -> u16 {
unsafe { ctru_sys::ndspChnGetWaveBufSeq(self.id.into()) } unsafe { ctru_sys::ndspChnGetWaveBufSeq(self.id.into()) }
} }
/// Pause or un-pause the channel's playback. /// Pause or un-pause the channel's playback.
///
/// # Example
///
/// ```no_run
/// # use std::error::Error;
/// # fn main() -> Result<(), Box<dyn Error>> {
/// #
/// use ctru::services::ndsp::Ndsp;
/// let ndsp = Ndsp::new()?;
/// let mut channel_0 = ndsp.channel(0)?;
///
/// channel_0.set_paused(true);
///
/// // The channel is paused.
/// assert!(channel_0.is_paused());
/// #
/// # Ok(())
/// # }
/// ```
#[doc(alias = "ndspChnSetPaused")]
pub fn set_paused(&mut self, state: bool) { pub fn set_paused(&mut self, state: bool) {
unsafe { ctru_sys::ndspChnSetPaused(self.id.into(), state) }; unsafe { ctru_sys::ndspChnSetPaused(self.id.into(), state) };
} }
/// Set the channel's output format. /// Set the channel's output format.
/// Change this setting based on the used sample's format. ///
/// Change this setting based on the used wave's format.
///
/// # Example
///
/// ```no_run
/// # use std::error::Error;
/// # fn main() -> Result<(), Box<dyn Error>> {
/// #
/// use ctru::services::ndsp::{AudioFormat, Ndsp};
/// let ndsp = Ndsp::new()?;
/// let mut channel_0 = ndsp.channel(0)?;
///
/// // Use the PCM16 interleaved dual-channel audio format.
/// channel_0.set_format(AudioFormat::PCM16Stereo);
/// #
/// # Ok(())
/// # }
/// ```
// TODO: Channels treat all waves as equal and do not read their format when playing them. Another good reason to re-write the service.
#[doc(alias = "ndspChnSetFormat")]
pub fn set_format(&mut self, format: AudioFormat) { pub fn set_format(&mut self, format: AudioFormat) {
unsafe { ctru_sys::ndspChnSetFormat(self.id.into(), format.into()) }; unsafe { ctru_sys::ndspChnSetFormat(self.id.into(), format.into()) };
} }
/// Set the channel's interpolation mode. /// Set the channel's interpolation mode.
///
/// # Example
///
/// ```no_run
/// # use std::error::Error;
/// # fn main() -> Result<(), Box<dyn Error>> {
/// #
/// use ctru::services::ndsp::{InterpolationType, Ndsp};
/// let ndsp = Ndsp::new()?;
/// let mut channel_0 = ndsp.channel(0)?;
///
/// // Use linear interpolation within frames.
/// channel_0.set_interpolation(InterpolationType::Linear);
/// #
/// # Ok(())
/// # }
/// ```
#[doc(alias = "ndspChnSetInterp")]
pub fn set_interpolation(&mut self, interp_type: InterpolationType) { pub fn set_interpolation(&mut self, interp_type: InterpolationType) {
unsafe { ctru_sys::ndspChnSetInterp(self.id.into(), interp_type.into()) }; unsafe { ctru_sys::ndspChnSetInterp(self.id.into(), interp_type.into()) };
} }
/// Set the channel's volume mix. /// Set the channel's volume mix.
///
/// Look at [`AudioMix`] for more information on the volume mix.
///
/// # Example
///
/// ```no_run
/// # use std::error::Error;
/// # use std::default::Default;
/// # fn main() -> Result<(), Box<dyn Error>> {
/// #
/// use ctru::services::ndsp::{AudioMix, Ndsp};
/// let ndsp = Ndsp::new()?;
/// let mut channel_0 = ndsp.channel(0)?;
///
/// // Front-left and front-right channel maxed.
/// channel_0.set_mix(&AudioMix::default());
/// #
/// # Ok(())
/// # }
/// ```
#[doc(alias = "ndspChnSetMix")]
pub fn set_mix(&mut self, mix: &AudioMix) { pub fn set_mix(&mut self, mix: &AudioMix) {
unsafe { ctru_sys::ndspChnSetMix(self.id.into(), mix.as_raw().as_ptr().cast_mut()) } unsafe { ctru_sys::ndspChnSetMix(self.id.into(), mix.as_raw().as_ptr().cast_mut()) }
} }
/// Set the channel's rate of sampling. /// Set the channel's rate of sampling in hertz.
///
/// # Example
///
/// ```no_run
/// # use std::error::Error;
/// # fn main() -> Result<(), Box<dyn Error>> {
/// #
/// use ctru::services::ndsp::Ndsp;
/// let ndsp = Ndsp::new()?;
/// let mut channel_0 = ndsp.channel(0)?;
///
/// // Standard CD sample rate. (44100 Hz)
/// channel_0.set_sample_rate(44100.);
/// #
/// # Ok(())
/// # }
/// ```
#[doc(alias = "ndspChnSetRate")]
pub fn set_sample_rate(&mut self, rate: f32) { pub fn set_sample_rate(&mut self, rate: f32) {
unsafe { ctru_sys::ndspChnSetRate(self.id.into(), rate) }; unsafe { ctru_sys::ndspChnSetRate(self.id.into(), rate) };
} }
// `ndspChnSetAdpcmCoefs` isn't wrapped on purpose. // TODO: wrap ADPCM format helpers.
// DSPADPCM is a proprietary format used by Nintendo, unavailable by "normal" means.
// We suggest using other wave formats when developing homebrew applications.
/// Clear the wave buffer queue and stop playback. /// Clear the wave buffer queue and stop playback.
///
/// # Example
///
/// ```no_run
/// # use std::error::Error;
/// # fn main() -> Result<(), Box<dyn Error>> {
/// #
/// use ctru::services::ndsp::Ndsp;
/// let ndsp = Ndsp::new()?;
/// let mut channel_0 = ndsp.channel(0)?;
///
/// // Clear the audio queue and stop playback.
/// channel_0.clear_queue();
/// #
/// # Ok(())
/// # }
/// ```
#[doc(alias = "ndspChnWaveBufClear")]
pub fn clear_queue(&mut self) { pub fn clear_queue(&mut self) {
unsafe { ctru_sys::ndspChnWaveBufClear(self.id.into()) }; unsafe { ctru_sys::ndspChnWaveBufClear(self.id.into()) };
} }
@ -204,11 +508,39 @@ impl Channel<'_> {
/// ///
/// # Warning /// # Warning
/// ///
/// `libctru` expects the user to manually keep the info data (in this case [Wave]) alive during playback. /// `libctru` expects the user to manually keep the info data (in this case [`Wave`]) alive during playback.
/// To ensure safety, checks within [Wave] will clear the whole channel queue if any queued [Wave] is dropped prematurely. /// To ensure safety, checks within [`Wave`] will clear the whole channel queue if any queued [`Wave`] is dropped prematurely.
pub fn queue_wave(&mut self, wave: &mut Wave) -> std::result::Result<(), NdspError> { ///
/// # Example
///
/// ```no_run
/// # #![feature(allocator_api)]
/// # use std::error::Error;
/// # fn main() -> Result<(), Box<dyn Error>> {
/// #
/// # use ctru::linear::LinearAllocator;
/// use ctru::services::ndsp::wave::Wave;
/// use ctru::services::ndsp::{AudioFormat, Ndsp};
/// let ndsp = Ndsp::new()?;
/// let mut channel_0 = ndsp.channel(0)?;
///
/// # let audio_data = Box::new_in([0u8; 96], LinearAllocator);
///
/// // Provide your own audio data.
/// let mut wave = Wave::new(audio_data, AudioFormat::PCM16Stereo, false);
///
/// // Clear the audio queue and stop playback.
/// channel_0.queue_wave(&mut wave);
/// #
/// # Ok(())
/// # }
/// ```
// TODO: Find a better way to handle the wave lifetime problem.
// These "alive wave" shenanigans are the most substantial reason why I'd like to fully re-write this service in Rust.
#[doc(alias = "ndspChnWaveBufAdd")]
pub fn queue_wave(&mut self, wave: &mut Wave) -> std::result::Result<(), Error> {
match wave.status() { match wave.status() {
WaveStatus::Playing | WaveStatus::Queued => return Err(NdspError::WaveBusy(self.id)), Status::Playing | Status::Queued => return Err(Error::WaveBusy(self.id)),
_ => (), _ => (),
} }
@ -222,65 +554,74 @@ impl Channel<'_> {
/// Functions to handle audio filtering. /// Functions to handle audio filtering.
/// ///
/// Refer to [libctru](https://libctru.devkitpro.org/channel_8h.html#a1da3b363c2edfd318c92276b527daae6) for more info. /// Refer to [`libctru`](https://libctru.devkitpro.org/channel_8h.html#a1da3b363c2edfd318c92276b527daae6) for more info.
impl Channel<'_> { impl Channel<'_> {
/// Enables/disables monopole filters. /// Enables/disables monopole filters.
#[doc(alias = "ndspChnIirMonoSetEnable")]
pub fn iir_mono_set_enabled(&mut self, enable: bool) { pub fn iir_mono_set_enabled(&mut self, enable: bool) {
unsafe { ctru_sys::ndspChnIirMonoSetEnable(self.id.into(), enable) }; unsafe { ctru_sys::ndspChnIirMonoSetEnable(self.id.into(), enable) };
} }
/// Sets the monopole to be a high pass filter. /// Set the monopole to be a high pass filter.
/// ///
/// # Notes /// # Notes
/// ///
/// This is a lower quality filter than the Biquad alternative. /// This is a lower quality filter than the Biquad alternative.
#[doc(alias = "ndspChnIirMonoSetParamsHighPassFilter")]
pub fn iir_mono_set_params_high_pass_filter(&mut self, cut_off_freq: f32) { pub fn iir_mono_set_params_high_pass_filter(&mut self, cut_off_freq: f32) {
unsafe { ctru_sys::ndspChnIirMonoSetParamsHighPassFilter(self.id.into(), cut_off_freq) }; unsafe { ctru_sys::ndspChnIirMonoSetParamsHighPassFilter(self.id.into(), cut_off_freq) };
} }
/// Sets the monopole to be a low pass filter. /// Set the monopole to be a low pass filter.
/// ///
/// # Notes /// # Notes
/// ///
/// This is a lower quality filter than the Biquad alternative. /// This is a lower quality filter than the Biquad alternative.
#[doc(alias = "ndspChnIirMonoSetParamsLowPassFilter")]
pub fn iir_mono_set_params_low_pass_filter(&mut self, cut_off_freq: f32) { pub fn iir_mono_set_params_low_pass_filter(&mut self, cut_off_freq: f32) {
unsafe { ctru_sys::ndspChnIirMonoSetParamsLowPassFilter(self.id.into(), cut_off_freq) }; unsafe { ctru_sys::ndspChnIirMonoSetParamsLowPassFilter(self.id.into(), cut_off_freq) };
} }
/// Enables/disables biquad filters. /// Enables/disables biquad filters.
#[doc(alias = "ndspChnIirBiquadSetEnable")]
pub fn iir_biquad_set_enabled(&mut self, enable: bool) { pub fn iir_biquad_set_enabled(&mut self, enable: bool) {
unsafe { ctru_sys::ndspChnIirBiquadSetEnable(self.id.into(), enable) }; unsafe { ctru_sys::ndspChnIirBiquadSetEnable(self.id.into(), enable) };
} }
/// Sets the biquad to be a high pass filter. /// Set the biquad to be a high pass filter.
#[doc(alias = "ndspChnIirBiquadSetParamsHighPassFilter")]
pub fn iir_biquad_set_params_high_pass_filter(&mut self, cut_off_freq: f32, quality: f32) { pub fn iir_biquad_set_params_high_pass_filter(&mut self, cut_off_freq: f32, quality: f32) {
unsafe { unsafe {
ctru_sys::ndspChnIirBiquadSetParamsHighPassFilter(self.id.into(), cut_off_freq, quality) ctru_sys::ndspChnIirBiquadSetParamsHighPassFilter(self.id.into(), cut_off_freq, quality)
}; };
} }
/// Sets the biquad to be a low pass filter. /// Set the biquad to be a low pass filter.
#[doc(alias = "ndspChnIirBiquadSetParamsLowPassFilter")]
pub fn iir_biquad_set_params_low_pass_filter(&mut self, cut_off_freq: f32, quality: f32) { pub fn iir_biquad_set_params_low_pass_filter(&mut self, cut_off_freq: f32, quality: f32) {
unsafe { unsafe {
ctru_sys::ndspChnIirBiquadSetParamsLowPassFilter(self.id.into(), cut_off_freq, quality) ctru_sys::ndspChnIirBiquadSetParamsLowPassFilter(self.id.into(), cut_off_freq, quality)
}; };
} }
/// Sets the biquad to be a notch filter. /// Set the biquad to be a notch filter.
#[doc(alias = "ndspChnIirBiquadSetParamsNotchFilter")]
pub fn iir_biquad_set_params_notch_filter(&mut self, notch_freq: f32, quality: f32) { pub fn iir_biquad_set_params_notch_filter(&mut self, notch_freq: f32, quality: f32) {
unsafe { unsafe {
ctru_sys::ndspChnIirBiquadSetParamsNotchFilter(self.id.into(), notch_freq, quality) ctru_sys::ndspChnIirBiquadSetParamsNotchFilter(self.id.into(), notch_freq, quality)
}; };
} }
/// Sets the biquad to be a band pass filter. /// Set the biquad to be a band pass filter.
#[doc(alias = "ndspChnIirBiquadSetParamsBandPassFilter")]
pub fn iir_biquad_set_params_band_pass_filter(&mut self, mid_freq: f32, quality: f32) { pub fn iir_biquad_set_params_band_pass_filter(&mut self, mid_freq: f32, quality: f32) {
unsafe { unsafe {
ctru_sys::ndspChnIirBiquadSetParamsBandPassFilter(self.id.into(), mid_freq, quality) ctru_sys::ndspChnIirBiquadSetParamsBandPassFilter(self.id.into(), mid_freq, quality)
}; };
} }
/// Sets the biquad to be a peaking equalizer. /// Set the biquad to be a peaking equalizer.
#[doc(alias = "ndspChnIirBiquadSetParamsPeakingEqualizer")]
pub fn iir_biquad_set_params_peaking_equalizer( pub fn iir_biquad_set_params_peaking_equalizer(
&mut self, &mut self,
central_freq: f32, central_freq: f32,
@ -301,9 +642,10 @@ impl Channel<'_> {
impl AudioFormat { impl AudioFormat {
/// Returns the amount of bytes needed to store one sample /// Returns the amount of bytes needed to store one sample
/// ///
/// Eg. /// # Example
/// 8 bit mono formats return 1 (byte) ///
/// 16 bit stereo (dual-channel) formats return 4 (bytes) /// - 8 bit mono formats return 1 (byte)
/// - 16 bit stereo (dual-channel) formats return 4 (bytes)
pub const fn size(self) -> usize { pub const fn size(self) -> usize {
match self { match self {
Self::PCM8Mono => 1, Self::PCM8Mono => 1,
@ -314,7 +656,7 @@ impl AudioFormat {
} }
impl AudioMix { impl AudioMix {
/// Creates a new [AudioMix] with all volumes set to 0. /// Creates a new [`AudioMix`] with all volumes set to 0.
pub fn zeroed() -> Self { pub fn zeroed() -> Self {
Self { raw: [0.; 12] } Self { raw: [0.; 12] }
} }
@ -340,86 +682,70 @@ impl AudioMix {
} }
/// Returns the values set for the "front" volume mix (left and right channel) for the specified auxiliary output device (either 0 or 1). /// Returns the values set for the "front" volume mix (left and right channel) for the specified auxiliary output device (either 0 or 1).
pub fn aux_front(&self, id: usize) -> (f32, f32) { pub fn aux_front(&self, id: AuxDevice) -> (f32, f32) {
if id > 1 { let index = 4 + (id as usize * 4);
panic!("invalid auxiliary output device index")
}
let index = 4 + id * 4;
(self.raw[index], self.raw[index + 1]) (self.raw[index], self.raw[index + 1])
} }
/// Returns the values set for the "back" volume mix (left and right channel) for the specified auxiliary output device (either 0 or 1). /// Returns the values set for the "back" volume mix (left and right channel) for the specified auxiliary output device (either 0 or 1).
pub fn aux_back(&self, id: usize) -> (f32, f32) { pub fn aux_back(&self, id: AuxDevice) -> (f32, f32) {
if id > 1 { let index = 6 + (id as usize * 4);
panic!("invalid auxiliary output device index")
}
let index = 6 + id * 4;
(self.raw[index], self.raw[index + 1]) (self.raw[index], self.raw[index + 1])
} }
/// Sets the values for the "front" volume mix (left and right channel). /// Set the values for the "front" volume mix (left and right channel).
/// ///
/// # Notes /// # Notes
/// ///
/// [Channel] will normalize the mix values to be within 0 and 1. /// [`Channel`] will normalize the mix values to be within 0 and 1.
/// However, an [AudioMix] instance with larger/smaller values is valid. /// However, an [`AudioMix`] instance with larger/smaller values is valid.
pub fn set_front(&mut self, left: f32, right: f32) { pub fn set_front(&mut self, left: f32, right: f32) {
self.raw[0] = left; self.raw[0] = left;
self.raw[1] = right; self.raw[1] = right;
} }
/// Sets the values for the "back" volume mix (left and right channel). /// Set the values for the "back" volume mix (left and right channel).
/// ///
/// # Notes /// # Notes
/// ///
/// [Channel] will normalize the mix values to be within 0 and 1. /// [`Channel`] will normalize the mix values to be within 0 and 1.
/// However, an [AudioMix] instance with larger/smaller values is valid. /// However, an [`AudioMix`] instance with larger/smaller values is valid.
pub fn set_back(&mut self, left: f32, right: f32) { pub fn set_back(&mut self, left: f32, right: f32) {
self.raw[2] = left; self.raw[2] = left;
self.raw[3] = right; self.raw[3] = right;
} }
/// Sets the values for the "front" volume mix (left and right channel) for the specified auxiliary output device (either 0 or 1). /// Set the values for the "front" volume mix (left and right channel) for the specified auxiliary output device (either 0 or 1).
/// ///
/// # Notes /// # Notes
/// ///
/// [Channel] will normalize the mix values to be within 0 and 1. /// [`Channel`] will normalize the mix values to be within 0 and 1.
/// However, an [AudioMix] instance with larger/smaller values is valid. /// However, an [`AudioMix`] instance with larger/smaller values is valid.
pub fn set_aux_front(&mut self, left: f32, right: f32, id: usize) { pub fn set_aux_front(&mut self, left: f32, right: f32, id: AuxDevice) {
if id > 1 { let index = 4 + (id as usize * 4);
panic!("invalid auxiliary output device index")
}
let index = 4 + id * 4;
self.raw[index] = left; self.raw[index] = left;
self.raw[index + 1] = right; self.raw[index + 1] = right;
} }
/// Sets the values for the "back" volume mix (left and right channel) for the specified auxiliary output device (either 0 or 1). /// Set the values for the "back" volume mix (left and right channel) for the specified auxiliary output device (either 0 or 1).
/// ///
/// # Notes /// # Notes
/// ///
/// [Channel] will normalize the mix values to be within 0 and 1. /// [`Channel`] will normalize the mix values to be within 0 and 1.
/// However, an [AudioMix] instance with larger/smaller values is valid. /// However, an [`AudioMix`] instance with larger/smaller values is valid.
pub fn set_aux_back(&mut self, left: f32, right: f32, id: usize) { pub fn set_aux_back(&mut self, left: f32, right: f32, id: AuxDevice) {
if id > 1 { let index = 6 + (id as usize * 4);
panic!("invalid auxiliary output device index")
}
let index = 6 + id * 4;
self.raw[index] = left; self.raw[index] = left;
self.raw[index + 1] = right; self.raw[index + 1] = right;
} }
} }
/// Returns an [AudioMix] object with "front left" and "front right" volumes set to 100%, and all other volumes set to 0%.
impl Default for AudioMix { impl Default for AudioMix {
/// Returns an [`AudioMix`] object with "front left" and "front right" volumes set to 100%, and all other volumes set to 0%.
fn default() -> Self { fn default() -> Self {
let mut mix = AudioMix::zeroed(); let mut mix = AudioMix::zeroed();
mix.set_front(1.0, 1.0); mix.set_front(1.0, 1.0);
@ -434,7 +760,7 @@ impl From<[f32; 12]> for AudioMix {
} }
} }
impl fmt::Display for NdspError { impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self { match self {
Self::InvalidChannel(id) => write!(f, "audio Channel with ID {id} doesn't exist. Valid channels have an ID between 0 and 23"), Self::InvalidChannel(id) => write!(f, "audio Channel with ID {id} doesn't exist. Valid channels have an ID between 0 and 23"),
@ -445,9 +771,10 @@ impl fmt::Display for NdspError {
} }
} }
impl error::Error for NdspError {} impl error::Error for Error {}
impl Drop for Ndsp { impl Drop for Ndsp {
#[doc(alias = "ndspExit")]
fn drop(&mut self) { fn drop(&mut self) {
for i in 0..NUMBER_OF_CHANNELS { for i in 0..NUMBER_OF_CHANNELS {
self.channel(i).unwrap().reset(); self.channel(i).unwrap().reset();

99
ctru-rs/src/services/ndsp/wave.rs

@ -1,7 +1,13 @@
use super::{AudioFormat, NdspError}; //! Audio wave.
//!
//! This modules has all methods and structs required to work with audio waves meant to be played via the [`ndsp`](crate::services::ndsp) service.
use super::{AudioFormat, Error};
use crate::linear::LinearAllocator; use crate::linear::LinearAllocator;
/// Informational struct holding the raw audio data and playback info. This corresponds to [ctru_sys::ndspWaveBuf]. /// Informational struct holding the raw audio data and playback info.
///
/// You can play audio [`Wave`]s by using [`Channel::queue_wave()`](super::Channel::queue_wave).
pub struct Wave { pub struct Wave {
/// Data block of the audio wave (and its format information). /// Data block of the audio wave (and its format information).
buffer: Box<[u8], LinearAllocator>, buffer: Box<[u8], LinearAllocator>,
@ -13,16 +19,37 @@ pub struct Wave {
#[derive(Copy, Clone, Debug, PartialEq, Eq)] #[derive(Copy, Clone, Debug, PartialEq, Eq)]
#[repr(u8)] #[repr(u8)]
/// Enum representing the playback status of a [Wave]. /// Playback status of a [`Wave`].
pub enum WaveStatus { pub enum Status {
/// Wave has never been used.
Free = ctru_sys::NDSP_WBUF_FREE as u8, Free = ctru_sys::NDSP_WBUF_FREE as u8,
/// Wave is currently queued for usage.
Queued = ctru_sys::NDSP_WBUF_QUEUED as u8, Queued = ctru_sys::NDSP_WBUF_QUEUED as u8,
/// Wave is currently playing.
Playing = ctru_sys::NDSP_WBUF_PLAYING as u8, Playing = ctru_sys::NDSP_WBUF_PLAYING as u8,
/// Wave has finished playing.
Done = ctru_sys::NDSP_WBUF_DONE as u8, Done = ctru_sys::NDSP_WBUF_DONE as u8,
} }
impl Wave { impl Wave {
/// Build a new playable wave object from a raw buffer on LINEAR memory and a some info. /// Build a new playable wave object from a raw buffer on [LINEAR memory](`crate::linear`) and some info.
///
/// # Example
///
/// ```
/// # #![feature(allocator_api)]
/// # fn main() {
/// # let _runner = test_runner::GdbRunner::default();
/// #
/// use ctru::linear::LinearAllocator;
/// use ctru::services::ndsp::{AudioFormat, wave::Wave};
///
/// // Zeroed box allocated in the LINEAR memory.
/// let audio_data = Box::new_in([0u8; 96], LinearAllocator);
///
/// let wave = Wave::new(audio_data, AudioFormat::PCM16Stereo, false);
/// # }
/// ```
pub fn new( pub fn new(
buffer: Box<[u8], LinearAllocator>, buffer: Box<[u8], LinearAllocator>,
audio_format: AudioFormat, audio_format: AudioFormat,
@ -60,41 +87,61 @@ impl Wave {
} }
} }
/// Return a slice to the audio data (on the LINEAR memory). /// Returns a slice to the audio data (on the LINEAR memory).
pub fn get_buffer(&self) -> &[u8] { pub fn get_buffer(&self) -> &[u8] {
&self.buffer &self.buffer
} }
/// Return a mutable slice to the audio data (on the LINEAR memory). /// Returns a mutable slice to the audio data (on the LINEAR memory).
/// ///
/// # Errors /// # Errors
/// ///
/// This function will return an error if the [Wave] is currently busy, /// This function will return an error if the [`Wave`] is currently busy,
/// with the id to the channel in which it's queued. /// with the id to the channel in which it's queued.
pub fn get_buffer_mut(&mut self) -> Result<&mut [u8], NdspError> { pub fn get_buffer_mut(&mut self) -> Result<&mut [u8], Error> {
match self.status() { match self.status() {
WaveStatus::Playing | WaveStatus::Queued => { Status::Playing | Status::Queued => {
Err(NdspError::WaveBusy(self.played_on_channel.unwrap())) Err(Error::WaveBusy(self.played_on_channel.unwrap()))
} }
_ => Ok(&mut self.buffer), _ => Ok(&mut self.buffer),
} }
} }
/// Return this wave's playback status. /// Returns this wave's playback status.
pub fn status(&self) -> WaveStatus { ///
/// # Example
///
/// ```
/// # #![feature(allocator_api)]
/// # fn main() {
/// # let _runner = test_runner::GdbRunner::default();
/// #
/// # use ctru::linear::LinearAllocator;
/// # let _audio_data = Box::new_in([0u8; 96], LinearAllocator);
/// #
/// use ctru::services::ndsp::{AudioFormat, wave::{Wave, Status}};
///
/// // Provide your own audio data.
/// let wave = Wave::new(_audio_data, AudioFormat::PCM16Stereo, false);
///
/// // The `Wave` is free if never played before.
/// assert!(matches!(wave.status(), Status::Free));
/// # }
/// ```
pub fn status(&self) -> Status {
self.raw_data.status.try_into().unwrap() self.raw_data.status.try_into().unwrap()
} }
/// Get the amounts of samples *read* by the NDSP process. /// Returns the amount of samples *read* by the NDSP process.
/// ///
/// # Notes /// # Notes
/// ///
/// This value varies depending on [Self::set_sample_count]. /// This value varies depending on [`Wave::set_sample_count`].
pub fn sample_count(&self) -> usize { pub fn sample_count(&self) -> usize {
self.raw_data.nsamples as usize self.raw_data.nsamples as usize
} }
/// Get the format of the audio data. /// Returns the format of the audio data.
pub fn format(&self) -> AudioFormat { pub fn format(&self) -> AudioFormat {
self.audio_format self.audio_format
} }
@ -107,21 +154,21 @@ impl Wave {
} }
/// Set the amount of samples to be read. /// Set the amount of samples to be read.
/// This function doesn't resize the internal buffer.
/// ///
/// # Note /// # Note
/// ///
/// Operations of this kind are particularly useful to allocate memory pools ///
/// for VBR (Variable BitRate) Formats, like OGG Vorbis. /// This function doesn't resize the internal buffer. Operations of this kind are particularly useful to allocate memory pools
/// for VBR (Variable BitRate) formats, like OGG Vorbis.
/// ///
/// # Errors /// # Errors
/// ///
/// This function will return an error if the sample size exceeds the buffer's capacity /// This function will return an error if the sample size exceeds the buffer's capacity
/// or if the [Wave] is currently queued. /// or if the [`Wave`] is currently queued.
pub fn set_sample_count(&mut self, sample_count: usize) -> Result<(), NdspError> { pub fn set_sample_count(&mut self, sample_count: usize) -> Result<(), Error> {
match self.status() { match self.status() {
WaveStatus::Playing | WaveStatus::Queued => { Status::Playing | Status::Queued => {
return Err(NdspError::WaveBusy(self.played_on_channel.unwrap())); return Err(Error::WaveBusy(self.played_on_channel.unwrap()));
} }
_ => (), _ => (),
} }
@ -129,7 +176,7 @@ impl Wave {
let max_count = self.buffer.len() / self.audio_format.size(); let max_count = self.buffer.len() / self.audio_format.size();
if sample_count > max_count { if sample_count > max_count {
return Err(NdspError::SampleCountOutOfBounds(sample_count, max_count)); return Err(Error::SampleCountOutOfBounds(sample_count, max_count));
} }
self.raw_data.nsamples = sample_count as u32; self.raw_data.nsamples = sample_count as u32;
@ -138,7 +185,7 @@ impl Wave {
} }
} }
impl TryFrom<u8> for WaveStatus { impl TryFrom<u8> for Status {
type Error = &'static str; type Error = &'static str;
fn try_from(value: u8) -> Result<Self, Self::Error> { fn try_from(value: u8) -> Result<Self, Self::Error> {
@ -157,7 +204,7 @@ impl Drop for Wave {
// This was the only way I found I could check for improper drops of `Wave`. // This was the only way I found I could check for improper drops of `Wave`.
// A panic was considered, but it would cause issues with drop order against `Ndsp`. // A panic was considered, but it would cause issues with drop order against `Ndsp`.
match self.status() { match self.status() {
WaveStatus::Free | WaveStatus::Done => (), Status::Free | Status::Done => (),
// If the status flag is "unfinished" // If the status flag is "unfinished"
_ => { _ => {
// The unwrap is safe, since it must have a value in the case the status is "unfinished". // The unwrap is safe, since it must have a value in the case the status is "unfinished".

105
ctru-rs/src/services/ps.rs

@ -1,40 +1,81 @@
//! Process Services (PS) module. This is used for miscellaneous utility tasks, but //! Process Services.
//! is particularly important because it is used to generate random data, which //!
//! is required for common things like [`HashMap`](std::collections::HashMap). //! This service handles miscellaneous utility tasks used by the various processes.
//! However, it is particularly important because it is used to generate cryptographically secure random data, which
//! is required for commonly used functionality such as hashing (e.g. [`HashMap`](std::collections::HashMap) will not work without it).
//!
//! See also <https://www.3dbrew.org/wiki/Process_Services> //! See also <https://www.3dbrew.org/wiki/Process_Services>
use crate::error::ResultCode; use crate::error::ResultCode;
use crate::Result; use crate::Result;
/// Type of AES algorithm to use.
#[doc(alias = "PS_AESAlgorithm")]
#[derive(Debug, Clone, Copy, PartialEq, Eq)] #[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u32)] #[repr(u32)]
pub enum AESAlgorithm { pub enum AESAlgorithm {
/// CBC encryption.
CbcEnc = ctru_sys::PS_ALGORITHM_CBC_ENC, CbcEnc = ctru_sys::PS_ALGORITHM_CBC_ENC,
/// CBC decryption.
CbcDec = ctru_sys::PS_ALGORITHM_CBC_DEC, CbcDec = ctru_sys::PS_ALGORITHM_CBC_DEC,
/// CTR encryption.
CtrEnc = ctru_sys::PS_ALGORITHM_CTR_ENC, CtrEnc = ctru_sys::PS_ALGORITHM_CTR_ENC,
/// CTR decryption.
CtrDec = ctru_sys::PS_ALGORITHM_CTR_DEC, CtrDec = ctru_sys::PS_ALGORITHM_CTR_DEC,
/// CCM encryption.
CcmEnc = ctru_sys::PS_ALGORITHM_CCM_ENC, CcmEnc = ctru_sys::PS_ALGORITHM_CCM_ENC,
/// CCM decryption.
CcmDec = ctru_sys::PS_ALGORITHM_CCM_DEC, CcmDec = ctru_sys::PS_ALGORITHM_CCM_DEC,
} }
/// PS Key slot to use.
#[doc(alias = "PS_AESKeyType")]
#[derive(Debug, Clone, Copy, PartialEq, Eq)] #[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u32)] #[repr(u32)]
pub enum AESKeyType { pub enum AESKeyType {
/// Keyslot 0x0D.
Keyslot0D = ctru_sys::PS_KEYSLOT_0D, Keyslot0D = ctru_sys::PS_KEYSLOT_0D,
/// Keyslot 0x2D.
Keyslot2D = ctru_sys::PS_KEYSLOT_2D, Keyslot2D = ctru_sys::PS_KEYSLOT_2D,
/// Keyslot 0x2E.
Keyslot2E = ctru_sys::PS_KEYSLOT_2E, Keyslot2E = ctru_sys::PS_KEYSLOT_2E,
/// Keyslot 0x31.
Keyslot31 = ctru_sys::PS_KEYSLOT_31, Keyslot31 = ctru_sys::PS_KEYSLOT_31,
/// Keyslot 0x32.
Keyslot32 = ctru_sys::PS_KEYSLOT_32, Keyslot32 = ctru_sys::PS_KEYSLOT_32,
/// Keyslot 0x36.
Keyslot36 = ctru_sys::PS_KEYSLOT_36, Keyslot36 = ctru_sys::PS_KEYSLOT_36,
/// Keyslot 0x38.
Keyslot38 = ctru_sys::PS_KEYSLOT_38, Keyslot38 = ctru_sys::PS_KEYSLOT_38,
/// Keyslot 0x39 (DLP).
Keyslot39Dlp = ctru_sys::PS_KEYSLOT_39_DLP, Keyslot39Dlp = ctru_sys::PS_KEYSLOT_39_DLP,
/// Keyslot 0x39 (NFC).
Keyslot39Nfc = ctru_sys::PS_KEYSLOT_39_NFC, Keyslot39Nfc = ctru_sys::PS_KEYSLOT_39_NFC,
/// Invalid keyslot.
KeyslotInvalid = ctru_sys::PS_KEYSLOT_INVALID, KeyslotInvalid = ctru_sys::PS_KEYSLOT_INVALID,
} }
/// Handle to the PS service.
pub struct Ps(()); pub struct Ps(());
impl Ps { impl Ps {
/// Initialize a new service handle.
///
/// # Example
///
/// ```
/// # let _runner = test_runner::GdbRunner::default();
/// # use std::error::Error;
/// # fn main() -> Result<(), Box<dyn Error>> {
/// #
/// use ctru::services::ps::Ps;
///
/// let ps = Ps::new()?;
/// #
/// # Ok(())
/// # }
/// ```
#[doc(alias = "psInit")]
pub fn new() -> Result<Self> { pub fn new() -> Result<Self> {
unsafe { unsafe {
ResultCode(ctru_sys::psInit())?; ResultCode(ctru_sys::psInit())?;
@ -42,6 +83,24 @@ impl Ps {
} }
} }
/// Returns the console's local friend code seed.
///
/// # Example
///
/// ```
/// # let _runner = test_runner::GdbRunner::default();
/// # use std::error::Error;
/// # fn main() -> Result<(), Box<dyn Error>> {
/// #
/// use ctru::services::ps::Ps;
/// let ps = Ps::new()?;
///
/// let friend_code_seed = ps.local_friend_code_seed()?;
/// #
/// # Ok(())
/// # }
/// ```
#[doc(alias = "PS_GetLocalFriendCodeSeed")]
pub fn local_friend_code_seed(&self) -> crate::Result<u64> { pub fn local_friend_code_seed(&self) -> crate::Result<u64> {
let mut seed: u64 = 0; let mut seed: u64 = 0;
@ -49,6 +108,24 @@ impl Ps {
Ok(seed) Ok(seed)
} }
/// Returns the console's devide ID.
///
/// # Example
///
/// ```
/// # let _runner = test_runner::GdbRunner::default();
/// # use std::error::Error;
/// # fn main() -> Result<(), Box<dyn Error>> {
/// #
/// use ctru::services::ps::Ps;
/// let ps = Ps::new()?;
///
/// let device_id = ps.device_id()?;
/// #
/// # Ok(())
/// # }
/// ```
#[doc(alias = "PS_GetDeviceId")]
pub fn device_id(&self) -> crate::Result<u32> { pub fn device_id(&self) -> crate::Result<u32> {
let mut id: u32 = 0; let mut id: u32 = 0;
@ -56,6 +133,27 @@ impl Ps {
Ok(id) Ok(id)
} }
/// Generates cryptografically secure random bytes and writes them into the `out` buffer.
///
/// # Example
///
/// ```
/// # let _runner = test_runner::GdbRunner::default();
/// # use std::error::Error;
/// # fn main() -> Result<(), Box<dyn Error>> {
/// #
/// use ctru::services::ps::Ps;
/// let ps = Ps::new()?;
///
/// let mut buffer = vec![0; 128];
///
/// // The buffer is now randomized!
/// ps.generate_random_bytes(&mut buffer)?;
/// #
/// # Ok(())
/// # }
/// ```
#[doc(alias = "PS_GenerateRandomBytes")]
pub fn generate_random_bytes(&self, out: &mut [u8]) -> crate::Result<()> { pub fn generate_random_bytes(&self, out: &mut [u8]) -> crate::Result<()> {
ResultCode(unsafe { ResultCode(unsafe {
ctru_sys::PS_GenerateRandomBytes(out.as_mut_ptr().cast(), out.len()) ctru_sys::PS_GenerateRandomBytes(out.as_mut_ptr().cast(), out.len())
@ -65,6 +163,7 @@ impl Ps {
} }
impl Drop for Ps { impl Drop for Ps {
#[doc(alias = "psExit")]
fn drop(&mut self) { fn drop(&mut self) {
unsafe { unsafe {
ctru_sys::psExit(); ctru_sys::psExit();

42
ctru-rs/src/services/reference.rs

@ -1,36 +1,37 @@
use crate::Error; use crate::Error;
use std::sync::Mutex; use std::sync::{Mutex, MutexGuard, TryLockError};
pub(crate) struct ServiceReference { pub(crate) struct ServiceReference {
counter: &'static Mutex<usize>, _guard: MutexGuard<'static, ()>,
close: Box<dyn Fn() + Send + Sync>, close: Box<dyn Fn() + Send + Sync>,
} }
impl ServiceReference { impl ServiceReference {
pub fn new<S, E>( pub fn new<S, E>(counter: &'static Mutex<()>, start: S, close: E) -> crate::Result<Self>
counter: &'static Mutex<usize>,
allow_multiple: bool,
start: S,
close: E,
) -> crate::Result<Self>
where where
S: FnOnce() -> crate::Result<()>, S: FnOnce() -> crate::Result<()>,
E: Fn() + Send + Sync + 'static, E: Fn() + Send + Sync + 'static,
{ {
let mut value = counter let _guard = match counter.try_lock() {
.lock() Ok(lock) => lock,
.expect("Mutex Counter for ServiceReference is poisoned"); // todo: handle poisoning Err(e) => match e {
TryLockError::Poisoned(guard) => {
// If the MutexGuard is poisoned that means that the "other" service instance (of which the thread panicked)
// was NOT properly closed. To avoid any weird behaviour, we try closing the service now, to then re-open a fresh instance.
//
// It's up to our `close()` implementations to avoid panicking/doing weird stuff again.
close();
if *value == 0 { guard.into_inner()
start()?;
} else if !allow_multiple {
return Err(Error::ServiceAlreadyActive);
} }
TryLockError::WouldBlock => return Err(Error::ServiceAlreadyActive),
},
};
*value += 1; start()?;
Ok(Self { Ok(Self {
counter, _guard,
close: Box::new(close), close: Box::new(close),
}) })
} }
@ -38,13 +39,6 @@ impl ServiceReference {
impl Drop for ServiceReference { impl Drop for ServiceReference {
fn drop(&mut self) { fn drop(&mut self) {
let mut value = self
.counter
.lock()
.expect("Mutex Counter for ServiceReference is poisoned"); // todo: handle poisoning
*value -= 1;
if *value == 0 {
(self.close)(); (self.close)();
} }
} }
}

61
ctru-rs/src/services/romfs.rs

@ -1,9 +1,14 @@
//! Read-Only Memory FileSystem //! Read-Only Memory FileSystem service.
//!
//! This service lets the application access a virtual mounted device created using a folder included within the application bundle.
//! After mounting the RomFS file system, the included files and folders will be accessible exactly like any other file, just by using the drive prefix `romfs:/<file-path>`.
//!
//! # Usage
//! //!
//! This module only gets compiled if the configured RomFS directory is found and the `romfs` //! This module only gets compiled if the configured RomFS directory is found and the `romfs`
//! feature is enabled. //! feature is enabled.
//! //!
//! Configure the path in Cargo.toml (the default path is "romfs"). Paths are relative to the //! Configure the path in your project's `Cargo.toml` manifest (the default path is "romfs"). Paths are relative to the
//! `CARGO_MANIFEST_DIR` environment variable, which is the directory containing the manifest of //! `CARGO_MANIFEST_DIR` environment variable, which is the directory containing the manifest of
//! your package. //! your package.
//! //!
@ -11,6 +16,16 @@
//! [package.metadata.cargo-3ds] //! [package.metadata.cargo-3ds]
//! romfs_dir = "romfs" //! romfs_dir = "romfs"
//! ``` //! ```
//!
//! Alternatively, you can include the RomFS archive manually when building with `3dsxtool`.
//!
//! # Notes
//!
//! `std::path` has problems when parsing file paths that include the `romfs:` prefix.
//! As such, it's suggested to use the paths directly or to do simple append operations to avoid unexpected behaviour.
//! Related [issue](https://github.com/rust-lang/rust/issues/52331).
#![doc(alias = "embed")]
#![doc(alias = "filesystem")]
use crate::error::ResultCode; use crate::error::ResultCode;
use std::ffi::CStr; use std::ffi::CStr;
@ -18,17 +33,37 @@ use std::sync::Mutex;
use crate::services::ServiceReference; use crate::services::ServiceReference;
/// Handle to the RomFS service.
pub struct RomFS { pub struct RomFS {
_service_handler: ServiceReference, _service_handler: ServiceReference,
} }
static ROMFS_ACTIVE: Mutex<usize> = Mutex::new(0); static ROMFS_ACTIVE: Mutex<()> = Mutex::new(());
impl RomFS { impl RomFS {
/// Mount the bundled RomFS archive as a virtual drive.
///
/// # Example
///
/// ```
/// # let _runner = test_runner::GdbRunner::default();
/// # use std::error::Error;
/// # fn main() -> Result<(), Box<dyn Error>> {
/// #
/// use ctru::services::romfs::RomFS;
///
/// let romfs = RomFS::new()?;
///
/// // Remember to include the RomFS archive and to use your actual files!
/// let contents = std::fs::read_to_string("romfs:/test-file.txt");
/// #
/// # Ok(())
/// # }
/// ```
#[doc(alias = "romfsMountSelf")]
pub fn new() -> crate::Result<Self> { pub fn new() -> crate::Result<Self> {
let _service_handler = ServiceReference::new( let _service_handler = ServiceReference::new(
&ROMFS_ACTIVE, &ROMFS_ACTIVE,
true,
|| { || {
let mount_name = CStr::from_bytes_with_nul(b"romfs\0").unwrap(); let mount_name = CStr::from_bytes_with_nul(b"romfs\0").unwrap();
ResultCode(unsafe { ctru_sys::romfsMountSelf(mount_name.as_ptr()) })?; ResultCode(unsafe { ctru_sys::romfsMountSelf(mount_name.as_ptr()) })?;
@ -36,7 +71,7 @@ impl RomFS {
}, },
|| { || {
let mount_name = CStr::from_bytes_with_nul(b"romfs\0").unwrap(); let mount_name = CStr::from_bytes_with_nul(b"romfs\0").unwrap();
unsafe { ctru_sys::romfsUnmount(mount_name.as_ptr()) }; let _ = unsafe { ctru_sys::romfsUnmount(mount_name.as_ptr()) };
}, },
)?; )?;
@ -48,17 +83,15 @@ impl RomFS {
mod tests { mod tests {
use super::*; use super::*;
// NOTE: this test only passes when run with a .3dsx, which for now requires separate build
// and run steps so the 3dsx is built before the runner looks for the executable
#[test] #[test]
fn romfs_counter() { #[should_panic]
let _romfs = RomFS::new().unwrap(); fn romfs_lock() {
let value = *ROMFS_ACTIVE.lock().unwrap(); let romfs = RomFS::new().unwrap();
assert_eq!(value, 1);
drop(_romfs);
let value = *ROMFS_ACTIVE.lock().unwrap(); ROMFS_ACTIVE.try_lock().unwrap();
assert_eq!(value, 0); drop(romfs);
} }
} }

111
ctru-rs/src/services/soc.rs

@ -1,4 +1,9 @@
//! Network Socket //! Network Socket service.
//!
//! By using this service the program enables the use of network sockets and utilities such as those found in `std::net`, which are completely inaccessible by default.
//! As such, remember to hold a handle to this service handle while using any network functionality, or else the `std::net` methods will return generic OS errors.
#![doc(alias = "socket")]
#![doc(alias = "network")]
use libc::memalign; use libc::memalign;
use std::net::Ipv4Addr; use std::net::Ipv4Addr;
@ -8,37 +13,66 @@ use crate::error::ResultCode;
use crate::services::ServiceReference; use crate::services::ServiceReference;
use crate::Error; use crate::Error;
/// Network socket service /// Handle to the Network Socket service.
///
/// Initializing this service will enable the use of network sockets and utilities
/// such as those found in `std::net`. The service will close once this struct gets dropped.
pub struct Soc { pub struct Soc {
_service_handler: ServiceReference, _service_handler: ServiceReference,
sock_3dslink: libc::c_int, sock_3dslink: libc::c_int,
} }
static SOC_ACTIVE: Mutex<usize> = Mutex::new(0); static SOC_ACTIVE: Mutex<()> = Mutex::new(());
impl Soc { impl Soc {
/// Initialize the Soc service with a default buffer size of 0x100000 bytes /// Initialize a new service handle using a socket buffer size of `0x100000` bytes.
/// ///
/// # Errors /// # Errors
/// ///
/// This function will return an error if the `Soc` service is already initialized /// This function will return an error if the [`Soc`] service is already being used.
///
/// # Example
///
/// ```
/// # let _runner = test_runner::GdbRunner::default();
/// # use std::error::Error;
/// # fn main() -> Result<(), Box<dyn Error>> {
/// #
/// use ctru::services::soc::Soc;
///
/// let soc = Soc::new()?;
/// #
/// # Ok(())
/// # }
/// ```
#[doc(alias = "socInit")]
pub fn new() -> crate::Result<Self> { pub fn new() -> crate::Result<Self> {
Self::init_with_buffer_size(0x100000) Self::init_with_buffer_size(0x100000)
} }
/// Initialize the Soc service with a custom buffer size in bytes. The size should be /// Initialize a new service handle using a custom socket buffer size.
/// 0x100000 bytes or greater. ///
/// The size should be `0x100000` bytes or greater.
/// ///
/// # Errors /// # Errors
/// ///
/// This function will return an error if the `Soc` service is already initialized /// This function will return an error if the [`Soc`] service is already being used.
///
/// # Example
///
/// ```
/// # let _runner = test_runner::GdbRunner::default();
/// # use std::error::Error;
/// # fn main() -> Result<(), Box<dyn Error>> {
/// #
/// use ctru::services::soc::Soc;
///
/// let soc = Soc::init_with_buffer_size(0x100000)?;
/// #
/// # Ok(())
/// # }
/// ```
#[doc(alias = "socInit")]
pub fn init_with_buffer_size(num_bytes: usize) -> crate::Result<Self> { pub fn init_with_buffer_size(num_bytes: usize) -> crate::Result<Self> {
let _service_handler = ServiceReference::new( let _service_handler = ServiceReference::new(
&SOC_ACTIVE, &SOC_ACTIVE,
false,
|| { || {
let soc_mem = unsafe { memalign(0x1000, num_bytes) } as *mut u32; let soc_mem = unsafe { memalign(0x1000, num_bytes) } as *mut u32;
ResultCode(unsafe { ctru_sys::socInit(soc_mem, num_bytes as u32) })?; ResultCode(unsafe { ctru_sys::socInit(soc_mem, num_bytes as u32) })?;
@ -60,19 +94,61 @@ impl Soc {
}) })
} }
/// IP Address of the Nintendo 3DS system. /// Returns the local IP Address of the Nintendo 3DS system.
///
/// # Example
///
/// ```
/// # let _runner = test_runner::GdbRunner::default();
/// # use std::error::Error;
/// # fn main() -> Result<(), Box<dyn Error>> {
/// #
/// use ctru::services::soc::Soc;
/// let soc = Soc::new()?;
///
/// let address = soc.host_address();
/// #
/// # Ok(())
/// # }
/// ```
#[doc(alias = "gethostid")]
pub fn host_address(&self) -> Ipv4Addr { pub fn host_address(&self) -> Ipv4Addr {
let raw_id = unsafe { libc::gethostid() }; let raw_id = unsafe { libc::gethostid() };
Ipv4Addr::from(raw_id.to_ne_bytes()) Ipv4Addr::from(raw_id.to_ne_bytes())
} }
/// Redirect output streams (i.e. [`println`] and [`eprintln`]) to the `3dslink` server. /// Redirect output streams (i.e. `stdout` and `stderr`) to the `3dslink` server.
/// Requires `3dslink` >= 0.6.1 and `new-hbmenu` >= 2.3.0. ///
/// With this redirection it is possible to send (and view in real time) the output of `stdout` operations,
/// such as `println!` or `dbg!`.
///
/// Requires `3dslink` >= 0.6.1 and `new-hbmenu` >= 2.3.0 and the use of the `--server` flag.
/// The `--server` flag is also availble to use via `cargo-3ds` if the requirements are met.
/// ///
/// # Errors /// # Errors
/// ///
/// Returns an error if a connection cannot be established to the server, or /// Returns an error if a connection cannot be established to the server,
/// output was already previously redirected. /// or if the output was already being redirected.
///
/// # Example
///
/// ```
/// # let _runner = test_runner::GdbRunner::default();
/// # use std::error::Error;
/// # fn main() -> Result<(), Box<dyn Error>> {
/// #
/// use ctru::services::soc::Soc;
/// let mut soc = Soc::new()?;
///
/// // Redirect to the `3dslink` server that sent this program.
/// let address = soc.redirect_to_3dslink(true, true)?;
///
/// println!("I'm visible from a PC!");
/// #
/// # Ok(())
/// # }
/// ```
#[doc(alias = "link3dsConnectToHost")]
pub fn redirect_to_3dslink(&mut self, stdout: bool, stderr: bool) -> crate::Result<()> { pub fn redirect_to_3dslink(&mut self, stdout: bool, stderr: bool) -> crate::Result<()> {
if self.sock_3dslink >= 0 { if self.sock_3dslink >= 0 {
return Err(Error::OutputAlreadyRedirected); return Err(Error::OutputAlreadyRedirected);
@ -92,6 +168,7 @@ impl Soc {
} }
impl Drop for Soc { impl Drop for Soc {
#[doc(alias = "socExit")]
fn drop(&mut self) { fn drop(&mut self) {
if self.sock_3dslink >= 0 { if self.sock_3dslink >= 0 {
unsafe { unsafe {

22
ctru-rs/src/services/sslc.rs

@ -1,13 +1,30 @@
//! SSLC (TLS) service //! SSLC (TLS) service.
// TODO: Implement remaining functions // TODO: Implement remaining functions
use crate::error::ResultCode; use crate::error::ResultCode;
/// Handle to the SSLC service.
pub struct SslC(()); pub struct SslC(());
impl SslC { impl SslC {
/// Initialize the service /// Initialize a new service handle.
///
/// # Example
///
/// ```
/// # let _runner = test_runner::GdbRunner::default();
/// # use std::error::Error;
/// # fn main() -> Result<(), Box<dyn Error>> {
/// #
/// use ctru::services::sslc::SslC;
///
/// let sslc = SslC::new()?;
/// #
/// # Ok(())
/// # }
/// ```
#[doc(alias = "sslcInit")]
pub fn new() -> crate::Result<Self> { pub fn new() -> crate::Result<Self> {
unsafe { unsafe {
ResultCode(ctru_sys::sslcInit(0))?; ResultCode(ctru_sys::sslcInit(0))?;
@ -17,6 +34,7 @@ impl SslC {
} }
impl Drop for SslC { impl Drop for SslC {
#[doc(alias = "sslcExit")]
fn drop(&mut self) { fn drop(&mut self) {
unsafe { ctru_sys::sslcExit() }; unsafe { ctru_sys::sslcExit() };
} }

73
ctru-rs/src/test_runner.rs

@ -1,73 +0,0 @@
//! Custom test runner for building/running unit tests on the 3DS.
extern crate test;
use std::io;
use test::{ColorConfig, OutputFormat, TestDescAndFn, TestFn, TestOpts};
use crate::prelude::*;
/// A custom runner to be used with `#[test_runner]`. This simple implementation
/// runs all tests in series, "failing" on the first one to panic (really, the
/// panic is just treated the same as any normal application panic).
pub(crate) fn run(tests: &[&TestDescAndFn]) {
let gfx = Gfx::new().unwrap();
let mut hid = Hid::new().unwrap();
let apt = Apt::new().unwrap();
let mut top_screen = gfx.top_screen.borrow_mut();
top_screen.set_wide_mode(true);
let _console = Console::new(top_screen);
let opts = TestOpts {
force_run_in_process: true,
run_tests: true,
// TODO: color doesn't work because of TERM/TERMINFO.
// With RomFS we might be able to fake this out nicely...
color: ColorConfig::AutoColor,
format: OutputFormat::Pretty,
// Hopefully this interface is more stable vs specifying individual options,
// and parsing the empty list of args should always work, I think.
// TODO Ideally we could pass actual std::env::args() here too
..test::test::parse_opts(&[]).unwrap().unwrap()
};
// Use the default test implementation with our hardcoded options
let _success = run_static_tests(&opts, tests).unwrap();
// Make sure the user can actually see the results before we exit
println!("Press START to exit.");
while apt.main_loop() {
gfx.wait_for_vblank();
hid.scan_input();
if hid.keys_down().contains(KeyPad::START) {
break;
}
}
}
/// Adapted from [`test::test_main_static`] and [`test::make_owned_test`].
fn run_static_tests(opts: &TestOpts, tests: &[&TestDescAndFn]) -> io::Result<bool> {
let tests = tests.iter().map(make_owned_test).collect();
test::run_tests_console(opts, tests)
}
/// Clones static values for putting into a dynamic vector, which test_main()
/// needs to hand out ownership of tests to parallel test runners.
///
/// This will panic when fed any dynamic tests, because they cannot be cloned.
fn make_owned_test(test: &&TestDescAndFn) -> TestDescAndFn {
match test.testfn {
TestFn::StaticTestFn(f) => TestDescAndFn {
testfn: TestFn::StaticTestFn(f),
desc: test.desc.clone(),
},
TestFn::StaticBenchFn(f) => TestDescAndFn {
testfn: TestFn::StaticBenchFn(f),
desc: test.desc.clone(),
},
_ => panic!("non-static tests passed to test::test_main_static"),
}
}

15
ctru-sys/Cargo.toml

@ -1,7 +1,12 @@
[package] [package]
name = "ctru-sys" name = "ctru-sys"
version = "21.2.0+2.1.2-1" version = "22.2.0+2.2.2-1"
authors = ["Rust3DS Org", "Ronald Kinard <furyhunter600@gmail.com>"] authors = ["Rust3DS Org", "Ronald Kinard <furyhunter600@gmail.com>"]
description = "Raw bindings to libctru"
repository = "https://github.com/rust3ds/ctru-rs"
keywords = ["3ds", "libctru"]
categories = ["os", "external-ffi-bindings", "no-std", "hardware-support"]
exclude = ["src/.gitattributes"]
license = "Zlib" license = "Zlib"
links = "ctru" links = "ctru"
edition = "2021" edition = "2021"
@ -10,4 +15,12 @@ edition = "2021"
libc = { version = "0.2.121", default-features = false } libc = { version = "0.2.121", default-features = false }
[build-dependencies] [build-dependencies]
bindgen = { version = "0.65.1", features = ["experimental"] }
cc = "1.0"
doxygen-rs = "0.4.2"
which = "4.4.0" which = "4.4.0"
[package.metadata.docs.rs]
default-target = "armv6k-nintendo-3ds"
targets = []
cargo-args = ["-Z", "build-std"]

33
ctru-sys/README.md

@ -0,0 +1,33 @@
# ctru-sys
Raw Rust bindings over the [`libctru`](https://github.com/devkitPro/libctru) C library.
## Requirements
To use the bindings provided by this crate you will need to link against the [`libctru`](https://github.com/devkitPro/libctru) library.
Consult the [`ctru-rs` wiki](https://github.com/rust3ds/ctru-rs/wiki/Getting-Started) to learn how to obtain the required packages
to use this library.
## Version
This crate's version changes according to the version of `libctru`
used to generate the bindings, with the following convention:
* [`libctru`](https://github.com/devkitPro/libctru) version `X.Y.Z-W`
* `ctru-sys` version `XY.Z.P+X.Y.Z-W`
where `P` is usually 0 but may be incremented for fixes in e.g.
binding generation, `libc` dependency bump, etc.
It may be possible to build this crate against a different version of [`libctru`](https://github.com/devkitPro/libctru),
but you may encounter linker errors or ABI issues. A build-time Cargo warning
(displayed when built with `-vv`) will be issued if the build script detects
a mismatch or is unable to check the installed [`libctru`](https://github.com/devkitPro/libctru) version.
## Generating bindings
Bindings of new versions of [`libctru`](https://github.com/devkitPro/libctru) can be built using the integrated [`bindgen.sh`](./bindgen.sh) script.
## License
This project is distributed under the Zlib license.

57
ctru-sys/bindgen.sh

@ -1,57 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
echo "Determining libctru version..."
pacman=dkp-pacman
if ! command -v $pacman &>/dev/null; then
pacman=pacman
if ! command -v $pacman &>/dev/null; then
echo >&2 "ERROR: Unable to automatically determine libctru version!"
exit 1
fi
fi
LIBCTRU_VERSION="$($pacman -Qi libctru | grep Version | cut -d: -f 2 | tr -d ' ')"
CTRU_SYS_VERSION="$(
printf '%s' "$LIBCTRU_VERSION" |
cut -d- -f1 |
sed -E 's/^([0-9]+)\.([0-9.]+)$/\1\2/'
)"
echo "Generating bindings.rs..."
bindgen "$DEVKITPRO/libctru/include/3ds.h" \
--rust-target nightly \
--use-core \
--distrust-clang-mangling \
--must-use-type 'Result' \
--no-layout-tests \
--ctypes-prefix "::libc" \
--no-prepend-enum-name \
--generate "functions,types,vars" \
--blocklist-type "u(8|16|32|64)" \
--blocklist-type "__builtin_va_list" \
--blocklist-type "__va_list" \
--opaque-type "MiiData" \
--with-derive-default \
-- \
--target=arm-none-eabi \
--sysroot="$DEVKITARM/arm-none-eabi" \
-isystem"$DEVKITARM/arm-none-eabi/include" \
-I"$DEVKITPRO/libctru/include" \
-mfloat-abi=hard \
-march=armv6k \
-mtune=mpcore \
-mfpu=vfp \
-DARM11 \
-D__3DS__ \
> src/bindings.rs
echo "Updating docstrings in bindings.rs..."
cargo run --quiet --package docstring-to-rustdoc -- src/bindings.rs
echo "Formatting generated files..."
cargo fmt --all
echo "Generated bindings for ctru-sys version \"${CTRU_SYS_VERSION}.x+${LIBCTRU_VERSION}\""

119
ctru-sys/build.rs

@ -1,14 +1,29 @@
use bindgen::callbacks::ParseCallbacks;
use bindgen::{Builder, RustTarget};
use std::env; use std::env;
use std::error::Error; use std::error::Error;
use std::path::{Path, PathBuf};
use std::process::{Command, Output, Stdio}; use std::process::{Command, Output, Stdio};
#[derive(Debug)]
struct CustomCallbacks;
impl ParseCallbacks for CustomCallbacks {
fn process_comment(&self, comment: &str) -> Option<String> {
Some(doxygen_rs::transform(comment))
}
}
fn main() { fn main() {
let dkp_path = env::var("DEVKITPRO").unwrap(); let devkitpro = env::var("DEVKITPRO").unwrap();
let devkitarm = env::var("DEVKITARM").unwrap();
let profile = env::var("PROFILE").unwrap(); let profile = env::var("PROFILE").unwrap();
let out_dir = PathBuf::from(env::var("OUT_DIR").unwrap());
println!("cargo:rerun-if-changed=build.rs"); println!("cargo:rerun-if-changed=build.rs");
println!("cargo:rerun-if-env-changed=DEVKITPRO"); println!("cargo:rerun-if-env-changed=DEVKITPRO");
println!("cargo:rustc-link-search=native={dkp_path}/libctru/lib"); println!("cargo:rustc-link-search=native={devkitpro}/libctru/lib");
println!( println!(
"cargo:rustc-link-lib=static={}", "cargo:rustc-link-lib=static={}",
match profile.as_str() { match profile.as_str() {
@ -30,9 +45,97 @@ fn main() {
} }
Err(err) => println!("cargo:warning=failed to check libctru version: {err}"), Err(err) => println!("cargo:warning=failed to check libctru version: {err}"),
} }
let gcc_version = get_gcc_version(PathBuf::from(&devkitarm).join("bin/arm-none-eabi-gcc"));
let include_path = PathBuf::from_iter([devkitpro.as_str(), "libctru", "include"]);
let ctru_header = include_path.join("3ds.h");
let sysroot = Path::new(&devkitarm).join("arm-none-eabi");
let system_include = sysroot.join("include");
let gcc_include = PathBuf::from(format!(
"{devkitarm}/lib/gcc/arm-none-eabi/{gcc_version}/include"
));
let errno_header = system_include.join("errno.h");
// Build libctru bindings
let bindings = Builder::default()
.header(ctru_header.to_str().unwrap())
.header(errno_header.to_str().unwrap())
.rust_target(RustTarget::Nightly)
.use_core()
.trust_clang_mangling(false)
.must_use_type("Result")
.layout_tests(false)
.ctypes_prefix("::libc")
.prepend_enum_name(false)
.blocklist_type("u(8|16|32|64)")
.blocklist_type("__builtin_va_list")
.blocklist_type("__va_list")
.opaque_type("MiiData")
.derive_default(true)
.wrap_static_fns(true)
.wrap_static_fns_path(out_dir.join("libctru_statics_wrapper"))
.clang_args([
"--target=arm-none-eabi",
"--sysroot",
sysroot.to_str().unwrap(),
"-isystem",
system_include.to_str().unwrap(),
"-isystem",
gcc_include.to_str().unwrap(),
"-I",
include_path.to_str().unwrap(),
"-mfloat-abi=hard",
"-march=armv6k",
"-mtune=mpcore",
"-mfpu=vfp",
"-DARM11",
"-D__3DS__",
])
.parse_callbacks(Box::new(CustomCallbacks))
.generate()
.expect("unable to generate bindings");
bindings
.write_to_file(out_dir.join("bindings.rs"))
.expect("Couldn't write bindings!");
// Compile static inline fns wrapper
let cc = Path::new(devkitarm.as_str()).join("bin/arm-none-eabi-gcc");
let ar = Path::new(devkitarm.as_str()).join("bin/arm-none-eabi-ar");
cc::Build::new()
.compiler(cc)
.archiver(ar)
.include(&include_path)
.file(out_dir.join("libctru_statics_wrapper.c"))
.flag("-march=armv6k")
.flag("-mtune=mpcore")
.flag("-mfloat-abi=hard")
.flag("-mfpu=vfp")
.flag("-mtp=soft")
.flag("-Wno-deprecated-declarations")
.compile("ctru_statics_wrapper");
}
fn get_gcc_version(path_to_gcc: PathBuf) -> String {
let Output { stdout, .. } = Command::new(path_to_gcc)
.arg("--version")
.stderr(Stdio::inherit())
.output()
.unwrap();
let stdout_str = String::from_utf8_lossy(&stdout);
stdout_str
.split(|c: char| c.is_whitespace())
.nth(4)
.unwrap()
.to_string()
} }
fn parse_version(version: &str) -> Result<(String, String, String), &str> { fn parse_libctru_version(version: &str) -> Result<(String, String, String), &str> {
let versions: Vec<_> = version let versions: Vec<_> = version
.split(|c| c == '.' || c == '-') .split(|c| c == '.' || c == '-')
.map(String::from) .map(String::from)
@ -68,7 +171,8 @@ fn check_libctru_version() -> Result<(String, String, String), Box<dyn Error>> {
if lib_version != crate_built_version { if lib_version != crate_built_version {
return Err(format!( return Err(format!(
"libctru version is {lib_version} but this crate was built for {crate_built_version}" "libctru version is {lib_version} but this crate was built for {crate_built_version}"
))?; )
.into());
} }
let Output { stdout, .. } = Command::new(pacman) let Output { stdout, .. } = Command::new(pacman)
@ -77,12 +181,13 @@ fn check_libctru_version() -> Result<(String, String, String), Box<dyn Error>> {
.output()?; .output()?;
for line in String::from_utf8_lossy(&stdout).split('\n') { for line in String::from_utf8_lossy(&stdout).split('\n') {
let Some((_pkg, file)) = line.split_once(char::is_whitespace) let Some((_pkg, file)) = line.split_once(char::is_whitespace) else {
else { continue }; continue;
};
println!("cargo:rerun-if-changed={file}"); println!("cargo:rerun-if-changed={file}");
} }
let (lib_major, lib_minor, lib_patch) = parse_version(lib_version)?; let (lib_major, lib_minor, lib_patch) = parse_libctru_version(lib_version)?;
Ok((lib_major, lib_minor, lib_patch)) Ok((lib_major, lib_minor, lib_patch))
} }

7
ctru-sys/docstring-to-rustdoc/Cargo.toml

@ -1,7 +0,0 @@
[package]
name = "docstring-to-rustdoc"
version = "0.1.0"
edition = "2021"
[dependencies]
doxygen-rs = "0.3.1"

31
ctru-sys/docstring-to-rustdoc/src/main.rs

@ -1,31 +0,0 @@
//! This script transforms _some_ Boxygen comments to Rustdoc format
//!
//! # Usage
//!
//! `cargo run --package docstring-to-rustdoc -- [location of the bindings.rs]`
//! Example: `cargo run --package docstring-to-rustdoc -- src/bindings.rs`
//!
//! # Transformations
//!
//! Check [doxygen-rs docs](https://techie-pi.github.io/doxygen-rs/doxygen_rs/)
use std::path::Path;
use std::{env, fs, io};
fn main() -> io::Result<()> {
let args: Vec<String> = env::args().collect();
let bindings_path = Path::new(args.get(1).expect("bindings.rs not provided in the args"));
let bindings = fs::read_to_string(bindings_path)?;
let parsed = doxygen_rs::transform_bindgen(bindings.as_str());
let old_bindings_path = bindings_path.to_str().unwrap().to_owned() + ".old";
// If something fails, the original bindings are available at ``bindings.rs.old``
fs::rename(bindings_path, &old_bindings_path)?;
fs::write(bindings_path, parsed)?;
fs::remove_file(&old_bindings_path)?;
Ok(())
}

22931
ctru-sys/src/bindings.rs generated

File diff suppressed because it is too large Load Diff

19
ctru-sys/src/lib.rs

@ -7,17 +7,14 @@
use core::arch::asm; use core::arch::asm;
pub mod result; pub mod result;
mod bindings;
pub use bindings::*;
pub use result::*; pub use result::*;
include!(concat!(env!("OUT_DIR"), "/bindings.rs"));
/// In lieu of a proper errno function exposed by libc /// In lieu of a proper errno function exposed by libc
/// (<https://github.com/rust-lang/libc/issues/1995>), this will retrieve the /// (<https://github.com/rust-lang/libc/issues/1995>).
/// last error set in the global reentrancy struct.
pub unsafe fn errno() -> s32 { pub unsafe fn errno() -> s32 {
(*__getreent())._errno *__errno()
} }
pub unsafe fn getThreadLocalStorage() -> *mut libc::c_void { pub unsafe fn getThreadLocalStorage() -> *mut libc::c_void {
@ -29,3 +26,11 @@ pub unsafe fn getThreadLocalStorage() -> *mut libc::c_void {
pub unsafe fn getThreadCommandBuffer() -> *mut u32 { pub unsafe fn getThreadCommandBuffer() -> *mut u32 {
(getThreadLocalStorage() as *mut u8).add(0x80) as *mut u32 (getThreadLocalStorage() as *mut u8).add(0x80) as *mut u32
} }
// TODO: not sure if there's a better way to do this, but I have gotten myself
// with this a couple times so having the hint seems nice to have.
#[cfg(test)]
compile_error!(concat!(
"ctru-sys doesn't have tests and its lib test will fail to build at link time. ",
"Try specifying `--package ctru-rs` to build those tests.",
));

Loading…
Cancel
Save