diff --git a/.agents/docs/2026-10-28-add-sdl3-mwrap-plan.md b/.agents/docs/2026-10-28-add-sdl3-mwrap-plan.md new file mode 100644 index 00000000..04b1c759 --- /dev/null +++ b/.agents/docs/2026-10-28-add-sdl3-mwrap-plan.md @@ -0,0 +1,61 @@ +# Add mcpplibs.mwrap.sdl3 (sdl3-m 3.4.18) + +## Form + +- Third-party upstream [libsdl-org/SDL](https://github.com/libsdl-org/SDL), release `release-3.4.18`, zlib. The + tarball (`c4b08b95…92665c90`) is the one compat.sdl3 3.4.18 already pins. +- Form A external mwrap module repository: [mcpplibs/sdl3-m](https://github.com/mcpplibs/sdl3-m), package + `mcpplibs.mwrap.sdl3`. The version is upstream's (first release of the identity, no revision suffix); tag + series `mwrap-${version}`. +- Implementation ownership: the library is compat.sdl3's (its configuration and ABI are sufficient). The + wrapper vendors no upstream code; it retains SDL's README and license under `upstream/sdl3/` with checksums in + `upstream/documents.json`. No implementation is linked twice. + +## Modules + +- `mcpplibs.mwrap.sdl3`: every function, type, struct and enumerator of `` and its closure, + `SDL_vulkan.h`, `SDL_main.h`'s functions (included with `SDL_MAIN_HANDLED`: no `main` redefinition) and + `SDL_revision.h`, by `export using`, each under the `#if` its header declares it under (Linux 2773, macOS 2771, + Windows 2779 declarations). Generated from the pinned headers by `tools/gen_exports.py`. +- The 16 `SDL_FORCE_INLINE` functions (`SDL_PointInRect`, `SDL_Swap32`, `SDL_size_mul_check_overflow` …) have + internal linkage; they are adapted under their names and signatures in `mcpplibs::mwrap::sdl3`, each calling + upstream's. +- `mcpplibs.mwrap.sdl3.macros`: 1060 object-like macros as `inline constexpr` constants of the macro's own type + (strings as `const char[]`: `SDL_HINT_*`, `SDL_PROP_*`), 87 function-like macros as functions whose body is + the macro expanded in the module (`SDL_BUTTON_MASK`, `SDL_FOURCC`, `SDL_BITSPERPIXEL`, `SDL_CreateThread` …); + the bit-extracting ones are templates, as upstream applies them to integers and enums alike. +- Stay macros, in `` (= ``): the assertions, `SDL_stack_alloc`, + stringification and pasting, attributes, the `SDL_PLATFORM_*` tests, SDL 2's names; every macro is listed with + its reason in `tools/policy.py`. Not wrapped (include directly): the Khronos GL/EGL headers SDL ships, + `SDL_intrin.h`, the SDL_test headers. + +## Verification + +Wrapper CI (Linux GCC 16.1 and LLVM 23.1.3, macOS LLVM, Windows LLVM, all required): build; tests through the +module (init, properties, hints, events, timers, threads, IO, filesystem, pixels, surfaces, the software +renderer and audio on the dummy drivers, a GPU device on lavapipe / Metal / Direct3D 12, required), the macro +mappings, the header mixed with the import, `SDL_main.h` and the app callbacks; the export audit, which on each +row preprocesses the headers with the module's own compile command and requires every declaration exported, +every inline function adapted and every macro mapped or listed; and SDL's own `testautomation` (26 suites, 377 +tests) compiled as C++ against the module (its SDL includes replaced by the import and the side macros) and run +as upstream's CTest runs it, with and without SIMD: 0 failures on every row. The templates job scaffolds the +default `info` template (headless) as an installed package and runs it, and builds the `window` template. + +Here, with the CI-pinned mcpp 2026.10.26.1: + +- `bash tests/run_members.sh sdl3-headless`: the real release archive through the candidate index; events, + properties, the software renderer read back, an audio conversion, the mapped macros: `test result ok`. +- `python3 tests/mwrap/check_installed_template.py --package mcpplibs.mwrap.sdl3@3.4.18`: the installed archive + and the unmodified default template. + +## Mirror and updates + +No `mcpp-res` access in this change, so the descriptor uses the GitHub release URL as a plain string (CN users +fall back to it); a maintainer can add the GitCode mirror with identical bytes. `extensions.autoupdate` follows +the repository's own `mwrap-${version}` tags. + +## Motivation + +openmui's desktop host (openmui.host.sdl3) and SDL_GPU backend (openmui.rhi.sdl) use SDL3. Through this module +their native units import SDL (`import mcpplibs.mwrap.sdl3;` and +`using namespace mcpplibs::mwrap::sdl3::macros;`) instead of including its headers. diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index b098ad7a..6021ffe8 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -1643,6 +1643,11 @@ jobs: shell: bash run: python3 tests/mwrap/check_installed_template.py --mcpp "$MCPP" --package mcpplibs.mwrap.harfbuzz@14.3.0 --deploy + - name: Installed SDL3 wrapper archive, actual default info template and its relocated notices + if: contains(format(' {0} ', env.MEMBERS), ' sdl3-headless ') && matrix.toolchain == 'default' + shell: bash + run: python3 tests/mwrap/check_installed_template.py --mcpp "$MCPP" --package mcpplibs.mwrap.sdl3@3.4.18 --deploy + - name: Installed Godot wrapper archive and actual standalone math template if: contains(format(' {0} ', env.MEMBERS), ' godot-math ') && matrix.toolchain == 'default' shell: bash diff --git a/mcpp.toml b/mcpp.toml index ae69346c..17c065c2 100644 --- a/mcpp.toml +++ b/mcpp.toml @@ -24,6 +24,7 @@ members = [ "tests/mwrap/fribidi-bidi", "tests/mwrap/freetype-render", "tests/mwrap/harfbuzz-shape", + "tests/mwrap/sdl3-headless", "tests/mwrap/grpc-cpu", "tests/mwrap/grpc-tools", "tests/mwrap/godot-math", diff --git a/pkgs/mwrap/sdl3.lua b/pkgs/mwrap/sdl3.lua new file mode 100644 index 00000000..bc129091 --- /dev/null +++ b/pkgs/mwrap/sdl3.lua @@ -0,0 +1,38 @@ +-- Immutable upstream-aligned mwrap source release (sdl3-m mwrap-3.4.18). +-- The CN mirror is added by a maintainer with mcpp-res access; until then CN +-- users fall back to the GitHub release (docs/cn-mirror.md). +package = { + spec = "1", + namespace = "mcpplibs.mwrap", + name = "sdl3", + description = "sdl3-m: SDL 3 (window, input, audio, GPU and platform layer) as C++23 modules", + licenses = { "Apache-2.0" }, + repo = "https://github.com/mcpplibs/sdl3-m", + type = "package", + extensions = { + autoupdate = { + tag = "mwrap-${version}", + url = "https://github.com/mcpplibs/sdl3-m/releases/download/${tag}/sdl3-m-${version}.tar.gz", + }, + }, + xpm = { + linux = { + ["3.4.18"] = { + url = "https://github.com/mcpplibs/sdl3-m/releases/download/mwrap-3.4.18/sdl3-m-3.4.18.tar.gz", + sha256 = "e056750efe3bdb9e03457cb202d7a17f9c942630637656f382ac0604d8a66d08", + }, + }, + macosx = { + ["3.4.18"] = { + url = "https://github.com/mcpplibs/sdl3-m/releases/download/mwrap-3.4.18/sdl3-m-3.4.18.tar.gz", + sha256 = "e056750efe3bdb9e03457cb202d7a17f9c942630637656f382ac0604d8a66d08", + }, + }, + windows = { + ["3.4.18"] = { + url = "https://github.com/mcpplibs/sdl3-m/releases/download/mwrap-3.4.18/sdl3-m-3.4.18.tar.gz", + sha256 = "e056750efe3bdb9e03457cb202d7a17f9c942630637656f382ac0604d8a66d08", + }, + }, + }, +} diff --git a/tests/mwrap/check_installed_template.py b/tests/mwrap/check_installed_template.py index 3d086827..7488b5c2 100644 --- a/tests/mwrap/check_installed_template.py +++ b/tests/mwrap/check_installed_template.py @@ -204,6 +204,24 @@ def main(): assert b"Apache License" in (roots[0] / "LICENSE").read_bytes() assert b"mcpp-community" in (roots[0] / "NOTICE").read_bytes() print(f"Verified {len(declared)} byte-identical wrapper/HarfBuzz license files", flush=True) + elif identity == "mcpplibs.mwrap.sdl3": + # SDL is zlib-licensed and linked statically: its license and + # README travel with the program, beside the wrapper's. + sources = [path for store in (home / "xpkgs", home / "registry/data/xpkgs") + for path in store.glob(f"mcpplibs.mwrap-x-sdl3/{version}/*/src/sdl3.cppm")] + assert len(sources) == 1, sources + source = sources[0].parent.parent + declared = tomllib.loads((source / "mcpp.toml").read_text(encoding="utf-8"))["runtime"]["deploy"] + roots = list(relocated.rglob("licenses/sdl3")) + assert len(roots) == 1, f"Missing SDL runtime license tree: {roots}" + for entry in declared: + notice = source / entry["from"] + copied = roots[0].parent.parent / entry["to"] / notice.name + assert copied.is_file() and copied.read_bytes() == notice.read_bytes(), copied + assert b"Sam Lantinga" in (roots[0] / "upstream/sdl3/LICENSE.txt").read_bytes() + assert b"Apache License" in (roots[0] / "LICENSE").read_bytes() + assert b"mcpp-community" in (roots[0] / "NOTICE").read_bytes() + print(f"Verified {len(declared)} byte-identical wrapper/SDL license files", flush=True) else: raise ValueError(f"No deployment acceptance contract for {identity}") runtime_env = {key: value for key, value in os.environ.items() diff --git a/tests/mwrap/sdl3-headless/mcpp.toml b/tests/mwrap/sdl3-headless/mcpp.toml new file mode 100644 index 00000000..86ecbb3e --- /dev/null +++ b/tests/mwrap/sdl3-headless/mcpp.toml @@ -0,0 +1,11 @@ +[package] +name = "sdl3-headless" +version = "3.4.18" +description = "Consume the immutable SDL3 wrapper headless: events, properties, a surface, the software renderer on the dummy driver, an audio stream, the mapped macros" + +[indices] +# Use the complete candidate built-in index prepared by tests/run_members.sh. +# An explicit empty table overrides the legacy workspace redirect. + +[dependencies] +"mcpplibs.mwrap.sdl3" = "3.4.18" diff --git a/tests/mwrap/sdl3-headless/tests/behavior.cpp b/tests/mwrap/sdl3-headless/tests/behavior.cpp new file mode 100644 index 00000000..8798eb6d --- /dev/null +++ b/tests/mwrap/sdl3-headless/tests/behavior.cpp @@ -0,0 +1,60 @@ +import std; +import mcpplibs.mwrap.sdl3; + +using namespace mcpplibs::mwrap::sdl3; // the adapted inline API +using namespace mcpplibs::mwrap::sdl3::macros; // SDL_INIT_VIDEO, SDL_HINT_*, SDL_BITSPERPIXEL ... + +int main() { + // SDL's headless drivers, over whatever the environment says. + SDL_SetHintWithPriority(SDL_HINT_VIDEO_DRIVER, "dummy", SDL_HINT_OVERRIDE); + SDL_SetHintWithPriority(SDL_HINT_AUDIO_DRIVER, "dummy", SDL_HINT_OVERRIDE); + if (SDL_GetVersion() != SDL_VERSION || !SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO)) return 1; + + // Properties and a user event, pushed and polled. + const SDL_PropertiesID props = SDL_CreateProperties(); + SDL_SetStringProperty(props, "name", "sdl3-headless"); + if (std::string_view(SDL_GetStringProperty(props, "name", "")) != "sdl3-headless") return 2; + SDL_DestroyProperties(props); + const Uint32 type = SDL_RegisterEvents(1); + SDL_Event e{}; + e.type = type; + e.user.code = 42; + if (!SDL_PushEvent(&e)) return 3; + int code = 0; + while (SDL_PollEvent(&e)) + if (e.type == type) code = e.user.code; + if (code != 42) return 4; + + // A window and the software renderer: a filled square read back. + SDL_Window* window = nullptr; + SDL_Renderer* renderer = nullptr; + if (!SDL_CreateWindowAndRenderer("sdl3-headless", 32, 32, SDL_WINDOW_HIDDEN, &window, &renderer)) return 5; + SDL_SetRenderDrawColor(renderer, 0, 0, 0, SDL_ALPHA_OPAQUE); + SDL_RenderClear(renderer); + SDL_SetRenderDrawColor(renderer, 255, 128, 0, SDL_ALPHA_OPAQUE); + const SDL_FRect square{8, 8, 16, 16}; + SDL_RenderFillRect(renderer, &square); + SDL_Surface* frame = SDL_RenderReadPixels(renderer, nullptr); + Uint8 r = 0, g = 0, b = 0, a = 0; + if (!frame || !SDL_ReadSurfacePixel(frame, 16, 16, &r, &g, &b, &a) || r != 255 || g != 128 || b != 0) return 6; + SDL_DestroySurface(frame); + SDL_DestroyRenderer(renderer); + SDL_DestroyWindow(window); + + // Pixel-format macros, an inline rectangle function, an audio conversion. + static_assert(SDL_BITSPERPIXEL(SDL_PIXELFORMAT_ARGB8888) == 32 && SDL_FOURCC('Y', 'V', '1', '2') == SDL_PIXELFORMAT_YV12); + const SDL_Rect rect{0, 0, 4, 4}; + const SDL_Point inside{1, 2}; + if (!SDL_PointInRect(&inside, &rect)) return 7; + const SDL_AudioSpec from{SDL_AUDIO_S16, 1, 8000}, to{SDL_AUDIO_F32, 1, 8000}; + SDL_AudioStream* stream = SDL_CreateAudioStream(&from, &to); + const Sint16 half = 16384; + float out = 0; + if (!stream || !SDL_PutAudioStreamData(stream, &half, sizeof half) || !SDL_FlushAudioStream(stream) || + SDL_GetAudioStreamData(stream, &out, sizeof out) != sizeof out || out != 0.5f) + return 8; + SDL_DestroyAudioStream(stream); + SDL_Quit(); + std::println("sdl3-headless: ok (SDL {}.{}.{}, {})", SDL_MAJOR_VERSION, SDL_MINOR_VERSION, SDL_MICRO_VERSION, SDL_GetPlatform()); + return 0; +}