feat(engine): rename facade to oakengine, build liboakengine.dylib
- src/facade/rust -> src/engine/rust; package oakfacade -> oakengine - crate-type += cdylib; module crates are real deps; linkage anchors force-link module C ABIs into the dylib - build.rs: -undefined dynamic_lookup for host-provided oakcore_*/fb_* - nm: 749 oakengine_* + 687 module oak*_* exports; undefined set is only the intended host-provided symbols - worker/cli updated to the new path/name; undo test race fix
This commit is contained in:
Generated
+1
@@ -3636,6 +3636,7 @@ dependencies = [
|
||||
"gpui_platform",
|
||||
"gpui_widgets",
|
||||
"image",
|
||||
"smallvec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
@@ -37,6 +37,10 @@ gpui = { path = "../../gpui/crates/gpui" }
|
||||
gpui_platform = { path = "../../gpui/crates/gpui_platform", features = ["font-kit"] }
|
||||
# Oak's widget library: menus, viewer, form controls, project explorer.
|
||||
gpui_widgets = { path = "../../gpui/crates/gpui_widgets" }
|
||||
# The mock engine's synthetic viewer frames (`image::Frame` in a
|
||||
# `RenderImage`), matching the versions gpui itself uses.
|
||||
image = "0.25"
|
||||
smallvec = "1"
|
||||
|
||||
[dev-dependencies]
|
||||
# `#[gpui::test]` harness for engine-seam smoke tests (test-support feature).
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 457 KiB After Width: | Height: | Size: 397 KiB |
@@ -57,12 +57,16 @@ fn main() -> Result<()> {
|
||||
})?;
|
||||
let handle: AnyWindowHandle = window.into();
|
||||
|
||||
// Let the platform settle, then draw one full frame into the rendered
|
||||
// scene so `render_to_image` has something to capture.
|
||||
// Draw a few frames so the layout settles: the node editor fits its graph
|
||||
// once the canvas size is known and the viewers upload their first CPU
|
||||
// frame, both of which happen on the frame after the initial render.
|
||||
for _ in 0..4 {
|
||||
cx.run_until_parked();
|
||||
cx.update_window(handle, |_root, window, app| {
|
||||
let _ = window.draw(app);
|
||||
})?;
|
||||
}
|
||||
cx.run_until_parked();
|
||||
cx.update_window(handle, |_root, window, app| {
|
||||
let _ = window.draw(app);
|
||||
})?;
|
||||
|
||||
let image = cx.capture_screenshot(handle)?;
|
||||
std::fs::create_dir_all(std::path::Path::new(OUT).parent().unwrap())?;
|
||||
|
||||
+2
-2
@@ -152,7 +152,7 @@ impl PanelRegistry for AppPanelRegistry {
|
||||
cx,
|
||||
)),
|
||||
"node-editor" => Some(PanelHandle::new(
|
||||
cx.new(|cx| NodeEditorPanel::new(window, cx)),
|
||||
cx.new(|cx| NodeEditorPanel::new(self.engine.clone(), window, cx)),
|
||||
cx,
|
||||
)),
|
||||
"inspector" => Some(PanelHandle::new(
|
||||
@@ -234,7 +234,7 @@ impl OakApp {
|
||||
cx,
|
||||
)
|
||||
});
|
||||
let node_editor = cx.new(|cx| NodeEditorPanel::new(window, cx));
|
||||
let node_editor = cx.new(|cx| NodeEditorPanel::new(engine.clone(), window, cx));
|
||||
let inspector = cx.new(|cx| InspectorPanel::new(engine.clone(), window, cx));
|
||||
let history = cx.new(|cx| HistoryPanel::new(window, cx));
|
||||
let timeline_panel =
|
||||
|
||||
+71
-9
@@ -95,17 +95,19 @@ pub fn set_language(language: Language) {
|
||||
Language::ZhCN => 1,
|
||||
}, Ordering::Relaxed);
|
||||
persist_language(language);
|
||||
sync_widgets();
|
||||
}
|
||||
|
||||
/// Loads the persisted language from the oakcommon config C ABI. Called once
|
||||
/// at startup. Never fails: without liboakcommon the default (en-US) stays.
|
||||
pub fn init() {
|
||||
let Some(store) = ConfigAbi::load() else {
|
||||
sync_widgets();
|
||||
return;
|
||||
};
|
||||
match store.get("Language") {
|
||||
Some(code) if !code.is_empty() => set_language(Language::from_code(&code)),
|
||||
_ => {}
|
||||
_ => sync_widgets(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -127,6 +129,28 @@ pub fn tr(key: &'static str) -> &'static str {
|
||||
}
|
||||
}
|
||||
|
||||
/// The keys the gpui widget crates localize through
|
||||
/// `gpui_widgets::i18n` (viewer transport labels, effect-stack empty state).
|
||||
pub const WIDGET_KEYS: &[&str] = &[
|
||||
"viewer.safe_frames",
|
||||
"viewer.zoom",
|
||||
"viewer.no_frame_source",
|
||||
"effect_stack.empty",
|
||||
"effect_stack.add",
|
||||
];
|
||||
|
||||
/// Installs the active language's widget strings into the
|
||||
/// `gpui_widgets::i18n` string-table hook, so the widget-baked labels follow
|
||||
/// the app language on the next render. Called on startup and on every
|
||||
/// language switch; the widgets keep their built-in defaults otherwise.
|
||||
pub fn sync_widgets() {
|
||||
let mut table = gpui_widgets::i18n::StringTable::new();
|
||||
for key in WIDGET_KEYS {
|
||||
table.insert((*key).to_string(), tr(key).to_string());
|
||||
}
|
||||
gpui_widgets::i18n::set_table(table);
|
||||
}
|
||||
|
||||
/// Looks `key` up in the en-US table.
|
||||
fn en(key: &'static str) -> &'static str {
|
||||
EN
|
||||
@@ -226,14 +250,16 @@ const EN: &[(&str, &str)] = &[
|
||||
("timeline.track_height", "Track Height"),
|
||||
("timeline.snap", "Snap"),
|
||||
// --- node editor ---
|
||||
("node.zoom_in", "Zoom In"),
|
||||
("node.zoom_out", "Zoom Out"),
|
||||
("node.fit", "Fit"),
|
||||
("node.fit_window", "Fit Window"),
|
||||
("node.placeholder", "Node Editor · Placeholder — gpui::node_graph not wired up yet"),
|
||||
// --- viewer header chips ---
|
||||
("viewer.source", "Source Viewer · Source"),
|
||||
("viewer.program", "Program Viewer · Program"),
|
||||
// --- widget-baked strings (synced to gpui_widgets::i18n) ---
|
||||
("viewer.safe_frames", "Safe Frames"),
|
||||
("viewer.zoom", "Zoom"),
|
||||
("viewer.no_frame_source", "No frame source"),
|
||||
("effect_stack.empty", "No selection"),
|
||||
("effect_stack.add", "+ Add Effect"),
|
||||
// --- inspector ---
|
||||
("inspector.params", "Parameters (placeholder)"),
|
||||
];
|
||||
@@ -319,14 +345,16 @@ const ZH: &[(&str, &str)] = &[
|
||||
("timeline.track_height", "轨道高"),
|
||||
("timeline.snap", "吸附"),
|
||||
// --- node editor ---
|
||||
("node.zoom_in", "放大"),
|
||||
("node.zoom_out", "缩小"),
|
||||
("node.fit", "适配"),
|
||||
("node.fit_window", "适配窗口"),
|
||||
("node.placeholder", "节点编辑器 · 占位 — gpui::node_graph 尚未接入"),
|
||||
// --- viewer header chips ---
|
||||
("viewer.source", "素材查看器 · 源"),
|
||||
("viewer.program", "序列查看器 · 节目"),
|
||||
// --- widget-baked strings (synced to gpui_widgets::i18n) ---
|
||||
("viewer.safe_frames", "安全框"),
|
||||
("viewer.zoom", "缩放"),
|
||||
("viewer.no_frame_source", "无帧源"),
|
||||
("effect_stack.empty", "未选择"),
|
||||
("effect_stack.add", "+ 添加效果"),
|
||||
// --- inspector ---
|
||||
("inspector.params", "参数(占位)"),
|
||||
];
|
||||
@@ -534,6 +562,40 @@ mod tests {
|
||||
assert_eq!(tr("menu.file.save"), "Save");
|
||||
}
|
||||
|
||||
/// `sync_widgets` installs the active language's strings into the widget
|
||||
/// string-table hook, so the widget-baked labels follow the app language.
|
||||
#[test]
|
||||
fn sync_widgets_installs_the_active_language() {
|
||||
let _guard = lang_lock().lock().unwrap();
|
||||
set_language(Language::EnUs);
|
||||
gpui_widgets::i18n::clear_table(); // simulate a fresh process
|
||||
sync_widgets();
|
||||
assert_eq!(
|
||||
gpui_widgets::i18n::tr("viewer.safe_frames", "安全框").to_string(),
|
||||
"Safe Frames"
|
||||
);
|
||||
assert_eq!(
|
||||
gpui_widgets::i18n::tr("viewer.zoom", "缩放").to_string(),
|
||||
"Zoom"
|
||||
);
|
||||
// Every widget key is covered by the installed table.
|
||||
for key in WIDGET_KEYS {
|
||||
let installed = gpui_widgets::i18n::tr(key, "fallback").to_string();
|
||||
assert_ne!(installed, "fallback", "widget key {key} not synced");
|
||||
}
|
||||
|
||||
set_language(Language::ZhCN);
|
||||
assert_eq!(
|
||||
gpui_widgets::i18n::tr("viewer.safe_frames", "Safe Frames").to_string(),
|
||||
"安全框"
|
||||
);
|
||||
assert_eq!(
|
||||
gpui_widgets::i18n::tr("effect_stack.add", "+ Add Effect").to_string(),
|
||||
"+ 添加效果"
|
||||
);
|
||||
set_language(Language::EnUs);
|
||||
}
|
||||
|
||||
/// The config code round-trips.
|
||||
#[test]
|
||||
fn language_code_round_trips() {
|
||||
|
||||
+699
-4
@@ -36,17 +36,25 @@
|
||||
//! The real engine will later implement the same gateway over the
|
||||
//! `liboakengine` C ABI; only the wiring in [`crate::app`] changes.
|
||||
|
||||
use std::collections::{BTreeSet, HashMap};
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::time::Instant;
|
||||
|
||||
use gpui::effect_stack::{
|
||||
EffectCardKind, EffectData, EffectId, EffectStackDataSource, EffectStackEvent,
|
||||
};
|
||||
use gpui::node_graph::{
|
||||
EdgeData, EdgeId, NodeData, NodeGraphDataSource, NodeGraphEvent, NodeId, PortData,
|
||||
PortDataType, PortId, PortKind,
|
||||
};
|
||||
use gpui::timeline::{
|
||||
ClipData, ClipId, Frame, FrameRange, FrameRate, TimelineDataSource, TrackData, TrackKind,
|
||||
};
|
||||
use gpui::{prelude::*, px, App, Context, Entity, Hsla, Pixels, SharedString};
|
||||
use gpui::{
|
||||
hsla, point, prelude::*, px, App, Context, Entity, Hsla, Pixels, Point, RenderImage,
|
||||
SharedString,
|
||||
};
|
||||
use gpui_widgets::audio_meter::AudioMeterDataSource;
|
||||
use gpui_widgets::project_explorer::{ProjectDataSource, ProjectEntry};
|
||||
use gpui_widgets::viewer::PlaybackClock;
|
||||
@@ -57,6 +65,12 @@ use super::transport::TransportState;
|
||||
/// The demo sequence length: 00:04:18:18 at 25 fps.
|
||||
const SEQUENCE_LENGTH: i64 = 6468;
|
||||
|
||||
/// The synthetic viewer test frame is rendered at a small proxy size (the
|
||||
/// real engine will deliver full-resolution frames; the mock only needs to
|
||||
/// prove the CPU-frame path end to end).
|
||||
const SYNTH_FRAME_WIDTH: u32 = 384;
|
||||
const SYNTH_FRAME_HEIGHT: u32 = 216;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Clocks
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -256,6 +270,125 @@ impl EffectData for MockEffect {
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Node graph model
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// A port on a mock node. `connected` is a model-side cache refreshed by
|
||||
/// [`MockEngine::refresh_port_connectivity`] after every graph mutation.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct MockPort {
|
||||
id: PortId,
|
||||
kind: PortKind,
|
||||
label: SharedString,
|
||||
data_type: PortDataType,
|
||||
connected: bool,
|
||||
}
|
||||
|
||||
impl PortData for MockPort {
|
||||
fn id(&self) -> PortId {
|
||||
self.id
|
||||
}
|
||||
|
||||
fn kind(&self) -> PortKind {
|
||||
self.kind
|
||||
}
|
||||
|
||||
fn label(&self) -> SharedString {
|
||||
self.label.clone()
|
||||
}
|
||||
|
||||
fn data_type(&self) -> PortDataType {
|
||||
self.data_type.clone()
|
||||
}
|
||||
|
||||
fn is_connected(&self) -> bool {
|
||||
self.connected
|
||||
}
|
||||
}
|
||||
|
||||
/// A node in the mock graph (媒体 → 变换 → … → 输出).
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct MockNode {
|
||||
id: NodeId,
|
||||
title: SharedString,
|
||||
position: Point<Pixels>,
|
||||
inputs: Vec<MockPort>,
|
||||
outputs: Vec<MockPort>,
|
||||
header_color: Option<Hsla>,
|
||||
enabled: bool,
|
||||
collapsed: bool,
|
||||
}
|
||||
|
||||
impl NodeData for MockNode {
|
||||
type Port = MockPort;
|
||||
|
||||
fn id(&self) -> NodeId {
|
||||
self.id
|
||||
}
|
||||
|
||||
fn title(&self) -> SharedString {
|
||||
self.title.clone()
|
||||
}
|
||||
|
||||
fn position(&self) -> Point<Pixels> {
|
||||
self.position
|
||||
}
|
||||
|
||||
fn inputs(&self) -> Vec<Self::Port> {
|
||||
self.inputs.clone()
|
||||
}
|
||||
|
||||
fn outputs(&self) -> Vec<Self::Port> {
|
||||
self.outputs.clone()
|
||||
}
|
||||
|
||||
fn header_color(&self) -> Option<Hsla> {
|
||||
self.header_color
|
||||
}
|
||||
|
||||
fn is_collapsed(&self) -> bool {
|
||||
self.collapsed
|
||||
}
|
||||
|
||||
fn is_enabled(&self) -> bool {
|
||||
self.enabled
|
||||
}
|
||||
}
|
||||
|
||||
/// An edge in the mock graph: a connection from an output port to an input
|
||||
/// port.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct MockEdge {
|
||||
id: EdgeId,
|
||||
from_node: NodeId,
|
||||
from_port: PortId,
|
||||
to_node: NodeId,
|
||||
to_port: PortId,
|
||||
}
|
||||
|
||||
impl EdgeData for MockEdge {
|
||||
fn id(&self) -> EdgeId {
|
||||
self.id
|
||||
}
|
||||
|
||||
fn from_node(&self) -> NodeId {
|
||||
self.from_node
|
||||
}
|
||||
|
||||
fn from_port(&self) -> PortId {
|
||||
self.from_port
|
||||
}
|
||||
|
||||
fn to_node(&self) -> NodeId {
|
||||
self.to_node
|
||||
}
|
||||
|
||||
fn to_port(&self) -> PortId {
|
||||
self.to_port
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// The engine
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -281,6 +414,19 @@ pub struct MockEngine {
|
||||
selected_item: Option<u64>,
|
||||
/// Phase counter driving the demo audio levels.
|
||||
meter_phase: u32,
|
||||
/// The demo node graph shown in the node editor.
|
||||
nodes: Vec<MockNode>,
|
||||
/// The demo node-graph edges.
|
||||
edges: Vec<MockEdge>,
|
||||
/// Id allocator for edges added at runtime.
|
||||
next_edge_id: u64,
|
||||
/// The node selection, kept in sync with the node editor (and, later, the
|
||||
/// effect stack) so both views share one selection.
|
||||
node_selection: BTreeSet<NodeId>,
|
||||
/// Cache of the synthetic CPU frames handed to the viewers, keyed by
|
||||
/// monitor. Entries are the playhead frame that produced the image, so a
|
||||
/// paused viewer never regenerates its picture.
|
||||
cpu_frame_cache: Mutex<HashMap<Monitor, (i64, Arc<RenderImage>)>>,
|
||||
}
|
||||
|
||||
impl MockEngine {
|
||||
@@ -307,8 +453,122 @@ impl MockEngine {
|
||||
l: 0.55,
|
||||
a: 1.0,
|
||||
};
|
||||
let node_color = |h: f32| Hsla {
|
||||
h,
|
||||
s: 0.55,
|
||||
l: 0.5,
|
||||
a: 1.0,
|
||||
};
|
||||
|
||||
Self {
|
||||
// The demo node graph: two clips, one through the transform, one
|
||||
// through the blur, merged in the mixer and LUT'd to the viewer.
|
||||
// Port ids are globally unique.
|
||||
let video_type = PortDataType::new("video", hsla(0.55, 0.75, 0.6, 1.0));
|
||||
let audio_type = PortDataType::new("audio", hsla(0.1, 0.7, 0.55, 1.0));
|
||||
let port = |id: u64,
|
||||
kind: PortKind,
|
||||
label: &str,
|
||||
data_type: &PortDataType| MockPort {
|
||||
id: PortId(id),
|
||||
kind,
|
||||
label: label.into(),
|
||||
data_type: data_type.clone(),
|
||||
connected: false,
|
||||
};
|
||||
let node = |id: u64,
|
||||
title: &str,
|
||||
position: (f32, f32),
|
||||
color: f32,
|
||||
inputs: Vec<MockPort>,
|
||||
outputs: Vec<MockPort>| MockNode {
|
||||
id: NodeId(id),
|
||||
title: title.into(),
|
||||
position: point(px(position.0), px(position.1)),
|
||||
inputs,
|
||||
outputs,
|
||||
header_color: Some(node_color(color)),
|
||||
enabled: true,
|
||||
collapsed: false,
|
||||
};
|
||||
let nodes = vec![
|
||||
node(
|
||||
0,
|
||||
"第一稿.mp4",
|
||||
(40.0, 40.0),
|
||||
0.55,
|
||||
vec![],
|
||||
vec![port(1, PortKind::Output, "video", &video_type)],
|
||||
),
|
||||
node(
|
||||
1,
|
||||
"B-roll.mp4",
|
||||
(40.0, 230.0),
|
||||
0.60,
|
||||
vec![],
|
||||
vec![
|
||||
port(3, PortKind::Output, "video", &video_type),
|
||||
port(4, PortKind::Output, "audio", &audio_type),
|
||||
],
|
||||
),
|
||||
node(
|
||||
2,
|
||||
"变换",
|
||||
(320.0, 40.0),
|
||||
0.35,
|
||||
vec![
|
||||
port(20, PortKind::Input, "in", &video_type),
|
||||
port(22, PortKind::Input, "mask", &video_type),
|
||||
],
|
||||
vec![port(21, PortKind::Output, "out", &video_type)],
|
||||
),
|
||||
node(
|
||||
3,
|
||||
"模糊",
|
||||
(320.0, 230.0),
|
||||
0.78,
|
||||
vec![port(30, PortKind::Input, "in", &video_type)],
|
||||
vec![port(31, PortKind::Output, "out", &video_type)],
|
||||
),
|
||||
node(
|
||||
4,
|
||||
"混合",
|
||||
(600.0, 130.0),
|
||||
0.08,
|
||||
vec![
|
||||
port(40, PortKind::Input, "A", &video_type),
|
||||
port(41, PortKind::Input, "B", &video_type),
|
||||
],
|
||||
vec![port(42, PortKind::Output, "out", &video_type)],
|
||||
),
|
||||
node(
|
||||
5,
|
||||
"输出",
|
||||
(880.0, 130.0),
|
||||
0.0,
|
||||
vec![port(50, PortKind::Input, "in", &video_type)],
|
||||
vec![],
|
||||
),
|
||||
];
|
||||
let edge = |id: u64,
|
||||
from_node: u64,
|
||||
from_port: u64,
|
||||
to_node: u64,
|
||||
to_port: u64| MockEdge {
|
||||
id: EdgeId(id),
|
||||
from_node: NodeId(from_node),
|
||||
from_port: PortId(from_port),
|
||||
to_node: NodeId(to_node),
|
||||
to_port: PortId(to_port),
|
||||
};
|
||||
let edges = vec![
|
||||
edge(1, 0, 1, 2, 20),
|
||||
edge(2, 1, 3, 3, 30),
|
||||
edge(3, 2, 21, 4, 40),
|
||||
edge(4, 3, 31, 4, 41),
|
||||
edge(5, 4, 42, 5, 50),
|
||||
];
|
||||
|
||||
let mut this = Self {
|
||||
project: Project {
|
||||
name: "第一稿".into(),
|
||||
path: PathBuf::from("/home/mikesolar/Videos/aaa.ove"),
|
||||
@@ -407,7 +667,16 @@ impl MockEngine {
|
||||
program_playing: false,
|
||||
selected_item: None,
|
||||
meter_phase: 0,
|
||||
}
|
||||
nodes,
|
||||
edges,
|
||||
next_edge_id: 6,
|
||||
node_selection: BTreeSet::new(),
|
||||
cpu_frame_cache: Mutex::new(HashMap::new()),
|
||||
};
|
||||
// The demo graph is born connected: derive every port's `connected`
|
||||
// flag from the edge list.
|
||||
this.refresh_port_connectivity();
|
||||
this
|
||||
}
|
||||
|
||||
/// The current sequence length (also used by the gateway).
|
||||
@@ -476,6 +745,134 @@ impl MockEngine {
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
/// Applies a node-editor request to the mock graph (the "edits are
|
||||
/// requests" loop: the view emits, the engine applies and notifies).
|
||||
pub fn apply_node_graph_event(&mut self, event: &NodeGraphEvent, cx: &mut Context<Self>) {
|
||||
match event {
|
||||
NodeGraphEvent::NodeMovePreview { .. } | NodeGraphEvent::ViewChanged { .. } => {}
|
||||
NodeGraphEvent::NodeMoveRequested { nodes, delta } => {
|
||||
for id in nodes {
|
||||
if let Some(node) = self.nodes.iter_mut().find(|n| n.id() == *id) {
|
||||
node.position = node.position + *delta;
|
||||
}
|
||||
}
|
||||
}
|
||||
NodeGraphEvent::ConnectionRequested { from, to } => {
|
||||
if self.can_connect(*from, *to) {
|
||||
let from_node = self.node_with_port(*from).map(|n| n.id());
|
||||
let to_node = self.node_with_port(*to).map(|n| n.id());
|
||||
if let (Some(from_node), Some(to_node)) = (from_node, to_node) {
|
||||
self.edges.push(MockEdge {
|
||||
id: EdgeId(self.next_edge_id),
|
||||
from_node,
|
||||
from_port: *from,
|
||||
to_node,
|
||||
to_port: *to,
|
||||
});
|
||||
self.next_edge_id += 1;
|
||||
self.refresh_port_connectivity();
|
||||
}
|
||||
}
|
||||
}
|
||||
NodeGraphEvent::DisconnectionRequested { edge } => {
|
||||
if let Some(index) = self.edges.iter().position(|e| e.id() == *edge) {
|
||||
self.edges.remove(index);
|
||||
self.refresh_port_connectivity();
|
||||
}
|
||||
}
|
||||
NodeGraphEvent::DeleteRequested { nodes, edges } => {
|
||||
self.edges.retain(|edge| {
|
||||
!edges.contains(&edge.id())
|
||||
&& !nodes.contains(&edge.from_node())
|
||||
&& !nodes.contains(&edge.to_node())
|
||||
});
|
||||
self.nodes.retain(|node| !nodes.contains(&node.id()));
|
||||
self.node_selection.clear();
|
||||
self.refresh_port_connectivity();
|
||||
}
|
||||
NodeGraphEvent::SelectionChanged { nodes } => {
|
||||
self.node_selection = nodes.clone();
|
||||
}
|
||||
NodeGraphEvent::BackgroundClicked { position } => {
|
||||
// The real app opens an "add node" menu here; the mock logs it.
|
||||
println!("[node editor] add-node menu at graph {position:?} (mock: ignored)");
|
||||
}
|
||||
}
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
/// The node currently under the cursor selection (demo state; the effect
|
||||
/// stack will sync to this once it drives the same selection).
|
||||
pub fn selected_node_ids(&self) -> &BTreeSet<NodeId> {
|
||||
&self.node_selection
|
||||
}
|
||||
|
||||
/// Looks up a node by id (test helper).
|
||||
#[cfg(test)]
|
||||
fn node(&self, id: NodeId) -> Option<&MockNode> {
|
||||
self.nodes.iter().find(|n| n.id() == id)
|
||||
}
|
||||
|
||||
/// Looks up a port by its globally unique id.
|
||||
fn port(&self, id: PortId) -> Option<&MockPort> {
|
||||
self.nodes
|
||||
.iter()
|
||||
.flat_map(|n| n.inputs.iter().chain(n.outputs.iter()))
|
||||
.find(|p| p.id() == id)
|
||||
}
|
||||
|
||||
/// Returns the node that owns `port`.
|
||||
fn node_with_port(&self, port: PortId) -> Option<&MockNode> {
|
||||
self.nodes.iter().find(|n| {
|
||||
n.inputs.iter().any(|p| p.id() == port)
|
||||
|| n.outputs.iter().any(|p| p.id() == port)
|
||||
})
|
||||
}
|
||||
|
||||
/// Whether an edge with the same endpoints already exists.
|
||||
fn edge_exists(&self, from: PortId, to: PortId) -> bool {
|
||||
self.edges
|
||||
.iter()
|
||||
.any(|e| e.from_port() == from && e.to_port() == to)
|
||||
}
|
||||
|
||||
/// Whether there is a directed path from node `from` to node `to`
|
||||
/// following the existing edges (DFS). `from == to` counts as a path.
|
||||
fn reaches(&self, from: NodeId, to: NodeId) -> bool {
|
||||
if from == to {
|
||||
return true;
|
||||
}
|
||||
let mut visited = std::collections::HashSet::new();
|
||||
let mut stack = vec![from];
|
||||
while let Some(current) = stack.pop() {
|
||||
if !visited.insert(current) {
|
||||
continue;
|
||||
}
|
||||
for edge in &self.edges {
|
||||
if edge.from_node() == current {
|
||||
let next = edge.to_node();
|
||||
if next == to {
|
||||
return true;
|
||||
}
|
||||
stack.push(next);
|
||||
}
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
/// Recomputed every port's `connected` flag from the current edge list.
|
||||
fn refresh_port_connectivity(&mut self) {
|
||||
for node in &mut self.nodes {
|
||||
for port in node.inputs.iter_mut().chain(node.outputs.iter_mut()) {
|
||||
port.connected = self
|
||||
.edges
|
||||
.iter()
|
||||
.any(|e| e.from_port() == port.id || e.to_port() == port.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets the row height of every timeline track (demo toolbar).
|
||||
pub fn set_track_height(&mut self, height: Pixels, cx: &mut Context<Self>) {
|
||||
for track in &mut self.tracks {
|
||||
@@ -649,6 +1046,44 @@ impl EffectStackDataSource for MockEngine {
|
||||
}
|
||||
}
|
||||
|
||||
impl NodeGraphDataSource for MockEngine {
|
||||
type Node = MockNode;
|
||||
type Edge = MockEdge;
|
||||
|
||||
fn nodes(&self) -> Vec<Self::Node> {
|
||||
self.nodes.clone()
|
||||
}
|
||||
|
||||
fn edges(&self) -> Vec<Self::Edge> {
|
||||
self.edges.clone()
|
||||
}
|
||||
|
||||
fn can_connect(&self, from: PortId, to: PortId) -> bool {
|
||||
let (Some(from_node), Some(to_node)) = (self.node_with_port(from), self.node_with_port(to))
|
||||
else {
|
||||
return false;
|
||||
};
|
||||
if from_node.id() == to_node.id() {
|
||||
return false;
|
||||
}
|
||||
let (Some(from_port), Some(to_port)) = (self.port(from), self.port(to)) else {
|
||||
return false;
|
||||
};
|
||||
if from_port.kind() != PortKind::Output || to_port.kind() != PortKind::Input {
|
||||
return false;
|
||||
}
|
||||
if from_port.data_type() != to_port.data_type() {
|
||||
return false;
|
||||
}
|
||||
if self.edge_exists(from, to) {
|
||||
return false;
|
||||
}
|
||||
// Connecting from_node → to_node would create a cycle iff there is
|
||||
// already a path from to_node back to from_node.
|
||||
!self.reaches(to_node.id(), from_node.id())
|
||||
}
|
||||
}
|
||||
|
||||
impl ProjectDataSource for MockEngine {
|
||||
fn roots(&self) -> Vec<ProjectEntry> {
|
||||
vec![
|
||||
@@ -698,6 +1133,87 @@ impl MockEngine {
|
||||
pub fn clock_frame(&self, monitor: Monitor, cx: &App) -> Frame {
|
||||
self.clock(monitor).read(cx).transport.frame()
|
||||
}
|
||||
|
||||
/// The synthetic CPU test frame for `monitor`, cached per playhead frame so
|
||||
/// a paused viewer never regenerates its picture. This is the frame the
|
||||
/// source/program viewers display through [`ViewerWidget::set_cpu_frame`],
|
||||
/// proving the CPU-frame path end to end before the real engine lands.
|
||||
pub fn cpu_frame(&self, monitor: Monitor, cx: &App) -> Arc<RenderImage> {
|
||||
let frame = self.clock_frame(monitor, cx);
|
||||
let mut cache = self.cpu_frame_cache.lock().unwrap();
|
||||
if let Some((cached_frame, image)) = cache.get(&monitor) {
|
||||
if *cached_frame == frame.0 {
|
||||
return image.clone();
|
||||
}
|
||||
}
|
||||
let image = Arc::new(self.synthetic_frame(frame));
|
||||
cache.insert(monitor, (frame.0, image.clone()));
|
||||
image
|
||||
}
|
||||
|
||||
/// Generates a synthetic test frame: SMPTE-style color bars with a white
|
||||
/// sweep whose x position follows `frame`, so playback is visibly moving.
|
||||
///
|
||||
/// Samples are computed as F32 RGBA (mirroring the real engine's pixel
|
||||
/// pipeline) and downconverted to BGRA8 for the viewer's CPU-frame path.
|
||||
/// The picture is rendered at a small proxy size ([`SYNTH_FRAME_WIDTH`] ×
|
||||
/// [`SYNTH_FRAME_HEIGHT`]); the real engine delivers full resolution.
|
||||
fn synthetic_frame(&self, frame: Frame) -> RenderImage {
|
||||
let width = SYNTH_FRAME_WIDTH;
|
||||
let height = SYNTH_FRAME_HEIGHT;
|
||||
|
||||
// F32 RGBA samples, then quantized to BGRA8 for the sprite atlas.
|
||||
let mut samples = vec![0.0f32; (width * height * 4) as usize];
|
||||
// SMPTE bars: 75% white, yellow, cyan, green, magenta, red, blue.
|
||||
let bars: [(f32, f32, f32); 7] = [
|
||||
(1.0, 1.0, 1.0),
|
||||
(1.0, 1.0, 0.0),
|
||||
(0.0, 1.0, 1.0),
|
||||
(0.0, 1.0, 0.0),
|
||||
(1.0, 0.0, 1.0),
|
||||
(1.0, 0.0, 0.0),
|
||||
(0.0, 0.0, 1.0),
|
||||
];
|
||||
// Bottom strip: blue, magenta, 75% white, black.
|
||||
let strip: [(f32, f32, f32); 4] = [
|
||||
(0.0, 0.0, 1.0),
|
||||
(1.0, 0.0, 1.0),
|
||||
(0.75, 0.75, 0.75),
|
||||
(0.0, 0.0, 0.0),
|
||||
];
|
||||
// The sweep moves 6 px per frame and wraps around the width, so
|
||||
// transport playback shows up as motion across the picture.
|
||||
let sweep = (frame.0 as f32 * 6.0) % width as f32;
|
||||
let bars_top = height as f32 * 0.66;
|
||||
|
||||
for y in 0..height {
|
||||
for x in 0..width {
|
||||
let in_sweep = (x as f32 - sweep).abs() < 6.0;
|
||||
let color = if in_sweep {
|
||||
(1.0, 1.0, 1.0)
|
||||
} else if (y as f32) < bars_top {
|
||||
bars[((x as f32 / width as f32) * 7.0) as usize]
|
||||
} else {
|
||||
strip[((x as f32 / width as f32) * 4.0) as usize]
|
||||
};
|
||||
let i = ((y * width + x) * 4) as usize;
|
||||
samples[i] = color.0;
|
||||
samples[i + 1] = color.1;
|
||||
samples[i + 2] = color.2;
|
||||
samples[i + 3] = 1.0;
|
||||
}
|
||||
}
|
||||
|
||||
let mut bytes = Vec::with_capacity((width * height * 4) as usize);
|
||||
for i in (0..samples.len()).step_by(4) {
|
||||
bytes.push((samples[i + 2] * 255.0) as u8); // B
|
||||
bytes.push((samples[i + 1] * 255.0) as u8); // G
|
||||
bytes.push((samples[i] * 255.0) as u8); // R
|
||||
bytes.push((samples[i + 3] * 255.0) as u8); // A
|
||||
}
|
||||
let buffer = image::RgbaImage::from_raw(width, height, bytes).expect("synthetic frame");
|
||||
RenderImage::new(smallvec::SmallVec::from_elem(image::Frame::new(buffer), 1))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -799,4 +1315,183 @@ mod tests {
|
||||
assert_eq!(before, after);
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn demo_graph_has_six_nodes_and_five_edges(cx: &mut TestAppContext) {
|
||||
cx.update(|app| {
|
||||
let engine = demo_engine(app);
|
||||
let engine = engine.read(app);
|
||||
let nodes = engine.nodes();
|
||||
let edges = engine.edges();
|
||||
|
||||
assert_eq!(nodes.len(), 6, "media, transform, blur, mixer, viewer");
|
||||
assert_eq!(edges.len(), 5);
|
||||
|
||||
// The chain ends at the viewer node, fed by the mixer.
|
||||
let viewer = nodes
|
||||
.iter()
|
||||
.find(|n| n.title() == "输出")
|
||||
.expect("viewer node");
|
||||
assert_eq!(viewer.inputs().len(), 1);
|
||||
assert!(viewer.outputs().is_empty());
|
||||
|
||||
// Every edge endpoint references an existing port, and every port
|
||||
// connectivity flag matches the edge list.
|
||||
for edge in &edges {
|
||||
assert!(
|
||||
nodes.iter().any(|n| n.id() == edge.from_node()),
|
||||
"edge {} from-node exists",
|
||||
edge.id().0
|
||||
);
|
||||
assert!(
|
||||
nodes.iter().any(|n| n.id() == edge.to_node()),
|
||||
"edge {} to-node exists",
|
||||
edge.id().0
|
||||
);
|
||||
}
|
||||
for node in nodes {
|
||||
for port in node.inputs().into_iter().chain(node.outputs()) {
|
||||
let expected = engine
|
||||
.edges()
|
||||
.iter()
|
||||
.any(|e| e.from_port() == port.id() || e.to_port() == port.id());
|
||||
assert_eq!(port.is_connected(), expected, "port {}", port.id().0);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn can_connect_enforces_direction_type_duplicates_and_cycles(
|
||||
cx: &mut TestAppContext,
|
||||
) {
|
||||
cx.update(|app| {
|
||||
let engine = demo_engine(app);
|
||||
let engine = engine.read(app);
|
||||
|
||||
// A valid, still-free connection: Clip1.video → Transform.in (the
|
||||
// transform already takes the clip-0 path, so this would be the
|
||||
// second input).
|
||||
assert!(engine.can_connect(PortId(3), PortId(20)));
|
||||
// The existing connection is not offered again.
|
||||
assert!(!engine.can_connect(PortId(21), PortId(40)), "duplicate edge");
|
||||
// Input → output is rejected (wrong direction).
|
||||
assert!(!engine.can_connect(PortId(20), PortId(1)), "wrong direction");
|
||||
// A port cannot connect to itself.
|
||||
assert!(!engine.can_connect(PortId(42), PortId(40)), "self connection");
|
||||
// Type mismatch: the clip's audio output is not a video signal.
|
||||
assert!(
|
||||
!engine.can_connect(PortId(4), PortId(20)),
|
||||
"audio cannot feed a video input"
|
||||
);
|
||||
// Unknown ports are rejected.
|
||||
assert!(!engine.can_connect(PortId(999), PortId(20)));
|
||||
assert!(!engine.can_connect(PortId(1), PortId(999)));
|
||||
|
||||
// Cycle rule: the graph flows left-to-right (Clip → Transform →
|
||||
// Mixer → Viewer), so no connection can close a loop — but the
|
||||
// reachability helper behind the rule is exercised directly.
|
||||
assert!(engine.reaches(NodeId(0), NodeId(5)), "main chain path");
|
||||
assert!(!engine.reaches(NodeId(5), NodeId(0)), "no backward path");
|
||||
assert!(engine.reaches(NodeId(4), NodeId(4)));
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn node_edits_apply_to_the_model(cx: &mut TestAppContext) {
|
||||
cx.update(|app| {
|
||||
let engine = demo_engine(app);
|
||||
// Move the viewer node.
|
||||
engine.update(app, |engine, cx| {
|
||||
engine.apply_node_graph_event(
|
||||
&NodeGraphEvent::NodeMoveRequested {
|
||||
nodes: vec![NodeId(5)],
|
||||
delta: point(px(100.0), px(-20.0)),
|
||||
},
|
||||
cx,
|
||||
);
|
||||
});
|
||||
let moved = engine.read(app).node(NodeId(5)).expect("viewer node");
|
||||
assert_eq!(moved.position(), point(px(980.0), px(110.0)));
|
||||
|
||||
// Connect the B-roll clip's video to the transform's unused mask
|
||||
// input, then disconnect the transform → mixer edge.
|
||||
engine.update(app, |engine, cx| {
|
||||
engine.apply_node_graph_event(
|
||||
&NodeGraphEvent::ConnectionRequested {
|
||||
from: PortId(3),
|
||||
to: PortId(22),
|
||||
},
|
||||
cx,
|
||||
);
|
||||
});
|
||||
let engine_read = engine.read(app);
|
||||
assert_eq!(engine_read.edges().len(), 6);
|
||||
assert!(engine_read.port(PortId(22)).is_some_and(|p| p.is_connected()));
|
||||
let edges = engine_read.edges();
|
||||
let extra = edges
|
||||
.iter()
|
||||
.find(|e| e.to_port() == PortId(22))
|
||||
.expect("the new connection");
|
||||
|
||||
engine.update(app, |engine, cx| {
|
||||
engine.apply_node_graph_event(
|
||||
&NodeGraphEvent::DisconnectionRequested { edge: extra.id() },
|
||||
cx,
|
||||
);
|
||||
});
|
||||
assert_eq!(engine.read(app).edges().len(), 5);
|
||||
assert!(
|
||||
!engine.read(app).port(PortId(22)).is_some_and(|p| p.is_connected()),
|
||||
"mask input freed again"
|
||||
);
|
||||
|
||||
// Deleting the blur node takes its incident edges with it.
|
||||
engine.update(app, |engine, cx| {
|
||||
engine.apply_node_graph_event(
|
||||
&NodeGraphEvent::DeleteRequested {
|
||||
nodes: vec![NodeId(3)],
|
||||
edges: vec![],
|
||||
},
|
||||
cx,
|
||||
);
|
||||
});
|
||||
let engine_read = engine.read(app);
|
||||
assert_eq!(engine_read.nodes().len(), 5);
|
||||
assert!(
|
||||
!engine_read.edges().iter().any(|e| e.to_node() == NodeId(3)
|
||||
|| e.from_node() == NodeId(3)),
|
||||
"edges incident to the deleted node are removed"
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn cpu_frame_is_cached_per_playhead_and_advances(cx: &mut TestAppContext) {
|
||||
cx.update(|app| {
|
||||
let engine = demo_engine(app);
|
||||
|
||||
// Same playhead → same cached image (Arc identity), so a paused
|
||||
// viewer never regenerates its picture.
|
||||
let a = engine.read(app).cpu_frame(Monitor::Program, app);
|
||||
let b = engine.read(app).cpu_frame(Monitor::Program, app);
|
||||
assert!(Arc::ptr_eq(&a, &b), "paused frame must be cached");
|
||||
|
||||
// Advancing the playhead produces a different image: the sweep
|
||||
// moved, so playback is visible.
|
||||
engine.update(app, |engine, cx| {
|
||||
engine.step(Monitor::Program, 25, cx);
|
||||
});
|
||||
let c = engine.read(app).cpu_frame(Monitor::Program, app);
|
||||
assert!(!Arc::ptr_eq(&a, &c), "a new playhead frame regenerates");
|
||||
|
||||
// The frame has the documented proxy size and opaque BGRA8 bytes.
|
||||
let size = c.size(0);
|
||||
assert_eq!(size.width, SYNTH_FRAME_WIDTH.into());
|
||||
assert_eq!(size.height, SYNTH_FRAME_HEIGHT.into());
|
||||
let bytes = c.as_bytes(0).expect("single frame");
|
||||
assert_eq!(bytes.len(), (SYNTH_FRAME_WIDTH * SYNTH_FRAME_HEIGHT * 4) as usize);
|
||||
assert!(bytes.chunks_exact(4).all(|px| px[3] == 255), "opaque alpha");
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,7 +43,7 @@ pub mod ids {
|
||||
pub const SOURCE_VIEWER: PanelId = PanelId::new(2);
|
||||
/// The program viewer (序列查看器).
|
||||
pub const PROGRAM_VIEWER: PanelId = PanelId::new(3);
|
||||
/// The node editor placeholder (节点编辑器).
|
||||
/// The node editor (节点编辑器).
|
||||
pub const NODE_EDITOR: PanelId = PanelId::new(4);
|
||||
/// The inspector / effect stack (检查器·效果栈).
|
||||
pub const INSPECTOR: PanelId = PanelId::new(5);
|
||||
|
||||
@@ -14,35 +14,139 @@
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
//! The node editor panel (节点编辑器): a placeholder tab sharing the program
|
||||
//! viewer's dock group.
|
||||
//! The node editor panel (节点编辑器): the real `gpui::node_graph` canvas over
|
||||
//! the engine's mock graph, with the design's zoom controls (+ / − / 适配).
|
||||
//!
|
||||
//! The design puts the node editor in the center, switchable with the program
|
||||
//! viewer. The real `gpui::node_graph` widget exists in the gpui submodule
|
||||
//! but is not wired up yet — this panel is a placeholder surface with the
|
||||
//! zoom controls the design specifies (+ / − / fit).
|
||||
//! The graph is a full [`NodeGraphView`] fed by the [`MockEngine`]'s
|
||||
//! [`NodeGraphDataSource`] implementation. Every gesture the view emits
|
||||
//! (move, connect, disconnect, delete, selection) is forwarded to the engine
|
||||
//! as a request; the engine applies it to its model and notifies, so the
|
||||
//! view re-reads on the next frame. The toolbar buttons drive the viewport
|
||||
//! directly: zoom in/out at the canvas center, or fit the whole graph.
|
||||
|
||||
use gpui::colors::DefaultColors;
|
||||
use gpui::dock::{DockPanel, PanelEvent};
|
||||
use gpui::node_graph::{
|
||||
MAX_ZOOM, MIN_ZOOM, NodeData, NodeElement, NodeGraphDataSource, NodeGraphEvent, NodeGraphView,
|
||||
NodeVisualState,
|
||||
};
|
||||
use gpui::{
|
||||
div, prelude::*, AnyElement, App, ClickEvent, Context, EventEmitter, Render, SharedString,
|
||||
Window,
|
||||
div, point, prelude::*, px, AnyElement, App, Bounds, ClickEvent, Context, Entity,
|
||||
EventEmitter, Pixels, Render, SharedString, Window,
|
||||
};
|
||||
|
||||
use crate::oakui::MockEngine;
|
||||
use crate::panels::ids::NODE_EDITOR;
|
||||
|
||||
/// The node editor placeholder panel.
|
||||
pub struct NodeEditorPanel;
|
||||
/// The node editor panel.
|
||||
pub struct NodeEditorPanel {
|
||||
/// The node-graph canvas over the engine's mock graph.
|
||||
graph: Entity<NodeGraphView<MockEngine>>,
|
||||
engine: Entity<MockEngine>,
|
||||
/// Whether the initial fit-to-window has been applied (the canvas size is
|
||||
/// only known after the first layout).
|
||||
fitted: bool,
|
||||
}
|
||||
|
||||
impl NodeEditorPanel {
|
||||
/// Creates the placeholder.
|
||||
pub fn new(_window: &mut Window, _cx: &mut Context<Self>) -> Self {
|
||||
Self
|
||||
/// Builds the graph canvas over `engine` and routes its edit requests back
|
||||
/// to the engine.
|
||||
pub fn new(
|
||||
engine: Entity<MockEngine>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Self {
|
||||
let graph = cx.new(|cx| NodeGraphView::new(engine.clone(), window, cx));
|
||||
// The "edits are requests" loop: every graph gesture goes to the
|
||||
// engine, which applies it to its model and notifies.
|
||||
cx.subscribe(&graph, |this, _graph, event: &NodeGraphEvent, cx| {
|
||||
this.engine
|
||||
.update(cx, |engine, cx| engine.apply_node_graph_event(event, cx));
|
||||
})
|
||||
.detach();
|
||||
|
||||
Self {
|
||||
graph,
|
||||
engine,
|
||||
fitted: false,
|
||||
}
|
||||
}
|
||||
|
||||
/// The union of every node's bounds in graph space, if the graph is
|
||||
/// non-empty.
|
||||
fn graph_bounds(&self, cx: &App) -> Option<Bounds<Pixels>> {
|
||||
let nodes = self.engine.read(cx).nodes();
|
||||
if nodes.is_empty() {
|
||||
return None;
|
||||
}
|
||||
let mut min_x = f32::MAX;
|
||||
let mut min_y = f32::MAX;
|
||||
let mut max_x = f32::MIN;
|
||||
let mut max_y = f32::MIN;
|
||||
for node in &nodes {
|
||||
let element = NodeElement::from_node(node, NodeVisualState::default());
|
||||
let position = node.position();
|
||||
let width = gpui::node_graph::DEFAULT_NODE_WIDTH;
|
||||
let height = element.height();
|
||||
let (x, y) = (f32::from(position.x), f32::from(position.y));
|
||||
min_x = min_x.min(x);
|
||||
min_y = min_y.min(y);
|
||||
max_x = max_x.max(x + f32::from(width));
|
||||
max_y = max_y.max(y + f32::from(height));
|
||||
}
|
||||
Some(Bounds::from_corners(
|
||||
point(px(min_x), px(min_y)),
|
||||
point(px(max_x), px(max_y)),
|
||||
))
|
||||
}
|
||||
|
||||
/// The canvas size to fit against: the graph view's own painted size once
|
||||
/// known, otherwise the window (before the first layout).
|
||||
fn fit_viewport(&self, window: &Window, cx: &App) -> gpui::Size<Pixels> {
|
||||
let viewport = self.graph.read(cx).viewport_size();
|
||||
if viewport.width > px(0.0) && viewport.height > px(0.0) {
|
||||
viewport
|
||||
} else {
|
||||
window.viewport_size()
|
||||
}
|
||||
}
|
||||
|
||||
/// Fits the whole graph into the canvas (the 适配 button).
|
||||
fn fit_graph(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let Some(rect) = self.graph_bounds(cx) else {
|
||||
return;
|
||||
};
|
||||
let viewport = self.fit_viewport(window, cx);
|
||||
self.graph.update(cx, |graph, cx| {
|
||||
graph.state_mut().fit_to_rect(rect, viewport);
|
||||
cx.notify();
|
||||
});
|
||||
}
|
||||
|
||||
/// Zooms the canvas by `factor` at its center (`+` / `−` buttons).
|
||||
fn zoom(&mut self, factor: f32, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let viewport = self.fit_viewport(window, cx);
|
||||
let anchor = point(viewport.width * 0.5, viewport.height * 0.5);
|
||||
self.graph.update(cx, |graph, cx| {
|
||||
graph.state_mut().zoom_at(anchor, factor);
|
||||
cx.notify();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for NodeEditorPanel {
|
||||
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
// Fit the graph once the canvas size is known (first layout). Before
|
||||
// that the viewport is zero-sized, so ask for another frame instead.
|
||||
if !self.fitted {
|
||||
if self.graph.read(cx).viewport_size() != Default::default() {
|
||||
self.fitted = true;
|
||||
self.fit_graph(window, cx);
|
||||
} else {
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
|
||||
let colors = cx.default_colors().clone();
|
||||
div()
|
||||
.size_full()
|
||||
@@ -57,43 +161,46 @@ impl Render for NodeEditorPanel {
|
||||
.py_1()
|
||||
.border_b_1()
|
||||
.border_color(colors.border)
|
||||
.child(zoom_button(
|
||||
cx,
|
||||
"node-zoom-in",
|
||||
"+",
|
||||
crate::i18n::tr("node.zoom_in"),
|
||||
))
|
||||
.child(zoom_button(
|
||||
cx,
|
||||
"node-zoom-out",
|
||||
"−",
|
||||
crate::i18n::tr("node.zoom_out"),
|
||||
))
|
||||
.child(zoom_button(cx, "node-zoom-in", "+", |this, window, cx| {
|
||||
this.zoom(1.25, window, cx);
|
||||
}))
|
||||
.child(zoom_button(cx, "node-zoom-out", "−", |this, window, cx| {
|
||||
this.zoom(1.0 / 1.25, window, cx);
|
||||
}))
|
||||
.child(zoom_button(
|
||||
cx,
|
||||
"node-zoom-fit",
|
||||
crate::i18n::tr("node.fit"),
|
||||
crate::i18n::tr("node.fit_window"),
|
||||
)),
|
||||
|this, window, cx| this.fit_graph(window, cx),
|
||||
))
|
||||
.child(div().flex_1())
|
||||
.child(
|
||||
div()
|
||||
.text_color(colors.disabled)
|
||||
.child(format!(
|
||||
"{}% · {}–{}",
|
||||
(self.graph.read(cx).state().zoom() * 100.0).round(),
|
||||
MIN_ZOOM,
|
||||
MAX_ZOOM,
|
||||
)),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.debug_selector(|| "node-editor-canvas".into())
|
||||
.flex_1()
|
||||
.flex()
|
||||
.items_center()
|
||||
.justify_center()
|
||||
.text_color(colors.disabled)
|
||||
.child(crate::i18n::tr("node.placeholder")),
|
||||
.min_h_0()
|
||||
.child(self.graph.clone()),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// A small toolbar button (the design's `+`/`−`/`适配` controls).
|
||||
/// A small toolbar button driving the graph viewport.
|
||||
fn zoom_button(
|
||||
cx: &mut Context<NodeEditorPanel>,
|
||||
id: &'static str,
|
||||
label: &'static str,
|
||||
title: &'static str,
|
||||
label: impl IntoElement,
|
||||
action: impl Fn(&mut NodeEditorPanel, &mut Window, &mut Context<NodeEditorPanel>) + 'static,
|
||||
) -> impl gpui::IntoElement {
|
||||
let colors = cx.default_colors().clone();
|
||||
let container = colors.container;
|
||||
@@ -107,11 +214,9 @@ fn zoom_button(
|
||||
.text_color(colors.text)
|
||||
.cursor_pointer()
|
||||
.hover(move |style| style.bg(container))
|
||||
.on_click(
|
||||
cx.listener(move |_this, _event: &ClickEvent, _window, _cx| {
|
||||
println!("[node editor] {title} (placeholder)");
|
||||
}),
|
||||
)
|
||||
.on_click(cx.listener(move |this, _event: &ClickEvent, window, cx| {
|
||||
action(this, window, cx);
|
||||
}))
|
||||
.child(label)
|
||||
}
|
||||
|
||||
@@ -132,3 +237,55 @@ impl DockPanel for NodeEditorPanel {
|
||||
.into_any_element()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use gpui::{TestAppContext, VisualTestContext, size};
|
||||
|
||||
/// Builds the panel in a window and returns a `VisualTestContext` for
|
||||
/// bounds assertions.
|
||||
fn panel_window(
|
||||
cx: &mut TestAppContext,
|
||||
) -> (&'static mut VisualTestContext, Entity<NodeEditorPanel>) {
|
||||
cx.update(|cx| cx.init_colors());
|
||||
let window = cx.open_window(size(px(640.0), px(480.0)), |window, cx| {
|
||||
let engine = cx.new(|cx| crate::oakui::MockEngine::demo(cx));
|
||||
NodeEditorPanel::new(engine, window, cx)
|
||||
});
|
||||
cx.run_until_parked();
|
||||
// The graph canvas reports its size only after the first paint, so the
|
||||
// initial fit applies on the following frame: draw a few more.
|
||||
for _ in 0..3 {
|
||||
cx.update_window(window.into(), |_root, window, app| {
|
||||
let _ = window.draw(app);
|
||||
})
|
||||
.expect("window still open");
|
||||
cx.run_until_parked();
|
||||
}
|
||||
let panel = window.root(cx).expect("node editor panel root");
|
||||
let cx = VisualTestContext::from_window(window.into(), cx).into_mut();
|
||||
(cx, panel)
|
||||
}
|
||||
|
||||
/// The panel lays out a graph canvas below the zoom toolbar, and the
|
||||
/// initial fit centers the graph so every demo node is on screen.
|
||||
#[gpui::test]
|
||||
async fn canvas_fills_the_panel_below_the_toolbar(cx: &mut TestAppContext) {
|
||||
let (cx, panel) = panel_window(cx);
|
||||
|
||||
let canvas = cx
|
||||
.debug_bounds("node-editor-canvas")
|
||||
.expect("graph canvas rendered");
|
||||
assert!(canvas.size.width > px(0.0));
|
||||
assert!(canvas.size.height > px(0.0));
|
||||
|
||||
// The initial fit moved the viewport off the default origin, so the
|
||||
// graph is framed rather than clipped at the corner.
|
||||
let state = cx.read(|app| {
|
||||
panel.read(app).graph.read(app).state().clone()
|
||||
});
|
||||
assert_ne!(state.offset(), gpui::node_graph::GraphViewState::new().offset());
|
||||
assert!(state.zoom() > 0.0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,6 +40,9 @@ pub struct ProgramViewerPanel {
|
||||
viewer: Entity<ViewerWidget<MockClock>>,
|
||||
meter: Entity<AudioLevelMeter<MockEngine>>,
|
||||
engine: Entity<MockEngine>,
|
||||
/// The last CPU frame handed to the viewer (compared by `Arc` identity so
|
||||
/// a paused playhead does not re-upload the picture every frame).
|
||||
last_cpu_frame: Option<std::sync::Arc<gpui::RenderImage>>,
|
||||
}
|
||||
|
||||
impl ProgramViewerPanel {
|
||||
@@ -69,12 +72,28 @@ impl ProgramViewerPanel {
|
||||
viewer,
|
||||
meter,
|
||||
engine,
|
||||
last_cpu_frame: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Pushes the engine's synthetic test frame into the viewer, but only when
|
||||
/// it actually changed (the engine caches one image per playhead frame).
|
||||
fn sync_frame(&mut self, cx: &mut Context<Self>) {
|
||||
let frame = self.engine.read(cx).cpu_frame(Monitor::Program, cx);
|
||||
if self.last_cpu_frame.as_ref().is_none_or(|last| !std::sync::Arc::ptr_eq(last, &frame))
|
||||
{
|
||||
self.last_cpu_frame = Some(frame.clone());
|
||||
let frame = frame.clone();
|
||||
self.viewer
|
||||
.update(cx, |viewer, cx| viewer.set_cpu_frame(Some(frame), cx));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for ProgramViewerPanel {
|
||||
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
self.sync_frame(cx);
|
||||
|
||||
let colors = cx.default_colors().clone();
|
||||
let format = self
|
||||
.engine
|
||||
|
||||
@@ -20,7 +20,8 @@
|
||||
use gpui::colors::DefaultColors;
|
||||
use gpui::dock::{DockPanel, PanelEvent};
|
||||
use gpui::{
|
||||
div, prelude::*, AnyElement, App, Context, Entity, EventEmitter, Render, SharedString, Window,
|
||||
div, prelude::*, AnyElement, App, Context, Entity, EventEmitter, Render, SharedString,
|
||||
Window,
|
||||
};
|
||||
use gpui_widgets::viewer::{ViewerEvent, ViewerWidget};
|
||||
|
||||
@@ -33,6 +34,9 @@ use crate::panels::ids::SOURCE_VIEWER;
|
||||
pub struct SourceViewerPanel {
|
||||
viewer: Entity<ViewerWidget<MockClock>>,
|
||||
engine: Entity<MockEngine>,
|
||||
/// The last CPU frame handed to the viewer (compared by `Arc` identity so
|
||||
/// a paused playhead does not re-upload the picture every frame).
|
||||
last_cpu_frame: Option<std::sync::Arc<gpui::RenderImage>>,
|
||||
}
|
||||
|
||||
impl SourceViewerPanel {
|
||||
@@ -56,12 +60,31 @@ impl SourceViewerPanel {
|
||||
})
|
||||
.detach();
|
||||
|
||||
Self { viewer, engine }
|
||||
Self {
|
||||
viewer,
|
||||
engine,
|
||||
last_cpu_frame: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Pushes the engine's synthetic test frame into the viewer, but only when
|
||||
/// it actually changed (the engine caches one image per playhead frame).
|
||||
fn sync_frame(&mut self, cx: &mut Context<Self>) {
|
||||
let frame = self.engine.read(cx).cpu_frame(Monitor::Source, cx);
|
||||
if self.last_cpu_frame.as_ref().is_none_or(|last| !std::sync::Arc::ptr_eq(last, &frame))
|
||||
{
|
||||
self.last_cpu_frame = Some(frame.clone());
|
||||
let frame = frame.clone();
|
||||
self.viewer
|
||||
.update(cx, |viewer, cx| viewer.set_cpu_frame(Some(frame), cx));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for SourceViewerPanel {
|
||||
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
self.sync_frame(cx);
|
||||
|
||||
let colors = cx.default_colors().clone();
|
||||
let format = self
|
||||
.engine
|
||||
|
||||
Generated
+1666
-3
File diff suppressed because it is too large
Load Diff
+1
-1
@@ -37,7 +37,7 @@ clap = { version = "4", features = ["derive"] }
|
||||
# deferral (src/deferred.rs). The extern declarations in src/ffi.rs mirror
|
||||
# the engine headers verbatim and resolve against this rlib the moment a
|
||||
# family is wrapped -- no manifest change needed.
|
||||
oakfacade = { path = "../../src/facade/rust" }
|
||||
oakengine = { path = "../../src/engine/rust" }
|
||||
|
||||
[profile.release]
|
||||
panic = "unwind"
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
//! Facade-family availability, mirroring `src/facade/rust/src/deferred.rs`.
|
||||
//!
|
||||
//! Every `oak-cli` subcommand depends on one or more families of the
|
||||
//! `oakengine_*` C ABI. Those families live in the `oakfacade` crate, and
|
||||
//! `oakengine_*` C ABI. Those families live in the `oakengine` crate, and
|
||||
//! some of them are **deferred**: the facade does not wrap them yet, so the
|
||||
//! subcommands must report a clear "not yet available" error instead of
|
||||
//! calling into the facade (the calls would not link, and faking behavior
|
||||
@@ -47,7 +47,7 @@ pub struct DeferredFamily {
|
||||
pub const INIT: DeferredFamily = DeferredFamily {
|
||||
name: "init",
|
||||
headers: "init.h",
|
||||
reason: "the facade shell (oakengine_init/shutdown) is not wrapped in oakfacade yet (its scope table covers only undo/common/audio/plugin)",
|
||||
reason: "the facade shell (oakengine_init/shutdown) is not wrapped in oakengine yet (its scope table covers only undo/common/audio/plugin)",
|
||||
};
|
||||
|
||||
/// `project.h` + `footage.h` — the oaknode module family.
|
||||
@@ -80,13 +80,13 @@ pub const TIMELINE: DeferredFamily = DeferredFamily {
|
||||
pub const RENDER: DeferredFamily = DeferredFamily {
|
||||
name: "render",
|
||||
headers: "renderer.h",
|
||||
reason: "deferred for session scope: the engine renderer.h family is not wrapped in oakfacade yet (no structural blocker)",
|
||||
reason: "deferred for session scope: the engine renderer.h family is not wrapped in oakengine yet (no structural blocker)",
|
||||
};
|
||||
|
||||
/// `exporter.h` — export/encode family.
|
||||
///
|
||||
/// Facade deferred.rs: exporter is a "genuinely facade-only area" (the
|
||||
/// liboakengine assembly layer) with no files in the oakfacade crate.
|
||||
/// liboakengine assembly layer) with no files in the oakengine crate.
|
||||
pub const EXPORT: DeferredFamily = DeferredFamily {
|
||||
name: "exporter",
|
||||
headers: "exporter.h",
|
||||
@@ -107,7 +107,7 @@ pub fn require(families: &[&DeferredFamily]) -> Result<(), String> {
|
||||
detail.push_str(&format!("\n - {} ({}): {}", f.name, f.headers, f.reason));
|
||||
}
|
||||
Err(format!(
|
||||
"not yet available in the Rust facade (oakfacade): these family(ies) are still deferred \
|
||||
"not yet available in the Rust facade (oakengine): these family(ies) are still deferred \
|
||||
(see src/facade/rust/src/deferred.rs):{detail}"
|
||||
))
|
||||
}
|
||||
@@ -126,7 +126,7 @@ mod tests {
|
||||
let err = require(&[&INIT]).unwrap_err();
|
||||
assert!(err.contains("not yet available"));
|
||||
assert!(err.contains("init"));
|
||||
assert!(err.contains("oakfacade"));
|
||||
assert!(err.contains("oakengine"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
+3
-3
@@ -28,15 +28,15 @@
|
||||
//! - `engine/include/oakengine/exporter.h` (export options + render)
|
||||
//!
|
||||
//! All of these families are **deferred** in the Rust facade crate
|
||||
//! (`oakfacade`, `src/facade/rust/src/deferred.rs`), so none of the symbols
|
||||
//! (`oakengine`, `src/facade/rust/src/deferred.rs`), so none of the symbols
|
||||
//! below is referenced from this crate yet — the subcommands gate on
|
||||
//! [`crate::deferred`] and report "not yet available" instead of calling
|
||||
//! them. The declarations exist so that:
|
||||
//!
|
||||
//! 1. the exact contract the CLI expects is pinned in one place (types,
|
||||
//! signatures, string conventions, error codes), and
|
||||
//! 2. when a family is wrapped by oakfacade, the call-through code in
|
||||
//! `src/cmd/` resolves against the already-linked `oakfacade` rlib
|
||||
//! 2. when a family is wrapped by oakengine, the call-through code in
|
||||
//! `src/cmd/` resolves against the already-linked `oakengine` rlib
|
||||
//! without any manifest or signature churn.
|
||||
//!
|
||||
//! Nothing here is ever called today, so no symbol needs to exist in the
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
//! 64 usage error.
|
||||
//!
|
||||
//! The facade families every subcommand depends on (init/project/timeline/
|
||||
//! render/footage/exporter) are still **deferred** in the `oakfacade` crate
|
||||
//! render/footage/exporter) are still **deferred** in the `oakengine` crate
|
||||
//! (see `src/facade/rust/src/deferred.rs`), so each subcommand validates its
|
||||
//! arguments faithfully, then reports the deferral with its reason and exits
|
||||
//! with the C++-compatible code — never crashing, never faking output.
|
||||
|
||||
@@ -71,7 +71,9 @@ fn info_on_a_fixture_reports_not_yet_available() {
|
||||
let (code, _stdout, stderr) = run(&["info", "tests/project_with_footage.ove"]);
|
||||
assert_eq!(code, 1);
|
||||
assert!(stderr.contains("error: info: not yet available"), "stderr: {stderr}");
|
||||
assert!(stderr.contains("oakfacade"));
|
||||
// The crate was renamed oakfacade -> oakengine; the deferral reason
|
||||
// names the current crate.
|
||||
assert!(stderr.contains("oakengine"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
+1
-1
Submodule gpui updated: 16ae7c42df...49e471ae64
+2
-1
@@ -784,13 +784,14 @@ name = "oakcore-rs"
|
||||
version = "0.1.0"
|
||||
|
||||
[[package]]
|
||||
name = "oakfacade"
|
||||
name = "oakengine"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"oakaudio",
|
||||
"oakcodec",
|
||||
"oakcommon",
|
||||
"oakcore-rs",
|
||||
"oaknode",
|
||||
"oakplugin",
|
||||
"oakrender",
|
||||
@@ -0,0 +1,70 @@
|
||||
[package]
|
||||
name = "oakengine"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
description = "Oak Video Editor facade: re-exports the frozen oakengine_* C ABI over the module C ABIs (Rust)"
|
||||
license = "GPL-3.0-or-later"
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib", "staticlib", "rlib"]
|
||||
|
||||
[profile.release]
|
||||
# FFI discipline: panics must be catchable at every exported entry.
|
||||
panic = "unwind"
|
||||
|
||||
[dependencies]
|
||||
# NDJSON control-plane protocol for the worker session (src/worker.rs) and
|
||||
# the shm error formatting in src/ipc.rs.
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
# POSIX shm_open/mmap/munmap/shm_unlink constants + syscalls for the
|
||||
# shared-memory frame-slot transport (src/ipc.rs).
|
||||
libc = "0.2"
|
||||
|
||||
# Every module call crosses the module C ABI as an `extern "C"` import
|
||||
# (src/bridge/). The module crates below are REAL dependencies so their
|
||||
# `#[no_mangle]` exports are linked into the final `liboakengine` cdylib:
|
||||
# the dylib then carries the module C ABIs itself (oakundo_*, oakcommon_*,
|
||||
# oaktimeline_*, oakcodec_*, oakaudio_*, oakrender_*, oaktask_*,
|
||||
# oakplugin_*, oaknode_*) alongside the facade's oakengine_* exports.
|
||||
#
|
||||
# The crates are linked WITHOUT their `test-stubs` features so the real
|
||||
# exports ship. Cross-module calls that the modules resolve with
|
||||
# dlsym(RTLD_DEFAULT) (oaknode, oakplugin, oakrender) now resolve against
|
||||
# the sibling modules inside the same dylib; the remaining undefined
|
||||
# imports are the C++ host-provided symbols (`oakcore_audioparams_*`,
|
||||
# `oakcore_rational_*` from liboakcore, `fb_*` from ffmpeg_bridge), which
|
||||
# build.rs leaves as runtime lookups for the host app.
|
||||
#
|
||||
# Tests link the same crates; the dev-dependencies below re-declare
|
||||
# oakcommon/oakplugin WITH `test-stubs` so their in-crate C ABI mocks
|
||||
# (the ffmpeg_bridge replacement and the oakrender dlsym stubs) are
|
||||
# compiled into the test binaries (features union with the normal
|
||||
# dependencies for test builds).
|
||||
#
|
||||
# NOTE (oaktimeline/oaktask): their `test-stubs` features are never used
|
||||
# here — the in-crate mocks define `oakundo_command_init` etc., which
|
||||
# would collide with the real oakundo rlib in one binary. Without
|
||||
# test-stubs their real exports reference the oaknode/oakundo/oakcommon
|
||||
# C ABI symbols as link-time externs, provided by the sibling crates in
|
||||
# the same dylib (or by the dev-dependency rlibs in a test binary).
|
||||
#
|
||||
# rustc would normally prune rlibs that are only touched through
|
||||
# `extern "C"` imports from the link; src/linkage.rs anchors every crate
|
||||
# (and oakcore-rs) so the linker pulls their object files.
|
||||
oakcore-rs = { path = "../../oakcore-rs" }
|
||||
oakundo = { path = "../../undo/rust" }
|
||||
oakcommon = { path = "../../common/rust" }
|
||||
oaktimeline = { path = "../../timeline/rust" }
|
||||
oakcodec = { path = "../../codec/rust" }
|
||||
oakaudio = { path = "../../audio/rust" }
|
||||
oakrender = { path = "../../render/rust" }
|
||||
oaktask = { path = "../../task/rust" }
|
||||
oakplugin = { path = "../../plugin/rust" }
|
||||
oaknode = { path = "../../node/rust" }
|
||||
|
||||
[dev-dependencies]
|
||||
# Test-only feature union (see the comment above): tests keep the
|
||||
# in-crate mocks these features compile.
|
||||
oakcommon = { path = "../../common/rust", features = ["test-stubs"] }
|
||||
oakplugin = { path = "../../plugin/rust", features = ["test-stubs"] }
|
||||
@@ -42,9 +42,16 @@ tests/
|
||||
The facade's regular dependencies are `serde`/`serde_json` (the worker's
|
||||
NDJSON control-plane protocol, `src/worker.rs`) and `libc` (POSIX
|
||||
`shm_open`/`mmap`/`munmap`/`shm_unlink` for `src/ipc.rs`). Every module
|
||||
call still crosses the module C ABI as an `extern "C"` import
|
||||
(`src/bridge/`), resolved at the final app link against the module shared
|
||||
libraries.
|
||||
call crosses the module C ABI as an `extern "C"` import (`src/bridge/`),
|
||||
and the module crates themselves are real dependencies: [`linkage`](src/linkage.rs)
|
||||
anchors them so their `#[no_mangle]` exports are linked into the
|
||||
`liboakengine` cdylib — the dylib carries the module C ABIs (oakundo_*,
|
||||
oakcommon_*, oaktimeline_*, oakcodec_*, oakaudio_*, oakrender_*,
|
||||
oaktask_*, oakplugin_*, oaknode_*) next to the facade's oakengine_*
|
||||
exports. The only remaining imports are the C++ host symbols
|
||||
(`oakcore_*` from liboakcore, `fb_*` from ffmpeg_bridge), which
|
||||
`build.rs` leaves as runtime lookups (macOS `-undefined dynamic_lookup`)
|
||||
resolved from the host Oak process.
|
||||
|
||||
### Handle mapping
|
||||
|
||||
@@ -89,16 +96,16 @@ control-plane message serializers remain unwrapped.
|
||||
|
||||
## Testing
|
||||
|
||||
`cargo test` links the module crates' rlibs (dev-dependencies) so the
|
||||
facade's bridge imports resolve:
|
||||
The module crates are real dependencies, so `cargo test` links the same
|
||||
rlibs the cdylib embeds; the dev-dependencies re-declare
|
||||
`oakcommon`/`oakplugin` with their `test-stubs` features so the test
|
||||
binaries keep the in-crate mocks (ffmpeg_bridge stub / render mocks):
|
||||
|
||||
- `oakcommon`/`oakplugin` use their `test-stubs` features (ffmpeg_bridge
|
||||
stub / in-crate render mocks).
|
||||
- `oaknode`/`oaktimeline`/`oaktask` are linked WITHOUT their `test-stubs`
|
||||
features: their in-crate mocks would collide with the real oakundo rlib
|
||||
in one test binary. Without test-stubs their real exports reference the
|
||||
oaknode/oakundo/oakcommon C ABI symbols as link-time externs, which the
|
||||
dev-dependency rlibs provide; oaknode itself resolves cross-module
|
||||
sibling crate rlibs provide; oaknode itself resolves cross-module
|
||||
symbols at runtime with `dlsym(RTLD_DEFAULT)`.
|
||||
- `tests/common/mod.rs` defines the `oakcore_*` (liboakcore) and `fb_*`
|
||||
(libffmpeg_bridge) symbols the oakcodec/oakaudio rlibs reference, and
|
||||
@@ -106,7 +113,9 @@ facade's bridge imports resolve:
|
||||
factory so the oaknode serializer's dlsym lookups resolve in every test
|
||||
binary.
|
||||
- `src/lib.rs`'s test-only `test_link` forces the oakrender/oaknode/
|
||||
oaktimeline/oaktask rlibs into the lib unit-test binary.
|
||||
oaktimeline/oaktask rlibs into the lib unit-test binary (the always-on
|
||||
`src/linkage.rs` anchors are `#[cfg(not(test))]`; they are what embeds
|
||||
the module C ABIs in the cdylib for `cargo build`).
|
||||
|
||||
Families whose wrapped behavior requires the real module dylibs carry
|
||||
`#[ignore]` tests with a documented reason; the smoke tests here exercise
|
||||
@@ -118,7 +127,8 @@ payloads in both directions, including wraparound and full/empty edges.
|
||||
cargo test # 71 tests green + 1 ignored (lib 35: ipc 17 + worker 18;
|
||||
# integration: undo 3, common 4, audio 4, plugin 3, codec 5,
|
||||
# render 6, linkage 1, node 3, timeline 2 + 1 ignored, task 5)
|
||||
cargo build # staticlib + rlib; module symbols resolve at the final app link
|
||||
cargo build # cdylib embeds the module C ABIs; oakcore_*/fb_* stay
|
||||
# runtime lookups (see build.rs)
|
||||
```
|
||||
|
||||
## FFI discipline
|
||||
@@ -0,0 +1,33 @@
|
||||
// Oak Video Editor - Non-Linear Video Editor
|
||||
// Copyright (C) 2026 Oak Team
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
//! Build-time link configuration for the `liboakengine` cdylib.
|
||||
//!
|
||||
//! The dylib now carries the module C ABIs itself (oakundo_*, oakcommon_*,
|
||||
//! ... — see Cargo.toml), so the only remaining undefined imports are the
|
||||
//! C++ host-provided symbols the modules call directly: `oakcore_*`
|
||||
//! (liboakcore's `oakcore_audioparams_*` / `oakcore_rational_*`, called by
|
||||
//! oakcodec) and `fb_find_best_pix_fmt_of_list` (ffmpeg_bridge, called by
|
||||
//! oakcommon's pixel-format helper). Those live in the host Oak process,
|
||||
//! which loads this dylib, so macOS `ld` must accept them as runtime
|
||||
//! lookups instead of link-time errors. Only the cdylib gets this flag —
|
||||
//! the rlib/staticlib (and the worker/cli consumers) are unaffected.
|
||||
|
||||
fn main() {
|
||||
if std::env::var("CARGO_CFG_TARGET_OS").as_deref() == Ok("macos") {
|
||||
println!("cargo:rustc-cdylib-link-arg=-Wl,-undefined,dynamic_lookup");
|
||||
}
|
||||
}
|
||||
@@ -14,7 +14,7 @@
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
//! # oakfacade — the `liboakengine` facade (Rust)
|
||||
//! # oakengine — the `liboakengine` facade (Rust)
|
||||
//!
|
||||
//! Re-exports the frozen `oakengine_*` C ABI
|
||||
//! (`engine/include/oakengine/*.h`) verbatim on top of the module C ABIs
|
||||
@@ -42,12 +42,17 @@
|
||||
//!
|
||||
//! ## Testing
|
||||
//!
|
||||
//! `cargo test` links the module crates' rlibs (dev-dependencies) so the
|
||||
//! bridge imports resolve; `tests/linkage.rs` references every crate to
|
||||
//! force rustc to pull the rlibs into the link. Where a wrapped family
|
||||
//! needs module behavior the crates do not implement yet, the engine
|
||||
//! function is a documented stub and its test carries `#[ignore]` with a
|
||||
//! reason (see README.md).
|
||||
//! The module crates are real dependencies (see Cargo.toml) and
|
||||
//! [`linkage`] anchors them into every link of this crate, so the module
|
||||
//! C ABIs are embedded in the `liboakengine` cdylib next to the facade's
|
||||
//! own exports. `cargo test` links the same crates' rlibs (plus the
|
||||
//! `test-stubs` feature union declared in the dev-dependencies, which
|
||||
//! compiles the oakcommon/oakplugin in-crate mocks); `tests/linkage.rs`
|
||||
//! additionally references every crate for the integration-test binaries
|
||||
//! and `test_link` (below) covers the unit-test binary. Where a wrapped
|
||||
//! family needs module behavior the crates do not implement yet, the
|
||||
//! engine function is a documented stub and its test carries `#[ignore]`
|
||||
//! with a reason (see README.md).
|
||||
|
||||
#![deny(unsafe_op_in_unsafe_fn)]
|
||||
#![warn(missing_docs)]
|
||||
@@ -60,6 +65,8 @@ pub mod deferred;
|
||||
pub mod error;
|
||||
pub mod handle;
|
||||
pub mod ipc;
|
||||
#[cfg(not(test))]
|
||||
pub mod linkage;
|
||||
pub mod node;
|
||||
pub mod plugin;
|
||||
pub mod render;
|
||||
@@ -0,0 +1,66 @@
|
||||
// Oak Video Editor - Non-Linear Video Editor
|
||||
// Copyright (C) 2026 Oak Team
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
//! Linkage anchors — force the module crates' rlibs into every link.
|
||||
//!
|
||||
//! The facade talks to the modules exclusively through `extern "C"`
|
||||
//! imports (src/bridge/), so rustc would otherwise consider the module
|
||||
//! crates unused and prune their rlibs from the link. This module
|
||||
//! references one `#[no_mangle]` export of every module crate (and
|
||||
//! `oakcore-rs`) from a `#[used]` static, which (a) marks each crate as
|
||||
//! used so its rlib reaches the linker and (b) keeps the anchor alive so
|
||||
//! the referenced object files are pulled. For the `liboakengine` cdylib
|
||||
//! this is what actually embeds the module C ABIs (oakundo_*,
|
||||
//! oakcommon_*, ...) into the dylib next to the facade's own oakengine_*
|
||||
//! exports.
|
||||
//!
|
||||
//! The per-crate symbol mirrors the test-force-link in
|
||||
//! tests/common/mod.rs (same paths, same `as usize` cast idiom), so the
|
||||
//! crate/module paths are proven against the current module layouts.
|
||||
|
||||
#![allow(dead_code)]
|
||||
|
||||
/// Pull every module crate into the link. Mirrors
|
||||
/// `tests/common/mod.rs::force_link`; the oakcommon XML/undo anchors are
|
||||
/// repeated because oaknode's serializer resolves those C ABI symbols at
|
||||
/// runtime via dlsym(RTLD_DEFAULT) and they must be present in the dylib
|
||||
/// for that lookup to succeed.
|
||||
fn force_link() -> usize {
|
||||
let fns: [usize; 13] = [
|
||||
// oakcore-rs (pure value types; referenced so its rlib is linked).
|
||||
oakcore_rs::Rational::new(1, 2).numerator() as usize,
|
||||
// One exported C ABI symbol per module crate.
|
||||
oakundo::ffi::undostack::oakundo_undostack_init as usize,
|
||||
oakcommon::ffi::config::oakcommon_config_get_int as usize,
|
||||
oaktimeline::ffi::marker::oaktimeline_marker_list_create as usize,
|
||||
oakcodec::ffi::format::oakcodec_encoding_format_count as usize,
|
||||
oakaudio::ffi::waveform::oakaudio_waveform_length as usize,
|
||||
oakrender::ffi::cache::oakrender_cache_indicator_height as usize,
|
||||
oaktask::ffi::manager::oaktask_manager_init as usize,
|
||||
oakplugin::ffi::oakplugin_host_plugin_count as usize,
|
||||
oaknode::ffi::project::oaknode_project_init as usize,
|
||||
// oaknode's dlsym(RTLD_DEFAULT) targets (see tests/common/mod.rs).
|
||||
oakcommon::ffi::xmlutils::oakcommon_xml_writer_init as usize,
|
||||
oakcommon::ffi::xmlutils::oakcommon_xml_reader_init as usize,
|
||||
oakundo::ffi::command::oakundo_command_init as usize,
|
||||
];
|
||||
fns.iter().sum()
|
||||
}
|
||||
|
||||
/// Keeps [`force_link`] (and through it every referenced export) alive in
|
||||
/// the cdylib/staticlib even though nothing calls it directly.
|
||||
#[used]
|
||||
static FORCE_LINK_ANCHOR: fn() -> usize = force_link;
|
||||
@@ -22,7 +22,7 @@
|
||||
#[path = "common/mod.rs"]
|
||||
mod common;
|
||||
|
||||
use oakfacade::audio::{
|
||||
use oakengine::audio::{
|
||||
oakengine_audio_clear_buffered_output, oakengine_audio_create_instance,
|
||||
oakengine_audio_destroy_instance, oakengine_audio_estimate_envelope_offset,
|
||||
oakengine_audio_get_output_device, oakengine_audio_hard_reset,
|
||||
@@ -22,7 +22,7 @@ mod common;
|
||||
|
||||
use std::ffi::{c_char, c_int};
|
||||
|
||||
use oakfacade::codec::{
|
||||
use oakengine::codec::{
|
||||
oakengine_encoding_codec_is_lossless, oakengine_encoding_codec_is_still_image,
|
||||
oakengine_encoding_codec_name, oakengine_encoding_filename_contains_digit_placeholder,
|
||||
oakengine_encoding_filename_remove_digit_placeholder, oakengine_encoding_format_audio_codec_count,
|
||||
@@ -45,7 +45,7 @@ use oakfacade::codec::{
|
||||
oakengine_encoding_params_video_pix_fmt, oakengine_encoding_params_set_video_pix_fmt,
|
||||
oakengine_encoding_pix_fmt_index,
|
||||
};
|
||||
use oakfacade::common::OakVideoParamsPod;
|
||||
use oakengine::common::OakVideoParamsPod;
|
||||
|
||||
/// Container format / codec metadata queries.
|
||||
#[test]
|
||||
@@ -142,7 +142,7 @@ fn params_handle_round_trip() {
|
||||
let mut vp: OakVideoParamsPod = unsafe { std::mem::zeroed() };
|
||||
assert_eq!(
|
||||
unsafe {
|
||||
oakfacade::common::oakengine_video_params_make(&mut vp, 1920, 1080, 1001, 30000, 4, 1, 1, 0, 1, 1)
|
||||
oakengine::common::oakengine_video_params_make(&mut vp, 1920, 1080, 1001, 30000, 4, 1, 1, 0, 1, 1)
|
||||
},
|
||||
0
|
||||
);
|
||||
@@ -25,7 +25,7 @@ mod common;
|
||||
|
||||
use std::ffi::{c_char, c_int};
|
||||
|
||||
use oakfacade::common::{
|
||||
use oakengine::common::{
|
||||
oakengine_config_get_int, oakengine_config_get_string, oakengine_config_load,
|
||||
oakengine_config_save, oakengine_config_set_error_handler, oakengine_config_set_int,
|
||||
oakengine_config_set_string, oakengine_video_params_bytes_per_pixel,
|
||||
@@ -87,12 +87,12 @@ fn config_error_handler() {
|
||||
assert_eq!(unsafe { oakengine_config_set_error_handler(Some(handler), std::ptr::null_mut()) }, 0);
|
||||
// Report an error through the handler.
|
||||
assert_eq!(unsafe {
|
||||
oakfacade::common::oakengine_config_report_error(c"t".as_ptr(), c"m".as_ptr())
|
||||
oakengine::common::oakengine_config_report_error(c"t".as_ptr(), c"m".as_ptr())
|
||||
}, 0);
|
||||
assert_eq!(CALLED.load(std::sync::atomic::Ordering::SeqCst), 1);
|
||||
// NULL handler clears; reporting then does not invoke.
|
||||
assert_eq!(unsafe { oakengine_config_set_error_handler(None, std::ptr::null_mut()) }, 0);
|
||||
unsafe { oakfacade::common::oakengine_config_report_error(c"t".as_ptr(), c"m".as_ptr()) };
|
||||
unsafe { oakengine::common::oakengine_config_report_error(c"t".as_ptr(), c"m".as_ptr()) };
|
||||
assert_eq!(CALLED.load(std::sync::atomic::Ordering::SeqCst), 1);
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ mod common;
|
||||
|
||||
use std::ffi::{c_char, c_int};
|
||||
|
||||
use oakfacade::node::{
|
||||
use oakengine::node::{
|
||||
oakengine_footage_borrow, oakengine_footage_last_error, oakengine_footage_probe,
|
||||
oakengine_node_connect, oakengine_node_disconnect, oakengine_node_factory_create_from_id,
|
||||
oakengine_node_factory_id_count, oakengine_node_factory_name_from_id,
|
||||
@@ -83,7 +83,7 @@ fn float_value(x: f64) -> OakNodeValue {
|
||||
}
|
||||
|
||||
/// The index of the first project node whose type id matches `id`, or -1.
|
||||
unsafe fn find_node(project: *mut oakfacade::handle::OakEngineProject, id: &str) -> c_int {
|
||||
unsafe fn find_node(project: *mut oakengine::handle::OakEngineProject, id: &str) -> c_int {
|
||||
let count = unsafe { oakengine_project_node_count(project) };
|
||||
for i in 0..count {
|
||||
let node = unsafe { oakengine_project_node_at(project, i) };
|
||||
@@ -137,15 +137,15 @@ fn project_node_keyframe_lifecycle() {
|
||||
0
|
||||
);
|
||||
assert_eq!(
|
||||
unsafe { oakengine_project_set_filename(project, c"/tmp/oakfacade_node_test.ovexml".as_ptr()) },
|
||||
unsafe { oakengine_project_set_filename(project, c"/tmp/oakengine_node_test.ovexml".as_ptr()) },
|
||||
0
|
||||
);
|
||||
let len = unsafe { oakengine_project_filename(project, buf.as_mut_ptr(), 256) };
|
||||
assert!(len > 0);
|
||||
assert!(unsafe { read_buf(&mut buf) }.ends_with("oakfacade_node_test.ovexml"));
|
||||
assert!(unsafe { read_buf(&mut buf) }.ends_with("oakengine_node_test.ovexml"));
|
||||
let len = unsafe { oakengine_project_name(project, buf.as_mut_ptr(), 256) };
|
||||
assert!(len > 0);
|
||||
assert_eq!(unsafe { read_buf(&mut buf) }, "oakfacade_node_test");
|
||||
assert_eq!(unsafe { read_buf(&mut buf) }, "oakengine_node_test");
|
||||
|
||||
// ---- factory + node creation ---------------------------------------
|
||||
let factory_count = oakengine_node_factory_id_count();
|
||||
@@ -269,9 +269,9 @@ fn project_node_keyframe_lifecycle() {
|
||||
assert!((at.f[0] - 0.5).abs() < 1e-6);
|
||||
|
||||
// ---- project save → fresh load round-trip ---------------------------
|
||||
let path = c"/tmp/oakfacade_node_test.ovexml";
|
||||
let path = c"/tmp/oakengine_node_test.ovexml";
|
||||
assert_eq!(unsafe { oakengine_project_save(project, path.as_ptr()) }, 0);
|
||||
assert!(std::path::Path::new("/tmp/oakfacade_node_test.ovexml").exists());
|
||||
assert!(std::path::Path::new("/tmp/oakengine_node_test.ovexml").exists());
|
||||
|
||||
unsafe { oakengine_project_free(project) };
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
#[path = "common/mod.rs"]
|
||||
mod common;
|
||||
|
||||
use oakfacade::plugin::{
|
||||
use oakengine::plugin::{
|
||||
oakengine_plugin_load_plugins, oakengine_plugin_node_push_button_clicked,
|
||||
oakengine_plugin_set_active_viewer_provider, oakengine_plugin_set_progress_reporter_factory,
|
||||
};
|
||||
@@ -27,7 +27,7 @@ use oakfacade::plugin::{
|
||||
/// Callback registration round-trips (NULL clears).
|
||||
#[test]
|
||||
fn provider_registration() {
|
||||
unsafe extern "C" fn viewer(_userdata: *mut std::ffi::c_void) -> *mut oakfacade::handle::OakEngineNode {
|
||||
unsafe extern "C" fn viewer(_userdata: *mut std::ffi::c_void) -> *mut oakengine::handle::OakEngineNode {
|
||||
std::ptr::null_mut()
|
||||
}
|
||||
assert_eq!(unsafe {
|
||||
@@ -26,7 +26,7 @@ mod common;
|
||||
|
||||
use std::ffi::{c_char, c_double};
|
||||
|
||||
use oakfacade::render::{
|
||||
use oakengine::render::{
|
||||
oakengine_color_last_error, oakengine_color_manager_get_config_filename,
|
||||
oakengine_color_processor_convert_color, oakengine_color_processor_create,
|
||||
oakengine_color_processor_free, oakengine_color_processor_is_valid,
|
||||
@@ -34,11 +34,11 @@ mod common;
|
||||
|
||||
use std::ffi::{c_char, c_int, c_void};
|
||||
|
||||
use oakfacade::node::{
|
||||
use oakengine::node::{
|
||||
oakengine_node_free, oakengine_project_create, oakengine_project_free, oakengine_project_new,
|
||||
oakengine_project_root, oakengine_project_set_filename,
|
||||
};
|
||||
use oakfacade::task::{
|
||||
use oakengine::task::{
|
||||
oakengine_cli_task_dialog_run, oakengine_task_cancel, oakengine_task_create_export,
|
||||
oakengine_task_create_project_import, oakengine_task_create_project_load,
|
||||
oakengine_task_create_project_load_otio, oakengine_task_create_project_save,
|
||||
@@ -187,7 +187,7 @@ fn project_task_lifecycle() {
|
||||
|
||||
// ---- save task on a real project → sync run writes the file ----------
|
||||
let save_path = std::env::temp_dir().join(format!(
|
||||
"oakfacade_task_save_{}.ovexml",
|
||||
"oakengine_task_save_{}.ovexml",
|
||||
std::process::id()
|
||||
));
|
||||
let save_c = std::ffi::CString::new(save_path.to_str().unwrap()).unwrap();
|
||||
@@ -217,7 +217,7 @@ fn project_task_lifecycle() {
|
||||
// project's own filename; NULL without one, a real task with one. ------
|
||||
assert!(unsafe { oakengine_task_create_project_save_otio(project) }.is_null());
|
||||
assert_eq!(
|
||||
unsafe { oakengine_project_set_filename(project, c"/tmp/oakfacade_task_otio.otio".as_ptr()) },
|
||||
unsafe { oakengine_project_set_filename(project, c"/tmp/oakengine_task_otio.otio".as_ptr()) },
|
||||
0
|
||||
);
|
||||
let otio_task = unsafe { oakengine_task_create_project_save_otio(project) };
|
||||
@@ -31,12 +31,12 @@ mod common;
|
||||
|
||||
use std::ffi::{c_char, c_int};
|
||||
|
||||
use oakfacade::handle::{box_handle, OakEngineNode};
|
||||
use oakfacade::node::{
|
||||
use oakengine::handle::{box_handle, OakEngineNode};
|
||||
use oakengine::node::{
|
||||
oakengine_footage_borrow, oakengine_project_create, oakengine_project_free,
|
||||
oakengine_project_new,
|
||||
};
|
||||
use oakfacade::timeline::{
|
||||
use oakengine::timeline::{
|
||||
oakengine_block_get_range, oakengine_block_get_track, oakengine_block_is_enabled,
|
||||
oakengine_block_is_gap, oakengine_block_link_count, oakengine_block_next,
|
||||
oakengine_block_prev, oakengine_block_set_enabled, oakengine_block_set_length_and_media_out,
|
||||
@@ -103,7 +103,7 @@ fn force_runtime_syms() -> usize {
|
||||
|
||||
/// Convert a facade `CHandle` to the layout-identical oaknode `CHandle`
|
||||
/// (distinct Rust types over the same C ABI struct).
|
||||
fn to_node_handle(h: oakfacade::handle::CHandle) -> oaknode::handle::CHandle {
|
||||
fn to_node_handle(h: oakengine::handle::CHandle) -> oaknode::handle::CHandle {
|
||||
oaknode::handle::CHandle {
|
||||
ctx: h.ctx,
|
||||
addref: h.addref,
|
||||
@@ -113,8 +113,8 @@ fn to_node_handle(h: oakfacade::handle::CHandle) -> oaknode::handle::CHandle {
|
||||
}
|
||||
|
||||
/// Convert an oaknode `CHandle` back to the facade `CHandle`.
|
||||
fn to_facade_handle(h: oaknode::handle::CHandle) -> oakfacade::handle::CHandle {
|
||||
oakfacade::handle::CHandle {
|
||||
fn to_facade_handle(h: oaknode::handle::CHandle) -> oakengine::handle::CHandle {
|
||||
oakengine::handle::CHandle {
|
||||
ctx: h.ctx,
|
||||
addref: h.addref,
|
||||
release: h.release,
|
||||
@@ -28,7 +28,7 @@ mod common;
|
||||
use std::ffi::{c_char, c_int, c_void};
|
||||
use std::sync::atomic::{AtomicI32, Ordering};
|
||||
|
||||
use oakfacade::undo::{
|
||||
use oakengine::undo::{
|
||||
oakengine_undo_can_redo, oakengine_undo_can_undo, oakengine_undo_clear,
|
||||
oakengine_undo_command_create, oakengine_undo_command_create_multi,
|
||||
oakengine_undo_command_free, oakengine_undo_command_multi_add_child,
|
||||
@@ -53,16 +53,28 @@ static CMD_FREE_COUNT: AtomicI32 = AtomicI32::new(0);
|
||||
static STK_REDO_COUNT: AtomicI32 = AtomicI32::new(0);
|
||||
static STK_UNDO_COUNT: AtomicI32 = AtomicI32::new(0);
|
||||
|
||||
/// Stack-test callbacks: bump only the `STK_*` counters. They must not
|
||||
/// touch the `CMD_*` counters — the command-lifecycle tests reset and
|
||||
/// assert those in parallel threads, so a stray bump here would race.
|
||||
unsafe extern "C" fn redo_cb(_userdata: *mut c_void) {
|
||||
CMD_REDO_COUNT.fetch_add(1, Ordering::SeqCst);
|
||||
STK_REDO_COUNT.fetch_add(1, Ordering::SeqCst);
|
||||
}
|
||||
|
||||
unsafe extern "C" fn undo_cb(_userdata: *mut c_void) {
|
||||
CMD_UNDO_COUNT.fetch_add(1, Ordering::SeqCst);
|
||||
STK_UNDO_COUNT.fetch_add(1, Ordering::SeqCst);
|
||||
}
|
||||
|
||||
/// Command-lifecycle-only callbacks: bump only the `CMD_*` counters. The
|
||||
/// serialized stack test runs in a parallel thread and must not flip
|
||||
/// these.
|
||||
unsafe extern "C" fn cmd_redo_cb(_userdata: *mut c_void) {
|
||||
CMD_REDO_COUNT.fetch_add(1, Ordering::SeqCst);
|
||||
}
|
||||
|
||||
unsafe extern "C" fn cmd_undo_cb(_userdata: *mut c_void) {
|
||||
CMD_UNDO_COUNT.fetch_add(1, Ordering::SeqCst);
|
||||
}
|
||||
|
||||
unsafe extern "C" fn free_cb(_userdata: *mut c_void) {
|
||||
CMD_FREE_COUNT.fetch_add(1, Ordering::SeqCst);
|
||||
}
|
||||
@@ -77,8 +89,8 @@ fn command_create_redo_undo_free() {
|
||||
let cmd = unsafe {
|
||||
oakengine_undo_command_create(
|
||||
c"custom".as_ptr(),
|
||||
Some(redo_cb),
|
||||
Some(undo_cb),
|
||||
Some(cmd_redo_cb),
|
||||
Some(cmd_undo_cb),
|
||||
Some(free_cb),
|
||||
std::ptr::null_mut(),
|
||||
)
|
||||
@@ -108,8 +120,8 @@ fn multi_command_add_child_count_redo() {
|
||||
let child = unsafe {
|
||||
oakengine_undo_command_create(
|
||||
c"child".as_ptr(),
|
||||
Some(redo_cb),
|
||||
Some(undo_cb),
|
||||
Some(cmd_redo_cb),
|
||||
Some(cmd_undo_cb),
|
||||
None,
|
||||
std::ptr::null_mut(),
|
||||
)
|
||||
@@ -1,55 +0,0 @@
|
||||
[package]
|
||||
name = "oakfacade"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
description = "Oak Video Editor facade: re-exports the frozen oakengine_* C ABI over the module C ABIs (Rust)"
|
||||
license = "GPL-3.0-or-later"
|
||||
|
||||
[lib]
|
||||
crate-type = ["staticlib", "rlib"]
|
||||
|
||||
[profile.release]
|
||||
# FFI discipline: panics must be catchable at every exported entry.
|
||||
panic = "unwind"
|
||||
|
||||
[dependencies]
|
||||
# NDJSON control-plane protocol for the worker session (src/worker.rs) and
|
||||
# the shm error formatting in src/ipc.rs.
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
# POSIX shm_open/mmap/munmap/shm_unlink constants + syscalls for the
|
||||
# shared-memory frame-slot transport (src/ipc.rs).
|
||||
libc = "0.2"
|
||||
|
||||
# Every module call crosses the module C ABI as an `extern "C"` import
|
||||
# (src/bridge/), resolved at the final link against the module shared
|
||||
# libraries (see README.md).
|
||||
#
|
||||
# `cargo test` links the module crates' rlibs instead (dev-dependencies
|
||||
# below): the crates' `#[no_mangle]` exports satisfy the facade's bridge
|
||||
# imports, so the smoke tests exercise the real module code where the
|
||||
# crates implement it. Tests reference every crate so rustc pulls the
|
||||
# rlibs into the link (see tests/common/mod.rs).
|
||||
#
|
||||
# test-stubs on oakcommon/oakplugin compiles those crates' in-crate C ABI
|
||||
# mocks: oakcommon's stub replaces the ffmpeg_bridge symbol, and
|
||||
# oakplugin's stubs replace its runtime dlsym lookups.
|
||||
#
|
||||
# NOTE (oaktimeline/oaktask): linked WITHOUT their `test-stubs` features.
|
||||
# Their in-crate mocks define `oakundo_command_init` etc., which would
|
||||
# collide with the real oakundo rlib in one test binary; without
|
||||
# test-stubs their real exports reference the oaknode/oakundo/oakcommon
|
||||
# C ABI symbols as link-time externs, which the dev-dependency rlibs
|
||||
# (oaknode, oakundo, oakcommon[test-stubs]) provide. oaknode itself
|
||||
# resolves cross-module symbols at runtime with dlsym(RTLD_DEFAULT), which
|
||||
# finds the linked rlibs in the test binary (see src/bridge/node.rs).
|
||||
[dev-dependencies]
|
||||
oakundo = { path = "../../undo/rust" }
|
||||
oakcodec = { path = "../../codec/rust" }
|
||||
oakaudio = { path = "../../audio/rust" }
|
||||
oakrender = { path = "../../render/rust" }
|
||||
oakcommon = { path = "../../common/rust", features = ["test-stubs"] }
|
||||
oakplugin = { path = "../../plugin/rust", features = ["test-stubs"] }
|
||||
oaknode = { path = "../../node/rust" }
|
||||
oaktimeline = { path = "../../timeline/rust" }
|
||||
oaktask = { path = "../../task/rust" }
|
||||
Generated
+392
-5
@@ -2,6 +2,21 @@
|
||||
# It is not intended for manual editing.
|
||||
version = 4
|
||||
|
||||
[[package]]
|
||||
name = "adler2"
|
||||
version = "2.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
|
||||
|
||||
[[package]]
|
||||
name = "aho-corasick"
|
||||
version = "1.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c982642fa9e8606056828ee9a8505737230110bb1099153c79efe865c59d12ba"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "android_system_properties"
|
||||
version = "0.1.6"
|
||||
@@ -82,6 +97,24 @@ version = "1.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53"
|
||||
|
||||
[[package]]
|
||||
name = "bindgen"
|
||||
version = "0.72.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895"
|
||||
dependencies = [
|
||||
"bitflags 2.13.1",
|
||||
"cexpr",
|
||||
"clang-sys",
|
||||
"itertools",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"regex",
|
||||
"rustc-hash 2.1.3",
|
||||
"shlex 1.3.0",
|
||||
"syn 2.0.119",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bit-set"
|
||||
version = "0.8.0"
|
||||
@@ -144,6 +177,12 @@ dependencies = [
|
||||
"syn 3.0.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "byteorder-lite"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495"
|
||||
|
||||
[[package]]
|
||||
name = "cc"
|
||||
version = "1.4.2"
|
||||
@@ -151,7 +190,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5d262e149917187838d5b42777c8253bcb64500067342904e7d429499a6f277e"
|
||||
dependencies = [
|
||||
"find-msvc-tools",
|
||||
"shlex",
|
||||
"shlex 2.0.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cexpr"
|
||||
version = "0.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766"
|
||||
dependencies = [
|
||||
"nom",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -166,6 +214,17 @@ version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f079e83a288787bcd14a6aea84cee5c87a67c5a3e660c30f557a3d24761b3527"
|
||||
|
||||
[[package]]
|
||||
name = "clang-sys"
|
||||
version = "1.9.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "157a8ba7b480713b56f4c09fd13fc3e0a22a5dfab8097ba61cbc5feef950788a"
|
||||
dependencies = [
|
||||
"glob",
|
||||
"libc",
|
||||
"libloading",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap"
|
||||
version = "4.6.6"
|
||||
@@ -259,6 +318,15 @@ dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crc32fast"
|
||||
version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crunchy"
|
||||
version = "0.2.4"
|
||||
@@ -274,18 +342,65 @@ dependencies = [
|
||||
"litrs",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "either"
|
||||
version = "1.17.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9e5e8f6c15a24b9a3ee5efec809ccd006d3b30e8b3bb63c39af737c7f87daa1d"
|
||||
|
||||
[[package]]
|
||||
name = "equivalent"
|
||||
version = "1.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
|
||||
|
||||
[[package]]
|
||||
name = "fax"
|
||||
version = "0.2.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "caf1079563223d5d59d83c85886a56e586cfd5c1a26292e971a0fa266531ac5a"
|
||||
|
||||
[[package]]
|
||||
name = "ffmpeg-next"
|
||||
version = "9.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6380599799e175191eb7ffe82c97f36a2a90a36cbc54c738a903e5287d7f516a"
|
||||
dependencies = [
|
||||
"bitflags 2.13.1",
|
||||
"ffmpeg-sys-next",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ffmpeg-sys-next"
|
||||
version = "9.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9b939bf79dd5949412a4b81cfe21a07f48ea21b47fcbb5f57816c8c2de5ae30b"
|
||||
dependencies = [
|
||||
"bindgen",
|
||||
"cc",
|
||||
"libc",
|
||||
"num_cpus",
|
||||
"pkg-config",
|
||||
"vcpkg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "find-msvc-tools"
|
||||
version = "0.1.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "26b73573e6edcd2af0cdf47bd6cb58f0b3839491263c314eaad1ccf24430e1de"
|
||||
|
||||
[[package]]
|
||||
name = "flate2"
|
||||
version = "1.1.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c"
|
||||
dependencies = [
|
||||
"crc32fast",
|
||||
"miniz_oxide",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "foldhash"
|
||||
version = "0.1.5"
|
||||
@@ -354,6 +469,12 @@ dependencies = [
|
||||
"xml-rs",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "glob"
|
||||
version = "0.3.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e4eba85ea1d0a966a983acd07deee566e67395d2d96b6fb39e62b5a833f1eb0b"
|
||||
|
||||
[[package]]
|
||||
name = "glow"
|
||||
version = "0.16.0"
|
||||
@@ -459,12 +580,31 @@ version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
|
||||
|
||||
[[package]]
|
||||
name = "hermit-abi"
|
||||
version = "0.5.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c"
|
||||
|
||||
[[package]]
|
||||
name = "hexf-parse"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dfa686283ad6dd069f105e5ab091b04c62850d3e4cf5d67debad1933f55023df"
|
||||
|
||||
[[package]]
|
||||
name = "image"
|
||||
version = "0.25.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "85ab80394333c02fe689eaf900ab500fbd0c2213da414687ebf995a65d5a6104"
|
||||
dependencies = [
|
||||
"bytemuck",
|
||||
"byteorder-lite",
|
||||
"moxcms",
|
||||
"num-traits",
|
||||
"tiff",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "indexmap"
|
||||
version = "2.14.0"
|
||||
@@ -481,6 +621,15 @@ version = "1.70.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695"
|
||||
|
||||
[[package]]
|
||||
name = "itertools"
|
||||
version = "0.13.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186"
|
||||
dependencies = [
|
||||
"either",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "itoa"
|
||||
version = "1.0.18"
|
||||
@@ -616,6 +765,32 @@ dependencies = [
|
||||
"paste",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "minimal-lexical"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
|
||||
|
||||
[[package]]
|
||||
name = "miniz_oxide"
|
||||
version = "0.8.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316"
|
||||
dependencies = [
|
||||
"adler2",
|
||||
"simd-adler32",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "moxcms"
|
||||
version = "0.8.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bb85c154ba489f01b25c0d36ae69a87e4a1c73a72631fc6c0eb6dde34a73e44b"
|
||||
dependencies = [
|
||||
"num-traits",
|
||||
"pxfm",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "naga"
|
||||
version = "25.0.1"
|
||||
@@ -634,7 +809,7 @@ dependencies = [
|
||||
"log",
|
||||
"num-traits",
|
||||
"once_cell",
|
||||
"rustc-hash",
|
||||
"rustc-hash 1.1.0",
|
||||
"spirv",
|
||||
"strum",
|
||||
"thiserror 2.0.20",
|
||||
@@ -650,6 +825,16 @@ dependencies = [
|
||||
"jni-sys 0.3.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nom"
|
||||
version = "7.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
"minimal-lexical",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-traits"
|
||||
version = "0.2.19"
|
||||
@@ -660,30 +845,100 @@ dependencies = [
|
||||
"libm",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num_cpus"
|
||||
version = "1.17.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "91df4bbde75afed763b708b7eee1e8e7651e02d97f6d5dd763e89367e957b23b"
|
||||
dependencies = [
|
||||
"hermit-abi",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "oak-worker"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"clap",
|
||||
"oakfacade",
|
||||
"oakengine",
|
||||
"oakrender",
|
||||
"serde",
|
||||
"serde_json",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "oakaudio"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"oakcore-rs",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "oakcodec"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"ffmpeg-next",
|
||||
"oakcore-rs",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "oakcommon"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"image",
|
||||
"log",
|
||||
"oakcore-rs",
|
||||
"ocio-rs",
|
||||
"quick-xml",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "oakcore-rs"
|
||||
version = "0.1.0"
|
||||
|
||||
[[package]]
|
||||
name = "oakfacade"
|
||||
name = "oakengine"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"oakaudio",
|
||||
"oakcodec",
|
||||
"oakcommon",
|
||||
"oakcore-rs",
|
||||
"oaknode",
|
||||
"oakplugin",
|
||||
"oakrender",
|
||||
"oaktask",
|
||||
"oaktimeline",
|
||||
"oakundo",
|
||||
"serde",
|
||||
"serde_json",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "oaknode"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"oakcore-rs",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "oakotio"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"oakcore-rs",
|
||||
"quick-xml",
|
||||
"serde",
|
||||
"serde_json",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "oakplugin"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"cc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "oakrender"
|
||||
version = "0.1.0"
|
||||
@@ -693,6 +948,28 @@ dependencies = [
|
||||
"wgpu",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "oaktask"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"oakcore-rs",
|
||||
"oakotio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "oaktimeline"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"oakcore-rs",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "oakundo"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"oakcore-rs",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "objc"
|
||||
version = "0.2.7"
|
||||
@@ -811,6 +1088,27 @@ version = "1.0.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3d595e54a326bc53c1c197b32d295e14b169e3cfeaa8dc82b529f947fba6bcf5"
|
||||
|
||||
[[package]]
|
||||
name = "pxfm"
|
||||
version = "0.1.30"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d55d956fa96f5ec02be2e13af0e20391a5aa83d6a074e3ad368959d0fab299ea"
|
||||
|
||||
[[package]]
|
||||
name = "quick-error"
|
||||
version = "2.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3"
|
||||
|
||||
[[package]]
|
||||
name = "quick-xml"
|
||||
version = "0.41.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e660451e55124f798a69a5af3f49ccfbefbd41910eefd25caf2393e1f3473ec1"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.47"
|
||||
@@ -841,6 +1139,35 @@ dependencies = [
|
||||
"bitflags 2.13.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex"
|
||||
version = "1.13.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f020237b6c8eed93db2e2cb53c00c60a8e1bc73da7d073199a1180401450218d"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"memchr",
|
||||
"regex-automata",
|
||||
"regex-syntax",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex-automata"
|
||||
version = "0.4.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ad8553b9b26413251cbf30e620595c7a41b3887f03da04579c0e6b0d6a06b4b2"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"memchr",
|
||||
"regex-syntax",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex-syntax"
|
||||
version = "0.8.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4"
|
||||
|
||||
[[package]]
|
||||
name = "renderdoc-sys"
|
||||
version = "1.1.0"
|
||||
@@ -853,6 +1180,12 @@ version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2"
|
||||
|
||||
[[package]]
|
||||
name = "rustc-hash"
|
||||
version = "2.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d"
|
||||
|
||||
[[package]]
|
||||
name = "rustversion"
|
||||
version = "1.0.23"
|
||||
@@ -901,6 +1234,7 @@ version = "1.0.151"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14"
|
||||
dependencies = [
|
||||
"indexmap",
|
||||
"itoa",
|
||||
"memchr",
|
||||
"serde",
|
||||
@@ -908,12 +1242,24 @@ dependencies = [
|
||||
"zmij",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "shlex"
|
||||
version = "1.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
|
||||
|
||||
[[package]]
|
||||
name = "shlex"
|
||||
version = "2.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba"
|
||||
|
||||
[[package]]
|
||||
name = "simd-adler32"
|
||||
version = "0.3.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3a219298ac11a56ea9a6d2120044824d6f01aeb034955e7af7bc16858527deea"
|
||||
|
||||
[[package]]
|
||||
name = "slab"
|
||||
version = "0.4.12"
|
||||
@@ -1049,6 +1395,20 @@ dependencies = [
|
||||
"syn 3.0.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tiff"
|
||||
version = "0.11.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b63feaf3343d35b6ca4d50483f94843803b0f51634937cc2ec519fc32232bc52"
|
||||
dependencies = [
|
||||
"fax",
|
||||
"flate2",
|
||||
"half",
|
||||
"quick-error",
|
||||
"weezl",
|
||||
"zune-jpeg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.24"
|
||||
@@ -1067,6 +1427,12 @@ version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
|
||||
|
||||
[[package]]
|
||||
name = "vcpkg"
|
||||
version = "0.2.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
|
||||
|
||||
[[package]]
|
||||
name = "version_check"
|
||||
version = "0.9.5"
|
||||
@@ -1138,6 +1504,12 @@ dependencies = [
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "weezl"
|
||||
version = "0.1.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a28ac98ddc8b9274cb41bb4d9d4d5c425b6020c50c46f25559911905610b4a88"
|
||||
|
||||
[[package]]
|
||||
name = "wgpu"
|
||||
version = "25.0.2"
|
||||
@@ -1187,7 +1559,7 @@ dependencies = [
|
||||
"portable-atomic",
|
||||
"profiling",
|
||||
"raw-window-handle",
|
||||
"rustc-hash",
|
||||
"rustc-hash 1.1.0",
|
||||
"smallvec",
|
||||
"thiserror 2.0.20",
|
||||
"wgpu-core-deps-apple",
|
||||
@@ -1468,3 +1840,18 @@ name = "zmij"
|
||||
version = "1.0.23"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b"
|
||||
|
||||
[[package]]
|
||||
name = "zune-core"
|
||||
version = "0.5.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d56377fd46368984a170bc5aac5567e52ca5da874caa60bea39fcbca78fb658b"
|
||||
|
||||
[[package]]
|
||||
name = "zune-jpeg"
|
||||
version = "0.5.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "27bc9d5b815bc103f142aa054f561d9187d191692ec7c2d1e2b4737f8dbd7296"
|
||||
dependencies = [
|
||||
"zune-core",
|
||||
]
|
||||
|
||||
@@ -33,11 +33,11 @@ serde_json = "1"
|
||||
# The liboakengine facade (Rust): its worker module owns the whole worker
|
||||
# runtime — render backend selection through the oakrender module C ABI,
|
||||
# the startup handshake and the NDJSON control loop
|
||||
# (`oakfacade::worker::worker_main`, the port of engine/src/capi/worker.cpp).
|
||||
# (`oakengine::worker::worker_main`, the port of engine/src/capi/worker.cpp).
|
||||
# This binary is a thin shell over it, like worker/workermain.cpp. Its ipc
|
||||
# module provides the real shared-memory frame-slot transport
|
||||
# (`oakfacade::ipc`) that src/session.rs + src/transport.rs attach through.
|
||||
oakfacade = { path = "../../src/facade/rust" }
|
||||
# (`oakengine::ipc`) that src/session.rs + src/transport.rs attach through.
|
||||
oakengine = { path = "../../src/engine/rust" }
|
||||
|
||||
# The oakrender module crate (the Rust rewrite of the oakrender module):
|
||||
# its C ABI (include/render/renderer.h) is how the facade's worker
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
// Oak Video Editor - Non-Linear Video Editor
|
||||
// Copyright (C) 2026 Oak Team
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
//! Link configuration for the `oak-worker` binary.
|
||||
//!
|
||||
//! The facade rlib (src/engine/rust) links the module C ABIs into any
|
||||
//! consumer that pulls its codec surface — oak-worker's use of
|
||||
//! `oakengine::worker` transitively pulls the facade's codec module, whose
|
||||
//! oakcodec references carry a few C++-host imports (`oakcore_audioparams_*`
|
||||
//! from liboakcore, `fb_*` from ffmpeg_bridge). Those live in the host Oak
|
||||
//! process and are only reachable on media-decode paths this worker never
|
||||
//! exercises; the CMake worker has the same property through the
|
||||
//! liboakengine dylib (whose build.rs allows runtime lookups). Mirror that
|
||||
//! here so the standalone Rust worker binary links.
|
||||
|
||||
fn main() {
|
||||
if std::env::var("CARGO_CFG_TARGET_OS").as_deref() == Ok("macos") {
|
||||
println!("cargo:rustc-link-arg=-Wl,-undefined,dynamic_lookup");
|
||||
}
|
||||
}
|
||||
@@ -19,7 +19,7 @@
|
||||
//! A thin shell over the facade, mirroring `worker/workermain.cpp`: all
|
||||
//! runtime logic — render backend selection (dynamic -> OpenGL fallback
|
||||
//! through the oakrender module C ABI), the startup handshake and the
|
||||
//! NDJSON control loop — lives in `oakfacade::worker` (the Rust port of
|
||||
//! NDJSON control loop — lives in `oakengine::worker` (the Rust port of
|
||||
//! `engine/src/capi/worker.cpp`, contract in
|
||||
//! `engine/include/oakengine/worker.h`). This crate keeps only the CLI
|
||||
//! surface (arg parsing) and the in-process session mirror
|
||||
@@ -37,7 +37,7 @@ use std::process::exit;
|
||||
use clap::Parser;
|
||||
|
||||
// Force-link the oakrender module crate: the facade's worker module
|
||||
// (oakfacade::worker) initializes the render backend through the oakrender
|
||||
// (oakengine::worker) initializes the render backend through the oakrender
|
||||
// C ABI, but its bridge imports are `extern "C"` declarations — nothing in
|
||||
// the worker source names the crate, so without this the oakrender rlib
|
||||
// would not be added to the link and those imports would stay undefined.
|
||||
@@ -46,7 +46,7 @@ use oakrender as _;
|
||||
|
||||
/// Protocol version announced in the startup handshake
|
||||
/// (`k_protocol_version` in worker.cpp). Mirrors
|
||||
/// `oakfacade::worker::PROTOCOL_VERSION`.
|
||||
/// `oakengine::worker::PROTOCOL_VERSION`.
|
||||
pub const PROTOCOL_VERSION: i32 = 1;
|
||||
|
||||
/// CLI surface (the C++ worker scans argv for `--backend`; clap formalizes
|
||||
@@ -68,7 +68,7 @@ fn main() {
|
||||
let args = Args::parse();
|
||||
// The facade's worker_main is the C++ oakengine_worker_main() — the
|
||||
// whole worker flow. Like workermain.cpp, this main only forwards.
|
||||
exit(oakfacade::worker::worker_main(&args.backend.to_ascii_lowercase()));
|
||||
exit(oakengine::worker::worker_main(&args.backend.to_ascii_lowercase()));
|
||||
}
|
||||
|
||||
/// Log a worker-side message to stderr, mirroring worker.cpp `log_error()`
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
|
||||
//! The worker-side session state machine — the in-process mirror of
|
||||
//! `OakWorkerSession` in `engine/src/capi/worker.cpp` (whose production
|
||||
//! Rust port lives in `oakfacade::worker`).
|
||||
//! Rust port lives in `oakengine::worker`).
|
||||
//!
|
||||
//! The session holds the attached shared-memory frame-slot pools
|
||||
//! ([`crate::transport::AttachedPools`]) and the shutdown flag, and
|
||||
@@ -61,7 +61,7 @@ impl WorkerSession {
|
||||
}
|
||||
|
||||
/// The attached output pool (the worker->main frame-slot pool).
|
||||
pub fn output_pool(&self) -> Option<&oakfacade::ipc::FrameSlotPool> {
|
||||
pub fn output_pool(&self) -> Option<&oakengine::ipc::FrameSlotPool> {
|
||||
self.pools.as_ref().map(|p| &p.output_pool)
|
||||
}
|
||||
|
||||
@@ -196,7 +196,7 @@ impl Default for WorkerSession {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use oakfacade::ipc::{FrameSlotPool, SharedMemoryRegion, ShmMode};
|
||||
use oakengine::ipc::{FrameSlotPool, SharedMemoryRegion, ShmMode};
|
||||
use serde_json::json;
|
||||
|
||||
/// A unique, temporary POSIX segment key for a test.
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
//! shared-memory segments holding an `olive::ipc::FrameSlotPool` — a fixed
|
||||
//! pool of frame slots whose free/ready queues are synchronized by the
|
||||
//! lock-free single-producer/single-consumer `SpscRingBuffer`. That
|
||||
//! machinery is implemented in the facade crate (`oakfacade::ipc`, the
|
||||
//! machinery is implemented in the facade crate (`oakengine::ipc`, the
|
||||
//! Rust port of `engine/render/ipc/` behind
|
||||
//! `engine/include/oakengine/ipc.h`) — this module is the worker-side
|
||||
//! transport over it.
|
||||
@@ -36,7 +36,7 @@
|
||||
//! as before: `oaknode` is a `todo!()` skeleton and the oakrender crate
|
||||
//! does not yet evaluate an arbitrary loaded graph to a frame.
|
||||
|
||||
use oakfacade::ipc::{FrameSlotPool, SharedMemoryRegion, ShmMode};
|
||||
use oakengine::ipc::{FrameSlotPool, SharedMemoryRegion, ShmMode};
|
||||
|
||||
use crate::ipc::HandshakeMsg;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user