code_editor/Cargo.toml | 1 - code_editor/src/lib.rs | 3 - draw/src/cx_2d.rs | 83 + draw/src/cx_draw.rs | 43 + draw/src/draw_list_2d.rs | 22 + draw/src/geometry/geometry_gen.rs | 7 +- draw/src/image_cache.rs | 51 +- draw/src/lib.rs | 1 - draw/src/shader/draw_text.rs | 40 +- draw/src/shader/mod.rs | 1 - draw/src/text/fonts.rs | 2 +- draw/src/text/mod.rs | 1 - draw/src/text/slug_atlas.rs | 2 +- draw/src/turtle.rs | 3469 ++++- draw/src/vector/triangulate.rs | 378 +- libs/error_log/src/lib.rs | 17 +- platform/network/src/backend.rs | 6 + platform/network/src/backend/apple/http.rs | 2 +- platform/network/src/http_server.rs | 15 +- platform/script/derive/src/derive_scriptable.rs | 19 +- platform/script/src/lib.rs | 4 + platform/script/src/shader_calls.rs | 92 +- platform/script/src/shader_hlsl.rs | 9 +- platform/script/src/shader_metal.rs | 18 +- platform/script/src/shader_wgsl.rs | 12 +- platform/script/tests/hook_scope.rs | 8 +- platform/src/action.rs | 12 +- platform/src/app_main.rs | 110 +- platform/src/cx.rs | 466 +- platform/src/cx_api.rs | 368 +- platform/src/draw_list.rs | 2598 +++- platform/src/draw_pass.rs | 35 +- platform/src/draw_vars.rs | 103 +- platform/src/geometry.rs | 588 +- platform/src/gpu_texture.rs | 2 +- platform/src/id_pool.rs | 71 +- platform/src/lib.rs | 26 +- platform/src/log.rs | 111 +- platform/src/os/apple/apple_game_input.rs | 6 + platform/src/os/apple/apple_sys.rs | 1 + platform/src/os/apple/macos/macos_app.rs | 384 +- platform/src/os/apple/metal.rs | 3347 +++- platform/src/os/cx_native.rs | 5 +- platform/src/os/cx_shared.rs | 202 +- platform/src/os/gpusim/event_loop.rs | 24 +- platform/src/os/gpusim/mod.rs | 8 + platform/src/os/gpusim/raster.rs | 11 + platform/src/os/linux/opengl.rs | 856 +- platform/src/os/linux/vulkan.rs | 752 +- platform/src/os/linux/vulkan_linux.rs | 4 - platform/src/os/linux/wayland/linux_wayland.rs | 15 +- platform/src/os/linux/wayland/opengl_wayland.rs | 67 +- platform/src/os/linux/wayland/wayland_state.rs | 37 +- platform/src/os/web/web.js | 1409 +- platform/src/os/web/web.rs | 212 +- platform/src/os/web/web_gl.rs | 284 +- platform/src/os/web/web_worker.js | 76 +- platform/src/os/windows/d3d11.rs | 1323 +- platform/src/remote.rs | 1297 +- platform/src/retained_instances.rs | 3 - platform/src/script/res.rs | 245 +- platform/src/sploded.rs | 59 +- platform/src/texture.rs | 416 +- platform/src/thread.rs | 2816 +++- platform/src/thread/ui_hang/linux.rs | 2 +- platform/src/thread/ui_hang/macos.rs | 22 +- platform/src/thread/ui_hang/native.rs | 3 + platform/src/thread/ui_hang/windows.rs | 2 +- platform/src/web_socket.rs | 45 +- platform/src/window.rs | 129 +- platform/tests/texture_lifetime.rs | 143 +- platform/video/src/stream_debug.rs | 2 +- platform/video/tests/stream_codec.rs | 85 +- widgets/derive_widget/src/derive_widget.rs | 4 +- widgets/src/animator.rs | 138 +- widgets/src/combo_box.rs | 10 + widgets/src/desktop_style.rs | 4 +- widgets/src/drop_down.rs | 96 + widgets/src/font_policy.rs | 25 + widgets/src/grid.rs | 1 + widgets/src/image.rs | 3 + widgets/src/lib.rs | 18 +- widgets/src/map/archive.rs | 544 +- widgets/src/map/geometry.rs | 34 +- widgets/src/map/label.rs | 48 +- widgets/src/map/tile.rs | 6890 ++++++--- widgets/src/map/tile_draw.rs | 2 - widgets/src/map/view.rs | 17896 ++++++++++++++-------- widgets/src/menu_bar.rs | 18 +- widgets/src/screen_cap.rs | 513 +- widgets/src/slider.rs | 6 + widgets/src/splash.rs | 280 +- widgets/src/tweaker.rs | 221 +- widgets/src/view.rs | 105 +- widgets/src/widget.rs | 5 +- widgets/src/widget_async.rs | 269 +- widgets/src/window.rs | 601 +- widgets/src/window_voice_input.rs | 17 +- 98 files changed, 38159 insertions(+), 12677 deletions(-) Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
217 lines
8.5 KiB
Rust
217 lines
8.5 KiB
Rust
//! Round-trip test for the low-latency hardware stream encoder/decoder
|
|
//! (`VideoStreamEncoder` / `VideoStreamDecoder`, VideoToolbox on macOS /
|
|
//! the H.264 MFTs on Windows). Runs wherever a backend exists; on Windows
|
|
//! that is a real Media Foundation pass (run it on a fleet box). The
|
|
//! captured-stream test replays access units another machine's encoder
|
|
//! produced (`MAKEPAD_H264_DEBUG` dumps them next to its trace) so a
|
|
//! cross-platform wire problem can be reproduced offline.
|
|
|
|
use makepad_video::{
|
|
annex_b, StreamVideoCodec, VideoStreamDecoder, VideoStreamEncoder, VideoStreamEncoderOptions,
|
|
};
|
|
|
|
const WIDTH: u32 = 320;
|
|
const HEIGHT: u32 = 240;
|
|
const FRAME_COUNT: usize = 30;
|
|
const FPS: u32 = 30;
|
|
const HNS_PER_FRAME: i64 = 10_000_000 / FPS as i64;
|
|
|
|
/// A moving diagonal gradient — visually distinct frame to frame (encoder
|
|
/// motion estimation actually has something to do) and cheap to generate.
|
|
fn synthetic_frame_rgb8(frame_index: usize) -> Vec<u8> {
|
|
let mut out = vec![0u8; WIDTH as usize * HEIGHT as usize * 3];
|
|
let shift = (frame_index * 4) as i32;
|
|
for y in 0..HEIGHT as usize {
|
|
for x in 0..WIDTH as usize {
|
|
let idx = (y * WIDTH as usize + x) * 3;
|
|
out[idx] = ((x as i32 + shift) % 256) as u8;
|
|
out[idx + 1] = (y % 256) as u8;
|
|
out[idx + 2] = (((x + y) as i32 + shift * 2) % 256) as u8;
|
|
}
|
|
}
|
|
out
|
|
}
|
|
|
|
fn psnr(a: &[u8], b: &[u8]) -> f64 {
|
|
assert_eq!(a.len(), b.len());
|
|
let sum_sq: f64 = a
|
|
.iter()
|
|
.zip(b.iter())
|
|
.map(|(&x, &y)| {
|
|
let d = x as f64 - y as f64;
|
|
d * d
|
|
})
|
|
.sum();
|
|
let mse = sum_sq / a.len() as f64;
|
|
if mse <= 0.0 {
|
|
return 100.0;
|
|
}
|
|
20.0 * 255f64.log10() - 10.0 * mse.log10()
|
|
}
|
|
|
|
/// `MAKEPAD_H264_SAMPLE_DIR=<dir>` holds `*.h264` access units (one file
|
|
/// each, sorted by name = send order); every one is pushed and the decoder
|
|
/// must yield at least one picture per AU after the first two.
|
|
#[cfg(any(target_os = "macos", target_os = "windows"))]
|
|
#[test]
|
|
fn decode_captured_access_units() {
|
|
let Ok(dir) = std::env::var("MAKEPAD_H264_SAMPLE_DIR") else {
|
|
eprintln!("decode_captured_access_units: MAKEPAD_H264_SAMPLE_DIR unset, skipping");
|
|
return;
|
|
};
|
|
let mut files: Vec<_> = std::fs::read_dir(&dir)
|
|
.expect("sample dir")
|
|
.filter_map(|e| e.ok().map(|e| e.path()))
|
|
.filter(|p| p.extension().is_some_and(|x| x == "h264"))
|
|
.collect();
|
|
files.sort();
|
|
assert!(!files.is_empty(), "no *.h264 access units in {dir}");
|
|
let mut decoder = VideoStreamDecoder::new(StreamVideoCodec::H264).expect("decoder creation");
|
|
let mut streamed = 0usize;
|
|
let mut per_au = Vec::new();
|
|
for (index, path) in files.iter().enumerate() {
|
|
let au = std::fs::read(path).expect("read au");
|
|
let frames = decoder.push_packet(&au, index as i64 * HNS_PER_FRAME).expect("decode packet").len();
|
|
per_au.push(format!("au{index}:{frames}"));
|
|
streamed += frames;
|
|
}
|
|
let flushed = decoder.flush().expect("flush").len();
|
|
eprintln!("captured stream: {streamed} pictures while streaming + {flushed} on flush [{}]", per_au.join(" "));
|
|
// Live use never flushes: every picture but the last one or two must
|
|
// come out while the stream is still running.
|
|
assert!(
|
|
streamed + 2 >= files.len(),
|
|
"only {streamed} of {} pictures came out while streaming",
|
|
files.len()
|
|
);
|
|
}
|
|
|
|
#[cfg(any(target_os = "macos", target_os = "windows"))]
|
|
#[test]
|
|
fn request_keyframe_forces_the_next_packet() {
|
|
let mut encoder = VideoStreamEncoder::new(VideoStreamEncoderOptions {
|
|
codec: StreamVideoCodec::H264,
|
|
width: WIDTH,
|
|
height: HEIGHT,
|
|
fps: FPS,
|
|
bitrate_kbps: 4_000,
|
|
keyint: 300,
|
|
low_latency: true,
|
|
})
|
|
.expect("encoder creation");
|
|
for index in 0..4 {
|
|
encoder.push_frame_rgb8(&synthetic_frame_rgb8(index), index as i64 * HNS_PER_FRAME).expect("encode");
|
|
}
|
|
encoder.request_keyframe();
|
|
let forced = encoder.push_frame_rgb8(&synthetic_frame_rgb8(4), 4 * HNS_PER_FRAME).expect("forced keyframe encode");
|
|
assert!(forced.iter().any(|p| p.is_key), "request_keyframe() did not force a keyframe");
|
|
}
|
|
|
|
#[cfg(any(target_os = "macos", target_os = "windows"))]
|
|
#[test]
|
|
fn encode_decode_round_trip_psnr_and_keyframes() {
|
|
let mut encoder = VideoStreamEncoder::new(VideoStreamEncoderOptions {
|
|
codec: StreamVideoCodec::H264,
|
|
width: WIDTH,
|
|
height: HEIGHT,
|
|
fps: FPS,
|
|
bitrate_kbps: 4_000,
|
|
keyint: FRAME_COUNT as u32, // one GOP for this test, forced keyframe below exercises a second
|
|
low_latency: true,
|
|
})
|
|
.expect("encoder creation");
|
|
|
|
let sources: Vec<Vec<u8>> = (0..FRAME_COUNT).map(synthetic_frame_rgb8).collect();
|
|
|
|
let mut packets = Vec::new();
|
|
for (index, frame) in sources.iter().enumerate() {
|
|
let pts = index as i64 * HNS_PER_FRAME;
|
|
let mut produced = encoder.push_frame_rgb8(frame, pts).expect("encode frame");
|
|
packets.append(&mut produced);
|
|
}
|
|
|
|
assert!(!packets.is_empty(), "encoder produced no packets at all");
|
|
let first = &packets[0];
|
|
assert!(first.is_key, "the very first packet must be a keyframe");
|
|
// The Media Foundation encoder opens every access unit with an access
|
|
// unit delimiter (NAL 9); VideoToolbox does not. Neither carries
|
|
// meaning for the decoder, so the parameter sets are checked after it.
|
|
let first_nals: Vec<&[u8]> = annex_b::split_annex_b(&first.data)
|
|
.into_iter()
|
|
.filter(|nal| annex_b::nal_unit_type(nal) != 9)
|
|
.collect();
|
|
assert!(!first_nals.is_empty(), "keyframe packet has no NAL units");
|
|
assert_eq!(
|
|
annex_b::nal_unit_type(first_nals[0]),
|
|
annex_b::NAL_TYPE_SPS,
|
|
"a keyframe packet must start with SPS"
|
|
);
|
|
assert!(
|
|
first_nals.iter().any(|nal| annex_b::nal_unit_type(nal) == annex_b::NAL_TYPE_PPS),
|
|
"a keyframe packet must carry a PPS"
|
|
);
|
|
assert!(
|
|
first_nals.iter().any(|nal| annex_b::nal_unit_type(nal) == annex_b::NAL_TYPE_IDR),
|
|
"a keyframe packet must carry an IDR slice"
|
|
);
|
|
|
|
// One more frame past the GOP so the decoder has a next access unit to
|
|
// close the last picture with (the forced-keyframe behaviour has its
|
|
// own test).
|
|
encoder.request_keyframe();
|
|
let extra_pts = FRAME_COUNT as i64 * HNS_PER_FRAME;
|
|
let extra_frame = synthetic_frame_rgb8(FRAME_COUNT);
|
|
let forced = encoder.push_frame_rgb8(&extra_frame, extra_pts).expect("extra frame encode");
|
|
|
|
// Decode everything (including the extra packet) back.
|
|
let mut decoder = VideoStreamDecoder::new(StreamVideoCodec::H264).expect("decoder creation");
|
|
let mut decoded_by_pts = std::collections::HashMap::new();
|
|
let mut streamed = 0usize;
|
|
for packet in packets.iter().chain(forced.iter()) {
|
|
for frame in decoder.push_packet(&packet.data, packet.pts_100ns).expect("decode packet") {
|
|
streamed += 1;
|
|
decoded_by_pts.insert(frame.pts_100ns, frame);
|
|
}
|
|
}
|
|
let flushed = decoder.flush().expect("decoder flush");
|
|
eprintln!("round trip: {streamed} pictures while streaming, {} on flush", flushed.len());
|
|
for frame in flushed {
|
|
decoded_by_pts.insert(frame.pts_100ns, frame);
|
|
}
|
|
|
|
assert!(
|
|
decoded_by_pts.len() >= FRAME_COUNT - 1,
|
|
"decoded {} frames, expected at least {}",
|
|
decoded_by_pts.len(),
|
|
FRAME_COUNT - 1
|
|
);
|
|
|
|
let mut checked = 0usize;
|
|
let mut psnr_sum = 0.0;
|
|
for (index, source) in sources.iter().enumerate() {
|
|
let pts = index as i64 * HNS_PER_FRAME;
|
|
let Some(decoded) = decoded_by_pts.get(&pts) else { continue };
|
|
assert_eq!(decoded.width, WIDTH);
|
|
assert_eq!(decoded.height, HEIGHT);
|
|
let decoded_rgb = decoded.to_rgb8();
|
|
assert_eq!(decoded_rgb.len(), source.len());
|
|
let db = psnr(source, &decoded_rgb);
|
|
psnr_sum += db;
|
|
checked += 1;
|
|
assert!(db > 30.0, "frame {index} PSNR {db:.2} dB too low");
|
|
}
|
|
assert!(checked >= FRAME_COUNT - 1, "only compared {checked} frames against source");
|
|
eprintln!("stream_codec round trip: {checked} frames, avg PSNR {:.2} dB", psnr_sum / checked as f64);
|
|
}
|
|
|
|
#[cfg(not(any(target_os = "macos", target_os = "windows")))]
|
|
#[test]
|
|
fn stream_codec_is_explicitly_unsupported_elsewhere() {
|
|
let err = VideoStreamEncoder::new(VideoStreamEncoderOptions {
|
|
width: WIDTH,
|
|
height: HEIGHT,
|
|
..Default::default()
|
|
})
|
|
.unwrap_err();
|
|
assert!(err.context.contains("not implemented"));
|
|
}
|