In this lab, you will be getting vengeance for lab 2 and making a real drawing program that uses the mouse (or trackpad, or stylus, or whatever) to draw on the screen.
A drawing program is conceptually not very complicated. It can be as simple as:
- get the current position of the mouse.
- if the user is holding down the mouse button, draw a pixel at that position.
- repeat.
But ours will be a little better than that.
0. Getting started
Here are the include files for this lab, as a zip file..
- download it.
- if it’s still compressed, right-click and “Decompress” or “Extract” or whatever.
- make a folder for lab 3 and move all the
.asm
files that came out of the zip file into it. - make a new file in MARS.
- save it in the same folder as the include files as
abc123_lab4.asm
whereabc123
is your username. - copy and paste this code into your
_lab4.asm
file, and put your name and username in the comments at the top
# it is very important that you do not put any code or variables
# before this .include line. you can put comments, but nothing else.
.include "display_2244_0203.asm"
.include "lab4_graphics.asm"
.eqv MODE_NOTHING 0
.eqv MODE_BRUSH 1
.eqv MODE_COLOR 2
.eqv PALETTE_X 32
.eqv PALETTE_Y 56
.eqv PALETTE_X2 96
.eqv PALETTE_Y2 72
.data
drawmode: .word MODE_NOTHING
last_x: .word -1
last_y: .word -1
color: .word 0b111111 # 3,3,3 (white)
.text
.global main
main:
Good lord, what IS all this stuff?
On labs 2 and 3, you were given some very simplified include files that had only what was needed to do those labs. This time, you have been given the entire display driver, or at least, everything I’ve written to this point.
display_2244_0203.asm
is the main file. it has a ton of functions. most of them, you won’t even use this lab.display_vars_2244_0203.asm
declares all the MMIO variables for interacting with the display.display_constants_2244_0203.asm
is a bunch of useful constants, including all the keyboard keys.macros.asm
is our growing collection of useful macros.- and finally,
lab4_graphics.asm
is the graphical assets needed, just like lab 3.
Don’t get too overwhelmed. Again, most of the functions you’ve been given, you won’t need to use. (But you’re free to look at them… :) )
1. main
and load_graphics
This starts very similarly to lab 3, so refer back to what you did there.
main
needs to:
- call
display_init(15, 1, 0)
- Yes, this time
display_init
has arguments! - The first argument is the number of milliseconds per frame. 15 ms is approximately 60 frames/second.
- The second is a boolean to enable the framebuffer, the thing you used on lab 2.
- The third is a boolean to disable the tilemap, the thing you used on lab 3.
- Yes, this time
- call
load_graphics
which you will be writing shortly - loop infinitely this time, and in the loop:
- call
check_input
- call
draw_cursor
- call
display_finish_frame
- call
Simple stuff.
load_graphics
needs to:
- call
display_load_sprite_gfx(cursor_gfx, CURSOR_TILE, N_CURSOR_TILES)
- this time, I gave you constants for the second and third arguments
- but it works just like lab 3.
- call
display_load_sprite_gfx(palette_sprite_gfx, PALETTE_TILE, N_PALETTE_TILES)
Finally, stub out the check_input
and draw_cursor
functions like you learned to do on lab 3, and check that your program assembles and runs. (You’ll have to hit the stop button to end it.)
drawmode
and the MODE_
constants
(This is a high-level explanation of what you’ll be building. You aren’t expected to be able to write all the code by only reading this. hashtag keep reading)
The drawmode
variable is going to affect the behavior of our program in a big way. Depending on what drawmode
is, the program will process input differently (i.e. do different things in check_input
).
It starts off as MODE_NOTHING
. This is the “default” mode that means we’re not doing anything. In this mode we can:
- start drawing with the “brush” by clicking the mouse (which puts us in
MODE_BRUSH
) - get the color under the cursor by Alt+clicking
- choose a new color by hitting
C
(which puts us inMODE_COLOR
) - do a “flood fill” (“bucket tool”) by hitting
F
In MODE_BRUSH
, we are holding down the mouse button and drawing. In this mode we just:
- stop drawing if the mouse button is released (i.e. go back to
MODE_NOTHING
)
Finally in MODE_COLOR
, we:
- check if the user clicked on the palette. if they did,
- change the drawing color,
- and switch back to
MODE_NOTHING
.
If this isn’t making a ton of sense, let’s look at something a bit more visual.
The shape of the program
This time, I am leaving it more up to you to split up your program into functions that make sense. But I’m not leaving you completely on your own. Here is a call graph of my solution. All the functions outside the yellow rectangle are my lab implementation. (display_init
and display_finish_frame
are not shown to make the diagram smaller):
You can see there are some functions that call several other functions (e.g. main
, check_input
); some that are called by several other functions (e.g. display_get_pixel
, display_draw_line
); and even a recursive function (flood_fill_rec
).
You are free to follow my program structure or do your own. Putting everything in 2 or 3 enormous functions will lose you points, and it will make it way harder for you to write, too.
You can see that check_input
calls one of my three drawmode_
functions, based on the current value of drawmode
. You can also see the various things that drawmode_nothing
calls, and can probably start to get an idea for what will happen in each function.
2. draw_cursor
Just like lab 3, you need to draw a cursor on the screen. Unlike lab 3, you’ll be using the mouse position as the cursor’s position instead of using your own variables and keyboard arrows to move it.
The MMIO variables for the mouse’s position are described in detail below. Your draw_cursor
function will be almost exactly the same as lab 3’s, with a couple changes:
- the coordinates will be
display_mouse_x - 3
anddisplay_mouse_y - 3
- the tile number will be
CURSOR_TILE
instead of 0
Done right, you should be able to see a crosshair cursor like in the image when you move your mouse over the display, and it should “stick to” your mouse cursor.
Also, when you move your mouse cursor off the display, it should disappear.
How to get mouse input in the display
There are two variables for getting the mouse position:
display_mouse_x
display_mouse_y
If you lw
from them, you will get the coordinates of the mouse measured from the top-left of the display. Both X and Y range [0, 127]. However, if the mouse cursor is outside of the display, both variables will instead be -1
. You only have to check one: either they’re both -1
or neither is.
There is a minor bug that happens when you click and drag on the display, which allows the display_mouse_x/y
variables to go outside the range [0, 127] if you drag the cursor off the edges of the display. I’m not 100% sure why it happens, but it’s easy to work around. Failing to work around it can cause strange crashes if you aren’t careful with what you do with those invalid coordinates.
For getting the mouse buttons, it works a little differently from keyboard input. Since there are only three buttons, you can get their state all at once, and use bitwise AND to extract the state of just one. For example:
lw t0, display_mouse_pressed # get the state of *all* buttons
and t0, t0, MOUSE_LBUTTON # use bitwise AND to extract just the left button
beq t0, 0, _endif_lbutton # 0 means not pressed, not-0 means pressed
# here, the left mouse button is pressed. respond to it!
_endif_lbutton:
The mouse button variables are:
display_mouse_pressed
- to check if a button went from not-held to held this frame (just pressed it)display_mouse_held
- to check if a button is being held down or notdisplay_mouse_released
- to check if a button went from held to not-held this frame (just let go)
And the constants you use in the bitwise AND are:
MOUSE_LBUTTON
- left button (trackpad normal click)MOUSE_RBUTTON
- right button (trackpad two-finger click or bottom-right click)MOUSE_MBUTTON
- middle button (scroll wheel click; trackpad three-finger click sometimes)
3. check_input
and the drawmode_
functions
Although it’s called check_input
it kinda ends up doing… most stuff. Lol. (Hey, there is a “more elegant” way to do this but it ends up being a bunch of extra complexity for not a lot of benefit in this case.)
All my check_input
does is essentially switch on the drawmode
variable and based on which value it is, calls one of drawmode_nothing
, drawmode_brush
, or drawmode_color
.
You already know enough to do this. You know how to make a switch-case, you know how to call functions, you know how to stub them out. Go do it. But,
STOP DOING THIS:
li t1, MODE_NOTHING # WHY????????
beq t0, t1, _case_nothing # SO CONFUSINGGGGGGGGG
START DOING THIS:
beq t0, MODE_NOTHING, _case_nothing
IT’S SHORTER AND EASIER TO READ AAAAAAAAAAAAA
…
Okay I’m normal now. Go test your program and make sure it doesn’t crash or something. Maybe put a test print in each of the three drawmode_
functions so you know which one is running, change the drawmode
variable’s initializer to make sure all three modes work, that kinda thing. Test your foundation before building anything else on top of it.
4. The brush
The brush is the most basic drawing tool. You click and drag to draw with a 1-pixel-wide “brush.” When you let go, it stops drawing.
Here’s what you need to do:
- when
drawmode == MODE_NOTHING
(i.e. inside yourdrawmode_nothing
function),- if the user presses the left mouse button (see the section above):
- set
drawmode
toMODE_BRUSH
- draw a 1-pixel-long line at the current mouse coordinates (see below) in the current
color
- (both ends of the line will be the current mouse coordinates)
- and then set the
last_x
andlast_y
to the current mouse coordinates.
- set
- if the user presses the left mouse button (see the section above):
- when
drawmode == MODE_BRUSH
,- if the user releases the left mouse button, OR if the mouse coordinates go offscreen (see below):
- set
drawmode
toMODE_NOTHING
- set
- else if the current mouse x !=
last_x
, OR the current mouse y !=last_y
:- draw a line from
last_x/y
to the current mouse coordinates in the currentcolor
- and then set the
last_x
andlast_y
to the current mouse coordinates.
- draw a line from
- if the user releases the left mouse button, OR if the mouse coordinates go offscreen (see below):
display_draw_line
This is one of the functions in the display driver. It draws a line of any color, at any angle, between two points. It’s easy to use but it is a little weird, because it takes five arguments.
In order to pass the fifth argument, I did something a little bad: I used v1
to pass the fifth argument. What! We’re not using it for anything else!!
So the arguments to this function are:
a0, a1
- the x, y coordinates of one enda2, a3
- the x, y coordinates of the other endv1
- the color to draw with
In this case you are going to use the value of the variable color
for that last argument. It is initialized to 0b111111
- decimal 63 - which is white.
Logical OR ||
In Java we can just write two conditions with ||
to check them both and do something if either one is true. We don’t have that in asm. We have to build it out of branches.
Let’s say I want to do something like if(x == 5 || x == 10)
. I’d write it like a weird switch-case:
lw t0, x
# it's like you're checking for two cases...
beq t0, 5, _then
beq t0, 10, _then
j _endif # and then you have a default that goes to the endif.
_then:
print_str "it's either 5 or 10!\n"
# now when you get here, think to yourself: do you want to
# leave this if and *run the next one?* or do you want to JUMP
# to *after* all the ifs? look at the description above.
_endif:
Offscreen coordinates
This is where that small bug pops up, but we will work around it by doing the following.
Due to a small bug, if the mouse goes offscreen (i.e. off the edges of the display), the values of display_mouse_x/y
can leave the range [0, 127]. So really, that first if
in the description above is:
if(user_released_lbutton ||
display_mouse_x < 0 ||
display_mouse_x > 127 ||
display_mouse_y < 0 ||
display_mouse_y > 127) {
drawmode = MODE_NOTHING;
}
So you’ve got 5 things to check for. 5 branches. ez.
Once you’ve got it implemented correctly, the brush should work like so:
Important things to test that this animation shows:
- Just clicking and letting go should draw a single dot.
- If it doesn’t, you aren’t properly drawing a 1-pixel-long line when the user first presses the button.
- Clicking and dragging should draw, and letting go should stop drawing.
- If you can only draw single pixels but can’t draw lines, you might not be switching to
MODE_BRUSH
when the user presses the button. - And if you can’t stop drawing when you let go, you might not be switching back to
MODE_NOTHING
when the user releases the button.
- If you can only draw single pixels but can’t draw lines, you might not be switching to
- Clicking and dragging off the display and then back on - in any direction - should stop drawing
- yes the drawn line might stop a little before the edge of the display, it’s fine, don’t worry
- But importantly, clicking and dragging off the top of the display a good ways should NOT crash.
- And finally, clicking and dragging off the bottom of the display a good ways should NOT cause the display to become unresponsive 🙃
- Yeah this is the real bad one. This is what you wanna avoid.
- If this happens to you, the only thing you can do is restart MARS.
5. Changing colors
White is boring! We actually have a palette of 64 colors that we can draw with. Let’s make that happen. Here’s what you need to do:
- when
drawmode == MODE_NOTHING
(so, add anotherif
indrawmode_nothing
):- if the user presses the C key,
- set
drawmode
toMODE_COLOR
, - and show the palette sprites (see below)
- set
- if the user presses the C key,
- when
drawmode == MODE_COLOR
,- if the user presses the left mouse button AND the current mouse coordinates are within the palette (see below),
- set
color
based on the coordinates (see below), - hide the palette sprites (by setting their flags to 0),
- and set
drawmode
toMODE_NOTHING
.
- set
- if the user presses the left mouse button AND the current mouse coordinates are within the palette (see below),
Showing the palette sprites
There are 16 palette tiles. You will be drawing those 16 tiles in an 8x2 arrangement like the diagram to the right. Gee, with an 8x2 rectangle, what kind of looping structure do you think you’ll have? (Remember load_map
on lab 3?)
- The
PALETTE_X/Y
constants are the location of the top-left of the palette. - The numbers in the diagram to the right are what you add to
PALETTE_TILE
for that sprite’s tile number. So 4 means that sprite uses tilePALETTE_TILE + 4
. - Each sprite is 8x8 pixels in size.
- Each sprite’s flags should be set to just 1, not 0x41.
- Remember that sprite 0 is your cursor, so don’t start there.
So just to get you started,
la t9, display_spr_table
add t9, t9, 4
# now t9 is pointing at sprite 1, so we don't
# overwrite the cursor sprite
# inside your...
# nested for loop...
# t0 = blah blah calculate the X coordinate somehow
sb t0, 0(t9)
# then calculate the Y
sb t0, 1(t9)
# then calculate which tile to use
sb t0, 2(t9)
# and finally the flags are just 1
li t0, 1
sb t0, 3(t9)
# finally, add 4 to t9 to move to the next sprite.
add t9, t9, 4
# inner loop increment and branch here
# outer loop increment and branch here
To test it out, just click on the display and hit the C
key. The palette should then appear:
Clicking on the palette and logical AND &&
PALETTE_X/Y
and PALETTE_X2/Y2
define a rectangle where the user can click inside to pick the color. In Java we’d write this check as:
if(x >= PALETTE_X && x < PALETTE_X2 && y >= PALETTE_Y && y < PALETTE_X2) {
// (x,y) is inside the palette!
}
You can think of &&
as a nested if:
if(x >= PALETTE_X) {
if(x < PALETTE_X2) {
if(y >= PALETTE_Y) {
if(y < PALETTE_X2) {
// (x,y) is inside the palette!
} } } } // I'm putting them all on one line for a reason
So in MIPS this would be implemented as 4 branches in a row, which all branch to the endif, and you only need one endif label for all 4. And of course, you need to invert the conditions. (It might make more sense if you think of it like, “if the mouse x is less than PALETTE_X
, then it’s to the left of the rectangle and can’t be inside, so skip the if.” and so on.)
Converting from mouse coordinates to a color value
This is a little weird, but just trust me. Here is some pseudocode.
// x and y can be registers, I'm just using names here for clarity.
x = display_mouse_x - PALETTE_X;
y = display_mouse_y - PALETTE_Y;
color = (x / 4 + (y / 4) * 16); // do NOT simplify!
You might see (y / 4) * 16
above and want to simplify it to y * 4
, but remember that we are using integer division, so the result of dividing then multiplying is not the same as just multiplying. What is this code doing? Well… let’s track the range of the y coordinate as we calculate:
y = display_mouse_y; // [56, 71] (since the conditions were met)
y = y - PALETTE_Y; // [0, 15] (PALETTE_Y == 56)
y = y / 4; // [0, 3]
y = y * 16; // can only be one of 4 values: { 0, 16, 32, 48 }
Try tracking the range of x. You’ll see that x + y
will always be in the range [0, 63].
Now, you should be able to change the drawing color:
Important things to test that this animation shows:
- Clicking anywhere outside the palette should do nothing.
- Clicking on the palette makes it disappear and we can resume drawing, but in the new color.
6. A couple “nice-to-have” features
Now you can draw in any color you like. But having to open the palette with C
every time you want to switch colors is annoying, especially when the color you wanna use is like… right there on the canvas…
Most drawing programs have an “eyedropper” tool that lets you pick up a color that is already in the drawing. It’s extremely easy to implement, we just need to make a little helper function first.
display_get_pixel
If you open display_2244_0203.asm
and go down to line 167, you’ll find display_set_pixel
. Here’s what you need to do to make display_get_pixel
:
- Copy the whole
display_set_pixel
function. - Paste it into your program, and rename it
display_get_pixel
. - Remove the 4 branches and the
_return:
label. - Change the
sb a2,
intolb v0,
.
That’s it. Now you have made a function that takes the coordinates in (a0,a1)
, gets the color of the pixel at those coordinates, and returns it in v0
!
Implementing the eyedropper, and the display_is_key_
macros
In your function for MODE_NOTHING
, you have an if
for checking when the user presses the left mouse button. Inside that if, you have some code that switches to brush mode (or, if you followed my program structure, just a jal start_brush
or similar).
Here’s all you need to do. Change the code from:
if(user is pressing left mouse button) {
start brush mode;
}
to:
if(user is pressing left mouse button) {
if(user is holding alt) { // see below!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
color = display_get_pixel(display_mouse_x, display_mouse_y);
} else {
start brush mode;
}
}
(btw if you put all the code for starting brush mode inside this if… now might be a good time to move it into its own function and jal
to that function instead.)
As for the “user is holding alt” condition, you could do something like this:
li t0, KEY_ALT
sw t0, display_key_held
lw t0, display_key_held
# t0 is 1 if it's held
But this is starting to get repetitive, huh. So there are some macros in the display driver that shorten this to:
display_is_key_held t0, KEY_ALT
# t0 is 1 if it's held
The macro does the exact same thing, just on one line. (It’s on line 112 of display_2244_0203.asm
.) There are also display_is_key_pressed
and display_is_key_released
, if you want to go back and shorten your check for KEY_C
.
Drawing straight lines
This one is even easier. Instead of making a dedicated “line” tool, we’ll just change the way the “start brush” code works.
Before, you would draw a 1-pixel-long line where both ends were the mouse’s current location. Something like this pseudocode:
a0 = display_mouse_x
a1 = display_mouse_y
a2 = display_mouse_x
a3 = display_mouse_y
v1 = color
display_draw_line()
All you need to do to implement the straight line feature is change it like this:
if(user is holding shift) {
a0 = last_x
a1 = last_y
} else {
a0 = display_mouse_x
a1 = display_mouse_y
}
a2 = display_mouse_x
a3 = display_mouse_y
v1 = color
display_draw_line()
Since last_x
and last_y
are set by the brush mode, the way drawing lines works is like this:
- click to put one end of the line, and don’t drag.
- move your cursor somewhere else.
- hold shift, and click again. it draws a line from the last point to this one.
So with these two new features, you should be able to do this. I’m shift+clicking to draw the straight lines, and I’m alt+clicking to pick up the colors.
If you stop here (and it all works correctly and your code style isn’t terrible), you’ll get an 8/10. But if you wanna get the full 10/10, you have to do one more thing…
7. Flood fill/”bucket” tool
We can draw straight lines now, but filling in areas with color is really time-consuming. Drawing programs typically have a “flood fill” or “bucket” tool that lets you fill in areas with a single click.
The algorithm for flood fill is actually really, really simple, and you have all the functions needed to implement it. Cmonnnnn you can do thisssssssssssss
Here’s how the flood fill will work:
- when in
MODE_NOTHING
, if the user pressesKEY_F
:- start a flood fill at the current mouse location with the current drawing color.
Yeah, that’s it. Notice in my program I have flood_fill
- that’s what I call when the user presses F
. But flood_fill
doesn’t do the work…
flood_fill
My flood_fill
function does this:
a0 = display_mouse_x
a1 = display_mouse_y
display_get_pixel();
a0 = display_mouse_x // yes, you have to load it again. ATV rule.
a1 = display_mouse_y // ditto.
a2 = v0
a3 = color
flood_fill_rec();
That is, it gets the color under the cursor, then passes that - along with the drawing color and current cursor position - to flood_fill_rec
.
flood_fill_rec
Alright, here it is, the flood fill algorithm. Are you ready? It’s not actually that hard! (Read the notes after the code, too.)
// target is the color to replace, repl is the color we replace it with.
// a0 a1 a2 a3
void flood_fill_rec(int x, int y, int target, int repl) {
// get the color of the pixel at (x, y).
display_get_pixel(x, y);
// if this pixel is already the replacement color, return.
if(v0 == repl) return;
// if this pixel is not the target color, return.
if(v0 != target) return;
// replace this pixel with repl...
display_set_pixel(x, y, repl);
// and then recurse in all 4 directions.
if(x > 0) {
flood_fill_rec(x - 1, y, target, repl); // left
}
if(x < 127) {
flood_fill_rec(x + 1, y, target, repl); // right
}
if(y > 0) {
flood_fill_rec(x, y - 1, target, repl); // up
}
if(y < 127) {
flood_fill_rec(x, y + 1, target, repl); // down
}
}
Notes:
- You are absolutely going to need
s
registers for this function.- You need to move
a0-a3
intos0-s3
as the first thing in the function. - That means you need to push
s0-s3
at the beginning and pop them at the end, along withra
.- watch the order of
pop
s at the end… backwards…
- watch the order of
- You need to move
- Remember to put a
_return:
label right before thepop
s, so that you can return from the function by just branching to it. - I would recommend just doing the left recursion first, to see if you can get it working.
- Then do the right recursion…
- Then do up and down.
- Filling the entire screen takes a couple seconds. If your program sits for more than 2 or 3 seconds without doing anything, you’re caught in an infinite loop and will have to stop the program.
Here is what it will do with only left recursion working: when you press F, it will draw a line to the left, until it hits a pixel of a different color. (Here I’m replacing black with white, so it stops when it sees white.)
Here’s what it does with left and right recursion: similar, but draws a line in both directions.
And finally, here’s what it does when all 4 directions are implemented. It’s a real drawing program now!
Submitting
To submit:
- On Canvas, go to “Assignments” and click on this lab.
- Click “Start Assignment.”
- Under “File Upload,” click the “Browse” button and choose your
.asm
file. - Click “Submit Assignment.”
If you need to resubmit, that’s fine, just click “New Attempt” on the assignment page and upload it again.
Going further?
If you look in lab4_graphics.asm
, you’ll see an unused set of tiles called rect_sprite_gfx
.
This is for another mode for drawing rectangles. I was originally going to assign it as part of the lab, but it ended up being……… more complicated than I expected, lol. Here’s the idea:
- if the user presses R (for Rectangle), you enter rectangle mode.
- the mouse position when R was pressed becomes one corner of the rectangle, and the current mouse position is the other.
- let’s call the original position (x1, y1) and the current position (x2, y2).
- while in rectangle mode, you draw 4 sprites using those extra tiles at the 4 corners of the rectangle.
- you have to be clever though, because depending on the relative position of the first and current points, the “top-left” corner could be at (x1, y1), (x1, y2), (x2, y1), or (x2, y2)!
- and the same applies for the other three corners.
- there is a very simple way of doing this that doesn’t require 16 cases. think: “swap”
- finally, when the user clicks the mouse in rectangle mode…
- you use
display_fill_rect
to fill in the rectangle with the current drawing color - hide the 4 corner sprites
- and go back to
MODE_NOTHING
.
- you use