Coursework: Week 9, Sketch

The coursework this week counts for 25% of your credit for the unit. The aims are to practice bit manipulation and input/output, and to design and develop functions on your own. Also, you will get to work on a multi-module program (though you only have to develop one module) and you get to see a graphics library being used (though you don't have to write any graphics code explicitly yourself).

Study the problem and files provided

A .sketch file contains a line drawing. The format has been designed to suit the capture of a freehand sketch in a compact file, in such a way that it can be replayed as an animation.

The challenge is to develop a program sketch.c which reads in and replays a .sketch file. The file contains single-byte operations, which the program translates into calls to a graphics module, which is provided. As a starting point, you are given these files:

There are lots of files

As this assignment is going to involve lots of files, it is suggested that you create a new empty folder to work in. (You should do that anyway, for each new assignment or project.)

Reminder: if you download Makefile directly, it is likely to end up as Makefile.txt and you will need to rename it back.

The right hand column of files is for stage 2.

There is a zip file

The file sketch.zip contains all the other files, zipped up. If you want to use that rather than download all the others one by one, copy it into your new empty folder, and then unzip it. (On Windows, in general, it is not recommended to use the native Windows unzip facility, because it often doesn't work properly on non-Windows zip files. Either install 7-zip, or install the unzip command in Cygwin/MSYS2.)

There is no sketch.c file

No sketch.c file is provided for you to start from. You have to provide it. That means you have to decide what headers to include, define types and constants, design and write functions that carry out the logic of the problem, design and write tests for those functions, and design and write input/output functions.

On the other hand, you won't need to change anything in any of the other files.

View the .sketch files with od

Since the .sketch files are binary, you can't view them in an editor or any of the other normal ways. To see what the bytes of a sketch file look like the best bet is to use the od command (see the aside on Unix commands), e.g.:

od -t x1 lines.sketch
0000000  1e 5e c0 1e 40 5e
0000005

A number on the first column like 0000000 says what position in the binary file the line of output refers to, which is useful for larger files. The rest of the line shows you the bytes in hex.

You might want to write a little throw-away experimental program to read and print out sketch files in any way you find helpful. To print out a byte in hex yourself, use printf("%02x\n",b) for example.

There is a graphics module provided

A module display.c is provided which uses the SDL graphics library to provide a few simple functions for drawing. Read display.h to see what is provided and, in your sketch.c file, put #include "display.h" near the top. See the aside on SDL for more information about SDL, though you don't need to understand it for this assignment.

You might want to write a little throw-away experimental program to call the display functions, so you can see what they do.

There is a test module provided

In the Makefile, there are two ways to compile your sketch.c file. One (typing make sketch) links sketch.c with display.c and the SDL library. The other (typing make test or just make) links it with test.c instead. The test.c module provides the same functions as display.c, but doesn't do any graphics. It just checks that the function calls your program makes are the ones expected.

This gives you two layers of testing. At a low level, you can write tests for your own bit-manipulation functions to check that they do what you intended. You can create suitable tests by looking for examples in this assignment description.

At a higher level, you can test the graphics calls you make to functions in display.h by compiling with test.c. In fact you may want to do that most of the time while you are developing. If you can't get SDL working for some reason, you can compile with test.c all the time, and still get credit for your work.

SDL has memory leaks

If you run the program with graphics using ./sketch, memory leaks may show up. This is likely to be SDL's fault, not yours, so you can ignore it.

On the other hand, if you test the program with ./test, any memory leaks that show up are likely to be your fault, and need to be investigated.

Maybe it is time to start using git

Since you are going to be working with many files, and there are a lot of opportunities for damaging them, you might like to start using git (with or without a backup repository in the cloud). See the aside on version control for details.

Understand stage 1

To help you, the problem is divided into two stages. The first stage explores simple sketches with no timing information. It is worth 40% so, if a pass is enough for you, you can stop there. Doing the second stage, which involves multi-byte operations, will raise your mark to 50% and then extra work can be done beyond that as usual. (You can do just stage 1 plus extra work, if you want.)

Each byte is an operation (or op for short)

In stage 1, each byte is a self-contained op, telling your program what to do, rather like a processor instruction.

It has an opcode and operand

The first (most significant) two bits form the opcode, short for operation code, which is a small integer, in this case 0 to 3, which specifies what the operation does. The other six bits form the operand, which is like the argument to an operator or function, and provides the op with a number to work with.

Ops can be described in an assembly-language style by writing down a symbol for the opcode followed by the operand. For example, the byte sequence 0x03 0x7D can be written DX 3; DY -3

There are three opcodes

During the replay of a sketch, there is a current (x,y) position, which is initially (0,0) at the top left corner of the drawing area. (Normal across-and-down graphics coordinates are used. They are a compromise between Cartesian and matrix coordinates.) There is also a notional pen, with "pen up" used to represent moving and "pen down" used to represent drawing. The pen is initially up.

The opcodes in stage 1 are:

A complex monochrome sketch with no timings is very compact, consisting mostly of pairs of DX and DY bytes.

Delayed drawing

Why is drawing not included in the DX op? Because then you would only be able to draw horizontal and vertical lines. Lines can be drawn in any direction.

Example bytes

Examples of operation bytes are:

Stage 1 sketch files

The file lines.sketch contains 6 bytes:

1e 5e c0 1e 40 5e

which mean

DX 30; DY 30; PEN; DX 30; DY 0; DY 30

Check that you understand that the sketch ops mean "move to (30,30) then draw a line to (60,30), then draw a line to (60,60)". In particular, DY 0 causes the first draw, and the DX value is implicitly zero in the final DY 30.

Only the first set of sketch files, from lines.sketch to cross.sketch, are relevant to stage 1.

Develop stage 1

Create small functions

If you stuff everything into one big function, it will be almost impossible to reach the end of the assignment (except by extreme luck or heroism). But if you keep your functions tiny, you may find the development surprisingly smooth and rewarding.

My own stage 1 program has 10 functions, plus 5 more for testing, each 5 lines long on average

For example, you could begin by designing a function to extract the opcode from a byte, writing a test function for it, and adding a main program which calls the test function. Even though the opcode extraction function is probably only one line long, it is a line which is extremely error prone, so this approach will give you something robust and reliable to build on. Look at the bits chapter, or previous bits assignment, for bit manipulation info.

Add input/output

As well as bit manipulation, you are also going to need functions to read bytes from sketch files, and functions to make graphics calls. Many programmers get caught up in a mess of I/O straight away, with nothing being testable. To avoid that, the best advice is to add I/O last. Check the I/O chapter for how to read binary files.

Use a stream style

You can process each byte one-by-one as they are read from the file, as if they were arriving in a stream from somewhere else. You only need a fairly small and fixed amount of information to keep track of the current state of the sketch.

You may choose to read the whole file into an array in memory. That isn't necessary, but it is reasonable. However, I would still recommend using a stream style, having a small fixed state and dealing with each byte from the array in turn.

You will need a state structure

You should be able to see from the description so far that you will need a state structure to hold the current position, whether the pen is up or down, the most recent DX value, and possibly the display object to draw on. You might also want to define some constants, e.g. DX for opcode 0 and so on.

External testing

As well as internal autotesting of your own functions, you need to use the external testing provided by the test.c module. The test.c module provides the same functions as display.c, but does no graphics. Instead, it just makes sure the calls you make are the ones expected. Use it by typing make test (or just make) instead of make sketch. Here is an example minimal program to illustrate how to do it:

#include "display.h"
int main(int n, char *args[n]) {
    if (n != 2) { fprintf(stdout, "Use ./sketch filename\n"); exit(1); }
    display *d = newDisplay(args[1], 200, 200);
    line(d, 30, 30, 60, 30);
    line(d, 60, 30, 60, 60);
    end(d);
}

The name of the sketch file should come from the command line (that's how I am going to be marking your program). So, you will type ./sketch lines.sketch.

The first argument to newDisplay must be the name of the sketch file such as lines.sketch, so that the test.c module can work out what calls to expect.

The two calls to line are the ones expected for lines.sketch. Try leaving one out, changing one, or adding another call, to see what feedback you get from test.c.

The end call tells test.c that the sketch is complete, so that it gives you a confirmation message that you have made all the expected calls.

Here are some images of what the stage 1 sketch files should produce, if you compile with display.c, assuming you create a 200x200 window:

Understand stage 2

In stage 2, the idea is to handle extra opcodes and larger operands. At the same time, the format is kept compact and stream-like.

Extended operands

Opcode 2 (which wasn't used in stage 1) represents a PR prefixing operation, for creating or extending operands. The state contains a current operand value under construction, and keeps track of whether the value has been initialized. Any op can be preceded by PR prefix ops.

In a sequence consisting of PR ops followed by DX or DY, the first op in the sequence initializes the value and establishes its first six bits, including its sign. Each further PR op and the final DX/DY op adds six more bits to the value.

For example, in the sequence PR a; PR b; DX c, the op PR a sets the value to signExtend(a), then PR b sets the value to (value << 6) | b and then DX c sets the value to (value << 6) | c. The DX op uses the final value as its operand, then resets the current value ready for the next op sequence.

Extended opcodes

Opcode 3 no longer means PEN. It now represents a DO operation, where the number in the remaining size bits, plus three, becomes the opcode. These ops have no direct operand within the instruction byte, but may be preceded by prefix ops to provide an operand. The extended opcodes supported are:

Extended opcodes can have extended operands

In a sequence PR a; PR b; DX c the operand is built up from a, b and c.

With an extended op, there are no operand bits in the byte itself. So, for example, in a sequence PR a; PR b; DT the operand to the DT op is just built from a and b (with no contribution from the DT byte).

The prefix scheme has several advantages

If you are interested, this prefix scheme is partly inspired by the transputer instruction set and the UTF8 character encoding. It has several advantages over the more normal approach of having each op read its operand from bytes which follow.

The state is more complex

As well as the current position, whether the pen is up or down, and the most recent DX value, the state now needs to store the most recent non-zero DT value, the current value of the operand under construction, and a flag to say whether that value has been initialized.

Zero prefixes

In the assembly language style of writing ops, we can now just write an opcode with an arbitrarily large operand. For example, DX 42 refers to the two bytes 80 2a. You could also write that as PR 0; DX -22. The zero prefix is needed to make sure the six bits in the DX op are treated as positive rather than negative.

The fact that prefixes are sometimes zero means that using "value == 0" as a test for the value being uninitialized doesn't work, even though an uninitialized value is implicitly zero. So a separate flag is recommended.

The second set of sketch files are tests for stage 2. The field.sketch file draws a large square, with extended operands; box.sketch draws a square as an animation; clear.sketch draws an oxo board, pauses, clears, draws a cross; key.sketch draws an oxo board, pauses, waits for a key press, clears, draws a cross; lawn.sketch draws a large green square.

Do creative extra work

For the remaining 50% of the assessment, you can do something creative which relates to sketching. For example, add other opcodes, or write a program to record sketches, or create some interesting sketches to share with your friends, or find a way of adapting some existing sketches to share, or look into smoothing (that's interpolation if there are too few points or simplification if there are too many).

Alternatively, write any other program you like, possibly using SDL graphics. You could write a graphical interface to add to a previous program that you've written for this unit. Add a short readme.txt file to explain what you've done.

Submit

All you have to submit is your sketch.c file. As usual, keeping the file name the same and submitting it directly and not inside a zip file will help with automarking.

The stages above are auto-tested, and the results of the auto-tests provide your mark for the first 50% of the assignment. Your mark will be 40% if you get to the end of stage 1, 50% if you get to the end of stage 2, and I will use my judgment if it is in between.