# HG changeset patch # User Michael Pavone # Date 1555310282 25200 # Node ID 49f65d240299f043f86719a68ac3de6cd874796e # Parent 0c6d07f9134619173e1ef5ba53f4efda87ecc17f# Parent 78abbabfd58d0c0f8417b9e40671d7a6c2abe015 Merge from default diff -r 0c6d07f91346 -r 49f65d240299 .hgtags --- a/.hgtags Thu Mar 14 23:40:50 2019 -0700 +++ b/.hgtags Sun Apr 14 23:38:02 2019 -0700 @@ -10,3 +10,4 @@ 3d48cb0c28be9045866e00795b698086018b825f v0.5.1 ef50c9affe6a7c86398f2c36eb5439a559808108 v0.6.0 357b4951d9b2d1999e4c2765ee53e946aaab864d v0.6.1 +8aeac7bd9fa7d9d978c99ec07e9a68989a12e453 v0.6.2 diff -r 0c6d07f91346 -r 49f65d240299 Android.mk --- a/Android.mk Thu Mar 14 23:40:50 2019 -0700 +++ b/Android.mk Sun Apr 14 23:38:02 2019 -0700 @@ -8,14 +8,21 @@ LOCAL_C_INCLUDES := $(LOCAL_PATH)/$(SDL_PATH)/include -LOCAL_CFLAGS += -std=gnu99 -DX86_32 -DDISABLE_OPENGL +LOCAL_CFLAGS += -std=gnu99 -DX86_32 -DUSE_GLES # Add your application source files here... LOCAL_SRC_FILES := $(SDL_PATH)/src/main/android/SDL_android_main.c \ 68kinst.c debug.c gst.c psg.c z80_to_x86.c backend.c io.c render_sdl.c \ tern.c backend_x86.c gdb_remote.c m68k_core.c romdb.c m68k_core_x86.c \ util.c wave.c blastem.c gen.c mem.c vdp.c ym2612.c config.c gen_x86.c \ - terminal.c z80inst.c menu.c arena.c + terminal.c z80inst.c menu.c arena.c zlib/adler32.c zlib/compress.c \ + zlib/crc32.c zlib/deflate.c zlib/gzclose.c zlib/gzlib.c zlib/gzread.c \ + zlib/gzwrite.c zlib/infback.c zlib/inffast.c zlib/inflate.c \ + zlib/inftrees.c zlib/trees.c zlib/uncompr.c zlib/zutil.c \ + nuklear_ui/font_android.c nuklear_ui/blastem_nuklear.c nuklear_ui/sfnt.c \ + ppm.c controller_info.c png.c system.c genesis.c sms.c serialize.c \ + saves.c hash.c xband.c zip.c bindings.c jcart.c paths.c megawifi.c \ + nor.c i2c.c sega_mapper.c realtec.c multi_game.c net.c LOCAL_SHARED_LIBRARIES := SDL2 diff -r 0c6d07f91346 -r 49f65d240299 CHANGELOG --- a/CHANGELOG Thu Mar 14 23:40:50 2019 -0700 +++ b/CHANGELOG Sun Apr 14 23:38:02 2019 -0700 @@ -1,3 +1,33 @@ +0.6.2 +----- +*New Features* + + - Zipped and gzipped SMD ROMs are now supported + - Gain control for overall volume and FM/PSG invidually + +*Accuracy/Completeness Improvements* + + - Fixed timing of a few instructions in Z80 core + - Added optional emulation of YM2612 imperfections (aka "ladder effect") + - Fixed some unintentional extra precision in some FM LFO calculations + - Added a 1 sample delay in some FM operator results when used as modulators to match hardware + +*Bugfixes* + + - Fixed regression in NBA JAM TE and possibly other 32MBit Acclaim mapper titles + - Added code to handle controllers that have their d-pads mapped as buttons or axes + - Removed some problematic SDL2 game controller mappings + - Fixed crash that occurred when releasing mouse too clickly when loading a ROM + - Fixed SMD ROM support + - Fixed handling of audio contexts with more or less than 2 channels + - Fixed off-by-one error in IO device selection UI + - Fixed regression in GDB remote debugging support on Linux and OS X + +*Other Changes* + + - MegaWiFi hardware can now be enabled by a header string (still gated by config) + - Tweaked the style of checkboxes in the Nuklear UI to hopefully make the on/off state more clear + 0.6.1 ----- *Bugfixes* @@ -40,7 +70,7 @@ - Added support for Open GL ES in addition to the existing desktop GL support - Some small optimizations - Added ROM DB entry for Squirrel King to support it's copy protection - - Added support for float32 audio output (fixes an issue with defautl SDL2 driver in Windows when using more recent SDL2 versions) + - Added support for float32 audio output (fixes an issue with default SDL2 driver in Windows when using more recent SDL2 versions) 0.5.1 ----- diff -r 0c6d07f91346 -r 49f65d240299 Makefile --- a/Makefile Thu Mar 14 23:40:50 2019 -0700 +++ b/Makefile Sun Apr 14 23:38:02 2019 -0700 @@ -1,28 +1,43 @@ +#disable built-in rules +.SUFFIXES : + ifndef OS OS:=$(shell uname -s) endif FIXUP:=true +BUNDLED_LIBZ:=zlib/adler32.o zlib/compress.o zlib/crc32.o zlib/deflate.o zlib/gzclose.o zlib/gzlib.o zlib/gzread.o\ + zlib/gzwrite.o zlib/infback.o zlib/inffast.o zlib/inflate.o zlib/inftrees.o zlib/trees.o zlib/uncompr.o zlib/zutil.o + ifeq ($(OS),Windows) -ifndef SDL2_PREFIX -SDL2_PREFIX:="sdl/i686-w64-mingw32" -endif -ifndef GLEW_PREFIX + GLEW_PREFIX:=glew -endif -ifndef GLEW32S_LIB -GLEW32S_LIB:=$(GLEW_PREFIX)/lib/Release/Win32/glew32s.lib -endif - MEM:=mem_win.o TERMINAL:=terminal_win.o FONT:=nuklear_ui/font_win.o NET:=net_win.o EXE:=.exe +SO:=dll +CPU:=i686 +ifeq ($(CPU),i686) CC:=i686-w64-mingw32-gcc-win32 -CFLAGS:=-std=gnu99 -Wreturn-type -Werror=return-type -Werror=implicit-function-declaration -I"$(SDL2_PREFIX)/include/SDL2" -I"$(GLEW_PREFIX)/include" -DGLEW_STATIC -LDFLAGS:= $(GLEW32S_LIB) -L"$(SDL2_PREFIX)/lib" -lm -lmingw32 -lSDL2main -lSDL2 -lws2_32 -lopengl32 -lglu32 -mwindows -CPU:=i686 +WINDRES:=i686-w64-mingw32-windres +GLUDIR:=Win32 +SDL2_PREFIX:="sdl/i686-w64-mingw32" +else +CC:=x86_64-w64-mingw32-gcc-win32 +WINDRES:=x86_64-w64-mingw32-windres +SDL2_PREFIX:="sdl/x86_64-w64-mingw32" +GLUDIR:=x64 +endif +GLEW32S_LIB:=$(GLEW_PREFIX)/lib/Release/$(GLUDIR)/glew32s.lib +CFLAGS:=-std=gnu99 -Wreturn-type -Werror=return-type -Werror=implicit-function-declaration +LDFLAGS:=-lm -lmingw32 -lws2_32 -mwindows +ifneq ($(MAKECMDGOALS),libblastem.dll) +CFLAGS+= -I"$(SDL2_PREFIX)/include/SDL2" -I"$(GLEW_PREFIX)/include" -DGLEW_STATIC +LDFLAGS+= $(GLEW32S_LIB) -L"$(SDL2_PREFIX)/lib" -lSDL2main -lSDL2 -lopengl32 -lglu32 +endif +LIBZOBJS=$(BUNDLED_LIBZ) else @@ -37,7 +52,9 @@ ifeq ($(OS),Darwin) LIBS=sdl2 glew FONT:=nuklear_ui/font_mac.o +SO:=dylib else +SO:=so ifdef USE_FBDEV LIBS=alsa @@ -64,8 +81,7 @@ LIBS+= zlib LIBZOBJS= else -LIBZOBJS=zlib/adler32.o zlib/compress.o zlib/crc32.o zlib/deflate.o zlib/gzclose.o zlib/gzlib.o zlib/gzread.o\ - zlib/gzwrite.o zlib/infback.o zlib/inffast.o zlib/inflate.o zlib/inftrees.o zlib/trees.o zlib/uncompr.o zlib/zutil.o +LIBZOBJS=$(BUNDLED_LIBZ) endif ifeq ($(OS),Darwin) @@ -96,7 +112,7 @@ endif #Darwin else -ifeq ($(MAKECMDGOALS),libblastem.so) +ifeq ($(MAKECMDGOALS),libblastem.$(SO)) LDFLAGS:=-lm else CFLAGS:=$(shell pkg-config --cflags-only-I $(LIBS)) $(CFLAGS) @@ -138,7 +154,6 @@ endif ifdef NOGL CFLAGS+= -DDISABLE_OPENGL -NONUKLEAR:=1 endif ifdef M68030 @@ -204,9 +219,9 @@ realtec.o i2c.o nor.o sega_mapper.o multi_game.o megawifi.o $(NET) serialize.o $(TERMINAL) $(CONFIGOBJS) \ $(M68KOBJS) $(TRANSOBJS) $(AUDIOOBJS) saves.o zip.o bindings.o jcart.o -LIBOBJS=libblastem.o system.o genesis.o debug.o gdb_remote.o vdp.o io.o romdb.o hash.o menu.o xband.o realtec.o \ +LIBOBJS=libblastem.o system.o genesis.o debug.o gdb_remote.o vdp.o io.o romdb.o hash.o xband.o realtec.o \ i2c.o nor.o sega_mapper.o multi_game.o megawifi.o $(NET) serialize.o $(TERMINAL) $(CONFIGOBJS) gst.o \ - $(M68KOBJS) $(TRANSOBJS) $(AUDIOOBJS) saves.o jcart.o + $(M68KOBJS) $(TRANSOBJS) $(AUDIOOBJS) saves.o jcart.o rom.db.o ifdef NONUKLEAR CFLAGS+= -DDISABLE_NUKLEAR @@ -250,13 +265,13 @@ ALL+= termhelper endif -ifeq ($(MAKECMDGOALS),libblastem.so) +ifeq ($(MAKECMDGOALS),libblastem.$(SO)) CFLAGS+= -fpic -DIS_LIB endif all : $(ALL) -libblastem.so : $(LIBOBJS) +libblastem.$(SO) : $(LIBOBJS) $(CC) -shared -o $@ $^ $(LDFLAGS) blastem$(EXE) : $(MAINOBJS) @@ -266,6 +281,9 @@ blastjag$(EXE) : jaguar.o jag_video.o $(RENDEROBJS) serialize.o $(M68KOBJS) $(TRANSOBJS) $(CONFIGOBJS) $(CC) -o $@ $^ $(LDFLAGS) +termhelper : termhelper.o + $(CC) -o $@ $^ $(LDFLAGS) + dis$(EXE) : dis.o 68kinst.o tern.o vos_program_module.o $(CC) -o $@ $^ $(OPT) @@ -328,6 +346,9 @@ %.c : %.cpu cpu_dsl.py ./cpu_dsl.py -d goto $< > $@ +%.db.c : %.db + sed $< -e 's/"/\\"/g' -e 's/^\(.*\)$$/"\1\\n"/' -e'1s/^\(.*\)$$/const char $(shell echo $< | tr '.' '_')_data[] = \1/' -e '$$s/^\(.*\)$$/\1;/' > $@ + %.o : %.S $(CC) -c -o $@ $< @@ -349,7 +370,7 @@ %.bin : %.sz8 vasmz80_mot -Fbin -spaces -o $@ $< res.o : blastem.rc - i686-w64-mingw32-windres blastem.rc res.o + $(WINDRES) blastem.rc res.o arrow.tiles : arrow.png cursor.tiles : cursor.png diff -r 0c6d07f91346 -r 49f65d240299 README --- a/README Thu Mar 14 23:40:50 2019 -0700 +++ b/README Sun Apr 14 23:38:02 2019 -0700 @@ -281,6 +281,22 @@ at least some Genesis/Megadrive models. Other models reportedly use an even lower value. +"gain" specifies the gain in decibels to be applied to the overall output. + +"fm_gain" specifies the gain to be applied to the emulated FM output before +mixing with the PSG. + +"psg_gain" specifies the gain to be applied to the emulated PSG output before +mixing with the FM chip. + +"fm_dac" controls the characteristics of the DAC in the emulated FM chip. If +this is set to "linear", then the DAC will have precise linear output similar +to the integrated YM3438 in later Gen/MD consoles. If it is set to "zero_offset", +there will be a larger gap between -1 and 0. This is commonly referred to as the +"ladder effect". This will also cause "leakage" on channels that are muted or +panned to one side in a similar manner to a discrete YM2612. + + Clocks ------ @@ -495,6 +511,15 @@ Eke-Eke - Eke-Eke wrote a great document on the use of I2C EEPROM in Genesis games and also left some useful very helpful comments about problematic games in Genesis Plus GX + +Sauraen - Sauraen has analyzed the YM2203 and YM2612 dies and written + a VHDL operator implementation. These have been useful in + improving the accuracy of my YM2612 core. + +Alexey Khokholov - Alexey (aka Nuke.YKT) has analyzed the YM3438 die and written + a fairly direct C implementation from that analysis. This + has been a useful reference for verifying and improving my + YM2612 core. Bart Trzynadlowski - His documents on the Genecyst save-state format and the mapper used in Super Street Fighter 2 were definitely @@ -530,5 +555,6 @@ modify the program as long as you follow the terms of the license. See the file COPYING for full license details. -Binary releases of BlastEm are packaged with GLEW and SDL2 which have thier own -licenses. See GLEW-LICENSE and SDL-LICENSE for details. +Binary releases of BlastEm are packaged with GLEW, SDL2 and zlib which have their +own licenses. See GLEW-LICENSE and SDL-LICENSE for details. For zlib license +information, please see zlib.h in the source code release. diff -r 0c6d07f91346 -r 49f65d240299 android/assets/default.cfg --- a/android/assets/default.cfg Thu Mar 14 23:40:50 2019 -0700 +++ b/android/assets/default.cfg Sun Apr 14 23:38:02 2019 -0700 @@ -14,9 +14,14 @@ f gamepads.1.mode enter gamepads.1.start + r ui.release_mouse [ ui.vdp_debug_mode - ] ui.vdp_debug_pal u ui.enter_debugger + p ui.screenshot + b ui.plane_debug + v ui.vram_debug + c ui.cram_debug + n ui.compositing_debug esc ui.exit ` ui.save_state 0 ui.set_speed.0 @@ -29,6 +34,11 @@ 7 ui.set_speed.7 = ui.next_speed - ui.prev_speed + f11 ui.toggle_fullscreen + tab ui.soft_reset + f5 ui.reload + z ui.sms_pause + rctrl ui.toggle_keyboard_captured select gamepads.1.c @@ -36,44 +46,230 @@ back ui.exit } pads { - 0 { + default { dpads { 0 { - up gamepads.1.up - down gamepads.1.down - left gamepads.1.left - right gamepads.1.right + up gamepads.n.up + down gamepads.n.down + left gamepads.n.left + right gamepads.n.right } } buttons { - 0 gamepads.1.a - 1 gamepads.1.b - 2 gamepads.1.c - 3 gamepads.1.x - 4 gamepads.1.y - 5 gamepads.1.z - 6 gamepads.1.mode - 7 gamepads.1.start + a gamepads.n.a + b gamepads.n.b + rightshoulder gamepads.n.c + x gamepads.n.x + y gamepads.n.y + leftshoulder gamepads.n.z + back gamepads.n.mode + start gamepads.n.start + guide ui.exit + leftstick ui.save_state + } + axes { + lefty.positive gamepads.n.down + lefty.negative gamepads.n.up + leftx.positive gamepads.n.right + leftx.negative gamepads.n.left + lefttrigger ui.prev_speed + righttrigger ui.next_speed + } + } + ps4_6b_right { + axes { + lefttrigger ui.next_speed + leftx.negative gamepads.n.up + leftx.positive gamepads.n.down + lefty.negative gamepads.n.left + lefty.positive gamepads.n.right + righttrigger gamepads.n.c + } + buttons { + a gamepads.n.a + b gamepads.n.b + back ui.sms_pause + guide ui.exit + leftshoulder gamepads.n.mode + leftstick ui.save_state + rightshoulder gamepads.n.z + rightstick ui.prev_speed + start gamepads.n.start + x gamepads.n.x + y gamepads.n.y + } + dpads { + 0 { + down gamepads.n.down + left gamepads.n.left + right gamepads.n.right + up gamepads.n.up + } } } - 1 { + ps3_6b_right { + axes { + lefttrigger ui.next_speed + leftx.negative gamepads.n.up + leftx.positive gamepads.n.down + lefty.negative gamepads.n.left + lefty.positive gamepads.n.right + righttrigger gamepads.n.c + } + buttons { + a gamepads.n.a + b gamepads.n.b + back ui.sms_pause + guide ui.exit + leftshoulder gamepads.n.mode + leftstick ui.save_state + rightshoulder gamepads.n.z + rightstick ui.prev_speed + start gamepads.n.start + x gamepads.n.x + y gamepads.n.y + } + dpads { + 0 { + down gamepads.n.down + left gamepads.n.left + right gamepads.n.right + up gamepads.n.up + } + } + } + xbox_360_6b_right { + axes { + lefttrigger ui.next_speed + leftx.negative gamepads.n.up + leftx.positive gamepads.n.down + lefty.negative gamepads.n.left + lefty.positive gamepads.n.right + righttrigger gamepads.n.c + } + buttons { + a gamepads.n.a + b gamepads.n.b + back ui.sms_pause + guide ui.exit + leftshoulder gamepads.n.mode + leftstick ui.save_state + rightshoulder gamepads.n.z + rightstick ui.prev_speed + start gamepads.n.start + x gamepads.n.x + y gamepads.n.y + } dpads { 0 { - up gamepads.2.up - down gamepads.2.down - left gamepads.2.left - right gamepads.2.right + down gamepads.n.down + left gamepads.n.left + right gamepads.n.right + up gamepads.n.up } } + } + xbone_6b_right { + axes { + lefttrigger ui.next_speed + leftx.negative gamepads.n.up + leftx.positive gamepads.n.down + lefty.negative gamepads.n.left + lefty.positive gamepads.n.right + righttrigger gamepads.n.c + } + buttons { + a gamepads.n.a + b gamepads.n.b + back ui.sms_pause + guide ui.exit + leftshoulder gamepads.n.mode + leftstick ui.save_state + rightshoulder gamepads.n.z + rightstick ui.prev_speed + start gamepads.n.start + x gamepads.n.x + y gamepads.n.y + } + dpads { + 0 { + down gamepads.n.down + left gamepads.n.left + right gamepads.n.right + up gamepads.n.up + } + } + } + genesis_6b_bumpers { + axes { + lefttrigger ui.exit + righttrigger gamepads.n.mode + } buttons { - 0 gamepads.2.a - 1 gamepads.2.b - 2 gamepads.2.c - 3 gamepads.2.x - 4 gamepads.2.y - 5 gamepads.2.z - 6 gamepads.2.mode - 7 gamepads.2.start + a gamepads.n.a + b gamepads.n.b + back ui.sms_pause + guide ui.exit + leftshoulder gamepads.n.z + rightshoulder gamepads.n.c + start gamepads.n.start + x gamepads.n.x + y gamepads.n.y + } + dpads { + 0 { + down gamepads.n.down + left gamepads.n.left + right gamepads.n.right + up gamepads.n.up + } + } + } + saturn_6b_bumpers { + axes { + lefttrigger ui.exit + righttrigger gamepads.n.mode + } + buttons { + a gamepads.n.a + b gamepads.n.b + back ui.sms_pause + guide ui.exit + leftshoulder gamepads.n.z + rightshoulder gamepads.n.c + start gamepads.n.start + x gamepads.n.x + y gamepads.n.y + } + dpads { + 0 { + down gamepads.n.down + left gamepads.n.left + right gamepads.n.right + up gamepads.n.up + } + } + } + } + mice { + 0 { + motion mouse.1.motion + buttons { + 1 mouse.1.left + 2 mouse.1.middle + 3 mouse.1.right + 4 mouse.1.start + } + } + #having the second host mouse also mapped to the first emulated + #mouse is useful for laptop users with an external mouse + 1 { + motion mouse.1.motion + buttons { + 1 mouse.1.left + 2 mouse.1.middle + 3 mouse.1.right + 4 mouse.1.start } } } @@ -87,19 +283,69 @@ } video { + #special value "stretch" will cause aspect to match window aspect ratio + aspect 4:3 width 640 + #height is normally calculated automatically from width using the aspect setting + #if you would like to set it explicitly, uncomment the line below + #height 480 vertex_shader default.v.glsl fragment_shader default.f.glsl + scanlines off + vsync off + fullscreen off + #setting gl to off, will force use of the SDL2 fallback renderer + #this is useful for those running on machines with Open GL 2.0 unavailable + #so the warning doesn't display on startup + gl on + #scaling can be linear (for linear interpolation) or nearest (for nearest neighbor) + scaling linear + ntsc { + overscan { + #these values will result in square pixels in H40 mode + top 2 + bottom 1 + #if you want to completely hide the border instead + #comment out those two lines and uncomment these + #top 11 + #bottom 8 + + #these values will completely hide the horizontal border + left 13 + right 14 + } + } + pal { + overscan { + #these values will produce the same size border in V30 mode + #as the default NTSC settings will produce in V24 mode + #this results in a slightly vertically squished picture + #which is probably approximately correct on a properly calibrated TV + top 21 + bottom 17 + #for square pixels and zero border in V30 mode + #coment out those two lines and uncomment these + #top 30 + #bottom 24 + + #these values will completely hide the horizontal border + left 13 + right 14 + } + } } audio { rate 48000 buffer 512 + lowpass_cutoff 3390 } clocks { + m68k_divider 7 max_cycles 3420 speeds { + 0 100 1 150 2 200 3 300 @@ -111,8 +357,39 @@ } ui { + #specifies the ROM that implements the Menu UI rom menu.bin + #starting path for ROM browsing, accepts special variables $HOME, $EXEDIR + #and variables defined in the OS environment + initial_path $HOME + #if this is set to on, then the menu will remember the last path when visited + #if it's set to off, initial_path will always be used on startup + remember_path on + #path for storing internal screenshots, accepts the same variables as initial_path + screenshot_path $HOME + #see strftime for the format specifiers valid in screenshot_template + screenshot_template blastem_%Y%m%d_%H%M%S.png + #path template for saving SRAM, EEPROM and savestates + #accepts special variables $HOME, $EXEDIR, $USERDATA, $ROMNAME + save_path $USERDATA/blastem/$ROMNAME + #space delimited list of file extensions to filter against in menu + extensions bin gen md smd sms gg zip gz + #specifies the preferred save-state format, set to gst for Genecyst compatible states + state_format native } -default_region U +system { + #controls how the emulated system is synced to the host + #video provides the smoothest experience when the host and emulated system have similar refresh rates + #audio provides lower audio latency, especially when there is a refresh rate mismatch + sync_source audio + #set this to random to debug initialization bugs + ram_init zero + default_region U + #controls whether MegaWiFi support is enabled or not + #MegaWiFi allows ROMs to make connections to the internet + #so it should only be enabled for ROMs you trust + megawifi off +} + diff -r 0c6d07f91346 -r 49f65d240299 android/assets/images --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/android/assets/images Sun Apr 14 23:38:02 2019 -0700 @@ -0,0 +1,1 @@ +../../images \ No newline at end of file diff -r 0c6d07f91346 -r 49f65d240299 android/assets/shaders --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/android/assets/shaders Sun Apr 14 23:38:02 2019 -0700 @@ -0,0 +1,1 @@ +../../shaders \ No newline at end of file diff -r 0c6d07f91346 -r 49f65d240299 android/jni/Application.mk --- a/android/jni/Application.mk Thu Mar 14 23:40:50 2019 -0700 +++ b/android/jni/Application.mk Sun Apr 14 23:38:02 2019 -0700 @@ -4,3 +4,5 @@ # APP_STL := stlport_static APP_ABI := x86 +APP_PLATFORM := android-16 +APP_OPTIM := release diff -r 0c6d07f91346 -r 49f65d240299 android/src/org/libsdl/app/SDL.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/android/src/org/libsdl/app/SDL.java Sun Apr 14 23:38:02 2019 -0700 @@ -0,0 +1,37 @@ +package org.libsdl.app; + +import android.content.Context; + +/** + SDL library initialization +*/ +public class SDL { + + // This function should be called first and sets up the native code + // so it can call into the Java classes + public static void setupJNI() { + SDLActivity.nativeSetupJNI(); + SDLAudioManager.nativeSetupJNI(); + SDLControllerManager.nativeSetupJNI(); + } + + // This function should be called each time the activity is started + public static void initialize() { + setContext(null); + + SDLActivity.initialize(); + SDLAudioManager.initialize(); + SDLControllerManager.initialize(); + } + + // This function stores the current activity (SDL or not) + public static void setContext(Context context) { + mContext = context; + } + + public static Context getContext() { + return mContext; + } + + protected static Context mContext; +} diff -r 0c6d07f91346 -r 49f65d240299 android/src/org/libsdl/app/SDLActivity.java --- a/android/src/org/libsdl/app/SDLActivity.java Thu Mar 14 23:40:50 2019 -0700 +++ b/android/src/org/libsdl/app/SDLActivity.java Sun Apr 14 23:38:02 2019 -0700 @@ -1,25 +1,30 @@ package org.libsdl.app; -import java.util.ArrayList; +import java.io.IOException; +import java.io.InputStream; import java.util.Arrays; -import java.util.Collections; -import java.util.Comparator; -import java.util.List; +import java.lang.reflect.Method; +import java.util.Objects; import android.app.*; import android.content.*; +import android.text.InputType; import android.view.*; import android.view.inputmethod.BaseInputConnection; import android.view.inputmethod.EditorInfo; import android.view.inputmethod.InputConnection; import android.view.inputmethod.InputMethodManager; -import android.widget.AbsoluteLayout; +import android.widget.RelativeLayout; +import android.widget.Button; +import android.widget.LinearLayout; +import android.widget.TextView; import android.os.*; import android.util.Log; +import android.util.SparseArray; import android.graphics.*; -import android.media.*; +import android.graphics.drawable.Drawable; import android.hardware.*; - +import android.content.pm.ActivityInfo; /** SDL Activity @@ -27,34 +32,96 @@ public class SDLActivity extends Activity { private static final String TAG = "SDL"; - // Keep track of the paused state - public static boolean mIsPaused, mIsSurfaceReady, mHasFocus; + public static boolean mIsResumedCalled, mIsSurfaceReady, mHasFocus; + + // Handle the state of the native layer + public enum NativeState { + INIT, RESUMED, PAUSED + } + + public static NativeState mNextNativeState; + public static NativeState mCurrentNativeState; + public static boolean mExitCalledFromJava; + /** If shared libraries (e.g. SDL or the native application) could not be loaded. */ + public static boolean mBrokenLibraries; + + // If we want to separate mouse and touch events. + // This is only toggled in native code when a hint is set! + public static boolean mSeparateMouseAndTouch; + // Main components protected static SDLActivity mSingleton; protected static SDLSurface mSurface; protected static View mTextEdit; + protected static boolean mScreenKeyboardShown; protected static ViewGroup mLayout; - protected static SDLJoystickHandler mJoystickHandler; + protected static SDLClipboardHandler mClipboardHandler; + // This is what SDL runs in. It invokes SDL_main(), eventually protected static Thread mSDLThread; - - // Audio - protected static AudioTrack mAudioTrack; + + /** + * This method returns the name of the shared object with the application entry point + * It can be overridden by derived classes. + */ + protected String getMainSharedObject() { + String library; + String[] libraries = SDLActivity.mSingleton.getLibraries(); + if (libraries.length > 0) { + library = "lib" + libraries[libraries.length - 1] + ".so"; + } else { + library = "libmain.so"; + } + return library; + } + + /** + * This method returns the name of the application entry point + * It can be overridden by derived classes. + */ + protected String getMainFunction() { + return "SDL_main"; + } + + /** + * This method is called by SDL before loading the native shared libraries. + * It can be overridden to provide names of shared libraries to be loaded. + * The default implementation returns the defaults. It never returns null. + * An array returned by a new implementation must at least contain "SDL2". + * Also keep in mind that the order the libraries are loaded may matter. + * @return names of shared libraries to be loaded (e.g. "SDL2", "main"). + */ + protected String[] getLibraries() { + return new String[] { + "SDL2", + // "SDL2_image", + // "SDL2_mixer", + // "SDL2_net", + // "SDL2_ttf", + "main" + }; + } // Load the .so - static { - System.loadLibrary("SDL2"); - //System.loadLibrary("SDL2_image"); - //System.loadLibrary("SDL2_mixer"); - //System.loadLibrary("SDL2_net"); - //System.loadLibrary("SDL2_ttf"); - System.loadLibrary("main"); + public void loadLibraries() { + for (String lib : getLibraries()) { + System.loadLibrary(lib); + } } - - + + /** + * This method is called by SDL before starting the native application thread. + * It can be overridden to provide the arguments after the application name. + * The default implementation returns an empty array. It never returns null. + * @return arguments for the native application. + */ + protected String[] getArguments() { + return new String[0]; + } + public static void initialize() { // The static nature of the singleton and Android quirkyness force us to initialize everything here // Otherwise, when exiting the app and returning to it, these variables *keep* their pre exit values @@ -62,78 +129,172 @@ mSurface = null; mTextEdit = null; mLayout = null; - mJoystickHandler = null; + mClipboardHandler = null; mSDLThread = null; - mAudioTrack = null; mExitCalledFromJava = false; - mIsPaused = false; + mBrokenLibraries = false; + mIsResumedCalled = false; mIsSurfaceReady = false; mHasFocus = true; + mNextNativeState = NativeState.INIT; + mCurrentNativeState = NativeState.INIT; } // Setup @Override protected void onCreate(Bundle savedInstanceState) { - Log.v("SDL", "onCreate():" + mSingleton); + Log.v(TAG, "Device: " + android.os.Build.DEVICE); + Log.v(TAG, "Model: " + android.os.Build.MODEL); + Log.v(TAG, "onCreate()"); super.onCreate(savedInstanceState); - - SDLActivity.initialize(); + + // Load shared libraries + String errorMsgBrokenLib = ""; + try { + loadLibraries(); + } catch(UnsatisfiedLinkError e) { + System.err.println(e.getMessage()); + mBrokenLibraries = true; + errorMsgBrokenLib = e.getMessage(); + } catch(Exception e) { + System.err.println(e.getMessage()); + mBrokenLibraries = true; + errorMsgBrokenLib = e.getMessage(); + } + + if (mBrokenLibraries) + { + AlertDialog.Builder dlgAlert = new AlertDialog.Builder(this); + dlgAlert.setMessage("An error occurred while trying to start the application. Please try again and/or reinstall." + + System.getProperty("line.separator") + + System.getProperty("line.separator") + + "Error: " + errorMsgBrokenLib); + dlgAlert.setTitle("SDL Error"); + dlgAlert.setPositiveButton("Exit", + new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog,int id) { + // if this button is clicked, close current activity + SDLActivity.mSingleton.finish(); + } + }); + dlgAlert.setCancelable(false); + dlgAlert.create().show(); + + return; + } + + // Set up JNI + SDL.setupJNI(); + + // Initialize state + SDL.initialize(); + // So we can call stuff from static callbacks mSingleton = this; + SDL.setContext(this); + + if (Build.VERSION.SDK_INT >= 11) { + mClipboardHandler = new SDLClipboardHandler_API11(); + } else { + /* Before API 11, no clipboard notification (eg no SDL_CLIPBOARDUPDATE) */ + mClipboardHandler = new SDLClipboardHandler_Old(); + } // Set up the surface mSurface = new SDLSurface(getApplication()); - - if(Build.VERSION.SDK_INT >= 12) { - mJoystickHandler = new SDLJoystickHandler_API12(); - } - else { - mJoystickHandler = new SDLJoystickHandler(); - } - mLayout = new AbsoluteLayout(this); + mLayout = new RelativeLayout(this); mLayout.addView(mSurface); setContentView(mLayout); + + // Get filename from "Open with" of another application + Intent intent = getIntent(); + if (intent != null && intent.getData() != null) { + String filename = intent.getData().getPath(); + if (filename != null) { + Log.v(TAG, "Got filename: " + filename); + SDLActivity.onNativeDropFile(filename); + } + } } // Events @Override protected void onPause() { - Log.v("SDL", "onPause()"); + Log.v(TAG, "onPause()"); super.onPause(); - SDLActivity.handlePause(); + mNextNativeState = NativeState.PAUSED; + mIsResumedCalled = false; + + if (SDLActivity.mBrokenLibraries) { + return; + } + + SDLActivity.handleNativeState(); } @Override protected void onResume() { - Log.v("SDL", "onResume()"); + Log.v(TAG, "onResume()"); super.onResume(); - SDLActivity.handleResume(); + mNextNativeState = NativeState.RESUMED; + mIsResumedCalled = true; + + if (SDLActivity.mBrokenLibraries) { + return; + } + + SDLActivity.handleNativeState(); } @Override public void onWindowFocusChanged(boolean hasFocus) { super.onWindowFocusChanged(hasFocus); - Log.v("SDL", "onWindowFocusChanged(): " + hasFocus); + Log.v(TAG, "onWindowFocusChanged(): " + hasFocus); + + if (SDLActivity.mBrokenLibraries) { + return; + } SDLActivity.mHasFocus = hasFocus; if (hasFocus) { - SDLActivity.handleResume(); + mNextNativeState = NativeState.RESUMED; + } else { + mNextNativeState = NativeState.PAUSED; } + + SDLActivity.handleNativeState(); } @Override public void onLowMemory() { - Log.v("SDL", "onLowMemory()"); + Log.v(TAG, "onLowMemory()"); super.onLowMemory(); + + if (SDLActivity.mBrokenLibraries) { + return; + } + SDLActivity.nativeLowMemory(); } @Override protected void onDestroy() { - Log.v("SDL", "onDestroy()"); + Log.v(TAG, "onDestroy()"); + + if (SDLActivity.mBrokenLibraries) { + super.onDestroy(); + // Reset everything in case the user re opens the app + SDLActivity.initialize(); + return; + } + + mNextNativeState = NativeState.PAUSED; + SDLActivity.handleNativeState(); + // Send a quit message to the application SDLActivity.mExitCalledFromJava = true; SDLActivity.nativeQuit(); @@ -143,57 +304,101 @@ try { SDLActivity.mSDLThread.join(); } catch(Exception e) { - Log.v("SDL", "Problem stopping thread: " + e); + Log.v(TAG, "Problem stopping thread: " + e); } SDLActivity.mSDLThread = null; - //Log.v("SDL", "Finished waiting for SDL thread"); + //Log.v(TAG, "Finished waiting for SDL thread"); } - + super.onDestroy(); + // Reset everything in case the user re opens the app SDLActivity.initialize(); } @Override public boolean dispatchKeyEvent(KeyEvent event) { + + if (SDLActivity.mBrokenLibraries) { + return false; + } + int keyCode = event.getKeyCode(); // Ignore certain special keys so they're handled by Android if (keyCode == KeyEvent.KEYCODE_VOLUME_DOWN || keyCode == KeyEvent.KEYCODE_VOLUME_UP || keyCode == KeyEvent.KEYCODE_CAMERA || - keyCode == 168 || /* API 11: KeyEvent.KEYCODE_ZOOM_IN */ - keyCode == 169 /* API 11: KeyEvent.KEYCODE_ZOOM_OUT */ + keyCode == KeyEvent.KEYCODE_ZOOM_IN || /* API 11 */ + keyCode == KeyEvent.KEYCODE_ZOOM_OUT /* API 11 */ ) { return false; } return super.dispatchKeyEvent(event); } - /** Called by onPause or surfaceDestroyed. Even if surfaceDestroyed - * is the first to be called, mIsSurfaceReady should still be set - * to 'true' during the call to onPause (in a usual scenario). - */ - public static void handlePause() { - if (!SDLActivity.mIsPaused && SDLActivity.mIsSurfaceReady) { - SDLActivity.mIsPaused = true; - SDLActivity.nativePause(); - mSurface.enableSensor(Sensor.TYPE_ACCELEROMETER, false); + /* Transition to next state */ + public static void handleNativeState() { + + if (mNextNativeState == mCurrentNativeState) { + // Already in same state, discard. + return; + } + + // Try a transition to init state + if (mNextNativeState == NativeState.INIT) { + + mCurrentNativeState = mNextNativeState; + return; + } + + // Try a transition to paused state + if (mNextNativeState == NativeState.PAUSED) { + nativePause(); + mSurface.handlePause(); + mCurrentNativeState = mNextNativeState; + return; + } + + // Try a transition to resumed state + if (mNextNativeState == NativeState.RESUMED) { + if (mIsSurfaceReady && mHasFocus && mIsResumedCalled) { + if (mSDLThread == null) { + // This is the entry point to the C app. + // Start up the C app thread and enable sensor input for the first time + // FIXME: Why aren't we enabling sensor input at start? + + final Thread sdlThread = new Thread(new SDLMain(), "SDLThread"); + mSurface.enableSensor(Sensor.TYPE_ACCELEROMETER, true); + sdlThread.start(); + + // Set up a listener thread to catch when the native thread ends + mSDLThread = new Thread(new Runnable() { + @Override + public void run() { + try { + sdlThread.join(); + } catch (Exception e) { + // Ignore any exception + } finally { + // Native thread has finished + if (!mExitCalledFromJava) { + handleNativeExit(); + } + } + } + }, "SDLThreadListener"); + + mSDLThread.start(); + } + + nativeResume(); + mSurface.handleResume(); + mCurrentNativeState = mNextNativeState; + } } } - /** Called by onResume or surfaceCreated. An actual resume should be done only when the surface is ready. - * Note: Some Android variants may send multiple surfaceChanged events, so we don't need to resume - * every time we get one of those events, only if it comes after surfaceDestroyed - */ - public static void handleResume() { - if (SDLActivity.mIsPaused && SDLActivity.mIsSurfaceReady && SDLActivity.mHasFocus) { - SDLActivity.mIsPaused = false; - SDLActivity.nativeResume(); - mSurface.enableSensor(Sensor.TYPE_ACCELEROMETER, true); - } - } - /* The native thread has finished */ public static void handleNativeExit() { SDLActivity.mSDLThread = null; @@ -205,6 +410,7 @@ static final int COMMAND_CHANGE_TITLE = 1; static final int COMMAND_UNUSED = 2; static final int COMMAND_TEXTEDIT_HIDE = 3; + static final int COMMAND_SET_KEEP_SCREEN_ON = 5; protected static final int COMMAND_USER = 0x8000; @@ -228,7 +434,7 @@ protected static class SDLCommandHandler extends Handler { @Override public void handleMessage(Message msg) { - Context context = getContext(); + Context context = SDL.getContext(); if (context == null) { Log.e(TAG, "error handling message, getContext() returned null"); return; @@ -243,13 +449,31 @@ break; case COMMAND_TEXTEDIT_HIDE: if (mTextEdit != null) { - mTextEdit.setVisibility(View.GONE); + // Note: On some devices setting view to GONE creates a flicker in landscape. + // Setting the View's sizes to 0 is similar to GONE but without the flicker. + // The sizes will be set to useful values when the keyboard is shown again. + mTextEdit.setLayoutParams(new RelativeLayout.LayoutParams(0, 0)); InputMethodManager imm = (InputMethodManager) context.getSystemService(Context.INPUT_METHOD_SERVICE); imm.hideSoftInputFromWindow(mTextEdit.getWindowToken(), 0); + + mScreenKeyboardShown = false; } break; - + case COMMAND_SET_KEEP_SCREEN_ON: + { + if (context instanceof Activity) { + Window window = ((Activity) context).getWindow(); + if (window != null) { + if ((msg.obj instanceof Integer) && (((Integer) msg.obj).intValue() != 0)) { + window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); + } else { + window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); + } + } + } + break; + } default: if ((context instanceof SDLActivity) && !((SDLActivity) context).onUnhandledMessage(msg.arg1, msg.obj)) { Log.e(TAG, "error handling message, command is " + msg.arg1); @@ -270,76 +494,123 @@ } // C functions we call - public static native void nativeInit(); + public static native int nativeSetupJNI(); + public static native int nativeRunMain(String library, String function, Object arguments); public static native void nativeLowMemory(); public static native void nativeQuit(); public static native void nativePause(); public static native void nativeResume(); - public static native void onNativeResize(int x, int y, int format); - public static native int onNativePadDown(int device_id, int keycode); - public static native int onNativePadUp(int device_id, int keycode); - public static native void onNativeJoy(int device_id, int axis, - float value); - public static native void onNativeHat(int device_id, int hat_id, - int x, int y); + public static native void onNativeDropFile(String filename); + public static native void onNativeResize(int x, int y, int format, float rate); public static native void onNativeKeyDown(int keycode); public static native void onNativeKeyUp(int keycode); public static native void onNativeKeyboardFocusLost(); + public static native void onNativeMouse(int button, int action, float x, float y); public static native void onNativeTouch(int touchDevId, int pointerFingerId, - int action, float x, + int action, float x, float y, float p); public static native void onNativeAccel(float x, float y, float z); + public static native void onNativeClipboardChanged(); public static native void onNativeSurfaceChanged(); public static native void onNativeSurfaceDestroyed(); - public static native void nativeFlipBuffers(); - public static native int nativeAddJoystick(int device_id, String name, - int is_accelerometer, int nbuttons, - int naxes, int nhats, int nballs); - public static native int nativeRemoveJoystick(int device_id); + public static native String nativeGetHint(String name); - public static void flipBuffers() { - SDLActivity.nativeFlipBuffers(); - } - + /** + * This method is called by SDL using JNI. + */ public static boolean setActivityTitle(String title) { // Called from SDLMain() thread and can't directly affect the view return mSingleton.sendCommand(COMMAND_CHANGE_TITLE, title); } - public static boolean sendMessage(int command, int param) { - return mSingleton.sendCommand(command, Integer.valueOf(param)); + /** + * This method is called by SDL using JNI. + * This is a static method for JNI convenience, it calls a non-static method + * so that is can be overridden + */ + public static void setOrientation(int w, int h, boolean resizable, String hint) + { + if (mSingleton != null) { + mSingleton.setOrientationBis(w, h, resizable, hint); + } + } + + /** + * This can be overridden + */ + public void setOrientationBis(int w, int h, boolean resizable, String hint) + { + int orientation = -1; + + if (!Objects.equals(hint, "")) { + if (hint.contains("LandscapeRight") && hint.contains("LandscapeLeft")) { + orientation = ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE; + } else if (hint.contains("LandscapeRight")) { + orientation = ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE; + } else if (hint.contains("LandscapeLeft")) { + orientation = ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE; + } else if (hint.contains("Portrait") && hint.contains("PortraitUpsideDown")) { + orientation = ActivityInfo.SCREEN_ORIENTATION_SENSOR_PORTRAIT; + } else if (hint.contains("Portrait")) { + orientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT; + } else if (hint.contains("PortraitUpsideDown")) { + orientation = ActivityInfo.SCREEN_ORIENTATION_REVERSE_PORTRAIT; + } + } + + /* no valid hint */ + if (orientation == -1) { + if (resizable) { + /* no fixed orientation */ + } else { + if (w > h) { + orientation = ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE; + } else { + orientation = ActivityInfo.SCREEN_ORIENTATION_SENSOR_PORTRAIT; + } + } + } + + Log.v("SDL", "setOrientation() orientation=" + orientation + " width=" + w +" height="+ h +" resizable=" + resizable + " hint=" + hint); + if (orientation != -1) { + mSingleton.setRequestedOrientation(orientation); + } } - public static Context getContext() { - return mSingleton; + + /** + * This method is called by SDL using JNI. + */ + public static boolean isScreenKeyboardShown() + { + if (mTextEdit == null) { + return false; + } + + if (!mScreenKeyboardShown) { + return false; + } + + InputMethodManager imm = (InputMethodManager) SDL.getContext().getSystemService(Context.INPUT_METHOD_SERVICE); + return imm.isAcceptingText(); + } /** - * @return result of getSystemService(name) but executed on UI thread. + * This method is called by SDL using JNI. */ - public Object getSystemServiceFromUiThread(final String name) { - final Object lock = new Object(); - final Object[] results = new Object[2]; // array for writable variables - synchronized (lock) { - runOnUiThread(new Runnable() { - @Override - public void run() { - synchronized (lock) { - results[0] = getSystemService(name); - results[1] = Boolean.TRUE; - lock.notify(); - } - } - }); - if (results[1] == null) { - try { - lock.wait(); - } catch (InterruptedException ex) { - ex.printStackTrace(); - } - } + public static boolean sendMessage(int command, int param) { + if (mSingleton == null) { + return false; } - return results[0]; + return mSingleton.sendCommand(command, Integer.valueOf(param)); + } + + /** + * This method is called by SDL using JNI. + */ + public static Context getContext() { + return SDL.getContext(); } static class ShowTextInputTask implements Runnable { @@ -361,11 +632,12 @@ @Override public void run() { - AbsoluteLayout.LayoutParams params = new AbsoluteLayout.LayoutParams( - w, h + HEIGHT_PADDING, x, y); + RelativeLayout.LayoutParams params = new RelativeLayout.LayoutParams(w, h + HEIGHT_PADDING); + params.leftMargin = x; + params.topMargin = y; if (mTextEdit == null) { - mTextEdit = new DummyEdit(getContext()); + mTextEdit = new DummyEdit(SDL.getContext()); mLayout.addView(mTextEdit, params); } else { @@ -375,101 +647,47 @@ mTextEdit.setVisibility(View.VISIBLE); mTextEdit.requestFocus(); - InputMethodManager imm = (InputMethodManager) getContext().getSystemService(Context.INPUT_METHOD_SERVICE); + InputMethodManager imm = (InputMethodManager) SDL.getContext().getSystemService(Context.INPUT_METHOD_SERVICE); imm.showSoftInput(mTextEdit, 0); + + mScreenKeyboardShown = true; } } + /** + * This method is called by SDL using JNI. + */ public static boolean showTextInput(int x, int y, int w, int h) { // Transfer the task to the main thread as a Runnable return mSingleton.commandHandler.post(new ShowTextInputTask(x, y, w, h)); } - - public static Surface getNativeSurface() { - return SDLActivity.mSurface.getNativeSurface(); + + public static boolean isTextInputEvent(KeyEvent event) { + + // Key pressed with Ctrl should be sent as SDL_KEYDOWN/SDL_KEYUP and not SDL_TEXTINPUT + if (android.os.Build.VERSION.SDK_INT >= 11) { + if (event.isCtrlPressed()) { + return false; + } + } + + return event.isPrintingKey() || event.getKeyCode() == KeyEvent.KEYCODE_SPACE; } - // Audio - public static int audioInit(int sampleRate, boolean is16Bit, boolean isStereo, int desiredFrames) { - int channelConfig = isStereo ? AudioFormat.CHANNEL_CONFIGURATION_STEREO : AudioFormat.CHANNEL_CONFIGURATION_MONO; - int audioFormat = is16Bit ? AudioFormat.ENCODING_PCM_16BIT : AudioFormat.ENCODING_PCM_8BIT; - int frameSize = (isStereo ? 2 : 1) * (is16Bit ? 2 : 1); - - Log.v("SDL", "SDL audio: wanted " + (isStereo ? "stereo" : "mono") + " " + (is16Bit ? "16-bit" : "8-bit") + " " + (sampleRate / 1000f) + "kHz, " + desiredFrames + " frames buffer"); - - // Let the user pick a larger buffer if they really want -- but ye - // gods they probably shouldn't, the minimums are horrifyingly high - // latency already - desiredFrames = Math.max(desiredFrames, (AudioTrack.getMinBufferSize(sampleRate, channelConfig, audioFormat) + frameSize - 1) / frameSize); - - if (mAudioTrack == null) { - mAudioTrack = new AudioTrack(AudioManager.STREAM_MUSIC, sampleRate, - channelConfig, audioFormat, desiredFrames * frameSize, AudioTrack.MODE_STREAM); - - // Instantiating AudioTrack can "succeed" without an exception and the track may still be invalid - // Ref: https://android.googlesource.com/platform/frameworks/base/+/refs/heads/master/media/java/android/media/AudioTrack.java - // Ref: http://developer.android.com/reference/android/media/AudioTrack.html#getState() - - if (mAudioTrack.getState() != AudioTrack.STATE_INITIALIZED) { - Log.e("SDL", "Failed during initialization of Audio Track"); - mAudioTrack = null; - return -1; - } - - mAudioTrack.play(); + /** + * This method is called by SDL using JNI. + */ + public static Surface getNativeSurface() { + if (SDLActivity.mSurface == null) { + return null; } - - Log.v("SDL", "SDL audio: got " + ((mAudioTrack.getChannelCount() >= 2) ? "stereo" : "mono") + " " + ((mAudioTrack.getAudioFormat() == AudioFormat.ENCODING_PCM_16BIT) ? "16-bit" : "8-bit") + " " + (mAudioTrack.getSampleRate() / 1000f) + "kHz, " + desiredFrames + " frames buffer"); - - return 0; - } - - public static void audioWriteShortBuffer(short[] buffer) { - for (int i = 0; i < buffer.length; ) { - int result = mAudioTrack.write(buffer, i, buffer.length - i); - if (result > 0) { - i += result; - } else if (result == 0) { - try { - Thread.sleep(1); - } catch(InterruptedException e) { - // Nom nom - } - } else { - Log.w("SDL", "SDL audio: error return from write(short)"); - return; - } - } - } - - public static void audioWriteByteBuffer(byte[] buffer) { - for (int i = 0; i < buffer.length; ) { - int result = mAudioTrack.write(buffer, i, buffer.length - i); - if (result > 0) { - i += result; - } else if (result == 0) { - try { - Thread.sleep(1); - } catch(InterruptedException e) { - // Nom nom - } - } else { - Log.w("SDL", "SDL audio: error return from write(byte)"); - return; - } - } - } - - public static void audioQuit() { - if (mAudioTrack != null) { - mAudioTrack.stop(); - mAudioTrack = null; - } + return SDLActivity.mSurface.getNativeSurface(); } // Input /** + * This method is called by SDL using JNI. * @return an array which may be empty but is never null. */ public static int[] inputGetInputDeviceIds(int sources) { @@ -484,41 +702,331 @@ } return Arrays.copyOf(filtered, used); } - - // Joystick glue code, just a series of stubs that redirect to the SDLJoystickHandler instance - public static boolean handleJoystickMotionEvent(MotionEvent event) { - return mJoystickHandler.handleMotionEvent(event); + + // APK expansion files support + + /** com.android.vending.expansion.zipfile.ZipResourceFile object or null. */ + private static Object expansionFile; + + /** com.android.vending.expansion.zipfile.ZipResourceFile's getInputStream() or null. */ + private static Method expansionFileMethod; + + /** + * This method is called by SDL using JNI. + * @return an InputStream on success or null if no expansion file was used. + * @throws IOException on errors. Message is set for the SDL error message. + */ + public static InputStream openAPKExpansionInputStream(String fileName) throws IOException { + // Get a ZipResourceFile representing a merger of both the main and patch files + if (expansionFile == null) { + String mainHint = nativeGetHint("SDL_ANDROID_APK_EXPANSION_MAIN_FILE_VERSION"); + if (mainHint == null) { + return null; // no expansion use if no main version was set + } + String patchHint = nativeGetHint("SDL_ANDROID_APK_EXPANSION_PATCH_FILE_VERSION"); + if (patchHint == null) { + return null; // no expansion use if no patch version was set + } + + Integer mainVersion; + Integer patchVersion; + try { + mainVersion = Integer.valueOf(mainHint); + patchVersion = Integer.valueOf(patchHint); + } catch (NumberFormatException ex) { + ex.printStackTrace(); + throw new IOException("No valid file versions set for APK expansion files", ex); + } + + try { + // To avoid direct dependency on Google APK expansion library that is + // not a part of Android SDK we access it using reflection + expansionFile = Class.forName("com.android.vending.expansion.zipfile.APKExpansionSupport") + .getMethod("getAPKExpansionZipFile", Context.class, int.class, int.class) + .invoke(null, SDL.getContext(), mainVersion, patchVersion); + + expansionFileMethod = expansionFile.getClass() + .getMethod("getInputStream", String.class); + } catch (Exception ex) { + ex.printStackTrace(); + expansionFile = null; + expansionFileMethod = null; + throw new IOException("Could not access APK expansion support library", ex); + } + } + + // Get an input stream for a known file inside the expansion file ZIPs + InputStream fileStream; + try { + fileStream = (InputStream)expansionFileMethod.invoke(expansionFile, fileName); + } catch (Exception ex) { + // calling "getInputStream" failed + ex.printStackTrace(); + throw new IOException("Could not open stream from APK expansion file", ex); + } + + if (fileStream == null) { + // calling "getInputStream" was successful but null was returned + throw new IOException("Could not find path in APK expansion file"); + } + + return fileStream; + } + + // Messagebox + + /** Result of current messagebox. Also used for blocking the calling thread. */ + protected final int[] messageboxSelection = new int[1]; + + /** Id of current dialog. */ + protected int dialogs = 0; + + /** + * This method is called by SDL using JNI. + * Shows the messagebox from UI thread and block calling thread. + * buttonFlags, buttonIds and buttonTexts must have same length. + * @param buttonFlags array containing flags for every button. + * @param buttonIds array containing id for every button. + * @param buttonTexts array containing text for every button. + * @param colors null for default or array of length 5 containing colors. + * @return button id or -1. + */ + public int messageboxShowMessageBox( + final int flags, + final String title, + final String message, + final int[] buttonFlags, + final int[] buttonIds, + final String[] buttonTexts, + final int[] colors) { + + messageboxSelection[0] = -1; + + // sanity checks + + if ((buttonFlags.length != buttonIds.length) && (buttonIds.length != buttonTexts.length)) { + return -1; // implementation broken + } + + // collect arguments for Dialog + + final Bundle args = new Bundle(); + args.putInt("flags", flags); + args.putString("title", title); + args.putString("message", message); + args.putIntArray("buttonFlags", buttonFlags); + args.putIntArray("buttonIds", buttonIds); + args.putStringArray("buttonTexts", buttonTexts); + args.putIntArray("colors", colors); + + // trigger Dialog creation on UI thread + + runOnUiThread(new Runnable() { + @Override + public void run() { + showDialog(dialogs++, args); + } + }); + + // block the calling thread + + synchronized (messageboxSelection) { + try { + messageboxSelection.wait(); + } catch (InterruptedException ex) { + ex.printStackTrace(); + return -1; + } + } + + // return selected value + + return messageboxSelection[0]; + } + + @Override + protected Dialog onCreateDialog(int ignore, Bundle args) { + + // TODO set values from "flags" to messagebox dialog + + // get colors + + int[] colors = args.getIntArray("colors"); + int backgroundColor; + int textColor; + int buttonBorderColor; + int buttonBackgroundColor; + int buttonSelectedColor; + if (colors != null) { + int i = -1; + backgroundColor = colors[++i]; + textColor = colors[++i]; + buttonBorderColor = colors[++i]; + buttonBackgroundColor = colors[++i]; + buttonSelectedColor = colors[++i]; + } else { + backgroundColor = Color.TRANSPARENT; + textColor = Color.TRANSPARENT; + buttonBorderColor = Color.TRANSPARENT; + buttonBackgroundColor = Color.TRANSPARENT; + buttonSelectedColor = Color.TRANSPARENT; + } + + // create dialog with title and a listener to wake up calling thread + + final Dialog dialog = new Dialog(this); + dialog.setTitle(args.getString("title")); + dialog.setCancelable(false); + dialog.setOnDismissListener(new DialogInterface.OnDismissListener() { + @Override + public void onDismiss(DialogInterface unused) { + synchronized (messageboxSelection) { + messageboxSelection.notify(); + } + } + }); + + // create text + + TextView message = new TextView(this); + message.setGravity(Gravity.CENTER); + message.setText(args.getString("message")); + if (textColor != Color.TRANSPARENT) { + message.setTextColor(textColor); + } + + // create buttons + + int[] buttonFlags = args.getIntArray("buttonFlags"); + int[] buttonIds = args.getIntArray("buttonIds"); + String[] buttonTexts = args.getStringArray("buttonTexts"); + + final SparseArray