Graphics

#include "libtini/graphics.h"

This header gives software surfaces in banked memory and functions that draw into them. Other functions publish the surfaces to the 320x200 Super Hi-Res screen with usual CPU writes. For many sprites that move on a plain background, use sprite scenes.

FunctionPurpose
ti_gfx_surface_create, ti_gfx_surface_freeOwn a banked surface
ti_gfx_clear, ti_gfx_rect, ti_gfx_lineFill a surface, fill a rectangle, draw a line
ti_gfx_text, ti_gfx_text_opaque, ti_digitsText in the built-in 5x7 font, and fixed-width numbers
ti_gfx_text_fontText in a bitmap font in a resource
ti_gfx_blit, ti_gfx_spriteCopy between surfaces, with a colour key or a mask
ti_gfx_map, ti_gfx_animation_frameDraw a tile map that scrolls; select an animation frame
ti_gfx_begin, ti_gfx_palette, ti_gfx_endGet the screen, set its colours, release the screen
ti_gfx_budget, ti_gfx_present, ti_gfx_present_rowsPublish to the screen in a budget

Pixels, surfaces and the screen

A pixel is a 4-bit palette index. One byte holds two pixels. The left pixel is in the high nibble. A surface row of width w uses (w + 1) / 2 bytes: this is its stride. With an odd width, the last low nibble of each row is not used.

The Super Hi-Res screen packs pixels in the same way. It has 200 rows of 160 bytes at AUX $2000, 200 scan-line control bytes at $9D00 and sixteen palettes at $9E00. libtini sets all lines to 320-pixel mode with palette 0. Thus the full screen uses one palette of sixteen 0RGB colours.

Surfaces are in banked memory. A draw call changes only a surface. The screen changes only when ti_gfx_present copies a rectangle of a 320x200 surface to the screen. Keep the surface as the authority: publication reads all bytes that it writes from the surface. This includes the neighbour pixel of a byte at the edge of the rectangle.

Constants

NameValueMeaning
TI_GFX_WIDTH, TI_GFX_HEIGHT320, 200Screen size in pixels
TI_GFX_OPAQUE255A blit colour key that copies all pixels
TI_GFX_CLOCK_VALID1Statistics flag: elapsed_ticks contains a measurement
TI_GFX_OVERRUN2Statistics flag: the measured time is more than the tick budget
TI_GFX_EDGE_GAP4Statistics flag, always set: the work can hide display edges

Types

ti_surface

typedef struct ti_surface {
    ti_handle pixels;
    uint32_t offset;   /* permits a borrowed view into one resource pack */
    uint16_t width, height, stride;
} ti_surface;

A surface is packed pixels in a resource, from offset. The width is 1-320 and the height is 1-200. stride must be equal to (width + 1) / 2. Each draw call checks these values and the size of the resource before it starts.

A surface from ti_gfx_surface_create owns its resource. A copy of the structure is only a view of the resource. A surface that you fill in to show an image in a pack is also a view. Do not free a view. Stop all use of a view before you free its owner.

ti_rect

typedef struct ti_rect { int16_t x, y; uint16_t width, height; } ti_rect;

A rectangle with a signed position. Calls clip it to the surface, unless the function entry gives a different rule.

ti_gfx_stats

typedef struct ti_gfx_stats { uint32_t bytes, elapsed_ticks; uint8_t flags; } ti_gfx_stats;

A publication fills this structure. It contains the bytes written to the screen, the ticks that the publication used when a budget clock is set, and the TI_GFX_* flags.

ti_sprite

typedef struct ti_sprite { ti_surface image; ti_handle mask; uint32_t mask_offset; } ti_sprite;

A sprite is an image with an optional opacity mask. The mask has one bit for each pixel, high bit first, and 1 means opaque. Each mask row has (image.width + 7) / 8 bytes. When mask is 0, colour 0 is transparent. The image and the mask can be in one resource at different offsets. Both are views.

ti_tileset

typedef struct ti_tileset {
    ti_surface image;
    uint8_t tile_width, tile_height, columns;
    uint16_t count;
} ti_tileset;

A tile set is an atlas of count tiles (1-256), with columns tiles in each row. The tile numbers go left to right, then top to bottom. The atlas must be columns * tile_width pixels wide or more, and it must have sufficient tile rows for count.

ti_tilemap

typedef struct ti_tilemap {
    ti_handle cells;   /* row-major uint8 tile IDs */
    uint32_t offset;
    uint16_t columns, rows;
} ti_tilemap;

A map of 1-256 by 1-256 tile numbers in a resource. Each tile number is one byte.

ti_animation

typedef struct ti_animation { uint16_t first, count, ticks_per_frame; } ti_animation;

A sequence of count frame numbers that starts at first. Each frame shows for ticks_per_frame device ticks.

ti_bitmap_font

typedef struct ti_bitmap_font {
    ti_handle glyphs;
    uint32_t offset;
    uint8_t width, height, first, count; /* one byte per glyph row, MSB first */
} ti_bitmap_font;

A bitmap font has glyphs of 1-8 by 1-16 pixels for the character codes first to first + count - 1. The resource holds height bytes for each glyph. The leftmost pixel is in the high bit. tools/assets.py font writes this layout after an 8-byte header.

Surface functions

ti_gfx_surface_create

ti_error ti_gfx_surface_create(ti_surface *surface, uint16_t width, uint16_t height);

This function allocates a resource of stride * height bytes and fills in *surface. It does not clear the pixels. Call ti_gfx_clear before you publish the surface. The structure must contain zeros before the call. Static and global structures contain zeros at startup.

Returns

TI_OKThe surface owns a new resource.
TI_E_ARGUMENTsurface is not writable near memory.
TI_E_STATEsurface->pixels is not zero: the structure already owns a resource, or it does not contain zeros.
TI_E_RANGEThe width is not 1-320, or the height is not 1-200.
TI_E_NOMEM, TI_E_STATEFrom ti_res_alloc: no free slot or space, or no ti_init. The sizes in the structure are set to zero.

ti_gfx_surface_free

ti_error ti_gfx_surface_free(ti_surface *surface);

This function frees the resource of the surface and sets the structure to zeros. You can then create the surface again.

Returns

TI_OK; TI_E_ARGUMENT for a pointer that is not writable; TI_E_HANDLE when the resource is not live, for example after a second free.

Draw functions

Draw calls are synchronous foreground calls and are not reentrant. Strings and structures must stay valid only until the call returns. Each call examines the surface first. It returns TI_E_ARGUMENT for a structure that it cannot read or for sizes that do not agree. It returns TI_E_HANDLE for a resource that is not live, and TI_E_RANGE when the pixels go past the end of the resource. Colours are 0-15. A larger colour gives TI_E_ARGUMENT.

Rectangles, text, clear and publication use one assembly routine. It moves full rows between the bank of the surface and a near row buffer. Lines and bitmap fonts plot single pixels through a 128-byte cache. Blits, sprites and maps use resource reads and writes. Thus these functions use more time for each pixel. A program that uses only rectangles and the built-in font does not link the pixel cache.

When a resource error occurs during a draw call, a part of the draw can be complete. A draw call is not a transaction. Stop, or draw again from a known state.

ti_gfx_clear

ti_error ti_gfx_clear(ti_surface *surface, uint8_t color);

This function fills the full surface with color. This includes the unused nibble at the end of each row of an odd width.

ti_gfx_rect

ti_error ti_gfx_rect(ti_surface *surface, int16_t x, int16_t y,
                     uint16_t width, uint16_t height, uint8_t color);

This function fills a rectangle and clips it to the surface. When the full rectangle is outside the surface, it returns TI_OK and draws nothing. The function makes one row of bytes and writes it to all lines. When an edge of the rectangle is in the middle of a byte, it reads each line first. Thus the pixel outside the rectangle keeps its colour.

ti_gfx_line

ti_error ti_gfx_line(ti_surface *surface, int16_t x0, int16_t y0,
                     int16_t x1, int16_t y1, uint8_t color);

This function draws a Bresenham line that includes the two endpoints. It clips each pixel. The endpoints must be in the range -1024 to 1024 on the two axes, which limits the work. Other endpoints give TI_E_RANGE.

ti_gfx_text

ti_error ti_gfx_text(ti_surface *surface, int16_t x, int16_t y,
                     const char *text, uint8_t color);

This function draws a NUL-terminated string in the built-in font. Each character is a 5x7 glyph in a 6x8 cell. The top-left corner of the cell is at the pen position. After each character, the pen moves 6 pixels to the right. A newline (byte 10) moves the pen back to x and 8 pixels down. The function draws only the lit pixels. The other pixels of the cell keep their colours. The function clips the text to the surface.

The font has the space, ! " ' ( ) * + , - . /, the digits, : < = > ?, the capital letters and [ ] _. The function draws lower-case letters as capital letters. It draws all other bytes as ?.

A call reads a maximum of 1024 characters, newlines included. For each pixel row of a text line, the function reads and writes the bytes of that line one time.

Returns

TI_OK; TI_E_ARGUMENT for a NULL string or a colour above 15; TI_E_RANGE when x or y is outside -1024 to 1024. The function also returns TI_E_RANGE when the string goes out of readable near memory before its NUL. It draws the lines before that point.

ti_gfx_text_opaque

ti_error ti_gfx_text_opaque(ti_surface *surface, int16_t x, int16_t y,
                            const char *text, uint8_t color, uint8_t background);

This function draws text as ti_gfx_text does. But first it fills each full 6x8 cell with background, the gap column and the eighth row included. Thus a new value covers an old value of the same length, and you do not have to clear a rectangle first. The area to publish is exactly 6 * length by 8 pixels. The function writes rows without a read, unless an edge of the line is in the middle of a byte.

static const ti_rect score_cells = {250, 64, 7 * 6, 8};
char text[8];
ti_gfx_text_opaque(&canvas, 250, 64, ti_digits(text, score, 7), 9, 0);
ti_gfx_present(&canvas, &score_cells, 0);   /* publish only these cells */

ti_digits

char *ti_digits(char *text, uint32_t value, uint8_t count);

This function writes exactly count decimal digits of value, with leading zeros, and a NUL. When the value has more digits, the function keeps the last count digits. For example, ti_digits(s, 123456, 4) writes "3456". count is 1-10. text must have count + 1 writable near bytes.

The function returns text, or NULL for an incorrect count or buffer. The text functions reject NULL with TI_E_ARGUMENT. Thus you can give the result directly to them. The function is in assembly. It doubles a BCD number for each bit and does not divide by ten.

ti_gfx_text_font

ti_error ti_gfx_text_font(ti_surface *surface, int16_t x, int16_t y,
                          const char *text, uint8_t color, const ti_bitmap_font *font);

This function draws a string in a bitmap font. It draws the lit pixels of each glyph. Then the pen moves width + 1 pixels. A character that is not in the font gives an empty space of the same width. A newline moves the pen back to x and height + 1 pixels down. The function reads a maximum of 1024 characters.

Returns

TI_OK; TI_E_ARGUMENT for a NULL font or string, a font or string that the function cannot read, a colour above 15, font sizes outside 1-8 by 1-16, or first + count above 256; TI_E_RANGE when the glyphs go past the end of the resource or the string goes out of near memory; TI_E_HANDLE for a glyph resource that is not live.

ti_gfx_blit

ti_error ti_gfx_blit(ti_surface *destination, int16_t x, int16_t y,
                     const ti_surface *source, const ti_rect *source_rectangle,
                     uint8_t transparent_color);

This function copies a rectangle of source to destination. The top-left corner of the copy is at x, y. The function clips the copy to the destination. NULL selects the full source. The function skips pixels of transparent_color (0-15). TI_GFX_OPAQUE copies all pixels. Odd source and destination positions are correct. The copy uses pieces of a maximum of 240 pixels for each row and moves them through near buffers.

Returns

TI_OK; TI_E_ARGUMENT when the two surfaces use the same resource handle (the function rejects a copy in one resource before it changes anything), for a colour key that is not 0-15 or 255, or for a rectangle that it cannot read; TI_E_RANGE when the source rectangle is not fully in the source; the surface errors above.

ti_gfx_sprite

ti_error ti_gfx_sprite(ti_surface *destination, int16_t x, int16_t y,
                       const ti_sprite *sprite);

This function draws a full sprite image and clips it. With a mask, a mask bit of 1 copies its pixel, colour 0 included. A mask bit of 0 keeps the destination pixel. Without a mask, colour 0 is transparent. The mask must not use the handle of the destination. The image has the same rules as a blit source.

Returns

The same results as ti_gfx_blit. Also TI_E_ARGUMENT when the function cannot read sprite, and TI_E_RANGE or TI_E_HANDLE when the mask does not fit in its resource.

ti_gfx_map

ti_error ti_gfx_map(ti_surface *destination, const ti_rect *viewport,
                    const ti_tileset *tiles, const ti_tilemap *map,
                    uint16_t camera_x, uint16_t camera_y);

This function fills viewport, clipped to the destination, with a part of the map. The top-left world pixel of that part is camera_x, camera_y. The function copies tiles opaquely. Pixels past the edge of the map are colour 0. To scroll, change the camera position.

Returns

TI_OK; TI_E_ARGUMENT for a viewport, tile set or map that the function cannot read; TI_E_RANGE for zero or too large tile or map sizes, an atlas that is too small for its tiles, a camera position plus viewport past 65,535, or a map cell with a tile number of count or more. The function finds an incorrect tile number during the draw, thus it draws the tiles before that cell. The function also returns the surface and handle errors of the atlas, the cells and the destination.

ti_gfx_animation_frame

uint16_t ti_gfx_animation_frame(const ti_animation *animation, uint32_t elapsed_ticks);

This function returns first + (elapsed_ticks / ticks_per_frame) % count. Give it elapsed device ticks, not a count of drawn frames. Thus the animation keeps its speed when the draw rate decreases. The function returns 0 for a NULL descriptor, a descriptor that it cannot read, a zero count or ticks_per_frame, or first + count past 65,535.

The video session

ti_gfx_begin

ti_error ti_gfx_begin(const uint16_t *palette);

This function gets the screen. It allocates a 32,768-byte resource and saves all of AUX bank 0 from $2000 to $9FFF in it. It sets the Appletini SHR display on. It sets all 200 scan lines, and the padding after them, to 320-pixel mode with palette 0. Then it writes palette: sixteen 0RGB words from 0x000 to 0xFFF.

The function does not change the slot or speed settings. The pixel area keeps its old contents. Thus publish a clear surface immediately after this call.

Returns

TI_OKThe session is active.
TI_E_STATEA session is already active, or there is no ti_init grant for the backup.
TI_E_ARGUMENTpalette is not 32 readable near bytes.
TI_E_RANGEAn entry is above 0x0FFF.
TI_E_NOMEMNo slot or space is available for the backup. The screen does not change.

ti_gfx_palette

ti_error ti_gfx_palette(const uint16_t *palette);

This function replaces the sixteen colours during a session with one 32-byte write. The cost is low, thus you can call it in each frame. When you change some entries, all pixels that use these entries change colour, and no draw is necessary.

Returns

TI_OK; TI_E_STATE without a session; TI_E_ARGUMENT or TI_E_RANGE as for ti_gfx_begin.

ti_gfx_end

ti_error ti_gfx_end(void);

This function sets SHR off, writes all saved bytes back to AUX bank 0 and frees the backup. Without a session, it does nothing and returns TI_OK. ti_shutdown calls it.

A file call during the session lends the screen memory to ProDOS. The backup then holds the more recent OS bytes. Thus the changes that ProDOS makes to /RAM stay.

ti_gfx_budget

void ti_gfx_budget(uint16_t byte_budget, ti_tick_source clock, uint32_t tick_budget);

This function sets the limits for ti_gfx_present and ti_gfx_present_rows. These functions reject a publication of more than byte_budget bytes before any write. A value of 0 means 32,000, a full screen.

With a clock, the library measures the time of each publication. When the time is more than a nonzero tick_budget, it sets TI_GFX_OVERRUN. With a NULL clock, the library does not measure the elapsed time. Tick differences are modulo 232.

/* A maximum of 4000 bytes for each publication; report a publication of more than 2 ticks. */
ti_gfx_budget(4000, ti_audio_ticks, 2);

ti_gfx_present

ti_error ti_gfx_present(const ti_surface *surface, const ti_rect *rectangle,
                       ti_gfx_stats *stats);

This function copies a rectangle of a 320x200 surface to the same position on the screen. NULL publishes the full screen. The function clips the rectangle and makes its left and right edges wider to full bytes. Each row goes through the near row buffer in bursts of a maximum of 128 bytes. stats can be NULL.

When the function returns, the CPU writes are complete. This does not mean that the Appletini shows them. There is no page swap. Thus the screen can show a large rectangle during two frames.

Returns

TI_OKThe rectangle is published. When the full rectangle is outside the surface, the function publishes nothing and reports 0 bytes.
TI_E_STATENo video session is active.
TI_E_RANGEThe surface is not 320x200.
TI_E_ARGUMENTThe function cannot read the rectangle or cannot write stats.
TI_E_TIMINGBefore the writes: the byte count is more than the budget, and the function writes nothing. After the writes: the measured time is more than the tick budget, and stats has TI_GFX_OVERRUN.

ti_gfx_present_rows

ti_error ti_gfx_present_rows(uint16_t x, uint16_t y, uint16_t width,
                            uint16_t height, const uint8_t *pixels,
                            uint16_t stride, ti_gfx_stats *stats);

This function publishes packed pixels from near memory during a session, without a surface. Row r of the rectangle comes from pixels + r * stride. The function writes only width / 2 bytes of each row. Use it for small areas that the game composes. The function does not clip or make edges wider, and it does not change a surface.

x, width
Even values. Thus each row starts and ends on a byte.
y, height
The rectangle must not be empty, and it must be fully in the 320x200 screen.
pixels
The first byte gives the two leftmost pixels. The span (height - 1) * stride + width / 2 must be readable near memory. The function uses the buffer only until it returns.
stride
The bytes from one source row to the next, width / 2 or more. Padding and odd strides are correct.

All checks, the byte budget included, are complete before the first write. A rejected call does not change stats. One assembly routine writes all rows. Each row is one burst with interrupts masked, or two bursts when the row is more than 128 bytes.

Returns

TI_OK; TI_E_STATE without a session; TI_E_ARGUMENT for a zero width or height, NULL pixels, or stats that the function cannot write; TI_E_RANGE for an odd x or width, a rectangle outside the screen, a stride that is too short or a span that the function cannot read; TI_E_TIMING as for ti_gfx_present.

Costs of draw and publication

These figures are measured on the development emulator at vtw33 (33.33 MHz) and with the turbo-f122 cost model for TURBO.

OperationBytesvtw33 msturbo-f122 ms
Publish the full screen32,00040.885.1
Draw a 65x17 rectangle at an odd x5611.62.6
Publish that rectangle5611.32.1
Replace a HUD value of 2-8 characters with opaque text and publish its cells1.91.5

A full-screen publication uses more time than one 60 Hz frame. Draw the static parts one time. Then publish only the rectangles that change.