changeset 2600:251cc75574af

Basic emscripten support
author Michael Pavone <pavone@retrodev.com>
date Thu, 13 Feb 2025 02:18:30 -0800
parents ca8141c2d6ba
children 6a84a38b3cf9
files Makefile blastem.c cpu_dsl.py genesis.c nuklear_ui/blastem_nuklear.c nuklear_ui/blastem_nuklear.h render.h render_sdl.c sms.c z80.cpu
diffstat 10 files changed, 196 insertions(+), 49 deletions(-) [+]
line wrap: on
line diff
--- a/Makefile	Sun Feb 09 22:46:07 2025 -0800
+++ b/Makefile	Thu Feb 13 02:18:30 2025 -0800
@@ -9,6 +9,7 @@
 endif
 SDL_UPPER:=$(shell echo $(SDL) | tr '[a-z]' '[A-Z]')
 FIXUP:=true
+Z80_DISPATCH:=goto
 
 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
@@ -62,6 +63,10 @@
 else
 SO:=so
 
+ifeq ($(CPU),wasm)
+USE_GLES:=1
+endif
+
 ifdef USE_FBDEV
 LIBS=alsa
 ifndef NOGL
@@ -76,6 +81,11 @@
 LIBS=$(SDL) glew gl
 endif #USE_GLES
 endif #USE_FBDEV
+ifeq ($(CPU),wasm)
+CHOOSER:=nuklear_ui/filechooser_null.o
+FONT:=nuklear_ui/font_web.o
+Z80_DISPATCH:=call
+else #CPU=wasm
 FONT:=nuklear_ui/font.o
 ifneq ($(MAKECMDGOALS),libblastem.$(SO))
 CHOOSER:=nuklear_ui/filechooser_gtk.o
@@ -87,6 +97,7 @@
 endif
 endif
 endif #neq ($(MAKECMDGOALS),libblastem.$(SO))
+endif #CPU=wasm
 ifeq ($(GTKFLAGS),)
 else
 EXTRA_NUKLEAR_LDFLAGS:=-ldl
@@ -106,6 +117,12 @@
 CFLAGS+= -Wno-logical-op-parentheses
 endif
 
+ifeq ($(CPU),wasm)
+CFLAGS+= --use-port=sdl2
+LDFLAGS+= --use-port=sdl2 --embed-file rom.db --embed-file default.cfg --embed-file systems.cfg --embed-file shaders/ --embed-file images/ --embed-file DroidSans.ttf --embed-file roms/
+EXE:=.html
+else #CPU=wasm
+
 ifdef PORTABLE
 ifdef USE_GLES
 ifndef GLES_LIB
@@ -152,6 +169,7 @@
 endif
 
 endif #PORTABLE
+endif #CPU=wasm
 endif #Windows
 
 ifndef OPT
@@ -372,8 +390,9 @@
 m68k.c : m68k.cpu cpu_dsl.py
 	./cpu_dsl.py -d call $< > $@
 
+.PRECIOUS: %.c
 %.c : %.cpu cpu_dsl.py
-	./cpu_dsl.py -d goto $< > $@
+	./cpu_dsl.py -d $(Z80_DISPATCH) $< > $@
 
 %.db.c : %.db
 	sed $< -e 's/"/\\"/g' -e 's/^\(.*\)$$/"\1\\n"/' -e'1s/^\(.*\)$$/const char $(shell echo $< | tr '.' '_')_data[] = \1/' -e '$$s/^\(.*\)$$/\1;/' > $@
--- a/blastem.c	Sun Feb 09 22:46:07 2025 -0800
+++ b/blastem.c	Thu Feb 13 02:18:30 2025 -0800
@@ -30,6 +30,9 @@
 #ifndef DISABLE_NUKLEAR
 #include "nuklear_ui/blastem_nuklear.h"
 #endif
+#ifdef __EMSCRIPTEN__
+#include <emscripten.h>
+#endif
 
 #include "version.inc"
 
@@ -271,6 +274,64 @@
 	update_title(game_system->info.name);
 }
 
+static uint8_t menu;
+static uint8_t use_nuklear;
+#ifdef __EMSCRIPTEN__
+void handle_frame_presented(void)
+{
+	if (current_system) {
+		current_system->request_exit(current_system);
+	}
+}
+
+void browser_main_loop(void)
+{
+	static uint8_t system_started;
+#ifndef DISABLE_NUKLEAR
+	static uint8_t was_menu;
+	if (use_nuklear) {
+		if (menu && !was_menu) {
+			ui_enter();
+		} else if (!menu && was_menu) {
+			ui_exit();
+		}
+		if (menu) {
+			render_update_display();
+		}
+	}
+#endif
+	if (!current_system && game_system) {
+		current_system = game_system;
+		menu = 0;
+#ifndef DISABLE_NUKLEAR
+		was_menu = 0;
+		ui_exit();
+#endif
+	}
+	if (current_system) {
+		if (current_system->next_rom) {
+			char *next_rom = current_system->next_rom;
+			current_system->next_rom = NULL;
+			init_system_with_media(next_rom, 0);
+			system_started = 0;
+			menu = 0;
+			current_system = game_system;
+		} else if (!menu) {
+			if (system_started) {
+				current_system->resume_context(current_system);
+			} else {
+				system_started = 1;
+				current_system->start_context(current_system, NULL);
+			}
+			if (current_system->force_release) {
+				menu = 1;
+			}
+		}
+	}
+	
+}
+#endif
+
 char *parse_addr_port(char *arg)
 {
 	while (*arg && *arg != ':') {
@@ -478,6 +539,12 @@
 	if (config_fullscreen && !strcmp("on", config_fullscreen)) {
 		fullscreen = !fullscreen;
 	}
+#ifdef __EMSCRIPTEN__
+	config = tern_insert_path(config, "system\0sync_source\0", (tern_val){.ptrval = strdup("video")}, TVAL_PTR);
+	config = tern_insert_path(config, "ui\0initial_path\0", (tern_val){.ptrval = strdup("/roms")}, TVAL_PTR);
+	render_set_frame_presented_fun(handle_frame_presented);
+	emscripten_set_main_loop(browser_main_loop, 0, 0);
+#endif
 	if (!headless) {
 		if (reader_addr) {
 			render_set_external_sync(1);
@@ -486,9 +553,8 @@
 		render_set_drag_drop_handler(on_drag_drop);
 	}
 	set_bindings();
+	menu = !loaded;
 
-	uint8_t menu = !loaded;
-	uint8_t use_nuklear = 0;
 #ifndef DISABLE_NUKLEAR
 	use_nuklear = !headless && is_nuklear_available();
 #endif
@@ -554,8 +620,10 @@
 #ifndef DISABLE_NUKLEAR
 	if (use_nuklear) {
 		blastem_nuklear_init(!menu);
+#ifndef __EMSCRIPTEN__
 		current_system = game_system;
 		menu = 0;
+#endif
 	}
 #endif
 
@@ -574,6 +642,7 @@
 
 	current_system->debugger_type = dtype;
 	current_system->enter_debugger = start_in_debugger && menu == debug_target;
+#ifndef __EMSCRIPTEN__
 	current_system->start_context(current_system,  menu ? NULL : statefile);
 	render_video_loop();
 	for(;;)
@@ -614,6 +683,7 @@
 			break;
 		}
 	}
+#endif //__EMSCRIPTEN__
 
 	return 0;
 }
--- a/cpu_dsl.py	Sun Feb 09 22:46:07 2025 -0800
+++ b/cpu_dsl.py	Thu Feb 13 02:18:30 2025 -0800
@@ -243,7 +243,7 @@
 			del self.locals[name]
 		
 		if prog.dispatch == 'call':
-			begin = '\nvoid ' + self.generateName(value) + '(' + prog.context_type + ' *context, uint32_t target_cycle)\n{'
+			begin = '\nstatic void ' + self.generateName(value) + '(' + prog.context_type + ' *context, uint32_t target_cycle)\n{'
 		elif prog.dispatch == 'goto':
 			begin = '\n' + self.generateName(value) + ': {'
 		else:
--- a/genesis.c	Sun Feb 09 22:46:07 2025 -0800
+++ b/genesis.c	Thu Feb 13 02:18:30 2025 -0800
@@ -2077,6 +2077,7 @@
 		sync_components(gen->m68k, 0);
 		m68k_execute(gen->m68k, gen->m68k->target_cycle);
 	}
+	gen->m68k->should_return = 0;
 #endif
 	handle_reset_requests(gen);
 	return;
@@ -2099,7 +2100,15 @@
 		}
 		render_resume_source(gen->psg->audio);
 	}
+#ifdef NEW_CORE
+	while (!gen->m68k->should_return) {
+		sync_components(gen->m68k, 0);
+		m68k_execute(gen->m68k, gen->m68k->target_cycle);
+	}
+	gen->m68k->should_return = 0;
+#else
 	resume_68k(gen->m68k);
+#endif
 	handle_reset_requests(gen);
 }
 
--- a/nuklear_ui/blastem_nuklear.c	Sun Feb 09 22:46:07 2025 -0800
+++ b/nuklear_ui/blastem_nuklear.c	Thu Feb 13 02:18:30 2025 -0800
@@ -2453,7 +2453,9 @@
 		{"Save State", view_save_state},
 		{"Load State", view_load_state},
 		{"Settings", view_settings},
+#ifndef __EMSCRIPTEN__
 		{"Exit", NULL}
+#endif
 	};
 	static menu_item sc3k_items[] = {
 		{"Resume", view_play},
@@ -2462,7 +2464,9 @@
 		{"Save State", view_save_state},
 		{"Load State", view_load_state},
 		{"Settings", view_settings},
+#ifndef __EMSCRIPTEN__
 		{"Exit", NULL}
+#endif
 	};
 
 	if (nk_begin(context, "Main Menu", nk_rect(0, 0, render_width(), render_height()), 0)) {
@@ -2511,9 +2515,25 @@
 	}
 }
 
+void ui_enter(void)
+{
+	render_enable_gamepad_events(1);
+}
+
+void ui_exit(void)
+{
+	if (config_dirty) {
+		apply_updated_config();
+		persist_config(config);
+		config_dirty = 0;
+	}
+	render_enable_gamepad_events(0);
+}
+
 void ui_idle_loop(void)
 {
-	render_enable_gamepad_events(1);
+#ifndef __EMSCRIPTEN__
+	ui_enter();
 	const uint32_t MIN_UI_DELAY = 15;
 	static uint32_t last;
 	while (current_view != view_play)
@@ -2525,12 +2545,8 @@
 		last = current;
 		render_update_display();
 	}
-	if (config_dirty) {
-		apply_updated_config();
-		persist_config(config);
-		config_dirty = 0;
-	}
-	render_enable_gamepad_events(0);
+	ui_exit();
+#endif
 }
 static void handle_event(SDL_Event *event)
 {
--- a/nuklear_ui/blastem_nuklear.h	Sun Feb 09 22:46:07 2025 -0800
+++ b/nuklear_ui/blastem_nuklear.h	Thu Feb 13 02:18:30 2025 -0800
@@ -15,6 +15,9 @@
 void show_play_view(void);
 uint8_t is_nuklear_active(void);
 uint8_t is_nuklear_available(void);
+//ui_idle_loop calls these automatically, needed externally for browser target
+void ui_enter(void);
+void ui_exit(void);
 void ui_idle_loop(void);
 
 #endif //BLASTEM_NUKLEAR_H_
--- a/render.h	Sun Feb 09 22:46:07 2025 -0800
+++ b/render.h	Thu Feb 13 02:18:30 2025 -0800
@@ -142,6 +142,7 @@
 void render_set_gl_context_handlers(ui_render_fun destroy, ui_render_fun create);
 void render_set_ui_render_fun(ui_render_fun);
 void render_set_ui_fb_resize_handler(ui_render_fun resize);
+void render_set_frame_presented_fun(ui_render_fun);
 void render_video_loop(void);
 uint8_t render_should_release_on_exit(void);
 void render_set_external_sync(uint8_t ext_sync_on);
--- a/render_sdl.c	Sun Feb 09 22:46:07 2025 -0800
+++ b/render_sdl.c	Thu Feb 13 02:18:30 2025 -0800
@@ -107,7 +107,11 @@
 
 uint8_t render_should_release_on_exit(void)
 {
+#ifdef __EMSCRIPTEN__
+	return 0;
+#else
 	return sync_src != SYNC_AUDIO_THREAD;
+#endif
 }
 
 void render_buffer_consumed(audio_source *src)
@@ -1271,7 +1275,11 @@
 	SDL_DisplayMode mode;
 	//TODO: Explicit multiple monitor support
 	SDL_GetCurrentDisplayMode(0, &mode);
+#ifdef __EMSCRIPTEN__
+	display_hz = 60; //TODO: FIXME
+#else
 	display_hz = mode.refresh_rate;
+#endif
 
 	if (fullscreen) {
 		//the SDL2 migration guide suggests setting width and height to 0 when using SDL_WINDOW_FULLSCREEN_DESKTOP
@@ -1509,10 +1517,12 @@
 	free(path);
 }
 
+#ifndef __EMSCRIPTEN__
 void GLAPIENTRY gl_message_callback(GLenum source, GLenum type, GLenum id, GLenum severity, GLsizei length, const GLchar *message, const void *user)
 {
 	fprintf(stderr, "GL Message: %d, %d, %d - %s\n", source, type, severity, message);
 }
+#endif
 
 uint8_t render_create_window(char *caption, uint32_t width, uint32_t height, window_close_handler close_handler)
 {
@@ -1553,10 +1563,12 @@
 	if (render_gl) {
 		extras[win_idx].gl_context = SDL_GL_CreateContext(extras[win_idx].win);
 		SDL_GL_MakeCurrent(extras[win_idx].win, extras[win_idx].gl_context);
+#ifndef __EMSCRIPTEN__
 		glEnable(GL_DEBUG_OUTPUT);
 		if (glDebugMessageCallback) {
 			glDebugMessageCallback(gl_message_callback, NULL);
 		}
+#endif
 		glGenTextures(2, extras[win_idx].gl_texture);
 		for (int i = 0; i < 2; i++)
 		{
@@ -2256,6 +2268,12 @@
 	render_ui = fun;
 }
 
+static ui_render_fun frame_presented;
+void render_set_frame_presented_fun(ui_render_fun fun)
+{
+	frame_presented = fun;
+}
+
 void render_update_display()
 {
 #ifndef DISABLE_OPENGL
@@ -2328,6 +2346,9 @@
 		process_events();
 	}
 	events_processed = 0;
+	if (frame_presented) {
+		frame_presented();
+	}
 }
 
 uint32_t render_emulated_width()
--- a/sms.c	Sun Feb 09 22:46:07 2025 -0800
+++ b/sms.c	Thu Feb 13 02:18:30 2025 -0800
@@ -1099,12 +1099,6 @@
 {
 	sms_context *sms = (sms_context *)system;
 	uint32_t target_cycle = sms->z80->Z80_CYCLE + 3420*16;
-	//TODO: PAL support
-	if (sms->vdp->type == VDP_GAMEGEAR) {
-		render_set_video_standard(VID_GAMEGEAR);
-	} else {
-		render_set_video_standard(VID_NTSC);
-	}
 	while (!sms->should_return)
 	{
 		if (system->delayed_load_slot) {
@@ -1214,6 +1208,12 @@
 	sms_context *sms = (sms_context *)system;
 	if (sms->header.force_release || render_should_release_on_exit()) {
 		sms->header.force_release = 0;
+		//TODO: PAL support
+		if (sms->vdp->type == VDP_GAMEGEAR) {
+			render_set_video_standard(VID_GAMEGEAR);
+		} else {
+			render_set_video_standard(VID_NTSC);
+		}
 		bindings_reacquire_capture();
 		vdp_reacquire_framebuffer(sms->vdp);
 		render_resume_source(sms->psg->audio);
@@ -1239,6 +1239,12 @@
 #endif
 	}
 
+	//TODO: PAL support
+	if (sms->vdp->type == VDP_GAMEGEAR) {
+		render_set_video_standard(VID_GAMEGEAR);
+	} else {
+		render_set_video_standard(VID_NTSC);
+	}
 	run_sms(system);
 }
 
--- a/z80.cpu	Sun Feb 09 22:46:07 2025 -0800
+++ b/z80.cpu	Thu Feb 13 02:18:30 2025 -0800
@@ -90,40 +90,42 @@
 	dispatch scratch1
 
 z80_interrupt
-	if cycles >=U int_cycle
-	
-		iff1 = 0
-		iff2 = 0
-		cycles 6
-		update_sync
-		
-		switch imode
-		case 0
-			dispatch int_value
-		
-		case 1
-			dispatch 0xFF
+	if cycles >=U nmi_cycle
 		
-		case 2
-			pc = i << 8
-			pc |= int_value
-			#CD is call
-			dispatch 0xCD
-		end
-		
+		nmi_cycle = 0xFFFFFFFF
+		iff1 = 0
+		local pch 8
+		pch = pc >> 8
+		meta high pch
+		meta low pc
+		z80_push
+		pc = 0x66
+		update_sync
+	
 	else
-		if cycles >=U nmi_cycle
-		
-			nmi_cycle = 0xFFFFFFFF
-			iff1 = 0
-			local pch 8
-			pch = pc >> 8
-			meta high pch
-			meta low pc
-			z80_push
-			pc = 0x66
-			update_sync
-		
+		if iff1
+			if cycles >=U int_cycle
+			
+				iff1 = 0
+				iff2 = 0
+				cycles 6
+				update_sync
+				
+				switch imode
+				case 0
+					dispatch int_value
+				
+				case 1
+					dispatch 0xFF
+				
+				case 2
+					pc = i << 8
+					pc |= int_value
+					#CD is call
+					dispatch 0xCD
+				end
+				
+			end
 		end
 	end