The sys crate's `static` feature copies the archives at BUILD time and
cargo does not track archive changes: after rebuilding the project
FFmpeg every binary silently keeps the previous objects until
`cargo clean -p ffmpeg-sys-next` (observed: an NVDEC-capable rebuild
never reached the binaries; av_hwdevice_ctx_create(CUDA) then failed
with ENOMEM because the old hwdevice table had no CUDA entry).
On multi-GPU boxes VAAPI's default render node can point at a device
with no VA driver (NVIDIA without libva-nvidia-driver), failing before
the working AMD/Intel node is ever tried; trying CUDA (NVDEC) first is
both the discrete-GPU path and sidesteps that misdirection. VAAPI stays
as the fallback for AMD/Intel-only machines.
Adds a hwcheck example printing which hw device a stream opens with and
the raw av_hwdevice_ctx_create result codes per device type.
Per-chunk submit->render wait and render time (audio_thread), the push
lead against the playhead, the negotiated stream config, and the true
device-side callback rate (outputdevice) -- the ground truth used to
localize the playback starvation to the sequence-rate/clock mismatch.
Two playback-audio defects found while chasing periodic pops, clipped
words and repeated phrases:
1. The playback clocks are created at engine init with the fallback
rate (hd_1080p25) and never updated, while audio chunks are sized by
the SEQUENCE time base (30/1 from the broken default). The playhead
advanced at 25 fps but each chunk carried only 1600 samples, so
production ran at 40000 samples/s against the device's 48000 -- ~17%
of playback time was zero-filled silence (clipped words). tick() now
syncs both clocks to the sequence's actual rate.
2. The underrun self-heal cleared the whole device queue (~190 ms of
unheard content skipped), reset the prefetch (whose first
re-rendered chunk was then always dropped as late) and re-anchored
the master clock, re-rendering already-played audio whenever the
playhead lagged (audible as a pop plus a repeated phrase, every
~1.1 s). It now trims just the stale lead from the queue
(PreviewAudioDevice::drop_front_frames): the zero-fill already
advanced the output clock past that content, so the rest of the
queue stays audible and the anchor stays valid.
Also adds the OAK_DEBUG_AUDIO event log used to diagnose all of this
(prefetch resets, late drops, underruns, heals, reanchor jumps,
per-tick queue state).
set_default_parameters looked up DefaultSequenceFrameRateNum/Den -- keys
that exist nowhere in the config -- so every sequence silently fell back
to 30/1. The canonical setting is the "num/den" string
DefaultSequenceFrameRate (a frame duration; rate = den/num, matching the
footage import path in oak-task). Sequences now default to the
configured 1001/30000.
threads=auto spawns one decode thread per logical core per decoder (16
observed on a 24-core box); with cores-2 render workers decoding
concurrently the machine is oversubscribed several times over and the
playback-audio render thread gets starved (late chunks are dropped).
4 threads decode 1080p h264 far past real-time; preview throughput comes
from the worker pool, not per-decoder threading. Applies to both the
software open path and the hwaccel open path.
retrieve_audio_to decodes whole codec frames but copies only the part
inside the requested chunk; the tail of the frame crossing the chunk end
(up to 1023 samples for AAC) was consumed by the decoder and lost, so
the next chunk started with a hole. On the playback grid (1920 samples
at 25fps/48kHz) the hole cycles 128..896 samples and hits 7 of 8 chunk
starts -- the heavy stutter/noise heard during playback.
Keep the overflow (decode and resampler-flush tails) in a per-session
carry buffer and serve it at the start of the next contiguous chunk;
clear it on seek, format change and chunk failure. Also make seek()
actually drop the cached resampler as its comment claimed.
Verified sample-exact: chunked decode of a 440Hz tone now matches a
one-shot decode bit for bit, and chunked renders of real media line up
with the ffmpeg CLI reference at correlation 1.0 / drift 0.
Adds a regression test (playback_sized_chunks_match_oneshot_sample_exact)
with a new tone fixture -- demo.mp4's audio is -90dB digital silence and
cannot expose the holes -- plus a render_audio_wav example used to
diagnose chunk-boundary artifacts offline.
Overlapping clips sum linearly in mix_audio_montage and can exceed full
scale (two hot clips reach +/-2; gain > 1 would too); the cpal sink
forwarded samples unclamped, so overlaps clipped at the DAC. Clamp the
accumulator after the montage mix (both the heap and shm-slot paths
share mix_audio_montage) and document it on render_audio_samples.
wgpu-core 29's snatch lock tracing (snatch.rs) captures a std Backtrace
on every device/queue lock acquisition when debug_assertions are on.
Profiling the dev build showed ~35% of UI-thread CPU in
_Unwind_Find_FDE/Backtrace::create, which starved playback-audio chunk
scheduling (late chunks are dropped -> pops).
Run 48's Linux leg flaked: full_res_worker_outlives_a_dropped_project
panicked with "the render manager failed to start" while all 236 other
tests passed -- a worker-pool startup race under full-suite parallelism,
not a regression (the same binary passes locally and passed on the
previous commit). Mirror the Windows job's retry-once policy and make
ensure_render_manager log the underlying init error so the next flake is
diagnosable. The Windows leg's real failure (gpui_widgets referenced
SurfaceSource::Texture, which only exists on linux/freebsd) is fixed in
the gpui submodule bump of the previous commit.
Preview now follows the project output colorspace end to end: the
display chain derives its content space from the project's OutputColorSpec
instead of a hardcoded sRGB name, self-managed ICC transforms go through
an XYZ D65 interchange stage (OCIO cie_xyz_d65_interchange) for non-sRGB
targets, and the platform layer declares the content colorspace (gpui
submodule bump). macOS defaults to OS-managed (fixes wide-gamut UI
oversaturation); Windows ACM warns once on non-sRGB targets.
Multi-monitor: the display ICC is looked up per the window's current
screen (macOS display id, Windows per-monitor DC, X11 RandR output
profile) with a throttled poll that invalidates frame caches on moves.
Pipeline precision: 10-bit+ sources fall back to YUV444P16LE + a Rust
matrix conversion when swscale lacks F32 output (no more 8-bit
truncation); BT.709/2020 SDR decodes with BT.1886 gamma 2.4 instead of
the sRGB EOTF; working-space compositing no longer clamps RGB to [0,1]
(alpha still clamped); the output node clamps to the target gamut;
frames without colorimetry metadata convert with BT.709 defaults
(warned once) instead of passing through; scopes read the
output-colorspace signal on both F32 paths.
Also: only emit rerun-if-changed for .env when it exists (a missing file
made every build fully dirty).
- the popup now offers a large SV palette with a hue bar plus RGB/HSV
mode tabs (shared slider model swap, hsv<->rgb pure functions)
- while the eyedropper is armed, clicks outside the popup pick a colour
instead of dismissing it, so the picked draft can still be confirmed
- regression test pinning the OFX plugin colour set/read/undo chain
- i18n: ofx.color.mode_rgb/mode_hsv in all eight packs
It is a plain deferred div rather than an anchored element, so it gets
no automatic BlockMouse hitbox; clicks on its padding fell through to
the controls below.
- oak-worker gains an LRU frame cache (default 64 MiB) keyed by the
render-deterministic spec subset; repeat frames (paused frames,
scrubs over rendered ranges, re-renders after an effect change) are
memcpy-cheap, which is what made adding an OFX plugin -- and the
in-flight batches after removing one -- stall the UI
- audio dispatch on the Processes backend now mixes inline on the UI
tick instead of queueing behind video batches in the worker pool, so
video stalls no longer starve the ~100ms cpal output buffer into
silence
The display path no longer passes through BGRA8: render workers are
asked for F32, rows are repacked, colour management runs on the F32
data, and the frame uploads to an Rgba16Float wgpu texture that the
viewer presents directly on the Rgb10a2Unorm swapchain. The BGRA8
image remains only for scopes, the eyedropper, thumbnails and the
no-GPU fallback.
Preferences gains a display-bit-depth combo stored in the config store;
at startup the app forwards it to gpui_wgpu via OAK_DISPLAY_BIT_DEPTH,
which prefers Rgb10a2Unorm for 10-bit presentation. Takes effect after
restart (noted in the dialog); i18n in all eight packs.
The 18 multicam switch actions were bound globally to keys 1-9, so
gpui dispatched every digit keystroke as an action and text fields
never received it -- every numeric input in the app (slider value
boxes, spin boxes, the colour picker's hex field) could only type 0,
and the colour swatch never updated because hex parsing failed.
The bindings now carry the MulticamPanel key context: digits switch
angles while the panel is focused and type normally everywhere else.
- paused full-res renders now scale by the playback divider instead of
always rendering at native sequence size
- when a clip's proxy is active, sequence/footage renders clamp to the
proxy resolution (never upscale a 720p proxy to 1080p and back),
which was making proxied playback slower than no proxy
Replaces the project-thumbnail picker: the color popup's eyedropper
button arms the program viewer, the next click on the picture samples
that pixel (contain-fit mapped, BGRA from the current CPU frame) into
the picker's draft; Esc or the button cancels. Coordinated through a
global eyedropper mailbox on the engine.
- the eight toolbar tools now drive real editing: razor splits at the
click point, ripple/slip/roll/slide emit new undoable composite
commands (ripple_trim_clip, roll_edit, slide_clip, slip_clip),
zoom clicks zoom anchored at the cursor, track-select selects the
track right of the click; panel tool state and the Tools menu stay
in sync both ways
- the zoom-in/out toolbar icons now actually zoom (anchored at the
playhead) and the snap magnet icon toggles snapping like its checkbox
Every item in the program/source viewer menu now works and reflects
real state: zoom levels, full-screen toggle, safe margins (off/on/
custom), stop-on-last (honoured by both playback clocks), waveform
mode, show-fps overlay, and save-frame (writes a PNG of the current
frame). The viewer widget's in/out/clear buttons are wired to the
shared program workarea.
- File > New > Sequence opens a real dialog (name, PAL/NTSC/HD presets,
width/height/frame rate, progressive/interlaced); VideoParams gains an
interlaced flag
- File > New > Folder creates a folder in the project root
- sequences are mounted under the root folder so they appear in the
project explorer (including the auto-created Sequence 1 and orphans
from older projects); right-click > Sequence Properties edits the
parameters afterwards
- dropping footage onto an empty, sequence-less timeline auto-creates a
sequence from the footage's first video stream
- the swatch now follows the draft color while the popup is open
(cancel still reverts) and set_effect_param failures are logged
instead of silently reverting the swatch
- the popup lists project footage thumbnails; clicking a pixel picks
its color into the draft (contain-fit coordinate mapping, no new
dependencies)
- i18n: ofx.color.pick_project in all eight packs
- clicking anywhere on the track moves the thumb to that position
(double-click still opens the inline editor)
- a compact numeric field next to every slider shows the value and
accepts typed input, clamped to the slider's min/max range
mov/mp4 muxers rewrite the stream time base at write_header (mov timescale
>= 10000) and FFmpeg 9 no longer rescales packets for us, so mpeg2video
clips were muxed with pts in encoder ticks -- a 10-frame/10fps clip became
1ms long and every seek past t=0 decoded to the EOF frame.
- read back the real stream time base after write_header and rescale
packets (including the flush path) before write_interleaved
- warn once when retrieve_frame's EOF fallback returns a frame far from
the requested timestamp
- testmedia round-trip now asserts container duration and per-frame
decode instead of only t=0
Match the timeline UI (V_max drawn topmost): composite tracks from
V1 up to V_max so the highest-numbered track is composited last, in
both the montage path and direct graph evaluation.
- take_node clears the partner's dangling link references
- are_linked checks both directions; link() repairs asymmetric pairs
- paste writes links through the graph API instead of BlockCore.links
- drop link undo removes only its own pair instead of restoring a snapshot
- move_clip_with_links skips off-track partners instead of failing
Proxy generation no longer pegs the machine:
- the transcode probes for a hardware H.264 encoder once per process
and uses it when present (macOS h264_videotoolbox; Windows/Linux
h264_nvenc -> h264_qsv -> h264_amf; libx264 remains the universal
fallback and the untouched C++ parity path), with decode-side
-hwaccel auto (HEVC 4:2:2 10-bit decodes in hardware on Apple
Silicon / RTX 50+ / Intel GPUs and falls back to software cleanly);
quality/preset map from the proxy CRF/preset per encoder
- the concurrency limit is configurable (ProxyMaxConcurrent, default
1) in the Proxy Settings dialog; auto-generated jobs queue and the
next starts when a slot frees. The invariant
concurrency x per-job threads <= logical cores / 2 holds by
construction (thread budget = half the cores / concurrency, clamped
to [1,8], covered by a unit test), and on Unix ffmpeg runs nice -n 10
so the background task never starves the foreground
- deleting a footage's proxy also removes it from the auto-generation
queue
playback_display_tracks_the_playhead asserted the displayed frame lag
the playhead by less than 4 frames at playhead 120 — a render-
throughput assumption that fails under machine load (parallel builds)
rather than for a broken pipeline, which made the test flaky. It now
asserts the machine-speed-independent property: the display advances
while the playhead advances (peak displayed grows between the two
checkpoints), which still catches a permanently frozen picture.
rebuild_timeline synchronously extracted every audio clip's waveform,
and the extractor decodes the clip's ENTIRE audio inline — with two
40-minute files on the timeline, opening the project froze the UI for
seconds ("the project takes a while to open"). Extraction now runs on
a background thread; the cache bumps a version counter on insert and
the engine tick repaints the timeline when it changes.
With the global proxy switch on, footage that needs a proxy now gets
one generated in the background automatically — on import, on project
open, and when proxy use is enabled for a footage — instead of only
through the manual Generate menu action. Footage explicitly opted out
(a recorded proxy path with use disabled) and footage whose last
generation failed are not restarted; the generation path enables proxy
use so preview switches to the proxy as soon as it is ready.
The bin shows the lifecycle as a corner badge on each footage entry
(green P = ready, amber P = generating, red ! = failed; the widget
half lives in the gpui fork, submodule bumped here).
Includes a real-media test driving ProxyTask end-to-end on 4K H.265
4:2:2 10-bit (the media class the 4K playback lag was reported on):
180 s transcodes to a 720p proxy in ~25 s.
The pipeline is ACEScg + F32 end to end by design; a plugin that does
not support F32 must not fail the render — its inputs convert down to
the negotiated depth and its output converts back to F32 (the previous
"Phase 2 F32 only" error path purple-framed those plugins).
- render_driver maps getClipPreferences' output bit depth to
Byte/Short/Half/Float, sets it on the input/output clips, allocates
the output image at the negotiated depth and converts the result
back to F32 for frame assembly (both the CPU path and the
GL-failure CPU fallback)
- the GL path keeps F32 clip params: GL textures are created in the
pipeline format and kOfxOpenGLPropPixelDepth is negotiated
separately, so clip props must match what the plugin actually sees
- fetch_image converts the decoded input down to the clip's
negotiated depth (default F32)
- new Image::convert_depth ([0,1]-normalized conversion between all
depth pairs) plus round-trip and f16 edge tests; f16 helpers moved
into image.rs and shared with clip.rs
Root causes found for the 4K stalls and the second-footage memory
blowup (audit + code review):
- ticket bookkeeping leaked unbounded: the procpool ticket table and
the arena slot map only ever grew (50-100 tickets/sec during
playback, each pinning montage params and shm region views).
Completed/cancelled/superseded/crashed entries are now removed, and
the arena reaps fire-and-forget tickets once finished; the sync poll
path reaps via a terminal result() read. InFlight duplicate submits
now answer State immediately instead of sitting in the map forever.
- decode ran a full-resolution swscale to F32 RGBA (~132 MB at 4K)
plus a second full-res copy before downscaling to the 480px proxy:
RetrieveVideoParams.target_size lets swscale convert AND resize in
one pass (bilinear, matching the old Rust resampler), so a 4K
preview frame costs ~1 MB instead of ~260 MB of churn. This applies
to proxy AND full-res requests alike.
- per-process decoder cache was unbounded (each session pins an FFmpeg
context + 2 native decoded frames): LRU-capped at 16, eviction drops
the map entry (in-flight renders keep their Arc; Drop releases
FFmpeg).
- playback window completions were not generation-gated: a stale
render from before an edit landed in the rebuilt window (wrong frame
displayed, fresh request blocked). Stale completions now return
their shm slot credit instead.
- async audio prefetch used the polling ticket submit without ever
polling: switched to the fire-and-forget submit so entries reap.
Three independent host-side gaps kept real plugins from instantiating:
- clips now define OfxImageClipPropFieldOrder (default OfxFieldNone):
ofxs Clip::getFieldOrder() is a strong read with no default, so
field-aware plugins (Mirror) threw PropertyUnknownToHost ->
MissingHostFeature from createInstance
- OfxParamPropParametricUIColour is no longer predefined as an empty
array: the OFX implicit-create semantics let the plugin's first
propSetDouble create the property and grow it index by index, while
the empty predefined array rejected every write at index >= 1 with
BadIndex (ColorLookup, HueCorrect describeInContext)
- Host::create_instance_preferred: context selection with fallback
(filter -> general -> tracker -> paint -> rest). TrackerPM advertises
the filter context but its createInstance fetches a Mask clip that
its describe only defines for tracker/general/paint; Natron simply
instantiates it in the tracker context, and now so do we. Both the
node-registration scan and the shared instance factory use it.
Still unsupported, by design: Premult/Unpremult (this openfx-misc
build hard-requires the Nuke multi-plane suite + dynamic choices) and
the stereo view plugins (Switch/anaglyph/joinViews/etc. need Natron's
isNatron multi-clip folding or view rendering).
Verified: ColorLookup, HueCorrect, Mirror, TrackerPM now instantiate;
full oak-plugin suite green.
The OFX abort contract is "0 = keep rendering, anything else = stop"
and the ofxs support library literally checks abort(...) != 0. Our
host answered kOfxStatReplyNo (13) for "not cancelled", so every
openfx-misc processor aborted its pixel loop before writing a single
row: the render action returned OK with the output buffer untouched —
a solid black frame everywhere an effect was applied (the previous
smoke test only asserted "not the purple failure frame", which let
this slip through as a fake pass).
- image-effect abort: return 0 when not cancelled, 1 when cancelled
(instance cancel flag or progress cancellation), BadHandle for
descriptor handles as before
- strengthen the real-plugin smoke tests to also reject an all-zero
output frame, so a silent no-write can no longer pass
- more trace-gated [ofx] diagnostics (value dumps for propGetN /
propGetPointer / clipGetImage fetches, multiThread entry, render
output buffer address) — the tooling that pinned this down
Verified with lldb watchpoints: the AddOFX/ChromaKeyerOFX processors
now write the real pixels through the executor path.
gpui delivers DragMoveEvent to every drop target with a matching
payload type under the cursor, and every slider shared the SliderDrag
payload type — dragging one slider (e.g. an OFX panel parameter) also
moved every other slider the cursor passed over (timeline zoom, track
height). Tag the payload with the starting control and ignore drag
moves that are not ours.
Also finish the gesture on mouse-up (inside or outside the track)
instead of on_drop: gpui only delivers drop events to the hovered
target, so releasing outside the track used to leave the drag
unfinished and the final value/undo edit unemitted.
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.