mirror of https://github.com/wayvr-org/wayvr.git
wgui: <Video> support [Squash]
commitda9ad32fe4Author: Aleksander <aleksander@oo8.dev> Date: Sat Jun 6 14:44:15 2026 +0200 wgui: video: variable speed control commit683b1115baAuthor: Aleksander <aleksander@oo8.dev> Date: Fri Jun 5 18:44:45 2026 +0200 wgui: video: framerate control, `looping` parameter in <Video> commitddf99d0b43Author: Aleksander <aleksander@oo8.dev> Date: Fri Jun 5 12:19:44 2026 +0200 do not break toml 1.0 syntax compatibility commit69803ec6cdAuthor: Aleksander <aleksander@oo8.dev> Date: Fri Jun 5 12:08:25 2026 +0200 wgui: dav1d video playback works commit5c929c4feeAuthor: Aleksander <aleksander@oo8.dev> Date: Thu Jun 4 23:09:30 2026 +0200 wgui: use dav1d (wip) commit3c2ac4b5f1Author: Aleksander <aleksander@oo8.dev> Date: Thu Jun 4 21:34:44 2026 +0200 wgui: <Video> tag (wip), Duck IVF parser (video container), add `video` feature
This commit is contained in:
parent
698ff86c14
commit
1742d5f589
|
|
@ -1470,6 +1470,15 @@ version = "0.3.2"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "be1e0bca6c3637f992fc1cc7cbc52a78c1ef6db076dbf1059c4323d6a2048376"
|
||||
|
||||
[[package]]
|
||||
name = "dav1d-sys"
|
||||
version = "0.8.3"
|
||||
source = "git+https://github.com/rust-av/dav1d-rs.git?rev=03477a36c3de4f2aacbab81a8951150ffdb9c4c3#03477a36c3de4f2aacbab81a8951150ffdb9c4c3"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"system-deps",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dbus"
|
||||
version = "0.9.11"
|
||||
|
|
@ -6657,7 +6666,9 @@ name = "wgui"
|
|||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bytes",
|
||||
"cosmic-text",
|
||||
"dav1d-sys",
|
||||
"etagere",
|
||||
"flate2",
|
||||
"glam",
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ members = [
|
|||
|
||||
[workspace.dependencies]
|
||||
anyhow = "1.0.100"
|
||||
bytes = { version = "1.11.1" }
|
||||
clap = { version = "4.5.53", features = ["derive"] }
|
||||
glam = { version = "0.30.9", features = ["mint", "serde"] }
|
||||
idmap = "0.2.2"
|
||||
|
|
@ -27,7 +28,7 @@ serde_json5 = "0.2.1"
|
|||
slotmap = "1.1.1"
|
||||
smol = "2.0.2"
|
||||
strum = { version = "0.27.2", features = ["derive"] }
|
||||
uuid = { version = "1.19.0", features = ["fast-rng", "v4", "serde"] }
|
||||
uuid = { version = "1.19.0", features = ["fast-rng", "serde", "v4"] }
|
||||
vulkano = { version = "0.35.2", default-features = false, features = [
|
||||
"macros",
|
||||
] }
|
||||
|
|
|
|||
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><!-- Icon from Material Symbols by Google - https://github.com/google/material-design-icons/blob/master/LICENSE --><path fill="currentColor" d="M11.7 18q-2.4-.125-4.05-1.85T6 12q0-2.5 1.75-4.25T12 6q2.425 0 4.15 1.65T18 11.7l-2.1-.625q-.325-1.35-1.4-2.212T12 8q-1.65 0-2.825 1.175T8 12q0 1.425.863 2.5t2.212 1.4zm8.825 4.5l-4.275-4.275L15 22l-3-10l10 3l-3.775 1.25l4.275 4.275z"/></svg>
|
||||
|
After Width: | Height: | Size: 470 B |
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><!-- Icon from Google Material Icons by Material Design Authors - https://github.com/material-icons/material-icons/blob/master/LICENSE --><path fill="currentColor" d="M20 12c0-2.54-1.19-4.81-3.04-6.27L16 0H8l-.95 5.73C5.19 7.19 4 9.45 4 12s1.19 4.81 3.05 6.27L8 24h8l.96-5.73A7.98 7.98 0 0 0 20 12M6 12c0-3.31 2.69-6 6-6s6 2.69 6 6s-2.69 6-6 6s-6-2.69-6-6"/></svg>
|
||||
|
After Width: | Height: | Size: 448 B |
|
|
@ -6,7 +6,7 @@
|
|||
<elements>
|
||||
<div flex_direction="column" flex_grow="1" gap="16" align_items="center" overflow_y="hidden">
|
||||
<!-- Content -->
|
||||
<div id="content" flex_direction="column" flex_grow="1" align_items="center" justify_content="center" gap="16" overflow_y="scroll">
|
||||
<div id="content" flex_direction="column" flex_grow="1" align_items="center" justify_content="center" gap="16" overflow_y="scroll" width="100%">
|
||||
<!-- filled-in at runtime -->
|
||||
</div>
|
||||
<!-- Page buttons -->
|
||||
|
|
|
|||
|
|
@ -0,0 +1,18 @@
|
|||
<layout>
|
||||
<!-- icon, title -->
|
||||
<template name="PageTitle">
|
||||
<div position="absolute" align_self="start" justify_self="center" width="100%" margin_top="8" gap="8" align_items="center" justify_content="center">
|
||||
<sprite width="48" height="48" src_builtin="${icon}" color="#FFFFFF"/>
|
||||
<label size="28" weight="bold" text="${title}" shadow="#000000" shadow_x="3" shadow_y="3" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- text -->
|
||||
<template name="TextDesc">
|
||||
<label wrap="1" text="${text}" size="20" width="500" shadow="#000000" weight="bold"/>
|
||||
</template>
|
||||
|
||||
<macro name="video_overlay_component" looping="1" width="100%" height="auto" max_height="100%" aspect_ratio="1.777777" position="relative"/>
|
||||
|
||||
<macro name="video_overlay_content" new_pass="1" position="absolute" width="100%" height="100%"/>
|
||||
</layout>
|
||||
|
|
@ -1,5 +1,15 @@
|
|||
<layout>
|
||||
<include src_builtin="welcome_common.xml"/>
|
||||
|
||||
<elements>
|
||||
<label text="Page 1"/>
|
||||
<rectangle color="#103142" width="100%" height="100%" position="absolute"/>
|
||||
<Video macro="video_overlay_component" src_builtin="video/onboarding_watch.ivf" aspect_ratio="1.0">
|
||||
<div macro="video_overlay_content">
|
||||
<PageTitle icon="dashboard/watch.svg" title="Watch"/>
|
||||
<div position="relative" align_self="end" padding="16">
|
||||
<TextDesc text="Look at your left wrist. That's your watch."/>
|
||||
</div>
|
||||
</div>
|
||||
</Video>
|
||||
</elements>
|
||||
</layout>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,14 @@
|
|||
<layout>
|
||||
<include src_builtin="welcome_common.xml"/>
|
||||
|
||||
<elements>
|
||||
<label text="Page 2" weight="bold" size="20"/>
|
||||
<Video macro="video_overlay_component" src_builtin="video/onboarding_double_press.ivf">
|
||||
<div macro="video_overlay_content">
|
||||
<PageTitle icon="dashboard/panorama.svg" title="Working Set"/>
|
||||
<div position="relative" align_self="end" padding="16">
|
||||
<TextDesc text="Double-press B (on Index) or Y (on Meta) to toggle the visibility of your workspace. Try it now."/>
|
||||
</div>
|
||||
</div>
|
||||
</Video>
|
||||
</elements>
|
||||
</layout>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,19 @@
|
|||
<layout>
|
||||
<include src_builtin="welcome_common.xml"/>
|
||||
|
||||
<elements>
|
||||
<label text="Page 3" weight="bold" size="50"/>
|
||||
<Video macro="video_overlay_component" src_builtin="video/onboarding_laser_colors.ivf">
|
||||
<div macro="video_overlay_content">
|
||||
<PageTitle icon="dashboard/click.svg" title="Laser Colors"/>
|
||||
<div position="relative" flex_direction="row" gap="16" margin="16" align_self="end" justify_self="end" align_items="center">
|
||||
<rectangle flex_direction="column" gap="8" padding="8" round="4" border_color="~color_accent" border="2" >
|
||||
<label size="16" weight="bold" color="#00FFFF" text="— Regular Mode: Blue laser"/>
|
||||
<label size="16" weight="bold" color="#FFAA00" text="— Right-click Mode: Orange laser" />
|
||||
<label size="16" weight="bold" color="#FF88FF" text="— Middle-click Mode: Purple laser" />
|
||||
</rectangle>
|
||||
<label wrap="1" size="18" width="450" weight="bold" shadow="#000000" text="Much of the functionality in WayVR depends on what color of laser is used to interact with a UI element. You don't need to press these buttons, just touch them!"/>
|
||||
</div>
|
||||
</div>
|
||||
</Video>
|
||||
</elements>
|
||||
</layout>
|
||||
|
|
|
|||
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -30,6 +30,8 @@ pub struct TabWelcome<T> {
|
|||
current_page: u8,
|
||||
id_pips: WidgetID,
|
||||
id_content: WidgetID,
|
||||
|
||||
state_tab: Option<ParserState>,
|
||||
}
|
||||
|
||||
const PAGE_COUNT: u8 = 5; // 0-4 inclusive
|
||||
|
|
@ -94,6 +96,7 @@ impl<T> TabWelcome<T> {
|
|||
id_pips,
|
||||
id_content,
|
||||
tasks,
|
||||
state_tab: None,
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -120,7 +123,7 @@ impl<T> TabWelcome<T> {
|
|||
|
||||
let globals = layout.state.globals.clone();
|
||||
|
||||
let _ = wgui::parser::parse_from_assets(
|
||||
self.state_tab = Some(wgui::parser::parse_from_assets(
|
||||
&ParseDocumentParams {
|
||||
globals,
|
||||
path: AssetPath::BuiltIn(&format!("gui/tab/welcome_page_{}.xml", self.current_page)),
|
||||
|
|
@ -128,7 +131,7 @@ impl<T> TabWelcome<T> {
|
|||
},
|
||||
layout,
|
||||
self.id_content,
|
||||
)?;
|
||||
)?);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -56,6 +56,8 @@ fn vdf_parse_libraryfolders<'a>(vdf_root: &'a Vdf<'a>) -> Option<Vec<AppEntry>>
|
|||
let mut res = Vec::<AppEntry>::new();
|
||||
|
||||
let mut num = 0;
|
||||
|
||||
#[allow(clippy::while_let_loop)]
|
||||
loop {
|
||||
let Some(library_folder) = get_obj_first(obj_libraryfolders, format!("{}", num).as_str()) else {
|
||||
// no more libraries to find
|
||||
|
|
@ -293,7 +295,7 @@ impl SteamUtils {
|
|||
games.sort_by(|a, b| b.name.cmp(&a.name));
|
||||
}
|
||||
GameSortMethod::PlayDateDesc => {
|
||||
games.sort_by(|a, b| b.last_played.cmp(&a.last_played));
|
||||
games.sort_by_key(|b| std::cmp::Reverse(b.last_played));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,25 @@
|
|||
<layout>
|
||||
<theme>
|
||||
<var key="width" value="426.666" />
|
||||
<var key="height" value="133.333" />
|
||||
</theme>
|
||||
<elements>
|
||||
<rectangle position="absolute" width="10000" height="10000" color="#112233"/>
|
||||
<div flex_direction="row">
|
||||
<div margin="16" gap="8" flex_direction="column" overflow_y="scroll" >
|
||||
<label text="640x200, looping"/>
|
||||
<Video src_builtin="video.ivf" width="~width" height="~height" looping="1"/>
|
||||
<label text="640x200, non-looping "/>
|
||||
<Video src_builtin="video.ivf" width="~width" height="~height" looping="0"/>
|
||||
</div>
|
||||
<div margin="16" gap="8" flex_direction="column" overflow_y="scroll" >
|
||||
<label text="640x200, looping, speed 2x"/>
|
||||
<Video src_builtin="video.ivf" width="~width" height="~height" looping="1" speed="2"/>
|
||||
<label text="640x200, looping, speed 0.5x"/>
|
||||
<Video src_builtin="video.ivf" width="~width" height="~height" looping="1" speed="0.5"/>
|
||||
<label text="640x200, looping, speed 4x"/>
|
||||
<Video src_builtin="video.ivf" width="~width" height="~height" looping="1" speed="4"/>
|
||||
</div>
|
||||
</div>
|
||||
</elements>
|
||||
</layout>
|
||||
Binary file not shown.
|
|
@ -8,18 +8,18 @@ authors = ["galister", "oo8dev"]
|
|||
repository = "https://github.com/wlx-team/wayvr"
|
||||
|
||||
[dependencies]
|
||||
bytes = "1.11.1"
|
||||
smallvec = "1.13.2"
|
||||
serde.workspace = true
|
||||
anyhow = "1.0.93"
|
||||
bytes.workspace = true
|
||||
log = "0.4.22"
|
||||
serde.workspace = true
|
||||
smallvec = "1.13.2"
|
||||
|
||||
# client-only deps
|
||||
interprocess = { version = "2.2.2", features = ["tokio"], optional = true }
|
||||
serde_json.workspace = true
|
||||
tokio = { version = "1.43.1", features = ["macros"], optional = true }
|
||||
tokio-util = { version = "0.7.13", optional = true }
|
||||
serde_json.workspace = true
|
||||
|
||||
[features]
|
||||
default = ["client"]
|
||||
client = ["dep:tokio", "dep:tokio-util", "dep:interprocess"]
|
||||
client = ["dep:interprocess", "dep:tokio", "dep:tokio-util"]
|
||||
|
|
|
|||
|
|
@ -43,8 +43,8 @@ vulkano.workspace = true
|
|||
vulkano-shaders.workspace = true
|
||||
xdg.workspace = true
|
||||
|
||||
ash = "^0.38.0" # must match vulkano
|
||||
bytes = { version = "1.11.1" }
|
||||
ash = "^0.38.0" # must match vulkano
|
||||
bytes = { workspace = true }
|
||||
chrono = { version = "0.4.42", features = ["unstable-locales"] }
|
||||
chrono-tz = "0.10.4"
|
||||
config = "0.15.19"
|
||||
|
|
@ -90,7 +90,7 @@ winit = { version = "0.30.12", optional = true }
|
|||
xcb = { version = "1.6.0", features = [
|
||||
"as-raw-xcb-connection",
|
||||
], optional = true }
|
||||
xkbcommon = { version = "0.8.0" } # 0.9.0 breaks keymap import on some distros
|
||||
xkbcommon = { version = "0.8.0" } # 0.9.0 breaks keymap import on some distros
|
||||
|
||||
[build-dependencies]
|
||||
regex.workspace = true
|
||||
|
|
|
|||
|
|
@ -125,10 +125,8 @@ impl InputState {
|
|||
hand.interaction.mode = PointerMode::Left;
|
||||
}
|
||||
}
|
||||
PointerMode::Right => {
|
||||
if !right_click_orientation {
|
||||
hand.interaction.mode = PointerMode::Left;
|
||||
}
|
||||
PointerMode::Right if !right_click_orientation => {
|
||||
hand.interaction.mode = PointerMode::Left;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -216,63 +216,51 @@ impl OpenVrInputSource {
|
|||
|
||||
app_hand.now.click = input
|
||||
.get_digital_action_data(self.click_hnd, hand.input_hnd)
|
||||
.map(|x| x.0.bState)
|
||||
.unwrap_or(false);
|
||||
.is_ok_and(|x| x.0.bState);
|
||||
|
||||
app_hand.now.grab = input
|
||||
.get_digital_action_data(self.grab_hnd, hand.input_hnd)
|
||||
.map(|x| x.0.bState)
|
||||
.unwrap_or(false);
|
||||
.is_ok_and(|x| x.0.bState);
|
||||
|
||||
app_hand.now.alt_click = input
|
||||
.get_digital_action_data(self.alt_click_hnd, hand.input_hnd)
|
||||
.map(|x| x.0.bState)
|
||||
.unwrap_or(false);
|
||||
.is_ok_and(|x| x.0.bState);
|
||||
|
||||
app_hand.now.show_hide = input
|
||||
.get_digital_action_data(self.show_hide_hnd, hand.input_hnd)
|
||||
.map(|x| x.0.bState)
|
||||
.unwrap_or(false);
|
||||
.is_ok_and(|x| x.0.bState);
|
||||
|
||||
app_hand.now.toggle_dashboard = input
|
||||
.get_digital_action_data(self.toggle_dashboard_hnd, hand.input_hnd)
|
||||
.map(|x| x.0.bState)
|
||||
.unwrap_or(false);
|
||||
.is_ok_and(|x| x.0.bState);
|
||||
|
||||
app_hand.now.space_drag = input
|
||||
.get_digital_action_data(self.space_drag_hnd, hand.input_hnd)
|
||||
.map(|x| x.0.bState)
|
||||
.unwrap_or(false);
|
||||
.is_ok_and(|x| x.0.bState);
|
||||
|
||||
app_hand.now.space_rotate = input
|
||||
.get_digital_action_data(self.space_rotate_hnd, hand.input_hnd)
|
||||
.map(|x| x.0.bState)
|
||||
.unwrap_or(false);
|
||||
.is_ok_and(|x| x.0.bState);
|
||||
|
||||
app_hand.now.space_reset = input
|
||||
.get_digital_action_data(self.space_reset_hnd, hand.input_hnd)
|
||||
.map(|x| x.0.bState)
|
||||
.unwrap_or(false);
|
||||
.is_ok_and(|x| x.0.bState);
|
||||
|
||||
app_hand.now.click_modifier_right = input
|
||||
.get_digital_action_data(self.click_modifier_right_hnd, hand.input_hnd)
|
||||
.map(|x| x.0.bState)
|
||||
.unwrap_or(false);
|
||||
.is_ok_and(|x| x.0.bState);
|
||||
|
||||
app_hand.now.click_modifier_middle = input
|
||||
.get_digital_action_data(self.click_modifier_middle_hnd, hand.input_hnd)
|
||||
.map(|x| x.0.bState)
|
||||
.unwrap_or(false);
|
||||
.is_ok_and(|x| x.0.bState);
|
||||
|
||||
app_hand.now.move_mouse = input
|
||||
.get_digital_action_data(self.move_mouse_hnd, hand.input_hnd)
|
||||
.map(|x| x.0.bState)
|
||||
.unwrap_or(false);
|
||||
.is_ok_and(|x| x.0.bState);
|
||||
|
||||
let scroll = input
|
||||
.get_analog_action_data(self.scroll_hnd, hand.input_hnd)
|
||||
.map(|x| (x.0.x, x.0.y))
|
||||
.unwrap_or((0.0, 0.0));
|
||||
.map_or((0.0, 0.0), |x| (x.0.x, x.0.y));
|
||||
app_hand.now.scroll_x = scroll.0;
|
||||
app_hand.now.scroll_y = scroll.1;
|
||||
}
|
||||
|
|
@ -364,8 +352,7 @@ fn get_tracked_device(
|
|||
index,
|
||||
ETrackedDeviceProperty::Prop_TrackingSystemName_String,
|
||||
)
|
||||
.map(|x: String| x.contains("ALVR"))
|
||||
.unwrap_or(false);
|
||||
.is_ok_and(|x: String| x.contains("ALVR"));
|
||||
|
||||
if is_alvr {
|
||||
// don't show ALVR's fake trackers on battery panel
|
||||
|
|
|
|||
|
|
@ -519,7 +519,7 @@ pub fn openxr_run(
|
|||
let watch = overlays.mut_by_id(watch_id).unwrap(); // want panic
|
||||
|
||||
if let Some(state) = watch.config.active_state.as_mut() {
|
||||
state.transform = watch_transform
|
||||
state.transform = watch_transform;
|
||||
}
|
||||
if !app.session.config.enable_watch {
|
||||
watch.config.deactivate();
|
||||
|
|
|
|||
|
|
@ -114,7 +114,7 @@ impl WGfxExtras {
|
|||
| MemoryTypeFilter::HOST_SEQUENTIAL_WRITE,
|
||||
..Default::default()
|
||||
},
|
||||
vertices.into_iter(),
|
||||
vertices,
|
||||
)?;
|
||||
|
||||
let mut cmd_xfer = gfx.create_xfer_command_buffer(CommandBufferUsage::OneTimeSubmit)?;
|
||||
|
|
|
|||
|
|
@ -45,7 +45,7 @@ impl DbusConnector {
|
|||
let proxy = connection.with_proxy(
|
||||
"org.freedesktop.DBus",
|
||||
"/org/freedesktop/DBus",
|
||||
Duration::from_millis(5000),
|
||||
Duration::from_secs(5),
|
||||
);
|
||||
let result: Result<(), dbus::Error> = proxy.method_call(
|
||||
"org.freedesktop.DBus.Monitoring",
|
||||
|
|
|
|||
|
|
@ -288,13 +288,13 @@ where
|
|||
self.sets_changed(app);
|
||||
}
|
||||
OverlayTask::SettingsChanged => {
|
||||
if let Some(watch) = self.mut_by_id(self.watch_id) {
|
||||
if app.session.config.enable_watch != watch.config.active_state.is_some() {
|
||||
if watch.config.active_state.is_some() {
|
||||
watch.config.deactivate();
|
||||
} else {
|
||||
watch.config.activate(app);
|
||||
}
|
||||
if let Some(watch) = self.mut_by_id(self.watch_id)
|
||||
&& app.session.config.enable_watch != watch.config.active_state.is_some()
|
||||
{
|
||||
if watch.config.active_state.is_some() {
|
||||
watch.config.deactivate();
|
||||
} else {
|
||||
watch.config.activate(app);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ repository = "https://github.com/wlx-team/wayvr"
|
|||
anyhow.workspace = true
|
||||
cosmic-text = "0.15.0"
|
||||
etagere = "0.2.15"
|
||||
flate2 = "1.1.5"
|
||||
glam.workspace = true
|
||||
image = { version = "0.25.9", default-features = false, features = [
|
||||
"gif",
|
||||
|
|
@ -26,6 +27,7 @@ parking_lot = "0.12.5"
|
|||
regex.workspace = true
|
||||
resvg = { version = "0.45.1", default-features = false }
|
||||
roxmltree = "0.21.1"
|
||||
rust-embed.workspace = true
|
||||
rustc-hash = "2.1.1"
|
||||
serde_json.workspace = true
|
||||
slotmap.workspace = true
|
||||
|
|
@ -33,5 +35,11 @@ smallvec = "1.15.1"
|
|||
taffy = "0.9.2"
|
||||
vulkano.workspace = true
|
||||
vulkano-shaders.workspace = true
|
||||
rust-embed.workspace = true
|
||||
flate2 = "1.1.5"
|
||||
|
||||
# `video` wgui feature
|
||||
bytes = { workspace = true, optional = true }
|
||||
dav1d-sys = { git = "https://github.com/rust-av/dav1d-rs.git", rev = "03477a36c3de4f2aacbab81a8951150ffdb9c4c3", optional = true }
|
||||
|
||||
[features]
|
||||
default = ["video"]
|
||||
video = ["dep:bytes", "dep:dav1d-sys"]
|
||||
|
|
|
|||
|
|
@ -49,6 +49,7 @@ pub struct CallbackData<'a> {
|
|||
pub widget_id: WidgetID,
|
||||
pub widget_boundary: Boundary,
|
||||
pub pos: f32, // 0.0 (start of animation) - 1.0 (end of animation)
|
||||
pub stop_me: &'a mut bool,
|
||||
}
|
||||
|
||||
pub type AnimationCallback = Box<dyn Fn(&mut CallbackDataCommon, &mut CallbackData)>;
|
||||
|
|
@ -93,13 +94,16 @@ impl Animation {
|
|||
}
|
||||
}
|
||||
|
||||
fn call(&self, state: &LayoutState, alterables: &mut EventAlterables, pos: f32) {
|
||||
/// @returns false if it wants to be stopped
|
||||
#[must_use]
|
||||
fn call(&self, state: &LayoutState, alterables: &mut EventAlterables, pos: f32) -> bool {
|
||||
let Some(widget) = state.widgets.get(self.target_widget).cloned() else {
|
||||
return; // failed
|
||||
return false; // failed
|
||||
};
|
||||
|
||||
let mut widget_state = widget.state();
|
||||
let (data, obj) = widget_state.get_data_obj_mut();
|
||||
let mut stop_me = false;
|
||||
|
||||
let data = &mut CallbackData {
|
||||
widget_id: self.target_widget,
|
||||
|
|
@ -107,11 +111,14 @@ impl Animation {
|
|||
obj,
|
||||
data,
|
||||
pos,
|
||||
stop_me: &mut stop_me,
|
||||
};
|
||||
|
||||
let common = &mut CallbackDataCommon { state, alterables };
|
||||
|
||||
(self.callback)(common, data);
|
||||
|
||||
!stop_me
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -135,7 +142,7 @@ impl Animations {
|
|||
anim.pos = pos;
|
||||
|
||||
if anim.last_tick {
|
||||
anim.call(state, alterables, 1.0);
|
||||
let _ = anim.call(state, alterables, 1.0);
|
||||
alterables.needs_redraw = true;
|
||||
} else {
|
||||
anim.ticks_remaining -= 1;
|
||||
|
|
@ -148,7 +155,9 @@ impl Animations {
|
|||
pub fn process(&mut self, state: &LayoutState, alterables: &mut EventAlterables, alpha: f32) {
|
||||
for anim in &mut self.running_animations {
|
||||
let pos = anim.pos_prev.lerp(anim.pos, alpha);
|
||||
anim.call(state, alterables, pos);
|
||||
if !anim.call(state, alterables, pos) {
|
||||
anim.ticks_remaining = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -18,6 +18,9 @@ pub mod slider;
|
|||
pub mod tabs;
|
||||
pub mod tooltip;
|
||||
|
||||
#[cfg(feature = "video")]
|
||||
pub mod video;
|
||||
|
||||
pub struct RefreshData<'a> {
|
||||
pub layout: &'a mut Layout,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,285 @@
|
|||
use image::ImageBuffer;
|
||||
use taffy::prelude::percent;
|
||||
|
||||
use crate::{
|
||||
animation::Animation,
|
||||
assets::AssetPath,
|
||||
components::{Component, ComponentBase, ComponentTrait, RefreshData},
|
||||
drawing::Color,
|
||||
event::EventAlterables,
|
||||
globals::WguiGlobals,
|
||||
layout::{Layout, WidgetID, WidgetPair},
|
||||
renderer_vk::text::custom_glyph::{CustomGlyphContent, CustomGlyphData},
|
||||
time::get_millis,
|
||||
video_dec::{self, Av1Decoder, IvfReader},
|
||||
widget::{
|
||||
ConstructEssentials,
|
||||
image::WidgetImage,
|
||||
rectangle::{WidgetRectangle, WidgetRectangleParams},
|
||||
},
|
||||
};
|
||||
use std::{
|
||||
cell::RefCell,
|
||||
rc::{Rc, Weak},
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct Params<'a> {
|
||||
pub style: taffy::Style,
|
||||
pub src: Option<AssetPath<'a>>,
|
||||
pub looping: bool,
|
||||
pub speed: f32,
|
||||
}
|
||||
|
||||
struct PlayingSource {
|
||||
demuxer: IvfReader,
|
||||
decoder: video_dec::Av1Decoder,
|
||||
cur_frame: u32,
|
||||
}
|
||||
|
||||
struct State {
|
||||
source: Option<PlayingSource>,
|
||||
self_ref: Weak<ComponentVideo>,
|
||||
playing: bool,
|
||||
play_requested: bool,
|
||||
}
|
||||
|
||||
struct Data {
|
||||
#[allow(dead_code)]
|
||||
id_container: WidgetID,
|
||||
id_image: WidgetID,
|
||||
|
||||
looping: bool,
|
||||
speed: f32,
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub struct ComponentVideo {
|
||||
base: ComponentBase,
|
||||
data: Rc<Data>,
|
||||
state: Rc<RefCell<State>>,
|
||||
}
|
||||
|
||||
impl ComponentTrait for ComponentVideo {
|
||||
fn base(&self) -> &ComponentBase {
|
||||
&self.base
|
||||
}
|
||||
|
||||
fn base_mut(&mut self) -> &mut ComponentBase {
|
||||
&mut self.base
|
||||
}
|
||||
|
||||
fn refresh(&self, data: &mut RefreshData) {
|
||||
let mut state = self.state.borrow_mut();
|
||||
|
||||
if state.play_requested {
|
||||
state.play_requested = false;
|
||||
if let Err(e) = self.play(&mut state, data.layout) {
|
||||
log::error!("play failed: {e:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const PLAYBACK_ANIMATION_ID: u32 = 1000;
|
||||
|
||||
impl ComponentVideo {
|
||||
fn play(&self, state: &mut State, layout: &mut Layout) -> anyhow::Result<()> {
|
||||
let Some(source) = &mut state.source else {
|
||||
// no source available, do nothing
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
state.playing = false;
|
||||
source.decoder = Av1Decoder::new()?;
|
||||
source.demuxer.rewind();
|
||||
source.cur_frame = 0;
|
||||
|
||||
if let Some(component) = state.self_ref.upgrade() {
|
||||
layout.defer_component_refresh(Component(component));
|
||||
}
|
||||
|
||||
layout
|
||||
.animations
|
||||
.stop_by_widget(self.data.id_image, Some(PLAYBACK_ANIMATION_ID));
|
||||
|
||||
state.playing = true;
|
||||
|
||||
let framerate = source.demuxer.framerate * self.data.speed;
|
||||
let looping = self.data.looping;
|
||||
let id_image = self.data.id_image;
|
||||
let start_time = get_millis();
|
||||
// log::info!("num frames: {}", source.demuxer.num_frames);
|
||||
|
||||
layout.animations.add(Animation::new_ex(
|
||||
self.data.id_image,
|
||||
PLAYBACK_ANIMATION_ID,
|
||||
u32::MAX, // infinity
|
||||
crate::animation::AnimationEasing::Linear,
|
||||
Box::new({
|
||||
let state_ref = self.state.clone();
|
||||
|
||||
move |common, data| {
|
||||
let mut state = state_ref.borrow_mut();
|
||||
if !state.playing {
|
||||
*data.stop_me = true;
|
||||
return;
|
||||
}
|
||||
|
||||
loop {
|
||||
let Some(source) = &mut state.source else {
|
||||
return;
|
||||
};
|
||||
|
||||
let cur_time = get_millis() - start_time;
|
||||
let target_frame = ((cur_time as f32) / 1000.0 * framerate) as u32;
|
||||
|
||||
let cur_frame = source.cur_frame;
|
||||
if cur_frame >= target_frame {
|
||||
break;
|
||||
}
|
||||
|
||||
let image = data.obj.cast_mut::<WidgetImage>().unwrap();
|
||||
|
||||
match ComponentVideo::read_next_frame(image, &mut state, common.alterables) {
|
||||
Ok(data_available) => {
|
||||
if !data_available {
|
||||
if looping {
|
||||
state.play_requested = true;
|
||||
common.alterables.refresh_component_once(&state.self_ref);
|
||||
common.mark_widget_dirty(id_image);
|
||||
} else {
|
||||
state.playing = false;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
state.playing = false;
|
||||
log::error!("read_next_frame failed: {e:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}),
|
||||
));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn read_next_frame(
|
||||
image: &mut WidgetImage,
|
||||
state: &mut State,
|
||||
alterables: &mut EventAlterables,
|
||||
) -> anyhow::Result<bool> {
|
||||
let Some(source) = &mut state.source else {
|
||||
return Ok(false);
|
||||
};
|
||||
|
||||
let rgbx_frame = match source.decoder.read_frame(&mut source.demuxer)? {
|
||||
video_dec::ReadFrameResult::Ok(rgbx_frame) => rgbx_frame,
|
||||
video_dec::ReadFrameResult::EndOfFile => {
|
||||
// log::info!("got EOF");
|
||||
return Ok(false);
|
||||
}
|
||||
};
|
||||
|
||||
let Some(buffer) = ImageBuffer::from_raw(rgbx_frame.width.into(), rgbx_frame.height.into(), rgbx_frame.data) else {
|
||||
anyhow::bail!("ImageBuffer failed");
|
||||
};
|
||||
|
||||
let glyph_content = CustomGlyphContent::Image(buffer);
|
||||
|
||||
image.set_content(
|
||||
alterables,
|
||||
Some(CustomGlyphData {
|
||||
// force-update image content (FIXME: stream image data directly instead)
|
||||
id: source.cur_frame as usize,
|
||||
content: Arc::new(glyph_content),
|
||||
}),
|
||||
);
|
||||
|
||||
source.cur_frame += 1;
|
||||
|
||||
Ok(true)
|
||||
}
|
||||
}
|
||||
|
||||
impl State {
|
||||
fn set_source(&mut self, globals: &WguiGlobals, src: AssetPath) -> anyhow::Result<()> {
|
||||
let video_data = globals.get_asset(src)?;
|
||||
let demuxer = IvfReader::new(video_data)?;
|
||||
let decoder = Av1Decoder::new()?;
|
||||
|
||||
self.source = Some(PlayingSource {
|
||||
demuxer,
|
||||
decoder,
|
||||
cur_frame: 0,
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn construct(ess: &mut ConstructEssentials, params: Params) -> anyhow::Result<(WidgetPair, Rc<ComponentVideo>)> {
|
||||
let style = params.style;
|
||||
|
||||
let (root, _) = ess.layout.add_child(
|
||||
ess.parent,
|
||||
WidgetRectangle::create(WidgetRectangleParams {
|
||||
color: Color::new(0.1, 0.1, 0.1, 1.0),
|
||||
..Default::default()
|
||||
}),
|
||||
style,
|
||||
)?;
|
||||
|
||||
let (image, _) = ess.layout.add_child(
|
||||
root.id,
|
||||
WidgetImage::create(Default::default()),
|
||||
taffy::Style {
|
||||
size: taffy::Size {
|
||||
width: percent(1.0),
|
||||
height: percent(1.0),
|
||||
},
|
||||
..Default::default()
|
||||
},
|
||||
)?;
|
||||
|
||||
let id_container = root.id;
|
||||
let data = Rc::new(Data {
|
||||
id_container,
|
||||
id_image: image.id,
|
||||
looping: params.looping,
|
||||
speed: params.speed,
|
||||
});
|
||||
|
||||
let state = Rc::new(RefCell::new(State {
|
||||
source: None,
|
||||
self_ref: Default::default(),
|
||||
playing: false,
|
||||
play_requested: false,
|
||||
}));
|
||||
|
||||
let base = ComponentBase {
|
||||
id: root.id,
|
||||
lhandles: Default::default(),
|
||||
};
|
||||
|
||||
let video = Rc::new(ComponentVideo { base, data, state });
|
||||
|
||||
// configure state and set video source
|
||||
{
|
||||
let mut state = video.state.borrow_mut();
|
||||
state.self_ref = Rc::downgrade(&video);
|
||||
if let Some(src) = params.src
|
||||
&& let Err(e) = state.set_source(&ess.layout.state.globals, src)
|
||||
{
|
||||
log::error!("set_source failed: {e:?}");
|
||||
}
|
||||
|
||||
state.play_requested = true;
|
||||
}
|
||||
|
||||
ess.layout.defer_component_refresh(Component(video.clone()));
|
||||
Ok((root, video))
|
||||
}
|
||||
|
|
@ -41,9 +41,13 @@ pub mod sound;
|
|||
pub mod stack;
|
||||
pub mod task;
|
||||
pub mod theme;
|
||||
pub mod time;
|
||||
pub mod widget;
|
||||
pub mod windowing;
|
||||
|
||||
#[cfg(feature = "video")]
|
||||
pub mod video_dec;
|
||||
|
||||
// re-exported libs
|
||||
pub use cosmic_text;
|
||||
pub use taffy;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,63 @@
|
|||
use crate::{
|
||||
assets::AssetPath,
|
||||
components::{self, Component},
|
||||
layout::WidgetID,
|
||||
parser::{
|
||||
AttribPair, ParserContext, ParserFile, get_asset_path_from_kv, parse_children, parse_f32, parse_i32,
|
||||
process_component, style::parse_style,
|
||||
},
|
||||
};
|
||||
|
||||
pub fn parse_component_video<'a>(
|
||||
file: &'a ParserFile,
|
||||
ctx: &mut ParserContext,
|
||||
node: roxmltree::Node<'a, 'a>,
|
||||
parent_id: WidgetID,
|
||||
attribs: &[AttribPair],
|
||||
tag_name: &str,
|
||||
) -> anyhow::Result<WidgetID> {
|
||||
let mut src: Option<AssetPath> = None;
|
||||
let mut looping: bool = false;
|
||||
let mut speed: f32 = 1.0;
|
||||
|
||||
let style = parse_style(ctx, attribs, tag_name);
|
||||
|
||||
for pair in attribs {
|
||||
let (key, value) = (pair.attrib.as_ref(), pair.value.as_ref());
|
||||
match key {
|
||||
"speed" => {
|
||||
speed = parse_f32(value).unwrap_or(speed);
|
||||
}
|
||||
"looping" => {
|
||||
if let Some(v) = parse_i32(value)
|
||||
&& v != 0
|
||||
{
|
||||
looping = true;
|
||||
}
|
||||
}
|
||||
"src" | "src_ext" | "src_builtin" | "src_internal" => {
|
||||
let asset_path = get_asset_path_from_kv("", key, value);
|
||||
|
||||
if !value.is_empty() {
|
||||
src = Some(asset_path);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
let (widget, video) = components::video::construct(
|
||||
&mut ctx.get_construct_essentials(parent_id),
|
||||
components::video::Params {
|
||||
style,
|
||||
src,
|
||||
looping,
|
||||
speed,
|
||||
},
|
||||
)?;
|
||||
|
||||
process_component(ctx, Component(video), widget.id, attribs);
|
||||
parse_children(file, ctx, node, widget.id)?;
|
||||
|
||||
Ok(widget.id)
|
||||
}
|
||||
|
|
@ -6,6 +6,10 @@ mod component_editbox;
|
|||
mod component_radio_group;
|
||||
mod component_slider;
|
||||
mod component_tabs;
|
||||
|
||||
#[cfg(feature = "video")]
|
||||
mod component_video;
|
||||
|
||||
mod helpers;
|
||||
mod style;
|
||||
mod widget_div;
|
||||
|
|
@ -582,6 +586,10 @@ impl ParserContext<'_> {
|
|||
Some(val / 100.0)
|
||||
}
|
||||
|
||||
pub fn parse_auto(value: &str) -> bool {
|
||||
value.contains("auto")
|
||||
}
|
||||
|
||||
fn parse_size_unit<T>(&self, tag_name: &str, key: &str, value: &str) -> Option<T>
|
||||
where
|
||||
T: taffy::prelude::FromPercent + taffy::prelude::FromLength,
|
||||
|
|
@ -1084,6 +1092,14 @@ fn parse_child<'a>(
|
|||
file, ctx, child_node, parent_id, &attribs, tag_name,
|
||||
)?);
|
||||
}
|
||||
#[cfg(feature = "video")]
|
||||
"Video" => {
|
||||
use crate::parser::component_video::parse_component_video;
|
||||
|
||||
new_widget_id = Some(parse_component_video(
|
||||
file, ctx, child_node, parent_id, &attribs, tag_name,
|
||||
)?);
|
||||
}
|
||||
"Slider" => {
|
||||
new_widget_id = Some(parse_component_slider(ctx, parent_id, &attribs, tag_name)?);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -233,34 +233,51 @@ pub fn parse_style(ctx: &ParserContext<'_>, attribs: &[AttribPair], tag_name: &s
|
|||
ctx.print_invalid_attrib(tag_name, key, value);
|
||||
}
|
||||
},
|
||||
"aspect_ratio" => {
|
||||
if let Some(aspect) = ctx.parse_val(tag_name, key, value) {
|
||||
style.aspect_ratio = Some(aspect);
|
||||
}
|
||||
}
|
||||
"min_width" => {
|
||||
if let Some(dim) = ctx.parse_size_unit(tag_name, key, value) {
|
||||
style.min_size.width = dim;
|
||||
} else if ParserContext::parse_auto(value) {
|
||||
style.min_size.width = taffy::prelude::auto();
|
||||
}
|
||||
}
|
||||
"min_height" => {
|
||||
if let Some(dim) = ctx.parse_size_unit(tag_name, key, value) {
|
||||
style.min_size.height = dim;
|
||||
} else if ParserContext::parse_auto(value) {
|
||||
style.min_size.height = taffy::prelude::auto();
|
||||
}
|
||||
}
|
||||
"max_width" => {
|
||||
if let Some(dim) = ctx.parse_size_unit(tag_name, key, value) {
|
||||
style.max_size.width = dim;
|
||||
} else if ParserContext::parse_auto(value) {
|
||||
style.max_size.width = taffy::prelude::auto();
|
||||
}
|
||||
}
|
||||
"max_height" => {
|
||||
if let Some(dim) = ctx.parse_size_unit(tag_name, key, value) {
|
||||
style.max_size.height = dim;
|
||||
} else if ParserContext::parse_auto(value) {
|
||||
style.max_size.height = taffy::prelude::auto();
|
||||
}
|
||||
}
|
||||
"width" => {
|
||||
if let Some(dim) = ctx.parse_size_unit(tag_name, key, value) {
|
||||
style.size.width = dim;
|
||||
} else if ParserContext::parse_auto(value) {
|
||||
style.size.width = taffy::prelude::auto();
|
||||
}
|
||||
}
|
||||
"height" => {
|
||||
if let Some(dim) = ctx.parse_size_unit(tag_name, key, value) {
|
||||
style.size.height = dim;
|
||||
} else if ParserContext::parse_auto(value) {
|
||||
style.size.height = taffy::prelude::auto();
|
||||
}
|
||||
}
|
||||
"gap" => {
|
||||
|
|
|
|||
|
|
@ -61,9 +61,7 @@ impl TextRenderer {
|
|||
|
||||
let resolution = viewport.resolution();
|
||||
let mut glyphs_to_render = Vec::new();
|
||||
let mut pending_glyph_uploads = Vec::new();
|
||||
let mut missing_glyphs = HashSet::new();
|
||||
let mut unavailable_glyphs = HashSet::new();
|
||||
let mut glyphs_todo = GlyphsTodo::default();
|
||||
|
||||
for text_area in text_areas {
|
||||
let bounds_min_x = text_area.bounds.left.max(0);
|
||||
|
|
@ -101,13 +99,13 @@ impl TextRenderer {
|
|||
.unwrap_or(text_area.default_color);
|
||||
|
||||
if queue_missing_glyph_upload(
|
||||
atlas,
|
||||
font_system,
|
||||
cache,
|
||||
cache_key,
|
||||
&mut missing_glyphs,
|
||||
&mut unavailable_glyphs,
|
||||
&mut pending_glyph_uploads,
|
||||
&mut QueueMissingGlyphUploadParams {
|
||||
atlas,
|
||||
font_system,
|
||||
cache,
|
||||
cache_key,
|
||||
glyphs_todo: &mut glyphs_todo,
|
||||
},
|
||||
|_cache, _font_system| -> Option<GetGlyphImageResult> {
|
||||
if cached_width == 0 || cached_height == 0 {
|
||||
return None;
|
||||
|
|
@ -180,13 +178,13 @@ impl TextRenderer {
|
|||
let cache_key = GlyphonCacheKey::Text(physical_glyph.cache_key);
|
||||
|
||||
if queue_missing_glyph_upload(
|
||||
atlas,
|
||||
font_system,
|
||||
cache,
|
||||
cache_key,
|
||||
&mut missing_glyphs,
|
||||
&mut unavailable_glyphs,
|
||||
&mut pending_glyph_uploads,
|
||||
&mut QueueMissingGlyphUploadParams {
|
||||
atlas,
|
||||
font_system,
|
||||
cache,
|
||||
cache_key,
|
||||
glyphs_todo: &mut glyphs_todo,
|
||||
},
|
||||
|cache, font_system| -> Option<GetGlyphImageResult> {
|
||||
let image = cache.get_image_uncached(font_system, physical_glyph.cache_key)?;
|
||||
|
||||
|
|
@ -229,7 +227,7 @@ impl TextRenderer {
|
|||
}
|
||||
}
|
||||
|
||||
upload_missing_glyphs(atlas, font_system, cache, pending_glyph_uploads)?;
|
||||
upload_missing_glyphs(atlas, font_system, cache, glyphs_todo.pending_uploads)?;
|
||||
|
||||
for glyph in &glyphs_to_render {
|
||||
if let Some(glyph_to_render) = prepare_glyph(&mut PrepareGlyphParams {
|
||||
|
|
@ -349,31 +347,43 @@ struct PrepareGlyphParams<'a> {
|
|||
model_buffer: &'a mut ModelBuffer,
|
||||
}
|
||||
|
||||
fn queue_missing_glyph_upload(
|
||||
atlas: &mut TextAtlas,
|
||||
font_system: &mut FontSystem,
|
||||
cache: &mut SwashCache,
|
||||
#[derive(Default)]
|
||||
struct GlyphsTodo {
|
||||
pending_uploads: Vec<PendingGlyphUpload>,
|
||||
missing: HashSet<GlyphonCacheKey>,
|
||||
unavailable: HashSet<GlyphonCacheKey>,
|
||||
}
|
||||
|
||||
struct QueueMissingGlyphUploadParams<'a> {
|
||||
atlas: &'a mut TextAtlas,
|
||||
font_system: &'a mut FontSystem,
|
||||
cache: &'a mut SwashCache,
|
||||
cache_key: GlyphonCacheKey,
|
||||
missing_glyphs: &mut HashSet<GlyphonCacheKey>,
|
||||
unavailable_glyphs: &mut HashSet<GlyphonCacheKey>,
|
||||
pending_glyph_uploads: &mut Vec<PendingGlyphUpload>,
|
||||
glyphs_todo: &'a mut GlyphsTodo,
|
||||
}
|
||||
|
||||
fn queue_missing_glyph_upload(
|
||||
par: &mut QueueMissingGlyphUploadParams,
|
||||
get_glyph_image: impl FnOnce(&mut SwashCache, &mut FontSystem) -> Option<GetGlyphImageResult>,
|
||||
) -> bool {
|
||||
if mark_glyph_in_use_if_cached(atlas, cache_key) {
|
||||
if mark_glyph_in_use_if_cached(par.atlas, par.cache_key) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if unavailable_glyphs.contains(&cache_key) {
|
||||
if par.glyphs_todo.unavailable.contains(&par.cache_key) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if missing_glyphs.insert(cache_key) {
|
||||
let Some(image) = get_glyph_image(cache, font_system) else {
|
||||
unavailable_glyphs.insert(cache_key);
|
||||
if par.glyphs_todo.missing.insert(par.cache_key) {
|
||||
let Some(image) = get_glyph_image(par.cache, par.font_system) else {
|
||||
par.glyphs_todo.unavailable.insert(par.cache_key);
|
||||
return false;
|
||||
};
|
||||
|
||||
pending_glyph_uploads.push(PendingGlyphUpload { cache_key, image });
|
||||
par.glyphs_todo.pending_uploads.push(PendingGlyphUpload {
|
||||
cache_key: par.cache_key,
|
||||
image,
|
||||
});
|
||||
}
|
||||
|
||||
true
|
||||
|
|
@ -422,6 +432,7 @@ fn upload_missing_glyphs(
|
|||
for (upload_index, upload) in rasterized_uploads.iter().enumerate() {
|
||||
let content_type = upload.image.content_type;
|
||||
|
||||
#[allow(clippy::never_loop)]
|
||||
let allocation = loop {
|
||||
if let Some(allocation) = {
|
||||
let inner = atlas.inner_for_content_mut(content_type);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,9 @@
|
|||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
|
||||
// Returns milliseconds since unix epoch
|
||||
pub fn get_millis() -> u64 {
|
||||
SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_millis() as u64
|
||||
}
|
||||
|
|
@ -0,0 +1,339 @@
|
|||
use std::ptr::null_mut;
|
||||
|
||||
use bytes::{Buf, Bytes};
|
||||
use dav1d_sys::{
|
||||
DAV1D_ERR_AGAIN, Dav1dContext, Dav1dData, Dav1dPicture, Dav1dSettings, dav1d_close, dav1d_default_settings,
|
||||
dav1d_get_picture, dav1d_open, dav1d_picture_unref, dav1d_send_data,
|
||||
};
|
||||
|
||||
pub struct IvfReader {
|
||||
file_data: Vec<u8>, // Used by `reader`, do not remove!
|
||||
reader: Bytes,
|
||||
header_size: usize,
|
||||
pub cur_frame: u32,
|
||||
pub num_frames: u32,
|
||||
pub framerate: f32,
|
||||
pub width: u16,
|
||||
pub height: u16,
|
||||
}
|
||||
|
||||
// Duck IVF video container. Documentation:
|
||||
// https://wiki.multimedia.cx/index.php/Duck_IVF
|
||||
// Yes, it's just as simple as that.
|
||||
//
|
||||
// Header:
|
||||
// bytes 0-3 signature: 'DKIF'
|
||||
// bytes 4-5 version (should be 0)
|
||||
// bytes 6-7 length of header in bytes
|
||||
// bytes 8-11 codec FourCC (e.g., 'VP80')
|
||||
// bytes 12-13 width in pixels
|
||||
// bytes 14-15 height in pixels
|
||||
// bytes 16-19 time base denominator
|
||||
// bytes 20-23 time base numerator
|
||||
// bytes 24-27 number of `Frame`s in file
|
||||
// bytes 28-31 unused
|
||||
//
|
||||
// Frame:
|
||||
// bytes 0-3 size of frame in bytes (not including the 12-byte header)
|
||||
// bytes 4-11 64-bit presentation timestamp
|
||||
// bytes 12.. frame data
|
||||
|
||||
const IVF_MAGIC: [u8; 4] = [0x44, 0x4B, 0x49, 0x46];
|
||||
|
||||
pub enum IvfReadFrameResult<'a> {
|
||||
Ok((&'a [u8], u64 /* pts */)),
|
||||
EndOfFile,
|
||||
}
|
||||
|
||||
impl IvfReader {
|
||||
pub fn new(file_data: Vec<u8>) -> anyhow::Result<IvfReader> {
|
||||
// safety: both reader and file_data are located in the same struct
|
||||
// file_data won't move at all.
|
||||
let mut reader = Bytes::from_static(unsafe { std::mem::transmute::<&[u8], &'static [u8]>(&file_data) });
|
||||
|
||||
// read ivf magic
|
||||
let mut header_magic: [u8; 4] = [0; 4];
|
||||
reader.try_copy_to_slice(&mut header_magic)?;
|
||||
|
||||
if header_magic != IVF_MAGIC {
|
||||
anyhow::bail!("invalid magic");
|
||||
}
|
||||
|
||||
let header_version = reader.try_get_u16_le()?;
|
||||
|
||||
if header_version != 0 {
|
||||
anyhow::bail!("unsupported version"); // there's no other version than 0
|
||||
}
|
||||
|
||||
let header_len = reader.try_get_u16_le()?;
|
||||
if header_len != 32 {
|
||||
anyhow::bail!("header length mismatching");
|
||||
}
|
||||
|
||||
let mut header_fourcc: [u8; 4] = [0; 4];
|
||||
reader.try_copy_to_slice(&mut header_fourcc)?;
|
||||
|
||||
let header_width = reader.try_get_u16_le()?;
|
||||
let header_height = reader.try_get_u16_le()?;
|
||||
let header_timebase_den = reader.try_get_u32_le()?;
|
||||
let header_timebase_num = reader.try_get_u32_le()?;
|
||||
let header_num_frames = reader.try_get_u32_le()?;
|
||||
|
||||
let framerate = header_timebase_den as f32 / header_timebase_num as f32;
|
||||
|
||||
let mut padding: [u8; 4] = [0; 4];
|
||||
reader.try_copy_to_slice(&mut padding)?;
|
||||
|
||||
log::debug!("IvfReader: width {header_width}, height {header_height}, framerate {framerate}");
|
||||
|
||||
let header_size = file_data.len() - reader.remaining();
|
||||
|
||||
Ok(IvfReader {
|
||||
header_size,
|
||||
file_data,
|
||||
reader,
|
||||
cur_frame: 0,
|
||||
width: header_width,
|
||||
height: header_height,
|
||||
framerate,
|
||||
num_frames: header_num_frames,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn rewind(&mut self) {
|
||||
self.reader = Bytes::from_static(unsafe { std::mem::transmute::<&[u8], &'static [u8]>(&self.file_data) });
|
||||
// do not read header again
|
||||
self.reader.advance(self.header_size);
|
||||
}
|
||||
|
||||
// Read demuxed video packet to a chunk
|
||||
pub fn read_frame(&mut self) -> anyhow::Result<IvfReadFrameResult<'_>> {
|
||||
if self.reader.remaining() == 0 {
|
||||
return Ok(IvfReadFrameResult::EndOfFile);
|
||||
}
|
||||
|
||||
let frame_size = self.reader.try_get_u32_le()? as usize;
|
||||
let frame_pts = self.reader.try_get_u64_le()?;
|
||||
|
||||
if frame_size > 8 * 1024 * 1024 {
|
||||
// something went really wrong
|
||||
anyhow::bail!("Invalid frame size {frame_size}");
|
||||
}
|
||||
|
||||
// SAFETY: reader.chunk() slice lifetime is the same as Self, no risk here.
|
||||
let chunk_a /* 'a */ = unsafe /* it's safe */ {
|
||||
let Some(chunk) = self.reader.chunk().get(0..(frame_size)) else {
|
||||
anyhow::bail!("chunk read error");
|
||||
};
|
||||
std::mem::transmute::<&[u8], &'static [u8]>(chunk)
|
||||
};
|
||||
|
||||
self.reader.advance(frame_size);
|
||||
|
||||
self.cur_frame += 1;
|
||||
|
||||
Ok(IvfReadFrameResult::Ok((chunk_a, frame_pts)))
|
||||
}
|
||||
}
|
||||
|
||||
// RGB with 255 A
|
||||
pub struct RgbxFrame {
|
||||
pub width: u16,
|
||||
pub height: u16,
|
||||
pub data: Vec<u8>,
|
||||
}
|
||||
|
||||
fn yuv_to_rgb(y: u8, u: u8, v: u8) -> (u8, u8, u8) {
|
||||
let y_i = i32::from(y);
|
||||
let u_i = i32::from(u) - 128;
|
||||
let v_i = i32::from(v) - 128;
|
||||
let c = (1192 * (y_i - 16)).max(0);
|
||||
let out_r = (c + 1634 * v_i) >> 10;
|
||||
let out_g = (c - 401 * u_i - 834 * v_i) >> 10;
|
||||
let out_b = (c + 2066 * u_i) >> 10;
|
||||
(
|
||||
out_r.clamp(0, 255) as u8,
|
||||
out_g.clamp(0, 255) as u8,
|
||||
out_b.clamp(0, 255) as u8,
|
||||
)
|
||||
}
|
||||
|
||||
impl RgbxFrame {
|
||||
// Simple, temporary, best-effort yuv420->rgb converter. Not performant at all, but at least it works.
|
||||
// Temporary till we get native YUV support in shaders (as 3 vulkan textures).
|
||||
// SAFETY: this function expects YUV420 data only.
|
||||
#[allow(clippy::uninit_vec)]
|
||||
fn from_yuv(
|
||||
width: u16,
|
||||
height: u16,
|
||||
data_y: *const u8,
|
||||
data_u: *const u8,
|
||||
data_v: *const u8,
|
||||
stride_luma: usize,
|
||||
stride_chroma: usize,
|
||||
) -> RgbxFrame {
|
||||
unsafe {
|
||||
let channels = 4; // rgbx
|
||||
|
||||
let rgbx_size = width as usize * height as usize * channels;
|
||||
let mut rgbx = Vec::<u8>::with_capacity(rgbx_size);
|
||||
rgbx.set_len(rgbx_size);
|
||||
|
||||
let ptr_rgbx = rgbx.as_mut_ptr();
|
||||
|
||||
for y in 0..height as usize {
|
||||
for x in 0..width as usize {
|
||||
let val_y = *data_y.add(y * stride_luma + x);
|
||||
let val_u = *data_u.add((y / 2) * stride_chroma + (x / 2));
|
||||
let val_v = *data_v.add((y / 2) * stride_chroma + (x / 2));
|
||||
let (r, g, b) = yuv_to_rgb(val_y, val_u, val_v);
|
||||
|
||||
let pos = y * (width as usize * channels) + x * channels;
|
||||
*ptr_rgbx.add(pos) = r;
|
||||
*ptr_rgbx.add(pos + 1) = g;
|
||||
*ptr_rgbx.add(pos + 2) = b;
|
||||
*ptr_rgbx.add(pos + 3) = 255;
|
||||
}
|
||||
}
|
||||
|
||||
RgbxFrame {
|
||||
width,
|
||||
height,
|
||||
data: rgbx,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Av1Decoder {
|
||||
ctx: *mut Dav1dContext,
|
||||
}
|
||||
|
||||
impl Drop for Av1Decoder {
|
||||
fn drop(&mut self) {
|
||||
unsafe {
|
||||
dav1d_close(&raw mut self.ctx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum GetPictureResult {
|
||||
Ok,
|
||||
Again,
|
||||
Failed(i32),
|
||||
}
|
||||
|
||||
enum ReadFrameIterResult {
|
||||
Ok(RgbxFrame),
|
||||
Again,
|
||||
EndOfFile,
|
||||
}
|
||||
|
||||
pub enum ReadFrameResult {
|
||||
Ok(RgbxFrame),
|
||||
EndOfFile,
|
||||
}
|
||||
|
||||
impl Av1Decoder {
|
||||
pub fn new() -> anyhow::Result<Self> {
|
||||
unsafe {
|
||||
let mut set: Dav1dSettings = std::mem::zeroed();
|
||||
dav1d_default_settings(&raw mut set);
|
||||
|
||||
let mut ctx = null_mut();
|
||||
let ret = dav1d_open(&raw mut ctx, &raw mut set);
|
||||
if ret < 0 {
|
||||
anyhow::bail!("dav1d_open failed: {ret}");
|
||||
}
|
||||
Ok(Self { ctx })
|
||||
}
|
||||
}
|
||||
|
||||
fn get_picture(&mut self, picture: &mut Dav1dPicture) -> GetPictureResult {
|
||||
let ret = unsafe { dav1d_get_picture(self.ctx, picture) };
|
||||
if ret == DAV1D_ERR_AGAIN {
|
||||
return GetPictureResult::Again;
|
||||
}
|
||||
|
||||
if ret < 0 {
|
||||
return GetPictureResult::Failed(ret);
|
||||
}
|
||||
|
||||
GetPictureResult::Ok
|
||||
}
|
||||
|
||||
fn send_data(&mut self, reader: &mut IvfReader) -> anyhow::Result<bool> {
|
||||
unsafe {
|
||||
let (packet, pts) = match reader.read_frame()? {
|
||||
IvfReadFrameResult::Ok((packet, pts)) => (packet, pts),
|
||||
IvfReadFrameResult::EndOfFile => return Ok(false),
|
||||
};
|
||||
let mut data: Dav1dData = std::mem::zeroed();
|
||||
data.data = packet.as_ptr();
|
||||
data.sz = packet.len();
|
||||
data.m.timestamp = pts as i64;
|
||||
data.m.size = packet.len();
|
||||
data.m.offset = -1;
|
||||
data.m.duration = 0;
|
||||
|
||||
let ret = dav1d_send_data(self.ctx, &raw mut data);
|
||||
if ret < 0 {
|
||||
anyhow::bail!("dav1d_send_data failed: {ret}");
|
||||
}
|
||||
|
||||
Ok(true)
|
||||
}
|
||||
}
|
||||
|
||||
fn read_frame_iter(&mut self, reader: &mut IvfReader) -> anyhow::Result<ReadFrameIterResult> {
|
||||
unsafe {
|
||||
let mut picture: Dav1dPicture = std::mem::zeroed();
|
||||
match self.get_picture(&mut picture) {
|
||||
GetPictureResult::Ok => {}
|
||||
GetPictureResult::Again => {
|
||||
dav1d_picture_unref(&raw mut picture);
|
||||
if !self.send_data(reader)? {
|
||||
return Ok(ReadFrameIterResult::EndOfFile);
|
||||
}
|
||||
return Ok(ReadFrameIterResult::Again);
|
||||
}
|
||||
GetPictureResult::Failed(code) => {
|
||||
dav1d_picture_unref(&raw mut picture);
|
||||
anyhow::bail!("dav1d_get_picture failed: {code}");
|
||||
}
|
||||
}
|
||||
|
||||
let pic_y = picture.data[0]; // luma Y
|
||||
let pic_u = picture.data[1]; // chroma U
|
||||
let pic_v = picture.data[2]; // chroma V
|
||||
let stride_luma = picture.stride[0];
|
||||
let stride_chroma = picture.stride[1];
|
||||
|
||||
let rgbx = RgbxFrame::from_yuv(
|
||||
reader.width,
|
||||
reader.height,
|
||||
pic_y as *const u8,
|
||||
pic_u as *const u8,
|
||||
pic_v as *const u8,
|
||||
stride_luma as usize,
|
||||
stride_chroma as usize,
|
||||
);
|
||||
|
||||
dav1d_picture_unref(&raw mut picture);
|
||||
|
||||
Ok(ReadFrameIterResult::Ok(rgbx))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn read_frame(&mut self, reader: &mut IvfReader) -> anyhow::Result<ReadFrameResult> {
|
||||
loop {
|
||||
match self.read_frame_iter(reader)? {
|
||||
ReadFrameIterResult::Ok(rgbx_frame) => {
|
||||
return Ok(ReadFrameResult::Ok(rgbx_frame));
|
||||
}
|
||||
ReadFrameIterResult::Again => { /* loop again */ }
|
||||
ReadFrameIterResult::EndOfFile => return Ok(ReadFrameResult::EndOfFile),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue