- MulticamPanel: playback angle refresh runs one cycle per 3 ticks
instead of re-requesting every source every tick — each angle decode
is a keyframe-scanning FFmpeg seek that stole worker capacity from
the main viewer
- PreRender frames default 120 -> 12: a window larger than what the
pool can render in real time queues far ahead of the playhead, so
the painted frame lags seconds behind (playback frozen); a smaller
window keeps the backlog bounded
- render_graph_frame: OAK_PERF clip-level timing
The per-process 'hardware session == 1, evict before every new open'
guard was turning every frame's open into a decoder re-open: after
inserting the fresh session, the NEXT frame's pre-open eviction dropped
it again, so no request ever hit the cache ([open] (request) on every
single frame, zero CACHED hits). Every decode then cost a full FFmpeg
session open (~0.5 s) + a keyframe-scanning seek — playback could never
keep up (the 'main viewer barely moves' report; [perf] showed 1.6-2.2 s
per frame).
Hardware VRAM is bounded by the LRU cap (hardware-first eviction at
MAX_CACHED_DECODERS) and the gpu-vram worker-count policy; the eager
pre-open eviction was the regression.
Re-applies the proxy selection (with the -1 tolerance) after the
revert: the node-graph viewer decode must stand in a ready proxy for
the 4K original, and a proxy generated before the stream-index
recording (index -1) still matches the first video stream rather than
silently falling back to the 4K source (the 'main viewer got stuck'
report).
- multicam_switch_to: a NO-SPLIT switch only changes current source —
push snapshot + invalidate preview windows, skip the full
apply_edit rebuild (the 'switch once then everything grinds' hit)
- MulticamPanel: playback angle refresh runs one cycle per 3 ticks
instead of re-requesting every source every tick (each angle decode
is a keyframe-scanning FFmpeg seek)
- footage value(): a ready proxy stands in for the original media in
the node-graph decode request (proxy enabled but 4K decoded report)
- proxy_resolution made public (engine's proxy-first preview size)
gpu_vram_bytes() now chains per vendor/platform:
- NVIDIA everywhere: nvidia-smi (shipped by the NVIDIA driver on every
OS) — the NVDEC path's primary device.
- AMD/Intel on Linux: the DRM mem_info_vram_total/used sysfs attributes
(amdgpu, i915, Xe). Free = total - used; the first non-zero card wins
(an iGPU without dedicated vram reports 0 and is skipped). The walk is
now testable via an injectable sysfs root; a card missing the attrs is
skipped, never aborts the walk.
- Apple Silicon: unified memory — no separate vram exists; the RAM/4
budget IS the correct bound for decode surfaces and render targets, so
no query (an explicit vram budget would double-count the same pool).
- Windows AMD/Intel: no portable CLI; DXGI QueryVideoMemoryInfo is the
real API but wgpu 25 does not expose it. Falling back to the RAM
policy is safe-side (under-sized pool loses throughput, never OOMs the
device).
Tests: the sysfs walk (fixture with a missing-attr card, a 0-total
iGPU and a discrete winner) and the per-worker budget scaling (1080p
baseline, 4K ~4x, 60 fps over-provision).
- open_hw_accel marks the device unavailable when the decoder OPEN fails
(cuvidCreateDecoder OOM at 4K) too, not just device-context creation:
without it every subsequent decoder session retried CUDA and flooded
the log per open.
- Decoder gains hardware_decoding(); the oak-render decode-session LRU
evicts hardware sessions first (each pins a GPU surface pool — ~100 MB
at 4K), so a full cache cannot exhaust video memory before the next
open.
- Worker pool count now factors GPU vram: per-worker budget = 1 GiB
(1080p peak) scaled by pixel ratio + 256 MiB idle floor, 10% reserve
of free vram; applied when hardware decoding is on (nvidia-smi query,
None otherwise falls back to the RAM/CPU policy).
- Dynamic pool resize: ProcessDispatcher::set_target_workers grows or
retires workers; retiring ones stop claiming, drain their in-flight
batch (future playback frames included), then exit naturally on the
shutdown signal — no mid-work kill (30 s deadline only as a hung-
decoder last resort). A retiring worker that dies re-queues its frames
to surviving workers. Resizes are throttled to 2 s (a resolution burst
merges; only the latest target applies) so 1080p<->4K flaps cannot
thrash process spawns.
- RenderManager::set_workspace_size announces the sequence resolution;
RealEngine calls it from refresh_sequence_info.
- Integration test: shrink 3->1 mid-wave (all frames complete, retired
workers exit naturally) then regrow 1->3 and render a fresh wave.
- Project explorer header gains a 新建序列 button (opens the existing
new-sequence dialog, seeded like the menu action).
- Sequence presets gain 4K UHD (3840x2160@25) and 4K DCI (4096x2160@24);
the sequence-properties dialog re-selects them on reopen. Format fields
are also seedable from a probed footage format (the drop flow).
- Chroma Key (and Color Difference Key) value() now box a ShaderJobPayload
like Despill: the old OCIO-processor gate pushed nothing (the processor
is never populated without the render bridge), so the traverser handed
the clip NodeValue::None and the rendered frame lost the clip. The
renderer resolves the OCIO stub at compile time from OCIO_SHADER_STUBS.
End-to-end graph test: green key on a green frame keys out, red key
keeps it.
- The OFX color picker's SV palette / hue bar / preview / swatch canvases
get size_full(): the bare canvases collapsed to zero height in the
block layout, so the palette painted nothing (the reported 色板没显示).
Regression test clicks the palette center and expects mid s/v.
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.
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.