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.
| Function | Purpose |
|---|---|
ti_gfx_surface_create, ti_gfx_surface_free | Own a banked surface |
ti_gfx_clear, ti_gfx_rect, ti_gfx_line | Fill a surface, fill a rectangle, draw a line |
ti_gfx_text, ti_gfx_text_opaque, ti_digits | Text in the built-in 5x7 font, and fixed-width numbers |
ti_gfx_text_font | Text in a bitmap font in a resource |
ti_gfx_blit, ti_gfx_sprite | Copy between surfaces, with a colour key or a mask |
ti_gfx_map, ti_gfx_animation_frame | Draw a tile map that scrolls; select an animation frame |
ti_gfx_begin, ti_gfx_palette, ti_gfx_end | Get the screen, set its colours, release the screen |
ti_gfx_budget, ti_gfx_present, ti_gfx_present_rows | Publish 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
| Name | Value | Meaning |
|---|---|---|
TI_GFX_WIDTH, TI_GFX_HEIGHT | 320, 200 | Screen size in pixels |
TI_GFX_OPAQUE | 255 | A blit colour key that copies all pixels |
TI_GFX_CLOCK_VALID | 1 | Statistics flag: elapsed_ticks contains a measurement |
TI_GFX_OVERRUN | 2 | Statistics flag: the measured time is more than the tick budget |
TI_GFX_EDGE_GAP | 4 | Statistics 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_OK | The surface owns a new resource. |
TI_E_ARGUMENT | surface is not writable near memory. |
TI_E_STATE | surface->pixels is not zero: the structure already owns a resource, or it does not contain zeros. |
TI_E_RANGE | The width is not 1-320, or the height is not 1-200. |
TI_E_NOMEM, TI_E_STATE | From 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_OK | The session is active. |
TI_E_STATE | A session is already active, or there is no ti_init grant for the backup. |
TI_E_ARGUMENT | palette is not 32 readable near bytes. |
TI_E_RANGE | An entry is above 0x0FFF. |
TI_E_NOMEM | No 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_OK | The rectangle is published. When the full rectangle is outside the surface, the function publishes nothing and reports 0 bytes. |
TI_E_STATE | No video session is active. |
TI_E_RANGE | The surface is not 320x200. |
TI_E_ARGUMENT | The function cannot read the rectangle or cannot write stats. |
TI_E_TIMING | Before 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 / 2must be readable near memory. The function uses the buffer only until it returns. - stride
- The bytes from one source row to the next,
width / 2or 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.
| Operation | Bytes | vtw33 ms | turbo-f122 ms |
|---|---|---|---|
| Publish the full screen | 32,000 | 40.8 | 85.1 |
| Draw a 65x17 rectangle at an odd x | 561 | 1.6 | 2.6 |
| Publish that rectangle | 561 | 1.3 | 2.1 |
| Replace a HUD value of 2-8 characters with opaque text and publish its cells | 1.9 | 1.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.