From ee7ea18d94263798e288e2c3f79862d1c645a0fe Mon Sep 17 00:00:00 2001 From: Mike Solar Date: Tue, 15 Sep 2026 19:29:32 +0800 Subject: [PATCH] clippy: clear the workspace errors and apply the machine fixes - Mark the raw-pointer interop entry points unsafe with # Safety docs (oak-core upload/download/frame-from-pixels, oak-audio convert) and satisfy the existing callers (tests). - mut_from_ref: allow with the ABI contract documented (the handle get_mut helpers in oak-timeline/oak-render/oak-task take the shared reference the C ABI passes; exclusivity is the caller's unsafe contract). - Fix the eq_op in the white-balance normalization (green / green). - Apply cargo clippy --fix across the workspace (redundant closures and field names, field reassignment, items after test modules, ...). - Revert the replace_box fix in image_effect's clip_define: a redefinition must allocate a new box, otherwise the old clip handle stays valid and the HS-map replace contract (clip != clip2) breaks. - 283 warnings remain; they are all non-machine-applicable (chunks_exact -> as_chunks needs a manual iter_mut, too_many_arguments, complex types, missing Safety docs, ...) and are tracked as the follow-up. --- crates/oak-app/src/app.rs | 2 +- crates/oak-app/src/dialogs.rs | 346 ++- crates/oak-app/src/i18n.rs | 34 +- crates/oak-app/src/manager.rs | 154 +- crates/oak-app/src/oakui/component/menu.rs | 84 +- crates/oak-app/src/oakui/displaycolor.rs | 13 +- crates/oak-app/src/oakui/gpu.rs | 8 +- crates/oak-app/src/oakui/graphops.rs | 91 +- crates/oak-app/src/oakui/mock.rs | 63 +- crates/oak-app/src/oakui/ofx.rs | 137 +- crates/oak-app/src/oakui/real.rs | 670 +++--- crates/oak-app/src/oakui/renderops.rs | 537 +++-- crates/oak-app/src/oakui/scopes.rs | 2 +- crates/oak-app/src/oakui/waveform.rs | 15 +- crates/oak-app/src/oakui/waveformsync.rs | 2 +- crates/oak-app/src/panels/effect_library.rs | 38 +- crates/oak-app/src/panels/history.rs | 2 +- crates/oak-app/src/panels/inspector.rs | 23 +- crates/oak-app/src/panels/multicam.rs | 2 +- crates/oak-app/src/panels/node_editor.rs | 64 +- crates/oak-app/src/panels/ofx_params.rs | 815 ++++--- crates/oak-app/src/panels/program_viewer.rs | 133 +- crates/oak-app/src/panels/timeline.rs | 2 +- crates/oak-audio/src/manager.rs | 71 +- crates/oak-audio/src/outputdevice.rs | 21 +- crates/oak-audio/src/previewdevice.rs | 10 +- crates/oak-audio/src/processor.rs | 26 +- crates/oak-audio/tests/manager_test.rs | 35 +- crates/oak-audio/tests/processor_test.rs | 61 +- crates/oak-codec/src/decoder.rs | 2 +- crates/oak-codec/src/encodingparams.rs | 2 +- crates/oak-codec/src/ffmpeg.rs | 10 +- crates/oak-codec/src/planarfiledevice.rs | 2 +- crates/oak-codec/src/proxymanager.rs | 5 +- crates/oak-codec/src/task.rs | 4 +- crates/oak-codec/src/timecodemetadata.rs | 2 +- crates/oak-core/src/backend.rs | 257 ++- crates/oak-core/src/color.rs | 65 +- crates/oak-core/src/colormath.rs | 281 ++- crates/oak-core/src/configstore.rs | 57 +- crates/oak-core/src/displayicc.rs | 28 +- crates/oak-core/src/filefunctions.rs | 56 +- crates/oak-core/src/miscutils.rs | 2 +- crates/oak-core/src/subtitleparams.rs | 4 +- crates/oak-core/src/texture.rs | 4 +- crates/oak-core/src/videoparams.rs | 8 +- crates/oak-node/src/footage.rs | 4 +- crates/oak-node/src/keyframe.rs | 7 +- crates/oak-node/src/nodes/blur.rs | 32 +- crates/oak-node/src/nodes/checkerboard.rs | 20 +- crates/oak-node/src/nodes/colorbars.rs | 20 +- crates/oak-node/src/nodes/colormatrix.rs | 20 +- .../src/nodes/cornerpindistortnode.rs | 22 +- crates/oak-node/src/nodes/cropdistortnode.rs | 22 +- crates/oak-node/src/nodes/dilate.rs | 20 +- crates/oak-node/src/nodes/dirblur.rs | 20 +- crates/oak-node/src/nodes/dissolve.rs | 20 +- crates/oak-node/src/nodes/dropshadowfilter.rs | 22 +- crates/oak-node/src/nodes/edgedetect.rs | 20 +- crates/oak-node/src/nodes/erode.rs | 20 +- crates/oak-node/src/nodes/flipdistortnode.rs | 22 +- crates/oak-node/src/nodes/keymix.rs | 20 +- crates/oak-node/src/nodes/matrix.rs | 22 +- crates/oak-node/src/nodes/merge.rs | 22 +- crates/oak-node/src/nodes/mirror.rs | 20 +- crates/oak-node/src/nodes/mosaicfilternode.rs | 22 +- crates/oak-node/src/nodes/noise.rs | 22 +- crates/oak-node/src/nodes/opacity.rs | 22 +- crates/oak-node/src/nodes/pan.rs | 22 +- crates/oak-node/src/nodes/plugin.rs | 4 +- crates/oak-node/src/nodes/polygon.rs | 24 +- crates/oak-node/src/nodes/position.rs | 20 +- crates/oak-node/src/nodes/premult.rs | 20 +- crates/oak-node/src/nodes/ramp.rs | 20 +- .../oak-node/src/nodes/rippledistortnode.rs | 22 +- crates/oak-node/src/nodes/shapenode.rs | 22 +- crates/oak-node/src/nodes/sharpen.rs | 20 +- crates/oak-node/src/nodes/solid.rs | 22 +- crates/oak-node/src/nodes/stroke.rs | 22 +- crates/oak-node/src/nodes/swirldistortnode.rs | 22 +- crates/oak-node/src/nodes/textv1.rs | 4 +- crates/oak-node/src/nodes/textv2.rs | 4 +- crates/oak-node/src/nodes/textv3.rs | 8 +- crates/oak-node/src/nodes/tiledistortnode.rs | 22 +- .../src/nodes/transformdistortnode.rs | 32 +- crates/oak-node/src/nodes/transitionfx.rs | 20 +- crates/oak-node/src/nodes/transitions.rs | 20 +- crates/oak-node/src/nodes/trigonometry.rs | 22 +- crates/oak-node/src/nodes/unpremult.rs | 20 +- crates/oak-node/src/nodes/valuenode.rs | 22 +- crates/oak-node/src/nodes/volume.rs | 22 +- crates/oak-node/src/nodes/wavedistortnode.rs | 22 +- crates/oak-node/src/nodes/whitebalance.rs | 35 +- crates/oak-node/src/ops.rs | 4 +- crates/oak-node/src/project.rs | 5 +- crates/oak-node/src/value.rs | 2 +- crates/oak-node/tests/serializer_test.rs | 109 +- crates/oak-node/tests/value_keyframe_test.rs | 4 +- crates/oak-otio/src/fcpxml.rs | 4 +- crates/oak-otio/tests/fcpxml.rs | 6 +- crates/oak-plugin/src/gl_bridge.rs | 2 +- crates/oak-plugin/src/instance.rs | 6 +- crates/oak-plugin/src/node_factory.rs | 4 +- crates/oak-plugin/src/param.rs | 6 +- crates/oak-plugin/src/render.rs | 2 +- crates/oak-plugin/src/render_driver.rs | 2 +- crates/oak-plugin/src/suites/draw.rs | 6 +- crates/oak-plugin/src/suites/gl_render.rs | 106 +- crates/oak-plugin/src/suites/image_effect.rs | 31 +- crates/oak-plugin/src/suites/multithread.rs | 14 +- crates/oak-plugin/src/suites/param.rs | 29 +- crates/oak-plugin/src/suites/parametric.rs | 14 +- crates/oak-plugin/src/suites/property.rs | 16 +- crates/oak-plugin/tests/node_e2e_test.rs | 37 +- .../oak-render/examples/render_audio_wav.rs | 12 +- crates/oak-render/src/cache.rs | 12 +- crates/oak-render/src/eval.rs | 8 +- crates/oak-render/src/handle.rs | 5 + crates/oak-render/src/ipc.rs | 15 +- crates/oak-render/src/procpool.rs | 7 +- .../oak-render/tests/footage_decode_test.rs | 20 +- crates/oak-render/tests/graph_render.rs | 1178 ++++++----- crates/oak-render/tests/ofxmisc_blur.rs | 2 +- crates/oak-render/tests/ofxmisc_color.rs | 2 +- crates/oak-render/tests/ofxmisc_gen.rs | 4 +- crates/oak-render/tests/ofxmisc_matrix.rs | 2 +- crates/oak-render/tests/ofxmisc_merge.rs | 2 +- crates/oak-render/tests/text_outline_glow.rs | 2 +- crates/oak-render/tests/transitionfx.rs | 2 +- .../oak-storage/src/backends/database/mod.rs | 62 +- crates/oak-task/src/export.rs | 61 +- crates/oak-task/src/handle.rs | 5 + crates/oak-task/src/manager.rs | 4 +- crates/oak-task/src/proxy.rs | 55 +- crates/oak-task/src/render.rs | 108 +- crates/oak-task/tests/export_test.rs | 9 +- crates/oak-timeline/src/handle.rs | 5 + crates/oak-timeline/src/multicam.rs | 1875 +++++++++-------- crates/oak-timeline/src/workarea.rs | 8 +- crates/oak-timeline/tests/handle_test.rs | 2 +- crates/oak-timeline/tests/marker_test.rs | 33 +- crates/oak-timeline/tests/workarea_test.rs | 12 +- crates/oak-undo/src/undostack.rs | 2 +- crates/oak-undo/tests/edge_coverage_test.rs | 12 +- crates/oak-undo/tests/undo_safe_test.rs | 26 +- crates/oak-worker/src/worker.rs | 136 +- 146 files changed, 5235 insertions(+), 4046 deletions(-) diff --git a/crates/oak-app/src/app.rs b/crates/oak-app/src/app.rs index 7e93c7773..b69c78109 100644 --- a/crates/oak-app/src/app.rs +++ b/crates/oak-app/src/app.rs @@ -5653,7 +5653,7 @@ mod tests { /// the mock engine's playback ticks with executor time, so a clock /// advance would move the playhead out from under the assertions. fn settle_key(cx: &mut TestAppContext, window: AnyWindowHandle, key: &str) { - cx.dispatch_keystroke(window.into(), gpui::Keystroke::parse(key).unwrap()); + cx.dispatch_keystroke(window, gpui::Keystroke::parse(key).unwrap()); cx.run_until_parked(); cx.run_until_parked(); } diff --git a/crates/oak-app/src/dialogs.rs b/crates/oak-app/src/dialogs.rs index 3b3ce66d1..5b7fc6703 100644 --- a/crates/oak-app/src/dialogs.rs +++ b/crates/oak-app/src/dialogs.rs @@ -36,23 +36,23 @@ use gpui::colors::DefaultColors; use gpui::prelude::*; use gpui::timeline::FrameRate; use gpui::{ - div, px, App, ClickEvent, Context, ElementId, Entity, EventEmitter, FocusHandle, Focusable, - InteractiveElement, Keystroke, PathPromptOptions, Render, SharedString, Window, + div, px, App, ClickEvent, Context, ElementId, Entity, EventEmitter, FocusHandle, Focusable, + InteractiveElement, Keystroke, PathPromptOptions, Render, SharedString, Window, }; use gpui_elements::editable_text::{EditableTextState, StringStorage, TextChanged}; use crate::actions::ActionId; use crate::i18n; use crate::oakui::real::{ - audio_input_device, audio_input_devices, audio_output_device, audio_output_devices, - config_get_bool, config_get_int, config_get_string, config_set_bool, config_set_int, - config_set_string, encoding_formats, proxy_dividers, renderer_backends, set_audio_input_device, - set_audio_output_device, set_theme_dark, theme_is_dark, CONFIG_KEY_DEFAULT_TRANSITION_SEC, - CONFIG_KEY_DISK_CACHE_PATH, CONFIG_KEY_FFMPEG_PATH, CONFIG_KEY_PG_URL, - CONFIG_KEY_PREVIEW_WINDOW, CONFIG_KEY_PROXY_DIVIDER, CONFIG_KEY_RENDERER_BACKEND, - CONFIG_KEY_SNAPSHOT_INTERVAL_SEC, CONFIG_KEY_STORAGE_BACKEND, CONFIG_KEY_USE_PROXY, - DEFAULT_PREVIEW_WINDOW_FORWARD, DEFAULT_SNAPSHOT_INTERVAL_SEC, DEFAULT_TRANSITION_SEC, - EXPORT_FORMAT_MP4, + audio_input_device, audio_input_devices, audio_output_device, audio_output_devices, + config_get_bool, config_get_int, config_get_string, config_set_bool, config_set_int, + config_set_string, encoding_formats, proxy_dividers, renderer_backends, set_audio_input_device, + set_audio_output_device, set_theme_dark, theme_is_dark, CONFIG_KEY_DEFAULT_TRANSITION_SEC, + CONFIG_KEY_DISK_CACHE_PATH, CONFIG_KEY_FFMPEG_PATH, CONFIG_KEY_PG_URL, + CONFIG_KEY_PREVIEW_WINDOW, CONFIG_KEY_PROXY_DIVIDER, CONFIG_KEY_RENDERER_BACKEND, + CONFIG_KEY_SNAPSHOT_INTERVAL_SEC, CONFIG_KEY_STORAGE_BACKEND, CONFIG_KEY_USE_PROXY, + DEFAULT_PREVIEW_WINDOW_FORWARD, DEFAULT_SNAPSHOT_INTERVAL_SEC, DEFAULT_TRANSITION_SEC, + EXPORT_FORMAT_MP4, }; // The `DisplayBitDepth` config key lives with the format mapping it // drives (oak-render's backend); the preferences dropdown and the @@ -184,20 +184,22 @@ impl PreferencesContent { ]; ComboBox::new(12, options, window, cx) }); - cx.subscribe(&display_bit_depth, |_this, _combo, event: &ComboBoxEvent, cx| { - let ComboBoxEvent::Selected { value, .. } = event; - let depth = if *value == 1 { "8" } else { "10" }; - config_set_string(CONFIG_KEY_DISPLAY_BIT_DEPTH, depth); - println!("[preferences] display bit depth → {depth}"); - let _ = cx; - }) + cx.subscribe( + &display_bit_depth, + |_this, _combo, event: &ComboBoxEvent, cx| { + let ComboBoxEvent::Selected { value, .. } = event; + let depth = if *value == 1 { "8" } else { "10" }; + config_set_string(CONFIG_KEY_DISPLAY_BIT_DEPTH, depth); + println!("[preferences] display bit depth → {depth}"); + let _ = cx; + }, + ) .detach(); - let bit_depth_selected = - if config_get_string(CONFIG_KEY_DISPLAY_BIT_DEPTH) == "8" { - 1 - } else { - 0 - }; + let bit_depth_selected = if config_get_string(CONFIG_KEY_DISPLAY_BIT_DEPTH) == "8" { + 1 + } else { + 0 + }; display_bit_depth.update(cx, |combo, cx| { combo.set_selected(Some(bit_depth_selected), cx) }); @@ -269,11 +271,8 @@ impl PreferencesContent { // playhead during playback (`PlaybackPreRenderFrames`, consumed by // `update_preview_window` every playback tick — no restart needed). let cache_ahead = cx.new(|cx| { - let current = config_get_int( - CONFIG_KEY_PREVIEW_WINDOW, - DEFAULT_PREVIEW_WINDOW_FORWARD, - ) - .clamp(8, 1200); + let current = config_get_int(CONFIG_KEY_PREVIEW_WINDOW, DEFAULT_PREVIEW_WINDOW_FORWARD) + .clamp(8, 1200); SpinBox::new( 10, SliderModel::new(ValueKind::Integer, 8.0, 1200.0, 8.0, current as f64), @@ -366,13 +365,13 @@ impl PreferencesContent { }) .detach(); - // --- 色彩 Color: display ICC color management ----------------------- - // On by default: the viewer frames are transformed through the - // display's ICC profile (system profile, or a custom file below). - // A mode change re-evaluates the platform display policy and - // retags the windows immediately (no restart). - use crate::oakui::displaycolor::{CONFIG_KEY_COLOR_MODE, CONFIG_KEY_CUSTOM_ICC}; - let display_icc = cx.new(|cx| { + // --- 色彩 Color: display ICC color management ----------------------- + // On by default: the viewer frames are transformed through the + // display's ICC profile (system profile, or a custom file below). + // A mode change re-evaluates the platform display policy and + // retags the windows immediately (no restart). + use crate::oakui::displaycolor::{CONFIG_KEY_COLOR_MODE, CONFIG_KEY_CUSTOM_ICC}; + let display_icc = cx.new(|cx| { let mode = config_get_string(CONFIG_KEY_COLOR_MODE); CheckBox::new( 13, @@ -500,18 +499,21 @@ impl PreferencesContent { ]; ComboBox::new(11, options, window, cx) }); - cx.subscribe(&storage_backend, |this, _combo, event: &ComboBoxEvent, cx| { - match *event { - ComboBoxEvent::Selected { value } => { - let backend = if value == 1 { "pg" } else { "sqlite" }; - this.storage_is_pg = backend == "pg"; - config_set_string(CONFIG_KEY_STORAGE_BACKEND, backend); - // Re-render so the connection-string row shows/hides with - // the selection. - cx.notify(); + cx.subscribe( + &storage_backend, + |this, _combo, event: &ComboBoxEvent, cx| { + match *event { + ComboBoxEvent::Selected { value } => { + let backend = if value == 1 { "pg" } else { "sqlite" }; + this.storage_is_pg = backend == "pg"; + config_set_string(CONFIG_KEY_STORAGE_BACKEND, backend); + // Re-render so the connection-string row shows/hides with + // the selection. + cx.notify(); + } } - } - }) + }, + ) .detach(); storage_backend.update(cx, |combo, cx| { combo.set_selected(Some(if storage_is_pg { 1 } else { 0 }), cx) @@ -1085,8 +1087,7 @@ impl ExportDialogContent { ComboBoxOption::new(0, i18n::tr("export.color.sdr")), ComboBoxOption::new(1, i18n::tr("export.color.hdr")), ]; - ComboBox::new(7, options, window, cx) - .with_placeholder(i18n::tr("export.color")) + ComboBox::new(7, options, window, cx).with_placeholder(i18n::tr("export.color")) }); color.update(cx, |combo, cx| combo.set_selected(Some(0), cx)); @@ -1095,8 +1096,7 @@ impl ExportDialogContent { ComboBoxOption::new(0, i18n::tr("export.range.all")), ComboBoxOption::new(1, i18n::tr("export.range.inout")), ]; - ComboBox::new(8, options, window, cx) - .with_placeholder(i18n::tr("export.range")) + ComboBox::new(8, options, window, cx).with_placeholder(i18n::tr("export.range")) }); range.update(cx, |combo, cx| combo.set_selected(Some(0), cx)); @@ -1149,8 +1149,7 @@ impl ExportDialogContent { // an empty list disables the dialog's OK via the picker staying // unselected. let sequence = cx.new(|cx| { - ComboBox::new(13, Vec::new(), window, cx) - .with_placeholder(i18n::tr("export.sequence")) + ComboBox::new(13, Vec::new(), window, cx).with_placeholder(i18n::tr("export.sequence")) }); Self { @@ -1188,7 +1187,8 @@ impl ExportDialogContent { .enumerate() .map(|(i, (_, name))| ComboBoxOption::new(i, name.clone())) .collect::>(); - self.sequence.update(cx, |combo, cx| combo.set_options(options, cx)); + self.sequence + .update(cx, |combo, cx| combo.set_options(options, cx)); self.sequence .update(cx, |combo, cx| combo.set_selected(preselect, cx)); self.sequences = sequences; @@ -1386,10 +1386,7 @@ impl Render for ExportDialogContent { .flex() .gap_2() .child(self.resolution_w.clone()) - .child(div() - .text_color(colors.disabled) - .text_xs() - .child("×")) + .child(div().text_color(colors.disabled).text_xs().child("×")) .child(self.resolution_h.clone()), )) .child(form_row( @@ -1639,7 +1636,11 @@ impl ProxyDialogContent { config_set_bool("ProxyIncludeAudio", params.include_audio); config_set_int( "ProxyMaxConcurrent", - self.max_concurrent.read(cx).value().to_f64().clamp(1.0, 16.0) as i64, + self.max_concurrent + .read(cx) + .value() + .to_f64() + .clamp(1.0, 16.0) as i64, ); config_set_string( CONFIG_KEY_FFMPEG_PATH, @@ -1727,8 +1728,8 @@ fn divider_label(divider: i32) -> String { /// The display string of a proxy lifecycle state. fn proxy_state_label(state: crate::oakui::engine::ProxyMediaState) -> String { - use crate::oakui::engine::ProxyMediaState; - match state { + use crate::oakui::engine::ProxyMediaState; + match state { ProxyMediaState::Missing => i18n::tr("proxydialog.state.missing"), ProxyMediaState::Generating => i18n::tr("proxydialog.state.generating"), ProxyMediaState::Ready => i18n::tr("proxydialog.state.ready"), @@ -1747,7 +1748,7 @@ impl Render for ProxyDialogContent { .map(|row| { let mut state = proxy_state_label(row.state); if row.has_custom { - state.push_str(&i18n::tr("proxydialog.custom_suffix")); + state.push_str(i18n::tr("proxydialog.custom_suffix")); } let enabled = row.enabled && row.state == crate::oakui::engine::ProxyMediaState::Ready; @@ -1960,20 +1961,20 @@ impl ProjectPropertiesContent { let (working, gamut, transfer) = engine.read(cx).project_color_settings(); working_space.update(cx, |combo, cx| { combo.set_selected( - Some(oak_core::colormath::WorkingColorSpace::from_setting(&working) as usize), - cx, + Some(oak_core::colormath::WorkingColorSpace::from_setting(&working) as usize), + cx, ) }); output_gamut.update(cx, |combo, cx| { combo.set_selected( - Some(oak_core::colormath::OutputGamut::from_setting(&gamut) as usize), - cx, + Some(oak_core::colormath::OutputGamut::from_setting(&gamut) as usize), + cx, ) }); output_transfer.update(cx, |combo, cx| { combo.set_selected( - Some(oak_core::colormath::OutputTransfer::from_setting(&transfer) as usize), - cx, + Some(oak_core::colormath::OutputTransfer::from_setting(&transfer) as usize), + cx, ) }); @@ -2057,8 +2058,8 @@ impl ProjectPropertiesContent { /// The color pipeline settings currently selected in the combos, as /// the canonical persisted strings. fn color_settings(&self, cx: &App) -> (String, String, String) { - use oak_core::colormath::{OutputGamut, OutputTransfer, WorkingColorSpace}; - let working = self + use oak_core::colormath::{OutputGamut, OutputTransfer, WorkingColorSpace}; + let working = self .working_space .read(cx) .selected() @@ -2139,7 +2140,7 @@ impl Render for ProjectPropertiesContent directories: false, multiple: false, prompt: None, - allowed_extensions: Vec::new(), + allowed_extensions: Vec::new(), }); cx.spawn(async move |this, cx| { if let Ok(Ok(Some(paths))) = receiver.await { @@ -2742,7 +2743,7 @@ impl Render for KeyboardTabContent { .child( div() .id(ElementId::named_usize("keyboard-shortcut-capture", index)) - .debug_selector(move || format!("keyboard-capture-{index}").into()) + .debug_selector(move || format!("keyboard-capture-{index}")) .min_w(px(150.0)) .px_2() .py_0p5() @@ -3348,7 +3349,11 @@ impl Render for TextValue { .bg(colors.background) .px_2() .py_1() - .child(text_input("oak-seq-name", cx).state(weak).accepts_input(true)) + .child( + text_input("oak-seq-name", cx) + .state(weak) + .accepts_input(true), + ) } } @@ -3388,12 +3393,13 @@ impl SequenceFormatSeed { pub fn from_format(format: &crate::oakui::engine::VideoFormat, interlaced: bool) -> Self { let preset = (1..=SEQUENCE_PRESET_COUNT) .find(|i| { - sequence_preset_format(*i) == Some(( - format.width, - format.height, - format.rate.num, - format.rate.den, - )) + sequence_preset_format(*i) + == Some(( + format.width, + format.height, + format.rate.num, + format.rate.den, + )) }) .unwrap_or(0); let rate = SEQUENCE_RATES @@ -3424,26 +3430,14 @@ pub struct SequenceFormatFields { impl SequenceFormatFields { /// Builds the fields and wires the preset / field cross-updates. - pub fn build( - seed: SequenceFormatSeed, - window: &mut Window, - cx: &mut Context, - ) -> Self { + pub fn build(seed: SequenceFormatSeed, window: &mut Window, cx: &mut Context) -> Self { let preset = cx.new(|cx| ComboBox::new(40, sequence_preset_options(), window, cx)); - preset.update(cx, |combo, cx| { - combo.set_selected(Some(seed.preset), cx) - }); + preset.update(cx, |combo, cx| combo.set_selected(Some(seed.preset), cx)); let width = cx.new(|cx| { SpinBox::new( 41, - SliderModel::new( - ValueKind::Integer, - 16.0, - 8192.0, - 2.0, - f64::from(seed.width), - ), + SliderModel::new(ValueKind::Integer, 16.0, 8192.0, 2.0, f64::from(seed.width)), window, cx, ) @@ -3504,27 +3498,24 @@ impl SequenceFormatFields { // Editing a dimension or the frame rate reverts to the custom entry. cx.subscribe(&width, |this, _spin, event: &SpinBoxEvent, cx| { if let SpinBoxEvent::ValueChanged { .. } = event { - this.preset.update(cx, |combo, cx| { - combo.set_selected(Some(0), cx) - }); + this.preset + .update(cx, |combo, cx| combo.set_selected(Some(0), cx)); cx.notify(); } }) .detach(); cx.subscribe(&height, |this, _spin, event: &SpinBoxEvent, cx| { if let SpinBoxEvent::ValueChanged { .. } = event { - this.preset.update(cx, |combo, cx| { - combo.set_selected(Some(0), cx) - }); + this.preset + .update(cx, |combo, cx| combo.set_selected(Some(0), cx)); cx.notify(); } }) .detach(); cx.subscribe(&rate, |this, _combo, event: &ComboBoxEvent, cx| { let ComboBoxEvent::Selected { .. } = event; - this.preset.update(cx, |combo, cx| { - combo.set_selected(Some(0), cx) - }); + this.preset + .update(cx, |combo, cx| combo.set_selected(Some(0), cx)); cx.notify(); }) .detach(); @@ -3709,17 +3700,15 @@ impl Render for NewSequenceContent { self.name.clone(), )) .child(format_rows) - .child( - if let Some(error) = &self.error { - div() - .debug_selector(|| "seqprops-error".into()) - .text_color(gpui::rgb(0xe5484d)) - .text_xs() - .child(error.clone()) - } else { - div() - }, - ) + .child(if let Some(error) = &self.error { + div() + .debug_selector(|| "seqprops-error".into()) + .text_color(gpui::rgb(0xe5484d)) + .text_xs() + .child(error.clone()) + } else { + div() + }) } } @@ -3818,13 +3807,7 @@ impl SequencePropertiesContent { let format = self.format(cx); let interlaced = self.interlaced(cx); self.engine.update(cx, |engine, cx| { - engine.update_sequence_parameters( - self.sequence_id, - name, - format, - interlaced, - cx, - ) + engine.update_sequence_parameters(self.sequence_id, name, format, interlaced, cx) })?; self.set_error(None, cx); Ok(()) @@ -3858,17 +3841,15 @@ impl Render for SequencePropertiesContent self.name.clone(), )) .child(format_rows) - .child( - if let Some(error) = &self.error { - div() - .debug_selector(|| "seqprops-error".into()) - .text_color(gpui::rgb(0xe5484d)) - .text_xs() - .child(error.clone()) - } else { - div() - }, - ) + .child(if let Some(error) = &self.error { + div() + .debug_selector(|| "seqprops-error".into()) + .text_color(gpui::rgb(0xe5484d)) + .text_xs() + .child(error.clone()) + } else { + div() + }) } } @@ -3970,11 +3951,7 @@ const WIZARD_SYNC_MODES: &[&str] = &[ impl MulticamWizardContent { /// Builds the content seeded with the engine's wizard footage. - pub fn new( - engine: Entity, - window: &mut Window, - cx: &mut Context, - ) -> Self { + pub fn new(engine: Entity, window: &mut Window, cx: &mut Context) -> Self { let name = cx.new(|cx| { let editor = cx.new(|cx| EditableTextState::new(StringStorage::default(), cx)); TextValue { editor } @@ -4061,18 +4038,22 @@ impl Render for MulticamWizardContent { .rounded_sm() .hover(|style| style.bg(colors.container)) .cursor_pointer() - .on_click(cx.listener( - move |this: &mut Self, _event: &ClickEvent, _window, cx| { + .on_click( + cx.listener(move |this: &mut Self, _event: &ClickEvent, _window, cx| { this.toggle_row(index, cx); - }, - )) + }), + ) .child( div() .size(px(12.0)) .flex() .items_center() .justify_center() - .bg(if is_checked { colors.selected } else { colors.background }) + .bg(if is_checked { + colors.selected + } else { + colors.background + }) .text_color(colors.text) .child(if is_checked { "✓" } else { "" }), ) @@ -4083,10 +4064,12 @@ impl Render for MulticamWizardContent { .child(entry.name.clone()), ) .child( - div() - .text_xs() - .text_color(colors.disabled) - .child(entry.duration_s.map(|d| format!("{d:.1}s")).unwrap_or_default()), + div().text_xs().text_color(colors.disabled).child( + entry + .duration_s + .map(|d| format!("{d:.1}s")) + .unwrap_or_default(), + ), ); rows_div = rows_div.child(row); } @@ -4157,16 +4140,11 @@ impl RenameContent { impl Render for RenameContent { fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { let colors = cx.default_colors().clone(); - div() - .flex() - .flex_col() - .gap_3() - .w_full() - .child(form_row( - &colors, - i18n::tr("project.context.rename").into(), - self.field.clone(), - )) + div().flex().flex_col().gap_3().w_full().child(form_row( + &colors, + i18n::tr("project.context.rename").into(), + self.field.clone(), + )) } } @@ -4193,7 +4171,10 @@ impl ExportProjectDialogContent { let formats: Vec<(i32, String)> = vec![ (PROJECT_FORMAT_OTIO, "OpenTimelineIO (.otio)".to_string()), (PROJECT_FORMAT_OVE, "Oak (.ove)".to_string()), - (PROJECT_FORMAT_FCPXML, "Final Cut Pro XML (.fcpxml)".to_string()), + ( + PROJECT_FORMAT_FCPXML, + "Final Cut Pro XML (.fcpxml)".to_string(), + ), ]; let options = formats .iter() @@ -4204,7 +4185,9 @@ impl ExportProjectDialogContent { ComboBox::new(4, options, window, cx) .with_placeholder(i18n::tr("project.export.format")) }); - format.update(cx, |combo, cx| combo.set_selected(Some(PROJECT_FORMAT_OTIO as usize), cx)); + format.update(cx, |combo, cx| { + combo.set_selected(Some(PROJECT_FORMAT_OTIO as usize), cx) + }); Self { format, formats } } @@ -4232,16 +4215,11 @@ impl ExportProjectDialogContent { impl Render for ExportProjectDialogContent { fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { let colors = cx.default_colors().clone(); - div() - .flex() - .flex_col() - .gap_3() - .w_full() - .child(form_row( - &colors, - i18n::tr("project.export.format").into(), - self.format.clone(), - )) + div().flex().flex_col().gap_3().w_full().child(form_row( + &colors, + i18n::tr("project.export.format").into(), + self.format.clone(), + )) } } @@ -4264,9 +4242,8 @@ impl NewProjectContent { name.update(cx, |field, cx| { field.set_value(i18n::tr("manager.new.default_name"), cx) }); - let format = cx.new(|cx| { - SequenceFormatFields::build(SequenceFormatSeed::hd_1080p25(), window, cx) - }); + let format = + cx.new(|cx| SequenceFormatFields::build(SequenceFormatSeed::hd_1080p25(), window, cx)); Self { name, format } } @@ -4305,8 +4282,8 @@ impl Render for NewProjectContent { #[cfg(test)] mod tests { - use super::*; - fn keystroke(key: &str) -> Keystroke { + use super::*; + fn keystroke(key: &str) -> Keystroke { gpui::Keystroke::parse(key).unwrap() } @@ -4316,9 +4293,10 @@ mod tests { #[gpui::test] async fn export_dialog_sequence_picker(cx: &mut gpui::TestAppContext) { cx.update(|cx| cx.init_colors()); - let window = cx.open_window(gpui::size(gpui::px(440.0), gpui::px(400.0)), |window, cx| { - ExportDialogContent::new(window, cx) - }); + let window = cx.open_window( + gpui::size(gpui::px(440.0), gpui::px(400.0)), + ExportDialogContent::new, + ); cx.run_until_parked(); let content = window.root(cx).expect("dialog content root"); @@ -4349,7 +4327,9 @@ mod tests { ); cx.update(|cx| { - content.update(cx, |content, cx| content.set_sequences(Vec::new(), None, cx)); + content.update(cx, |content, cx| { + content.set_sequences(Vec::new(), None, cx) + }); }); assert_eq!( cx.read(|cx| content.read(cx).selected_sequence(cx)), @@ -4370,8 +4350,16 @@ mod tests { assert_eq!(sequence_preset_format(7), None); // The preset labels come from the language packs (default English // here): the 4K entries say what they set. - assert!(labels[5].contains("3840×2160"), "4K UHD label: {}", labels[5]); - assert!(labels[6].contains("4096×2160"), "4K DCI label: {}", labels[6]); + assert!( + labels[5].contains("3840×2160"), + "4K UHD label: {}", + labels[5] + ); + assert!( + labels[6].contains("4096×2160"), + "4K DCI label: {}", + labels[6] + ); } /// A probed footage format seeds the new-sequence fields: an exact diff --git a/crates/oak-app/src/i18n.rs b/crates/oak-app/src/i18n.rs index 20da1be37..e17ed21c6 100644 --- a/crates/oak-app/src/i18n.rs +++ b/crates/oak-app/src/i18n.rs @@ -296,6 +296,23 @@ fn reload_for_test() { *tables = load_packs(); } +/// Serializes tests that mutate the language global / HOME. +#[cfg(test)] +pub(crate) fn test_lock() -> std::sync::MutexGuard<'static, ()> { + lang_test_lock().lock().unwrap_or_else(|e| e.into_inner()) +} + +/// Compatibility alias used by the app/actions test modules. Both entry +/// points MUST share the one static mutex: with two separate mutexes the +/// i18n tests and the app/actions tests do not exclude each other, and +/// the language global races (observed as a Windows-only CI failure +/// where `tr` returned the English value mid-test). +#[cfg(test)] +pub(crate) fn lang_test_lock() -> &'static std::sync::Mutex<()> { + static LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(()); + &LOCK +} + #[cfg(test)] mod tests { use super::*; @@ -454,20 +471,3 @@ mod tests { set_language_code("en-US"); } } - -/// Serializes tests that mutate the language global / HOME. -#[cfg(test)] -pub(crate) fn test_lock() -> std::sync::MutexGuard<'static, ()> { - lang_test_lock().lock().unwrap_or_else(|e| e.into_inner()) -} - -/// Compatibility alias used by the app/actions test modules. Both entry -/// points MUST share the one static mutex: with two separate mutexes the -/// i18n tests and the app/actions tests do not exclude each other, and -/// the language global races (observed as a Windows-only CI failure -/// where `tr` returned the English value mid-test). -#[cfg(test)] -pub(crate) fn lang_test_lock() -> &'static std::sync::Mutex<()> { - static LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(()); - &LOCK -} diff --git a/crates/oak-app/src/manager.rs b/crates/oak-app/src/manager.rs index 1c5d82a75..81e19ba6d 100644 --- a/crates/oak-app/src/manager.rs +++ b/crates/oak-app/src/manager.rs @@ -30,6 +30,7 @@ //! are unit tested here; the app-level flows are covered by the //! `OakApp` tests in `crate::app`. +use crate::oakui::component::text_input; use gpui::colors::DefaultColors; use gpui::prelude::*; use gpui::{ @@ -37,7 +38,6 @@ use gpui::{ Window, }; use gpui_elements::editable_text::{EditableTextState, StringStorage}; -use crate::oakui::component::text_input; use crate::i18n; use crate::oakui::{AppEngine, LibraryProject}; @@ -162,7 +162,11 @@ impl ProjectManager { } /// An action-row button targeting the selection. - fn selected_action(&mut self, action: impl FnOnce(String) -> ManagerEvent, cx: &mut Context) { + fn selected_action( + &mut self, + action: impl FnOnce(String) -> ManagerEvent, + cx: &mut Context, + ) { if let Some(uuid) = self.selected_uuid() { self.emit(action(uuid), cx); } @@ -252,13 +256,11 @@ impl Render for ProjectManager { .min_h_0() .overflow_y_scroll() .children(if self.rows.is_empty() { - vec![ - div() - .p_4() - .text_color(colors.disabled) - .child(i18n::tr("manager.empty")) - .into_any_element(), - ] + vec![div() + .p_4() + .text_color(colors.disabled) + .child(i18n::tr("manager.empty")) + .into_any_element()] } else { self.rows .iter() @@ -272,11 +274,14 @@ impl Render for ProjectManager { .gap_2() .py_1() .cursor_pointer() - .on_click(cx.listener(move |this, event: &ClickEvent, _w, cx| { - this.row_clicked(index, event.click_count(), cx); - })); + .on_click(cx.listener( + move |this, event: &ClickEvent, _w, cx| { + this.row_clicked(index, event.click_count(), cx); + }, + )); if selected { - line = line.bg(colors.selected).text_color(colors.selected_text); + line = + line.bg(colors.selected).text_color(colors.selected_text); } else { line = line.text_color(colors.text); } @@ -301,72 +306,69 @@ impl Render for ProjectManager { }), ); - let mut root = div() - .flex() - .flex_col() - .gap_3() - .w_full() - .child( - div() - .flex() - .gap_2() - .child( - tool("manager-new", i18n::tr("manager.new")).on_click( - cx.listener(|this, _e: &ClickEvent, _w, cx| { - this.emit(ManagerEvent::Create, cx); - }), - ), - ) - .child( - tool("manager-import", i18n::tr("manager.import")).on_click( + let mut root = + div() + .flex() + .flex_col() + .gap_3() + .w_full() + .child( + div() + .flex() + .gap_2() + .child( + tool("manager-new", i18n::tr("manager.new")).on_click(cx.listener( + |this, _e: &ClickEvent, _w, cx| { + this.emit(ManagerEvent::Create, cx); + }, + )), + ) + .child(tool("manager-import", i18n::tr("manager.import")).on_click( cx.listener(|this, _e: &ClickEvent, _w, cx| { this.emit(ManagerEvent::Import, cx); }), - ), - ), - ) - .child(list) - .child( - div() - .flex() - .justify_end() - .gap_2() - .child( - action("manager-open", i18n::tr("manager.open")).on_click( + )), + ) + .child(list) + .child( + div() + .flex() + .justify_end() + .gap_2() + .child(action("manager-open", i18n::tr("manager.open")).on_click( cx.listener(|this, _e: &ClickEvent, _w, cx| { this.selected_action(ManagerEvent::Open, cx); }), + )) + .child( + action("manager-rename", i18n::tr("manager.rename")).on_click( + cx.listener(|this, _e: &ClickEvent, _w, cx| { + this.selected_action(ManagerEvent::Rename, cx); + }), + ), + ) + .child( + action("manager-duplicate", i18n::tr("manager.duplicate")).on_click( + cx.listener(|this, _e: &ClickEvent, _w, cx| { + this.selected_action(ManagerEvent::Duplicate, cx); + }), + ), + ) + .child( + action("manager-delete", i18n::tr("manager.delete")).on_click( + cx.listener(|this, _e: &ClickEvent, _w, cx| { + this.selected_action(ManagerEvent::Delete, cx); + }), + ), + ) + .child( + action("manager-export", i18n::tr("manager.export")).on_click( + cx.listener(|this, _e: &ClickEvent, _w, cx| { + this.selected_action(ManagerEvent::Export, cx); + }), + ), ), - ) - .child( - action("manager-rename", i18n::tr("manager.rename")).on_click( - cx.listener(|this, _e: &ClickEvent, _w, cx| { - this.selected_action(ManagerEvent::Rename, cx); - }), - ), - ) - .child( - action("manager-duplicate", i18n::tr("manager.duplicate")).on_click( - cx.listener(|this, _e: &ClickEvent, _w, cx| { - this.selected_action(ManagerEvent::Duplicate, cx); - }), - ), - ) - .child( - action("manager-delete", i18n::tr("manager.delete")).on_click( - cx.listener(|this, _e: &ClickEvent, _w, cx| { - this.selected_action(ManagerEvent::Delete, cx); - }), - ), - ) - .child( - action("manager-export", i18n::tr("manager.export")).on_click( - cx.listener(|this, _e: &ClickEvent, _w, cx| { - this.selected_action(ManagerEvent::Export, cx); - }), - ), - ), - ); + ); if let Some(status) = &self.status { root = root.child( div() @@ -396,10 +398,7 @@ pub struct NamePrompt { impl NamePrompt { /// Builds the prompt seeded with `initial`. pub fn new(initial: &str, cx: &mut Context) -> Self { - let editor = cx.new(|cx| { - let editor = EditableTextState::new(StringStorage::default(), cx); - editor - }); + let editor = cx.new(|cx| EditableTextState::new(StringStorage::default(), cx)); editor.update(cx, |editor, cx| editor.emplace(initial, cx)); Self { editor } } @@ -411,7 +410,8 @@ impl NamePrompt { /// Replaces the entered name (tests / prefill). pub fn set_value(&mut self, value: &str, cx: &mut Context) { - self.editor.update(cx, |editor, cx| editor.emplace(value, cx)); + self.editor + .update(cx, |editor, cx| editor.emplace(value, cx)); cx.notify(); } } diff --git a/crates/oak-app/src/oakui/component/menu.rs b/crates/oak-app/src/oakui/component/menu.rs index 664b4cdec..c8af2af32 100644 --- a/crates/oak-app/src/oakui/component/menu.rs +++ b/crates/oak-app/src/oakui/component/menu.rs @@ -82,7 +82,8 @@ impl ContextMenuHandle { /// Open the menu at `position` (window coordinates). pub fn show(&self, position: Point, menu: Menu, cx: &mut App) { - self.menu.update(cx, |menu_view, cx| menu_view.show(position, menu, cx)); + self.menu + .update(cx, |menu_view, cx| menu_view.show(position, menu, cx)); } /// The popup entity, to be rendered as a child of the panel so the @@ -126,7 +127,8 @@ pub const LANG_ITEM_COUNT: usize = 32; /// Maps a menu item id in the language range back to its index into /// [`crate::i18n::available_languages`]. pub fn language_item_index(item: usize) -> Option { - (item >= LANG_ITEM_BASE && item < LANG_ITEM_BASE + LANG_ITEM_COUNT) + (LANG_ITEM_BASE..LANG_ITEM_BASE + LANG_ITEM_COUNT) + .contains(&item) .then(|| item - LANG_ITEM_BASE) } @@ -262,7 +264,8 @@ pub fn color_label_item(selected: Option) -> MenuItem { /// The color index (`0..COLOR_LABEL_COUNT`) behind a triggered menu item id, /// when `item` is one of the color-label items. pub fn color_label_index(item: usize) -> Option { - (item >= COLOR_LABEL_BASE && item < COLOR_LABEL_BASE + COLOR_LABEL_COUNT) + (COLOR_LABEL_BASE..COLOR_LABEL_BASE + COLOR_LABEL_COUNT) + .contains(&item) .then(|| item - COLOR_LABEL_BASE) } @@ -334,12 +337,17 @@ impl Default for ViewerMenuState { pub fn viewer_menu(state: &ViewerMenuState) -> Menu { use crate::i18n::tr; // Zoom: Fit + one entry per zoom level, checked against the live state. - let mut zoom_items = vec![MenuItem::new(LOCAL_VIEWER_ZOOM_FIT, tr("viewer.context.zoom_fit")) - .with_checked(state.zoom == ViewerZoom::Fit)]; + let mut zoom_items = vec![ + MenuItem::new(LOCAL_VIEWER_ZOOM_FIT, tr("viewer.context.zoom_fit")) + .with_checked(state.zoom == ViewerZoom::Fit), + ]; for (index, level) in VIEWER_ZOOM_LEVELS.iter().enumerate() { zoom_items.push( - MenuItem::new(viewer_zoom_level_id(index), format!("{:.0}%", level * 100.0)) - .with_checked(state.zoom == ViewerZoom::Level(index)), + MenuItem::new( + viewer_zoom_level_id(index), + format!("{:.0}%", level * 100.0), + ) + .with_checked(state.zoom == ViewerZoom::Level(index)), ); } // Playback Resolution radio group (the C++ `PlaybackDivider` config): @@ -376,9 +384,10 @@ pub fn viewer_menu(state: &ViewerMenuState) -> Menu { Menu::new(vec![ MenuItem::new(0, tr("viewer.context.zoom")).with_submenu(Menu::new(zoom_items)), MenuItem::new(LOCAL_VIEWER_FULL_SCREEN, tr("viewer.context.full_screen")), - MenuItem::new(0, tr("viewer.context.playback_resolution")) - .with_submenu(resolution_menu), - MenuItem::new(0, tr("viewer.context.safe_margins")).with_submenu(safe_menu).separated(), + MenuItem::new(0, tr("viewer.context.playback_resolution")).with_submenu(resolution_menu), + MenuItem::new(0, tr("viewer.context.safe_margins")) + .with_submenu(safe_menu) + .separated(), MenuItem::new(LOCAL_VIEWER_STOP_ON_LAST, tr("viewer.context.stop_on_last")) .with_checked(state.stop_on_last) .separated(), @@ -426,7 +435,9 @@ pub fn viewer_menu_action(item: usize) -> Option { } else if item >= LOCAL_VIEWER_ZOOM_LEVELS_BASE && item < LOCAL_VIEWER_ZOOM_LEVELS_BASE + VIEWER_ZOOM_LEVELS.len() { - Some(ViewerMenuAction::ZoomLevel(item - LOCAL_VIEWER_ZOOM_LEVELS_BASE)) + Some(ViewerMenuAction::ZoomLevel( + item - LOCAL_VIEWER_ZOOM_LEVELS_BASE, + )) } else { match item { LOCAL_VIEWER_FULL_SCREEN => Some(ViewerMenuAction::FullScreen), @@ -585,7 +596,9 @@ mod tests { fn viewer_menu_offers_every_zoom_level() { // The label lookups below race with tests that flip the process // language: pin en-US under the shared lock. - let _guard = crate::i18n::lang_test_lock().lock().unwrap_or_else(|e| e.into_inner()); + let _guard = crate::i18n::lang_test_lock() + .lock() + .unwrap_or_else(|e| e.into_inner()); crate::i18n::set_language_code("en-US"); let menu = viewer_menu(&ViewerMenuState::default()); let zoom = menu @@ -599,7 +612,10 @@ mod tests { assert_eq!(zoom_items[0].checked, Some(true), "Fit checked by default"); for (index, level) in VIEWER_ZOOM_LEVELS.iter().enumerate() { assert_eq!(zoom_items[index + 1].id, viewer_zoom_level_id(index)); - assert_eq!(zoom_items[index + 1].label, format!("{:.0}%", level * 100.0)); + assert_eq!( + zoom_items[index + 1].label, + format!("{:.0}%", level * 100.0) + ); assert_eq!( zoom_items[index + 1].checked, Some(false), @@ -614,7 +630,9 @@ mod tests { fn viewer_menu_radio_groups_default_to_the_first_entry() { // The label lookups below race with tests that flip the process // language: pin en-US under the shared lock. - let _guard = crate::i18n::lang_test_lock().lock().unwrap_or_else(|e| e.into_inner()); + let _guard = crate::i18n::lang_test_lock() + .lock() + .unwrap_or_else(|e| e.into_inner()); crate::i18n::set_language_code("en-US"); let menu = viewer_menu(&ViewerMenuState::default()); for label_key in [ @@ -630,7 +648,9 @@ mod tests { let sub = item.submenu.as_ref().unwrap(); assert_eq!(sub.items[0].checked, Some(true), "{label_key} default"); assert!( - sub.items[1..].iter().all(|item| item.checked == Some(false)), + sub.items[1..] + .iter() + .all(|item| item.checked == Some(false)), "{label_key} non-defaults unchecked" ); } @@ -640,7 +660,9 @@ mod tests { #[test] fn viewer_menu_resolution_radio_follows_the_divider() { // Label lookup: pin en-US under the shared language lock. - let _guard = crate::i18n::lang_test_lock().lock().unwrap_or_else(|e| e.into_inner()); + let _guard = crate::i18n::lang_test_lock() + .lock() + .unwrap_or_else(|e| e.into_inner()); crate::i18n::set_language_code("en-US"); let menu = viewer_menu(&ViewerMenuState { playback_divider: 4, @@ -662,7 +684,9 @@ mod tests { #[test] fn viewer_menu_reflects_live_state() { // Label lookup: pin en-US under the shared language lock. - let _guard = crate::i18n::lang_test_lock().lock().unwrap_or_else(|e| e.into_inner()); + let _guard = crate::i18n::lang_test_lock() + .lock() + .unwrap_or_else(|e| e.into_inner()); crate::i18n::set_language_code("en-US"); let menu = viewer_menu(&ViewerMenuState { playback_divider: 2, @@ -721,7 +745,10 @@ mod tests { None, "zoom id past the last level" ); - assert_eq!(viewer_menu_action(LOCAL_VIEWER_FULL_SCREEN), Some(V::FullScreen)); + assert_eq!( + viewer_menu_action(LOCAL_VIEWER_FULL_SCREEN), + Some(V::FullScreen) + ); for (id, divider) in [ (LOCAL_VIEWER_RES_FULL, 1), (LOCAL_VIEWER_RES_HALF, 2), @@ -732,8 +759,14 @@ mod tests { } assert_eq!(viewer_menu_action(LOCAL_VIEWER_SAFE_OFF), Some(V::SafeOff)); assert_eq!(viewer_menu_action(LOCAL_VIEWER_SAFE_ON), Some(V::SafeOn)); - assert_eq!(viewer_menu_action(LOCAL_VIEWER_SAFE_CUSTOM), Some(V::SafeCustom)); - assert_eq!(viewer_menu_action(LOCAL_VIEWER_STOP_ON_LAST), Some(V::StopOnLast)); + assert_eq!( + viewer_menu_action(LOCAL_VIEWER_SAFE_CUSTOM), + Some(V::SafeCustom) + ); + assert_eq!( + viewer_menu_action(LOCAL_VIEWER_STOP_ON_LAST), + Some(V::StopOnLast) + ); assert_eq!( viewer_menu_action(LOCAL_VIEWER_WF_AUTOMATIC), Some(V::Waveform(WaveformMode::Automatic)) @@ -747,8 +780,15 @@ mod tests { Some(V::Waveform(WaveformMode::Both)) ); assert_eq!(viewer_menu_action(LOCAL_VIEWER_SHOW_FPS), Some(V::ShowFps)); - assert_eq!(viewer_menu_action(LOCAL_VIEWER_SAVE_FRAME), Some(V::SaveFrame)); - assert_eq!(viewer_menu_action(0), None, "registry-range id is not local"); + assert_eq!( + viewer_menu_action(LOCAL_VIEWER_SAVE_FRAME), + Some(V::SaveFrame) + ); + assert_eq!( + viewer_menu_action(0), + None, + "registry-range id is not local" + ); assert_eq!(viewer_menu_action(LOCAL_ID_BASE), None, "color-label id"); } } diff --git a/crates/oak-app/src/oakui/displaycolor.rs b/crates/oak-app/src/oakui/displaycolor.rs index 099c6fb27..5e7a2fb86 100644 --- a/crates/oak-app/src/oakui/displaycolor.rs +++ b/crates/oak-app/src/oakui/displaycolor.rs @@ -82,9 +82,9 @@ use std::sync::{Arc, LazyLock, Mutex}; use std::time::{Duration, Instant}; +use oak_core::color::{pipeline_output_spec, ColorProcessor}; use oak_core::colormath::{output_spec_to_xyz_d65, OutputColorSpec, OutputGamut, OutputTransfer}; use oak_core::configstore::ConfigStore; -use oak_core::color::{pipeline_output_spec, ColorProcessor}; /// Config key: the display color management mode ("icc" / "off"). The /// preference only applies where the platform policy allows self-management @@ -167,9 +167,7 @@ pub fn display_policy() -> DisplayPolicy { "os" => return DisplayPolicy::OsManaged, "self" => return DisplayPolicy::SelfManaged, other => { - log::warn!( - "OAK_DISPLAY_POLICY={other:?} ignored (expected \"os\" or \"self\")" - ); + log::warn!("OAK_DISPLAY_POLICY={other:?} ignored (expected \"os\" or \"self\")"); } } } @@ -208,7 +206,10 @@ pub fn display_policy() -> DisplayPolicy { // mapping, so self-management is the only way to honor wide-gamut // displays there (the "off" preference opts back into OS-management). #[cfg(target_os = "macos")] - match ConfigStore::instance().get(None, CONFIG_KEY_COLOR_MODE).as_deref() { + match ConfigStore::instance() + .get(None, CONFIG_KEY_COLOR_MODE) + .as_deref() + { Ok("icc") => DisplayPolicy::SelfManaged, _ => DisplayPolicy::OsManaged, } @@ -288,7 +289,7 @@ fn current_monitor_fingerprint(window: &gpui::Window, cx: &gpui::App) -> Option< let center = window.bounds().center(); let x = f64::from(center.x) * f64::from(window.scale_factor()); let y = f64::from(center.y) * f64::from(window.scale_factor()); - return oak_core::displayicc::x11_monitor_fingerprint_at(x, y); + oak_core::displayicc::x11_monitor_fingerprint_at(x, y) } #[cfg(not(any(target_os = "macos", target_os = "windows", target_os = "linux")))] { diff --git a/crates/oak-app/src/oakui/gpu.rs b/crates/oak-app/src/oakui/gpu.rs index 1c6e4c86b..49b240b1e 100644 --- a/crates/oak-app/src/oakui/gpu.rs +++ b/crates/oak-app/src/oakui/gpu.rs @@ -90,7 +90,7 @@ fn build_display_lut() -> oak_core::lut::Lut3d { ); super::displaycolor::apply_f32_rgba(&mut samples, n as i64); let mut data = Vec::with_capacity(n * 3); - for px in samples.chunks_exact(4) { + for px in samples.as_chunks::<4>().0 { data.extend_from_slice(&px[..3]); } oak_core::lut::Lut3d { edge, lo, hi, data } @@ -128,7 +128,8 @@ pub fn register_context(device: std::sync::Arc, queue: std::sync:: if let Ok(mut ctx) = GPU_CONTEXT.lock() { *ctx = Some((device.clone(), queue.clone())); } - let adopted = oak_core::backend::GpuContext::adopt(device, queue, oak_core::backend::BackendKind::Auto); + let adopted = + oak_core::backend::GpuContext::adopt(device, queue, oak_core::backend::BackendKind::Auto); if !oak_core::backend::GpuContext::install_shared(Some(adopted)) { // The engine context was already used for GPU work before the // window opened: it cannot be replaced, so present falls back to @@ -193,8 +194,7 @@ pub fn upload_rgba16f( let ctx = GPU_CONTEXT.lock().ok()?; ctx.as_ref().map(|(d, q)| (d.clone(), q.clone()))? }; - let (bytes, bytes_per_row) = - super::frames::f32_rgba_to_16f_bytes(width, height, samples)?; + let (bytes, bytes_per_row) = super::frames::f32_rgba_to_16f_bytes(width, height, samples)?; let texture = device.create_texture(&wgpu::TextureDescriptor { label: Some("oak_display_rgba16f"), size: wgpu::Extent3d { diff --git a/crates/oak-app/src/oakui/graphops.rs b/crates/oak-app/src/oakui/graphops.rs index 8650f509a..4be372fee 100644 --- a/crates/oak-app/src/oakui/graphops.rs +++ b/crates/oak-app/src/oakui/graphops.rs @@ -699,12 +699,11 @@ pub fn ts_to_rational(ts: i64, tb: (i64, i64)) -> Rational { /// The sequence's track list of `kind`, when it exists. pub fn track_list_of(g: &Graph, seq: NodeId, kind: TrackType) -> Option { - for &list_id in &sequence_behavior(g, seq)?.track_lists { - if track_list_behavior(g, list_id).map(|l| l.kind) == Some(kind) { - return Some(list_id); - } - } - None + sequence_behavior(g, seq)? + .track_lists + .iter() + .find(|&&list_id| track_list_behavior(g, list_id).map(|l| l.kind) == Some(kind)) + .copied() } /// Find (or create) the sequence's track list of `kind` (the facade's @@ -1700,7 +1699,12 @@ pub fn create_adjustment_layer( let index = track_list_behavior(&g.graph, list) .and_then(|l| l.tracks.iter().position(|&t| t == track)) .ok_or_else(|| "the track is not in its list".to_string())?; - (list, index, ts_to_rational(in_ts, tb), ts_to_rational(out_ts, tb)) + ( + list, + index, + ts_to_rational(in_ts, tb), + ts_to_rational(out_ts, tb), + ) }; // The block itself: no label, no color override, no media wiring — @@ -2370,7 +2374,12 @@ pub fn place_generator_clip( in_r, ) .to_command(); - let edge = connect_command(p, generator, clip, oak_node::block::clip_input::TEXTURE_INPUT)?; + let edge = connect_command( + p, + generator, + clip, + oak_node::block::clip_input::TEXTURE_INPUT, + )?; push_multi(vec![place, edge], "Add Clip")?; Ok(clip) } @@ -4557,7 +4566,15 @@ mod undo_cycle_ops_tests { /// tests use. fn two_touching_clips( media: &std::path::Path, - ) -> (ProjectRef, NodeId, NodeId, NodeId, NodeId, (i64, i64), Rational) { + ) -> ( + ProjectRef, + NodeId, + NodeId, + NodeId, + NodeId, + (i64, i64), + Rational, + ) { let project = create_project(); let seq = create_sequence(&project, "Default Transition"); let footage = import_footage(&project, media).expect("import"); @@ -4586,8 +4603,8 @@ mod undo_cycle_ops_tests { fn default_transition_covers_both_ends_of_a_lone_clip() { let _g = test_lock(); oak_undo::global::clear().unwrap(); - let media = std::env::temp_dir() - .join(format!("oak_lone_transition_{}.mp4", std::process::id())); + let media = + std::env::temp_dir().join(format!("oak_lone_transition_{}.mp4", std::process::id())); oak_codec::testmedia::write_test_clip(&media, 64, 64, 10, 10).expect("generate"); let project = create_project(); let seq = create_sequence(&project, "Lone Transition"); @@ -4604,10 +4621,14 @@ mod undo_cycle_ops_tests { let half = ts_to_rational(3, tb); let before = oak_undo::global::count().unwrap(); - let created = add_default_transition(&project, seq, &[clip], half) - .expect("add default transition"); + let created = + add_default_transition(&project, seq, &[clip], half).expect("add default transition"); assert_eq!(created, 2, "a lone clip takes both ends"); - assert_eq!(oak_undo::global::count().unwrap(), before + 1, "one undo row"); + assert_eq!( + oak_undo::global::count().unwrap(), + before + 1, + "one undo row" + ); let edge = half + half; { @@ -4615,7 +4636,10 @@ mod undo_cycle_ops_tests { let blocks = track_behavior(&g.graph, track) .map(|t| t.blocks.clone()) .unwrap_or_default(); - let index = blocks.iter().position(|&b| b == clip).expect("clip on track"); + let index = blocks + .iter() + .position(|&b| b == clip) + .expect("clip on track"); // (A leading gap when the clip starts past zero,) the head // transition, the clip, the tail transition. let (head, tail) = (blocks[index - 1], blocks[index + 1]); @@ -4636,12 +4660,14 @@ mod undo_cycle_ops_tests { "the head transition spans the clip's head" ); assert_eq!( - g.graph.connected_output(head, oak_node::block::transition_input::IN_BLOCK, -1), + g.graph + .connected_output(head, oak_node::block::transition_input::IN_BLOCK, -1), Some(clip), "the head transition wires only the clip in" ); assert_eq!( - g.graph.connected_output(head, oak_node::block::transition_input::OUT_BLOCK, -1), + g.graph + .connected_output(head, oak_node::block::transition_input::OUT_BLOCK, -1), None, "the head transition has no outgoing side" ); @@ -4662,12 +4688,14 @@ mod undo_cycle_ops_tests { "the tail transition spans the clip's tail" ); assert_eq!( - g.graph.connected_output(tail, oak_node::block::transition_input::OUT_BLOCK, -1), + g.graph + .connected_output(tail, oak_node::block::transition_input::OUT_BLOCK, -1), Some(clip), "the tail transition wires only the clip out" ); assert_eq!( - g.graph.connected_output(tail, oak_node::block::transition_input::IN_BLOCK, -1), + g.graph + .connected_output(tail, oak_node::block::transition_input::IN_BLOCK, -1), None, "the tail transition has no incoming side" ); @@ -4694,8 +4722,8 @@ mod undo_cycle_ops_tests { fn default_transition_covers_the_selected_seams() { let _g = test_lock(); oak_undo::global::clear().unwrap(); - let media = std::env::temp_dir() - .join(format!("oak_default_transition_{}.mp4", std::process::id())); + let media = + std::env::temp_dir().join(format!("oak_default_transition_{}.mp4", std::process::id())); oak_codec::testmedia::write_test_clip(&media, 64, 64, 10, 10).expect("generate"); let (project, seq, track, a, b, tb, half) = two_touching_clips(&media); @@ -4716,7 +4744,11 @@ mod undo_cycle_ops_tests { let blocks = track_behavior(&g.graph, track) .map(|t| t.blocks.clone()) .unwrap_or_default(); - assert_eq!(blocks.len(), 3, "the transition joins the track: {blocks:?}"); + assert_eq!( + blocks.len(), + 3, + "the transition joins the track: {blocks:?}" + ); let t = blocks[1]; let seam = ts_to_rational(10, tb); let core = block_core_of(&g.graph, t).expect("transition core"); @@ -4737,12 +4769,14 @@ mod undo_cycle_ops_tests { "a default transition is symmetric" ); assert_eq!( - g.graph.connected_output(t, oak_node::block::transition_input::OUT_BLOCK, -1), + g.graph + .connected_output(t, oak_node::block::transition_input::OUT_BLOCK, -1), Some(a), "the previous clip feeds out_block_in" ); assert_eq!( - g.graph.connected_output(t, oak_node::block::transition_input::IN_BLOCK, -1), + g.graph + .connected_output(t, oak_node::block::transition_input::IN_BLOCK, -1), Some(b), "the next clip feeds in_block_in" ); @@ -4787,7 +4821,10 @@ mod undo_cycle_ops_tests { assert_eq!(oak_undo::global::count().unwrap(), rows); { let g = lock(&project); - assert!(g.graph.is_valid(transition), "the refused batch changed nothing"); + assert!( + g.graph.is_valid(transition), + "the refused batch changed nothing" + ); } oak_undo::global::clear().unwrap(); @@ -4844,8 +4881,8 @@ mod undo_cycle_ops_tests { cycle_assert(&project, seq, &post, "transition length"); // A request longer than the dragged clip is clamped to its length. - let applied = - set_transition_length(&project, b, true, frame, ts_to_rational(9_999, tb)).expect("clamp"); + let applied = set_transition_length(&project, b, true, frame, ts_to_rational(9_999, tb)) + .expect("clamp"); assert_eq!( applied, ts_to_rational(10, tb), diff --git a/crates/oak-app/src/oakui/mock.rs b/crates/oak-app/src/oakui/mock.rs index 575d30c67..8c08c4093 100644 --- a/crates/oak-app/src/oakui/mock.rs +++ b/crates/oak-app/src/oakui/mock.rs @@ -43,19 +43,19 @@ use std::sync::{Arc, Mutex}; use std::time::Instant; use gpui::effect_stack::{ - EffectCardKind, EffectData, EffectId, EffectStackDataSource, EffectStackEvent, + EffectCardKind, EffectData, EffectId, EffectStackDataSource, EffectStackEvent, }; use gpui::node_graph::{ - EdgeData, EdgeId, NodeData, NodeGraphDataSource, NodeGraphEvent, NodeId, PortData, - PortDataType, PortId, PortKind, + EdgeData, EdgeId, NodeData, NodeGraphDataSource, NodeGraphEvent, NodeId, PortData, + PortDataType, PortId, PortKind, }; use gpui::timeline::{ - ClipData, ClipId, Frame, FrameRange, FrameRate, Marker, TimelineDataSource, TimelineEvent, - TrackData, TrackKind, + ClipData, ClipId, Frame, FrameRange, FrameRate, Marker, TimelineDataSource, TimelineEvent, + TrackData, TrackKind, }; use gpui::{ - hsla, point, prelude::*, px, App, Context, Entity, Hsla, Pixels, Point, RenderImage, - SharedString, + 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}; @@ -67,8 +67,8 @@ use oak_node::track::TrackType; use oak_timeline::util::{block_clip_create, track_append_block}; use super::engine::{ - AppEngine, EngineGateway, ExportEvent, ExportSession, LibraryProject, Monitor, MulticamState, - Project, ScopeData, Sequence, VideoFormat, WizardFootage, WizardSyncOffset, + AppEngine, EngineGateway, ExportEvent, ExportSession, LibraryProject, Monitor, MulticamState, + Project, ScopeData, Sequence, VideoFormat, WizardFootage, WizardSyncOffset, }; use super::graphops; use super::nodegraph::GraphEndpoint; @@ -1001,7 +1001,7 @@ impl MockEngine { 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; + node.position += *delta; } } } @@ -2099,8 +2099,7 @@ impl AppEngine for MockEngine { .is_some_and(|t| t.kind == TrackKind::Video) { track_index - } else if let Some(index) = self.tracks.iter().position(|t| t.kind == TrackKind::Video) - { + } else if let Some(index) = self.tracks.iter().position(|t| t.kind == TrackKind::Video) { index } else { return Err("no video track".to_string()); @@ -2494,7 +2493,7 @@ impl AppEngine for MockEngine { if entry.is_dir { continue; } - let is_audio = crate::oakui::filename_is_audio(&entry.name.to_string()); + let is_audio = crate::oakui::filename_is_audio(entry.name.as_ref()); let state = self .proxy_states .get(&entry.id) @@ -2916,7 +2915,11 @@ impl ProjectDataSource for MockEngine { // shared node label `文本`, so the demo's browser stays locale- // independent). for &id in &self.text_footages { - roots.push(ProjectEntry::new(id, super::graphops::TEXT_FOOTAGE_LABEL, false)); + roots.push(ProjectEntry::new( + id, + super::graphops::TEXT_FOOTAGE_LABEL, + false, + )); } roots } @@ -2969,11 +2972,11 @@ impl DemoMulticamGraph { /// tracks are built directly in the graph (no `Add Track` undo entries — /// the demo's initial state is not a user edit). fn build() -> Self { - use oak_node::node::NodeCore; - use oak_node::sequence::SequenceBehavior; - use oak_node::track::{TrackBehavior, TrackListBehavior}; + use oak_node::node::NodeCore; + use oak_node::sequence::SequenceBehavior; + use oak_node::track::{TrackBehavior, TrackListBehavior}; - let project = graphops::create_project(); + let project = graphops::create_project(); let sequence = graphops::create_sequence(&project, "Multicam Demo"); // A video track list with four tracks, wired into the sequence. { @@ -3257,12 +3260,12 @@ impl MockEngine { #[cfg(test)] mod tests { - use super::*; - use gpui::TestAppContext; - use gpui::timeline::TrimEdge; + use super::*; + use gpui::timeline::TrimEdge; + use gpui::TestAppContext; - fn demo_engine(app: &mut gpui::App) -> Entity { - app.new(|cx| MockEngine::demo(cx)) + fn demo_engine(app: &mut gpui::App) -> Entity { + app.new(MockEngine::demo) } #[gpui::test] @@ -3327,8 +3330,8 @@ mod tests { /// The stop-on-last tick pauses on the final frame instead of wrapping. #[test] fn clock_tick_stops_on_the_last_frame_when_asked() { - use std::time::{Duration, Instant}; - let mut clock = MockClock::new(FrameRate::new(30, 1)); + use std::time::{Duration, Instant}; + let mut clock = MockClock::new(FrameRate::new(30, 1)); clock.play(); // 10 s at 30 fps = 300 frames into a 5-frame sequence: wrapped 60×. clock.started = Some((Instant::now() - Duration::from_secs(10), Frame(0))); @@ -3348,8 +3351,8 @@ mod tests { /// Without stop-on-last the tick wraps modulo the sequence length. #[test] fn clock_tick_loops_when_not_stopping() { - use std::time::{Duration, Instant}; - let mut clock = MockClock::new(FrameRate::new(30, 1)); + use std::time::{Duration, Instant}; + let mut clock = MockClock::new(FrameRate::new(30, 1)); clock.play(); clock.started = Some((Instant::now() - Duration::from_secs(10), Frame(0))); @@ -3973,9 +3976,9 @@ mod tests { .collect(); assert_eq!(listed.len(), 2, "both text entries are listed in the bin"); assert!( - listed.iter().all(|e| { - e.name == crate::oakui::graphops::TEXT_FOOTAGE_LABEL && !e.is_dir - }), + listed + .iter() + .all(|e| { e.name == crate::oakui::graphops::TEXT_FOOTAGE_LABEL && !e.is_dir }), "the entries carry the shared 文本 label as leaves" ); }); diff --git a/crates/oak-app/src/oakui/ofx.rs b/crates/oak-app/src/oakui/ofx.rs index 567385a0d..4a757036e 100644 --- a/crates/oak-app/src/oakui/ofx.rs +++ b/crates/oak-app/src/oakui/ofx.rs @@ -309,7 +309,9 @@ fn active_interact_slot() -> &'static Mutex> { pub fn sync_active_interact(instance: Option) { // Unchanged target: nothing to do. { - let mut slot = active_interact_slot().lock().unwrap_or_else(|e| e.into_inner()); + let mut slot = active_interact_slot() + .lock() + .unwrap_or_else(|e| e.into_inner()); if slot .as_ref() .is_some_and(|active| Some(active.instance) == instance) @@ -340,7 +342,9 @@ pub fn sync_active_interact(instance: Option) { // Describe → CreateInstance before any draw/pen/key action). interact.describe(); interact.create_instance(); - let mut slot = active_interact_slot().lock().unwrap_or_else(|e| e.into_inner()); + let mut slot = active_interact_slot() + .lock() + .unwrap_or_else(|e| e.into_inner()); *slot = Some(ActiveInteract { instance: id, interact, @@ -357,14 +361,19 @@ pub fn sync_active_interact(instance: Option) { /// interact). The viewport reports the last drawn size and updates as the /// viewer's frame size changes. pub fn active_interact() -> Option<(u64, Arc, InteractViewport)> { - let slot = active_interact_slot().lock().unwrap_or_else(|e| e.into_inner()); - slot.as_ref().map(|a| (a.instance, a.interact.clone(), a.viewport)) + let slot = active_interact_slot() + .lock() + .unwrap_or_else(|e| e.into_inner()); + slot.as_ref() + .map(|a| (a.instance, a.interact.clone(), a.viewport)) } /// Records the viewport the interact was last drawn at (keeps /// [`active_interact`]'s viewport current). fn note_interact_viewport(instance: u64, viewport: InteractViewport) { - let mut slot = active_interact_slot().lock().unwrap_or_else(|e| e.into_inner()); + let mut slot = active_interact_slot() + .lock() + .unwrap_or_else(|e| e.into_inner()); if let Some(active) = slot.as_mut() { if active.instance == instance { active.viewport = viewport; @@ -389,7 +398,7 @@ fn bgra_image_to_f32_rgba(img: &RenderImage) -> Option<(u32, u32, Vec)> { return None; } let mut out = Vec::with_capacity(expected); - for px in bytes[..expected].chunks_exact(4) { + for px in bytes[..expected].as_chunks::<4>().0 { out.push(px[2] as f32 / 255.0); // R out.push(px[1] as f32 / 255.0); // G out.push(px[0] as f32 / 255.0); // B @@ -401,7 +410,9 @@ fn bgra_image_to_f32_rgba(img: &RenderImage) -> Option<(u32, u32, Vec)> { /// Reads an `oakplugin` F32 RGBA image into a tightly packed `Vec`. fn read_image_f32(img: &oak_plugin::image::Image) -> Vec { img.pixels() - .chunks_exact(4) + .as_chunks::<4>() + .0 + .iter() .map(|c| f32::from_ne_bytes(c[0..4].try_into().unwrap())) .collect() } @@ -423,7 +434,7 @@ fn read_image_f32(img: &oak_plugin::image::Image) -> Vec { /// straight-alpha "over". `base` is the displayed frame (alpha 1.0), so /// the result alpha stays 1.0. Returns `None` on a length mismatch. pub fn composite_overlay(overlay: &[f32], base: &[f32]) -> Option> { - if overlay.len() != base.len() || overlay.len() % 4 != 0 { + if overlay.len() != base.len() || !overlay.len().is_multiple_of(4) { return None; } let mut out = vec![0.0f32; overlay.len()]; @@ -519,7 +530,9 @@ pub fn draw_interact_composite( } let merged = composite_overlay(&read_image_f32(&overlay), &base_f32)?; note_interact_viewport(instance, *viewport); - Some(Arc::new(super::frames::f32_rgba_to_bgra_image(bw, bh, &merged))) + Some(Arc::new(super::frames::f32_rgba_to_bgra_image( + bw, bh, &merged, + ))) } // --------------------------------------------------------------------------- @@ -695,8 +708,14 @@ mod tests { let p = viewport_pixel_to_pen(point(320.0, 10.0), area, frame); assert_eq!(p, None); // Zero-size inputs. - assert_eq!(viewport_pixel_to_pen(point(0.0, 0.0), size(0.0, 0.0), frame), None); - assert_eq!(viewport_pixel_to_pen(point(0.0, 0.0), area, size(0.0, 0.0)), None); + assert_eq!( + viewport_pixel_to_pen(point(0.0, 0.0), size(0.0, 0.0), frame), + None + ); + assert_eq!( + viewport_pixel_to_pen(point(0.0, 0.0), area, size(0.0, 0.0)), + None + ); } /// Key mapping: the common keys land on the ofxKeySyms values; printable @@ -710,22 +729,49 @@ mod tests { assert_eq!(key_symbol(&ks("z")), (ofx_key::KEY_Z, "z".to_string())); assert_eq!(key_symbol(&ks("1")), (b'1' as i32, "1".to_string())); // Named keys: symbol only, empty string. - assert_eq!(key_symbol(&ks("space")), (ofx_key::KEY_SPACE, String::new())); - assert_eq!(key_symbol(&ks("enter")), (ofx_key::KEY_RETURN, String::new())); - assert_eq!(key_symbol(&ks("escape")), (ofx_key::KEY_ESCAPE, String::new())); - assert_eq!(key_symbol(&ks("backspace")), (ofx_key::KEY_BACKSPACE, String::new())); - assert_eq!(key_symbol(&ks("delete")), (ofx_key::KEY_DELETE, String::new())); + assert_eq!( + key_symbol(&ks("space")), + (ofx_key::KEY_SPACE, String::new()) + ); + assert_eq!( + key_symbol(&ks("enter")), + (ofx_key::KEY_RETURN, String::new()) + ); + assert_eq!( + key_symbol(&ks("escape")), + (ofx_key::KEY_ESCAPE, String::new()) + ); + assert_eq!( + key_symbol(&ks("backspace")), + (ofx_key::KEY_BACKSPACE, String::new()) + ); + assert_eq!( + key_symbol(&ks("delete")), + (ofx_key::KEY_DELETE, String::new()) + ); assert_eq!(key_symbol(&ks("left")), (ofx_key::KEY_LEFT, String::new())); - assert_eq!(key_symbol(&ks("right")), (ofx_key::KEY_RIGHT, String::new())); + assert_eq!( + key_symbol(&ks("right")), + (ofx_key::KEY_RIGHT, String::new()) + ); assert_eq!(key_symbol(&ks("up")), (ofx_key::KEY_UP, String::new())); assert_eq!(key_symbol(&ks("down")), (ofx_key::KEY_DOWN, String::new())); assert_eq!(key_symbol(&ks("home")), (ofx_key::KEY_HOME, String::new())); assert_eq!(key_symbol(&ks("end")), (ofx_key::KEY_END, String::new())); - assert_eq!(key_symbol(&ks("pageup")), (ofx_key::KEY_PAGE_UP, String::new())); - assert_eq!(key_symbol(&ks("pagedown")), (ofx_key::KEY_PAGE_DOWN, String::new())); + assert_eq!( + key_symbol(&ks("pageup")), + (ofx_key::KEY_PAGE_UP, String::new()) + ); + assert_eq!( + key_symbol(&ks("pagedown")), + (ofx_key::KEY_PAGE_DOWN, String::new()) + ); assert_eq!(key_symbol(&ks("tab")), (ofx_key::KEY_TAB, String::new())); assert_eq!(key_symbol(&ks("f1")), (ofx_key::KEY_F1, String::new())); - assert_eq!(key_symbol(&ks("f12")), (ofx_key::KEY_F1 + 11, String::new())); + assert_eq!( + key_symbol(&ks("f12")), + (ofx_key::KEY_F1 + 11, String::new()) + ); // Unknown multi-character keys. let (sym, s) = key_symbol(&ks("insert")); assert_eq!(sym, ofx_key::KEY_UNKNOWN); @@ -745,7 +791,11 @@ mod tests { let overlay = [1.0, 0.0, 0.0, 0.5]; let out = composite_overlay(&overlay, &base).unwrap(); for (i, expected) in [(0, 0.5), (1, 0.0), (2, 0.5), (3, 1.0)] { - assert!((out[i] - expected).abs() < 1e-6, "channel {i}: {} != {expected}", out[i]); + assert!( + (out[i] - expected).abs() < 1e-6, + "channel {i}: {} != {expected}", + out[i] + ); } // Transparent overlay → base unchanged. let overlay = [0.0, 0.0, 0.0, 0.0]; @@ -763,8 +813,21 @@ mod tests { let img = super::super::frames::f32_rgba_to_bgra_image(w, h, &samples); let (got_w, got_h, rgba) = bgra_image_to_f32_rgba(&img).unwrap(); assert_eq!((got_w, got_h), (w, h)); - for (i, expected) in [(0, 1.0), (1, 0.0), (2, 0.5), (3, 1.0), (4, 0.25), (5, 0.5), (6, 0.75), (7, 1.0)] { - assert!((rgba[i] - expected).abs() < 0.01, "channel {i}: {} != {expected}", rgba[i]); + for (i, expected) in [ + (0, 1.0), + (1, 0.0), + (2, 0.5), + (3, 1.0), + (4, 0.25), + (5, 0.5), + (6, 0.75), + (7, 1.0), + ] { + assert!( + (rgba[i] - expected).abs() < 0.01, + "channel {i}: {} != {expected}", + rgba[i] + ); } } @@ -823,7 +886,11 @@ mod tests { println!("SKIP: minimal test plugin not built"); return false; }; - if oak_plugin::host::Host::global().cache.scan_path(&dir).is_err() { + if oak_plugin::host::Host::global() + .cache + .scan_path(&dir) + .is_err() + { println!("SKIP: test plugin scan failed"); return false; } @@ -913,9 +980,7 @@ mod tests { let deadline = std::time::Instant::now() + std::time::Duration::from_secs(5); let lines = loop { let lines = read_marker(&marker); - if lines.iter().any(|l| l == "destroy") - || std::time::Instant::now() >= deadline - { + if lines.iter().any(|l| l == "destroy") || std::time::Instant::now() >= deadline { break lines; } std::thread::sleep(std::time::Duration::from_millis(10)); @@ -939,15 +1004,21 @@ mod tests { // The forwarded events with real arguments (C %g drops trailing // zeros). assert!( - lines.iter().any(|l| l == "pen_motion vp=20,20 canon=20,20 pressure=1"), + lines + .iter() + .any(|l| l == "pen_motion vp=20,20 canon=20,20 pressure=1"), "pen_motion not recorded with pen-down state: {lines:?}" ); assert!( - lines.iter().any(|l| l == "pen_down vp=20,20 canon=20,20 pressure=1"), + lines + .iter() + .any(|l| l == "pen_down vp=20,20 canon=20,20 pressure=1"), "pen_down not recorded: {lines:?}" ); assert!( - lines.iter().any(|l| l == "pen_up vp=20,20 canon=20,20 pressure=0"), + lines + .iter() + .any(|l| l == "pen_up vp=20,20 canon=20,20 pressure=0"), "pen_up not recorded: {lines:?}" ); assert!( @@ -992,7 +1063,11 @@ mod tests { for px in base_samples.chunks_exact_mut(4) { px.copy_from_slice(&[0.0, 0.0, 1.0, 1.0]); } - let base = Arc::new(super::super::frames::f32_rgba_to_bgra_image(w, h, &base_samples)); + let base = Arc::new(super::super::frames::f32_rgba_to_bgra_image( + w, + h, + &base_samples, + )); let viewport = InteractViewport::at_frame_size(w, h); let composite = draw_interact_composite(handle, &interact, &viewport, 0.0, &base) .expect("GL overlay composite"); diff --git a/crates/oak-app/src/oakui/real.rs b/crates/oak-app/src/oakui/real.rs index e4e12e4db..834703fdf 100644 --- a/crates/oak-app/src/oakui/real.rs +++ b/crates/oak-app/src/oakui/real.rs @@ -66,12 +66,12 @@ use std::sync::{Arc, Mutex}; use std::time::Instant; use gpui::effect_stack::{ - EffectCardKind, EffectData, EffectId, EffectStackDataSource, EffectStackEvent, + EffectCardKind, EffectData, EffectId, EffectStackDataSource, EffectStackEvent, }; use gpui::node_graph::{NodeGraphDataSource, NodeGraphEvent}; use gpui::timeline::{ - ClipData, ClipId, Frame, FrameRange, FrameRate, Marker, TimelineDataSource, TimelineEvent, - TrackData, TrackHeaderEvent, TrackKind, TrimEdge, + ClipData, ClipId, Frame, FrameRange, FrameRate, Marker, TimelineDataSource, TimelineEvent, + TrackData, TrackHeaderEvent, TrackKind, TrimEdge, }; use gpui::{prelude::*, px, App, Context, Entity, Hsla, Pixels, RenderImage, SharedString}; use gpui_widgets::audio_meter::AudioMeterDataSource; @@ -86,8 +86,8 @@ use oak_timeline::handle::CHandle; use oak_timeline::util::NodeRef; use super::engine::{ - AppEngine, EngineGateway, ExportSession, LibraryProject, Monitor, MulticamState, Project, - ScopeData, Sequence, SequenceParameters, VideoFormat, WizardFootage, WizardSyncOffset, + AppEngine, EngineGateway, ExportSession, LibraryProject, Monitor, MulticamState, Project, + ScopeData, Sequence, SequenceParameters, VideoFormat, WizardFootage, WizardSyncOffset, }; use super::frames::{bgra_bytes_to_render_image, f32_rgba_to_bgra_image, synthetic_frame_samples}; use super::graphops::{self, ProjectRef}; @@ -422,7 +422,10 @@ impl AudioPrefetch { let ts = *ts; let (_, data) = self.buffered.pop_front().unwrap(); if ts >= playhead { - audio_dbg(&format!("push chunk {ts} (lead {} vs playhead {playhead})", ts - playhead)); + audio_dbg(&format!( + "push chunk {ts} (lead {} vs playhead {playhead})", + ts - playhead + )); out.push(data); } else { audio_dbg(&format!( @@ -449,7 +452,10 @@ pub(crate) fn audio_dbg(msg: &str) { return; } static T0: std::sync::OnceLock = std::sync::OnceLock::new(); - let ms = T0.get_or_init(std::time::Instant::now).elapsed().as_millis(); + let ms = T0 + .get_or_init(std::time::Instant::now) + .elapsed() + .as_millis(); eprintln!("[audio {ms:>8}ms] {msg}"); } @@ -674,7 +680,7 @@ fn read_f32_frame(frame: &super::renderops::RenderedFrame) -> Option<(u32, u32, let mut samples = vec![0.0f32; (*width * *height * 4) as usize]; for y in 0..*height as usize { let row = &data[y * linesize..y * linesize + row_bytes]; - for (i, px) in row.chunks_exact(4).enumerate() { + for (i, px) in row.as_chunks::<4>().0.iter().enumerate() { let v = f32::from_ne_bytes([px[0], px[1], px[2], px[3]]); samples[y * (*width as usize) * 4 + i] = v; } @@ -712,7 +718,8 @@ fn rendered_to_owned_image(rendered: &super::renderops::RenderedFrame) -> Option // register the RGBA16F texture for the viewer; the BGRA8 // image below is only the CPU fallback (scope / eyedropper / // cached fills without a GPU). - let mut samples = repack_f32_row_bytes(meta.width, meta.height, meta.linesize, data)?; + let mut samples = + repack_f32_row_bytes(meta.width, meta.height, meta.linesize, data)?; apply_output_node_f32(&mut samples); super::displaycolor::apply_f32_rgba(&mut samples, (w * h) as i64); let image = f32_rgba_to_bgra_image(w, h, &samples); @@ -760,7 +767,9 @@ fn samples_from_cpu_frame(frame: &oak_core::texture::Frame) -> Option<(u32, u32, let mut samples = vec![0.0f32; w * h * 4]; for y in 0..h { for (i, px) in frame.data[y * stride..y * stride + w * 16] - .chunks_exact(16) + .as_chunks::<16>() + .0 + .iter() .enumerate() { for c in 0..4 { @@ -776,9 +785,9 @@ fn samples_from_cpu_frame(frame: &oak_core::texture::Frame) -> Option<(u32, u32, /// project's output colorspace); pass-through in the legacy working space. fn apply_output_node_f32(samples: &mut [f32]) { oak_core::colormath::working_to_display_target( - samples, - oak_core::color::pipeline_working_space(), - oak_core::color::pipeline_output_spec(), + samples, + oak_core::color::pipeline_working_space(), + oak_core::color::pipeline_output_spec(), ); } @@ -796,7 +805,7 @@ fn repack_f32_row_bytes(width: i32, height: i32, linesize: i32, data: &[u8]) -> let mut samples = vec![0.0f32; (width * height * 4) as usize]; for y in 0..height as usize { let row = &data[y * linesize..y * linesize + row_bytes]; - for (i, px) in row.chunks_exact(4).enumerate() { + for (i, px) in row.as_chunks::<4>().0.iter().enumerate() { let v = f32::from_ne_bytes([px[0], px[1], px[2], px[3]]); samples[y * (width as usize) * 4 + i] = v; } @@ -1355,12 +1364,12 @@ impl RealEngine { // footage as the re-point target. let target_footage = { let g = graphops::lock(&project); - let mc = g - .graph - .connected_output(clip, oak_node::block::clip_input::TEXTURE_INPUT, -1)?; - let seq = g - .graph - .connected_output(mc, oak_node::nodes::multicamnode::SEQUENCE_INPUT, -1)?; + let mc = + g.graph + .connected_output(clip, oak_node::block::clip_input::TEXTURE_INPUT, -1)?; + let seq = + g.graph + .connected_output(mc, oak_node::nodes::multicamnode::SEQUENCE_INPUT, -1)?; let list = graphops::track_list_of(&g.graph, seq, TrackType::Audio)?; let list_behavior = graphops::track_list_behavior(&g.graph, list)?; let audio_track = list_behavior.tracks.get(source as usize)?; @@ -1382,18 +1391,15 @@ impl RealEngine { let mut g = graphops::lock(&p_undo); for (from, input, element) in g.graph.input_connections(aclip) { if input.as_str() == oak_node::block::clip_input::TEXTURE_INPUT { - let _ = g.graph.disconnect(from, aclip, &input, element); + g.graph.disconnect(from, aclip, &input, element); } } }, move || { let mut g = graphops::lock(&p_redo); - let _ = g.graph.connect( - old, - aclip, - oak_node::block::clip_input::TEXTURE_INPUT, - -1, - ); + let _ = g + .graph + .connect(old, aclip, oak_node::block::clip_input::TEXTURE_INPUT, -1); }, ); let p_connect = project.clone(); @@ -1402,7 +1408,7 @@ impl RealEngine { let mut g = graphops::lock(&p_connect); for (from, input, element) in g.graph.input_connections(aclip) { if input.as_str() == oak_node::block::clip_input::TEXTURE_INPUT { - let _ = g.graph.disconnect(from, aclip, &input, element); + g.graph.disconnect(from, aclip, &input, element); } } let _ = g.graph.connect( @@ -1418,15 +1424,12 @@ impl RealEngine { let mut g = graphops::lock(&p); for (from, input, element) in g.graph.input_connections(aclip) { if input.as_str() == oak_node::block::clip_input::TEXTURE_INPUT { - let _ = g.graph.disconnect(from, aclip, &input, element); + g.graph.disconnect(from, aclip, &input, element); } } - let _ = g.graph.connect( - old, - aclip, - oak_node::block::clip_input::TEXTURE_INPUT, - -1, - ); + let _ = + g.graph + .connect(old, aclip, oak_node::block::clip_input::TEXTURE_INPUT, -1); } }, ); @@ -1794,7 +1797,12 @@ impl RealEngine { *slot = RendererSlot::Ready; let (image, scope, samples) = rendered.to_display()?; if let Some(samples) = samples { - super::gpu::register_display_frame(image.id.0, width as u32, height as u32, &samples); + super::gpu::register_display_frame( + image.id.0, + width as u32, + height as u32, + &samples, + ); } release_rendered_frame(&rendered); Some((image, scope)) @@ -1849,7 +1857,12 @@ impl RealEngine { *slot = RendererSlot::Ready; let (image, scope, samples) = rendered.to_display()?; if let Some(samples) = samples { - super::gpu::register_display_frame(image.id.0, width as u32, height as u32, &samples); + super::gpu::register_display_frame( + image.id.0, + width as u32, + height as u32, + &samples, + ); } release_rendered_frame(&rendered); Some((image, scope)) @@ -2294,10 +2307,22 @@ impl RealEngine { for frame in new_frames { let params = match monitor { Monitor::Program => super::renderops::sequence_frame_params( - &project, node, frame, tb, width, height, Some(oak_core::PixelFormat::F32), + &project, + node, + frame, + tb, + width, + height, + Some(oak_core::PixelFormat::F32), ), Monitor::Source => super::renderops::footage_frame_params( - &project, node, frame, tb, width, height, Some(oak_core::PixelFormat::F32), + &project, + node, + frame, + tb, + width, + height, + Some(oak_core::PixelFormat::F32), ), }; let Ok(params) = params else { continue }; @@ -2422,7 +2447,12 @@ impl RealEngine { let (image, scope, samples) = super::renderops::RenderedFrame::Gpu(texture).to_display()?; if let Some(samples) = samples { - super::gpu::register_display_frame(image.id.0, w.max(0) as u32, h.max(0) as u32, &samples); + super::gpu::register_display_frame( + image.id.0, + w.max(0) as u32, + h.max(0) as u32, + &samples, + ); } Some((Arc::new(image), scope)) } @@ -2502,8 +2532,8 @@ impl RealEngine { /// mapped onto the widget's badge enum; folders and footage without /// proxy state get none. fn proxy_badge_of(&self, id: u64) -> Option { - use gpui_widgets::project_explorer::ProxyBadge; - let project = self.project.as_ref()?; + use gpui_widgets::project_explorer::ProxyBadge; + let project = self.project.as_ref()?; let node = graphops::id_of(id)?; let guard = graphops::lock(project); let f = graphops::footage_behavior(&guard.graph, node)?; @@ -2839,8 +2869,8 @@ impl RealEngine { f: &oak_node::footage::FootageBehavior, node: NodeId, ) -> super::engine::ProxyMediaState { - use super::engine::ProxyMediaState; - if self.proxy_runs.iter().any(|run| run.footage == node) { + use super::engine::ProxyMediaState; + if self.proxy_runs.iter().any(|run| run.footage == node) { return ProxyMediaState::Generating; } if f.proxy.is_empty() { @@ -2866,9 +2896,9 @@ impl RealEngine { /// every clip is re-placed so its source head lines up with the /// reference's at the anchor (one multi-undo). fn sync_clips_by_source_time_internal(&mut self, clips: &[ClipId]) { - use oak_audio::synchronizer::{place_by_source_time, SourceClip}; + use oak_audio::synchronizer::{place_by_source_time, SourceClip}; - let Some(project) = self.project.clone() else { + let Some(project) = self.project.clone() else { return; }; @@ -2990,12 +3020,12 @@ impl RealEngine { /// offset triggers a rate search whose winner also rescales the clip /// speed (one multi-undo). fn sync_clips_by_waveform_internal(&mut self, clips: &[ClipId], allow_speed: bool) { - use oak_audio::synchronizer::place_by_waveform_offset; - use oak_audio::waveformsync::{ - estimate_envelope_offset_valid, estimate_stretch_and_offset, - }; + use oak_audio::synchronizer::place_by_waveform_offset; + use oak_audio::waveformsync::{ + estimate_envelope_offset_valid, estimate_stretch_and_offset, + }; - let Some(cache) = self.waveform_cache() else { + let Some(cache) = self.waveform_cache() else { return; }; let Some(project) = self.project.clone() else { @@ -3591,8 +3621,7 @@ impl RealEngine { } else { entry.core.label.clone() }; - let multicam = - super::multicam::clip_is_multicam(graph, block); + let multicam = super::multicam::clip_is_multicam(graph, block); Some(RealClip { id: ClipId(block.identity()), range: FrameRange::new(Frame(to_ts(in_r)), Frame(to_ts(out_r))), @@ -3981,8 +4010,7 @@ impl EngineGateway for RealEngine { if let Some(manager) = oak_audio::manager::instance() { if manager.seconds(&mut secs).is_ok() && secs > start_secs { let rate = self.program_clock.read(cx).frame_rate(); - let mut target = - audio_target_frame(start_frame, start_secs, secs, rate); + let mut target = audio_target_frame(start_frame, start_secs, secs, rate); if length.0 > 0 && target >= length.0 { target %= length.0; } @@ -4648,10 +4676,10 @@ impl AppEngine for RealEngine { // inspector shows that effect's params). let prev = self.selected_graph_node; self.selected_graph_node = (nodes.len() == 1).then(|| { - (*nodes + nodes .iter() .next() - .expect("a one-element set always yields an item")) + .expect("a one-element set always yields an item") .0 }); if prev != self.selected_graph_node { @@ -5293,10 +5321,7 @@ impl AppEngine for RealEngine { if !guard.graph.is_valid(node) { return None; } - guard - .graph - .get(node) - .map(|e| e.core.label.clone().into()) + guard.graph.get(node).map(|e| e.core.label.clone().into()) } fn replace_footage( @@ -5490,12 +5515,18 @@ impl AppEngine for RealEngine { // rearrange afterwards. let audio_target = loop { let video_number = self.tracks[video_target].track_index; - if let Some(index) = self.tracks.iter().position(|t| { - t.kind == TrackKind::Audio && t.track_index == video_number - }) { + if let Some(index) = self + .tracks + .iter() + .position(|t| t.kind == TrackKind::Audio && t.track_index == video_number) + { break index; } - let audio_count = self.tracks.iter().filter(|t| t.kind == TrackKind::Audio).count(); + let audio_count = self + .tracks + .iter() + .filter(|t| t.kind == TrackKind::Audio) + .count(); if audio_count > video_number { // Same-numbered audio tracks have run out without a // hit (should not happen: counting is contiguous). @@ -5507,7 +5538,11 @@ impl AppEngine for RealEngine { return; } self.add_track(TrackKind::Audio, cx); - let after = self.tracks.iter().filter(|t| t.kind == TrackKind::Audio).count(); + let after = self + .tracks + .iter() + .filter(|t| t.kind == TrackKind::Audio) + .count(); if after == audio_count { // The track add failed: give up this drop. println!( @@ -5538,8 +5573,8 @@ impl AppEngine for RealEngine { seq, footage, &[ - (TrackType::Video, video_index as usize), - (TrackType::Audio, audio_index as usize), + (TrackType::Video, video_index), + (TrackType::Audio, audio_index), ], in_ts, in_ts + length, @@ -5801,9 +5836,15 @@ impl AppEngine for RealEngine { fn project_color_settings(&self) -> (String, String, String) { let Some(project) = self.project_ref() else { return ( - oak_core::colormath::WorkingColorSpace::default().as_setting().to_string(), - oak_core::colormath::OutputGamut::default().as_setting().to_string(), - oak_core::colormath::OutputTransfer::default().as_setting().to_string(), + oak_core::colormath::WorkingColorSpace::default() + .as_setting() + .to_string(), + oak_core::colormath::OutputGamut::default() + .as_setting() + .to_string(), + oak_core::colormath::OutputTransfer::default() + .as_setting() + .to_string(), ); }; let guard = graphops::lock(project); @@ -5817,16 +5858,16 @@ impl AppEngine for RealEngine { }; ( get( - oak_node::project::SETTING_WORKING_COLOR_SPACE, - oak_core::colormath::WorkingColorSpace::default().as_setting(), + oak_node::project::SETTING_WORKING_COLOR_SPACE, + oak_core::colormath::WorkingColorSpace::default().as_setting(), ), get( - oak_node::project::SETTING_OUTPUT_GAMUT, - oak_core::colormath::OutputGamut::default().as_setting(), + oak_node::project::SETTING_OUTPUT_GAMUT, + oak_core::colormath::OutputGamut::default().as_setting(), ), get( - oak_node::project::SETTING_OUTPUT_TRANSFER, - oak_core::colormath::OutputTransfer::default().as_setting(), + oak_node::project::SETTING_OUTPUT_TRANSFER, + oak_core::colormath::OutputTransfer::default().as_setting(), ), ) } @@ -5933,9 +5974,9 @@ impl AppEngine for RealEngine { .video_params .first() .copied(); - let (width, height, interlaced) = match params { - Some(v) => (v.width.max(1) as u32, v.height.max(1) as u32, v.interlaced), - None => return None, + let (width, height, interlaced) = { + let v = params?; + (v.width.max(1) as u32, v.height.max(1) as u32, v.interlaced) }; Some(SequenceParameters { name: graphops::node_label(&guard.graph, node), @@ -6026,7 +6067,10 @@ impl AppEngine for RealEngine { let (core, behavior) = oak_node::factory::Factory::global() .create_any(type_id) .ok_or_else(|| format!("unknown effect \"{type_id}\""))?; - if !behavior.categories().contains(&oak_node::node::Category::Generator) { + if !behavior + .categories() + .contains(&oak_node::node::Category::Generator) + { return Err(format!("effect \"{type_id}\" is not a generator")); } let generator = { @@ -6039,8 +6083,7 @@ impl AppEngine for RealEngine { .is_some_and(|t| t.kind == TrackKind::Video) { track_index - } else if let Some(index) = self.tracks.iter().position(|t| t.kind == TrackKind::Video) - { + } else if let Some(index) = self.tracks.iter().position(|t| t.kind == TrackKind::Video) { index } else { return Err("no video track".to_string()); @@ -6087,8 +6130,7 @@ impl AppEngine for RealEngine { .is_some_and(|t| t.kind == TrackKind::Video) { track_index - } else if let Some(index) = self.tracks.iter().position(|t| t.kind == TrackKind::Video) - { + } else if let Some(index) = self.tracks.iter().position(|t| t.kind == TrackKind::Video) { index } else { return Err("no video track".to_string()); @@ -6125,7 +6167,11 @@ impl AppEngine for RealEngine { continue; }; for (edge, start_edge) in [(in_r, true), (out_r, false)] { - let dist = if edge > time_r { edge - time_r } else { time_r - edge }; + let dist = if edge > time_r { + edge - time_r + } else { + time_r - edge + }; if best.as_ref().is_none_or(|(d, ..)| dist < *d) { best = Some((dist, clip, start_edge)); } @@ -6144,7 +6190,10 @@ impl AppEngine for RealEngine { .map(|t| t.blocks.clone()) .unwrap_or_default(); let index = blocks.iter().position(|&b| b == clip); - let prev = index.and_then(|i| i.checked_sub(1)).and_then(|i| blocks.get(i)).copied(); + let prev = index + .and_then(|i| i.checked_sub(1)) + .and_then(|i| blocks.get(i)) + .copied(); let next = index.and_then(|i| blocks.get(i + 1)).copied(); let prev_touch = prev.is_some_and(|b| { graphops::clip_behavior(&guard.graph, b).is_some() @@ -6703,7 +6752,11 @@ impl AppEngine for RealEngine { return Vec::new(); }; let mut out = Vec::new(); - collect(project, crate::oakui::projectbrowser::roots(project), &mut out); + collect( + project, + crate::oakui::projectbrowser::roots(project), + &mut out, + ); out } @@ -6958,7 +7011,7 @@ impl AppEngine for RealEngine { fn multicam_wizard_footage(&self) -> Option> { let project = self.project_ref()?; - Some(super::multicam::wizard_footage(&project)) + Some(super::multicam::wizard_footage(project)) } fn multicam_wizard_sync_offsets( @@ -6981,7 +7034,7 @@ impl AppEngine for RealEngine { // reference angle is angle 0. let source_paths: Vec<(u64, String, i32)> = { let project = self.project_ref().ok_or("no project open")?; - let g = graphops::lock(&project); + let g = graphops::lock(project); let mut out = Vec::new(); for entry in selected { let node = graphops::id_of(entry.id).ok_or("angle missing")?; @@ -7005,10 +7058,8 @@ impl AppEngine for RealEngine { envelopes.push(envelope); } let reference_envelope = envelopes[0].clone(); - let offsets = super::multicam::estimate_wizard_offsets( - &reference_envelope, - &envelopes[1..], - ); + let offsets = + super::multicam::estimate_wizard_offsets(&reference_envelope, &envelopes[1..]); let mut out = vec![WizardSyncOffset { footage: reference.id, offset_s: 0.0, @@ -7041,12 +7092,8 @@ impl AppEngine for RealEngine { }; // Build the source sequence (one clip per angle on its own track, // offset by the sync result) plus the multicam node over it. - let (source_seq, mc_node) = super::multicam::build_multicam_sequence( - &project, - &selected, - &offsets, - &name, - )?; + let (source_seq, mc_node) = + super::multicam::build_multicam_sequence(&project, &selected, &offsets, &name)?; // Mount the source sequence under the root folder (non-undoable, // like `create_sequence` does). let root = graphops::lock(&project).root; @@ -7094,7 +7141,7 @@ impl AppEngine for RealEngine { // wired footage->clip). for (from, input, element) in g.graph.input_connections(host_id) { if input.as_str() == oak_node::block::clip_input::TEXTURE_INPUT { - let _ = g.graph.disconnect(from, host_id, &input, element); + g.graph.disconnect(from, host_id, &input, element); } } g.graph @@ -7121,9 +7168,7 @@ impl AppEngine for RealEngine { // linked both ways); its source starts at angle 0's audio // and follows every switch. Muting the host audio track // disables the follow-through audio. - if let Err(e) = - self.ensure_host_afv_audio(&project, host_id, &selected, cx) - { + if let Err(e) = self.ensure_host_afv_audio(&project, host_id, &selected, cx) { println!("[real engine] AFV audio placement skipped: {e}"); } self.apply_edit(Ok(()), "create multicam sequence", cx); @@ -7188,9 +7233,11 @@ impl RealEngine { let out_r = graphops::clip_range(&g.graph, host_clip) .ok_or_else(|| "host clip has no range".to_string())? .1; - let tb = graphops::sequence_time_base(&g.graph, host_seq) - .unwrap_or((1, 25)); - (graphops::rational_to_ts(in_r, tb), graphops::rational_to_ts(out_r, tb)) + let tb = graphops::sequence_time_base(&g.graph, host_seq).unwrap_or((1, 25)); + ( + graphops::rational_to_ts(in_r, tb), + graphops::rational_to_ts(out_r, tb), + ) }; let audio_clip = graphops::place_footage_clip( project, @@ -7232,8 +7279,7 @@ impl RealEngine { .is_some_and(|t| t.kind == TrackKind::Video) { track_index - } else if let Some(index) = self.tracks.iter().position(|t| t.kind == TrackKind::Video) - { + } else if let Some(index) = self.tracks.iter().position(|t| t.kind == TrackKind::Video) { index } else { println!("[real engine] drop text: no video track"); @@ -7291,8 +7337,7 @@ impl RealEngine { .is_some_and(|t| t.kind == TrackKind::Video) { track_index - } else if let Some(index) = self.tracks.iter().position(|t| t.kind == TrackKind::Video) - { + } else if let Some(index) = self.tracks.iter().position(|t| t.kind == TrackKind::Video) { index } else { println!("[real engine] drop sequence: no video track"); @@ -7350,16 +7395,11 @@ impl RealEngine { let mut g = graphops::lock(project); for (from, input, element) in g.graph.input_connections(placed) { if input.as_str() == oak_node::block::clip_input::TEXTURE_INPUT { - let _ = g.graph.disconnect(from, placed, &input, element); + g.graph.disconnect(from, placed, &input, element); } } g.graph - .connect( - mc, - placed, - oak_node::block::clip_input::TEXTURE_INPUT, - -1, - ) + .connect(mc, placed, oak_node::block::clip_input::TEXTURE_INPUT, -1) .map_err(|e| format!("connect multicam to dropped clip: {e:?}")) }; self.apply_edit(result, "drop sequence (multicam)", cx); @@ -7440,13 +7480,12 @@ impl RealEngine { let Some(angle) = graphops::id_of(angle.id) else { return Err("the angle footage is not in the project".to_string()); }; - let video_target = if let Some(index) = - self.tracks.iter().position(|t| t.kind == TrackKind::Video) - { - index - } else { - return Err("no video track on the host sequence".to_string()); - }; + let video_target = + if let Some(index) = self.tracks.iter().position(|t| t.kind == TrackKind::Video) { + index + } else { + return Err("no video track on the host sequence".to_string()); + }; let video_index = self.tracks[video_target].track_index; let (length, in_ts) = { let tb = self.frame_rate(); @@ -7917,7 +7956,7 @@ pub fn set_audio_input_device(name: &str) { /// enumeration (empty when the configured device is gone). pub fn audio_output_device() -> String { let name = config_get_string(CONFIG_KEY_AUDIO_OUTPUT); - if name.is_empty() || audio_output_devices().iter().any(|n| *n == name) { + if name.is_empty() || audio_output_devices().contains(&name) { name } else { String::new() @@ -7927,7 +7966,7 @@ pub fn audio_output_device() -> String { /// The configured input device name (see [`audio_output_device`]). pub fn audio_input_device() -> String { let name = config_get_string(CONFIG_KEY_AUDIO_INPUT); - if name.is_empty() || audio_input_devices().iter().any(|n| *n == name) { + if name.is_empty() || audio_input_devices().contains(&name) { name } else { String::new() @@ -7985,11 +8024,11 @@ pub fn library_list() -> Result, String> { #[cfg(test)] mod tests { - use super::*; - use std::sync::mpsc as std_mpsc; - use std::time::Duration; + use super::*; + use std::sync::mpsc as std_mpsc; + use std::time::Duration; - /// Serializes the media/FFmpeg-heavy tests (the codec library is not + /// Serializes the media/FFmpeg-heavy tests (the codec library is not /// thread-safe against concurrent decode sessions) and shares the /// process-global undo stack with the other app test modules. fn media_lock() -> std::sync::MutexGuard<'static, ()> { @@ -8291,8 +8330,9 @@ mod tests { // The app's proxy size: sequence aspect (default 1920x1080) scaled // to a 480px long edge. - let frame = crate::oakui::renderops::render_sequence_frame(&project, seq, 0, tb, 480, 270, None) - .expect("render_frame must produce a frame"); + let frame = + crate::oakui::renderops::render_sequence_frame(&project, seq, 0, tb, 480, 270, None) + .expect("render_frame must produce a frame"); assert_eq!((frame.width(), frame.height()), (480, 270)); assert!( frame.is_shm(), @@ -8320,8 +8360,9 @@ mod tests { // Clip covering [0, 10) frames at the sequence's rate. graphops::place_footage_clip(&project, seq, footage, TrackType::Video, 0, 0, 10, 0) .expect("clip placement"); - let frame = crate::oakui::renderops::render_sequence_frame(&project, seq, 0, tb, 480, 270, None) - .expect("render_frame with a clip must produce a frame"); + let frame = + crate::oakui::renderops::render_sequence_frame(&project, seq, 0, tb, 480, 270, None) + .expect("render_frame with a clip must produce a frame"); let (image, _scope, _samples) = frame.to_display().expect("display image from the slot"); let bytes = image.as_bytes(0).expect("one frame"); let nonzero = bytes @@ -8343,13 +8384,15 @@ mod tests { release_rendered_frame(&frame); // A second frame at a later timestamp renders too. - assert!( - crate::oakui::renderops::render_sequence_frame(&project, seq, 30, tb, 480, 270, None).is_ok() - ); + assert!(crate::oakui::renderops::render_sequence_frame( + &project, seq, 30, tb, 480, 270, None + ) + .is_ok()); // Invalid geometry is rejected. assert!( - crate::oakui::renderops::render_sequence_frame(&project, seq, 0, tb, 0, 270, None).is_err() + crate::oakui::renderops::render_sequence_frame(&project, seq, 0, tb, 0, 270, None) + .is_err() ); oak_undo::global::clear().unwrap(); @@ -8367,11 +8410,11 @@ mod tests { fn process_backend_preview_path_is_zero_copy() { let _media = media_lock(); let _worker = WorkerBinGuard::set(); - use oak_render::manager::{RenderBackendChoice, RenderManager}; - use oak_render::procpool::{ - main_heap_frame_copies, reset_main_heap_frame_copies, DispatcherConfig, - }; - RenderManager::shutdown(); + use oak_render::manager::{RenderBackendChoice, RenderManager}; + use oak_render::procpool::{ + main_heap_frame_copies, reset_main_heap_frame_copies, DispatcherConfig, + }; + RenderManager::shutdown(); let config = DispatcherConfig { worker_bin: Some( std::path::Path::new(env!("CARGO_MANIFEST_DIR")) @@ -8392,8 +8435,9 @@ mod tests { let tb = graphops::sequence_time_base(&graphops::lock(&project).graph, seq).unwrap(); reset_main_heap_frame_copies(); - let frame = crate::oakui::renderops::render_sequence_frame(&project, seq, 0, tb, 64, 64, None) - .expect("render_frame must produce a frame"); + let frame = + crate::oakui::renderops::render_sequence_frame(&project, seq, 0, tb, 64, 64, None) + .expect("render_frame must produce a frame"); let crate::oakui::renderops::RenderedFrame::Shm(slot) = &frame else { panic!("the process backend must deliver a shm slot"); }; @@ -8409,8 +8453,9 @@ mod tests { assert_eq!(main_heap_frame_copies(), 0); // The long-lived full-res path is the one counted copy. - let frame = crate::oakui::renderops::render_sequence_frame(&project, seq, 0, tb, 64, 64, None) - .expect("render_frame must produce a frame"); + let frame = + crate::oakui::renderops::render_sequence_frame(&project, seq, 0, tb, 64, 64, None) + .expect("render_frame must produce a frame"); let image = rendered_to_owned_image(&frame).expect("owned full-res image"); assert_eq!(image.as_bytes(0).expect("one frame").len(), 64 * 64 * 4); assert_eq!( @@ -8467,7 +8512,7 @@ mod tests { cx: &mut gpui::TestAppContext, ) { let _media = media_lock(); - let engine = cx.update(|cx| cx.new(|cx| RealEngine::create(cx))); + let engine = cx.update(|cx| cx.new(RealEngine::create)); cx.update(|app| engine.update(app, |engine, cx| engine.new_project(cx))); let media = @@ -8508,7 +8553,7 @@ mod tests { #[gpui::test] async fn real_engine_wizard_footage_lists_imported_media(cx: &mut gpui::TestAppContext) { let _media = media_lock(); - let engine = cx.update(|cx| cx.new(|cx| RealEngine::create(cx))); + let engine = cx.update(|cx| cx.new(RealEngine::create)); cx.update(|app| engine.update(app, |engine, cx| engine.new_project(cx))); // Use the repo's committed fixture media (the same file the other @@ -8548,7 +8593,7 @@ mod tests { if !crate::oakui::renderops::ensure_render_manager() { panic!("the render manager failed to start"); } - let engine = cx.update(|cx| cx.new(|cx| RealEngine::create(cx))); + let engine = cx.update(|cx| cx.new(RealEngine::create)); cx.update(|app| engine.update(app, |engine, cx| engine.new_project(cx))); let media = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/demo.mp4"); @@ -8581,7 +8626,7 @@ mod tests { // The async path installs it: a fresh engine (no cached done entries) // spawns the worker on `roots()`, and the drain installs the completed // path into the cache so the entry re-reads with the thumbnail. - let engine = cx.update(|cx| cx.new(|cx| RealEngine::create(cx))); + let engine = cx.update(|cx| cx.new(RealEngine::create)); cx.update(|app| engine.update(app, |engine, cx| engine.new_project(cx))); cx.update(|app| engine.update(app, |engine, cx| engine.import_footage(media.clone(), cx))) .expect("re-import the footage"); @@ -8625,7 +8670,7 @@ mod tests { async fn real_engine_track_toggles_are_undoable(cx: &mut gpui::TestAppContext) { use gpui::timeline::TrackHeaderEvent; let _media = media_lock(); - let engine = cx.update(|cx| cx.new(|cx| RealEngine::create(cx))); + let engine = cx.update(|cx| cx.new(RealEngine::create)); cx.update(|app| engine.update(app, |engine, cx| engine.new_project(cx))); cx.update(|app| engine.update(app, |engine, cx| engine.add_track(TrackKind::Video, cx))); cx.update(|app| engine.update(app, |engine, cx| engine.add_track(TrackKind::Audio, cx))); @@ -8670,7 +8715,7 @@ mod tests { #[gpui::test] async fn real_engine_history_tracks_the_undo_stack(cx: &mut gpui::TestAppContext) { let _media = media_lock(); - let engine = cx.update(|cx| cx.new(|cx| RealEngine::create(cx))); + let engine = cx.update(|cx| cx.new(RealEngine::create)); cx.update(|app| engine.update(app, |engine, cx| engine.new_project(cx))); // A fresh project leaves the bottom "New/Open Project" command on @@ -8686,7 +8731,7 @@ mod tests { assert_eq!(entries.len(), base + 2); assert!(entries.iter().all(|e| e.done), "fresh rows are done"); assert!( - entries[base].name.is_empty() == false && entries[base + 1].name.is_empty() == false, + !entries[base].name.is_empty() && !entries[base + 1].name.is_empty(), "edit rows carry their command labels" ); assert_eq!( @@ -8738,7 +8783,7 @@ mod tests { async fn real_engine_multicam_switch_round_trips_through_undo(cx: &mut gpui::TestAppContext) { use oak_node::block::clip_input::TEXTURE_INPUT; let _media = media_lock(); - let engine = cx.update(|cx| cx.new(|cx| RealEngine::create(cx))); + let engine = cx.update(|cx| cx.new(RealEngine::create)); // A project whose clip is fed by a sequence (the multicam host). let clip_id = cx.update(|app| { @@ -8923,7 +8968,7 @@ mod tests { #[gpui::test] async fn selection_links_timeline_graph_and_inspector(cx: &mut gpui::TestAppContext) { let _media = media_lock(); - let engine = cx.update(|cx| cx.new(|cx| RealEngine::create(cx))); + let engine = cx.update(|cx| cx.new(RealEngine::create)); let (clip_id, effect_ident, footage_ident) = cx.update(|app| { engine.update(app, |engine, cx| { @@ -9305,7 +9350,7 @@ mod tests { async fn real_engine_fills_full_res_behind_the_proxy(cx: &mut gpui::TestAppContext) { let _media = media_lock(); let _worker = WorkerBinGuard::set(); - let engine = cx.update(|cx| cx.new(|cx| RealEngine::create(cx))); + let engine = cx.update(|cx| cx.new(RealEngine::create)); cx.update(|app| engine.update(app, |engine, cx| engine.new_project(cx))); // The sequence's native size is the fill geometry. @@ -9366,12 +9411,12 @@ mod tests { async fn effect_edit_updates_the_paused_frame(cx: &mut gpui::TestAppContext) { let _media = media_lock(); let _worker = WorkerBinGuard::set(); - let engine = cx.update(|cx| cx.new(|cx| RealEngine::create(cx))); + let engine = cx.update(|cx| cx.new(RealEngine::create)); cx.update(|app| engine.update(app, |engine, cx| engine.new_project(cx))); // A solid green clip on the timeline, selected for the stack. - let media = std::env::temp_dir() - .join(format!("oakapp_editframe_{}.mp4", std::process::id())); + let media = + std::env::temp_dir().join(format!("oakapp_editframe_{}.mp4", std::process::id())); cx.update(|app| { engine.update(app, |engine, _cx| { let project = engine.project.clone().unwrap(); @@ -9471,7 +9516,7 @@ mod tests { // GLOBAL undo stack, and this test asserts on it — running lock-free // raced a parallel test's undo history. let _media = media_lock(); - let engine = cx.update(|cx| cx.new(|cx| RealEngine::create(cx))); + let engine = cx.update(|cx| cx.new(RealEngine::create)); cx.update(|app| engine.update(app, |engine, cx| engine.new_project(cx))); let kinds: Vec = cx.read(|app| engine.read(app).tracks.iter().map(|t| t.kind).collect()); @@ -9494,7 +9539,7 @@ mod tests { #[gpui::test] async fn set_track_height_does_not_self_deadlock(cx: &mut gpui::TestAppContext) { let _media = media_lock(); - let engine = cx.update(|cx| cx.new(|cx| RealEngine::create(cx))); + let engine = cx.update(|cx| cx.new(RealEngine::create)); cx.update(|app| engine.update(app, |engine, cx| engine.new_project(cx))); cx.update(|app| engine.update(app, |engine, cx| engine.set_track_height(px(96.0), cx))); let height = cx.read(|app| engine.read(app).tracks[0].height()); @@ -9507,7 +9552,7 @@ mod tests { #[gpui::test] async fn drop_av_footage_places_linked_video_and_audio_clips(cx: &mut gpui::TestAppContext) { let _media = media_lock(); - let engine = cx.update(|cx| cx.new(|cx| RealEngine::create(cx))); + let engine = cx.update(|cx| cx.new(RealEngine::create)); cx.update(|app| engine.update(app, |engine, cx| engine.new_project(cx))); // demo.mp4: 1080p H.264 video + AAC audio (+ timecode stream). @@ -9629,7 +9674,7 @@ mod tests { #[gpui::test] async fn drop_av_footage_same_numbered_tracks(cx: &mut gpui::TestAppContext) { let _media = media_lock(); - let engine = cx.update(|cx| cx.new(|cx| RealEngine::create(cx))); + let engine = cx.update(|cx| cx.new(RealEngine::create)); cx.update(|app| engine.update(app, |engine, cx| engine.new_project(cx))); let media = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/demo.mp4"); @@ -9670,7 +9715,10 @@ mod tests { }); let (video_index, audio_index) = cx.read(|app| { let engine = engine.read(app); - (engine.tracks[video_row].track_index, engine.tracks[audio_row].track_index) + ( + engine.tracks[video_row].track_index, + engine.tracks[audio_row].track_index, + ) }); assert_eq!( video_index, 1, @@ -9690,7 +9738,7 @@ mod tests { cx: &mut gpui::TestAppContext, ) { let _media = media_lock(); - let engine = cx.update(|cx| cx.new(|cx| RealEngine::create(cx))); + let engine = cx.update(|cx| cx.new(RealEngine::create)); cx.update(|app| engine.update(app, |engine, cx| engine.new_project(cx))); let media = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/demo.mp4"); @@ -9711,9 +9759,7 @@ mod tests { .expect("imported footage is listed"); // Remove A2 (display row 3 in the default [V2, V1, A1, A2] layout). - cx.update(|app| { - engine.update(app, |engine, cx| engine.remove_track(3, cx)) - }); + cx.update(|app| engine.update(app, |engine, cx| engine.remove_track(3, cx))); // Drop onto V2 (row 0): the audio has no A2 to go to. cx.update(|app| { engine.update(app, |engine, cx| { @@ -9745,10 +9791,7 @@ mod tests { let engine = engine.read(app); engine.tracks[audio_clip_row.expect("audio clip row")].track_index }); - assert_eq!( - audio_index, 1, - "the audio clip lands on the recreated A2" - ); + assert_eq!(audio_index, 1, "the audio clip lands on the recreated A2"); } /// Dragging a clip of a linked A/V pair drags its partner in lockstep: @@ -9758,7 +9801,7 @@ mod tests { #[gpui::test] async fn moving_a_linked_clip_drags_its_partner(cx: &mut gpui::TestAppContext) { let _media = media_lock(); - let engine = cx.update(|cx| cx.new(|cx| RealEngine::create(cx))); + let engine = cx.update(|cx| cx.new(RealEngine::create)); cx.update(|app| engine.update(app, |engine, cx| engine.new_project(cx))); let media = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/demo.mp4"); @@ -9890,7 +9933,7 @@ mod tests { #[gpui::test] async fn moving_a_multi_selection_drags_the_whole_group(cx: &mut gpui::TestAppContext) { let _media = media_lock(); - let engine = cx.update(|cx| cx.new(|cx| RealEngine::create(cx))); + let engine = cx.update(|cx| cx.new(RealEngine::create)); cx.update(|app| engine.update(app, |engine, cx| engine.new_project(cx))); let media = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/demo.mp4"); @@ -9945,7 +9988,14 @@ mod tests { .map(|c| (c.range.start.0, c.id)) .collect(); audios.sort(); - (videos[0].1, videos[1].1, audios[0].1, audios[1].1, video_idx, audio_idx) + ( + videos[0].1, + videos[1].1, + audios[0].1, + audios[1].1, + video_idx, + audio_idx, + ) }); let starts = |cx: &mut gpui::TestAppContext, id: ClipId, track: usize| { cx.read(|app| { @@ -9960,7 +10010,9 @@ mod tests { // Select BOTH video clips (the audio partners stay unselected) and // drag the first one +40 frames. cx.update(|app| { - engine.update(app, |engine, cx| engine.set_selected_clips(vec![v1, v2], cx)) + engine.update(app, |engine, cx| { + engine.set_selected_clips(vec![v1, v2], cx) + }) }); cx.update(|app| { engine.update(app, |engine, cx| { @@ -9975,7 +10027,11 @@ mod tests { }) }); - assert_eq!(starts(cx, v1, video_idx), Some(80), "the dragged clip moved"); + assert_eq!( + starts(cx, v1, video_idx), + Some(80), + "the dragged clip moved" + ); assert_eq!( starts(cx, v2, video_idx), Some(640), @@ -10006,7 +10062,7 @@ mod tests { #[gpui::test] async fn cross_kind_drag_is_silently_ignored(cx: &mut gpui::TestAppContext) { let _media = media_lock(); - let engine = cx.update(|cx| cx.new(|cx| RealEngine::create(cx))); + let engine = cx.update(|cx| cx.new(RealEngine::create)); cx.update(|app| engine.update(app, |engine, cx| engine.new_project(cx))); let media = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/demo.mp4"); @@ -10119,7 +10175,7 @@ mod tests { #[gpui::test] async fn link_unlink_toggles_the_selection_links(cx: &mut gpui::TestAppContext) { let _media = media_lock(); - let engine = cx.update(|cx| cx.new(|cx| RealEngine::create(cx))); + let engine = cx.update(|cx| cx.new(RealEngine::create)); cx.update(|app| engine.update(app, |engine, cx| engine.new_project(cx))); let media = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/demo.mp4"); @@ -10245,7 +10301,7 @@ mod tests { #[gpui::test] async fn split_at_playhead_links_the_rear_halves(cx: &mut gpui::TestAppContext) { let _media = media_lock(); - let engine = cx.update(|cx| cx.new(|cx| RealEngine::create(cx))); + let engine = cx.update(|cx| cx.new(RealEngine::create)); cx.update(|app| engine.update(app, |engine, cx| engine.new_project(cx))); let media = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/demo.mp4"); cx.update(|app| { @@ -10337,7 +10393,7 @@ mod tests { async fn playback_window_supplies_playhead_frames(cx: &mut gpui::TestAppContext) { let _media = media_lock(); let _worker = WorkerBinGuard::set(); - let engine = cx.update(|cx| cx.new(|cx| RealEngine::create(cx))); + let engine = cx.update(|cx| cx.new(RealEngine::create)); cx.update(|app| engine.update(app, |engine, cx| engine.new_project(cx))); let media = @@ -10412,7 +10468,7 @@ mod tests { async fn interactive_seek_renders_without_hanging(cx: &mut gpui::TestAppContext) { let _media = media_lock(); let _worker = WorkerBinGuard::set(); - let engine = cx.update(|cx| cx.new(|cx| RealEngine::create(cx))); + let engine = cx.update(|cx| cx.new(RealEngine::create)); cx.update(|app| engine.update(app, |engine, cx| engine.new_project(cx))); let media = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/demo.mp4"); cx.update(|app| { @@ -10471,7 +10527,7 @@ mod tests { #[gpui::test] async fn save_load_roundtrips_a_timeline_clip(cx: &mut gpui::TestAppContext) { let _media = media_lock(); - let engine = cx.update(|cx| cx.new(|cx| RealEngine::create(cx))); + let engine = cx.update(|cx| cx.new(RealEngine::create)); cx.update(|app| engine.update(app, |engine, cx| engine.new_project(cx))); let media = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/demo.mp4"); cx.update(|app| { @@ -10534,7 +10590,7 @@ mod tests { async fn playback_display_tracks_the_playhead(cx: &mut gpui::TestAppContext) { let _media = media_lock(); let _worker = WorkerBinGuard::set(); - let engine = cx.update(|cx| cx.new(|cx| RealEngine::create(cx))); + let engine = cx.update(|cx| cx.new(RealEngine::create)); cx.update(|app| engine.update(app, |engine, cx| engine.new_project(cx))); let media = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/demo.mp4"); @@ -10677,10 +10733,13 @@ mod tests { st.next_submit = 40; st.insert(10, audio_chunk(10).1); let _ = st.drain_ready(10); // served cursor now 20 - // A seek far ahead (past the submitted window) resets. + // A seek far ahead (past the submitted window) resets. assert!(st.needs_reset(200)); // A backward seek past the serve-ahead lead resets too. - assert!(st.needs_reset(0), "playhead jumped back behind the served cursor"); + assert!( + st.needs_reset(0), + "playhead jumped back behind the served cursor" + ); st.reset(200, 10); assert_eq!(st.buffered.len(), 0, "old chunks dropped"); assert!(st.drain_ready(200).is_empty()); @@ -10741,7 +10800,11 @@ mod tests { assert_eq!(audio_target_frame(100, 2.0, 4.0, rate), 150); // Fractional seconds round to the nearest frame. assert_eq!(audio_target_frame(0, 0.0, 0.02, rate), 1, "0.5 s rounds up"); - assert_eq!(audio_target_frame(0, 0.0, 0.018, rate), 0, "0.45 s rounds down"); + assert_eq!( + audio_target_frame(0, 0.0, 0.018, rate), + 0, + "0.45 s rounds down" + ); // NTSC 29.97: num/den stays a rational, not rounded to 30. let ntsc = FrameRate::NTSC_2997; assert_eq!(audio_target_frame(0, 0.0, 1.0, ntsc), 30); @@ -10757,7 +10820,7 @@ mod tests { /// or headless). #[gpui::test] async fn audio_master_anchor_falls_back_to_wall_clock(cx: &mut gpui::TestAppContext) { - let engine = cx.update(|cx| cx.new(|cx| RealEngine::create(cx))); + let engine = cx.update(|cx| cx.new(RealEngine::create)); cx.update(|app| { engine.update(app, |engine, cx| { // A stale anchor (as if the first chunk was pushed). @@ -10779,7 +10842,7 @@ mod tests { /// proxy proves the non-blocking fallback. #[gpui::test] async fn paused_playhead_miss_serves_last_displayed_frame(cx: &mut gpui::TestAppContext) { - let engine = cx.update(|cx| cx.new(|cx| RealEngine::create(cx))); + let engine = cx.update(|cx| cx.new(RealEngine::create)); let (width, height, samples) = synthetic_frame_samples(Frame(0)); let scope = analyze_f32_rgba(width, height, &samples); let image = Arc::new(f32_rgba_to_bgra_image(width, height, &samples)); @@ -10818,7 +10881,7 @@ mod tests { #[gpui::test] async fn engine_creates_folder_and_sequence_with_params(cx: &mut gpui::TestAppContext) { let _media = media_lock(); - let engine = cx.update(|cx| cx.new(|cx| RealEngine::create(cx))); + let engine = cx.update(|cx| cx.new(RealEngine::create)); cx.update(|app| engine.update(app, |engine, cx| engine.new_project(cx))); let folder = cx.update(|app| { @@ -10925,7 +10988,7 @@ mod tests { cx: &mut gpui::TestAppContext, ) { let _media = media_lock(); - let engine = cx.update(|cx| cx.new(|cx| RealEngine::create(cx))); + let engine = cx.update(|cx| cx.new(RealEngine::create)); cx.update(|app| engine.update(app, |engine, cx| engine.new_project(cx))); let text = cx.update(|app| { @@ -10962,21 +11025,20 @@ mod tests { // Open a host sequence; the drop targets its first video track. let seq = cx.update(|app| { - engine - .update(app, |engine, cx| { - engine - .create_sequence_with_params( - "Text Drop".to_string(), - VideoFormat { - width: 1920, - height: 1080, - rate: FrameRate::new(30000, 1001), - }, - false, - cx, - ) - .expect("create sequence") - }) + engine.update(app, |engine, cx| { + engine + .create_sequence_with_params( + "Text Drop".to_string(), + VideoFormat { + width: 1920, + height: 1080, + rate: FrameRate::new(30000, 1001), + }, + false, + cx, + ) + .expect("create sequence") + }) }); cx.update(|app| { engine.update(app, |engine, cx| { @@ -11063,7 +11125,7 @@ mod tests { #[gpui::test] async fn engine_drops_a_generator_effect_as_a_clip(cx: &mut gpui::TestAppContext) { let _media = media_lock(); - let engine = cx.update(|cx| cx.new(|cx| RealEngine::create(cx))); + let engine = cx.update(|cx| cx.new(RealEngine::create)); cx.update(|app| engine.update(app, |engine, cx| engine.new_project(cx))); let seq = cx.update(|app| { engine.update(app, |engine, cx| { @@ -11086,12 +11148,7 @@ mod tests { cx.update(|app| { engine.update(app, |engine, cx| { engine - .drop_generator_clip( - "org.olivevideoeditor.Olive.colorbars", - 0, - Frame(25), - cx, - ) + .drop_generator_clip("org.olivevideoeditor.Olive.colorbars", 0, Frame(25), cx) .expect("drop generator clip") }) }); @@ -11156,7 +11213,7 @@ mod tests { #[gpui::test] async fn engine_drops_a_transition_at_a_clip_edge(cx: &mut gpui::TestAppContext) { let _media = media_lock(); - let engine = cx.update(|cx| cx.new(|cx| RealEngine::create(cx))); + let engine = cx.update(|cx| cx.new(RealEngine::create)); cx.update(|app| engine.update(app, |engine, cx| engine.new_project(cx))); let seq = cx.update(|app| { engine.update(app, |engine, cx| { @@ -11181,12 +11238,7 @@ mod tests { cx.update(|app| { engine.update(app, |engine, cx| { engine - .drop_generator_clip( - "org.olivevideoeditor.Olive.colorbars", - 0, - Frame(0), - cx, - ) + .drop_generator_clip("org.olivevideoeditor.Olive.colorbars", 0, Frame(0), cx) .expect("drop generator clip") }) }); @@ -11196,12 +11248,7 @@ mod tests { cx.update(|app| { engine.update(app, |engine, cx| { engine - .drop_transition_at( - "org.olivevideoeditor.Olive.transitionfx", - 0, - Frame(0), - cx, - ) + .drop_transition_at("org.olivevideoeditor.Olive.transitionfx", 0, Frame(0), cx) .expect("drop transition at the clip head") }) }); @@ -11216,9 +11263,11 @@ mod tests { let transition = graphops::transition_of_clip(&guard.graph, clips[0], true) .expect("a single-sided transition sits on the clip's head edge"); assert_eq!( - guard - .graph - .connected_output(transition, oak_node::block::transition_input::OUT_BLOCK, -1), + guard.graph.connected_output( + transition, + oak_node::block::transition_input::OUT_BLOCK, + -1 + ), None, "a head transition wires no outgoing clip" ); @@ -11233,7 +11282,7 @@ mod tests { #[gpui::test] async fn engine_adds_and_undoes_adjustment_layer(cx: &mut gpui::TestAppContext) { let _media = media_lock(); - let engine = cx.update(|cx| cx.new(|cx| RealEngine::create(cx))); + let engine = cx.update(|cx| cx.new(RealEngine::create)); cx.update(|app| engine.update(app, |engine, cx| engine.new_project(cx))); let seq = cx.update(|app| { @@ -11373,7 +11422,11 @@ mod tests { }) }); let selected = cx.read(|app| engine.read(app).selected_clip_node()); - assert_eq!(selected, Some(block), "the selected layer resolves to its block"); + assert_eq!( + selected, + Some(block), + "the selected layer resolves to its block" + ); // One undo removes the placement (and the leading gap) again. oak_undo::global::undo().unwrap(); @@ -11399,7 +11452,7 @@ mod tests { #[gpui::test] async fn engine_update_sequence_parameters_round_trips(cx: &mut gpui::TestAppContext) { let _media = media_lock(); - let engine = cx.update(|cx| cx.new(|cx| RealEngine::create(cx))); + let engine = cx.update(|cx| cx.new(RealEngine::create)); cx.update(|app| engine.update(app, |engine, cx| engine.new_project(cx))); let seq = cx.update(|app| { @@ -11454,7 +11507,7 @@ mod tests { #[gpui::test] async fn drop_footage_on_empty_timeline_auto_creates_sequence(cx: &mut gpui::TestAppContext) { let _media = media_lock(); - let engine = cx.update(|cx| cx.new(|cx| RealEngine::create(cx))); + let engine = cx.update(|cx| cx.new(RealEngine::create)); cx.update(|app| engine.update(app, |engine, cx| engine.new_project(cx))); let media = std::env::temp_dir().join(format!("oak_seq_auto_{}.mp4", std::process::id())); @@ -11535,10 +11588,11 @@ mod tests { #[gpui::test] async fn footage_video_params_returns_the_probed_stream(cx: &mut gpui::TestAppContext) { let _media = media_lock(); - let engine = cx.update(|cx| cx.new(|cx| RealEngine::create(cx))); + let engine = cx.update(|cx| cx.new(RealEngine::create)); cx.update(|app| engine.update(app, |engine, cx| engine.new_project(cx))); - let media = std::env::temp_dir().join(format!("oak_probe_params_{}.mp4", std::process::id())); + let media = + std::env::temp_dir().join(format!("oak_probe_params_{}.mp4", std::process::id())); oak_codec::testmedia::write_test_clip(&media, 64, 64, 10, 10).expect("generate"); cx.update(|app| { engine @@ -11556,14 +11610,13 @@ mod tests { }) .expect("imported footage is listed"); - let (width, height, rate_num, rate_den) = - cx.read(|app| { - let result: Option<(u32, u32, u32, u32)> = engine - .read(app) - .footage_video_params(entry.id) - .map(|(w, h, rate, _)| (w, h, rate.num, rate.den)); - result.expect("probed video stream") - }); + let (width, height, rate_num, rate_den) = cx.read(|app| { + let result: Option<(u32, u32, u32, u32)> = engine + .read(app) + .footage_video_params(entry.id) + .map(|(w, h, rate, _)| (w, h, rate.num, rate.den)); + result.expect("probed video stream") + }); // The values match the probe (not hard-coded). let expected = { let project = cx.read(|app| engine.read(app).project.clone().expect("project")); @@ -11589,11 +11642,9 @@ mod tests { /// clip becomes the multi-cam clip; switching to the source sequence /// left N camera rows on the timeline and an "未检测到多机位片段" panel). #[gpui::test] - async fn wizard_create_keeps_host_sequence_and_panel_detects( - cx: &mut gpui::TestAppContext, - ) { + async fn wizard_create_keeps_host_sequence_and_panel_detects(cx: &mut gpui::TestAppContext) { let _media = media_lock(); - let engine = cx.update(|cx| cx.new(|cx| RealEngine::create(cx))); + let engine = cx.update(|cx| cx.new(RealEngine::create)); cx.update(|app| engine.update(app, |engine, cx| engine.new_project(cx))); let media = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/demo.mp4"); @@ -11608,9 +11659,8 @@ mod tests { let name = media.file_name().unwrap().to_string_lossy(); let entry = footage.iter().find(|f| f.name.as_ref() == name).cloned(); let entry = entry.expect("the imported file is a wizard angle"); - let host_before = cx.read(|app| { - engine.read(app).current_sequence().map(|s| s.name.clone()) - }); + let host_before = + cx.read(|app| engine.read(app).current_sequence().map(|s| s.name.clone())); cx.update(|app| { engine.update(app, |engine, cx| { engine @@ -11618,15 +11668,14 @@ mod tests { .expect("create multicam") }) }); - let host_after = cx.read(|app| { - engine.read(app).current_sequence().map(|s| s.name.clone()) - }); + let host_after = cx.read(|app| engine.read(app).current_sequence().map(|s| s.name.clone())); assert_eq!( host_after, host_before, "the user's sequence stays current (no camera rows on the timeline)" ); assert_eq!( - host_after, Some("Sequence 1".to_string()), + host_after, + Some("Sequence 1".to_string()), "the fresh project's default sequence stays open" ); // The panel resolves the playhead clip to the multicam state now. @@ -11700,8 +11749,10 @@ mod tests { let Some(project) = engine.project.clone() else { return; }; - if let Some(audio_row) = - engine.tracks.iter().position(|t| t.kind == TrackKind::Audio) + if let Some(audio_row) = engine + .tracks + .iter() + .position(|t| t.kind == TrackKind::Audio) { let track = engine.tracks[audio_row].track; let _ = graphops::set_track_muted(&project, track, true); @@ -11716,7 +11767,11 @@ mod tests { .tracks .iter() .find(|t| t.kind == TrackKind::Audio) - .map(|t| graphops::track_behavior(&g.graph, t.track).map(|b| b.muted).unwrap_or(false)) + .map(|t| { + graphops::track_behavior(&g.graph, t.track) + .map(|b| b.muted) + .unwrap_or(false) + }) .unwrap_or(false) }); assert!(muted, "the AFV audio track is muted — no audio follows"); @@ -11732,7 +11787,7 @@ mod tests { cx: &mut gpui::TestAppContext, ) { let _media = media_lock(); - let engine = cx.update(|cx| cx.new(|cx| RealEngine::create(cx))); + let engine = cx.update(|cx| cx.new(RealEngine::create)); cx.update(|app| engine.update(app, |engine, cx| engine.new_project(cx))); // The wizard create (host clip placed on the default sequence). @@ -11791,7 +11846,10 @@ mod tests { .map(|e| e.id) .expect("the multicam source sequence lists in the project") }); - assert_ne!(source_entry_id, source_seq, "the return value is the host sequence id"); + assert_ne!( + source_entry_id, source_seq, + "the return value is the host sequence id" + ); cx.update(|app| { engine.update(app, |engine, cx| { engine.drop_footage(source_entry_id, TrackKind::Video, 0, Frame(80), cx) @@ -11823,20 +11881,16 @@ mod tests { }); let guard = graphops::lock(&project); let dropped_node = graphops::id_of(dropped_clip).expect("clip node"); - let src = guard - .graph - .connected_output(dropped_node, oak_node::block::clip_input::TEXTURE_INPUT, -1); - assert!( - src.is_some(), - "the dropped clip has a texture source" + let src = guard.graph.connected_output( + dropped_node, + oak_node::block::clip_input::TEXTURE_INPUT, + -1, ); + assert!(src.is_some(), "the dropped clip has a texture source"); let is_multicam = src.is_some_and(|s| { graphops::node_type_id(&guard.graph, s) == "org.olivevideoeditor.Olive.multicam" }); - assert!( - is_multicam, - "the drop rewire feeds from the multicam node" - ); + assert!(is_multicam, "the drop rewire feeds from the multicam node"); drop(guard); assert!( state.is_some(), @@ -11849,7 +11903,9 @@ mod tests { // angle's footage. NOTE: the project guard is NOT held across // `video_montage` (it locks the project itself — the same // non-recursive-mutex reentry that froze the wizard). - let seq = cx.read(|app| engine.read(app).sequence).expect("current sequence node"); + let seq = cx + .read(|app| engine.read(app).sequence) + .expect("current sequence node"); let montage_files = { let time = { let g = graphops::lock(&project); @@ -11866,7 +11922,8 @@ mod tests { "the dropped multi-cam clip's montage resolves media (not black)" ); assert_eq!( - montage_files.len(), 1, + montage_files.len(), + 1, "the host clip's current angle is the montage's only entry" ); } @@ -11877,19 +11934,21 @@ mod tests { #[gpui::test] async fn multicam_switch_follows_audio_to_the_new_source(cx: &mut gpui::TestAppContext) { let _media = media_lock(); - let engine = cx.update(|cx| cx.new(|cx| RealEngine::create(cx))); + let engine = cx.update(|cx| cx.new(RealEngine::create)); cx.update(|app| engine.update(app, |engine, cx| engine.new_project(cx))); - let media_a = - std::env::temp_dir().join(format!("oakapp_afv_a_{}.mp4", std::process::id())); - let media_b = - std::env::temp_dir().join(format!("oakapp_afv_b_{}.mp4", std::process::id())); + let media_a = std::env::temp_dir().join(format!("oakapp_afv_a_{}.mp4", std::process::id())); + let media_b = std::env::temp_dir().join(format!("oakapp_afv_b_{}.mp4", std::process::id())); oak_codec::testmedia::write_test_clip(&media_a, 64, 64, 10, 10).expect("media A"); oak_codec::testmedia::write_test_clip(&media_b, 64, 64, 10, 10).expect("media B"); cx.update(|app| { engine.update(app, |engine, cx| { - engine.import_footage(media_a.clone(), cx).expect("import A"); - engine.import_footage(media_b.clone(), cx).expect("import B"); + engine + .import_footage(media_a.clone(), cx) + .expect("import A"); + engine + .import_footage(media_b.clone(), cx) + .expect("import B"); }) }); let footage = cx @@ -11910,12 +11969,7 @@ mod tests { cx.update(|app| { engine.update(app, |engine, cx| { engine - .multicam_create_sequence( - angles.clone(), - vec![0.0, 0.0], - "AFV".to_string(), - cx, - ) + .multicam_create_sequence(angles.clone(), vec![0.0, 0.0], "AFV".to_string(), cx) .expect("create multicam") }) }); @@ -11934,13 +11988,17 @@ mod tests { let f = graphops::footage_behavior(&g.graph, footage)?; Some(f.filename.clone()) }; - let before = cx.read(|app| audio_source(engine.read(app))).expect("AFV audio before"); + let before = cx + .read(|app| audio_source(engine.read(app))) + .expect("AFV audio before"); // Switch to source 1 (the second angle). cx.update(|app| { engine.update(app, |engine, cx| engine.multicam_switch_to(1, false, cx)); }); - let after = cx.read(|app| audio_source(engine.read(app))).expect("AFV audio after switch"); + let after = cx + .read(|app| audio_source(engine.read(app))) + .expect("AFV audio after switch"); let b_name = media_b.to_string_lossy().into_owned(); assert_eq!(after, b_name, "the AFV audio follows to angle 1's media"); assert_ne!(before, after, "the audio source actually changed"); @@ -11949,8 +12007,9 @@ mod tests { cx.update(|app| { engine.update(app, |engine, cx| engine.undo(cx)); }); - let undone = - cx.read(|app| audio_source(engine.read(app))).expect("AFV audio after undo"); + let undone = cx + .read(|app| audio_source(engine.read(app))) + .expect("AFV audio after undo"); assert_eq!(undone, before, "undo restores the previous source's audio"); let _ = std::fs::remove_file(&media_a); @@ -11964,7 +12023,7 @@ mod tests { #[gpui::test] async fn entry_rename_and_delete_are_real(cx: &mut gpui::TestAppContext) { let _media = media_lock(); - let engine = cx.update(|cx| cx.new(|cx| RealEngine::create(cx))); + let engine = cx.update(|cx| cx.new(RealEngine::create)); cx.update(|app| engine.update(app, |engine, cx| engine.new_project(cx))); let media = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/demo.mp4"); @@ -11973,15 +12032,14 @@ mod tests { engine.import_footage(media.clone(), cx).expect("import") }) }); - let entry = cx - .read(|app| { - engine - .read(app) - .roots() - .into_iter() - .find(|e| e.name.as_ref() == "demo.mp4") - .expect("imported") - }); + let entry = cx.read(|app| { + engine + .read(app) + .roots() + .into_iter() + .find(|e| e.name.as_ref() == "demo.mp4") + .expect("imported") + }); // Rename: the node label changes. cx.update(|app| { @@ -11998,16 +12056,8 @@ mod tests { // Delete: the entry leaves the project browser; ONE undo brings it // back with the rename still applied. - cx.update(|app| { - engine.update(app, |engine, cx| engine.delete_entry(entry.id, cx)) - }); - let gone = cx.read(|app| { - engine - .read(app) - .roots() - .iter() - .any(|e| e.id == entry.id) - }); + cx.update(|app| engine.update(app, |engine, cx| engine.delete_entry(entry.id, cx))); + let gone = cx.read(|app| engine.read(app).roots().iter().any(|e| e.id == entry.id)); assert!(!gone, "the deleted entry leaves the project browser"); cx.update(|app| { engine.update(app, |engine, cx| engine.undo(cx)); diff --git a/crates/oak-app/src/oakui/renderops.rs b/crates/oak-app/src/oakui/renderops.rs index 38bd83e5b..f30d762ad 100644 --- a/crates/oak-app/src/oakui/renderops.rs +++ b/crates/oak-app/src/oakui/renderops.rs @@ -179,7 +179,10 @@ fn clip_multicam_media( // The source sequence and the current source's track. let seq = g.connected_output(mc, SEQUENCE_INPUT, -1)?; let kind = { - let t = mc_entry.core.standard_value(SEQUENCE_TYPE_INPUT, -1).to_double() as i32; + let t = mc_entry + .core + .standard_value(SEQUENCE_TYPE_INPUT, -1) + .to_double() as i32; oak_node::track::TrackType::from_c(t).unwrap_or(oak_node::track::TrackType::Video) }; let seq_pos = super::graphops::track_list_of(g, seq, kind)?; @@ -221,7 +224,10 @@ fn clip_multicam_media( /// unchanged). Parameters are the inspector's parameter set (non-hidden, /// non-connection inputs at their standard values; keyframed values are /// not time-resolved on this path). -fn clip_effects(g: &oak_node::graph::Graph, block_id: NodeId) -> Vec { +fn clip_effects( + g: &oak_node::graph::Graph, + block_id: NodeId, +) -> Vec { use super::effectchain; effectchain::chain(g, block_id) .into_iter() @@ -483,7 +489,9 @@ pub fn render_multicam_angle_frame( width: i32, height: i32, ) -> Result { - render_video(multicam_angle_frame_params(p, seq, track, frame_ts, tb, width, height)?) + render_video(multicam_angle_frame_params( + p, seq, track, frame_ts, tb, width, height, + )?) } /// The audio montage over `range`: every audio clip overlapping the @@ -518,8 +526,7 @@ pub fn audio_montage(p: &ProjectRef, seq: NodeId, range: TimeRange) -> Vec= range.out() { continue; } - let Some((filename, stream_index)) = - clip_preview_media(&g.graph, block_id, false) + let Some((filename, stream_index)) = clip_preview_media(&g.graph, block_id, false) else { continue; }; @@ -631,23 +638,24 @@ impl RenderedFrame { let (w, h) = (meta.width.max(0) as u32, meta.height.max(0) as u32); let pixels = f.shm.slot_bytes(f.slot); let data = pixels.get(..meta.data_size.max(0) as usize)?; - if meta.format == PIXEL_FORMAT_F32 { - // M15 S3: the worker rendered F32 (the 10-bit display - // path) — repack the padded rows, transform and hand the - // samples back for the RGBA16F texture. - let mut samples = repack_f32_rows(meta.width, meta.height, meta.linesize, data)?; - // Output node: working space → the project's output - // colorspace. The scopes below read the output-colorspace - // signal (same convention as the BGRA8 slot); the display - // policy then decides whether the display ICC is applied on - // top (self-managed) or the OS maps the declared content - // colorspace (OS-managed). - apply_output_node_f32(&mut samples); - let scope = analyze_f32_rgba(w, h, &samples); - super::displaycolor::apply_f32_rgba(&mut samples, (w * h) as i64); - let image = f32_rgba_to_bgra_image(w, h, &samples); - Some((image, scope, Some(samples))) - } else { + if meta.format == PIXEL_FORMAT_F32 { + // M15 S3: the worker rendered F32 (the 10-bit display + // path) — repack the padded rows, transform and hand the + // samples back for the RGBA16F texture. + let mut samples = + repack_f32_rows(meta.width, meta.height, meta.linesize, data)?; + // Output node: working space → the project's output + // colorspace. The scopes below read the output-colorspace + // signal (same convention as the BGRA8 slot); the display + // policy then decides whether the display ICC is applied on + // top (self-managed) or the OS maps the declared content + // colorspace (OS-managed). + apply_output_node_f32(&mut samples); + let scope = analyze_f32_rgba(w, h, &samples); + super::displaycolor::apply_f32_rgba(&mut samples, (w * h) as i64); + let image = f32_rgba_to_bgra_image(w, h, &samples); + Some((image, scope, Some(samples))) + } else { // BGRA8 slot: the worker already downconverted — wrap the // bytes directly (no 10-bit path available for them). let scope = analyze_bgra8(w, h, data); @@ -687,11 +695,7 @@ impl RenderedFrame { apply_output_node_f32(&mut samples); let scope = analyze_f32_rgba(w, h, &samples); super::displaycolor::apply_f32_rgba(&mut samples, (w * h) as i64); - Some(( - f32_rgba_to_bgra_image(w, h, &samples), - scope, - Some(samples), - )) + Some((f32_rgba_to_bgra_image(w, h, &samples), scope, Some(samples))) } RenderedFrame::CpuF32 { width, @@ -706,11 +710,7 @@ impl RenderedFrame { apply_output_node_f32(&mut samples); let scope = analyze_f32_rgba(w, h, &samples); super::displaycolor::apply_f32_rgba(&mut samples, (w * h) as i64); - Some(( - f32_rgba_to_bgra_image(w, h, &samples), - scope, - Some(samples), - )) + Some((f32_rgba_to_bgra_image(w, h, &samples), scope, Some(samples))) } } } @@ -721,9 +721,9 @@ impl RenderedFrame { /// Pass-through in the legacy sRGB working space. fn apply_output_node_f32(samples: &mut [f32]) { oak_core::colormath::working_to_display_target( - samples, - oak_core::color::pipeline_working_space(), - oak_core::color::pipeline_output_spec(), + samples, + oak_core::color::pipeline_working_space(), + oak_core::color::pipeline_output_spec(), ); } @@ -742,7 +742,7 @@ fn repack_f32_rows(width: i32, height: i32, linesize: i32, data: &[u8]) -> Optio let mut samples = vec![0.0f32; (width * height * 4) as usize]; for y in 0..height as usize { let row = &data[y * linesize..y * linesize + row_bytes]; - for (i, px) in row.chunks_exact(4).enumerate() { + for (i, px) in row.as_chunks::<4>().0.iter().enumerate() { let v = f32::from_ne_bytes([px[0], px[1], px[2], px[3]]); samples[y * (width as usize) * 4 + i] = v; } @@ -876,7 +876,8 @@ fn proxy_limits_at(p: &ProjectRef, seq: NodeId, time: Rational) -> Vec<(i32, i32 /// variant without copying frame bytes — the shm slot is read zero-copy /// by the caller and released after building the display image). fn render_video(params: VideoTicketParams) -> Result { - let m = RenderManager::global().ok_or_else(|| "render manager is not initialized".to_string())?; + let m = + RenderManager::global().ok_or_else(|| "render manager is not initialized".to_string())?; let id = m.tickets.next_id(); if std::env::var_os("OAK_DEBUG_DISPATCH").is_some() { eprintln!("renderops: sync render submit arena ticket {}", id.0); @@ -959,7 +960,9 @@ pub fn render_sequence_frame( height: i32, format: Option, ) -> Result { - render_video(sequence_frame_params(p, seq, frame_ts, tb, width, height, format)?) + render_video(sequence_frame_params( + p, seq, frame_ts, tb, width, height, format, + )?) } /// Build the video ticket params for one single-footage frame (M15 S2: @@ -1025,7 +1028,9 @@ pub fn render_footage_frame( height: i32, format: Option, ) -> Result { - render_video(footage_frame_params(p, footage, frame_ts, tb, width, height, format)?) + render_video(footage_frame_params( + p, footage, frame_ts, tb, width, height, format, + )?) } /// Rendered interleaved f32 audio (the module audio ticket payload). @@ -1057,7 +1062,8 @@ pub fn render_audio_range( Rational::new((start_ts + len_ts) * tb.0, tb.1), ); let montage = audio_montage(p, seq, range); - let m = RenderManager::global().ok_or_else(|| "render manager is not initialized".to_string())?; + let m = + RenderManager::global().ok_or_else(|| "render manager is not initialized".to_string())?; let id = m.tickets.next_id(); m.tickets.submit_audio_with_id( id, @@ -1178,21 +1184,23 @@ pub fn encoding_params( // Export range: the work area when enabled, otherwise the whole // sequence. Frames -> seconds rationals in the sequence's frame-rate // timebase (frame duration = rate_den / rate_num). - let (has_custom_range, range_in, range_out, length) = - match workarea.filter(|(s, e)| e > s) { - Some((in_ts, out_ts)) => ( - true, - Rational::new(in_ts * i64::from(rate_den), i64::from(rate_num)), - Rational::new(out_ts * i64::from(rate_den), i64::from(rate_num)), - Rational::new((out_ts - in_ts) * i64::from(rate_den), i64::from(rate_num)), + let (has_custom_range, range_in, range_out, length) = match workarea.filter(|(s, e)| e > s) { + Some((in_ts, out_ts)) => ( + true, + Rational::new(in_ts * i64::from(rate_den), i64::from(rate_num)), + Rational::new(out_ts * i64::from(rate_den), i64::from(rate_num)), + Rational::new((out_ts - in_ts) * i64::from(rate_den), i64::from(rate_num)), + ), + None => ( + false, + Rational::new(0, 1), + Rational::new(0, 1), + Rational::new( + length_frames.max(0) * i64::from(rate_den), + i64::from(rate_num), ), - None => ( - false, - Rational::new(0, 1), - Rational::new(0, 1), - Rational::new(length_frames.max(0) * i64::from(rate_den), i64::from(rate_num)), - ), - }; + ), + }; Ok(oak_task::export::EncodingParams { filename: path.to_string_lossy().into_owned(), format, @@ -1245,12 +1253,22 @@ pub fn encoding_params_with_settings( .iter() .find(|c| **c as i32 == settings.video_codec) .copied() - .ok_or_else(|| format!("codec {} not supported by {container:?}", settings.video_codec))?; + .ok_or_else(|| { + format!( + "codec {} not supported by {container:?}", + settings.video_codec + ) + })?; let audio_codec = oak_codec::exportformat::Format::get_audio_codecs(container) .iter() .find(|c| **c as i32 == settings.audio_codec) .copied() - .ok_or_else(|| format!("audio codec {} not supported by {container:?}", settings.audio_codec))?; + .ok_or_else(|| { + format!( + "audio codec {} not supported by {container:?}", + settings.audio_codec + ) + })?; let (mut width, mut height, rate) = { let g = lock(p); super::graphops::sequence_video_params(&g.graph, seq) @@ -1281,24 +1299,36 @@ pub fn encoding_params_with_settings( _ => match workarea.filter(|(s, e)| e > s) { Some((in_ts, out_ts)) => ( true, - Rational::new(in_ts * i64::from(rate_den.max(1)), i64::from(rate_num.max(1))), - Rational::new(out_ts * i64::from(rate_den.max(1)), i64::from(rate_num.max(1))), - Rational::new((out_ts - in_ts) * i64::from(rate_den.max(1)), i64::from(rate_num.max(1))), + Rational::new( + in_ts * i64::from(rate_den.max(1)), + i64::from(rate_num.max(1)), + ), + Rational::new( + out_ts * i64::from(rate_den.max(1)), + i64::from(rate_num.max(1)), + ), + Rational::new( + (out_ts - in_ts) * i64::from(rate_den.max(1)), + i64::from(rate_num.max(1)), + ), ), None => ( false, Rational::new(0, 1), Rational::new(0, 1), - Rational::new(length_frames.max(0) * i64::from(rate_den.max(1)), i64::from(rate_num.max(1))), + Rational::new( + length_frames.max(0) * i64::from(rate_den.max(1)), + i64::from(rate_num.max(1)), + ), ), }, }; let bit_depth = settings.bit_depth; let pixel_format = match bit_depth { 10 => 1, // PixelFormat::U10 (only when the codec supports it; the - // dialog gates 10-bit behind HDR — codecs without 10-bit - // stay at the 8-bit default and the dialog disables HDR). - _ => 0, // PixelFormat::U8 + // dialog gates 10-bit behind HDR — codecs without 10-bit + // stay at the 8-bit default and the dialog disables HDR). + _ => 0, // PixelFormat::U8 }; Ok(oak_task::export::EncodingParams { filename: path.to_string_lossy().into_owned(), @@ -1394,7 +1424,8 @@ mod tests { fn project_with_clip(media: &std::path::Path) -> (ProjectRef, NodeId, NodeId) { let project = graphops::create_project(); let seq = graphops::create_sequence(&project, "Montage Test"); - let footage = graphops::import_footage(&project, media).expect("import the generated media"); + let footage = + graphops::import_footage(&project, media).expect("import the generated media"); graphops::add_track(&project, seq, TrackType::Video).expect("add a video track"); graphops::place_footage_clip(&project, seq, footage, TrackType::Video, 0, 0, 10, 0) .expect("place the clip"); @@ -1416,9 +1447,12 @@ mod tests { queue, oak_core::backend::BackendKind::Auto, ); - let mut frame = - oak_render::eval::generate_frame(Rational::new(0, 1), (2, 1), oak_core::PixelFormat::F32) - .unwrap(); + let mut frame = oak_render::eval::generate_frame( + Rational::new(0, 1), + (2, 1), + oak_core::PixelFormat::F32, + ) + .unwrap(); for px in frame.data.chunks_exact_mut(16) { for (c, v) in px.chunks_exact_mut(4).zip([0.25f32, 0.5, 0.75, 1.0]) { c.copy_from_slice(&v.to_le_bytes()); @@ -1446,7 +1480,13 @@ mod tests { // the display function takes the single explicit readback. let token = base.create_texture(2, 1).unwrap(); base.upload(token, &frame).unwrap(); - let private = RenderedFrame::Gpu(Texture::gpu(base.clone(), token, 2, 1, oak_core::PixelFormat::F32)); + let private = RenderedFrame::Gpu(Texture::gpu( + base.clone(), + token, + 2, + 1, + oak_core::PixelFormat::F32, + )); oak_core::backend::reset_gpu_transfer_counters(); let displayed = private.to_display().expect("fallback display"); assert!(displayed.2.is_some(), "the fallback hands CPU samples"); @@ -1521,7 +1561,10 @@ mod tests { let montage = video_montage(&project, seq, at(0)); assert_eq!(montage.len(), 1, "one clip covers frame 0"); assert_eq!(montage[0].filename, media.to_string_lossy()); - assert!(video_montage(&project, seq, at(9)).len() == 1, "frame 9 is still covered"); + assert!( + video_montage(&project, seq, at(9)).len() == 1, + "frame 9 is still covered" + ); assert!( video_montage(&project, seq, at(10)).is_empty(), "frame 10 is past the clip's out point" @@ -1543,8 +1586,7 @@ mod tests { fn video_montage_stacks_highest_track_on_top() { let _media = media_lock(); oak_undo::global::clear().unwrap(); - let red = - std::env::temp_dir().join(format!("oakapp_stack_red_{}.mp4", std::process::id())); + let red = std::env::temp_dir().join(format!("oakapp_stack_red_{}.mp4", std::process::id())); let blue = std::env::temp_dir().join(format!("oakapp_stack_blue_{}.mp4", std::process::id())); oak_codec::testmedia::write_test_clip_solid(&red, 64, 64, 10, 10, [0.9, 0.1, 0.1, 1.0]) @@ -1555,7 +1597,8 @@ mod tests { let project = graphops::create_project(); let seq = graphops::create_sequence(&project, "Stack Montage"); let red_footage = graphops::import_footage(&project, &red).expect("import the red media"); - let blue_footage = graphops::import_footage(&project, &blue).expect("import the blue media"); + let blue_footage = + graphops::import_footage(&project, &blue).expect("import the blue media"); graphops::place_footage_clip(&project, seq, red_footage, TrackType::Video, 0, 0, 10, 0) .expect("place the V1 (red) clip"); graphops::place_footage_clip(&project, seq, blue_footage, TrackType::Video, 1, 0, 10, 0) @@ -1566,7 +1609,11 @@ mod tests { // Bottom-to-top: V1's (red) clip first, V2's (blue) last. let montage = video_montage(&project, seq, time); assert_eq!(montage.len(), 2, "both clips cover frame 0"); - assert_eq!(montage[0].filename, red.to_string_lossy(), "V1's clip is the bottom of the stack"); + assert_eq!( + montage[0].filename, + red.to_string_lossy(), + "V1's clip is the bottom of the stack" + ); assert_eq!( montage[1].filename, blue.to_string_lossy(), @@ -1595,7 +1642,10 @@ mod tests { let off = (8 * 64 + 8) * 16; let r = f32::from_le_bytes(dst[off..off + 4].try_into().unwrap()); let b = f32::from_le_bytes(dst[off + 8..off + 12].try_into().unwrap()); - assert!(b > 0.5 && r < 0.4, "V2's blue covers V1's red (r={r}, b={b})"); + assert!( + b > 0.5 && r < 0.4, + "V2's blue covers V1's red (r={r}, b={b})" + ); oak_undo::global::clear().unwrap(); let _ = std::fs::remove_file(&red); @@ -1614,8 +1664,8 @@ mod tests { // legacy sRGB pass-through so the pixel-value assertions hold // regardless of the ACEScg default. oak_core::color::set_pipeline_color_settings( - oak_core::colormath::WorkingColorSpace::SrgbLegacy, - oak_core::colormath::OutputColorSpec::default(), + oak_core::colormath::WorkingColorSpace::SrgbLegacy, + oak_core::colormath::OutputColorSpec::default(), ); oak_undo::global::clear().unwrap(); let media = @@ -1624,10 +1674,12 @@ mod tests { let project = graphops::create_project(); let seq = graphops::create_sequence(&project, "Effect Montage"); - let footage = graphops::import_footage(&project, &media).expect("import the generated media"); + let footage = + graphops::import_footage(&project, &media).expect("import the generated media"); graphops::add_track(&project, seq, TrackType::Video).expect("add a video track"); - let block = graphops::place_footage_clip(&project, seq, footage, TrackType::Video, 0, 0, 10, 0) - .expect("place the clip"); + let block = + graphops::place_footage_clip(&project, seq, footage, TrackType::Video, 0, 0, 10, 0) + .expect("place the clip"); // The effect-library double-click flow: append a 50% Opacity to the // footage-backed clip (the insert rewires footage -> effect -> clip). @@ -1674,7 +1726,9 @@ mod tests { // A left-half pixel of the test pattern (known content, r ~= 0.9). let pixel = |frame: &[u8]| { let off = (8 * 64 + 8) * 16; - [0, 1, 2, 3].map(|i| f32::from_le_bytes(frame[off + i * 4..off + i * 4 + 4].try_into().unwrap())) + [0, 1, 2, 3].map(|i| { + f32::from_le_bytes(frame[off + i * 4..off + i * 4 + 4].try_into().unwrap()) + }) }; let with_fx = video_montage(&project, seq, time); @@ -1684,7 +1738,10 @@ mod tests { 1, "the montage carries the clip's effect stack (not the footage source node)" ); - assert_eq!(with_fx[0].effects[0].type_id, "org.olivevideoeditor.Olive.opacity"); + assert_eq!( + with_fx[0].effects[0].type_id, + "org.olivevideoeditor.Olive.opacity" + ); assert!(with_fx[0].effects[0].enabled); let plain = render( @@ -1699,7 +1756,11 @@ mod tests { ); let effected = render(with_fx); let (p, e) = (pixel(&plain), pixel(&effected)); - assert!(p[0] > 0.6, "the plain render shows the test pattern (r={})", p[0]); + assert!( + p[0] > 0.6, + "the plain render shows the test pattern (r={})", + p[0] + ); // 50% opacity: the shader halves every channel, then the composite // over transparent black halves it again via the halved alpha. assert!( @@ -1746,7 +1807,8 @@ mod tests { ) -> (NodeId, NodeId) { let (tb, in_r) = { let g = lock(p); - let tb = graphops::sequence_time_base(&g.graph, seq).expect("the sequence has a timebase"); + let tb = + graphops::sequence_time_base(&g.graph, seq).expect("the sequence has a timebase"); (tb, graphops::ts_to_rational(in_ts, tb)) }; let block = { @@ -1824,7 +1886,8 @@ mod tests { let project = graphops::create_project(); let seq = graphops::create_sequence(&project, "Adjustment Montage"); let red_footage = graphops::import_footage(&project, &red).expect("import the red media"); - let blue_footage = graphops::import_footage(&project, &blue).expect("import the blue media"); + let blue_footage = + graphops::import_footage(&project, &blue).expect("import the blue media"); graphops::place_footage_clip(&project, seq, red_footage, TrackType::Video, 0, 0, 10, 0) .expect("place the V1 (red) clip"); graphops::place_footage_clip(&project, seq, blue_footage, TrackType::Video, 1, 0, 10, 0) @@ -1839,36 +1902,38 @@ mod tests { let tb = graphops::sequence_time_base(&lock(&project).graph, seq).unwrap(); let at = |frame: i64| graphops::ts_to_rational(frame, tb); - let render = |montage: Vec, adjustments: Vec, at_time: Rational| { - let params = VideoTicketParams { - viewer: 0, - project: String::new(), - time: at_time, - force_size: Some((64, 64)), - force_format: Some(oak_core::PixelFormat::F32), - cache: None, - cache_dir: None, - cache_id: None, - cache_timebase: None, - footage: None, - montage, - adjustments, + let render = + |montage: Vec, adjustments: Vec, at_time: Rational| { + let params = VideoTicketParams { + viewer: 0, + project: String::new(), + time: at_time, + force_size: Some((64, 64)), + force_format: Some(oak_core::PixelFormat::F32), + cache: None, + cache_dir: None, + cache_id: None, + cache_timebase: None, + footage: None, + montage, + adjustments, + }; + let mut dst = vec![0u8; 64 * 64 * 16]; + oak_render::eval::render_montage_frame_into( + params.time, + ¶ms, + (64, 64), + &mut dst, + 64 * 16, + ) + .expect("montage render"); + dst }; - let mut dst = vec![0u8; 64 * 64 * 16]; - oak_render::eval::render_montage_frame_into( - params.time, - ¶ms, - (64, 64), - &mut dst, - 64 * 16, - ) - .expect("montage render"); - dst - }; let pixel = |frame: &[u8]| { let off = (8 * 64 + 8) * 16; - [0, 1, 2, 3] - .map(|i| f32::from_le_bytes(frame[off + i * 4..off + i * 4 + 4].try_into().unwrap())) + [0, 1, 2, 3].map(|i| { + f32::from_le_bytes(frame[off + i * 4..off + i * 4 + 4].try_into().unwrap()) + }) }; // Inside the layer's range the span rides the ticket, above the stack. @@ -1880,8 +1945,15 @@ mod tests { montage.len(), "the layer sits above every clip (it grades the whole composite)" ); - assert_eq!(spans[0].effects.len(), 1, "the layer's stack rides the span"); - assert_eq!(spans[0].effects[0].type_id, "org.olivevideoeditor.Olive.opacity"); + assert_eq!( + spans[0].effects.len(), + 1, + "the layer's stack rides the span" + ); + assert_eq!( + spans[0].effects[0].type_id, + "org.olivevideoeditor.Olive.opacity" + ); let baseline = render(montage.clone(), Vec::new(), at(5)); let graded = render(montage.clone(), spans, at(5)); @@ -1948,7 +2020,8 @@ mod tests { oak_core::colormath::OutputColorSpec::default(), ); oak_undo::global::clear().unwrap(); - let red = std::env::temp_dir().join(format!("oakapp_adjlow_red_{}.mp4", std::process::id())); + let red = + std::env::temp_dir().join(format!("oakapp_adjlow_red_{}.mp4", std::process::id())); let blue = std::env::temp_dir().join(format!("oakapp_adjlow_blue_{}.mp4", std::process::id())); oak_codec::testmedia::write_test_clip_solid(&red, 64, 64, 10, 10, [0.9, 0.1, 0.1, 1.0]) @@ -1959,7 +2032,8 @@ mod tests { let project = graphops::create_project(); let seq = graphops::create_sequence(&project, "Mid Adjustment Montage"); let red_footage = graphops::import_footage(&project, &red).expect("import the red media"); - let blue_footage = graphops::import_footage(&project, &blue).expect("import the blue media"); + let blue_footage = + graphops::import_footage(&project, &blue).expect("import the blue media"); graphops::place_footage_clip(&project, seq, red_footage, TrackType::Video, 0, 0, 10, 0) .expect("place the V1 (red) clip"); graphops::add_track(&project, seq, TrackType::Video).expect("add the adjustment track"); @@ -1970,9 +2044,17 @@ mod tests { let (_adj, _fx) = adjustment_layer_with_opacity(&project, seq, adj_track, 0, 5, 0.5); // V3's clip carries its own 50% Opacity so the composite keeps both // contributions visible (an opaque top clip would hide the grading). - let blue_clip = - graphops::place_footage_clip(&project, seq, blue_footage, TrackType::Video, 2, 0, 10, 0) - .expect("place the V3 (blue) clip"); + let blue_clip = graphops::place_footage_clip( + &project, + seq, + blue_footage, + TrackType::Video, + 2, + 0, + 10, + 0, + ) + .expect("place the V3 (blue) clip"); let clip_fx = crate::oakui::effectchain::insert( &project, blue_clip, @@ -1990,36 +2072,38 @@ mod tests { let tb = graphops::sequence_time_base(&lock(&project).graph, seq).unwrap(); let time = graphops::ts_to_rational(0, tb); - let render = |montage: Vec, adjustments: Vec, at_time: Rational| { - let params = VideoTicketParams { - viewer: 0, - project: String::new(), - time: at_time, - force_size: Some((64, 64)), - force_format: Some(oak_core::PixelFormat::F32), - cache: None, - cache_dir: None, - cache_id: None, - cache_timebase: None, - footage: None, - montage, - adjustments, + let render = + |montage: Vec, adjustments: Vec, at_time: Rational| { + let params = VideoTicketParams { + viewer: 0, + project: String::new(), + time: at_time, + force_size: Some((64, 64)), + force_format: Some(oak_core::PixelFormat::F32), + cache: None, + cache_dir: None, + cache_id: None, + cache_timebase: None, + footage: None, + montage, + adjustments, + }; + let mut dst = vec![0u8; 64 * 64 * 16]; + oak_render::eval::render_montage_frame_into( + at_time, + ¶ms, + (64, 64), + &mut dst, + 64 * 16, + ) + .expect("montage render"); + dst }; - let mut dst = vec![0u8; 64 * 64 * 16]; - oak_render::eval::render_montage_frame_into( - at_time, - ¶ms, - (64, 64), - &mut dst, - 64 * 16, - ) - .expect("montage render"); - dst - }; let pixel = |frame: &[u8]| { let off = (8 * 64 + 8) * 16; - [0, 1, 2, 3] - .map(|i| f32::from_le_bytes(frame[off + i * 4..off + i * 4 + 4].try_into().unwrap())) + [0, 1, 2, 3].map(|i| { + f32::from_le_bytes(frame[off + i * 4..off + i * 4 + 4].try_into().unwrap()) + }) }; let (montage, spans) = video_montage_with_adjustments(&project, seq, time); @@ -2027,10 +2111,17 @@ mod tests { // V1's red then V3's blue, and the boundary sits between them. assert_eq!(montage.len(), 2, "the two clips cover frame 0"); assert!(montage[0].effects.is_empty(), "the red clip has no stack"); - assert_eq!(montage[1].effects.len(), 1, "the blue clip's own stack rides it"); + assert_eq!( + montage[1].effects.len(), + 1, + "the blue clip's own stack rides it" + ); assert_eq!(spans.len(), 1, "the layer emits one span"); assert_eq!(spans[0].track_index, 1, "the layer grades only V1's clip"); - assert!(!spans[0].effects.is_empty(), "the layer carries its opacity"); + assert!( + !spans[0].effects.is_empty(), + "the layer carries its opacity" + ); let baseline = pixel(&render(montage.clone(), Vec::new(), time)); let graded = pixel(&render(montage.clone(), spans.clone(), time)); @@ -2082,8 +2173,7 @@ mod tests { #[test] fn clip_multicam_media_resolves_the_current_angle() { let _media = media_lock(); - let media = - std::env::temp_dir().join(format!("oakapp_mcmedia_{}.mp4", std::process::id())); + let media = std::env::temp_dir().join(format!("oakapp_mcmedia_{}.mp4", std::process::id())); oak_codec::testmedia::write_test_clip(&media, 64, 64, 10, 10).expect("generate test media"); let (project, seq, footage) = project_with_clip(&media); @@ -2114,19 +2204,20 @@ mod tests { -1, oak_node::value::NodeValue::Combo(0), ); - } + } g.graph - .connect(source_seq, id, oak_node::nodes::multicamnode::SEQUENCE_INPUT, -1) + .connect( + source_seq, + id, + oak_node::nodes::multicamnode::SEQUENCE_INPUT, + -1, + ) .unwrap(); // The angle CLIP feeds the source array at element 0 (the // traverser's `active_elements_at_time` keeps only the current // source, so multi `value()` forwards exactly it). g.graph - .input_array_insert( - id, - oak_node::nodes::multicamnode::SOURCES_INPUT, - 0, - ) + .input_array_insert(id, oak_node::nodes::multicamnode::SOURCES_INPUT, 0) .unwrap(); g.graph .connect( @@ -2140,15 +2231,24 @@ mod tests { }; // A host clip on the host sequence (default layout V1) fed from the // multicam output; the clip's own input must first be connected. - let host_clip = graphops::place_footage_clip(&project, seq, footage, TrackType::Video, 0, 0, 10, 0) - .expect("place the host clip"); + let host_clip = + graphops::place_footage_clip(&project, seq, footage, TrackType::Video, 0, 0, 10, 0) + .expect("place the host clip"); { let mut g = graphops::lock(&project); - let _ = g - .graph - .disconnect(footage, host_clip, oak_node::block::clip_input::TEXTURE_INPUT, -1); + g.graph.disconnect( + footage, + host_clip, + oak_node::block::clip_input::TEXTURE_INPUT, + -1, + ); g.graph - .connect(mc, host_clip, oak_node::block::clip_input::TEXTURE_INPUT, -1) + .connect( + mc, + host_clip, + oak_node::block::clip_input::TEXTURE_INPUT, + -1, + ) .unwrap(); } // The montage at host time 0 resolves to the angle's media. @@ -2263,8 +2363,9 @@ mod tests { .copied() .expect("red clip") }; - let blue_clip = graphops::place_footage_clip(&project, seq, bfoot, TrackType::Video, 1, 0, 10, 0) - .expect("blue angle slot"); + let blue_clip = + graphops::place_footage_clip(&project, seq, bfoot, TrackType::Video, 1, 0, 10, 0) + .expect("blue angle slot"); let mc = { let mut g = graphops::lock(&project); let (core, behavior) = oak_node::nodes::multicamnode::create(); @@ -2280,13 +2381,23 @@ mod tests { .input_array_insert(id, oak_node::nodes::multicamnode::SOURCES_INPUT, 0) .unwrap(); g.graph - .connect(red_clip, id, oak_node::nodes::multicamnode::SOURCES_INPUT, 0) + .connect( + red_clip, + id, + oak_node::nodes::multicamnode::SOURCES_INPUT, + 0, + ) .unwrap(); g.graph .input_array_insert(id, oak_node::nodes::multicamnode::SOURCES_INPUT, 1) .unwrap(); g.graph - .connect(blue_clip, id, oak_node::nodes::multicamnode::SOURCES_INPUT, 1) + .connect( + blue_clip, + id, + oak_node::nodes::multicamnode::SOURCES_INPUT, + 1, + ) .unwrap(); id }; @@ -2296,14 +2407,19 @@ mod tests { .expect("host clip"); { let mut g = graphops::lock(&project); - let _ = g.graph.disconnect( + g.graph.disconnect( _rfoot, host_clip, oak_node::block::clip_input::TEXTURE_INPUT, -1, ); g.graph - .connect(mc, host_clip, oak_node::block::clip_input::TEXTURE_INPUT, -1) + .connect( + mc, + host_clip, + oak_node::block::clip_input::TEXTURE_INPUT, + -1, + ) .unwrap(); } @@ -2332,7 +2448,10 @@ mod tests { }; // Source 0 = red angle. let (r0, b0) = render_rgb(0); - assert!(r0 > 0.4 && b0 < 0.4, "source 0 is the red angle (r={r0} b={b0})"); + assert!( + r0 > 0.4 && b0 < 0.4, + "source 0 is the red angle (r={r0} b={b0})" + ); // Switch to source 1: current_in drives the element the multi // `value()` forwards — the frame must turn blue. @@ -2490,7 +2609,8 @@ mod tests { #[test] fn audio_montage_overlaps_the_range() { let _media = media_lock(); - let media = std::env::temp_dir().join(format!("oakapp_montage_a_{}.mp4", std::process::id())); + let media = + std::env::temp_dir().join(format!("oakapp_montage_a_{}.mp4", std::process::id())); oak_codec::testmedia::write_test_clip(&media, 64, 64, 10, 10).expect("generate test media"); let (project, seq, footage) = project_with_clip(&media); @@ -2502,9 +2622,15 @@ mod tests { let overlapping = audio_montage(&project, seq, TimeRange::new(at(0), at(5))); assert_eq!(overlapping.len(), 1, "the range overlaps the audio clip"); - assert_eq!(overlapping[0].stream_index, 1, "the audio stream is selected"); + assert_eq!( + overlapping[0].stream_index, 1, + "the audio stream is selected" + ); let disjoint = audio_montage(&project, seq, TimeRange::new(at(10), at(20))); - assert!(disjoint.is_empty(), "a range past the clip overlaps nothing"); + assert!( + disjoint.is_empty(), + "a range past the clip overlaps nothing" + ); oak_undo::global::clear().unwrap(); let _ = std::fs::remove_file(&media); } @@ -2515,7 +2641,8 @@ mod tests { #[test] fn montages_skip_muted_tracks() { let _media = media_lock(); - let media = std::env::temp_dir().join(format!("oakapp_montage_m_{}.mp4", std::process::id())); + let media = + std::env::temp_dir().join(format!("oakapp_montage_m_{}.mp4", std::process::id())); oak_codec::testmedia::write_test_clip(&media, 64, 64, 10, 10).expect("generate test media"); let (project, seq, footage) = project_with_clip(&media); @@ -2548,8 +2675,16 @@ mod tests { oak_undo::global::undo().unwrap(); oak_undo::global::undo().unwrap(); - assert_eq!(video_montage(&project, seq, at(0)).len(), 1, "undo restores the video clip"); - assert_eq!(audio_montage(&project, seq, range).len(), 1, "undo restores the audio clip"); + assert_eq!( + video_montage(&project, seq, at(0)).len(), + 1, + "undo restores the video clip" + ); + assert_eq!( + audio_montage(&project, seq, range).len(), + 1, + "undo restores the audio clip" + ); oak_undo::global::clear().unwrap(); let _ = std::fs::remove_file(&media); } @@ -2569,7 +2704,11 @@ mod tests { // underflows the renderable minimum. assert_eq!(full_res_render_size(0, 0, 1), (2, 2)); assert_eq!(full_res_render_size(1, 1, 8), (2, 2)); - assert_eq!(full_res_render_size(1920, 1080, 0), (1920, 1080), "a zero divider means Full"); + assert_eq!( + full_res_render_size(1920, 1080, 0), + (1920, 1080), + "a zero divider means Full" + ); } /// Clamping only shrinks: a render that already fits a limit is left @@ -2583,7 +2722,11 @@ mod tests { (960, 540), "a smaller request is not upscaled to the proxy" ); - assert_eq!(clamp_render_size(1920, 1080, &[]), (1920, 1080), "no limits = no clamping"); + assert_eq!( + clamp_render_size(1920, 1080, &[]), + (1920, 1080), + "no limits = no clamping" + ); assert_eq!( clamp_render_size(1920, 1080, &[(640, 640)]), (640, 360), @@ -2609,7 +2752,8 @@ mod tests { std::env::temp_dir().join(format!("oakapp_proxy_src_{}.mp4", std::process::id())); let proxy = std::env::temp_dir().join(format!("oakapp_proxy_32_{}.mp4", std::process::id())); - oak_codec::testmedia::write_test_clip(&orig, 64, 64, 10, 10).expect("generate the source media"); + oak_codec::testmedia::write_test_clip(&orig, 64, 64, 10, 10) + .expect("generate the source media"); oak_codec::testmedia::write_test_clip_solid(&proxy, 32, 32, 10, 10, [0.1, 0.1, 0.9, 1.0]) .expect("generate the proxy media"); @@ -2626,7 +2770,11 @@ mod tests { let f = g .graph .get_mut(footage) - .and_then(|e| e.behavior.as_any_mut()?.downcast_mut::()) + .and_then(|e| { + e.behavior + .as_any_mut()? + .downcast_mut::() + }) .expect("the footage behavior"); f.proxy = proxy.to_string_lossy().into_owned(); f.proxy_enabled = true; @@ -2644,22 +2792,39 @@ mod tests { let f = graphops::footage_behavior(&g.graph, footage).expect("the footage behavior"); preview_footage_media(f, true).0 }; - assert_eq!(selected, proxy.to_string_lossy(), "the proxy stands in for the source"); + assert_eq!( + selected, + proxy.to_string_lossy(), + "the proxy stands in for the source" + ); // …and the sequence frame ticket is clamped to its 32x32 size. let tb = graphops::sequence_time_base(&lock(&project).graph, seq).unwrap(); - let params = - sequence_frame_params(&project, seq, 0, tb, 64, 64, None).expect("sequence frame params"); - assert_eq!(params.force_size, Some((32, 32)), "the proxy bounds the render size"); + let params = sequence_frame_params(&project, seq, 0, tb, 64, 64, None) + .expect("sequence frame params"); + assert_eq!( + params.force_size, + Some((32, 32)), + "the proxy bounds the render size" + ); // The decoded pixels are the proxy's (solid blue), not the source's. let mut dst = vec![0u8; 32 * 32 * 16]; - oak_render::eval::render_montage_frame_into(params.time, ¶ms, (32, 32), &mut dst, 32 * 16) - .expect("montage render at the proxy size"); + oak_render::eval::render_montage_frame_into( + params.time, + ¶ms, + (32, 32), + &mut dst, + 32 * 16, + ) + .expect("montage render at the proxy size"); let off = (8 * 32 + 8) * 16; let r = f32::from_le_bytes(dst[off..off + 4].try_into().unwrap()); let b = f32::from_le_bytes(dst[off + 8..off + 12].try_into().unwrap()); - assert!(b > 0.5 && r < 0.4, "the proxy's blue covers the frame (r={r}, b={b})"); + assert!( + b > 0.5 && r < 0.4, + "the proxy's blue covers the frame (r={r}, b={b})" + ); store.set(None, "UseProxyMedia", &old); oak_undo::global::clear().unwrap(); diff --git a/crates/oak-app/src/oakui/scopes.rs b/crates/oak-app/src/oakui/scopes.rs index 35d91356a..8fb81276d 100644 --- a/crates/oak-app/src/oakui/scopes.rs +++ b/crates/oak-app/src/oakui/scopes.rs @@ -97,7 +97,7 @@ pub(crate) fn analyze_bgra8(width: u32, height: u32, bytes: &[u8]) -> ScopeData let mut luma = Vec::with_capacity(pixels); let mut chroma = Vec::with_capacity(pixels); let src = bytes.get(..pixels * 4).unwrap_or_default(); - for px in src.chunks_exact(4) { + for px in src.as_chunks::<4>().0 { let b = f32::from(px[0]) / 255.0; let g = f32::from(px[1]) / 255.0; let r = f32::from(px[2]) / 255.0; diff --git a/crates/oak-app/src/oakui/waveform.rs b/crates/oak-app/src/oakui/waveform.rs index 2b56a914b..38f039ff0 100644 --- a/crates/oak-app/src/oakui/waveform.rs +++ b/crates/oak-app/src/oakui/waveform.rs @@ -90,7 +90,11 @@ impl WaveformCache { /// The cached waveform of `clip`, if extracted. pub fn get(&self, clip: u64) -> Option> { - self.map.lock().unwrap_or_else(|e| e.into_inner()).get(&clip).cloned() + self.map + .lock() + .unwrap_or_else(|e| e.into_inner()) + .get(&clip) + .cloned() } /// Ensure `clip` (with media `filename`, `duration_frames` long) has a @@ -105,16 +109,17 @@ impl WaveformCache { return; }; let mut map = self.map.lock().unwrap_or_else(|e| e.into_inner()); - if !map.contains_key(&clip) { - map.insert(clip, Arc::new(waveform)); - self.version.fetch_add(1, std::sync::atomic::Ordering::Relaxed); + if let std::collections::hash_map::Entry::Vacant(e) = map.entry(clip) { + e.insert(Arc::new(waveform)); + self.version + .fetch_add(1, std::sync::atomic::Ordering::Relaxed); } } } /// Extract a clip's waveform (first channel) via oakaudio's waveform /// extractor. - fn extract(filename: &str, duration_frames: i64, _fps: f32) -> Option { +fn extract(filename: &str, duration_frames: i64, _fps: f32) -> Option { let cname = std::ffi::CString::new(filename).ok()?; const SAMPLES_PER_POINT: i32 = 256; // Audio-stream index 0 (probe numbering). diff --git a/crates/oak-app/src/oakui/waveformsync.rs b/crates/oak-app/src/oakui/waveformsync.rs index ee1cbfe32..8cd7f2f80 100644 --- a/crates/oak-app/src/oakui/waveformsync.rs +++ b/crates/oak-app/src/oakui/waveformsync.rs @@ -106,7 +106,7 @@ mod tests { let peaks: Vec = (0..points) .map(|i| { let sample = i64::from(i) * i64::from(SPP); - let inside = sample >= 48_000 && sample < 96_000; + let inside = (48_000..96_000).contains(&sample); if inside { MinMax { min: -1.0, max: 1.0 } } else { diff --git a/crates/oak-app/src/panels/effect_library.rs b/crates/oak-app/src/panels/effect_library.rs index 38e8b751a..4beeccbfc 100644 --- a/crates/oak-app/src/panels/effect_library.rs +++ b/crates/oak-app/src/panels/effect_library.rs @@ -23,6 +23,7 @@ use std::collections::HashSet; +use crate::oakui::component::text_input; use gpui::colors::DefaultColors; use gpui::dock::{DockPanel, PanelEvent}; use gpui::{ @@ -30,7 +31,6 @@ use gpui::{ Render, SharedString, Window, }; use gpui_elements::editable_text::{EditableTextState, StringStorage, TextChanged}; -use crate::oakui::component::text_input; use crate::i18n; use crate::oakui::effectchain::group_label; @@ -60,7 +60,8 @@ impl EffectLibraryPanel { pub fn new(engine: Entity, _window: &mut Window, cx: &mut Context) -> Self { // Re-read the effect table whenever the engine notifies (the table // itself is static, but the selection hint depends on the target). - cx.observe(&engine, |_this, _engine, cx| cx.notify()).detach(); + cx.observe(&engine, |_this, _engine, cx| cx.notify()) + .detach(); let search = cx.new(|cx| EditableTextState::new(StringStorage::default(), cx)); cx.subscribe(&search, |_this, _state, _event: &TextChanged, cx| { cx.notify(); @@ -212,7 +213,7 @@ impl Render for EffectLibraryPanel { list = list.child( div() .id(SharedString::from(format!("effect-library-{type_id}"))) - .debug_selector(move || format!("effect-library-row-{row_id}").into()) + .debug_selector(move || format!("effect-library-row-{row_id}")) .cursor_pointer() .px_2() .py_1() @@ -296,7 +297,9 @@ fn group_header( on_toggle: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static, ) -> impl IntoElement { div() - .id(SharedString::from(format!("effect-library-group-{group_key}"))) + .id(SharedString::from(format!( + "effect-library-group-{group_key}" + ))) .debug_selector({ let key = group_key.to_string(); move || format!("effect-library-group-row-{key}") @@ -397,7 +400,10 @@ mod tests { let mut collapsed = HashSet::new(); assert!(!hidden_by_collapse(&color, "", &collapsed)); - assert!(toggle_collapsed(&mut collapsed, "color"), "first toggle collapses"); + assert!( + toggle_collapsed(&mut collapsed, "color"), + "first toggle collapses" + ); assert!(hidden_by_collapse(&color, "", &collapsed)); assert!( !hidden_by_collapse(&color, "blur", &collapsed), @@ -407,7 +413,10 @@ mod tests { !hidden_by_collapse(&color, "", &HashSet::new()), "an unlisted group stays open" ); - assert!(!toggle_collapsed(&mut collapsed, "color"), "second toggle expands"); + assert!( + !toggle_collapsed(&mut collapsed, "color"), + "second toggle expands" + ); assert!(collapsed.is_empty()); } @@ -440,9 +449,14 @@ mod tests { #[test] fn collapsed_state_persists_in_the_config() { let _guard = collapsed_config_lock(); - assert!(load_collapsed().is_empty(), "the lock starts from a clear key"); - let collapsed: HashSet = - ["distort", "general"].iter().map(|k| k.to_string()).collect(); + assert!( + load_collapsed().is_empty(), + "the lock starts from a clear key" + ); + let collapsed: HashSet = ["distort", "general"] + .iter() + .map(|k| k.to_string()) + .collect(); store_collapsed(&collapsed); assert_eq!(load_collapsed(), collapsed); store_collapsed(&HashSet::new()); @@ -456,7 +470,7 @@ mod tests { let _guard = collapsed_config_lock(); cx.update(|cx| cx.init_colors()); let window = cx.open_window(size(px(400.0), px(600.0)), |window, cx| { - let engine = cx.new(|cx| MockEngine::demo(cx)); + let engine = cx.new(MockEngine::demo); EffectLibraryPanel::new(engine, window, cx) }); cx.run_until_parked(); @@ -492,7 +506,7 @@ mod tests { cx.update(|cx| cx.init_colors()); let window = cx.open_window(size(px(400.0), px(600.0)), |window, cx| { - let engine = cx.new(|cx| MockEngine::demo(cx)); + let engine = cx.new(MockEngine::demo); let mut panel = EffectLibraryPanel::new(engine, window, cx); // The state the toggle writes (and the config reloads on the // next start), without touching the process-global key. @@ -546,7 +560,7 @@ mod tests { cx.update(|cx| cx.init_colors()); let window = cx.open_window(size(px(400.0), px(600.0)), |window, cx| { - let engine = cx.new(|cx| MockEngine::demo(cx)); + let engine = cx.new(MockEngine::demo); EffectLibraryPanel::new(engine, window, cx) }); cx.run_until_parked(); diff --git a/crates/oak-app/src/panels/history.rs b/crates/oak-app/src/panels/history.rs index a73d7108b..198e227ac 100644 --- a/crates/oak-app/src/panels/history.rs +++ b/crates/oak-app/src/panels/history.rs @@ -268,7 +268,7 @@ mod tests { ) { cx.update(|cx| cx.init_colors()); let window = cx.open_window(size(px(320.0), px(400.0)), |window, cx| { - let engine = cx.new(|cx| MockEngine::demo(cx)); + let engine = cx.new(MockEngine::demo); HistoryPanel::new(engine, window, cx) }); cx.run_until_parked(); diff --git a/crates/oak-app/src/panels/inspector.rs b/crates/oak-app/src/panels/inspector.rs index 8e14370f4..d1acac314 100644 --- a/crates/oak-app/src/panels/inspector.rs +++ b/crates/oak-app/src/panels/inspector.rs @@ -19,6 +19,7 @@ //! 输出) with add / remove / reorder — no engine, the mock applies the edits //! to its own model. +use crate::oakui::component::menu::{Menu, MenuItem}; use gpui::colors::DefaultColors; use gpui::dock::{DockPanel, PanelEvent}; use gpui::effect_stack::{EffectCardKind, EffectId, EffectStackEvent, EffectStackView}; @@ -26,7 +27,6 @@ use gpui::{ div, prelude::*, AnyElement, App, Context, Entity, EventEmitter, MouseButton, Render, SharedString, Window, }; -use crate::oakui::component::menu::{Menu, MenuItem}; use crate::oakui::component::menu::{ContextMenuHandle, ContextMenuTriggered}; use crate::oakui::effectchain::group_label; @@ -57,15 +57,13 @@ impl InspectorPanel { pub fn new(engine: Entity, window: &mut Window, cx: &mut Context) -> Self { let stack = cx.new(|cx| { let engine_for_params = engine.clone(); - EffectStackView::new(engine.clone(), cx).params_renderer( - move |effect, window, cx| { - // The OFX parameter view: auto-generated controls for the - // effect's inputs (empty state for effects without a - // parameter UI). - cx.new(|cx| OfxParamsView::new(*effect, engine_for_params.clone(), window, cx)) - .into() - }, - ) + EffectStackView::new(engine.clone(), cx).params_renderer(move |effect, window, cx| { + // The OFX parameter view: auto-generated controls for the + // effect's inputs (empty state for effects without a + // parameter UI). + cx.new(|cx| OfxParamsView::new(*effect, engine_for_params.clone(), window, cx)) + .into() + }) }); // The "edits are requests" loop: forward each request to the engine, // which applies it to its model and notifies. `AddRequested` carries @@ -98,7 +96,8 @@ impl InspectorPanel { // The stack re-renders whenever the engine changes (selection-driven // highlight, effect edits, project drops) — the stack itself does not // subscribe to the engine entity. - cx.observe(&engine, |_this, _engine, cx| cx.notify()).detach(); + cx.observe(&engine, |_this, _engine, cx| cx.notify()) + .detach(); let context_menu = ContextMenuHandle::new(Self::on_local_menu_item, window, cx); @@ -174,7 +173,7 @@ impl InspectorPanel { list = list.child( div() .id(SharedString::from(format!("add-effect-group-{key}"))) - .debug_selector(move || format!("add-effect-group-row-{key}").into()) + .debug_selector(move || format!("add-effect-group-row-{key}")) .pt_2() .pb_1() .px_2() diff --git a/crates/oak-app/src/panels/multicam.rs b/crates/oak-app/src/panels/multicam.rs index 35604e6bf..7b0959def 100644 --- a/crates/oak-app/src/panels/multicam.rs +++ b/crates/oak-app/src/panels/multicam.rs @@ -538,7 +538,7 @@ mod tests { async fn panel_renders_the_mock_grid(cx: &mut TestAppContext) { cx.update(|cx| cx.init_colors()); let window = cx.open_window(size(px(640.0), px(360.0)), |window, cx| { - let engine = cx.new(|cx| crate::oakui::MockEngine::demo(cx)); + let engine = cx.new(crate::oakui::MockEngine::demo); let clock = engine.read(cx).program_clock().clone(); MulticamPanel::new(engine, clock, window, cx) }); diff --git a/crates/oak-app/src/panels/node_editor.rs b/crates/oak-app/src/panels/node_editor.rs index f6263320a..759f60527 100644 --- a/crates/oak-app/src/panels/node_editor.rs +++ b/crates/oak-app/src/panels/node_editor.rs @@ -27,6 +27,7 @@ use std::collections::BTreeSet; +use crate::oakui::component::menu::{Menu, MenuItem}; use gpui::colors::DefaultColors; use gpui::dock::{DockPanel, PanelEvent}; use gpui::node_graph::{ @@ -34,13 +35,12 @@ use gpui::node_graph::{ NodeVisualState, MAX_ZOOM, MIN_ZOOM, }; use gpui::{ - div, point, prelude::*, px, AnyElement, App, Bounds, ClickEvent, Context, Entity, - EventEmitter, MouseButton, Pixels, Point, Render, SharedString, Window, + div, point, prelude::*, px, AnyElement, App, Bounds, ClickEvent, Context, Entity, EventEmitter, + MouseButton, Pixels, Point, Render, SharedString, Window, }; -use crate::oakui::component::menu::{Menu, MenuItem}; -use crate::oakui::component::menu::{ContextMenuHandle, ContextMenuTriggered}; use crate::oakui::component::menu; +use crate::oakui::component::menu::{ContextMenuHandle, ContextMenuTriggered}; use crate::oakui::{AppEngine, NodeLibraryEntry}; use crate::panels::commands::PanelCommandHandler; use crate::panels::ids::NODE_EDITOR; @@ -203,8 +203,9 @@ impl NodeEditorPanel { let Some(node) = node else { return; }; - self.graph - .update(cx, |graph, cx| graph.set_selection(BTreeSet::from([NodeId(node)]), cx)); + self.graph.update(cx, |graph, cx| { + graph.set_selection(BTreeSet::from([NodeId(node)]), cx) + }); } /// Handles the node editor's local (non-registry) context-menu items. @@ -221,9 +222,10 @@ impl NodeEditorPanel { .map(|entry| entry.1.clone()); if let Some(type_id) = type_id { let position = self.add_node_position.unwrap_or_default(); - if let Err(err) = self.engine.update(cx, |engine, cx| { - engine.add_node_at(&type_id, position, cx) - }) { + if let Err(err) = self + .engine + .update(cx, |engine, cx| engine.add_node_at(&type_id, position, cx)) + { println!("[node editor] add node failed: {err}"); } } @@ -394,8 +396,7 @@ impl Render for NodeEditorPanel { .graph_position_at(window.mouse_position()); let type_id = payload.type_id.to_string(); this.engine.update(cx, |engine, cx| { - if let Err(err) = engine.add_node_at(&type_id, graph_position, cx) - { + if let Err(err) = engine.add_node_at(&type_id, graph_position, cx) { println!("[node editor] add node failed: {err}"); } }); @@ -508,12 +509,18 @@ pub(crate) fn node_menu(protected: bool) -> Menu { items.push(MenuItem::new(LOCAL_GROUP, tr("node.context.group"))); items.push(MenuItem::new(LOCAL_UNGROUP, tr("node.context.ungroup"))); items.push(menu::color_label_item(None).separated()); - items.push(MenuItem::new(LOCAL_OPEN_IN_VIEWER, tr("node.context.open_in_viewer"))); + items.push(MenuItem::new( + LOCAL_OPEN_IN_VIEWER, + tr("node.context.open_in_viewer"), + )); items.push(MenuItem::new( LOCAL_SHOW_IN_PARAM_EDITOR, tr("node.context.show_in_param_editor"), )); - items.push(MenuItem::new(LOCAL_NODE_PROPERTIES, tr("menu.context.properties"))); + items.push(MenuItem::new( + LOCAL_NODE_PROPERTIES, + tr("menu.context.properties"), + )); Menu::new(items) } @@ -538,27 +545,21 @@ pub(crate) fn background_menu( let mut add_items: Vec = Vec::new(); let mut next_id = LOCAL_ADD_NODE_BASE; for (category_key, mut entries) in groups { - entries.sort_by(|a, b| a.name.to_lowercase().cmp(&b.name.to_lowercase())); + entries.sort_by_key(|a| a.name.to_lowercase()); let mut submenu = Vec::with_capacity(entries.len()); for entry in entries { submenu.push(MenuItem::new(next_id, entry.name.clone())); add_menu_ids.push((next_id, entry.type_id)); next_id += 1; } - add_items.push( - MenuItem::new(0, tr(category_key)).with_submenu(Menu::new(submenu)), - ); + add_items.push(MenuItem::new(0, tr(category_key)).with_submenu(Menu::new(submenu))); } let direction_menu = Menu::new(vec![ - MenuItem::new(LOCAL_DIR_TOP_BOTTOM, tr("node.context.dir_top_bottom")) - .with_checked(true), - MenuItem::new(LOCAL_DIR_BOTTOM_TOP, tr("node.context.dir_bottom_top")) - .with_checked(false), - MenuItem::new(LOCAL_DIR_LEFT_RIGHT, tr("node.context.dir_left_right")) - .with_checked(false), - MenuItem::new(LOCAL_DIR_RIGHT_LEFT, tr("node.context.dir_right_left")) - .with_checked(false), + MenuItem::new(LOCAL_DIR_TOP_BOTTOM, tr("node.context.dir_top_bottom")).with_checked(true), + MenuItem::new(LOCAL_DIR_BOTTOM_TOP, tr("node.context.dir_bottom_top")).with_checked(false), + MenuItem::new(LOCAL_DIR_LEFT_RIGHT, tr("node.context.dir_left_right")).with_checked(false), + MenuItem::new(LOCAL_DIR_RIGHT_LEFT, tr("node.context.dir_right_left")).with_checked(false), ]); Menu::new(vec![ @@ -592,7 +593,7 @@ mod tests { ) { 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)); + let engine = cx.new(crate::oakui::MockEngine::demo); NodeEditorPanel::new(engine, window, cx) }); cx.run_until_parked(); @@ -638,7 +639,9 @@ mod tests { fn node_menu_carries_grouping_and_reveals() { // The color-label lookup below matches on a localized label: pin // en-US under the shared language lock. - let _guard = crate::i18n::lang_test_lock().lock().unwrap_or_else(|e| e.into_inner()); + let _guard = crate::i18n::lang_test_lock() + .lock() + .unwrap_or_else(|e| e.into_inner()); crate::i18n::set_language_code("en-US"); let menu = node_menu(false); let ids: Vec = menu.items.iter().map(|item| item.id).collect(); @@ -761,7 +764,12 @@ mod tests { cx.update(|_window, app| { let engine = panel.read(app).engine.clone(); engine.update(app, |engine, cx| { - engine.apply_effect_event(&EffectStackEvent::CardSelected { effect: EffectId(1) }, cx); + engine.apply_effect_event( + &EffectStackEvent::CardSelected { + effect: EffectId(1), + }, + cx, + ); }); }); cx.run_until_parked(); diff --git a/crates/oak-app/src/panels/ofx_params.rs b/crates/oak-app/src/panels/ofx_params.rs index b12f37d99..ffc32f004 100644 --- a/crates/oak-app/src/panels/ofx_params.rs +++ b/crates/oak-app/src/panels/ofx_params.rs @@ -45,24 +45,24 @@ //! in-progress slider drags); the view observes the engine and re-syncs //! the widget values from the engine snapshot on every render. -use std::sync::Arc; use crate::oakui::component::text_input; +use std::sync::Arc; -use gpui::effect_stack::EffectId; -use gpui::colors::DefaultColors; -use gpui::{ - div, prelude::*, px, rgb, size, point, ClickEvent, Context, Entity, EventEmitter, Focusable, - Render, SharedString, Window, -}; -use gpui::{ - Anchor, App, Bounds, ElementId, Hsla, KeyDownEvent, MouseButton, MouseDownEvent, MouseUpEvent, - Point, Pixels, Rgba, anchored, canvas, deferred, fill, -}; -use gpui_elements::editable_text::{EditableTextState, StringStorage}; use crate::oakui::component::controls::{CheckBox, CheckBoxEvent, CheckState}; use crate::oakui::component::controls::{ComboBox, ComboBoxEvent, ComboBoxOption}; use crate::oakui::component::controls::{Slider, SliderEvent, SliderModel, SliderValue, ValueKind}; use crate::oakui::component::controls::{SpinBox, SpinBoxEvent}; +use gpui::colors::DefaultColors; +use gpui::effect_stack::EffectId; +use gpui::{ + anchored, canvas, deferred, fill, Anchor, App, Bounds, ElementId, Hsla, KeyDownEvent, + MouseButton, MouseDownEvent, MouseUpEvent, Pixels, Point, Rgba, +}; +use gpui::{ + div, point, prelude::*, px, rgb, size, ClickEvent, Context, Entity, EventEmitter, Focusable, + Render, SharedString, Window, +}; +use gpui_elements::editable_text::{EditableTextState, StringStorage}; use oak_node::value::{NodeValue, ValueType}; @@ -157,7 +157,11 @@ impl OfxParamsView { /// Applies the engine's current values to every control (called each /// render so external edits / undo / redo land on the controls). fn sync_values(&mut self, window: &Window, cx: &mut Context) { - let params = self.engine.read(cx).effect_params(self.effect).unwrap_or_default(); + let params = self + .engine + .read(cx) + .effect_params(self.effect) + .unwrap_or_default(); for control in &self.controls { let Some(param) = params.iter().find(|p| p.input_id == control.input_id) else { continue; @@ -336,10 +340,13 @@ fn curve_point_to_editor( let h = (next.key - p.key) / sx; CurveVec2::new(h / 3.0, m_norm * h / 3.0) }); - let handle_in = index.checked_sub(1).and_then(|pi| points.get(pi)).map(|prev| { - let h = (p.key - prev.key) / sx; - CurveVec2::new(-h / 3.0, -m_norm * h / 3.0) - }); + let handle_in = index + .checked_sub(1) + .and_then(|pi| points.get(pi)) + .map(|prev| { + let h = (p.key - prev.key) / sx; + CurveVec2::new(-h / 3.0, -m_norm * h / 3.0) + }); CurvePoint { x, y, @@ -396,11 +403,19 @@ fn curve_from_editor( (Some(pi), None) if n > 1 => { let prev = &points[pi]; let dx = p.x - prev.x; - if dx.abs() > 1e-9 { (p.y - prev.y) / dx } else { 0.0 } + if dx.abs() > 1e-9 { + (p.y - prev.y) / dx + } else { + 0.0 + } } (None, Some(next)) if n > 1 => { let dx = next.x - p.x; - if dx.abs() > 1e-9 { (next.y - p.y) / dx } else { 0.0 } + if dx.abs() > 1e-9 { + (next.y - p.y) / dx + } else { + 0.0 + } } _ => 0.0, } @@ -421,9 +436,8 @@ fn curve_points_close( b: &[gpui_widgets::curve_editor::CurvePoint], ) -> bool { use gpui_widgets::curve_editor::CurveVec2; - let vec_close = |a: &CurveVec2, b: &CurveVec2| { - (a.x - b.x).abs() < 1e-6 && (a.y - b.y).abs() < 1e-6 - }; + let vec_close = + |a: &CurveVec2, b: &CurveVec2| (a.x - b.x).abs() < 1e-6 && (a.y - b.y).abs() < 1e-6; let handle_close = |a: &Option, b: &Option| match (a, b) { (None, None) => true, (Some(a), Some(b)) => vec_close(a, b), @@ -431,10 +445,8 @@ fn curve_points_close( }; a.len() == b.len() && a.iter().zip(b.iter()).all(|(a, b)| { - vec_close( - &CurveVec2::new(a.x, a.y), - &CurveVec2::new(b.x, b.y), - ) && handle_close(&a.handle_in, &b.handle_in) + vec_close(&CurveVec2::new(a.x, a.y), &CurveVec2::new(b.x, b.y)) + && handle_close(&a.handle_in, &b.handle_in) && handle_close(&a.handle_out, &b.handle_out) }) } @@ -582,7 +594,11 @@ fn slider_range_and_step(param: &EffectParam) -> (f64, f64, f64) { match (range_property(param, "min"), range_property(param, "max")) { // A floor but no ceiling. (Some(min), None) => { - let v = if value.is_finite() { value.max(min) } else { min }; + let v = if value.is_finite() { + value.max(min) + } else { + min + }; let span = v - min; let step = if span > 0.0 { nice_grid_step(span) @@ -593,7 +609,11 @@ fn slider_range_and_step(param: &EffectParam) -> (f64, f64, f64) { } // A ceiling but no floor: the mirror image. (None, Some(max)) => { - let v = if value.is_finite() { value.min(max) } else { max }; + let v = if value.is_finite() { + value.min(max) + } else { + max + }; let span = max - v; let step = if span > 0.0 { nice_grid_step(span) @@ -758,7 +778,11 @@ fn build_control( let (min, max) = default_range(param.value_type); let mut spins = Vec::new(); for channel in 0..count { - let value = components.get(channel).copied().unwrap_or(0.0).clamp(min, max); + let value = components + .get(channel) + .copied() + .unwrap_or(0.0) + .clamp(min, max); let model = SliderModel::new(ValueKind::Float, min, max, 0.001, value); let spin = cx.new(|cx| SpinBox::new(*next_id, model, window, cx)); *next_id += 1; @@ -771,7 +795,7 @@ fn build_control( // always 0..1 regardless of any attached min/max). let components = value_components(¶m.value); let color = Rgba { - r: components.get(0).copied().unwrap_or(0.0) as f32, + r: components.first().copied().unwrap_or(0.0) as f32, g: components.get(1).copied().unwrap_or(0.0) as f32, b: components.get(2).copied().unwrap_or(0.0) as f32, a: components.get(3).copied().unwrap_or(1.0) as f32, @@ -785,8 +809,9 @@ fn build_control( // One curve editor per dimension; points are seeded from the // JSON mirror of the curves (the input's Text value). let curves = match ¶m.value { - NodeValue::Text(json) => oak_plugin::param_curve::curves_from_json(json) - .unwrap_or_default(), + NodeValue::Text(json) => { + oak_plugin::param_curve::curves_from_json(json).unwrap_or_default() + } _ => Vec::new(), }; let domain = curve_domain(param, &curves); @@ -835,17 +860,24 @@ fn wire_controls(view: &OfxParamsView, cx: &mut Context { let slider = slider.clone(); - cx.subscribe(&slider, move |_, _, event: &crate::oakui::component::controls::SliderEvent, cx| { - if let crate::oakui::component::controls::SliderEvent::ValueChanged { value, .. } = event { - let nv = match value { - SliderValue::Integer(v) => NodeValue::Int(*v), - _ => NodeValue::Float(value.to_f64()), - }; - engine.update(cx, |engine, cx| { - let _ = engine.set_effect_param(effect, &input_id, nv, cx); - }); - } - }) + cx.subscribe( + &slider, + move |_, _, event: &crate::oakui::component::controls::SliderEvent, cx| { + if let crate::oakui::component::controls::SliderEvent::ValueChanged { + value, + .. + } = event + { + let nv = match value { + SliderValue::Integer(v) => NodeValue::Int(*v), + _ => NodeValue::Float(value.to_f64()), + }; + engine.update(cx, |engine, cx| { + let _ = engine.set_effect_param(effect, &input_id, nv, cx); + }); + } + }, + ) .detach(); } ControlKind::CheckBox(check) => { @@ -875,9 +907,7 @@ fn wire_controls(view: &OfxParamsView, cx: &mut Context { let haystack = combo_haystack(p); - NodeValue::StrCombo( - haystack.get(*value).cloned().unwrap_or_default(), - ) + NodeValue::StrCombo(haystack.get(*value).cloned().unwrap_or_default()) } _ => NodeValue::Combo(*value as i64), }; @@ -908,7 +938,9 @@ fn wire_controls(view: &OfxParamsView, cx: &mut Context(view: &OfxParamsView, cx: &mut Context { let picker = picker.clone(); - cx.subscribe(&picker, move |_, _, event: &OfxColorEvent, cx| match event { - // Only OK commits; slider drags update the draft inside - // the picker, so a drag session is one undo row. - OfxColorEvent::Committed(color) => { - let nv = NodeValue::Color([ - color.r as f64, - color.g as f64, - color.b as f64, - color.a as f64, - ]); - engine.update(cx, |engine, cx| { - if let Err(err) = engine.set_effect_param(effect, &input_id, nv, cx) { - // A failed set is the only path that snaps the - // swatch back to the old engine value (the value - // sync re-reads it every frame), so log it rather - // than swallowing it. - println!("[ofx params] set colour param {input_id:?} failed: {err}"); - } - }); - } - // Arming the picker points the program viewer's cursor at - // the frame; the next click there lands in the draft. - OfxColorEvent::PickViewerToggle { armed } => { - engine.update(cx, |engine, cx| { - engine.set_eyedropper_armed(*armed, cx); - }); - } - // Open/close are purely local to the popup. - OfxColorEvent::Opened | OfxColorEvent::Cancelled => {} - }) + cx.subscribe( + &picker, + move |_, _, event: &OfxColorEvent, cx| match event { + // Only OK commits; slider drags update the draft inside + // the picker, so a drag session is one undo row. + OfxColorEvent::Committed(color) => { + let nv = NodeValue::Color([ + color.r as f64, + color.g as f64, + color.b as f64, + color.a as f64, + ]); + engine.update(cx, |engine, cx| { + if let Err(err) = engine.set_effect_param(effect, &input_id, nv, cx) + { + // A failed set is the only path that snaps the + // swatch back to the old engine value (the value + // sync re-reads it every frame), so log it rather + // than swallowing it. + println!( + "[ofx params] set colour param {input_id:?} failed: {err}" + ); + } + }); + } + // Arming the picker points the program viewer's cursor at + // the frame; the next click there lands in the draft. + OfxColorEvent::PickViewerToggle { armed } => { + engine.update(cx, |engine, cx| { + engine.set_eyedropper_armed(*armed, cx); + }); + } + // Open/close are purely local to the popup. + OfxColorEvent::Opened | OfxColorEvent::Cancelled => {} + }, + ) .detach(); } ControlKind::Text { .. } => { @@ -967,28 +1005,33 @@ fn wire_controls(view: &OfxParamsView, cx: &mut Context {} - } - let curves: Vec = editors_all - .iter() - .map(|e| { - let points = e.read(cx).points().to_vec(); - curve_from_editor(&points, domain) - }) - .collect(); - let json = oak_plugin::param_curve::curves_to_json(&curves); - engine.update(cx, |engine, cx| { - let _ = engine.set_effect_param( - effect, - &input_id, - NodeValue::Text(json), - cx, - ); - }); - }) + cx.subscribe( + &editor, + move |_, _, event: &gpui_widgets::curve_editor::CurveEditorEvent, cx| { + use gpui_widgets::curve_editor::CurveEditorEvent as E; + match event { + E::PointMoved { .. } + | E::HandleMoved { .. } + | E::PointAdded { .. } => {} + } + let curves: Vec = editors_all + .iter() + .map(|e| { + let points = e.read(cx).points().to_vec(); + curve_from_editor(&points, domain) + }) + .collect(); + let json = oak_plugin::param_curve::curves_to_json(&curves); + engine.update(cx, |engine, cx| { + let _ = engine.set_effect_param( + effect, + &input_id, + NodeValue::Text(json), + cx, + ); + }); + }, + ) .detach(); } } @@ -1074,9 +1117,15 @@ impl Render for OfxParamsView { .text_color(colors.text) .child(control.display_name.clone()); let widget = match &control.kind { - ControlKind::Slider(slider) => div().flex_1().child(slider.clone()).into_any_element(), - ControlKind::CheckBox(check) => div().flex_1().child(check.clone()).into_any_element(), - ControlKind::Combo(combo) => div().flex_1().child(combo.clone()).into_any_element(), + ControlKind::Slider(slider) => { + div().flex_1().child(slider.clone()).into_any_element() + } + ControlKind::CheckBox(check) => { + div().flex_1().child(check.clone()).into_any_element() + } + ControlKind::Combo(combo) => { + div().flex_1().child(combo.clone()).into_any_element() + } ControlKind::Spin(spins) => { let mut row = div().flex_1().flex().gap_1(); for (spin, _) in spins { @@ -1138,7 +1187,10 @@ impl Render for OfxParamsView { // string to the engine (avoids the re-render loop of // committing on every keystroke). div() - .id(SharedString::from(format!("ofx-commit-{}", control.input_id))) + .id(SharedString::from(format!( + "ofx-commit-{}", + control.input_id + ))) .cursor_pointer() .rounded_sm() .border_1() @@ -1152,8 +1204,12 @@ 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(effect, &input_id, NodeValue::Text(text), cx); + let _ = engine.set_effect_param( + effect, + &input_id, + NodeValue::Text(text), + cx, + ); }); }), ) @@ -1185,7 +1241,10 @@ impl Render for OfxParamsView { ControlKind::PushButton => unreachable!("handled above"), }; div() - .id(SharedString::from(format!("ofx-param-{}", control.input_id))) + .id(SharedString::from(format!( + "ofx-param-{}", + control.input_id + ))) .flex() .items_center() .gap_2() @@ -1358,7 +1417,7 @@ impl OfxColorPicker { 1 => self.apply_draft_rgb(self.draft.r, v, self.draft.b, cx), 2 => self.apply_draft_rgb(self.draft.r, self.draft.g, v, cx), 3 => self.apply_draft_alpha(v, cx), - _ => return, + _ => (), } } ColorMode::Hsv => { @@ -1487,10 +1546,7 @@ impl OfxColorPicker { } }; let sliders = [&self.c0, &self.c1, &self.c2]; - for (slider, (range, value)) in sliders - .into_iter() - .zip(ranges.iter().zip(vals.iter())) - { + for (slider, (range, value)) in sliders.into_iter().zip(ranges.iter().zip(vals.iter())) { let (min, max, step) = *range; let value = *value; let slider = slider.clone(); @@ -1521,7 +1577,9 @@ impl OfxColorPicker { /// every frame, so this picker stays in sync with the armed state. fn toggle_viewer_pick(&mut self, cx: &mut Context) { self.picking = !self.picking; - cx.emit(OfxColorEvent::PickViewerToggle { armed: self.picking }); + cx.emit(OfxColorEvent::PickViewerToggle { + armed: self.picking, + }); cx.notify(); } @@ -1618,14 +1676,21 @@ impl OfxColorPicker { div() .id(SharedString::from(format!( "ofx-color-mode-{control}-{}", - if this_mode == ColorMode::Rgb { "rgb" } else { "hsv" } + if this_mode == ColorMode::Rgb { + "rgb" + } else { + "hsv" + } ))) .debug_selector(move || { format!( "ofx-color-mode-{control}-{}", - if this_mode == ColorMode::Rgb { "rgb" } else { "hsv" } + if this_mode == ColorMode::Rgb { + "rgb" + } else { + "hsv" + } ) - .into() }) .cursor_pointer() .flex_1() @@ -1652,12 +1717,17 @@ impl OfxColorPicker { this.set_mode(this_mode, cx); })) }; - let tab_row = div().flex().gap_1().child(tab(ColorMode::Rgb)).child(tab(ColorMode::Hsv)); + let tab_row = div() + .flex() + .gap_1() + .child(tab(ColorMode::Rgb)) + .child(tab(ColorMode::Hsv)); // Photoshop-style S/V palette + hue bar. Both record their layout // bounds each frame (an invisible canvas) so a click / drag can map // the cursor to a value. - let sv_bounds = std::sync::Arc::new(std::sync::RwLock::new(None::>)); + let sv_bounds = + std::sync::Arc::new(std::sync::RwLock::new(None::>)); let sv_drag = std::sync::Arc::new(std::sync::RwLock::new(SvPaletteDrag)); let (hue, _, _) = rgb_to_hsv(draft.r, draft.g, draft.b); let record_sv = sv_bounds.clone(); @@ -1680,41 +1750,39 @@ impl OfxColorPicker { .border_color(colors.border) .overflow_hidden() .cursor_pointer() - .on_mouse_down( - MouseButton::Left, - { - let sv_bounds = sv_bounds.clone(); - cx.listener(move |this, event: &MouseDownEvent, _window, cx| { - let Some(bounds) = sv_bounds.read().unwrap().as_ref().copied() else { - return; - }; - let (s, v) = sv_from_point(bounds, event.position); - this.on_palette(s, v, cx); - }) - }, - ) + .on_mouse_down(MouseButton::Left, { + let sv_bounds = sv_bounds.clone(); + cx.listener(move |this, event: &MouseDownEvent, _window, cx| { + let Some(bounds) = sv_bounds.read().unwrap().as_ref().copied() else { + return; + }; + let (s, v) = sv_from_point(bounds, event.position); + this.on_palette(s, v, cx); + }) + }) .on_drag(sv_drag.clone(), |_payload, _offset, _window, cx| { cx.new(|_| SvPaletteDragGhost) }) - .on_drag_move( - { - let sv_bounds = sv_bounds.clone(); - cx.listener( - move |this, - event: &gpui::DragMoveEvent>>, - _window, - cx| { - let Some(bounds) = sv_bounds.read().unwrap().as_ref().copied() else { - return; - }; - let (s, v) = sv_from_point(bounds, event.event.position); - this.on_palette(s, v, cx); - }, - ) - }, - ) + .on_drag_move({ + let sv_bounds = sv_bounds.clone(); + cx.listener( + move |this, + event: &gpui::DragMoveEvent< + std::sync::Arc>, + >, + _window, + cx| { + let Some(bounds) = sv_bounds.read().unwrap().as_ref().copied() else { + return; + }; + let (s, v) = sv_from_point(bounds, event.event.position); + this.on_palette(s, v, cx); + }, + ) + }) .child(sv_canvas); - let hue_bounds = std::sync::Arc::new(std::sync::RwLock::new(None::>)); + let hue_bounds = + std::sync::Arc::new(std::sync::RwLock::new(None::>)); let hue_drag = std::sync::Arc::new(std::sync::RwLock::new(HueBarDrag)); let record_hue = hue_bounds.clone(); let hue_canvas = canvas( @@ -1736,37 +1804,34 @@ impl OfxColorPicker { .border_color(colors.border) .overflow_hidden() .cursor_pointer() - .on_mouse_down( - MouseButton::Left, - { - let hue_bounds = hue_bounds.clone(); - cx.listener(move |this, event: &MouseDownEvent, _window, cx| { - let Some(bounds) = hue_bounds.read().unwrap().as_ref().copied() else { - return; - }; - this.on_hue(hue_from_point(bounds, event.position), cx); - }) - }, - ) + .on_mouse_down(MouseButton::Left, { + let hue_bounds = hue_bounds.clone(); + cx.listener(move |this, event: &MouseDownEvent, _window, cx| { + let Some(bounds) = hue_bounds.read().unwrap().as_ref().copied() else { + return; + }; + this.on_hue(hue_from_point(bounds, event.position), cx); + }) + }) .on_drag(hue_drag.clone(), |_payload, _offset, _window, cx| { cx.new(|_| HueBarDragGhost) }) - .on_drag_move( - { - let hue_bounds = hue_bounds.clone(); - cx.listener( - move |this, - event: &gpui::DragMoveEvent>>, - _window, - cx| { - let Some(bounds) = hue_bounds.read().unwrap().as_ref().copied() else { - return; - }; - this.on_hue(hue_from_point(bounds, event.event.position), cx); - }, - ) - }, - ) + .on_drag_move({ + let hue_bounds = hue_bounds.clone(); + cx.listener( + move |this, + event: &gpui::DragMoveEvent< + std::sync::Arc>, + >, + _window, + cx| { + let Some(bounds) = hue_bounds.read().unwrap().as_ref().copied() else { + return; + }; + this.on_hue(hue_from_point(bounds, event.event.position), cx); + }, + ) + }) .child(hue_canvas); let palette_row = div().flex().gap_1().child(sv_panel).child(hue_bar); @@ -1810,26 +1875,21 @@ impl OfxColorPicker { .border_color(colors.border) .overflow_hidden() .child(preview_canvas); - let hex_row = div() - .flex() - .items_center() - .gap_1() - .child(preview) - .child( - div() - .flex_1() - .rounded_md() - .border_1() - .border_color(colors.border) - .bg(colors.background) - .px_2() - .py_1() - .child( - text_input(format!("ofx-color-hex-{control}"), cx) - .state(hex_weak) - .accepts_input(true), - ), - ); + let hex_row = div().flex().items_center().gap_1().child(preview).child( + div() + .flex_1() + .rounded_md() + .border_1() + .border_color(colors.border) + .bg(colors.background) + .px_2() + .py_1() + .child( + text_input(format!("ofx-color-hex-{control}"), cx) + .state(hex_weak) + .accepts_input(true), + ), + ); // Hex parse error hint. let error_hint = if hex_error { @@ -1873,18 +1933,29 @@ impl OfxColorPicker { .on_click(cx.listener(|this, _event: &ClickEvent, _window, cx| { this.commit(cx); })); - let buttons = div().flex().justify_between().gap_1().child(cancel).child(ok); + let buttons = div() + .flex() + .justify_between() + .gap_1() + .child(cancel) + .child(ok); // "Pick from viewer": arms the eyedropper in the program viewer; the // next click on the frame samples the pixel under the cursor. let pick_viewer = div() - .id(SharedString::from(format!("ofx-color-pick-viewer-{control}"))) - .debug_selector(move || format!("ofx-color-pick-viewer-{control}").into()) + .id(SharedString::from(format!( + "ofx-color-pick-viewer-{control}" + ))) + .debug_selector(move || format!("ofx-color-pick-viewer-{control}")) .cursor_pointer() .rounded_sm() .border_1() .border_color(colors.border) - .bg(if self.picking { colors.selected } else { colors.background }) + .bg(if self.picking { + colors.selected + } else { + colors.background + }) .text_sm() .text_color(colors.text) .px_2() @@ -1901,53 +1972,53 @@ impl OfxColorPicker { .offset(point(px(0.0), px(36.0))) .snap_to_window_with_margin(px(8.0)) .child( - div() - .w(px(300.0)) - .p_2() - .rounded_lg() - .border_1() - .border_color(colors.border) - .bg(colors.container) - .flex() - .flex_col() - .gap_1() - .debug_selector(|| "ofx-color-popup".into()) - .on_mouse_up_out( - MouseButton::Left, - cx.listener(|this, _event: &MouseUpEvent, _window, cx| { - // While the viewer eyedropper is armed, the click - // that follows is a *pick* on the program viewer, - // not an outside-click dismissal: the popup must - // survive it so the sampled colour lands in the - // draft. The pick lands mid-click (before the - // button comes up) and disarms the eyedropper, so - // its mouse-up is swallowed once explicitly. - if this.swallow_next_outside_up { - this.swallow_next_outside_up = false; - } else if !this.picking { - this.close_menu(cx); + div() + .w(px(300.0)) + .p_2() + .rounded_lg() + .border_1() + .border_color(colors.border) + .bg(colors.container) + .flex() + .flex_col() + .gap_1() + .debug_selector(|| "ofx-color-popup".into()) + .on_mouse_up_out( + MouseButton::Left, + cx.listener(|this, _event: &MouseUpEvent, _window, cx| { + // While the viewer eyedropper is armed, the click + // that follows is a *pick* on the program viewer, + // not an outside-click dismissal: the popup must + // survive it so the sampled colour lands in the + // draft. The pick lands mid-click (before the + // button comes up) and disarms the eyedropper, so + // its mouse-up is swallowed once explicitly. + if this.swallow_next_outside_up { + this.swallow_next_outside_up = false; + } else if !this.picking { + this.close_menu(cx); + } + }), + ) + .on_key_down(cx.listener(|this, event: &KeyDownEvent, _window, cx| { + if event.keystroke.key == "escape" { + if this.picking { + this.toggle_viewer_pick(cx); + } else { + this.close_menu(cx); + } } - }), - ) - .on_key_down(cx.listener(|this, event: &KeyDownEvent, _window, cx| { - if event.keystroke.key == "escape" { - if this.picking { - this.toggle_viewer_pick(cx); - } else { - this.close_menu(cx); - } - } - })) - .child(tab_row) - .child(palette_row) - .child(slider_rows) - .child(hex_row) - .child(error_hint) - .child(buttons) - .child(pick_viewer), + })) + .child(tab_row) + .child(palette_row) + .child(slider_rows) + .child(hex_row) + .child(error_hint) + .child(buttons) + .child(pick_viewer), ), - ) - .with_priority(1) + ) + .with_priority(1) } } @@ -1959,7 +2030,11 @@ impl Render for OfxColorPicker { let control = self.control; // While the popup is open the swatch follows the draft live (the // user's in-progress edit), otherwise it shows the committed value. - let swatch_color = if self.open { self.draft } else { self.committed }; + let swatch_color = if self.open { + self.draft + } else { + self.committed + }; let swatch = div() .id(ElementId::named_usize("ofx-color-swatch", control)) @@ -1989,13 +2064,15 @@ impl Render for OfxColorPicker { } cx.stop_propagation(); })) - .child(canvas( - |bounds, _window, _cx| bounds, - move |bounds, _content, window, cx| { - paint_checker_swatch(bounds, swatch_color, window, cx); - }, - ) - .size_full()); + .child( + canvas( + |bounds, _window, _cx| bounds, + move |bounds, _content, window, cx| { + paint_checker_swatch(bounds, swatch_color, window, cx); + }, + ) + .size_full(), + ); let popup = if self.open { self.popup_anchored(cx, &colors) @@ -2045,12 +2122,7 @@ fn format_hex(color: Rgba) -> String { /// Paint a two-tone checkerboard (8 px cells) with `color` over it — the /// alpha channel reads through the checkerboard. -fn paint_checker_swatch( - bounds: Bounds, - color: Rgba, - window: &mut Window, - _cx: &mut App, -) { +fn paint_checker_swatch(bounds: Bounds, color: Rgba, window: &mut Window, _cx: &mut App) { const CELL: f32 = 8.0; let width = f32::from(bounds.size.width); let height = f32::from(bounds.size.height); @@ -2276,13 +2348,13 @@ mod tests { #[test] fn hex_parse_rejects_malformed() { for bad in [ - "", // empty - "102030", // missing '#' - "#12345", // too short - "#1234567", // 7 digits - "#GGHHII", // non-hex digits - "#123456789", // too long - "# 123456", // whitespace inside + "", // empty + "102030", // missing '#' + "#12345", // too short + "#1234567", // 7 digits + "#GGHHII", // non-hex digits + "#123456789", // too long + "# 123456", // whitespace inside ] { assert!(parse_hex(bad).is_none(), "expected {bad:?} to be rejected"); } @@ -2296,7 +2368,11 @@ mod tests { picker: Entity, } impl Render for Host { - fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { + fn render( + &mut self, + _window: &mut Window, + _cx: &mut Context, + ) -> impl IntoElement { div().size_full().child(self.picker.clone()) } } @@ -2364,7 +2440,9 @@ mod tests { // Open the popup by clicking the swatch, then re-draw so the popup's // deferred layer is laid out (and the palette canvas records its // bounds for the click mapping). - let swatch = visual.debug_bounds("ofx-color-swatch").expect("swatch painted"); + let swatch = visual + .debug_bounds("ofx-color-swatch") + .expect("swatch painted"); let swatch_center = Point::new( swatch.origin.x + swatch.size.width * 0.5, swatch.origin.y + swatch.size.height * 0.5, @@ -2415,7 +2493,11 @@ mod tests { _subscription: Subscription, } impl Render for Host { - fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { + fn render( + &mut self, + _window: &mut Window, + _cx: &mut Context, + ) -> impl IntoElement { div().size_full().child(self.picker.clone()) } } @@ -2437,9 +2519,12 @@ mod tests { }); let events = Arc::new(Mutex::new(Vec::new())); let events_sub = events.clone(); - let _subscription = cx.subscribe(&picker, move |_this, _emitter, event: &OfxColorEvent, _cx| { - events_sub.lock().unwrap().push(*event); - }); + let _subscription = cx.subscribe( + &picker, + move |_this, _emitter, event: &OfxColorEvent, _cx| { + events_sub.lock().unwrap().push(*event); + }, + ); Host { picker, events, @@ -2455,7 +2540,9 @@ mod tests { // Open the popup by clicking the swatch, then re-draw so the popup's // deferred layer is laid out. - let swatch = visual.debug_bounds("ofx-color-swatch").expect("swatch painted"); + let swatch = visual + .debug_bounds("ofx-color-swatch") + .expect("swatch painted"); let swatch_center = Point::new( swatch.origin.x + swatch.size.width * 0.5, swatch.origin.y + swatch.size.height * 0.5, @@ -2478,7 +2565,10 @@ mod tests { let host = window.root(cx).expect("host root"); let events = cx.read(|cx| host.read(cx).events.lock().unwrap().clone()); assert!( - matches!(events.last(), Some(OfxColorEvent::PickViewerToggle { armed: true })), + matches!( + events.last(), + Some(OfxColorEvent::PickViewerToggle { armed: true }) + ), "clicking pick should arm the eyedropper, got {events:?}" ); @@ -2487,7 +2577,10 @@ mod tests { cx.run_until_parked(); let events = cx.read(|cx| host.read(cx).events.lock().unwrap().clone()); assert!( - matches!(events.last(), Some(OfxColorEvent::PickViewerToggle { armed: false })), + matches!( + events.last(), + Some(OfxColorEvent::PickViewerToggle { armed: false }) + ), "second click should disarm the eyedropper, got {events:?}" ); } @@ -2505,7 +2598,11 @@ mod tests { _subscription: Subscription, } impl Render for Host { - fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { + fn render( + &mut self, + _window: &mut Window, + _cx: &mut Context, + ) -> impl IntoElement { div().size_full().child(self.picker.clone()) } } @@ -2527,9 +2624,12 @@ mod tests { }); let events = Arc::new(Mutex::new(Vec::new())); let events_sub = events.clone(); - let _subscription = cx.subscribe(&picker, move |_this, _emitter, event: &OfxColorEvent, _cx| { - events_sub.lock().unwrap().push(*event); - }); + let _subscription = cx.subscribe( + &picker, + move |_this, _emitter, event: &OfxColorEvent, _cx| { + events_sub.lock().unwrap().push(*event); + }, + ); Host { picker, events, @@ -2545,7 +2645,9 @@ mod tests { // Open the popup by clicking the swatch, then re-draw so the popup's // deferred layer is laid out. - let swatch = visual.debug_bounds("ofx-color-swatch").expect("swatch painted"); + let swatch = visual + .debug_bounds("ofx-color-swatch") + .expect("swatch painted"); let swatch_center = Point::new( swatch.origin.x + swatch.size.width * 0.5, swatch.origin.y + swatch.size.height * 0.5, @@ -2591,7 +2693,10 @@ mod tests { .any(|e| matches!(e, OfxColorEvent::Cancelled)), ) }); - assert!(open, "the popup must survive an outside click while picking"); + assert!( + open, + "the popup must survive an outside click while picking" + ); assert!(picking, "the eyedropper stays armed through the pick"); assert!(!cancelled, "no dismissal may be emitted while picking"); @@ -2672,7 +2777,11 @@ mod tests { _subscription: Subscription, } impl Render for Host { - fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { + fn render( + &mut self, + _window: &mut Window, + _cx: &mut Context, + ) -> impl IntoElement { div().size_full().child(self.picker.clone()) } } @@ -2694,9 +2803,12 @@ mod tests { }); let events = Arc::new(Mutex::new(Vec::new())); let events_sub = events.clone(); - let _subscription = cx.subscribe(&picker, move |_this, _emitter, event: &OfxColorEvent, _cx| { - events_sub.lock().unwrap().push(*event); - }); + let _subscription = cx.subscribe( + &picker, + move |_this, _emitter, event: &OfxColorEvent, _cx| { + events_sub.lock().unwrap().push(*event); + }, + ); Host { picker, events, @@ -2711,7 +2823,9 @@ mod tests { }); // Open the popup, re-draw for the deferred layer, arm the eyedropper. - let swatch = visual.debug_bounds("ofx-color-swatch").expect("swatch painted"); + let swatch = visual + .debug_bounds("ofx-color-swatch") + .expect("swatch painted"); visual.simulate_click( Point::new( swatch.origin.x + swatch.size.width * 0.5, @@ -2746,7 +2860,8 @@ mod tests { let host = window.root(cx).expect("host root"); visual.update(|_window, cx| { host.update(cx, |host, cx| { - host.picker.update(cx, |picker, cx| picker.apply_viewer_pick(red, cx)); + host.picker + .update(cx, |picker, cx| picker.apply_viewer_pick(red, cx)); }); }); let popup = visual @@ -2800,18 +2915,90 @@ mod tests { let close = |a: f32, b: f32| (a - b).abs() < 1e-4; // The six canonical corners of the RGB cube. - assert_eq!(hsv(Rgba { r: 1.0, g: 0.0, b: 0.0, a: 1.0 }), (0.0, 1.0, 1.0)); - assert_eq!(hsv(Rgba { r: 0.0, g: 1.0, b: 0.0, a: 1.0 }), (120.0, 1.0, 1.0)); - assert_eq!(hsv(Rgba { r: 0.0, g: 0.0, b: 1.0, a: 1.0 }), (240.0, 1.0, 1.0)); - assert_eq!(hsv(Rgba { r: 1.0, g: 1.0, b: 0.0, a: 1.0 }), (60.0, 1.0, 1.0)); - assert_eq!(hsv(Rgba { r: 1.0, g: 1.0, b: 1.0, a: 1.0 }), (0.0, 0.0, 1.0)); - assert_eq!(hsv(Rgba { r: 0.0, g: 0.0, b: 0.0, a: 1.0 }), (0.0, 0.0, 0.0)); + assert_eq!( + hsv(Rgba { + r: 1.0, + g: 0.0, + b: 0.0, + a: 1.0 + }), + (0.0, 1.0, 1.0) + ); + assert_eq!( + hsv(Rgba { + r: 0.0, + g: 1.0, + b: 0.0, + a: 1.0 + }), + (120.0, 1.0, 1.0) + ); + assert_eq!( + hsv(Rgba { + r: 0.0, + g: 0.0, + b: 1.0, + a: 1.0 + }), + (240.0, 1.0, 1.0) + ); + assert_eq!( + hsv(Rgba { + r: 1.0, + g: 1.0, + b: 0.0, + a: 1.0 + }), + (60.0, 1.0, 1.0) + ); + assert_eq!( + hsv(Rgba { + r: 1.0, + g: 1.0, + b: 1.0, + a: 1.0 + }), + (0.0, 0.0, 1.0) + ); + assert_eq!( + hsv(Rgba { + r: 0.0, + g: 0.0, + b: 0.0, + a: 1.0 + }), + (0.0, 0.0, 0.0) + ); // The same corners back to RGB. let rgb = |h: f32, s: f32, v: f32| hsv_to_rgb(h, s, v); - assert_eq!(rgb(0.0, 1.0, 1.0), Rgba { r: 1.0, g: 0.0, b: 0.0, a: 1.0 }); - assert_eq!(rgb(120.0, 1.0, 1.0), Rgba { r: 0.0, g: 1.0, b: 0.0, a: 1.0 }); - assert_eq!(rgb(240.0, 1.0, 1.0), Rgba { r: 0.0, g: 0.0, b: 1.0, a: 1.0 }); + assert_eq!( + rgb(0.0, 1.0, 1.0), + Rgba { + r: 1.0, + g: 0.0, + b: 0.0, + a: 1.0 + } + ); + assert_eq!( + rgb(120.0, 1.0, 1.0), + Rgba { + r: 0.0, + g: 1.0, + b: 0.0, + a: 1.0 + } + ); + assert_eq!( + rgb(240.0, 1.0, 1.0), + Rgba { + r: 0.0, + g: 0.0, + b: 1.0, + a: 1.0 + } + ); // Arbitrary RGB round-trips. for (r, g, b) in [(0.5, 0.25, 0.75), (0.1, 0.9, 0.2), (0.33, 0.67, 0.44)] { @@ -2866,14 +3053,19 @@ mod tests { view: Entity>, } impl Render for Host { - fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { + fn render( + &mut self, + _window: &mut Window, + _cx: &mut Context, + ) -> impl IntoElement { div().size_full().child(self.view.clone()) } } cx.update(|cx| cx.init_colors()); let window = cx.open_window(size(px(400.0), px(200.0)), |window, cx| { - let engine = cx.new(|cx| MockEngine::create(cx)); - let view = cx.new(|cx| OfxParamsView::::new(EffectId(900), engine, window, cx)); + let engine = cx.new(MockEngine::create); + let view = + cx.new(|cx| OfxParamsView::::new(EffectId(900), engine, window, cx)); Host { view } }); cx.run_until_parked(); @@ -2953,14 +3145,19 @@ mod tests { view: Entity>, } impl Render for Host { - fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { + fn render( + &mut self, + _window: &mut Window, + _cx: &mut Context, + ) -> impl IntoElement { div().size_full().child(self.view.clone()) } } cx.update(|cx| cx.init_colors()); let window = cx.open_window(size(px(400.0), px(600.0)), |window, cx| { - let engine = cx.new(|cx| MockEngine::create(cx)); - let view = cx.new(|cx| OfxParamsView::::new(EffectId(900), engine, window, cx)); + let engine = cx.new(MockEngine::create); + let view = + cx.new(|cx| OfxParamsView::::new(EffectId(900), engine, window, cx)); Host { view } }); cx.run_until_parked(); @@ -3023,13 +3220,17 @@ mod tests { } ControlKind::CheckBox(_) => "checkbox", ControlKind::Combo(combo) => { - snap.combos.push((control.input_id.clone(), combo.read(cx).selected())); + snap.combos + .push((control.input_id.clone(), combo.read(cx).selected())); "combo" } ControlKind::Spin(spins) => { snap.spins.push(( control.input_id.clone(), - spins.iter().map(|(spin, _)| spin.read(cx).value().to_f64()).collect(), + spins + .iter() + .map(|(spin, _)| spin.read(cx).value().to_f64()) + .collect(), )); "spin" } @@ -3214,7 +3415,10 @@ mod tests { param: param( ValueType::Float, NodeValue::Float(0.5), - vec![("min", NodeValue::Float(0.0)), ("max", NodeValue::Float(1.0))], + vec![ + ("min", NodeValue::Float(0.0)), + ("max", NodeValue::Float(1.0)), + ], ), want_value: 0.5, want_range: (0.0, 1.0), @@ -3275,4 +3479,3 @@ mod tests { } } } - diff --git a/crates/oak-app/src/panels/program_viewer.rs b/crates/oak-app/src/panels/program_viewer.rs index 9d1aacd10..10bd28d4b 100644 --- a/crates/oak-app/src/panels/program_viewer.rs +++ b/crates/oak-app/src/panels/program_viewer.rs @@ -138,45 +138,55 @@ impl ProgramViewerPanel { // Route every transport request to the engine's program monitor, and // forward the picture's pointer/key events to the active OFX interact // (no-op when none is live). - cx.subscribe(&viewer, |this, _viewer, event: &ViewerEvent, cx| match event { - ViewerEvent::InteractPointer { - kind, - position, - button, - pressed, - } => this.forward_interact_pointer(*kind, *position, *button, *pressed, cx), - ViewerEvent::InteractKey { down, keystroke } => { - this.forward_interact_key(*down, keystroke, cx) - } - // The widget already toggled its own overlay state; nothing to - // forward to the engine. - ViewerEvent::ToggleSafeFramesRequested { .. } | ViewerEvent::ToggleZoomRequested { .. } => {} - // The loop in/out range is the shell-owned program workarea, so - // the panel re-emits the request. - ViewerEvent::InPointRequested { .. } => cx.emit(menu::ViewerPanelEvent::SetInPoint), - ViewerEvent::OutPointRequested { .. } => cx.emit(menu::ViewerPanelEvent::SetOutPoint), - ViewerEvent::ClearRangeRequested { .. } => cx.emit(menu::ViewerPanelEvent::ClearRange), - // The OFX color picker armed the eyedropper and the user clicked - // the frame: hand the sampled colour back to the engine's mailbox. - ViewerEvent::EyedropperPick { color } => { - this.engine - .update(cx, |engine, cx| engine.eyedropper_picked(*color, cx)) - } - event => { - let monitor = Monitor::Program; - this.engine.update(cx, |engine, cx| match event { - ViewerEvent::PlayRequested { .. } => engine.play(monitor, cx), - ViewerEvent::PauseRequested { .. } => engine.pause(monitor, cx), - ViewerEvent::StepRequested { delta, .. } => engine.step(monitor, *delta, cx), - other => println!("[program viewer] request: {other:?}"), - }); - } - }) + cx.subscribe( + &viewer, + |this, _viewer, event: &ViewerEvent, cx| match event { + ViewerEvent::InteractPointer { + kind, + position, + button, + pressed, + } => this.forward_interact_pointer(*kind, *position, *button, *pressed, cx), + ViewerEvent::InteractKey { down, keystroke } => { + this.forward_interact_key(*down, keystroke, cx) + } + // The widget already toggled its own overlay state; nothing to + // forward to the engine. + ViewerEvent::ToggleSafeFramesRequested { .. } + | ViewerEvent::ToggleZoomRequested { .. } => {} + // The loop in/out range is the shell-owned program workarea, so + // the panel re-emits the request. + ViewerEvent::InPointRequested { .. } => cx.emit(menu::ViewerPanelEvent::SetInPoint), + ViewerEvent::OutPointRequested { .. } => { + cx.emit(menu::ViewerPanelEvent::SetOutPoint) + } + ViewerEvent::ClearRangeRequested { .. } => { + cx.emit(menu::ViewerPanelEvent::ClearRange) + } + // The OFX color picker armed the eyedropper and the user clicked + // the frame: hand the sampled colour back to the engine's mailbox. + ViewerEvent::EyedropperPick { color } => this + .engine + .update(cx, |engine, cx| engine.eyedropper_picked(*color, cx)), + event => { + let monitor = Monitor::Program; + this.engine.update(cx, |engine, cx| match event { + ViewerEvent::PlayRequested { .. } => engine.play(monitor, cx), + ViewerEvent::PauseRequested { .. } => engine.pause(monitor, cx), + ViewerEvent::StepRequested { delta, .. } => { + engine.step(monitor, *delta, cx) + } + other => println!("[program viewer] request: {other:?}"), + }); + } + }, + ) .detach(); // Re-render whenever the engine's global eyedropper arm state flips, // so `sync_frame` mirrors it into the viewer widget. - cx.observe(&engine, |_this, _engine, cx| cx.notify()).detach(); + cx.observe(&engine, |_this, _engine, cx| cx.notify()) + .detach(); // A throttled idle pump for the OFX interact: the plugin's UI work // loop is served on the viewer's own timer (the app tick is @@ -198,8 +208,7 @@ impl ProgramViewerPanel { }) .detach(); - let context_menu = - ContextMenuHandle::new(Self::on_local_menu_item, window, cx); + let context_menu = ContextMenuHandle::new(Self::on_local_menu_item, window, cx); let scope_state = cx.new(|_cx| ScopeState { luma: Vec::new(), @@ -246,11 +255,13 @@ impl ProgramViewerPanel { match action { menu::ViewerMenuAction::ZoomFit => { let zoom = ViewerZoom::Fit; - self.viewer.update(cx, |viewer, cx| viewer.set_zoom(zoom, cx)); + self.viewer + .update(cx, |viewer, cx| viewer.set_zoom(zoom, cx)); } menu::ViewerMenuAction::ZoomLevel(index) => { let zoom = ViewerZoom::Level(index); - self.viewer.update(cx, |viewer, cx| viewer.set_zoom(zoom, cx)); + self.viewer + .update(cx, |viewer, cx| viewer.set_zoom(zoom, cx)); } menu::ViewerMenuAction::Resolution(divider) => self .engine @@ -280,7 +291,8 @@ impl ProgramViewerPanel { }), menu::ViewerMenuAction::ShowFps => { let show = self.viewer.read(cx).show_fps(); - self.viewer.update(cx, |viewer, cx| viewer.set_show_fps(!show, cx)); + self.viewer + .update(cx, |viewer, cx| viewer.set_show_fps(!show, cx)); } menu::ViewerMenuAction::SaveFrame => self.save_frame(cx), menu::ViewerMenuAction::FullScreen => { @@ -306,7 +318,11 @@ impl ProgramViewerPanel { .filter(|dir| std::fs::create_dir_all(dir).is_ok()) .unwrap_or_else(|| std::path::PathBuf::from(".")); let path = dir.join(name); - match self.engine.read(cx).save_frame(Monitor::Program, path.clone(), cx) { + match self + .engine + .read(cx) + .save_frame(Monitor::Program, path.clone(), cx) + { Ok(path) => println!("[program viewer] saved frame to {}", path.display()), Err(error) => println!("[program viewer] save frame failed: {error}"), } @@ -335,7 +351,8 @@ impl ProgramViewerPanel { // cursor (the setter no-ops when the state is unchanged, so a paused // viewer stays inert). let armed = self.engine.read(cx).eyedropper_armed(cx); - self.viewer.update(cx, |viewer, cx| viewer.set_eyedropper_armed(armed, cx)); + self.viewer + .update(cx, |viewer, cx| viewer.set_eyedropper_armed(armed, cx)); // Keep the main-process interact in sync with the inspector's // current selection (creates/destroys the interact as the target @@ -380,7 +397,7 @@ impl ProgramViewerPanel { if let Some(bytes) = displayed.as_bytes(0) { let (w, h) = (sz.width.0 as usize, sz.height.0 as usize); let mut ppm = format!("P6\n{w} {h}\n255\n").into_bytes(); - for px in bytes[..w * h * 4].chunks_exact(4) { + for px in bytes[..w * h * 4].as_chunks::<4>().0 { ppm.extend_from_slice(&[px[2], px[1], px[0]]); } let _ = std::fs::write("/tmp/oak_viewer_frame.ppm", ppm); @@ -406,10 +423,8 @@ impl ProgramViewerPanel { return frame.clone(); }; let frame_size = frame.size(0); - let viewport = InteractViewport::at_frame_size( - frame_size.width.0 as u32, - frame_size.height.0 as u32, - ); + let viewport = + InteractViewport::at_frame_size(frame_size.width.0 as u32, frame_size.height.0 as u32); let time = self.playhead_seconds(cx); // Redraw when the base frame, the target instance, the playhead, or @@ -428,11 +443,7 @@ impl ProgramViewerPanel { }); if stale { if let Some(image) = crate::oakui::ofx::draw_interact_composite( - instance, - &interact, - &viewport, - time, - frame, + instance, &interact, &viewport, time, frame, ) { self.overlay = Some(OverlayComposite { frame: frame.clone(), @@ -480,14 +491,9 @@ impl ProgramViewerPanel { return; }; let area = size(f32::from(bounds.size.width), f32::from(bounds.size.height)); - let frame_size = self - .engine - .read(cx) - .cpu_frame(Monitor::Program, cx) - .size(0); + let frame_size = self.engine.read(cx).cpu_frame(Monitor::Program, cx).size(0); let frame = size(frame_size.width.0 as f32, frame_size.height.0 as f32); - let Some((px, py)) = crate::oakui::ofx::viewport_pixel_to_pen(position, area, frame) - else { + let Some((px, py)) = crate::oakui::ofx::viewport_pixel_to_pen(position, area, frame) else { // The pointer is in the letterbox (outside the frame rect). return; }; @@ -519,7 +525,12 @@ impl ProgramViewerPanel { /// consumed by any binding. The plugin's return status is deliberately /// not acted on: the app does not steal key repeat or other listeners, /// so the interact is a passive consumer of otherwise-unused keys. - fn forward_interact_key(&mut self, down: bool, keystroke: &gpui::Keystroke, cx: &mut Context) { + fn forward_interact_key( + &mut self, + down: bool, + keystroke: &gpui::Keystroke, + cx: &mut Context, + ) { let Some((_, interact, _)) = crate::oakui::ofx::active_interact() else { return; }; @@ -790,7 +801,7 @@ mod tests { async fn scopes_tab_renders_from_the_current_frame(cx: &mut TestAppContext) { cx.update(|cx| cx.init_colors()); let window = cx.open_window(size(px(640.0), px(360.0)), |window, cx| { - let engine = cx.new(|cx| MockEngine::demo(cx)); + let engine = cx.new(MockEngine::demo); let clock = engine.read(cx).program_clock().clone(); let meter = cx.new(|cx| AudioLevelMeter::new(30, engine.clone(), window, cx)); ProgramViewerPanel::new(engine, clock, meter, window, cx) diff --git a/crates/oak-app/src/panels/timeline.rs b/crates/oak-app/src/panels/timeline.rs index 4da91566c..3915ade84 100644 --- a/crates/oak-app/src/panels/timeline.rs +++ b/crates/oak-app/src/panels/timeline.rs @@ -1546,7 +1546,7 @@ mod tests { ) { cx.update(|cx| cx.init_colors()); let window = cx.open_window(size(px(width), px(height)), |window, cx| { - let engine = cx.new(|cx| crate::oakui::MockEngine::demo(cx)); + let engine = cx.new(crate::oakui::MockEngine::demo); let timeline = cx.new(|cx| TimelineView::new(engine.clone(), window, cx).zoom(2.0)); TimelinePanel::new(engine, timeline, window, cx) }); diff --git a/crates/oak-audio/src/manager.rs b/crates/oak-audio/src/manager.rs index da629cf0f..8caf49a41 100644 --- a/crates/oak-audio/src/manager.rs +++ b/crates/oak-audio/src/manager.rs @@ -24,15 +24,15 @@ //! Recording goes through the oakcodec encoder C ABI ([`crate::bridge`]); //! device/config lookups go through oak_core. -use std::sync::atomic::{AtomicBool, Ordering}; -use std::sync::{Arc, Mutex, MutexGuard, OnceLock}; -use cpal::Device; -use cpal::traits::{DeviceTrait, HostTrait}; -use oak_codec::encoder::Encoder; -use oak_codec::encodingparams::EncodingParams; use crate::error::{Error, Result}; use crate::params::AudioParams; use crate::previewdevice::PreviewAudioDevice; +use cpal::traits::{DeviceTrait, HostTrait}; +use cpal::Device; +use oak_codec::encoder::Encoder; +use oak_codec::encodingparams::EncodingParams; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::{Arc, Mutex, MutexGuard, OnceLock}; /// `paNoDevice` (PortAudio "no device" sentinel; also the default when no /// device is configured). @@ -178,7 +178,9 @@ impl ManagerInner { let rate = params.sample_rate; let channels = params.channel_count(); let sink = self.output_buffer.clone(); - let _ = self.output_device_stream.ensure_open(device, rate, channels, sink); + let _ = self + .output_device_stream + .ensure_open(device, rate, channels, sink); } self.output_buffer.write(samples); self.output_started = true; @@ -339,8 +341,6 @@ impl ManagerInner { Ok(()) } - - /// Peak level (linear, 0..1 and above) of each channel of the buffered, /// not-yet-consumed output, written to `peaks` in channel order. /// Returns the channel count (0 when no output is configured or the @@ -394,7 +394,7 @@ impl ManagerInner { } else { for (ch, plane) in planes.iter_mut().enumerate() { let start = ch * frame_count * 4; - for b in buf[start..start + frame_count * 4].chunks_exact(4) { + for b in buf[start..start + frame_count * 4].as_chunks::<4>().0 { plane.push(f32::from_le_bytes([b[0], b[1], b[2], b[3]])); } } @@ -406,7 +406,6 @@ impl ManagerInner { } Ok(channels) } - } /// Lock the process-wide manager singleton (the direct-Rust replacement @@ -441,7 +440,7 @@ pub fn find_config_device_by_name_s(is_output_device: bool) -> Result { pub fn find_device_by_name_s_or_default(name: &String, _is_output_device: bool) -> Result { let host = cpal::default_host(); let device = host.devices()?.find(|d| { - if d.id().is_err(){ + if d.id().is_err() { return false; } if d.supports_output() && d.id().unwrap().id() == name { @@ -451,16 +450,13 @@ pub fn find_device_by_name_s_or_default(name: &String, _is_output_device: bool) }); if let Some(device) = device { Ok(device) - } - else{ + } else { if let Some(device) = host.default_output_device() { Ok(device) - } - else{ + } else { Err(Box::new(Error::NotFound)) } } - } /// The host's output device names in enumeration order; the list index is @@ -505,7 +501,6 @@ pub fn device_index_by_name(name: &str, output: bool) -> Option { names.iter().position(|n| n == name).map(|i| i as i32) } - #[cfg(test)] mod tests { use super::*; @@ -527,22 +522,19 @@ mod tests { output_callback_body(); let _ = tx.send(()); }); - if rx - .recv_timeout(std::time::Duration::from_secs(60)) - .is_err() - { + if rx.recv_timeout(std::time::Duration::from_secs(60)).is_err() { eprintln!("audio host wedged (headless/CI); skipping"); - return; } } /// The test body (see the wrapping test for the watchdog rationale). - fn output_callback_body() { // Skip when the audio system cannot actually run a stream: open a + fn output_callback_body() { + // Skip when the audio system cannot actually run a stream: open a // silent stream and require at least one callback within 2 s. A // device existing is not enough — headless sessions report the // stream running while delivering zero callbacks. - use std::sync::atomic::AtomicI64 as A; use cpal::traits::{DeviceTrait, HostTrait, StreamTrait}; + use std::sync::atomic::AtomicI64 as A; static PROBE: A = A::new(0); PROBE.store(0, Ordering::Relaxed); let can_play = (|| { @@ -556,12 +548,7 @@ mod tests { } }; let stream = device - .build_output_stream( - config.config(), - cb, - |_| {}, - None, - ) + .build_output_stream(config.config(), cb, |_| {}, None) .ok()?; stream.play().ok()?; let deadline = std::time::Instant::now() + std::time::Duration::from_secs(2); @@ -593,16 +580,12 @@ mod tests { samples.push(v); samples.push(v); } - let bytes: Vec = samples - .iter() - .flat_map(|s| s.to_le_bytes()) - .collect(); - manager.lock().unwrap().push_to_output( - params, - &bytes, - &mut vec![0u8; 256], - ) - .expect("push succeeds even without an explicit device"); + let bytes: Vec = samples.iter().flat_map(|s| s.to_le_bytes()).collect(); + manager + .lock() + .unwrap() + .push_to_output(params, &bytes, &mut vec![0u8; 256]) + .expect("push succeeds even without an explicit device"); // Give the audio thread time to consume. PortAudio/CoreAudio // stream startup can take SECONDS in some environments (audio HAL @@ -614,7 +597,11 @@ mod tests { let target = frames as i64 - 1024; let mut consumed = 0i64; while std::time::Instant::now() < deadline { - consumed = manager.lock().unwrap().output_buffer.output_frames_consumed(); + consumed = manager + .lock() + .unwrap() + .output_buffer + .output_frames_consumed(); if consumed >= target { break; } diff --git a/crates/oak-audio/src/outputdevice.rs b/crates/oak-audio/src/outputdevice.rs index 59df874cb..355d53cca 100644 --- a/crates/oak-audio/src/outputdevice.rs +++ b/crates/oak-audio/src/outputdevice.rs @@ -28,12 +28,9 @@ use std::cell::RefCell; use std::sync::Arc; -use cpal::traits::{DeviceTrait, HostTrait, StreamTrait}; -use cpal::{ - BufferSize, Device, Host, SampleFormat, Stream, StreamConfig, - SupportedBufferSize, -}; use crate::previewdevice::PreviewAudioDevice; +use cpal::traits::{DeviceTrait, HostTrait, StreamTrait}; +use cpal::{BufferSize, Device, Host, SampleFormat, Stream, StreamConfig, SupportedBufferSize}; /// The default frames-per-buffer requested for the preview stream /// (clamped to the device's supported range). @@ -100,21 +97,19 @@ impl PortAudioOutput { } self.close(); - if self.host.is_none(){ + if self.host.is_none() { self.host = Some(cpal::default_host()) } let host = self.host.as_ref().unwrap(); - let output_device = resolve_device(&host, device) - .ok_or_else(|| "no output device available".to_string())?; + let output_device = + resolve_device(host, device).ok_or_else(|| "no output device available".to_string())?; let config = pick_config(&output_device, rate, channels)?; if stream_dbg_enabled() { eprintln!( "[audio-stream] config: {} Hz, {} ch, buffer {:?}", - u32::from(config.sample_rate), - config.channels, - config.buffer_size + config.sample_rate, config.channels, config.buffer_size ); } @@ -217,8 +212,8 @@ fn pick_config(device: &Device, rate: i32, channels: i32) -> Result 0 { let new_bytes_read = inner.bytes_read + copy_length; - if inner.notify_interval > 0 && inner.notify_callback.is_some() { - if (inner.bytes_read / inner.notify_interval) + if inner.notify_interval > 0 + && inner.notify_callback.is_some() + && (inner.bytes_read / inner.notify_interval) != (new_bytes_read / inner.notify_interval) - { - notify = true; - } + { + notify = true; } inner.bytes_read = new_bytes_read; diff --git a/crates/oak-audio/src/processor.rs b/crates/oak-audio/src/processor.rs index 0c672f7e7..0189f1467 100644 --- a/crates/oak-audio/src/processor.rs +++ b/crates/oak-audio/src/processor.rs @@ -135,7 +135,9 @@ fn fix_channel_layout(params: AudioParams) -> AudioParams { fn build_graph(from: &AudioParams, to: &AudioParams, speed: f64) -> Result { let in_format = to_ffmpeg_sample_format(from.format); if in_format == Sample::None { - return Err(Box::new(Error::Failed("invalid input sample format".to_string()))); + return Err(Box::new(Error::Failed( + "invalid input sample format".to_string(), + ))); } let abuffer = ffmpeg::filter::find("abuffer") @@ -257,10 +259,19 @@ impl Processor { /// Push planar float input and pull converted output. Returns the number /// of output frames written. /// + /// # Safety + /// + /// `in_planar` must be null or point to at least `from.channel_count()` + /// readable `*const f32` plane pointers, each valid for + /// `in_frame_count` samples; `out_planar` must be null or point to at + /// least `to.channel_count()` writable `*mut f32` plane pointers, each + /// valid for `out_capacity_frames` samples (NULL plane entries are + /// skipped). + /// /// `// CPP-PARITY: src/audio/c_api/processor.cpp:91` (validation, state /// check, null `out_planar` short-circuit) and /// `src/audio/src/audioprocessor.cpp:141` (push/pull loop, byte counting). - pub fn convert( + pub unsafe fn convert( &self, in_planar: *const *const f32, in_frame_count: i32, @@ -273,7 +284,9 @@ impl Processor { if inner.graph.is_none() { return Err(Box::from(Error::State)); } - if in_frame_count < 0 || out_capacity_frames < 0 || (in_frame_count > 0 && in_planar.is_null()) + if in_frame_count < 0 + || out_capacity_frames < 0 + || (in_frame_count > 0 && in_planar.is_null()) { return Err(Box::from(Error::Invalid)); } @@ -294,7 +307,8 @@ impl Processor { let nb = in_frame_count as usize; let in_channels = from.channel_count().max(0) as usize; let layout = channel_layout_from_mask(from.channel_layout); - let mut frame = ffmpeg::frame::Audio::new(to_ffmpeg_sample_format(from.format), nb, layout); + let mut frame = + ffmpeg::frame::Audio::new(to_ffmpeg_sample_format(from.format), nb, layout); frame.set_rate(from.sample_rate as u32); let planar = from.format.is_planar(); // `plane_mut::` requires the exact sample type of the frame @@ -317,7 +331,9 @@ impl Processor { // samples. let src = unsafe { *in_planar } as *const $t; let dst = frame.plane_mut::<$t>(0); - unsafe { ptr::copy_nonoverlapping(src, dst.as_mut_ptr(), nb * in_channels) }; + unsafe { + ptr::copy_nonoverlapping(src, dst.as_mut_ptr(), nb * in_channels) + }; } }}; } diff --git a/crates/oak-audio/tests/manager_test.rs b/crates/oak-audio/tests/manager_test.rs index b196f8517..1fd9b320b 100644 --- a/crates/oak-audio/tests/manager_test.rs +++ b/crates/oak-audio/tests/manager_test.rs @@ -21,10 +21,10 @@ mod common; use common::lock_manager; -use oak_codec::encodingparams::EncodingParams; use oak_audio::error::{Error, OAKAUDIO_E_INVALID}; use oak_audio::manager::instance; use oak_audio::params::{AudioParams, SampleFormat}; +use oak_codec::encodingparams::EncodingParams; /// Audio params for the tests: stereo f32 at 48 kHz. fn stereo() -> AudioParams { @@ -97,11 +97,13 @@ fn push_output_starts_clock() { // With no explicit device the push still succeeds (M12 P1). m.set_output_device(-1).unwrap(); let samples = vec![0u8; 480 * 2 * 4]; - m.push_to_output(stereo(), &samples, &mut vec![0u8; 64]).unwrap(); + m.push_to_output(stereo(), &samples, &mut [0u8; 64]) + .unwrap(); // After selecting a device the push succeeds and the clock starts at 0. m.set_output_device(0).unwrap(); - m.push_to_output(stereo(), &samples, &mut vec![0u8; 64]).unwrap(); + m.push_to_output(stereo(), &samples, &mut [0u8; 64]) + .unwrap(); m.seconds(&mut secs).unwrap(); assert!(secs >= 0.0, "clock started (got {secs})"); } @@ -142,7 +144,8 @@ fn output_control_flags() { m.set_output_notify_interval(1024).unwrap(); let err = m.set_output_notify_interval(-1).unwrap_err(); assert_eq!( - err.downcast_ref::().map(|e| e.code()), + err.downcast_ref::() + .map(|e| e.code()), Some(OAKAUDIO_E_INVALID) ); m.clear_buffered_output().unwrap(); @@ -151,7 +154,8 @@ fn output_control_flags() { // Push starts the stream, then stop_output halts it (clock -> -1). m.set_output_device(0).unwrap(); let samples = vec![0u8; 480 * 2 * 4]; - m.push_to_output(stereo(), &samples, &mut vec![0u8; 64]).unwrap(); + m.push_to_output(stereo(), &samples, &mut [0u8; 64]) + .unwrap(); m.stop_output().unwrap(); let mut secs = 1.0f64; m.seconds(&mut secs).unwrap(); @@ -171,7 +175,9 @@ fn recording_start_stop() { // No input device -> explainable failure, no crash. m.set_input_device(-1).unwrap(); - assert!(m.start_recording(&wav_params("unused.wav"), &mut vec![0u8; 64]).is_err()); + assert!(m + .start_recording(&wav_params("unused.wav"), &mut [0u8; 64]) + .is_err()); m.set_input_device(0).unwrap(); let path = std::env::temp_dir().join(format!("oakaudio_rec_{}.wav", std::process::id())); @@ -193,8 +199,14 @@ fn recording_start_stop() { #[test] fn config_defaults() { assert_eq!(oak_audio::config::output_buffer_size(), 0); - assert!(oak_audio::config::device_name(true).is_err(), "no configured output device"); - assert!(oak_audio::config::device_name(false).is_err(), "no configured input device"); + assert!( + oak_audio::config::device_name(true).is_err(), + "no configured output device" + ); + assert!( + oak_audio::config::device_name(false).is_err(), + "no configured input device" + ); } /// PreviewAudioDevice pull-side plumbing (read/notify callback/clock). @@ -262,7 +274,7 @@ fn output_levels_reports_buffered_peaks() { packed.extend_from_slice(&(0.05f32 * t).to_le_bytes()); packed.extend_from_slice(&(0.2f32 * t).to_le_bytes()); } - m.push_to_output(stereo(), &packed, &mut vec![0u8; 64]).unwrap(); + m.push_to_output(stereo(), &packed, &mut [0u8; 64]).unwrap(); let mut peaks = [0.0f32; 4]; let n = m.output_levels(&mut peaks).unwrap(); @@ -288,7 +300,10 @@ fn output_levels_reports_buffered_peaks() { // The error mapping is intact. assert_eq!(Error::Invalid.code(), OAKAUDIO_E_INVALID); - assert_eq!(Error::Failed("x".to_string()).code(), oak_audio::error::OAKAUDIO_E_FAILED); + assert_eq!( + Error::Failed("x".to_string()).code(), + oak_audio::error::OAKAUDIO_E_FAILED + ); // Leave the singleton as we found it: the push flipped // `output_started`, which other tests' seconds() assertions depend on. diff --git a/crates/oak-audio/tests/processor_test.rs b/crates/oak-audio/tests/processor_test.rs index 9dd03121f..1a59342e8 100644 --- a/crates/oak-audio/tests/processor_test.rs +++ b/crates/oak-audio/tests/processor_test.rs @@ -54,6 +54,19 @@ fn plane_mut_ptrs(planes: &mut [Vec]) -> Vec<*mut f32> { planes.iter_mut().map(|p| p.as_mut_ptr()).collect() } +/// [`Processor::convert`] with its raw-pointer contract upheld by the +/// plane helpers above (the pointer arrays outlive the call). The single +/// `unsafe` block keeps the call sites readable. +fn convert( + p: &Processor, + in_ptrs: *const *const f32, + in_frames: i32, + out_ptrs: *const *mut f32, + out_capacity: i32, +) -> oak_audio::error::Result { + unsafe { p.convert(in_ptrs, in_frames, out_ptrs, out_capacity) } +} + /// The crate error code of a failed call (the API surfaces /// `Box`). fn code(err: Box) -> i32 { @@ -67,11 +80,11 @@ fn code(err: Box) -> i32 { #[test] fn processor_open_isopen_close() { let p = Processor::init(); - assert_eq!(p.is_open().unwrap(), false); + assert!(!p.is_open().unwrap()); open_identity(&p).unwrap(); - assert_eq!(p.is_open().unwrap(), true); + assert!(p.is_open().unwrap()); p.close().unwrap(); - assert_eq!(p.is_open().unwrap(), false); + assert!(!p.is_open().unwrap()); } /// open with matching in/out rate and format is an identity passthrough: @@ -86,7 +99,7 @@ fn identity_convert_passthrough() { let mut out = vec![vec![0f32; 32]; 2]; let out_ptrs = plane_mut_ptrs(&mut out); - let n = p.convert(in_ptrs.as_ptr(), 32, out_ptrs.as_ptr(), 32).unwrap(); + let n = convert(&p, in_ptrs.as_ptr(), 32, out_ptrs.as_ptr(), 32).unwrap(); assert_eq!(n, 32); for ch in 0..2 { for i in 0..32 { @@ -112,7 +125,7 @@ fn convert_capacity_truncation() { let mut out = vec![vec![9.9f32; 10]; 2]; let out_ptrs = plane_mut_ptrs(&mut out); - let n = p.convert(in_ptrs.as_ptr(), 32, out_ptrs.as_ptr(), 10).unwrap(); + let n = convert(&p, in_ptrs.as_ptr(), 32, out_ptrs.as_ptr(), 10).unwrap(); assert_eq!(n, 10); for ch in 0..2 { for i in 0..10 { @@ -123,7 +136,7 @@ fn convert_capacity_truncation() { // The graph has already drained; nothing further to pull. let mut out2 = vec![vec![0f32; 32]; 2]; let out2_ptrs = plane_mut_ptrs(&mut out2); - let n = p.convert(in_ptrs.as_ptr(), 0, out2_ptrs.as_ptr(), 32).unwrap(); + let n = convert(&p, in_ptrs.as_ptr(), 0, out2_ptrs.as_ptr(), 32).unwrap(); assert_eq!(n, 0); } @@ -136,7 +149,7 @@ fn open_invalid_params() { code(p.open(params(0), params(48000), 1.0).unwrap_err()), OAKAUDIO_E_INVALID ); - assert_eq!(p.is_open().unwrap(), false); + assert!(!p.is_open().unwrap()); // The output format is forced to planar f32 by the processor contract. let mut wrong_out = params(48000); wrong_out.format = SampleFormat::F32; @@ -144,12 +157,12 @@ fn open_invalid_params() { code(p.open(params(48000), wrong_out, 1.0).unwrap_err()), OAKAUDIO_E_INVALID ); - assert_eq!(p.is_open().unwrap(), false); + assert!(!p.is_open().unwrap()); // convert before open is a state error; a non-positive speed is // rejected on open. assert_eq!( - code(p.convert(std::ptr::null(), 0, std::ptr::null(), 0).unwrap_err()), + code(convert(&p, std::ptr::null(), 0, std::ptr::null(), 0).unwrap_err()), OAKAUDIO_E_STATE ); assert_eq!( @@ -173,13 +186,18 @@ fn resample_and_flush() { let mut out = vec![vec![0f32; frames]; 2]; let out_ptrs = plane_mut_ptrs(&mut out); - let mut total = p - .convert(in_ptrs.as_ptr(), frames as i32, out_ptrs.as_ptr(), frames as i32) - .unwrap(); + let mut total = convert( + &p, + in_ptrs.as_ptr(), + frames as i32, + out_ptrs.as_ptr(), + frames as i32, + ) + .unwrap(); p.flush().unwrap(); // Drain the resampler delay after end-of-input. while total < frames as i32 { - let n = p.convert(std::ptr::null(), 0, out_ptrs.as_ptr(), frames as i32).unwrap(); + let n = convert(&p, std::ptr::null(), 0, out_ptrs.as_ptr(), frames as i32).unwrap(); if n == 0 { break; } @@ -189,7 +207,7 @@ fn resample_and_flush() { (total - 22050).abs() <= 2, "half-rate output must halve the frame count (got {total})" ); - assert_eq!(p.is_open().unwrap(), true); + assert!(p.is_open().unwrap()); } /// A tempo factor != 1.0 time-stretches: tempo 2.0 halves the frame count @@ -207,12 +225,17 @@ fn tempo_stretch() { let mut out = vec![vec![0f32; frames]; 2]; let out_ptrs = plane_mut_ptrs(&mut out); - let mut total = p - .convert(in_ptrs.as_ptr(), frames as i32, out_ptrs.as_ptr(), frames as i32) - .unwrap(); + let mut total = convert( + &p, + in_ptrs.as_ptr(), + frames as i32, + out_ptrs.as_ptr(), + frames as i32, + ) + .unwrap(); p.flush().unwrap(); while total < frames as i32 { - let n = p.convert(std::ptr::null(), 0, out_ptrs.as_ptr(), frames as i32).unwrap(); + let n = convert(&p, std::ptr::null(), 0, out_ptrs.as_ptr(), frames as i32).unwrap(); if n == 0 { break; } @@ -222,7 +245,7 @@ fn tempo_stretch() { (total - 24000).abs() <= 2400, "tempo 2.0 must halve the frame count (got {total})" ); - assert_eq!(p.is_open().unwrap(), true); + assert!(p.is_open().unwrap()); p.close().unwrap(); // Re-opening a closed processor works; opening an open one is a state diff --git a/crates/oak-codec/src/decoder.rs b/crates/oak-codec/src/decoder.rs index 827cb7417..ab5f85bc7 100644 --- a/crates/oak-codec/src/decoder.rs +++ b/crates/oak-codec/src/decoder.rs @@ -250,7 +250,7 @@ impl CodecStream { /// Associated timeline block (borrowed; only compared, never used). pub fn block(&self) -> Option { - self.block.clone() + self.block } } diff --git a/crates/oak-codec/src/encodingparams.rs b/crates/oak-codec/src/encodingparams.rs index 0b1a412ed..34a7003e3 100644 --- a/crates/oak-codec/src/encodingparams.rs +++ b/crates/oak-codec/src/encodingparams.rs @@ -558,7 +558,7 @@ impl<'a> Cursor<'a> { } fn skip_ws(&mut self) { - while self.peek().map_or(false, char::is_whitespace) { + while self.peek().is_some_and(char::is_whitespace) { self.i += 1; } } diff --git a/crates/oak-codec/src/ffmpeg.rs b/crates/oak-codec/src/ffmpeg.rs index 30bf0c8ca..40d66d3a5 100644 --- a/crates/oak-codec/src/ffmpeg.rs +++ b/crates/oak-codec/src/ffmpeg.rs @@ -1178,12 +1178,12 @@ impl DecoderState { ctx.run(&f, &mut out).map_err(ffmpeg_err)?; let bytes = if f32_ok { let stride = out.stride(0); - convert_rgba_f32_le(&out.data(0), w, h, stride) + convert_rgba_f32_le(out.data(0), w, h, stride) } else if out_fmt == Pixel::YUV444P16LE { convert_yuv444p16_to_rgba_f32(&out, w, h, yuv_matrix_for(raw_space, src_w, src_h), full_range) } else { let stride = out.stride(0); - convert_rgba8_to_f32(&out.data(0), w, h, stride) + convert_rgba8_to_f32(out.data(0), w, h, stride) }; let meta = crate::decoder::DecodedColorMeta { color_primaries: raw_primaries, @@ -2084,9 +2084,9 @@ fn probe_file(filename: &str, cancelled: Option<&CancelAtom>) -> Option) -> Option {} diff --git a/crates/oak-codec/src/planarfiledevice.rs b/crates/oak-codec/src/planarfiledevice.rs index bed598900..446403428 100644 --- a/crates/oak-codec/src/planarfiledevice.rs +++ b/crates/oak-codec/src/planarfiledevice.rs @@ -243,7 +243,7 @@ mod tests { assert!(!d.is_open()); // Already open -> refuse. - assert!(d.open(&[ok.clone()], OpenMode::WriteOnly)); + assert!(d.open(std::slice::from_ref(&ok), OpenMode::WriteOnly)); assert!(!d.open(&[ok], OpenMode::WriteOnly)); } diff --git a/crates/oak-codec/src/proxymanager.rs b/crates/oak-codec/src/proxymanager.rs index 370321595..b9c0a3952 100644 --- a/crates/oak-codec/src/proxymanager.rs +++ b/crates/oak-codec/src/proxymanager.rs @@ -317,11 +317,10 @@ impl ProxyManager { /// Locate an ffmpeg executable (empty string when none found). pub fn find_ffmpeg(configured_path: &str) -> String { // An explicitly configured path takes precedence if it is usable. - if !configured_path.is_empty() { - if is_executable_file(Path::new(configured_path)) { + if !configured_path.is_empty() + && is_executable_file(Path::new(configured_path)) { return absolute(configured_path); } - } // Fall back to searching the system PATH (split per platform: // `;` on Windows, `:` elsewhere — see [`split_path_env`]). diff --git a/crates/oak-codec/src/task.rs b/crates/oak-codec/src/task.rs index 1ed63f94b..2b8fcda00 100644 --- a/crates/oak-codec/src/task.rs +++ b/crates/oak-codec/src/task.rs @@ -335,8 +335,8 @@ mod tests { #[test] fn task_kind_values_match_abi() { - assert_eq!(TaskKind::Conform as i32, OAKCODEC_TASK_CONFORM as i32); - assert_eq!(TaskKind::Proxy as i32, OAKCODEC_TASK_PROXY as i32); + assert_eq!(TaskKind::Conform as i32, OAKCODEC_TASK_CONFORM); + assert_eq!(TaskKind::Proxy as i32, OAKCODEC_TASK_PROXY); } // ABI constants mirrored from include/codec/task.h. diff --git a/crates/oak-codec/src/timecodemetadata.rs b/crates/oak-codec/src/timecodemetadata.rs index c6cdb050f..f049a35f8 100644 --- a/crates/oak-codec/src/timecodemetadata.rs +++ b/crates/oak-codec/src/timecodemetadata.rs @@ -66,7 +66,7 @@ fn frame_rate(timebase: &Rational) -> f64 { /// `drop_frame` is true when the (already trimmed) string contains a `;`. /// Returns `None` on any parse failure. fn timecode_to_time(timecode: &str, timebase: &Rational, drop_frame: bool) -> Option { - let mut tokens: Vec<&str> = timecode.split(|c| c == ':' || c == ';').collect(); + let mut tokens: Vec<&str> = timecode.split([':', ';']).collect(); let element_count = 4; // Keep only the leading `HH:MM:SS:FF` tokens. diff --git a/crates/oak-core/src/backend.rs b/crates/oak-core/src/backend.rs index b55a6b7e3..685a05867 100644 --- a/crates/oak-core/src/backend.rs +++ b/crates/oak-core/src/backend.rs @@ -40,9 +40,9 @@ use std::sync::{Arc, Mutex, MutexGuard}; use crate::PixelFormat; +use crate::error::{Error, Result}; use crate::frame::VideoParamsPod; use crate::texture::{Frame, Texture}; -use crate::error::{Error, Result}; /// Backend selection preference (mapped onto wgpu backends). /// @@ -183,9 +183,8 @@ impl DisplayBitDepth { /// Read the user's persisted choice through the oak_core config C ABI /// ("DisplayBitDepth"). pub fn from_user_config() -> DisplayBitDepth { - let configured = - crate::commonutil::config_get_string(None, CONFIG_KEY_DISPLAY_BIT_DEPTH) - .unwrap_or_default(); + let configured = crate::commonutil::config_get_string(None, CONFIG_KEY_DISPLAY_BIT_DEPTH) + .unwrap_or_default(); DisplayBitDepth::from_config_string(&configured) } @@ -243,10 +242,10 @@ pub trait GpuContextLike: Send + Sync { fn download(&self, token: u64) -> Result; /// Blit texture → texture (plain copy; color-managed deferred). fn blit( - &self, - src: u64, - dst: u64, - processor: Option<&crate::color::ColorProcessor>, + &self, + src: u64, + dst: u64, + processor: Option<&crate::color::ColorProcessor>, ) -> Result<()>; /// Concrete-context downcast hook (M2). The present path uses it to @@ -432,7 +431,9 @@ impl GpuContext { // (widely available on desktop GPUs); without it effect // shaders sample nearest — a quality degradation, not a // failure (logged once by the shaderfx runner). - let filterable = adapter.features().contains(wgpu::Features::FLOAT32_FILTERABLE); + let filterable = adapter + .features() + .contains(wgpu::Features::FLOAT32_FILTERABLE); let mut required_features = wgpu::Features::empty(); if filterable { required_features |= wgpu::Features::FLOAT32_FILTERABLE; @@ -789,12 +790,7 @@ impl GpuContext { /// nodes (`ColorTransformJob`, M2). The LUT texture is uploaded once /// per key (the caller passes a stable processor cache id); applying /// it is GPU→GPU. - pub fn apply_color_lut( - &self, - src: u64, - key: &str, - lut: &crate::lut::Lut3d, - ) -> Result { + pub fn apply_color_lut(&self, src: u64, key: &str, lut: &crate::lut::Lut3d) -> Result { let token = { let mut cache = lock(&self.color_luts); if let Some((_, token)) = cache.iter().find(|(k, _)| k == key) { @@ -873,7 +869,8 @@ impl GpuContext { usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST, mapped_at_creation: false, }); - self.queue.write_buffer(&uniform, 0, &f32_uniform_bytes(¶ms)); + self.queue + .write_buffer(&uniform, 0, &f32_uniform_bytes(¶ms)); let src_view = src_tex .texture .create_view(&wgpu::TextureViewDescriptor::default()); @@ -1023,12 +1020,11 @@ impl GpuContext { cache: None, }); if let Some(err) = pollster_block_on(scope.pop()) { - return Err(Error::Failed(format!("present pipeline validation failed: {err}"))); + return Err(Error::Failed(format!( + "present pipeline validation failed: {err}" + ))); } - let program = PresentPipeline { - pipeline, - layout, - }; + let program = PresentPipeline { pipeline, layout }; cache.push((format, program.clone())); Ok(program) } @@ -1090,7 +1086,8 @@ impl GpuContext { usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST, mapped_at_creation: false, }); - self.queue.write_buffer(&uniform, 0, &f32_uniform_bytes(¶ms)); + self.queue + .write_buffer(&uniform, 0, &f32_uniform_bytes(¶ms)); let y_view = y_tex .texture .create_view(&wgpu::TextureViewDescriptor::default()); @@ -1239,12 +1236,11 @@ impl GpuContext { cache: None, }); if let Some(err) = pollster_block_on(scope.pop()) { - return Err(Error::Failed(format!("YUV pipeline validation failed: {err}"))); + return Err(Error::Failed(format!( + "YUV pipeline validation failed: {err}" + ))); } - let program = PresentPipeline { - pipeline, - layout, - }; + let program = PresentPipeline { pipeline, layout }; *cache = Some(program.clone()); Ok(program) } @@ -1438,10 +1434,10 @@ impl GpuContext { /// of this pass (see README §4), so a `Some` processor returns /// `Error::Failed` and the plain-copy WGSL pipeline is used for `None`. pub fn blit( - &self, - src: u64, - dst: u64, - processor: Option<&crate::color::ColorProcessor>, + &self, + src: u64, + dst: u64, + processor: Option<&crate::color::ColorProcessor>, ) -> Result<()> { if processor.is_some() { return Err(Error::Failed( @@ -1767,7 +1763,9 @@ impl GpuContext { cache: None, }); if let Some(err) = pollster_block_on(scope.pop()) { - return Err(Error::Failed(format!("effect pipeline validation failed: {err}"))); + return Err(Error::Failed(format!( + "effect pipeline validation failed: {err}" + ))); } let program = Arc::new(ShaderProgram { @@ -2121,7 +2119,6 @@ fn vs_main(@builtin(vertex_index) vi: u32) -> VsOut { } "#; - impl GpuContextLike for GpuContext { fn kind(&self) -> BackendKind { self.kind() @@ -2140,10 +2137,10 @@ impl GpuContextLike for GpuContext { } fn blit( - &self, - src: u64, - dst: u64, - processor: Option<&crate::color::ColorProcessor>, + &self, + src: u64, + dst: u64, + processor: Option<&crate::color::ColorProcessor>, ) -> Result<()> { self.blit(src, dst, processor) } @@ -2197,11 +2194,11 @@ fn pollster_block_on(future: F) -> F::Output { // Minimal futures executor (wgpu brings futures-core transitively; a tiny // block_on is enough for the immediately-ready adapter/device futures). mod futures_executor { - use std::future::Future; - use std::pin::pin; - use std::task::{Context, Poll, RawWaker, RawWakerVTable, Waker}; + use std::future::Future; + use std::pin::pin; + use std::task::{Context, Poll, RawWaker, RawWakerVTable, Waker}; - fn noop_raw_waker() -> RawWaker { + fn noop_raw_waker() -> RawWaker { fn no_op(_: *const ()) {} fn clone(_: *const ()) -> RawWaker { noop_raw_waker() @@ -2353,7 +2350,13 @@ impl DisplayRenderer { } /// Upload pixels into a texture (GPU: backend upload; CPU: buffer copy). - pub fn upload_texture( + /// + /// # Safety + /// + /// `pixels` must point to at least `linesize * height` readable bytes + /// of the texture's pixel format, and `linesize` must equal the + /// frame's line size. + pub unsafe fn upload_texture( &self, texture: &mut Texture, pixels: *const u8, @@ -2362,7 +2365,7 @@ impl DisplayRenderer { let size = texture.size(); match texture { Texture::Gpu { token, ctx, .. } => { - let frame = frame_from_pixels_for_upload(size, pixels, linesize)?; + let frame = unsafe { frame_from_pixels_for_upload(size, pixels, linesize) }?; ctx.upload(*token, &frame) } Texture::Cpu(frame) => { @@ -2380,7 +2383,17 @@ impl DisplayRenderer { } /// Download a texture's pixels into `dst` (with `linesize` stride). - pub fn download_texture(&self, texture: &Texture, dst: *mut u8, linesize: usize) -> Result<()> { + /// + /// # Safety + /// + /// `dst` must point to at least `linesize * height` writable bytes, + /// and `linesize` must equal the frame's line size. + pub unsafe fn download_texture( + &self, + texture: &Texture, + dst: *mut u8, + linesize: usize, + ) -> Result<()> { let frame = texture.to_frame()?; let stride = frame.linesize_bytes(); if linesize != stride { @@ -2397,10 +2410,10 @@ impl DisplayRenderer { /// GPU path: plain-copy WGSL blit; a color processor on the GPU path is /// deferred (`Error::Failed`, see [`GpuContext::blit`]). pub fn blit_color_managed( - &self, - src: Option<&Texture>, - dst: &mut Texture, - processor: Option<&crate::color::ColorProcessor>, + &self, + src: Option<&Texture>, + dst: &mut Texture, + processor: Option<&crate::color::ColorProcessor>, ) -> Result<()> { match (src, dst) { ( @@ -2430,7 +2443,12 @@ impl DisplayRenderer { /// Cross-backend texture download by id (GPU registry only; CPU /// textures have no id registry — documented). - pub fn download_from_texture( + /// + /// # Safety + /// + /// `dst` must point to at least `linesize * height` writable bytes, + /// and `linesize` must equal the F32 line size. + pub unsafe fn download_from_texture( &self, texture_id: i32, params: &VideoParamsPod, @@ -2464,7 +2482,12 @@ pub fn texture_id_of(t: &Texture) -> i32 { } /// Build an F32 frame from raw pixels (for GPU upload). -pub fn frame_from_pixels_for_upload( +/// +/// # Safety +/// +/// `pixels` must point to `linesize * height` readable bytes of F32 RGBA +/// data (a null pointer is rejected with `Error::Invalid`). +pub unsafe fn frame_from_pixels_for_upload( size: (i32, i32), pixels: *const u8, linesize: usize, @@ -2489,14 +2512,14 @@ pub fn frame_from_pixels_for_upload( #[cfg(test)] mod tests { - use super::*; + use super::*; - /// Serializes the tests that assert on the process-global GPU - /// transfer counters (and install a display LUT): parallel tests would - /// otherwise see each other's transfers. - static GPU_COUNTER_LOCK: Mutex<()> = Mutex::new(()); + /// Serializes the tests that assert on the process-global GPU + /// transfer counters (and install a display LUT): parallel tests would + /// otherwise see each other's transfers. + static GPU_COUNTER_LOCK: Mutex<()> = Mutex::new(()); - #[test] + #[test] fn backend_string_roundtrip() { for (s, kind) in [ ("auto", BackendKind::Auto), @@ -2534,13 +2557,19 @@ mod tests { #[test] fn display_bit_depth_string_roundtrip() { - assert_eq!(DisplayBitDepth::from_config_string("8"), DisplayBitDepth::Bit8); + assert_eq!( + DisplayBitDepth::from_config_string("8"), + DisplayBitDepth::Bit8 + ); assert_eq!( DisplayBitDepth::from_config_string("10"), DisplayBitDepth::Bit10 ); // Unknown / missing → the 10-bit default. - assert_eq!(DisplayBitDepth::from_config_string(""), DisplayBitDepth::Bit10); + assert_eq!( + DisplayBitDepth::from_config_string(""), + DisplayBitDepth::Bit10 + ); assert_eq!( DisplayBitDepth::from_config_string("bogus"), DisplayBitDepth::Bit10 @@ -2560,12 +2589,18 @@ mod tests { // 10-bit → the 10-bit (RGB10A2) swapchain format, HDR float fallback. assert_eq!( DisplayBitDepth::Bit10.present_formats(), - &[wgpu::TextureFormat::Rgb10a2Unorm, wgpu::TextureFormat::Rgba16Float] + &[ + wgpu::TextureFormat::Rgb10a2Unorm, + wgpu::TextureFormat::Rgba16Float + ] ); // 8-bit → the engine's current default preference list. assert_eq!( DisplayBitDepth::Bit8.present_formats(), - &[wgpu::TextureFormat::Bgra8Unorm, wgpu::TextureFormat::Rgba8Unorm] + &[ + wgpu::TextureFormat::Bgra8Unorm, + wgpu::TextureFormat::Rgba8Unorm + ] ); } @@ -2590,8 +2625,7 @@ mod tests { assert!(!GpuContext::shared().unwrap().is_adopted()); let (device, queue) = base.device_queue(); - let adopted = - GpuContext::adopt(device, queue, BackendKind::Auto); + let adopted = GpuContext::adopt(device, queue, BackendKind::Auto); assert!( GpuContext::install_shared(Some(adopted.clone())), "an unused engine context is replaceable by the UI device" @@ -2605,8 +2639,7 @@ mod tests { // pulled out from under in-flight work. let _texture = adopted.create_texture(2, 2).unwrap(); let (device, queue) = adopted.device_queue(); - let second = - GpuContext::adopt(device, queue, BackendKind::Auto); + let second = GpuContext::adopt(device, queue, BackendKind::Auto); assert!( !GpuContext::install_shared(Some(second)), "a used context is not replaceable" @@ -2728,8 +2761,7 @@ mod tests { for i in 0..(w * h) as usize { let px = sample(i); for (c, v) in px.iter().enumerate() { - frame.data[i * 16 + c * 4..i * 16 + c * 4 + 4] - .copy_from_slice(&v.to_le_bytes()); + frame.data[i * 16 + c * 4..i * 16 + c * 4 + 4].copy_from_slice(&v.to_le_bytes()); } } ctx.upload(src, &frame).unwrap(); @@ -2859,10 +2891,10 @@ mod tests { let mut expected = vec![0.0f32; w * h * 4]; let mut compare = |ctx: &GpuContext, - tag: &str, - matrix: crate::colormath::YuvMatrix, - transform: YuvTransform, - full_range: bool| { + tag: &str, + matrix: crate::colormath::YuvMatrix, + transform: YuvTransform, + full_range: bool| { crate::colormath::yuv444p16_to_rgb_f32( &y_code_plane, w * 2, @@ -2893,12 +2925,48 @@ mod tests { } }; use crate::colormath::YuvMatrix; - compare(&ctx, "bt601 limited", YuvMatrix::Bt601, YuvTransform::bt601_limited(), false); - compare(&ctx, "bt601 full", YuvMatrix::Bt601, YuvTransform::bt601_full(), true); - compare(&ctx, "bt709 limited", YuvMatrix::Bt709, YuvTransform::bt709_limited(), false); - compare(&ctx, "bt709 full", YuvMatrix::Bt709, YuvTransform::bt709_full(), true); - compare(&ctx, "bt2020 limited", YuvMatrix::Bt2020, YuvTransform::bt2020_limited(), false); - compare(&ctx, "bt2020 full", YuvMatrix::Bt2020, YuvTransform::bt2020_full(), true); + compare( + &ctx, + "bt601 limited", + YuvMatrix::Bt601, + YuvTransform::bt601_limited(), + false, + ); + compare( + &ctx, + "bt601 full", + YuvMatrix::Bt601, + YuvTransform::bt601_full(), + true, + ); + compare( + &ctx, + "bt709 limited", + YuvMatrix::Bt709, + YuvTransform::bt709_limited(), + false, + ); + compare( + &ctx, + "bt709 full", + YuvMatrix::Bt709, + YuvTransform::bt709_full(), + true, + ); + compare( + &ctx, + "bt2020 limited", + YuvMatrix::Bt2020, + YuvTransform::bt2020_limited(), + false, + ); + compare( + &ctx, + "bt2020 full", + YuvMatrix::Bt2020, + YuvTransform::bt2020_full(), + true, + ); ctx.destroy_texture(y); ctx.destroy_texture(u); @@ -2914,9 +2982,7 @@ mod tests { let Some(ctx) = any_gpu() else { return; }; - let lut = crate::lut::Lut3d::build(9, [0.0; 3], [1.0; 3], |c| { - [c[1], c[2], c[0]] - }); + let lut = crate::lut::Lut3d::build(9, [0.0; 3], [1.0; 3], |c| [c[1], c[2], c[0]]); let (w, h) = (4, 2); let src = ctx.create_texture(w, h).unwrap(); let mut frame = Frame::new(); @@ -2933,8 +2999,7 @@ mod tests { 1.0, ]; for (c, v) in px.iter().enumerate() { - frame.data[i * 16 + c * 4..i * 16 + c * 4 + 4] - .copy_from_slice(&v.to_le_bytes()); + frame.data[i * 16 + c * 4..i * 16 + c * 4 + 4].copy_from_slice(&v.to_le_bytes()); } } ctx.upload(src, &frame).unwrap(); @@ -2960,7 +3025,9 @@ mod tests { let want = lut.eval(src_px); for c in 0..3 { let g = f32::from_le_bytes( - got.data[i * 16 + c * 4..i * 16 + c * 4 + 4].try_into().unwrap(), + got.data[i * 16 + c * 4..i * 16 + c * 4 + 4] + .try_into() + .unwrap(), ); assert!( (g - want[c]).abs() < 1e-4, @@ -3008,7 +3075,7 @@ mod tests { #[test] fn display_renderer_rejects_foreign_gl_context() { let mut r = DisplayRenderer::new(BackendKind::Gl); - let fake = 0x1 as *mut std::ffi::c_void; + let fake = std::ptr::dangling_mut::(); assert_eq!(r.init(fake).unwrap_err().code(), Error::Invalid.code()); } @@ -3039,20 +3106,20 @@ mod tests { frame.allocate(); frame.data[0] = 0x77; let mut upload_target = r3.create_texture(&pod, None).unwrap(); - r3.upload_texture( - &mut upload_target, - frame.data.as_ptr(), - frame.linesize_bytes(), - ) + unsafe { + r3.upload_texture( + &mut upload_target, + frame.data.as_ptr(), + frame.linesize_bytes(), + ) + } .unwrap(); let mut buf = vec![0u8; frame.linesize_bytes() * 4]; - r3.download_texture(&upload_target, buf.as_mut_ptr(), frame.linesize_bytes()) + unsafe { r3.download_texture(&upload_target, buf.as_mut_ptr(), frame.linesize_bytes()) } .unwrap(); assert_eq!(buf[0], 0x77); // Stride mismatch rejected. - assert!(r3 - .upload_texture(&mut upload_target, frame.data.as_ptr(), 1) - .is_err()); + assert!(unsafe { r3.upload_texture(&mut upload_target, frame.data.as_ptr(), 1) }.is_err()); } #[test] @@ -3077,9 +3144,9 @@ mod tests { assert_eq!(df.data[4], 0x22); // Pass-through processor is a no-op. r.blit_color_managed( - Some(&src), - &mut dst, - Some(&crate::color::ColorProcessor::pass_through()), + Some(&src), + &mut dst, + Some(&crate::color::ColorProcessor::pass_through()), ) .unwrap(); // Size mismatch rejected. diff --git a/crates/oak-core/src/color.rs b/crates/oak-core/src/color.rs index d68fc001c..39790333d 100644 --- a/crates/oak-core/src/color.rs +++ b/crates/oak-core/src/color.rs @@ -318,7 +318,8 @@ impl ColorProcessor { } /// Convert a whole F32 frame in place (row-major RGBA). - pub fn convert_frame(&self, frame: &mut Frame) -> Result<()> { let Some(cpu) = &self.cpu else { + pub fn convert_frame(&self, frame: &mut Frame) -> Result<()> { + let Some(cpu) = &self.cpu else { return Ok(()); // pass-through }; if frame.format != PixelFormat::F32 { @@ -366,8 +367,7 @@ impl ColorProcessor { if data.len() < count { return Err(Error::Invalid); } - let mut f32s: Vec = - data[..count].iter().map(|&v| v as f32 / 255.0).collect(); + let mut f32s: Vec = data[..count].iter().map(|&v| v as f32 / 255.0).collect(); cpu.try_apply_rgba_pixels(&mut f32s, pixels, 4) .map_err(|e| Error::Failed(format!("OCIO f32 apply (bgra8 detour): {e}")))?; for (dst, v) in data[..count].iter_mut().zip(f32s) { @@ -409,7 +409,7 @@ impl GradingStyle { /// Reinterpret a byte buffer as f32 when its length is a multiple of 4 /// (the pipeline guarantees F32 RGBA frames, so alignment is exact). fn bytemuck_f32_slice(data: &mut [u8]) -> Option<&mut [f32]> { - if data.len() % 4 != 0 { + if !data.len().is_multiple_of(4) { return None; } // SAFETY: length is a multiple of 4 and `data` is byte-aligned; the @@ -427,11 +427,17 @@ fn bytemuck_f32_slice(data: &mut [u8]) -> Option<&mut [f32]> { /// project-properties commit). Render and export paths read it — a single /// project is open at a time, so a process global is the same shape as the /// OCIO default config above. -static PIPELINE_COLOR: LazyLock> = - LazyLock::new(|| Mutex::new(( +static PIPELINE_COLOR: LazyLock< + Mutex<( + crate::colormath::WorkingColorSpace, + crate::colormath::OutputColorSpec, + )>, +> = LazyLock::new(|| { + Mutex::new(( crate::colormath::WorkingColorSpace::default(), crate::colormath::OutputColorSpec::default(), - ))); + )) +}); /// Set the pipeline color settings (working space + output spec). pub fn set_pipeline_color_settings( @@ -594,9 +600,7 @@ pub fn config_path() -> Option { return Some(path); } } - if default_config().is_none() { - return None; - } + default_config()?; Some(format!( "{}/ocioconf/config.ocio", crate::commonutil::configuration_location() @@ -836,8 +840,8 @@ mod tests { let _lock = config_lock(); if set_up_default_config().is_err() { return; - } // Create via the built-in config; a valid processor must exist for - // the ACES scene→display-encoded pairing and must preserve alpha. + } // Create via the built-in config; a valid processor must exist for + // the ACES scene→display-encoded pairing and must preserve alpha. let p = ColorProcessor::create("ACEScg", "sRGB Encoded Rec.709 (sRGB)", Direction::Normal); let p = p.expect("processor handle always returned"); assert!( @@ -879,7 +883,8 @@ mod tests { return; } let display = config.display_all(0).unwrap(); - let n = config.num_views_by_reference_space(ocio_rs::SearchReferenceSpaceType::Scene, &display); + let n = + config.num_views_by_reference_space(ocio_rs::SearchReferenceSpaceType::Scene, &display); assert!(n >= 0); if n > 0 { let view = config @@ -913,7 +918,7 @@ mod tests { let f32s: &mut [f32] = unsafe { std::slice::from_raw_parts_mut(f.data.as_mut_ptr() as *mut f32, f.pixel_count() * 4) }; - for px in f32s.chunks_exact_mut(4) { + for px in f32s.as_chunks_mut::<4>().0 { px[0] = 0.18; px[1] = 0.18; px[2] = 0.18; @@ -960,8 +965,10 @@ mod tests { "0.18 linear grey should land near 0.5 sRGB (got {})", out[0] ); - assert!((out[0] - out[1]).abs() < 1e-3 && (out[1] - out[2]).abs() < 1e-3, - "grey stays grey: {out:?}"); + assert!( + (out[0] - out[1]).abs() < 1e-3 && (out[1] - out[2]).abs() < 1e-3, + "grey stays grey: {out:?}" + ); assert!((out[3] - 1.0).abs() < 1e-5, "alpha preserved"); } @@ -1073,7 +1080,7 @@ mod tests { let f32_result = p32.convert_f32_rgba(&mut f32s, 4); eprintln!("f32 convert: {f32_result:?} -> {f32s:?}"); bgra_result.expect("convert"); - for (i, px) in data.chunks_exact(4).enumerate() { + for (i, px) in data.as_chunks::<4>().0.iter().enumerate() { assert_eq!(px[3], 255, "pixel {i}: alpha preserved"); let rgb: u32 = px[0] as u32 + px[1] as u32 + px[2] as u32; assert!(rgb > 0, "pixel {i} must not be crushed to black: {px:?}"); @@ -1085,7 +1092,7 @@ mod tests { "grey stays grey through the display ICC: {b} {g} {r}" ); f32_result.expect("convert f32"); - for (i, px) in f32s.chunks_exact(4).enumerate() { + for (i, px) in f32s.as_chunks::<4>().0.iter().enumerate() { assert!((px[3] - 1.0).abs() < 1e-3, "pixel {i}: alpha preserved"); let rgb = px[0] + px[1] + px[2]; assert!(rgb > 0.0, "pixel {i} must not be crushed to black: {px:?}"); @@ -1171,7 +1178,10 @@ mod tests { stub.contains("ove_grading_primary"), "function name present" ); - assert!(!stub.contains("sampler"), "no LUT upload expected for grading"); + assert!( + !stub.contains("sampler"), + "no LUT upload expected for grading" + ); // Cache hit: a repeated call returns the same text. let again = grading_primary_function_shader(style).unwrap(); assert_eq!(stub, again); @@ -1200,8 +1210,14 @@ mod tests { "cie_xyz_d65_interchange", ) .expect("default config generates an analytic shader"); - assert!(stub.contains("SceneLinearToCIEXYZ_d65"), "function name present"); - assert!(!stub.contains("sampler"), "no LUT upload expected in the default config"); + assert!( + stub.contains("SceneLinearToCIEXYZ_d65"), + "function name present" + ); + assert!( + !stub.contains("sampler"), + "no LUT upload expected in the default config" + ); // Cache hit: a repeated call returns the same text. let again = ocio_function_shader( "SceneLinearToCIEXYZ_d65", @@ -1217,9 +1233,9 @@ mod tests { let _lock = config_lock(); // studio-config's Rec.709 display is a CLF LUT chain (unlike the // analytic sRGB one); without a LUT upload path it must be refused. - let Ok(cfg) = ocio_rs::Config::create_from_builtin_config( - "studio-config-v2.1.0_aces-v1.3_ocio-v2.3", - ) else { + let Ok(cfg) = + ocio_rs::Config::create_from_builtin_config("studio-config-v2.1.0_aces-v1.3_ocio-v2.3") + else { return; }; let cfg = SafeConfig(cfg); @@ -1234,4 +1250,3 @@ mod tests { ); } } - diff --git a/crates/oak-core/src/colormath.rs b/crates/oak-core/src/colormath.rs index e5b6fc6fb..8e9e1dd7a 100644 --- a/crates/oak-core/src/colormath.rs +++ b/crates/oak-core/src/colormath.rs @@ -495,15 +495,14 @@ const LUT_N: usize = 4096; /// Declare one lazily-built transfer LUT over [0, 1]. macro_rules! tf_lut { ($name:ident, $f:expr) => { - static $name: std::sync::LazyLock<[f32; LUT_N + 1]> = - std::sync::LazyLock::new(|| { - let f: fn(f32) -> f32 = $f; - let mut t = [0.0f32; LUT_N + 1]; - for (i, e) in t.iter_mut().enumerate() { - *e = f(i as f32 / LUT_N as f32); - } - t - }); + static $name: std::sync::LazyLock<[f32; LUT_N + 1]> = std::sync::LazyLock::new(|| { + let f: fn(f32) -> f32 = $f; + let mut t = [0.0f32; LUT_N + 1]; + for (i, e) in t.iter_mut().enumerate() { + *e = f(i as f32 / LUT_N as f32); + } + t + }); }; } @@ -663,8 +662,8 @@ pub enum SourceTransfer { /// (AVCOL_PRI_* numbering, H.273 ISO codes.) pub fn source_primaries_from_av(color_primaries: i32) -> SourcePrimaries { match color_primaries { - 1 => SourcePrimaries::Bt709, // BT.709 - 9 => SourcePrimaries::Bt2020, // BT.2020 + 1 => SourcePrimaries::Bt709, // BT.709 + 9 => SourcePrimaries::Bt2020, // BT.2020 11 => SourcePrimaries::DisplayP3, // SMPTE RP 431-2 (DCI-P3) 12 => SourcePrimaries::DisplayP3, // SMPTE EG 432-1 (Display P3) _ => SourcePrimaries::Unknown, @@ -675,16 +674,16 @@ pub fn source_primaries_from_av(color_primaries: i32) -> SourcePrimaries { /// (AVCOL_TRC_* numbering, H.273 ISO codes.) pub fn source_transfer_from_av(color_trc: i32) -> SourceTransfer { match color_trc { - 1 => SourceTransfer::SdrGamma, // BT.709 (see the enum docs: OETF inverse ≈ 2.2) - 4 => SourceTransfer::Gamma22, // gamma 2.2 - 5 => SourceTransfer::Gamma28, // gamma 2.8 - 6 => SourceTransfer::SdrGamma, // SMPTE 170M + 1 => SourceTransfer::SdrGamma, // BT.709 (see the enum docs: OETF inverse ≈ 2.2) + 4 => SourceTransfer::Gamma22, // gamma 2.2 + 5 => SourceTransfer::Gamma28, // gamma 2.8 + 6 => SourceTransfer::SdrGamma, // SMPTE 170M 13 => SourceTransfer::SdrGamma, // sRGB (its EOTF is applied at decode) 14 => SourceTransfer::SdrGamma, // BT.2020 10-bit 15 => SourceTransfer::SdrGamma, // BT.2020 12-bit - 16 => SourceTransfer::Pq, // SMPTE ST 2084 - 18 => SourceTransfer::Hlg, // ARIB STD-B67 - 8 => SourceTransfer::Linear, // linear + 16 => SourceTransfer::Pq, // SMPTE ST 2084 + 18 => SourceTransfer::Hlg, // ARIB STD-B67 + 8 => SourceTransfer::Linear, // linear _ => SourceTransfer::Unknown, } } @@ -708,15 +707,11 @@ impl SourcePrimaries { /// own primaries) → ACEScg linear. `samples` is an F32 RGBA buffer, /// transformed in place. Linearize and the primaries matrix are fused /// into a single pass (LUT'd transfer, row-band parallel). -pub fn decode_to_acescg( - samples: &mut [f32], - primaries: SourcePrimaries, - transfer: SourceTransfer, -) { +pub fn decode_to_acescg(samples: &mut [f32], primaries: SourcePrimaries, transfer: SourceTransfer) { let linearize = decode_transfer_fn(transfer); let matrix = rgb_to_rgb_matrix(primaries.primaries(), PRIMARIES_AP1); par_pixels_f32(samples, move |band| { - for px in band.chunks_exact_mut(4) { + for px in band.as_chunks_mut::<4>().0 { for c in 0..3 { px[c] = linearize(px[c]); } @@ -740,7 +735,7 @@ pub fn decode_to_acescg_bytes( let linearize = decode_transfer_fn(transfer); let matrix = rgb_to_rgb_matrix(primaries.primaries(), PRIMARIES_AP1); par_pixels_bytes(bytes, move |band| { - for px in band.chunks_exact_mut(16) { + for px in band.as_chunks_mut::<16>().0 { let mut v = [ f32::from_le_bytes(px[0..4].try_into().unwrap()), f32::from_le_bytes(px[4..8].try_into().unwrap()), @@ -772,7 +767,7 @@ pub fn acescg_to_output(samples: &mut [f32], spec: OutputColorSpec) { let matrix = rgb_to_rgb_matrix(PRIMARIES_AP1, spec.gamut.primaries()); let encode = output_transfer_fn(spec.transfer); par_pixels_f32(samples, move |band| { - for px in band.chunks_exact_mut(4) { + for px in band.as_chunks_mut::<4>().0 { apply_mat(matrix, px); for c in 0..3 { px[c] = encode(px[c].clamp(0.0, 1.0)); @@ -792,7 +787,7 @@ pub fn acescg_to_output_bytes(bytes: &mut [u8], pixels: usize, spec: OutputColor let matrix = rgb_to_rgb_matrix(PRIMARIES_AP1, spec.gamut.primaries()); let encode = output_transfer_fn(spec.transfer); par_pixels_bytes(bytes, move |band| { - for px in band.chunks_exact_mut(16) { + for px in band.as_chunks_mut::<16>().0 { let mut v = [ f32::from_le_bytes(px[0..4].try_into().unwrap()), f32::from_le_bytes(px[4..8].try_into().unwrap()), @@ -816,7 +811,7 @@ pub fn acescg_to_output_bytes(bytes: &mut [u8], pixels: usize, spec: OutputColor pub fn apply_transfer_oetf(samples: &mut [f32], transfer: OutputTransfer) { let encode = output_transfer_fn(transfer); par_pixels_f32(samples, move |band| { - for px in band.chunks_exact_mut(4) { + for px in band.as_chunks_mut::<4>().0 { for c in 0..3 { px[c] = encode(px[c]); } @@ -860,7 +855,7 @@ pub fn output_spec_to_xyz_d65(samples: &mut [f32], spec: OutputColorSpec) { }); let matrix = rgb_to_xyz_matrix(spec.gamut.primaries()); par_pixels_f32(samples, move |band| { - for px in band.chunks_exact_mut(4) { + for px in band.as_chunks_mut::<4>().0 { for c in 0..3 { px[c] = linearize(px[c]); } @@ -931,9 +926,18 @@ pub fn yuv444p16_to_rgb_f32( // `width` samples. let row_bytes = width.saturating_mul(2); let rows_after = height.saturating_sub(1); - if y_plane.len() < rows_after.saturating_mul(y_stride).saturating_add(row_bytes) - || u_plane.len() < rows_after.saturating_mul(u_stride).saturating_add(row_bytes) - || v_plane.len() < rows_after.saturating_mul(v_stride).saturating_add(row_bytes) + if y_plane.len() + < rows_after + .saturating_mul(y_stride) + .saturating_add(row_bytes) + || u_plane.len() + < rows_after + .saturating_mul(u_stride) + .saturating_add(row_bytes) + || v_plane.len() + < rows_after + .saturating_mul(v_stride) + .saturating_add(row_bytes) { return; } @@ -946,7 +950,11 @@ pub fn yuv444p16_to_rgb_f32( } else { (4096.0f32, 32768.0f32, 1.0 / 57344.0) }; - let y_scale = if full_range { 1.0 / 65535.0 } else { 1.0 / 56064.0 }; + let y_scale = if full_range { + 1.0 / 65535.0 + } else { + 1.0 / 56064.0 + }; for row in 0..height { let y_row = row * y_stride; let u_row = row * u_stride; @@ -1002,9 +1010,9 @@ mod tests { // 0.0206157712 0.1095697056 0.8698145232 let m = rgb_to_rgb_matrix(PRIMARIES_SRGB, PRIMARIES_AP1); let expected: Mat3 = [ - [0.6131324224, 0.3395230762, 0.0473445014], - [0.0701922769, 0.9163536767, 0.0134540464], - [0.0206157712, 0.1095697056, 0.8698145232], + [0.613_132_4, 0.339_523_08, 0.047_344_502], + [0.070_192_28, 0.916_353_7, 0.013_454_046], + [0.020_615_771, 0.109_569_706, 0.869_814_5], ]; for r in 0..3 { for c in 0..3 { @@ -1034,7 +1042,11 @@ mod tests { // → sRGB linear → (OETF) → the original codes. let original = [0.25f32, 0.5, 0.75, 1.0]; let mut samples = original; - decode_to_acescg(&mut samples, SourcePrimaries::Bt709, SourceTransfer::SdrGamma); + decode_to_acescg( + &mut samples, + SourcePrimaries::Bt709, + SourceTransfer::SdrGamma, + ); acescg_to_output( &mut samples, OutputColorSpec { @@ -1056,7 +1068,11 @@ mod tests { fn round_trip_bt2020_acescg_bt2020() { let original = [0.2f32, 0.6, 0.9, 1.0]; let mut samples = original; - decode_to_acescg(&mut samples, SourcePrimaries::Bt2020, SourceTransfer::SdrGamma); + decode_to_acescg( + &mut samples, + SourcePrimaries::Bt2020, + SourceTransfer::SdrGamma, + ); acescg_to_output( &mut samples, OutputColorSpec { @@ -1081,7 +1097,11 @@ mod tests { let mut samples = [1.0f32, 1.0, 1.0, 1.0]; decode_to_acescg(&mut samples, SourcePrimaries::Bt709, SourceTransfer::Linear); for i in 0..3 { - assert!(approx(samples[i], 1.0, 1e-3), "white channel {i} = {}", samples[i]); + assert!( + approx(samples[i], 1.0, 1e-3), + "white channel {i} = {}", + samples[i] + ); } } @@ -1093,7 +1113,11 @@ mod tests { assert!(approx(srgb_eotf(0.0), 0.0, 1e-6), "eotf(0)"); assert!(approx(srgb_eotf(1.0), 1.0, 1e-6), "eotf(1)"); // Mid-grey: linear 0.18 → ~0.461 sRGB code (the classic check). - assert!(approx(srgb_oetf(0.18), 0.4613, 1e-3), "oetf(0.18) = {}", srgb_oetf(0.18)); + assert!( + approx(srgb_oetf(0.18), 0.4613, 1e-3), + "oetf(0.18) = {}", + srgb_oetf(0.18) + ); // Round trip. for v in [0.0f32, 0.01, 0.18, 0.5, 0.99, 1.0] { assert!(approx(srgb_eotf(srgb_oetf(v)), v, 1e-4), "round trip {v}"); @@ -1103,9 +1127,17 @@ mod tests { #[test] fn pq_transfer_anchors() { // PQ code 1.0 = 10 000 nits = normalized 1.0. - assert!(approx(pq_eotf(1.0), 1.0, 1e-4), "pq eotf(1) = {}", pq_eotf(1.0)); + assert!( + approx(pq_eotf(1.0), 1.0, 1e-4), + "pq eotf(1) = {}", + pq_eotf(1.0) + ); // PQ code 0.5 ≈ 100 nits (normalized 0.01). - assert!(approx(pq_eotf(0.5), 0.01008, 2e-3), "pq eotf(0.5) = {}", pq_eotf(0.5)); + assert!( + approx(pq_eotf(0.5), 0.01008, 2e-3), + "pq eotf(0.5) = {}", + pq_eotf(0.5) + ); // Round trip. for v in [0.0f32, 0.25, 0.5, 0.75, 1.0] { assert!(approx(pq_eotf(pq_oetf(v)), v, 1e-3), "pq round trip {v}"); @@ -1115,9 +1147,17 @@ mod tests { #[test] fn hlg_transfer_anchors() { assert!(approx(hlg_eotf(0.0), 0.0, 1e-6), "hlg eotf(0)"); - assert!(approx(hlg_eotf(1.0), 1.0, 1e-3), "hlg eotf(1) = {}", hlg_eotf(1.0)); + assert!( + approx(hlg_eotf(1.0), 1.0, 1e-3), + "hlg eotf(1) = {}", + hlg_eotf(1.0) + ); // The piecewise join: code 0.5 ↔ linear 1/12. - assert!(approx(hlg_eotf(0.5), 1.0 / 12.0, 1e-3), "hlg eotf(0.5) = {}", hlg_eotf(0.5)); + assert!( + approx(hlg_eotf(0.5), 1.0 / 12.0, 1e-3), + "hlg eotf(0.5) = {}", + hlg_eotf(0.5) + ); // Round trip. for v in [0.0f32, 0.1, 0.5, 0.9, 1.0] { assert!(approx(hlg_eotf(hlg_oetf(v)), v, 1e-3), "hlg round trip {v}"); @@ -1146,18 +1186,30 @@ mod tests { #[test] fn setting_round_trips() { - assert_eq!(WorkingColorSpace::from_setting("acescg"), WorkingColorSpace::AcesCg); + assert_eq!( + WorkingColorSpace::from_setting("acescg"), + WorkingColorSpace::AcesCg + ); assert_eq!( WorkingColorSpace::from_setting("srgb_legacy"), WorkingColorSpace::SrgbLegacy ); - assert_eq!(WorkingColorSpace::from_setting("bogus"), WorkingColorSpace::AcesCg); - assert_eq!(OutputGamut::from_setting("displayp3"), OutputGamut::DisplayP3); + assert_eq!( + WorkingColorSpace::from_setting("bogus"), + WorkingColorSpace::AcesCg + ); + assert_eq!( + OutputGamut::from_setting("displayp3"), + OutputGamut::DisplayP3 + ); assert_eq!(OutputGamut::from_setting("bt2020"), OutputGamut::Bt2020); assert_eq!(OutputGamut::from_setting(""), OutputGamut::Srgb); assert_eq!(OutputTransfer::from_setting("pq"), OutputTransfer::Pq); assert_eq!(OutputTransfer::from_setting("hlg"), OutputTransfer::Hlg); - assert_eq!(OutputTransfer::from_setting("gamma22"), OutputTransfer::Gamma22); + assert_eq!( + OutputTransfer::from_setting("gamma22"), + OutputTransfer::Gamma22 + ); assert_eq!(OutputTransfer::from_setting(""), OutputTransfer::Srgb); assert_eq!( OutputColorSpec::from_settings("displayp3", "pq"), @@ -1184,13 +1236,31 @@ mod tests { fn gamma_transfer_anchors() { // Pure-power EOTFs at code 0.5. The 2.4 case is the BT.1886 // mid-grey link: 0.5²·⁴ ≈ 0.189 (≈ the classic 18 % grey). - assert!(approx(gamma_eotf(0.5, 2.4), 0.1894646, 1e-5), "eotf(0.5, 2.4) = {}", gamma_eotf(0.5, 2.4)); - assert!(approx(gamma_eotf(0.5, 2.2), 0.2176376, 1e-5), "eotf(0.5, 2.2) = {}", gamma_eotf(0.5, 2.2)); - assert!(approx(gamma_eotf(0.5, 2.8), 0.1435894, 1e-5), "eotf(0.5, 2.8) = {}", gamma_eotf(0.5, 2.8)); + assert!( + approx(gamma_eotf(0.5, 2.4), 0.1894646, 1e-5), + "eotf(0.5, 2.4) = {}", + gamma_eotf(0.5, 2.4) + ); + assert!( + approx(gamma_eotf(0.5, 2.2), 0.2176376, 1e-5), + "eotf(0.5, 2.2) = {}", + gamma_eotf(0.5, 2.2) + ); + assert!( + approx(gamma_eotf(0.5, 2.8), 0.1435894, 1e-5), + "eotf(0.5, 2.8) = {}", + gamma_eotf(0.5, 2.8) + ); // Endpoints and round trips. for gamma in [2.2f32, 2.4, 2.8] { - assert!(approx(gamma_eotf(0.0, gamma), 0.0, 1e-6), "eotf(0) g{gamma}"); - assert!(approx(gamma_eotf(1.0, gamma), 1.0, 1e-6), "eotf(1) g{gamma}"); + assert!( + approx(gamma_eotf(0.0, gamma), 0.0, 1e-6), + "eotf(0) g{gamma}" + ); + assert!( + approx(gamma_eotf(1.0, gamma), 1.0, 1e-6), + "eotf(1) g{gamma}" + ); for v in [0.0f32, 0.18, 0.5, 0.9, 1.0] { assert!( approx(gamma_eotf(gamma_oetf(v, gamma), gamma), v, 1e-4), @@ -1244,14 +1314,20 @@ mod tests { .map(|i: usize| ((i.wrapping_mul(2654435761)) % 1000) as f32 / 999.0) .collect(); let reference: Vec = samples - .chunks_exact(4) + .as_chunks::<4>() + .0 + .iter() .flat_map(|px| { let mut v = [srgb_eotf(px[0]), srgb_eotf(px[1]), srgb_eotf(px[2]), px[3]]; apply_mat(rgb_to_rgb_matrix(PRIMARIES_SRGB, PRIMARIES_AP1), &mut v); v }) .collect(); - decode_to_acescg(&mut samples, SourcePrimaries::Bt709, SourceTransfer::SdrGamma); + decode_to_acescg( + &mut samples, + SourcePrimaries::Bt709, + SourceTransfer::SdrGamma, + ); for (i, (&got, &want)) in samples.iter().zip(reference.iter()).enumerate() { assert!(approx(got, want, 1e-4), "pixel {i}: {got} vs {want}"); } @@ -1267,7 +1343,11 @@ mod tests { // (the ~1.1 system gamma belongs at final display only). for code in [0.05f32, 0.18, 0.5, 0.75, 1.0] { let mut samples = [code, code, code, 1.0]; - decode_to_acescg(&mut samples, SourcePrimaries::Bt709, SourceTransfer::SdrGamma); + decode_to_acescg( + &mut samples, + SourcePrimaries::Bt709, + SourceTransfer::SdrGamma, + ); acescg_to_output(&mut samples, OutputColorSpec::default()); for i in 0..3 { assert!( @@ -1283,7 +1363,12 @@ mod tests { bytes[0..4].copy_from_slice(&0.5f32.to_le_bytes()); bytes[4..8].copy_from_slice(&0.5f32.to_le_bytes()); bytes[8..12].copy_from_slice(&0.5f32.to_le_bytes()); - decode_to_acescg_bytes(&mut bytes, 1, SourcePrimaries::Bt709, SourceTransfer::SdrGamma); + decode_to_acescg_bytes( + &mut bytes, + 1, + SourcePrimaries::Bt709, + SourceTransfer::SdrGamma, + ); let r = f32::from_le_bytes(bytes[0..4].try_into().unwrap()); let m = rgb_to_rgb_matrix(PRIMARIES_SRGB, PRIMARIES_AP1); let expected = mat_vec(m, [srgb_eotf(0.5); 3]); @@ -1321,7 +1406,11 @@ mod tests { samples[c], expected[c] ); - assert!(samples[c] >= 0.0 && samples[c] <= 1.0, "channel {c} escaped [0, 1]: {}", samples[c]); + assert!( + samples[c] >= 0.0 && samples[c] <= 1.0, + "channel {c} escaped [0, 1]: {}", + samples[c] + ); } assert_eq!(samples[3], 0.5, "alpha must be untouched"); } @@ -1373,10 +1462,26 @@ mod tests { let u = yuv_plane16(&[&[128 << 8], &[128 << 8]], width, stride); let v = u.clone(); let mut out = [0.0f32; 8]; - yuv444p16_to_rgb_f32(&y, stride, &u, stride, &v, stride, width, height, YuvMatrix::Bt709, false, &mut out); + yuv444p16_to_rgb_f32( + &y, + stride, + &u, + stride, + &v, + stride, + width, + height, + YuvMatrix::Bt709, + false, + &mut out, + ); for c in 0..3 { assert!(approx(out[c], 1.0, 1e-4), "white channel {c}: {}", out[c]); - assert!(approx(out[4 + c], 0.0, 1e-4), "black channel {c}: {}", out[4 + c]); + assert!( + approx(out[4 + c], 0.0, 1e-4), + "black channel {c}: {}", + out[4 + c] + ); } assert_eq!(out[3], 1.0, "alpha"); assert_eq!(out[7], 1.0, "alpha"); @@ -1394,7 +1499,19 @@ mod tests { let u = yuv_plane16(&[&[11059]], width, stride); let v = yuv_plane16(&[&[5329]], width, stride); let mut out = [0.0f32; 4]; - yuv444p16_to_rgb_f32(&y, stride, &u, stride, &v, stride, width, height, YuvMatrix::Bt601, true, &mut out); + yuv444p16_to_rgb_f32( + &y, + stride, + &u, + stride, + &v, + stride, + width, + height, + YuvMatrix::Bt601, + true, + &mut out, + ); assert!(approx(out[0], 0.0, 1e-3), "R = {}", out[0]); assert!(approx(out[1], 1.0, 1e-3), "G = {}", out[1]); assert!(approx(out[2], 0.0, 1e-3), "B = {}", out[2]); @@ -1412,7 +1529,19 @@ mod tests { let u = yuv_plane16(&[&[10666]], width, stride); let v = yuv_plane16(&[&[6725]], width, stride); let mut out = [0.0f32; 4]; - yuv444p16_to_rgb_f32(&y, stride, &u, stride, &v, stride, width, height, YuvMatrix::Bt709, false, &mut out); + yuv444p16_to_rgb_f32( + &y, + stride, + &u, + stride, + &v, + stride, + width, + height, + YuvMatrix::Bt709, + false, + &mut out, + ); assert!(approx(out[0], 0.0, 1e-3), "R = {}", out[0]); assert!(approx(out[1], 1.0, 1e-3), "G = {}", out[1]); assert!(approx(out[2], 0.0, 1e-3), "B = {}", out[2]); @@ -1423,11 +1552,35 @@ mod tests { // A 2×2 frame at stride 4 needs (2−1)·4 + 2·2 = 8 bytes per plane; // 4 bytes is too short → early return, `out` untouched. let mut out = [7.0f32; 4]; - yuv444p16_to_rgb_f32(&[0u8; 4], 4, &[0u8; 4], 4, &[0u8; 4], 4, 2, 2, YuvMatrix::Bt709, false, &mut out); + yuv444p16_to_rgb_f32( + &[0u8; 4], + 4, + &[0u8; 4], + 4, + &[0u8; 4], + 4, + 2, + 2, + YuvMatrix::Bt709, + false, + &mut out, + ); assert_eq!(out, [7.0; 4]); // `out` too short for the frame → early return. let mut out2 = [7.0f32; 3]; - yuv444p16_to_rgb_f32(&[0u8; 8], 4, &[0u8; 8], 4, &[0u8; 8], 4, 2, 2, YuvMatrix::Bt709, false, &mut out2); + yuv444p16_to_rgb_f32( + &[0u8; 8], + 4, + &[0u8; 8], + 4, + &[0u8; 8], + 4, + 2, + 2, + YuvMatrix::Bt709, + false, + &mut out2, + ); assert_eq!(out2, [7.0; 3]); } diff --git a/crates/oak-core/src/configstore.rs b/crates/oak-core/src/configstore.rs index a8041b687..37a7ae8b7 100644 --- a/crates/oak-core/src/configstore.rs +++ b/crates/oak-core/src/configstore.rs @@ -308,10 +308,7 @@ impl ConfigStore { Some(slash) => { let group = key[..slash].to_string(); let sub = key[slash + 1..].to_string(); - sections - .entry(group) - .or_default() - .insert(sub, toml_value); + sections.entry(group).or_default().insert(sub, toml_value); } None => { sections @@ -823,7 +820,7 @@ fn format_g(v: f64) -> String { let sci = format!("{:.*e}", (PRECISION - 1) as usize, a); // "d.ddddd e±N" let exp = sci.split('e').nth(1).unwrap().parse::().unwrap(); let xr = exp; - let body = if xr < -4 || xr >= PRECISION { + let body = if !(-4..PRECISION).contains(&xr) { // Scientific notation: mantissa is already rounded; strip trailing zeros. let mant = sci.split('e').next().unwrap(); let m = trim_mantissa(mant); @@ -1164,7 +1161,11 @@ mod tests { let content = std::fs::read_to_string(dir.join("config.toml")).unwrap(); // Native TOML types: ints/bools/floats unquoted, strings quoted. assert!(content.contains("FlatKey = 1"), "content: {}", content); - assert!(content.contains("StrKey = \"hello\""), "content: {}", content); + assert!( + content.contains("StrKey = \"hello\""), + "content: {}", + content + ); assert!(content.contains("DblKey = 2.5"), "content: {}", content); assert!(content.contains("BoolKey = true"), "content: {}", content); // Grouped keys land in [group] tables. @@ -1262,9 +1263,21 @@ UnknownTypedThing=hello assert_eq!(s.get_double(Some("render"), "gain", -1.0), -1.0); // The file holds native TOML types, not stringified values. let content = std::fs::read_to_string(dir.join("config.toml")).unwrap(); - assert!(content.contains("DefaultSequenceWidth = 640"), "content: {}", content); - assert!(content.contains("SplitClipsCopyNodes = false"), "content: {}", content); - assert!(content.contains("ProxyPreset = \"medium\""), "content: {}", content); + assert!( + content.contains("DefaultSequenceWidth = 640"), + "content: {}", + content + ); + assert!( + content.contains("SplitClipsCopyNodes = false"), + "content: {}", + content + ); + assert!( + content.contains("ProxyPreset = \"medium\""), + "content: {}", + content + ); assert!(content.contains("gain = 1.5"), "content: {}", content); }); } @@ -1293,10 +1306,22 @@ CustomKey=hello assert!(dir.join("config.toml").is_file()); assert!(dir.join("config.ini").is_file()); let content = std::fs::read_to_string(dir.join("config.toml")).unwrap(); - assert!(content.contains("DefaultSequenceWidth = 640"), "content: {}", content); - assert!(content.contains("UseProxyMedia = false"), "content: {}", content); + assert!( + content.contains("DefaultSequenceWidth = 640"), + "content: {}", + content + ); + assert!( + content.contains("UseProxyMedia = false"), + "content: {}", + content + ); assert!(content.contains("[section]"), "content: {}", content); - assert!(content.contains("CustomKey = \"hello\""), "content: {}", content); + assert!( + content.contains("CustomKey = \"hello\""), + "content: {}", + content + ); // A second load reads the migrated TOML, not the INI. s.reset_defaults().unwrap(); @@ -1615,7 +1640,11 @@ CustomKey=hello // No group defaults exist; every top-level key is a plain value. assert!(table.values().all(|v| !v.is_table())); // Serialized output must not contain INI-style "k=v" lines. - assert!(!content.contains("DefaultSequenceWidth=1920"), "content: {}", content); + assert!( + !content.contains("DefaultSequenceWidth=1920"), + "content: {}", + content + ); }); } @@ -1698,7 +1727,7 @@ FlatAfterEmptySection=ok assert_eq!(s.get(Some("g"), "KeyWithEquals").unwrap(), "a=b"); // "[]" empties the group, so the key is flat. assert_eq!(s.get(None, "FlatAfterEmptySection").unwrap(), "ok"); - assert!(matches!(s.get(Some(""), "FlatAfterEmptySection"), Ok(_))); + assert!(s.get(Some(""), "FlatAfterEmptySection").is_ok()); }); } diff --git a/crates/oak-core/src/displayicc.rs b/crates/oak-core/src/displayicc.rs index 577d95510..58022abef 100644 --- a/crates/oak-core/src/displayicc.rs +++ b/crates/oak-core/src/displayicc.rs @@ -229,9 +229,7 @@ fn parse_xprop_icc_hex(output: &str) -> Option> { n_digits += 1; } if n_digits > 0 { - if let Ok(byte) = - u8::from_str_radix(&rest[digits_start..digits_start + n_digits], 16) - { + if let Ok(byte) = u8::from_str_radix(&rest[digits_start..digits_start + n_digits], 16) { bytes.push(byte); } } @@ -563,9 +561,8 @@ mod windows { let sub_key = wide_nul(r"Software\Microsoft\Windows\CurrentVersion\VideoSettings"); let value_name = wide_nul("EnableAutoColorManagement"); let mut key: *mut c_void = std::ptr::null_mut(); - let status = unsafe { - RegOpenKeyExW(HKEY_CURRENT_USER, sub_key.as_ptr(), 0, KEY_READ, &mut key) - }; + let status = + unsafe { RegOpenKeyExW(HKEY_CURRENT_USER, sub_key.as_ptr(), 0, KEY_READ, &mut key) }; if status != ERROR_SUCCESS { return None; } @@ -628,7 +625,12 @@ mod windows { pub(super) fn display_icc_for(device: &str) -> Option { let name = wide_nul(device); let hdc = unsafe { - CreateDCW(name.as_ptr(), name.as_ptr(), std::ptr::null(), std::ptr::null()) + CreateDCW( + name.as_ptr(), + name.as_ptr(), + std::ptr::null(), + std::ptr::null(), + ) }; if hdc.is_null() { return None; @@ -660,7 +662,10 @@ mod windows { sz_device: [0; 32], }; let ok = unsafe { - GetMonitorInfoW(hmonitor as *const c_void, &mut info as *mut _ as *mut c_void) + GetMonitorInfoW( + hmonitor as *const c_void, + &mut info as *mut _ as *mut c_void, + ) }; if ok == 0 { return None; @@ -864,7 +869,7 @@ mod linux { .lines() .map(str::trim_start) .find(|l| l.starts_with("Filename:"))?; - let name = line.splitn(2, ':').nth(1)?.trim(); + let name = line.split_once(':')?.1.trim(); if name.is_empty() { None } else { @@ -1066,7 +1071,10 @@ Monitors: 1 #[test] fn parse_xrandr_monitors_malformed() { assert_eq!(parse_xrandr_monitors(""), Vec::::new()); - assert_eq!(parse_xrandr_monitors("Monitors: 0\n"), Vec::::new()); + assert_eq!( + parse_xrandr_monitors("Monitors: 0\n"), + Vec::::new() + ); // No `:` separator, a missing geometry, and a geometry with no // outputs are all skipped without panicking. let out = "\ diff --git a/crates/oak-core/src/filefunctions.rs b/crates/oak-core/src/filefunctions.rs index a90bed2bb..5b035addd 100644 --- a/crates/oak-core/src/filefunctions.rs +++ b/crates/oak-core/src/filefunctions.rs @@ -51,7 +51,7 @@ fn ends_with_case_insensitive(s: &str, suffix: &str) -> bool { s.as_bytes()[offset..] .iter() .zip(suffix.as_bytes().iter()) - .all(|(a, b)| a.to_ascii_lowercase() == b.to_ascii_lowercase()) + .all(|(a, b)| a.eq_ignore_ascii_case(b)) } /// The filefunctions family (stateless; the handle only exists for C ABI @@ -424,6 +424,33 @@ pub(crate) fn config_location_path() -> Result { .map(PathBuf::from) } +/// The default disk cache directory (C++ `DiskManager:: +/// get_default_disk_cache_path`): `/mediacache`. +/// +/// The `DiskCachePath` config key overrides the location when set (the +/// preferences dialog's cache-directory setting); an empty/absent value +/// keeps the default. +/// +/// Single-lib unification: this used to live in the oakrender crate's +/// `bridge::common` fallback (see `docs/zh/plans/riir/single-lib.md`); +/// oaknode and oakrender both call it directly now. +pub fn default_disk_cache_path() -> String { + // A configured override wins (whitespace-only counts as absent). + if let Ok(custom) = crate::configstore::ConfigStore::instance().get(None, "DiskCachePath") { + if !custom.trim().is_empty() { + return custom; + } + } + Path::new( + &FileFunctions::new() + .get_configuration_location() + .unwrap_or_default(), + ) + .join("mediacache") + .to_string_lossy() + .into_owned() +} + #[cfg(test)] mod tests { use super::*; @@ -927,30 +954,3 @@ mod tests { let _ = std::fs::remove_dir_all(&b); } } - -/// The default disk cache directory (C++ `DiskManager:: -/// get_default_disk_cache_path`): `/mediacache`. -/// -/// The `DiskCachePath` config key overrides the location when set (the -/// preferences dialog's cache-directory setting); an empty/absent value -/// keeps the default. -/// -/// Single-lib unification: this used to live in the oakrender crate's -/// `bridge::common` fallback (see `docs/zh/plans/riir/single-lib.md`); -/// oaknode and oakrender both call it directly now. -pub fn default_disk_cache_path() -> String { - // A configured override wins (whitespace-only counts as absent). - if let Ok(custom) = crate::configstore::ConfigStore::instance().get(None, "DiskCachePath") { - if !custom.trim().is_empty() { - return custom; - } - } - Path::new( - &FileFunctions::new() - .get_configuration_location() - .unwrap_or_default(), - ) - .join("mediacache") - .to_string_lossy() - .into_owned() -} diff --git a/crates/oak-core/src/miscutils.rs b/crates/oak-core/src/miscutils.rs index 505261d3c..283018158 100644 --- a/crates/oak-core/src/miscutils.rs +++ b/crates/oak-core/src/miscutils.rs @@ -482,7 +482,7 @@ mod tests { assert!(cur.get_video_params().unwrap().is_null()); // Store with a destructor. - let p1 = 0x1 as *mut c_void; + let p1 = std::ptr::dangling_mut::(); assert!(cur.set_video_params(p1, Some(count_destroy)).is_ok()); assert_eq!(cur.get_video_params().unwrap(), p1); diff --git a/crates/oak-core/src/subtitleparams.rs b/crates/oak-core/src/subtitleparams.rs index d41a0a2ce..f254a692f 100644 --- a/crates/oak-core/src/subtitleparams.rs +++ b/crates/oak-core/src/subtitleparams.rs @@ -271,10 +271,10 @@ impl SubtitleParams { out.push_str("'); out.push_str(&escape_text(&sub.text)); out.push_str(""); diff --git a/crates/oak-core/src/texture.rs b/crates/oak-core/src/texture.rs index 31867b012..8d584883e 100644 --- a/crates/oak-core/src/texture.rs +++ b/crates/oak-core/src/texture.rs @@ -16,8 +16,8 @@ //! Textures and CPU frames. -use crate::Rational; use crate::PixelFormat; +use crate::Rational; use std::sync::Arc; use crate::backend::{BackendKind, GpuContextLike}; @@ -385,7 +385,7 @@ mod tests { let pod = f.video_params(); assert_eq!(pod.width, 3); assert_eq!(pod.pixel_aspect_num, 2); - assert_eq!(f.is_float(), true); + assert!(f.is_float()); assert_eq!(f.pixel_count(), 6); f.allocate(); assert!(!f.data().is_null()); diff --git a/crates/oak-core/src/videoparams.rs b/crates/oak-core/src/videoparams.rs index 647de9fd4..4ca761aae 100644 --- a/crates/oak-core/src/videoparams.rs +++ b/crates/oak-core/src/videoparams.rs @@ -703,9 +703,9 @@ impl VideoParams { let divider = squared_divider.sqrt(); if divider <= DIVIDERS[0] as f64 { - return DIVIDERS[0]; + DIVIDERS[0] } else if divider >= DIVIDERS[7] as f64 { - return DIVIDERS[7]; + DIVIDERS[7] } else { for i in 1..DIVIDERS.len() { let prev = DIVIDERS[i - 1]; @@ -716,7 +716,7 @@ impl VideoParams { return if prev_diff < next_diff { prev } else { next }; } } - return 1; // fallback, unreachable + 1 // fallback, unreachable } } @@ -1075,7 +1075,7 @@ fn format_g(value: f64) -> String { e += 1; } - let body = if PREC > e && e >= -4 { + let body = if (-4..PREC).contains(&e) { let decimals = (PREC - 1 - e).max(0) as usize; trim_frac_trailing(format!("{:.*}", decimals, v)) } else { diff --git a/crates/oak-node/src/footage.rs b/crates/oak-node/src/footage.rs index d6139a402..9a01a49f7 100644 --- a/crates/oak-node/src/footage.rs +++ b/crates/oak-node/src/footage.rs @@ -537,7 +537,7 @@ impl NodeBehavior for FootageBehavior { if let Some(v) = reader.attribute("ppreset") { if !v.is_empty() { let mut a = [0u8; 32]; - let n = v.as_bytes().len().min(31); + let n = v.len().min(31); a[..n].copy_from_slice(&v.as_bytes()[..n]); custom.preset = a; } @@ -545,7 +545,7 @@ impl NodeBehavior for FootageBehavior { if let Some(v) = reader.attribute("pext") { if !v.is_empty() { let mut a = [0u8; 32]; - let n = v.as_bytes().len().min(31); + let n = v.len().min(31); a[..n].copy_from_slice(&v.as_bytes()[..n]); custom.extension = a; } diff --git a/crates/oak-node/src/keyframe.rs b/crates/oak-node/src/keyframe.rs index 518158e24..eeec01ef2 100644 --- a/crates/oak-node/src/keyframe.rs +++ b/crates/oak-node/src/keyframe.rs @@ -130,12 +130,7 @@ impl KeyframeTrack { } } - let (b, a) = match before { - Some(pair) => pair, - // Unreachable given the front/back guards; C++ logs and falls - // through to the standard value. - None => return None, - }; + let (b, a) = before?; let before_key = &keys[b]; let after_key = &keys[a]; diff --git a/crates/oak-node/src/nodes/blur.rs b/crates/oak-node/src/nodes/blur.rs index a3ea0a4ff..b2f3252ab 100644 --- a/crates/oak-node/src/nodes/blur.rs +++ b/crates/oak-node/src/nodes/blur.rs @@ -364,12 +364,11 @@ impl NodeBehavior for BlurFilterNode { let mut can_push_job = true; if radius > 0.0 { // Method-specific considerations. - if method == Method::Box as i64 || method == Method::Gaussian as i64 { - if !horiz && !vert { + if (method == Method::Box as i64 || method == Method::Gaussian as i64) + && !horiz && !vert { // Disable the job if both directions are unchecked. can_push_job = false; } - } } else { can_push_job = false; } @@ -378,11 +377,10 @@ impl NodeBehavior for BlurFilterNode { // all other methods are single-pass (C++ `iterations = 2` only for // the double-pass case). let mut iterations = 1; - if method == Method::Box as i64 || method == Method::Gaussian as i64 { - if horiz && vert { + if (method == Method::Box as i64 || method == Method::Gaussian as i64) + && horiz && vert { iterations = 2; } - } if can_push_job { // The shader-job box (C++ ShaderJob): the behavior's type id @@ -578,6 +576,17 @@ pub fn create() -> (NodeCore, Box) { (core, Box::new(BlurFilterNode)) } +/// Register this node type (C++ `k_blur_filter` in +/// `factory.cpp::create_from_factory_index`). +pub fn register(meta: &mut Vec) { + meta.push(NodeMeta { + type_id: "org.olivevideoeditor.Olive.blur", + name: "Blur", + categories: &[Category::Filter], + create, + }); +} + #[cfg(test)] mod tests { use super::*; @@ -736,14 +745,3 @@ mod tests { assert_eq!(dup.name(), "Blur"); } } - -/// Register this node type (C++ `k_blur_filter` in -/// `factory.cpp::create_from_factory_index`). -pub fn register(meta: &mut Vec) { - meta.push(NodeMeta { - type_id: "org.olivevideoeditor.Olive.blur", - name: "Blur", - categories: &[Category::Filter], - create, - }); -} diff --git a/crates/oak-node/src/nodes/checkerboard.rs b/crates/oak-node/src/nodes/checkerboard.rs index f4f9e30a0..3e0d1c5e1 100644 --- a/crates/oak-node/src/nodes/checkerboard.rs +++ b/crates/oak-node/src/nodes/checkerboard.rs @@ -234,6 +234,16 @@ pub fn create() -> (NodeCore, Box) { (core, Box::new(CheckerBoardNode)) } +/// Register this node type. +pub fn register(meta: &mut Vec) { + meta.push(NodeMeta { + type_id: "org.olivevideoeditor.Olive.checkerboard", + name: "Checkerboard", + categories: &[Category::Generator], + create, + }); +} + #[cfg(test)] mod tests { use super::*; @@ -398,13 +408,3 @@ mod tests { assert_eq!(dup.name(), "Checkerboard"); } } - -/// Register this node type. -pub fn register(meta: &mut Vec) { - meta.push(NodeMeta { - type_id: "org.olivevideoeditor.Olive.checkerboard", - name: "Checkerboard", - categories: &[Category::Generator], - create, - }); -} diff --git a/crates/oak-node/src/nodes/colorbars.rs b/crates/oak-node/src/nodes/colorbars.rs index 94b37f613..483a925d7 100644 --- a/crates/oak-node/src/nodes/colorbars.rs +++ b/crates/oak-node/src/nodes/colorbars.rs @@ -257,6 +257,16 @@ pub fn create() -> (NodeCore, Box) { (core, Box::new(ColorBarsNode)) } +/// Register this node type. +pub fn register(meta: &mut Vec) { + meta.push(NodeMeta { + type_id: "org.olivevideoeditor.Olive.colorbars", + name: "Color Bars", + categories: &[Category::Generator], + create, + }); +} + #[cfg(test)] mod tests { use super::*; @@ -383,13 +393,3 @@ mod tests { assert_eq!(dup.name(), "Color Bars"); } } - -/// Register this node type. -pub fn register(meta: &mut Vec) { - meta.push(NodeMeta { - type_id: "org.olivevideoeditor.Olive.colorbars", - name: "Color Bars", - categories: &[Category::Generator], - create, - }); -} diff --git a/crates/oak-node/src/nodes/colormatrix.rs b/crates/oak-node/src/nodes/colormatrix.rs index 032bebcee..4ff7bf5d6 100644 --- a/crates/oak-node/src/nodes/colormatrix.rs +++ b/crates/oak-node/src/nodes/colormatrix.rs @@ -198,6 +198,16 @@ pub fn create() -> (NodeCore, Box) { (core, Box::new(ColorMatrixNode)) } +/// Register this node type. +pub fn register(meta: &mut Vec) { + meta.push(NodeMeta { + type_id: "org.olivevideoeditor.Olive.colormatrix", + name: "Color Matrix", + categories: &[Category::Color], + create, + }); +} + #[cfg(test)] mod tests { use super::*; @@ -329,13 +339,3 @@ mod tests { assert_eq!(dup.name(), "Color Matrix"); } } - -/// Register this node type. -pub fn register(meta: &mut Vec) { - meta.push(NodeMeta { - type_id: "org.olivevideoeditor.Olive.colormatrix", - name: "Color Matrix", - categories: &[Category::Color], - create, - }); -} diff --git a/crates/oak-node/src/nodes/cornerpindistortnode.rs b/crates/oak-node/src/nodes/cornerpindistortnode.rs index 74a227b6c..98ec29a2c 100644 --- a/crates/oak-node/src/nodes/cornerpindistortnode.rs +++ b/crates/oak-node/src/nodes/cornerpindistortnode.rs @@ -518,6 +518,17 @@ pub fn create() -> (NodeCore, Box) { ) } +/// Register this node type (C++ factory entry for +/// `org.olivevideoeditor.Olive.cornerpin`). +pub fn register(meta: &mut Vec) { + meta.push(NodeMeta { + type_id: "org.olivevideoeditor.Olive.cornerpin", + name: "Corner Pin", + categories: &[Category::Distort], + create, + }); +} + #[cfg(test)] mod tests { use super::*; @@ -675,14 +686,3 @@ mod tests { assert_eq!(dup.name(), "Corner Pin"); } } - -/// Register this node type (C++ factory entry for -/// `org.olivevideoeditor.Olive.cornerpin`). -pub fn register(meta: &mut Vec) { - meta.push(NodeMeta { - type_id: "org.olivevideoeditor.Olive.cornerpin", - name: "Corner Pin", - categories: &[Category::Distort], - create, - }); -} diff --git a/crates/oak-node/src/nodes/cropdistortnode.rs b/crates/oak-node/src/nodes/cropdistortnode.rs index 5570df164..457d8c680 100644 --- a/crates/oak-node/src/nodes/cropdistortnode.rs +++ b/crates/oak-node/src/nodes/cropdistortnode.rs @@ -410,6 +410,17 @@ fn create_crop_side_input(core: &mut NodeCore, id: &str) { core.add_input(input); } +/// Register this node type (C++ factory entry for +/// `org.olivevideoeditor.Olive.crop`). +pub fn register(meta: &mut Vec) { + meta.push(NodeMeta { + type_id: "org.olivevideoeditor.Olive.crop", + name: "Crop", + categories: &[Category::Distort], + create, + }); +} + #[cfg(test)] mod tests { use super::*; @@ -528,14 +539,3 @@ mod tests { assert_eq!(dup.name(), "Crop"); } } - -/// Register this node type (C++ factory entry for -/// `org.olivevideoeditor.Olive.crop`). -pub fn register(meta: &mut Vec) { - meta.push(NodeMeta { - type_id: "org.olivevideoeditor.Olive.crop", - name: "Crop", - categories: &[Category::Distort], - create, - }); -} diff --git a/crates/oak-node/src/nodes/dilate.rs b/crates/oak-node/src/nodes/dilate.rs index cb98b1de4..162c41abe 100644 --- a/crates/oak-node/src/nodes/dilate.rs +++ b/crates/oak-node/src/nodes/dilate.rs @@ -196,6 +196,16 @@ pub fn create() -> (NodeCore, Box) { (core, Box::new(DilateNode)) } +/// Register this node type. +pub fn register(meta: &mut Vec) { + meta.push(NodeMeta { + type_id: "org.olivevideoeditor.Olive.dilate", + name: "Dilate", + categories: &[Category::Filter], + create, + }); +} + #[cfg(test)] mod tests { use super::*; @@ -318,13 +328,3 @@ mod tests { assert_eq!(dup.name(), "Dilate"); } } - -/// Register this node type. -pub fn register(meta: &mut Vec) { - meta.push(NodeMeta { - type_id: "org.olivevideoeditor.Olive.dilate", - name: "Dilate", - categories: &[Category::Filter], - create, - }); -} diff --git a/crates/oak-node/src/nodes/dirblur.rs b/crates/oak-node/src/nodes/dirblur.rs index a3b282d9a..8b46c7424 100644 --- a/crates/oak-node/src/nodes/dirblur.rs +++ b/crates/oak-node/src/nodes/dirblur.rs @@ -244,6 +244,16 @@ pub fn create() -> (NodeCore, Box) { (core, Box::new(DirBlurNode)) } +/// Register this node type. +pub fn register(meta: &mut Vec) { + meta.push(NodeMeta { + type_id: "org.olivevideoeditor.Olive.dirblur", + name: "Directional Blur", + categories: &[Category::Filter], + create, + }); +} + #[cfg(test)] mod tests { use super::*; @@ -394,13 +404,3 @@ mod tests { assert_eq!(dup.name(), "Directional Blur"); } } - -/// Register this node type. -pub fn register(meta: &mut Vec) { - meta.push(NodeMeta { - type_id: "org.olivevideoeditor.Olive.dirblur", - name: "Directional Blur", - categories: &[Category::Filter], - create, - }); -} diff --git a/crates/oak-node/src/nodes/dissolve.rs b/crates/oak-node/src/nodes/dissolve.rs index a409e6d1a..2aec8ab86 100644 --- a/crates/oak-node/src/nodes/dissolve.rs +++ b/crates/oak-node/src/nodes/dissolve.rs @@ -243,6 +243,16 @@ pub fn create() -> (NodeCore, Box) { (core, Box::new(DissolveNode)) } +/// Register this node type. +pub fn register(meta: &mut Vec) { + meta.push(NodeMeta { + type_id: "org.olivevideoeditor.Olive.dissolve", + name: "Dissolve", + categories: &[Category::Math], + create, + }); +} + #[cfg(test)] mod tests { use super::*; @@ -402,13 +412,3 @@ mod tests { assert_eq!(dup.name(), "Dissolve"); } } - -/// Register this node type. -pub fn register(meta: &mut Vec) { - meta.push(NodeMeta { - type_id: "org.olivevideoeditor.Olive.dissolve", - name: "Dissolve", - categories: &[Category::Math], - create, - }); -} diff --git a/crates/oak-node/src/nodes/dropshadowfilter.rs b/crates/oak-node/src/nodes/dropshadowfilter.rs index 22781fc0f..b591d4477 100644 --- a/crates/oak-node/src/nodes/dropshadowfilter.rs +++ b/crates/oak-node/src/nodes/dropshadowfilter.rs @@ -367,6 +367,17 @@ pub fn create() -> (NodeCore, Box) { (core, Box::new(DropShadowFilter)) } +/// Register this node type (C++ `k_drop_shadow_filter` in +/// `factory.cpp::create_from_factory_index`). +pub fn register(meta: &mut Vec) { + meta.push(NodeMeta { + type_id: "org.olivevideoeditor.Olive.dropshadow", + name: "Drop Shadow", + categories: &[Category::Filter], + create, + }); +} + #[cfg(test)] mod tests { use super::*; @@ -485,14 +496,3 @@ mod tests { assert_eq!(dup.name(), "Drop Shadow"); } } - -/// Register this node type (C++ `k_drop_shadow_filter` in -/// `factory.cpp::create_from_factory_index`). -pub fn register(meta: &mut Vec) { - meta.push(NodeMeta { - type_id: "org.olivevideoeditor.Olive.dropshadow", - name: "Drop Shadow", - categories: &[Category::Filter], - create, - }); -} diff --git a/crates/oak-node/src/nodes/edgedetect.rs b/crates/oak-node/src/nodes/edgedetect.rs index 68366de92..2326e2705 100644 --- a/crates/oak-node/src/nodes/edgedetect.rs +++ b/crates/oak-node/src/nodes/edgedetect.rs @@ -190,6 +190,16 @@ pub fn create() -> (NodeCore, Box) { (core, Box::new(EdgeDetectNode)) } +/// Register this node type. +pub fn register(meta: &mut Vec) { + meta.push(NodeMeta { + type_id: "org.olivevideoeditor.Olive.edgedetect", + name: "Edge Detect", + categories: &[Category::Filter], + create, + }); +} + #[cfg(test)] mod tests { use super::*; @@ -307,13 +317,3 @@ mod tests { assert_eq!(dup.name(), "Edge Detect"); } } - -/// Register this node type. -pub fn register(meta: &mut Vec) { - meta.push(NodeMeta { - type_id: "org.olivevideoeditor.Olive.edgedetect", - name: "Edge Detect", - categories: &[Category::Filter], - create, - }); -} diff --git a/crates/oak-node/src/nodes/erode.rs b/crates/oak-node/src/nodes/erode.rs index cd0df79d4..cdd14df45 100644 --- a/crates/oak-node/src/nodes/erode.rs +++ b/crates/oak-node/src/nodes/erode.rs @@ -196,6 +196,16 @@ pub fn create() -> (NodeCore, Box) { (core, Box::new(ErodeNode)) } +/// Register this node type. +pub fn register(meta: &mut Vec) { + meta.push(NodeMeta { + type_id: "org.olivevideoeditor.Olive.erode", + name: "Erode", + categories: &[Category::Filter], + create, + }); +} + #[cfg(test)] mod tests { use super::*; @@ -318,13 +328,3 @@ mod tests { assert_eq!(dup.name(), "Erode"); } } - -/// Register this node type. -pub fn register(meta: &mut Vec) { - meta.push(NodeMeta { - type_id: "org.olivevideoeditor.Olive.erode", - name: "Erode", - categories: &[Category::Filter], - create, - }); -} diff --git a/crates/oak-node/src/nodes/flipdistortnode.rs b/crates/oak-node/src/nodes/flipdistortnode.rs index 965198069..5bfae6833 100644 --- a/crates/oak-node/src/nodes/flipdistortnode.rs +++ b/crates/oak-node/src/nodes/flipdistortnode.rs @@ -199,6 +199,17 @@ pub fn create() -> (NodeCore, Box) { (core, Box::new(FlipDistortNode)) } +/// Register this node type (C++ factory entry for +/// `org.olivevideoeditor.Olive.flip`). +pub fn register(meta: &mut Vec) { + meta.push(NodeMeta { + type_id: "org.olivevideoeditor.Olive.flip", + name: "Flip", + categories: &[Category::Distort], + create, + }); +} + #[cfg(test)] mod tests { use super::*; @@ -315,14 +326,3 @@ mod tests { assert_eq!(dup.name(), "Flip"); } } - -/// Register this node type (C++ factory entry for -/// `org.olivevideoeditor.Olive.flip`). -pub fn register(meta: &mut Vec) { - meta.push(NodeMeta { - type_id: "org.olivevideoeditor.Olive.flip", - name: "Flip", - categories: &[Category::Distort], - create, - }); -} diff --git a/crates/oak-node/src/nodes/keymix.rs b/crates/oak-node/src/nodes/keymix.rs index 994996bfa..ee685e515 100644 --- a/crates/oak-node/src/nodes/keymix.rs +++ b/crates/oak-node/src/nodes/keymix.rs @@ -219,6 +219,16 @@ pub fn create() -> (NodeCore, Box) { (core, Box::new(KeyMixNode)) } +/// Register this node type. +pub fn register(meta: &mut Vec) { + meta.push(NodeMeta { + type_id: "org.olivevideoeditor.Olive.keymix", + name: "KeyMix", + categories: &[Category::Math], + create, + }); +} + #[cfg(test)] mod tests { use super::*; @@ -357,13 +367,3 @@ mod tests { assert_eq!(dup.name(), "KeyMix"); } } - -/// Register this node type. -pub fn register(meta: &mut Vec) { - meta.push(NodeMeta { - type_id: "org.olivevideoeditor.Olive.keymix", - name: "KeyMix", - categories: &[Category::Math], - create, - }); -} diff --git a/crates/oak-node/src/nodes/matrix.rs b/crates/oak-node/src/nodes/matrix.rs index ef2da7486..f2e089ada 100644 --- a/crates/oak-node/src/nodes/matrix.rs +++ b/crates/oak-node/src/nodes/matrix.rs @@ -406,6 +406,17 @@ pub fn create() -> (NodeCore, Box) { (core, Box::new(MatrixGenerator)) } +/// Register this node type (C++ factory entry for +/// `org.olivevideoeditor.Olive.ortho`). +pub fn register(meta: &mut Vec) { + meta.push(NodeMeta { + type_id: "org.olivevideoeditor.Olive.ortho", + name: "Orthographic Matrix", + categories: &[Category::Generator, Category::Math], + create, + }); +} + #[cfg(test)] mod tests { use super::*; @@ -587,14 +598,3 @@ mod tests { assert_eq!(dup.name(), "Orthographic Matrix"); } } - -/// Register this node type (C++ factory entry for -/// `org.olivevideoeditor.Olive.ortho`). -pub fn register(meta: &mut Vec) { - meta.push(NodeMeta { - type_id: "org.olivevideoeditor.Olive.ortho", - name: "Orthographic Matrix", - categories: &[Category::Generator, Category::Math], - create, - }); -} diff --git a/crates/oak-node/src/nodes/merge.rs b/crates/oak-node/src/nodes/merge.rs index d99bea3c4..d23330824 100644 --- a/crates/oak-node/src/nodes/merge.rs +++ b/crates/oak-node/src/nodes/merge.rs @@ -208,6 +208,17 @@ pub fn create() -> (NodeCore, Box) { (core, Box::new(MergeNode)) } +/// Register this node type (C++ `k_merge_node` in +/// `factory.cpp::create_from_factory_index`). +pub fn register(meta: &mut Vec) { + meta.push(NodeMeta { + type_id: "org.olivevideoeditor.Olive.merge", + name: "Merge", + categories: &[Category::Math], + create, + }); +} + #[cfg(test)] mod tests { use super::*; @@ -306,14 +317,3 @@ mod tests { assert_eq!(dup.name(), "Merge"); } } - -/// Register this node type (C++ `k_merge_node` in -/// `factory.cpp::create_from_factory_index`). -pub fn register(meta: &mut Vec) { - meta.push(NodeMeta { - type_id: "org.olivevideoeditor.Olive.merge", - name: "Merge", - categories: &[Category::Math], - create, - }); -} diff --git a/crates/oak-node/src/nodes/mirror.rs b/crates/oak-node/src/nodes/mirror.rs index ddb6d0dbb..62ab1231f 100644 --- a/crates/oak-node/src/nodes/mirror.rs +++ b/crates/oak-node/src/nodes/mirror.rs @@ -189,6 +189,16 @@ pub fn create() -> (NodeCore, Box) { (core, Box::new(MirrorNode)) } +/// Register this node type. +pub fn register(meta: &mut Vec) { + meta.push(NodeMeta { + type_id: "org.olivevideoeditor.Olive.mirror", + name: "Mirror", + categories: &[Category::Distort], + create, + }); +} + #[cfg(test)] mod tests { use super::*; @@ -327,13 +337,3 @@ mod tests { assert_eq!(dup.name(), "Mirror"); } } - -/// Register this node type. -pub fn register(meta: &mut Vec) { - meta.push(NodeMeta { - type_id: "org.olivevideoeditor.Olive.mirror", - name: "Mirror", - categories: &[Category::Distort], - create, - }); -} diff --git a/crates/oak-node/src/nodes/mosaicfilternode.rs b/crates/oak-node/src/nodes/mosaicfilternode.rs index 10ba99d50..7cdddb9cb 100644 --- a/crates/oak-node/src/nodes/mosaicfilternode.rs +++ b/crates/oak-node/src/nodes/mosaicfilternode.rs @@ -214,6 +214,17 @@ pub fn create() -> (NodeCore, Box) { (core, Box::new(MosaicFilterNode)) } +/// Register this node type (C++ `k_mosaic_filter` in +/// `factory.cpp::create_from_factory_index`). +pub fn register(meta: &mut Vec) { + meta.push(NodeMeta { + type_id: "org.olivevideoeditor.Olive.mosaicfilter", + name: "Mosaic", + categories: &[Category::Filter], + create, + }); +} + #[cfg(test)] mod tests { use super::*; @@ -312,14 +323,3 @@ mod tests { assert_eq!(dup.name(), "Mosaic"); } } - -/// Register this node type (C++ `k_mosaic_filter` in -/// `factory.cpp::create_from_factory_index`). -pub fn register(meta: &mut Vec) { - meta.push(NodeMeta { - type_id: "org.olivevideoeditor.Olive.mosaicfilter", - name: "Mosaic", - categories: &[Category::Filter], - create, - }); -} diff --git a/crates/oak-node/src/nodes/noise.rs b/crates/oak-node/src/nodes/noise.rs index 4d01674d4..06faf3f65 100644 --- a/crates/oak-node/src/nodes/noise.rs +++ b/crates/oak-node/src/nodes/noise.rs @@ -219,6 +219,17 @@ pub fn create() -> (NodeCore, Box) { (core, Box::new(NoiseGeneratorNode)) } +/// Register this node type (C++ factory entry for +/// `org.olivevideoeditor.Olive.noise`). +pub fn register(meta: &mut Vec) { + meta.push(NodeMeta { + type_id: "org.olivevideoeditor.Olive.noise", + name: "Noise", + categories: &[Category::Generator], + create, + }); +} + #[cfg(test)] mod tests { use super::*; @@ -310,14 +321,3 @@ mod tests { assert_eq!(dup.name(), "Noise"); } } - -/// Register this node type (C++ factory entry for -/// `org.olivevideoeditor.Olive.noise`). -pub fn register(meta: &mut Vec) { - meta.push(NodeMeta { - type_id: "org.olivevideoeditor.Olive.noise", - name: "Noise", - categories: &[Category::Generator], - create, - }); -} diff --git a/crates/oak-node/src/nodes/opacity.rs b/crates/oak-node/src/nodes/opacity.rs index a81ea32a2..ecc1c7e21 100644 --- a/crates/oak-node/src/nodes/opacity.rs +++ b/crates/oak-node/src/nodes/opacity.rs @@ -244,6 +244,17 @@ pub fn create() -> (NodeCore, Box) { ) } +/// Register this node type (C++ `k_opacity_effect` in +/// `factory.cpp::create_from_factory_index`). +pub fn register(meta: &mut Vec) { + meta.push(NodeMeta { + type_id: "org.olivevideoeditor.Olive.opacity", + name: "Opacity", + categories: &[Category::Filter], + create, + }); +} + #[cfg(test)] mod tests { use super::*; @@ -396,14 +407,3 @@ mod tests { assert_eq!(dup.name(), "Opacity"); } } - -/// Register this node type (C++ `k_opacity_effect` in -/// `factory.cpp::create_from_factory_index`). -pub fn register(meta: &mut Vec) { - meta.push(NodeMeta { - type_id: "org.olivevideoeditor.Olive.opacity", - name: "Opacity", - categories: &[Category::Filter], - create, - }); -} diff --git a/crates/oak-node/src/nodes/pan.rs b/crates/oak-node/src/nodes/pan.rs index 8a91c1645..addc79274 100644 --- a/crates/oak-node/src/nodes/pan.rs +++ b/crates/oak-node/src/nodes/pan.rs @@ -196,6 +196,17 @@ pub fn create() -> (NodeCore, Box) { (core, Box::new(PanNode)) } +/// Register this node type (C++ `k_audio_panning` in +/// `factory.cpp::create_from_factory_index`). +pub fn register(meta: &mut Vec) { + meta.push(NodeMeta { + type_id: "org.olivevideoeditor.Olive.pan", + name: "Pan", + categories: &[Category::Filter], + create, + }); +} + #[cfg(test)] mod tests { use super::*; @@ -338,14 +349,3 @@ mod tests { assert_eq!(dup.name(), "Pan"); } } - -/// Register this node type (C++ `k_audio_panning` in -/// `factory.cpp::create_from_factory_index`). -pub fn register(meta: &mut Vec) { - meta.push(NodeMeta { - type_id: "org.olivevideoeditor.Olive.pan", - name: "Pan", - categories: &[Category::Filter], - create, - }); -} diff --git a/crates/oak-node/src/nodes/plugin.rs b/crates/oak-node/src/nodes/plugin.rs index 9ad972047..8f60155e1 100644 --- a/crates/oak-node/src/nodes/plugin.rs +++ b/crates/oak-node/src/nodes/plugin.rs @@ -546,7 +546,7 @@ mod tests { sample_count: 3, // Planar layout: channel 0 plane [1, 2, 3], channel 1 plane // [4, 5, 6]. - data: vec![1.0f32, 2.0, 3.0, 4.0, 5.0, 6.0] + data: [1.0f32, 2.0, 3.0, 4.0, 5.0, 6.0] .iter() .flat_map(|f| f.to_le_bytes()) .collect(), @@ -580,7 +580,7 @@ mod tests { format: oak_core::SampleFormat::F32, channels: 1, sample_count: 2, - data: vec![1.0f32, 2.0] + data: [1.0f32, 2.0] .iter() .flat_map(|f| f.to_le_bytes()) .collect(), diff --git a/crates/oak-node/src/nodes/polygon.rs b/crates/oak-node/src/nodes/polygon.rs index cd26f8ed7..9b1376dd1 100644 --- a/crates/oak-node/src/nodes/polygon.rs +++ b/crates/oak-node/src/nodes/polygon.rs @@ -112,7 +112,7 @@ pub fn point_array(core: &NodeCore, inputs: &crate::value::NodeValueRow, time: o .inputs .iter() .find(|i| i.id == POINTS_INPUT) - .map(|i| (i.array_size as usize).min(64)) + .map(|i| i.array_size.min(64)) .unwrap_or(5); let mut points: Vec<[f64; 4]> = Vec::new(); for i in 0..size { @@ -380,6 +380,17 @@ pub fn create() -> (NodeCore, Box) { (core, Box::new(PolygonGenerator)) } +/// Register this node type (C++ factory entry for +/// `org.olivevideoeditor.Olive.polygon`). +pub fn register(meta: &mut Vec) { + meta.push(NodeMeta { + type_id: "org.olivevideoeditor.Olive.polygon", + name: "Polygon", + categories: &[Category::Generator], + create, + }); +} + #[cfg(test)] mod tests { use super::*; @@ -507,14 +518,3 @@ mod tests { assert_eq!(dup.name(), "Polygon"); } } - -/// Register this node type (C++ factory entry for -/// `org.olivevideoeditor.Olive.polygon`). -pub fn register(meta: &mut Vec) { - meta.push(NodeMeta { - type_id: "org.olivevideoeditor.Olive.polygon", - name: "Polygon", - categories: &[Category::Generator], - create, - }); -} diff --git a/crates/oak-node/src/nodes/position.rs b/crates/oak-node/src/nodes/position.rs index 487ce4e4e..8c8d06171 100644 --- a/crates/oak-node/src/nodes/position.rs +++ b/crates/oak-node/src/nodes/position.rs @@ -193,6 +193,16 @@ pub fn create() -> (NodeCore, Box) { (core, Box::new(PositionNode)) } +/// Register this node type. +pub fn register(meta: &mut Vec) { + meta.push(NodeMeta { + type_id: "org.olivevideoeditor.Olive.position", + name: "Position", + categories: &[Category::Distort], + create, + }); +} + #[cfg(test)] mod tests { use super::*; @@ -307,13 +317,3 @@ mod tests { assert_eq!(dup.name(), "Position"); } } - -/// Register this node type. -pub fn register(meta: &mut Vec) { - meta.push(NodeMeta { - type_id: "org.olivevideoeditor.Olive.position", - name: "Position", - categories: &[Category::Distort], - create, - }); -} diff --git a/crates/oak-node/src/nodes/premult.rs b/crates/oak-node/src/nodes/premult.rs index fb14939bd..ed4aebaa9 100644 --- a/crates/oak-node/src/nodes/premult.rs +++ b/crates/oak-node/src/nodes/premult.rs @@ -233,6 +233,16 @@ pub fn create() -> (NodeCore, Box) { (core, Box::new(PremultiplyNode)) } +/// Register this node type. +pub fn register(meta: &mut Vec) { + meta.push(NodeMeta { + type_id: "org.olivevideoeditor.Olive.premult", + name: "Premultiply", + categories: &[Category::Math], + create, + }); +} + #[cfg(test)] mod tests { use super::*; @@ -372,13 +382,3 @@ mod tests { assert_eq!(dup.name(), "Premultiply"); } } - -/// Register this node type. -pub fn register(meta: &mut Vec) { - meta.push(NodeMeta { - type_id: "org.olivevideoeditor.Olive.premult", - name: "Premultiply", - categories: &[Category::Math], - create, - }); -} diff --git a/crates/oak-node/src/nodes/ramp.rs b/crates/oak-node/src/nodes/ramp.rs index ac29584c7..215d8b98a 100644 --- a/crates/oak-node/src/nodes/ramp.rs +++ b/crates/oak-node/src/nodes/ramp.rs @@ -233,6 +233,16 @@ pub fn create() -> (NodeCore, Box) { (core, Box::new(RampNode)) } +/// Register this node type. +pub fn register(meta: &mut Vec) { + meta.push(NodeMeta { + type_id: "org.olivevideoeditor.Olive.ramp", + name: "Ramp", + categories: &[Category::Generator], + create, + }); +} + #[cfg(test)] mod tests { use super::*; @@ -394,13 +404,3 @@ mod tests { assert_eq!(dup.name(), "Ramp"); } } - -/// Register this node type. -pub fn register(meta: &mut Vec) { - meta.push(NodeMeta { - type_id: "org.olivevideoeditor.Olive.ramp", - name: "Ramp", - categories: &[Category::Generator], - create, - }); -} diff --git a/crates/oak-node/src/nodes/rippledistortnode.rs b/crates/oak-node/src/nodes/rippledistortnode.rs index 2fd54ae7d..ce9745f08 100644 --- a/crates/oak-node/src/nodes/rippledistortnode.rs +++ b/crates/oak-node/src/nodes/rippledistortnode.rs @@ -299,6 +299,17 @@ pub fn create() -> (NodeCore, Box) { (core, Box::new(RippleDistortNode { gizmo })) } +/// Register this node type (C++ factory entry for +/// `org.olivevideoeditor.Olive.ripple`). +pub fn register(meta: &mut Vec) { + meta.push(NodeMeta { + type_id: "org.olivevideoeditor.Olive.ripple", + name: "Ripple", + categories: &[Category::Distort], + create, + }); +} + #[cfg(test)] mod tests { use super::*; @@ -404,14 +415,3 @@ mod tests { assert_eq!(dup.name(), "Ripple"); } } - -/// Register this node type (C++ factory entry for -/// `org.olivevideoeditor.Olive.ripple`). -pub fn register(meta: &mut Vec) { - meta.push(NodeMeta { - type_id: "org.olivevideoeditor.Olive.ripple", - name: "Ripple", - categories: &[Category::Distort], - create, - }); -} diff --git a/crates/oak-node/src/nodes/shapenode.rs b/crates/oak-node/src/nodes/shapenode.rs index 5985d0dd6..573b07392 100644 --- a/crates/oak-node/src/nodes/shapenode.rs +++ b/crates/oak-node/src/nodes/shapenode.rs @@ -359,6 +359,17 @@ pub fn create() -> (NodeCore, Box) { (core, Box::new(ShapeNode)) } +/// Register this node type (C++ factory entry for +/// `org.olivevideoeditor.Olive.shape`). +pub fn register(meta: &mut Vec) { + meta.push(NodeMeta { + type_id: "org.olivevideoeditor.Olive.shape", + name: "Shape", + categories: &[Category::Generator], + create, + }); +} + #[cfg(test)] mod tests { use super::*; @@ -516,14 +527,3 @@ mod tests { assert_eq!(dup.name(), "Shape"); } } - -/// Register this node type (C++ factory entry for -/// `org.olivevideoeditor.Olive.shape`). -pub fn register(meta: &mut Vec) { - meta.push(NodeMeta { - type_id: "org.olivevideoeditor.Olive.shape", - name: "Shape", - categories: &[Category::Generator], - create, - }); -} diff --git a/crates/oak-node/src/nodes/sharpen.rs b/crates/oak-node/src/nodes/sharpen.rs index af295dc0f..96ae9f167 100644 --- a/crates/oak-node/src/nodes/sharpen.rs +++ b/crates/oak-node/src/nodes/sharpen.rs @@ -209,6 +209,16 @@ pub fn create() -> (NodeCore, Box) { (core, Box::new(SharpenNode)) } +/// Register this node type. +pub fn register(meta: &mut Vec) { + meta.push(NodeMeta { + type_id: "org.olivevideoeditor.Olive.sharpen", + name: "Sharpen", + categories: &[Category::Filter], + create, + }); +} + #[cfg(test)] mod tests { use super::*; @@ -327,13 +337,3 @@ mod tests { assert_eq!(dup.name(), "Sharpen"); } } - -/// Register this node type. -pub fn register(meta: &mut Vec) { - meta.push(NodeMeta { - type_id: "org.olivevideoeditor.Olive.sharpen", - name: "Sharpen", - categories: &[Category::Filter], - create, - }); -} diff --git a/crates/oak-node/src/nodes/solid.rs b/crates/oak-node/src/nodes/solid.rs index 055b74260..dca9f6a4b 100644 --- a/crates/oak-node/src/nodes/solid.rs +++ b/crates/oak-node/src/nodes/solid.rs @@ -139,6 +139,17 @@ pub fn create() -> (NodeCore, Box) { (core, Box::new(SolidGenerator)) } +/// Register this node type (C++ factory entry for +/// `org.olivevideoeditor.Olive.solidgenerator`). +pub fn register(meta: &mut Vec) { + meta.push(NodeMeta { + type_id: "org.olivevideoeditor.Olive.solidgenerator", + name: "Solid", + categories: &[Category::Generator], + create, + }); +} + #[cfg(test)] mod tests { use super::*; @@ -209,14 +220,3 @@ mod tests { assert_eq!(dup.name(), "Solid"); } } - -/// Register this node type (C++ factory entry for -/// `org.olivevideoeditor.Olive.solidgenerator`). -pub fn register(meta: &mut Vec) { - meta.push(NodeMeta { - type_id: "org.olivevideoeditor.Olive.solidgenerator", - name: "Solid", - categories: &[Category::Generator], - create, - }); -} diff --git a/crates/oak-node/src/nodes/stroke.rs b/crates/oak-node/src/nodes/stroke.rs index b30c83436..72fdf693c 100644 --- a/crates/oak-node/src/nodes/stroke.rs +++ b/crates/oak-node/src/nodes/stroke.rs @@ -295,6 +295,17 @@ pub fn create() -> (NodeCore, Box) { (core, Box::new(StrokeFilterNode)) } +/// Register this node type (C++ `k_stroke_filter` in +/// `factory.cpp::create_from_factory_index`). +pub fn register(meta: &mut Vec) { + meta.push(NodeMeta { + type_id: "org.olivevideoeditor.Olive.stroke", + name: "Stroke", + categories: &[Category::Filter], + create, + }); +} + #[cfg(test)] mod tests { use super::*; @@ -413,14 +424,3 @@ mod tests { assert_eq!(dup.name(), "Stroke"); } } - -/// Register this node type (C++ `k_stroke_filter` in -/// `factory.cpp::create_from_factory_index`). -pub fn register(meta: &mut Vec) { - meta.push(NodeMeta { - type_id: "org.olivevideoeditor.Olive.stroke", - name: "Stroke", - categories: &[Category::Filter], - create, - }); -} diff --git a/crates/oak-node/src/nodes/swirldistortnode.rs b/crates/oak-node/src/nodes/swirldistortnode.rs index 49e9afdcd..8ddb3e087 100644 --- a/crates/oak-node/src/nodes/swirldistortnode.rs +++ b/crates/oak-node/src/nodes/swirldistortnode.rs @@ -274,6 +274,17 @@ pub fn create() -> (NodeCore, Box) { (core, Box::new(SwirlDistortNode { gizmo })) } +/// Register this node type (C++ factory entry for +/// `org.olivevideoeditor.Olive.swirl`). +pub fn register(meta: &mut Vec) { + meta.push(NodeMeta { + type_id: "org.olivevideoeditor.Olive.swirl", + name: "Swirl", + categories: &[Category::Distort], + create, + }); +} + #[cfg(test)] mod tests { use super::*; @@ -390,14 +401,3 @@ mod tests { assert_eq!(dup.name(), "Swirl"); } } - -/// Register this node type (C++ factory entry for -/// `org.olivevideoeditor.Olive.swirl`). -pub fn register(meta: &mut Vec) { - meta.push(NodeMeta { - type_id: "org.olivevideoeditor.Olive.swirl", - name: "Swirl", - categories: &[Category::Distort], - create, - }); -} diff --git a/crates/oak-node/src/nodes/textv1.rs b/crates/oak-node/src/nodes/textv1.rs index 34146cc9f..3d34d103f 100644 --- a/crates/oak-node/src/nodes/textv1.rs +++ b/crates/oak-node/src/nodes/textv1.rs @@ -84,7 +84,7 @@ impl TextGeneratorV1 { let text = row .get(TEXT_INPUT) .map(to_text) - .unwrap_or_else(|| String::new()); + .unwrap_or_default(); let html = matches!(row.get(HTML_INPUT), Some(NodeValue::Boolean(true))); let mut mode = TextLayoutMode::PlainText; let text = if html { @@ -100,7 +100,7 @@ impl TextGeneratorV1 { TextLayoutRequest { text, mode, - font_family: row.get(FONT_INPUT).map(to_text).unwrap_or_else(String::new), + font_family: row.get(FONT_INPUT).map(to_text).unwrap_or_default(), font_size_pt: row .get(FONT_SIZE_INPUT) .map(NodeValue::to_double) diff --git a/crates/oak-node/src/nodes/textv2.rs b/crates/oak-node/src/nodes/textv2.rs index c56b25ea3..71f4212f4 100644 --- a/crates/oak-node/src/nodes/textv2.rs +++ b/crates/oak-node/src/nodes/textv2.rs @@ -92,7 +92,7 @@ impl TextGeneratorV2 { let text = row .get(TEXT_INPUT) .map(to_text) - .unwrap_or_else(|| String::new()); + .unwrap_or_default(); let html = matches!(row.get(HTML_INPUT), Some(NodeValue::Boolean(true))); let mut mode = TextLayoutMode::PlainText; let text = if html { @@ -111,7 +111,7 @@ impl TextGeneratorV2 { TextLayoutRequest { text, mode, - font_family: row.get(FONT_INPUT).map(to_text).unwrap_or_else(String::new), + font_family: row.get(FONT_INPUT).map(to_text).unwrap_or_default(), font_size_pt: row .get(FONT_SIZE_INPUT) .map(NodeValue::to_double) diff --git a/crates/oak-node/src/nodes/textv3.rs b/crates/oak-node/src/nodes/textv3.rs index 6dd913025..8f50e59f1 100644 --- a/crates/oak-node/src/nodes/textv3.rs +++ b/crates/oak-node/src/nodes/textv3.rs @@ -760,12 +760,12 @@ impl TextGeneratorV3 { text: row .get(PLAIN_TEXT_INPUT) .map(to_text) - .unwrap_or_else(String::new), + .unwrap_or_default(), mode: TextLayoutMode::PlainText, font_family: row .get(FONT_FAMILY_INPUT) .map(to_text) - .unwrap_or_else(String::new), + .unwrap_or_default(), font_size_pt: row .get(FONT_SIZE_INPUT) .map(|v| v.to_double()) @@ -776,7 +776,7 @@ impl TextGeneratorV3 { } } else { TextLayoutRequest { - text: row.get(TEXT_INPUT).map(to_text).unwrap_or_else(String::new), + text: row.get(TEXT_INPUT).map(to_text).unwrap_or_default(), mode: TextLayoutMode::OliveHtml, font_family: String::new(), font_size_pt: 0.0, @@ -2257,7 +2257,7 @@ mod tests { else { panic!("expected a CPU coverage frame"); }; - let stride = frame.linesize_bytes() as usize; + let stride = frame.linesize_bytes(); let at = y * stride + x * 16; let mut out = [0f32; 4]; for (c, v) in out.iter_mut().enumerate() { diff --git a/crates/oak-node/src/nodes/tiledistortnode.rs b/crates/oak-node/src/nodes/tiledistortnode.rs index e360fed1c..bc5547fae 100644 --- a/crates/oak-node/src/nodes/tiledistortnode.rs +++ b/crates/oak-node/src/nodes/tiledistortnode.rs @@ -381,6 +381,17 @@ pub fn create() -> (NodeCore, Box) { (core, Box::new(TileDistortNode { gizmo })) } +/// Register this node type (C++ factory entry for +/// `org.olivevideoeditor.Olive.tile`). +pub fn register(meta: &mut Vec) { + meta.push(NodeMeta { + type_id: "org.olivevideoeditor.Olive.tile", + name: "Tile", + categories: &[Category::Distort], + create, + }); +} + #[cfg(test)] mod tests { use super::*; @@ -483,14 +494,3 @@ mod tests { assert_eq!(dup.name(), "Tile"); } } - -/// Register this node type (C++ factory entry for -/// `org.olivevideoeditor.Olive.tile`). -pub fn register(meta: &mut Vec) { - meta.push(NodeMeta { - type_id: "org.olivevideoeditor.Olive.tile", - name: "Tile", - categories: &[Category::Distort], - create, - }); -} diff --git a/crates/oak-node/src/nodes/transformdistortnode.rs b/crates/oak-node/src/nodes/transformdistortnode.rs index 7691edb03..8a114a94a 100644 --- a/crates/oak-node/src/nodes/transformdistortnode.rs +++ b/crates/oak-node/src/nodes/transformdistortnode.rs @@ -246,19 +246,19 @@ impl TransformDistortNode { let scale_by_x = sequence_res.0 / texture_res.0; let scale_by_y = sequence_res.1 / texture_res.1; - let autoscale_val; + - if (autoscale_type == AutoScaleType::Fit) == (sequence_real_ar > footage_real_ar) { + let autoscale_val = if (autoscale_type == AutoScaleType::Fit) == (sequence_real_ar > footage_real_ar) { // Scale by height. Either the sequence is wider than // the footage or we're using fill and cutting off the // sides. - autoscale_val = scale_by_y; + scale_by_y } else { // Scale by width. Either the footage is wider than the // sequence or we're using fill and cutting off the top // and bottom. - autoscale_val = scale_by_x; - } + scale_by_x + }; adjusted_matrix = super::matrix::matrix_scale(adjusted_matrix, autoscale_val, autoscale_val, 1.0); @@ -671,6 +671,17 @@ pub fn create() -> (NodeCore, Box) { ) } +/// Register this node type (C++ factory entry for +/// `org.olivevideoeditor.Olive.transform`). +pub fn register(meta: &mut Vec) { + meta.push(NodeMeta { + type_id: "org.olivevideoeditor.Olive.transform", + name: "Transform", + categories: &[Category::Distort], + create, + }); +} + #[cfg(test)] mod tests { use super::*; @@ -960,14 +971,3 @@ mod tests { assert_eq!(dup.short_name(), "Transform"); } } - -/// Register this node type (C++ factory entry for -/// `org.olivevideoeditor.Olive.transform`). -pub fn register(meta: &mut Vec) { - meta.push(NodeMeta { - type_id: "org.olivevideoeditor.Olive.transform", - name: "Transform", - categories: &[Category::Distort], - create, - }); -} diff --git a/crates/oak-node/src/nodes/transitionfx.rs b/crates/oak-node/src/nodes/transitionfx.rs index 8d02e17e7..345b118f8 100644 --- a/crates/oak-node/src/nodes/transitionfx.rs +++ b/crates/oak-node/src/nodes/transitionfx.rs @@ -478,6 +478,16 @@ pub fn create() -> (NodeCore, Box) { (core, Box::new(TransitionFxNode)) } +/// Register this node type. +pub fn register(meta: &mut Vec) { + meta.push(NodeMeta { + type_id: "org.olivevideoeditor.Olive.transitionfx", + name: "Transition FX", + categories: &[Category::Effect], + create, + }); +} + #[cfg(test)] mod tests { use super::*; @@ -754,13 +764,3 @@ mod tests { assert_eq!(dup.name(), "Transition FX"); } } - -/// Register this node type. -pub fn register(meta: &mut Vec) { - meta.push(NodeMeta { - type_id: "org.olivevideoeditor.Olive.transitionfx", - name: "Transition FX", - categories: &[Category::Effect], - create, - }); -} diff --git a/crates/oak-node/src/nodes/transitions.rs b/crates/oak-node/src/nodes/transitions.rs index 4037a7121..4e90faae4 100644 --- a/crates/oak-node/src/nodes/transitions.rs +++ b/crates/oak-node/src/nodes/transitions.rs @@ -403,6 +403,16 @@ pub fn create() -> (NodeCore, Box) { (core, Box::new(TransitionNode)) } +/// Register this node type. +pub fn register(meta: &mut Vec) { + meta.push(NodeMeta { + type_id: "org.olivevideoeditor.Olive.transition", + name: "Transition", + categories: &[Category::Timeline], + create, + }); +} + #[cfg(test)] mod tests { use super::*; @@ -596,13 +606,3 @@ mod tests { assert_eq!(dup.name(), "Transition"); } } - -/// Register this node type. -pub fn register(meta: &mut Vec) { - meta.push(NodeMeta { - type_id: "org.olivevideoeditor.Olive.transition", - name: "Transition", - categories: &[Category::Timeline], - create, - }); -} diff --git a/crates/oak-node/src/nodes/trigonometry.rs b/crates/oak-node/src/nodes/trigonometry.rs index dcc01448c..576e484dc 100644 --- a/crates/oak-node/src/nodes/trigonometry.rs +++ b/crates/oak-node/src/nodes/trigonometry.rs @@ -213,6 +213,17 @@ pub const OPERATION_NAMES: [&str; 9] = [ "Hyperbolic Tangent", ]; +/// Register this node type (C++ `k_trigonometry_node` in +/// `factory.cpp::create_from_factory_index`). +pub fn register(meta: &mut Vec) { + meta.push(NodeMeta { + type_id: "org.olivevideoeditor.Olive.trigonometry", + name: "Trigonometry", + categories: &[Category::Math], + create, + }); +} + #[cfg(test)] mod tests { use super::*; @@ -363,14 +374,3 @@ mod tests { assert_eq!(dup.name(), "Trigonometry"); } } - -/// Register this node type (C++ `k_trigonometry_node` in -/// `factory.cpp::create_from_factory_index`). -pub fn register(meta: &mut Vec) { - meta.push(NodeMeta { - type_id: "org.olivevideoeditor.Olive.trigonometry", - name: "Trigonometry", - categories: &[Category::Math], - create, - }); -} diff --git a/crates/oak-node/src/nodes/unpremult.rs b/crates/oak-node/src/nodes/unpremult.rs index 2fc2862d6..0708e579b 100644 --- a/crates/oak-node/src/nodes/unpremult.rs +++ b/crates/oak-node/src/nodes/unpremult.rs @@ -242,6 +242,16 @@ pub fn create() -> (NodeCore, Box) { (core, Box::new(UnpremultiplyNode)) } +/// Register this node type. +pub fn register(meta: &mut Vec) { + meta.push(NodeMeta { + type_id: "org.olivevideoeditor.Olive.unpremult", + name: "Unpremultiply", + categories: &[Category::Math], + create, + }); +} + #[cfg(test)] mod tests { use super::*; @@ -370,13 +380,3 @@ mod tests { assert_eq!(dup.name(), "Unpremultiply"); } } - -/// Register this node type. -pub fn register(meta: &mut Vec) { - meta.push(NodeMeta { - type_id: "org.olivevideoeditor.Olive.unpremult", - name: "Unpremultiply", - categories: &[Category::Math], - create, - }); -} diff --git a/crates/oak-node/src/nodes/valuenode.rs b/crates/oak-node/src/nodes/valuenode.rs index 2c228f199..07f8183c8 100644 --- a/crates/oak-node/src/nodes/valuenode.rs +++ b/crates/oak-node/src/nodes/valuenode.rs @@ -171,6 +171,17 @@ pub fn create() -> (NodeCore, Box) { (core, Box::new(ValueNode)) } +/// Register this node type (C++ `k_value_node` in +/// `factory.cpp::create_from_factory_index`). +pub fn register(meta: &mut Vec) { + meta.push(NodeMeta { + type_id: "org.olivevideoeditor.Olive.value", + name: "Value", + categories: &[Category::Generator], + create, + }); +} + #[cfg(test)] mod tests { use super::*; @@ -262,14 +273,3 @@ mod tests { assert_eq!(dup.name(), "Value"); } } - -/// Register this node type (C++ `k_value_node` in -/// `factory.cpp::create_from_factory_index`). -pub fn register(meta: &mut Vec) { - meta.push(NodeMeta { - type_id: "org.olivevideoeditor.Olive.value", - name: "Value", - categories: &[Category::Generator], - create, - }); -} diff --git a/crates/oak-node/src/nodes/volume.rs b/crates/oak-node/src/nodes/volume.rs index 681429361..3e6a89e41 100644 --- a/crates/oak-node/src/nodes/volume.rs +++ b/crates/oak-node/src/nodes/volume.rs @@ -177,6 +177,17 @@ pub fn create() -> (NodeCore, Box) { (core, Box::new(VolumeNode)) } +/// Register this node type (C++ `k_audio_volume` in +/// `factory.cpp::create_from_factory_index`). +pub fn register(meta: &mut Vec) { + meta.push(NodeMeta { + type_id: "org.olivevideoeditor.Olive.volume", + name: "Volume", + categories: &[Category::Filter], + create, + }); +} + #[cfg(test)] mod tests { use super::*; @@ -331,14 +342,3 @@ mod tests { assert_eq!(dup.type_id(), "org.olivevideoeditor.Olive.volume"); } } - -/// Register this node type (C++ `k_audio_volume` in -/// `factory.cpp::create_from_factory_index`). -pub fn register(meta: &mut Vec) { - meta.push(NodeMeta { - type_id: "org.olivevideoeditor.Olive.volume", - name: "Volume", - categories: &[Category::Filter], - create, - }); -} diff --git a/crates/oak-node/src/nodes/wavedistortnode.rs b/crates/oak-node/src/nodes/wavedistortnode.rs index 05671b86d..66f064506 100644 --- a/crates/oak-node/src/nodes/wavedistortnode.rs +++ b/crates/oak-node/src/nodes/wavedistortnode.rs @@ -241,6 +241,17 @@ pub fn create() -> (NodeCore, Box) { (core, Box::new(WaveDistortNode)) } +/// Register this node type (C++ factory entry for +/// `org.olivevideoeditor.Olive.wave`). +pub fn register(meta: &mut Vec) { + meta.push(NodeMeta { + type_id: "org.olivevideoeditor.Olive.wave", + name: "Wave", + categories: &[Category::Distort], + create, + }); +} + #[cfg(test)] mod tests { use super::*; @@ -341,14 +352,3 @@ mod tests { assert_eq!(dup.name(), "Wave"); } } - -/// Register this node type (C++ factory entry for -/// `org.olivevideoeditor.Olive.wave`). -pub fn register(meta: &mut Vec) { - meta.push(NodeMeta { - type_id: "org.olivevideoeditor.Olive.wave", - name: "Wave", - categories: &[Category::Distort], - create, - }); -} diff --git a/crates/oak-node/src/nodes/whitebalance.rs b/crates/oak-node/src/nodes/whitebalance.rs index 27a6ff3d8..eb4d7dd38 100644 --- a/crates/oak-node/src/nodes/whitebalance.rs +++ b/crates/oak-node/src/nodes/whitebalance.rs @@ -110,7 +110,7 @@ impl WhiteBalanceNode { // exposure, then let tint move along the green-magenta axis. let tint_gain = (1.0 + tint).clamp(0.0, 2.0); - [red / green, green / green * tint_gain, blue / green] + [red / green, tint_gain, blue / green] } } @@ -189,16 +189,18 @@ impl NodeBehavior for WhiteBalanceNode { params.insert(GAIN_INPUT.to_string(), crate::value::NodeValue::Vec3(gain)); table.push( crate::value::ValueType::Texture, - crate::value::NodeValue::Texture(crate::handle::make_owned(Job::ShaderJob(ShaderJobPayload { - node_id: crate::id::NodeId::INVALID, - time, - iterations: 1, - type_id: self.type_id().to_string(), - shader_id: String::new(), - effect_input: core.effect_input.clone(), - params, - iterative_input: String::new(), - }))), + crate::value::NodeValue::Texture(crate::handle::make_owned(Job::ShaderJob( + ShaderJobPayload { + node_id: crate::id::NodeId::INVALID, + time, + iterations: 1, + type_id: self.type_id().to_string(), + shader_id: String::new(), + effect_input: core.effect_input.clone(), + params, + iterative_input: String::new(), + }, + ))), None, ); } @@ -270,11 +272,11 @@ pub fn register(meta: &mut Vec) { #[cfg(test)] mod tests { - use super::*; - use crate::value::{NodeValue, NodeValueTable, ValueType}; - use oak_core::Rational; + use super::*; + use crate::value::{NodeValue, NodeValueTable, ValueType}; + use oak_core::Rational; - #[test] + #[test] fn input_names() { let n = WhiteBalanceNode; assert_eq!(n.input_name(TEXTURE_INPUT), "Input"); @@ -391,8 +393,7 @@ mod tests { panic!("expected a texture-typed value"); }; let payload = - unsafe { crate::jobs::shader_job(handle) } - .expect("payload boxed behind the handle"); + unsafe { crate::jobs::shader_job(handle) }.expect("payload boxed behind the handle"); assert_eq!(payload.type_id, "org.olivevideoeditor.Olive.whitebalance"); assert_eq!(payload.shader_id, ""); assert_eq!(payload.iterations, 1); diff --git a/crates/oak-node/src/ops.rs b/crates/oak-node/src/ops.rs index 5c533aac4..0caf0a17e 100644 --- a/crates/oak-node/src/ops.rs +++ b/crates/oak-node/src/ops.rs @@ -252,9 +252,7 @@ pub fn set_value_at_time_command( .and_then(|e| e.core.keyframe_track(&input, element)) .and_then(|t| { t.keys() - .iter() - .filter(|k| k.time <= time) - .next_back() + .iter().rfind(|k| k.time <= time) .map(|k| k.interpolation) }) .unwrap_or(Interpolation::Linear); diff --git a/crates/oak-node/src/project.rs b/crates/oak-node/src/project.rs index 7c8e51c65..2bed43ba3 100644 --- a/crates/oak-node/src/project.rs +++ b/crates/oak-node/src/project.rs @@ -347,8 +347,8 @@ impl Project { return self.custom_cache_path.clone(); } } - 1 => { - if !self.filename.is_empty() { + 1 + if !self.filename.is_empty() => { let dir = std::path::Path::new(&self.filename) .parent() .map(|p| p.to_string_lossy().into_owned()) @@ -357,7 +357,6 @@ impl Project { return format!("{}/cache", dir); } } - } _ => {} } // Default location: the shared disk-cache directory (single-lib: diff --git a/crates/oak-node/src/value.rs b/crates/oak-node/src/value.rs index 4bedc005f..915d1d175 100644 --- a/crates/oak-node/src/value.rs +++ b/crates/oak-node/src/value.rs @@ -690,7 +690,7 @@ impl Clone for NodeValue { fn clone(&self) -> Self { match self { NodeValue::Texture(h) => { - let h2 = h.clone(); + let h2 = *h; if let Some(f) = h2.addref { // Safety: `h2` is a valid handle; addref only touches // the refcount. diff --git a/crates/oak-node/tests/serializer_test.rs b/crates/oak-node/tests/serializer_test.rs index fa0d541c7..4457e9766 100644 --- a/crates/oak-node/tests/serializer_test.rs +++ b/crates/oak-node/tests/serializer_test.rs @@ -163,10 +163,7 @@ fn golden_project_with_footage_loads() { // The root folder (from the settings "root" key) holds the bin. let root = p.graph.get(p.root).expect("root folder resolves"); - assert_eq!( - root.behavior.type_id(), - "org.olivevideoeditor.Olive.folder" - ); + assert_eq!(root.behavior.type_id(), "org.olivevideoeditor.Olive.folder"); let folder = root .behavior .as_any() @@ -507,10 +504,10 @@ fn build_full_project() -> std::sync::Arc( - p: &'a oak_node::project::Project, +fn list_of( + p: &oak_node::project::Project, id: oak_node::id::NodeId, -) -> &'a oak_node::track::TrackListBehavior { +) -> &oak_node::track::TrackListBehavior { p.graph .get(id) .unwrap() @@ -521,7 +518,10 @@ fn list_of<'a>( } /// Field-by-field comparison of the round-tripped full project. -fn assert_full_roundtrip_fields(orig: &oak_node::project::Project, loaded: &oak_node::project::Project) { +fn assert_full_roundtrip_fields( + orig: &oak_node::project::Project, + loaded: &oak_node::project::Project, +) { use oak_node::block::ClipBlockBehavior; use oak_node::folder::FolderBehavior; use oak_node::footage::FootageBehavior; @@ -533,7 +533,11 @@ fn assert_full_roundtrip_fields(orig: &oak_node::project::Project, loaded: &oak_ assert_eq!(loaded.settings, orig.settings, "settings"); // The graph keeps its node count, types and edge count. - assert_eq!(loaded.graph.node_count(), orig.graph.node_count(), "node count"); + assert_eq!( + loaded.graph.node_count(), + orig.graph.node_count(), + "node count" + ); let o_types: Vec<&str> = orig .graph .node_ids() @@ -607,7 +611,10 @@ fn assert_full_roundtrip_fields(orig: &oak_node::project::Project, loaded: &oak_ l_f.proxy_video_stream_index, o_f.proxy_video_stream_index, "proxy stream" ); - assert_eq!(l_f.proxy_preset_version, o_f.proxy_preset_version, "proxy preset"); + assert_eq!( + l_f.proxy_preset_version, o_f.proxy_preset_version, + "proxy preset" + ); assert_eq!(l_f.streams.len(), o_f.streams.len(), "stream count"); for (ls, os) in l_f.streams.iter().zip(&o_f.streams) { assert_eq!(ls.index, os.index, "stream index"); @@ -644,7 +651,11 @@ fn assert_full_roundtrip_fields(orig: &oak_node::project::Project, loaded: &oak_ orig.graph.get(o_seq).unwrap().core.override_color, "sequence color" ); - assert_eq!(l_s.track_lists.len(), o_s.track_lists.len(), "track list count"); + assert_eq!( + l_s.track_lists.len(), + o_s.track_lists.len(), + "track list count" + ); let (o_vlist, l_vlist) = (o_s.track_lists[0], l_s.track_lists[0]); let (o_alist, l_alist) = (o_s.track_lists[1], l_s.track_lists[1]); @@ -653,14 +664,22 @@ fn assert_full_roundtrip_fields(orig: &oak_node::project::Project, loaded: &oak_ assert_eq!(l_vl.kind, o_vl.kind, "video list kind"); assert_eq!(l_vl.array_base, o_vl.array_base, "video list base"); assert_eq!(l_vl.sequence, Some(l_seq), "video list sequence backref"); - assert_eq!(l_vl.tracks.len(), o_vl.tracks.len(), "video list track count"); + assert_eq!( + l_vl.tracks.len(), + o_vl.tracks.len(), + "video list track count" + ); let (o_vtrack, l_vtrack) = (o_vl.tracks[0], l_vl.tracks[0]); let o_al = list_of(orig, o_alist); let l_al = list_of(loaded, l_alist); assert_eq!(l_al.kind, o_al.kind, "audio list kind"); assert_eq!(l_al.array_base, o_al.array_base, "audio list base"); assert_eq!(l_al.sequence, Some(l_seq), "audio list sequence backref"); - assert_eq!(l_al.tracks.len(), o_al.tracks.len(), "audio list track count"); + assert_eq!( + l_al.tracks.len(), + o_al.tracks.len(), + "audio list track count" + ); let (o_atrack, l_atrack) = (o_al.tracks[0], l_al.tracks[0]); // Video track: kind/blocks/muted/locked/height/index/backref. @@ -686,7 +705,11 @@ fn assert_full_roundtrip_fields(orig: &oak_node::project::Project, loaded: &oak_ assert_eq!(l_t.height, o_t.height, "video track height"); assert_eq!(l_t.index, o_t.index, "video track index"); assert_eq!(l_t.track_list, Some(l_vlist), "video track list backref"); - assert_eq!(l_t.blocks.len(), o_t.blocks.len(), "video track block count"); + assert_eq!( + l_t.blocks.len(), + o_t.blocks.len(), + "video track block count" + ); let o_clip1 = o_t.blocks[0]; let o_gap1 = o_t.blocks[1]; let o_clip2 = o_t.blocks[2]; @@ -816,7 +839,11 @@ fn assert_full_roundtrip_fields(orig: &oak_node::project::Project, loaded: &oak_ assert_eq!(l_at.kind, TrackType::Audio, "audio track kind"); assert_eq!(l_at.locked, o_at.locked, "audio track locked"); assert_eq!(l_at.track_list, Some(l_alist), "audio track list backref"); - assert_eq!(l_at.blocks.len(), o_at.blocks.len(), "audio track block count"); + assert_eq!( + l_at.blocks.len(), + o_at.blocks.len(), + "audio track block count" + ); let o_c3 = orig .graph .get(o_at.blocks[0]) @@ -956,7 +983,11 @@ fn multicam_node_round_trip_preserves_current_in() { // serializes combo indices as Int — `string_to_value` maps // `ValueType::Combo` to `NodeValue::Int`). assert_eq!( - l.graph.get(loaded_mc).unwrap().core.standard_value(CURRENT_INPUT, -1), + l.graph + .get(loaded_mc) + .unwrap() + .core + .standard_value(CURRENT_INPUT, -1), NodeValue::Int(2), "current_in survives the round-trip" ); @@ -1004,8 +1035,11 @@ fn dynamic_plugin_node_round_trips_across_load() { // from its OFX params). let probe_core = || { let mut core = NodeCore::new(); - core.inputs - .push(Input::new(INPUT_ID, ValueType::Float, NodeValue::Float(0.0))); + core.inputs.push(Input::new( + INPUT_ID, + ValueType::Float, + NodeValue::Float(0.0), + )); core }; { @@ -1049,7 +1083,11 @@ fn dynamic_plugin_node_round_trips_across_load() { .find(|id| l.graph.get(*id).map(|e| e.behavior.type_id()) == Some(TYPE_ID)) .expect("loaded project has the dynamic node"); assert_eq!( - l.graph.get(probe).unwrap().core.standard_value(INPUT_ID, -1), + l.graph + .get(probe) + .unwrap() + .core + .standard_value(INPUT_ID, -1), NodeValue::Float(2.5), "the standard value survives the rebuild" ); @@ -1077,7 +1115,10 @@ fn multicam_clip_round_trip_preserves_wiring() { let seq_id = p.graph.add_node(core, behavior); let (core, behavior) = TrackListBehavior::create(); let list_id = p.graph.add_node(core, behavior); - let (core, behavior) = (NodeCore::new(), Box::new(TrackBehavior::new(TrackType::Video))); + let (core, behavior) = ( + NodeCore::new(), + Box::new(TrackBehavior::new(TrackType::Video)), + ); let track_id = p.graph.add_node(core, behavior); { let seq = p.graph.get_mut(seq_id).unwrap(); @@ -1132,18 +1173,13 @@ fn multicam_clip_round_trip_preserves_wiring() { // The multicam node routed between the sequence and the clip. let (core, behavior) = create(); let mc_id = p.graph.add_node(core, behavior); - p.graph - .connect(mc_id, clip_id, "tex_in", -1) - .unwrap(); + p.graph.connect(mc_id, clip_id, "tex_in", -1).unwrap(); p.graph.connect(seq_id, mc_id, SEQUENCE_INPUT, -1).unwrap(); let mc = p.graph.get_mut(mc_id).unwrap(); mc.core .set_standard_value(CURRENT_INPUT, -1, NodeValue::Combo(2)); - mc.core.set_standard_value( - SEQUENCE_TYPE_INPUT, - -1, - NodeValue::Combo(0), - ); + mc.core + .set_standard_value(SEQUENCE_TYPE_INPUT, -1, NodeValue::Combo(0)); (seq_id, clip_id, mc_id) }; let _ = (seq_id, clip_id, mc_id); @@ -1165,12 +1201,20 @@ fn multicam_clip_round_trip_preserves_wiring() { }) .expect("loaded project has a MultiCamNode"); assert_eq!( - l.graph.get(loaded_mc).unwrap().core.standard_value(CURRENT_INPUT, -1), + l.graph + .get(loaded_mc) + .unwrap() + .core + .standard_value(CURRENT_INPUT, -1), NodeValue::Int(2), "current_in survives" ); assert_eq!( - l.graph.get(loaded_mc).unwrap().core.standard_value(SEQUENCE_TYPE_INPUT, -1), + l.graph + .get(loaded_mc) + .unwrap() + .core + .standard_value(SEQUENCE_TYPE_INPUT, -1), NodeValue::Int(0), "sequence_type_in survives" ); @@ -1332,5 +1376,8 @@ fn legacy_project_without_endpoints_migrates_on_load() { let r = reloaded.lock().unwrap(); assert_eq!(r.graph.endpoints(), Some((li, lo))); assert_eq!(r.graph.node_count(), legacy_count + 2); - assert_eq!(r.graph.connected_output(lo, GRAPH_OUTPUT_INPUT, -1), Some(li)); + assert_eq!( + r.graph.connected_output(lo, GRAPH_OUTPUT_INPUT, -1), + Some(li) + ); } diff --git a/crates/oak-node/tests/value_keyframe_test.rs b/crates/oak-node/tests/value_keyframe_test.rs index 914f514d1..70276c9c9 100644 --- a/crates/oak-node/tests/value_keyframe_test.rs +++ b/crates/oak-node/tests/value_keyframe_test.rs @@ -70,7 +70,7 @@ fn texture_value_drop_releases() { assert_eq!(refs(&h), 2); { - let v = NodeValue::Texture(h.clone()); + let v = NodeValue::Texture(h); assert_eq!(refs(&h), 2, "no extra reference taken on clone"); drop(v); // must release the payload's reference assert_eq!(refs(&h), 1); @@ -97,7 +97,7 @@ fn texture_value_clone_addrefs() { assert_eq!(refs(&h), 1); { - let a = NodeValue::Texture(h.clone()); + let a = NodeValue::Texture(h); assert_eq!(refs(&h), 1, "construction takes the caller's reference"); { let b = a.clone(); diff --git a/crates/oak-otio/src/fcpxml.rs b/crates/oak-otio/src/fcpxml.rs index c815487bf..2a7585b17 100644 --- a/crates/oak-otio/src/fcpxml.rs +++ b/crates/oak-otio/src/fcpxml.rs @@ -1455,7 +1455,7 @@ mod tests { assert!(FcpxmlError::UnsupportedVersion("x".into()) .to_string() .contains("version error")); - let io = FcpxmlError::Io(std::io::Error::new(std::io::ErrorKind::Other, "io")); + let io = FcpxmlError::Io(std::io::Error::other("io")); assert!(io.to_string().contains("file error")); assert!(io.source().is_some()); assert!(FcpxmlError::Xml("x".into()).source().is_none()); @@ -1577,6 +1577,6 @@ mod tests { #[test] fn file_io_errors() { assert!(from_fcpxml_file("/nonexistent/oakotio-test.fcpxml").is_err()); - assert!(to_fcpxml_file(&[], &std::env::temp_dir()).is_err()); + assert!(to_fcpxml_file(&[], std::env::temp_dir()).is_err()); } } diff --git a/crates/oak-otio/tests/fcpxml.rs b/crates/oak-otio/tests/fcpxml.rs index 4e585efc9..402d4c583 100644 --- a/crates/oak-otio/tests/fcpxml.rs +++ b/crates/oak-otio/tests/fcpxml.rs @@ -20,7 +20,9 @@ //! version) and lenient handling of unknown elements. use oak_core::Rational; -use oak_otio::{Clip, Composable, ExternalReference, MediaReference, RationalTime, Timeline, Track}; +use oak_otio::{ + Clip, Composable, ExternalReference, MediaReference, RationalTime, Timeline, Track, +}; /// The NTSC video rate 30000/1001 (~29.97 fps) as an exact rational. const NTSC_RATE: f64 = 30000.0 / 1001.0; @@ -189,7 +191,7 @@ fn parse_synthetic_document() { #[test] fn model_round_trips_through_fcpxml() { let original = parse_synthetic(); - let xml = oak_otio::to_fcpxml_string(&[original.clone()]).expect("export fcpxml"); + let xml = oak_otio::to_fcpxml_string(std::slice::from_ref(&original)).expect("export fcpxml"); let reparsed = oak_otio::from_fcpxml_string(&xml).expect("reparse exported fcpxml"); assert_eq!(reparsed.len(), 1); assert_eq!(reparsed[0], original, "model survives fcpxml round trip"); diff --git a/crates/oak-plugin/src/gl_bridge.rs b/crates/oak-plugin/src/gl_bridge.rs index 9fea73b51..bc24e1dda 100644 --- a/crates/oak-plugin/src/gl_bridge.rs +++ b/crates/oak-plugin/src/gl_bridge.rs @@ -960,7 +960,7 @@ mod tests { let stride = w as usize * 4; for (i, g) in got.iter().enumerate() { let row = i / stride; - let inv_row = (h as usize - 1 - row) as usize; + let inv_row = h as usize - 1 - row; let expected = want[inv_row * stride + i % stride]; assert!( (g - expected).abs() < 1e-6, diff --git a/crates/oak-plugin/src/instance.rs b/crates/oak-plugin/src/instance.rs index a0e9c8779..69ccc9c09 100644 --- a/crates/oak-plugin/src/instance.rs +++ b/crates/oak-plugin/src/instance.rs @@ -640,11 +640,10 @@ impl Instance { // 进度报告器(facade 回调优先;无回调而 app 注册了 UI 工厂 // 时装静默报告器,progressStart 再经工厂现造 UI 报告器)。 - if let Some((cb, userdata)) = self + if let Some((cb, userdata)) = *self .progress_cb .lock() .unwrap_or_else(|e| e.into_inner()) - .clone() { crate::suites::progress::set_current(Some(unsafe { crate::progress::ProgressReporter::new(cb, userdata as *mut std::ffi::c_void) @@ -735,11 +734,10 @@ impl Instance { })); // GL 模式无 CPU 输出图像(current_output 保持 None)。 - if let Some((cb, userdata)) = self + if let Some((cb, userdata)) = *self .progress_cb .lock() .unwrap_or_else(|e| e.into_inner()) - .clone() { crate::suites::progress::set_current(Some(unsafe { crate::progress::ProgressReporter::new(cb, userdata as *mut std::ffi::c_void) diff --git a/crates/oak-plugin/src/node_factory.rs b/crates/oak-plugin/src/node_factory.rs index f8338557f..264a27939 100644 --- a/crates/oak-plugin/src/node_factory.rs +++ b/crates/oak-plugin/src/node_factory.rs @@ -942,9 +942,7 @@ fn shared_plugin_instance(identifier: &str) -> Option { return Some(id); } let host = Host::global(); - if host.cache.find(identifier).is_none() { - return None; - } + host.cache.find(identifier)?; // 上下文选择:filter 优先、实例化失败按序回退(与 // [`register_plugin_nodes`] 同一策略,见 // [`Host::create_instance_preferred`])。 diff --git a/crates/oak-plugin/src/param.rs b/crates/oak-plugin/src/param.rs index 2e3c0f399..82e7beb44 100644 --- a/crates/oak-plugin/src/param.rs +++ b/crates/oak-plugin/src/param.rs @@ -619,7 +619,7 @@ pub(crate) fn param_value_from_node( TYPE_DOUBLE3D => { (v.r#type == T::VEC3).then(|| ParamValue::Double([v.f[0], v.f[1], v.f[2]], 3)) } - TYPE_INTEGER => (v.r#type == T::INT).then(|| ParamValue::Int([v.num as i32, 0, 0], 1)), + TYPE_INTEGER => (v.r#type == T::INT).then_some(ParamValue::Int([v.num as i32, 0, 0], 1)), TYPE_INTEGER2D | TYPE_INTEGER3D => { let dim = if ofx_type == TYPE_INTEGER2D { 2 } else { 3 }; Some(ParamValue::Int( @@ -627,8 +627,8 @@ pub(crate) fn param_value_from_node( dim, )) } - TYPE_BOOLEAN => (v.r#type == T::BOOL).then(|| ParamValue::Bool(v.num != 0)), - TYPE_CHOICE => (v.r#type == T::COMBO).then(|| ParamValue::Choice(v.num as i32)), + TYPE_BOOLEAN => (v.r#type == T::BOOL).then_some(ParamValue::Bool(v.num != 0)), + TYPE_CHOICE => (v.r#type == T::COMBO).then_some(ParamValue::Choice(v.num as i32)), TYPE_RGB => { (v.r#type == T::COLOR).then(|| ParamValue::Color([v.f[0], v.f[1], v.f[2], 0.0], 3)) } diff --git a/crates/oak-plugin/src/render.rs b/crates/oak-plugin/src/render.rs index 461f57c7a..a12fe36e3 100644 --- a/crates/oak-plugin/src/render.rs +++ b/crates/oak-plugin/src/render.rs @@ -108,7 +108,7 @@ pub fn texture_create( return Err(Error::Invalid); } // 行跨度推导通道数(RGB 3 通道等非常规分量)。 - if linesize > 0 && w > 0 && (linesize as usize) % (w * bpc) == 0 { + if linesize > 0 && w > 0 && (linesize as usize).is_multiple_of(w * bpc) { frame.channels = (linesize as usize / (w * bpc)) as i32; } let tight = frame.linesize_bytes(); diff --git a/crates/oak-plugin/src/render_driver.rs b/crates/oak-plugin/src/render_driver.rs index 1baae1c1a..d677720c6 100644 --- a/crates/oak-plugin/src/render_driver.rs +++ b/crates/oak-plugin/src/render_driver.rs @@ -256,7 +256,7 @@ pub fn render_frame( if clip.name == "Output" { continue; } - if pick_input(&clip.name, job).map_or(false, |t| usable(&t)) { + if pick_input(&clip.name, job).is_some_and(|t| usable(&t)) { clip.set_region_of_definition(region_of_interest, job.time); clip.set_video_params(depth_format, 4); } diff --git a/crates/oak-plugin/src/suites/draw.rs b/crates/oak-plugin/src/suites/draw.rs index ca49f6069..0ab37c0b9 100644 --- a/crates/oak-plugin/src/suites/draw.rs +++ b/crates/oak-plugin/src/suites/draw.rs @@ -257,7 +257,7 @@ unsafe extern "C" fn set_colour(ctx: *mut c_void, colour: *const OfxRGBAColourF) if colour.is_null() { return Err(status::ERR_VALUE); } - let _ = resolve_mut(ctx)?; + resolve_mut(ctx)?; let v = unsafe { &*colour }; let addr = ctx as usize; let mut live = lock(); @@ -269,7 +269,7 @@ unsafe extern "C" fn set_colour(ctx: *mut c_void, colour: *const OfxRGBAColourF) unsafe extern "C" fn set_line_width(ctx: *mut c_void, width: c_float) -> c_int { caught(|| { - let _ = resolve_mut(ctx)?; + resolve_mut(ctx)?; let addr = ctx as usize; let mut live = lock(); let slot = live.get_mut(&addr).expect("resolve 已保证存活"); @@ -283,7 +283,7 @@ unsafe extern "C" fn set_line_stipple(ctx: *mut c_void, pattern: c_int) -> c_int if !(0..=stipple::DOT_DASH).contains(&pattern) { return Err(status::ERR_VALUE); } - let _ = resolve_mut(ctx)?; + resolve_mut(ctx)?; let addr = ctx as usize; let mut live = lock(); let slot = live.get_mut(&addr).expect("resolve 已保证存活"); diff --git a/crates/oak-plugin/src/suites/gl_render.rs b/crates/oak-plugin/src/suites/gl_render.rs index 8ec68c55c..36cff2e9b 100644 --- a/crates/oak-plugin/src/suites/gl_render.rs +++ b/crates/oak-plugin/src/suites/gl_render.rs @@ -122,52 +122,6 @@ pub(crate) fn pick_gl_pixel_depth( None } -#[cfg(test)] -mod tests { - use super::*; - use crate::property::{PropertySet, Value}; - - fn cs(s: &str) -> CString { - CString::new(s).unwrap() - } - - /// GL 像素深度协商矩阵(ofxGPURender.h kOfxOpenGLPropPixelDepth): - /// 未声明/含 Float → 管线 F32 可行;声明且不含 Float → None - /// (GL 模式不可行,回退 CPU)。 - #[test] - fn pick_gl_pixel_depth_matrix() { - let props = PropertySet::new(); - assert_eq!(pick_gl_pixel_depth(&props), Some("OfxBitDepthFloat")); - - let props2 = PropertySet::new(); - props2.define( - crate::host::PROP_GL_PIXEL_DEPTH, - vec![ - Value::String(cs("OfxBitDepthHalf")), - Value::String(cs("OfxBitDepthByte")), - ], - ); - assert_eq!(pick_gl_pixel_depth(&props2), None); - - let props3 = PropertySet::new(); - props3.define( - crate::host::PROP_GL_PIXEL_DEPTH, - vec![ - Value::String(cs("OfxBitDepthByte")), - Value::String(cs("OfxBitDepthFloat")), - ], - ); - assert_eq!(pick_gl_pixel_depth(&props3), Some("OfxBitDepthFloat")); - - let props4 = PropertySet::new(); - props4.define( - crate::host::PROP_GL_PIXEL_DEPTH, - vec![Value::String(cs("OfxBitDepthFloat"))], - ); - assert_eq!(pick_gl_pixel_depth(&props4), Some("OfxBitDepthFloat")); - } -} - // ---- 存活纹理表 ----------------------------------------------------------- /// 存活 GL 纹理表:clipLoadTexture 产出(props 地址 → 属性集 + @@ -224,11 +178,9 @@ pub(crate) fn purge_leftovers() { .collect(); live.retain(|_, (_, _, is_output, _)| *is_output); drop(live); - for gl in dropped { - if let Some(gl) = gl { - delete_gl_texture_if_gl(gl); - } - } + for gl in dropped.into_iter().flatten() { + delete_gl_texture_if_gl(gl); + } } /// 公共入口模板:panic 兜底。 @@ -539,8 +491,54 @@ pub struct GlRenderSuiteV1 { pub fn suite_v1() -> &'static GlRenderSuiteV1 { static SUITE: std::sync::OnceLock = std::sync::OnceLock::new(); SUITE.get_or_init(|| GlRenderSuiteV1 { - clip_load_texture: clip_load_texture, - clip_free_texture: clip_free_texture, - flush_resources: flush_resources, + clip_load_texture, + clip_free_texture, + flush_resources, }) } + +#[cfg(test)] +mod tests { + use super::*; + use crate::property::{PropertySet, Value}; + + fn cs(s: &str) -> CString { + CString::new(s).unwrap() + } + + /// GL 像素深度协商矩阵(ofxGPURender.h kOfxOpenGLPropPixelDepth): + /// 未声明/含 Float → 管线 F32 可行;声明且不含 Float → None + /// (GL 模式不可行,回退 CPU)。 + #[test] + fn pick_gl_pixel_depth_matrix() { + let props = PropertySet::new(); + assert_eq!(pick_gl_pixel_depth(&props), Some("OfxBitDepthFloat")); + + let props2 = PropertySet::new(); + props2.define( + crate::host::PROP_GL_PIXEL_DEPTH, + vec![ + Value::String(cs("OfxBitDepthHalf")), + Value::String(cs("OfxBitDepthByte")), + ], + ); + assert_eq!(pick_gl_pixel_depth(&props2), None); + + let props3 = PropertySet::new(); + props3.define( + crate::host::PROP_GL_PIXEL_DEPTH, + vec![ + Value::String(cs("OfxBitDepthByte")), + Value::String(cs("OfxBitDepthFloat")), + ], + ); + assert_eq!(pick_gl_pixel_depth(&props3), Some("OfxBitDepthFloat")); + + let props4 = PropertySet::new(); + props4.define( + crate::host::PROP_GL_PIXEL_DEPTH, + vec![Value::String(cs("OfxBitDepthFloat"))], + ); + assert_eq!(pick_gl_pixel_depth(&props4), Some("OfxBitDepthFloat")); + } +} diff --git a/crates/oak-plugin/src/suites/image_effect.rs b/crates/oak-plugin/src/suites/image_effect.rs index e55f53867..bb346761d 100644 --- a/crates/oak-plugin/src/suites/image_effect.rs +++ b/crates/oak-plugin/src/suites/image_effect.rs @@ -200,7 +200,10 @@ unsafe extern "C" fn clip_define( EffectRef::Instance(_) => return Err(status::ERR_BAD_HANDLE), }; let clip = ClipDescriptor::new(n); - // 重复定义:替换原 Box(HS map 覆盖语义)。 + // 重复定义:替换原 Box(HS map 覆盖语义)。必须是新的 Box: + // 旧句柄随即失效(测试断言 clip != clip2),复用同一分配会 + // 破坏这一 C ABI 契约,故此处保留 `Box::new`。 + #[allow(clippy::replace_box)] if let Some(existing) = desc.clips.iter_mut().find(|c| c.name == n) { *existing = Box::new(clip); } else { @@ -469,19 +472,19 @@ use crate::property::Value; pub fn suite_v1() -> &'static ImageEffectSuiteV1 { static SUITE: std::sync::OnceLock = std::sync::OnceLock::new(); SUITE.get_or_init(|| ImageEffectSuiteV1 { - get_property_set: get_property_set, - get_param_set: get_param_set, - clip_define: clip_define, - clip_get_handle: clip_get_handle, - clip_get_property_set: clip_get_property_set, - clip_get_image: clip_get_image, - clip_release_image: clip_release_image, - clip_get_region_of_definition: clip_get_region_of_definition, - abort: abort, - image_memory_alloc: image_memory_alloc, - image_memory_free: image_memory_free, - image_memory_lock: image_memory_lock, - image_memory_unlock: image_memory_unlock, + get_property_set, + get_param_set, + clip_define, + clip_get_handle, + clip_get_property_set, + clip_get_image, + clip_release_image, + clip_get_region_of_definition, + abort, + image_memory_alloc, + image_memory_free, + image_memory_lock, + image_memory_unlock, }) } diff --git a/crates/oak-plugin/src/suites/multithread.rs b/crates/oak-plugin/src/suites/multithread.rs index 112ce56c2..e26385542 100644 --- a/crates/oak-plugin/src/suites/multithread.rs +++ b/crates/oak-plugin/src/suites/multithread.rs @@ -84,7 +84,7 @@ unsafe extern "C" fn multi_thread( // 裸指针不可 Send:以 usize 搬运,线程内还原(C 侧本即 // 整数传递语义)。 let arg = thread_arg as usize; - let _ = std::thread::scope(|scope| { + std::thread::scope(|scope| { for i in 0..n_threads { scope.spawn(move || { THREAD_INDEX.with(|t| t.set(Some(i))); @@ -244,15 +244,15 @@ unsafe extern "C" fn mutex_try_lock(handle: *mut c_void) -> c_int { pub fn suite_v1() -> &'static MultiThreadSuiteV1 { static SUITE: std::sync::OnceLock = std::sync::OnceLock::new(); SUITE.get_or_init(|| MultiThreadSuiteV1 { - multi_thread: multi_thread, + multi_thread, num_cpus: multi_thread_num_cpus, index: multi_thread_index, is_spawned: multi_thread_is_spawned, - mutex_create: mutex_create, - mutex_destroy: mutex_destroy, - mutex_lock: mutex_lock, - mutex_unlock: mutex_unlock, - mutex_try_lock: mutex_try_lock, + mutex_create, + mutex_destroy, + mutex_lock, + mutex_unlock, + mutex_try_lock, }) } diff --git a/crates/oak-plugin/src/suites/param.rs b/crates/oak-plugin/src/suites/param.rs index 934d053eb..8e83b19b5 100644 --- a/crates/oak-plugin/src/suites/param.rs +++ b/crates/oak-plugin/src/suites/param.rs @@ -160,11 +160,10 @@ fn caught(f: impl FnOnce() -> Result<(), c_int>) -> c_int { |_| status::FAILED, |r| r.map_or_else(|c| c, |()| status::OK), ); - if std::env::var_os("OAK_OFX_TRACE").is_some() { - if code != status::OK { + if std::env::var_os("OAK_OFX_TRACE").is_some() + && code != status::OK { eprintln!("[ofx] param suite error {code} at {caller}"); } - } code } @@ -712,24 +711,24 @@ extern "C" { pub fn suite_v1() -> &'static ParameterSuiteV1 { static SUITE: std::sync::OnceLock = std::sync::OnceLock::new(); SUITE.get_or_init(|| ParameterSuiteV1 { - param_define: param_define, - param_get_handle: param_get_handle, - param_set_get_property_set: param_set_get_property_set, - param_get_property_set: param_get_property_set, + param_define, + param_get_handle, + param_set_get_property_set, + param_get_property_set, param_get_value: ofx_param_get_value_shim, param_get_value_at_time: ofx_param_get_value_at_time_shim, param_get_derivative: ofx_param_get_derivative_shim, param_get_integral: ofx_param_get_integral_shim, param_set_value: ofx_param_set_value_shim, param_set_value_at_time: ofx_param_set_value_at_time_shim, - param_get_num_keys: param_get_num_keys, - param_get_key_time: param_get_key_time, - param_get_key_index: param_get_key_index, - param_delete_key: param_delete_key, - param_delete_all_keys: param_delete_all_keys, - param_copy: param_copy, - param_edit_begin: param_edit_begin, - param_edit_end: param_edit_end, + param_get_num_keys, + param_get_key_time, + param_get_key_index, + param_delete_key, + param_delete_all_keys, + param_copy, + param_edit_begin, + param_edit_end, }) } diff --git a/crates/oak-plugin/src/suites/parametric.rs b/crates/oak-plugin/src/suites/parametric.rs index 62dd79819..aaa09e259 100644 --- a/crates/oak-plugin/src/suites/parametric.rs +++ b/crates/oak-plugin/src/suites/parametric.rs @@ -399,13 +399,13 @@ unsafe extern "C" fn parametric_param_delete_all_control_points( pub fn suite_v1() -> &'static ParametricParameterSuiteV1 { static SUITE: std::sync::OnceLock = std::sync::OnceLock::new(); SUITE.get_or_init(|| ParametricParameterSuiteV1 { - parametric_param_get_value: parametric_param_get_value, - parametric_param_get_n_control_points: parametric_param_get_n_control_points, - parametric_param_get_nth_control_point: parametric_param_get_nth_control_point, - parametric_param_set_nth_control_point: parametric_param_set_nth_control_point, - parametric_param_add_control_point: parametric_param_add_control_point, - parametric_param_delete_control_point: parametric_param_delete_control_point, - parametric_param_delete_all_control_points: parametric_param_delete_all_control_points, + parametric_param_get_value, + parametric_param_get_n_control_points, + parametric_param_get_nth_control_point, + parametric_param_set_nth_control_point, + parametric_param_add_control_point, + parametric_param_delete_control_point, + parametric_param_delete_all_control_points, }) } diff --git a/crates/oak-plugin/src/suites/property.rs b/crates/oak-plugin/src/suites/property.rs index efa3fe0f9..bf7d8a51e 100644 --- a/crates/oak-plugin/src/suites/property.rs +++ b/crates/oak-plugin/src/suites/property.rs @@ -563,11 +563,10 @@ unsafe extern "C" fn prop_get_int( } _ => Err(status::FAILED), } - .map_err(|c| { + .inspect_err(|&c| { if std::env::var_os("OAK_OFX_TRACE").is_some() { eprintln!("[ofx] propGetInt({name}) -> {c}"); } - c }) }) } @@ -618,14 +617,13 @@ fn get_n( } Ok(()) }) - .map_err(|code| { + .inspect_err(|&code| { // The property the plugin asked for and we did not have — the // single most useful line when a plugin reports // MissingHostFeature (trace-gated, like fetchSuite misses). if std::env::var_os("OAK_OFX_TRACE").is_some() { eprintln!("[ofx] property miss: {name} (code {code})"); } - code }) } @@ -641,10 +639,7 @@ unsafe extern "C" fn prop_get_pointer_n( if count > 0 && out.is_null() { return Err(status::ERR_VALUE); } - get_n(set, name, count, Kind::Pointer, |v, i| match v { - Value::Pointer(p) => *out.add(i) = *p, - _ => {} - }) + get_n(set, name, count, Kind::Pointer, |v, i| if let Value::Pointer(p) = v { *out.add(i) = *p }) }) } } @@ -661,10 +656,7 @@ unsafe extern "C" fn prop_get_string_n( if count > 0 && out.is_null() { return Err(status::ERR_VALUE); } - get_n(set, name, count, Kind::Str, |v, i| match v { - Value::String(s) => *out.add(i) = s.as_ptr() as *mut c_char, - _ => {} - }) + get_n(set, name, count, Kind::Str, |v, i| if let Value::String(s) = v { *out.add(i) = s.as_ptr() as *mut c_char }) }) } } diff --git a/crates/oak-plugin/tests/node_e2e_test.rs b/crates/oak-plugin/tests/node_e2e_test.rs index c252f5ab0..d20bb6128 100644 --- a/crates/oak-plugin/tests/node_e2e_test.rs +++ b/crates/oak-plugin/tests/node_e2e_test.rs @@ -27,6 +27,7 @@ mod common; +use oak_core::texture::Texture; use oak_core::{PixelFormat, Rational}; use oak_node::factory::Factory; use oak_node::graph::Graph; @@ -34,7 +35,6 @@ use oak_node::node::{NodeBehavior, NodeCore}; use oak_node::traverser::{EvalRequest, Traverser}; use oak_node::value::{NodeValue, ValueType}; use oak_plugin::host::Host; -use oak_core::texture::Texture; const PLUGIN_ID: &str = "org.oak.test-plugin"; const IDENTITY_ID: &str = "org.oak.test-plugin.identity"; @@ -70,7 +70,7 @@ impl NodeBehavior for ConstSource { ) { let mut frame = oak_render::eval::generate_frame(time, self.size, PixelFormat::F32).unwrap(); - for pixel in frame.data.chunks_exact_mut(16) { + for pixel in frame.data.as_chunks_mut::<16>().0 { for (i, v) in self.rgba.iter().enumerate() { pixel[i * 4..i * 4 + 4].copy_from_slice(&v.to_le_bytes()); } @@ -98,12 +98,8 @@ fn scan_and_register() -> bool { } /// 取输出表的渲染纹理(resolve 后的真纹理盒)。 -fn rendered_texture( - table: &oak_node::value::NodeValueTable, -) -> Texture { - let NodeValue::Texture(handle) = table - .get(ValueType::Texture) - .expect("根输出应有纹理") +fn rendered_texture(table: &oak_node::value::NodeValueTable) -> Texture { + let NodeValue::Texture(handle) = table.get(ValueType::Texture).expect("根输出应有纹理") else { panic!("纹理槽不是 Texture 值"); }; @@ -143,10 +139,7 @@ fn plugin_nodes_register_with_translated_inputs() { ); let meta = entries.iter().find(|m| m.type_id == PLUGIN_ID).unwrap(); assert_eq!(meta.sub_category, "Filter"); - assert_eq!( - meta.categories, - vec![oak_node::node::Category::OpenFx] - ); + assert_eq!(meta.categories, vec![oak_node::node::Category::OpenFx]); let (core, behavior) = Factory::global() .create_any(PLUGIN_ID) @@ -214,9 +207,7 @@ fn plugin_renders_constant_frame_end_to_end() { if !scan_and_register() { return; } - let (core, behavior) = Factory::global() - .create_any(PLUGIN_ID) - .expect("create_any"); + let (core, behavior) = Factory::global().create_any(PLUGIN_ID).expect("create_any"); let mut graph = Graph::new(); let src_id = graph.add_node( @@ -306,10 +297,7 @@ fn param_overrides_reach_instance() { let id = oak_plugin::node_factory::register_instance(inst.clone()); // 数值覆盖(gain = 1.25)。 - let job_values = vec![( - "gain".to_string(), - oak_node::value::NodeValue::Float(1.25), - )]; + let job_values = [("gain".to_string(), oak_node::value::NodeValue::Float(1.25))]; let pod: Vec<(String, oak_plugin::node::Value)> = job_values .iter() .filter_map(|(k, v)| { @@ -331,8 +319,7 @@ fn param_overrides_reach_instance() { clear_destination: false, interactive: false, }; - oak_plugin::render_driver::render_frame(&inst.value, &job) - .expect("render_frame 应成功"); + oak_plugin::render_driver::render_frame(&inst.value, &job).expect("render_frame 应成功"); let gain = inst.value.params.find("gain").unwrap().get(); assert_eq!( gain, @@ -340,10 +327,7 @@ fn param_overrides_reach_instance() { ); // NaN 覆盖回退默认(gain 默认 0.0)。 - let pod_nan = vec![( - "gain".to_string(), - oak_plugin::node::Value::float(f64::NAN), - )]; + let pod_nan = vec![("gain".to_string(), oak_plugin::node::Value::float(f64::NAN))]; let dst = oak_render::eval::generate_frame(Rational::new(0, 1), (2, 2), PixelFormat::F32) .unwrap(); let src = oak_render::eval::generate_frame(Rational::new(0, 1), (2, 2), PixelFormat::F32) @@ -359,8 +343,7 @@ fn param_overrides_reach_instance() { clear_destination: false, interactive: false, }; - oak_plugin::render_driver::render_frame(&inst.value, &job) - .expect("NaN 覆盖不应失败"); + oak_plugin::render_driver::render_frame(&inst.value, &job).expect("NaN 覆盖不应失败"); assert_eq!( inst.value.params.find("gain").unwrap().get(), oak_plugin::param::ParamValue::Double([0.0, 0.0, 0.0], 1) diff --git a/crates/oak-render/examples/render_audio_wav.rs b/crates/oak-render/examples/render_audio_wav.rs index 1549363c4..320b2cd0d 100644 --- a/crates/oak-render/examples/render_audio_wav.rs +++ b/crates/oak-render/examples/render_audio_wav.rs @@ -12,7 +12,9 @@ use oak_render::ticket::{AudioTicketParams, MontageClip, TicketPayload}; fn main() { let args: Vec = std::env::args().collect(); if args.len() < 6 { - eprintln!("usage: render_audio_wav [fps]"); + eprintln!( + "usage: render_audio_wav [fps]" + ); std::process::exit(64); } let media = &args[1]; @@ -20,7 +22,11 @@ fn main() { let start_sec: f64 = args[3].parse().unwrap(); let end_sec: f64 = args[4].parse().unwrap(); let out_path = &args[5]; - let fps: i64 = if args.len() > 6 { args[6].parse().unwrap() } else { 25 }; + let fps: i64 = if args.len() > 6 { + args[6].parse().unwrap() + } else { + 25 + }; let sample_rate = 48000i32; let channel_layout = 0x3u64; // stereo @@ -58,7 +64,7 @@ fn main() { other => { eprintln!("frame {f}: render failed: {:?}", other.is_err()); let frames = ((sample_rate as f64) / fps as f64).round() as usize; - pcm.extend(std::iter::repeat(0i16).take(frames * channels)); + pcm.extend(std::iter::repeat_n(0i16, frames * channels)); } } } diff --git a/crates/oak-render/src/cache.rs b/crates/oak-render/src/cache.rs index af6b54a6d..0c27fb402 100644 --- a/crates/oak-render/src/cache.rs +++ b/crates/oak-render/src/cache.rs @@ -144,7 +144,7 @@ fn uuid_text_to_bytes(uuid: &str) -> [u8; 16] { break; } let d = c.to_digit(16).unwrap_or(0) as u8; - if nibble % 2 == 0 { + if nibble.is_multiple_of(2) { bytes[nibble / 2] = d << 4; } else { bytes[nibble / 2] |= d; @@ -286,7 +286,7 @@ impl PlaybackCache { pub fn set_uuid(&mut self, uuid: &str) { self.uuid = uuid.to_string(); let dir = self.disk_dir.clone(); - let _ = self.load_state(&std::path::Path::new(&dir)); + let _ = self.load_state(std::path::Path::new(&dir)); } /// Set the frame timebase (frame-hash flavors; C++ `set_timebase`). @@ -319,7 +319,7 @@ impl PlaybackCache { self.passthroughs.retain(|(r, _)| !overlaps(*r, range)); if self.saving_enabled { let dir = self.disk_dir.clone(); - let _ = self.save_state(&std::path::Path::new(&dir)); + let _ = self.save_state(std::path::Path::new(&dir)); } } @@ -328,7 +328,7 @@ impl PlaybackCache { self.validated.insert(range); if self.saving_enabled { let dir = self.disk_dir.clone(); - let _ = self.save_state(&std::path::Path::new(&dir)); + let _ = self.save_state(std::path::Path::new(&dir)); } } @@ -401,7 +401,7 @@ impl PlaybackCache { } if self.saving_enabled { let dir = self.disk_dir.clone(); - let _ = self.save_state(&std::path::Path::new(&dir)); + let _ = self.save_state(std::path::Path::new(&dir)); } } @@ -421,7 +421,7 @@ impl PlaybackCache { } if self.saving_enabled { let dir = self.disk_dir.clone(); - let _ = self.save_state(&std::path::Path::new(&dir)); + let _ = self.save_state(std::path::Path::new(&dir)); } } diff --git a/crates/oak-render/src/eval.rs b/crates/oak-render/src/eval.rs index ed87a7a30..7d96b7b4d 100644 --- a/crates/oak-render/src/eval.rs +++ b/crates/oak-render/src/eval.rs @@ -559,7 +559,7 @@ impl RenderEvalHooks { in_flight: &mut HashSet, ) -> NodeValue { let mut payload = payload.clone(); - for (_, value) in payload.params.iter_mut() { + for value in payload.params.values_mut() { self.resolve_value(value, depth, in_flight); } match self.process_shader_job(&payload) { @@ -584,7 +584,7 @@ impl RenderEvalHooks { in_flight: &mut HashSet, ) -> Option { let mut payload = payload.clone(); - for (_, value) in payload.values.iter_mut() { + for value in payload.values.values_mut() { self.resolve_value(value, depth, in_flight); } @@ -843,7 +843,7 @@ impl RenderEvalHooks { return None; } let nested = (unsafe { oak_node::jobs::shader_job(handle) }).cloned()?; - Some(self.process_shader_job_depth(&nested, depth + 1)?) + self.process_shader_job_depth(&nested, depth + 1) }); let tex = tex?; let (token, tex_size) = match &tex { @@ -1797,7 +1797,7 @@ fn blend_transition( // Fallback: without a blend (a missing side, or a shader job that // could not run) show the side that natively covers `time` — the // outgoing block before the cut, the incoming one at or after it. - Ok(blended.or_else(|| if progress < 0.5 { from } else { to })) + Ok(blended.or(if progress < 0.5 { from } else { to })) } /// Walk an adjustment block's effect chain to its head: the first node diff --git a/crates/oak-render/src/handle.rs b/crates/oak-render/src/handle.rs index 9a81e4b3b..c5f365988 100644 --- a/crates/oak-render/src/handle.rs +++ b/crates/oak-render/src/handle.rs @@ -100,6 +100,11 @@ pub unsafe fn get(h: &CHandle) -> Option<&T> { /// `T` must be the boxed type, and the caller must guarantee the handle /// is not concurrently borrowed (the C ABI contract: a handle passed by /// value is exclusively owned for the duration of the call). +// The shared reference is the ABI input; exclusivity is the caller's +// `unsafe` contract above, so the lint's usual aliasing concern is +// discharged by the caller, not by `&mut CHandle` (which the C ABI does +// not pass). +#[allow(clippy::mut_from_ref)] pub unsafe fn get_mut(h: &CHandle) -> Option<&mut T> { if h.is_null() { return None; diff --git a/crates/oak-render/src/ipc.rs b/crates/oak-render/src/ipc.rs index 5012b6177..a508a9684 100644 --- a/crates/oak-render/src/ipc.rs +++ b/crates/oak-render/src/ipc.rs @@ -287,9 +287,11 @@ pub struct HelloCapsMsg { /// representation and are dropped by [`WireNodeValue::from_node_value`]. #[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] #[serde(tag = "t", content = "v", rename_all = "snake_case")] +#[derive(Default)] pub enum WireNodeValue { /// No value. - None, + #[default] + None, /// Integer. Int(i64), /// Float. @@ -318,11 +320,6 @@ pub enum WireNodeValue { Binary(Vec), } -impl Default for WireNodeValue { - fn default() -> Self { - WireNodeValue::None - } -} impl WireNodeValue { /// The wire form of a node value, or `None` when the variant has no @@ -1174,7 +1171,8 @@ impl FrameSlotPool { if unsafe { (*header).magic } != FRAMEPOOL_MAGIC { return FrameSlotPool::invalid(); } - let pool = unsafe { + + unsafe { FrameSlotPool { base: mem, header, @@ -1183,8 +1181,7 @@ impl FrameSlotPool { meta: mem.add((*header).meta_offset as usize) as *mut FrameSlotMeta, data: mem.add((*header).data_offset as usize), } - }; - pool + } } /// An invalid pool (attach on a non-pool segment). diff --git a/crates/oak-render/src/procpool.rs b/crates/oak-render/src/procpool.rs index f8b0c6637..59ebbb07a 100644 --- a/crates/oak-render/src/procpool.rs +++ b/crates/oak-render/src/procpool.rs @@ -1379,11 +1379,10 @@ impl ProcessDispatcher { pub fn broadcast_plugin_cancel(&self) { let mut inner = lock(&self.inner); for handle in inner.workers.iter_mut() { - if matches!(handle.state, WorkerState::Alive | WorkerState::Starting) { - if self.send_json(handle, &plugin_cancel_json()).is_err() { + if matches!(handle.state, WorkerState::Alive | WorkerState::Starting) + && self.send_json(handle, &plugin_cancel_json()).is_err() { handle.state = WorkerState::Dead; } - } } } @@ -2386,7 +2385,7 @@ impl JobDispatch for ProcessDispatcher { // Every ticket still open completes with cancellation; the map // is dropped with the dispatcher (clear for hygiene — leaked // entries pin shm region views). - for (_, pt) in inner.tickets.iter_mut() { + for pt in inner.tickets.values_mut() { if let Some(done) = pt.done.take() { fired.push((done, Err(Error::State))); } diff --git a/crates/oak-render/tests/footage_decode_test.rs b/crates/oak-render/tests/footage_decode_test.rs index b35cd7449..551041841 100644 --- a/crates/oak-render/tests/footage_decode_test.rs +++ b/crates/oak-render/tests/footage_decode_test.rs @@ -75,24 +75,27 @@ fn footage_decode_renders_known_content() { let off = y * stride + x * 16; let mut out = [0f32; 4]; for i in 0..4 { - out[i] = f32::from_le_bytes( - frame.data[off + i * 4..off + i * 4 + 4] - .try_into() - .unwrap(), - ); + out[i] = + f32::from_le_bytes(frame.data[off + i * 4..off + i * 4 + 4].try_into().unwrap()); } out }; for &(x, y) in &[(8usize, 32usize), (20, 10)] { let [r, g, b, a] = read(x, y); assert!(r > 0.5, "left half red at ({x},{y}): {r}"); - assert!(g < 0.4 && b < 0.4, "left half not red at ({x},{y}): {r},{g},{b}"); + assert!( + g < 0.4 && b < 0.4, + "left half not red at ({x},{y}): {r},{g},{b}" + ); assert!(a > 0.9, "opaque at ({x},{y}): {a}"); } for &(x, y) in &[(56usize, 32usize), (40, 50)] { let [r, g, b, a] = read(x, y); assert!(b > 0.5, "right half blue at ({x},{y}): {b}"); - assert!(r < 0.4 && g < 0.4, "right half not blue at ({x},{y}): {r},{g},{b}"); + assert!( + r < 0.4 && g < 0.4, + "right half not blue at ({x},{y}): {r},{g},{b}" + ); assert!(a > 0.9, "opaque at ({x},{y}): {a}"); } @@ -131,7 +134,6 @@ fn footage_decode_fails_explainably_for_missing_file() { (16, 16), PixelFormat::F32, ) - .err() - .expect("decode of a missing file must fail"); + .expect_err("decode of a missing file must fail"); let _ = err.code(); // explainable error, not a panic } diff --git a/crates/oak-render/tests/graph_render.rs b/crates/oak-render/tests/graph_render.rs index 5bf341b7a..8c0ac454a 100644 --- a/crates/oak-render/tests/graph_render.rs +++ b/crates/oak-render/tests/graph_render.rs @@ -40,7 +40,7 @@ mod common; /// Unique temp path per test (the process id disambiguates parallel test /// binaries; the tag separates tests inside one binary). fn clip_path(tag: &str) -> std::path::PathBuf { - std::env::temp_dir().join(format!("oakrender_graph_{tag}_{}.mp4", std::process::id())) + std::env::temp_dir().join(format!("oakrender_graph_{tag}_{}.mp4", std::process::id())) } /// These tests verify graph/decode/composite MECHANICS (stacking, scaling, @@ -49,92 +49,97 @@ fn clip_path(tag: &str) -> std::path::PathBuf { /// pixel-value assertions hold regardless of the ACEScg default. All tests /// in this binary set the same value, so the shared global is race-free. fn pin_legacy_working_space() { - oak_core::color::set_pipeline_color_settings( - oak_core::colormath::WorkingColorSpace::SrgbLegacy, - oak_core::colormath::OutputColorSpec::default(), - ); + oak_core::color::set_pipeline_color_settings( + oak_core::colormath::WorkingColorSpace::SrgbLegacy, + oak_core::colormath::OutputColorSpec::default(), + ); } /// One sequence + one video track list with one track per clip /// `(filename, [in, out))`. The LAST entry's track composites on top /// (NLE stacking: the highest-numbered track is topmost). fn build_project(clips: &[(&str, Rational, Rational)]) -> (Arc>, NodeId) { - pin_legacy_working_space(); - let project = Project::new(); - let seq; - { - let mut p = project.lock().unwrap(); - let (score, sbehavior) = SequenceBehavior::create(); - seq = p.graph.add_node(score, sbehavior); + pin_legacy_working_space(); + let project = Project::new(); + let seq; + { + let mut p = project.lock().unwrap(); + let (score, sbehavior) = SequenceBehavior::create(); + seq = p.graph.add_node(score, sbehavior); - let (tcore, tbehavior) = TrackListBehavior::create(); - let tl = p.graph.add_node(tcore, tbehavior); + let (tcore, tbehavior) = TrackListBehavior::create(); + let tl = p.graph.add_node(tcore, tbehavior); - for &(path, in_, out) in clips { - let (tcore, tbehavior) = TrackBehavior::create(); - let track = p.graph.add_node(tcore, tbehavior); + for &(path, in_, out) in clips { + let (tcore, tbehavior) = TrackBehavior::create(); + let track = p.graph.add_node(tcore, tbehavior); - let mut footage = FootageBehavior::new(path); - footage.probe().expect("probe the generated clip"); - let footage = p.graph.add_node(NodeCore::new(), Box::new(footage)); + let mut footage = FootageBehavior::new(path); + footage.probe().expect("probe the generated clip"); + let footage = p.graph.add_node(NodeCore::new(), Box::new(footage)); - let (ccore, cbehavior) = oak_node::block::clip_create(); - let clip = p.graph.add_node(ccore, cbehavior); - p.graph - .connect(footage, clip, oak_node::block::clip_input::TEXTURE_INPUT, -1) - .expect("connect footage to clip"); + let (ccore, cbehavior) = oak_node::block::clip_create(); + let clip = p.graph.add_node(ccore, cbehavior); + p.graph + .connect( + footage, + clip, + oak_node::block::clip_input::TEXTURE_INPUT, + -1, + ) + .expect("connect footage to clip"); - let clip_behavior = p - .graph - .get_mut(clip) - .unwrap() - .behavior - .as_any_mut() - .unwrap() - .downcast_mut::() - .expect("clip block"); - clip_behavior.core.range = TimeRange::new(in_, out); + let clip_behavior = p + .graph + .get_mut(clip) + .unwrap() + .behavior + .as_any_mut() + .unwrap() + .downcast_mut::() + .expect("clip block"); + clip_behavior.core.range = TimeRange::new(in_, out); - p.graph - .get_mut(track) - .unwrap() - .behavior - .as_any_mut() - .unwrap() - .downcast_mut::() - .expect("video track") - .append_block(clip); - p.graph - .get_mut(tl) - .unwrap() - .behavior - .as_any_mut() - .unwrap() - .downcast_mut::() - .expect("video track list") - .tracks - .push(track); - } + p.graph + .get_mut(track) + .unwrap() + .behavior + .as_any_mut() + .unwrap() + .downcast_mut::() + .expect("video track") + .append_block(clip); + p.graph + .get_mut(tl) + .unwrap() + .behavior + .as_any_mut() + .unwrap() + .downcast_mut::() + .expect("video track list") + .tracks + .push(track); + } - p.graph - .get_mut(seq) - .unwrap() - .behavior - .as_any_mut() - .unwrap() - .downcast_mut::() - .expect("sequence") - .track_lists - .push(tl); - } - (project, seq) + p.graph + .get_mut(seq) + .unwrap() + .behavior + .as_any_mut() + .unwrap() + .downcast_mut::() + .expect("sequence") + .track_lists + .push(tl); + } + (project, seq) } /// The raw CPU frame bytes of a rendered texture. /// The raw frame bytes of a rendered texture (GPU textures are read back /// for the assertion; the playback path itself never downloads). fn frame_data(texture: &Texture) -> Vec { - texture.to_frame().expect("graph frame readback").data + texture.to_frame().expect("graph frame readback").data } /// Two clips on two tracks, non-overlapping in time: at each request time @@ -142,67 +147,97 @@ fn frame_data(texture: &Texture) -> Vec { /// render byte for byte (same decode + same composite path). #[test] fn graph_sequence_renders_two_tracks() { - let path_a = clip_path("two_tracks_a"); - let path_b = clip_path("two_tracks_b"); - oak_codec::testmedia::write_test_clip(&path_a, 64, 64, 10, 10).expect("clip A generation"); - oak_codec::testmedia::write_test_clip(&path_b, 32, 32, 10, 10).expect("clip B generation"); + let path_a = clip_path("two_tracks_a"); + let path_b = clip_path("two_tracks_b"); + oak_codec::testmedia::write_test_clip(&path_a, 64, 64, 10, 10).expect("clip A generation"); + oak_codec::testmedia::write_test_clip(&path_b, 32, 32, 10, 10).expect("clip B generation"); - let (project, seq) = build_project(&[ - (&path_a.to_string_lossy(), Rational::new(0, 1), Rational::new(1, 1)), - (&path_b.to_string_lossy(), Rational::new(1, 1), Rational::new(2, 1)), - ]); + let (project, seq) = build_project(&[ + ( + &path_a.to_string_lossy(), + Rational::new(0, 1), + Rational::new(1, 1), + ), + ( + &path_b.to_string_lossy(), + Rational::new(1, 1), + Rational::new(2, 1), + ), + ]); - let t05 = oak_render::eval::render_graph_frame( - &project, - seq, - Rational::new(1, 2), - (64, 64), - PixelFormat::F32, - ) - .expect("render t=0.5"); - let t15 = oak_render::eval::render_graph_frame( - &project, - seq, - Rational::new(3, 2), - (64, 64), - PixelFormat::F32, - ) - .expect("render t=1.5"); - assert_eq!(t05.size(), (64, 64)); - assert_eq!(t15.size(), (64, 64)); + let t05 = oak_render::eval::render_graph_frame( + &project, + seq, + Rational::new(1, 2), + (64, 64), + PixelFormat::F32, + ) + .expect("render t=0.5"); + let t15 = oak_render::eval::render_graph_frame( + &project, + seq, + Rational::new(3, 2), + (64, 64), + PixelFormat::F32, + ) + .expect("render t=1.5"); + assert_eq!(t05.size(), (64, 64)); + assert_eq!(t15.size(), (64, 64)); - // Solo renders of each clip for byte comparison. - let (solo_a, seq_a) = build_project(&[(&path_a.to_string_lossy(), Rational::new(0, 1), Rational::new(1, 1))]); - let solo_a_tex = oak_render::eval::render_graph_frame( - &solo_a, - seq_a, - Rational::new(1, 2), - (64, 64), - PixelFormat::F32, - ) - .expect("solo A render"); - let (solo_b, seq_b) = build_project(&[(&path_b.to_string_lossy(), Rational::new(1, 1), Rational::new(2, 1))]); - let solo_b_tex = oak_render::eval::render_graph_frame( - &solo_b, - seq_b, - Rational::new(3, 2), - (64, 64), - PixelFormat::F32, - ) - .expect("solo B render"); + // Solo renders of each clip for byte comparison. + let (solo_a, seq_a) = build_project(&[( + &path_a.to_string_lossy(), + Rational::new(0, 1), + Rational::new(1, 1), + )]); + let solo_a_tex = oak_render::eval::render_graph_frame( + &solo_a, + seq_a, + Rational::new(1, 2), + (64, 64), + PixelFormat::F32, + ) + .expect("solo A render"); + let (solo_b, seq_b) = build_project(&[( + &path_b.to_string_lossy(), + Rational::new(1, 1), + Rational::new(2, 1), + )]); + let solo_b_tex = oak_render::eval::render_graph_frame( + &solo_b, + seq_b, + Rational::new(3, 2), + (64, 64), + PixelFormat::F32, + ) + .expect("solo B render"); - // Each time picks exactly the clip covering it, unchanged by the other - // track (B is 32x32 and must scale up to the 64x64 target). - assert_eq!(frame_data(&t05), frame_data(&solo_a_tex), "t=0.5 renders clip A"); - assert_eq!(frame_data(&t15), frame_data(&solo_b_tex), "t=1.5 renders clip B"); - assert_ne!(frame_data(&t05), frame_data(&t15), "the two clips differ"); + // Each time picks exactly the clip covering it, unchanged by the other + // track (B is 32x32 and must scale up to the 64x64 target). + assert_eq!( + frame_data(&t05), + frame_data(&solo_a_tex), + "t=0.5 renders clip A" + ); + assert_eq!( + frame_data(&t15), + frame_data(&solo_b_tex), + "t=1.5 renders clip B" + ); + assert_ne!(frame_data(&t05), frame_data(&t15), "the two clips differ"); - // Both frames carry real content. - assert!(frame_data(&t05).iter().any(|&b| b != 0), "t=0.5 is not black"); - assert!(frame_data(&t15).iter().any(|&b| b != 0), "t=1.5 is not black"); + // Both frames carry real content. + assert!( + frame_data(&t05).iter().any(|&b| b != 0), + "t=0.5 is not black" + ); + assert!( + frame_data(&t15).iter().any(|&b| b != 0), + "t=1.5 is not black" + ); - let _ = std::fs::remove_file(&path_a); - let _ = std::fs::remove_file(&path_b); + let _ = std::fs::remove_file(&path_a); + let _ = std::fs::remove_file(&path_b); } /// NLE stacking regression: two OPAQUE solid-color clips covering the @@ -211,65 +246,104 @@ fn graph_sequence_renders_two_tracks() { /// the timeline UI (the highest-numbered track displays on top). #[test] fn graph_sequence_stacks_highest_track_on_top() { - let red = clip_path("stack_red"); - let blue = clip_path("stack_blue"); - oak_codec::testmedia::write_test_clip_solid(&red, 64, 64, 10, 10, [0.9, 0.1, 0.1, 1.0]) - .expect("red clip generation"); - oak_codec::testmedia::write_test_clip_solid(&blue, 64, 64, 10, 10, [0.1, 0.1, 0.9, 1.0]) - .expect("blue clip generation"); + let red = clip_path("stack_red"); + let blue = clip_path("stack_blue"); + oak_codec::testmedia::write_test_clip_solid(&red, 64, 64, 10, 10, [0.9, 0.1, 0.1, 1.0]) + .expect("red clip generation"); + oak_codec::testmedia::write_test_clip_solid(&blue, 64, 64, 10, 10, [0.1, 0.1, 0.9, 1.0]) + .expect("blue clip generation"); - // V1 = red (bottom), V2 = blue (top). - let (project, seq) = build_project(&[ - (&red.to_string_lossy(), Rational::new(0, 1), Rational::new(1, 1)), - (&blue.to_string_lossy(), Rational::new(0, 1), Rational::new(1, 1)), - ]); - let tex = oak_render::eval::render_graph_frame(&project, seq, Rational::new(0, 1), (64, 64), PixelFormat::F32) - .expect("stacked render"); - let data = frame_data(&tex); - assert!( - channel(&data, 8, 8, 2) > 0.5 && channel(&data, 8, 8, 0) < 0.4, - "V2's blue covers V1's red (r={}, b={})", - channel(&data, 8, 8, 0), - channel(&data, 8, 8, 2) - ); + // V1 = red (bottom), V2 = blue (top). + let (project, seq) = build_project(&[ + ( + &red.to_string_lossy(), + Rational::new(0, 1), + Rational::new(1, 1), + ), + ( + &blue.to_string_lossy(), + Rational::new(0, 1), + Rational::new(1, 1), + ), + ]); + let tex = oak_render::eval::render_graph_frame( + &project, + seq, + Rational::new(0, 1), + (64, 64), + PixelFormat::F32, + ) + .expect("stacked render"); + let data = frame_data(&tex); + assert!( + channel(&data, 8, 8, 2) > 0.5 && channel(&data, 8, 8, 0) < 0.4, + "V2's blue covers V1's red (r={}, b={})", + channel(&data, 8, 8, 0), + channel(&data, 8, 8, 2) + ); - // Distinguishability guard: solo, the V1 clip really is red (the two - // tracks carry different content). - let (solo, solo_seq) = build_project(&[(&red.to_string_lossy(), Rational::new(0, 1), Rational::new(1, 1))]); - let solo_tex = oak_render::eval::render_graph_frame(&solo, solo_seq, Rational::new(0, 1), (64, 64), PixelFormat::F32) - .expect("solo V1 render"); - let solo_data = frame_data(&solo_tex); - assert!( - channel(&solo_data, 8, 8, 0) > 0.5 && channel(&solo_data, 8, 8, 2) < 0.4, - "solo V1 is red (r={}, b={})", - channel(&solo_data, 8, 8, 0), - channel(&solo_data, 8, 8, 2) - ); + // Distinguishability guard: solo, the V1 clip really is red (the two + // tracks carry different content). + let (solo, solo_seq) = build_project(&[( + &red.to_string_lossy(), + Rational::new(0, 1), + Rational::new(1, 1), + )]); + let solo_tex = oak_render::eval::render_graph_frame( + &solo, + solo_seq, + Rational::new(0, 1), + (64, 64), + PixelFormat::F32, + ) + .expect("solo V1 render"); + let solo_data = frame_data(&solo_tex); + assert!( + channel(&solo_data, 8, 8, 0) > 0.5 && channel(&solo_data, 8, 8, 2) < 0.4, + "solo V1 is red (r={}, b={})", + channel(&solo_data, 8, 8, 0), + channel(&solo_data, 8, 8, 2) + ); - let _ = std::fs::remove_file(&red); - let _ = std::fs::remove_file(&blue); + let _ = std::fs::remove_file(&red); + let _ = std::fs::remove_file(&blue); } /// The driver rejects bad arguments explainably: non-F32 format, a /// non-positive size, and a missing viewer. #[test] fn graph_render_rejects_bad_inputs() { - let (project, seq) = build_project(&[]); + let (project, seq) = build_project(&[]); - let err = oak_render::eval::render_graph_frame(&project, seq, Rational::new(0, 1), (64, 64), PixelFormat::U8) - .err() - .expect("non-F32 format rejected"); - assert_eq!(err.code(), oak_render::error::Error::Invalid.code()); + let err = oak_render::eval::render_graph_frame( + &project, + seq, + Rational::new(0, 1), + (64, 64), + PixelFormat::U8, + ) + .expect_err("non-F32 format rejected"); + assert_eq!(err.code(), oak_render::error::Error::Invalid.code()); - let err = oak_render::eval::render_graph_frame(&project, seq, Rational::new(0, 1), (0, 64), PixelFormat::F32) - .err() - .expect("non-positive size rejected"); - assert_eq!(err.code(), oak_render::error::Error::Invalid.code()); + let err = oak_render::eval::render_graph_frame( + &project, + seq, + Rational::new(0, 1), + (0, 64), + PixelFormat::F32, + ) + .expect_err("non-positive size rejected"); + assert_eq!(err.code(), oak_render::error::Error::Invalid.code()); - let err = oak_render::eval::render_graph_frame(&project, NodeId::INVALID, Rational::new(0, 1), (64, 64), PixelFormat::F32) - .err() - .expect("missing viewer rejected"); - assert_eq!(err.code(), oak_render::error::Error::NotFound.code()); + let err = oak_render::eval::render_graph_frame( + &project, + NodeId::INVALID, + Rational::new(0, 1), + (64, 64), + PixelFormat::F32, + ) + .expect_err("missing viewer rejected"); + assert_eq!(err.code(), oak_render::error::Error::NotFound.code()); } /// One sequence + one track with a single clip, with an effect node @@ -278,83 +352,88 @@ fn graph_render_rejects_bad_inputs() { /// the graph, and returns the effect node id. The clip keeps the /// `(in, out)` range from `clip`. fn build_effect_project( - clip: (&str, Rational, Rational), - insert_effect: impl FnOnce(&mut Project, NodeId, NodeId) -> NodeId, + clip: (&str, Rational, Rational), + insert_effect: impl FnOnce(&mut Project, NodeId, NodeId) -> NodeId, ) -> (Arc>, NodeId) { - pin_legacy_working_space(); - let project = Project::new(); - let seq; - { - let mut p = project.lock().unwrap(); - let (score, sbehavior) = SequenceBehavior::create(); - seq = p.graph.add_node(score, sbehavior); + pin_legacy_working_space(); + let project = Project::new(); + let seq; + { + let mut p = project.lock().unwrap(); + let (score, sbehavior) = SequenceBehavior::create(); + seq = p.graph.add_node(score, sbehavior); - let (tcore, tbehavior) = TrackListBehavior::create(); - let tl = p.graph.add_node(tcore, tbehavior); + let (tcore, tbehavior) = TrackListBehavior::create(); + let tl = p.graph.add_node(tcore, tbehavior); - let (tcore, tbehavior) = TrackBehavior::create(); - let track = p.graph.add_node(tcore, tbehavior); + let (tcore, tbehavior) = TrackBehavior::create(); + let track = p.graph.add_node(tcore, tbehavior); - let mut footage = FootageBehavior::new(clip.0); - footage.probe().expect("probe the generated clip"); - let footage = p.graph.add_node(NodeCore::new(), Box::new(footage)); + let mut footage = FootageBehavior::new(clip.0); + footage.probe().expect("probe the generated clip"); + let footage = p.graph.add_node(NodeCore::new(), Box::new(footage)); - let (ccore, cbehavior) = oak_node::block::clip_create(); - let clip_node = p.graph.add_node(ccore, cbehavior); - p.graph - .connect(footage, clip_node, oak_node::block::clip_input::TEXTURE_INPUT, -1) - .expect("connect footage to clip"); + let (ccore, cbehavior) = oak_node::block::clip_create(); + let clip_node = p.graph.add_node(ccore, cbehavior); + p.graph + .connect( + footage, + clip_node, + oak_node::block::clip_input::TEXTURE_INPUT, + -1, + ) + .expect("connect footage to clip"); - let clip_behavior = p - .graph - .get_mut(clip_node) - .unwrap() - .behavior - .as_any_mut() - .unwrap() - .downcast_mut::() - .expect("clip block"); - clip_behavior.core.range = TimeRange::new(clip.1, clip.2); + let clip_behavior = p + .graph + .get_mut(clip_node) + .unwrap() + .behavior + .as_any_mut() + .unwrap() + .downcast_mut::() + .expect("clip block"); + clip_behavior.core.range = TimeRange::new(clip.1, clip.2); - let _effect = insert_effect(&mut p, footage, clip_node); + let _effect = insert_effect(&mut p, footage, clip_node); - p.graph - .get_mut(track) - .unwrap() - .behavior - .as_any_mut() - .unwrap() - .downcast_mut::() - .expect("video track") - .append_block(clip_node); - p.graph - .get_mut(tl) - .unwrap() - .behavior - .as_any_mut() - .unwrap() - .downcast_mut::() - .expect("video track list") - .tracks - .push(track); - p.graph - .get_mut(seq) - .unwrap() - .behavior - .as_any_mut() - .unwrap() - .downcast_mut::() - .expect("sequence") - .track_lists - .push(tl); - } - (project, seq) + p.graph + .get_mut(track) + .unwrap() + .behavior + .as_any_mut() + .unwrap() + .downcast_mut::() + .expect("video track") + .append_block(clip_node); + p.graph + .get_mut(tl) + .unwrap() + .behavior + .as_any_mut() + .unwrap() + .downcast_mut::() + .expect("video track list") + .tracks + .push(track); + p.graph + .get_mut(seq) + .unwrap() + .behavior + .as_any_mut() + .unwrap() + .downcast_mut::() + .expect("sequence") + .track_lists + .push(tl); + } + (project, seq) } /// The F32 RGBA channel of a 64x64 frame at `(x, y)`. fn channel(data: &[u8], x: usize, y: usize, c: usize) -> f32 { - let off = (y * 64 + x) * 16 + c * 4; - f32::from_le_bytes(data[off..off + 4].try_into().unwrap()) + let off = (y * 64 + x) * 16 + c * 4; + f32::from_le_bytes(data[off..off + 4].try_into().unwrap()) } /// M12 phase 3a: an opacity shader job (scalar 0.5) pushed by the effect @@ -365,82 +444,91 @@ fn channel(data: &[u8], x: usize, y: usize, c: usize) -> f32 { /// when no GPU adapter exists. #[test] fn shader_job_opacity_halves_pixels() { - if oak_core::backend::shared_gpu_or_skip("shader_job_opacity_halves_pixels").is_none() { - return; - } - let path = clip_path("opacity_job"); - oak_codec::testmedia::write_test_clip(&path, 64, 64, 10, 10).expect("clip generation"); + if oak_core::backend::shared_gpu_or_skip("shader_job_opacity_halves_pixels").is_none() { + return; + } + let path = clip_path("opacity_job"); + oak_codec::testmedia::write_test_clip(&path, 64, 64, 10, 10).expect("clip generation"); - let (plain_project, plain_seq) = build_project(&[( - &path.to_string_lossy(), - Rational::new(0, 1), - Rational::new(1, 1), - )]); - let plain_tex = oak_render::eval::render_graph_frame( - &plain_project, - plain_seq, - Rational::new(0, 1), - (64, 64), - PixelFormat::F32, - ) - .expect("plain render"); - let plain = frame_data(&plain_tex).to_vec(); + let (plain_project, plain_seq) = build_project(&[( + &path.to_string_lossy(), + Rational::new(0, 1), + Rational::new(1, 1), + )]); + let plain_tex = oak_render::eval::render_graph_frame( + &plain_project, + plain_seq, + Rational::new(0, 1), + (64, 64), + PixelFormat::F32, + ) + .expect("plain render"); + let plain = frame_data(&plain_tex).to_vec(); - let (effect_project, effect_seq) = build_effect_project( - (&path.to_string_lossy(), Rational::new(0, 1), Rational::new(1, 1)), - |p, footage, clip| { - let (ecore, ebehavior) = oak_node::nodes::opacity::create(); - let effect = p.graph.add_node(ecore, ebehavior); - p.graph.disconnect(footage, clip, oak_node::block::clip_input::TEXTURE_INPUT, -1); - p.graph - .connect(footage, effect, oak_node::nodes::opacity::TEXTURE_INPUT, -1) - .expect("connect footage to effect"); - p.graph - .connect(effect, clip, oak_node::block::clip_input::TEXTURE_INPUT, -1) - .expect("connect effect to clip"); - p.graph - .get_mut(effect) - .unwrap() - .core - .set_standard_value( - oak_node::nodes::opacity::VALUE_INPUT, - -1, - oak_node::value::NodeValue::Float(0.5), - ); - effect - }, - ); - let effect_tex = oak_render::eval::render_graph_frame( - &effect_project, - effect_seq, - Rational::new(0, 1), - (64, 64), - PixelFormat::F32, - ) - .expect("opacity render"); - let blurred = frame_data(&effect_tex).to_vec(); + let (effect_project, effect_seq) = build_effect_project( + ( + &path.to_string_lossy(), + Rational::new(0, 1), + Rational::new(1, 1), + ), + |p, footage, clip| { + let (ecore, ebehavior) = oak_node::nodes::opacity::create(); + let effect = p.graph.add_node(ecore, ebehavior); + p.graph.disconnect( + footage, + clip, + oak_node::block::clip_input::TEXTURE_INPUT, + -1, + ); + p.graph + .connect(footage, effect, oak_node::nodes::opacity::TEXTURE_INPUT, -1) + .expect("connect footage to effect"); + p.graph + .connect(effect, clip, oak_node::block::clip_input::TEXTURE_INPUT, -1) + .expect("connect effect to clip"); + p.graph.get_mut(effect).unwrap().core.set_standard_value( + oak_node::nodes::opacity::VALUE_INPUT, + -1, + oak_node::value::NodeValue::Float(0.5), + ); + effect + }, + ); + let effect_tex = oak_render::eval::render_graph_frame( + &effect_project, + effect_seq, + Rational::new(0, 1), + (64, 64), + PixelFormat::F32, + ) + .expect("opacity render"); + let blurred = frame_data(&effect_tex).to_vec(); - // Sample away from the x=32 half boundary (MPEG-2 chroma bleed and - // luma ringing stay within a few pixels of it). - let mut ratios: Vec = Vec::new(); - for y in 4..60 { - for x in (4..24).chain(40..60) { - for c in 0..3 { - let a = channel(&plain, x, y, c); - if a > 0.02 { - ratios.push(channel(&blurred, x, y, c) / a); - } - } - } - } - assert!(ratios.len() >= 512, "too few comparable samples: {}", ratios.len()); - let mean = ratios.iter().sum::() / ratios.len() as f32; - assert!( - (mean - 0.25).abs() < 0.02, - "opacity channel ratio {mean} is not 0.25" - ); + // Sample away from the x=32 half boundary (MPEG-2 chroma bleed and + // luma ringing stay within a few pixels of it). + let mut ratios: Vec = Vec::new(); + for y in 4..60 { + for x in (4..24).chain(40..60) { + for c in 0..3 { + let a = channel(&plain, x, y, c); + if a > 0.02 { + ratios.push(channel(&blurred, x, y, c) / a); + } + } + } + } + assert!( + ratios.len() >= 512, + "too few comparable samples: {}", + ratios.len() + ); + let mean = ratios.iter().sum::() / ratios.len() as f32; + assert!( + (mean - 0.25).abs() < 0.02, + "opacity channel ratio {mean} is not 0.25" + ); - let _ = std::fs::remove_file(&path); + let _ = std::fs::remove_file(&path); } /// M12 phase 3a: a box-blur shader job (radius 2, both axes) is resolved @@ -450,94 +538,103 @@ fn shader_job_opacity_halves_pixels() { /// boundary pixel on the right half. Skipped when no GPU adapter exists. #[test] fn shader_job_blur_smooths_edge() { - if oak_core::backend::shared_gpu_or_skip("shader_job_blur_smooths_edge").is_none() { - return; - } - let path = clip_path("blur_job"); - oak_codec::testmedia::write_test_clip(&path, 64, 64, 10, 10).expect("clip generation"); + if oak_core::backend::shared_gpu_or_skip("shader_job_blur_smooths_edge").is_none() { + return; + } + let path = clip_path("blur_job"); + oak_codec::testmedia::write_test_clip(&path, 64, 64, 10, 10).expect("clip generation"); - let (plain_project, plain_seq) = build_project(&[( - &path.to_string_lossy(), - Rational::new(0, 1), - Rational::new(1, 1), - )]); - let plain_tex = oak_render::eval::render_graph_frame( - &plain_project, - plain_seq, - Rational::new(0, 1), - (64, 64), - PixelFormat::F32, - ) - .expect("plain render"); - let plain = frame_data(&plain_tex).to_vec(); + let (plain_project, plain_seq) = build_project(&[( + &path.to_string_lossy(), + Rational::new(0, 1), + Rational::new(1, 1), + )]); + let plain_tex = oak_render::eval::render_graph_frame( + &plain_project, + plain_seq, + Rational::new(0, 1), + (64, 64), + PixelFormat::F32, + ) + .expect("plain render"); + let plain = frame_data(&plain_tex).to_vec(); - let (effect_project, effect_seq) = build_effect_project( - (&path.to_string_lossy(), Rational::new(0, 1), Rational::new(1, 1)), - |p, footage, clip| { - let (ecore, ebehavior) = oak_node::nodes::blur::create(); - let effect = p.graph.add_node(ecore, ebehavior); - p.graph.disconnect(footage, clip, oak_node::block::clip_input::TEXTURE_INPUT, -1); - p.graph - .connect(footage, effect, oak_node::nodes::blur::TEXTURE_INPUT, -1) - .expect("connect footage to effect"); - p.graph - .connect(effect, clip, oak_node::block::clip_input::TEXTURE_INPUT, -1) - .expect("connect effect to clip"); - let core = &mut p.graph.get_mut(effect).unwrap().core; - core.set_standard_value( - oak_node::nodes::blur::METHOD_INPUT, - -1, - oak_node::value::NodeValue::Combo(0), - ); - core.set_standard_value( - oak_node::nodes::blur::RADIUS_INPUT, - -1, - oak_node::value::NodeValue::Float(2.0), - ); - core.set_standard_value( - oak_node::nodes::blur::HORIZ_INPUT, - -1, - oak_node::value::NodeValue::Boolean(true), - ); - core.set_standard_value( - oak_node::nodes::blur::VERT_INPUT, - -1, - oak_node::value::NodeValue::Boolean(true), - ); - effect - }, - ); - let effect_tex = oak_render::eval::render_graph_frame( - &effect_project, - effect_seq, - Rational::new(0, 1), - (64, 64), - PixelFormat::F32, - ) - .expect("blur render"); - let blurred = frame_data(&effect_tex).to_vec(); + let (effect_project, effect_seq) = build_effect_project( + ( + &path.to_string_lossy(), + Rational::new(0, 1), + Rational::new(1, 1), + ), + |p, footage, clip| { + let (ecore, ebehavior) = oak_node::nodes::blur::create(); + let effect = p.graph.add_node(ecore, ebehavior); + p.graph.disconnect( + footage, + clip, + oak_node::block::clip_input::TEXTURE_INPUT, + -1, + ); + p.graph + .connect(footage, effect, oak_node::nodes::blur::TEXTURE_INPUT, -1) + .expect("connect footage to effect"); + p.graph + .connect(effect, clip, oak_node::block::clip_input::TEXTURE_INPUT, -1) + .expect("connect effect to clip"); + let core = &mut p.graph.get_mut(effect).unwrap().core; + core.set_standard_value( + oak_node::nodes::blur::METHOD_INPUT, + -1, + oak_node::value::NodeValue::Combo(0), + ); + core.set_standard_value( + oak_node::nodes::blur::RADIUS_INPUT, + -1, + oak_node::value::NodeValue::Float(2.0), + ); + core.set_standard_value( + oak_node::nodes::blur::HORIZ_INPUT, + -1, + oak_node::value::NodeValue::Boolean(true), + ); + core.set_standard_value( + oak_node::nodes::blur::VERT_INPUT, + -1, + oak_node::value::NodeValue::Boolean(true), + ); + effect + }, + ); + let effect_tex = oak_render::eval::render_graph_frame( + &effect_project, + effect_seq, + Rational::new(0, 1), + (64, 64), + PixelFormat::F32, + ) + .expect("blur render"); + let blurred = frame_data(&effect_tex).to_vec(); - // The blur must actually change pixels (a silently dropped job would - // fall back to the pass-through input and byte-match the plain frame). - assert_ne!(plain, blurred, "the blur job must actually change pixels"); + // The blur must actually change pixels (a silently dropped job would + // fall back to the pass-through input and byte-match the plain frame). + assert_ne!(plain, blurred, "the blur job must actually change pixels"); - // Row y=32 (vertically uniform): the boundary step x=31 -> x=32 must - // shrink, and the right-side boundary pixel picks up left-half content. - let r = |data: &[u8], x: usize| channel(data, x, 32, 0); - let plain_step = (r(&plain, 32) - r(&plain, 31)).abs(); - let blurred_step = (r(&blurred, 32) - r(&blurred, 31)).abs(); - assert!( - blurred_step < plain_step, - "boundary step {blurred_step} not below the plain {plain_step}" - ); - assert!( - r(&blurred, 32) > r(&plain, 32), - "blurred boundary pixel {} not above the plain {}", - r(&blurred, 32), - r(&plain, 32) - ); + // Row y=32 (vertically uniform): the boundary step x=31 -> x=32 must + // shrink, and the right-side boundary pixel picks up left-half content. + let r = |data: &[u8], x: usize| channel(data, x, 32, 0); + let plain_step = (r(&plain, 32) - r(&plain, 31)).abs(); + let blurred_step = (r(&blurred, 32) - r(&blurred, 31)).abs(); + assert!( + blurred_step < plain_step, + "boundary step {blurred_step} not below the plain {plain_step}" + ); + assert!( + r(&blurred, 32) > r(&plain, 32), + "blurred boundary pixel {} not above the plain {}", + r(&blurred, 32), + r(&plain, 32) + ); - let _ = std::fs::remove_file(&path); + let _ = std::fs::remove_file(&path); } /// Chroma Key on a green clip: the keying node's job payload flows through @@ -551,90 +648,99 @@ fn shader_job_blur_smooths_edge() { /// Skipped when no GPU adapter or OCIO config exists. #[test] fn chromakey_job_keys_green_with_ociobased_stub() { - if oak_core::backend::shared_gpu_or_skip("chromakey_job_keys_green_with_ociobased_stub").is_none() - { - return; - } - if oak_core::color::set_up_default_config().is_err() { - eprintln!("skipping chromakey_job_keys_green_with_ociobased_stub: no OCIO config"); - return; - } + if oak_core::backend::shared_gpu_or_skip("chromakey_job_keys_green_with_ociobased_stub") + .is_none() + { + return; + } + if oak_core::color::set_up_default_config().is_err() { + eprintln!("skipping chromakey_job_keys_green_with_ociobased_stub: no OCIO config"); + return; + } - let path = clip_path("chromakey_job"); - oak_codec::testmedia::write_test_clip_solid(&path, 64, 64, 10, 10, [0.0, 1.0, 0.0, 1.0]) - .expect("green clip generation"); + let path = clip_path("chromakey_job"); + oak_codec::testmedia::write_test_clip_solid(&path, 64, 64, 10, 10, [0.0, 1.0, 0.0, 1.0]) + .expect("green clip generation"); - // The chromakey built from the factory (the same `create()` the - // inspector's effect stack uses), defaulting to the green key color. - let render = |key: [f64; 4]| { - let (project, seq) = build_effect_project( - (&path.to_string_lossy(), Rational::new(0, 1), Rational::new(1, 1)), - |p, footage, clip| { - let (ecore, ebehavior) = - oak_node::factory::Factory::global() - .create_any("org.olivevideoeditor.Olive.chromakey") - .expect("chromakey factory entry"); - let effect = p.graph.add_node(ecore, ebehavior); - p.graph - .disconnect(footage, clip, oak_node::block::clip_input::TEXTURE_INPUT, -1); - p.graph - .connect(footage, effect, "tex_in", -1) - .expect("connect footage to chromakey"); - p.graph - .connect(effect, clip, oak_node::block::clip_input::TEXTURE_INPUT, -1) - .expect("connect chromakey to clip"); - p.graph - .get_mut(effect) - .unwrap() - .core - .set_standard_value("color_key", -1, oak_node::value::NodeValue::Color(key)); - effect - }, - ); - let tex = oak_render::eval::render_graph_frame( - &project, - seq, - Rational::new(0, 1), - (64, 64), - PixelFormat::F32, - ) - .expect("chromakey render"); - frame_data(&tex).to_vec() - }; + // The chromakey built from the factory (the same `create()` the + // inspector's effect stack uses), defaulting to the green key color. + let render = |key: [f64; 4]| { + let (project, seq) = build_effect_project( + ( + &path.to_string_lossy(), + Rational::new(0, 1), + Rational::new(1, 1), + ), + |p, footage, clip| { + let (ecore, ebehavior) = oak_node::factory::Factory::global() + .create_any("org.olivevideoeditor.Olive.chromakey") + .expect("chromakey factory entry"); + let effect = p.graph.add_node(ecore, ebehavior); + p.graph.disconnect( + footage, + clip, + oak_node::block::clip_input::TEXTURE_INPUT, + -1, + ); + p.graph + .connect(footage, effect, "tex_in", -1) + .expect("connect footage to chromakey"); + p.graph + .connect(effect, clip, oak_node::block::clip_input::TEXTURE_INPUT, -1) + .expect("connect chromakey to clip"); + p.graph.get_mut(effect).unwrap().core.set_standard_value( + "color_key", + -1, + oak_node::value::NodeValue::Color(key), + ); + effect + }, + ); + let tex = oak_render::eval::render_graph_frame( + &project, + seq, + Rational::new(0, 1), + (64, 64), + PixelFormat::F32, + ) + .expect("chromakey render"); + frame_data(&tex).to_vec() + }; - // Sum the RGB channels over a 48x48 center crop (MPEG-2 chroma bleed - // stays near the edges); a keyed-out frame contributes nothing. - let energy = |frame: &[u8]| -> f32 { - let mut total = 0.0f32; - for y in 8..56 { - for x in 8..56 { - let off = (y * 64 + x) * 16; - for c in 0..3 { - total += f32::from_le_bytes(frame[off + c * 4..off + c * 4 + 4].try_into().unwrap()); - } - } - } - total - }; + // Sum the RGB channels over a 48x48 center crop (MPEG-2 chroma bleed + // stays near the edges); a keyed-out frame contributes nothing. + let energy = |frame: &[u8]| -> f32 { + let mut total = 0.0f32; + for y in 8..56 { + for x in 8..56 { + let off = (y * 64 + x) * 16; + for c in 0..3 { + total += + f32::from_le_bytes(frame[off + c * 4..off + c * 4 + 4].try_into().unwrap()); + } + } + } + total + }; - // Green key on the green clip: fully keyed (transparent black). - let green_keyed = render([0.0, 1.0, 0.0, 1.0]); - assert!( - energy(&green_keyed) < 1.0, - "green frame against the green key must key out (energy {})", - energy(&green_keyed) - ); + // Green key on the green clip: fully keyed (transparent black). + let green_keyed = render([0.0, 1.0, 0.0, 1.0]); + assert!( + energy(&green_keyed) < 1.0, + "green frame against the green key must key out (energy {})", + energy(&green_keyed) + ); - // Red key on the green clip: far from the key color, mask ~1, the - // frame stays opaque — the key color parametrizes the result. - let red_keyed = render([1.0, 0.0, 0.0, 1.0]); - assert!( - energy(&red_keyed) > 10.0, - "green frame against the red key must stay (energy {})", - energy(&red_keyed) - ); + // Red key on the green clip: far from the key color, mask ~1, the + // frame stays opaque — the key color parametrizes the result. + let red_keyed = render([1.0, 0.0, 0.0, 1.0]); + assert!( + energy(&red_keyed) > 10.0, + "green frame against the red key must stay (energy {})", + energy(&red_keyed) + ); - let _ = std::fs::remove_file(&path); + let _ = std::fs::remove_file(&path); } /// Generator sizing is anchored to the SEQUENCE resolution (C++ inserts @@ -647,74 +753,84 @@ fn chromakey_job_keys_green_with_ociobased_stub() { /// target-anchored behavior drew 50% vs 12.5%). #[test] fn shape_generator_size_is_sequence_relative() { - if oak_core::backend::shared_gpu_or_skip("shape_generator_size_is_sequence_relative").is_none() { - return; - } - let path = clip_path("shape_seqrel"); - oak_codec::testmedia::write_test_clip_solid(&path, 64, 64, 10, 10, [0.0, 0.0, 1.0, 1.0]) - .expect("blue clip generation"); + if oak_core::backend::shared_gpu_or_skip("shape_generator_size_is_sequence_relative").is_none() + { + return; + } + let path = clip_path("shape_seqrel"); + oak_codec::testmedia::write_test_clip_solid(&path, 64, 64, 10, 10, [0.0, 0.0, 1.0, 1.0]) + .expect("blue clip generation"); - let (project, seq) = build_effect_project( - (&path.to_string_lossy(), Rational::new(0, 1), Rational::new(1, 1)), - |p, footage, clip| { - let (ecore, ebehavior) = oak_node::factory::Factory::global() - .create_any("org.olivevideoeditor.Olive.shape") - .expect("shape factory entry"); - let effect = p.graph.add_node(ecore, ebehavior); - p.graph.disconnect(footage, clip, oak_node::block::clip_input::TEXTURE_INPUT, -1); - // The shape module is crate-private; the input ids are the C++ - // kBaseInput/kSizeInput strings. - p.graph - .connect(footage, effect, "base_in", -1) - .expect("connect footage to shape base"); - p.graph - .connect(effect, clip, oak_node::block::clip_input::TEXTURE_INPUT, -1) - .expect("connect shape to clip"); - p.graph.get_mut(effect).unwrap().core.set_standard_value( - "size_in", - -1, - oak_node::value::NodeValue::Vec2([96.0, 96.0]), - ); - effect - }, - ); + let (project, seq) = build_effect_project( + ( + &path.to_string_lossy(), + Rational::new(0, 1), + Rational::new(1, 1), + ), + |p, footage, clip| { + let (ecore, ebehavior) = oak_node::factory::Factory::global() + .create_any("org.olivevideoeditor.Olive.shape") + .expect("shape factory entry"); + let effect = p.graph.add_node(ecore, ebehavior); + p.graph.disconnect( + footage, + clip, + oak_node::block::clip_input::TEXTURE_INPUT, + -1, + ); + // The shape module is crate-private; the input ids are the C++ + // kBaseInput/kSizeInput strings. + p.graph + .connect(footage, effect, "base_in", -1) + .expect("connect footage to shape base"); + p.graph + .connect(effect, clip, oak_node::block::clip_input::TEXTURE_INPUT, -1) + .expect("connect shape to clip"); + p.graph.get_mut(effect).unwrap().core.set_standard_value( + "size_in", + -1, + oak_node::value::NodeValue::Vec2([96.0, 96.0]), + ); + effect + }, + ); - // The fraction of the middle row covered by the opaque red square. - let red_fraction = |width: i32, height: i32| -> f32 { - let tex = oak_render::eval::render_graph_frame( - &project, - seq, - Rational::new(0, 1), - (width, height), - PixelFormat::F32, - ) - .expect("shape render"); - let frame = tex.to_frame().expect("shape frame readback"); - let stride = frame.linesize_bytes() as usize; - let y = (height / 2) as usize; - let mut red = 0usize; - for x in 0..width as usize { - let off = y * stride + x * 16; - let r = f32::from_le_bytes(frame.data[off..off + 4].try_into().unwrap()); - let g = f32::from_le_bytes(frame.data[off + 4..off + 8].try_into().unwrap()); - let a = f32::from_le_bytes(frame.data[off + 12..off + 16].try_into().unwrap()); - if r > 0.9 && g < 0.1 && a > 0.9 { - red += 1; - } - } - red as f32 / width as f32 - }; + // The fraction of the middle row covered by the opaque red square. + let red_fraction = |width: i32, height: i32| -> f32 { + let tex = oak_render::eval::render_graph_frame( + &project, + seq, + Rational::new(0, 1), + (width, height), + PixelFormat::F32, + ) + .expect("shape render"); + let frame = tex.to_frame().expect("shape frame readback"); + let stride = frame.linesize_bytes() as usize; + let y = (height / 2) as usize; + let mut red = 0usize; + for x in 0..width as usize { + let off = y * stride + x * 16; + let r = f32::from_le_bytes(frame.data[off..off + 4].try_into().unwrap()); + let g = f32::from_le_bytes(frame.data[off + 4..off + 8].try_into().unwrap()); + let a = f32::from_le_bytes(frame.data[off + 12..off + 16].try_into().unwrap()); + if r > 0.9 && g < 0.1 && a > 0.9 { + red += 1; + } + } + red as f32 / width as f32 + }; - let small = red_fraction(192, 108); - let large = red_fraction(768, 432); - assert!( + let small = red_fraction(192, 108); + let large = red_fraction(768, 432); + assert!( (small - large).abs() < 0.02, "the shape's relative size must not depend on the render target: {small} at 192px vs {large} at 768px" ); - assert!( - (small - 0.05).abs() < 0.02, - "96px on the 1920px sequence is 5%%, got {small}" - ); + assert!( + (small - 0.05).abs() < 0.02, + "96px on the 1920px sequence is 5%%, got {small}" + ); - let _ = std::fs::remove_file(&path); + let _ = std::fs::remove_file(&path); } diff --git a/crates/oak-render/tests/ofxmisc_blur.rs b/crates/oak-render/tests/ofxmisc_blur.rs index 01618636a..7e9429a09 100644 --- a/crates/oak-render/tests/ofxmisc_blur.rs +++ b/crates/oak-render/tests/ofxmisc_blur.rs @@ -11,7 +11,7 @@ fn filled_frame(size: (i32, i32), rgba: [f32; 4]) -> Texture { Texture::wrap_frame(f) } fn pixel_at(frame: &oak_core::texture::Frame, x: usize, y: usize) -> [f32; 4] { - let stride = frame.linesize_bytes() as usize; + let stride = frame.linesize_bytes(); let at = y * stride + x * 16; let mut out = [0f32; 4]; for c in 0..4 { out[c] = f32::from_le_bytes(frame.data[at + c*4..at + c*4 + 4].try_into().unwrap()); } diff --git a/crates/oak-render/tests/ofxmisc_color.rs b/crates/oak-render/tests/ofxmisc_color.rs index 9fb0d36ab..60c532db0 100644 --- a/crates/oak-render/tests/ofxmisc_color.rs +++ b/crates/oak-render/tests/ofxmisc_color.rs @@ -11,7 +11,7 @@ fn filled_frame(size: (i32, i32), rgba: [f32; 4]) -> Texture { Texture::wrap_frame(f) } fn pixel_at(frame: &oak_core::texture::Frame, x: usize, y: usize) -> [f32; 4] { - let stride = frame.linesize_bytes() as usize; + let stride = frame.linesize_bytes(); let at = y * stride + x * 16; let mut out = [0f32; 4]; for c in 0..4 { out[c] = f32::from_le_bytes(frame.data[at + c*4..at + c*4 + 4].try_into().unwrap()); } diff --git a/crates/oak-render/tests/ofxmisc_gen.rs b/crates/oak-render/tests/ofxmisc_gen.rs index 341ce237e..3e1b803e7 100644 --- a/crates/oak-render/tests/ofxmisc_gen.rs +++ b/crates/oak-render/tests/ofxmisc_gen.rs @@ -11,7 +11,7 @@ fn filled_frame(size: (i32, i32), rgba: [f32; 4]) -> Texture { Texture::wrap_frame(f) } fn pixel_at(frame: &oak_core::texture::Frame, x: usize, y: usize) -> [f32; 4] { - let stride = frame.linesize_bytes() as usize; + let stride = frame.linesize_bytes(); let at = y * stride + x * 16; let mut out = [0f32; 4]; for c in 0..4 { out[c] = f32::from_le_bytes(frame.data[at + c*4..at + c*4 + 4].try_into().unwrap()); } @@ -44,7 +44,7 @@ const BLACK: [f32; 4] = [0.0, 0.0, 0.0, 1.0]; /// Paint one RGBA pixel into a CPU frame (these F32 frames are 16 bytes a /// pixel). fn paint(frame: &mut oak_core::texture::Frame, x: usize, y: usize, rgba: [f32; 4]) { - let at = y * frame.linesize_bytes() as usize + x * 16; + let at = y * frame.linesize_bytes() + x * 16; for (c, v) in rgba.iter().enumerate() { frame.data[at + c * 4..at + c * 4 + 4].copy_from_slice(&v.to_le_bytes()); } diff --git a/crates/oak-render/tests/ofxmisc_matrix.rs b/crates/oak-render/tests/ofxmisc_matrix.rs index 30264d849..4ade8f081 100644 --- a/crates/oak-render/tests/ofxmisc_matrix.rs +++ b/crates/oak-render/tests/ofxmisc_matrix.rs @@ -11,7 +11,7 @@ fn filled_frame(size: (i32, i32), rgba: [f32; 4]) -> Texture { Texture::wrap_frame(f) } fn pixel_at(frame: &oak_core::texture::Frame, x: usize, y: usize) -> [f32; 4] { - let stride = frame.linesize_bytes() as usize; + let stride = frame.linesize_bytes(); let at = y * stride + x * 16; let mut out = [0f32; 4]; for c in 0..4 { out[c] = f32::from_le_bytes(frame.data[at + c*4..at + c*4 + 4].try_into().unwrap()); } diff --git a/crates/oak-render/tests/ofxmisc_merge.rs b/crates/oak-render/tests/ofxmisc_merge.rs index aafbdc070..0b6c91087 100644 --- a/crates/oak-render/tests/ofxmisc_merge.rs +++ b/crates/oak-render/tests/ofxmisc_merge.rs @@ -11,7 +11,7 @@ fn filled_frame(size: (i32, i32), rgba: [f32; 4]) -> Texture { Texture::wrap_frame(f) } fn pixel_at(frame: &oak_core::texture::Frame, x: usize, y: usize) -> [f32; 4] { - let stride = frame.linesize_bytes() as usize; + let stride = frame.linesize_bytes(); let at = y * stride + x * 16; let mut out = [0f32; 4]; for c in 0..4 { out[c] = f32::from_le_bytes(frame.data[at + c*4..at + c*4 + 4].try_into().unwrap()); } diff --git a/crates/oak-render/tests/text_outline_glow.rs b/crates/oak-render/tests/text_outline_glow.rs index 0c311c843..34d3c3085 100644 --- a/crates/oak-render/tests/text_outline_glow.rs +++ b/crates/oak-render/tests/text_outline_glow.rs @@ -18,7 +18,7 @@ fn filled_frame(size: (i32, i32), rgba: [f32; 4]) -> Texture { Texture::wrap_frame(f) } fn pixel_at(frame: &oak_core::texture::Frame, x: usize, y: usize) -> [f32; 4] { - let stride = frame.linesize_bytes() as usize; + let stride = frame.linesize_bytes(); let at = y * stride + x * 16; let mut out = [0f32; 4]; for c in 0..4 { out[c] = f32::from_le_bytes(frame.data[at + c*4..at + c*4 + 4].try_into().unwrap()); } diff --git a/crates/oak-render/tests/transitionfx.rs b/crates/oak-render/tests/transitionfx.rs index f2eb720ee..181301fc7 100644 --- a/crates/oak-render/tests/transitionfx.rs +++ b/crates/oak-render/tests/transitionfx.rs @@ -47,7 +47,7 @@ fn gradient_frame_x(size: (i32, i32)) -> Texture { Texture::wrap_frame(f) } fn pixel_at(frame: &oak_core::texture::Frame, x: usize, y: usize) -> [f32; 4] { - let stride = frame.linesize_bytes() as usize; + let stride = frame.linesize_bytes(); let at = y * stride + x * 16; let mut out = [0f32; 4]; for c in 0..4 { out[c] = f32::from_le_bytes(frame.data[at + c*4..at + c*4 + 4].try_into().unwrap()); } diff --git a/crates/oak-storage/src/backends/database/mod.rs b/crates/oak-storage/src/backends/database/mod.rs index f8b26b5a0..cd6183c3a 100644 --- a/crates/oak-storage/src/backends/database/mod.rs +++ b/crates/oak-storage/src/backends/database/mod.rs @@ -124,9 +124,7 @@ impl DbTarget { /// The `?project=` selector. fn project(&self) -> Option<&str> { match self { - DbTarget::Sqlite { project, .. } | DbTarget::Pg { project, .. } => { - project.as_deref() - } + DbTarget::Sqlite { project, .. } | DbTarget::Pg { project, .. } => project.as_deref(), } } } @@ -610,7 +608,12 @@ impl DatabaseBackend { /// backend's save semantics (same serializer output, file container; /// assembled in memory, nothing is written to the library). The /// target URI must be a `file://` URI. - pub fn export_to_file(&self, uri: &StorageUri, uuid: &str, file_uri: &StorageUri) -> Result<()> { + pub fn export_to_file( + &self, + uri: &StorageUri, + uuid: &str, + file_uri: &StorageUri, + ) -> Result<()> { if file_uri.scheme != "file" { return Err(Error::Invalid); } @@ -856,9 +859,9 @@ async fn connect_sqlite(path: &str) -> Result { /// A single `connect()` surfaces the refusal immediately, so a dead / /// unreachable host fails fast with a clean E_IO. async fn connect_pg(conn: &str) -> Result { - use std::str::FromStr; use sea_orm::sqlx::postgres::{PgConnectOptions, PgPoolOptions}; use sea_orm::sqlx::ConnectOptions; + use std::str::FromStr; let url = format!("postgres://{conn}"); let options = PgConnectOptions::from_str(&url) .map_err(|e| Error::Io(format!("invalid postgres connection string: {e}")))?; @@ -978,8 +981,15 @@ fn serialize_node_xml(p: &Project, id: NodeId) -> Result { let mut w = XmlWriterBridge::new() .ok_or_else(|| Error::Failed("oak_core XML writer unavailable".to_string()))?; w.start_element("node"); - oak_node::serializer::save_node(&mut w, &entry.core, &*entry.behavior, id, &type_id, &connections) - .map_err(|e| Error::Format(e.to_string()))?; + oak_node::serializer::save_node( + &mut w, + &entry.core, + &*entry.behavior, + id, + &type_id, + &connections, + ) + .map_err(|e| Error::Format(e.to_string()))?; w.end_element(); Ok(w.output()) } @@ -1088,7 +1098,10 @@ async fn replay_state( .map_err(db_err)?; for r in rows { if r.node_identity == SETTINGS_NODE { - settings = r.new_xml.clone().unwrap_or_else(|| EMPTY_SETTINGS.to_string()); + settings = r + .new_xml + .clone() + .unwrap_or_else(|| EMPTY_SETTINGS.to_string()); } else if let Some(new) = r.new_xml { nodes.insert((r.node_identity - 1) as u64, new); } else { @@ -1373,8 +1386,11 @@ async fn maybe_snapshot( .one(conn) .await .map_err(db_err)?; - let interval = oak_core::configstore::ConfigStore::instance() - .get_int(Some("Storage"), "SnapshotIntervalSec", 600); + let interval = oak_core::configstore::ConfigStore::instance().get_int( + Some("Storage"), + "SnapshotIntervalSec", + 600, + ); let due = match &last { None => true, Some(s) => { @@ -1426,7 +1442,7 @@ async fn prune_snapshots(conn: &C, project_id: i64) -> Resul let keep: Vec = snapshot::Entity::find() .filter(snapshot::Column::ProjectId.eq(project_id)) .order_by_desc(snapshot::Column::CommandSeq) - .limit(SNAPSHOT_KEEP as u64) + .limit(SNAPSHOT_KEEP) .all(conn) .await .map_err(db_err)? @@ -1450,9 +1466,16 @@ async fn prune_snapshots(conn: &C, project_id: i64) -> Resul /// the newest snapshot already covers them, so the head state stays /// reconstructible from the snapshot plus the remaining rows (plan §1: /// history beyond the window is forfeit). -async fn retention_prune(conn: &C, project_id: i64, now: DateTime) -> Result<()> { - let days = oak_core::configstore::ConfigStore::instance() - .get_int(Some("Storage"), "JournalRetentionDays", 0); +async fn retention_prune( + conn: &C, + project_id: i64, + now: DateTime, +) -> Result<()> { + let days = oak_core::configstore::ConfigStore::instance().get_int( + Some("Storage"), + "JournalRetentionDays", + 0, + ); if days <= 0 { return Ok(()); } @@ -1609,12 +1632,11 @@ mod tests { .unwrap() .create)(); let a = guard.graph.add_node(core, behavior); - guard - .graph - .get_mut(a) - .unwrap() - .core - .set_standard_value("param_a_in", -1, oak_node::value::NodeValue::Float(2.5)); + guard.graph.get_mut(a).unwrap().core.set_standard_value( + "param_a_in", + -1, + oak_node::value::NodeValue::Float(2.5), + ); } let guard = p.lock().unwrap(); let (nodes, settings, _) = serialize_project_state(&guard).unwrap(); diff --git a/crates/oak-task/src/export.rs b/crates/oak-task/src/export.rs index 973b309f4..4185b0616 100644 --- a/crates/oak-task/src/export.rs +++ b/crates/oak-task/src/export.rs @@ -42,8 +42,8 @@ use std::sync::Arc; use oak_codec::encoder::{create_from_params, Encoder}; use oak_codec::encodingparams::EncodingParams as CodecEncodingParams; -use oak_core::videoparams::VideoParams as CommonVideoParams; use oak_core::texture::Texture; +use oak_core::videoparams::VideoParams as CommonVideoParams; use crate::error::{Error, Result}; use crate::nodeops::{self, NodeRef}; @@ -127,7 +127,6 @@ pub struct EncodingParams { pub color_trc: i32, /// AVColorSpace (SDR 709 = 1, HDR BT.2020 = 9). pub color_space: i32, - } impl ExportTask { @@ -147,7 +146,13 @@ impl ExportTask { // `it_export` run). let video_params = nodeops::sequence_video_params(&viewer.0, viewer.1, 0) .or_else(|| nodeops::footage_video_params(&viewer.0, viewer.1, 0)); - let render = RenderTask::new(base, video_params, viewer.clone(), ForceParams::default(), None); + let render = RenderTask::new( + base, + video_params, + viewer.clone(), + ForceParams::default(), + None, + ); ExportTask { render, viewer_node: viewer, @@ -231,15 +236,13 @@ impl ExportTask { /// The project's pipeline color settings (working colorspace + the /// delivery output spec) read off the exported node's project — the /// export renders to the project's delivery target, not to the display. - fn delivery_color(&self) -> ( - oak_core::colormath::WorkingColorSpace, - oak_core::colormath::OutputColorSpec, + fn delivery_color( + &self, + ) -> ( + oak_core::colormath::WorkingColorSpace, + oak_core::colormath::OutputColorSpec, ) { - let guard = self - .viewer_node - .0 - .lock() - .unwrap_or_else(|e| e.into_inner()); + let guard = self.viewer_node.0.lock().unwrap_or_else(|e| e.into_inner()); (guard.working_color_space(), guard.output_color_spec()) } @@ -288,25 +291,26 @@ impl ExportTask { let frame = match texture { Texture::Cpu(frame) => frame.clone(), Texture::Gpu { .. } => texture.to_frame().map_err(|e| { - Error::Failed(format!("Render frame readback for the encoder failed: {e:?}")) + Error::Failed(format!( + "Render frame readback for the encoder failed: {e:?}" + )) })?, }; let frame = &frame; let params = CommonVideoParams::new_basic( - frame.width, - frame.height, - oak_core::ocioutils::PixelFormat::from_code(frame.format as i32), - 4, - 1, - 1, - 0, - 1, + frame.width, + frame.height, + oak_core::ocioutils::PixelFormat::from_code(frame.format as i32), + 4, + 1, + 1, + 0, + 1, ); let mut out = oak_codec::frame::Frame::with_params(params); out.set_timestamp(frame.timestamp); - out.allocate().map_err(|e| { - Error::Failed(format!("Failed to allocate encoder frame: {e:?}")) - })?; + out.allocate() + .map_err(|e| Error::Failed(format!("Failed to allocate encoder frame: {e:?}")))?; let dst_stride = out.linesize_bytes() as usize; let Some(dst) = out.data_mut() else { return Err(Error::Failed( @@ -386,7 +390,7 @@ impl TaskBehavior for ExportTask { result?; // Flush the encoder and surface any trailing error. - if let Err(_) = encoder.flush() { + if encoder.flush().is_err() { // Fall through to the error read below (the flush error is // surfaced through `get_error()` like the C ABI did). } @@ -411,7 +415,7 @@ impl RenderTaskBehavior for ExportTask { // project's delivery colorspace before encoding. (No-op in the // legacy sRGB working space.) self.apply_output_node(&mut codec_frame); - if let Err(_) = encoder.write_video(&codec_frame) { + if encoder.write_video(&codec_frame).is_err() { let err = encoder.get_error(); task.set_error(&err); return Err(Error::Failed("Failed to write frame".to_string())); @@ -433,7 +437,10 @@ impl RenderTaskBehavior for ExportTask { } else { 0 }; - if let Err(_) = encoder.write_audio(&samples.samples, frame_count as i32) { + if encoder + .write_audio(&samples.samples, frame_count as i32) + .is_err() + { let err = encoder.get_error(); task.set_error(&err); return Err(Error::Failed("Failed to write audio".to_string())); @@ -450,7 +457,7 @@ impl RenderTaskBehavior for ExportTask { }; // The simplified path does not carry the subtitle block's in/out // times; write with 0.0/0.0 (the encoder default interval). - if let Err(_) = encoder.write_subtitle(text, 0.0, 0.0) { + if encoder.write_subtitle(text, 0.0, 0.0).is_err() { let err = encoder.get_error(); task.set_error(&err); return Err(Error::Failed("Failed to write subtitle".to_string())); diff --git a/crates/oak-task/src/handle.rs b/crates/oak-task/src/handle.rs index b12ae8ccf..5bd08e3d6 100644 --- a/crates/oak-task/src/handle.rs +++ b/crates/oak-task/src/handle.rs @@ -101,6 +101,11 @@ pub unsafe fn get(h: &CHandle) -> Option<&T> { /// # Safety /// `T` must be the boxed type, and the handle must not be concurrently /// shared mutably. +// The shared reference is the ABI input; exclusivity is the caller's +// `unsafe` contract above, so the lint's usual aliasing concern is +// discharged by the caller, not by `&mut CHandle` (which the C ABI does +// not pass). +#[allow(clippy::mut_from_ref)] pub unsafe fn get_mut(h: &CHandle) -> Option<&mut T> { if h.ctx.is_null() { return None; diff --git a/crates/oak-task/src/manager.rs b/crates/oak-task/src/manager.rs index ae7c345a9..c50ebc454 100644 --- a/crates/oak-task/src/manager.rs +++ b/crates/oak-task/src/manager.rs @@ -194,9 +194,7 @@ impl TaskManager { /// Index of the task with the given address. pub fn find_index(&self, ptr: *const Task) -> Option { - self.tasks - .iter() - .position(|t| t.task() as *const Task == ptr) + self.tasks.iter().position(|t| std::ptr::eq(t.task(), ptr)) } /// Cancel the task with the given address (no-op when absent). diff --git a/crates/oak-task/src/proxy.rs b/crates/oak-task/src/proxy.rs index fe2a17d3b..6a5a327e4 100644 --- a/crates/oak-task/src/proxy.rs +++ b/crates/oak-task/src/proxy.rs @@ -199,8 +199,9 @@ impl ProxyTask { /// 探测本机可用的硬件 H.264 编码器(每进程每 ffmpeg 路径缓存一次: /// `ffmpeg -hide_banner -encoders` 的输出里按平台优先级找)。 pub fn probe_hw_encoder(ffmpeg_path: &str) -> HwEncoder { - static CACHE: std::sync::OnceLock>> = - std::sync::OnceLock::new(); + static CACHE: std::sync::OnceLock< + std::sync::Mutex>, + > = std::sync::OnceLock::new(); let cache = CACHE.get_or_init(|| std::sync::Mutex::new(std::collections::HashMap::new())); let mut cache = cache.lock().unwrap_or_else(|e| e.into_inner()); if let Some(enc) = cache.get(ffmpeg_path) { @@ -424,13 +425,14 @@ impl TaskBehavior for ProxyTask { // Create the output directory if needed. if let Some(parent) = std::path::Path::new(&self.output_filename).parent() { - if !parent.as_os_str().is_empty() && !parent.exists() { - if std::fs::create_dir_all(parent).is_err() { - task.set_error("Failed to create proxy output directory"); - return Err(Error::Failed( - "Failed to create proxy output directory".to_string(), - )); - } + if !parent.as_os_str().is_empty() + && !parent.exists() + && std::fs::create_dir_all(parent).is_err() + { + task.set_error("Failed to create proxy output directory"); + return Err(Error::Failed( + "Failed to create proxy output directory".to_string(), + )); } } @@ -583,7 +585,12 @@ mod tests { #[test] fn transcode_arguments_software_keeps_parity_body() { let args = ProxyTask::build_transcode_arguments( - "/src.mov", 0, ¶ms(), "/dst.mp4", HwEncoder::Software, 4, + "/src.mov", + 0, + ¶ms(), + "/dst.mp4", + HwEncoder::Software, + 4, ); assert!(args.windows(2).any(|w| w == ["-hwaccel", "auto"])); assert!(args.windows(2).any(|w| w == ["-threads", "4"])); @@ -600,27 +607,47 @@ mod tests { #[test] fn transcode_arguments_hw_swaps_encoder() { let vt = ProxyTask::build_transcode_arguments( - "/src.mov", 0, ¶ms(), "/dst.mp4", HwEncoder::VideoToolbox, 4, + "/src.mov", + 0, + ¶ms(), + "/dst.mp4", + HwEncoder::VideoToolbox, + 4, ); assert!(vt.windows(2).any(|w| w == ["-c:v", "h264_videotoolbox"])); assert!(vt.windows(2).any(|w| w == ["-q:v", "23"])); assert!(!vt.iter().any(|a| a == "libx264" || a == "-crf")); let nv = ProxyTask::build_transcode_arguments( - "/src.mov", 0, ¶ms(), "/dst.mp4", HwEncoder::Nvenc, 4, + "/src.mov", + 0, + ¶ms(), + "/dst.mp4", + HwEncoder::Nvenc, + 4, ); assert!(nv.windows(2).any(|w| w == ["-c:v", "h264_nvenc"])); assert!(nv.windows(2).any(|w| w == ["-preset", "p3"])); assert!(nv.windows(2).any(|w| w == ["-cq", "23"])); let qsv = ProxyTask::build_transcode_arguments( - "/src.mov", 0, ¶ms(), "/dst.mp4", HwEncoder::Qsv, 4, + "/src.mov", + 0, + ¶ms(), + "/dst.mp4", + HwEncoder::Qsv, + 4, ); assert!(qsv.windows(2).any(|w| w == ["-c:v", "h264_qsv"])); assert!(qsv.windows(2).any(|w| w == ["-global_quality", "23"])); let amf = ProxyTask::build_transcode_arguments( - "/src.mov", 0, ¶ms(), "/dst.mp4", HwEncoder::Amf, 4, + "/src.mov", + 0, + ¶ms(), + "/dst.mp4", + HwEncoder::Amf, + 4, ); assert!(amf.windows(2).any(|w| w == ["-c:v", "h264_amf"])); assert!(amf.windows(2).any(|w| w == ["-quality", "speed"])); diff --git a/crates/oak-task/src/render.rs b/crates/oak-task/src/render.rs index 778ea4014..fd508775f 100644 --- a/crates/oak-task/src/render.rs +++ b/crates/oak-task/src/render.rs @@ -57,19 +57,15 @@ use oak_core::videoparams::VideoParams; use oak_node::footage::FootageBehavior; use oak_node::sequence::SequenceBehavior; use oak_node::track::{TrackBehavior, TrackListBehavior, TrackType}; -use oak_render::procpool::{ - bgra8_to_f32_rgba, DispatcherConfig, ProcessDispatcher, ShmFrameRef, -}; +use oak_render::procpool::{bgra8_to_f32_rgba, DispatcherConfig, ProcessDispatcher, ShmFrameRef}; use oak_render::ticket::{ - ticket_kind, AudioTicketParams, MontageClip, MontageEffect, TicketArena, TicketId, TicketPayload, - TicketResult, VideoTicketParams, + ticket_kind, AudioTicketParams, MontageClip, MontageEffect, TicketArena, TicketId, + TicketPayload, TicketResult, VideoTicketParams, }; use oak_render::worker::JobDispatch; use crate::error::{Error, Result}; -use crate::nodeops::{ - find_input_footage, pixel_format_from_code, NodeRef, ProjectRef, -}; +use crate::nodeops::{find_input_footage, pixel_format_from_code, NodeRef, ProjectRef}; use crate::task::Task; use oak_core::{Rational, TimeRange}; @@ -112,12 +108,16 @@ pub struct ForceParams { pub trait RenderTaskBehavior { /// Called for each rendered video frame. fn frame_downloaded( - &mut self, - task: &mut Task, - frame: &oak_core::texture::Texture, + &mut self, + task: &mut Task, + frame: &oak_core::texture::Texture, ) -> Result<()>; /// Called for each rendered audio buffer. - fn audio_downloaded(&mut self, task: &mut Task, samples: &oak_render::ticket::AudioSamples) -> Result<()>; + fn audio_downloaded( + &mut self, + task: &mut Task, + samples: &oak_render::ticket::AudioSamples, + ) -> Result<()>; /// Called to encode a subtitle. fn encode_subtitle(&mut self, task: &mut Task, text: &str) -> Result<()>; } @@ -230,7 +230,10 @@ impl RenderTask { /// The forced output size from [`ForceParams`], or `None`. fn force_size(&self) -> Option<(i32, i32)> { if self.force_params.force_width > 0 && self.force_params.force_height > 0 { - Some((self.force_params.force_width, self.force_params.force_height)) + Some(( + self.force_params.force_width, + self.force_params.force_height, + )) } else { None } @@ -253,7 +256,10 @@ impl RenderTask { /// traverser's bypass pushes the effect input through unchanged). /// Parameters are the non-hidden, non-connection data inputs at /// their standard (non-keyframed) values. - fn clip_effects(graph: &oak_node::graph::Graph, host: oak_node::id::NodeId) -> Vec { + fn clip_effects( + graph: &oak_node::graph::Graph, + host: oak_node::id::NodeId, + ) -> Vec { // Walk the chain: from the host's effect input upstream until an // unconnected input or a node without an effect input; a `seen` // guard protects against malformed cycles. @@ -333,10 +339,12 @@ impl RenderTask { /// (bottom-most track first so the topmost track — the highest-numbered /// one, the list's last — composites last; /// `// CPP-PARITY: M12 P0 montage contract`). - fn video_montage(project: &ProjectRef, sequence: oak_node::id::NodeId, time: Rational) -> Vec { - let guard = project - .lock() - .unwrap_or_else(|e| e.into_inner()); + fn video_montage( + project: &ProjectRef, + sequence: oak_node::id::NodeId, + time: Rational, + ) -> Vec { + let guard = project.lock().unwrap_or_else(|e| e.into_inner()); let Some(entry) = guard.graph.get(sequence) else { return Vec::new(); }; @@ -374,8 +382,7 @@ impl RenderTask { continue; }; for block_id in &track.blocks { - let Some(core) = crate::nodeops::block_core_of(&guard.graph, *block_id) - else { + let Some(core) = crate::nodeops::block_core_of(&guard.graph, *block_id) else { continue; }; if time < core.in_() || time >= core.out() { @@ -411,10 +418,12 @@ impl RenderTask { /// Flatten the audio tracks of `sequence` into an audio montage /// (track order is irrelevant — the mixer accumulates gains). - fn audio_montage(project: &ProjectRef, sequence: oak_node::id::NodeId, time: Rational) -> Vec { - let guard = project - .lock() - .unwrap_or_else(|e| e.into_inner()); + fn audio_montage( + project: &ProjectRef, + sequence: oak_node::id::NodeId, + time: Rational, + ) -> Vec { + let guard = project.lock().unwrap_or_else(|e| e.into_inner()); let Some(entry) = guard.graph.get(sequence) else { return Vec::new(); }; @@ -452,8 +461,7 @@ impl RenderTask { continue; }; for block_id in &track.blocks { - let Some(core) = crate::nodeops::block_core_of(&guard.graph, *block_id) - else { + let Some(core) = crate::nodeops::block_core_of(&guard.graph, *block_id) else { continue; }; if time < core.in_() || time >= core.out() { @@ -569,7 +577,8 @@ impl RenderTask { /// single range; the C++ submits one ticket per audio range). fn build_audio_ticket(&self, range: TimeRange) -> Result { let (project, viewer_id) = &self.viewer; - let (sample_rate, channel_layout) = crate::nodeops::sequence_audio_params(project, *viewer_id); + let (sample_rate, channel_layout) = + crate::nodeops::sequence_audio_params(project, *viewer_id); let montage = Self::audio_montage(project, *viewer_id, range.in_()); Ok(AudioTicketParams { viewer: viewer_id.identity(), @@ -595,9 +604,13 @@ impl RenderTask { let params = self.build_video_ticket(time)?; let id = arena.next_id(); let dispatch_ptr = DispatchPtr(dispatch); - arena.submit_video_background_with_id(id, params, Box::new(move |result| { - push_finished(id, result, dispatch_ptr); - })); + arena.submit_video_background_with_id( + id, + params, + Box::new(move |result| { + push_finished(id, result, dispatch_ptr); + }), + ); Ok(id) } @@ -612,9 +625,13 @@ impl RenderTask { let params = self.build_audio_ticket(range)?; let id = arena.next_id(); let dispatch_ptr = DispatchPtr(dispatch); - arena.submit_audio_with_id(id, params, Box::new(move |result| { - push_finished(id, result, dispatch_ptr); - })); + arena.submit_audio_with_id( + id, + params, + Box::new(move |result| { + push_finished(id, result, dispatch_ptr); + }), + ); Ok(id) } @@ -638,10 +655,7 @@ impl RenderTask { match self.submit_video_ticket(arena, time, dispatch) { Ok(id) => { in_flight.push(id); - ticket_keys.insert( - id, - (TICKET_VIDEO, time.numerator(), time.denominator()), - ); + ticket_keys.insert(id, (TICKET_VIDEO, time.numerator(), time.denominator())); Ok(()) } Err(e) => { @@ -772,8 +786,7 @@ impl RenderTask { .start() .map_err(|e| Error::Failed(format!("render worker pool start: {e}")))?; let producer: oak_render::ticket::Producer = Arc::new(|time, params| { - oak_render::eval::render_produced_frame(time, params) - .map(TicketPayload::Video) + oak_render::eval::render_produced_frame(time, params).map(TicketPayload::Video) }); // M15 S3: the private dispatcher routes audio through the // worker pool too; the inline dispatcher is the fallback @@ -910,9 +923,8 @@ impl RenderTask { break; } Err(e) => { - result = Err(Error::Failed(format!( - "Audio render ticket failed: {e:?}" - ))); + result = + Err(Error::Failed(format!("Audio render ticket failed: {e:?}"))); break; } } @@ -956,9 +968,8 @@ impl RenderTask { break; } Err(e) => { - result = Err(Error::Failed(format!( - "Frame render ticket failed: {e:?}" - ))); + result = + Err(Error::Failed(format!("Frame render ticket failed: {e:?}"))); break; } } @@ -1020,8 +1031,7 @@ impl RenderTask { .finished .lock() .unwrap_or_else(|e| e.into_inner()) - .is_empty() - && dispatch_ref.running.load(Ordering::SeqCst) == 0 + .is_empty() && dispatch_ref.running.load(Ordering::SeqCst) == 0 { // Every ticket finished and its queue copy was consumed. break; @@ -1202,7 +1212,9 @@ fn shm_frame_to_texture(frame: &ShmFrameRef) -> oak_core::texture::Texture { f.channels = 4; f.timestamp = oak_core::Rational::new(meta.time_num, meta.time_den); f.data = samples - .chunks_exact(4) + .as_chunks::<4>() + .0 + .iter() .flat_map(|px| { let mut bytes = [0u8; 16]; for (i, v) in px.iter().enumerate() { diff --git a/crates/oak-task/tests/export_test.rs b/crates/oak-task/tests/export_test.rs index e9971e022..287c8c8fc 100644 --- a/crates/oak-task/tests/export_test.rs +++ b/crates/oak-task/tests/export_test.rs @@ -69,7 +69,12 @@ fn build_project(media: &str) -> (Arc>, NodeId) { let (ccore, cbehavior) = oak_node::block::clip_create(); let clip = p.graph.add_node(ccore, cbehavior); p.graph - .connect(footage, clip, oak_node::block::clip_input::TEXTURE_INPUT, -1) + .connect( + footage, + clip, + oak_node::block::clip_input::TEXTURE_INPUT, + -1, + ) .expect("connect footage to clip"); p.graph .get_mut(clip) @@ -207,7 +212,7 @@ fn export_writes_a_real_playable_mp4() { let oak_core::texture::Texture::Cpu(frame) = &tex else { panic!("decode produced a GPU texture"); }; - let off = 32 * frame.linesize_bytes() as usize + 32 * 16; + let off = 32 * frame.linesize_bytes() + 32 * 16; let r = f32::from_le_bytes(frame.data[off..off + 4].try_into().unwrap()); let g = f32::from_le_bytes(frame.data[off + 4..off + 8].try_into().unwrap()); assert!( diff --git a/crates/oak-timeline/src/handle.rs b/crates/oak-timeline/src/handle.rs index e55277e1d..2a35d3916 100644 --- a/crates/oak-timeline/src/handle.rs +++ b/crates/oak-timeline/src/handle.rs @@ -130,6 +130,11 @@ pub unsafe fn get(h: &CHandle) -> Option<&T> { /// `T` must be the boxed type, and the caller must guarantee exclusive /// access to the boxed value for the duration of the borrow (no two /// mutable views alive at once). +// The shared reference is the ABI input; exclusivity is the caller's +// `unsafe` contract above, so the lint's usual aliasing concern is +// discharged by the caller, not by `&mut CHandle` (which the C ABI does +// not pass). +#[allow(clippy::mut_from_ref)] pub unsafe fn get_mut(h: &CHandle) -> Option<&mut T> { if h.ctx.is_null() { return None; diff --git a/crates/oak-timeline/src/multicam.rs b/crates/oak-timeline/src/multicam.rs index 5f96c05c1..eb74bd4af 100644 --- a/crates/oak-timeline/src/multicam.rs +++ b/crates/oak-timeline/src/multicam.rs @@ -53,17 +53,15 @@ use oak_node::block::clip_input; use oak_node::graph::{Graph, NodeEntry}; use oak_node::id::NodeId; use oak_node::nodes::multicamnode::{ - MultiCamNode, CURRENT_INPUT, SEQUENCE_INPUT, SEQUENCE_TYPE_INPUT, + MultiCamNode, CURRENT_INPUT, SEQUENCE_INPUT, SEQUENCE_TYPE_INPUT, }; use oak_node::project::Project; use oak_node::track::TrackType; use oak_node::value::NodeValue; use oak_undo::undocommand::UndoCommand; -use crate::util::{ - block_in, block_kind, block_out, block_track, BlockKind, NodeRef, -}; use crate::undosplit::BlockSplitPreservingLinksCommand; +use crate::util::{block_in, block_kind, block_out, block_track, BlockKind, NodeRef}; /// Undo label for [`multicam_enable`] (C++ /// `tr("Multi-Cam Enabled On %1 Clip(s)")`). @@ -77,12 +75,12 @@ pub const SWITCH_LABEL: &str = "Switched Multi-Camera Source"; /// Format [`ENABLE_LABEL`] with a clip count. pub fn enable_label(clip_count: usize) -> String { - ENABLE_LABEL.replace("%1", &clip_count.to_string()) + ENABLE_LABEL.replace("%1", &clip_count.to_string()) } /// Format [`DISABLE_LABEL`] with a clip count. pub fn disable_label(clip_count: usize) -> String { - DISABLE_LABEL.replace("%1", &clip_count.to_string()) + DISABLE_LABEL.replace("%1", &clip_count.to_string()) } // --------------------------------------------------------------------------- @@ -95,86 +93,88 @@ pub fn disable_label(clip_count: usize) -> String { /// `find_input_nodes_connected_to_input(input, 1)` /// (`// CPP-PARITY: clip.cpp:657-665`). pub fn clip_find_multicam(clip: &NodeRef) -> Option { - let project = clip.project.clone(); - { - let p = project.lock().unwrap_or_else(|poisoned| poisoned.into_inner()); - let mut list = Vec::new(); - find_input_nodes_connected_to_input_internal( - &p.graph, - clip.id, - clip_input::TEXTURE_INPUT, - -1, - 1, - &mut list, - ); - list.first().map(|id| *id) - } - .map(|id| NodeRef::new(project, id)) + let project = clip.project.clone(); + { + let p = project + .lock() + .unwrap_or_else(|poisoned| poisoned.into_inner()); + let mut list = Vec::new(); + find_input_nodes_connected_to_input_internal( + &p.graph, + clip.id, + clip_input::TEXTURE_INPUT, + -1, + 1, + &mut list, + ); + list.first().copied() + } + .map(|id| NodeRef::new(project, id)) } /// One step of the C++ `find_input_nodes_connected_to_input`: the node /// feeding `node.input[element]`, then a recursive walk of its inputs, /// stopping after `maximum` matches (`maximum == 0` = unlimited). fn find_input_nodes_connected_to_input_internal( - graph: &Graph, - node: NodeId, - input: &str, - element: i32, - maximum: usize, - list: &mut Vec, + graph: &Graph, + node: NodeId, + input: &str, + element: i32, + maximum: usize, + list: &mut Vec, ) { - let Some(source) = graph.connected_output(node, input, element) else { - return; - }; - if is_multicam(graph, source) { - list.push(source); - if maximum != 0 && list.len() == maximum { - return; - } - } - find_input_node_internal(graph, source, maximum, list); + let Some(source) = graph.connected_output(node, input, element) else { + return; + }; + if is_multicam(graph, source) { + list.push(source); + if maximum != 0 && list.len() == maximum { + return; + } + } + find_input_node_internal(graph, source, maximum, list); } /// C++ `find_input_node_internal` — walk `node`'s input connections, /// checking each source and recursing. fn find_input_node_internal(graph: &Graph, node: NodeId, maximum: usize, list: &mut Vec) { - for (from, _input, _element) in graph.input_connections(node) { - if is_multicam(graph, from) { - list.push(from); - if maximum != 0 && list.len() == maximum { - return; - } - } - find_input_node_internal(graph, from, maximum, list); - if maximum != 0 && list.len() == maximum { - return; - } - } + for (from, _input, _element) in graph.input_connections(node) { + if is_multicam(graph, from) { + list.push(from); + if maximum != 0 && list.len() == maximum { + return; + } + } + find_input_node_internal(graph, from, maximum, list); + if maximum != 0 && list.len() == maximum { + return; + } + } } /// Whether `id` names a `MultiCamNode`. fn is_multicam(graph: &Graph, id: NodeId) -> bool { - graph - .get(id) - .and_then(|e| e.behavior.as_any()) - .and_then(|a| a.downcast_ref::()) - .is_some() + graph + .get(id) + .and_then(|e| e.behavior.as_any()) + .and_then(|a| a.downcast_ref::()) + .is_some() } /// Set/clear the `MultiCamNode` sequence state after a `sequence_in` edge /// edit (the graph arena fires no behavior events, so the command keeps the /// behavior in sync — C++ `InputConnectedEvent`/`InputDisconnectedEvent`). fn sync_multicam_sequence(graph: &mut Graph, mc: NodeId, sequence: Option) { - let Some(entry) = graph.get_mut(mc) else { - return; - }; - if let Some(mc_node) = entry - .behavior - .as_any_mut() - .and_then(|a| a.downcast_mut::()) - { - mc_node.set_sequence(&mut entry.core, sequence); - } + let Some(entry) = graph.get_mut(mc) else { + return; + }; + if let Some(mc_node) = entry + .behavior + .as_any_mut() + .and_then(|a| a.downcast_mut::()) + { + mc_node.set_sequence(&mut entry.core, sequence); + } } /// C++ `Node::find_ways_node_arrives_here()` — every input slot along @@ -182,88 +182,93 @@ fn sync_multicam_sequence(graph: &mut Graph, mc: NodeId, sequence: Option, + graph: &Graph, + output: NodeId, + node: NodeId, + v: &mut Vec<(NodeId, String, i32)>, ) { - for (from, input, element) in graph.input_connections(node) { - if from == output { - v.push((node, input, element)); - } else { - find_ways_node_arrives_here(graph, output, from, v); - } - } + for (from, input, element) in graph.input_connections(node) { + if from == output { + v.push((node, input, element)); + } else { + find_ways_node_arrives_here(graph, output, from, v); + } + } } /// The `current_in` value of a multicam node (its currently selected /// source; `-1` when the node is stale). fn multicam_current_source(graph: &Graph, mc: NodeId) -> i32 { - graph - .get(mc) - .map(|e| e.core.standard_value(CURRENT_INPUT, -1).to_double() as i32) - .unwrap_or(-1) + graph + .get(mc) + .map(|e| e.core.standard_value(CURRENT_INPUT, -1).to_double() as i32) + .unwrap_or(-1) } /// Set the `current_in` standard value of a multicam node. fn multicam_set_current(graph: &mut Graph, mc: NodeId, source: i32) { - if let Some(entry) = graph.get_mut(mc) { - entry - .core - .set_standard_value(CURRENT_INPUT, -1, NodeValue::Combo(source as i64)); - } + if let Some(entry) = graph.get_mut(mc) { + entry + .core + .set_standard_value(CURRENT_INPUT, -1, NodeValue::Combo(source as i64)); + } } /// The clip's track media type (C++ `track_type_of(block_track(c))`); /// defaults to video when the clip is trackless. fn clip_track_type(clip: &NodeRef) -> TrackType { - block_track(clip) - .and_then(|track| { - let p = track.project.lock().unwrap_or_else(|poisoned| poisoned.into_inner()); - p.graph - .get(track.id) - .and_then(|e| e.behavior.as_any()) - .and_then(|a| a.downcast_ref::()) - .map(|t| t.kind) - }) - .unwrap_or(TrackType::Video) + block_track(clip) + .and_then(|track| { + let p = track + .project + .lock() + .unwrap_or_else(|poisoned| poisoned.into_inner()); + p.graph + .get(track.id) + .and_then(|e| e.behavior.as_any()) + .and_then(|a| a.downcast_ref::()) + .map(|t| t.kind) + }) + .unwrap_or(TrackType::Video) } /// Linked block ids of `block` in the project graph (C++ /// `Block::block_links()`). fn block_links(clip: &NodeRef) -> Vec { - let project = clip.project.clone(); - let p = project.lock().unwrap_or_else(|poisoned| poisoned.into_inner()); - p.graph - .links_of(clip.id) - .into_iter() - .map(|id| NodeRef::new(project.clone(), id)) - .collect() + let project = clip.project.clone(); + let p = project + .lock() + .unwrap_or_else(|poisoned| poisoned.into_inner()); + p.graph + .links_of(clip.id) + .into_iter() + .map(|id| NodeRef::new(project.clone(), id)) + .collect() } /// For every clip in `clips`, the `(clip, multicam, old_current)` tuple for /// each multicam found on the clip or its clip links (C++ Switch: set the /// source on the new clip's multicam and every linked clip's multicam). -fn collect_switch_targets( - clip: &NodeRef, -) -> Vec<(NodeRef, NodeId, i32)> { - let project = clip.project.clone(); - let mut targets = Vec::new(); - let mut seen: HashSet = HashSet::new(); - for c in std::iter::once(clip.clone()).chain(block_links(clip)) { - if !seen.insert(c.id) { - continue; - } - if block_kind(&c) != BlockKind::Clip { - continue; - } - if let Some(mc) = clip_find_multicam(&c) { - let p = project.lock().unwrap_or_else(|poisoned| poisoned.into_inner()); - let old = multicam_current_source(&p.graph, mc.id); - targets.push((c, mc.id, old)); - } - } - targets +fn collect_switch_targets(clip: &NodeRef) -> Vec<(NodeRef, NodeId, i32)> { + let project = clip.project.clone(); + let mut targets = Vec::new(); + let mut seen: HashSet = HashSet::new(); + for c in std::iter::once(clip.clone()).chain(block_links(clip)) { + if !seen.insert(c.id) { + continue; + } + if block_kind(&c) != BlockKind::Clip { + continue; + } + if let Some(mc) = clip_find_multicam(&c) { + let p = project + .lock() + .unwrap_or_else(|poisoned| poisoned.into_inner()); + let old = multicam_current_source(&p.graph, mc.id); + targets.push((c, mc.id, old)); + } + } + targets } // --------------------------------------------------------------------------- @@ -272,152 +277,162 @@ fn collect_switch_targets( /// Per-clip state of [`MultiCamEnableCommand`]. struct EnableState { - /// The multicam node created for the clip (valid after the first redo). - multicam: Option, - /// Detached arena entry while the node is out of the graph between - /// `undo` and the next `redo`. - entry: Option, - /// `sequence_type_in` value (the clip's track type ordinal). - sequence_type: i32, - /// Inputs the sequence fed along the clip's chain before the enable - /// (`(target, input_id, element)`), re-routed through the multicam. - rerouted: Vec<(NodeId, String, i32)>, + /// The multicam node created for the clip (valid after the first redo). + multicam: Option, + /// Detached arena entry while the node is out of the graph between + /// `undo` and the next `redo`. + entry: Option, + /// `sequence_type_in` value (the clip's track type ordinal). + sequence_type: i32, + /// Inputs the sequence fed along the clip's chain before the enable + /// (`(target, input_id, element)`), re-routed through the multicam. + rerouted: Vec<(NodeId, String, i32)>, } /// `MultiCamEnableCommand` — wrap each clip's source through a fresh /// `MultiCamNode` (C++ `multicam_enabled_triggered(true)`). One undo /// command covering every clip; push with [`enable_label`]. pub struct MultiCamEnableCommand { - /// Clips to enable multicam on. - clips: Vec, - /// The sequence whose track list supplies the angles. - sequence: NodeRef, - /// Per-clip state, built on the first redo. - state: Vec, + /// Clips to enable multicam on. + clips: Vec, + /// The sequence whose track list supplies the angles. + sequence: NodeRef, + /// Per-clip state, built on the first redo. + state: Vec, } impl MultiCamEnableCommand { - /// Construct from clips + sequence. - pub fn new(clips: Vec, sequence: NodeRef) -> Self { - Self { - clips, - sequence, - state: Vec::new(), - } - } + /// Construct from clips + sequence. + pub fn new(clips: Vec, sequence: NodeRef) -> Self { + Self { + clips, + sequence, + state: Vec::new(), + } + } - /// `prepare`: plan each clip — create its multicam node and record the - /// sequence-fed inputs to re-route. - fn prepare(&mut self) { - if !self.state.is_empty() { - return; - } - let project = self.sequence.project.clone(); - for clip in &self.clips { - let mc = { - let (core, behavior) = oak_node::nodes::multicamnode::create(); - let mut p = project.lock().unwrap_or_else(|poisoned| poisoned.into_inner()); - p.graph.add_node(core, behavior) - }; - let sequence_type = clip_track_type(clip).to_c(); - let rerouted = { - let p = project.lock().unwrap_or_else(|poisoned| poisoned.into_inner()); - let mut v = Vec::new(); - find_ways_node_arrives_here(&p.graph, self.sequence.id, clip.id, &mut v); - v - }; - self.state.push(EnableState { - multicam: Some(mc), - entry: None, - sequence_type, - rerouted, - }); - } - } + /// `prepare`: plan each clip — create its multicam node and record the + /// sequence-fed inputs to re-route. + fn prepare(&mut self) { + if !self.state.is_empty() { + return; + } + let project = self.sequence.project.clone(); + for clip in &self.clips { + let mc = { + let (core, behavior) = oak_node::nodes::multicamnode::create(); + let mut p = project + .lock() + .unwrap_or_else(|poisoned| poisoned.into_inner()); + p.graph.add_node(core, behavior) + }; + let sequence_type = clip_track_type(clip).to_c(); + let rerouted = { + let p = project + .lock() + .unwrap_or_else(|poisoned| poisoned.into_inner()); + let mut v = Vec::new(); + find_ways_node_arrives_here(&p.graph, self.sequence.id, clip.id, &mut v); + v + }; + self.state.push(EnableState { + multicam: Some(mc), + entry: None, + sequence_type, + rerouted, + }); + } + } - /// `redo`: (re-)insert the multicam nodes and re-route the sequence - /// edges through them. - pub fn redo(&mut self) { - self.prepare(); - let project = self.sequence.project.clone(); - for st in self.state.iter_mut() { - let Some(mc) = st.multicam else { - continue; - }; - let mut p = project.lock().unwrap_or_else(|poisoned| poisoned.into_inner()); - // Re-attach the node if a previous undo detached it. - if let Some(entry) = st.entry.take() { - p.graph.add_entry(entry, mc); - } - // Set the sequence type selector (the clip's track type). - if let Some(entry) = p.graph.get_mut(mc) { - entry.core.set_standard_value( - SEQUENCE_TYPE_INPUT, - -1, - NodeValue::Combo(st.sequence_type as i64), - ); - } - // Disconnect the sequence from each input it fed and connect - // the multicam in its place. - for (target, input, element) in &st.rerouted { - p.graph.disconnect(self.sequence.id, *target, input, *element); - p.graph.connect(mc, *target, input, *element).ok(); - } - // Connect the sequence to the multicam's sequence_in. - p.graph - .disconnect(self.sequence.id, mc, SEQUENCE_INPUT, -1); - p.graph - .connect(self.sequence.id, mc, SEQUENCE_INPUT, -1) - .ok(); - // Keep the behavior's cached sequence state in sync. - sync_multicam_sequence(&mut p.graph, mc, Some(self.sequence.id)); - } - } + /// `redo`: (re-)insert the multicam nodes and re-route the sequence + /// edges through them. + pub fn redo(&mut self) { + self.prepare(); + let project = self.sequence.project.clone(); + for st in self.state.iter_mut() { + let Some(mc) = st.multicam else { + continue; + }; + let mut p = project + .lock() + .unwrap_or_else(|poisoned| poisoned.into_inner()); + // Re-attach the node if a previous undo detached it. + if let Some(entry) = st.entry.take() { + p.graph.add_entry(entry, mc); + } + // Set the sequence type selector (the clip's track type). + if let Some(entry) = p.graph.get_mut(mc) { + entry.core.set_standard_value( + SEQUENCE_TYPE_INPUT, + -1, + NodeValue::Combo(st.sequence_type as i64), + ); + } + // Disconnect the sequence from each input it fed and connect + // the multicam in its place. + for (target, input, element) in &st.rerouted { + p.graph + .disconnect(self.sequence.id, *target, input, *element); + p.graph.connect(mc, *target, input, *element).ok(); + } + // Connect the sequence to the multicam's sequence_in. + p.graph.disconnect(self.sequence.id, mc, SEQUENCE_INPUT, -1); + p.graph + .connect(self.sequence.id, mc, SEQUENCE_INPUT, -1) + .ok(); + // Keep the behavior's cached sequence state in sync. + sync_multicam_sequence(&mut p.graph, mc, Some(self.sequence.id)); + } + } - /// `undo`: disconnect the multicam, re-connect the sequence straight to - /// each original input, and detach the multicam node. - pub fn undo(&mut self) { - let project = self.sequence.project.clone(); - for st in self.state.iter_mut() { - let Some(mc) = st.multicam else { - continue; - }; - let mut p = project.lock().unwrap_or_else(|poisoned| poisoned.into_inner()); - // The multicam fed every re-routed input; wire the sequence - // back directly. - for (target, input, element) in &st.rerouted { - p.graph.disconnect(mc, *target, input, *element); - p.graph.connect(self.sequence.id, *target, input, *element).ok(); - } - // Drop the sequence_in edge. - p.graph.disconnect(self.sequence.id, mc, SEQUENCE_INPUT, -1); - // Detach the multicam node (identity preserved for the next - // redo). - if st.entry.is_none() { - st.entry = p.graph.take_node(mc); - } - } - } + /// `undo`: disconnect the multicam, re-connect the sequence straight to + /// each original input, and detach the multicam node. + pub fn undo(&mut self) { + let project = self.sequence.project.clone(); + for st in self.state.iter_mut() { + let Some(mc) = st.multicam else { + continue; + }; + let mut p = project + .lock() + .unwrap_or_else(|poisoned| poisoned.into_inner()); + // The multicam fed every re-routed input; wire the sequence + // back directly. + for (target, input, element) in &st.rerouted { + p.graph.disconnect(mc, *target, input, *element); + p.graph + .connect(self.sequence.id, *target, input, *element) + .ok(); + } + // Drop the sequence_in edge. + p.graph.disconnect(self.sequence.id, mc, SEQUENCE_INPUT, -1); + // Detach the multicam node (identity preserved for the next + // redo). + if st.entry.is_none() { + st.entry = p.graph.take_node(mc); + } + } + } - /// Wrap as an oakundo command value. - pub fn to_command(self) -> UndoCommand { - crate::undocommon::box_command(self) - } + /// Wrap as an oakundo command value. + pub fn to_command(self) -> UndoCommand { + crate::undocommon::box_command(self) + } } impl crate::undocommon::Command for MultiCamEnableCommand { - fn redo(&mut self) { - self.redo(); - } + fn redo(&mut self) { + self.redo(); + } - fn undo(&mut self) { - self.undo(); - } + fn undo(&mut self) { + self.undo(); + } } /// Build the enable command (C++ `multicam_enabled_triggered(true)`). pub fn multicam_enable(clips: Vec, sequence: NodeRef) -> UndoCommand { - MultiCamEnableCommand::new(clips, sequence).to_command() + MultiCamEnableCommand::new(clips, sequence).to_command() } // --------------------------------------------------------------------------- @@ -426,149 +441,156 @@ pub fn multicam_enable(clips: Vec, sequence: NodeRef) -> UndoCommand { /// Per-clip state of [`MultiCamDisableCommand`]. struct DisableState { - /// The multicam node currently feeding the clip. - multicam: NodeId, - /// The sequence the multicam pulled angles from (from its - /// `sequence_in` edge). - sequence: NodeId, - /// The multicam's output edges `(target, input_id, element)` that - /// must be re-wired straight to the sequence. - outputs: Vec<(NodeId, String, i32)>, - /// Detached arena entry while the node is out of the graph between - /// `undo` and the next `redo`. - entry: Option, + /// The multicam node currently feeding the clip. + multicam: NodeId, + /// The sequence the multicam pulled angles from (from its + /// `sequence_in` edge). + sequence: NodeId, + /// The multicam's output edges `(target, input_id, element)` that + /// must be re-wired straight to the sequence. + outputs: Vec<(NodeId, String, i32)>, + /// Detached arena entry while the node is out of the graph between + /// `undo` and the next `redo`. + entry: Option, } /// `MultiCamDisableCommand` — bypass a clip's `MultiCamNode` and remove it /// (C++ `multicam_enabled_triggered(false)`). One undo command covering /// every clip; push with [`disable_label`]. pub struct MultiCamDisableCommand { - /// Clips to disable multicam on. - clips: Vec, - /// Per-clip state, built on the first redo. - state: Vec, + /// Clips to disable multicam on. + clips: Vec, + /// Per-clip state, built on the first redo. + state: Vec, } impl MultiCamDisableCommand { - /// Construct from clips. - pub fn new(clips: Vec) -> Self { - Self { - clips, - state: Vec::new(), - } - } + /// Construct from clips. + pub fn new(clips: Vec) -> Self { + Self { + clips, + state: Vec::new(), + } + } - /// `prepare`: locate each clip's multicam and the outputs to re-wire. - fn prepare(&mut self) { - if !self.state.is_empty() { - return; - } - let clips = self.clips.clone(); - for clip in clips { - let Some(mc) = clip_find_multicam(&clip) else { - continue; - }; - let (sequence, outputs) = { - let p = clip.project.lock().unwrap_or_else(|poisoned| poisoned.into_inner()); - let sequence = p - .graph - .connected_output(mc.id, SEQUENCE_INPUT, -1) - .unwrap_or(NodeId::INVALID); - let outputs = p.graph.output_connections(mc.id); - (sequence, outputs) - }; - if sequence == NodeId::INVALID { - // No connected sequence: nothing to bypass back to; leave - // the node untouched. - continue; - } - self.state.push(DisableState { - multicam: mc.id, - sequence, - outputs, - entry: None, - }); - } - } + /// `prepare`: locate each clip's multicam and the outputs to re-wire. + fn prepare(&mut self) { + if !self.state.is_empty() { + return; + } + let clips = self.clips.clone(); + for clip in clips { + let Some(mc) = clip_find_multicam(&clip) else { + continue; + }; + let (sequence, outputs) = { + let p = clip + .project + .lock() + .unwrap_or_else(|poisoned| poisoned.into_inner()); + let sequence = p + .graph + .connected_output(mc.id, SEQUENCE_INPUT, -1) + .unwrap_or(NodeId::INVALID); + let outputs = p.graph.output_connections(mc.id); + (sequence, outputs) + }; + if sequence == NodeId::INVALID { + // No connected sequence: nothing to bypass back to; leave + // the node untouched. + continue; + } + self.state.push(DisableState { + multicam: mc.id, + sequence, + outputs, + entry: None, + }); + } + } - /// `redo`: re-wire the multicam's outputs straight to the sequence and - /// detach the multicam node. - pub fn redo(&mut self) { - self.prepare(); - let mut project: Option>> = None; - for st in self.state.iter_mut() { - if project.is_none() { - if let Some(clip) = self.clips.first() { - project = Some(clip.project.clone()); - } else { - return; - } - } - let project = project.as_ref().expect("set above"); - let mut p = project.lock().unwrap_or_else(|poisoned| poisoned.into_inner()); - // Re-attach the node if a previous undo detached it. - if let Some(entry) = st.entry.take() { - p.graph.add_entry(entry, st.multicam); - } - // Re-wire outputs to the sequence. - for (target, input, element) in &st.outputs { - p.graph.disconnect(st.multicam, *target, input, *element); - p.graph.connect(st.sequence, *target, input, *element).ok(); - } - // Detach the multicam (the sequence_in edge goes with it). - if st.entry.is_none() { - st.entry = p.graph.take_node(st.multicam); - } - } - } + /// `redo`: re-wire the multicam's outputs straight to the sequence and + /// detach the multicam node. + pub fn redo(&mut self) { + self.prepare(); + let mut project: Option>> = None; + for st in self.state.iter_mut() { + if project.is_none() { + if let Some(clip) = self.clips.first() { + project = Some(clip.project.clone()); + } else { + return; + } + } + let project = project.as_ref().expect("set above"); + let mut p = project + .lock() + .unwrap_or_else(|poisoned| poisoned.into_inner()); + // Re-attach the node if a previous undo detached it. + if let Some(entry) = st.entry.take() { + p.graph.add_entry(entry, st.multicam); + } + // Re-wire outputs to the sequence. + for (target, input, element) in &st.outputs { + p.graph.disconnect(st.multicam, *target, input, *element); + p.graph.connect(st.sequence, *target, input, *element).ok(); + } + // Detach the multicam (the sequence_in edge goes with it). + if st.entry.is_none() { + st.entry = p.graph.take_node(st.multicam); + } + } + } - /// `undo`: re-insert the multicam node, re-route the outputs through it - /// (disconnecting the sequence), and re-connect the sequence_in edge. - pub fn undo(&mut self) { - // `undo` only runs after `redo`, which returned early when `clips` - // was empty, so a project is always available here. - let project = self - .clips - .first() - .map(|c| c.project.clone()) - .unwrap_or_else(Project::new); - for st in self.state.iter_mut() { - let mut p = project.lock().unwrap_or_else(|poisoned| poisoned.into_inner()); - if let Some(entry) = st.entry.take() { - p.graph.add_entry(entry, st.multicam); - } - // The sequence now feeds every output directly; re-route them - // through the multicam and restore the sequence_in edge. - for (target, input, element) in &st.outputs { - p.graph.disconnect(st.sequence, *target, input, *element); - p.graph.connect(st.multicam, *target, input, *element).ok(); - } - p.graph - .connect(st.sequence, st.multicam, SEQUENCE_INPUT, -1) - .ok(); - sync_multicam_sequence(&mut p.graph, st.multicam, Some(st.sequence)); - } - } + /// `undo`: re-insert the multicam node, re-route the outputs through it + /// (disconnecting the sequence), and re-connect the sequence_in edge. + pub fn undo(&mut self) { + // `undo` only runs after `redo`, which returned early when `clips` + // was empty, so a project is always available here. + let project = self + .clips + .first() + .map(|c| c.project.clone()) + .unwrap_or_else(Project::new); + for st in self.state.iter_mut() { + let mut p = project + .lock() + .unwrap_or_else(|poisoned| poisoned.into_inner()); + if let Some(entry) = st.entry.take() { + p.graph.add_entry(entry, st.multicam); + } + // The sequence now feeds every output directly; re-route them + // through the multicam and restore the sequence_in edge. + for (target, input, element) in &st.outputs { + p.graph.disconnect(st.sequence, *target, input, *element); + p.graph.connect(st.multicam, *target, input, *element).ok(); + } + p.graph + .connect(st.sequence, st.multicam, SEQUENCE_INPUT, -1) + .ok(); + sync_multicam_sequence(&mut p.graph, st.multicam, Some(st.sequence)); + } + } - /// Wrap as an oakundo command value. - pub fn to_command(self) -> UndoCommand { - crate::undocommon::box_command(self) - } + /// Wrap as an oakundo command value. + pub fn to_command(self) -> UndoCommand { + crate::undocommon::box_command(self) + } } impl crate::undocommon::Command for MultiCamDisableCommand { - fn redo(&mut self) { - self.redo(); - } + fn redo(&mut self) { + self.redo(); + } - fn undo(&mut self) { - self.undo(); - } + fn undo(&mut self) { + self.undo(); + } } /// Build the disable command (C++ `multicam_enabled_triggered(false)`). pub fn multicam_disable(clips: Vec) -> UndoCommand { - MultiCamDisableCommand::new(clips).to_command() + MultiCamDisableCommand::new(clips).to_command() } // --------------------------------------------------------------------------- @@ -579,136 +601,139 @@ pub fn multicam_disable(clips: Vec) -> UndoCommand { /// splitting the clip at the playhead first (C++ `MulticamWidget::Switch`). /// One undo command for the whole operation; push with [`SWITCH_LABEL`]. pub struct MultiCamSwitchCommand { - /// The clip whose multicam source changes. - clip: NodeRef, - /// The new source index. - source: i32, - /// Whether to split the clip at the playhead (source change applies - /// from the playhead forward). - split_clip: bool, - /// The playhead time (split point). - playhead: Rational, - /// The split command, when `split_clip` and the playhead lies strictly - /// inside the clip. - split: Option, - /// Whether the clip was actually split on the first redo. - did_split: bool, - /// `(clip, multicam, old_source)` targets captured on the first redo; - /// the ids stay stable across undo/redo (the split re-attaches its - /// copies identity-preserving). - targets: Vec<(NodeRef, NodeId, i32)>, - /// True when the clip has no multicam: the command is a no-op (the C++ - /// `if (!node_) return;` guard). - noop: bool, + /// The clip whose multicam source changes. + clip: NodeRef, + /// The new source index. + source: i32, + /// Whether to split the clip at the playhead (source change applies + /// from the playhead forward). + split_clip: bool, + /// The playhead time (split point). + playhead: Rational, + /// The split command, when `split_clip` and the playhead lies strictly + /// inside the clip. + split: Option, + /// Whether the clip was actually split on the first redo. + did_split: bool, + /// `(clip, multicam, old_source)` targets captured on the first redo; + /// the ids stay stable across undo/redo (the split re-attaches its + /// copies identity-preserving). + targets: Vec<(NodeRef, NodeId, i32)>, + /// True when the clip has no multicam: the command is a no-op (the C++ + /// `if (!node_) return;` guard). + noop: bool, } impl MultiCamSwitchCommand { - /// Construct from clip + source + split flag + playhead. - pub fn new(clip: NodeRef, source: i32, split_clip: bool, playhead: Rational) -> Self { - Self { - clip, - source, - split_clip, - playhead, - split: None, - did_split: false, - targets: Vec::new(), - noop: false, - } - } + /// Construct from clip + source + split flag + playhead. + pub fn new(clip: NodeRef, source: i32, split_clip: bool, playhead: Rational) -> Self { + Self { + clip, + source, + split_clip, + playhead, + split: None, + did_split: false, + targets: Vec::new(), + noop: false, + } + } - /// `redo`: split (if requested and the playhead is strictly inside), - /// then write `current_in` on the affected multicam copies. - pub fn redo(&mut self) { - if self.split.is_none() && !self.noop { - if clip_find_multicam(&self.clip).is_none() { - // C++ `if (!node_) return;` — nothing to switch. - self.noop = true; - return; - } - if self.split_clip { - let clip_in = block_in(&self.clip); - let clip_out = block_out(&self.clip); - if clip_in < self.playhead && self.playhead < clip_out { - // Split the clip and every linked block, preserving - // links. Each half keeps its own multicam copy. - let mut blocks = vec![self.clip.clone()]; - blocks.extend(block_links(&self.clip)); - let times = vec![self.playhead]; - let mut split = - BlockSplitPreservingLinksCommand::new(blocks, times); - split.redo(); - let new_clip = split.get_split(&self.clip, 0); - self.did_split = true; - // Targets come from the new (post-playhead) halves. - if let Some(new_clip) = new_clip { - self.targets = collect_switch_targets(&new_clip); - } - self.split = Some(split); - } - } - if !self.did_split { - // No split (split disabled or playhead outside the clip): - // the whole clip (and its links) switch source. - self.targets = collect_switch_targets(&self.clip); - } - } else if self.did_split { - // Redo after undo: re-run the split (the halves re-attach - // identity-preserving) and re-apply the stored targets. - if let Some(split) = self.split.as_mut() { - split.redo(); - } - } + /// `redo`: split (if requested and the playhead is strictly inside), + /// then write `current_in` on the affected multicam copies. + pub fn redo(&mut self) { + if self.split.is_none() && !self.noop { + if clip_find_multicam(&self.clip).is_none() { + // C++ `if (!node_) return;` — nothing to switch. + self.noop = true; + return; + } + if self.split_clip { + let clip_in = block_in(&self.clip); + let clip_out = block_out(&self.clip); + if clip_in < self.playhead && self.playhead < clip_out { + // Split the clip and every linked block, preserving + // links. Each half keeps its own multicam copy. + let mut blocks = vec![self.clip.clone()]; + blocks.extend(block_links(&self.clip)); + let times = vec![self.playhead]; + let mut split = BlockSplitPreservingLinksCommand::new(blocks, times); + split.redo(); + let new_clip = split.get_split(&self.clip, 0); + self.did_split = true; + // Targets come from the new (post-playhead) halves. + if let Some(new_clip) = new_clip { + self.targets = collect_switch_targets(&new_clip); + } + self.split = Some(split); + } + } + if !self.did_split { + // No split (split disabled or playhead outside the clip): + // the whole clip (and its links) switch source. + self.targets = collect_switch_targets(&self.clip); + } + } else if self.did_split { + // Redo after undo: re-run the split (the halves re-attach + // identity-preserving) and re-apply the stored targets. + if let Some(split) = self.split.as_mut() { + split.redo(); + } + } - // Write the new source on every captured multicam. - let project = self.clip.project.clone(); - let mut p = project.lock().unwrap_or_else(|poisoned| poisoned.into_inner()); - for (_clip, mc, _old) in &self.targets { - multicam_set_current(&mut p.graph, *mc, self.source); - } - } + // Write the new source on every captured multicam. + let project = self.clip.project.clone(); + let mut p = project + .lock() + .unwrap_or_else(|poisoned| poisoned.into_inner()); + for (_clip, mc, _old) in &self.targets { + multicam_set_current(&mut p.graph, *mc, self.source); + } + } - /// `undo`: restore the old sources and undo the split. - pub fn undo(&mut self) { - // Restore the previous sources first (the targets are still - // attached); undoing the split afterwards discards the copies. - let project = self.clip.project.clone(); - let mut p = project.lock().unwrap_or_else(|poisoned| poisoned.into_inner()); - for (_clip, mc, old) in &self.targets { - multicam_set_current(&mut p.graph, *mc, *old); - } - drop(p); - if self.did_split { - if let Some(split) = self.split.as_mut() { - split.undo(); - } - } - } + /// `undo`: restore the old sources and undo the split. + pub fn undo(&mut self) { + // Restore the previous sources first (the targets are still + // attached); undoing the split afterwards discards the copies. + let project = self.clip.project.clone(); + let mut p = project + .lock() + .unwrap_or_else(|poisoned| poisoned.into_inner()); + for (_clip, mc, old) in &self.targets { + multicam_set_current(&mut p.graph, *mc, *old); + } + drop(p); + if self.did_split { + if let Some(split) = self.split.as_mut() { + split.undo(); + } + } + } - /// Wrap as an oakundo command value. - pub fn to_command(self) -> UndoCommand { - crate::undocommon::box_command(self) - } + /// Wrap as an oakundo command value. + pub fn to_command(self) -> UndoCommand { + crate::undocommon::box_command(self) + } } impl crate::undocommon::Command for MultiCamSwitchCommand { - fn redo(&mut self) { - self.redo(); - } + fn redo(&mut self) { + self.redo(); + } - fn undo(&mut self) { - self.undo(); - } + fn undo(&mut self) { + self.undo(); + } } /// Build the switch command (C++ `MulticamWidget::Switch`). pub fn multicam_switch( - clip: NodeRef, - source: i32, - split_clip: bool, - playhead: Rational, + clip: NodeRef, + source: i32, + split_clip: bool, + playhead: Rational, ) -> UndoCommand { - MultiCamSwitchCommand::new(clip, source, split_clip, playhead).to_command() + MultiCamSwitchCommand::new(clip, source, split_clip, playhead).to_command() } // --------------------------------------------------------------------------- @@ -717,493 +742,493 @@ pub fn multicam_switch( #[cfg(test)] mod tests { - use super::*; - use crate::util::{ - block_clip_create, block_in, block_length, block_out, same_block, - track_append_block, track_block_at, track_block_count, NodeRef, - }; - use oak_core::TimeRange; - use oak_node::block::ClipBlockBehavior; - use oak_node::node::NodeCore; - use oak_node::sequence::SequenceBehavior; - use oak_node::track::{TrackBehavior, TrackListBehavior}; + use super::*; + use crate::util::{ + block_clip_create, block_in, block_length, block_out, same_block, track_append_block, + track_block_at, track_block_count, NodeRef, + }; + use oak_core::TimeRange; + use oak_node::block::ClipBlockBehavior; + use oak_node::node::NodeCore; + use oak_node::sequence::SequenceBehavior; + use oak_node::track::{TrackBehavior, TrackListBehavior}; - /// Project fixture: a sequence owning one video track list with one - /// video track. - struct Fixture { - project: Arc>, - seq: NodeRef, - track: NodeRef, - } + /// Project fixture: a sequence owning one video track list with one + /// video track. + struct Fixture { + project: Arc>, + seq: NodeRef, + track: NodeRef, + } - fn fixture() -> Fixture { - let project = Project::new(); - let (seq_id, list_id, track_id) = { - let mut p = project.lock().unwrap(); - let (core, behavior) = SequenceBehavior::create(); - let seq_id = p.graph.add_node(core, behavior); - let (core, behavior) = TrackListBehavior::create(); - let list_id = p.graph.add_node(core, behavior); - let (core, behavior) = (NodeCore::new(), Box::new(TrackBehavior::new(TrackType::Video))); - let track_id = p.graph.add_node(core, behavior); - { - let seq = p.graph.get_mut(seq_id).unwrap(); - let s = seq - .behavior - .as_any_mut() - .unwrap() - .downcast_mut::() - .unwrap(); - s.track_lists.push(list_id); - } - let list = p.graph.get_mut(list_id).unwrap(); - let l = list - .behavior - .as_any_mut() - .unwrap() - .downcast_mut::() - .unwrap(); - l.sequence = Some(seq_id); - l.tracks.push(track_id); - let track = p.graph.get_mut(track_id).unwrap(); - let t = track - .behavior - .as_any_mut() - .unwrap() - .downcast_mut::() - .unwrap(); - t.kind = TrackType::Video; - t.track_list = Some(list_id); - (seq_id, list_id, track_id) - }; - let _ = list_id; - Fixture { - project: project.clone(), - seq: NodeRef::new(project.clone(), seq_id), - track: NodeRef::new(project, track_id), - } - } + fn fixture() -> Fixture { + let project = Project::new(); + let (seq_id, list_id, track_id) = { + let mut p = project.lock().unwrap(); + let (core, behavior) = SequenceBehavior::create(); + let seq_id = p.graph.add_node(core, behavior); + let (core, behavior) = TrackListBehavior::create(); + let list_id = p.graph.add_node(core, behavior); + let (core, behavior) = ( + NodeCore::new(), + Box::new(TrackBehavior::new(TrackType::Video)), + ); + let track_id = p.graph.add_node(core, behavior); + { + let seq = p.graph.get_mut(seq_id).unwrap(); + let s = seq + .behavior + .as_any_mut() + .unwrap() + .downcast_mut::() + .unwrap(); + s.track_lists.push(list_id); + } + let list = p.graph.get_mut(list_id).unwrap(); + let l = list + .behavior + .as_any_mut() + .unwrap() + .downcast_mut::() + .unwrap(); + l.sequence = Some(seq_id); + l.tracks.push(track_id); + let track = p.graph.get_mut(track_id).unwrap(); + let t = track + .behavior + .as_any_mut() + .unwrap() + .downcast_mut::() + .unwrap(); + t.kind = TrackType::Video; + t.track_list = Some(list_id); + (seq_id, list_id, track_id) + }; + let _ = list_id; + Fixture { + project: project.clone(), + seq: NodeRef::new(project.clone(), seq_id), + track: NodeRef::new(project, track_id), + } + } - /// Add a clip spanning `[in, out)` on the fixture's video track. - fn add_clip(fx: &Fixture, in_: Rational, out: Rational) -> NodeRef { - let clip = block_clip_create(&fx.project); - { - let mut p = fx.project.lock().unwrap(); - let e = p.graph.get_mut(clip.id).unwrap(); - let c = e - .behavior - .as_any_mut() - .unwrap() - .downcast_mut::() - .unwrap(); - c.core.range = TimeRange::new(in_, out); - } - track_append_block(&fx.track, &clip); - clip - } + /// Add a clip spanning `[in, out)` on the fixture's video track. + fn add_clip(fx: &Fixture, in_: Rational, out: Rational) -> NodeRef { + let clip = block_clip_create(&fx.project); + { + let mut p = fx.project.lock().unwrap(); + let e = p.graph.get_mut(clip.id).unwrap(); + let c = e + .behavior + .as_any_mut() + .unwrap() + .downcast_mut::() + .unwrap(); + c.core.range = TimeRange::new(in_, out); + } + track_append_block(&fx.track, &clip); + clip + } - /// The pre-enable topology: the sequence feeds the clip's effect input. - fn connect_sequence_to_clip(fx: &Fixture, clip: &NodeRef) { - let mut p = fx.project.lock().unwrap(); - p.graph - .connect(fx.seq.id, clip.id, clip_input::TEXTURE_INPUT, -1) - .unwrap(); - } + /// The pre-enable topology: the sequence feeds the clip's effect input. + fn connect_sequence_to_clip(fx: &Fixture, clip: &NodeRef) { + let mut p = fx.project.lock().unwrap(); + p.graph + .connect(fx.seq.id, clip.id, clip_input::TEXTURE_INPUT, -1) + .unwrap(); + } - /// The multicam node currently feeding `clip` (via `clip_find_multicam`). - #[allow(unused_variables)] - fn multicam_of(_fx: &Fixture, clip: &NodeRef) -> Option { - clip_find_multicam(clip) - } + /// The multicam node currently feeding `clip` (via `clip_find_multicam`). + #[allow(unused_variables)] + fn multicam_of(_fx: &Fixture, clip: &NodeRef) -> Option { + clip_find_multicam(clip) + } - /// The multicam node feeding `clip`, asserted present. - fn multicam_of_expect(fx: &Fixture, clip: &NodeRef) -> NodeRef { - multicam_of(fx, clip).expect("clip has a multicam") - } + /// The multicam node feeding `clip`, asserted present. + fn multicam_of_expect(fx: &Fixture, clip: &NodeRef) -> NodeRef { + multicam_of(fx, clip).expect("clip has a multicam") + } - /// The `current_in` value of a multicam node. - fn current_of(fx: &Fixture, mc: &NodeRef) -> i32 { - let p = fx.project.lock().unwrap(); - multicam_current_source(&p.graph, mc.id) - } + /// The `current_in` value of a multicam node. + fn current_of(fx: &Fixture, mc: &NodeRef) -> i32 { + let p = fx.project.lock().unwrap(); + multicam_current_source(&p.graph, mc.id) + } - /// Set the `current_in` of a multicam node (test setup for non-default - /// initial source). - fn set_current(fx: &Fixture, mc: &NodeRef, source: i32) { - let mut p = fx.project.lock().unwrap(); - multicam_set_current(&mut p.graph, mc.id, source); - } + /// Set the `current_in` of a multicam node (test setup for non-default + /// initial source). + fn set_current(fx: &Fixture, mc: &NodeRef, source: i32) { + let mut p = fx.project.lock().unwrap(); + multicam_set_current(&mut p.graph, mc.id, source); + } - /// The `sequence_type_in` value of a multicam node. - fn sequence_type_of(fx: &Fixture, mc: &NodeRef) -> i64 { - let p = fx.project.lock().unwrap(); - let v = p.graph.get(mc.id).unwrap().core.standard_value(SEQUENCE_TYPE_INPUT, -1); - match v { - NodeValue::Combo(i) => i, - other => panic!("sequence_type_in is {other:?}, expected Combo"), - } - } + /// The `sequence_type_in` value of a multicam node. + fn sequence_type_of(fx: &Fixture, mc: &NodeRef) -> i64 { + let p = fx.project.lock().unwrap(); + let v = p + .graph + .get(mc.id) + .unwrap() + .core + .standard_value(SEQUENCE_TYPE_INPUT, -1); + match v { + NodeValue::Combo(i) => i, + other => panic!("sequence_type_in is {other:?}, expected Combo"), + } + } - #[test] - fn labels_format_count() { - assert_eq!(enable_label(3), "Multi-Cam Enabled On 3 Clip(s)"); - assert_eq!(disable_label(2), "Multi-Cam Disabled On 2 Clip(s)"); - assert_eq!(SWITCH_LABEL, "Switched Multi-Camera Source"); - } + #[test] + fn labels_format_count() { + assert_eq!(enable_label(3), "Multi-Cam Enabled On 3 Clip(s)"); + assert_eq!(disable_label(2), "Multi-Cam Disabled On 2 Clip(s)"); + assert_eq!(SWITCH_LABEL, "Switched Multi-Camera Source"); + } - /// `clip_find_multicam` is None for a plain clip and Some after the - /// sequence is routed through a multicam (the C++ - /// `ClipBlock::find_multicam` parity). - #[test] - fn find_multicam_positive_and_negative() { - let fx = fixture(); - let clip = add_clip(&fx, Rational::new(0, 1), Rational::new(50, 1)); + /// `clip_find_multicam` is None for a plain clip and Some after the + /// sequence is routed through a multicam (the C++ + /// `ClipBlock::find_multicam` parity). + #[test] + fn find_multicam_positive_and_negative() { + let fx = fixture(); + let clip = add_clip(&fx, Rational::new(0, 1), Rational::new(50, 1)); - // No multicam anywhere: nothing found. - assert!(multicam_of(&fx, &clip).is_none()); + // No multicam anywhere: nothing found. + assert!(multicam_of(&fx, &clip).is_none()); - // A non-multicam source on the effect input is not reported either. - connect_sequence_to_clip(&fx, &clip); - assert!(multicam_of(&fx, &clip).is_none()); + // A non-multicam source on the effect input is not reported either. + connect_sequence_to_clip(&fx, &clip); + assert!(multicam_of(&fx, &clip).is_none()); - // After the enable command, the multicam feeds the clip. - let mut cmd = MultiCamEnableCommand::new(vec![clip.clone()], fx.seq.clone()); - cmd.redo(); - let mc = multicam_of_expect(&fx, &clip); - { - let p = fx.project.lock().unwrap(); - assert_eq!( - p.graph.get(mc.id).unwrap().behavior.type_id(), - "org.olivevideoeditor.Olive.multicam" - ); - } + // After the enable command, the multicam feeds the clip. + let mut cmd = MultiCamEnableCommand::new(vec![clip.clone()], fx.seq.clone()); + cmd.redo(); + let mc = multicam_of_expect(&fx, &clip); + { + let p = fx.project.lock().unwrap(); + assert_eq!( + p.graph.get(mc.id).unwrap().behavior.type_id(), + "org.olivevideoeditor.Olive.multicam" + ); + } - // After undo the multicam is gone again. - cmd.undo(); - assert!(multicam_of(&fx, &clip).is_none()); - } + // After undo the multicam is gone again. + cmd.undo(); + assert!(multicam_of(&fx, &clip).is_none()); + } - /// `multicam_enable` re-routes the sequence→clip edges through a fresh - /// multicam node and sets `sequence_type_in`; undo restores the exact - /// pre-enable edges. - #[test] - fn enable_reroutes_through_multicam_and_undo_restores() { - let fx = fixture(); - let clip = add_clip(&fx, Rational::new(0, 1), Rational::new(50, 1)); - connect_sequence_to_clip(&fx, &clip); + /// `multicam_enable` re-routes the sequence→clip edges through a fresh + /// multicam node and sets `sequence_type_in`; undo restores the exact + /// pre-enable edges. + #[test] + fn enable_reroutes_through_multicam_and_undo_restores() { + let fx = fixture(); + let clip = add_clip(&fx, Rational::new(0, 1), Rational::new(50, 1)); + connect_sequence_to_clip(&fx, &clip); - let mut cmd = MultiCamEnableCommand::new(vec![clip.clone()], fx.seq.clone()); - cmd.redo(); + let mut cmd = MultiCamEnableCommand::new(vec![clip.clone()], fx.seq.clone()); + cmd.redo(); - let mc = multicam_of_expect(&fx, &clip); - { - let p = fx.project.lock().unwrap(); - // The sequence no longer feeds the clip; the multicam does. - assert_eq!( - p.graph.connected_output(clip.id, clip_input::TEXTURE_INPUT, -1), - Some(mc.id) - ); - // The sequence feeds the multicam's sequence_in. - assert_eq!( - p.graph.connected_output(mc.id, SEQUENCE_INPUT, -1), - Some(fx.seq.id) - ); - } - // The type selector mirrors the video track type. - assert_eq!(sequence_type_of(&fx, &mc), 0); + let mc = multicam_of_expect(&fx, &clip); + { + let p = fx.project.lock().unwrap(); + // The sequence no longer feeds the clip; the multicam does. + assert_eq!( + p.graph + .connected_output(clip.id, clip_input::TEXTURE_INPUT, -1), + Some(mc.id) + ); + // The sequence feeds the multicam's sequence_in. + assert_eq!( + p.graph.connected_output(mc.id, SEQUENCE_INPUT, -1), + Some(fx.seq.id) + ); + } + // The type selector mirrors the video track type. + assert_eq!(sequence_type_of(&fx, &mc), 0); - cmd.undo(); - // Back to the pre-enable edge; the multicam node is gone. - assert!(multicam_of(&fx, &clip).is_none()); - { - let p = fx.project.lock().unwrap(); - assert_eq!( - p.graph.connected_output(clip.id, clip_input::TEXTURE_INPUT, -1), - Some(fx.seq.id) - ); - assert!(!p.graph.is_valid(mc.id)); - } + cmd.undo(); + // Back to the pre-enable edge; the multicam node is gone. + assert!(multicam_of(&fx, &clip).is_none()); + { + let p = fx.project.lock().unwrap(); + assert_eq!( + p.graph + .connected_output(clip.id, clip_input::TEXTURE_INPUT, -1), + Some(fx.seq.id) + ); + assert!(!p.graph.is_valid(mc.id)); + } - // Redo re-creates the multicam (fresh node id) and re-routes again. - cmd.redo(); - let mc2 = multicam_of_expect(&fx, &clip); - { - let p = fx.project.lock().unwrap(); - assert_eq!( - p.graph.connected_output(clip.id, clip_input::TEXTURE_INPUT, -1), - Some(mc2.id) - ); - } - } + // Redo re-creates the multicam (fresh node id) and re-routes again. + cmd.redo(); + let mc2 = multicam_of_expect(&fx, &clip); + { + let p = fx.project.lock().unwrap(); + assert_eq!( + p.graph + .connected_output(clip.id, clip_input::TEXTURE_INPUT, -1), + Some(mc2.id) + ); + } + } - /// `multicam_disable` re-wires the multicam's outputs back to the - /// sequence and removes the node; undo restores the enabled state. - #[test] - fn disable_round_trip() { - let fx = fixture(); - let clip = add_clip(&fx, Rational::new(0, 1), Rational::new(50, 1)); - connect_sequence_to_clip(&fx, &clip); + /// `multicam_disable` re-wires the multicam's outputs back to the + /// sequence and removes the node; undo restores the enabled state. + #[test] + fn disable_round_trip() { + let fx = fixture(); + let clip = add_clip(&fx, Rational::new(0, 1), Rational::new(50, 1)); + connect_sequence_to_clip(&fx, &clip); - let mut enable = MultiCamEnableCommand::new(vec![clip.clone()], fx.seq.clone()); - enable.redo(); - let mc = multicam_of_expect(&fx, &clip); + let mut enable = MultiCamEnableCommand::new(vec![clip.clone()], fx.seq.clone()); + enable.redo(); + let mc = multicam_of_expect(&fx, &clip); - let mut disable = MultiCamDisableCommand::new(vec![clip.clone()]); - disable.redo(); - { - let p = fx.project.lock().unwrap(); - assert_eq!( - p.graph.connected_output(clip.id, clip_input::TEXTURE_INPUT, -1), - Some(fx.seq.id) - ); - assert!(!p.graph.is_valid(mc.id)); - } + let mut disable = MultiCamDisableCommand::new(vec![clip.clone()]); + disable.redo(); + { + let p = fx.project.lock().unwrap(); + assert_eq!( + p.graph + .connected_output(clip.id, clip_input::TEXTURE_INPUT, -1), + Some(fx.seq.id) + ); + assert!(!p.graph.is_valid(mc.id)); + } - disable.undo(); - let restored_mc = multicam_of_expect(&fx, &clip); - { - let p = fx.project.lock().unwrap(); - assert_eq!( - p.graph.connected_output(clip.id, clip_input::TEXTURE_INPUT, -1), - Some(restored_mc.id) - ); - assert_eq!( - p.graph.connected_output(restored_mc.id, SEQUENCE_INPUT, -1), - Some(fx.seq.id) - ); - } - } + disable.undo(); + let restored_mc = multicam_of_expect(&fx, &clip); + { + let p = fx.project.lock().unwrap(); + assert_eq!( + p.graph + .connected_output(clip.id, clip_input::TEXTURE_INPUT, -1), + Some(restored_mc.id) + ); + assert_eq!( + p.graph.connected_output(restored_mc.id, SEQUENCE_INPUT, -1), + Some(fx.seq.id) + ); + } + } - /// `multicam_switch` without splitting sets the source on the clip and - /// its linked clips' multicams; a single undo restores every source. - #[test] - fn switch_no_split_updates_clip_and_links() { - let fx = fixture(); - let clip_a = add_clip(&fx, Rational::new(0, 1), Rational::new(50, 1)); - let clip_b = add_clip(&fx, Rational::new(50, 1), Rational::new(100, 1)); - connect_sequence_to_clip(&fx, &clip_a); - connect_sequence_to_clip(&fx, &clip_b); - { - let mut p = fx.project.lock().unwrap(); - p.graph.link(clip_a.id, clip_b.id); - } - let mut enable = - MultiCamEnableCommand::new(vec![clip_a.clone(), clip_b.clone()], fx.seq.clone()); - enable.redo(); - let mc_a = multicam_of_expect(&fx, &clip_a); - let mc_b = multicam_of_expect(&fx, &clip_b); - set_current(&fx, &mc_a, 1); - set_current(&fx, &mc_b, 1); + /// `multicam_switch` without splitting sets the source on the clip and + /// its linked clips' multicams; a single undo restores every source. + #[test] + fn switch_no_split_updates_clip_and_links() { + let fx = fixture(); + let clip_a = add_clip(&fx, Rational::new(0, 1), Rational::new(50, 1)); + let clip_b = add_clip(&fx, Rational::new(50, 1), Rational::new(100, 1)); + connect_sequence_to_clip(&fx, &clip_a); + connect_sequence_to_clip(&fx, &clip_b); + { + let mut p = fx.project.lock().unwrap(); + p.graph.link(clip_a.id, clip_b.id); + } + let mut enable = + MultiCamEnableCommand::new(vec![clip_a.clone(), clip_b.clone()], fx.seq.clone()); + enable.redo(); + let mc_a = multicam_of_expect(&fx, &clip_a); + let mc_b = multicam_of_expect(&fx, &clip_b); + set_current(&fx, &mc_a, 1); + set_current(&fx, &mc_b, 1); - // Split disabled: the switch writes the source directly. - let mut cmd = MultiCamSwitchCommand::new( - clip_a.clone(), - 2, - false, - Rational::new(30, 1), - ); - cmd.redo(); - assert_eq!(current_of(&fx, &mc_a), 2); - assert_eq!(current_of(&fx, &mc_b), 2); - // No split happened: still one block per track slot. - assert_eq!(track_block_count(&fx.track), 2); + // Split disabled: the switch writes the source directly. + let mut cmd = MultiCamSwitchCommand::new(clip_a.clone(), 2, false, Rational::new(30, 1)); + cmd.redo(); + assert_eq!(current_of(&fx, &mc_a), 2); + assert_eq!(current_of(&fx, &mc_b), 2); + // No split happened: still one block per track slot. + assert_eq!(track_block_count(&fx.track), 2); - cmd.undo(); - assert_eq!(current_of(&fx, &mc_a), 1); - assert_eq!(current_of(&fx, &mc_b), 1); - assert_eq!(track_block_count(&fx.track), 2); - } + cmd.undo(); + assert_eq!(current_of(&fx, &mc_a), 1); + assert_eq!(current_of(&fx, &mc_b), 1); + assert_eq!(track_block_count(&fx.track), 2); + } - /// `multicam_switch` with a playhead strictly inside the clip splits it - /// preserving links: the halves each own an independent multicam copy, - /// the post-playhead half switches source and its linked half follows, - /// the pre-playhead half keeps the old source. Undo restores the single - /// clip and its original source. - #[test] - fn switch_splits_and_copies_multicam() { - let fx = fixture(); - let clip_a = add_clip(&fx, Rational::new(0, 1), Rational::new(100, 1)); - let clip_b = add_clip(&fx, Rational::new(0, 1), Rational::new(100, 1)); - // Same track layout: clip_b on a second track slot is created after; - // link the two clips so the split is link-preserving. - { - let mut p = fx.project.lock().unwrap(); - p.graph.link(clip_a.id, clip_b.id); - } - connect_sequence_to_clip(&fx, &clip_a); - connect_sequence_to_clip(&fx, &clip_b); - let mut enable = - MultiCamEnableCommand::new(vec![clip_a.clone(), clip_b.clone()], fx.seq.clone()); - enable.redo(); - let mc_a = multicam_of_expect(&fx, &clip_a); - let mc_b = multicam_of_expect(&fx, &clip_b); - set_current(&fx, &mc_a, 1); - set_current(&fx, &mc_b, 1); + /// `multicam_switch` with a playhead strictly inside the clip splits it + /// preserving links: the halves each own an independent multicam copy, + /// the post-playhead half switches source and its linked half follows, + /// the pre-playhead half keeps the old source. Undo restores the single + /// clip and its original source. + #[test] + fn switch_splits_and_copies_multicam() { + let fx = fixture(); + let clip_a = add_clip(&fx, Rational::new(0, 1), Rational::new(100, 1)); + let clip_b = add_clip(&fx, Rational::new(0, 1), Rational::new(100, 1)); + // Same track layout: clip_b on a second track slot is created after; + // link the two clips so the split is link-preserving. + { + let mut p = fx.project.lock().unwrap(); + p.graph.link(clip_a.id, clip_b.id); + } + connect_sequence_to_clip(&fx, &clip_a); + connect_sequence_to_clip(&fx, &clip_b); + let mut enable = + MultiCamEnableCommand::new(vec![clip_a.clone(), clip_b.clone()], fx.seq.clone()); + enable.redo(); + let mc_a = multicam_of_expect(&fx, &clip_a); + let mc_b = multicam_of_expect(&fx, &clip_b); + set_current(&fx, &mc_a, 1); + set_current(&fx, &mc_b, 1); - // Switch at t=40 (strictly inside [0,100)) with split. - let mut cmd = MultiCamSwitchCommand::new( - clip_a.clone(), - 3, - true, - Rational::new(40, 1), - ); - cmd.redo(); + // Switch at t=40 (strictly inside [0,100)) with split. + let mut cmd = MultiCamSwitchCommand::new(clip_a.clone(), 3, true, Rational::new(40, 1)); + cmd.redo(); - // Two halves per clip (both linked clips split): 4 blocks total. - assert_eq!(track_block_count(&fx.track), 4); - let first = track_block_at(&fx.track, 0).unwrap(); - let second = track_block_at(&fx.track, 1).unwrap(); - assert!(same_block(&first, &clip_a), "original clip keeps the in half"); - assert_eq!(block_in(&first), Rational::new(0, 1)); - assert_eq!(block_out(&first), Rational::new(40, 1)); - assert_eq!(block_in(&second), Rational::new(40, 1)); - assert_eq!(block_out(&second), Rational::new(100, 1)); + // Two halves per clip (both linked clips split): 4 blocks total. + assert_eq!(track_block_count(&fx.track), 4); + let first = track_block_at(&fx.track, 0).unwrap(); + let second = track_block_at(&fx.track, 1).unwrap(); + assert!( + same_block(&first, &clip_a), + "original clip keeps the in half" + ); + assert_eq!(block_in(&first), Rational::new(0, 1)); + assert_eq!(block_out(&first), Rational::new(40, 1)); + assert_eq!(block_in(&second), Rational::new(40, 1)); + assert_eq!(block_out(&second), Rational::new(100, 1)); - // The pre-playhead half keeps its own multicam and old source; the - // post-playhead half has a distinct multicam copy with the new - // source. - let mc_first = multicam_of_expect(&fx, &first); - let mc_second = multicam_of_expect(&fx, &second); - assert_ne!(mc_first.id, mc_second.id, "independent multicam copies"); - assert_eq!(current_of(&fx, &mc_first), 1); - assert_eq!(current_of(&fx, &mc_second), 3); + // The pre-playhead half keeps its own multicam and old source; the + // post-playhead half has a distinct multicam copy with the new + // source. + let mc_first = multicam_of_expect(&fx, &first); + let mc_second = multicam_of_expect(&fx, &second); + assert_ne!(mc_first.id, mc_second.id, "independent multicam copies"); + assert_eq!(current_of(&fx, &mc_first), 1); + assert_eq!(current_of(&fx, &mc_second), 3); - // The linked clip's post-playhead half switched too. - let linked_second = { - let p = fx.project.lock().unwrap(); - // The second half of clip_a links to the second half of clip_b. - let links = p.graph.links_of(second.id); - links.iter().find(|id| **id != second.id).copied().unwrap() - }; - assert_ne!(linked_second, clip_a.id); - let mc_linked = multicam_of_expect(&fx, &NodeRef::new(fx.project.clone(), linked_second)); - assert_eq!(current_of(&fx, &mc_linked), 3); + // The linked clip's post-playhead half switched too. + let linked_second = { + let p = fx.project.lock().unwrap(); + // The second half of clip_a links to the second half of clip_b. + let links = p.graph.links_of(second.id); + links.iter().find(|id| **id != second.id).copied().unwrap() + }; + assert_ne!(linked_second, clip_a.id); + let mc_linked = multicam_of_expect(&fx, &NodeRef::new(fx.project.clone(), linked_second)); + assert_eq!(current_of(&fx, &mc_linked), 3); - // A single undo restores the two original clips and the old sources. - cmd.undo(); - assert_eq!(track_block_count(&fx.track), 2); - assert_eq!(block_length(&clip_a), Rational::new(100, 1)); - assert_eq!(current_of(&fx, &multicam_of_expect(&fx, &clip_a)), 1); - assert_eq!(current_of(&fx, &multicam_of_expect(&fx, &clip_b)), 1); + // A single undo restores the two original clips and the old sources. + cmd.undo(); + assert_eq!(track_block_count(&fx.track), 2); + assert_eq!(block_length(&clip_a), Rational::new(100, 1)); + assert_eq!(current_of(&fx, &multicam_of_expect(&fx, &clip_a)), 1); + assert_eq!(current_of(&fx, &multicam_of_expect(&fx, &clip_b)), 1); - // Redo re-splits (the copies re-attach identity-preserving) and - // re-applies the new source. - cmd.redo(); - assert_eq!(track_block_count(&fx.track), 4); - let second_again = track_block_at(&fx.track, 1).unwrap(); - let mc_second_again = multicam_of_expect(&fx, &second_again); - assert_eq!(current_of(&fx, &mc_second_again), 3); - let mc_first_again = multicam_of_expect(&fx, &track_block_at(&fx.track, 0).unwrap()); - assert_eq!(current_of(&fx, &mc_first_again), 1); - } + // Redo re-splits (the copies re-attach identity-preserving) and + // re-applies the new source. + cmd.redo(); + assert_eq!(track_block_count(&fx.track), 4); + let second_again = track_block_at(&fx.track, 1).unwrap(); + let mc_second_again = multicam_of_expect(&fx, &second_again); + assert_eq!(current_of(&fx, &mc_second_again), 3); + let mc_first_again = multicam_of_expect(&fx, &track_block_at(&fx.track, 0).unwrap()); + assert_eq!(current_of(&fx, &mc_first_again), 1); + } - /// The split's dependency-graph copy gives the two halves independent - /// multicam nodes: mutating one half's multicam does not affect the - /// other's. - #[test] - fn split_copies_dependency_graph_independently() { - let fx = fixture(); - let clip = add_clip(&fx, Rational::new(0, 1), Rational::new(100, 1)); - connect_sequence_to_clip(&fx, &clip); - let mut enable = MultiCamEnableCommand::new(vec![clip.clone()], fx.seq.clone()); - enable.redo(); - let mc_orig = multicam_of_expect(&fx, &clip); + /// The split's dependency-graph copy gives the two halves independent + /// multicam nodes: mutating one half's multicam does not affect the + /// other's. + #[test] + fn split_copies_dependency_graph_independently() { + let fx = fixture(); + let clip = add_clip(&fx, Rational::new(0, 1), Rational::new(100, 1)); + connect_sequence_to_clip(&fx, &clip); + let mut enable = MultiCamEnableCommand::new(vec![clip.clone()], fx.seq.clone()); + enable.redo(); + let mc_orig = multicam_of_expect(&fx, &clip); - // Split manually through the split command. - let mut split = crate::undosplit::BlockSplitCommand::new( - clip.clone(), - Rational::new(40, 1), - ); - split.prepare(); - split.redo(); - let second = split.new_block().unwrap(); + // Split manually through the split command. + let mut split = + crate::undosplit::BlockSplitCommand::new(clip.clone(), Rational::new(40, 1)); + split.prepare(); + split.redo(); + let second = split.new_block().unwrap(); - let mc_first = multicam_of_expect(&fx, &clip); - let mc_second = multicam_of_expect(&fx, &second); - assert_ne!(mc_first.id, mc_second.id); - assert_ne!(mc_orig.id, mc_second.id); + let mc_first = multicam_of_expect(&fx, &clip); + let mc_second = multicam_of_expect(&fx, &second); + assert_ne!(mc_first.id, mc_second.id); + assert_ne!(mc_orig.id, mc_second.id); - // Both copies kept the original source value. - assert_eq!(current_of(&fx, &mc_first), 0); - assert_eq!(current_of(&fx, &mc_second), 0); + // Both copies kept the original source value. + assert_eq!(current_of(&fx, &mc_first), 0); + assert_eq!(current_of(&fx, &mc_second), 0); - // Changing the second half's source leaves the first half's copy - // untouched. - let mut p = fx.project.lock().unwrap(); - multicam_set_current(&mut p.graph, mc_second.id, 5); - drop(p); - assert_eq!(current_of(&fx, &mc_first), 0); - assert_eq!(current_of(&fx, &mc_second), 5); - } + // Changing the second half's source leaves the first half's copy + // untouched. + let mut p = fx.project.lock().unwrap(); + multicam_set_current(&mut p.graph, mc_second.id, 5); + drop(p); + assert_eq!(current_of(&fx, &mc_first), 0); + assert_eq!(current_of(&fx, &mc_second), 5); + } - /// A switch with `split_clip` but a playhead outside the clip does not - /// split; it writes the source directly (C++ only splits when the - /// playhead lies strictly inside). - #[test] - fn switch_outside_playhead_does_not_split() { - let fx = fixture(); - let clip = add_clip(&fx, Rational::new(0, 1), Rational::new(50, 1)); - connect_sequence_to_clip(&fx, &clip); - let mut enable = MultiCamEnableCommand::new(vec![clip.clone()], fx.seq.clone()); - enable.redo(); - let mc = multicam_of_expect(&fx, &clip); + /// A switch with `split_clip` but a playhead outside the clip does not + /// split; it writes the source directly (C++ only splits when the + /// playhead lies strictly inside). + #[test] + fn switch_outside_playhead_does_not_split() { + let fx = fixture(); + let clip = add_clip(&fx, Rational::new(0, 1), Rational::new(50, 1)); + connect_sequence_to_clip(&fx, &clip); + let mut enable = MultiCamEnableCommand::new(vec![clip.clone()], fx.seq.clone()); + enable.redo(); + let mc = multicam_of_expect(&fx, &clip); - let mut cmd = MultiCamSwitchCommand::new( - clip.clone(), - 2, - true, - Rational::new(70, 1), // past the out point - ); - cmd.redo(); - assert_eq!(track_block_count(&fx.track), 1); - assert_eq!(current_of(&fx, &mc), 2); + let mut cmd = MultiCamSwitchCommand::new( + clip.clone(), + 2, + true, + Rational::new(70, 1), // past the out point + ); + cmd.redo(); + assert_eq!(track_block_count(&fx.track), 1); + assert_eq!(current_of(&fx, &mc), 2); - cmd.undo(); - assert_eq!(current_of(&fx, &mc), 0); - } + cmd.undo(); + assert_eq!(current_of(&fx, &mc), 0); + } - /// A switch on a clip without any multicam is a no-op (the C++ - /// `if (!node_) return;` guard) — no split, no writes. - #[test] - fn switch_without_multicam_is_noop() { - let fx = fixture(); - let clip = add_clip(&fx, Rational::new(0, 1), Rational::new(100, 1)); + /// A switch on a clip without any multicam is a no-op (the C++ + /// `if (!node_) return;` guard) — no split, no writes. + #[test] + fn switch_without_multicam_is_noop() { + let fx = fixture(); + let clip = add_clip(&fx, Rational::new(0, 1), Rational::new(100, 1)); - let mut cmd = MultiCamSwitchCommand::new( - clip.clone(), - 2, - true, - Rational::new(40, 1), - ); - cmd.redo(); - // No split happened. - assert_eq!(track_block_count(&fx.track), 1); - cmd.undo(); - assert_eq!(track_block_count(&fx.track), 1); - } + let mut cmd = MultiCamSwitchCommand::new(clip.clone(), 2, true, Rational::new(40, 1)); + cmd.redo(); + // No split happened. + assert_eq!(track_block_count(&fx.track), 1); + cmd.undo(); + assert_eq!(track_block_count(&fx.track), 1); + } - #[test] - fn disable_skips_clips_without_sequence_connection() { - let fx = fixture(); - let clip = add_clip(&fx, Rational::new(0, 1), Rational::new(50, 1)); - connect_sequence_to_clip(&fx, &clip); - let mut enable = MultiCamEnableCommand::new(vec![clip.clone()], fx.seq.clone()); - enable.redo(); - let mc = multicam_of_expect(&fx, &clip); + #[test] + fn disable_skips_clips_without_sequence_connection() { + let fx = fixture(); + let clip = add_clip(&fx, Rational::new(0, 1), Rational::new(50, 1)); + connect_sequence_to_clip(&fx, &clip); + let mut enable = MultiCamEnableCommand::new(vec![clip.clone()], fx.seq.clone()); + enable.redo(); + let mc = multicam_of_expect(&fx, &clip); - // A second clip without any multicam is a no-op for the disable. - let bare = add_clip(&fx, Rational::new(50, 1), Rational::new(100, 1)); - let mut disable = MultiCamDisableCommand::new(vec![clip.clone(), bare.clone()]); - disable.redo(); - { - let p = fx.project.lock().unwrap(); - assert_eq!( - p.graph.connected_output(clip.id, clip_input::TEXTURE_INPUT, -1), - Some(fx.seq.id) - ); - assert!(!p.graph.is_valid(mc.id)); - } - // The bare clip is untouched. - assert!(multicam_of(&fx, &bare).is_none()); - } + // A second clip without any multicam is a no-op for the disable. + let bare = add_clip(&fx, Rational::new(50, 1), Rational::new(100, 1)); + let mut disable = MultiCamDisableCommand::new(vec![clip.clone(), bare.clone()]); + disable.redo(); + { + let p = fx.project.lock().unwrap(); + assert_eq!( + p.graph + .connected_output(clip.id, clip_input::TEXTURE_INPUT, -1), + Some(fx.seq.id) + ); + assert!(!p.graph.is_valid(mc.id)); + } + // The bare clip is untouched. + assert!(multicam_of(&fx, &bare).is_none()); + } } diff --git a/crates/oak-timeline/src/workarea.rs b/crates/oak-timeline/src/workarea.rs index 597b5b95d..eaba6ffb2 100644 --- a/crates/oak-timeline/src/workarea.rs +++ b/crates/oak-timeline/src/workarea.rs @@ -111,9 +111,9 @@ impl WorkareaSetEnabledCommand { let wa = unsafe { get::>>(&points) }.cloned(); let old_enabled = wa .as_ref() - .and_then(|w| { + .map(|w| { let w = w.lock().unwrap_or_else(|e| e.into_inner()); - Some(w.enabled()) + w.enabled() }) .unwrap_or(false); Self { @@ -187,9 +187,9 @@ impl WorkareaSetRangeCommand { let wa = unsafe { get::>>(&workarea) }.cloned(); let old_range = wa .as_ref() - .and_then(|w| { + .map(|w| { let w = w.lock().unwrap_or_else(|e| e.into_inner()); - Some(*w.range()) + *w.range() }) .unwrap_or(range); Self::new_with_old_opt(wa, range, old_range) diff --git a/crates/oak-timeline/tests/handle_test.rs b/crates/oak-timeline/tests/handle_test.rs index 6f0012f87..3a1991f8a 100644 --- a/crates/oak-timeline/tests/handle_test.rs +++ b/crates/oak-timeline/tests/handle_test.rs @@ -73,7 +73,7 @@ fn make_owned_round_trips_value() { #[test] fn make_owned_shares_one_object() { let h1 = make_owned(0i32); - let h2 = h1.clone(); + let h2 = h1; // SAFETY: both handles box the same `Arc>` allocation. let a = unsafe { get::>>(&h1) }.unwrap().clone(); let b = unsafe { get::>>(&h2) }.unwrap().clone(); diff --git a/crates/oak-timeline/tests/marker_test.rs b/crates/oak-timeline/tests/marker_test.rs index 75d0813ab..2c418865a 100644 --- a/crates/oak-timeline/tests/marker_test.rs +++ b/crates/oak-timeline/tests/marker_test.rs @@ -104,7 +104,11 @@ fn marker_sibling_detection() { list.add_marker(a); list.add_marker(b); list.add_marker(other); - assert_eq!(list.get_marker_at_time(Rational::new(42, 1)).map(|m| m.name()), Some("a")); + assert_eq!( + list.get_marker_at_time(Rational::new(42, 1)) + .map(|m| m.name()), + Some("a") + ); assert!(list.get_marker_at_time(Rational::new(7, 1)).is_some()); assert!(list.get_marker_at_time(Rational::new(8, 1)).is_none()); } @@ -254,7 +258,7 @@ fn marker_list_closest_to_time() { fn marker_add_command_undo_before_redo() { let list_h = make_owned(TimelineMarkerList::new()); let mut cmd = MarkerAddCommand::new( - list_h.clone(), + list_h, TimeRange::new(Rational::new(1, 1), Rational::new(2, 1)), "m", 0, @@ -272,7 +276,7 @@ fn marker_remove_command_double_redo() { TimeRange::new(Rational::new(1, 1), Rational::new(2, 1)), "m", )); - let mut cmd = MarkerRemoveCommand::new(list_h.clone(), 0); + let mut cmd = MarkerRemoveCommand::new(list_h, 0); cmd.redo(); cmd.redo(); assert_eq!(list_of(&list_h).size(), 0); @@ -290,7 +294,7 @@ fn marker_commands_trait_dispatch() { )); let mut add = MarkerAddCommand::new( - list_h.clone(), + list_h, TimeRange::new(Rational::new(9, 1), Rational::new(10, 1)), "n", 0, @@ -300,26 +304,26 @@ fn marker_commands_trait_dispatch() { Command::undo(&mut add); assert_eq!(list_of(&list_h).size(), 1); - let mut remove = MarkerRemoveCommand::new(list_h.clone(), 0); + let mut remove = MarkerRemoveCommand::new(list_h, 0); Command::redo(&mut remove); assert_eq!(list_of(&list_h).size(), 0); Command::undo(&mut remove); assert_eq!(list_of(&list_h).size(), 1); - let mut color = MarkerChangeColorCommand::new(list_h.clone(), 0, 7); + let mut color = MarkerChangeColorCommand::new(list_h, 0, 7); Command::redo(&mut color); assert_eq!(list_of(&list_h).at(0).unwrap().color(), 7); Command::undo(&mut color); assert_eq!(list_of(&list_h).at(0).unwrap().color(), 1); - let mut name = MarkerChangeNameCommand::new(list_h.clone(), 0, "z"); + let mut name = MarkerChangeNameCommand::new(list_h, 0, "z"); Command::redo(&mut name); assert_eq!(list_of(&list_h).at(0).unwrap().name(), "z"); Command::undo(&mut name); assert_eq!(list_of(&list_h).at(0).unwrap().name(), "m"); let mut time = MarkerChangeTimeCommand::new( - list_h.clone(), + list_h, 0, TimeRange::new(Rational::new(50, 1), Rational::new(51, 1)), ); @@ -373,7 +377,7 @@ fn marker_list_resort_after_time_change() { fn marker_add_command_redo_undo() { let list_h = make_owned(TimelineMarkerList::new()); let mut cmd = MarkerAddCommand::new( - list_h.clone(), + list_h, TimeRange::new(Rational::new(1, 1), Rational::new(2, 1)), "m", 4, @@ -405,7 +409,7 @@ fn marker_remove_command_redo_undo() { "m", )); - let mut cmd = MarkerRemoveCommand::new(list_h.clone(), 0); + let mut cmd = MarkerRemoveCommand::new(list_h, 0); cmd.redo(); assert_eq!(list_of(&list_h).size(), 0); @@ -426,7 +430,7 @@ fn marker_change_color_command_redo_undo() { "m", )); - let mut cmd = MarkerChangeColorCommand::new(list_h.clone(), 0, 9); + let mut cmd = MarkerChangeColorCommand::new(list_h, 0, 9); cmd.redo(); assert_eq!(list_of(&list_h).at(0).unwrap().color(), 9); cmd.undo(); @@ -444,7 +448,7 @@ fn marker_change_name_command_redo_undo() { "m", )); - let mut cmd = MarkerChangeNameCommand::new(list_h.clone(), 0, "renamed"); + let mut cmd = MarkerChangeNameCommand::new(list_h, 0, "renamed"); cmd.redo(); assert_eq!(list_of(&list_h).at(0).unwrap().name(), "renamed"); cmd.undo(); @@ -463,7 +467,7 @@ fn marker_change_time_command_redo_undo() { )); let new_t = TimeRange::new(Rational::new(50, 1), Rational::new(51, 1)); - let mut cmd = MarkerChangeTimeCommand::new(list_h.clone(), 0, new_t); + let mut cmd = MarkerChangeTimeCommand::new(list_h, 0, new_t); cmd.redo(); assert_eq!( list_of(&list_h).at(0).unwrap().time().in_(), @@ -490,7 +494,7 @@ fn marker_commands_box_to_undo_command() { )); let mut cmd = MarkerAddCommand::new( - list_h.clone(), + list_h, TimeRange::new(Rational::new(5, 1), Rational::new(6, 1)), "n", 0, @@ -501,4 +505,3 @@ fn marker_commands_box_to_undo_command() { cmd.undo_now(); assert_eq!(list_of(&list_h).size(), 1); } - diff --git a/crates/oak-timeline/tests/workarea_test.rs b/crates/oak-timeline/tests/workarea_test.rs index f35e2559c..6a4053251 100644 --- a/crates/oak-timeline/tests/workarea_test.rs +++ b/crates/oak-timeline/tests/workarea_test.rs @@ -107,7 +107,7 @@ fn workarea_zero_out_resets_range() { #[test] fn workarea_set_enabled_command_redo_undo() { let wa_h = make_owned(TimelineWorkArea::new()); - let mut cmd = WorkareaSetEnabledCommand::new(wa_h.clone(), true); + let mut cmd = WorkareaSetEnabledCommand::new(wa_h, true); cmd.redo(); assert!(wa_of(&wa_h).enabled()); cmd.undo(); @@ -120,7 +120,7 @@ fn workarea_set_enabled_command_redo_undo() { fn workarea_set_range_command_redo_undo() { let wa_h = make_owned(TimelineWorkArea::new()); let new_range = TimeRange::new(Rational::new(10, 1), Rational::new(20, 1)); - let mut cmd = WorkareaSetRangeCommand::new(wa_h.clone(), new_range); + let mut cmd = WorkareaSetRangeCommand::new(wa_h, new_range); cmd.redo(); assert_eq!(*wa_of(&wa_h).range(), new_range); cmd.undo(); @@ -135,14 +135,14 @@ fn workarea_set_range_command_redo_undo() { #[test] fn workarea_commands_box_to_undo_command() { let wa_h = make_owned(TimelineWorkArea::new()); - let mut enabled_cmd = WorkareaSetEnabledCommand::new(wa_h.clone(), true).to_command(); + let mut enabled_cmd = WorkareaSetEnabledCommand::new(wa_h, true).to_command(); enabled_cmd.redo_now(); assert!(wa_of(&wa_h).enabled()); enabled_cmd.undo_now(); assert!(!wa_of(&wa_h).enabled()); let mut range_cmd = WorkareaSetRangeCommand::new( - wa_h.clone(), + wa_h, TimeRange::new(Rational::new(1, 1), Rational::new(2, 1)), ) .to_command(); @@ -158,14 +158,14 @@ fn workarea_commands_box_to_undo_command() { fn workarea_commands_trait_dispatch() { let wa_h = make_owned(TimelineWorkArea::new()); - let mut e = WorkareaSetEnabledCommand::new(wa_h.clone(), true); + let mut e = WorkareaSetEnabledCommand::new(wa_h, true); Command::redo(&mut e); assert!(wa_of(&wa_h).enabled()); Command::undo(&mut e); assert!(!wa_of(&wa_h).enabled()); let mut r = WorkareaSetRangeCommand::new( - wa_h.clone(), + wa_h, TimeRange::new(Rational::new(3, 1), Rational::new(4, 1)), ); Command::redo(&mut r); diff --git a/crates/oak-undo/src/undostack.rs b/crates/oak-undo/src/undostack.rs index 9dc229cfa..268eb7d54 100644 --- a/crates/oak-undo/src/undostack.rs +++ b/crates/oak-undo/src/undostack.rs @@ -144,7 +144,7 @@ impl UndoStack { && !self .commands .back() - .map_or(true, |entry| entry.command.is_empty()) + .is_none_or(|entry| entry.command.is_empty()) } /// Whether a redo is possible. diff --git a/crates/oak-undo/tests/edge_coverage_test.rs b/crates/oak-undo/tests/edge_coverage_test.rs index 0660f6ded..3f3287f10 100644 --- a/crates/oak-undo/tests/edge_coverage_test.rs +++ b/crates/oak-undo/tests/edge_coverage_test.rs @@ -20,7 +20,10 @@ //! (the former handle-level/refcount tests were removed together with //! the `CHandle` layer they exercised). -use oak_undo::error::{Error, OAKUNDO_E_FAILED, OAKUNDO_E_INVALID, OAKUNDO_E_NOMEM, OAKUNDO_E_NOT_FOUND, OAKUNDO_E_STATE}; +use oak_undo::error::{ + Error, OAKUNDO_E_FAILED, OAKUNDO_E_INVALID, OAKUNDO_E_NOMEM, OAKUNDO_E_NOT_FOUND, + OAKUNDO_E_STATE, +}; use oak_undo::undocommand::{MultiUndoCommand, UndoCommand}; use oak_undo::undostack::{EmptyCommand, UndoStack, K_MAX_UNDO_COMMANDS}; @@ -37,7 +40,7 @@ fn error_code_mapping_is_complete() { /// `Default` impls mirror `new()`. #[test] fn default_impls_match_new() { - let _empty = EmptyCommand::default(); + let _empty = EmptyCommand; let stack = UndoStack::default(); assert_eq!( stack.done_count(), @@ -71,7 +74,10 @@ fn push_pre_executed_clears_redo_tail_and_caps() { } stack.undo().unwrap(); stack.push_pre_executed(UndoCommand::from_closures(|| {}, || {}), name); - assert!(!stack.can_redo(), "push_pre_executed drops the redoable tail"); + assert!( + !stack.can_redo(), + "push_pre_executed drops the redoable tail" + ); // Fill past the cap with pre-executed commands: the oldest rows are // evicted and the count stays at K_MAX_UNDO_COMMANDS. diff --git a/crates/oak-undo/tests/undo_safe_test.rs b/crates/oak-undo/tests/undo_safe_test.rs index 3aa18a621..234139149 100644 --- a/crates/oak-undo/tests/undo_safe_test.rs +++ b/crates/oak-undo/tests/undo_safe_test.rs @@ -55,7 +55,10 @@ struct FreeProbe { impl Drop for FreeProbe { fn drop(&mut self) { - self.trace.lock().unwrap().push(format!("free:{}", self.name)); + self.trace + .lock() + .unwrap() + .push(format!("free:{}", self.name)); } } @@ -106,11 +109,14 @@ fn command_drop_frees_exactly_once() { name: "a", trace: trace.clone(), }; - let cmd = UndoCommand::from_closures(move || {}, move || { - // `probe` must be captured by value so that dropping the command - // frees it; dropping a reference (the previous body) did nothing. - let _ = &probe; - }); + let cmd = UndoCommand::from_closures( + move || {}, + move || { + // `probe` must be captured by value so that dropping the command + // frees it; dropping a reference (the previous body) did nothing. + let _ = &probe; + }, + ); drop(cmd); assert_eq!(events(&trace), vec!["free:a"]); @@ -141,8 +147,8 @@ fn multi_redo_undo_ordering() { assert_eq!(multi.multi_child_count(), 3); // Children are reachable and named. - assert_eq!(multi.multi_child(0).unwrap().is_done(), false); - assert_eq!(multi.multi_child(2).unwrap().is_done(), false); + assert!(!multi.multi_child(0).unwrap().is_done()); + assert!(!multi.multi_child(2).unwrap().is_done()); assert!(matches!(multi.multi_child(3), Err(Error::NotFound))); // Redo in insertion order; undo in reverse. @@ -182,7 +188,7 @@ fn stack_new_has_empty_bottom() { assert!(!s.can_undo()); assert!(!s.can_redo()); assert_eq!(s.command_name(0).unwrap(), "New/Open Project"); - assert_eq!(s.command_is_done(0).unwrap(), true); + assert!(s.command_is_done(0).unwrap()); } #[test] @@ -203,7 +209,7 @@ fn stack_push_undo_redo_branch() { assert!(s.can_redo()); assert_eq!(s.done_count(), 2); assert_eq!(s.command_name(2).unwrap(), "B"); // undone row is still labeled - assert_eq!(s.command_is_done(2).unwrap(), false); + assert!(!s.command_is_done(2).unwrap()); // Redo restores. s.redo().unwrap(); diff --git a/crates/oak-worker/src/worker.rs b/crates/oak-worker/src/worker.rs index c87e3a7e1..6e2641f78 100644 --- a/crates/oak-worker/src/worker.rs +++ b/crates/oak-worker/src/worker.rs @@ -63,9 +63,9 @@ use crate::framecache::FrameCache; use crate::ipc::{ error_message, write_message, AudioTicketSpec, BatchTicketSpec, FrameSlotMeta, FrameSlotPool, HandshakeMsg, LoadGraphMsg, PluginProgressMsg, RenderAudioBatchMsg, RenderBatchMsg, - RenderFrameMsg, SharedMemoryRegion, ShmMode, SLOT_FORMAT_AUDIO_F32, SLOT_FORMAT_BGRA8, TYPE_CANCEL, TYPE_HANDSHAKE, TYPE_LOAD_GRAPH, - TYPE_PLUGIN_CANCEL, TYPE_RENDER_AUDIO_BATCH, TYPE_RENDER_BATCH, TYPE_RENDER_FRAME, - TYPE_SHUTDOWN, + RenderFrameMsg, SharedMemoryRegion, ShmMode, SLOT_FORMAT_AUDIO_F32, SLOT_FORMAT_BGRA8, + TYPE_CANCEL, TYPE_HANDSHAKE, TYPE_LOAD_GRAPH, TYPE_PLUGIN_CANCEL, TYPE_RENDER_AUDIO_BATCH, + TYPE_RENDER_BATCH, TYPE_RENDER_FRAME, TYPE_SHUTDOWN, }; use crate::{log_error, PROTOCOL_VERSION}; @@ -376,7 +376,9 @@ impl WorkerSession { // non-fatal: the worker stays up for plugin-free graphs. log_error("runtime: scanning and registering OFX plugins"); if let Err(e) = oak_plugin::host::Host::global().cache.scan() { - log_error(&format!("runtime: OFX plugin scan failed ({e}); continuing")); + log_error(&format!( + "runtime: OFX plugin scan failed ({e}); continuing" + )); } let discovered = oak_plugin::host::Host::global().cache.count(); let registered = oak_plugin::node_factory::register_plugin_nodes(); @@ -574,10 +576,7 @@ impl WorkerSession { let content = match std::fs::read_to_string(&load.path) { Ok(c) => c, Err(e) => { - return Some(error_message( - &format!("graph file unreadable: {e}"), - None, - )) + return Some(error_message(&format!("graph file unreadable: {e}"), None)) } }; match oak_node::serializer::load_with_id_map(&content) { @@ -747,11 +746,7 @@ impl WorkerSession { /// `frame_ready` or `frame_failed` per ticket. Crashes the process /// deliberately when the crash-mode environment asks for it (the /// crash-isolation test hook). - fn handle_render_batch_stream( - &mut self, - line: &str, - out: &mut impl Write, - ) -> io::Result<()> { + fn handle_render_batch_stream(&mut self, line: &str, out: &mut impl Write) -> io::Result<()> { let batch: RenderBatchMsg = match serde_json::from_str(line) { Ok(b) => b, Err(_) => { @@ -785,10 +780,7 @@ impl WorkerSession { "slot": slot, }), Err(e) => { - log_error(&format!( - "render_batch: ticket {} failed: {e}", - spec.ticket - )); + log_error(&format!("render_batch: ticket {} failed: {e}", spec.ticket)); json!({ "type": crate::ipc::TYPE_FRAME_FAILED, "ticket": spec.ticket, @@ -912,7 +904,10 @@ impl WorkerSession { let batch: RenderAudioBatchMsg = match serde_json::from_str(line) { Ok(b) => b, Err(_) => { - return write_message(out, &error_message("invalid render_audio_batch message", None)) + return write_message( + out, + &error_message("invalid render_audio_batch message", None), + ) } }; @@ -984,10 +979,7 @@ impl WorkerSession { // SAFETY: `slot` was acquired above; the block is live shared memory // of the attached pool. let dst = unsafe { - std::slice::from_raw_parts_mut( - pool.slot_data(spec.slot as u32), - pool.slot_data_bytes(), - ) + std::slice::from_raw_parts_mut(pool.slot_data(spec.slot as u32), pool.slot_data_bytes()) }; eval::render_audio_samples_into(¶ms, &mut dst[..need]).map_err(|e| e.to_string())?; @@ -1037,15 +1029,15 @@ impl WorkerSession { out_time: Rational::new(c.out_num, c.out_den), media_in: Rational::new(c.media_in_num, c.media_in_den), gain: c.gain, - effects: c.effects.iter().map(crate::ipc::montage_effect_from).collect(), + effects: c + .effects + .iter() + .map(crate::ipc::montage_effect_from) + .collect(), }) .collect(); Ok(AudioTicketParams { - viewer: self - .graph - .as_ref() - .map(|g| g.project_copy) - .unwrap_or(0), + viewer: self.graph.as_ref().map(|g| g.project_copy).unwrap_or(0), range: oak_core::TimeRange::new(start, start + duration), sample_rate: spec.sample_rate, channel_layout: spec.channel_layout, @@ -1061,8 +1053,12 @@ impl WorkerSession { /// covers tickets in flight before that IPC lands, and mirrors /// [`handle_load_graph`]'s adopt step. fn sync_pipeline_color_from_graph(&mut self) -> bool { - let Some(graph) = &self.graph else { return false; }; - let Some(project) = &graph.project else { return false; }; + let Some(graph) = &self.graph else { + return false; + }; + let Some(project) = &graph.project else { + return false; + }; let (working, output) = { let guard = project.lock().unwrap_or_else(|e| e.into_inner()); (guard.working_color_space(), guard.output_color_spec()) @@ -1077,7 +1073,11 @@ impl WorkerSession { } /// Render `spec` into the slot's data block and fill the slot meta. - fn render_spec_pixels(&mut self, spec: &BatchTicketSpec, pool: &FrameSlotPool) -> Result<(), String> { + fn render_spec_pixels( + &mut self, + spec: &BatchTicketSpec, + pool: &FrameSlotPool, + ) -> Result<(), String> { // Derive the pipeline colors from the loaded project on every render: // eval's decode linearization and the output node below read the // process global, which the resync (load_graph) keeps current — but a @@ -1136,7 +1136,14 @@ impl WorkerSession { match &cached { Some(c) => dst[..f32_need].copy_from_slice(&c[..f32_need]), None => { - render_f32_into(spec, ¶ms, &self.graph, time, (w, h), &mut dst[..dst_need])?; + render_f32_into( + spec, + ¶ms, + &self.graph, + time, + (w, h), + &mut dst[..dst_need], + )?; self.frame_cache.insert(key, dst[..f32_need].to_vec()); } } @@ -1207,7 +1214,11 @@ impl WorkerSession { out_time: Rational::new(c.out_num, c.out_den), media_in: Rational::new(c.media_in_num, c.media_in_den), gain: c.gain, - effects: c.effects.iter().map(crate::ipc::montage_effect_from).collect(), + effects: c + .effects + .iter() + .map(crate::ipc::montage_effect_from) + .collect(), }) .collect(); let adjustments: Vec = spec @@ -1219,10 +1230,7 @@ impl WorkerSession { viewer: if spec.viewer_node != 0 { spec.viewer_node } else { - self.graph - .as_ref() - .map(|g| g.project_copy) - .unwrap_or(0) + self.graph.as_ref().map(|g| g.project_copy).unwrap_or(0) }, project: spec.project_key.clone(), time, @@ -1279,7 +1287,13 @@ fn render_f32_into( .or_else(|| oak_node::id::NodeId::from_identity(spec.viewer_node)); match viewer_id { Some(viewer_id) => { - let rendered = eval::render_graph_frame(project, viewer_id, time, (w, h), PixelFormat::F32); + let rendered = eval::render_graph_frame( + project, + viewer_id, + time, + (w, h), + PixelFormat::F32, + ); // M2: the graph renders all-GPU in-process; the worker's // wire format is a CPU shm slot, so this is the explicit // readback boundary of the process backend. @@ -1300,7 +1314,7 @@ fn render_f32_into( } }; if let Some(frame) = frame { - let src_stride = frame.linesize_bytes() as usize; + let src_stride = frame.linesize_bytes(); let row_bytes = (w as usize) * 16; if frame.data.len() < src_stride * (h as usize) || dst.len() < row_bytes * (h as usize) @@ -1366,7 +1380,9 @@ fn render_f32_into( fn warn_graph_fallback(viewer: u64, why: &str) { static WARNED: AtomicBool = AtomicBool::new(false); if !WARNED.swap(true, Ordering::Relaxed) { - log_error(&format!("graph-mode ticket viewer {viewer}: {why}; falling back to montage")); + log_error(&format!( + "graph-mode ticket viewer {viewer}: {why}; falling back to montage" + )); } } @@ -1895,9 +1911,9 @@ mod tests { ); assert!(resp.is_none(), "unexpected error: {resp:?}"); assert_eq!( - oak_core::color::pipeline_working_space(), - WorkingColorSpace::AcesCg, - "load_graph must adopt the snapshot's working space" + oak_core::color::pipeline_working_space(), + WorkingColorSpace::AcesCg, + "load_graph must adopt the snapshot's working space" ); let _ = std::fs::remove_file(&path); // Restore the default global so parallel tests are not disturbed. @@ -1930,17 +1946,17 @@ mod tests { // reaches this worker later. A render ticket in that window must not // run under the stale colors. oak_core::color::set_pipeline_color_settings( - WorkingColorSpace::SrgbLegacy, - oak_core::color::pipeline_output_spec(), + WorkingColorSpace::SrgbLegacy, + oak_core::color::pipeline_output_spec(), ); assert!( s.sync_pipeline_color_from_graph(), "stale global must be refreshed from the loaded project" ); assert_eq!( - oak_core::color::pipeline_working_space(), - WorkingColorSpace::AcesCg, - "sync must restore the snapshot's working space" + oak_core::color::pipeline_working_space(), + WorkingColorSpace::AcesCg, + "sync must restore the snapshot's working space" ); assert!( !s.sync_pipeline_color_from_graph(), @@ -1962,8 +1978,8 @@ mod tests { // the code (sRGB EOTF) into ACEScg and then encodes for output, so // the mid-gray comes back near 0.5. use oak_core::colormath::{ - acescg_to_output_bytes, decode_to_acescg_bytes, srgb_oetf, OutputColorSpec, OutputGamut, - OutputTransfer, SourcePrimaries, SourceTransfer, WorkingColorSpace, + acescg_to_output_bytes, decode_to_acescg_bytes, srgb_oetf, OutputColorSpec, + OutputGamut, OutputTransfer, SourcePrimaries, SourceTransfer, WorkingColorSpace, }; let spec = OutputColorSpec { gamut: OutputGamut::Srgb, @@ -1985,7 +2001,12 @@ mod tests { // Correct path: decode (sRGB EOTF) → ACEScg → output encode. let mut bytes = [0.5f32, 0.5, 0.5, 1.0].map(f32::to_le_bytes).concat(); - decode_to_acescg_bytes(&mut bytes, 1, SourcePrimaries::Bt709, SourceTransfer::SdrGamma); + decode_to_acescg_bytes( + &mut bytes, + 1, + SourcePrimaries::Bt709, + SourceTransfer::SdrGamma, + ); acescg_to_output_bytes(&mut bytes, 1, spec); let out = f32::from_le_bytes(bytes[0..4].try_into().unwrap()); assert!( @@ -2040,7 +2061,9 @@ mod tests { assert_eq!(meta.format, PixelFormat::F32 as i32); assert_eq!(meta.data_size, 4 * 4 * 16); // Generated frame: transparent black. - let data = unsafe { std::slice::from_raw_parts(parent_pool.slot_data_const(consumed), 4 * 4 * 16) }; + let data = unsafe { + std::slice::from_raw_parts(parent_pool.slot_data_const(consumed), 4 * 4 * 16) + }; assert!(data.iter().all(|&b| b == 0)); unsafe { parent_pool.release(consumed) }; } @@ -2167,12 +2190,15 @@ mod tests { assert_eq!(meta.channel_count, 2); assert_eq!(meta.linesize, 2 * 4); assert_eq!(meta.data_size, slot_bytes as i32); - let data = - unsafe { std::slice::from_raw_parts(parent_pool.slot_data_const(consumed), slot_bytes) }; + let data = unsafe { + std::slice::from_raw_parts(parent_pool.slot_data_const(consumed), slot_bytes) + }; assert!(data.iter().all(|&b| b == 0), "empty montage is silence"); // The samples parse back as 2000 stereo frames. let parsed: Vec = data - .chunks_exact(4) + .as_chunks::<4>() + .0 + .iter() .map(|c| f32::from_le_bytes([c[0], c[1], c[2], c[3]])) .collect(); assert_eq!(parsed.len(), 2000 * 2);