The Cmd+K split ran the plain per-block split for every selected clip,
so an originally linked audio/video pair came out with its front halves
linked but the two rear halves unlinked — dragging one rear clip left
its mate behind and Link/Relink could not repair it.
Split the whole target set as ONE BlockSplitPreservingLinksCommand (the
same command the multicam path uses): any pair of originally linked
blocks split at the same time gets its new halves linked too. Adds a
gpui test asserting front and rear halves of a dropped A/V clip stay
linked after split_at_playhead.
Real plugins (CImg ChromaKeyerOFX, AddOFX) failed the render action with
kOfxStatFailed / MissingHostFeature and painted the magenta failure frame:
- images lacked the mandatory ImageBase properties (OfxPropType,
PixelAspectRatio, PreMultiplication, Field, RenderScale); the ofxs
ImageBase constructor throws on the missing/invalid strong reads
- PreMultiplication used the made-up string "OfxImagePreMultiplied";
kOfxImagePreMultiplied is actually "OfxImageAlphaPremultiplied", the
only value mapStrToPreMultiplicationEnum accepts (lldb __cxa_throw
backtrace pinpointed this)
- RenderWindow is Int x4 per ofxsPropertyValidation, not Double x4
- field strings use the real constant "OfxFieldNone"
- clips define OfxImageClipPropConnected (isConnected is a strong read;
optional mask clips blew up without it)
- choice params predefine empty ChoiceEnum / ChoiceLabelOption arrays
- isIdentity failure is no longer fatal (the C++ plugin renderer never
calls it; plugins that error on it simply render normally)
- property suite coerces Int <-> Double on reads (the CImg framework
reads the render window with propGetIntN against a Double store)
- in-args carry NatronOfxPropNativeOverlays=0 for the Natron framework
- plugin jobs pass a GL-kind marker so GL-only plugins take the real
gl_bridge offscreen path instead of the CPU MissingHostFeature path
- trace-gated [ofx] diagnostics for property misses and suite calls
Verified with new smoke tests that render the real AddOFX and
ChromaKeyerOFX plugins through the executor and assert the output is
not the purple failure frame.
Adding an effect to a clip did nothing: the sequence render is
flattened into a montage (decode + composite), and MontageClip carried
no effect data at all.
- MontageClip gains an ordered effect stack (type id / enabled /
effect input / parameter values); protocol v2 carries it as an
additive wire field (older peers default to an empty stack).
- renderops::video_montage fills the stack from the effect chain
(the footage source node — the chain end without an effect input —
is dropped; the montage decodes the footage itself). Export
(oak-task) and the multicam single-track montage fill it too.
- The worker applies the stack between decode and composite: built-in
Opacity gets a CPU evaluator (C++ opacity.frag parity — whole vec4,
alpha included, unity pass-through); everything else dispatches as an
OFX plugin job through a new instance-factory slot (oak-plugin
lazily creates + caches one instance per identifier per render
process) with the montage's parameters injected. Disabled effects
bypass (the C++ traverser pushes the effect input through). Unknown
types warn once per type id and pass through — no silent no-ops.
Not covered (explicitly): Transform/Crop and the other ~30 built-in
effects have no CPU evaluator in oak-render (they pass through with a
warning), keyframed parameter animation, audio effect chains, and the
CLI's simplified montage.
Acceptance: a real 50% Opacity on real media quarters the rendered
pixels both in-process (renderops test) and through a real worker
process over IPC + shared memory (procpool_integration test);
disabling restores the plain render byte-for-byte.
Link toggle rule refinement (on top of 1ad2d71c8): the toggle now
unlinks only when the selection is FULLY linked internally — the C++
crude "any member has ANY link" check made split halves (which inherit
the original clip's A/V links via BlockSplitPreservingLinksCommand)
impossible to link to each other. Test: link_unlink_toggles_the_
selection_links covers toggle/undo/split-link.
Node editor: only stream inputs (texture/samples) become ports — OFX
plugins declare every parameter as an input, which buried the card
under internal params (the garbled NatronOfxParamProcess* wall). Secret
inputs stay hidden; port labels use the input's display name (the OFX
param label); edges resolve ports by input id, not by label.
编辑 > 链接/重新链接 (Cmd+L) was an unhandled stub
(PanelCommandHandler::toggle_links returned false), so linking clips
never created graph links and linked drags never happened.
- graphops::set_clips_linked: one undoable entry toggling every pair of
the selection; the undo restores the exact prior internal topology.
- RealEngine::toggle_clip_links: fully-linked selections unlink,
otherwise the selection links together (the C++ crude "any member has
ANY link" rule would have made split halves — which inherit the
original clip's A/V links — impossible to link to each other).
- TimelinePanel overrides toggle_links, routing the timeline selection.
- MockEngine stores demo link pairs.
- Test: link_unlink_toggles_the_selection_links — dropped A/V pair
toggles off/on with undo, and split halves link manually.
- docs/zh/plans: finished plans move to completed/ (the RIIR series, the
event-bridge and dependency plans, the v04 manual test plan).
- New design docs: the external (functional) plugin system
(process-isolated, JSON-RPC/shm) and its protocol.
- ai-agent-design refreshed; README pointers follow the moves.
Touchpad fixes from user testing:
- SpinBox: the wheel only steps a FOCUSED field — hover-wheel is inert,
so two-finger scrolling across a dialog no longer drifts values.
- SpinBox: direct numeric entry — double-click opens the editor (the
app's text input; commit on blur, Escape cancels), typing a digit on
the focused field starts editing with that character. Previously the
field accepted no text at all.
- ComboBox: a click anywhere outside closes the open popup
(on_mouse_down_out).
- gpui bump: the timeline header-occlusion fix.
- Help > About Oak… opens a real dialog (was a placeholder print): name
+ version, the GPL-3.0 line, the Olive fork notice and the thanks to
Enzo GD (Community Promoter).
- Modal (gpui_widgets, submodule bump): a title-row close button on
every dialog and opt-in backdrop dismissal (dismiss_on_mask) — Help >
Search Actions and About use it, so they close on an outside click
(previously a buttonless modal could only be closed with Escape).
- Preferences general tab grows two live settings: the cache-ahead
pre-render window (PlaybackPreRenderFrames, consumed by the preview
scheduler every playback tick) and the library storage backend
(Storage/Backend sqlite/pg + Storage/PgUrl connection string, read by
oak-storage's write-through when a project binds; the pg URL field
shows only for PostgreSQL and commits on dialog close). The C++
auto-recovery toggle is skipped on purpose: the Rust snapshot loop
has no disable semantic, so the switch would be dead.
i18n keys for all eight packs; new tests cover the about modal and the
preferences writes.
The popup was a plain absolute child: dialog rows after the combo drew
OVER it, making the list look transparent (its text overlapped the
content behind). deferred() paints it after all ancestors.
The menu item was a placeholder print; it now opens a real dialog (the
C++ ProjectPropertiesDialog):
- Per-project OCIO config override with a 浏览… picker: validated on OK
(an invalid config keeps the dialog open with the error shown, like
the C++ accept()), persisted in the project settings, applied to the
display color pipeline on accept and on project open, and reverted to
the app default when the project closes. oak-render gains
set_up_default_config_from for the explicit-path load.
- Disk-cache location (default / alongside the project / custom path):
persisted through the OVE serializer (cachesetting/customcachepath
round-trip the settings map, clamped on load) and honored by the
thumbnail writer — the first live consumer of Project::cache_path.
- PathField gains an enabled state (the custom path field follows the
combo selection).
The C++ color tab's Default Input Color Space and Reference Space
combos are intentionally absent: the Rust render pipeline has no
consumer for them today (decode performs no input transfer conversion),
so showing them would be dead settings.
Tests: dialog opens, OK applies the cache location, an invalid OCIO
config keeps the dialog open with the error row. i18n keys for all
eight packs.
convert_bgra8 applied packed u8 pixels to the default (F32-finalized)
OCIO CPU processor, which rejects them with a bit-depth mismatch; the
callers swallow the error, so the display-ICC transform was silently
inert on the shm preview path. ocio-rs exposes no Uint8-finalized CPU
processor, so the conversion now detours through F32. Verified against
the machine's actual display profile with the new
display_icc_bgra8_never_outputs_black test (OAK_DISPLAY_ICC).
Also:
- procpool_integration: audio tickets are Seek priority and claimable
by any worker now, so the shard-spread assertion goes (rendering on a
live worker is what matters).
- OAK_DEBUG_VIEWER=1: the program viewer logs frame pushes and dumps
the displayed frame to /tmp/oak_viewer_frame.ppm (the black-screen
investigation tooling).
RealEngine::set_track_height held the project graph lock while calling
graphops::set_track_height, which locks the same mutex — an instant
same-thread deadlock with any sequence open (sampled live: main thread
in nudge_track_height -> set_track_height -> Mutex::lock, no other
lock-holder thread). Collect the track ids under the lock, drop it,
then apply the heights.
Also:
- Regression test set_track_height_does_not_self_deadlock.
- save_load_roundtrips_a_timeline_clip: real footage through the OVE
serializer (also the fixture generator for viewer debugging).
Three compounding bugs froze the UI when dragging the playhead after
playback:
1. Self-deadlock on preview_windows: supply_preview_window /
cancel_preview_windows / cancel_preview_window called
cancel_preview_sequence / cancel_preview_frame while HOLDING the
preview_windows mutex; those calls fire completions synchronously and
the completion locks preview_windows again. Caught by sampling the
hung process: UI thread in cancel_preview_sequence -> TicketSlot::
finish -> completion -> Mutex::lock. Cancels/releases are now
collected under the lock and fired after it is dropped.
2. Seek starvation by shard pinning: a Seek request's scheduler frame
is its ticket id, pinning it to worker (id mod W). The playback
window fills every worker's slots (window slots are only released by
UI-thread consumption), so the seek's pinned worker could have zero
free slots while the UI thread blocked on the seek — permanent
starvation. Seeks (interactive frame / real-time audio) are now
claimable by ANY worker; the no-stealing shard rule stays for
Playback frames (adjacent frames finish together).
3. No per-worker reserve: the global preview_window_capacity reserve is
pool-wide accounting, but exhaustion happens per worker. Playback /
Background claims now leave one credit unused per worker; Seek
claims may use the last slot (they complete on the worker without
UI involvement).
Also: RealEngine::drop cancels the preview windows — ShmFrameRef has no
self-release, so every dropped engine leaked its window's slots from
the shared pool, starving later windows (surfaced as the full-suite
playback_window_supplies_playhead_frames failure once the new probe
test shifted the test schedule). new_sequence_has_default_two_video_
two_audio_tracks now takes the engine test lock (it asserts on the
global undo stack; running lock-free raced parallel undo histories).
New regression probe interactive_seek_renders_without_hanging: play 30
ticks (window fills and holds shm slots), pause, seek, synchronously
render — must not hang. Scheduler tests updated for the reserve and
seek-any-worker contract. OAK_DEBUG_DISPATCH=1 enables the dispatcher
starvation/pool diagnostics used to track this down.
Every test_project() node packs the same NodeId::identity (a fresh
graph's first node), so two tests running in parallel collide in the
process-wide registry and one resolves the other's live entry —
identity_project_dropped's dangling-upgrade assert then fails
intermittently on CI. Both identity tests now share a mutex.
ci (Windows): the runner's msys2 shell starts as the base MSYS
environment (MSYSTEM=MSYS), which install-deps.sh rejects. Set the
job-level MSYSTEM=UCRT64 env and prepend /ucrt64/bin to PATH in every
Windows step, so pacman installs and the toolchain resolve against the
mingw-w64-ucrt-x86_64 packages.
fixtures: the restructured workspace moved the media fixtures into
crates/oak-app/tests; the remaining references pointed at the old
repo-root tests/ — oak-codec realmedia_tests + hwdecode, oak-node
serializer golden, oak-worker procpool integration. All point at
../oak-app/tests now (oak-app's own tests/demo.mp4 references were
already correct after the move).
Three real-engine throughput loops (thumbnail pipeline, full-res fill
behind the proxy, playback window supply) failed on machine slowness:
their pass/fail was a wall-clock Instant deadline, so a loaded machine
broke them for speed, not for a broken pipeline. Each loop now counts
engine pumps — machine-speed independent — and asserts the condition
within a bounded number of pumps. The two single-frame worker channel
receives keep a generous 60 s recv_timeout (a one-shot bounded
operation, not a throughput loop).
oak-cli: the integration fixtures moved with the app crate during the
workspace restructure; point the fixture helpers at
../oak-app/tests instead of the (now empty) repo-root tests/.
playback_display_tracks_the_playhead used a 30 s wall-clock deadline as
its pass/fail criterion, so a slow or loaded machine failed the test
for machine speed, not for a broken pipeline — spurious, unrelated to
any race. The loop now terminates on playback-clock progress (playhead
>= 120, ~5 s of playback; the transport advances independently of
render speed, so termination is guaranteed) and the only judgment is
the tracking invariant at that point. No Instant::now() remains.
Verified green on a heavily loaded machine in 84 s.
oakui/component/controls.rs owns the effect/editor controls instead of
reaching into gpui_widgets:
- Slider: horizontal 1:1-cursor drag (the gpui_widgets slider only
responded to vertical cursor movement, so horizontal drags did
nothing), wheel, middle-click reset, arrow keys (Shift = 1/10 step,
Home/End = range ends), and double-click numeric editing (app text
input; commit on blur, Escape cancels). A gesture emits
ValueChanged exactly once on drop — one undoable edit per drag, so
per-mouse-move edits + frame invalidation can no longer freeze the
UI thread.
- CheckBox: click / Space / Enter toggle, request-only contract
(Toggled + set_state), theme colors, optional label.
- ComboBox: click opens a popup list, Up/Down navigate (open) or
change the selection (closed), Enter commits/opens, Escape closes.
- SpinBox: wheel + Up/Down (Shift fine) + Home/End.
The params panel, timeline and dialogs import from the component
module. App-move fallout: i18n packs resolve from the repo-root
assets/i18n via CARGO_MANIFEST_DIR (crates/oak-app is not the repo
root anymore), and the render tests' worker-binary paths point at
../../target/debug/oak-worker.
All crates take the oak-* kebab-case naming (oak-audio, oak-codec,
oak-common, oak-core, oak-ffmpeg-link, oak-node, oak-otio, oak-plugin,
oak-render, oak-storage, oak-task, oak-timeline, oak-undo), with the
lib identifiers rewritten (oakrender:: -> oak_render::, oakcore_rs:: ->
oak_core::, ...) across all 226 referencing files.
The GUI application moves from the workspace root into
crates/oak-app/: src/, build.rs (paths fixed for the new location) and
tests/ travel with it, the root Cargo.toml becomes workspace-only
([workspace] + workspace.package + profiles), and the app package
inherits the workspace version. The screenshots example becomes a
standalone crate examples/simple_player/ with its own Cargo.toml.
Every crate now inherits the single workspace version
(version.workspace = true), and the workflows' crate paths and the
build docs follow the renames.
Validated with a clean cargo check --workspace.
preview_window_capacity used the *configured* worker count, so a
window opened while workers were still handshaking (or after a crash)
could claim every slot of the smaller live pool — the synchronous
render ticket then never gets a free slot, and since the slot-releasing
cleanup runs on the same UI thread that is blocked in TicketArena::wait,
playback deadlocks permanently. Intermittent on Linux CI (the
playback_display_tracks_the_playhead hang, caught by the new test
watchdog): depends on how many workers had handshaken when playback
started. Count only Alive workers (fall back to the configured count
while none are alive, keeping the existing unit test semantics).
oakstorage: the sqlite URI parse tests used /tmp/lib.db, which is not
absolute on Windows, so parse_target's is_absolute check rejected it.
Pick the absolute path per platform (C:/tmp/lib.db on Windows).
ci (Linux): wrap the test step in a 1500 s watchdog — a deadlocked
test prints nothing and never fails; on timeout the watchdog dumps
every test/worker process's thread stacks with gdb and then kills the
suite. (One such hang already ate a run; the previous green run needed
~4 min.)
ci+cd: Swatinem/rust-cache gains cache-on-failure everywhere, so a
red run still saves its compile cache (the actions/cache FFmpeg cache
already saves in its post phase regardless of outcome).
examples/screenshot.rs: migrate to language_code()/set_language_code
(the Language enum is gone); only CI's example build caught it — local
--lib runs never compile examples.
oakcodec tests: build path expectations with Path::join instead of
'/'-joined literals — production uses platform-native separators, so
the derivation assertions failed on Windows ("dir\img007.jpg" vs
"dir/img007.jpg"). Behaviour unchanged; the tests were never reached
on Windows before (earlier failures aborted the run first).
oakui::ofx interact test: poll up to 5s for the destroy marker record
before asserting. The active-interact slot is process-global; a
concurrent viewer frame sync from another test's real engine can take
the interact out of the slot and be preempted between take and
destroy, so the plugin's destroy record occasionally lands a few
milliseconds after this test removed the marker env var (Linux CI:
"lifecycle actions missing: [...]" with everything but destroy
present).
i18n: test_lock() and lang_test_lock() used to be TWO different static
mutexes, so the i18n tests and the ~30 app/actions/dialogs tests that
mutate the language global never excluded each other. Windows thread
scheduling exposed the race: tr_falls_back_to_english_then_the_key got
the English value because another test flipped the language mid-assert.
Both entry points now lock the same mutex.
oak_test_plugin.c: on Windows the plugin DLL has its own CRT
environment block, so getenv() never sees what the host's
std::env::set_var set via SetEnvironmentVariableW — the interact
lifecycle test's marker file stayed empty ("lifecycle actions
missing: []"). Read the marker path through GetEnvironmentVariableA
on _WIN32.
The fixture plugin binary was copied into Contents/Linux-x86-64 under
the extension-less name "plugin" on every non-macOS platform. On
Windows the host never loads it: LoadLibrary appends .dll to
extension-less module names, so the scan found the bundle but produced
no plugin — and because the (passing) draw-overlay test scans first,
the path dedupe then hid the failure from the lifecycle test, which
died with "interact variant instance: NotFound". Use Contents/Win64
and plugin.dll on Windows in both bundle assembly sites.
oakrender/build.rs: -Wl,-export_dynamic is the macOS spelling. Since
Rust 1.90 x86_64-unknown-linux-gnu links with rust-lld by default, and
lld parses the single-dash form as '-e xport_dynamic', every oakrender
integration test binary was linked with NO entry point and died with
SIGSEGV inside ld.so's dl_main (jumping to the image base) before
printing anything — the copier_test CI failure. Emit the flag on macOS
only.
oaknode timeformat: value_localtime_flag_routes_to_localtime_r called
localtime_r/gmtime_r directly, which do not exist on Windows. Factor
the cfg-gated FFI (localtime_r/gmtime_r vs _localtime64_s/_gmtime64_s)
into break_down_time() and use it from both value() and the test.
oakcodec: gate find_ffmpeg_searches_path to unix (chmod 0755 + shebang
fixture) and make find_ffmpeg_missing_returns_empty assert absoluteness
instead of a '/' prefix so the tests compile and pass on Windows.
ci (Windows): export RUSTFLAGS=-C link-args=-lmsvcrt in the build and
test steps. mingw-w64 (Nov 2025) forwards _assert to __msvcrt_assert
inside libmingwex.a, and rustc's link order leaves -lmingwex last, so
binaries that pull _assert.o (oakcommon's real_ocio test) fail to link;
a trailing -lmsvcrt re-scans the CRT import lib afterwards.
ci (Linux): copier_test dies inside ld.so before printing anything.
Replace the LD_DEBUG probe with stronger forensics: exported dynsyms
(interposition suspects), strace tail, valgrind tail, and siginfo
(si_code/si_addr) from the gdb run.
oakaudio: drop cpal's `asio` feature. asio-sys needs the proprietary
Steinberg ASIO SDK at link time (undefined ASIOGetSamplePosition etc.
on the GNU toolchain); WASAPI remains the Windows backend.
ci: the failure-only gdb step passed test args without --args, so gdb
treated --nocapture as a core file. Also collect loader-stage evidence
for the copier_test dl_main SIGSEGV: IRELATIVE reloc count, LD_DEBUG
tail, full backtrace and registers.
- localtime_s/gmtime_s are MinGW header inlines, not symbols — link
_localtime64_s/_gmtime64_s
- copier_test also segfaults only on the Linux runner; add it to the
on-failure gdb backtrace
- timeformat node: localtime_s/gmtime_s (reversed args, 64-bit time_t)
on Windows — MinGW has no localtime_r/gmtime_r
- the multithread suite test passed non-NUL-terminated property names
(str::as_ptr) to the C property suite — UB that resolved to garbage
lookups on the CI runner
- libsnappy off on the MinGW FFmpeg build (only feeds the hap encoder;
its pkg-config entry does not reach the static link)
- FFmpeg for Windows no longer enables libopenh264 (redundant with the
native h264 decoder + x264 encoder; its MinGW packaging does not
satisfy the static link — Wels* undefined references)
- suites_test's question-type call passed a 3-placeholder format with
one variadic arg — UB that vsnprintf turns into a SIGSEGV on glibc
(masked on macOS); use a placeholder-free format
- oakffmpeg-link forwards pkg-config --static --libs verbatim; FFmpeg's
.pc files can list -ldl via external deps, and MinGW has no libdl
- suites_test segfaults on the Linux runner too; run both plugin test
binaries under gdb on test failure
Tests (and any host that shuts down then rescans) can hold instances of
a PREVIOUS plugin generation; their entry points dangle after dlclose,
so the next shutdown's destroyInstance notification jumped into
unmapped memory (SIGSEGV on Linux; masked on macOS). Plugin gains an
flag set by unload_all before dlclose; call_action/call_entry
fail fast instead of calling into freed code.
- the Windows runner image exports MSVC's INCLUDE/LIB; cc-rs was
appending the MSVC SDK headers to MinGW compiles (vcruntime.h not
found)
- oak-worker handshake test helper advertised the input pool's total
byte size as per-slot data bytes (macOS tolerated the oversized
attach; Linux correctly rejects it)
- hw/sw decode comparison tolerance 0.05 -> 0.08 (VideoToolbox's
YUV->RGB legitimately differs by ~1 LSB of intermediate depth)
- the vendored OCIO source needs MSVC-only constructs (wide-path
ifstream); MSYS2's mingw build of the exact 2.5.2 the bridge targets
is the sane Windows path — DLLs get packaged next to the binaries
- oak-worker handshake test prints the error response on failure
(CI-only attach failure needs the message)
Each dimension of a parametric param renders as a CurveEditor in the
inspector (bezier handles map to the Hermite slopes of the host curve
model; edits serialize back through the JSON mirror — undoable and
project-persisted). The engine re-sync skips in-progress drags and
identical curves so the per-render sync neither steals gestures nor
loops. Also: physical-memory probe for the worker-count policy on
Windows (GlobalMemoryStatusEx).
- ValueType::Parametric; the node input carries the whole curve set as
NodeValue::Text(JSON) so undo and project serialization come for free
- translation pass builds the input with the default-curve JSON and the
dimension/range/ui-colour properties
- edits flow both ways: node input (UI) -> curves_from_json ->
set_ofx(Parametric) on the instance; plugin-side Set/Add/Delete ->
notify_instance_changed -> JSON written back to the input (undoable)
- screenshot example: gate the macOS-only offscreen capture items so
the workspace tests build on Linux/Windows
- oakplugin host: Win32 LoadLibraryExW/GetProcAddress/FreeLibrary
backend (LOAD_WITH_ALTERED_SEARCH_PATH so bundle-sibling DLLs
resolve), same dl_open/dl_sym/dlclose surface — the POSIX path is
untouched; OFX hosts now compile on Windows
- proxymanager: PATH split via std::env::split_paths (Windows ';'),
ffmpeg.exe name, Windows candidate locations; split logic unit
tested
SharedMemoryRegion gains a Win32 backend behind the unchanged public
API: Local\OakShm<key> names, OpenFileMapping for attach, VirtualQuery
for the size check, UnmapViewOfFile/CloseHandle for teardown. Semantic
differences from POSIX are documented: unlink_key is a no-op (the
kernel destroys the object with the last handle, so crashed owners
self-heal) and Create on a live name fails instead of replacing.
Windows CI builds the workspace again.
Parametric (curve/LUT) parameters: ParamValue::Parametric holds one
ordered control-point curve per dimension (identity default over the
declared range), evaluated as piecewise cubic Hermite with auto
(centered-difference) slopes; the full suite — evaluate / count / get /
set / add / delete / delete-all — with the spec's error codes, descriptor
defaults copied to instances, and instanceChanged notifications on
edits. paramDefine accepts OfxParamTypeParametric; the dimension/range
and UI-colour properties round-trip. 148/148 real plugins discovered,
135 registered (one more than before: the parametric-suite consumer).
- cargo fetch does not extract sources; the yaml-cpp <cstdint> patch
now untars the .crate into the registry src dir first (the glob
found nothing and the step failed with exit 2)
- oakaudio: the watchdog-wrapped audio test called Self::... from a
free-function test module (compile error in lib test)
- Linux: libxkbcommon-x11-dev for the gpui X11 client link
- Windows: patch <cstdint> into the vendored yaml-cpp (a cached cmake
configure ignores CXXFLAGS; the patch is idempotent and runs after
cargo fetch)
- macOS: the hw-decode test skips its VideoToolbox engagement
assertions on hosts where VT cannot initialize (headless/virtualized
runners) instead of failing
- display color management: the display ICC (system or custom) is
applied to viewer frames at present time (F32 in place, or in place
on the BGRA staging copy with the R/B swizzle baked into the OCIO
chain); preferences get a Color section (mode + custom ICC file); on
macOS the Metal layer is tagged with the display colorspace when
self-managing so ColorSync passes pixels through (no double
correction); frame caches track the transform generation so a mode
or profile change drops stale pixels
The distro OCIO is too old for the bridge's API floor where it matters
(Ubuntu 24.04 ships 2.1; the bridge uses 2.4+ APIs), and version drift
across platforms is a support hazard — enable ocio-rs' bundled feature
and drop the OCIO_INSTALL_DIR/system-package wiring from CI and CD so
Linux, macOS and Windows all build the same vendored OCIO. cmake/make/
diffutils added where the runners lack them (Windows FFmpeg build needs
make + cmp).
- serializer resolves node types through the factory's dynamic
(runtime-registered OpenFX) entries, so a project carrying plugin
nodes loads again (was: "unknown node type"); covered by a new
CI-gated round-trip test driving the real fixture plugin
- built-in effect nodes expose their inputs as inspector parameters
like the C++ parameter editor: localized input names from the
behavior, combo option tables via the new
NodeBehavior::input_combo_strings (16 nodes, string-for-string from
the C++ set_combo_box_strings), connection/data inputs excluded
- effect library: live drag-and-drop — onto the inspector's effect
stack (lands at the indicator position) and onto the node editor
canvas (creates the node at the drop point); double-click still
appends to the selected clip
- inspector parameter controls are no longer recreated per render
(gpui stack view caches them per effect), so sliders drag and
checkboxes click; the view observes the engine and silently re-syncs
values (undo/redo land on the widgets)
- timeline: left-press selects clips (plain/keep-multi/Ctrl-Cmd
toggle); clip moves clamp the shared delta so no clip of a linked
group lands before frame 0 instead of failing with "invalid move
target"
- oakplugin: createInstance-rejected instances skip the destroyInstance
notification (the plugin never owned them); vendor-suite fetchSuite
misses moved behind OAK_OFX_TRACE; the worker logs the discovered/
registered plugin counts
- CI: the OFX probe step also runs the serialization round-trip test
- gpui submodule: params view caching, clip click-select, library
drag payload, graph_position_at
- new minimal C OFX plugin fixture (ci_test_plugin.c) compiled into a
real .ofx.bundle by build_fixture.sh; the CI step points
OFX_PLUGIN_PATH at it and asserts the scan_probe example discovers
AND registers it (Linux/macOS)
- host bundle binary search now also covers the OFX-standard Win64
platform directory
- cd.yml: restore the Windows NSIS packaging job (obsolete oakengine
cdylib prebuild dropped) and repair the job indentation that had
silently detached the macos/release jobs from the jobs: map;
releases now gate on all three platforms
- save() writes <config>/config.toml (atomic temp+rename), flat keys
at the top level and group/sub keys as [group] tables, values as
native TOML int/float/bool/string (non-finite doubles degrade to
strings and restore via the declared type)
- load() prefers config.toml; a legacy config.ini (C++ or pre-TOML
Rust builds) is read once and immediately re-persisted as TOML; the
INI file is left in place; a corrupt TOML is reported, never
silently discarded
- cd.yml: drop a stale oakengine comment (the crate is retired)
- NLE track growth is now a display concern: video/subtitle track
lists render reversed (a new track lands on top), audio lists render
in order (a new track lands at the bottom); the graph list always
appends. Track-add undo removes THIS track by id instead of blindly
removing the last one
- add_track returns the actual index of the new track (diffed against
the pre-command list) instead of assuming append-at-end
- status bar proxy segment reflects the real Use Proxy Media switch
instead of a static "Proxy: Off"
- proxy transcode PROGRESS events no longer invalidate the rendered
frame cache on every tick (only completion does) — progress updates
used to keep the playback cache permanently cold while generating
- effect library: live search box (name/type-id substring), Built-in
group header, and the addable-effects table is sorted alphabetically
(built-ins first, then OFX sub-category groups)
Real openfx-misc/CImg/Shadertoy bundles (148 plugins at
/Library/OFX/Plugins) all failed to load before; every failure was
silent. Root causes found one by one with a probe example + lldb:
- property suite rejected propSet on undefined properties and
propGetDimension on empty ones, and disallowed the index==size
append — OFX semantics are create-on-set and appendable dimensions
(this alone failed every plugin's describe)
- host property set missed the mandatory OfxPropType/OfxPropAPIVersion
and the capability props ofxs' fetchHostDescription reads with
throwOnFailure=true (IsBackground, TemporalClipAccess, MaxPages,
PageRowColumnCount, host SupportedContexts, ...) — one missing prop
aborted the read chain and left a half-initialised host description,
which made every temporal plugin refuse to load
- MultiThreadSuiteV1 lacked the five mutex functions (the plugin reads
past the short table — UB); implemented as a real counting-semaphore
registry
- the OfxHost struct was a stack local; ofxs keeps the POINTER past
setHost, so describe/render-time fetchSuite calls dereferenced a
dangling stack address (bus error once plugins actually loaded) —
the struct is now a leaked process global
- General is a standard OFX context and is no longer filtered out
(Roto/AppendClip/STMap declare only it)
- every scan/load/describe early-out now logs its reason; suite entry
points report non-OK statuses with caller location under
OAK_OFX_TRACE
- examples/scan_probe.rs: scans the real plugin dirs and prints
discovered/registered counts (also usable from CI)
Result: 148/148 plugins discovered, 134 registered as node types (the
remaining 14 need vendor suites — Vegas stereoscopic etc. — and are
logged, not silent)
- The Window menu lists every panel, checks the open ones, and toggles
visibility on click (closed panels reopen at their last dock target,
falling back to the default group) — a panel closed by accident
(e.g. the inspector) is one menu click away again. The menu refreshes
on dock structure events.
- OFX plugin scanning now covers the full standard location set:
per-user (~/.OFX/Plugins, ~/.local/share, ~/Library/OFX/Plugins on
macOS), system-level (/Library/OFX/Plugins, /usr/OFX/Plugins,
/usr/local, %ProgramFiles%\Common Files\OFX\Plugins), app-relative,
and the OFX_PLUGIN_PATH environment variable.