The full pure-C facade used by the app: node/project/timeline/viewer/
undo/task/events/serializer/playback/preview/renderer/gizmo/color/
audio/footage/proxy/encoding/exporter/config/disk/ipc/plugin/worker
families, plus undo-group semantics, display renderer handles,
NodeFactory accessors, and per-family pure-C engine tests.
Complete documentation set for the facade migration (B1-R6) and beyond:
facade-migration-roadmap, handoffs v3-v6, R5 guides, R6 cleanup plan,
R7 pure-ABI plan, long-term plans (riir/ai-agent/gtest/ui-redesign),
updated Chinese README draft, UI design mockups, and the Google Test +
struct-typedef rules in CONTRIBUTING.
- new primitives: clip_toggle_enabled (per-block flip), clip_set_linked,
sequence_add_default_transition (config-driven, sequence timebase),
node_set_label_many, node_set_color_label, plus observation getters
- toggle-links, default transitions, enable toggles, color labels, and
block renaming now go through the facade; the stray empty undo entry
from block renaming is gone along the way
- nest/multicam/waveform-sync stay as documented composites: they mix
redo_now intermediate state, graph surgery, and app-side computation
that a single primitive cannot express faithfully
- the timeline panel's command execution paths are now fully migrated
- new primitives: keyframes_set_time_many (conflict-safe batch time
move), keyframes_set_value_many (captured or explicit old values),
keyframes_set_bezier_many (double precision), and
keyframe_set_bezier_point (single handle with NaN-capture fallback)
- dialog and curveview drag finalization go through the facade; the
tests exercise the same global undo stack via oakengine_project_undo
- keyframeview/keyframeviewundo.{h,cpp} removed with zero remaining
references
- new facade API: set_input_at_time (element addressing, track=-1 for
all components at once), set_input_string_at_time, frame_time_base,
array_insert_at/remove_at, disconnect_ex (element-aware), and
keyframes_set_type_many (first cross-track keyframe op, addressed by
(time,track) pairs)
- the widget bridge's commit funnel, color path, array ops, label
disconnect, and keyframe set-type actions in keyframeview/curvewidget
now go through the facade; keyframeviewundo.h loses two consumers
- deliberate leftovers with rationale: keyframecontrol's multi-track
composite ops (documented track-0-only limitation of the keyframe
family), keyframeproperties dialog (needs a set_time primitive),
curveview's drag UX, and NodeInputDragger (already engine-side)
- new primitives: ripple_delete_in_to_out (ripple or gap fill plus
work-area state, one undo command), trim_clips_to (batch edge trim
returning a count), delete_empty_tracks (type-filtered batch), and
marker_remove_many (sparse marker deletion by timestamp array)
- delete-in-to-out, edit-to, delete-all-empty-tracks, and sequence
viewer marker deletion now go through the facade; empty operations
no longer push empty undo entries
- footage viewer marker deletion keeps its app path deliberately
(facade marker handles are Sequences, not generic viewers); the
tentative subtitle track and pointer drag chain stay as documented
leftovers
- new batch primitives: split_clips (link-preserving, single undo
command), delete_clips (gap replace + optional ripple with explicit
region support), ripple_delete_range, marker_add_ex with color
- razor/split-at-playhead, clip delete, ripple-to-point, track delete,
and the non-dialog marker path now issue facade commands instead of
the app's own undo command classes
- batch operations deliberately produce one undo command per user
action (deleting twenty clips is one entry, not twenty); selection
and transition removal stay UI-side as documented leftovers
- new facade API: video params ex (dimensions/rate/par/interlacing/
preview format/divider), audio params, preview divider, and
undoable-flagged setters mirroring the dialog's dual undo/no-undo
modes; label setting gains node_set_label_ex
- the dialog's own SequenceParamCommand is gone; accept now issues
facade calls (per-field commands, unchanged fields skipped)
- preset system stays UI-side by design: its flat XML schema never
touches engine objects
- the auto-cache checkbox maps to the engine's existing stub (no undo
noise)
- new facade API: video stream overrides (colorspace/range/interlacing/
premultiply), pixel aspect, image-sequence params, stream enable,
source start time, and colorspace candidates - all undoable
- project explorer proxy actions now run through FacadeProxyTask and
the facade media-management functions (ProxyManager references in
projectexplorer.cpp drop from 5 call sites to a comment)
- footage properties dialog reads/writes through the facade; its two
app-side undo command classes are gone
- handle-model fix: the footage handle is a heap state object, not a
plain pointer cast - oakengine_footage_borrow() wraps app-held
Footage nodes correctly (nine UB reinterpret_casts caught by the
DialogFootageProperties tests)
- oakengine_export_render_ex covers the dialog's entire option surface
(formats, codecs, pix fmts, audio params, ranges incl. still frame,
subtitles, scaling, threads, custom OCIO color transform names,
per-codec key/value options) - zero feature reduction
- the dialog's Start now creates a FacadeExportTask that drives
oakengine_export_render_ex instead of constructing ExportTask in the
UI; progress flows through the facade callback and cancel through
oakengine_export_cancel (OAKENGINE_E_CANCELLED preserves the
keep-dialog-open semantics)
- two real fixes: audio sample format 0 no longer means an
AAC-unsupported u8_p (default is f32_p), and image-sequence exports
use the engine's real [#####] placeholder instead of a made-up -%04d
Drives ConformManager for every audio-streaming footage with the same
AudioParams the export will use, before starting ExportTask - matching
the app's preview prewarm semantics. Root-cause analysis showed the
export's own wait-for-conform path already worked; this remains as a
first-export speedup and a belt-and-braces guarantee
- clip-level loop mode (off/loop/clamp), undoable
- per-channel audio levels as one-frame RMS at a timestamp
- waveform min/max buckets over a footage range, rendered on demand
- two real fixes uncovered by this family: conform completion signals
were starved by msleep-only waits (audio renders always came back
empty), and incomplete tickets from conform-pending renders are now
retried until the conform is ready
- relink through Footage::set_filename with the engine's natural
clear+reprobe cascade; find_offline_footage recovers offline media
by exact filename match under a search directory
- synchronous proxy generate (event-loop driven like the export
family), state query, enable/disable, path, and delete, mirroring
the app's proxy dialog assembly
- documented as non-undoable, matching the app's existing semantics
- undoable track removal (content restored on undo) and true-move
track reordering assembled from the edge commands (the engine has
no move-track API)
- track height/mute/lock getters and setters, documented as
non-undoable to match the engine's current semantics
- undoable marker add/remove/rename; duplicate timestamps are
rejected with E_STATE instead of hitting the engine's debug assert
- keyframed state, count, read (time in sequence timebase + mapped
values), add/remove, easing read/write (linear/bezier/hold with
control points), and clear - all undoable with full undo/redo
assertions
- easing set commands are minimal capi-local UndoCommands matching
the app-layer semantics (engine has none of its own); same-time
duplicates are rejected with E_STATE instead of hitting the engine's
debug assert
- project node enumeration, type/name/label metadata, input
introspection (id, mapped value type, connection state)
- typed parameter read/write for the eight common NodeValue kinds
(int/float/bool/rational/color/vec2-4/combo/string), undoable via
NodeParamSetSplitStandardValueCommand - the split-track value path,
chosen after proving the standard-value command stores whole
variants into a single component track
- graph operations (add/remove/connect/disconnect) reusing the
engine's undo commands, all covered by undo/redo assertions
All four are undoable and reuse the timeline/ command classes verbatim
(BlockSplitCommand, TrackRippleRemoveAreaCommand, BlockTrimCommand,
ReplaceBlockWithGap+PlaceBlock), with undo/redo assertions covering
split halves, media-in alignment on trim, ripple shift amounts, and
full restoration on undo
- oakengine_export_render drives ExportTask synchronously (offline
render + encode) with a progress callback, codec probing, and a
thread-local error channel; exporter.h keeps clear of the visibility
macro header
- oak-cli transcode now defaults to mp4 (H.264/AAC) with --format ppm
keeping the raw output path
- two real concurrency bugs found by the facade's own test: ExportTask
deadlocks when start()ed synchronously (queued conform handshake
needs an event loop), and the progress callback must be captured by
value because it fires on the task thread
- oakengine_sequence_add_track and add_footage_clip are the facade's
first editing primitives: undoable track creation and clip placement
with full range validation, clip enumeration, and gap filtering
- oak-cli transcode closes the loop: media file -> import -> clip ->
render, producing scaled PPM frames and a WAV from just the C ABI
- engine fix uncovered by transcode: the render worker used an invalid
empty AudioParams for IPC render frames, crashing any sequence that
contains audio; it now derives them from the rendered node itself
- sequence_new hardens its defaults against missing audio config keys
- oakengine_footage_probe inspects media without a project (decoder
probe): stream counts, per-stream video info (dimensions, rate,
duration, color tags, interlacing), audio info, duration, decoder
name, source start time; thread_local last_error for the NULL-handle
failure paths
- oakengine_project_import_footage adds probed footage to a project
through the same undoable command path as the app
- oak-cli probe prints decoder/duration/per-stream details and runs as
a ctest everywhere (no GL)
- dual ownership documented: probe handles are owned, imported footage
is borrowed from its project
- oakengine_sequence_get_video_params (width/height/pixel aspect) with
assertions for the default 1920x1080 square-pixel sequence and the
invalid-handle path
- oak-cli now renders at the sequence's real dimensions instead of the
hardcoded 1920x1080
- oak-cli info prints project/sequence/footage details; oak-cli render
writes PPM frames and a PCM WAV through the facade only - no engine
C++ headers, no Qt headers, links just liboakengine
- exit code 2 marks rendering-unavailable so ctest can skip cleanly on
machines without a GL render backend; info test runs everywhere
- facade fix uncovered by the CLI: project load now absolutizes the
path, so relative footage paths can't be mistaken for a moved
project; the fixture project now carries probed footage wired into
its sequence so it renders real content
- packaged like the other binaries (Linux bin install, macOS bundle,
Windows DLL copies); DESTDIR-verified
- oakengine_renderer_create/set_mode/last_error, render_frame (sync,
60s timeout, CPU frames via the existing worker pool),
render_audio (planar float), cancel; frames and audio buffers are
owned handles with borrowed data pointers
- output colorspace names map to OCIO display transforms, with
graceful fallback to reference-space output
- oakengine_renderer_test: parameter validation and error paths need
no GL and always run; render assertions gate on
DynamicRenderer backend availability and SKIP cleanly otherwise
- fix a facade bug found by its own test: wait_for_ticket leaked a
connected lambda capturing a stack reference, which a subsequent
cancelled ticket could fire into reused stack memory
- oakengine/export.h establishes the OAKENGINE_API visibility macros;
include/oakengine/ipc.h is the first pure-C surface (41 functions:
shm, frame slot pool, and the worker IPC messages as POD<->JSON
build/parse), implemented in engine/src/capi/
- the IPC implementations move to engine/src/oliveimpl (namespace
olive::engine::internal::ipc); engine/render/ipc/*.h are rebuilt as
same-name/same-API wrapper classes forwarding across the C boundary
- FrameSlotMeta is shared with the C header verbatim so the app/worker
wire format (v1) is bit-identical; static_asserts pin sizeof and
field offsets
- spscringbuffer.h moves to include/oakengine/ as an inline-only
header (no symbols, not ABI)
- new pure-C test oakengine_ipc_test (make_oakengine_test, no GL)
covers shm, frame pool, message round-trips and the layout asserts;
full gtest suite stays green (1986 tests)
Physical split: app/{audio,cli,codec,common,config,node,pluginSupport,
render,task,timeline,undo,tool,shaders} plus coreengine, version and
ui/icons+colorcoding move to a new top-level engine/ tree, built as
liboakengine.so (shared). The render backends (oakgl/oakvulkan) move
with it and link the engine library instead of embedding a static
render-core subset (libolive-rendercore is gone).
- oak-render-worker now links liboakengine instead of the whole
libolive-editor object set: 336MB -> 2.9MB, no Qt Widgets UI
- the editor links liboakengine for the engine and keeps only UI
objects in libolive-editor
- install/packaging: GNUInstallDirs libdir on Linux, bundle copy on
macOS, oakengine.dll staged for NSIS, AppImage validation entry
- fix backend lookup for the new layout: DynamicRenderer searched
../app but backends now live in engine/; a stale pre-split liboakgl
in the build tree got dlopened instead, re-initialized and later
destroyed the interposed engine statics (full-suite segfault at
DialogSequenceParameterTab, found via gdb watchpoint)
- NodeFactory's menu creation moves to UI-side widget/menu/factorymenu
(the factory only exposes its node library read-only now)
- DiskManager's cache-settings dialog is created through a registered
std::function handler (registered by Core at startup)
- OlivePluginInstance creates progress UIs through a
PluginProgressReporter interface (Null fallback headless) and queries
the active viewer through a provider callback, both registered by Core
- factory.h, diskmanager and pluginSupport no longer reference any
widget//dialog//panel//window headers or classes
EngineCore (new app/coreengine.{h,cpp}) owns every engine-safe part of
the old Core singleton: CoreParams, lifecycle of the engine managers,
UndoStack, tool/snapping/timecode state, locale, autorecovery, recent
projects, footage filters, clipboard, project registry, type
declarations, and the proxy toggle. UI dependencies are inverted
through hooks instead: status-bar/cache-full signals and std::function
handlers for image-sequence confirmation, footage relink, OTIO import,
project save/close and layout load (same pattern as
Config::ErrorHandler).
Core (app/) now derives from EngineCore and keeps only UI behavior:
the main window, dialogs, panel heuristics, import/export flows and
project lifecycle presentation. Its public API is unchanged (all
inherited), and Core::instance() covariantly static_casts the engine
singleton. The render worker constructs EngineCore directly, making it
the first binary that no longer needs the UI side of Core.
~25 engine call sites move from core.h to coreengine.h; a dozen more
drop a vestigial core.h include (gaining direct includes for symbols
they were borrowing transitively). Full gtest suite green (1986 tests,
0 failures).
- slider DisplayType enums sink to node/sliderdisplaytype.h (canonical
engine home); FloatSlider/RationalSlider alias them for compatibility
- DropWithoutSequenceBehavior enum sinks to common/dropworkflowbehavior.h
- Config errors now go through a registered ErrorHandler hook instead of
QMessageBox with a MainWindow parent; the style default no longer
depends on the UI style manager
- MainWindowLayoutInfo moves to node/project/serializer/ and its panel
dependency is reduced to a plain std::map alias (PanelLayoutInfo),
breaking the engine -> PanelWidget -> KDDockWidgets chain
- ProjectImportErrorDialog moves from task/ to dialog/projectimport/
- remove confirmed-redundant UI includes and give project.h/import.h/
project.cpp the direct includes they were borrowing transitively
- project.h includes folder/sequence headers directly (it used both
types in its own API all along)
core was added with EXCLUDE_FROM_ALL, which also excluded its install
rules, so liboakcore.so never made it into packages. It now installs to
the platform libdir (/usr/lib on this machine, verified via DESTDIR).
Even statically linked runtime libraries must not leak symbols: the
linker version script now whitelists oakcore_* only. nm reports 188
oakcore_* exports and zero anything else.
liboakcore is now a shared library that exposes only a C ABI:
- every value class (Rational, TimeRange, Color, Bezier, AudioParams,
SampleBuffer) and the free-function groups (StringUtils, fraction
utils, Timecode) is wrapped in an opaque-handle C API under
core/include/olive/core/oakcore/ (init/copy/free + self-first
functions), implemented in core/src/capi/
- consumers keep the original C++ API unchanged through same-name
wrapper classes that hold the handle and forward across the C
boundary; original implementations moved to core/src/oliveimpl
(namespace olive::core::internal) and are hidden from export
- TimeRangeList/TimeRangeListFrameIterator are reimplemented inline
over the wrapper (iterators/containers don't cross C ABI)
- generic Value container stays internal (unused by consumers) and is
no longer part of the public umbrella header
- hidden visibility + OAKCORE_BUILD export macro; nm shows zero
olive::* symbols exported
- install into the platform's standard libdir (GNUInstallDirs);
Windows DLLs next to the executables, macOS into the app bundle
- TimelineWorkArea::in/out/length now return by value: the wrapped
TimeRange getters return values, and forwarding them through const
references dangled (found via RenderWorkerFootageTest crash)
- tests: 9 new pure C ABI test executables (oakcore_*_test) covering
every public C function; 4 stale legacy core tests removed (they
targeted a long-renamed API and were never built due to a malformed
option() that also kept OLIVECORE_BUILD_TESTS off)
- CI/CD: oakcore.dll staged for NSIS, liboakcore.so added to the
AppImage validation list, build-tree DLL copies on Windows
The load/save of the per-footage custom divider attribute was lost when
the proxy changes were re-applied during the three-way commit split;
ProxyManager.FootagePersistsCustomProxyParams caught it.
- oak-render-worker now builds from worker/ (own CMakeLists.txt) as a
peer of app/; RenderWorkerPool resolves the new build-tree location
- deduplicated the Linux install() rules for the worker
- worker-spawning tests resolve build/worker instead of build/app
- cd.yml: Windows staging copies the worker from its new output path
- media color primaries/transfer tags now flow from the FFmpeg probe
through VideoParams into Footage::get_colorspace_to_use(); precedence
is user override > media tags > project default
- export nclc tags derive from the output colorspace (PQ/HLG/BT.2020,
P3, sRGB, Rec.601, Rec.709) instead of hardcoded BT.709
- new OCIO Color Grading (Log) node (lift/gamma/gain) and White Balance
node (kelvin temperature + tint, HDR-safe)
- LUT whitelist extended to 9 OCIO-supported formats
- waveform scope gains an RGB parade mode (GPU and software paths)
- tests updated for the new colorspace precedence
- playback timer uses the audio output device as its master clock: the
PortAudio callback counts consumed frames (including underrun
zero-fill) so video cannot drift away from what is heard; wall clock
remains as fallback when no clocked output is running
- output clock compensates for device output latency; new Preferences >
Audio buffer size setting (0 = auto)
- SampleBuffer::speed() now uses linear interpolation instead of
nearest-neighbor sampling
- regression tests: audio-clock driven timer (fwd/rev/speed), wall
clock fallback, interpolation correctness
- FootageJob::should_use_proxy() centralizes the proxy decision; worker
pre-decode now honors the render mode so exports always decode the
original media (previously every frame was pre-decoded from proxies)
- global Tools > Use Proxy Media toggle with footage invalidation
- ffmpeg -progress parsing for real percentage feedback while generating
- divider mode (1/2, 1/4, 1/8 of source resolution) with UI, proxy
filename tags and per-footage persistence (pdivider)
- Media Offline warning slat rendered for missing footage
- regression tests: export isolation, relink invalidation, offline slat,
progress parsing, divider arguments