Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 61 additions & 0 deletions .agents/docs/2026-10-28-add-sdl3-mwrap-plan.md
Original file line number Diff line number Diff line change
@@ -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 `<SDL3/SDL.h>` 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 `<mwrap/sdl3/macros.hpp>` (= `<SDL3/SDL.h>`): 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.
5 changes: 5 additions & 0 deletions .github/workflows/validate.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions mcpp.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
38 changes: 38 additions & 0 deletions pkgs/mwrap/sdl3.lua
Original file line number Diff line number Diff line change
@@ -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",
},
},
},
}
18 changes: 18 additions & 0 deletions tests/mwrap/check_installed_template.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
11 changes: 11 additions & 0 deletions tests/mwrap/sdl3-headless/mcpp.toml
Original file line number Diff line number Diff line change
@@ -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"
60 changes: 60 additions & 0 deletions tests/mwrap/sdl3-headless/tests/behavior.cpp
Original file line number Diff line number Diff line change
@@ -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;
}
Loading