mirror of
https://github.com/libretro/FBNeo.git
synced 2026-11-07 11:06:01 -10:00
* Add headless frontend for embedding FBNeo
A new "headless" port that builds libfbneo as a shared library (fbneo_headless.dylib) exposing a small C API, for embedding in native host frontends.
Built originally to support OpenEmu-Silicon.
* Add input mapping and timing/aspect/reset to the C API
Rounds out the headless frontend with the functions a host wrapper needs to
actually run and control a game, instead of only loading one.
Input mapping (headless_input.cpp): FBHeadlessSetInput(player, button, value)
turns a simple arcade control (Up/Down/Left/Right, buttons 1-6, Start, Coin,
Service) into the right input for whatever driver is loaded. It does this by
reading each input's canonical name from the driver ("p1 up", "p1 fire 1",
"p1 coin", "p1 start", and the "coin N"/"start N" variants) and rebinding that
input to a private switch code the headless input plugin reads back. Inputs
that don't match a control are parked on a dead code so they always read as
released.
Timing and geometry, needed for correct video/audio sync and aspect ratio:
- FBHeadlessGetFrameRate() - the game's refresh rate (fps x 100)
- FBHeadlessGetAspectX/Y() - the game's pixel aspect ratio
- FBHeadlessReset() - reset the running game
The FBHeadlessButton enum lists the supported controls in a fixed order so the
host can pass them straight through.
* Add save/load functions
* Add support for cheats and RetroAchievements
* Address comments from adversarial review
* Honor BDF_16BIT_ONLY (Cave, CPS3, Megadrive)
These drivers only blit RGB565. Rendering them at 32bpp produced garbled graphics. Render at 16bpp and convert to 32bpp on present (combined with the existing rotation pass so vertical games get both), fixing Cave, CPS3 and Megadrive-arcade video while keeping the 32bpp zero-copy fast path.
* Map the Service and Configure keys to the test and service switches
Most drivers name their cabinet switches "diag" (test switch, opens the
settings menu) and "service" with no player prefix, so the headless mapper
never bound them and the frontend's Service key did nothing.
Service now drives the test switch, or "service" when a driver has no test
switch. A new FBHeadlessButtonConfigure drives "service" in drivers that
have both (it usually adds a service credit).
* Map twin-stick games
Drivers name the second stick inconsistently: "pN <dir> 2", or another player's plain d-pad (Robotron's "Fire Up" is "p2 up", Smash TV's fire stick is p3/p4, Angel Kids' left stick is p3). The headless mapper dropped the first form and sent the second to the wrong player, so Robotron's fire stick only worked from a second controller.
When the display name names a stick (Left Stick, Right Stick, Fire, Shoot, Aim...), it now decides the stick and the player; otherwise "pN <dir> 2" is player N's right stick. Like the MAME core, the left stick goes on the d-pad and the right stick on buttons 1-4 (left, up, down, right), since OpenEmu's arcade controller has one stick.
* Report little-endian cheat memory for TMS34010 and V60 boards
GetMainMemoryBigEndian returned 1 for CPUs with MB_CHEAT_ENDI_SWAP, but those (TMS34010, V60) are little-endian; the flag only exists because FBNeo's own cheat engine stores values big-endian. The host then wrote multi-byte cheats in the wrong order: in Mortal Kombat "0000A260:00A1" stored bytes 00 A1, so P1 health read as 0xA100 (bar overfilled, dies in one hit).
Every board now reports little-endian, which is what the 68K boards already did by the RetroAchievements/libretro convention.
Headless check (mk, round 1, cheat 0000A260:00A1 at frame 3100):
before: A260=00 A261=A1 after the cheat (was A1 00)
after: A260=A1 A261=00, unchanged from what the game wrote
* Save battery-backed RAM in the headless burner
Drivers keep battery-backed RAM (ACB_NVRAM: Mortal Kombat's settings,
Robotron's high scores...) only as a scan area; the frontend has to save
it. The Windows burner does that with StatedAuto in DrvInit/DrvExit, but
the headless burner never did, so those games lost their settings every
session (unlike EEPROM games, which the device saves itself).
Like the Windows burner, DrvInit(bRestore) now loads
<szAppEEPROMPath><set>.fs and DrvExit saves it. Loading a game and Reset
both restore it. Drivers without NVRAM write no file.
Headless check (mk, three runs sharing the support folder, 600 frames):
before: only mk.ini saved
after: mk.fs saved; run 1 (blank NVRAM) sits on one screen, runs 2 and
3 boot normally and are identical. sfa still saves sfa.nv only.
193 lines
5.7 KiB
C++
193 lines
5.7 KiB
C++
// Headless video output for FinalBurn Neo.
|
|
// Author: Leonardo Kasperavičius
|
|
//
|
|
// Adapted from the macOS video interface by Akop Karapetyan
|
|
// https://github.com/0xe1f/FinalBurn-X (Apache License 2.0)
|
|
//
|
|
// Built originally to support OpenEmu-Silicon
|
|
// (https://github.com/OpenEmu-Silicon/OpenEmu-Silicon).
|
|
//
|
|
// FBNeo renders every game in its native orientation (vertical games come out
|
|
// sideways), so this keeps a native render buffer for the emulator and presents
|
|
// an upright, correctly-pitched buffer to the host, rotating when needed.
|
|
// Subject to the FB Neo license (see src/license.txt).
|
|
|
|
#include "burner.h"
|
|
#include "fbheadless.h"
|
|
|
|
static unsigned char *nativeBuffer = NULL; // FBNeo renders here (pBurnDraw), nativeBpp deep
|
|
static unsigned char *displayBuffer = NULL; // exposed to the host, always 32bpp upright
|
|
static int nativeW = 0, nativeH = 0;
|
|
static int dispW = 0, dispH = 0;
|
|
static int nativeBpp = 0; // FBNeo render depth in bytes: 2 (BDF_16BIT_ONLY) or 4
|
|
static int rotate = 0; // 0 none, 1 CW90, 2 180, 3 CCW90
|
|
|
|
static void freeBuffers()
|
|
{
|
|
if (displayBuffer && displayBuffer != nativeBuffer)
|
|
free(displayBuffer);
|
|
free(nativeBuffer);
|
|
nativeBuffer = NULL;
|
|
displayBuffer = NULL;
|
|
}
|
|
|
|
static int HeadlessVideoInit()
|
|
{
|
|
int vw = 0, vh = 0;
|
|
int flags = BurnDrvGetFlags();
|
|
BurnDrvGetVisibleSize(&vw, &vh);
|
|
|
|
// Some drivers (Cave, CPS3, Megadrive) are BDF_16BIT_ONLY: their blitters only
|
|
// write RGB565, so render those at 16bpp and convert to 32bpp on present. All
|
|
// others render straight 32bpp XRGB8888, which the host consumes directly.
|
|
if (flags & BDF_16BIT_ONLY) {
|
|
nativeBpp = 2;
|
|
nVidImageDepth = 16;
|
|
} else {
|
|
nativeBpp = 4;
|
|
nVidImageDepth = 32;
|
|
}
|
|
nVidImageBPP = nativeBpp;
|
|
SetBurnHighCol(nVidImageDepth);
|
|
|
|
if (flags & BDF_ORIENTATION_VERTICAL) {
|
|
// Visible size is the upright (display) size; FBNeo renders it sideways.
|
|
nativeW = vh; nativeH = vw;
|
|
dispW = vw; dispH = vh;
|
|
rotate = (flags & BDF_ORIENTATION_FLIPPED) ? 1 : 3; // CW : CCW
|
|
} else {
|
|
nativeW = vw; nativeH = vh;
|
|
dispW = vw; dispH = vh;
|
|
rotate = (flags & BDF_ORIENTATION_FLIPPED) ? 2 : 0; // 180 : none
|
|
}
|
|
|
|
freeBuffers();
|
|
nativeBuffer = (unsigned char *)malloc(nativeW * nativeH * nativeBpp);
|
|
if (nativeBuffer == NULL)
|
|
return 1;
|
|
memset(nativeBuffer, 0, nativeW * nativeH * nativeBpp);
|
|
|
|
// The host always receives 32bpp. Zero-copy only when FBNeo already renders
|
|
// 32bpp AND upright; otherwise present() fills a dedicated 32bpp buffer
|
|
// (converting 16->32 and/or rotating).
|
|
if (nativeBpp == 4 && rotate == 0) {
|
|
displayBuffer = nativeBuffer;
|
|
} else {
|
|
displayBuffer = (unsigned char *)malloc(dispW * dispH * 4);
|
|
if (displayBuffer == NULL)
|
|
return 1;
|
|
memset(displayBuffer, 0, dispW * dispH * 4);
|
|
}
|
|
|
|
// FBNeo draws into the native buffer, pitch = native width.
|
|
nVidImageWidth = nativeW;
|
|
nVidImageHeight = nativeH;
|
|
nVidImagePitch = nativeW * nativeBpp;
|
|
nBurnBpp = nativeBpp;
|
|
nBurnPitch = nativeW * nativeBpp;
|
|
pVidImage = nativeBuffer;
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int HeadlessVideoExit()
|
|
{
|
|
freeBuffers();
|
|
nativeW = nativeH = dispW = dispH = nativeBpp = rotate = 0;
|
|
return 0;
|
|
}
|
|
|
|
// RGB565 -> 0x00RRGGBB, matching SetBurnHighCol(32)'s component order.
|
|
static inline uint32_t rgb565_to_xrgb8888(uint16_t p)
|
|
{
|
|
uint32_t r = (p >> 11) & 0x1F;
|
|
uint32_t g = (p >> 5) & 0x3F;
|
|
uint32_t b = p & 0x1F;
|
|
r = (r << 3) | (r >> 2);
|
|
g = (g << 2) | (g >> 4);
|
|
b = (b << 3) | (b >> 2);
|
|
return (r << 16) | (g << 8) | b;
|
|
}
|
|
|
|
// Map a display pixel (dx,dy) back to its native-buffer coordinate for the
|
|
// current rotation. Native/display dims are swapped for the 90-degree cases.
|
|
static inline void src_coord(int dx, int dy, int *sx, int *sy)
|
|
{
|
|
switch (rotate) {
|
|
case 1: *sx = dy; *sy = nativeH - 1 - dx; break; // CW 90
|
|
case 3: *sx = nativeW - 1 - dy; *sy = dx; break; // CCW 90
|
|
case 2: *sx = dispW - 1 - dx; *sy = dispH - 1 - dy; break; // 180
|
|
default: *sx = dx; *sy = dy; break; // none
|
|
}
|
|
}
|
|
|
|
// Present the freshly rendered native frame to the host: convert 16->32bpp for
|
|
// BDF_16BIT_ONLY drivers and/or rotate vertical games upright. Called by the
|
|
// frontend after BurnDrvFrame. The zero-copy case (32bpp, upright) is a no-op.
|
|
void headless_video_present(void)
|
|
{
|
|
if (displayBuffer == nativeBuffer)
|
|
return;
|
|
|
|
uint32_t *dst = (uint32_t *)displayBuffer;
|
|
|
|
if (nativeBpp == 2) {
|
|
const uint16_t *src = (const uint16_t *)nativeBuffer;
|
|
for (int dy = 0; dy < dispH; dy++)
|
|
for (int dx = 0; dx < dispW; dx++) {
|
|
int sx, sy;
|
|
src_coord(dx, dy, &sx, &sy);
|
|
dst[dy * dispW + dx] = rgb565_to_xrgb8888(src[sy * nativeW + sx]);
|
|
}
|
|
} else {
|
|
const uint32_t *src = (const uint32_t *)nativeBuffer;
|
|
for (int dy = 0; dy < dispH; dy++)
|
|
for (int dx = 0; dx < dispW; dx++) {
|
|
int sx, sy;
|
|
src_coord(dx, dy, &sx, &sy);
|
|
dst[dy * dispW + dx] = src[sy * nativeW + sx];
|
|
}
|
|
}
|
|
}
|
|
|
|
static int HeadlessVideoFrame(bool redraw)
|
|
{
|
|
if (pVidImage == NULL || bRunPause)
|
|
return 0;
|
|
VidFrameCallback(redraw);
|
|
return 0;
|
|
}
|
|
|
|
static int HeadlessVideoPaint(int /* validate */)
|
|
{
|
|
return 0;
|
|
}
|
|
|
|
static int HeadlessVideoScale(RECT *, int, int)
|
|
{
|
|
return 0;
|
|
}
|
|
|
|
static int HeadlessVideoGetSettings(InterfaceInfo *)
|
|
{
|
|
return 0;
|
|
}
|
|
|
|
struct VidOut VidOutHeadless = {
|
|
HeadlessVideoInit,
|
|
HeadlessVideoExit,
|
|
HeadlessVideoFrame,
|
|
HeadlessVideoPaint,
|
|
HeadlessVideoScale,
|
|
HeadlessVideoGetSettings,
|
|
_T("Headless Video"),
|
|
};
|
|
|
|
// --- C accessors for the host wrapper ---
|
|
|
|
extern "C" const unsigned char *FBHeadlessGetVideoBuffer(void) { return displayBuffer; }
|
|
extern "C" int FBHeadlessGetVideoWidth(void) { return dispW; }
|
|
extern "C" int FBHeadlessGetVideoHeight(void) { return dispH; }
|
|
extern "C" int FBHeadlessGetVideoPitch(void) { return dispW * 4; }
|
|
extern "C" int FBHeadlessGetVideoBPP(void) { return 4; }
|