- 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.
- Autocache range jobs now post at Background priority
(submit_video_background): they used to go through the Seek path and,
after the M4 seek over-admission, jumped ahead of playback and past the
render-queue bound. The interactive single-frame preview keeps Seek.
- Job.cancelled: the arena installs the slot's cancel atom, and
execute_job finishes a cancelled job with Error::State before running
the producer — a cancel no longer burns a full render/GPU pass only to
discard the result. Exactly-once delivery is unchanged.
- DECODE_LRU_CAP 8 -> 2: the decode service's LRU is a hand-off buffer,
not the cache of record (the eval-side decoded_frames LRU is); the
double-cache footprint at 1080p F32 drops by ~6 frames. A hand-off miss
is served from the eval cache without a new decode.
- Tests: sequence-aware preview cancel, over-admitted seek ordering,
deterministic prefetch LRU reuse, cancelled-job skip, autocache
priority. docs §3.4 backfilled with the A/B/C audit outcomes.
docs/zh/plans/render-pipeline-threads.md M4: the thread pipeline now
keeps its decode thread ahead of the render thread and the app's
playback window consumes in-process frames.
- Render queue: priority-ordered by JobSchedule.priority (Seek >
Playback > Background, FIFO within a class), so interactive frames
jump playback exports/autocache. Seek posts may over-admit the bound:
priority only reorders queued jobs, so a full queue of background work
must not park the UI thread until an export frame finishes.
- Decode queue: rendezvous Requests are served ahead of queued
Prefetches (a frame the renderer needs never waits behind speculative
decodes); Sync barriers stay FIFO. The queue is a bounded
Mutex+Condvar structure, preserving the request backpressure and the
wait_idle contract.
- Playback read-ahead: a Playback job's footage decode requests are
derived from its montage/footage spec on post (same media time, size
and force_format.unwrap_or(F32) as the eval) and queued immediately,
so frame N+1 decodes while frame N runs its GPU passes.
- App window: PreviewWindow slots are generalized to
PreviewSlot::{Shm, Video}; the pipeline's in-process TicketPayload is
cached and consumed by cpu_frame exactly like a worker slot.
PipelineBackend::preview_window_capacity reports the render-queue
headroom, so playback posts are capped to what the queue can take;
cancel_preview_frame drops queued frames the playhead has passed,
matched on the full (sequence, frame, version) key so one monitor's
window never drops the other sequence's same-numbered frame.
- Tests: decode-queue preemption/FIFO, render-queue ordering, request
derivation, and deterministic end-to-end M4 tests: a prefetch that
must be reused by the render request (LRU hit, single decode — the
read-ahead claim is falsifiable), a parked-render-thread priority test
where a full queue of background work still lets a Seek over-admit and
run first, and a sequence-aware cancel test. The playback prefetch
smoke asserts prefetches == distinct decodes == frames; it does not
claim zero heap copies (Frame.data is deep-copied at the eval-cache
and service-LRU boundaries today).
- bench_playback gains a pipeline mode with CPU (self+children) and
first-frame latency; both backends now produce F32 frames so the
comparison is like-for-like. The §3.4 backfill records the numbers:
at the proxy size the pipeline is faster with a lower first frame; at
1080p peak throughput is below the multi-worker pool, but that is an
artifact of the decode still being CPU software (M5), not a case for
pooling decode threads — GPU decode is a single device/queue and the
zero-copy import shares one GPU memory pool, so the single decode
thread stays the target shape.
docs/zh/plans/render-pipeline-threads.md M3 (design 3.2): OpenFX crash
isolation moves from "every worker hosts plugins" to a single dedicated
host process, served over NDJSON + shared memory.
- oak-worker --ofx-host mode (src/ofx_host.rs): loads every plugin once,
resolves jobs by the cross-process-stable OFX identifier, and renders
through the same in-process executor the workers used to install.
- oak-render/ofxhost.rs: the single-host client. The render manager
creates and installs it for the Pipeline backend (lazy spawn on the
first plugin job); eval::process_plugin_job prefers it and falls back
to the in-process executor otherwise, so the process backend keeps its
current behavior until M4.
- Data plane: input/output FrameSlotPool pairs (the handshake's input_*
fields are used for the first time). Named clips and the source frame
are written to input slots after the explicit CPU readback; the plugin
output returns through an output slot. Pool size/capacity grow by a
host restart when a job needs more (safe: submissions are serialized
and one job is in flight).
- Crash loop: reader EOF fails the in-flight submit, which respawns the
host and re-posts the same job (frames are read back once); after three
consecutive crashes the client is permanently dead and the evaluator
falls back to a purple frame. The dead child is reaped immediately, and
a submit mutex enforces the one-job-in-flight contract.
- Progress/cancel: the host flushes plugin_progress immediately (live
progress), and reads stdin on its own thread so plugin_cancel takes
effect mid-render at the plugin's next progressUpdate; the sticky flag
resets at progressStart and request_plugin_cancel_all broadcasts to
both the worker pool and the host.
- JobSpec::Plugin / PluginJobPayload carry the plugin type_id (stable
across processes); `--ofx-crash-once` / `--ofx-crash-always` are the
deterministic crash hooks, matching the worker's env hooks.
- Tests: wire round-trips; host unit tests (crash budget, cancel-flag
reset through the factory, source mapping); oak-worker integration
tests against the real host + bundled test plugin (render + progress,
crash respawn and re-post, three-crash give-up, mid-render cancel on
the new slow variant, concurrent submits); eval's purple fallback.
- Windows: vcpkg ships `pkgconf` without the `pkg-config` shim, so the
oak-ffmpeg-link build script failed with "program not found". Probe
`pkg-config`, fall back to `pkgconf` (or honor `PKG_CONFIG`), and join
the child's `PKG_CONFIG_PATH` with the platform separator instead of a
hard-coded `:` (which split `C:\...` apart).
- Linux (and openKylin): the test binaries link the VAAPI stack via
vcpkg's FFmpeg; install the `libva2`/`libva-drm2`/`libvdpau1` runtime
packages the loader needs.
- macOS: bump the gpui submodule
(OakVideoEditorCommunity/oak-gpui@fix/macos-metal-layer-and-dead-code):
`setColorspace:` now sends to the `MetalLayerRef` (`self.layer.as_ref()`
made `&*layer` the owned type, which is not `objc::Message`), and the
viewer's `GpuFrameEntry` carries the non-Linux dead-code allowance.
- openKylin ARM64: switch the ocio patch to the fork's
fix/aarch64-c-char rev. Upstream models C `char*` as `*const i8`;
aarch64's `c_char` is u8, so the crates did not compile. The fix uses
`c_char` throughout ocio-sys and the ocio-rs boundary (pushed as
30338c6a169bbbada862fb3ac256e79b656159cf).
docs/zh/plans/render-pipeline-threads.md M2: the graph's textures stay
on the GPU from evaluation through presentation, and presentation runs
on the UI's own wgpu device.
- wgpu 25 -> 29 (naga 29) across the engine, unifying it with
gpui_wgpu so engine textures are directly sampleable by the presenter
(a single wgpu remains in the lockfile).
- GpuContext::adopt/install_shared: the app registers the window's
device at startup and the render thread renders on it;
texture_handle hands the raw Arc<wgpu::Texture> to
SurfaceSource::Texture - zero-copy present on Linux/FreeBSD. The
shared slot replaces an engine context that has not touched the GPU
yet (startup-order guard) and refuses once it has.
- Texture::Gpu shares a GpuLease so clones release the registry token
exactly once; the compositor, transitions and adjustment sweeps keep
GPU textures end to end (no per-clip readbacks; GPU clears for
black/generated frames).
- Color management stays on the GPU: the output node + display ICC
chain is baked into a 65^3 3D LUT with the exact CPU reference and
applied by the present WGSL pass (manual trilinear);
ColorTransformJob bakes its OCIO processor the same way. Neither
path skips color management.
- The explicit readback boundaries accept GPU textures: export
encoder, CLI, worker shm, disk cache; CPU OpenFX already read back.
- M5 dependency: the YUV->RGB GPU pass (BT.601/709/2020 x
limited/full) matches colormath::yuv444p16_to_rgb_f32.
- Acceptance: gpu_transfer_counters; single-clip and layered
(multi-track + transition + adjustment) playback tests assert zero
GPU->CPU readbacks, and the app test asserts adopted-device present
is zero-copy. GPU tests hard-fail when OAK_REQUIRE_GPU is set (CI
lavapipe) instead of skipping silently.
docs/zh/plans/render-pipeline-threads.md M1: an in-process
alternative to the worker-process pool, behind OAK_PIPELINE=threads
(processes stays the default and is fully retained).
- pipeline.rs: PipelineBackend implements JobDispatch over a single
render thread draining a bounded FIFO (cap 8; blocking post with
condvar backpressure and a one-ahead exception for re-posts from
the render thread itself; shutdown drains with Error::State like
the inline dispatcher). The DecodeService is a single decode
thread behind a bounded command queue with a real LRU (tick-based
eviction), rendezvous requests (None on shutdown -> the caller
decodes inline), prefetch gated on render-queue room, and a Sync
barrier; it installs into a process-wide slot that eval's footage
path consults per frame (no service -> the synchronous decode it
always was).
- The manager gains RenderBackendChoice::Pipeline; init() reads
OAK_PIPELINE (threads -> pipeline, anything else -> the process
pool), audio stays deliberately inline.
- Present mapping: the UI thread consumes through the ticket
completion, unchanged — no fourth thread is invented.
- Tests: decode-service unit tests (rendezvous, LRU hit/eviction,
error propagation, backpressure gate) plus a six-case integration
suite matrixed over inline vs pipeline — consecutive-frame and
out-of-order seek pixel equality asserted byte for byte, with
decode counters proving the service (not the caller) did the
codec work.
The 8.1.0 downgrade broke the build and the audio export: 8.1.0's
typed video encoder has no set_color_primaries /
set_color_transfer_characteristic, and its older audio path sent
near-NaN samples into the AAC encoder (transcode_mp4 and the oak-task
export test both failed). ffmpeg-next 9.0.0 supports ffmpeg_8_0/8_1
(the vcpkg pin of 8.1.2#3 is unaffected) and carries ffmpeg_9_0 cfg
branches for the day FFmpeg 9 lands.
254 warnings (320 counting replayed-cache re-emitters) cleaned:
unused mut/imports/variables, irrefutable if-lets and unreachable
patterns, dead code removed or annotated #[allow(dead_code)] with
the reason (C++ parity value sets, cfg(test) helpers, public API
reservations), drop(&ref) no-ops removed, fn-pointer identity via
std::ptr::fn_addr_eq, the test-stubs feature declared in
oak-node's manifest, missing docs filled. Every unused-Result site
was judged individually: meaningful errors propagate, intentional
ignores are let _ = with a note.
Two pre-existing latent bugs are documented in place, behavior
preserved: app.rs's timeline-tool observer and dialogs.rs's format
subscription both drop the returned Subscription immediately, so
they never fire.
macOS has no posix_fallocate (and the libc crate rightly does not
expose it there), so the shm segment setup failed to compile. The
eager reservation is a tmpfs concern; off Linux the call is skipped
and the existing touch-every-page fallback runs instead.
The GraphInput/GraphOutput cards render through the same build path
as every other node (real graph data, fixed header accents outside
both palettes), GraphOutput as a pure sink with no output ports.
UI-layer protection keeps the pair fixed: the context menu drops the
whole edit section (cut/copy/paste/duplicate/rename/delete) for
them, and delete requests naming an endpoint are narrowed at the
panel — the endpoints and the wires hanging off them always stay,
a request left with nothing is dropped whole. The engine reports the
protected set through AppEngine::protected_graph_nodes (the real
engine resolves Graph::endpoints; the mock demo graph carries its
own marked pair).
Per docs/zh/plans/render-pipeline-threads.md §3.8:
- oak-node/nodes/graphendpoints.rs: the GraphInput/GraphOutput
virtual node pair — factory-registered but hidden from every create
menu, duplicate refused, real value() semantics (the input forwards
its feed_in row, the output publishes its tex_in as the frame).
The input endpoint also declares a connectable feed_in port
(documented deviation: footage/generator sources have no connectable
inputs, so the walk needs a feeder anchor).
- graph.rs: ensure_endpoints/endpoints/is_endpoint — idempotent,
identified by type id, default input->output edge only while the
output's tex_in is free; remove_node refuses endpoints.
- project.rs + serializer.rs: every project graph carries the pair;
a legacy file without endpoints migrates on load (roundtrip and
legacy-migration tests, re-save is idempotent).
- traverser.rs: eval_graph_bfs — the endpoint-to-endpoint Kahn
sweep. Live set = (input's forward cone U its feeder cone) INTERSECT
(output's backward cone); multi-input nodes dequeue at zero
in-degree over the live subgraph; deterministic ascending-id ready
order (Graph::edges is a BTreeSet, so insertion order is
unrecoverable — documented); time-shifted upstreams pull through
the shared DFS memo (walk_dfs, factored out of evaluate);
un-orderable remainder reports a named cycle; missing endpoints /
unreachable output are errors. Eight BFS tests cover the plan's
acceptance bullets.
- oak-render: bfs_endpoint_sweep_renders_footage_through_position —
real clip through a real Position node via the sweep, shifted
pixels asserted against a reference decode.
- Endpoint names localized in all eight i18n packs; storage/structure
tests updated for the two extra nodes.
M0a of the render-pipeline plan (docs/zh/plans/render-pipeline-threads.md):
- oak-node: every payload push site (58 across footage.rs, plugin.rs
and the nodes/* effects) now boxes the Job enum instead of the raw
payload. The enum gains CacheJob with a CacheJobPayload (path +
time + fallback value, the C++ cachejob.h shape), plus safe as_*
accessors and unsafe probe helpers beside job_ref.
- oak-render: RenderEvalHooks::resolve is one loop over the table —
a single get_checked::<Job> probe per texture value, a match
dispatch to process_footage/shader/plugin/color_transform/cache,
and recursive resolution of the job boxes embedded in a payload's
inputs (depth-capped, cycle-guarded) — replacing the four
sequential full-table scans (resolve_*_jobs, deleted).
- The disk frame cache is real: frameio.rs implements a minimal
self-describing F32 container (magic/version/dims/format/timestamp
+ payload, tmp-write + atomic rename, full header validation on
load) because the OIIO bridge is a stub and EXR is unavailable in
this build; process_cache_job genuinely reads the file before
falling back to the job's (already resolved) fallback value.
- Tests: CacheJob roundtrip (save -> resolve -> pixel equality),
missing-file fallback, nested cache-job-through-shader resolution,
plus four frameio container tests. 2330 passed, 0 failed across
the workspace.
The off-frame mask edit redeclared vec2 uv inside main(); naga's GLSL
frontend rejects the redeclaration, so the swirl shader failed to
translate for wgpu.
A clip drag moved only the clip under the cursor (plus its graph-linked
A/V partner); the rest of the selection stayed behind. The engine now
keeps the full timeline selection (it used to collapse it to the
effect-stack's single target) and expands ClipMoveRequested to the
grabbed clip's transitive link group UNION, when the grabbed clip is
part of the multi-selection, every other selected clip and their link
groups. Each follower stays on its own track and shifts by the same
frame delta (relative positions preserved), the group-wide clamp keeps
every clip at or after frame 0, and the whole move is one undoable
entry. Followers on locked tracks are left in place.
moving_a_multi_selection_drags_the_whole_group covers two A/V pairs:
selecting both video clips and dragging one moves all four clips by
the same delta, and one undo restores them.
Dropping a transition on the timeline failed with "the track has no
frame rate": drop_transition_at asked sequence_time_base for the TRACK
node, but the frame rate lives on the sequence (tracks carry no video
params, so the lookup always returned None).
With that fixed the drop deadlocked instead: the edge resolution ran
while holding the project lock, and the add_transition_at_seam/edge
builders lock the project internally. The function now plans under the
lock and executes after it is released.
Covered by engine_drops_a_transition_at_a_clip_edge: a head-edge drop
with no previous clip lands a single-sided transition wired into the
clip only.
Generator effects (bars, checkerboard) can be dragged from the library
onto the timeline, where they land as a standalone five-second clip
built from the node factory; the inspector shows the generator's
parameters as the clip's own chain.
Transitions are no longer junction-only. The render planner accepts a
transition with at least one wired neighbor and blends the missing
side against transparent black, so head transitions fade in from black
and tail transitions fade out to black. add_transition_at_edge creates
those single-sided blocks (wired to just the IN or OUT block), the
default-transition command covers both ends of a lone clip, and an
effect drag dropped near a clip edge routes to the nearest seam or
edge within a one-second window.
Translating, rotating or warping content past the frame edge used to
smear the clamped edge row/column across the vacated region. The
transform, position, swirl, ripple and wave shaders now multiply the
sample by an in-bounds mask so off-frame pixels come out transparent
(and composite as black when nothing sits below). Tile deliberately
keeps its wrapping lookup.
Typing in a text input fired the global key bindings (space toggled
playback, Delete removed clips, ...). Every non-multicam binding now
carries a !EditableText context predicate so the app's own bindings
stay inactive while an editable text element has focus.
Predicate-gated bindings never match on an empty context stack, and
the app shell had no root key context — so the root element now sets
key_context("OakApp") to keep the dispatch stack non-empty
everywhere outside the panels.
The render worker process never installed a text backend, so text clips
rendered as empty frames in playback and export. The cosmic-text engine
now lives in oak-render (the crate both the app and the worker link),
and the worker installs it during runtime initialization.
Adjustment layers (docs/zh/plans/adjustment-layers-and-transitions.md):
a new timeline block type whose effect chain grades the composite of
every video track below it, over its own range (spanning clips or a
slice of one). The graph path flushes the lower tracks at the block's
track boundary and sweeps the composite through the chain via a
transient texture-source node; the montage path mirrors it with
AdjustmentSpan tickets (wire-compatible), so worker previews and
exports agree. An empty-area context menu creates one; the block
trims/moves/deletes like a clip, with undo everywhere.
Transitions: seam blocks come alive - cross dissolve/fade/wipe/slide
evaluate both neighbors through the graph path with progress from the
transition's own range (never the whole clip). Ctrl+Shift+D or the clip
menu inserts a default transition; the gpui wedges render and drag to
resize offsets undoably, and TransitionRemoveCommand now restores
offsets and edges on undo. The transitionfx node form runs the same
shaders on an adjustment layer with progress_in auto-filled from the
layer's span (explicit value wins).
Also: every built-in effect name and parameter name is now
translatable (360 node.* keys per locale, zh-CN fully translated, two
coverage tests guard future gaps); the new nodes register in
nodes/mod.rs with the factory smoke table updated; textfootage and
adjustment-layer i18n keys included.
Text is no longer a hand-written HTML effect (docs in
docs/zh/plans/text-footage-redesign.md):
- textv3 gains structured inputs - plain text, font family/size, font
color, outline (enable/color/width), glow (enable/color/radius); the
legacy text_in HTML is hidden and auto-migrated to plain text on load.
- Outline and glow render as GPU post-process chains (dilate/blur +
colorize under the text); the font color tints the raster
premultiplied. Plain text now rasterizes even with both passes off
(previously a null deferred job), fixing a use-after-free where the
handle was lifted out of an owning Option<NodeValue> before addref.
- A cosmic-text backend (the lockfile's 0.19) installs at engine
startup through the textbackend hooks and feeds the font-family combo.
- The project panel gains 添加文本素材 next to 新建序列: a text entry
in the bin that drops onto the timeline as a clip (one undo row), its
parameters shown as structured fields in the inspector (multiline
text area, no HTML anywhere). text3 is hidden from the effect add
menus; legacy text3 chains keep evaluating.
21 built-in effects reimplemented as native GPU nodes from the
OpenFX-Misc algorithm references (cleanroom, docs in
docs/zh/plans/ofx-misc-gpu-cleanroom.md):
- Color: Color Correct, Gamma, Saturation, Invert, Clamp, Grade
- Matrix/morphology: Color Matrix, Edge Detect, Dilate, Erode
- Blur: Directional Blur, Sharpen (unsharp mask)
- Merge: Dissolve, Key Mix, Premultiply, Unpremultiply
- Geometry/generators: Position, Mirror, Checkerboard, Color Bars, Ramp
Every node carries unit tests plus GPU pixel tests (28 cases over five
ofxmisc_* suites). The effect library groups built-ins by category
(color/filter/distort/keying/generator/math/general) with collapsible
group headers persisted to the config; the inspector's add menu groups
the same way. Registration wiring and the factory smoke table land with
the adjustment/transition wave sharing the same files.
The params view reapplies the engine snapshot to every control on each
render (every engine tick), which wiped the in-progress text a frame
after each keystroke - the text effect's field was effectively
untypeable. Skip the re-sync while the field is focused (the same
guard the curve editor has for drags); it re-syncs on blur and the
row's explicit commit writes the edit back. MockEngine gains a
sentinel text parameter for the regression test.
Edits invalidated the display caches, but the debounced snapshot
upload (150 ms) landed later - meanwhile the viewer had already
rendered and cached a proxy through the stale snapshot, and nothing
invalidated it again, so a paused monitor kept the pre-edit picture
until the playhead moved. Re-invalidate when the fresh snapshot is
actually installed so the next paint re-renders from it.
The transform shader sampled in a top-left-origin pixel space while
Olive's transform semantics (and every other node) are center-origin:
rotation swung the image around the top-left corner, pushing it partly
off-frame - reading exactly like an unwanted zoom. Match the C++
transform.vert projection: position (0,0) is the frame center and
rotation/scale pivot around the anchor, so rotation and scale stay
independent user controls. GPU tests pin the 90-degree landing spot
(no smearing) and the 2x scale centroid (stays centered).
- available_range declared a zero duration for every clip (the media
was 'unusable', so Resolve invalidated all of them): it now covers
the whole media - video clips in the probed frame rate, audio clips
in the probed sample rate (no more hardcoded 48000/0).
- Gap ranges mixed time bases (a 24 fps block start, a 1.0 trailing
duration): gaps now stay in the sequence's time base, the trailing
pad in whole frames.
- Empty sequences (no clips) are skipped, so a lone edited sequence
exports as a bare Timeline root instead of a SerializableCollection
Resolve cannot read.
- FCPXML: an asset used on both video and audio tracks now declares
hasVideo AND hasAudio (the first registration's track kind used to
hide the other stream); asset durations recover with available_range.
- Runtime window icon: X11 takes the embedded 512px PNG up front
(_NET_WM_ICON), Wayland resolves it from oak.desktop via the app id,
Windows reads it from the exe's icon resource.
- assets/appicon: 512px PNG + 7-size ICO rasterized from Oak_Icon.svg.
- Windows: oak.rc (ICON resource ID 1, also the Explorer icon) compiled
by build.rs through embed-resource.
- Linux packaging (deb/rpm/PKGBUILD/oak.spec): install Oak_Icon.svg
into the hicolor scalable theme alongside the existing 512px PNG.
- macOS: no change needed - cargo packager already builds the .icns
from the CD-generated icons/icon.png (same source art).
Two export killers fixed and the feature surfaced properly:
- TicketArena::allocate() reaps finished fire-and-forget slots on every
new submit, but RenderTask classified finished tickets by asking the
arena for their kind/time afterwards - any ticket that completed
before the next submit (all of them on the inline backend) failed
with 'Render ticket reported an unexpected timestamp' and the export
died after writing a header-only file. RenderTask now records each
ticket's delivery key at submit time instead.
- File > Export becomes 导出序列 (all 8 locales): the dialog offers a
sequence picker (any sequence in the project, the current one
preselected) instead of silently exporting the current one, and the
project bin's sequence context menu gains 导出序列 opening the same
dialog. Engines expose sequence_entries / current_sequence_id /
start_export_of.
- The export-project dialog drops its dead hand-typed path field (OK
already asked the platform save dialog).
- New end-to-end test: a one-second sequence exports to a real
H.264/AAC MP4 that probes and decodes back to the clip's content.
Preview renders at proxy size while a paused frame renders full-res, so
anchoring resolution_in to the render target made every sequence-pixel
effect (shape size/pos, transform offsets, corner pin points, drop
shadow distance) change apparent size whenever the transport stopped.
Pre-fill resolution_in from the sequence's video params (C++ inserts
the NodeGlobals square resolution at job-build time), covering the
nested generator job inside a merge as well; a node that inserted its
own resolution_in keeps it.
The sampled pixel lands (disarming the eyedropper) between the button
going down on the program viewer and coming back up, so the trailing
mouse-up read as an outside click: the popup closed and close_menu
discarded the just-picked draft - pick-from-viewer could never be
confirmed. Swallow the pick's own mouse-up once; a later genuine
outside click still dismisses.
naga's WGSL emitter rejects fall-through-capable GLSL switch blocks, so
every shape and despill job failed to compile and silently fell back to
the effect input - both effects were no-ops. Rewrite the type/method
dispatch as if/else chains (same semantics as the C++ shaders) and
cover all three shape types plus green-screen despill with GPU pixel
tests. Also drop the now-stale nested-payload/merge-binding TODO notes.
- process_shader_job: bind all texture params by name, recurse into nested
shader payloads (depth cap 8), fall back to frame size without inputs
- run_effect: take iterative_input so dropshadow previous_iteration_in works
- merge: actually composite inputs; keyer mask, opacity modulation, math
texture ops and mrg generator layers now bind their textures
- transform distort: real fragment-side inverse-matrix sampling
- time offset / time remap: wire NodeBehavior time adjustment hooks
- plugin: fix first-node identity colliding with unbound sentinel
oak-common is gone; its modules (configstore, xmlutils, ocioutils,
oiioutils, colormath, colortransform, videoparams, ffmpegutils, ...)
now live in oak-core alongside the value types. The render value/GPU
types moved too: backend (wgpu context + DisplayRenderer), color
(ColorProcessor over ocio-rs), texture, frame, and the commonutil
config helpers.
Fix-ups to make the merged tree build and pass tests:
- oak-core Cargo.toml: wgpu back to 25 (the moved backend code is
written against that API generation); add the toml/quick-xml/image
deps oak-common carried.
- lib.rs: drop the duplicate 'pub mod error;'.
- error.rs: unified OAKCORE_* codes; restore Error::new() and
From<OcioError> from oak-common's error type.
- backend.rs/color.rs: oak_core::/oak_render:: self-references
rewritten to crate::; the shaderfx-dependent GPU effect test moved
to oak-render's shaderfx tests (shaderfx depends on oak-node and
cannot live in oak-core).
- oak-render's error module re-exports oak_core::error::{Error,
Result}; the OAKRENDER_* codes stay as the public-code contract.
- oak-node jobs.rs: ColorProcessor imported from oak_core::color.
- Integration tests repointed at oak_core::{texture, frame, backend,
color, colormath}.
- the display-ICC regression test treats an empty OAK_DISPLAY_ICC as
unset, matching displayicc::env_override_icc.
The OCIO grading nodes previously pushed null texture handles; they now
push real ShaderJobPayloads whose GLSL is the OCIO-generated dynamic
grading-primary GPU shader — the exact code the C++ path applies, so no
approximation:
- color: grading_primary_function_shader(style) builds a dynamic
GradingPrimaryTransform (LIN/LOG) on the default config, extracts the
GLSL via GpuShaderDesc (function 'ove_grading_primary', resource
prefix ocio_, no LUT textures) and caches it per style + config id.
- eval: OCIO_GRADING_STUBS maps the two node type ids to the grading
style; process_shader_job resolves the stub and splices it into the
node's %1 marker (same wiring as the chromakey OCIO stub); the
pipeline cache key folds the stub text so a config change recompiles.
- nodes: value() pushes a ShaderJobPayload with the C++ value()
rewrite applied to the row — vec4 (RGBM x=master) grading inputs to
the vec3 GPU uniform form (lin: contrast RGB=c*m, offset RGB=c+m,
exposure RGB=2^(c+m); log: lift RGB=c+m, gain c*m, gamma c*m), plus
pivot/saturation floats, the log pivotBlack/pivotWhite normalization
range (0/1), clamp sentinels (NoClampBlack -1 / NoClampWhite 2),
white>black enforcement per frame, and localBypass=false. Generated
uniform names bind by name (the log node's OCIO_NAMESPACE_ id text
normalizes to the ocio_ resource prefix).
- Tests: grading stub generation (analytic GLSL, cache) in color,
end-to-end GPU exposure doubling for lin (+1 stop on 0.2 gray -> 0.4)
and lift for log, node payload rewrite assertions, and the
all-shaders sweep now retries grading stubs. oak-render 181,
oak-node 441, oak-app 272 lib tests pass.
Polygon and mask previously pushed null texture handles ('fake'
implementations). They now generate real ShaderJobPayloads and render
through the existing GPU shader pipeline:
- shaderfx: std140 uniform array support (Vec4Array(N)) — the parser
accepts 'uniform <type> <name>[N];' declarations, translate()
re-emits them as vec4[N] block members with per-element std140
offsets, and pack_uniforms writes array items from the new
NodeValue::Vec4Array value, padding short arrays to the declared N.
- polygon: value() collects the inherited points array (row element
keys 'points_in[i]', else the node's own per-element values — an
unconnected array resolves to the default pentagon via
GetValueAtTime parity) and pushes a ShaderJobPayload; the 'rgb'
fragment shader rasterizes the closed point loop with an odd-even
fill in screen space (center-translated, y-flipped to match the C++
point convention) and outputs color_in inside / transparent outside.
The CPU QPainterPath generate_frame stays a documented no-op.
- mask: value() pushes a single ShaderJobPayload whose new 'mask'
fragment shader folds the whole C++ chain into one GPU pass — base
texture multiplied by the polygon matte, optional invert, and the
optional feather gaussian softens the matte during sampling (the
separable blur.frag h/v iterations as a one-pass product,
density-normalized, radius capped at 16 px).
- Tests: translate/pack array coverage in shaderfx, GPU end-to-end
rasterization of the pentagon (center white, corner transparent),
mask multiply/invert/feather on real frames, and updated oak-node
payload assertions. oak-render 178, oak-node 440, oak-app 272 lib
tests pass.
Tests run in parallel inside one process; each GraphSnapshotStore::new()
used the same 'oakrender-snapshots-<pid>' root, so one test's cleanup()
deleted another test's live snapshot — the
acquire_rewrite_forces_file_rewrite_on_same_key flake on the 16-core
Windows runner (file written, then exists() == false). Append a per-store
sequence number to the directory.
- multicamnode: type_id() assertions called the method on dyn
NodeBehavior, which method resolution routed to std::any::Any's
TypeId::of — qualify via NodeBehavior::type_id so the trait method
(the &str node type id) is compared
- procpool: the per-worker GPU budget grew a security headroom (×2)
for the CUDA-OOM flood; the two budget tests now assert against the
real formula (2 GiB + 256 MiB at 1080p24, 2 workers at 4K/24 GiB)
- cli info fixture: the fixture runs 29.97 fps; the assertion expected
30/1 (stale from the older fixture)
The extra oak_task export fields (bitrates + color override code
points) added in the export-settings protocol are zero/defaults in the
CLI's sync exporter — audio/video bit rates codec-default, no explicit
color override (the project's delivery colorspace applies).
- 导出工程文件: the in-app dialog only picks the format (OTIO / OVE /
FCPXML); OK asks the system save dialog with the suggested file name
carrying the format's extension, then exports
- 打开项目: the system open dialog restricts the choosable files to
.ove/.ovexml/.otio/.fcpxml (PathPromptOptions.allowed_extensions
plumbed through gpui; Linux portal glob filter)
The three values are SpinBoxes (typed input + stepping): width & height
(0 = use the sequence size), frame rate in fps (0 = sequence rate),
bit rate in bit/s (0 = the codec default). The engine already accepted
arbitrary values, so the dialog now exposes them instead of the closed
preset lists. i18n across the 8 packs.
- ExportDialogContent: container format (existing), then the video and
audio codec combos rebuilt per container from the compatibility tables
(an unsupported codec is never offered — switching container
re-selects the first compatible entry), a color-space combo (SDR
Rec.709 8-bit / HDR BT.2020 10-bit), an export-range combo (whole
timeline / in-out) and the output path
- ComboBox::set_options added for the container-driven list rebuild
- settings() gathers the ExportSettings the engine consumes
- begin_export routes through start_export_with
- i18n keys across the 8 packs (zh-CN uses distinct values so the
en==zh equality test passes)
- ExportSettings: container format, video/audio codecs, size/fps/
bitrate, bit depth (8/10 → U8/U10), SDR 709 + HDR 2020 code points,
export range
- AppEngine::start_export_with(settings, path); real builds the
task params via encoding_params_with_settings (codec tables gate
each container — a non-supporting combo errors, never silently
defaults; the one menu only offers compatible codecs)
- oak-task EncodingParams: video_bit_rate / audio_bit_rate +
color_override_enabled + primaries/trc/space (override wins over
the project's delivery colorspace)
- mock start_export_with (no-op fake progress)
The dialog UI rebuild (fields + container→codec→color enability
linking) follows in the next unit.
- save_project_as(name): library_duplicate the open project's row,
rename the copy, open it as the current project
- 文件 → 另存为 opens a name dialog (prefilled '<name> Copy'); OK
duplicates + switches (the save-as semantics: a new, differently-
named project in the library, editing continues on the copy)
- 文件 → 新建项目 opens an in-app dialog: project name + the first
sequence's format (preset / custom / interlaced, reusing
SequenceFormatFields); OK creates the library project then seeds the
first sequence from the chosen format
- 打开项目 prompt advertises the supported project formats
(.ove / .otio / .fcpxml)
- manager.new.project_name across the 8 i18n packs
- action-search first-item test follows the new flow (New Project now
opens its dialog)
文件 → 导出工程文件… now opens an in-app dialog (format dropdown:
OpenTimelineIO / Oak / FCPXML + output path) instead of the bare
platform save prompt; OK routes the chosen extension through the
engine's export_project_path. Adds project.export.format i18n across
the 8 packs.
The 窗口 → 多机位向导 entry is removed from the menu bar and the
action-search list (menu_id 0, the hidden convention); the engine
pathway (wizard dialog, sync, multicam creation) stays intact behind
the action dispatch for a future release. The panel's multi-cam
feature remains.