From 96ca3aa77c85c7a8fc49b204e9fd0819355651fa Mon Sep 17 00:00:00 2001 From: Un1q32 Date: Sat, 12 Sep 2026 14:29:11 -0400 Subject: [PATCH 1/7] remove the old web port because it sucks --- .github/workflows/build-android.yml | 42 ++++ .github/workflows/build-other.yml | 93 -------- CMakeLists.txt | 93 +------- src/web-meta/main.c | 71 ------- src/web/main.c | 319 ---------------------------- src/web/stb_impl.c | 9 - 6 files changed, 49 insertions(+), 578 deletions(-) create mode 100644 .github/workflows/build-android.yml delete mode 100644 .github/workflows/build-other.yml delete mode 100644 src/web-meta/main.c delete mode 100644 src/web/main.c delete mode 100644 src/web/stb_impl.c diff --git a/.github/workflows/build-android.yml b/.github/workflows/build-android.yml new file mode 100644 index 000000000..e6966c88c --- /dev/null +++ b/.github/workflows/build-android.yml @@ -0,0 +1,42 @@ +name: Other platform builds + +on: + workflow_call: + +jobs: + build-android: + name: Build (Android) + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@v7 + + - name: Set up JDK 25 + uses: actions/setup-java@v4 + with: + java-version: '25' + distribution: 'temurin' + cache: 'gradle' + cache-dependency-path: src/android/frontend/gradle/wrapper/gradle-wrapper.properties + + - name: Install ccache + run: sudo apt-get update && sudo apt-get install -y ccache + + - uses: actions/cache@v6 + with: + path: ~/.cache/ccache + key: ccache-android-${{ github.sha }} + restore-keys: | + ccache-android- + + - name: Build APK + run: | + cd src/android/frontend + ./gradlew assembleDebug \ + -Pandroid.cxx.ndk.ccache=true \ + -Porg.gradle.caching=true + + - name: Upload APK + uses: actions/upload-artifact@v7 + with: + name: butterscotch-android + path: src/android/frontend/app/build/outputs/apk/debug/app-debug.apk diff --git a/.github/workflows/build-other.yml b/.github/workflows/build-other.yml deleted file mode 100644 index 7a1519ab5..000000000 --- a/.github/workflows/build-other.yml +++ /dev/null @@ -1,93 +0,0 @@ -name: Other platform builds - -on: - workflow_call: - -jobs: - build-web: - name: Build (Web)${{ matrix.label }} - runs-on: ubuntu-24.04 - strategy: - fail-fast: false - matrix: - include: - - label: "" - platform: web - artifact: butterscotch-web - output_glob: build/butterscotch.* - - label: " [Meta]" - platform: web-meta - artifact: butterscotch-web-meta - output_glob: build/butterscotch-meta.* - steps: - - uses: actions/checkout@v7 - - - name: Get commit info - id: commit-info - run: | - echo "hash=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT - echo "date=$(git log -1 --format=%cd --date=short)" >> $GITHUB_OUTPUT - - - name: Setup Emscripten SDK - uses: mymindstorm/setup-emsdk@v14 - with: - version: latest - - - name: Install ccache - run: sudo apt-get update && sudo apt-get install -y ccache - - - uses: actions/cache@v6 - with: - path: ~/.cache/ccache - key: ccache-web-${{ matrix.platform }}-${{ github.sha }} - restore-keys: | - ccache-web-${{ matrix.platform }}- - - - name: Configure - run: emcmake cmake -B build -G Ninja -DWERROR=ON -DPLATFORM=${{ matrix.platform }} -DCMAKE_BUILD_TYPE=Release -DCMAKE_INTERPROCEDURAL_OPTIMIZATION=ON -DCMAKE_C_COMPILER_LAUNCHER=ccache -DCMAKE_CXX_COMPILER_LAUNCHER=ccache - - - name: Build - run: cmake --build build - - - name: Upload artifact - uses: actions/upload-artifact@v7 - with: - name: ${{ matrix.artifact }} - path: ${{ matrix.output_glob }} - - build-android: - name: Build (Android) - runs-on: ubuntu-24.04 - steps: - - uses: actions/checkout@v7 - - - name: Set up JDK 25 - uses: actions/setup-java@v4 - with: - java-version: '25' - distribution: 'temurin' - cache: 'gradle' - cache-dependency-path: src/android/frontend/gradle/wrapper/gradle-wrapper.properties - - - name: Install ccache - run: sudo apt-get update && sudo apt-get install -y ccache - - - uses: actions/cache@v6 - with: - path: ~/.cache/ccache - key: ccache-android-${{ github.sha }} - restore-keys: | - ccache-android- - - - name: Build APK - run: | - cd src/android/frontend - ./gradlew assembleDebug \ - -Pandroid.cxx.ndk.ccache=true \ - -Porg.gradle.caching=true - - - name: Upload APK - uses: actions/upload-artifact@v7 - with: - name: butterscotch-android - path: src/android/frontend/app/build/outputs/apk/debug/app-debug.apk \ No newline at end of file diff --git a/CMakeLists.txt b/CMakeLists.txt index 8de80dba2..4180b248b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -85,11 +85,7 @@ add_compile_definitions( # Audio backend defaults if(AUDIO_BACKEND STREQUAL "") - if(PLATFORM STREQUAL "web") - set(AUDIO_BACKEND "web") - elseif(PLATFORM STREQUAL "web-meta") - set(AUDIO_BACKEND "none") - elseif(PLATFORM STREQUAL "ps3" OR PLATFORM STREQUAL "vita" OR PLATFORM STREQUAL "switch") + if(PLATFORM STREQUAL "ps3" OR PLATFORM STREQUAL "vita" OR PLATFORM STREQUAL "switch") set(AUDIO_BACKEND "openal") elseif(PLATFORM STREQUAL "ps2") set(AUDIO_BACKEND "ps2") @@ -105,8 +101,6 @@ elseif(AUDIO_BACKEND STREQUAL "miniaudio") if(PLATFORM STREQUAL "vita") add_compile_definitions(MA_NO_RUNTIME_LINKING) endif() -elseif(AUDIO_BACKEND STREQUAL "web") - file(GLOB AUDIO_SOURCES src/audio/web/*.c src/audio/miniaudio/*.c) elseif(AUDIO_BACKEND STREQUAL "ps2") file(GLOB AUDIO_SOURCES src/audio/ps2/*.c) elseif(NOT AUDIO_BACKEND STREQUAL "none") @@ -158,21 +152,11 @@ if(ENABLE_NOOP_RENDERER) add_compile_definitions(ENABLE_NOOP_RENDERER) endif() -if(PLATFORM STREQUAL "web-meta") - # Metadata-only build: just the data.win parser and its direct deps. No VM, no runner, no I/O subsystems. - set(SOURCES - src/data_win.c - src/binary_reader.c - src/log.c - src/stb_ds.c - ) -else() - file(GLOB SOURCES src/*.c) - # These platforms haven't been converted to the refactored system yet. - # This list should NEVER grow, all new platforms should use the new system. - if(PLATFORM STREQUAL "ps2" OR PLATFORM STREQUAL "ps3" OR PLATFORM STREQUAL "web" OR PLATFORM STREQUAL "android") - list(REMOVE_ITEM SOURCES "${CMAKE_CURRENT_SOURCE_DIR}/src/loop.c") - endif() +file(GLOB SOURCES src/*.c) +# These platforms haven't been converted to the refactored system yet. +# This list should NEVER grow, all new platforms should use the new system. +if(PLATFORM STREQUAL "ps2" OR PLATFORM STREQUAL "ps3" OR PLATFORM STREQUAL "android") + list(REMOVE_ITEM SOURCES "${CMAKE_CURRENT_SOURCE_DIR}/src/loop.c") endif() # Platform specific files builds file(GLOB PLATFORM_SOURCES src/${PLATFORM}/*.c) @@ -205,11 +189,6 @@ elseif(AUDIO_BACKEND STREQUAL "miniaudio") if (NOT WIN32) set(AUDIO_LIBRARIES pthread) endif() -elseif(AUDIO_BACKEND STREQUAL "web") - target_include_directories(butterscotch SYSTEM PUBLIC vendor/miniaudio) - target_include_directories(butterscotch PUBLIC src/audio/web) - target_include_directories(butterscotch PUBLIC src/audio/miniaudio) - add_compile_definitions(USE_WEB_AUDIO) elseif(AUDIO_BACKEND STREQUAL "ps2") target_include_directories(butterscotch PUBLIC src/audio/ps2) add_compile_definitions(USE_PS2_AUDIO) @@ -260,7 +239,7 @@ if(ENABLE_VM_EXCEPTIONS_LOGS) add_compile_definitions(ENABLE_VM_EXCEPTIONS_LOGS) endif() -if(PLATFORM STREQUAL "cli" OR PLATFORM STREQUAL "vita" OR PLATFORM STREQUAL "switch") +if(PLATFORM STREQUAL "cli" OR PLATFORM STREQUAL "vita" OR PLATFORM STREQUAL "switch" OR PLATFORM STREQUAL "web") if(PLATFORM STREQUAL "vita") # Normally, __VITA__ should already be defined, but for some reason, # in my case, this didn't really work... so just in case, when we're @@ -466,64 +445,6 @@ if(PLATFORM STREQUAL "cli" OR PLATFORM STREQUAL "vita" OR PLATFORM STREQUAL "swi ${CMAKE_SOURCE_DIR}/vendor/gamecontrollerdb.txt $/gamecontrollerdb.txt ) -elseif(PLATFORM STREQUAL "web") - if (NOT ENABLE_MODERN_GL) - message(FATAL_ERROR "Web requires modern gl!") - endif() - file(GLOB GL_SOURCES src/gl/*.c src/gl_common/*.c src/image/*.c) - target_sources(butterscotch PRIVATE ${GL_SOURCES}) - target_include_directories(butterscotch PRIVATE ${CMAKE_SOURCE_DIR}/src/gl) - target_include_directories(butterscotch PRIVATE ${CMAKE_SOURCE_DIR}/src/gl_common) - target_include_directories(butterscotch PRIVATE ${CMAKE_SOURCE_DIR}/src/image) - - # stb_image - target_include_directories(butterscotch PUBLIC vendor/stb/image) - - # bzip2 - target_link_libraries(butterscotch PRIVATE bzip2 stb_ds sha1 stb_vorbis) - - set_target_properties(butterscotch PROPERTIES SUFFIX ".mjs") - - target_link_options( - butterscotch - PRIVATE - "-sMODULARIZE=1" - "-sEXPORT_ES6=1" - "-sENVIRONMENT=worker" - "-sWASMFS=1" - "-sEXPORTED_RUNTIME_METHODS=['HEAP8', 'HEAPU8', 'HEAPF32', 'ccall', 'cwrap', 'specialHTMLTargets', 'UTF8ToString']" - "-sEXPORTED_FUNCTIONS=['_main', '_mountOpfs', '_startRunner', '_stopRunner', '_malloc', '_free', '_getKeyDownPtr', '_getKeyUpPtr', '_getKeyCount', '_setAudioSampleRate', '_pullAudioFrames']" - "-sOFFSCREENCANVAS_SUPPORT=1" - "-sOFFSCREEN_FRAMEBUFFER=1" - "-sMIN_WEBGL_VERSION=2" - "-sMAX_WEBGL_VERSION=2" - "-sFULL_ES3=1" - "-sALLOW_MEMORY_GROWTH" - "-sSHARED_MEMORY=1" - "-sPTHREAD_POOL_SIZE=4" - "-pthread" - ) - - target_compile_options(butterscotch PRIVATE "-pthread") -elseif(PLATFORM STREQUAL "web-meta") - set_target_properties(butterscotch PROPERTIES - SUFFIX ".mjs" - OUTPUT_NAME "butterscotch-meta" - ) - - target_link_libraries(butterscotch PRIVATE stb_ds) - - target_link_options( - butterscotch - PRIVATE - "-sMODULARIZE=1" - "-sEXPORT_ES6=1" - "-sENVIRONMENT=web" - "-sEXPORTED_RUNTIME_METHODS=['HEAPU8', 'ccall', 'cwrap', 'UTF8ToString', 'FS']" - "-sEXPORTED_FUNCTIONS=['_main', '_parseDataWin', '_freeDataWin', '_getGameName', '_getGameDisplayName', '_getMajorVersion', '_getMinorVersion', '_getRelease', '_getBuild', '_getDefaultWindowWidth', '_getDefaultWindowHeight', '_malloc', '_free']" - "-sALLOW_MEMORY_GROWTH=1" - "-sFORCE_FILESYSTEM=1" - ) elseif(PLATFORM STREQUAL "android") if (NOT ENABLE_MODERN_GL) message(FATAL_ERROR "Android requires modern gl!") diff --git a/src/web-meta/main.c b/src/web-meta/main.c deleted file mode 100644 index f1878a762..000000000 --- a/src/web-meta/main.c +++ /dev/null @@ -1,71 +0,0 @@ -#include "common.h" -#include "stdio_compat.h" -#include -#include "string_compat.h" -#include "data_win.h" - -void platformLog(const logType type, const char *format, va_list va) { - FILE *out = stderr; - switch (type) { - case LOG_TYPE_NORMAL: - out = stdout; - break; - case LOG_TYPE_WARNING: - fputs("Warning: ", out); - break; - case LOG_TYPE_ERROR: - fputs("Error: ", out); - break; - case LOG_TYPE_DEBUG: - fputs("Debug: ", out); - break; - } - vfprintf(out, format, va); -} - -DataWin* parseDataWin(const char* path) { - DataWinParserOptions opts = {0}; - opts.parseGen8 = true; - opts.parseStrg = true; // GEN8 stores string offsets that point into STRG - return DataWin_parse(path, opts); -} - -void freeDataWin(DataWin* dw) { - if (dw != nullptr) DataWin_free(dw); -} - -const char* getGameName(DataWin* dw) { - return (dw != nullptr) ? dw->gen8.name : nullptr; -} - -const char* getGameDisplayName(DataWin* dw) { - return (dw != nullptr) ? dw->gen8.displayName : nullptr; -} - -uint32_t getMajorVersion(DataWin* dw) { - return (dw != nullptr) ? dw->gen8.major : 0; -} - -uint32_t getMinorVersion(DataWin* dw) { - return (dw != nullptr) ? dw->gen8.minor : 0; -} - -uint32_t getRelease(DataWin* dw) { - return (dw != nullptr) ? dw->gen8.release : 0; -} - -uint32_t getBuild(DataWin* dw) { - return (dw != nullptr) ? dw->gen8.build : 0; -} - -uint32_t getDefaultWindowWidth(DataWin* dw) { - return (dw != nullptr) ? dw->gen8.defaultWindowWidth : 0; -} - -uint32_t getDefaultWindowHeight(DataWin* dw) { - return (dw != nullptr) ? dw->gen8.defaultWindowHeight : 0; -} - -int main(void) { - return 0; -} diff --git a/src/web/main.c b/src/web/main.c deleted file mode 100644 index 6375bee19..000000000 --- a/src/web/main.c +++ /dev/null @@ -1,319 +0,0 @@ -#include "stdio_compat.h" -#include "string_compat.h" -#include -#include -#include -#include -#include -#include -#include "data_win.h" -#include "noop_audio_system.h" -#include "web_audio_system.h" -#include "overlay_file_system.h" -#include "runner.h" -#include "gl/gl_renderer.h" -#include "gettime.h" - -static EMSCRIPTEN_WEBGL_CONTEXT_HANDLE ctx = 0; -static Runner* gRunner; -static WebAudioSystem* gWebAudio = nullptr; -static int32_t gAudioSampleRate = 48000; - -uint8_t keyDown[GML_KEY_COUNT] = {0}; -uint8_t keyUp[GML_KEY_COUNT] = {0}; - -void platformLog(const logType type, const char *format, va_list va) { - FILE *out = stderr; - switch (type) { - case LOG_TYPE_NORMAL: - out = stdout; - break; - case LOG_TYPE_WARNING: - fputs("Warning: ", out); - break; - case LOG_TYPE_ERROR: - fputs("Error: ", out); - break; - case LOG_TYPE_DEBUG: - fputs("Debug: ", out); - break; - } - vfprintf(out, format, va); -} - -// Configures the sample rate that miniaudio will mix at. Must match the AudioContext's sampleRate -// on the JS side, and must be called BEFORE startRunner. -void setAudioSampleRate(int32_t rate) { - if (rate > 0) gAudioSampleRate = rate; -} - -// Pulls frameCount interleaved-stereo float32 frames into outPtr (which must point into wasm memory). -// Called from JS by the worker's audio pull loop. Safe to call before the runner starts (returns silence). -void pullAudioFrames(float* outPtr, int32_t frameCount) { - if (gWebAudio == nullptr || frameCount <= 0) { - if (outPtr != nullptr && frameCount > 0) { - memset(outPtr, 0, (size_t) frameCount * 2 * sizeof(float)); - } - return; - } - WebAudioSystem_pullFrames(gWebAudio, outPtr, frameCount); -} - -uint8_t* getKeyDownPtr() { - return keyDown; -} - -uint8_t* getKeyUpPtr() { - return keyUp; -} - -int getKeyCount() { - return GML_KEY_COUNT; -} - -int main() { - logInfo("Howdy! Loritta is so cute! lol\n"); - emscripten_exit_with_live_runtime(); - return 0; -} - -// Mounts the browser's OPFS at "/butterscotch" in the WASMFS virtual filesystem. -int mountOpfs(void) { - backend_t opfs = wasmfs_create_opfs_backend(); - if (!opfs) { - logWarn("Failed to create OPFS backend\n"); - return -1; - } - int rc = wasmfs_create_directory("/butterscotch", 0777, opfs); - if (rc != 0) { - logWarn("Failed to mount OPFS at /butterscotch: %s\n", strerror(errno)); - return -1; - } - return 0; -} - -// mkdir -p for WASMFS paths. Used to ensure the saves directory exists before the runner tries to write into it. -static int mkdirP(const char* path) { - char buf[512]; - size_t len = strlen(path); - if (len >= sizeof(buf)) return -1; - memcpy(buf, path, len + 1); - for (size_t i = 1; len > i; i++) { - if (buf[i] == '/') { - buf[i] = '\0'; - if (mkdir(buf, 0777) != 0 && errno != EEXIST) return -1; - buf[i] = '/'; - } - } - if (mkdir(buf, 0777) != 0 && errno != EEXIST) return -1; - return 0; -} - -void* loop() { - double lastFrameStartMs = emscripten_get_now(); // for delta_time and frame pacing - - gRunner->gameStartTime = nowNanos(); - while (!gRunner->shouldExit) { - double frameStartMs = emscripten_get_now(); - gRunner->deltaTime = (frameStartMs - lastFrameStartMs) * 1000.0; - lastFrameStartMs = frameStartMs; - - RunnerKeyboard_beginFrame(gRunner->keyboard); - - // Process inputs - repeat(GML_KEY_COUNT, i) { - if (keyDown[i]) { - RunnerKeyboard_onKeyDown(gRunner->keyboard, i); - keyDown[i] = 0; - } - if (keyUp[i]) { - RunnerKeyboard_onKeyUp(gRunner->keyboard, i); - keyUp[i] = 0; - } - } - - emscripten_webgl_make_context_current(ctx); - - float audioDt = (float) (gRunner->deltaTime / 1000000.0); - if (0.0f > audioDt) audioDt = 0.0f; - if (audioDt > 0.1f) audioDt = 0.1f; - gRunner->audioSystem->vtable->update(gRunner->audioSystem, audioDt); - - // Run one game step (Begin Step, Keyboard, Alarms, Step, End Step, room transitions) - Runner_step(gRunner); - - int32_t gameW = (int32_t) gRunner->dataWin->gen8.defaultWindowWidth; - int32_t gameH = (int32_t) gRunner->dataWin->gen8.defaultWindowHeight; - - Runner_drawPre(gRunner, 640, 480); - - Runner_beginFrame(gRunner, gameW, gameH, 640, 480, 640, 480); - - Runner_drawViews(gRunner, gameW, gameH, false); - gRunner->renderer->vtable->endFrameInit(gRunner->renderer); - Runner_drawPost(gRunner, 640, 480); - gRunner->renderer->vtable->endFrameEnd(gRunner->renderer); - Runner_drawGUI(gRunner, 640, 480, gameW, gameH); - - // Just like glfwSwapBuffers. - // Only swap when there isn't a room change to match the original runner. - if (gRunner->pendingRoom == -1) { - emscripten_webgl_commit_frame(); - } - Runner_handlePendingRoomChange(gRunner); - - // Frame pacing: sleep until the next frame is due, based on the room's speed. - // emscripten_get_now() returns milliseconds (performance.now()) and works in workers. - if (gRunner->currentRoom != nullptr && gRunner->currentRoom->speed > 0) { - double targetFrameTimeMs = 1000.0 / (double) gRunner->currentRoom->speed; - double nextFrameTimeMs = lastFrameStartMs + targetFrameTimeMs; - double remainingMs = nextFrameTimeMs - emscripten_get_now(); - // Sleep for most of the remaining time, then spin-wait for precision. - if (remainingMs > 2.0) { - struct timespec ts; - ts.tv_sec = 0; - ts.tv_nsec = (long) ((remainingMs - 1.0) * 1000000.0); - nanosleep(&ts, nullptr); - } - while (emscripten_get_now() < nextFrameTimeMs) { - // Spin-wait for the remaining sub-millisecond - } - } - } - - // Cleanup - logInfo("Cleaning up runner!\n"); - - gRunner->audioSystem->vtable->destroy(gRunner->audioSystem); - gRunner->audioSystem = nullptr; - gWebAudio = nullptr; - gRunner->renderer->vtable->destroy(gRunner->renderer); - - DataWin* dataWin = gRunner->dataWin; - VMContext* vm = gRunner->vmContext; - Runner_free(gRunner); - VM_free(vm); - DataWin_free(dataWin); - - // We want to *know* when the runner has actually exited, because we also need to track things like "Leave Game" buttons/actions in the game - MAIN_THREAD_EM_ASM({ postMessage({ type: 'runnerExit' }); }); - - return nullptr; -} - -void setWindowTitle(const char* title) { - MAIN_THREAD_EM_ASM({ postMessage({ type: 'windowTitle', title: UTF8ToString($0) }); }, title); -} - -// gamePath: WASMFS path to the data.win to load (example: "/butterscotch/games/undertale/data.win"). -// savesPath: WASMFS directory where saves should live (example: "/butterscotch/saves/undertale" - Created if it does not exist). -void startRunner(const char* gamePath, const char* savesPath) { - logInfo("Starting runner! gamePath=%s savesPath=%s\n", gamePath, savesPath); - - EmscriptenWebGLContextAttributes attrs; - emscripten_webgl_init_context_attributes(&attrs); - - attrs.majorVersion = 2; - attrs.minorVersion = 0; - attrs.alpha = 0; - attrs.antialias = 0; // Required to avoid "WebGL warning: blitFramebuffer: DRAW_FRAMEBUFFER may not have multiple samples." - // Both of these are required to allow us to use emscripten_webgl_commit_frame - attrs.explicitSwapControl = true; - attrs.renderViaOffscreenBackBuffer = true; - - // Yes, "#canvas" feels nasty as HELL - // But that's how Emscripten works for SOME REASON - ctx = emscripten_webgl_create_context("#canvas", &attrs); - if (0 >= ctx) { - logError("Failed to create WebGL context: %d\n", (int)ctx); - abort(); - } - - emscripten_webgl_make_context_current(ctx); - - glClearColor(0.0f, 0.0f, 0.0f, 1.0f); - glClear(GL_COLOR_BUFFER_BIT); - - // Make sure the saves directory exists. The FileSystem impl will write into it. - if (savesPath != nullptr && savesPath[0] != '\0') { - if (mkdirP(savesPath) != 0) { - logWarn("failed to ensure saves dir exists at %s: %s\n", savesPath, strerror(errno)); - } - } - - DataWinParserOptions options = {0}; - options.parseGen8 = true; - options.parseOptn = true; - options.parseLang = true; - options.parseExtn = true; - options.parseSond = true; - options.parseAgrp = true; - options.parseSprt = true; - options.parseBgnd = true; - options.parsePath = true; - options.parseScpt = true; - options.parseGlob = true; - options.parseShdr = true; - options.parseFont = true; - options.parseTmln = true; - options.parseObjt = true; - options.parseRoom = true; - options.parseTpag = true; - options.parseCode = true; - options.parseVari = true; - options.parseFunc = true; - options.parseStrg = true; - options.parseTxtr = true; - options.parseAudo = true; - options.skipLoadingPreciseMasksForNonPreciseSprites = true; - options.lazyLoadRooms = false; - options.eagerlyLoadedRooms = nullptr; - DataWin* dataWin = DataWin_parse(gamePath, options); - - // return strdup(dataWin->gen8.name); - - // Initialize VM - VMContext* vm = VM_create(dataWin); - - Renderer* renderer = GLRenderer_create(); - - // Bundle path = directory containing data.win, e.g. "/butterscotch/games/undertale/". - // Save path = whatever the worker passed in, e.g. "/butterscotch/saves/undertale/". - char* bundleDir = nullptr; - const char* lastSlash = strrchr(gamePath, '/'); - if (lastSlash != nullptr) { - size_t len = (size_t) (lastSlash - gamePath + 1); - bundleDir = (char *)safeMalloc(len + 1); - memcpy(bundleDir, gamePath, len); - bundleDir[len] = '\0'; - } else { - bundleDir = safeStrdup("./"); - } - OverlayFileSystem* overlayFs = OverlayFileSystem_create(bundleDir, savesPath); - free(bundleDir); - - gWebAudio = WebAudioSystem_create(dataWin, gAudioSampleRate); - AudioSystem* audioSystem = (AudioSystem*) gWebAudio; - - // Initialize the runner - Runner* runner = Runner_create(dataWin, vm, renderer, (FileSystem*) overlayFs, audioSystem, 0); - runner->setWindowTitle = setWindowTitle; - runner->windowHasFocus = nullptr; - - setWindowTitle(dataWin->gen8.name); - - gRunner = runner; - - // Initialize the first room and fire Game Start / Room Start events - Runner_initFirstRoom(runner); - - // Start a new thread - pthread_t tid; - pthread_create(&tid, NULL, loop, NULL); - pthread_detach(tid); -} - -void stopRunner() { - logInfo("Marked runner to exit!\n"); - gRunner->shouldExit = true; -} diff --git a/src/web/stb_impl.c b/src/web/stb_impl.c deleted file mode 100644 index 3ba0774af..000000000 --- a/src/web/stb_impl.c +++ /dev/null @@ -1,9 +0,0 @@ -#define STB_IMAGE_IMPLEMENTATION -#define STBI_NO_STDIO -#define STBI_ONLY_PNG -#define STBI_ONLY_JPEG -#define STBI_ONLY_GIF -#include "stb_image.h" - -#define STB_IMAGE_WRITE_IMPLEMENTATION -#include "stb_image_write.h" From bfbac3f4364d1aaa26dc768be2738afa9fe9db64 Mon Sep 17 00:00:00 2001 From: Un1q32 Date: Sat, 12 Sep 2026 16:21:15 -0400 Subject: [PATCH 2/7] new web port --- CMakeLists.txt | 58 ++++++++++++++++++--- src/web/log.c | 22 ++++++++ src/web/main.c | 57 +++++++++++++++++++++ src/web/shell.html | 123 +++++++++++++++++++++++++++++++++++++++++++++ src/web/stb_impl.c | 6 +++ 5 files changed, 260 insertions(+), 6 deletions(-) create mode 100644 src/web/log.c create mode 100644 src/web/main.c create mode 100644 src/web/shell.html create mode 100644 src/web/stb_impl.c diff --git a/CMakeLists.txt b/CMakeLists.txt index 4180b248b..f31e0c190 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -26,6 +26,10 @@ endif() set(BACKEND "sdl2") +elseif(PLATFORM STREQUAL "web") +# Emscripten provides its own toolchain file via emcmake, nothing to set up here. +set(BACKEND "sdl2") + endif() project(butterscotch C) @@ -118,6 +122,10 @@ option(ENABLE_LEGACY_GL "Enable the legacy OpenGL renderer" ON) if(PLATFORM STREQUAL "switch") set(ENABLE_LEGACY_GL OFF CACHE BOOL "Enable the legacy OpenGL renderer" FORCE) endif() +if(PLATFORM STREQUAL "web") + # WebGL2 only exposes GLES3, so the fixed-function legacy GL renderer can't work there. + set(ENABLE_LEGACY_GL OFF CACHE BOOL "Enable the legacy OpenGL renderer" FORCE) +endif() option(ENABLE_MODERN_GL "Enable the modern OpenGL renderer" ON) option(ENABLE_NOOP_RENDERER "Enable the no-op renderer (headless stub, no drawing)" OFF) # Noop renderer is exclusive to the noop desktop backend (no window, no GL). @@ -254,6 +262,13 @@ if(PLATFORM STREQUAL "cli" OR PLATFORM STREQUAL "vita" OR PLATFORM STREQUAL "swi elseif(PLATFORM STREQUAL "switch") add_compile_definitions(PLATFORM_SWITCH) + set(VM_GML_PROFILER_DEFAULT OFF) + set(VM_TRACING_DEFAULT OFF) + set(VM_OPCODE_PROFILER_DEFAULT OFF) + set(VM_STUB_LOGS_DEFAULT OFF) + elseif(PLATFORM STREQUAL "web") + add_compile_definitions(PLATFORM_WEB) + set(VM_GML_PROFILER_DEFAULT OFF) set(VM_TRACING_DEFAULT OFF) set(VM_OPCODE_PROFILER_DEFAULT OFF) @@ -317,11 +332,15 @@ if(PLATFORM STREQUAL "cli" OR PLATFORM STREQUAL "vita" OR PLATFORM STREQUAL "swi set(PLATFORM_LIBRARIES bzip2 stb_ds sha1 stb_vorbis) if(NOT PLATFORM STREQUAL "vita" AND NOT PLATFORM STREQUAL "switch") + # Emscripten provides its own GLES3 headers, so cross-compiled web builds don't + # need GLAD. Native PLATFORM=web builds (used for testing) still use it like cli. + if(NOT (PLATFORM STREQUAL "web" AND CMAKE_SYSTEM_NAME STREQUAL "Emscripten")) # GLAD add_library(glad STATIC vendor/glad/src/glad.c) target_include_directories(glad PUBLIC vendor/glad/include) target_compile_options(glad PRIVATE -Wno-unused-but-set-variable) set(PLATFORM_LIBRARIES ${PLATFORM_LIBRARIES} glad) + endif() endif() if(NOT MSVC) set(PLATFORM_LIBRARIES ${PLATFORM_LIBRARIES} m) @@ -371,11 +390,17 @@ if(PLATFORM STREQUAL "cli" OR PLATFORM STREQUAL "vita" OR PLATFORM STREQUAL "swi elseif(BACKEND STREQUAL "sdl2") add_compile_definitions(USE_SDL2) - find_package(PkgConfig REQUIRED) - pkg_check_modules(SDL2 REQUIRED sdl2) - target_include_directories(butterscotch PRIVATE ${SDL2_INCLUDE_DIRS}) - target_link_directories(butterscotch PRIVATE ${SDL2_LIBRARY_DIRS}) - set(BACKEND_LIBRARIES ${SDL2_LIBRARIES}) + if(CMAKE_SYSTEM_NAME STREQUAL "Emscripten") + # Emscripten ships SDL2 as a port, there is no pkg-config package for it. + target_compile_options(butterscotch PRIVATE "-sUSE_SDL=2") + set(BACKEND_LIBRARIES "-sUSE_SDL=2") + else() + find_package(PkgConfig REQUIRED) + pkg_check_modules(SDL2 REQUIRED sdl2) + target_include_directories(butterscotch PRIVATE ${SDL2_INCLUDE_DIRS}) + target_link_directories(butterscotch PRIVATE ${SDL2_LIBRARY_DIRS}) + set(BACKEND_LIBRARIES ${SDL2_LIBRARIES}) + endif() elseif(BACKEND STREQUAL "sdl3") add_compile_definitions(USE_SDL3) @@ -420,6 +445,27 @@ if(PLATFORM STREQUAL "cli" OR PLATFORM STREQUAL "vita" OR PLATFORM STREQUAL "swi nx_generate_nacp(OUTPUT Butterscotch.nacp NAME "Butterscotch" AUTHOR "Butterscotch" VERSION "1.0.0") nx_create_nro(butterscotch NACP Butterscotch.nacp) endif() + + if (PLATFORM STREQUAL "web" AND CMAKE_SYSTEM_NAME STREQUAL "Emscripten") + if (NOT ENABLE_MODERN_GL) + message(FATAL_ERROR "Web requires modern gl!") + endif() + # Emit a runnable page (butterscotch.html + .js + .wasm). + set_target_properties(butterscotch PROPERTIES SUFFIX ".html") + target_link_options(butterscotch PRIVATE + "-sUSE_SDL=2" + "-sUSE_WEBGL2=1" + "-sFULL_ES3=1" + "-sALLOW_MEMORY_GROWTH=1" + # The shared game loop is blocking, ASYNCIFY lets it yield to the browser. + "-sASYNCIFY=1" + "-sFORCE_FILESYSTEM=1" + # The shell page drives the game through these, and they are + # not attached to Module by default. + "-sEXPORTED_RUNTIME_METHODS=['FS','callMain']" + "--shell-file=${CMAKE_SOURCE_DIR}/src/web/shell.html" + ) + endif() endif() if(BACKEND STREQUAL "appkit") target_sources(butterscotch PRIVATE src/backends/${BACKEND}.m) @@ -434,7 +480,7 @@ if(PLATFORM STREQUAL "cli" OR PLATFORM STREQUAL "vita" OR PLATFORM STREQUAL "swi set(ENABLE_ASAN_DEFAULT OFF) endif() set(ENABLE_ASAN ${ENABLE_ASAN_DEFAULT} CACHE BOOL "Enable AddressSanitizer") - if(ENABLE_ASAN AND NOT WIN32 AND NOT CMAKE_SYSTEM_NAME STREQUAL "Haiku") + if(ENABLE_ASAN AND NOT WIN32 AND NOT CMAKE_SYSTEM_NAME STREQUAL "Haiku" AND NOT CMAKE_SYSTEM_NAME STREQUAL "Emscripten") target_compile_options(butterscotch PRIVATE -fsanitize=address -fno-omit-frame-pointer) target_link_options(butterscotch PRIVATE -fsanitize=address) endif() diff --git a/src/web/log.c b/src/web/log.c new file mode 100644 index 000000000..72f897411 --- /dev/null +++ b/src/web/log.c @@ -0,0 +1,22 @@ +#include + +#include "log.h" + +void platformLog(const logType type, const char *format, va_list va) { + FILE *out = stderr; + switch (type) { + case LOG_TYPE_NORMAL: + out = stdout; + break; + case LOG_TYPE_WARNING: + fputs("Warning: ", out); + break; + case LOG_TYPE_ERROR: + fputs("Error: ", out); + break; + case LOG_TYPE_DEBUG: + fputs("Debug: ", out); + break; + } + vfprintf(out, format, va); +} diff --git a/src/web/main.c b/src/web/main.c new file mode 100644 index 000000000..244e97b4c --- /dev/null +++ b/src/web/main.c @@ -0,0 +1,57 @@ +#include + +/* For SDL_main */ +#if defined(USE_SDL1) +#include +#elif defined(USE_SDL2) +#include +#elif defined(USE_SDL3) +#include +#endif + +// Web (Emscripten + SDL2) entry point. +// +// This is intentionally kept close to src/embedded/main.c: the SDL2 backend +// (src/backends/sdl2.c) and the shared game loop (src/loop.c) do all of the +// heavy lifting, so the platform layer only has to provide fixed startup +// arguments. The blocking loop() call yields to the browser via -sASYNCIFY +// (see the PLATFORM STREQUAL "web" block in the top-level CMakeLists.txt). +// +// The game data is loaded from the Emscripten virtual filesystem at +// "data.win", which is where src/web/shell.html stages the folder the user +// picks (a --preload-file data.win baked into the build lands there too). +// The shell also accepts sibling names like game.unx and stages them as +// data.win, so the runner only ever has to look in one place. +int main(int argc, char* argv[]) { + (void)argc; + setbuf(stderr, NULL); + + CommandLineArgs args = {0}; + + args.exitAtFrame = -1; +#ifdef ENABLE_VM_TRACING + args.traceBytecodeAfterFrame = 0; +#endif + args.speedMultiplier = 1.0; + args.fastForwardSpeed = 0.0; + args.osType = OS_WINDOWS; + args.profilerFramesBetween = 0; + args.loadType = DATAWINLOADTYPE_LOAD_PER_CHUNK; + args.lazyRooms = true; + args.lazyTextures = true; + args.lazyAudio = true; +#if defined(ENABLE_MODERN_GL) + args.renderer = MODERN_GL; +#elif defined(ENABLE_LEGACY_GL) + args.renderer = LEGACY_GL; +#else + args.renderer = SOFTWARE; +#endif + // Matches where src/web/shell.html stages the uploaded folder in the + // Emscripten virtual filesystem. + args.dataWinPath = "data.win"; + + int ret = loop(args, argv[0]); + freeCommandLineArgs(&args); + return ret; +} diff --git a/src/web/shell.html b/src/web/shell.html new file mode 100644 index 000000000..439a022fa --- /dev/null +++ b/src/web/shell.html @@ -0,0 +1,123 @@ + + + + +Butterscotch + + + + + + + +{{{ SCRIPT }}} + + diff --git a/src/web/stb_impl.c b/src/web/stb_impl.c new file mode 100644 index 000000000..873f3656b --- /dev/null +++ b/src/web/stb_impl.c @@ -0,0 +1,6 @@ +#define STB_IMAGE_IMPLEMENTATION +#define STBI_NO_STDIO +#include "stb_image.h" + +#define STB_IMAGE_WRITE_IMPLEMENTATION +#include "stb_image_write.h" From 6471e9274d0e014c8a963ca673fe33286d7062d7 Mon Sep 17 00:00:00 2001 From: Un1q32 Date: Sat, 12 Sep 2026 16:30:37 -0400 Subject: [PATCH 3/7] fast-forward --- src/web/main.c | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/web/main.c b/src/web/main.c index 244e97b4c..5a0635ca7 100644 --- a/src/web/main.c +++ b/src/web/main.c @@ -33,7 +33,7 @@ int main(int argc, char* argv[]) { args.traceBytecodeAfterFrame = 0; #endif args.speedMultiplier = 1.0; - args.fastForwardSpeed = 0.0; + args.fastForwardSpeed = 2.0; args.osType = OS_WINDOWS; args.profilerFramesBetween = 0; args.loadType = DATAWINLOADTYPE_LOAD_PER_CHUNK; @@ -47,8 +47,6 @@ int main(int argc, char* argv[]) { #else args.renderer = SOFTWARE; #endif - // Matches where src/web/shell.html stages the uploaded folder in the - // Emscripten virtual filesystem. args.dataWinPath = "data.win"; int ret = loop(args, argv[0]); From c06fed3587919a29bff8fce22d7909b7c8cece07 Mon Sep 17 00:00:00 2001 From: Un1q32 Date: Sat, 12 Sep 2026 17:11:13 -0400 Subject: [PATCH 4/7] remove useless ai comment --- src/web/main.c | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/src/web/main.c b/src/web/main.c index 5a0635ca7..e7f41010f 100644 --- a/src/web/main.c +++ b/src/web/main.c @@ -9,19 +9,6 @@ #include #endif -// Web (Emscripten + SDL2) entry point. -// -// This is intentionally kept close to src/embedded/main.c: the SDL2 backend -// (src/backends/sdl2.c) and the shared game loop (src/loop.c) do all of the -// heavy lifting, so the platform layer only has to provide fixed startup -// arguments. The blocking loop() call yields to the browser via -sASYNCIFY -// (see the PLATFORM STREQUAL "web" block in the top-level CMakeLists.txt). -// -// The game data is loaded from the Emscripten virtual filesystem at -// "data.win", which is where src/web/shell.html stages the folder the user -// picks (a --preload-file data.win baked into the build lands there too). -// The shell also accepts sibling names like game.unx and stages them as -// data.win, so the runner only ever has to look in one place. int main(int argc, char* argv[]) { (void)argc; setbuf(stderr, NULL); From 55558cae2f26790f1197793e3e6590f6a571d636 Mon Sep 17 00:00:00 2001 From: Un1q32 Date: Sat, 12 Sep 2026 17:11:43 -0400 Subject: [PATCH 5/7] buh --- src/web/stb_impl.c | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/web/stb_impl.c b/src/web/stb_impl.c index 873f3656b..3ba0774af 100644 --- a/src/web/stb_impl.c +++ b/src/web/stb_impl.c @@ -1,5 +1,8 @@ #define STB_IMAGE_IMPLEMENTATION #define STBI_NO_STDIO +#define STBI_ONLY_PNG +#define STBI_ONLY_JPEG +#define STBI_ONLY_GIF #include "stb_image.h" #define STB_IMAGE_WRITE_IMPLEMENTATION From d04e5285f10387d2d25c4afd60ce384c5a92a4bb Mon Sep 17 00:00:00 2001 From: Un1q32 Date: Sat, 12 Sep 2026 17:50:28 -0400 Subject: [PATCH 6/7] pages --- .github/workflows/build-web.yml | 44 ++++++++++++++++++++ .github/workflows/build.yml | 2 +- .github/workflows/pages.yml | 73 +++++++++++++++++++++++++++++++++ 3 files changed, 118 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/build-web.yml create mode 100644 .github/workflows/pages.yml diff --git a/.github/workflows/build-web.yml b/.github/workflows/build-web.yml new file mode 100644 index 000000000..81a139046 --- /dev/null +++ b/.github/workflows/build-web.yml @@ -0,0 +1,44 @@ +name: Web builds + +on: + workflow_call: + +jobs: + build-web: + name: Build (Web) + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@v7 + + - name: Get commit info + id: commit-info + run: | + echo "hash=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT + echo "date=$(git log -1 --format=%cd --date=short)" >> $GITHUB_OUTPUT + + - name: Setup Emscripten SDK + uses: mymindstorm/setup-emsdk@v14 + with: + version: latest + + - name: Install ccache + run: sudo apt-get update && sudo apt-get install -y ccache + + - uses: actions/cache@v6 + with: + path: ~/.cache/ccache + key: ccache-web-${{ github.sha }} + restore-keys: | + ccache-web- + + - name: Configure + run: emcmake cmake -B build -G Ninja -DWERROR=ON -DPLATFORM=web -DCMAKE_BUILD_TYPE=Release -DCMAKE_C_COMPILER_LAUNCHER=ccache -DCMAKE_CXX_COMPILER_LAUNCHER=ccache "-DBUTTERSCOTCH_COMMIT_HASH=${{ steps.commit-info.outputs.hash }}" "-DBUTTERSCOTCH_COMMIT_DATE=${{ steps.commit-info.outputs.date }}" + + - name: Build + run: cmake --build build + + - name: Upload artifact + uses: actions/upload-artifact@v7 + with: + name: butterscotch-web + path: build/butterscotch.* diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 8df58084b..fbda82b66 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -23,7 +23,7 @@ jobs: uses: ./.github/workflows/build-console.yml Other: - uses: ./.github/workflows/build-other.yml + uses: ./.github/workflows/build-web.yml Compilers: uses: ./.github/workflows/build-old-compilers.yml \ No newline at end of file diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml new file mode 100644 index 000000000..11bb205ae --- /dev/null +++ b/.github/workflows/pages.yml @@ -0,0 +1,73 @@ +name: Deploy Web to GitHub Pages + +on: + push: + branches: [main] + workflow_dispatch: + +permissions: + contents: read + pages: write + id-token: write + +concurrency: + group: pages + cancel-in-progress: false + +jobs: + build: + name: Build (Web) + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@v7 + + - name: Get commit info + id: commit-info + run: | + echo "hash=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT + echo "date=$(git log -1 --format=%cd --date=short)" >> $GITHUB_OUTPUT + + - name: Setup Emscripten SDK + uses: mymindstorm/setup-emsdk@v14 + with: + version: latest + + - name: Install ccache + run: sudo apt-get update && sudo apt-get install -y ccache + + - uses: actions/cache@v6 + with: + path: ~/.cache/ccache + key: ccache-pages-${{ github.sha }} + restore-keys: | + ccache-pages- + ccache-web- + + - name: Configure + run: emcmake cmake -B build -G Ninja -DWERROR=ON -DPLATFORM=web -DCMAKE_BUILD_TYPE=Release -DCMAKE_C_COMPILER_LAUNCHER=ccache -DCMAKE_CXX_COMPILER_LAUNCHER=ccache "-DBUTTERSCOTCH_COMMIT_HASH=${{ steps.commit-info.outputs.hash }}" "-DBUTTERSCOTCH_COMMIT_DATE=${{ steps.commit-info.outputs.date }}" + + - name: Build + run: cmake --build build + + - name: Stage site + run: | + mkdir -p site + cp build/butterscotch.html site/index.html + cp build/butterscotch.js build/butterscotch.wasm site/ + + - name: Upload Pages artifact + uses: actions/upload-pages-artifact@v3 + with: + path: site + + deploy: + name: Deploy to GitHub Pages + needs: build + runs-on: ubuntu-24.04 + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + steps: + - name: Deploy + id: deployment + uses: actions/deploy-pages@v4 From b1fc2721d793e11b88bb82c7cb529c201dd71130 Mon Sep 17 00:00:00 2001 From: Un1q32 Date: Sat, 12 Sep 2026 18:09:16 -0400 Subject: [PATCH 7/7] buh --- .github/workflows/pages.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml index 11bb205ae..69f002f04 100644 --- a/.github/workflows/pages.yml +++ b/.github/workflows/pages.yml @@ -2,7 +2,7 @@ name: Deploy Web to GitHub Pages on: push: - branches: [main] + branches: [web-refactor] workflow_dispatch: permissions: