BizHawk/LibretroBridge/LibretroBridge.cpp

741 lines
23 KiB
C++

#include "LibretroBridge.h"
#include <cassert>
#include <cstdarg>
#include <cstring>
#include <memory>
#include <string>
#include <vector>
namespace LibretroBridge {
class CallbackHandler;
class CallbackHandler {
public:
CallbackHandler()
: supportsNoGame(false)
, retroMessageString()
, retroMessageTime(0)
, geoInfoDirty(false)
, geoInfo()
, timingInfoDirty(false)
, timingInfo()
, variablesDirty(false)
, variableCount(0)
, variableKeys()
, variableComments()
, variables()
, systemDirectory()
, saveDirectory()
, coreDirectory()
, coreAssetsDirectory()
, rotation(0)
, pixelFormat(RETRO_PIXEL_FORMAT::ZRGB1555)
, width(0)
, height(0)
, videoBuf()
, videoBufSz(0)
, numSamples(0)
, sampleBuf()
{
std::memset(joypads[0], 0, sizeof (joypads[0]));
std::memset(joypads[1], 0, sizeof (joypads[1]));
std::memset(mouse, 0, sizeof (mouse));
std::memset(keyboard, 0, sizeof (keyboard));
std::memset(lightGun, 0, sizeof (lightGun));
std::memset(analog, 0, sizeof (analog));
std::memset(pointer, 0, sizeof (pointer));
}
static void RetroLog(RETRO_LOG level, const char* fmt, ...) {
va_list args;
va_start(args, fmt);
std::size_t sz = std::vsnprintf(NULL, 0, fmt, args);
va_end(args);
if (static_cast<s64>(sz) < 0) {
std::puts("vsnprintf failed!");
std::fflush(stdout);
return;
}
va_start(args, fmt);
std::unique_ptr<char[]> msg(new char[sz + 1]);
std::vsnprintf(msg.get(), sz + 1, fmt, args);
va_end(args);
std::string finalMsg;
switch (level) {
case RETRO_LOG::DEBUG:
finalMsg = "[RETRO_LOG_DEBUG] ";
break;
case RETRO_LOG::INFO:
finalMsg = "[RETRO_LOG_INFO] ";
break;
case RETRO_LOG::WARN:
finalMsg = "[RETRO_LOG_WARN] ";
break;
case RETRO_LOG::ERROR:
finalMsg = "[RETRO_LOG_ERROR] ";
break;
default:
finalMsg = "[RETRO_LOG_UNKNOWN] ";
break;
}
finalMsg += std::string(msg.get());
std::printf("%s", finalMsg.c_str());
std::fflush(stdout);
}
boolean RetroEnvironment(u32 cmd, void* data) {
switch (static_cast<RETRO_ENVIRONMENT>(cmd)) {
case RETRO_ENVIRONMENT::SET_ROTATION:
rotation = *static_cast<const u32*>(const_cast<const void*>(data));
assert(rotation < 4);
rotation *= 90;
return true;
case RETRO_ENVIRONMENT::GET_OVERSCAN:
*static_cast<boolean*>(data) = false;
return true;
case RETRO_ENVIRONMENT::GET_CAN_DUPE:
*static_cast<boolean*>(data) = true;
return true;
case RETRO_ENVIRONMENT::SET_MESSAGE:
{
const retro_message* message = static_cast<retro_message*>(data);
retroMessageString = message->msg;
retroMessageTime = message->frames;
return true;
}
case RETRO_ENVIRONMENT::SHUTDOWN:
//TODO low priority
//maybe we can tell the frontend to stop frame advancing?
return false;
case RETRO_ENVIRONMENT::SET_PERFORMANCE_LEVEL:
//unneeded
return false;
case RETRO_ENVIRONMENT::GET_SYSTEM_DIRECTORY:
*static_cast<const char**>(data) = systemDirectory.c_str();
return true;
case RETRO_ENVIRONMENT::SET_PIXEL_FORMAT:
{
const u32 tmp = *static_cast<const u32*>(const_cast<const void*>(data));
assert(tmp < 3);
pixelFormat = static_cast<RETRO_PIXEL_FORMAT>(tmp);
return true;
}
case RETRO_ENVIRONMENT::SET_INPUT_DESCRIPTORS:
//TODO medium priority
//would need some custom form displaying these probably
return false;
case RETRO_ENVIRONMENT::SET_KEYBOARD_CALLBACK:
//TODO high priority (to support keyboard consoles, probably high value for us. but that may take a lot of infrastructure work)
//the callback set is meant to be called from frontend side, so perhaps we should just call this on a SetInput call with keyboard
return false;
case RETRO_ENVIRONMENT::SET_DISK_CONTROL_INTERFACE:
//TODO high priority (to support disc systems)
return false;
case RETRO_ENVIRONMENT::SET_HW_RENDER:
//TODO high priority (to support 3d renderers)
return false;
case RETRO_ENVIRONMENT::GET_VARIABLE:
{
//according to retroarch's `core_option_manager_get` this is what we should do
variablesDirty = false;
retro_variable* req = static_cast<retro_variable*>(data);
req->value = nullptr;
for (u32 i = 0; i < variableCount; i++) {
if (variableKeys[i] == std::string(req->key)) {
req->value = variables[i].c_str();
return true;
}
}
return true;
}
case RETRO_ENVIRONMENT::SET_VARIABLES:
{
const retro_variable* var = static_cast<const retro_variable*>(data);
u32 nVars = 0;
while (var->key) {
var++;
nVars++;
}
variableCount = nVars;
variables.reset(new std::string[nVars]);
variableKeys.reset(new std::string[nVars]);
variableComments.reset(new std::string[nVars]);
var = static_cast<const retro_variable*>(data);
for (u32 i = 0; i < nVars; i++) {
variableKeys[i] = std::string(var[i].key);
variableComments[i] = std::string(var[i].value);
//analyze to find default and save it
std::string comment = variableComments[i];
std::size_t ofs = comment.find_first_of(';') + 2;
std::size_t pipe = comment.find('|', ofs);
if (pipe == std::string::npos) {
variables[i] = comment.substr(ofs);
} else {
variables[i] = comment.substr(ofs, pipe - ofs);
}
}
return true;
}
case RETRO_ENVIRONMENT::GET_VARIABLE_UPDATE:
*static_cast<boolean*>(data) = variablesDirty;
return true;
case RETRO_ENVIRONMENT::SET_SUPPORT_NO_GAME:
supportsNoGame = *static_cast<const boolean*>(data);
return true;
case RETRO_ENVIRONMENT::GET_LIBRETRO_PATH:
*static_cast<const char**>(data) = coreDirectory.c_str();
return true;
case RETRO_ENVIRONMENT::SET_AUDIO_CALLBACK:
//seems to be meant for async sound?
//the callback is meant to be called frontend side
//so in practice this callback is useless
return false;
case RETRO_ENVIRONMENT::SET_FRAME_TIME_CALLBACK:
//the frontend can send real time to the core
//and the frontend is meant to tamper with
//this timing for fast forward/slow motion?
//just no, those are pure frontend responsibilities
return false;
case RETRO_ENVIRONMENT::GET_RUMBLE_INTERFACE:
//TODO low priority
return false;
case RETRO_ENVIRONMENT::GET_INPUT_DEVICE_CAPABILITIES:
//TODO medium priority - other input methods
*static_cast<u64*>(data) = 1 << static_cast<u32>(RETRO_DEVICE::JOYPAD)
| 1 << static_cast<u32>(RETRO_DEVICE::KEYBOARD)
| 1 << static_cast<u32>(RETRO_DEVICE::POINTER);
return true;
case RETRO_ENVIRONMENT::GET_LOG_INTERFACE:
{
retro_log_callback* cb = static_cast<retro_log_callback*>(data);
cb->log = &RetroLog;
return true;
}
case RETRO_ENVIRONMENT::GET_PERF_INTERFACE:
//callbacks for performance counters?
//for performance logging???
//just no
return false;
case RETRO_ENVIRONMENT::GET_LOCATION_INTERFACE:
//the frontend is not a GPS
//the core should not need this info
return false;
case RETRO_ENVIRONMENT::GET_CORE_ASSETS_DIRECTORY:
*static_cast<const char**>(data) = coreAssetsDirectory.c_str();
return true;
case RETRO_ENVIRONMENT::GET_SAVE_DIRECTORY:
*static_cast<const char**>(data) = saveDirectory.c_str();
return true;
case RETRO_ENVIRONMENT::SET_SYSTEM_AV_INFO:
{
const retro_system_av_info* av = static_cast<const retro_system_av_info*>(data);
std::memcpy(&geoInfo, &av->geometry, sizeof (retro_game_geometry));
SetVideoSize(geoInfo.max_width * geoInfo.max_height);
geoInfoDirty = true;
std::memcpy(&timingInfo, &av->timing, sizeof (retro_system_timing));
timingInfoDirty = true;
return true;
}
case RETRO_ENVIRONMENT::SET_PROC_ADDRESS_CALLBACK:
//this is some way to get symbols for API extensions
//of which none exist
//so this is useless
return false;
case RETRO_ENVIRONMENT::SET_SUBSYSTEM_INFO:
//needs retro_load_game_special to be useful; not supported yet
return false;
case RETRO_ENVIRONMENT::SET_CONTROLLER_INFO:
//TODO medium priority probably
return false;
case RETRO_ENVIRONMENT::SET_GEOMETRY:
{
const retro_game_geometry* geo = static_cast<const retro_game_geometry*>(data);
std::memcpy(&geoInfo, geo, sizeof (retro_game_geometry));
SetVideoSize(geoInfo.max_width * geoInfo.max_height);
geoInfoDirty = true;
return true;
}
case RETRO_ENVIRONMENT::GET_USERNAME:
//we definitely want to return false here so the core will do something deterministic
return false;
case RETRO_ENVIRONMENT::GET_LANGUAGE:
*static_cast<RETRO_LANGUAGE*>(data) = RETRO_LANGUAGE::ENGLISH;
return true;
default:
return false;
}
}
template <u32 rot>
static u32* PixelAddress(u32 width, u32 height, u32 x, u32 y, u32* dstBuf, u32* dst) {
switch (rot) {
case 0:
return dst;
case 90:
{
u32 dx = y;
u32 dy = height - x - 1;
return dstBuf + dy * width + dx;
}
case 180:
{
u32 dx = width - y - 1;
u32 dy = height - x - 1;
return dstBuf + dy * width + dx;
}
case 270:
{
u32 dx = width - y - 1;
u32 dy = x;
return dstBuf + dy * width + dx;
}
default:
__builtin_unreachable();
}
}
template <u32 rot>
static void Blit555(const u16* srcBuf, u32* dstBuf, u32 width, u32 height, std::size_t pitch) {
u32* dst = dstBuf;
for (u32 y = 0; y < height; y++) {
const u16* row = srcBuf;
for (u32 x = 0; x < width; x++) {
u16 ci = *row;
u32 r = (ci & 0x001F) >> 0;
u32 g = (ci & 0x03E0) >> 5;
u32 b = (ci & 0x7C00) >> 10;
r = (r << 3) | (r >> 2);
g = (g << 3) | (g >> 2);
b = (b << 3) | (b >> 2);
u32 co = r | (g << 8) | (b << 16) | 0xFF000000U;
*PixelAddress<rot>(width, height, x, y, dstBuf, dst) = co;
dst++;
row++;
}
srcBuf += pitch / 2;
}
}
template <u32 rot>
static void Blit565(const u16* srcBuf, u32* dstBuf, u32 width, u32 height, std::size_t pitch) {
u32* dst = dstBuf;
for (u32 y = 0; y < height; y++) {
const u16* row = srcBuf;
for (u32 x = 0; x < width; x++) {
u16 ci = *row;
u32 r = (ci & 0x001F) >> 0;
u32 g = (ci & 0x07E0) >> 5;
u32 b = (ci & 0xF800) >> 11;
r = (r << 3) | (r >> 2);
g = (g << 2) | (g >> 4);
b = (b << 3) | (b >> 2);
u32 co = r | (g << 8) | (b << 16) | 0xFF000000U;
*PixelAddress<rot>(width, height, x, y, dstBuf, dst) = co;
dst++;
row++;
}
srcBuf += pitch / 2;
}
}
template <u32 rot>
static void Blit888(const u32* srcBuf, u32* dstBuf, u32 width, u32 height, std::size_t pitch) {
u32* dst = dstBuf;
for (u32 y = 0; y < height; y++) {
const u32* row = srcBuf;
for (u32 x = 0; x < width; x++) {
u32 ci = *row;
u32 co = ci | 0xFF000000U;
*PixelAddress<rot>(width, height, x, y, dstBuf, dst) = co;
dst++;
row++;
}
srcBuf += pitch / 4;
}
}
void RetroVideoRefresh(const void* data, u32 width, u32 height, std::size_t pitch) {
if (!data) {
return;
}
assert((width * height) <= videoBufSz);
this->width = width;
this->height = height;
switch (pixelFormat) {
case RETRO_PIXEL_FORMAT::ZRGB1555:
switch (rotation) {
case 0: return Blit555<0>(static_cast<const u16*>(data), videoBuf.get(), width, height, pitch);
case 90: return Blit555<90>(static_cast<const u16*>(data), videoBuf.get(), width, height, pitch);
case 180: return Blit555<180>(static_cast<const u16*>(data), videoBuf.get(), width, height, pitch);
case 270: return Blit555<270>(static_cast<const u16*>(data), videoBuf.get(), width, height, pitch);
default: __builtin_unreachable();
}
case RETRO_PIXEL_FORMAT::XRGB8888:
switch (rotation) {
case 0: return Blit888<0>(static_cast<const u32*>(data), videoBuf.get(), width, height, pitch);
case 90: return Blit888<90>(static_cast<const u32*>(data), videoBuf.get(), width, height, pitch);
case 180: return Blit888<180>(static_cast<const u32*>(data), videoBuf.get(), width, height, pitch);
case 270: return Blit888<270>(static_cast<const u32*>(data), videoBuf.get(), width, height, pitch);
default: __builtin_unreachable();
}
case RETRO_PIXEL_FORMAT::RGB565:
switch (rotation) {
case 0: return Blit565<0>(static_cast<const u16*>(data), videoBuf.get(), width, height, pitch);
case 90: return Blit565<90>(static_cast<const u16*>(data), videoBuf.get(), width, height, pitch);
case 180: return Blit565<180>(static_cast<const u16*>(data), videoBuf.get(), width, height, pitch);
case 270: return Blit565<270>(static_cast<const u16*>(data), videoBuf.get(), width, height, pitch);
default: __builtin_unreachable();
}
default:
__builtin_unreachable();
}
}
void RetroAudioSample(s16 left, s16 right) {
sampleBuf.push_back(left);
sampleBuf.push_back(right);
numSamples++;
}
std::size_t RetroAudioSampleBatch(const s16* data, std::size_t frames) {
const std::size_t ret = frames;
while (frames--) {
sampleBuf.push_back(*data++);
sampleBuf.push_back(*data++);
numSamples++;
}
return ret;
}
void RetroInputPoll() {
// this is useless
}
s16 RetroInputState(u32 port, u32 device, __attribute__((unused)) u32 index, u32 id) {
assert(device < static_cast<u32>(RETRO_DEVICE::LAST));
switch (static_cast<RETRO_DEVICE>(device)) {
case RETRO_DEVICE::NONE:
return 0;
case RETRO_DEVICE::JOYPAD:
if (port < 2) {
assert(id < sizeof (joypads[port]));
return joypads[port][id];
}
return 0; // todo: is this valid?
case RETRO_DEVICE::MOUSE:
assert(id < sizeof (mouse));
return mouse[id];
case RETRO_DEVICE::KEYBOARD:
assert(id < sizeof (keyboard));
return keyboard[id];
case RETRO_DEVICE::LIGHTGUN:
assert(id < sizeof (lightGun));
return lightGun[id];
case RETRO_DEVICE::ANALOG:
assert(id < sizeof (analog));
return analog[id];
case RETRO_DEVICE::POINTER:
assert(id < sizeof (pointer));
return pointer[id];
default:
__builtin_unreachable();
}
}
bool GetSupportsNoGame() {
return supportsNoGame;
}
void GetRetroMessage(retro_message* m) {
m->msg = retroMessageString.c_str();
m->frames = retroMessageTime;
if (retroMessageTime > 0) {
retroMessageTime--;
}
}
bool GetRetroGeometryInfo(retro_game_geometry* geo) {
if (!geoInfoDirty) {
return false;
}
std::memcpy(geo, &geoInfo, sizeof (retro_game_geometry));
geoInfoDirty = false;
return true;
}
bool GetRetroTimingInfo(retro_system_timing* timing) {
if (!timingInfoDirty) {
return false;
}
std::memcpy(timing, &timingInfo, sizeof (retro_system_timing));
timingInfoDirty = false;
return true;
}
// need some way to communicate with retro variables later
void SetDirectories(const char* systemDirectory, const char* saveDirectory, const char* coreDirectory, const char* coreAssetsDirectory) {
this->systemDirectory = systemDirectory;
this->saveDirectory = saveDirectory;
this->coreDirectory = coreDirectory;
this->coreAssetsDirectory = coreAssetsDirectory;
}
void SetVideoSize(u32 sz) {
videoBuf.reset(new u32[sz]);
videoBufSz = sz;
}
void GetVideo(u32* width, u32* height, u32* videoBuf) {
*width = this->width;
*height = this->height;
std::memcpy(videoBuf, this->videoBuf.get(), videoBufSz * sizeof (u32));
}
u32 GetAudioSize() {
return sampleBuf.size();
}
void GetAudio(u32* numSamples, s16* sampleBuf) {
*numSamples = this->numSamples;
std::memcpy(sampleBuf, this->sampleBuf.data(), this->sampleBuf.size() * sizeof (s16));
this->numSamples = 0;
this->sampleBuf.clear();
}
void SetInput(RETRO_DEVICE device, u32 port, s16* input) {
switch (device) {
case RETRO_DEVICE::NONE:
break;
case RETRO_DEVICE::JOYPAD:
assert(port < 2);
std::memcpy(joypads[port], input, sizeof (joypads[port]));
break;
case RETRO_DEVICE::MOUSE:
std::memcpy(mouse, input, sizeof (mouse));
break;
case RETRO_DEVICE::KEYBOARD:
std::memcpy(keyboard, input, sizeof (keyboard));
break;
case RETRO_DEVICE::LIGHTGUN:
std::memcpy(lightGun, input, sizeof (lightGun));
break;
case RETRO_DEVICE::ANALOG:
std::memcpy(analog, input, sizeof (analog));
break;
case RETRO_DEVICE::POINTER:
std::memcpy(pointer, input, sizeof (pointer));
break;
default:
__builtin_unreachable();
}
}
private:
// environment vars
bool supportsNoGame;
std::string retroMessageString;
u32 retroMessageTime;
bool geoInfoDirty;
retro_game_geometry geoInfo;
bool timingInfoDirty;
retro_system_timing timingInfo;
bool variablesDirty;
u32 variableCount;
std::unique_ptr<std::string[]> variableKeys;
std::unique_ptr<std::string[]> variableComments;
std::unique_ptr<std::string[]> variables;
std::string systemDirectory;
std::string saveDirectory;
std::string coreDirectory;
std::string coreAssetsDirectory;
// video vars
u32 rotation; // also an environ var
RETRO_PIXEL_FORMAT pixelFormat; // also an environ var
u32 width;
u32 height;
std::unique_ptr<u32[]> videoBuf;
u32 videoBufSz;
// audio vars
u32 numSamples;
std::vector<s16> sampleBuf;
// input vars
s16 joypads[2][static_cast<u32>(RETRO_DEVICE_ID_JOYPAD::LAST)];
s16 mouse[static_cast<u32>(RETRO_DEVICE_ID_MOUSE::LAST)];
s16 keyboard[static_cast<u32>(RETRO_KEY::LAST)];
s16 lightGun[static_cast<u32>(RETRO_DEVICE_ID_LIGHTGUN::LAST)];
s16 analog[static_cast<u32>(RETRO_DEVICE_ID_ANALOG::LAST)];
s16 pointer[static_cast<u32>(RETRO_DEVICE_ID_POINTER::LAST)];
};
static CallbackHandler * gCbHandler = nullptr;
// make a callback handler
EXPORT CallbackHandler * LibretroBridge_CreateCallbackHandler() {
return new CallbackHandler();
}
// destroy a callback handler
// this will clear the global callback handler if the passed pointer equals the global pointer
EXPORT void LibretroBridge_DestroyCallbackHandler(CallbackHandler* cbHandler) {
if (cbHandler == gCbHandler) {
gCbHandler = nullptr;
}
delete cbHandler;
}
// set a "global" callback handler
EXPORT void LibretroBridge_SetGlobalCallbackHandler(CallbackHandler* cbHandler) {
gCbHandler = cbHandler;
}
// get whether the core has reported support for no game loaded
EXPORT bool LibretroBridge_GetSupportsNoGame(CallbackHandler* cbHandler) {
return cbHandler->GetSupportsNoGame();
}
// get current retro_message set by the core
EXPORT void LibretroBridge_GetRetroMessage(CallbackHandler* cbHandler, retro_message* m) {
cbHandler->GetRetroMessage(m);
}
// get current retro_game_geometry set by the core
// returns false if no changes are needed for geometry info
// geo is only valid if true is returned
EXPORT bool LibretroBridge_GetRetroGeometryInfo(CallbackHandler* cbHandler, retro_game_geometry* geo) {
return cbHandler->GetRetroGeometryInfo(geo);
}
// get current retro_system_timing set by the core
// returns false if no changes are needed for timing info
// timing is only valid if true is returned
EXPORT bool LibretroBridge_GetRetroTimingInfo(CallbackHandler* cbHandler, retro_system_timing* timing) {
return cbHandler->GetRetroTimingInfo(timing);
}
// set directories for the core
EXPORT void LibretroBridge_SetDirectories(CallbackHandler* cbHandler, const char* systemDirectory, const char* saveDirectory, const char* coreDirectory, const char* coreAssetsDirectory) {
cbHandler->SetDirectories(systemDirectory, saveDirectory, coreDirectory, coreAssetsDirectory);
}
// set size of video in pixels
// this should equal max_width * max_height
EXPORT void LibretroBridge_SetVideoSize(CallbackHandler* cbHandler, u32 sz) {
cbHandler->SetVideoSize(sz);
}
// get video width/height and a copy of the video buffer
EXPORT void LibretroBridge_GetVideo(CallbackHandler* cbHandler, u32* width, u32* height, u32* videoBuf) {
cbHandler->GetVideo(width, height, videoBuf);
}
// get size of audio in 16 bit units
// this should be used to allocate a buffer for retrieving the audio buffer
EXPORT u32 LibretroBridge_GetAudioSize(CallbackHandler* cbHandler) {
return cbHandler->GetAudioSize();
}
// get number of stereo samples and a copy of the audio buffer
// calling this function will also reset the current audio buffer
EXPORT void LibretroBridge_GetAudio(CallbackHandler* cbHandler, u32* numSamples, s16* sampleBuf) {
cbHandler->GetAudio(numSamples, sampleBuf);
}
// set input for specific device and port
// input is expected to be sent through an array of signed 16 bit integers, the positions of input in this array defined by RETRO_DEVICE_ID_* or RETRO_KEY
EXPORT void LibretroBridge_SetInput(CallbackHandler* cbHandler, RETRO_DEVICE device, u32 port, s16* input) {
cbHandler->SetInput(device, port, input);
}
// retro callbacks
static boolean retro_environment(u32 cmd, void* data) {
assert(gCbHandler);
return gCbHandler->RetroEnvironment(cmd, data);
}
static void retro_video_refresh(const void* data, u32 width, u32 height, std::size_t pitch) {
assert(gCbHandler);
return gCbHandler->RetroVideoRefresh(data, width, height, pitch);
}
static void retro_audio_sample(s16 left, s16 right) {
assert(gCbHandler);
return gCbHandler->RetroAudioSample(left, right);
}
static std::size_t retro_audio_sample_batch(const s16* data, std::size_t frames) {
assert(gCbHandler);
return gCbHandler->RetroAudioSampleBatch(data, frames);
}
static void retro_input_poll() {
assert(gCbHandler);
return gCbHandler->RetroInputPoll();
}
static s16 retro_input_state(u32 port, u32 device, u32 index, u32 id) {
assert(gCbHandler);
return gCbHandler->RetroInputState(port, device, index, id);
}
struct RetroProcs {
decltype(&retro_environment) retro_environment_proc;
decltype(&retro_video_refresh) retro_video_refresh_proc;
decltype(&retro_audio_sample) retro_audio_sample_proc;
decltype(&retro_audio_sample_batch) retro_audio_sample_batch_proc;
decltype(&retro_input_poll) retro_input_poll_proc;
decltype(&retro_input_state) retro_input_state_proc;
};
// get procs for retro callbacks
// these are not linked to any specific callback handler
// please call LibretroBridge_SetCallbackHandler to set a global callback handler
// as you can guess, this means only a single instance can use this at a time :/
// (not like you can multi-instance libretro cores in the first place)
EXPORT void LibretroBridge_GetRetroProcs(RetroProcs* retroProcs) {
retroProcs->retro_environment_proc = &retro_environment;
retroProcs->retro_video_refresh_proc = &retro_video_refresh;
retroProcs->retro_audio_sample_proc = &retro_audio_sample;
retroProcs->retro_audio_sample_batch_proc = &retro_audio_sample_batch;
retroProcs->retro_input_poll_proc = &retro_input_poll;
retroProcs->retro_input_state_proc = &retro_input_state;
}
}