#include "LibretroBridge.h" #include #include #include #include #include #include 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(sz) < 0) { std::puts("vsnprintf failed!"); std::fflush(stdout); return; } va_start(args, fmt); std::unique_ptr 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(cmd)) { case RETRO_ENVIRONMENT::SET_ROTATION: rotation = *static_cast(const_cast(data)); assert(rotation < 4); rotation *= 90; return true; case RETRO_ENVIRONMENT::GET_OVERSCAN: *static_cast(data) = false; return true; case RETRO_ENVIRONMENT::GET_CAN_DUPE: *static_cast(data) = true; return true; case RETRO_ENVIRONMENT::SET_MESSAGE: { const retro_message* message = static_cast(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(data) = systemDirectory.c_str(); return true; case RETRO_ENVIRONMENT::SET_PIXEL_FORMAT: { const u32 tmp = *static_cast(const_cast(data)); assert(tmp < 3); pixelFormat = static_cast(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(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(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(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(data) = variablesDirty; return true; case RETRO_ENVIRONMENT::SET_SUPPORT_NO_GAME: supportsNoGame = *static_cast(data); return true; case RETRO_ENVIRONMENT::GET_LIBRETRO_PATH: *static_cast(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(data) = 1 << static_cast(RETRO_DEVICE::JOYPAD) | 1 << static_cast(RETRO_DEVICE::KEYBOARD) | 1 << static_cast(RETRO_DEVICE::POINTER); return true; case RETRO_ENVIRONMENT::GET_LOG_INTERFACE: { retro_log_callback* cb = static_cast(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(data) = coreAssetsDirectory.c_str(); return true; case RETRO_ENVIRONMENT::GET_SAVE_DIRECTORY: *static_cast(data) = saveDirectory.c_str(); return true; case RETRO_ENVIRONMENT::SET_SYSTEM_AV_INFO: { const retro_system_av_info* av = static_cast(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(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(data) = RETRO_LANGUAGE::ENGLISH; return true; default: return false; } } template 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 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(width, height, x, y, dstBuf, dst) = co; dst++; row++; } srcBuf += pitch / 2; } } template 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(width, height, x, y, dstBuf, dst) = co; dst++; row++; } srcBuf += pitch / 2; } } template 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(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(data), videoBuf.get(), width, height, pitch); case 90: return Blit555<90>(static_cast(data), videoBuf.get(), width, height, pitch); case 180: return Blit555<180>(static_cast(data), videoBuf.get(), width, height, pitch); case 270: return Blit555<270>(static_cast(data), videoBuf.get(), width, height, pitch); default: __builtin_unreachable(); } case RETRO_PIXEL_FORMAT::XRGB8888: switch (rotation) { case 0: return Blit888<0>(static_cast(data), videoBuf.get(), width, height, pitch); case 90: return Blit888<90>(static_cast(data), videoBuf.get(), width, height, pitch); case 180: return Blit888<180>(static_cast(data), videoBuf.get(), width, height, pitch); case 270: return Blit888<270>(static_cast(data), videoBuf.get(), width, height, pitch); default: __builtin_unreachable(); } case RETRO_PIXEL_FORMAT::RGB565: switch (rotation) { case 0: return Blit565<0>(static_cast(data), videoBuf.get(), width, height, pitch); case 90: return Blit565<90>(static_cast(data), videoBuf.get(), width, height, pitch); case 180: return Blit565<180>(static_cast(data), videoBuf.get(), width, height, pitch); case 270: return Blit565<270>(static_cast(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(RETRO_DEVICE::LAST)); switch (static_cast(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 variableKeys; std::unique_ptr variableComments; std::unique_ptr 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 videoBuf; u32 videoBufSz; // audio vars u32 numSamples; std::vector sampleBuf; // input vars s16 joypads[2][static_cast(RETRO_DEVICE_ID_JOYPAD::LAST)]; s16 mouse[static_cast(RETRO_DEVICE_ID_MOUSE::LAST)]; s16 keyboard[static_cast(RETRO_KEY::LAST)]; s16 lightGun[static_cast(RETRO_DEVICE_ID_LIGHTGUN::LAST)]; s16 analog[static_cast(RETRO_DEVICE_ID_ANALOG::LAST)]; s16 pointer[static_cast(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; } }