diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 3506563e8..d8f3c4494 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -56,3 +56,16 @@ jobs: run: | SDK="$(xcrun --sdk iphoneos --show-sdk-path)" zig build -Dtarget=aarch64-ios -Dinclude_path="$SDK/usr/include" -Dframework_path="$SDK/System/Library/Frameworks" --summary all + + - name: Setup Emscripten + if: runner.os == 'Linux' + uses: mymindstorm/setup-emsdk@v14 + with: + version: 6.0.12 + + - name: Build for Emscripten + if: runner.os == 'Linux' + # The Emscripten sysroot is not bundled with Zig, and emsdk only populates it on first use. + run: | + embuilder build sysroot + zig build example -Dtarget=wasm32-emscripten -Dinclude_path="$(em-config CACHE)/sysroot/include" --summary all diff --git a/README.md b/README.md index 559f404f5..8203a85d5 100644 --- a/README.md +++ b/README.md @@ -55,6 +55,18 @@ zig build run-example This should produce a window with a pulsing gradient. +For Emscripten, the example is linked with `emcc` into `zig-out/web`, and `run-example` opens it in +your browser with `emrun`. Both need to be on your `PATH`, e.g. by sourcing `emsdk_env.sh`. On a +fresh emsdk install, the sysroot headers must be generated before the first build: + +```sh +embuilder build sysroot +zig build run-example -Dtarget=wasm32-emscripten -Dinclude_path="$(em-config CACHE)/sysroot/include" +``` + +Zig can't link Emscripten executables, so your own app will need to be linked with `emcc` too. See +`build.zig` for an example of how to do this. + ## Help, `SDL_Init` failed on Linux! ```sh @@ -114,7 +126,10 @@ This library provides a default configuration for common targets: `-Dinclude_path=$SDK/usr/include -Dframework_path=$SDK/System/Library/Frameworks` with `SDK=$(xcrun --sdk iphoneos --show-sdk-path)`) - [x] Android (pass `-Dinclude_path=/toolchains/llvm/prebuilt//sysroot/usr/include`) -- [ ] [Emscripten (help wanted!)](https://github.com/allyourcodebase/SDL/issues/5) +- [x] Emscripten (the Emscripten sysroot is not bundled with Zig; pass + `-Dinclude_path=$(em-config CACHE)/sysroot/include` with + [emsdk](https://emscripten.org/docs/getting_started/downloads.html) activated, static linkage + only) - [ ] [Consoles (help wanted!)](https://github.com/allyourcodebase/SDL/issues/6) You can override the default target configuration by setting `default_target_config` to `false`, and diff --git a/build.zig b/build.zig index 88a608e52..538a4a679 100644 --- a/build.zig +++ b/build.zig @@ -4,6 +4,7 @@ const windows = @import("src/windows.zig"); const macos = @import("src/macos.zig"); const ios = @import("src/ios.zig"); const android = @import("src/android.zig"); +const emscripten = @import("src/emscripten.zig"); const build_zon = @import("build.zig.zon"); const assert = std.debug.assert; @@ -51,6 +52,12 @@ pub fn build(b: *std.Build) !void { , ) orelse .static; + // Upstream doesn't support building SDL as a shared library for Emscripten + if (target.result.os.tag == .emscripten and linkage == .dynamic) { + std.log.err("'-Dlinkage=dynamic' is not supported when building SDL for Emscripten", .{}); + std.process.exit(1); + } + // Cross-compiling to a target whose SDK isn't bundled with Zig (iOS, Android) needs the // SDK's headers/frameworks/libs passed in explicitly. Scoped to the SDL library only, unlike // `--sysroot` this does not re-roots paths across the whole build graph. @@ -103,6 +110,15 @@ pub fn build(b: *std.Build) !void { .flags = flags, }); + // Emscripten provides its own main callbacks implementation + if (target.result.os.tag != .emscripten) { + lib.root_module.addCSourceFiles(.{ + .files = &sources.main_callbacks_generic, + .root = upstream.path("src"), + .flags = flags, + }); + } + if (default_target_config) { const build_config_h = b.addConfigHeader(.{ .style = .{ .cmake = upstream.path("include/build_config/SDL_build_config.h.cmake") }, @@ -136,6 +152,7 @@ pub fn build(b: *std.Build) !void { .windows => windows.build(b, target.result, lib, build_config_h), .macos => macos.build(b, target.result, lib, build_config_h), .ios => ios.build(b, target.result, lib, build_config_h, system_paths), + .emscripten => emscripten.build(b, target.result, lib, build_config_h, system_paths), else => @panic("target has no default config"), } } @@ -152,26 +169,61 @@ pub fn build(b: *std.Build) !void { translate_c.defineCMacro("USING_GENERATED_CONFIG_H", "1"); translate_c.addIncludePath(upstream.path("include")); translate_c.addIncludePath(upstream.path("src/video/khronos")); + if (target.result.os.tag == .emscripten) { + // The SDL headers include libc headers, which Zig doesn't bundle for Emscripten + if (system_paths.include) |include_path| translate_c.addSystemIncludePath(include_path); + } const module = translate_c.addModule("sdl3"); module.linkLibrary(lib); // Add the example - const example = b.addExecutable(.{ - .name = "example", - .root_module = b.createModule(.{ - .root_source_file = b.path("src/example.zig"), - .target = target, - .optimize = optimize, - }), - }); - example.root_module.addImport("sdl3", module); - const build_example_step = b.step("example", "Build the example app"); - build_example_step.dependOn(&example.step); - - const run_example = b.addRunArtifact(example); const run_step = b.step("run-example", "Run the example app"); - run_step.dependOn(&run_example.step); + if (target.result.os.tag == .emscripten) { + // Zig can't link Emscripten executables, so build the example as a static library and + // link it with `emcc`, which also generates the JavaScript and HTML needed to run it. + const example = b.addLibrary(.{ + .name = "example", + .root_module = b.createModule(.{ + .root_source_file = b.path("src/example.zig"), + .target = target, + .optimize = optimize, + .link_libc = true, + }), + }); + example.root_module.addImport("sdl3", module); + + const emcc = b.addSystemCommand(&.{ "emcc", "-sALLOW_MEMORY_GROWTH" }); + emcc.addArtifactArg(example); + emcc.addArtifactArg(lib); + const html = emcc.addPrefixedOutputFileArg("-o", "example.html"); + + // `emcc` writes the `.js` and `.wasm` files next to the `.html` file + const install_example = b.addInstallDirectory(.{ + .source_dir = html.dirname(), + .install_dir = .prefix, + .install_subdir = "web", + }); + build_example_step.dependOn(&install_example.step); + + const run_example = b.addSystemCommand(&.{"emrun"}); + run_example.addFileArg(html); + run_step.dependOn(&run_example.step); + } else { + const example = b.addExecutable(.{ + .name = "example", + .root_module = b.createModule(.{ + .root_source_file = b.path("src/example.zig"), + .target = target, + .optimize = optimize, + }), + }); + example.root_module.addImport("sdl3", module); + build_example_step.dependOn(&example.step); + + const run_example = b.addRunArtifact(example); + run_step.dependOn(&run_example.step); + } } /// Fetches a lazy dependency, unless provided via `-fsys`. diff --git a/src/emscripten.zig b/src/emscripten.zig new file mode 100644 index 000000000..7883ddc73 --- /dev/null +++ b/src/emscripten.zig @@ -0,0 +1,282 @@ +const std = @import("std"); +const sources = @import("sdl.zon"); +const root = @import("../build.zig"); + +pub fn build( + b: *std.Build, + target: std.Target, + lib: *std.Build.Step.Compile, + build_config_h: *std.Build.Step.ConfigHeader, + paths: root.SystemPaths, +) void { + _ = target; + + const sdl = b.dependency("sdl", .{}); + + // The Emscripten sysroot is not bundled with Zig; it's provided by emsdk and populated the + // first time `emcc` runs. + const include_path = root.SystemPaths.print_missing_system_path_option( + paths.include, + "include_path ($EMSDK/upstream/emscripten/cache/sysroot/include)", + "Emscripten", + ); + lib.root_module.addSystemIncludePath(include_path); + + // Zig's UBSan runtime can't be built for Emscripten, and the final link is done by `emcc` + // which doesn't provide it by default, so trap on undefined behavior instead of calling into a + // runtime. Other optimize modes don't sanitize C code by default. + switch (lib.root_module.optimize orelse std.builtin.OptimizeMode.Debug) { + .Debug, .ReleaseSafe => lib.root_module.sanitize_c = .trap, + .ReleaseFast, .ReleaseSmall => {}, + } + + // Add the platform specific sources. Threads are disabled by default on Emscripten (enabling + // them requires `-pthread` and cross-origin isolation), so only the generic thread stubs are + // needed. + lib.root_module.addCMacro("GL_GLEXT_PROTOTYPES", "1"); + lib.root_module.addCSourceFiles(.{ + // The core/video/audio/sensor/camera Emscripten sources live in the generic group that + // build.zig already compiles for every target; listing them here too duplicates them. + .files = &(sources.emscripten ++ sources.thread_generic), + .root = sdl.path("src"), + .flags = root.flags, + }); + + // Set the platform specific build config, mirroring `cmake/PreseedEmscriptenCache.cmake` and + // the Emscripten section of upstream's `CMakeLists.txt`. + build_config_h.addValues(.{ + .HAVE_GCC_ATOMICS = 1, + + // Useful headers + .HAVE_FLOAT_H = 1, + .HAVE_STDARG_H = 1, + .HAVE_STDDEF_H = 1, + .HAVE_STDINT_H = 1, + .HAVE_LIBC = 1, + .HAVE_ALLOCA_H = 1, + .HAVE_ICONV_H = 1, + .HAVE_INTTYPES_H = 1, + .HAVE_LIMITS_H = 1, + .HAVE_MALLOC_H = 1, + .HAVE_MATH_H = 1, + .HAVE_MEMORY_H = 1, + .HAVE_SIGNAL_H = 1, + .HAVE_STDIO_H = 1, + .HAVE_STDLIB_H = 1, + .HAVE_STRINGS_H = 1, + .HAVE_STRING_H = 1, + .HAVE_SYS_TYPES_H = 1, + .HAVE_WCHAR_H = 1, + + // C library functions + .HAVE_MALLOC = 1, + .HAVE_GETENV = 1, + .HAVE_GETHOSTNAME = 1, + .HAVE_SETENV = 1, + .HAVE_PUTENV = 1, + .HAVE_UNSETENV = 1, + .HAVE_ABS = 1, + .HAVE_BCOPY = 1, + .HAVE_MEMSET = 1, + .HAVE_MEMCPY = 1, + .HAVE_MEMMOVE = 1, + .HAVE_MEMCMP = 1, + .HAVE_WCSLEN = 1, + .HAVE_WCSNLEN = 1, + .HAVE_WCSCMP = 1, + .HAVE_WCSNCMP = 1, + .HAVE_WCSSTR = 1, + .HAVE_WCSTOL = 1, + .HAVE_STRLEN = 1, + .HAVE_STRNLEN = 1, + .HAVE_STRLCPY = 1, + .HAVE_STRLCAT = 1, + .HAVE_INDEX = 1, + .HAVE_RINDEX = 1, + .HAVE_STRCHR = 1, + .HAVE_STRRCHR = 1, + .HAVE_STRSTR = 1, + .HAVE_STRTOK_R = 1, + .HAVE_STRTOL = 1, + .HAVE_STRTOUL = 1, + .HAVE_STRTOLL = 1, + .HAVE_STRTOULL = 1, + .HAVE_STRTOD = 1, + .HAVE_ATOI = 1, + .HAVE_ATOF = 1, + .HAVE_STRCMP = 1, + .HAVE_STRNCMP = 1, + .HAVE_STRPBRK = 1, + .HAVE_VSSCANF = 1, + .HAVE_VSNPRINTF = 1, + .HAVE_ACOS = 1, + .HAVE_ACOSF = 1, + .HAVE_ASIN = 1, + .HAVE_ASINF = 1, + .HAVE_ATAN = 1, + .HAVE_ATANF = 1, + .HAVE_ATAN2 = 1, + .HAVE_ATAN2F = 1, + .HAVE_CEIL = 1, + .HAVE_CEILF = 1, + .HAVE_COPYSIGN = 1, + .HAVE_COPYSIGNF = 1, + .HAVE_COS = 1, + .HAVE_COSF = 1, + .HAVE_EXP = 1, + .HAVE_EXPF = 1, + .HAVE_FABS = 1, + .HAVE_FABSF = 1, + .HAVE_FLOOR = 1, + .HAVE_FLOORF = 1, + .HAVE_FMOD = 1, + .HAVE_FMODF = 1, + .HAVE_ISINF = 1, + .HAVE_ISINFF = 1, + .HAVE_ISINF_FLOAT_MACRO = 1, + .HAVE_ISNAN = 1, + .HAVE_ISNANF = 1, + .HAVE_ISNAN_FLOAT_MACRO = 1, + .HAVE_LOG = 1, + .HAVE_LOGF = 1, + .HAVE_LOG10 = 1, + .HAVE_LOG10F = 1, + .HAVE_LROUND = 1, + .HAVE_LROUNDF = 1, + .HAVE_MODF = 1, + .HAVE_MODFF = 1, + .HAVE_POW = 1, + .HAVE_POWF = 1, + .HAVE_ROUND = 1, + .HAVE_ROUNDF = 1, + .HAVE_SCALBN = 1, + .HAVE_SCALBNF = 1, + .HAVE_SIN = 1, + .HAVE_SINF = 1, + .HAVE_SQRT = 1, + .HAVE_SQRTF = 1, + .HAVE_TAN = 1, + .HAVE_TANF = 1, + .HAVE_TRUNC = 1, + .HAVE_TRUNCF = 1, + .HAVE__EXIT = 1, + .HAVE_FOPEN64 = 1, + .HAVE_FSEEKO = 1, + .HAVE_FSEEKO64 = 1, + .HAVE_SIGACTION = 1, + .HAVE_SA_SIGACTION = 1, + .HAVE_ST_MTIM = 1, + .HAVE_SETJMP = 1, + .HAVE_NANOSLEEP = 1, + .HAVE_GMTIME_R = 1, + .HAVE_LOCALTIME_R = 1, + .HAVE_NL_LANGINFO = 1, + .HAVE_SYSCONF = 1, + .HAVE_CLOCK_GETTIME = 1, + .HAVE_GETPAGESIZE = 1, + .HAVE_O_CLOEXEC = 1, + .HAVE_POSIX_FALLOCATE = 1, + .HAVE_FDATASYNC = 1, + .HAVE_SIGTIMEDWAIT = 1, + + // Enable various audio drivers + .SDL_AUDIO_DRIVER_EMSCRIPTEN = 1, + + // Enable various input drivers. There's no HIDAPI or haptics in the browser. + .SDL_HIDAPI_DISABLED = 1, + .SDL_JOYSTICK_EMSCRIPTEN = 1, + .SDL_JOYSTICK_VIRTUAL = 1, + .SDL_HAPTIC_DUMMY = 1, + .SDL_SENSOR_EMSCRIPTEN = 1, + + // Enable various process implementations + .SDL_PROCESS_DUMMY = 1, + + // Enable various shared object loading systems. Emscripten has no general purpose dynamic + // library loading, and upstream disables the dynamic API on this platform. + .SDL_LOADSO_DUMMY = 1, + + // Disable threads, matching upstream's default for Emscripten without `-pthread` + .SDL_THREADS_DISABLED = 1, + + // Enable various RTC systems + .SDL_TIME_UNIX = 1, + + // Enable various timer systems + .SDL_TIMER_UNIX = 1, + + // Enable various video drivers + .SDL_VIDEO_DRIVER_EMSCRIPTEN = 1, + + // Enable video render APIs. The browser only exposes WebGL, which maps to GLES2. + .SDL_VIDEO_RENDER_OGL_ES2 = 1, + .SDL_VIDEO_OPENGL_ES2 = 1, + + // Enable system power support + .SDL_POWER_EMSCRIPTEN = 1, + + // The browser has no Steam client + .SDL_STORAGE_STEAM = 0, + + // Enable filesystem support + .SDL_FILESYSTEM_EMSCRIPTEN = 1, + .SDL_FSOPS_POSIX = 1, + + // Enable camera driver + .SDL_CAMERA_DRIVER_EMSCRIPTEN = 1, + .SDL_CAMERA_DRIVER_DUMMY = 1, + + // Enable the dummy dialog/tray implementations, there's no native equivalent in the browser + .SDL_DIALOG_DUMMY = 1, + .SDL_TRAY_DUMMY = 1, + + // Unused + .HAVE_PTHREAD_SETNAME_NP = 0, + .HAVE_SYSCTLBYNAME = 0, + .SDL_AUDIO_DRIVER_COREAUDIO = 0, + .SDL_AUDIO_DRIVER_ALSA_DYNAMIC = "", + .SDL_AUDIO_DRIVER_JACK_DYNAMIC = "", + .SDL_AUDIO_DRIVER_PIPEWIRE_DYNAMIC = "", + .SDL_AUDIO_DRIVER_PULSEAUDIO_DYNAMIC = "", + .SDL_AUDIO_DRIVER_SNDIO_DYNAMIC = "", + .SDL_JOYSTICK_MFI = 0, + .SDL_LIBUSB_DYNAMIC = "", + .SDL_UDEV_DYNAMIC = "", + .SDL_VIDEO_DRIVER_UIKIT = 0, + .SDL_VIDEO_DRIVER_KMSDRM_DYNAMIC = "", + .SDL_VIDEO_DRIVER_KMSDRM_DYNAMIC_GBM = "", + .SDL_VIDEO_DRIVER_WAYLAND_DYNAMIC = "", + .SDL_VIDEO_DRIVER_WAYLAND_DYNAMIC_CURSOR = "", + .SDL_VIDEO_DRIVER_WAYLAND_DYNAMIC_EGL = "", + .SDL_VIDEO_DRIVER_WAYLAND_DYNAMIC_LIBDECOR = "", + .SDL_VIDEO_DRIVER_WAYLAND_DYNAMIC_XKBCOMMON = "", + .SDL_VIDEO_DRIVER_X11_DYNAMIC = "", + .SDL_VIDEO_DRIVER_X11_DYNAMIC_XCURSOR = "", + .SDL_VIDEO_DRIVER_X11_DYNAMIC_XEXT = "", + .SDL_VIDEO_DRIVER_X11_DYNAMIC_XFIXES = "", + .SDL_VIDEO_DRIVER_X11_DYNAMIC_XINPUT2 = "", + .SDL_VIDEO_DRIVER_X11_DYNAMIC_XRANDR = "", + .SDL_VIDEO_DRIVER_X11_DYNAMIC_XSS = "", + .SDL_VIDEO_DRIVER_X11_DYNAMIC_XTEST = "", + .SDL_VIDEO_DRIVER_X11_XINPUT2_SUPPORTS_GESTURE = "", + .SDL_VIDEO_RENDER_METAL = 0, + .SDL_VIDEO_METAL = 0, + .SDL_GPU_METAL = 0, + .SDL_SENSOR_COREMOTION = 0, + .SDL_POWER_UIKIT = 0, + .SDL_FILESYSTEM_COCOA = 0, + .SDL_CAMERA_DRIVER_COREMEDIA = 0, + .SDL_CAMERA_DRIVER_PIPEWIRE_DYNAMIC = "", + .SDL_LIBDECOR_VERSION_MAJOR = "", + .SDL_LIBDECOR_VERSION_MINOR = "", + .SDL_LIBDECOR_VERSION_PATCH = "", + .SDL_FRIBIDI_DYNAMIC = "", + .SDL_LIBTHAI_DYNAMIC = "", + .SDL_XKBCOMMON_VERSION_MAJOR = "", + .SDL_XKBCOMMON_VERSION_MINOR = "", + .SDL_XKBCOMMON_VERSION_PATCH = "", + .SDL_IPHONE_KEYBOARD = 0, + .SDL_IPHONE_LAUNCHSCREEN = 0, + .SDL_EMSCRIPTEN_PERSISTENT_PATH_STRING = "", + }); +} diff --git a/src/example.zig b/src/example.zig index 68999e601..363a544da 100644 --- a/src/example.zig +++ b/src/example.zig @@ -1,23 +1,49 @@ const std = @import("std"); +const builtin = @import("builtin"); const sdl = @import("sdl3"); -const Io = std.Io; +// Zig 0.16's `std.Io.Threaded` fails to compile for Emscripten (`std.os.emscripten.W.STOPSIG` has +// the wrong return type), and it's referenced by default through `std.debug`'s I/O. Opt out of it +// there, which is why this example logs with `SDL_Log` rather than `std.debug.print`. +pub const std_options_debug_threaded_io: ?*std.Io.Threaded = if (builtin.os.tag == .emscripten) + null +else + std.Io.Threaded.global_single_threaded; +pub const std_options_debug_io: std.Io = if (builtin.os.tag == .emscripten) + std.Io.failing +else + std.Io.Threaded.global_single_threaded.io(); -pub fn main() void { - var threaded_io: Io.Threaded = .init_single_threaded; - const io = threaded_io.io(); +// `SDL_main.h` isn't part of the translated module (see `sdl.h`), so declare the entry point for +// the main callbacks ourselves. Using callbacks instead of a `while` loop lets platforms that own +// the main loop, such as Emscripten where blocking would freeze the browser tab, drive the app. +extern fn SDL_EnterAppMainCallbacks( + argc: c_int, + argv: ?[*]?[*:0]u8, + appinit: sdl.SDL_AppInit_func, + appiter: sdl.SDL_AppIterate_func, + appevent: sdl.SDL_AppEvent_func, + appquit: sdl.SDL_AppQuit_func, +) c_int; + +var window: ?*sdl.SDL_Window = null; +var renderer: ?*sdl.SDL_Renderer = null; + +pub fn main() u8 { + return @intCast(SDL_EnterAppMainCallbacks(0, null, init, iterate, event, quit)); +} + +fn init(appstate: ?*?*anyopaque, argc: c_int, argv: [*c][*c]u8) callconv(.c) sdl.SDL_AppResult { + _ = appstate; + _ = argc; + _ = argv; // Initialize SDL - if (!sdl.SDL_Init(sdl.SDL_INIT_VIDEO)) { - std.debug.panic("{s}", .{sdl.SDL_GetError()}); - } - defer sdl.SDL_Quit(); + if (!sdl.SDL_Init(sdl.SDL_INIT_VIDEO)) return fail(); - std.debug.print("video driver: {s}\n", .{sdl.SDL_GetCurrentVideoDriver() orelse @as([*c]const u8, "null")}); + sdl.SDL_Log("video driver: %s", sdl.SDL_GetCurrentVideoDriver() orelse @as([*c]const u8, "null")); // Create a window and renderer - var window: ?*sdl.SDL_Window = null; - var renderer: ?*sdl.SDL_Renderer = null; if (!sdl.SDL_CreateWindowAndRenderer( "example", 960, @@ -25,37 +51,42 @@ pub fn main() void { sdl.SDL_WINDOW_RESIZABLE | sdl.SDL_WINDOW_HIGH_PIXEL_DENSITY, &window, &renderer, - )) { - std.debug.panic("{s}", .{sdl.SDL_GetError()}); - } - defer sdl.SDL_DestroyWindow(window); - defer sdl.SDL_DestroyRenderer(renderer); - - // Main loop - while (true) { - // Poll events - var event: sdl.SDL_Event = undefined; - while (sdl.SDL_PollEvent(&event)) { - if (event.type == sdl.SDL_EVENT_QUIT) { - std.process.cleanExit(io); - return; - } - } - - // Update the background color - const now = @as(f64, @floatFromInt(sdl.SDL_GetTicks())) / 1000.0; - const r: f32 = @floatCast(0.5 + 0.5 * @sin(now)); - const g: f32 = @floatCast(0.5 + 0.5 * @sin(now + std.math.pi * 2 / 3.0)); - const b: f32 = @floatCast(0.5 + 0.5 * @sin(now + std.math.pi * 4 / 3.0)); - - if (!sdl.SDL_SetRenderDrawColorFloat(renderer, r, g, b, sdl.SDL_ALPHA_OPAQUE_FLOAT)) { - std.debug.panic("{s}", .{sdl.SDL_GetError()}); - } - if (!sdl.SDL_RenderClear(renderer)) { - std.debug.panic("{s}", .{sdl.SDL_GetError()}); - } - if (!sdl.SDL_RenderPresent(renderer)) { - std.debug.panic("{s}", .{sdl.SDL_GetError()}); - } - } + )) return fail(); + + return sdl.SDL_APP_CONTINUE; +} + +fn event(appstate: ?*anyopaque, ev: [*c]sdl.SDL_Event) callconv(.c) sdl.SDL_AppResult { + _ = appstate; + if (ev.*.type == sdl.SDL_EVENT_QUIT) return sdl.SDL_APP_SUCCESS; + return sdl.SDL_APP_CONTINUE; +} + +fn iterate(appstate: ?*anyopaque) callconv(.c) sdl.SDL_AppResult { + _ = appstate; + + // Update the background color + const now = @as(f64, @floatFromInt(sdl.SDL_GetTicks())) / 1000.0; + const r: f32 = @floatCast(0.5 + 0.5 * @sin(now)); + const g: f32 = @floatCast(0.5 + 0.5 * @sin(now + std.math.pi * 2 / 3.0)); + const b: f32 = @floatCast(0.5 + 0.5 * @sin(now + std.math.pi * 4 / 3.0)); + + if (!sdl.SDL_SetRenderDrawColorFloat(renderer, r, g, b, sdl.SDL_ALPHA_OPAQUE_FLOAT)) return fail(); + if (!sdl.SDL_RenderClear(renderer)) return fail(); + if (!sdl.SDL_RenderPresent(renderer)) return fail(); + + return sdl.SDL_APP_CONTINUE; +} + +fn fail() sdl.SDL_AppResult { + sdl.SDL_Log("%s", sdl.SDL_GetError()); + return sdl.SDL_APP_FAILURE; +} + +fn quit(appstate: ?*anyopaque, result: sdl.SDL_AppResult) callconv(.c) void { + _ = appstate; + _ = result; + sdl.SDL_DestroyRenderer(renderer); + sdl.SDL_DestroyWindow(window); + // SDL calls `SDL_Quit` for us after this callback returns } diff --git a/src/sdl.zon b/src/sdl.zon index 74674984a..58226224c 100644 --- a/src/sdl.zon +++ b/src/sdl.zon @@ -180,7 +180,6 @@ "loadso/windows/SDL_sysloadso.c", "locale/SDL_locale.c", "main/emscripten/SDL_sysmain_runapp.c", - "main/generic/SDL_sysmain_callbacks.c", "main/n3ds/SDL_sysmain_runapp.c", "main/ps2/SDL_sysmain_runapp.c", "main/psp/SDL_sysmain_runapp.c", @@ -429,6 +428,11 @@ "video/yuv2rgb/yuv_rgb_std.c", "core/linux/SDL_progressbar.c", }, + // The generic main callbacks implementation. Upstream only `#ifdef`s it out on iOS, so platforms + // that provide their own (e.g. Emscripten) need to leave it out to avoid a duplicate definition. + .main_callbacks_generic = .{ + "main/generic/SDL_sysmain_callbacks.c", + }, // Dummy drivers. Useful when porting to a new platform. .dummy = .{ "locale/dummy/SDL_syslocale.c", @@ -488,6 +492,8 @@ "main/emscripten/SDL_sysmain_callbacks.c", "misc/emscripten/SDL_sysurl.c", "power/emscripten/SDL_syspower.c", + "sensor/emscripten/SDL_emscriptensensor.c", + "tray/dummy/SDL_tray.c", }, .thread_generic = .{ "thread/generic/SDL_syscond.c",