Skip to content

Commit

Permalink
Add support for emulation in Genesis Plus GX and emscripten
Browse files Browse the repository at this point in the history
Also fixes bugs in preceding refactors
  • Loading branch information
joeyparrish committed Oct 14, 2024
1 parent e1fcdcd commit 94f7b3c
Show file tree
Hide file tree
Showing 6 changed files with 642 additions and 173 deletions.
63 changes: 63 additions & 0 deletions .github/workflows/build-emulators.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -203,3 +203,66 @@ jobs:
name: blastem-macos
path: blastem-0.6.2-kinetoscope
retention-days: 1

build-genesis-plus-gx-web:
name: Build Genesis Plus GX for web
runs-on: ubuntu-latest
steps:
- name: Checkout Genesis Plus GX
uses: actions/checkout@v4
with:
repository: libretro/Genesis-Plus-GX
ref: 7de0f0b6cde9bda1235b448aa607044b3f80ab3c
path: .

- name: Checkout Kinetoscope
uses: actions/checkout@v4
with:
repository: ${{ github.repository }}
ref: ${{ inputs.ref || github.ref }}
path: core/kinetoscope

- name: Checkout RetroArch
uses: actions/checkout@v4
with:
repository: libretro/RetroArch
ref: fbf2c70e0df88cbe9bf74752834592cf5613fca9
path: retroarch

- name: Setup Emscripten
uses: mymindstorm/setup-emsdk@v14
with:
version: 3.1.46
# This triggers the SDK to be cached
actions-cache-folder: 'emsdk-cache'

- name: Apply patch
run: patch -p1 -i core/kinetoscope/emulator-patches/genesis-plus-gx.patch

# Instructions based on
# https://github.com/libretro/RetroArch/blob/master/pkg/emscripten/README.md
- name: Build Genesis Plus GX Core
run: |
# Build the Genesis Plux GX core
emmake make -f Makefile.libretro platform=emscripten
# Copy it into RetroArch's build
cp *_libretro_emscripten.bc retroarch/libretro_emscripten.bc
# Go to RetroArch
cd retroarch
# Build support infrastructure and perform final linking step
emmake make -f Makefile.emscripten LIBRETRO=genesis_plus_gx 'LIBS=-s USE_ZLIB=1 -s FETCH=1' -j all
# Outputs are genesis_plus_gx_libretro.*
- name: Upload libretro wasm core
uses: actions/upload-artifact@v4
# FIXME: Fix artifacts
#if: inputs.for-release
with:
name: genesis-plus-gx-web
path: genesis_plus_gx_libretro.*
retention-days: 1

# TODO: deploy this with nostalgist to GitHub Pages
- name: Deploy to GitHub Pages
if: inputs.for-release
run: true
43 changes: 40 additions & 3 deletions emulator-patches/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,48 @@ We provide patches for the following emulators:
5. `patch -p1 -i kinetoscope/emulator-patches/blastem-0.6.2.patch`
6. `make` or `./build_release`

- [Genesis Plus GX](https://github.com/libretro/Genesis-Plus-GX):
1. `git clone https://github.com/libretro/Genesis-Plus-GX`
2. `cd Genesis-Plus-GX`
3. `git checkout 7de0f0b6` # Other revisions may also work
4. `git clone https://github.com/joeyparrish/kinetoscope core/kinetoscope`
5. `patch -p1 -i core/kinetoscope/emulator-patches/genesis-plus-gx.patch`
6. Compile as usual (varies widely by target platform). To compile with
Emscripten for the web:

1. `git clone https://github.com/emscripten-core/emsdk`
2. `emsdk/emsdk install 3.1.46`
3. `emsdk/emsdk install 3.1.46`
4. `source emsdk/emsdk_env.sh`
5. `emmake make -f Makefile.libretro platform=emscripten`
6. `git clone https://github.com/libretro/RetroArch`
7. `cp *_libretro_emscripten.bc RetroArch/libretro_emscripten.bc`
8. `cd RetroArch`
9. `emmake make -f Makefile.emscripten LIBRETRO=genesis_plus_gx 'LIBS=-s USE_ZLIB=1 -s FETCH=1' -j all`
10. Deploy `genesis_plus_gx_libretro.*`


## Pre-built emulators

Binary builds of BlastEm (for Linux, Windows, and macOS) and Genesis Plus GX
(for the web via retroarch, nostalgist.js, or any other libretro-compatible JS
project) are available from the releases page:

https://github.com/joeyparrish/kinetoscope/releases


## Licensing

These patches and Kinetoscope code in general are licensed under the MIT
license found in LICENSE.txt. BlastEm is licensed under GPLv3. These are
compatible in this arrangement, but the final build of BlastEm using these
patches must be distributed under GPLv3.
license found in LICENSE.txt.

BlastEm is licensed under GPLv3. BlastEm licensing is compatible with our
patches, but the final build of BlastEm using these patches must be distributed
under GPLv3.

Genesis Plus GX is licensed under a variety of open source licenses. See
https://github.com/libretro/Genesis-Plus-GX/blob/master/LICENSE.txt for full
details. Kinetoscope's MIT license is compatible with these, and the final
build of Genesis Plus GX is available under that project's original terms.

See also https://libguides.wvu.edu/c.php?g=1260463&p=9239106#s-lg-box-29255221
25 changes: 12 additions & 13 deletions emulator-patches/blastem-0.6.2.patch
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
commit 4c23028aa3a829d67910dba2ab9acff6e1a9c603
commit 169ef692633bd8b89ff0f714b6606dbe5a494889
Author: Joey Parrish <[email protected]>
Date: Wed Mar 27 07:39:28 2024 -0700

Expand All @@ -21,7 +21,7 @@ index 0000000..8f39f7f
+zdis
+kinetoscope
diff --git a/Makefile b/Makefile
index 0dddc9f..dbe9c28 100644
index 0dddc9f..9a776a5 100644
--- a/Makefile
+++ b/Makefile
@@ -23,11 +23,11 @@ ifeq ($(CPU),i686)
Expand Down Expand Up @@ -105,7 +105,7 @@ index 0dddc9f..dbe9c28 100644
%.o : %.m
$(CC) $(CFLAGS) -c -o $@ $<

+kinetoscope.o : kinetoscope/emulator-patches/kinetoscope.c
+kinetoscope.o : kinetoscope/emulator-patches/kinetoscope.c kinetoscope/emulator-patches/fetch.c
+ $(CC) $(CFLAGS) -c -o $@ $<
+
%.png : %.xcf
Expand Down Expand Up @@ -278,7 +278,7 @@ index c91273c..f637add 100755

def gchannel(Val):
diff --git a/romdb.c b/romdb.c
index 79afa76..f7ca40d 100644
index 79afa76..b5e7c9a 100644
--- a/romdb.c
+++ b/romdb.c
@@ -14,6 +14,7 @@
Expand All @@ -289,7 +289,7 @@ index 79afa76..f7ca40d 100644

#define DOM_TITLE_START 0x120
#define DOM_TITLE_END 0x150
@@ -338,6 +339,46 @@ void add_memmap_header(rom_info *info, uint8_t *rom, uint32_t size, memmap_chunk
@@ -338,6 +339,45 @@ void add_memmap_header(rom_info *info, uint8_t *rom, uint32_t size, memmap_chunk
warning("ROM uses MegaWiFi, but it is disabled\n");
}
return;
Expand All @@ -304,6 +304,8 @@ index 79afa76..f7ca40d 100644
+ info->map[0].end = 0x400000;
+ info->map[0].mask = 0x1FFFFF;
+ info->map[0].flags = MMAP_READ;
+ // Returns the address of the buffer that emulates the SRAM banks (2MB).
+ info->map[0].buffer = kinetoscope_init();
+
+ // In hardware, this whole range will trigger the /TIME signal.
+ info->map[1].start = 0xA13000;
Expand All @@ -316,6 +318,11 @@ index 79afa76..f7ca40d 100644
+ // 0x...40, ... would all map to the same port.
+ info->map[1].mask = 0x00001F;
+
+ info->map[1].write_16 = kinetoscope_write_16;
+ info->map[1].write_8 = kinetoscope_write_8;
+ info->map[1].read_16 = kinetoscope_read_16;
+ info->map[1].read_8 = kinetoscope_read_8;
+
+ info->map[2].start = 0x000000;
+ info->map[2].end = 0x200000;
+ if (rom_end < info->map[2].end) {
Expand All @@ -324,14 +331,6 @@ index 79afa76..f7ca40d 100644
+ info->map[2].mask = 0x1FFFFF;
+ info->map[2].flags = MMAP_READ;
+ info->map[2].buffer = rom;
+
+ info->map[1].write_16 = kinetoscope_write_16;
+ info->map[1].write_8 = kinetoscope_write_8;
+ info->map[1].read_16 = kinetoscope_read_16;
+ info->map[1].read_8 = kinetoscope_read_8;
+
+ // Returns the address of the buffer that emulates the SRAM banks (2MB).
+ info->map[0].buffer = kinetoscope_init();
+ return;
} else if (has_ram_header(rom, size)) {
uint32_t ram_start = read_ram_header(info, rom);
Expand Down
144 changes: 144 additions & 0 deletions emulator-patches/fetch.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
// Kinetoscope: A Sega Genesis Video Player
//
// Copyright (c) 2024 Joey Parrish
//
// See MIT License in LICENSE.txt

// Emulation of Kinetoscope video streaming hardware.
// Separate implementation of fetching, with Curl+pthread for native and with
// emscripten_fetch for web.
// An emscripten build requires -s FETCH=1 at link time.

#if defined(__EMSCRIPTEN__)
# include <emscripten/fetch.h>
#else
# include <curl/curl.h>
# include <pthread.h>
#endif

#include <stdint.h>
#include <stdio.h>

// bytes written buffer, num_things, thing_size, context
typedef size_t (*WriteCallback)(char*, size_t, size_t, void *);
// ok, status, context
typedef void (*DoneCallback)(bool, int, void *);
// terminated error string
typedef void (*ReportError)(const char* buf);

typedef struct FetchContext {
void* user_ctx;
char* url;
WriteCallback write_callback;
DoneCallback done_callback;
#if !defined(__EMSCRIPTEN__)
CURL* handle;
#endif
} FetchContext;

#if defined(__EMSCRIPTEN__)
static void fetch_with_emscripten_success(emscripten_fetch_t* fetch) {
FetchContext* ctx = (FetchContext*)fetch->userData;

int http_code = fetch->status;
printf("Kinetoscope: url = %s, http status = %d\n", ctx->url, http_code);

bool ok = http_code == 200 || http_code == 206;
if (ok) {
ctx->write_callback((char*)fetch->data, fetch->numBytes, 1, ctx->user_ctx);
}

if (ctx->done_callback) {
ctx->done_callback(ok, http_code, ctx->user_ctx);
}

free(ctx->url);
free(ctx);
emscripten_fetch_close(fetch);
}

static void fetch_with_emscripten_error(emscripten_fetch_t* fetch) {
FetchContext* ctx = (FetchContext*)fetch->userData;

printf("Kinetoscope: url = %s, error!\n", ctx->url);
ctx->done_callback(/* ok= */ false, /* http_code= */ 0, ctx->user_ctx);

free(ctx->url);
free(ctx);
emscripten_fetch_close(fetch);
}
#else
static void* fetch_with_curl_in_pthread(void* thread_ctx) {
FetchContext* ctx = (FetchContext*)thread_ctx;

CURLcode res = curl_easy_perform(ctx->handle);
long http_code = 0;
curl_easy_getinfo(ctx->handle, CURLINFO_RESPONSE_CODE, &http_code);
curl_easy_cleanup(ctx->handle);

printf("Kinetoscope: url = %s, CURLcode = %d, http status = %ld\n",
ctx->url, res, http_code);
if (res != CURLE_OK) {
printf("Curl error: %s\n", curl_easy_strerror(res));
}

bool ok = res == CURLE_OK && (http_code == 200 || http_code == 206);
ctx->done_callback(ok, http_code, ctx->user_ctx);

free(ctx->url);
free(ctx);
pthread_exit(NULL);
return NULL;
}
#endif

static void fetch_range_async(const char* url, size_t first_byte, size_t size,
WriteCallback write_callback,
DoneCallback done_callback,
void* user_ctx) {
char range[32];
bool use_range = false;
if (size != (size_t)-1) {
size_t last_byte = first_byte + size - 1;
snprintf(range, 32, "%zd-%zd", first_byte, last_byte);
// snprintf doesn't guarantee a terminator when it overflows.
range[31] = '\0';
use_range = true;
}

FetchContext* ctx = (FetchContext*)malloc(sizeof(FetchContext));
ctx->user_ctx = user_ctx;
ctx->url = strdup(url);
ctx->write_callback = write_callback;
ctx->done_callback = done_callback;

#if !defined(__EMSCRIPTEN__)
CURL* handle = curl_easy_init();
ctx->handle = handle;

curl_easy_setopt(handle, CURLOPT_URL, url);
curl_easy_setopt(handle, CURLOPT_WRITEFUNCTION, write_callback);
curl_easy_setopt(handle, CURLOPT_WRITEDATA, ctx);
if (use_range) {
curl_easy_setopt(handle, CURLOPT_RANGE, range);
}

pthread_t thread;
pthread_create(&thread, NULL, fetch_with_curl_in_pthread, ctx);
#else
emscripten_fetch_attr_t fetch_attributes;
emscripten_fetch_attr_init(&fetch_attributes);

strcpy(fetch_attributes.requestMethod, "GET");
fetch_attributes.attributes = EMSCRIPTEN_FETCH_LOAD_TO_MEMORY;

const char* headers[] = { "Range", range, NULL };
fetch_attributes.requestHeaders = headers;

fetch_attributes.userData = ctx;
fetch_attributes.onsuccess = fetch_with_emscripten_success;
fetch_attributes.onerror = fetch_with_emscripten_error;

emscripten_fetch(&fetch_attributes, url);
#endif
}
Loading

0 comments on commit 94f7b3c

Please sign in to comment.