diff --git a/Cargo.lock b/Cargo.lock index 331bdd60918..7111fd96610 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -361,6 +361,14 @@ version = "0.2.183" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d" +[[package]] +name = "libgfx_rust" +version = "0.1.0" +dependencies = [ + "cbindgen", + "yuv", +] + [[package]] name = "libjs_rust" version = "0.1.0" @@ -892,6 +900,15 @@ dependencies = [ "synstructure", ] +[[package]] +name = "yuv" +version = "0.8.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47d3a7e2cda3061858987ee2fb028f61695f5ee13f9490d75be6c3900df9a4ea" +dependencies = [ + "num-traits", +] + [[package]] name = "zerofrom" version = "0.1.6" diff --git a/Cargo.toml b/Cargo.toml index 69f2fad9d41..6d5a3dd2a35 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,6 @@ [workspace] members = [ + "Libraries/LibGfx/Rust", "Libraries/LibJS/Rust", "Libraries/LibRegex/Rust", "Libraries/LibUnicode/Rust", diff --git a/Libraries/LibGfx/CMakeLists.txt b/Libraries/LibGfx/CMakeLists.txt index b2b3e331d14..55b0ce0872d 100644 --- a/Libraries/LibGfx/CMakeLists.txt +++ b/Libraries/LibGfx/CMakeLists.txt @@ -118,6 +118,9 @@ find_package(harfbuzz REQUIRED) target_link_libraries(LibGfx PRIVATE PkgConfig::WOFF2 JPEG::JPEG PNG::PNG avif WebP::webp WebP::webpdecoder WebP::webpdemux WebP::libwebpmux skia harfbuzz) +import_rust_crate(MANIFEST_PATH Rust/Cargo.toml CRATE_NAME libgfx_rust FFI_HEADER RustFFI.h) +target_link_libraries(LibGfx PRIVATE libgfx_rust) + if (HAS_FONTCONFIG) target_link_libraries(LibGfx PRIVATE Fontconfig::Fontconfig) endif() diff --git a/Libraries/LibGfx/ImmutableBitmap.cpp b/Libraries/LibGfx/ImmutableBitmap.cpp index 6fa0d66e2ea..4b11531e125 100644 --- a/Libraries/LibGfx/ImmutableBitmap.cpp +++ b/Libraries/LibGfx/ImmutableBitmap.cpp @@ -208,8 +208,10 @@ ErrorOr> ImmutableBitmap::create_from_yuv(Nonnull auto context = SkiaBackendContext::the(); auto* gr_context = context ? context->sk_context() : nullptr; - if (!gr_context) - return Error::from_string_literal("GPU context is unavailable"); + if (!gr_context) { + auto bitmap = TRY(yuv_data->to_bitmap()); + return create(move(bitmap), move(color_space)); + } if (yuv_data->bit_depth() > 8) yuv_data->expand_samples_to_full_16_bit_range(); diff --git a/Libraries/LibGfx/Rust/Cargo.toml b/Libraries/LibGfx/Rust/Cargo.toml new file mode 100644 index 00000000000..c10a8625bc5 --- /dev/null +++ b/Libraries/LibGfx/Rust/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "libgfx_rust" +version = "0.1.0" +edition = "2024" + +[lib] +crate-type = ["staticlib"] + +[dependencies] +yuv = "0.8" + +[build-dependencies] +cbindgen = "0.29" diff --git a/Libraries/LibGfx/Rust/build.rs b/Libraries/LibGfx/Rust/build.rs new file mode 100644 index 00000000000..bbb11fef6cf --- /dev/null +++ b/Libraries/LibGfx/Rust/build.rs @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2026-present, the Ladybird developers. + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +use std::env; +use std::error::Error; +use std::path::PathBuf; + +fn main() -> Result<(), Box> { + let manifest_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR")?); + let out_dir = PathBuf::from(env::var("OUT_DIR")?); + + println!("cargo:rerun-if-changed=build.rs"); + println!("cargo:rerun-if-changed=cbindgen.toml"); + println!("cargo:rerun-if-env-changed=FFI_OUTPUT_DIR"); + println!("cargo:rerun-if-changed=src"); + + let ffi_out_dir = env::var("FFI_OUTPUT_DIR") + .map(PathBuf::from) + .unwrap_or_else(|_| out_dir.clone()); + + cbindgen::generate(manifest_dir).map_or_else( + |error| match error { + cbindgen::Error::ParseSyntaxError { .. } => {} + e => panic!("{e:?}"), + }, + |bindings| { + let header_path = out_dir.join("RustFFI.h"); + bindings.write_to_file(&header_path); + + if ffi_out_dir != out_dir { + bindings.write_to_file(ffi_out_dir.join("RustFFI.h")); + } + }, + ); + + Ok(()) +} diff --git a/Libraries/LibGfx/Rust/cbindgen.toml b/Libraries/LibGfx/Rust/cbindgen.toml new file mode 100644 index 00000000000..be9a2fd74a5 --- /dev/null +++ b/Libraries/LibGfx/Rust/cbindgen.toml @@ -0,0 +1,23 @@ +language = "C++" +header = """/* + * Copyright (c) 2026-present, the Ladybird developers. + * + * SPDX-License-Identifier: BSD-2-Clause + */""" +pragma_once = true +include_version = true +line_length = 120 +tab_width = 4 +no_includes = true +sys_includes = ["stdint.h", "stddef.h"] +usize_is_size_t = true +namespaces = ["Gfx", "FFI"] + +[parse] +parse_deps = false + +[parse.expand] +all_features = false + +[export.mangle] +rename_types = "PascalCase" diff --git a/Libraries/LibGfx/Rust/src/lib.rs b/Libraries/LibGfx/Rust/src/lib.rs new file mode 100644 index 00000000000..b866f44631e --- /dev/null +++ b/Libraries/LibGfx/Rust/src/lib.rs @@ -0,0 +1,241 @@ +/* + * Copyright (c) 2026-present, the Ladybird developers. + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +use yuv::{YuvPlanarImage, YuvRange, YuvStandardMatrix}; + +#[repr(u8)] +pub enum YUVRange { + Limited = 0, + Full = 1, +} + +#[repr(u8)] +pub enum YUVMatrix { + Bt709 = 0, + Fcc = 1, + Bt470BG = 2, + Bt601 = 3, + Smpte240 = 4, + Bt2020 = 5, +} + +impl From for YuvRange { + fn from(range: YUVRange) -> Self { + match range { + YUVRange::Limited => YuvRange::Limited, + YUVRange::Full => YuvRange::Full, + } + } +} + +impl From for YuvStandardMatrix { + fn from(matrix: YUVMatrix) -> Self { + match matrix { + YUVMatrix::Bt709 => YuvStandardMatrix::Bt709, + YUVMatrix::Fcc => YuvStandardMatrix::Fcc, + YUVMatrix::Bt470BG => YuvStandardMatrix::Bt470_6, + YUVMatrix::Bt601 => YuvStandardMatrix::Bt601, + YUVMatrix::Smpte240 => YuvStandardMatrix::Smpte240, + YUVMatrix::Bt2020 => YuvStandardMatrix::Bt2020, + } + } +} + +/// # Safety +/// All plane pointers must be valid for the specified dimensions and strides. +/// `dst` must point to a buffer of at least `dst_stride * height` bytes. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn yuv_u8_to_rgba( + y_plane: *const u8, + y_stride: u32, + u_plane: *const u8, + u_stride: u32, + v_plane: *const u8, + v_stride: u32, + width: u32, + height: u32, + subsampling_x: bool, + subsampling_y: bool, + dst: *mut u8, + dst_stride: u32, + range: YUVRange, + matrix: YUVMatrix, +) -> bool { + let y_len = y_stride as usize * height as usize; + let uv_height = if subsampling_y { + height.div_ceil(2) + } else { + height + } as usize; + let uv_len = u_stride as usize * uv_height; + + let planar_image = YuvPlanarImage { + y_plane: unsafe { core::slice::from_raw_parts(y_plane, y_len) }, + y_stride, + u_plane: unsafe { core::slice::from_raw_parts(u_plane, uv_len) }, + u_stride, + v_plane: unsafe { core::slice::from_raw_parts(v_plane, uv_len) }, + v_stride, + width, + height, + }; + + let dst_len = dst_stride as usize * height as usize; + let dst_slice = unsafe { core::slice::from_raw_parts_mut(dst, dst_len) }; + + let result = match (subsampling_x, subsampling_y) { + (true, true) => yuv::yuv420_to_rgba( + &planar_image, + dst_slice, + dst_stride, + range.into(), + matrix.into(), + ), + (true, false) => yuv::yuv422_to_rgba( + &planar_image, + dst_slice, + dst_stride, + range.into(), + matrix.into(), + ), + (false, true) => return false, + (false, false) => yuv::yuv444_to_rgba( + &planar_image, + dst_slice, + dst_stride, + range.into(), + matrix.into(), + ), + }; + + result.is_ok() +} + +/// # Safety +/// All plane pointers must be valid for the specified dimensions and strides. +/// `dst` must point to a buffer of at least `dst_stride * height` bytes. +/// Values in the u16 planes must be in 0-1023 range for 10-bit or 0-4095 for 12-bit. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn yuv_u16_to_rgba( + y_plane: *const u16, + y_stride: u32, + u_plane: *const u16, + u_stride: u32, + v_plane: *const u16, + v_stride: u32, + width: u32, + height: u32, + bit_depth: u8, + subsampling_x: bool, + subsampling_y: bool, + dst: *mut u8, + dst_stride: u32, + range: YUVRange, + matrix: YUVMatrix, +) -> bool { + let y_len = y_stride as usize * height as usize; + let uv_height = if subsampling_y { + height.div_ceil(2) + } else { + height + } as usize; + let uv_len = u_stride as usize * uv_height; + + let planar_image = YuvPlanarImage { + y_plane: unsafe { core::slice::from_raw_parts(y_plane, y_len) }, + y_stride, + u_plane: unsafe { core::slice::from_raw_parts(u_plane, uv_len) }, + u_stride, + v_plane: unsafe { core::slice::from_raw_parts(v_plane, uv_len) }, + v_stride, + width, + height, + }; + + let dst_len = dst_stride as usize * height as usize; + let dst_slice = unsafe { core::slice::from_raw_parts_mut(dst, dst_len) }; + + let result = if bit_depth <= 10 { + match (subsampling_x, subsampling_y) { + (true, true) => yuv::i010_to_rgba( + &planar_image, + dst_slice, + dst_stride, + range.into(), + matrix.into(), + ), + (true, false) => yuv::i210_to_rgba( + &planar_image, + dst_slice, + dst_stride, + range.into(), + matrix.into(), + ), + (false, true) => return false, + (false, false) => yuv::i410_to_rgba( + &planar_image, + dst_slice, + dst_stride, + range.into(), + matrix.into(), + ), + } + } else { + // 12-bit 4:4:4 has no 8-bit RGBA output; shift to 10-bit and use I410. + if !subsampling_x && !subsampling_y { + let y_10: Vec = unsafe { core::slice::from_raw_parts(y_plane, y_len) } + .iter() + .map(|&v| v >> 2) + .collect(); + let u_10: Vec = unsafe { core::slice::from_raw_parts(u_plane, uv_len) } + .iter() + .map(|&v| v >> 2) + .collect(); + let v_10: Vec = unsafe { core::slice::from_raw_parts(v_plane, uv_len) } + .iter() + .map(|&v| v >> 2) + .collect(); + let planar_10 = YuvPlanarImage { + y_plane: &y_10, + y_stride, + u_plane: &u_10, + u_stride, + v_plane: &v_10, + v_stride, + width, + height, + }; + return yuv::i410_to_rgba( + &planar_10, + dst_slice, + dst_stride, + range.into(), + matrix.into(), + ) + .is_ok(); + } + match (subsampling_x, subsampling_y) { + (true, true) => yuv::i012_to_rgba( + &planar_image, + dst_slice, + dst_stride, + range.into(), + matrix.into(), + ), + (true, false) => yuv::i212_to_rgba( + &planar_image, + dst_slice, + dst_stride, + range.into(), + matrix.into(), + ), + (false, true) => return false, + (false, false) => unreachable!(), + } + }; + + result.is_ok() +} diff --git a/Libraries/LibGfx/YUVData.cpp b/Libraries/LibGfx/YUVData.cpp index 9121960108a..0f0b7c49325 100644 --- a/Libraries/LibGfx/YUVData.cpp +++ b/Libraries/LibGfx/YUVData.cpp @@ -4,9 +4,12 @@ * SPDX-License-Identifier: BSD-2-Clause */ +#include +#include #include #include #include +#include #include #include @@ -102,6 +105,126 @@ Bytes YUVData::v_data() return m_impl->v_buffer.span(); } +static FFI::YUVMatrix yuv_matrix_for_cicp(Media::CodingIndependentCodePoints const& cicp) +{ + switch (cicp.matrix_coefficients()) { + case Media::MatrixCoefficients::Identity: + VERIFY_NOT_REACHED(); + case Media::MatrixCoefficients::FCC: + return FFI::YUVMatrix::Fcc; + case Media::MatrixCoefficients::BT470BG: + return FFI::YUVMatrix::Bt470BG; + case Media::MatrixCoefficients::BT601: + return FFI::YUVMatrix::Bt601; + case Media::MatrixCoefficients::SMPTE240: + return FFI::YUVMatrix::Smpte240; + case Media::MatrixCoefficients::BT2020NonConstantLuminance: + case Media::MatrixCoefficients::BT2020ConstantLuminance: + return FFI::YUVMatrix::Bt2020; + case Media::MatrixCoefficients::BT709: + case Media::MatrixCoefficients::Unspecified: + default: + return FFI::YUVMatrix::Bt709; + } +} + +ErrorOr> YUVData::to_bitmap() const +{ + auto const& impl = *m_impl; + VERIFY(impl.bit_depth <= 12); + + auto bitmap = TRY(Bitmap::create(BitmapFormat::RGBA8888, AlphaType::Premultiplied, impl.size)); + auto* dst = reinterpret_cast(bitmap->scanline(0)); + auto dst_stride = static_cast(bitmap->pitch()); + + auto width = static_cast(impl.size.width()); + auto height = static_cast(impl.size.height()); + + if (impl.cicp.matrix_coefficients() == Media::MatrixCoefficients::Identity) { + if (impl.subsampling.x() || impl.subsampling.y()) + return Error::from_string_literal("Subsampled RGB is unsupported"); + + if (impl.bit_depth <= 8) { + auto const* y_data = impl.y_buffer.data(); + auto const* u_data = impl.u_buffer.data(); + auto const* v_data = impl.v_buffer.data(); + auto y_stride = static_cast(width); + + for (u32 row = 0; row < height; row++) { + auto* dst_row = dst + (static_cast(row) * dst_stride); + auto const* y_row = y_data + (static_cast(row) * y_stride); + auto const* u_row = u_data + (static_cast(row) * y_stride); + auto const* v_row = v_data + (static_cast(row) * y_stride); + for (u32 col = 0; col < width; col++) { + dst_row[(col * 4) + 0] = v_row[col]; + dst_row[(col * 4) + 1] = y_row[col]; + dst_row[(col * 4) + 2] = u_row[col]; + dst_row[(col * 4) + 3] = 255; + } + } + } else { + // Our buffers hold native N-bit values in the low bits of each u16; shift right to reduce + // to 8-bit for the output. + auto shift = impl.bit_depth - 8; + auto const* y_data = reinterpret_cast(impl.y_buffer.data()); + auto const* u_data = reinterpret_cast(impl.u_buffer.data()); + auto const* v_data = reinterpret_cast(impl.v_buffer.data()); + auto y_stride = static_cast(width); + + for (u32 row = 0; row < height; row++) { + auto* dst_row = dst + (static_cast(row) * dst_stride); + auto const* y_row = y_data + (static_cast(row) * y_stride); + auto const* u_row = u_data + (static_cast(row) * y_stride); + auto const* v_row = v_data + (static_cast(row) * y_stride); + for (u32 col = 0; col < width; col++) { + dst_row[(col * 4) + 0] = static_cast(v_row[col] >> shift); + dst_row[(col * 4) + 1] = static_cast(y_row[col] >> shift); + dst_row[(col * 4) + 2] = static_cast(u_row[col] >> shift); + dst_row[(col * 4) + 3] = 255; + } + } + } + + return bitmap; + } + + auto uv_size = impl.subsampling.subsampled_size(impl.size).to_type(); + + bool full_range = impl.cicp.video_full_range_flag() == Media::VideoFullRangeFlag::Full; + auto range = full_range ? FFI::YUVRange::Full : FFI::YUVRange::Limited; + auto matrix = yuv_matrix_for_cicp(impl.cicp); + + auto y_stride = width; + auto uv_stride = uv_size.width(); + + bool success; + if (impl.bit_depth <= 8) { + success = FFI::yuv_u8_to_rgba( + impl.y_buffer.data(), y_stride, + impl.u_buffer.data(), uv_stride, + impl.v_buffer.data(), uv_stride, + width, height, + impl.subsampling.x(), impl.subsampling.y(), + dst, dst_stride, + range, matrix); + } else { + success = FFI::yuv_u16_to_rgba( + reinterpret_cast(impl.y_buffer.data()), y_stride, + reinterpret_cast(impl.u_buffer.data()), uv_stride, + reinterpret_cast(impl.v_buffer.data()), uv_stride, + width, height, + impl.bit_depth, + impl.subsampling.x(), impl.subsampling.y(), + dst, dst_stride, + range, matrix); + } + + if (!success) + return Error::from_string_literal("YUV-to-RGB conversion failed"); + + return bitmap; +} + void YUVData::expand_samples_to_full_16_bit_range() { auto const shift = 16 - m_impl->bit_depth; diff --git a/Libraries/LibGfx/YUVData.h b/Libraries/LibGfx/YUVData.h index aeb793f1552..1d51cee73e7 100644 --- a/Libraries/LibGfx/YUVData.h +++ b/Libraries/LibGfx/YUVData.h @@ -9,6 +9,8 @@ #include #include #include +#include +#include #include #include #include @@ -42,6 +44,8 @@ public: Bytes u_data(); Bytes v_data(); + ErrorOr> to_bitmap() const; + SkYUVAPixmaps make_pixmaps() const; void expand_samples_to_full_16_bit_range(); diff --git a/Meta/CMake/flatpak/cargo-sources.json b/Meta/CMake/flatpak/cargo-sources.json index 3017bb0a6e0..c1ffe00bc89 100644 --- a/Meta/CMake/flatpak/cargo-sources.json +++ b/Meta/CMake/flatpak/cargo-sources.json @@ -656,6 +656,13 @@ "sha256": "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e", "dest": "cargo/vendor/yoke-derive-0.8.2" }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/yuv/yuv-0.8.13.crate", + "sha256": "47d3a7e2cda3061858987ee2fb028f61695f5ee13f9490d75be6c3900df9a4ea", + "dest": "cargo/vendor/yuv-0.8.13" + }, { "type": "archive", "archive-type": "tar-gzip", @@ -794,6 +801,7 @@ "echo '{\"files\": {}, \"package\": \"9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9\"}' > cargo/vendor/writeable-0.6.2/.cargo-checksum.json", "echo '{\"files\": {}, \"package\": \"abe8c5fda708d9ca3df187cae8bfb9ceda00dd96231bed36e445a1a48e66f9ca\"}' > cargo/vendor/yoke-0.8.2/.cargo-checksum.json", "echo '{\"files\": {}, \"package\": \"de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e\"}' > cargo/vendor/yoke-derive-0.8.2/.cargo-checksum.json", + "echo '{\"files\": {}, \"package\": \"47d3a7e2cda3061858987ee2fb028f61695f5ee13f9490d75be6c3900df9a4ea\"}' > cargo/vendor/yuv-0.8.13/.cargo-checksum.json", "echo '{\"files\": {}, \"package\": \"50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5\"}' > cargo/vendor/zerofrom-0.1.6/.cargo-checksum.json", "echo '{\"files\": {}, \"package\": \"d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502\"}' > cargo/vendor/zerofrom-derive-0.1.6/.cargo-checksum.json", "echo '{\"files\": {}, \"package\": \"0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf\"}' > cargo/vendor/zerotrie-0.2.4/.cargo-checksum.json",