use azul_core::resources::UpdateImageType;
use azul_core::callbacks::Update;
use azul_core::gl::gl::{RGBA, TEXTURE_2D, UNSIGNED_BYTE};
use azul_core::gl::{GlContextPtr, OptionU8VecRef, Texture, U8VecRef};
use azul_core::geom::PhysicalSizeU32;
use azul_core::refany::RefAny;
use azul_core::resources::ImageRef;
use azul_core::video::VideoFrame;
use azul_css::impl_option_inner; use azul_css::props::basic::ColorU;
use crate::callbacks::CallbackInfo;
pub type OnVideoFrameCallbackType = extern "C" fn(RefAny, CallbackInfo, VideoFrame) -> Update;
impl_widget_callback!(
OnVideoFrame,
OptionOnVideoFrame,
OnVideoFrameCallback,
OnVideoFrameCallbackType
);
azul_core::impl_managed_callback! {
wrapper: OnVideoFrameCallback,
info_ty: CallbackInfo,
return_ty: Update,
default_ret: Update::DoNothing,
invoker_static: ON_VIDEO_FRAME_INVOKER,
invoker_ty: AzOnVideoFrameCallbackInvoker,
thunk_fn: az_on_video_frame_callback_thunk,
setter_fn: AzApp_setOnVideoFrameCallbackInvoker,
from_handle_fn: AzOnVideoFrameCallback_createFromHostHandle,
extra_args: [ frame: VideoFrame ],
}
pub fn invoke_on_frame(
hook: &OptionOnVideoFrame,
info: &mut CallbackInfo,
frame: &VideoFrame,
) -> Update {
match hook {
OptionOnVideoFrame::Some(h) => {
(h.callback.cb)(h.refany.clone(), *info, frame.clone())
}
OptionOnVideoFrame::None => Update::DoNothing,
}
}
pub fn present_frame(
info: &mut CallbackInfo,
dataset: RefAny,
current_id: Option<u32>,
frame: &VideoFrame,
) -> Option<u32> {
use azul_core::resources::{RawImage, RawImageData, RawImageFormat};
let Some(gl) = info.get_gl_context().into_option() else {
if let Some(img) = ImageRef::new_rawimage(RawImage {
pixels: RawImageData::U8(frame.bytes.clone()),
width: frame.width as usize,
height: frame.height as usize,
premultiplied_alpha: false,
data_format: RawImageFormat::RGBA8,
tag: b"azul-capture-frame".to_vec().into(),
}) {
if let Some(node) = info.get_node_id_of_root_dataset(dataset) {
if let Some(nid) = node.node.into_crate_internal() {
info.change_node_image(node.dom, nid, img, UpdateImageType::Content);
}
}
}
return current_id;
};
if let Some(id) = current_id {
upload_rgba(&gl, id, frame);
info.update_all_image_callbacks();
Some(id)
} else {
let tex = Texture::allocate_rgba8(
gl.clone(),
PhysicalSizeU32 {
width: frame.width,
height: frame.height,
},
ColorU {
r: 0,
g: 0,
b: 0,
a: 0,
},
);
let id = tex.texture_id;
upload_rgba(&gl, id, frame);
let image = ImageRef::new_gltexture(tex);
if let Some(node) = info.get_node_id_of_root_dataset(dataset) {
if let Some(nid) = node.node.into_crate_internal() {
info.change_node_image(node.dom, nid, image, UpdateImageType::Content);
}
}
Some(id)
}
}
#[allow(clippy::cast_possible_wrap)] pub fn upload_rgba(gl: &GlContextPtr, texture_id: u32, frame: &VideoFrame) {
gl.bind_texture(TEXTURE_2D, texture_id);
gl.tex_image_2d(
TEXTURE_2D,
0,
RGBA as i32,
frame.width as i32,
frame.height as i32,
0,
RGBA,
UNSIGNED_BYTE,
OptionU8VecRef::Some(U8VecRef::from(frame.bytes.as_ref())),
);
}
#[derive(Debug, Clone, Copy)]
pub struct CaptureVTable {
pub open: fn(index: u32, width: u32, height: u32) -> u64,
pub read: fn(handle: u64, out: &mut Vec<u8>) -> (u32, u32),
pub close: fn(handle: u64),
}
static CAMERA_BACKEND: std::sync::OnceLock<CaptureVTable> = std::sync::OnceLock::new();
static SCREEN_BACKEND: std::sync::OnceLock<CaptureVTable> = std::sync::OnceLock::new();
pub fn register_camera_backend(vtable: CaptureVTable) {
let _ = CAMERA_BACKEND.set(vtable);
}
pub fn register_screen_backend(vtable: CaptureVTable) {
let _ = SCREEN_BACKEND.set(vtable);
}
pub fn camera_backend() -> Option<CaptureVTable> {
CAMERA_BACKEND.get().copied()
}
pub fn screen_backend() -> Option<CaptureVTable> {
SCREEN_BACKEND.get().copied()
}
#[derive(Debug, Clone, Copy)]
pub struct AudioCaptureVTable {
pub open: fn(sample_rate: u32, channels: u16) -> u64,
pub read: fn(handle: u64, out: &mut Vec<f32>) -> u32,
pub close: fn(handle: u64),
}
static MIC_BACKEND: std::sync::OnceLock<AudioCaptureVTable> = std::sync::OnceLock::new();
pub fn register_mic_backend(vtable: AudioCaptureVTable) {
let _ = MIC_BACKEND.set(vtable);
}
pub fn mic_backend() -> Option<AudioCaptureVTable> {
MIC_BACKEND.get().copied()
}
#[cfg(test)]
#[allow(clippy::too_many_lines)] mod autotest_generated {
use std::{
collections::{BTreeMap, HashMap},
panic::{catch_unwind, AssertUnwindSafe},
rc::Rc,
sync::{Arc, Mutex, PoisonError},
};
use azul_core::{
dom::{Dom, DomId, DomNodeId, NodeId, NodeType},
geom::{LogicalRect, OptionLogicalPosition},
gl::{GenericGlContext, OptionGlContextPtr, GLvoid},
hit_test::ScrollPosition,
refany::OptionRefAny,
resources::{DecodedImage, RendererResources},
styled_dom::{NodeHierarchyItemId, StyledDom},
window::{MonitorVec, RawWindowHandle, RendererType},
};
use azul_css::system::SystemStyle;
use rust_fontconfig::FcFontCache;
use super::*;
#[cfg(feature = "icu")]
use crate::icu::IcuLocalizerHandle;
use crate::{
callbacks::{CallbackChange, CallbackInfoRefData, ExternalSystemCallbacks},
solver3::{display_list::DisplayList, layout_tree::LayoutTree},
window::{DomLayoutResult, LayoutWindow},
window_state::FullWindowState,
};
const RECORDED_TEXTURE_ID: u32 = 42;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum GlCall {
GenTextures {
n: i32,
},
BindTexture {
target: u32,
texture: u32,
},
TexImage2d {
target: u32,
level: i32,
internal_format: i32,
width: i32,
height: i32,
border: i32,
format: u32,
ty: u32,
has_pixels: bool,
},
}
static GL_LOG: Mutex<Vec<GlCall>> = Mutex::new(Vec::new());
static GL_SERIAL: Mutex<()> = Mutex::new(());
fn gl_log_push(call: GlCall) {
GL_LOG
.lock()
.unwrap_or_else(PoisonError::into_inner)
.push(call);
}
extern "system" fn rec_gen_textures(n: i32, out: *mut u32) {
gl_log_push(GlCall::GenTextures { n });
for i in 0..n.max(0) {
unsafe { out.add(i as usize).write(RECORDED_TEXTURE_ID + i as u32) };
}
}
extern "system" fn rec_bind_texture(target: u32, texture: u32) {
gl_log_push(GlCall::BindTexture { target, texture });
}
#[allow(clippy::too_many_arguments)] extern "system" fn rec_tex_image_2d(
target: u32,
level: i32,
internal_format: i32,
width: i32,
height: i32,
border: i32,
format: u32,
ty: u32,
pixels: *const GLvoid,
) {
gl_log_push(GlCall::TexImage2d {
target,
level,
internal_format,
width,
height,
border,
format,
ty,
has_pixels: !pixels.is_null(),
});
}
fn null_gl_context() -> GlContextPtr {
let ctx: GenericGlContext = unsafe { core::mem::zeroed() };
GlContextPtr::new(RendererType::Software, Rc::new(ctx))
}
fn recording_gl_context() -> GlContextPtr {
let mut ctx: GenericGlContext = unsafe { core::mem::zeroed() };
ctx.glGenTextures = rec_gen_textures as *const () as *mut azul_core::gl::c_void;
ctx.glBindTexture = rec_bind_texture as *const () as *mut azul_core::gl::c_void;
ctx.glTexImage2D = rec_tex_image_2d as *const () as *mut azul_core::gl::c_void;
GlContextPtr::new(RendererType::Software, Rc::new(ctx))
}
fn with_recorded_gl<R>(f: impl FnOnce(GlContextPtr) -> R) -> (R, Vec<GlCall>) {
let _serial = GL_SERIAL.lock().unwrap_or_else(PoisonError::into_inner);
GL_LOG
.lock()
.unwrap_or_else(PoisonError::into_inner)
.clear();
let out = f(recording_gl_context());
let log = GL_LOG
.lock()
.unwrap_or_else(PoisonError::into_inner)
.clone();
(out, log)
}
fn layout_result(styled_dom: StyledDom) -> DomLayoutResult {
DomLayoutResult {
styled_dom,
layout_tree: LayoutTree {
nodes: Vec::new(),
warm: Vec::new(),
cold: Vec::new(),
root: 0,
dom_to_layout: BTreeMap::new(),
children_arena: Vec::new(),
children_offsets: Vec::new(),
subtree_needs_intrinsic: Vec::new(),
},
calculated_positions: Vec::new(),
viewport: LogicalRect::zero(),
display_list: DisplayList::default(),
scroll_ids: HashMap::new(),
scroll_id_to_node_id: HashMap::new(),
}
}
fn with_callback_info<R>(
styled: Option<StyledDom>,
gl_context: OptionGlContextPtr,
f: impl FnOnce(&mut CallbackInfo) -> R,
) -> (R, Vec<CallbackChange>) {
let mut layout_window =
LayoutWindow::new(FcFontCache::default()).expect("LayoutWindow::new failed");
if let Some(sd) = styled {
layout_window
.layout_results
.insert(DomId::ROOT_ID, layout_result(sd));
}
let renderer_resources = RendererResources::default();
let previous_window_state: Option<FullWindowState> = None;
let current_window_state = FullWindowState::default();
let scroll_states: BTreeMap<DomId, BTreeMap<NodeHierarchyItemId, ScrollPosition>> =
BTreeMap::new();
let window_handle = RawWindowHandle::Unsupported;
let system_callbacks = ExternalSystemCallbacks::rust_internal();
let ref_data = CallbackInfoRefData {
layout_window: &layout_window,
renderer_resources: &renderer_resources,
previous_window_state: &previous_window_state,
current_window_state: ¤t_window_state,
gl_context: &gl_context,
current_scroll_manager: &scroll_states,
current_window_handle: &window_handle,
system_callbacks: &system_callbacks,
system_style: Arc::new(SystemStyle::default()),
monitors: Arc::new(Mutex::new(MonitorVec::from_const_slice(&[]))),
#[cfg(feature = "icu")]
icu_localizer: IcuLocalizerHandle::default(),
ctx: OptionRefAny::None,
};
let changes: Arc<Mutex<Vec<CallbackChange>>> = Arc::new(Mutex::new(Vec::new()));
let mut info = CallbackInfo::new(
&ref_data,
&changes,
DomNodeId {
dom: DomId::ROOT_ID,
node: NodeHierarchyItemId::from_crate_internal(Some(NodeId::new(0))),
},
OptionLogicalPosition::None,
OptionLogicalPosition::None,
);
let out = f(&mut info);
let recorded = core::mem::take(&mut *changes.lock().expect("change log poisoned"));
(out, recorded)
}
#[derive(Debug, Default)]
struct CamState {
_texture_id: Option<u32>,
}
#[derive(Debug, Default)]
struct OtherState {
_unused: u8,
}
fn div_with(ds: Option<RefAny>) -> Dom {
let d = Dom::create_node(NodeType::Div);
match ds {
Some(r) => d.with_dataset(OptionRefAny::Some(r)),
None => d,
}
}
fn dom_with_datasets(first: Option<RefAny>, second: Option<RefAny>) -> StyledDom {
let dom = Dom::create_node(NodeType::Body)
.with_child(div_with(first))
.with_child(div_with(second));
let styled = StyledDom::create_from_dom(dom);
assert_eq!(
styled.node_hierarchy.as_ref().len(),
3,
"fixture must flatten to exactly body + 2 divs"
);
styled
}
fn frame(width: u32, height: u32) -> VideoFrame {
let len = (width as usize) * (height as usize) * 4;
let bytes: Vec<u8> = (0..len).map(|i| (i % 251) as u8).collect();
VideoFrame::new(width, height, bytes.into())
}
fn frame_raw(width: u32, height: u32, bytes: Vec<u8>) -> VideoFrame {
VideoFrame::new(width, height, bytes.into())
}
fn image_installs(
changes: &[CallbackChange],
) -> Vec<(DomId, usize, &ImageRef, UpdateImageType)> {
changes
.iter()
.filter_map(|c| match c {
CallbackChange::ChangeNodeImage {
dom_id,
node_id,
image,
update_type,
} => Some((*dom_id, node_id.index(), image, *update_type)),
_ => None,
})
.collect()
}
fn recomposites(changes: &[CallbackChange]) -> usize {
changes
.iter()
.filter(|c| matches!(c, CallbackChange::UpdateAllImageCallbacks))
.count()
}
#[derive(Debug)]
struct HookLog {
seen: Vec<(u32, u32, usize, Option<u8>)>,
reply: Update,
}
extern "C" fn hook_record(mut data: RefAny, _: CallbackInfo, frame: VideoFrame) -> Update {
let mut reply = Update::DoNothing;
if let Some(mut log) = data.downcast_mut::<HookLog>() {
let bytes = frame.bytes.as_ref();
log.seen
.push((frame.width, frame.height, bytes.len(), bytes.first().copied()));
reply = log.reply;
}
reply
}
extern "C" fn hook_recomposite(_: RefAny, mut info: CallbackInfo, _: VideoFrame) -> Update {
info.update_all_image_callbacks();
Update::RefreshDomAllWindows
}
fn hook(cb: OnVideoFrameCallbackType, data: RefAny) -> OptionOnVideoFrame {
OptionOnVideoFrame::Some(OnVideoFrame {
refany: data,
callback: cb.into(),
})
}
fn hook_seen(data: &mut RefAny) -> Vec<(u32, u32, usize, Option<u8>)> {
data.downcast_ref::<HookLog>()
.expect("payload must still be a HookLog")
.seen
.clone()
}
#[test]
fn invoke_on_frame_without_a_hook_is_do_nothing_and_touches_nothing() {
let (update, changes) = with_callback_info(None, OptionGlContextPtr::None, |info| {
invoke_on_frame(&OptionOnVideoFrame::None, info, &frame(2, 2))
});
assert_eq!(
update,
Update::DoNothing,
"an unset on_frame hook must be a no-op"
);
assert!(
changes.is_empty(),
"an unset hook must not record any change, got {changes:?}"
);
}
#[test]
fn invoke_on_frame_returns_the_hooks_update_verbatim() {
for reply in [
Update::DoNothing,
Update::RefreshDom,
Update::RefreshDomAllWindows,
] {
let data = RefAny::new(HookLog {
seen: Vec::new(),
reply,
});
let h = hook(hook_record, data);
let (update, _) = with_callback_info(None, OptionGlContextPtr::None, |info| {
invoke_on_frame(&h, info, &frame(1, 1))
});
assert_eq!(
update, reply,
"invoke_on_frame must return the user's Update unchanged"
);
}
}
#[test]
fn invoke_on_frame_forwards_every_frame_into_the_hooks_shared_refany() {
let mut data = RefAny::new(HookLog {
seen: Vec::new(),
reply: Update::RefreshDom,
});
let h = hook(hook_record, data.clone());
with_callback_info(None, OptionGlContextPtr::None, |info| {
for (w, hgt) in [(1_u32, 1_u32), (2, 3), (4, 4)] {
invoke_on_frame(&h, info, &frame(w, hgt));
}
});
assert_eq!(
hook_seen(&mut data),
vec![
(1, 1, 4, Some(0)),
(2, 3, 24, Some(0)),
(4, 4, 64, Some(0)),
],
"every frame must reach the hook, in order, with its bytes intact"
);
}
#[test]
fn invoke_on_frame_forwards_degenerate_frames_unvalidated_and_without_panicking() {
let mut data = RefAny::new(HookLog {
seen: Vec::new(),
reply: Update::DoNothing,
});
let h = hook(hook_record, data.clone());
with_callback_info(None, OptionGlContextPtr::None, |info| {
invoke_on_frame(&h, info, &frame_raw(0, 0, Vec::new()));
invoke_on_frame(&h, info, &frame_raw(9, 9, vec![7, 8, 9]));
invoke_on_frame(&h, info, &frame_raw(u32::MAX, u32::MAX, Vec::new()));
invoke_on_frame(&h, info, &frame_raw(u32::MAX, 1, vec![255]));
});
assert_eq!(
hook_seen(&mut data),
vec![
(0, 0, 0, None),
(9, 9, 3, Some(7)),
(u32::MAX, u32::MAX, 0, None),
(u32::MAX, 1, 1, Some(255)),
],
"invoke_on_frame must forward frames verbatim, without validating them"
);
}
#[test]
fn invoke_on_frame_hook_writes_through_the_shared_callback_info() {
let h = hook(hook_recomposite, RefAny::new(OtherState::default()));
let (update, changes) = with_callback_info(None, OptionGlContextPtr::None, |info| {
invoke_on_frame(&h, info, &frame(1, 1))
});
assert_eq!(update, Update::RefreshDomAllWindows);
assert_eq!(
recomposites(&changes),
1,
"a change made by the hook must be visible to the widget's writeback"
);
}
#[test]
fn upload_rgba_forwards_the_texture_id_and_the_rgba8_constants() {
for id in [0_u32, 1, 7, u32::MAX] {
let ((), log) = with_recorded_gl(|gl| upload_rgba(&gl, id, &frame(2, 2)));
assert_eq!(
log,
vec![
GlCall::BindTexture {
target: TEXTURE_2D,
texture: id,
},
GlCall::TexImage2d {
target: TEXTURE_2D,
level: 0,
internal_format: RGBA as i32,
width: 2,
height: 2,
border: 0,
format: RGBA,
ty: UNSIGNED_BYTE,
has_pixels: true,
},
],
"upload_rgba must bind exactly texture {id} and upload tightly-packed RGBA8"
);
}
}
#[test]
fn upload_rgba_zero_sized_frame_is_forwarded_as_a_0x0_upload() {
let ((), log) = with_recorded_gl(|gl| upload_rgba(&gl, 3, &frame_raw(0, 0, Vec::new())));
assert_eq!(
log,
vec![
GlCall::BindTexture {
target: TEXTURE_2D,
texture: 3,
},
GlCall::TexImage2d {
target: TEXTURE_2D,
level: 0,
internal_format: RGBA as i32,
width: 0,
height: 0,
border: 0,
format: RGBA,
ty: UNSIGNED_BYTE,
has_pixels: true,
},
],
"a 0x0 frame must still be a well-formed (if empty) glTexImage2D, not a panic"
);
}
#[test]
fn upload_rgba_dimensions_above_i32_max_wrap_to_negative_glsizei() {
let cases: [(u32, u32, i32, i32); 4] = [
(i32::MAX as u32, 1, i32::MAX, 1),
(i32::MAX as u32 + 1, 1, i32::MIN, 1),
(u32::MAX, u32::MAX, -1, -1),
(u32::MAX - 1, 2, -2, 2),
];
for (w, h, want_w, want_h) in cases {
let ((), log) = with_recorded_gl(|gl| upload_rgba(&gl, 1, &frame_raw(w, h, Vec::new())));
let tex = log
.iter()
.find_map(|c| match c {
GlCall::TexImage2d { width, height, .. } => Some((*width, *height)),
_ => None,
})
.expect("upload_rgba must always call glTexImage2D");
assert_eq!(
tex,
(want_w, want_h),
"{w}x{h} must cast to GLsizei {want_w}x{want_h}"
);
}
}
#[test]
fn upload_rgba_against_an_unloaded_driver_is_a_silent_no_op() {
let gl = null_gl_context();
upload_rgba(&gl, 0, &frame(2, 2));
upload_rgba(&gl, u32::MAX, &frame_raw(u32::MAX, u32::MAX, Vec::new()));
upload_rgba(&gl, 1, &frame_raw(0, 0, Vec::new()));
}
#[test]
fn present_frame_without_gl_installs_a_raw_image_on_the_dataset_node() {
let ds = RefAny::new(CamState::default());
let styled = dom_with_datasets(Some(ds.clone()), None);
let (id, changes) = with_callback_info(Some(styled), OptionGlContextPtr::None, |info| {
present_frame(info, ds.clone(), None, &frame(4, 4))
});
assert_eq!(id, None, "the cpurender path must not invent a texture id");
let installs = image_installs(&changes);
assert_eq!(installs.len(), 1, "exactly one image install per frame");
let (dom_id, node_idx, image, update_type) = installs[0];
assert_eq!(dom_id, DomId::ROOT_ID);
assert_eq!(node_idx, 1, "the image must land on the dataset's node");
assert_eq!(update_type, UpdateImageType::Content);
match image.get_data() {
DecodedImage::Raw((descriptor, _)) => {
assert_eq!(
(descriptor.width, descriptor.height),
(4, 4),
"the installed image must keep the frame's dimensions"
);
}
other => panic!("cpurender must install a raw image, got {other:?}"),
}
assert_eq!(
recomposites(&changes),
0,
"the CPU path swaps the node's image instead of recompositing a texture"
);
}
#[test]
fn present_frame_without_gl_returns_the_current_id_verbatim() {
for current in [None, Some(0_u32), Some(1), Some(u32::MAX)] {
let ds = RefAny::new(CamState::default());
let styled = dom_with_datasets(Some(ds.clone()), None);
let (id, changes) =
with_callback_info(Some(styled), OptionGlContextPtr::None, |info| {
present_frame(info, ds.clone(), current, &frame(2, 2))
});
assert_eq!(
id, current,
"the cpurender path must round-trip current_id ({current:?}) untouched"
);
assert_eq!(
image_installs(&changes).len(),
1,
"the CPU path re-installs the image on *every* frame"
);
}
}
#[test]
fn present_frame_without_gl_and_without_a_matching_dataset_installs_nothing() {
let node_ds = RefAny::new(OtherState::default());
let styled = dom_with_datasets(Some(node_ds), None);
let search = RefAny::new(CamState::default());
let (id, changes) = with_callback_info(Some(styled), OptionGlContextPtr::None, |info| {
present_frame(info, search.clone(), Some(9), &frame(2, 2))
});
assert_eq!(id, Some(9), "a failed node lookup must not lose the id");
assert!(
changes.is_empty(),
"no node owns the dataset, so nothing may be installed: {changes:?}"
);
}
#[test]
fn present_frame_without_gl_and_without_any_layout_result_installs_nothing() {
let ds = RefAny::new(CamState::default());
let (id, changes) = with_callback_info(None, OptionGlContextPtr::None, |info| {
present_frame(info, ds.clone(), Some(3), &frame(2, 2))
});
assert_eq!(id, Some(3));
assert!(
changes.is_empty(),
"an empty window must not be written to: {changes:?}"
);
}
#[test]
fn present_frame_without_gl_rejects_a_frame_whose_byte_count_disagrees_with_its_size() {
for (w, h, bytes) in [
(4_u32, 4_u32, vec![0_u8; 3]), (4, 4, vec![0_u8; 63]), (4, 4, vec![0_u8; 65]), (2, 2, Vec::new()), ] {
let ds = RefAny::new(CamState::default());
let styled = dom_with_datasets(Some(ds.clone()), None);
let (id, changes) =
with_callback_info(Some(styled), OptionGlContextPtr::None, |info| {
present_frame(info, ds.clone(), Some(5), &frame_raw(w, h, bytes.clone()))
});
assert_eq!(id, Some(5), "a rejected frame must not disturb the id");
assert!(
changes.is_empty(),
"a {w}x{h} frame with {} bytes must be rejected, not installed: {changes:?}",
bytes.len()
);
}
}
#[test]
fn present_frame_without_gl_installs_a_degenerate_image_for_a_0x0_frame() {
let ds = RefAny::new(CamState::default());
let styled = dom_with_datasets(Some(ds.clone()), None);
let (id, changes) = with_callback_info(Some(styled), OptionGlContextPtr::None, |info| {
present_frame(info, ds.clone(), Some(2), &frame_raw(0, 0, Vec::new()))
});
assert_eq!(id, Some(2));
let installs = image_installs(&changes);
assert_eq!(installs.len(), 1);
match installs[0].2.get_data() {
DecodedImage::Raw((descriptor, _)) => {
assert_eq!((descriptor.width, descriptor.height), (0, 0));
}
other => panic!("expected a raw image, got {other:?}"),
}
}
#[test]
fn present_frame_without_gl_survives_dimensions_whose_byte_count_overflows_usize() {
let ds = RefAny::new(CamState::default());
let styled = dom_with_datasets(Some(ds.clone()), None);
let (result, _changes) = with_callback_info(Some(styled), OptionGlContextPtr::None, |info| {
catch_unwind(AssertUnwindSafe(|| {
present_frame(
info,
ds.clone(),
Some(11),
&frame_raw(1_u32 << 31, 1_u32 << 31, Vec::new()),
)
}))
});
match result {
Ok(id) => assert_eq!(
id,
Some(11),
"the cpurender path must always hand back current_id"
),
Err(_) => eprintln!(
"NOTE: present_frame panicked (usize overflow of width*height*4) for a \
2^31 x 2^31 frame — a malformed capture backend can take the process down"
),
}
}
#[test]
fn present_frame_installs_exactly_one_image_when_two_nodes_share_a_dataset_type() {
let styled = dom_with_datasets(
Some(RefAny::new(CamState::default())),
Some(RefAny::new(CamState::default())),
);
let search = RefAny::new(CamState::default());
let (id, changes) = with_callback_info(Some(styled), OptionGlContextPtr::None, |info| {
present_frame(info, search.clone(), Some(4), &frame(2, 2))
});
assert_eq!(id, Some(4));
let installs = image_installs(&changes);
assert_eq!(
installs.len(),
1,
"a frame must never be installed on two nodes at once: {changes:?}"
);
assert!(
installs[0].1 == 1 || installs[0].1 == 2,
"the image must land on a node that owns a dataset, not on node {}",
installs[0].1
);
}
#[test]
fn present_frame_matches_datasets_by_type_id_not_by_identity() {
let node_ds = RefAny::new(CamState::default());
let styled = dom_with_datasets(Some(node_ds), None);
let unrelated = RefAny::new(CamState::default()); let (id, changes) = with_callback_info(Some(styled), OptionGlContextPtr::None, |info| {
present_frame(info, unrelated.clone(), None, &frame(2, 2))
});
assert_eq!(id, None);
assert_eq!(
image_installs(&changes).len(),
1,
"an unrelated RefAny of the same type still resolves to the node"
);
}
#[test]
fn present_frame_with_gl_first_frame_allocates_uploads_and_installs_once() {
let ds = RefAny::new(CamState::default());
let styled = dom_with_datasets(Some(ds.clone()), None);
let ((id, changes), log) = with_recorded_gl(|gl| {
with_callback_info(Some(styled), OptionGlContextPtr::Some(gl), |info| {
present_frame(info, ds.clone(), None, &frame(4, 4))
})
});
assert_eq!(
id,
Some(RECORDED_TEXTURE_ID),
"the first frame must hand back the texture name the driver allocated"
);
let installs = image_installs(&changes);
assert_eq!(installs.len(), 1, "the node's image is installed ONCE");
assert_eq!(installs[0].1, 1);
assert_eq!(installs[0].3, UpdateImageType::Content);
match installs[0].2.get_data() {
DecodedImage::Gl(texture) => {
assert_eq!(texture.texture_id, RECORDED_TEXTURE_ID);
assert_eq!(
(texture.size.width, texture.size.height),
(4, 4),
"the external texture must be sized like the frame"
);
}
other => panic!("the GL path must install an external texture, got {other:?}"),
}
assert_eq!(
recomposites(&changes),
0,
"the install itself rebuilds the display list; no extra recomposite needed"
);
assert_eq!(
log.iter()
.filter(|c| matches!(c, GlCall::GenTextures { .. }))
.count(),
1,
"exactly one texture may be allocated for the first frame"
);
assert_eq!(
log.last(),
Some(&GlCall::TexImage2d {
target: TEXTURE_2D,
level: 0,
internal_format: RGBA as i32,
width: 4,
height: 4,
border: 0,
format: RGBA,
ty: UNSIGNED_BYTE,
has_pixels: true,
}),
"the last GL call must be the pixel upload, not the empty allocation"
);
assert!(
log.contains(&GlCall::BindTexture {
target: TEXTURE_2D,
texture: RECORDED_TEXTURE_ID,
}),
"the upload must target the freshly allocated texture: {log:?}"
);
}
#[test]
fn present_frame_with_gl_later_frames_reupload_into_the_same_texture() {
let ds = RefAny::new(CamState::default());
let styled = dom_with_datasets(Some(ds.clone()), None);
let ((id, changes), log) = with_recorded_gl(|gl| {
with_callback_info(Some(styled), OptionGlContextPtr::Some(gl), |info| {
present_frame(info, ds.clone(), Some(RECORDED_TEXTURE_ID), &frame(4, 4))
})
});
assert_eq!(id, Some(RECORDED_TEXTURE_ID), "the texture id must stay stable");
assert!(
image_installs(&changes).is_empty(),
"steady-state frames must NOT re-install the node's image (that would \
rebuild the display list every frame): {changes:?}"
);
assert_eq!(
recomposites(&changes),
1,
"a steady-state frame recomposites exactly once"
);
assert_eq!(
log,
vec![
GlCall::BindTexture {
target: TEXTURE_2D,
texture: RECORDED_TEXTURE_ID,
},
GlCall::TexImage2d {
target: TEXTURE_2D,
level: 0,
internal_format: RGBA as i32,
width: 4,
height: 4,
border: 0,
format: RGBA,
ty: UNSIGNED_BYTE,
has_pixels: true,
},
],
"a steady-state frame must be exactly one re-upload — no new texture"
);
}
#[test]
fn present_frame_with_gl_round_trips_extreme_texture_ids() {
for current in [Some(0_u32), Some(u32::MAX)] {
let ds = RefAny::new(CamState::default());
let styled = dom_with_datasets(Some(ds.clone()), None);
let ((id, changes), log) = with_recorded_gl(|gl| {
with_callback_info(Some(styled), OptionGlContextPtr::Some(gl), |info| {
present_frame(info, ds.clone(), current, &frame(1, 1))
})
});
assert_eq!(
id, current,
"a stored texture id must survive the writeback unchanged"
);
assert_eq!(recomposites(&changes), 1);
assert!(
log.contains(&GlCall::BindTexture {
target: TEXTURE_2D,
texture: current.expect("current is Some"),
}),
"the id must be forwarded to glBindTexture verbatim: {log:?}"
);
}
}
#[test]
fn present_frame_with_gl_first_frame_without_a_matching_node_still_returns_the_id() {
let styled = dom_with_datasets(Some(RefAny::new(OtherState::default())), None);
let search = RefAny::new(CamState::default());
let ((id, changes), log) = with_recorded_gl(|gl| {
with_callback_info(Some(styled), OptionGlContextPtr::Some(gl), |info| {
present_frame(info, search.clone(), None, &frame(2, 2))
})
});
assert_eq!(id, Some(RECORDED_TEXTURE_ID));
assert!(
changes.is_empty(),
"nothing may be installed when no node owns the dataset: {changes:?}"
);
assert_eq!(
log.iter()
.filter(|c| matches!(c, GlCall::GenTextures { .. }))
.count(),
1,
"a texture is allocated even though it can never be shown"
);
}
fn open_a(index: u32, width: u32, height: u32) -> u64 {
u64::from(index) + u64::from(width) * 3 + u64::from(height)
}
fn read_a(handle: u64, out: &mut Vec<u8>) -> (u32, u32) {
out.clear();
out.extend_from_slice(&[1, 2, 3, 4]);
(handle as u32, 1)
}
fn close_a(_handle: u64) {}
fn open_b(index: u32, width: u32, height: u32) -> u64 {
u64::from(index) * 7 + u64::from(width) + u64::from(height) * 11
}
fn read_b(_handle: u64, out: &mut Vec<u8>) -> (u32, u32) {
out.push(9);
(0, 0)
}
fn close_b(_handle: u64) {
let _ = core::hint::black_box(1_u8);
}
fn vtable_a() -> CaptureVTable {
CaptureVTable {
open: open_a,
read: read_a,
close: close_a,
}
}
fn vtable_b() -> CaptureVTable {
CaptureVTable {
open: open_b,
read: read_b,
close: close_b,
}
}
fn same_vtable(a: CaptureVTable, b: CaptureVTable) -> bool {
a.open as usize == b.open as usize
&& a.read as usize == b.read as usize
&& a.close as usize == b.close as usize
}
#[test]
fn register_camera_backend_is_first_wins_and_never_overwritten() {
let before = camera_backend();
register_camera_backend(vtable_a());
let first = camera_backend().expect("a backend is registered after the first call");
register_camera_backend(vtable_b());
register_camera_backend(vtable_b());
let after = camera_backend().expect("the backend must still be there");
assert!(
same_vtable(first, after),
"the first registration must win; a later one must not replace it"
);
if let Some(pre) = before {
assert!(
same_vtable(pre, after),
"a backend registered before this test must not have been replaced"
);
} else {
assert!(
same_vtable(vtable_a(), after),
"camera_backend() must hand back exactly the vtable that was registered"
);
assert_eq!((after.open)(1, 2, 3), open_a(1, 2, 3));
assert_eq!((after.open)(u32::MAX, u32::MAX, u32::MAX), open_a(u32::MAX, u32::MAX, u32::MAX));
let mut buf = vec![0_u8; 8];
assert_eq!((after.read)(u64::from(u32::MAX), &mut buf), (u32::MAX, 1));
assert_eq!(buf, vec![1, 2, 3, 4], "read must be able to resize `out`");
(after.close)(0);
(after.close)(u64::MAX);
}
}
#[test]
fn register_screen_backend_is_independent_of_the_camera_backend() {
let before = screen_backend();
register_screen_backend(vtable_b());
let after = screen_backend().expect("a screen backend is registered");
if let Some(pre) = before {
assert!(same_vtable(pre, after), "first registration wins");
} else {
assert!(
same_vtable(vtable_b(), after),
"the screen registry must hand back the screen vtable"
);
if let Some(cam) = camera_backend() {
assert!(
!same_vtable(cam, vtable_b()),
"the camera registry must not pick up the screen vtable"
);
}
let mut buf = Vec::new();
assert_eq!((after.read)(0, &mut buf), (0, 0));
}
}
fn mic_open(sample_rate: u32, channels: u16) -> u64 {
u64::from(sample_rate) * 2 + u64::from(channels)
}
fn mic_read(handle: u64, out: &mut Vec<f32>) -> u32 {
out.clear();
out.extend_from_slice(&[f32::NAN, f32::INFINITY, f32::NEG_INFINITY, -0.0]);
(handle % 3) as u32
}
fn mic_close(_handle: u64) {}
fn mic_open_other(sample_rate: u32, channels: u16) -> u64 {
u64::from(sample_rate) ^ u64::from(channels)
}
fn mic_read_other(_handle: u64, out: &mut Vec<f32>) -> u32 {
out.push(1.0);
0
}
fn mic_close_other(_handle: u64) {
let _ = core::hint::black_box(2_u8);
}
#[test]
fn register_mic_backend_is_first_wins_and_passes_f32_samples_through() {
let before = mic_backend();
register_mic_backend(AudioCaptureVTable {
open: mic_open,
read: mic_read,
close: mic_close,
});
register_mic_backend(AudioCaptureVTable {
open: mic_open_other,
read: mic_read_other,
close: mic_close_other,
});
let vt = mic_backend().expect("a mic backend is registered");
if before.is_none() {
assert_eq!(
vt.open as usize, mic_open as usize,
"the first mic registration must win"
);
assert_eq!((vt.open)(0, 0), 0);
assert_eq!((vt.open)(u32::MAX, u16::MAX), mic_open(u32::MAX, u16::MAX));
let mut samples = Vec::new();
let frames = (vt.read)(4, &mut samples);
assert_eq!(frames, 1, "the frame count must be the vtable's, verbatim");
assert_eq!(samples.len(), 4);
assert!(samples[0].is_nan(), "a NaN sample must not be normalised");
assert_eq!(samples[1], f32::INFINITY);
assert_eq!(samples[2], f32::NEG_INFINITY);
assert!(
samples[3] == 0.0 && samples[3].is_sign_negative(),
"-0.0 must keep its sign bit"
);
assert_eq!((vt.read)(3, &mut samples), 0);
(vt.close)(u64::MAX);
}
}
}