LibGfx: Implement YUV->RGBA color conversion for CPU painting

Using the Rust yuv crate, eagerly convert from YUV to RGBA on the CPU
when a GPU context is unavailable.

Time spent converting an 8-bit YUV frame with this crate is better than
libyuv on ARM by about 20%, and on x86 with AVX2, it achieves similar
numbers to libyuv.
This commit is contained in:
Zaggy1024
2026-04-16 20:43:09 -05:00
committed by Gregory Bertilson
parent f434b56ffa
commit 3cfe1f7542
Notes: github-actions[bot] 2026-04-18 06:25:55 +00:00
11 changed files with 477 additions and 2 deletions

17
Cargo.lock generated
View File

@@ -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"

View File

@@ -1,5 +1,6 @@
[workspace]
members = [
"Libraries/LibGfx/Rust",
"Libraries/LibJS/Rust",
"Libraries/LibRegex/Rust",
"Libraries/LibUnicode/Rust",

View File

@@ -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()

View File

@@ -208,8 +208,10 @@ ErrorOr<NonnullRefPtr<ImmutableBitmap>> 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();

View File

@@ -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"

View File

@@ -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<dyn Error>> {
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(())
}

View File

@@ -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"

View File

@@ -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<YUVRange> for YuvRange {
fn from(range: YUVRange) -> Self {
match range {
YUVRange::Limited => YuvRange::Limited,
YUVRange::Full => YuvRange::Full,
}
}
}
impl From<YUVMatrix> 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<u16> = unsafe { core::slice::from_raw_parts(y_plane, y_len) }
.iter()
.map(|&v| v >> 2)
.collect();
let u_10: Vec<u16> = unsafe { core::slice::from_raw_parts(u_plane, uv_len) }
.iter()
.map(|&v| v >> 2)
.collect();
let v_10: Vec<u16> = 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()
}

View File

@@ -4,9 +4,12 @@
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <AK/Time.h>
#include <LibGfx/Bitmap.h>
#include <LibGfx/ImmutableBitmap.h>
#include <LibGfx/SkiaBackendContext.h>
#include <LibGfx/YUVData.h>
#include <RustFFI.h>
#include <core/SkColorSpace.h>
#include <core/SkImage.h>
@@ -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<NonnullRefPtr<Bitmap>> 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<u8*>(bitmap->scanline(0));
auto dst_stride = static_cast<u32>(bitmap->pitch());
auto width = static_cast<u32>(impl.size.width());
auto height = static_cast<u32>(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<int>(width);
for (u32 row = 0; row < height; row++) {
auto* dst_row = dst + (static_cast<size_t>(row) * dst_stride);
auto const* y_row = y_data + (static_cast<size_t>(row) * y_stride);
auto const* u_row = u_data + (static_cast<size_t>(row) * y_stride);
auto const* v_row = v_data + (static_cast<size_t>(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<u16 const*>(impl.y_buffer.data());
auto const* u_data = reinterpret_cast<u16 const*>(impl.u_buffer.data());
auto const* v_data = reinterpret_cast<u16 const*>(impl.v_buffer.data());
auto y_stride = static_cast<int>(width);
for (u32 row = 0; row < height; row++) {
auto* dst_row = dst + (static_cast<size_t>(row) * dst_stride);
auto const* y_row = y_data + (static_cast<size_t>(row) * y_stride);
auto const* u_row = u_data + (static_cast<size_t>(row) * y_stride);
auto const* v_row = v_data + (static_cast<size_t>(row) * y_stride);
for (u32 col = 0; col < width; col++) {
dst_row[(col * 4) + 0] = static_cast<u8>(v_row[col] >> shift);
dst_row[(col * 4) + 1] = static_cast<u8>(y_row[col] >> shift);
dst_row[(col * 4) + 2] = static_cast<u8>(u_row[col] >> shift);
dst_row[(col * 4) + 3] = 255;
}
}
}
return bitmap;
}
auto uv_size = impl.subsampling.subsampled_size(impl.size).to_type<u32>();
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<u16 const*>(impl.y_buffer.data()), y_stride,
reinterpret_cast<u16 const*>(impl.u_buffer.data()), uv_stride,
reinterpret_cast<u16 const*>(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;

View File

@@ -9,6 +9,8 @@
#include <AK/Error.h>
#include <AK/FixedArray.h>
#include <AK/NonnullOwnPtr.h>
#include <AK/NonnullRefPtr.h>
#include <LibGfx/Forward.h>
#include <LibGfx/Size.h>
#include <LibMedia/Color/CodingIndependentCodePoints.h>
#include <LibMedia/Subsampling.h>
@@ -42,6 +44,8 @@ public:
Bytes u_data();
Bytes v_data();
ErrorOr<NonnullRefPtr<Bitmap>> to_bitmap() const;
SkYUVAPixmaps make_pixmaps() const;
void expand_samples_to_full_16_bit_range();

View File

@@ -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",