How libtini works
This page gives the base for the rest of the reference: the two types of memory, the owner of each part of the machine, the order of startup and shutdown, and the difference between a device clock and the display.
The target
A libtini program is a ProDOS SYS file. ProDOS loads it at $2000 on an enhanced //e. The C code runs from main memory.
The Super Hi-Res picture is in auxiliary memory, bank 0, at $2000-$9FFF. The Appletini shows this picture when its fake SHR mode is on. RamWorks expansion banks hold the large data of the game. A write to $C073 selects a bank. The slot-4 card gives sound, speech and a timer that does not change with the CPU speed.
The library uses the C99 subset of cc65 2.19 and the default cc65 calling convention: non-variadic functions are fastcall. All public types are fixed-width integers from stdint.h. Errors are uint8_t codes. Handles are uint16_t. This ABI is the ABI of the first implementation. It is not a promise for future versions.
Near memory and banked resources
libtini keeps two types of memory separate.
Near memory
Near memory is usual C memory in main RAM: global variables, the C stack, constants and the near arena. Each function that receives a near pointer checks it against the regions of the linker layout. The check occurs before the function reads or writes data. These regions are:
- The writable data and BSS of the program, from the end of its saved OS state to the memory end of the layout.
- The C software stack.
- The near arena, which holds the variables of the
NEARBSSsegment. - For reads only: the code and constants of the program from $2000, and in the full-game layouts, the constants at $0800-$1FFF.
A pointer outside these regions gives TI_E_RANGE or TI_E_ARGUMENT. Examples are the text screen, the ProDOS global page at $BF00 and I/O space. A zero length accepts all pointers, NULL included, and does not touch memory. The checks find errors. They do not record which object owns which bytes.
Banked resources
Large data is in the RamWorks banks that the game grants with ti_init(first_bank, bank_count). Each granted bank gives 48,640 bytes, its $0200-$BFFF range (TI_BANK_BYTES). The granted banks make one logical stream. An allocation is contiguous in this stream and can cross from one bank into the next bank.
A resource is a ti_handle and byte offsets. No C pointer points to banked bytes. The CPU sees a bank only during one transfer burst, with interrupts masked, from a small trampoline in page one. Reads, writes, fills and copies move data between a resource and near memory, or between two resources. Each of these functions checks all handles and ranges before it starts.
- There are eight resource slots (
TI_RESOURCE_SLOTS). A full-screen surface uses one slot. The screen backup of the active video session uses one more slot. - Allocation is first fit in the logical stream. An allocation fails with
TI_E_NOMEMwhen there is not sufficient free space or no free slot. - A handle contains its slot and a generation. When you free a resource, all copies of its handle become invalid. A subsequent call with such a handle returns
TI_E_HANDLE. - Bank 0 is never a resource bank. Firmware F1.3.2 serves banks 1-126. The configured memory is not one flat 8 MB heap: a grant of all 126 banks gives 6,128,640 resource bytes.
The number of banks for a game
Some parts of the library allocate their own resources:
| Allocation | Bytes | Duration |
|---|---|---|
A 320x200 surface from ti_gfx_surface_create | 32,000 | Until ti_gfx_surface_free or shutdown |
The original AUX0 screen, which ti_gfx_begin saves | 32,768 | Until ti_gfx_end |
The picture, during a video session, while a file call, ti_audio_init or ti_audio_shutdown lends AUX0 to ProDOS | 32,768 | During that call only |
A pack entry from ti_pack_load | Its size | Until the caller frees it |
A game with one full canvas, which starts audio after graphics, needs 32,000 + 32,768 + 32,768 = 97,536 bytes. This is 256 bytes more than two banks hold. Thus the starter and STARFALL grant three banks. A game that draws only with a sprite scene needs no canvas.
Requirements of each part
| Part | Requirement |
|---|---|
| Keyboard and buttons | None |
| Near moves and fills | None |
| Resources, surfaces and packs | ti_init |
| A video session | ti_init, with space for the 32,768-byte screen backup |
| A sprite scene | An active video session; no surface |
| Audio and speech | The slot-4 card. During a video session, also a free slot and 32,768 resource bytes for ti_audio_init and ti_audio_shutdown |
The game clock, ti_audio_ticks | Initialized audio |
| Joystick reads | Initialized audio, which owns the VIA timers |
| Fixed-rate steps | A device tick source, usually ti_audio_ticks |
| File reads and writes | A linker layout that defines the ProDOS file buffer. During a video session, also a free slot and 32,768 resource bytes |
Startup and shutdown
The startup code of the project runs before main. It does these steps:
- It saves the cc65 zero page bytes, the page-one trampoline area, the entry processor status and the display switches that it can read.
- It masks interrupts.
- In the full-game layout, it copies the low segments into position.
- It clears BSS and the near arena.
- It prepares the C stack.
- It enables interrupts again if they were enabled at entry.
A usual game then calls these functions in this sequence:
ti_initto grant banks.ti_gfx_surface_createandti_gfx_beginfor the screen.ti_audio_initfor sound and the clock, andti_steps_beginfor fixed-rate updates.- The game loop.
ti_audio_shutdown,ti_gfx_end,ti_gfx_surface_freeandti_shutdown.
The last step is optional. When main returns, or the game calls exit, startup calls ti_shutdown. This function stops audio, ends the video session, restores the saved AUX0 screen, removes acceleration hooks and makes all resources invalid. Startup then restores the saved machine state and calls ProDOS QUIT. A reset or a power loss cannot do this cleanup.
If ProDOS does not remove the audio interrupt handler, shutdown returns TI_E_IO. The hardware is silent and the handler code stays in memory. Startup does not quit, because after a quit ProDOS can call the unloaded handler code. It shows CLEANUP ERROR. KEY RETRIES; CODE RETAINED on the text screen and tries again when you push a key.
All functions run in the foreground
All libtini functions are synchronous, and no function is reentrant. An interrupt or NMI handler must not call libtini. It must not change memory mappings or the RamWorks selector, touch the libtini zero page or read granted banks. The interrupt handler of the library, the audio clock, is assembly code. It only counts ticks and acknowledges the timer.
During long work, the game must keep the sound active. A sprite scene calls its service function after each fourth band. A game can also call ti_audio_update between its own draw calls.
Errors
Each function that can fail returns a ti_error. TI_OK is zero. Most games do not test each call. They put the calls in ti_error_keep, which keeps the first failure, and they examine ti_error_first one time in each frame. Types and errors gives the codes.
ti_error_keep(ti_gfx_rect(&canvas, 0, 0, 320, 12, 1));
ti_error_keep(ti_gfx_text(&canvas, 8, 2, "SCORE", 13));
ti_error_keep(ti_gfx_present(&canvas, &bar, 0));
if (ti_error_first()) stop_game();
Device time and display edges
A CPU delay loop cannot time a game that must run at the same speed at 33 MHz and in TURBO. libtini uses the VIA timer of the slot-4 card. ti_audio_init sets the timer to a nominal 100 ticks each second (TI_AUDIO_TICKS_PER_SECOND), and an interrupt handler counts the ticks. ti_audio_ticks reads the count. The real rate comes from the clock of the card. The via_hz setting accepts a calibrated value.
The vertical blank flag of the //e at $C019 is different. It is a level that libtini samples. ti_time_poll counts the blank-to-active edges that it sees. This count is a lower limit of the frames that the display shows. It is not a clock.
Some work stops the clock intentionally. During a file call, ProDOS controls the machine and the audio driver pauses. The tick count does not increase, so the pause is not game time. A long interruption that the driver does not know about causes a gap. ti_audio_update and ti_steps_due report the gap and do not add time. Game loop and timing shows how a game handles the two conditions.
Pixels on the screen
libtini draws into software surfaces in banked memory, or composes a sprite scene in a near strip. It then copies the changed bytes to AUX bank 0 with usual CPU writes. When ti_gfx_present returns, these writes are complete.
There is no second screen page and no atomic page swap. The Appletini samples the screen at its own frame boundary. Thus a large update can show half old and half new data. Keep updates small: publish only the rectangles that change. ti_gfx_present and ti_gfx_present_rows refuse an update that is larger than their byte budget, before they write. An optional clock measures each publication.
The CPU-only build and the optional AMEM build
The default build contains only CPU code paths. tools/build.py --private --build-dir build/private also links a transport for the private AMEM memory service of the Appletini. In this build, ti_amem_enable first checks the exact controller and service. Large banked transfers can then use AMEM, with the same results as the CPU path. The CPU always writes the screen. See Optional AMEM.