mirror of
https://github.com/ivanizag/izapple2.git
synced 2026-10-22 00:02:27 +00:00
A libretro core
Runs izapple2 inside RetroArch and the other libretro frontends. It is built as a C shared library with cgo, so it needs a C toolchain, and it is not part of the releases. The frontend owns the frame timing, calling retro_run once per frame, so the core advances the machine with RunCycles and hands over one video frame and the matching audio samples on the thread it was called on, with no goroutines. A frame is 17030 cycles. While a drive asks for fast mode it runs sixteen frames worth, or loading a disk takes half a minute. Kept small on purpose: no super hi-res, no Videx video, no RGB card and no savestates. What is left is generated as 560x192, so the framebuffer is a fixed 564x192 and the geometry never changes. The cards that produce the other modes are taken out of the slots. Diskettes are swapped from the menu through the extended disk control interface, with the list coming from an M3U playlist, and the saves go to the save directory of the frontend as overlays. Go cannot call the function pointers the frontend hands over, so shim.c keeps them and reaches them through plain C functions, and the callbacks the core provides go the other way through it. Four entry points take a const pointer, which cgo cannot generate, so those are wrapped there as well. The default machine is the new gaming model, an enhanced //e with a Mockingboard, no mouse and no VidHD, which also serves the command line. See doc/libretro.md to use it, frontend/a2libretro/README.md for how it works.
This commit is contained in:
1 parent
9e11107e4f
commit
b98cf17696
25 files changed
+10313
-13
No files matched your search
@@ -12,6 +12,10 @@ frontend/*/*.zip
|
||||
frontend/*/images
|
||||
frontend/a2fyne/a2fyne
|
||||
frontend/headless/headless
|
||||
frontend/a2libretro/izapple2_libretro.so
|
||||
frontend/a2libretro/izapple2_libretro.dylib
|
||||
frontend/a2libretro/izapple2_libretro.dll
|
||||
frontend/a2libretro/izapple2_libretro.h
|
||||
frontend/*/snapshot.gif
|
||||
frontend/*/snapshot.png
|
||||
izapple2headless
|
||||
|
||||
@@ -28,6 +28,10 @@ go build .
|
||||
# Build console frontend
|
||||
cd frontend/a2console
|
||||
go build .
|
||||
|
||||
# Build the libretro core, needs a C toolchain
|
||||
cd frontend/a2libretro
|
||||
make
|
||||
```
|
||||
|
||||
### Install Dependencies
|
||||
|
||||
@@ -186,6 +186,12 @@ Line:
|
||||
|
||||
```
|
||||
|
||||
### RetroArch and other libretro frontends
|
||||
|
||||
izapple2 can also be built as a libretro core, to run inside RetroArch, Lakka,
|
||||
Batocera, RetroPie and the rest. See [doc/libretro.md](doc/libretro.md) for how
|
||||
to build it, install it and what it supports.
|
||||
|
||||
### Command line options
|
||||
|
||||
See [doc/command_line.md](doc/command_line.md) for a complete guide on command line configuration.
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
# A machine set up for playing games. It is the default of the libretro core.
|
||||
#
|
||||
# An enhanced Apple //e, the model that runs most of the Apple II catalogue,
|
||||
# with the Mockingboard for the games that play their music through it and the
|
||||
# accelerator that the game launchers use to load faster.
|
||||
#
|
||||
# There is no mouse, games do not need one. There is no VidHD either: the
|
||||
# libretro core does not render super hi-res, and taking the card out keeps the
|
||||
# software from switching to a mode that would not be shown.
|
||||
name: Apple //e for gaming
|
||||
parent: 2enh
|
||||
rgb: false
|
||||
s2: empty
|
||||
+22
-13
@@ -475,6 +475,25 @@ func expandSlotConfiguration(configString string) (string, error) {
|
||||
return configString, nil
|
||||
}
|
||||
|
||||
// expandSlotConfigurations expands the partial slot configurations of every
|
||||
// slot (e.g. "-s4 disk.dsk" -> "-s4 diskii,disk1=disk.dsk")
|
||||
func expandSlotConfigurations(c *configuration) error {
|
||||
slotParams := []string{confS0, confS1, confS2, confS3, confS4, confS5, confS6, confS7}
|
||||
for _, slotParam := range slotParams {
|
||||
if c.has(slotParam) {
|
||||
slotConfig := c.get(slotParam)
|
||||
expandedConfig, err := expandSlotConfiguration(slotConfig)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error expanding slot configuration for %s: %w", slotParam, err)
|
||||
}
|
||||
if expandedConfig != slotConfig {
|
||||
c.set(slotParam, expandedConfig)
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func getConfigurationFromCommandLine() (*configuration, error) {
|
||||
models, configuration, err := loadConfigurationModelsAndDefault()
|
||||
if err != nil {
|
||||
@@ -498,19 +517,9 @@ func getConfigurationFromCommandLine() (*configuration, error) {
|
||||
configuration.set(f.Name, f.Value.String())
|
||||
})
|
||||
|
||||
// Expand partial slot configurations (e.g., "-s4 disk.dsk" -> "-s4 diskii,disk1=disk.dsk")
|
||||
slotParams := []string{confS0, confS1, confS2, confS3, confS4, confS5, confS6, confS7}
|
||||
for _, slotParam := range slotParams {
|
||||
if configuration.has(slotParam) {
|
||||
slotConfig := configuration.get(slotParam)
|
||||
expandedConfig, err := expandSlotConfiguration(slotConfig)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error expanding slot configuration for %s: %w", slotParam, err)
|
||||
}
|
||||
if expandedConfig != slotConfig {
|
||||
configuration.set(slotParam, expandedConfig)
|
||||
}
|
||||
}
|
||||
err = expandSlotConfigurations(configuration)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Process positional filenames (e.g., "program disk.dsk")
|
||||
|
||||
@@ -313,6 +313,7 @@ The available pre-configured models are:
|
||||
cpm65: Apple //e with CPM-65
|
||||
desktop: Apple II DeskTop
|
||||
dos32: Apple ][ with 13 sectors disk adapter and DOS 3.2x
|
||||
gaming: Apple //e for gaming
|
||||
pascal: Apple //e with Apple Pascal 1.3
|
||||
prodos: Apple //e Prodos
|
||||
swyft: swyft
|
||||
|
||||
+329
@@ -0,0 +1,329 @@
|
||||
# Using izapple2 as a libretro core
|
||||
|
||||
izapple2 can be built as a [libretro](https://www.libretro.com/) core, so the
|
||||
emulator runs inside RetroArch and the other libretro frontends instead of in
|
||||
its own window. Your Apple II disks then sit in the same library as the rest of
|
||||
your collection, with the same controller setup, video filters and shaders.
|
||||
|
||||
The core is not part of the [releases](https://github.com/ivanizag/izapple2/releases),
|
||||
you have to build it from source. It is a small build, the instructions are below.
|
||||
|
||||
If you just want to run Apple II software on your computer, the regular
|
||||
`izapple2sdl` executable is easier and does more. Use the core if you already
|
||||
live in RetroArch.
|
||||
|
||||
## Building
|
||||
|
||||
The core is a shared library built with cgo, so besides Go you need a C
|
||||
compiler. You do not need SDL2.
|
||||
|
||||
### Linux
|
||||
|
||||
``` terminal
|
||||
git clone https://github.com/ivanizag/izapple2
|
||||
cd izapple2/frontend/a2libretro
|
||||
make
|
||||
```
|
||||
|
||||
This produces `izapple2_libretro.so`.
|
||||
|
||||
### macOS
|
||||
|
||||
``` terminal
|
||||
git clone https://github.com/ivanizag/izapple2
|
||||
cd izapple2/frontend/a2libretro
|
||||
make
|
||||
```
|
||||
|
||||
This produces `izapple2_libretro.dylib`.
|
||||
|
||||
### Windows
|
||||
|
||||
CGO needs a gcc compiler. Install
|
||||
[mingw-w64](http://mingw-w64.org/doku.php/download/mingw-builds) and run:
|
||||
|
||||
``` terminal
|
||||
git clone https://github.com/ivanizag/izapple2
|
||||
cd izapple2\frontend\a2libretro
|
||||
go build -buildmode=c-shared -o izapple2_libretro.dll .
|
||||
```
|
||||
|
||||
## Installing the core
|
||||
|
||||
Copy the library you built into the cores directory of your frontend.
|
||||
|
||||
### RetroArch
|
||||
|
||||
| System | Cores directory |
|
||||
| --- | --- |
|
||||
| Linux | `~/.config/retroarch/cores` |
|
||||
| macOS | `~/Library/Application Support/RetroArch/cores` |
|
||||
| Windows | `cores` inside the RetroArch folder |
|
||||
|
||||
If you are not sure, RetroArch shows the path in
|
||||
*Settings > Directory > Cores*.
|
||||
|
||||
Also copy `izapple2_libretro.info`, next to the sources in
|
||||
`frontend/a2libretro`, into the info directory, shown in
|
||||
*Settings > Directory > Core Info*. It is what makes RetroArch show the core
|
||||
name, the supported extensions and the list of what it can and cannot do
|
||||
instead of treating it as an unknown core. The core works without it, but the
|
||||
content dialog will not filter by extension.
|
||||
|
||||
Then load it with *Load Core > Install or Restore a Core*, or pick it from
|
||||
*Load Core* after restarting RetroArch.
|
||||
|
||||
The core is not in the RetroArch online updater, so it will not be downloaded
|
||||
or updated for you, and the playlist scanner will not recognize Apple II disks
|
||||
by itself. Load them by hand with *Load Core* followed by *Load Content*.
|
||||
|
||||
### Lakka, Batocera, RetroPie, Recalbox
|
||||
|
||||
All of these run RetroArch underneath, so the core works the same way. Copy the
|
||||
`.so` you built for that machine's architecture into its cores directory
|
||||
(`/tmp/cores` or `/usr/lib/libretro` on Lakka,
|
||||
`/userdata/system/configs/retroarch/cores` on Batocera,
|
||||
`/opt/retropie/libretrocores` on RetroPie) and select it as the core for your
|
||||
Apple II games.
|
||||
|
||||
Be careful to build for the right architecture. A core built on your x86 laptop
|
||||
will not load on a Raspberry Pi; you need to cross compile with the toolchain of
|
||||
the target, setting `CC`, `GOOS` and `GOARCH`.
|
||||
|
||||
### Other frontends
|
||||
|
||||
Anything that loads libretro cores works: Kodi's game add-ons, Emulation
|
||||
Station DE, Provenance, and the rest. Nothing in the core is specific to
|
||||
RetroArch.
|
||||
|
||||
## Running software
|
||||
|
||||
### With no disk
|
||||
|
||||
Load the core and start it with no content. You get an enhanced Apple //e
|
||||
booting DOS 3.3, ready for Applesoft BASIC. The ROMs and the DOS 3.3 diskette
|
||||
are built into the core, there is nothing else to download.
|
||||
|
||||
In RetroArch this is *Load Core*, then *Start Core*.
|
||||
|
||||
### With a disk
|
||||
|
||||
Use *Load Content* and pick your disk image. These extensions are recognized:
|
||||
|
||||
| Extension | What it is |
|
||||
| --- | --- |
|
||||
| `dsk`, `do`, `po`, `nib`, `woz` | 5 1/4 diskettes |
|
||||
| `2mg`, `hdv` | 3.5 disks and hard disks |
|
||||
| `wav` | cassette tape recordings |
|
||||
| `zip`, `gz` | any of the above, compressed |
|
||||
| `m3u` | a playlist for a game on several diskettes, see below |
|
||||
|
||||
The core decides where to put the file the same way the command line does: a
|
||||
diskette goes into the Disk II in slot 6, a hard disk image into a SmartPort
|
||||
card, and a WAV into the cassette input.
|
||||
|
||||
### Games on several diskettes
|
||||
|
||||
Plenty of Apple II games ask you to insert disk 2 partway through. List the
|
||||
diskettes in an `.m3u` playlist, a plain text file with one image per line, and
|
||||
load that instead of a single disk:
|
||||
|
||||
``` terminal
|
||||
Bard's Tale - Disk 1, Side A.woz
|
||||
Bard's Tale - Disk 1, Side B.woz
|
||||
Bard's Tale - Disk 2, Side A.woz
|
||||
```
|
||||
|
||||
Relative paths are taken from the folder of the playlist, and lines starting
|
||||
with `#` are ignored. The disks are named in the menu after their file names.
|
||||
|
||||
When the game asks for another disk, open *Quick Menu > Disk Control*, eject
|
||||
the current disk, pick the one you need and insert it. RetroArch remembers
|
||||
which one you were on the next time you load the playlist.
|
||||
|
||||
Only the first drive of the controller in slot 6 is swapped, which is what
|
||||
games ask for.
|
||||
|
||||
### Saved games
|
||||
|
||||
Games that save write to the disk, so the core keeps your collection out of it.
|
||||
The changes never reach the image you loaded: they go to an overlay file in the
|
||||
save directory of the frontend, shown in *Settings > Directory > Saves*, named
|
||||
after the disk with `.ovl` added.
|
||||
|
||||
An overlay only holds the parts the game actually wrote, not a copy of the
|
||||
disk, so it is small: a few kilobytes for a diskette, and for a 32 Mb hard disk
|
||||
only the blocks that were saved. Loading the game again puts them back on top
|
||||
of the image and you are where you left off.
|
||||
|
||||
Because the image is only ever read, this also works for the disks that could
|
||||
not be written at all before: the ones inside a zip or a gzip, the ones loaded
|
||||
from an URL, and the DOS 3.3 built into the core. You can save to all of them
|
||||
now.
|
||||
|
||||
Diskettes and hard disks both work this way, so a ProDOS collection on a HDV or
|
||||
2MG is left untouched as well.
|
||||
|
||||
An overlay carries a checksum of the image it was made from, so it is never
|
||||
applied to a different disk that happens to have the same name. If you get an
|
||||
error saying the overlay belongs to another image, delete the `.ovl` file.
|
||||
|
||||
To start over from a clean disk, delete its `.ovl` file from the save
|
||||
directory.
|
||||
|
||||
WOZ and NIB diskettes are read only by nature, and games on them cannot save.
|
||||
|
||||
This is not specific to the core. Any izapple2 frontend gets the same with the
|
||||
`-saveDir` command line option, and each disk card can name a directory of its
|
||||
own, see
|
||||
[Keeping the Disk Images Unmodified](command_line.md#keeping-the-disk-images-unmodified).
|
||||
|
||||
### Speed
|
||||
|
||||
Disk access automatically runs faster than the real machine, as it does in the
|
||||
other izapple2 frontends, so a DOS 3.3 boot takes about two seconds instead of
|
||||
half a minute. The sound breaks up while this happens. That is expected.
|
||||
|
||||
RetroArch's own fast forward and slow motion work as usual on top of that.
|
||||
|
||||
## Core options
|
||||
|
||||
In *Quick Menu > Core Options*:
|
||||
|
||||
| Option | Values | Meaning |
|
||||
| --- | --- | --- |
|
||||
| Machine model | `gaming` (default), `2`, `2e`, `2enh`, `2plus`, `base64a`, `basis108`, `cpm`, `cpm3`, `cpm65`, `desktop`, `dos32`, `pascal`, `prodos`, `swyft` | The machine to emulate, the same models as the `-model` command line flag |
|
||||
| Monitor | `color` (default), `green` | An NTSC colour television or a green phosphor monitor |
|
||||
|
||||
Changing the model takes effect on the next **Reset**, which cold boots the
|
||||
machine. The monitor applies immediately.
|
||||
|
||||
### The default machine
|
||||
|
||||
`gaming` is an enhanced Apple //e set up for playing, the model that runs most
|
||||
of the Apple II catalogue:
|
||||
|
||||
- 128Kb of RAM, expanded to 8Mb with a RAMWorks card, which the big game
|
||||
collections use
|
||||
- A Mockingboard sound card in slot 4, for the games that play their music
|
||||
through it
|
||||
- A FASTChip accelerator in slot 3, which the game launchers use to load faster
|
||||
- A Disk II controller in slot 6, with DOS 3.3 in the first drive
|
||||
- No mouse, and no VidHD card, since the core does not render super hi-res
|
||||
|
||||
Loading a hard disk image adds a SmartPort card in slot 7 by itself.
|
||||
|
||||
The other models are the regular izapple2 ones, see
|
||||
[command_line.md](command_line.md) for what each of them is. They are all
|
||||
usable, but the video cards the core cannot render are taken out of them too.
|
||||
|
||||
## Controls
|
||||
|
||||
### Keyboard
|
||||
|
||||
The core reads your keyboard directly, so just type. Most keys do the obvious
|
||||
thing.
|
||||
|
||||
| Key | Apple II |
|
||||
| --- | --- |
|
||||
| Return | Return |
|
||||
| Left arrow, Backspace | Left arrow |
|
||||
| Right, Up, Down arrows | The matching arrows |
|
||||
| Escape | Escape |
|
||||
| Tab | Tab |
|
||||
| Delete | Delete |
|
||||
| Ctrl + letter | The control code, for example Ctrl-C to break |
|
||||
| Left alt or option | Open apple |
|
||||
| Right alt or option | Closed apple |
|
||||
|
||||
Reset is the frontend's own Reset, in *Quick Menu > Restart*, or whatever you
|
||||
bound the reset hotkey to. Note that RetroArch grabs some keys for its own
|
||||
hotkeys, F1 by default for the menu; rebind them in
|
||||
*Settings > Input > Hotkeys* if they get in the way.
|
||||
|
||||
### Gamepad
|
||||
|
||||
The first controller port drives the Apple II paddles:
|
||||
|
||||
| Control | Apple II |
|
||||
| --- | --- |
|
||||
| Left analog stick | Paddles 0 and 1 |
|
||||
| D-pad | Paddles 0 and 1, for controllers with no stick |
|
||||
| A | Button 0, same as the open apple |
|
||||
| B | Button 1, same as the closed apple |
|
||||
| X | Button 2 |
|
||||
|
||||
Only one joystick is emulated. The second pair of paddles always reads
|
||||
centered.
|
||||
|
||||
## What is not supported
|
||||
|
||||
The core is deliberately smaller than the other frontends. If you need any of
|
||||
this, use `izapple2sdl`.
|
||||
|
||||
- **Super hi-res graphics.** The VidHD card is removed from the machine, so the
|
||||
box art of the [Total Replay](https://archive.org/details/TotalReplay)
|
||||
collection does not show. Total Replay itself runs, it just looks like it does
|
||||
on a machine without a VidHD.
|
||||
- **Videx 80 column video**, so the `ultraterm` model is not offered and the
|
||||
Videx cards are removed from the models that carry them.
|
||||
- **The RGB card** and its extra graphic modes.
|
||||
- **Savestates**, and therefore no rewind, no run ahead and no netplay. The
|
||||
emulator has no way to save the state of the machine, so this is not something
|
||||
the core can turn on.
|
||||
- **The mouse.** The models with a mouse card run, but the mouse never moves.
|
||||
- **Cheats**, which do not apply to a computer.
|
||||
|
||||
Everything else that izapple2 emulates works: the disk drives, the sound
|
||||
including the Mockingboard, the language card, the 80 column card, the
|
||||
accelerators and the rest.
|
||||
|
||||
## Video
|
||||
|
||||
The core hands the frontend a 564x192 image with a 4:3 aspect ratio. The
|
||||
picture is not scaled, and the gaps between the scan lines are not drawn, so
|
||||
that you can use whatever shader you prefer. A CRT shader such as `crt-geom` or
|
||||
`crt-royale` gives a good period look on top of either monitor.
|
||||
|
||||
The choice of monitor is a core option and not a shader, because the two are
|
||||
not the same thing. A green monitor does not just tint the picture: the Apple II
|
||||
draws half width pixels on it that a colour television cannot show, so the
|
||||
emulator has to render the screen differently from the start.
|
||||
|
||||
If the picture looks stretched, check that *Settings > Video > Scaling > Aspect
|
||||
Ratio* is set to `Core Provided`.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
**The core does not appear in the list.** Use *Load Core > Install or Restore a
|
||||
Core* and point at the library file. If it loads but shows up unnamed, or the
|
||||
content dialog does not filter Apple II disks, the `izapple2_libretro.info`
|
||||
file is not in the info directory.
|
||||
|
||||
**The core fails to load.** Almost always an architecture mismatch, a core
|
||||
built for a different machine than the one running it. Rebuild on the target,
|
||||
or cross compile with the right `CC`, `GOOS` and `GOARCH`.
|
||||
|
||||
**No sound during loading.** Expected while the disk is being read at speed,
|
||||
see [Speed](#speed) above.
|
||||
|
||||
**Rewind and run ahead are greyed out.** The core has no savestates, see above.
|
||||
|
||||
**A game asks for disk 2, and the Disk Control menu is empty.** You loaded a
|
||||
single disk image rather than an `.m3u` playlist listing all of them. Write the
|
||||
playlist and load that instead, see
|
||||
[Games on several diskettes](#games-on-several-diskettes).
|
||||
|
||||
**My saved game disappeared.** The saves live in the `.ovl` file next to the
|
||||
other saves, see [Saved games](#saved-games). If you deleted it, or the frontend
|
||||
is pointing at a different save directory than before, the disk loads as it came
|
||||
and the saves are not there. Renaming the disk image also loses them, the
|
||||
overlay is found by the name of the file.
|
||||
|
||||
**"the overlay belongs to another disk image".** The `.ovl` file was made from
|
||||
a different image with the same name, so applying it would corrupt the disk and
|
||||
the core refuses. Delete the `.ovl` file, losing what was saved in it, or put
|
||||
back the image it was made from.
|
||||
|
||||
**A game cannot save.** It is probably on a WOZ or NIB diskette, which are read
|
||||
only. Look for the same game as a DSK or PO.
|
||||
@@ -62,6 +62,7 @@ The available pre-configured models are:
|
||||
cpm65: Apple //e with CPM-65
|
||||
desktop: Apple II DeskTop
|
||||
dos32: Apple ][ with 13 sectors disk adapter and DOS 3.2x
|
||||
gaming: Apple //e for gaming
|
||||
pascal: Apple //e with Apple Pascal 1.3
|
||||
prodos: Apple //e Prodos
|
||||
swyft: swyft
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
# Builds the libretro core for the host platform.
|
||||
#
|
||||
# The core is a C shared library, so a C toolchain is needed. Cross compiling
|
||||
# needs the toolchain of the target platform and the matching CC, GOOS and
|
||||
# GOARCH.
|
||||
|
||||
UNAME := $(shell uname -s)
|
||||
|
||||
ifeq ($(UNAME),Darwin)
|
||||
EXT := dylib
|
||||
else ifneq (,$(findstring MINGW,$(UNAME)))
|
||||
EXT := dll
|
||||
else
|
||||
EXT := so
|
||||
endif
|
||||
|
||||
TARGET := izapple2_libretro.$(EXT)
|
||||
|
||||
.PHONY: all clean
|
||||
|
||||
all: $(TARGET)
|
||||
|
||||
$(TARGET):
|
||||
CGO_ENABLED=1 go build -buildmode=c-shared -o $(TARGET) .
|
||||
|
||||
clean:
|
||||
rm -f izapple2_libretro.so izapple2_libretro.dylib izapple2_libretro.dll
|
||||
rm -f izapple2_libretro.h
|
||||
@@ -0,0 +1,111 @@
|
||||
# izapple2 libretro core
|
||||
|
||||
A [libretro](https://www.libretro.com/) core, to run izapple2 inside RetroArch
|
||||
and the other libretro frontends.
|
||||
|
||||
**If you want to use the core, read [doc/libretro.md](../../doc/libretro.md)**,
|
||||
which covers building it for each platform, installing it in the frontends,
|
||||
loading disks, the controls and what it supports. What follows are the notes on
|
||||
how it is put together.
|
||||
|
||||
## Build
|
||||
|
||||
```bash
|
||||
make # izapple2_libretro.{so,dylib,dll} for the host
|
||||
```
|
||||
|
||||
Or directly:
|
||||
|
||||
```bash
|
||||
go build -buildmode=c-shared -o izapple2_libretro.so ./frontend/a2libretro
|
||||
```
|
||||
|
||||
`izapple2_libretro.info` goes with the library, in the info directory of the
|
||||
frontend. Keep `corename`, `display_version` and `supported_extensions` in it
|
||||
in step with what `retro_get_system_info` reports in `core.go`.
|
||||
|
||||
## How it differs from the other frontends
|
||||
|
||||
The other frontends let the emulation run free on its own goroutine and sample
|
||||
the video and the audio whenever they need them. Here the frontend owns the
|
||||
frame timing: it calls `retro_run()` once per frame, and the core has to
|
||||
advance the machine by one frame, hand over the video frame and the matching
|
||||
audio samples, and return. That is what `Apple2.RunCycles` is for, and it means
|
||||
everything happens on the thread that calls `retro_run()`, with no goroutines
|
||||
involved.
|
||||
|
||||
A frame is 17030 cycles, 65 for each of the 262 lines of an NTSC frame.
|
||||
|
||||
While a device asks for fast mode, which the Disk II does while the motor is
|
||||
on, the core runs 16 frames worth of cycles per frame. Without it a DOS 3.3
|
||||
boot, that spends some 37 million cycles mostly waiting for the drive, would
|
||||
take more than half a minute. Sound is choppy while it lasts, as it is on the
|
||||
other frontends when they run at full speed.
|
||||
|
||||
## What is not supported
|
||||
|
||||
Kept out on purpose, to keep the core small:
|
||||
|
||||
- **Super hi-res and the Videx 80 column video.** Every remaining mode is
|
||||
generated as 560x192, four pixels more with the NTSC color filter, so the
|
||||
framebuffer is a fixed 564x192 and the geometry never changes. The cards that
|
||||
generate the other modes are taken out of the slots, and the `ultraterm`
|
||||
model is not offered.
|
||||
- **The RGB card**, that would widen the framebuffer to 644.
|
||||
- **The gaps between the scan lines.** The core only uses the two screen modes
|
||||
without them, `screen.ScreenModeColor` and `screen.ScreenModeGreen`, so the
|
||||
image keeps its 192 lines instead of becoming four times taller, and the
|
||||
frontend draws the CRT look with a shader if it wants one. The phosphor is the
|
||||
other axis of the screen modes and is kept as a core option: a green monitor
|
||||
shows half width pixels that no shader can recover from a colour picture.
|
||||
- **Savestates.** `retro_serialize_size()` reports zero, so the frontend
|
||||
disables rewind, run ahead and netplay. The emulator has no way to serialize
|
||||
the machine.
|
||||
- **The mouse.** A stub provider is attached because the mouse card reads its
|
||||
provider without checking that there is one, so the models that carry it, like
|
||||
`desktop`, would crash the frontend.
|
||||
|
||||
## Where the games save
|
||||
|
||||
The emulation writes to a disk in the file it was loaded from, which is right
|
||||
on the command line and wrong in a frontend that expects the content to be left
|
||||
alone. `saves.go` asks the frontend for its save directory and passes it as the
|
||||
`saveDir` configuration of the machine, the same option the command line has.
|
||||
It is a setting of the machine, applied when it is built, so it is given as a
|
||||
configuration override rather than set afterwards: the disks are inserted while
|
||||
the machine is being built. From there `setupCard` passes it to the cards that
|
||||
declare a `savedir` parameter and do not name one themselves.
|
||||
|
||||
The overlay itself is in the storage package, `overlay.go`, and is shared with
|
||||
the other frontends. The core only chooses where it lives.
|
||||
|
||||
## Swapping diskettes
|
||||
|
||||
`disk.go` implements the extended disk control interface, falling back to the
|
||||
older one without labels if the frontend does not know it. The list of
|
||||
diskettes comes from an `.m3u` playlist; a single disk image gives a list of
|
||||
one.
|
||||
|
||||
The drive has no eject, so ejecting only bookkeeps and the diskette is inserted
|
||||
when the tray is closed again, which is the order the frontends use. Inserting
|
||||
goes through `SendLoadDisk`, which queues on the command channel that
|
||||
`RunCycles` drains at the top of the next frame, so it lands on the right
|
||||
thread by itself. The selected diskette is also kept in `filenames`, so a reset
|
||||
boots what is in the drive rather than what was loaded first.
|
||||
|
||||
## The C bridge
|
||||
|
||||
Go can not call the function pointers that the frontend hands to the core, so
|
||||
`shim.c` keeps them and reaches them through plain C functions. The keyboard
|
||||
callback goes the other way: the frontend needs a C function pointer, so
|
||||
`shim_keyboard_callback` forwards to a Go function exported with cgo.
|
||||
|
||||
Four entry points, `retro_load_game`, `retro_load_game_special`,
|
||||
`retro_unserialize` and `retro_cheat_set`, take a const pointer. cgo can not
|
||||
put const in the declarations it generates for exported Go functions, and they
|
||||
would clash with the ones in `libretro.h`, so those are exported under another
|
||||
name and wrapped in `shim.c`. The other twenty one are exported straight from
|
||||
`core.go`.
|
||||
|
||||
`libretro.h` is vendored from
|
||||
[libretro-common](https://github.com/libretro/libretro-common).
|
||||
@@ -0,0 +1,75 @@
|
||||
package main
|
||||
|
||||
/*
|
||||
#include "libretro.h"
|
||||
#include "shim.h"
|
||||
*/
|
||||
import "C"
|
||||
|
||||
import (
|
||||
a2audio "github.com/ivanizag/izapple2/audio"
|
||||
)
|
||||
|
||||
const sampleRate = a2audio.SampleRate
|
||||
|
||||
/*
|
||||
The mixer needs no special handling here. It keeps its render clock a fixed
|
||||
30 ms behind the newest sound event, and the core pushes one frame of events
|
||||
and reads one frame of samples on every call, so the two stay in step and that
|
||||
lag is just constant latency.
|
||||
*/
|
||||
type audioOutput struct {
|
||||
mixer *a2audio.Mixer
|
||||
|
||||
samplesPerFrame float64
|
||||
// Samples owed to the frontend, as the samples per frame are not a whole
|
||||
// number
|
||||
pending float64
|
||||
|
||||
mono []float32
|
||||
stereo []C.int16_t
|
||||
}
|
||||
|
||||
func newAudioOutput(clockMhz float64) *audioOutput {
|
||||
samplesPerFrame := sampleRate * cyclesPerFrame / (clockMhz * 1_000_000)
|
||||
|
||||
// Room for a frame and a bit, the pending samples never add up to a full
|
||||
// extra sample
|
||||
size := int(samplesPerFrame) + 2
|
||||
|
||||
return &audioOutput{
|
||||
mixer: a2audio.NewMixer(clockMhz),
|
||||
samplesPerFrame: samplesPerFrame,
|
||||
mono: make([]float32, size),
|
||||
stereo: make([]C.int16_t, 2*size),
|
||||
}
|
||||
}
|
||||
|
||||
// render sends the samples of the frame to the frontend as interleaved stereo
|
||||
func (s *audioOutput) render() {
|
||||
s.pending += s.samplesPerFrame
|
||||
samples := int(s.pending)
|
||||
s.pending -= float64(samples)
|
||||
if samples == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
s.mixer.ReadSamples(s.mono[:samples])
|
||||
for i, v := range s.mono[:samples] {
|
||||
sample := toInt16(v)
|
||||
s.stereo[2*i] = sample
|
||||
s.stereo[2*i+1] = sample
|
||||
}
|
||||
|
||||
C.shim_audio_sample_batch(&s.stereo[0], C.size_t(samples))
|
||||
}
|
||||
|
||||
func toInt16(v float32) C.int16_t {
|
||||
scaled := int32(v * 32767)
|
||||
if scaled > 32767 {
|
||||
scaled = 32767
|
||||
} else if scaled < -32768 {
|
||||
scaled = -32768
|
||||
}
|
||||
return C.int16_t(scaled)
|
||||
}
|
||||
@@ -0,0 +1,307 @@
|
||||
/*
|
||||
A libretro core for izapple2.
|
||||
|
||||
Unlike the other frontends, that let the emulation run free on its own
|
||||
goroutine, the libretro frontend owns the frame timing: it calls retro_run()
|
||||
once per frame and the core has to advance the machine by exactly one frame,
|
||||
hand over one video frame and the matching audio samples, and return. That is
|
||||
what Apple2.RunCycles is for, and everything here happens on the thread that
|
||||
calls retro_run().
|
||||
|
||||
Kept deliberately small: no super hi-res, no Videx video, no RGB card and no
|
||||
savestates. See README.md.
|
||||
*/
|
||||
package main
|
||||
|
||||
/*
|
||||
#include <stdlib.h>
|
||||
#include "libretro.h"
|
||||
#include "shim.h"
|
||||
*/
|
||||
import "C"
|
||||
|
||||
import (
|
||||
"unsafe"
|
||||
|
||||
"github.com/ivanizag/izapple2"
|
||||
)
|
||||
|
||||
const (
|
||||
// cyclesPerFrame is a full NTSC video frame, 65 cycles for each of the
|
||||
// 262 lines
|
||||
cyclesPerFrame = 17030
|
||||
|
||||
/*
|
||||
fastModeFactor is how many frames worth of cycles are run in a single
|
||||
frame while a device asks for fast mode, which the disk drive does
|
||||
while the motor is on. Without it a DOS 3.3 boot, that takes some 37
|
||||
million cycles of mostly waiting for the drive, would need more than
|
||||
half a minute. The other frontends let the emulation run free instead,
|
||||
but a libretro core has to return in time for the next frame, so the
|
||||
speed up is capped. Sound is choppy while it lasts, as it is on the
|
||||
other frontends.
|
||||
*/
|
||||
fastModeFactor = 16
|
||||
)
|
||||
|
||||
// core is the state of the loaded core. All of it is touched only from the
|
||||
// thread that calls retro_run().
|
||||
type core struct {
|
||||
a *izapple2.Apple2
|
||||
video *videoOutput
|
||||
audio *audioOutput
|
||||
keyboard *keyboard
|
||||
joysticks *joysticks
|
||||
disks *diskControl
|
||||
|
||||
// Media to reload when the machine is rebuilt on reset
|
||||
filenames []string
|
||||
}
|
||||
|
||||
var theCore = core{disks: newDiskControl()}
|
||||
|
||||
// build creates the machine from the current core options and the media
|
||||
func (c *core) build() bool {
|
||||
c.close()
|
||||
|
||||
options := readOptions()
|
||||
|
||||
a, err := izapple2.CreateAppleFromModel(
|
||||
options.model, buildOverrides(options.model), c.filenames)
|
||||
if err != nil {
|
||||
errorf("could not create the machine: %v", err)
|
||||
return false
|
||||
}
|
||||
|
||||
c.a = a
|
||||
c.video = newVideoOutput(options.screenMode)
|
||||
c.audio = newAudioOutput(a.GetClockMhz())
|
||||
c.keyboard = newKeyboard(a)
|
||||
c.joysticks = newJoysticks()
|
||||
|
||||
for _, source := range a.GetAudioSources() {
|
||||
source.SetAudioSink(c.audio.mixer.NewSource())
|
||||
}
|
||||
a.SetJoysticksProvider(c.joysticks)
|
||||
a.SetMouseProvider(newMouse())
|
||||
|
||||
a.Init()
|
||||
return true
|
||||
}
|
||||
|
||||
func (c *core) close() {
|
||||
c.a = nil
|
||||
c.video = nil
|
||||
c.audio = nil
|
||||
c.keyboard = nil
|
||||
c.joysticks = nil
|
||||
}
|
||||
|
||||
//export retro_api_version
|
||||
func retro_api_version() C.unsigned {
|
||||
return C.RETRO_API_VERSION
|
||||
}
|
||||
|
||||
//export retro_get_system_info
|
||||
func retro_get_system_info(info *C.struct_retro_system_info) {
|
||||
info.library_name = cLibraryName
|
||||
info.library_version = cLibraryVersion
|
||||
info.valid_extensions = cValidExtensions
|
||||
info.need_fullpath = true
|
||||
info.block_extract = false
|
||||
}
|
||||
|
||||
//export retro_get_system_av_info
|
||||
func retro_get_system_av_info(info *C.struct_retro_system_av_info) {
|
||||
clockMhz := izapple2.CPUClockMhz
|
||||
if theCore.a != nil {
|
||||
clockMhz = theCore.a.GetClockMhz()
|
||||
}
|
||||
|
||||
info.geometry.base_width = frameWidth
|
||||
info.geometry.base_height = frameHeight
|
||||
info.geometry.max_width = frameWidth
|
||||
info.geometry.max_height = frameHeight
|
||||
// The image has two pixels per Apple II pixel, the display is 4:3
|
||||
info.geometry.aspect_ratio = 4.0 / 3.0
|
||||
|
||||
info.timing.fps = C.double(clockMhz * 1_000_000 / cyclesPerFrame)
|
||||
info.timing.sample_rate = C.double(sampleRate)
|
||||
}
|
||||
|
||||
//export retro_set_environment
|
||||
func retro_set_environment(cb C.retro_environment_t) {
|
||||
C.shim_set_environment(cb)
|
||||
|
||||
// Take the log of the frontend before anything can need it
|
||||
setupLog()
|
||||
|
||||
// The core boots to BASIC with no media
|
||||
noGame := C.bool(true)
|
||||
C.shim_environment(C.RETRO_ENVIRONMENT_SET_SUPPORT_NO_GAME, unsafe.Pointer(&noGame))
|
||||
|
||||
setVariables()
|
||||
}
|
||||
|
||||
//export retro_set_video_refresh
|
||||
func retro_set_video_refresh(cb C.retro_video_refresh_t) {
|
||||
C.shim_set_video_refresh(cb)
|
||||
}
|
||||
|
||||
//export retro_set_audio_sample
|
||||
func retro_set_audio_sample(cb C.retro_audio_sample_t) {
|
||||
// Not used, the core delivers the samples in batches
|
||||
}
|
||||
|
||||
//export retro_set_audio_sample_batch
|
||||
func retro_set_audio_sample_batch(cb C.retro_audio_sample_batch_t) {
|
||||
C.shim_set_audio_sample_batch(cb)
|
||||
}
|
||||
|
||||
//export retro_set_input_poll
|
||||
func retro_set_input_poll(cb C.retro_input_poll_t) {
|
||||
C.shim_set_input_poll(cb)
|
||||
}
|
||||
|
||||
//export retro_set_input_state
|
||||
func retro_set_input_state(cb C.retro_input_state_t) {
|
||||
C.shim_set_input_state(cb)
|
||||
}
|
||||
|
||||
//export retro_init
|
||||
func retro_init() {
|
||||
}
|
||||
|
||||
//export retro_deinit
|
||||
func retro_deinit() {
|
||||
theCore.close()
|
||||
}
|
||||
|
||||
//export retro_set_controller_port_device
|
||||
func retro_set_controller_port_device(port C.unsigned, device C.unsigned) {
|
||||
}
|
||||
|
||||
/*
|
||||
The four entry points that take a const pointer are exported to C under
|
||||
another name and reached through a wrapper in shim.c: cgo can not put const in
|
||||
the declarations it generates, and they would clash with libretro.h.
|
||||
*/
|
||||
|
||||
//export izapple2LoadGame
|
||||
func izapple2LoadGame(info *C.struct_retro_game_info) C.bool {
|
||||
format := C.enum_retro_pixel_format(C.RETRO_PIXEL_FORMAT_XRGB8888)
|
||||
if !C.shim_environment(C.RETRO_ENVIRONMENT_SET_PIXEL_FORMAT, unsafe.Pointer(&format)) {
|
||||
errorf("the frontend does not support the XRGB8888 pixel format")
|
||||
return false
|
||||
}
|
||||
|
||||
theCore.filenames = nil
|
||||
theCore.disks = newDiskControl()
|
||||
|
||||
if info != nil && info.path != nil {
|
||||
images, err := buildImageList(C.GoString(info.path))
|
||||
if err != nil {
|
||||
errorf("could not read the playlist: %v", err)
|
||||
return false
|
||||
}
|
||||
theCore.disks.images = images
|
||||
|
||||
// Start with the diskette that was in use the last time
|
||||
if initialIndex < uint(len(images)) {
|
||||
theCore.disks.index = initialIndex
|
||||
}
|
||||
if path, ok := theCore.disks.current(); ok {
|
||||
theCore.filenames = []string{path}
|
||||
}
|
||||
}
|
||||
|
||||
if !theCore.build() {
|
||||
return false
|
||||
}
|
||||
|
||||
registerKeyboardCallback()
|
||||
registerDiskControl()
|
||||
setInputDescriptors()
|
||||
return true
|
||||
}
|
||||
|
||||
//export izapple2LoadGameSpecial
|
||||
func izapple2LoadGameSpecial(gameType C.unsigned, info *C.struct_retro_game_info, numInfo C.size_t) C.bool {
|
||||
return false
|
||||
}
|
||||
|
||||
//export retro_unload_game
|
||||
func retro_unload_game() {
|
||||
theCore.close()
|
||||
theCore.filenames = nil
|
||||
theCore.disks = newDiskControl()
|
||||
}
|
||||
|
||||
//export retro_reset
|
||||
func retro_reset() {
|
||||
// Rebuild the machine instead of sending a reset to the 6502, so that a
|
||||
// change of the model option is picked up with a cold boot
|
||||
theCore.build()
|
||||
}
|
||||
|
||||
//export retro_run
|
||||
func retro_run() {
|
||||
if theCore.a == nil {
|
||||
return
|
||||
}
|
||||
|
||||
C.shim_input_poll()
|
||||
theCore.video.setScreenMode(readOptions().screenMode)
|
||||
|
||||
cycles := uint64(cyclesPerFrame)
|
||||
if theCore.a.IsFastModeRequested() {
|
||||
cycles *= fastModeFactor
|
||||
}
|
||||
theCore.a.RunCycles(cycles)
|
||||
|
||||
theCore.video.render(theCore.a.GetVideoSource())
|
||||
theCore.audio.render()
|
||||
}
|
||||
|
||||
//export retro_get_region
|
||||
func retro_get_region() C.unsigned {
|
||||
return C.RETRO_REGION_NTSC
|
||||
}
|
||||
|
||||
//export retro_get_memory_data
|
||||
func retro_get_memory_data(id C.unsigned) unsafe.Pointer {
|
||||
return nil
|
||||
}
|
||||
|
||||
//export retro_get_memory_size
|
||||
func retro_get_memory_size(id C.unsigned) C.size_t {
|
||||
return 0
|
||||
}
|
||||
|
||||
// The core does not implement savestates, so it reports a size of zero and
|
||||
// the frontend disables rewind, run ahead and netplay
|
||||
|
||||
//export retro_serialize_size
|
||||
func retro_serialize_size() C.size_t {
|
||||
return 0
|
||||
}
|
||||
|
||||
//export retro_serialize
|
||||
func retro_serialize(data unsafe.Pointer, size C.size_t) C.bool {
|
||||
return false
|
||||
}
|
||||
|
||||
//export izapple2Unserialize
|
||||
func izapple2Unserialize(data unsafe.Pointer, size C.size_t) C.bool {
|
||||
return false
|
||||
}
|
||||
|
||||
//export retro_cheat_reset
|
||||
func retro_cheat_reset() {
|
||||
}
|
||||
|
||||
//export izapple2CheatSet
|
||||
func izapple2CheatSet(index C.unsigned, enabled C.bool, code *C.char) {
|
||||
}
|
||||
|
||||
func main() {}
|
||||
@@ -0,0 +1,234 @@
|
||||
package main
|
||||
|
||||
/*
|
||||
#include <stdlib.h>
|
||||
#include "libretro.h"
|
||||
#include "shim.h"
|
||||
*/
|
||||
import "C"
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
/*
|
||||
The disk control interface, what lets the frontend swap diskettes from its own
|
||||
menu so that the games spanning several disks can be finished.
|
||||
|
||||
The list of diskettes comes from an .m3u playlist, the libretro convention for
|
||||
multi disk content: a text file with one image per line. Loading a single disk
|
||||
image gives a list of one.
|
||||
|
||||
The Apple II has no way to know that a diskette was taken out, and the drive
|
||||
has no eject, so ejecting only bookkeeps. The diskette of the selected index is
|
||||
inserted when the frontend closes the tray again, which is the order it always
|
||||
uses: eject, select, insert.
|
||||
*/
|
||||
|
||||
const m3uExtension = ".m3u"
|
||||
|
||||
type diskImage struct {
|
||||
path string
|
||||
label string
|
||||
}
|
||||
|
||||
type diskControl struct {
|
||||
images []diskImage
|
||||
index uint
|
||||
ejected bool
|
||||
}
|
||||
|
||||
// initialIndex is what the frontend asks for before the content is loaded, to
|
||||
// restore the diskette that was in use the last time
|
||||
var initialIndex uint
|
||||
|
||||
func newDiskControl() *diskControl {
|
||||
return &diskControl{}
|
||||
}
|
||||
|
||||
// registerDiskControl offers the interface to the frontend, falling back to the
|
||||
// older one that has no labels if the extended one is not known
|
||||
func registerDiskControl() {
|
||||
if !C.shim_set_disk_control_interface() {
|
||||
logf("the frontend does not support swapping diskettes")
|
||||
}
|
||||
}
|
||||
|
||||
// buildImageList returns the diskettes of the content, expanding a playlist
|
||||
func buildImageList(path string) ([]diskImage, error) {
|
||||
if !strings.EqualFold(filepath.Ext(path), m3uExtension) {
|
||||
return []diskImage{newDiskImage(path)}, nil
|
||||
}
|
||||
|
||||
content, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
dir := filepath.Dir(path)
|
||||
images := make([]diskImage, 0)
|
||||
for _, line := range strings.Split(string(content), "\n") {
|
||||
line = strings.TrimSpace(line)
|
||||
if line == "" || strings.HasPrefix(line, "#") {
|
||||
continue
|
||||
}
|
||||
if !filepath.IsAbs(line) {
|
||||
line = filepath.Join(dir, line)
|
||||
}
|
||||
images = append(images, newDiskImage(line))
|
||||
}
|
||||
return images, nil
|
||||
}
|
||||
|
||||
func newDiskImage(path string) diskImage {
|
||||
name := filepath.Base(path)
|
||||
return diskImage{
|
||||
path: path,
|
||||
label: strings.TrimSuffix(name, filepath.Ext(name)),
|
||||
}
|
||||
}
|
||||
|
||||
// current returns the path of the selected diskette
|
||||
func (d *diskControl) current() (string, bool) {
|
||||
if d.index >= uint(len(d.images)) {
|
||||
return "", false
|
||||
}
|
||||
return d.images[d.index].path, true
|
||||
}
|
||||
|
||||
// insert puts the selected diskette in the first drive
|
||||
func (d *diskControl) insert() bool {
|
||||
path, ok := d.current()
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
if theCore.a == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
theCore.a.SendLoadDisk(0, path)
|
||||
|
||||
// A reset rebuilds the machine, keep it booting what is in the drive
|
||||
theCore.filenames = []string{path}
|
||||
return true
|
||||
}
|
||||
|
||||
//export izapple2DiskSetEjectState
|
||||
func izapple2DiskSetEjectState(ejected C.int) C.int {
|
||||
d := theCore.disks
|
||||
wasEjected := d.ejected
|
||||
d.ejected = ejected != 0
|
||||
|
||||
if wasEjected && !d.ejected {
|
||||
// The tray is closed again, load whatever was selected meanwhile.
|
||||
// With no diskette selected there is nothing to load, and that is not
|
||||
// a failure: the frontend is just taking the diskette out.
|
||||
d.insert()
|
||||
}
|
||||
return 1
|
||||
}
|
||||
|
||||
//export izapple2DiskGetEjectState
|
||||
func izapple2DiskGetEjectState() C.int {
|
||||
return boolToC(theCore.disks.ejected)
|
||||
}
|
||||
|
||||
//export izapple2DiskGetImageIndex
|
||||
func izapple2DiskGetImageIndex() C.uint {
|
||||
return C.uint(theCore.disks.index)
|
||||
}
|
||||
|
||||
//export izapple2DiskSetImageIndex
|
||||
func izapple2DiskSetImageIndex(index C.uint) C.int {
|
||||
d := theCore.disks
|
||||
|
||||
// Any index from the number of diskettes up means taking the diskette out
|
||||
// without putting another one in, so every value is accepted and it is
|
||||
// current() that tells whether there is something to insert
|
||||
d.index = uint(index)
|
||||
|
||||
// The frontends eject before selecting, but swap right away for the ones
|
||||
// that do not, instead of leaving the drive with the previous diskette
|
||||
if !d.ejected {
|
||||
d.insert()
|
||||
}
|
||||
return 1
|
||||
}
|
||||
|
||||
//export izapple2DiskGetNumImages
|
||||
func izapple2DiskGetNumImages() C.uint {
|
||||
return C.uint(len(theCore.disks.images))
|
||||
}
|
||||
|
||||
//export izapple2DiskReplaceImageIndex
|
||||
func izapple2DiskReplaceImageIndex(index C.uint, info *C.struct_retro_game_info) C.int {
|
||||
d := theCore.disks
|
||||
if uint(index) >= uint(len(d.images)) {
|
||||
return 0
|
||||
}
|
||||
|
||||
if info == nil || info.path == nil {
|
||||
// Remove the entry
|
||||
d.images = append(d.images[:index], d.images[index+1:]...)
|
||||
if d.index >= uint(len(d.images)) && len(d.images) > 0 {
|
||||
d.index = uint(len(d.images)) - 1
|
||||
}
|
||||
return 1
|
||||
}
|
||||
|
||||
d.images[index] = newDiskImage(C.GoString(info.path))
|
||||
return 1
|
||||
}
|
||||
|
||||
//export izapple2DiskAddImageIndex
|
||||
func izapple2DiskAddImageIndex() C.int {
|
||||
theCore.disks.images = append(theCore.disks.images, diskImage{})
|
||||
return 1
|
||||
}
|
||||
|
||||
//export izapple2DiskSetInitialImage
|
||||
func izapple2DiskSetInitialImage(index C.uint, path *C.char) C.int {
|
||||
initialIndex = uint(index)
|
||||
return 1
|
||||
}
|
||||
|
||||
//export izapple2DiskGetImagePath
|
||||
func izapple2DiskGetImagePath(index C.uint, buffer *C.char, length C.size_t) C.int {
|
||||
d := theCore.disks
|
||||
if uint(index) >= uint(len(d.images)) {
|
||||
return 0
|
||||
}
|
||||
return copyToBuffer(buffer, length, d.images[index].path)
|
||||
}
|
||||
|
||||
//export izapple2DiskGetImageLabel
|
||||
func izapple2DiskGetImageLabel(index C.uint, buffer *C.char, length C.size_t) C.int {
|
||||
d := theCore.disks
|
||||
if uint(index) >= uint(len(d.images)) {
|
||||
return 0
|
||||
}
|
||||
return copyToBuffer(buffer, length, d.images[index].label)
|
||||
}
|
||||
|
||||
// copyToBuffer writes a null terminated string in a buffer owned by the
|
||||
// frontend, truncating it if it does not fit
|
||||
func copyToBuffer(buffer *C.char, length C.size_t, value string) C.int {
|
||||
if buffer == nil || length == 0 {
|
||||
return 0
|
||||
}
|
||||
|
||||
destination := unsafe.Slice((*byte)(unsafe.Pointer(buffer)), int(length))
|
||||
written := copy(destination[:len(destination)-1], value)
|
||||
destination[written] = 0
|
||||
return 1
|
||||
}
|
||||
|
||||
func boolToC(value bool) C.int {
|
||||
if value {
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
# Software Information
|
||||
display_name = "Apple II (izapple2)"
|
||||
authors = "Ivan Izaguirre"
|
||||
supported_extensions = "dsk|do|po|nib|woz|2mg|hdv|wav|zip|gz|m3u"
|
||||
corename = "izapple2"
|
||||
categories = "Emulator"
|
||||
license = "GPLv3"
|
||||
permissions = ""
|
||||
display_version = "2.0"
|
||||
|
||||
# Hardware Information
|
||||
manufacturer = "Apple"
|
||||
systemname = "Apple II"
|
||||
systemid = "apple2"
|
||||
|
||||
# Libretro Features
|
||||
supports_no_game = "true"
|
||||
savestate = "false"
|
||||
savestate_features = "null"
|
||||
cheats = "false"
|
||||
input_descriptors = "true"
|
||||
memory_descriptors = "false"
|
||||
libretro_saves = "true"
|
||||
core_options = "true"
|
||||
core_options_version = "0"
|
||||
load_subsystem = "false"
|
||||
hw_render = "false"
|
||||
needs_fullpath = "true"
|
||||
disk_control = "true"
|
||||
is_experimental = "false"
|
||||
firmware_count = 0
|
||||
|
||||
description = "An emulator of the Apple II+ and the Apple //e, written in Go. The ROMs and a DOS 3.3 diskette are built into the core, so it starts into Applesoft BASIC with no content loaded. It boots 5 1/4 diskettes in DSK, PO, NIB and WOZ format, 3.5 disks and hard disks in 2MG and HDV format, and reads cassette tapes from WAV recordings, compressed with gzip or zip or not. Games spanning several diskettes are loaded from an M3U playlist and swapped from the disk control menu. The default machine is an enhanced Apple //e with a Mockingboard, set up for playing games."
|
||||
|
||||
notes = "The core has no savestates, so rewind, run ahead and netplay are not available.|Super hi-res, the Videx 80 column video and the RGB card modes are not rendered, so the box art of collections like Total Replay is not shown.|For a game on several diskettes, load an M3U playlist listing them, one path per line.|Games save into an overlay file in the save directory, holding only the parts written. The disk image loaded is never modified, diskettes and hard disks alike, so compressed and read only disks can be saved to as well.|WOZ and NIB diskettes cannot save.|The gaps between the scan lines are not drawn, use a shader for the CRT look. The monitor, NTSC colour or green phosphor, is a core option.|Disk access runs faster than the real machine, and the sound breaks up while it does."
|
||||
@@ -0,0 +1,72 @@
|
||||
package main
|
||||
|
||||
/*
|
||||
#include "libretro.h"
|
||||
#include "shim.h"
|
||||
*/
|
||||
import "C"
|
||||
|
||||
/*
|
||||
joysticks maps the first libretro controller port to the Apple II paddles.
|
||||
|
||||
The methods are called from the emulation, which under RunCycles runs on the
|
||||
same thread as retro_run, so the input state can be read straight from the
|
||||
frontend as the machine asks for it.
|
||||
*/
|
||||
type joysticks struct {
|
||||
// The open and closed apple keys, that the Apple II reads as the buttons
|
||||
// of the first joystick
|
||||
openApple bool
|
||||
closedApple bool
|
||||
}
|
||||
|
||||
func newJoysticks() *joysticks {
|
||||
return &joysticks{}
|
||||
}
|
||||
|
||||
func (j *joysticks) ReadButton(i int) bool {
|
||||
switch i {
|
||||
case 0:
|
||||
return j.openApple || joypadPressed(C.RETRO_DEVICE_ID_JOYPAD_A)
|
||||
case 1:
|
||||
return j.closedApple || joypadPressed(C.RETRO_DEVICE_ID_JOYPAD_B)
|
||||
case 2:
|
||||
return joypadPressed(C.RETRO_DEVICE_ID_JOYPAD_X)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (j *joysticks) ReadPaddle(i int) (uint8, bool) {
|
||||
var negative, positive, axis C.uint
|
||||
switch i {
|
||||
case 0:
|
||||
negative = C.RETRO_DEVICE_ID_JOYPAD_LEFT
|
||||
positive = C.RETRO_DEVICE_ID_JOYPAD_RIGHT
|
||||
axis = C.RETRO_DEVICE_ID_ANALOG_X
|
||||
case 1:
|
||||
negative = C.RETRO_DEVICE_ID_JOYPAD_UP
|
||||
positive = C.RETRO_DEVICE_ID_JOYPAD_DOWN
|
||||
axis = C.RETRO_DEVICE_ID_ANALOG_Y
|
||||
default:
|
||||
// Only one joystick, the second one reads centered
|
||||
return 128, false
|
||||
}
|
||||
|
||||
// The d-pad takes precedence, for the controllers with no analog stick
|
||||
if joypadPressed(negative) {
|
||||
return 0, true
|
||||
}
|
||||
if joypadPressed(positive) {
|
||||
return 255, true
|
||||
}
|
||||
|
||||
value := C.shim_input_state(0, C.RETRO_DEVICE_ANALOG,
|
||||
C.RETRO_DEVICE_INDEX_ANALOG_LEFT, axis)
|
||||
|
||||
// The analog axis goes from -32768 to 32767, the paddle from 0 to 255
|
||||
return uint8((int32(value) + 32768) >> 8), true
|
||||
}
|
||||
|
||||
func joypadPressed(id C.uint) bool {
|
||||
return C.shim_input_state(0, C.RETRO_DEVICE_JOYPAD, 0, id) != 0
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
package main
|
||||
|
||||
/*
|
||||
#include "libretro.h"
|
||||
#include "shim.h"
|
||||
*/
|
||||
import "C"
|
||||
|
||||
import (
|
||||
"unsafe"
|
||||
|
||||
"github.com/ivanizag/izapple2"
|
||||
)
|
||||
|
||||
// keyboard turns the key events of the frontend into the codes the Apple II
|
||||
// keyboard would produce
|
||||
type keyboard struct {
|
||||
channel *izapple2.KeyboardChannel
|
||||
}
|
||||
|
||||
func newKeyboard(a *izapple2.Apple2) *keyboard {
|
||||
return &keyboard{
|
||||
channel: izapple2.NewKeyboardChannel(a),
|
||||
}
|
||||
}
|
||||
|
||||
// registerKeyboardCallback asks the frontend to report the key events. The
|
||||
// callback is a C function that forwards to izapple2KeyboardEvent.
|
||||
func registerKeyboardCallback() {
|
||||
callback := C.struct_retro_keyboard_callback{
|
||||
callback: C.retro_keyboard_event_t(C.shim_keyboard_callback),
|
||||
}
|
||||
C.shim_environment(C.RETRO_ENVIRONMENT_SET_KEYBOARD_CALLBACK, unsafe.Pointer(&callback))
|
||||
}
|
||||
|
||||
//export izapple2KeyboardEvent
|
||||
func izapple2KeyboardEvent(down C.int, keycode C.uint, character C.uint32_t, keyModifiers C.uint16_t) {
|
||||
if theCore.keyboard == nil {
|
||||
return
|
||||
}
|
||||
|
||||
// The Apple II reads the open and closed apple keys as joystick buttons,
|
||||
// so they are a state and not a keystroke
|
||||
switch keycode {
|
||||
case C.RETROK_LALT:
|
||||
theCore.joysticks.openApple = down != 0
|
||||
return
|
||||
case C.RETROK_RALT:
|
||||
theCore.joysticks.closedApple = down != 0
|
||||
return
|
||||
}
|
||||
|
||||
if down == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
if key, ok := specialKey(uint32(keycode), uint16(keyModifiers)); ok {
|
||||
theCore.keyboard.channel.PutChar(key)
|
||||
return
|
||||
}
|
||||
|
||||
// PutRune drops what is not printable ASCII and applies the upper case of
|
||||
// the models without lower case
|
||||
theCore.keyboard.channel.PutRune(rune(character))
|
||||
}
|
||||
|
||||
// specialKey returns the code of the keys that do not come as a printable
|
||||
// character, and of the control combinations
|
||||
func specialKey(keycode uint32, modifiers uint16) (uint8, bool) {
|
||||
switch keycode {
|
||||
case C.RETROK_RETURN, C.RETROK_KP_ENTER:
|
||||
return 13, true
|
||||
case C.RETROK_TAB:
|
||||
return 9, true
|
||||
case C.RETROK_ESCAPE:
|
||||
return 27, true
|
||||
case C.RETROK_BACKSPACE, C.RETROK_LEFT:
|
||||
return 8, true
|
||||
case C.RETROK_RIGHT:
|
||||
return 21, true
|
||||
case C.RETROK_UP:
|
||||
return 11, true
|
||||
case C.RETROK_DOWN:
|
||||
return 10, true
|
||||
case C.RETROK_DELETE:
|
||||
return 127, true
|
||||
}
|
||||
|
||||
if modifiers&C.RETROKMOD_CTRL != 0 &&
|
||||
keycode >= C.RETROK_a && keycode <= C.RETROK_z {
|
||||
return uint8(keycode-C.RETROK_a) + 1, true
|
||||
}
|
||||
|
||||
return 0, false
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,18 @@
|
||||
package main
|
||||
|
||||
/*
|
||||
mouse reports a mouse that never moves and is never pressed.
|
||||
|
||||
The core does not map the host pointer to the Apple II mouse, but the mouse
|
||||
card reads its provider without checking that there is one, so the models that
|
||||
carry the card, like "desktop", need one to be attached.
|
||||
*/
|
||||
type mouse struct{}
|
||||
|
||||
func newMouse() *mouse {
|
||||
return &mouse{}
|
||||
}
|
||||
|
||||
func (m *mouse) ReadMouse() (x uint16, y uint16, pressed bool) {
|
||||
return 0, 0, false
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
package main
|
||||
|
||||
/*
|
||||
#include <stdlib.h>
|
||||
#include "libretro.h"
|
||||
#include "shim.h"
|
||||
*/
|
||||
import "C"
|
||||
|
||||
import (
|
||||
"slices"
|
||||
"strings"
|
||||
"unsafe"
|
||||
|
||||
"github.com/ivanizag/izapple2"
|
||||
"github.com/ivanizag/izapple2/screen"
|
||||
)
|
||||
|
||||
const (
|
||||
// defaultModel is the machine set up for playing games, see
|
||||
// configs/gaming.cfg
|
||||
defaultModel = "gaming"
|
||||
|
||||
// unsupportedModel is left out of the core options, its reason to exist is
|
||||
// the Videx Ultraterm video that this core does not render
|
||||
unsupportedModel = "ultraterm"
|
||||
)
|
||||
|
||||
// The strings handed to the frontend have to outlive the call, so they are
|
||||
// allocated once and never freed
|
||||
var (
|
||||
cLibraryName = C.CString("izapple2")
|
||||
cLibraryVersion = C.CString("2.0")
|
||||
cValidExtensions = C.CString("dsk|do|po|nib|woz|2mg|hdv|wav|zip|gz|m3u")
|
||||
|
||||
cKeyModel = C.CString("izapple2_model")
|
||||
cKeyScreen = C.CString("izapple2_screen")
|
||||
)
|
||||
|
||||
type options struct {
|
||||
model string
|
||||
screenMode int
|
||||
}
|
||||
|
||||
// readOptions returns the settings currently chosen in the frontend
|
||||
func readOptions() options {
|
||||
return options{
|
||||
model: readVariable(cKeyModel, defaultModel),
|
||||
screenMode: screenModeByName(readVariable(cKeyScreen, "color")),
|
||||
}
|
||||
}
|
||||
|
||||
// screenModeByName is the monitor to emulate. Both are the modes without scan
|
||||
// lines, the frontend draws those with a shader if it wants them.
|
||||
func screenModeByName(name string) int {
|
||||
if name == "green" {
|
||||
return screen.ScreenModeGreen
|
||||
}
|
||||
return screen.ScreenModeColor
|
||||
}
|
||||
|
||||
/*
|
||||
videoOverrides takes out the cards that generate the video this core does not
|
||||
render. The VidHD card, that adds super hi-res, is always in slot 2. The Videx
|
||||
80 column card is in slot 3 of the two models that carry it.
|
||||
*/
|
||||
func videoOverrides(model string) map[string]string {
|
||||
overrides := map[string]string{"s2": "empty"}
|
||||
switch model {
|
||||
case "2plus", "basis108":
|
||||
overrides["s3"] = "empty"
|
||||
}
|
||||
return overrides
|
||||
}
|
||||
|
||||
// buildOverrides is everything the core imposes on the model chosen
|
||||
func buildOverrides(model string) map[string]string {
|
||||
overrides := videoOverrides(model)
|
||||
for key, value := range saveOverrides() {
|
||||
overrides[key] = value
|
||||
}
|
||||
return overrides
|
||||
}
|
||||
|
||||
func readVariable(key *C.char, fallback string) string {
|
||||
variable := C.struct_retro_variable{key: key}
|
||||
ok := C.shim_environment(C.RETRO_ENVIRONMENT_GET_VARIABLE, unsafe.Pointer(&variable))
|
||||
if !ok || variable.value == nil {
|
||||
return fallback
|
||||
}
|
||||
value := C.GoString(variable.value)
|
||||
if value == "" {
|
||||
return fallback
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
// setVariables declares the core options. The frontend keeps the array, so it
|
||||
// is built in C memory.
|
||||
func setVariables() {
|
||||
variables := []struct{ key, value string }{
|
||||
{"izapple2_model", "Machine model; " + modelChoices()},
|
||||
{"izapple2_screen", "Monitor; color|green"},
|
||||
}
|
||||
|
||||
// One extra entry, the array is terminated by a pair of null pointers
|
||||
count := len(variables) + 1
|
||||
size := C.size_t(count) * C.size_t(unsafe.Sizeof(C.struct_retro_variable{}))
|
||||
array := (*C.struct_retro_variable)(C.malloc(size))
|
||||
entries := unsafe.Slice(array, count)
|
||||
|
||||
for i, variable := range variables {
|
||||
entries[i].key = C.CString(variable.key)
|
||||
entries[i].value = C.CString(variable.value)
|
||||
}
|
||||
entries[count-1].key = nil
|
||||
entries[count-1].value = nil
|
||||
|
||||
C.shim_environment(C.RETRO_ENVIRONMENT_SET_VARIABLES, unsafe.Pointer(array))
|
||||
}
|
||||
|
||||
// modelChoices lists the models for the option, the default one first
|
||||
func modelChoices() string {
|
||||
models, err := izapple2.AvailableModels()
|
||||
if err != nil {
|
||||
logf("could not list the models: %v", err)
|
||||
return defaultModel
|
||||
}
|
||||
|
||||
choices := []string{defaultModel}
|
||||
for _, model := range models {
|
||||
if model != defaultModel && model != unsupportedModel {
|
||||
choices = append(choices, model)
|
||||
}
|
||||
}
|
||||
slices.Sort(choices[1:])
|
||||
|
||||
return strings.Join(choices, "|")
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
package main
|
||||
|
||||
/*
|
||||
#include <stdlib.h>
|
||||
#include "libretro.h"
|
||||
#include "shim.h"
|
||||
*/
|
||||
import "C"
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
// messageFrames is how long a message stays on screen, about three seconds
|
||||
const messageFrames = 180
|
||||
|
||||
// setupLog takes the log of the frontend, if it offers one. Everything goes to
|
||||
// stderr until it does, and if it never does.
|
||||
func setupLog() {
|
||||
var callback C.struct_retro_log_callback
|
||||
if C.shim_environment(C.RETRO_ENVIRONMENT_GET_LOG_INTERFACE, unsafe.Pointer(&callback)) {
|
||||
C.shim_set_log(callback.log)
|
||||
}
|
||||
}
|
||||
|
||||
// logf reports something worth knowing to whoever is watching the log
|
||||
func logf(format string, args ...any) {
|
||||
writeLog(C.RETRO_LOG_INFO, fmt.Sprintf(format, args...))
|
||||
}
|
||||
|
||||
// errorf reports a failure the user has to know about: it goes to the log and
|
||||
// on screen, as nobody reads the log of a frontend
|
||||
func errorf(format string, args ...any) {
|
||||
message := fmt.Sprintf(format, args...)
|
||||
writeLog(C.RETRO_LOG_ERROR, message)
|
||||
showMessage(message)
|
||||
}
|
||||
|
||||
func writeLog(level C.int, message string) {
|
||||
if !C.shim_has_log() {
|
||||
fmt.Fprintf(os.Stderr, "[izapple2] %v\n", message)
|
||||
return
|
||||
}
|
||||
|
||||
cMessage := C.CString(message)
|
||||
defer C.free(unsafe.Pointer(cMessage))
|
||||
C.shim_log(level, cMessage)
|
||||
}
|
||||
|
||||
// showMessage puts a notice on the screen of the frontend
|
||||
func showMessage(message string) {
|
||||
cMessage := C.CString(message)
|
||||
defer C.free(unsafe.Pointer(cMessage))
|
||||
|
||||
notice := C.struct_retro_message{
|
||||
msg: cMessage,
|
||||
frames: messageFrames,
|
||||
}
|
||||
C.shim_environment(C.RETRO_ENVIRONMENT_SET_MESSAGE, unsafe.Pointer(¬ice))
|
||||
}
|
||||
|
||||
// setInputDescriptors names the controls in the menus of the frontend, so that
|
||||
// remapping them shows what they do on the Apple II
|
||||
func setInputDescriptors() {
|
||||
type descriptor struct {
|
||||
device C.uint
|
||||
index C.uint
|
||||
id C.uint
|
||||
description string
|
||||
}
|
||||
|
||||
descriptors := []descriptor{
|
||||
{C.RETRO_DEVICE_JOYPAD, 0, C.RETRO_DEVICE_ID_JOYPAD_LEFT, "Paddle 0 left"},
|
||||
{C.RETRO_DEVICE_JOYPAD, 0, C.RETRO_DEVICE_ID_JOYPAD_RIGHT, "Paddle 0 right"},
|
||||
{C.RETRO_DEVICE_JOYPAD, 0, C.RETRO_DEVICE_ID_JOYPAD_UP, "Paddle 1 up"},
|
||||
{C.RETRO_DEVICE_JOYPAD, 0, C.RETRO_DEVICE_ID_JOYPAD_DOWN, "Paddle 1 down"},
|
||||
{C.RETRO_DEVICE_JOYPAD, 0, C.RETRO_DEVICE_ID_JOYPAD_A, "Button 0 (open apple)"},
|
||||
{C.RETRO_DEVICE_JOYPAD, 0, C.RETRO_DEVICE_ID_JOYPAD_B, "Button 1 (closed apple)"},
|
||||
{C.RETRO_DEVICE_JOYPAD, 0, C.RETRO_DEVICE_ID_JOYPAD_X, "Button 2"},
|
||||
{C.RETRO_DEVICE_ANALOG, C.RETRO_DEVICE_INDEX_ANALOG_LEFT, C.RETRO_DEVICE_ID_ANALOG_X, "Paddle 0"},
|
||||
{C.RETRO_DEVICE_ANALOG, C.RETRO_DEVICE_INDEX_ANALOG_LEFT, C.RETRO_DEVICE_ID_ANALOG_Y, "Paddle 1"},
|
||||
}
|
||||
|
||||
// The frontend keeps the array until the content is unloaded, so it is
|
||||
// built in C memory and ends with a null description
|
||||
count := len(descriptors) + 1
|
||||
size := C.size_t(count) * C.size_t(unsafe.Sizeof(C.struct_retro_input_descriptor{}))
|
||||
array := (*C.struct_retro_input_descriptor)(C.malloc(size))
|
||||
entries := unsafe.Slice(array, count)
|
||||
|
||||
for i, d := range descriptors {
|
||||
entries[i].port = 0
|
||||
entries[i].device = d.device
|
||||
entries[i].index = d.index
|
||||
entries[i].id = d.id
|
||||
entries[i].description = C.CString(d.description)
|
||||
}
|
||||
entries[count-1].description = nil
|
||||
|
||||
C.shim_environment(C.RETRO_ENVIRONMENT_SET_INPUT_DESCRIPTORS, unsafe.Pointer(array))
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
package main
|
||||
|
||||
/*
|
||||
#include "libretro.h"
|
||||
#include "shim.h"
|
||||
*/
|
||||
import "C"
|
||||
|
||||
import (
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
/*
|
||||
Where the games save.
|
||||
|
||||
The emulation writes to a diskette as the software saves, and by default it
|
||||
writes straight into the file the diskette was loaded from. That is what the
|
||||
command line does, but not what somebody loading a game in a libretro frontend
|
||||
expects: the content should be left alone and the saves should go to the save
|
||||
directory, where the frontend backs them up.
|
||||
|
||||
So the core points the emulator at the save directory and the changes go to an
|
||||
overlay file next to the other saves, one per diskette. Only the tracks the
|
||||
software writes are stored, so an overlay is a few kilobytes rather than a copy
|
||||
of the diskette, and the image loaded is never touched.
|
||||
|
||||
It also means that the diskettes that could not be written at all, the ones
|
||||
inside a zip or a gzip, the ones loaded from an URL and the DOS 3.3 built into
|
||||
the core, can now be saved to.
|
||||
*/
|
||||
|
||||
/*
|
||||
saveOverrides is the configuration that sends the changes written to the disks
|
||||
to the save directory. It is a setting of the machine, applied when it is built,
|
||||
so it is given as a configuration override and not set afterwards: the disks are
|
||||
inserted while the machine is being built.
|
||||
*/
|
||||
func saveOverrides() map[string]string {
|
||||
directory, ok := saveDirectory()
|
||||
if !ok {
|
||||
logf("the frontend gave no save directory, the disks will be written where they are")
|
||||
return nil
|
||||
}
|
||||
|
||||
return map[string]string{"saveDir": directory}
|
||||
}
|
||||
|
||||
// saveDirectory returns the directory the frontend keeps the saves in
|
||||
func saveDirectory() (string, bool) {
|
||||
var directory *C.char
|
||||
ok := C.shim_environment(C.RETRO_ENVIRONMENT_GET_SAVE_DIRECTORY, unsafe.Pointer(&directory))
|
||||
if !ok || directory == nil {
|
||||
return "", false
|
||||
}
|
||||
|
||||
path := C.GoString(directory)
|
||||
if path == "" {
|
||||
return "", false
|
||||
}
|
||||
return path, true
|
||||
}
|
||||
@@ -0,0 +1,178 @@
|
||||
#include "shim.h"
|
||||
#include "_cgo_export.h"
|
||||
|
||||
static retro_log_printf_t log_cb;
|
||||
|
||||
void shim_set_log(retro_log_printf_t cb) { log_cb = cb; }
|
||||
bool shim_has_log(void) { return log_cb != NULL; }
|
||||
|
||||
void shim_log(int level, const char *message)
|
||||
{
|
||||
if (log_cb)
|
||||
log_cb((enum retro_log_level)level, "%s\n", message);
|
||||
}
|
||||
|
||||
static retro_environment_t env_cb;
|
||||
static retro_video_refresh_t video_cb;
|
||||
static retro_audio_sample_batch_t audio_batch_cb;
|
||||
static retro_input_poll_t input_poll_cb;
|
||||
static retro_input_state_t input_state_cb;
|
||||
|
||||
void shim_set_environment(retro_environment_t cb) { env_cb = cb; }
|
||||
void shim_set_video_refresh(retro_video_refresh_t cb) { video_cb = cb; }
|
||||
void shim_set_audio_sample_batch(retro_audio_sample_batch_t cb) { audio_batch_cb = cb; }
|
||||
void shim_set_input_poll(retro_input_poll_t cb) { input_poll_cb = cb; }
|
||||
void shim_set_input_state(retro_input_state_t cb) { input_state_cb = cb; }
|
||||
|
||||
bool shim_environment(unsigned cmd, void *data)
|
||||
{
|
||||
if (!env_cb)
|
||||
return false;
|
||||
return env_cb(cmd, data);
|
||||
}
|
||||
|
||||
void shim_video_refresh(const void *data, unsigned width, unsigned height, size_t pitch)
|
||||
{
|
||||
if (video_cb)
|
||||
video_cb(data, width, height, pitch);
|
||||
}
|
||||
|
||||
size_t shim_audio_sample_batch(const int16_t *data, size_t frames)
|
||||
{
|
||||
if (!audio_batch_cb)
|
||||
return 0;
|
||||
return audio_batch_cb(data, frames);
|
||||
}
|
||||
|
||||
void shim_input_poll(void)
|
||||
{
|
||||
if (input_poll_cb)
|
||||
input_poll_cb();
|
||||
}
|
||||
|
||||
int16_t shim_input_state(unsigned port, unsigned device, unsigned index, unsigned id)
|
||||
{
|
||||
if (!input_state_cb)
|
||||
return 0;
|
||||
return input_state_cb(port, device, index, id);
|
||||
}
|
||||
|
||||
void shim_keyboard_callback(bool down, unsigned keycode, uint32_t character, uint16_t key_modifiers)
|
||||
{
|
||||
/* izapple2KeyboardEvent is exported by keyboard.go */
|
||||
izapple2KeyboardEvent(down ? 1 : 0, keycode, character, key_modifiers);
|
||||
}
|
||||
|
||||
/*
|
||||
The disk control interface. The frontend needs C function pointers, so these
|
||||
forward to the Go functions exported by disk.go.
|
||||
*/
|
||||
|
||||
static bool disk_set_eject_state(bool ejected)
|
||||
{
|
||||
return izapple2DiskSetEjectState(ejected ? 1 : 0) != 0;
|
||||
}
|
||||
|
||||
static bool disk_get_eject_state(void)
|
||||
{
|
||||
return izapple2DiskGetEjectState() != 0;
|
||||
}
|
||||
|
||||
static unsigned disk_get_image_index(void)
|
||||
{
|
||||
return izapple2DiskGetImageIndex();
|
||||
}
|
||||
|
||||
static bool disk_set_image_index(unsigned index)
|
||||
{
|
||||
return izapple2DiskSetImageIndex(index) != 0;
|
||||
}
|
||||
|
||||
static unsigned disk_get_num_images(void)
|
||||
{
|
||||
return izapple2DiskGetNumImages();
|
||||
}
|
||||
|
||||
static bool disk_replace_image_index(unsigned index, const struct retro_game_info *info)
|
||||
{
|
||||
return izapple2DiskReplaceImageIndex(index, (struct retro_game_info *)info) != 0;
|
||||
}
|
||||
|
||||
static bool disk_add_image_index(void)
|
||||
{
|
||||
return izapple2DiskAddImageIndex() != 0;
|
||||
}
|
||||
|
||||
static bool disk_set_initial_image(unsigned index, const char *path)
|
||||
{
|
||||
return izapple2DiskSetInitialImage(index, (char *)path) != 0;
|
||||
}
|
||||
|
||||
static bool disk_get_image_path(unsigned index, char *s, size_t len)
|
||||
{
|
||||
return izapple2DiskGetImagePath(index, s, len) != 0;
|
||||
}
|
||||
|
||||
static bool disk_get_image_label(unsigned index, char *s, size_t len)
|
||||
{
|
||||
return izapple2DiskGetImageLabel(index, s, len) != 0;
|
||||
}
|
||||
|
||||
bool shim_set_disk_control_interface(void)
|
||||
{
|
||||
static const struct retro_disk_control_ext_callback ext = {
|
||||
disk_set_eject_state,
|
||||
disk_get_eject_state,
|
||||
disk_get_image_index,
|
||||
disk_set_image_index,
|
||||
disk_get_num_images,
|
||||
disk_replace_image_index,
|
||||
disk_add_image_index,
|
||||
disk_set_initial_image,
|
||||
disk_get_image_path,
|
||||
disk_get_image_label
|
||||
};
|
||||
|
||||
if (shim_environment(RETRO_ENVIRONMENT_SET_DISK_CONTROL_EXT_INTERFACE, (void *)&ext))
|
||||
return true;
|
||||
|
||||
/* An older frontend, that knows only the first seven and has no labels */
|
||||
static const struct retro_disk_control_callback basic = {
|
||||
disk_set_eject_state,
|
||||
disk_get_eject_state,
|
||||
disk_get_image_index,
|
||||
disk_set_image_index,
|
||||
disk_get_num_images,
|
||||
disk_replace_image_index,
|
||||
disk_add_image_index
|
||||
};
|
||||
|
||||
return shim_environment(RETRO_ENVIRONMENT_SET_DISK_CONTROL_INTERFACE, (void *)&basic);
|
||||
}
|
||||
|
||||
/*
|
||||
The libretro entry points that take a const pointer are defined here. cgo can
|
||||
not put const in the declarations it generates for the exported Go functions,
|
||||
so those four would clash with the ones in libretro.h. The rest of the
|
||||
retro_ functions are exported straight from core.go.
|
||||
*/
|
||||
|
||||
bool retro_load_game(const struct retro_game_info *game)
|
||||
{
|
||||
return izapple2LoadGame((struct retro_game_info *)game);
|
||||
}
|
||||
|
||||
bool retro_load_game_special(unsigned game_type, const struct retro_game_info *info, size_t num_info)
|
||||
{
|
||||
return izapple2LoadGameSpecial(game_type, (struct retro_game_info *)info, num_info);
|
||||
}
|
||||
|
||||
bool retro_unserialize(const void *data, size_t size)
|
||||
{
|
||||
return izapple2Unserialize((void *)data, size);
|
||||
}
|
||||
|
||||
void retro_cheat_set(unsigned index, bool enabled, const char *code)
|
||||
{
|
||||
izapple2CheatSet(index, enabled, (char *)code);
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
/*
|
||||
Bridge between the libretro C API and Go.
|
||||
|
||||
Go can not call the function pointers that the libretro frontend hands to the
|
||||
core, so every callback is stored here and reached through a plain C function.
|
||||
The keyboard callback goes the other way: the frontend needs a C function
|
||||
pointer, so shim_keyboard_callback forwards to the Go side.
|
||||
*/
|
||||
|
||||
#ifndef IZAPPLE2_SHIM_H
|
||||
#define IZAPPLE2_SHIM_H
|
||||
|
||||
#include "libretro.h"
|
||||
|
||||
/* Store the callbacks given by the frontend */
|
||||
void shim_set_environment(retro_environment_t cb);
|
||||
void shim_set_video_refresh(retro_video_refresh_t cb);
|
||||
void shim_set_audio_sample_batch(retro_audio_sample_batch_t cb);
|
||||
void shim_set_input_poll(retro_input_poll_t cb);
|
||||
void shim_set_input_state(retro_input_state_t cb);
|
||||
|
||||
/* Call the callbacks given by the frontend */
|
||||
bool shim_environment(unsigned cmd, void *data);
|
||||
void shim_video_refresh(const void *data, unsigned width, unsigned height, size_t pitch);
|
||||
size_t shim_audio_sample_batch(const int16_t *data, size_t frames);
|
||||
void shim_input_poll(void);
|
||||
int16_t shim_input_state(unsigned port, unsigned device, unsigned index, unsigned id);
|
||||
|
||||
/* The keyboard callback the core registers with the frontend */
|
||||
void shim_keyboard_callback(bool down, unsigned keycode, uint32_t character, uint16_t key_modifiers);
|
||||
|
||||
/* Offer the disk control interface, extended if the frontend knows it */
|
||||
bool shim_set_disk_control_interface(void);
|
||||
|
||||
/*
|
||||
The log of the frontend. It is a variadic function, which Go can not call, so
|
||||
the message is formatted on the Go side and passed as a single string.
|
||||
*/
|
||||
void shim_set_log(retro_log_printf_t cb);
|
||||
bool shim_has_log(void);
|
||||
void shim_log(int level, const char *message);
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,89 @@
|
||||
package main
|
||||
|
||||
/*
|
||||
#include "libretro.h"
|
||||
#include "shim.h"
|
||||
*/
|
||||
import "C"
|
||||
|
||||
import (
|
||||
"unsafe"
|
||||
|
||||
"github.com/ivanizag/izapple2/screen"
|
||||
)
|
||||
|
||||
/*
|
||||
The core renders every mode into a framebuffer of a fixed size, so the geometry
|
||||
never has to change.
|
||||
|
||||
Only the screen modes without scan lines are used, so the image keeps its 192
|
||||
lines instead of becoming four times taller. Drawing the gaps is left to the
|
||||
shaders of the frontend, which do it better. The colour of the phosphor is
|
||||
another matter and is a core option: no shader turns the colour picture into the
|
||||
half width pixels of a green monitor.
|
||||
|
||||
With super hi-res, the Videx video and the RGB card left out, every remaining
|
||||
mode is generated as 560x192, and the NTSC color filter widens it by four
|
||||
pixels. Green skips that filter and stays at 560, so the columns it does not
|
||||
cover are left black.
|
||||
*/
|
||||
const (
|
||||
frameWidth = 564
|
||||
frameHeight = 192
|
||||
)
|
||||
|
||||
type videoOutput struct {
|
||||
buffer []uint32
|
||||
screenMode int
|
||||
}
|
||||
|
||||
func newVideoOutput(screenMode int) *videoOutput {
|
||||
return &videoOutput{
|
||||
buffer: make([]uint32, frameWidth*frameHeight),
|
||||
screenMode: screenMode,
|
||||
}
|
||||
}
|
||||
|
||||
func (v *videoOutput) setScreenMode(screenMode int) {
|
||||
v.screenMode = screenMode
|
||||
}
|
||||
|
||||
// render builds the frame and hands it to the frontend as XRGB8888
|
||||
func (v *videoOutput) render(vs screen.VideoSource) {
|
||||
img := screen.Snapshot(vs, v.screenMode)
|
||||
if img == nil {
|
||||
return
|
||||
}
|
||||
|
||||
// A mode wider or taller than the framebuffer should not happen with the
|
||||
// cards this core allows, but crop it instead of writing out of bounds
|
||||
bounds := img.Bounds()
|
||||
width := min(bounds.Dx(), frameWidth)
|
||||
height := min(bounds.Dy(), frameHeight)
|
||||
|
||||
for y := range height {
|
||||
src := y * img.Stride
|
||||
dst := y * frameWidth
|
||||
for x := range width {
|
||||
p := src + 4*x
|
||||
v.buffer[dst+x] = uint32(img.Pix[p])<<16 |
|
||||
uint32(img.Pix[p+1])<<8 |
|
||||
uint32(img.Pix[p+2])
|
||||
}
|
||||
for x := width; x < frameWidth; x++ {
|
||||
v.buffer[dst+x] = 0
|
||||
}
|
||||
}
|
||||
for y := height; y < frameHeight; y++ {
|
||||
dst := y * frameWidth
|
||||
for x := range frameWidth {
|
||||
v.buffer[dst+x] = 0
|
||||
}
|
||||
}
|
||||
|
||||
C.shim_video_refresh(
|
||||
unsafe.Pointer(&v.buffer[0]),
|
||||
C.uint(frameWidth),
|
||||
C.uint(frameHeight),
|
||||
C.size_t(frameWidth*4))
|
||||
}
|
||||
@@ -275,3 +275,48 @@ func CreateConfiguredApple() (*Apple2, error) {
|
||||
}
|
||||
return a, nil
|
||||
}
|
||||
|
||||
// CreateAppleFromModel builds a machine without reading the command line, for
|
||||
// the frontends that get their settings elsewhere, like the libretro core.
|
||||
// The model is the name of a preconfigured model, as returned by
|
||||
// AvailableModels, or the path to a .cfg file. The overrides replace the
|
||||
// values of the model, keyed as the command line flags. The filenames are the
|
||||
// media to load, assigned to the slots as the positional arguments of the
|
||||
// command line. Video, keyboard and speaker won't be defined.
|
||||
func CreateAppleFromModel(model string, overrides map[string]string, filenames []string) (*Apple2, error) {
|
||||
models, _, err := loadConfigurationModelsAndDefault()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
overrideValues := newConfiguration()
|
||||
for key, value := range overrides {
|
||||
overrideValues.set(key, value)
|
||||
}
|
||||
|
||||
configuration, err := models.getWithOverrides(model, overrideValues)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = expandSlotConfigurations(configuration)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = processPositionalFilenames(configuration, filenames)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return configure(configuration)
|
||||
}
|
||||
|
||||
// AvailableModels returns the names of the preconfigured models
|
||||
func AvailableModels() ([]string, error) {
|
||||
models, _, err := loadConfigurationModelsAndDefault()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return models.availableModels(), nil
|
||||
}
|
||||
Reference in new issue
Block a user