Alternate-history fiction: Avenell Stagecraft and the Cyclorama console never existed. The hardware described here is real — fully specified, and emulated at playcyclorama.com. Play ERG: NIGHTSIDE RUN

Cyclorama SDKSDK library reference

SDK library reference

This page lists every routine a program can call from the SDK runtime: the SCRIM runtime library (src/scrim/runtime.inc), which the compiler links into a program that uses it, and the Cue Sound Driver (roms/lib/cuesound.inc), which a program includes or links. For each routine it gives the call, the inputs and outputs, the registers and flags it changes, the stack and RAM it uses, and its cost in CPU cycles.

Every number on this page was measured on the reference emulator. tests/example-sdk-library.test.ts measures them again and fails if the page and the emulator disagree. The calling examples are quoted from two programs in examples/sdk-library/, which that test also runs.

This page does not repeat how to use the driver or write its music: that is the driver's README, with the .song format. The SCRIM calling convention is in the language specification, §4, and linking the driver into a SCRIM program in §10.

How the numbers were measured

Summary

RoutineConventionWhat it does
__divuregistersunsigned / and MOD
__divsregisterssigned / and MOD
MEMCPYSCRIMcopy bytes
MEMSETSCRIMfill bytes
VRAM_COPYSCRIMDMA words to VRAM
RANDOMSCRIMnext pseudo-random word
SEED_RANDOMSCRIMrestart the sequence
WAIT_VBLANKSCRIMwait for line 224
SND_INITassemblysilence the Cue, clear the driver
SND_PLAY_SONGassemblystart a song
SND_STOPassemblystop the music
SND_SFXassemblystart a sound effect
SND_TICKassemblyplay one frame of music and effects
SND_TEMPOassemblyoverride the tempo
SND_FADEassemblyfade the music out
SND_STATUSassemblywhat is playing
SndInit … SndStatusSCRIMthe driver entries for SCRIM
SND_FREQStableFREQ of every note

SCRIM runtime

The compiler links a runtime routine only when the program uses it (§6, rulings 3, 17 and 18 of §9). The routines named in capitals follow the SCRIM convention: arguments are pushed right to left as words, the caller removes them, and a result comes back in R0. __divu and __divs are the compiler's helpers for / and MOD, with arguments in registers; a SCRIM program cannot call them by name, because names beginning __ are reserved.

The example program calls each routine once. Its division, copies and random numbers:

From examples/sdk-library/runtime.scr, lines 37–50

BEGIN
  a := 1000; b := 7;
  q := a / b;                              -- WORD: __divu, 142
  r := a MOD b;                            -- 6
  s := -1000; t := 7;
  sq := s / t;                             -- INT: __divs, -142
  sr := s MOD t;                           -- -6: the dividend's sign
  MEMCPY(@copy[0], @source[0], 16);        -- 16 bytes, all even: the word path
  MEMSET(@fill[0], $A5, 5);                -- fill[0..4] = A5, fill[5] stays 0
  VRAM_COPY($0000, @source[0], 8);         -- 8 words to VRAM 0000
  SEED_RANDOM(1987);
  rnd[0] := RANDOM();
  rnd[1] := RANDOM();
  rnd[2] := RANDOM();

__divu

Unsigned 16-bit division, for / and MOD on WORD and BYTE operands. A constant divisor that is a power of two compiles to a shift or an AND instead, and costs no call.

CaseCycles
divisor 014
divisor 1-65,535 (b = the number of 1 bits in the quotient)312 + 2 × b

A division costs 312 to 344 cycles. 1000 / 7 = 142 (binary 10001110, four 1 bits) takes 320.

__divs

Signed 16-bit division, for / and MOD on INT operands. The quotient is truncated toward zero and the remainder takes the dividend's sign: −7 / 2 = −3 remainder −1, and 7 / −2 = −3 remainder 1. −32768 / −1 wraps to −32768, remainder 0.

CaseCycles
divisor 074
divisor not 0 (b = the number of 1 bits in the quotient's magnitude)372 + 2 × b

It costs 60 cycles more than __divu, which it calls. −1000 / 7 = −142 takes 380 cycles, and −32768 / −1 takes 374.

MEMCPY

MEMCPY(dst, src, n) copies n bytes upward from src to dst. It moves words when dst, src and n are all even, and bytes otherwise. Overlapping blocks are copied correctly only when dst < src. The compiler also calls it to assign a whole array or record.

CaseCycles
n = 024
dst, src and n even, n = 2 × w: word moves35 + 14 × w
otherwise: byte moves34 + 14 × n

A word move copies two bytes in the time a byte move copies one: 16 bytes cost 147 cycles when all three are even, 258 when one is odd.

MEMSET

MEMSET(dst, value, n) stores the low byte of value into n bytes from dst.

CaseCycles
n = 024
n ≥ 120 + 10 × n

VRAM_COPY

VRAM_COPY(vramAddr, src, words) copies words words from CPU address src (0000-DFFF) to VRAM at vramAddr with the DMA channel. The CPU is stopped until the transfer ends (manual 5.12.2).

CaseCycles
DMACTL written outside active display48 + 2 × w
DMACTL written during active display48 + 4 × w

w is the number of words. The routine writes DMACTL 32 cycles after it starts, and the beam at that moment sets the rate: active display is dots 0-255 of lines 0-223, that is, cycles 0-170 of each of those lines. Only the beam counts, not whether the display is on: the test measured the active-display rate with the display switched off. Eight words cost 64 cycles in vertical blank and 80 during active display.

RANDOM

RANDOM() returns the next state of a 16-bit Galois LFSR (taps B400, period 65,535). The state is the work-RAM word __rnd, which the compiler places after the program's VARs and the startup code sets to ACE1.

CaseCycles
state bit 0 = 025
state bit 0 = 127

The sequence depends only on the seed. After SEED_RANDOM(1987) (07C3) the first three values are B7E1, EFF0, 77F8.

SEED_RANDOM

SEED_RANDOM(seed) restarts the RANDOM sequence from seed. Seed 0 would stop the LFSR, so it becomes ACE1.

CaseCycles
seed not 022
seed 024

WAIT_VBLANK

WAIT_VBLANK returns at the start of the next vertical blank: it reads VCOUNT until the line is

  1. Called during vertical blank (lines 224-261), it first waits for that blank to end, so it

returns at line 224 of the next frame. Reading VCOUNT has no side effects, and the routine leaves the interrupt state alone. Its loop reads VCOUNT every 14 cycles, so it returns 18 to 31 cycles after line 224 begins.

CaseCycles
called at CPU cycle c of the frame, c < 51,072 (k = ⌈(51,060 − c) / 14⌉, at least 0)30 + 14 × k
called at the first cycle of line 224 (c = 51,072)59,754
called at the last cycle of the frame (c = 59,735)51,102

Called at the first cycle of line 0, it takes 51,102 cycles (k = 3,648). These counts are with interrupts off; an interrupt handler that runs during the wait adds its own time. The example uses it once, to start its frame loop at the top of vertical blank:

From examples/sdk-library/runtime.scr, lines 55–58

  WAIT_VBLANK;                             -- the top of vertical blank
  IE := 1;                                 -- the VBLANK raised at line 224 is pending:
  ASM EI END;                              -- onVblank runs at once
  WHILE TRUE DO WAIT END

Cue Sound Driver

A CYASM program defines SND_RAM (272 bytes of work RAM, even address) and includes cuesound.inc; a SCRIM program declares SND_RAM as a VAR and links it (README). The entries in capitals use the assembly convention: the argument, if any, in R0. The README's entry table says what each one does. SND_INIT, SND_PLAY_SONG, SND_STOP, SND_SFX and SND_FADE mask interrupts while they change the driver's state, then restore the caller's flags with SETF: all five flags come back as they were. That is measured, not promised: the driver's documented contract is that flags are not preserved, so rely on the convention (R1-R7 kept, R0 and the flags not) and not on this version's behaviour. The lower-case labels of cuesound.inc (snd_halt, snd_music, snd_row and others) are internal; their cost is part of the entries that call them.

The example program in assembly starts the song at power-on and calls SND_TICK first thing in each VBLANK:

From examples/sdk-library/driver.asm, lines 11–25

SND_RAM = $8100                         ; the driver's RAM: SND_RAM_SIZE (272) bytes, even
PREV    = $8000                         ; last frame's pad

        .org $0040
start:  MOVQ R0, #0
        ST   R0, [PREV]
        CALL SND_INIT                   ; voices off, driver RAM cleared, MASTERVOL 15
        MOVW R0, #SONG_tune
        CALL SND_PLAY_SONG              ; the first row plays on the next SND_TICK
        MOVQ R0, #1
        ST   R0, [IF]
        ST   R0, [IE]
        EI
main:   WAI
        BRA  main

From examples/sdk-library/driver.asm, lines 27–49

vblank: PUSHM R0-R2                     ; the entries change R0 only; R1 and R2 are ours
        CALL SND_TICK                   ; first thing in VBLANK: steady timing
        LD   R1, [PAD1]
        LD   R2, [PREV]
        ST   R1, [PREV]
        NOT  R2
        AND  R2, R1                     ; R2 = buttons newly pressed
        TST  R2, #16                    ; A: an effect on voice 2
        BEQ  .noa
        MOVW R0, #SFX_blip
        CALL SND_SFX
.noa:   TST  R2, #32                    ; B: the jingle replaces the song
        BEQ  .nob
        MOVW R0, #SONG_jingle
        CALL SND_PLAY_SONG
.nob:   TST  R2, #128                   ; START: one volume step every 4 frames, then stop
        BEQ  .nos
        MOVQ R0, #4
        CALL SND_FADE
.nos:   MOVQ R0, #1
        ST   R0, [IF]                   ; acknowledge the VBLANK
        POPM R0-R2
        RTI

The cycle counts below use the example's music (sdk-library.song): SONG_tune plays on all four voices and has a wavetable, SONG_jingle plays on voice 0 only, SFX_blip is an effect on voice 2 (priority 1) and SFX_thud one on voice 3 (priority 2). What each entry does is tested in tests/cuesound.test.ts.

SND_INIT

Keys off all four voices, clears the driver's RAM, gives each track its voice and sets MASTERVOL to 15. Call it once before any other entry.

CaseCycles
any state1,638

SND_PLAY_SONG

Starts a song from its first row. It first stops the song playing, as SND_STOP does, then loads the song's wavetable into WAVE0-7 if it has one, sets the master volume to 15 and cancels a fade. The first row plays on the next SND_TICK.

CaseCycles
v = voices the song uses, t = 1 if it has a wavetable, a and m as for SND_STOP302 + 28 × v + 114 × t + 28 × a + 22 × m

From silence, SONG_tune costs 528 cycles and SONG_jingle 330.

SND_STOP

Stops the music and keys off the voices it holds. A playing effect plays on.

CaseCycles
a = music tracks playing on a voice of their own, m = music tracks whose voice an effect has140 + 28 × a + 22 × m

SND_SFX

Starts an effect on the voice its header names. The music on that voice goes quiet until the effect ends (voice borrowing).

CaseCycles
no effect on its voice137
an effect of the same or lower priority on its voice: replaced147
an effect of higher priority on its voice: ignored85

SND_TICK

Plays one frame: the fade, the tempo, the music rows that fall due, the effect rows and the per-frame pitch effects. Call it once per frame, first thing in the VBLANK handler, so the timing is steady.

CaseCycles
nothing playing166
the first tick after SND_PLAY_SONG with SONG_tune: four notes key on1,444
the Sound Test measurement of the README: worst tick of the first 1,200 frames2,166
the same input: worst tick of 3,600 frames2,206

The cost depends on the music, so the last two rows repeat the README's own measurement: the Sound Test cartridge (roms/sound-test) from frame 17, with controller 1 pressing A, B, C and Left in turn, one on every other frame (the input of tests/sound-test.test.ts). Over its first 1,200 frames (20 seconds) the worst tick is 2,166 cycles, the README's figure. Run on to 3,600 frames, the same input reaches 2,206 cycles, 3.7 % of the frame. To budget for your own music, measure it: tickCycles in tools/song-wav.ts meters every call of SND_TICK in a running cartridge. The deepest stack measured, in the first tick of SONG_tune and in the Sound Test run, is 24 bytes; the README allows 26.

SND_TEMPO

Overrides the song's tempo with a rate: rows per frame × 55,440 (tempo). Rate 0 goes back to the song's own tempo.

CaseCycles
rate 0 (the song's own)30
rate 1-55,44026
rate above 55,440 (55,440 is used)28

SND_FADE

Fades the music out: the master volume goes down one step every n frames, and when it reaches 0 the music stops and the volume returns to 15. n = 0 cancels a fade and restores full volume.

CaseCycles
n = 0 (cancel)58
n = 1-25546
n above 255 (255 is used)48

SND_STATUS

Says what is playing.

CaseCycles
any state204

Stack-convention entries

A SCRIM program calls the driver through these entries, declared with EXTERN (§10). Each one loads its argument, if it has one, from [SP+2] into R0 and jumps to its entry. So it costs the entry's cycles plus the extra below, uses the same stack, and has the same effect.

EntryJumps toExtra cycles
SndInitSND_INIT6
SndPlaySongSND_PLAY_SONG10
SndStopSND_STOP6
SndSfxSND_SFX10
SndTickSND_TICK6
SndTempoSND_TEMPO10
SndFadeSND_FADE10
SndStatusSND_STATUS6

The SCRIM example declares the entries it uses, links the driver and its music, and calls SndTick from its VBLANK handler:

From examples/sdk-library/runtime.scr, lines 10–18

VAR SND_RAM: ARRAY [136] OF WORD;          -- the driver's RAM (272 bytes)
LINK "../../roms/lib/cuesound.inc";
ASSET "sdk-library.song";
EXTERN PROC SndInit;
EXTERN PROC SndPlaySong(song: WORD);
EXTERN PROC SndSfx(sfx: WORD);
EXTERN PROC SndTick;
EXTERN FUNC SndStatus: WORD;
EXTERN CONST SONG_tune: WORD; SFX_blip: WORD;

From examples/sdk-library/runtime.scr, lines 29–35

INTERRUPT PROC onVblank;
BEGIN
  SndTick;                                 -- once per frame, first thing in VBLANK
  frames := frames + 1;
  IF_PORT := 1
END;
VECTOR VBLANK := onVblank;

From examples/sdk-library/runtime.scr, lines 51–54

  SndInit;
  SndPlaySong(SONG_tune);
  SndSfx(SFX_blip);
  status := SndStatus();                   -- 4F: music on voices 0-3, the effect on voice 2

SND_FREQS

A table, not a routine: 108 words (216 bytes) in ROM, the FREQ value of each note from C-0 to B-8, in note-number order (C-0 = 0, A-4 = 57, whose FREQ is 601). The word for note n is at SND_FREQS + 2 × n. The README gives how the values are computed (note table).