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:
2026-08-10 16:55:35 +08:00
parent e563b340ac
commit 0ca9cad448
66 changed files with 3398 additions and 204 deletions
+1
View File
@@ -3636,6 +3636,7 @@ dependencies = [
"gpui_platform",
"gpui_widgets",
"image",
"smallvec",
]
[[package]]
+4
View File
@@ -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

+9 -5
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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");
});
}
}
+1 -1
View File
@@ -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);
+198 -41
View File
@@ -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);
}
}
+19
View File
@@ -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
+25 -2
View File
@@ -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
+1666 -3
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -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"
+6 -6
View File
@@ -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
View File
@@ -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
+1 -1
View File
@@ -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.
+3 -1
View File
@@ -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
View File
@@ -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",
+70
View File
@@ -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
+33
View File
@@ -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;
+66
View File
@@ -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(),
)
-55
View File
@@ -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" }
+392 -5
View File
@@ -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",
]
+3 -3
View File
@@ -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
+33
View File
@@ -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");
}
}
+4 -4
View File
@@ -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()`
+3 -3
View File
@@ -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.
+2 -2
View File
@@ -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;