Commit Graph
142 Commits
Author SHA1 Message Date
Mike-Solar 3be8c3e8fd codec: prefer native NVDEC over VAAPI on Linux
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.
2026-08-30 00:31:32 +08:00
Mike-Solar 197212bc46 app: extend OAK_DEBUG_AUDIO diagnostics to the render thread and output stream
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.
2026-08-29 23:31:48 +08:00
Mike-Solar 84caa3589b app: sync the playback clock to the sequence rate; heal underruns by trimming
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).
2026-08-29 23:31:35 +08:00
Mike-Solar 10ec63efb3 node: read the canonical DefaultSequenceFrameRate key for sequence defaults
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.
2026-08-29 23:31:16 +08:00
Mike-Solar d2ac02a474 build: relax the ocio-rs constraint to 0.2
CI / Build & test (Linux) (push) Successful in 17m43s
CI / Build & test (Windows) (push) Failing after 32m37s
Pre-existing local resolution fix so the dependency set resolves here;
Cargo.lock follows (getrandom 0.4.3 -> 0.3.4 in the ocio tree).
2026-08-29 19:52:28 +08:00
Mike-Solar 81c3839937 codec: cap FFmpeg decoder frame threads at 4
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.
2026-08-29 19:52:28 +08:00
Mike-Solar 9dab9efc38 codec: carry codec-frame overflow across audio chunk boundaries
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.
2026-08-29 19:51:59 +08:00
Mike-Solar e128a6aca1 render: clamp mixed audio to [-1,1] before it reaches the device
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.
2026-08-29 19:51:25 +08:00
Mike-Solar cbd4ba2442 optimize: video and audio render
CI / Build & test (Linux) (push) Canceled after 0s
CI / Build & test (Windows) (push) Canceled after 0s
2026-08-29 05:01:55 +08:00
Mike-Solar ca18105c8b ci: retry the Linux test suite once; log render-manager init failures
CI / Build & test (Linux) (push) Successful in 18m4s
CI / Build & test (Windows) (push) Successful in 34m6s
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.
2026-08-29 00:37:24 +08:00
Mike-Solar fdb5caabd5 color: non-sRGB preview, per-monitor display ICC, pipeline hardening
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).
2026-08-29 00:24:15 +08:00
Mike-Solar 879ff8da5b app: PS-style colour picker and an eyedropper that keeps the popup open
CI / Build & test (Linux) (push) Failing after 9s
CI / Build & test (Windows) (push) Failing after 22s
- 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
2026-08-27 20:43:27 +08:00
Mike-Solar c730061018 app: occlude the app-owned ComboBox dropdown
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.
2026-08-27 20:43:09 +08:00
Mike-Solar 9c0269bcbb render: cache worker frames and decouple audio from the video workers
- 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
2026-08-27 20:35:50 +08:00
Mike-Solar 246de6352f app: true 10-bit presentation -- F32 frames reach the swapchain unquantized
CI / Build & test (Linux) (push) Failing after 17m38s
CI / Build & test (Windows) (push) Failing after 17m40s
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.
2026-08-27 19:55:05 +08:00
Mike-Solar 5d21f83e1e render/app: display bit depth option (10-bit default, 8-bit optional)
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.
2026-08-27 19:08:55 +08:00
Mike-Solar 82ad9891ad app: white fill with black border for unchecked checkboxes
Keeps the white well but swaps the water-blue theme border for black,
per user feedback.
2026-08-27 19:08:19 +08:00
Mike-Solar eb5643249c app: scope multicam digit shortcuts to the multicam panel
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.
2026-08-27 19:08:19 +08:00
Mike-Solar 719e43eba3 app: render at proxy resolution and honour the playback divider
- 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
2026-08-27 18:54:03 +08:00
Mike-Solar 54945e39af app: pick colors from the program viewer frame (PS-style eyedropper)
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.
2026-08-27 18:32:34 +08:00
Mike-Solar 1ae03c1b1e app: real timeline tools, live zoom buttons, clickable snap toggle
CI / Build & test (Linux) (push) Successful in 20m7s
CI / Build & test (Windows) (push) Successful in 41m55s
- 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
2026-08-27 17:59:27 +08:00
Mike-Solar 4e619efb93 app: make the viewer context menu real
CI / Build & test (Windows) (push) Canceled after 12m22s
CI / Build & test (Linux) (push) Canceled after 14m4s
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.
2026-08-27 17:39:03 +08:00
Mike-Solar bad2f9552e app: sequence management -- new-sequence dialog, folders, sequences in the project bin
- 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
2026-08-27 17:38:30 +08:00
Mike-Solar d65b84a6ca app: live color swatch and a project-media eyedropper in the color picker
- 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
2026-08-27 17:34:28 +08:00
Mike-Solar 2dfdf8a5b0 app: click-to-jump and a persistent value box on sliders
- 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
2026-08-27 17:32:48 +08:00
Mike-Solar a372d72446 app: white background with inner shadow for unchecked checkboxes
The unchecked state used the panel background color, making the box
nearly invisible on the dark theme.
2026-08-27 17:32:37 +08:00
Mike-Solar 9f88b9b856 codec: rescale packet timestamps to the stream time base the muxer chose
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
2026-08-27 17:32:00 +08:00
Mike-Solar c03f1ec604 render: stack higher-numbered video tracks on top
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.
2026-08-27 15:20:18 +08:00
Mike-Solar 663d879908 node/app: keep clip links symmetric and intact across paste, delete, and undo
- 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
2026-08-27 15:20:18 +08:00
Mike-Solar f7352ae19d render: fall back to CPU when the adapter cannot render Rgba32Float
CI / Build & test (Linux) (push) Successful in 19m8s
CI / Build & test (Windows) (push) Successful in 32m35s
2026-08-27 13:52:03 +08:00
Mike-Solar cbf9d3c543 app: silently ignore clip drags across track kinds (video<->audio) 2026-08-27 10:13:11 +08:00
Mike-Solar 8c306986ab app: stamp clips with a stable creation-time color and footage-name label 2026-08-27 10:13:11 +08:00
Mike-Solar c51a349070 render: translate node GLSL shaders to WGSL and run them as wgpu passes
CI / Build & test (Linux) (push) Failing after 16m57s
CI / Build & test (Windows) (push) Successful in 31m46s
2026-08-27 07:07:09 +08:00
Mike-Solar 4f6c44a0d0 proxy: hardware-accelerated transcode with configurable, resource-aware concurrency
CI / Build & test (Linux) (push) Successful in 19m14s
CI / Build & test (Windows) (push) Successful in 32m5s
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
2026-08-26 06:40:06 +08:00
Mike-Solar ca64b61156 app: drop the throughput assumption from the playhead tracking test
CI / Build & test (Linux) (push) Successful in 19m21s
CI / Build & test (Windows) (push) Successful in 31m53s
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.
2026-08-26 04:23:11 +08:00
Mike-Solar 96df9293e5 app: extract audio waveforms off the UI thread
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.
2026-08-26 04:23:11 +08:00
Mike-Solar 09c7e7de32 app: auto-generate proxies in the background and badge them in the bin
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.
2026-08-26 04:22:44 +08:00
Mike-Solar 287a17399b plugin: render non-F32 OFX plugins by converting in and out
CI / Build & test (Linux) (push) Successful in 18m49s
CI / Build & test (Windows) (push) Successful in 32m47s
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
2026-08-26 01:31:35 +08:00
Mike-Solar fa3951344b render: fix 4K playback memory growth and decode-to-target-size
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.
2026-08-26 01:31:10 +08:00
Mike-Solar ea9b451d0b plugin: instantiate more real-world plugins (FieldOrder, UIColour, context fallback)
CI / Build & test (Linux) (push) Successful in 18m55s
CI / Build & test (Windows) (push) Successful in 33m21s
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.
2026-08-25 21:56:33 +08:00
Mike-Solar 3792c49854 plugin: abort() must return 0/non-zero, not ReplyYes/No — fixes black frames
CI / Build & test (Linux) (push) Successful in 18m37s
CI / Build & test (Windows) (push) Successful in 44m45s
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.
2026-08-25 20:30:37 +08:00
Mike-Solar 3bf914a2b8 app: stop slider drags from leaking into other sliders
CI / Build & test (Linux) (push) Successful in 18m42s
CI / Build & test (Windows) (push) Successful in 44m58s
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.
2026-08-25 19:10:25 +08:00
Mike-Solar a56f6ddf87 app: link the rear halves when splitting an A/V pair at the playhead
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.
2026-08-25 19:10:08 +08:00
Mike-Solar 2828984187 plugin: fix OFX host property-suite gaps that purple-framed real plugins
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.
2026-08-25 19:04:18 +08:00
Mike-Solar f2aab8ce15 render: apply clip effect stacks in the montage path
CI / Build & test (Windows) (push) Failing after 16m26s
CI / Build & test (Linux) (push) Successful in 19m12s
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.
2026-08-25 04:49:00 +08:00
Mike-Solar 28f4ed655e app: refine the link toggle + human-readable node ports
CI / Build & test (Linux) (push) Successful in 18m51s
CI / Build & test (Windows) (push) Failing after 17m33s
Link toggle rule refinement (on top of 1ad2d71c8): the toggle now
unlinks only when the selection is FULLY linked internally — the C++
crude "any member has ANY link" check made split halves (which inherit
the original clip's A/V links via BlockSplitPreservingLinksCommand)
impossible to link to each other. Test: link_unlink_toggles_the_
selection_links covers toggle/undo/split-link.

Node editor: only stream inputs (texture/samples) become ports — OFX
plugins declare every parameter as an input, which buried the card
under internal params (the garbled NatronOfxParamProcess* wall). Secret
inputs stay hidden; port labels use the input's display name (the OFX
param label); edges resolve ports by input id, not by label.
2026-08-25 03:53:00 +08:00
Mike-Solar 1ad2d71c81 app: implement Link/Unlink for timeline clips
CI / Build & test (Linux) (push) Canceled after 8m56s
CI / Build & test (Windows) (push) Canceled after 7m16s
编辑 > 链接/重新链接 (Cmd+L) was an unhandled stub
(PanelCommandHandler::toggle_links returned false), so linking clips
never created graph links and linked drags never happened.

- graphops::set_clips_linked: one undoable entry toggling every pair of
  the selection; the undo restores the exact prior internal topology.
- RealEngine::toggle_clip_links: fully-linked selections unlink,
  otherwise the selection links together (the C++ crude "any member has
  ANY link" rule would have made split halves — which inherit the
  original clip's A/V links — impossible to link to each other).
- TimelinePanel overrides toggle_links, routing the timeline selection.
- MockEngine stores demo link pairs.
- Test: link_unlink_toggles_the_selection_links — dropped A/V pair
  toggles off/on with undo, and split halves link manually.
2026-08-25 03:40:57 +08:00
Mike-Solar 3cc6f75ccc docs: archive completed plans, add the external plugin system design
- docs/zh/plans: finished plans move to completed/ (the RIIR series, the
  event-bridge and dependency plans, the v04 manual test plan).
- New design docs: the external (functional) plugin system
  (process-isolated, JSON-RPC/shm) and its protocol.
- ai-agent-design refreshed; README pointers follow the moves.
2026-08-25 03:03:28 +08:00
Mike-Solar 8679bef6de components: focus-gated spin wheel, spin text entry, combo outside-dismiss
CI / Build & test (Linux) (push) Canceled after 7m56s
CI / Build & test (Windows) (push) Canceled after 6m26s
Touchpad fixes from user testing:

- SpinBox: the wheel only steps a FOCUSED field — hover-wheel is inert,
  so two-finger scrolling across a dialog no longer drifts values.
- SpinBox: direct numeric entry — double-click opens the editor (the
  app's text input; commit on blur, Escape cancels), typing a digit on
  the focused field starts editing with that character. Previously the
  field accepted no text at all.
- ComboBox: a click anywhere outside closes the open popup
  (on_mouse_down_out).
- gpui bump: the timeline header-occlusion fix.
2026-08-25 02:51:44 +08:00
Mike-Solar 186c41b894 app: about dialog, modal close affordances, more preferences
CI / Build & test (Windows) (push) Failing after 16m35s
CI / Build & test (Linux) (push) Successful in 18m59s
- Help > About Oak… opens a real dialog (was a placeholder print): name
  + version, the GPL-3.0 line, the Olive fork notice and the thanks to
  Enzo GD (Community Promoter).
- Modal (gpui_widgets, submodule bump): a title-row close button on
  every dialog and opt-in backdrop dismissal (dismiss_on_mask) — Help >
  Search Actions and About use it, so they close on an outside click
  (previously a buttonless modal could only be closed with Escape).
- Preferences general tab grows two live settings: the cache-ahead
  pre-render window (PlaybackPreRenderFrames, consumed by the preview
  scheduler every playback tick) and the library storage backend
  (Storage/Backend sqlite/pg + Storage/PgUrl connection string, read by
  oak-storage's write-through when a project binds; the pg URL field
  shows only for PostgreSQL and commits on dialog close). The C++
  auto-recovery toggle is skipped on purpose: the Rust snapshot loop
  has no disable semantic, so the switch would be dead.

i18n keys for all eight packs; new tests cover the about modal and the
preferences writes.
2026-08-25 01:55:08 +08:00