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
63 changes: 63 additions & 0 deletions .agents/docs/2026-10-30-add-libvpx-mwrap-plan.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
# Add mcpplibs.mwrap.libvpx (libvpx-m 1.17.0)

## Form

- Third-party upstreams, both BSD-3-Clause with the WebM project's royalty-free patent grant: [libvpx](https://chromium.googlesource.com/webm/libvpx)
1.17.0 (VP9) and [libwebm](https://github.com/webmproject/libwebm) 1.0.0.32 (the WebM container), the tag archives
compat.libvpx and compat.libwebm pin (#624).
- Form A external mwrap module repository [mcpplibs/libvpx-m](https://github.com/mcpplibs/libvpx-m), package
`mcpplibs.mwrap.libvpx`, version 1.17.0 (libvpx's version; the first release of the identity, no revision suffix; libwebm
1.0.0.32 in `[package.metadata.mwrap]`), tag series `mwrap-${version}`, tagged on the merged main commit (`676fceb`);
release asset `libvpx-m-1.17.0.tar.gz` (`e6a5a062…9211`). No CN mirror yet (docs/cn-mirror.md: CN users fall back to the
GitHub release).
- The container is bundled with the codec as libogg is with libopus in opus-m: WebM is how VP9 travels, and libvpx's own
tree carries libwebm for vpxenc/vpxdec.
- Implementation ownership: the libraries are compat.libvpx and compat.libwebm (static libraries from the tag archives).
The wrapper vendors no library code. It retains each upstream's README, LICENSE, PATENTS and AUTHORS under
`upstream/{libvpx,libwebm}/`, with checksums in `upstream/documents.json`, and deploys them with its own LICENSE/NOTICE
(`runtime.deploy`).

## Modules

- `mcpplibs.mwrap.libvpx`: libvpx — 133 types, 192 enumerators, the 35 functions upstream exports (vpx/exports_* and vp9's
lists) and the two VP9 interfaces, by `export using`. VP8 is not built (compat.libvpx: `--disable-vp8`): its interfaces,
which vp8cx.h/vp8dx.h declare, are not exported. The headers' 86 typed controls (static functions made by
`VPX_CTRL_USE_TYPE`) adapted under the same names in `mcpplibs::mwrap::libvpx`, each calling `vpx_codec_control_` as
upstream's body does. Re-exports the two below.
- `mcpplibs.mwrap.libvpx.webm`: libwebm — the classes, types, enumerators and free functions of mkvmuxer (29/0/26),
mkvparser (24/0/11) and libwebm (4/154/9) in their own namespaces; the namespace-scope constants (internal linkage) under
the same names in `mcpplibs::mwrap::libvpx::mkvmuxer` / `::mkvparser`.
- `mcpplibs.mwrap.libvpx.macros`: 74 macros as `inline constexpr` of their expression's type, `vpx_codec_enc_init`,
`vpx_codec_enc_init_multi`, `vpx_codec_dec_init`, `vpx_codec_version_major/_minor/_patch` as functions,
`VPX_VERSION_MAJOR/_MINOR/_PATCH` as constexpr functions.
- Stay macros, in `<mwrap/libvpx/macros.hpp>`: `vpx_codec_control` (token concatenation), `VPX_CTRL_USE_TYPE*`,
`VPX_CTRL_VOID`, the 86 empty `VPX_CTRL_<id>` feature tests, the declaration annotations, libwebm's
`LIBWEBM_DISALLOW_COPY_AND_ASSIGN`, the include guards.

`tools/check_exports.py` fails when the generated sources are stale, a declaration is not exported, a control not adapted,
or a macro neither mapped nor accounted for; every mapping is checked against the raw headers in a header-mixing test,
and every exported function and control's address is taken in a link test.

## Verification

Wrapper CI (Linux x86_64 GCC 16.1 and LLVM 23.1.3, Linux arm64 GCC 16.1, macOS LLVM, Windows LLVM, wasm32-emscripten under
node; all required): build, the three test programs in the dev and release profiles — VP9 at good quality, real time, four
threads and lossless, muxed into WebM, parsed and decoded back, each file's FNV-1a pinned and the same on every row —, the
export audit, and the conformance run on Linux and macOS: `examples/vpxwebm`, written against the module, writes the same
IVF bytes as upstream's `vpxenc` (built from the same tag by its own configure) for six settings, and `vpxdec` reads its
WebM files to the same frames as the module reads `vpxenc`'s (18 comparisons). The templates job scaffolds the default
`webm` template as an installed package and runs it.

Here, with the CI-pinned mcpp 2026.10.27.1:

- `bash tests/run_members.sh libvpx-webm`: the real release archive; VP9 frames through the typed controls, muxed with
mkvmuxer (track UID given: the same bytes twice), parsed with mkvparser and decoded (50.7 dB), and the side header
beside the import: `test result ok`.
- `python3 tests/mwrap/check_installed_template.py --package mcpplibs.mwrap.libvpx@1.17.0 --deploy`: the unmodified
template from the real archive (60 frames, 51.8 dB), then a relocated self-contained bundle whose `licenses/libvpx/`
holds the wrapper's LICENSE/NOTICE and both libraries' README, LICENSE, PATENTS and AUTHORS byte for byte.

## Motivation

openmui's video component (openmui.video, D-86) encodes its frames and sound track into WebM: VP9 through this wrapper and
Opus through mcpplibs.mwrap.opus, both royalty-free and BSD-3-Clause, linked statically under the project's licence policy.
5 changes: 5 additions & 0 deletions .github/workflows/validate.yml
Original file line number Diff line number Diff line change
Expand Up @@ -1663,6 +1663,11 @@ jobs:
shell: bash
run: python3 tests/mwrap/check_installed_template.py --mcpp "$MCPP" --package mcpplibs.mwrap.libwebp@1.5.0 --deploy

- name: Installed libvpx wrapper archive, actual default webm template and its relocated notices
if: contains(format(' {0} ', env.MEMBERS), ' libvpx-webm ') && matrix.toolchain == 'default'
shell: bash
run: python3 tests/mwrap/check_installed_template.py --mcpp "$MCPP" --package mcpplibs.mwrap.libvpx@1.17.0 --deploy

- name: Installed PocketPy wrapper archive, actual default script template and its relocated notices
if: contains(format(' {0} ', env.MEMBERS), ' pocketpy-embed ') && matrix.toolchain == 'default'
shell: bash
Expand Down
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,16 @@

### Added

- **mcpplibs.mwrap.libvpx 1.17.0(新包)。** libvpx-m:以 C++23 模块(`import mcpplibs.mwrap.libvpx;`)使用 VP9 编解码库
libvpx 1.17.0 与 WebM 容器库 libwebm 1.0.0.32(建立在 compat.libvpx 与 compat.libwebm 之上)。根模块导出 libvpx 头文件的
133 个类型、192 个枚举值、上游导出的 35 个函数与两个 VP9 接口(VP8 未构建,其接口不导出),头文件中 86 个类型化控制函数
(`VPX_CTRL_USE_TYPE` 生成的 static 函数)在 `mcpplibs::mwrap::libvpx` 中同名适配;`mcpplibs.mwrap.libvpx.webm` 在各自
命名空间导出 libwebm 的 mkvmuxer、mkvparser 与 libwebm(命名空间作用域常量为内部链接,其值在
`mcpplibs::mwrap::libvpx::mkvmuxer` / `::mkvparser` 下同名提供);`mcpplibs.mwrap.libvpx.macros` 把 74 个宏映射为常量、
函数与 constexpr 函数,其余(`vpx_codec_control` 等)留在 `<mwrap/libvpx/macros.hpp>`。描述符为 libvpx-m 的不可变
`mwrap-1.17.0` 发布包(暂无 CN 镜像)。新增使用者 `tests/mwrap/libvpx-webm`:经类型化控制编码 VP9、mkvmuxer 封装、
mkvparser 解析并解码(PSNR 下限)、两次封装字节相同,以及侧边头文件与 import 同时使用;validate.yml 新增已安装模板检查
(wrapper 的 LICENSE/NOTICE 与两个库的 README、LICENSE、PATENTS、AUTHORS 在搬迁后的发布包中逐字节一致)。
- **compat.libvpx 1.17.0(新包)。** libvpx:WebM 项目的 VP9 视频编解码器(编码器与解码器;BSD-3-Clause,附 WebM 项目的
免版税专利授权 PATENTS)。从 `v1.17.0` 标签归档构建上游 `configure --target=generic-gnu --disable-vp8 --enable-vp9
--enable-multithread`(不含示例、工具、文档、单元测试、webm-io、libyuv)所编译的 101 个 C 源文件:通用 C 实现,无 SIMD、
Expand Down
1 change: 1 addition & 0 deletions mcpp.toml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ members = [
"tests/mwrap/sheenbidi-bidi",
"tests/mwrap/dr_libs-audio",
"tests/mwrap/opus-ogg",
"tests/mwrap/libvpx-webm",
"tests/mwrap/box2d-physics",
"tests/mwrap/miniaudio-engine",
"tests/mwrap/freetype-render",
Expand Down
44 changes: 44 additions & 0 deletions pkgs/mwrap/libvpx.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
-- Immutable upstream-aligned mwrap source release (libvpx-m mwrap-1.17.0).
-- 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).
--
-- The wrapper is Apache-2.0. The libraries it links are BSD-3-Clause with the
-- WebM project's patent grant, linked statically: compat.libvpx (libvpx
-- 1.17.0, VP9) and compat.libwebm (libwebm 1.0.0.32, the WebM container). The
-- package deploys each one's README, LICENSE, PATENTS and AUTHORS beside
-- programs (runtime.deploy).
package = {
spec = "1",
namespace = "mcpplibs.mwrap",
name = "libvpx",
description = "libvpx-m: VP9 video (libvpx 1.17.0) and the WebM container (libwebm 1.0.0.32) as C++23 modules",
licenses = { "Apache-2.0" },
repo = "https://github.com/mcpplibs/libvpx-m",
type = "package",
extensions = {
autoupdate = {
tag = "mwrap-${version}",
url = "https://github.com/mcpplibs/libvpx-m/releases/download/${tag}/libvpx-m-${version}.tar.gz",
},
},
xpm = {
linux = {
["1.17.0"] = {
url = "https://github.com/mcpplibs/libvpx-m/releases/download/mwrap-1.17.0/libvpx-m-1.17.0.tar.gz",
sha256 = "e6a5a062923b17961d44c7d1d74cd4190f774d4eb38d639607530ff987669211",
},
},
macosx = {
["1.17.0"] = {
url = "https://github.com/mcpplibs/libvpx-m/releases/download/mwrap-1.17.0/libvpx-m-1.17.0.tar.gz",
sha256 = "e6a5a062923b17961d44c7d1d74cd4190f774d4eb38d639607530ff987669211",
},
},
windows = {
["1.17.0"] = {
url = "https://github.com/mcpplibs/libvpx-m/releases/download/mwrap-1.17.0/libvpx-m-1.17.0.tar.gz",
sha256 = "e6a5a062923b17961d44c7d1d74cd4190f774d4eb38d639607530ff987669211",
},
},
},
}
24 changes: 24 additions & 0 deletions tests/mwrap/check_installed_template.py
Original file line number Diff line number Diff line change
Expand Up @@ -272,6 +272,30 @@ def main():
notice = (roots[0] / "NOTICE").read_bytes()
assert b"mcpp-community" in notice and b"Google" in notice
print(f"Verified {len(declared)} byte-identical wrapper/libwebp license files", flush=True)
elif identity == "mcpplibs.mwrap.libvpx":
# libvpx and libwebm (BSD-3-Clause with the WebM project's
# patent grant) are linked statically: the wrapper's
# LICENSE/NOTICE and each library's README, LICENSE, PATENTS
# and AUTHORS travel with the program.
sources = [path for store in (home / "xpkgs", home / "registry/data/xpkgs")
for path in store.glob(f"mcpplibs.mwrap-x-libvpx/{version}/*/src/libvpx.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/libvpx"))
assert len(roots) == 1, f"Missing libvpx 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"The WebM Project authors" in (roots[0] / "upstream/libvpx/LICENSE").read_bytes()
assert b"Google Inc." in (roots[0] / "upstream/libwebm/LICENSE.TXT").read_bytes()
for patents in ("upstream/libvpx/PATENTS", "upstream/libwebm/PATENTS.TXT"):
assert b"patent" in (roots[0] / patents).read_bytes(), patents
assert b"Apache License" in (roots[0] / "LICENSE").read_bytes()
notice = (roots[0] / "NOTICE").read_bytes()
assert b"mcpp-community" in notice and b"WebM Project" in notice
print(f"Verified {len(declared)} byte-identical wrapper/libvpx/libwebm license files", flush=True)
elif identity == "mcpplibs.mwrap.pocketpy":
# PocketPy is MIT and linked statically: its LICENSE and README
# travel with the program beside the wrapper's LICENSE/NOTICE.
Expand Down
11 changes: 11 additions & 0 deletions tests/mwrap/libvpx-webm/mcpp.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
[package]
name = "libvpx-webm"
version = "1.17.0"
description = "Consume the immutable libvpx wrapper: VP9 frames encoded through the typed controls, muxed into a WebM with mkvmuxer, parsed back with mkvparser and decoded within a PSNR, the bytes the same twice; the side header beside the import"

[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.libvpx" = "1.17.0"
134 changes: 134 additions & 0 deletions tests/mwrap/libvpx-webm/tests/behavior.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
// The wrapper through its modules alone: VP9 frames encoded through the typed
// controls, muxed into a WebM in memory with mkvmuxer (track UID given),
// parsed back with mkvparser and decoded within a PSNR; the same bytes twice.
import std;
import mcpplibs.mwrap.libvpx;

namespace m = mcpplibs::mwrap::libvpx::macros;
namespace vpx = mcpplibs::mwrap::libvpx;

namespace {

constexpr int W = 160, H = 96, FRAMES = 20;

void draw(vpx_image_t& img, int t) {
for (int y = 0; y < H; ++y)
for (int x = 0; x < W; ++x)
img.planes[0][y * img.stride[0] + x] = static_cast<unsigned char>(x >= 20 + 4 * t && x < 50 + 4 * t && y >= 30 && y < 60 ? 230 : 16 + (x + y + 3 * t) % 200);
for (int p = 1; p < 3; ++p)
for (int y = 0; y < H / 2; ++y)
for (int x = 0; x < W / 2; ++x) img.planes[p][y * img.stride[p] + x] = static_cast<unsigned char>(p == 1 ? 100 + x : 150 - y);
}

class Memory final : public mkvmuxer::IMkvWriter {
public:
std::vector<unsigned char> bytes;
std::int64_t at = 0;
mkvmuxer::int32 Write(const void* buf, mkvmuxer::uint32 len) override {
if (bytes.size() < static_cast<std::size_t>(at) + len) bytes.resize(static_cast<std::size_t>(at) + len);
std::memcpy(bytes.data() + at, buf, len);
at += len;
return 0;
}
mkvmuxer::int64 Position() const override { return at; }
mkvmuxer::int32 Position(mkvmuxer::int64 p) override { return at = p, 0; }
bool Seekable() const override { return true; }
void ElementStartNotify(mkvmuxer::uint64, mkvmuxer::int64) override {}
};

class Reader final : public mkvparser::IMkvReader {
public:
explicit Reader(const std::vector<unsigned char>& b) : bytes(b) {}
int Read(long long pos, long len, unsigned char* buf) override {
if (pos < 0 || len < 0 || static_cast<std::size_t>(pos + len) > bytes.size()) return -1;
std::memcpy(buf, bytes.data() + pos, static_cast<std::size_t>(len));
return 0;
}
int Length(long long* total, long long* available) override {
*total = *available = static_cast<long long>(bytes.size());
return 0;
}
const std::vector<unsigned char>& bytes;
};

std::vector<unsigned char> webm() {
vpx_codec_enc_cfg_t cfg;
vpx_codec_enc_config_default(vpx_codec_vp9_cx(), &cfg, 0);
cfg.g_w = W;
cfg.g_h = H;
cfg.g_timebase = {1, 30};
cfg.g_lag_in_frames = 0;
cfg.rc_end_usage = VPX_Q;
vpx_codec_ctx_t enc;
if (m::vpx_codec_enc_init(&enc, vpx_codec_vp9_cx(), &cfg, 0) != VPX_CODEC_OK) return {};
vpx::vpx_codec_control_VP8E_SET_CPUUSED(&enc, VP8E_SET_CPUUSED, 6);
vpx::vpx_codec_control_VP8E_SET_CQ_LEVEL(&enc, VP8E_SET_CQ_LEVEL, 20u);
Memory out;
mkvmuxer::Segment segment;
segment.Init(&out);
segment.set_mode(mkvmuxer::Segment::kFile);
const auto track = segment.AddVideoTrack(W, H, 1);
auto* v = static_cast<mkvmuxer::VideoTrack*>(segment.GetTrackByNumber(track));
v->set_codec_id(mkvmuxer::Tracks::kVp9CodecId);
v->set_uid(7);
vpx_image_t img;
vpx_img_alloc(&img, VPX_IMG_FMT_I420, W, H, 16);
for (int t = 0; t <= FRAMES; ++t) {
if (t < FRAMES) draw(img, t);
vpx_codec_encode(&enc, t < FRAMES ? &img : nullptr, t, 1, 0, m::VPX_DL_GOOD_QUALITY);
vpx_codec_iter_t it = nullptr;
while (const vpx_codec_cx_pkt_t* p = vpx_codec_get_cx_data(&enc, &it))
if (p->kind == VPX_CODEC_CX_FRAME_PKT)
segment.AddFrame(static_cast<const std::uint8_t*>(p->data.frame.buf), p->data.frame.sz, track,
static_cast<std::uint64_t>(p->data.frame.pts) * 1'000'000'000 / 30, (p->data.frame.flags & m::VPX_FRAME_IS_KEY) != 0);
}
vpx_img_free(&img);
vpx_codec_destroy(&enc);
segment.Finalize();
return out.bytes;
}

} // namespace

int main() {
const auto file = webm();
if (file.empty() || file != webm()) return 1;
Reader reader(file);
long long pos = 0;
mkvparser::EBMLHeader header;
mkvparser::Segment* raw = nullptr;
if (header.Parse(&reader, pos) != 0 || std::string_view(header.m_docType) != "webm") return 2;
if (mkvparser::Segment::CreateInstance(&reader, pos, raw) != 0 || !raw || raw->Load() < 0) return 3;
std::unique_ptr<mkvparser::Segment> segment(raw);
vpx_codec_dec_cfg_t dcfg{};
vpx_codec_ctx_t dec;
if (m::vpx_codec_dec_init(&dec, vpx_codec_vp9_dx(), &dcfg, 0) != VPX_CODEC_OK) return 4;
vpx_image_t ref;
vpx_img_alloc(&ref, VPX_IMG_FMT_I420, W, H, 16);
int frames = 0;
double worst = 99;
for (const mkvparser::Cluster* c = segment->GetFirst(); c && !c->EOS(); c = segment->GetNext(c)) {
const mkvparser::BlockEntry* e = nullptr;
for (c->GetFirst(e); e && !e->EOS(); c->GetNext(e, e)) {
const mkvparser::Block::Frame& f = e->GetBlock()->GetFrame(0);
std::vector<unsigned char> data(static_cast<std::size_t>(f.len));
f.Read(&reader, data.data());
vpx_codec_decode(&dec, data.data(), static_cast<unsigned>(data.size()), nullptr, 0);
vpx_codec_iter_t it = nullptr;
while (vpx_image_t* out = vpx_codec_get_frame(&dec, &it)) {
draw(ref, frames++);
double se = 0;
for (int y = 0; y < H; ++y)
for (int x = 0; x < W; ++x) {
const int d = out->planes[0][y * out->stride[0] + x] - ref.planes[0][y * ref.stride[0] + x];
se += d * d;
}
if (se > 0) worst = std::min(worst, 10 * std::log10(255.0 * 255.0 * W * H / se));
}
}
}
vpx_img_free(&ref);
vpx_codec_destroy(&dec);
std::println("{} frames in {} bytes of WebM, worst PSNR {:.1f} dB ({})", frames, file.size(), worst, vpx_codec_version_str());
return frames == FRAMES && worst > 35 ? 0 : 5;
}
32 changes: 32 additions & 0 deletions tests/mwrap/libvpx-webm/tests/side_header.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
// The side header from the installed package, beside the import: the
// header's vpx_codec_control macro (its typed static control) and the
// adapted control set the same thing; a VPX_CTRL_<id> feature test; the
// mapped constants beside the macros (a function-like macro is not expanded
// when its name is followed by `)`).
#include <mwrap/libvpx/macros.hpp>

import std;
import mcpplibs.mwrap.libvpx;

namespace m = mcpplibs::mwrap::libvpx::macros;

#ifndef VPX_CTRL_VP9E_SET_ROW_MT
#error "the VPX_CTRL_<id> feature tests come with the header"
#endif

int main() {
vpx_codec_enc_cfg_t cfg;
if (vpx_codec_enc_config_default(vpx_codec_vp9_cx(), &cfg, 0) != VPX_CODEC_OK) return 1;
cfg.g_w = 64;
cfg.g_h = 64;
vpx_codec_ctx_t a, b;
if ((m::vpx_codec_enc_init)(&a, vpx_codec_vp9_cx(), &cfg, 0) != VPX_CODEC_OK) return 2;
if (vpx_codec_enc_init(&b, vpx_codec_vp9_cx(), &cfg, 0) != VPX_CODEC_OK) return 3;
if (vpx_codec_control(&a, VP8E_SET_CPUUSED, 7) != VPX_CODEC_OK) return 4;
if (mcpplibs::mwrap::libvpx::vpx_codec_control_VP8E_SET_CPUUSED(&b, VP8E_SET_CPUUSED, 7) != VPX_CODEC_OK) return 5;
if (&::vpx_codec_encode != &vpx_codec_encode) return 6;
static_assert((m::VPX_VERSION_MAJOR)(0x011100) == VPX_VERSION_MAJOR(0x011100));
vpx_codec_destroy(&a);
vpx_codec_destroy(&b);
return 0;
}
Loading