mirror of
https://github.com/italicsjenga/gba.git
synced 2024-12-23 19:01:30 +11:00
Merge pull request #65 from rust-console/lokathor
IO Registers + Bitmap book section
This commit is contained in:
commit
98a9eefaf5
|
@ -1,7 +1,7 @@
|
|||
[package]
|
||||
name = "gba"
|
||||
description = "A crate (and book) for making GBA games with Rust."
|
||||
version = "0.4.0"
|
||||
version = "0.4.0-pre"
|
||||
authors = ["Lokathor <zefria@gmail.com>", "Thomas Winwood <twwinwood@gmail.com>"]
|
||||
repository = "https://github.com/rust-console/gba"
|
||||
readme = "README.md"
|
||||
|
|
|
@ -1 +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();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
|
|
@ -1 +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<DisplayControlSetting> = 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<u16> = 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<u16> = 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.
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -26,7 +26,8 @@ fn panic(_info: &core::panic::PanicInfo) -> ! {
|
|||
|
||||
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);
|
||||
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));
|
||||
|
@ -90,8 +91,8 @@ static mut PIXEL: usize = 0;
|
|||
|
||||
fn write_pixel(color: Color) {
|
||||
unsafe {
|
||||
Mode3::write_pixel(PIXEL, 0, color);
|
||||
PIXEL = (PIXEL + 1) % Mode3::SCREEN_PIXEL_COUNT;
|
||||
Mode3::write(PIXEL, 0, color);
|
||||
PIXEL = (PIXEL + 1) % (Mode3::WIDTH * Mode3::HEIGHT);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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"
|
||||
|
|
|
@ -19,10 +19,7 @@ pub struct Fx<T, F: Unsigned> {
|
|||
impl<T, F: Unsigned> Fx<T, F> {
|
||||
/// 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<T, F: Unsigned> Fx<T, F> {
|
|||
|
||||
/// Casts the base type, keeping the fractional bit quantity the same.
|
||||
pub fn cast_inner<Z, C: Fn(T) -> Z>(self, op: C) -> Fx<Z, F> {
|
||||
Fx {
|
||||
num: op(self.num),
|
||||
phantom: PhantomData,
|
||||
}
|
||||
Fx { num: op(self.num), phantom: PhantomData }
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Add<Output = T>, F: Unsigned> Add for Fx<T, F> {
|
||||
type Output = Self;
|
||||
fn add(self, rhs: Fx<T, F>) -> Self::Output {
|
||||
Fx {
|
||||
num: self.num + rhs.num,
|
||||
phantom: PhantomData,
|
||||
}
|
||||
Fx { num: self.num + rhs.num, phantom: PhantomData }
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Sub<Output = T>, F: Unsigned> Sub for Fx<T, F> {
|
||||
type Output = Self;
|
||||
fn sub(self, rhs: Fx<T, F>) -> Self::Output {
|
||||
Fx {
|
||||
num: self.num - rhs.num,
|
||||
phantom: PhantomData,
|
||||
}
|
||||
Fx { num: self.num - rhs.num, phantom: PhantomData }
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Shl<u32, Output = T>, F: Unsigned> Shl<u32> for Fx<T, F> {
|
||||
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<T: Shr<u32, Output = T>, F: Unsigned> Shr<u32> for Fx<T, F> {
|
||||
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<T: Neg<Output = T>, F: Unsigned> Neg for Fx<T, F> {
|
||||
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<F: Unsigned> 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 }
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
|
@ -6,6 +6,7 @@ use super::*;
|
|||
pub const BLDCNT: VolAddress<ColorEffectSetting> = unsafe { VolAddress::new(0x400_0050) };
|
||||
|
||||
newtype! {
|
||||
/// TODO: docs
|
||||
ColorEffectSetting, u16
|
||||
}
|
||||
|
||||
|
@ -29,10 +30,15 @@ impl ColorEffectSetting {
|
|||
}
|
||||
|
||||
newtype_enum! {
|
||||
/// TODO: docs
|
||||
ColorSpecialEffect = u16,
|
||||
/// TODO: docs
|
||||
None = 0,
|
||||
/// TODO: docs
|
||||
AlphaBlending = 1,
|
||||
/// TODO: docs
|
||||
BrightnessIncrease = 2,
|
||||
/// TODO: docs
|
||||
BrightnessDecrease = 3,
|
||||
}
|
||||
|
||||
|
@ -40,6 +46,7 @@ newtype_enum! {
|
|||
pub const BLDALPHA: VolAddress<AlphaBlendingSetting> = unsafe { VolAddress::new(0x400_0052) };
|
||||
|
||||
newtype! {
|
||||
/// TODO: docs
|
||||
AlphaBlendingSetting, u16
|
||||
}
|
||||
|
||||
|
@ -55,6 +62,7 @@ impl AlphaBlendingSetting {
|
|||
pub const BLDY: VolAddress<BrightnessSetting> = unsafe { VolAddress::new(0x400_0054) };
|
||||
|
||||
newtype! {
|
||||
/// TODO: docs
|
||||
BrightnessSetting, u32
|
||||
}
|
||||
|
||||
|
|
|
@ -122,42 +122,11 @@ 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<u16> = unsafe { VolAddress::new(0x400_0006) };
|
||||
pub const VCOUNT: ROVolAddress<u16> = 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.
|
||||
///
|
||||
/// NOTE: This method isn't very power efficient, since it is equivalent to
|
||||
/// calling "halt" repeatedly. The recommended way to wait for a VBlank or VDraw
|
||||
/// is to set an IRQ handler with
|
||||
/// [`io::irq::set_irq_handler`](`io::irq::set_irq_handler`) and using
|
||||
/// [`bios::vblank_intr_wait`](bios::vblank_interrupt_wait) to sleep the CPU
|
||||
/// until a VBlank IRQ is generated. See the [`io::irq`](io::irq) module for
|
||||
/// more details.
|
||||
pub fn spin_until_vblank() {
|
||||
while vcount() < VBLANK_SCANLINE {}
|
||||
}
|
||||
|
||||
/// Performs a busy loop until VDraw starts.
|
||||
///
|
||||
/// NOTE: This method isn't very power efficient, since it is equivalent to
|
||||
/// calling "halt" repeatedly. The recommended way to wait for a VBlank or VDraw
|
||||
/// is to set an IRQ handler with
|
||||
/// [`io::irq::set_irq_handler`](`io::irq::set_irq_handler`) and using
|
||||
/// [`bios::vblank_intr_wait`](bios::vblank_interrupt_wait) to sleep the CPU
|
||||
/// until a VBlank IRQ is generated. See the [`io::irq`](io::irq) module for
|
||||
/// more details.
|
||||
pub fn spin_until_vdraw() {
|
||||
while vcount() >= VBLANK_SCANLINE {}
|
||||
}
|
||||
|
||||
/// Global mosaic effect control. Write-only.
|
||||
pub const MOSAIC: VolAddress<MosaicSetting> = unsafe { VolAddress::new(0x400_004C) };
|
||||
|
||||
|
|
|
@ -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<u16> = unsafe { VolAddress::new(0x400_0130) };
|
||||
pub const KEYINPUT: ROVolAddress<u16> = 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() {
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
///! Module for sound registers.
|
||||
//! Module for sound registers.
|
||||
|
||||
use super::*;
|
||||
|
||||
//TODO within these "read/write" registers only some bits are actually read/write!
|
||||
|
@ -7,6 +8,7 @@ use super::*;
|
|||
pub const SOUND1CNT_L: VolAddress<SweepRegisterSetting> = unsafe { VolAddress::new(0x400_0060) };
|
||||
|
||||
newtype! {
|
||||
/// TODO: docs
|
||||
SweepRegisterSetting, u16
|
||||
}
|
||||
|
||||
|
@ -23,6 +25,7 @@ impl SweepRegisterSetting {
|
|||
pub const SOUND1CNT_H: VolAddress<DutyLenEnvelopeSetting> = unsafe { VolAddress::new(0x400_0062) };
|
||||
|
||||
newtype! {
|
||||
/// TODO: docs
|
||||
DutyLenEnvelopeSetting, u16
|
||||
}
|
||||
|
||||
|
@ -41,6 +44,7 @@ impl DutyLenEnvelopeSetting {
|
|||
pub const SOUND1CNT_X: VolAddress<FrequencyControlSetting> = unsafe { VolAddress::new(0x400_0064) };
|
||||
|
||||
newtype! {
|
||||
/// TODO: docs
|
||||
FrequencyControlSetting, u32 // TODO: u16 or u32?
|
||||
}
|
||||
|
||||
|
@ -60,9 +64,11 @@ pub const SOUND2CNT_L: VolAddress<DutyLenEnvelopeSetting> = unsafe { VolAddress:
|
|||
pub const SOUND2CNT_H: VolAddress<FrequencyControlSetting> = unsafe { VolAddress::new(0x400_006C) };
|
||||
|
||||
/// Sound Channel 3 Stop/Wave RAM select (`NR23`, `NR24`). Read/Write.
|
||||
pub const SOUND3CNT_L: VolAddress<StopWaveRAMSelectSetting> = unsafe { VolAddress::new(0x400_0070) };
|
||||
pub const SOUND3CNT_L: VolAddress<StopWaveRAMSelectSetting> =
|
||||
unsafe { VolAddress::new(0x400_0070) };
|
||||
|
||||
newtype! {
|
||||
/// TODO: docs
|
||||
StopWaveRAMSelectSetting, u16
|
||||
}
|
||||
|
||||
|
@ -79,6 +85,7 @@ impl StopWaveRAMSelectSetting {
|
|||
pub const SOUND3CNT_H: VolAddress<LengthVolumeSetting> = unsafe { VolAddress::new(0x400_0072) };
|
||||
|
||||
newtype! {
|
||||
/// TODO: docs
|
||||
LengthVolumeSetting, u16
|
||||
}
|
||||
|
||||
|
@ -115,6 +122,7 @@ pub const WAVE_RAM3_H: VolAddress<u16> = unsafe { VolAddress::new(0x400_009E) };
|
|||
pub const SOUND4CNT_L: VolAddress<LengthEnvelopeSetting> = unsafe { VolAddress::new(0x400_0078) };
|
||||
|
||||
newtype! {
|
||||
/// TODO: docs
|
||||
LengthEnvelopeSetting, u32 // TODO: is this u32?
|
||||
}
|
||||
|
||||
|
@ -132,6 +140,7 @@ impl LengthEnvelopeSetting {
|
|||
pub const SOUND4CNT_H: VolAddress<NoiseFrequencySetting> = unsafe { VolAddress::new(0x400_007C) };
|
||||
|
||||
newtype! {
|
||||
/// TODO: docs
|
||||
NoiseFrequencySetting, u32 // TODO: is this u32?
|
||||
}
|
||||
|
||||
|
@ -158,9 +167,11 @@ pub const FIFO_B_L: VolAddress<u16> = unsafe { VolAddress::new(0x400_00A4) };
|
|||
pub const FIFO_B_H: VolAddress<u16> = unsafe { VolAddress::new(0x400_00A6) };
|
||||
|
||||
/// Channel L/R Volume/Enable (`NR50`, `NR51`). Read/Write.
|
||||
pub const SOUNDCNT_L: VolAddress<NonWaveVolumeEnableSetting> = unsafe { VolAddress::new(0x400_0080) };
|
||||
pub const SOUNDCNT_L: VolAddress<NonWaveVolumeEnableSetting> =
|
||||
unsafe { VolAddress::new(0x400_0080) };
|
||||
|
||||
newtype! {
|
||||
/// TODO: docs
|
||||
NonWaveVolumeEnableSetting, u16
|
||||
}
|
||||
|
||||
|
@ -178,6 +189,7 @@ impl NonWaveVolumeEnableSetting {
|
|||
pub const SOUNDCNT_H: VolAddress<WaveVolumeEnableSetting> = unsafe { VolAddress::new(0x400_0082) };
|
||||
|
||||
newtype! {
|
||||
/// TODO: docs
|
||||
WaveVolumeEnableSetting, u16
|
||||
}
|
||||
|
||||
|
@ -199,9 +211,13 @@ impl WaveVolumeEnableSetting {
|
|||
}
|
||||
|
||||
newtype_enum! {
|
||||
/// TODO: docs
|
||||
NumberSoundVolume = u16,
|
||||
/// TODO: docs
|
||||
Quarter = 0,
|
||||
/// TODO: docs
|
||||
Half = 1,
|
||||
/// TODO: docs
|
||||
Full = 2,
|
||||
}
|
||||
|
||||
|
@ -209,6 +225,7 @@ newtype_enum! {
|
|||
pub const SOUNDCNT_X: VolAddress<SoundMasterSetting> = unsafe { VolAddress::new(0x400_0084) };
|
||||
|
||||
newtype! {
|
||||
/// TODO: docs
|
||||
SoundMasterSetting, u16
|
||||
}
|
||||
|
||||
|
@ -227,6 +244,7 @@ impl SoundMasterSetting {
|
|||
pub const SOUNDBIAS: VolAddress<SoundPWMSetting> = unsafe { VolAddress::new(0x400_0088) };
|
||||
|
||||
newtype! {
|
||||
/// TODO: docs
|
||||
SoundPWMSetting, u16
|
||||
}
|
||||
|
||||
|
|
|
@ -9,6 +9,7 @@ pub const WIN0H: VolAddress<HorizontalWindowSetting> = unsafe { VolAddress::new(
|
|||
pub const WIN1H: VolAddress<HorizontalWindowSetting> = unsafe { VolAddress::new(0x400_0042) };
|
||||
|
||||
newtype! {
|
||||
/// TODO: docs
|
||||
HorizontalWindowSetting, u16
|
||||
}
|
||||
|
||||
|
@ -27,6 +28,7 @@ pub const WIN0V: VolAddress<VerticalWindowSetting> = unsafe { VolAddress::new(0x
|
|||
pub const WIN1V: VolAddress<VerticalWindowSetting> = unsafe { VolAddress::new(0x400_0046) };
|
||||
|
||||
newtype! {
|
||||
/// TODO: docs
|
||||
VerticalWindowSetting, u16
|
||||
}
|
||||
|
||||
|
@ -42,6 +44,7 @@ impl VerticalWindowSetting {
|
|||
pub const WININ: VolAddress<InsideWindowSetting> = unsafe { VolAddress::new(0x400_0048) };
|
||||
|
||||
newtype! {
|
||||
/// TODO: docs
|
||||
InsideWindowSetting, u16
|
||||
}
|
||||
|
||||
|
@ -67,6 +70,7 @@ impl InsideWindowSetting {
|
|||
pub const WINOUT: VolAddress<OutsideWindowSetting> = unsafe { VolAddress::new(0x400_004A) };
|
||||
|
||||
newtype! {
|
||||
/// TODO: docs
|
||||
OutsideWindowSetting, u16
|
||||
}
|
||||
|
||||
|
|
97
src/lib.rs
97
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,91 +19,9 @@
|
|||
//! do, it's a giant bag of Undefined Behavior.
|
||||
|
||||
pub(crate) use gba_proc_macro::phantom_fields;
|
||||
pub(crate) use voladdress::{VolAddress, VolBlock};
|
||||
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;
|
||||
|
||||
|
@ -142,7 +60,7 @@ extern "C" {
|
|||
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 {
|
||||
|
@ -153,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))
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
|
|
203
src/macros.rs
Normal file
203
src/macros.rs
Normal file
|
@ -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
|
||||
}};
|
||||
}
|
|
@ -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 {
|
||||
|
|
|
@ -29,11 +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: VolBlock<[u8; 0x4000], U6> = unsafe { VolBlock::new(VRAM_BASE_USIZE) };
|
||||
|
||||
/// The screen entry base blocks.
|
||||
pub const SCREEN_BASE_BLOCKS: VolBlock<[u8; 0x800], U32> = unsafe { VolBlock::new(VRAM_BASE_USIZE) };
|
||||
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.
|
||||
|
|
|
@ -1,155 +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<Color, <U256 as Mul<U160>>::Output> =
|
||||
unsafe { VolBlock::new(VRAM_BASE_USIZE) };
|
||||
|
||||
/// The Mode 3 VRAM.
|
||||
const WORDS_BLOCK: VolBlock<u32, <<U256 as Mul<U160>>::Output as Div<U2>>::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: VolBlock<Color, <U256 as Mul<U160>>::Output> = unsafe { VolBlock::new(VRAM_BASE_USIZE) };
|
||||
|
||||
/// private iterator over the pixels, two at a time
|
||||
const VRAM_BULK: VolBlock<u32, <<U256 as Mul<U160>>::Output as Div<U2>>::Output> = unsafe { VolBlock::new(VRAM_BASE_USIZE) };
|
||||
|
||||
/// 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<Color> {
|
||||
Self::VRAM.get(col + row * Self::SCREEN_WIDTH).map(VolAddress::read)
|
||||
/// Gives `None` if out of bounds
|
||||
fn get(col: usize, row: usize) -> Option<VolAddress<Color>> {
|
||||
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<Color> {
|
||||
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::VRAM_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<u8, <U256 as Mul<U160>>::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<u8, <U256 as Mul<U160>>::Output> =
|
||||
unsafe { VolBlock::new(VRAM_BASE_USIZE + PAGE1_OFFSET) };
|
||||
|
||||
// TODO: newtype this?
|
||||
const PAGE0_BLOCK8: VolBlock<u8, <U256 as Mul<U160>>::Output> = unsafe { VolBlock::new(VRAM_BASE_USIZE) };
|
||||
const PAGE0_WORDS: VolBlock<u32, <<U256 as Mul<U160>>::Output as Div<U4>>::Output> =
|
||||
unsafe { VolBlock::new(VRAM_BASE_USIZE) };
|
||||
|
||||
// TODO: newtype this?
|
||||
const PAGE1_BLOCK8: VolBlock<u8, <U256 as Mul<U160>>::Output> = unsafe { VolBlock::new(VRAM_BASE_USIZE + 0xA000) };
|
||||
const PAGE1_WORDS: VolBlock<u32, <<U256 as Mul<U160>>::Output as Div<U4>>::Output> =
|
||||
unsafe { VolBlock::new(VRAM_BASE_USIZE + PAGE1_OFFSET) };
|
||||
|
||||
// TODO: newtype this?
|
||||
const PAGE0_BLOCK16: VolBlock<u16, <<U256 as Mul<U160>>::Output as Div<U2>>::Output> = unsafe { VolBlock::new(VRAM_BASE_USIZE) };
|
||||
|
||||
// TODO: newtype this?
|
||||
const PAGE1_BLOCK16: VolBlock<u16, <<U256 as Mul<U160>>::Output as Div<U2>>::Output> = unsafe { VolBlock::new(VRAM_BASE_USIZE + 0xA000) };
|
||||
|
||||
/// private iterator over the page0 pixels, four at a time
|
||||
const PAGE0_BULK32: VolBlock<u32, <<U256 as Mul<U160>>::Output as Div<U4>>::Output> = unsafe { VolBlock::new(VRAM_BASE_USIZE) };
|
||||
|
||||
/// private iterator over the page1 pixels, four at a time
|
||||
const PAGE1_BULK32: VolBlock<u32, <<U256 as Mul<U160>>::Output as Div<U4>>::Output> = unsafe { VolBlock::new(VRAM_BASE_USIZE + 0xA000) };
|
||||
|
||||
/// 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<u8> {
|
||||
// Note(Lokathor): byte _reads_ from VRAM are okay.
|
||||
if page1 {
|
||||
Self::PAGE1_BLOCK8.get(col + row * Self::SCREEN_WIDTH).map(VolAddress::read)
|
||||
} else {
|
||||
Self::PAGE0_BLOCK8.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<u8> {
|
||||
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<u16> = unsafe {
|
||||
if page1 {
|
||||
Self::PAGE1_BLOCK8.index(rounded_down_index).cast()
|
||||
} else {
|
||||
Self::PAGE0_BLOCK8.index(rounded_down_index).cast()
|
||||
match page {
|
||||
Page::Zero => Self::PAGE0_INDEXES,
|
||||
Page::One => Self::PAGE1_INDEXES,
|
||||
}
|
||||
.index_unchecked(rounded_down_index)
|
||||
.cast::<u16>()
|
||||
};
|
||||
if real_index == rounded_down_index {
|
||||
// even byte, change the high bits
|
||||
|
@ -166,137 +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<u16> = if page1 {
|
||||
Self::PAGE1_BLOCK16.index(wide_index)
|
||||
} else {
|
||||
Self::PAGE0_BLOCK16.index(wide_index)
|
||||
};
|
||||
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::PAGE1_BULK32 } else { Self::PAGE0_BULK32 }).iter() {
|
||||
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<Color, <U160 as Mul<U128>>::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<Color, <U160 as Mul<U128>>::Output> =
|
||||
unsafe { VolBlock::new(VRAM_BASE_USIZE + PAGE1_OFFSET) };
|
||||
|
||||
// TODO: newtype this?
|
||||
const PAGE0_BLOCK: VolBlock<Color, <U160 as Mul<U128>>::Output> = unsafe { VolBlock::new(VRAM_BASE_USIZE) };
|
||||
const PAGE0_WORDS: VolBlock<u32, <<U160 as Mul<U128>>::Output as Div<U2>>::Output> =
|
||||
unsafe { VolBlock::new(VRAM_BASE_USIZE) };
|
||||
|
||||
// TODO: newtype this?
|
||||
const PAGE1_BLOCK: VolBlock<Color, <U160 as Mul<U128>>::Output> = unsafe { VolBlock::new(VRAM_BASE_USIZE + 0xA000) };
|
||||
const PAGE1_WORDS: VolBlock<u32, <<U160 as Mul<U128>>::Output as Div<U2>>::Output> =
|
||||
unsafe { VolBlock::new(VRAM_BASE_USIZE + PAGE1_OFFSET) };
|
||||
|
||||
/// private iterator over the page0 pixels, four at a time
|
||||
const PAGE0_BULK32: VolBlock<u32, <<U160 as Mul<U128>>::Output as Div<U2>>::Output> = unsafe { VolBlock::new(VRAM_BASE_USIZE) };
|
||||
|
||||
/// private iterator over the page1 pixels, four at a time
|
||||
const PAGE1_BULK32: VolBlock<u32, <<U160 as Mul<U128>>::Output as Div<U2>>::Output> = unsafe { VolBlock::new(VRAM_BASE_USIZE + 0xA000) };
|
||||
|
||||
/// 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<Color> {
|
||||
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<Color> {
|
||||
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::PAGE1_BULK32 } else { Self::PAGE0_BULK32 }).iter() {
|
||||
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?
|
||||
|
|
Loading…
Reference in a new issue