use std::collections::HashMap;
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
use std::sync::{Arc, Mutex, MutexGuard};
use crate::PixelFormat;
use crate::error::{Error, Result};
use crate::frame::VideoParamsPod;
use crate::texture::{Frame, PlanarDescriptor, PlanarFormat, PlaneStorage, Subsampling, Texture};
pub mod external;
#[cfg(target_os = "linux")]
pub use external::DmaBufPlane;
#[cfg(any(target_os = "linux", target_os = "windows"))]
pub use external::GlMemoryExport;
pub use external::ImportGuard;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum BackendKind {
Auto,
Metal,
Vulkan,
Gl,
Cpu,
}
impl BackendKind {
pub fn from_config_string(s: &str) -> BackendKind {
match s.trim().to_ascii_lowercase().as_str() {
"" | "auto" => BackendKind::Auto,
"metal" => BackendKind::Metal,
"vulkan" => BackendKind::Vulkan,
"opengl" | "gl" => BackendKind::Gl,
"cpu" | "dummy" | "multiprocess" => BackendKind::Cpu,
_ => BackendKind::Auto,
}
}
pub fn to_config_string(self) -> &'static str {
match self {
BackendKind::Auto => "auto",
BackendKind::Metal => "metal",
BackendKind::Vulkan => "vulkan",
BackendKind::Gl => "opengl",
BackendKind::Cpu => "cpu",
}
}
pub fn from_user_config() -> BackendKind {
if let Ok(v) = std::env::var("QUERCUS_RENDER_BACKEND") {
if !v.is_empty() {
return BackendKind::from_config_string(&v);
}
}
let configured =
crate::commonutil::config_get_string(None, "GraphicsBackend").unwrap_or_default();
BackendKind::from_config_string(&configured)
}
fn wgpu_fallbacks(self) -> Vec<wgpu::Backends> {
match self {
BackendKind::Auto | BackendKind::Metal => {
vec![
wgpu::Backends::METAL,
wgpu::Backends::VULKAN,
wgpu::Backends::GL,
]
}
BackendKind::Vulkan => {
vec![
wgpu::Backends::VULKAN,
wgpu::Backends::METAL,
wgpu::Backends::GL,
]
}
BackendKind::Gl => {
vec![
wgpu::Backends::GL,
wgpu::Backends::METAL,
wgpu::Backends::VULKAN,
]
}
BackendKind::Cpu => Vec::new(),
}
}
fn from_wgpu_backend(b: wgpu::Backend) -> BackendKind {
match b {
wgpu::Backend::Metal => BackendKind::Metal,
wgpu::Backend::Vulkan => BackendKind::Vulkan,
wgpu::Backend::Gl => BackendKind::Gl,
_ => BackendKind::Cpu,
}
}
}
pub const CONFIG_KEY_DISPLAY_BIT_DEPTH: &str = "DisplayBitDepth";
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum DisplayBitDepth {
Bit8,
Bit10,
}
impl DisplayBitDepth {
pub fn from_config_string(s: &str) -> DisplayBitDepth {
match s.trim() {
"8" => DisplayBitDepth::Bit8,
_ => DisplayBitDepth::Bit10,
}
}
pub fn to_config_string(self) -> &'static str {
match self {
DisplayBitDepth::Bit8 => "8",
DisplayBitDepth::Bit10 => "10",
}
}
pub fn from_user_config() -> DisplayBitDepth {
let configured = crate::commonutil::config_get_string(None, CONFIG_KEY_DISPLAY_BIT_DEPTH)
.unwrap_or_default();
DisplayBitDepth::from_config_string(&configured)
}
pub fn present_formats(self) -> &'static [wgpu::TextureFormat] {
match self {
DisplayBitDepth::Bit10 => &[
wgpu::TextureFormat::Rgb10a2Unorm,
wgpu::TextureFormat::Rgba16Float,
],
DisplayBitDepth::Bit8 => &[
wgpu::TextureFormat::Bgra8Unorm,
wgpu::TextureFormat::Rgba8Unorm,
],
}
}
}
#[derive(Clone)]
struct GpuTexture {
texture: Arc<wgpu::Texture>,
width: u32,
height: u32,
format: wgpu::TextureFormat,
aspect: wgpu::TextureAspect,
layer: Option<(u32, u32)>,
#[cfg(any(target_os = "linux", target_os = "windows"))]
external: Option<Arc<external::ExternalTexture>>,
keep_alive: Option<Arc<dyn std::any::Any + Send + Sync>>,
}
fn lock<T>(m: &Mutex<T>) -> MutexGuard<'_, T> {
m.lock().unwrap_or_else(|e| e.into_inner())
}
fn create_lock() -> MutexGuard<'static, ()> {
static LOCK: Mutex<()> = Mutex::new(());
lock(&LOCK)
}
pub trait GpuContextLike: Send + Sync {
fn kind(&self) -> BackendKind;
fn destroy_texture(&self, token: u64);
fn upload(&self, token: u64, frame: &Frame) -> Result<()>;
fn download(&self, token: u64) -> Result<Frame>;
fn download_async(&self, token: u64) -> Result<FrameReadback> {
Err(Error::State)
}
fn blit(
&self,
src: u64,
dst: u64,
processor: Option<&crate::color::ColorProcessor>,
) -> Result<()>;
fn as_any(&self) -> Option<&dyn std::any::Any> {
None
}
fn texture_handle(&self, _token: u64) -> Option<Arc<wgpu::Texture>> {
None
}
}
pub struct GpuContext {
_instance: Option<wgpu::Instance>,
_adapter: Option<wgpu::Adapter>,
device: wgpu::Device,
queue: wgpu::Queue,
kind: BackendKind,
textures: Mutex<HashMap<u64, GpuTexture>>,
next_token: AtomicU64,
blit: Mutex<Option<wgpu::RenderPipeline>>,
blit16: Mutex<Option<wgpu::RenderPipeline>>,
filterable: bool,
programs: Mutex<HashMap<String, Arc<ShaderProgram>>>,
placeholder: Mutex<Option<u64>>,
display_lut: Mutex<Option<DisplayLutState>>,
present: Mutex<Vec<(wgpu::TextureFormat, PresentPipeline)>>,
yuv: Mutex<Option<PresentPipeline>>,
yuv_planar: Mutex<Option<PresentPipeline>>,
planar_matrix: Mutex<Option<PresentPipeline>>,
color_luts: Mutex<Vec<(String, u64)>>,
used: AtomicBool,
device_lost: Arc<AtomicBool>,
external_lock: Arc<Mutex<()>>,
ofx_zerocopy: bool,
}
struct DisplayLutState {
token: u64,
edge: u32,
lo: [f32; 3],
hi: [f32; 3],
}
static GPU_UPLOADS: AtomicU64 = AtomicU64::new(0);
static GPU_DOWNLOADS: AtomicU64 = AtomicU64::new(0);
pub fn gpu_transfer_counters() -> (u64, u64) {
(
GPU_UPLOADS.load(Ordering::Relaxed),
GPU_DOWNLOADS.load(Ordering::Relaxed),
)
}
pub fn reset_gpu_transfer_counters() {
GPU_UPLOADS.store(0, Ordering::Relaxed);
GPU_DOWNLOADS.store(0, Ordering::Relaxed);
}
struct SharedSlot {
decided: bool,
host_installed: bool,
ctx: Option<Arc<GpuContext>>,
}
fn shared_slot() -> &'static Mutex<SharedSlot> {
static SLOT: std::sync::OnceLock<Mutex<SharedSlot>> = std::sync::OnceLock::new();
SLOT.get_or_init(|| {
Mutex::new(SharedSlot {
decided: false,
host_installed: false,
ctx: None,
})
})
}
fn zerocopy_requested() -> bool {
#[cfg(target_os = "linux")]
{
!matches!(
std::env::var("QUERCUS_OFX_GPU_ZEROCOPY").ok().as_deref(),
Some("0") | Some("false") | Some("FALSE") | Some("False")
)
}
#[cfg(target_os = "windows")]
{
matches!(
std::env::var("QUERCUS_OFX_GPU_ZEROCOPY").ok().as_deref(),
Some("1") | Some("true") | Some("TRUE") | Some("True")
)
}
#[cfg(not(any(target_os = "linux", target_os = "windows")))]
{
false
}
}
fn require_gpu_from_value(value: Option<&str>) -> bool {
match value {
Some(v) => !(v == "0" || v.eq_ignore_ascii_case("false")),
None => false,
}
}
pub fn require_gpu_adapter() -> bool {
require_gpu_from_value(std::env::var("QUERCUS_REQUIRE_GPU").ok().as_deref())
}
fn skip_or_fail_gpu_with(required: bool, what: &str) {
if required {
panic!("no GPU adapter available for {what} (QUERCUS_REQUIRE_GPU is set)");
}
eprintln!("no GPU adapter; skipping {what}");
}
pub fn skip_or_fail_gpu(what: &str) {
skip_or_fail_gpu_with(require_gpu_adapter(), what);
}
pub fn gpu_or_skip(what: &str) -> Option<Arc<GpuContext>> {
let ctx = GpuContext::create(BackendKind::Auto);
if ctx.is_none() {
skip_or_fail_gpu(what);
}
ctx
}
pub fn shared_gpu_or_skip(what: &str) -> Option<Arc<GpuContext>> {
let ctx = GpuContext::shared();
if ctx.is_none() {
skip_or_fail_gpu(what);
}
ctx
}
unsafe impl Send for GpuContext {}
unsafe impl Sync for GpuContext {}
#[derive(Clone, Copy, Debug)]
pub struct PlaneData<'a> {
pub data: &'a [u8],
pub stride: usize,
}
impl<'a> PlaneData<'a> {
pub fn tight(data: &'a [u8]) -> Self {
Self { data, stride: 0 }
}
pub fn strided(data: &'a [u8], stride: usize) -> Self {
Self { data, stride }
}
}
const PLANE_ROW_ALIGNMENT: usize = wgpu::COPY_BYTES_PER_ROW_ALIGNMENT as usize;
fn aligned_plane_row_bytes(row_bytes: usize) -> usize {
row_bytes.div_ceil(PLANE_ROW_ALIGNMENT) * PLANE_ROW_ALIGNMENT
}
fn repack_plane_rows(
data: &[u8],
src_stride: usize,
row_bytes: usize,
height: usize,
) -> Result<Vec<u8>> {
if row_bytes == 0 || height == 0 {
return Err(Error::Invalid);
}
let stride = if src_stride == 0 {
row_bytes
} else {
src_stride
};
if stride < row_bytes {
return Err(Error::Invalid);
}
let needed = height
.checked_sub(1)
.and_then(|rows| rows.checked_mul(stride))
.and_then(|n| n.checked_add(row_bytes))
.ok_or(Error::Invalid)?;
if data.len() < needed {
return Err(Error::Invalid);
}
let staged_row = aligned_plane_row_bytes(row_bytes);
let staged_len = staged_row.checked_mul(height).ok_or(Error::Invalid)?;
let mut staged = vec![0u8; staged_len];
for row in 0..height {
let src_start = row * stride;
let dst_start = row * staged_row;
staged[dst_start..dst_start + row_bytes]
.copy_from_slice(&data[src_start..src_start + row_bytes]);
}
Ok(staged)
}
fn expand_rgb_to_rgba(
data: &[u8],
src_stride: usize,
width: usize,
height: usize,
storage: PlaneStorage,
) -> Result<Vec<u8>> {
if width == 0 || height == 0 {
return Err(Error::Invalid);
}
let bpc = storage.bytes_per_component();
let src_row = width.checked_mul(3 * bpc).ok_or(Error::Invalid)?;
let dst_row = width.checked_mul(4 * bpc).ok_or(Error::Invalid)?;
let stride = if src_stride == 0 { src_row } else { src_stride };
if stride < src_row {
return Err(Error::Invalid);
}
let needed = height
.checked_sub(1)
.and_then(|rows| rows.checked_mul(stride))
.and_then(|n| n.checked_add(src_row))
.ok_or(Error::Invalid)?;
if data.len() < needed {
return Err(Error::Invalid);
}
let alpha: &[u8] = match storage {
PlaneStorage::U8 => &[0xFF],
PlaneStorage::U16 => &[0xFF, 0xFF],
PlaneStorage::F16 => &[0x00, 0x3C],
PlaneStorage::F32 => &[0x00, 0x00, 0x80, 0x3F],
};
let mut out = vec![0u8; dst_row.checked_mul(height).ok_or(Error::Invalid)?];
for row in 0..height {
let src_start = row * stride;
let dst_start = row * dst_row;
for pixel in 0..width {
let s = src_start + pixel * 3 * bpc;
let d = dst_start + pixel * 4 * bpc;
out[d..d + 3 * bpc].copy_from_slice(&data[s..s + 3 * bpc]);
out[d + 3 * bpc..d + 4 * bpc].copy_from_slice(&alpha[..bpc]);
}
}
Ok(out)
}
pub struct FrameReadback {
rx: std::sync::mpsc::Receiver<Result<Frame>>,
_guard: Option<Arc<dyn std::any::Any + Send + Sync>>,
}
impl FrameReadback {
pub fn wait(self) -> Result<Frame> {
self.rx
.recv()
.unwrap_or_else(|_| Err(Error::Failed("readback worker stopped".into())))
}
}
struct ReadbackJob {
device: wgpu::Device,
queue: wgpu::Queue,
texture: Arc<wgpu::Texture>,
format: wgpu::TextureFormat,
width: u32,
height: u32,
external_lock: Arc<Mutex<()>>,
tx: std::sync::mpsc::Sender<Result<Frame>>,
}
fn readback_worker() -> &'static std::sync::mpsc::Sender<ReadbackJob> {
static TX: std::sync::OnceLock<std::sync::mpsc::Sender<ReadbackJob>> = std::sync::OnceLock::new();
TX.get_or_init(|| {
let (tx, rx) = std::sync::mpsc::channel::<ReadbackJob>();
let _ = std::thread::Builder::new()
.name("quercus-readback".into())
.spawn(move || {
while let Ok(job) = rx.recv() {
let _external = job
.external_lock
.lock()
.unwrap_or_else(|e| e.into_inner());
let result = download_texture(
&job.device,
&job.queue,
&job.texture,
job.format,
job.width,
job.height,
);
let _ = job.tx.send(result);
}
});
tx
})
}
fn download_texture(
device: &wgpu::Device,
queue: &wgpu::Queue,
texture: &wgpu::Texture,
format: wgpu::TextureFormat,
width: u32,
height: u32,
) -> Result<Frame> {
GPU_DOWNLOADS.fetch_add(1, Ordering::Relaxed);
let (bpp, convert_half) = match format {
wgpu::TextureFormat::Rgba32Float => (16usize, false),
wgpu::TextureFormat::Rgba16Float => (8usize, true),
other => {
return Err(Error::Failed(format!(
"texture download: unsupported format {other:?}"
)))
}
};
let w = width as usize;
let h = height as usize;
let linesize = w * bpp;
let padded = (linesize + 255) & !255;
let buffer = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("quercusrender-download"),
size: (padded * h) as u64,
usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
mapped_at_creation: false,
});
let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
label: Some("quercusrender-download"),
});
encoder.copy_texture_to_buffer(
wgpu::TexelCopyTextureInfo {
texture,
mip_level: 0,
origin: wgpu::Origin3d::ZERO,
aspect: wgpu::TextureAspect::All,
},
wgpu::TexelCopyBufferInfo {
buffer: &buffer,
layout: wgpu::TexelCopyBufferLayout {
offset: 0,
bytes_per_row: Some(padded as u32),
rows_per_image: None,
},
},
wgpu::Extent3d {
width: w as u32,
height: h as u32,
depth_or_array_layers: 1,
},
);
queue.submit(Some(encoder.finish()));
let (tx, rx) = std::sync::mpsc::channel();
buffer
.slice(..)
.map_async(wgpu::MapMode::Read, move |result| {
let _ = tx.send(result.is_ok());
});
let deadline =
std::time::Instant::now() + std::time::Duration::from_secs(30);
let mapped_ok = loop {
let poll = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
let _ = device.poll(wgpu::PollType::Poll);
}));
if poll.is_err() {
return Err(Error::Failed(
"wgpu device lost during texture download".into(),
));
}
match rx.try_recv() {
Ok(ok) => break ok,
Err(std::sync::mpsc::TryRecvError::Disconnected) => break false,
Err(std::sync::mpsc::TryRecvError::Empty) => {}
}
if std::time::Instant::now() > deadline {
return Err(Error::Failed("texture download map timed out".into()));
}
std::thread::sleep(std::time::Duration::from_millis(1));
};
if !mapped_ok {
return Err(Error::Failed("texture download map failed".into()));
}
let mapped = buffer.slice(..).get_mapped_range();
let mut data = if padded == linesize {
mapped.to_vec()
} else {
let mut data = vec![0u8; linesize * h];
for row in 0..h {
let src = &mapped[row * padded..row * padded + linesize];
data[row * linesize..(row + 1) * linesize].copy_from_slice(src);
}
data
};
drop(mapped);
buffer.unmap();
if convert_half {
let mut f32_data = vec![0u8; w * h * 16];
for i in 0..w * h * 4 {
let bits = u16::from_le_bytes([data[i * 2], data[i * 2 + 1]]);
let v = half::f16::from_bits(bits).to_f32();
f32_data[i * 4..i * 4 + 4].copy_from_slice(&v.to_le_bytes());
}
data = f32_data;
}
let mut frame = Frame::new();
let mut pod = VideoParamsPod::default();
pod.width = w as i32;
pod.height = h as i32;
pod.format = PixelFormat::F32 as i32;
frame.set_video_params(pod);
frame.data = data;
Ok(frame)
}
impl GpuContext {
pub fn create(prefer: BackendKind) -> Option<Arc<Self>> {
let _create_guard = create_lock();
for backends in prefer.wgpu_fallbacks() {
let instance = wgpu::Instance::new(wgpu::InstanceDescriptor {
backends,
..wgpu::InstanceDescriptor::new_without_display_handle()
});
let adapter =
match pollster_block_on(instance.request_adapter(&wgpu::RequestAdapterOptions {
power_preference: wgpu::PowerPreference::HighPerformance,
compatible_surface: None,
force_fallback_adapter: false,
})) {
Ok(a) => a,
Err(_) => continue, };
let info = adapter.get_info();
if !adapter
.get_texture_format_features(wgpu::TextureFormat::Rgba32Float)
.allowed_usages
.contains(wgpu::TextureUsages::RENDER_ATTACHMENT)
{
continue;
}
let filterable = adapter
.features()
.contains(wgpu::Features::FLOAT32_FILTERABLE);
let mut required_features = wgpu::Features::empty();
if filterable {
required_features |= wgpu::Features::FLOAT32_FILTERABLE;
}
for feature in [
wgpu::Features::TEXTURE_FORMAT_NV12,
wgpu::Features::TEXTURE_FORMAT_P010,
] {
if adapter.features().contains(feature) {
required_features |= feature;
}
}
if adapter
.features()
.contains(wgpu::Features::TEXTURE_FORMAT_16BIT_NORM)
{
required_features |= wgpu::Features::TEXTURE_FORMAT_16BIT_NORM;
}
#[cfg(target_os = "windows")]
if adapter
.features()
.contains(wgpu::Features::VULKAN_EXTERNAL_MEMORY_WIN32)
{
required_features |= wgpu::Features::VULKAN_EXTERNAL_MEMORY_WIN32;
}
let (device, queue) =
match pollster_block_on(adapter.request_device(&wgpu::DeviceDescriptor {
label: Some("quercusrender"),
required_features,
required_limits: wgpu::Limits::default(),
experimental_features: wgpu::ExperimentalFeatures::disabled(),
memory_hints: wgpu::MemoryHints::default(),
trace: wgpu::Trace::Off,
})) {
Ok(dq) => dq,
Err(_) => continue,
};
let kind = BackendKind::from_wgpu_backend(info.backend);
if kind == BackendKind::Cpu {
continue;
}
let device_lost = Arc::new(AtomicBool::new(false));
install_device_lost_logging(&device, device_lost.clone());
return Some(Arc::new(Self {
_instance: Some(instance),
_adapter: Some(adapter),
device,
queue,
kind,
textures: Mutex::new(HashMap::new()),
next_token: AtomicU64::new(1),
blit: Mutex::new(None),
blit16: Mutex::new(None),
filterable,
programs: Mutex::new(HashMap::new()),
placeholder: Mutex::new(None),
display_lut: Mutex::new(None),
present: Mutex::new(Vec::new()),
yuv: Mutex::new(None),
yuv_planar: Mutex::new(None),
planar_matrix: Mutex::new(None),
color_luts: Mutex::new(Vec::new()),
used: AtomicBool::new(false),
device_lost,
external_lock: Arc::new(Mutex::new(())),
ofx_zerocopy: zerocopy_requested(),
}));
}
None
}
pub fn adopt(
device: Arc<wgpu::Device>,
queue: Arc<wgpu::Queue>,
kind: BackendKind,
) -> Arc<Self> {
let filterable = device
.features()
.contains(wgpu::Features::FLOAT32_FILTERABLE);
Arc::new(Self {
_instance: None,
_adapter: None,
device: (*device).clone(),
queue: (*queue).clone(),
kind,
textures: Mutex::new(HashMap::new()),
next_token: AtomicU64::new(1),
blit: Mutex::new(None),
blit16: Mutex::new(None),
filterable,
programs: Mutex::new(HashMap::new()),
placeholder: Mutex::new(None),
display_lut: Mutex::new(None),
present: Mutex::new(Vec::new()),
yuv: Mutex::new(None),
yuv_planar: Mutex::new(None),
planar_matrix: Mutex::new(None),
color_luts: Mutex::new(Vec::new()),
used: AtomicBool::new(false),
device_lost: Arc::new(AtomicBool::new(false)),
external_lock: Arc::new(Mutex::new(())),
ofx_zerocopy: zerocopy_requested(),
})
}
pub fn is_adopted(&self) -> bool {
self._adapter.is_none()
}
pub fn adapter_name(&self) -> Option<String> {
self._adapter.as_ref().map(|a| a.get_info().name)
}
pub fn is_used(&self) -> bool {
self.used.load(Ordering::Acquire)
}
pub fn device_queue(&self) -> (Arc<wgpu::Device>, Arc<wgpu::Queue>) {
(Arc::new(self.device.clone()), Arc::new(self.queue.clone()))
}
pub fn wait_idle(&self) {
let _ = self.queue.submit(std::iter::empty());
if std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
let _ = self.device.poll(wgpu::PollType::wait_indefinitely());
}))
.is_err()
{
self.mark_device_lost();
}
}
pub fn wait_idle_for(&self, timeout: std::time::Duration) -> bool {
let _ = self.queue.submit(std::iter::empty());
std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
matches!(
self.device.poll(wgpu::PollType::Wait {
submission_index: None,
timeout: Some(timeout),
}),
Ok(wgpu::PollStatus::WaitSucceeded) | Ok(wgpu::PollStatus::QueueEmpty)
)
}))
.unwrap_or_else(|_| {
self.mark_device_lost();
false
})
}
pub fn external_lifecycle_lock(&self) -> Arc<Mutex<()>> {
self.external_lock.clone()
}
pub fn on_submitted_work_done(&self, callback: impl FnOnce() + Send + 'static) {
self.queue.on_submitted_work_done(callback);
}
pub fn device_lost(&self) -> bool {
self.device_lost.load(std::sync::atomic::Ordering::Acquire)
}
fn mark_device_lost(&self) {
self.device_lost
.store(true, std::sync::atomic::Ordering::Release);
}
pub fn kind(&self) -> BackendKind {
self.kind
}
pub fn is_gpu(&self) -> bool {
self.kind != BackendKind::Cpu
}
pub fn create_texture(&self, width: i32, height: i32) -> Result<u64> {
self.create_texture_format(
width,
height,
1,
wgpu::TextureFormat::Rgba32Float,
wgpu::TextureUsages::TEXTURE_BINDING
| wgpu::TextureUsages::RENDER_ATTACHMENT
| wgpu::TextureUsages::COPY_DST
| wgpu::TextureUsages::COPY_SRC,
)
}
pub fn create_texture_format(
&self,
width: i32,
height: i32,
depth: u32,
format: wgpu::TextureFormat,
usage: wgpu::TextureUsages,
) -> Result<u64> {
if width <= 0 || height <= 0 || depth == 0 {
return Err(Error::Invalid);
}
#[cfg(any(target_os = "linux", target_os = "windows"))]
if self.ofx_zerocopy && depth == 1 && format == wgpu::TextureFormat::Rgba32Float {
match self.create_exportable_texture(width, height, format, usage) {
Ok(token) => return Ok(token),
Err(err) => {
log::debug!("exportable Rgba32Float texture unavailable, falling back: {err}");
}
}
}
self.used.store(true, Ordering::Release);
let size = wgpu::Extent3d {
width: width as u32,
height: height as u32,
depth_or_array_layers: depth,
};
let texture = self.device.create_texture(&wgpu::TextureDescriptor {
label: Some("quercusrender-texture"),
size,
mip_level_count: 1,
sample_count: 1,
dimension: if depth > 1 {
wgpu::TextureDimension::D3
} else {
wgpu::TextureDimension::D2
},
format,
usage,
view_formats: &[],
});
let token = self.next_token.fetch_add(1, Ordering::Relaxed);
lock(&self.textures).insert(
token,
GpuTexture {
texture: Arc::new(texture),
width: width as u32,
height: height as u32,
format,
aspect: wgpu::TextureAspect::All,
layer: None,
#[cfg(any(target_os = "linux", target_os = "windows"))]
external: None,
keep_alive: None,
},
);
Ok(token)
}
pub fn destroy_texture(&self, token: u64) {
lock(&self.textures).remove(&token);
}
pub fn set_texture_keep_alive(
&self,
token: u64,
guard: Arc<dyn std::any::Any + Send + Sync>,
) {
if let Some(entry) = lock(&self.textures).get_mut(&token) {
entry.keep_alive = Some(guard);
}
}
pub fn texture_size(&self, token: u64) -> Option<(u32, u32)> {
lock(&self.textures)
.get(&token)
.map(|t| (t.width, t.height))
}
pub fn has_texture(&self, token: u64) -> bool {
lock(&self.textures).contains_key(&token)
}
pub fn texture_handle(&self, token: u64) -> Option<Arc<wgpu::Texture>> {
lock(&self.textures).get(&token).map(|t| t.texture.clone())
}
pub fn take_texture_handle(&self, token: u64) -> Option<Arc<wgpu::Texture>> {
lock(&self.textures).remove(&token).map(|t| t.texture)
}
pub fn texture_format(&self, token: u64) -> Option<wgpu::TextureFormat> {
lock(&self.textures).get(&token).map(|t| t.format)
}
pub fn upload_plane(&self, token: u64, data: &[u8]) -> Result<()> {
GPU_UPLOADS.fetch_add(1, Ordering::Relaxed);
let entry = lock(&self.textures)
.get(&token)
.cloned()
.ok_or(Error::NotFound)?;
let w = entry.width;
let h = entry.height;
let bpp = match entry.format {
wgpu::TextureFormat::R8Unorm => 1usize,
wgpu::TextureFormat::R16Unorm | wgpu::TextureFormat::R16Float => 2,
wgpu::TextureFormat::R32Float => 4,
_ => return Err(Error::Invalid),
};
let row = w as usize * bpp;
if data.len() < row * h as usize {
return Err(Error::Invalid);
}
self.queue.write_texture(
wgpu::TexelCopyTextureInfo {
texture: &entry.texture,
mip_level: 0,
origin: wgpu::Origin3d::ZERO,
aspect: wgpu::TextureAspect::All,
},
data,
wgpu::TexelCopyBufferLayout {
offset: 0,
bytes_per_row: Some(row as u32),
rows_per_image: None,
},
wgpu::Extent3d {
width: w,
height: h,
depth_or_array_layers: 1,
},
);
Ok(())
}
pub fn supports_16bit_norm_planes(&self) -> bool {
self.device
.features()
.contains(wgpu::Features::TEXTURE_FORMAT_16BIT_NORM)
}
pub fn create_planar_matrix_textures(
&self,
descriptor: PlanarDescriptor,
width: i32,
height: i32,
) -> Result<Vec<u64>> {
if width <= 0 || height <= 0 || descriptor.plane_count() == 0 {
return Err(Error::Invalid);
}
if descriptor.storage == PlaneStorage::U16 && !self.supports_16bit_norm_planes() {
return Err(Error::Failed(
"16-bit normalized texture formats are not enabled on this device".into(),
));
}
let usage = wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST;
let mut tokens = Vec::with_capacity(descriptor.plane_count());
for plane in 0..descriptor.plane_count() {
let (plane_width, plane_height) = descriptor
.plane_size(plane, width, height)
.ok_or(Error::Invalid)?;
let format = descriptor
.plane_texture_format(plane)
.ok_or(Error::Invalid)?;
match self.create_texture_format(plane_width, plane_height, 1, format, usage) {
Ok(token) => tokens.push(token),
Err(err) => {
for token in &tokens {
self.destroy_texture(*token);
}
return Err(err);
}
}
}
Ok(tokens)
}
pub fn upload_planar_matrix_planes(
&self,
planes: &[u64],
descriptor: PlanarDescriptor,
data: &[PlaneData<'_>],
) -> Result<()> {
if planes.len() != descriptor.plane_count() || data.len() != planes.len() {
return Err(Error::Invalid);
}
for (plane, (token, plane_data)) in planes.iter().zip(data).enumerate() {
self.write_planar_plane(*token, descriptor, plane, plane_data)?;
}
Ok(())
}
pub fn upload_planar_matrix_frame(
&self,
descriptor: PlanarDescriptor,
width: i32,
height: i32,
data: &[PlaneData<'_>],
) -> Result<Vec<u64>> {
if data.len() != descriptor.plane_count() {
return Err(Error::Invalid);
}
let planes = self.create_planar_matrix_textures(descriptor, width, height)?;
if let Err(err) = self.upload_planar_matrix_planes(&planes, descriptor, data) {
for token in &planes {
self.destroy_texture(*token);
}
return Err(err);
}
Ok(planes)
}
pub fn create_planar_textures(
&self,
format: PlanarFormat,
width: i32,
height: i32,
) -> Result<(u64, u64)> {
let mut planes = self.create_planar_matrix_textures(format.descriptor(), width, height)?;
if planes.len() == 2 {
Ok((planes[0], planes[1]))
} else {
for token in &planes {
self.destroy_texture(*token);
}
planes.clear();
Err(Error::Invalid)
}
}
pub fn upload_planar_planes(
&self,
y: u64,
uv: u64,
y_bytes: &[u8],
uv_bytes: &[u8],
) -> Result<()> {
self.upload_planar_planes_strided(y, uv, y_bytes, 0, uv_bytes, 0)
}
pub fn upload_planar_planes_strided(
&self,
y: u64,
uv: u64,
y_bytes: &[u8],
y_stride: usize,
uv_bytes: &[u8],
uv_stride: usize,
) -> Result<()> {
let descriptor = self.infer_semi_planar_descriptor(y, uv, None)?;
self.upload_planar_matrix_planes(
&[y, uv],
descriptor,
&[
PlaneData::strided(y_bytes, y_stride),
PlaneData::strided(uv_bytes, uv_stride),
],
)
}
#[allow(clippy::too_many_arguments)]
pub fn upload_planar_frame(
&self,
format: PlanarFormat,
width: i32,
height: i32,
y_bytes: &[u8],
y_stride: usize,
uv_bytes: &[u8],
uv_stride: usize,
) -> Result<(u64, u64)> {
let descriptor = format.descriptor();
let planes = self.create_planar_matrix_textures(descriptor, width, height)?;
if let Err(err) = self.upload_planar_matrix_planes(
&planes,
descriptor,
&[
PlaneData::strided(y_bytes, y_stride),
PlaneData::strided(uv_bytes, uv_stride),
],
) {
for token in &planes {
self.destroy_texture(*token);
}
return Err(err);
}
Ok((planes[0], planes[1]))
}
fn write_planar_plane(
&self,
token: u64,
descriptor: PlanarDescriptor,
plane: usize,
data: &PlaneData<'_>,
) -> Result<()> {
let entry = lock(&self.textures)
.get(&token)
.cloned()
.ok_or(Error::NotFound)?;
let expected_format = descriptor
.plane_texture_format(plane)
.ok_or(Error::Invalid)?;
if entry.format != expected_format
|| !entry
.texture
.usage()
.contains(wgpu::TextureUsages::COPY_DST)
{
return Err(Error::Invalid);
}
let source_components = descriptor
.plane_source_components(plane)
.ok_or(Error::Invalid)?;
let texture_components = descriptor
.plane_texture_components(plane)
.ok_or(Error::Invalid)?;
let bpc = descriptor.storage.bytes_per_component();
let width = entry.width as usize;
let height = entry.height as usize;
if width == 0 || height == 0 {
return Err(Error::Invalid);
}
let source_row_bytes = width
.checked_mul(source_components * bpc)
.ok_or(Error::Invalid)?;
let texture_row_bytes = width
.checked_mul(texture_components * bpc)
.ok_or(Error::Invalid)?;
let staged = if source_components == texture_components {
repack_plane_rows(data.data, data.stride, source_row_bytes, height)?
} else if source_components == 3 && texture_components == 4 {
let expanded =
expand_rgb_to_rgba(data.data, data.stride, width, height, descriptor.storage)?;
repack_plane_rows(&expanded, 0, texture_row_bytes, height)?
} else {
return Err(Error::Invalid);
};
let staged_row = aligned_plane_row_bytes(texture_row_bytes);
GPU_UPLOADS.fetch_add(1, Ordering::Relaxed);
self.queue.write_texture(
wgpu::TexelCopyTextureInfo {
texture: &entry.texture,
mip_level: 0,
origin: wgpu::Origin3d::ZERO,
aspect: wgpu::TextureAspect::All,
},
&staged,
wgpu::TexelCopyBufferLayout {
offset: 0,
bytes_per_row: Some(staged_row as u32),
rows_per_image: Some(entry.height),
},
wgpu::Extent3d {
width: entry.width,
height: entry.height,
depth_or_array_layers: 1,
},
);
Ok(())
}
pub fn set_display_lut(&self, lut: &crate::lut::Lut3d) -> Result<()> {
let token = self.upload_lut(lut)?;
let mut slot = lock(&self.display_lut);
if let Some(old) = slot.take() {
self.destroy_texture(old.token);
}
*slot = Some(DisplayLutState {
token,
edge: lut.edge,
lo: lut.lo,
hi: lut.hi,
});
Ok(())
}
fn upload_lut(&self, lut: &crate::lut::Lut3d) -> Result<u64> {
let expected = (lut.edge as usize).pow(3) * 3;
if lut.data.len() != expected {
return Err(Error::Invalid);
}
let token = self.create_texture_format(
lut.edge as i32,
lut.edge as i32,
lut.edge,
wgpu::TextureFormat::Rgba32Float,
wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
)?;
let row = lut.edge as usize * 16;
let padded = (row + 255) & !255;
let mut bytes = vec![0u8; padded * lut.edge as usize * lut.edge as usize];
for z in 0..lut.edge as usize {
for y in 0..lut.edge as usize {
let src = &lut.data[(z * lut.edge as usize + y) * lut.edge as usize * 3..];
let dst_off = (z * lut.edge as usize + y) * padded;
for x in 0..lut.edge as usize {
bytes[dst_off + x * 16..dst_off + x * 16 + 4]
.copy_from_slice(&src[x * 3].to_le_bytes());
bytes[dst_off + x * 16 + 4..dst_off + x * 16 + 8]
.copy_from_slice(&src[x * 3 + 1].to_le_bytes());
bytes[dst_off + x * 16 + 8..dst_off + x * 16 + 12]
.copy_from_slice(&src[x * 3 + 2].to_le_bytes());
bytes[dst_off + x * 16 + 12..dst_off + x * 16 + 16]
.copy_from_slice(&1.0f32.to_le_bytes());
}
}
}
let entry = lock(&self.textures)
.get(&token)
.cloned()
.ok_or(Error::NotFound)?;
GPU_UPLOADS.fetch_add(1, Ordering::Relaxed);
self.queue.write_texture(
wgpu::TexelCopyTextureInfo {
texture: &entry.texture,
mip_level: 0,
origin: wgpu::Origin3d::ZERO,
aspect: wgpu::TextureAspect::All,
},
&bytes,
wgpu::TexelCopyBufferLayout {
offset: 0,
bytes_per_row: Some(padded as u32),
rows_per_image: Some(lut.edge),
},
wgpu::Extent3d {
width: lut.edge,
height: lut.edge,
depth_or_array_layers: lut.edge,
},
);
Ok(token)
}
pub fn has_display_lut(&self) -> bool {
lock(&self.display_lut).is_some()
}
pub fn present_texture(&self, src: u64) -> Result<u64> {
let lut = {
let slot = lock(&self.display_lut);
let Some(lut) = slot.as_ref() else {
return Err(Error::Failed("no display LUT installed".into()));
};
(lut.token, lut.edge, lut.lo, lut.hi)
};
self.apply_lut_to(
src,
lut.0,
lut.1,
lut.2,
lut.3,
wgpu::TextureFormat::Rgba32Float,
)
}
pub fn apply_color_lut(&self, src: u64, key: &str, lut: &crate::lut::Lut3d) -> Result<u64> {
let token = {
let mut cache = lock(&self.color_luts);
if let Some((_, token)) = cache.iter().find(|(k, _)| k == key) {
*token
} else {
let token = self.upload_lut(lut)?;
if cache.len() >= 8 {
let (_, old) = cache.remove(0);
self.destroy_texture(old);
}
cache.push((key.to_string(), token));
token
}
};
self.apply_lut_to(
src,
token,
lut.edge,
lut.lo,
lut.hi,
wgpu::TextureFormat::Rgba32Float,
)
}
fn apply_lut_to(
&self,
src: u64,
lut_token: u64,
edge: u32,
lo: [f32; 3],
hi: [f32; 3],
dst_format: wgpu::TextureFormat,
) -> Result<u64> {
let _external = self
.external_lock
.lock()
.unwrap_or_else(|e| e.into_inner());
let src_tex = lock(&self.textures)
.get(&src)
.cloned()
.ok_or(Error::NotFound)?;
let lut_tex = lock(&self.textures)
.get(&lut_token)
.cloned()
.ok_or(Error::NotFound)?;
let dst = self.create_texture_format(
src_tex.width as i32,
src_tex.height as i32,
1,
dst_format,
wgpu::TextureUsages::TEXTURE_BINDING
| wgpu::TextureUsages::RENDER_ATTACHMENT
| wgpu::TextureUsages::COPY_SRC,
)?;
let dst_tex = lock(&self.textures)
.get(&dst)
.cloned()
.ok_or(Error::NotFound)?;
let pipeline = self.present_pipeline(dst_format)?;
let params: [f32; 12] = [
edge as f32,
0.0,
0.0,
0.0,
lo[0],
lo[1],
lo[2],
0.0,
hi[0],
hi[1],
hi[2],
0.0,
];
let uniform = self.device.create_buffer(&wgpu::BufferDescriptor {
label: Some("quercusrender-present-params"),
size: 48,
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
self.queue
.write_buffer(&uniform, 0, &f32_uniform_bytes(¶ms));
let src_view = src_tex
.texture
.create_view(&wgpu::TextureViewDescriptor::default());
let lut_view = lut_tex
.texture
.create_view(&wgpu::TextureViewDescriptor::default());
let dst_view = dst_tex
.texture
.create_view(&wgpu::TextureViewDescriptor::default());
let bind_group = self.device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("quercusrender-present-bg"),
layout: &pipeline.layout,
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: wgpu::BindingResource::TextureView(&src_view),
},
wgpu::BindGroupEntry {
binding: 1,
resource: wgpu::BindingResource::TextureView(&lut_view),
},
wgpu::BindGroupEntry {
binding: 2,
resource: uniform.as_entire_binding(),
},
],
});
let mut encoder = self
.device
.create_command_encoder(&wgpu::CommandEncoderDescriptor {
label: Some("quercusrender-present"),
});
{
let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some("quercusrender-present-pass"),
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view: &dst_view,
depth_slice: None,
resolve_target: None,
ops: wgpu::Operations {
load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT),
store: wgpu::StoreOp::Store,
},
})],
depth_stencil_attachment: None,
timestamp_writes: None,
occlusion_query_set: None,
multiview_mask: None,
});
pass.set_pipeline(&pipeline.pipeline);
pass.set_bind_group(0, &bind_group, &[]);
pass.draw(0..3, 0..1);
}
self.queue.submit(Some(encoder.finish()));
Ok(dst)
}
fn present_pipeline(&self, format: wgpu::TextureFormat) -> Result<PresentPipeline> {
let mut cache = lock(&self.present);
if let Some((_, p)) = cache.iter().find(|(f, _)| *f == format) {
return Ok(p.clone());
}
let layout = self
.device
.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("quercusrender-present-layout"),
entries: &[
wgpu::BindGroupLayoutEntry {
binding: 0,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Texture {
sample_type: wgpu::TextureSampleType::Float { filterable: false },
view_dimension: wgpu::TextureViewDimension::D2,
multisampled: false,
},
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 1,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Texture {
sample_type: wgpu::TextureSampleType::Float { filterable: false },
view_dimension: wgpu::TextureViewDimension::D3,
multisampled: false,
},
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 2,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Uniform,
has_dynamic_offset: false,
min_binding_size: None,
},
count: None,
},
],
});
let vs = self
.device
.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some("quercusrender-present-vs"),
source: wgpu::ShaderSource::Wgsl(std::borrow::Cow::Borrowed(EFFECT_VS_WGSL)),
});
let fs = self
.device
.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some("quercusrender-present-fs"),
source: wgpu::ShaderSource::Wgsl(std::borrow::Cow::Borrowed(PRESENT_WGSL)),
});
let pipeline_layout = self
.device
.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("quercusrender-present-pipeline-layout"),
bind_group_layouts: &[Some(&layout)],
immediate_size: 0,
});
let scope = self.device.push_error_scope(wgpu::ErrorFilter::Validation);
let pipeline = self
.device
.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: Some("quercusrender-present"),
layout: Some(&pipeline_layout),
vertex: wgpu::VertexState {
module: &vs,
entry_point: Some("vs_main"),
compilation_options: Default::default(),
buffers: &[],
},
primitive: wgpu::PrimitiveState::default(),
depth_stencil: None,
multisample: wgpu::MultisampleState::default(),
fragment: Some(wgpu::FragmentState {
module: &fs,
entry_point: Some("main"),
compilation_options: Default::default(),
targets: &[Some(wgpu::ColorTargetState {
format,
blend: None,
write_mask: wgpu::ColorWrites::ALL,
})],
}),
multiview_mask: None,
cache: None,
});
if let Some(err) = pollster_block_on(scope.pop()) {
return Err(Error::Failed(format!(
"present pipeline validation failed: {err}"
)));
}
let program = PresentPipeline { pipeline, layout };
cache.push((format, program.clone()));
Ok(program)
}
pub fn run_yuv_to_rgb(
&self,
y: u64,
u: u64,
v: u64,
dst: u64,
transform: &YuvTransform,
) -> Result<()> {
let y_tex = lock(&self.textures)
.get(&y)
.cloned()
.ok_or(Error::NotFound)?;
let u_tex = lock(&self.textures)
.get(&u)
.cloned()
.ok_or(Error::NotFound)?;
let v_tex = lock(&self.textures)
.get(&v)
.cloned()
.ok_or(Error::NotFound)?;
let dst_tex = lock(&self.textures)
.get(&dst)
.cloned()
.ok_or(Error::NotFound)?;
let pipeline = self.yuv_pipeline()?;
let m = transform.matrix;
let b = transform.offset;
let params: [f32; 16] = [
m[0][0],
m[0][1],
m[0][2],
b[0],
m[1][0],
m[1][1],
m[1][2],
b[1],
m[2][0],
m[2][1],
m[2][2],
b[2],
u_tex.width as f32 / y_tex.width as f32,
u_tex.height as f32 / y_tex.height as f32,
0.0,
0.0,
];
let uniform = self.device.create_buffer(&wgpu::BufferDescriptor {
label: Some("quercusrender-yuv-params"),
size: 64,
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
self.queue
.write_buffer(&uniform, 0, &f32_uniform_bytes(¶ms));
let y_view = y_tex
.texture
.create_view(&wgpu::TextureViewDescriptor::default());
let u_view = u_tex
.texture
.create_view(&wgpu::TextureViewDescriptor::default());
let v_view = v_tex
.texture
.create_view(&wgpu::TextureViewDescriptor::default());
let dst_view = dst_tex
.texture
.create_view(&wgpu::TextureViewDescriptor::default());
let bind_group = self.device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("quercusrender-yuv-bg"),
layout: &pipeline.layout,
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: wgpu::BindingResource::TextureView(&y_view),
},
wgpu::BindGroupEntry {
binding: 1,
resource: wgpu::BindingResource::TextureView(&u_view),
},
wgpu::BindGroupEntry {
binding: 2,
resource: wgpu::BindingResource::TextureView(&v_view),
},
wgpu::BindGroupEntry {
binding: 3,
resource: uniform.as_entire_binding(),
},
],
});
let mut encoder = self
.device
.create_command_encoder(&wgpu::CommandEncoderDescriptor {
label: Some("quercusrender-yuv"),
});
{
let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some("quercusrender-yuv-pass"),
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view: &dst_view,
depth_slice: None,
resolve_target: None,
ops: wgpu::Operations {
load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT),
store: wgpu::StoreOp::Store,
},
})],
depth_stencil_attachment: None,
timestamp_writes: None,
occlusion_query_set: None,
multiview_mask: None,
});
pass.set_pipeline(&pipeline.pipeline);
pass.set_bind_group(0, &bind_group, &[]);
pass.draw(0..3, 0..1);
}
self.queue.submit(Some(encoder.finish()));
Ok(())
}
fn yuv_pipeline(&self) -> Result<PresentPipeline> {
let mut cache = lock(&self.yuv);
if let Some(p) = cache.as_ref() {
return Ok(p.clone());
}
let plane = |binding: u32| wgpu::BindGroupLayoutEntry {
binding,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Texture {
sample_type: wgpu::TextureSampleType::Float { filterable: false },
view_dimension: wgpu::TextureViewDimension::D2,
multisampled: false,
},
count: None,
};
let layout = self
.device
.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("quercusrender-yuv-layout"),
entries: &[
plane(0),
plane(1),
plane(2),
wgpu::BindGroupLayoutEntry {
binding: 3,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Uniform,
has_dynamic_offset: false,
min_binding_size: None,
},
count: None,
},
],
});
let vs = self
.device
.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some("quercusrender-yuv-vs"),
source: wgpu::ShaderSource::Wgsl(std::borrow::Cow::Borrowed(EFFECT_VS_WGSL)),
});
let fs = self
.device
.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some("quercusrender-yuv-fs"),
source: wgpu::ShaderSource::Wgsl(std::borrow::Cow::Borrowed(YUV_WGSL)),
});
let pipeline_layout = self
.device
.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("quercusrender-yuv-pipeline-layout"),
bind_group_layouts: &[Some(&layout)],
immediate_size: 0,
});
let scope = self.device.push_error_scope(wgpu::ErrorFilter::Validation);
let pipeline = self
.device
.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: Some("quercusrender-yuv"),
layout: Some(&pipeline_layout),
vertex: wgpu::VertexState {
module: &vs,
entry_point: Some("vs_main"),
compilation_options: Default::default(),
buffers: &[],
},
primitive: wgpu::PrimitiveState::default(),
depth_stencil: None,
multisample: wgpu::MultisampleState::default(),
fragment: Some(wgpu::FragmentState {
module: &fs,
entry_point: Some("main"),
compilation_options: Default::default(),
targets: &[Some(wgpu::ColorTargetState {
format: wgpu::TextureFormat::Rgba32Float,
blend: None,
write_mask: wgpu::ColorWrites::ALL,
})],
}),
multiview_mask: None,
cache: None,
});
if let Some(err) = pollster_block_on(scope.pop()) {
return Err(Error::Failed(format!(
"YUV pipeline validation failed: {err}"
)));
}
let program = PresentPipeline { pipeline, layout };
*cache = Some(program.clone());
Ok(program)
}
pub fn run_planar_yuv_to_rgb(
&self,
y: u64,
uv: u64,
dst: u64,
transform: &YuvTransform,
) -> Result<()> {
let y_view = self.texture_view(y)?;
let uv_view = self.texture_view(uv)?;
let (y_size, uv_size) = {
let textures = lock(&self.textures);
let y = textures.get(&y).ok_or(Error::NotFound)?;
let uv = textures.get(&uv).ok_or(Error::NotFound)?;
((y.width, y.height), (uv.width, uv.height))
};
let pipeline = self.planar_yuv_pipeline()?;
let m = transform.matrix;
let b = transform.offset;
let params: [f32; 16] = [
m[0][0],
m[0][1],
m[0][2],
b[0],
m[1][0],
m[1][1],
m[1][2],
b[1],
m[2][0],
m[2][1],
m[2][2],
b[2],
uv_size.0 as f32 / y_size.0.max(1) as f32,
uv_size.1 as f32 / y_size.1.max(1) as f32,
0.0,
0.0,
];
self.run_planar_views_pass(
&[&y_view, &uv_view],
dst,
&pipeline,
¶ms,
"quercusrender-yuv-planar",
)
}
fn run_planar_views_pass(
&self,
views: &[&wgpu::TextureView],
dst: u64,
pipeline: &PresentPipeline,
params: &[f32],
label: &str,
) -> Result<()> {
let _external = self
.external_lock
.lock()
.unwrap_or_else(|e| e.into_inner());
if self.device_lost() {
return Err(Error::Failed(
"wgpu device lost before planar pass".into(),
));
}
if views.is_empty() || views.len() > 3 {
return Err(Error::Invalid);
}
let dst_tex = lock(&self.textures)
.get(&dst)
.cloned()
.ok_or(Error::NotFound)?;
let mut uniform_bytes = f32_uniform_bytes(params);
let uniform_size = (uniform_bytes.len() as u64).next_multiple_of(16).max(16);
uniform_bytes.resize(uniform_size as usize, 0);
let params_label = format!("{label}-params");
let uniform = match uniform_buffer_from_bytes(
&self.device,
¶ms_label,
&uniform_bytes,
uniform_size,
) {
Ok(buffer) => buffer,
Err(error) => {
self.mark_device_lost();
return Err(error);
}
};
let dst_view = dst_tex
.texture
.create_view(&wgpu::TextureViewDescriptor::default());
let mut entries: Vec<wgpu::BindGroupEntry<'_>> = Vec::with_capacity(views.len() + 1);
for (binding, view) in views.iter().enumerate() {
entries.push(wgpu::BindGroupEntry {
binding: binding as u32,
resource: wgpu::BindingResource::TextureView(view),
});
}
entries.push(wgpu::BindGroupEntry {
binding: views.len() as u32,
resource: uniform.as_entire_binding(),
});
let bg_label = format!("{label}-bg");
let bind_group = self.device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some(&bg_label),
layout: &pipeline.layout,
entries: &entries,
});
let pass_label = format!("{label}-pass");
let mut encoder = self
.device
.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: Some(label) });
{
let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some(&pass_label),
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view: &dst_view,
depth_slice: None,
resolve_target: None,
ops: wgpu::Operations {
load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT),
store: wgpu::StoreOp::Store,
},
})],
depth_stencil_attachment: None,
timestamp_writes: None,
occlusion_query_set: None,
multiview_mask: None,
});
pass.set_pipeline(&pipeline.pipeline);
pass.set_bind_group(0, &bind_group, &[]);
pass.draw(0..3, 0..1);
}
self.queue.submit(Some(encoder.finish()));
Ok(())
}
fn planar_yuv_pipeline(&self) -> Result<PresentPipeline> {
let mut cache = lock(&self.yuv_planar);
if let Some(p) = cache.as_ref() {
return Ok(p.clone());
}
let program = self.build_planar_pipeline(YUV_PLANAR_WGSL, 2, "quercusrender-yuv-planar")?;
*cache = Some(program.clone());
Ok(program)
}
fn planar_matrix_pipeline(&self) -> Result<PresentPipeline> {
let mut cache = lock(&self.planar_matrix);
if let Some(p) = cache.as_ref() {
return Ok(p.clone());
}
let program =
self.build_planar_pipeline(YUV_PLANAR_MATRIX_WGSL, 3, "quercusrender-planar-matrix")?;
*cache = Some(program.clone());
Ok(program)
}
fn build_planar_pipeline(
&self,
shader: &str,
texture_count: usize,
label: &str,
) -> Result<PresentPipeline> {
let plane = |binding: u32| wgpu::BindGroupLayoutEntry {
binding,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Texture {
sample_type: wgpu::TextureSampleType::Float { filterable: false },
view_dimension: wgpu::TextureViewDimension::D2,
multisampled: false,
},
count: None,
};
let mut layout_entries = Vec::with_capacity(texture_count + 1);
for binding in 0..texture_count {
layout_entries.push(plane(binding as u32));
}
layout_entries.push(wgpu::BindGroupLayoutEntry {
binding: texture_count as u32,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Uniform,
has_dynamic_offset: false,
min_binding_size: None,
},
count: None,
});
let layout_label = format!("{label}-layout");
let layout = self
.device
.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some(&layout_label),
entries: &layout_entries,
});
let vs_label = format!("{label}-vs");
let vs = self
.device
.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some(&vs_label),
source: wgpu::ShaderSource::Wgsl(std::borrow::Cow::Borrowed(EFFECT_VS_WGSL)),
});
let fs_label = format!("{label}-fs");
let fs = self
.device
.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some(&fs_label),
source: wgpu::ShaderSource::Wgsl(std::borrow::Cow::Borrowed(shader)),
});
let pipeline_layout_label = format!("{label}-pipeline-layout");
let pipeline_layout = self
.device
.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some(&pipeline_layout_label),
bind_group_layouts: &[Some(&layout)],
immediate_size: 0,
});
let pipeline_label = format!("{label}-pipeline");
let scope = self.device.push_error_scope(wgpu::ErrorFilter::Validation);
let pipeline = self
.device
.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: Some(&pipeline_label),
layout: Some(&pipeline_layout),
vertex: wgpu::VertexState {
module: &vs,
entry_point: Some("vs_main"),
compilation_options: Default::default(),
buffers: &[],
},
primitive: wgpu::PrimitiveState::default(),
depth_stencil: None,
multisample: wgpu::MultisampleState::default(),
fragment: Some(wgpu::FragmentState {
module: &fs,
entry_point: Some("main"),
compilation_options: Default::default(),
targets: &[Some(wgpu::ColorTargetState {
format: wgpu::TextureFormat::Rgba32Float,
blend: None,
write_mask: wgpu::ColorWrites::ALL,
})],
}),
multiview_mask: None,
cache: None,
});
if let Some(err) = pollster_block_on(scope.pop()) {
return Err(Error::Failed(format!(
"{label} pipeline validation failed: {err}"
)));
}
Ok(PresentPipeline { pipeline, layout })
}
pub fn run_planar_matrix_to_rgb(
&self,
planes: &[u64],
dst: u64,
descriptor: PlanarDescriptor,
transform: &YuvTransform,
src_size: (u32, u32),
dst_size: (u32, u32),
) -> Result<()> {
let plane_count = descriptor.plane_count();
if planes.len() != plane_count
|| src_size.0 == 0
|| src_size.1 == 0
|| dst_size.0 == 0
|| dst_size.1 == 0
{
return Err(Error::Invalid);
}
let mut views = Vec::with_capacity(plane_count);
for (plane, token) in planes.iter().enumerate() {
let view = self.texture_view(*token)?;
let (size, format) = {
let textures = lock(&self.textures);
let entry = textures.get(token).ok_or(Error::NotFound)?;
((entry.width, entry.height), entry.format)
};
let want_size = descriptor
.plane_size(plane, src_size.0 as i32, src_size.1 as i32)
.ok_or(Error::Invalid)?;
if size.0 < want_size.0 as u32
|| size.1 < want_size.1 as u32
|| Some(format) != descriptor.plane_texture_format(plane)
{
if std::env::var_os("OAK_DEBUG_HW").is_some() {
eprintln!(
"[planar] plane {plane} mismatch: size={size:?} format={format:?} want={want_size:?} descriptor={:?}",
descriptor.plane_texture_format(plane),
);
}
return Err(Error::Invalid);
}
views.push(view);
}
{
let dst_tex = lock(&self.textures)
.get(&dst)
.cloned()
.ok_or(Error::NotFound)?;
if (dst_tex.width, dst_tex.height) != dst_size
|| dst_tex.format != wgpu::TextureFormat::Rgba32Float
{
return Err(Error::Invalid);
}
}
let pad_view = if plane_count < 3 {
Some(self.texture_view(planes[0])?)
} else {
None
};
let v0 = &views[0];
let v1 = views.get(1).or(pad_view.as_ref()).unwrap_or(v0);
let v2 = views.get(2).or(pad_view.as_ref()).unwrap_or(v0);
let mode = match descriptor.model {
crate::texture::ColorModel::YuvPlanar => 0.0,
crate::texture::ColorModel::YuvSemiPlanar => 1.0,
crate::texture::ColorModel::Rgb => 2.0,
crate::texture::ColorModel::Rgba => 3.0,
};
let m = transform.matrix;
let b = transform.offset;
let uv_scale = descriptor.uv_scale();
let chroma_size = descriptor
.plane_size(1, src_size.0 as i32, src_size.1 as i32)
.unwrap_or((src_size.0 as i32, src_size.1 as i32));
let params: [f32; 24] = [
m[0][0],
m[0][1],
m[0][2],
b[0],
m[1][0],
m[1][1],
m[1][2],
b[1],
m[2][0],
m[2][1],
m[2][2],
b[2],
uv_scale.0,
uv_scale.1,
mode,
0.0,
src_size.0 as f32 / dst_size.0 as f32,
src_size.1 as f32 / dst_size.1 as f32,
0.0,
0.0,
src_size.0 as f32,
src_size.1 as f32,
chroma_size.0 as f32,
chroma_size.1 as f32,
];
let pipeline = self.planar_matrix_pipeline()?;
self.run_planar_views_pass(
&[v0, v1, v2],
dst,
&pipeline,
¶ms,
"quercusrender-planar-matrix",
)
}
pub fn run_planar_matrix_to_rgb_native(
&self,
planes: &[u64],
dst: u64,
descriptor: PlanarDescriptor,
transform: &YuvTransform,
) -> Result<()> {
let first = planes.first().copied().ok_or(Error::Invalid)?;
let (width, height) = self.texture_size(first).ok_or(Error::NotFound)?;
let (dst_width, dst_height) = self.texture_size(dst).ok_or(Error::NotFound)?;
self.run_planar_matrix_to_rgb(
planes,
dst,
descriptor,
transform,
(width, height),
(dst_width, dst_height),
)
}
pub fn run_planar_yuv_to_rgb_scaled(
&self,
y: u64,
uv: u64,
dst: u64,
transform: &YuvTransform,
src_size: (u32, u32),
dst_size: (u32, u32),
) -> Result<()> {
if src_size.0 == 0 || src_size.1 == 0 || dst_size.0 == 0 || dst_size.1 == 0 {
return Err(Error::Invalid);
}
let descriptor = self.infer_semi_planar_descriptor(y, uv, Some(src_size))?;
self.run_planar_matrix_to_rgb(&[y, uv], dst, descriptor, transform, src_size, dst_size)
}
fn infer_semi_planar_descriptor(
&self,
y: u64,
uv: u64,
visible_size: Option<(u32, u32)>,
) -> Result<PlanarDescriptor> {
let (y_size, y_format, uv_size) = {
let textures = lock(&self.textures);
let y = textures.get(&y).ok_or(Error::NotFound)?;
let uv = textures.get(&uv).ok_or(Error::NotFound)?;
((y.width, y.height), y.format, (uv.width, uv.height))
};
let visible = visible_size.unwrap_or(y_size);
if visible.0 == 0 || visible.1 == 0 || y_size.0 < visible.0 || y_size.1 < visible.1 {
return Err(Error::Invalid);
}
let subsampling = [Subsampling::Cs444, Subsampling::Cs422, Subsampling::Cs420]
.into_iter()
.find(|candidate| {
let (width, height) = candidate.chroma_size(visible.0 as i32, visible.1 as i32);
width > 0 && height > 0 && uv_size.0 >= width as u32 && uv_size.1 >= height as u32
})
.ok_or(Error::Invalid)?;
let storage = PlaneStorage::from_texture_format(y_format).ok_or(Error::Invalid)?;
Ok(PlanarDescriptor::yuv_semi_planar(subsampling, storage))
}
pub fn clear_texture(&self, token: u64) -> Result<()> {
let entry = lock(&self.textures)
.get(&token)
.cloned()
.ok_or(Error::NotFound)?;
let view = entry
.texture
.create_view(&wgpu::TextureViewDescriptor::default());
let mut encoder = self
.device
.create_command_encoder(&wgpu::CommandEncoderDescriptor {
label: Some("quercusrender-clear"),
});
{
let _pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some("quercusrender-clear-pass"),
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view: &view,
depth_slice: None,
resolve_target: None,
ops: wgpu::Operations {
load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT),
store: wgpu::StoreOp::Store,
},
})],
depth_stencil_attachment: None,
timestamp_writes: None,
occlusion_query_set: None,
multiview_mask: None,
});
}
self.queue.submit(Some(encoder.finish()));
Ok(())
}
pub fn upload(&self, token: u64, frame: &Frame) -> Result<()> {
GPU_UPLOADS.fetch_add(1, Ordering::Relaxed);
if frame.format != PixelFormat::F32 {
return Err(Error::Invalid);
}
let entry = lock(&self.textures)
.get(&token)
.cloned()
.ok_or(Error::NotFound)?;
let w = entry.width as usize;
let h = entry.height as usize;
if frame.width != w as i32 || frame.height != h as i32 {
return Err(Error::Invalid);
}
let linesize = frame.linesize_bytes();
if frame.data.len() < linesize * h {
return Err(Error::Invalid);
}
self.queue.write_texture(
wgpu::TexelCopyTextureInfo {
texture: &entry.texture,
mip_level: 0,
origin: wgpu::Origin3d::ZERO,
aspect: wgpu::TextureAspect::All,
},
&frame.data[..linesize * h],
wgpu::TexelCopyBufferLayout {
offset: 0,
bytes_per_row: Some(linesize as u32),
rows_per_image: None,
},
wgpu::Extent3d {
width: w as u32,
height: h as u32,
depth_or_array_layers: 1,
},
);
Ok(())
}
pub fn download(&self, token: u64) -> Result<Frame> {
if self.device_lost() {
return Err(Error::Failed(
"wgpu device lost; texture download unavailable".into(),
));
}
let entry = lock(&self.textures)
.get(&token)
.cloned()
.ok_or(Error::NotFound)?;
let _external = self
.external_lock
.lock()
.unwrap_or_else(|e| e.into_inner());
download_texture(
&self.device,
&self.queue,
&entry.texture,
entry.format,
entry.width,
entry.height,
)
}
pub fn download_shared_texture(
&self,
texture: &wgpu::Texture,
format: wgpu::TextureFormat,
width: u32,
height: u32,
) -> Result<Frame> {
if self.device_lost() {
return Err(Error::Failed(
"wgpu device lost; texture download unavailable".into(),
));
}
let _external = self
.external_lock
.lock()
.unwrap_or_else(|e| e.into_inner());
download_texture(&self.device, &self.queue, texture, format, width, height)
}
pub fn download_async(&self, token: u64) -> Result<FrameReadback> {
if self.device_lost() {
return Err(Error::Failed(
"wgpu device lost; texture download unavailable".into(),
));
}
let entry = lock(&self.textures)
.get(&token)
.cloned()
.ok_or(Error::NotFound)?;
let guard = entry.keep_alive.clone();
let (tx, rx) = std::sync::mpsc::channel();
readback_worker()
.send(ReadbackJob {
device: self.device.clone(),
queue: self.queue.clone(),
texture: entry.texture,
format: entry.format,
width: entry.width,
height: entry.height,
external_lock: self.external_lock.clone(),
tx,
})
.map_err(|_| Error::Failed("readback worker stopped".into()))?;
Ok(FrameReadback { rx, _guard: guard })
}
pub fn blit(
&self,
src: u64,
dst: u64,
processor: Option<&crate::color::ColorProcessor>,
) -> Result<()> {
if processor.is_some() {
return Err(Error::Failed(
"color-managed GPU blit deferred: OCIO→WGSL generation not in this pass".into(),
));
}
let (src_tex, dst_tex) = {
let reg = lock(&self.textures);
let s = reg.get(&src).ok_or(Error::NotFound)?;
let d = reg.get(&dst).ok_or(Error::NotFound)?;
(s.texture.clone(), d.texture.clone())
};
let pipeline = {
let mut cache = lock(&self.blit);
if cache.is_none() {
*cache = Some(self.create_blit_pipeline()?);
}
cache.clone().unwrap()
};
let src_view = src_tex.create_view(&wgpu::TextureViewDescriptor::default());
let dst_view = dst_tex.create_view(&wgpu::TextureViewDescriptor::default());
let bind_group = self.device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("quercusrender-blit-bg"),
layout: &self.blit_bind_group_layout(),
entries: &[wgpu::BindGroupEntry {
binding: 0,
resource: wgpu::BindingResource::TextureView(&src_view),
}],
});
let mut encoder = self
.device
.create_command_encoder(&wgpu::CommandEncoderDescriptor {
label: Some("quercusrender-blit"),
});
{
let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some("quercusrender-blit-pass"),
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view: &dst_view,
depth_slice: None,
resolve_target: None,
ops: wgpu::Operations {
load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT),
store: wgpu::StoreOp::Store,
},
})],
depth_stencil_attachment: None,
timestamp_writes: None,
occlusion_query_set: None,
multiview_mask: None,
});
pass.set_pipeline(&pipeline);
pass.set_bind_group(0, &bind_group, &[]);
pass.draw(0..3, 0..1);
}
self.queue.submit(Some(encoder.finish()));
Ok(())
}
fn blit_bind_group_layout(&self) -> wgpu::BindGroupLayout {
self.device
.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("quercusrender-blit-layout"),
entries: &[wgpu::BindGroupLayoutEntry {
binding: 0,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Texture {
sample_type: wgpu::TextureSampleType::Float { filterable: false },
view_dimension: wgpu::TextureViewDimension::D2,
multisampled: false,
},
count: None,
}],
})
}
pub fn convert_to_rgba16f(&self, src: u64) -> Result<u64> {
let (src_tex, width, height) = {
let reg = lock(&self.textures);
let entry = reg.get(&src).ok_or(Error::NotFound)?;
(entry.texture.clone(), entry.width, entry.height)
};
let dst = self.create_texture_format(
width as i32,
height as i32,
1,
wgpu::TextureFormat::Rgba16Float,
wgpu::TextureUsages::TEXTURE_BINDING
| wgpu::TextureUsages::RENDER_ATTACHMENT
| wgpu::TextureUsages::COPY_DST
| wgpu::TextureUsages::COPY_SRC,
)?;
let dst_tex = {
let reg = lock(&self.textures);
reg.get(&dst).map(|e| e.texture.clone()).ok_or(Error::NotFound)?
};
let pipeline = {
let mut cache = lock(&self.blit16);
if cache.is_none() {
*cache = Some(self.create_blit_pipeline16()?);
}
cache.clone().unwrap()
};
let src_view = src_tex.create_view(&wgpu::TextureViewDescriptor::default());
let dst_view = dst_tex.create_view(&wgpu::TextureViewDescriptor::default());
let bind_group = self.device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("quercusrender-blit16-bg"),
layout: &self.blit_bind_group_layout(),
entries: &[wgpu::BindGroupEntry {
binding: 0,
resource: wgpu::BindingResource::TextureView(&src_view),
}],
});
let mut encoder = self.device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
label: Some("quercusrender-blit16"),
});
{
let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some("quercusrender-blit16-pass"),
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view: &dst_view,
depth_slice: None,
resolve_target: None,
ops: wgpu::Operations {
load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT),
store: wgpu::StoreOp::Store,
},
})],
depth_stencil_attachment: None,
timestamp_writes: None,
occlusion_query_set: None,
multiview_mask: None,
});
pass.set_pipeline(&pipeline);
pass.set_bind_group(0, &bind_group, &[]);
pass.draw(0..3, 0..1);
}
self.queue.submit(Some(encoder.finish()));
Ok(dst)
}
fn create_blit_pipeline16(&self) -> Result<wgpu::RenderPipeline> {
let shader = self.device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some("quercusrender-blit16-shader"),
source: wgpu::ShaderSource::Wgsl(std::borrow::Cow::Borrowed(BLIT_WGSL)),
});
let layout = self.blit_bind_group_layout();
let pipeline_layout = self.device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("quercusrender-blit16-layout"),
bind_group_layouts: &[Some(&layout)],
immediate_size: 0,
});
Ok(self.device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: Some("quercusrender-blit16"),
layout: Some(&pipeline_layout),
vertex: wgpu::VertexState {
module: &shader,
entry_point: Some("vs_main"),
compilation_options: Default::default(),
buffers: &[],
},
primitive: wgpu::PrimitiveState::default(),
depth_stencil: None,
multisample: wgpu::MultisampleState::default(),
fragment: Some(wgpu::FragmentState {
module: &shader,
entry_point: Some("fs_main"),
compilation_options: Default::default(),
targets: &[Some(wgpu::ColorTargetState {
format: wgpu::TextureFormat::Rgba16Float,
blend: None,
write_mask: wgpu::ColorWrites::ALL,
})],
}),
multiview_mask: None,
cache: None,
}))
}
fn create_blit_pipeline(&self) -> Result<wgpu::RenderPipeline> {
let shader = self
.device
.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some("quercusrender-blit-shader"),
source: wgpu::ShaderSource::Wgsl(std::borrow::Cow::Borrowed(BLIT_WGSL)),
});
let layout = self.blit_bind_group_layout();
let pipeline_layout = self
.device
.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("quercusrender-blit-layout"),
bind_group_layouts: &[Some(&layout)],
immediate_size: 0,
});
let pipeline = self
.device
.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: Some("quercusrender-blit"),
layout: Some(&pipeline_layout),
vertex: wgpu::VertexState {
module: &shader,
entry_point: Some("vs_main"),
compilation_options: Default::default(),
buffers: &[],
},
primitive: wgpu::PrimitiveState::default(),
depth_stencil: None,
multisample: wgpu::MultisampleState::default(),
fragment: Some(wgpu::FragmentState {
module: &shader,
entry_point: Some("fs_main"),
compilation_options: Default::default(),
targets: &[Some(wgpu::ColorTargetState {
format: wgpu::TextureFormat::Rgba32Float,
blend: None,
write_mask: wgpu::ColorWrites::ALL,
})],
}),
multiview_mask: None,
cache: None,
});
Ok(pipeline)
}
pub fn shared() -> Option<Arc<GpuContext>> {
let mut slot = lock(shared_slot());
if !slot.decided {
slot.ctx = Self::create(BackendKind::from_user_config());
slot.decided = true;
}
slot.ctx.clone()
}
pub fn install_shared(ctx: Option<Arc<GpuContext>>) -> bool {
let mut slot = lock(shared_slot());
if slot.decided {
let replaceable = ctx.is_some()
&& slot
.ctx
.as_ref()
.is_some_and(|c| !c.is_adopted() && !c.is_used());
if !replaceable {
return false;
}
}
slot.host_installed = ctx.is_some();
slot.ctx = ctx;
slot.decided = true;
true
}
pub fn host_gpu_installed() -> bool {
lock(shared_slot()).host_installed
}
pub fn mark_host_context() -> bool {
let mut slot = lock(shared_slot());
if !slot.decided {
slot.ctx = Self::create(BackendKind::from_user_config());
slot.decided = true;
}
slot.host_installed = slot.ctx.is_some();
slot.ctx.is_some()
}
pub fn shared_is_installed() -> bool {
let slot = lock(shared_slot());
slot.decided && slot.ctx.is_some()
}
pub fn is_filterable(&self) -> bool {
self.filterable
}
pub fn placeholder_texture(&self) -> Result<u64> {
let mut slot = lock(&self.placeholder);
if let Some(token) = *slot {
return Ok(token);
}
let token = self.create_texture(1, 1)?;
let mut pod = VideoParamsPod::default();
pod.width = 1;
pod.height = 1;
pod.format = PixelFormat::F32 as i32;
let mut frame = Frame::new();
frame.set_video_params(pod);
frame.allocate();
self.upload(token, &frame)?;
*slot = Some(token);
Ok(token)
}
pub fn compile_shader_pass(
&self,
key: &str,
wgsl: &str,
texture_count: u32,
has_uniforms: bool,
filtering: bool,
) -> Result<Arc<ShaderProgram>> {
self.used.store(true, Ordering::Release);
if let Some(p) = lock(&self.programs).get(key) {
return Ok(p.clone());
}
let mut entries = Vec::new();
if has_uniforms {
entries.push(wgpu::BindGroupLayoutEntry {
binding: 0,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Uniform,
has_dynamic_offset: false,
min_binding_size: None,
},
count: None,
});
}
let sample_type = wgpu::TextureSampleType::Float {
filterable: self.filterable && filtering,
};
for i in 0..texture_count {
entries.push(wgpu::BindGroupLayoutEntry {
binding: 1 + 2 * i,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Texture {
sample_type,
view_dimension: wgpu::TextureViewDimension::D2,
multisampled: false,
},
count: None,
});
entries.push(wgpu::BindGroupLayoutEntry {
binding: 2 + 2 * i,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Sampler(if self.filterable && filtering {
wgpu::SamplerBindingType::Filtering
} else {
wgpu::SamplerBindingType::NonFiltering
}),
count: None,
});
}
let layout = self
.device
.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("quercusrender-fx-layout"),
entries: &entries,
});
let vs_module = self
.device
.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some("quercusrender-fx-vs"),
source: wgpu::ShaderSource::Wgsl(std::borrow::Cow::Borrowed(EFFECT_VS_WGSL)),
});
let fs_module = self
.device
.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some("quercusrender-fx-fs"),
source: wgpu::ShaderSource::Wgsl(std::borrow::Cow::Owned(wgsl.to_string())),
});
let pipeline_layout = self
.device
.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("quercusrender-fx-pipeline-layout"),
bind_group_layouts: &[Some(&layout)],
immediate_size: 0,
});
let scope = self.device.push_error_scope(wgpu::ErrorFilter::Validation);
let pipeline = self
.device
.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: Some("quercusrender-fx"),
layout: Some(&pipeline_layout),
vertex: wgpu::VertexState {
module: &vs_module,
entry_point: Some("vs_main"),
compilation_options: Default::default(),
buffers: &[],
},
primitive: wgpu::PrimitiveState::default(),
depth_stencil: None,
multisample: wgpu::MultisampleState::default(),
fragment: Some(wgpu::FragmentState {
module: &fs_module,
entry_point: Some("main"),
compilation_options: Default::default(),
targets: &[Some(wgpu::ColorTargetState {
format: wgpu::TextureFormat::Rgba32Float,
blend: None,
write_mask: wgpu::ColorWrites::ALL,
})],
}),
multiview_mask: None,
cache: None,
});
if let Some(err) = pollster_block_on(scope.pop()) {
return Err(Error::Failed(format!(
"effect pipeline validation failed: {err}"
)));
}
let program = Arc::new(ShaderProgram {
pipeline,
layout,
texture_count,
has_uniforms,
filtering,
});
lock(&self.programs).insert(key.to_string(), program.clone());
Ok(program)
}
pub fn run_shader_pass(
&self,
program: &ShaderProgram,
uniforms: &[u8],
textures: &[u64],
dst: u64,
) -> Result<()> {
if textures.len() != program.texture_count as usize {
return Err(Error::Invalid);
}
let _external = self
.external_lock
.lock()
.unwrap_or_else(|e| e.into_inner());
let dst_tex = lock(&self.textures)
.get(&dst)
.cloned()
.ok_or(Error::NotFound)?;
let uniform_buffer = if program.has_uniforms {
let size = uniforms.len().max(16) as u64;
let buffer = uniform_buffer_from_bytes(
&self.device,
"quercusrender-fx-uniforms",
uniforms,
size,
)?;
Some(buffer)
} else {
None
};
let sampler = self.device.create_sampler(&wgpu::SamplerDescriptor {
label: Some("quercusrender-fx-sampler"),
mag_filter: if self.filterable && program.filtering {
wgpu::FilterMode::Linear
} else {
wgpu::FilterMode::Nearest
},
min_filter: if self.filterable && program.filtering {
wgpu::FilterMode::Linear
} else {
wgpu::FilterMode::Nearest
},
address_mode_u: wgpu::AddressMode::ClampToEdge,
address_mode_v: wgpu::AddressMode::ClampToEdge,
..Default::default()
});
let mut bg_entries: Vec<wgpu::BindGroupEntry> = Vec::new();
if let Some(buffer) = &uniform_buffer {
bg_entries.push(wgpu::BindGroupEntry {
binding: 0,
resource: buffer.as_entire_binding(),
});
}
let views: Vec<wgpu::TextureView> = textures
.iter()
.map(|t| {
let reg = lock(&self.textures);
let tex = reg.get(t).ok_or(Error::NotFound)?;
Ok(tex
.texture
.create_view(&wgpu::TextureViewDescriptor::default()))
})
.collect::<Result<Vec<_>>>()?;
for (i, view) in views.iter().enumerate() {
bg_entries.push(wgpu::BindGroupEntry {
binding: 1 + 2 * i as u32,
resource: wgpu::BindingResource::TextureView(view),
});
bg_entries.push(wgpu::BindGroupEntry {
binding: 2 + 2 * i as u32,
resource: wgpu::BindingResource::Sampler(&sampler),
});
}
let bind_group = self.device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("quercusrender-fx-bg"),
layout: &program.layout,
entries: &bg_entries,
});
let dst_view = dst_tex
.texture
.create_view(&wgpu::TextureViewDescriptor::default());
let mut encoder = self
.device
.create_command_encoder(&wgpu::CommandEncoderDescriptor {
label: Some("quercusrender-fx"),
});
{
let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some("quercusrender-fx-pass"),
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view: &dst_view,
depth_slice: None,
resolve_target: None,
ops: wgpu::Operations {
load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT),
store: wgpu::StoreOp::Store,
},
})],
depth_stencil_attachment: None,
timestamp_writes: None,
occlusion_query_set: None,
multiview_mask: None,
});
pass.set_pipeline(&program.pipeline);
pass.set_bind_group(0, &bind_group, &[]);
pass.draw(0..3, 0..1);
}
self.queue.submit(Some(encoder.finish()));
Ok(())
}
}
pub struct ShaderProgram {
pipeline: wgpu::RenderPipeline,
layout: wgpu::BindGroupLayout,
pub texture_count: u32,
pub has_uniforms: bool,
pub filtering: bool,
}
#[derive(Clone)]
struct PresentPipeline {
pipeline: wgpu::RenderPipeline,
layout: wgpu::BindGroupLayout,
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct YuvTransform {
pub matrix: [[f32; 3]; 3],
pub offset: [f32; 3],
}
impl YuvTransform {
pub fn from_matrix(matrix: crate::colormath::YuvMatrix, full_range: bool) -> Self {
Self::from_matrix_depth(matrix, full_range, 16)
}
pub fn from_matrix_depth(
matrix: crate::colormath::YuvMatrix,
full_range: bool,
bit_depth: u32,
) -> Self {
let (kr, kb) = matrix.kr_kb();
let kg = 1.0 - kr - kb;
let max = ((1u32 << bit_depth) - 1) as f32;
let scale = if bit_depth > 8 {
1u32 << (bit_depth - 8)
} else {
1
};
let (y_black, y_white, c_mid, c_span) = if full_range {
(0.0, max, (1u32 << (bit_depth - 1)) as f32, max)
} else {
(
(16 * scale) as f32,
(235 * scale) as f32,
(128 * scale) as f32,
(224 * scale) as f32,
)
};
let ay = max / (y_white - y_black);
let by = y_black / (y_white - y_black);
let ac = max / c_span;
let bc = c_mid / c_span;
let rv = 2.0 * (1.0 - kr) * ac;
let bu = 2.0 * (1.0 - kb) * ac;
let gu = -(2.0 * kb * (1.0 - kb) / kg) * ac;
let gv = -(2.0 * kr * (1.0 - kr) / kg) * ac;
Self {
matrix: [[ay, 0.0, rv], [ay, gu, gv], [ay, bu, 0.0]],
offset: [
-by - 2.0 * (1.0 - kr) * bc,
-by + (2.0 * kr * (1.0 - kr) + 2.0 * kb * (1.0 - kb)) / kg * bc,
-by - 2.0 * (1.0 - kb) * bc,
],
}
}
pub fn bt601_limited() -> Self {
Self::from_matrix(crate::colormath::YuvMatrix::Bt601, false)
}
pub fn bt601_full() -> Self {
Self::from_matrix(crate::colormath::YuvMatrix::Bt601, true)
}
pub fn bt709_limited() -> Self {
Self::from_matrix(crate::colormath::YuvMatrix::Bt709, false)
}
pub fn bt709_full() -> Self {
Self::from_matrix(crate::colormath::YuvMatrix::Bt709, true)
}
pub fn bt2020_limited() -> Self {
Self::from_matrix(crate::colormath::YuvMatrix::Bt2020, false)
}
pub fn bt2020_full() -> Self {
Self::from_matrix(crate::colormath::YuvMatrix::Bt2020, true)
}
}
fn install_device_lost_logging(device: &wgpu::Device, device_lost: Arc<AtomicBool>) {
device.set_device_lost_callback(move |reason, message| {
eprintln!("[wgpu] device lost: reason={reason:?} message={message}");
device_lost.store(true, Ordering::Release);
});
}
fn uniform_buffer_from_bytes(
device: &wgpu::Device,
label: &str,
data: &[u8],
size: u64,
) -> Result<wgpu::Buffer> {
let validation_scope = device.push_error_scope(wgpu::ErrorFilter::Validation);
let oom_scope = device.push_error_scope(wgpu::ErrorFilter::OutOfMemory);
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
let buffer = device.create_buffer(&wgpu::BufferDescriptor {
label: Some(label),
size,
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: true,
});
{
let mut view = buffer.slice(..).get_mapped_range_mut();
let n = data.len().min(view.len());
view.slice(..n).copy_from_slice(&data[..n]);
}
buffer.unmap();
buffer
}));
let oom_error = pollster_block_on(oom_scope.pop());
let validation_error = pollster_block_on(validation_scope.pop());
if let Some(error) = oom_error.or(validation_error) {
eprintln!("[wgpu] create buffer {label} failed: {error}");
return Err(Error::Failed(format!(
"create planar uniform failed: {error}"
)));
}
result.map_err(|_| {
Error::Failed("wgpu device lost creating planar uniforms".into())
})
}
fn f32_uniform_bytes(values: &[f32]) -> Vec<u8> {
let mut out = Vec::with_capacity(values.len() * 4);
for v in values {
out.extend_from_slice(&v.to_le_bytes());
}
out
}
const PRESENT_WGSL: &str = r#"
struct Params {
edge: vec4<f32>,
lo: vec4<f32>,
hi: vec4<f32>,
};
@group(0) @binding(0) var src_tex: texture_2d<f32>;
@group(0) @binding(1) var lut_tex: texture_3d<f32>;
@group(0) @binding(2) var<uniform> p: Params;
fn lut_at(x: i32, y: i32, z: i32) -> vec3<f32> {
return textureLoad(lut_tex, vec3<i32>(x, y, z), 0).rgb;
}
@fragment
fn main(@builtin(position) frag: vec4<f32>) -> @location(0) vec4<f32> {
let dims = textureDimensions(src_tex);
let coord = clamp(
vec2<i32>(i32(frag.x), i32(frag.y)),
vec2<i32>(0, 0),
vec2<i32>(i32(dims.x), i32(dims.y)) - vec2<i32>(1, 1),
);
let c = textureLoad(src_tex, coord, 0);
let n = i32(p.edge.x);
let last = f32(n - 1);
let span = p.hi.xyz - p.lo.xyz;
let u = (c.xyz - p.lo.xyz) / max(span, vec3<f32>(1e-12));
let t = clamp(u, vec3<f32>(0.0), vec3<f32>(1.0)) * last;
let i0 = vec3<i32>(floor(t));
let f = t - floor(t);
let i1 = min(i0 + vec3<i32>(1, 1, 1), vec3<i32>(n - 1));
let c000 = lut_at(i0.x, i0.y, i0.z);
let c100 = lut_at(i1.x, i0.y, i0.z);
let c010 = lut_at(i0.x, i1.y, i0.z);
let c110 = lut_at(i1.x, i1.y, i0.z);
let c001 = lut_at(i0.x, i0.y, i1.z);
let c101 = lut_at(i1.x, i0.y, i1.z);
let c011 = lut_at(i0.x, i1.y, i1.z);
let c111 = lut_at(i1.x, i1.y, i1.z);
let c00 = mix(c000, c100, f.x);
let c10 = mix(c010, c110, f.x);
let c01 = mix(c001, c101, f.x);
let c11 = mix(c011, c111, f.x);
let c0 = mix(c00, c10, f.y);
let c1 = mix(c01, c11, f.y);
let out = mix(c0, c1, f.z);
return vec4<f32>(out, c.a);
}
"#;
const YUV_WGSL: &str = r#"
struct Params {
m0: vec4<f32>,
m1: vec4<f32>,
m2: vec4<f32>,
uv_scale: vec4<f32>,
};
@group(0) @binding(0) var y_tex: texture_2d<f32>;
@group(0) @binding(1) var u_tex: texture_2d<f32>;
@group(0) @binding(2) var v_tex: texture_2d<f32>;
@group(0) @binding(3) var<uniform> p: Params;
@fragment
fn main(@builtin(position) frag: vec4<f32>) -> @location(0) vec4<f32> {
let yd = textureDimensions(y_tex);
let coord = clamp(
vec2<i32>(i32(frag.x), i32(frag.y)),
vec2<i32>(0, 0),
vec2<i32>(i32(yd.x), i32(yd.y)) - vec2<i32>(1, 1),
);
let yv = textureLoad(y_tex, coord, 0).r;
let ud = textureDimensions(u_tex);
let uc = clamp(
vec2<i32>(vec2<f32>(coord) * p.uv_scale.xy),
vec2<i32>(0, 0),
vec2<i32>(i32(ud.x), i32(ud.y)) - vec2<i32>(1, 1),
);
let uv = textureLoad(u_tex, uc, 0).r;
let vv = textureLoad(v_tex, uc, 0).r;
let yuv = vec3<f32>(yv, uv, vv);
let rgb = vec3<f32>(
dot(p.m0.xyz, yuv) + p.m0.w,
dot(p.m1.xyz, yuv) + p.m1.w,
dot(p.m2.xyz, yuv) + p.m2.w,
);
return vec4<f32>(rgb, 1.0);
}
"#;
const YUV_PLANAR_WGSL: &str = r#"
struct Params {
m0: vec4<f32>,
m1: vec4<f32>,
m2: vec4<f32>,
uv_scale: vec4<f32>,
};
@group(0) @binding(0) var y_tex: texture_2d<f32>;
@group(0) @binding(1) var uv_tex: texture_2d<f32>;
@group(0) @binding(2) var<uniform> p: Params;
@fragment
fn main(@builtin(position) frag: vec4<f32>) -> @location(0) vec4<f32> {
let yd = textureDimensions(y_tex);
let coord = clamp(
vec2<i32>(i32(frag.x), i32(frag.y)),
vec2<i32>(0, 0),
vec2<i32>(i32(yd.x), i32(yd.y)) - vec2<i32>(1, 1),
);
let yv = textureLoad(y_tex, coord, 0).r;
let ud = textureDimensions(uv_tex);
let uc = clamp(
vec2<i32>(vec2<f32>(coord) * p.uv_scale.xy),
vec2<i32>(0, 0),
vec2<i32>(i32(ud.x), i32(ud.y)) - vec2<i32>(1, 1),
);
let uv = textureLoad(uv_tex, uc, 0);
let yuv = vec3<f32>(yv, uv.r, uv.g);
let rgb = vec3<f32>(
dot(p.m0.xyz, yuv) + p.m0.w,
dot(p.m1.xyz, yuv) + p.m1.w,
dot(p.m2.xyz, yuv) + p.m2.w,
);
return vec4<f32>(rgb, 1.0);
}
"#;
const YUV_PLANAR_MATRIX_WGSL: &str = r#"
struct Params {
m0: vec4<f32>,
m1: vec4<f32>,
m2: vec4<f32>,
// x,y = chroma/luma dimension ratio; z = model (0 planar, 1
// semi-planar, 2 rgb, 3 rgba); w unused.
uv_scale_mode: vec4<f32>,
// x,y = source luma size / destination size.
src_scale: vec4<f32>,
// x,y = visible luma size; z,w = nominal (ceil-subsampled) chroma
// size. Sampling is clamped to these, not to the (possibly padded)
// texture allocation.
src_size: vec4<f32>,
};
@group(0) @binding(0) var plane0: texture_2d<f32>;
@group(0) @binding(1) var plane1: texture_2d<f32>;
@group(0) @binding(2) var plane2: texture_2d<f32>;
@group(0) @binding(3) var<uniform> p: Params;
fn sample_bilinear(tex: texture_2d<f32>, pos: vec2<f32>, limit: vec2<f32>) -> vec4<f32> {
// Clamp to the visible plane extent, then to the allocation (hardware
// planes may be padded beyond the visible region).
let dims = textureDimensions(tex);
let hi = min(limit, vec2<f32>(dims) - vec2<f32>(1.0));
let clamped = clamp(pos, vec2<f32>(0.0), hi);
let base = vec2<i32>(floor(clamped));
let f = clamped - vec2<f32>(base);
let x1 = min(base.x + 1, i32(hi.x));
let y1 = min(base.y + 1, i32(hi.y));
let c00 = textureLoad(tex, base, 0);
let c10 = textureLoad(tex, vec2<i32>(x1, base.y), 0);
let c01 = textureLoad(tex, vec2<i32>(base.x, y1), 0);
let c11 = textureLoad(tex, vec2<i32>(x1, y1), 0);
let top = mix(c00, c10, f.x);
let bottom = mix(c01, c11, f.x);
return mix(top, bottom, f.y);
}
@fragment
fn main(@builtin(position) frag: vec4<f32>) -> @location(0) vec4<f32> {
let mode = i32(p.uv_scale_mode.z + 0.5);
// Destination pixel center -> source luma texel coordinate.
let y_pos = frag.xy * p.src_scale.xy - vec2<f32>(0.5);
let y_limit = vec2<f32>(p.src_size.x, p.src_size.y) - vec2<f32>(1.0);
// At 1:1, use exact texel loads: bilinear on a subsampled UV plane
// would blend neighbouring chroma samples that the CPU/native path
// does not blend. Scaled output keeps the 4-tap bilinear.
let native = p.src_scale.x == 1.0 && p.src_scale.y == 1.0;
if (mode >= 2) {
var c: vec4<f32>;
if (native) {
c = textureLoad(plane0, vec2<i32>(frag.xy), 0);
} else {
c = sample_bilinear(plane0, y_pos, y_limit);
}
if (mode == 3) {
return vec4<f32>(c.rgb, c.a);
}
return vec4<f32>(c.rgb, 1.0);
}
var yv: f32;
if (native) {
yv = textureLoad(plane0, vec2<i32>(frag.xy), 0).r;
} else {
yv = sample_bilinear(plane0, y_pos, y_limit).r;
}
let c_pos = frag.xy * (p.src_scale.xy * p.uv_scale_mode.xy) - vec2<f32>(0.5);
let c_limit = vec2<f32>(p.src_size.z, p.src_size.w) - vec2<f32>(1.0);
let c_native = vec2<i32>(frag.xy * p.uv_scale_mode.xy);
var u: f32;
var v: f32;
if (mode == 0) {
if (native) {
u = textureLoad(plane1, c_native, 0).r;
v = textureLoad(plane2, c_native, 0).r;
} else {
u = sample_bilinear(plane1, c_pos, c_limit).r;
v = sample_bilinear(plane2, c_pos, c_limit).r;
}
} else {
var uv: vec4<f32>;
if (native) {
uv = textureLoad(plane1, c_native, 0);
} else {
uv = sample_bilinear(plane1, c_pos, c_limit);
}
u = uv.r;
v = uv.g;
}
let yuv = vec3<f32>(yv, u, v);
let rgb = vec3<f32>(
dot(p.m0.xyz, yuv) + p.m0.w,
dot(p.m1.xyz, yuv) + p.m1.w,
dot(p.m2.xyz, yuv) + p.m2.w,
);
return vec4<f32>(rgb, 1.0);
}
"#;
const EFFECT_VS_WGSL: &str = r#"
struct VsOut {
@builtin(position) pos: vec4<f32>,
@location(0) uv: vec2<f32>,
};
@vertex
fn vs_main(@builtin(vertex_index) vi: u32) -> VsOut {
var pos = array<vec2<f32>, 3>(
vec2<f32>(-1.0, -1.0),
vec2<f32>(3.0, -1.0),
vec2<f32>(-1.0, 3.0),
);
let p = pos[vi];
return VsOut(vec4<f32>(p, 0.0, 1.0), vec2<f32>(0.5 + 0.5 * p.x, 0.5 - 0.5 * p.y));
}
"#;
impl GpuContextLike for GpuContext {
fn kind(&self) -> BackendKind {
self.kind()
}
fn destroy_texture(&self, token: u64) {
self.destroy_texture(token);
}
fn upload(&self, token: u64, frame: &Frame) -> Result<()> {
self.upload(token, frame)
}
fn download(&self, token: u64) -> Result<Frame> {
self.download(token)
}
fn download_async(&self, token: u64) -> Result<FrameReadback> {
self.download_async(token)
}
fn blit(
&self,
src: u64,
dst: u64,
processor: Option<&crate::color::ColorProcessor>,
) -> Result<()> {
self.blit(src, dst, processor)
}
fn as_any(&self) -> Option<&dyn std::any::Any> {
Some(self)
}
fn texture_handle(&self, token: u64) -> Option<Arc<wgpu::Texture>> {
self.texture_handle(token)
}
}
const BLIT_WGSL: &str = r#"
@group(0) @binding(0) var src_tex: texture_2d<f32>;
@vertex
fn vs_main(@builtin(vertex_index) vi: u32) -> @builtin(position) vec4<f32> {
var pos = array<vec2<f32>, 3>(
vec2<f32>(-1.0, -1.0),
vec2<f32>(3.0, -1.0),
vec2<f32>(-1.0, 3.0),
);
return vec4<f32>(pos[vi], 0.0, 1.0);
}
@fragment
fn fs_main(@builtin(position) frag: vec4<f32>) -> @location(0) vec4<f32> {
let dims = textureDimensions(src_tex);
let coord = clamp(
vec2<u32>(u32(i32(frag.x)), u32(i32(frag.y))),
vec2<u32>(0u, 0u),
dims - vec2<u32>(1u, 1u),
);
return textureLoad(src_tex, coord, 0);
}
"#;
fn pollster_block_on<F: std::future::Future>(future: F) -> F::Output {
futures_executor::block_on(future)
}
mod futures_executor {
use std::future::Future;
use std::pin::pin;
use std::task::{Context, Poll, RawWaker, RawWakerVTable, Waker};
fn noop_raw_waker() -> RawWaker {
fn no_op(_: *const ()) {}
fn clone(_: *const ()) -> RawWaker {
noop_raw_waker()
}
static VTABLE: RawWakerVTable = RawWakerVTable::new(clone, no_op, no_op, no_op);
RawWaker::new(std::ptr::null(), &VTABLE)
}
fn noop_waker() -> Waker {
unsafe { Waker::from_raw(noop_raw_waker()) }
}
pub fn block_on<F: Future>(future: F) -> F::Output {
let mut future = pin!(future);
let waker = noop_waker();
let mut cx = Context::from_waker(&waker);
loop {
match future.as_mut().poll(&mut cx) {
Poll::Ready(out) => return out,
Poll::Pending => std::thread::yield_now(),
}
}
}
}
pub struct DisplayRenderer {
ctx: Option<Arc<GpuContext>>,
backend: BackendKind,
}
impl DisplayRenderer {
pub fn new(backend: BackendKind) -> Self {
Self { ctx: None, backend }
}
pub fn init(&mut self, gl_context: *mut std::ffi::c_void) -> Result<()> {
if !gl_context.is_null() {
return Err(Error::Invalid);
}
self.ctx = if self.backend == BackendKind::from_user_config() {
GpuContext::shared()
} else {
GpuContext::create(self.backend)
};
if self.ctx.is_none() {
return Err(Error::Failed("no GPU adapter available".into()));
}
Ok(())
}
pub fn backend(&self) -> BackendKind {
self.backend
}
pub fn context(&self) -> Option<&Arc<GpuContext>> {
self.ctx.as_ref()
}
pub fn is_open_gl(&self) -> bool {
self.backend == BackendKind::Gl
}
pub fn is_vulkan(&self) -> bool {
self.backend == BackendKind::Vulkan
}
pub fn is_initialized(&self) -> bool {
self.ctx.is_some()
}
pub fn create_texture(
&self,
params: &VideoParamsPod,
pixels: Option<(*const u8, usize)>,
) -> Result<Texture> {
let width = params.effective_width();
let height = params.effective_height();
if width <= 0 || height <= 0 {
return Err(Error::Invalid);
}
if let Some(ctx) = &self.ctx {
let token = ctx.create_texture(width, height)?;
if let Some((ptr, linesize)) = pixels {
let mut frame = Frame::new();
let mut pod = *params;
pod.width = width;
pod.height = height;
pod.format = PixelFormat::F32 as i32;
frame.set_video_params(pod);
let frame_linesize = frame.linesize_bytes();
if linesize != frame_linesize {
ctx.destroy_texture(token);
return Err(Error::Invalid);
}
frame.data = unsafe {
std::slice::from_raw_parts(ptr, frame_linesize * height as usize).to_vec()
};
ctx.upload(token, &frame)?;
}
Ok(Texture::gpu(
ctx.clone(),
token,
width,
height,
PixelFormat::F32,
))
} else {
let mut frame = Frame::new();
let mut pod = *params;
pod.width = width;
pod.height = height;
pod.format = PixelFormat::F32 as i32;
frame.set_video_params(pod);
frame.allocate();
if let Some((ptr, linesize)) = pixels {
let frame_linesize = frame.linesize_bytes();
if linesize != frame_linesize {
return Err(Error::Invalid);
}
frame.data = unsafe {
std::slice::from_raw_parts(ptr, frame_linesize * height as usize).to_vec()
};
}
Ok(Texture::wrap_frame(frame))
}
}
pub unsafe fn upload_texture(
&self,
texture: &mut Texture,
pixels: *const u8,
linesize: usize,
) -> Result<()> {
let size = texture.size();
match texture {
Texture::Gpu { token, ctx, .. } => {
let frame = unsafe { frame_from_pixels_for_upload(size, pixels, linesize) }?;
ctx.upload(*token, &frame)
}
Texture::Planar(_) => Err(Error::State),
Texture::Cpu(frame) => {
let stride = frame.linesize_bytes();
if linesize != stride {
return Err(Error::Invalid);
}
let h = frame.height as usize;
frame
.data
.copy_from_slice(unsafe { std::slice::from_raw_parts(pixels, stride * h) });
Ok(())
}
}
}
pub unsafe fn download_texture(
&self,
texture: &Texture,
dst: *mut u8,
linesize: usize,
) -> Result<()> {
let frame = texture.to_frame()?;
let stride = frame.linesize_bytes();
if linesize != stride {
return Err(Error::Invalid);
}
let h = frame.height as usize;
unsafe {
std::ptr::copy_nonoverlapping(frame.data.as_ptr(), dst, stride * h);
}
Ok(())
}
pub fn blit_color_managed(
&self,
src: Option<&Texture>,
dst: &mut Texture,
processor: Option<&crate::color::ColorProcessor>,
) -> Result<()> {
match (src, dst) {
(
Some(Texture::Gpu { token: s, ctx, .. }),
Texture::Gpu {
token: d,
ctx: dctx,
..
},
) if Arc::ptr_eq(ctx, dctx) => ctx.blit(*s, *d, processor),
(Some(Texture::Cpu(sf)), Texture::Cpu(df)) => {
if sf.width != df.width || sf.height != df.height {
return Err(Error::Invalid);
}
df.data.clear();
df.data.extend_from_slice(&sf.data);
if let Some(p) = processor {
p.convert_frame(df)?;
}
Ok(())
}
_ => Err(Error::Failed(
"mixed CPU/GPU texture blit unsupported".into(),
)),
}
}
pub unsafe fn download_from_texture(
&self,
texture_id: i32,
params: &VideoParamsPod,
dst: *mut u8,
linesize: usize,
) -> Result<()> {
let ctx = self
.ctx
.as_ref()
.ok_or(Error::Failed("no GPU context".into()))?;
let frame = ctx.download(texture_id as u64)?;
let want_linesize = params.effective_width() as usize * 4 * 4;
if linesize != want_linesize {
return Err(Error::Invalid);
}
let h = frame.height as usize;
unsafe {
std::ptr::copy_nonoverlapping(frame.data.as_ptr(), dst, want_linesize * h);
}
Ok(())
}
}
pub fn texture_id_of(t: &Texture) -> i32 {
match t {
Texture::Gpu { token, .. } => *token as i32,
Texture::Planar(_) | Texture::Cpu(_) => 0,
}
}
pub unsafe fn frame_from_pixels_for_upload(
size: (i32, i32),
pixels: *const u8,
linesize: usize,
) -> Result<Frame> {
let (w, h) = size;
if w <= 0 || h <= 0 || pixels.is_null() {
return Err(Error::Invalid);
}
let mut frame = Frame::new();
let mut pod = VideoParamsPod::default();
pod.width = w;
pod.height = h;
pod.format = PixelFormat::F32 as i32;
frame.set_video_params(pod);
let stride = frame.linesize_bytes();
if linesize != stride {
return Err(Error::Invalid);
}
frame.data = unsafe { std::slice::from_raw_parts(pixels, stride * h as usize).to_vec() };
Ok(frame)
}
#[cfg(test)]
mod tests {
use super::*;
static GPU_COUNTER_LOCK: Mutex<()> = Mutex::new(());
#[test]
fn backend_string_roundtrip() {
for (s, kind) in [
("auto", BackendKind::Auto),
("metal", BackendKind::Metal),
("vulkan", BackendKind::Vulkan),
("opengl", BackendKind::Gl),
("gl", BackendKind::Gl),
("cpu", BackendKind::Cpu),
] {
assert_eq!(BackendKind::from_config_string(s), kind);
assert_eq!(BackendKind::from_config_string(&s.to_uppercase()), kind);
}
assert_eq!(BackendKind::from_config_string("dummy"), BackendKind::Cpu);
assert_eq!(
BackendKind::from_config_string("multiprocess"),
BackendKind::Cpu
);
assert_eq!(BackendKind::from_config_string("bogus"), BackendKind::Auto);
assert_eq!(BackendKind::from_config_string(""), BackendKind::Auto);
assert_eq!(
BackendKind::from_config_string(BackendKind::Metal.to_config_string()),
BackendKind::Metal
);
}
struct EnvVarGuard {
key: &'static str,
saved: Option<std::ffi::OsString>,
}
impl EnvVarGuard {
fn set(key: &'static str, value: &str) -> Self {
let saved = std::env::var_os(key);
std::env::set_var(key, value);
Self { key, saved }
}
}
impl Drop for EnvVarGuard {
fn drop(&mut self) {
match self.saved.take() {
Some(v) => std::env::set_var(self.key, v),
None => std::env::remove_var(self.key),
}
}
}
#[test]
fn user_config_env_override() {
let _guard = GPU_COUNTER_LOCK.lock().unwrap_or_else(|e| e.into_inner());
let _env = EnvVarGuard::set("QUERCUS_RENDER_BACKEND", "vulkan");
assert_eq!(BackendKind::from_user_config(), BackendKind::Vulkan);
}
#[test]
fn display_bit_depth_string_roundtrip() {
assert_eq!(
DisplayBitDepth::from_config_string("8"),
DisplayBitDepth::Bit8
);
assert_eq!(
DisplayBitDepth::from_config_string("10"),
DisplayBitDepth::Bit10
);
assert_eq!(
DisplayBitDepth::from_config_string(""),
DisplayBitDepth::Bit10
);
assert_eq!(
DisplayBitDepth::from_config_string("bogus"),
DisplayBitDepth::Bit10
);
assert_eq!(
DisplayBitDepth::from_config_string(DisplayBitDepth::Bit8.to_config_string()),
DisplayBitDepth::Bit8
);
assert_eq!(
DisplayBitDepth::from_config_string(DisplayBitDepth::Bit10.to_config_string()),
DisplayBitDepth::Bit10
);
}
#[test]
fn display_bit_depth_present_formats() {
assert_eq!(
DisplayBitDepth::Bit10.present_formats(),
&[
wgpu::TextureFormat::Rgb10a2Unorm,
wgpu::TextureFormat::Rgba16Float
]
);
assert_eq!(
DisplayBitDepth::Bit8.present_formats(),
&[
wgpu::TextureFormat::Bgra8Unorm,
wgpu::TextureFormat::Rgba8Unorm
]
);
}
#[test]
fn cpu_backend_has_no_adapter() {
assert!(GpuContext::create(BackendKind::Cpu).is_none());
}
#[test]
fn shared_slot_replaces_an_unused_engine_context() {
let _guard = GPU_COUNTER_LOCK.lock().unwrap_or_else(|e| e.into_inner());
let Some(base) = any_gpu() else {
return;
};
assert!(
GpuContext::install_shared(Some(base.clone())),
"the slot starts undecided"
);
assert!(!GpuContext::shared().unwrap().is_adopted());
let (device, queue) = base.device_queue();
let adopted = GpuContext::adopt(device, queue, BackendKind::Auto);
assert!(
GpuContext::install_shared(Some(adopted.clone())),
"an unused engine context is replaceable by the UI device"
);
assert!(
GpuContext::shared().unwrap().is_adopted(),
"the adopted context is now shared"
);
let _texture = adopted.create_texture(2, 2).unwrap();
let (device, queue) = adopted.device_queue();
let second = GpuContext::adopt(device, queue, BackendKind::Auto);
assert!(
!GpuContext::install_shared(Some(second)),
"a used context is not replaceable"
);
assert!(GpuContext::shared().unwrap().is_adopted());
}
fn any_gpu() -> Option<Arc<GpuContext>> {
gpu_or_skip("a backend GPU test")
}
#[test]
fn gpu_texture_upload_download_roundtrip() {
let _guard = GPU_COUNTER_LOCK.lock().unwrap_or_else(|e| e.into_inner());
let Some(ctx) = any_gpu() else {
return;
};
let w = 8;
let h = 4;
let token = ctx.create_texture(w, h).unwrap();
let mut frame = Frame::new();
let mut pod = VideoParamsPod::default();
pod.width = w;
pod.height = h;
frame.set_video_params(pod);
frame.allocate();
for i in 0..frame.data.len() / 4 {
let f = (i as f32) * 0.25 + 0.125;
let bytes = f.to_le_bytes();
frame.data[i * 4] = bytes[0];
frame.data[i * 4 + 1] = bytes[1];
frame.data[i * 4 + 2] = bytes[2];
frame.data[i * 4 + 3] = bytes[3];
}
ctx.upload(token, &frame).unwrap();
let out = ctx.download(token).unwrap();
assert_eq!(out.data.len(), frame.data.len());
assert_eq!(out.data, frame.data, "F32 bit-exact round-trip");
ctx.destroy_texture(token);
assert!(!ctx.has_texture(token));
}
#[test]
fn gpu_blit_copies_pixels() {
let _guard = GPU_COUNTER_LOCK.lock().unwrap_or_else(|e| e.into_inner());
let Some(ctx) = any_gpu() else {
return;
};
let w = 8;
let h = 4;
let src = ctx.create_texture(w, h).unwrap();
let dst = ctx.create_texture(w, h).unwrap();
let mut frame = Frame::new();
let mut pod = VideoParamsPod::default();
pod.width = w;
pod.height = h;
frame.set_video_params(pod);
frame.allocate();
for i in 0..frame.data.len() / 4 {
frame.data[i * 4] = (i % 251) as u8;
frame.data[i * 4 + 1] = (i * 3 % 251) as u8;
frame.data[i * 4 + 2] = (i * 7 % 251) as u8;
frame.data[i * 4 + 3] = 255;
}
ctx.upload(src, &frame).unwrap();
ctx.blit(src, dst, None).unwrap();
let out = ctx.download(dst).unwrap();
assert_eq!(out.data, frame.data, "plain-copy blit is pixel-exact");
assert!(ctx
.blit(
src,
dst,
Some(&crate::color::ColorProcessor::pass_through())
)
.is_err());
ctx.destroy_texture(src);
ctx.destroy_texture(dst);
}
#[test]
fn gpu_present_lut_matches_cpu_trilinear() {
let _guard = GPU_COUNTER_LOCK.lock().unwrap_or_else(|e| e.into_inner());
let Some(ctx) = any_gpu() else {
return;
};
let lut = crate::lut::Lut3d::build(9, [0.0; 3], [1.0; 3], |c| {
[
(0.5 * c[0] + 0.25 * c[1] + 0.25 * c[2]).powf(0.8),
c[1].powf(1.2),
(0.2 * c[0] + 0.8 * c[2]).powf(0.9),
]
});
ctx.set_display_lut(&lut).unwrap();
assert!(ctx.has_display_lut());
let w = 4;
let h = 3;
let src = ctx.create_texture(w, h).unwrap();
let mut frame = Frame::new();
let mut pod = VideoParamsPod::default();
pod.width = w;
pod.height = h;
frame.set_video_params(pod);
frame.allocate();
let sample = |i: usize| -> [f32; 4] {
[
(i as f32 * 0.13) % 1.0,
(i as f32 * 0.29) % 1.0,
(i as f32 * 0.47) % 1.0,
1.0,
]
};
for i in 0..(w * h) as usize {
let px = sample(i);
for (c, v) in px.iter().enumerate() {
frame.data[i * 16 + c * 4..i * 16 + c * 4 + 4].copy_from_slice(&v.to_le_bytes());
}
}
ctx.upload(src, &frame).unwrap();
reset_gpu_transfer_counters();
let dst = ctx.present_texture(src).unwrap();
assert_eq!(
ctx.texture_handle(dst).unwrap().format(),
wgpu::TextureFormat::Rgba32Float
);
assert_eq!(gpu_transfer_counters(), (0, 0), "present is GPU→GPU");
let out = ctx.download(dst).unwrap();
for i in 0..(w * h) as usize {
let px = sample(i);
let expect = lut.eval([px[0], px[1], px[2]]);
let got = [
f32::from_le_bytes(out.data[i * 16..i * 16 + 4].try_into().unwrap()),
f32::from_le_bytes(out.data[i * 16 + 4..i * 16 + 8].try_into().unwrap()),
f32::from_le_bytes(out.data[i * 16 + 8..i * 16 + 12].try_into().unwrap()),
];
for c in 0..3 {
assert!(
(got[c] - expect[c]).abs() < 5e-3,
"px {i} c{c}: {} vs {}",
got[c],
expect[c]
);
}
}
ctx.destroy_texture(src);
ctx.destroy_texture(dst);
}
#[test]
fn planar_passes_serialize_concurrent_uniform_uploads() {
let _guard = GPU_COUNTER_LOCK.lock().unwrap_or_else(|e| e.into_inner());
let Some(ctx) = any_gpu() else {
return;
};
let mut workers = Vec::new();
for _ in 0..8 {
let ctx = ctx.clone();
workers.push(std::thread::spawn(move || {
let pipeline = ctx.planar_yuv_pipeline().unwrap();
let src = ctx.create_texture(2, 2).unwrap();
let dst = ctx.create_texture(2, 2).unwrap();
let src_view = ctx.texture_view(src).unwrap();
let params = [0.0f32; 16];
for _ in 0..16 {
ctx.run_planar_views_pass(
&[&src_view, &src_view],
dst,
&pipeline,
¶ms,
"quercusrender-planar-concurrency-test",
)
.unwrap();
}
ctx.destroy_texture(src);
ctx.destroy_texture(dst);
}));
}
for worker in workers {
worker.join().expect("planar worker panicked");
}
}
#[test]
fn gpu_present_requires_a_lut() {
let Some(ctx) = any_gpu() else {
return;
};
assert!(!ctx.has_display_lut());
let tex = ctx.create_texture(2, 2).unwrap();
assert!(ctx.present_texture(tex).is_err());
ctx.destroy_texture(tex);
}
#[test]
fn gpu_copy_counters_track_cpu_transfers() {
let _guard = GPU_COUNTER_LOCK.lock().unwrap_or_else(|e| e.into_inner());
let Some(ctx) = any_gpu() else {
return;
};
reset_gpu_transfer_counters();
let token = ctx.create_texture(2, 2).unwrap();
let mut frame = Frame::new();
let mut pod = VideoParamsPod::default();
pod.width = 2;
pod.height = 2;
frame.set_video_params(pod);
frame.allocate();
ctx.upload(token, &frame).unwrap();
assert_eq!(gpu_transfer_counters(), (1, 0));
ctx.download(token).unwrap();
assert_eq!(gpu_transfer_counters(), (1, 1));
}
#[test]
fn gpu_yuv_to_rgb_matches_cpu_reference() {
let _guard = GPU_COUNTER_LOCK.lock().unwrap_or_else(|e| e.into_inner());
let Some(ctx) = any_gpu() else {
return;
};
let (w, h) = (8usize, 4usize);
let mut y_code_plane = vec![0u8; w * h * 2];
let mut u_code_plane = vec![0u8; w * h * 2];
let mut v_code_plane = vec![0u8; w * h * 2];
let mut y_plane = vec![0u8; w * h * 2];
let mut u_plane = vec![0u8; w * h * 2];
let mut v_plane = vec![0u8; w * h * 2];
for row in 0..h {
for x in 0..w {
let i = (row * w + x) * 2;
let y_code = (4096 + (x * 8191) % 56064) as u16;
let u_code = (32768i32 - 20000 + (row * 4000) as i32) as u16;
let v_code = (32768i32 + (x * 5000) as i32 - 20000) as u16;
for (code_plane, plane, code) in [
(&mut y_code_plane, &mut y_plane, y_code),
(&mut u_code_plane, &mut u_plane, u_code),
(&mut v_code_plane, &mut v_plane, v_code),
] {
code_plane[i..i + 2].copy_from_slice(&code.to_le_bytes());
let norm = code as f32 / 65535.0;
plane[i..i + 2]
.copy_from_slice(&half::f16::from_f32(norm).to_bits().to_le_bytes());
}
}
}
let y = ctx
.create_texture_format(
w as i32,
h as i32,
1,
wgpu::TextureFormat::R16Float,
wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
)
.unwrap();
let u = ctx
.create_texture_format(
w as i32,
h as i32,
1,
wgpu::TextureFormat::R16Float,
wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
)
.unwrap();
let v = ctx
.create_texture_format(
w as i32,
h as i32,
1,
wgpu::TextureFormat::R16Float,
wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
)
.unwrap();
ctx.upload_plane(y, &y_plane).unwrap();
ctx.upload_plane(u, &u_plane).unwrap();
ctx.upload_plane(v, &v_plane).unwrap();
let dst = ctx.create_texture(w as i32, h as i32).unwrap();
let mut expected = vec![0.0f32; w * h * 4];
let mut compare = |ctx: &GpuContext,
tag: &str,
matrix: crate::colormath::YuvMatrix,
transform: YuvTransform,
full_range: bool| {
crate::colormath::yuv444p16_to_rgb_f32(
&y_code_plane,
w * 2,
&u_code_plane,
w * 2,
&v_code_plane,
w * 2,
w,
h,
matrix,
full_range,
&mut expected,
);
ctx.run_yuv_to_rgb(y, u, v, dst, &transform).unwrap();
let out = ctx.download(dst).unwrap();
for i in 0..w * h {
let got = [
f32::from_le_bytes(out.data[i * 16..i * 16 + 4].try_into().unwrap()),
f32::from_le_bytes(out.data[i * 16 + 4..i * 16 + 8].try_into().unwrap()),
f32::from_le_bytes(out.data[i * 16 + 8..i * 16 + 12].try_into().unwrap()),
];
for (got_c, want) in got.iter().zip(&expected[i * 4..i * 4 + 3]) {
assert!(
(got_c - want).abs() < 4e-3,
"{tag}: px {i}: got {got_c} want {want}"
);
}
}
};
use crate::colormath::YuvMatrix;
compare(
&ctx,
"bt601 limited",
YuvMatrix::Bt601,
YuvTransform::bt601_limited(),
false,
);
compare(
&ctx,
"bt601 full",
YuvMatrix::Bt601,
YuvTransform::bt601_full(),
true,
);
compare(
&ctx,
"bt709 limited",
YuvMatrix::Bt709,
YuvTransform::bt709_limited(),
false,
);
compare(
&ctx,
"bt709 full",
YuvMatrix::Bt709,
YuvTransform::bt709_full(),
true,
);
compare(
&ctx,
"bt2020 limited",
YuvMatrix::Bt2020,
YuvTransform::bt2020_limited(),
false,
);
compare(
&ctx,
"bt2020 full",
YuvMatrix::Bt2020,
YuvTransform::bt2020_full(),
true,
);
ctx.destroy_texture(y);
ctx.destroy_texture(u);
ctx.destroy_texture(v);
ctx.destroy_texture(dst);
}
#[test]
fn yuv_transform_depth_matches_reference_math() {
use crate::colormath::YuvMatrix;
let t8 = YuvTransform::from_matrix_depth(YuvMatrix::Bt709, false, 8);
let (y, u, v) = (145.0f32 / 255.0, 117.0 / 255.0, 164.0 / 255.0);
let rgb = [
t8.matrix[0][0] * y + t8.matrix[0][2] * v + t8.offset[0],
t8.matrix[1][0] * y + t8.matrix[1][1] * u + t8.matrix[1][2] * v + t8.offset[1],
t8.matrix[2][0] * y + t8.matrix[2][1] * u + t8.offset[2],
];
let expect = [0.8421f32, 0.5230, 0.4979];
for c in 0..3 {
assert!(
(rgb[c] - expect[c]).abs() < 1e-3,
"channel {c}: {} vs {}",
rgb[c],
expect[c]
);
}
for matrix in [YuvMatrix::Bt601, YuvMatrix::Bt709, YuvMatrix::Bt2020] {
for full in [false, true] {
assert_eq!(
YuvTransform::from_matrix(matrix, full),
YuvTransform::from_matrix_depth(matrix, full, 16)
);
}
}
}
#[test]
fn gpu_apply_color_lut_matches_cpu() {
let _guard = GPU_COUNTER_LOCK.lock().unwrap_or_else(|e| e.into_inner());
let Some(ctx) = any_gpu() else {
return;
};
let lut = crate::lut::Lut3d::build(9, [0.0; 3], [1.0; 3], |c| [c[1], c[2], c[0]]);
let (w, h) = (4, 2);
let src = ctx.create_texture(w, h).unwrap();
let mut frame = Frame::new();
let mut pod = VideoParamsPod::default();
pod.width = w;
pod.height = h;
frame.set_video_params(pod);
frame.allocate();
for i in 0..(w * h) as usize {
let px = [
(i as f32 * 0.11) % 1.0,
(i as f32 * 0.23) % 1.0,
(i as f32 * 0.37) % 1.0,
1.0,
];
for (c, v) in px.iter().enumerate() {
frame.data[i * 16 + c * 4..i * 16 + c * 4 + 4].copy_from_slice(&v.to_le_bytes());
}
}
ctx.upload(src, &frame).unwrap();
reset_gpu_transfer_counters();
let dst = ctx.apply_color_lut(src, "test/swap", &lut).unwrap();
assert_eq!(
gpu_transfer_counters(),
(1, 0),
"the first apply uploads the LUT once and never reads back"
);
reset_gpu_transfer_counters();
let dst2 = ctx.apply_color_lut(src, "test/swap", &lut).unwrap();
assert_eq!(gpu_transfer_counters(), (0, 0), "cached apply is GPU→GPU");
let out = ctx.download(dst).unwrap();
let out2 = ctx.download(dst2).unwrap();
for got in [&out, &out2] {
for i in 0..(w * h) as usize {
let src_px = [
f32::from_le_bytes(frame.data[i * 16..i * 16 + 4].try_into().unwrap()),
f32::from_le_bytes(frame.data[i * 16 + 4..i * 16 + 8].try_into().unwrap()),
f32::from_le_bytes(frame.data[i * 16 + 8..i * 16 + 12].try_into().unwrap()),
];
let want = lut.eval(src_px);
for c in 0..3 {
let g = f32::from_le_bytes(
got.data[i * 16 + c * 4..i * 16 + c * 4 + 4]
.try_into()
.unwrap(),
);
assert!(
(g - want[c]).abs() < 1e-4,
"px {i} c{c}: {g} vs {}",
want[c]
);
}
}
}
ctx.destroy_texture(src);
ctx.destroy_texture(dst);
ctx.destroy_texture(dst2);
}
#[test]
fn gpu_missing_texture_errors() {
let _guard = GPU_COUNTER_LOCK.lock().unwrap_or_else(|e| e.into_inner());
let Some(ctx) = any_gpu() else {
return;
};
assert_eq!(
ctx.download(999999).unwrap_err().code(),
Error::NotFound.code()
);
assert!(!ctx.has_texture(999999));
ctx.destroy_texture(999999); }
#[test]
fn display_renderer_cpu_path() {
let mut r = DisplayRenderer::new(BackendKind::Cpu);
assert!(r.init(std::ptr::null_mut()).is_err());
let _ = &mut r;
let mut pod = VideoParamsPod::default();
pod.width = 4;
pod.height = 4;
let tex = r.create_texture(&pod, None).unwrap();
assert!(matches!(tex, Texture::Cpu(_)));
let frame = tex.to_frame().unwrap();
assert_eq!(frame.width, 4);
assert_eq!(frame.format, PixelFormat::F32);
}
#[test]
fn display_renderer_rejects_foreign_gl_context() {
let mut r = DisplayRenderer::new(BackendKind::Gl);
let fake = std::ptr::dangling_mut::<std::ffi::c_void>();
assert_eq!(r.init(fake).unwrap_err().code(), Error::Invalid.code());
}
#[test]
fn display_renderer_queries_and_texture_id() {
let r = DisplayRenderer::new(BackendKind::Gl);
assert!(r.is_open_gl());
assert!(!r.is_vulkan());
assert!(!r.is_initialized());
let r2 = DisplayRenderer::new(BackendKind::Vulkan);
assert!(r2.is_vulkan());
assert!(!r2.is_open_gl());
let r3 = DisplayRenderer::new(BackendKind::Cpu);
let mut pod = VideoParamsPod::default();
pod.width = 4;
pod.height = 4;
let tex = r3.create_texture(&pod, None).unwrap();
assert_eq!(crate::backend::texture_id_of(&tex), 0);
let mut bad = VideoParamsPod::default();
bad.width = 0;
assert!(r3.create_texture(&bad, None).is_err());
let mut frame = Frame::new();
frame.set_video_params(pod);
frame.allocate();
frame.data[0] = 0x77;
let mut upload_target = r3.create_texture(&pod, None).unwrap();
unsafe {
r3.upload_texture(
&mut upload_target,
frame.data.as_ptr(),
frame.linesize_bytes(),
)
}
.unwrap();
let mut buf = vec![0u8; frame.linesize_bytes() * 4];
unsafe { r3.download_texture(&upload_target, buf.as_mut_ptr(), frame.linesize_bytes()) }
.unwrap();
assert_eq!(buf[0], 0x77);
assert!(unsafe { r3.upload_texture(&mut upload_target, frame.data.as_ptr(), 1) }.is_err());
}
#[test]
fn cpu_blit_applies_color_and_copy() {
let r = DisplayRenderer::new(BackendKind::Cpu);
let mut pod = VideoParamsPod::default();
pod.width = 2;
pod.height = 2;
let mut src = r.create_texture(&pod, None).unwrap();
let mut dst = r.create_texture(&pod, None).unwrap();
let Texture::Cpu(sf) = &mut src else {
unreachable!()
};
sf.data[0] = 0x11;
sf.data[4] = 0x22;
r.blit_color_managed(Some(&src), &mut dst, None).unwrap();
let Texture::Cpu(df) = &dst else {
unreachable!()
};
assert_eq!(df.data[0], 0x11);
assert_eq!(df.data[4], 0x22);
r.blit_color_managed(
Some(&src),
&mut dst,
Some(&crate::color::ColorProcessor::pass_through()),
)
.unwrap();
let mut pod2 = VideoParamsPod::default();
pod2.width = 3;
pod2.height = 2;
let mut other = r.create_texture(&pod2, None).unwrap();
assert!(r.blit_color_managed(Some(&src), &mut other, None).is_err());
}
#[test]
fn backend_kind_strings_cover_every_variant() {
for (kind, s) in [
(BackendKind::Auto, "auto"),
(BackendKind::Metal, "metal"),
(BackendKind::Vulkan, "vulkan"),
(BackendKind::Gl, "opengl"),
(BackendKind::Cpu, "cpu"),
] {
assert_eq!(kind.to_config_string(), s);
assert_eq!(BackendKind::from_config_string(s), kind);
}
assert_eq!(
BackendKind::from_config_string(" VULKAN "),
BackendKind::Vulkan
);
assert_eq!(
BackendKind::Auto.wgpu_fallbacks(),
BackendKind::Metal.wgpu_fallbacks()
);
assert_eq!(
BackendKind::Vulkan.wgpu_fallbacks().first(),
Some(&wgpu::Backends::VULKAN)
);
assert_eq!(
BackendKind::Gl.wgpu_fallbacks().first(),
Some(&wgpu::Backends::GL)
);
assert!(BackendKind::Cpu.wgpu_fallbacks().is_empty());
}
#[test]
fn require_gpu_adapter_parses_env_values() {
for (value, expected) in [
(None, false), (Some("0"), false),
(Some("false"), false),
(Some("FALSE"), false),
(Some("1"), true),
(Some("yes"), true),
(Some(""), true),
] {
assert_eq!(require_gpu_from_value(value), expected, "value {value:?}");
}
skip_or_fail_gpu_with(false, "a coverage probe");
let panicked = std::panic::catch_unwind(|| skip_or_fail_gpu_with(true, "a coverage probe"));
assert!(
panicked.is_err(),
"a required GPU must hard-fail a missing adapter"
);
}
#[test]
fn shared_slot_host_marking_and_queries() {
let _guard = GPU_COUNTER_LOCK.lock().unwrap_or_else(|e| e.into_inner());
let marked = GpuContext::mark_host_context();
assert_eq!(marked, GpuContext::shared().is_some());
assert_eq!(GpuContext::host_gpu_installed(), marked);
assert_eq!(GpuContext::shared_is_installed(), marked);
}
#[test]
fn future_executor_drives_pending_futures() {
use std::future::Future;
use std::pin::Pin;
use std::task::{Context, Poll};
struct CloneWakerThenYield {
yielded: bool,
}
impl Future for CloneWakerThenYield {
type Output = u32;
fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<u32> {
let waker = cx.waker().clone();
assert!(waker.will_wake(cx.waker()));
drop(waker);
if self.yielded {
Poll::Ready(42)
} else {
self.yielded = true;
Poll::Pending
}
}
}
assert_eq!(
pollster_block_on(CloneWakerThenYield { yielded: false }),
42
);
assert_eq!(f32_uniform_bytes(&[1.0f32]), vec![0x00, 0x00, 0x80, 0x3f]);
}
#[test]
fn frame_from_pixels_for_upload_validates_arguments() {
let mut frame = Frame::new();
let pod = VideoParamsPod {
width: 2,
height: 2,
..Default::default()
};
frame.set_video_params(pod);
frame.allocate();
let stride = frame.linesize_bytes();
assert!(unsafe { frame_from_pixels_for_upload((2, 2), std::ptr::null(), stride) }.is_err());
assert!(
unsafe { frame_from_pixels_for_upload((0, 2), frame.data.as_ptr(), stride) }.is_err()
);
assert!(
unsafe { frame_from_pixels_for_upload((2, -1), frame.data.as_ptr(), stride) }.is_err()
);
assert!(unsafe { frame_from_pixels_for_upload((2, 2), frame.data.as_ptr(), 3) }.is_err());
let built =
unsafe { frame_from_pixels_for_upload((2, 2), frame.data.as_ptr(), stride) }.unwrap();
assert_eq!(built.width, 2);
assert_eq!(built.height, 2);
assert_eq!(built.data.len(), frame.data.len());
}
#[test]
fn gpu_context_accessors_and_registry_errors() {
let _guard = GPU_COUNTER_LOCK.lock().unwrap_or_else(|e| e.into_inner());
let Some(ctx) = any_gpu() else {
return;
};
assert!(ctx.is_gpu());
assert_ne!(ctx.kind(), BackendKind::Cpu);
assert!(!ctx.is_adopted());
assert!(!ctx.is_used());
let _ = ctx.is_filterable();
let (device, queue) = ctx.device_queue();
assert!(Arc::strong_count(&device) >= 1);
assert!(Arc::strong_count(&queue) >= 1);
assert_eq!(
ctx.create_texture(0, 4).unwrap_err().code(),
Error::Invalid.code()
);
assert_eq!(
ctx.create_texture(4, -1).unwrap_err().code(),
Error::Invalid.code()
);
assert_eq!(
ctx.create_texture_format(
4,
4,
0,
wgpu::TextureFormat::R8Unorm,
wgpu::TextureUsages::TEXTURE_BINDING,
)
.unwrap_err()
.code(),
Error::Invalid.code()
);
let token = ctx.create_texture(4, 3).unwrap();
assert!(ctx.is_used());
assert_eq!(ctx.texture_size(token), Some((4, 3)));
assert_eq!(
ctx.texture_format(token),
Some(wgpu::TextureFormat::Rgba32Float)
);
assert!(ctx.texture_handle(token).is_some());
assert_eq!(ctx.texture_size(999999), None);
assert_eq!(ctx.texture_format(999999), None);
assert!(!ctx.has_texture(999999));
let placeholder = ctx.placeholder_texture().unwrap();
assert_eq!(ctx.placeholder_texture().unwrap(), placeholder);
assert!(ctx.has_texture(placeholder));
ctx.clear_texture(token).unwrap();
assert_eq!(
ctx.clear_texture(999999).unwrap_err().code(),
Error::NotFound.code()
);
let like: &dyn GpuContextLike = ctx.as_ref();
assert_eq!(like.kind(), ctx.kind());
assert!(like.as_any().is_some());
assert!(like.texture_handle(placeholder).is_some());
}
#[test]
fn gpu_plane_upload_formats_and_bounds() {
let _guard = GPU_COUNTER_LOCK.lock().unwrap_or_else(|e| e.into_inner());
let Some(ctx) = any_gpu() else {
return;
};
let usage = wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST;
let r8 = ctx
.create_texture_format(4, 2, 1, wgpu::TextureFormat::R8Unorm, usage)
.unwrap();
ctx.upload_plane(r8, &[7u8; 8]).unwrap();
let r16f = ctx
.create_texture_format(4, 2, 1, wgpu::TextureFormat::R16Float, usage)
.unwrap();
ctx.upload_plane(r16f, &[0u8; 16]).unwrap();
let r32f = ctx
.create_texture_format(4, 2, 1, wgpu::TextureFormat::R32Float, usage)
.unwrap();
ctx.upload_plane(r32f, &[0u8; 32]).unwrap();
assert_eq!(
ctx.upload_plane(r8, &[0u8; 7]).unwrap_err().code(),
Error::Invalid.code()
);
let rgba = ctx
.create_texture_format(4, 2, 1, wgpu::TextureFormat::Rgba8Unorm, usage)
.unwrap();
assert_eq!(
ctx.upload_plane(rgba, &[0u8; 32]).unwrap_err().code(),
Error::Invalid.code()
);
assert_eq!(
ctx.upload_plane(999999, &[0u8; 8]).unwrap_err().code(),
Error::NotFound.code()
);
assert_eq!(
ctx.download(999999).unwrap_err().code(),
Error::NotFound.code()
);
let err = ctx.download(r8).unwrap_err();
assert!(
err.to_string().contains("unsupported format"),
"the format error is named: {err}"
);
}
#[test]
fn gpu_lut_lifecycle_cache_and_validation() {
let _guard = GPU_COUNTER_LOCK.lock().unwrap_or_else(|e| e.into_inner());
let Some(ctx) = any_gpu() else {
return;
};
let lut = crate::lut::Lut3d::build(3, [0.0; 3], [1.0; 3], |c| [c[1], c[2], c[0]]);
ctx.set_display_lut(&lut).unwrap();
assert!(ctx.has_display_lut());
ctx.set_display_lut(&lut).unwrap();
assert!(ctx.has_display_lut());
let src = ctx.create_texture(2, 2).unwrap();
let presented = ctx.present_texture(src).unwrap();
let presented2 = ctx.present_texture(src).unwrap();
assert_ne!(presented, presented2);
reset_gpu_transfer_counters();
let first = ctx.apply_color_lut(src, "swap", &lut).unwrap();
assert_eq!(gpu_transfer_counters(), (1, 0));
reset_gpu_transfer_counters();
let again = ctx.apply_color_lut(src, "swap", &lut).unwrap();
assert_eq!(gpu_transfer_counters(), (0, 0));
assert_ne!(first, again);
for i in 0..9 {
ctx.apply_color_lut(src, &format!("key-{i}"), &lut).unwrap();
}
reset_gpu_transfer_counters();
ctx.apply_color_lut(src, "swap", &lut).unwrap();
assert_eq!(gpu_transfer_counters(), (1, 0));
let mut malformed = lut.clone();
malformed.data.truncate(3);
assert_eq!(
ctx.upload_lut(&malformed).unwrap_err().code(),
Error::Invalid.code()
);
assert_eq!(
ctx.apply_color_lut(999999, "swap", &lut)
.unwrap_err()
.code(),
Error::NotFound.code()
);
}
#[test]
fn gpu_upload_download_and_blit_validate_arguments() {
let _guard = GPU_COUNTER_LOCK.lock().unwrap_or_else(|e| e.into_inner());
let Some(ctx) = any_gpu() else {
return;
};
let token = ctx.create_texture(4, 4).unwrap();
let other = ctx.create_texture(4, 4).unwrap();
let mut frame = Frame::new();
let pod = VideoParamsPod {
width: 4,
height: 4,
..Default::default()
};
frame.set_video_params(pod);
frame.allocate();
ctx.upload(token, &frame).unwrap();
let mut wrong_format = frame.clone();
wrong_format.format = PixelFormat::U8;
assert_eq!(
ctx.upload(token, &wrong_format).unwrap_err().code(),
Error::Invalid.code()
);
let mut wrong_size = Frame::new();
let small = VideoParamsPod {
width: 2,
height: 4,
..Default::default()
};
wrong_size.set_video_params(small);
wrong_size.allocate();
assert_eq!(
ctx.upload(token, &wrong_size).unwrap_err().code(),
Error::Invalid.code()
);
let mut truncated = frame.clone();
truncated.data.truncate(frame.linesize_bytes() * 4 - 1);
assert_eq!(
ctx.upload(token, &truncated).unwrap_err().code(),
Error::Invalid.code()
);
assert_eq!(
ctx.upload(999999, &frame).unwrap_err().code(),
Error::NotFound.code()
);
let like: &dyn GpuContextLike = ctx.as_ref();
like.upload(token, &frame).unwrap();
let out = like.download(token).unwrap();
assert_eq!(out.data, frame.data);
like.blit(token, other, None).unwrap();
assert!(like
.blit(
token,
other,
Some(&crate::color::ColorProcessor::pass_through())
)
.is_err());
assert_eq!(
ctx.blit(999999, other, None).unwrap_err().code(),
Error::NotFound.code()
);
assert_eq!(
ctx.blit(token, 999999, None).unwrap_err().code(),
Error::NotFound.code()
);
}
#[test]
fn gpu_compile_and_run_shader_pass() {
let _guard = GPU_COUNTER_LOCK.lock().unwrap_or_else(|e| e.into_inner());
let Some(ctx) = any_gpu() else {
return;
};
let wgsl = r#"
@group(0) @binding(1) var src_tex: texture_2d<f32>;
@group(0) @binding(2) var src_smp: sampler;
@fragment
fn main(@builtin(position) frag: vec4<f32>) -> @location(0) vec4<f32> {
return textureLoad(src_tex, vec2<i32>(i32(frag.x), i32(frag.y)), 0);
}
"#;
let program = ctx
.compile_shader_pass("test/pass", wgsl, 1, false, false)
.unwrap();
assert_eq!(program.texture_count, 1);
assert!(!program.has_uniforms);
assert!(!program.filtering);
let cached = ctx
.compile_shader_pass("test/pass", wgsl, 1, false, false)
.unwrap();
assert!(Arc::ptr_eq(&program, &cached));
let src = ctx.create_texture(2, 2).unwrap();
let dst = ctx.create_texture(2, 2).unwrap();
assert_eq!(
ctx.run_shader_pass(&program, &[], &[], dst)
.unwrap_err()
.code(),
Error::Invalid.code()
);
assert_eq!(
ctx.run_shader_pass(&program, &[], &[999999], dst)
.unwrap_err()
.code(),
Error::NotFound.code()
);
assert_eq!(
ctx.run_shader_pass(&program, &[], &[src], 999999)
.unwrap_err()
.code(),
Error::NotFound.code()
);
ctx.run_shader_pass(&program, &[], &[src], dst).unwrap();
let with_uniform = r#"
struct U { v: vec4<f32> };
@group(0) @binding(0) var<uniform> u: U;
@group(0) @binding(1) var src_tex: texture_2d<f32>;
@group(0) @binding(2) var src_smp: sampler;
@fragment
fn main() -> @location(0) vec4<f32> { return u.v; }
"#;
let uniform_program = ctx
.compile_shader_pass("test/uniform", with_uniform, 1, true, true)
.unwrap();
assert!(uniform_program.has_uniforms);
assert!(uniform_program.filtering);
ctx.run_shader_pass(&uniform_program, &[], &[src], dst)
.unwrap();
ctx.run_shader_pass(&uniform_program, &[0u8; 16], &[src], dst)
.unwrap();
let bad = ctx.compile_shader_pass(
"test/bad",
"@fragment fn not_main() -> @location(0) vec4<f32> { return vec4<f32>(1.0); }",
0,
false,
false,
);
assert!(bad.is_err(), "a missing entry point must fail the compile");
}
#[test]
fn gpu_yuv_and_planar_passes_cover_error_arms() {
let _guard = GPU_COUNTER_LOCK.lock().unwrap_or_else(|e| e.into_inner());
let Some(ctx) = any_gpu() else {
return;
};
let usage = wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST;
let (w, h) = (4u32, 2u32);
let y = ctx
.create_texture_format(w as i32, h as i32, 1, wgpu::TextureFormat::R16Float, usage)
.unwrap();
let uv = ctx
.create_texture_format(w as i32, h as i32, 1, wgpu::TextureFormat::Rg8Unorm, usage)
.unwrap();
let dst = ctx.create_texture(w as i32, h as i32).unwrap();
let y_data: Vec<u8> = (0..w * h)
.flat_map(|_| half::f16::from_f32(0.5).to_bits().to_le_bytes())
.collect();
ctx.upload_plane(y, &y_data).unwrap();
let uv_data = vec![128u8; (w * h * 2) as usize];
ctx.queue.write_texture(
wgpu::TexelCopyTextureInfo {
texture: &ctx.texture_handle(uv).unwrap(),
mip_level: 0,
origin: wgpu::Origin3d::ZERO,
aspect: wgpu::TextureAspect::All,
},
&uv_data,
wgpu::TexelCopyBufferLayout {
offset: 0,
bytes_per_row: Some(w * 2),
rows_per_image: None,
},
wgpu::Extent3d {
width: w,
height: h,
depth_or_array_layers: 1,
},
);
let transform = YuvTransform::bt709_limited();
ctx.run_planar_yuv_to_rgb(y, uv, dst, &transform).unwrap();
ctx.run_planar_yuv_to_rgb(y, uv, dst, &transform).unwrap();
let out = ctx.download(dst).unwrap();
assert_eq!(out.data.len(), (w * h) as usize * 16);
assert!(
out.data.iter().any(|&b| b != 0),
"the planar pass writes pixels"
);
assert_eq!(
ctx.run_planar_yuv_to_rgb(999999, uv, dst, &transform)
.unwrap_err()
.code(),
Error::NotFound.code()
);
assert_eq!(
ctx.run_planar_yuv_to_rgb(y, 999999, dst, &transform)
.unwrap_err()
.code(),
Error::NotFound.code()
);
assert_eq!(
ctx.run_planar_yuv_to_rgb(y, uv, 999999, &transform)
.unwrap_err()
.code(),
Error::NotFound.code()
);
assert_eq!(
ctx.run_yuv_to_rgb(999999, uv, uv, dst, &transform)
.unwrap_err()
.code(),
Error::NotFound.code()
);
assert_eq!(
ctx.run_yuv_to_rgb(y, 999999, uv, dst, &transform)
.unwrap_err()
.code(),
Error::NotFound.code()
);
assert_eq!(
ctx.run_yuv_to_rgb(y, uv, 999999, dst, &transform)
.unwrap_err()
.code(),
Error::NotFound.code()
);
assert_eq!(
ctx.run_yuv_to_rgb(y, uv, uv, 999999, &transform)
.unwrap_err()
.code(),
Error::NotFound.code()
);
}
#[test]
fn display_renderer_gpu_texture_paths() {
let _guard = GPU_COUNTER_LOCK.lock().unwrap_or_else(|e| e.into_inner());
let Some(ctx) = any_gpu() else {
return;
};
let mut renderer = DisplayRenderer::new(BackendKind::Cpu);
renderer.ctx = Some(ctx.clone());
assert!(renderer.is_initialized());
assert_eq!(renderer.backend(), BackendKind::Cpu);
assert_eq!(renderer.context().map(|c| c.kind()), Some(ctx.kind()));
let pod = VideoParamsPod {
width: 4,
height: 4,
..Default::default()
};
let mut frame = Frame::new();
frame.set_video_params(pod);
frame.allocate();
frame.data[0] = 0x5a;
let src = renderer
.create_texture(&pod, Some((frame.data.as_ptr(), frame.linesize_bytes())))
.unwrap();
assert!(matches!(src, Texture::Gpu { .. }));
let Texture::Gpu { token, .. } = &src else {
unreachable!()
};
let token = *token;
assert_eq!(crate::backend::texture_id_of(&src), token as i32);
assert_eq!(
renderer
.create_texture(&pod, Some((frame.data.as_ptr(), 1)))
.unwrap_err()
.code(),
Error::Invalid.code()
);
let mut target = renderer.create_texture(&pod, None).unwrap();
unsafe {
renderer.upload_texture(&mut target, frame.data.as_ptr(), frame.linesize_bytes())
}
.unwrap();
let _ =
unsafe { renderer.upload_texture(&mut target, frame.data.as_ptr(), 1) }.unwrap_err();
let mut buf = vec![0u8; frame.linesize_bytes() * 4];
unsafe { renderer.download_texture(&target, buf.as_mut_ptr(), frame.linesize_bytes()) }
.unwrap();
assert_eq!(buf[0], 0x5a);
let _ = unsafe { renderer.download_texture(&target, buf.as_mut_ptr(), 1) }.unwrap_err();
let mut other = renderer.create_texture(&pod, None).unwrap();
renderer
.blit_color_managed(Some(&target), &mut other, None)
.unwrap();
let y = ctx
.create_texture_format(
4,
2,
1,
wgpu::TextureFormat::R8Unorm,
wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
)
.unwrap();
let uv = ctx
.create_texture_format(
4,
2,
1,
wgpu::TextureFormat::Rg8Unorm,
wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
)
.unwrap();
let planar = crate::texture::PlanarTexture::new(
ctx.clone(),
crate::texture::PlanarFormat::Nv12,
(4, 2),
(y, uv),
YuvTransform::bt709_limited(),
(1, 1),
);
let mut planar_texture = Texture::wrap_planar(planar);
assert_eq!(crate::backend::texture_id_of(&planar_texture), 0);
assert!(planar_texture.is_planar());
assert!(planar_texture.to_frame().is_err());
let _ = unsafe { renderer.upload_texture(&mut planar_texture, std::ptr::null(), 0) }
.unwrap_err();
let cpu_renderer = DisplayRenderer::new(BackendKind::Cpu);
let mut cpu_src = cpu_renderer.create_texture(&pod, None).unwrap();
assert_eq!(
renderer
.blit_color_managed(Some(&cpu_src), &mut other, None)
.unwrap_err()
.code(),
crate::error::QUERCUSCORE_E_FAILED
);
assert!(renderer
.blit_color_managed(Some(&target), &mut cpu_src, None)
.is_err());
let mut readback = vec![0u8; frame.linesize_bytes() * 4];
assert_eq!(
unsafe {
cpu_renderer.download_from_texture(
token as i32,
&pod,
readback.as_mut_ptr(),
frame.linesize_bytes(),
)
}
.unwrap_err()
.code(),
crate::error::QUERCUSCORE_E_FAILED
);
unsafe {
renderer.download_from_texture(
token as i32,
&pod,
readback.as_mut_ptr(),
frame.linesize_bytes(),
)
}
.unwrap();
assert_eq!(readback[0], 0x5a);
assert!(unsafe {
renderer.download_from_texture(token as i32, &pod, readback.as_mut_ptr(), 4)
}
.is_err());
}
#[test]
fn display_renderer_cpu_pixel_initialization() {
let renderer = DisplayRenderer::new(BackendKind::Cpu);
let pod = VideoParamsPod {
width: 2,
height: 2,
..Default::default()
};
let mut frame = Frame::new();
frame.set_video_params(pod);
frame.allocate();
frame.data[3] = 0x33;
let texture = renderer
.create_texture(&pod, Some((frame.data.as_ptr(), frame.linesize_bytes())))
.unwrap();
let Texture::Cpu(cpu) = &texture else {
unreachable!()
};
assert_eq!(cpu.data[3], 0x33);
assert_eq!(
renderer
.create_texture(&pod, Some((frame.data.as_ptr(), 3)))
.unwrap_err()
.code(),
Error::Invalid.code()
);
}
#[test]
fn display_bit_depth_from_user_config_reads_the_store() {
let depth = DisplayBitDepth::from_user_config();
assert!(matches!(
depth,
DisplayBitDepth::Bit8 | DisplayBitDepth::Bit10
));
assert_eq!(
depth.to_config_string(),
if depth == DisplayBitDepth::Bit8 {
"8"
} else {
"10"
}
);
}
struct FakeContext;
impl GpuContextLike for FakeContext {
fn kind(&self) -> BackendKind {
BackendKind::Cpu
}
fn destroy_texture(&self, _token: u64) {}
fn upload(&self, _token: u64, _frame: &Frame) -> Result<()> {
Ok(())
}
fn download(&self, _token: u64) -> Result<Frame> {
Ok(Frame::dummy())
}
fn blit(
&self,
_src: u64,
_dst: u64,
_processor: Option<&crate::color::ColorProcessor>,
) -> Result<()> {
Ok(())
}
}
#[test]
fn trait_only_context_uses_the_default_downcast_hooks() {
let fake = FakeContext;
assert_eq!(fake.kind(), BackendKind::Cpu);
assert!(fake.as_any().is_none());
assert!(fake.texture_handle(1).is_none());
let texture = Texture::gpu(Arc::new(FakeContext), 7, 2, 2, PixelFormat::F32);
assert_eq!(texture_id_of(&texture), 7);
assert!(format!("{texture:?}").contains("Texture::Gpu"));
}
#[test]
fn display_renderer_init_uses_the_shared_context_for_the_user_backend() {
let _guard = GPU_COUNTER_LOCK.lock().unwrap_or_else(|e| e.into_inner());
let kind = BackendKind::from_user_config();
let mut renderer = DisplayRenderer::new(kind);
let result = renderer.init(std::ptr::null_mut());
assert_eq!(
result.is_ok(),
GpuContext::shared().is_some(),
"init follows the shared slot"
);
assert_eq!(renderer.is_initialized(), result.is_ok());
}
fn pixel_f32(data: &[u8], index: usize) -> [f32; 4] {
let base = index * 16;
[
f32::from_le_bytes(data[base..base + 4].try_into().unwrap()),
f32::from_le_bytes(data[base + 4..base + 8].try_into().unwrap()),
f32::from_le_bytes(data[base + 8..base + 12].try_into().unwrap()),
f32::from_le_bytes(data[base + 12..base + 16].try_into().unwrap()),
]
}
fn bilinear_code_ref(codes: &[u16], width: usize, height: usize, pos: (f32, f32)) -> f32 {
let max_x = (width - 1) as f32;
let max_y = (height - 1) as f32;
let x = pos.0.clamp(0.0, max_x);
let y = pos.1.clamp(0.0, max_y);
let x0 = x.floor() as usize;
let y0 = y.floor() as usize;
let fx = x - x0 as f32;
let fy = y - y0 as f32;
let x1 = (x0 + 1).min(width - 1);
let y1 = (y0 + 1).min(height - 1);
let s = |px: usize, py: usize| codes[py * width + px] as f32 / 65535.0;
let top = s(x0, y0) * (1.0 - fx) + s(x1, y0) * fx;
let bottom = s(x0, y1) * (1.0 - fx) + s(x1, y1) * fx;
top * (1.0 - fy) + bottom * fy
}
fn identity_yuv_transform() -> YuvTransform {
YuvTransform {
matrix: [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]],
offset: [0.0, 0.0, 0.0],
}
}
fn u16_bytes(values: &[u16]) -> Vec<u8> {
values.iter().flat_map(|v| v.to_le_bytes()).collect()
}
#[test]
fn plane_row_packing_and_rgb_expansion() {
let src = [1u8, 2, 3, 0xFF, 0xFF, 4, 5, 6, 0xFF, 0xFF];
let staged = repack_plane_rows(&src, 5, 3, 2).unwrap();
assert_eq!(staged.len(), 2 * PLANE_ROW_ALIGNMENT);
assert_eq!(&staged[..3], &[1, 2, 3]);
assert_eq!(
&staged[PLANE_ROW_ALIGNMENT..PLANE_ROW_ALIGNMENT + 3],
&[4, 5, 6]
);
assert!(staged[3..PLANE_ROW_ALIGNMENT].iter().all(|&b| b == 0));
let tight = repack_plane_rows(&[7, 8, 9, 10], 0, 2, 2).unwrap();
assert_eq!(&tight[..2], &[7, 8]);
assert_eq!(
&tight[PLANE_ROW_ALIGNMENT..PLANE_ROW_ALIGNMENT + 2],
&[9, 10]
);
assert_eq!(aligned_plane_row_bytes(1), PLANE_ROW_ALIGNMENT);
assert_eq!(aligned_plane_row_bytes(256), 256);
assert_eq!(aligned_plane_row_bytes(257), 512);
assert!(repack_plane_rows(&[0u8; 3], 0, 2, 2).is_err());
assert!(repack_plane_rows(&[0u8; 4], 1, 2, 2).is_err());
assert!(repack_plane_rows(&[], 0, 0, 0).is_err());
let rgb8 = [1u8, 2, 3, 4, 5, 6];
assert_eq!(
expand_rgb_to_rgba(&rgb8, 0, 2, 1, PlaneStorage::U8).unwrap(),
vec![1, 2, 3, 255, 4, 5, 6, 255]
);
let rgb16 = [0x34u8, 0x12, 0x78, 0x56, 0xBC, 0x9A];
assert_eq!(
expand_rgb_to_rgba(&rgb16, 0, 1, 1, PlaneStorage::U16).unwrap(),
vec![0x34, 0x12, 0x78, 0x56, 0xBC, 0x9A, 0xFF, 0xFF]
);
let rgb_f16 = [0u8; 6];
assert_eq!(
expand_rgb_to_rgba(&rgb_f16, 0, 1, 1, PlaneStorage::F16).unwrap(),
vec![0, 0, 0, 0, 0, 0, 0x00, 0x3C]
);
let rgb_f32 = [0u8; 12];
assert_eq!(
expand_rgb_to_rgba(&rgb_f32, 0, 1, 1, PlaneStorage::F32).unwrap(),
vec![0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0x00, 0x00, 0x80, 0x3F]
);
let mut padded = vec![0u8; 8 * 2];
padded[0..3].copy_from_slice(&[10, 11, 12]);
padded[8..11].copy_from_slice(&[20, 21, 22]);
let expanded = expand_rgb_to_rgba(&padded, 8, 1, 2, PlaneStorage::U8).unwrap();
assert_eq!(expanded, vec![10, 11, 12, 255, 20, 21, 22, 255]);
assert!(expand_rgb_to_rgba(&[1u8, 2, 3], 0, 2, 1, PlaneStorage::U8).is_err());
assert!(expand_rgb_to_rgba(&rgb8, 2, 2, 1, PlaneStorage::U8).is_err());
}
#[test]
fn gpu_planar_nv12_cpu_upload_native_and_scaled() {
let _guard = GPU_COUNTER_LOCK.lock().unwrap_or_else(|e| e.into_inner());
let Some(ctx) = any_gpu() else {
return;
};
let (w, h) = (10usize, 6usize);
let y_bytes: Vec<u8> = (0..w * h).map(|i| ((i * 7) % 251) as u8).collect();
let (cw, ch) = (w.div_ceil(2), h.div_ceil(2));
let uv_bytes = vec![128u8; cw * ch * 2];
let (y, uv) = ctx
.upload_planar_frame(
PlanarFormat::Nv12,
w as i32,
h as i32,
&y_bytes,
0,
&uv_bytes,
0,
)
.unwrap();
assert_eq!(ctx.texture_size(y), Some((w as u32, h as u32)));
assert_eq!(ctx.texture_size(uv), Some((cw as u32, ch as u32)));
assert_eq!(ctx.texture_format(y), Some(wgpu::TextureFormat::R8Unorm));
let identity = identity_yuv_transform();
let dst = ctx.create_texture(w as i32, h as i32).unwrap();
ctx.run_planar_yuv_to_rgb(y, uv, dst, &identity).unwrap();
let native = ctx.download(dst).unwrap();
for row in 0..h {
for col in 0..w {
let i = row * w + col;
let got = pixel_f32(&native.data, i);
assert!((got[0] - y_bytes[i] as f32 / 255.0).abs() < 1e-6);
assert!((got[1] - 128.0 / 255.0).abs() < 1e-6);
assert!((got[2] - 128.0 / 255.0).abs() < 1e-6);
assert_eq!(got[3], 1.0);
}
}
let scaled = ctx.create_texture(w as i32, h as i32).unwrap();
ctx.run_planar_yuv_to_rgb_scaled(
y,
uv,
scaled,
&identity,
(w as u32, h as u32),
(w as u32, h as u32),
)
.unwrap();
let scaled_out = ctx.download(scaled).unwrap();
for i in 0..w * h {
let a = pixel_f32(&native.data, i);
let b = pixel_f32(&scaled_out.data, i);
for channel in 0..3 {
assert!(
(a[channel] - b[channel]).abs() < 1e-6,
"px {i} channel {channel}: {a:?} vs {b:?}"
);
}
}
for token in [y, uv, dst, scaled] {
ctx.destroy_texture(token);
}
}
#[test]
fn gpu_planar_yuv422p10_scaled_bilinear_matches_reference() {
let _guard = GPU_COUNTER_LOCK.lock().unwrap_or_else(|e| e.into_inner());
let Some(ctx) = any_gpu() else {
return;
};
if !ctx.supports_16bit_norm_planes() {
eprintln!("16-bit normalized texture formats unavailable; skipping");
return;
}
let (w, h) = (6usize, 4usize);
let (cw, ch) = (3usize, 4usize);
let (dw, dh) = (9usize, 7usize);
let y_codes: Vec<u16> = (0..w * h)
.map(|i| 1000 + ((i as u16) * 977) % 60000)
.collect();
let uv_codes: Vec<u16> = (0..cw * ch).map(|i| 4000 + (i as u16) * 2500).collect();
let mut uv_bytes = Vec::with_capacity(cw * ch * 4);
for u in &uv_codes {
uv_bytes.extend_from_slice(&u.to_le_bytes());
uv_bytes.extend_from_slice(&0x8000u16.to_le_bytes());
}
let (y, uv) = ctx
.upload_planar_frame(
PlanarFormat::Yuv422P10,
w as i32,
h as i32,
&u16_bytes(&y_codes),
0,
&uv_bytes,
0,
)
.unwrap();
assert_eq!(ctx.texture_size(y), Some((w as u32, h as u32)));
assert_eq!(ctx.texture_size(uv), Some((cw as u32, ch as u32)));
assert_eq!(ctx.texture_format(uv), Some(wgpu::TextureFormat::Rg16Unorm));
let dst = ctx.create_texture(dw as i32, dh as i32).unwrap();
ctx.run_planar_yuv_to_rgb_scaled(
y,
uv,
dst,
&identity_yuv_transform(),
(w as u32, h as u32),
(dw as u32, dh as u32),
)
.unwrap();
let out = ctx.download(dst).unwrap();
for row in 0..dh {
for col in 0..dw {
let fx = (col as f32 + 0.5) * (w as f32 / dw as f32) - 0.5;
let fy = (row as f32 + 0.5) * (h as f32 / dh as f32) - 0.5;
let cx = (col as f32 + 0.5) * (cw as f32 / dw as f32) - 0.5;
let cy = (row as f32 + 0.5) * (ch as f32 / dh as f32) - 0.5;
let want_y = bilinear_code_ref(&y_codes, w, h, (fx, fy));
let want_u = bilinear_code_ref(&uv_codes, cw, ch, (cx, cy));
let got = pixel_f32(&out.data, row * dw + col);
assert!(
(got[0] - want_y).abs() < 1e-4,
"Y px ({col},{row}): got {} want {want_y}",
got[0]
);
assert!(
(got[1] - want_u).abs() < 1e-4,
"U px ({col},{row}): got {} want {want_u}",
got[1]
);
assert!((got[2] - 0.5).abs() < 2e-5, "V px ({col},{row})");
assert_eq!(got[3], 1.0);
}
}
for token in [y, uv, dst] {
ctx.destroy_texture(token);
}
}
#[test]
fn gpu_planar_padded_planes_and_odd_visible_size() {
let _guard = GPU_COUNTER_LOCK.lock().unwrap_or_else(|e| e.into_inner());
let Some(ctx) = any_gpu() else {
return;
};
let usage = wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST;
let (vw, vh) = (7usize, 5usize);
let (cw, ch) = (4usize, 3usize);
let (yw, yh) = (9usize, 7usize);
let (uw, uh) = (5usize, 4usize);
let y = ctx
.create_texture_format(yw as i32, yh as i32, 1, wgpu::TextureFormat::R8Unorm, usage)
.unwrap();
let uv = ctx
.create_texture_format(
uw as i32,
uh as i32,
1,
wgpu::TextureFormat::Rg8Unorm,
usage,
)
.unwrap();
let mut y_data = vec![255u8; yw * yh];
for row in 0..vh {
for col in 0..vw {
y_data[row * yw + col] = (col * 17 + row * 31) as u8;
}
}
let mut uv_data = vec![255u8; uw * uh * 2];
for row in 0..ch {
for col in 0..cw {
let offset = (row * uw + col) * 2;
uv_data[offset] = 128;
uv_data[offset + 1] = 64;
}
}
let descriptor = PlanarDescriptor::yuv_semi_planar(Subsampling::Cs420, PlaneStorage::U8);
ctx.upload_planar_matrix_planes(
&[y, uv],
descriptor,
&[PlaneData::tight(&y_data), PlaneData::tight(&uv_data)],
)
.unwrap();
let identity = identity_yuv_transform();
let dst = ctx.create_texture(vw as i32, vh as i32).unwrap();
ctx.run_planar_matrix_to_rgb(
&[y, uv],
dst,
descriptor,
&identity,
(vw as u32, vh as u32),
(vw as u32, vh as u32),
)
.unwrap();
let out = ctx.download(dst).unwrap();
for row in 0..vh {
for col in 0..vw {
let i = row * vw + col;
let got = pixel_f32(&out.data, i);
let want = y_data[row * yw + col] as f32 / 255.0;
assert!((got[0] - want).abs() < 1e-6, "Y px ({col},{row})");
assert!((got[1] - 128.0 / 255.0).abs() < 1e-6, "U px ({col},{row})");
assert!((got[2] - 64.0 / 255.0).abs() < 1e-6, "V px ({col},{row})");
assert_eq!(got[3], 1.0);
}
}
let scaled = ctx.create_texture(11, 9).unwrap();
ctx.run_planar_matrix_to_rgb(
&[y, uv],
scaled,
descriptor,
&identity,
(vw as u32, vh as u32),
(11, 9),
)
.unwrap();
let scaled_out = ctx.download(scaled).unwrap();
let max_visible_y = 226.0 / 255.0;
for i in 0..11 * 9 {
let got = pixel_f32(&scaled_out.data, i);
assert!(
got[0] <= max_visible_y + 1e-6,
"padding leaked into Y at px {i}: {}",
got[0]
);
assert!(
got[1] <= 128.0 / 255.0 + 1e-6,
"padding leaked into U at px {i}: {}",
got[1]
);
}
for token in [y, uv, dst, scaled] {
ctx.destroy_texture(token);
}
}
#[test]
fn gpu_planar_matrix_cells_444_rgb_rgba() {
let _guard = GPU_COUNTER_LOCK.lock().unwrap_or_else(|e| e.into_inner());
let Some(ctx) = any_gpu() else {
return;
};
let identity = identity_yuv_transform();
if ctx.supports_16bit_norm_planes() {
let (w, h) = (7usize, 5usize);
let descriptor = PlanarDescriptor::yuv_planar(Subsampling::Cs444, PlaneStorage::U16);
let y_codes: Vec<u16> = (0..w * h).map(|i| 1000 + (i as u16 % 50000)).collect();
let u_codes: Vec<u16> = (0..w * h).map(|i| 500 + (i as u16 * 3) % 40000).collect();
let v_codes: Vec<u16> = (0..w * h).map(|i| 30000 - (i as u16 * 2) % 20000).collect();
let planes = ctx
.upload_planar_matrix_frame(
descriptor,
w as i32,
h as i32,
&[
PlaneData::tight(&u16_bytes(&y_codes)),
PlaneData::tight(&u16_bytes(&u_codes)),
PlaneData::tight(&u16_bytes(&v_codes)),
],
)
.unwrap();
assert_eq!(planes.len(), 3);
let dst = ctx.create_texture(w as i32, h as i32).unwrap();
ctx.run_planar_matrix_to_rgb_native(&planes, dst, descriptor, &identity)
.unwrap();
let out = ctx.download(dst).unwrap();
for i in 0..w * h {
let got = pixel_f32(&out.data, i);
assert!((got[0] - y_codes[i] as f32 / 65535.0).abs() < 1e-6);
assert!((got[1] - u_codes[i] as f32 / 65535.0).abs() < 1e-6);
assert!((got[2] - v_codes[i] as f32 / 65535.0).abs() < 1e-6);
assert_eq!(got[3], 1.0);
}
for token in planes.iter().chain([&dst]) {
ctx.destroy_texture(*token);
}
}
{
let (w, h) = (4usize, 2usize);
let (cw, ch) = (2usize, 1usize);
let descriptor =
PlanarDescriptor::yuv_semi_planar(Subsampling::Cs420, PlaneStorage::F16);
let half = |v: f32| half::f16::from_f32(v).to_bits().to_le_bytes();
let y_bytes: Vec<u8> = (0..w * h).flat_map(|_| half(0.5)).collect();
let mut uv_bytes = Vec::with_capacity(cw * ch * 4);
for _ in 0..cw * ch {
uv_bytes.extend_from_slice(&half(0.25));
uv_bytes.extend_from_slice(&half(0.75));
}
let planes = ctx
.upload_planar_matrix_frame(
descriptor,
w as i32,
h as i32,
&[PlaneData::tight(&y_bytes), PlaneData::tight(&uv_bytes)],
)
.unwrap();
let dst = ctx.create_texture(w as i32, h as i32).unwrap();
ctx.run_planar_matrix_to_rgb_native(&planes, dst, descriptor, &identity)
.unwrap();
let out = ctx.download(dst).unwrap();
for i in 0..w * h {
let got = pixel_f32(&out.data, i);
assert_eq!(got, [0.5, 0.25, 0.75, 1.0], "px {i}");
}
for token in planes.iter().chain([&dst]) {
ctx.destroy_texture(*token);
}
}
{
let (w, h) = (5usize, 3usize);
let descriptor = PlanarDescriptor::rgb(PlaneStorage::U8);
let stride = w * 3 + 2;
let mut rgb = vec![0u8; stride * h];
for row in 0..h {
for col in 0..w {
let i = row * w + col;
let o = row * stride + col * 3;
rgb[o] = (i * 3) as u8;
rgb[o + 1] = (i * 5 + 1) as u8;
rgb[o + 2] = (i * 7 + 2) as u8;
}
}
let planes = ctx
.upload_planar_matrix_frame(
descriptor,
w as i32,
h as i32,
&[PlaneData::strided(&rgb, stride)],
)
.unwrap();
assert_eq!(planes.len(), 1);
assert_eq!(
ctx.texture_format(planes[0]),
Some(wgpu::TextureFormat::Rgba8Unorm)
);
let dst = ctx.create_texture(w as i32, h as i32).unwrap();
ctx.run_planar_matrix_to_rgb_native(&planes, dst, descriptor, &identity)
.unwrap();
let out = ctx.download(dst).unwrap();
for row in 0..h {
for col in 0..w {
let i = row * w + col;
let o = row * stride + col * 3;
let got = pixel_f32(&out.data, i);
assert!((got[0] - rgb[o] as f32 / 255.0).abs() < 1e-6);
assert!((got[1] - rgb[o + 1] as f32 / 255.0).abs() < 1e-6);
assert!((got[2] - rgb[o + 2] as f32 / 255.0).abs() < 1e-6);
assert_eq!(got[3], 1.0);
}
}
for token in planes.iter().chain([&dst]) {
ctx.destroy_texture(*token);
}
}
{
let (w, h) = (4usize, 2usize);
let descriptor = PlanarDescriptor::rgba(PlaneStorage::F32);
let mut rgba = vec![0u8; w * h * 16];
for i in 0..w * h {
for channel in 0..4 {
let value = (i * 4 + channel) as f32 * 0.25;
let base = i * 16 + channel * 4;
rgba[base..base + 4].copy_from_slice(&value.to_le_bytes());
}
}
let planes = ctx
.upload_planar_matrix_frame(
descriptor,
w as i32,
h as i32,
&[PlaneData::tight(&rgba)],
)
.unwrap();
let dst = ctx.create_texture(w as i32, h as i32).unwrap();
ctx.run_planar_matrix_to_rgb_native(&planes, dst, descriptor, &identity)
.unwrap();
let out = ctx.download(dst).unwrap();
assert_eq!(&out.data, &rgba, "RGBA F32 is a direct passthrough");
for token in planes.iter().chain([&dst]) {
ctx.destroy_texture(*token);
}
}
}
#[test]
fn gpu_planar_matrix_creation_upload_and_pass_validation() {
let _guard = GPU_COUNTER_LOCK.lock().unwrap_or_else(|e| e.into_inner());
let Some(ctx) = any_gpu() else {
return;
};
let identity = identity_yuv_transform();
let (y, uv) = ctx
.create_planar_textures(PlanarFormat::Nv12, 6, 4)
.unwrap();
assert_eq!(ctx.texture_size(y), Some((6, 4)));
assert_eq!(ctx.texture_size(uv), Some((3, 2)));
assert_eq!(ctx.texture_format(uv), Some(wgpu::TextureFormat::Rg8Unorm));
assert_eq!(
ctx.upload_planar_planes(y, uv, &[0u8; 24], &[0u8; 11])
.unwrap_err()
.code(),
Error::Invalid.code()
);
assert_eq!(
ctx.upload_planar_planes(999_999, uv, &[0u8; 24], &[0u8; 12])
.unwrap_err()
.code(),
Error::NotFound.code()
);
assert_eq!(
ctx.create_planar_textures(PlanarFormat::Nv12, 0, 4)
.unwrap_err()
.code(),
Error::Invalid.code()
);
let u16_descriptor = PlanarDescriptor::yuv_planar(Subsampling::Cs422, PlaneStorage::U16);
if ctx.supports_16bit_norm_planes() {
let planes = ctx
.create_planar_matrix_textures(u16_descriptor, 5, 3)
.unwrap();
assert_eq!(planes.len(), 3);
assert_eq!(ctx.texture_size(planes[1]), Some((3, 3)));
assert_eq!(
ctx.texture_format(planes[1]),
Some(wgpu::TextureFormat::R16Unorm)
);
let readonly = ctx
.create_texture_format(
3,
3,
1,
wgpu::TextureFormat::R16Unorm,
wgpu::TextureUsages::TEXTURE_BINDING,
)
.unwrap();
assert_eq!(
ctx.upload_planar_matrix_planes(
&[planes[0], readonly],
u16_descriptor,
&[PlaneData::tight(&[0u8; 30]), PlaneData::tight(&[0u8; 36])],
)
.unwrap_err()
.code(),
Error::Invalid.code()
);
ctx.destroy_texture(readonly);
for token in &planes {
ctx.destroy_texture(*token);
}
let before = lock(&ctx.textures).len();
assert_eq!(
ctx.upload_planar_matrix_frame(
u16_descriptor,
5,
3,
&[PlaneData::tight(&[0u8; 30]), PlaneData::tight(&[0u8; 17])],
)
.unwrap_err()
.code(),
Error::Invalid.code()
);
assert_eq!(lock(&ctx.textures).len(), before, "no leaked plane tokens");
} else {
assert!(matches!(
ctx.create_planar_matrix_textures(u16_descriptor, 5, 3),
Err(Error::Failed(_))
));
}
let descriptor = PlanarDescriptor::yuv_semi_planar(Subsampling::Cs420, PlaneStorage::U8);
let dst = ctx.create_texture(6, 4).unwrap();
let wrong_subsampling =
PlanarDescriptor::yuv_semi_planar(Subsampling::Cs422, PlaneStorage::U8);
assert_eq!(
ctx.run_planar_matrix_to_rgb(
&[y, uv],
dst,
wrong_subsampling,
&identity,
(6, 4),
(6, 4),
)
.unwrap_err()
.code(),
Error::Invalid.code()
);
assert_eq!(
ctx.run_planar_matrix_to_rgb(
&[999_999, uv],
dst,
descriptor,
&identity,
(6, 4),
(6, 4)
)
.unwrap_err()
.code(),
Error::NotFound.code()
);
assert_eq!(
ctx.run_planar_matrix_to_rgb(&[y], dst, descriptor, &identity, (6, 4), (6, 4))
.unwrap_err()
.code(),
Error::Invalid.code()
);
assert_eq!(
ctx.run_planar_matrix_to_rgb(&[y, uv], dst, descriptor, &identity, (7, 4), (6, 4))
.unwrap_err()
.code(),
Error::Invalid.code()
);
assert_eq!(
ctx.run_planar_matrix_to_rgb(&[y, uv], dst, descriptor, &identity, (6, 4), (7, 4))
.unwrap_err()
.code(),
Error::Invalid.code()
);
let wrong_dst = ctx
.create_texture_format(
6,
4,
1,
wgpu::TextureFormat::Rgba16Float,
wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::RENDER_ATTACHMENT,
)
.unwrap();
assert_eq!(
ctx.run_planar_matrix_to_rgb(
&[y, uv],
wrong_dst,
descriptor,
&identity,
(6, 4),
(6, 4)
)
.unwrap_err()
.code(),
Error::Invalid.code()
);
assert_eq!(
ctx.run_planar_matrix_to_rgb_native(&[y, uv], wrong_dst, descriptor, &identity)
.unwrap_err()
.code(),
Error::Invalid.code()
);
assert_eq!(
ctx.run_planar_matrix_to_rgb_native(&[999_999], dst, descriptor, &identity)
.unwrap_err()
.code(),
Error::NotFound.code()
);
assert_eq!(
ctx.run_planar_yuv_to_rgb_scaled(y, uv, dst, &identity, (0, 4), (6, 4))
.unwrap_err()
.code(),
Error::Invalid.code()
);
assert_eq!(
ctx.run_planar_yuv_to_rgb_scaled(999_999, uv, dst, &identity, (6, 4), (6, 4))
.unwrap_err()
.code(),
Error::NotFound.code()
);
for token in [y, uv, dst, wrong_dst] {
ctx.destroy_texture(token);
}
}
}