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:

  1. get the current position of the mouse.
  2. if the user is holding down the mouse button, draw a pixel at that position.
  3. 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..

# 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.

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:

Simple stuff.

load_graphics needs to:

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:

In MODE_BRUSH, we are holding down the mouse button and drawing. In this mode we just:

Finally in MODE_COLOR, we:

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:

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:

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:

And the constants you use in the bitwise AND are:


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:

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:

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:


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:

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?)

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:


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:

  1. Copy the whole display_set_pixel function.
  2. Paste it into your program, and rename it display_get_pixel.
  3. Remove the 4 branches and the _return: label.
  4. Change the sb a2, into lb 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:

  1. click to put one end of the line, and don’t drag.
  2. move your cursor somewhere else.
  3. 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:

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:

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:

  1. On Canvas, go to “Assignments” and click on this lab.
  2. Click “Start Assignment.”
  3. Under “File Upload,” click the “Browse” button and choose your .asm file.
  4. 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: