From 621233c80cc8372573d0f5c8e321a150ece7890f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leonardo=20Kasperavi=C4=8Dius?= Date: Thu, 29 Oct 2026 01:21:11 +0100 Subject: [PATCH] Add an optional headless embedding layer (C API) for native frontends (#2841) * 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 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 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 .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. --- .gitignore | 1 + makefile | 3 + makefile.headless | 639 +++++++++++++++++++++++ src/burn/burn.cpp | 2 +- src/burn/cheat.cpp | 57 ++ src/burn/cheat.h | 7 + src/burner/burner.h | 2 + src/burner/headless/burner_headless.h | 140 +++++ src/burner/headless/fbheadless.h | 101 ++++ src/burner/headless/headless_drv.cpp | 134 +++++ src/burner/headless/headless_input.cpp | 209 ++++++++ src/burner/headless/headless_main.cpp | 192 +++++++ src/burner/headless/headless_memory.cpp | 217 ++++++++ src/burner/headless/headless_misc.cpp | 49 ++ src/burner/headless/tchar.h | 72 +++ src/intf/audio/aud_interface.cpp | 4 + src/intf/audio/headless/aud_headless.cpp | 49 ++ src/intf/input/headless/inp_headless.cpp | 63 +++ src/intf/input/inp_interface.cpp | 4 + src/intf/video/headless/vid_headless.cpp | 192 +++++++ src/intf/video/vid_interface.cpp | 4 + 21 files changed, 2140 insertions(+), 1 deletion(-) create mode 100644 makefile.headless create mode 100644 src/burner/headless/burner_headless.h create mode 100644 src/burner/headless/fbheadless.h create mode 100644 src/burner/headless/headless_drv.cpp create mode 100644 src/burner/headless/headless_input.cpp create mode 100644 src/burner/headless/headless_main.cpp create mode 100644 src/burner/headless/headless_memory.cpp create mode 100644 src/burner/headless/headless_misc.cpp create mode 100644 src/burner/headless/tchar.h create mode 100644 src/intf/audio/headless/aud_headless.cpp create mode 100644 src/intf/input/headless/inp_headless.cpp create mode 100644 src/intf/video/headless/vid_headless.cpp diff --git a/.gitignore b/.gitignore index ab82dd175..25fa1cc46 100644 --- a/.gitignore +++ b/.gitignore @@ -24,3 +24,4 @@ nvram *.nvr config/* fbneo.dat +fbneo_headless.dylib diff --git a/makefile b/makefile index ec5e0417b..fcefc86c2 100644 --- a/makefile +++ b/makefile @@ -120,6 +120,9 @@ sdl: FORCE sdl2: FORCE @$(MAKE) -s -f makefile.sdl2 +headless: FORCE + @$(MAKE) -s -f makefile.headless + vc: FORCE @$(MAKE) -s -f makefile.vc diff --git a/makefile.headless b/makefile.headless new file mode 100644 index 000000000..ced2ab643 --- /dev/null +++ b/makefile.headless @@ -0,0 +1,639 @@ +# Makefile for FBNeo, headless core (derived from makefile.sdl2) +# +# The first pass makes sure all intermediary targets are present. The second pass updates +# any targets, if necessary. (Intermediary) targets which have their own unique rules +# are generated as required. + +unexport + +UNAME_S := $(shell uname -s) +ifeq ($(UNAME_S),Darwin) +DARWIN=1 +endif + +ifdef DARWIN +PLATFLAGS += -arch arm64 -mmacosx-version-min=11.0 -Wno-deprecated-declarations +endif + + +ifeq ($(OS),Windows_NT) +WINDOWS=1 +endif + +# +# Flags. Uncomment any of these declarations to enable their function. +# + +# Check for changes in header files +ifndef SKIPDEPEND +DEPEND = 1 +endif + +# SANITIZE = 1 + +# +# Declare variables +# + +# Specify the name of the executable file, without ".exe" +NAME = fbneo + +PERL = 1 +LSB_FIRST = 1 +INCLUDE_7Z_SUPPORT = 1 + +BUILD_X86_ASM= +INCLUDE_AVI_RECORDING= +BUILD_A68K= +UNICODE= + + +# +# Specify paths/files +# + +objdir = obj/ +srcdir = src/ + +include makefile.burn_rules + +# Platform stuff +alldir += burner burner/headless burner/sdl dep/libs/libspng dep/libs/lib7z dep/libs/zlib intf intf/video \ + intf/video/scalers intf/video/headless intf/audio intf/audio/headless intf/input intf/input/headless intf/cd \ + dep/generated + +depobj += cdlist.o \ + \ + conc.o cong.o dat.o gamc.o gami.o image.o ioapi.o misc.o sshot.o state.o statec.o unzip.o zipfn.o \ + \ + adler32.o compress.o crc32.o deflate.o gzclose.o gzlib.o gzread.o gzwrite.o infback.o inffast.o inflate.o inftrees.o \ + trees.o uncompr.o zutil.o \ + \ + spng.o \ + \ + aud_dsp.o aud_interface.o cd_interface.o cd_chd.o inp_interface.o interface.o lowpass2.o vid_interface.o \ + vid_support.o \ + \ + inp_headless.o aud_headless.o vid_headless.o support_paths.o ips_manager.o scrn.o localise_gamelist.o romdata.o \ + cd_img.o stringset.o bzip.o media.o \ + inpdipsw.o inputbuf.o replay.o stated.o headless_main.o headless_drv.o headless_misc.o headless_input.o headless_memory.o + +ifdef FORCE_PULSE_AUDIO +alldir += intf/audio/linux + +depobj += aud_pulse_simple.o +endif + +ifdef INCLUDE_7Z_SUPPORT +depobj += un7z.o \ + \ + 7zArcIn.o 7zBuf.o 7zBuf2.o 7zCrc.o 7zCrcOpt.o 7zDec.o 7zFile.o 7zStream.o Alloc.o Bcj2.o Bra.o Bra86.o BraIA64.o CpuArch.o \ + Delta.o LzFindOpt.o LzmaDec.o Lzma2Dec.o MtDec.o Ppmd7.o Ppmd7Dec.o Ppmd7aDec.o Sha256.o Sha1Opt.o Sha256Opt.o Threads.o Xxh64.o Xz.o XzCrc64.o XzCrc64Opt.o XzDec.o ZstdDec.o +endif + +autobj += $(depobj) + +ifdef BUILD_X86_ASM +autobj += eagle_fm.o 2xsaimmx.o hq2x32.o hq3x32.o hq4x32.o superscale.o +endif + +# End, platform stuff + +incdir = $(foreach dir,$(alldir),-I$(srcdir)$(dir)) -I$(objdir)dep/generated + +lib = -lstdc++ -lm -lpthread + +ifdef FORCE_PULSE_AUDIO +lib += -lpulse-simple +endif + + +ifdef INCLUDE_SWITCHRES +incdir += `pkg-config --cflags switchres` +lib += `pkg-config --libs switchres` +endif + +autdep = $(depobj:.o=.d) +drvdep = $(drvsrc:.o=.d) + +ifdef BUILD_A68K +a68k.o = $(objdir)cpu/a68k/a68k.o +endif + +driverlist.h = $(srcdir)dep/generated/driverlist.h +ctv.h = $(srcdir)dep/generated/ctv.h +toa_gp9001_func.h = $(srcdir)dep/generated/toa_gp9001_func.h +neo_sprite_func.h = $(srcdir)dep/generated/neo_sprite_func.h +cave_tile_func.h = $(srcdir)dep/generated/cave_tile_func.h +cave_sprite_func.h = $(srcdir)dep/generated/cave_sprite_func.h +psikyo_tile_func.h = $(srcdir)dep/generated/psikyo_tile_func.h +pgm_sprite.h = $(srcdir)dep/generated/pgm_sprite.h +build_details.h = $(srcdir)dep/generated/build_details.h + +allobj = $(objdir)cpu/m68k/m68kcpu.o $(objdir)cpu/m68k/m68kops.o \ + $(foreach file,$(autobj:.o=.c), \ + $(foreach dir,$(alldir),$(subst $(srcdir),$(objdir), \ + $(firstword $(subst .c,.o,$(wildcard $(srcdir)$(dir)/$(file))))))) \ + $(foreach file,$(autobj:.o=.cpp), \ + $(foreach dir,$(alldir),$(subst $(srcdir),$(objdir), \ + $(firstword $(subst .cpp,.o,$(wildcard $(srcdir)$(dir)/$(file))))))) \ + $(foreach file,$(autobj:.o=.asm), \ + $(foreach dir,$(alldir),$(subst $(srcdir),$(objdir), \ + $(firstword $(subst .asm,.o,$(wildcard $(srcdir)$(dir)/$(file))))))) \ + $(foreach file,$(autobj:.o=.rc), \ + $(foreach dir,$(alldir),$(subst $(srcdir),$(objdir), \ + $(firstword $(subst .rc,.o,$(wildcard $(srcdir)$(dir)/$(file))))))) + +ifdef BUILD_A68K +allobj += $(a68k.o) +endif + +alldep = $(foreach file,$(autobj:.o=.c), \ + $(foreach dir,$(alldir),$(subst $(srcdir),$(objdir), \ + $(firstword $(subst .c,.d,$(wildcard $(srcdir)$(dir)/$(file))))))) \ + $(foreach file,$(autobj:.o=.cpp), \ + $(foreach dir,$(alldir),$(subst $(srcdir),$(objdir), \ + $(firstword $(subst .cpp,.d,$(wildcard $(srcdir)$(dir)/$(file))))))) \ + $(foreach file,$(autobj:.o=.rc), \ + $(foreach dir,$(alldir),$(subst $(srcdir),$(objdir), \ + $(firstword $(subst .rc,.d,$(wildcard $(srcdir)$(dir)/$(file))))))) + +autdrv := $(drvsrc:.cpp=.o) + +# +# +# Specify compiler/linker/assembler +# +# + +ifdef DARWIN + CC = gcc +else + CC = gcc +endif + +CXX = $(CC) +LD = $(CC) +AS = nasm + +# While cross-compiling, tools need to be compiled for host so that they can be executed by host. +# if HOST_CC is not set, then we aren't cross-compiling, so we'll use target compiler as host compiler +ifeq ($(HOST_CC),) +HOST_CC := $(CC) +HOST_CXX := $(CXX) +HOST_CFLAGS := $(CFLAGS) +HOST_CXXFLAGS := $(CXXFLAGS) +HOST_LDFLAGS := $(LDFLAGS) +endif + +#LDFLAGS = -static + +CFLAGS = -O2 -fomit-frame-pointer -Wno-write-strings \ + -Wall -Wno-long-long -Wno-sign-compare -Wno-uninitialized -Wno-unused \ + -Wno-conversion -Wno-attributes \ + -Wno-unused-parameter -Wno-unused-value -std=c99 \ + $(PLATFLAGS) $(DEF) $(incdir) + +CXXFLAGS = -O2 -fomit-frame-pointer -Wno-write-strings \ + -Wall -W -Wno-long-long -Wno-sign-compare \ + -Wunknown-pragmas -Wundef -Wconversion -Wno-missing-braces \ + -Wuninitialized -Wpointer-arith -Winline -Wno-multichar \ + -Wno-conversion -Wno-attributes \ + -Wno-unused-parameter -Wno-unused-value -Wno-narrowing \ + $(PLATFLAGS) $(DEF) $(incdir) + +ASFLAGS = -O1 -f coff -w-orphan-labels + +# D3DUtils & D3DMath need these +# DEF = -Dsinf=\(float\)sin -Dcosf=\(float\)cos -Dasinf=\(float\)asin -Dacosf=\(float\)acos -Dsqrtf=\(float\)sqrt + +# FIXME + DEF := -DBUILD_HEADLESS -DINCLUDE_FLACMP3_SUPPORT -DUSE_SPEEDHACKS -DFILENAME=$(NAME) -DUSE_FILE32API -DHAVE_UNISTD_H + +ifdef FORCE_PULSE_AUDIO + DEF := $(DEF) -DFORCE_PULSE_AUDIO +endif + +ifdef WINDOWS + DEF := $(DEF) -DSDL_WINDOWS +endif + +ifdef UNICODE + DEF := $(DEF) -D_UNICODE +endif + +ifdef SPECIALBUILD + DEF := $(DEF) -DSPECIALBUILD=$(SPECIALBUILD) +endif + +ifdef FASTCALL + DEF := $(DEF) -DFASTCALL +endif + +ifdef DEBUG + DEF := $(DEF) -DFBNEO_DEBUG +endif + +ifdef ROM_VERIFY + DEF := $(DEF) -DROM_VERIFY +endif + +ifdef INCLUDE_7Z_SUPPORT + DEF := $(DEF) -DINCLUDE_7Z_SUPPORT +endif + +ifdef INCLUDE_AVI_RECORDING + DEF := $(DEF) -DINCLUDE_AVI_RECORDING +endif + +ifdef LSB_FIRST + DEF := $(DEF) -DLSB_FIRST +endif + +ifdef INCLUDE_LIB_PNGH + DEF := $(DEF) -DINCLUDE_LIB_PNGH +endif + +ifdef BUILD_A68K + DEF := $(DEF) -DBUILD_A68K +endif + +ifdef BUILD_X86_ASM + DEF := $(DEF) -DBUILD_X86_ASM +endif + +ifdef BUILD_X64_EXE + DEF := $(DEF) -DBUILD_X64_EXE +endif + +ifdef INCLUDE_SWITCHRES + DEF := $(DEF) -DINCLUDE_SWITCHRES +endif + +ifdef SANITIZE +ifndef SYMBOL +SYMBOL =1 +endif +CFLAGS += -fsanitize=address -fsanitize=undefined -fsanitize=bounds-strict +CXXFLAGS += -fsanitize=address -fsanitize=undefined -fsanitize=bounds-strict +endif + +ifdef SYMBOL + CFLAGS += -ggdb3 -fno-omit-frame-pointer + CXXFLAGS += -ggdb3 -fno-omit-frame-pointer + ASFLAGS += -g + DEF := $(DEF) -D_DEBUG +else + LDFLAGS += -s +endif + +ifdef PROFILE + CFLAGS += -pg + CXXFLAGS += -pg +endif + + +ifdef BUILD_NATIVE + CFLAGS += -march=native -mtune=native + CXXFLAGS += -march=native -mtune=native +endif + +# For zlib +DEF := $(DEF) -DNO_VIZ -D_LARGEFILE64_SOURCE=0 -D_FILE_OFFSET_BITS=64 + +# For libchdr +DEF := $(DEF) -DCHDR_SYSTEM_ZLIB + +# For lib7z +ifdef INCLUDE_7Z_SUPPORT +DEF := $(DEF) -D_7ZIP_PPMD_SUPPPORT -D_7ZIP_ST +endif + +# +# +# Specify paths +# +# + +vpath %.asm $(foreach dir,$(alldir),$(srcdir)$(dir)/ ) +vpath %.cpp $(foreach dir,$(alldir),$(srcdir)$(dir)/ ) +vpath %.c $(foreach dir,$(alldir),$(srcdir)$(dir)/ ) +vpath %.h $(foreach dir,$(alldir),$(srcdir)$(dir)/ ) +vpath %.rc $(foreach dir,$(alldir),$(srcdir)$(dir)/ ) + +vpath %.o $(foreach dir,$(alldir),$(objdir)$(dir)/ ) +vpath %.d $(foreach dir,$(alldir),$(objdir)$(dir)/ ) + +# +# +# Rules +# +# + +.PHONY: all init cleandep touch clean + +ifeq ($(MAKELEVEL),1) +ifdef DEPEND + +all: init $(drvdep) $(autdep) $(autobj) $(autdrv) + @$(MAKE) -f makefile.headless -s + +else + +all: init $(autobj) $(autdrv) + @$(MAKE) -f makefile.headless -s + +endif +else + +all: fbneo_headless.dylib + +endif + +# +# +# Rule for linking the executable +# +# + +ifeq ($(MAKELEVEL),2) + +$(objdir)drivers.o: $(autdrv) + @echo Linking drivers... + @$(LD) -r -nostdlib -o $@ $^ + +ifdef WINDOWS + +$(NAME): $(allobj) $(objdir)drivers.o + @echo + @echo Linking executable... $(NAME) + @$(LD) -Wl,--image-base -Wl,0x10000000 -mconsole $(CFLAGS) $(LDFLAGS) -o $@ $^ $(lib) + +else + +fbneo_headless.dylib: $(allobj) $(objdir)drivers.o + @echo + @echo Linking dylib... $@ + @$(LD) -dynamiclib -install_name @rpath/fbneo_headless.dylib $(CFLAGS) $(LDFLAGS) -o $@ $^ $(lib) +endif + + +ifdef DEBUG + +# Don't compress when making a debug build + +else +ifdef COMPRESS + @upx --best $@ +endif +endif +endif + +ifeq ($(MAKELEVEL),1) +ifdef FORCE_UPDATE +$(build_details.h): FORCE +endif +endif + +# +# Generate the gamelist +# + +burn.o burn.d: $(driverlist.h) + +$(driverlist.h): $(drvsrc) $(srcdir)dep/scripts/gamelist.pl +ifdef PERL + @$(srcdir)dep/scripts/gamelist.pl -o $@ -l gamelist.txt \ + $(filter %.cpp,$(foreach file,$(drvsrc:.o=.cpp),$(foreach dir,$(alldir), \ + $(firstword $(wildcard $(srcdir)$(dir)/$(file)))))) +else +ifeq ($(MAKELEVEL),2) + @echo + @echo Warning: Perl is not available on this system. + @echo $@ cannot be updated or created! + @echo +endif +endif + +# +# Verify if driverlist.h needs to be updated +# + +#ifeq ($(MAKELEVEL),1) +#ifdef FORCE_UPDATE +#$(driverlist.h): FORCE +#endif +#endif + + +# +# Compile 68000 cores +# + +# A68K + +ifdef BUILD_A68K +$(a68k.o): fba_make68k.c + @echo Compiling A68K MC68000 core... + @$(CC) -mconsole $(CFLAGS) $(LDFLAGS) -DWIN32 -Wno-unused -Wno-conversion -Wno-missing-prototypes \ + -s $< -o $(subst $(srcdir),$(objdir),$($@ + +# +# Extra rules for generated header file toa_gp9001_func.h, needed by toa_gp9001.cpp +# + +toa_bcu2.d toa_bcu2.o toa_gp9001.d toa_gp9001.o: $(toa_gp9001_func.h) + +$(toa_gp9001_func.h): $(srcdir)dep/scripts/toa_gp9001_func.pl + @$(srcdir)dep/scripts/toa_gp9001_func.pl -o $(toa_gp9001_func.h) + +# +# Extra rules for generated header file neo_sprite_func.h, needed by neo_sprite.cpp +# + +neo_sprite.d neo_sprite.o: $(neo_sprite_func.h) + +$(neo_sprite_func.h): $(srcdir)dep/scripts/neo_sprite_func.pl + @$(srcdir)dep/scripts/neo_sprite_func.pl -o $(neo_sprite_func.h) + +# +# Extra rules for generated header file cave_tile_func.h, needed by cave_tile.cpp +# + +cave_tile.d cave_tile.o: $(cave_tile_func.h) + +$(cave_tile_func.h): $(srcdir)dep/scripts/cave_tile_func.pl + @$(srcdir)dep/scripts/cave_tile_func.pl -o $(cave_tile_func.h) + +# +# Extra rules for generated header file cave_sprite_func.h, needed by cave_sprite.cpp +# + +cave_sprite.d cave_sprite.o: $(cave_sprite_func.h) + +$(cave_sprite_func.h): $(srcdir)dep/scripts/cave_sprite_func.pl + @$(srcdir)dep/scripts/cave_sprite_func.pl -o $(cave_sprite_func.h) + +# +# Extra rules for generated header file psikyo_tile_func.h / psikyo_sprite_func.h, needed by psikyo_tile.cpp / psikyo_sprite.cpp +# + +psikyo_tile.d psikyo_tile.o psikyosprite.d psikyo_sprite.o: $(psikyo_tile_func.h) + +$(psikyo_tile_func.h): $(srcdir)dep/scripts/psikyo_tile_func.pl + $(srcdir)dep/scripts/psikyo_tile_func.pl -o $(psikyo_tile_func.h) + +# +# Extra rules for generated header file pgm_sprite.h, needed by pgm_draw.cpp +# + +pgm_draw.d pgm_draw.o: $(pgm_sprite.h) + +$(pgm_sprite.h): pgm_sprite_create.cpp + @echo Generating $(srcdir)dep/generated/$(@F)... + @$(HOST_CC) $(HOST_CXXFLAGS) $(HOST_LDFLAGS) $< \ + -o $(subst $(srcdir),$(objdir),$($@ + + +ifeq ($(MAKELEVEL),2) +ifdef DEPEND + +include $(alldep) + +endif +endif + + +# +# Generic rules for C/C++ files +# +# Note: require init to complete before assembling anything (see "| init" below) +# to avoid parallization issues on fresh builds. +# + +ifeq ($(MAKELEVEL),1) + +%.o: %.cpp + @echo Compiling $<... + @$(CC) $(CXXFLAGS) -c $< -o $(subst $(srcdir),$(objdir),$($(subst $(srcdir),$(objdir),$($(subst $(srcdir),$(objdir),$($(subst $(srcdir),$(objdir),$(nMemorySize; + return (size == ~0U) ? 0 : size; +} + +// Bulk-read a contiguous range from the main CPU (cpu 0) address space, opening +// the cpu once. Returns the number of bytes actually read (clamped to memory size). +unsigned int ReadMemoryRange(HWAddressType address, unsigned char *buffer, unsigned int length) +{ + if (!bDrvOkay || buffer == NULL) + return 0; + + cheat_ptr = &cpus[0]; + cpu_core_config *cfg = cheat_ptr->cpuconfig; + unsigned int memSize = (unsigned int)cfg->nMemorySize; + + INT32 nActiveCPU = cfg->active(); + if (nActiveCPU >= 0) cfg->close(); + cfg->open(cheat_ptr->nCPU); + + unsigned int i; + for (i = 0; i < length; i++) { + if (address + i >= memSize) break; + buffer[i] = cfg->read(address + i); + } + + cfg->close(); + if (nActiveCPU >= 0) cfg->open(nActiveCPU); + + return i; +} + +// Multi-byte order hint for the host's cheat search and cheat writes. This is +// deliberately NOT derived from nAddressFlags: RetroAchievements sets and +// libretro .cht cheats (the formats the host matches) are authored +// little-endian for the 68K arcade boards -- every FBNeo libretro .cht is +// big_endian="false" -- and the CPUs that set MB_CHEAT_ENDI_SWAP (TMS34010, +// V60) are natively little-endian; the flag only exists because FBNeo's own +// cheat engine stores values big-endian. Reporting 1 for those made the host +// write "0000A260:00A1" in Mortal Kombat as bytes 00 A1, so the game read +// P1 health as 0xA100. Every board is therefore reported little-endian. +int GetMainMemoryBigEndian() +{ + return 0; +} +#endif diff --git a/src/burn/cheat.h b/src/burn/cheat.h index ccce6c2fa..03c911c07 100644 --- a/src/burn/cheat.h +++ b/src/burn/cheat.h @@ -81,3 +81,10 @@ bool WriteValueAtHardwareAddress(HWAddressType address, unsigned int value, unsi bool WriteValueAtHardwareAddress_audio(HWAddressType address, unsigned int value, unsigned int size, int isLittleEndian); bool IsHardwareAddressValid(HWAddressType address); + +#ifdef BUILD_HEADLESS +// OpenEmu-Silicon headless helpers (see cheat.cpp). +unsigned int GetMainMemorySize(); +unsigned int ReadMemoryRange(HWAddressType address, unsigned char *buffer, unsigned int length); +int GetMainMemoryBigEndian(); // multibyte order for host cheats; always 0 (little-endian), see cheat.cpp +#endif diff --git a/src/burner/burner.h b/src/burner/burner.h index b57bdbee2..a7adff052 100644 --- a/src/burner/burner.h +++ b/src/burner/burner.h @@ -45,6 +45,8 @@ typedef struct tagIMAGE { #include "burner_macos.h" #elif defined (BUILD_SDL) || defined (BUILD_SDL2) #include "burner_sdl.h" +#elif defined (BUILD_HEADLESS) + #include "burner_headless.h" #elif defined (_XBOX) && !defined(__LIBRETRO__) #include "burner_xbox.h" #elif defined(__LIBRETRO__) diff --git a/src/burner/headless/burner_headless.h b/src/burner/headless/burner_headless.h new file mode 100644 index 000000000..cddf5cdae --- /dev/null +++ b/src/burner/headless/burner_headless.h @@ -0,0 +1,140 @@ +// Platform burner header for the headless FinalBurn Neo frontend. +// Author: Leonardo Kasperavičius +// +// Built originally to support OpenEmu-Silicon +// (https://github.com/OpenEmu-Silicon/OpenEmu-Silicon). +// +// Declares the types, globals and frontend prototypes the shared burner code +// expects (the headless counterpart of burner_sdl.h / burner_macos.h), without +// any SDL or GUI dependencies. Subject to the FB Neo license (see src/license.txt). + +#ifndef BURNER_HEADLESS_PLATFORM_H +#define BURNER_HEADLESS_PLATFORM_H + +// Win32 type aliases the shared code relies on. +#ifndef _WIN32 +typedef struct tagRECT +{ + int left; + int top; + int right; + int bottom; +} RECT, * PRECT, * LPRECT; +typedef const RECT* LPCRECT; +typedef UINT32 DWORD; +typedef UINT8 BYTE; +#else +#include +#endif + +#ifndef MAX_PATH +#define MAX_PATH 511 +#endif + +#ifndef __cdecl +#define __cdecl +#endif + +// misc.cpp +int SetBurnHighCol(int nDepth); + +extern int nAppVirtualFps; +extern bool bRunPause; +extern bool bAppDoFast; +extern char fpsstring[20]; +extern bool bAppShowFPS; +extern bool bAlwaysProcessKeyboardInput; +extern TCHAR szAppBurnVer[16]; +extern bool bAppFullscreen; +extern bool bIntegerScale; +extern int nWindowScale; +extern bool bAlwaysMenu; +extern int nGameSelect; +extern int nFilterSelect; +extern bool bShowAvailableOnly; +extern bool bShowClones; +extern int gameSelectedFromFilter; +extern TCHAR* GetIsoPath(); + +TCHAR* ANSIToTCHAR(const char* pszInString, TCHAR* pszOutString, int nOutSize); +char* TCHARToANSI(const TCHAR* pszInString, char* pszOutString, int nOutSize); + +#define _TtoA(a) TCHARToANSI(a, NULL, 0) +#define _AtoT(a) ANSIToTCHAR(a, NULL, 0) + +bool AppProcessKeyboardInput(); + +// config +int ConfigAppLoad(); +int ConfigAppSave(); + +// drv.cpp (headless) +extern int bDrvOkay; +extern char szAppRomPaths[DIRS_MAX][MAX_PATH]; +int DrvInit(int nDrvNum, bool bRestore); +int DrvInitCallback(); +int DrvExit(); +int ProgressUpdateBurner(double dProgress, const TCHAR* pszText, bool bAbs); +int AppError(TCHAR* szText, int bWarning); + +// run +extern int RunMessageLoop(); +extern int RunReset(); + +#define MESSAGE_MAX_FRAMES 180 +#define MESSAGE_MAX_LENGTH 255 + +extern UINT32 messageFrames; +extern char lastMessage[MESSAGE_MAX_LENGTH]; + +void UpdateMessage(char* message); +int StatedAuto(int bSave); + +// media.cpp +int MediaInit(); +int MediaExit(); + +// inpdipsw.cpp +#define DIP_MAX_NAME 64 +#define MAXDIPSWITCHES 32 +#define MAXDIPOPTIONS 32 +bool setDIPSwitchOption(int dipgroup, int dipoption); +int InpDIPSWCreate(); +void InpDIPSWResetDIPs(); + +struct GroupOfDIPSwitches +{ + BurnDIPInfo dipSwitch; + UINT16 DefaultDIPOption; + UINT16 SelectedDIPOption; + char OptionsNamesWithCheckBoxes[MAXDIPOPTIONS][DIP_MAX_NAME]; + BurnDIPInfo dipSwitchesOptions[MAXDIPOPTIONS]; +}; + +// inp_interface.cpp +int InputInit(); +int InputExit(); +int InputMake(bool bCopy); + +// stated.cpp +int QuickState(int bSave); +extern int bDrvSaveAll; + +// stringset.cpp +class StringSet { +public: + TCHAR* szText; + int nLen; + int __cdecl Add(TCHAR* szFormat, ...); + int Reset(); + + StringSet(); + ~StringSet(); +}; + +// support_paths.cpp +extern TCHAR szAppListsPath[MAX_PATH]; +extern TCHAR szAppDatListsPath[MAX_PATH]; +extern TCHAR szAppArchivesPath[MAX_PATH]; + +#endif // BURNER_HEADLESS_PLATFORM_H diff --git a/src/burner/headless/fbheadless.h b/src/burner/headless/fbheadless.h new file mode 100644 index 000000000..b44b4edc4 --- /dev/null +++ b/src/burner/headless/fbheadless.h @@ -0,0 +1,101 @@ +// Headless C API for FinalBurn Neo. +// Author: Leonardo Kasperavičius +// +// Built originally to support OpenEmu-Silicon +// (https://github.com/OpenEmu-Silicon/OpenEmu-Silicon). +// +// Consumed by a thin host wrapper so the emulator-side glue stays in the fork. +// Subject to the FB Neo license (see src/license.txt). + +#ifndef BURNER_HEADLESS_H +#define BURNER_HEADLESS_H + +#ifdef __cplusplus +extern "C" { +#endif + +// Lifecycle +int FBHeadlessInit(void); +void FBHeadlessExit(void); +void FBHeadlessSetRomPath(const char *path); +int FBHeadlessLoadDriver(const char *shortName); // e.g. "mslug"; returns 0 on success +void FBHeadlessRunFrame(int draw); + +// Video (filled by intf/video/headless) +const unsigned char *FBHeadlessGetVideoBuffer(void); +int FBHeadlessGetVideoWidth(void); +int FBHeadlessGetVideoHeight(void); +int FBHeadlessGetVideoPitch(void); +int FBHeadlessGetVideoBPP(void); + +// Audio (filled each frame by the frontend) +int FBHeadlessGetAudioSampleRate(void); +const short *FBHeadlessGetAudioBuffer(void); +int FBHeadlessGetAudioFrames(void); // stereo sample frames produced last RunFrame + +// Input: value is non-zero when pressed. code is an FBNeo input switch code. +void FBHeadlessSetInputState(int code, int value); + +// Frame timing / geometry (valid after a driver is loaded). +int FBHeadlessGetFrameRate(void); // frames per second * 100 (e.g. 6000 = 60fps) +int FBHeadlessGetAspectX(void); +int FBHeadlessGetAspectY(void); + +// Reset the loaded driver. +void FBHeadlessReset(void); + +// Save/load a full machine state to/from a file. Returns 0 on success. +int FBHeadlessSaveState(const char *path); +int FBHeadlessLoadState(const char *path); + +// Memory access for cheats, cheat search, and achievements. Addresses are in the +// main CPU (cpu 0) hardware address space. size is 1..4 bytes; littleEndian +// controls byte order for multi-byte values. A value found via a memory-range +// snapshot can be written back at the same address with FBHeadlessWriteMemory. +unsigned int FBHeadlessGetMainMemorySize(void); // 0 until a driver is loaded +unsigned int FBHeadlessReadMemoryRange(unsigned int address, unsigned char *out, unsigned int length); // returns bytes read +unsigned int FBHeadlessReadMemory(unsigned int address, int size, int littleEndian); +int FBHeadlessWriteMemory(unsigned int address, unsigned int value, int size, int littleEndian); // 1 on success +int FBHeadlessIsAddressValid(unsigned int address); + +// Achievements memory. RetroAchievements sets are authored against FBNeo's flat +// main-RAM exposure (per driver hardware type), not the CPU address space. +// FBHeadlessCheevosInit rebuilds the map (call after load/reset); the read is +// gated to the mapped regions. Returns bytes read (stops at the first unmapped byte). +void FBHeadlessCheevosInit(void); +unsigned int FBHeadlessReadAchievementMemory(unsigned int address, unsigned char *out, unsigned int length); + +// Cheats: write into the achievement memory map (same flat RAM the search reads), and +// enumerate the mapped regions so the host can present cheat search. bigEndian follows +// the libretro/RetroAchievements convention the host matches: 0 (little-endian) for every +// board, including the 68K ones. See GetMainMemoryBigEndian. +int FBHeadlessWriteAchievementMemory(unsigned int address, unsigned int value, int size, int littleEndian); +int FBHeadlessGetMemoryRegionCount(void); +int FBHeadlessGetMemoryRegion(int index, unsigned int *start, unsigned int *length, int *bigEndian); + +// High-level input. player is 0-based; button is an FBHeadlessButton value. The +// fork maps it to the active driver's inputs (parsed from its input names). +typedef enum { + FBHeadlessButtonUp = 0, + FBHeadlessButtonDown, + FBHeadlessButtonLeft, + FBHeadlessButtonRight, + FBHeadlessButton1, + FBHeadlessButton2, + FBHeadlessButton3, + FBHeadlessButton4, + FBHeadlessButton5, + FBHeadlessButton6, + FBHeadlessButtonStart, + FBHeadlessButtonCoin, + FBHeadlessButtonService, // test switch (settings menu); "service" when there is none + FBHeadlessButtonConfigure, // "service" input of drivers that also have a test switch + FBHeadlessButtonCount +} FBHeadlessButton; +void FBHeadlessSetInput(int player, int button, int value); + +#ifdef __cplusplus +} +#endif + +#endif // BURNER_HEADLESS_H diff --git a/src/burner/headless/headless_drv.cpp b/src/burner/headless/headless_drv.cpp new file mode 100644 index 000000000..171596770 --- /dev/null +++ b/src/burner/headless/headless_drv.cpp @@ -0,0 +1,134 @@ +// Headless driver glue for FinalBurn Neo. +// Author: Leonardo Kasperavičius +// +// Built originally to support OpenEmu-Silicon +// (https://github.com/OpenEmu-Silicon/OpenEmu-Silicon). +// +// The headless counterpart of the SDL burner's drv.cpp: wraps BurnDrvInit in the +// zip loader and provides the frontend callbacks the shared burner code calls. +// CD systems (NeoGeo CD / PCE CD) are not wired yet. Subject to the FB Neo +// license (see src/license.txt). + +#include "burner.h" + +int bDrvOkay = 0; +char szAppRomPaths[DIRS_MAX][MAX_PATH] = { { "roms/" }, }; + +static bool bSaveRAM = false; + +// Battery-backed RAM (ACB_NVRAM: Mortal Kombat's settings, Robotron's high +// scores...) is not saved by the driver, unlike the EEPROM device. Like the +// Windows burner's StatedAuto, keep it in .fs: loaded +// after init and saved before exit. Drivers without NVRAM write no file. +static void DrvNvramAuto(int bSave) +{ + TCHAR szName[MAX_PATH]; + _stprintf(szName, _T("%s%s.fs"), szAppEEPROMPath, BurnDrvGetText(DRV_NAME)); + if (bSave) + BurnStateSave(szName, 0); + else + BurnStateLoad(szName, 0, NULL); +} + +static int DoLibInit() // Init the Burn library's driver +{ + BzipOpen(false); + int nRet = BurnDrvInit(); + BzipClose(); + return nRet ? 1 : 0; +} + +// Catch BurnLoadRom() calls once emulation has started: open the zip module, +// forward, then exit cleanly. +static int DrvLoadRom(unsigned char* Dest, int* pnWrote, int i) +{ + BzipOpen(false); + int nRet = BurnExtLoadRom(Dest, pnWrote, i); + BzipClose(); + + BurnExtLoadRom = DrvLoadRom; + return nRet; +} + +int DrvInit(int nDrvNum, bool bRestore) +{ + DrvExit(); // Make sure exited + MediaExit(); + + nBurnDrvActive = nDrvNum; + + { // Init input and audio; defer blitter init. + bVidOkay = 1; + bAudOkay = 1; + MediaInit(); + bVidOkay = 0; + bAudOkay = 0; + } + + nMaxPlayers = BurnDrvGetMaxPlayers(); + GameInpInit(); + + bSaveInputs = ConfigGameLoad(true); + if (bSaveInputs) ConfigGameLoadHardwareDefaults(); + + InputMake(true); + GameInpDefault(); + + if (DoLibInit()) { + BurnDrvExit(); + return 1; + } + + BurnExtLoadRom = DrvLoadRom; + + bDrvOkay = 1; + bSaveRAM = false; + nBurnLayer = 0xFF; + ConfigCheatLoad(); + RunReset(); + if (bRestore) { + DrvNvramAuto(0); + bSaveRAM = true; + } + return 0; +} + +int DrvInitCallback() +{ + return DrvInit(nBurnDrvActive, false); +} + +int DrvExit() +{ + if (bDrvOkay) { + if (nBurnDrvActive < nBurnDrvCount) { + if (bSaveRAM) { + DrvNvramAuto(1); + bSaveRAM = false; + } + ConfigGameSave(bSaveInputs); + GameInpExit(); + BurnDrvExit(); + } + } + + BurnExtLoadRom = NULL; + bDrvOkay = 0; + return 0; +} + +int ProgressUpdateBurner(double /* dProgress */, const TCHAR* /* pszText */, bool /* bAbs */) +{ + return 0; +} + +int AppError(TCHAR* /* szText */, int /* bWarning */) +{ + return 0; +} + +// The host drives frame timing, so there is no throttle to reset. +int RunReset() +{ + return 0; +} diff --git a/src/burner/headless/headless_input.cpp b/src/burner/headless/headless_input.cpp new file mode 100644 index 000000000..c4cc8dbe4 --- /dev/null +++ b/src/burner/headless/headless_input.cpp @@ -0,0 +1,209 @@ +// Headless input mapping for FinalBurn Neo. +// Author: Leonardo Kasperavičius +// +// Built originally to support OpenEmu-Silicon +// (https://github.com/OpenEmu-Silicon/OpenEmu-Silicon). +// +// Maps a (player, FBHeadlessButton) pair to the active driver's GameInp. FBNeo +// exposes each input's canonical lowercase name via szInfo ("p1 up", +// "p1 fire 1", "p1 coin", "p1 start"); each matched digital input is rebound to +// a private switch code the headless input plugin reads from its state table. +// Subject to the FB Neo license (see src/license.txt). + +#include "burner.h" +#include "fbheadless.h" + +// Private switch-code space, chosen to avoid the driver's default key/joy codes. +// code = HL_CODE_BASE + player*16 + button; unmatched digital inputs get a dead +// code that is never set so they read as released. +#define HL_CODE_BASE 0x100 +#define HL_CODE_DEAD 0x1FF + +static int hl_code(int player, int button) +{ + return HL_CODE_BASE + player * 16 + button; +} + +// Parse an FBNeo szInfo name into (player, FBHeadlessButton). Returns 1 on match. +static int hl_classify(const char *szInfo, int *outPlayer, int *outButton) +{ + if (szInfo == NULL) + return 0; + + // "pN " form (the common case). + if ((szInfo[0] == 'p' || szInfo[0] == 'P') && + szInfo[1] >= '1' && szInfo[1] <= '4' && szInfo[2] == ' ') { + int player = szInfo[1] - '1'; + const char *r = szInfo + 3; + int b = -1; + + if (!strcmp(r, "up")) b = FBHeadlessButtonUp; + else if (!strcmp(r, "down")) b = FBHeadlessButtonDown; + else if (!strcmp(r, "left")) b = FBHeadlessButtonLeft; + else if (!strcmp(r, "right")) b = FBHeadlessButtonRight; + else if (!strcmp(r, "start")) b = FBHeadlessButtonStart; + else if (!strcmp(r, "coin")) b = FBHeadlessButtonCoin; + else if (!strcmp(r, "service")) b = FBHeadlessButtonService; + else if (!strcmp(r, "diagnostic")) b = FBHeadlessButtonService; + else if (!strncmp(r, "fire ", 5) && r[5] >= '1' && r[5] <= '6') + b = FBHeadlessButton1 + (r[5] - '1'); + else if (!strncmp(r, "button ", 7) && r[7] >= '1' && r[7] <= '6') + b = FBHeadlessButton1 + (r[7] - '1'); + + if (b >= 0) { + *outPlayer = player; + *outButton = b; + return 1; + } + return 0; + } + + // System "coin N" / "start N" form. + if (!strncmp(szInfo, "coin ", 5) && szInfo[5] >= '1' && szInfo[5] <= '4') { + *outPlayer = szInfo[5] - '1'; + *outButton = FBHeadlessButtonCoin; + return 1; + } + if (!strncmp(szInfo, "start ", 6) && szInfo[6] >= '1' && szInfo[6] <= '4') { + *outPlayer = szInfo[6] - '1'; + *outButton = FBHeadlessButtonStart; + return 1; + } + + return 0; +} + +// Cabinet-wide test switch ("diag" in most drivers), which opens the game's +// settings menu. +static int hl_is_test_switch(const char *szInfo) +{ + return szInfo != NULL && + (!strcmp(szInfo, "diag") || !strcmp(szInfo, "diagnostic") || + !strcmp(szInfo, "diagnostics") || !strcmp(szInfo, "test")); +} + +// Cabinet-wide "service" input. Drivers with a separate test switch use it as a +// service credit; older drivers without one use it as the test switch itself. +static int hl_is_service_switch(const char *szInfo) +{ + return szInfo != NULL && !strcmp(szInfo, "service"); +} + +// Index of a direction word at the start of s (0 left, 1 up, 2 down, 3 right), +// or -1. *len receives its length. +static int hl_direction(const char *s, size_t *len) +{ + static const char *const dirs[] = { "left", "up", "down", "right" }; + for (int d = 0; d < 4; d++) { + size_t n = strlen(dirs[d]); + if (!strncasecmp(s, dirs[d], n)) { + *len = n; + return d; + } + } + return -1; +} + +// Twin-stick games (Robotron, Smash TV, Angel Kids, Ikari Warriors...), mapped +// the way the MAME core does: the left stick on the player's d-pad, the right +// stick on buttons 1-4 (left, up, down, right), since OpenEmu's arcade +// controller has one stick. Drivers name the sticks inconsistently: the second +// stick may be "pN 2", or either stick may borrow another player's plain +// d-pad ("p2 up" for Robotron's "Fire Up", "p3 up" for Angel Kids' "P1 Left +// Stick Up"). The display name, when it names a stick, decides the stick and +// the player; otherwise "pN 2" is player N's right stick. +static int hl_twin_stick(const char *szName, const char *szInfo, int *outPlayer, int *outButton) +{ + if (szName == NULL || szInfo == NULL) + return 0; + if (szInfo[0] != 'p' || szInfo[1] < '1' || szInfo[1] > '4' || szInfo[2] != ' ') + return 0; + + size_t len = 0; + int dir = hl_direction(szInfo + 3, &len); + if (dir < 0) + return 0; + const char *rest = szInfo + 3 + len; + int secondStick = !strcmp(rest, " 2"); + if (!secondStick && *rest != '\0') + return 0; + + static const struct { const char *prefix; int right; } sticks[] = { + { "Left Stick ", 0 }, { "Lft Stick ", 0 }, { "L Stick ", 0 }, { "Left ", 0 }, + { "Right Stick ", 1 }, { "Rght Stick ", 1 }, { "ight Stick ", 1 }, { "R Stick ", 1 }, + { "Right ", 1 }, { "Fire ", 1 }, { "Shoot ", 1 }, { "Aim ", 1 }, + }; + static const int dpad[] = { FBHeadlessButtonLeft, FBHeadlessButtonUp, FBHeadlessButtonDown, FBHeadlessButtonRight }; + + const char *n = szName; + int player = 0; + if ((n[0] == 'P' || n[0] == 'p') && n[1] >= '1' && n[1] <= '4' && n[2] == ' ') { + player = n[1] - '1'; + n += 3; + } + for (size_t s = 0; s < sizeof(sticks) / sizeof(sticks[0]); s++) { + size_t sl = strlen(sticks[s].prefix); + size_t nameDirLen = 0; + if (strncasecmp(n, sticks[s].prefix, sl) != 0) + continue; + if (hl_direction(n + sl, &nameDirLen) != dir || n[sl + nameDirLen] != '\0') + break; + *outPlayer = player; + *outButton = sticks[s].right ? FBHeadlessButton1 + dir : dpad[dir]; + return 1; + } + + if (secondStick) { + *outPlayer = szInfo[1] - '1'; + *outButton = FBHeadlessButton1 + dir; + return 1; + } + return 0; +} + +// Rebind every digital input of the loaded driver to the headless code space. +// Called after DrvInit (which runs GameInpInit/GameInpDefault). +void headless_map_inputs(void) +{ + struct BurnInputInfo bii; + + // Service always opens the settings menu: it drives the test switch when the + // driver has one (and Configure drives "service"), otherwise "service". + int hasTestSwitch = 0; + for (UINT32 i = 0; i < nGameInpCount; i++) { + if (BurnDrvGetInputInfo(&bii, i) == 0 && bii.nType == BIT_DIGITAL && hl_is_test_switch(bii.szInfo)) { + hasTestSwitch = 1; + break; + } + } + + for (UINT32 i = 0; i < nGameInpCount; i++) { + if (BurnDrvGetInputInfo(&bii, i)) + continue; + if (bii.nType != BIT_DIGITAL) + continue; + + GameInp[i].nInput = GIT_SWITCH; + + int player = 0, button = 0; + if (hl_is_test_switch(bii.szInfo)) + GameInp[i].Input.Switch.nCode = (UINT16)hl_code(0, FBHeadlessButtonService); + else if (hl_is_service_switch(bii.szInfo)) + GameInp[i].Input.Switch.nCode = (UINT16)hl_code(0, hasTestSwitch ? FBHeadlessButtonConfigure : FBHeadlessButtonService); + else if (hl_twin_stick(bii.szName, bii.szInfo, &player, &button)) + GameInp[i].Input.Switch.nCode = (UINT16)hl_code(player, button); + else if (hl_classify(bii.szInfo, &player, &button) && player >= 0 && player < 4) + GameInp[i].Input.Switch.nCode = (UINT16)hl_code(player, button); + else + GameInp[i].Input.Switch.nCode = HL_CODE_DEAD; + } +} + +extern "C" void FBHeadlessSetInput(int player, int button, int value) +{ + if (player < 0 || player >= 4) + return; + if (button < 0 || button >= FBHeadlessButtonCount) + return; + FBHeadlessSetInputState(hl_code(player, button), value); +} diff --git a/src/burner/headless/headless_main.cpp b/src/burner/headless/headless_main.cpp new file mode 100644 index 000000000..ac4b2f5b4 --- /dev/null +++ b/src/burner/headless/headless_main.cpp @@ -0,0 +1,192 @@ +// Headless frontend for FinalBurn Neo. +// Author: Leonardo Kasperavičius +// +// Built originally to support OpenEmu-Silicon +// (https://github.com/OpenEmu-Silicon/OpenEmu-Silicon). +// +// Provides the burner-side globals the FBNeo library expects from a frontend and +// a small C API the host drives. Video/audio/input are routed through the +// intf/{video,audio,input}/headless plugins; driver init/exit lives in drv.cpp. +// +// Subject to the FB Neo license (see src/license.txt). + +#include "burner.h" +#include "fbheadless.h" + +// Implemented in headless_input.cpp. +void headless_map_inputs(void); + +// Implemented in vid_headless.cpp (rotates the native frame to the display buffer). +void headless_video_present(void); + +// --- Burner globals expected by the FBNeo library / shared burner code --- +int nAppVirtualFps = 6000; // app fps * 100 +bool bRunPause = 0; +bool bAppDoFast = 0; +char fpsstring[20] = ""; +bool bAppShowFPS = 0; +bool bAlwaysProcessKeyboardInput = 0; +bool bAppFullscreen = 0; +bool bIntegerScale = false; +int nWindowScale = 2; +bool bAlwaysMenu = false; +int nGameSelect = 0; +int nFilterSelect = 0; +bool bShowAvailableOnly = true; +bool bShowClones = true; +int gameSelectedFromFilter = -1; +UINT32 messageFrames = 0; +char lastMessage[MESSAGE_MAX_LENGTH] = ""; +int bDrvSaveAll = 0; +TCHAR szAppBurnVer[16] = _T(""); + +// --- Frontend audio state --- +static short *audBuffer = NULL; +static int audFrames = 0; // stereo sample frames produced last frame +static int audSampleRate = 48000; + +static int allocAudio() +{ + free(audBuffer); + audBuffer = NULL; + if (nBurnSoundLen <= 0) + return 1; + audBuffer = (short *)malloc((nBurnSoundLen * 2 + 16) * sizeof(short)); + if (audBuffer == NULL) + return 1; + memset(audBuffer, 0, (nBurnSoundLen * 2 + 16) * sizeof(short)); + return 0; +} + +// --- C API --- + +int FBHeadlessInit(void) +{ + BurnLibInit(); + return 0; +} + +void FBHeadlessExit(void) +{ + DrvExit(); + VidExit(); + AudSoundExit(); + InputExit(); + free(audBuffer); + audBuffer = NULL; + BurnLibExit(); +} + +void FBHeadlessSetRomPath(const char *path) +{ + // BzipOpen builds zip paths as szAppRomPaths[d] + ".zip" with no + // separator, so the path must end with a slash. + size_t n = strlen(path); + if (n > 0 && path[n - 1] == '/') + snprintf(szAppRomPaths[0], MAX_PATH, "%s", path); + else + snprintf(szAppRomPaths[0], MAX_PATH, "%s/", path); +} + +int FBHeadlessLoadDriver(const char *shortName) +{ + UINT32 i; + for (i = 0; i < nBurnDrvCount; i++) { + nBurnDrvActive = i; + if (strcmp(BurnDrvGetTextA(DRV_NAME), shortName) == 0) + break; + } + if (i >= nBurnDrvCount) + return 1; + + nAudSampleRate[0] = audSampleRate; + nBurnSoundRate = audSampleRate; + + InputInit(); + AudSoundInit(); + + if (DrvInit(i, true)) // true: restore battery-backed RAM + return 1; + + VidInit(); // after DrvInit: the game's visible size is known + headless_map_inputs(); + + nAppVirtualFps = nBurnFPS; + if (nAppVirtualFps <= 0) + nAppVirtualFps = 6000; + nBurnSoundLen = (audSampleRate * 100 + (nAppVirtualFps >> 1)) / nAppVirtualFps; + allocAudio(); + + FBHeadlessCheevosInit(); // build the achievement memory map + return 0; +} + +void FBHeadlessRunFrame(int draw) +{ + if (!bDrvOkay) + return; + + InputMake(true); + + pBurnDraw = draw ? pVidImage : NULL; + pBurnSoundOut = audBuffer; + + BurnDrvFrame(); + + if (draw) + headless_video_present(); + + audFrames = nBurnSoundLen; +} + +// Audio accessors +int FBHeadlessGetAudioSampleRate(void) { return audSampleRate; } +const short *FBHeadlessGetAudioBuffer(void) { return audBuffer; } +int FBHeadlessGetAudioFrames(void) { return audFrames; } + +// Frame timing / geometry +int FBHeadlessGetFrameRate(void) { return nBurnFPS; } + +int FBHeadlessGetAspectX(void) +{ + INT32 x = 0, y = 0; + if (bDrvOkay) + BurnDrvGetAspect(&x, &y); + return x; +} + +int FBHeadlessGetAspectY(void) +{ + INT32 x = 0, y = 0; + if (bDrvOkay) + BurnDrvGetAspect(&x, &y); + return y; +} + +void FBHeadlessReset(void) +{ + if (!bDrvOkay) + return; + DrvInit(nBurnDrvActive, true); // full re-init; DrvExit saves battery RAM, DrvInit restores it + VidInit(); // DrvInit defers blitter init + frees buffers; recreate them + headless_map_inputs(); + FBHeadlessCheevosInit(); // RAM buffers were reallocated; refresh the map +} + +int FBHeadlessSaveState(const char *path) +{ + if (!bDrvOkay || path == NULL) + return 1; + TCHAR szName[1024]; + ANSIToTCHAR(path, szName, 1024); + return BurnStateSave(szName, 1); // bAll=1: full state +} + +int FBHeadlessLoadState(const char *path) +{ + if (!bDrvOkay || path == NULL) + return 1; + TCHAR szName[1024]; + ANSIToTCHAR(path, szName, 1024); + return BurnStateLoad(szName, 1, NULL); // game already loaded, so no reload callback +} diff --git a/src/burner/headless/headless_memory.cpp b/src/burner/headless/headless_memory.cpp new file mode 100644 index 000000000..6ff14b857 --- /dev/null +++ b/src/burner/headless/headless_memory.cpp @@ -0,0 +1,217 @@ +// Memory access for cheats, cheat search, and achievements (FinalBurn Neo headless). +// Author: Leonardo Kasperavičius +// +// Built originally to support OpenEmu-Silicon +// (https://github.com/OpenEmu-Silicon/OpenEmu-Silicon). +// +// Thin C wrappers over FBNeo's cheat-engine memory interface (src/burn/cheat.cpp). +// All addresses are in the main CPU (cpu 0) hardware address space — the same +// space FBNeo's own cheat and achievement support uses, so a value found by a +// memory search can be written back by address. +// +// Subject to the FB Neo license (see src/license.txt). + +#include "burner.h" +#include "state.h" // BurnArea, BurnAreaScan, BurnAcb, ACB_FULLSCAN +#include "fbheadless.h" + +unsigned int FBHeadlessGetMainMemorySize(void) +{ + return GetMainMemorySize(); +} + +unsigned int FBHeadlessReadMemoryRange(unsigned int address, unsigned char *out, unsigned int length) +{ + return ReadMemoryRange(address, out, length); +} + +// Standard byte order: byte i lives at address+i. littleEndian controls which end +// is the least-significant byte. (FBNeo's own Read/WriteValueAtHardwareAddress use +// an MSB-first, address-decrementing convention — not exposed here to avoid surprises.) +unsigned int FBHeadlessReadMemory(unsigned int address, int size, int littleEndian) +{ + if (!bDrvOkay || size < 1 || size > 4) + return 0; + unsigned char b[4] = { 0, 0, 0, 0 }; + ReadMemoryRange(address, b, (unsigned int)size); + unsigned int value = 0; + for (int i = 0; i < size; i++) { + int shift = littleEndian ? (8 * i) : (8 * (size - 1 - i)); + value |= (unsigned int)b[i] << shift; + } + return value; +} + +int FBHeadlessWriteMemory(unsigned int address, unsigned int value, int size, int littleEndian) +{ + if (!bDrvOkay || size < 1 || size > 4) + return 0; + for (int i = 0; i < size; i++) { + int shift = littleEndian ? (8 * i) : (8 * (size - 1 - i)); + unsigned char byte = (value >> shift) & 0xFF; + WriteValueAtHardwareAddress(address + i, byte, 1, 1); // single byte, no endian ambiguity + } + return 1; +} + +int FBHeadlessIsAddressValid(unsigned int address) +{ + return IsHardwareAddressValid(address) ? 1 : 0; +} + +// --- Achievements memory map --- +// +// RetroAchievements sets are authored against the flat main-RAM buffer FBNeo's +// libretro port exposes (see CheevosInit / StateGetMainRamAcb in retro_memory.cpp), +// NOT the CPU hardware address space. Find the same RAM area(s) by driver hardware +// type and expose them at the same RA base addresses the libretro port uses. + +#define FBH_MAX_RA_REGIONS 10 +typedef struct { unsigned int start; unsigned int len; unsigned char *ptr; } FBHRaRegion; +static FBHRaRegion s_raRegions[FBH_MAX_RA_REGIONS]; +static int s_raRegionCount = 0; +static int s_raBigEndian = 0; // main CPU multibyte order (68K etc. = big-endian) + +static void fbh_ra_add(unsigned int start, unsigned int len, void *ptr) +{ + if (s_raRegionCount >= FBH_MAX_RA_REGIONS || ptr == NULL || len == 0) + return; + s_raRegions[s_raRegionCount].start = start; + s_raRegions[s_raRegionCount].len = len; + s_raRegions[s_raRegionCount].ptr = (unsigned char *)ptr; + s_raRegionCount++; +} + +static INT32 __cdecl fbh_ra_acb(struct BurnArea *pba) +{ + if (!pba->szName) + return 0; + + switch (BurnDrvGetHardwareCode() & HARDWARE_PUBLIC_MASK) { + case HARDWARE_CAPCOM_CPS1: + case HARDWARE_CAPCOM_CPS1_QSOUND: + case HARDWARE_CAPCOM_CPS1_GENERIC: + case HARDWARE_CAPCOM_CPSCHANGER: + case HARDWARE_CAPCOM_CPS2: + if (strcmp(pba->szName, "CpsRamFF") == 0) fbh_ra_add(0, pba->nLen, pba->Data); + return 0; + case HARDWARE_CAPCOM_CPS3: + if (strcmp(pba->szName, "Main RAM") == 0) fbh_ra_add(0, pba->nLen, pba->Data); + return 0; + case HARDWARE_SNK_NEOCD: + if (strcmp(pba->szName, "68K program RAM") == 0) fbh_ra_add(0x00000000, pba->nLen, pba->Data); + if (strcmp(pba->szName, "Memory card") == 0) fbh_ra_add(0x00800000, pba->nLen, pba->Data); + return 0; + case HARDWARE_SNK_NEOGEO: + case HARDWARE_IGS_PGM: + if (strcmp(pba->szName, "68K RAM") == 0) fbh_ra_add(0, pba->nLen, pba->Data); + return 0; + case HARDWARE_PSIKYO: + if (strcmp(pba->szName, "All RAM") == 0 || strcmp(pba->szName, "68K RAM") == 0) fbh_ra_add(0, pba->nLen, pba->Data); + return 0; + case HARDWARE_CAVE_68K_Z80: + case HARDWARE_CAVE_68K_ONLY: + if (strcmp(pba->szName, "RAM") == 0 || strcmp(pba->szName, "68K RAM") == 0) fbh_ra_add(0, pba->nLen, pba->Data); + return 0; + case HARDWARE_SEGA_MEGADRIVE: + if (strcmp(pba->szName, "RAM") == 0) fbh_ra_add(0x00FF0000, pba->nLen, pba->Data); + if (strcmp(pba->szName, "NV RAM") == 0) fbh_ra_add(0x00000000, pba->nLen, pba->Data); + return 0; + case HARDWARE_SEGA_MASTER_SYSTEM: + case HARDWARE_SEGA_GAME_GEAR: + if (strcmp(pba->szName, "sms") == 0) fbh_ra_add(0, pba->nLen, pba->Data); + return 0; + case HARDWARE_NES: + case HARDWARE_FDS: + case HARDWARE_NVS: + if (strcmp(pba->szName, "CPU Ram") == 0) fbh_ra_add(0x00000000, pba->nLen, pba->Data); + if (strcmp(pba->szName, "Work Ram") == 0) fbh_ra_add(0x00006000, pba->nLen, pba->Data); + return 0; + case HARDWARE_SNK_NGP: + case HARDWARE_SNK_NGPC: + if (strcmp(pba->szName, "Main Ram") == 0) fbh_ra_add(0x00004000, pba->nLen, pba->Data); + if (strcmp(pba->szName, "Shared Ram") == 0) fbh_ra_add(0x00007000, pba->nLen, pba->Data); + return 0; + case HARDWARE_TOAPLAN_RAIZING: + if (strcmp(pba->szName, "All Ram") == 0 || strcmp(pba->szName, "All RAM") == 0 || strcmp(pba->szName, "RAM") == 0) fbh_ra_add(0, pba->nLen, pba->Data); + return 0; + default: + // Most other drivers expose main RAM as "All Ram" / "All RAM". + if (strcmp(pba->szName, "All Ram") == 0 || strcmp(pba->szName, "All RAM") == 0) fbh_ra_add(0, pba->nLen, pba->Data); + return 0; + } +} + +// Rebuild the achievement memory map. Call after a driver loads or resets, since +// DrvInit reallocates the RAM buffers (the cached pointers would otherwise dangle). +void FBHeadlessCheevosInit(void) +{ + INT32 nMin = 0; + s_raRegionCount = 0; + s_raBigEndian = 0; + if (!bDrvOkay) + return; + s_raBigEndian = GetMainMemoryBigEndian(); + BurnAcb = fbh_ra_acb; + BurnAreaScan(ACB_FULLSCAN, &nMin); +} + +unsigned int FBHeadlessReadAchievementMemory(unsigned int address, unsigned char *out, unsigned int length) +{ + if (out == NULL) + return 0; + + unsigned int i; + for (i = 0; i < length; i++) { + unsigned int a = address + i; + int found = 0; + for (int r = 0; r < s_raRegionCount; r++) { + if (a >= s_raRegions[r].start && a < s_raRegions[r].start + s_raRegions[r].len) { + out[i] = s_raRegions[r].ptr[a - s_raRegions[r].start]; + found = 1; + break; + } + } + if (!found) + return i; // stop at the first unmapped address + } + return length; +} + +// Write a value into the achievement memory map (used to apply cheats). Bytes land +// at address+i for littleEndian, or MSB-first for big-endian. Returns 1 if anything +// was written. Direct buffer writes — the CPU sees them next frame (plain RAM). +int FBHeadlessWriteAchievementMemory(unsigned int address, unsigned int value, int size, int littleEndian) +{ + if (size < 1 || size > 4) + return 0; + int wrote = 0; + for (int i = 0; i < size; i++) { + unsigned int a = littleEndian ? (address + i) : (address + (size - 1 - i)); + unsigned char byte = (value >> (8 * i)) & 0xFF; + for (int r = 0; r < s_raRegionCount; r++) { + if (a >= s_raRegions[r].start && a < s_raRegions[r].start + s_raRegions[r].len) { + s_raRegions[r].ptr[a - s_raRegions[r].start] = byte; + wrote = 1; + break; + } + } + } + return wrote; +} + +// Enumerate the mapped regions so the host can present cheat-search memory. +int FBHeadlessGetMemoryRegionCount(void) +{ + return s_raRegionCount; +} + +int FBHeadlessGetMemoryRegion(int index, unsigned int *start, unsigned int *length, int *bigEndian) +{ + if (index < 0 || index >= s_raRegionCount) + return 0; + if (start) *start = s_raRegions[index].start; + if (length) *length = s_raRegions[index].len; + if (bigEndian) *bigEndian = s_raBigEndian; + return 1; +} diff --git a/src/burner/headless/headless_misc.cpp b/src/burner/headless/headless_misc.cpp new file mode 100644 index 000000000..9bd2f6b15 --- /dev/null +++ b/src/burner/headless/headless_misc.cpp @@ -0,0 +1,49 @@ +// Headless frontend helpers for FinalBurn Neo. +// Author: Leonardo Kasperavičius +// +// Built originally to support OpenEmu-Silicon +// (https://github.com/OpenEmu-Silicon/OpenEmu-Silicon). +// +// Small frontend functions/globals the shared burner code expects, provided by +// each platform (the headless counterpart of burner/macos/misc.cpp). Subject to +// the FB Neo license (see src/license.txt). + +#include "burner.h" + +// General-purpose debug global referenced by various drivers. +int counter = 0; + +TCHAR* ANSIToTCHAR(const char* pszInString, TCHAR* pszOutString, int /* nOutSize */) +{ + if (pszOutString) { + _tcscpy(pszOutString, pszInString); + return pszOutString; + } + return (TCHAR*)pszInString; +} + +char* TCHARToANSI(const TCHAR* pszInString, char* pszOutString, int /* nOutSize */) +{ + if (pszOutString) { + _tcscpy(pszOutString, pszInString); + return pszOutString; + } + return (char*)pszInString; +} + +TCHAR* AdaptiveEncodingReads(const TCHAR* /* pszFileName */) +{ + return NULL; +} + +bool AppProcessKeyboardInput() +{ + // Input is always live for the headless core (the host gates pause/focus), + // and CinpState() zeroes keyboard-range codes (< 0x4000, which ours are) + // unless this returns true. + return true; +} + +void UpdateMessage(char* /* message */) +{ +} diff --git a/src/burner/headless/tchar.h b/src/burner/headless/tchar.h new file mode 100644 index 000000000..2e388807d --- /dev/null +++ b/src/burner/headless/tchar.h @@ -0,0 +1,72 @@ +#ifndef TCHAR_H +#define TCHAR_H + +// Always available so non-_UNICODE code that still handles wide buffers +// (e.g. burn.cpp zip-name parsing) can call wcslen/wcscpy. +#include + +#ifdef _UNICODE + +#include + +#define __TEXT(q) L##q + +typedef wchar_t TCHAR; +typedef wchar_t _TCHAR; + +#else /* ifndef _UNICODE */ + +#define __TEXT(q) q + +typedef char TCHAR; +typedef char _TCHAR; + +//#define wcslen(void) + +#define _tcslen strlen +#define _tcscpy strcpy +#define _tcsncpy strncpy + +#define _tprintf printf +#define _vstprintf vsprintf +#define _vsntprintf vsnprintf +#define _stprintf sprintf +#define _sntprintf snprintf +#define _ftprintf fprintf +#define _tsprintf sprintf + +#define _tcscmp strcmp +#define _tcsncmp strncmp +#define _tcsicmp strcasecmp +#define _tcsnicmp strncasecmp +#define _tcstol strtol +#define _tcsrchr strrchr +#define _tcsstr strstr + +#define _tcsstr strstr + +#define _fgetts fgets +#define _fputts fputs + +#define _istspace isspace + +#define _tfopen fopen + +#define _stricmp strcmp +#define _strnicmp strncmp + +// Win32 string/char shims the shared code uses on POSIX builds. +#include +#define stricmp strcasecmp +#define strnicmp strncasecmp +#define _totlower(c) tolower((unsigned char)(c)) +#define _totupper(c) toupper((unsigned char)(c)) + +#define dprintf(...) fprintf (stderr, __VA_ARGS__) + +#endif + +#define _TEXT(x) __TEXT(x) +#define _T(x) __TEXT(x) + +#endif diff --git a/src/intf/audio/aud_interface.cpp b/src/intf/audio/aud_interface.cpp index b831bfdf2..a1e59348e 100644 --- a/src/intf/audio/aud_interface.cpp +++ b/src/intf/audio/aud_interface.cpp @@ -22,6 +22,8 @@ static UINT32 nAudActive = 0; extern struct AudOut AudOutWASAPI; #elif defined (BUILD_MACOS) extern struct AudOut AudOutMacOS; +#elif defined (BUILD_HEADLESS) + extern struct AudOut AudOutHeadless; #elif defined (BUILD_SDL) extern struct AudOut AudOutSDL; #elif defined (BUILD_SDL2) @@ -47,6 +49,8 @@ static struct AudOut *pAudOut[]= &AudOutWASAPI, #elif defined (BUILD_MACOS) &AudOutMacOS, +#elif defined (BUILD_HEADLESS) + &AudOutHeadless, #elif defined (BUILD_SDL) &AudOutSDL, #elif defined (BUILD_SDL2) diff --git a/src/intf/audio/headless/aud_headless.cpp b/src/intf/audio/headless/aud_headless.cpp new file mode 100644 index 000000000..a42593168 --- /dev/null +++ b/src/intf/audio/headless/aud_headless.cpp @@ -0,0 +1,49 @@ +// Headless audio output for FinalBurn Neo. +// Author: Leonardo Kasperavičius +// +// Built originally to support OpenEmu-Silicon +// (https://github.com/OpenEmu-Silicon/OpenEmu-Silicon). +// +// The frontend drives sound directly (it points pBurnSoundOut at its own buffer +// and reads it after BurnDrvFrame), so this plugin only satisfies the AudOut +// interface. Subject to the FB Neo license (see src/license.txt). + +#include "burner.h" + +static int HeadlessSoundBlank() { return 0; } +static int HeadlessSoundCheck() { return 0; } + +static int HeadlessSoundInit() +{ + if (nAudSampleRate[0] <= 0) + return 1; + bAudOkay = 1; + return 0; +} + +static int HeadlessSetCallback(int (* /* pCallback */)(int)) { return 0; } +static int HeadlessSoundPlay() { bAudPlaying = 1; return 0; } +static int HeadlessSoundStop() { bAudPlaying = 0; return 0; } + +static int HeadlessSoundExit() +{ + bAudOkay = 0; + bAudPlaying = 0; + return 0; +} + +static int HeadlessSoundSetVolume() { return 0; } +static int HeadlessSoundGetSettings(InterfaceInfo *) { return 0; } + +struct AudOut AudOutHeadless = { + HeadlessSoundBlank, + HeadlessSoundCheck, + HeadlessSoundInit, + HeadlessSetCallback, + HeadlessSoundPlay, + HeadlessSoundStop, + HeadlessSoundExit, + HeadlessSoundSetVolume, + HeadlessSoundGetSettings, + _T("Headless audio output"), +}; diff --git a/src/intf/input/headless/inp_headless.cpp b/src/intf/input/headless/inp_headless.cpp new file mode 100644 index 000000000..02d2caf46 --- /dev/null +++ b/src/intf/input/headless/inp_headless.cpp @@ -0,0 +1,63 @@ +// Headless input for FinalBurn Neo. +// Author: Leonardo Kasperavičius +// +// Built originally to support OpenEmu-Silicon +// (https://github.com/OpenEmu-Silicon/OpenEmu-Silicon). +// +// Reads a digital switch state table populated by the host via +// FBHeadlessSetInputState(). InputMake() calls ReadSwitch() while building each +// frame's GameInp. Subject to the FB Neo license (see src/license.txt). + +#include "burner.h" +#include "fbheadless.h" + +// Indexed by FBNeo input switch code (masked); 0 = released, non-zero = pressed. +#define HEADLESS_INPUT_TABLE_SIZE 0x200 +static unsigned char inputState[HEADLESS_INPUT_TABLE_SIZE]; + +static int HeadlessInpInit() +{ + memset(inputState, 0, sizeof(inputState)); + bInputOkay = 1; + return 0; +} + +static int HeadlessInpExit() +{ + bInputOkay = 0; + return 0; +} + +static int HeadlessInpSetCooperativeLevel(bool /* bExclusive */, bool /* bForeground */) { return 0; } +static int HeadlessInpNewFrame() { return 0; } + +static int HeadlessInpReadSwitch(int nCode) +{ + return inputState[nCode & (HEADLESS_INPUT_TABLE_SIZE - 1)] ? 1 : 0; +} + +static int HeadlessInpReadJoyAxis(int /* i */, int /* nAxis */) { return 0; } +static int HeadlessInpReadMouseAxis(int /* i */, int /* nAxis */) { return 0; } +static int HeadlessInpFind(bool /* createBaseline */) { return -1; } + +static int HeadlessInpGetControlName(int /* nCode */, TCHAR * /* dev */, TCHAR * /* ctrl */) { return 0; } +static int HeadlessInpGetSettings(InterfaceInfo *) { return 0; } + +struct InputInOut InputInOutHeadless = { + HeadlessInpInit, + HeadlessInpExit, + HeadlessInpSetCooperativeLevel, + HeadlessInpNewFrame, + HeadlessInpReadSwitch, + HeadlessInpReadJoyAxis, + HeadlessInpReadMouseAxis, + HeadlessInpFind, + HeadlessInpGetControlName, + HeadlessInpGetSettings, + _T("Headless input"), +}; + +extern "C" void FBHeadlessSetInputState(int code, int value) +{ + inputState[code & (HEADLESS_INPUT_TABLE_SIZE - 1)] = value ? 1 : 0; +} diff --git a/src/intf/input/inp_interface.cpp b/src/intf/input/inp_interface.cpp index dc6159b27..212aac546 100644 --- a/src/intf/input/inp_interface.cpp +++ b/src/intf/input/inp_interface.cpp @@ -16,6 +16,8 @@ static bool bCinpOkay; extern struct InputInOut InputInOutXInput; #elif defined (BUILD_MACOS) extern struct InputInOut InputInOutMacOS; +#elif defined (BUILD_HEADLESS) + extern struct InputInOut InputInOutHeadless; #elif defined (BUILD_SDL) extern struct InputInOut InputInOutSDL; #elif defined (BUILD_SDL2) @@ -33,6 +35,8 @@ static struct InputInOut *pInputInOut[]= &InputInOutXInput, #elif defined (BUILD_MACOS) &InputInOutMacOS, +#elif defined (BUILD_HEADLESS) + &InputInOutHeadless, #elif defined (BUILD_SDL2) &InputInOutSDL2, #elif defined (BUILD_SDL) diff --git a/src/intf/video/headless/vid_headless.cpp b/src/intf/video/headless/vid_headless.cpp new file mode 100644 index 000000000..3d7d6114c --- /dev/null +++ b/src/intf/video/headless/vid_headless.cpp @@ -0,0 +1,192 @@ +// 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; } diff --git a/src/intf/video/vid_interface.cpp b/src/intf/video/vid_interface.cpp index b6209d5fe..2c40137ca 100644 --- a/src/intf/video/vid_interface.cpp +++ b/src/intf/video/vid_interface.cpp @@ -15,6 +15,8 @@ extern struct VidOut VidOutD3D11; #elif defined (BUILD_MACOS) extern struct VidOut VidOutMacOS; +#elif defined (BUILD_HEADLESS) + extern struct VidOut VidOutHeadless; #elif defined (BUILD_PI) extern struct VidOut VidOutPi; #elif defined (BUILD_SDL2) @@ -39,6 +41,8 @@ static struct VidOut *pVidOut[] = { &VidOutD3D11, #elif defined (BUILD_MACOS) &VidOutMacOS, +#elif defined (BUILD_HEADLESS) + &VidOutHeadless, #elif defined (BUILD_PI) &VidOutPi, #elif defined (BUILD_SDL2)