mirror of
https://github.com/italicsjenga/gba.git
synced 2024-12-23 19:01:30 +11:00
Revert "Deploy rust-console/gba to github.com/rust-console/gba.git:master"
This reverts commit 2f1c243742
.
This commit is contained in:
parent
2f1c243742
commit
a4b6a4e8b2
14
.gitignore
vendored
Normal file
14
.gitignore
vendored
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
# Generated by Cargo
|
||||||
|
# will have compiled files and executables
|
||||||
|
/target/
|
||||||
|
|
||||||
|
# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries
|
||||||
|
# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html
|
||||||
|
Cargo.lock
|
||||||
|
|
||||||
|
# These are backup files generated by rustfmt
|
||||||
|
**/*.rs.bk
|
||||||
|
|
||||||
|
# The crt0.o file should, under the currently suggested build scheme, be
|
||||||
|
# recompiled every build anyway.
|
||||||
|
crt0.o
|
30
.travis.yml
Normal file
30
.travis.yml
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
language: rust
|
||||||
|
sudo: false
|
||||||
|
|
||||||
|
cache:
|
||||||
|
- cargo
|
||||||
|
|
||||||
|
rust:
|
||||||
|
- nightly
|
||||||
|
|
||||||
|
before_script:
|
||||||
|
- (test -x $HOME/.cargo/bin/cargo-install-update || cargo install cargo-update)
|
||||||
|
- (test -x $HOME/.cargo/bin/mdbook || cargo install --vers "^0.1" mdbook)
|
||||||
|
- cargo install-update -a
|
||||||
|
|
||||||
|
script:
|
||||||
|
- cargo check && cargo check --release
|
||||||
|
# at the moment we DO NOT run "cargo test" because of lang-item issues
|
||||||
|
- cd book && mdbook build
|
||||||
|
|
||||||
|
deploy:
|
||||||
|
provider: pages
|
||||||
|
skip-cleanup: true
|
||||||
|
github-token: $GITHUB_TOKEN
|
||||||
|
local-dir: docs
|
||||||
|
target-branch: master
|
||||||
|
keep-history: true
|
||||||
|
name: DocsBot
|
||||||
|
verbose: true
|
||||||
|
on:
|
||||||
|
branch: master
|
17
Cargo.toml
Normal file
17
Cargo.toml
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
[package]
|
||||||
|
name = "gba"
|
||||||
|
description = "A crate (and book) for making GBA games with Rust."
|
||||||
|
version = "0.1.0"
|
||||||
|
authors = ["Lokathor <zefria@gmail.com>", "Ketsuban"]
|
||||||
|
repository = "https://github.com/rust-console/gba"
|
||||||
|
readme = "README.md"
|
||||||
|
keywords = ["gba"]
|
||||||
|
edition = "2018"
|
||||||
|
license = "Apache-2.0"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
gba-proc-macro = "0.1.1"
|
||||||
|
|
||||||
|
[profile.release]
|
||||||
|
lto = true
|
||||||
|
panic = "abort"
|
201
LICENSE-APACHE2.txt
Normal file
201
LICENSE-APACHE2.txt
Normal file
|
@ -0,0 +1,201 @@
|
||||||
|
Apache License
|
||||||
|
Version 2.0, January 2004
|
||||||
|
http://www.apache.org/licenses/
|
||||||
|
|
||||||
|
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||||
|
|
||||||
|
1. Definitions.
|
||||||
|
|
||||||
|
"License" shall mean the terms and conditions for use, reproduction,
|
||||||
|
and distribution as defined by Sections 1 through 9 of this document.
|
||||||
|
|
||||||
|
"Licensor" shall mean the copyright owner or entity authorized by
|
||||||
|
the copyright owner that is granting the License.
|
||||||
|
|
||||||
|
"Legal Entity" shall mean the union of the acting entity and all
|
||||||
|
other entities that control, are controlled by, or are under common
|
||||||
|
control with that entity. For the purposes of this definition,
|
||||||
|
"control" means (i) the power, direct or indirect, to cause the
|
||||||
|
direction or management of such entity, whether by contract or
|
||||||
|
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||||
|
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||||
|
|
||||||
|
"You" (or "Your") shall mean an individual or Legal Entity
|
||||||
|
exercising permissions granted by this License.
|
||||||
|
|
||||||
|
"Source" form shall mean the preferred form for making modifications,
|
||||||
|
including but not limited to software source code, documentation
|
||||||
|
source, and configuration files.
|
||||||
|
|
||||||
|
"Object" form shall mean any form resulting from mechanical
|
||||||
|
transformation or translation of a Source form, including but
|
||||||
|
not limited to compiled object code, generated documentation,
|
||||||
|
and conversions to other media types.
|
||||||
|
|
||||||
|
"Work" shall mean the work of authorship, whether in Source or
|
||||||
|
Object form, made available under the License, as indicated by a
|
||||||
|
copyright notice that is included in or attached to the work
|
||||||
|
(an example is provided in the Appendix below).
|
||||||
|
|
||||||
|
"Derivative Works" shall mean any work, whether in Source or Object
|
||||||
|
form, that is based on (or derived from) the Work and for which the
|
||||||
|
editorial revisions, annotations, elaborations, or other modifications
|
||||||
|
represent, as a whole, an original work of authorship. For the purposes
|
||||||
|
of this License, Derivative Works shall not include works that remain
|
||||||
|
separable from, or merely link (or bind by name) to the interfaces of,
|
||||||
|
the Work and Derivative Works thereof.
|
||||||
|
|
||||||
|
"Contribution" shall mean any work of authorship, including
|
||||||
|
the original version of the Work and any modifications or additions
|
||||||
|
to that Work or Derivative Works thereof, that is intentionally
|
||||||
|
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||||
|
or by an individual or Legal Entity authorized to submit on behalf of
|
||||||
|
the copyright owner. For the purposes of this definition, "submitted"
|
||||||
|
means any form of electronic, verbal, or written communication sent
|
||||||
|
to the Licensor or its representatives, including but not limited to
|
||||||
|
communication on electronic mailing lists, source code control systems,
|
||||||
|
and issue tracking systems that are managed by, or on behalf of, the
|
||||||
|
Licensor for the purpose of discussing and improving the Work, but
|
||||||
|
excluding communication that is conspicuously marked or otherwise
|
||||||
|
designated in writing by the copyright owner as "Not a Contribution."
|
||||||
|
|
||||||
|
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||||
|
on behalf of whom a Contribution has been received by Licensor and
|
||||||
|
subsequently incorporated within the Work.
|
||||||
|
|
||||||
|
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
copyright license to reproduce, prepare Derivative Works of,
|
||||||
|
publicly display, publicly perform, sublicense, and distribute the
|
||||||
|
Work and such Derivative Works in Source or Object form.
|
||||||
|
|
||||||
|
3. Grant of Patent License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
(except as stated in this section) patent license to make, have made,
|
||||||
|
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||||
|
where such license applies only to those patent claims licensable
|
||||||
|
by such Contributor that are necessarily infringed by their
|
||||||
|
Contribution(s) alone or by combination of their Contribution(s)
|
||||||
|
with the Work to which such Contribution(s) was submitted. If You
|
||||||
|
institute patent litigation against any entity (including a
|
||||||
|
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||||
|
or a Contribution incorporated within the Work constitutes direct
|
||||||
|
or contributory patent infringement, then any patent licenses
|
||||||
|
granted to You under this License for that Work shall terminate
|
||||||
|
as of the date such litigation is filed.
|
||||||
|
|
||||||
|
4. Redistribution. You may reproduce and distribute copies of the
|
||||||
|
Work or Derivative Works thereof in any medium, with or without
|
||||||
|
modifications, and in Source or Object form, provided that You
|
||||||
|
meet the following conditions:
|
||||||
|
|
||||||
|
(a) You must give any other recipients of the Work or
|
||||||
|
Derivative Works a copy of this License; and
|
||||||
|
|
||||||
|
(b) You must cause any modified files to carry prominent notices
|
||||||
|
stating that You changed the files; and
|
||||||
|
|
||||||
|
(c) You must retain, in the Source form of any Derivative Works
|
||||||
|
that You distribute, all copyright, patent, trademark, and
|
||||||
|
attribution notices from the Source form of the Work,
|
||||||
|
excluding those notices that do not pertain to any part of
|
||||||
|
the Derivative Works; and
|
||||||
|
|
||||||
|
(d) If the Work includes a "NOTICE" text file as part of its
|
||||||
|
distribution, then any Derivative Works that You distribute must
|
||||||
|
include a readable copy of the attribution notices contained
|
||||||
|
within such NOTICE file, excluding those notices that do not
|
||||||
|
pertain to any part of the Derivative Works, in at least one
|
||||||
|
of the following places: within a NOTICE text file distributed
|
||||||
|
as part of the Derivative Works; within the Source form or
|
||||||
|
documentation, if provided along with the Derivative Works; or,
|
||||||
|
within a display generated by the Derivative Works, if and
|
||||||
|
wherever such third-party notices normally appear. The contents
|
||||||
|
of the NOTICE file are for informational purposes only and
|
||||||
|
do not modify the License. You may add Your own attribution
|
||||||
|
notices within Derivative Works that You distribute, alongside
|
||||||
|
or as an addendum to the NOTICE text from the Work, provided
|
||||||
|
that such additional attribution notices cannot be construed
|
||||||
|
as modifying the License.
|
||||||
|
|
||||||
|
You may add Your own copyright statement to Your modifications and
|
||||||
|
may provide additional or different license terms and conditions
|
||||||
|
for use, reproduction, or distribution of Your modifications, or
|
||||||
|
for any such Derivative Works as a whole, provided Your use,
|
||||||
|
reproduction, and distribution of the Work otherwise complies with
|
||||||
|
the conditions stated in this License.
|
||||||
|
|
||||||
|
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||||
|
any Contribution intentionally submitted for inclusion in the Work
|
||||||
|
by You to the Licensor shall be under the terms and conditions of
|
||||||
|
this License, without any additional terms or conditions.
|
||||||
|
Notwithstanding the above, nothing herein shall supersede or modify
|
||||||
|
the terms of any separate license agreement you may have executed
|
||||||
|
with Licensor regarding such Contributions.
|
||||||
|
|
||||||
|
6. Trademarks. This License does not grant permission to use the trade
|
||||||
|
names, trademarks, service marks, or product names of the Licensor,
|
||||||
|
except as required for reasonable and customary use in describing the
|
||||||
|
origin of the Work and reproducing the content of the NOTICE file.
|
||||||
|
|
||||||
|
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||||
|
agreed to in writing, Licensor provides the Work (and each
|
||||||
|
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||||
|
implied, including, without limitation, any warranties or conditions
|
||||||
|
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||||
|
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||||
|
appropriateness of using or redistributing the Work and assume any
|
||||||
|
risks associated with Your exercise of permissions under this License.
|
||||||
|
|
||||||
|
8. Limitation of Liability. In no event and under no legal theory,
|
||||||
|
whether in tort (including negligence), contract, or otherwise,
|
||||||
|
unless required by applicable law (such as deliberate and grossly
|
||||||
|
negligent acts) or agreed to in writing, shall any Contributor be
|
||||||
|
liable to You for damages, including any direct, indirect, special,
|
||||||
|
incidental, or consequential damages of any character arising as a
|
||||||
|
result of this License or out of the use or inability to use the
|
||||||
|
Work (including but not limited to damages for loss of goodwill,
|
||||||
|
work stoppage, computer failure or malfunction, or any and all
|
||||||
|
other commercial damages or losses), even if such Contributor
|
||||||
|
has been advised of the possibility of such damages.
|
||||||
|
|
||||||
|
9. Accepting Warranty or Additional Liability. While redistributing
|
||||||
|
the Work or Derivative Works thereof, You may choose to offer,
|
||||||
|
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||||
|
or other liability obligations and/or rights consistent with this
|
||||||
|
License. However, in accepting such obligations, You may act only
|
||||||
|
on Your own behalf and on Your sole responsibility, not on behalf
|
||||||
|
of any other Contributor, and only if You agree to indemnify,
|
||||||
|
defend, and hold each Contributor harmless for any liability
|
||||||
|
incurred by, or claims asserted against, such Contributor by reason
|
||||||
|
of your accepting any such warranty or additional liability.
|
||||||
|
|
||||||
|
END OF TERMS AND CONDITIONS
|
||||||
|
|
||||||
|
APPENDIX: How to apply the Apache License to your work.
|
||||||
|
|
||||||
|
To apply the Apache License to your work, attach the following
|
||||||
|
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||||
|
replaced with your own identifying information. (Don't include
|
||||||
|
the brackets!) The text should be enclosed in the appropriate
|
||||||
|
comment syntax for the file format. We also recommend that a
|
||||||
|
file or class name and description of purpose be included on the
|
||||||
|
same "printed page" as the copyright notice for easier
|
||||||
|
identification within third-party archives.
|
||||||
|
|
||||||
|
Copyright [yyyy] [name of copyright owner]
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
17
README.md
Normal file
17
README.md
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
[![License:Apache2](https://img.shields.io/badge/License-Apache2-green.svg)](https://www.apache.org/licenses/LICENSE-2.0)
|
||||||
|
|
||||||
|
# gba
|
||||||
|
|
||||||
|
A crate that helps you make GBA games
|
||||||
|
|
||||||
|
# First Time Setup
|
||||||
|
|
||||||
|
This crate requires a fair amount of special setup. All of the steps are
|
||||||
|
detailed for you [in the 0th chapter of the
|
||||||
|
book](https://rust-console.github.io/gba/ch0/index.html) that goes with this
|
||||||
|
crate.
|
||||||
|
|
||||||
|
# Contribution
|
||||||
|
|
||||||
|
This crate is Apache2 licensed and any contributions you submit must also be
|
||||||
|
Apache2 licensed.
|
8
book/book.toml
Normal file
8
book/book.toml
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
[book]
|
||||||
|
title = "Rust GBA Tutorials"
|
||||||
|
authors = ["Lokathor"]
|
||||||
|
#description = "Rust GBA Tutorials."
|
||||||
|
|
||||||
|
[build]
|
||||||
|
build-dir = "../docs"
|
||||||
|
create-missing = true
|
15
book/src/SUMMARY.md
Normal file
15
book/src/SUMMARY.md
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
|
||||||
|
# Rust GBA Tutorials
|
||||||
|
|
||||||
|
* [Introduction](introduction.md)
|
||||||
|
* [Ch 0: Development Setup](ch0/index.md)
|
||||||
|
* [Ch 1: Hello GBA](ch1/index.md)
|
||||||
|
* [hello1](ch1/hello1.md)
|
||||||
|
* [IO Registers](ch1/io_registers.md)
|
||||||
|
* [The Display Control Register](ch1/the_display_control_register.md)
|
||||||
|
* [Video Memory Intro](ch1/video_memory_intro.md)
|
||||||
|
* [hello2](ch1/hello2.md)
|
||||||
|
* [Ch 2: User Input](ch2/index.md)
|
||||||
|
* [The Key Input Register](ch2/the_key_input_register.md)
|
||||||
|
* [The VCount Register](ch2/the_vcount_register.md)
|
||||||
|
* [light_cycle](ch2/light_cycle.md)
|
134
book/src/ch0/index.md
Normal file
134
book/src/ch0/index.md
Normal file
|
@ -0,0 +1,134 @@
|
||||||
|
# Chapter 0: Development Setup
|
||||||
|
|
||||||
|
Before you can build a GBA game you'll have to follow some special steps to
|
||||||
|
setup the development environment. Perhaps unfortunately, there's enough detail
|
||||||
|
here to warrant a mini-chapter all on its own.
|
||||||
|
|
||||||
|
Before we begin I'd like to give a special thanks to **Ketsuban**, who is the
|
||||||
|
wizard that arranged for all of this to be able to happen and laid out the
|
||||||
|
details of the plan to the rest of the world.
|
||||||
|
|
||||||
|
## Per System Setup
|
||||||
|
|
||||||
|
Obviously you need your computer to have a working rust installation. However,
|
||||||
|
you'll also need to ensure that you're using a nightly toolchain. You can run
|
||||||
|
`rustup default nightly` to set nightly as the system wide default toolchain, or
|
||||||
|
you can use a [toolchain
|
||||||
|
file](https://github.com/rust-lang-nursery/rustup.rs#the-toolchain-file) to use
|
||||||
|
nightly just on a specific project, but either way we'll be assuming nightly
|
||||||
|
from now on.
|
||||||
|
|
||||||
|
Next you need [devkitpro](https://devkitpro.org/wiki/Getting_Started). They've
|
||||||
|
got a graphical installer for Windows, and `pacman` support on Linux. We'll be
|
||||||
|
using a few of their binutils for the `arm-none-eabi` target, and we'll also be
|
||||||
|
using some of their tools that are specific to GBA development, so _even if_ you
|
||||||
|
already have the right binutils for whatever reason, you'll still want devkitpro
|
||||||
|
for the `gbafix` utility.
|
||||||
|
|
||||||
|
* On Windows you'll want something like `C:\devkitpro\devkitARM\bin` and
|
||||||
|
`C:\devkitpro\tools\bin` to be [added to your
|
||||||
|
PATH](https://stackoverflow.com/q/44272416/455232), depending on where you
|
||||||
|
installed it to and such.
|
||||||
|
* On Linux you'll also want it to be added to your path, but if you're using
|
||||||
|
Linux I'll just assume you know how to do all that.
|
||||||
|
|
||||||
|
Finally, you'll need `cargo-xbuild`. Just run `cargo install cargo-xbuild` and
|
||||||
|
cargo will figure it all out for you.
|
||||||
|
|
||||||
|
## Per Project Setup
|
||||||
|
|
||||||
|
Now you'll need some particular files each time you want to start a new project.
|
||||||
|
You can find them in the root of the [rust-console/gba
|
||||||
|
repo](https://github.com/rust-console/gba).
|
||||||
|
|
||||||
|
* `thumbv4-none-agb.json` describes the overall GBA to cargo-xbuild so it knows
|
||||||
|
what to do. This is actually a somewhat made up target name since there's no
|
||||||
|
official target name. The GBA is essentially the same as a normal
|
||||||
|
`thumbv4-none-eabi` device, but we give it the "agb" signifier so that later
|
||||||
|
on we'll be able to use rust's `cfg` ability to allow our code to know if it's
|
||||||
|
specifically targeting a GBA or some other similar device (like an NDS).
|
||||||
|
* `crt0.s` describes some ASM startup stuff. If you have more ASM to place here
|
||||||
|
later on this is where you can put it. You also need to build it into a
|
||||||
|
`crt0.o` file before it can actually be used, but we'll cover that below.
|
||||||
|
* `linker.ld` tells the linker more critical info about the layout expectations
|
||||||
|
that the GBA has about our program.
|
||||||
|
|
||||||
|
## Compiling
|
||||||
|
|
||||||
|
The next steps only work once you've got some source code to build. If you need
|
||||||
|
a quick test, copy the `hello1.rs` file from our examples directory in the
|
||||||
|
repository.
|
||||||
|
|
||||||
|
Once you've got something to build, you perform the following steps:
|
||||||
|
|
||||||
|
* `arm-none-eabi-as crt0.s -o crt0.o`
|
||||||
|
* This builds your text format `crt0.s` file into object format `crt0.o`. You
|
||||||
|
don't need to perform it every time, only when `crt0.s` changes, but you
|
||||||
|
might as well do it every time so that you never forget to because it's a
|
||||||
|
practically instant operation.
|
||||||
|
|
||||||
|
* `cargo xbuild --target thumbv4-none-agb.json`
|
||||||
|
* This builds your Rust source. It accepts _most of_ the normal options, such
|
||||||
|
as `--release`, and options, such as `--bin foo` or `--examples`, that you'd
|
||||||
|
expect `cargo` to accept.
|
||||||
|
* You **can not** build and run tests this way, because they require `std`,
|
||||||
|
which the GBA doesn't have. You can still run some of your project's tests
|
||||||
|
with `cargo test`, but that builds for your local machine, so anything
|
||||||
|
specific to the GBA (such as reading and writing registers) won't be
|
||||||
|
testable that way. If you want to isolate and try out some piece code
|
||||||
|
running on the GBA you'll unfortunately have to make a demo for it in your
|
||||||
|
`examples/` directory and then run the demo in an emulator and see if it
|
||||||
|
does what you expect.
|
||||||
|
* The file extension is important. `cargo xbuild` takes it as a flag to
|
||||||
|
compile dependencies with the same sysroot, so you can include crates
|
||||||
|
normally. Well, creates that work in the GBA's limited environment, but you
|
||||||
|
get the idea.
|
||||||
|
|
||||||
|
At this point you have an ELF binary that some emulators can execute directly.
|
||||||
|
This is helpful because it'll have debug symbols and all that, assuming a debug
|
||||||
|
build. Specifically, [mgba 0.7 beta
|
||||||
|
1](https://mgba.io/2018/09/24/mgba-0.7-beta1/) can do it, and perhaps other
|
||||||
|
emulators can also do it.
|
||||||
|
|
||||||
|
However, if you want a "real" ROM that works in all emulators and that you could
|
||||||
|
transfer to a flash cart there's a little more to do.
|
||||||
|
|
||||||
|
* `arm-none-eabi-objcopy -O binary target/thumbv4-none-agb/MODE/BIN_NAME target/ROM_NAME.gba`
|
||||||
|
* This will perform an [objcopy](https://linux.die.net/man/1/objcopy) on our
|
||||||
|
program. Here I've named the program `arm-none-eabi-objcopy`, which is what
|
||||||
|
devkitpro calls their version of `objcopy` that's specific to the GBA in the
|
||||||
|
Windows install. If the program isn't found under that name, have a look in
|
||||||
|
your installation directory to see if it's under a slightly different name
|
||||||
|
or something.
|
||||||
|
* As you can see from reading the man page, the `-O binary` option takes our
|
||||||
|
lovely ELF file with symbols and all that and strips it down to basically a
|
||||||
|
bare memory dump of the program.
|
||||||
|
* The next argument is the input file. You might not be familiar with how
|
||||||
|
`cargo` arranges stuff in the `target/` directory, and between RLS and
|
||||||
|
`cargo doc` and stuff it gets kinda crowded, so it goes like this:
|
||||||
|
* Since our program was built for a non-local target, first we've got a
|
||||||
|
directory named for that target, `thumbv4-none-agb/`
|
||||||
|
* Next, the "MODE" is either `debug/` or `release/`, depending on if we had
|
||||||
|
the `--release` flag included. You'll probably only be packing release
|
||||||
|
mode programs all the way into GBA roms, but it works with either mode.
|
||||||
|
* Finally, the name of the program. If your program is something out of the
|
||||||
|
project's `src/bin/` then it'll be that file's name, or whatever name you
|
||||||
|
configured for the bin in the `Cargo.toml` file. If your program is
|
||||||
|
something out of the project's `examples/` directory there will be a
|
||||||
|
similar `examples/` sub-directory first, and then the example's name.
|
||||||
|
* The final argument is the output of the `objcopy`, which I suggest putting
|
||||||
|
at just the top level of the `target/` directory. Really it could go
|
||||||
|
anywhere, but if you're using git then it's likely that your `.gitignore`
|
||||||
|
file is already setup to exclude everything in `target/`, so this makes sure
|
||||||
|
that your intermediate game builds don't get checked into your git.
|
||||||
|
|
||||||
|
* `gbafix target/ROM_NAME.gba`
|
||||||
|
* The `gbafix` tool also comes from devkitpro. The GBA is very picky about a
|
||||||
|
ROM's format, and `gbafix` patches the ROM's header and such so that it'll
|
||||||
|
work right. Unlike `objcopy`, this tool is custom built for GBA development,
|
||||||
|
so it works just perfectly without any arguments beyond the file name. The
|
||||||
|
ROM is patched in place, so we don't even need to specify a new destination.
|
||||||
|
|
||||||
|
And you're finally done!
|
||||||
|
|
||||||
|
Of course, you probably want to make a script for all that, but it's up to you.
|
193
book/src/ch1/hello1.md
Normal file
193
book/src/ch1/hello1.md
Normal file
|
@ -0,0 +1,193 @@
|
||||||
|
# hello1
|
||||||
|
|
||||||
|
Ready? Here goes:
|
||||||
|
|
||||||
|
`hello1.rs`
|
||||||
|
|
||||||
|
```rust
|
||||||
|
#![feature(start)]
|
||||||
|
#![no_std]
|
||||||
|
|
||||||
|
#[cfg(not(test))]
|
||||||
|
#[panic_handler]
|
||||||
|
fn panic(_info: &core::panic::PanicInfo) -> ! {
|
||||||
|
loop {}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[start]
|
||||||
|
fn main(_argc: isize, _argv: *const *const u8) -> isize {
|
||||||
|
unsafe {
|
||||||
|
(0x04000000 as *mut u16).write_volatile(0x0403);
|
||||||
|
(0x06000000 as *mut u16).offset(120 + 80 * 240).write_volatile(0x001F);
|
||||||
|
(0x06000000 as *mut u16).offset(136 + 80 * 240).write_volatile(0x03E0);
|
||||||
|
(0x06000000 as *mut u16).offset(120 + 96 * 240).write_volatile(0x7C00);
|
||||||
|
loop {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Throw that into your project, build the program (as described back in Chapter
|
||||||
|
0), and give it a run. You should see a red, green, and blue dot close-ish to
|
||||||
|
the middle of the screen. If you don't, something already went wrong. Double
|
||||||
|
check things, phone a friend, write your senators, try asking Ketsuban on the
|
||||||
|
[Rust Community Discord](https://discordapp.com/invite/aVESxV8), until you're
|
||||||
|
able to get your three dots going.
|
||||||
|
|
||||||
|
## Explaining hello1
|
||||||
|
|
||||||
|
So, what just happened? Even if you're used to Rust that might look pretty
|
||||||
|
strange. We'll go over each part extra carefully.
|
||||||
|
|
||||||
|
```rust
|
||||||
|
#![feature(start)]
|
||||||
|
```
|
||||||
|
|
||||||
|
This enables the [start
|
||||||
|
feature](https://doc.rust-lang.org/beta/unstable-book/language-features/start.html),
|
||||||
|
which you would normally be able to read about in the unstable book, except that
|
||||||
|
the book tells you nothing at all except to look at the [tracking
|
||||||
|
issue](https://github.com/rust-lang/rust/issues/29633).
|
||||||
|
|
||||||
|
Basically, a GBA game is even more low-level than the _normal_ amount of
|
||||||
|
low-level that you get from Rust, so we have to tell the compiler to account for
|
||||||
|
that by specifying a `#[start]`, and we need this feature on to do that.
|
||||||
|
|
||||||
|
```rust
|
||||||
|
#![no_std]
|
||||||
|
```
|
||||||
|
|
||||||
|
There's no standard library available on the GBA, so we'll have to live a core
|
||||||
|
only life.
|
||||||
|
|
||||||
|
```rust
|
||||||
|
#[cfg(not(test))]
|
||||||
|
#[panic_handler]
|
||||||
|
fn panic(_info: &core::panic::PanicInfo) -> ! {
|
||||||
|
loop {}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
This sets our [panic
|
||||||
|
handler](https://doc.rust-lang.org/nightly/nomicon/panic-handler.html).
|
||||||
|
Basically, if we somehow trigger a panic, this is where the program goes.
|
||||||
|
However, right now we don't know how to get any sort of message out to the user
|
||||||
|
so... we do nothing at all. We _can't even return_ from here, so we just sit in
|
||||||
|
an infinite loop. The player will have to reset the universe from the outside.
|
||||||
|
|
||||||
|
The `#[cfg(not(test))]` part makes this item only exist in the program when
|
||||||
|
we're _not_ in a test build. This is so that `cargo test` and such work right as
|
||||||
|
much as possible.
|
||||||
|
|
||||||
|
```rust
|
||||||
|
#[start]
|
||||||
|
fn main(_argc: isize, _argv: *const *const u8) -> isize {
|
||||||
|
```
|
||||||
|
|
||||||
|
This is our `#[start]`. We call it `main`, but it's not like a `main` that you'd
|
||||||
|
see in a Rust program. It's _more like_ the sort of `main` that you'd see in a C
|
||||||
|
program, but it's still **not** that either. If you compile a `#[start]` program
|
||||||
|
for a target with an OS such as `arm-none-eabi-nm` you can open up the debug
|
||||||
|
info and see that your result will have the symbol for the C `main` along side
|
||||||
|
the symbol for the start `main` that we write here. Our start `main` is just its
|
||||||
|
own unique thing, and the inputs and outputs have to be like that because that's
|
||||||
|
how `#[start]` is specified to work in Rust.
|
||||||
|
|
||||||
|
If you think about it for a moment you'll probably realize that, those inputs
|
||||||
|
and outputs are totally useless to us on a GBA. There's no OS on the GBA to call
|
||||||
|
our program, and there's no place for our program to "return to" when it's done.
|
||||||
|
|
||||||
|
Side note: if you want to learn more about stuff "before main gets called" you
|
||||||
|
can watch a great [CppCon talk](https://www.youtube.com/watch?v=dOfucXtyEsU) by
|
||||||
|
Matt Godbolt (yes, that Godbolt) where he delves into quite a bit of it. The
|
||||||
|
talk doesn't really apply to the GBA, but it's pretty good.
|
||||||
|
|
||||||
|
```rust
|
||||||
|
unsafe {
|
||||||
|
```
|
||||||
|
|
||||||
|
I hope you're all set for some `unsafe`, because there's a lot of it to be had.
|
||||||
|
|
||||||
|
```rust
|
||||||
|
(0x04000000 as *mut u16).write_volatile(0x0403);
|
||||||
|
```
|
||||||
|
|
||||||
|
Sure!
|
||||||
|
|
||||||
|
```rust
|
||||||
|
(0x06000000 as *mut u16).offset(120 + 80 * 240).write_volatile(0x001F);
|
||||||
|
(0x06000000 as *mut u16).offset(136 + 80 * 240).write_volatile(0x03E0);
|
||||||
|
(0x06000000 as *mut u16).offset(120 + 96 * 240).write_volatile(0x7C00);
|
||||||
|
```
|
||||||
|
|
||||||
|
Ah, of course.
|
||||||
|
|
||||||
|
```rust
|
||||||
|
loop {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
And, as mentioned above, there's no place for a GBA program to "return to", so
|
||||||
|
we can't ever let `main` try to return there. Instead, we go into an infinite
|
||||||
|
`loop` that does nothing. The fact that this doesn't ever return an `isize`
|
||||||
|
value doesn't seem to bother Rust, because I guess we're at least not returning
|
||||||
|
any other type of thing instead.
|
||||||
|
|
||||||
|
Fun fact: unlike in C++, an infinite loop with no side effects isn't Undefined
|
||||||
|
Behavior for us rustaceans... _semantically_. In truth LLVM has a [known
|
||||||
|
bug](https://github.com/rust-lang/rust/issues/28728) in this area, so we won't
|
||||||
|
actually be relying on empty loops in any future programs.
|
||||||
|
|
||||||
|
## All Those Magic Numbers
|
||||||
|
|
||||||
|
Alright, I cheated quite a bit in the middle there. The program works, but I
|
||||||
|
didn't really tell you why because I didn't really tell you what any of those
|
||||||
|
magic numbers mean or do.
|
||||||
|
|
||||||
|
* `0x04000000` is the address of an IO Register called the Display Control.
|
||||||
|
* `0x06000000` is the start of Video RAM.
|
||||||
|
|
||||||
|
So we write some magic to the display control register once, then we write some
|
||||||
|
other magic to three locations of magic to the Video RAM. We get three dots,
|
||||||
|
each in their own location... so that second part makes sense at least.
|
||||||
|
|
||||||
|
We'll get into the magic number details in the other sections of this chapter.
|
||||||
|
|
||||||
|
## Sidebar: Volatile
|
||||||
|
|
||||||
|
We'll get into what all that is in a moment, but first let's ask ourselves: Why
|
||||||
|
are we doing _volatile_ writes? You've probably never used it before at all.
|
||||||
|
What is volatile anyway?
|
||||||
|
|
||||||
|
Well, the optimizer is pretty aggressive some of the time, and so it'll skip
|
||||||
|
reads and writes when it thinks can. Like if you write to a pointer once, and
|
||||||
|
then again a moment later, and it didn't see any other reads in between, it'll
|
||||||
|
think that it can just skip doing that first write since it'll get overwritten
|
||||||
|
anyway. Sometimes that's right, but sometimes it's wrong.
|
||||||
|
|
||||||
|
Marking a read or write as _volatile_ tells the compiler that it really must do
|
||||||
|
that action, and in the exact order that we wrote it out. It says that there
|
||||||
|
might even be special hardware side effects going on that the compiler isn't
|
||||||
|
aware of. In this case, the write to the display control register sets a video
|
||||||
|
mode, and the writes to the Video RAM set pixels that will show up on the
|
||||||
|
screen.
|
||||||
|
|
||||||
|
Similar to "atomic" operations you might have heard about, all volatile
|
||||||
|
operations are enforced to happen in the exact order that you specify them, but
|
||||||
|
only relative to other volatile operations. So something like
|
||||||
|
|
||||||
|
```rust
|
||||||
|
c.volatile_write(5);
|
||||||
|
a += b;
|
||||||
|
d.volatile_write(7);
|
||||||
|
```
|
||||||
|
|
||||||
|
might end up changing `a` either before or after the change to `c` (since the
|
||||||
|
value of `a` doesn't affect the write to `c`), but the write to `d` will
|
||||||
|
_always_ happen after the write to `c` even though the compiler doesn't see any
|
||||||
|
direct data dependency there.
|
||||||
|
|
||||||
|
If you ever use volatile stuff on other platforms it's important to note that
|
||||||
|
volatile doesn't make things thread-safe, you still need atomic for that.
|
||||||
|
However, the GBA doesn't have threads, so we don't have to worry about thread
|
||||||
|
safety concerns.
|
114
book/src/ch1/hello2.md
Normal file
114
book/src/ch1/hello2.md
Normal file
|
@ -0,0 +1,114 @@
|
||||||
|
# hello2
|
||||||
|
|
||||||
|
Okay so let's have a look again:
|
||||||
|
|
||||||
|
`hello1`
|
||||||
|
|
||||||
|
```rust
|
||||||
|
#![feature(start)]
|
||||||
|
#![no_std]
|
||||||
|
|
||||||
|
#[cfg(not(test))]
|
||||||
|
#[panic_handler]
|
||||||
|
fn panic(_info: &core::panic::PanicInfo) -> ! {
|
||||||
|
loop {}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[start]
|
||||||
|
fn main(_argc: isize, _argv: *const *const u8) -> isize {
|
||||||
|
unsafe {
|
||||||
|
(0x04000000 as *mut u16).write_volatile(0x0403);
|
||||||
|
(0x06000000 as *mut u16).offset(120 + 80 * 240).write_volatile(0x001F);
|
||||||
|
(0x06000000 as *mut u16).offset(136 + 80 * 240).write_volatile(0x03E0);
|
||||||
|
(0x06000000 as *mut u16).offset(120 + 96 * 240).write_volatile(0x7C00);
|
||||||
|
loop {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Now let's clean this up so that it's clearer what's going on.
|
||||||
|
|
||||||
|
First we'll label that display control stuff:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
pub const DISPCNT: *mut u16 = 0x04000000 as *mut u16;
|
||||||
|
pub const MODE3: u16 = 3;
|
||||||
|
pub const BG2: u16 = 0b100_0000_0000;
|
||||||
|
```
|
||||||
|
|
||||||
|
Next we make some const values for the actual pixel drawing
|
||||||
|
|
||||||
|
```rust
|
||||||
|
pub const VRAM: usize = 0x06000000;
|
||||||
|
pub const SCREEN_WIDTH: isize = 240;
|
||||||
|
```
|
||||||
|
|
||||||
|
And then we want a small helper function for putting together a color value.
|
||||||
|
|
||||||
|
Happily, this one can even be declared as a const function. At the time of
|
||||||
|
writing, we've got the "minimal const fn" support in nightly. It really is quite
|
||||||
|
limited, but I'm happy to let rustc and LLVM pre-compute as much as they can
|
||||||
|
when it comes to the GBA's tiny CPU.
|
||||||
|
|
||||||
|
```rust
|
||||||
|
pub const fn rgb16(red: u16, green: u16, blue: u16) -> u16 {
|
||||||
|
blue << 10 | green << 5 | red
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Finally, we'll make a function for drawing a pixel in Mode 3. Even though it's
|
||||||
|
just a one-liner, having the "important parts" be labeled as function arguments
|
||||||
|
usually helps you think about it a lot better.
|
||||||
|
|
||||||
|
```rust
|
||||||
|
pub unsafe fn mode3_pixel(col: isize, row: isize, color: u16) {
|
||||||
|
(VRAM as *mut u16).offset(col + row * SCREEN_WIDTH).write_volatile(color);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
So now we've got this:
|
||||||
|
|
||||||
|
`hello2`
|
||||||
|
|
||||||
|
```rust
|
||||||
|
#![feature(start)]
|
||||||
|
#![no_std]
|
||||||
|
|
||||||
|
#[cfg(not(test))]
|
||||||
|
#[panic_handler]
|
||||||
|
fn panic(_info: &core::panic::PanicInfo) -> ! {
|
||||||
|
loop {}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[start]
|
||||||
|
fn main(_argc: isize, _argv: *const *const u8) -> isize {
|
||||||
|
unsafe {
|
||||||
|
DISPCNT.write_volatile(MODE3 | BG2);
|
||||||
|
mode3_pixel(120, 80, rgb16(31, 0, 0));
|
||||||
|
mode3_pixel(136, 80, rgb16(0, 31, 0));
|
||||||
|
mode3_pixel(120, 96, rgb16(0, 0, 31));
|
||||||
|
loop {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const DISPCNT: *mut u16 = 0x04000000 as *mut u16;
|
||||||
|
pub const MODE3: u16 = 3;
|
||||||
|
pub const BG2: u16 = 0b100_0000_0000;
|
||||||
|
|
||||||
|
pub const VRAM: usize = 0x06000000;
|
||||||
|
pub const SCREEN_WIDTH: isize = 240;
|
||||||
|
|
||||||
|
pub const fn rgb16(red: u16, green: u16, blue: u16) -> u16 {
|
||||||
|
blue << 10 | green << 5 | red
|
||||||
|
}
|
||||||
|
|
||||||
|
pub unsafe fn mode3_pixel(col: isize, row: isize, color: u16) {
|
||||||
|
(VRAM as *mut u16).offset(col + row * SCREEN_WIDTH).write_volatile(color);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Exact same program that we started with, but much easier to read.
|
||||||
|
|
||||||
|
Of course, in the full `gba` crate that this book is a part of we have these and
|
||||||
|
other elements all labeled and sorted out for you. Still, for educational
|
||||||
|
purposes it's often best to do it yourself at least once.
|
10
book/src/ch1/index.md
Normal file
10
book/src/ch1/index.md
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
# Ch 1: Hello GBA
|
||||||
|
|
||||||
|
Traditionally a person writes a "hello, world" program so that they can test
|
||||||
|
that their development environment is setup properly and to just get a feel for
|
||||||
|
using the tools involved. To get an idea of what a small part of a source file
|
||||||
|
will look like. All that stuff.
|
||||||
|
|
||||||
|
Normally, you write a program that prints "hello, world" to the terminal. The
|
||||||
|
GBA has no terminal, but it does have a screen, so instead we're going to draw
|
||||||
|
three dots to the screen.
|
33
book/src/ch1/io_registers.md
Normal file
33
book/src/ch1/io_registers.md
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
# IO Registers
|
||||||
|
|
||||||
|
The GBA has a large number of **IO Registers** (not to be confused with CPU
|
||||||
|
registers). These are special memory locations from `0x04000000` to
|
||||||
|
`0x040003FE`. GBATEK has a [full
|
||||||
|
list](http://problemkaputt.de/gbatek.htm#gbaiomap), but we only need to learn
|
||||||
|
about a few of them at a time as we go, so don't be worried.
|
||||||
|
|
||||||
|
The important facts to know about IO Registers are these:
|
||||||
|
|
||||||
|
* Each has their own specific size. Most are `u16`, but some are `u32`.
|
||||||
|
* All of them must be accessed in a `volatile` style.
|
||||||
|
* Each register is specifically readable or writable or both. Actually, with
|
||||||
|
some registers there are even individual bits that are read-only or
|
||||||
|
write-only.
|
||||||
|
* If you write to a read-only position, those writes are simply ignored. This
|
||||||
|
mostly matters if a writable register contains a read-only bit (such as the
|
||||||
|
Display Control, next section).
|
||||||
|
* If you read from a write-only position, you get back values that are
|
||||||
|
[basically
|
||||||
|
nonsense](http://problemkaputt.de/gbatek.htm#gbaunpredictablethings). There
|
||||||
|
aren't really any registers that mix writable bits with read only bits, so
|
||||||
|
you're basically safe here. The only (mild) concern is that when you write a
|
||||||
|
value into a write-only register you need to keep track of what you wrote
|
||||||
|
somewhere else if you want to know what you wrote (such to adjust an offset
|
||||||
|
value by +1, or whatever).
|
||||||
|
* You can always check GBATEK to be sure, but if I don't mention it then a bit
|
||||||
|
is probably both read and write.
|
||||||
|
* Some registers have invalid bit patterns. For example, the lowest three bits
|
||||||
|
of the Display Control register can't legally be set to the values 6 or 7.
|
||||||
|
|
||||||
|
When talking about bit positions, the numbers are _zero indexed_ just like an
|
||||||
|
array index is.
|
112
book/src/ch1/the_display_control_register.md
Normal file
112
book/src/ch1/the_display_control_register.md
Normal file
|
@ -0,0 +1,112 @@
|
||||||
|
# The Display Control Register
|
||||||
|
|
||||||
|
The display control register is our first actual IO Register. GBATEK gives it the
|
||||||
|
shorthand [DISPCNT](http://problemkaputt.de/gbatek.htm#lcdiodisplaycontrol), so
|
||||||
|
you might see it under that name if you read other guides.
|
||||||
|
|
||||||
|
Among IO Registers, it's one of the simpler ones, but it's got enough complexity
|
||||||
|
that we can get a hint of what's to come.
|
||||||
|
|
||||||
|
Also it's the one that you basically always need to set at least once in every
|
||||||
|
GBA game, so it's a good starting one to go over for that reason too.
|
||||||
|
|
||||||
|
The display control register holds a `u16` value, and is located at `0x0400_0000`.
|
||||||
|
|
||||||
|
Many of the bits here won't mean much to you right now. **That is fine.** You do
|
||||||
|
NOT need to memorize them all or what they all do right away. We'll just skim
|
||||||
|
over all the parts of this register to start, and then we'll go into more detail
|
||||||
|
in later chapters when we need to come back and use more of the bits.
|
||||||
|
|
||||||
|
## Video Modes
|
||||||
|
|
||||||
|
The lowest three bits (0-2) let you select from among the GBA's six video modes.
|
||||||
|
You'll notice that 3 bits allows for eight modes, but the values 6 and 7 are
|
||||||
|
prohibited.
|
||||||
|
|
||||||
|
Modes 0, 1, and 2 are "tiled" modes. These are actually the modes that you
|
||||||
|
should eventually learn to use as much as possible. It lets the GBA's limited
|
||||||
|
video hardware do as much of the work as possible, leaving more of your CPU time
|
||||||
|
for gameplay computations. However, they're also complex enough to deserve their
|
||||||
|
own demos and chapters later on, so that's all we'll say about them for now.
|
||||||
|
|
||||||
|
Modes 3, 4, and 5 are "bitmap" modes. These let you write individual pixels to
|
||||||
|
locations on the screen.
|
||||||
|
|
||||||
|
* **Mode 3** is full resolution (240w x 160h) RGB15 color. You might not be used
|
||||||
|
to RGB15, since modern computers have 24 or 32 bit colors. In RGB15, there's 5
|
||||||
|
bits for each color channel stored within a `u16` value, and the highest bit is
|
||||||
|
simply ignored.
|
||||||
|
* **Mode 4** is full resolution paletted color. Instead of being a `u16` color, each
|
||||||
|
pixel value is a `u8` palette index entry, and then the display uses the
|
||||||
|
palette memory (which we'll talk about later) to store the actual color data.
|
||||||
|
Since each pixel is half sized, we can fit twice as many. This lets us have
|
||||||
|
two "pages". At any given moment only one page is active, and you can draw to
|
||||||
|
the other page without the user noticing. You set which page to show with
|
||||||
|
another bit we'll get to in a moment.
|
||||||
|
* **Mode 5** is full color, but also with pages. This means that we must have a
|
||||||
|
reduced resolution to compensate (video memory is only so big!). The screen is
|
||||||
|
effectively only 160w x 128h in this mode.
|
||||||
|
|
||||||
|
## CGB Mode
|
||||||
|
|
||||||
|
Bit 3 is effectively read only. Technically it can be flipped using a BIOS call,
|
||||||
|
but when you write to the display control register normally it won't write to
|
||||||
|
this bit, so we'll call it effectively read only.
|
||||||
|
|
||||||
|
This bit is on if the CPU is in CGB mode.
|
||||||
|
|
||||||
|
## Page Flipping
|
||||||
|
|
||||||
|
Bit 4 lets you pick which page to use. This is only relevent in video modes 4 or
|
||||||
|
5, and is just ignored otherwise. It's very easy to remember: when the bit is 0
|
||||||
|
the 0th page is used, and when the bit is 1 the 1st page is used.
|
||||||
|
|
||||||
|
The second page always starts at `0x0600_A000`.
|
||||||
|
|
||||||
|
## OAM, VRAM, and Blanking
|
||||||
|
|
||||||
|
Bit 5 lets you access OAM during HBlank if enabled. This is cool, but it reduces
|
||||||
|
the maximum sprites per scanline, so it's not default.
|
||||||
|
|
||||||
|
Bit 6 lets you adjust if the GBA should treat Object Character VRAM as being 2d
|
||||||
|
(off) or 1d (on). This particular control can be kinda tricky to wrap your head
|
||||||
|
around, so we'll be sure to have some extra diagrams in the chapter that deals
|
||||||
|
with it.
|
||||||
|
|
||||||
|
Bit 7 forces the screen to stay in VBlank as long as it's set. This allows the
|
||||||
|
fastest use of the VRAM, Palette, and Object Attribute Memory. Obviously if you
|
||||||
|
leave this on for too long the player will notice a blank screen, but it might
|
||||||
|
be okay to use for a moment or two every once in a while.
|
||||||
|
|
||||||
|
## Screen Layers
|
||||||
|
|
||||||
|
Bits 8 through 11 control if Background layers 0 through 3 should be active.
|
||||||
|
|
||||||
|
Bit 12 affects the Object layer.
|
||||||
|
|
||||||
|
Note that not all background layers are available in all video modes:
|
||||||
|
|
||||||
|
* Mode 0: all
|
||||||
|
* Mode 1: 0/1/2
|
||||||
|
* Mode 2: 2/3
|
||||||
|
* Mode 3/4/5: 2
|
||||||
|
|
||||||
|
Bit 13 and 14 enable the display of Windows 0 and 1, and Bit 15 enables the
|
||||||
|
object display window. We'll get into how windows work later on, they let you do
|
||||||
|
some nifty graphical effects.
|
||||||
|
|
||||||
|
## In Conclusion...
|
||||||
|
|
||||||
|
So what did we do to the display control register in `hello1`?
|
||||||
|
|
||||||
|
```rust
|
||||||
|
(0x04000000 as *mut u16).write_volatile(0x0403);
|
||||||
|
```
|
||||||
|
|
||||||
|
First let's [convert that to
|
||||||
|
binary](https://www.wolframalpha.com/input/?i=0x0403), and we get
|
||||||
|
`0b100_0000_0011`. So, that's setting Mode 3 with background 2 enabled and
|
||||||
|
nothing else special.
|
||||||
|
|
||||||
|
However, I think we can do better than that. This is a prime target for more
|
||||||
|
newtyping as we attempt a `hello2` program.
|
113
book/src/ch1/video_memory_intro.md
Normal file
113
book/src/ch1/video_memory_intro.md
Normal file
|
@ -0,0 +1,113 @@
|
||||||
|
# Video Memory Intro
|
||||||
|
|
||||||
|
The GBA's Video RAM is 96k stretching from `0x0600_0000` to `0x0601_7FFF`.
|
||||||
|
|
||||||
|
The Video RAM can only be accessed totally freely during a Vertical Blank (aka
|
||||||
|
"VBlank", though sometimes I forget and don't capitalize it properly). At other
|
||||||
|
times, if the CPU tries to touch the same part of video memory as the display
|
||||||
|
controller is accessing then the CPU gets bumped by a cycle to avoid a clash.
|
||||||
|
|
||||||
|
Annoyingly, VRAM can only be properly written to in 16 and 32 bit segments (same
|
||||||
|
with PALRAM and OAM). If you try to write just an 8 bit segment, then both parts
|
||||||
|
of the 16 bit segment get the same value written to them. In other words, if you
|
||||||
|
write the byte `5` to `0x0600_0000`, then both `0x0600_0000` and ALSO
|
||||||
|
`0x0600_0001` will have the byte `5` in them. We have to be extra careful when
|
||||||
|
trying to set an individual byte, and we also have to be careful if we use
|
||||||
|
`memcopy` or `memset` as well, because they're byte oriented by default and
|
||||||
|
don't know to follow the special rules.
|
||||||
|
|
||||||
|
## RGB15
|
||||||
|
|
||||||
|
As I said before, RGB15 stores a color within a `u16` value using 5 bits for
|
||||||
|
each color channel.
|
||||||
|
|
||||||
|
```rust
|
||||||
|
pub const RED: u16 = 0b0_00000_00000_11111;
|
||||||
|
pub const GREEN: u16 = 0b0_00000_11111_00000;
|
||||||
|
pub const BLUE: u16 = 0b0_11111_00000_00000;
|
||||||
|
```
|
||||||
|
|
||||||
|
In Mode 3 and Mode 5 we write direct color values into VRAM, and in Mode 4 we
|
||||||
|
write palette index values, and then the color values go into the PALRAM.
|
||||||
|
|
||||||
|
## Mode 3
|
||||||
|
|
||||||
|
Mode 3 is pretty easy. We have a full resolution grid of rgb15 pixels. There's
|
||||||
|
160 rows of 240 pixels each, with the base address being the top left corner. A
|
||||||
|
particular pixel uses normal "2d indexing" math:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
let row_five_col_seven = 5 + (7 * SCREEN_WIDTH);
|
||||||
|
```
|
||||||
|
|
||||||
|
To draw a pixel, we just write a value at the address for the row and col that
|
||||||
|
we want to draw to.
|
||||||
|
|
||||||
|
## Mode 4
|
||||||
|
|
||||||
|
Mode 4 introduces page flipping. Instead of one giant page at `0x0600_0000`,
|
||||||
|
there's Page 0 at `0x0600_0000` and then Page 1 at `0x0600_A000`. The resolution
|
||||||
|
for each page is the same as above, but instead of writing `u16` values, the
|
||||||
|
memory is treated as `u8` indexes into PALRAM. The PALRAM starts at
|
||||||
|
`0x0500_0000`, and there's enough space for 256 palette entries (each a `u16`).
|
||||||
|
|
||||||
|
To set the color of a palette entry we just do a normal `u16` write_volatile.
|
||||||
|
|
||||||
|
```rust
|
||||||
|
(0x0500_0000 as *mut u16).offset(target_index).write_volatile(new_color)
|
||||||
|
```
|
||||||
|
|
||||||
|
To draw a pixel we set the palette entry that we want the pixel to use. However,
|
||||||
|
we must remember the "minimum size" write limitation that applies to VRAM. So,
|
||||||
|
if we want to change just a single pixel at a time we must
|
||||||
|
|
||||||
|
1) Read the full `u16` it's a part of.
|
||||||
|
2) Clear the half of the `u16` we're going to replace
|
||||||
|
3) Write the half of the `u16` we're going to replace with the new value
|
||||||
|
4) Write that result back to the address.
|
||||||
|
|
||||||
|
So, the math for finding a byte offset is the same as Mode 3 (since they're both
|
||||||
|
a 2d grid). If the byte offset is EVEN it'll be the high bits of the `u16` at
|
||||||
|
half the byte offset rounded down. If the offset is ODD it'll be the low bits of
|
||||||
|
the `u16` at half the byte.
|
||||||
|
|
||||||
|
Does that make sense?
|
||||||
|
|
||||||
|
* If we want to write pixel (0,0) the byte offset is 0, so we change the high
|
||||||
|
bits of `u16` offset 0. Then we want to write to (1,0), so the byte offset is
|
||||||
|
1, so we change the low bits of `u16` offset 0. The pixels are next to each
|
||||||
|
other, and the target bytes are next to each other, good so far.
|
||||||
|
* If we want to write to (5,6) that'd be byte `5 + 6 * 240 = 1445`, so we'd
|
||||||
|
target the low bits of `u16` offset `floor(1445/2) = 722`.
|
||||||
|
|
||||||
|
As you can see, trying to write individual pixels in Mode 4 is mostly a bad
|
||||||
|
time. Fret not! We don't _have_ to write individual bytes. If our data is
|
||||||
|
arranged correctly ahead of time we can just write `u16` or `u32` values
|
||||||
|
directly. The video hardware doesn't care, it'll get along just fine.
|
||||||
|
|
||||||
|
## Mode 5
|
||||||
|
|
||||||
|
Mode 5 is also a two page mode, but instead of compressing the size of a pixel's
|
||||||
|
data to fit in two pages, we compress the resolution.
|
||||||
|
|
||||||
|
Mode 5 is full `u16` color, but only 160w x 128h per page.
|
||||||
|
|
||||||
|
## In Conclusion...
|
||||||
|
|
||||||
|
So what got written into VRAM in `hello1`?
|
||||||
|
|
||||||
|
```rust
|
||||||
|
(0x06000000 as *mut u16).offset(120 + 80 * 240).write_volatile(0x001F);
|
||||||
|
(0x06000000 as *mut u16).offset(136 + 80 * 240).write_volatile(0x03E0);
|
||||||
|
(0x06000000 as *mut u16).offset(120 + 96 * 240).write_volatile(0x7C00);
|
||||||
|
```
|
||||||
|
|
||||||
|
So at pixels `(120,80)`, `(136,80)`, and `(120,96)` we write three values. Once
|
||||||
|
again we probably need to [convert them](https://www.wolframalpha.com/) into
|
||||||
|
binary to make sense of it.
|
||||||
|
|
||||||
|
* 0x001F: 0b11111
|
||||||
|
* 0x03E0: 0b11111_00000
|
||||||
|
* 0x7C00: 0b11111_00000_00000
|
||||||
|
|
||||||
|
Ah, of course, a red pixel, a green pixel, and a blue pixel.
|
22
book/src/ch2/index.md
Normal file
22
book/src/ch2/index.md
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
# Ch 2: User Input
|
||||||
|
|
||||||
|
It's all well and good to draw three pixels, but they don't do anything yet. We
|
||||||
|
want them to do something, and for that we need to get some input from the user.
|
||||||
|
|
||||||
|
The GBA, as I'm sure you know, has an arrow pad, A and B, L and R, Start and
|
||||||
|
Select. That's a little more than the NES/GB/CGB had, and a little less than the
|
||||||
|
SNES had. As you can guess, we get key state info from an IO register.
|
||||||
|
|
||||||
|
Also, we will need a way to keep the program from running "too fast". On a
|
||||||
|
modern computer or console you do this with vsync info from the GPU and Monitor,
|
||||||
|
and on the GBA we'll be using vsync info from an IO register that tracks what
|
||||||
|
the display hardware is doing.
|
||||||
|
|
||||||
|
As a way to apply our knowledge We'll make a simple "light cycle" game where
|
||||||
|
your dot leaves a trail behind them and you die if you go off the screen or if
|
||||||
|
you touch your own trail. We just make a copy of `hello2.rs` named
|
||||||
|
`light_cycle.rs` and then fill it in as we go through the chapter. Normally you
|
||||||
|
might not place the entire program into a single source file, particularly as it
|
||||||
|
grows over time, but since these are small examples it's much better to have
|
||||||
|
them be completely self contained than it is to have them be "properly
|
||||||
|
organized" for the long term.
|
119
book/src/ch2/light_cycle.md
Normal file
119
book/src/ch2/light_cycle.md
Normal file
|
@ -0,0 +1,119 @@
|
||||||
|
# light_cycle
|
||||||
|
|
||||||
|
Now let's make a game of "light_cycle" with our new knowledge.
|
||||||
|
|
||||||
|
## Gameplay
|
||||||
|
|
||||||
|
`light_cycle` is pretty simple, and very obvious if you've ever seen Tron. The
|
||||||
|
player moves around the screen with a trail left behind them. They die if they
|
||||||
|
go off the screen or if they touch their own trail.
|
||||||
|
|
||||||
|
## Operations
|
||||||
|
|
||||||
|
We need some better drawing operations this time around.
|
||||||
|
|
||||||
|
```rust
|
||||||
|
pub unsafe fn mode3_clear_screen(color: u16) {
|
||||||
|
let color = color as u32;
|
||||||
|
let bulk_color = color << 16 | color;
|
||||||
|
let mut ptr = VRAM as *mut u32;
|
||||||
|
for _ in 0..SCREEN_HEIGHT {
|
||||||
|
for _ in 0..(SCREEN_WIDTH / 2) {
|
||||||
|
ptr.write_volatile(bulk_color);
|
||||||
|
ptr = ptr.offset(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub unsafe fn mode3_draw_pixel(col: isize, row: isize, color: u16) {
|
||||||
|
(VRAM as *mut u16).offset(col + row * SCREEN_WIDTH).write_volatile(color);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub unsafe fn mode3_read_pixel(col: isize, row: isize) -> u16 {
|
||||||
|
(VRAM as *mut u16).offset(col + row * SCREEN_WIDTH).read_volatile()
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The draw pixel and read pixel are both pretty obvious. What's new is the clear
|
||||||
|
screen operation. It changes the `u16` color into a `u32` and then packs the
|
||||||
|
value in twice. Then we write out `u32` values the whole way through screen
|
||||||
|
memory. This means we have to do less write operations overall, and so the
|
||||||
|
screen clear is twice as fast.
|
||||||
|
|
||||||
|
Now we just have to fill in the main function:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
#[start]
|
||||||
|
fn main(_argc: isize, _argv: *const *const u8) -> isize {
|
||||||
|
unsafe {
|
||||||
|
DISPCNT.write_volatile(MODE3 | BG2);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut px = SCREEN_WIDTH / 2;
|
||||||
|
let mut py = SCREEN_HEIGHT / 2;
|
||||||
|
let mut color = rgb16(31, 0, 0);
|
||||||
|
|
||||||
|
loop {
|
||||||
|
// read the input for this frame
|
||||||
|
let this_frame_keys = read_key_input();
|
||||||
|
|
||||||
|
// adjust game state and wait for vblank
|
||||||
|
px += 2 * this_frame_keys.column_direction() as isize;
|
||||||
|
py += 2 * this_frame_keys.row_direction() as isize;
|
||||||
|
wait_until_vblank();
|
||||||
|
|
||||||
|
// draw the new game and wait until the next frame starts.
|
||||||
|
unsafe {
|
||||||
|
if px < 0 || py < 0 || px == SCREEN_WIDTH || py == SCREEN_HEIGHT {
|
||||||
|
// out of bounds, reset the screen and position.
|
||||||
|
mode3_clear_screen(0);
|
||||||
|
color = color.rotate_left(5);
|
||||||
|
px = SCREEN_WIDTH / 2;
|
||||||
|
py = SCREEN_HEIGHT / 2;
|
||||||
|
} else {
|
||||||
|
let color_here = mode3_read_pixel(px, py);
|
||||||
|
if color_here != 0 {
|
||||||
|
// crashed into our own line, reset the screen
|
||||||
|
mode3_clear_screen(0);
|
||||||
|
color = color.rotate_left(5);
|
||||||
|
} else {
|
||||||
|
// draw the new part of the line
|
||||||
|
mode3_draw_pixel(px, py, color);
|
||||||
|
mode3_draw_pixel(px, py + 1, color);
|
||||||
|
mode3_draw_pixel(px + 1, py, color);
|
||||||
|
mode3_draw_pixel(px + 1, py + 1, color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
wait_until_vdraw();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Oh that's a lot more than before!
|
||||||
|
|
||||||
|
First we set Mode 3 and Background 2, we know about that.
|
||||||
|
|
||||||
|
Then we're going to store the player's x and y, along with a color value for
|
||||||
|
their light cycle. Then we enter the core loop.
|
||||||
|
|
||||||
|
We read the keys for input, and then do as much as we can without touching video
|
||||||
|
memory. Since we're using video memory as the place to store the player's light
|
||||||
|
trail, we can't do much, we just update their position and wait for VBlank to
|
||||||
|
start. The player will be a 2x2 square, so the arrows will move you 2 pixels per
|
||||||
|
frame.
|
||||||
|
|
||||||
|
Once we're in VBlank we check to see what kind of drawing we're doing. If the
|
||||||
|
player has gone out of bounds, we clear the screen, rotate their color, and then
|
||||||
|
reset their position. Why rotate the color? Just because it's fun to have
|
||||||
|
different colors.
|
||||||
|
|
||||||
|
Next, if the player is in bounds we read the video memory for their position. If
|
||||||
|
it's not black that means we've been here before and the player has crashed into
|
||||||
|
their own line. In this case, we reset the game without moving them to a new
|
||||||
|
location.
|
||||||
|
|
||||||
|
Finally, if the player is in bounds and they haven't crashed, we write their color into memory at this position.
|
||||||
|
|
||||||
|
Regardless of how it worked out, we hold here until vdraw starts before going to
|
||||||
|
the next loop.
|
211
book/src/ch2/the_key_input_register.md
Normal file
211
book/src/ch2/the_key_input_register.md
Normal file
|
@ -0,0 +1,211 @@
|
||||||
|
# The Key Input Register
|
||||||
|
|
||||||
|
The Key Input Register is our next IO register. Its shorthand name is
|
||||||
|
[KEYINPUT](http://problemkaputt.de/gbatek.htm#gbakeypadinput) and it's a `u16`
|
||||||
|
at `0x4000130`. The entire register is obviously read only, you can't tell the
|
||||||
|
GBA what buttons are pressed.
|
||||||
|
|
||||||
|
Each button is exactly one bit:
|
||||||
|
|
||||||
|
| Bit | Button |
|
||||||
|
|:---:|:------:|
|
||||||
|
| 0 | A |
|
||||||
|
| 1 | B |
|
||||||
|
| 2 | Select |
|
||||||
|
| 3 | Start |
|
||||||
|
| 4 | Right |
|
||||||
|
| 5 | Left |
|
||||||
|
| 6 | Up |
|
||||||
|
| 7 | Down |
|
||||||
|
| 8 | R |
|
||||||
|
| 9 | L |
|
||||||
|
|
||||||
|
The higher bits above are not used at all.
|
||||||
|
|
||||||
|
Similar to other old hardware devices, the convention here is that a button's
|
||||||
|
bit is **clear when pressed, active when released**. In other words, when the
|
||||||
|
user is not touching the device at all the KEYINPUT value will read
|
||||||
|
`0b0000_0011_1111_1111`. There's similar values for when the user is pressing as
|
||||||
|
many buttons as possible, but since the left/right and up/down keys are on an
|
||||||
|
arrow pad the value can never be 0 since you can't ever press every single key
|
||||||
|
at once.
|
||||||
|
|
||||||
|
When dealing with key input, the register always shows the exact key values at
|
||||||
|
any moment you read it. Obviously that's what it should do, but what it means to
|
||||||
|
you as a programmer is that you should usually gather input once at the top of a
|
||||||
|
game frame and then use that single input poll as the input values across the
|
||||||
|
whole game frame.
|
||||||
|
|
||||||
|
Of course, you might want to know if a user's key state changed from frame to
|
||||||
|
frame. That's fairly easy too: We just store the last frame keys as well as the
|
||||||
|
current frame keys (it's only a `u16`) and then we can xor the two values.
|
||||||
|
Anything that shows up in the xor result is a key that changed. If it's changed
|
||||||
|
and it's now down, that means it was pushed this frame. If it's changed and it's
|
||||||
|
now up, that means it was released this frame.
|
||||||
|
|
||||||
|
The other major thing you might frequently want is to know "which way" the arrow
|
||||||
|
pad is pointing: Up/Down/None and Left/Right/None. Sounds like an enum to me.
|
||||||
|
Except that often time we'll have situations where the direction just needs to
|
||||||
|
be multiplied by a speed and applied as a delta to a position. We want to
|
||||||
|
support that as well as we can too.
|
||||||
|
|
||||||
|
## Key Input Code
|
||||||
|
|
||||||
|
Let's get down to some code. First we want to make a way to read the address as
|
||||||
|
a `u16` and then wrap that in our newtype which will implement methods for
|
||||||
|
reading and writing the key bits.
|
||||||
|
|
||||||
|
```rust
|
||||||
|
pub const KEYINPUT: *mut u16 = 0x400_0130 as *mut u16;
|
||||||
|
|
||||||
|
/// A newtype over the key input state of the GBA.
|
||||||
|
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
|
||||||
|
#[repr(transparent)]
|
||||||
|
pub struct KeyInputSetting(u16);
|
||||||
|
|
||||||
|
pub fn read_key_input() -> KeyInputSetting {
|
||||||
|
unsafe { KeyInputSetting(KEYINPUT.read_volatile()) }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Now we want a way to check if a key is _being pressed_, since that's normally
|
||||||
|
how we think of things as a game designer and even as a player. That is, usually
|
||||||
|
you'd say "if you press A, then X happens" instead of "if you don't press A,
|
||||||
|
then X does not happen".
|
||||||
|
|
||||||
|
Normally we'd pick a constant for the bit we want, `&` it with our value, and
|
||||||
|
then check for `val != 0`. Since the bit we're looking for is `0` in the "true"
|
||||||
|
state we still pick the same constant and we still do the `&`, but we test with
|
||||||
|
`== 0`. Practically the same, right? Well, since I'm asking a rhetorical
|
||||||
|
question like that you can probably already guess that it's not the same. I was
|
||||||
|
shocked to learn this too.
|
||||||
|
|
||||||
|
All we have to do is ask our good friend
|
||||||
|
[Godbolt](https://rust.godbolt.org/z/d-8oCe) what's gonna happen when the code
|
||||||
|
compiles. The link there has the page set for the `stable` 1.30 compiler just so
|
||||||
|
that the link results stay consistent if you read this book in a year or
|
||||||
|
something. Also, we've set the target to `thumbv6m-none-eabi`, which is a
|
||||||
|
slightly later version of ARM than the actual GBA, but it's close enough for
|
||||||
|
just checking. Of course, in a full program small functions like these will
|
||||||
|
probably get inlined into the calling code and disappear entirely as they're
|
||||||
|
folded and refolded by the compiler, but we can just check.
|
||||||
|
|
||||||
|
It turns out that the `!=0` test is 4 instructions and the `==0` test is 6
|
||||||
|
instructions. Since we want to get savings where we can, and we'll probably
|
||||||
|
check the keys of an input often enough, we'll just always use a `!=0` test and
|
||||||
|
then adjust how we initially read the register to compensate.
|
||||||
|
|
||||||
|
```rust
|
||||||
|
pub fn read_key_input() -> KeyInputSetting {
|
||||||
|
unsafe { KeyInputSetting(KEYINPUT.read_volatile() ^ 0b1111_1111_1111_1111) }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Now we add a method for seeing if a key is pressed. In the full library there's
|
||||||
|
a more advanced version of this that's built up via macro, but for this example
|
||||||
|
we'll just name a bunch of `const` values and then have a method that takes a
|
||||||
|
value and says if that bit is on.
|
||||||
|
|
||||||
|
```rust
|
||||||
|
pub const KEY_A: u16 = 1 << 0;
|
||||||
|
pub const KEY_B: u16 = 1 << 1;
|
||||||
|
pub const KEY_SELECT: u16 = 1 << 2;
|
||||||
|
pub const KEY_START: u16 = 1 << 3;
|
||||||
|
pub const KEY_RIGHT: u16 = 1 << 4;
|
||||||
|
pub const KEY_LEFT: u16 = 1 << 5;
|
||||||
|
pub const KEY_UP: u16 = 1 << 6;
|
||||||
|
pub const KEY_DOWN: u16 = 1 << 7;
|
||||||
|
pub const KEY_R: u16 = 1 << 8;
|
||||||
|
pub const KEY_L: u16 = 1 << 9;
|
||||||
|
|
||||||
|
impl KeyInputSetting {
|
||||||
|
pub fn contains(&self, key: u16) -> bool {
|
||||||
|
(self.0 & key) != 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Because each key is a unique bit you can even check for more than one key at
|
||||||
|
once by just adding two key values together.
|
||||||
|
|
||||||
|
```rust
|
||||||
|
let input_contains_a_and_l = input.contains(KEY_A + KEY_L);
|
||||||
|
```
|
||||||
|
|
||||||
|
And we wanted to save the state of an old frame and compare it to the current
|
||||||
|
frame to see what was different:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
pub fn difference(&self, other: KeyInputSetting) -> KeyInputSetting {
|
||||||
|
KeyInputSetting(self.0 ^ other.0)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Anything that's "in" the difference output is a key that _changed_, and then if
|
||||||
|
the key reads as pressed this frame that means it was just pressed. The exact
|
||||||
|
mechanics of all the ways you might care to do something based on new key
|
||||||
|
presses is obviously quite varied, but it might be something like this:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
let this_frame_diff = this_frame_input.difference(last_frame_input);
|
||||||
|
|
||||||
|
if this_frame_diff.contains(KEY_B) && this_frame_input.contains(KEY_B) {
|
||||||
|
// the user just pressed B, react in some way
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
And for the arrow pad, we'll make an enum that easily casts into `i32`. Whenever
|
||||||
|
we're working with stuff we can try to use `i32` / `isize` as often as possible
|
||||||
|
just because it's easier on the GBA's CPU if we stick to its native number size.
|
||||||
|
Having it be an enum lets us use `match` and be sure that we've covered all our
|
||||||
|
cases.
|
||||||
|
|
||||||
|
```rust
|
||||||
|
/// A "tribool" value helps us interpret the arrow pad.
|
||||||
|
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
|
||||||
|
#[repr(i32)]
|
||||||
|
pub enum TriBool {
|
||||||
|
Minus = -1,
|
||||||
|
Neutral = 0,
|
||||||
|
Plus = +1,
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Now, how do we determine _which way_ is plus or minus? Well... I don't know.
|
||||||
|
Really. I'm not sure what the best one is because the GBA really wants the
|
||||||
|
origin at 0,0 with higher rows going down and higher cols going right. On the
|
||||||
|
other hand, all the normal math you and I learned in school is oriented with
|
||||||
|
increasing Y being upward on the page. So, at least for this demo, we're going
|
||||||
|
to go with what the GBA wants us to do and give it a try. If we don't end up
|
||||||
|
confusing ourselves then we can stick with that. Maybe we can cover it over
|
||||||
|
somehow later on.
|
||||||
|
|
||||||
|
```rust
|
||||||
|
pub fn column_direction(&self) -> TriBool {
|
||||||
|
if self.contains(KEY_RIGHT) {
|
||||||
|
TriBool::Plus
|
||||||
|
} else if self.contains(KEY_LEFT) {
|
||||||
|
TriBool::Minus
|
||||||
|
} else {
|
||||||
|
TriBool::Neutral
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn row_direction(&self) -> TriBool {
|
||||||
|
if self.contains(KEY_DOWN) {
|
||||||
|
TriBool::Plus
|
||||||
|
} else if self.contains(KEY_UP) {
|
||||||
|
TriBool::Minus
|
||||||
|
} else {
|
||||||
|
TriBool::Neutral
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
So then in our game, every frame we can check for `column_direction` and
|
||||||
|
`row_direction` and then apply those to the player's current position to make
|
||||||
|
them move around the screen.
|
||||||
|
|
||||||
|
With that settled I think we're all done with user input for now. There's some
|
||||||
|
other things to eventually know about like key interrupts that you can set and
|
||||||
|
stuff, but we'll cover that later on because it's not necessary right now.
|
71
book/src/ch2/the_vcount_register.md
Normal file
71
book/src/ch2/the_vcount_register.md
Normal file
|
@ -0,0 +1,71 @@
|
||||||
|
# The VCount Register
|
||||||
|
|
||||||
|
There's an IO register called
|
||||||
|
[VCOUNT](http://problemkaputt.de/gbatek.htm#lcdiointerruptsandstatus) that shows
|
||||||
|
you, what else, the Vertical (row) COUNT(er). It's a `u16` at address
|
||||||
|
`0x0400_0006`, and it's how we'll be doing our very poor quality vertical sync
|
||||||
|
code to start.
|
||||||
|
|
||||||
|
* **What makes it poor?** Well, we're just going to read from the vcount value as
|
||||||
|
often as possible every time we need to wait for a specific value to come up,
|
||||||
|
and then proceed once it hits the point we're looking for.
|
||||||
|
* **Why is this bad?** Because we're making the CPU do a lot of useless work,
|
||||||
|
which uses a lot more power that necessary. Even if you're not on an actual
|
||||||
|
GBA you might be running inside an emulator on a phone or other handheld. You
|
||||||
|
wanna try to save battery if all you're doing with that power use is waiting
|
||||||
|
instead of making a game actually do something.
|
||||||
|
* **Can we do better?** We can, but not yet. The better way to do things is to
|
||||||
|
use a BIOS call to put the CPU into low power mode until a VBlank interrupt
|
||||||
|
happens. However, we don't know about interrupts yet, and we don't know about
|
||||||
|
BIOS calls yet, so we'll do the basic thing for now and then upgrade later.
|
||||||
|
|
||||||
|
So the way that display hardware actually displays each frame is that it moves a
|
||||||
|
tiny pointer left to right across each pixel row one pixel at a time. When it's
|
||||||
|
within the actual screen width (240px) it's drawing out those pixels. Then it
|
||||||
|
goes _past_ the edge of the screen for 68px during a period known as the
|
||||||
|
"horizontal blank" (HBlank). Then it starts on the next row and does that loop
|
||||||
|
over again. This happens for the whole screen height (160px) and then once again
|
||||||
|
it goes past the last row for another 68px into a "vertical blank" (VBlank)
|
||||||
|
period.
|
||||||
|
|
||||||
|
* One pixel is 4 CPU cycles
|
||||||
|
* HDraw is 240 pixels, HBlank is 68 pixels (1,232 cycles per full scanline)
|
||||||
|
* VDraw is 150 scanlines, VBlank is 68 scanlines (280,896 cycles per full refresh)
|
||||||
|
|
||||||
|
Now you may remember some stuff from the display control register section where
|
||||||
|
it was mentioned that some parts of memory are best accessed during VBlank, and
|
||||||
|
also during hblank with a setting applied. These blanking periods are what was
|
||||||
|
being talked about. At other times if you attempt to access video or object
|
||||||
|
memory you (the CPU) might try touching the same memory that the display device
|
||||||
|
is trying to use, in which case you get bumped back a cycle so that the display
|
||||||
|
can finish what it's doing. Also, if you really insist on doing video memory
|
||||||
|
changes while the screen is being drawn then you might get some visual glitches.
|
||||||
|
If you can, just prepare all your changes ahead of time and then assign then all
|
||||||
|
quickly during the blank period.
|
||||||
|
|
||||||
|
So first we want a way to check the vcount value at all:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
pub const VCOUNT: *mut u16 = 0x0400_0006 as *mut u16;
|
||||||
|
|
||||||
|
pub fn read_vcount() -> u16 {
|
||||||
|
unsafe { VCOUNT.read_volatile() }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Then we want two little helper functions to wait until VBlank and vdraw.
|
||||||
|
|
||||||
|
```rust
|
||||||
|
pub const SCREEN_HEIGHT: isize = 160;
|
||||||
|
|
||||||
|
pub fn wait_until_vblank() {
|
||||||
|
while read_vcount() < SCREEN_HEIGHT as u16 {}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn wait_until_vdraw() {
|
||||||
|
while read_vcount() >= SCREEN_HEIGHT as u16 {}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
And... that's it. No special types to be made this time around, it's just a
|
||||||
|
number we read out of memory.
|
28
book/src/introduction.md
Normal file
28
book/src/introduction.md
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
# Introduction
|
||||||
|
|
||||||
|
Here's a book that'll help you program in Rust on the GBA.
|
||||||
|
|
||||||
|
It's very "work in progress". At the moment there's only one demo program.
|
||||||
|
|
||||||
|
## Getting Help
|
||||||
|
|
||||||
|
If you want to contact us you should join the [Rust Community
|
||||||
|
Discord](https://discordapp.com/invite/aVESxV8) and ask in the `#gamedev`
|
||||||
|
channel. `Ketsuban` is the wizard who knows much more about how it all works,
|
||||||
|
and `Lokathor` is the fool who decided to write a crate and book for it.
|
||||||
|
|
||||||
|
If it's _not_ a GBA specific question then you can also ask any of the few
|
||||||
|
hundred other folks in the server as well.
|
||||||
|
|
||||||
|
## Other Works
|
||||||
|
|
||||||
|
If you want to read more about developing on the GBA there are some other good resources as well:
|
||||||
|
|
||||||
|
* [Tonc](https://www.coranac.com/tonc/text/toc.htm), a tutorial series written
|
||||||
|
for C, but it's what I based the ordering of this book's sections on.
|
||||||
|
* [GBATEK](http://problemkaputt.de/gbatek.htm), a homebrew tech manual for
|
||||||
|
GBA/NDS/DSi. We will regularly link to parts of it when talking about various
|
||||||
|
bits of the GBA.
|
||||||
|
* [CowBite](https://www.cs.rit.edu/~tjh8300/CowBite/CowBiteSpec.htm) is another
|
||||||
|
tech specification that's more GBA specific. It's sometimes got more ASCII
|
||||||
|
art diagrams and example C struct layouts than GBATEK does.
|
14
build.bat
Normal file
14
build.bat
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
|
||||||
|
@rem Build the crt0 file before we begin
|
||||||
|
arm-none-eabi-as crt0.s -o crt0.o
|
||||||
|
|
||||||
|
@rem Build all examples, both debug and release
|
||||||
|
cargo xbuild --examples --target thumbv4-none-agb.json
|
||||||
|
cargo xbuild --examples --target thumbv4-none-agb.json --release
|
||||||
|
|
||||||
|
@echo Packing examples into ROM files...
|
||||||
|
@for %%I in (.\examples\*.*) do @(
|
||||||
|
echo %%~nI
|
||||||
|
arm-none-eabi-objcopy -O binary target/thumbv4-none-agb/release/examples/%%~nI target/example-%%~nI.gba >nul
|
||||||
|
gbafix target/example-%%~nI.gba >nul
|
||||||
|
)
|
34
crt0.s
Normal file
34
crt0.s
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
.arm
|
||||||
|
__start:
|
||||||
|
b .Linit
|
||||||
|
|
||||||
|
@ ROM header will be filled in by gbafix
|
||||||
|
.fill 188, 1, 0
|
||||||
|
|
||||||
|
.Linit:
|
||||||
|
@ set IRQ stack pointer
|
||||||
|
mov r0, #0x12
|
||||||
|
msr CPSR_cf, r0
|
||||||
|
ldr sp, =0x3007fa0
|
||||||
|
|
||||||
|
@ set user stack pointer
|
||||||
|
mov r0, #0x1f
|
||||||
|
msr CPSR_cf, r0
|
||||||
|
ldr sp, =0x3007f00
|
||||||
|
|
||||||
|
@ copy .data section to IWRAM
|
||||||
|
ldr r0, =__data_lma @ source address
|
||||||
|
ldr r1, =__data_start @ destination address
|
||||||
|
ldr r3, =__data_end
|
||||||
|
sub r2, r3, r1
|
||||||
|
beq .Lskip @ don't try to copy an empty .data section
|
||||||
|
add r2, #3
|
||||||
|
mov r2, r2, asr #2 @ length (in words)
|
||||||
|
add r2, #0x04000000 @ copy by words
|
||||||
|
swi 0xb0000
|
||||||
|
|
||||||
|
.Lskip:
|
||||||
|
@ jump to user code
|
||||||
|
ldr r0, =main
|
||||||
|
bx r0
|
||||||
|
.pool
|
Before Width: | Height: | Size: 348 KiB After Width: | Height: | Size: 348 KiB |
Before Width: | Height: | Size: 5.5 KiB After Width: | Height: | Size: 5.5 KiB |
|
@ -139,13 +139,6 @@
|
||||||
<a class="header" href="#introduction" id="introduction"><h1>Introduction</h1></a>
|
<a class="header" href="#introduction" id="introduction"><h1>Introduction</h1></a>
|
||||||
<p>Here's a book that'll help you program in Rust on the GBA.</p>
|
<p>Here's a book that'll help you program in Rust on the GBA.</p>
|
||||||
<p>It's very "work in progress". At the moment there's only one demo program.</p>
|
<p>It's very "work in progress". At the moment there's only one demo program.</p>
|
||||||
<a class="header" href="#getting-help" id="getting-help"><h2>Getting Help</h2></a>
|
|
||||||
<p>If you want to contact us you should join the <a href="https://discordapp.com/invite/aVESxV8">Rust Community
|
|
||||||
Discord</a> and ask in the <code>#gamedev</code>
|
|
||||||
channel. <code>Ketsuban</code> is the wizard who knows much more about how it all works,
|
|
||||||
and <code>Lokathor</code> is the fool who decided to write a crate and book for it.</p>
|
|
||||||
<p>If it's <em>not</em> a GBA specific question then you can also ask any of the few
|
|
||||||
hundred other folks in the server as well.</p>
|
|
||||||
<a class="header" href="#other-works" id="other-works"><h2>Other Works</h2></a>
|
<a class="header" href="#other-works" id="other-works"><h2>Other Works</h2></a>
|
||||||
<p>If you want to read more about developing on the GBA there are some other good resources as well:</p>
|
<p>If you want to read more about developing on the GBA there are some other good resources as well:</p>
|
||||||
<ul>
|
<ul>
|
|
@ -139,13 +139,6 @@
|
||||||
<a class="header" href="#introduction" id="introduction"><h1>Introduction</h1></a>
|
<a class="header" href="#introduction" id="introduction"><h1>Introduction</h1></a>
|
||||||
<p>Here's a book that'll help you program in Rust on the GBA.</p>
|
<p>Here's a book that'll help you program in Rust on the GBA.</p>
|
||||||
<p>It's very "work in progress". At the moment there's only one demo program.</p>
|
<p>It's very "work in progress". At the moment there's only one demo program.</p>
|
||||||
<a class="header" href="#getting-help" id="getting-help"><h2>Getting Help</h2></a>
|
|
||||||
<p>If you want to contact us you should join the <a href="https://discordapp.com/invite/aVESxV8">Rust Community
|
|
||||||
Discord</a> and ask in the <code>#gamedev</code>
|
|
||||||
channel. <code>Ketsuban</code> is the wizard who knows much more about how it all works,
|
|
||||||
and <code>Lokathor</code> is the fool who decided to write a crate and book for it.</p>
|
|
||||||
<p>If it's <em>not</em> a GBA specific question then you can also ask any of the few
|
|
||||||
hundred other folks in the server as well.</p>
|
|
||||||
<a class="header" href="#other-works" id="other-works"><h2>Other Works</h2></a>
|
<a class="header" href="#other-works" id="other-works"><h2>Other Works</h2></a>
|
||||||
<p>If you want to read more about developing on the GBA there are some other good resources as well:</p>
|
<p>If you want to read more about developing on the GBA there are some other good resources as well:</p>
|
||||||
<ul>
|
<ul>
|
0
mark.min.js → docs/mark.min.js
vendored
0
mark.min.js → docs/mark.min.js
vendored
|
@ -139,13 +139,6 @@
|
||||||
<a class="header" href="#introduction" id="introduction"><h1>Introduction</h1></a>
|
<a class="header" href="#introduction" id="introduction"><h1>Introduction</h1></a>
|
||||||
<p>Here's a book that'll help you program in Rust on the GBA.</p>
|
<p>Here's a book that'll help you program in Rust on the GBA.</p>
|
||||||
<p>It's very "work in progress". At the moment there's only one demo program.</p>
|
<p>It's very "work in progress". At the moment there's only one demo program.</p>
|
||||||
<a class="header" href="#getting-help" id="getting-help"><h2>Getting Help</h2></a>
|
|
||||||
<p>If you want to contact us you should join the <a href="https://discordapp.com/invite/aVESxV8">Rust Community
|
|
||||||
Discord</a> and ask in the <code>#gamedev</code>
|
|
||||||
channel. <code>Ketsuban</code> is the wizard who knows much more about how it all works,
|
|
||||||
and <code>Lokathor</code> is the fool who decided to write a crate and book for it.</p>
|
|
||||||
<p>If it's <em>not</em> a GBA specific question then you can also ask any of the few
|
|
||||||
hundred other folks in the server as well.</p>
|
|
||||||
<a class="header" href="#other-works" id="other-works"><h2>Other Works</h2></a>
|
<a class="header" href="#other-works" id="other-works"><h2>Other Works</h2></a>
|
||||||
<p>If you want to read more about developing on the GBA there are some other good resources as well:</p>
|
<p>If you want to read more about developing on the GBA there are some other good resources as well:</p>
|
||||||
<ul>
|
<ul>
|
1
docs/searchindex.js
Normal file
1
docs/searchindex.js
Normal file
File diff suppressed because one or more lines are too long
1
docs/searchindex.json
Normal file
1
docs/searchindex.json
Normal file
File diff suppressed because one or more lines are too long
19
examples/hello1.rs
Normal file
19
examples/hello1.rs
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
#![feature(start)]
|
||||||
|
#![no_std]
|
||||||
|
|
||||||
|
#[cfg(not(test))]
|
||||||
|
#[panic_handler]
|
||||||
|
fn panic(_info: &core::panic::PanicInfo) -> ! {
|
||||||
|
loop {}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[start]
|
||||||
|
fn main(_argc: isize, _argv: *const *const u8) -> isize {
|
||||||
|
unsafe {
|
||||||
|
(0x04000000 as *mut u16).write_volatile(0x0403);
|
||||||
|
(0x06000000 as *mut u16).offset(120 + 80 * 240).write_volatile(0x001F);
|
||||||
|
(0x06000000 as *mut u16).offset(136 + 80 * 240).write_volatile(0x03E0);
|
||||||
|
(0x06000000 as *mut u16).offset(120 + 96 * 240).write_volatile(0x7C00);
|
||||||
|
loop {}
|
||||||
|
}
|
||||||
|
}
|
34
examples/hello2.rs
Normal file
34
examples/hello2.rs
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
#![feature(start)]
|
||||||
|
#![no_std]
|
||||||
|
|
||||||
|
#[cfg(not(test))]
|
||||||
|
#[panic_handler]
|
||||||
|
fn panic(_info: &core::panic::PanicInfo) -> ! {
|
||||||
|
loop {}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[start]
|
||||||
|
fn main(_argc: isize, _argv: *const *const u8) -> isize {
|
||||||
|
unsafe {
|
||||||
|
DISPCNT.write_volatile(MODE3 | BG2);
|
||||||
|
mode3_pixel(120, 80, rgb16(31, 0, 0));
|
||||||
|
mode3_pixel(136, 80, rgb16(0, 31, 0));
|
||||||
|
mode3_pixel(120, 96, rgb16(0, 0, 31));
|
||||||
|
loop {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const DISPCNT: *mut u16 = 0x04000000 as *mut u16;
|
||||||
|
pub const MODE3: u16 = 3;
|
||||||
|
pub const BG2: u16 = 0b100_0000_0000;
|
||||||
|
|
||||||
|
pub const VRAM: usize = 0x06000000;
|
||||||
|
pub const SCREEN_WIDTH: isize = 240;
|
||||||
|
|
||||||
|
pub const fn rgb16(red: u16, green: u16, blue: u16) -> u16 {
|
||||||
|
blue << 10 | green << 5 | red
|
||||||
|
}
|
||||||
|
|
||||||
|
pub unsafe fn mode3_pixel(col: isize, row: isize, color: u16) {
|
||||||
|
(VRAM as *mut u16).offset(col + row * SCREEN_WIDTH).write_volatile(color);
|
||||||
|
}
|
161
examples/light_cycle.rs
Normal file
161
examples/light_cycle.rs
Normal file
|
@ -0,0 +1,161 @@
|
||||||
|
#![feature(start)]
|
||||||
|
#![no_std]
|
||||||
|
|
||||||
|
#[cfg(not(test))]
|
||||||
|
#[panic_handler]
|
||||||
|
fn panic(_info: &core::panic::PanicInfo) -> ! {
|
||||||
|
loop {}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[start]
|
||||||
|
fn main(_argc: isize, _argv: *const *const u8) -> isize {
|
||||||
|
unsafe {
|
||||||
|
DISPCNT.write_volatile(MODE3 | BG2);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut px = SCREEN_WIDTH / 2;
|
||||||
|
let mut py = SCREEN_HEIGHT / 2;
|
||||||
|
let mut color = rgb16(31, 0, 0);
|
||||||
|
|
||||||
|
loop {
|
||||||
|
// read the input for this frame
|
||||||
|
let this_frame_keys = read_key_input();
|
||||||
|
|
||||||
|
// adjust game state and wait for vblank
|
||||||
|
px += 2 * this_frame_keys.column_direction() as isize;
|
||||||
|
py += 2 * this_frame_keys.row_direction() as isize;
|
||||||
|
wait_until_vblank();
|
||||||
|
|
||||||
|
// draw the new game and wait until the next frame starts.
|
||||||
|
unsafe {
|
||||||
|
if px < 0 || py < 0 || px == SCREEN_WIDTH || py == SCREEN_HEIGHT {
|
||||||
|
// out of bounds, reset the screen and position.
|
||||||
|
mode3_clear_screen(0);
|
||||||
|
color = color.rotate_left(5);
|
||||||
|
px = SCREEN_WIDTH / 2;
|
||||||
|
py = SCREEN_HEIGHT / 2;
|
||||||
|
} else {
|
||||||
|
let color_here = mode3_read_pixel(px, py);
|
||||||
|
if color_here != 0 {
|
||||||
|
// crashed into our own line, reset the screen
|
||||||
|
mode3_clear_screen(0);
|
||||||
|
color = color.rotate_left(5);
|
||||||
|
} else {
|
||||||
|
// draw the new part of the line
|
||||||
|
mode3_draw_pixel(px, py, color);
|
||||||
|
mode3_draw_pixel(px, py + 1, color);
|
||||||
|
mode3_draw_pixel(px + 1, py, color);
|
||||||
|
mode3_draw_pixel(px + 1, py + 1, color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
wait_until_vdraw();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const DISPCNT: *mut u16 = 0x04000000 as *mut u16;
|
||||||
|
pub const MODE3: u16 = 3;
|
||||||
|
pub const BG2: u16 = 0b100_0000_0000;
|
||||||
|
|
||||||
|
pub const VRAM: usize = 0x06000000;
|
||||||
|
pub const SCREEN_WIDTH: isize = 240;
|
||||||
|
pub const SCREEN_HEIGHT: isize = 160;
|
||||||
|
|
||||||
|
pub const fn rgb16(red: u16, green: u16, blue: u16) -> u16 {
|
||||||
|
blue << 10 | green << 5 | red
|
||||||
|
}
|
||||||
|
|
||||||
|
pub unsafe fn mode3_clear_screen(color: u16) {
|
||||||
|
let color = color as u32;
|
||||||
|
let bulk_color = color << 16 | color;
|
||||||
|
let mut ptr = VRAM as *mut u32;
|
||||||
|
for _ in 0..SCREEN_HEIGHT {
|
||||||
|
for _ in 0..(SCREEN_WIDTH / 2) {
|
||||||
|
ptr.write_volatile(bulk_color);
|
||||||
|
ptr = ptr.offset(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub unsafe fn mode3_draw_pixel(col: isize, row: isize, color: u16) {
|
||||||
|
(VRAM as *mut u16).offset(col + row * SCREEN_WIDTH).write_volatile(color);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub unsafe fn mode3_read_pixel(col: isize, row: isize) -> u16 {
|
||||||
|
(VRAM as *mut u16).offset(col + row * SCREEN_WIDTH).read_volatile()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const KEYINPUT: *mut u16 = 0x400_0130 as *mut u16;
|
||||||
|
|
||||||
|
/// A newtype over the key input state of the GBA.
|
||||||
|
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
|
||||||
|
#[repr(transparent)]
|
||||||
|
pub struct KeyInputSetting(u16);
|
||||||
|
|
||||||
|
/// A "tribool" value helps us interpret the arrow pad.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
#[repr(i32)]
|
||||||
|
pub enum TriBool {
|
||||||
|
Minus = -1,
|
||||||
|
Neutral = 0,
|
||||||
|
Plus = 1,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn read_key_input() -> KeyInputSetting {
|
||||||
|
unsafe { KeyInputSetting(KEYINPUT.read_volatile() ^ 0b1111_1111_1111_1111) }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const KEY_A: u16 = 1 << 0;
|
||||||
|
pub const KEY_B: u16 = 1 << 1;
|
||||||
|
pub const KEY_SELECT: u16 = 1 << 2;
|
||||||
|
pub const KEY_START: u16 = 1 << 3;
|
||||||
|
pub const KEY_RIGHT: u16 = 1 << 4;
|
||||||
|
pub const KEY_LEFT: u16 = 1 << 5;
|
||||||
|
pub const KEY_UP: u16 = 1 << 6;
|
||||||
|
pub const KEY_DOWN: u16 = 1 << 7;
|
||||||
|
pub const KEY_R: u16 = 1 << 8;
|
||||||
|
pub const KEY_L: u16 = 1 << 9;
|
||||||
|
|
||||||
|
impl KeyInputSetting {
|
||||||
|
pub fn contains(&self, key: u16) -> bool {
|
||||||
|
(self.0 & key) != 0
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn difference(&self, other: KeyInputSetting) -> KeyInputSetting {
|
||||||
|
KeyInputSetting(self.0 ^ other.0)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn column_direction(&self) -> TriBool {
|
||||||
|
if self.contains(KEY_RIGHT) {
|
||||||
|
TriBool::Plus
|
||||||
|
} else if self.contains(KEY_LEFT) {
|
||||||
|
TriBool::Minus
|
||||||
|
} else {
|
||||||
|
TriBool::Neutral
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn row_direction(&self) -> TriBool {
|
||||||
|
if self.contains(KEY_DOWN) {
|
||||||
|
TriBool::Plus
|
||||||
|
} else if self.contains(KEY_UP) {
|
||||||
|
TriBool::Minus
|
||||||
|
} else {
|
||||||
|
TriBool::Neutral
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const VCOUNT: *mut u16 = 0x0400_0006 as *mut u16;
|
||||||
|
|
||||||
|
pub fn read_vcount() -> u16 {
|
||||||
|
unsafe { VCOUNT.read_volatile() }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn wait_until_vblank() {
|
||||||
|
while read_vcount() < SCREEN_HEIGHT as u16 {}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn wait_until_vdraw() {
|
||||||
|
while read_vcount() >= SCREEN_HEIGHT as u16 {}
|
||||||
|
}
|
63
linker.ld
Normal file
63
linker.ld
Normal file
|
@ -0,0 +1,63 @@
|
||||||
|
ENTRY(__start)
|
||||||
|
|
||||||
|
MEMORY {
|
||||||
|
ewram (w!x) : ORIGIN = 0x2000000, LENGTH = 256K
|
||||||
|
iwram (w!x) : ORIGIN = 0x3000000, LENGTH = 32K
|
||||||
|
rom (rx) : ORIGIN = 0x8000000, LENGTH = 32M
|
||||||
|
}
|
||||||
|
|
||||||
|
SECTIONS {
|
||||||
|
.text : {
|
||||||
|
KEEP(crt0.o(.text));
|
||||||
|
*(.text .text.*);
|
||||||
|
. = ALIGN(4);
|
||||||
|
} >rom = 0xff
|
||||||
|
|
||||||
|
.rodata : {
|
||||||
|
*(.rodata .rodata.*);
|
||||||
|
. = ALIGN(4);
|
||||||
|
} >rom = 0xff
|
||||||
|
|
||||||
|
__data_lma = .;
|
||||||
|
.data : {
|
||||||
|
__data_start = ABSOLUTE(.);
|
||||||
|
*(.data .data.*);
|
||||||
|
. = ALIGN(4);
|
||||||
|
__data_end = ABSOLUTE(.);
|
||||||
|
} >iwram AT>rom = 0xff
|
||||||
|
|
||||||
|
/* debugging sections */
|
||||||
|
/* Stabs */
|
||||||
|
.stab 0 : { *(.stab) }
|
||||||
|
.stabstr 0 : { *(.stabstr) }
|
||||||
|
.stab.excl 0 : { *(.stab.excl) }
|
||||||
|
.stab.exclstr 0 : { *(.stab.exclstr) }
|
||||||
|
.stab.index 0 : { *(.stab.index) }
|
||||||
|
.stab.indexstr 0 : { *(.stab.indexstr) }
|
||||||
|
.comment 0 : { *(.comment) }
|
||||||
|
/* DWARF 1 */
|
||||||
|
.debug 0 : { *(.debug) }
|
||||||
|
.line 0 : { *(.line) }
|
||||||
|
/* GNU DWARF 1 extensions */
|
||||||
|
.debug_srcinfo 0 : { *(.debug_srcinfo) }
|
||||||
|
.debug_sfnames 0 : { *(.debug_sfnames) }
|
||||||
|
/* DWARF 1.1 and DWARF 2 */
|
||||||
|
.debug_aranges 0 : { *(.debug_aranges) }
|
||||||
|
.debug_pubnames 0 : { *(.debug_pubnames) }
|
||||||
|
/* DWARF 2 */
|
||||||
|
.debug_info 0 : { *(.debug_info) }
|
||||||
|
.debug_abbrev 0 : { *(.debug_abbrev) }
|
||||||
|
.debug_line 0 : { *(.debug_line) }
|
||||||
|
.debug_frame 0 : { *(.debug_frame) }
|
||||||
|
.debug_str 0 : { *(.debug_str) }
|
||||||
|
.debug_loc 0 : { *(.debug_loc) }
|
||||||
|
.debug_macinfo 0 : { *(.debug_macinfo) }
|
||||||
|
/* SGI/MIPS DWARF 2 extensions */
|
||||||
|
.debug_weaknames 0 : { *(.debug_weaknames) }
|
||||||
|
.debug_funcnames 0 : { *(.debug_funcnames) }
|
||||||
|
.debug_typenames 0 : { *(.debug_typenames) }
|
||||||
|
.debug_varnames 0 : { *(.debug_varnames) }
|
||||||
|
|
||||||
|
/* discard anything not already mentioned */
|
||||||
|
/DISCARD/ : { *(*) }
|
||||||
|
}
|
8
rustfmt.toml
Normal file
8
rustfmt.toml
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
error_on_line_overflow = false
|
||||||
|
fn_args_density = "Compressed"
|
||||||
|
merge_imports = true
|
||||||
|
reorder_imports = true
|
||||||
|
use_try_shorthand = true
|
||||||
|
tab_spaces = 2
|
||||||
|
max_width = 150
|
||||||
|
color = "Never"
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
50
src/core_extras.rs
Normal file
50
src/core_extras.rs
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
//! Things that I wish were in core, but aren't.
|
||||||
|
|
||||||
|
/// A simple wrapper to any `*mut T` so that the basic "read" and "write"
|
||||||
|
/// operations are volatile.
|
||||||
|
///
|
||||||
|
/// Accessing the GBA's IO registers and video ram and specific other places on
|
||||||
|
/// **must** be done with volatile operations. Having this wrapper makes that
|
||||||
|
/// more clear for all the global const values into IO registers.
|
||||||
|
#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, PartialOrd, Ord)]
|
||||||
|
#[repr(transparent)]
|
||||||
|
pub struct VolatilePtr<T>(pub *mut T);
|
||||||
|
|
||||||
|
impl<T> core::fmt::Pointer for VolatilePtr<T> {
|
||||||
|
/// Formats exactly like the inner `*mut T`.
|
||||||
|
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
|
||||||
|
write!(f, "{:p}", self.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> VolatilePtr<T> {
|
||||||
|
/// Performs a volatile read.
|
||||||
|
///
|
||||||
|
/// # Safety
|
||||||
|
///
|
||||||
|
/// This method adds absolutely no additional safety, so all safety concerns
|
||||||
|
/// for a normal raw pointer volatile read apply.
|
||||||
|
pub unsafe fn read(&self) -> T {
|
||||||
|
core::ptr::read_volatile(self.0)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Performs a volatile write.
|
||||||
|
///
|
||||||
|
/// # Safety
|
||||||
|
///
|
||||||
|
/// This method adds absolutely no additional safety, so all safety concerns
|
||||||
|
/// for a normal raw pointer volatile write apply.
|
||||||
|
pub unsafe fn write(&self, data: T) {
|
||||||
|
core::ptr::write_volatile(self.0, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Offsets this address by the amount given.
|
||||||
|
///
|
||||||
|
/// # Safety
|
||||||
|
///
|
||||||
|
/// This is a standard offset, so all safety concerns of a normal raw pointer
|
||||||
|
/// offset apply.
|
||||||
|
pub unsafe fn offset(self, count: isize) -> Self {
|
||||||
|
VolatilePtr(self.0.offset(count))
|
||||||
|
}
|
||||||
|
}
|
477
src/io_registers.rs
Normal file
477
src/io_registers.rs
Normal file
|
@ -0,0 +1,477 @@
|
||||||
|
//! The module for all things relating to the IO Register portion of the GBA's
|
||||||
|
//! memory map.
|
||||||
|
//!
|
||||||
|
//! Here we define many constants for the volatile pointers to the various IO
|
||||||
|
//! registers. Each raw register constant is named according to the name given
|
||||||
|
//! to it in GBATEK's [GBA I/O
|
||||||
|
//! Map](http://problemkaputt.de/gbatek.htm#gbaiomap). They program in C, and so
|
||||||
|
//! of course all the names terrible and missing as many vowels as possible.
|
||||||
|
//! However, being able to look it up online is the most important thing here,
|
||||||
|
//! so oh well.
|
||||||
|
//!
|
||||||
|
//! In addition to the const `VolatilePtr` values, we will over time be adding
|
||||||
|
//! safe wrappers around each register, including newtypes and such so that you
|
||||||
|
//! can easily work with whatever each specific register is doing.
|
||||||
|
|
||||||
|
// TODO(lokathor): IO Register newtypes.
|
||||||
|
|
||||||
|
use gba_proc_macro::register_bit;
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
/// LCD Control. Read/Write.
|
||||||
|
///
|
||||||
|
/// * [gbatek entry](http://problemkaputt.de/gbatek.htm#lcdiodisplaycontrol)
|
||||||
|
pub const DISPCNT: VolatilePtr<u16> = VolatilePtr(0x4000000 as *mut u16);
|
||||||
|
|
||||||
|
/// A newtype over the various display control options that you have on a GBA.
|
||||||
|
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
|
||||||
|
#[repr(transparent)]
|
||||||
|
pub struct DisplayControlSetting(u16);
|
||||||
|
|
||||||
|
#[allow(missing_docs)]
|
||||||
|
impl DisplayControlSetting {
|
||||||
|
pub const BG_MODE_MASK: u16 = 0b111;
|
||||||
|
|
||||||
|
pub fn mode(&self) -> DisplayControlMode {
|
||||||
|
match self.0 & Self::BG_MODE_MASK {
|
||||||
|
0 => DisplayControlMode::Tiled0,
|
||||||
|
1 => DisplayControlMode::Tiled1,
|
||||||
|
2 => DisplayControlMode::Tiled2,
|
||||||
|
3 => DisplayControlMode::Bitmap3,
|
||||||
|
4 => DisplayControlMode::Bitmap4,
|
||||||
|
5 => DisplayControlMode::Bitmap5,
|
||||||
|
_ => unreachable!(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn set_mode(&mut self, new_mode: DisplayControlMode) {
|
||||||
|
self.0 &= !Self::BG_MODE_MASK;
|
||||||
|
self.0 |= match new_mode {
|
||||||
|
DisplayControlMode::Tiled0 => 0,
|
||||||
|
DisplayControlMode::Tiled1 => 1,
|
||||||
|
DisplayControlMode::Tiled2 => 2,
|
||||||
|
DisplayControlMode::Bitmap3 => 3,
|
||||||
|
DisplayControlMode::Bitmap4 => 4,
|
||||||
|
DisplayControlMode::Bitmap5 => 5,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
register_bit!(CGB_MODE_BIT, u16, 0b1000, cgb_mode, read);
|
||||||
|
register_bit!(PAGE_SELECT_BIT, u16, 0b1_0000, page1_enabled, read_write);
|
||||||
|
register_bit!(HBLANK_INTERVAL_FREE_BIT, u16, 0b10_0000, hblank_interval_free, read_write);
|
||||||
|
register_bit!(OBJECT_MEMORY_1D, u16, 0b100_0000, object_memory_1d, read_write);
|
||||||
|
register_bit!(FORCE_BLANK_BIT, u16, 0b1000_0000, force_blank, read_write);
|
||||||
|
register_bit!(DISPLAY_BG0_BIT, u16, 0b1_0000_0000, display_bg0, read_write);
|
||||||
|
register_bit!(DISPLAY_BG1_BIT, u16, 0b10_0000_0000, display_bg1, read_write);
|
||||||
|
register_bit!(DISPLAY_BG2_BIT, u16, 0b100_0000_0000, display_bg2, read_write);
|
||||||
|
register_bit!(DISPLAY_BG3_BIT, u16, 0b1000_0000_0000, display_bg3, read_write);
|
||||||
|
register_bit!(DISPLAY_OBJECT_BIT, u16, 0b1_0000_0000_0000, display_object, read_write);
|
||||||
|
register_bit!(DISPLAY_WINDOW0_BIT, u16, 0b10_0000_0000_0000, display_window0, read_write);
|
||||||
|
register_bit!(DISPLAY_WINDOW1_BIT, u16, 0b100_0000_0000_0000, display_window1, read_write);
|
||||||
|
register_bit!(OBJECT_WINDOW_BIT, u16, 0b1000_0000_0000_0000, display_object_window, read_write);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The six display modes available on the GBA.
|
||||||
|
pub enum DisplayControlMode {
|
||||||
|
/// This basically allows for the most different things at once (all layers,
|
||||||
|
/// 1024 tiles, two palette modes, etc), but you can't do affine
|
||||||
|
/// transformations.
|
||||||
|
Tiled0,
|
||||||
|
/// This is a mix of `Tile0` and `Tile2`: BG0 and BG1 run as if in `Tiled0`,
|
||||||
|
/// and BG2 runs as if in `Tiled2`.
|
||||||
|
Tiled1,
|
||||||
|
/// This allows affine transformations, but only uses BG2 and BG3.
|
||||||
|
Tiled2,
|
||||||
|
/// This is the basic bitmap draw mode. The whole screen is a single bitmap.
|
||||||
|
/// Uses BG2 only.
|
||||||
|
Bitmap3,
|
||||||
|
/// This uses _paletted color_ so that there's enough space to have two pages
|
||||||
|
/// at _full resolution_, allowing page flipping. Uses BG2 only.
|
||||||
|
Bitmap4,
|
||||||
|
/// This uses _reduced resolution_ so that there's enough space to have two
|
||||||
|
/// pages with _full color_, allowing page flipping. Uses BG2 only.
|
||||||
|
Bitmap5,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Assigns the given display control setting.
|
||||||
|
pub fn set_display_control(setting: DisplayControlSetting) {
|
||||||
|
unsafe {
|
||||||
|
DISPCNT.write(setting.0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/// Obtains the current display control setting.
|
||||||
|
pub fn display_control() -> DisplayControlSetting {
|
||||||
|
unsafe { DisplayControlSetting(DISPCNT.read()) }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// General LCD Status (STAT,LYC)
|
||||||
|
pub const DISPSTAT: VolatilePtr<u16> = VolatilePtr(0x4000004 as *mut u16);
|
||||||
|
|
||||||
|
/// Vertical Counter (LY)
|
||||||
|
pub const VCOUNT: VolatilePtr<u16> = VolatilePtr(0x4000006 as *mut u16);
|
||||||
|
|
||||||
|
/// BG0 Control
|
||||||
|
pub const BG0CNT: VolatilePtr<u16> = VolatilePtr(0x4000008 as *mut u16);
|
||||||
|
|
||||||
|
/// BG1 Control
|
||||||
|
pub const BG1CNT: VolatilePtr<u16> = VolatilePtr(0x400000A as *mut u16);
|
||||||
|
|
||||||
|
/// BG2 Control
|
||||||
|
pub const BG2CNT: VolatilePtr<u16> = VolatilePtr(0x400000C as *mut u16);
|
||||||
|
|
||||||
|
/// BG3 Control
|
||||||
|
pub const BG3CNT: VolatilePtr<u16> = VolatilePtr(0x400000E as *mut u16);
|
||||||
|
|
||||||
|
/// BG0 X-Offset
|
||||||
|
pub const BG0HOFS: VolatilePtr<u16> = VolatilePtr(0x4000010 as *mut u16);
|
||||||
|
|
||||||
|
/// BG0 Y-Offset
|
||||||
|
pub const BG0VOFS: VolatilePtr<u16> = VolatilePtr(0x4000012 as *mut u16);
|
||||||
|
|
||||||
|
/// BG1 X-Offset
|
||||||
|
pub const BG1HOFS: VolatilePtr<u16> = VolatilePtr(0x4000014 as *mut u16);
|
||||||
|
|
||||||
|
/// BG1 Y-Offset
|
||||||
|
pub const BG1VOFS: VolatilePtr<u16> = VolatilePtr(0x4000016 as *mut u16);
|
||||||
|
|
||||||
|
/// BG2 X-Offset
|
||||||
|
pub const BG2HOFS: VolatilePtr<u16> = VolatilePtr(0x4000018 as *mut u16);
|
||||||
|
|
||||||
|
/// BG2 Y-Offset
|
||||||
|
pub const BG2VOFS: VolatilePtr<u16> = VolatilePtr(0x400001A as *mut u16);
|
||||||
|
|
||||||
|
/// BG3 X-Offset
|
||||||
|
pub const BG3HOFS: VolatilePtr<u16> = VolatilePtr(0x400001C as *mut u16);
|
||||||
|
|
||||||
|
/// BG3 Y-Offset
|
||||||
|
pub const BG3VOFS: VolatilePtr<u16> = VolatilePtr(0x400001E as *mut u16);
|
||||||
|
|
||||||
|
/// BG2 Rotation/Scaling Parameter A (dx)
|
||||||
|
pub const BG2PA: VolatilePtr<u16> = VolatilePtr(0x4000020 as *mut u16);
|
||||||
|
|
||||||
|
/// BG2 Rotation/Scaling Parameter B (dmx)
|
||||||
|
pub const BG2PB: VolatilePtr<u16> = VolatilePtr(0x4000022 as *mut u16);
|
||||||
|
|
||||||
|
/// BG2 Rotation/Scaling Parameter C (dy)
|
||||||
|
pub const BG2PC: VolatilePtr<u16> = VolatilePtr(0x4000024 as *mut u16);
|
||||||
|
|
||||||
|
/// BG2 Rotation/Scaling Parameter D (dmy)
|
||||||
|
pub const BG2PD: VolatilePtr<u16> = VolatilePtr(0x4000026 as *mut u16);
|
||||||
|
|
||||||
|
/// BG2 Reference Point X-Coordinate
|
||||||
|
pub const BG2X: VolatilePtr<u32> = VolatilePtr(0x4000028 as *mut u32);
|
||||||
|
|
||||||
|
/// BG2 Reference Point Y-Coordinate
|
||||||
|
pub const BG2Y: VolatilePtr<u32> = VolatilePtr(0x400002C as *mut u32);
|
||||||
|
|
||||||
|
/// BG3 Rotation/Scaling Parameter A (dx)
|
||||||
|
pub const BG3PA: VolatilePtr<u16> = VolatilePtr(0x4000030 as *mut u16);
|
||||||
|
|
||||||
|
/// BG3 Rotation/Scaling Parameter B (dmx)
|
||||||
|
pub const BG3PB: VolatilePtr<u16> = VolatilePtr(0x4000032 as *mut u16);
|
||||||
|
|
||||||
|
/// BG3 Rotation/Scaling Parameter C (dy)
|
||||||
|
pub const BG3PC: VolatilePtr<u16> = VolatilePtr(0x4000034 as *mut u16);
|
||||||
|
|
||||||
|
/// BG3 Rotation/Scaling Parameter D (dmy)
|
||||||
|
pub const BG3PD: VolatilePtr<u16> = VolatilePtr(0x4000036 as *mut u16);
|
||||||
|
|
||||||
|
/// BG3 Reference Point X-Coordinate
|
||||||
|
pub const BG3X: VolatilePtr<u32> = VolatilePtr(0x4000038 as *mut u32);
|
||||||
|
|
||||||
|
/// BG3 Reference Point Y-Coordinate
|
||||||
|
pub const BG3Y: VolatilePtr<u32> = VolatilePtr(0x400003C as *mut u32);
|
||||||
|
|
||||||
|
/// Window 0 Horizontal Dimensions
|
||||||
|
pub const WIN0H: VolatilePtr<u16> = VolatilePtr(0x4000040 as *mut u16);
|
||||||
|
|
||||||
|
/// Window 1 Horizontal Dimensions
|
||||||
|
pub const WIN1H: VolatilePtr<u16> = VolatilePtr(0x4000042 as *mut u16);
|
||||||
|
|
||||||
|
/// Window 0 Vertical Dimensions
|
||||||
|
pub const WIN0V: VolatilePtr<u16> = VolatilePtr(0x4000044 as *mut u16);
|
||||||
|
|
||||||
|
/// Window 1 Vertical Dimensions
|
||||||
|
pub const WIN1V: VolatilePtr<u16> = VolatilePtr(0x4000046 as *mut u16);
|
||||||
|
|
||||||
|
/// Inside of Window 0 and 1
|
||||||
|
pub const WININ: VolatilePtr<u16> = VolatilePtr(0x4000048 as *mut u16);
|
||||||
|
|
||||||
|
/// Inside of OBJ Window & Outside of Windows
|
||||||
|
pub const WINOUT: VolatilePtr<u16> = VolatilePtr(0x400004A as *mut u16);
|
||||||
|
|
||||||
|
/// Mosaic Size
|
||||||
|
pub const MOSAIC: VolatilePtr<u16> = VolatilePtr(0x400004C as *mut u16);
|
||||||
|
|
||||||
|
/// Color Special Effects Selection
|
||||||
|
pub const BLDCNT: VolatilePtr<u16> = VolatilePtr(0x4000050 as *mut u16);
|
||||||
|
|
||||||
|
/// Alpha Blending Coefficients
|
||||||
|
pub const BLDALPHA: VolatilePtr<u16> = VolatilePtr(0x4000052 as *mut u16);
|
||||||
|
|
||||||
|
/// Brightness (Fade-In/Out) Coefficient
|
||||||
|
pub const BLDY: VolatilePtr<u16> = VolatilePtr(0x4000054 as *mut u16);
|
||||||
|
|
||||||
|
/// Channel 1 Sweep register (NR10)
|
||||||
|
pub const UND1CNT_L: VolatilePtr<u16> = VolatilePtr(0x4000060 as *mut u16);
|
||||||
|
|
||||||
|
/// Channel 1 Duty/Length/Envelope (NR11, NR12)
|
||||||
|
pub const UND1CNT_H: VolatilePtr<u16> = VolatilePtr(0x4000062 as *mut u16);
|
||||||
|
|
||||||
|
/// Channel 1 Frequency/Control (NR13, NR14)
|
||||||
|
pub const UND1CNT_X: VolatilePtr<u16> = VolatilePtr(0x4000064 as *mut u16);
|
||||||
|
|
||||||
|
/// Channel 2 Duty/Length/Envelope (NR21, NR22)
|
||||||
|
pub const UND2CNT_L: VolatilePtr<u16> = VolatilePtr(0x4000068 as *mut u16);
|
||||||
|
|
||||||
|
/// Channel 2 Frequency/Control (NR23, NR24)
|
||||||
|
pub const UND2CNT_H: VolatilePtr<u16> = VolatilePtr(0x400006C as *mut u16);
|
||||||
|
|
||||||
|
/// Channel 3 Stop/Wave RAM select (NR30)
|
||||||
|
pub const UND3CNT_L: VolatilePtr<u16> = VolatilePtr(0x4000070 as *mut u16);
|
||||||
|
|
||||||
|
/// Channel 3 Length/Volume (NR31, NR32)
|
||||||
|
pub const UND3CNT_H: VolatilePtr<u16> = VolatilePtr(0x4000072 as *mut u16);
|
||||||
|
|
||||||
|
/// Channel 3 Frequency/Control (NR33, NR34)
|
||||||
|
pub const UND3CNT_X: VolatilePtr<u16> = VolatilePtr(0x4000074 as *mut u16);
|
||||||
|
|
||||||
|
/// Channel 4 Length/Envelope (NR41, NR42)
|
||||||
|
pub const UND4CNT_L: VolatilePtr<u16> = VolatilePtr(0x4000078 as *mut u16);
|
||||||
|
|
||||||
|
/// Channel 4 Frequency/Control (NR43, NR44)
|
||||||
|
pub const UND4CNT_H: VolatilePtr<u16> = VolatilePtr(0x400007C as *mut u16);
|
||||||
|
|
||||||
|
/// Control Stereo/Volume/Enable (NR50, NR51)
|
||||||
|
pub const UNDCNT_L: VolatilePtr<u16> = VolatilePtr(0x4000080 as *mut u16);
|
||||||
|
|
||||||
|
/// Control Mixing/DMA Control
|
||||||
|
pub const UNDCNT_H: VolatilePtr<u16> = VolatilePtr(0x4000082 as *mut u16);
|
||||||
|
|
||||||
|
/// Control Sound on/off (NR52)
|
||||||
|
pub const UNDCNT_X: VolatilePtr<u16> = VolatilePtr(0x4000084 as *mut u16);
|
||||||
|
|
||||||
|
/// Sound PWM Control
|
||||||
|
pub const UNDBIAS: VolatilePtr<u16> = VolatilePtr(0x4000088 as *mut u16);
|
||||||
|
|
||||||
|
/// Channel 3 Wave Pattern RAM (W/R)
|
||||||
|
pub const WAVE_RAM0_L: VolatilePtr<u16> = VolatilePtr(0x4000090 as *mut u16);
|
||||||
|
|
||||||
|
/// Channel 3 Wave Pattern RAM (W/R)
|
||||||
|
pub const WAVE_RAM0_H: VolatilePtr<u16> = VolatilePtr(0x4000092 as *mut u16);
|
||||||
|
|
||||||
|
/// Channel 3 Wave Pattern RAM (W/R)
|
||||||
|
pub const WAVE_RAM1_L: VolatilePtr<u16> = VolatilePtr(0x4000094 as *mut u16);
|
||||||
|
|
||||||
|
/// Channel 3 Wave Pattern RAM (W/R)
|
||||||
|
pub const WAVE_RAM1_H: VolatilePtr<u16> = VolatilePtr(0x4000096 as *mut u16);
|
||||||
|
|
||||||
|
/// Channel 3 Wave Pattern RAM (W/R)
|
||||||
|
pub const WAVE_RAM2_L: VolatilePtr<u16> = VolatilePtr(0x4000098 as *mut u16);
|
||||||
|
|
||||||
|
/// Channel 3 Wave Pattern RAM (W/R)
|
||||||
|
pub const WAVE_RAM2_H: VolatilePtr<u16> = VolatilePtr(0x400009A as *mut u16);
|
||||||
|
|
||||||
|
/// Channel 3 Wave Pattern RAM (W/R)
|
||||||
|
pub const WAVE_RAM3_L: VolatilePtr<u16> = VolatilePtr(0x400009C as *mut u16);
|
||||||
|
|
||||||
|
/// Channel 3 Wave Pattern RAM (W/R)
|
||||||
|
pub const WAVE_RAM3_H: VolatilePtr<u16> = VolatilePtr(0x400009E as *mut u16);
|
||||||
|
|
||||||
|
/// Channel A FIFO, Data 0-3
|
||||||
|
pub const FIFO_A: VolatilePtr<u32> = VolatilePtr(0x40000A0 as *mut u32);
|
||||||
|
|
||||||
|
/// Channel B FIFO, Data 0-3
|
||||||
|
pub const FIFO_B: VolatilePtr<u32> = VolatilePtr(0x40000A4 as *mut u32);
|
||||||
|
|
||||||
|
/// DMA 0 Source Address
|
||||||
|
pub const DMA0SAD: VolatilePtr<u32> = VolatilePtr(0x40000B0 as *mut u32);
|
||||||
|
|
||||||
|
/// DMA 0 Destination Address
|
||||||
|
pub const DMA0DAD: VolatilePtr<u32> = VolatilePtr(0x40000B4 as *mut u32);
|
||||||
|
|
||||||
|
/// DMA 0 Word Count
|
||||||
|
pub const DMA0CNT_L: VolatilePtr<u16> = VolatilePtr(0x40000B8 as *mut u16);
|
||||||
|
|
||||||
|
/// DMA 0 Control
|
||||||
|
pub const DMA0CNT_H: VolatilePtr<u16> = VolatilePtr(0x40000BA as *mut u16);
|
||||||
|
|
||||||
|
/// DMA 1 Source Address
|
||||||
|
pub const DMA1SAD: VolatilePtr<u32> = VolatilePtr(0x40000BC as *mut u32);
|
||||||
|
|
||||||
|
/// DMA 1 Destination Address
|
||||||
|
pub const DMA1DAD: VolatilePtr<u32> = VolatilePtr(0x40000C0 as *mut u32);
|
||||||
|
|
||||||
|
/// DMA 1 Word Count
|
||||||
|
pub const DMA1CNT_L: VolatilePtr<u16> = VolatilePtr(0x40000C4 as *mut u16);
|
||||||
|
|
||||||
|
/// DMA 1 Control
|
||||||
|
pub const DMA1CNT_H: VolatilePtr<u16> = VolatilePtr(0x40000C6 as *mut u16);
|
||||||
|
|
||||||
|
/// DMA 2 Source Address
|
||||||
|
pub const DMA2SAD: VolatilePtr<u32> = VolatilePtr(0x40000C8 as *mut u32);
|
||||||
|
|
||||||
|
/// DMA 2 Destination Address
|
||||||
|
pub const DMA2DAD: VolatilePtr<u32> = VolatilePtr(0x40000CC as *mut u32);
|
||||||
|
|
||||||
|
/// DMA 2 Word Count
|
||||||
|
pub const DMA2CNT_L: VolatilePtr<u16> = VolatilePtr(0x40000D0 as *mut u16);
|
||||||
|
|
||||||
|
/// DMA 2 Control
|
||||||
|
pub const DMA2CNT_H: VolatilePtr<u16> = VolatilePtr(0x40000D2 as *mut u16);
|
||||||
|
|
||||||
|
/// DMA 3 Source Address
|
||||||
|
pub const DMA3SAD: VolatilePtr<u32> = VolatilePtr(0x40000D4 as *mut u32);
|
||||||
|
|
||||||
|
/// DMA 3 Destination Address
|
||||||
|
pub const DMA3DAD: VolatilePtr<u32> = VolatilePtr(0x40000D8 as *mut u32);
|
||||||
|
|
||||||
|
/// DMA 3 Word Count
|
||||||
|
pub const DMA3CNT_L: VolatilePtr<u16> = VolatilePtr(0x40000DC as *mut u16);
|
||||||
|
|
||||||
|
/// DMA 3 Control
|
||||||
|
pub const DMA3CNT_H: VolatilePtr<u16> = VolatilePtr(0x40000DE as *mut u16);
|
||||||
|
|
||||||
|
/// Timer 0 Counter/Reload
|
||||||
|
pub const TM0CNT_L: VolatilePtr<u16> = VolatilePtr(0x4000100 as *mut u16);
|
||||||
|
|
||||||
|
/// Timer 0 Control
|
||||||
|
pub const TM0CNT_H: VolatilePtr<u16> = VolatilePtr(0x4000102 as *mut u16);
|
||||||
|
|
||||||
|
/// Timer 1 Counter/Reload
|
||||||
|
pub const TM1CNT_L: VolatilePtr<u16> = VolatilePtr(0x4000104 as *mut u16);
|
||||||
|
|
||||||
|
/// Timer 1 Control
|
||||||
|
pub const TM1CNT_H: VolatilePtr<u16> = VolatilePtr(0x4000106 as *mut u16);
|
||||||
|
|
||||||
|
/// Timer 2 Counter/Reload
|
||||||
|
pub const TM2CNT_L: VolatilePtr<u16> = VolatilePtr(0x4000108 as *mut u16);
|
||||||
|
|
||||||
|
/// Timer 2 Control
|
||||||
|
pub const TM2CNT_H: VolatilePtr<u16> = VolatilePtr(0x400010A as *mut u16);
|
||||||
|
|
||||||
|
/// Timer 3 Counter/Reload
|
||||||
|
pub const TM3CNT_L: VolatilePtr<u16> = VolatilePtr(0x400010C as *mut u16);
|
||||||
|
|
||||||
|
/// Timer 3 Control
|
||||||
|
pub const TM3CNT_H: VolatilePtr<u16> = VolatilePtr(0x400010E as *mut u16);
|
||||||
|
|
||||||
|
/// SIO Data (Normal-32bit Mode; shared with below)
|
||||||
|
pub const SIODATA32: VolatilePtr<u32> = VolatilePtr(0x4000120 as *mut u32);
|
||||||
|
|
||||||
|
/// SIO Data 0 (Parent) (Multi-Player Mode)
|
||||||
|
pub const SIOMULTI0: VolatilePtr<u16> = VolatilePtr(0x4000120 as *mut u16);
|
||||||
|
|
||||||
|
/// SIO Data 1 (1st Child) (Multi-Player Mode)
|
||||||
|
pub const SIOMULTI1: VolatilePtr<u16> = VolatilePtr(0x4000122 as *mut u16);
|
||||||
|
|
||||||
|
/// SIO Data 2 (2nd Child) (Multi-Player Mode)
|
||||||
|
pub const SIOMULTI2: VolatilePtr<u16> = VolatilePtr(0x4000124 as *mut u16);
|
||||||
|
|
||||||
|
/// SIO Data 3 (3rd Child) (Multi-Player Mode)
|
||||||
|
pub const SIOMULTI3: VolatilePtr<u16> = VolatilePtr(0x4000126 as *mut u16);
|
||||||
|
|
||||||
|
/// SIO Control Register
|
||||||
|
pub const SIOCNT: VolatilePtr<u16> = VolatilePtr(0x4000128 as *mut u16);
|
||||||
|
|
||||||
|
/// D SIO Data (Local of MultiPlayer; shared below)
|
||||||
|
pub const SIOMLT_SEN: VolatilePtr<u16> = VolatilePtr(0x400012A as *mut u16);
|
||||||
|
|
||||||
|
/// SIO Data (Normal-8bit and UART Mode)
|
||||||
|
pub const SIODATA8: VolatilePtr<u16> = VolatilePtr(0x400012A as *mut u16);
|
||||||
|
|
||||||
|
/// Key Status
|
||||||
|
pub const KEYINPUT: VolatilePtr<u16> = VolatilePtr(0x4000130 as *mut u16);
|
||||||
|
|
||||||
|
/// A newtype over the key input state of the GBA.
|
||||||
|
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
|
||||||
|
#[repr(transparent)]
|
||||||
|
pub struct KeyInputSetting(u16);
|
||||||
|
|
||||||
|
/// A "tribool" value helps us interpret the arrow pad.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
#[repr(i32)]
|
||||||
|
#[allow(missing_docs)]
|
||||||
|
pub enum TriBool {
|
||||||
|
Minus = -1,
|
||||||
|
Neutral = 0,
|
||||||
|
Plus = 1,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(missing_docs)]
|
||||||
|
impl KeyInputSetting {
|
||||||
|
register_bit!(A_BIT, u16, 1 << 0, a_pressed, read_write);
|
||||||
|
register_bit!(B_BIT, u16, 1 << 1, b_pressed, read_write);
|
||||||
|
register_bit!(SELECT_BIT, u16, 1 << 2, select_pressed, read_write);
|
||||||
|
register_bit!(START_BIT, u16, 1 << 3, start_pressed, read_write);
|
||||||
|
register_bit!(RIGHT_BIT, u16, 1 << 4, right_pressed, read_write);
|
||||||
|
register_bit!(LEFT_BIT, u16, 1 << 5, left_pressed, read_write);
|
||||||
|
register_bit!(UP_BIT, u16, 1 << 6, up_pressed, read_write);
|
||||||
|
register_bit!(DOWN_BIT, u16, 1 << 7, down_pressed, read_write);
|
||||||
|
register_bit!(R_BIT, u16, 1 << 8, r_pressed, read_write);
|
||||||
|
register_bit!(L_BIT, u16, 1 << 9, l_pressed, read_write);
|
||||||
|
|
||||||
|
/// Takes the difference between these keys and another set of keys.
|
||||||
|
pub fn difference(&self, other: KeyInputSetting) -> KeyInputSetting {
|
||||||
|
KeyInputSetting(self.0 ^ other.0)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Gives the arrow pad value as a tribool, with Plus being increased column
|
||||||
|
/// value (right).
|
||||||
|
pub fn column_direction(&self) -> TriBool {
|
||||||
|
if self.right_pressed() {
|
||||||
|
TriBool::Plus
|
||||||
|
} else if self.left_pressed() {
|
||||||
|
TriBool::Minus
|
||||||
|
} else {
|
||||||
|
TriBool::Neutral
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Gives the arrow pad value as a tribool, with Plus being increased row
|
||||||
|
/// value (down).
|
||||||
|
pub fn row_direction(&self) -> TriBool {
|
||||||
|
if self.down_pressed() {
|
||||||
|
TriBool::Plus
|
||||||
|
} else if self.up_pressed() {
|
||||||
|
TriBool::Minus
|
||||||
|
} else {
|
||||||
|
TriBool::Neutral
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Gets the current state of the keys
|
||||||
|
pub fn read_key_input() -> KeyInputSetting {
|
||||||
|
unsafe { KeyInputSetting(KEYINPUT.read() ^ 0b1111_1111_1111_1111) }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Key Interrupt Control
|
||||||
|
pub const KEYCNT: VolatilePtr<u16> = VolatilePtr(0x4000132 as *mut u16);
|
||||||
|
|
||||||
|
/// SIO Mode Select/General Purpose Data
|
||||||
|
pub const RCNT: VolatilePtr<u16> = VolatilePtr(0x4000134 as *mut u16);
|
||||||
|
|
||||||
|
/// SIO JOY Bus Control
|
||||||
|
pub const JOYCNT: VolatilePtr<u16> = VolatilePtr(0x4000140 as *mut u16);
|
||||||
|
|
||||||
|
/// SIO JOY Bus Receive Data
|
||||||
|
pub const JOY_RECV: VolatilePtr<u32> = VolatilePtr(0x4000150 as *mut u32);
|
||||||
|
|
||||||
|
/// SIO JOY Bus Transmit Data
|
||||||
|
pub const JOY_TRANS: VolatilePtr<u32> = VolatilePtr(0x4000154 as *mut u32);
|
||||||
|
|
||||||
|
/// SIO JOY Bus Receive Status
|
||||||
|
pub const JOYSTAT: VolatilePtr<u16> = VolatilePtr(0x4000158 as *mut u16);
|
||||||
|
|
||||||
|
/// Interrupt Enable Register
|
||||||
|
pub const IE: VolatilePtr<u16> = VolatilePtr(0x4000200 as *mut u16);
|
||||||
|
|
||||||
|
/// Interrupt Request Flags / IRQ Acknowledge
|
||||||
|
pub const IF: VolatilePtr<u16> = VolatilePtr(0x4000202 as *mut u16);
|
||||||
|
|
||||||
|
/// Game Pak Waitstate Control
|
||||||
|
pub const WAITCNT: VolatilePtr<u16> = VolatilePtr(0x4000204 as *mut u16);
|
||||||
|
|
||||||
|
/// Interrupt Master Enable Register
|
||||||
|
pub const IME: VolatilePtr<u16> = VolatilePtr(0x4000208 as *mut u16);
|
27
src/lib.rs
Normal file
27
src/lib.rs
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
#![no_std]
|
||||||
|
#![warn(missing_docs)]
|
||||||
|
|
||||||
|
//! This crate helps you write GBA ROMs.
|
||||||
|
//!
|
||||||
|
//! # SAFETY POLICY
|
||||||
|
//!
|
||||||
|
//! Some parts of this crate are safe wrappers around unsafe operations. This is
|
||||||
|
//! good, and what you'd expect from a Rust crate.
|
||||||
|
//!
|
||||||
|
//! However, the safe wrappers all assume that you will _only_ attempt to
|
||||||
|
//! execute this crate on a GBA or in a GBA Emulator.
|
||||||
|
//!
|
||||||
|
//! **Do not** use this crate in programs that aren't running on the GBA. If you
|
||||||
|
//! do, it's a giant bag of Undefined Behavior.
|
||||||
|
|
||||||
|
pub mod core_extras;
|
||||||
|
pub(crate) use crate::core_extras::*;
|
||||||
|
|
||||||
|
pub mod io_registers;
|
||||||
|
|
||||||
|
pub mod video_ram;
|
||||||
|
|
||||||
|
/// Combines the Red, Blue, and Green provided into a single color value.
|
||||||
|
pub const fn rgb16(red: u16, green: u16, blue: u16) -> u16 {
|
||||||
|
blue << 10 | green << 5 | red
|
||||||
|
}
|
8
src/macros.rs
Normal file
8
src/macros.rs
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
//! Module for all macros.
|
||||||
|
//!
|
||||||
|
//! Macros are the only thing in Rust where declaration order matters, so we
|
||||||
|
//! place all of them here regardless of what they do so that the macros module
|
||||||
|
//! can appear at the "top" of the library and all other modules can see them
|
||||||
|
//! properly.
|
||||||
|
|
||||||
|
// no macros yet!
|
49
src/video_ram.rs
Normal file
49
src/video_ram.rs
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
//! Module for all things relating to the Video RAM.
|
||||||
|
//!
|
||||||
|
//! Note that the GBA has six different display modes available, and the
|
||||||
|
//! _meaning_ of Video RAM depends on which display mode is active. In all
|
||||||
|
//! cases, Video RAM is **96kb** from `0x0600_0000` to `0x0601_7FFF`.
|
||||||
|
//!
|
||||||
|
//! # Safety
|
||||||
|
//!
|
||||||
|
//! Note that all possible bit patterns are technically allowed within Video
|
||||||
|
//! Memory. If you write the "wrong" thing into video memory you don't crash the
|
||||||
|
//! GBA, instead you just get graphical glitches (or perhaps nothing at all).
|
||||||
|
//! Accordingly, the "safe" functions here will check that you're in bounds, but
|
||||||
|
//! they won't bother to check that you've set the video mode they're designed
|
||||||
|
//! for.
|
||||||
|
|
||||||
|
/// The physical width in pixels of the GBA screen.
|
||||||
|
pub const SCREEN_WIDTH: isize = 240;
|
||||||
|
|
||||||
|
/// The physical height in pixels of the GBA screen.
|
||||||
|
pub const SCREEN_HEIGHT: isize = 160;
|
||||||
|
|
||||||
|
/// The start of VRAM.
|
||||||
|
///
|
||||||
|
/// Depending on what display mode is currently set there's different ways that
|
||||||
|
/// your program should interpret the VRAM space. Accordingly, we give the raw
|
||||||
|
/// value as just being a `usize`.
|
||||||
|
pub const VRAM_BASE_ADDRESS: usize = 0x0600_0000;
|
||||||
|
|
||||||
|
/// Draws a pixel to the screen while in Display Mode 3, with bounds checks.
|
||||||
|
pub fn mode3_pixel(col: isize, row: isize, color: u16) {
|
||||||
|
assert!(col >= 0 && col < SCREEN_WIDTH);
|
||||||
|
assert!(row >= 0 && row < SCREEN_HEIGHT);
|
||||||
|
unsafe { mode3_pixel_unchecked(col, row, color) }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Draws a pixel to the screen while in Display Mode 3.
|
||||||
|
///
|
||||||
|
/// Coordinates are relative to the top left corner.
|
||||||
|
///
|
||||||
|
/// If you're in another mode you'll get something weird drawn, but it's memory
|
||||||
|
/// safe in the rust sense as long as you stay in bounds.
|
||||||
|
///
|
||||||
|
/// # Safety
|
||||||
|
///
|
||||||
|
/// * `col` must be in `0..SCREEN_WIDTH`
|
||||||
|
/// * `row` must be in `0..SCREEN_HEIGHT`
|
||||||
|
pub unsafe fn mode3_pixel_unchecked(col: isize, row: isize, color: u16) {
|
||||||
|
core::ptr::write_volatile((VRAM_BASE_ADDRESS as *mut u16).offset(col + row * SCREEN_WIDTH), color);
|
||||||
|
}
|
34
thumbv4-none-agb.json
Normal file
34
thumbv4-none-agb.json
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
{
|
||||||
|
"abi-blacklist": [
|
||||||
|
"stdcall",
|
||||||
|
"fastcall",
|
||||||
|
"vectorcall",
|
||||||
|
"thiscall",
|
||||||
|
"win64",
|
||||||
|
"sysv64"
|
||||||
|
],
|
||||||
|
"arch": "arm",
|
||||||
|
"atomic-cas": false,
|
||||||
|
"cpu": "arm7tdmi",
|
||||||
|
"data-layout": "e-m:e-p:32:32-i64:64-v128:64:128-a:0:32-n32-S64",
|
||||||
|
"emit-debug-gdb-scripts": false,
|
||||||
|
"env": "",
|
||||||
|
"executables": true,
|
||||||
|
"features": "+soft-float,+strict-align",
|
||||||
|
"linker": "arm-none-eabi-ld",
|
||||||
|
"linker-flavor": "ld",
|
||||||
|
"linker-is-gnu": true,
|
||||||
|
"llvm-target": "thumbv4-none-agb",
|
||||||
|
"os": "none",
|
||||||
|
"panic-strategy": "abort",
|
||||||
|
"pre-link-args": {
|
||||||
|
"ld": [
|
||||||
|
"-Tlinker.ld"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"relocation-model": "static",
|
||||||
|
"target-c-int-width": "32",
|
||||||
|
"target-endian": "little",
|
||||||
|
"target-pointer-width": "32",
|
||||||
|
"vendor": ""
|
||||||
|
}
|
Loading…
Reference in a new issue