diff --git a/.travis.yml b/.travis.yml index eea3dd4..6af87ae 100644 --- a/.travis.yml +++ b/.travis.yml @@ -6,12 +6,18 @@ cache: directories: - $HOME/.cargo +branches: + only: + - staging + - trying + - master + - lokathor + rust: - nightly before_script: - rustup component add rust-src - - rustup component add clippy --toolchain=nightly - (test -x $HOME/.cargo/bin/cargo-install-update || cargo install cargo-update) - (test -x $HOME/.cargo/bin/cargo-xbuild || cargo install cargo-xbuild) - (test -x $HOME/.cargo/bin/cargo-make || cargo install cargo-make) @@ -31,15 +37,13 @@ script: - export PATH="$PATH:/opt/devkitpro/tools/bin" - cd .. # Run all verificaions, both debug and release - #- cargo clippy - #- cargo clippy --release - - cargo test --no-fail-fast --lib - - cargo test --no-fail-fast --lib --release - - cargo test --no-fail-fast --tests - - cargo test --no-fail-fast --tests --release + - cargo test --lib + - cargo test --lib --release + - cargo test --tests + - cargo test --tests --release # Let cargo make take over the rest - cargo make justrelease - # Test build the book so that a failed book build kills this run + # Test a build of the book so that a failed book build kills this run - cd book && mdbook build deploy: diff --git a/Cargo.toml b/Cargo.toml index bb3d4ac..0483081 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "gba" description = "A crate (and book) for making GBA games with Rust." -version = "0.3.2" +version = "0.4.0-pre" authors = ["Lokathor ", "Thomas Winwood "] repository = "https://github.com/rust-console/gba" readme = "README.md" @@ -9,10 +9,11 @@ keywords = ["gba"] edition = "2018" license = "Apache-2.0" -#publish = false +publish = false [dependencies] typenum = "1.10" +voladdress = "0.2" gba-proc-macro = "0.5" [profile.release] diff --git a/book/src-bak/03-volatile_destination.md b/book/src-bak/03-volatile_destination.md index ce50a98..18f8fdb 100644 --- a/book/src-bak/03-volatile_destination.md +++ b/book/src-bak/03-volatile_destination.md @@ -146,7 +146,7 @@ So, again using the `hello_magic` example, we had And instead we could declare ```rust -const MAGIC_LOCATION: VolAddress = unsafe { VolAddress::new_unchecked(0x400_0000) }; +const MAGIC_LOCATION: VolAddress = unsafe { VolAddress::new(0x400_0000) }; ``` ### Using A VolAddress Value @@ -300,7 +300,7 @@ Now we can have something like: ```rust const OTHER_MAGIC: VolAddressBlock = unsafe { VolAddressBlock::new_unchecked( - VolAddress::new_unchecked(0x600_0000), + VolAddress::new(0x600_0000), 240 * 160 ) }; diff --git a/book/src/SUMMARY.md b/book/src/SUMMARY.md index 153ef29..327fe00 100644 --- a/book/src/SUMMARY.md +++ b/book/src/SUMMARY.md @@ -2,4 +2,8 @@ # Rust GBA Guide * [Development Setup](development-setup.md) +* [Volatile](volatile.md) +* [The Hardware Memory Map](the-hardware-memory-map.md) +* [IO Registers](io-registers.md) +* [Bitmap Video](bitmap-video.md) * [GBA Assembly](gba-asm.md) diff --git a/book/src/bitmap-video.md b/book/src/bitmap-video.md new file mode 100644 index 0000000..0b10fa3 --- /dev/null +++ b/book/src/bitmap-video.md @@ -0,0 +1,214 @@ +# Bitmap Video + +Our first video modes to talk about are the bitmap video modes. + +It's not because they're the best and fastest, it's because they're the +_simplest_. You can get going and practice with them really quickly. Usually +after that you end up wanting to move on to the other video modes because they +have better hardware support, so you can draw more complex things with the small +number of cycles that the GBA allows. + +## The Three Bitmap Modes + +As I said in the Hardware Memory Map section, the Video RAM lives in the address +space at `0x600_0000`. Depending on our video mode the display controller will +consider this memory to be in one of a few totally different formats. + +### Mode 3 + +The screen is 160 rows, each 240 pixels long, of `u16` color values. + +This is "full" resolution, and "full" color. It adds up to 76,800 bytes. VRAM is +only 96,304 bytes total though. There's enough space left over after the bitmap +for some object tile data if you want to use objects, but basically Mode3 is +using all of VRAM as one huge canvas. + +### Mode 4 + +The screen is 160 rows, each 240 pixels long, of `u8` palette values. + +This has half as much space per pixel. What's a palette value? That's an index +into the background PALRAM which says what the color of that pixel should be. We +still have the full color space available, but we can only use 256 colors at the +same time. + +What did we get in exchange for this? Well, now there's a second "page". The +second page starts `0xA000` bytes into VRAM (in both Mode 4 and Mode 5). It's an +entire second set of pixel data. You determine if Page 0 or Page 1 is shown +using bit 4 of DISPCNT. When you swap which page is being displayed it's called +page flipping or flipping the page, or something like that. + +Having two pages is cool, but Mode 4 has a big drawback: it's part of VRAM so +that "can't write 1 byte at a time" rule applies. This means that to set a +single byte we need to read a `u16`, adjust just one side of it, and then write +that `u16` back. We can hide the complication behind a method call, but it +simply takes longer to do all that, so editing pixels ends up being +unfortunately slow compared to the other bitmap modes. + +### Mode 5 + +The screen is 128 rows, each 160 pixels long, of `u16` color values. + +Mode 5 has two pages like Mode 4 does, but instead of keeping full resolution we +keep full color. The pixels are displayed in the top left and it's just black on +the right and bottom edges. You can use the background control registers to +shift it around, maybe center it, but there's no way to get around the fact that +not having full resolution is kinda awkward. + +## Using Mode 3 + +Let's have a look at how this comes together. We'll call this one +`hello_world.rs`, since it's our first real program. + +### Module Attributes and Imports + +At the top of our file we're still `no_std` and we're still using +`feature(start)`, but now we're using the `gba` crate so we're 100% safe code! +Often enough we'll need a little `unsafe`, but for just bitmap drawing we don't +need it. + +```rust +#![no_std] +#![feature(start)] +#![forbid(unsafe_code)] + +use gba::{ + fatal, + io::{ + display::{DisplayControlSetting, DisplayMode, DISPCNT, VBLANK_SCANLINE, VCOUNT}, + keypad::read_key_input, + }, + vram::bitmap::Mode3, + Color, +}; +``` + +### Panic Handler + +Before we had a panic handler that just looped forever. Now that we're using the +`gba` crate we can rely on the debug output channel from `mGBA` to get a message +into the real world. There's macros setup for each message severity, and they +all accept a format string and arguments, like how `println` works. The catch is +that a given message is capped at a length of 255 bytes, and it should probably +be ASCII only. + +In the case of the `fatal` message level, it also halts the emulator. + +Of course, if the program is run on real hardware then the `fatal` message won't +stop the program, so we still need the infinite loop there too. + +(not that this program _can_ panic, but `rustc` doesn't know that so it demands +we have a `panic_handler`) + +```rust +#[panic_handler] +fn panic(info: &core::panic::PanicInfo) -> ! { + // This kills the emulation with a message if we're running within mGBA. + fatal!("{}", info); + // If we're _not_ running within mGBA then we still need to not return, so + // loop forever doing nothing. + loop {} +} +``` + +### Waiting Around + +Like I talked about before, sometimes we need to wait around a bit for the right +moment to start doing work. However, we don't know how to do the good version of +waiting for VBlank and VDraw to start, so we'll use the really bad version of it +for now. + +```rust +/// Performs a busy loop until VBlank starts. +/// +/// This is very inefficient, and please keep following the lessons until we +/// cover how interrupts work! +pub fn spin_until_vblank() { + while VCOUNT.read() < VBLANK_SCANLINE {} +} + +/// Performs a busy loop until VDraw starts. +/// +/// This is very inefficient, and please keep following the lessons until we +/// cover how interrupts work! +pub fn spin_until_vdraw() { + while VCOUNT.read() >= VBLANK_SCANLINE {} +} +``` + +### Setup in `main` + +In main we set the display control value we want and declare a few variables +we're going to use in our primary loop. + +```rust +#[start] +fn main(_argc: isize, _argv: *const *const u8) -> isize { + const SETTING: DisplayControlSetting = + DisplayControlSetting::new().with_mode(DisplayMode::Mode3).with_bg2(true); + DISPCNT.write(SETTING); + + let mut px = Mode3::WIDTH / 2; + let mut py = Mode3::HEIGHT / 2; + let mut color = Color::from_rgb(31, 0, 0); +``` + +### Stuff During VDraw + +When a frame starts we want to read the keys, then adjust as much of the game +state as we can without touching VRAM. + +Once we're ready, we do our spin loop until VBlank starts. + +In this case, we're going to adjust `px` and `py` depending on the arrow pad +input, and also we'll cycle around the color depending on L and R being pressed. + +```rust + loop { + // read our keys for this frame + let this_frame_keys = read_key_input(); + + // adjust game state and wait for vblank + px = px.wrapping_add(2 * this_frame_keys.x_tribool() as usize); + py = py.wrapping_add(2 * this_frame_keys.y_tribool() as usize); + if this_frame_keys.l() { + color = Color(color.0.rotate_left(5)); + } + if this_frame_keys.r() { + color = Color(color.0.rotate_right(5)); + } + + // now we wait + spin_until_vblank(); +``` + +### Stuff During VBlank + +When VBlank starts we want want to update video memory to display the new +frame's situation. + +In our case, we're going to paint a little square of the current color, but also +if you go off the map it resets the screen. + +At the end, we spin until VDraw starts so we can do the whole thing again. + +```rust + // draw the new game and wait until the next frame starts. + if px >= Mode3::WIDTH || py >= Mode3::HEIGHT { + // out of bounds, reset the screen and position. + Mode3::dma_clear_to(Color::from_rgb(0, 0, 0)); + px = Mode3::WIDTH / 2; + py = Mode3::HEIGHT / 2; + } else { + // draw the new part of the line + Mode3::write(px, py, color); + Mode3::write(px, py + 1, color); + Mode3::write(px + 1, py, color); + Mode3::write(px + 1, py + 1, color); + } + + // now we wait again + spin_until_vdraw(); + } +} +``` diff --git a/book/src/development-setup.md b/book/src/development-setup.md index f84c387..d191358 100644 --- a/book/src/development-setup.md +++ b/book/src/development-setup.md @@ -171,12 +171,14 @@ fn main(_argc: isize, _argv: *const *const u8) -> isize { } ``` -Throw that into your project skeleton, build the program, 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 `Lokathor` or `Ketsuban` on the [Rust Community +Throw that into your project skeleton, build the program, and give it a run in +an emulator. I suggest [mgba](https://mgba.io/2019/01/26/mgba-0.7.0/), it has +some developer tools we'll use later on. 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 +`Lokathor` or `Ketsuban` on the [Rust Community Discord](https://discordapp.com/invite/aVESxV8), until you're eventually able to get your three dots going. -Of course, I'm sure you want to know why those numbers are the numbers to use. -Well that's what the whole rest of the book is about! +Of course, I'm sure you want to know why those particular numbers are the +numbers to use. Well that's what the whole rest of the book is about! diff --git a/book/src/gba-asm.md b/book/src/gba-asm.md index 9558853..082aa4e 100644 --- a/book/src/gba-asm.md +++ b/book/src/gba-asm.md @@ -17,21 +17,32 @@ sometimes. Accordingly, you should know how assembly works on the GBA. Version](http://infocenter.arm.com/help/topic/com.arm.doc.ddi0210c/DDI0210B.pdf) of the documentation available, if you'd like that. +* In addition to the `ARM7TDMI` book, which is specific to the GBA's CPU, you'll + need to find a copy of the ARM Architecture Reference Manual if you want + general ARM knowledge. The ARM Infocenter has the + [ARMv5](http://infocenter.arm.com/help/index.jsp?topic=/com.arm.doc.ddi0100i/index.html) + version of said manual hosted on their site. Unfortunately, they don't seem to + host the `ARMv4T` version of the manual any more. + * The [GBATek: ARM CPU Overview](https://problemkaputt.de/gbatek.htm#armcpuoverview) also has quite a bit of info. Some of it is a duplication of what you'd find in the ARM - Infocenter reference manual. Some of it is specific to the GBA's chip. Some of - it is specific to the ARM chips within the DS and DSi. It's a bit of a jumbled - mess, and as with the rest of GBATEK, the explanations are in a "sparse" style - (to put it nicely), so I wouldn't take it as your only source. + Infocenter reference manuals. Some of it is information that's specific to the + GBA's layout and how the CPU interacts with other parts (such as how its + timings and the display adapter's timings line up). Some of it is specific to + the ARM chips _within the DS and DSi_, so be careful to make sure that you + don't wander into the wrong section. GBATEK is always a bit of a jumbled mess, + and the explanations are often "sparse" (to put it nicely), so I'd advise that + you also look at the official ARM manuals. * The [Compiler Explorer](https://rust.godbolt.org/z/ndCnk3) can be used to quickly look at assembly versions of your Rust code. That link there will load up an essentially blank `no_std` file with `opt-level=3` set and targeting `thumbv6m-none-eabi`. That's _not_ the same target as the GBA (it's two ISA - revisions later, ARMv6 instead of ARMv4), but it's the closest CPU target that - is bundled with rustc, so it's the closest you can get with the compiler - explorer website. If you're very dedicated I suppose you could setup a [local + revisions later, `ARMv6` instead of `ARMv4`), but it's the closest CPU target + that is bundled with `rustc`, so it's the closest you can get with the + compiler explorer website. If you're very dedicated I suppose you could setup + a [local instance](https://github.com/mattgodbolt/compiler-explorer#running-a-local-instance) of compiler explorer and then add the extra target definition and so on, but that's _probably_ overkill. diff --git a/book/src/io-registers.md b/book/src/io-registers.md new file mode 100644 index 0000000..de3aa7c --- /dev/null +++ b/book/src/io-registers.md @@ -0,0 +1,237 @@ +# IO Registers + +As I said before, the IO registers are how you tell the GBA to do all the things +you want it to do. If you want a hint at what's available, they're all listed +out in the [GBA I/O Map](https://problemkaputt.de/gbatek.htm#gbaiomap) section +of GBATEK. Go have a quick look. + +Each individual IO register has a particular address just like we talked about +in the Hardware Memory Map section. They also have a size (listed in bytes), and +a note on if they're read only, write only, or read-write. Finally, each +register has a name and a one line summary. Unfortunately for us, the names are +all C style names with heavy shorthand. I'm not normally a fan of shorthand +names, but the `gba` crate uses the register names from GBATEK as much as +possible, since they're the most commonly used set of names among GBA +programmers. That way, if you're reading other guides and they say to set the +`BG2CNT` register, then you know exactly what register to look for within the +`gba` docs. + +## Register Bits + +There's only about 100 registers, but there's a lot more than 100 details we +want to have control over on the GBA. How does that work? Well, let's use a +particular register to talk about it. The first one on the list is `DISPCNT`, +the "Display Control" register. It's one of the most important IO registers, so +this is a "two birds with one stone" situation. + +Naturally there's a whole lot of things involved in the LCD that we want to +control, and it's all "one" value, but that value is actually many "fields" +packed into one value. When learning about an IO register, you have to look at +its bit pattern breakdown. For `DISPCNT` the GBATEK entry looks like this: + +```txt +4000000h - DISPCNT - LCD Control (Read/Write) + Bit Expl. + 0-2 BG Mode (0-5=Video Mode 0-5, 6-7=Prohibited) + 3 Reserved / CGB Mode (0=GBA, 1=CGB; can be set only by BIOS opcodes) + 4 Display Frame Select (0-1=Frame 0-1) (for BG Modes 4,5 only) + 5 H-Blank Interval Free (1=Allow access to OAM during H-Blank) + 6 OBJ Character VRAM Mapping (0=Two dimensional, 1=One dimensional) + 7 Forced Blank (1=Allow FAST access to VRAM,Palette,OAM) + 8 Screen Display BG0 (0=Off, 1=On) + 9 Screen Display BG1 (0=Off, 1=On) + 10 Screen Display BG2 (0=Off, 1=On) + 11 Screen Display BG3 (0=Off, 1=On) + 12 Screen Display OBJ (0=Off, 1=On) + 13 Window 0 Display Flag (0=Off, 1=On) + 14 Window 1 Display Flag (0=Off, 1=On) + 15 OBJ Window Display Flag (0=Off, 1=On) +``` + +So what we're supposed to understand here is that we've got a `u16`, and then we +set the individual bits for the things that we want. In the `hello_magic` +example you might recall that we set this register to the value `0x0403`. That +was a bit of a trick on my part because hex numbers usually look far more +mysterious than decimal or binary numbers. If we converted it to binary it'd +look like this: + +```rust +0b100_0000_0011 +``` + +And then you can just go down the list of settings to see what bits are what: + +* Bits 0-2 (BG Mode) are `0b011`, so that's Video Mode 3 +* Bit 10 (Display BG2) is enabled +* Everything else is disabled + +Naturally, trying to remember exactly what bit does what can be difficult. In +the `gba` crate we attempt as much as possible to make types that wrap over a +`u16` or `u32` and then have getters and setters _as if_ all the inner bits were +different fields. + +* If it's a single bit then the getter/setter will use `bool`. +* If it's more than one bit and each pattern has some non-numeric meaning then + it'll use an `enum`. +* If it's more than one bit and numeric in nature then it'll just use the + wrapped integer type. Note that you generally won't get the full range of the + inner number type, and any excess gets truncated down to fit in the bits + available. + +All the getters and setters are defined as `const` functions, so you can make +constant declarations for the exact setting combinations that you want. + +## Some Important IO Registers + +It's not easy to automatically see what registers will be important for getting +started and what registers can be saved to learn about later. + +We'll go over three IO registers here that will help us the most to get started, +then next lesson we'll cover how that Video Mode 3 bitmap drawing works, and +then by the end of the next lesson we'll be able to put it all together into +something interactive. + +### DISPCNT: Display Control + +The [DISPCNT](https://problemkaputt.de/gbatek.htm#lcdiodisplaycontrol) register +lets us affect the major details of our video output. There's a lot of other +registers involved too, but it all starts here. + +```rust +pub const DISPCNT: VolAddress = unsafe { VolAddress::new(0x400_0000) }; +``` + +As you can see, the display control register is, like most registers, +complicated enough that we make it a dedicated type with getters and setters for +the "phantom" fields. In this case it's mostly a bunch of `bool` values we can +set, and also the video mode is an `enum`. + +We already looked at the bit listing above, let's go over what's important right +now and skip the other bits: + +* BG Mode sets how the whole screen is going to work and even how the display + adapter is going to interpret the bit layout of video memory for pixel + processing. We'll start with Mode 3, which is the simplest to learn. +* The "Forced Blank" bit is one of the very few bits that starts _on_ at the + start of the main program. When it's enabled it prevents the display adapter + from displaying anything at all. You use this bit when you need to do a very + long change to video memory and you don't want the user to see the + intermediate states being partly drawn. +* The "Screen Display" bits let us enable different display layers. We care + about BG2 right now because the bitmap modes (3, 4, and 5) are all treated as + if they were drawing into BG2 (even though it's the only BG layer available in + those modes). + +There's a bunch of other stuff, but we'll get to those things later. They're not +relevent right now, and there's enough to learn already. Already we can see that +when the `hello_magic` demo says + +```rust + (0x400_0000 as *mut u16).write_volatile(0x0403); +``` + +We could re-write that more sensibly like this + +```rust + const SETTING: DisplayControlSetting = + DisplayControlSetting::new().with_mode(DisplayMode::Mode3).with_bg2(true); + DISPCNT.write(SETTING); +``` + +### VCOUNT: Vertical Display Counter + +The [VCOUNT](https://problemkaputt.de/gbatek.htm#lcdiointerruptsandstatus) +register lets us find out what row of pixels (called a **scanline**) is +currently being processed. + +```rust +pub const VCOUNT: ROVolAddress = unsafe { ROVolAddress::new(0x400_0006) }; +``` + +You see, the display adapter is constantly running its own loop, along side the +CPU. It starts at the very first pixel of the very first scanline, takes 4 +cycles to determine what color that pixel is, and then processes the next +pixel. Each scanline is 240 pixels long, followed by 68 "virtual" pixels so that +you have just a moment to setup for the next scanline to be drawn if you need +it. 272 cycles (68*4) is not a lot of time, but it's enough that you could +change some palette colors or move some objects around if you need to. + +* Horizontal pixel value `0..240`: "HDraw" +* Horizontal pixel value `240..308`: "HBlank" + +There's no way to check the current horizontal counter, but there is a way to +have the CPU interrupt the normal code when the HBlank period starts, which +we'll learn about later. + +Once a complete scanline has been processed (including the blank period), the +display adapter keeps going with the next scanline. Similar to how the +horizontal processing works, there's 160 scanlines in the real display, and then +it's followed by 68 "virtual" scanlines to give you time for adjusting video +memory between the frames of the game. + +* Vertical Count `0..160`: "VDraw" +* Vertical Count `160..228`: "VBlank" + +Once every scanline has been processed (including the vblank period), the +display adapter starts the whole loop over again with scanline 0. A total of +280,896 cycles per display loop (4 * 308 * 228), and about 59.59ns per CPU +cycle, gives us a full speed display rate of 59.73fps. That's close enough to +60fps that I think we can just round up a bit whenever we're not counting it +down to the exact cycle timings. + +However, there's a bit of a snag. If we change video memory during the middle of +a scanline the display will _immediately_ start processing using the new state +of video memory. The picture before the change and after the change won't look +like a single, clean picture. Instead you'll get what's called "[screen +tearing](https://en.wikipedia.org/wiki/Screen_tearing)", which is usually +considered to be the mark of a badly programmed game. + +To avoid this we just need to only adjust video memory during one of the blank +periods. If you're really cool you can adjust things during HBlank, but we're +not that cool yet. Starting out our general program flow will be: + +1) Gather input for the frame (next part of this lesson) and update the game + state, getting everything ready for when VBlank actually starts. +2) Once VBlank starts we update all of the video memory as fast as we can. +3) Once we're done drawing we again wait for the VDraw period to begin and then + do it all again. + +Now, it's not the most efficient way, but to get our timings right we can just +read from `VCOUNT` over and over in a "busy loop". Once we read a value of 160 +we know that we've entered VBlank. Once it goes back to 0 we know that we're +back in VDraw. + +Doing a busy loop like this actually drains the batteries way more than +necessary. It keeps the CPU active constantly, which is what uses a fair amount +of the power. Normally you're supposed to put the CPU to sleep if you're just +waiting around for something to happen. However, that also requires learning +about some more concepts to get right. So to keep things easier starting out +we'll do the bad/lazy version and then upgrade our technique later. + +### KEYINPUT: Key Input Reading + +The [KEYINPUT](https://problemkaputt.de/gbatek.htm#gbakeypadinput) register is +the last one we've got to learn about this lesson. It lets you check the status +of all 10 buttons on the GBA. + +```rust +pub const KEYINPUT: ROVolAddress = unsafe { ROVolAddress::new(0x400_0130) }; +``` + +There's little to say here. It's a read only register, and the data just +contains one bit per button. The only thing that's a little weird about it is +that the bits follow a "low active" convention, so if the button is pressed then +the bit is 0, and if the button is released the bit is 1. + +You _could_ work with that directly, but I think it's a lot easier to think +about having `true` for pressed and `false` for not pressed. So the `gba` crate +flips the bits when you read the keys: + +```rust +/// Gets the current state of the keys +pub fn read_key_input() -> KeyInput { + KeyInput(KEYINPUT.read() ^ 0b0000_0011_1111_1111) +} +``` + +Now we can treat the KeyInput values like a totally normal bitset. diff --git a/book/src/the-hardware-memory-map.md b/book/src/the-hardware-memory-map.md new file mode 100644 index 0000000..5ef79a5 --- /dev/null +++ b/book/src/the-hardware-memory-map.md @@ -0,0 +1,379 @@ +# The Hardware Memory Map + +So we saw `hello_magic.rs` and then we learned what `volatile` was all about, +but we've still got a few things that are a bit mysterious. You can't just cast +a number into a pointer and start writing to it! That's totally crazy! That's +writing to un-allocated memory! Against the rules! + +Well, _kinda_. It's true that you're not allowed to write _anywhere at all_, but +those locations were carefully selected locations. + +You see, on a modern computer if you need to check if a key is pressed you ask +the Operating System (OS) to please go check for you. If you need to play a +sound, you ask the OS to please play the sound on a default sound output. If you +need to show a picture you ask the OS to give you access to the video driver so +that you can ask the video driver to please put some pixels on the screen. +That's mostly fine, except how does the OS actually do it? It doesn't have an OS +to go ask, it has to stop somewhere. + +Ultimately, every piece of hardware is mapped into somewhere in the address +space of the CPU. You can't actually tell that this is the case as a normal user +because your program runs inside a virtualized address space. That way you can't +go writing into another program's memory and crash what they're doing or steal +their data (well, hopefully, it's obviously not perfect). Outside of the +virtualization layer the OS is running directly in the "true" address space, and +it can access the hardware on behalf of a program whenever it's asked to. + +How does directly accessing the hardware work, _precisely_? It's just the same +as accessing the RAM. Each address holds some bits, and the CPU picks an address +and loads in the bits. Then the program gets the bits and has to decide what +they mean. The "driver" of a hardware device is just the layer that translates +between raw bits in the outside world and more meaningful values inside of the +program. + +Of course, memory mapped hardware can change its bits at any time. The user can +press and release a key and you can't stop them. This is where `volatile` comes +in. Whenever there's memory mapped hardware you want to access it with +`volatile` operations so that you can be sure that you're sending the data every +time, and that you're getting fresh data every time. + +## GBA Specifics + +That's enough about the general concept of memory mapped hardware, let's get to +some GBA specifics. The GBA has the following sections in its memory map. + +* BIOS +* External Work RAM (EWRAM) +* Internal Work RAM (IWRAM) +* IO Registers +* Palette RAM (PALRAM) +* Video RAM (VRAM) +* Object Attribute Memory (OAM) +* Game Pak ROM (ROM) +* Save RAM (SRAM) + +Each of these has a few key points of interest: + +* **Bus Width:** Also just called "bus", this is how many little wires are + _physically_ connecting a part of the address space to the CPU. If you need to + transfer more data than fits in the bus you have to do repeated transfers + until it all gets through. +* **Read/Write Modes:** Most parts of the address space can be read from in 8, + 16, or 32 bits at a time (there's a few exceptions we'll see). However, a + significant portion of the address space can't accept 8 bit writes. Usually + this isn't a big deal, but standard `memcopy` routine switches to doing a + byte-by-byte copy in some situations, so we'll have to be careful about using + it in combination with those regions of the memory. +* **Access Speed:** On top of the bus width issue, not all memory can be + accessed at the same speed. The "fast" parts of memory can do a read or write + in 1 cycle, but the slower parts of memory can take a few cycles per access. + These are called "wait cycles". The exact timings depend on what you configure + the system to use, which is also limited by what your cartridge physically + supports. You'll often see timings broken down into `N` cycles (non-sequential + memory access) and `S` cycles (sequential memory access, often faster). There + are also `I` cycles (internal cycles) which happen whenever the CPU does an + internal operation that's more than one cycle to complete (like a multiply). + Don't worry, you don't have to count exact cycle timings unless you're on the + razor's edge of the GBA's abilities. For more normal games you just have to be + mindful of what you're doing and it'll be fine. + +Let's briefly go over the major talking points of each memory region. All of +this information is also available in GBATEK, mostly in their [memory +map](http://www.akkit.org/info/gbatek.htm#gbamemorymap) section (though somewhat +spread through the rest of the document too). + +Though I'm going to list the location range of each memory space below, most of +the hardware locations are actually mirrored at several points throughout the +address space. + +### BIOS + +* **Location:** `0x0` to `0x3FFF` +* **Bus:** 32-bit +* **Access:** Memory protected read-only (see text). +* **Wait Cycles:** None + +The "basic input output system". This contains a grab bag of utilities that do +various tasks. The code is optimized for small size rather than great speed, so +you can sometimes write faster versions of these routines. Also, calling a bios +function has more overhead than a normal function call. You can think of bios +calls as being similar to system calls to the OS on a desktop computer. Useful, +but costly. + +As a side note, not only is BIOS memory read only, but it's memory protected so +that you can't even read from bios memory unless the system is currently +executing a function that's in bios memory. If you try then the system just +gives back a nonsensical value that's not really what you asked for. If you +really want to know what's inside, there's actually a bug in one bios call +(`MidiKey2Freq`) that lets you read the bios section one byte at a time. + +Also, there's not just one bios! Of course there's the official bios from +Nintendo that's used on actual hardware, but since that's code instead of +hardware it's protected by copyright. Since a bios is needed to run a GBA +emulator properly, people have come up with their own open source versions or +they simply make the emulator special case the bios and act _as if_ the function +call had done the right thing. + +* The [TempGBA](https://github.com/Nebuleon/TempGBA) repository has an easy to + look at version written in assembly. It's API and effects are close enough to + the Nintendo version that most games will run just fine. +* You can also check out the [mGBA + bios](https://github.com/mgba-emu/mgba/blob/master/src/gba/bios.c) if you want + to see the C version of what various bios functions are doing. + +### External Work RAM (EWRAM) + +* **Location:** `0x200_0000` to `0x203_FFFF` (256k) +* **Bus:** 16-bit +* **Access:** Read-write, any size. +* **Wait Cycles:** 2 + +The external work ram is a sizable amount of space, but the 2 wait cycles per +access and 16-bit bus mean that you should probably think of it as being a +"heap" to avoid putting things in if you don't have to. + +The GBA itself doesn't use this for anything, so any use is totally up to you. + +At the moment, the linker script and `crt0.s` files provided with the `gba` +crate also have no defined use for the EWRAM, so it's 100% on you to decide how +you wanna use them. + +(Note: There is an undocumented control register that lets you adjust the wait +cycles on EWRAM. Using it, you can turn EWRAM from the default 2 wait cycles +down to 1. However, not all GBA-like things support it. The GBA and GBA SP do, +the GBA Micro and DS do not. Emulators might or might not depending on the +particular emulator. See the [GBATEK system +control](https://problemkaputt.de/gbatek.htm#gbasystemcontrol) page for a full +description of that register, though probably only once you've read more of this +tutorial book and know how to make sense of IO registers and such.) + +### Internal Work RAM (IWRAM) + +* **Location:** `0x300_0000` to `0x300_7FFF` (32k) +* **Bus:** 32-bit +* **Access:** Read-write, any size. +* **Wait Cycles:** 0 + +This is where the "fast" memory for general purposes lives. By default the +system uses the 256 _bytes_ starting at `0x300_7F00` _and up_ for system and +interrupt purposes, while Rust's program stack starts at that same address _and +goes down_ from there. + +Even though your stack exists in this space, it's totally reasonable to use the +bottom parts of this memory space for whatever quick scratch purposes, same as +EWRAM. 32k is fairly huge, and the stack going down from the top and the scratch +data going up from the bottom are unlikely to hit each other. If they do you +were probably well on your way to a stack overflow anyway. + +The linker script and `crt0.s` file provided with the `gba` crate use the bottom +of IWRAM to store the `.data` and `.bss` [data +segments](https://en.wikipedia.org/wiki/Data_segment). That's where your global +variables get placed (both `static` and `static mut`). The `.data` segment holds +any variable that's initialized to non-zero, and the `.bss` section is for any +variable initialized to zero. When the GBA is powered on, some code in the +`crt0.s` file runs and copies the initial `.data` values into place within IWRAM +(all of `.bss` starts at 0, so there's no copy for those variables). + +If you have no global variables at all, then you don't need to worry about those +details, but if you do have some global variables then you can use the _address +of_ the `__bss_end` symbol defined in the top of the `gba` crate as a marker for +where it's safe for you to start using IWRAM without overwriting your globals. + +### IO Registers + +* **Location:** `0x400_0000` to `0x400_03FE` +* **Bus:** 32-bit +* **Access:** different for each IO register +* **Wait Cycles:** 0 + +The IO Registers are where most of the magic happens, and it's where most of the +variety happens too. Each IO register is a specific width, usually 16-bit but +sometimes 32-bit. Most of them are fully read/write, but some of them are read +only or write only. Some of them have individual bits that are read only even +when the rest of the register is writable. Some of them can be written to, but +the write doesn't change the value you read back, it sets something else. +Really. + +The IO registers are how you control every bit of hardware besides the CPU +itself. Reading the buttons, setting display modes, enabling timers, all of that +goes through different IO registers. Actually, even a few parts of the CPU's +operation can be controlled via IO register. + +We'll go over IO registers more in the next section, including a few specific +registers, and then we'll constantly encounter more IO registers as we explore +each new topic through the rest of the book. + +### Palette RAM (PALRAM) + +* **Location:** `0x500_0000` to `0x500_03FF` (1k) +* **Bus:** 16-bit +* **Access:** Read any, single bytes mirrored (see text). +* **Wait Cycles:** Video Memory Wait (see text) + +This is where the GBA stores color palette data. There's 256 slots for +Background color, and then 256 slots for Object color. + +GBA colors are 15 bits each, with five bits per channel and the highest bit +being totally ignored, so we store them as `u16` values: + +* `X_BBBBB_GGGGG_RRRRR` + +Of note is the fact that the 256 palette slots can be viewed in two different +ways. There's two different formats for images in video memory: "8 bit per +pixel" (8bpp) and "4 bit per pixel mode" (4bpp). + +* **8bpp:** Each pixel in the image is 8 bits and indexes directly into the full + 256 entry palette array. An index of 0 means that pixel should be transparent, + so there's 255 possible colors. +* **4bpp:** Each pixel in the image is 4 bits and indexes into a "palbank" of 16 + colors within the palette data. Some exterior control selects the palbank to + be used. An index of 0 still means that the pixel should be transparent, so + there's 15 possible colors. + +Different images can use different modes all at once, as long as you can fit all +the colors you want to use into your palette layout. + +PALRAM can't be written to in individual bytes. This isn't normally a problem at +all, because you wouldn't really want to write half of a color entry anyway. If +you do try to write a single byte then it gets "mirrored" into both halves of +the `u16` that would be associated with that address. For example, if you tried +to write `0x01u8` to either `0x500_0000` or `0x500_0001` then you'd actually +_effectively_ be writing `0x0101u16` to `0x500_0000`. + +PALRAM follows what we'll call the "Video Memory Wait" rule: If you to access +the memory during a vertical blank or horizontal blank period there's 0 wait +cycles, and if you try to access the memory while the display controller is +drawing there is a 1 cycle wait inserted _if_ the display controller was using +that memory at that moment. + +### Video RAM (VRAM) + +* **Location:** `0x600_0000` to `0x601_7FFF` (96k or 64k+32k depending on mode) +* **Bus:** 16-bit +* **Access:** Read any, single bytes _sometimes_ mirrored (see text). +* **Wait Cycles:** Video Memory Wait (see text) + +Video RAM is the memory for what you want the display controller to be +displaying. The GBA actually has 6 different display modes (numbered 0 through +5), and depending on the mode you're using the layout that you should imagine +VRAM having changes. Because there's so much involved here, I'll leave more +precise details to the following sections which talk about how to use VRAM in +each mode. + +VRAM can't be written to in individual bytes. If you try to write a single byte +to background VRAM the byte gets mirrored like with PALRAM, and if you try with +object VRAM the write gets ignored entirely. Exactly what address ranges those +memory types are depends on video mode, but just don't bother with individual +byte writes to VRAM. If you want to change a single byte of data (and you might) +then the correct style is to read the full `u16`, mask out the old data, mask in +your new value, and then write the whole `u16`. + +VRAM follows the same "Video Memory Wait" rule that PALRAM has. + +### Object Attribute Memory (OAM) + +* **Location:** `0x700_0000` to `0x700_03FF` (1k) +* **Bus:** 32-bit +* **Access:** Read any, single bytes no effect (see text). +* **Wait Cycles:** Video Memory Wait (see text) + +This part of memory controls the "Objects" (OBJ) on the screen. An object is +_similar to_ the concept of a "sprite". However, because of an object's size +limitations, a single sprite might require more than one object to be drawn +properly. In general, if you want to think in terms of sprites at all, you +should think of sprites as being a logical / programming concept, and objects as +being a hardware concept. + +While VRAM has the _image_ data for each object, this part of memory has the +_control_ data for each object. An objects "attributes" describe what part of +the VRAM to use, where to place is on the screen, any special graphical effects +to use, all that stuff. Each object has 6 bytes of attribute data (arranged as +three `u16` values), and there's a total of 128 objects (indexed 0 through 127). + +But 6 bytes each times 128 entries out of 1024 bytes leaves us with 256 bytes +left over. What's the other space used for? Well, it's a little weird, but after +every three `u16` object attribute fields there's one `i16` "affine parameter" +field mixed in. It takes four such fields to make a complete set of affine +parameters (a 2x2 matrix), so we get a total of 32 affine parameter entries +across all of OAM. "Affine" might sound fancy but it just means a transformation +where anything that started parallel stays parallel after the transform. The +affine parameters can be used to scale, rotate, and/or skew a background or +object as it's being displayed on the screen. It takes more computing power than +the non-affine display, so you can't display as many different things at once +when using the affine modes. + +OAM can't ever be written to with individual bytes. The write just has no effect +at all. + +OAM follows the same "Video Memory Wait" rule that PALRAM has, **and** you can +also only freely access OAM during a horizontal blank if you set a special +"HBlank Interval Free" bit in one of the IO registers (the "Display Control" +register, which we'll talk about next lesson). The reason that you might _not_ +want to set that bit is because when it's enabled you can't draw as many objects +at once. You don't lose the use of an exact number of objects, you actually lose +the use of a number of display adapter drawing cycles. Since not all objects +take the same number of cycles to render, it depends on what you're drawing. +GBATEK [has the details](https://problemkaputt.de/gbatek.htm#lcdobjoverview) if +you want to know precisely. + +### Game Pak ROM (ROM) + +* **Location:** Special (max of 32MB) +* **Bus:** 16-bit +* **Access:** Special +* **Wait Cycles:** Special + +This is where your actual game is located! As you might guess, since each +cartridge is different, the details here depend quite a bit on the cartridge +that you use for your game. Even a simple statement like "you can't write to the +ROM region" isn't true for some carts if they have FlashROM. + +The _most important_ thing to concern yourself with when considering the ROM +portion of memory is the 32MB limit. That's compiled code, images, sound, +everything put together. The total has to stay under 32MB. + +The next most important thing to consider is that 16-bit bus. It means that we +compile our programs using "Thumb state" code instead of "ARM state" code. +Details about this can be found in the GBA Assembly section of the book, but +just be aware that there's two different types of assembly on the GBA. You can +switch between them, but the default for us is always Thumb state. + +Another detail which you actually _don't_ have to think about much, but that you +might care if you're doing precise optimization, is that the ROM address space +is actually mirrored across three different locations: + +* `0x800_0000` to `0x9FF_FFFF`: Wait State 0 +* `0xA00_0000` to `0xBFF_FFFF`: Wait State 1 +* `0xC00_0000` to `0xDFF_FFFF`: Wait State 2 + +These _don't_ mean 0, 1, and 2 wait cycles, they mean the wait cycles associated +with ROM mirrors 0, 1, and 2. On some carts the game will store different parts +of the data into different chips that are wired to be accessible through +different parts of the mirroring. The actual wait cycles used are even +configurable via an IO register called the +[WAITCNT](https://problemkaputt.de/gbatek.htm#gbasystemcontrol) ("Wait Control", +I don't know why C programmers have to give everything the worst names it's not +1980 any more). + +### Save RAM (SRAM) + +* **Location:** Special (max of 64k) +* **Bus:** 8-bit +* **Access:** Special +* **Wait Cycles:** Special + +The Save RAM is also part of the cart that you've got your game on, so it also +depends on your hardware. + +SRAM _starts_ at `0xE00_0000` and you can save up to however much the hardware +supports, to a maximum of 64k. However, you can only read and write SRAM one +_byte_ at a time. What's worse, while you can _write_ to SRAM using code +executing anywhere, you can only _read_ with code that's executing out of either +Internal or External Work RAM, not from with code that's executing out of ROM. +This means that you need to copy the code for doing the read into some scratch +space (either at startup or on the fly, doesn't matter) and call that function +you've carefully placed. It's a bit annoying, but soon enough a routine for it +all will be provided in the `gba` crate and we won't have to worry too much +about it. + +(TODO: Provide the routine that I just claimed we would provide.) diff --git a/book/src/volatile.md b/book/src/volatile.md new file mode 100644 index 0000000..57c8257 --- /dev/null +++ b/book/src/volatile.md @@ -0,0 +1,48 @@ +# Volatile + +I know that you just got your first program running and you're probably excited +to learn more about GBA stuff, but first we have to cover a subject that's not +quite GBA specific. + +In the `hello_magic.rs` file we had these lines + +```rust + (0x600_0000 as *mut u16).offset(120 + 80 * 240).write_volatile(0x001F); + (0x600_0000 as *mut u16).offset(136 + 80 * 240).write_volatile(0x03E0); + (0x600_0000 as *mut u16).offset(120 + 96 * 240).write_volatile(0x7C00); +``` + +You've probably seen or heard of the +[write](https://doc.rust-lang.org/core/ptr/fn.write.html) function before, but +you'd be excused if you've never heard of its cousin function, +[write_volatile](https://doc.rust-lang.org/core/ptr/fn.write_volatile.html). + +What's the difference? Well, when the compiler sees normal reads and writes, it +assumes that those go into plain old memory locations. CPU registers, RAM, +wherever it is that the value's being placed. The compiler assumes that it's +safe to optimize away some of the reads and writes, or maybe issue the reads and +writes in a different order from what you wrote. Normally this is okay, and it's +exactly what we want the compiler to be doing, quietly making things faster for us. + +However, some of the time we access values from parts of memory where it's +important that each access happen, and in the exact order that we say. In our +`hello_magic.rs` example, we're writing directly into the video memory of the +display. The compiler sees that the rest of the Rust program never read out of +those locations, so it might think "oh, we can skip those writes, they're +pointless". It doesn't know that we're having a side effect besides just storing +some value at an address. + +By declaring a particular read or write to be `volatile` then we can force the +compiler to issue that access. Further, we're guaranteed that all `volatile` +access will happen in exactly the order it appears in the program relative to +other `volatile` access. However, non-volatile access can still be re-ordered +relative to a volatile access. In other words, for parts of the memory that are +volatile, we must _always_ use a volatile read or write for our program to +perform properly. + +For exactly this reason, we've got the [voladdress](https://docs.rs/voladdress/) +crate. It used to be part of the GBA crate, but it became big enough to break +out into a stand alone crate. It doesn't even do too much, it just makes it a +lot less error prone to accidentally forget to use volatile with our memory +mapped addresses. We just call `read` and `write` on any `VolAddress` that we +happen to see and the right thing will happen. diff --git a/bors.toml b/bors.toml new file mode 100644 index 0000000..359f894 --- /dev/null +++ b/bors.toml @@ -0,0 +1 @@ +status = ["continuous-integration/travis-ci/push"] diff --git a/crt0.s b/crt0.s index 9b06b47..53437f1 100644 --- a/crt0.s +++ b/crt0.s @@ -6,6 +6,11 @@ __start: .fill 188, 1, 0 .Linit: + @ Set address of user IRQ handler + ldr r0, =MainIrqHandler + ldr r1, =0x03FFFFFC + str r0, [r1] + @ set IRQ stack pointer mov r0, #0x12 msr CPSR_cf, r0 @@ -31,4 +36,55 @@ __start: @ jump to user code ldr r0, =main bx r0 + + .arm + .global MainIrqHandler + .align 4, 0 +MainIrqHandler: + @ Load base I/O register address + mov r2, #0x04000000 + add r2, r2, #0x200 + + @ Save IRQ stack pointer and IME + mrs r0, spsr + ldrh r1, [r2, #8] + stmdb sp!, {r0-r2,lr} + + @ Disable all interrupts by writing to IME + mov r0, #0 + strh r0, [r2, #8] + + @ Acknowledge all received interrupts that were enabled in IE + ldr r3, [r2, #0] + and r0, r3, r3, lsr #16 + strh r0, [r2, #2] + + @ Switch to system mode + mrs r2, cpsr + bic r2, r2, #0x1F + orr r2, r2, #0x1F + msr cpsr_cf, r2 + + @ Jump to user specified IRQ handler + ldr r2, =__IRQ_HANDLER + ldr r1, [r2] + stmdb sp!, {lr} + adr lr, MainIrqHandler_Return + bx r1 +MainIrqHandler_Return: + ldmia sp!, {lr} + + @ Switch to IRQ mode + mrs r2, cpsr + bic r2, r2, #0x1F + orr r2, r2, #0x92 + msr cpsr_cf, r2 + + @ Restore IRQ stack pointer and IME + ldmia sp!, {r0-r2,lr} + strh r1, [r2, #8] + msr spsr_cf, r0 + + @ Return to BIOS IRQ handler + bx lr .pool diff --git a/examples/bg_demo.rs b/examples-bak/bg_demo.rs similarity index 100% rename from examples/bg_demo.rs rename to examples-bak/bg_demo.rs diff --git a/examples/mgba_panic_handler.rs b/examples-bak/mgba_panic_handler.rs similarity index 100% rename from examples/mgba_panic_handler.rs rename to examples-bak/mgba_panic_handler.rs diff --git a/examples/hello_magic.rs b/examples/hello_magic.rs index 75eb6bc..8aa3c55 100644 --- a/examples/hello_magic.rs +++ b/examples/hello_magic.rs @@ -16,3 +16,8 @@ fn main(_argc: isize, _argv: *const *const u8) -> isize { loop {} } } + +#[no_mangle] +static __IRQ_HANDLER: extern "C" fn() = irq_handler; + +extern "C" fn irq_handler() {} diff --git a/examples/hello_world.rs b/examples/hello_world.rs index b126ee9..9f6fad4 100644 --- a/examples/hello_world.rs +++ b/examples/hello_world.rs @@ -3,22 +3,82 @@ #![forbid(unsafe_code)] use gba::{ - io::display::{DisplayControlSetting, DisplayMode, DISPCNT}, + fatal, + io::{ + display::{DisplayControlSetting, DisplayMode, DISPCNT, VBLANK_SCANLINE, VCOUNT}, + keypad::read_key_input, + }, vram::bitmap::Mode3, Color, }; #[panic_handler] -fn panic(_info: &core::panic::PanicInfo) -> ! { +fn panic(info: &core::panic::PanicInfo) -> ! { + // This kills the emulation with a message if we're running within mGBA. + fatal!("{}", info); + // If we're _not_ running within mGBA then we still need to not return, so + // loop forever doing nothing. loop {} } +/// Performs a busy loop until VBlank starts. +/// +/// This is very inefficient, and please keep following the lessons until we +/// cover how interrupts work! +pub fn spin_until_vblank() { + while VCOUNT.read() < VBLANK_SCANLINE {} +} + +/// Performs a busy loop until VDraw starts. +/// +/// This is very inefficient, and please keep following the lessons until we +/// cover how interrupts work! +pub fn spin_until_vdraw() { + while VCOUNT.read() >= VBLANK_SCANLINE {} +} + #[start] fn main(_argc: isize, _argv: *const *const u8) -> isize { - const SETTING: DisplayControlSetting = DisplayControlSetting::new().with_mode(DisplayMode::Mode3).with_bg2(true); + const SETTING: DisplayControlSetting = + DisplayControlSetting::new().with_mode(DisplayMode::Mode3).with_bg2(true); DISPCNT.write(SETTING); - Mode3::write_pixel(120, 80, Color::from_rgb(31, 0, 0)); - Mode3::write_pixel(136, 80, Color::from_rgb(0, 31, 0)); - Mode3::write_pixel(120, 96, Color::from_rgb(0, 0, 31)); - loop {} + + let mut px = Mode3::WIDTH / 2; + let mut py = Mode3::HEIGHT / 2; + let mut color = Color::from_rgb(31, 0, 0); + + loop { + // read our keys for this frame + let this_frame_keys = read_key_input(); + + // adjust game state and wait for vblank + px = px.wrapping_add(2 * this_frame_keys.x_tribool() as usize); + py = py.wrapping_add(2 * this_frame_keys.y_tribool() as usize); + if this_frame_keys.l() { + color = Color(color.0.rotate_left(5)); + } + if this_frame_keys.r() { + color = Color(color.0.rotate_right(5)); + } + + // now we wait + spin_until_vblank(); + + // draw the new game and wait until the next frame starts. + if px >= Mode3::WIDTH || py >= Mode3::HEIGHT { + // out of bounds, reset the screen and position. + Mode3::dma_clear_to(Color::from_rgb(0, 0, 0)); + px = Mode3::WIDTH / 2; + py = Mode3::HEIGHT / 2; + } else { + // draw the new part of the line + Mode3::write(px, py, color); + Mode3::write(px, py + 1, color); + Mode3::write(px + 1, py, color); + Mode3::write(px + 1, py + 1, color); + } + + // now we wait again + spin_until_vdraw(); + } } diff --git a/examples/irq.rs b/examples/irq.rs new file mode 100644 index 0000000..931ecc6 --- /dev/null +++ b/examples/irq.rs @@ -0,0 +1,147 @@ +#![no_std] +#![feature(start)] + +use gba::{ + io::{ + display::{DisplayControlSetting, DisplayMode, DisplayStatusSetting, DISPCNT, DISPSTAT}, + irq::{self, IrqEnableSetting, IrqFlags, BIOS_IF, IE, IME}, + keypad::read_key_input, + timers::{TimerControlSetting, TimerTickRate, TM0CNT_H, TM0CNT_L, TM1CNT_H, TM1CNT_L}, + }, + vram::bitmap::Mode3, + Color, +}; + +const BLACK: Color = Color::from_rgb(0, 0, 0); +const RED: Color = Color::from_rgb(31, 0, 0); +const GREEN: Color = Color::from_rgb(0, 31, 0); +const BLUE: Color = Color::from_rgb(0, 0, 31); +const YELLOW: Color = Color::from_rgb(31, 31, 0); +const PINK: Color = Color::from_rgb(31, 0, 31); + +#[panic_handler] +fn panic(_info: &core::panic::PanicInfo) -> ! { + loop {} +} + +fn start_timers() { + let init_val: u16 = u32::wrapping_sub(0x1_0000, 64) as u16; + const TIMER_SETTINGS: TimerControlSetting = + TimerControlSetting::new().with_overflow_irq(true).with_enabled(true); + + TM0CNT_L.write(init_val); + TM0CNT_H.write(TIMER_SETTINGS.with_tick_rate(TimerTickRate::CPU1024)); + TM1CNT_L.write(init_val); + TM1CNT_H.write(TIMER_SETTINGS.with_tick_rate(TimerTickRate::CPU64)); +} + +#[start] +fn main(_argc: isize, _argv: *const *const u8) -> isize { + DISPCNT.write(DisplayControlSetting::new().with_mode(DisplayMode::Mode3).with_bg2(true)); + Mode3::clear_to(BLACK); + + // Set the IRQ handler to use. + irq::set_irq_handler(irq_handler); + + // Enable all interrupts that are set in the IE register. + IME.write(IrqEnableSetting::UseIE); + + // Request that VBlank, HBlank and VCount will generate IRQs. + const DISPLAY_SETTINGS: DisplayStatusSetting = DisplayStatusSetting::new() + .with_vblank_irq_enable(true) + .with_hblank_irq_enable(true) + .with_vcounter_irq_enable(true); + DISPSTAT.write(DISPLAY_SETTINGS); + + // Start two timers with overflow IRQ generation. + start_timers(); + + loop { + let this_frame_keys = read_key_input(); + + // The VBlank IRQ must be enabled at minimum, or else the CPU will halt + // at the call to vblank_interrupt_wait() as the VBlank IRQ will never + // be triggered. + let mut flags = IrqFlags::new().with_vblank(true); + + // Enable interrupts based on key input. + if this_frame_keys.a() { + flags = flags.with_hblank(true); + } + if this_frame_keys.b() { + flags = flags.with_vcounter(true); + } + if this_frame_keys.l() { + flags = flags.with_timer0(true); + } + if this_frame_keys.r() { + flags = flags.with_timer1(true); + } + + IE.write(flags); + + // Puts the CPU into low power mode until a VBlank IRQ is received. This + // will yield considerably better power efficiency as opposed to spin + // waiting. + gba::bios::vblank_interrupt_wait(); + } +} + +static mut PIXEL: usize = 0; + +fn write_pixel(color: Color) { + unsafe { + Mode3::write(PIXEL, 0, color); + PIXEL = (PIXEL + 1) % (Mode3::WIDTH * Mode3::HEIGHT); + } +} + +extern "C" fn irq_handler(flags: IrqFlags) { + if flags.vblank() { + vblank_handler(); + } + if flags.hblank() { + hblank_handler(); + } + if flags.vcounter() { + vcounter_handler(); + } + if flags.timer0() { + timer0_handler(); + } + if flags.timer1() { + timer1_handler(); + } +} + +fn vblank_handler() { + write_pixel(BLUE); + + // When using `interrupt_wait()` or `vblank_interrupt_wait()`, IRQ handlers must acknowledge + // the IRQ on the BIOS Interrupt Flags register. + BIOS_IF.write(BIOS_IF.read().with_vblank(true)); +} + +fn hblank_handler() { + write_pixel(GREEN); + + BIOS_IF.write(BIOS_IF.read().with_hblank(true)); +} + +fn vcounter_handler() { + write_pixel(RED); + + BIOS_IF.write(BIOS_IF.read().with_vcounter(true)); +} + +fn timer0_handler() { + write_pixel(YELLOW); + + BIOS_IF.write(BIOS_IF.read().with_timer0(true)); +} + +fn timer1_handler() { + write_pixel(PINK); + + BIOS_IF.write(BIOS_IF.read().with_timer1(true)); +} diff --git a/examples/light_cycle.rs b/examples/light_cycle.rs deleted file mode 100644 index 3514998..0000000 --- a/examples/light_cycle.rs +++ /dev/null @@ -1,61 +0,0 @@ -#![no_std] -#![feature(start)] -#![forbid(unsafe_code)] - -use gba::{ - io::{ - display::{spin_until_vblank, spin_until_vdraw, DisplayControlSetting, DisplayMode, DISPCNT}, - keypad::read_key_input, - }, - vram::bitmap::Mode3, - Color, -}; - -#[panic_handler] -fn panic(_info: &core::panic::PanicInfo) -> ! { - loop {} -} - -#[start] -fn main(_argc: isize, _argv: *const *const u8) -> isize { - const SETTING: DisplayControlSetting = DisplayControlSetting::new().with_mode(DisplayMode::Mode3).with_bg2(true); - DISPCNT.write(SETTING); - - let mut px = Mode3::SCREEN_WIDTH / 2; - let mut py = Mode3::SCREEN_HEIGHT / 2; - let mut color = Color::from_rgb(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 = px.wrapping_add(2 * this_frame_keys.column_direction() as usize); - py = py.wrapping_add(2 * this_frame_keys.row_direction() as usize); - spin_until_vblank(); - - // draw the new game and wait until the next frame starts. - const BLACK: Color = Color::from_rgb(0, 0, 0); - if px >= Mode3::SCREEN_WIDTH || py >= Mode3::SCREEN_HEIGHT { - // out of bounds, reset the screen and position. - Mode3::clear_to(BLACK); - color = color.rotate_left(5); - px = Mode3::SCREEN_WIDTH / 2; - py = Mode3::SCREEN_HEIGHT / 2; - } else { - let color_here = Mode3::read_pixel(px, py); - if color_here != Some(BLACK) { - // crashed into our own line, reset the screen - Mode3::dma_clear_to(BLACK); - color = color.rotate_left(5); - } else { - // draw the new part of the line - Mode3::write_pixel(px, py, color); - Mode3::write_pixel(px, py + 1, color); - Mode3::write_pixel(px + 1, py, color); - Mode3::write_pixel(px + 1, py + 1, color); - } - } - spin_until_vdraw(); - } -} diff --git a/rustfmt.toml b/rustfmt.toml index 60a3f8d..b7f001b 100644 --- a/rustfmt.toml +++ b/rustfmt.toml @@ -1,8 +1,9 @@ +color = "Never" 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" +max_width = 100 +use_small_heuristics = "Max" diff --git a/src/base.rs b/src/base.rs index 5bbf7d2..ed07b9e 100644 --- a/src/base.rs +++ b/src/base.rs @@ -1,7 +1,7 @@ -//! Holds fundamental types/ops which the rest of the crate it built on. +//! Holds fundamental types/ops which the rest of the crate is built on. +//! +//! These things should probably, in time, be extracted out into their own +//! crate (or convert to using a robust existing crate). pub mod fixed_point; //pub(crate) use self::fixed_point::*; - -pub mod volatile; -pub(crate) use self::volatile::*; diff --git a/src/base/fixed_point.rs b/src/base/fixed_point.rs index cc1bed6..fbd9ef8 100644 --- a/src/base/fixed_point.rs +++ b/src/base/fixed_point.rs @@ -19,10 +19,7 @@ pub struct Fx { impl Fx { /// Uses the provided value directly. pub fn from_raw(r: T) -> Self { - Fx { - num: r, - phantom: PhantomData, - } + Fx { num: r, phantom: PhantomData } } /// Unwraps the inner value. @@ -32,60 +29,42 @@ impl Fx { /// Casts the base type, keeping the fractional bit quantity the same. pub fn cast_inner Z>(self, op: C) -> Fx { - Fx { - num: op(self.num), - phantom: PhantomData, - } + Fx { num: op(self.num), phantom: PhantomData } } } impl, F: Unsigned> Add for Fx { type Output = Self; fn add(self, rhs: Fx) -> Self::Output { - Fx { - num: self.num + rhs.num, - phantom: PhantomData, - } + Fx { num: self.num + rhs.num, phantom: PhantomData } } } impl, F: Unsigned> Sub for Fx { type Output = Self; fn sub(self, rhs: Fx) -> Self::Output { - Fx { - num: self.num - rhs.num, - phantom: PhantomData, - } + Fx { num: self.num - rhs.num, phantom: PhantomData } } } impl, F: Unsigned> Shl for Fx { type Output = Self; fn shl(self, rhs: u32) -> Self::Output { - Fx { - num: self.num << rhs, - phantom: PhantomData, - } + Fx { num: self.num << rhs, phantom: PhantomData } } } impl, F: Unsigned> Shr for Fx { type Output = Self; fn shr(self, rhs: u32) -> Self::Output { - Fx { - num: self.num >> rhs, - phantom: PhantomData, - } + Fx { num: self.num >> rhs, phantom: PhantomData } } } impl, F: Unsigned> Neg for Fx { type Output = Self; fn neg(self) -> Self::Output { - Fx { - num: -self.num, - phantom: PhantomData, - } + Fx { num: -self.num, phantom: PhantomData } } } @@ -94,18 +73,12 @@ macro_rules! fixed_point_methods { impl Fx<$t, F> { /// Gives the smallest positive non-zero value. pub fn precision() -> Self { - Fx { - num: 1, - phantom: PhantomData, - } + Fx { num: 1, phantom: PhantomData } } /// Makes a value with the integer part shifted into place. pub fn from_int_part(i: $t) -> Self { - Fx { - num: i << F::U8, - phantom: PhantomData, - } + Fx { num: i << F::U8, phantom: PhantomData } } /// Changes the fractional bit quantity, keeping the base type the same. @@ -140,21 +113,12 @@ macro_rules! fixed_point_signed_multiply { let pre_shift = (self.num as i32).wrapping_mul(rhs.num as i32); if pre_shift < 0 { if pre_shift == core::i32::MIN { - Fx { - num: core::$t::MIN, - phantom: PhantomData, - } + Fx { num: core::$t::MIN, phantom: PhantomData } } else { - Fx { - num: (-((-pre_shift) >> F::U8)) as $t, - phantom: PhantomData, - } + Fx { num: (-((-pre_shift) >> F::U8)) as $t, phantom: PhantomData } } } else { - Fx { - num: (pre_shift >> F::U8) as $t, - phantom: PhantomData, - } + Fx { num: (pre_shift >> F::U8) as $t, phantom: PhantomData } } } } @@ -192,10 +156,7 @@ macro_rules! fixed_point_signed_division { fn div(self, rhs: Fx<$t, F>) -> Self::Output { let mul_output: i32 = (self.num as i32).wrapping_mul(1 << F::U8); let divide_result: i32 = crate::bios::div(mul_output, rhs.num as i32); - Fx { - num: divide_result as $t, - phantom: PhantomData, - } + Fx { num: divide_result as $t, phantom: PhantomData } } } }; @@ -213,10 +174,7 @@ macro_rules! fixed_point_unsigned_division { fn div(self, rhs: Fx<$t, F>) -> Self::Output { let mul_output: i32 = (self.num as i32).wrapping_mul(1 << F::U8); let divide_result: i32 = crate::bios::div(mul_output, rhs.num as i32); - Fx { - num: divide_result as $t, - phantom: PhantomData, - } + Fx { num: divide_result as $t, phantom: PhantomData } } } }; diff --git a/src/base/volatile.rs b/src/base/volatile.rs deleted file mode 100644 index 5d44beb..0000000 --- a/src/base/volatile.rs +++ /dev/null @@ -1,298 +0,0 @@ -//! Holds types for correct handling of volatile memory. - -use core::{cmp::Ordering, iter::FusedIterator, marker::PhantomData, num::NonZeroUsize}; - -// TODO: striding block/iter - -/// Abstracts the use of a volatile hardware address. -/// -/// If you're trying to do anything other than abstract a volatile hardware -/// device then you _do not want to use this type_. Use one of the many other -/// smart pointer types. -/// -/// A volatile address doesn't store a value in the normal way: It maps to some -/// real hardware _other than_ RAM, and that hardware might have any sort of -/// strange rules. The specifics of reading and writing depend on the hardware -/// being mapped. For example, a particular address might be read only (ignoring -/// writes), write only (returning some arbitrary value if you read it), -/// "normal" read write (where you read back what you wrote), or some complex -/// read-write situation where writes have an effect but you _don't_ read back -/// what you wrote. -/// -/// As you imagine it can be very unsafe. The design of this type is set up so -/// that _creation_ is unsafe, and _use_ is safe. This gives an optimal -/// experience, since you'll use memory locations a lot more often than you try -/// to name them, on average. -/// -/// `VolAddress` is _not_ a thread safe type. If your device is multi-threaded -/// then you must arrange for synchronization in some other way. A `VolAddress` -/// _can_ be used to share data between an interrupt running on a core and a -/// thread running on that core as long as all access of that location is -/// volatile (if you're using the `asm!` macro add the "volatile" option, if -/// you're linking in ASM with the linker that's effectively volatile since the -/// compiler doesn't get a chance to mess with it). -/// -/// # Safety -/// -/// In order for values of this type to operate correctly they must follow quite -/// a few safety limits: -/// -/// * The declared address must be non-null (it uses the `NonNull` optimization -/// for better iteration results). This shouldn't be a big problem, since -/// hardware can't really live at the null address. -/// * The declared address must be aligned for the declared type of `T`. -/// * The declared address must _always_ read as something that's a valid bit -/// pattern for `T`. Don't pick any enums or things like that if your hardware -/// doesn't back it up. If there's _any_ doubt at all, you must instead read -/// or write an unsigned int of the correct bit size and then parse the bits -/// by hand. -/// * The declared address must be a part of the address space that Rust's -/// allocator and/or stack frames will never use. If you're not sure, please -/// re-read the hardware specs of your device and its memory map until you -/// know. -/// -/// The exact points of UB are if the address is ever 0, or if you ever `read` -/// or `write` with the invalid pointer. For example, if you offset to some -/// crazy (non-zero) value and then never use it that won't be an immediate -/// trigger of UB. -#[derive(Debug)] -#[repr(transparent)] -pub struct VolAddress { - address: NonZeroUsize, - marker: PhantomData<*mut T>, -} -// Note(Lokathor): We have to hand implement all these traits because if we use -// `derive` then they only get derived if the inner `T` has the trait. However, -// since we're acting like a pointer to `T`, the capability we offer isn't -// affected by whatever type `T` ends up being. -impl Clone for VolAddress { - fn clone(&self) -> Self { - *self - } -} -impl Copy for VolAddress {} -impl PartialEq for VolAddress { - fn eq(&self, other: &Self) -> bool { - self.address == other.address - } -} -impl Eq for VolAddress {} -impl PartialOrd for VolAddress { - fn partial_cmp(&self, other: &Self) -> Option { - Some(self.address.cmp(&other.address)) - } -} -impl Ord for VolAddress { - fn cmp(&self, other: &Self) -> Ordering { - self.address.cmp(&other.address) - } -} - -impl VolAddress { - /// Constructs a new address. - /// - /// # Safety - /// - /// You must follow the standard safety rules as outlined in the type docs. - pub const unsafe fn new_unchecked(address: usize) -> Self { - VolAddress { - address: NonZeroUsize::new_unchecked(address), - marker: PhantomData, - } - } - - /// Casts the type of `T` into type `Z`. - /// - /// # Safety - /// - /// You must follow the standard safety rules as outlined in the type docs. - pub const unsafe fn cast(self) -> VolAddress { - VolAddress { - address: self.address, - marker: PhantomData, - } - } - - /// Offsets the address by `offset` slots (like `pointer::wrapping_offset`). - /// - /// # Safety - /// - /// You must follow the standard safety rules as outlined in the type docs. - pub const unsafe fn offset(self, offset: isize) -> Self { - VolAddress { - address: NonZeroUsize::new_unchecked(self.address.get().wrapping_add(offset as usize * core::mem::size_of::())), - marker: PhantomData, - } - } - - /// Checks that the current target type of this address is aligned at this - /// address value. - /// - /// Technically it's a safety violation to even make a `VolAddress` that isn't - /// aligned. However, I know you're gonna try doing the bad thing, and it's - /// better to give you a chance to call `is_aligned` and potentially back off - /// from the operation or throw a `debug_assert!` or something instead of - /// triggering UB. Eventually this will be `const fn`, which will potentially - /// let you spot errors without even having to run your program. - pub const fn is_aligned(self) -> bool { - self.address.get() % core::mem::align_of::() == 0 - } - - /// Makes an iterator starting here across the given number of slots. - /// - /// # Safety - /// - /// The normal safety rules must be correct for each address iterated over. - pub const unsafe fn iter_slots(self, slots: usize) -> VolAddressIter { - VolAddressIter { vol_address: self, slots } - } - - // non-const and never can be. - - /// Reads a `Copy` value out of the address. - /// - /// The `Copy` bound is actually supposed to be `!Drop`, but rust doesn't - /// allow negative trait bounds. If your type isn't `Copy` you can use the - /// `read_non_copy` fallback to do an unsafe read. - /// - /// That said, I don't think that you legitimately have hardware that maps to - /// a Rust type with a `Drop` impl. If you do please tell me, I'm interested - /// to hear about it. - pub fn read(self) -> T - where - T: Copy, - { - unsafe { (self.address.get() as *mut T).read_volatile() } - } - - /// Reads a value out of the address with no trait bound. - /// - /// # Safety - /// - /// This is _not_ a move, it forms a bit duplicate of the current address - /// value. If `T` has a `Drop` trait that does anything it is up to you to - /// ensure that repeated drops do not cause UB (such as a double free). - pub unsafe fn read_non_copy(self) -> T { - (self.address.get() as *mut T).read_volatile() - } - - /// Writes a value to the address. - /// - /// Semantically, the value is moved into the `VolAddress` and then forgotten, - /// so if `T` has a `Drop` impl then that will never get executed. This is - /// "safe" under Rust's safety rules, but could cause something unintended - /// (eg: a memory leak). - pub fn write(self, val: T) { - unsafe { (self.address.get() as *mut T).write_volatile(val) } - } -} - -/// An iterator that produces a series of `VolAddress` values. -#[derive(Debug)] -pub struct VolAddressIter { - vol_address: VolAddress, - slots: usize, -} -impl Clone for VolAddressIter { - fn clone(&self) -> Self { - VolAddressIter { - vol_address: self.vol_address, - slots: self.slots, - } - } -} -impl PartialEq for VolAddressIter { - fn eq(&self, other: &Self) -> bool { - self.vol_address == other.vol_address && self.slots == other.slots - } -} -impl Eq for VolAddressIter {} -impl Iterator for VolAddressIter { - type Item = VolAddress; - - fn next(&mut self) -> Option { - if self.slots > 0 { - let out = self.vol_address; - unsafe { - self.slots -= 1; - self.vol_address = self.vol_address.offset(1); - } - Some(out) - } else { - None - } - } -} -impl FusedIterator for VolAddressIter {} - -/// This type is like `VolAddress`, but for when you have a block of values all -/// in a row. -/// -/// This is similar to the idea of an array or a slice, but called a "block" -/// because you could _also_ construct a `[VolAddress]`, and we want to avoid -/// any accidental confusion. -#[derive(Debug)] -pub struct VolAddressBlock { - vol_address: VolAddress, - slots: usize, -} -impl Clone for VolAddressBlock { - fn clone(&self) -> Self { - VolAddressBlock { - vol_address: self.vol_address, - slots: self.slots, - } - } -} -impl PartialEq for VolAddressBlock { - fn eq(&self, other: &Self) -> bool { - self.vol_address == other.vol_address && self.slots == other.slots - } -} -impl Eq for VolAddressBlock {} - -impl VolAddressBlock { - /// Constructs a new `VolAddressBlock`. - /// - /// # Safety - /// - /// The given `VolAddress` must be valid when offset by each of `0 .. slots` - pub const unsafe fn new_unchecked(vol_address: VolAddress, slots: usize) -> Self { - VolAddressBlock { vol_address, slots } - } - - /// Gives an iterator over this block's slots. - pub const fn iter(self) -> VolAddressIter { - VolAddressIter { - vol_address: self.vol_address, - slots: self.slots, - } - } - - /// Unchecked indexing into the block. - /// - /// # Safety - /// - /// The slot given must be in bounds. - pub const unsafe fn index_unchecked(self, slot: usize) -> VolAddress { - self.vol_address.offset(slot as isize) - } - - /// Checked "indexing" style access of the block, giving either a `VolAddress` or a panic. - pub fn index(self, slot: usize) -> VolAddress { - if slot < self.slots { - unsafe { self.vol_address.offset(slot as isize) } - } else { - panic!("Index Requested: {} >= Slot Count: {}", slot, self.slots) - } - } - - /// Checked "getting" style access of the block, giving an Option value. - pub fn get(self, slot: usize) -> Option> { - if slot < self.slots { - unsafe { Some(self.vol_address.offset(slot as isize)) } - } else { - None - } - } -} diff --git a/src/bios.rs b/src/bios.rs index 111aa99..03f6c8e 100644 --- a/src/bios.rs +++ b/src/bios.rs @@ -11,6 +11,7 @@ #![cfg_attr(not(all(target_vendor = "nintendo", target_env = "agb")), allow(unused_variables))] use super::*; +use io::irq::IrqFlags; //TODO: ALL functions in this module should have `if cfg!(test)` blocks. The //functions that never return must panic, the functions that return nothing @@ -184,16 +185,16 @@ pub fn stop() { /// * The first argument controls if you want to ignore all current flags and /// wait until a new flag is set. /// * The second argument is what flags you're waiting on (same format as the -/// IE/IF registers). +/// [`IE`](io::irq::IE)/[`IF`](io::irq::IF) registers). /// /// If you're trying to handle more than one interrupt at once this has less /// overhead than calling `halt` over and over. /// /// When using this routing your interrupt handler MUST update the BIOS -/// Interrupt Flags `0x300_7FF8` in addition to the usual interrupt -/// acknowledgement. +/// Interrupt Flags at [`BIOS_IF`](io::irq::BIOS_IF) in addition to +/// the usual interrupt acknowledgement. #[inline(always)] -pub fn interrupt_wait(ignore_current_flags: bool, target_flags: u16) { +pub fn interrupt_wait(ignore_current_flags: bool, target_flags: IrqFlags) { #[cfg(not(all(target_vendor = "nintendo", target_env = "agb")))] { unimplemented!() @@ -203,19 +204,19 @@ pub fn interrupt_wait(ignore_current_flags: bool, target_flags: u16) { unsafe { asm!(/* ASM */ "swi 0x04" :/* OUT */ // none - :/* INP */ "{r0}"(ignore_current_flags), "{r1}"(target_flags) + :/* INP */ "{r0}"(ignore_current_flags), "{r1}"(target_flags.0) :/* CLO */ // none :/* OPT */ "volatile" ); } } } -//TODO(lokathor): newtype this flag business. /// (`swi 0x05`) "VBlankIntrWait", VBlank Interrupt Wait. /// -/// This is as per `interrupt_wait(true, 1)` (aka "wait for a new vblank"). You -/// must follow the same guidelines that `interrupt_wait` outlines. +/// This is as per `interrupt_wait(true, IrqFlags::new().with_vblank(true))` +/// (aka "wait for a new vblank"). You must follow the same guidelines that +/// [`interrupt_wait`](interrupt_wait) outlines. #[inline(always)] pub fn vblank_interrupt_wait() { #[cfg(not(all(target_vendor = "nintendo", target_env = "agb")))] @@ -225,7 +226,7 @@ pub fn vblank_interrupt_wait() { #[cfg(all(target_vendor = "nintendo", target_env = "agb"))] { unsafe { - asm!(/* ASM */ "swi 0x04" + asm!(/* ASM */ "swi 0x05" :/* OUT */ // none :/* INP */ // none :/* CLO */ "r0", "r1" // both set to 1 by the routine diff --git a/src/io.rs b/src/io.rs index 076813b..23e4d47 100644 --- a/src/io.rs +++ b/src/io.rs @@ -12,6 +12,7 @@ pub mod background; pub mod color_blend; pub mod display; pub mod dma; +pub mod irq; pub mod keypad; pub mod sound; pub mod timers; diff --git a/src/io/background.rs b/src/io/background.rs index 09748ea..6b34f2a 100644 --- a/src/io/background.rs +++ b/src/io/background.rs @@ -3,13 +3,13 @@ use super::*; /// BG0 Control. Read/Write. Display Mode 0/1 only. -pub const BG0CNT: VolAddress = unsafe { VolAddress::new_unchecked(0x400_0008) }; +pub const BG0CNT: VolAddress = unsafe { VolAddress::new(0x400_0008) }; /// BG1 Control. Read/Write. Display Mode 0/1 only. -pub const BG1CNT: VolAddress = unsafe { VolAddress::new_unchecked(0x400_000A) }; +pub const BG1CNT: VolAddress = unsafe { VolAddress::new(0x400_000A) }; /// BG2 Control. Read/Write. Display Mode 0/1/2 only. -pub const BG2CNT: VolAddress = unsafe { VolAddress::new_unchecked(0x400_000C) }; +pub const BG2CNT: VolAddress = unsafe { VolAddress::new(0x400_000C) }; /// BG3 Control. Read/Write. Display Mode 0/2 only. -pub const BG3CNT: VolAddress = unsafe { VolAddress::new_unchecked(0x400_000E) }; +pub const BG3CNT: VolAddress = unsafe { VolAddress::new(0x400_000E) }; newtype! { /// Allows configuration of a background layer. @@ -66,24 +66,24 @@ pub enum BGSize { } /// BG0 X-Offset. Write only. Text mode only. 9 bits. -pub const BG0HOFS: VolAddress = unsafe { VolAddress::new_unchecked(0x400_0010) }; +pub const BG0HOFS: VolAddress = unsafe { VolAddress::new(0x400_0010) }; /// BG0 Y-Offset. Write only. Text mode only. 9 bits. -pub const BG0VOFS: VolAddress = unsafe { VolAddress::new_unchecked(0x400_0012) }; +pub const BG0VOFS: VolAddress = unsafe { VolAddress::new(0x400_0012) }; /// BG1 X-Offset. Write only. Text mode only. 9 bits. -pub const BG1HOFS: VolAddress = unsafe { VolAddress::new_unchecked(0x400_0014) }; +pub const BG1HOFS: VolAddress = unsafe { VolAddress::new(0x400_0014) }; /// BG1 Y-Offset. Write only. Text mode only. 9 bits. -pub const BG1VOFS: VolAddress = unsafe { VolAddress::new_unchecked(0x400_0016) }; +pub const BG1VOFS: VolAddress = unsafe { VolAddress::new(0x400_0016) }; /// BG2 X-Offset. Write only. Text mode only. 9 bits. -pub const BG2HOFS: VolAddress = unsafe { VolAddress::new_unchecked(0x400_0018) }; +pub const BG2HOFS: VolAddress = unsafe { VolAddress::new(0x400_0018) }; /// BG2 Y-Offset. Write only. Text mode only. 9 bits. -pub const BG2VOFS: VolAddress = unsafe { VolAddress::new_unchecked(0x400_001A) }; +pub const BG2VOFS: VolAddress = unsafe { VolAddress::new(0x400_001A) }; /// BG3 X-Offset. Write only. Text mode only. 9 bits. -pub const BG3HOFS: VolAddress = unsafe { VolAddress::new_unchecked(0x400_001C) }; +pub const BG3HOFS: VolAddress = unsafe { VolAddress::new(0x400_001C) }; /// BG3 Y-Offset. Write only. Text mode only. 9 bits. -pub const BG3VOFS: VolAddress = unsafe { VolAddress::new_unchecked(0x400_001E) }; +pub const BG3VOFS: VolAddress = unsafe { VolAddress::new(0x400_001E) }; // TODO: affine backgrounds // BG2X_L @@ -100,14 +100,14 @@ pub const BG3VOFS: VolAddress = unsafe { VolAddress::new_unchecked(0x400_00 // BG3PD // TODO: windowing -// pub const WIN0H: VolAddress = unsafe { VolAddress::new_unchecked(0x400_0040) }; -// pub const WIN1H: VolAddress = unsafe { VolAddress::new_unchecked(0x400_0042) }; -// pub const WIN0V: VolAddress = unsafe { VolAddress::new_unchecked(0x400_0044) }; -// pub const WIN1V: VolAddress = unsafe { VolAddress::new_unchecked(0x400_0046) }; -// pub const WININ: VolAddress = unsafe { VolAddress::new_unchecked(0x400_0048) }; -// pub const WINOUT: VolAddress = unsafe { VolAddress::new_unchecked(0x400_004A) }; +// pub const WIN0H: VolAddress = unsafe { VolAddress::new(0x400_0040) }; +// pub const WIN1H: VolAddress = unsafe { VolAddress::new(0x400_0042) }; +// pub const WIN0V: VolAddress = unsafe { VolAddress::new(0x400_0044) }; +// pub const WIN1V: VolAddress = unsafe { VolAddress::new(0x400_0046) }; +// pub const WININ: VolAddress = unsafe { VolAddress::new(0x400_0048) }; +// pub const WINOUT: VolAddress = unsafe { VolAddress::new(0x400_004A) }; // TODO: blending -// pub const BLDCNT: VolAddress = unsafe { VolAddress::new_unchecked(0x400_0050) }; -// pub const BLDALPHA: VolAddress = unsafe { VolAddress::new_unchecked(0x400_0052) }; -// pub const BLDY: VolAddress = unsafe { VolAddress::new_unchecked(0x400_0054) }; +// pub const BLDCNT: VolAddress = unsafe { VolAddress::new(0x400_0050) }; +// pub const BLDALPHA: VolAddress = unsafe { VolAddress::new(0x400_0052) }; +// pub const BLDY: VolAddress = unsafe { VolAddress::new(0x400_0054) }; diff --git a/src/io/color_blend.rs b/src/io/color_blend.rs index a1d041f..6eea535 100644 --- a/src/io/color_blend.rs +++ b/src/io/color_blend.rs @@ -3,9 +3,10 @@ use super::*; /// Color Special Effects Selection (R/W) -pub const BLDCNT: VolAddress = unsafe { VolAddress::new_unchecked(0x400_0050) }; +pub const BLDCNT: VolAddress = unsafe { VolAddress::new(0x400_0050) }; newtype! { + /// TODO: docs ColorEffectSetting, u16 } @@ -29,17 +30,23 @@ impl ColorEffectSetting { } newtype_enum! { + /// TODO: docs ColorSpecialEffect = u16, + /// TODO: docs None = 0, + /// TODO: docs AlphaBlending = 1, + /// TODO: docs BrightnessIncrease = 2, + /// TODO: docs BrightnessDecrease = 3, } /// Alpha Blending Coefficients (R/W) (not W) -pub const BLDALPHA: VolAddress = unsafe { VolAddress::new_unchecked(0x400_0052) }; +pub const BLDALPHA: VolAddress = unsafe { VolAddress::new(0x400_0052) }; newtype! { + /// TODO: docs AlphaBlendingSetting, u16 } @@ -52,9 +59,10 @@ impl AlphaBlendingSetting { } /// Brightness (Fade-In/Out) Coefficient (W) (not R/W) -pub const BLDY: VolAddress = unsafe { VolAddress::new_unchecked(0x400_0054) }; +pub const BLDY: VolAddress = unsafe { VolAddress::new(0x400_0054) }; newtype! { + /// TODO: docs BrightnessSetting, u32 } diff --git a/src/io/display.rs b/src/io/display.rs index 058143e..553967c 100644 --- a/src/io/display.rs +++ b/src/io/display.rs @@ -5,7 +5,7 @@ use super::*; /// LCD Control. Read/Write. /// /// The "force vblank" bit is always set when your Rust code first executes. -pub const DISPCNT: VolAddress = unsafe { VolAddress::new_unchecked(0x400_0000) }; +pub const DISPCNT: VolAddress = unsafe { VolAddress::new(0x400_0000) }; newtype!( /// Setting for the display control register. @@ -96,7 +96,7 @@ pub fn display_control() -> DisplayControlSetting { } /// Display Status and IRQ Control. Read/Write. -pub const DISPSTAT: VolAddress = unsafe { VolAddress::new_unchecked(0x400_0004) }; +pub const DISPSTAT: VolAddress = unsafe { VolAddress::new(0x400_0004) }; newtype!( /// A newtype over display status and interrupt control values. @@ -122,30 +122,13 @@ impl DisplayStatusSetting { /// Gives the current scanline that the display controller is working on. If /// this is at or above the `VBLANK_SCANLINE` value then the display controller /// is in a "vertical blank" period. -pub const VCOUNT: VolAddress = unsafe { VolAddress::new_unchecked(0x400_0006) }; +pub const VCOUNT: ROVolAddress = unsafe { ROVolAddress::new(0x400_0006) }; /// If the `VCOUNT` register reads equal to or above this then you're in vblank. pub const VBLANK_SCANLINE: u16 = 160; -/// Obtains the current `VCOUNT` value. -pub fn vcount() -> u16 { - VCOUNT.read() -} - -/// Performs a busy loop until VBlank starts. -pub fn spin_until_vblank() { - // TODO: make this the better version with BIOS and interrupts and such. - while vcount() < VBLANK_SCANLINE {} -} - -/// Performs a busy loop until VDraw starts. -pub fn spin_until_vdraw() { - // TODO: make this the better version with BIOS and interrupts and such. - while vcount() >= VBLANK_SCANLINE {} -} - /// Global mosaic effect control. Write-only. -pub const MOSAIC: VolAddress = unsafe { VolAddress::new_unchecked(0x400_004C) }; +pub const MOSAIC: VolAddress = unsafe { VolAddress::new(0x400_004C) }; newtype! { /// Allows control of the Mosaic effect. diff --git a/src/io/dma.rs b/src/io/dma.rs index 38f7567..96d9d14 100644 --- a/src/io/dma.rs +++ b/src/io/dma.rs @@ -127,13 +127,13 @@ pub enum DMAStartTiming { pub struct DMA0; impl DMA0 { /// DMA 0 Source Address, read only. - const DMA0SAD: VolAddress<*const u32> = unsafe { VolAddress::new_unchecked(0x400_00B0) }; + const DMA0SAD: VolAddress<*const u32> = unsafe { VolAddress::new(0x400_00B0) }; /// DMA 0 Destination Address, read only. - const DMA0DAD: VolAddress<*mut u32> = unsafe { VolAddress::new_unchecked(0x400_00B4) }; + const DMA0DAD: VolAddress<*mut u32> = unsafe { VolAddress::new(0x400_00B4) }; /// DMA 0 Word Count, read only. - const DMA0CNT_L: VolAddress = unsafe { VolAddress::new_unchecked(0x400_00B8) }; + const DMA0CNT_L: VolAddress = unsafe { VolAddress::new(0x400_00B8) }; /// DMA 0 Control, read/write. - const DMA0CNT_H: VolAddress = unsafe { VolAddress::new_unchecked(0x400_00BA) }; + const DMA0CNT_H: VolAddress = unsafe { VolAddress::new(0x400_00BA) }; /// Assigns the source register. /// @@ -188,13 +188,13 @@ impl DMA0 { pub struct DMA1; impl DMA1 { /// DMA 1 Source Address, read only. - const DMA1SAD: VolAddress<*const u32> = unsafe { VolAddress::new_unchecked(0x400_00BC) }; + const DMA1SAD: VolAddress<*const u32> = unsafe { VolAddress::new(0x400_00BC) }; /// DMA 1 Destination Address, read only. - const DMA1DAD: VolAddress<*mut u32> = unsafe { VolAddress::new_unchecked(0x400_00C0) }; + const DMA1DAD: VolAddress<*mut u32> = unsafe { VolAddress::new(0x400_00C0) }; /// DMA 1 Word Count, read only. - const DMA1CNT_L: VolAddress = unsafe { VolAddress::new_unchecked(0x400_00C4) }; + const DMA1CNT_L: VolAddress = unsafe { VolAddress::new(0x400_00C4) }; /// DMA 1 Control, read/write. - const DMA1CNT_H: VolAddress = unsafe { VolAddress::new_unchecked(0x400_00C6) }; + const DMA1CNT_H: VolAddress = unsafe { VolAddress::new(0x400_00C6) }; /// Assigns the source register. /// @@ -249,13 +249,13 @@ impl DMA1 { pub struct DMA2; impl DMA2 { /// DMA 2 Source Address, read only. - const DMA2SAD: VolAddress<*const u32> = unsafe { VolAddress::new_unchecked(0x400_00C8) }; + const DMA2SAD: VolAddress<*const u32> = unsafe { VolAddress::new(0x400_00C8) }; /// DMA 2 Destination Address, read only. - const DMA2DAD: VolAddress<*mut u32> = unsafe { VolAddress::new_unchecked(0x400_00CC) }; + const DMA2DAD: VolAddress<*mut u32> = unsafe { VolAddress::new(0x400_00CC) }; /// DMA 2 Word Count, read only. - const DMA2CNT_L: VolAddress = unsafe { VolAddress::new_unchecked(0x400_00D0) }; + const DMA2CNT_L: VolAddress = unsafe { VolAddress::new(0x400_00D0) }; /// DMA 2 Control, read/write. - const DMA2CNT_H: VolAddress = unsafe { VolAddress::new_unchecked(0x400_00D2) }; + const DMA2CNT_H: VolAddress = unsafe { VolAddress::new(0x400_00D2) }; /// Assigns the source register. /// @@ -311,13 +311,13 @@ impl DMA2 { pub struct DMA3; impl DMA3 { /// DMA 3 Source Address, read only. - const DMA3SAD: VolAddress<*const u32> = unsafe { VolAddress::new_unchecked(0x400_00D4) }; + const DMA3SAD: VolAddress<*const u32> = unsafe { VolAddress::new(0x400_00D4) }; /// DMA 3 Destination Address, read only. - const DMA3DAD: VolAddress<*mut u32> = unsafe { VolAddress::new_unchecked(0x400_00D8) }; + const DMA3DAD: VolAddress<*mut u32> = unsafe { VolAddress::new(0x400_00D8) }; /// DMA 3 Word Count, read only. - const DMA3CNT_L: VolAddress = unsafe { VolAddress::new_unchecked(0x400_00DC) }; + const DMA3CNT_L: VolAddress = unsafe { VolAddress::new(0x400_00DC) }; /// DMA 3 Control, read/write. - const DMA3CNT_H: VolAddress = unsafe { VolAddress::new_unchecked(0x400_00DE) }; + const DMA3CNT_H: VolAddress = unsafe { VolAddress::new(0x400_00DE) }; /// Assigns the source register. /// diff --git a/src/io/irq.rs b/src/io/irq.rs new file mode 100644 index 0000000..2f3a9bb --- /dev/null +++ b/src/io/irq.rs @@ -0,0 +1,180 @@ +//! Module containing a wrapper for interrupt request (IRQ) handling. +//! +//! When an interrupt is executed, the CPU will be set to IRQ mode and code +//! execution will jump to the physical interrupt vector, located in BIOS. The +//! BIOS interrupt handler will then save several registers to the IRQ stack +//! pointer and execution will jump to the user interrupt handler starting at +//! `0x0300_7FFC`, in ARM mode. +//! +//! Currently, the user interrupt handler is defined in `crt0.s`. It is set up +//! to execute a user-specified interrupt handler after saving some registers. +//! This handler is declared as a static function pointer on the Rust side, and +//! can be set by using [`set_irq_handler`](irq::set_irq_handler). +//! +//! ## Notes +//! * The interrupt will only be triggered if [`IME`](irq::IME) is enabled, the +//! flag corresponding to the interrupt is enabled on the [`IE`](irq::IE) +//! register, and the "IRQ Enable" flag is set on the register related to the +//! interrupt, which varies. For example, to enable interrupts on VBlank you +//! would set the +//! [`vblank_irq_enable`](io::display::DisplayStatusSetting::vblank_irq_enable) +//! flag on the [`DISPSTAT`](io::display::DISPCNT) register. +//! * If you intend to use [`interrupt_wait`](bios::interrupt_wait) or +//! [`vblank_interrupt_wait`](bios::vblank_interrupt_wait) to wait for an +//! interrupt, your interrupt handler MUST update the BIOS Interrupt Flags at +//! [`BIOS_IF`](irq::BIOS_IF) in addition to the usual interrupt +//! acknowledgement (which is handled for you by the user interrupt handler). +//! This is done by setting the corresponding IRQ flag on +//! [`BIOS_IF`](irq::BIOS_IF) at the end of the interrupt handler. +//! * You can change the low-level details of the interrupt handler by editing +//! the `MainIrqHandler` routine in `crt0.s`. For example, you could declare +//! an external static variable in Rust holding a table of interrupt function +//! pointers and jump directly into one of them in assembly, without the need +//! to write the branching logic in Rust. However, note that the main +//! interrupt handler MUST acknowledge all interrupts received by setting +//! their corresponding bits to `1` in the [`IF`](irq::IF) register. +//! * If you wait on one or more interrupts, be sure at least one of them is +//! able to be triggered or the call to wait will never return. +//! * If you wait on multiple interrupts and those interrupts fire too quickly, +//! it is possible that the call to wait will never return as interrupts will +//! be constantly received before control is returned to the caller. This +//! usually only happens when waiting on multiple timer interrupts with very +//! fast overflow rates. +//! +//! ## Example +//! +//! ```rust +//! extern "C" fn irq_handler(flags: IrqFlags) { +//! if flags.vblank() { +//! // Run drawing logic here. +//! +//! // Acknowledge the IRQ on the BIOS Interrupt Flags register. +//! BIOS_IF.write(BIOS_IF.read().with_vblank(true)); +//! } +//! } +//! +//! fn main_loop() { +//! // Set the IRQ handler to use. +//! irq::set_irq_handler(irq_handler); +//! +//! // Handle only the VBlank interrupt. +//! const FLAGS: IrqFlags = IrqFlags::new().with_vblank(true); +//! IE.write(flags); +//! +//! // Enable all interrupts that are set in the IE register. +//! IME.write(IrqEnableSetting::UseIE); +//! +//! // Enable IRQ generation during VBlank. +//! const DISPLAY_SETTINGS: DisplayStatusSetting = DisplayStatusSetting::new() +//! .with_vblank_irq_enable(true); +//! DISPSTAT.write(DISPLAY_SETTINGS); +//! +//! loop { +//! // Sleep the CPU until a VBlank IRQ is generated. +//! bios::vblank_interrupt_wait(); +//! } +//! } +//! ``` +//! +//! ## Implementation Details +//! +//! This is the setup the provided user interrupt handler in `crt0.s` will do +//! when an interrupt is received, in order. It is based on the _Recommended +//! User Interrupt Handling_ portion of the GBATEK reference. +//! +//! 1. Save the status of [`IME`](irq::IME). +//! 2. Save the IRQ stack pointer and change to system mode to use the user +//! stack instead of the IRQ stack (to prevent stack overflow). +//! 3. Disable interrupts by setting [`IME`](irq::IME) to 0, so other interrupts +//! will not preempt the main interrupt handler. +//! 4. Acknowledge all IRQs that occurred and were enabled in the +//! [`IE`](irq::IE) register by writing the bits to the [`IF`](irq::IF) +//! register. +//! 5. Save the user stack pointer, switch to Thumb mode and jump to the +//! user-specified interrupt handler. The IRQ flags that were set are passed +//! as an argument in `r0`. +//! 6. When the handler returns, restore the user stack pointer and switch back +//! to IRQ mode. +//! 7. Restore the IRQ stack pointer and the status of [`IME`](irq::IME). +//! 8. Return to the BIOS interrupt handler. + +use super::*; + +newtype!( + /// A newtype over all interrupt flags. + IrqFlags, pub u16 +); + +impl IrqFlags { + phantom_fields! { + self.0: u16, + vblank: 0, + hblank: 1, + vcounter: 2, + timer0: 3, + timer1: 4, + timer2: 5, + timer3: 6, + serial: 7, + dma0: 8, + dma1: 9, + dma2: 10, + dma3: 11, + keypad: 12, + game_pak: 13, + } +} + +/// Interrupt Enable Register. Read/Write. +/// +/// After setting up interrupt handlers, set the flags on this register type corresponding to the +/// IRQs you want to handle. +pub const IE: VolAddress = unsafe { VolAddress::new(0x400_0200) }; + +/// Interrupt Request Flags / IRQ Acknowledge. Read/Write. +/// +/// The main user interrupt handler will acknowledge the interrupt that was set +/// by writing to this register, so there is usually no need to modify it. +/// However, if the main interrupt handler in `crt0.s` is changed, then the +/// handler must write a `1` bit to all bits that are enabled on this register +/// when it is called. +pub const IF: VolAddress = unsafe { VolAddress::new(0x400_0200) }; + +newtype_enum! { + /// Setting to control whether interrupts are enabled. + IrqEnableSetting = u32, + /// Disable all interrupts. + DisableAll = 0, + /// Enable interrupts according to the flags set in the [`IE`](irq::IE) register. + UseIE = 1, +} + +/// Interrupt Master Enable Register. Read/Write. +pub const IME: VolAddress = unsafe { VolAddress::new(0x400_0208) }; + +/// BIOS Interrupt Flags. Read/Write. +/// +/// When using either [`interrupt_wait`](bios::interrupt_wait) or +/// [`vblank_interrupt_wait`](bios::vblank_interrupt_wait), the corresponding +/// interrupt handler MUST set the flag of the interrupt it has handled on this +/// register in addition to the usual interrupt acknowledgement. +pub const BIOS_IF: VolAddress = unsafe { VolAddress::new(0x0300_7FF8) }; + +/// A function pointer for use as an interrupt handler. +pub type IrqHandler = extern "C" fn(IrqFlags); + +/// Sets the function to run when an interrupt is executed. The function will +/// receive the interrupts that were acknowledged by the main interrupt handler +/// as an argument. +pub fn set_irq_handler(handler: IrqHandler) { + unsafe { + __IRQ_HANDLER = handler; + } +} + +extern "C" fn default_handler(_flags: IrqFlags) {} + +// Inner definition of the interrupt handler. It is referenced in `crt0.s`. +#[doc(hidden)] +#[no_mangle] +static mut __IRQ_HANDLER: IrqHandler = default_handler; diff --git a/src/io/keypad.rs b/src/io/keypad.rs index 487c04d..5805e12 100644 --- a/src/io/keypad.rs +++ b/src/io/keypad.rs @@ -8,7 +8,7 @@ use super::*; /// follow the "high-active" convention (hint: you probably do, it's far easier /// to work with) then call `read_key_input()` rather than reading this register /// directly. It will perform the necessary bit flip operation for you. -pub const KEYINPUT: VolAddress = unsafe { VolAddress::new_unchecked(0x400_0130) }; +pub const KEYINPUT: ROVolAddress = unsafe { ROVolAddress::new(0x400_0130) }; /// A "tribool" value helps us interpret the arrow pad. #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -50,9 +50,10 @@ impl KeyInput { KeyInput(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 { + /// Right/left tribool. + /// + /// Right is Plus and Left is Minus + pub fn x_tribool(self) -> TriBool { if self.right() { TriBool::Plus } else if self.left() { @@ -62,9 +63,10 @@ impl KeyInput { } } - /// Gives the arrow pad value as a tribool, with Plus being increased row - /// value (down). - pub fn row_direction(self) -> TriBool { + /// Up/down tribool. + /// + /// Down is Plus and Up is Minus + pub fn y_tribool(self) -> TriBool { if self.down() { TriBool::Plus } else if self.up() { @@ -86,7 +88,7 @@ pub fn read_key_input() -> KeyInput { /// Use this to configure when a keypad interrupt happens. /// /// See the `KeyInterruptSetting` type for more. -pub const KEYCNT: VolAddress = unsafe { VolAddress::new_unchecked(0x400_0132) }; +pub const KEYCNT: VolAddress = unsafe { VolAddress::new(0x400_0132) }; newtype! { /// Allows configuration of when a keypad interrupt fires. @@ -101,8 +103,9 @@ newtype! { /// of the interrupt firing. /// /// NOTE: This _only_ configures the operation of when keypad interrupts can - /// fire. You must still set the `IME` to have interrupts at all, and you must - /// further set `IE` for keypad interrupts to be possible. + /// fire. You must still set the [`IME`](irq::IME) to have interrupts at all, + /// and you must further set [`IE`](irq::IE) for keypad interrupts to be + /// possible. KeyInterruptSetting, u16 } #[allow(missing_docs)] diff --git a/src/io/sound.rs b/src/io/sound.rs index 46afcbc..dcb6736 100644 --- a/src/io/sound.rs +++ b/src/io/sound.rs @@ -1,12 +1,14 @@ -///! Module for sound registers. +//! Module for sound registers. + use super::*; //TODO within these "read/write" registers only some bits are actually read/write! /// Sound Channel 1 Sweep Register (`NR10`). Read/Write. -pub const SOUND1CNT_L: VolAddress = unsafe { VolAddress::new_unchecked(0x400_0060) }; +pub const SOUND1CNT_L: VolAddress = unsafe { VolAddress::new(0x400_0060) }; newtype! { + /// TODO: docs SweepRegisterSetting, u16 } @@ -20,9 +22,10 @@ impl SweepRegisterSetting { } /// Sound Channel 1 Duty/Length/Envelope (`NR11`, `NR12`). Read/Write. -pub const SOUND1CNT_H: VolAddress = unsafe { VolAddress::new_unchecked(0x400_0062) }; +pub const SOUND1CNT_H: VolAddress = unsafe { VolAddress::new(0x400_0062) }; newtype! { + /// TODO: docs DutyLenEnvelopeSetting, u16 } @@ -38,9 +41,10 @@ impl DutyLenEnvelopeSetting { } /// Sound Channel 1 Frequency/Control (`NR13`, `NR14`). Read/Write. -pub const SOUND1CNT_X: VolAddress = unsafe { VolAddress::new_unchecked(0x400_0064) }; +pub const SOUND1CNT_X: VolAddress = unsafe { VolAddress::new(0x400_0064) }; newtype! { + /// TODO: docs FrequencyControlSetting, u32 // TODO: u16 or u32? } @@ -54,15 +58,17 @@ impl FrequencyControlSetting { } /// Sound Channel 2 Channel 2 Duty/Length/Envelope (`NR21`, `NR22`). Read/Write. -pub const SOUND2CNT_L: VolAddress = unsafe { VolAddress::new_unchecked(0x400_0068) }; +pub const SOUND2CNT_L: VolAddress = unsafe { VolAddress::new(0x400_0068) }; /// Sound Channel 2 Frequency/Control (`NR23`, `NR24`). Read/Write. -pub const SOUND2CNT_H: VolAddress = unsafe { VolAddress::new_unchecked(0x400_006C) }; +pub const SOUND2CNT_H: VolAddress = unsafe { VolAddress::new(0x400_006C) }; /// Sound Channel 3 Stop/Wave RAM select (`NR23`, `NR24`). Read/Write. -pub const SOUND3CNT_L: VolAddress = unsafe { VolAddress::new_unchecked(0x400_0070) }; +pub const SOUND3CNT_L: VolAddress = + unsafe { VolAddress::new(0x400_0070) }; newtype! { + /// TODO: docs StopWaveRAMSelectSetting, u16 } @@ -76,9 +82,10 @@ impl StopWaveRAMSelectSetting { } /// Sound Channel 3 Length/Volume (`NR23`, `NR24`). Read/Write. -pub const SOUND3CNT_H: VolAddress = unsafe { VolAddress::new_unchecked(0x400_0072) }; +pub const SOUND3CNT_H: VolAddress = unsafe { VolAddress::new(0x400_0072) }; newtype! { + /// TODO: docs LengthVolumeSetting, u16 } @@ -92,29 +99,30 @@ impl LengthVolumeSetting { } /// Sound Channel 3 Frequency/Control (`NR33`, `NR34`). Read/Write. -pub const SOUND3CNT_X: VolAddress = unsafe { VolAddress::new_unchecked(0x400_0074) }; +pub const SOUND3CNT_X: VolAddress = unsafe { VolAddress::new(0x400_0074) }; /// Channel 3 Wave Pattern RAM (W/R) -pub const WAVE_RAM0_L: VolAddress = unsafe { VolAddress::new_unchecked(0x400_0090) }; +pub const WAVE_RAM0_L: VolAddress = unsafe { VolAddress::new(0x400_0090) }; /// Channel 3 Wave Pattern RAM (W/R) -pub const WAVE_RAM0_H: VolAddress = unsafe { VolAddress::new_unchecked(0x400_0092) }; +pub const WAVE_RAM0_H: VolAddress = unsafe { VolAddress::new(0x400_0092) }; /// Channel 3 Wave Pattern RAM (W/R) -pub const WAVE_RAM1_L: VolAddress = unsafe { VolAddress::new_unchecked(0x400_0094) }; +pub const WAVE_RAM1_L: VolAddress = unsafe { VolAddress::new(0x400_0094) }; /// Channel 3 Wave Pattern RAM (W/R) -pub const WAVE_RAM1_H: VolAddress = unsafe { VolAddress::new_unchecked(0x400_0096) }; +pub const WAVE_RAM1_H: VolAddress = unsafe { VolAddress::new(0x400_0096) }; /// Channel 3 Wave Pattern RAM (W/R) -pub const WAVE_RAM2_L: VolAddress = unsafe { VolAddress::new_unchecked(0x400_0098) }; +pub const WAVE_RAM2_L: VolAddress = unsafe { VolAddress::new(0x400_0098) }; /// Channel 3 Wave Pattern RAM (W/R) -pub const WAVE_RAM2_H: VolAddress = unsafe { VolAddress::new_unchecked(0x400_009A) }; +pub const WAVE_RAM2_H: VolAddress = unsafe { VolAddress::new(0x400_009A) }; /// Channel 3 Wave Pattern RAM (W/R) -pub const WAVE_RAM3_L: VolAddress = unsafe { VolAddress::new_unchecked(0x400_009C) }; +pub const WAVE_RAM3_L: VolAddress = unsafe { VolAddress::new(0x400_009C) }; /// Channel 3 Wave Pattern RAM (W/R) -pub const WAVE_RAM3_H: VolAddress = unsafe { VolAddress::new_unchecked(0x400_009E) }; +pub const WAVE_RAM3_H: VolAddress = unsafe { VolAddress::new(0x400_009E) }; /// Sound Channel 4 Length/Envelope (`NR41`, `NR42`). Read/Write. -pub const SOUND4CNT_L: VolAddress = unsafe { VolAddress::new_unchecked(0x400_0078) }; +pub const SOUND4CNT_L: VolAddress = unsafe { VolAddress::new(0x400_0078) }; newtype! { + /// TODO: docs LengthEnvelopeSetting, u32 // TODO: is this u32? } @@ -129,9 +137,10 @@ impl LengthEnvelopeSetting { } /// Sound Channel 4 Frequency/Control (`NR43`, `NR44`). Read/Write. -pub const SOUND4CNT_H: VolAddress = unsafe { VolAddress::new_unchecked(0x400_007C) }; +pub const SOUND4CNT_H: VolAddress = unsafe { VolAddress::new(0x400_007C) }; newtype! { + /// TODO: docs NoiseFrequencySetting, u32 // TODO: is this u32? } @@ -149,18 +158,20 @@ impl NoiseFrequencySetting { // TODO: unify FIFO as /// Sound A FIFO, Data 0 and Data 1 (W) -pub const FIFO_A_L: VolAddress = unsafe { VolAddress::new_unchecked(0x400_00A0) }; +pub const FIFO_A_L: VolAddress = unsafe { VolAddress::new(0x400_00A0) }; /// Sound A FIFO, Data 2 and Data 3 (W) -pub const FIFO_A_H: VolAddress = unsafe { VolAddress::new_unchecked(0x400_00A2) }; +pub const FIFO_A_H: VolAddress = unsafe { VolAddress::new(0x400_00A2) }; /// Sound B FIFO, Data 0 and Data 1 (W) -pub const FIFO_B_L: VolAddress = unsafe { VolAddress::new_unchecked(0x400_00A4) }; +pub const FIFO_B_L: VolAddress = unsafe { VolAddress::new(0x400_00A4) }; /// Sound B FIFO, Data 2 and Data 3 (W) -pub const FIFO_B_H: VolAddress = unsafe { VolAddress::new_unchecked(0x400_00A6) }; +pub const FIFO_B_H: VolAddress = unsafe { VolAddress::new(0x400_00A6) }; /// Channel L/R Volume/Enable (`NR50`, `NR51`). Read/Write. -pub const SOUNDCNT_L: VolAddress = unsafe { VolAddress::new_unchecked(0x400_0080) }; +pub const SOUNDCNT_L: VolAddress = + unsafe { VolAddress::new(0x400_0080) }; newtype! { + /// TODO: docs NonWaveVolumeEnableSetting, u16 } @@ -175,9 +186,10 @@ impl NonWaveVolumeEnableSetting { } /// DMA Sound Control/Mixing. Read/Write. -pub const SOUNDCNT_H: VolAddress = unsafe { VolAddress::new_unchecked(0x400_0082) }; +pub const SOUNDCNT_H: VolAddress = unsafe { VolAddress::new(0x400_0082) }; newtype! { + /// TODO: docs WaveVolumeEnableSetting, u16 } @@ -199,16 +211,21 @@ impl WaveVolumeEnableSetting { } newtype_enum! { + /// TODO: docs NumberSoundVolume = u16, + /// TODO: docs Quarter = 0, + /// TODO: docs Half = 1, + /// TODO: docs Full = 2, } /// Sound on/off (`NR52`). Read/Write. -pub const SOUNDCNT_X: VolAddress = unsafe { VolAddress::new_unchecked(0x400_0084) }; +pub const SOUNDCNT_X: VolAddress = unsafe { VolAddress::new(0x400_0084) }; newtype! { + /// TODO: docs SoundMasterSetting, u16 } @@ -224,9 +241,10 @@ impl SoundMasterSetting { } /// Sound on/off (`NR52`). Read/Write. -pub const SOUNDBIAS: VolAddress = unsafe { VolAddress::new_unchecked(0x400_0088) }; +pub const SOUNDBIAS: VolAddress = unsafe { VolAddress::new(0x400_0088) }; newtype! { + /// TODO: docs SoundPWMSetting, u16 } diff --git a/src/io/timers.rs b/src/io/timers.rs index 25cc516..1257ba4 100644 --- a/src/io/timers.rs +++ b/src/io/timers.rs @@ -28,28 +28,28 @@ use super::*; // TODO: striding blocks? /// Timer 0 Counter/Reload. Special (see module). -pub const TM0CNT_L: VolAddress = unsafe { VolAddress::new_unchecked(0x400_0100) }; +pub const TM0CNT_L: VolAddress = unsafe { VolAddress::new(0x400_0100) }; /// Timer 1 Counter/Reload. Special (see module). -pub const TM1CNT_L: VolAddress = unsafe { VolAddress::new_unchecked(0x400_0104) }; +pub const TM1CNT_L: VolAddress = unsafe { VolAddress::new(0x400_0104) }; /// Timer 2 Counter/Reload. Special (see module). -pub const TM2CNT_L: VolAddress = unsafe { VolAddress::new_unchecked(0x400_0108) }; +pub const TM2CNT_L: VolAddress = unsafe { VolAddress::new(0x400_0108) }; /// Timer 3 Counter/Reload. Special (see module). -pub const TM3CNT_L: VolAddress = unsafe { VolAddress::new_unchecked(0x400_010C) }; +pub const TM3CNT_L: VolAddress = unsafe { VolAddress::new(0x400_010C) }; /// Timer 0 Control. Read/Write. -pub const TM0CNT_H: VolAddress = unsafe { VolAddress::new_unchecked(0x400_0102) }; +pub const TM0CNT_H: VolAddress = unsafe { VolAddress::new(0x400_0102) }; /// Timer 1 Control. Read/Write. -pub const TM1CNT_H: VolAddress = unsafe { VolAddress::new_unchecked(0x400_0106) }; +pub const TM1CNT_H: VolAddress = unsafe { VolAddress::new(0x400_0106) }; /// Timer 2 Control. Read/Write. -pub const TM2CNT_H: VolAddress = unsafe { VolAddress::new_unchecked(0x400_010A) }; +pub const TM2CNT_H: VolAddress = unsafe { VolAddress::new(0x400_010A) }; /// Timer 3 Control. Read/Write. -pub const TM3CNT_H: VolAddress = unsafe { VolAddress::new_unchecked(0x400_010E) }; +pub const TM3CNT_H: VolAddress = unsafe { VolAddress::new(0x400_010E) }; newtype! { /// Allows control of a timer unit. diff --git a/src/io/window.rs b/src/io/window.rs index 0e09794..c163824 100644 --- a/src/io/window.rs +++ b/src/io/window.rs @@ -3,12 +3,13 @@ use super::*; /// Window 0 Horizontal Dimensions (W) -pub const WIN0H: VolAddress = unsafe { VolAddress::new_unchecked(0x400_0040) }; +pub const WIN0H: VolAddress = unsafe { VolAddress::new(0x400_0040) }; /// Window 1 Horizontal Dimensions (W) -pub const WIN1H: VolAddress = unsafe { VolAddress::new_unchecked(0x400_0042) }; +pub const WIN1H: VolAddress = unsafe { VolAddress::new(0x400_0042) }; newtype! { + /// TODO: docs HorizontalWindowSetting, u16 } @@ -21,12 +22,13 @@ impl HorizontalWindowSetting { } /// Window 0 Vertical Dimensions (W) -pub const WIN0V: VolAddress = unsafe { VolAddress::new_unchecked(0x400_0044) }; +pub const WIN0V: VolAddress = unsafe { VolAddress::new(0x400_0044) }; /// Window 1 Vertical Dimensions (W) -pub const WIN1V: VolAddress = unsafe { VolAddress::new_unchecked(0x400_0046) }; +pub const WIN1V: VolAddress = unsafe { VolAddress::new(0x400_0046) }; newtype! { + /// TODO: docs VerticalWindowSetting, u16 } @@ -39,9 +41,10 @@ impl VerticalWindowSetting { } /// Control of Inside of Window(s) (R/W) -pub const WININ: VolAddress = unsafe { VolAddress::new_unchecked(0x400_0048) }; +pub const WININ: VolAddress = unsafe { VolAddress::new(0x400_0048) }; newtype! { + /// TODO: docs InsideWindowSetting, u16 } @@ -64,9 +67,10 @@ impl InsideWindowSetting { } /// Control of Outside of Windows & Inside of OBJ Window (R/W) -pub const WINOUT: VolAddress = unsafe { VolAddress::new_unchecked(0x400_004A) }; +pub const WINOUT: VolAddress = unsafe { VolAddress::new(0x400_004A) }; newtype! { + /// TODO: docs OutsideWindowSetting, u16 } diff --git a/src/lib.rs b/src/lib.rs index 240d76a..ca17759 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -3,7 +3,7 @@ #![feature(cfg_target_vendor)] #![allow(clippy::cast_lossless)] #![deny(clippy::float_arithmetic)] -//#![warn(missing_docs)] +#![warn(missing_docs)] //! This crate helps you write GBA ROMs. //! @@ -19,93 +19,11 @@ //! do, it's a giant bag of Undefined Behavior. pub(crate) use gba_proc_macro::phantom_fields; +pub(crate) use voladdress::{read_only::ROVolAddress, VolAddress, VolBlock}; -/// Assists in defining a newtype wrapper over some base type. -/// -/// Note that rustdoc and derives are all the "meta" stuff, so you can write all -/// of your docs and derives in front of your newtype in the same way you would -/// for a normal struct. Then the inner type to be wrapped it name. -/// -/// The macro _assumes_ that you'll be using it to wrap numeric types and that -/// it's safe to have a `0` value, so it automatically provides a `const fn` -/// method for `new` that just wraps `0`. Also, it derives Debug, Clone, Copy, -/// Default, PartialEq, and Eq. If all this is not desired you can add `, no -/// frills` to the invocation. -/// -/// Example: -/// ``` -/// newtype! { -/// /// Records a particular key press combination. -/// KeyInput, u16 -/// } -/// newtype! { -/// /// You can't derive most stuff above array size 32, so we add -/// /// the `, no frills` modifier to this one. -/// BigArray, [u8; 200], no frills -/// } -/// ``` -#[macro_export] -macro_rules! newtype { - ($(#[$attr:meta])* $new_name:ident, $v:vis $old_name:ty) => { - $(#[$attr])* - #[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] - #[repr(transparent)] - pub struct $new_name($v $old_name); - impl $new_name { - /// A `const` "zero value" constructor - pub const fn new() -> Self { - $new_name(0) - } - } - }; - ($(#[$attr:meta])* $new_name:ident, $v:vis $old_name:ty, no frills) => { - $(#[$attr])* - #[repr(transparent)] - pub struct $new_name($v $old_name); - }; -} - -/// Assists in defining a newtype that's an enum. -/// -/// First give `NewType = OldType,`, then define the tags and their explicit -/// values with zero or more entries of `TagName = base_value,`. In both cases -/// you can place doc comments or other attributes directly on to the type -/// declaration or the tag declaration. -/// -/// The generated enum will get an appropriate `repr` attribute as well as Debug, Clone, Copy, -/// -/// Example: -/// ``` -/// newtype_enum! { -/// /// The Foo -/// Foo = u16, -/// /// The Bar -/// Bar = 0, -/// /// The Zap -/// Zap = 1, -/// } -/// ``` -#[macro_export] -macro_rules! newtype_enum { - ( - $(#[$struct_attr:meta])* - $new_name:ident = $old_name:ident, - $($(#[$tag_attr:meta])* $tag_name:ident = $base_value:expr,)* - ) => { - $(#[$struct_attr])* - #[derive(Debug, Clone, Copy, PartialEq, Eq)] - #[repr($old_name)] - pub enum $new_name { - $( - $(#[$tag_attr])* - $tag_name = $base_value, - )* - } - }; -} +pub mod macros; pub mod base; -pub(crate) use self::base::*; pub mod bios; @@ -133,13 +51,16 @@ extern "C" { /// Memory in IWRAM _before_ this location is not free to use, you'll trash /// your globals and stuff. Memory here or after is freely available for use /// (careful that you don't run into your own stack of course). - static __bss_end: u8; + /// + /// The actual value is unimportant, you just want to use the _address of_ + /// this location as the start of your IWRAM usage. + pub static __bss_end: u8; } newtype! { /// A color on the GBA is an RGB 5.5.5 within a `u16` #[derive(PartialOrd, Ord, Hash)] - Color, u16 + Color, pub u16 } impl Color { @@ -150,13 +71,6 @@ impl Color { pub const fn from_rgb(r: u16, g: u16, b: u16) -> Color { Color(b << 10 | g << 5 | r) } - - /// Does a left rotate of the bits. - /// - /// This has no particular meaning but is a wild way to cycle colors. - pub const fn rotate_left(self, n: u32) -> Color { - Color(self.0.rotate_left(n)) - } } // diff --git a/src/macros.rs b/src/macros.rs new file mode 100644 index 0000000..5cbc981 --- /dev/null +++ b/src/macros.rs @@ -0,0 +1,203 @@ +//! Contains the macros for the crate. +//! +//! Because (unlike everything else in Rust) a macro has to be declared before +//! use, we place them in their own module and then declare that module at the +//! start of the crate. + +/// Assists in defining a newtype wrapper over some base type. +/// +/// Note that rustdoc and derives are all the "meta" stuff, so you can write all +/// of your docs and derives in front of your newtype in the same way you would +/// for a normal struct. Then the inner type to be wrapped it name. +/// +/// The macro _assumes_ that you'll be using it to wrap numeric types and that +/// it's safe to have a `0` value, so it automatically provides a `const fn` +/// method for `new` that just wraps `0`. Also, it derives Debug, Clone, Copy, +/// Default, PartialEq, and Eq. If all this is not desired you can add `, no +/// frills` to the invocation. +/// +/// ```no_run +/// newtype! { +/// /// Records a particular key press combination. +/// KeyInput, u16 +/// } +/// newtype! { +/// /// You can't derive most stuff above array size 32, so we add +/// /// the `, no frills` modifier to this one. +/// BigArray, [u8; 200], no frills +/// } +/// ``` +#[macro_export] +macro_rules! newtype { + ($(#[$attr:meta])* $new_name:ident, $v:vis $old_name:ty) => { + $(#[$attr])* + #[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] + #[repr(transparent)] + pub struct $new_name($v $old_name); + impl $new_name { + /// A `const` "zero value" constructor + pub const fn new() -> Self { + $new_name(0) + } + } + }; + ($(#[$attr:meta])* $new_name:ident, $v:vis $old_name:ty, no frills) => { + $(#[$attr])* + #[repr(transparent)] + pub struct $new_name($v $old_name); + }; +} + +/// Assists in defining a newtype that's an enum. +/// +/// First give `NewType = OldType,`, then define the tags and their explicit +/// values with zero or more entries of `TagName = base_value,`. In both cases +/// you can place doc comments or other attributes directly on to the type +/// declaration or the tag declaration. +/// +/// The generated enum will get an appropriate `repr` attribute as well as +/// Debug, Clone, Copy, PartialEq, and Eq +/// +/// ```no_run +/// newtype_enum! { +/// /// The Foo +/// Foo = u16, +/// /// The Bar +/// Bar = 0, +/// /// The Zap +/// Zap = 1, +/// } +/// ``` +#[macro_export] +macro_rules! newtype_enum { + ( + $(#[$struct_attr:meta])* + $new_name:ident = $old_name:ident, + $($(#[$tag_attr:meta])* $tag_name:ident = $base_value:expr,)* + ) => { + $(#[$struct_attr])* + #[derive(Debug, Clone, Copy, PartialEq, Eq)] + #[repr($old_name)] + pub enum $new_name { + $( + $(#[$tag_attr])* + $tag_name = $base_value, + )* + } + }; +} + +/// Delivers a fatal message to the mGBA output, halting emulation. +/// +/// This works basically like `println`. mGBA is a C program and all, so you +/// should only attempt to print non-null ASCII values through this. There's +/// also a maximum length of 255 bytes per message. +/// +/// This has no effect if you're not using mGBA. +#[macro_export] +macro_rules! fatal { + ($($arg:tt)*) => {{ + use $crate::mgba::{MGBADebug, MGBADebugLevel}; + use core::fmt::Write; + if let Some(mut mgba) = MGBADebug::new() { + let _ = write!(mgba, $($arg)*); + mgba.send(MGBADebugLevel::Fatal); + } + }}; +} + +/// Delivers an error message to the mGBA output. +/// +/// This works basically like `println`. mGBA is a C program and all, so you +/// should only attempt to print non-null ASCII values through this. There's +/// also a maximum length of 255 bytes per message. +/// +/// This has no effect if you're not using mGBA. +#[macro_export] +macro_rules! error { + ($($arg:tt)*) => {{ + use $crate::mgba::{MGBADebug, MGBADebugLevel}; + use core::fmt::Write; + if let Some(mut mgba) = MGBADebug::new() { + let _ = write!(mgba, $($arg)*); + mgba.send(MGBADebugLevel::Error); + } + }}; +} + +/// Delivers a warning message to the mGBA output. +/// +/// This works basically like `println`. mGBA is a C program and all, so you +/// should only attempt to print non-null ASCII values through this. There's +/// also a maximum length of 255 bytes per message. +/// +/// This has no effect if you're not using mGBA. +#[macro_export] +macro_rules! warn { + ($($arg:tt)*) => {{ + use $crate::mgba::{MGBADebug, MGBADebugLevel}; + use core::fmt::Write; + if let Some(mut mgba) = MGBADebug::new() { + let _ = write!(mgba, $($arg)*); + mgba.send(MGBADebugLevel::Warning); + } + }}; +} + +/// Delivers an info message to the mGBA output. +/// +/// This works basically like `println`. mGBA is a C program and all, so you +/// should only attempt to print non-null ASCII values through this. There's +/// also a maximum length of 255 bytes per message. +/// +/// This has no effect if you're not using mGBA. +#[macro_export] +macro_rules! info { + ($($arg:tt)*) => {{ + use $crate::mgba::{MGBADebug, MGBADebugLevel}; + use core::fmt::Write; + if let Some(mut mgba) = MGBADebug::new() { + let _ = write!(mgba, $($arg)*); + mgba.send(MGBADebugLevel::Info); + } + }}; +} + +/// Delivers a debug message to the mGBA output. +/// +/// This works basically like `println`. mGBA is a C program and all, so you +/// should only attempt to print non-null ASCII values through this. There's +/// also a maximum length of 255 bytes per message. +/// +/// This has no effect if you're not using mGBA. +#[macro_export] +macro_rules! debug { + ($($arg:tt)*) => {{ + use $crate::mgba::{MGBADebug, MGBADebugLevel}; + use core::fmt::Write; + if let Some(mut mgba) = MGBADebug::new() { + let _ = write!(mgba, $($arg)*); + mgba.send(MGBADebugLevel::Debug); + } + }}; +} + +/// Using timers 0 and 1, performs a crude timing of the expression given. +#[macro_export] +macro_rules! time_this01 { + ($x:expr) => {{ + use $crate::io::timers::*; + const NORMAL_ON: TimerControlSetting = TimerControlSetting::new().with_enabled(true); + const CASCADE_ON: TimerControlSetting = + TimerControlSetting::new().with_enabled(true).with_tick_rate(TimerTickRate::Cascade); + const OFF: TimerControlSetting = TimerControlSetting::new(); + TM1CNT_H.write(CASCADE_ON); + TM0CNT_H.write(NORMAL_ON); + $x; + TM0CNT_H.write(OFF); + TM1CNT_H.write(OFF); + let end_low = TM0CNT_L.read() as u32; + let end_high = TM1CNT_L.read() as u32; + end_high << 16 | end_low + }}; +} diff --git a/src/mgba.rs b/src/mgba.rs index a108b1b..c6a50ad 100644 --- a/src/mgba.rs +++ b/src/mgba.rs @@ -24,13 +24,13 @@ pub struct MGBADebug { bytes_written: u8, } impl MGBADebug { - const ENABLE_ADDRESS: VolAddress = unsafe { VolAddress::new_unchecked(0x4fff780) }; + const ENABLE_ADDRESS: VolAddress = unsafe { VolAddress::new(0x4fff780) }; const ENABLE_ADDRESS_INPUT: u16 = 0xC0DE; const ENABLE_ADDRESS_OUTPUT: u16 = 0x1DEA; - const OUTPUT_BASE: VolAddress = unsafe { VolAddress::new_unchecked(0x4fff600) }; + const OUTPUT_BASE: VolAddress = unsafe { VolAddress::new(0x4fff600) }; - const SEND_ADDRESS: VolAddress = unsafe { VolAddress::new_unchecked(0x4fff700) }; + const SEND_ADDRESS: VolAddress = unsafe { VolAddress::new(0x4fff700) }; const SEND_FLAG: u16 = 0x100; /// Gives a new MGBADebug, if running within `mGBA` @@ -55,8 +55,6 @@ impl MGBADebug { /// it might accidentally be discarded. pub fn send(&mut self, level: MGBADebugLevel) { if level == MGBADebugLevel::Fatal { - Self::SEND_ADDRESS.write(Self::SEND_FLAG | MGBADebugLevel::Error as u16); - // Note(Lokathor): A Fatal send causes the emulator to halt! Self::SEND_ADDRESS.write(Self::SEND_FLAG | MGBADebugLevel::Fatal as u16); } else { diff --git a/src/oam.rs b/src/oam.rs index b84f2e3..82b3f58 100644 --- a/src/oam.rs +++ b/src/oam.rs @@ -2,6 +2,8 @@ use super::*; +use typenum::consts::{U128, U32}; + newtype! { /// 0th part of an object's attributes. /// @@ -137,7 +139,8 @@ pub struct ObjectAttributes { /// The object attributes, but there are gaps in the array, so we must not /// expose this directly. -const OBJ_ATTR_APPROX: VolAddressBlock<[u16; 4]> = unsafe { VolAddressBlock::new_unchecked(VolAddress::new_unchecked(0x700_0000), 128) }; +const OBJ_ATTR_APPROX: VolBlock<[u16; 4], U128> = unsafe { VolBlock::new(0x700_0000) }; +// TODO: VolSeries pub fn write_obj_attributes(slot: usize, attributes: ObjectAttributes) -> Option<()> { OBJ_ATTR_APPROX.get(slot).map(|va| unsafe { @@ -169,7 +172,8 @@ pub struct AffineParameters { /// The object attributes, but there are gaps in the array, so we must not /// expose this directly. -const AFFINE_PARAMS_APPROX: VolAddressBlock<[i16; 16]> = unsafe { VolAddressBlock::new_unchecked(VolAddress::new_unchecked(0x700_0000), 32) }; +const AFFINE_PARAMS_APPROX: VolBlock<[i16; 16], U32> = unsafe { VolBlock::new(0x700_0000) }; +// TODO: VolSeries pub fn write_affine_parameters(slot: usize, params: AffineParameters) -> Option<()> { AFFINE_PARAMS_APPROX.get(slot).map(|va| unsafe { diff --git a/src/palram.rs b/src/palram.rs index 5158804..7d8769e 100644 --- a/src/palram.rs +++ b/src/palram.rs @@ -23,18 +23,17 @@ //! display will show if no background or object draws over top of a given pixel //! during rendering. -use super::{ - base::volatile::{VolAddress, VolAddressBlock}, - Color, -}; +use super::*; + +use typenum::consts::U256; // TODO: PalIndex newtypes? /// The `PALRAM` for background colors, 256 slot view. -pub const PALRAM_BG: VolAddressBlock = unsafe { VolAddressBlock::new_unchecked(VolAddress::new_unchecked(0x500_0000), 256) }; +pub const PALRAM_BG: VolBlock = unsafe { VolBlock::new(0x500_0000) }; /// The `PALRAM` for object colors, 256 slot view. -pub const PALRAM_OBJ: VolAddressBlock = unsafe { VolAddressBlock::new_unchecked(VolAddress::new_unchecked(0x500_0200), 256) }; +pub const PALRAM_OBJ: VolBlock = unsafe { VolBlock::new(0x500_0200) }; /// Obtains the address of the specified 8bpp background palette slot. pub const fn index_palram_bg_8bpp(slot: u8) -> VolAddress { diff --git a/src/vram.rs b/src/vram.rs index d4eaf9a..a0efeb2 100644 --- a/src/vram.rs +++ b/src/vram.rs @@ -15,6 +15,8 @@ pub(crate) use super::*; +use typenum::consts::{U256, U32, U512, U6}; + pub mod affine; pub mod bitmap; pub mod text; @@ -27,12 +29,14 @@ pub mod text; /// being the correct thing. pub const VRAM_BASE_USIZE: usize = 0x600_0000; +pub const PAGE1_OFFSET: usize = 0xA000; + /// The character base blocks. -pub const CHAR_BASE_BLOCKS: VolAddressBlock<[u8; 0x4000]> = unsafe { VolAddressBlock::new_unchecked(VolAddress::new_unchecked(VRAM_BASE_USIZE), 6) }; +pub const CHAR_BASE_BLOCKS: VolBlock<[u8; 0x4000], U6> = unsafe { VolBlock::new(VRAM_BASE_USIZE) }; /// The screen entry base blocks. -pub const SCREEN_BASE_BLOCKS: VolAddressBlock<[u8; 0x800]> = - unsafe { VolAddressBlock::new_unchecked(VolAddress::new_unchecked(VRAM_BASE_USIZE), 32) }; +pub const SCREEN_BASE_BLOCKS: VolBlock<[u8; 0x800], U32> = + unsafe { VolBlock::new(VRAM_BASE_USIZE) }; newtype! { /// An 8x8 tile with 4bpp, packed as `u32` values for proper alignment. @@ -47,11 +51,11 @@ newtype! { } /// Gives the specified charblock in 4bpp view. -pub fn get_4bpp_character_block(slot: usize) -> VolAddressBlock { - unsafe { VolAddressBlock::new_unchecked(CHAR_BASE_BLOCKS.index(slot).cast::(), 512) } +pub fn get_4bpp_character_block(slot: usize) -> VolBlock { + unsafe { VolBlock::new(CHAR_BASE_BLOCKS.index(slot).to_usize()) } } /// Gives the specified charblock in 8bpp view. -pub fn get_8bpp_character_block(slot: usize) -> VolAddressBlock { - unsafe { VolAddressBlock::new_unchecked(CHAR_BASE_BLOCKS.index(slot).cast::(), 256) } +pub fn get_8bpp_character_block(slot: usize) -> VolBlock { + unsafe { VolBlock::new(CHAR_BASE_BLOCKS.index(slot).to_usize()) } } diff --git a/src/vram/bitmap.rs b/src/vram/bitmap.rs index a716d9c..0dc8606 100644 --- a/src/vram/bitmap.rs +++ b/src/vram/bitmap.rs @@ -1,154 +1,213 @@ //! Module for the Bitmap video modes. use super::*; +use core::ops::{Div, Mul}; +use typenum::consts::{U128, U160, U2, U256, U4}; -/// Mode 3 is a bitmap mode with full color and full resolution. +/// A bitmap video mode with full color and full resolution. /// /// * **Width:** 240 /// * **Height:** 160 /// -/// Because the memory requirements are so large, there's only a single page -/// available instead of two pages like the other video modes have. +/// Because it takes so much space to have full color and full resolution at the +/// same time, there's no alternate page available when using mode 3. /// -/// As with all bitmap modes, the image itself utilizes BG2 for display, so you -/// must have BG2 enabled in addition to being within Mode 3. +/// As with all the bitmap video modes, the bitmap is considered to be BG2, so +/// you have to enable BG2 as well if you want to see the bitmap. pub struct Mode3; + impl Mode3 { - /// The physical width in pixels of the GBA screen. - pub const SCREEN_WIDTH: usize = 240; + /// The screen's width in this mode. + pub const WIDTH: usize = 240; - /// The physical height in pixels of the GBA screen. - pub const SCREEN_HEIGHT: usize = 160; + /// The screen's height in this mode. + pub const HEIGHT: usize = 160; - /// The number of pixels on the screen. - pub const SCREEN_PIXEL_COUNT: usize = Self::SCREEN_WIDTH * Self::SCREEN_HEIGHT; + const VRAM: VolBlock>::Output> = + unsafe { VolBlock::new(VRAM_BASE_USIZE) }; - /// The Mode 3 VRAM. + const WORDS_BLOCK: VolBlock>::Output as Div>::Output> = + unsafe { VolBlock::new(VRAM_BASE_USIZE) }; + + /// Gets the address of the pixel specified. /// - /// Use `col + row * SCREEN_WIDTH` to get the address of an individual pixel, - /// or use the helpers provided in this module. - pub const VRAM: VolAddressBlock = - unsafe { VolAddressBlock::new_unchecked(VolAddress::new_unchecked(VRAM_BASE_USIZE), Self::SCREEN_PIXEL_COUNT) }; - - /// private iterator over the pixels, two at a time - const BULK_ITER: VolAddressIter = - unsafe { VolAddressBlock::new_unchecked(VolAddress::new_unchecked(VRAM_BASE_USIZE), Self::SCREEN_PIXEL_COUNT / 2).iter() }; - - /// Reads the pixel at the given (col,row). + /// ## Failure /// - /// # Failure - /// - /// Gives `None` if out of bounds. - pub fn read_pixel(col: usize, row: usize) -> Option { - Self::VRAM.get(col + row * Self::SCREEN_WIDTH).map(VolAddress::read) + /// Gives `None` if out of bounds + fn get(col: usize, row: usize) -> Option> { + Self::VRAM.get(col + row * Self::WIDTH) } - /// Writes the pixel at the given (col,row). + /// Reads the color of the pixel specified. /// - /// # Failure + /// ## Failure /// - /// Gives `None` if out of bounds. - pub fn write_pixel(col: usize, row: usize, color: Color) -> Option<()> { - Self::VRAM.get(col + row * Self::SCREEN_WIDTH).map(|va| va.write(color)) + /// Gives `None` if out of bounds + pub fn read(col: usize, row: usize) -> Option { + Self::get(col, row).map(VolAddress::read) } - /// Clears the whole screen to the desired color. + /// Writes a color to the pixel specified. + /// + /// ## Failure + /// + /// Gives `None` if out of bounds + pub fn write(col: usize, row: usize, color: Color) -> Option<()> { + Self::get(col, row).map(|va| va.write(color)) + } + + /// Clear the screen to the color specified. + /// + /// Takes ~430,000 cycles (~1.5 frames). pub fn clear_to(color: Color) { let color32 = color.0 as u32; let bulk_color = color32 << 16 | color32; - for va in Self::BULK_ITER { + for va in Self::WORDS_BLOCK.iter() { va.write(bulk_color) } } - /// Clears the whole screen to the desired color using DMA3. + /// Clears the screen to the color specified using DMA3. + /// + /// Takes ~61,500 frames (~73% of VBlank) pub fn dma_clear_to(color: Color) { use crate::io::dma::DMA3; - let color32 = color.0 as u32; let bulk_color = color32 << 16 | color32; - unsafe { DMA3::fill32(&bulk_color, VRAM_BASE_USIZE as *mut u32, (Self::SCREEN_PIXEL_COUNT / 2) as u16) }; + unsafe { + DMA3::fill32(&bulk_color, VRAM_BASE_USIZE as *mut u32, Self::WORDS_BLOCK.len() as u16) + }; + } + + /// Draws a line between the two points given `(c1,r1,c2,r2,color)`. + /// + /// Works fine with out of bounds points. It only draws to in bounds + /// locations. + pub fn draw_line(c1: isize, r1: isize, c2: isize, r2: isize, color: Color) { + let mut col = c1; + let mut row = r1; + let w = c2 - c1; + let h = r2 - r1; + let mut dx1 = 0; + let mut dx2 = 0; + let mut dy1 = 0; + let mut dy2 = 0; + let mut longest = w.abs(); + let mut shortest = h.abs(); + if w < 0 { + dx1 = -1; + } else if w > 0 { + dx1 = 1; + }; + if h < 0 { + dy1 = -1; + } else if h > 0 { + dy1 = 1; + }; + if w < 0 { + dx2 = -1; + } else if w > 0 { + dx2 = 1; + }; + if !(longest > shortest) { + core::mem::swap(&mut longest, &mut shortest); + if h < 0 { + dy2 = -1; + } else if h > 0 { + dy2 = 1 + }; + dx2 = 0; + } + let mut numerator = longest >> 1; + + (0..(longest + 1)).for_each(|_| { + Self::write(col as usize, row as usize, color); + numerator += shortest; + if !(numerator < longest) { + numerator -= longest; + col += dx1; + row += dy1; + } else { + col += dx2; + row += dy2; + } + }); } } -//TODO: Mode3 Iter Scanlines / Pixels? -//TODO: Mode3 Line Drawing? +/// Used to select what page to read from or write to in Mode 4 and Mode 5. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Page { + /// Page 0 + Zero, + /// Page 1 + One, +} -/// Mode 4 is a bitmap mode with 8bpp paletted color. +/// A bitmap video mode with full resolution and paletted color. /// /// * **Width:** 240 /// * **Height:** 160 /// * **Pages:** 2 /// -/// VRAM has a minimum write size of 2 bytes at a time, so writing individual -/// palette entries for the pixels is more costly than with the other bitmap -/// modes. +/// Because the pixels use palette indexes there's enough space to have two +/// pages. /// -/// As with all bitmap modes, the image itself utilizes BG2 for display, so you -/// must have BG2 enabled in addition to being within Mode 4. +/// As with all the bitmap video modes, the bitmap is considered to be BG2, so +/// you have to enable BG2 as well if you want to see the bitmap. pub struct Mode4; + impl Mode4 { - /// The physical width in pixels of the GBA screen. - pub const SCREEN_WIDTH: usize = 240; + /// The screen's width in this mode. + pub const WIDTH: usize = 240; - /// The physical height in pixels of the GBA screen. - pub const SCREEN_HEIGHT: usize = 160; + /// The screen's height in this mode. + pub const HEIGHT: usize = 160; - /// The number of pixels on the screen. - pub const SCREEN_PIXEL_COUNT: usize = Self::SCREEN_WIDTH * Self::SCREEN_HEIGHT; + const PAGE0_INDEXES: VolBlock>::Output> = + unsafe { VolBlock::new(VRAM_BASE_USIZE) }; - /// Used for bulk clearing operations. - const SCREEN_U32_COUNT: usize = Self::SCREEN_PIXEL_COUNT / 4; + const PAGE1_INDEXES: VolBlock>::Output> = + unsafe { VolBlock::new(VRAM_BASE_USIZE + PAGE1_OFFSET) }; - // TODO: newtype this? - const PAGE0_BASE: VolAddress = unsafe { VolAddress::new_unchecked(VRAM_BASE_USIZE) }; + const PAGE0_WORDS: VolBlock>::Output as Div>::Output> = + unsafe { VolBlock::new(VRAM_BASE_USIZE) }; - // TODO: newtype this? - const PAGE0_BLOCK: VolAddressBlock = unsafe { VolAddressBlock::new_unchecked(Self::PAGE0_BASE, Self::SCREEN_PIXEL_COUNT) }; + const PAGE1_WORDS: VolBlock>::Output as Div>::Output> = + unsafe { VolBlock::new(VRAM_BASE_USIZE + PAGE1_OFFSET) }; - // TODO: newtype this? - const PAGE1_BASE: VolAddress = unsafe { VolAddress::new_unchecked(VRAM_BASE_USIZE + 0xA000) }; - - // TODO: newtype this? - const PAGE1_BLOCK: VolAddressBlock = unsafe { VolAddressBlock::new_unchecked(Self::PAGE1_BASE, Self::SCREEN_PIXEL_COUNT) }; - - /// private iterator over the page0 pixels, four at a time - const BULK_ITER0: VolAddressIter = unsafe { VolAddressBlock::new_unchecked(Self::PAGE0_BASE.cast::(), Self::SCREEN_U32_COUNT).iter() }; - - /// private iterator over the page1 pixels, four at a time - const BULK_ITER1: VolAddressIter = unsafe { VolAddressBlock::new_unchecked(Self::PAGE1_BASE.cast::(), Self::SCREEN_U32_COUNT).iter() }; - - /// Reads the pixel at the given (col,row). + /// Reads the color of the pixel specified. /// - /// # Failure + /// ## Failure /// - /// Gives `None` if out of bounds. - pub fn read_pixel(page1: bool, col: usize, row: usize) -> Option { - // Note(Lokathor): byte _reads_ from VRAM are okay. - if page1 { - Self::PAGE1_BLOCK.get(col + row * Self::SCREEN_WIDTH).map(VolAddress::read) - } else { - Self::PAGE0_BLOCK.get(col + row * Self::SCREEN_WIDTH).map(VolAddress::read) + /// Gives `None` if out of bounds + pub fn read(page: Page, col: usize, row: usize) -> Option { + match page { + Page::Zero => Self::PAGE0_INDEXES, + Page::One => Self::PAGE1_INDEXES, } + .get(col + row * Self::WIDTH) + .map(VolAddress::read) } - /// Writes the pixel at the given (col,row). + /// Writes a color to the pixel specified. /// - /// # Failure + /// ## Failure /// - /// Gives `None` if out of bounds. - pub fn write_pixel(page1: bool, col: usize, row: usize, pal8bpp: u8) -> Option<()> { - // Note(Lokathor): byte _writes_ to VRAM are not permitted. We must jump - // through hoops when we attempt to write just a single byte. - if col < Self::SCREEN_WIDTH && row < Self::SCREEN_HEIGHT { - let real_index = col + row * Self::SCREEN_WIDTH; + /// Gives `None` if out of bounds + pub fn write(page: Page, col: usize, row: usize, pal8bpp: u8) -> Option<()> { + // Note(Lokathor): Byte writes to VRAM aren't permitted, we have to jump + // through some hoops. + if col < Self::WIDTH && row < Self::HEIGHT { + let real_index = col + row * Self::WIDTH; let rounded_down_index = real_index & !1; let address: VolAddress = unsafe { - if page1 { - Self::PAGE1_BASE.offset(rounded_down_index as isize).cast() - } else { - Self::PAGE0_BASE.offset(rounded_down_index as isize).cast() + match page { + Page::Zero => Self::PAGE0_INDEXES, + Page::One => Self::PAGE1_INDEXES, } + .index_unchecked(rounded_down_index) + .cast::() }; if real_index == rounded_down_index { // even byte, change the high bits @@ -165,145 +224,237 @@ impl Mode4 { } } - /// Writes a "wide" pairing of palette entries to the location specified. + /// Clear the screen to the palette index specified. /// - /// The page is imagined to be a series of `u16` values rather than `u8` - /// values, allowing you to write two palette entries side by side as a single - /// write operation. - pub fn write_wide_pixel(page1: bool, wide_col: usize, row: usize, wide_pal8bpp: u16) -> Option<()> { - if wide_col < Self::SCREEN_WIDTH / 2 && row < Self::SCREEN_HEIGHT { - let wide_index = wide_col + row * Self::SCREEN_WIDTH / 2; - let address: VolAddress = unsafe { - if page1 { - Self::PAGE1_BASE.cast::().offset(wide_index as isize) - } else { - Self::PAGE0_BASE.cast::().offset(wide_index as isize) - } - }; - Some(address.write(wide_pal8bpp)) - } else { - None - } - } - - /// Clears the page to the desired color. - pub fn clear_page_to(page1: bool, pal8bpp: u8) { + /// Takes ~215,000 cycles (~76% of a frame) + pub fn clear_to(page: Page, pal8bpp: u8) { let pal8bpp_32 = pal8bpp as u32; let bulk_color = (pal8bpp_32 << 24) | (pal8bpp_32 << 16) | (pal8bpp_32 << 8) | pal8bpp_32; - for va in if page1 { Self::BULK_ITER1 } else { Self::BULK_ITER0 } { + let words = match page { + Page::Zero => Self::PAGE0_WORDS, + Page::One => Self::PAGE1_WORDS, + }; + for va in words.iter() { va.write(bulk_color) } } - /// Clears the page to the desired color using DMA3. - pub fn dma_clear_page_to(page1: bool, pal8bpp: u8) { + /// Clears the screen to the palette index specified using DMA3. + /// + /// Takes ~30,800 frames (~37% of VBlank) + pub fn dma_clear_to(page: Page, pal8bpp: u8) { use crate::io::dma::DMA3; let pal8bpp_32 = pal8bpp as u32; let bulk_color = (pal8bpp_32 << 24) | (pal8bpp_32 << 16) | (pal8bpp_32 << 8) | pal8bpp_32; - let write_target = if page1 { - VRAM_BASE_USIZE as *mut u32 - } else { - (VRAM_BASE_USIZE + 0xA000) as *mut u32 + let words_address = unsafe { + match page { + Page::Zero => Self::PAGE0_WORDS.index_unchecked(0).to_usize(), + Page::One => Self::PAGE1_WORDS.index_unchecked(0).to_usize(), + } }; - unsafe { DMA3::fill32(&bulk_color, write_target, Self::SCREEN_U32_COUNT as u16) }; + unsafe { DMA3::fill32(&bulk_color, words_address as *mut u32, Self::PAGE0_WORDS.len() as u16) }; + } + + /// Draws a line between the two points given `(c1,r1,c2,r2,color)`. + /// + /// Works fine with out of bounds points. It only draws to in bounds + /// locations. + pub fn draw_line(page: Page, c1: isize, r1: isize, c2: isize, r2: isize, pal8bpp: u8) { + let mut col = c1; + let mut row = r1; + let w = c2 - c1; + let h = r2 - r1; + let mut dx1 = 0; + let mut dx2 = 0; + let mut dy1 = 0; + let mut dy2 = 0; + let mut longest = w.abs(); + let mut shortest = h.abs(); + if w < 0 { + dx1 = -1; + } else if w > 0 { + dx1 = 1; + }; + if h < 0 { + dy1 = -1; + } else if h > 0 { + dy1 = 1; + }; + if w < 0 { + dx2 = -1; + } else if w > 0 { + dx2 = 1; + }; + if !(longest > shortest) { + core::mem::swap(&mut longest, &mut shortest); + if h < 0 { + dy2 = -1; + } else if h > 0 { + dy2 = 1 + }; + dx2 = 0; + } + let mut numerator = longest >> 1; + + (0..(longest + 1)).for_each(|_| { + Self::write(page, col as usize, row as usize, pal8bpp); + numerator += shortest; + if !(numerator < longest) { + numerator -= longest; + col += dx1; + row += dy1; + } else { + col += dx2; + row += dy2; + } + }); } } -//TODO: Mode4 Iter Scanlines / Pixels? -//TODO: Mode4 Line Drawing? - /// Mode 5 is a bitmap mode with full color and reduced resolution. /// /// * **Width:** 160 /// * **Height:** 128 /// * **Pages:** 2 /// -/// Because of the reduced resolution, we're allowed two pages for display. +/// Because of the reduced resolutions there's enough space to have two pages. /// -/// As with all bitmap modes, the image itself utilizes BG2 for display, so you -/// must have BG2 enabled in addition to being within Mode 3. +/// As with all the bitmap video modes, the bitmap is considered to be BG2, so +/// you have to enable BG2 as well if you want to see the bitmap. pub struct Mode5; + impl Mode5 { - /// The physical width in pixels of the GBA screen. - pub const SCREEN_WIDTH: usize = 160; + /// The screen's width in this mode. + pub const WIDTH: usize = 160; - /// The physical height in pixels of the GBA screen. - pub const SCREEN_HEIGHT: usize = 128; + /// The screen's height in this mode. + pub const HEIGHT: usize = 128; - /// The number of pixels on the screen. - pub const SCREEN_PIXEL_COUNT: usize = Self::SCREEN_WIDTH * Self::SCREEN_HEIGHT; + const PAGE0_PIXELS: VolBlock>::Output> = + unsafe { VolBlock::new(VRAM_BASE_USIZE) }; - /// Used for bulk clearing operations. - const SCREEN_U32_COUNT: usize = Self::SCREEN_PIXEL_COUNT / 2; + const PAGE1_PIXELS: VolBlock>::Output> = + unsafe { VolBlock::new(VRAM_BASE_USIZE + PAGE1_OFFSET) }; - // TODO: newtype this? - const PAGE0_BASE: VolAddress = unsafe { VolAddress::new_unchecked(VRAM_BASE_USIZE) }; + const PAGE0_WORDS: VolBlock>::Output as Div>::Output> = + unsafe { VolBlock::new(VRAM_BASE_USIZE) }; - // TODO: newtype this? - const PAGE0_BLOCK: VolAddressBlock = unsafe { VolAddressBlock::new_unchecked(Self::PAGE0_BASE, Self::SCREEN_PIXEL_COUNT) }; + const PAGE1_WORDS: VolBlock>::Output as Div>::Output> = + unsafe { VolBlock::new(VRAM_BASE_USIZE + PAGE1_OFFSET) }; - // TODO: newtype this? - const PAGE1_BASE: VolAddress = unsafe { VolAddress::new_unchecked(VRAM_BASE_USIZE + 0xA000) }; - - // TODO: newtype this? - const PAGE1_BLOCK: VolAddressBlock = unsafe { VolAddressBlock::new_unchecked(Self::PAGE1_BASE, Self::SCREEN_PIXEL_COUNT) }; - - /// private iterator over the page0 pixels, four at a time - const BULK_ITER0: VolAddressIter = unsafe { VolAddressBlock::new_unchecked(Self::PAGE0_BASE.cast::(), Self::SCREEN_U32_COUNT).iter() }; - - /// private iterator over the page1 pixels, four at a time - const BULK_ITER1: VolAddressIter = unsafe { VolAddressBlock::new_unchecked(Self::PAGE1_BASE.cast::(), Self::SCREEN_U32_COUNT).iter() }; - - /// Reads the pixel at the given (col,row). + /// Reads the color of the pixel specified. /// - /// # Failure + /// ## Failure /// - /// Gives `None` if out of bounds. - pub fn read_pixel(page1: bool, col: usize, row: usize) -> Option { - if page1 { - Self::PAGE1_BLOCK.get(col + row * Self::SCREEN_WIDTH).map(VolAddress::read) - } else { - Self::PAGE0_BLOCK.get(col + row * Self::SCREEN_WIDTH).map(VolAddress::read) + /// Gives `None` if out of bounds + pub fn read(page: Page, col: usize, row: usize) -> Option { + match page { + Page::Zero => Self::PAGE0_PIXELS, + Page::One => Self::PAGE1_PIXELS, } + .get(col + row * Self::WIDTH) + .map(VolAddress::read) } - /// Writes the pixel at the given (col,row). + /// Writes a color to the pixel specified. /// - /// # Failure + /// ## Failure /// - /// Gives `None` if out of bounds. - pub fn write_pixel(page1: bool, col: usize, row: usize, color: Color) -> Option<()> { - if page1 { - Self::PAGE1_BLOCK.get(col + row * Self::SCREEN_WIDTH).map(|va| va.write(color)) - } else { - Self::PAGE0_BLOCK.get(col + row * Self::SCREEN_WIDTH).map(|va| va.write(color)) + /// Gives `None` if out of bounds + pub fn write(page: Page, col: usize, row: usize, color: Color) -> Option<()> { + match page { + Page::Zero => Self::PAGE0_PIXELS, + Page::One => Self::PAGE1_PIXELS, } + .get(col + row * Self::WIDTH) + .map(|va| va.write(color)) } - /// Clears the whole screen to the desired color. - pub fn clear_page_to(page1: bool, color: Color) { + /// Clear the screen to the color specified. + /// + /// Takes ~215,000 cycles (~76% of a frame) + pub fn clear_to(page: Page, color: Color) { let color32 = color.0 as u32; let bulk_color = color32 << 16 | color32; - for va in if page1 { Self::BULK_ITER1 } else { Self::BULK_ITER0 } { + let words = match page { + Page::Zero => Self::PAGE0_WORDS, + Page::One => Self::PAGE1_WORDS, + }; + for va in words.iter() { va.write(bulk_color) } } - /// Clears the whole screen to the desired color using DMA3. - pub fn dma_clear_page_to(page1: bool, color: Color) { + /// Clears the screen to the color specified using DMA3. + /// + /// Takes ~30,800 frames (~37% of VBlank) + pub fn dma_clear_to(page: Page, color: Color) { use crate::io::dma::DMA3; let color32 = color.0 as u32; let bulk_color = color32 << 16 | color32; - let write_target = if page1 { - VRAM_BASE_USIZE as *mut u32 - } else { - (VRAM_BASE_USIZE + 0xA000) as *mut u32 + let words_address = unsafe { + match page { + Page::Zero => Self::PAGE0_WORDS.index_unchecked(0).to_usize(), + Page::One => Self::PAGE1_WORDS.index_unchecked(0).to_usize(), + } }; - unsafe { DMA3::fill32(&bulk_color, write_target, Self::SCREEN_U32_COUNT as u16) }; + unsafe { DMA3::fill32(&bulk_color, words_address as *mut u32, Self::PAGE0_WORDS.len() as u16) }; + } + + /// Draws a line between the two points given `(c1,r1,c2,r2,color)`. + /// + /// Works fine with out of bounds points. It only draws to in bounds + /// locations. + pub fn draw_line(page: Page, c1: isize, r1: isize, c2: isize, r2: isize, color: Color) { + let mut col = c1; + let mut row = r1; + let w = c2 - c1; + let h = r2 - r1; + let mut dx1 = 0; + let mut dx2 = 0; + let mut dy1 = 0; + let mut dy2 = 0; + let mut longest = w.abs(); + let mut shortest = h.abs(); + if w < 0 { + dx1 = -1; + } else if w > 0 { + dx1 = 1; + }; + if h < 0 { + dy1 = -1; + } else if h > 0 { + dy1 = 1; + }; + if w < 0 { + dx2 = -1; + } else if w > 0 { + dx2 = 1; + }; + if !(longest > shortest) { + core::mem::swap(&mut longest, &mut shortest); + if h < 0 { + dy2 = -1; + } else if h > 0 { + dy2 = 1 + }; + dx2 = 0; + } + let mut numerator = longest >> 1; + + (0..(longest + 1)).for_each(|_| { + Self::write(page, col as usize, row as usize, color); + numerator += shortest; + if !(numerator < longest) { + numerator -= longest; + col += dx1; + row += dy1; + } else { + col += dx2; + row += dy2; + } + }); } } - -//TODO: Mode5 Iter Scanlines / Pixels? -//TODO: Mode5 Line Drawing?