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. 32
      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. 38
      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. 41
      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. 60
      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. 379
      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. 1129
      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. 492
      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. 46
      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. 17
      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 @@ @@ -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: @@ -9,19 +9,13 @@ on:
- master
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:
lint:
strategy:
matrix:
toolchain:
# Run against a "known good" nightly
- nightly-2023-01-13
# Run against a "known good" nightly. Rustc version is 1 day behind the toolchain date
- nightly-2023-06-01
# Check for breakage on latest nightly
- nightly
@ -33,11 +27,14 @@ jobs: @@ -33,11 +27,14 @@ jobs:
- name: Checkout branch
uses: actions/checkout@v2
- uses: ./.github/actions/setup
- uses: rust3ds/test-runner/setup@v1
with:
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' }}
run: |
echo "::remove-matcher owner=clippy::"
@ -46,18 +43,17 @@ jobs: @@ -46,18 +43,17 @@ jobs:
- name: Check formatting
run: cargo fmt --all --verbose -- --check
- name: Cargo check
run: cargo 3ds clippy --color=always --workspace --verbose --all-targets
# --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"
# feature, but https://github.com/actions/runner/issues/2341 means we
# can't have both that *and* colored output.
- name: Cargo check ctru-sys (without tests)
run: cargo 3ds clippy --package ctru-sys --color=always --verbose
- name: Cargo check ctru-rs (including tests)
run: cargo 3ds clippy --package ctru-rs --color=always --verbose --all-targets
doctests:
test:
strategy:
matrix:
toolchain:
- nightly-2023-01-13
- nightly-2023-06-01
- nightly
continue-on-error: ${{ matrix.toolchain == 'nightly' }}
runs-on: ubuntu-latest
@ -66,15 +62,36 @@ jobs: @@ -66,15 +62,36 @@ jobs:
- name: Checkout branch
uses: actions/checkout@v2
- uses: ./.github/actions/setup
- uses: rust3ds/test-runner/setup@v1
with:
toolchain: ${{ matrix.toolchain }}
- name: Hide duplicated warnings from lint job
run: echo "::remove-matcher owner=clippy::"
- name: Build doc tests
run: cargo 3ds test --doc --verbose
# This needs to be done separately from running the tests to ensure the
# 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.
# and run it somehow, but exactly how remains to be seen.
- name: Build and run doc tests
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 @@ -4,3 +4,4 @@ Cargo.lock
# IDE files
.idea
.vscode

8
Cargo.toml

@ -1,6 +1,10 @@ @@ -1,6 +1,10 @@
[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']
# 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" }

32
README.md

@ -1,33 +1,27 @@ @@ -1,33 +1,27 @@
# 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
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`
used to generate the bindings, with the following convention:
* `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.
Specific information about how to use the crates is present in the individual README for each package.
Have a look at `ctru-rs`' [README.md](./ctru-rs/README.md) for a broad overview.
## Original version
This project is based on the efforts the original authors:
* [Eidolon](https://github.com/HybridEidolon)
* [FenrirWolf](https://github.com/FenrirWolf)
This project is based on the efforts of the original authors:
* [Eidolon](https://github.com/HybridEidolon)
* [FenrirWolf](https://github.com/FenrirWolf)
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 @@ @@ -1,11 +1,15 @@
[package]
authors = ["Rust3DS Org", "Ronald Kinard <furyhunter600@gmail.com>"]
description = "A safe wrapper around smealum's ctrulib."
license = "Zlib"
name = "ctru-rs"
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"
rust-version = "1.64"
rust-version = "1.70"
[lib]
crate-type = ["rlib"]
@ -13,25 +17,26 @@ name = "ctru" @@ -13,25 +17,26 @@ name = "ctru"
[dependencies]
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"
shim-3ds = { git = "https://github.com/rust3ds/shim-3ds.git" }
pthread-3ds = { git = "https://github.com/rust3ds/pthread-3ds.git" }
libc = "0.2.121"
bitflags = "1.0.0"
bitflags = "2.3.3"
widestring = "0.2.2"
[build-dependencies]
toml = "0.5"
[dev-dependencies]
bytemuck = "1.12.3"
cfg-if = "1.0.0"
ferris-says = "0.2.1"
futures = "0.3"
lewton = "0.10.2"
test-runner = { git = "https://github.com/rust3ds/test-runner.git" }
time = "0.3.7"
tokio = { version = "1.16", features = ["rt", "time", "sync", "macros"] }
cfg-if = "1.0.0"
bytemuck = "1.12.3"
lewton = "0.10.2"
[features]
default = ["romfs", "big-stack"]
@ -45,6 +50,11 @@ std-threads = [] @@ -45,6 +50,11 @@ std-threads = []
[package.metadata.cargo-3ds]
romfs_dir = "examples/romfs"
[package.metadata.docs.rs]
default-target = "armv6k-nintendo-3ds"
targets = []
cargo-args = ["-Z", "build-std"]
[[example]]
name = "thread-basic"
required-features = ["std-threads"]

22
ctru-rs/README.md

@ -0,0 +1,22 @@ @@ -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 @@ @@ -1,3 +1,7 @@
//! Audio Filters example.
//!
//! This example showcases basic audio functionality using [`Ndsp`].
#![feature(allocator_api)]
use std::f32::consts::PI;
@ -5,21 +9,22 @@ use std::f32::consts::PI; @@ -5,21 +9,22 @@ use std::f32::consts::PI;
use ctru::linear::LinearAllocator;
use ctru::prelude::*;
use ctru::services::ndsp::{
wave::{Wave, WaveStatus},
wave::{Status, Wave},
AudioFormat, AudioMix, InterpolationType, Ndsp, OutputMode,
};
// Configuration for the NDSP process and channels.
const SAMPLE_RATE: usize = 22050;
const SAMPLES_PER_BUF: usize = SAMPLE_RATE / 10; // 2205
const BYTES_PER_SAMPLE: usize = AudioFormat::PCM16Stereo.size();
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.];
// 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) {
// 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);
for (i, chunk) in formatted_data.iter_mut().enumerate() {
@ -35,8 +40,6 @@ fn fill_buffer(audio_data: &mut [u8], frequency: f32) { @@ -35,8 +40,6 @@ fn fill_buffer(audio_data: &mut [u8], frequency: f32) {
}
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");
@ -44,8 +47,7 @@ fn main() { @@ -44,8 +47,7 @@ fn main() {
let mut note: usize = 4;
// Filters
// Filter names to display.
let filter_names = [
"None",
"Low-Pass",
@ -60,19 +62,26 @@ fn main() { @@ -60,19 +62,26 @@ fn main() {
// We set up two wave buffers and alternate between the two,
// 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);
// 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]);
// Clone the original buffer to obtain an equal buffer on the LINEAR memory used for double buffering.
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_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);
// Channel configuration. We use channel zero but any channel would do just fine.
let mut channel_zero = ndsp.channel(0).unwrap();
channel_zero.set_interpolation(InterpolationType::Linear);
channel_zero.set_sample_rate(SAMPLE_RATE as f32);
@ -82,6 +91,7 @@ fn main() { @@ -82,6 +91,7 @@ fn main() {
let mix = AudioMix::default();
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_info2).unwrap();
@ -93,6 +103,8 @@ fn main() { @@ -93,6 +103,8 @@ fn main() {
filter_names[filter as usize]
);
println!("\x1b[29;16HPress Start to exit");
let mut altern = true; // true is wave_info1, false is wave_info2
while apt.main_loop() {
@ -101,14 +113,16 @@ fn main() { @@ -101,14 +113,16 @@ fn main() {
if keys_down.contains(KeyPad::START) {
break;
} // break in order to return to hbmenu
}
// Note frequency controller using the buttons.
if keys_down.intersects(KeyPad::DOWN) {
note = note.saturating_sub(1);
} else if keys_down.intersects(KeyPad::UP) {
note = std::cmp::min(note + 1, NOTEFREQ.len() - 1);
}
// Filter controller using the buttons.
let mut update_params = false;
if keys_down.intersects(KeyPad::LEFT) {
filter -= 1;
@ -139,14 +153,16 @@ fn main() { @@ -139,14 +153,16 @@ fn main() {
}
}
// Double buffer alternation depending on the one used.
let current: &mut Wave = if altern {
&mut wave_info1
} else {
&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();
if let WaveStatus::Done = status {
if let Status::Done = status {
fill_buffer(current.get_buffer_mut().unwrap(), NOTEFREQ[note]);
channel_zero.queue_wave(current).unwrap();
@ -154,7 +170,6 @@ fn main() { @@ -154,7 +170,6 @@ fn main() {
altern = !altern;
}
//Wait for VBlank
gfx.wait_for_vblank();
}
}

34
ctru-rs/examples/buttons.rs

@ -1,8 +1,10 @@ @@ -1,8 +1,10 @@
//! Buttons example.
//!
//! This example showcases how to retrieve button inputs from the console's HID.
use ctru::prelude::*;
fn main() {
ctru::use_panic_handler();
let apt = Apt::new().unwrap();
let mut hid = Hid::new().unwrap();
let gfx = Gfx::new().unwrap();
@ -18,29 +20,35 @@ fn main() { @@ -18,29 +20,35 @@ fn main() {
// Scan for user input on the current frame.
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();
// 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
// from what they were on the previous frame
// from what they were on the previous frame.
if keys != old_keys {
// Clear the screen
// Clear the screen.
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!("\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");
// Print to the screen depending on which keys were held.
//
// The .contains() method checks for all of the provided keys,
// and the .intersects() method checks for any 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.
//
// You can also use the .bits() method to do direct comparisons on
// the underlying bits
// You can also use the `.bits()` method to do direct comparisons on
// the underlying bits.
if keys.contains(KeyPad::A) {
println!("You held A!");
@ -54,13 +62,13 @@ fn main() { @@ -54,13 +62,13 @@ fn main() {
if keys.intersects(KeyPad::L | KeyPad::R | KeyPad::ZL | KeyPad::ZR) {
println!("You held a shoulder button!");
}
if keys.intersects(KeyPad::START) {
if keys.contains(KeyPad::START) {
println!("See ya!");
break;
}
}
// Save our current key presses for the next frame
// Save our current key presses for the next frame.
old_keys = keys;
gfx.wait_for_vblank();

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

@ -1,40 +1,39 @@ @@ -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::services::cam::{Cam, Camera, OutputFormat, ShutterSound, ViewSize};
use ctru::services::gfx::{Flush, Screen, Swap};
use ctru::services::cam::{
Cam, Camera, OutputFormat, ShutterSound, Trimming, ViewSize, WhiteBalance,
};
use ctru::services::gfx::{Flush, Screen, Swap, TopScreen3D};
use ctru::services::gspgpu::FramebufferFormat;
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);
fn main() {
ctru::use_panic_handler();
let apt = Apt::new().expect("Failed to initialize Apt service.");
let mut hid = Hid::new().expect("Failed to initialize Hid service.");
let gfx = Gfx::new().expect("Failed to initialize GFX service.");
let mut top_screen = gfx.top_screen.borrow_mut();
top_screen.set_double_buffering(true);
top_screen.set_framebuffer_format(FramebufferFormat::Rgb565);
gfx.top_screen.borrow_mut().set_double_buffering(true);
gfx.top_screen
.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");
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
.set_view_size(ViewSize::TopLCD)
.expect("Failed to set camera size");
@ -48,48 +47,69 @@ fn main() { @@ -48,48 +47,69 @@ fn main() {
.set_auto_exposure(true)
.expect("Failed to enable auto exposure");
camera
.set_auto_white_balance(true)
.set_white_balance(WhiteBalance::Auto)
.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
.set_trimming(false)
.expect("Failed to disable trimming");
.set_trimming(Trimming::new_centered_with_view(ViewSize::TopLCD))
.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!("Press Start to exit to Homebrew Launcher");
println!("Press Start to exit");
while apt.main_loop() {
hid.scan_input();
keys_down = hid.keys_down();
let keys_down = hid.keys_down();
if keys_down.contains(KeyPad::START) {
break;
}
// If the user presses the R button.
if keys_down.contains(KeyPad::R) {
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
.take_picture(
&mut buf,
WIDTH.try_into().unwrap(),
HEIGHT.try_into().unwrap(),
WAIT_TIMEOUT,
)
.take_picture(&mut buf, WAIT_TIMEOUT)
.expect("Failed to take picture");
let (width, height) = camera.final_view_size();
// Play the normal shutter sound.
cam.play_shutter_sound(ShutterSound::Normal)
.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`
top_screen.flush_buffers();
top_screen.swap_buffers();
// We will only flush and swap the "camera" screen, since the other screen is handled by the `Console`.
top_screen_3d.flush_buffers();
top_screen_3d.swap_buffers();
gfx.wait_for_vblank();
}
@ -99,6 +119,7 @@ fn main() { @@ -99,6 +119,7 @@ fn main() {
// 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.
// 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) {
for j in 0..height {
for i in 0..width {
@ -115,10 +136,10 @@ fn rotate_image_to_screen(src: &[u8], framebuf: *mut u8, width: usize, height: u @@ -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)
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.
let pixel_pointer = framebuf.offset(draw_index as isize);
pixel_pointer.copy_from(src.as_ptr().offset(read_index as isize), 2);
let pixel_pointer = framebuf.add(draw_index);
pixel_pointer.copy_from(src.as_ptr().add(read_index), 2);
}
}
}

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

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

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

@ -13,8 +13,6 @@ use futures::StreamExt; @@ -13,8 +13,6 @@ use futures::StreamExt;
use std::os::horizon::thread::BuilderExt;
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");

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

@ -6,8 +6,6 @@ use std::os::horizon::thread::BuilderExt; @@ -6,8 +6,6 @@ use std::os::horizon::thread::BuilderExt;
use std::time::Duration;
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");

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

@ -1,23 +1,30 @@ @@ -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::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");
static ZERO: &[u8] = &[0; IMAGE.len()];
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.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);
@ -25,39 +32,40 @@ fn main() { @@ -25,39 +32,40 @@ fn main() {
let mut current_side = Side::Left;
// 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;
}
// Split the TopScreen3D to get references to the two render surfaces.
let (mut left, mut right) = top_screen.split_mut();
let left_buf = left.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 {
left_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) {
// flip which buffer we're writing to
// Switch which buffer we're writing to.
current_side = match current_side {
Side::Left => Side::Right,
Side::Right => Side::Left,
};
}
// Obtain the framebuffer of the currently rendered side.
let buf = match current_side {
Side::Left => left_buf.ptr,
Side::Right => right_buf.ptr,
};
// Render the image to the surface's buffer.
unsafe {
buf.copy_from(IMAGE.as_ptr(), IMAGE.len());
}
@ -67,7 +75,6 @@ fn main() { @@ -67,7 +75,6 @@ fn main() {
top_screen.flush_buffers();
top_screen.swap_buffers();
//Wait for VBlank
gfx.wait_for_vblank();
}
}

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

@ -1,3 +1,6 @@ @@ -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::services::gfx::{Flush, Screen, Swap};
@ -15,14 +18,13 @@ 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");
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;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();
@ -37,38 +39,44 @@ fn main() { @@ -37,38 +39,44 @@ fn main() {
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() {
//Scan all the inputs. This should be done once for each frame
hid.scan_input();
if hid.keys_down().contains(KeyPad::START) {
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) {
image_bytes = if std::ptr::eq(image_bytes, IMAGE) {
&flipped_image[..]
} else {
IMAGE
};
}
// this copies more than necessary (once per frame) but it's fine...
unsafe {
frame_buffer
.ptr
.copy_from(image_bytes.as_ptr(), image_bytes.len());
let frame_buffer = bottom_screen.raw_framebuffer();
// We render the newly switched image to the framebuffer.
unsafe {
frame_buffer
.ptr
.copy_from(image_bytes.as_ptr(), image_bytes.len());
}
}
// Flush framebuffers. Since we're not using double buffering,
// this will render the pixels immediately
bottom_screen.flush_buffers();
//Wait for VBlank
gfx.wait_for_vblank();
}
}

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

@ -1,14 +1,20 @@ @@ -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::*;
fn main() {
ctru::use_panic_handler();
let apt = Apt::new().unwrap();
let mut hid = Hid::new().unwrap();
let gfx = Gfx::new().unwrap();
let mut console = Console::new(gfx.top_screen.borrow_mut());
println!("Press A to enable/disable wide screen mode.");
println!("\x1b[29;16HPress Start to exit");
while apt.main_loop() {
hid.scan_input();
@ -17,14 +23,18 @@ fn main() { @@ -17,14 +23,18 @@ fn main() {
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) {
drop(console);
// Switch the state of the wide-mode.
let wide_mode = gfx.top_screen.borrow().is_wide();
gfx.top_screen.borrow_mut().set_wide_mode(!wide_mode);
console = Console::new(gfx.top_screen.borrow_mut());
println!("Press A to enable/disable wide screen mode.");
println!("\x1b[29;16HPress Start to exit");
}
gfx.wait_for_vblank();

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

@ -1,57 +0,0 @@ @@ -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 @@ @@ -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::*;
fn main() {
ctru::use_panic_handler();
// Initialize services
//
// HashMaps generate hashes thanks to the 3DS' cryptografically secure generator.
// This generator is only active when activating the `PS` service.
// This service is automatically initialized.
@ -19,6 +23,7 @@ fn main() { @@ -19,6 +23,7 @@ fn main() {
map.remove("A Key!");
println!("{map:#?}");
println!("\x1b[29;16HPress Start to exit");
while apt.main_loop() {
gfx.wait_for_vblank();

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

@ -1,8 +1,10 @@ @@ -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::*;
fn main() {
ctru::use_panic_handler();
let apt = Apt::new().unwrap();
let mut hid = Hid::new().unwrap();
let gfx = Gfx::new().unwrap();
@ -11,14 +13,15 @@ fn main() { @@ -11,14 +13,15 @@ fn main() {
let top_screen = Console::new(gfx.top_screen.borrow_mut());
// 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'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();
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();
println!("\x1b[14;00HThis is the bottom screen.");
println!("There's not as much space down here, but that's okay.");

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

@ -1,15 +1,21 @@ @@ -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;
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 mut hid = Hid::new().expect("Couldn't obtain HID 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());
// Snazzy message created via `ferris_says`.
let out = b"Hello fellow Rustaceans, I'm on the Nintendo 3DS!";
let width = 24;
@ -21,16 +27,18 @@ fn main() { @@ -21,16 +27,18 @@ fn main() {
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() {
//Scan all the inputs. This should be done once for each frame
// Scan all the controller inputs.
hid.scan_input();
if hid.keys_down().contains(KeyPad::START) {
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();
}
}

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

@ -1,21 +1,27 @@ @@ -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)]
use ctru::linear::LinearAllocator;
use ctru::prelude::*;
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());
// 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();
// Normal `Box` on the heap
// Normal `Box` on the heap.
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);
println!("This value is from the heap: {heap_box}");
@ -29,16 +35,13 @@ fn main() { @@ -29,16 +35,13 @@ fn main() {
println!("\x1b[29;16HPress Start to exit");
// 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;
}
//Wait for VBlank
gfx.wait_for_vblank();
}
}

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

@ -1,40 +1,49 @@ @@ -1,40 +1,49 @@
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::*;
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());
// Setup the Mii Selector configuration.
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.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!");
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() {
Ok(result) => {
println!("Mii type: {:?}", result.mii_type);
println!("Name: {:?}", result.mii_data.name);
println!("Author: {:?}", result.mii_data.author_name);
println!(
"Does the Mii have moles?: {:?}",
result.mii_data.mole_details.is_enabled
);
}
Err(Error::InvalidChecksum) => println!("Corrupt Mii selected"),
Err(Error::NoMiiSelected) => println!("No Mii selected"),
}
println!("Is Mii selected?: {:?}", result.is_mii_selected);
println!("Mii type: {:?}", result.mii_type);
println!("Name: {:?}", result.mii_data.name);
println!("Author: {:?}", result.mii_data.author_name);
println!(
"Does the Mii have moles?: {:?}",
result.mii_data.mole_details.is_enabled
);
println!("\x1b[29;16HPress Start to exit");
// 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;
}
//Wait for VBlank
gfx.wait_for_vblank();
}
}

52
ctru-rs/examples/movement.rs

@ -0,0 +1,52 @@ @@ -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 @@ @@ -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 std::io::{self, Read, Write};
@ -5,29 +9,36 @@ use std::net::{Shutdown, TcpListener}; @@ -5,29 +9,36 @@ use std::net::{Shutdown, TcpListener};
use std::time::Duration;
fn main() {
ctru::use_panic_handler();
let gfx = Gfx::new().unwrap();
let _console = Console::new(gfx.top_screen.borrow_mut());
let mut hid = Hid::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();
// Listen on the standard HTTP port (80).
let server = TcpListener::bind("0.0.0.0:80").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() {
gfx.wait_for_vblank();
hid.scan_input();
if hid.keys_down().contains(KeyPad::START) {
break;
};
// Receive any incoming connections.
match server.accept() {
Ok((mut stream, 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];
match stream.read(&mut buf) {
Ok(_) => {
@ -43,15 +54,18 @@ fn main() { @@ -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";
if let Err(e) = stream.write(response) {
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();
}
Err(e) => match e.kind() {
// If the TCP socket would block execution, just try again.
std::io::ErrorKind::WouldBlock => {}
_ => {
println!("Error accepting connection: {e}");
@ -60,9 +74,6 @@ fn main() { @@ -60,9 +74,6 @@ fn main() {
},
}
hid.scan_input();
if hid.keys_down().contains(KeyPad::START) {
break;
};
gfx.wait_for_vblank();
}
}

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

@ -1,4 +1,6 @@ @@ -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.
//!
//! For now, `cargo 3ds run` does not support this flag, so to run this example
@ -11,30 +13,27 @@ @@ -11,30 +13,27 @@
use ctru::prelude::*;
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");
// We need to use network sockets to send the data stream back.
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)
.expect("unable to redirect stdout/err to 3dslink server");
println!("Hello 3dslink!");
eprintln!("Press Start on the device to disconnect and exit.");
// 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;
}
//Wait for VBlank
gfx.wait_for_vblank();
}
}

22
ctru-rs/examples/romfs.rs

@ -1,43 +1,45 @@ @@ -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::*;
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());
cfg_if::cfg_if! {
// Run this code if RomFS are wanted and available
// This never fails as `ctru-rs` examples inherit all of the `ctru` features,
// but it might if a normal user application wasn't setup correctly
// Run this code if RomFS are wanted and available.
// 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.
if #[cfg(all(feature = "romfs", romfs_exists))] {
// Mount the romfs volume.
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();
println!("Contents of test-file.txt: \n{f}\n");
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");
} else {
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() {
//Scan all the inputs. This should be done once for each frame
hid.scan_input();
if hid.keys_down().contains(KeyPad::START) {
break;
}
//Wait for VBlank
gfx.wait_for_vblank();
}
}

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

@ -1,29 +1,34 @@ @@ -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::*;
fn main() {
ctru::use_panic_handler();
let apt = Apt::new().unwrap();
let mut hid = Hid::new().unwrap();
let gfx = Gfx::new().unwrap();
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() {
gfx.wait_for_vblank();
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) {
// 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.
let mut keyboard = Swkbd::default();
let mut keyboard = SoftwareKeyboard::default();
// 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) {
Ok((text, Button::Right)) => println!("You entered: {text}"),
Ok((_, Button::Left)) => println!("Cancelled"),
@ -32,8 +37,6 @@ fn main() { @@ -32,8 +37,6 @@ fn main() {
}
}
if hid.keys_down().contains(KeyPad::START) {
break;
}
gfx.wait_for_vblank();
}
}

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

@ -1,29 +1,33 @@ @@ -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::services::cfgu::Cfgu;
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 cfgu = Cfgu::new().expect("Couldn't obtain CFGU controller");
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[10;0HLanguage: {:?}", cfgu.language().unwrap());
println!("\x1b[20;0HModel: {:?}", cfgu.model().unwrap());
// Main loop
println!("\x1b[29;16HPress Start to exit");
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;
}
//Wait for VBlank
gfx.wait_for_vblank();
}
}

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

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

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

@ -7,8 +7,6 @@ use ctru::prelude::*; @@ -7,8 +7,6 @@ use ctru::prelude::*;
use std::os::horizon::thread::BuilderExt;
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");
@ -36,7 +34,7 @@ fn main() { @@ -36,7 +34,7 @@ fn main() {
.unwrap();
println!("sys thread exited");
println!("\nPress Start to exit");
println!("\x1b[29;16HPress Start to exit");
while apt.main_loop() {
hid.scan_input();

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

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

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

@ -1,19 +1,20 @@ @@ -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::*;
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());
print!("\x1b[30;16HPress Start to exit.");
println!("\x1b[29;16HPress Start to exit");
// 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) {
@ -21,7 +22,7 @@ fn main() { @@ -21,7 +22,7 @@ fn main() {
}
// 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 hours = cur_time.hour();
@ -36,7 +37,6 @@ fn main() { @@ -36,7 +37,6 @@ fn main() {
println!("\x1b[1;1H{hours:0>2}:{minutes:0>2}:{seconds:0>2}");
println!("{weekday} {month} {day} {year}");
//Wait for VBlank
gfx.wait_for_vblank();
}
}

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

@ -1,27 +1,37 @@ @@ -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::services::am::Am;
use ctru::services::fs::FsMediaType;
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 am = Am::new().expect("Couldn't obtain AM controller");
let top_screen = Console::new(gfx.top_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
.title_count(FsMediaType::Sd)
.expect("Failed to get sd title count");
// List of titles installed on the SD card.
let sd_list = am
.title_list(FsMediaType::Sd)
.expect("Failed to get sd title list");
// Amount of titles installed on the NAND storage.
let nand_count = am
.title_count(FsMediaType::Nand)
.expect("Failed to get nand title count");
// List of titles installed on the NAND storage.
let nand_list = am
.title_list(FsMediaType::Nand)
.expect("Failed to get nand title list");
@ -30,9 +40,7 @@ fn main() { @@ -30,9 +40,7 @@ fn main() {
let mut refresh = true;
let mut use_nand = false;
// 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) {
@ -48,23 +56,23 @@ fn main() { @@ -48,23 +56,23 @@ fn main() {
if hid.keys_down().intersects(KeyPad::DOWN) {
if offset + 1 < cur_list.len() {
offset = offset + 1;
refresh = true;
}
} else if hid.keys_down().intersects(KeyPad::UP) {
if offset > 0 {
offset = offset - 1;
offset += 1;
refresh = true;
}
} else if hid.keys_down().intersects(KeyPad::UP) && offset > 0 {
offset -= 1;
refresh = true;
}
// Render the title list via a scrollable text UI.
if refresh {
let mut selected_title = cur_list.iter().skip(offset).next().unwrap();
// Clear top screen and write title ids to it
let mut selected_title = cur_list.get(offset).unwrap();
// Clear the top screen and write title IDs to it.
top_screen.select();
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() {
if i == 0 {
selected_title = title;
@ -74,25 +82,18 @@ fn main() { @@ -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();
println!("\x1b[2J");
// Move cursor to top left
bottom_screen.clear();
println!("Press Start to exit");
// Move cursor to top left.
println!("\x1b[1;1");
match selected_title.title_info() {
Ok(info) => {
println!("Size: {} KB", info.size_bytes() / 1024);
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!("Size: {} kB", selected_title.size() / 1024);
println!("Version: 0x{:x}", selected_title.version());
println!("Product code: \"{}\"", selected_title.product_code());
println!("\x1b[26;0HPress START to exit");
if use_nand {
println!("Press SELECT to choose SD Card");
println!("Current medium: NAND");
@ -106,7 +107,6 @@ fn main() { @@ -106,7 +107,6 @@ fn main() {
refresh = false;
}
//Wait for VBlank
gfx.wait_for_vblank();
}
}

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

@ -1,15 +1,17 @@ @@ -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::*;
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());
// 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);
println!("\x1b[29;16HPress Start to exit");
@ -26,22 +28,22 @@ fn main() { @@ -26,22 +28,22 @@ fn main() {
let touch: (u16, u16) = hid.touch_position();
// 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 {
// 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.
// This is done to avoid some screen tearing.
if touch == (0, 0) {
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");
}
// 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;
gfx.wait_for_vblank();

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

@ -1,73 +1,87 @@ @@ -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 std::ffi::CString;
use std::{ffi::CString, fmt};
/// Index of a Mii used to configure some parameters of the Mii Selector
/// Can be either a single index, or _all_ Miis
/// Index of a Mii on the [`MiiSelector`] interface.
///
/// See [`MiiSelector::allowlist_user_mii()`] and related functions for more information.
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
pub enum Index {
/// Specific Mii index.
///
/// # Notes
///
/// Indexes start at 0.
Index(u32),
/// All Miis.
All,
}
/// The type of a Mii with their respective data
/// The type of a Mii.
#[derive(Debug, Clone, Eq, PartialEq)]
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,
}
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 {
/// Show the cancel button
const MII_SELECTOR_CANCEL = ctru_sys::MIISELECTOR_CANCEL;
/// Make guest Miis selectable
const MII_SELECTOR_GUESTS = ctru_sys::MIISELECTOR_GUESTS;
/// Show on the top screen
const MII_SELECTOR_TOP = ctru_sys::MIISELECTOR_TOP;
/// Start on the guest's page
const MII_SELECTOR_GUEST_START = ctru_sys::MIISELECTOR_GUESTSTART;
/// Show the cancel button.
const ENABLE_CANCEL = ctru_sys::MIISELECTOR_CANCEL;
/// Make guest Miis available to select.
const ENABLE_GUESTS = ctru_sys::MIISELECTOR_GUESTS;
/// Show the [`MiiSelector`] window on the top screen.
const USE_TOP_SCREEN = ctru_sys::MIISELECTOR_TOP;
/// Start the [`MiiSelector`] on the guests' page. Requires [`Options::ENABLE_GUESTS`].
const START_WITH_GUESTS = ctru_sys::MIISELECTOR_GUESTSTART;
}
}
/// An instance of the Mii Selector
///
/// # 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();
/// ```
/// Configuration structure to setup the Mii Selector applet.
#[doc(alias = "MiiSelectorConf")]
#[derive(Clone, Debug)]
pub struct MiiSelector {
config: Box<ctru_sys::MiiSelectorConf>,
}
/// Return value from a MiiSelector's launch
/// Return value of a successful [`MiiSelector::launch()`].
#[non_exhaustive]
#[derive(Clone, Debug)]
pub struct SelectionResult {
pub mii_data: MiiData,
pub is_mii_selected: bool,
pub struct Selection {
/// Data of the selected Mii.
pub mii_data: Mii,
/// Type of the selected Mii.
pub mii_type: MiiType,
}
/// Error type for the Mii selector
/// Error returned by an unsuccessful [`MiiSelector::launch()`].
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub enum LaunchError {
pub enum Error {
/// The selected Mii's data is corrupt.
InvalidChecksum,
/// Either the user cancelled the selection (see [`Options::ENABLE_CANCEL`]) or no valid Miis were available to select.
NoMiiSelected,
}
impl MiiSelector {
/// Initializes a Mii Selector
/// Initialize a new configuration for the Mii Selector applet.
#[doc(alias = "miiSelectorInit")]
pub fn new() -> Self {
let mut config = Box::<ctru_sys::MiiSelectorConf>::default();
unsafe {
@ -76,9 +90,22 @@ impl MiiSelector { @@ -76,9 +90,22 @@ impl MiiSelector {
Self { config }
}
/// Set the title of the Mii Selector.
/// Set the title of the Mii Selector window.
///
/// # Panics
/// This function will panic if the given `&str` contains NUL bytes.
///
/// # Example
///
/// This function would panic if the given ``&str`` contains NUL bytes.
/// ```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) {
// This can only fail if the text contains NUL bytes in the string... which seems
// unlikely and is documented
@ -88,13 +115,48 @@ impl MiiSelector { @@ -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) {
unsafe { ctru_sys::miiSelectorSetOptions(self.config.as_mut(), options.bits) }
unsafe { ctru_sys::miiSelectorSetOptions(self.config.as_mut(), options.bits()) }
}
/// Whitelist a guest Mii
pub fn whitelist_guest_mii(&mut self, mii_index: Index) {
/// Allowlist a guest Mii based on its 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 {
Index::Index(i) => i,
Index::All => ctru_sys::MIISELECTOR_GUESTMII_SLOTS,
@ -103,8 +165,27 @@ impl MiiSelector { @@ -103,8 +165,27 @@ impl MiiSelector {
unsafe { ctru_sys::miiSelectorWhitelistGuestMii(self.config.as_mut(), index) }
}
/// Blacklist a guest Mii
pub fn blacklist_guest_mii(&mut self, mii_index: Index) {
/// Blocklist a guest Mii based on its 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 {
Index::Index(i) => i,
Index::All => ctru_sys::MIISELECTOR_GUESTMII_SLOTS,
@ -113,8 +194,22 @@ impl MiiSelector { @@ -113,8 +194,22 @@ impl MiiSelector {
unsafe { ctru_sys::miiSelectorBlacklistGuestMii(self.config.as_mut(), index) }
}
/// Whitelist a user Mii
pub fn whitelist_user_mii(&mut self, mii_index: Index) {
/// Allowlist a user-created Mii based on its 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 {
Index::Index(i) => i,
Index::All => ctru_sys::MIISELECTOR_USERMII_SLOTS,
@ -123,8 +218,22 @@ impl MiiSelector { @@ -123,8 +218,22 @@ impl MiiSelector {
unsafe { ctru_sys::miiSelectorWhitelistUserMii(self.config.as_mut(), index) }
}
/// Blacklist a user Mii
pub fn blacklist_user_mii(&mut self, mii_index: Index) {
/// Blocklist a user-created Mii based on its 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 {
Index::Index(i) => i,
Index::All => ctru_sys::MIISELECTOR_USERMII_SLOTS,
@ -133,24 +242,52 @@ impl MiiSelector { @@ -133,24 +242,52 @@ impl MiiSelector {
unsafe { ctru_sys::miiSelectorBlacklistUserMii(self.config.as_mut(), index) }
}
/// Set where the cursor will be.
/// If there's no Mii at that index, the cursor will start at the Mii with the index 0
/// 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.
#[doc(alias = "miiSelectorSetInitialIndex")]
pub fn set_initial_index(&mut self, index: usize) {
// This function is static inline in libctru
// https://github.com/devkitPro/libctru/blob/af5321c78ee5c72a55b526fd2ed0d95ca1c05af9/libctru/include/3ds/applets/miiselector.h#L155
self.config.initial_index = index as u32;
unsafe { ctru_sys::miiSelectorSetInitialIndex(self.config.as_mut(), index as u32) };
}
/// 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();
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()) } {
Ok((*return_val).into())
} else {
Err(LaunchError::InvalidChecksum)
Err(Error::InvalidChecksum)
}
}
}
@ -161,14 +298,24 @@ impl Default for MiiSelector { @@ -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 {
let raw_mii_data = ret.mii;
let mut guest_mii_name = ret.guest_mii_name;
SelectionResult {
Selection {
mii_data: raw_mii_data.into(),
is_mii_selected: ret.no_mii_selected == 0,
mii_type: if ret.guest_mii_index != 0xFFFFFFFF {
MiiType::Guest {
index: ret.guest_mii_index,

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

@ -1,2 +1,12 @@ @@ -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 swkbd;

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

@ -1,106 +1,212 @@ @@ -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 ctru_sys::{
self, swkbdInit, swkbdInputText, swkbdSetButton, swkbdSetFeatures, swkbdSetHintText, SwkbdState,
self, swkbdInit, swkbdInputText, swkbdSetButton, swkbdSetFeatures, swkbdSetHintText,
swkbdSetInitialText, SwkbdState,
};
use libc;
use std::fmt::Display;
use std::iter::once;
use std::str;
/// An instance of the software keyboard.
/// Configuration structure to setup the Software Keyboard applet.
#[doc(alias = "SwkbdState")]
#[derive(Clone)]
pub struct Swkbd {
pub struct SoftwareKeyboard {
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)
/// Qwerty is a QWERTY-only keyboard.
/// 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.
/// Can be set with [`SoftwareKeyboard::new()`]
#[doc(alias = "SwkbdType")]
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
#[repr(u32)]
pub enum Kind {
/// Normal keyboard composed of several pages (QWERTY, accents, symbols, mobile).
Normal = ctru_sys::SWKBD_TYPE_NORMAL,
/// Only QWERTY keyboard.
Qwerty = ctru_sys::SWKBD_TYPE_QWERTY,
/// Only number pad.
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,
}
/// 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)]
#[repr(u32)]
pub enum Button {
/// Left button. Usually corresponds to "Cancel".
Left = ctru_sys::SWKBD_BUTTON_LEFT,
/// Middle button. Usually corresponds to "I Forgot".
Middle = ctru_sys::SWKBD_BUTTON_MIDDLE,
/// Right button. Usually corresponds to "OK".
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)]
#[repr(i32)]
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,
/// Home button was pressed while [`SoftwareKeyboard`] was running.
HomePressed = ctru_sys::SWKBD_HOMEPRESSED,
/// Reset button was pressed while [`SoftwareKeyboard`] was running.
ResetPressed = ctru_sys::SWKBD_RESETPRESSED,
/// Power button was pressed while [`SoftwareKeyboard`] was running.
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,
/// The parental lock PIN was incorrect.
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,
}
/// 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)]
#[repr(u32)]
pub enum ValidInput {
/// All inputs are accepted.
Anything = ctru_sys::SWKBD_ANYTHING,
/// Empty inputs are not accepted.
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,
/// 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,
}
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 {
/// 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 DARKEN_TOP_SCREEN = ctru_sys::SWKBD_DARKEN_TOP_SCREEN;
/// Darken top screen while the [`SoftwareKeyboard`] is active.
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 MULTILINE = ctru_sys::SWKBD_MULTILINE;
/// Enable multiline input.
const MULTILINE = ctru_sys::SWKBD_MULTILINE;
/// Enable fixed-width mode.
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;
/// Allow the usage of the Reset Button while the [`SoftwareKeyboard`] is running.
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;
/// Default to the QWERTY page when the [`SoftwareKeyboard`] is shown.
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 {
/// 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;
/// Disallow the usage of the "at" (@) sign.
const AT = ctru_sys::SWKBD_FILTER_AT;
/// Disallow the usage of the "percent" (%) sign.
const PERCENT = ctru_sys::SWKBD_FILTER_PERCENT;
/// Disallow the usage of the "backslash" (\) sign.
const BACKSLASH = ctru_sys::SWKBD_FILTER_BACKSLASH;
/// Disallow the use of profanity via Nintendo's profanity filter.
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;
}
}
impl Swkbd {
/// Initializes a software keyboard of the specified type and the chosen number of buttons
/// (from 1-3).
impl SoftwareKeyboard {
/// Initialize a new configuration for the Software Keyboard applet depending on how many "exit" buttons are available to the user (1, 2 or 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 {
unsafe {
let mut state = Box::<SwkbdState>::default();
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`.
///
/// 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> {
// 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
@ -123,8 +229,31 @@ impl Swkbd { @@ -123,8 +229,31 @@ impl Swkbd {
/// Fills the provided buffer with a UTF-8 encoded, NUL-terminated sequence of bytes from
/// this software keyboard.
///
/// # Notes
///
/// 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> {
unsafe {
match swkbdInputText(self.state.as_mut(), buf.as_mut_ptr(), buf.len()) {
@ -137,25 +266,118 @@ impl Swkbd { @@ -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) {
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) {
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
/// `Filters::DIGITS` flag is enabled
/// Configure the maximum number of digits that can be entered in the keyboard when the [`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) {
self.state.max_digits = digits;
}
/// Sets the hint text for this software keyboard (that is, the help text that is displayed
/// when the textbox is empty)
/// Set the initial text for this software keyboard.
///
/// 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) {
unsafe {
let nul_terminated: String = text.chars().chain(once('\0')).collect();
@ -163,12 +385,33 @@ impl Swkbd { @@ -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.
///
/// `button` is the `Button` to be configured
/// `text` configures the display text for the button
/// `submit` configures whether pressing the button will accept the keyboard's input or
/// discard it.
/// # 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};
///
/// // We create a `SoftwareKeyboard` with left and right buttons.
/// let mut keyboard = SoftwareKeyboard::new(Kind::Normal, 2);
///
/// // 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) {
unsafe {
let nul_terminated: String = text.chars().chain(once('\0')).collect();
@ -181,19 +424,35 @@ impl Swkbd { @@ -181,19 +424,35 @@ impl Swkbd {
}
}
/// Configures the maximum number of UTF-16 code units that can be entered into the software
/// keyboard. By default the limit is 0xFDE8 code units.
/// Configure the maximum number of UTF-16 code units that can be entered into the software
/// 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
/// 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) {
self.state.max_text_len = len;
}
fn parse_swkbd_error(&self) -> Error {
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_HOMEPRESSED => Error::HomePressed,
ctru_sys::SWKBD_RESETPRESSED => Error::ResetPressed,
@ -206,12 +465,44 @@ impl Swkbd { @@ -206,12 +465,44 @@ 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 {
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!(Button, ctru_sys::SwkbdButton);
from_impl!(Error, ctru_sys::SwkbdResult);

326
ctru-rs/src/console.rs

@ -1,3 +1,10 @@ @@ -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::default::Default;
@ -7,31 +14,129 @@ use crate::services::gfx::Screen; @@ -7,31 +14,129 @@ use crate::services::gfx::Screen;
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> {
context: Box<PrintConsole>,
_screen: RefMut<'screen, dyn Screen>,
screen: RefMut<'screen, dyn Screen>,
}
impl<'screen> Console<'screen> {
/// Initialize a console on the chosen screen, overwriting whatever was on the screen
/// previously (including other consoles). The new console is automatically selected for
/// printing.
/// Initialize a console on the chosen screen.
///
/// # 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 {
let mut context = Box::<PrintConsole>::default();
unsafe { consoleInit(screen.as_raw(), context.as_mut()) };
Console {
context,
_screen: screen,
}
Console { context, 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 {
unsafe {
let current_console = ctru_sys::consoleSelect(&mut EMPTY_CONSOLE);
@ -44,28 +149,178 @@ impl<'screen> Console<'screen> { @@ -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) {
unsafe {
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) {
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
/// 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);
///
/// println!("I'm becoming claustrophobic in here!");
/// #
/// # Ok(())
/// # }
/// ```
#[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.
///
/// # Safety
/// This function is unsafe because it does not validate that the input will produce
/// a console that actually fits on the screen
pub unsafe fn set_window(&mut self, x: i32, y: i32, width: i32, height: i32) {
consoleSetWindow(self.context.as_mut(), x, y, width, height);
/// 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<'_> { @@ -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 @@ @@ -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::error;
use std::ffi::CStr;
@ -6,8 +10,30 @@ use std::ops::{ControlFlow, FromResidual, Try}; @@ -6,8 +10,30 @@ use std::ops::{ControlFlow, FromResidual, Try};
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>;
/// 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)]
#[repr(transparent)]
pub struct ResultCode(pub ctru_sys::Result);
@ -48,13 +74,20 @@ impl<T> FromResidual<Error> for Result<T> { @@ -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]
pub enum Error {
/// Raw [`ctru_sys::Result`] codes.
Os(ctru_sys::Result),
/// Generic [`libc`] errors.
Libc(String),
/// Requested service is already active and cannot be activated again.
ServiceAlreadyActive,
/// `stdout` is already being redirected.
OutputAlreadyRedirected,
/// The buffer provided by the user to store some data is shorter than required.
BufferTooShort {
/// Length of the buffer provided by the user.
provided: usize,
@ -64,8 +97,9 @@ pub enum Error { @@ -64,8 +97,9 @@ pub enum Error {
}
impl Error {
/// Create an [`Error`] out of the last set value in `errno`. This can be used
/// to get a human-readable error string from calls to `libc` functions.
/// Create an [`Error`] out of the last set value in `errno`.
///
/// This can be used to get a human-readable error string from calls to `libc` functions.
pub(crate) fn from_errno() -> Self {
let error_str = unsafe {
let errno = ctru_sys::errno();
@ -126,6 +160,8 @@ impl fmt::Debug for Error { @@ -126,6 +160,8 @@ impl fmt::Debug for Error {
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
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!(
f,
"libctru result code 0x{err:08X}: [{} {}] {}: {}",

87
ctru-rs/src/lib.rs

@ -1,18 +1,49 @@ @@ -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_name = "ctru"]
#![feature(test)]
#![warn(missing_docs)]
#![feature(custom_test_frameworks)]
#![feature(try_trait_v2)]
#![feature(allocator_api)]
#![feature(nonnull_slice_from_raw_parts)]
#![test_runner(test_runner::run)]
#![test_runner(test_runner::run_gdb)] // TODO: does this make sense to have configurable?
#![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.
extern crate pthread_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]
#[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
macro_rules! from_impl {
@ -25,59 +56,13 @@ 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 console;
pub mod error;
pub mod linear;
pub mod mii;
pub mod os;
pub mod prelude;
pub mod services;
#[cfg(test)]
mod test_runner;
pub use crate::error::{Error, Result};

21
ctru-rs/src/linear.rs

@ -1,11 +1,12 @@ @@ -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.
//! As such, it is used for fast and safe memory sharing between services (and is especially needed for GPU and DSP).
//! 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 different hardware components (such as the GPU and the DSP processor).
//!
//! Resources:<br>
//! <https://github.com/devkitPro/libctru/blob/master/libctru/source/allocator/linear.cpp><br>
//! <https://www.3dbrew.org/wiki/Memory_layout>
//! # Additional Resources
//!
//! - <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::ptr::NonNull;
@ -15,19 +16,22 @@ 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,
// 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.
#[derive(Copy, Clone, Default, Debug)]
pub struct 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 {
unsafe { ctru_sys::linearSpaceFree() }
}
}
unsafe impl Allocator for LinearAllocator {
#[doc(alias = "linearAlloc", alias = "linearMemAlign")]
fn allocate(&self, layout: Layout) -> Result<NonNull<[u8]>, AllocError> {
let pointer = unsafe { ctru_sys::linearMemAlign(layout.size(), layout.align()) };
@ -36,6 +40,7 @@ unsafe impl Allocator for LinearAllocator { @@ -36,6 +40,7 @@ unsafe impl Allocator for LinearAllocator {
.ok_or(AllocError)
}
#[doc(alias = "linearFree")]
unsafe fn deallocate(&self, ptr: NonNull<u8>, _layout: Layout) {
ctru_sys::linearFree(ptr.as_ptr().cast());
}

176
ctru-rs/src/mii.rs

@ -1,213 +1,301 @@ @@ -1,213 +1,301 @@
//! Mii Data
//! Mii data.
//!
//! 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)]
pub enum RegionLock {
/// No region-lock.
None,
/// Japan region-lock.
Japan,
/// USA region-lock.
USA,
/// Europe region-lock.
Europe,
}
/// Represent the charset of the console
/// Charset of the Mii.
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub enum Charset {
/// Japan-USA-Europe unified charset.
JapanUSAEurope,
/// China charset.
China,
/// Korea charset.
Korea,
/// Taiwan charset.
Taiwan,
}
/// Represents the options of the Mii
/// Generic options of the Mii.
#[derive(Copy, Clone, Debug)]
pub struct MiiDataOptions {
pub struct Options {
/// Whether it is allowed to copy the Mii.
pub is_copying_allowed: bool,
/// Whether the profanity flag is active.
pub is_profanity_flag_enabled: bool,
/// The Mii's active region-lock.
pub region_lock: RegionLock,
/// The Mii's used 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)]
pub struct SelectorPosition {
/// Index of the page where the Mii is found.
pub page_index: u8,
/// Index of the slot (relative to the page) where the Mii is found.
pub slot_index: u8,
}
/// Represents the kind of origin console
/// Console model from which the Mii originated.
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub enum OriginConsole {
/// Nintendo Wii.
Wii,
/// Nintendo 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,
/// Nintendo Wii U/Switch.
WiiUSwitch,
}
/// Represents the identity of the origin console
/// Identity of the origin console.
#[derive(Copy, Clone, Debug)]
pub struct ConsoleIdentity {
/// From which console the Mii originated from.
pub origin_console: OriginConsole,
}
/// Represents the sex of the Mii
/// Sex of the Mii.
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub enum MiiSex {
pub enum Sex {
/// Male sex.
Male,
/// Female sex.
Female,
}
/// Represents the details of the Mii
/// Generic details of the Mii.
#[derive(Copy, Clone, Debug)]
pub struct Details {
pub sex: MiiSex,
/// Sex of the Mii.
pub sex: Sex,
/// Birthday month.
pub birthday_month: u8,
/// Birthday day.
pub birthday_day: u8,
/// Color of the Mii's shirt.
pub shirt_color: u8,
/// Whether the Mii is a favorite.
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)]
pub struct FaceStyle {
pub is_sharing_enabled: bool,
/// Face shape.
pub shape: u8,
/// Skin color.
pub skin_color: u8,
}
/// Represents the face details of the Mii
/// Face details of the Mii.
#[derive(Copy, Clone, Debug)]
pub struct FaceDetails {
/// Face style.
pub style: FaceStyle,
/// Wrinkles.
pub wrinkles: u8,
/// Makeup.
pub makeup: u8,
}
/// Represents the hair details of the Mii
/// Hair details of the Mii.
#[derive(Copy, Clone, Debug)]
pub struct HairDetails {
/// Hair style.
pub style: u8,
/// Hair color.
pub color: u8,
/// Whether the Mii's hair is flipped.
pub is_flipped: bool,
}
/// Represents the eye details of the Mii
/// Eye details of the Mii.
#[derive(Copy, Clone, Debug)]
pub struct EyeDetails {
/// Eye style.
pub style: u8,
/// Eye color.
pub color: u8,
/// Eye scale.
pub scale: u8,
/// Eye scale (y-axis).
pub y_scale: u8,
/// Eye rotation.
pub rotation: u8,
/// Spacing between the eyes
/// Spacing between the eyes.
pub x_spacing: u8,
/// Eye height.
pub y_position: u8,
}
/// Represents the eyebrow details of the Mii
/// Eyebrow details of the Mii.
#[derive(Copy, Clone, Debug)]
pub struct EyebrowDetails {
/// Eyebrow style.
pub style: u8,
/// Eyebrow color.
pub color: u8,
/// Eyebrow scale.
pub scale: u8,
/// Eyebrow scale (y-axis).
pub y_scale: u8,
/// Eyebrow rotation.
pub rotation: u8,
/// Spacing between the eyebrows
pub x_spacing: u8,
/// Eyebrow height.
pub y_position: u8,
}
/// Represents the details of the nose
/// Nose details of the Mii.
#[derive(Copy, Clone, Debug)]
pub struct NoseDetails {
/// Nose style.
pub style: u8,
/// Nose scale.
pub scale: u8,
/// Nose height.
pub y_position: u8,
}
/// Represents the details of the mouth
/// Mouth details of the Mii.
#[derive(Copy, Clone, Debug)]
pub struct MouthDetails {
/// Mouth style.
pub style: u8,
/// Mouth color.
pub color: u8,
/// Mouth scale.
pub scale: u8,
/// Mouth scale (y-axis).
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)]
pub struct MustacheDetails {
pub mouth_y_position: u8,
/// Mustache style.
pub mustache_style: u8,
}
/// Represents the details of the beard
/// Beard details of the Mii.
#[derive(Copy, Clone, Debug)]
pub struct BeardDetails {
/// Beard style
pub style: u8,
/// Beard color.
pub color: u8,
/// Beard scale.
pub scale: u8,
/// Beard height.
pub y_position: u8,
}
/// Represents the details of the glass
/// Glasses details of the Mii.
#[derive(Copy, Clone, Debug)]
pub struct GlassDetails {
pub struct GlassesDetails {
/// Glasses style.
pub style: u8,
/// Glasses color.
pub color: u8,
/// Glasses scale.
pub scale: u8,
/// Glasses height.
pub y_position: u8,
}
/// Represents the details of the mole
/// Mole details of the Mii.
#[derive(Copy, Clone, Debug)]
pub struct MoleDetails {
/// Whether the Mii has a mole.
pub is_enabled: bool,
/// Mole scale.
pub scale: u8,
/// Mole position (x-axis).
pub x_position: u8,
/// Mole position (y-axis).
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:
/// <https://www.3dbrew.org/wiki/Mii#Mapped_Editor_.3C-.3E_Hex_values>
/// 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).
///
/// 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)]
pub struct MiiData {
pub options: MiiDataOptions,
pub struct Mii {
/// Mii options.
pub options: Options,
/// Position taken by the Mii on the Mii Selector screen.
pub selector_position: SelectorPosition,
/// Console the Mii was created on.
pub console_identity: ConsoleIdentity,
/// Unique system ID, not dependant on the MAC address
pub system_id: [u8; 8],
/// Console's MAC address.
pub mac_address: [u8; 6],
/// General information about the Mii.
pub details: Details,
/// Mii name.
pub name: String,
/// Mii height.
pub height: u8,
/// Mii width.
pub width: u8,
/// Face details.
pub face_details: FaceDetails,
/// Hair details.
pub hair_details: HairDetails,
/// Eyes details.
pub eye_details: EyeDetails,
/// Eyebrow details.
pub eyebrow_details: EyebrowDetails,
/// Nose details.
pub nose_details: NoseDetails,
/// Mouth details.
pub mouth_details: MouthDetails,
/// Mustache details.
pub mustache_details: MustacheDetails,
/// Beard details.
pub beard_details: BeardDetails,
pub glass_details: GlassDetails,
/// Glasses details.
pub glass_details: GlassesDetails,
/// Mole details.
pub mole_details: MoleDetails,
/// Name of the Mii's original author.
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 {
let raw_mii_data = mii_data._bindgen_opaque_blob;
// 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 { @@ -272,7 +360,7 @@ impl From<ctru_sys::MiiData> for MiiData {
let name = utf16_byte_pairs_to_string(raw_utf16_name);
let author_name = utf16_byte_pairs_to_string(raw_utf16_author);
let options = MiiDataOptions {
let options = Options {
is_copying_allowed: raw_options[0],
is_profanity_flag_enabled: raw_options[1],
region_lock: {
@ -312,19 +400,19 @@ impl From<ctru_sys::MiiData> for MiiData { @@ -312,19 +400,19 @@ impl From<ctru_sys::MiiData> for MiiData {
let details = Details {
sex: {
match raw_details[0] {
true => MiiSex::Female,
false => MiiSex::Male,
true => Sex::Female,
false => Sex::Male,
}
},
birthday_month: partial_u8_bits_to_u8(&raw_details[1..=4]),
birthday_day: partial_u8_bits_to_u8(&raw_details[5..=9]),
shirt_color: partial_u8_bits_to_u8(&raw_details[10..=13]),
is_favorite: raw_details[14],
is_sharing_enabled: !raw_face_style[0],
};
let face_details = FaceDetails {
style: FaceStyle {
is_sharing_enabled: !raw_face_style[0],
shape: partial_u8_bits_to_u8(&raw_face_style[1..=4]),
skin_color: partial_u8_bits_to_u8(&raw_face_style[5..=7]),
},
@ -371,10 +459,10 @@ impl From<ctru_sys::MiiData> for MiiData { @@ -371,10 +459,10 @@ impl From<ctru_sys::MiiData> for MiiData {
color: partial_u8_bits_to_u8(&raw_mouth_details[6..=8]),
scale: partial_u8_bits_to_u8(&raw_mouth_details[9..=12]),
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 {
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]),
};
@ -385,7 +473,7 @@ impl From<ctru_sys::MiiData> for MiiData { @@ -385,7 +473,7 @@ impl From<ctru_sys::MiiData> for MiiData {
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]),
color: partial_u8_bits_to_u8(&raw_glass_details[4..=6]),
scale: partial_u8_bits_to_u8(&raw_glass_details[7..=10]),
@ -399,7 +487,7 @@ impl From<ctru_sys::MiiData> for MiiData { @@ -399,7 +487,7 @@ impl From<ctru_sys::MiiData> for MiiData {
y_position: partial_u8_bits_to_u8(&raw_mole_details[10..=14]),
};
MiiData {
Mii {
options,
selector_position,
console_identity,

154
ctru-rs/src/os.rs

@ -0,0 +1,154 @@ @@ -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 @@ @@ -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::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 @@ @@ -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::services::fs::FsMediaType;
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> {
id: u64,
mediatype: FsMediaType,
size: u64,
version: u16,
_am: PhantomData<&'a Am>,
}
impl<'a> Title<'a> {
/// Returns this title's ID.
pub fn id(&self) -> u64 {
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];
// This operation is safe as long as the title was correctly obtained via [`Am::title_list()`].
unsafe {
ResultCode(ctru_sys::AM_GetTitleProductCode(
self.mediatype.into(),
self.id,
buf.as_mut_ptr(),
))?;
let _ =
ctru_sys::AM_GetTitleProductCode(self.mediatype.into(), self.id, buf.as_mut_ptr());
}
Ok(String::from_utf8_lossy(&buf).to_string())
}
pub fn title_info(&self) -> crate::Result<TitleInfo> {
let mut info = MaybeUninit::zeroed();
String::from_utf8_lossy(&buf).to_string()
}
unsafe {
ResultCode(ctru_sys::AM_GetTitleInfo(
self.mediatype.into(),
1,
&mut self.id.clone(),
info.as_mut_ptr() as _,
))?;
/// Returns the size of this title in bytes.
pub fn size(&self) -> u64 {
self.size
}
Ok(info.assume_init())
}
/// Returns the installed version of this title.
pub fn version(&self) -> u16 {
self.version
}
}
/// Handle to the Application Manager service.
pub struct 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> {
unsafe {
ResultCode(ctru_sys::amInit())?;
@ -69,6 +81,29 @@ impl Am { @@ -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> {
unsafe {
let mut count = 0;
@ -77,10 +112,34 @@ impl Am { @@ -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>> {
let count = self.title_count(mediatype)?;
let mut buf = vec![0; count as usize];
let mut read_amount = 0;
unsafe {
ResultCode(ctru_sys::AM_GetTitleList(
&mut read_amount,
@ -89,11 +148,27 @@ impl Am { @@ -89,11 +148,27 @@ impl Am {
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()
.map(|id| Title {
id,
.map(|title| Title {
id: title.titleID,
mediatype,
size: title.size,
version: title.version,
_am: PhantomData,
})
.collect())
@ -101,6 +176,7 @@ impl Am { @@ -101,6 +176,7 @@ impl Am {
}
impl Drop for Am {
#[doc(alias = "amExit")]
fn drop(&mut self) {
unsafe { ctru_sys::amExit() };
}

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

@ -1,8 +1,34 @@ @@ -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;
/// Handle to the Applet service.
pub struct 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> {
unsafe {
ResultCode(ctru_sys::aptInit())?;
@ -10,10 +36,46 @@ impl Apt { @@ -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 {
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<()> {
unsafe {
ResultCode(ctru_sys::APT_SetAppCpuTimeLimit(percent))?;
@ -23,6 +85,7 @@ impl Apt { @@ -23,6 +85,7 @@ impl Apt {
}
impl Drop for Apt {
#[doc(alias = "aptExit")]
fn drop(&mut self) {
unsafe { ctru_sys::aptExit() };
}

1129
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 @@ @@ -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;
/// Console region.
#[doc(alias = "CFG_Region")]
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
#[repr(u32)]
pub enum Region {
/// Japan.
Japan = ctru_sys::CFG_REGION_JPN,
/// USA.
USA = ctru_sys::CFG_REGION_USA,
/// Europe.
Europe = ctru_sys::CFG_REGION_EUR,
/// Australia.
Australia = ctru_sys::CFG_REGION_AUS,
/// China.
China = ctru_sys::CFG_REGION_CHN,
/// Korea.
Korea = ctru_sys::CFG_REGION_KOR,
/// Taiwan.
Taiwan = ctru_sys::CFG_REGION_TWN,
}
/// Language set for the console's OS.
#[doc(alias = "CFG_Language")]
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
#[repr(u32)]
pub enum Language {
/// Japanese.
Japanese = ctru_sys::CFG_LANGUAGE_JP,
/// English.
English = ctru_sys::CFG_LANGUAGE_EN,
/// French.
French = ctru_sys::CFG_LANGUAGE_FR,
/// German.
German = ctru_sys::CFG_LANGUAGE_DE,
/// Italian.
Italian = ctru_sys::CFG_LANGUAGE_IT,
/// Spanish.
Spanish = ctru_sys::CFG_LANGUAGE_ES,
SimplifiedChinese = ctru_sys::CFG_LANGUAGE_ZH,
/// Korean.
Korean = ctru_sys::CFG_LANGUAGE_KO,
/// Dutch.
Dutch = ctru_sys::CFG_LANGUAGE_NL,
/// Portuguese.
Portuguese = ctru_sys::CFG_LANGUAGE_PT,
/// Russian.
Russian = ctru_sys::CFG_LANGUAGE_RU,
/// Simplified Chinese.
SimplifiedChinese = ctru_sys::CFG_LANGUAGE_ZH,
/// Traditional Chinese.
TraditionalChinese = ctru_sys::CFG_LANGUAGE_TW,
}
/// Specific model of the console.
#[doc(alias = "CFG_SystemModel")]
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
#[repr(u32)]
pub enum SystemModel {
/// Old Nintendo 3DS.
Old3DS = ctru_sys::CFG_MODEL_3DS,
/// Old Nintendo 3DS XL.
Old3DSXL = ctru_sys::CFG_MODEL_3DSXL,
/// New Nintendo 3DS.
New3DS = ctru_sys::CFG_MODEL_N3DS,
/// Old Nintendo 2DS.
Old2DS = ctru_sys::CFG_MODEL_2DS,
/// New Nintendo 3DS XL.
New3DSXL = ctru_sys::CFG_MODEL_N3DSXL,
/// New Nintendo 2DS XL.
New2DSXL = ctru_sys::CFG_MODEL_N2DSXL,
}
/// Represents the configuration service. No actions can be performed
/// until an instance of this struct is created.
///
/// The service exits when all instances of this struct go out of scope.
/// Handle to the System Configuration service.
pub struct 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
/// as many times as desired and the service will not exit until all
/// instances of Cfgu drop out of scope.
/// let cfgu = Cfgu::new()?;
/// #
/// # Ok(())
/// # }
/// ```
#[doc(alias = "cfguInit")]
pub fn new() -> crate::Result<Cfgu> {
ResultCode(unsafe { ctru_sys::cfguInit() })?;
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> {
let mut region: u8 = 0;
@ -74,7 +127,24 @@ impl Cfgu { @@ -74,7 +127,24 @@ impl Cfgu {
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> {
let mut model: u8 = 0;
@ -82,7 +152,24 @@ impl Cfgu { @@ -82,7 +152,24 @@ impl Cfgu {
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> {
let mut language: u8 = 0;
@ -90,7 +177,26 @@ impl Cfgu { @@ -90,7 +177,26 @@ impl Cfgu {
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> {
let mut supported: bool = false;
@ -98,7 +204,28 @@ impl Cfgu { @@ -98,7 +204,28 @@ impl Cfgu {
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> {
let mut is_2ds_family: u8 = 0;
@ -108,6 +235,7 @@ impl Cfgu { @@ -108,6 +235,7 @@ impl Cfgu {
}
impl Drop for Cfgu {
#[doc(alias = "cfguExit")]
fn drop(&mut self) {
unsafe {
ctru_sys::cfguExit();

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

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

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

@ -1,4 +1,8 @@ @@ -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::marker::PhantomData;
@ -20,9 +24,12 @@ mod private { @@ -20,9 +24,12 @@ mod private {
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
/// drawing to the screens. Graphics-related code can be made generic over this
/// trait to work with any of the given screens.
#[doc(alias = "gfxScreen_t")]
pub trait Screen: private::Sealed {
/// Returns the `libctru` value for the Screen kind.
fn as_raw(&self) -> ctru_sys::gfxScreen_t;
@ -30,10 +37,17 @@ pub trait Screen: private::Sealed { @@ -30,10 +37,17 @@ pub trait Screen: private::Sealed {
/// Returns the Screen side (left or right).
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
/// change after each call to this function if double buffering is enabled.
/// # Panics
///
/// 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 {
let mut width: u16 = 0;
let mut height: u16 = 0;
@ -48,15 +62,8 @@ pub trait Screen: private::Sealed { @@ -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.
#[doc(alias = "gfxGetScreenFormat")]
fn framebuffer_format(&self) -> FramebufferFormat {
unsafe { ctru_sys::gfxGetScreenFormat(self.as_raw()) }.into()
}
@ -65,40 +72,53 @@ pub trait Screen: private::Sealed { @@ -65,40 +72,53 @@ pub trait Screen: private::Sealed {
///
/// [`Swap::swap_buffers`] must be called after this method for the configuration
/// change to take effect.
#[doc(alias = "gfxSetScreenFormat")]
fn set_framebuffer_format(&mut self, fmt: FramebufferFormat) {
unsafe { ctru_sys::gfxSetScreenFormat(self.as_raw(), fmt.into()) }
}
}
/// The top screen. 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`].
/// The top LCD screen.
///
/// 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 {
left: TopScreenLeft,
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
/// converted into this, 3D mode will be enabled until this struct is dropped.
pub struct TopScreen3D<'screen> {
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.
pub trait Swap: private::Sealed {
/// Swaps the video buffers.
///
/// 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`],
/// [`set_framebuffer_format`], [`set_double_buffering`]).
/// Even if double buffering is disabled, "swapping" the buffers has the side effect
/// of committing any configuration changes to the buffers (e.g. [`TopScreen::set_wide_mode()`],
/// [`Screen::set_framebuffer_format()`], [`Swap::set_double_buffering()`]), so it should still be used.
///
/// This should be called once per frame at most.
///
/// [`set_wide_mode`]: TopScreen::set_wide_mode
/// [`set_framebuffer_format`]: Screen::set_framebuffer_format
/// [`set_double_buffering`]: Screen::set_double_buffering
#[doc(alias = "gfxScreenSwapBuffers")]
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<'_> {
@ -107,6 +127,10 @@ impl Swap for TopScreen3D<'_> { @@ -107,6 +127,10 @@ impl Swap for TopScreen3D<'_> {
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 {
@ -115,6 +139,10 @@ impl Swap for TopScreen { @@ -115,6 +139,10 @@ impl Swap for TopScreen {
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 {
@ -123,13 +151,20 @@ impl Swap for BottomScreen { @@ -123,13 +151,20 @@ impl Swap for BottomScreen {
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`]
/// that has data written to its frame buffer.
/// A screen with buffers that can be flushed.
///
/// This trait applies to any [`Screen`] that has data written to its frame buffer.
pub trait Flush: private::Sealed {
/// Flushes the video buffer(s) for this screen. Note that you must still call
/// [`Swap::swap_buffers`] after this method for the buffer contents to be displayed.
/// Flushes the video buffer(s) for this screen.
///
/// 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);
}
@ -138,7 +173,7 @@ impl<S: Screen> Flush for S { @@ -138,7 +173,7 @@ impl<S: Screen> Flush for S {
let framebuffer = self.raw_framebuffer();
// Flush the data array. `self.raw_framebuffer` should get the correct parameters for all kinds of screens
unsafe {
let _ = unsafe {
ctru_sys::GSPGPU_FlushDataCache(
framebuffer.ptr.cast(),
(framebuffer.height * framebuffer.width) as u32,
@ -167,14 +202,16 @@ pub struct TopScreenLeft; @@ -167,14 +202,16 @@ pub struct TopScreenLeft;
#[non_exhaustive]
pub struct TopScreenRight;
/// The bottom screen. Mutable access to this struct is required to write to the
/// bottom screen's frame buffer.
/// The bottom LCD screen.
///
/// Mutable access to this struct is required to write to the bottom screen's frame buffer.
#[derive(Debug)]
#[non_exhaustive]
pub struct BottomScreen;
/// Representation of a framebuffer for one [`Side`] of the top screen, or the
/// entire bottom screen. The inner pointer is only valid for one frame if double
/// Representation of a framebuffer for one [`Side`] of the top screen, or the entire bottom screen.
///
/// The inner pointer is only valid for one frame if double
/// buffering is enabled. Data written to `ptr` will be rendered to the screen.
#[derive(Debug)]
pub struct RawFrameBuffer<'screen> {
@ -188,11 +225,12 @@ pub struct RawFrameBuffer<'screen> { @@ -188,11 +225,12 @@ pub struct RawFrameBuffer<'screen> {
screen: PhantomData<&'screen mut dyn Screen>,
}
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
#[repr(u32)]
/// Side of top screen framebuffer
/// Side of the [`TopScreen`]'s framebuffer.
///
/// 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 {
/// The left framebuffer. This framebuffer is also the one used when 3D is disabled
Left = ctru_sys::GFX_LEFT,
@ -200,40 +238,139 @@ pub enum Side { @@ -200,40 +238,139 @@ pub enum Side {
Right = ctru_sys::GFX_RIGHT,
}
/// A handle to libctru's gfx module. This module is a wrapper around the GSPGPU service that
/// provides helper functions and utilities for software rendering.
/// Handle to the GFX service.
///
/// 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 {
/// Top screen representation.
pub top_screen: RefCell<TopScreen>,
/// Bottom screen representation.
pub bottom_screen: RefCell<BottomScreen>,
_service_handler: ServiceReference,
}
static GFX_ACTIVE: Mutex<usize> = Mutex::new(0);
static GFX_ACTIVE: Mutex<()> = Mutex::new(());
impl Gfx {
/// Creates a new [Gfx] instance with default init values
/// It's the same as calling:
/// `Gfx::with_formats(FramebufferFormat::Bgr8, FramebufferFormat::Bgr8, false)`
/// Initialize a new default service handle.
///
/// # 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> {
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
/// screens
/// Initialize a new service handle with the chosen framebuffer formats on the HEAP for the top and bottom 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,
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> {
let handler = ServiceReference::new(
&GFX_ACTIVE,
false,
|| 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(())
},
@ -247,9 +384,34 @@ impl Gfx { @@ -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
///
/// # 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) {
gspgpu::wait_for_event(gspgpu::Event::VBlank0, true);
}
@ -269,7 +431,36 @@ impl TopScreen3D<'_> { @@ -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> {
#[doc(alias = "gfxSet3D")]
fn from(top_screen: &'screen RefCell<TopScreen>) -> Self {
unsafe {
ctru_sys::gfxSet3D(true);
@ -297,8 +488,13 @@ impl TopScreen { @@ -297,8 +488,13 @@ impl TopScreen {
/// Enable or disable wide mode on the top screen.
///
/// # Notes
///
/// [`Swap::swap_buffers`] must be called after this method for the configuration
/// 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) {
unsafe {
ctru_sys::gfxSetWide(enable);
@ -306,6 +502,7 @@ impl TopScreen { @@ -306,6 +502,7 @@ impl TopScreen {
}
/// Returns whether or not wide mode is enabled on the top screen.
#[doc(alias = "gfxIsWide")]
pub fn is_wide(&self) -> bool {
unsafe { ctru_sys::gfxIsWide() }
}
@ -362,7 +559,10 @@ mod tests { @@ -362,7 +559,10 @@ mod tests {
#[test]
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)));
}
}

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

@ -1,18 +1,28 @@ @@ -1,18 +1,28 @@
//! GSPGPU service
/// GSPGPU events that can be awaited.
#[doc(alias = "GSPGPU_Event")]
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
#[repr(u32)]
pub enum Event {
/// Memory fill 1 completed.
Psc0 = ctru_sys::GSPGPU_EVENT_PSC0,
/// Memory fill 2 completed.
Psc1 = ctru_sys::GSPGPU_EVENT_PSC1,
/// Top screen VBlank.
VBlank0 = ctru_sys::GSPGPU_EVENT_VBlank0,
/// Bottom screen VBlank.
VBlank1 = ctru_sys::GSPGPU_EVENT_VBlank1,
/// Display transfer completed.
PPF = ctru_sys::GSPGPU_EVENT_PPF,
/// Command list processing completed.
P3D = ctru_sys::GSPGPU_EVENT_P3D,
/// Direct Memory Access requested.
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)]
#[repr(u32)]
pub enum FramebufferFormat {
@ -45,6 +55,7 @@ impl FramebufferFormat { @@ -45,6 +55,7 @@ impl FramebufferFormat {
/// Waits for a GSPGPU event to occur.
///
/// `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) {
unsafe {
ctru_sys::gspWaitForEvent(ev.into(), discard_current);

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

@ -1,75 +1,218 @@ @@ -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,
//! and circle pad information. It also provides information from the sound volume slider,
//! the accelerometer, and the gyroscope.
//! 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](Hid::circlepad_position). It also provides information from the [volume slider](Hid::volume_slider()),
//! 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;
bitflags::bitflags! {
/// A set of flags corresponding to the button and directional pad
/// inputs on the 3DS
use crate::services::ServiceReference;
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 {
/// A button.
const A = ctru_sys::KEY_A;
/// B button.
const B = ctru_sys::KEY_B;
/// Select button.
const SELECT = ctru_sys::KEY_SELECT;
/// Start button.
const START = ctru_sys::KEY_START;
/// D-Pad Right.
const DPAD_RIGHT = ctru_sys::KEY_DRIGHT;
/// D-Pad Left.
const DPAD_LEFT = ctru_sys::KEY_DLEFT;
/// D-Pad Up.
const DPAD_UP = ctru_sys::KEY_DUP;
/// D-Pad Down.
const DPAD_DOWN = ctru_sys::KEY_DDOWN;
/// R button.
const R = ctru_sys::KEY_R;
/// L button.
const L = ctru_sys::KEY_L;
/// X button.
const X = ctru_sys::KEY_X;
/// Y button.
const Y = ctru_sys::KEY_Y;
/// ZL button.
const ZL = ctru_sys::KEY_ZL;
/// ZR button.
const ZR = ctru_sys::KEY_ZR;
/// Touchscreen.
const TOUCH = ctru_sys::KEY_TOUCH;
/// C-Stick Right.
const CSTICK_RIGHT = ctru_sys::KEY_CSTICK_RIGHT;
/// C-Stick Left.
const CSTICK_LEFT = ctru_sys::KEY_CSTICK_LEFT;
/// C-Stick Up.
const CSTICK_UP = ctru_sys::KEY_CSTICK_UP;
/// C-Stick Down.
const CSTICK_DOWN = ctru_sys::KEY_CSTICK_DOWN;
/// CirclePad Right.
const CPAD_RIGHT = ctru_sys::KEY_CPAD_RIGHT;
/// CirclePad Left.
const CPAD_LEFT = ctru_sys::KEY_CPAD_LEFT;
/// CirclePad Up.
const CPAD_UP = ctru_sys::KEY_CPAD_UP;
/// CirclePad 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();
/// Direction Down (either D-Pad or CirclePad).
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();
/// Direction Right (either D-Pad or CirclePad).
const RIGHT = KeyPad::DPAD_RIGHT.bits() | KeyPad::CPAD_RIGHT.bits();
}
}
/// A reference-counted handle to the HID service. The service is closed
/// when all instances of this struct fall out of scope.
///
/// This service requires no special permissions to use.
pub struct Hid(());
/// Error enum for generic errors within the [`Hid`] service.
#[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,
}
/// Initializes the HID service.
/// Representation of the acceleration vector read by the accelerometer.
///
/// # Errors
/// Have a look at [`Hid::set_accelerometer()`] for more information.
#[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.
///
/// 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
/// rare in practice.
/// 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,
}
/// 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
///
/// 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 rare in practice.
///
/// # 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> {
unsafe {
ResultCode(ctru_sys::hidInit())?;
Ok(Hid(()))
}
let handler = ServiceReference::new(
&HID_ACTIVE,
|| {
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
/// frame. This function should be called on every frame when polling
/// Scan the HID service for all user input occurring on the current frame.
///
/// This function should be called on every frame when polling
/// 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) {
unsafe { ctru_sys::hidScanInput() };
}
/// Returns a bitflag struct representing which buttons have just been pressed
/// 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 {
unsafe {
let keys = ctru_sys::hidKeysDown();
@ -79,6 +222,27 @@ impl Hid { @@ -79,6 +222,27 @@ impl Hid {
/// Returns a bitflag struct representing which buttons have been held down
/// 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 {
unsafe {
let keys = ctru_sys::hidKeysHeld();
@ -88,6 +252,27 @@ impl Hid { @@ -88,6 +252,27 @@ impl Hid {
/// Returns a bitflag struct representing which buttons have just been released on
/// 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 {
unsafe {
let keys = ctru_sys::hidKeysUp();
@ -100,12 +285,32 @@ impl Hid { @@ -100,12 +285,32 @@ impl Hid {
/// # Notes
///
/// (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 };
unsafe {
ctru_sys::hidTouchRead(&mut res);
}
(res.px, res.py)
}
@ -114,18 +319,247 @@ impl Hid { @@ -114,18 +319,247 @@ impl Hid {
/// # Notes
///
/// (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 };
unsafe {
ctru_sys::hidCircleRead(&mut res);
}
(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);
}
let mut res = ctru_sys::angularRate { x: 0, y: 0, z: 0 };
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 Drop for Hid {
fn drop(&mut self) {
unsafe { ctru_sys::hidExit() };
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 @@ @@ -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.
//!
//! 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 apt;
@ -44,7 +50,4 @@ cfg_if::cfg_if! { @@ -44,7 +50,4 @@ cfg_if::cfg_if! {
}
}
pub use self::apt::Apt;
pub use self::hid::Hid;
pub(crate) use self::reference::ServiceReference;

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

@ -1,7 +1,21 @@ @@ -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;
use wave::{Wave, WaveStatus};
use wave::{Status, Wave};
use crate::error::ResultCode;
use crate::services::ServiceReference;
@ -14,60 +28,101 @@ use std::sync::Mutex; @@ -14,60 +28,101 @@ use std::sync::Mutex;
const NUMBER_OF_CHANNELS: u8 = 24;
/// Audio output mode.
#[doc(alias = "ndspOutputMode")]
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
#[repr(u32)]
pub enum OutputMode {
/// Single-Channel.
Mono = ctru_sys::NDSP_OUTPUT_MONO,
/// Dual-Channel.
Stereo = ctru_sys::NDSP_OUTPUT_STEREO,
/// Surround.
Surround = ctru_sys::NDSP_OUTPUT_SURROUND,
}
/// PCM formats supported by the audio engine.
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
#[repr(u32)]
pub enum AudioFormat {
/// PCM 8bit single-channel.
PCM8Mono = ctru_sys::NDSP_FORMAT_MONO_PCM8,
/// PCM 16bit single-channel.
PCM16Mono = ctru_sys::NDSP_FORMAT_MONO_PCM16,
/// PCM 8bit interleaved dual-channel.
PCM8Stereo = ctru_sys::NDSP_FORMAT_STEREO_PCM8,
/// PCM 16bit interleaved dual-channel.
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)]
pub struct AudioMix {
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)]
#[repr(u32)]
pub enum InterpolationType {
/// Polyphase interpolation.
Polyphase = ctru_sys::NDSP_INTERP_POLYPHASE,
/// Linear interpolation.
Linear = ctru_sys::NDSP_INTERP_LINEAR,
/// No interpolation.
None = ctru_sys::NDSP_INTERP_NONE,
}
/// Errors returned by [`ndsp`](self) functions.
#[non_exhaustive]
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum NdspError {
/// Channel ID
pub enum Error {
/// Channel with the specified ID does not exist.
InvalidChannel(u8),
/// Channel ID
/// Channel with the specified ID is already being used.
ChannelAlreadyInUse(u8),
/// Channel ID
/// The wave is already busy playing in the channel with the specified ID.
WaveBusy(u8),
/// Sample amount requested, Max sample amount
/// The sample amount requested was larger than the maximum.
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> {
id: u8,
_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 "instance" of this struct can exist at a time.
/// Only one handle for this service can exist at a time.
pub struct Ndsp {
_service_handler: ServiceReference,
channel_flags: [RefCell<()>; NUMBER_OF_CHANNELS as usize],
@ -78,12 +133,27 @@ impl Ndsp { @@ -78,12 +133,27 @@ impl Ndsp {
///
/// # Errors
///
/// This function will return an error if an instance of the `Ndsp` struct already exists
/// or if there are any issues during initialization.
/// This function will return an error if an instance of the [`Ndsp`] struct already exists
/// 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> {
let _service_handler = ServiceReference::new(
&NDSP_ACTIVE,
false,
|| {
ResultCode(unsafe { ctru_sys::ndspInit() })?;
@ -105,7 +175,22 @@ impl Ndsp { @@ -105,7 +175,22 @@ impl Ndsp {
/// # 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.
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);
match in_bounds {
@ -113,41 +198,145 @@ impl Ndsp { @@ -113,41 +198,145 @@ impl Ndsp {
let flag = ref_cell.try_borrow_mut();
match flag {
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) {
unsafe { ctru_sys::ndspSetOutputMode(mode.into()) };
}
}
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) {
unsafe { ctru_sys::ndspChnReset(self.id.into()) };
}
/// Initialize the channel's parameters
pub fn init_parameters(&self) {
/// Initialize the channel's parameters with default values.
///
/// # 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()) };
}
/// 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 {
unsafe { ctru_sys::ndspChnIsPlaying(self.id.into()) }
}
/// 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 {
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 {
self.id
}
@ -155,46 +344,161 @@ impl Channel<'_> { @@ -155,46 +344,161 @@ impl Channel<'_> {
/// 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.
#[doc(alias = "ndspChnGetSamplePos")]
pub fn sample_position(&self) -> usize {
(unsafe { ctru_sys::ndspChnGetSamplePos(self.id.into()) }) as usize
}
/// Returns the channel's current wave sequence's id.
#[doc(alias = "ndspChnGetWaveBufSeq")]
pub fn wave_sequence_id(&self) -> u16 {
unsafe { ctru_sys::ndspChnGetWaveBufSeq(self.id.into()) }
}
/// 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) {
unsafe { ctru_sys::ndspChnSetPaused(self.id.into(), state) };
}
/// 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) {
unsafe { ctru_sys::ndspChnSetFormat(self.id.into(), format.into()) };
}
/// 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) {
unsafe { ctru_sys::ndspChnSetInterp(self.id.into(), interp_type.into()) };
}
/// 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) {
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) {
unsafe { ctru_sys::ndspChnSetRate(self.id.into(), rate) };
}
// `ndspChnSetAdpcmCoefs` isn't wrapped on purpose.
// DSPADPCM is a proprietary format used by Nintendo, unavailable by "normal" means.
// We suggest using other wave formats when developing homebrew applications.
// TODO: wrap ADPCM format helpers.
/// 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) {
unsafe { ctru_sys::ndspChnWaveBufClear(self.id.into()) };
}
@ -204,11 +508,39 @@ impl Channel<'_> { @@ -204,11 +508,39 @@ impl Channel<'_> {
///
/// # Warning
///
/// `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.
pub fn queue_wave(&mut self, wave: &mut Wave) -> std::result::Result<(), NdspError> {
/// `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.
///
/// # 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() {
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<'_> { @@ -222,65 +554,74 @@ impl Channel<'_> {
/// 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<'_> {
/// Enables/disables monopole filters.
#[doc(alias = "ndspChnIirMonoSetEnable")]
pub fn iir_mono_set_enabled(&mut self, enable: bool) {
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
///
/// 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) {
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
///
/// 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) {
unsafe { ctru_sys::ndspChnIirMonoSetParamsLowPassFilter(self.id.into(), cut_off_freq) };
}
/// Enables/disables biquad filters.
#[doc(alias = "ndspChnIirBiquadSetEnable")]
pub fn iir_biquad_set_enabled(&mut self, enable: bool) {
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) {
unsafe {
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) {
unsafe {
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) {
unsafe {
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) {
unsafe {
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(
&mut self,
central_freq: f32,
@ -301,9 +642,10 @@ impl Channel<'_> { @@ -301,9 +642,10 @@ impl Channel<'_> {
impl AudioFormat {
/// Returns the amount of bytes needed to store one sample
///
/// Eg.
/// 8 bit mono formats return 1 (byte)
/// 16 bit stereo (dual-channel) formats return 4 (bytes)
/// # Example
///
/// - 8 bit mono formats return 1 (byte)
/// - 16 bit stereo (dual-channel) formats return 4 (bytes)
pub const fn size(self) -> usize {
match self {
Self::PCM8Mono => 1,
@ -314,7 +656,7 @@ impl AudioFormat { @@ -314,7 +656,7 @@ impl AudioFormat {
}
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 {
Self { raw: [0.; 12] }
}
@ -340,86 +682,70 @@ impl AudioMix { @@ -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).
pub fn aux_front(&self, id: usize) -> (f32, f32) {
if id > 1 {
panic!("invalid auxiliary output device index")
}
let index = 4 + id * 4;
pub fn aux_front(&self, id: AuxDevice) -> (f32, f32) {
let index = 4 + (id as usize * 4);
(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).
pub fn aux_back(&self, id: usize) -> (f32, f32) {
if id > 1 {
panic!("invalid auxiliary output device index")
}
let index = 6 + id * 4;
pub fn aux_back(&self, id: AuxDevice) -> (f32, f32) {
let index = 6 + (id as usize * 4);
(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
///
/// [Channel] will normalize the mix values to be within 0 and 1.
/// However, an [AudioMix] instance with larger/smaller values is valid.
/// [`Channel`] will normalize the mix values to be within 0 and 1.
/// However, an [`AudioMix`] instance with larger/smaller values is valid.
pub fn set_front(&mut self, left: f32, right: f32) {
self.raw[0] = left;
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
///
/// [Channel] will normalize the mix values to be within 0 and 1.
/// However, an [AudioMix] instance with larger/smaller values is valid.
/// [`Channel`] will normalize the mix values to be within 0 and 1.
/// However, an [`AudioMix`] instance with larger/smaller values is valid.
pub fn set_back(&mut self, left: f32, right: f32) {
self.raw[2] = left;
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
///
/// [Channel] will normalize the mix values to be within 0 and 1.
/// However, an [AudioMix] instance with larger/smaller values is valid.
pub fn set_aux_front(&mut self, left: f32, right: f32, id: usize) {
if id > 1 {
panic!("invalid auxiliary output device index")
}
let index = 4 + id * 4;
/// [`Channel`] will normalize the mix values to be within 0 and 1.
/// However, an [`AudioMix`] instance with larger/smaller values is valid.
pub fn set_aux_front(&mut self, left: f32, right: f32, id: AuxDevice) {
let index = 4 + (id as usize * 4);
self.raw[index] = left;
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
///
/// [Channel] will normalize the mix values to be within 0 and 1.
/// However, an [AudioMix] instance with larger/smaller values is valid.
pub fn set_aux_back(&mut self, left: f32, right: f32, id: usize) {
if id > 1 {
panic!("invalid auxiliary output device index")
}
let index = 6 + id * 4;
/// [`Channel`] will normalize the mix values to be within 0 and 1.
/// However, an [`AudioMix`] instance with larger/smaller values is valid.
pub fn set_aux_back(&mut self, left: f32, right: f32, id: AuxDevice) {
let index = 6 + (id as usize * 4);
self.raw[index] = left;
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 {
/// Returns an [`AudioMix`] object with "front left" and "front right" volumes set to 100%, and all other volumes set to 0%.
fn default() -> Self {
let mut mix = AudioMix::zeroed();
mix.set_front(1.0, 1.0);
@ -434,7 +760,7 @@ impl From<[f32; 12]> for AudioMix { @@ -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 {
match self {
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 { @@ -445,9 +771,10 @@ impl fmt::Display for NdspError {
}
}
impl error::Error for NdspError {}
impl error::Error for Error {}
impl Drop for Ndsp {
#[doc(alias = "ndspExit")]
fn drop(&mut self) {
for i in 0..NUMBER_OF_CHANNELS {
self.channel(i).unwrap().reset();

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

@ -1,7 +1,13 @@ @@ -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;
/// 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 {
/// Data block of the audio wave (and its format information).
buffer: Box<[u8], LinearAllocator>,
@ -13,16 +19,37 @@ pub struct Wave { @@ -13,16 +19,37 @@ pub struct Wave {
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
#[repr(u8)]
/// Enum representing the playback status of a [Wave].
pub enum WaveStatus {
/// Playback status of a [`Wave`].
pub enum Status {
/// Wave has never been used.
Free = ctru_sys::NDSP_WBUF_FREE as u8,
/// Wave is currently queued for usage.
Queued = ctru_sys::NDSP_WBUF_QUEUED as u8,
/// Wave is currently playing.
Playing = ctru_sys::NDSP_WBUF_PLAYING as u8,
/// Wave has finished playing.
Done = ctru_sys::NDSP_WBUF_DONE as u8,
}
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(
buffer: Box<[u8], LinearAllocator>,
audio_format: AudioFormat,
@ -60,41 +87,61 @@ impl Wave { @@ -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] {
&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
///
/// 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.
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() {
WaveStatus::Playing | WaveStatus::Queued => {
Err(NdspError::WaveBusy(self.played_on_channel.unwrap()))
Status::Playing | Status::Queued => {
Err(Error::WaveBusy(self.played_on_channel.unwrap()))
}
_ => Ok(&mut self.buffer),
}
}
/// Return this wave's playback status.
pub fn status(&self) -> WaveStatus {
/// Returns this wave's playback status.
///
/// # 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()
}
/// Get the amounts of samples *read* by the NDSP process.
/// Returns the amount of samples *read* by the NDSP process.
///
/// # 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 {
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 {
self.audio_format
}
@ -107,21 +154,21 @@ impl Wave { @@ -107,21 +154,21 @@ impl Wave {
}
/// Set the amount of samples to be read.
/// This function doesn't resize the internal buffer.
///
/// # 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
///
/// This function will return an error if the sample size exceeds the buffer's capacity
/// or if the [Wave] is currently queued.
pub fn set_sample_count(&mut self, sample_count: usize) -> Result<(), NdspError> {
/// or if the [`Wave`] is currently queued.
pub fn set_sample_count(&mut self, sample_count: usize) -> Result<(), Error> {
match self.status() {
WaveStatus::Playing | WaveStatus::Queued => {
return Err(NdspError::WaveBusy(self.played_on_channel.unwrap()));
Status::Playing | Status::Queued => {
return Err(Error::WaveBusy(self.played_on_channel.unwrap()));
}
_ => (),
}
@ -129,7 +176,7 @@ impl Wave { @@ -129,7 +176,7 @@ impl Wave {
let max_count = self.buffer.len() / self.audio_format.size();
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;
@ -138,7 +185,7 @@ impl Wave { @@ -138,7 +185,7 @@ impl Wave {
}
}
impl TryFrom<u8> for WaveStatus {
impl TryFrom<u8> for Status {
type Error = &'static str;
fn try_from(value: u8) -> Result<Self, Self::Error> {
@ -157,7 +204,7 @@ impl Drop for Wave { @@ -157,7 +204,7 @@ impl Drop for 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`.
match self.status() {
WaveStatus::Free | WaveStatus::Done => (),
Status::Free | Status::Done => (),
// If the status flag 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 @@ @@ -1,40 +1,81 @@
//! Process Services (PS) module. This is used for miscellaneous utility tasks, but
//! is particularly important because it is used to generate random data, which
//! is required for common things like [`HashMap`](std::collections::HashMap).
//! Process Services.
//!
//! 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>
use crate::error::ResultCode;
use crate::Result;
/// Type of AES algorithm to use.
#[doc(alias = "PS_AESAlgorithm")]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u32)]
pub enum AESAlgorithm {
/// CBC encryption.
CbcEnc = ctru_sys::PS_ALGORITHM_CBC_ENC,
/// CBC decryption.
CbcDec = ctru_sys::PS_ALGORITHM_CBC_DEC,
/// CTR encryption.
CtrEnc = ctru_sys::PS_ALGORITHM_CTR_ENC,
/// CTR decryption.
CtrDec = ctru_sys::PS_ALGORITHM_CTR_DEC,
/// CCM encryption.
CcmEnc = ctru_sys::PS_ALGORITHM_CCM_ENC,
/// CCM decryption.
CcmDec = ctru_sys::PS_ALGORITHM_CCM_DEC,
}
/// PS Key slot to use.
#[doc(alias = "PS_AESKeyType")]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u32)]
pub enum AESKeyType {
/// Keyslot 0x0D.
Keyslot0D = ctru_sys::PS_KEYSLOT_0D,
/// Keyslot 0x2D.
Keyslot2D = ctru_sys::PS_KEYSLOT_2D,
/// Keyslot 0x2E.
Keyslot2E = ctru_sys::PS_KEYSLOT_2E,
/// Keyslot 0x31.
Keyslot31 = ctru_sys::PS_KEYSLOT_31,
/// Keyslot 0x32.
Keyslot32 = ctru_sys::PS_KEYSLOT_32,
/// Keyslot 0x36.
Keyslot36 = ctru_sys::PS_KEYSLOT_36,
/// Keyslot 0x38.
Keyslot38 = ctru_sys::PS_KEYSLOT_38,
/// Keyslot 0x39 (DLP).
Keyslot39Dlp = ctru_sys::PS_KEYSLOT_39_DLP,
/// Keyslot 0x39 (NFC).
Keyslot39Nfc = ctru_sys::PS_KEYSLOT_39_NFC,
/// Invalid keyslot.
KeyslotInvalid = ctru_sys::PS_KEYSLOT_INVALID,
}
/// Handle to the PS service.
pub struct 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> {
unsafe {
ResultCode(ctru_sys::psInit())?;
@ -42,6 +83,24 @@ impl Ps { @@ -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> {
let mut seed: u64 = 0;
@ -49,6 +108,24 @@ impl Ps { @@ -49,6 +108,24 @@ impl Ps {
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> {
let mut id: u32 = 0;
@ -56,6 +133,27 @@ impl Ps { @@ -56,6 +133,27 @@ impl Ps {
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<()> {
ResultCode(unsafe {
ctru_sys::PS_GenerateRandomBytes(out.as_mut_ptr().cast(), out.len())
@ -65,6 +163,7 @@ impl Ps { @@ -65,6 +163,7 @@ impl Ps {
}
impl Drop for Ps {
#[doc(alias = "psExit")]
fn drop(&mut self) {
unsafe {
ctru_sys::psExit();

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

@ -1,36 +1,37 @@ @@ -1,36 +1,37 @@
use crate::Error;
use std::sync::Mutex;
use std::sync::{Mutex, MutexGuard, TryLockError};
pub(crate) struct ServiceReference {
counter: &'static Mutex<usize>,
_guard: MutexGuard<'static, ()>,
close: Box<dyn Fn() + Send + Sync>,
}
impl ServiceReference {
pub fn new<S, E>(
counter: &'static Mutex<usize>,
allow_multiple: bool,
start: S,
close: E,
) -> crate::Result<Self>
pub fn new<S, E>(counter: &'static Mutex<()>, start: S, close: E) -> crate::Result<Self>
where
S: FnOnce() -> crate::Result<()>,
E: Fn() + Send + Sync + 'static,
{
let mut value = counter
.lock()
.expect("Mutex Counter for ServiceReference is poisoned"); // todo: handle poisoning
let _guard = match counter.try_lock() {
Ok(lock) => lock,
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 {
start()?;
} else if !allow_multiple {
return Err(Error::ServiceAlreadyActive);
}
guard.into_inner()
}
TryLockError::WouldBlock => return Err(Error::ServiceAlreadyActive),
},
};
*value += 1;
start()?;
Ok(Self {
counter,
_guard,
close: Box::new(close),
})
}
@ -38,13 +39,6 @@ impl ServiceReference { @@ -38,13 +39,6 @@ impl ServiceReference {
impl Drop for ServiceReference {
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 @@ @@ -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`
//! 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
//! your package.
//!
@ -11,6 +16,16 @@ @@ -11,6 +16,16 @@
//! [package.metadata.cargo-3ds]
//! 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 std::ffi::CStr;
@ -18,17 +33,37 @@ use std::sync::Mutex; @@ -18,17 +33,37 @@ use std::sync::Mutex;
use crate::services::ServiceReference;
/// Handle to the RomFS service.
pub struct RomFS {
_service_handler: ServiceReference,
}
static ROMFS_ACTIVE: Mutex<usize> = Mutex::new(0);
static ROMFS_ACTIVE: Mutex<()> = Mutex::new(());
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> {
let _service_handler = ServiceReference::new(
&ROMFS_ACTIVE,
true,
|| {
let mount_name = CStr::from_bytes_with_nul(b"romfs\0").unwrap();
ResultCode(unsafe { ctru_sys::romfsMountSelf(mount_name.as_ptr()) })?;
@ -36,7 +71,7 @@ impl RomFS { @@ -36,7 +71,7 @@ impl RomFS {
},
|| {
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 { @@ -48,17 +83,15 @@ impl RomFS {
mod tests {
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]
fn romfs_counter() {
let _romfs = RomFS::new().unwrap();
let value = *ROMFS_ACTIVE.lock().unwrap();
assert_eq!(value, 1);
drop(_romfs);
#[should_panic]
fn romfs_lock() {
let romfs = RomFS::new().unwrap();
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 @@ @@ -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 std::net::Ipv4Addr;
@ -8,37 +13,66 @@ use crate::error::ResultCode; @@ -8,37 +13,66 @@ use crate::error::ResultCode;
use crate::services::ServiceReference;
use crate::Error;
/// 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.
/// Handle to the Network Socket service.
pub struct Soc {
_service_handler: ServiceReference,
sock_3dslink: libc::c_int,
}
static SOC_ACTIVE: Mutex<usize> = Mutex::new(0);
static SOC_ACTIVE: Mutex<()> = Mutex::new(());
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
///
/// 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> {
Self::init_with_buffer_size(0x100000)
}
/// Initialize the Soc service with a custom buffer size in bytes. The size should be
/// 0x100000 bytes or greater.
/// Initialize a new service handle using a custom socket buffer size.
///
/// The size should be `0x100000` bytes or greater.
///
/// # 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> {
let _service_handler = ServiceReference::new(
&SOC_ACTIVE,
false,
|| {
let soc_mem = unsafe { memalign(0x1000, num_bytes) } as *mut u32;
ResultCode(unsafe { ctru_sys::socInit(soc_mem, num_bytes as u32) })?;
@ -60,19 +94,61 @@ impl Soc { @@ -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 {
let raw_id = unsafe { libc::gethostid() };
Ipv4Addr::from(raw_id.to_ne_bytes())
}
/// Redirect output streams (i.e. [`println`] and [`eprintln`]) to the `3dslink` server.
/// Requires `3dslink` >= 0.6.1 and `new-hbmenu` >= 2.3.0.
/// Redirect output streams (i.e. `stdout` and `stderr`) to the `3dslink` server.
///
/// 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
///
/// Returns an error if a connection cannot be established to the server, or
/// output was already previously redirected.
/// Returns an error if a connection cannot be established to the server,
/// 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<()> {
if self.sock_3dslink >= 0 {
return Err(Error::OutputAlreadyRedirected);
@ -92,6 +168,7 @@ impl Soc { @@ -92,6 +168,7 @@ impl Soc {
}
impl Drop for Soc {
#[doc(alias = "socExit")]
fn drop(&mut self) {
if self.sock_3dslink >= 0 {
unsafe {

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

@ -1,13 +1,30 @@ @@ -1,13 +1,30 @@
//! SSLC (TLS) service
//! SSLC (TLS) service.
// TODO: Implement remaining functions
use crate::error::ResultCode;
/// Handle to the SSLC service.
pub struct 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> {
unsafe {
ResultCode(ctru_sys::sslcInit(0))?;
@ -17,6 +34,7 @@ impl SslC { @@ -17,6 +34,7 @@ impl SslC {
}
impl Drop for SslC {
#[doc(alias = "sslcExit")]
fn drop(&mut self) {
unsafe { ctru_sys::sslcExit() };
}

73
ctru-rs/src/test_runner.rs

@ -1,73 +0,0 @@ @@ -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"),
}
}

17
ctru-sys/Cargo.toml

@ -1,7 +1,12 @@ @@ -1,7 +1,12 @@
[package]
name = "ctru-sys"
version = "21.2.0+2.1.2-1"
authors = [ "Rust3DS Org", "Ronald Kinard <furyhunter600@gmail.com>" ]
version = "22.2.0+2.2.2-1"
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"
links = "ctru"
edition = "2021"
@ -10,4 +15,12 @@ edition = "2021" @@ -10,4 +15,12 @@ edition = "2021"
libc = { version = "0.2.121", default-features = false }
[build-dependencies]
bindgen = { version = "0.65.1", features = ["experimental"] }
cc = "1.0"
doxygen-rs = "0.4.2"
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 @@ @@ -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 @@ @@ -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 @@ @@ -1,14 +1,29 @@
use bindgen::callbacks::ParseCallbacks;
use bindgen::{Builder, RustTarget};
use std::env;
use std::error::Error;
use std::path::{Path, PathBuf};
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() {
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 out_dir = PathBuf::from(env::var("OUT_DIR").unwrap());
println!("cargo:rerun-if-changed=build.rs");
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!(
"cargo:rustc-link-lib=static={}",
match profile.as_str() {
@ -30,9 +45,97 @@ fn main() { @@ -30,9 +45,97 @@ fn main() {
}
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
.split(|c| c == '.' || c == '-')
.map(String::from)
@ -68,7 +171,8 @@ fn check_libctru_version() -> Result<(String, String, String), Box<dyn Error>> { @@ -68,7 +171,8 @@ fn check_libctru_version() -> Result<(String, String, String), Box<dyn Error>> {
if lib_version != crate_built_version {
return Err(format!(
"libctru version is {lib_version} but this crate was built for {crate_built_version}"
))?;
)
.into());
}
let Output { stdout, .. } = Command::new(pacman)
@ -77,12 +181,13 @@ fn check_libctru_version() -> Result<(String, String, String), Box<dyn Error>> { @@ -77,12 +181,13 @@ fn check_libctru_version() -> Result<(String, String, String), Box<dyn Error>> {
.output()?;
for line in String::from_utf8_lossy(&stdout).split('\n') {
let Some((_pkg, file)) = line.split_once(char::is_whitespace)
else { continue };
let Some((_pkg, file)) = line.split_once(char::is_whitespace) else {
continue;
};
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))
}

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

@ -1,7 +0,0 @@ @@ -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 @@ @@ -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 @@ @@ -7,17 +7,14 @@
use core::arch::asm;
pub mod result;
mod bindings;
pub use bindings::*;
pub use result::*;
include!(concat!(env!("OUT_DIR"), "/bindings.rs"));
/// In lieu of a proper errno function exposed by libc
/// (<https://github.com/rust-lang/libc/issues/1995>), this will retrieve the
/// last error set in the global reentrancy struct.
/// (<https://github.com/rust-lang/libc/issues/1995>).
pub unsafe fn errno() -> s32 {
(*__getreent())._errno
*__errno()
}
pub unsafe fn getThreadLocalStorage() -> *mut libc::c_void {
@ -29,3 +26,11 @@ 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 {
(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