diff --git a/CMakeLists.txt b/CMakeLists.txt index d4c9ff999..65ba946f1 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -26,6 +26,19 @@ set(CMAKE_CXX_STANDARD_REQUIRED ON) add_compile_definitions("$<$:_DEBUG>") add_compile_options(-Werror=return-type -Wno-switch) +if (EMSCRIPTEN) + add_compile_options( + -fexceptions + -Oz + ) + + add_link_options( + -fexceptions + -sALLOW_MEMORY_GROWTH=1 + -Oz + ) +endif() + if (CMAKE_CXX_COMPILER_ID STREQUAL "GNU") add_compile_options(-Werror=format -Wno-error=format-overflow -Wno-error=format-truncation -Wno-psabi) endif() diff --git a/minizip/CMakeLists.txt b/minizip/CMakeLists.txt index 769bbbd95..abac3ba7d 100644 --- a/minizip/CMakeLists.txt +++ b/minizip/CMakeLists.txt @@ -15,10 +15,12 @@ target_include_directories(minizip INTERFACE ${CMAKE_CURRENT_SOURCE_DIR}/.. ) -# use 32‑bit file API to fix Android compilation issue -target_compile_definitions(minizip PUBLIC - -DUSE_FILE32API -) +if (NOT EMSCRIPTEN) + # use 32‑bit file API to fix Android compilation issue + target_compile_definitions(minizip PUBLIC + -DUSE_FILE32API + ) +endif() target_link_libraries(minizip PUBLIC zlib2 diff --git a/source/AY8910.cpp b/source/AY8910.cpp index 22cc7a41d..0be6c3cc5 100644 --- a/source/AY8910.cpp +++ b/source/AY8910.cpp @@ -784,7 +784,7 @@ void AY8913::sound_ay_write( int reg, int val, libspectrum_dword now ) } else { - LogOutput("AY reg write discarded: %02X = %02X\n", reg, val); + // LogOutput("AY reg write discarded: %02X = %02X\n", reg, val); } } diff --git a/source/CMakeLists.txt b/source/CMakeLists.txt index 6f0037295..a5828d50d 100644 --- a/source/CMakeLists.txt +++ b/source/CMakeLists.txt @@ -5,7 +5,7 @@ option(ENABLE_NETWORKING "Enable networking support (SLIRP/PCAP)" ON) set(SLIRP_FOUND "0") set(PCAP_FOUND "0") -if (ENABLE_NETWORKING AND NOT WIN32 AND NOT IOS) +if (ENABLE_NETWORKING AND NOT WIN32 AND NOT IOS AND NOT EMSCRIPTEN) pkg_search_module(SLIRP slirp) # if slirp is not found, we will try pcap diff --git a/source/Video.h b/source/Video.h index de6a00287..b5dea7828 100644 --- a/source/Video.h +++ b/source/Video.h @@ -100,10 +100,17 @@ enum AppleFont_e // TODO: Replace with WinGDI.h / RGBQUAD struct bgra_t { +#ifdef __EMSCRIPTEN__ + uint8_t r; + uint8_t g; + uint8_t b; + uint8_t a; // reserved on Win32 +#else uint8_t b; uint8_t g; uint8_t r; uint8_t a; // reserved on Win32 +#endif }; struct WinBmpHeader_t diff --git a/source/frontends/common2/fileregistry.cpp b/source/frontends/common2/fileregistry.cpp index 1960e59a0..288a96499 100644 --- a/source/frontends/common2/fileregistry.cpp +++ b/source/frontends/common2/fileregistry.cpp @@ -36,6 +36,9 @@ namespace std::string getLocation() const override; + protected: + void synchronise() override; + private: const std::filesystem::path myFilename; }; @@ -53,6 +56,17 @@ namespace } } + void Configuration::synchronise() + { + try + { + saveToYamlFile(myFilename.string()); + } + catch (const std::exception &) + { + } + } + Configuration::~Configuration() { try diff --git a/source/frontends/common2/gnuframe.cpp b/source/frontends/common2/gnuframe.cpp index 388f7bdea..3a65a000e 100644 --- a/source/frontends/common2/gnuframe.cpp +++ b/source/frontends/common2/gnuframe.cpp @@ -76,7 +76,11 @@ namespace common2 : CommonFrame(options) { // should this go down to LinuxFrame (maybe Initialisation?) +#ifndef __EMSCRIPTEN__ g_sProgramDir = getResourceFolder("bin").string() + PATH_SEPARATOR; +#else + g_sProgramDir = "/home/web_user/disks/"; +#endif LogFileOutput("Program Dir: '%s'\n", g_sProgramDir.c_str()); } diff --git a/source/frontends/common2/ptreeregistry.cpp b/source/frontends/common2/ptreeregistry.cpp index 474892e36..e50bd9bb9 100644 --- a/source/frontends/common2/ptreeregistry.cpp +++ b/source/frontends/common2/ptreeregistry.cpp @@ -34,11 +34,13 @@ namespace common2 void PTreeRegistry::putString(const std::string §ion, const std::string &key, const std::string &value) { myData[section][key] = value; + synchronise(); } void PTreeRegistry::putDWord(const std::string §ion, const std::string &key, const uint32_t value) { myData[section][key] = std::to_string(value); + synchronise(); } const std::map> &PTreeRegistry::getAllValues() const diff --git a/source/frontends/common2/utils.cpp b/source/frontends/common2/utils.cpp index 373f9fdf3..d69607508 100644 --- a/source/frontends/common2/utils.cpp +++ b/source/frontends/common2/utils.cpp @@ -5,15 +5,29 @@ #include "SaveState.h" #include "Registry.h" +#include + namespace { - std::string getEnvOrDefault(const char *var, const char *fallback = nullptr) + std::optional tryGetEnv(const char *var) { const char *value = getenv(var); if (value) { - return value; + std::cout << "Environment variable " << var << " = " << value << std::endl; + return std::string(value); + } + std::cout << "Environment variable " << var << " missing" << std::endl; + return std::nullopt; + } + + std::string getEnvOrDefault(const char *var, const char *fallback = nullptr) + { + std::optional value = tryGetEnv(var); + if (value.has_value()) + { + return *value; } if (fallback) { @@ -34,10 +48,18 @@ namespace common2 return profile; #else // https://specifications.freedesktop.org/basedir-spec/latest/ - const std::filesystem::path home = getHomeDir(); - const std::filesystem::path config = getEnvOrDefault("XDG_CONFIG_HOME", ".config"); +#ifdef __EMSCRIPTEN__ + const std::optional xdgConfigHome = "/storage/.config"; +#else + const std::optional xdgConfigHome = tryGetEnv("XDG_CONFIG_HOME"); +#endif + if (xdgConfigHome.has_value()) + { + return *xdgConfigHome; + } - return home / config; + const std::filesystem::path home = getHomeDir(); + return home / ".config"; #endif } diff --git a/source/frontends/sdl/CMakeLists.txt b/source/frontends/sdl/CMakeLists.txt index 051cf6f68..0e7d657cc 100644 --- a/source/frontends/sdl/CMakeLists.txt +++ b/source/frontends/sdl/CMakeLists.txt @@ -10,14 +10,21 @@ option(SA2_SDL3 "Use SDL3" OFF) if (SA2_SDL3) message("sa2: using SDL3") + find_package(SDL3 REQUIRED) - find_package(SDL3_image REQUIRED) target_link_libraries(sa2 PRIVATE SDL3::SDL3 - SDL3_image::SDL3_image ) + if (NOT EMSCRIPTEN) + find_package(SDL3_image REQUIRED) + + target_link_libraries(sa2 PRIVATE + SDL3_image::SDL3_image + ) + endif() + target_sources(sa2 PRIVATE ${IMGUI_PATH}/backends/imgui_impl_sdl3.cpp ) @@ -25,6 +32,11 @@ if (SA2_SDL3) target_compile_definitions(sa2 PRIVATE SDL_ENABLE_OLD_NAMES) else() message("sa2: using SDL2") + + if (EMSCRIPTEN) + message(FATAL_ERROR "Emscripten only supports SDL3") + endif() + find_package(SDL2 REQUIRED) # we should use find_package, but Ubuntu does not provide it for SDL2_image pkg_search_module(SDL2_IMAGE REQUIRED SDL2_image) @@ -67,6 +79,7 @@ endif() set(SOURCE_FILES + em_js.cpp main.cpp gamepad.cpp sdirectsound.cpp @@ -74,6 +87,7 @@ set(SOURCE_FILES sdlframe.cpp processfile.cpp sdlcompat.cpp + sdlappmain.cpp renderer/sdlrendererframe.cpp ) @@ -84,6 +98,7 @@ set(HEADER_FILES sdlframe.h processfile.h sdlcompat.h + sdlappmain.h renderer/sdlrendererframe.h ) @@ -153,6 +168,22 @@ target_compile_definitions(sa2 PRIVATE IMGUI_USER_CONFIG="frontends/sdl/imgui/sa2_imconfig.h" ) +if (EMSCRIPTEN) + set_target_properties(sa2 PROPERTIES + SUFFIX ".html" + ) + + set(EMSC_DIR ${CMAKE_CURRENT_SOURCE_DIR}/emsc) + + target_link_options(sa2 PRIVATE + -sFULL_ES3=1 + -sMIN_WEBGL_VERSION=2 + "SHELL:-lidbfs.js" + "SHELL:--shell-file ${EMSC_DIR}/src/a2e.html" + "SHELL:--preload-file ${EMSC_DIR}/fs@/" + ) +endif() + configure_file(sa2_config.h.in sa2_config.h) install(TARGETS sa2 diff --git a/source/frontends/sdl/em_js.cpp b/source/frontends/sdl/em_js.cpp new file mode 100644 index 000000000..ba74eba29 --- /dev/null +++ b/source/frontends/sdl/em_js.cpp @@ -0,0 +1,32 @@ +#ifdef __EMSCRIPTEN__ + +#include "frontends/sdl/sdlcompat.h" +#include + +namespace +{ + void push_simple_event(SDL_EventType type) + { + SDL_Event e; + SDL_zero(e); + e.type = type; + SDL_PushEvent(&e); + } + +} // namespace + +extern "C" EMSCRIPTEN_KEEPALIVE void sdl_dropfile(const char *filename) +{ + push_simple_event(SDL_DROPBEGIN); + + SDL_Event e; + SDL_zero(e); + e.type = SDL_DROPFILE; + e.drop.data = SDL_strdup(filename); // SDL3 will free this memory + printf("[DND] pushing SDL_DROPFILE event for '%s'\n", filename); + SDL_PushEvent(&e); + + push_simple_event(SDL_DROPCOMPLETE); +} + +#endif diff --git a/source/frontends/sdl/emsc/fs/defaults/.config/applewin/applewin.yaml b/source/frontends/sdl/emsc/fs/defaults/.config/applewin/applewin.yaml new file mode 100644 index 000000000..4fe5b039c --- /dev/null +++ b/source/frontends/sdl/emsc/fs/defaults/.config/applewin/applewin.yaml @@ -0,0 +1,21 @@ +--- +Configuration: + Emulation Speed: 10 +Configuration\Slot 0: + Card type: 17 +Configuration\Slot 1: + Card type: 0 +Configuration\Slot 2: + Card type: 0 +Configuration\Slot 3: + Card type: 0 + Uthernet Interface: +Configuration\Slot 6: + Card type: 1 + Last Disk Image 1: /disks/DOS 3.3 System Master - 680-0210-A.dsk + Last Disk Image 2: /disks/ProDOS_2_4_3.po +Configuration\Slot Auxiliary: + Card type: 13 + Number of Banks: 1 +Preferences: + Starting Directory: /disks/ diff --git a/source/frontends/sdl/emsc/fs/defaults/.config/applewin/imgui.ini b/source/frontends/sdl/emsc/fs/defaults/.config/applewin/imgui.ini new file mode 100644 index 000000000..962fd9dcf --- /dev/null +++ b/source/frontends/sdl/emsc/fs/defaults/.config/applewin/imgui.ini @@ -0,0 +1,24 @@ +[Window][Debug##Default] +Pos=60,60 +Size=400,400 + +[Window][Shortcuts] +Pos=60,60 +Size=1148,656 + +[Window][Settings] +Pos=60,60 +Size=1199,504 + +[Window][Memory viewer] +Pos=60,60 +Size=772,492 + +[Window][Memory editor] +Pos=60,60 +Size=924,672 + +[Window][Debugger] +Pos=60,60 +Size=1077,799 + diff --git a/source/frontends/sdl/emsc/fs/disks/A2_BASIC.SYM b/source/frontends/sdl/emsc/fs/disks/A2_BASIC.SYM new file mode 120000 index 000000000..58a5a6f59 --- /dev/null +++ b/source/frontends/sdl/emsc/fs/disks/A2_BASIC.SYM @@ -0,0 +1 @@ +../../../../../../bin/A2_BASIC.SYM \ No newline at end of file diff --git a/source/frontends/sdl/emsc/fs/disks/APPLE2E.SYM b/source/frontends/sdl/emsc/fs/disks/APPLE2E.SYM new file mode 120000 index 000000000..dbd033795 --- /dev/null +++ b/source/frontends/sdl/emsc/fs/disks/APPLE2E.SYM @@ -0,0 +1 @@ +../../../../../../bin/APPLE2E.SYM \ No newline at end of file diff --git a/source/frontends/sdl/emsc/fs/disks/BLANK.DSK b/source/frontends/sdl/emsc/fs/disks/BLANK.DSK new file mode 120000 index 000000000..6fa5df75e --- /dev/null +++ b/source/frontends/sdl/emsc/fs/disks/BLANK.DSK @@ -0,0 +1 @@ +../../../../../../bin/BLANK.DSK \ No newline at end of file diff --git a/source/frontends/sdl/emsc/fs/disks/DOS 3.3 System Master - 680-0210-A.dsk b/source/frontends/sdl/emsc/fs/disks/DOS 3.3 System Master - 680-0210-A.dsk new file mode 120000 index 000000000..699f90a72 --- /dev/null +++ b/source/frontends/sdl/emsc/fs/disks/DOS 3.3 System Master - 680-0210-A.dsk @@ -0,0 +1 @@ +../../../../../../bin/DOS 3.3 System Master - 680-0210-A.dsk \ No newline at end of file diff --git a/source/frontends/sdl/emsc/fs/disks/applewriterii.dsk b/source/frontends/sdl/emsc/fs/disks/applewriterii.dsk new file mode 100644 index 000000000..7ca75546d Binary files /dev/null and b/source/frontends/sdl/emsc/fs/disks/applewriterii.dsk differ diff --git a/source/frontends/sdl/emsc/src/a2e.html b/source/frontends/sdl/emsc/src/a2e.html new file mode 100644 index 000000000..790894b23 --- /dev/null +++ b/source/frontends/sdl/emsc/src/a2e.html @@ -0,0 +1,36 @@ + + + + + + AppleWin Emscripten + + + + + + + + + {{{ SCRIPT }}} + + + + \ No newline at end of file diff --git a/source/frontends/sdl/emsc/src/dragdrop.js b/source/frontends/sdl/emsc/src/dragdrop.js new file mode 100644 index 000000000..53dce8a48 --- /dev/null +++ b/source/frontends/sdl/emsc/src/dragdrop.js @@ -0,0 +1,65 @@ +Module.init_dragdrop = function () { + console.log('[DND] Drag & drop initialized'); + + function prevent(e) { + e.preventDefault(); + e.stopPropagation(); + return false; + } + + // Convert JS string to UTF8 pointer for WASM + function cstr(str) { + const len = lengthBytesUTF8(str) + 1; + const ptr = _malloc(len); + stringToUTF8(str, ptr, len); + return ptr; + } + + const DROP_PATH = '/disks'; + + // Ensure drop directory exists in MEMFS + if (!FS.analyzePath(DROP_PATH).exists) { + FS.mkdir(DROP_PATH); + console.log('[DND] Created ' + DROP_PATH + ' directory in MEMFS'); + } + + // Attach global handlers + document.addEventListener('dragenter', prevent, false); + document.addEventListener('dragover', prevent, false); + document.addEventListener('dragleave', prevent, false); + + document.addEventListener('drop', (e) => { + prevent(e); + + const files = e.dataTransfer.files; + console.log('[DND] drop event, files:', files.length); + + for (let i = 0; i < files.length; ++i) { + const file = files[i]; + console.log('[DND] dropped file:', file.name, file.size, 'bytes'); + + const reader = new FileReader(); + reader.onload = function (evt) { + const data = new Uint8Array(evt.target.result); + + // MEMFS path + const path = DROP_PATH + '/' + file.name; + + try { + FS.writeFile(path, data); + console.log('[DND] saved to MEMFS:', path); + + // Push SDL_DROPFILE event + const pathPtr = cstr(path); + Module._sdl_dropfile(pathPtr); + _free(pathPtr); + + } catch (err) { + console.error('[DND] MEMFS write failed:', err); + } + }; + + reader.readAsArrayBuffer(file); + } + }, false); +}; diff --git a/source/frontends/sdl/imgui/glselector.h b/source/frontends/sdl/imgui/glselector.h index 39bb7a0eb..d8e347a85 100644 --- a/source/frontends/sdl/imgui/glselector.h +++ b/source/frontends/sdl/imgui/glselector.h @@ -2,7 +2,20 @@ #include "frontends/sdl/sdlcompat.h" -#if defined(IMGUI_IMPL_OPENGL_ES2) +#if defined __EMSCRIPTEN__ + +#include + +#define SA2_CONTEXT_FLAGS 0 +#define SA2_CONTEXT_PROFILE_MASK SDL_GL_CONTEXT_PROFILE_ES +#define SA2_CONTEXT_MAJOR_VERSION 3 +#define SA2_CONTEXT_MINOR_VERSION 0 + +// this is defined in gl2ext.h and nowhere in gl3.h +#define SA2_IMAGE_FORMAT_INTERNAL GL_RGBA8 +#define SA2_IMAGE_FORMAT GL_RGBA + +#elif defined(IMGUI_IMPL_OPENGL_ES2) // Pi3 with Fake KMS // "OpenGL ES 2.0 Mesa 19.3.2" @@ -17,7 +30,7 @@ #define SA2_CONTEXT_MINOR_VERSION 0 // this is defined in gl2ext.h and nowhere in gl3.h -#define SA2_IMAGE_FORMAT_INTERNAL GL_BGRA_EXT +#define SA2_IMAGE_FORMAT_INTERNAL GL_RGBA #define SA2_IMAGE_FORMAT GL_BGRA_EXT #elif defined(IMGUI_IMPL_OPENGL_ES3) @@ -33,7 +46,14 @@ // "310 es" is accepted on a Pi4, but the imgui shaders do not compile #include -#include + +#ifndef GL_BGRA +#ifdef GL_BGRA_EXT +#define GL_BGRA GL_BGRA_EXT +#else +#define GL_BGRA 0x80E1 +#endif +#endif #define SA2_CONTEXT_FLAGS 0 #define SA2_CONTEXT_PROFILE_MASK SDL_GL_CONTEXT_PROFILE_ES @@ -41,8 +61,8 @@ #define SA2_CONTEXT_MINOR_VERSION 0 // this is defined in gl2ext.h and nowhere in gl3.h -#define SA2_IMAGE_FORMAT_INTERNAL GL_BGRA_EXT -#define SA2_IMAGE_FORMAT GL_BGRA_EXT +#define SA2_IMAGE_FORMAT_INTERNAL GL_RGBA8 +#define SA2_IMAGE_FORMAT GL_BGRA #elif defined(__APPLE__) @@ -51,7 +71,7 @@ #define SA2_CONTEXT_MAJOR_VERSION 3 #define SA2_CONTEXT_MINOR_VERSION 2 -#define SA2_IMAGE_FORMAT_INTERNAL GL_RGBA +#define SA2_IMAGE_FORMAT_INTERNAL GL_RGBA8 #define SA2_IMAGE_FORMAT GL_BGRA #else @@ -61,7 +81,7 @@ #define SA2_CONTEXT_MAJOR_VERSION 3 #define SA2_CONTEXT_MINOR_VERSION 2 -#define SA2_IMAGE_FORMAT_INTERNAL GL_RGBA +#define SA2_IMAGE_FORMAT_INTERNAL GL_RGBA8 #define SA2_IMAGE_FORMAT GL_BGRA #endif diff --git a/source/frontends/sdl/imgui/image.cpp b/source/frontends/sdl/imgui/image.cpp index da32f5cb0..5adcf51f5 100644 --- a/source/frontends/sdl/imgui/image.cpp +++ b/source/frontends/sdl/imgui/image.cpp @@ -30,24 +30,29 @@ namespace sa2 void allocateTexture(GLuint texture, size_t width, size_t height) { glBindTexture(GL_TEXTURE_2D, texture); + glPixelStorei(GL_UNPACK_ALIGNMENT, 1); - const GLenum type = GL_UNSIGNED_BYTE; - glTexImage2D(GL_TEXTURE_2D, 0, SA2_IMAGE_FORMAT_INTERNAL, width, height, 0, SA2_IMAGE_FORMAT, type, nullptr); - } - - void loadTextureFromData(GLuint texture, const uint8_t *data, size_t width, size_t height, size_t pitch) - { - glBindTexture(GL_TEXTURE_2D, texture); - glPixelStorei(UGL_UNPACK_LENGTH, pitch); // in pixels +#if defined(__EMSCRIPTEN__) || defined(GL_ES_VERSION_3_0) + glTexStorage2D(GL_TEXTURE_2D, 1, SA2_IMAGE_FORMAT_INTERNAL, width, height); +#else + glTexImage2D( + GL_TEXTURE_2D, 0, SA2_IMAGE_FORMAT_INTERNAL, width, height, 0, SA2_IMAGE_FORMAT, GL_UNSIGNED_BYTE, nullptr); +#endif // Setup filtering parameters for display glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); + } + + void loadTextureFromData(GLuint texture, const uint8_t *data, size_t width, size_t height, size_t pitchPixels) + { + glBindTexture(GL_TEXTURE_2D, texture); + glPixelStorei(GL_UNPACK_ALIGNMENT, 1); - const GLenum type = GL_UNSIGNED_BYTE; - glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, width, height, SA2_IMAGE_FORMAT, type, data); + glPixelStorei(UGL_UNPACK_LENGTH, pitchPixels); // in pixels + glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, width, height, SA2_IMAGE_FORMAT, GL_UNSIGNED_BYTE, data); // reset to default state glPixelStorei(UGL_UNPACK_LENGTH, 0); } diff --git a/source/frontends/sdl/imgui/sdlimguiframe.cpp b/source/frontends/sdl/imgui/sdlimguiframe.cpp index 037fa7304..c2eb4ac3a 100644 --- a/source/frontends/sdl/imgui/sdlimguiframe.cpp +++ b/source/frontends/sdl/imgui/sdlimguiframe.cpp @@ -238,6 +238,17 @@ namespace sa2 if (!myPresenting) { myPresenting = true; + + int w, h; + +#if SDL_VERSION_ATLEAST(3, 0, 0) + SDL_GetWindowSizeInPixels(myWindow.get(), &w, &h); +#else + SDL_GL_GetDrawableSize(myWindow.get(), &w, &h); +#endif + + glViewport(0, 0, w, h); + ImGui_ImplOpenGL3_NewFrame(); ImGui_ImplSDLX_NewFrame(); ImGui::NewFrame(); diff --git a/source/frontends/sdl/main.cpp b/source/frontends/sdl/main.cpp index 0a1132d4b..61d6fa0ba 100644 --- a/source/frontends/sdl/main.cpp +++ b/source/frontends/sdl/main.cpp @@ -1,193 +1,64 @@ -#include -#include -#include +#include "frontends/sdl/sdlappmain.h" -#include "StdAfx.h" -#include "linux/benchmark.h" -#include "linux/context.h" -#include "linux/version.h" - -#include "frontends/common2/fileregistry.h" -#include "frontends/common2/commoncontext.h" -#include "frontends/common2/argparser.h" -#include "frontends/common2/programoptions.h" -#include "frontends/common2/timer.h" -#include "frontends/sdl/gamepad.h" -#include "frontends/sdl/sdirectsound.h" -#include "frontends/sdl/sdlcompat.h" -#include "frontends/sdl/utils.h" - -#include "frontends/sdl/renderer/sdlrendererframe.h" -#include "frontends/sdl/imgui/sdlimguiframe.h" +#if SDL_VERSION_ATLEAST(3, 0, 0) -#include "Core.h" -#include "NTSC.h" -#include "Interface.h" +#define SDL_MAIN_USE_CALLBACKS +#include -// comment out to test / debug init / shutdown only -#define EMULATOR_RUN +#else -namespace +int main(int argc, char **argv) { + void *appstate = nullptr; + SDL_AppResult final_result = SDL_APP_FAILURE; - int getRefreshRate() + const Uint32 flags = SDL_INIT_VIDEO | SA2_INIT_GAMEPAD | SDL_INIT_AUDIO | SDL_INIT_EVENTS | SDL_INIT_JOYSTICK; + if (SDL_Init(flags) != 0) { - SDL_DisplayMode dummy; - const SDL_DisplayMode *current = sa2::compat::getCurrentDisplayMode(dummy); - return current->refresh_rate ? current->refresh_rate : 60; + SDL_Log("SDL_Init failed: %s", SDL_GetError()); + return 1; } - struct Data + SDL_AppResult rc = SDL_AppInit(&appstate, argc, argv); + if (rc != SDL_APP_CONTINUE) { - sa2::SDLFrame *frame; - SDL_mutex *mutex; - common2::Timer *timer; - }; - -} // namespace - -void run_sdl(int argc, char *const argv[]) -{ - common2::EmulatorOptions options; - - const bool run = getEmulatorOptions(argc, argv, common2::OptionsType::sa2, "SDL2", options); - - if (!run) - return; - - std::cerr << std::fixed << std::setprecision(2); - - sa2::printVideoInfo(std::cerr); - sa2::printAudioInfo(std::cerr); - - const LoggerContext logger(options.log); - const RegistryContext registryContext(CreateFileRegistry(options)); - const std::shared_ptr paddle = - sa2::Gamepad::create(options.gameControllerIndex, options.gameControllerMappingFile); - - sa2::setAudioOptions(options); - - std::shared_ptr frame; - if (options.imgui) - { - frame = std::make_shared(options); - } - else - { - frame = std::make_shared(options); + final_result = rc; + SDL_AppQuit(appstate, final_result); + SDL_Quit(); + return (rc == SDL_APP_SUCCESS) ? 0 : 1; } - std::cerr << "GL swap interval: " << sa2::compat::getGLSwapInterval() << std::endl; - - const common2::CommonInitialisation init(frame, paddle, options); - - const int fps = getRefreshRate(); - std::cerr << "Video refresh rate: " << fps << " Hz, " << 1000.0 / fps << " ms" << std::endl; - -#ifdef EMULATOR_RUN - if (options.benchmark) - { - // we need to switch off vsync, otherwise FPS is limited to 60 - // and it will take longer to run - sa2::SDLFrame::setGLSwapInterval(0); - - const auto redraw = [&frame] { frame->VideoPresentScreen(); }; - - Video &video = GetVideo(); - const auto refresh = [redraw, &video] - { - NTSC_SetVideoMode(video.GetVideoMode()); - NTSC_VideoRedrawWholeScreen(); - redraw(); - }; - - VideoBenchmark(redraw, refresh); - } - else + bool running = true; + while (running) { - common2::Timer global; - common2::Timer refreshScreenTimer; - common2::Timer cpuTimer; - common2::Timer eventTimer; - common2::Timer frameTimer; - - const std::string globalTag = ". ."; - std::string updateTextureTimerTag, refreshScreenTimerTag, cpuTimerTag, eventTimerTag; - - // it does not need to be exact - const int64_t oneFrameMicros = 1000000 / fps; - - bool quit = false; - - do + SDL_Event event; + while (SDL_PollEvent(&event)) { - frameTimer.tic(); - - eventTimer.tic(); - frame->ProcessEvents(quit); - eventTimer.toc(); - - cpuTimer.tic(); - frame->ExecuteOneFrame(oneFrameMicros); - cpuTimer.toc(); - - if (!options.headless) + rc = SDL_AppEvent(appstate, &event); + if (rc != SDL_APP_CONTINUE) { - refreshScreenTimer.tic(); - if (g_bFullSpeed) - { - frame->VideoRedrawScreenDuringFullSpeed(g_dwCyclesThisFrame); - } - else - { - frame->SyncVideoPresentScreen(oneFrameMicros); - } - refreshScreenTimer.toc(); + final_result = rc; + running = false; + break; } + } - frameTimer.toc(); - } while (!quit && !frame->Quit()); - - global.toc(); - - std::cerr << "Global: " << global << std::endl; - std::cerr << "Frame: " << frameTimer << std::endl; - std::cerr << "Screen: " << refreshScreenTimer << std::endl; - std::cerr << "Events: " << eventTimer << std::endl; - std::cerr << "CPU: " << cpuTimer << std::endl; - } -#endif -} - -int main(int argc, char *argv[]) -{ -#if SDL_VERSION_ATLEAST(3, 0, 0) - const std::string version = getVersion(); - SDL_SetAppMetadata("AppleWin", version.c_str(), "org.applewin"); -#endif - - // First we need to start up SDL, and make sure it went ok - const Uint32 flags = SDL_INIT_VIDEO | SA2_INIT_GAMEPAD | SDL_INIT_AUDIO | SDL_INIT_EVENTS | SDL_INIT_JOYSTICK; - - if (!SA2_OK(SDL_Init(flags))) - { - std::cerr << "SDL_Init Error: " << SDL_GetError() << std::endl; - return 1; - } - - int exit = 0; + if (!running) + { + break; + } - try - { - run_sdl(argc, argv); - } - catch (const std::exception &e) - { - exit = 2; - std::cerr << e.what() << std::endl; + rc = SDL_AppIterate(appstate); + if (rc != SDL_APP_CONTINUE) + { + final_result = rc; + break; + } } + SDL_AppQuit(appstate, final_result); SDL_Quit(); - - return exit; + return (final_result == SDL_APP_SUCCESS) ? 0 : 1; } + +#endif diff --git a/source/frontends/sdl/sdlappmain.cpp b/source/frontends/sdl/sdlappmain.cpp new file mode 100644 index 000000000..5e7c4e341 --- /dev/null +++ b/source/frontends/sdl/sdlappmain.cpp @@ -0,0 +1,288 @@ +#include "StdAfx.h" +#include "linux/benchmark.h" +#include "linux/context.h" +#include "linux/version.h" + +#include "frontends/common2/fileregistry.h" +#include "frontends/common2/commoncontext.h" +#include "frontends/common2/argparser.h" +#include "frontends/common2/programoptions.h" +#include "frontends/common2/timer.h" +#include "frontends/sdl/gamepad.h" +#include "frontends/sdl/sdirectsound.h" +#include "frontends/sdl/sdlcompat.h" +#include "frontends/sdl/utils.h" + +#include "frontends/sdl/renderer/sdlrendererframe.h" +#include "frontends/sdl/imgui/sdlimguiframe.h" + +#include "Core.h" +#include "NTSC.h" +#include "Interface.h" + +#include +#include + +// comment out to test / debug init / shutdown only +#define EMULATOR_RUN + +namespace +{ + + void setupPersistentConfig() + { + namespace fs = std::filesystem; + + const fs::path storage_path = "/storage/.config/applewin"; + const fs::path default_path = "/defaults/.config/applewin"; + const std::vector config_files = {"applewin.yaml", "imgui.ini"}; + + // Ensure the destination directory exists in IDBFS + if (!fs::exists(storage_path)) + { + fs::create_directories(storage_path); + } + + for (const auto &file : config_files) + { + const auto dst = storage_path / file; + const auto src = default_path / file; + + if (!fs::exists(dst)) + { + std::cerr << "Restoring default config: " << file << std::endl; + std::error_code ec; + fs::copy_file(src, dst, fs::copy_options::overwrite_existing, ec); + if (ec) + { + std::cerr << "Failed to copy " << file << ": " << ec.message() << std::endl; + } + } + } + } + + int getRefreshRate() + { + SDL_DisplayMode dummy; + const SDL_DisplayMode *current = sa2::compat::getCurrentDisplayMode(dummy); + return current->refresh_rate ? current->refresh_rate : 60; + } + + struct AppData + { + const common2::EmulatorOptions myOptions; + const LoggerContext myLogger; + const RegistryContext mtRegistryContext; + const std::shared_ptr myPaddle; + + std::shared_ptr myFrame; + std::shared_ptr myInit; + + AppData(const common2::EmulatorOptions &options) + : myOptions(options) + , myLogger(options.log) + , mtRegistryContext(CreateFileRegistry(options)) + , myPaddle(sa2::Gamepad::create(options.gameControllerIndex, options.gameControllerMappingFile)) + { + if (options.imgui) + { + myFrame = std::make_shared(options); + } + else + { + myFrame = std::make_shared(options); + } + +#ifdef __EMSCRIPTEN__ + SDL_SetHint(SDL_HINT_EMSCRIPTEN_KEYBOARD_ELEMENT, "#canvas"); +#endif + std::cerr << "GL swap interval: " << sa2::compat::getGLSwapInterval() << std::endl; + + myInit = std::make_shared(myFrame, myPaddle, options); + } + }; + + class AppState + { + private: + const std::shared_ptr myData; + const int64_t myOneFrameMicros; + + common2::Timer myGlobal; + common2::Timer myRefreshScreenTimer; + common2::Timer myCpuTimer; + common2::Timer myFrameTimer; + + public: + AppState(const std::shared_ptr &data, const int fps) + : myData(data) + , myOneFrameMicros(1000000 / fps) + { + } + + SDL_AppResult loop() + { + myFrameTimer.tic(); + + myCpuTimer.tic(); + myData->myFrame->ExecuteOneFrame(myOneFrameMicros); + myCpuTimer.toc(); + + if (!myData->myOptions.headless) + { + myRefreshScreenTimer.tic(); + if (g_bFullSpeed) + { + myData->myFrame->VideoRedrawScreenDuringFullSpeed(g_dwCyclesThisFrame); + } + else + { + myData->myFrame->SyncVideoPresentScreen(myOneFrameMicros); + } + myRefreshScreenTimer.toc(); + } + + myFrameTimer.toc(); + const bool quit = myData->myFrame->Quit(); + return quit ? SDL_APP_SUCCESS : SDL_APP_CONTINUE; + } + + SDL_AppResult event(const SDL_Event &e) + { + bool quit = false; + myData->myFrame->ProcessSingleEvent(e, quit); + return quit ? SDL_APP_SUCCESS : SDL_APP_CONTINUE; + } + + void end() + { + myGlobal.toc(); + + std::cerr << "Global: " << myGlobal << std::endl; + std::cerr << "Frame: " << myFrameTimer << std::endl; + std::cerr << "Screen: " << myRefreshScreenTimer << std::endl; + std::cerr << "CPU: " << myCpuTimer << std::endl; + } + }; + + SDL_AppResult SDL_AppCreate(void **appstate, int argc, char **argv) + { + common2::EmulatorOptions options; + const bool run = getEmulatorOptions(argc, argv, common2::OptionsType::sa2, "SDL2", options); + if (!run) + { + return SDL_APP_SUCCESS; + } + + std::cerr << std::fixed << std::setprecision(2); + + sa2::printVideoInfo(std::cerr); + sa2::printAudioInfo(std::cerr); + sa2::setAudioOptions(options); + + std::shared_ptr data = std::make_shared(options); + + const int fps = getRefreshRate(); + std::cerr << "Video refresh rate: " << fps << " Hz, " << 1000.0 / fps << " ms" << std::endl; + +#ifdef EMULATOR_RUN + if (options.benchmark) + { + // we need to switch off vsync, otherwise FPS is limited to 60 + // and it will take longer to run + sa2::SDLFrame::setGLSwapInterval(0); + + const auto redraw = [&data] { data->myFrame->VideoPresentScreen(); }; + + Video &video = GetVideo(); + const auto refresh = [redraw, &video] + { + NTSC_SetVideoMode(video.GetVideoMode()); + NTSC_VideoRedrawWholeScreen(); + redraw(); + }; + + VideoBenchmark(redraw, refresh); + return SDL_APP_SUCCESS; + } + else + { + *appstate = new AppState(data, fps); + return SDL_APP_CONTINUE; + } +#else + return SDL_APP_SUCCESS; +#endif + } + +} // namespace + +extern "C" +{ + SDL_AppResult SDL_AppInit(void **appstate, int argc, char **argv) + { + try + { +#if SDL_VERSION_ATLEAST(3, 0, 0) + // SDL_INIT_AUDIO is needed or it does not work + // SDL_INIT_VIDEO allows the diagnostics to work + if (!SA2_OK(SDL_InitSubSystem(SDL_INIT_AUDIO | SDL_INIT_VIDEO))) + { + throw std::runtime_error(sa2::decorateSDLError("SDL_InitSubSystem")); + } + + const std::string version = getVersion(); + SDL_SetAppMetadata("AppleWin", version.c_str(), "org.applewin"); +#endif + +#ifdef __EMSCRIPTEN__ + setupPersistentConfig(); +#endif + + return SDL_AppCreate(appstate, argc, argv); + } + catch (const std::exception &e) + { + std::cerr << e.what() << std::endl; + return SDL_APP_FAILURE; + } + } + + SDL_AppResult SDL_AppEvent(void *appstate, SDL_Event *event) + { + try + { + AppState *state = static_cast(appstate); + return state->event(*event); + } + catch (const std::exception &e) + { + std::cerr << e.what() << std::endl; + return SDL_APP_FAILURE; + } + } + + SDL_AppResult SDL_AppIterate(void *appstate) + { + try + { + AppState *state = static_cast(appstate); + return state->loop(); + } + catch (const std::exception &e) + { + std::cerr << e.what() << std::endl; + return SDL_APP_FAILURE; + } + } + + void SDL_AppQuit(void *appstate, SDL_AppResult result) + { + AppState *state = static_cast(appstate); + if (state) + { + state->end(); + delete state; + } + } +} diff --git a/source/frontends/sdl/sdlappmain.h b/source/frontends/sdl/sdlappmain.h new file mode 100644 index 000000000..e4ce750af --- /dev/null +++ b/source/frontends/sdl/sdlappmain.h @@ -0,0 +1,11 @@ +#pragma once + +#include "frontends/sdl/sdlcompat.h" + +extern "C" +{ + SDL_AppResult SDL_AppInit(void **appstate, int argc, char **argv); + SDL_AppResult SDL_AppEvent(void *appstate, SDL_Event *event); + SDL_AppResult SDL_AppIterate(void *appstate); + void SDL_AppQuit(void *appstate, SDL_AppResult result); +} diff --git a/source/frontends/sdl/sdlcompat.cpp b/source/frontends/sdl/sdlcompat.cpp index d6c46c83c..dc463d65a 100644 --- a/source/frontends/sdl/sdlcompat.cpp +++ b/source/frontends/sdl/sdlcompat.cpp @@ -87,12 +87,13 @@ namespace sa2 SDL_Renderer *createRenderer(SDL_Window *window, const int index) { - // I am not sure whether we should worry about SDL_PROP_RENDERER_CREATE_PRESENT_VSYNC_NUMBER - const char *name = index >= 0 ? SDL_GetRenderDriver(index) : nullptr; - SDL_PropertiesID props = SDL_CreateProperties(); SDL_SetPointerProperty(props, SDL_PROP_RENDERER_CREATE_WINDOW_POINTER, window); + +#ifndef __EMSCRIPTEN__ SDL_SetBooleanProperty(props, SDL_PROP_RENDERER_CREATE_PRESENT_VSYNC_NUMBER, true); +#endif + if (index >= 0) { SDL_SetStringProperty(props, SDL_PROP_RENDERER_CREATE_NAME_STRING, SDL_GetRenderDriver(index)); @@ -103,6 +104,7 @@ namespace sa2 return renderer; } +#ifndef __EMSCRIPTEN__ SDL_Surface *createSurfaceFromResource(const unsigned char *data, unsigned int size) { SDL_IOStream *ops = SDL_IOFromConstMem(data, size); @@ -117,6 +119,7 @@ namespace sa2 } return surface; } +#endif bool convertAudio( const SDL_AudioSpec &wavSpec, const Uint8 *wavBuffer, const Uint32 wavLength, std::vector &output) diff --git a/source/frontends/sdl/sdlcompat.h b/source/frontends/sdl/sdlcompat.h index 6e18f17c1..64e7e0586 100644 --- a/source/frontends/sdl/sdlcompat.h +++ b/source/frontends/sdl/sdlcompat.h @@ -11,7 +11,10 @@ #include #include + +#ifndef __EMSCRIPTEN__ #include +#endif #define ImGui_ImplSDLX_InitForOpenGL ImGui_ImplSDL3_InitForOpenGL #define ImGui_ImplSDLX_Shutdown ImGui_ImplSDL3_Shutdown @@ -64,6 +67,13 @@ typedef SDL_Rect Renderer_Rect_t; typedef SDL_PixelFormatEnum PixelFormat_t; typedef int Joystick_t; +enum SDL_AppResult +{ + SDL_APP_CONTINUE = 0, + SDL_APP_SUCCESS = 1, + SDL_APP_FAILURE = 2 +}; + #endif namespace common2 diff --git a/source/frontends/sdl/sdlframe.cpp b/source/frontends/sdl/sdlframe.cpp index bdfabb949..ad04ef313 100644 --- a/source/frontends/sdl/sdlframe.cpp +++ b/source/frontends/sdl/sdlframe.cpp @@ -241,6 +241,7 @@ namespace sa2 void SDLFrame::SetApplicationIcon() { +#ifndef __EMSCRIPTEN__ const auto resource = GetResourceData(IDC_APPLEWIN_ICON); std::shared_ptr icon( @@ -249,6 +250,7 @@ namespace sa2 { SDL_SetWindowIcon(myWindow.get(), icon.get()); } +#endif } const std::shared_ptr &SDLFrame::GetWindow() const @@ -266,15 +268,6 @@ namespace sa2 return IDOK; } - void SDLFrame::ProcessEvents(bool &quit) - { - SDL_Event e; - while (SDL_PollEvent(&e) != 0) - { - ProcessSingleEvent(e, quit); - } - } - void SDLFrame::ProcessSingleEvent(const SDL_Event &e, bool &quit) { switch (e.type) @@ -418,6 +411,7 @@ namespace sa2 void SDLFrame::ProcessDropEvent(const SDL_DropEvent &drop) { const auto file = SA2_DROP_FILE(drop); + printf("File dropped: %s\n", file); processFile(this, file, myDragAndDropSlot, myDragAndDropDrive); sa2::compat::maybeSDLfree(file); } diff --git a/source/frontends/sdl/sdlframe.h b/source/frontends/sdl/sdlframe.h index ddbed5693..2dff55c22 100644 --- a/source/frontends/sdl/sdlframe.h +++ b/source/frontends/sdl/sdlframe.h @@ -25,10 +25,9 @@ namespace sa2 std::shared_ptr CreateSoundBuffer( uint32_t dwBufferSize, uint32_t nSampleRate, int nChannels, const char *pszVoiceName) override; + virtual void ProcessSingleEvent(const SDL_Event &event, bool &quit); virtual bool Quit() const = 0; - void ProcessEvents(bool &quit); - void FrameResetMachineState(); const std::shared_ptr &GetWindow() const; @@ -53,7 +52,6 @@ namespace sa2 void SetApplicationIcon(); void SetGLSynchronisation(const common2::EmulatorOptions &options); - virtual void ProcessSingleEvent(const SDL_Event &event, bool &quit); virtual void GetRelativeMousePosition(const SDL_MouseMotionEvent &motion, float &x, float &y) const = 0; virtual void ProcessKeyDown(const SDL_KeyboardEvent &key, bool &quit); virtual void ToggleMouseCursor() = 0; diff --git a/source/linux/libwindows/misc.h b/source/linux/libwindows/misc.h index 62f6fa459..a08e164a7 100644 --- a/source/linux/libwindows/misc.h +++ b/source/linux/libwindows/misc.h @@ -29,7 +29,9 @@ #define IDYES 6 #define IDNO 7 +#ifndef EINVAL #define EINVAL 22 +#endif BOOL WINAPI PostMessage(HWND hWnd, UINT Msg, WPARAM wParam, LPARAM lParam); int MessageBox(HWND, const char *, const char *, UINT); diff --git a/source/linux/registryclass.cpp b/source/linux/registryclass.cpp index ad6d003a5..006f9159c 100644 --- a/source/linux/registryclass.cpp +++ b/source/linux/registryclass.cpp @@ -68,3 +68,8 @@ void RegSaveValue(LPCTSTR section, LPCTSTR key, BOOL peruser, uint32_t value) Registry::instance->putDWord(section, key, value); LogFileOutput("RegSaveValue: %s - %s = %d\n", section, key, value); } + +void Registry::synchronise() +{ + +} diff --git a/source/linux/registryclass.h b/source/linux/registryclass.h index a0b2309f6..a54738231 100644 --- a/source/linux/registryclass.h +++ b/source/linux/registryclass.h @@ -22,4 +22,6 @@ class Registry virtual const std::map> &getAllValues() const = 0; virtual std::string getLocation() const = 0; + + virtual void synchronise(); };