This lab is a lead-in to project 1. Project 1 will assume that you have done this lab. Skipping this lab would be a very, very bad idea.
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
- call
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)
- people mess up this call a lot. be careful about your arguments.
- call
- Finally, stub out the
check_input
anddraw_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
) - choose a new color by hitting
C
(which puts us inMODE_COLOR
)
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
) and one that is called by multiple other functions (display_draw_line
).
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.
If you see nothing:
- Make sure you aren’t stuck in an infinite loop in
load_graphics
. You know how to check for that by now, and you know how to fix it. - Another possibility is that
load_graphics
isn’t properly loading the cursor graphics.
How to get mouse input in the display
This section is for reference. This isn’t a step in the lab.
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.
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)
This is what the variables keep track of (and this is also true for display_key_pressed
etc.):
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.)
This is what check_input
should do:
- If they pressed
KEY_ESCAPE
, use syscall 10 to exit.- This is very much like lab 3 but even simpler.
- Actually, let me make it even simpler. In lab 3 you learned to do it like:
li t0, KEY_ESCAPE sw t0, display_key_pressed lw t0, display_key_pressed # now t0 is 1 if it's pressed
- But this is starting to get repetitive, huh. So there are some macros in the display driver that shorten this to:
display_is_key_pressed t0, KEY_ESCAPE # now t0 is 1 if it's pressed
- The macro does the exact same thing, just on one line. (go look for it in
display_2244_0203.asm
.) There are alsodisplay_is_key_held
anddisplay_is_key_released
- Otherwise, switch on the
drawmode
variable and based on which value it is, call one ofdrawmode_nothing
,drawmode_brush
, ordrawmode_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 part 3 for how to check this):
- 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 part 3 for how to check this):
- when
drawmode == MODE_BRUSH
(i.e. inside yourdrawmode_brush
function),- if the user releases the left mouse button (check with
display_mouse_released
), 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 (check with
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
When the mouse goes offscreen (i.e. off the edges of the display), display_mouse_x/y
will be -1, but you only need to check for one of them. So the if
in the above code is something like:
if(user_released_lbutton || display_mouse_x < 0) {
drawmode = MODE_NOTHING;
} else { ...
So you’ve got 2 things to check for. 2 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 continuously, 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 continuously, 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
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:
If the palette doesn’t appear, go back to the load_graphics
step and make sure you are loading the palette graphics correctly. If you are, then maybe you are setting the palette sprite flags to 0x41
instead of 1
.
If the palette appears, but it looks wrong (e.g. repeated colors, all blue or something), you are probably just not calculating the tile number correctly for each sprite.
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.
That’s it!
It’s not a very full-featured drawing program……………
yet.
Guess what project 1 will be?
Submitting
Submit only your _lab4.asm
file. Do not submit the provided files.
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.