From aedd7b2c070055aef25045fd73bc8d7e89bb835d Mon Sep 17 00:00:00 2001 From: Un1q32 Date: Tue, 7 Jul 2026 03:09:26 -0400 Subject: [PATCH 001/109] software renderer --- CMakeLists.txt | 28 +- Makefile | 9 + src/sw/defines.h | 24 + src/sw/pixel_convert.h | 105 ++ src/sw/sw_renderer.c | 2346 ++++++++++++++++++++++++++++++++++++++++ src/sw/sw_renderer.h | 7 + 6 files changed, 2512 insertions(+), 7 deletions(-) create mode 100644 src/sw/defines.h create mode 100644 src/sw/pixel_convert.h create mode 100644 src/sw/sw_renderer.c create mode 100644 src/sw/sw_renderer.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 924d470fa..398d07370 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -71,6 +71,7 @@ option(ENABLE_WAD16 "Enable support for WAD Version 15/16" ON) option(ENABLE_WAD17 "Enable support for WAD Version 17" ON) option(ENABLE_LEGACY_GL "Enable the legacy OpenGL renderer" ON) option(ENABLE_MODERN_GL "Enable the modern OpenGL renderer" ON) +option(ENABLE_SW_RENDERER "Enable the software renderer" ON) if(NOT ENABLE_WAD14 AND NOT ENABLE_WAD16 AND NOT ENABLE_WAD17) message(FATAL_ERROR "You need to build Butterscotch with at least one WAD version enabled!") endif() @@ -89,6 +90,9 @@ endif() if(ENABLE_MODERN_GL) add_compile_definitions(ENABLE_MODERN_GL) endif() +if(ENABLE_SW_RENDERER) + add_compile_definitions(ENABLE_SW_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. @@ -186,10 +190,15 @@ if(PLATFORM STREQUAL "desktop") # Matches the GitHub Actions container flags (FORTIFY requires building with optimizations enabled!) target_compile_options(butterscotch PRIVATE "$<$:-U_FORTIFY_SOURCE;-D_FORTIFY_SOURCE=2>") - if(NOT ENABLE_LEGACY_GL AND NOT ENABLE_MODERN_GL) + if(NOT ENABLE_LEGACY_GL AND NOT ENABLE_MODERN_GL AND NOT ENABLE_SW_RENDERER) message(FATAL_ERROR "You must enable at least one renderer!") endif() - file(GLOB GL_SOURCES src/image/*.c src/gl_common/*.c) + file(GLOB GL_SOURCES src/image/*.c) + if(ENABLE_LEGACY_GL OR ENABLE_MODERN_GL) + file(GLOB GL_SOURCES ${GL_SOURCES} src/gl_common/*.c) + target_include_directories(butterscotch PRIVATE ${CMAKE_SOURCE_DIR}/src/gl) + target_include_directories(butterscotch PRIVATE ${CMAKE_SOURCE_DIR}/src/gl_common) + endif() if(ENABLE_LEGACY_GL) file(GLOB GL_SOURCES ${GL_SOURCES} src/gl_legacy/*.c) target_include_directories(butterscotch PRIVATE ${CMAKE_SOURCE_DIR}/src/gl_legacy) @@ -197,9 +206,11 @@ if(PLATFORM STREQUAL "desktop") if(ENABLE_MODERN_GL) file(GLOB GL_SOURCES ${GL_SOURCES} src/gl/*.c) endif() + if(ENABLE_SW_RENDERER) + file(GLOB GL_SOURCES ${GL_SOURCES} src/sw/*.c) + target_include_directories(butterscotch PRIVATE ${CMAKE_SOURCE_DIR}/src/sw) + endif() 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) if (MSVC) target_include_directories(butterscotch PRIVATE ${CMAKE_SOURCE_DIR}/compat/getopt) @@ -231,13 +242,16 @@ if(PLATFORM STREQUAL "desktop") endif() # GLAD - add_library(glad STATIC vendor/glad/src/glad.c) - target_include_directories(glad PUBLIC vendor/glad/include) + if(ENABLE_LEGACY_GL OR ENABLE_MODERN_GL OR DESKTOP_BACKEND STREQUAL "glfw3" OR DESKTOP_BACKEND STREQUAL "glfw2") + add_library(glad STATIC vendor/glad/src/glad.c) + target_include_directories(glad PUBLIC vendor/glad/include) + set(PLATFORM_LIBRARIES glad) + endif() # stb_image target_include_directories(butterscotch PUBLIC vendor/stb/image) - set(PLATFORM_LIBRARIES glad bzip2 stb_ds sha1 stb_vorbis) + set(PLATFORM_LIBRARIES ${PLATFORM_LIBRARIES} bzip2 stb_ds sha1 stb_vorbis) if(NOT MSVC) set(PLATFORM_LIBRARIES ${PLATFORM_LIBRARIES} m) endif() diff --git a/Makefile b/Makefile index 15159842c..c95691507 100644 --- a/Makefile +++ b/Makefile @@ -128,6 +128,13 @@ SRCS += $(wildcard src/gl/*.c) HEADERS += $(wildcard src/gl/*.h) endif +ifndef DISABLE_SW_RENDERER +DEFINES += -DENABLE_SW_RENDERER +SRCS += $(wildcard src/sw/*.c) +HEADERS += $(wildcard src/sw/*.h) +INCLUDES += -Isrc/sw +endif + ifdef DISABLE_WAD14 ifdef DISABLE_WAD16 ifdef DISABLE_WAD17 @@ -138,9 +145,11 @@ endif ifdef DISABLE_LEGACY_GL ifdef DISABLE_MODERN_GL +ifdef DISABLE_SW_RENDERER $(error must enable at least 1 renderer) endif endif +endif ifeq ($(AUDIO_BACKEND),miniaudio) INCLUDES += $(INCLUDE)src/audio/miniaudio $(INCLUDE)vendor/miniaudio diff --git a/src/sw/defines.h b/src/sw/defines.h new file mode 100644 index 000000000..1cfd49d88 --- /dev/null +++ b/src/sw/defines.h @@ -0,0 +1,24 @@ +#pragma once + +#include "common.h" + +// CONFIG: Change the size of a pixel. +// +// 32-bit: 0xAARRGGBB +// 16-bit: 0b0RRRRRGGGGGBBBBB +// 8-bit: 0bBBGGGRRR +#define PIXEL_SIZE 32 +//#define PIXEL_SIZE 16 +//#define PIXEL_SIZE 8 + +#if defined(__GNUC__) && (__GNUC__ >= 3 || (__GNUC__ == 2 && __GNUC_MINOR__ >= 96)) +#define LIKELY(cond) __builtin_expect(!!(cond), 1) +#define UNLIKELY(cond) __builtin_expect(!!(cond), 0) +#else +#define LIKELY(cond) (cond) +#define UNLIKELY(cond) (cond) +#endif + +#define UNUSED __attribute__ ((unused)) +#define FORCE_INLINE static inline __attribute__((always_inline)) + diff --git a/src/sw/pixel_convert.h b/src/sw/pixel_convert.h new file mode 100644 index 000000000..7eee4c2f9 --- /dev/null +++ b/src/sw/pixel_convert.h @@ -0,0 +1,105 @@ +#pragma once + +#include "defines.h" +#include "binary_utils.h" + +#if PIXEL_SIZE == 32 +typedef uint32_t uintpixel_t; +#elif PIXEL_SIZE == 16 +typedef uint16_t uintpixel_t; +#elif PIXEL_SIZE == 8 +typedef uint8_t uintpixel_t; +#define PXL_TRANSPARENT (0xAA) +#else +#error "Unknown pixel size!" +#endif + +// The native format coming out of GameMaker Studio's assets. +typedef union +{ + struct { + uint8_t r, g, b, a; + } p; + uint32_t l; +} +Pixel32ABGR; + +// This is the format the renderer knows how to work with. +typedef union +{ + struct { +#ifdef IS_BIG_ENDIAN + uint8_t a, r, g, b; +#else + uint8_t b, g, r, a; +#endif + } p; + uint32_t l; +} +Pixel32ARGB; + +FORCE_INLINE UNUSED +uint16_t abgr8888_to_rgb1555(uint32_t xl) +{ + Pixel32ABGR x; + x.l = xl; + return (x.p.b >> 3) | ((x.p.g >> 3) << 5) | ((x.p.r >> 3) << 10) | ((x.p.a >> 7) << 15); +} + +FORCE_INLINE UNUSED +uint8_t abgr8888_to_rgb332(uint32_t xl) +{ + Pixel32ABGR x; + x.l = xl; + +#if PIXEL_SIZE == 8 + //check if transparent + if (x.p.a < 128) + return PXL_TRANSPARENT; +#endif + + uint8_t pxl = (x.p.r >> 5) | ((x.p.g >> 5) << 3) | ((x.p.b >> 6) << 6); + +#if PIXEL_SIZE == 8 + //hacky fixup + if (pxl == PXL_TRANSPARENT) + pxl++; +#endif + + return pxl; +} + +FORCE_INLINE UNUSED +uintpixel_t swrConvertPixelBase(uint32_t gmPixel) +{ +#if PIXEL_SIZE == 32 + return (gmPixel & 0xFF00FF00) | ((gmPixel & 0xFF) << 16) | ((gmPixel >> 16) & 0xFF); +#elif PIXEL_SIZE == 16 + return abgr8888_to_rgb1555(gmPixel); +#elif PIXEL_SIZE == 8 + return abgr8888_to_rgb332(gmPixel); +#endif +} + +#if PIXEL_SIZE == 32 +#define TRANSPARENT_MASK 0xFF000000 +#elif PIXEL_SIZE == 16 +#define TRANSPARENT_MASK 0x8000 +#endif + +#if PIXEL_SIZE == 8 + +#define swrConvertPixel(x) swrConvertPixelBase((x) | 0xFF000000) +#define swrConvertPixelTexture(x) swrConvertPixelBase(x) + +#elif defined IS_BIG_ENDIAN + +#define swrConvertPixel(x) swrConvertPixelBase(x) +#define swrConvertPixelTexture(x) swrConvertPixelBase(BinaryUtils_bswap32(x)) + +#else + +#define swrConvertPixel swrConvertPixelBase +#define swrConvertPixelTexture swrConvertPixelBase + +#endif diff --git a/src/sw/sw_renderer.c b/src/sw/sw_renderer.c new file mode 100644 index 000000000..4490355a3 --- /dev/null +++ b/src/sw/sw_renderer.c @@ -0,0 +1,2346 @@ +#include +#include +#include +#include +#include "defines.h" +#include "sw_renderer.h" +#include "text_utils.h" +#include "pixel_convert.h" +#include "image/image_decoder.h" + +#define UNIMP() do { fprintf(stderr, "NYI %s\n", __func__); } while (0) +//#define UNIMP() do { } while (0) +#define UNIMP2() do { } while (0) + +#ifndef M_PI +#define M_PI 3.1415926535897932384626 +#endif + +#define TEXTURE_LRU_LENGTH 64 + +#define SURFACE_MAX_COUNT 64 + +typedef struct +{ + uintpixel_t* buffer; + uint16_t width, height; +} +SWTexture; + +typedef struct +{ + Renderer base; + + // Window Properties + uint16_t width; + uint16_t height; + // Framebuffer + uintpixel_t* fb; + uint16_t fbPitch; // in sizeof(uintpixel_t) units, NOT in bytes! + + uintpixel_t* mainFb; + uint16_t mainWidth; + uint16_t mainHeight; + uint16_t mainPitch; + bool drawingToSurface; + + SWTexture** textures; + uint32_t* textureIndexLRU; + uint32_t textureIndexLRUHead; + uint32_t textureIndexLRUTail; + size_t textureCount; + size_t surfaceCount; + size_t totalTextureCount; + size_t originalTPagCount; + size_t originalSpriteCount; + + bool viewActive; + int viewX, viewY, viewW, viewH; + int portX, portY, portW, portH; + int gameW, gameH, maxX, maxY; +} +SWRenderer; + +void Runner_setNextFrame(uintpixel_t* framebuffer, int width, int height); + +FORCE_INLINE int swrMin(int a, int b) { return a < b ? a : b; } +FORCE_INLINE int swrMax(int a, int b) { return a > b ? a : b; } +FORCE_INLINE int swrAbs(int x) { return x < 0 ? -x : x; } + +FORCE_INLINE bool opaque(uintpixel_t color) +{ +#if PIXEL_SIZE == 8 + return (color != PXL_TRANSPARENT); +#else + return (color & TRANSPARENT_MASK) != 0; +#endif +} + +FORCE_INLINE uintpixel_t tint(uintpixel_t tintColor, uintpixel_t color) +{ +#if PIXEL_SIZE == 32 + Pixel32ARGB x, y; + + if ((tintColor & 0xFFFFFF) == 0xFFFFFF) + return color; + + x.l = color; + y.l = tintColor; + + x.p.b = (int)x.p.b * y.p.b / 255; + x.p.g = (int)x.p.g * y.p.g / 255; + x.p.r = (int)x.p.r * y.p.r / 255; + return x.l; +#elif PIXEL_SIZE == 16 + if ((tintColor & 0x7FFF) == 0x7FFF) + return color; + + int tcb = tintColor & 0x1F; + int tcg = (tintColor >> 5) & 0x1F; + int tcr = (tintColor >> 10) & 0x1F; + + int cb = color & 0x1F; + int cg = (color >> 5) & 0x1F; + int cr = (color >> 10) & 0x1F; + int ca = color & 0x8000; + + cb = (cb * tcb) / 32; + cg = (cg * tcg) / 32; + cr = (cr * tcr) / 32; + return ca | cb | (cg << 5) | (cr << 10); +#elif PIXEL_SIZE == 8 + // fast but hacky + if (tintColor == 0xFF || tintColor == PXL_TRANSPARENT) + return color; + + return color & tintColor; +#endif +} + +// NOTE: alpha is between 0 and 256, NOT between 0 and 255! +FORCE_INLINE void alphaBlend(uintpixel_t* dcolor, uintpixel_t scolor, int alpha) +{ +#if PIXEL_SIZE == 32 || PIXEL_SIZE == 16 + // it's so insignificant here nobody will notice if we just don't... + if (alpha < 3) + return; + + // it's so significant here we might as well fill in the whole color + if (alpha > 253) + { + *dcolor = scolor; + return; + } + + int inval = 256 - alpha; +#endif + +#if PIXEL_SIZE == 32 + Pixel32ARGB dc, sc; + dc.l = *dcolor; + sc.l = scolor; + + dc.p.r = (dc.p.r * inval + sc.p.r * alpha) >> 8; + dc.p.g = (dc.p.g * inval + sc.p.g * alpha) >> 8; + dc.p.b = (dc.p.b * inval + sc.p.b * alpha) >> 8; + + *dcolor = dc.l; +#elif PIXEL_SIZE == 16 + int scb = scolor & 0x1F; + int scg = (scolor >> 5) & 0x1F; + int scr = (scolor >> 10) & 0x1F; + + uintpixel_t _dcolor = *dcolor; + int dcb = _dcolor & 0x1F; + int dcg = (_dcolor >> 5) & 0x1F; + int dcr = (_dcolor >> 10) & 0x1F; + int dca = _dcolor & 0x8000; + + dcr = (dcr * inval + scr * alpha) >> 8; + dcg = (dcg * inval + scg * alpha) >> 8; + dcb = (dcb * inval + scb * alpha) >> 8; + + *dcolor = dca | dcb | (dcg << 5) | (dcr << 10); +#else + if (alpha < 240) { + static int alphaApproximationThingy = 0; + alphaApproximationThingy += 1339; + if (alphaApproximationThingy > 601000) + alphaApproximationThingy = 0; + + //gotta love that RNG + if ((alphaApproximationThingy & 0xFF) >= alpha) + return; + } + + *dcolor = scolor; +#endif +} + +FORCE_INLINE int swrIntAlpha(float alphaf) +{ + return (int)(alphaf * 256); +} + +FORCE_INLINE bool swrMustRotate(float angleDeg) +{ + int angleDegInt = (int)(angleDeg * 4); + angleDegInt %= 360*4; + + if (angleDegInt > 180*4) + angleDegInt -= 360*4; + + return swrAbs(angleDegInt) >= 1; // 0.25 degrees +} + +FORCE_INLINE bool swrMustRotateTolerant(float angleDeg) +{ + int angleDegInt = (int)(angleDeg * 16); + angleDegInt %= 360*16; + + if (angleDegInt > 180*16) + angleDegInt -= 360*16; + + return swrAbs(angleDegInt) >= 1; // 1/16 of a degree +} + +FORCE_INLINE int swrSgn(float x) +{ + if (x < 0) return -1; + return 1; +} + +FORCE_INLINE int swrFloor(float x) +{ + int i = (int) x; + return i - (x < (float) i); +} + +FORCE_INLINE int swrCeiling(float x) +{ + int i = (int) x; + return i + (x > (float) i); +} + +static SWTexture* swrCreateTexture(const uint8_t* srcBuffer, int width, int height) +{ + SWTexture* txt = (SWTexture*) safeMalloc(sizeof(SWTexture)); + txt->buffer = (uintpixel_t*) safeMalloc(width * height * sizeof(uintpixel_t)); + + const uint32_t* rgbaSrc = (const uint32_t*) srcBuffer; + + size_t sz = width * height; + + if (srcBuffer) + { + for (size_t i = 0; i < sz; i++) + txt->buffer[i] = swrConvertPixelTexture(rgbaSrc[i]); + } + + txt->width = (uint16_t) width; + txt->height = (uint16_t) height; + + return txt; +} + +static void swrFreeTexture(SWTexture* texture) +{ + free(texture->buffer); + free(texture); +} + +static bool swrAddTextureIndexToLRU(SWRenderer* swr, int textureIndex) +{ + uint32_t newIndex = (swr->textureIndexLRUHead + 1) % TEXTURE_LRU_LENGTH; + if (newIndex == swr->textureIndexLRUTail) { + // about to collide with tail from the other side -- nope. + return false; + } + + swr->textureIndexLRU[swr->textureIndexLRUHead] = textureIndex; + swr->textureIndexLRUHead = newIndex; + return true; +} + +static int swrTailTextureIndexLRU(SWRenderer* swr, bool remove) +{ + if (swr->textureIndexLRUHead == swr->textureIndexLRUTail) + return -1; + + uint32_t textureIndex = swr->textureIndexLRU[swr->textureIndexLRUTail]; + + if (remove) + swr->textureIndexLRUTail = (swr->textureIndexLRUTail + 1) % TEXTURE_LRU_LENGTH; + + return textureIndex; +} + +static void swrEvictTextureFromCache(SWRenderer* swr, int textureIndex) +{ + SWTexture* texture = swr->textures[textureIndex]; + swr->textures[textureIndex] = NULL; + + swrFreeTexture(texture); +} + +// Lazily decodes and uploads a TXTR page on first access. +// Returns true if the texture is ready, false if it failed to decode. +static bool swrEnsureTextureIsLoaded(SWRenderer* swr, uint32_t pageId) +{ + if (swr->textures[pageId]) + return true; + + DataWin* dw = swr->base.dataWin; + Texture* txtr = &dw->txtr.textures[pageId]; + + int w, h; + bool gm2022_5 = DataWin_isVersionAtLeast(dw, 2022, 5, 0, 0); + + uint8_t* pixels = NULL; + + do + { + pixels = ImageDecoder_decodeToRgba(txtr->blobData, (size_t) txtr->blobSize, gm2022_5, &w, &h); + if (pixels) + break; + + fprintf(stderr, "swr: Failed to decode TXTR page %u. This is likely because we're out of memory, so evicting a texture.\n", pageId); + + int tail = swrTailTextureIndexLRU(swr, true); + if (tail == -1) { + fprintf(stderr, "swr: Looks like we can't fit this texture in memory at all. Bummer.\n"); + break; + } + + swrEvictTextureFromCache(swr, tail); + fprintf(stderr, "swr: Evicted texture %d, trying again.\n", tail); + } + while (!pixels); + + if (pixels == nullptr) { + fprintf(stderr, "swr: Failed to decode TXTR page %u.\n", pageId); + return false; + } + + swr->textures[pageId] = swrCreateTexture(pixels, w, h); + free(pixels); + + fprintf(stderr, "SWR: Loaded TXTR page %u (%dx%d)\n", pageId, w, h); + + // add it to the LRU + do + { + bool added = swrAddTextureIndexToLRU(swr, pageId); + if (added) + break; + + int tail = swrTailTextureIndexLRU(swr, true); + if (tail == -1) { + fprintf(stderr, "swr: Come on now.\n"); + assert(tail != -1); + } + + swrEvictTextureFromCache(swr, tail); + } + while (true); + + return true; +} + +static void SWRenderer_init(Renderer* renderer, DataWin* dataWin) +{ + SWRenderer* swr = (SWRenderer*) renderer; + + renderer->dataWin = dataWin; + + //allocate texture buffer + swr->textureCount = dataWin->txtr.count; + swr->surfaceCount = SURFACE_MAX_COUNT; + swr->totalTextureCount = swr->textureCount + swr->surfaceCount; + swr->textures = (SWTexture**) safeCalloc(swr->totalTextureCount, sizeof(SWTexture*)); + + //allocate texture LRU cache to allow for dynamic unloading of textures + swr->textureIndexLRU = (uint32_t*) safeCalloc(TEXTURE_LRU_LENGTH, sizeof(uint32_t)); + swr->textureIndexLRUHead = 0; + swr->textureIndexLRUTail = 0; + + //HACK: this isn't good, really. This should seriously be refactored. + //expand datawin's tpag items list to include our surface count. + swr->originalTPagCount = dataWin->tpag.count; + dataWin->tpag.items = (TexturePageItem*) safeRealloc(dataWin->tpag.items, sizeof(TexturePageItem) * (dataWin->tpag.count + swr->surfaceCount)); + dataWin->tpag.count += swr->surfaceCount; + + swr->originalSpriteCount = dataWin->sprt.count; + + for (size_t i = swr->originalTPagCount; i < dataWin->tpag.count; i++) + { + memset(&dataWin->tpag.items[i], 0, sizeof(TexturePageItem)); + dataWin->tpag.items[i].texturePageId = -1; + } + + fprintf(stderr, "SWRenderer initialized.\n"); +} + +static void SWRenderer_destroy(Renderer* renderer) +{ + SWRenderer* swr = (SWRenderer*) renderer; + + (void) swr; + + fprintf(stderr, "SWRenderer destroyed.\n"); +} + +static void SWRenderer_beginFrame(Renderer* renderer, int32_t gameW, int32_t gameH, int32_t windowW, int32_t windowH) +{ + SWRenderer* swr = (SWRenderer*) renderer; + swr->gameW = gameW; + swr->gameH = gameH; + swr->drawingToSurface = false; + if (swr->width != windowW || swr->height != windowH) + { + //allocate frame buffer + free(swr->fb); + swr->fb = (uintpixel_t*) safeMalloc(windowW * windowH * sizeof(uintpixel_t)); + swr->fbPitch = windowW; + swr->width = windowW; + swr->height = windowH; + } +} + +// This used to be just one, "endFrame". Not sure what the different is. +static void SWRenderer_endFrameInit(Renderer* renderer) +{ + (void) renderer; + + //this is kinda useless to do twice isn't it? +} + +static void SWRenderer_endFrameEnd(Renderer* renderer) +{ + SWRenderer* swr = (SWRenderer*) renderer; + assert(!swr->drawingToSurface); + Runner_setNextFrame(swr->fb, swr->width, swr->height); +} + +static void SWRenderer_beginView(Renderer* renderer, int32_t viewX, int32_t viewY, int32_t viewW, int32_t viewH, + int32_t portX, int32_t portY, int32_t portW, int32_t portH, float viewAngle) +{ + (void)renderer; (void)viewX; (void)viewY; (void)viewW; (void)viewH; + (void)portX; (void)portY; (void)portW; (void)portH; (void)viewAngle; + UNIMP2(); + + SWRenderer* swr = (SWRenderer*) renderer; + + float xratio, yratio; + + if (swr->drawingToSurface) { + UNIMP(); + xratio = 1.0f; + yratio = 1.0f; + portX = (int)(portX * xratio); + portY = (int)(portY * yratio); + } + else { + float scaleX = (float) swr->width / swr->gameW; + float scaleY = (float) swr->height / swr->gameH; + float scale = (scaleX < scaleY) ? scaleX : scaleY; + + int32_t scaledW = (int32_t)(swr->gameW * scale); + int32_t scaledH = (int32_t)(swr->gameH * scale); + int32_t offsetX = (swr->width - scaledW) / 2; + int32_t offsetY = (swr->height - scaledH) / 2; + + xratio = scale; + yratio = scale; + + portX = (int)(portX * xratio) + offsetX; + portY = (int)(portY * yratio) + offsetY; + } + portW = (int)(portW * xratio); + portH = (int)(portH * yratio); + + swr->viewActive = true; + swr->viewX = viewX; + swr->viewY = viewY; + swr->viewW = viewW; + swr->viewH = viewH; + swr->portX = portX; + swr->portY = portY; + swr->portW = portW; + swr->portH = portH; + swr->maxX = portX + portW; + swr->maxY = portY + portH; +} + +static void SWRenderer_endView(Renderer* renderer) +{ + (void)renderer; + UNIMP2(); + + SWRenderer* swr = (SWRenderer*) renderer; + swr->viewActive = false; + + swr->portX = swr->viewX = 0; + swr->portY = swr->viewY = 0; + swr->portW = swr->viewW = swr->width; + swr->portH = swr->viewH = swr->height; +} + +static void SWRenderer_beginGUI(Renderer* renderer, int32_t guiW, int32_t guiH, + int32_t portX, int32_t portY, int32_t portW, int32_t portH, int32_t targetSurfaceId) +{ + (void)renderer; (void)guiW; (void)guiH; + (void)portX; (void)portY; (void)portW; (void)portH; + (void)targetSurfaceId; + UNIMP2(); +} + +static void SWRenderer_setGuiProjection(Renderer* renderer, int32_t guiW, int32_t guiH, int32_t portW, int32_t portH, bool renderingToUserSurface) +{ + (void) renderer; + (void) guiW; (void) guiH; + (void) portW; (void) portH; + (void) renderingToUserSurface; + UNIMP(); +} + +static void SWRenderer_endGUI(Renderer* renderer) +{ + (void)renderer; + UNIMP2(); +} + +// TODO[MrPowerGamerBR]: This is supposed to be refactored, not to modify data.win structs directly. +static int32_t swrFindSurfaceTextureSlot(SWRenderer* swr) +{ + // NOTE: dynamic textures are not enrolled into the eviction cache for + // hopefully obvious reasons ... + for (size_t i = swr->textureCount; i != swr->totalTextureCount; i++) + { + if (swr->textures[i] == NULL) { + return (int32_t) i; + } + } + + return -1; +} + +static int32_t swrFindSurfaceTPagSlot(SWRenderer* swr) +{ + DataWin* dw = swr->base.dataWin; + for (size_t i = swr->originalTPagCount; i != dw->tpag.count; i++) + { + if (dw->tpag.items[i].texturePageId == -1) { + return (int32_t) i; + } + } + + return -1; +} + +static void swrTransformPosIfNeeded(SWRenderer* swr, float* dx, float* dy) +{ + if (!swr->viewActive) return; + + if (dx) { + float xscale = ((float)swr->portW / swr->viewW); + *dx -= swr->viewX; + *dx *= xscale; + *dx += swr->portX; + } + if (dy) { + float yscale = ((float)swr->portH / swr->viewH); + *dy -= swr->viewY; + *dy *= yscale; + *dy += swr->portY; + } +} + +static void swrTransformSizeIfNeeded(SWRenderer* swr, float* dx, float* dy) +{ + if (!swr->viewActive || !swr->viewW || !swr->viewH) return; + + if (dx) *dx *= ((float)swr->portW / swr->viewW); + if (dy) *dy *= ((float)swr->portH / swr->viewH); +} + +static void swrTransformPosIntIfNeeded(SWRenderer* swr, int32_t* dx, int32_t* dy) +{ + if (!swr->viewActive) return; + + if (dx) { + *dx -= swr->viewX; + *dx = *dx * swr->portW / swr->viewW; + *dx += swr->portX; + } + if (dy) { + *dy -= swr->viewY; + *dy = *dy * swr->portH / swr->viewH; + *dy += swr->portY; + } +} + +static void swrTransformSizeIntIfNeeded(SWRenderer* swr, int32_t* dx, int32_t* dy) +{ + if (!swr->viewActive || !swr->viewW || !swr->viewH) return; + + if (dx) *dx = *dx * swr->portW / swr->viewW; + if (dy) *dy = *dy * swr->portH / swr->viewH; +} + +static void swrReverseTransformSizeIntIfNeeded(SWRenderer* swr, int32_t* dx, int32_t* dy) +{ + if (!swr->viewActive) return; + + if (dx) *dx = *dx * swr->viewW / swr->portW; + if (dy) *dy = *dy * swr->viewH / swr->portH; +} + +FORCE_INLINE void swrPlotPixel(Renderer* renderer, int x, int y, uintpixel_t color, int alpha) +{ + SWRenderer* swr = (SWRenderer*) renderer; + + if (x < swr->portX || y < swr->portY) return; + if (x >= swr->maxX || y >= swr->maxY) return; + + alphaBlend(&swr->fb[y * swr->fbPitch + x], color, alpha); +} + +static void swrDrawHLineInt(Renderer* renderer, int dx, int dy, int dw, uintpixel_t color, UNUSED uintpixel_t color2, int alpha) +{ + SWRenderer *swr = (SWRenderer*) renderer; + + if (dy < swr->portY) return; + if (dy >= swr->maxY) return; + if (dx < swr->portX) { dw += dx - swr->portX; dx = swr->portX; } + if (dx + dw >= swr->maxX) dw = swr->maxX - dx; + if (dw <= 0) return; + +#if PIXEL_SIZE == 32 + if (color == color2) +#endif + { + uintpixel_t *line = &swr->fb[dy * swr->fbPitch + dx]; + for (int i = 0; i < dw; i++) + alphaBlend(&line[i], color, alpha); + } +#if PIXEL_SIZE == 32 + else + { + Pixel32ARGB clr1, clr2; + clr1.l = color; + clr2.l = color2; + + uint32_t rinit = clr1.p.r << 20; + uint32_t ginit = clr1.p.g << 20; + uint32_t binit = clr1.p.b << 20; + int32_t rstep = ((int)clr2.p.r - clr1.p.r) << 20; + int32_t gstep = ((int)clr2.p.g - clr1.p.g) << 20; + int32_t bstep = ((int)clr2.p.b - clr1.p.b) << 20; + rstep /= dw; + gstep /= dw; + bstep /= dw; + + uintpixel_t *line = &swr->fb[dy * swr->fbPitch + dx]; + for (int i = 0; i < dw; i++) + { + Pixel32ARGB resultPixel; + resultPixel.p.r = rinit >> 20; + resultPixel.p.g = ginit >> 20; + resultPixel.p.b = binit >> 20; + rinit += rstep; + ginit += gstep; + binit += bstep; + alphaBlend(&line[i], resultPixel.l, alpha); + } + } +#endif +} + +static void swrDrawHLine(Renderer* renderer, float dx, float dy, float dw, uintpixel_t color, uintpixel_t color2, float alpha) +{ + SWRenderer *swr = (SWRenderer*) renderer; + float thickness = 1; + + swrTransformPosIfNeeded(swr, &dx, &dy); + swrTransformSizeIfNeeded(swr, &dw, &thickness); + + // TODO: use thickness + swrDrawHLineInt(renderer, swrFloor(dx), swrFloor(dy), swrCeiling(dw), color, color2, swrIntAlpha(alpha)); +} + +static void swrDrawVLineInt(Renderer* renderer, int dx, int dy, int dh, uintpixel_t color, UNUSED uintpixel_t color2, int alpha) +{ + SWRenderer *swr = (SWRenderer*) renderer; + + if (dx < swr->portX) return; + if (dx >= swr->maxX) return; + if (dy < swr->portY) { dh += dy - swr->portY; dy = swr->portY; } + if (dy + dh >= swr->maxY) dh = swr->maxY - dy; + if (dh <= 0) return; + +#if PIXEL_SIZE == 32 + if (color == color2) +#endif + { + for (int i = 0; i < dh; i++) + { + uintpixel_t *line = &swr->fb[(dy + i) * swr->fbPitch + dx]; + alphaBlend(&line[0], color, alpha); + } + } +#if PIXEL_SIZE == 32 + else + { + Pixel32ARGB clr1, clr2; + clr1.l = color; + clr2.l = color2; + + uint32_t rinit = clr1.p.r << 20; + uint32_t ginit = clr1.p.g << 20; + uint32_t binit = clr1.p.b << 20; + int32_t rstep = ((int)clr2.p.r - clr1.p.r) << 20; + int32_t gstep = ((int)clr2.p.g - clr1.p.g) << 20; + int32_t bstep = ((int)clr2.p.b - clr1.p.b) << 20; + rstep /= dh; + gstep /= dh; + bstep /= dh; + + for (int i = 0; i < dh; i++) + { + uintpixel_t *line = &swr->fb[(dy + i) * swr->fbPitch + dx]; + Pixel32ARGB resultPixel; + resultPixel.p.r = rinit >> 20; + resultPixel.p.g = ginit >> 20; + resultPixel.p.b = binit >> 20; + rinit += rstep; + ginit += gstep; + binit += bstep; + alphaBlend(&line[0], resultPixel.l, alpha); + } + } +#endif +} + +static void swrDrawVLine(Renderer* renderer, float dx, float dy, float dh, uintpixel_t color, uintpixel_t color2, float alpha) +{ + SWRenderer *swr = (SWRenderer*) renderer; + float thickness = 1; + + swrTransformPosIfNeeded(swr, &dx, &dy); + swrTransformSizeIfNeeded(swr, &thickness, &dh); + + // TODO: use thickness + swrDrawVLineInt(renderer, swrFloor(dx), swrFloor(dy), swrCeiling(dh), color, color2, swrIntAlpha(alpha)); +} + +static void swrDrawRectangle(Renderer* renderer, float x1, float y1, float x2, float y2, uintpixel_t color, float alpha) +{ + swrDrawHLine(renderer, x1, y1, (x2 - x1) + 1, color, color, alpha); + swrDrawHLine(renderer, x1, y2, (x2 - x1) + 1, color, color, alpha); + swrDrawVLine(renderer, x1, y1, (y2 - y1) + 1, color, color, alpha); + swrDrawVLine(renderer, x2, y1, (y2 - y1) + 1, color, color, alpha); +} + +static void swrDrawRectangleColor(Renderer* renderer, float x1, float y1, float x2, float y2, uintpixel_t color1, uintpixel_t color2, uintpixel_t color3, uintpixel_t color4, float alpha) +{ + swrDrawHLine(renderer, x1, y1, (x2 - x1) + 1, color1, color2, alpha); + swrDrawHLine(renderer, x1, y2, (x2 - x1) + 1, color3, color4, alpha); + swrDrawVLine(renderer, x1, y1, (y2 - y1) + 1, color1, color3, alpha); + swrDrawVLine(renderer, x2, y1, (y2 - y1) + 1, color2, color4, alpha); +} + +static void swrDrawLineInt(Renderer* renderer, int x1, int y1, int x2, int y2, MAYBE_UNUSED int width, uintpixel_t color1, uintpixel_t color2, int alpha) +{ + if (x1 == x2) + { + swrDrawVLineInt(renderer, x1, swrMin(y1, y2), swrAbs(y1 - y2), color1, color2, alpha); + return; + } + if (y1 == y2) + { + swrDrawHLineInt(renderer, swrMin(x1, x2), y1, swrAbs(x1 - x2), color1, color2, alpha); + return; + } + + int dx = x2 - x1, dy = y2 - y1; + int dx1 = swrAbs(dx), dy1 = swrAbs(dy), xe, ye, x, y; + int px = 2 * dy1 - dx1, py = 2 * dx1 - dy1; + + uintpixel_t color = color1; +#if PIXEL_SIZE == 32 + Pixel32ARGB clr1, clr2; + clr1.l = color1; + clr2.l = color2; + + uint32_t rinit = clr1.p.r << 20; + uint32_t ginit = clr1.p.g << 20; + uint32_t binit = clr1.p.b << 20; + int32_t rstep = ((int)clr2.p.r - clr1.p.r) << 20; + int32_t gstep = ((int)clr2.p.g - clr1.p.g) << 20; + int32_t bstep = ((int)clr2.p.b - clr1.p.b) << 20; +#endif + + if (dy1 <= dx1) + { + if (dx >= 0) + { + x = x1, y = y1, xe = x2; + } + else + { + x = x2, y = y2, xe = x1; + } + +#if PIXEL_SIZE == 32 + if (dx1 > 0) { + rstep /= dx1; + gstep /= dx1; + bstep /= dx1; + } else { + rstep = gstep = bstep = 0; + } +#endif + + swrPlotPixel(renderer, x, y, color, alpha); + + for (int i = 0; x < xe; i++) + { + x++; + if (px < 0) + { + px += 2 * dy1; + } + else + { + if ((dx < 0 && dy < 0) || (dx > 0 && dy > 0)) y++; else y--; + px += 2 * (dy1 - dx1); + } + +#if PIXEL_SIZE == 32 + Pixel32ARGB resultPixel; + resultPixel.p.r = rinit >> 20; + resultPixel.p.g = ginit >> 20; + resultPixel.p.b = binit >> 20; + rinit += rstep; + ginit += gstep; + binit += bstep; + color = resultPixel.l; +#endif + + swrPlotPixel(renderer, x, y, color, alpha); + } + } + else + { + if (dy >= 0) + { + x = x1, y = y1, ye = y2; + } + else + { + x = x2, y = y2, ye = y1; + } + +#if PIXEL_SIZE == 32 + if (dy1 > 0) { + rstep /= dy1; + gstep /= dy1; + bstep /= dy1; + } else { + rstep = gstep = bstep = 0; + } +#endif + + swrPlotPixel(renderer, x, y, color, alpha); + + for (int i = 0; y < ye; i++) + { + y++; + if (py <= 0) + { + py += 2 * dx1; + } + else + { + if ((dx < 0 && dy < 0) || (dx > 0 && dy > 0)) x++; else x--; + py += 2 * (dx1 - dy1); + } + +#if PIXEL_SIZE == 32 + Pixel32ARGB resultPixel; + resultPixel.p.r = rinit >> 20; + resultPixel.p.g = ginit >> 20; + resultPixel.p.b = binit >> 20; + rinit += rstep; + ginit += gstep; + binit += bstep; + color = resultPixel.l; +#endif + + swrPlotPixel(renderer, x, y, color, alpha); + } + } +} + +static void swrDrawLine(Renderer* renderer, float x1, float y1, float x2, float y2, float width, uintpixel_t color, uintpixel_t color2, float alpha) +{ + SWRenderer* swr = (SWRenderer*) renderer; + swrTransformPosIfNeeded(swr, &x1, &y1); + swrTransformPosIfNeeded(swr, &x2, &y2); + swrTransformSizeIfNeeded(swr, &width, NULL); + swrDrawLineInt(renderer, swrFloor(x1), swrFloor(y1), swrCeiling(x2), swrCeiling(y2), swrCeiling(width), color, color2, swrIntAlpha(alpha)); +} + +static void swrDrawSpriteInternal( + Renderer* renderer, int dx, int dy, int dw, int dh, + SWTexture* texture, int sx, int sy, int sw, int sh, + uintpixel_t tintColor, int alpha +) +{ + SWRenderer *swr = (SWRenderer*) renderer; + + bool flipX = false, flipY = false; + if (dw < 0) { dx += dw; dw = -dw; flipX = true; } + if (dh < 0) { dy += dh; dh = -dh; flipY = true; } + + //basic out of bound checks + if (dw == 0 || dh == 0) return; + if (sw == 0) sw = 1; + if (sh == 0) sh = 1; + if (dx + dw <= swr->portX) return; + if (dy + dh <= swr->portY) return; + if (dx >= swr->maxX) return; + if (dy >= swr->maxY) return; + + int odw = dw, odh = dh; + int osw = sw, osh = sh; + + int minx = swr->portX, miny = swr->portY, maxx = swr->portX + swr->portW, maxy = swr->portY + swr->portH; + + //out of bounds adjustment checks + int diffxl = 0, diffyl = 0, diffxu = 0, diffyu = 0; + if (dx < minx) { diffxl = minx - dx; dx = minx; dw -= diffxl; } + if (dy < miny) { diffyl = miny - dy; dy = miny; dh -= diffyl; } + if (dx + dw > maxx) { diffxu = dx + dw - maxx; dw -= diffxu; } + if (dy + dh > maxy) { diffyu = dy + dh - maxy; dh -= diffyu; } + + if (diffxl != 0 || diffyl != 0 || diffxu != 0 || diffyu != 0) + { + //adjust source coordinates too + diffxl = (int)((long)diffxl * osw / odw); + diffyl = (int)((long)diffyl * osh / odh); + diffxu = (int)((long)(diffxu + 1) * osw / odw); + diffyu = (int)((long)(diffyu + 1) * osh / odh); + sx += flipX ? diffxu : diffxl; + sy += flipY ? diffyu : diffyl; + sw -= diffxl + diffxu; + sh -= diffyl + diffyu; + if (sw <= 0 || sh <= 0) return; + } + + //clip the source coords into bounds too + if (sx < 0) { sw += sx; sx = 0; } + if (sy < 0) { sh += sy; sy = 0; } + if (sx + sw >= texture->width) { sw = texture->width - sx; } + if (sy + sh >= texture->height) { sh = texture->height - sy; } + + //okay, now we can finally get on with rendering + + int ixs = 0, oxs = 1, iys = 0, oys = 1; + if (flipX) ixs = dw - 1, oxs = -1; + if (flipY) iys = dh - 1, oys = -1; + + // tweak these if stuff doesn't look right + typedef int32_t fixedp_t; + const int fp_prec = 14; + + fixedp_t ystep = (sh == dh) ? (1 << fp_prec) : ((fixedp_t) osh << fp_prec) / odh; + fixedp_t xstep = (sw == dw) ? (1 << fp_prec) : ((fixedp_t) osw << fp_prec) / odw; + fixedp_t oxs2 = oxs * xstep; + fixedp_t oys2 = oys * ystep; + fixedp_t ixs2 = ixs * xstep; + fixedp_t iys2 = iys * ystep; + + if (sw == dw) + { + fixedp_t ys2 = (fixedp_t) iys2; + for (int y = 0, ys = iys; y < dh; y++, ys += oys, ys2 += oys2) + { + uintpixel_t* dstline; + const uintpixel_t* srcline; + dstline = &swr->fb[(dy + y) * swr->fbPitch + dx]; + if (dh == sh) + srcline = &texture->buffer[(sy + ys) * texture->width + sx]; + else + srcline = &texture->buffer[(sy + (int)(ys2 >> fp_prec)) * texture->width + sx]; + + for (int x = 0, xs = ixs; x < dw; x++, xs += oxs) + { + uintpixel_t pixel = srcline[xs]; + if (opaque(pixel)) + alphaBlend(&dstline[x], tint(tintColor, pixel), alpha); + } + } + } + else + { + fixedp_t ys2 = iys2; + for (int y = 0, ys = iys; y < dh; y++, ys += oys, ys2 += oys2) + { + uintpixel_t* dstline; + const uintpixel_t* srcline; + dstline = &swr->fb[(dy + y) * swr->fbPitch + dx]; + if (dh == sh) + srcline = &texture->buffer[(sy + ys) * texture->width + sx]; + else + srcline = &texture->buffer[(sy + (int)(ys2 >> fp_prec)) * texture->width + sx]; + + fixedp_t xs2 = ixs2; + for (int x = 0, xs = ixs; x < dw; x++, xs += oxs, xs2 += oxs2) + { + uintpixel_t pixel = srcline[(int)(xs2 >> fp_prec)]; + if (opaque(pixel)) + alphaBlend(&dstline[x], tint(tintColor, pixel), alpha); + } + } + } +} + +static void swrDrawSprite( + Renderer* renderer, float dx, float dy, float dw, float dh, + SWTexture* texture, int sx, int sy, int sw, int sh, + uint32_t tintColor, float alpha +) +{ + SWRenderer *swr = (SWRenderer*) renderer; + + swrTransformPosIfNeeded(swr, &dx, &dy); + swrTransformSizeIfNeeded(swr, &dw, &dh); + + swrDrawSpriteInternal( + renderer, + swrFloor(dx), + swrFloor(dy), + swrCeiling(dw), + swrCeiling(dh), + texture, + sx, sy, + sw, sh, + swrConvertPixel(tintColor), + swrIntAlpha(alpha) + ); +} + +static void swrDrawSpriteRotatedInternal( + Renderer* renderer, int dx, int dy, int dw, int dh, + SWTexture* texture, int sx, int sy, int sw, int sh, + uintpixel_t tintColor, int alpha, + float angleDeg, + float pivotX, + float pivotY +) +{ + SWRenderer* swr = (SWRenderer*) renderer; + float angleRad = -angleDeg * M_PI / 180.0f; + + bool flipX = false, flipY = false; + if (dw < 0) { dw = -dw; dx -= dw; pivotX = dw - pivotX; flipX = true; } + if (dh < 0) { dh = -dh; dy -= dh; pivotY = dh - pivotY; flipY = true; } + + float cosA = cosf(angleRad); + float sinA = sinf(angleRad); + + float cnrx[4], cnry[4]; + cnrx[0] = cnrx[3] = dx; + cnry[0] = cnry[1] = dy; + cnrx[1] = cnrx[2] = dx + dw; + cnry[2] = cnry[3] = dy + dh; + + float pxa = pivotX + dx; + float pya = pivotY + dy; + + float minXf = FLT_MAX, minYf = FLT_MAX, maxXf = -FLT_MAX, maxYf = -FLT_MAX; + for (int i = 0; i < 4; i++) + { + float cxi = cnrx[i] - pxa; + float cyi = cnry[i] - pya; + float rx = cosA * cxi - sinA * cyi + pxa; + float ry = sinA * cxi + cosA * cyi + pya; + if (minXf > rx) minXf = rx; + if (maxXf < rx) maxXf = rx; + if (minYf > ry) minYf = ry; + if (maxYf < ry) maxYf = ry; + } + + // minX, minY, maxX, maxY now represent an AABB of pixels we should loop over + int minX = swrFloor(minXf); + int minY = swrFloor(minYf); + int maxX = swrCeiling(maxXf); + int maxY = swrCeiling(maxYf); + + // basic out-of-bound checks + if (maxX < swr->portX) return; + if (maxY < swr->portY) return; + if (minX >= swr->maxX) return; + if (minY >= swr->maxY) return; + + // however, we'll need to clip it against out of bounds first + int minXc = minX, minYc = minY, maxXc = maxX, maxYc = maxY; + int minx = swr->portX, miny = swr->portY, maxx = swr->portX + swr->portW, maxy = swr->portY + swr->portH; + + if (minXc < minx) minXc = minx; + if (minYc < miny) minYc = miny; + if (maxXc >= maxx) maxXc = maxx; + if (maxYc >= maxy) maxYc = maxy; + + // some final clip checks + if (minXc >= maxXc || minYc >= maxYc) return; + + int sox = flipX ? sw - 1 : 0; + int soy = flipY ? sh - 1 : 0; + int six = flipX ? -1 : 1; + int siy = flipY ? -1 : 1; + + float sw_dw = (float) sw / dw; + float sh_dh = (float) sh / dh; + + for (int cy = minYc; cy < maxYc; cy++) + { + uintpixel_t *dstline = &swr->fb[cy * swr->fbPitch]; + for (int cx = minXc; cx < maxXc; cx++) + { + // we need to determine the texture-space coordinate of cx/cy + float ox = (float) cx + 0.5f - pxa; + float oy = (float) cy + 0.5f - pya; + + // "undo" the rotation + float lx = cosA * ox + sinA * oy; + float ly = -sinA * ox + cosA * oy; + + // turn it into a texture-local coordinate + lx += pxa - dx; + ly += pya - dy; + + if (lx < 0 || ly < 0 || lx >= (float) dw || ly >= (float) dh) continue; + + lx = lx * sw_dw; + ly = ly * sh_dh; + + int tx = (int)(sox + lx * six); + int ty = (int)(soy + ly * siy); + + if (tx < 0) tx = 0; + if (ty < 0) ty = 0; + if (tx >= sw) tx = sw - 1; + if (ty >= sh) ty = sh - 1; + + tx += sx; + ty += sy; + + uintpixel_t src = texture->buffer[ty * texture->width + tx]; + + if (opaque(src)) + alphaBlend(&dstline[cx], tint(tintColor, src), alpha); + } + } +} + +static void swrDrawSpriteRotated( + Renderer* renderer, float dx, float dy, float dw, float dh, + SWTexture* texture, int sx, int sy, int sw, int sh, + uint32_t tintColor, float alpha, + float angleDeg, + float pivotX, + float pivotY +) +{ + SWRenderer* swr = (SWRenderer*) renderer; + + swrTransformPosIfNeeded(swr, &dx, &dy); + swrTransformSizeIfNeeded(swr, &pivotX, &pivotY); + swrTransformSizeIfNeeded(swr, &dw, &dh); + + swrDrawSpriteRotatedInternal( + renderer, + swrFloor(dx), + swrFloor(dy), + swrCeiling(dw), + swrCeiling(dh), + texture, + sx, sy, + sw, sh, + swrConvertPixel(tintColor), + swrIntAlpha(alpha), + angleDeg, + pivotX, + pivotY + ); +} + +static void SWRenderer_drawSprite(Renderer* renderer, int32_t tpagIndex, float x, float y, + float originX, float originY, float xscale, float yscale, + float angleDeg, uint32_t color, float alpha) +{ + SWRenderer* swr = (SWRenderer*) renderer; + DataWin* dwin = renderer->dataWin; + + if (tpagIndex < 0 || (uint32_t) tpagIndex >= dwin->tpag.count) { + fprintf(stderr, "%s: tpagIndex of %d is invalid\n", __func__, tpagIndex); + return; + } + + TexturePageItem* tpag = &dwin->tpag.items[tpagIndex]; + int16_t pageId = tpag->texturePageId; + if (0 > pageId || swr->totalTextureCount <= (uint32_t) pageId) { + fprintf(stderr, "%s: tpagIndex of %d is invalid, as pageId of %d is invalid\n", __func__, tpagIndex, pageId); + return; + } + if (!swrEnsureTextureIsLoaded(swr, (uint32_t) pageId)) { + fprintf(stderr, "%s: could not ensure texture is loaded, tpagIndex: %d, pageId: %d\n", __func__, tpagIndex, pageId); + return; + } + + int sx = tpag->sourceX; + int sy = tpag->sourceY; + int sw = tpag->sourceWidth; + int sh = tpag->sourceHeight; + + float dx = (float)(tpag->targetX - originX); + float dy = (float)(tpag->targetY - originY); + int dw = (int)(xscale * tpag->targetWidth); + int dh = (int)(yscale * tpag->targetHeight); + dx *= xscale; + dy *= yscale; + dx += x; + dy += y; + + SWTexture* texture = swr->textures[pageId]; + + if (UNLIKELY(swrMustRotate(angleDeg))) + { + float pivotX = (x - dx) * swrSgn(xscale); + float pivotY = (y - dy) * swrSgn(yscale); + + if (tpag->targetWidth != tpag->sourceWidth) + pivotX *= (float)tpag->targetWidth / tpag->sourceWidth; + if (tpag->targetHeight != tpag->sourceHeight) + pivotY *= (float)tpag->targetHeight/ tpag->sourceHeight; + + swrDrawSpriteRotated(renderer, dx, dy, dw, dh, texture, sx, sy, sw, sh, color, alpha, angleDeg, pivotX, pivotY); + } + else + { + swrDrawSprite(renderer, dx, dy, dw, dh, texture, sx, sy, sw, sh, color, alpha); + } +} + +static void SWRenderer_drawSpritePart(Renderer* renderer, int32_t tpagIndex, + int32_t srcOffX, int32_t srcOffY, int32_t srcW, int32_t srcH, + float x, float y, float xscale, float yscale, float angleDeg, + float pivotX, float pivotY, uint32_t color, float alpha) +{ + SWRenderer* swr = (SWRenderer*) renderer; + DataWin* dwin = renderer->dataWin; + + if (tpagIndex < 0 || (uint32_t) tpagIndex >= dwin->tpag.count) return; + + TexturePageItem* tpag = &dwin->tpag.items[tpagIndex]; + int16_t pageId = tpag->texturePageId; + if (0 > pageId || swr->totalTextureCount <= (uint32_t) pageId) return; + if (!swrEnsureTextureIsLoaded(swr, (uint32_t) pageId)) return; + + int sx = tpag->sourceX + srcOffX; + int sy = tpag->sourceY + srcOffY; + int sw = srcW; + int sh = srcH; + + float dx = x; + float dy = y; + int dw = swrCeiling(xscale * sw); + int dh = swrCeiling(yscale * sh); + + SWTexture* texture = swr->textures[pageId]; + + if (UNLIKELY(swrMustRotate(angleDeg))) + { + swrDrawSpriteRotated(renderer, dx, dy, dw, dh, texture, sx, sy, sw, sh, color, alpha, angleDeg, pivotX * dw, pivotY * dh); + } + else + { + swrDrawSprite(renderer, dx, dy, dw, dh, texture, sx, sy, sw, sh, color, alpha); + } +} + +static void SWRenderer_drawSpritePos(Renderer* renderer, int32_t tpagIndex, + float x1, float y1, float x2, float y2, + float x3, float y3, float x4, float y4, float alpha) +{ + (void)renderer; (void)tpagIndex; + (void)x1; (void)y1; (void)x2; (void)y2; + (void)x3; (void)y3; (void)x4; (void)y4; (void)alpha; + UNIMP(); +} + +static void SWRenderer_drawRectangle(Renderer* renderer, float x1, float y1, float x2, float y2, + uint32_t color, float alpha, bool outline) +{ + uintpixel_t pxcolor = swrConvertPixel(color); + + SWRenderer* swr = (SWRenderer*) renderer; + + if (outline) + { + swrDrawRectangle(renderer, x1, y1, x2, y2, pxcolor, alpha); + } + else + { + swrTransformPosIfNeeded(swr, &x1, &y1); + swrTransformPosIfNeeded(swr, &x2, &y2); + + int alphaInt = swrIntAlpha(alpha); + int x1i = swrFloor(x1), x2i = swrCeiling(x2), y1i = swrFloor(y1), y2i = swrCeiling(y2); + int xd = x2i - x1i; + int yd = y2i - y1i; + if (xd <= 0 || yd <= 0) return; + + for (int y = 0; y <= yd; y++) { + swrDrawHLineInt(renderer, x1i, y1i + y, xd, pxcolor, pxcolor, alphaInt); + } + } +} + +static void SWRenderer_drawRectangleColor(Renderer* renderer, float x1, float y1, float x2, float y2, + uint32_t color1, uint32_t color2, uint32_t color3, uint32_t color4, + float alpha, bool outline) +{ + uintpixel_t pxcolor1 = swrConvertPixel(color1); + uintpixel_t pxcolor2 = swrConvertPixel(color2); + uintpixel_t pxcolor3 = swrConvertPixel(color3); + uintpixel_t pxcolor4 = swrConvertPixel(color4); + + SWRenderer* swr = (SWRenderer*) renderer; + + if (outline) + { + swrDrawRectangleColor(renderer, x1, y1, x2, y2, pxcolor1, pxcolor2, pxcolor3, pxcolor4, alpha); + } + else + { + swrTransformPosIfNeeded(swr, &x1, &y1); + swrTransformPosIfNeeded(swr, &x2, &y2); + + int alphaInt = swrIntAlpha(alpha); + int x1i = swrFloor(x1), x2i = swrCeiling(x2), y1i = swrFloor(y1), y2i = swrCeiling(y2); + int xd = x2i - x1i; + int yd = y2i - y1i; + if (xd <= 0 || yd <= 0) return; + + // TODO: blending vertically + for (int y = 0; y <= yd; y++) { + swrDrawHLineInt(renderer, x1i, y1i + y, xd, pxcolor1, pxcolor2, alphaInt); + } + } +} + +static void SWRenderer_drawLine(Renderer* renderer, float x1, float y1, float x2, float y2, + float width, uint32_t color, float alpha) +{ + (void)renderer; (void)x1; (void)y1; (void)x2; (void)y2; + (void)width; (void)color; (void)alpha; + + uintpixel_t colorCvt = swrConvertPixel(color); + swrDrawLine(renderer, x1, y1, x2, y2, width, colorCvt, colorCvt, alpha); +} + +static void swrDrawTriangleInternal(SWRenderer* swr, int xup, int yup, int xleft, int yleft, int xright, int yright, uint32_t color1, uint32_t color2, uint32_t color3, int alpha) +{ + // TODO: update this + (void) color2; + (void) color3; + + // Figure out the maximum Y extent of the triangle. + // (Note that we know yup is the minimum.) + int xmid, ymid, xmid2 = xup, xmax, ymax; + if (yleft < yright) { + xmax = xright, ymax = yright; + xmid = xleft, ymid = yleft; + if (yright != yup) + xmid2 = xup + (xright - xup) * (ymid - yup) / (yright - yup); + } else { + xmax = xleft, ymax = yleft; + xmid = xright, ymid = yright; + if (yleft != yup) + xmid2 = xup + (xleft - xup) * (ymid - yup) / (yleft - yup); + } + + for (int y = yup; y < ymax; y++) + { + if (y < 0) continue; + if (y >= swr->height) break; + + int x1 = xup, x2 = xup; + if (y <= ymid) + { + // Lines: between up and mid, and between up and max + if (ymid != yup) + x1 = xup + (xmid - xup) * (y - yup) / (ymid - yup); + + if (ymid != yup) + x2 = xup + (xmid2 - xup) * (y - yup) / (ymid - yup); + } + else + { + // Lines: between mid and max, and between up and max + if (ymax != yup) + x1 = xup + (xmax - xup) * (y - yup) / (ymax - yup); + + if (ymax != ymid) + x2 = xmid + (xmax - xmid) * (y - ymid) / (ymax - ymid); + } + + if (x1 >= x2) { + int tmp = x1; + x1 = x2; + x2 = tmp; + } + + if (x1 < swr->portX) x1 = swr->portX; + if (x1 >= swr->maxX) continue; + if (x2 < swr->portX) continue; + if (x2 >= swr->maxX) x2 = swr->maxX - 1; + if (x1 > x2) continue; + + uintpixel_t* line = &swr->fb[y * swr->width]; + for (int x = x1; x < x2; x++) { + alphaBlend(&line[x], color1, alpha); + } + } +} + +static void swrDrawTriangle(Renderer* renderer, float x1, float y1, float x2, float y2, float x3, float y3, uint32_t color1, uint32_t color2, uint32_t color3, float alpha) +{ + float xup, yup, xleft, yleft, xright, yright; + uint32_t colorup, colorleft, colorright; + + SWRenderer* swr = (SWRenderer*) renderer; + swrTransformPosIfNeeded(swr, &x1, &y1); + swrTransformPosIfNeeded(swr, &x2, &y2); + swrTransformPosIfNeeded(swr, &x3, &y3); + + //which vertex is higher? + xup = x1, yup = y1; colorup = color1; + xleft = x2, yleft = y2; colorleft = color2; + xright = x3, yright = y3; colorright = color3; + if (yup > y2) { + xup = x2, yup = y2, colorup = color2; + xleft = x1, yleft = y1, colorleft = color1; + //xright = x3, yright = y3; + } + if (yup > y3) { + xup = x3, yup = y3, colorup = color3; + xleft = x1, yleft = y1, colorleft = color1; + xright = x2, yright = y2, colorright = color2; + } + + if (xleft > xright) { + float tmp = xleft; + xleft = xright; + xright = tmp; + tmp = yleft; + yleft = yright; + yright = tmp; + uint32_t tmp2 = colorleft; + colorleft = colorright; + colorright = tmp2; + } + + swrDrawTriangleInternal( + swr, + swrFloor(xup), swrFloor(yup), + swrFloor(xleft), swrCeiling(yleft), + swrFloor(xright), swrCeiling(yright), + swrConvertPixel(colorup), + swrConvertPixel(colorleft), + swrConvertPixel(colorright), + swrIntAlpha(alpha) + ); +} + +static void SWRenderer_drawTriangle(Renderer* renderer, + float x1, float y1, float x2, float y2, float x3, float y3, + uint32_t color1, uint32_t color2, uint32_t color3, + float alpha, bool outline) +{ + if (outline) + { + uintpixel_t color1cvt = swrConvertPixel(color1); + uintpixel_t color2cvt = swrConvertPixel(color2); + uintpixel_t color3cvt = swrConvertPixel(color3); + swrDrawLine(renderer, x1, y1, x2, y2, 1, color1cvt, color2cvt, renderer->drawAlpha); + swrDrawLine(renderer, x1, y1, x3, y3, 1, color1cvt, color3cvt, renderer->drawAlpha); + swrDrawLine(renderer, x2, y2, x3, y3, 1, color3cvt, color3cvt, renderer->drawAlpha); + } + else + { + swrDrawTriangle(renderer, x1, y1, x2, y2, x3, y3, color1, color2, color3, alpha); + } +} + +static void SWRenderer_drawLineColor(Renderer* renderer, float x1, float y1, float x2, float y2, + float width, uint32_t color1, uint32_t color2, float alpha) +{ + swrDrawLine(renderer, x1, y1, x2, y2, width, swrConvertPixel(color1), swrConvertPixel(color2), alpha); +} + +typedef struct +{ + Font* font; + TexturePageItem* fontTpag; // single TPAG for regular fonts (NULL for sprite fonts) + int fontTpagIndex; + int fontPageId; + Sprite* spriteFontSprite; // source sprite for sprite fonts (NULL for regular fonts) +} +SwrFontState; + +static bool swrResolveFontState(SWRenderer* swr, DataWin* dw, Font* font, SwrFontState* state) +{ + state->font = font; + state->fontTpag = NULL; + state->fontTpagIndex = 0; + state->spriteFontSprite = NULL; + + if (font->isSpriteFont) + { + state->spriteFontSprite = &dw->sprt.sprites[font->spriteIndex]; + } + else + { + state->fontTpagIndex = font->tpagIndex; + if (state->fontTpagIndex < 0) return false; + + state->fontTpag = &dw->tpag.items[state->fontTpagIndex]; + int16_t pageId = state->fontTpag->texturePageId; + if (0 > pageId || (uint32_t) pageId >= swr->totalTextureCount) return false; + if (!swrEnsureTextureIsLoaded(swr, (uint32_t) pageId)) return false; + + state->fontPageId = pageId; + } + + return true; +} + +static bool swrResolveGlyph( + SWRenderer* swr, DataWin* dw, SwrFontState* state, FontGlyph* glyph, float cursorX, float cursorY, + int* tpagIndex, int* pageId, int* sx, int* sy, int* sw, int* sh, float* dx, float* dy +) +{ + Font* font = state->font; + if (font->isSpriteFont && state->spriteFontSprite != NULL) + { + Sprite* sprite = state->spriteFontSprite; + int32_t glyphIndex = (int32_t) (glyph - font->glyphs); + if (0 > glyphIndex || glyphIndex >= (int32_t) sprite->textureCount) return false; + + int32_t tpagIdx = sprite->tpagIndices[glyphIndex]; + if (0 > tpagIdx) return false; + + TexturePageItem* glyphTpag = &dw->tpag.items[tpagIdx]; + int16_t pid = glyphTpag->texturePageId; + if (0 > pid || (uint32_t) pid >= swr->totalTextureCount) return false; + if (!swrEnsureTextureIsLoaded(swr, (uint32_t) pid)) return false; + + *tpagIndex = tpagIdx; + *pageId = glyphTpag->texturePageId; + + *sx = glyphTpag->sourceX; + *sy = glyphTpag->sourceY; + *sw = glyphTpag->sourceWidth; + *sh = glyphTpag->sourceHeight; + + *dx = cursorX + glyph->offset; + *dy = cursorY + glyphTpag->targetY - sprite->originY; + } + else + { + *tpagIndex = state->fontTpagIndex; + *pageId = state->fontPageId; + + *sx = state->fontTpag->sourceX + glyph->sourceX; + *sy = state->fontTpag->sourceY + glyph->sourceY; + *sw = glyph->sourceWidth; + *sh = glyph->sourceHeight; + + *dx = cursorX + glyph->offset; + *dy = cursorY; + } + + return true; +} + +static void swrDrawText(SWRenderer* swr, const char* text, float x, float y, float xscale, float yscale, float angleDeg, int32_t color, float alpha, float lineSeparation) +{ + Renderer* renderer = &swr->base; + DataWin* dwin = renderer->dataWin; + + int32_t fontIndex = renderer->drawFont; + if (0 > fontIndex || dwin->font.count <= (uint32_t) fontIndex) return; + + Font* font = &dwin->font.fonts[fontIndex]; + + SwrFontState fontState; + memset(&fontState, 0, sizeof fontState); // silence warning treated as error + + if (!swrResolveFontState(swr, dwin, font, &fontState)) return; + + // TODO: do we need to mirror the way the text scrolls too?! + float cosA = 1.0f, sinA = 0.0f, angleRad = 0.0f; + bool mustRotate = swrMustRotateTolerant(angleDeg); + if (UNLIKELY(mustRotate)) + { + angleRad = -angleDeg * M_PI / 180.0f; + cosA = cosf(angleRad); + sinA = sinf(angleRad); + } + + int textLen = (int) strlen(text); + int lineCount = TextUtils_countLines(text, textLen); + float lineStride = (0.0f > lineSeparation) ? TextUtils_lineStride(font) : (lineSeparation / (font->scaleY != 0.0f ? font->scaleY : 1.0f)); + + // Vertical alignment offset + float totalHeight = (float) lineCount * lineStride; + float valignOffset = 0; + if (renderer->drawValign == 1) valignOffset = -totalHeight / 2.0f; + else if (renderer->drawValign == 2) valignOffset = -totalHeight; + + xscale *= font->scaleX; + yscale *= font->scaleY; + + // Iterate through lines. HTML5 subtracts ascenderOffset from the per-line y offset + // (see yyFont.GR_Text_Draw), shifting glyphs up so the baseline aligns with the drawn y. + float cursorY = valignOffset - (float) font->ascenderOffset; + int32_t lineStart = 0; + + for (int32_t lineIdx = 0; lineCount > lineIdx; lineIdx++) { + // Find end of current line + int32_t lineEnd = lineStart; + while (textLen > lineEnd && !TextUtils_isNewlineChar(text[lineEnd])) { + lineEnd++; + } + int32_t lineLen = lineEnd - lineStart; + + // Horizontal alignment offset for this line + float lineWidth = TextUtils_measureLineWidth(font, text + lineStart, lineLen); + float halignOffset = 0; + if (renderer->drawHalign == 1) halignOffset = -lineWidth / 2.0f; + else if (renderer->drawHalign == 2) halignOffset = -lineWidth; + + float cursorX = halignOffset; + + // Render each glyph in the line - decode each codepoint once and carry it forward as next iteration's ch (also used for kerning) + int32_t pos = 0; + uint16_t ch = 0; + bool hasCh = false; + if (lineLen > pos) { + ch = TextUtils_decodeUtf8(text + lineStart, lineLen, &pos); + hasCh = true; + } + + while (hasCh) { + FontGlyph* glyph = TextUtils_findGlyph(font, ch); + + uint16_t nextCh = 0; + bool hasNext = lineLen > pos; + if (hasNext) nextCh = TextUtils_decodeUtf8(text + lineStart, lineLen, &pos); + + if (glyph != nullptr) { + bool drewSuccessfully = false; + if (glyph->sourceWidth != 0 && glyph->sourceHeight != 0) { + int fontTpagIndex = 0, pageId = 0; + int sx, sy, sw, sh, dw, dh; + float dx, dy; + if (swrResolveGlyph(swr, dwin, &fontState, glyph, cursorX, cursorY, + &fontTpagIndex, &pageId, &sx, &sy, &sw, &sh, &dx, &dy)) + { + dx *= xscale; dx += x; + dy *= xscale; dy += y; + dw = swrCeiling(xscale * glyph->sourceWidth); + dh = swrCeiling(yscale * glyph->sourceHeight); + + // TODO: at 640x480, for some reason, without this fixup the + // letters in the "Name the fallen human." screen don't shake + dx = roundf(dx * 2) / 2; + dy = roundf(dy * 2) / 2; + + SWTexture* texture = swr->textures[pageId]; + + if (UNLIKELY(mustRotate)) + { + dx -= x; + dy -= y; + float ndx = cosA * dx - sinA * dy; + float ndy = sinA * dx + cosA * dy; + ndx += x; + ndy += y; + swrDrawSpriteRotated(renderer, ndx, ndy, dw, dh, texture, sx, sy, sw, sh, color, alpha, angleDeg, 0.0f, 0.0f); + } + else + { + swrDrawSprite(renderer, dx, dy, dw, dh, texture, sx, sy, sw, sh, color, alpha); + } + + drewSuccessfully = true; + } + } + + cursorX += glyph->shift; + if (drewSuccessfully && hasNext) { + cursorX += TextUtils_getKerningOffset(glyph, nextCh); + } + } + + ch = nextCh; + hasCh = hasNext; + } + + cursorY += lineStride; + // Skip past the newline, treating \r\n and \n\r as single breaks + if (textLen > lineEnd) { + lineStart = TextUtils_skipNewline(text, lineEnd, textLen); + } else { + lineStart = lineEnd; + } + } +} + +static void SWRenderer_drawText(Renderer* renderer, const char* text, float x, float y, + float xscale, float yscale, float angleDeg, float lineSeparation) +{ + SWRenderer* swr = (SWRenderer*) renderer; + swrDrawText(swr, text, x, y, xscale, yscale, angleDeg, renderer->drawColor, renderer->drawAlpha, lineSeparation); +} + +static void SWRenderer_drawTextColor(Renderer* renderer, const char* text, float x, float y, + float xscale, float yscale, float angleDeg, + int32_t c1, int32_t c2, int32_t c3, int32_t c4, MAYBE_UNUSED float alpha, + float lineSeparation) +{ + SWRenderer* swr = (SWRenderer*) renderer; + + // TODO: allow c2, c3, c4 + (void) c2; + (void) c3; + (void) c4; + + swrDrawText(swr, text, x, y, xscale, yscale, angleDeg, c1, renderer->drawAlpha, lineSeparation); +} + +static void SWRenderer_drawSpriteTiled(Renderer* renderer, int32_t tpagIndex, + float originX, float originY, float x, float y, + float xscale, float yscale, bool tileX, bool tileY, + float roomW, float roomH, uint32_t color, float alpha) +{ + SWRenderer* swr = (SWRenderer*) renderer; + DataWin* dwin = renderer->dataWin; + + if (0 > tpagIndex || dwin->tpag.count <= (uint32_t) tpagIndex) return; + + TexturePageItem* tpag = &dwin->tpag.items[tpagIndex]; + int16_t pageId = tpag->texturePageId; + if (0 > pageId || swr->totalTextureCount <= (uint32_t) pageId) return; + if (!swrEnsureTextureIsLoaded(swr, (uint32_t) pageId)) return; + + float axScale = fabsf(xscale); + float ayScale = fabsf(yscale); + float tileW = (float) tpag->boundingWidth * axScale; + float tileH = (float) tpag->boundingHeight * ayScale; + if (0 >= tileW || 0 >= tileH) return; + + float startX, endX, startY, endY; + if (tileX) { + startX = fmodf(x - originX * axScale, tileW); + if (startX > 0) startX -= tileW; + endX = roomW; + } else { + startX = x - originX * axScale; + endX = startX + tileW; + } + if (tileY) { + startY = fmodf(y - originY * ayScale, tileH); + if (startY > 0) startY -= tileH; + endY = roomH; + } else { + startY = y - originY * ayScale; + endY = startY + tileH; + } + + int sx = tpag->sourceX; + int sy = tpag->sourceY; + int sw = tpag->sourceWidth; + int sh = tpag->sourceHeight; + + int localX0 = tpag->targetX - originX; + int localY0 = tpag->targetY - originY; + int localX1 = localX0 + tpag->sourceWidth; + int localY1 = localY0 + tpag->sourceHeight; + int sx0 = xscale * localX0; + int sy0 = yscale * localY0; + int sx1 = xscale * localX1; + int sy1 = yscale * localY1; + + for (int dy = startY; endY > dy; dy += tileH) { + int cy = dy + (int)(originY * ayScale); + int vy0 = cy + sy0; + int vy1 = cy + sy1; + int dh = vy1 - vy0; + + for (int dx = startX; endX > dx; dx += tileW) { + int cx = dx + (int)(originX * axScale); + int vx0 = cx + sx0; + int vx1 = cx + sx1; + int dw = vx1 - vx0; + + swrDrawSprite(renderer, vx0, vy0, dw, dh, swr->textures[pageId], sx, sy, sw, sh, color, alpha); + } + } +} + +static void SWRenderer_drawSurfaceTiled(Renderer* renderer, int32_t surfaceID, float x, float y, float xscale, float yscale, float roomW, float roomH, uint32_t color, float alpha) +{ + (void) renderer; + (void) surfaceID; + (void) x; (void) y; + (void) xscale; (void) yscale; + (void) roomW; (void) roomH; + (void) color; (void) alpha; + + UNIMP2(); +} + +static void SWRenderer_flush(Renderer* renderer) +{ + (void)renderer; + UNIMP(); +} + +static void SWRenderer_clearScreen(Renderer* renderer, uint32_t color, float alpha) +{ + (void)renderer; (void)color; (void)alpha; + UNIMP(); +} + +static void SWRenderer_gpuSetBlendMode(Renderer* renderer, int32_t mode) +{ + UNIMP(); + (void)renderer; (void)mode; +} + +static void SWRenderer_gpuSetBlendModeExt(Renderer* renderer, int32_t sfactor, int32_t dfactor, int32_t sfactor_alpha, int32_t dfactor_alpha) +{ + UNIMP(); + (void)renderer; (void)sfactor; (void)dfactor; (void)sfactor_alpha; (void)dfactor_alpha; +} + +static void SWRenderer_gpuSetBlendEnable(Renderer* renderer, bool enable) +{ + UNIMP(); + (void)renderer; (void)enable; +} + +static void SWRenderer_gpuSetAlphaTestEnable(Renderer* renderer, bool enable) +{ + UNIMP(); + (void)renderer; (void)enable; +} + +static void SWRenderer_gpuSetAlphaTestRef(Renderer* renderer, uint8_t ref) +{ + UNIMP(); + (void)renderer; (void)ref; +} + +static void SWRenderer_gpuSetColorWriteEnable(Renderer* renderer, bool red, bool green, bool blue, bool alpha) +{ + UNIMP(); + (void)renderer; (void)red; (void)green; (void)blue; (void)alpha; +} + +static void SWRenderer_gpuGetColorWriteEnable(Renderer* renderer, bool* red, bool* green, bool* blue, bool* alpha) +{ + UNIMP(); + *red = false; + *green = false; + *blue = false; + *alpha = false; + (void)renderer; (void)red; (void)green; (void)blue; (void)alpha; +} + +static bool SWRenderer_gpuGetBlendEnable(Renderer* renderer) +{ + UNIMP(); + (void)renderer; + return false; +} + +static void SWRenderer_gpuSetFog(Renderer* renderer, bool enable, uint32_t color) +{ + UNIMP(); + (void)renderer; (void)enable; (void)color; +} + +static int32_t SWRenderer_createSurface(Renderer* renderer, int32_t width, int32_t height) +{ + UNIMP(); + (void)renderer; (void)width; (void)height; + return 0; +} + +static bool SWRenderer_surfaceExists(Renderer* renderer, int32_t surfaceID) +{ + UNIMP(); + (void)renderer; (void)surfaceID; + return false; +} + +static bool SWRenderer_setRenderTarget(Renderer* renderer, int32_t surfaceID, bool implicitApplicationSurface) +{ + UNIMP(); + (void)renderer; (void)surfaceID; + (void)implicitApplicationSurface; + return false; +} + +static float SWRenderer_getSurfaceWidth(Renderer* renderer, int32_t surfaceID) +{ + SWRenderer* swr = (SWRenderer*) renderer; + if (surfaceID == APPLICATION_SURFACE_ID) { + return (float) swr->width; + } + + UNIMP(); + (void)renderer; (void)surfaceID; + return 0.0f; +} + +static float SWRenderer_getSurfaceHeight(Renderer* renderer, int32_t surfaceID) +{ + SWRenderer* swr = (SWRenderer*) renderer; + if (surfaceID == APPLICATION_SURFACE_ID) { + return (float) swr->height; + } + + UNIMP(); + (void)renderer; (void)surfaceID; + return 0.0f; +} + +static void SWRenderer_drawSurface(Renderer* renderer, int32_t surfaceID, + int32_t srcLeft, int32_t srcTop, int32_t srcWidth, int32_t srcHeight, + float x, float y, float xscale, float yscale, float angleDeg, + uint32_t color, float alpha) +{ + UNIMP(); + (void)renderer; (void)surfaceID; + (void)srcLeft; (void)srcTop; (void)srcWidth; (void)srcHeight; + (void)x; (void)y; (void)xscale; (void)yscale; (void)angleDeg; + (void)color; (void)alpha; +} + +static void SWRenderer_surfaceResize(Renderer* renderer, int32_t surfaceID, int32_t width, int32_t height) +{ + UNIMP(); + (void)renderer; (void)surfaceID; (void)width; (void)height; +} + +static void SWRenderer_surfaceFree(Renderer* renderer, int32_t surfaceID) +{ + UNIMP(); + (void)renderer; (void)surfaceID; +} + +static void SWRenderer_surfaceCopy(Renderer* renderer, + int32_t DestSurfaceID, int32_t DestX, int32_t DestY, + int32_t SrcSurfaceID, int32_t SrcX, int32_t SrcY, + int32_t SrcW, int32_t SrcH, bool part) +{ + UNIMP(); + (void)renderer; + (void)DestSurfaceID; (void)DestX; (void)DestY; + (void)SrcSurfaceID; (void)SrcX; (void)SrcY; + (void)SrcW; (void)SrcH; (void)part; +} + +static bool SWRenderer_surfaceGetPixels(Renderer* renderer, int32_t surfaceID, uint8_t* outRGBA) +{ + UNIMP(); + (void)renderer; (void)surfaceID; (void)outRGBA; + return false; +} + +static int32_t SWRenderer_createSpriteFromSurface(Renderer* renderer, int32_t surfaceID, + int32_t x, int32_t y, int32_t w, int32_t h, + bool removeback, bool smooth, + int32_t xorig, int32_t yorig) +{ + SWRenderer* swr = (SWRenderer*) renderer; + + swrTransformPosIntIfNeeded(swr, &x, &y); + swrTransformSizeIntIfNeeded(swr, &w, &h); + swrTransformSizeIntIfNeeded(swr, &xorig, &yorig); + + (void) removeback; + (void) smooth; + + if (surfaceID != -1) { + fprintf(stderr, "%s: Surfaces other than application_surface aren't supported yet!\n", __func__); + return 0; + } + + int32_t texturePageId = swrFindSurfaceTextureSlot(swr); + int32_t tpagIndex = swrFindSurfaceTPagSlot(swr); + if (texturePageId == -1 || tpagIndex == -1) { + fprintf(stderr, "%s: Surface overflow!!\n", __func__); + return 0; + } + + SWTexture* tex = swrCreateTexture(NULL, w, h); + + // grab the pixels. + for (int iy = 0; iy < h; iy++) + { + uintpixel_t* dstline = &tex->buffer[iy * tex->width]; + if ((iy + y) < 0 || (iy + y) >= swr->height) + { + for (int ix = 0; ix < w; ix++) + dstline[ix] = 0; + + continue; + } + + uintpixel_t* srcline = &swr->fb[(iy + y) * swr->width + x]; + + int ix = 0, sx = x; + // left edge + for (; sx < 0 && ix < w; sx++, ix++) + dstline[ix] = 0; + + // in-bounds + for (; sx < swr->width && ix < w; sx++, ix++) +#if PIXEL_SIZE == 8 + dstline[ix] = srcline[ix]; +#else + dstline[ix] = srcline[ix] | TRANSPARENT_MASK; +#endif + + // right edge + for (; ix < w; ix++) + dstline[ix] = 0; + } + + int32_t spriteW = w; + int32_t spriteH = h; + + int32_t targetW = spriteW; + int32_t targetH = spriteH; + swrReverseTransformSizeIntIfNeeded(swr, &targetW, &targetH); + + swr->textures[texturePageId] = tex; + + // TODO[MrPowerGamerBR]: This is supposed to be refactored, not to modify data.win structs directly. + DataWin* dw = swr->base.dataWin; + TexturePageItem* tpag = &dw->tpag.items[tpagIndex]; + tpag->sourceX = 0; + tpag->sourceY = 0; + tpag->sourceWidth = (uint16_t) spriteW; + tpag->sourceHeight = (uint16_t) spriteH; + tpag->targetX = 0; + tpag->targetY = 0; + tpag->targetWidth = (uint16_t) (spriteW * swr->viewW / swr->portW); + tpag->targetHeight = (uint16_t) (spriteH * swr->viewH / swr->portH); + tpag->boundingWidth = (uint16_t) spriteW; + tpag->boundingHeight = (uint16_t) spriteH; + tpag->texturePageId = texturePageId; + + uint32_t spriteIndex = DataWin_allocSpriteSlot(dw, swr->originalSpriteCount); + Sprite* sprite = &dw->sprt.sprites[spriteIndex]; + // name was set by DataWin_allocSpriteSlot ("__newsprite"); don't overwrite it here + sprite->width = (uint32_t) w; + sprite->height = (uint32_t) h; + sprite->originX = xorig; + sprite->originY = yorig; + sprite->textureCount = 1; + sprite->tpagIndices = (int32_t*) safeMalloc(sizeof(int32_t)); + sprite->tpagIndices[0] = (int32_t) tpagIndex; + sprite->maskCount = 0; + sprite->masks = nullptr; + + fprintf(stderr, "%s: Allocated surface sprite with ID %d\n", __func__, spriteIndex); + return spriteIndex; +} + +static void SWRenderer_deleteSprite(Renderer* renderer, int32_t spriteIndex) +{ + SWRenderer* swr = (SWRenderer*) renderer; + + DataWin* dw = renderer->dataWin; + if (0 > spriteIndex || dw->sprt.count <= (uint32_t) spriteIndex) return; + + // Refuse to delete original data.win sprites + if (swr->originalSpriteCount > (uint32_t) spriteIndex) { + fprintf(stderr, "%s: Sprite index %d is invalid.\n", __func__, spriteIndex); + return; + } + + Sprite* sprite = &dw->sprt.sprites[spriteIndex]; + if (sprite->textureCount == 0) return; // already deleted + + for (uint32_t i = 0; i < sprite->textureCount; i++) + { + int32_t tpagIdx = sprite->tpagIndices[i]; + if (tpagIdx >= 0 && (uint32_t) tpagIdx >= swr->originalTPagCount) { + TexturePageItem* tpag = &dw->tpag.items[tpagIdx]; + int16_t pageId = tpag->texturePageId; + if (pageId >= 0 && swr->totalTextureCount > (uint32_t) pageId) { + swrFreeTexture(swr->textures[pageId]); + swr->textures[pageId] = NULL; + } + // Mark TPAG slot as free for reuse + tpag->texturePageId = -1; + } + } + + free(sprite->tpagIndices); + sprite->tpagIndices = NULL; + + const char* keepName = sprite->name; + memset(sprite, 0, sizeof(Sprite)); + sprite->name = keepName; + + fprintf(stderr, "SWR: Deleted sprite %d\n", spriteIndex); +} + +static void SWRenderer_drawTiledPart(Renderer* renderer, int32_t tpagIndex, + int32_t srcX, int32_t srcY, int32_t srcW, int32_t srcH, + float dstX, float dstY, float dstW, float dstH, + uint32_t color, float alpha) +{ + UNIMP(); + (void)renderer; (void)tpagIndex; + (void)srcX; (void)srcY; (void)srcW; (void)srcH; + (void)dstX; (void)dstY; (void)dstW; (void)dstH; + (void)color; (void)alpha; +} + +static int32_t SWRenderer_ensureApplicationSurface(Renderer* renderer, int32_t width, int32_t height) +{ + UNIMP(); + (void) renderer; + (void) width; + (void) height; + return -1; +} + +static RendererVtable swrVtable; + +void SWRenderer_clearFrameBuffer(Renderer* renderer, uint32_t color) +{ + SWRenderer* swr = (SWRenderer*) renderer; + + uintpixel_t pxcolor = swrConvertPixel(color); + + size_t fbSize = swr->fbPitch; + fbSize *= swr->height; + for (size_t i = 0; i < fbSize; i++) + { + swr->fb[i] = pxcolor; + } +} + +static uint32_t SWRenderer_spriteGetTexture(Renderer* renderer, int32_t tpagIndex) +{ + (void) renderer; + + return (uint32_t) tpagIndex + 1; +} + +static uint32_t SWRenderer_surfaceGetTexture(Renderer* renderer, int32_t surfaceID) +{ + (void) renderer; + (void) surfaceID; + + return (uint32_t) -1; +} + +static float SWRenderer_textureGetTexelWidth(Renderer* renderer, uint32_t texID) +{ + (void) renderer; + (void) texID; + + return 1.0f; +} + +static float SWRenderer_textureGetTexelHeight(Renderer* renderer, uint32_t texID) +{ + (void) renderer; + (void) texID; + + return 1.0f; +} + +static bool SWRenderer_textureGetUVs(Renderer* renderer, uint32_t texID, float* outUVs) +{ + (void) renderer; + (void) texID; + (void) outUVs; + + return false; +} + +static void SWRenderer_textureSetStage(Renderer* renderer, int32_t slot, uint32_t texID) +{ + (void) renderer; + (void) slot; + (void) texID; +} + +static bool SWRenderer_shaderIsCompiled(Renderer* renderer, int32_t shader) +{ + (void) renderer; + (void) shader; + + return false; +} + +static bool SWRenderer_shadersSupported(void) +{ + return false; +} + +static void SWRenderer_gpuSetShader(Renderer* renderer, int32_t shaderIndex) +{ + (void) renderer; + (void) shaderIndex; +} + +static void SWRenderer_gpuResetShader(Renderer* renderer) +{ + (void) renderer; +} + +static int32_t SWRenderer_shaderGetUniform(Renderer* renderer, int32_t shaderIndex, char* uniform) +{ + (void) renderer; + (void) shaderIndex; + (void) uniform; + + return 0; +} + +static int32_t SWRenderer_shaderGetSamplerIndex(Renderer* renderer, int32_t shaderIndex, char* uniform) +{ + (void) renderer; + (void) shaderIndex; + (void) uniform; + + return 0; +} + +static void SWRenderer_shaderSetUniformF(Renderer* renderer, int32_t handle, int32_t count, float value1, float value2, float value3, float value4) +{ + (void) renderer; + (void) handle; + (void) count; + (void) value1; + (void) value2; + (void) value3; + (void) value4; +} + +static void SWRenderer_shaderSetUniformI(Renderer* renderer, int32_t handle, int32_t count, int32_t value1, int32_t value2, int32_t value3, int32_t value4) +{ + (void) renderer; + (void) handle; + (void) count; + (void) value1; + (void) value2; + (void) value3; + (void) value4; +} + +Renderer* SWRenderer_create(void) +{ + SWRenderer* swr = (SWRenderer*) safeCalloc(1, sizeof(SWRenderer)); + swr->base.vtable = &swrVtable; + swrVtable.init = SWRenderer_init; + swrVtable.destroy = SWRenderer_destroy; + swrVtable.beginFrame = SWRenderer_beginFrame; + swrVtable.endFrameInit = SWRenderer_endFrameInit; + swrVtable.endFrameEnd = SWRenderer_endFrameEnd; + swrVtable.beginView = SWRenderer_beginView; + swrVtable.endView = SWRenderer_endView; + swrVtable.beginGUI = SWRenderer_beginGUI; + swrVtable.setGuiProjection = SWRenderer_setGuiProjection; + swrVtable.endGUI = SWRenderer_endGUI; + swrVtable.drawSprite = SWRenderer_drawSprite; + swrVtable.drawSpritePart = SWRenderer_drawSpritePart; + swrVtable.drawSpritePos = SWRenderer_drawSpritePos; + swrVtable.drawRectangle = SWRenderer_drawRectangle; + swrVtable.drawRectangleColor = SWRenderer_drawRectangleColor; + swrVtable.drawLine = SWRenderer_drawLine; + swrVtable.drawTriangle = SWRenderer_drawTriangle; + swrVtable.drawLineColor = SWRenderer_drawLineColor; + swrVtable.drawText = SWRenderer_drawText; + swrVtable.drawTextColor = SWRenderer_drawTextColor; + swrVtable.flush = SWRenderer_flush; + swrVtable.clearScreen = SWRenderer_clearScreen; + swrVtable.createSpriteFromSurface = SWRenderer_createSpriteFromSurface; + swrVtable.deleteSprite = SWRenderer_deleteSprite; + swrVtable.gpuSetBlendMode = SWRenderer_gpuSetBlendMode; + swrVtable.gpuSetBlendModeExt = SWRenderer_gpuSetBlendModeExt; + swrVtable.gpuSetBlendEnable = SWRenderer_gpuSetBlendEnable; + swrVtable.gpuSetAlphaTestEnable = SWRenderer_gpuSetAlphaTestEnable; + swrVtable.gpuSetAlphaTestRef = SWRenderer_gpuSetAlphaTestRef; + swrVtable.gpuSetColorWriteEnable = SWRenderer_gpuSetColorWriteEnable; + swrVtable.gpuGetColorWriteEnable = SWRenderer_gpuGetColorWriteEnable; + swrVtable.gpuGetBlendEnable = SWRenderer_gpuGetBlendEnable; + swrVtable.gpuSetFog = SWRenderer_gpuSetFog; + swrVtable.drawSpriteTiled = SWRenderer_drawSpriteTiled; + swrVtable.drawSurfaceTiled = SWRenderer_drawSurfaceTiled; + swrVtable.createSurface = SWRenderer_createSurface; + swrVtable.surfaceExists = SWRenderer_surfaceExists; + swrVtable.setRenderTarget = SWRenderer_setRenderTarget; + swrVtable.ensureApplicationSurface = SWRenderer_ensureApplicationSurface; + swrVtable.getSurfaceWidth = SWRenderer_getSurfaceWidth; + swrVtable.getSurfaceHeight = SWRenderer_getSurfaceHeight; + swrVtable.drawSurface = SWRenderer_drawSurface; + swrVtable.surfaceResize = SWRenderer_surfaceResize; + swrVtable.surfaceFree = SWRenderer_surfaceFree; + swrVtable.surfaceCopy = SWRenderer_surfaceCopy; + swrVtable.surfaceGetPixels = SWRenderer_surfaceGetPixels; + swrVtable.drawTiledPart = SWRenderer_drawTiledPart; + swrVtable.spriteGetTexture = SWRenderer_spriteGetTexture; + swrVtable.surfaceGetTexture = SWRenderer_surfaceGetTexture; + swrVtable.textureGetTexelWidth = SWRenderer_textureGetTexelWidth; + swrVtable.textureGetTexelHeight = SWRenderer_textureGetTexelHeight; + swrVtable.textureGetUVs = SWRenderer_textureGetUVs; + swrVtable.textureSetStage = SWRenderer_textureSetStage; + swrVtable.gpuSetShader = SWRenderer_gpuSetShader; + swrVtable.gpuResetShader = SWRenderer_gpuResetShader; + swrVtable.shaderGetUniform = SWRenderer_shaderGetUniform; + swrVtable.shaderGetSamplerIndex = SWRenderer_shaderGetSamplerIndex; + swrVtable.shaderSetUniformF = SWRenderer_shaderSetUniformF; + swrVtable.shaderSetUniformI = SWRenderer_shaderSetUniformI; + swrVtable.shaderIsCompiled = SWRenderer_shaderIsCompiled; + swrVtable.shadersSupported = SWRenderer_shadersSupported; + + swrVtable.drawTile = NULL; + + swr->base.drawColor = 0xFFFFFF; + swr->base.drawAlpha = 1.0f; + swr->base.drawFont = -1; + swr->base.drawHalign = 0; + swr->base.drawValign = 0; + swr->base.circlePrecision = 24; + + return (Renderer*) swr; +} diff --git a/src/sw/sw_renderer.h b/src/sw/sw_renderer.h new file mode 100644 index 000000000..1b2618779 --- /dev/null +++ b/src/sw/sw_renderer.h @@ -0,0 +1,7 @@ +#pragma once + +#include "renderer.h" + +Renderer* SWRenderer_create(void); + +void SWRenderer_clearFrameBuffer(Renderer* renderer, uint32_t color); From 4853e0bcabbf32ab7cd8156a4b7d46951c17d859 Mon Sep 17 00:00:00 2001 From: Un1q32 Date: Tue, 30 Jun 2026 22:03:38 -0400 Subject: [PATCH 002/109] we doin this again --- Makefile | 15 ++- src/desktop/backends/ios.c | 236 +++++++++++++++++++++++++++++++++++++ src/desktop/main.c | 8 +- 3 files changed, 253 insertions(+), 6 deletions(-) create mode 100644 src/desktop/backends/ios.c diff --git a/Makefile b/Makefile index 15159842c..197ae68e4 100644 --- a/Makefile +++ b/Makefile @@ -39,8 +39,14 @@ INCLUDES += $(INCLUDE). \ HEADERS := $(wildcard src/*.h) $(shell find vendor -name '*.h') SRCS := $(wildcard src/*.c) $(wildcard src/image/*.c) $(wildcard vendor/bzip2/*.c) vendor/md5/md5.c vendor/sha1/sha1.c vendor/base64/base64.c +ifeq ($(OS),iOS) +DESKTOP_BACKEND := ios +AUDIO_BACKEND := openal +DISABLE_LEGACY_GL := 1 +else DESKTOP_BACKEND := glfw3 AUDIO_BACKEND := miniaudio +endif ifdef BUTTERSCOTCH_COMMIT_DATE DEFINES += $(DEFINE)BUTTERSCOTCH_COMMIT_DATE=\"$(BUTTERSCOTCH_COMMIT_DATE)\" @@ -98,7 +104,10 @@ SDL3_LIBS += $(shell pkg-config $(PKG_CONFIG_FLAGS) --libs sdl3) LIBS += $(SDL3_LIBS) DEFINES += $(DEFINE)USE_SDL3 endif - +ifeq ($(DESKTOP_BACKEND),ios) +LIBS += -framework Foundation -framework UIKit -framework OpenGLES -framework QuartzCore +DEFINES += -DUSE_IOS +endif # GNU make doesn't have a way to do OR in conditionals, stupid language for clowns ifndef DISABLE_LEGACY_GL @@ -156,7 +165,7 @@ INCLUDES += $(INCLUDE)src/audio/openal DEFINES += $(DEFINE)USE_OPENAL SRCS += $(wildcard src/audio/openal/*.c) HEADERS += $(wildcard src/audio/openal/*.h) -ifeq ($(OS),Darwin) +ifneq ($(filter Darwin iOS,$(OS)),) # OS is 'Darwin' or 'iOS' LIBS += -framework OpenAL else LIBS += -lopenal @@ -178,7 +187,7 @@ DEFINES += $(DEFINE)_CRT_SECURE_NO_WARNINGS endif DEFINES += $(DEFINE)WIN32_LEAN_AND_MEAN else -ifeq ($(OS),Darwin) +ifneq ($(filter Darwin iOS,$(OS)),) # OS is 'Darwin' or 'iOS' LIBS += -lobjc else LIBS += -lm diff --git a/src/desktop/backends/ios.c b/src/desktop/backends/ios.c new file mode 100644 index 000000000..d680bd6cb --- /dev/null +++ b/src/desktop/backends/ios.c @@ -0,0 +1,236 @@ +#include +#include +#include + +#include +#include +#include +#include + +#include "common.h" +#include "input_recording.h" +#include "desktop/platformdefs.h" +#include "gettime.h" +#include "runner_mouse.h" + +/* + * TODO: look into implementing platformSetCursor and platformGetWindowFocus + * They might actually be meaningful on newer iPadOS versions. + */ + +static Runner *g_runner; + +static EAGLContext *glcontext; +static GLuint framebuffer; +static GLuint renderbuffer; +static GLint fbWidth = 0; +static GLint fbHeight = 0; +static CAEAGLLayer *layer; + +void platformSetWindowTitle(const char* title) { + (void)title; +} + +bool platformGetWindowSize(int32_t* outW, int32_t* outH) { + if (!outW || !outH) return false; + if (fbWidth <= 0 || fbHeight <= 0) return false; + *outW = fbWidth; + *outH = fbHeight; + return true; +} + +bool platformGetScaledWindowSize(int32_t* outW, int32_t* outH) { + if (!outW || !outH) return false; + CGRect bounds = [[UIScreen mainScreen] bounds]; + if (bounds.size.width <= 0 || bounds.size.height <= 0) return false; + *outW = bounds.size.width; + *outH = bounds.size.height; + return true; +} + +/* + * TODO: allow this to resize the render resolution when the target value is + * lower than the screen resolution, so we can gain some performance on early + * retina devices by not being forced to always render at full resolution. + */ +void platformSetWindowSize(int32_t width, int32_t height) { + (void)width; (void)height; +} + +/* TODO: touchscreen mouse support */ +void platformGetMousePos(double *xPos, double *yPos) { + *xPos = 0.0; + *yPos = 0.0; +} + +bool platformInit(int32_t reqW, int32_t reqH, const char *title, bool headless) { + (void)reqW; (void)reqH; + (void)title; (void)headless; + +#ifdef ENABLE_MODERN_GL + if (gfx == MODERN_GL) + glcontext = [[EAGLContext alloc] initWithAPI:3]; +#endif + + if (!glcontext) { + fprintf(stderr, "Failed to create an OpenGLES context\n"); + return false; + } + + if (![EAGLContext setCurrentContext:glcontext]) { + fprintf(stderr, "Failed to set the OpenGLES context\n"); + return false; + } + + return true; +} + +void platformExit(void) { + [EAGLContext setCurrentContext:glcontext]; + if (framebuffer) glDeleteFramebuffersOES(1, &framebuffer); + if (renderbuffer) glDeleteRenderbuffersOES(1, &renderbuffer); + [glcontext release]; +} + +void platformInitFunctions(Runner *runner) { + g_runner = runner; + + /* this can't be in platformInit because glad hasn't initialized yet */ + glGenFramebuffersOES(1, &framebuffer); + glGenRenderbuffersOES(1, &renderbuffer); + + glBindFramebufferOES(GL_FRAMEBUFFER_OES, framebuffer); + glBindRenderbufferOES(GL_RENDERBUFFER_OES, renderbuffer); + + [glcontext renderbufferStorage:GL_RENDERBUFFER_OES fromDrawable:layer]; + + glGetRenderbufferParameterivOES(GL_RENDERBUFFER_OES, + GL_RENDERBUFFER_WIDTH_OES, + &fbWidth); + + glGetRenderbufferParameterivOES(GL_RENDERBUFFER_OES, + GL_RENDERBUFFER_HEIGHT_OES, + &fbHeight); + + glFramebufferRenderbufferOES(GL_FRAMEBUFFER_OES, + GL_COLOR_ATTACHMENT0_OES, + GL_RENDERBUFFER_OES, + renderbuffer); +} + +void platformSwapBuffers(void) { + //static float color = 1.0f; + //color -= 0.01f; + //if (color < 0.0f) + // color = 1.0f; + //glClearColor(color, color, color, 1.0f); + //glClear(GL_COLOR_BUFFER_BIT); + [glcontext presentRenderbuffer:GL_RENDERBUFFER_OES]; + glBindFramebufferOES(GL_FRAMEBUFFER_OES, framebuffer); +} + +void *platformGetProcAddress(const char *name) { + return dlsym(RTLD_NEXT, name); +} + +bool platformHandleEvents(void) { + return false; +} + +void platformSleepUntil(uint64_t time) { + int64_t remaining = time - nowNanos(); + if (remaining > 2000000) { + remaining -= 1000000; + struct timespec ts; + ts.tv_sec = 0; + ts.tv_nsec = remaining; + nanosleep(&ts, NULL); + } + while (nowNanos() < time) { + // Spin-wait for the remaining sub-millisecond + YIELD(); + } +} + +@interface GLView : UIView +@end + +@implementation GLView + ++ (Class)layerClass { + return [CAEAGLLayer class]; +} + +- (id)initWithFrame:(CGRect)frame { + if ((self = [super initWithFrame:frame])) { + layer = (CAEAGLLayer *)self.layer; + layer.opaque = YES; + } + return self; +} + +- (void)dealloc { + [super dealloc]; +} + +@end + +extern int game_main(int argc, char *argv[]); + +@interface AppDelegate : NSObject { + UIWindow *window; + GLView *view; +} +@end + +@implementation AppDelegate + +- (void)game { + NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; + + static char arg0[] = "butterscotch"; + static char arg1[] = "/var/mobile/Documents/Butterscotch/undertale/data.win"; + static char arg2[] = "--renderer=software"; + char *argv[] = { arg0, arg1, arg2, NULL }; + game_main(3, argv); + + [pool release]; +} + +- (void)applicationDidFinishLaunching:(UIApplication *)application { + CGRect bounds = [[UIScreen mainScreen] bounds]; + + window = [[UIWindow alloc] initWithFrame:bounds]; + view = [[GLView alloc] initWithFrame:bounds]; + + if ([window respondsToSelector:@selector(setRootViewController:)]) { + UIViewController *vc = [[UIViewController alloc] init]; + vc.view = view; + [window performSelector:@selector(setRootViewController:) withObject:vc]; + } else { + [window addSubview:view]; + } + [window makeKeyAndVisible]; + + [NSThread detachNewThreadSelector:@selector(game) toTarget:self withObject:nil]; +} + +- (void)dealloc { + [view release]; + [window release]; + [super dealloc]; +} + +@end + +int main(int argc, char *argv[]) { + freopen("/tmp/bsout", "w", stderr); + freopen("/tmp/bsout", "w", stdout); + setbuf(stdout, NULL); + + NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; + int ret = UIApplicationMain(argc, argv, nil, @"AppDelegate"); + [pool release]; + + return ret; +} diff --git a/src/desktop/main.c b/src/desktop/main.c index b6f1e607b..b5debd5e9 100644 --- a/src/desktop/main.c +++ b/src/desktop/main.c @@ -54,13 +54,15 @@ #include "profiler.h" #include "gettime.h" -/* For SDL_main */ +/* For main function overrides */ #if defined(USE_SDL1) #include #elif defined(USE_SDL2) #include #elif defined(USE_SDL3) #include +#elif defined(USE_IOS) +#define main game_main #endif enum GraphicsAPI gfx; @@ -1328,8 +1330,8 @@ int main(int argc, char* argv[]) { return 1; } -#if defined(ENABLE_LEGACY_GL) || defined(ENABLE_MODERN_GL) || ((defined(USE_GLFW3) || defined(USE_GLFW2)) && defined(ENABLE_SW_RENDERER) ) -#if defined(USE_GLFW3) || defined(USE_GLFW2) +#if defined(ENABLE_LEGACY_GL) || defined(ENABLE_MODERN_GL) || ((defined(USE_GLFW3) || defined(USE_GLFW2) || defined(USE_IOS)) && defined(ENABLE_SW_RENDERER) ) +#if defined(USE_GLFW3) || defined(USE_GLFW2) || defined(USE_IOS) if (gfx == LEGACY_GL || gfx == MODERN_GL || gfx == SOFTWARE) { #else if (gfx == LEGACY_GL || gfx == MODERN_GL) { From cc2e8747c20ecd2b48e00e4c2e026d2841ab01a1 Mon Sep 17 00:00:00 2001 From: Un1q32 Date: Tue, 30 Jun 2026 22:23:27 -0400 Subject: [PATCH 003/109] ok --- src/desktop/backends/ios.c | 46 ++++++++++++++++++++------------------ 1 file changed, 24 insertions(+), 22 deletions(-) diff --git a/src/desktop/backends/ios.c b/src/desktop/backends/ios.c index d680bd6cb..bbbd3a90a 100644 --- a/src/desktop/backends/ios.c +++ b/src/desktop/backends/ios.c @@ -7,6 +7,8 @@ #include #include +#include + #include "common.h" #include "input_recording.h" #include "desktop/platformdefs.h" @@ -87,8 +89,8 @@ bool platformInit(int32_t reqW, int32_t reqH, const char *title, bool headless) void platformExit(void) { [EAGLContext setCurrentContext:glcontext]; - if (framebuffer) glDeleteFramebuffersOES(1, &framebuffer); - if (renderbuffer) glDeleteRenderbuffersOES(1, &renderbuffer); + if (framebuffer) glDeleteFramebuffers(1, &framebuffer); + if (renderbuffer) glDeleteRenderbuffers(1, &renderbuffer); [glcontext release]; } @@ -96,37 +98,37 @@ void platformInitFunctions(Runner *runner) { g_runner = runner; /* this can't be in platformInit because glad hasn't initialized yet */ - glGenFramebuffersOES(1, &framebuffer); - glGenRenderbuffersOES(1, &renderbuffer); + glGenFramebuffers(1, &framebuffer); + glGenRenderbuffers(1, &renderbuffer); - glBindFramebufferOES(GL_FRAMEBUFFER_OES, framebuffer); - glBindRenderbufferOES(GL_RENDERBUFFER_OES, renderbuffer); + glBindFramebuffer(GL_FRAMEBUFFER, framebuffer); + glBindRenderbuffer(GL_RENDERBUFFER, renderbuffer); - [glcontext renderbufferStorage:GL_RENDERBUFFER_OES fromDrawable:layer]; + [glcontext renderbufferStorage:GL_RENDERBUFFER fromDrawable:layer]; - glGetRenderbufferParameterivOES(GL_RENDERBUFFER_OES, - GL_RENDERBUFFER_WIDTH_OES, + glGetRenderbufferParameteriv(GL_RENDERBUFFER, + GL_RENDERBUFFER_WIDTH, &fbWidth); - glGetRenderbufferParameterivOES(GL_RENDERBUFFER_OES, - GL_RENDERBUFFER_HEIGHT_OES, + glGetRenderbufferParameteriv(GL_RENDERBUFFER, + GL_RENDERBUFFER_HEIGHT, &fbHeight); - glFramebufferRenderbufferOES(GL_FRAMEBUFFER_OES, - GL_COLOR_ATTACHMENT0_OES, - GL_RENDERBUFFER_OES, + glFramebufferRenderbuffer(GL_FRAMEBUFFER, + GL_COLOR_ATTACHMENT0, + GL_RENDERBUFFER, renderbuffer); } void platformSwapBuffers(void) { - //static float color = 1.0f; - //color -= 0.01f; - //if (color < 0.0f) - // color = 1.0f; - //glClearColor(color, color, color, 1.0f); - //glClear(GL_COLOR_BUFFER_BIT); - [glcontext presentRenderbuffer:GL_RENDERBUFFER_OES]; - glBindFramebufferOES(GL_FRAMEBUFFER_OES, framebuffer); + static float color = 1.0f; + color -= 0.01f; + if (color < 0.0f) + color = 1.0f; + glClearColor(color, color, color, 1.0f); + glClear(GL_COLOR_BUFFER_BIT); + [glcontext presentRenderbuffer:GL_RENDERBUFFER]; + glBindFramebuffer(GL_FRAMEBUFFER, framebuffer); } void *platformGetProcAddress(const char *name) { From f0c58bfaf9ac9062c6f38181a849b34635ebe415 Mon Sep 17 00:00:00 2001 From: Un1q32 Date: Tue, 30 Jun 2026 22:29:22 -0400 Subject: [PATCH 004/109] unbuffer --- src/desktop/backends/ios.c | 1 + 1 file changed, 1 insertion(+) diff --git a/src/desktop/backends/ios.c b/src/desktop/backends/ios.c index bbbd3a90a..9a3daee7e 100644 --- a/src/desktop/backends/ios.c +++ b/src/desktop/backends/ios.c @@ -228,6 +228,7 @@ extern int game_main(int argc, char *argv[]); int main(int argc, char *argv[]) { freopen("/tmp/bsout", "w", stderr); freopen("/tmp/bsout", "w", stdout); + setbuf(stderr, NULL); setbuf(stdout, NULL); NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; From f0728a97a568c337b7d2cdd4d9f9a59a5133c4e4 Mon Sep 17 00:00:00 2001 From: Un1q32 Date: Tue, 30 Jun 2026 22:30:28 -0400 Subject: [PATCH 005/109] dont --- src/desktop/backends/ios.c | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/desktop/backends/ios.c b/src/desktop/backends/ios.c index 9a3daee7e..ef50ab0a0 100644 --- a/src/desktop/backends/ios.c +++ b/src/desktop/backends/ios.c @@ -192,9 +192,8 @@ extern int game_main(int argc, char *argv[]); static char arg0[] = "butterscotch"; static char arg1[] = "/var/mobile/Documents/Butterscotch/undertale/data.win"; - static char arg2[] = "--renderer=software"; - char *argv[] = { arg0, arg1, arg2, NULL }; - game_main(3, argv); + char *argv[] = { arg0, arg1, NULL }; + game_main(2, argv); [pool release]; } From ace6b0da0be1d2b61ba3763122112f4fd72cf5f6 Mon Sep 17 00:00:00 2001 From: Un1q32 Date: Tue, 30 Jun 2026 22:56:02 -0400 Subject: [PATCH 006/109] stuffs --- src/desktop/backends/ios.c | 45 +++++++++++++++++++++----------------- 1 file changed, 25 insertions(+), 20 deletions(-) diff --git a/src/desktop/backends/ios.c b/src/desktop/backends/ios.c index ef50ab0a0..efce3b5af 100644 --- a/src/desktop/backends/ios.c +++ b/src/desktop/backends/ios.c @@ -25,6 +25,7 @@ static Runner *g_runner; static EAGLContext *glcontext; static GLuint framebuffer; static GLuint renderbuffer; +static GLuint depthRenderbuffer; static GLint fbWidth = 0; static GLint fbHeight = 0; static CAEAGLLayer *layer; @@ -72,6 +73,8 @@ bool platformInit(int32_t reqW, int32_t reqH, const char *title, bool headless) #ifdef ENABLE_MODERN_GL if (gfx == MODERN_GL) glcontext = [[EAGLContext alloc] initWithAPI:3]; + if (!glcontext) + glcontext = [[EAGLContext alloc] initWithAPI:2]; #endif if (!glcontext) { @@ -100,35 +103,37 @@ void platformInitFunctions(Runner *runner) { /* this can't be in platformInit because glad hasn't initialized yet */ glGenFramebuffers(1, &framebuffer); glGenRenderbuffers(1, &renderbuffer); + glGenRenderbuffers(1, &depthRenderbuffer); glBindFramebuffer(GL_FRAMEBUFFER, framebuffer); - glBindRenderbuffer(GL_RENDERBUFFER, renderbuffer); + // Color renderbuffer — sized by the drawable + glBindRenderbuffer(GL_RENDERBUFFER, renderbuffer); [glcontext renderbufferStorage:GL_RENDERBUFFER fromDrawable:layer]; + glGetRenderbufferParameteriv(GL_RENDERBUFFER, GL_RENDERBUFFER_WIDTH, &fbWidth); + glGetRenderbufferParameteriv(GL_RENDERBUFFER, GL_RENDERBUFFER_HEIGHT, &fbHeight); + glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, renderbuffer); + + // Depth/stencil renderbuffer — sized to match + glBindRenderbuffer(GL_RENDERBUFFER, depthRenderbuffer); + glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH24_STENCIL8_OES, fbWidth, fbHeight); + glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, depthRenderbuffer); + glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_STENCIL_ATTACHMENT, GL_RENDERBUFFER, depthRenderbuffer); + + // IMPORTANT: presentRenderbuffer: presents whatever is bound to + // GL_RENDERBUFFER at call time, not a framebuffer attachment. + // Leave the *color* renderbuffer bound, not the depth one. + glBindRenderbuffer(GL_RENDERBUFFER, renderbuffer); - glGetRenderbufferParameteriv(GL_RENDERBUFFER, - GL_RENDERBUFFER_WIDTH, - &fbWidth); - - glGetRenderbufferParameteriv(GL_RENDERBUFFER, - GL_RENDERBUFFER_HEIGHT, - &fbHeight); - - glFramebufferRenderbuffer(GL_FRAMEBUFFER, - GL_COLOR_ATTACHMENT0, - GL_RENDERBUFFER, - renderbuffer); + GLenum status = glCheckFramebufferStatus(GL_FRAMEBUFFER); + if (status != GL_FRAMEBUFFER_COMPLETE) { + fprintf(stderr, "iOS framebuffer incomplete: 0x%x\n", status); + } } void platformSwapBuffers(void) { - static float color = 1.0f; - color -= 0.01f; - if (color < 0.0f) - color = 1.0f; - glClearColor(color, color, color, 1.0f); - glClear(GL_COLOR_BUFFER_BIT); - [glcontext presentRenderbuffer:GL_RENDERBUFFER]; glBindFramebuffer(GL_FRAMEBUFFER, framebuffer); + [glcontext presentRenderbuffer:GL_RENDERBUFFER]; } void *platformGetProcAddress(const char *name) { From 0ce2e799f51bddff3637c995e623022439eb3777 Mon Sep 17 00:00:00 2001 From: Un1q32 Date: Tue, 30 Jun 2026 23:09:55 -0400 Subject: [PATCH 007/109] dude fuck yes --- Makefile | 1 + src/desktop/backends/ios.c | 7 ++++++- src/gl/gl_renderer.c | 1 + 3 files changed, 8 insertions(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 197ae68e4..396151b92 100644 --- a/Makefile +++ b/Makefile @@ -215,6 +215,7 @@ build/butterscotch: $(OBJS) @{ [ -z "$(NO_COLOR)" ] && [ -t 1 ]; } && printf " \033[1;34mLD\033[0m butterscotch\n" || printf " LD butterscotch\n" $(V)$(_CC) $(LDFLAGS) $(OBJS) $(LIBS) $(EXTRALIBS) $(OUTPUT_EXE)$@ @[ -f $@.exe ] && chmod +x $@.exe || true + @ldid -S build/butterscotch build/%.c.$(OBJ_EXT): %.c compat/config.mk $(if $(DISABLE_MMD),$(HEADERS)) @mkdir -p $(dir $@) diff --git a/src/desktop/backends/ios.c b/src/desktop/backends/ios.c index efce3b5af..8802c2d31 100644 --- a/src/desktop/backends/ios.c +++ b/src/desktop/backends/ios.c @@ -14,6 +14,9 @@ #include "desktop/platformdefs.h" #include "gettime.h" #include "runner_mouse.h" +#ifdef ENABLE_MODERN_GL +#include "gl_renderer.h" +#endif /* * TODO: look into implementing platformSetCursor and platformGetWindowFocus @@ -116,7 +119,7 @@ void platformInitFunctions(Runner *runner) { // Depth/stencil renderbuffer — sized to match glBindRenderbuffer(GL_RENDERBUFFER, depthRenderbuffer); - glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH24_STENCIL8_OES, fbWidth, fbHeight); + glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH24_STENCIL8, fbWidth, fbHeight); glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, depthRenderbuffer); glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_STENCIL_ATTACHMENT, GL_RENDERBUFFER, depthRenderbuffer); @@ -129,6 +132,8 @@ void platformInitFunctions(Runner *runner) { if (status != GL_FRAMEBUFFER_COMPLETE) { fprintf(stderr, "iOS framebuffer incomplete: 0x%x\n", status); } + + ((GLRenderer *)runner->renderer)->hostFramebuffer = framebuffer; } void platformSwapBuffers(void) { diff --git a/src/gl/gl_renderer.c b/src/gl/gl_renderer.c index 74088a911..fedadc2f3 100644 --- a/src/gl/gl_renderer.c +++ b/src/gl/gl_renderer.c @@ -1,6 +1,7 @@ #include "gl_renderer.h" #include "matrix_math.h" #include "text_utils.h" +#include #if defined(__EMSCRIPTEN__) || defined(__ANDROID__) #include From c69ee6f53cecb7c09eb48b41de73a476cc5e4e8a Mon Sep 17 00:00:00 2001 From: Un1q32 Date: Tue, 30 Jun 2026 23:14:00 -0400 Subject: [PATCH 008/109] remove bullshit --- src/desktop/backends/ios.c | 9 --------- 1 file changed, 9 deletions(-) diff --git a/src/desktop/backends/ios.c b/src/desktop/backends/ios.c index 8802c2d31..b80d887cb 100644 --- a/src/desktop/backends/ios.c +++ b/src/desktop/backends/ios.c @@ -28,7 +28,6 @@ static Runner *g_runner; static EAGLContext *glcontext; static GLuint framebuffer; static GLuint renderbuffer; -static GLuint depthRenderbuffer; static GLint fbWidth = 0; static GLint fbHeight = 0; static CAEAGLLayer *layer; @@ -106,7 +105,6 @@ void platformInitFunctions(Runner *runner) { /* this can't be in platformInit because glad hasn't initialized yet */ glGenFramebuffers(1, &framebuffer); glGenRenderbuffers(1, &renderbuffer); - glGenRenderbuffers(1, &depthRenderbuffer); glBindFramebuffer(GL_FRAMEBUFFER, framebuffer); @@ -117,12 +115,6 @@ void platformInitFunctions(Runner *runner) { glGetRenderbufferParameteriv(GL_RENDERBUFFER, GL_RENDERBUFFER_HEIGHT, &fbHeight); glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, renderbuffer); - // Depth/stencil renderbuffer — sized to match - glBindRenderbuffer(GL_RENDERBUFFER, depthRenderbuffer); - glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH24_STENCIL8, fbWidth, fbHeight); - glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, depthRenderbuffer); - glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_STENCIL_ATTACHMENT, GL_RENDERBUFFER, depthRenderbuffer); - // IMPORTANT: presentRenderbuffer: presents whatever is bound to // GL_RENDERBUFFER at call time, not a framebuffer attachment. // Leave the *color* renderbuffer bound, not the depth one. @@ -137,7 +129,6 @@ void platformInitFunctions(Runner *runner) { } void platformSwapBuffers(void) { - glBindFramebuffer(GL_FRAMEBUFFER, framebuffer); [glcontext presentRenderbuffer:GL_RENDERBUFFER]; } From 5b4818db929c95dd0748d49da91d2ce743f78177 Mon Sep 17 00:00:00 2001 From: Un1q32 Date: Tue, 30 Jun 2026 23:15:25 -0400 Subject: [PATCH 009/109] remove more bullshit --- src/desktop/backends/ios.c | 8 -------- 1 file changed, 8 deletions(-) diff --git a/src/desktop/backends/ios.c b/src/desktop/backends/ios.c index b80d887cb..00fe86452 100644 --- a/src/desktop/backends/ios.c +++ b/src/desktop/backends/ios.c @@ -115,16 +115,8 @@ void platformInitFunctions(Runner *runner) { glGetRenderbufferParameteriv(GL_RENDERBUFFER, GL_RENDERBUFFER_HEIGHT, &fbHeight); glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, renderbuffer); - // IMPORTANT: presentRenderbuffer: presents whatever is bound to - // GL_RENDERBUFFER at call time, not a framebuffer attachment. - // Leave the *color* renderbuffer bound, not the depth one. glBindRenderbuffer(GL_RENDERBUFFER, renderbuffer); - GLenum status = glCheckFramebufferStatus(GL_FRAMEBUFFER); - if (status != GL_FRAMEBUFFER_COMPLETE) { - fprintf(stderr, "iOS framebuffer incomplete: 0x%x\n", status); - } - ((GLRenderer *)runner->renderer)->hostFramebuffer = framebuffer; } From 873d7181d7b92a32b3eba01c028007721dee932f Mon Sep 17 00:00:00 2001 From: Un1q32 Date: Tue, 30 Jun 2026 23:31:50 -0400 Subject: [PATCH 010/109] resizing --- src/desktop/backends/ios.c | 35 ++++++++++++++++++++++++++--------- 1 file changed, 26 insertions(+), 9 deletions(-) diff --git a/src/desktop/backends/ios.c b/src/desktop/backends/ios.c index 00fe86452..7260e87b9 100644 --- a/src/desktop/backends/ios.c +++ b/src/desktop/backends/ios.c @@ -1,3 +1,4 @@ +#include #include #include #include @@ -23,6 +24,8 @@ * They might actually be meaningful on newer iPadOS versions. */ +static atomic_bool needsResize = false; + static Runner *g_runner; static EAGLContext *glcontext; @@ -99,16 +102,9 @@ void platformExit(void) { [glcontext release]; } -void platformInitFunctions(Runner *runner) { - g_runner = runner; - - /* this can't be in platformInit because glad hasn't initialized yet */ - glGenFramebuffers(1, &framebuffer); - glGenRenderbuffers(1, &renderbuffer); - +static void resizeFramebuffer(void) { glBindFramebuffer(GL_FRAMEBUFFER, framebuffer); - // Color renderbuffer — sized by the drawable glBindRenderbuffer(GL_RENDERBUFFER, renderbuffer); [glcontext renderbufferStorage:GL_RENDERBUFFER fromDrawable:layer]; glGetRenderbufferParameteriv(GL_RENDERBUFFER, GL_RENDERBUFFER_WIDTH, &fbWidth); @@ -117,7 +113,21 @@ void platformInitFunctions(Runner *runner) { glBindRenderbuffer(GL_RENDERBUFFER, renderbuffer); - ((GLRenderer *)runner->renderer)->hostFramebuffer = framebuffer; + if (g_runner && g_runner->renderer) + ((GLRenderer *)g_runner->renderer)->hostFramebuffer = framebuffer; + + glViewport(0, 0, fbWidth, fbHeight); +} + +void platformInitFunctions(Runner *runner) { + g_runner = runner; + + /* this can't be in platformInit because glad hasn't initialized yet */ + glGenFramebuffers(1, &framebuffer); + glGenRenderbuffers(1, &renderbuffer); + + resizeFramebuffer(); + atomic_store(&needsResize, false); } void platformSwapBuffers(void) { @@ -129,6 +139,8 @@ void *platformGetProcAddress(const char *name) { } bool platformHandleEvents(void) { + if (atomic_exchange(&needsResize, false)) + resizeFramebuffer(); return false; } @@ -164,6 +176,11 @@ void platformSleepUntil(uint64_t time) { return self; } +- (void)layoutSubviews { + [super layoutSubviews]; + atomic_store(&needsResize, true); +} + - (void)dealloc { [super dealloc]; } From 8581b7adf40bec398e8fbebe3aa8d7f40db82d21 Mon Sep 17 00:00:00 2001 From: Un1q32 Date: Tue, 30 Jun 2026 23:39:49 -0400 Subject: [PATCH 011/109] fix scale --- src/desktop/backends/ios.c | 1 + 1 file changed, 1 insertion(+) diff --git a/src/desktop/backends/ios.c b/src/desktop/backends/ios.c index 7260e87b9..b64510916 100644 --- a/src/desktop/backends/ios.c +++ b/src/desktop/backends/ios.c @@ -172,6 +172,7 @@ void platformSleepUntil(uint64_t time) { if ((self = [super initWithFrame:frame])) { layer = (CAEAGLLayer *)self.layer; layer.opaque = YES; + layer.contentsScale = [UIScreen mainScreen].scale; } return self; } From fdde3c07081dc650829b9e795d32946ad81aa4da Mon Sep 17 00:00:00 2001 From: Un1q32 Date: Tue, 30 Jun 2026 23:45:14 -0400 Subject: [PATCH 012/109] scale --- src/desktop/backends/ios.c | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/desktop/backends/ios.c b/src/desktop/backends/ios.c index b64510916..1af0fa84e 100644 --- a/src/desktop/backends/ios.c +++ b/src/desktop/backends/ios.c @@ -172,7 +172,17 @@ void platformSleepUntil(uint64_t time) { if ((self = [super initWithFrame:frame])) { layer = (CAEAGLLayer *)self.layer; layer.opaque = YES; - layer.contentsScale = [UIScreen mainScreen].scale; + + UIScreen *screen = [UIScreen mainScreen]; + CGFloat scale = 1.0f; /* pre-iOS 4: no retina, 1x is correct */ + if ([screen respondsToSelector:@selector(scale)]) { + CGFloat (*getScale)(id, SEL) = (CGFloat (*)(id, SEL))objc_msgSend; + scale = getScale(screen, @selector(scale)); + } + if ([layer respondsToSelector:@selector(setContentsScale:)]) { + void (*setScale)(id, SEL, CGFloat) = (void (*)(id, SEL, CGFloat))objc_msgSend; + setScale(layer, @selector(setContentsScale:), scale); + } } return self; } From da436081bd6e526bf9a71c4b3f8d709ac5efc817 Mon Sep 17 00:00:00 2001 From: Un1q32 Date: Wed, 1 Jul 2026 00:00:47 -0400 Subject: [PATCH 013/109] resizing --- src/desktop/backends/ios.c | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) mode change 100644 => 100755 src/desktop/backends/ios.c diff --git a/src/desktop/backends/ios.c b/src/desktop/backends/ios.c old mode 100644 new mode 100755 index 1af0fa84e..3a8f7248d --- a/src/desktop/backends/ios.c +++ b/src/desktop/backends/ios.c @@ -172,6 +172,7 @@ void platformSleepUntil(uint64_t time) { if ((self = [super initWithFrame:frame])) { layer = (CAEAGLLayer *)self.layer; layer.opaque = YES; + self.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; UIScreen *screen = [UIScreen mainScreen]; CGFloat scale = 1.0f; /* pre-iOS 4: no retina, 1x is correct */ @@ -198,6 +199,25 @@ void platformSleepUntil(uint64_t time) { @end +/* + * Binaries linked against pre-iOS-6 SDKs get rotation handled via the legacy + * shouldAutorotateToInterfaceOrientation: API, even when running on newer + * OS versions. Plain UIViewController's default implementation only allows + * UIInterfaceOrientationPortrait, which silently blocks all rotation unless + * overridden here. + */ +@interface BSViewController : UIViewController +@end + +@implementation BSViewController + +- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation { + (void)interfaceOrientation; + return YES; +} + +@end + extern int game_main(int argc, char *argv[]); @interface AppDelegate : NSObject { @@ -226,7 +246,7 @@ extern int game_main(int argc, char *argv[]); view = [[GLView alloc] initWithFrame:bounds]; if ([window respondsToSelector:@selector(setRootViewController:)]) { - UIViewController *vc = [[UIViewController alloc] init]; + UIViewController *vc = [[BSViewController alloc] init]; vc.view = view; [window performSelector:@selector(setRootViewController:) withObject:vc]; } else { From 0361545cf08758a44aee60ece0dd62cb88836865 Mon Sep 17 00:00:00 2001 From: Un1q32 Date: Wed, 1 Jul 2026 00:46:18 -0400 Subject: [PATCH 014/109] working iOS --- Makefile | 2 +- src/desktop/backends/ios.c | 370 ++++++++++++++++++++++++++++++++++++- 2 files changed, 366 insertions(+), 6 deletions(-) diff --git a/Makefile b/Makefile index 396151b92..299c62474 100644 --- a/Makefile +++ b/Makefile @@ -105,7 +105,7 @@ LIBS += $(SDL3_LIBS) DEFINES += $(DEFINE)USE_SDL3 endif ifeq ($(DESKTOP_BACKEND),ios) -LIBS += -framework Foundation -framework UIKit -framework OpenGLES -framework QuartzCore +LIBS += -framework Foundation -framework UIKit -framework OpenGLES -framework QuartzCore -framework CoreGraphics DEFINES += -DUSE_IOS endif diff --git a/src/desktop/backends/ios.c b/src/desktop/backends/ios.c index 3a8f7248d..417396267 100755 --- a/src/desktop/backends/ios.c +++ b/src/desktop/backends/ios.c @@ -2,8 +2,10 @@ #include #include #include +#include "math_compat.h" #include +#include #include #include #include @@ -25,6 +27,7 @@ */ static atomic_bool needsResize = false; +static atomic_bool quitRequested = false; static Runner *g_runner; @@ -35,6 +38,128 @@ static GLint fbWidth = 0; static GLint fbHeight = 0; static CAEAGLLayer *layer; +/* w/h of the game's requested resolution. Used only for layout decisions; + * the renderer itself already letterboxes/pillarboxes to this ratio + * regardless of the actual framebuffer size we hand it. */ +static float g_aspectRatio = 1.0f; + +/* The window/overlay are created on the main thread before platformInit() + * (which runs on the game thread) knows the real aspect ratio, so the + * initial layout uses the g_aspectRatio default of 1.0. These let us force + * a relayout once the real value is known, instead of waiting for the + * first rotation to happen to trigger one. */ +static UIView *g_glView = nil; +static UIView *g_overlayView = nil; + +static void bsRequestRelayout(void) { + if (g_glView) { + [g_glView performSelectorOnMainThread:@selector(setNeedsLayout) withObject:nil waitUntilDone:YES]; + [g_glView performSelectorOnMainThread:@selector(layoutIfNeeded) withObject:nil waitUntilDone:YES]; + } + if (g_overlayView) { + [g_overlayView performSelectorOnMainThread:@selector(setNeedsLayout) withObject:nil waitUntilDone:YES]; + [g_overlayView performSelectorOnMainThread:@selector(layoutIfNeeded) withObject:nil waitUntilDone:YES]; + [g_overlayView performSelectorOnMainThread:@selector(setNeedsDisplay) withObject:nil waitUntilDone:YES]; + } +} + +/* --------------------------------------------------------------------- + * Touch control layout + * + * Portrait: game view anchored to the top of the screen, dpad + z/x/c + * strip along the bottom (gameboy-ish). Landscape: game view centered, + * dpad on the left, z/x/c on the right. If the requested aspect ratio + * needs more space than what's left after reserving the control strip, + * the game view is allowed to grow into the control area rather than + * get squashed -- the controls are translucent, so this is fine. + * ------------------------------------------------------------------- */ + +#define BS_CONTROL_STRIP_PORTRAIT_H 160.0f +#define BS_CONTROL_STRIP_LANDSCAPE_W 120.0f +#define BS_QUIT_BUTTON_SIZE 22.0f +#define BS_QUIT_BUTTON_MARGIN 8.0f +#define BS_GAME_TOP_PADDING (BS_QUIT_BUTTON_MARGIN + BS_QUIT_BUTTON_SIZE + 4.0f) + +typedef struct { + CGRect gameFrame; + CGRect dpadFrame; + CGRect buttonsFrame; + CGRect quitFrame; + bool portrait; +} BSLayout; + +static BSLayout computeLayout(CGSize screen) { + BSLayout layout; + layout.portrait = screen.height >= screen.width; + + if (layout.portrait) { + CGFloat neededH = screen.width / g_aspectRatio; + CGFloat gameH = fminf(neededH, screen.height - BS_GAME_TOP_PADDING); + layout.gameFrame = CGRectMake(0, BS_GAME_TOP_PADDING, screen.width, gameH); + + CGFloat stripY = screen.height - BS_CONTROL_STRIP_PORTRAIT_H; + layout.dpadFrame = CGRectMake(16, stripY + 10, 130, 130); + layout.buttonsFrame = CGRectMake(screen.width - 16 - 150, stripY + 30, 150, 50); + } else { + CGFloat neededW = screen.height * g_aspectRatio; + CGFloat gameW = fminf(neededW, screen.width); + CGFloat gameX = (screen.width - gameW) / 2.0f; + layout.gameFrame = CGRectMake(gameX, 0, gameW, screen.height); + + layout.dpadFrame = CGRectMake(10, (screen.height - 130) / 2.0f + 35, 110, 130); + layout.buttonsFrame = CGRectMake(screen.width - 10 - 170, (screen.height - 130) / 2.0f + 60, 170, 60); + } + + layout.quitFrame = CGRectMake(screen.width - BS_QUIT_BUTTON_MARGIN - BS_QUIT_BUTTON_SIZE, + BS_QUIT_BUTTON_MARGIN, BS_QUIT_BUTTON_SIZE, BS_QUIT_BUTTON_SIZE); + return layout; +} + +static void dpadArmRects(CGRect dpad, CGRect *up, CGRect *down, CGRect *left, CGRect *right) { + CGFloat cx = dpad.origin.x + dpad.size.width / 2.0f; + CGFloat cy = dpad.origin.y + dpad.size.height / 2.0f; + CGFloat armW = dpad.size.width / 3.0f; + CGFloat armH = dpad.size.height / 3.0f; + *up = CGRectMake(cx - armW / 2.0f, dpad.origin.y, armW, armH); + *down = CGRectMake(cx - armW / 2.0f, dpad.origin.y + dpad.size.height - armH, armW, armH); + *left = CGRectMake(dpad.origin.x, cy - armH / 2.0f, armW, armH); + *right = CGRectMake(dpad.origin.x + dpad.size.width - armW, cy - armH / 2.0f, armW, armH); +} + +/* 8-way split around the dpad's center so a single touch can express + * diagonals (e.g. up+left) without needing two fingers. */ +static void dpadDirectionsForPoint(CGRect dpad, CGPoint p, bool *up, bool *down, bool *left, bool *right) { + CGFloat cx = dpad.origin.x + dpad.size.width / 2.0f; + CGFloat cy = dpad.origin.y + dpad.size.height / 2.0f; + CGFloat dx = p.x - cx; + CGFloat dy = p.y - cy; + CGFloat deadzone = fminf(dpad.size.width, dpad.size.height) * 0.15f; + + *up = *down = *left = *right = false; + if (fabsf(dx) < deadzone && fabsf(dy) < deadzone) return; + + CGFloat deg = atan2f(dy, dx) * 180.0f / (CGFloat)M_PI; /* 0 = right, 90 = down (screen coords) */ + if (deg < 0) deg += 360.0f; + + if (deg >= 337.5f || deg < 22.5f) { *right = true; } + else if (deg < 67.5f) { *right = true; *down = true; } + else if (deg < 112.5f) { *down = true; } + else if (deg < 157.5f) { *down = true; *left = true; } + else if (deg < 202.5f) { *left = true; } + else if (deg < 247.5f) { *left = true; *up = true; } + else if (deg < 292.5f) { *up = true; } + else { *up = true; *right = true; } +} + +static void actionButtonRects(CGRect bf, CGRect out[3]) { + CGFloat btnSize = fminf(bf.size.height, bf.size.width / 3.0f) - 8.0f; + for (int i = 0; i < 3; i++) { + out[i] = CGRectMake(bf.origin.x + i * (bf.size.width / 3.0f) + 4.0f, + bf.origin.y + (bf.size.height - btnSize) / 2.0f, + btnSize, btnSize); + } +} + void platformSetWindowTitle(const char* title) { (void)title; } @@ -72,9 +197,11 @@ void platformGetMousePos(double *xPos, double *yPos) { } bool platformInit(int32_t reqW, int32_t reqH, const char *title, bool headless) { - (void)reqW; (void)reqH; (void)title; (void)headless; + g_aspectRatio = (reqH > 0) ? ((float)reqW / (float)reqH) : 1.0f; + bsRequestRelayout(); + #ifdef ENABLE_MODERN_GL if (gfx == MODERN_GL) glcontext = [[EAGLContext alloc] initWithAPI:3]; @@ -113,8 +240,10 @@ static void resizeFramebuffer(void) { glBindRenderbuffer(GL_RENDERBUFFER, renderbuffer); +#ifdef ENABLE_MODERN_GL if (g_runner && g_runner->renderer) ((GLRenderer *)g_runner->renderer)->hostFramebuffer = framebuffer; +#endif glViewport(0, 0, fbWidth, fbHeight); } @@ -141,6 +270,8 @@ void *platformGetProcAddress(const char *name) { bool platformHandleEvents(void) { if (atomic_exchange(&needsResize, false)) resizeFramebuffer(); + if (atomic_load(&quitRequested)) + return true; return false; } @@ -172,7 +303,9 @@ void platformSleepUntil(uint64_t time) { if ((self = [super initWithFrame:frame])) { layer = (CAEAGLLayer *)self.layer; layer.opaque = YES; - self.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; + /* Frame is fully recomputed in -layoutSubviews on every layout pass + * (rotation, first layout, etc), so we don't rely on autoresizing. */ + self.autoresizingMask = UIViewAutoresizingNone; UIScreen *screen = [UIScreen mainScreen]; CGFloat scale = 1.0f; /* pre-iOS 4: no retina, 1x is correct */ @@ -190,6 +323,10 @@ void platformSleepUntil(uint64_t time) { - (void)layoutSubviews { [super layoutSubviews]; + if (self.superview) { + BSLayout bsLayout = computeLayout(self.superview.bounds.size); + self.frame = bsLayout.gameFrame; + } atomic_store(&needsResize, true); } @@ -199,6 +336,205 @@ void platformSleepUntil(uint64_t time) { @end +/* --------------------------------------------------------------------- + * On-screen touch controls: dpad + z/x/c + quit. Sits as a sibling of + * GLView, sized to the full screen (so it can draw controls in the area + * outside the game view, and translucently over it when the game view + * grows into that area). + * ------------------------------------------------------------------- */ + +@interface BSTouchOverlay : UIView { + UITouch *dpadTouch; + int32_t dpadKeysDown[4]; /* up, down, left, right */ + UITouch *buttonTouches[3]; /* z, x, c */ + UITouch *quitTouch; +} +@end + +@implementation BSTouchOverlay + +- (id)initWithFrame:(CGRect)frame { + if ((self = [super initWithFrame:frame])) { + self.backgroundColor = [UIColor clearColor]; + self.opaque = NO; + self.multipleTouchEnabled = YES; + self.userInteractionEnabled = YES; + self.autoresizingMask = UIViewAutoresizingNone; + dpadTouch = nil; + quitTouch = nil; + for (int i = 0; i < 4; i++) dpadKeysDown[i] = 0; + for (int i = 0; i < 3; i++) buttonTouches[i] = nil; + } + return self; +} + +- (void)layoutSubviews { + [super layoutSubviews]; + if (self.superview) { + self.frame = self.superview.bounds; + } + [self setNeedsDisplay]; +} + +static void drawTranslucentCircle(CGContextRef ctx, CGRect frame, BOOL highlighted) { + CGFloat alpha = highlighted ? 0.55f : 0.30f; + CGContextSetRGBFillColor(ctx, 1.0f, 1.0f, 1.0f, alpha); + CGContextFillEllipseInRect(ctx, frame); + CGContextSetRGBStrokeColor(ctx, 1.0f, 1.0f, 1.0f, 0.6f); + CGContextStrokeEllipseInRect(ctx, frame); +} + +static void drawCenteredLabel(NSString *text, CGRect rect, UIFont *font) { + CGSize size = [text sizeWithFont:font]; + CGPoint pt = CGPointMake(rect.origin.x + (rect.size.width - size.width) / 2.0f, + rect.origin.y + (rect.size.height - size.height) / 2.0f); + [[UIColor whiteColor] set]; + [text drawAtPoint:pt withFont:font]; +} + +- (void)drawRect:(CGRect)rect { + (void)rect; + CGContextRef ctx = UIGraphicsGetCurrentContext(); + BSLayout bsLayout = computeLayout(self.bounds.size); + + CGRect up, down, left, right; + dpadArmRects(bsLayout.dpadFrame, &up, &down, &left, &right); + + CGContextSetRGBFillColor(ctx, 1.0f, 1.0f, 1.0f, dpadKeysDown[0] ? 0.55f : 0.28f); + CGContextFillRect(ctx, up); + CGContextSetRGBFillColor(ctx, 1.0f, 1.0f, 1.0f, dpadKeysDown[1] ? 0.55f : 0.28f); + CGContextFillRect(ctx, down); + CGContextSetRGBFillColor(ctx, 1.0f, 1.0f, 1.0f, dpadKeysDown[2] ? 0.55f : 0.28f); + CGContextFillRect(ctx, left); + CGContextSetRGBFillColor(ctx, 1.0f, 1.0f, 1.0f, dpadKeysDown[3] ? 0.55f : 0.28f); + CGContextFillRect(ctx, right); + + CGRect actionRects[3]; + actionButtonRects(bsLayout.buttonsFrame, actionRects); + NSString *actionLabels[3] = { @"Z", @"X", @"C" }; + UIFont *actionFont = [UIFont boldSystemFontOfSize:18.0f]; + for (int i = 0; i < 3; i++) { + drawTranslucentCircle(ctx, actionRects[i], buttonTouches[i] != nil); + drawCenteredLabel(actionLabels[i], actionRects[i], actionFont); + } + + drawTranslucentCircle(ctx, bsLayout.quitFrame, quitTouch != nil); + drawCenteredLabel(@"X", bsLayout.quitFrame, [UIFont boldSystemFontOfSize:14.0f]); +} + +- (void)updateDpadUp:(bool)up down:(bool)down left:(bool)left right:(bool)right { + bool newState[4] = { up, down, left, right }; + int32_t keys[4] = { VK_UP, VK_DOWN, VK_LEFT, VK_RIGHT }; + for (int i = 0; i < 4; i++) { + if (newState[i] && !dpadKeysDown[i]) { + if (g_runner) RunnerKeyboard_onKeyDown(g_runner->keyboard, keys[i]); + } else if (!newState[i] && dpadKeysDown[i]) { + if (g_runner) RunnerKeyboard_onKeyUp(g_runner->keyboard, keys[i]); + } + dpadKeysDown[i] = newState[i] ? 1 : 0; + } +} + +- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event { + (void)event; + BSLayout bsLayout = computeLayout(self.bounds.size); + CGRect actionRects[3]; + actionButtonRects(bsLayout.buttonsFrame, actionRects); + + for (UITouch *touch in touches) { + CGPoint p = [touch locationInView:self]; + + if (!quitTouch && CGRectContainsPoint(CGRectInset(bsLayout.quitFrame, -6, -6), p)) { + quitTouch = [touch retain]; + [self setNeedsDisplay]; + continue; + } + + if (!dpadTouch && CGRectContainsPoint(CGRectInset(bsLayout.dpadFrame, -20, -20), p)) { + dpadTouch = [touch retain]; + bool up, down, left, right; + dpadDirectionsForPoint(bsLayout.dpadFrame, p, &up, &down, &left, &right); + [self updateDpadUp:up down:down left:left right:right]; + [self setNeedsDisplay]; + continue; + } + + for (int i = 0; i < 3; i++) { + if (!buttonTouches[i] && CGRectContainsPoint(CGRectInset(actionRects[i], -6, -6), p)) { + buttonTouches[i] = [touch retain]; + int32_t vk = (i == 0) ? 'Z' : (i == 1) ? 'X' : 'C'; + if (g_runner) RunnerKeyboard_onKeyDown(g_runner->keyboard, vk); + [self setNeedsDisplay]; + break; + } + } + } +} + +- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event { + (void)event; + if (!dpadTouch) return; + BSLayout bsLayout = computeLayout(self.bounds.size); + for (UITouch *touch in touches) { + if (touch == dpadTouch) { + CGPoint p = [touch locationInView:self]; + bool up, down, left, right; + dpadDirectionsForPoint(bsLayout.dpadFrame, p, &up, &down, &left, &right); + [self updateDpadUp:up down:down left:left right:right]; + [self setNeedsDisplay]; + break; + } + } +} + +- (void)handleTouchEnd:(NSSet *)touches { + for (UITouch *touch in touches) { + if (touch == dpadTouch) { + [self updateDpadUp:false down:false left:false right:false]; + [dpadTouch release]; + dpadTouch = nil; + [self setNeedsDisplay]; + continue; + } + if (touch == quitTouch) { + [quitTouch release]; + quitTouch = nil; + atomic_store(&quitRequested, true); + [self setNeedsDisplay]; + continue; + } + for (int i = 0; i < 3; i++) { + if (touch == buttonTouches[i]) { + int32_t vk = (i == 0) ? 'Z' : (i == 1) ? 'X' : 'C'; + if (g_runner) RunnerKeyboard_onKeyUp(g_runner->keyboard, vk); + [buttonTouches[i] release]; + buttonTouches[i] = nil; + [self setNeedsDisplay]; + break; + } + } + } +} + +- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event { + (void)event; + [self handleTouchEnd:touches]; +} + +- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event { + (void)event; + [self handleTouchEnd:touches]; +} + +- (void)dealloc { + if (dpadTouch) [dpadTouch release]; + if (quitTouch) [quitTouch release]; + for (int i = 0; i < 3; i++) if (buttonTouches[i]) [buttonTouches[i] release]; + [super dealloc]; +} + +@end + /* * Binaries linked against pre-iOS-6 SDKs get rotation handled via the legacy * shouldAutorotateToInterfaceOrientation: API, even when running on newer @@ -216,6 +552,12 @@ void platformSleepUntil(uint64_t time) { return YES; } +/* Pre-iOS-7: without this, the view is offset 20pt down to make room for + * the status bar, even though we hide the status bar at launch. */ +- (BOOL)wantsFullScreenLayout { + return YES; +} + @end extern int game_main(int argc, char *argv[]); @@ -223,6 +565,8 @@ extern int game_main(int argc, char *argv[]); @interface AppDelegate : NSObject { UIWindow *window; GLView *view; + BSTouchOverlay *overlay; + UIView *rootView; } @end @@ -234,23 +578,35 @@ extern int game_main(int argc, char *argv[]); static char arg0[] = "butterscotch"; static char arg1[] = "/var/mobile/Documents/Butterscotch/undertale/data.win"; char *argv[] = { arg0, arg1, NULL }; - game_main(2, argv); + exit(game_main(2, argv)); [pool release]; } - (void)applicationDidFinishLaunching:(UIApplication *)application { + [application setStatusBarHidden:YES]; + CGRect bounds = [[UIScreen mainScreen] bounds]; + BSLayout bsLayout = computeLayout(bounds.size); window = [[UIWindow alloc] initWithFrame:bounds]; - view = [[GLView alloc] initWithFrame:bounds]; + view = [[GLView alloc] initWithFrame:bsLayout.gameFrame]; + overlay = [[BSTouchOverlay alloc] initWithFrame:bounds]; + g_glView = view; + g_overlayView = overlay; if ([window respondsToSelector:@selector(setRootViewController:)]) { + rootView = [[UIView alloc] initWithFrame:bounds]; + rootView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; + [rootView addSubview:view]; + [rootView addSubview:overlay]; /* on top, so controls are visible over the game */ + UIViewController *vc = [[BSViewController alloc] init]; - vc.view = view; + vc.view = rootView; [window performSelector:@selector(setRootViewController:) withObject:vc]; } else { [window addSubview:view]; + [window addSubview:overlay]; } [window makeKeyAndVisible]; @@ -258,6 +614,10 @@ extern int game_main(int argc, char *argv[]); } - (void)dealloc { + g_glView = nil; + g_overlayView = nil; + [overlay release]; + [rootView release]; [view release]; [window release]; [super dealloc]; From 3ee4721a9254de01c537766966a77015e72a592a Mon Sep 17 00:00:00 2001 From: Un1q32 Date: Wed, 1 Jul 2026 00:54:41 -0400 Subject: [PATCH 015/109] fix --- src/desktop/backends/ios.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/desktop/backends/ios.c b/src/desktop/backends/ios.c index 417396267..3516efa04 100755 --- a/src/desktop/backends/ios.c +++ b/src/desktop/backends/ios.c @@ -136,7 +136,7 @@ static void dpadDirectionsForPoint(CGRect dpad, CGPoint p, bool *up, bool *down, CGFloat deadzone = fminf(dpad.size.width, dpad.size.height) * 0.15f; *up = *down = *left = *right = false; - if (fabsf(dx) < deadzone && fabsf(dy) < deadzone) return; + if (fabs(dx) < deadzone && fabs(dy) < deadzone) return; CGFloat deg = atan2f(dy, dx) * 180.0f / (CGFloat)M_PI; /* 0 = right, 90 = down (screen coords) */ if (deg < 0) deg += 360.0f; From 109a5cce72066e8a40566e1f1fd06374dfddd3ce Mon Sep 17 00:00:00 2001 From: Un1q32 Date: Wed, 1 Jul 2026 00:54:56 -0400 Subject: [PATCH 016/109] lul --- Makefile | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Makefile b/Makefile index 299c62474..55d5ce83e 100644 --- a/Makefile +++ b/Makefile @@ -157,9 +157,13 @@ DEFINES += $(DEFINE)USE_MINIAUDIO SRCS += $(wildcard src/audio/miniaudio/*.c) HEADERS += $(wildcard src/audio/miniaudio/*.h) ifneq ($(OS),Windows) +ifeq ($(OS),iOS) +LIBS += -framework AVFoundation -framework AudioToolbox +else LIBS += -pthread endif endif +endif ifeq ($(AUDIO_BACKEND),openal) INCLUDES += $(INCLUDE)src/audio/openal DEFINES += $(DEFINE)USE_OPENAL From 71e40058eaa2a62275843c790410a706c13d5b64 Mon Sep 17 00:00:00 2001 From: Un1q32 Date: Wed, 1 Jul 2026 01:17:56 -0400 Subject: [PATCH 017/109] fix game_change --- src/desktop/backends/ios.c | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/desktop/backends/ios.c b/src/desktop/backends/ios.c index 3516efa04..fc0935c1e 100755 --- a/src/desktop/backends/ios.c +++ b/src/desktop/backends/ios.c @@ -34,6 +34,7 @@ static Runner *g_runner; static EAGLContext *glcontext; static GLuint framebuffer; static GLuint renderbuffer; +static bool glInited = false; static GLint fbWidth = 0; static GLint fbHeight = 0; static CAEAGLLayer *layer; @@ -227,6 +228,7 @@ void platformExit(void) { if (framebuffer) glDeleteFramebuffers(1, &framebuffer); if (renderbuffer) glDeleteRenderbuffers(1, &renderbuffer); [glcontext release]; + glInited = false; } static void resizeFramebuffer(void) { @@ -252,8 +254,11 @@ void platformInitFunctions(Runner *runner) { g_runner = runner; /* this can't be in platformInit because glad hasn't initialized yet */ - glGenFramebuffers(1, &framebuffer); - glGenRenderbuffers(1, &renderbuffer); + if (!glInited) { + glGenFramebuffers(1, &framebuffer); + glGenRenderbuffers(1, &renderbuffer); + glInited = true; + } resizeFramebuffer(); atomic_store(&needsResize, false); From ba7b863030d756fa663cd1c3d0ee07253183c311 Mon Sep 17 00:00:00 2001 From: Un1q32 Date: Wed, 1 Jul 2026 01:25:26 -0400 Subject: [PATCH 018/109] leaks --- src/desktop/backends/ios.c | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/desktop/backends/ios.c b/src/desktop/backends/ios.c index fc0935c1e..3d70a594c 100755 --- a/src/desktop/backends/ios.c +++ b/src/desktop/backends/ios.c @@ -216,6 +216,8 @@ bool platformInit(int32_t reqW, int32_t reqH, const char *title, bool headless) } if (![EAGLContext setCurrentContext:glcontext]) { + [glcontext release]; + glcontext = nil; fprintf(stderr, "Failed to set the OpenGLES context\n"); return false; } @@ -228,6 +230,8 @@ void platformExit(void) { if (framebuffer) glDeleteFramebuffers(1, &framebuffer); if (renderbuffer) glDeleteRenderbuffers(1, &renderbuffer); [glcontext release]; + framebuffer = 0; + renderbuffer = 0; glInited = false; } @@ -609,6 +613,7 @@ extern int game_main(int argc, char *argv[]); UIViewController *vc = [[BSViewController alloc] init]; vc.view = rootView; [window performSelector:@selector(setRootViewController:) withObject:vc]; + [vc release]; } else { [window addSubview:view]; [window addSubview:overlay]; From 86440c479c797c35ed635311ec7115ff325d2506 Mon Sep 17 00:00:00 2001 From: Un1q32 Date: Wed, 1 Jul 2026 14:41:28 -0400 Subject: [PATCH 019/109] fixes --- Makefile | 3 +-- compat/configure.sh | 1 + src/desktop/backends/ios.c | 0 3 files changed, 2 insertions(+), 2 deletions(-) mode change 100755 => 100644 src/desktop/backends/ios.c diff --git a/Makefile b/Makefile index 55d5ce83e..56d191414 100644 --- a/Makefile +++ b/Makefile @@ -211,7 +211,7 @@ all: build/butterscotch ifeq ($(filter clean distclean,$(MAKECMDGOALS)),) compat/config.mk: compat/configure.sh compat/tmp/cc - @CC="$(CC)" $(SHELL) compat/configure.sh + @CC="$(CC)" OS="$(OS)" $(SHELL) compat/configure.sh endif @@ -219,7 +219,6 @@ build/butterscotch: $(OBJS) @{ [ -z "$(NO_COLOR)" ] && [ -t 1 ]; } && printf " \033[1;34mLD\033[0m butterscotch\n" || printf " LD butterscotch\n" $(V)$(_CC) $(LDFLAGS) $(OBJS) $(LIBS) $(EXTRALIBS) $(OUTPUT_EXE)$@ @[ -f $@.exe ] && chmod +x $@.exe || true - @ldid -S build/butterscotch build/%.c.$(OBJ_EXT): %.c compat/config.mk $(if $(DISABLE_MMD),$(HEADERS)) @mkdir -p $(dir $@) diff --git a/compat/configure.sh b/compat/configure.sh index c5493910c..153b9f346 100644 --- a/compat/configure.sh +++ b/compat/configure.sh @@ -115,6 +115,7 @@ elif $CC tmp/test.c -o tmp/a.out >> tmp/config.log 2>&1; then config 'CFLAGS := -O2 -DNDEBUG' config 'INCLUDE := -I' config 'DEFINE := -D' + [ "$OS" = 'iOS' ] && config "_CC := \$(CC) -ObjC" else printred 'unknown' printf 'unable to find a working compiler syntax, this is probably because your compiler is broken.\n' diff --git a/src/desktop/backends/ios.c b/src/desktop/backends/ios.c old mode 100755 new mode 100644 From df083d694e4c21b367eb494a152aeb9e8f5891c0 Mon Sep 17 00:00:00 2001 From: Un1q32 Date: Wed, 1 Jul 2026 16:08:04 -0400 Subject: [PATCH 020/109] detect iOS --- Makefile | 2 +- compat/configure.sh | 16 ++++++++++++++-- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/Makefile b/Makefile index 56d191414..44fdf8de6 100644 --- a/Makefile +++ b/Makefile @@ -211,7 +211,7 @@ all: build/butterscotch ifeq ($(filter clean distclean,$(MAKECMDGOALS)),) compat/config.mk: compat/configure.sh compat/tmp/cc - @CC="$(CC)" OS="$(OS)" $(SHELL) compat/configure.sh + @CC="$(CC)" $(SHELL) compat/configure.sh endif diff --git a/compat/configure.sh b/compat/configure.sh index 153b9f346..61673f9e2 100644 --- a/compat/configure.sh +++ b/compat/configure.sh @@ -115,7 +115,6 @@ elif $CC tmp/test.c -o tmp/a.out >> tmp/config.log 2>&1; then config 'CFLAGS := -O2 -DNDEBUG' config 'INCLUDE := -I' config 'DEFINE := -D' - [ "$OS" = 'iOS' ] && config "_CC := \$(CC) -ObjC" else printred 'unknown' printf 'unable to find a working compiler syntax, this is probably because your compiler is broken.\n' @@ -136,7 +135,20 @@ else fi configlog 'checking the target OS' -if checkdefine '_WIN32' > /dev/null; then + +printf '%s' "\ +#include +#if !TARGET_OS_IPHONE +#error not iOS +#endif +int main(void){return 0;} +" > tmp/test.c + +if check 'if we are targetting iOS' > /dev/null; then + printgreen 'ios' + config 'OS := iOS' + config "_CC := \$(CC) -ObjC" +elif checkdefine '_WIN32' > /dev/null; then printgreen 'windows' config 'OS := Windows' elif checkdefine '__APPLE__' > /dev/null; then From ca93db55b0f9304fe77b27ee18e4dabd43e63190 Mon Sep 17 00:00:00 2001 From: Un1q32 Date: Wed, 1 Jul 2026 16:11:00 -0400 Subject: [PATCH 021/109] remove --- src/gl/gl_renderer.c | 1 - 1 file changed, 1 deletion(-) diff --git a/src/gl/gl_renderer.c b/src/gl/gl_renderer.c index fedadc2f3..74088a911 100644 --- a/src/gl/gl_renderer.c +++ b/src/gl/gl_renderer.c @@ -1,7 +1,6 @@ #include "gl_renderer.h" #include "matrix_math.h" #include "text_utils.h" -#include #if defined(__EMSCRIPTEN__) || defined(__ANDROID__) #include From 5ad95f3d23cd1545b538b2b3d1772e40471f4efd Mon Sep 17 00:00:00 2001 From: Un1q32 Date: Wed, 1 Jul 2026 16:21:04 -0400 Subject: [PATCH 022/109] silence deprication warns --- src/desktop/backends/ios.c | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/desktop/backends/ios.c b/src/desktop/backends/ios.c index 3d70a594c..08db7b9db 100644 --- a/src/desktop/backends/ios.c +++ b/src/desktop/backends/ios.c @@ -9,6 +9,7 @@ #include #include #include +#include #include @@ -394,11 +395,19 @@ static void drawTranslucentCircle(CGContextRef ctx, CGRect frame, BOOL highlight } static void drawCenteredLabel(NSString *text, CGRect rect, UIFont *font) { +#if __IPHONE_OS_VERSION_MIN_REQUIRED >= 70000 + CGSize size = [text sizeWithAttributes:@{NSFontAttributeName: font}]; +#else CGSize size = [text sizeWithFont:font]; +#endif CGPoint pt = CGPointMake(rect.origin.x + (rect.size.width - size.width) / 2.0f, rect.origin.y + (rect.size.height - size.height) / 2.0f); [[UIColor whiteColor] set]; +#if __IPHONE_OS_VERSION_MIN_REQUIRED >= 70000 + [text drawAtPoint:pt withAttributes:@{NSFontAttributeName: font}]; +#else [text drawAtPoint:pt withFont:font]; +#endif } - (void)drawRect:(CGRect)rect { From 04fe9b17762fdb6a2656b2a888815beeefba6e8d Mon Sep 17 00:00:00 2001 From: Un1q32 Date: Wed, 1 Jul 2026 16:51:00 -0400 Subject: [PATCH 023/109] dont need this --- src/desktop/backends/ios.c | 1 - 1 file changed, 1 deletion(-) diff --git a/src/desktop/backends/ios.c b/src/desktop/backends/ios.c index 08db7b9db..006868e16 100644 --- a/src/desktop/backends/ios.c +++ b/src/desktop/backends/ios.c @@ -227,7 +227,6 @@ bool platformInit(int32_t reqW, int32_t reqH, const char *title, bool headless) } void platformExit(void) { - [EAGLContext setCurrentContext:glcontext]; if (framebuffer) glDeleteFramebuffers(1, &framebuffer); if (renderbuffer) glDeleteRenderbuffers(1, &renderbuffer); [glcontext release]; From 56d5601769c88c6f92fe2c6358997f12c9edcb3e Mon Sep 17 00:00:00 2001 From: Un1q32 Date: Wed, 1 Jul 2026 17:06:29 -0400 Subject: [PATCH 024/109] fix rotation when built with newer SDKs --- src/desktop/backends/ios.c | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/desktop/backends/ios.c b/src/desktop/backends/ios.c index 006868e16..855047ea6 100644 --- a/src/desktop/backends/ios.c +++ b/src/desktop/backends/ios.c @@ -569,6 +569,11 @@ static void drawCenteredLabel(NSString *text, CGRect rect, UIFont *font) { return YES; } +- (void)viewDidLayoutSubviews { + [super viewDidLayoutSubviews]; + bsRequestRelayout(); +} + /* Pre-iOS-7: without this, the view is offset 20pt down to make room for * the status bar, even though we hide the status bar at launch. */ - (BOOL)wantsFullScreenLayout { From f43e1aff15059660ba807d610ccc822eb24359be Mon Sep 17 00:00:00 2001 From: Un1q32 Date: Wed, 1 Jul 2026 17:13:58 -0400 Subject: [PATCH 025/109] ok --- src/desktop/backends/ios.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/desktop/backends/ios.c b/src/desktop/backends/ios.c index 855047ea6..788de0fa1 100644 --- a/src/desktop/backends/ios.c +++ b/src/desktop/backends/ios.c @@ -569,10 +569,12 @@ static void drawCenteredLabel(NSString *text, CGRect rect, UIFont *font) { return YES; } +#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 50000 - (void)viewDidLayoutSubviews { [super viewDidLayoutSubviews]; bsRequestRelayout(); } +#endif /* Pre-iOS-7: without this, the view is offset 20pt down to make room for * the status bar, even though we hide the status bar at launch. */ From 172e2d0f8dca4b3967407ce3ee6d59c195128212 Mon Sep 17 00:00:00 2001 From: Un1q32 Date: Wed, 1 Jul 2026 20:52:06 -0400 Subject: [PATCH 026/109] fix redirecting issues --- src/desktop/backends/ios.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/desktop/backends/ios.c b/src/desktop/backends/ios.c index 788de0fa1..0f81ca547 100644 --- a/src/desktop/backends/ios.c +++ b/src/desktop/backends/ios.c @@ -651,8 +651,8 @@ extern int game_main(int argc, char *argv[]); @end int main(int argc, char *argv[]) { - freopen("/tmp/bsout", "w", stderr); freopen("/tmp/bsout", "w", stdout); + dup2(fileno(stdout), fileno(stderr)); setbuf(stderr, NULL); setbuf(stdout, NULL); From 18ea006c884228ed36405ac54120d5543b115f6e Mon Sep 17 00:00:00 2001 From: Un1q32 Date: Wed, 1 Jul 2026 20:56:04 -0400 Subject: [PATCH 027/109] game select menu --- src/desktop/backends/ios.c | 174 +++++++++++++++++++++++++++++++++---- 1 file changed, 158 insertions(+), 16 deletions(-) diff --git a/src/desktop/backends/ios.c b/src/desktop/backends/ios.c index 0f81ca547..edf1d302c 100644 --- a/src/desktop/backends/ios.c +++ b/src/desktop/backends/ios.c @@ -1,7 +1,9 @@ #include #include +#include #include #include +#include #include "math_compat.h" #include @@ -53,6 +55,12 @@ static float g_aspectRatio = 1.0f; static UIView *g_glView = nil; static UIView *g_overlayView = nil; +/* Path to the selected game's data.win, filled in by the game list + * controller before the game thread is started. */ +static char g_gamePath[PATH_MAX]; + +#define BS_GAMES_ROOT_PATH @"/var/mobile/Documents/Butterscotch" + static void bsRequestRelayout(void) { if (g_glView) { [g_glView performSelectorOnMainThread:@selector(setNeedsLayout) withObject:nil waitUntilDone:YES]; @@ -230,6 +238,7 @@ void platformExit(void) { if (framebuffer) glDeleteFramebuffers(1, &framebuffer); if (renderbuffer) glDeleteRenderbuffers(1, &renderbuffer); [glcontext release]; + glcontext = nil; framebuffer = 0; renderbuffer = 0; glInited = false; @@ -584,6 +593,93 @@ static void drawCenteredLabel(NSString *text, CGRect rect, UIFont *font) { @end +/* --------------------------------------------------------------------- + * Game selection menu. Scans BS_GAMES_ROOT_PATH for subfolders that + * contain a data.win, and lets the user pick one. Re-scans every time + * the view (re)appears so games dropped in via file transfer while the + * app is running (or after returning from a game) show up. + * ------------------------------------------------------------------- */ + +@interface BSGameListViewController : UITableViewController { + NSMutableArray *games; +} +- (void)reloadGames; +@end + +@implementation BSGameListViewController + +- (id)init { +#if __IPHONE_OS_VERSION_MIN_REQUIRED >= 30000 + if ((self = [super initWithStyle:UITableViewStylePlain])) { +#else + if ((self = [super init])) { +#endif + games = [[NSMutableArray alloc] init]; + self.title = @"Butterscotch"; + [self reloadGames]; + } + return self; +} + +- (void)reloadGames { + [games removeAllObjects]; + + NSFileManager *fm = [NSFileManager defaultManager]; + NSArray *entries = [fm contentsOfDirectoryAtPath:BS_GAMES_ROOT_PATH error:NULL]; + for (NSString *name in entries) { + NSString *dir = [BS_GAMES_ROOT_PATH stringByAppendingPathComponent:name]; + BOOL isDir = NO; + if (![fm fileExistsAtPath:dir isDirectory:&isDir] || !isDir) continue; + + NSString *dataWin = [dir stringByAppendingPathComponent:@"data.win"]; + if ([fm fileExistsAtPath:dataWin]) [games addObject:name]; + } + + [self.tableView reloadData]; +} + +- (void)viewWillAppear:(BOOL)animated { + [super viewWillAppear:animated]; + [self reloadGames]; +} + +- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { + (void)tableView; (void)section; + return [games count]; +} + +- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { + static NSString *reuseId = @"BSGameCell"; + UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:reuseId]; + if (!cell) { +#if __IPHONE_OS_VERSION_MIN_REQUIRED >= 30000 + cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:reuseId] autorelease]; +#else + cell = [[[UITableViewCell alloc] initWithFrame:CGRectZero reuseIdentifier:reuseId] autorelease]; +#endif + } +#if __IPHONE_OS_VERSION_MIN_REQUIRED >= 30000 + cell.textLabel.text = [games objectAtIndex:indexPath.row]; +#else + cell.text = [games objectAtIndex:indexPath.row]; +#endif + return cell; +} + +- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath { + [tableView deselectRowAtIndexPath:indexPath animated:YES]; + NSString *folder = [games objectAtIndex:indexPath.row]; + id delegate = [[UIApplication sharedApplication] delegate]; + [delegate performSelector:@selector(startGameWithFolder:) withObject:folder]; +} + +- (void)dealloc { + [games release]; + [super dealloc]; +} + +@end + extern int game_main(int argc, char *argv[]); @interface AppDelegate : NSObject { @@ -591,51 +687,96 @@ extern int game_main(int argc, char *argv[]); GLView *view; BSTouchOverlay *overlay; UIView *rootView; + BSGameListViewController *gameListVC; + BOOL usingRootViewController; } +- (void)startGameWithFolder:(NSString *)folderName; +- (void)returnToMenu; @end @implementation AppDelegate -- (void)game { +- (void)gameThread { NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; static char arg0[] = "butterscotch"; - static char arg1[] = "/var/mobile/Documents/Butterscotch/undertale/data.win"; - char *argv[] = { arg0, arg1, NULL }; - exit(game_main(2, argv)); + char *argv[] = { arg0, g_gamePath, NULL }; + game_main(2, argv); + + [self performSelectorOnMainThread:@selector(returnToMenu) withObject:nil waitUntilDone:NO]; [pool release]; } -- (void)applicationDidFinishLaunching:(UIApplication *)application { - [application setStatusBarHidden:YES]; +- (void)startGameWithFolder:(NSString *)folderName { + NSString *dataWin = [[BS_GAMES_ROOT_PATH stringByAppendingPathComponent:folderName] + stringByAppendingPathComponent:@"data.win"]; + strlcpy(g_gamePath, [dataWin fileSystemRepresentation], sizeof(g_gamePath)); + + atomic_store(&quitRequested, false); CGRect bounds = [[UIScreen mainScreen] bounds]; BSLayout bsLayout = computeLayout(bounds.size); - window = [[UIWindow alloc] initWithFrame:bounds]; view = [[GLView alloc] initWithFrame:bsLayout.gameFrame]; overlay = [[BSTouchOverlay alloc] initWithFrame:bounds]; g_glView = view; g_overlayView = overlay; - if ([window respondsToSelector:@selector(setRootViewController:)]) { - rootView = [[UIView alloc] initWithFrame:bounds]; - rootView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; - [rootView addSubview:view]; - [rootView addSubview:overlay]; /* on top, so controls are visible over the game */ + rootView = [[UIView alloc] initWithFrame:bounds]; + rootView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; + [rootView addSubview:view]; + [rootView addSubview:overlay]; /* on top, so controls are visible over the game */ + if (usingRootViewController) { UIViewController *vc = [[BSViewController alloc] init]; vc.view = rootView; [window performSelector:@selector(setRootViewController:) withObject:vc]; [vc release]; } else { - [window addSubview:view]; - [window addSubview:overlay]; + NSArray *subs = [[window.subviews copy] autorelease]; + for (UIView *sub in subs) [sub removeFromSuperview]; + [window addSubview:rootView]; + } + + [NSThread detachNewThreadSelector:@selector(gameThread) toTarget:self withObject:nil]; +} + +- (void)returnToMenu { + g_glView = nil; + g_overlayView = nil; + + if (usingRootViewController) { + [window performSelector:@selector(setRootViewController:) withObject:gameListVC]; + } else { + NSArray *subs = [[window.subviews copy] autorelease]; + for (UIView *sub in subs) [sub removeFromSuperview]; + [window addSubview:gameListVC.view]; } - [window makeKeyAndVisible]; - [NSThread detachNewThreadSelector:@selector(game) toTarget:self withObject:nil]; + [overlay release]; + overlay = nil; + [rootView release]; + rootView = nil; + [view release]; + view = nil; +} + +- (void)applicationDidFinishLaunching:(UIApplication *)application { + [application setStatusBarHidden:YES]; + + CGRect bounds = [[UIScreen mainScreen] bounds]; + window = [[UIWindow alloc] initWithFrame:bounds]; + usingRootViewController = [window respondsToSelector:@selector(setRootViewController:)]; + + gameListVC = [[BSGameListViewController alloc] init]; + + if (usingRootViewController) { + [window performSelector:@selector(setRootViewController:) withObject:gameListVC]; + } else { + [window addSubview:gameListVC.view]; + } + [window makeKeyAndVisible]; } - (void)dealloc { @@ -644,6 +785,7 @@ extern int game_main(int argc, char *argv[]); [overlay release]; [rootView release]; [view release]; + [gameListVC release]; [window release]; [super dealloc]; } From 26e71fe2f655f05f41e7871b6edda397495d7b38 Mon Sep 17 00:00:00 2001 From: Un1q32 Date: Sun, 5 Jul 2026 19:37:36 -0400 Subject: [PATCH 028/109] lazy textures --- src/desktop/backends/ios.c | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/desktop/backends/ios.c b/src/desktop/backends/ios.c index edf1d302c..25f09b6e7 100644 --- a/src/desktop/backends/ios.c +++ b/src/desktop/backends/ios.c @@ -700,8 +700,9 @@ extern int game_main(int argc, char *argv[]); NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; static char arg0[] = "butterscotch"; - char *argv[] = { arg0, g_gamePath, NULL }; - game_main(2, argv); + static char arg1[] = "--lazy-textures"; + char *argv[] = { arg0, arg1, g_gamePath, NULL }; + game_main(3, argv); [self performSelectorOnMainThread:@selector(returnToMenu) withObject:nil waitUntilDone:NO]; From 0599dde5eaa6fec6af0b53410b3ef12a69af5223 Mon Sep 17 00:00:00 2001 From: Un1q32 Date: Sun, 5 Jul 2026 19:57:29 -0400 Subject: [PATCH 029/109] lazy rooms --- src/desktop/backends/ios.c | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/desktop/backends/ios.c b/src/desktop/backends/ios.c index 25f09b6e7..fe89d9cc1 100644 --- a/src/desktop/backends/ios.c +++ b/src/desktop/backends/ios.c @@ -701,8 +701,9 @@ extern int game_main(int argc, char *argv[]); static char arg0[] = "butterscotch"; static char arg1[] = "--lazy-textures"; - char *argv[] = { arg0, arg1, g_gamePath, NULL }; - game_main(3, argv); + static char arg2[] = "--lazy-rooms"; + char *argv[] = { arg0, arg1, arg2, g_gamePath, NULL }; + game_main(4, argv); [self performSelectorOnMainThread:@selector(returnToMenu) withObject:nil waitUntilDone:NO]; From 16d8f970a2d971a454f95378f4bb0da5b3bc10c5 Mon Sep 17 00:00:00 2001 From: Un1q32 Date: Sun, 5 Jul 2026 20:02:05 -0400 Subject: [PATCH 030/109] fix rotation --- src/desktop/backends/ios.c | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/desktop/backends/ios.c b/src/desktop/backends/ios.c index fe89d9cc1..5f4aa8fbb 100644 --- a/src/desktop/backends/ios.c +++ b/src/desktop/backends/ios.c @@ -578,12 +578,14 @@ static void drawCenteredLabel(NSString *text, CGRect rect, UIFont *font) { return YES; } -#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 50000 -- (void)viewDidLayoutSubviews { - [super viewDidLayoutSubviews]; +- (void)willAnimateRotationToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation + duration:(NSTimeInterval)duration { + (void)toInterfaceOrientation; + [UIView beginAnimations:nil context:NULL]; + [UIView setAnimationDuration:duration]; bsRequestRelayout(); + [UIView commitAnimations]; } -#endif /* Pre-iOS-7: without this, the view is offset 20pt down to make room for * the status bar, even though we hide the status bar at launch. */ From 4f1f462caecb9f9632a0ce6ae477930000e511b1 Mon Sep 17 00:00:00 2001 From: Un1q32 Date: Sun, 5 Jul 2026 20:33:32 -0400 Subject: [PATCH 031/109] rotation FIX --- src/desktop/backends/ios.c | 91 +++++++++++++++++++++++++++++++++++--- 1 file changed, 86 insertions(+), 5 deletions(-) mode change 100644 => 100755 src/desktop/backends/ios.c diff --git a/src/desktop/backends/ios.c b/src/desktop/backends/ios.c old mode 100644 new mode 100755 index 5f4aa8fbb..b7996d5d6 --- a/src/desktop/backends/ios.c +++ b/src/desktop/backends/ios.c @@ -562,11 +562,37 @@ static void drawCenteredLabel(NSString *text, CGRect rect, UIFont *font) { @end /* - * Binaries linked against pre-iOS-6 SDKs get rotation handled via the legacy - * shouldAutorotateToInterfaceOrientation: API, even when running on newer - * OS versions. Plain UIViewController's default implementation only allows - * UIInterfaceOrientationPortrait, which silently blocks all rotation unless - * overridden here. + * Rotation support, layered across every mechanism Apple has used over the + * years, since which one(s) the running OS actually consults depends on + * BOTH the SDK a binary was linked against AND the OS version it's running + * on -- neither axis alone tells you which callback chain is live: + * + * - Info.plist UISupportedInterfaceOrientations (iOS 6+): must list all + * four orientations. This is compiled-independent (a plist key isn't + * affected by SDK version) and should already be set in the app's + * Info.plist. + * - -shouldAutorotateToInterfaceOrientation: (iOS 2.0+, legacy): consulted + * on iOS 2-5, and also apparently consulted on some later OS versions + * for binaries linked against pre-iOS-6 SDKs. Always compiled in. + * - -shouldAutorotate / -supportedInterfaceOrientations (iOS 6+): the + * modern per-view-controller replacement. Requires the 6.0 SDK to + * compile. + * - -application:supportedInterfaceOrientationsForWindow: (iOS 6+): same + * idea as above but for windows without a root view controller (the + * pre-iOS-3 codepath in this file where views are added directly to + * the UIWindow). + * - -willAnimateRotationToInterfaceOrientation:duration: (iOS 2.0+, + * deprecated iOS 8): drives the relayout animation on the legacy + * rotation path. Uses the old begin/commit animation API rather than + * the block-based +animateWithDuration:animations:, since that block + * API requires the 4.0 SDK to compile. + * - -viewWillTransitionToSize:withTransitionCoordinator: (iOS 8+): the + * modern replacement for the above. Requires the 8.0 SDK to compile. + * - UIDeviceOrientationDidChangeNotification (iOS 2.0+): does NOT depend + * on any of the view-controller rotation machinery above, so it's kept + * as a belt-and-suspenders relayout trigger in AppDelegate for cases + * where an old-SDK-linked binary running on a newer OS doesn't invoke + * any of the view-controller callbacks at all. */ @interface BSViewController : UIViewController @end @@ -578,6 +604,16 @@ static void drawCenteredLabel(NSString *text, CGRect rect, UIFont *font) { return YES; } +#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 60000 +- (BOOL)shouldAutorotate { + return YES; +} + +- (NSUInteger)supportedInterfaceOrientations { + return UIInterfaceOrientationMaskAll; +} +#endif + - (void)willAnimateRotationToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)duration { (void)toInterfaceOrientation; @@ -587,6 +623,17 @@ static void drawCenteredLabel(NSString *text, CGRect rect, UIFont *font) { [UIView commitAnimations]; } +#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 80000 +- (void)viewWillTransitionToSize:(CGSize)size + withTransitionCoordinator:(id)coordinator { + [super viewWillTransitionToSize:size withTransitionCoordinator:coordinator]; + [coordinator animateAlongsideTransition:^(id context) { + (void)context; + bsRequestRelayout(); + } completion:nil]; +} +#endif + /* Pre-iOS-7: without this, the view is offset 20pt down to make room for * the status bar, even though we hide the status bar at launch. */ - (BOOL)wantsFullScreenLayout { @@ -694,6 +741,7 @@ extern int game_main(int argc, char *argv[]); } - (void)startGameWithFolder:(NSString *)folderName; - (void)returnToMenu; +- (void)orientationChanged:(NSNotification *)note; @end @implementation AppDelegate @@ -766,9 +814,39 @@ extern int game_main(int argc, char *argv[]); view = nil; } +/* + * Belt-and-suspenders relayout trigger: fires purely off physical device + * orientation, independent of whichever (if any) view-controller rotation + * callback chain the running OS decides to invoke for this binary's linked + * SDK version. Harmless to have alongside the view-controller-driven paths + * above -- bsRequestRelayout() just forces a layout pass against the + * views' current bounds, so redundant calls are cheap no-ops. + */ +- (void)orientationChanged:(NSNotification *)note { + (void)note; + bsRequestRelayout(); +} + +#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 60000 +/* Mirrors BSViewController's supportedInterfaceOrientations for the + * pre-iOS-3 codepath where views are added directly to the UIWindow + * instead of going through a root view controller. */ +- (NSUInteger)application:(UIApplication *)application + supportedInterfaceOrientationsForWindow:(UIWindow *)appWindow { + (void)application; (void)appWindow; + return UIInterfaceOrientationMaskAll; +} +#endif + - (void)applicationDidFinishLaunching:(UIApplication *)application { [application setStatusBarHidden:YES]; + [[UIDevice currentDevice] beginGeneratingDeviceOrientationNotifications]; + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(orientationChanged:) + name:UIDeviceOrientationDidChangeNotification + object:nil]; + CGRect bounds = [[UIScreen mainScreen] bounds]; window = [[UIWindow alloc] initWithFrame:bounds]; usingRootViewController = [window respondsToSelector:@selector(setRootViewController:)]; @@ -784,6 +862,9 @@ extern int game_main(int argc, char *argv[]); } - (void)dealloc { + [[NSNotificationCenter defaultCenter] removeObserver:self name:UIDeviceOrientationDidChangeNotification object:nil]; + [[UIDevice currentDevice] endGeneratingDeviceOrientationNotifications]; + g_glView = nil; g_overlayView = nil; [overlay release]; From a4eaa1b5780ada6a5f1ef513861f4c05a4834c8f Mon Sep 17 00:00:00 2001 From: Un1q32 Date: Sun, 5 Jul 2026 20:44:34 -0400 Subject: [PATCH 032/109] remove BULLSHIT --- src/desktop/backends/ios.c | 65 -------------------------------------- 1 file changed, 65 deletions(-) diff --git a/src/desktop/backends/ios.c b/src/desktop/backends/ios.c index b7996d5d6..0db87b934 100755 --- a/src/desktop/backends/ios.c +++ b/src/desktop/backends/ios.c @@ -561,39 +561,6 @@ static void drawCenteredLabel(NSString *text, CGRect rect, UIFont *font) { @end -/* - * Rotation support, layered across every mechanism Apple has used over the - * years, since which one(s) the running OS actually consults depends on - * BOTH the SDK a binary was linked against AND the OS version it's running - * on -- neither axis alone tells you which callback chain is live: - * - * - Info.plist UISupportedInterfaceOrientations (iOS 6+): must list all - * four orientations. This is compiled-independent (a plist key isn't - * affected by SDK version) and should already be set in the app's - * Info.plist. - * - -shouldAutorotateToInterfaceOrientation: (iOS 2.0+, legacy): consulted - * on iOS 2-5, and also apparently consulted on some later OS versions - * for binaries linked against pre-iOS-6 SDKs. Always compiled in. - * - -shouldAutorotate / -supportedInterfaceOrientations (iOS 6+): the - * modern per-view-controller replacement. Requires the 6.0 SDK to - * compile. - * - -application:supportedInterfaceOrientationsForWindow: (iOS 6+): same - * idea as above but for windows without a root view controller (the - * pre-iOS-3 codepath in this file where views are added directly to - * the UIWindow). - * - -willAnimateRotationToInterfaceOrientation:duration: (iOS 2.0+, - * deprecated iOS 8): drives the relayout animation on the legacy - * rotation path. Uses the old begin/commit animation API rather than - * the block-based +animateWithDuration:animations:, since that block - * API requires the 4.0 SDK to compile. - * - -viewWillTransitionToSize:withTransitionCoordinator: (iOS 8+): the - * modern replacement for the above. Requires the 8.0 SDK to compile. - * - UIDeviceOrientationDidChangeNotification (iOS 2.0+): does NOT depend - * on any of the view-controller rotation machinery above, so it's kept - * as a belt-and-suspenders relayout trigger in AppDelegate for cases - * where an old-SDK-linked binary running on a newer OS doesn't invoke - * any of the view-controller callbacks at all. - */ @interface BSViewController : UIViewController @end @@ -604,16 +571,6 @@ static void drawCenteredLabel(NSString *text, CGRect rect, UIFont *font) { return YES; } -#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 60000 -- (BOOL)shouldAutorotate { - return YES; -} - -- (NSUInteger)supportedInterfaceOrientations { - return UIInterfaceOrientationMaskAll; -} -#endif - - (void)willAnimateRotationToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)duration { (void)toInterfaceOrientation; @@ -623,17 +580,6 @@ static void drawCenteredLabel(NSString *text, CGRect rect, UIFont *font) { [UIView commitAnimations]; } -#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 80000 -- (void)viewWillTransitionToSize:(CGSize)size - withTransitionCoordinator:(id)coordinator { - [super viewWillTransitionToSize:size withTransitionCoordinator:coordinator]; - [coordinator animateAlongsideTransition:^(id context) { - (void)context; - bsRequestRelayout(); - } completion:nil]; -} -#endif - /* Pre-iOS-7: without this, the view is offset 20pt down to make room for * the status bar, even though we hide the status bar at launch. */ - (BOOL)wantsFullScreenLayout { @@ -827,17 +773,6 @@ extern int game_main(int argc, char *argv[]); bsRequestRelayout(); } -#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 60000 -/* Mirrors BSViewController's supportedInterfaceOrientations for the - * pre-iOS-3 codepath where views are added directly to the UIWindow - * instead of going through a root view controller. */ -- (NSUInteger)application:(UIApplication *)application - supportedInterfaceOrientationsForWindow:(UIWindow *)appWindow { - (void)application; (void)appWindow; - return UIInterfaceOrientationMaskAll; -} -#endif - - (void)applicationDidFinishLaunching:(UIApplication *)application { [application setStatusBarHidden:YES]; From cc93a3fd1a92180c7c9607e4511729d32a5baf85 Mon Sep 17 00:00:00 2001 From: Un1q32 Date: Sun, 5 Jul 2026 21:05:16 -0400 Subject: [PATCH 033/109] render scale --- src/desktop/backends/ios.c | 97 ++++++++++++++++++++++++++++++++++---- 1 file changed, 87 insertions(+), 10 deletions(-) diff --git a/src/desktop/backends/ios.c b/src/desktop/backends/ios.c index 0db87b934..2071a0f80 100755 --- a/src/desktop/backends/ios.c +++ b/src/desktop/backends/ios.c @@ -42,6 +42,14 @@ static GLint fbWidth = 0; static GLint fbHeight = 0; static CAEAGLLayer *layer; +/* Requested render resolution, as set via platformSetWindowSize() (and + * seeded from platformInit()'s reqW/reqH before that's ever called). Used + * to pick a CAEAGLLayer contentsScale that renders at roughly this pixel + * resolution instead of unconditionally rendering at the device's native + * scale -- see applyRenderScale() below. */ +static int32_t g_reqRenderWidth = 0; +static int32_t g_reqRenderHeight = 0; + /* w/h of the game's requested resolution. Used only for layout decisions; * the renderer itself already letterboxes/pillarboxes to this ratio * regardless of the actual framebuffer size we hand it. */ @@ -192,12 +200,21 @@ bool platformGetScaledWindowSize(int32_t* outW, int32_t* outH) { } /* - * TODO: allow this to resize the render resolution when the target value is - * lower than the screen resolution, so we can gain some performance on early - * retina devices by not being forced to always render at full resolution. + * Stores the game's requested render resolution; applyRenderScale() (run + * from resizeFramebuffer(), triggered here via needsResize) derives the + * actual CAEAGLLayer contentsScale from this plus the game view's current + * point size -- see applyRenderScale() for the up-vs-down-clamp logic. + * We don't recompute the scale directly here because the frame's point + * size can also change independently of this call (rotation), so both + * paths just flag needsResize and let resizeFramebuffer() re-derive it + * from whatever's current on the next frame. */ void platformSetWindowSize(int32_t width, int32_t height) { - (void)width; (void)height; + if (width <= 0 || height <= 0) return; + + g_reqRenderWidth = width; + g_reqRenderHeight = height; + atomic_store(&needsResize, true); } /* TODO: touchscreen mouse support */ @@ -210,6 +227,8 @@ bool platformInit(int32_t reqW, int32_t reqH, const char *title, bool headless) (void)title; (void)headless; g_aspectRatio = (reqH > 0) ? ((float)reqW / (float)reqH) : 1.0f; + g_reqRenderWidth = reqW; + g_reqRenderHeight = reqH; bsRequestRelayout(); #ifdef ENABLE_MODERN_GL @@ -244,7 +263,66 @@ void platformExit(void) { glInited = false; } +/* [screen respondsToSelector:@selector(scale)] guard is for pre-iOS-4, + * where UIScreen has no notion of a Retina content scale and 1x is + * correct. */ +static CGFloat nativeScreenScale(void) { + UIScreen *screen = [UIScreen mainScreen]; + CGFloat scale = 1.0f; + if ([screen respondsToSelector:@selector(scale)]) { + CGFloat (*getScale)(id, SEL) = (CGFloat (*)(id, SEL))objc_msgSend; + scale = getScale(screen, @selector(scale)); + } + return scale; +} + +/* + * Picks the CAEAGLLayer's contentsScale -- and therefore the framebuffer's + * pixel resolution, since the drawable size handed to + * renderbufferStorage:fromDrawable: is just (layer bounds in points) * + * contentsScale -- based on the game's requested render resolution vs. + * the device's native scale: + * + * - requested < native: render at the requested (lower) resolution. + * Saves fill-rate, which matters a lot on early Retina devices that + * got a 4x pixel-count jump without a matching GPU jump. + * - requested > native: clamp to native. Upsampling past native just + * produces blur with no visible benefit, and burns performance that + * already-underpowered low-res devices can't spare. + * + * Recomputed from scratch every call (cheap) rather than cached, since + * either input -- the requested resolution, or the frame's current point + * size -- can change independently (platformSetWindowSize vs. rotation). + */ +static void applyRenderScale(void) { + if (!layer) return; + + CGSize sz = layer.bounds.size; + if (sz.width <= 0.0f || sz.height <= 0.0f) return; + + CGFloat nativeScale = nativeScreenScale(); + CGFloat targetScale = nativeScale; + + if (g_reqRenderWidth > 0 && g_reqRenderHeight > 0) { + CGFloat sW = (CGFloat)g_reqRenderWidth / sz.width; + CGFloat sH = (CGFloat)g_reqRenderHeight / sz.height; + /* Use whichever axis wants the smaller scale, so we never exceed + * the requested pixel count on either dimension -- then clamp to + * native so we never exceed the device's real resolution either. */ + targetScale = fminf(nativeScale, fminf(sW, sH)); + } + + if (targetScale <= 0.0f) targetScale = nativeScale; /* safety net */ + + if ([layer respondsToSelector:@selector(setContentsScale:)]) { + void (*setScale)(id, SEL, CGFloat) = (void (*)(id, SEL, CGFloat))objc_msgSend; + setScale(layer, @selector(setContentsScale:), targetScale); + } +} + static void resizeFramebuffer(void) { + applyRenderScale(); + glBindFramebuffer(GL_FRAMEBUFFER, framebuffer); glBindRenderbuffer(GL_RENDERBUFFER, renderbuffer); @@ -325,12 +403,11 @@ void platformSleepUntil(uint64_t time) { * (rotation, first layout, etc), so we don't rely on autoresizing. */ self.autoresizingMask = UIViewAutoresizingNone; - UIScreen *screen = [UIScreen mainScreen]; - CGFloat scale = 1.0f; /* pre-iOS 4: no retina, 1x is correct */ - if ([screen respondsToSelector:@selector(scale)]) { - CGFloat (*getScale)(id, SEL) = (CGFloat (*)(id, SEL))objc_msgSend; - scale = getScale(screen, @selector(scale)); - } + /* Seed with the native scale so the layer is sane for the brief + * window between view creation and the first resizeFramebuffer() + * call; applyRenderScale() will correct this to the requested + * render resolution's scale once sizing kicks in. */ + CGFloat scale = nativeScreenScale(); if ([layer respondsToSelector:@selector(setContentsScale:)]) { void (*setScale)(id, SEL, CGFloat) = (void (*)(id, SEL, CGFloat))objc_msgSend; setScale(layer, @selector(setContentsScale:), scale); From d4fabded557ebe38c53b8d26da9e23807d9cb70b Mon Sep 17 00:00:00 2001 From: Un1q32 Date: Mon, 6 Jul 2026 04:17:47 -0400 Subject: [PATCH 034/109] settings menu --- src/desktop/backends/ios.c | 197 +++++++++++++++++++++++++++++++++++-- 1 file changed, 190 insertions(+), 7 deletions(-) diff --git a/src/desktop/backends/ios.c b/src/desktop/backends/ios.c index 2071a0f80..1d3b7c819 100755 --- a/src/desktop/backends/ios.c +++ b/src/desktop/backends/ios.c @@ -67,6 +67,11 @@ static UIView *g_overlayView = nil; * controller before the game thread is started. */ static char g_gamePath[PATH_MAX]; +/* Built from the persisted fast-forward speed setting right before each + * game launch -- see AppDelegate startGameWithFolder:. Sized generously + * for any reasonable floating-point value typed into the settings field. */ +static char g_ffSpeedArg[64] = "--fast-forward-speed=4"; + #define BS_GAMES_ROOT_PATH @"/var/mobile/Documents/Butterscotch" static void bsRequestRelayout(void) { @@ -96,13 +101,16 @@ static void bsRequestRelayout(void) { #define BS_CONTROL_STRIP_LANDSCAPE_W 120.0f #define BS_QUIT_BUTTON_SIZE 22.0f #define BS_QUIT_BUTTON_MARGIN 8.0f -#define BS_GAME_TOP_PADDING (BS_QUIT_BUTTON_MARGIN + BS_QUIT_BUTTON_SIZE + 4.0f) +#define BS_FF_BUTTON_SIZE 32.0f /* a bit bigger than the quit button */ +/* padded off the taller of the two top-corner buttons so neither gets clipped */ +#define BS_GAME_TOP_PADDING (BS_QUIT_BUTTON_MARGIN + BS_FF_BUTTON_SIZE + 4.0f) typedef struct { CGRect gameFrame; CGRect dpadFrame; CGRect buttonsFrame; CGRect quitFrame; + CGRect ffFrame; bool portrait; } BSLayout; @@ -130,6 +138,10 @@ static BSLayout computeLayout(CGSize screen) { layout.quitFrame = CGRectMake(screen.width - BS_QUIT_BUTTON_MARGIN - BS_QUIT_BUTTON_SIZE, BS_QUIT_BUTTON_MARGIN, BS_QUIT_BUTTON_SIZE, BS_QUIT_BUTTON_SIZE); + + /* Fast-forward: top-left, mirrors the quit button on the top-right. */ + layout.ffFrame = CGRectMake(BS_QUIT_BUTTON_MARGIN, BS_QUIT_BUTTON_MARGIN, + BS_FF_BUTTON_SIZE, BS_FF_BUTTON_SIZE); return layout; } @@ -443,6 +455,7 @@ void platformSleepUntil(uint64_t time) { int32_t dpadKeysDown[4]; /* up, down, left, right */ UITouch *buttonTouches[3]; /* z, x, c */ UITouch *quitTouch; + UITouch *ffTouch; } @end @@ -457,6 +470,7 @@ void platformSleepUntil(uint64_t time) { self.autoresizingMask = UIViewAutoresizingNone; dpadTouch = nil; quitTouch = nil; + ffTouch = nil; for (int i = 0; i < 4; i++) dpadKeysDown[i] = 0; for (int i = 0; i < 3; i++) buttonTouches[i] = nil; } @@ -523,6 +537,9 @@ static void drawCenteredLabel(NSString *text, CGRect rect, UIFont *font) { drawTranslucentCircle(ctx, bsLayout.quitFrame, quitTouch != nil); drawCenteredLabel(@"X", bsLayout.quitFrame, [UIFont boldSystemFontOfSize:14.0f]); + + drawTranslucentCircle(ctx, bsLayout.ffFrame, ffTouch != nil); + drawCenteredLabel(@">>", bsLayout.ffFrame, [UIFont boldSystemFontOfSize:16.0f]); } - (void)updateDpadUp:(bool)up down:(bool)down left:(bool)left right:(bool)right { @@ -553,6 +570,13 @@ static void drawCenteredLabel(NSString *text, CGRect rect, UIFont *font) { continue; } + if (!ffTouch && CGRectContainsPoint(CGRectInset(bsLayout.ffFrame, -6, -6), p)) { + ffTouch = [touch retain]; + if (g_runner) RunnerKeyboard_onKeyDown(g_runner->keyboard, VK_TAB); + [self setNeedsDisplay]; + continue; + } + if (!dpadTouch && CGRectContainsPoint(CGRectInset(bsLayout.dpadFrame, -20, -20), p)) { dpadTouch = [touch retain]; bool up, down, left, right; @@ -606,6 +630,13 @@ static void drawCenteredLabel(NSString *text, CGRect rect, UIFont *font) { [self setNeedsDisplay]; continue; } + if (touch == ffTouch) { + if (g_runner) RunnerKeyboard_onKeyUp(g_runner->keyboard, VK_TAB); + [ffTouch release]; + ffTouch = nil; + [self setNeedsDisplay]; + continue; + } for (int i = 0; i < 3; i++) { if (touch == buttonTouches[i]) { int32_t vk = (i == 0) ? 'Z' : (i == 1) ? 'X' : 'C'; @@ -632,6 +663,7 @@ static void drawCenteredLabel(NSString *text, CGRect rect, UIFont *font) { - (void)dealloc { if (dpadTouch) [dpadTouch release]; if (quitTouch) [quitTouch release]; + if (ffTouch) [ffTouch release]; for (int i = 0; i < 3; i++) if (buttonTouches[i]) [buttonTouches[i] release]; [super dealloc]; } @@ -665,6 +697,128 @@ static void drawCenteredLabel(NSString *text, CGRect rect, UIFont *font) { @end +/* --------------------------------------------------------------------- + * Settings: currently just the fast-forward speed multiplier, persisted + * via NSUserDefaults and applied as a --fast-forward-speed= argv entry + * the next time a game is launched (see AppDelegate startGameWithFolder:). + * ------------------------------------------------------------------- */ + +#define BS_FF_SPEED_DEFAULTS_KEY @"BSFastForwardSpeed" +#define BS_DEFAULT_FF_SPEED 4.0 + +/* Falls back to the default any time the stored value is missing or + * non-positive (e.g. first launch, or a corrupted/edited defaults plist). */ +static double bsLoadFastForwardSpeed(void) { + double v = [[NSUserDefaults standardUserDefaults] doubleForKey:BS_FF_SPEED_DEFAULTS_KEY]; + return (v > 0.0) ? v : BS_DEFAULT_FF_SPEED; +} + +/* Renders a simple gear glyph into a UIImage via Core Graphics, rather than + * relying on a Unicode gear character rendering correctly on very old + * font/rendering stacks. Uses UIGraphicsBeginImageContext (iOS 2+) rather + * than the *WithOptions variant (iOS 4+) to stay compatible with the low + * end of the SDK matrix. */ +static UIImage *createGearIconImage(CGFloat size, UIColor *color) { + UIGraphicsBeginImageContext(CGSizeMake(size, size)); + CGContextRef ctx = UIGraphicsGetCurrentContext(); + + CGPoint center = CGPointMake(size / 2.0f, size / 2.0f); + CGFloat outerR = size * 0.46f; + CGFloat toothR = size * 0.12f; + CGFloat innerR = size * 0.28f; + const int teeth = 8; + + CGContextSetFillColorWithColor(ctx, color.CGColor); + + CGMutablePathRef path = CGPathCreateMutable(); + for (int i = 0; i < teeth * 2; i++) { + CGFloat angle = (CGFloat)i * (CGFloat)M_PI / (CGFloat)teeth; + CGFloat r = (i % 2 == 0) ? (outerR + toothR) : outerR; + CGFloat x = center.x + r * cosf(angle); + CGFloat y = center.y + r * sinf(angle); + if (i == 0) CGPathMoveToPoint(path, NULL, x, y); + else CGPathAddLineToPoint(path, NULL, x, y); + } + CGPathCloseSubpath(path); + CGContextAddPath(ctx, path); + CGContextFillPath(ctx); + CGPathRelease(path); + + /* Punch the center hole so it reads as a gear rather than a spiky disc. */ + CGContextSetBlendMode(ctx, kCGBlendModeClear); + CGContextFillEllipseInRect(ctx, CGRectMake(center.x - innerR, center.y - innerR, + innerR * 2.0f, innerR * 2.0f)); + + UIImage *img = UIGraphicsGetImageFromCurrentImageContext(); + UIGraphicsEndImageContext(); + return img; +} + +@interface BSSettingsViewController : UIViewController { + UITextField *speedField; +} +@end + +@implementation BSSettingsViewController + +- (void)loadView { + CGRect bounds = [[UIScreen mainScreen] bounds]; + UIView *root = [[[UIView alloc] initWithFrame:bounds] autorelease]; + root.backgroundColor = [UIColor whiteColor]; + root.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; + + UILabel *fieldLabel = [[[UILabel alloc] initWithFrame:CGRectMake(20, 20, bounds.size.width - 40, 24)] autorelease]; + fieldLabel.text = @"Fast forward speed (multiplier):"; + fieldLabel.font = [UIFont systemFontOfSize:15.0f]; + fieldLabel.backgroundColor = [UIColor clearColor]; + [root addSubview:fieldLabel]; + + speedField = [[UITextField alloc] initWithFrame:CGRectMake(20, 48, bounds.size.width - 40, 36)]; + speedField.borderStyle = UITextBorderStyleRoundedRect; + speedField.keyboardType = UIKeyboardTypeDecimalPad; + speedField.delegate = self; + speedField.text = [NSString stringWithFormat:@"%g", bsLoadFastForwardSpeed()]; + [root addSubview:speedField]; + + self.view = root; +} + +- (id)init { + if ((self = [super init])) { + self.title = @"Settings"; + UIBarButtonItem *saveItem = [[UIBarButtonItem alloc] initWithTitle:@"Save" + style:UIBarButtonItemStyleDone + target:self action:@selector(saveTapped)]; + self.navigationItem.rightBarButtonItem = saveItem; + [saveItem release]; + } + return self; +} + +- (void)saveTapped { + [speedField resignFirstResponder]; + double v = [speedField.text doubleValue]; + if (v > 0.0) { + [[NSUserDefaults standardUserDefaults] setDouble:v forKey:BS_FF_SPEED_DEFAULTS_KEY]; + [[NSUserDefaults standardUserDefaults] synchronize]; + } + /* Invalid/non-positive input is silently ignored -- previously saved + * value (or the default) is left untouched. */ + [[[UIApplication sharedApplication] delegate] performSelector:@selector(settingsDone)]; +} + +- (BOOL)textFieldShouldReturn:(UITextField *)textField { + [textField resignFirstResponder]; + return YES; +} + +- (void)dealloc { + [speedField release]; + [super dealloc]; +} + +@end + /* --------------------------------------------------------------------- * Game selection menu. Scans BS_GAMES_ROOT_PATH for subfolders that * contain a data.win, and lets the user pick one. Re-scans every time @@ -688,11 +842,23 @@ static void drawCenteredLabel(NSString *text, CGRect rect, UIFont *font) { #endif games = [[NSMutableArray alloc] init]; self.title = @"Butterscotch"; + + UIImage *gearImg = createGearIconImage(22.0f, [UIColor darkGrayColor]); + UIBarButtonItem *gearItem = [[UIBarButtonItem alloc] initWithImage:gearImg + style:UIBarButtonItemStylePlain + target:self action:@selector(settingsTapped)]; + self.navigationItem.leftBarButtonItem = gearItem; + [gearItem release]; + [self reloadGames]; } return self; } +- (void)settingsTapped { + [[[UIApplication sharedApplication] delegate] performSelector:@selector(showSettings)]; +} + - (void)reloadGames { [games removeAllObjects]; @@ -760,10 +926,14 @@ extern int game_main(int argc, char *argv[]); BSTouchOverlay *overlay; UIView *rootView; BSGameListViewController *gameListVC; + BSSettingsViewController *settingsVC; + UINavigationController *navController; BOOL usingRootViewController; } - (void)startGameWithFolder:(NSString *)folderName; - (void)returnToMenu; +- (void)showSettings; +- (void)settingsDone; - (void)orientationChanged:(NSNotification *)note; @end @@ -775,8 +945,8 @@ extern int game_main(int argc, char *argv[]); static char arg0[] = "butterscotch"; static char arg1[] = "--lazy-textures"; static char arg2[] = "--lazy-rooms"; - char *argv[] = { arg0, arg1, arg2, g_gamePath, NULL }; - game_main(4, argv); + char *argv[] = { arg0, arg1, arg2, g_ffSpeedArg, g_gamePath, NULL }; + game_main(5, argv); [self performSelectorOnMainThread:@selector(returnToMenu) withObject:nil waitUntilDone:NO]; @@ -787,6 +957,7 @@ extern int game_main(int argc, char *argv[]); NSString *dataWin = [[BS_GAMES_ROOT_PATH stringByAppendingPathComponent:folderName] stringByAppendingPathComponent:@"data.win"]; strlcpy(g_gamePath, [dataWin fileSystemRepresentation], sizeof(g_gamePath)); + snprintf(g_ffSpeedArg, sizeof(g_ffSpeedArg), "--fast-forward-speed=%g", bsLoadFastForwardSpeed()); atomic_store(&quitRequested, false); @@ -822,11 +993,11 @@ extern int game_main(int argc, char *argv[]); g_overlayView = nil; if (usingRootViewController) { - [window performSelector:@selector(setRootViewController:) withObject:gameListVC]; + [window performSelector:@selector(setRootViewController:) withObject:navController]; } else { NSArray *subs = [[window.subviews copy] autorelease]; for (UIView *sub in subs) [sub removeFromSuperview]; - [window addSubview:gameListVC.view]; + [window addSubview:navController.view]; } [overlay release]; @@ -837,6 +1008,15 @@ extern int game_main(int argc, char *argv[]); view = nil; } +- (void)showSettings { + if (!settingsVC) settingsVC = [[BSSettingsViewController alloc] init]; + [navController pushViewController:settingsVC animated:YES]; +} + +- (void)settingsDone { + [navController popViewControllerAnimated:YES]; +} + /* * Belt-and-suspenders relayout trigger: fires purely off physical device * orientation, independent of whichever (if any) view-controller rotation @@ -864,11 +1044,12 @@ extern int game_main(int argc, char *argv[]); usingRootViewController = [window respondsToSelector:@selector(setRootViewController:)]; gameListVC = [[BSGameListViewController alloc] init]; + navController = [[UINavigationController alloc] initWithRootViewController:gameListVC]; if (usingRootViewController) { - [window performSelector:@selector(setRootViewController:) withObject:gameListVC]; + [window performSelector:@selector(setRootViewController:) withObject:navController]; } else { - [window addSubview:gameListVC.view]; + [window addSubview:navController.view]; } [window makeKeyAndVisible]; } @@ -883,6 +1064,8 @@ extern int game_main(int argc, char *argv[]); [rootView release]; [view release]; [gameListVC release]; + [settingsVC release]; + [navController release]; [window release]; [super dealloc]; } From 4c308cbf78d9783d2c019dd40583cefe6c13700e Mon Sep 17 00:00:00 2001 From: Un1q32 Date: Mon, 6 Jul 2026 04:26:14 -0400 Subject: [PATCH 035/109] iOS 2 --- src/desktop/backends/ios.c | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/src/desktop/backends/ios.c b/src/desktop/backends/ios.c index 1d3b7c819..0b9b451b2 100755 --- a/src/desktop/backends/ios.c +++ b/src/desktop/backends/ios.c @@ -759,6 +759,20 @@ static UIImage *createGearIconImage(CGFloat size, UIColor *color) { } @end +/* UIKeyboardTypeDecimalPad was added in iOS 4.1. Rather than gate on the + * SDK this file happens to be compiled against (which says nothing about + * whether the *running* device actually supports it, and may not even + * declare the symbol), reference its known raw enum value directly and + * decide whether to use it purely from the device's reported OS version + * at runtime. Falls back to NumbersAndPunctuation (available since iOS + * 2.0) on anything older -- still exposes a decimal point. */ +static UIKeyboardType bsNumericKeyboardType(void) { + NSString *sysVersion = [[UIDevice currentDevice] systemVersion]; + if ([sysVersion compare:@"4.1" options:NSNumericSearch] != NSOrderedAscending) + return (UIKeyboardType)8; /* UIKeyboardTypeDecimalPad's raw value */ + return UIKeyboardTypeNumbersAndPunctuation; +} + @implementation BSSettingsViewController - (void)loadView { @@ -775,7 +789,7 @@ static UIImage *createGearIconImage(CGFloat size, UIColor *color) { speedField = [[UITextField alloc] initWithFrame:CGRectMake(20, 48, bounds.size.width - 40, 36)]; speedField.borderStyle = UITextBorderStyleRoundedRect; - speedField.keyboardType = UIKeyboardTypeDecimalPad; + speedField.keyboardType = bsNumericKeyboardType(); speedField.delegate = self; speedField.text = [NSString stringWithFormat:@"%g", bsLoadFastForwardSpeed()]; [root addSubview:speedField]; From 290f1fd015cb6daa63d40232bd09a00cec55d19d Mon Sep 17 00:00:00 2001 From: Un1q32 Date: Mon, 6 Jul 2026 15:59:04 -0400 Subject: [PATCH 036/109] manual rotation --- src/desktop/backends/ios.c | 100 ++++++++++++++++++++++++++++++------- 1 file changed, 83 insertions(+), 17 deletions(-) diff --git a/src/desktop/backends/ios.c b/src/desktop/backends/ios.c index 0b9b451b2..a308a995b 100755 --- a/src/desktop/backends/ios.c +++ b/src/desktop/backends/ios.c @@ -63,6 +63,13 @@ static float g_aspectRatio = 1.0f; static UIView *g_glView = nil; static UIView *g_overlayView = nil; +/* Last UIDeviceOrientation actually applied to rootView by + * applyDeviceOrientation:, used to no-op duplicate/redundant notifications. + * Reset to Unknown in startGameWithFolder: so each new game session always + * re-syncs to the device's current orientation, even if it's the same + * orientation the previous session last settled into. */ +static UIDeviceOrientation g_lastAppliedOrientation = UIDeviceOrientationUnknown; + /* Path to the selected game's data.win, filled in by the game list * controller before the game thread is started. */ static char g_gamePath[PATH_MAX]; @@ -675,18 +682,27 @@ static void drawCenteredLabel(NSString *text, CGRect rect, UIFont *font) { @implementation BSViewController +/* Rotation is now handled manually (see AppDelegate applyDeviceOrientation: + * below) instead of via UIKit's autorotation machinery, whose trigger + * mechanism and defaults have shifted across the iOS 2-10 / SDK 2-26 + * matrix this targets. Returning NO everywhere below means the OS never + * tries to rotate the view controller's view itself, on any version -- + * we lock to Portrait and do all the rotating ourselves. */ - (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation { (void)interfaceOrientation; - return YES; + return NO; } -- (void)willAnimateRotationToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation - duration:(NSTimeInterval)duration { - (void)toInterfaceOrientation; - [UIView beginAnimations:nil context:NULL]; - [UIView setAnimationDuration:duration]; - bsRequestRelayout(); - [UIView commitAnimations]; +/* iOS 6+ query this pair instead of the method above. NSUInteger (rather + * than UIInterfaceOrientationMask) as the return type keeps this + * compiling against SDKs that predate that type -- it's still called + * correctly at runtime on OS versions that support it. */ +- (BOOL)shouldAutorotate { + return NO; +} + +- (NSUInteger)supportedInterfaceOrientations { + return 1; /* UIInterfaceOrientationMaskPortrait's raw value (1 << UIInterfaceOrientationPortrait) */ } /* Pre-iOS-7: without this, the view is offset 20pt down to make room for @@ -949,6 +965,7 @@ extern int game_main(int argc, char *argv[]); - (void)showSettings; - (void)settingsDone; - (void)orientationChanged:(NSNotification *)note; +- (void)applyDeviceOrientation:(UIDeviceOrientation)devOrientation; @end @implementation AppDelegate @@ -984,10 +1001,16 @@ extern int game_main(int argc, char *argv[]); g_overlayView = overlay; rootView = [[UIView alloc] initWithFrame:bounds]; - rootView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; + rootView.autoresizingMask = UIViewAutoresizingNone; /* we drive rootView's frame manually now -- autoresizing masks are unreliable once a transform is applied */ [rootView addSubview:view]; [rootView addSubview:overlay]; /* on top, so controls are visible over the game */ + /* Force a fresh sync even if the device is in the same physical + * orientation the last game session ended in -- g_lastAppliedOrientation + * is stale for this brand-new rootView. */ + g_lastAppliedOrientation = UIDeviceOrientationUnknown; + [self applyDeviceOrientation:[[UIDevice currentDevice] orientation]]; + if (usingRootViewController) { UIViewController *vc = [[BSViewController alloc] init]; vc.view = rootView; @@ -1031,16 +1054,59 @@ extern int game_main(int argc, char *argv[]); [navController popViewControllerAnimated:YES]; } -/* - * Belt-and-suspenders relayout trigger: fires purely off physical device - * orientation, independent of whichever (if any) view-controller rotation - * callback chain the running OS decides to invoke for this binary's linked - * SDK version. Harmless to have alongside the view-controller-driven paths - * above -- bsRequestRelayout() just forces a layout pass against the - * views' current bounds, so redundant calls are cheap no-ops. - */ - (void)orientationChanged:(NSNotification *)note { (void)note; + [self applyDeviceOrientation:[[UIDevice currentDevice] orientation]]; +} + +/* --------------------------------------------------------------------- + * Manual rotation for the game screen. Locks the interface orientation + * (see BSViewController) and instead rotates rootView ourselves off raw + * UIDeviceOrientation notifications, which have been stable since iOS + * 2.0 -- unlike the view-controller rotation callback chain, which isn't. + * + * rootView's children (GLView, BSTouchOverlay) are untouched by this; + * they just read superview.bounds.size on layout, same as always, so + * computeLayout()'s existing portrait/landscape logic keeps working + * without any changes on that side. + * + * Only applies when a game is running (rootView != nil). The menu / + * settings navigation stack is left in the OS's default fixed + * orientation and does not rotate. + * ------------------------------------------------------------------- */ +- (void)applyDeviceOrientation:(UIDeviceOrientation)devOrientation { + if (!rootView) return; + + CGFloat angle; + BOOL swapped; + switch (devOrientation) { + case UIDeviceOrientationPortrait: angle = 0.0f; swapped = NO; break; + case UIDeviceOrientationPortraitUpsideDown: angle = (CGFloat)M_PI; swapped = NO; break; + /* Device rotated so its left edge points "up" corresponds to + * interface orientation LandscapeRight, i.e. a +90 degree + * compensating rotation; the other case is the mirror image. */ + case UIDeviceOrientationLandscapeLeft: angle = (CGFloat)M_PI_2; swapped = YES; break; + case UIDeviceOrientationLandscapeRight: angle = -(CGFloat)M_PI_2; swapped = YES; break; + default: + /* FaceUp / FaceDown / Unknown: not a usable orientation -- + * keep whatever we last applied. */ + return; + } + + if (devOrientation == g_lastAppliedOrientation) return; + g_lastAppliedOrientation = devOrientation; + + CGRect nativeBounds = [[UIScreen mainScreen] bounds]; + CGSize logicalSize = swapped ? CGSizeMake(nativeBounds.size.height, nativeBounds.size.width) + : nativeBounds.size; + + [UIView beginAnimations:nil context:NULL]; + [UIView setAnimationDuration:0.3]; + rootView.transform = CGAffineTransformMakeRotation(angle); + rootView.bounds = CGRectMake(0, 0, logicalSize.width, logicalSize.height); + rootView.center = CGPointMake(CGRectGetMidX(nativeBounds), CGRectGetMidY(nativeBounds)); + [UIView commitAnimations]; + bsRequestRelayout(); } From 1ecfccd5ed1557e0cc19760e94d2a64c073dd1bc Mon Sep 17 00:00:00 2001 From: Un1q32 Date: Mon, 6 Jul 2026 16:12:04 -0400 Subject: [PATCH 037/109] fix resolution scaling --- src/desktop/backends/ios.c | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/desktop/backends/ios.c b/src/desktop/backends/ios.c index a308a995b..3947e0575 100755 --- a/src/desktop/backends/ios.c +++ b/src/desktop/backends/ios.c @@ -333,6 +333,20 @@ static void applyRenderScale(void) { if (targetScale <= 0.0f) targetScale = nativeScale; /* safety net */ + /* CAEAGLLayer derives its backing pixel size as bounds.size (points) * + * contentsScale, and truncates rather than rounds that product down + * to an integer. The division/multiplication chain above -- aspect + * ratio, then layout, then the division just above -- can leave + * targetScale a hair below the value that would exactly reproduce + * the intended pixel count (e.g. 1.499998 instead of 1.5), which + * then silently truncates a whole pixel off the framebuffer on each + * axis (observed as 639x479 instead of 640x480 on an iPhone 5S in + * landscape). Nudge just past that truncation boundary -- this is + * far too small (a fraction of a point in scale) to visibly affect + * the actual rendered resolution, but reliably avoids the off-by-one. */ + targetScale += 0.0005f; + if (targetScale > nativeScale) targetScale = nativeScale; + if ([layer respondsToSelector:@selector(setContentsScale:)]) { void (*setScale)(id, SEL, CGFloat) = (void (*)(id, SEL, CGFloat))objc_msgSend; setScale(layer, @selector(setContentsScale:), targetScale); From 0ea56eaebe4d86c2ab3fc23644b83a773ff3c082 Mon Sep 17 00:00:00 2001 From: Un1q32 Date: Mon, 6 Jul 2026 16:22:35 -0400 Subject: [PATCH 038/109] high res setting --- src/desktop/backends/ios.c | 85 ++++++++++++++++++++++++++++++++++---- 1 file changed, 76 insertions(+), 9 deletions(-) diff --git a/src/desktop/backends/ios.c b/src/desktop/backends/ios.c index 3947e0575..a3c23259d 100755 --- a/src/desktop/backends/ios.c +++ b/src/desktop/backends/ios.c @@ -79,6 +79,12 @@ static char g_gamePath[PATH_MAX]; * for any reasonable floating-point value typed into the settings field. */ static char g_ffSpeedArg[64] = "--fast-forward-speed=4"; +/* Cached copy of the "high resolution" setting (see BS_HIGH_RES_DEFAULTS_KEY + * below), refreshed from NSUserDefaults once per game launch in + * startGameWithFolder: rather than read on every applyRenderScale() call -- + * the setting can only be changed from the menu, never mid-game. */ +static atomic_bool g_highResEnabled = false; + #define BS_GAMES_ROOT_PATH @"/var/mobile/Documents/Butterscotch" static void bsRequestRelayout(void) { @@ -227,12 +233,22 @@ bool platformGetScaledWindowSize(int32_t* outW, int32_t* outH) { * size can also change independently of this call (rotation), so both * paths just flag needsResize and let resizeFramebuffer() re-derive it * from whatever's current on the next frame. + * + * The requested width/height also determine g_aspectRatio, which drives + * computeLayout()'s letterboxing -- previously only set once in + * platformInit(), so a game changing its resolution mid-run (e.g. a + * GameMaker room with a different size) kept the *old* aspect ratio's + * gameFrame even though the framebuffer underneath it got resized to + * match the new one. Recompute it here too, and force a relayout the + * same way platformInit() does for the initial value. */ void platformSetWindowSize(int32_t width, int32_t height) { if (width <= 0 || height <= 0) return; g_reqRenderWidth = width; g_reqRenderHeight = height; + g_aspectRatio = (float)width / (float)height; + bsRequestRelayout(); atomic_store(&needsResize, true); } @@ -310,8 +326,9 @@ static CGFloat nativeScreenScale(void) { * already-underpowered low-res devices can't spare. * * Recomputed from scratch every call (cheap) rather than cached, since - * either input -- the requested resolution, or the frame's current point - * size -- can change independently (platformSetWindowSize vs. rotation). + * the inputs -- the requested resolution, the "high resolution" setting, + * or the frame's current point size -- can each change independently + * (platformSetWindowSize, the settings menu, or rotation respectively). */ static void applyRenderScale(void) { if (!layer) return; @@ -322,9 +339,27 @@ static void applyRenderScale(void) { CGFloat nativeScale = nativeScreenScale(); CGFloat targetScale = nativeScale; - if (g_reqRenderWidth > 0 && g_reqRenderHeight > 0) { - CGFloat sW = (CGFloat)g_reqRenderWidth / sz.width; - CGFloat sH = (CGFloat)g_reqRenderHeight / sz.height; + int32_t effReqWidth = g_reqRenderWidth; + int32_t effReqHeight = g_reqRenderHeight; + + /* "High resolution" setting (see BS_HIGH_RES_DEFAULTS_KEY): instead of + * clamping down to the game's requested render resolution (e.g. + * 640x480), target the full available logical screen size at native + * pixel density instead -- most noticeable in landscape, where the + * requested resolution would otherwise get scaled down well below + * what the device can actually display. g_glView's superview + * (rootView) already reflects the current device orientation (see + * AppDelegate applyDeviceOrientation:), so its bounds are exactly the + * logical on-screen size for however the device is currently held. */ + if (atomic_load(&g_highResEnabled) && g_glView && g_glView.superview) { + CGSize logicalSize = g_glView.superview.bounds.size; + effReqWidth = (int32_t)(logicalSize.width * nativeScale); + effReqHeight = (int32_t)(logicalSize.height * nativeScale); + } + + if (effReqWidth > 0 && effReqHeight > 0) { + CGFloat sW = (CGFloat)effReqWidth / sz.width; + CGFloat sH = (CGFloat)effReqHeight / sz.height; /* Use whichever axis wants the smaller scale, so we never exceed * the requested pixel count on either dimension -- then clamp to * native so we never exceed the device's real resolution either. */ @@ -728,14 +763,18 @@ static void drawCenteredLabel(NSString *text, CGRect rect, UIFont *font) { @end /* --------------------------------------------------------------------- - * Settings: currently just the fast-forward speed multiplier, persisted - * via NSUserDefaults and applied as a --fast-forward-speed= argv entry - * the next time a game is launched (see AppDelegate startGameWithFolder:). + * Settings: the fast-forward speed multiplier and the "high resolution" + * toggle, both persisted via NSUserDefaults. The fast-forward speed is + * applied as a --fast-forward-speed= argv entry, and the high-res flag + * is cached into g_highResEnabled, the next time a game is launched (see + * AppDelegate startGameWithFolder:). * ------------------------------------------------------------------- */ #define BS_FF_SPEED_DEFAULTS_KEY @"BSFastForwardSpeed" #define BS_DEFAULT_FF_SPEED 4.0 +#define BS_HIGH_RES_DEFAULTS_KEY @"BSHighResolution" + /* Falls back to the default any time the stored value is missing or * non-positive (e.g. first launch, or a corrupted/edited defaults plist). */ static double bsLoadFastForwardSpeed(void) { @@ -743,6 +782,12 @@ static double bsLoadFastForwardSpeed(void) { return (v > 0.0) ? v : BS_DEFAULT_FF_SPEED; } +/* boolForKey: returns NO when the key has never been set, so this is off + * by default with no separate first-launch handling needed. */ +static bool bsLoadHighResEnabled(void) { + return [[NSUserDefaults standardUserDefaults] boolForKey:BS_HIGH_RES_DEFAULTS_KEY]; +} + /* Renders a simple gear glyph into a UIImage via Core Graphics, rather than * relying on a Unicode gear character rendering correctly on very old * font/rendering stacks. Uses UIGraphicsBeginImageContext (iOS 2+) rather @@ -786,6 +831,7 @@ static UIImage *createGearIconImage(CGFloat size, UIColor *color) { @interface BSSettingsViewController : UIViewController { UITextField *speedField; + UISwitch *highResSwitch; } @end @@ -824,6 +870,22 @@ static UIKeyboardType bsNumericKeyboardType(void) { speedField.text = [NSString stringWithFormat:@"%g", bsLoadFastForwardSpeed()]; [root addSubview:speedField]; + /* sizeToFit rather than a hardcoded frame, since UISwitch's on-screen + * size has drifted slightly across OS versions. */ + highResSwitch = [[UISwitch alloc] initWithFrame:CGRectZero]; + [highResSwitch sizeToFit]; + CGRect swFrame = highResSwitch.frame; + swFrame.origin = CGPointMake(bounds.size.width - 20 - swFrame.size.width, 100); + highResSwitch.frame = swFrame; + highResSwitch.on = bsLoadHighResEnabled(); + [root addSubview:highResSwitch]; + + UILabel *highResLabel = [[[UILabel alloc] initWithFrame:CGRectMake(20, 100, swFrame.origin.x - 30, swFrame.size.height)] autorelease]; + highResLabel.text = @"High resolution (landscape):"; + highResLabel.font = [UIFont systemFontOfSize:15.0f]; + highResLabel.backgroundColor = [UIColor clearColor]; + [root addSubview:highResLabel]; + self.view = root; } @@ -844,10 +906,13 @@ static UIKeyboardType bsNumericKeyboardType(void) { double v = [speedField.text doubleValue]; if (v > 0.0) { [[NSUserDefaults standardUserDefaults] setDouble:v forKey:BS_FF_SPEED_DEFAULTS_KEY]; - [[NSUserDefaults standardUserDefaults] synchronize]; } /* Invalid/non-positive input is silently ignored -- previously saved * value (or the default) is left untouched. */ + + [[NSUserDefaults standardUserDefaults] setBool:highResSwitch.isOn forKey:BS_HIGH_RES_DEFAULTS_KEY]; + [[NSUserDefaults standardUserDefaults] synchronize]; + [[[UIApplication sharedApplication] delegate] performSelector:@selector(settingsDone)]; } @@ -858,6 +923,7 @@ static UIKeyboardType bsNumericKeyboardType(void) { - (void)dealloc { [speedField release]; + [highResSwitch release]; [super dealloc]; } @@ -1003,6 +1069,7 @@ extern int game_main(int argc, char *argv[]); stringByAppendingPathComponent:@"data.win"]; strlcpy(g_gamePath, [dataWin fileSystemRepresentation], sizeof(g_gamePath)); snprintf(g_ffSpeedArg, sizeof(g_ffSpeedArg), "--fast-forward-speed=%g", bsLoadFastForwardSpeed()); + atomic_store(&g_highResEnabled, bsLoadHighResEnabled()); atomic_store(&quitRequested, false); From e33dc49be828db6b4fa3285a747b3375567b6ef5 Mon Sep 17 00:00:00 2001 From: Un1q32 Date: Mon, 6 Jul 2026 16:53:13 -0400 Subject: [PATCH 039/109] add ios assets --- targets/ios/Default-568h@2x.png | 1 + targets/ios/Default-667h@2x.png | 1 + targets/ios/Default-736h@3x.png | 1 + targets/ios/Default.png | Bin 0 -> 308 bytes targets/ios/Default@2x.png | 1 + targets/ios/Icon-20.png | Bin 0 -> 1084 bytes targets/ios/Icon-20@2x.png | Bin 0 -> 2118 bytes targets/ios/Icon-20@3x.png | Bin 0 -> 3143 bytes targets/ios/Icon-40.png | Bin 0 -> 2118 bytes targets/ios/Icon-40@2x.png | Bin 0 -> 3952 bytes targets/ios/Icon-40@3x.png | Bin 0 -> 5009 bytes targets/ios/Icon-60@2x.png | Bin 0 -> 5009 bytes targets/ios/Icon-60@3x.png | Bin 0 -> 5539 bytes targets/ios/Icon-72.png | Bin 0 -> 3562 bytes targets/ios/Icon-72@2x.png | Bin 0 -> 5185 bytes targets/ios/Icon-76.png | Bin 0 -> 3839 bytes targets/ios/Icon-76@2x.png | Bin 0 -> 5310 bytes targets/ios/Icon-83.5@2x.png | Bin 0 -> 5466 bytes targets/ios/Icon-Small.png | Bin 0 -> 1503 bytes targets/ios/Icon-Small@2x.png | Bin 0 -> 3124 bytes targets/ios/Icon-Small@3x.png | Bin 0 -> 4173 bytes targets/ios/Icon.png | Bin 0 -> 2983 bytes targets/ios/Icon@2x.png | Bin 0 -> 5024 bytes targets/ios/Info.plist | 56 ++++++++++++++++++++++++++++++++ targets/ios/iTunesArtwork | Bin 0 -> 1059 bytes targets/ios/iTunesArtwork@2x | Bin 0 -> 17469 bytes 26 files changed, 60 insertions(+) create mode 120000 targets/ios/Default-568h@2x.png create mode 120000 targets/ios/Default-667h@2x.png create mode 120000 targets/ios/Default-736h@3x.png create mode 100644 targets/ios/Default.png create mode 120000 targets/ios/Default@2x.png create mode 100644 targets/ios/Icon-20.png create mode 100644 targets/ios/Icon-20@2x.png create mode 100644 targets/ios/Icon-20@3x.png create mode 100644 targets/ios/Icon-40.png create mode 100644 targets/ios/Icon-40@2x.png create mode 100644 targets/ios/Icon-40@3x.png create mode 100644 targets/ios/Icon-60@2x.png create mode 100644 targets/ios/Icon-60@3x.png create mode 100644 targets/ios/Icon-72.png create mode 100644 targets/ios/Icon-72@2x.png create mode 100644 targets/ios/Icon-76.png create mode 100644 targets/ios/Icon-76@2x.png create mode 100644 targets/ios/Icon-83.5@2x.png create mode 100644 targets/ios/Icon-Small.png create mode 100644 targets/ios/Icon-Small@2x.png create mode 100644 targets/ios/Icon-Small@3x.png create mode 100644 targets/ios/Icon.png create mode 100644 targets/ios/Icon@2x.png create mode 100644 targets/ios/Info.plist create mode 100644 targets/ios/iTunesArtwork create mode 100644 targets/ios/iTunesArtwork@2x diff --git a/targets/ios/Default-568h@2x.png b/targets/ios/Default-568h@2x.png new file mode 120000 index 000000000..7b6e25fbf --- /dev/null +++ b/targets/ios/Default-568h@2x.png @@ -0,0 +1 @@ +Default.png \ No newline at end of file diff --git a/targets/ios/Default-667h@2x.png b/targets/ios/Default-667h@2x.png new file mode 120000 index 000000000..7b6e25fbf --- /dev/null +++ b/targets/ios/Default-667h@2x.png @@ -0,0 +1 @@ +Default.png \ No newline at end of file diff --git a/targets/ios/Default-736h@3x.png b/targets/ios/Default-736h@3x.png new file mode 120000 index 000000000..7b6e25fbf --- /dev/null +++ b/targets/ios/Default-736h@3x.png @@ -0,0 +1 @@ +Default.png \ No newline at end of file diff --git a/targets/ios/Default.png b/targets/ios/Default.png new file mode 100644 index 0000000000000000000000000000000000000000..a97feb2672fbde096c96d3718e6d7b1879640d6c GIT binary patch literal 308 zcmeAS@N?(olHy`uVBq!ia0vp^j35jm7|ip2ssJg4WRD z45bDP46hOx7_4S6Fo+k-*%fF5lwc|e@(X5QD4TrN0>n%5c6VW5yxS$b1ju7A@$_|N zf5pxwVyx?STR9OZq*~${QIe8al4_M)lnSI6j0}v-bPddP4a`D}46IBItxQa{4GgRd z49BQ;SOya|>|mdBL1&1k}Uj>Eal|aXmQ! Y$Yx?-e5AO?7|3GqboFyt=akR{0DOT;qyPW_ literal 0 HcmV?d00001 diff --git a/targets/ios/Default@2x.png b/targets/ios/Default@2x.png new file mode 120000 index 000000000..7b6e25fbf --- /dev/null +++ b/targets/ios/Default@2x.png @@ -0,0 +1 @@ +Default.png \ No newline at end of file diff --git a/targets/ios/Icon-20.png b/targets/ios/Icon-20.png new file mode 100644 index 0000000000000000000000000000000000000000..c4bceae2bd27993edb279b562df155cc1151dd1c GIT binary patch literal 1084 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 F^)IjenkWDO literal 0 HcmV?d00001 diff --git a/targets/ios/Icon-40.png b/targets/ios/Icon-40.png new file mode 100644 index 0000000000000000000000000000000000000000..46ebaebabed63f184061ca4e6846f1eabe03c329 GIT binary patch literal 2118 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 literal 0 HcmV?d00001 diff --git a/targets/ios/Icon-40@3x.png b/targets/ios/Icon-40@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..df60e45a2c1676a57d244cfa6b55d05000045524 GIT binary patch literal 5009 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

10s@R2PeFYIVvg{9wj z)vs1hLadwNKru57_M~*th(BT9g0Yr9y(YAsz0tyU%zLK40_jbKVnaLzX?T=PS4MsJQZon{P43_TFRHKCT6@pHli?o7{Q2b{kJPttvzhsa8!c?Z{9O5+ zN<{Rg{UAz7B}ypfsqAmFfJX%!35N~C1Z|4hrc+=(UYdp?+e|6_0BMyzHb+`KW#TB& zP#HMI=me5)AF(G!`l}~fK5K&RtL+$t{j~WF)l>QJLUpzxr@B4MJ}2hle3-@U?yD4B4!qML^XXPDSpi)((0rwde*F0 z&6d_d_}C2xPBSGR%+d4Nm*Ty1F&mGxo|1!k#Yq3mM)=Et#hDb71lUkLZq#V}p(xpy zV9z^l~!~BUVZ-tkVK7H|TEa*1EYgMYR>f-2CM%r5N zg^Dnx++8p&Lz8@#;>WFLeocCBPa7-x+49m-{c8)0#TeGU$5Pasa|-)BDF#)(|J6LF zU?!Vbhl&DEk;CM1wV}?_-R0BI+Xj|WCq?VtzxJ`{w;-CBv%s)6H8OU1z(@D+N~15Y zT)Xs!mwvs4N5rrF{)sO;q8cO>DRR70f0Lo^`m#N`A@BM)gKNPnjP9uIvdfqMAY1Hn zS5WB|Jcmi+my4?ii@Tbi6+hemdBoEo4H9x)5g6;Q8j;ZcvQ-G?XCZty0GW5RyAa7-u literal 0 HcmV?d00001 diff --git a/targets/ios/Icon-60@2x.png b/targets/ios/Icon-60@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..df60e45a2c1676a57d244cfa6b55d05000045524 GIT binary patch literal 5009 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

10s@R2PeFYIVvg{9wj z)vs1hLadwNKru57_M~*th(BT9g0Yr9y(YAsz0tyU%zLK40_jbKVnaLzX?T=PS4MsJQZon{P43_TFRHKCT6@pHli?o7{Q2b{kJPttvzhsa8!c?Z{9O5+ zN<{Rg{UAz7B}ypfsqAmFfJX%!35N~C1Z|4hrc+=(UYdp?+e|6_0BMyzHb+`KW#TB& zP#HMI=me5)AF(G!`l}~fK5K&RtL+$t{j~WF)l>QJLUpzxr@B4MJ}2hle3-@U?yD4B4!qML^XXPDSpi)((0rwde*F0 z&6d_d_}C2xPBSGR%+d4Nm*Ty1F&mGxo|1!k#Yq3mM)=Et#hDb71lUkLZq#V}p(xpy zV9z^l~!~BUVZ-tkVK7H|TEa*1EYgMYR>f-2CM%r5N zg^Dnx++8p&Lz8@#;>WFLeocCBPa7-x+49m-{c8)0#TeGU$5Pasa|-)BDF#)(|J6LF zU?!Vbhl&DEk;CM1wV}?_-R0BI+Xj|WCq?VtzxJ`{w;-CBv%s)6H8OU1z(@D+N~15Y zT)Xs!mwvs4N5rrF{)sO;q8cO>DRR70f0Lo^`m#N`A@BM)gKNPnjP9uIvdfqMAY1Hn zS5WB|Jcmi+my4?ii@Tbi6+hemdBoEo4H9x)5g6;Q8j;ZcvQ-G?XCZty0GW5RyAa7-u literal 0 HcmV?d00001 diff --git a/targets/ios/Icon-60@3x.png b/targets/ios/Icon-60@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..99f841187116f0af619b2020a07eb5b223ce714f GIT binary patch literal 5539 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 t14J@)u*P3Rsm*wql7}vWRupq zHx%V+Mb2bNczx%cd1v0=%x|7~p3gI%XFl_pdFJ`&!I_!rgE#~@0002U&;V(EiU$8C z8}PKwJQU?V1z{Hh^P2!bq&NT&p9BE>Ii=!P0Duq#0I-Sy0MzmU0KNcxJ4*d@z~XGI zj|80j8>OAkvQHWI00WzQ001ZC-(&zh#q*z%tigsi^;lO~S)htSd3@JPPbslrJ?miC zJHc*hEz;1uNXRo zO!-N!3;T4q+pylhpWJ%}lZ|o7${W{pMJVeS+|0g>nvl+!sq(5d$}tnb5pU|~8L~<9 z8l3=)D=J2i2?IHXr;7+M0Tl9lZoHr8@VserHe&X$Xk2T99DX!2YqRMLJMd_E(@rnE zMxz&fjfl;=p%o4j(wjG~@3F*KB{(pLx(%K-PwkmQD{&5`G@=)O`?-_Yjifs@x$^jz)i-V#p@*^1l!^m z!vra9VegEVag7pOJ{ysYvihxCtMQ|%XEmKluK?Y;3t~Pa2-}277kev# zr`sbIkLZ4%+TIr5N(0~X_y5`;^!FAwlbxUA!Na&cz7I)S8IHt1%iS^2pB_4DsKjDh zk?`1)&d;a73zWg&t~zskFQzCS5&H3t|MU?=Q`OyxWH>;b3v2y-lJ0Tx^|P z)H>frTN9PPNxR3xNAz{r@U>!v?Bs7FjakCMAvF^%B}a?z#&~&h5qmtK---LQ2FPmRe7ExW>TXf6Wy=$-6zXU#L6Yaqc@V_FxRN%-^FV4r|uxz%+HEdWc`#TOc zN@)QxH4&c*M4CCfd#5YZ5h)*hEZg_12gv@Nzcw~Bo+ExWmTz|Nk9SfGX@qx~4_l&4 z7M_V0sz^prw=!yBN>WaiD@(CUajb;JMcjO%m2wKP!8+GwGW#Nd@d0aS_pzO}Z=Ghb zCuFjLvbP=d#8=4bdh*KYG=%E?;tr|$hvU*(v4 zsY8VuQ=x|s7%p+xQw8kg4xmU=G_NVTpB;_AN9k1!JVT5pN`n))`)}ZXsFPi}?rS6A z(oA6VzCKl8*cMgP$WeF=0gqfO3Zr*Nj0hYr!~9=Y3P4u|#8ob(U2klyROk%NR`~f0 zrc@9KNi)U=ee&BWe8aF5cbHqq4<{`9vVR?eO2Z|YPTtTN!4A~>umwYRuc0aV^;S#d zniA-i#?g5!pR%jwy+ik}^ePK~il%%0!6$vexNp7s`E9b3F9?m-p|vwd7YfIiaA9?h ze-FAZ!qN_^%Ei3%kO21EvQ@-EcQHUhLaQlsM40E7#FIeE}7;DSzk(l#E1+!wrkFf&vG%s zy}S&7j}ZJ@>-!NxmDb}q_Om(Z=|MYFrKudNwzp#>d-4mg=?d>EmNSYoU4r-CdkV-Y z=#yohn*sIG>yTr~z#BolJ92p{xr~L*gleYdXqK~<7df7x zup-P-+-g$EnHO!#W^BNj>_j%a;zhO=C=miGs#fDo9{5>q0O~olf0&*L>su^`)L18i zR78*Z&#fWH*)@N`K6vD_u`r9@&gXhCCo!B-_KWiwPp{~8oBM1dNFAdx0?z`jn!l7n z$6^hhSTlum)qi;7#j>{*Rvi9)nwFuQcC?$aCx|-QPhIl$HYY|>*|c@GF)!|Zd(T0z zxExNvW9=AUTjv0%j}rkM2Zalb*$iM(nj9ZiI`>MIRf|dyC6!Pl+h~iLWbp|K3JUO; zvM%&B2v=A`$9S(sn`@N3uI87$f#}ca!!zVu@&ZnB40-!yilt6cmVH@R zxC!#6G^m14kbfd*(z)rkC$MxN$CV(=l>Xg80nLq+a<8v07TiZ4U(IsF$%mITebJ!&^z9d8vHiAYMn%-Gk$M>PtR~q z;#)vUP0aQ7s4@}mx0x|f%?_0vU&)0=HnX8&#Axo!?;J!#Y5>rx_)3G*#B2d{G)JtP`!Zs*@I6b{@k*3^!9Q3defrL(`Ys4C zbTgxAyWwdYgJ}`YE$Cxid}vK`IySQR^xERXqo? zv!X=8pTDs89EXWD^~=N%m4><9>6&++cW{O7U&V(d5X_1p+f);xNz35micbZ1QfHrw z>4dTPbBD>*2&Q;RW(?fvPkG!-+_!;EBCj}D<9J>E3!OF0G$v5j68U`|9qEpt;0*Xv z;-$fXA#%xz7e$&;b!kD~&5LAL>ANpRD-11#H+EyWRcZKa<1IBNCEvr{T(JxF$DzU)qNC`l z4OL9>?7P}?`3m1+I!7Rf_cm>z>5 z5It6doT;-ke5#3+TkJp&g~VRbR8>A6MhpRkOuGYxNk>a9i<8Ps7v#1!{SPa$y>4HJ zgNIF{G=p#nI*k<{yMN@mu0Rcdh?L^k?JcL6Fehcn){&TnTv$o;!q-)v?bT3jZ89ga z;{d>d$W~4tR4rFFsi+jL143xJG_Ix^a-n8)%?1AX{SVuno4b}xAcdRlQ`8|?tucO} zU0LSi^AdY_%{hLE#!*w>a7T27K=XClMt9g@!=~2NmLm7>KOUOPi(pV5nkpq~G`t5~ z@V(gOk$!;P*mUSoNNt8{RPG9X?Wac{u(Mdjs%AbwBkyCLQE$Xq>!W-pP<$S4Lh+XY zRVZTAAW+EMF0Q* literal 0 HcmV?d00001 diff --git a/targets/ios/Icon-72@2x.png b/targets/ios/Icon-72@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..08044b6e0f05e6926b1f4fbb8aa022823becf5dc GIT binary patch literal 5185 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 literal 0 HcmV?d00001 diff --git a/targets/ios/Icon-76@2x.png b/targets/ios/Icon-76@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..5e65aa54a9c5933fa4f36a124ef0e53c42bd995b GIT binary patch literal 5310 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 literal 0 HcmV?d00001 diff --git a/targets/ios/Icon-83.5@2x.png b/targets/ios/Icon-83.5@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..f105f10e1c1af146af252002168bc1ccbe4922b9 GIT binary patch literal 5466 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 literal 0 HcmV?d00001 diff --git a/targets/ios/Icon-Small.png b/targets/ios/Icon-Small.png new file mode 100644 index 0000000000000000000000000000000000000000..826b6e7fb7cc74d83904a1025c76278ce0e2192b GIT binary patch literal 1503 zcmZ{keKgYx7{`CK%xe~6dAahEn@0Wi5^bftY%*_A=F;8Rw52x$58W0FFBVKuS6Q%&DW4VE{M^0)T=F060DXn8gYn z1aDOzw4(fW;DMzdbM>JxU!Ca0`h>FpK+pJxG=O3OTpdE$!~g+9|AEk` zXB0KC?wB53>oxp^<-=%=+1B0RTiq#CwGJG|M_;4(}$xq2sKVC%(jt zDLWGm$*VZX2>M|LB5aVpViQWoJGs8Hz6N4!fSfV%EVHJs;a{a{Iy5!Hr_zFL={BG- z!Cq@E$bQWC*SbRp?H03D4@Y9gutO7RHWv4>?c?=B(%IN=txK1`^<$yJC(-TB>*5jZ zzYb@+PIA|1&nV1cp9LBVu7$9?w!yj{X^}qz^wz#m5gzJRSN4Lo3;?K{4|mTY>x_2( zwB`MY{_#>aE(fLwkfiGc36kD=Y~ou#i)1mpuY|QcRi#>-p5C%$d-u#;No@$qQM7M$ z7D0Ywb|Gr>3ShBmE`V8elP!UG+}9~&1v&;ap?`Zc<5+}Qh-tMZcdcQZ>YbR|tADP{ z{rIcQ2+E@a%@U0;V5WDb*P+Ygj(4oddFxyls)DQ2;9XXT9f*+ZB5eq6IbF~7 zci2cR_kqwLDez3J^i9SlMZ5df!sj#ypX?QI%aFVO>os||w7#)p&rV9g3#?w|8A|Ef zv9!|C-NIhpGQfN0d}Umed-I*q%5Bm8DEKELCG~lY zS1W_8k$YLz;MhPZlO-uRESas=&2mi5)<>@giNAlJo_@5@(mg+R$jUm29Fbn>k=lW( zwm0G+AApBd`U;#EOOTswWSixg+QB6!JdN5}Esj4W|EN&*4lr^JEZ&E;58I{t3%+kG zf#FxPFSU;F{2Kc=h>`B5;n@}OXi?kAE6#Zrwry)Se#MzHbrCa@pV0HnW=@z^udcGX z^!XT%^%A~A+e|l}YeOPo@_E9iMb)cnJkFq>zsz5Ww@-|DoYjA^vjW;ZIw(ND{K))v z?8Wkw)l#8M_TgfSLR=8Yxg}fAH?eH%zz`kR+Fy2{cwFzHy?bzS3Uf0P8H&-z5Oe!_FhB$hy{a$XJPqM0rbUvY0Y`*!~9ei^R-;tbtVV!Q7 zy{^$D)Tw}Ru$h~B>74{Y9w>2T#$sFjd@=b>4ZL64Awy?in!IQIHOj>fbrOVAFoU&bxY)Yr5gPvzu pC;K#nZ=nG7Yvi83k)`SPv&IaV$2#NX2ov?E1Biq`e3d8l^k0FZrL_P6 literal 0 HcmV?d00001 diff --git a/targets/ios/Icon-Small@2x.png b/targets/ios/Icon-Small@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..f040f104627d9bf2092f30a4741a8bf9e384bce5 GIT binary patch literal 3124 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 DNRuyb literal 0 HcmV?d00001 diff --git a/targets/ios/Info.plist b/targets/ios/Info.plist new file mode 100644 index 000000000..ddcebecc5 --- /dev/null +++ b/targets/ios/Info.plist @@ -0,0 +1,56 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleDisplayName + Butterscotch + CFBundleExecutable + Butterscotch + CFBundleIdentifier + com.mrpowergamerbr.butterscotch.ios + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + Butterscotch + CFBundlePackageType + APPL + CFBundleSignature + ???? + CFBundleVersion + 0.7.6 + CFBundleShortVersionString + 0.7.6 + CFBundleSupportedPlatforms + + iPhoneOS + + LSRequiresIPhoneOS + + MinimumOSVersion + 2.0 + UIDeviceFamily + + 1 + 2 + + UIStatusBarHidden + + CFBundleIconFiles + + Icon.png + Icon@2x.png + Icon-72.png + Icon-72@2x.png + Icon-Small-50.png + Icon-Small-50@2x.png + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + + diff --git a/targets/ios/iTunesArtwork b/targets/ios/iTunesArtwork new file mode 100644 index 0000000000000000000000000000000000000000..82acdc50f7fb1a62883b9dbbbc47b10f67ba588b GIT binary patch literal 1059 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 literal 0 HcmV?d00001 From b3f999dd6ab5b4ad0a4610c3145bac834d265cce Mon Sep 17 00:00:00 2001 From: Un1q32 Date: Mon, 6 Jul 2026 16:56:45 -0400 Subject: [PATCH 040/109] add entitlements plist --- targets/ios/butterscotch.entitlements | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 targets/ios/butterscotch.entitlements diff --git a/targets/ios/butterscotch.entitlements b/targets/ios/butterscotch.entitlements new file mode 100644 index 000000000..42cfb9cdc --- /dev/null +++ b/targets/ios/butterscotch.entitlements @@ -0,0 +1,8 @@ + + + + + get-task-allow + + + From ebbfaf9377f2d81a31766bdd3ab7507b52402bca Mon Sep 17 00:00:00 2001 From: Un1q32 Date: Mon, 6 Jul 2026 16:58:33 -0400 Subject: [PATCH 041/109] reorder --- targets/ios/Icon-60@2x.png | Bin 5009 -> 0 bytes targets/ios/{ => assets}/Default-568h@2x.png | 0 targets/ios/{ => assets}/Default-667h@2x.png | 0 targets/ios/{ => assets}/Default-736h@3x.png | 0 targets/ios/{ => assets}/Default.png | Bin targets/ios/{ => assets}/Default@2x.png | 0 targets/ios/{ => assets}/Icon-20.png | Bin targets/ios/{ => assets}/Icon-20@2x.png | Bin targets/ios/{ => assets}/Icon-20@3x.png | Bin targets/ios/{ => assets}/Icon-40.png | Bin targets/ios/{ => assets}/Icon-40@2x.png | Bin targets/ios/{ => assets}/Icon-40@3x.png | Bin targets/ios/assets/Icon-60@2x.png | 1 + targets/ios/{ => assets}/Icon-60@3x.png | Bin targets/ios/{ => assets}/Icon-72.png | Bin targets/ios/{ => assets}/Icon-72@2x.png | Bin targets/ios/{ => assets}/Icon-76.png | Bin targets/ios/{ => assets}/Icon-76@2x.png | Bin targets/ios/{ => assets}/Icon-83.5@2x.png | Bin targets/ios/{ => assets}/Icon-Small.png | Bin targets/ios/{ => assets}/Icon-Small@2x.png | Bin targets/ios/{ => assets}/Icon-Small@3x.png | Bin targets/ios/{ => assets}/Icon.png | Bin targets/ios/{ => assets}/Icon@2x.png | Bin targets/ios/{ => assets}/Info.plist | 0 targets/ios/{ => assets}/iTunesArtwork | Bin targets/ios/{ => assets}/iTunesArtwork@2x | Bin 27 files changed, 1 insertion(+) delete mode 100644 targets/ios/Icon-60@2x.png rename targets/ios/{ => assets}/Default-568h@2x.png (100%) rename targets/ios/{ => assets}/Default-667h@2x.png (100%) rename targets/ios/{ => assets}/Default-736h@3x.png (100%) rename targets/ios/{ => assets}/Default.png (100%) rename targets/ios/{ => assets}/Default@2x.png (100%) rename targets/ios/{ => assets}/Icon-20.png (100%) rename targets/ios/{ => assets}/Icon-20@2x.png (100%) rename targets/ios/{ => assets}/Icon-20@3x.png (100%) rename targets/ios/{ => assets}/Icon-40.png (100%) rename targets/ios/{ => assets}/Icon-40@2x.png (100%) rename targets/ios/{ => assets}/Icon-40@3x.png (100%) create mode 120000 targets/ios/assets/Icon-60@2x.png rename targets/ios/{ => assets}/Icon-60@3x.png (100%) rename targets/ios/{ => assets}/Icon-72.png (100%) rename targets/ios/{ => assets}/Icon-72@2x.png (100%) rename targets/ios/{ => assets}/Icon-76.png (100%) rename targets/ios/{ => assets}/Icon-76@2x.png (100%) rename targets/ios/{ => assets}/Icon-83.5@2x.png (100%) rename targets/ios/{ => assets}/Icon-Small.png (100%) rename targets/ios/{ => assets}/Icon-Small@2x.png (100%) rename targets/ios/{ => assets}/Icon-Small@3x.png (100%) rename targets/ios/{ => assets}/Icon.png (100%) rename targets/ios/{ => assets}/Icon@2x.png (100%) rename targets/ios/{ => assets}/Info.plist (100%) rename targets/ios/{ => assets}/iTunesArtwork (100%) rename targets/ios/{ => assets}/iTunesArtwork@2x (100%) diff --git a/targets/ios/Icon-60@2x.png b/targets/ios/Icon-60@2x.png deleted file mode 100644 index df60e45a2c1676a57d244cfa6b55d05000045524..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5009 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

10s@R2PeFYIVvg{9wj z)vs1hLadwNKru57_M~*th(BT9g0Yr9y(YAsz0tyU%zLK40_jbKVnaLzX?T=PS4MsJQZon{P43_TFRHKCT6@pHli?o7{Q2b{kJPttvzhsa8!c?Z{9O5+ zN<{Rg{UAz7B}ypfsqAmFfJX%!35N~C1Z|4hrc+=(UYdp?+e|6_0BMyzHb+`KW#TB& zP#HMI=me5)AF(G!`l}~fK5K&RtL+$t{j~WF)l>QJLUpzxr@B4MJ}2hle3-@U?yD4B4!qML^XXPDSpi)((0rwde*F0 z&6d_d_}C2xPBSGR%+d4Nm*Ty1F&mGxo|1!k#Yq3mM)=Et#hDb71lUkLZq#V}p(xpy zV9z^l~!~BUVZ-tkVK7H|TEa*1EYgMYR>f-2CM%r5N zg^Dnx++8p&Lz8@#;>WFLeocCBPa7-x+49m-{c8)0#TeGU$5Pasa|-)BDF#)(|J6LF zU?!Vbhl&DEk;CM1wV}?_-R0BI+Xj|WCq?VtzxJ`{w;-CBv%s)6H8OU1z(@D+N~15Y zT)Xs!mwvs4N5rrF{)sO;q8cO>DRR70f0Lo^`m#N`A@BM)gKNPnjP9uIvdfqMAY1Hn zS5WB|Jcmi+my4?ii@Tbi6+hemdBoEo4H9x)5g6;Q8j;ZcvQ-G?XCZty0GW5RyAa7-u diff --git a/targets/ios/Default-568h@2x.png b/targets/ios/assets/Default-568h@2x.png similarity index 100% rename from targets/ios/Default-568h@2x.png rename to targets/ios/assets/Default-568h@2x.png diff --git a/targets/ios/Default-667h@2x.png b/targets/ios/assets/Default-667h@2x.png similarity index 100% rename from targets/ios/Default-667h@2x.png rename to targets/ios/assets/Default-667h@2x.png diff --git a/targets/ios/Default-736h@3x.png b/targets/ios/assets/Default-736h@3x.png similarity index 100% rename from targets/ios/Default-736h@3x.png rename to targets/ios/assets/Default-736h@3x.png diff --git a/targets/ios/Default.png b/targets/ios/assets/Default.png similarity index 100% rename from targets/ios/Default.png rename to targets/ios/assets/Default.png diff --git a/targets/ios/Default@2x.png b/targets/ios/assets/Default@2x.png similarity index 100% rename from targets/ios/Default@2x.png rename to targets/ios/assets/Default@2x.png diff --git a/targets/ios/Icon-20.png b/targets/ios/assets/Icon-20.png similarity index 100% rename from targets/ios/Icon-20.png rename to targets/ios/assets/Icon-20.png diff --git a/targets/ios/Icon-20@2x.png b/targets/ios/assets/Icon-20@2x.png similarity index 100% rename from targets/ios/Icon-20@2x.png rename to targets/ios/assets/Icon-20@2x.png diff --git a/targets/ios/Icon-20@3x.png b/targets/ios/assets/Icon-20@3x.png similarity index 100% rename from targets/ios/Icon-20@3x.png rename to targets/ios/assets/Icon-20@3x.png diff --git a/targets/ios/Icon-40.png b/targets/ios/assets/Icon-40.png similarity index 100% rename from targets/ios/Icon-40.png rename to targets/ios/assets/Icon-40.png diff --git a/targets/ios/Icon-40@2x.png b/targets/ios/assets/Icon-40@2x.png similarity index 100% rename from targets/ios/Icon-40@2x.png rename to targets/ios/assets/Icon-40@2x.png diff --git a/targets/ios/Icon-40@3x.png b/targets/ios/assets/Icon-40@3x.png similarity index 100% rename from targets/ios/Icon-40@3x.png rename to targets/ios/assets/Icon-40@3x.png diff --git a/targets/ios/assets/Icon-60@2x.png b/targets/ios/assets/Icon-60@2x.png new file mode 120000 index 000000000..88f3dd0ff --- /dev/null +++ b/targets/ios/assets/Icon-60@2x.png @@ -0,0 +1 @@ +Icon-40@3x.png \ No newline at end of file diff --git a/targets/ios/Icon-60@3x.png b/targets/ios/assets/Icon-60@3x.png similarity index 100% rename from targets/ios/Icon-60@3x.png rename to targets/ios/assets/Icon-60@3x.png diff --git a/targets/ios/Icon-72.png b/targets/ios/assets/Icon-72.png similarity index 100% rename from targets/ios/Icon-72.png rename to targets/ios/assets/Icon-72.png diff --git a/targets/ios/Icon-72@2x.png b/targets/ios/assets/Icon-72@2x.png similarity index 100% rename from targets/ios/Icon-72@2x.png rename to targets/ios/assets/Icon-72@2x.png diff --git a/targets/ios/Icon-76.png b/targets/ios/assets/Icon-76.png similarity index 100% rename from targets/ios/Icon-76.png rename to targets/ios/assets/Icon-76.png diff --git a/targets/ios/Icon-76@2x.png b/targets/ios/assets/Icon-76@2x.png similarity index 100% rename from targets/ios/Icon-76@2x.png rename to targets/ios/assets/Icon-76@2x.png diff --git a/targets/ios/Icon-83.5@2x.png b/targets/ios/assets/Icon-83.5@2x.png similarity index 100% rename from targets/ios/Icon-83.5@2x.png rename to targets/ios/assets/Icon-83.5@2x.png diff --git a/targets/ios/Icon-Small.png b/targets/ios/assets/Icon-Small.png similarity index 100% rename from targets/ios/Icon-Small.png rename to targets/ios/assets/Icon-Small.png diff --git a/targets/ios/Icon-Small@2x.png b/targets/ios/assets/Icon-Small@2x.png similarity index 100% rename from targets/ios/Icon-Small@2x.png rename to targets/ios/assets/Icon-Small@2x.png diff --git a/targets/ios/Icon-Small@3x.png b/targets/ios/assets/Icon-Small@3x.png similarity index 100% rename from targets/ios/Icon-Small@3x.png rename to targets/ios/assets/Icon-Small@3x.png diff --git a/targets/ios/Icon.png b/targets/ios/assets/Icon.png similarity index 100% rename from targets/ios/Icon.png rename to targets/ios/assets/Icon.png diff --git a/targets/ios/Icon@2x.png b/targets/ios/assets/Icon@2x.png similarity index 100% rename from targets/ios/Icon@2x.png rename to targets/ios/assets/Icon@2x.png diff --git a/targets/ios/Info.plist b/targets/ios/assets/Info.plist similarity index 100% rename from targets/ios/Info.plist rename to targets/ios/assets/Info.plist diff --git a/targets/ios/iTunesArtwork b/targets/ios/assets/iTunesArtwork similarity index 100% rename from targets/ios/iTunesArtwork rename to targets/ios/assets/iTunesArtwork diff --git a/targets/ios/iTunesArtwork@2x b/targets/ios/assets/iTunesArtwork@2x similarity index 100% rename from targets/ios/iTunesArtwork@2x rename to targets/ios/assets/iTunesArtwork@2x From 9f1dff13f6d700347d3018c7247dd1d07e9a8230 Mon Sep 17 00:00:00 2001 From: Un1q32 Date: Mon, 6 Jul 2026 17:22:47 -0400 Subject: [PATCH 042/109] ipa build --- Makefile | 1 + targets/ios/assets/Info.plist | 2 +- targets/ios/build-ipa.sh | 26 ++++++++++++++++++++++++++ 3 files changed, 28 insertions(+), 1 deletion(-) create mode 100755 targets/ios/build-ipa.sh diff --git a/Makefile b/Makefile index 44fdf8de6..10022c353 100644 --- a/Makefile +++ b/Makefile @@ -219,6 +219,7 @@ build/butterscotch: $(OBJS) @{ [ -z "$(NO_COLOR)" ] && [ -t 1 ]; } && printf " \033[1;34mLD\033[0m butterscotch\n" || printf " LD butterscotch\n" $(V)$(_CC) $(LDFLAGS) $(OBJS) $(LIBS) $(EXTRALIBS) $(OUTPUT_EXE)$@ @[ -f $@.exe ] && chmod +x $@.exe || true + @if [ "$(OS)" = 'iOS' ]; then targets/ios/build-ipa.sh; fi build/%.c.$(OBJ_EXT): %.c compat/config.mk $(if $(DISABLE_MMD),$(HEADERS)) @mkdir -p $(dir $@) diff --git a/targets/ios/assets/Info.plist b/targets/ios/assets/Info.plist index ddcebecc5..a5618daed 100644 --- a/targets/ios/assets/Info.plist +++ b/targets/ios/assets/Info.plist @@ -13,7 +13,7 @@ CFBundleInfoDictionaryVersion 6.0 CFBundleName - Butterscotch + butterscotch CFBundlePackageType APPL CFBundleSignature diff --git a/targets/ios/build-ipa.sh b/targets/ios/build-ipa.sh new file mode 100755 index 000000000..e1569115d --- /dev/null +++ b/targets/ios/build-ipa.sh @@ -0,0 +1,26 @@ +#!/bin/sh +set -e + +apppath="build/Payload/butterscotch.app" + +if ! [ -f build/butterscotch ]; then + printf 'Expected a binary at build/butterscotch\n' + exit 1 +fi + +printf 'Creating butterscotch.ipa\n' + +rm -rf "$apppath" +mkdir -p "$apppath" +if command -v ldid >/dev/null; then + ldid -Stargets/ios/butterscotch.entitlements build/butterscotch +else + codesign -f -s - --entitlements targets/ios/butterscotch.entitlements build/butterscotch +fi +cp build/butterscotch "$apppath/butterscotch" +cp -a targets/ios/assets/* "$apppath" +command -v plistutil >/dev/null && plistutil -i targets/ios/assets/Info.plist -o "$apppath/Info.plist" -f bin + +cd build +rm -f butterscotch.ipa +zip -qr butterscotch.ipa Payload From 028da9360601290c8936a5244bbac853924efb15 Mon Sep 17 00:00:00 2001 From: Un1q32 Date: Mon, 6 Jul 2026 17:26:40 -0400 Subject: [PATCH 043/109] fix --- targets/ios/assets/Info.plist | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/targets/ios/assets/Info.plist b/targets/ios/assets/Info.plist index a5618daed..db2fdad93 100644 --- a/targets/ios/assets/Info.plist +++ b/targets/ios/assets/Info.plist @@ -7,13 +7,13 @@ CFBundleDisplayName Butterscotch CFBundleExecutable - Butterscotch + butterscotch CFBundleIdentifier com.mrpowergamerbr.butterscotch.ios CFBundleInfoDictionaryVersion 6.0 CFBundleName - butterscotch + Butterscotch CFBundlePackageType APPL CFBundleSignature From cc58f3491b4d8aa6fcd5e1b09bf9fe433a83c6c0 Mon Sep 17 00:00:00 2001 From: Un1q32 Date: Mon, 6 Jul 2026 17:46:54 -0400 Subject: [PATCH 044/109] format --- src/desktop/backends/ios.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/desktop/backends/ios.c b/src/desktop/backends/ios.c index a3c23259d..62d665de5 100755 --- a/src/desktop/backends/ios.c +++ b/src/desktop/backends/ios.c @@ -1166,8 +1166,8 @@ extern int game_main(int argc, char *argv[]); /* Device rotated so its left edge points "up" corresponds to * interface orientation LandscapeRight, i.e. a +90 degree * compensating rotation; the other case is the mirror image. */ - case UIDeviceOrientationLandscapeLeft: angle = (CGFloat)M_PI_2; swapped = YES; break; - case UIDeviceOrientationLandscapeRight: angle = -(CGFloat)M_PI_2; swapped = YES; break; + case UIDeviceOrientationLandscapeLeft: angle = (CGFloat)M_PI_2; swapped = YES; break; + case UIDeviceOrientationLandscapeRight: angle = -(CGFloat)M_PI_2; swapped = YES; break; default: /* FaceUp / FaceDown / Unknown: not a usable orientation -- * keep whatever we last applied. */ From 76cc14fc66049a4ce3a3c27b48f185547932a1bf Mon Sep 17 00:00:00 2001 From: Un1q32 Date: Mon, 6 Jul 2026 17:52:37 -0400 Subject: [PATCH 045/109] fix shit --- Makefile | 2 +- targets/ios/assets/Info.plist | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index 10022c353..abbd6e8b5 100644 --- a/Makefile +++ b/Makefile @@ -215,7 +215,7 @@ compat/config.mk: compat/configure.sh compat/tmp/cc endif -build/butterscotch: $(OBJS) +build/butterscotch: $(OBJS) $(if $(filter iOS,$(OS)),targets/ios/build-ipa.sh $(wildcard targets/ios/assets/*)) @{ [ -z "$(NO_COLOR)" ] && [ -t 1 ]; } && printf " \033[1;34mLD\033[0m butterscotch\n" || printf " LD butterscotch\n" $(V)$(_CC) $(LDFLAGS) $(OBJS) $(LIBS) $(EXTRALIBS) $(OUTPUT_EXE)$@ @[ -f $@.exe ] && chmod +x $@.exe || true diff --git a/targets/ios/assets/Info.plist b/targets/ios/assets/Info.plist index db2fdad93..43de3b081 100644 --- a/targets/ios/assets/Info.plist +++ b/targets/ios/assets/Info.plist @@ -9,7 +9,7 @@ CFBundleExecutable butterscotch CFBundleIdentifier - com.mrpowergamerbr.butterscotch.ios + com.mrpowergamerbr.butterscotch CFBundleInfoDictionaryVersion 6.0 CFBundleName @@ -37,6 +37,8 @@ UIStatusBarHidden + UIViewControllerBasedStatusBarAppearance + CFBundleIconFiles Icon.png From 63e73c2e35ec72ceb26d0038ec8d7a5f91a197b1 Mon Sep 17 00:00:00 2001 From: Un1q32 Date: Mon, 6 Jul 2026 19:59:54 -0400 Subject: [PATCH 046/109] fix --- src/desktop/backends/ios.c | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/src/desktop/backends/ios.c b/src/desktop/backends/ios.c index 62d665de5..42ccd4297 100755 --- a/src/desktop/backends/ios.c +++ b/src/desktop/backends/ios.c @@ -731,15 +731,12 @@ static void drawCenteredLabel(NSString *text, CGRect rect, UIFont *font) { @implementation BSViewController -/* Rotation is now handled manually (see AppDelegate applyDeviceOrientation: - * below) instead of via UIKit's autorotation machinery, whose trigger - * mechanism and defaults have shifted across the iOS 2-10 / SDK 2-26 - * matrix this targets. Returning NO everywhere below means the OS never - * tries to rotate the view controller's view itself, on any version -- - * we lock to Portrait and do all the rotating ourselves. */ +/* + * Apple keeps changing how you're supposed to tell UIKit to handle shit, + * so we just pretend we're portrait only and then rotate manually. + */ - (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation { - (void)interfaceOrientation; - return NO; + return interfaceOrientation == UIInterfaceOrientationPortrait; } /* iOS 6+ query this pair instead of the method above. NSUInteger (rather From 0ef72d20a1747f8a3b19e2146cf19fde880dd0fc Mon Sep 17 00:00:00 2001 From: Un1q32 Date: Mon, 6 Jul 2026 23:16:54 -0400 Subject: [PATCH 047/109] fix warning --- src/desktop/backends/ios.c | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/desktop/backends/ios.c b/src/desktop/backends/ios.c index 42ccd4297..eedce3457 100755 --- a/src/desktop/backends/ios.c +++ b/src/desktop/backends/ios.c @@ -13,6 +13,9 @@ #include #include +/* Undefine macros that conflict with glad headers */ +#undef GL_UNSIGNED_SHORT_1_5_5_5_REV + #include #include "common.h" From 1edbf4d3632263e3d522a4f88bdb71dbfb70f13f Mon Sep 17 00:00:00 2001 From: Un1q32 Date: Tue, 7 Jul 2026 00:02:47 -0400 Subject: [PATCH 048/109] ability to delete games from the app --- src/desktop/backends/ios.c | 68 +++++++++++++++++++++++++++++++++++++- 1 file changed, 67 insertions(+), 1 deletion(-) diff --git a/src/desktop/backends/ios.c b/src/desktop/backends/ios.c index eedce3457..596afaefd 100755 --- a/src/desktop/backends/ios.c +++ b/src/desktop/backends/ios.c @@ -936,8 +936,9 @@ static UIKeyboardType bsNumericKeyboardType(void) { * app is running (or after returning from a game) show up. * ------------------------------------------------------------------- */ -@interface BSGameListViewController : UITableViewController { +@interface BSGameListViewController : UITableViewController { NSMutableArray *games; + NSIndexPath *pendingDeleteIndexPath; } - (void)reloadGames; @end @@ -1014,6 +1015,70 @@ static UIKeyboardType bsNumericKeyboardType(void) { return cell; } +/* Enables the standard swipe-left-to-delete gesture for every row; UIKit + * reveals its built-in red "Delete" button automatically once this + * returns YES, no custom gesture recognizer needed. */ +- (BOOL)tableView:(UITableView *)tableView canEditRowAtIndexPath:(NSIndexPath *)indexPath { + (void)tableView; (void)indexPath; + return YES; +} + +/* Called when the user actually taps the revealed Delete button. We don't + * touch the filesystem here -- removing a game folder also wipes any save + * data living alongside data.win, so we confirm first via UIAlertView + * (rather than UIAlertController, which needs iOS 8+) and do the real + * removal from the alert's delegate callback below. */ +- (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle + forRowAtIndexPath:(NSIndexPath *)indexPath { + (void)tableView; + if (editingStyle != UITableViewCellEditingStyleDelete) return; + + NSString *name = [games objectAtIndex:indexPath.row]; + + [pendingDeleteIndexPath release]; + pendingDeleteIndexPath = [indexPath retain]; + + NSString *msg = [NSString stringWithFormat: + @"Delete \"%@\" and all of its save data? This cannot be undone.", name]; + UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"Delete Game" + message:msg + delegate:self + cancelButtonTitle:@"Cancel" + otherButtonTitles:@"Delete", nil]; + [alert show]; + [alert release]; +} + +- (void)alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex { + NSIndexPath *indexPath = pendingDeleteIndexPath; + pendingDeleteIndexPath = nil; + + if (!indexPath) return; + if (buttonIndex == alertView.cancelButtonIndex) { + [indexPath release]; + return; + } + + NSString *name = [games objectAtIndex:indexPath.row]; + NSString *dir = [BS_GAMES_ROOT_PATH stringByAppendingPathComponent:name]; + + /* removeItemAtPath: recursively removes directory contents, so this + * takes data.win and any save files sitting next to it in one shot. */ + NSError *error = nil; + if ([[NSFileManager defaultManager] removeItemAtPath:dir error:&error]) { + [games removeObjectAtIndex:indexPath.row]; + [self.tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] + withRowAnimation:UITableViewRowAnimationFade]; + } else { + NSLog(@"Butterscotch: failed to delete game directory %@: %@", dir, error); + /* Directory listing may now be stale (partial delete, permissions + * error, etc) -- resync from disk rather than trust our cache. */ + [self reloadGames]; + } + + [indexPath release]; +} + - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath { [tableView deselectRowAtIndexPath:indexPath animated:YES]; NSString *folder = [games objectAtIndex:indexPath.row]; @@ -1022,6 +1087,7 @@ static UIKeyboardType bsNumericKeyboardType(void) { } - (void)dealloc { + [pendingDeleteIndexPath release]; [games release]; [super dealloc]; } From eee8869df70d6a626ab5fe5629f5a526a98b7a03 Mon Sep 17 00:00:00 2001 From: Un1q32 Date: Tue, 7 Jul 2026 04:20:48 -0400 Subject: [PATCH 049/109] software --- src/desktop/backends/ios.c | 72 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 72 insertions(+) diff --git a/src/desktop/backends/ios.c b/src/desktop/backends/ios.c index 596afaefd..ff9b60d29 100755 --- a/src/desktop/backends/ios.c +++ b/src/desktop/backends/ios.c @@ -45,6 +45,11 @@ static GLint fbWidth = 0; static GLint fbHeight = 0; static CAEAGLLayer *layer; +#ifdef ENABLE_SW_RENDERER +static uint32_t* nextFb = NULL; +static GLuint swTexture = 0; +#endif + /* Requested render resolution, as set via platformSetWindowSize() (and * seeded from platformInit()'s reqW/reqH before that's ever called). Used * to pick a CAEAGLLayer contentsScale that renders at roughly this pixel @@ -275,6 +280,10 @@ bool platformInit(int32_t reqW, int32_t reqH, const char *title, bool headless) if (!glcontext) glcontext = [[EAGLContext alloc] initWithAPI:2]; #endif +#ifdef ENABLE_SW_RENDERER + if (gfx == SOFTWARE) + glcontext = [[EAGLContext alloc] initWithAPI:1]; +#endif if (!glcontext) { fprintf(stderr, "Failed to create an OpenGLES context\n"); @@ -294,6 +303,9 @@ bool platformInit(int32_t reqW, int32_t reqH, const char *title, bool headless) void platformExit(void) { if (framebuffer) glDeleteFramebuffers(1, &framebuffer); if (renderbuffer) glDeleteRenderbuffers(1, &renderbuffer); +#ifdef ENABLE_SW_RENDERER + if (swTexture) glDeleteTextures(1, &swTexture); +#endif [glcontext release]; glcontext = nil; framebuffer = 0; @@ -392,6 +404,20 @@ static void applyRenderScale(void) { } static void resizeFramebuffer(void) { +#ifdef ENABLE_SW_RENDERER + if (gfx == SOFTWARE) { + applyRenderScale(); + + glBindRenderbuffer(GL_RENDERBUFFER, renderbuffer); + [glcontext renderbufferStorage:GL_RENDERBUFFER fromDrawable:layer]; + glGetRenderbufferParameteriv(GL_RENDERBUFFER, GL_RENDERBUFFER_WIDTH, &fbWidth); + glGetRenderbufferParameteriv(GL_RENDERBUFFER, GL_RENDERBUFFER_HEIGHT, &fbHeight); + + glViewport(0, 0, fbWidth, fbHeight); + return; + } +#endif + applyRenderScale(); glBindFramebuffer(GL_FRAMEBUFFER, framebuffer); @@ -419,6 +445,10 @@ void platformInitFunctions(Runner *runner) { if (!glInited) { glGenFramebuffers(1, &framebuffer); glGenRenderbuffers(1, &renderbuffer); +#ifdef ENABLE_SW_RENDERER + if (gfx == SOFTWARE) + glGenTextures(1, &swTexture); +#endif glInited = true; } @@ -426,7 +456,49 @@ void platformInitFunctions(Runner *runner) { atomic_store(&needsResize, false); } +#ifdef ENABLE_SW_RENDERER + +void Runner_setNextFrame(uint32_t* framebuffer, int width, int height) { + nextFb = framebuffer; + fbWidth = width; + fbHeight = height; +} + +#endif + void platformSwapBuffers(void) { +#ifdef ENABLE_SW_RENDERER + if (gfx == SOFTWARE && nextFb) { + glBindFramebuffer(GL_FRAMEBUFFER, 0); + + glBindTexture(GL_TEXTURE_2D, swTexture); + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, fbWidth, fbHeight, 0, GL_BGRA, GL_UNSIGNED_BYTE, nextFb); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP); + + glEnable(GL_TEXTURE_2D); + glDisable(GL_BLEND); + + glMatrixMode(GL_PROJECTION); + glLoadIdentity(); + glOrthof(0, fbWidth, 0, fbHeight, -1, 1); + + glMatrixMode(GL_MODELVIEW); + glLoadIdentity(); + + glColor4f(1, 1, 1, 1); + glBegin(GL_QUADS); + glTexCoord2f(0, 0); glVertex2f(0, 0); + glTexCoord2f(1, 0); glVertex2f(fbWidth, 0); + glTexCoord2f(1, 1); glVertex2f(fbWidth, fbHeight); + glTexCoord2f(0, 1); glVertex2f(0, fbHeight); + glEnd(); + + nextFb = NULL; + } +#endif [glcontext presentRenderbuffer:GL_RENDERBUFFER]; } From c88d5e8e2f26fa345a32b59ab893d40f21617a1b Mon Sep 17 00:00:00 2001 From: Un1q32 Date: Tue, 7 Jul 2026 04:29:45 -0400 Subject: [PATCH 050/109] glad --- vendor/glad/include/glad/glad.h | 237 +++++++++++++++++++++++- vendor/glad/src/glad.c | 315 ++++++++++++++++++++++++++++++-- 2 files changed, 537 insertions(+), 15 deletions(-) diff --git a/vendor/glad/include/glad/glad.h b/vendor/glad/include/glad/glad.h index 01d618327..e7a5a55df 100644 --- a/vendor/glad/include/glad/glad.h +++ b/vendor/glad/include/glad/glad.h @@ -1,10 +1,10 @@ /* - OpenGL, OpenGL ES loader generated by glad 0.1.36 on Thu Jul 2 22:44:49 2026. + OpenGL, OpenGL ES loader generated by glad 0.1.36 on Tue Jul 7 08:27:13 2026. Language/Generator: C/C++ Specification: gl - APIs: gl=4.1, gles2=3.0 + APIs: gl=4.1, gles1=1.0, gles2=3.0 Profile: compatibility Extensions: GL_ARB_debug_output, @@ -13,6 +13,7 @@ GL_EXT_framebuffer_blit, GL_EXT_framebuffer_object, GL_KHR_debug, + GL_OES_framebuffer_object, GL_OES_vertex_array_object Loader: True Local files: False @@ -20,9 +21,9 @@ Reproducible: False Commandline: - --profile="compatibility" --api="gl=4.1,gles2=3.0" --generator="c" --spec="gl" --extensions="GL_ARB_debug_output,GL_ARB_framebuffer_object,GL_ARB_vertex_array_object,GL_EXT_framebuffer_blit,GL_EXT_framebuffer_object,GL_KHR_debug,GL_OES_vertex_array_object" + --profile="compatibility" --api="gl=4.1,gles1=1.0,gles2=3.0" --generator="c" --spec="gl" --extensions="GL_ARB_debug_output,GL_ARB_framebuffer_object,GL_ARB_vertex_array_object,GL_EXT_framebuffer_blit,GL_EXT_framebuffer_object,GL_KHR_debug,GL_OES_framebuffer_object,GL_OES_vertex_array_object" Online: - https://glad.dav1d.de/#profile=compatibility&language=c&specification=gl&loader=on&api=gl%3D4.1&api=gles2%3D3.0&extensions=GL_ARB_debug_output&extensions=GL_ARB_framebuffer_object&extensions=GL_ARB_vertex_array_object&extensions=GL_EXT_framebuffer_blit&extensions=GL_EXT_framebuffer_object&extensions=GL_KHR_debug&extensions=GL_OES_vertex_array_object + https://glad.dav1d.de/#profile=compatibility&language=c&specification=gl&loader=on&api=gl%3D4.1&api=gles1%3D1.0&api=gles2%3D3.0&extensions=GL_ARB_debug_output&extensions=GL_ARB_framebuffer_object&extensions=GL_ARB_vertex_array_object&extensions=GL_EXT_framebuffer_blit&extensions=GL_EXT_framebuffer_object&extensions=GL_KHR_debug&extensions=GL_OES_framebuffer_object&extensions=GL_OES_vertex_array_object */ @@ -102,6 +103,8 @@ GLAPI int gladLoadGL(void); GLAPI int gladLoadGLLoader(GLADloadproc); +GLAPI int gladLoadGLES1Loader(GLADloadproc); + GLAPI int gladLoadGLES2Loader(GLADloadproc); #include @@ -1513,6 +1516,9 @@ typedef void (APIENTRY *GLVULKANPROCNV)(void); #define GL_LAYER_PROVOKING_VERTEX 0x825E #define GL_VIEWPORT_INDEX_PROVOKING_VERTEX 0x825F #define GL_UNDEFINED_VERTEX 0x8260 +#define GL_VERSION_ES_CL_1_0 1 +#define GL_VERSION_ES_CM_1_1 1 +#define GL_VERSION_ES_CL_1_1 1 #define GL_FRAMEBUFFER_INCOMPLETE_DIMENSIONS 0x8CD9 #define GL_PRIMITIVE_RESTART_FIXED_INDEX 0x8D69 #define GL_COPY_READ_BUFFER_BINDING 0x8F36 @@ -4164,6 +4170,139 @@ typedef void (APIENTRYP PFNGLGETDOUBLEI_VPROC)(GLenum target, GLuint index, GLdo GLAPI PFNGLGETDOUBLEI_VPROC glad_glGetDoublei_v; #define glGetDoublei_v glad_glGetDoublei_v #endif +#ifndef GL_VERSION_ES_CM_1_0 +#define GL_VERSION_ES_CM_1_0 1 +GLAPI int GLAD_GL_VERSION_ES_CM_1_0; +typedef void (APIENTRYP PFNGLCLIPPLANEFPROC)(GLenum p, const GLfloat *eqn); +GLAPI PFNGLCLIPPLANEFPROC glad_glClipPlanef; +#define glClipPlanef glad_glClipPlanef +typedef void (APIENTRYP PFNGLFRUSTUMFPROC)(GLfloat l, GLfloat r, GLfloat b, GLfloat t, GLfloat n, GLfloat f); +GLAPI PFNGLFRUSTUMFPROC glad_glFrustumf; +#define glFrustumf glad_glFrustumf +typedef void (APIENTRYP PFNGLGETCLIPPLANEFPROC)(GLenum plane, GLfloat *equation); +GLAPI PFNGLGETCLIPPLANEFPROC glad_glGetClipPlanef; +#define glGetClipPlanef glad_glGetClipPlanef +typedef void (APIENTRYP PFNGLORTHOFPROC)(GLfloat l, GLfloat r, GLfloat b, GLfloat t, GLfloat n, GLfloat f); +GLAPI PFNGLORTHOFPROC glad_glOrthof; +#define glOrthof glad_glOrthof +typedef void (APIENTRYP PFNGLALPHAFUNCXPROC)(GLenum func, GLfixed ref); +GLAPI PFNGLALPHAFUNCXPROC glad_glAlphaFuncx; +#define glAlphaFuncx glad_glAlphaFuncx +typedef void (APIENTRYP PFNGLCLEARCOLORXPROC)(GLfixed red, GLfixed green, GLfixed blue, GLfixed alpha); +GLAPI PFNGLCLEARCOLORXPROC glad_glClearColorx; +#define glClearColorx glad_glClearColorx +typedef void (APIENTRYP PFNGLCLEARDEPTHXPROC)(GLfixed depth); +GLAPI PFNGLCLEARDEPTHXPROC glad_glClearDepthx; +#define glClearDepthx glad_glClearDepthx +typedef void (APIENTRYP PFNGLCLIPPLANEXPROC)(GLenum plane, const GLfixed *equation); +GLAPI PFNGLCLIPPLANEXPROC glad_glClipPlanex; +#define glClipPlanex glad_glClipPlanex +typedef void (APIENTRYP PFNGLCOLOR4XPROC)(GLfixed red, GLfixed green, GLfixed blue, GLfixed alpha); +GLAPI PFNGLCOLOR4XPROC glad_glColor4x; +#define glColor4x glad_glColor4x +typedef void (APIENTRYP PFNGLDEPTHRANGEXPROC)(GLfixed n, GLfixed f); +GLAPI PFNGLDEPTHRANGEXPROC glad_glDepthRangex; +#define glDepthRangex glad_glDepthRangex +typedef void (APIENTRYP PFNGLFOGXPROC)(GLenum pname, GLfixed param); +GLAPI PFNGLFOGXPROC glad_glFogx; +#define glFogx glad_glFogx +typedef void (APIENTRYP PFNGLFOGXVPROC)(GLenum pname, const GLfixed *param); +GLAPI PFNGLFOGXVPROC glad_glFogxv; +#define glFogxv glad_glFogxv +typedef void (APIENTRYP PFNGLFRUSTUMXPROC)(GLfixed l, GLfixed r, GLfixed b, GLfixed t, GLfixed n, GLfixed f); +GLAPI PFNGLFRUSTUMXPROC glad_glFrustumx; +#define glFrustumx glad_glFrustumx +typedef void (APIENTRYP PFNGLGETCLIPPLANEXPROC)(GLenum plane, GLfixed *equation); +GLAPI PFNGLGETCLIPPLANEXPROC glad_glGetClipPlanex; +#define glGetClipPlanex glad_glGetClipPlanex +typedef void (APIENTRYP PFNGLGETFIXEDVPROC)(GLenum pname, GLfixed *data); +GLAPI PFNGLGETFIXEDVPROC glad_glGetFixedv; +#define glGetFixedv glad_glGetFixedv +typedef void (APIENTRYP PFNGLGETLIGHTXVPROC)(GLenum light, GLenum pname, GLfixed *params); +GLAPI PFNGLGETLIGHTXVPROC glad_glGetLightxv; +#define glGetLightxv glad_glGetLightxv +typedef void (APIENTRYP PFNGLGETMATERIALXVPROC)(GLenum face, GLenum pname, GLfixed *params); +GLAPI PFNGLGETMATERIALXVPROC glad_glGetMaterialxv; +#define glGetMaterialxv glad_glGetMaterialxv +typedef void (APIENTRYP PFNGLGETTEXENVXVPROC)(GLenum target, GLenum pname, GLfixed *params); +GLAPI PFNGLGETTEXENVXVPROC glad_glGetTexEnvxv; +#define glGetTexEnvxv glad_glGetTexEnvxv +typedef void (APIENTRYP PFNGLGETTEXPARAMETERXVPROC)(GLenum target, GLenum pname, GLfixed *params); +GLAPI PFNGLGETTEXPARAMETERXVPROC glad_glGetTexParameterxv; +#define glGetTexParameterxv glad_glGetTexParameterxv +typedef void (APIENTRYP PFNGLLIGHTMODELXPROC)(GLenum pname, GLfixed param); +GLAPI PFNGLLIGHTMODELXPROC glad_glLightModelx; +#define glLightModelx glad_glLightModelx +typedef void (APIENTRYP PFNGLLIGHTMODELXVPROC)(GLenum pname, const GLfixed *param); +GLAPI PFNGLLIGHTMODELXVPROC glad_glLightModelxv; +#define glLightModelxv glad_glLightModelxv +typedef void (APIENTRYP PFNGLLIGHTXPROC)(GLenum light, GLenum pname, GLfixed param); +GLAPI PFNGLLIGHTXPROC glad_glLightx; +#define glLightx glad_glLightx +typedef void (APIENTRYP PFNGLLIGHTXVPROC)(GLenum light, GLenum pname, const GLfixed *params); +GLAPI PFNGLLIGHTXVPROC glad_glLightxv; +#define glLightxv glad_glLightxv +typedef void (APIENTRYP PFNGLLINEWIDTHXPROC)(GLfixed width); +GLAPI PFNGLLINEWIDTHXPROC glad_glLineWidthx; +#define glLineWidthx glad_glLineWidthx +typedef void (APIENTRYP PFNGLLOADMATRIXXPROC)(const GLfixed *m); +GLAPI PFNGLLOADMATRIXXPROC glad_glLoadMatrixx; +#define glLoadMatrixx glad_glLoadMatrixx +typedef void (APIENTRYP PFNGLMATERIALXPROC)(GLenum face, GLenum pname, GLfixed param); +GLAPI PFNGLMATERIALXPROC glad_glMaterialx; +#define glMaterialx glad_glMaterialx +typedef void (APIENTRYP PFNGLMATERIALXVPROC)(GLenum face, GLenum pname, const GLfixed *param); +GLAPI PFNGLMATERIALXVPROC glad_glMaterialxv; +#define glMaterialxv glad_glMaterialxv +typedef void (APIENTRYP PFNGLMULTMATRIXXPROC)(const GLfixed *m); +GLAPI PFNGLMULTMATRIXXPROC glad_glMultMatrixx; +#define glMultMatrixx glad_glMultMatrixx +typedef void (APIENTRYP PFNGLMULTITEXCOORD4XPROC)(GLenum texture, GLfixed s, GLfixed t, GLfixed r, GLfixed q); +GLAPI PFNGLMULTITEXCOORD4XPROC glad_glMultiTexCoord4x; +#define glMultiTexCoord4x glad_glMultiTexCoord4x +typedef void (APIENTRYP PFNGLNORMAL3XPROC)(GLfixed nx, GLfixed ny, GLfixed nz); +GLAPI PFNGLNORMAL3XPROC glad_glNormal3x; +#define glNormal3x glad_glNormal3x +typedef void (APIENTRYP PFNGLORTHOXPROC)(GLfixed l, GLfixed r, GLfixed b, GLfixed t, GLfixed n, GLfixed f); +GLAPI PFNGLORTHOXPROC glad_glOrthox; +#define glOrthox glad_glOrthox +typedef void (APIENTRYP PFNGLPOINTPARAMETERXPROC)(GLenum pname, GLfixed param); +GLAPI PFNGLPOINTPARAMETERXPROC glad_glPointParameterx; +#define glPointParameterx glad_glPointParameterx +typedef void (APIENTRYP PFNGLPOINTPARAMETERXVPROC)(GLenum pname, const GLfixed *params); +GLAPI PFNGLPOINTPARAMETERXVPROC glad_glPointParameterxv; +#define glPointParameterxv glad_glPointParameterxv +typedef void (APIENTRYP PFNGLPOINTSIZEXPROC)(GLfixed size); +GLAPI PFNGLPOINTSIZEXPROC glad_glPointSizex; +#define glPointSizex glad_glPointSizex +typedef void (APIENTRYP PFNGLPOLYGONOFFSETXPROC)(GLfixed factor, GLfixed units); +GLAPI PFNGLPOLYGONOFFSETXPROC glad_glPolygonOffsetx; +#define glPolygonOffsetx glad_glPolygonOffsetx +typedef void (APIENTRYP PFNGLROTATEXPROC)(GLfixed angle, GLfixed x, GLfixed y, GLfixed z); +GLAPI PFNGLROTATEXPROC glad_glRotatex; +#define glRotatex glad_glRotatex +typedef void (APIENTRYP PFNGLSAMPLECOVERAGEXPROC)(GLclampx value, GLboolean invert); +GLAPI PFNGLSAMPLECOVERAGEXPROC glad_glSampleCoveragex; +#define glSampleCoveragex glad_glSampleCoveragex +typedef void (APIENTRYP PFNGLSCALEXPROC)(GLfixed x, GLfixed y, GLfixed z); +GLAPI PFNGLSCALEXPROC glad_glScalex; +#define glScalex glad_glScalex +typedef void (APIENTRYP PFNGLTEXENVXPROC)(GLenum target, GLenum pname, GLfixed param); +GLAPI PFNGLTEXENVXPROC glad_glTexEnvx; +#define glTexEnvx glad_glTexEnvx +typedef void (APIENTRYP PFNGLTEXENVXVPROC)(GLenum target, GLenum pname, const GLfixed *params); +GLAPI PFNGLTEXENVXVPROC glad_glTexEnvxv; +#define glTexEnvxv glad_glTexEnvxv +typedef void (APIENTRYP PFNGLTEXPARAMETERXPROC)(GLenum target, GLenum pname, GLfixed param); +GLAPI PFNGLTEXPARAMETERXPROC glad_glTexParameterx; +#define glTexParameterx glad_glTexParameterx +typedef void (APIENTRYP PFNGLTEXPARAMETERXVPROC)(GLenum target, GLenum pname, const GLfixed *params); +GLAPI PFNGLTEXPARAMETERXVPROC glad_glTexParameterxv; +#define glTexParameterxv glad_glTexParameterxv +typedef void (APIENTRYP PFNGLTRANSLATEXPROC)(GLfixed x, GLfixed y, GLfixed z); +GLAPI PFNGLTRANSLATEXPROC glad_glTranslatex; +#define glTranslatex glad_glTranslatex +#endif #ifndef GL_ES_VERSION_2_0 #define GL_ES_VERSION_2_0 1 GLAPI int GLAD_GL_ES_VERSION_2_0; @@ -4342,6 +4481,39 @@ GLAPI PFNGLGETINTERNALFORMATIVPROC glad_glGetInternalformativ; #define GL_STACK_OVERFLOW_KHR 0x0503 #define GL_STACK_UNDERFLOW_KHR 0x0504 #define GL_DISPLAY_LIST 0x82E7 +#define GL_NONE_OES 0 +#define GL_FRAMEBUFFER_OES 0x8D40 +#define GL_RENDERBUFFER_OES 0x8D41 +#define GL_RGBA4_OES 0x8056 +#define GL_RGB5_A1_OES 0x8057 +#define GL_RGB565_OES 0x8D62 +#define GL_DEPTH_COMPONENT16_OES 0x81A5 +#define GL_RENDERBUFFER_WIDTH_OES 0x8D42 +#define GL_RENDERBUFFER_HEIGHT_OES 0x8D43 +#define GL_RENDERBUFFER_INTERNAL_FORMAT_OES 0x8D44 +#define GL_RENDERBUFFER_RED_SIZE_OES 0x8D50 +#define GL_RENDERBUFFER_GREEN_SIZE_OES 0x8D51 +#define GL_RENDERBUFFER_BLUE_SIZE_OES 0x8D52 +#define GL_RENDERBUFFER_ALPHA_SIZE_OES 0x8D53 +#define GL_RENDERBUFFER_DEPTH_SIZE_OES 0x8D54 +#define GL_RENDERBUFFER_STENCIL_SIZE_OES 0x8D55 +#define GL_FRAMEBUFFER_ATTACHMENT_OBJECT_TYPE_OES 0x8CD0 +#define GL_FRAMEBUFFER_ATTACHMENT_OBJECT_NAME_OES 0x8CD1 +#define GL_FRAMEBUFFER_ATTACHMENT_TEXTURE_LEVEL_OES 0x8CD2 +#define GL_FRAMEBUFFER_ATTACHMENT_TEXTURE_CUBE_MAP_FACE_OES 0x8CD3 +#define GL_COLOR_ATTACHMENT0_OES 0x8CE0 +#define GL_DEPTH_ATTACHMENT_OES 0x8D00 +#define GL_STENCIL_ATTACHMENT_OES 0x8D20 +#define GL_FRAMEBUFFER_COMPLETE_OES 0x8CD5 +#define GL_FRAMEBUFFER_INCOMPLETE_ATTACHMENT_OES 0x8CD6 +#define GL_FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT_OES 0x8CD7 +#define GL_FRAMEBUFFER_INCOMPLETE_DIMENSIONS_OES 0x8CD9 +#define GL_FRAMEBUFFER_INCOMPLETE_FORMATS_OES 0x8CDA +#define GL_FRAMEBUFFER_UNSUPPORTED_OES 0x8CDD +#define GL_FRAMEBUFFER_BINDING_OES 0x8CA6 +#define GL_RENDERBUFFER_BINDING_OES 0x8CA7 +#define GL_MAX_RENDERBUFFER_SIZE_OES 0x84E8 +#define GL_INVALID_FRAMEBUFFER_OPERATION_OES 0x0506 #define GL_VERTEX_ARRAY_BINDING_OES 0x85B5 #ifndef GL_ARB_debug_output #define GL_ARB_debug_output 1 @@ -4500,6 +4672,55 @@ GLAPI PFNGLGETPOINTERVKHRPROC glad_glGetPointervKHR; #define GL_KHR_debug 1 GLAPI int GLAD_GL_KHR_debug; #endif +#ifndef GL_OES_framebuffer_object +#define GL_OES_framebuffer_object 1 +GLAPI int GLAD_GL_OES_framebuffer_object; +typedef GLboolean (APIENTRYP PFNGLISRENDERBUFFEROESPROC)(GLuint renderbuffer); +GLAPI PFNGLISRENDERBUFFEROESPROC glad_glIsRenderbufferOES; +#define glIsRenderbufferOES glad_glIsRenderbufferOES +typedef void (APIENTRYP PFNGLBINDRENDERBUFFEROESPROC)(GLenum target, GLuint renderbuffer); +GLAPI PFNGLBINDRENDERBUFFEROESPROC glad_glBindRenderbufferOES; +#define glBindRenderbufferOES glad_glBindRenderbufferOES +typedef void (APIENTRYP PFNGLDELETERENDERBUFFERSOESPROC)(GLsizei n, const GLuint *renderbuffers); +GLAPI PFNGLDELETERENDERBUFFERSOESPROC glad_glDeleteRenderbuffersOES; +#define glDeleteRenderbuffersOES glad_glDeleteRenderbuffersOES +typedef void (APIENTRYP PFNGLGENRENDERBUFFERSOESPROC)(GLsizei n, GLuint *renderbuffers); +GLAPI PFNGLGENRENDERBUFFERSOESPROC glad_glGenRenderbuffersOES; +#define glGenRenderbuffersOES glad_glGenRenderbuffersOES +typedef void (APIENTRYP PFNGLRENDERBUFFERSTORAGEOESPROC)(GLenum target, GLenum internalformat, GLsizei width, GLsizei height); +GLAPI PFNGLRENDERBUFFERSTORAGEOESPROC glad_glRenderbufferStorageOES; +#define glRenderbufferStorageOES glad_glRenderbufferStorageOES +typedef void (APIENTRYP PFNGLGETRENDERBUFFERPARAMETERIVOESPROC)(GLenum target, GLenum pname, GLint *params); +GLAPI PFNGLGETRENDERBUFFERPARAMETERIVOESPROC glad_glGetRenderbufferParameterivOES; +#define glGetRenderbufferParameterivOES glad_glGetRenderbufferParameterivOES +typedef GLboolean (APIENTRYP PFNGLISFRAMEBUFFEROESPROC)(GLuint framebuffer); +GLAPI PFNGLISFRAMEBUFFEROESPROC glad_glIsFramebufferOES; +#define glIsFramebufferOES glad_glIsFramebufferOES +typedef void (APIENTRYP PFNGLBINDFRAMEBUFFEROESPROC)(GLenum target, GLuint framebuffer); +GLAPI PFNGLBINDFRAMEBUFFEROESPROC glad_glBindFramebufferOES; +#define glBindFramebufferOES glad_glBindFramebufferOES +typedef void (APIENTRYP PFNGLDELETEFRAMEBUFFERSOESPROC)(GLsizei n, const GLuint *framebuffers); +GLAPI PFNGLDELETEFRAMEBUFFERSOESPROC glad_glDeleteFramebuffersOES; +#define glDeleteFramebuffersOES glad_glDeleteFramebuffersOES +typedef void (APIENTRYP PFNGLGENFRAMEBUFFERSOESPROC)(GLsizei n, GLuint *framebuffers); +GLAPI PFNGLGENFRAMEBUFFERSOESPROC glad_glGenFramebuffersOES; +#define glGenFramebuffersOES glad_glGenFramebuffersOES +typedef GLenum (APIENTRYP PFNGLCHECKFRAMEBUFFERSTATUSOESPROC)(GLenum target); +GLAPI PFNGLCHECKFRAMEBUFFERSTATUSOESPROC glad_glCheckFramebufferStatusOES; +#define glCheckFramebufferStatusOES glad_glCheckFramebufferStatusOES +typedef void (APIENTRYP PFNGLFRAMEBUFFERRENDERBUFFEROESPROC)(GLenum target, GLenum attachment, GLenum renderbuffertarget, GLuint renderbuffer); +GLAPI PFNGLFRAMEBUFFERRENDERBUFFEROESPROC glad_glFramebufferRenderbufferOES; +#define glFramebufferRenderbufferOES glad_glFramebufferRenderbufferOES +typedef void (APIENTRYP PFNGLFRAMEBUFFERTEXTURE2DOESPROC)(GLenum target, GLenum attachment, GLenum textarget, GLuint texture, GLint level); +GLAPI PFNGLFRAMEBUFFERTEXTURE2DOESPROC glad_glFramebufferTexture2DOES; +#define glFramebufferTexture2DOES glad_glFramebufferTexture2DOES +typedef void (APIENTRYP PFNGLGETFRAMEBUFFERATTACHMENTPARAMETERIVOESPROC)(GLenum target, GLenum attachment, GLenum pname, GLint *params); +GLAPI PFNGLGETFRAMEBUFFERATTACHMENTPARAMETERIVOESPROC glad_glGetFramebufferAttachmentParameterivOES; +#define glGetFramebufferAttachmentParameterivOES glad_glGetFramebufferAttachmentParameterivOES +typedef void (APIENTRYP PFNGLGENERATEMIPMAPOESPROC)(GLenum target); +GLAPI PFNGLGENERATEMIPMAPOESPROC glad_glGenerateMipmapOES; +#define glGenerateMipmapOES glad_glGenerateMipmapOES +#endif #ifndef GL_OES_vertex_array_object #define GL_OES_vertex_array_object 1 GLAPI int GLAD_GL_OES_vertex_array_object; @@ -4516,6 +4737,14 @@ typedef GLboolean (APIENTRYP PFNGLISVERTEXARRAYOESPROC)(GLuint array); GLAPI PFNGLISVERTEXARRAYOESPROC glad_glIsVertexArrayOES; #define glIsVertexArrayOES glad_glIsVertexArrayOES #endif +#ifndef GL_KHR_debug +#define GL_KHR_debug 1 +GLAPI int GLAD_GL_KHR_debug; +#endif +#ifndef GL_OES_vertex_array_object +#define GL_OES_vertex_array_object 1 +GLAPI int GLAD_GL_OES_vertex_array_object; +#endif #ifdef __cplusplus } diff --git a/vendor/glad/src/glad.c b/vendor/glad/src/glad.c index 7b968cf82..7c8b488ba 100644 --- a/vendor/glad/src/glad.c +++ b/vendor/glad/src/glad.c @@ -1,10 +1,10 @@ /* - OpenGL, OpenGL ES loader generated by glad 0.1.36 on Thu Jul 2 22:44:49 2026. + OpenGL, OpenGL ES loader generated by glad 0.1.36 on Tue Jul 7 08:27:13 2026. Language/Generator: C/C++ Specification: gl - APIs: gl=4.1, gles2=3.0 + APIs: gl=4.1, gles1=1.0, gles2=3.0 Profile: compatibility Extensions: GL_ARB_debug_output, @@ -13,6 +13,7 @@ GL_EXT_framebuffer_blit, GL_EXT_framebuffer_object, GL_KHR_debug, + GL_OES_framebuffer_object, GL_OES_vertex_array_object Loader: True Local files: False @@ -20,9 +21,9 @@ Reproducible: False Commandline: - --profile="compatibility" --api="gl=4.1,gles2=3.0" --generator="c" --spec="gl" --extensions="GL_ARB_debug_output,GL_ARB_framebuffer_object,GL_ARB_vertex_array_object,GL_EXT_framebuffer_blit,GL_EXT_framebuffer_object,GL_KHR_debug,GL_OES_vertex_array_object" + --profile="compatibility" --api="gl=4.1,gles1=1.0,gles2=3.0" --generator="c" --spec="gl" --extensions="GL_ARB_debug_output,GL_ARB_framebuffer_object,GL_ARB_vertex_array_object,GL_EXT_framebuffer_blit,GL_EXT_framebuffer_object,GL_KHR_debug,GL_OES_framebuffer_object,GL_OES_vertex_array_object" Online: - https://glad.dav1d.de/#profile=compatibility&language=c&specification=gl&loader=on&api=gl%3D4.1&api=gles2%3D3.0&extensions=GL_ARB_debug_output&extensions=GL_ARB_framebuffer_object&extensions=GL_ARB_vertex_array_object&extensions=GL_EXT_framebuffer_blit&extensions=GL_EXT_framebuffer_object&extensions=GL_KHR_debug&extensions=GL_OES_vertex_array_object + https://glad.dav1d.de/#profile=compatibility&language=c&specification=gl&loader=on&api=gl%3D4.1&api=gles1%3D1.0&api=gles2%3D3.0&extensions=GL_ARB_debug_output&extensions=GL_ARB_framebuffer_object&extensions=GL_ARB_vertex_array_object&extensions=GL_EXT_framebuffer_blit&extensions=GL_EXT_framebuffer_object&extensions=GL_KHR_debug&extensions=GL_OES_framebuffer_object&extensions=GL_OES_vertex_array_object */ #include @@ -275,12 +276,14 @@ int GLAD_GL_VERSION_3_2 = 0; int GLAD_GL_VERSION_3_3 = 0; int GLAD_GL_VERSION_4_0 = 0; int GLAD_GL_VERSION_4_1 = 0; +int GLAD_GL_VERSION_ES_CM_1_0 = 0; int GLAD_GL_ES_VERSION_2_0 = 0; int GLAD_GL_ES_VERSION_3_0 = 0; PFNGLACCUMPROC glad_glAccum = NULL; PFNGLACTIVESHADERPROGRAMPROC glad_glActiveShaderProgram = NULL; PFNGLACTIVETEXTUREPROC glad_glActiveTexture = NULL; PFNGLALPHAFUNCPROC glad_glAlphaFunc = NULL; +PFNGLALPHAFUNCXPROC glad_glAlphaFuncx = NULL; PFNGLARETEXTURESRESIDENTPROC glad_glAreTexturesResident = NULL; PFNGLARRAYELEMENTPROC glad_glArrayElement = NULL; PFNGLATTACHSHADERPROC glad_glAttachShader = NULL; @@ -326,13 +329,17 @@ PFNGLCLEARBUFFERFVPROC glad_glClearBufferfv = NULL; PFNGLCLEARBUFFERIVPROC glad_glClearBufferiv = NULL; PFNGLCLEARBUFFERUIVPROC glad_glClearBufferuiv = NULL; PFNGLCLEARCOLORPROC glad_glClearColor = NULL; +PFNGLCLEARCOLORXPROC glad_glClearColorx = NULL; PFNGLCLEARDEPTHPROC glad_glClearDepth = NULL; PFNGLCLEARDEPTHFPROC glad_glClearDepthf = NULL; +PFNGLCLEARDEPTHXPROC glad_glClearDepthx = NULL; PFNGLCLEARINDEXPROC glad_glClearIndex = NULL; PFNGLCLEARSTENCILPROC glad_glClearStencil = NULL; PFNGLCLIENTACTIVETEXTUREPROC glad_glClientActiveTexture = NULL; PFNGLCLIENTWAITSYNCPROC glad_glClientWaitSync = NULL; PFNGLCLIPPLANEPROC glad_glClipPlane = NULL; +PFNGLCLIPPLANEFPROC glad_glClipPlanef = NULL; +PFNGLCLIPPLANEXPROC glad_glClipPlanex = NULL; PFNGLCOLOR3BPROC glad_glColor3b = NULL; PFNGLCOLOR3BVPROC glad_glColor3bv = NULL; PFNGLCOLOR3DPROC glad_glColor3d = NULL; @@ -365,6 +372,7 @@ PFNGLCOLOR4UIPROC glad_glColor4ui = NULL; PFNGLCOLOR4UIVPROC glad_glColor4uiv = NULL; PFNGLCOLOR4USPROC glad_glColor4us = NULL; PFNGLCOLOR4USVPROC glad_glColor4usv = NULL; +PFNGLCOLOR4XPROC glad_glColor4x = NULL; PFNGLCOLORMASKPROC glad_glColorMask = NULL; PFNGLCOLORMASKIPROC glad_glColorMaski = NULL; PFNGLCOLORMATERIALPROC glad_glColorMaterial = NULL; @@ -410,6 +418,7 @@ PFNGLDEPTHRANGEPROC glad_glDepthRange = NULL; PFNGLDEPTHRANGEARRAYVPROC glad_glDepthRangeArrayv = NULL; PFNGLDEPTHRANGEINDEXEDPROC glad_glDepthRangeIndexed = NULL; PFNGLDEPTHRANGEFPROC glad_glDepthRangef = NULL; +PFNGLDEPTHRANGEXPROC glad_glDepthRangex = NULL; PFNGLDETACHSHADERPROC glad_glDetachShader = NULL; PFNGLDISABLEPROC glad_glDisable = NULL; PFNGLDISABLECLIENTSTATEPROC glad_glDisableClientState = NULL; @@ -469,6 +478,8 @@ PFNGLFOGFPROC glad_glFogf = NULL; PFNGLFOGFVPROC glad_glFogfv = NULL; PFNGLFOGIPROC glad_glFogi = NULL; PFNGLFOGIVPROC glad_glFogiv = NULL; +PFNGLFOGXPROC glad_glFogx = NULL; +PFNGLFOGXVPROC glad_glFogxv = NULL; PFNGLFRAMEBUFFERRENDERBUFFERPROC glad_glFramebufferRenderbuffer = NULL; PFNGLFRAMEBUFFERTEXTUREPROC glad_glFramebufferTexture = NULL; PFNGLFRAMEBUFFERTEXTURE1DPROC glad_glFramebufferTexture1D = NULL; @@ -477,6 +488,8 @@ PFNGLFRAMEBUFFERTEXTURE3DPROC glad_glFramebufferTexture3D = NULL; PFNGLFRAMEBUFFERTEXTURELAYERPROC glad_glFramebufferTextureLayer = NULL; PFNGLFRONTFACEPROC glad_glFrontFace = NULL; PFNGLFRUSTUMPROC glad_glFrustum = NULL; +PFNGLFRUSTUMFPROC glad_glFrustumf = NULL; +PFNGLFRUSTUMXPROC glad_glFrustumx = NULL; PFNGLGENBUFFERSPROC glad_glGenBuffers = NULL; PFNGLGENFRAMEBUFFERSPROC glad_glGenFramebuffers = NULL; PFNGLGENLISTSPROC glad_glGenLists = NULL; @@ -506,10 +519,13 @@ PFNGLGETBUFFERPARAMETERIVPROC glad_glGetBufferParameteriv = NULL; PFNGLGETBUFFERPOINTERVPROC glad_glGetBufferPointerv = NULL; PFNGLGETBUFFERSUBDATAPROC glad_glGetBufferSubData = NULL; PFNGLGETCLIPPLANEPROC glad_glGetClipPlane = NULL; +PFNGLGETCLIPPLANEFPROC glad_glGetClipPlanef = NULL; +PFNGLGETCLIPPLANEXPROC glad_glGetClipPlanex = NULL; PFNGLGETCOMPRESSEDTEXIMAGEPROC glad_glGetCompressedTexImage = NULL; PFNGLGETDOUBLEI_VPROC glad_glGetDoublei_v = NULL; PFNGLGETDOUBLEVPROC glad_glGetDoublev = NULL; PFNGLGETERRORPROC glad_glGetError = NULL; +PFNGLGETFIXEDVPROC glad_glGetFixedv = NULL; PFNGLGETFLOATI_VPROC glad_glGetFloati_v = NULL; PFNGLGETFLOATVPROC glad_glGetFloatv = NULL; PFNGLGETFRAGDATAINDEXPROC glad_glGetFragDataIndex = NULL; @@ -522,11 +538,13 @@ PFNGLGETINTEGERVPROC glad_glGetIntegerv = NULL; PFNGLGETINTERNALFORMATIVPROC glad_glGetInternalformativ = NULL; PFNGLGETLIGHTFVPROC glad_glGetLightfv = NULL; PFNGLGETLIGHTIVPROC glad_glGetLightiv = NULL; +PFNGLGETLIGHTXVPROC glad_glGetLightxv = NULL; PFNGLGETMAPDVPROC glad_glGetMapdv = NULL; PFNGLGETMAPFVPROC glad_glGetMapfv = NULL; PFNGLGETMAPIVPROC glad_glGetMapiv = NULL; PFNGLGETMATERIALFVPROC glad_glGetMaterialfv = NULL; PFNGLGETMATERIALIVPROC glad_glGetMaterialiv = NULL; +PFNGLGETMATERIALXVPROC glad_glGetMaterialxv = NULL; PFNGLGETMULTISAMPLEFVPROC glad_glGetMultisamplefv = NULL; PFNGLGETPIXELMAPFVPROC glad_glGetPixelMapfv = NULL; PFNGLGETPIXELMAPUIVPROC glad_glGetPixelMapuiv = NULL; @@ -561,6 +579,7 @@ PFNGLGETSUBROUTINEUNIFORMLOCATIONPROC glad_glGetSubroutineUniformLocation = NULL PFNGLGETSYNCIVPROC glad_glGetSynciv = NULL; PFNGLGETTEXENVFVPROC glad_glGetTexEnvfv = NULL; PFNGLGETTEXENVIVPROC glad_glGetTexEnviv = NULL; +PFNGLGETTEXENVXVPROC glad_glGetTexEnvxv = NULL; PFNGLGETTEXGENDVPROC glad_glGetTexGendv = NULL; PFNGLGETTEXGENFVPROC glad_glGetTexGenfv = NULL; PFNGLGETTEXGENIVPROC glad_glGetTexGeniv = NULL; @@ -571,6 +590,7 @@ PFNGLGETTEXPARAMETERIIVPROC glad_glGetTexParameterIiv = NULL; PFNGLGETTEXPARAMETERIUIVPROC glad_glGetTexParameterIuiv = NULL; PFNGLGETTEXPARAMETERFVPROC glad_glGetTexParameterfv = NULL; PFNGLGETTEXPARAMETERIVPROC glad_glGetTexParameteriv = NULL; +PFNGLGETTEXPARAMETERXVPROC glad_glGetTexParameterxv = NULL; PFNGLGETTRANSFORMFEEDBACKVARYINGPROC glad_glGetTransformFeedbackVarying = NULL; PFNGLGETUNIFORMBLOCKINDEXPROC glad_glGetUniformBlockIndex = NULL; PFNGLGETUNIFORMINDICESPROC glad_glGetUniformIndices = NULL; @@ -623,17 +643,23 @@ PFNGLLIGHTMODELFPROC glad_glLightModelf = NULL; PFNGLLIGHTMODELFVPROC glad_glLightModelfv = NULL; PFNGLLIGHTMODELIPROC glad_glLightModeli = NULL; PFNGLLIGHTMODELIVPROC glad_glLightModeliv = NULL; +PFNGLLIGHTMODELXPROC glad_glLightModelx = NULL; +PFNGLLIGHTMODELXVPROC glad_glLightModelxv = NULL; PFNGLLIGHTFPROC glad_glLightf = NULL; PFNGLLIGHTFVPROC glad_glLightfv = NULL; PFNGLLIGHTIPROC glad_glLighti = NULL; PFNGLLIGHTIVPROC glad_glLightiv = NULL; +PFNGLLIGHTXPROC glad_glLightx = NULL; +PFNGLLIGHTXVPROC glad_glLightxv = NULL; PFNGLLINESTIPPLEPROC glad_glLineStipple = NULL; PFNGLLINEWIDTHPROC glad_glLineWidth = NULL; +PFNGLLINEWIDTHXPROC glad_glLineWidthx = NULL; PFNGLLINKPROGRAMPROC glad_glLinkProgram = NULL; PFNGLLISTBASEPROC glad_glListBase = NULL; PFNGLLOADIDENTITYPROC glad_glLoadIdentity = NULL; PFNGLLOADMATRIXDPROC glad_glLoadMatrixd = NULL; PFNGLLOADMATRIXFPROC glad_glLoadMatrixf = NULL; +PFNGLLOADMATRIXXPROC glad_glLoadMatrixx = NULL; PFNGLLOADNAMEPROC glad_glLoadName = NULL; PFNGLLOADTRANSPOSEMATRIXDPROC glad_glLoadTransposeMatrixd = NULL; PFNGLLOADTRANSPOSEMATRIXFPROC glad_glLoadTransposeMatrixf = NULL; @@ -652,10 +678,13 @@ PFNGLMATERIALFPROC glad_glMaterialf = NULL; PFNGLMATERIALFVPROC glad_glMaterialfv = NULL; PFNGLMATERIALIPROC glad_glMateriali = NULL; PFNGLMATERIALIVPROC glad_glMaterialiv = NULL; +PFNGLMATERIALXPROC glad_glMaterialx = NULL; +PFNGLMATERIALXVPROC glad_glMaterialxv = NULL; PFNGLMATRIXMODEPROC glad_glMatrixMode = NULL; PFNGLMINSAMPLESHADINGPROC glad_glMinSampleShading = NULL; PFNGLMULTMATRIXDPROC glad_glMultMatrixd = NULL; PFNGLMULTMATRIXFPROC glad_glMultMatrixf = NULL; +PFNGLMULTMATRIXXPROC glad_glMultMatrixx = NULL; PFNGLMULTTRANSPOSEMATRIXDPROC glad_glMultTransposeMatrixd = NULL; PFNGLMULTTRANSPOSEMATRIXFPROC glad_glMultTransposeMatrixf = NULL; PFNGLMULTIDRAWARRAYSPROC glad_glMultiDrawArrays = NULL; @@ -693,6 +722,7 @@ PFNGLMULTITEXCOORD4IPROC glad_glMultiTexCoord4i = NULL; PFNGLMULTITEXCOORD4IVPROC glad_glMultiTexCoord4iv = NULL; PFNGLMULTITEXCOORD4SPROC glad_glMultiTexCoord4s = NULL; PFNGLMULTITEXCOORD4SVPROC glad_glMultiTexCoord4sv = NULL; +PFNGLMULTITEXCOORD4XPROC glad_glMultiTexCoord4x = NULL; PFNGLMULTITEXCOORDP1UIPROC glad_glMultiTexCoordP1ui = NULL; PFNGLMULTITEXCOORDP1UIVPROC glad_glMultiTexCoordP1uiv = NULL; PFNGLMULTITEXCOORDP2UIPROC glad_glMultiTexCoordP2ui = NULL; @@ -712,10 +742,13 @@ PFNGLNORMAL3IPROC glad_glNormal3i = NULL; PFNGLNORMAL3IVPROC glad_glNormal3iv = NULL; PFNGLNORMAL3SPROC glad_glNormal3s = NULL; PFNGLNORMAL3SVPROC glad_glNormal3sv = NULL; +PFNGLNORMAL3XPROC glad_glNormal3x = NULL; PFNGLNORMALP3UIPROC glad_glNormalP3ui = NULL; PFNGLNORMALP3UIVPROC glad_glNormalP3uiv = NULL; PFNGLNORMALPOINTERPROC glad_glNormalPointer = NULL; PFNGLORTHOPROC glad_glOrtho = NULL; +PFNGLORTHOFPROC glad_glOrthof = NULL; +PFNGLORTHOXPROC glad_glOrthox = NULL; PFNGLPASSTHROUGHPROC glad_glPassThrough = NULL; PFNGLPATCHPARAMETERFVPROC glad_glPatchParameterfv = NULL; PFNGLPATCHPARAMETERIPROC glad_glPatchParameteri = NULL; @@ -732,9 +765,13 @@ PFNGLPOINTPARAMETERFPROC glad_glPointParameterf = NULL; PFNGLPOINTPARAMETERFVPROC glad_glPointParameterfv = NULL; PFNGLPOINTPARAMETERIPROC glad_glPointParameteri = NULL; PFNGLPOINTPARAMETERIVPROC glad_glPointParameteriv = NULL; +PFNGLPOINTPARAMETERXPROC glad_glPointParameterx = NULL; +PFNGLPOINTPARAMETERXVPROC glad_glPointParameterxv = NULL; PFNGLPOINTSIZEPROC glad_glPointSize = NULL; +PFNGLPOINTSIZEXPROC glad_glPointSizex = NULL; PFNGLPOLYGONMODEPROC glad_glPolygonMode = NULL; PFNGLPOLYGONOFFSETPROC glad_glPolygonOffset = NULL; +PFNGLPOLYGONOFFSETXPROC glad_glPolygonOffsetx = NULL; PFNGLPOLYGONSTIPPLEPROC glad_glPolygonStipple = NULL; PFNGLPOPATTRIBPROC glad_glPopAttrib = NULL; PFNGLPOPCLIENTATTRIBPROC glad_glPopClientAttrib = NULL; @@ -841,7 +878,9 @@ PFNGLRENDERBUFFERSTORAGEMULTISAMPLEPROC glad_glRenderbufferStorageMultisample = PFNGLRESUMETRANSFORMFEEDBACKPROC glad_glResumeTransformFeedback = NULL; PFNGLROTATEDPROC glad_glRotated = NULL; PFNGLROTATEFPROC glad_glRotatef = NULL; +PFNGLROTATEXPROC glad_glRotatex = NULL; PFNGLSAMPLECOVERAGEPROC glad_glSampleCoverage = NULL; +PFNGLSAMPLECOVERAGEXPROC glad_glSampleCoveragex = NULL; PFNGLSAMPLEMASKIPROC glad_glSampleMaski = NULL; PFNGLSAMPLERPARAMETERIIVPROC glad_glSamplerParameterIiv = NULL; PFNGLSAMPLERPARAMETERIUIVPROC glad_glSamplerParameterIuiv = NULL; @@ -851,6 +890,7 @@ PFNGLSAMPLERPARAMETERIPROC glad_glSamplerParameteri = NULL; PFNGLSAMPLERPARAMETERIVPROC glad_glSamplerParameteriv = NULL; PFNGLSCALEDPROC glad_glScaled = NULL; PFNGLSCALEFPROC glad_glScalef = NULL; +PFNGLSCALEXPROC glad_glScalex = NULL; PFNGLSCISSORPROC glad_glScissor = NULL; PFNGLSCISSORARRAYVPROC glad_glScissorArrayv = NULL; PFNGLSCISSORINDEXEDPROC glad_glScissorIndexed = NULL; @@ -930,6 +970,8 @@ PFNGLTEXENVFPROC glad_glTexEnvf = NULL; PFNGLTEXENVFVPROC glad_glTexEnvfv = NULL; PFNGLTEXENVIPROC glad_glTexEnvi = NULL; PFNGLTEXENVIVPROC glad_glTexEnviv = NULL; +PFNGLTEXENVXPROC glad_glTexEnvx = NULL; +PFNGLTEXENVXVPROC glad_glTexEnvxv = NULL; PFNGLTEXGENDPROC glad_glTexGend = NULL; PFNGLTEXGENDVPROC glad_glTexGendv = NULL; PFNGLTEXGENFPROC glad_glTexGenf = NULL; @@ -947,6 +989,8 @@ PFNGLTEXPARAMETERFPROC glad_glTexParameterf = NULL; PFNGLTEXPARAMETERFVPROC glad_glTexParameterfv = NULL; PFNGLTEXPARAMETERIPROC glad_glTexParameteri = NULL; PFNGLTEXPARAMETERIVPROC glad_glTexParameteriv = NULL; +PFNGLTEXPARAMETERXPROC glad_glTexParameterx = NULL; +PFNGLTEXPARAMETERXVPROC glad_glTexParameterxv = NULL; PFNGLTEXSTORAGE2DPROC glad_glTexStorage2D = NULL; PFNGLTEXSTORAGE3DPROC glad_glTexStorage3D = NULL; PFNGLTEXSUBIMAGE1DPROC glad_glTexSubImage1D = NULL; @@ -955,6 +999,7 @@ PFNGLTEXSUBIMAGE3DPROC glad_glTexSubImage3D = NULL; PFNGLTRANSFORMFEEDBACKVARYINGSPROC glad_glTransformFeedbackVaryings = NULL; PFNGLTRANSLATEDPROC glad_glTranslated = NULL; PFNGLTRANSLATEFPROC glad_glTranslatef = NULL; +PFNGLTRANSLATEXPROC glad_glTranslatex = NULL; PFNGLUNIFORM1DPROC glad_glUniform1d = NULL; PFNGLUNIFORM1DVPROC glad_glUniform1dv = NULL; PFNGLUNIFORM1FPROC glad_glUniform1f = NULL; @@ -1146,6 +1191,7 @@ int GLAD_GL_ARB_vertex_array_object = 0; int GLAD_GL_EXT_framebuffer_blit = 0; int GLAD_GL_EXT_framebuffer_object = 0; int GLAD_GL_KHR_debug = 0; +int GLAD_GL_OES_framebuffer_object = 0; int GLAD_GL_OES_vertex_array_object = 0; PFNGLDEBUGMESSAGECONTROLARBPROC glad_glDebugMessageControlARB = NULL; PFNGLDEBUGMESSAGEINSERTARBPROC glad_glDebugMessageInsertARB = NULL; @@ -1190,6 +1236,21 @@ PFNGLGETOBJECTLABELKHRPROC glad_glGetObjectLabelKHR = NULL; PFNGLOBJECTPTRLABELKHRPROC glad_glObjectPtrLabelKHR = NULL; PFNGLGETOBJECTPTRLABELKHRPROC glad_glGetObjectPtrLabelKHR = NULL; PFNGLGETPOINTERVKHRPROC glad_glGetPointervKHR = NULL; +PFNGLISRENDERBUFFEROESPROC glad_glIsRenderbufferOES = NULL; +PFNGLBINDRENDERBUFFEROESPROC glad_glBindRenderbufferOES = NULL; +PFNGLDELETERENDERBUFFERSOESPROC glad_glDeleteRenderbuffersOES = NULL; +PFNGLGENRENDERBUFFERSOESPROC glad_glGenRenderbuffersOES = NULL; +PFNGLRENDERBUFFERSTORAGEOESPROC glad_glRenderbufferStorageOES = NULL; +PFNGLGETRENDERBUFFERPARAMETERIVOESPROC glad_glGetRenderbufferParameterivOES = NULL; +PFNGLISFRAMEBUFFEROESPROC glad_glIsFramebufferOES = NULL; +PFNGLBINDFRAMEBUFFEROESPROC glad_glBindFramebufferOES = NULL; +PFNGLDELETEFRAMEBUFFERSOESPROC glad_glDeleteFramebuffersOES = NULL; +PFNGLGENFRAMEBUFFERSOESPROC glad_glGenFramebuffersOES = NULL; +PFNGLCHECKFRAMEBUFFERSTATUSOESPROC glad_glCheckFramebufferStatusOES = NULL; +PFNGLFRAMEBUFFERRENDERBUFFEROESPROC glad_glFramebufferRenderbufferOES = NULL; +PFNGLFRAMEBUFFERTEXTURE2DOESPROC glad_glFramebufferTexture2DOES = NULL; +PFNGLGETFRAMEBUFFERATTACHMENTPARAMETERIVOESPROC glad_glGetFramebufferAttachmentParameterivOES = NULL; +PFNGLGENERATEMIPMAPOESPROC glad_glGenerateMipmapOES = NULL; PFNGLBINDVERTEXARRAYOESPROC glad_glBindVertexArrayOES = NULL; PFNGLDELETEVERTEXARRAYSOESPROC glad_glDeleteVertexArraysOES = NULL; PFNGLGENVERTEXARRAYSOESPROC glad_glGenVertexArraysOES = NULL; @@ -2283,6 +2344,245 @@ int gladLoadGLLoader(GLADloadproc load) { return GLVersion.major != 0 || GLVersion.minor != 0; } +static void load_GL_VERSION_ES_CM_1_0(GLADloadproc load) { + if(!GLAD_GL_VERSION_ES_CM_1_0) return; + glad_glAlphaFunc = (PFNGLALPHAFUNCPROC)load("glAlphaFunc"); + glad_glClearColor = (PFNGLCLEARCOLORPROC)load("glClearColor"); + glad_glClearDepthf = (PFNGLCLEARDEPTHFPROC)load("glClearDepthf"); + glad_glClipPlanef = (PFNGLCLIPPLANEFPROC)load("glClipPlanef"); + glad_glColor4f = (PFNGLCOLOR4FPROC)load("glColor4f"); + glad_glDepthRangef = (PFNGLDEPTHRANGEFPROC)load("glDepthRangef"); + glad_glFogf = (PFNGLFOGFPROC)load("glFogf"); + glad_glFogfv = (PFNGLFOGFVPROC)load("glFogfv"); + glad_glFrustumf = (PFNGLFRUSTUMFPROC)load("glFrustumf"); + glad_glGetClipPlanef = (PFNGLGETCLIPPLANEFPROC)load("glGetClipPlanef"); + glad_glGetFloatv = (PFNGLGETFLOATVPROC)load("glGetFloatv"); + glad_glGetLightfv = (PFNGLGETLIGHTFVPROC)load("glGetLightfv"); + glad_glGetMaterialfv = (PFNGLGETMATERIALFVPROC)load("glGetMaterialfv"); + glad_glGetTexEnvfv = (PFNGLGETTEXENVFVPROC)load("glGetTexEnvfv"); + glad_glGetTexParameterfv = (PFNGLGETTEXPARAMETERFVPROC)load("glGetTexParameterfv"); + glad_glLightModelf = (PFNGLLIGHTMODELFPROC)load("glLightModelf"); + glad_glLightModelfv = (PFNGLLIGHTMODELFVPROC)load("glLightModelfv"); + glad_glLightf = (PFNGLLIGHTFPROC)load("glLightf"); + glad_glLightfv = (PFNGLLIGHTFVPROC)load("glLightfv"); + glad_glLineWidth = (PFNGLLINEWIDTHPROC)load("glLineWidth"); + glad_glLoadMatrixf = (PFNGLLOADMATRIXFPROC)load("glLoadMatrixf"); + glad_glMaterialf = (PFNGLMATERIALFPROC)load("glMaterialf"); + glad_glMaterialfv = (PFNGLMATERIALFVPROC)load("glMaterialfv"); + glad_glMultMatrixf = (PFNGLMULTMATRIXFPROC)load("glMultMatrixf"); + glad_glMultiTexCoord4f = (PFNGLMULTITEXCOORD4FPROC)load("glMultiTexCoord4f"); + glad_glNormal3f = (PFNGLNORMAL3FPROC)load("glNormal3f"); + glad_glOrthof = (PFNGLORTHOFPROC)load("glOrthof"); + glad_glPointParameterf = (PFNGLPOINTPARAMETERFPROC)load("glPointParameterf"); + glad_glPointParameterfv = (PFNGLPOINTPARAMETERFVPROC)load("glPointParameterfv"); + glad_glPointSize = (PFNGLPOINTSIZEPROC)load("glPointSize"); + glad_glPolygonOffset = (PFNGLPOLYGONOFFSETPROC)load("glPolygonOffset"); + glad_glRotatef = (PFNGLROTATEFPROC)load("glRotatef"); + glad_glScalef = (PFNGLSCALEFPROC)load("glScalef"); + glad_glTexEnvf = (PFNGLTEXENVFPROC)load("glTexEnvf"); + glad_glTexEnvfv = (PFNGLTEXENVFVPROC)load("glTexEnvfv"); + glad_glTexParameterf = (PFNGLTEXPARAMETERFPROC)load("glTexParameterf"); + glad_glTexParameterfv = (PFNGLTEXPARAMETERFVPROC)load("glTexParameterfv"); + glad_glTranslatef = (PFNGLTRANSLATEFPROC)load("glTranslatef"); + glad_glActiveTexture = (PFNGLACTIVETEXTUREPROC)load("glActiveTexture"); + glad_glAlphaFuncx = (PFNGLALPHAFUNCXPROC)load("glAlphaFuncx"); + glad_glBindBuffer = (PFNGLBINDBUFFERPROC)load("glBindBuffer"); + glad_glBindTexture = (PFNGLBINDTEXTUREPROC)load("glBindTexture"); + glad_glBlendFunc = (PFNGLBLENDFUNCPROC)load("glBlendFunc"); + glad_glBufferData = (PFNGLBUFFERDATAPROC)load("glBufferData"); + glad_glBufferSubData = (PFNGLBUFFERSUBDATAPROC)load("glBufferSubData"); + glad_glClear = (PFNGLCLEARPROC)load("glClear"); + glad_glClearColorx = (PFNGLCLEARCOLORXPROC)load("glClearColorx"); + glad_glClearDepthx = (PFNGLCLEARDEPTHXPROC)load("glClearDepthx"); + glad_glClearStencil = (PFNGLCLEARSTENCILPROC)load("glClearStencil"); + glad_glClientActiveTexture = (PFNGLCLIENTACTIVETEXTUREPROC)load("glClientActiveTexture"); + glad_glClipPlanex = (PFNGLCLIPPLANEXPROC)load("glClipPlanex"); + glad_glColor4ub = (PFNGLCOLOR4UBPROC)load("glColor4ub"); + glad_glColor4x = (PFNGLCOLOR4XPROC)load("glColor4x"); + glad_glColorMask = (PFNGLCOLORMASKPROC)load("glColorMask"); + glad_glColorPointer = (PFNGLCOLORPOINTERPROC)load("glColorPointer"); + glad_glCompressedTexImage2D = (PFNGLCOMPRESSEDTEXIMAGE2DPROC)load("glCompressedTexImage2D"); + glad_glCompressedTexSubImage2D = (PFNGLCOMPRESSEDTEXSUBIMAGE2DPROC)load("glCompressedTexSubImage2D"); + glad_glCopyTexImage2D = (PFNGLCOPYTEXIMAGE2DPROC)load("glCopyTexImage2D"); + glad_glCopyTexSubImage2D = (PFNGLCOPYTEXSUBIMAGE2DPROC)load("glCopyTexSubImage2D"); + glad_glCullFace = (PFNGLCULLFACEPROC)load("glCullFace"); + glad_glDeleteBuffers = (PFNGLDELETEBUFFERSPROC)load("glDeleteBuffers"); + glad_glDeleteTextures = (PFNGLDELETETEXTURESPROC)load("glDeleteTextures"); + glad_glDepthFunc = (PFNGLDEPTHFUNCPROC)load("glDepthFunc"); + glad_glDepthMask = (PFNGLDEPTHMASKPROC)load("glDepthMask"); + glad_glDepthRangex = (PFNGLDEPTHRANGEXPROC)load("glDepthRangex"); + glad_glDisable = (PFNGLDISABLEPROC)load("glDisable"); + glad_glDisableClientState = (PFNGLDISABLECLIENTSTATEPROC)load("glDisableClientState"); + glad_glDrawArrays = (PFNGLDRAWARRAYSPROC)load("glDrawArrays"); + glad_glDrawElements = (PFNGLDRAWELEMENTSPROC)load("glDrawElements"); + glad_glEnable = (PFNGLENABLEPROC)load("glEnable"); + glad_glEnableClientState = (PFNGLENABLECLIENTSTATEPROC)load("glEnableClientState"); + glad_glFinish = (PFNGLFINISHPROC)load("glFinish"); + glad_glFlush = (PFNGLFLUSHPROC)load("glFlush"); + glad_glFogx = (PFNGLFOGXPROC)load("glFogx"); + glad_glFogxv = (PFNGLFOGXVPROC)load("glFogxv"); + glad_glFrontFace = (PFNGLFRONTFACEPROC)load("glFrontFace"); + glad_glFrustumx = (PFNGLFRUSTUMXPROC)load("glFrustumx"); + glad_glGetBooleanv = (PFNGLGETBOOLEANVPROC)load("glGetBooleanv"); + glad_glGetBufferParameteriv = (PFNGLGETBUFFERPARAMETERIVPROC)load("glGetBufferParameteriv"); + glad_glGetClipPlanex = (PFNGLGETCLIPPLANEXPROC)load("glGetClipPlanex"); + glad_glGenBuffers = (PFNGLGENBUFFERSPROC)load("glGenBuffers"); + glad_glGenTextures = (PFNGLGENTEXTURESPROC)load("glGenTextures"); + glad_glGetError = (PFNGLGETERRORPROC)load("glGetError"); + glad_glGetFixedv = (PFNGLGETFIXEDVPROC)load("glGetFixedv"); + glad_glGetIntegerv = (PFNGLGETINTEGERVPROC)load("glGetIntegerv"); + glad_glGetLightxv = (PFNGLGETLIGHTXVPROC)load("glGetLightxv"); + glad_glGetMaterialxv = (PFNGLGETMATERIALXVPROC)load("glGetMaterialxv"); + glad_glGetPointerv = (PFNGLGETPOINTERVPROC)load("glGetPointerv"); + glad_glGetString = (PFNGLGETSTRINGPROC)load("glGetString"); + glad_glGetTexEnviv = (PFNGLGETTEXENVIVPROC)load("glGetTexEnviv"); + glad_glGetTexEnvxv = (PFNGLGETTEXENVXVPROC)load("glGetTexEnvxv"); + glad_glGetTexParameteriv = (PFNGLGETTEXPARAMETERIVPROC)load("glGetTexParameteriv"); + glad_glGetTexParameterxv = (PFNGLGETTEXPARAMETERXVPROC)load("glGetTexParameterxv"); + glad_glHint = (PFNGLHINTPROC)load("glHint"); + glad_glIsBuffer = (PFNGLISBUFFERPROC)load("glIsBuffer"); + glad_glIsEnabled = (PFNGLISENABLEDPROC)load("glIsEnabled"); + glad_glIsTexture = (PFNGLISTEXTUREPROC)load("glIsTexture"); + glad_glLightModelx = (PFNGLLIGHTMODELXPROC)load("glLightModelx"); + glad_glLightModelxv = (PFNGLLIGHTMODELXVPROC)load("glLightModelxv"); + glad_glLightx = (PFNGLLIGHTXPROC)load("glLightx"); + glad_glLightxv = (PFNGLLIGHTXVPROC)load("glLightxv"); + glad_glLineWidthx = (PFNGLLINEWIDTHXPROC)load("glLineWidthx"); + glad_glLoadIdentity = (PFNGLLOADIDENTITYPROC)load("glLoadIdentity"); + glad_glLoadMatrixx = (PFNGLLOADMATRIXXPROC)load("glLoadMatrixx"); + glad_glLogicOp = (PFNGLLOGICOPPROC)load("glLogicOp"); + glad_glMaterialx = (PFNGLMATERIALXPROC)load("glMaterialx"); + glad_glMaterialxv = (PFNGLMATERIALXVPROC)load("glMaterialxv"); + glad_glMatrixMode = (PFNGLMATRIXMODEPROC)load("glMatrixMode"); + glad_glMultMatrixx = (PFNGLMULTMATRIXXPROC)load("glMultMatrixx"); + glad_glMultiTexCoord4x = (PFNGLMULTITEXCOORD4XPROC)load("glMultiTexCoord4x"); + glad_glNormal3x = (PFNGLNORMAL3XPROC)load("glNormal3x"); + glad_glNormalPointer = (PFNGLNORMALPOINTERPROC)load("glNormalPointer"); + glad_glOrthox = (PFNGLORTHOXPROC)load("glOrthox"); + glad_glPixelStorei = (PFNGLPIXELSTOREIPROC)load("glPixelStorei"); + glad_glPointParameterx = (PFNGLPOINTPARAMETERXPROC)load("glPointParameterx"); + glad_glPointParameterxv = (PFNGLPOINTPARAMETERXVPROC)load("glPointParameterxv"); + glad_glPointSizex = (PFNGLPOINTSIZEXPROC)load("glPointSizex"); + glad_glPolygonOffsetx = (PFNGLPOLYGONOFFSETXPROC)load("glPolygonOffsetx"); + glad_glPopMatrix = (PFNGLPOPMATRIXPROC)load("glPopMatrix"); + glad_glPushMatrix = (PFNGLPUSHMATRIXPROC)load("glPushMatrix"); + glad_glReadPixels = (PFNGLREADPIXELSPROC)load("glReadPixels"); + glad_glRotatex = (PFNGLROTATEXPROC)load("glRotatex"); + glad_glSampleCoverage = (PFNGLSAMPLECOVERAGEPROC)load("glSampleCoverage"); + glad_glSampleCoveragex = (PFNGLSAMPLECOVERAGEXPROC)load("glSampleCoveragex"); + glad_glScalex = (PFNGLSCALEXPROC)load("glScalex"); + glad_glScissor = (PFNGLSCISSORPROC)load("glScissor"); + glad_glShadeModel = (PFNGLSHADEMODELPROC)load("glShadeModel"); + glad_glStencilFunc = (PFNGLSTENCILFUNCPROC)load("glStencilFunc"); + glad_glStencilMask = (PFNGLSTENCILMASKPROC)load("glStencilMask"); + glad_glStencilOp = (PFNGLSTENCILOPPROC)load("glStencilOp"); + glad_glTexCoordPointer = (PFNGLTEXCOORDPOINTERPROC)load("glTexCoordPointer"); + glad_glTexEnvi = (PFNGLTEXENVIPROC)load("glTexEnvi"); + glad_glTexEnvx = (PFNGLTEXENVXPROC)load("glTexEnvx"); + glad_glTexEnviv = (PFNGLTEXENVIVPROC)load("glTexEnviv"); + glad_glTexEnvxv = (PFNGLTEXENVXVPROC)load("glTexEnvxv"); + glad_glTexImage2D = (PFNGLTEXIMAGE2DPROC)load("glTexImage2D"); + glad_glTexParameteri = (PFNGLTEXPARAMETERIPROC)load("glTexParameteri"); + glad_glTexParameterx = (PFNGLTEXPARAMETERXPROC)load("glTexParameterx"); + glad_glTexParameteriv = (PFNGLTEXPARAMETERIVPROC)load("glTexParameteriv"); + glad_glTexParameterxv = (PFNGLTEXPARAMETERXVPROC)load("glTexParameterxv"); + glad_glTexSubImage2D = (PFNGLTEXSUBIMAGE2DPROC)load("glTexSubImage2D"); + glad_glTranslatex = (PFNGLTRANSLATEXPROC)load("glTranslatex"); + glad_glVertexPointer = (PFNGLVERTEXPOINTERPROC)load("glVertexPointer"); + glad_glViewport = (PFNGLVIEWPORTPROC)load("glViewport"); +} +static void load_GL_OES_framebuffer_object(GLADloadproc load) { + if(!GLAD_GL_OES_framebuffer_object) return; + glad_glIsRenderbufferOES = (PFNGLISRENDERBUFFEROESPROC)load("glIsRenderbufferOES"); + glad_glBindRenderbufferOES = (PFNGLBINDRENDERBUFFEROESPROC)load("glBindRenderbufferOES"); + glad_glDeleteRenderbuffersOES = (PFNGLDELETERENDERBUFFERSOESPROC)load("glDeleteRenderbuffersOES"); + glad_glGenRenderbuffersOES = (PFNGLGENRENDERBUFFERSOESPROC)load("glGenRenderbuffersOES"); + glad_glRenderbufferStorageOES = (PFNGLRENDERBUFFERSTORAGEOESPROC)load("glRenderbufferStorageOES"); + glad_glGetRenderbufferParameterivOES = (PFNGLGETRENDERBUFFERPARAMETERIVOESPROC)load("glGetRenderbufferParameterivOES"); + glad_glIsFramebufferOES = (PFNGLISFRAMEBUFFEROESPROC)load("glIsFramebufferOES"); + glad_glBindFramebufferOES = (PFNGLBINDFRAMEBUFFEROESPROC)load("glBindFramebufferOES"); + glad_glDeleteFramebuffersOES = (PFNGLDELETEFRAMEBUFFERSOESPROC)load("glDeleteFramebuffersOES"); + glad_glGenFramebuffersOES = (PFNGLGENFRAMEBUFFERSOESPROC)load("glGenFramebuffersOES"); + glad_glCheckFramebufferStatusOES = (PFNGLCHECKFRAMEBUFFERSTATUSOESPROC)load("glCheckFramebufferStatusOES"); + glad_glFramebufferRenderbufferOES = (PFNGLFRAMEBUFFERRENDERBUFFEROESPROC)load("glFramebufferRenderbufferOES"); + glad_glFramebufferTexture2DOES = (PFNGLFRAMEBUFFERTEXTURE2DOESPROC)load("glFramebufferTexture2DOES"); + glad_glGetFramebufferAttachmentParameterivOES = (PFNGLGETFRAMEBUFFERATTACHMENTPARAMETERIVOESPROC)load("glGetFramebufferAttachmentParameterivOES"); + glad_glGenerateMipmapOES = (PFNGLGENERATEMIPMAPOESPROC)load("glGenerateMipmapOES"); +} +static void load_GL_OES_vertex_array_object(GLADloadproc load) { + if(!GLAD_GL_OES_vertex_array_object) return; + glad_glBindVertexArrayOES = (PFNGLBINDVERTEXARRAYOESPROC)load("glBindVertexArrayOES"); + glad_glDeleteVertexArraysOES = (PFNGLDELETEVERTEXARRAYSOESPROC)load("glDeleteVertexArraysOES"); + glad_glGenVertexArraysOES = (PFNGLGENVERTEXARRAYSOESPROC)load("glGenVertexArraysOES"); + glad_glIsVertexArrayOES = (PFNGLISVERTEXARRAYOESPROC)load("glIsVertexArrayOES"); +} +static int find_extensionsGLES1(void) { + if (!get_exts()) return 0; + GLAD_GL_KHR_debug = has_ext("GL_KHR_debug"); + GLAD_GL_OES_framebuffer_object = has_ext("GL_OES_framebuffer_object"); + GLAD_GL_OES_vertex_array_object = has_ext("GL_OES_vertex_array_object"); + free_exts(); + return 1; +} + +static void find_coreGLES1(void) { + + /* Thank you @elmindreda + * https://github.com/elmindreda/greg/blob/master/templates/greg.c.in#L176 + * https://github.com/glfw/glfw/blob/master/src/context.c#L36 + */ + int i, major, minor; + + const char* version; + const char* prefixes[] = { + "OpenGL ES-CM ", + "OpenGL ES-CL ", + "OpenGL ES ", + NULL + }; + + version = (const char*) glGetString(GL_VERSION); + if (!version) return; + + for (i = 0; prefixes[i]; i++) { + const size_t length = strlen(prefixes[i]); + if (strncmp(version, prefixes[i], length) == 0) { + version += length; + break; + } + } + +/* PR #18 */ +#ifdef _MSC_VER + sscanf_s(version, "%d.%d", &major, &minor); +#else + sscanf(version, "%d.%d", &major, &minor); +#endif + + GLVersion.major = major; GLVersion.minor = minor; + max_loaded_major = major; max_loaded_minor = minor; + GLAD_GL_VERSION_ES_CM_1_0 = (major == 1 && minor >= 0) || major > 1; + if (GLVersion.major > 1 || (GLVersion.major >= 1 && GLVersion.minor >= 0)) { + max_loaded_major = 1; + max_loaded_minor = 0; + } +} + +int gladLoadGLES1Loader(GLADloadproc load) { + GLVersion.major = 0; GLVersion.minor = 0; + glGetString = (PFNGLGETSTRINGPROC)load("glGetString"); + if(glGetString == NULL) return 0; + if(glGetString(GL_VERSION) == NULL) return 0; + find_coreGLES1(); + load_GL_VERSION_ES_CM_1_0(load); + + if (!find_extensionsGLES1()) return 0; + load_GL_KHR_debug(load); + load_GL_OES_framebuffer_object(load); + load_GL_OES_vertex_array_object(load); + return GLVersion.major != 0 || GLVersion.minor != 0; +} + static void load_GL_ES_VERSION_2_0(GLADloadproc load) { if(!GLAD_GL_ES_VERSION_2_0) return; glad_glActiveTexture = (PFNGLACTIVETEXTUREPROC)load("glActiveTexture"); @@ -2535,13 +2835,6 @@ static void load_GL_ES_VERSION_3_0(GLADloadproc load) { glad_glTexStorage3D = (PFNGLTEXSTORAGE3DPROC)load("glTexStorage3D"); glad_glGetInternalformativ = (PFNGLGETINTERNALFORMATIVPROC)load("glGetInternalformativ"); } -static void load_GL_OES_vertex_array_object(GLADloadproc load) { - if(!GLAD_GL_OES_vertex_array_object) return; - glad_glBindVertexArrayOES = (PFNGLBINDVERTEXARRAYOESPROC)load("glBindVertexArrayOES"); - glad_glDeleteVertexArraysOES = (PFNGLDELETEVERTEXARRAYSOESPROC)load("glDeleteVertexArraysOES"); - glad_glGenVertexArraysOES = (PFNGLGENVERTEXARRAYSOESPROC)load("glGenVertexArraysOES"); - glad_glIsVertexArrayOES = (PFNGLISVERTEXARRAYOESPROC)load("glIsVertexArrayOES"); -} static int find_extensionsGLES2(void) { if (!get_exts()) return 0; GLAD_GL_KHR_debug = has_ext("GL_KHR_debug"); From 31a3d71e8254477ea23825b831522357729b6387 Mon Sep 17 00:00:00 2001 From: Un1q32 Date: Tue, 7 Jul 2026 06:02:00 -0400 Subject: [PATCH 051/109] ok --- src/desktop/backends/ios.c | 90 +++++++++++++++++++++++++++++++++++--- src/desktop/main.c | 5 ++- 2 files changed, 88 insertions(+), 7 deletions(-) diff --git a/src/desktop/backends/ios.c b/src/desktop/backends/ios.c index ff9b60d29..5f9940007 100755 --- a/src/desktop/backends/ios.c +++ b/src/desktop/backends/ios.c @@ -93,6 +93,10 @@ static char g_ffSpeedArg[64] = "--fast-forward-speed=4"; * the setting can only be changed from the menu, never mid-game. */ static atomic_bool g_highResEnabled = false; +/* Cached copy of the renderer setting, refreshed from NSUserDefaults once per + * game launch in startGameWithFolder:. */ +static char g_rendererArg[32] = "software"; + #define BS_GAMES_ROOT_PATH @"/var/mobile/Documents/Butterscotch" static void bsRequestRelayout(void) { @@ -443,12 +447,16 @@ void platformInitFunctions(Runner *runner) { /* this can't be in platformInit because glad hasn't initialized yet */ if (!glInited) { - glGenFramebuffers(1, &framebuffer); - glGenRenderbuffers(1, &renderbuffer); #ifdef ENABLE_SW_RENDERER - if (gfx == SOFTWARE) + if (gfx == SOFTWARE) { + glGenFramebuffers = glGenFramebuffersOES; + glGenRenderbuffers = glGenRenderbuffersOES; + fprintf(stderr, "glGenTextures: %p\n", glGenTextures); glGenTextures(1, &swTexture); + } #endif + glGenFramebuffers(1, &framebuffer); + glGenRenderbuffers(1, &renderbuffer); glInited = true; } @@ -847,6 +855,10 @@ static void drawCenteredLabel(NSString *text, CGRect rect, UIFont *font) { #define BS_HIGH_RES_DEFAULTS_KEY @"BSHighResolution" +#define BS_RENDERER_DEFAULTS_KEY @"BSRenderer" +#define BS_RENDERER_SOFTWARE 0 +#define BS_RENDERER_MODERN_GL 1 + /* Falls back to the default any time the stored value is missing or * non-positive (e.g. first launch, or a corrupted/edited defaults plist). */ static double bsLoadFastForwardSpeed(void) { @@ -860,6 +872,25 @@ static bool bsLoadHighResEnabled(void) { return [[NSUserDefaults standardUserDefaults] boolForKey:BS_HIGH_RES_DEFAULTS_KEY]; } +/* Returns the saved renderer preference, defaulting to software if never set. */ +static int bsLoadRendererPreference(void) { + NSInteger v = [[NSUserDefaults standardUserDefaults] integerForKey:BS_RENDERER_DEFAULTS_KEY]; + if (v != BS_RENDERER_SOFTWARE && v != BS_RENDERER_MODERN_GL) + return BS_RENDERER_SOFTWARE; + return (int)v; +} + +/* Checks if the device supports OpenGL ES 2.0. On iOS 2.0+, this can be + * checked by trying to create an EAGLContext with API=2. */ +static bool bsSupportsGLES2(void) { + EAGLContext *testContext = [[EAGLContext alloc] initWithAPI:2]; + if (testContext) { + [testContext release]; + return true; + } + return false; +} + /* Renders a simple gear glyph into a UIImage via Core Graphics, rather than * relying on a Unicode gear character rendering correctly on very old * font/rendering stacks. Uses UIGraphicsBeginImageContext (iOS 2+) rather @@ -904,6 +935,7 @@ static UIImage *createGearIconImage(CGFloat size, UIColor *color) { @interface BSSettingsViewController : UIViewController { UITextField *speedField; UISwitch *highResSwitch; + UISegmentedControl *rendererControl; } @end @@ -958,6 +990,32 @@ static UIKeyboardType bsNumericKeyboardType(void) { highResLabel.backgroundColor = [UIColor clearColor]; [root addSubview:highResLabel]; + /* Renderer selection: segmented control with Software and Modern GL options. + * Only show this if both renderers are available, otherwise force the appropriate one. */ +#if !defined(ENABLE_MODERN_GL) || !defined(ENABLE_SW_RENDERER) + rendererControl = nil; +#else + bool supportsGLES2 = bsSupportsGLES2(); + rendererControl = [[UISegmentedControl alloc] initWithItems:[NSArray arrayWithObjects:@"Software", @"Modern GL", nil]]; + [rendererControl sizeToFit]; + CGRect rendererFrame = rendererControl.frame; + rendererFrame.origin = CGPointMake(20, 150); + rendererFrame.size.width = bounds.size.width - 40; + rendererControl.frame = rendererFrame; + rendererControl.selectedSegmentIndex = bsLoadRendererPreference(); + rendererControl.enabled = supportsGLES2; + if (!supportsGLES2) { + rendererControl.selectedSegmentIndex = BS_RENDERER_SOFTWARE; + } + [root addSubview:rendererControl]; + + UILabel *rendererLabel = [[[UILabel alloc] initWithFrame:CGRectMake(20, 130, bounds.size.width - 40, 20)] autorelease]; + rendererLabel.text = @"Renderer:"; + rendererLabel.font = [UIFont systemFontOfSize:15.0f]; + rendererLabel.backgroundColor = [UIColor clearColor]; + [root addSubview:rendererLabel]; +#endif + self.view = root; } @@ -983,6 +1041,10 @@ static UIKeyboardType bsNumericKeyboardType(void) { * value (or the default) is left untouched. */ [[NSUserDefaults standardUserDefaults] setBool:highResSwitch.isOn forKey:BS_HIGH_RES_DEFAULTS_KEY]; +#if defined(ENABLE_MODERN_GL) && defined(ENABLE_SW_RENDERER) + if (rendererControl) + [[NSUserDefaults standardUserDefaults] setInteger:rendererControl.selectedSegmentIndex forKey:BS_RENDERER_DEFAULTS_KEY]; +#endif [[NSUserDefaults standardUserDefaults] synchronize]; [[[UIApplication sharedApplication] delegate] performSelector:@selector(settingsDone)]; @@ -996,6 +1058,7 @@ static UIKeyboardType bsNumericKeyboardType(void) { - (void)dealloc { [speedField release]; [highResSwitch release]; + if (rendererControl) [rendererControl release]; [super dealloc]; } @@ -1194,8 +1257,9 @@ extern int game_main(int argc, char *argv[]); static char arg0[] = "butterscotch"; static char arg1[] = "--lazy-textures"; static char arg2[] = "--lazy-rooms"; - char *argv[] = { arg0, arg1, arg2, g_ffSpeedArg, g_gamePath, NULL }; - game_main(5, argv); + static char arg3[] = "--renderer"; + char *argv[] = { arg0, arg1, arg2, g_ffSpeedArg, arg3, g_rendererArg, g_gamePath, NULL }; + game_main(7, argv); [self performSelectorOnMainThread:@selector(returnToMenu) withObject:nil waitUntilDone:NO]; @@ -1209,6 +1273,22 @@ extern int game_main(int argc, char *argv[]); snprintf(g_ffSpeedArg, sizeof(g_ffSpeedArg), "--fast-forward-speed=%g", bsLoadFastForwardSpeed()); atomic_store(&g_highResEnabled, bsLoadHighResEnabled()); +#if defined(ENABLE_MODERN_GL) && defined(ENABLE_SW_RENDERER) + int rendererPref = bsLoadRendererPreference(); + if (rendererPref == BS_RENDERER_MODERN_GL && bsSupportsGLES2()) { + snprintf(g_rendererArg, sizeof(g_rendererArg), "modern-gl"); + } else { + snprintf(g_rendererArg, sizeof(g_rendererArg), "software"); + } +#elif defined(ENABLE_MODERN_GL) + snprintf(g_rendererArg, sizeof(g_rendererArg), "modern-gl"); +#elif defined(ENABLE_SW_RENDERER) + snprintf(g_rendererArg, sizeof(g_rendererArg), "software"); +#else + /* Neither renderer available - this shouldn't happen */ + snprintf(g_rendererArg, sizeof(g_rendererArg), "software"); +#endif + atomic_store(&quitRequested, false); CGRect bounds = [[UIScreen mainScreen] bounds]; diff --git a/src/desktop/main.c b/src/desktop/main.c index b5debd5e9..502f0bbe2 100644 --- a/src/desktop/main.c +++ b/src/desktop/main.c @@ -118,10 +118,11 @@ static int platformInitGlad(GLADloadproc load) { } else return 0; // Load OpenGL function pointers via GLAD - // This will need to be modified if we ever want to support GLES 1.x if (version && strstr(version, "OpenGL ES")) { + bool ret = gladLoadGLES1Loader(platformGetProcAddress); + if (ret) fprintf(stderr, "glad gles 1 good\n"); if (!gladLoadGLES2Loader(platformGetProcAddress)) - return 0; + return ret ? 3 : 0; return 2; } else { if (!gladLoadGLLoader(platformGetProcAddress)) From c000065b07107deb8d5a4e0de6fc27f165babe55 Mon Sep 17 00:00:00 2001 From: Un1q32 Date: Tue, 7 Jul 2026 06:22:50 -0400 Subject: [PATCH 052/109] ok --- src/desktop/backends/ios.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/desktop/backends/ios.c b/src/desktop/backends/ios.c index 5f9940007..5ea5382a0 100755 --- a/src/desktop/backends/ios.c +++ b/src/desktop/backends/ios.c @@ -451,6 +451,8 @@ void platformInitFunctions(Runner *runner) { if (gfx == SOFTWARE) { glGenFramebuffers = glGenFramebuffersOES; glGenRenderbuffers = glGenRenderbuffersOES; + glBindFramebuffer = glBindFramebufferOES; + glBindRenderbuffer = glBindRenderbufferOES; fprintf(stderr, "glGenTextures: %p\n", glGenTextures); glGenTextures(1, &swTexture); } From e386fbf097800bad420a6a2dacb30425552d67be Mon Sep 17 00:00:00 2001 From: Un1q32 Date: Tue, 7 Jul 2026 06:29:08 -0400 Subject: [PATCH 053/109] more --- src/desktop/backends/ios.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/desktop/backends/ios.c b/src/desktop/backends/ios.c index 5ea5382a0..b1ee9a8c6 100755 --- a/src/desktop/backends/ios.c +++ b/src/desktop/backends/ios.c @@ -453,6 +453,8 @@ void platformInitFunctions(Runner *runner) { glGenRenderbuffers = glGenRenderbuffersOES; glBindFramebuffer = glBindFramebufferOES; glBindRenderbuffer = glBindRenderbufferOES; + glGetRenderbufferParameteriv = glGetRenderbufferParameterivOES; + glFramebufferRenderbuffer = glFramebufferRenderbufferOES; fprintf(stderr, "glGenTextures: %p\n", glGenTextures); glGenTextures(1, &swTexture); } From 9f8ea51195bfa2c09b748efb0cab321fa5d52737 Mon Sep 17 00:00:00 2001 From: Un1q32 Date: Tue, 7 Jul 2026 06:32:00 -0400 Subject: [PATCH 054/109] maybe this will work --- src/desktop/backends/ios.c | 28 ++++++++++++++++++++++------ 1 file changed, 22 insertions(+), 6 deletions(-) diff --git a/src/desktop/backends/ios.c b/src/desktop/backends/ios.c index b1ee9a8c6..25c42826b 100755 --- a/src/desktop/backends/ios.c +++ b/src/desktop/backends/ios.c @@ -501,12 +501,28 @@ void platformSwapBuffers(void) { glLoadIdentity(); glColor4f(1, 1, 1, 1); - glBegin(GL_QUADS); - glTexCoord2f(0, 0); glVertex2f(0, 0); - glTexCoord2f(1, 0); glVertex2f(fbWidth, 0); - glTexCoord2f(1, 1); glVertex2f(fbWidth, fbHeight); - glTexCoord2f(0, 1); glVertex2f(0, fbHeight); - glEnd(); + + /* GLES 1.1 doesn't support glBegin/glEnd, use vertex arrays instead */ + GLfloat vertices[] = { + 0, 0, + fbWidth, 0, + fbWidth, fbHeight, + 0, fbHeight + }; + GLfloat texCoords[] = { + 0, 0, + 1, 0, + 1, 1, + 0, 1 + }; + + glEnableClientState(GL_VERTEX_ARRAY); + glEnableClientState(GL_TEXTURE_COORD_ARRAY); + glVertexPointer(2, GL_FLOAT, 0, vertices); + glTexCoordPointer(2, GL_FLOAT, 0, texCoords); + glDrawArrays(GL_TRIANGLE_FAN, 0, 4); + glDisableClientState(GL_VERTEX_ARRAY); + glDisableClientState(GL_TEXTURE_COORD_ARRAY); nextFb = NULL; } From dedca18bcab4667f7e6dc08e01f8fc10af04d6b1 Mon Sep 17 00:00:00 2001 From: Un1q32 Date: Tue, 7 Jul 2026 06:38:00 -0400 Subject: [PATCH 055/109] bullshit --- src/desktop/backends/ios.c | 79 ++++++++++++++++++++++++++------------ 1 file changed, 55 insertions(+), 24 deletions(-) diff --git a/src/desktop/backends/ios.c b/src/desktop/backends/ios.c index 25c42826b..dff3e0e8c 100755 --- a/src/desktop/backends/ios.c +++ b/src/desktop/backends/ios.c @@ -37,6 +37,16 @@ static atomic_bool quitRequested = false; static Runner *g_runner; +#ifdef ENABLE_SW_RENDERER +static int NearestPO2(int i) { + for (int j = 1; j < 1024 * 1024; j *= 2) { + if (i < j) + return j; + } + return i; /* fallback */ +} +#endif + static EAGLContext *glcontext; static GLuint framebuffer; static GLuint renderbuffer; @@ -48,6 +58,8 @@ static CAEAGLLayer *layer; #ifdef ENABLE_SW_RENDERER static uint32_t* nextFb = NULL; static GLuint swTexture = 0; +static uint32_t* swFbCopy = NULL; +static int swFbCopyWidth = 0, swFbCopyHeight = 0; #endif /* Requested render resolution, as set via platformSetWindowSize() (and @@ -309,6 +321,7 @@ void platformExit(void) { if (renderbuffer) glDeleteRenderbuffers(1, &renderbuffer); #ifdef ENABLE_SW_RENDERER if (swTexture) glDeleteTextures(1, &swTexture); + if (swFbCopy) free(swFbCopy); #endif [glcontext release]; glcontext = nil; @@ -474,53 +487,71 @@ void Runner_setNextFrame(uint32_t* framebuffer, int width, int height) { nextFb = framebuffer; fbWidth = width; fbHeight = height; + + /* Allocate power-of-2 sized buffer for texture upload */ + int glWidth = NearestPO2(fbWidth), glHeight = NearestPO2(fbHeight); + if (swFbCopyWidth != glWidth || swFbCopyHeight != glHeight) { + if (swFbCopy) + free(swFbCopy); + size_t rfbSize = sizeof(uint32_t) * glWidth * glHeight; + swFbCopy = malloc(rfbSize); + memset(swFbCopy, 0, rfbSize); + swFbCopyWidth = glWidth; + swFbCopyHeight = glHeight; + } } #endif void platformSwapBuffers(void) { #ifdef ENABLE_SW_RENDERER - if (gfx == SOFTWARE && nextFb) { + if (gfx == SOFTWARE && nextFb && swFbCopy) { glBindFramebuffer(GL_FRAMEBUFFER, 0); glBindTexture(GL_TEXTURE_2D, swTexture); - glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, fbWidth, fbHeight, 0, GL_BGRA, GL_UNSIGNED_BYTE, nextFb); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP); - glEnable(GL_TEXTURE_2D); - glDisable(GL_BLEND); + /* Copy and swap RGB bytes (BGRA to RGBA) */ + for (int y = 0; y < fbHeight; y++) { + uint32_t* dstline = swFbCopy + y * swFbCopyWidth; + const uint32_t* srcline = nextFb + y * fbWidth; + for (int x = 0; x < fbWidth; x++) { + uint32_t swapped = srcline[x]; + swapped = (swapped & 0xFF00FF00) | ((swapped & 0xFF) << 16) | ((swapped & 0xFF0000) >> 16); + dstline[x] = swapped; + } + } - glMatrixMode(GL_PROJECTION); - glLoadIdentity(); - glOrthof(0, fbWidth, 0, fbHeight, -1, 1); + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, swFbCopyWidth, swFbCopyHeight, 0, GL_RGBA, GL_UNSIGNED_BYTE, swFbCopy); - glMatrixMode(GL_MODELVIEW); - glLoadIdentity(); + glEnable(GL_TEXTURE_2D); + glDisable(GL_BLEND); - glColor4f(1, 1, 1, 1); + /* Calculate texture coordinate scaling */ + CGRect bounds = [[UIScreen mainScreen] bounds]; + float xs = (float)swFbCopyWidth / bounds.size.width; + float ys = (float)swFbCopyHeight / bounds.size.height; - /* GLES 1.1 doesn't support glBegin/glEnd, use vertex arrays instead */ + /* Interleaved vertex and texture coordinate arrays */ GLfloat vertices[] = { - 0, 0, - fbWidth, 0, - fbWidth, fbHeight, - 0, fbHeight - }; - GLfloat texCoords[] = { - 0, 0, - 1, 0, - 1, 1, - 0, 1 + /* tri 1 */ + -1, -1, 0, 1.0f / ys, + -1, 1, 0, 0, + 1, 1, 1.0f / xs, 0, + /* tri 2 */ + -1, -1, 0, 1.0f / ys, + 1, 1, 1.0f / xs, 0, + 1, -1, 1.0f / xs, 1.0f / ys, }; glEnableClientState(GL_VERTEX_ARRAY); glEnableClientState(GL_TEXTURE_COORD_ARRAY); - glVertexPointer(2, GL_FLOAT, 0, vertices); - glTexCoordPointer(2, GL_FLOAT, 0, texCoords); - glDrawArrays(GL_TRIANGLE_FAN, 0, 4); + glVertexPointer(2, GL_FLOAT, 4 * sizeof(float), vertices); + glTexCoordPointer(2, GL_FLOAT, 4 * sizeof(float), vertices + 2); + glDrawArrays(GL_TRIANGLES, 0, 6); glDisableClientState(GL_VERTEX_ARRAY); glDisableClientState(GL_TEXTURE_COORD_ARRAY); From d6b6171744f51bbf0839d91cb99362bdc61abd0c Mon Sep 17 00:00:00 2001 From: Un1q32 Date: Tue, 7 Jul 2026 06:48:45 -0400 Subject: [PATCH 056/109] this might do something --- src/desktop/backends/ios.c | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/desktop/backends/ios.c b/src/desktop/backends/ios.c index dff3e0e8c..0c70982bc 100755 --- a/src/desktop/backends/ios.c +++ b/src/desktop/backends/ios.c @@ -506,7 +506,8 @@ void Runner_setNextFrame(uint32_t* framebuffer, int width, int height) { void platformSwapBuffers(void) { #ifdef ENABLE_SW_RENDERER if (gfx == SOFTWARE && nextFb && swFbCopy) { - glBindFramebuffer(GL_FRAMEBUFFER, 0); + glBindFramebuffer(GL_FRAMEBUFFER, framebuffer); + glClear(GL_COLOR_BUFFER_BIT); glBindTexture(GL_TEXTURE_2D, swTexture); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); @@ -514,18 +515,16 @@ void platformSwapBuffers(void) { glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP); - /* Copy and swap RGB bytes (BGRA to RGBA) */ + /* Copy to power-of-2 buffer */ for (int y = 0; y < fbHeight; y++) { uint32_t* dstline = swFbCopy + y * swFbCopyWidth; const uint32_t* srcline = nextFb + y * fbWidth; for (int x = 0; x < fbWidth; x++) { - uint32_t swapped = srcline[x]; - swapped = (swapped & 0xFF00FF00) | ((swapped & 0xFF) << 16) | ((swapped & 0xFF0000) >> 16); - dstline[x] = swapped; + dstline[x] = srcline[x]; } } - glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, swFbCopyWidth, swFbCopyHeight, 0, GL_RGBA, GL_UNSIGNED_BYTE, swFbCopy); + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, swFbCopyWidth, swFbCopyHeight, 0, GL_BGRA, GL_UNSIGNED_BYTE, swFbCopy); glEnable(GL_TEXTURE_2D); glDisable(GL_BLEND); From 7dbdc29a0f19124a73b6d53074570c5ec17d04a7 Mon Sep 17 00:00:00 2001 From: Un1q32 Date: Tue, 7 Jul 2026 07:11:56 -0400 Subject: [PATCH 057/109] copied manually --- src/desktop/backends/ios.c | 50 +++++++++++++++++--------------------- 1 file changed, 22 insertions(+), 28 deletions(-) diff --git a/src/desktop/backends/ios.c b/src/desktop/backends/ios.c index 0c70982bc..589c1eaac 100755 --- a/src/desktop/backends/ios.c +++ b/src/desktop/backends/ios.c @@ -494,7 +494,7 @@ void Runner_setNextFrame(uint32_t* framebuffer, int width, int height) { if (swFbCopy) free(swFbCopy); size_t rfbSize = sizeof(uint32_t) * glWidth * glHeight; - swFbCopy = malloc(rfbSize); + swFbCopy = safeMalloc(rfbSize); memset(swFbCopy, 0, rfbSize); swFbCopyWidth = glWidth; swFbCopyHeight = glHeight; @@ -509,52 +509,46 @@ void platformSwapBuffers(void) { glBindFramebuffer(GL_FRAMEBUFFER, framebuffer); glClear(GL_COLOR_BUFFER_BIT); + glEnable(GL_TEXTURE_2D); glBindTexture(GL_TEXTURE_2D, swTexture); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP); + + float xs = swFbCopyWidth / fbWidth; + float ys = swFbCopyHeight / fbHeight; /* Copy to power-of-2 buffer */ - for (int y = 0; y < fbHeight; y++) { + for (int y = 0; y < fbHeight; ++y) { uint32_t* dstline = swFbCopy + y * swFbCopyWidth; const uint32_t* srcline = nextFb + y * fbWidth; for (int x = 0; x < fbWidth; x++) { dstline[x] = srcline[x]; } } + nextFb = NULL; glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, swFbCopyWidth, swFbCopyHeight, 0, GL_BGRA, GL_UNSIGNED_BYTE, swFbCopy); - glEnable(GL_TEXTURE_2D); - glDisable(GL_BLEND); - - /* Calculate texture coordinate scaling */ - CGRect bounds = [[UIScreen mainScreen] bounds]; - float xs = (float)swFbCopyWidth / bounds.size.width; - float ys = (float)swFbCopyHeight / bounds.size.height; - - /* Interleaved vertex and texture coordinate arrays */ - GLfloat vertices[] = { - /* tri 1 */ - -1, -1, 0, 1.0f / ys, - -1, 1, 0, 0, - 1, 1, 1.0f / xs, 0, - /* tri 2 */ - -1, -1, 0, 1.0f / ys, - 1, 1, 1.0f / xs, 0, - 1, -1, 1.0f / xs, 1.0f / ys, - }; - glEnableClientState(GL_VERTEX_ARRAY); glEnableClientState(GL_TEXTURE_COORD_ARRAY); + GLfloat vertices[] = { + // tri 1 + -1, -1, 0, 1.0f / ys, + -1, 1, 0, 0, + 1, 1, 1.0f / xs, 0, + // tri 2 + -1, -1, 0, 1.0f / ys, + 1, 1, 1.0f / xs, 0, + 1, -1, 1.0f / xs, 1.0f / ys, + }; + + // count, type, stride, pointer glVertexPointer(2, GL_FLOAT, 4 * sizeof(float), vertices); glTexCoordPointer(2, GL_FLOAT, 4 * sizeof(float), vertices + 2); glDrawArrays(GL_TRIANGLES, 0, 6); + + glDisable(GL_TEXTURE_2D); glDisableClientState(GL_VERTEX_ARRAY); glDisableClientState(GL_TEXTURE_COORD_ARRAY); - - nextFb = NULL; + glBindRenderbuffer(GL_RENDERBUFFER, colorRenderbuffer); } #endif [glcontext presentRenderbuffer:GL_RENDERBUFFER]; From eae1bee1460f84fe8c8213872d786642df1442c6 Mon Sep 17 00:00:00 2001 From: Un1q32 Date: Tue, 7 Jul 2026 07:13:22 -0400 Subject: [PATCH 058/109] fix --- src/desktop/backends/ios.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/desktop/backends/ios.c b/src/desktop/backends/ios.c index 589c1eaac..fb4727a97 100755 --- a/src/desktop/backends/ios.c +++ b/src/desktop/backends/ios.c @@ -548,7 +548,7 @@ void platformSwapBuffers(void) { glDisable(GL_TEXTURE_2D); glDisableClientState(GL_VERTEX_ARRAY); glDisableClientState(GL_TEXTURE_COORD_ARRAY); - glBindRenderbuffer(GL_RENDERBUFFER, colorRenderbuffer); + glBindRenderbuffer(GL_RENDERBUFFER, renderbuffer); } #endif [glcontext presentRenderbuffer:GL_RENDERBUFFER]; From 0e7b7c9bee2c612ba9cd7487942081d0bccc5e4b Mon Sep 17 00:00:00 2001 From: Un1q32 Date: Tue, 7 Jul 2026 07:21:52 -0400 Subject: [PATCH 059/109] ok --- src/desktop/backends/ios.c | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/desktop/backends/ios.c b/src/desktop/backends/ios.c index fb4727a97..cc564c472 100755 --- a/src/desktop/backends/ios.c +++ b/src/desktop/backends/ios.c @@ -506,7 +506,6 @@ void Runner_setNextFrame(uint32_t* framebuffer, int width, int height) { void platformSwapBuffers(void) { #ifdef ENABLE_SW_RENDERER if (gfx == SOFTWARE && nextFb && swFbCopy) { - glBindFramebuffer(GL_FRAMEBUFFER, framebuffer); glClear(GL_COLOR_BUFFER_BIT); glEnable(GL_TEXTURE_2D); @@ -548,7 +547,6 @@ void platformSwapBuffers(void) { glDisable(GL_TEXTURE_2D); glDisableClientState(GL_VERTEX_ARRAY); glDisableClientState(GL_TEXTURE_COORD_ARRAY); - glBindRenderbuffer(GL_RENDERBUFFER, renderbuffer); } #endif [glcontext presentRenderbuffer:GL_RENDERBUFFER]; From a8915eb1bc95077fe4301cc9d5ce6f53778f8bcb Mon Sep 17 00:00:00 2001 From: Un1q32 Date: Tue, 7 Jul 2026 07:26:09 -0400 Subject: [PATCH 060/109] plea --- src/desktop/backends/ios.c | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/src/desktop/backends/ios.c b/src/desktop/backends/ios.c index cc564c472..f84606084 100755 --- a/src/desktop/backends/ios.c +++ b/src/desktop/backends/ios.c @@ -421,20 +421,6 @@ static void applyRenderScale(void) { } static void resizeFramebuffer(void) { -#ifdef ENABLE_SW_RENDERER - if (gfx == SOFTWARE) { - applyRenderScale(); - - glBindRenderbuffer(GL_RENDERBUFFER, renderbuffer); - [glcontext renderbufferStorage:GL_RENDERBUFFER fromDrawable:layer]; - glGetRenderbufferParameteriv(GL_RENDERBUFFER, GL_RENDERBUFFER_WIDTH, &fbWidth); - glGetRenderbufferParameteriv(GL_RENDERBUFFER, GL_RENDERBUFFER_HEIGHT, &fbHeight); - - glViewport(0, 0, fbWidth, fbHeight); - return; - } -#endif - applyRenderScale(); glBindFramebuffer(GL_FRAMEBUFFER, framebuffer); From c551fc3f8ab12801abc294f1ab1ff9333094bc94 Mon Sep 17 00:00:00 2001 From: Un1q32 Date: Tue, 7 Jul 2026 07:28:48 -0400 Subject: [PATCH 061/109] show me something --- src/desktop/backends/ios.c | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/desktop/backends/ios.c b/src/desktop/backends/ios.c index f84606084..d0bdec4c9 100755 --- a/src/desktop/backends/ios.c +++ b/src/desktop/backends/ios.c @@ -454,7 +454,6 @@ void platformInitFunctions(Runner *runner) { glBindRenderbuffer = glBindRenderbufferOES; glGetRenderbufferParameteriv = glGetRenderbufferParameterivOES; glFramebufferRenderbuffer = glFramebufferRenderbufferOES; - fprintf(stderr, "glGenTextures: %p\n", glGenTextures); glGenTextures(1, &swTexture); } #endif @@ -510,7 +509,7 @@ void platformSwapBuffers(void) { } nextFb = NULL; - glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, swFbCopyWidth, swFbCopyHeight, 0, GL_BGRA, GL_UNSIGNED_BYTE, swFbCopy); + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, swFbCopyWidth, swFbCopyHeight, 0, GL_RGBA, GL_UNSIGNED_BYTE, swFbCopy); glEnableClientState(GL_VERTEX_ARRAY); glEnableClientState(GL_TEXTURE_COORD_ARRAY); From 5485e64e5062ee796e7004d957565666b4065cb9 Mon Sep 17 00:00:00 2001 From: Un1q32 Date: Tue, 7 Jul 2026 07:31:56 -0400 Subject: [PATCH 062/109] ok --- src/desktop/backends/ios.c | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/desktop/backends/ios.c b/src/desktop/backends/ios.c index d0bdec4c9..6c97b3c07 100755 --- a/src/desktop/backends/ios.c +++ b/src/desktop/backends/ios.c @@ -504,7 +504,9 @@ void platformSwapBuffers(void) { uint32_t* dstline = swFbCopy + y * swFbCopyWidth; const uint32_t* srcline = nextFb + y * fbWidth; for (int x = 0; x < fbWidth; x++) { - dstline[x] = srcline[x]; + uint32_t swapped = srcline[x]; + swapped = (swapped & 0xFF00FF00) | ((swapped & 0xFF) << 16) | ((swapped & 0xFF0000) >> 16); + dstline[x] = swapped; } } nextFb = NULL; From 6c16448adf9d463e86b5a7585e4cdf174837cb4c Mon Sep 17 00:00:00 2001 From: Un1q32 Date: Tue, 7 Jul 2026 07:39:38 -0400 Subject: [PATCH 063/109] something --- src/desktop/backends/ios.c | 23 +++++++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/src/desktop/backends/ios.c b/src/desktop/backends/ios.c index 6c97b3c07..cf4f67c56 100755 --- a/src/desktop/backends/ios.c +++ b/src/desktop/backends/ios.c @@ -455,6 +455,23 @@ void platformInitFunctions(Runner *runner) { glGetRenderbufferParameteriv = glGetRenderbufferParameterivOES; glFramebufferRenderbuffer = glFramebufferRenderbufferOES; glGenTextures(1, &swTexture); + + /* GLES defaults GL_TEXTURE_MIN_FILTER to + * GL_NEAREST_MIPMAP_LINEAR, which requires a complete mipmap + * chain. platformSwapBuffers() only ever uploads a single base + * level via glTexImage2D, so without overriding this the + * texture is "incomplete" per the GLES spec -- and on iOS's + * PowerVR-derived driver stack, sampling an incomplete + * texture returns solid white rather than black. That's the + * pure-white-screen bug: set filtering (no mipmaps needed) + * and clamp-to-edge wrap (harmless, and avoids any GL_REPEAT + * seam artifacts from the unused padding in the PO2 buffer) + * once, right after the texture object is created. */ + glBindTexture(GL_TEXTURE_2D, swTexture); + 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); } #endif glGenFramebuffers(1, &framebuffer); @@ -504,14 +521,12 @@ void platformSwapBuffers(void) { uint32_t* dstline = swFbCopy + y * swFbCopyWidth; const uint32_t* srcline = nextFb + y * fbWidth; for (int x = 0; x < fbWidth; x++) { - uint32_t swapped = srcline[x]; - swapped = (swapped & 0xFF00FF00) | ((swapped & 0xFF) << 16) | ((swapped & 0xFF0000) >> 16); - dstline[x] = swapped; + dstline[x] = srcline[x]; } } nextFb = NULL; - glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, swFbCopyWidth, swFbCopyHeight, 0, GL_RGBA, GL_UNSIGNED_BYTE, swFbCopy); + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, swFbCopyWidth, swFbCopyHeight, 0, GL_BGRA, GL_UNSIGNED_BYTE, swFbCopy); glEnableClientState(GL_VERTEX_ARRAY); glEnableClientState(GL_TEXTURE_COORD_ARRAY); From 65654ebabb0d53ee18954d7b4e119388f34b6fda Mon Sep 17 00:00:00 2001 From: Un1q32 Date: Tue, 7 Jul 2026 07:44:15 -0400 Subject: [PATCH 064/109] fuxk yes --- src/desktop/backends/ios.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/desktop/backends/ios.c b/src/desktop/backends/ios.c index cf4f67c56..2d19b975b 100755 --- a/src/desktop/backends/ios.c +++ b/src/desktop/backends/ios.c @@ -513,8 +513,8 @@ void platformSwapBuffers(void) { glEnable(GL_TEXTURE_2D); glBindTexture(GL_TEXTURE_2D, swTexture); - float xs = swFbCopyWidth / fbWidth; - float ys = swFbCopyHeight / fbHeight; + float xs = (float)swFbCopyWidth / (float)fbWidth; + float ys = (float)swFbCopyHeight / (float)fbHeight; /* Copy to power-of-2 buffer */ for (int y = 0; y < fbHeight; ++y) { From 46c066013afbc146a0182a194fde83d7fec49b48 Mon Sep 17 00:00:00 2001 From: Un1q32 Date: Tue, 7 Jul 2026 07:59:16 -0400 Subject: [PATCH 065/109] useless path --- src/desktop/backends/ios.c | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/desktop/backends/ios.c b/src/desktop/backends/ios.c index 2d19b975b..389f70029 100755 --- a/src/desktop/backends/ios.c +++ b/src/desktop/backends/ios.c @@ -1326,10 +1326,7 @@ extern int game_main(int argc, char *argv[]); } #elif defined(ENABLE_MODERN_GL) snprintf(g_rendererArg, sizeof(g_rendererArg), "modern-gl"); -#elif defined(ENABLE_SW_RENDERER) - snprintf(g_rendererArg, sizeof(g_rendererArg), "software"); #else - /* Neither renderer available - this shouldn't happen */ snprintf(g_rendererArg, sizeof(g_rendererArg), "software"); #endif From e1d4fd48bd90ea4eb5cc924e8d4ba9b3d2cd7f02 Mon Sep 17 00:00:00 2001 From: Un1q32 Date: Tue, 7 Jul 2026 08:18:44 -0400 Subject: [PATCH 066/109] fix crash --- src/desktop/backends/ios.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/desktop/backends/ios.c b/src/desktop/backends/ios.c index 389f70029..45a48b027 100755 --- a/src/desktop/backends/ios.c +++ b/src/desktop/backends/ios.c @@ -454,6 +454,8 @@ void platformInitFunctions(Runner *runner) { glBindRenderbuffer = glBindRenderbufferOES; glGetRenderbufferParameteriv = glGetRenderbufferParameterivOES; glFramebufferRenderbuffer = glFramebufferRenderbufferOES; + glDeleteFramebuffers = glDeleteFramebuffersOES; + glDeleteRenderbuffers = glDeleteRenderbuffersOES; glGenTextures(1, &swTexture); /* GLES defaults GL_TEXTURE_MIN_FILTER to From a6846e215d2618a62da9712f57296b1c791fee2a Mon Sep 17 00:00:00 2001 From: Un1q32 Date: Tue, 7 Jul 2026 17:47:52 -0400 Subject: [PATCH 067/109] memcpy --- src/desktop/backends/ios.c | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/desktop/backends/ios.c b/src/desktop/backends/ios.c index 45a48b027..93cf0e895 100755 --- a/src/desktop/backends/ios.c +++ b/src/desktop/backends/ios.c @@ -522,9 +522,7 @@ void platformSwapBuffers(void) { for (int y = 0; y < fbHeight; ++y) { uint32_t* dstline = swFbCopy + y * swFbCopyWidth; const uint32_t* srcline = nextFb + y * fbWidth; - for (int x = 0; x < fbWidth; x++) { - dstline[x] = srcline[x]; - } + memcpy(dstline, srcline, fbWidth); } nextFb = NULL; From adc07ad0235c3435ff5b27d9d7382a786db049f2 Mon Sep 17 00:00:00 2001 From: Un1q32 Date: Tue, 7 Jul 2026 17:52:38 -0400 Subject: [PATCH 068/109] format --- src/desktop/backends/ios.c | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/src/desktop/backends/ios.c b/src/desktop/backends/ios.c index 93cf0e895..51554920e 100755 --- a/src/desktop/backends/ios.c +++ b/src/desktop/backends/ios.c @@ -456,19 +456,8 @@ void platformInitFunctions(Runner *runner) { glFramebufferRenderbuffer = glFramebufferRenderbufferOES; glDeleteFramebuffers = glDeleteFramebuffersOES; glDeleteRenderbuffers = glDeleteRenderbuffersOES; - glGenTextures(1, &swTexture); - /* GLES defaults GL_TEXTURE_MIN_FILTER to - * GL_NEAREST_MIPMAP_LINEAR, which requires a complete mipmap - * chain. platformSwapBuffers() only ever uploads a single base - * level via glTexImage2D, so without overriding this the - * texture is "incomplete" per the GLES spec -- and on iOS's - * PowerVR-derived driver stack, sampling an incomplete - * texture returns solid white rather than black. That's the - * pure-white-screen bug: set filtering (no mipmaps needed) - * and clamp-to-edge wrap (harmless, and avoids any GL_REPEAT - * seam artifacts from the unused padding in the PO2 buffer) - * once, right after the texture object is created. */ + glGenTextures(1, &swTexture); glBindTexture(GL_TEXTURE_2D, swTexture); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); From ea328e33ead184d3d47e994d9d63de5f95a4d9d4 Mon Sep 17 00:00:00 2001 From: Un1q32 Date: Tue, 7 Jul 2026 17:56:03 -0400 Subject: [PATCH 069/109] thuing --- src/desktop/backends/ios.c | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/src/desktop/backends/ios.c b/src/desktop/backends/ios.c index 51554920e..cf255645d 100755 --- a/src/desktop/backends/ios.c +++ b/src/desktop/backends/ios.c @@ -37,16 +37,6 @@ static atomic_bool quitRequested = false; static Runner *g_runner; -#ifdef ENABLE_SW_RENDERER -static int NearestPO2(int i) { - for (int j = 1; j < 1024 * 1024; j *= 2) { - if (i < j) - return j; - } - return i; /* fallback */ -} -#endif - static EAGLContext *glcontext; static GLuint framebuffer; static GLuint renderbuffer; @@ -476,6 +466,14 @@ void platformInitFunctions(Runner *runner) { #ifdef ENABLE_SW_RENDERER +static GLint NearestPO2(GLint i) { + for (GLint j = 1; j < 1024 * 1024; j *= 2) + if (i < j) + return j; + assert(!"this shouldn't happen"); + return i; /* fallback */ +} + void Runner_setNextFrame(uint32_t* framebuffer, int width, int height) { nextFb = framebuffer; fbWidth = width; From 863e891cb071f5ea1b366fc5e24edac0c4322c7b Mon Sep 17 00:00:00 2001 From: Un1q32 Date: Tue, 7 Jul 2026 18:10:20 -0400 Subject: [PATCH 070/109] fix shit --- src/desktop/backends/ios.c | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/desktop/backends/ios.c b/src/desktop/backends/ios.c index cf255645d..07476095b 100755 --- a/src/desktop/backends/ios.c +++ b/src/desktop/backends/ios.c @@ -486,7 +486,6 @@ void Runner_setNextFrame(uint32_t* framebuffer, int width, int height) { free(swFbCopy); size_t rfbSize = sizeof(uint32_t) * glWidth * glHeight; swFbCopy = safeMalloc(rfbSize); - memset(swFbCopy, 0, rfbSize); swFbCopyWidth = glWidth; swFbCopyHeight = glHeight; } @@ -509,7 +508,7 @@ void platformSwapBuffers(void) { for (int y = 0; y < fbHeight; ++y) { uint32_t* dstline = swFbCopy + y * swFbCopyWidth; const uint32_t* srcline = nextFb + y * fbWidth; - memcpy(dstline, srcline, fbWidth); + memcpy(dstline, srcline, fbWidth * sizeof(uint32_t)); } nextFb = NULL; From 07abc772b59c9ba05dba27652ddbbd36c7da1c8d Mon Sep 17 00:00:00 2001 From: Un1q32 Date: Tue, 7 Jul 2026 18:21:58 -0400 Subject: [PATCH 071/109] fix crash --- src/desktop/backends/ios.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/desktop/backends/ios.c b/src/desktop/backends/ios.c index 07476095b..b7c3c6074 100755 --- a/src/desktop/backends/ios.c +++ b/src/desktop/backends/ios.c @@ -312,6 +312,8 @@ void platformExit(void) { #ifdef ENABLE_SW_RENDERER if (swTexture) glDeleteTextures(1, &swTexture); if (swFbCopy) free(swFbCopy); + swFbCopy = NULL; + swTexture = 0; #endif [glcontext release]; glcontext = nil; From 4f95614aed66515a7818928987f523372c039532 Mon Sep 17 00:00:00 2001 From: Un1q32 Date: Tue, 7 Jul 2026 18:22:30 -0400 Subject: [PATCH 072/109] better --- src/desktop/backends/ios.c | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/desktop/backends/ios.c b/src/desktop/backends/ios.c index b7c3c6074..4163a6e4e 100755 --- a/src/desktop/backends/ios.c +++ b/src/desktop/backends/ios.c @@ -311,9 +311,11 @@ void platformExit(void) { if (renderbuffer) glDeleteRenderbuffers(1, &renderbuffer); #ifdef ENABLE_SW_RENDERER if (swTexture) glDeleteTextures(1, &swTexture); - if (swFbCopy) free(swFbCopy); - swFbCopy = NULL; swTexture = 0; + if (swFbCopy) { + free(swFbCopy); + swFbCopy = NULL; + } #endif [glcontext release]; glcontext = nil; From 54fdf61542e90c703be4d94a3bf78a9b0823a876 Mon Sep 17 00:00:00 2001 From: Un1q32 Date: Tue, 7 Jul 2026 18:25:20 -0400 Subject: [PATCH 073/109] fix --- src/desktop/backends/ios.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/desktop/backends/ios.c b/src/desktop/backends/ios.c index 4163a6e4e..973535b38 100755 --- a/src/desktop/backends/ios.c +++ b/src/desktop/backends/ios.c @@ -281,10 +281,11 @@ bool platformInit(int32_t reqW, int32_t reqH, const char *title, bool headless) bsRequestRelayout(); #ifdef ENABLE_MODERN_GL - if (gfx == MODERN_GL) + if (gfx == MODERN_GL) { glcontext = [[EAGLContext alloc] initWithAPI:3]; if (!glcontext) glcontext = [[EAGLContext alloc] initWithAPI:2]; + } #endif #ifdef ENABLE_SW_RENDERER if (gfx == SOFTWARE) From 34f78e2133e6f01eb840255bc51830332026891e Mon Sep 17 00:00:00 2001 From: Un1q32 Date: Tue, 7 Jul 2026 18:27:49 -0400 Subject: [PATCH 074/109] nits --- src/desktop/backends/ios.c | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/src/desktop/backends/ios.c b/src/desktop/backends/ios.c index 973535b38..7323e6d28 100755 --- a/src/desktop/backends/ios.c +++ b/src/desktop/backends/ios.c @@ -308,11 +308,19 @@ bool platformInit(int32_t reqW, int32_t reqH, const char *title, bool headless) } void platformExit(void) { - if (framebuffer) glDeleteFramebuffers(1, &framebuffer); - if (renderbuffer) glDeleteRenderbuffers(1, &renderbuffer); + if (framebuffer) { + glDeleteFramebuffers(1, &framebuffer); + framebuffer = 0; + } + if (renderbuffer) { + glDeleteRenderbuffers(1, &renderbuffer); + renderbuffer = 0; + } #ifdef ENABLE_SW_RENDERER - if (swTexture) glDeleteTextures(1, &swTexture); - swTexture = 0; + if (swTexture) { + glDeleteTextures(1, &swTexture); + swTexture = 0; + } if (swFbCopy) { free(swFbCopy); swFbCopy = NULL; @@ -320,8 +328,6 @@ void platformExit(void) { #endif [glcontext release]; glcontext = nil; - framebuffer = 0; - renderbuffer = 0; glInited = false; } From a1b8177d804926ef590bf79c09bff2a907d544d5 Mon Sep 17 00:00:00 2001 From: Un1q32 Date: Tue, 7 Jul 2026 18:30:49 -0400 Subject: [PATCH 075/109] reset getopt_long before calling game_main --- src/desktop/backends/ios.c | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/desktop/backends/ios.c b/src/desktop/backends/ios.c index 7323e6d28..1e5587d12 100755 --- a/src/desktop/backends/ios.c +++ b/src/desktop/backends/ios.c @@ -1295,6 +1295,9 @@ extern int game_main(int argc, char *argv[]); - (void)gameThread { NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; + optind = 1; + optreset = 1; + static char arg0[] = "butterscotch"; static char arg1[] = "--lazy-textures"; static char arg2[] = "--lazy-rooms"; From 502bafeffdd422a82e7d08389162160b72f54a15 Mon Sep 17 00:00:00 2001 From: Un1q32 Date: Tue, 7 Jul 2026 19:15:54 -0400 Subject: [PATCH 076/109] event queue, fixes fast forward button --- src/desktop/backends/ios.c | 66 ++++++++++++++++++++++++++++++++++---- 1 file changed, 60 insertions(+), 6 deletions(-) diff --git a/src/desktop/backends/ios.c b/src/desktop/backends/ios.c index 1e5587d12..046295181 100755 --- a/src/desktop/backends/ios.c +++ b/src/desktop/backends/ios.c @@ -113,6 +113,59 @@ static void bsRequestRelayout(void) { } } +/* --------------------------------------------------------------------- + * Deferred keyboard event queue. + * + * Touch handling runs on the main UI thread, but RunnerKeyboard_onKeyDown/ + * onKeyUp mutate keyPressed/keyReleased arrays that are also cleared once + * per frame by RunnerKeyboard_beginFrame() on the game thread. Calling + * onKeyDown/onKeyUp directly from touchesBegan/touchesEnded races against + * that clear -- an event set here can be wiped by beginFrame() before + * main.c's next poll ever observes it, more easily on fast devices since + * frames (and beginFrame() calls) come more often, shrinking the window + * in which an async touch event is safe from being clobbered. + * + * Rather than touch runner_keyboard.c or main.c (both intentionally + * platform-independent), buffer transitions here and only apply them + * from platformHandleEvents() -- already polled once per frame on the + * game thread, in the same place needsResize/quitRequested are handled. + * That makes the actual RunnerKeyboard_onKeyDown/onKeyUp calls happen on + * the correct thread, safely ordered relative to that frame's + * beginFrame(), exactly as if a real keyboard event arrived there. + * + * Single-producer (UI thread only ever advances head)/single-consumer + * (game thread only ever advances tail), so the plain atomic_int + * load/store already used elsewhere in this file is sufficient -- no + * locks needed. + * ------------------------------------------------------------------- */ +#define BS_KEY_QUEUE_SIZE 32 +typedef struct { int32_t key; bool isDown; } BSKeyEvent; +static BSKeyEvent bsKeyQueue[BS_KEY_QUEUE_SIZE]; +static atomic_int bsKeyQueueHead = 0; /* next slot to write -- producer (UI thread) owned */ +static atomic_int bsKeyQueueTail = 0; /* next slot to read -- consumer (game thread) owned */ + +static void bsEnqueueKeyEvent(int32_t key, bool isDown) { + int head = atomic_load(&bsKeyQueueHead); + int next = (head + 1) % BS_KEY_QUEUE_SIZE; + if (next == atomic_load(&bsKeyQueueTail)) return; /* full -- drop rather than corrupt, shouldn't happen at touch event rates */ + bsKeyQueue[head].key = key; + bsKeyQueue[head].isDown = isDown; + atomic_store(&bsKeyQueueHead, next); +} + +static void bsDrainKeyEvents(void) { + for (;;) { + int tail = atomic_load(&bsKeyQueueTail); + if (tail == atomic_load(&bsKeyQueueHead)) break; /* empty */ + BSKeyEvent ev = bsKeyQueue[tail]; + atomic_store(&bsKeyQueueTail, (tail + 1) % BS_KEY_QUEUE_SIZE); + if (g_runner) { + if (ev.isDown) RunnerKeyboard_onKeyDown(g_runner->keyboard, ev.key); + else RunnerKeyboard_onKeyUp(g_runner->keyboard, ev.key); + } + } +} + /* --------------------------------------------------------------------- * Touch control layout * @@ -556,6 +609,7 @@ void *platformGetProcAddress(const char *name) { } bool platformHandleEvents(void) { + bsDrainKeyEvents(); if (atomic_exchange(&needsResize, false)) resizeFramebuffer(); if (atomic_load(&quitRequested)) @@ -727,9 +781,9 @@ static void drawCenteredLabel(NSString *text, CGRect rect, UIFont *font) { int32_t keys[4] = { VK_UP, VK_DOWN, VK_LEFT, VK_RIGHT }; for (int i = 0; i < 4; i++) { if (newState[i] && !dpadKeysDown[i]) { - if (g_runner) RunnerKeyboard_onKeyDown(g_runner->keyboard, keys[i]); + bsEnqueueKeyEvent(keys[i], true); } else if (!newState[i] && dpadKeysDown[i]) { - if (g_runner) RunnerKeyboard_onKeyUp(g_runner->keyboard, keys[i]); + bsEnqueueKeyEvent(keys[i], false); } dpadKeysDown[i] = newState[i] ? 1 : 0; } @@ -752,7 +806,7 @@ static void drawCenteredLabel(NSString *text, CGRect rect, UIFont *font) { if (!ffTouch && CGRectContainsPoint(CGRectInset(bsLayout.ffFrame, -6, -6), p)) { ffTouch = [touch retain]; - if (g_runner) RunnerKeyboard_onKeyDown(g_runner->keyboard, VK_TAB); + bsEnqueueKeyEvent(VK_TAB, true); [self setNeedsDisplay]; continue; } @@ -770,7 +824,7 @@ static void drawCenteredLabel(NSString *text, CGRect rect, UIFont *font) { if (!buttonTouches[i] && CGRectContainsPoint(CGRectInset(actionRects[i], -6, -6), p)) { buttonTouches[i] = [touch retain]; int32_t vk = (i == 0) ? 'Z' : (i == 1) ? 'X' : 'C'; - if (g_runner) RunnerKeyboard_onKeyDown(g_runner->keyboard, vk); + bsEnqueueKeyEvent(vk, true); [self setNeedsDisplay]; break; } @@ -811,7 +865,7 @@ static void drawCenteredLabel(NSString *text, CGRect rect, UIFont *font) { continue; } if (touch == ffTouch) { - if (g_runner) RunnerKeyboard_onKeyUp(g_runner->keyboard, VK_TAB); + bsEnqueueKeyEvent(VK_TAB, false); [ffTouch release]; ffTouch = nil; [self setNeedsDisplay]; @@ -820,7 +874,7 @@ static void drawCenteredLabel(NSString *text, CGRect rect, UIFont *font) { for (int i = 0; i < 3; i++) { if (touch == buttonTouches[i]) { int32_t vk = (i == 0) ? 'Z' : (i == 1) ? 'X' : 'C'; - if (g_runner) RunnerKeyboard_onKeyUp(g_runner->keyboard, vk); + bsEnqueueKeyEvent(vk, false); [buttonTouches[i] release]; buttonTouches[i] = nil; [self setNeedsDisplay]; From 186b874b62c1f9c2ec73654052a33a4f0fd2304b Mon Sep 17 00:00:00 2001 From: Un1q32 Date: Tue, 7 Jul 2026 20:29:27 -0400 Subject: [PATCH 077/109] move --- .../ios/assets/Default-568h@2x.png | 0 .../ios/assets/Default-667h@2x.png | 0 .../ios/assets/Default-736h@3x.png | 0 {targets => artifacts}/ios/assets/Default.png | Bin {targets => artifacts}/ios/assets/Default@2x.png | 0 {targets => artifacts}/ios/assets/Icon-20.png | Bin {targets => artifacts}/ios/assets/Icon-20@2x.png | Bin {targets => artifacts}/ios/assets/Icon-20@3x.png | Bin {targets => artifacts}/ios/assets/Icon-40.png | Bin {targets => artifacts}/ios/assets/Icon-40@2x.png | Bin {targets => artifacts}/ios/assets/Icon-40@3x.png | Bin {targets => artifacts}/ios/assets/Icon-60@2x.png | 0 {targets => artifacts}/ios/assets/Icon-60@3x.png | Bin {targets => artifacts}/ios/assets/Icon-72.png | Bin {targets => artifacts}/ios/assets/Icon-72@2x.png | Bin {targets => artifacts}/ios/assets/Icon-76.png | Bin {targets => artifacts}/ios/assets/Icon-76@2x.png | Bin {targets => artifacts}/ios/assets/Icon-83.5@2x.png | Bin {targets => artifacts}/ios/assets/Icon-Small.png | Bin {targets => artifacts}/ios/assets/Icon-Small@2x.png | Bin {targets => artifacts}/ios/assets/Icon-Small@3x.png | Bin {targets => artifacts}/ios/assets/Icon.png | Bin {targets => artifacts}/ios/assets/Icon@2x.png | Bin {targets => artifacts}/ios/assets/Info.plist | 0 {targets => artifacts}/ios/assets/iTunesArtwork | Bin {targets => artifacts}/ios/assets/iTunesArtwork@2x | Bin {targets => artifacts}/ios/build-ipa.sh | 8 ++++---- .../ios/butterscotch.entitlements | 0 28 files changed, 4 insertions(+), 4 deletions(-) rename {targets => artifacts}/ios/assets/Default-568h@2x.png (100%) rename {targets => artifacts}/ios/assets/Default-667h@2x.png (100%) rename {targets => artifacts}/ios/assets/Default-736h@3x.png (100%) rename {targets => artifacts}/ios/assets/Default.png (100%) rename {targets => artifacts}/ios/assets/Default@2x.png (100%) rename {targets => artifacts}/ios/assets/Icon-20.png (100%) rename {targets => artifacts}/ios/assets/Icon-20@2x.png (100%) rename {targets => artifacts}/ios/assets/Icon-20@3x.png (100%) rename {targets => artifacts}/ios/assets/Icon-40.png (100%) rename {targets => artifacts}/ios/assets/Icon-40@2x.png (100%) rename {targets => artifacts}/ios/assets/Icon-40@3x.png (100%) rename {targets => artifacts}/ios/assets/Icon-60@2x.png (100%) rename {targets => artifacts}/ios/assets/Icon-60@3x.png (100%) rename {targets => artifacts}/ios/assets/Icon-72.png (100%) rename {targets => artifacts}/ios/assets/Icon-72@2x.png (100%) rename {targets => artifacts}/ios/assets/Icon-76.png (100%) rename {targets => artifacts}/ios/assets/Icon-76@2x.png (100%) rename {targets => artifacts}/ios/assets/Icon-83.5@2x.png (100%) rename {targets => artifacts}/ios/assets/Icon-Small.png (100%) rename {targets => artifacts}/ios/assets/Icon-Small@2x.png (100%) rename {targets => artifacts}/ios/assets/Icon-Small@3x.png (100%) rename {targets => artifacts}/ios/assets/Icon.png (100%) rename {targets => artifacts}/ios/assets/Icon@2x.png (100%) rename {targets => artifacts}/ios/assets/Info.plist (100%) rename {targets => artifacts}/ios/assets/iTunesArtwork (100%) rename {targets => artifacts}/ios/assets/iTunesArtwork@2x (100%) rename {targets => artifacts}/ios/build-ipa.sh (55%) rename {targets => artifacts}/ios/butterscotch.entitlements (100%) diff --git a/targets/ios/assets/Default-568h@2x.png b/artifacts/ios/assets/Default-568h@2x.png similarity index 100% rename from targets/ios/assets/Default-568h@2x.png rename to artifacts/ios/assets/Default-568h@2x.png diff --git a/targets/ios/assets/Default-667h@2x.png b/artifacts/ios/assets/Default-667h@2x.png similarity index 100% rename from targets/ios/assets/Default-667h@2x.png rename to artifacts/ios/assets/Default-667h@2x.png diff --git a/targets/ios/assets/Default-736h@3x.png b/artifacts/ios/assets/Default-736h@3x.png similarity index 100% rename from targets/ios/assets/Default-736h@3x.png rename to artifacts/ios/assets/Default-736h@3x.png diff --git a/targets/ios/assets/Default.png b/artifacts/ios/assets/Default.png similarity index 100% rename from targets/ios/assets/Default.png rename to artifacts/ios/assets/Default.png diff --git a/targets/ios/assets/Default@2x.png b/artifacts/ios/assets/Default@2x.png similarity index 100% rename from targets/ios/assets/Default@2x.png rename to artifacts/ios/assets/Default@2x.png diff --git a/targets/ios/assets/Icon-20.png b/artifacts/ios/assets/Icon-20.png similarity index 100% rename from targets/ios/assets/Icon-20.png rename to artifacts/ios/assets/Icon-20.png diff --git a/targets/ios/assets/Icon-20@2x.png b/artifacts/ios/assets/Icon-20@2x.png similarity index 100% rename from targets/ios/assets/Icon-20@2x.png rename to artifacts/ios/assets/Icon-20@2x.png diff --git a/targets/ios/assets/Icon-20@3x.png b/artifacts/ios/assets/Icon-20@3x.png similarity index 100% rename from targets/ios/assets/Icon-20@3x.png rename to artifacts/ios/assets/Icon-20@3x.png diff --git a/targets/ios/assets/Icon-40.png b/artifacts/ios/assets/Icon-40.png similarity index 100% rename from targets/ios/assets/Icon-40.png rename to artifacts/ios/assets/Icon-40.png diff --git a/targets/ios/assets/Icon-40@2x.png b/artifacts/ios/assets/Icon-40@2x.png similarity index 100% rename from targets/ios/assets/Icon-40@2x.png rename to artifacts/ios/assets/Icon-40@2x.png diff --git a/targets/ios/assets/Icon-40@3x.png b/artifacts/ios/assets/Icon-40@3x.png similarity index 100% rename from targets/ios/assets/Icon-40@3x.png rename to artifacts/ios/assets/Icon-40@3x.png diff --git a/targets/ios/assets/Icon-60@2x.png b/artifacts/ios/assets/Icon-60@2x.png similarity index 100% rename from targets/ios/assets/Icon-60@2x.png rename to artifacts/ios/assets/Icon-60@2x.png diff --git a/targets/ios/assets/Icon-60@3x.png b/artifacts/ios/assets/Icon-60@3x.png similarity index 100% rename from targets/ios/assets/Icon-60@3x.png rename to artifacts/ios/assets/Icon-60@3x.png diff --git a/targets/ios/assets/Icon-72.png b/artifacts/ios/assets/Icon-72.png similarity index 100% rename from targets/ios/assets/Icon-72.png rename to artifacts/ios/assets/Icon-72.png diff --git a/targets/ios/assets/Icon-72@2x.png b/artifacts/ios/assets/Icon-72@2x.png similarity index 100% rename from targets/ios/assets/Icon-72@2x.png rename to artifacts/ios/assets/Icon-72@2x.png diff --git a/targets/ios/assets/Icon-76.png b/artifacts/ios/assets/Icon-76.png similarity index 100% rename from targets/ios/assets/Icon-76.png rename to artifacts/ios/assets/Icon-76.png diff --git a/targets/ios/assets/Icon-76@2x.png b/artifacts/ios/assets/Icon-76@2x.png similarity index 100% rename from targets/ios/assets/Icon-76@2x.png rename to artifacts/ios/assets/Icon-76@2x.png diff --git a/targets/ios/assets/Icon-83.5@2x.png b/artifacts/ios/assets/Icon-83.5@2x.png similarity index 100% rename from targets/ios/assets/Icon-83.5@2x.png rename to artifacts/ios/assets/Icon-83.5@2x.png diff --git a/targets/ios/assets/Icon-Small.png b/artifacts/ios/assets/Icon-Small.png similarity index 100% rename from targets/ios/assets/Icon-Small.png rename to artifacts/ios/assets/Icon-Small.png diff --git a/targets/ios/assets/Icon-Small@2x.png b/artifacts/ios/assets/Icon-Small@2x.png similarity index 100% rename from targets/ios/assets/Icon-Small@2x.png rename to artifacts/ios/assets/Icon-Small@2x.png diff --git a/targets/ios/assets/Icon-Small@3x.png b/artifacts/ios/assets/Icon-Small@3x.png similarity index 100% rename from targets/ios/assets/Icon-Small@3x.png rename to artifacts/ios/assets/Icon-Small@3x.png diff --git a/targets/ios/assets/Icon.png b/artifacts/ios/assets/Icon.png similarity index 100% rename from targets/ios/assets/Icon.png rename to artifacts/ios/assets/Icon.png diff --git a/targets/ios/assets/Icon@2x.png b/artifacts/ios/assets/Icon@2x.png similarity index 100% rename from targets/ios/assets/Icon@2x.png rename to artifacts/ios/assets/Icon@2x.png diff --git a/targets/ios/assets/Info.plist b/artifacts/ios/assets/Info.plist similarity index 100% rename from targets/ios/assets/Info.plist rename to artifacts/ios/assets/Info.plist diff --git a/targets/ios/assets/iTunesArtwork b/artifacts/ios/assets/iTunesArtwork similarity index 100% rename from targets/ios/assets/iTunesArtwork rename to artifacts/ios/assets/iTunesArtwork diff --git a/targets/ios/assets/iTunesArtwork@2x b/artifacts/ios/assets/iTunesArtwork@2x similarity index 100% rename from targets/ios/assets/iTunesArtwork@2x rename to artifacts/ios/assets/iTunesArtwork@2x diff --git a/targets/ios/build-ipa.sh b/artifacts/ios/build-ipa.sh similarity index 55% rename from targets/ios/build-ipa.sh rename to artifacts/ios/build-ipa.sh index e1569115d..45a80cc41 100755 --- a/targets/ios/build-ipa.sh +++ b/artifacts/ios/build-ipa.sh @@ -13,13 +13,13 @@ printf 'Creating butterscotch.ipa\n' rm -rf "$apppath" mkdir -p "$apppath" if command -v ldid >/dev/null; then - ldid -Stargets/ios/butterscotch.entitlements build/butterscotch + ldid -Sartifacts/ios/butterscotch.entitlements build/butterscotch else - codesign -f -s - --entitlements targets/ios/butterscotch.entitlements build/butterscotch + codesign -f -s - --entitlements artifacts/ios/butterscotch.entitlements build/butterscotch fi cp build/butterscotch "$apppath/butterscotch" -cp -a targets/ios/assets/* "$apppath" -command -v plistutil >/dev/null && plistutil -i targets/ios/assets/Info.plist -o "$apppath/Info.plist" -f bin +cp -a artifacts/ios/assets/* "$apppath" +command -v plistutil >/dev/null && plistutil -i artifacts/ios/assets/Info.plist -o "$apppath/Info.plist" -f bin cd build rm -f butterscotch.ipa diff --git a/targets/ios/butterscotch.entitlements b/artifacts/ios/butterscotch.entitlements similarity index 100% rename from targets/ios/butterscotch.entitlements rename to artifacts/ios/butterscotch.entitlements From b622f9a0b07f64da6ab35e43390ad8c74e76cfdb Mon Sep 17 00:00:00 2001 From: Un1q32 Date: Tue, 7 Jul 2026 20:30:26 -0400 Subject: [PATCH 078/109] fix --- Makefile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index abbd6e8b5..a5f181412 100644 --- a/Makefile +++ b/Makefile @@ -215,11 +215,11 @@ compat/config.mk: compat/configure.sh compat/tmp/cc endif -build/butterscotch: $(OBJS) $(if $(filter iOS,$(OS)),targets/ios/build-ipa.sh $(wildcard targets/ios/assets/*)) +build/butterscotch: $(OBJS) $(if $(filter iOS,$(OS)),artifacts/ios/build-ipa.sh $(wildcard artifacts/ios/assets/*)) @{ [ -z "$(NO_COLOR)" ] && [ -t 1 ]; } && printf " \033[1;34mLD\033[0m butterscotch\n" || printf " LD butterscotch\n" $(V)$(_CC) $(LDFLAGS) $(OBJS) $(LIBS) $(EXTRALIBS) $(OUTPUT_EXE)$@ @[ -f $@.exe ] && chmod +x $@.exe || true - @if [ "$(OS)" = 'iOS' ]; then targets/ios/build-ipa.sh; fi + @if [ "$(OS)" = 'iOS' ]; then artifacts/ios/build-ipa.sh; fi build/%.c.$(OBJ_EXT): %.c compat/config.mk $(if $(DISABLE_MMD),$(HEADERS)) @mkdir -p $(dir $@) From 41d33c7d5edea7955c4dec36f44939926ebec9e6 Mon Sep 17 00:00:00 2001 From: Un1q32 Date: Tue, 7 Jul 2026 21:00:26 -0400 Subject: [PATCH 079/109] ios toolchain script --- artifacts/ios/build-toolchain.sh | 128 +++++++++++++++++++++++++++++++ artifacts/ios/ios-cc | 18 +++++ 2 files changed, 146 insertions(+) create mode 100755 artifacts/ios/build-toolchain.sh create mode 100755 artifacts/ios/ios-cc diff --git a/artifacts/ios/build-toolchain.sh b/artifacts/ios/build-toolchain.sh new file mode 100755 index 000000000..5d008f6b0 --- /dev/null +++ b/artifacts/ios/build-toolchain.sh @@ -0,0 +1,128 @@ +#!/bin/sh +# shellcheck disable=2086 +set -e + +[ "${0%/*}" = "$0" ] && scriptroot="." || scriptroot="${0%/*}" +cd "$scriptroot" + +platformdir=$PWD + +workdir="$PWD/build" +mkdir -p "$workdir" +cd "$workdir" + +# Increase this if we ever make a change to the SDK, for example +# using a newer SDK version, and we need to invalidate the cache. +sdkver=1 +mkdir -p sdks +if ! [ -d sdks/ios8-sdk ] || [ "$(cat sdks/sdkver 2>/dev/null)" != "$sdkver" ]; then + # The iOS 8 SDK supports arm64, armv7s, and armv7 and is small. + # It also doesn't use tbd stubs so we don't need to link ld64 with libtapi. + printf '\nDownloading iOS SDK...\n\n' + [ -d sdks/ios8-sdk ] && rm -rf sdks/ios8-sdk + rm -f iPhoneOS8.0.sdk.tar.lzma + wget https://invoxiplaygames.uk/sdks/iPhoneOS8.0.sdk.tar.lzma + tar -x --lzma -f iPhoneOS8.0.sdk.tar.lzma + mv iPhoneOS8.0.sdk sdks/ios8-sdk + rm iPhoneOS8.0.sdk.tar.lzma + printf '%s' "$sdkver" > sdks/sdkver +fi + +if command -v nproc >/dev/null; then + ncpus="$(nproc)" +else + ncpus="$(sysctl -n hw.ncpu)" +fi + +if command -v gmake > /dev/null; then + _MAKE="gmake" +elif command -v make > /dev/null; then + _make_version="$(command make --version 2>/dev/null)" + case "$_make_version" in + (*GNU*) _MAKE="make" ;; + (*) + printf 'Missing dependency: GNU make\n' + exit 1 + ;; + esac +else + printf 'Missing dependency: GNU make\n' + exit 1 +fi + +make() { + command "$_MAKE" "$@" +} + +if [ -z "$LLVM_CONFIG" ]; then + if command -v llvm-config >/dev/null; then + export LLVM_CONFIG=llvm-config + else + export LLVM_CONFIG=false + fi +fi + +# toolchainver should be increased if we ever make a change to the toolchain, +# for example using a newer cctools version, and we need to invalidate the cache. +toolchainver=1 +if [ "$(cat toolchain/toolchainver 2>/dev/null)" != "$toolchainver" ]; then + outdated_toolchain=1 +fi + +# invalidate toolchain cache if settings change +"$LLVM_CONFIG" --version > toolchainsettings || true +if ! cmp -s toolchainsettings toolchain/lasttoolchainsettings; then + outdated_toolchain=1 +fi + +if [ -z "$outdated_toolchain" ]; then + printf 'Toolchain already built! :)\n' + exit 0 +fi + +rm -rf toolchain +mkdir -p toolchain/bin +mv toolchainsettings toolchain/lasttoolchainsettings + +printf '\nBuilding toolchain...\n\n' + +# this step is needed even on macOS since newer versions of Xcode will straight up not let you link for old iOS versions anymore +cctools_commit=fee8115127bb849d7481ea0015f181d3ebbd33cf +rm -rf cctools-port-* +wget -O- "https://github.com/Un1q32/cctools-port/archive/$cctools_commit.tar.gz" | tar -xz + +cd "cctools-port-$cctools_commit/cctools" +./configure \ + --enable-silent-rules \ + --with-llvm-config="$LLVM_CONFIG" +make -C ld64 -j"$ncpus" +strip ld64/src/ld/ld +mv ld64/src/ld/ld ../../toolchain/bin/ld64.ld64 +make -C libmacho -j"$ncpus" +make -C libstuff -j"$ncpus" +make -C misc strip lipo -j"$ncpus" +strip misc/strip misc/lipo +mv misc/strip ../../toolchain/bin/cctools-strip +mv misc/lipo ../../toolchain/bin/lipo +cd ../.. +rm -rf "cctools-port-$cctools_commit" & + +if [ "$(uname -s)" != "Darwin" ] && ! command -v ldid >/dev/null; then + printf '\nBuilding ldid...\n\n' + + ldid_commit=ef330422ef001ef2aa5792f4c6970d69f3c1f478 + rm -rf ldid-* + wget -O- "https://github.com/ProcursusTeam/ldid/archive/$ldid_commit.tar.gz" | tar -xz + + cd "ldid-$ldid_commit" + make + strip ldid + mv ldid ../toolchain/bin + cd .. + rm -rf "ldid-$ldid_commit" & +fi + +cp ../ios-cc toolchain/bin + +printf '%s' "$toolchainver" > toolchain/toolchainver +wait diff --git a/artifacts/ios/ios-cc b/artifacts/ios/ios-cc new file mode 100755 index 000000000..8f3fd8788 --- /dev/null +++ b/artifacts/ios/ios-cc @@ -0,0 +1,18 @@ +#!/bin/sh + +[ "${0%/*}" = "$0" ] && scriptroot="." || scriptroot="${0%/*}" + +case $0 in + *++) cc="clang++" ;; + *) cc="clang" ;; +esac + +exec "$cc" \ + -mlinker-version=762 \ + "$@" \ + -isysroot "$scriptroot/../../sdks/ios8-sdk" \ + -target unknown-apple-ios \ + -arch arm64 -Xarch_arm64 -mios-version-min=7.0 \ + -arch armv7 -Xarch_armv7 -mios-version-min=3.0 \ + -fuse-ld=ld64 \ + -Wno-unused-command-line-argument From c71ce44f4ac61b6e6e66f2921d11cb47c76c4558 Mon Sep 17 00:00:00 2001 From: Un1q32 Date: Tue, 7 Jul 2026 21:06:12 -0400 Subject: [PATCH 080/109] ios ci --- .github/workflows/build.yml | 40 ++++++++++++++++++++++++++++++++ artifacts/ios/build-toolchain.sh | 2 -- 2 files changed, 40 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 141ec6c47..638850b78 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -180,6 +180,46 @@ jobs: - name: Pack cache run: cd ~ && tar cJf ccache.tar.xz .cache/ccache workspacecache.tar + build-ios: + name: Build (iOS) + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@v4 + - name: Get Time + id: get-time + run: echo "time=$(date -u '+%Y-%m-%d-%H:%M:%S')" >> $GITHUB_OUTPUT + - uses: actions/cache@v5 + with: + path: ~/ccache.tar.xz + key: ios-artifact-${{ steps.get-time.outputs.time }} + restore-keys: ios-artifact- + - name: Unpack cache + run: | + cd ~ + [ -f ccache.tar.xz ] && tar xf ccache.tar.xz || true + [ -f workspacecache.tar ] && mv workspacecache.tar ${{ github.workspace }} || true + - name: Install dependencies + run: | + sudo apt-get update + sudo apt-get install -y ccache + [ -f workspacecache.tar ] && tar xf workspacecache.tar + ./artifacts/ios/build-toolchain.sh + tar cf workspacecache.tar artifacts/ios/build/toolchain + mv workspacecache.tar ~ + - name: Build + run: | + PATH="$PWD/artifacts/ios/build/toolchain/bin:$PATH" make \ + NO_COLOR=1 \ + VERBOSE=1 \ + CC='ccache ios-cc' + - name: Upload artifact + uses: actions/upload-artifact@v4 + with: + name: butterscotch-ios + path: build/butterscotch.ipa + - name: Pack cache + run: cd ~ && tar cJf ccache.tar.xz .cache/ccache workspacecache.tar + build-ps2: name: Build (PS2)${{ matrix.label }} runs-on: ubuntu-24.04 diff --git a/artifacts/ios/build-toolchain.sh b/artifacts/ios/build-toolchain.sh index 5d008f6b0..a3020b632 100755 --- a/artifacts/ios/build-toolchain.sh +++ b/artifacts/ios/build-toolchain.sh @@ -5,8 +5,6 @@ set -e [ "${0%/*}" = "$0" ] && scriptroot="." || scriptroot="${0%/*}" cd "$scriptroot" -platformdir=$PWD - workdir="$PWD/build" mkdir -p "$workdir" cd "$workdir" From 260e43fb979c9265aeb888b7bb305d97537fe00f Mon Sep 17 00:00:00 2001 From: Un1q32 Date: Tue, 7 Jul 2026 21:09:16 -0400 Subject: [PATCH 081/109] fix build --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 638850b78..85b248024 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -201,7 +201,7 @@ jobs: - name: Install dependencies run: | sudo apt-get update - sudo apt-get install -y ccache + sudo apt-get install -y ccache libplist-dev [ -f workspacecache.tar ] && tar xf workspacecache.tar ./artifacts/ios/build-toolchain.sh tar cf workspacecache.tar artifacts/ios/build/toolchain From 24cdd7d74c9ed08cf50a72298f787e70026062d9 Mon Sep 17 00:00:00 2001 From: Un1q32 Date: Tue, 7 Jul 2026 23:17:18 -0400 Subject: [PATCH 082/109] add sdk settings thingy so clang will use the 8.0 sdk version, and the armv7 slice will get the iOS 7 UI on newer devices --- artifacts/ios/SDKSettings.json | 28 ++++++++++++++++++++++++++++ artifacts/ios/build-toolchain.sh | 1 + 2 files changed, 29 insertions(+) create mode 100644 artifacts/ios/SDKSettings.json diff --git a/artifacts/ios/SDKSettings.json b/artifacts/ios/SDKSettings.json new file mode 100644 index 000000000..03bca4505 --- /dev/null +++ b/artifacts/ios/SDKSettings.json @@ -0,0 +1,28 @@ +{ + "DisplayName": "iOS 8.0", + "DocSetFeedURL": "http://developer.apple.com/rss/com.apple.adc.documentation.AppleiPhone8_0.atom", + "MinimalDisplayName": "8.0", + "DefaultProperties": { + "AD_HOC_CODE_SIGNING_ALLOWED": "NO", + "SUPPORTED_DEVICE_FAMILIES": "1,2", + "DEFAULT_COMPILER": "com.apple.compilers.llvm.clang.1_0", + "IPHONEOS_DEPLOYMENT_TARGET": "8.0", + "DEAD_CODE_STRIPPING": "YES", + "CODE_SIGN_ENTITLEMENTS": "", + "ENTITLEMENTS_REQUIRED": "YES", + "CODE_SIGNING_REQUIRED": "YES", + "PLATFORM_NAME": "iphoneos", + "GCC_THUMB_SUPPORT": "YES" + }, + "Version": "8.0", + "Toolchains": [ + "com.apple.dt.toolchain.iOS8_0" + ], + "MinimumSupportedToolsVersion": "3.2.3", + "IsBaseSDK": "YES", + "AlternateSDK": "iphonesimulator8.0", + "MaximumDeploymentTarget": "8.0.99", + "CanonicalName": "iphoneos8.0", + "DocSetFeedName": "Apple iPhone OS 8.0", + "CustomProperties": {} +} diff --git a/artifacts/ios/build-toolchain.sh b/artifacts/ios/build-toolchain.sh index a3020b632..5c42864a1 100755 --- a/artifacts/ios/build-toolchain.sh +++ b/artifacts/ios/build-toolchain.sh @@ -24,6 +24,7 @@ if ! [ -d sdks/ios8-sdk ] || [ "$(cat sdks/sdkver 2>/dev/null)" != "$sdkver" ]; mv iPhoneOS8.0.sdk sdks/ios8-sdk rm iPhoneOS8.0.sdk.tar.lzma printf '%s' "$sdkver" > sdks/sdkver + cp ../SDKSettings.json sdks/ios8-sdk fi if command -v nproc >/dev/null; then From e0caf7295049c9255050a74d59daba619d3c18b3 Mon Sep 17 00:00:00 2001 From: Un1q32 Date: Wed, 8 Jul 2026 17:15:16 -0400 Subject: [PATCH 083/109] ok --- src/desktop/main.c | 1 - 1 file changed, 1 deletion(-) diff --git a/src/desktop/main.c b/src/desktop/main.c index 502f0bbe2..7d5b1eb43 100644 --- a/src/desktop/main.c +++ b/src/desktop/main.c @@ -120,7 +120,6 @@ static int platformInitGlad(GLADloadproc load) { // Load OpenGL function pointers via GLAD if (version && strstr(version, "OpenGL ES")) { bool ret = gladLoadGLES1Loader(platformGetProcAddress); - if (ret) fprintf(stderr, "glad gles 1 good\n"); if (!gladLoadGLES2Loader(platformGetProcAddress)) return ret ? 3 : 0; return 2; From cc9e736291b900b9a7c3b9368a14364ba6c14c80 Mon Sep 17 00:00:00 2001 From: Un1q32 Date: Wed, 8 Jul 2026 17:37:33 -0400 Subject: [PATCH 084/109] use clang 22 --- .github/workflows/build.yml | 7 ++++++- artifacts/ios/ios-cc | 7 +------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 85b248024..9d9f63393 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -200,18 +200,23 @@ jobs: [ -f workspacecache.tar ] && mv workspacecache.tar ${{ github.workspace }} || true - name: Install dependencies run: | + wget https://apt.llvm.org/llvm.sh + sudo bash llvm.sh 22 sudo apt-get update - sudo apt-get install -y ccache libplist-dev + sudo apt-get install -y ccache libplist-dev libplist-utils clang-22 [ -f workspacecache.tar ] && tar xf workspacecache.tar ./artifacts/ios/build-toolchain.sh tar cf workspacecache.tar artifacts/ios/build/toolchain mv workspacecache.tar ~ - name: Build run: | + ios-cc -v PATH="$PWD/artifacts/ios/build/toolchain/bin:$PATH" make \ NO_COLOR=1 \ VERBOSE=1 \ CC='ccache ios-cc' + env: + CLANG: clang-22 - name: Upload artifact uses: actions/upload-artifact@v4 with: diff --git a/artifacts/ios/ios-cc b/artifacts/ios/ios-cc index 8f3fd8788..efacb52a7 100755 --- a/artifacts/ios/ios-cc +++ b/artifacts/ios/ios-cc @@ -2,12 +2,7 @@ [ "${0%/*}" = "$0" ] && scriptroot="." || scriptroot="${0%/*}" -case $0 in - *++) cc="clang++" ;; - *) cc="clang" ;; -esac - -exec "$cc" \ +exec "${CLANG:-clang}" \ -mlinker-version=762 \ "$@" \ -isysroot "$scriptroot/../../sdks/ios8-sdk" \ From a93e7d31d7a09867860b0706f92e6f8cd84af33d Mon Sep 17 00:00:00 2001 From: Un1q32 Date: Wed, 8 Jul 2026 17:41:32 -0400 Subject: [PATCH 085/109] fix --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 9d9f63393..dc0e61d6c 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -210,7 +210,7 @@ jobs: mv workspacecache.tar ~ - name: Build run: | - ios-cc -v + PATH="$PWD/artifacts/ios/build/toolchain/bin:$PATH" ios-cc -v PATH="$PWD/artifacts/ios/build/toolchain/bin:$PATH" make \ NO_COLOR=1 \ VERBOSE=1 \ From 339698b33968fb38863a3211b206a38d4f9c632b Mon Sep 17 00:00:00 2001 From: Un1q32 Date: Wed, 8 Jul 2026 17:43:51 -0400 Subject: [PATCH 086/109] what the fuck --- .github/workflows/build.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index dc0e61d6c..fb627ba56 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -210,6 +210,7 @@ jobs: mv workspacecache.tar ~ - name: Build run: | + clang-22 -v PATH="$PWD/artifacts/ios/build/toolchain/bin:$PATH" ios-cc -v PATH="$PWD/artifacts/ios/build/toolchain/bin:$PATH" make \ NO_COLOR=1 \ From 4d26b491bf725807077035a515abea77f35812af Mon Sep 17 00:00:00 2001 From: Un1q32 Date: Wed, 8 Jul 2026 17:45:14 -0400 Subject: [PATCH 087/109] dude --- .github/workflows/build.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index fb627ba56..99cb35fae 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -210,8 +210,7 @@ jobs: mv workspacecache.tar ~ - name: Build run: | - clang-22 -v - PATH="$PWD/artifacts/ios/build/toolchain/bin:$PATH" ios-cc -v + PATH="$PWD/artifacts/ios/build/toolchain/bin:$PATH" CLANG=clang-22 ios-cc -v PATH="$PWD/artifacts/ios/build/toolchain/bin:$PATH" make \ NO_COLOR=1 \ VERBOSE=1 \ From 937af22503f4b6b5c526e018cf747206324f195c Mon Sep 17 00:00:00 2001 From: Un1q32 Date: Wed, 8 Jul 2026 17:54:35 -0400 Subject: [PATCH 088/109] son --- .github/workflows/build.yml | 2 +- artifacts/ios/build-toolchain.sh | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 99cb35fae..dc0e61d6c 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -210,7 +210,7 @@ jobs: mv workspacecache.tar ~ - name: Build run: | - PATH="$PWD/artifacts/ios/build/toolchain/bin:$PATH" CLANG=clang-22 ios-cc -v + PATH="$PWD/artifacts/ios/build/toolchain/bin:$PATH" ios-cc -v PATH="$PWD/artifacts/ios/build/toolchain/bin:$PATH" make \ NO_COLOR=1 \ VERBOSE=1 \ diff --git a/artifacts/ios/build-toolchain.sh b/artifacts/ios/build-toolchain.sh index 5c42864a1..5531a23f7 100755 --- a/artifacts/ios/build-toolchain.sh +++ b/artifacts/ios/build-toolchain.sh @@ -63,7 +63,7 @@ fi # toolchainver should be increased if we ever make a change to the toolchain, # for example using a newer cctools version, and we need to invalidate the cache. -toolchainver=1 +toolchainver=2 if [ "$(cat toolchain/toolchainver 2>/dev/null)" != "$toolchainver" ]; then outdated_toolchain=1 fi @@ -121,7 +121,7 @@ if [ "$(uname -s)" != "Darwin" ] && ! command -v ldid >/dev/null; then rm -rf "ldid-$ldid_commit" & fi -cp ../ios-cc toolchain/bin +ln -s ../../../ios-cc toolchain/bin printf '%s' "$toolchainver" > toolchain/toolchainver wait From 809f3c907244293c3841864d4b6897e03211be12 Mon Sep 17 00:00:00 2001 From: Un1q32 Date: Wed, 8 Jul 2026 17:57:10 -0400 Subject: [PATCH 089/109] ok --- .github/workflows/build.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index dc0e61d6c..c4dd619c3 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -210,7 +210,6 @@ jobs: mv workspacecache.tar ~ - name: Build run: | - PATH="$PWD/artifacts/ios/build/toolchain/bin:$PATH" ios-cc -v PATH="$PWD/artifacts/ios/build/toolchain/bin:$PATH" make \ NO_COLOR=1 \ VERBOSE=1 \ From b4870b8fa9e3ef53431a2199ed04a4679dc14bc6 Mon Sep 17 00:00:00 2001 From: Un1q32 Date: Wed, 8 Jul 2026 21:22:32 -0400 Subject: [PATCH 090/109] pull to refresh --- src/desktop/backends/ios.c | 61 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 61 insertions(+) diff --git a/src/desktop/backends/ios.c b/src/desktop/backends/ios.c index 046295181..0aac082a5 100755 --- a/src/desktop/backends/ios.c +++ b/src/desktop/backends/ios.c @@ -1169,8 +1169,12 @@ static UIKeyboardType bsNumericKeyboardType(void) { @interface BSGameListViewController : UITableViewController { NSMutableArray *games; NSIndexPath *pendingDeleteIndexPath; + UIActivityIndicatorView *refreshIndicator; + UIView *refreshOverlay; + BOOL isRefreshing; } - (void)reloadGames; +- (void)handleRefresh:(id)sender; @end @implementation BSGameListViewController @@ -1192,6 +1196,28 @@ static UIKeyboardType bsNumericKeyboardType(void) { [gearItem release]; [self reloadGames]; + + /* Add pull-to-refresh support. UIRefreshControl is available since iOS 6, + * so use it if present, otherwise fall back to a custom implementation. */ + Class refreshControlClass = NSClassFromString(@"UIRefreshControl"); + if (refreshControlClass) { + id refreshControl = [[refreshControlClass alloc] init]; + [refreshControl addTarget:self action:@selector(handleRefresh:) forControlEvents:UIControlEventValueChanged]; + [self setValue:refreshControl forKey:@"refreshControl"]; + [refreshControl release]; + } else { + /* Custom refresh for iOS < 6: use centered activity indicator overlay */ + refreshIndicator = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleWhiteLarge]; + refreshIndicator.hidesWhenStopped = YES; + refreshIndicator.hidden = YES; + + refreshOverlay = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 100, 100)]; + refreshOverlay.backgroundColor = [UIColor colorWithRed:0.0 green:0.0 blue:0.0 alpha:0.7]; + refreshOverlay.hidden = YES; + + refreshIndicator.center = CGPointMake(50, 50); + [refreshOverlay addSubview:refreshIndicator]; + } } return self; } @@ -1217,11 +1243,44 @@ static UIKeyboardType bsNumericKeyboardType(void) { [self.tableView reloadData]; } +- (void)handleRefresh:(id)sender { + [self reloadGames]; + + /* End the refresh animation for UIRefreshControl (iOS 6+) */ + Class refreshControlClass = NSClassFromString(@"UIRefreshControl"); + if (refreshControlClass && [sender isKindOfClass:refreshControlClass]) { + [sender performSelector:@selector(endRefreshing)]; + } else { + /* End custom refresh for iOS < 6 */ + isRefreshing = NO; + [refreshIndicator stopAnimating]; + refreshOverlay.hidden = YES; + [refreshOverlay removeFromSuperview]; + } +} + - (void)viewWillAppear:(BOOL)animated { [super viewWillAppear:animated]; [self reloadGames]; } +/* Custom pull-to-refresh implementation for iOS < 6 */ +- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate { + (void)decelerate; + if (!refreshIndicator) return; /* Using UIRefreshControl on iOS 6+ */ + + CGFloat offset = scrollView.contentOffset.y; + if (offset < -60 && !isRefreshing) { + isRefreshing = YES; + refreshOverlay.hidden = NO; + refreshOverlay.center = CGPointMake(self.tableView.bounds.size.width / 2.0f, + self.tableView.bounds.size.height / 2.0f); + [self.tableView addSubview:refreshOverlay]; + [refreshIndicator startAnimating]; + [self performSelector:@selector(handleRefresh:) withObject:nil afterDelay:0.5]; + } +} + - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { (void)tableView; (void)section; return [games count]; @@ -1319,6 +1378,8 @@ static UIKeyboardType bsNumericKeyboardType(void) { - (void)dealloc { [pendingDeleteIndexPath release]; [games release]; + [refreshIndicator release]; + [refreshOverlay release]; [super dealloc]; } From d9f1dd992804dca390d5dbc2c68f12cb6e326e42 Mon Sep 17 00:00:00 2001 From: Un1q32 Date: Wed, 8 Jul 2026 21:39:04 -0400 Subject: [PATCH 091/109] error loggin --- src/desktop/backends/ios.c | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/desktop/backends/ios.c b/src/desktop/backends/ios.c index 0aac082a5..c82e2137d 100755 --- a/src/desktop/backends/ios.c +++ b/src/desktop/backends/ios.c @@ -1230,7 +1230,12 @@ static UIKeyboardType bsNumericKeyboardType(void) { [games removeAllObjects]; NSFileManager *fm = [NSFileManager defaultManager]; - NSArray *entries = [fm contentsOfDirectoryAtPath:BS_GAMES_ROOT_PATH error:NULL]; + NSError *error = nil; + NSArray *entries = [fm contentsOfDirectoryAtPath:BS_GAMES_ROOT_PATH error:&error]; + if (error) { + fprintf(stderr, "Error scanning game directory: %s\n", [[error localizedDescription] UTF8String]); + return; + } for (NSString *name in entries) { NSString *dir = [BS_GAMES_ROOT_PATH stringByAppendingPathComponent:name]; BOOL isDir = NO; From 52d18b788a3c597a5c08a464aefac046c2dabd54 Mon Sep 17 00:00:00 2001 From: Un1q32 Date: Wed, 8 Jul 2026 21:51:13 -0400 Subject: [PATCH 092/109] popup --- src/desktop/backends/ios.c | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/desktop/backends/ios.c b/src/desktop/backends/ios.c index c82e2137d..763de9ce5 100755 --- a/src/desktop/backends/ios.c +++ b/src/desktop/backends/ios.c @@ -1233,7 +1233,13 @@ static UIKeyboardType bsNumericKeyboardType(void) { NSError *error = nil; NSArray *entries = [fm contentsOfDirectoryAtPath:BS_GAMES_ROOT_PATH error:&error]; if (error) { - fprintf(stderr, "Error scanning game directory: %s\n", [[error localizedDescription] UTF8String]); + UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"Error" + message:[error localizedDescription] + delegate:nil + cancelButtonTitle:@"OK" + otherButtonTitles:nil]; + [alert show]; + [alert release]; return; } for (NSString *name in entries) { From cdd0bb02be20aff539c2ac7200102187d62c4584 Mon Sep 17 00:00:00 2001 From: Un1q32 Date: Wed, 8 Jul 2026 21:56:09 -0400 Subject: [PATCH 093/109] more error logging --- src/desktop/backends/ios.c | 29 ++++++++++++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/src/desktop/backends/ios.c b/src/desktop/backends/ios.c index 763de9ce5..1d26c5207 100755 --- a/src/desktop/backends/ios.c +++ b/src/desktop/backends/ios.c @@ -1231,10 +1231,37 @@ static UIKeyboardType bsNumericKeyboardType(void) { NSFileManager *fm = [NSFileManager defaultManager]; NSError *error = nil; + + /* Check if the games directory exists, create it if not */ + BOOL isDir = NO; + if (![fm fileExistsAtPath:BS_GAMES_ROOT_PATH isDirectory:&isDir]) { + error = nil; + [fm createDirectoryAtPath:BS_GAMES_ROOT_PATH withIntermediateDirectories:YES attributes:nil error:&error]; + if (error) { + UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"Error" + message:[NSString stringWithFormat:@"Could not create games directory: %@", [error localizedDescription]] + delegate:nil + cancelButtonTitle:@"OK" + otherButtonTitles:nil]; + [alert show]; + [alert release]; + return; + } + } else if (!isDir) { + UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"Error" + message:@"Games path exists but is not a directory" + delegate:nil + cancelButtonTitle:@"OK" + otherButtonTitles:nil]; + [alert show]; + [alert release]; + return; + } + NSArray *entries = [fm contentsOfDirectoryAtPath:BS_GAMES_ROOT_PATH error:&error]; if (error) { UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"Error" - message:[error localizedDescription] + message:[NSString stringWithFormat:@"Could not scan games directory: %@", [error localizedDescription]] delegate:nil cancelButtonTitle:@"OK" otherButtonTitles:nil]; From 316c0c6b41d10abce7f58132ed57f56472b78d33 Mon Sep 17 00:00:00 2001 From: Un1q32 Date: Wed, 8 Jul 2026 22:20:14 -0400 Subject: [PATCH 094/109] documents folder --- src/desktop/backends/ios.c | 98 ++++++++++++++++++++++++-------------- 1 file changed, 61 insertions(+), 37 deletions(-) diff --git a/src/desktop/backends/ios.c b/src/desktop/backends/ios.c index 1d26c5207..0d6f9bf9c 100755 --- a/src/desktop/backends/ios.c +++ b/src/desktop/backends/ios.c @@ -99,7 +99,56 @@ static atomic_bool g_highResEnabled = false; * game launch in startGameWithFolder:. */ static char g_rendererArg[32] = "software"; -#define BS_GAMES_ROOT_PATH @"/var/mobile/Documents/Butterscotch" +/* Games root directory. NSSearchPathForDirectoriesInDomains( + * NSDocumentDirectory, ...) resolves correctly on every SDK back to iOS + * 2, but what it resolves *to* depends on how the app is installed: + * + * - Sandboxed install (App Store, or a sandboxed jailbreak profile): + * resolves to this app's own container, e.g. + * /var/mobile/Containers/Data/Application//Documents -- already + * private to Butterscotch, so use it as-is. + * - Unsandboxed system app (installed to /Applications, classic + * jailbreak-style): NSHomeDirectory() has no per-app container and + * just resolves to /var/mobile, so NSDocumentDirectory alone gives + * the literal /var/mobile/Documents -- the same shared folder every + * other unsandboxed app on the device also gets pointed at. Only in + * this specific case, append "Butterscotch" to keep this install + * path isolated too, matching the original hardcoded behavior. + * + * Distinguish the two by comparing against the known shared path + * literally, rather than e.g. checking for "Containers" in the path -- + * the sandboxed container layout has shifted before and isn't ours to + * assume; /var/mobile/Documents as the unsandboxed shared docs folder + * has been stable since early iOS and is the one thing we can rely on. + * + * The appended subfolder is created if missing (harmless no-op if it + * already exists) since an app can't assume the OS already created it. + * Computed and cached once, since neither the container path nor the + * sandboxed-vs-not status changes over the process's lifetime. */ +static NSString *bsGamesRootPath(void) { + static NSString *cached = nil; + if (!cached) { + NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES); + NSString *docs = [paths objectAtIndex:0]; + + if ([docs isEqualToString:@"/var/mobile/Documents"]) { + NSString *root = [docs stringByAppendingPathComponent:@"Butterscotch"]; + NSError *error = nil; + [[NSFileManager defaultManager] createDirectoryAtPath:root + withIntermediateDirectories:YES + attributes:nil + error:&error]; + /* If creation failed (e.g. read-only filesystem in some exotic + * install), fall back to docs itself rather than a path that + * may not exist -- reloadGames' contentsOfDirectoryAtPath: + * will just report the real error either way. */ + cached = [(error ? docs : root) retain]; + } else { + cached = [docs retain]; + } + } + return cached; +} static void bsRequestRelayout(void) { if (g_glView) { @@ -1160,10 +1209,12 @@ static UIKeyboardType bsNumericKeyboardType(void) { @end /* --------------------------------------------------------------------- - * Game selection menu. Scans BS_GAMES_ROOT_PATH for subfolders that - * contain a data.win, and lets the user pick one. Re-scans every time - * the view (re)appears so games dropped in via file transfer while the - * app is running (or after returning from a game) show up. + * Game selection menu. Scans the games root (see bsGamesRootPath() + * above -- a "Butterscotch" folder under Documents, one way or another) + * for subfolders that contain a data.win, and lets the user pick one. + * Re-scans every time the view (re)appears so games dropped in via file + * transfer while the app is running (or after returning from a game) + * show up. * ------------------------------------------------------------------- */ @interface BSGameListViewController : UITableViewController { @@ -1231,37 +1282,10 @@ static UIKeyboardType bsNumericKeyboardType(void) { NSFileManager *fm = [NSFileManager defaultManager]; NSError *error = nil; - - /* Check if the games directory exists, create it if not */ - BOOL isDir = NO; - if (![fm fileExistsAtPath:BS_GAMES_ROOT_PATH isDirectory:&isDir]) { - error = nil; - [fm createDirectoryAtPath:BS_GAMES_ROOT_PATH withIntermediateDirectories:YES attributes:nil error:&error]; - if (error) { - UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"Error" - message:[NSString stringWithFormat:@"Could not create games directory: %@", [error localizedDescription]] - delegate:nil - cancelButtonTitle:@"OK" - otherButtonTitles:nil]; - [alert show]; - [alert release]; - return; - } - } else if (!isDir) { - UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"Error" - message:@"Games path exists but is not a directory" - delegate:nil - cancelButtonTitle:@"OK" - otherButtonTitles:nil]; - [alert show]; - [alert release]; - return; - } - - NSArray *entries = [fm contentsOfDirectoryAtPath:BS_GAMES_ROOT_PATH error:&error]; + NSArray *entries = [fm contentsOfDirectoryAtPath:bsGamesRootPath() error:&error]; if (error) { UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"Error" - message:[NSString stringWithFormat:@"Could not scan games directory: %@", [error localizedDescription]] + message:[error localizedDescription] delegate:nil cancelButtonTitle:@"OK" otherButtonTitles:nil]; @@ -1270,7 +1294,7 @@ static UIKeyboardType bsNumericKeyboardType(void) { return; } for (NSString *name in entries) { - NSString *dir = [BS_GAMES_ROOT_PATH stringByAppendingPathComponent:name]; + NSString *dir = [bsGamesRootPath() stringByAppendingPathComponent:name]; BOOL isDir = NO; if (![fm fileExistsAtPath:dir isDirectory:&isDir] || !isDir) continue; @@ -1387,7 +1411,7 @@ static UIKeyboardType bsNumericKeyboardType(void) { } NSString *name = [games objectAtIndex:indexPath.row]; - NSString *dir = [BS_GAMES_ROOT_PATH stringByAppendingPathComponent:name]; + NSString *dir = [bsGamesRootPath() stringByAppendingPathComponent:name]; /* removeItemAtPath: recursively removes directory contents, so this * takes data.win and any save files sitting next to it in one shot. */ @@ -1464,7 +1488,7 @@ extern int game_main(int argc, char *argv[]); } - (void)startGameWithFolder:(NSString *)folderName { - NSString *dataWin = [[BS_GAMES_ROOT_PATH stringByAppendingPathComponent:folderName] + NSString *dataWin = [[bsGamesRootPath() stringByAppendingPathComponent:folderName] stringByAppendingPathComponent:@"data.win"]; strlcpy(g_gamePath, [dataWin fileSystemRepresentation], sizeof(g_gamePath)); snprintf(g_ffSpeedArg, sizeof(g_ffSpeedArg), "--fast-forward-speed=%g", bsLoadFastForwardSpeed()); From 9f02d5be0a43ef7db541cdb9b8daa3227d7192bd Mon Sep 17 00:00:00 2001 From: Un1q32 Date: Wed, 8 Jul 2026 22:25:33 -0400 Subject: [PATCH 095/109] scan both --- src/desktop/backends/ios.c | 154 +++++++++++++++++++++++-------------- 1 file changed, 98 insertions(+), 56 deletions(-) diff --git a/src/desktop/backends/ios.c b/src/desktop/backends/ios.c index 0d6f9bf9c..1dd556918 100755 --- a/src/desktop/backends/ios.c +++ b/src/desktop/backends/ios.c @@ -75,7 +75,7 @@ static UIView *g_overlayView = nil; /* Last UIDeviceOrientation actually applied to rootView by * applyDeviceOrientation:, used to no-op duplicate/redundant notifications. - * Reset to Unknown in startGameWithFolder: so each new game session always + * Reset to Unknown in startGameWithPath: so each new game session always * re-syncs to the device's current orientation, even if it's the same * orientation the previous session last settled into. */ static UIDeviceOrientation g_lastAppliedOrientation = UIDeviceOrientationUnknown; @@ -85,51 +85,63 @@ static UIDeviceOrientation g_lastAppliedOrientation = UIDeviceOrientationUnknown static char g_gamePath[PATH_MAX]; /* Built from the persisted fast-forward speed setting right before each - * game launch -- see AppDelegate startGameWithFolder:. Sized generously + * game launch -- see AppDelegate startGameWithPath:. Sized generously * for any reasonable floating-point value typed into the settings field. */ static char g_ffSpeedArg[64] = "--fast-forward-speed=4"; /* Cached copy of the "high resolution" setting (see BS_HIGH_RES_DEFAULTS_KEY * below), refreshed from NSUserDefaults once per game launch in - * startGameWithFolder: rather than read on every applyRenderScale() call -- + * startGameWithPath: rather than read on every applyRenderScale() call -- * the setting can only be changed from the menu, never mid-game. */ static atomic_bool g_highResEnabled = false; /* Cached copy of the renderer setting, refreshed from NSUserDefaults once per - * game launch in startGameWithFolder:. */ + * game launch in startGameWithPath:. */ static char g_rendererArg[32] = "software"; -/* Games root directory. NSSearchPathForDirectoriesInDomains( +/* Games root(s) to scan. NSSearchPathForDirectoriesInDomains( * NSDocumentDirectory, ...) resolves correctly on every SDK back to iOS - * 2, but what it resolves *to* depends on how the app is installed: + * 2, but what it resolves *to* -- and therefore what else is worth + * checking -- depends on how the app is installed: * - * - Sandboxed install (App Store, or a sandboxed jailbreak profile): - * resolves to this app's own container, e.g. - * /var/mobile/Containers/Data/Application//Documents -- already - * private to Butterscotch, so use it as-is. * - Unsandboxed system app (installed to /Applications, classic * jailbreak-style): NSHomeDirectory() has no per-app container and * just resolves to /var/mobile, so NSDocumentDirectory alone gives * the literal /var/mobile/Documents -- the same shared folder every - * other unsandboxed app on the device also gets pointed at. Only in - * this specific case, append "Butterscotch" to keep this install - * path isolated too, matching the original hardcoded behavior. + * other unsandboxed app on the device also gets pointed at. Append + * "Butterscotch" to keep this install path isolated, matching the + * original hardcoded behavior. There's only one root to scan here. + * + * - Sandboxed install (App Store, or a sandboxed jailbreak profile): + * resolves to this app's own container, e.g. + * /var/mobile/Containers/Data/Application//Documents. Used + * as-is as the primary root. But since some file managers / tweaks + * write into /var/mobile/Documents/Butterscotch regardless of a + * particular app's sandboxing, also scan that as a secondary root + * so games dropped there are still picked up. We never create it + * ourselves in this case (a sandboxed app creating files outside + * its own container is a sandbox violation waiting to happen), and + * if it's unreadable -- doesn't exist, permission denied, sandbox + * blocks the access outright -- that's fine, reloadGames() below + * just silently skips it. * - * Distinguish the two by comparing against the known shared path - * literally, rather than e.g. checking for "Containers" in the path -- - * the sandboxed container layout has shifted before and isn't ours to - * assume; /var/mobile/Documents as the unsandboxed shared docs folder - * has been stable since early iOS and is the one thing we can rely on. + * Distinguish sandboxed vs not by comparing the resolved Documents path + * against the known shared path literally, rather than e.g. checking + * for "Containers" in the path -- the sandboxed container layout has + * shifted before and isn't ours to assume; /var/mobile/Documents as the + * unsandboxed shared docs folder has been stable since early iOS and is + * the one thing we can rely on. * - * The appended subfolder is created if missing (harmless no-op if it - * already exists) since an app can't assume the OS already created it. * Computed and cached once, since neither the container path nor the - * sandboxed-vs-not status changes over the process's lifetime. */ -static NSString *bsGamesRootPath(void) { - static NSString *cached = nil; + * sandboxed-vs-not status changes over the process's lifetime. Index 0 + * is always the primary root (the only one whose read errors get + * surfaced to the user -- see reloadGames()). */ +static NSArray *bsGamesRootsToScan(void) { + static NSArray *cached = nil; if (!cached) { NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES); NSString *docs = [paths objectAtIndex:0]; + NSMutableArray *roots = [NSMutableArray array]; if ([docs isEqualToString:@"/var/mobile/Documents"]) { NSString *root = [docs stringByAppendingPathComponent:@"Butterscotch"]; @@ -142,10 +154,13 @@ static NSString *bsGamesRootPath(void) { * install), fall back to docs itself rather than a path that * may not exist -- reloadGames' contentsOfDirectoryAtPath: * will just report the real error either way. */ - cached = [(error ? docs : root) retain]; + [roots addObject:(error ? docs : root)]; } else { - cached = [docs retain]; + [roots addObject:docs]; + [roots addObject:@"/var/mobile/Documents/Butterscotch"]; } + + cached = [roots retain]; } return cached; } @@ -991,7 +1006,7 @@ static void drawCenteredLabel(NSString *text, CGRect rect, UIFont *font) { * toggle, both persisted via NSUserDefaults. The fast-forward speed is * applied as a --fast-forward-speed= argv entry, and the high-res flag * is cached into g_highResEnabled, the next time a game is launched (see - * AppDelegate startGameWithFolder:). + * AppDelegate startGameWithPath:). * ------------------------------------------------------------------- */ #define BS_FF_SPEED_DEFAULTS_KEY @"BSFastForwardSpeed" @@ -1209,7 +1224,7 @@ static UIKeyboardType bsNumericKeyboardType(void) { @end /* --------------------------------------------------------------------- - * Game selection menu. Scans the games root (see bsGamesRootPath() + * Game selection menu. Scans the games roots (see bsGamesRootsToScan() * above -- a "Butterscotch" folder under Documents, one way or another) * for subfolders that contain a data.win, and lets the user pick one. * Re-scans every time the view (re)appears so games dropped in via file @@ -1277,29 +1292,57 @@ static UIKeyboardType bsNumericKeyboardType(void) { [[[UIApplication sharedApplication] delegate] performSelector:@selector(showSettings)]; } +/* Each entry in `games` is an NSDictionary with: + * "name" -- the folder's display name (may repeat across roots if the + * same name exists in more than one scanned root -- we don't + * dedupe, since they could be entirely different games that + * just happen to share a folder name). + * "path" -- the full path to that folder, already resolved against + * whichever root it was found under. Kept in full rather + * than re-derived from bsGamesRootsToScan() + name at + * use-time, since a name alone is now ambiguous as to which + * root it came from. */ - (void)reloadGames { [games removeAllObjects]; NSFileManager *fm = [NSFileManager defaultManager]; - NSError *error = nil; - NSArray *entries = [fm contentsOfDirectoryAtPath:bsGamesRootPath() error:&error]; - if (error) { - UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"Error" - message:[error localizedDescription] - delegate:nil - cancelButtonTitle:@"OK" - otherButtonTitles:nil]; - [alert show]; - [alert release]; - return; - } - for (NSString *name in entries) { - NSString *dir = [bsGamesRootPath() stringByAppendingPathComponent:name]; - BOOL isDir = NO; - if (![fm fileExistsAtPath:dir isDirectory:&isDir] || !isDir) continue; + NSArray *roots = bsGamesRootsToScan(); + + for (NSUInteger i = 0; i < [roots count]; i++) { + NSString *root = [roots objectAtIndex:i]; + NSError *error = nil; + NSArray *entries = [fm contentsOfDirectoryAtPath:root error:&error]; + if (error) { + /* Only the primary root (index 0) is expected to always be + * readable -- it's either our own sandboxed container or a + * freshly-created directory. Secondary roots (currently just + * the shared /var/mobile/Documents/Butterscotch courtesy + * scan when sandboxed) are allowed to be missing/unreadable; + * silently skip those rather than alerting the user to + * something they can't do anything about. */ + if (i == 0) { + UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"Error" + message:[error localizedDescription] + delegate:nil + cancelButtonTitle:@"OK" + otherButtonTitles:nil]; + [alert show]; + [alert release]; + } + continue; + } - NSString *dataWin = [dir stringByAppendingPathComponent:@"data.win"]; - if ([fm fileExistsAtPath:dataWin]) [games addObject:name]; + for (NSString *name in entries) { + NSString *dir = [root stringByAppendingPathComponent:name]; + BOOL isDir = NO; + if (![fm fileExistsAtPath:dir isDirectory:&isDir] || !isDir) continue; + + NSString *dataWin = [dir stringByAppendingPathComponent:@"data.win"]; + if ([fm fileExistsAtPath:dataWin]) { + [games addObject:[NSDictionary dictionaryWithObjectsAndKeys: + name, @"name", dir, @"path", nil]]; + } + } } [self.tableView reloadData]; @@ -1359,9 +1402,9 @@ static UIKeyboardType bsNumericKeyboardType(void) { #endif } #if __IPHONE_OS_VERSION_MIN_REQUIRED >= 30000 - cell.textLabel.text = [games objectAtIndex:indexPath.row]; + cell.textLabel.text = [[games objectAtIndex:indexPath.row] objectForKey:@"name"]; #else - cell.text = [games objectAtIndex:indexPath.row]; + cell.text = [[games objectAtIndex:indexPath.row] objectForKey:@"name"]; #endif return cell; } @@ -1384,7 +1427,7 @@ static UIKeyboardType bsNumericKeyboardType(void) { (void)tableView; if (editingStyle != UITableViewCellEditingStyleDelete) return; - NSString *name = [games objectAtIndex:indexPath.row]; + NSString *name = [[games objectAtIndex:indexPath.row] objectForKey:@"name"]; [pendingDeleteIndexPath release]; pendingDeleteIndexPath = [indexPath retain]; @@ -1410,8 +1453,8 @@ static UIKeyboardType bsNumericKeyboardType(void) { return; } - NSString *name = [games objectAtIndex:indexPath.row]; - NSString *dir = [bsGamesRootPath() stringByAppendingPathComponent:name]; + NSDictionary *entry = [games objectAtIndex:indexPath.row]; + NSString *dir = [entry objectForKey:@"path"]; /* removeItemAtPath: recursively removes directory contents, so this * takes data.win and any save files sitting next to it in one shot. */ @@ -1432,9 +1475,9 @@ static UIKeyboardType bsNumericKeyboardType(void) { - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath { [tableView deselectRowAtIndexPath:indexPath animated:YES]; - NSString *folder = [games objectAtIndex:indexPath.row]; + NSString *gameDir = [[games objectAtIndex:indexPath.row] objectForKey:@"path"]; id delegate = [[UIApplication sharedApplication] delegate]; - [delegate performSelector:@selector(startGameWithFolder:) withObject:folder]; + [delegate performSelector:@selector(startGameWithPath:) withObject:gameDir]; } - (void)dealloc { @@ -1459,7 +1502,7 @@ extern int game_main(int argc, char *argv[]); UINavigationController *navController; BOOL usingRootViewController; } -- (void)startGameWithFolder:(NSString *)folderName; +- (void)startGameWithPath:(NSString *)gamePath; - (void)returnToMenu; - (void)showSettings; - (void)settingsDone; @@ -1487,9 +1530,8 @@ extern int game_main(int argc, char *argv[]); [pool release]; } -- (void)startGameWithFolder:(NSString *)folderName { - NSString *dataWin = [[bsGamesRootPath() stringByAppendingPathComponent:folderName] - stringByAppendingPathComponent:@"data.win"]; +- (void)startGameWithPath:(NSString *)gamePath { + NSString *dataWin = [gamePath stringByAppendingPathComponent:@"data.win"]; strlcpy(g_gamePath, [dataWin fileSystemRepresentation], sizeof(g_gamePath)); snprintf(g_ffSpeedArg, sizeof(g_ffSpeedArg), "--fast-forward-speed=%g", bsLoadFastForwardSpeed()); atomic_store(&g_highResEnabled, bsLoadHighResEnabled()); From 24f65de11a0b56b8597ef839b66826449388a90b Mon Sep 17 00:00:00 2001 From: Un1q32 Date: Wed, 8 Jul 2026 22:29:31 -0400 Subject: [PATCH 096/109] fix --- artifacts/ios/assets/Info.plist | 2 -- 1 file changed, 2 deletions(-) diff --git a/artifacts/ios/assets/Info.plist b/artifacts/ios/assets/Info.plist index 43de3b081..dfeddf697 100644 --- a/artifacts/ios/assets/Info.plist +++ b/artifacts/ios/assets/Info.plist @@ -51,8 +51,6 @@ UISupportedInterfaceOrientations UIInterfaceOrientationPortrait - UIInterfaceOrientationLandscapeLeft - UIInterfaceOrientationLandscapeRight From 56f05b98a71e9bd7910ff67b567ebe756a0aed8a Mon Sep 17 00:00:00 2001 From: Un1q32 Date: Wed, 8 Jul 2026 22:30:05 -0400 Subject: [PATCH 097/109] enable thingy --- artifacts/ios/assets/Info.plist | 2 ++ 1 file changed, 2 insertions(+) diff --git a/artifacts/ios/assets/Info.plist b/artifacts/ios/assets/Info.plist index dfeddf697..5831bbdb9 100644 --- a/artifacts/ios/assets/Info.plist +++ b/artifacts/ios/assets/Info.plist @@ -37,6 +37,8 @@ UIStatusBarHidden + UIFileSharingEnabled + UIViewControllerBasedStatusBarAppearance CFBundleIconFiles From 9c1ba5aa753c1db6b7e533f0df50654930f046e8 Mon Sep 17 00:00:00 2001 From: Un1q32 Date: Wed, 8 Jul 2026 23:42:47 -0400 Subject: [PATCH 098/109] ok --- artifacts/ios/assets/Default-568h@2x.png | 1 - artifacts/ios/assets/Default-667h@2x.png | 1 - artifacts/ios/assets/Default-736h@3x.png | 1 - artifacts/ios/assets/Default.png | Bin 308 -> 0 bytes artifacts/ios/assets/Default@2x.png | 1 - artifacts/ios/assets/Info.plist | 94 ++++++++++++++++++++++- 6 files changed, 92 insertions(+), 6 deletions(-) delete mode 120000 artifacts/ios/assets/Default-568h@2x.png delete mode 120000 artifacts/ios/assets/Default-667h@2x.png delete mode 120000 artifacts/ios/assets/Default-736h@3x.png delete mode 100644 artifacts/ios/assets/Default.png delete mode 120000 artifacts/ios/assets/Default@2x.png mode change 100644 => 100755 artifacts/ios/assets/Info.plist diff --git a/artifacts/ios/assets/Default-568h@2x.png b/artifacts/ios/assets/Default-568h@2x.png deleted file mode 120000 index 7b6e25fbf..000000000 --- a/artifacts/ios/assets/Default-568h@2x.png +++ /dev/null @@ -1 +0,0 @@ -Default.png \ No newline at end of file diff --git a/artifacts/ios/assets/Default-667h@2x.png b/artifacts/ios/assets/Default-667h@2x.png deleted file mode 120000 index 7b6e25fbf..000000000 --- a/artifacts/ios/assets/Default-667h@2x.png +++ /dev/null @@ -1 +0,0 @@ -Default.png \ No newline at end of file diff --git a/artifacts/ios/assets/Default-736h@3x.png b/artifacts/ios/assets/Default-736h@3x.png deleted file mode 120000 index 7b6e25fbf..000000000 --- a/artifacts/ios/assets/Default-736h@3x.png +++ /dev/null @@ -1 +0,0 @@ -Default.png \ No newline at end of file diff --git a/artifacts/ios/assets/Default.png b/artifacts/ios/assets/Default.png deleted file mode 100644 index a97feb2672fbde096c96d3718e6d7b1879640d6c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 308 zcmeAS@N?(olHy`uVBq!ia0vp^j35jm7|ip2ssJg4WRD z45bDP46hOx7_4S6Fo+k-*%fF5lwc|e@(X5QD4TrN0>n%5c6VW5yxS$b1ju7A@$_|N zf5pxwVyx?STR9OZq*~${QIe8al4_M)lnSI6j0}v-bPddP4a`D}46IBItxQa{4GgRd z49BQ;SOya|>|mdBL1&1k}Uj>Eal|aXmQ! Y$Yx?-e5AO?7|3GqboFyt=akR{0DOT;qyPW_ diff --git a/artifacts/ios/assets/Default@2x.png b/artifacts/ios/assets/Default@2x.png deleted file mode 120000 index 7b6e25fbf..000000000 --- a/artifacts/ios/assets/Default@2x.png +++ /dev/null @@ -1 +0,0 @@ -Default.png \ No newline at end of file diff --git a/artifacts/ios/assets/Info.plist b/artifacts/ios/assets/Info.plist old mode 100644 new mode 100755 index 5831bbdb9..8f7b2eef6 --- a/artifacts/ios/assets/Info.plist +++ b/artifacts/ios/assets/Info.plist @@ -39,6 +39,8 @@ UIFileSharingEnabled + LSSupportsOpeningDocumentsInPlace + UIViewControllerBasedStatusBarAppearance CFBundleIconFiles @@ -47,8 +49,96 @@ Icon@2x.png Icon-72.png Icon-72@2x.png - Icon-Small-50.png - Icon-Small-50@2x.png + Icon-Small.png + Icon-Small@2x.png + + CFBundleIcons + + CFBundlePrimaryIcon + + CFBundleIconFiles + + Icon-20.png + Icon-20@2x.png + Icon-20@3x.png + Icon-40.png + Icon-40@2x.png + Icon-40@3x.png + Icon-60@2x.png + Icon-60@3x.png + + + + CFBundleIcons~ipad + + CFBundlePrimaryIcon + + CFBundleIconFiles + + Icon-20.png + Icon-20@2x.png + Icon-40.png + Icon-40@2x.png + Icon-72.png + Icon-72@2x.png + Icon-76.png + Icon-76@2x.png + Icon-83.5@2x.png + + + + UILaunchImages + + + UILaunchImageName + Default + UILaunchImageMinimumOSVersion + 2.0 + UILaunchImageOrientation + Portrait + UILaunchImageSize + {320, 480} + + + UILaunchImageName + Default-568h + UILaunchImageMinimumOSVersion + 7.0 + UILaunchImageOrientation + Portrait + UILaunchImageSize + {320, 568} + + + UILaunchImageName + Default-667h + UILaunchImageMinimumOSVersion + 8.0 + UILaunchImageOrientation + Portrait + UILaunchImageSize + {375, 667} + + + UILaunchImageName + Default-736h + UILaunchImageMinimumOSVersion + 8.0 + UILaunchImageOrientation + Portrait + UILaunchImageSize + {414, 736} + + + UILaunchImageName + Default-812h + UILaunchImageMinimumOSVersion + 11.0 + UILaunchImageOrientation + Portrait + UILaunchImageSize + {375, 812} + UISupportedInterfaceOrientations From 29c12ee7d502a067250f9f48e33b2dfaf9bea60a Mon Sep 17 00:00:00 2001 From: Un1q32 Date: Wed, 8 Jul 2026 23:43:05 -0400 Subject: [PATCH 099/109] launch images --- artifacts/ios/assets/Default-568h@2x.png | Bin 0 -> 410 bytes artifacts/ios/assets/Default-667h@2x.png | Bin 0 -> 444 bytes artifacts/ios/assets/Default-736h@3x.png | Bin 0 -> 657 bytes artifacts/ios/assets/Default-812h@3x.png | Bin 0 -> 656 bytes artifacts/ios/assets/Default.png | Bin 0 -> 340 bytes artifacts/ios/assets/Default@2x.png | Bin 0 -> 396 bytes 6 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 artifacts/ios/assets/Default-568h@2x.png create mode 100644 artifacts/ios/assets/Default-667h@2x.png create mode 100644 artifacts/ios/assets/Default-736h@3x.png create mode 100644 artifacts/ios/assets/Default-812h@3x.png create mode 100644 artifacts/ios/assets/Default.png create mode 100644 artifacts/ios/assets/Default@2x.png diff --git a/artifacts/ios/assets/Default-568h@2x.png b/artifacts/ios/assets/Default-568h@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..bba25c99d3b50d4d8587e1630a8a0ec96f15b468 GIT binary patch literal 410 zcmeAS@N?(olHy`uVBq!ia0y~yU}|7sU@2e(3NQqHxv>^VDI|LY`7$t6sWC7#v@kII z0tz*}U|=XUU|@Kaz`$TNgMmT3V9u^U8=wSJNswPK14G&5vlbv;lDE4H1LNH;;Uz#G zdx@v7EBh;UPG&8Ok36RhfkLV!t`Q|Ei6yC4$wjF^iowXh$V}J3T-U%d#K73f#K_9j zOxwV~%D~|CKi?B58glbfGSez?YuL5;YY9+;2Hb{{%-q!ClEmBs+%ohNev;lTJ>GceZy>AEz-q<_6kvF?hHo{HQb_g)@?~JCQe$9fXklRZ z1r%y{!N5>zz`*b-fq}tl1_Oh5!JJ)zHb4ock|4ie28Oc9XDvXyByV>Y2FANx!b^ZW z_7YEDSN2!zoXlEQ@=U8_fkLV!t`Q|Ei6yC4$wjF^iowXh$V}J3T-U%d#K73f#K_9j zLfgQ=%D`auL9?kS8glbfGSez?Yj`!MPZOv?18ze}W^QV6Nn&mRZasTD*M9@*ndIr> w7*cWT?Lk9OFdSO&)jyZ5egTjz_EZ4D8O*=WswPxQb_g)@?~JCQe$9fXklRZ z1r%y{!N5>zz`*b-fq}tl1_Oh5!JJ)zHb4ock|4ie28Oc9XDvXyByV>Y2FANx!b^ZW z_7YEDSN2!zoXpw`2SlCT0fkgcTq8BQ;SOya|>|m(R67!&A`Bz?&;zf xQgQ3;K|@e599rp=fS?83{1OUo>z%ybRRbqy>-42-QzjI9ie zwG9lc3=Hg^u$!W2$jwj5OsmAL!GGuePM`)2xD6$lxv9k^iMa*1^+>Fl7skNAnC9u? y7*cWT?LkIfpb3W-eD}}gGhYB?jsoh1z&>90W(LN#=cfcgW_r5%xvX45bDP46hOx7_4S6Fo+k-*%fF5lwc|e@(X5QD4TrN0>n%5c6VW5yxS$b1ju7A z@$_|Nf5pzptYw^bPiQAlNVUW@q9i4;B-JXpC>2OC7#SFu=^B{p8d!!H7+aYbS(zGY z8yHv_81O|FyP;^v%}>cptHiCLp5-*ir5bP>N-}d(i%Sx73vlbv5PYQy)T8C;;uunK r>+L~9P%s=?@YO$;(|-Ywi4N@8d>$|`zOL$i50de8^>bP0l+XkKTAoy! literal 0 HcmV?d00001 diff --git a/artifacts/ios/assets/Default@2x.png b/artifacts/ios/assets/Default@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..b8fa5d47aaef0fff7720a0a89961d364015adb4b GIT binary patch literal 396 zcmeAS@N?(olHy`uVBq!ia0y~yU}|7sU_QVI6kzC``85nkDI|LY`7$t6sWC7#v@kII z0tz*}U|=XUU|@Kaz`$TNgMmT3V9u^U8=wSJNswPK14G&5vlbv;lDE4H1LNH;;Uz#G zdx@v7EBh;UPG&9B{=M6KfI_Mzt`Q|Ei6yC4$wjF^iowXh$V}J3T-U%d#K73f#K_9j zSlhtB%D`aev7~$y4Y~O#nQ4`{HN2R8>oQP-2Hb{{%-q!ClEmBs+YvN4zW~Uj7O0TWKhMB;i)D5K$V5+9KbLh*2~7aTg;t0F literal 0 HcmV?d00001 From 4883bf15b23f5c9613b4752b0e74eef172894875 Mon Sep 17 00:00:00 2001 From: Un1q32 Date: Thu, 9 Jul 2026 00:06:02 -0400 Subject: [PATCH 100/109] update logging function to better tolerate non-jailbroken environment --- src/desktop/backends/ios.c | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/desktop/backends/ios.c b/src/desktop/backends/ios.c index 1dd556918..0eede8d40 100755 --- a/src/desktop/backends/ios.c +++ b/src/desktop/backends/ios.c @@ -1712,10 +1712,13 @@ extern int game_main(int argc, char *argv[]); @end int main(int argc, char *argv[]) { - freopen("/tmp/bsout", "w", stdout); - dup2(fileno(stdout), fileno(stderr)); - setbuf(stderr, NULL); - setbuf(stdout, NULL); + FILE *f = fopen("/tmp/bsout", "w"); + if (f) { + dup2(fileno(f), STDOUT_FILENO); + dup2(fileno(f), STDERR_FILENO); + setbuf(stdout, NULL); + setbuf(stderr, NULL); + } NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; int ret = UIApplicationMain(argc, argv, nil, @"AppDelegate"); From ebdfd7e7cb6340fbf47485037d0f684e1443b2e2 Mon Sep 17 00:00:00 2001 From: Un1q32 Date: Thu, 9 Jul 2026 01:24:27 -0400 Subject: [PATCH 101/109] folder layout change --- src/desktop/backends/ios.c | 122 +++++++++++++++++++++++++++++++++---- 1 file changed, 111 insertions(+), 11 deletions(-) diff --git a/src/desktop/backends/ios.c b/src/desktop/backends/ios.c index 0eede8d40..4171168e5 100755 --- a/src/desktop/backends/ios.c +++ b/src/desktop/backends/ios.c @@ -99,6 +99,10 @@ static atomic_bool g_highResEnabled = false; * game launch in startGameWithPath:. */ static char g_rendererArg[32] = "software"; +/* Save folder path, derived from the selected game's directory in + * startGameWithPath: and used in gameThread to pass --save-folder. */ +static char g_saveFolderPath[PATH_MAX]; + /* Games root(s) to scan. NSSearchPathForDirectoriesInDomains( * NSDocumentDirectory, ...) resolves correctly on every SDK back to iOS * 2, but what it resolves *to* -- and therefore what else is worth @@ -1235,6 +1239,7 @@ static UIKeyboardType bsNumericKeyboardType(void) { @interface BSGameListViewController : UITableViewController { NSMutableArray *games; NSIndexPath *pendingDeleteIndexPath; + NSIndexPath *longPressIndexPath; UIActivityIndicatorView *refreshIndicator; UIView *refreshOverlay; BOOL isRefreshing; @@ -1280,10 +1285,19 @@ static UIKeyboardType bsNumericKeyboardType(void) { refreshOverlay = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 100, 100)]; refreshOverlay.backgroundColor = [UIColor colorWithRed:0.0 green:0.0 blue:0.0 alpha:0.7]; refreshOverlay.hidden = YES; - refreshIndicator.center = CGPointMake(50, 50); [refreshOverlay addSubview:refreshIndicator]; } + + /* Add long-press gesture for game context menu (iOS 3.2+) */ + Class lpClass = NSClassFromString(@"UILongPressGestureRecognizer"); + if (lpClass) { + id lp = [lpClass alloc]; + lp = ((id(*)(id, SEL, id, SEL))objc_msgSend)(lp, @selector(initWithTarget:action:), self, @selector(handleLongPress:)); + ((void(*)(id, SEL, id))objc_msgSend)(self.tableView, @selector(addGestureRecognizer:), lp); + [lp release]; + } + } return self; } @@ -1310,8 +1324,18 @@ static UIKeyboardType bsNumericKeyboardType(void) { for (NSUInteger i = 0; i < [roots count]; i++) { NSString *root = [roots objectAtIndex:i]; + NSString *gamesDir = [root stringByAppendingPathComponent:@"games"]; + + /* Ensure the games directory exists for the primary root */ + if (i == 0) { + [[NSFileManager defaultManager] createDirectoryAtPath:gamesDir + withIntermediateDirectories:YES + attributes:nil + error:nil]; + } + NSError *error = nil; - NSArray *entries = [fm contentsOfDirectoryAtPath:root error:&error]; + NSArray *entries = [fm contentsOfDirectoryAtPath:gamesDir error:&error]; if (error) { /* Only the primary root (index 0) is expected to always be * readable -- it's either our own sandboxed container or a @@ -1322,10 +1346,10 @@ static UIKeyboardType bsNumericKeyboardType(void) { * something they can't do anything about. */ if (i == 0) { UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"Error" - message:[error localizedDescription] - delegate:nil - cancelButtonTitle:@"OK" - otherButtonTitles:nil]; + message:[error localizedDescription] + delegate:nil + cancelButtonTitle:@"OK" + otherButtonTitles:nil]; [alert show]; [alert release]; } @@ -1333,7 +1357,7 @@ static UIKeyboardType bsNumericKeyboardType(void) { } for (NSString *name in entries) { - NSString *dir = [root stringByAppendingPathComponent:name]; + NSString *dir = [gamesDir stringByAppendingPathComponent:name]; BOOL isDir = NO; if (![fm fileExistsAtPath:dir isDirectory:&isDir] || !isDir) continue; @@ -1444,6 +1468,42 @@ static UIKeyboardType bsNumericKeyboardType(void) { } - (void)alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex { + if (longPressIndexPath) { + if (buttonIndex == alertView.cancelButtonIndex) { + [longPressIndexPath release]; + longPressIndexPath = nil; + return; + } + + NSDictionary *entry = [games objectAtIndex:longPressIndexPath.row]; + NSString *dir = [entry objectForKey:@"path"]; + NSString *name = [entry objectForKey:@"name"]; + + if (buttonIndex == 2) { + /* "Delete Game" — reuse the existing confirmation flow */ + NSString *msg = [NSString stringWithFormat: + @"Delete \"%@\" and all of its save data? This cannot be undone.", name]; + pendingDeleteIndexPath = longPressIndexPath; + longPressIndexPath = nil; + UIAlertView *confirm = [[UIAlertView alloc] initWithTitle:@"Delete Game" + message:msg + delegate:self + cancelButtonTitle:@"Cancel" + otherButtonTitles:@"Delete", nil]; + [confirm show]; + [confirm release]; + } else { + /* "Delete Save Data" — remove only the saves folder */ + NSString *gamesRoot = [dir stringByDeletingLastPathComponent]; + NSString *butterscotchDir = [gamesRoot stringByDeletingLastPathComponent]; + NSString *saveDir = [[butterscotchDir stringByAppendingPathComponent:@"saves"] stringByAppendingPathComponent:name]; + [[NSFileManager defaultManager] removeItemAtPath:saveDir error:nil]; + [longPressIndexPath release]; + longPressIndexPath = nil; + } + return; + } + NSIndexPath *indexPath = pendingDeleteIndexPath; pendingDeleteIndexPath = nil; @@ -1456,8 +1516,14 @@ static UIKeyboardType bsNumericKeyboardType(void) { NSDictionary *entry = [games objectAtIndex:indexPath.row]; NSString *dir = [entry objectForKey:@"path"]; - /* removeItemAtPath: recursively removes directory contents, so this - * takes data.win and any save files sitting next to it in one shot. */ + /* Derive the corresponding saves folder and remove it too. */ + NSString *gameName = [dir lastPathComponent]; + NSString *gamesRoot = [dir stringByDeletingLastPathComponent]; + NSString *butterscotchDir = [gamesRoot stringByDeletingLastPathComponent]; + NSString *saveDir = [[butterscotchDir stringByAppendingPathComponent:@"saves"] stringByAppendingPathComponent:gameName]; + [[NSFileManager defaultManager] removeItemAtPath:saveDir error:nil]; + + /* removeItemAtPath: recursively removes directory contents. */ NSError *error = nil; if ([[NSFileManager defaultManager] removeItemAtPath:dir error:&error]) { [games removeObjectAtIndex:indexPath.row]; @@ -1480,7 +1546,28 @@ static UIKeyboardType bsNumericKeyboardType(void) { [delegate performSelector:@selector(startGameWithPath:) withObject:gameDir]; } +- (void)handleLongPress:(id)gesture { + if ([gesture state] != 1) return; /* UIGestureRecognizerStateBegan */ + + CGPoint p = [gesture locationInView:self.tableView]; + NSIndexPath *indexPath = [self.tableView indexPathForRowAtPoint:p]; + if (!indexPath) return; + + [longPressIndexPath release]; + longPressIndexPath = [indexPath retain]; + + NSString *name = [[games objectAtIndex:indexPath.row] objectForKey:@"name"]; + UIAlertView *alert = [[UIAlertView alloc] initWithTitle:name + message:nil + delegate:self + cancelButtonTitle:@"Cancel" + otherButtonTitles:@"Delete Save Data", @"Delete Game", nil]; + [alert show]; + [alert release]; +} + - (void)dealloc { + [longPressIndexPath release]; [pendingDeleteIndexPath release]; [games release]; [refreshIndicator release]; @@ -1522,8 +1609,9 @@ extern int game_main(int argc, char *argv[]); static char arg1[] = "--lazy-textures"; static char arg2[] = "--lazy-rooms"; static char arg3[] = "--renderer"; - char *argv[] = { arg0, arg1, arg2, g_ffSpeedArg, arg3, g_rendererArg, g_gamePath, NULL }; - game_main(7, argv); + static char arg4[] = "--save-folder"; + char *argv[] = { arg0, arg1, arg2, g_ffSpeedArg, arg3, g_rendererArg, arg4, g_saveFolderPath, g_gamePath, NULL }; + game_main(9, argv); [self performSelectorOnMainThread:@selector(returnToMenu) withObject:nil waitUntilDone:NO]; @@ -1536,6 +1624,18 @@ extern int game_main(int argc, char *argv[]); snprintf(g_ffSpeedArg, sizeof(g_ffSpeedArg), "--fast-forward-speed=%g", bsLoadFastForwardSpeed()); atomic_store(&g_highResEnabled, bsLoadHighResEnabled()); + /* Derive the save folder from the game path: strip "games/" to + * get the Butterscotch base, then append "saves/". */ + NSString *gameName = [gamePath lastPathComponent]; + NSString *gamesDir = [gamePath stringByDeletingLastPathComponent]; + NSString *butterscotchDir = [gamesDir stringByDeletingLastPathComponent]; + NSString *gameSaveDir = [[butterscotchDir stringByAppendingPathComponent:@"saves"] stringByAppendingPathComponent:gameName]; + [[NSFileManager defaultManager] createDirectoryAtPath:gameSaveDir + withIntermediateDirectories:YES + attributes:nil + error:nil]; + strlcpy(g_saveFolderPath, [gameSaveDir fileSystemRepresentation], sizeof(g_saveFolderPath)); + #if defined(ENABLE_MODERN_GL) && defined(ENABLE_SW_RENDERER) int rendererPref = bsLoadRendererPreference(); if (rendererPref == BS_RENDERER_MODERN_GL && bsSupportsGLES2()) { From 5abc2f3b7ae54fe3c884bfbee2da0b16669c449a Mon Sep 17 00:00:00 2001 From: Un1q32 Date: Thu, 9 Jul 2026 01:36:53 -0400 Subject: [PATCH 102/109] change log path --- src/desktop/backends/ios.c | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/src/desktop/backends/ios.c b/src/desktop/backends/ios.c index 4171168e5..16ac117e6 100755 --- a/src/desktop/backends/ios.c +++ b/src/desktop/backends/ios.c @@ -1812,7 +1812,23 @@ extern int game_main(int argc, char *argv[]); @end int main(int argc, char *argv[]) { - FILE *f = fopen("/tmp/bsout", "w"); + NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; + + NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES); + NSString *docs = [paths objectAtIndex:0]; + NSString *logPath; + if ([docs isEqualToString:@"/var/mobile/Documents"]) { + NSString *butterscotch = [docs stringByAppendingPathComponent:@"Butterscotch"]; + [[NSFileManager defaultManager] createDirectoryAtPath:butterscotch + withIntermediateDirectories:YES + attributes:nil + error:nil]; + logPath = [butterscotch stringByAppendingPathComponent:@"lastrun.log"]; + } else { + logPath = [docs stringByAppendingPathComponent:@"lastrun.log"]; + } + + FILE *f = fopen([logPath fileSystemRepresentation], "w"); if (f) { dup2(fileno(f), STDOUT_FILENO); dup2(fileno(f), STDERR_FILENO); @@ -1820,7 +1836,6 @@ int main(int argc, char *argv[]) { setbuf(stderr, NULL); } - NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; int ret = UIApplicationMain(argc, argv, nil, @"AppDelegate"); [pool release]; From e00a16ac193fe7d8184be82fe5a1042104c0cc42 Mon Sep 17 00:00:00 2001 From: Un1q32 Date: Thu, 9 Jul 2026 01:40:13 -0400 Subject: [PATCH 103/109] txt file --- src/desktop/backends/ios.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/desktop/backends/ios.c b/src/desktop/backends/ios.c index 16ac117e6..ee0365162 100755 --- a/src/desktop/backends/ios.c +++ b/src/desktop/backends/ios.c @@ -1823,9 +1823,9 @@ int main(int argc, char *argv[]) { withIntermediateDirectories:YES attributes:nil error:nil]; - logPath = [butterscotch stringByAppendingPathComponent:@"lastrun.log"]; + logPath = [butterscotch stringByAppendingPathComponent:@"latest_log.txt"]; } else { - logPath = [docs stringByAppendingPathComponent:@"lastrun.log"]; + logPath = [docs stringByAppendingPathComponent:@"latest_log.txt"]; } FILE *f = fopen([logPath fileSystemRepresentation], "w"); From 94746b61c4cca628f08d6cf73f4d6eaf9441de82 Mon Sep 17 00:00:00 2001 From: Un1q32 Date: Thu, 9 Jul 2026 02:34:57 -0400 Subject: [PATCH 104/109] iphone X fixes --- src/desktop/backends/ios.c | 33 +++++++++++++++++++++++++++++++-- 1 file changed, 31 insertions(+), 2 deletions(-) diff --git a/src/desktop/backends/ios.c b/src/desktop/backends/ios.c index ee0365162..15e4bb652 100755 --- a/src/desktop/backends/ios.c +++ b/src/desktop/backends/ios.c @@ -262,6 +262,15 @@ typedef struct { bool portrait; } BSLayout; +/* All notched iPhones have an aspect ratio > 2.0 (375x812, 414x896, + * 390x844, 428x926, etc). Older/non-notched devices are <= 1.78. */ +static bool bsDeviceHasNotch(void) { + CGRect bounds = [[UIScreen mainScreen] bounds]; + CGFloat maxDim = fmaxf(bounds.size.width, bounds.size.height); + CGFloat minDim = fminf(bounds.size.width, bounds.size.height); + return (maxDim / minDim > 2.0f); +} + static BSLayout computeLayout(CGSize screen) { BSLayout layout; layout.portrait = screen.height >= screen.width; @@ -280,8 +289,12 @@ static BSLayout computeLayout(CGSize screen) { CGFloat gameX = (screen.width - gameW) / 2.0f; layout.gameFrame = CGRectMake(gameX, 0, gameW, screen.height); - layout.dpadFrame = CGRectMake(10, (screen.height - 130) / 2.0f + 35, 110, 130); - layout.buttonsFrame = CGRectMake(screen.width - 10 - 170, (screen.height - 130) / 2.0f + 60, 170, 60); + /* On notched devices both landscape orientations have ~44pt of + * safe-area inset on each side (notch cutout zone). Shift the + * controls in so they're not behind the notch. */ + CGFloat notchInset = bsDeviceHasNotch() ? 44.0f : 0.0f; + layout.dpadFrame = CGRectMake(10 + notchInset, (screen.height - 130) / 2.0f + 35, 110, 130); + layout.buttonsFrame = CGRectMake(screen.width - notchInset - 10 - 170, (screen.height - 130) / 2.0f + 60, 170, 60); } layout.quitFrame = CGRectMake(screen.width - BS_QUIT_BUTTON_MARGIN - BS_QUIT_BUTTON_SIZE, @@ -1185,6 +1198,8 @@ static UIKeyboardType bsNumericKeyboardType(void) { - (id)init { if ((self = [super init])) { self.title = @"Settings"; + if ([self respondsToSelector:@selector(setEdgesForExtendedLayout:)]) + self.edgesForExtendedLayout = UIRectEdgeNone; UIBarButtonItem *saveItem = [[UIBarButtonItem alloc] initWithTitle:@"Save" style:UIBarButtonItemStyleDone target:self action:@selector(saveTapped)]; @@ -1388,6 +1403,20 @@ static UIKeyboardType bsNumericKeyboardType(void) { } } +- (void)viewDidLoad { + [super viewDidLoad]; + /* Disable all automatic inset adjustment — the 7.0 SDK's + * automaticallyAdjustsScrollViewInsets doesn't understand the XS safe + * area (44pt status) and only insets 64pt (44 nav + 20 old-style status). + * On iOS 11+ the KVC call also disables the modern equivalent. */ + self.automaticallyAdjustsScrollViewInsets = NO; + if ([self.tableView respondsToSelector:@selector(setContentInsetAdjustmentBehavior:)]) + [self.tableView setValue:@0 forKey:@"contentInsetAdjustmentBehavior"]; + CGFloat topInset = bsDeviceHasNotch() ? 88.0f : 64.0f; + self.tableView.contentInset = UIEdgeInsetsMake(topInset, 0, 0, 0); + self.tableView.scrollIndicatorInsets = self.tableView.contentInset; +} + - (void)viewWillAppear:(BOOL)animated { [super viewWillAppear:animated]; [self reloadGames]; From f1059ef77881182ab4ec6c9f0181276b7637ac4d Mon Sep 17 00:00:00 2001 From: Un1q32 Date: Thu, 9 Jul 2026 02:40:47 -0400 Subject: [PATCH 105/109] more fix --- src/desktop/backends/ios.c | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/src/desktop/backends/ios.c b/src/desktop/backends/ios.c index 15e4bb652..e1321aff1 100755 --- a/src/desktop/backends/ios.c +++ b/src/desktop/backends/ios.c @@ -1403,18 +1403,17 @@ static UIKeyboardType bsNumericKeyboardType(void) { } } -- (void)viewDidLoad { - [super viewDidLoad]; - /* Disable all automatic inset adjustment — the 7.0 SDK's - * automaticallyAdjustsScrollViewInsets doesn't understand the XS safe - * area (44pt status) and only insets 64pt (44 nav + 20 old-style status). - * On iOS 11+ the KVC call also disables the modern equivalent. */ - self.automaticallyAdjustsScrollViewInsets = NO; - if ([self.tableView respondsToSelector:@selector(setContentInsetAdjustmentBehavior:)]) - [self.tableView setValue:@0 forKey:@"contentInsetAdjustmentBehavior"]; - CGFloat topInset = bsDeviceHasNotch() ? 88.0f : 64.0f; - self.tableView.contentInset = UIEdgeInsetsMake(topInset, 0, 0, 0); - self.tableView.scrollIndicatorInsets = self.tableView.contentInset; +- (void)viewDidLayoutSubviews { + [super viewDidLayoutSubviews]; + /* The 7.0 SDK's automaticallyAdjustsScrollViewInsets only insets 64pt + * (44 nav + 20 old-style status). On a notched device the status area + * is 44pt, so we need 24pt more for the first row to be fully visible. */ + if (bsDeviceHasNotch() && self.tableView.contentInset.top < 66.0f) { + UIEdgeInsets inset = self.tableView.contentInset; + inset.top = 88.0f; + self.tableView.contentInset = inset; + self.tableView.scrollIndicatorInsets = inset; + } } - (void)viewWillAppear:(BOOL)animated { From 692c08012052bf8cde43c89ca5c392bfd57e74b0 Mon Sep 17 00:00:00 2001 From: Un1q32 Date: Thu, 9 Jul 2026 03:09:22 -0400 Subject: [PATCH 106/109] ok --- artifacts/ios/assets/Info.plist | 2 -- 1 file changed, 2 deletions(-) diff --git a/artifacts/ios/assets/Info.plist b/artifacts/ios/assets/Info.plist index 8f7b2eef6..fe149fdff 100755 --- a/artifacts/ios/assets/Info.plist +++ b/artifacts/ios/assets/Info.plist @@ -26,8 +26,6 @@ iPhoneOS - LSRequiresIPhoneOS - MinimumOSVersion 2.0 UIDeviceFamily From 2ae4e65142034335d8007bfc04499e8e9af53a32 Mon Sep 17 00:00:00 2001 From: Un1q32 Date: Thu, 9 Jul 2026 04:21:46 -0400 Subject: [PATCH 107/109] iOS 2 --- src/desktop/backends/ios.c | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/desktop/backends/ios.c b/src/desktop/backends/ios.c index e1321aff1..20ff05f95 100755 --- a/src/desktop/backends/ios.c +++ b/src/desktop/backends/ios.c @@ -1199,7 +1199,7 @@ static UIKeyboardType bsNumericKeyboardType(void) { if ((self = [super init])) { self.title = @"Settings"; if ([self respondsToSelector:@selector(setEdgesForExtendedLayout:)]) - self.edgesForExtendedLayout = UIRectEdgeNone; + ((void(*)(id, SEL, NSUInteger))objc_msgSend)(self, @selector(setEdgesForExtendedLayout:), 0); UIBarButtonItem *saveItem = [[UIBarButtonItem alloc] initWithTitle:@"Save" style:UIBarButtonItemStyleDone target:self action:@selector(saveTapped)]; @@ -1251,6 +1251,14 @@ static UIKeyboardType bsNumericKeyboardType(void) { * show up. * ------------------------------------------------------------------- */ +/* Forward-declare viewDidLayoutSubviews on UITableViewController so the + * compiler doesn't warn when we call super on it -- the method is only + * called at runtime when the runtime supports it (iOS 5+), guarded by + * instancesRespondToSelector:. */ +@interface UITableViewController (BSIOS5Compat) +- (void)viewDidLayoutSubviews; +@end + @interface BSGameListViewController : UITableViewController { NSMutableArray *games; NSIndexPath *pendingDeleteIndexPath; @@ -1404,7 +1412,8 @@ static UIKeyboardType bsNumericKeyboardType(void) { } - (void)viewDidLayoutSubviews { - [super viewDidLayoutSubviews]; + if ([UIViewController instancesRespondToSelector:@selector(viewDidLayoutSubviews)]) + [super viewDidLayoutSubviews]; /* The 7.0 SDK's automaticallyAdjustsScrollViewInsets only insets 64pt * (44 nav + 20 old-style status). On a notched device the status area * is 44pt, so we need 24pt more for the first row to be fully visible. */ From 498ff3e613529dcafc11e8e844415eb29ef072cd Mon Sep 17 00:00:00 2001 From: Un1q32 Date: Thu, 9 Jul 2026 05:28:12 -0400 Subject: [PATCH 108/109] joystick --- src/desktop/backends/ios.c | 69 ++++++++++++++++++++++++-------------- 1 file changed, 43 insertions(+), 26 deletions(-) diff --git a/src/desktop/backends/ios.c b/src/desktop/backends/ios.c index 20ff05f95..5f044e980 100755 --- a/src/desktop/backends/ios.c +++ b/src/desktop/backends/ios.c @@ -237,12 +237,13 @@ static void bsDrainKeyEvents(void) { /* --------------------------------------------------------------------- * Touch control layout * - * Portrait: game view anchored to the top of the screen, dpad + z/x/c - * strip along the bottom (gameboy-ish). Landscape: game view centered, - * dpad on the left, z/x/c on the right. If the requested aspect ratio - * needs more space than what's left after reserving the control strip, - * the game view is allowed to grow into the control area rather than - * get squashed -- the controls are translucent, so this is fine. + * Portrait: game view anchored to the top of the screen, joystick + + * z/x/c strip along the bottom (gameboy-ish). Landscape: game view + * centered, joystick on the left, z/x/c on the right. If the requested + * aspect ratio needs more space than what's left after reserving the + * control strip, the game view is allowed to grow into the control + * area rather than get squashed -- the controls are translucent, so + * this is fine. * ------------------------------------------------------------------- */ #define BS_CONTROL_STRIP_PORTRAIT_H 160.0f @@ -306,19 +307,23 @@ static BSLayout computeLayout(CGSize screen) { return layout; } -static void dpadArmRects(CGRect dpad, CGRect *up, CGRect *down, CGRect *left, CGRect *right) { +static CGFloat joystickRadius(CGRect dpad) { + return fminf(dpad.size.width, dpad.size.height) * 0.45f; +} + +static CGPoint joystickClampOffset(CGRect dpad, CGPoint touchPoint) { CGFloat cx = dpad.origin.x + dpad.size.width / 2.0f; CGFloat cy = dpad.origin.y + dpad.size.height / 2.0f; - CGFloat armW = dpad.size.width / 3.0f; - CGFloat armH = dpad.size.height / 3.0f; - *up = CGRectMake(cx - armW / 2.0f, dpad.origin.y, armW, armH); - *down = CGRectMake(cx - armW / 2.0f, dpad.origin.y + dpad.size.height - armH, armW, armH); - *left = CGRectMake(dpad.origin.x, cy - armH / 2.0f, armW, armH); - *right = CGRectMake(dpad.origin.x + dpad.size.width - armW, cy - armH / 2.0f, armW, armH); + CGFloat dx = touchPoint.x - cx; + CGFloat dy = touchPoint.y - cy; + CGFloat maxR = joystickRadius(dpad); + CGFloat dist = sqrtf(dx*dx + dy*dy); + if (dist < maxR) return CGPointMake(dx, dy); + return CGPointMake(dx / dist * maxR, dy / dist * maxR); } -/* 8-way split around the dpad's center so a single touch can express - * diagonals (e.g. up+left) without needing two fingers. */ +/* 8-way split around the joystick's center so a single touch can + * express diagonals (e.g. up+left) without needing two fingers. */ static void dpadDirectionsForPoint(CGRect dpad, CGPoint p, bool *up, bool *down, bool *left, bool *right) { CGFloat cx = dpad.origin.x + dpad.size.width / 2.0f; CGFloat cy = dpad.origin.y + dpad.size.height / 2.0f; @@ -759,7 +764,7 @@ void platformSleepUntil(uint64_t time) { @end /* --------------------------------------------------------------------- - * On-screen touch controls: dpad + z/x/c + quit. Sits as a sibling of + * On-screen touch controls: joystick + z/x/c + quit. Sits as a sibling of * GLView, sized to the full screen (so it can draw controls in the area * outside the game view, and translucently over it when the game view * grows into that area). @@ -768,6 +773,7 @@ void platformSleepUntil(uint64_t time) { @interface BSTouchOverlay : UIView { UITouch *dpadTouch; int32_t dpadKeysDown[4]; /* up, down, left, right */ + CGPoint joystickThumbOffset; UITouch *buttonTouches[3]; /* z, x, c */ UITouch *quitTouch; UITouch *ffTouch; @@ -787,6 +793,7 @@ void platformSleepUntil(uint64_t time) { quitTouch = nil; ffTouch = nil; for (int i = 0; i < 4; i++) dpadKeysDown[i] = 0; + joystickThumbOffset = CGPointZero; for (int i = 0; i < 3; i++) buttonTouches[i] = nil; } return self; @@ -829,17 +836,24 @@ static void drawCenteredLabel(NSString *text, CGRect rect, UIFont *font) { CGContextRef ctx = UIGraphicsGetCurrentContext(); BSLayout bsLayout = computeLayout(self.bounds.size); - CGRect up, down, left, right; - dpadArmRects(bsLayout.dpadFrame, &up, &down, &left, &right); + CGFloat cx = bsLayout.dpadFrame.origin.x + bsLayout.dpadFrame.size.width / 2.0f; + CGFloat cy = bsLayout.dpadFrame.origin.y + bsLayout.dpadFrame.size.height / 2.0f; + CGFloat baseR = joystickRadius(bsLayout.dpadFrame); + CGRect baseRect = CGRectMake(cx - baseR, cy - baseR, baseR * 2, baseR * 2); + + BOOL anyDpadDown = dpadKeysDown[0] || dpadKeysDown[1] || dpadKeysDown[2] || dpadKeysDown[3]; + CGFloat baseAlpha = anyDpadDown ? 0.55f : 0.28f; + CGContextSetRGBFillColor(ctx, 1.0f, 1.0f, 1.0f, baseAlpha); + CGContextFillEllipseInRect(ctx, baseRect); + CGContextSetRGBStrokeColor(ctx, 1.0f, 1.0f, 1.0f, 0.6f); + CGContextStrokeEllipseInRect(ctx, baseRect); - CGContextSetRGBFillColor(ctx, 1.0f, 1.0f, 1.0f, dpadKeysDown[0] ? 0.55f : 0.28f); - CGContextFillRect(ctx, up); - CGContextSetRGBFillColor(ctx, 1.0f, 1.0f, 1.0f, dpadKeysDown[1] ? 0.55f : 0.28f); - CGContextFillRect(ctx, down); - CGContextSetRGBFillColor(ctx, 1.0f, 1.0f, 1.0f, dpadKeysDown[2] ? 0.55f : 0.28f); - CGContextFillRect(ctx, left); - CGContextSetRGBFillColor(ctx, 1.0f, 1.0f, 1.0f, dpadKeysDown[3] ? 0.55f : 0.28f); - CGContextFillRect(ctx, right); + CGFloat thumbR = baseR * 0.35f; + CGFloat thumbX = cx + joystickThumbOffset.x; + CGFloat thumbY = cy + joystickThumbOffset.y; + CGRect thumbRect = CGRectMake(thumbX - thumbR, thumbY - thumbR, thumbR * 2, thumbR * 2); + CGContextSetRGBFillColor(ctx, 1.0f, 1.0f, 1.0f, 0.55f); + CGContextFillEllipseInRect(ctx, thumbRect); CGRect actionRects[3]; actionButtonRects(bsLayout.buttonsFrame, actionRects); @@ -897,6 +911,7 @@ static void drawCenteredLabel(NSString *text, CGRect rect, UIFont *font) { bool up, down, left, right; dpadDirectionsForPoint(bsLayout.dpadFrame, p, &up, &down, &left, &right); [self updateDpadUp:up down:down left:left right:right]; + joystickThumbOffset = joystickClampOffset(bsLayout.dpadFrame, p); [self setNeedsDisplay]; continue; } @@ -923,6 +938,7 @@ static void drawCenteredLabel(NSString *text, CGRect rect, UIFont *font) { bool up, down, left, right; dpadDirectionsForPoint(bsLayout.dpadFrame, p, &up, &down, &left, &right); [self updateDpadUp:up down:down left:left right:right]; + joystickThumbOffset = joystickClampOffset(bsLayout.dpadFrame, p); [self setNeedsDisplay]; break; } @@ -933,6 +949,7 @@ static void drawCenteredLabel(NSString *text, CGRect rect, UIFont *font) { for (UITouch *touch in touches) { if (touch == dpadTouch) { [self updateDpadUp:false down:false left:false right:false]; + joystickThumbOffset = CGPointZero; [dpadTouch release]; dpadTouch = nil; [self setNeedsDisplay]; From c369bb276b3c25fa96fe48a8b2783d3fa18e6158 Mon Sep 17 00:00:00 2001 From: Un1q32 Date: Thu, 9 Jul 2026 08:14:50 -0400 Subject: [PATCH 109/109] per chunk --- src/desktop/backends/ios.c | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/desktop/backends/ios.c b/src/desktop/backends/ios.c index 5f044e980..4870e3b05 100755 --- a/src/desktop/backends/ios.c +++ b/src/desktop/backends/ios.c @@ -1662,10 +1662,11 @@ extern int game_main(int argc, char *argv[]); static char arg0[] = "butterscotch"; static char arg1[] = "--lazy-textures"; static char arg2[] = "--lazy-rooms"; - static char arg3[] = "--renderer"; - static char arg4[] = "--save-folder"; - char *argv[] = { arg0, arg1, arg2, g_ffSpeedArg, arg3, g_rendererArg, arg4, g_saveFolderPath, g_gamePath, NULL }; - game_main(9, argv); + static char arg3[] = "--load-type=load-per-chunk"; + static char arg4[] = "--renderer"; + static char arg5[] = "--save-folder"; + char *argv[] = { arg0, arg1, arg2, arg3, g_ffSpeedArg, arg4, g_rendererArg, arg5, g_saveFolderPath, g_gamePath, NULL }; + game_main(10, argv); [self performSelectorOnMainThread:@selector(returnToMenu) withObject:nil waitUntilDone:NO];