- Actions moved to their latest majors: checkout v4->v7, cache v4->v6
(restore/save too), upload-artifact v4->v7, download-artifact v4->v8,
action-gh-release v2->v3. rust-cache and rust-toolchain already track
their latest majors.
- vcpkg caches now save under a per-run key and restore through
`restore-keys` (newest entry for the exact manifest first, then the
newest entry for that OS): saving can no longer fail because the key
already exists ("update if present, create if not"), and a manifest
change reuses vcpkg's content-addressed archives instead of rebuilding
every port.
- Every Test step gets an explicit 40-minute bound. macOS had no watchdog
at all (the Linux/openKylin scripts carry their own) and the current run
has been sitting in Test with no progress; a hung suite now fails the
step instead of running to the job timeout.
- Windows: the first test executable died with STATUS_DLL_NOT_FOUND
(0xc0000135) because vcpkg's x64-windows DLLs (ffmpeg and its codecs)
were not on PATH; add vcpkg_installed/x64-windows/bin in the configure
step.
- openKylin: failure_paths_report_cleanly asserts that a read-only library
write fails, but the container runs as root, which bypasses the file
permission bits (CAP_DAC_OVERRIDE) and the write succeeds; skip that
sub-check when euid is 0 (read from /proc/self/status on Linux) and note
it in the log.
- Disable Windows Defender real-time/script/archive scanning (and exclude
the workspace, cargo, rustup and vcpkg trees) at the start of the
Windows job: the ephemeral runner spends a large share of a cold build
having every object file scanned.
- Raise the openKylin container /dev/shm from 2 GiB to 8 GiB: the suite's
parallel worker pools plus the 512 MiB shared-memory spike used to run
dry, surfacing as an intermittent SIGSEGV in the oak-render tests.
- Add a gdb backtrace step on failure for the big suites (the openKylin
image installs gdb) so a native crash lands in the log next time.
Four independent CI failures the first real cross-platform run surfaced:
- macOS SIGSEGV: the real-GL unit tests in gl_bridge ran wherever CGL is
available, including the headless CI runner. Gate them with the same
OAK_GPU_TESTS switch the integration GL tests already use (skip on CI,
opt in on a real Mac).
- Windows build: examples compile under `cargo test`, and bench_playback
used libc::getrusage unconditionally. Keep the Unix CPU accounting
behind #[cfg(unix)] and report zero CPU seconds elsewhere.
- openKylin arm64: engine_without_a_project_hits_the_guard_paths assumed
the library backend was unconfigured while a parallel config test
transiently set Storage/Backend=sqlite. Take the shared config lock and
pin the key off for the test's duration.
- openKylin x64: the prefetch smoke test asserted an exact decode count,
but the hand-off LRU holds only DECODE_LRU_CAP (2) frames, so a request
can miss the prefetched copy under scheduling pressure and re-run the
producer (the eval cache still serves the pixels). Bound the count
instead of pinning it; the deterministic sibling test pins read-ahead
usage.
Dropping a new clip whose in-point landed in empty track space (the
stored-range model allows holes between blocks; the C++ layout is
contiguous) left the overlapped clip untouched AND inserted the new clip
before it in track order, so it slid UNDER the clip it covered. Starting
on a clip already overwrote correctly, so the behavior depended on where
the in-point happened to fall.
TrackRippleRemoveAreaCommand::prepare now handles the hole case (no
block spans the range start): nothing is trimmed on the left, the
insertion anchor is the last block ending at/before the range, and the
shared trailing scan removes/head-trims the blocks the range covers.
Regression tests: domain_test (command level) and graphops (the app's
place_footage_clip path).
The openKylin container image ships no fonts at all, so the font-backend
combo test found zero families and failed both retries; install
fonts-dejavu-core in the job (the Ubuntu runner image already has them).
Also includes the runner-size bumps (windows 32x, macos 12x) and the
vcpkg/cargo cache save conditions (`if: always()`).
The macOS job finally reached the build (after the vcpkg manifest fix) and
hit a macOS-only compile error in the VideoToolbox import: `*ptr as
*const T` parses as `(*ptr) as *const T`, so `sw_format` was read off a
pointer instead of the AVHWFramesContext. Bind the frames pointer first.
Also fix the warnings the cross-check surfaced: the redundant
MTLPixelFormat import, and doc comments on an extern block and a
thread_local! (rustdoc does not document those).
Verified locally with a host-cc wrapper:
`cargo check -p oak-core -p oak-codec -p oak-node -p oak-render
-p oak-task -p oak-plugin --target aarch64-apple-darwin` is clean.
(oak-app itself needs a real Apple toolchain for ring.)
The other app test modules nest the process-wide locks language then
config; taking them the other way round could deadlock two tests running
in parallel.
On every startup (unless Preferences > General turns the new "Check for
updates" toggle off) the app GETs
https://www.oakvideoeditor.org/api/v1/update/latest, parses the
documented latest-release JSON and, when the remote version is newer
than the running build, prompts with a dialog whose primary button opens
https://www.oakvideoeditor.org/downloads. The blocking fetch runs on the
gpui background executor with a 5 s bound; transport and parse failures
are silent, and a release found while another modal is up (the project
manager on a fresh start) is deferred until the modal layer frees.
The transport sits behind an `UpdateTransport` seam so tests script the
response without touching the network; version comparison strips the
`v` prefix and pre-release suffixes and orders the components
numerically (an unparsable remote falls back to string inequality).
Help > Report a Bug... opens
https://www.oakvideoeditor.org/bug-report.
The eight shipped i18n packs carry the new menu/preferences/update keys.
The openKylin image runs as root while Actions sets HOME=/github/home;
dtolnay/rust-toolchain fails with "$HOME differs from euid-obtained home
directory" and the job never reaches the build. Pin HOME/CARGO_HOME/
RUSTUP_HOME to root's for the whole job.
vcpkg resolves the ffmpeg dependency before anything builds and rejects
the manifest because the pinned 8.1.2#3 port has no `png` feature
("ffmpeg@8.1.2#3 does not have required feature png needed by oak"),
so every desktop job died in "Install dependencies (vcpkg manifest)"
and never reached the build or test steps.
PNG decoding in FFmpeg needs zlib (png_decoder_deps=zlib); libpng is
only the encoder backend and this port never enables it. Request
`zlib` instead.
Also point the stale comments/docs at the actual pin: the override is
8.1.2#3 (matching the ffmpeg-next 8.x binding after the 9.0.0 binding
was found broken upstream), not 9.0.1#1.
PanelHandle snapshots DockPanel::title at registration and the tab strip
renders that cache, so switching the UI language at runtime (the menu's
language items or the preferences combo) left every docked tab in the
previous language: the English UI with Chinese tabs from the report.
Add dock panel title/content refresh to gpui (oak-gpui 45871acf1a):
DockArea::refresh_panel_titles re-reads every held title through a
type-erased provider captured from the concrete panel entity, marks each
panel view dirty so its localized content re-renders, and repaints the
chrome. Call it from both shell language-switch paths; the preferences
dialog repaints itself too.
The app test pins the refresh end to end: after LanguageChanged the
project tab's cached title follows the new language.
The two BlockCore length setters swapped their anchors relative to the
C++ semantics they document, so the ported edit commands produced wrong
geometry on the live UI paths: roll edits kept the seam still, slides
left negative in-points, and trims wrote the timeline in-point into
media_in (playing the wrong media content).
Adopt three stored-range primitives in block.rs:
- set_length_and_media_out: in fixed, out moves, media untouched
(resize, trim-out, gaps growing rightward).
- set_length_and_media_in: in fixed, out moves, media_in += old-new
(resize-with-media-in, splice right half, ripple trim-in).
- set_length_keeping_out (new): out fixed, in moves, media_in +=
old-new (trim-in body and out-neighbour, slide out-neighbour,
ripple trim-in of the trailing block).
Point every command at the primitive matching its intent (undopointer,
undogeneral, undoripple, undosplit, graphops, cli, nodeops) and fix the
two real defects the swap hid:
- TrackReplaceBlockWithGapCommand grew a following gap rightward,
swallowing whatever followed it: the "dragging one clip moves
unrelated clips" regression. The gap now grows leftward over the
removed block's span; regression test in domain_test.
- The ripple/splice trims now advance media_in instead of rewriting it,
and BlockSplitCommand writes both halves' ranges and media
explicitly (the second half continues from the split point).
Rewrite the KNOWN-SWAP expectations to the correct geometry (roll moves
the seam, slide has no negative in-point, insert-gaps grows rightward,
resize-with-media-in yields media_in = 20) and add the missing media
assertions. TrackSlideCommand documents that the caller positions the
sliding blocks (the stored model has no track layout).
The OCIO grading primaries expose contrast/offset/exposure as Vec4 inputs
(master + RGB); build_control had no Vec4 arm, so the inspector rendered an
empty read-only row instead of controls. Add a four-spin arm bounded by the
per-component min/max and stepped by the node's base.
Float inputs that only declare base (pivot 0.18, clampBlack/White) fell
through to the wide +/-10000 default and a single drag could hurl the value
thousands of units away; anchor the slider on value +/-100*base instead,
matching the C++ RationalSlider step semantics.
Mock/real engine boundaries, shell modals and menus, timeline/
inspector/node-editor/project-explorer panels, dialogs, and the
editor controls, including the review remediation assertions.
Render evaluation fallbacks, the process pool (dispatch, cancel,
restart, teardown), half-float display packing, and the worker's
shared-memory job paths; includes the M5 footage import acceptance
tests and the software-decode byte-exactness guard.
Fixture-backed unit and contract tests for FFmpeg helpers and state
machines, OCIO color factories, wgpu backend fallbacks, and the safe
parts of the platform import module (review report section 10.2).
Adds the 90/80 coverage plan and the two-round review report, updates the
M5 backfill and plan index, moves finished plans to completed/, and
removes the machine-specific tarpaulin HTML report from the tree.
- ForceParams: hand-written Default with force_format = -1 (was 0 = U8,
which pushed the F32 pipeline into the U8 scale path).
- Plugin clip output: write CPU pixels back into the target texture
instead of the deep clone returned by texture_get_frame.
- Display ICC: probe the Debian/Ubuntu icc-profiles-free path.
- RippleInfo: public constructor and accessors so the ripple command is
reachable from integration tests.
- MockEngine: record effect-parameter and push-button attempts so the
params-view routing tests are falsifiable.
- OFX params: log rejected parameter writes instead of discarding them.
- Manager docs: state the synchronous codec-submission contract.
Adds VAAPI DMA-BUF, D3D11VA shared-handle and VideoToolbox IOSurface
imports behind a tri-state outcome (imported / unsupported / failed),
planar textures with bounded residency and a CPU staging fallback, the
staged montage decode path, reference-counted decoder frames, VAAPI-first
device selection on Linux, and the host-GPU context plumbing used by the
app and worker. See docs/zh/plans/render-pipeline-threads.md (M5).
- 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.
Three independent failures from the last run:
- Linux undefined `vaMapBuffer2`: the runner had apt libva 2.20 while
vcpkg builds shared libva 2.24.1, and the loader had no path to it.
Drop the apt libva packages and put `$prefix/lib` on LD_LIBRARY_PATH
for the Linux and openKylin jobs.
- openKylin "render manager failed to start": a container's /dev/shm is
64 MiB, but the process pool reserves >64 MiB per worker with
posix_fallocate, so segment creation fails. Run the container with
`--shm-size=2g`.
- macOS "no decoder for codec PNG": the vcpkg ffmpeg manifest never
enabled the `png` feature the oak-cli transcode tests need. Add it.
- Windows dependency install took 41 minutes: it was rebuilding FFmpeg
and every codec port from source because the binary archive cache was
never saved. The combined `actions/cache` step with the deprecated
`save-always: true` does not save after a failed job, and the previous
run failed in Build. Replace it in all four jobs with an explicit
restore + `actions/cache/save (if: always() && cache-hit != 'true')`.
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.
- vcpkg bootstrapped in every job (the Warp runners carry none):
clone + bootstrap into .cache/vcpkg, VCPKG_ROOT exported.
- vcpkg.json: ffmpeg pinned at 9.0.1#1 via overrides with
builtin-baseline 771b0a2e pinning the port tree; feature fixes
(gnutls -> openssl, ffnvcodec -> platform-qualified nvcodec,
vaapi on Linux, librsvg windows-only).
- Linux and macOS CI/CD jobs also take FFmpeg from the manifest
(static triplets x64-linux / arm64-osx keep the packaging story);
the build-ffmpeg.sh steps, FFmpeg caches and the codec dev
packages leave the workflows — system package managers keep only
the X11/audio/GL/Vulkan/tooling deps, now documented in
docs/build.md.
- New openKylin container job (openkylin/openkylin:latest) on x64
(warp-ubuntu-latest-x64-8x) and ARM64
(warp-ubuntu-latest-arm64-16x, arm64-linux triplet): openKylin
package names surveyed against the live image's apt index
(nasm/zip come from the kylinsoft anything3.0 PPA), clang for
bindgen, xvfb + lavapipe headless tests with the watchdog and
retry policy.
Known follow-ups (declared in the commit chain): vcpkg has not run
end-to-end yet, the pkg-config vs pkgconf executable name on
Windows, TLS semantics moving gnutls -> openssl.
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.
The MinGW path kept fighting the environment (the GitHub image's
MSVC INCLUDE/LIB poison the GNU compiles; ocio-sys' fork then
dragged the MSVC-only headers into g++ and died on vcruntime.h).
The Windows jobs on both workflows now:
- run on warp-windows-2025-vs2026-x64-16x (preinstalled VS 2026)
with the stable MSVC Rust toolchain;
- install dependencies through vcpkg MANIFEST mode: vcpkg.json at
the repo root carries FFmpeg with every free codec + hwaccel
(mirroring build-ffmpeg.sh's configure), pkgconf and librsvg;
the vcpkg_installed tree plus the binary-cache archives are
cached on the manifest hash with save-always;
- build OCIO bundled (ocio-sys' vendored sources are what MSVC
wants — the MSYS2 package was the workaround, not the
preference), so OCIO_RS_NO_MSVC_INCLUDES is gone;
- ship the vcpkg runtime DLLs next to the binaries in the NSIS
installer with a static-CRT release build (no vcruntime DLLs),
replacing the ntldd-based MSYS2 bundling.
FFmpeg version pinning via builtin-baseline is a documented
follow-up: the Configure step logs vcpkg list so the first green
run reports the resolved versions. docs/build.md keeps the MSYS2
flow as the local alternative and points at the CI path.
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.
Two user-mandated amendments:
- Decode must be GPU wherever possible and share the render GPU's
memory: hardware surfaces (NV12/P010) are imported as GPU textures
via the platform interop paths (DMA-BUF / DXGI / IOSurface /
CUDA-Vulkan), av_hwframe_transfer_data is never executed on the hw
path, and CPU decode + staging upload demotes to fallback only.
FFmpeg hwaccel first (the hwdecode.rs device model already builds
the device contexts; upstream Olive has no hw decode at all, so the
reference for this part is FFmpeg + the existing crate), hand-written
GPU decode strictly second. YUV->RGB becomes a built-in GPU pass
replacing CPU swscale. Milestone M5 becomes the GPU-decode
zero-copy track with HW_TRANSFERS zero as its acceptance counter.
- The Job graph becomes a real adjacency structure (no linear table,
no 2D array, possibly not fully connected) with a fixed pair of
virtual GraphInput/GraphOutput nodes per graph: connected by
default, undeletable, un-duplicable, shown in the node editor.
resolve is a Kahn-style BFS from the input node — multi-input joins
wait for every input, multi-output fans out, the order is
deterministic and graph-explicit, cycles error out, unreachable
nodes never run — until every branch converges at the output node.
M0 splits into M0a (Job enum + single-loop match) and M0b (virtual
endpoints + BFS + node-editor display).
Task book for the render-pipeline rearchitecture: one decode thread
and one render thread feeding queue-linked stages with the main
process presenting (the GPU's single queue makes the multi-process
backend dead weight), one dedicated OpenFX host process with
bounded respawn, GPU-resident frames end-to-end except at the CPU
OFX/export/cache boundaries, a per-backend interop table, and the
resolve rewrite to a single-loop match over a completed Job enum
(CacheJob included) following upstream Olive's
NodeTraverser::ResolveJobs. Milestones M0-M5 with the thread backend
kept behind an OAK_PIPELINE fallback switch.
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.
The Gitea migration left CI on the self-hosted instance with every
cache commented out and the CD workflow broken at parse time
(kiname:). CI runs on GitHub-hosted runners again:
- Linux: ubuntu-latest with the rust toolchain from
dtolnay/rust-toolchain (the self-hosted runner's custom
RUSTUP_HOME/CARGO_HOME lines are gone) and the cargo + FFmpeg caches
re-enabled.
- Windows: windows-latest with msys2/setup-msys2 provisioning the
UCRT64 environment (the Gitea runner had it preinstalled); the
msys2 {0} shell, the GNU-target MSYS2 Rust, the
OCIO_RS_NO_MSVC_INCLUDES gate and the -lmsvcrt link-order workaround
carry over, as do both caches.
- macOS: new job on macos-14 (Apple Silicon) — Homebrew deps, vendored
static OCIO, cached FFmpeg, cargo check + the full suite with the
same retry-once flake policy.
CD fixes: the kiname: typo that kept the workflow from parsing, the
Warp runner labels become the standard GitHub ones, and all eight
commented-out cache blocks are restored. The gpui submodule URL
follows the move to GitHub, the README badge points at the GitHub
workflow, and the .gitea directory is dropped.
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.