gigatron/rom/Contrib/dhkolf/libgtemu/README.md
2025-01-28 19:17:01 +03:00

542 lines
16 KiB
Markdown

libgtemu -- David Kolf's Library for Gigatron Emulation
=======================================================
Version 0.4.0
About
-----
libgtemu provides functions for emulating the [Gigatron TTL microcomputer][gt]
and rendering its graphics through the [SDL library][sdl].
It can be used as a lightweight stand-alone emulator as well as being
controlled by a scripting language such as [Lua][lua].
[gt]: https://gigatron.io/
[sdl]: http://www.libsdl.org/
[lua]: https://www.lua.org/
License
-------
Copyright (C) 2019 [David Heiko Kolf](http://dkolf.de/).
Published under the [BSD 2-Clause License][lic].
[lic]: https://opensource.org/licenses/BSD-2-Clause
Compiling
---------
You will probably have to modify the provided Makefile to fit to your
system, but there are only a few files and few dependencies. As an example
for compiling Lua bindings you might look at [LPEG][lpeg].
[lpeg]: http://www.inf.puc-rio.br/~roberto/lpeg/#download
### Files
* Main library: gtemu.c, gtloader.c and gtsdl.c
* Example application: gtmain.c
* Lua bindings: luagt.c
The main library could also be compiled without gtsdl.c if you want to
use a different library for graphics and sound.
### Dependencies
* [Core SDL][sdl] at least version 2.0.4.
* [Lua][lua] version 5.3 (for the Lua bindings).
Future
------
The following features might be nice to have:
* Python bindings: Python is used for most other scripts related to the
Gigatron, so it would be nice to be able to use this emulator from
Python as well.
* Proper Makefile and precompiled binaries for Windows.
* More RAM aliases in the script bindings.
* IIR audio filters to replicate the sound output of the Gigatron more
precisely. According to the [Gigatron schematics][scm] (page 8/8)
there are two single order filters in series, a low pass at 700 Hz
and a high pass at 160 Hz that overlap to reduce the peak volume by a
factor of 5.
[scm]: https://cdn.hackaday.io/files/20781889094304/Schematics.pdf
Stand-alone emulator
--------------------
The stand-alone emulator offers the following basic features on the
command line:
usage: ./gtrun [-h] [options]
Arguments:
-h Display this help.
-l filename.gt1 GT1 program to be loaded at the start.
-t filename.gtb Text file to be sent with Ctrl-F3.
-r filename.rom ROM file (default name: gigatron.rom).
-64 Expand RAM to 64k.
Special keys:
Ctrl-F2 Send designated GT1 file.
Ctrl-F3 Send designated text file.
Alt-L Perform hard reset and select loader.
Alt-X Perform hard reset and send GT1 file.
ESC Close the emulation.
Structure and C-API
-------------------
This library is split into two parts: pure emulation functions without
dependencies and SDL rendering functions. You can use the emulation
functions directly and use other means to output graphics and sound.
### Emulation and peripherals
The core emulation is written without external dependencies and just
performs the raw computations. Its functions are defined in the header
file gtemu.h.
gtemu.h defines two major structures: `GTState` and `GTPeriph`. `GTState`
containes the state of the CPU and its fields are open for inspection and
manipulation:
struct GTRomEntry {
unsigned char i;
unsigned char d;
};
struct GTState {
int pc;
unsigned char ir, d, ac, x, y, out, in;
struct GTRomEntry *rom;
size_t romcount;
unsigned char *ram;
unsigned int rammask;
};
`GTPeriph` on the other hand contains the state of the peripherals; the
board, the video output, the audio output, the GT1 loader and the serial
output. The variables are implementation details and should not be
accessed directly.
struct GTPeriph;
#### gtemu_init
extern void gtemu_init (struct GTState *gt,
struct GTRomEntry *rom, size_t romsize,
unsigned char *ram, size_t ramsize);
Initializes the contents of the `GTState` structure and sets
pointers to the given RAM and ROM arrays. You can (and should) initialize
the content of those arrays. `romsize` is the size in bytes, not the number
of entries.
The RAM size has to be at least 256 bytes.
#### gtemu_initperiph
extern void gtemu_initperiph (struct GTPeriph *ph, int audiofreq,
unsigned long randseed);
Initializes the state of the simulated peripherals.
`audiofreq` should be a valid value even if you do not want to output the
sound (just set it to 48000). Otherwise you can get it from the SDL
functions. `randseed` is a seed value for the random number generator.
#### gtemu_randomizemem
unsigned long gtemu_randomizemem (unsigned long seed,
void *mem, size_t size);
Randomizes a designated area of memory (like the RAM or the ROM). The
return value is the new state of the xorshift32-random number generator.
#### gtemu_getclock
extern unsigned long long gtemu_getclock (struct GTPeriph *ph);
Returns the total number of instructions executed so far.
#### gtemu_getxout
extern unsigned char gtemu_getxout (struct GTPeriph *ph);
Returns the current state of the XOUT register (the "blinkenlights"
and the sound output).
#### gtemu_processtick
extern int gtemu_processtick (struct GTState *gt, struct GTPeriph *ph);
Advances the emulation one tick further. This does not output any graphics
or sound but still keeps track of the position of the video beam.
It returns the value of rising edges on the synchronization signals in OUT.
#### gtemu_processscreen
extern int gtemu_processscreen (struct GTState *gt, struct GTPeriph *ph,
void *pixels, int pitch,
unsigned short *samples, size_t maxsamples, size_t *nsamples);
Advances the simulation until either the next rising edge of the VSync
signal was detected or 110000 steps were processed.
This function is called by internally by `gtsdl_render` (and
`gtsdl_runuiframe`), you only need to call it when not using one of those
functions.
It will output the pixels in a 16-bit RGB444 format, the pitch variable
determines the length of one row in bytes. The pixel and pitch arguments
are chosen to be compatible with the `SDL_LockTexture` function from
[SDL][sdl].
The sound samples are expected to be 16-bit in native byte order. This
function will output only positive 15-bit numbers to be compatible with
both unsigned and signed formats.
The pointers `pixels` and `samples` may be NULL if you are not interested
in the output.
`nsamples` points to a variable with the current position inside the samples
array and must not be NULL, even if no sound is being played. Just let
it point to a dummy variable on the stack.
The return value is the number of instructions that were executed.
#### gtemu_placelights
void gtemu_placelights (struct GTPeriph *ph, void *pixels, int pitch,
int power);
Places LEDs as pixels on the bottom of the screen. The positions are
chosen to not obscure any pixel of the emulation completely and the colors
are chosen from outside the palette available to Gigatron applications.
This function is called internally by `gtsdl_render` (and
`gtsdl_runuiframe`), you only need to use it when not using one of those
functions.
`power` indicates the state of the power LED. The default rendering function
uses it to indicate that the frame rate is at the expected 60 FPS.
#### gtserialout_setbuffer
void gtserialout_setbuffer (struct GTPeriph *ph, char *buffer,
size_t buffersize, size_t *bufferpos);
Sets the buffer into which emulated programs can write data by manipulating
the synchronization signals. `bufferpos` points to a variable containing
the current index into the buffer. You can change that variable at any
time to make place for new data.
TinyBasic on the Gigatron sends an empty newline (`\n`) when it wants to
clear the buffer and start a new output. In this emulation the empty line
is just placed into the buffer as well without having any built-in special
effects.
#### gtloader_sendgt1
extern int gtloader_sendgt1 (struct GTPeriph *ph,
const char *data, size_t datasize);
Instructs the emulation to send the contents of a GT1 file to the emulated
Gigatron that can be evaluated by the Loader program. This function does
not check whether the GT1 is valid and expects the Loader application to be
running already.
The memory pointed to by `data` must stay valid until everything is sent.
It returns 1 on success and 0 on failure when there is still previous data
that was not completely sent.
#### gtloader_isactive
extern int gtloader_isactive (struct GTPeriph *ph);
Returns whether the loader is active at the moment.
#### gtloader_validategt1
extern int gtloader_validategt1 (const char *data, size_t datasize);
Validates a GT1 file.
The return value is 1 if the contents were valid, 0 otherwise.
#### gtloader_sendtext
extern int gtloader_sendtext (struct GTPeriph *ph,
const char *data, size_t datasize);
Sends data as keystrokes to the emulated Gigatron that can be received by
TinyBASIC and other programs accepting text input.
The memory pointed to by `data` must stay valid until everything is sent.
It returns 1 on success and 0 on failure when there is still previous data
that was not completely sent.
#### gtloader_sendkey
extern int gtloader_sendkey (struct GTState *gt, struct GTPeriph *ph,
char key);
Sends a single keypress to the emulated Gigatron. When two different keys
are sent quickly after each other, the second key will directly replace
the input value without first sending a null input.
It returns 1 on success and 0 on failure when there is still previous data
that was not completely sent.
### SDL
The necessary resources for SDL and some state are kept in the GTSDLState
structure. You should access its content only through the provided
functions.
struct GTSDLState;
#### gtsdl_openwindow
extern int gtsdl_openwindow (struct GTSDLState *s, const char *title);
Initializes the necessary SDL subsystems (you should call `SDL_Init` before)
and creates a standalone window.
It returns 1 in case of success and 0 in case of errors. The error can
be requested using `SDL_GetError`.
#### gtsdl_getaudiocallback
extern SDL_AudioCallback gtsdl_getaudiocallback();
Returns a pointer to the audio callback. Can be used if you want to set
up audio on your own (see also `gtsdl_setup`).
#### gtsdl_setup
extern int gtsdl_setup (struct GTSDLState *s, SDL_Renderer *renderer,
SDL_AudioDeviceID audiodev, SDL_AudioSpec *audiospec);
This function is an alternative to `gtsdl_openwindow` in case your
application initialized SDL and created an window on its own.
The audiospec has to be 16-bit integer and you can get the callback
function through `gtsdl_getaudiocallback`.
In case you want to mix the audio you could avoid the provided SDL
functions of this library and just call `gtemu_processscreen` directly in
your own sound and video rendering function.
It returns 1 in case of success and 0 in case of errors. The error can
be requested using `SDL_GetError`.
#### gtsdl_close
extern void gtsdl_close (struct GTSDLState *s);
Closes all SDL resources that were requested by this library. In case of
the `gtsdl_openwindow` function it will free all resources and you just need
to call `SDL_Close`. In case of the `gtsdl_setup` function it will not free
resources allocated outside of the library.
#### gtsdl_getaudiofreq
extern int gtsdl_getaudiofreq (struct GTSDLState *s);
Returns the frequency of the used audio device.
#### gtsdl_render
extern SDL_Texture *gtsdl_render (struct GTSDLState *s, struct GTState *gt,
struct GTPeriph *ph);
Emulates and renders a single frame, queues the audio and returns the
finished texture.
This function should be used when the library was initialized with
`gtsdl_setup`. You need to keep control of the frame rate yourself,
otherwise the emulation might get ahead of the audio output.
#### gtsdl_runuiframe
extern int gtsdl_runuiframe (struct GTSDLState *s, struct GTState *gt,
struct GTPeriph *ph, SDL_Event *ev);
Renders one frame in the standalone window created by
`gtsdl_openwindow` and checks for `SDL_Events` using `SDL_PollEvent`.
In case there is a event this function returns immediately without any
further actions with the return value 1. The SDL_Event structure contains
the current event.
When there is no event it will emulate and render one frame and will call
`SDL_Delay` to limit the frame rate to 60 FPS. In this case the return
value is 0;
#### gtsdl_handleevent
extern int gtsdl_handleevent (struct GTSDLState *s, struct GTState *gt,
SDL_Event *ev);
Processes keyboard events and updates the IN register accordingly. It
returns 1 if it was a handled event, 0 otherwise.
Lua bindings
------------
### About Lua
[Lua][lua] is a lightweight scripting language that can be embedded into
other applications as well as being used as a standalone interpreter with
an interactive prompt.
The Lua bindings for the emulation library can be loaded using
gtemu = require "gtemu"
assuming that the DLL is in the packages search path.
### API
#### gtemu.initsdl ()
Calls `SDL_Init` and registers `SDL_Close` for `atexit`.
#### gtemu.openwindow (title)
Initializes the SDL subsystems and creates a window. A window object is
returned.
#### window:runloop (emulation, eventhandlers)
Runs an event loop for the specified emulation (see below) until either the
event `SQL_QUIT` was received, one of the eventhandlers requested a break
or the Escape key was detected.
`eventhandlers` is an optional table where the callback functions
`onkeydown (keyname, mods, repeated, scancode, keycode)`,
`onkeyup (keyname, mods, repeated, scancode, keycode)`,
`ontextinput (text)`, `ondropfile (filename)` and `onframe ()` can be
defined.
The functions can return either `true`, `false` or the string `"break"`.
When `true` is returned the event handling is finished, for `false` the
default event handling is executed, for `break` the loop is interrupted.
#### window:close ()
Closes the window and releases the SDL subsystems. This function is called
automatically when the window object is garbage collected or at the end
of the application.
#### gtemu.newemulation (window [, ramsize])
Initializes the emulation. `ramsize` can be either 32 or 64. When
omitted it defaults to 32.
This function returns an emulation object.
Example of setting up the application and starting an emulation:
gtemu = require "gtemu"
gtemu.initsdl()
window = gtemu.openwindow("Gigatron in Lua")
emulation = gtemu.newemulation(window, 64)
f = assert(io.open("gigatron.rom", "rb"))
emulation:loadrom(f:read("*a"))
f:close()
window:runloop(emulation)
#### Properties of emulation
In the emulation the following registers can be both read and modified:
pc, ir, d, ac, x, y, out, inp
For example:
if emulation.pc == 0x0123 then
emulation.inp = 0xff
end
The contents of the RAM can be accessed using the array notation:
if emulation[0x0123] == 0x10 then
emulation[0x1234] = 0x20
end
There is a special variable that maps the the vPC memory location in
ram:
if emulation.vpc >= 0x5a0c then
-- An application (probably the Loader) is running inside
-- screen memory
end
emulation.vpc = 0x200
This is equivalent to manual access to the RAM locations 0x16 and 0x17.
Further aliases might be defined in future versions.
The following read-only values are also available:
clock, xout
#### emulation:loadrom (data)
Loads a string into ROM:
f = assert(io.open("gigatron.rom", "rb"))
emulation:loadrom(f:read("*a"))
f:close()
#### emulation:processtick ()
Advances the simulation for a single tick without any output.
#### emulation:processscreen ()
Advances the simulation for an entire screen without any output.
#### emulation:sendgt1 (data)
Sends data from a GT1 file to the Loader application (assuming it is
running):
f = assert(io.open("Overworld.gt1", "rb"))
gt1 = f:read("*a")
f:close()
if emulation.vpc >= 0x5a0c then
emulation:sendgt1(gt1)
end
The data is automatically verified before sending, an error will be raised
for invalid data.
#### emulation:createbuffer (size)
Creates a buffer the Gigatron can send output to.
#### emulation:getbuffer ()
Requests the current contents of the buffer.
#### emulation:resetbuffer ()
Clears the buffer.