RTR logo


Accessing the SDL 2.0 API

Introduction to the SDL 2.0 API

BBC BASIC for SDL 2.0 allows you to access the SDL 2.0 Application Program Interface (API) by means of the SYS statement. There are several hundred API functions, many of which are unlikely to be of use to a BASIC program, and no attempt will be made to provide a comprehensive list here. Further details can be found in the SDL 2.0 documentation on their web site.

The SYS statement allows you to call an API function either by name or by specifying its memory address (pointer). The latter method is a little faster, so when speed is important you may wish to discover, in the initialisation section of your program, the address(es) of any functions you later call, especially if you call them frequently. You can do that as follows:

`SDL_SetWindowTitle` = SYS("SDL_SetWindowTitle")
You do not need to adopt this naming convention, but it is important that the address be assigned to a variable capable of holding a 64-bit pointer, i.e. either a 64-bit integer (%% suffix) or a numeric variant (no suffix).

Changing the window title

Normally, the text in the title bar of BASIC's output window is set to the name of the BASIC program itself. You can change the title by using the SDL_SetWindowTitle function:
title$ = "New title"
SYS "SDL_SetWindowTitle", @hwnd%, title$, @memhdc%

Finding the display size

You may wish to know the width and height of the desktop area in order to select an appropriate screen mode for your program. You can do that using the SDL_GetDisplayUsableBounds function:
DIM rc{x%, y%, w%, h%}
SYS "SDL_GetDisplayUsableBounds", 0, rc{}, @memhdc%
xscreen% = rc.w%
yscreen% = rc.h%
The above program segment will load the variable xscreen% with the width of the desktop and the variable yscreen% with the height of the desktop, both in pixels.

Displaying a message box

If you want to display a message in its own 'message window', you can use the SDL_ShowSimpleMessageBox function:
message$ = "Test message"
caption$ = "Test caption"
SYS "SDL_ShowSimpleMessageBox", flags%, caption$, message$, @hwnd%, @memhdc%
The flags% parameter determines what kind of symbol is displayed:
16Error symbol
32Warning symbol
64Information symbol

Playing WAV files

If you want to play a sound which is stored in a standard WAV file, the most straightforward way is probably to use the audiolib library, but an alternative is to use this function:
DEF FNplaywave(wav$, mix%)
LOCAL R%, S%, r%%, s%% : PRIVATE Q%, q%%, audiospec{}
IF wav$ = "" THEN
  IF @hwo% SYS "SDL_CloseAudioDevice", @hwo%, @memhdc% : @hwo% = 0
  IF q%% SYS "SDL_FreeWAV", q%% : q%% = 0 : Q% = 0
  = TRUE
DIM audiospec{freq%, fmt{l&,h&}, channels&, silence&, samples%, size%, cb%%, userdata%%}
SYS "SDL_RWFromFile", wav$, "rb" TO r%% : IF @platform% AND &40 ELSE r%% = !^r%%
IF r%% = 0 THEN = FALSE   : REM Couldn't open WAV file
SYS "SDL_LoadWAV_RW", r%%, 1, audiospec{}, ^s%%, ^S%
IF s%% = 0 THEN = FALSE   : REM Couldn't load WAV file
IF @hwo% = 0 SYS "SDL_OpenAudioDevice", FALSE, FALSE, audiospec{}, 0, 0, @memhdc% TO @hwo%
IF @hwo% = 0 THEN = FALSE : REM Couldn't open audio device
SYS "SDL_GetQueuedAudioSize", @hwo%, @memhdc% TO R%
IF mix% IF S% <= (Q% - R%) IF q%% THEN
  SYS "SDL_MixAudioFormat", q%% + Q% - R%, s%%, &FFFF AND !^audiospec.fmt.l&, S%, mix%
  SYS "SDL_FreeWAV", s%% : s%% = q%% + Q% - R% : S% = R%
  IF q%% SYS "SDL_FreeWAV", q%%
  q%% = s%% : Q% = S%
SYS "SDL_PauseAudioDevice", @hwo%, 1, @memhdc%
SYS "SDL_ClearQueuedAudio", @hwo%, @memhdc%
SYS "SDL_QueueAudio", @hwo%, s%%, S%, @memhdc%
SYS "SDL_PauseAudioDevice", @hwo%, FALSE, @memhdc%
The wav$ parameter should be set to the (path and) filename of the WAV file to be played, or to an empty string ("") to stop a previous file playing and to shut down the sound system. The mix% parameter should be set to zero to play the file normally (replacing any previous WAV file still being played), or to a non-zero value (1 to 128) to mix the new file with a currently-playing WAV at the specified level (128 corresponding to '100%').

Note that the mix facility only works if the remaining duration of the previously-playing WAV file is greater than or equal to the total duration of the new sound. Otherwise the function will behave as though mix% was zero (and there is no control over the level). It is most useful for playing short-duration sound effects over a background music track.

The function returns TRUE if the WAV file was opened and played successfully, and FALSE if not (for example the file could not be opened, or was in a format that wasn't recognised).

Timing program execution

You can discover how long BBC BASIC for SDL 2.0 has been running by calling the SDL_GetTicks function; this can be a useful adjunct to the built in TIME pseudo-variable:
SYS "SDL_GetTicks" TO tick%
The value of tick% will be set to the number of milliseconds since BBCSDL was started (it wraps around to a negative value after approximately 24 days and 20 hours). If this could be an issue there is a 64-bit version of the function:
SYS "SDL_GetTicks64" TO tick%%

Pausing a program

A common way of pausing a program under software control is to use the INKEY function:
pause = INKEY(delay%)
or to use the TIME pseudo-variable:
TIME = 0
However both of these methods have their disadvantages. The INKEY delay can be truncated by pressing a key, which may be undesirable, and the TIME method keeps the processor fully occupied, so other applications will run slowly whilst your program is paused. A better method is to use WAIT:
WAIT delay%
This is probably the best method for long delays, but an alternative is to use the SDL_Delay function:
SYS "SDL_Delay", delay%
The program will pause for approximately delay% milliseconds. Note that during this time the ESCape key is not tested, nor can the window be closed. Therefore this method should be used only for short delays.

Discovering an 'unknown error'

If SDL 2.0 reports an error condition which BASIC was not expecting, an Unknown error (error code 255) results. You can discover the true cause of the error by using the SDL_GetError function:
DEF FNsdlerror
LOCAL message%%
SYS "SDL_GetError" TO message%%
IF @platform% AND &40 ELSE message%% = !^message%%
= $$message%%
However be aware that it's possible that another thread has overwritten the message before you had a chance to read it.

Repositioning the window

You can set the position of BASIC's output window without changing its size by calling the SDL_SetWindowPosition function:
SYS "SDL_SetWindowPosition", @hwnd%, xpos%, ypos%, @memhdc%
The position is specified as the offset, in pixels, from the top-left corner of the desktop to the top-left corner of BASIC's output window. To change the window's size without moving it you can do the following:
SYS "SDL_SetWindowSize", @hwnd%, width%, height%, @memhdc%
VDU 26
The width and height are specified in pixels, including the border and the title bar. If want to specify the dimensions excluding the border and title bar, i.e. of the region usable by BASIC, then the best way of doing it is to select a user-defined mode with VDU 23,22....

Whenever you intentionally change the window size, you should reset BASIC's text and graphics clipping regions to the new size by issuing the VDU 26 command.

To discover the current size of the window you can use the SDL_GL_GetDrawableSize function, which returns the width and height in pixels:

SYS "SDL_GL_GetDrawableSize", @hwnd%, ^Width%, ^Height%, @memhdc%
This gives the usable size of the window, i.e. excluding the title bar etc.

Fixing the window size

Normally the user can re-size the output window by dragging the side or corner to a new position. However there may be circumstances when you would prefer to fix the window size, and prevent it from being changed by the user. You can do that as follows:
SYS "SDL_SetWindowResizable", @hwnd%, FALSE, @memhdc%

Minimising or maximising the window

You can minimise BASIC's output window under program control using the SDL_MinimizeWindow function:
SYS "SDL_MinimizeWindow", @hwnd%, @memhdc% 
This will have exactly the same effect as clicking on the minimise button in the title bar. To restore the window to its original size and position:
SYS "SDL_RestoreWindow", @hwnd%, @memhdc%
To maximise the window (equivalent to clicking on the maximise button):
SYS "SDL_MaximizeWindow", @hwnd%, @memhdc%
VDU 26
If you maximise the window you should normally use a VDU 26 to reset the text and graphics viewports so they fill the full area.

Bringing the window to the front

To bring your window to the front you can use the SDL_RaiseWindow function:
SYS "SDL_RaiseWindow", @hwnd%, @memhdc%

Removing the title bar

You can remove the title bar and borders from the output window as follows:
SYS "SDL_SetWindowBordered", @hwnd%, FALSE, @memhdc%
As this removes the close button, make sure you provide an obvious means for quitting the program (Alt-F4 will still work so long as you haven't used ON CLOSE).

Using the entire screen

If you want your program to run 'full screen', without even a title bar or borders, you can do that as follows:
SYS "SDL_SetWindowFullscreen", @hwnd%, &1001, @memhdc%
VDU 26
When you run your program not even the taskbar will be visible, nor will the user be able to re-size or move the window, so make sure you provide an obvious means for quitting the program (Alt-F4 will still work so long as you haven't used ON CLOSE).

To restore windowed output do:

SYS "SDL_SetWindowFullscreen", @hwnd%, 0, @memhdc%
VDU 26

Automatically scaling the output

You can arrange that on a window resize event (which includes maximizing the window) your program's output can be automatically scaled (and centred if necessary) to fit the new window size. This is much easier than your program scaling and redrawing everything itself, but the resulting quality may be a little poorer.

To do this incorporate an ON MOVE interrupt as follows (typically during the initialisation phase of your program, but after setting the initial window size with MODE or VDU 23,22...):

IF POS REM Thread sync to ensure @size{} is not 'stale'
ON MOVE PROCresize(@msg%, @lparam%, @size.x%, @size.y%) : RETURN
and include the following procedure, for example at the end of your program:
DEF PROCresize(M%, L%, X%, Y%) IF M% <> 5 ENDPROC
H% = L% >>> 16
IF W%/H% > X%/Y% THEN
  @zoom% = &8000 * H% / Y%
  @zoom% = &8000 * W% / X%
IF @zoom% < &8000 @zoom% = &8000
IF (@platform% AND 7) < 3 THEN
  @panx% = (X% - W% * &8000 / @zoom%) / 2
  @pany% = (Y% - H% * &8000 / @zoom%) / 2
Note: Do not include a VDU 26 in your program, it is important that the logical size of the 'screen' (output canvas) remains fixed at its original value.

Using the clipboard

You may want your program to be able to access the clipboard, either to put text on the clipboard or to read text from the clipboard. The following program segment writes the string text$ to the clipboard; if you want to include a new-line you should append a carriage-return, line-feed pair to the string as shown in the example:
text$ = "The five boxing wizards jump quickly"+CHR$13+CHR$10
SYS "SDL_SetClipboardText", text$
To check whether the clipboard contains any text, you can use the following function:
SYS "SDL_HasClipboardText" TO res%
which will set res% to 1 if there is text in the clipboard and to 0 otherwise. Once you have established that there is text available, you can read it with the following function:
DEF FNgetclipboardtext
SYS "SDL_GetClipboardText" TO t%%
IF @platform% AND &40 ELSE t%% = !^t%%
IF t%% = 0 THEN = ""
= $$t%%
The SDL 2.0 clipboard always uses UTF-8 encoding.

Loading or saving part of a file

To load a file into memory you can use the *LOAD command, which is very fast. However, this only allows you to load an entire file. If you want to load part of a file you can read it into a string using GET$#file BY size% and, if necessary, discover the memory address of the string using PTR.

But if you want to read part of a file directly into memory rather than via a string, you can do that as follows:

DIM store%% size%-1
file% = OPENIN(filename$)
PTR#file% = offset%%
SYS "SDL_RWread", @hfile%(file%), store%%, 1, size%
CLOSE #file%
Note the use of the system variable @hfile%() to discover the SDL 2.0 file context.

Similarly to write part of a file you can use BPUT #file,a$; to write the contents of a string, but if you want to write directly from memory rather than via a string you can do that as follows:

DIM store%% size%-1
file% = OPENUP(filename$)
PTR#file% = offset%%
SYS "SDL_RWwrite", @hfile%(file%), store%%, 1, size%
CLOSE #file%

If you need to call this routine multiple times ensure that you move the DIM statement so that it will be executed only once. If you don't, you may eventually run out of memory. Alternatively, consider using DIM LOCAL.

Changing the thread priority

There may be rare occasions when your BASIC program has to perform a time-critical operation, during which time you don't want any other programs (or the operating system) to interrupt it. You can achieve this by setting the priority of the interpreter's thread to its highest possible value:
SYS "SDL_SetThreadPriority", 2
The following program segment restores the priority to its normal value:
SYS "SDL_SetThreadPriority", 1
You should use this facility with extreme care, because while the priority of your program is raised other programs may run very slowly or not at all. You may not even be able to stop or interrupt your own program! You should raise the priority only for the shortest possible period, and then return it to normal.

Checking for input focus

You may wish to know whether your program currently has the input focus, that is whether keyboard and mouse input is being directed to your program. You can test that using the SDL_GetWindowFlags function as follows:
SYS "SDL_GetWindowFlags", @hwnd%, @memhdc% TO flags%
IF flags% AND &200 THEN
  REM program has input focus

Downloading files from the internet

The following function will download a file from the internet, assuming you have a connection; it uses the socklib library which must be INSTALLed and initialised beforehand:
DEF FNhttpget(url$, T%)
LOCAL b$, I%, S%, host$, path$, ret$
b$ = STRING$(256, CHR$0)
S% = INSTR(url$,"//")+2 : IF S%=2 S%=1
I% = INSTR(url$,"/",S%)
host$ = MID$(url$,S%,I%-S%)
path$ = MID$(url$,I%)
S% = FN_tcpconnect(host$, "80")
I% = FN_writelinesocket(S%,"GET "+path$+" HTTP/1.0")
I% = FN_writelinesocket(S%,"Host: "+host$)
I% = FN_writelinesocket(S%,"User-agent: BBC BASIC")
I% = FN_writelinesocket(S%,"Accept: */*")
I% = FN_writelinesocket(S%,"")
T% += TIME
  I% = FN_readsocket(S%,PTR(b$),256)
  IF I% = 0 WAIT 1
  IF I% > 0 ret$ += LEFT$(b$,I%)
CASE VALMID$(ret$,9,255) OF
  WHEN 200:
    I% = INSTR(ret$,CHR$&D+CHR$&A+CHR$&D+CHR$&A)
    IF I% ret$ = MID$(ret$,I%+4)
  WHEN 301:
    I% = INSTR(ret$,"Location:")
    IF I%=0 I% = INSTR(ret$,"location:")
    IF I% ret$ = FNhttpget("http://" + host$ + $(PTR(ret$) + I% + 9), T% - TIME)
= ret$
The parameter url$ should be set to the URL of the file or page you want to download; this must be a regular (http://) URL, not an encrypted (https://) URL. The second parameter should be set to a timeout value, in centiseconds, after which the function will return even if the file has not been fully read.

The requested file is returned as a string; you can easily write it to a local file if you wish. If an empty string is returned the file could not be read within the specified timeout period (e.g. you are not connected to the internet).



Best viewed with Any Browser Valid HTML 3.2!
© Richard Russell 2023