diff --git a/crates/oak-app/src/oakui/mock.rs b/crates/oak-app/src/oakui/mock.rs index 8c08c4093..6192511b4 100644 --- a/crates/oak-app/src/oakui/mock.rs +++ b/crates/oak-app/src/oakui/mock.rs @@ -493,6 +493,17 @@ pub struct MockEngine { effects: Vec, /// Id allocator for effects added at runtime. next_effect_id: u64, + /// Parameter-write attempts from the params view (`input_id` + value + /// debug), recorded even when the call fails, so the routing tests are + /// falsifiable (review §10.2: previously the mock swallowed every write + /// and the tests could not observe it). + effect_param_attempts: Mutex>, + /// Push-button attempts from the params view. + effect_push_attempts: Mutex>, + /// Result returned by `set_effect_param`/`effect_push_button` (default + /// `Err`: the demo models an unsupported node; tests can flip it to cover + /// the success path). + effect_param_ok: bool, /// Whether the program monitor is playing (mirrors the clock; kept here /// because the audio-meter data source has no `App` to read the clock). program_playing: bool, @@ -588,6 +599,29 @@ pub struct MockEngine { impl MockEngine { /// Builds the demo project: 第一稿.ove, one HD sequence, four tracks. + /// The parameter-write attempts the params view routed to this engine + /// (`(effect, input_id, value debug)`), in call order. + pub fn effect_param_attempts(&self) -> Vec<(EffectId, String, String)> { + self.effect_param_attempts + .lock() + .unwrap_or_else(|e| e.into_inner()) + .clone() + } + + /// The push-button attempts the params view routed to this engine. + pub fn effect_push_attempts(&self) -> Vec<(EffectId, String)> { + self.effect_push_attempts + .lock() + .unwrap_or_else(|e| e.into_inner()) + .clone() + } + + /// Flip the result of `set_effect_param`/`effect_push_button` (default + /// `Err`); tests use `true` to cover the accepted path. + pub fn set_effect_param_ok(&mut self, ok: bool) { + self.effect_param_ok = ok; + } + pub fn demo(cx: &mut Context) -> Self { let rate = VideoFormat::hd_1080p25().rate; let clip = @@ -862,6 +896,9 @@ impl MockEngine { }, ], next_effect_id: 4, + effect_param_attempts: Mutex::new(Vec::new()), + effect_push_attempts: Mutex::new(Vec::new()), + effect_param_ok: false, program_playing: false, selected_item: None, meter_phase: 0, @@ -1681,6 +1718,43 @@ impl AppEngine for MockEngine { }) } + fn set_effect_param( + &mut self, + effect: EffectId, + input_id: &str, + value: oak_node::value::NodeValue, + cx: &mut Context, + ) -> Result<(), String> { + self.effect_param_attempts + .lock() + .unwrap_or_else(|e| e.into_inner()) + .push((effect, input_id.to_string(), format!("{value:?}"))); + if self.effect_param_ok { + cx.notify(); + Ok(()) + } else { + Err("the demo engine does not apply effect parameters".into()) + } + } + + fn effect_push_button( + &mut self, + effect: EffectId, + input_id: &str, + cx: &mut Context, + ) -> Result<(), String> { + self.effect_push_attempts + .lock() + .unwrap_or_else(|e| e.into_inner()) + .push((effect, input_id.to_string())); + if self.effect_param_ok { + cx.notify(); + Ok(()) + } else { + Err("the demo engine does not trigger effect push buttons".into()) + } + } + fn add_node_at( &mut self, type_id: &str, diff --git a/crates/oak-app/src/panels/ofx_params.rs b/crates/oak-app/src/panels/ofx_params.rs index ffc32f004..2bc43dce0 100644 --- a/crates/oak-app/src/panels/ofx_params.rs +++ b/crates/oak-app/src/panels/ofx_params.rs @@ -873,7 +873,9 @@ fn wire_controls(view: &OfxParamsView, cx: &mut Context NodeValue::Float(value.to_f64()), }; engine.update(cx, |engine, cx| { - let _ = engine.set_effect_param(effect, &input_id, nv, cx); + if let Err(err) = engine.set_effect_param(effect, &input_id, nv, cx) { + eprintln!("[ofx params] set {input_id:?} failed: {err}"); + } }); } }, @@ -886,7 +888,9 @@ fn wire_controls(view: &OfxParamsView, cx: &mut Context(view: &OfxParamsView, cx: &mut Context NodeValue::Combo(*value as i64), }; engine.update(cx, |engine, cx| { - let _ = engine.set_effect_param(effect, &input_id, nv, cx); + if let Err(err) = engine.set_effect_param(effect, &input_id, nv, cx) { + eprintln!("[ofx params] set {input_id:?} failed: {err}"); + } }); }) .detach(); @@ -1023,12 +1029,14 @@ fn wire_controls(view: &OfxParamsView, cx: &mut Context Render for OfxParamsView { .child(button_label) .on_click(move |_event: &ClickEvent, _window, cx| { engine.update(cx, |engine, cx| { - let _ = engine.effect_push_button(effect, &input_id, cx); + if let Err(err) = engine.effect_push_button(effect, &input_id, cx) { + eprintln!("[ofx params] push {input_id:?} failed: {err}"); + } }); }) .into_any_element() @@ -1204,12 +1214,14 @@ impl Render for OfxParamsView { .on_click(move |_event: &ClickEvent, _window, cx| { let text = editor_commit.read(cx).as_str().to_string(); engine.update(cx, |engine, cx| { - let _ = engine.set_effect_param( + if let Err(err) = engine.set_effect_param( effect, &input_id, NodeValue::Text(text), cx, - ); + ) { + eprintln!("[ofx params] set {input_id:?} failed: {err}"); + } }); }), ) diff --git a/crates/oak-plugin/src/clip.rs b/crates/oak-plugin/src/clip.rs index b1bebf922..9ca94eec5 100644 --- a/crates/oak-plugin/src/clip.rs +++ b/crates/oak-plugin/src/clip.rs @@ -346,7 +346,7 @@ impl ClipInstance { use crate::error::Error; use crate::render::{texture_get_frame, PIXEL_FORMAT_F32}; - let texture = self + let mut texture = self .output_texture .lock() .unwrap_or_else(|e| e.into_inner()) @@ -384,11 +384,22 @@ impl ClipInstance { let s = y * tight; dst_bytes[d..d + tight].copy_from_slice(&pixels[s..s + tight]); } - // GPU 目标纹理:拷贝只落在下载帧上,经后端 upload 回写 - // (CPU 纹理无需上传)。 - if let crate::render::Texture::Gpu { token, ctx, .. } = &texture { - ctx.upload(*token, &frame) - .map_err(|e| Error::Failed(format!("输出纹理上传失败:{e}")))?; + // 写回目标纹理:CPU 纹理的 `to_frame` 是深拷贝,必须把改写 + // 后的帧放回本体(同 render_driver::write_output_frame 的值 + // 模型修复);GPU 目标纹理:拷贝只落在下载帧上,经后端 + // upload 回写。 + match &mut texture { + crate::render::Texture::Cpu(f) => { + f.data = frame.data; + } + crate::render::Texture::Gpu { token, ctx, .. } => { + ctx.upload(*token, &frame) + .map_err(|e| Error::Failed(format!("输出纹理上传失败:{e}")))?; + } + // 未解析的平面纹理不会成为插件输出目标(解码路径会先解析)。 + crate::render::Texture::Planar(_) => { + return Err(Error::Failed("平面纹理不能作为插件输出目标".into())); + } } Ok(texture) } diff --git a/crates/oak-task/src/manager.rs b/crates/oak-task/src/manager.rs index c50ebc454..49caf5be7 100644 --- a/crates/oak-task/src/manager.rs +++ b/crates/oak-task/src/manager.rs @@ -16,9 +16,11 @@ //! The `TaskManager` singleton, mirroring `src/task/src/taskmanager.h`. //! -//! Holds the set of live [`crate::task::Task`] instances; starting a task -//! hands its ownership to the manager, which also exposes the codec task -//! submitter registration (see [`crate::codecbridge`]). +//! Holds the set of live [`crate::task::Task`] instances and starts them +//! on worker threads. Codec task-submitter registration is a **separate** +//! concern owned by [`crate::codecbridge`]: the embedding application +//! calls `register_codec_task_submitter()` explicitly (the manager never +//! installs the callback itself — see [`TaskManager::init`]). //! //! CPP-PARITY: src/task/src/taskmanager.h //! @@ -43,7 +45,10 @@ pub struct TaskManager { tasks: Vec>, /// Worker threads, parallel to `tasks` (moved out before joining). threads: Vec>>, - /// Whether the codec task submitter is currently registered. + /// Whether the embedding application registered the codec task + /// submitter (`crate::codecbridge::register_codec_task_submitter`). + /// Pure bookkeeping on the manager: [`TaskManager::init`] never + /// registers anything itself, so a fresh singleton reads `false`. codec_submitter_registered: bool, } @@ -81,9 +86,23 @@ impl TaskManager { guard.as_ref().map(|p| unsafe { &*p.0 }) } - /// Create the singleton and register the codec task submitter. Returns - /// `Err(Error::State)` if already initialized (mirrors - /// `oaktask_manager_init`). + /// Create the singleton. Returns `Err(Error::State)` if already + /// initialized (mirrors `oaktask_manager_init`). + /// + /// This **does not** register the codec task submitter: registration is + /// `crate::codecbridge::register_codec_task_submitter`, an explicit + /// call by the embedding application. Two reasons: + /// + /// - the C++ parity (`TaskManager::create_instance`) creates the + /// manager only; codecbridge wires the callback separately, and + /// - the codec bridge's interim contract is *synchronous* submission + /// (`submit_codec_task` runs the task inline), so registration needs + /// no manager state. + /// + /// Consequently a fresh manager reports + /// [`TaskManager::codec_submitter_registered`] as `false`; the + /// application flips it via [`TaskManager::set_codec_submitter_registered`] + /// when it performs the registration itself. pub fn init() -> Result<()> { let mut guard = INSTANCE.lock().unwrap(); if guard.is_some() { @@ -98,7 +117,9 @@ impl TaskManager { Ok(()) } - /// Destroy the singleton. Idempotent. + /// Destroy the singleton. Idempotent. Does not unregister the codec + /// task submitter (the registration is owned by the embedding + /// application, not by the manager — see [`TaskManager::init`]). pub fn shutdown() { let ptr = { let mut guard = INSTANCE.lock().unwrap(); @@ -231,12 +252,19 @@ impl TaskManager { out } - /// Whether this manager registered the codec submitter itself. + /// Whether this manager's bookkeeping flag says the embedding + /// application registered the codec submitter. Never set by + /// [`TaskManager::init`] (which registers nothing); see + /// [`TaskManager::set_codec_submitter_registered`]. pub fn codec_submitter_registered(&self) -> bool { self.codec_submitter_registered } - /// Mark whether this manager registered the codec submitter. + /// Record whether the embedding application registered the codec + /// submitter. This is pure bookkeeping: it neither installs nor + /// removes the oakcodec callback (that is + /// `crate::codecbridge::register_codec_task_submitter` / + /// `unregister_codec_task_submitter`). pub fn set_codec_submitter_registered(&mut self, registered: bool) { self.codec_submitter_registered = registered; } diff --git a/crates/oak-task/src/render.rs b/crates/oak-task/src/render.rs index fd508775f..cf8f9e3dd 100644 --- a/crates/oak-task/src/render.rs +++ b/crates/oak-task/src/render.rs @@ -83,7 +83,7 @@ const TICKET_AUDIO: i32 = ticket_kind::AUDIO; /// dropped with the C ABI: the direct ticket arena carries a forced size /// and pixel format only (the eval producer performs no color /// management). The matrix fields are retained for API parity but unused. -#[derive(Clone, Debug, Default)] +#[derive(Clone, Debug)] pub struct ForceParams { /// Forced output width (0 = off). pub force_width: i32, @@ -100,6 +100,22 @@ pub struct ForceParams { pub force_channel_count: i32, } +impl Default for ForceParams { + /// All overrides off: in particular `force_format` is `-1` ("off"), not + /// `0` (which is a real `PixelFormat` code — U8, and the F32 render + /// pipeline rejects it). + fn default() -> Self { + ForceParams { + force_width: 0, + force_height: 0, + force_matrix: [0.0; 16], + has_force_matrix: false, + force_format: -1, + force_channel_count: 0, + } + } +} + /// Subclass hooks, standing in for the C++ protected virtuals /// `download_frame`/`frame_downloaded`/`audio_downloaded`/`encode_subtitle`. /// Frames and audio are the direct `oakrender` value types (the deleted @@ -1225,3 +1241,4 @@ fn shm_frame_to_texture(frame: &ShmFrameRef) -> oak_core::texture::Texture { .collect(); oak_core::texture::Texture::wrap_frame(f) } + .collect(); diff --git a/crates/oak-timeline/src/undoripple.rs b/crates/oak-timeline/src/undoripple.rs index 23a1a1c65..5d05516af 100644 --- a/crates/oak-timeline/src/undoripple.rs +++ b/crates/oak-timeline/src/undoripple.rs @@ -580,6 +580,30 @@ pub struct RippleInfo { append_gap: bool, } +impl RippleInfo { + /// Creates a per-block ripple record: `block` is the block whose length + /// the ripple changes, and `append_gap` requests inserting a gap of the + /// movement span ahead of it instead (C++ + /// `TrackListRippleToolCommand::RippleInfo{block, append_gap}`). + /// + /// Added for the M6 test-coverage pass: without a constructor the + /// integration tests could only build an empty info list, leaving the + /// whole `ripple()` body unreachable (review §M5). + pub fn new(block: NodeRef, append_gap: bool) -> Self { + Self { block, append_gap } + } + + /// The block this record targets. + pub fn block(&self) -> &NodeRef { + &self.block + } + + /// Whether the ripple inserts a gap instead of resizing the block. + pub fn append_gap(&self) -> bool { + self.append_gap + } +} + /// `TrackListRippleToolCommand` — ripple-tool edit: shift blocks on the listed /// tracks by `ripple_movement` (timelineundoripple.h). #[allow(dead_code)] // `track_list` mirrors the C++ member; the per-track info drives the work.