The GPU path composites frames bottom (last) to top (first); the CPU
fallback iterated top-first, so on machines without a working adapter
every multi-layer frame had its layering order inverted (the
composite_tracks test caught it as 0.8125 vs the documented 0.625).
Factor the CPU half into composite_tracks_cpu, iterate it in reverse
and pin the math in the test by calling the CPU path directly (the old
assertion silently exercised the GPU path whenever another test had
installed a shared context).
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.
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.
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.
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.
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.
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.
Generator effects (bars, checkerboard) can be dragged from the library
onto the timeline, where they land as a standalone five-second clip
built from the node factory; the inspector shows the generator's
parameters as the clip's own chain.
Transitions are no longer junction-only. The render planner accepts a
transition with at least one wired neighbor and blends the missing
side against transparent black, so head transitions fade in from black
and tail transitions fade out to black. add_transition_at_edge creates
those single-sided blocks (wired to just the IN or OUT block), the
default-transition command covers both ends of a lone clip, and an
effect drag dropped near a clip edge routes to the nearest seam or
edge within a one-second window.
Translating, rotating or warping content past the frame edge used to
smear the clamped edge row/column across the vacated region. The
transform, position, swirl, ripple and wave shaders now multiply the
sample by an in-bounds mask so off-frame pixels come out transparent
(and composite as black when nothing sits below). Tile deliberately
keeps its wrapping lookup.
Adjustment layers (docs/zh/plans/adjustment-layers-and-transitions.md):
a new timeline block type whose effect chain grades the composite of
every video track below it, over its own range (spanning clips or a
slice of one). The graph path flushes the lower tracks at the block's
track boundary and sweeps the composite through the chain via a
transient texture-source node; the montage path mirrors it with
AdjustmentSpan tickets (wire-compatible), so worker previews and
exports agree. An empty-area context menu creates one; the block
trims/moves/deletes like a clip, with undo everywhere.
Transitions: seam blocks come alive - cross dissolve/fade/wipe/slide
evaluate both neighbors through the graph path with progress from the
transition's own range (never the whole clip). Ctrl+Shift+D or the clip
menu inserts a default transition; the gpui wedges render and drag to
resize offsets undoably, and TransitionRemoveCommand now restores
offsets and edges on undo. The transitionfx node form runs the same
shaders on an adjustment layer with progress_in auto-filled from the
layer's span (explicit value wins).
Also: every built-in effect name and parameter name is now
translatable (360 node.* keys per locale, zh-CN fully translated, two
coverage tests guard future gaps); the new nodes register in
nodes/mod.rs with the factory smoke table updated; textfootage and
adjustment-layer i18n keys included.
The transform shader sampled in a top-left-origin pixel space while
Olive's transform semantics (and every other node) are center-origin:
rotation swung the image around the top-left corner, pushing it partly
off-frame - reading exactly like an unwanted zoom. Match the C++
transform.vert projection: position (0,0) is the frame center and
rotation/scale pivot around the anchor, so rotation and scale stay
independent user controls. GPU tests pin the 90-degree landing spot
(no smearing) and the 2x scale centroid (stays centered).
Preview renders at proxy size while a paused frame renders full-res, so
anchoring resolution_in to the render target made every sequence-pixel
effect (shape size/pos, transform offsets, corner pin points, drop
shadow distance) change apparent size whenever the transport stopped.
Pre-fill resolution_in from the sequence's video params (C++ inserts
the NodeGlobals square resolution at job-build time), covering the
nested generator job inside a merge as well; a node that inserted its
own resolution_in keeps it.
naga's WGSL emitter rejects fall-through-capable GLSL switch blocks, so
every shape and despill job failed to compile and silently fell back to
the effect input - both effects were no-ops. Rewrite the type/method
dispatch as if/else chains (same semantics as the C++ shaders) and
cover all three shape types plus green-screen despill with GPU pixel
tests. Also drop the now-stale nested-payload/merge-binding TODO notes.
- process_shader_job: bind all texture params by name, recurse into nested
shader payloads (depth cap 8), fall back to frame size without inputs
- run_effect: take iterative_input so dropshadow previous_iteration_in works
- merge: actually composite inputs; keyer mask, opacity modulation, math
texture ops and mrg generator layers now bind their textures
- transform distort: real fragment-side inverse-matrix sampling
- time offset / time remap: wire NodeBehavior time adjustment hooks
- plugin: fix first-node identity colliding with unbound sentinel
oak-common is gone; its modules (configstore, xmlutils, ocioutils,
oiioutils, colormath, colortransform, videoparams, ffmpegutils, ...)
now live in oak-core alongside the value types. The render value/GPU
types moved too: backend (wgpu context + DisplayRenderer), color
(ColorProcessor over ocio-rs), texture, frame, and the commonutil
config helpers.
Fix-ups to make the merged tree build and pass tests:
- oak-core Cargo.toml: wgpu back to 25 (the moved backend code is
written against that API generation); add the toml/quick-xml/image
deps oak-common carried.
- lib.rs: drop the duplicate 'pub mod error;'.
- error.rs: unified OAKCORE_* codes; restore Error::new() and
From<OcioError> from oak-common's error type.
- backend.rs/color.rs: oak_core::/oak_render:: self-references
rewritten to crate::; the shaderfx-dependent GPU effect test moved
to oak-render's shaderfx tests (shaderfx depends on oak-node and
cannot live in oak-core).
- oak-render's error module re-exports oak_core::error::{Error,
Result}; the OAKRENDER_* codes stay as the public-code contract.
- oak-node jobs.rs: ColorProcessor imported from oak_core::color.
- Integration tests repointed at oak_core::{texture, frame, backend,
color, colormath}.
- the display-ICC regression test treats an empty OAK_DISPLAY_ICC as
unset, matching displayicc::env_override_icc.
The OCIO grading nodes previously pushed null texture handles; they now
push real ShaderJobPayloads whose GLSL is the OCIO-generated dynamic
grading-primary GPU shader — the exact code the C++ path applies, so no
approximation:
- color: grading_primary_function_shader(style) builds a dynamic
GradingPrimaryTransform (LIN/LOG) on the default config, extracts the
GLSL via GpuShaderDesc (function 'ove_grading_primary', resource
prefix ocio_, no LUT textures) and caches it per style + config id.
- eval: OCIO_GRADING_STUBS maps the two node type ids to the grading
style; process_shader_job resolves the stub and splices it into the
node's %1 marker (same wiring as the chromakey OCIO stub); the
pipeline cache key folds the stub text so a config change recompiles.
- nodes: value() pushes a ShaderJobPayload with the C++ value()
rewrite applied to the row — vec4 (RGBM x=master) grading inputs to
the vec3 GPU uniform form (lin: contrast RGB=c*m, offset RGB=c+m,
exposure RGB=2^(c+m); log: lift RGB=c+m, gain c*m, gamma c*m), plus
pivot/saturation floats, the log pivotBlack/pivotWhite normalization
range (0/1), clamp sentinels (NoClampBlack -1 / NoClampWhite 2),
white>black enforcement per frame, and localBypass=false. Generated
uniform names bind by name (the log node's OCIO_NAMESPACE_ id text
normalizes to the ocio_ resource prefix).
- Tests: grading stub generation (analytic GLSL, cache) in color,
end-to-end GPU exposure doubling for lin (+1 stop on 0.2 gray -> 0.4)
and lift for log, node payload rewrite assertions, and the
all-shaders sweep now retries grading stubs. oak-render 181,
oak-node 441, oak-app 272 lib tests pass.
- 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.
- 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.
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.
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).
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.
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.
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.
All crates take the oak-* kebab-case naming (oak-audio, oak-codec,
oak-common, oak-core, oak-ffmpeg-link, oak-node, oak-otio, oak-plugin,
oak-render, oak-storage, oak-task, oak-timeline, oak-undo), with the
lib identifiers rewritten (oakrender:: -> oak_render::, oakcore_rs:: ->
oak_core::, ...) across all 226 referencing files.
The GUI application moves from the workspace root into
crates/oak-app/: src/, build.rs (paths fixed for the new location) and
tests/ travel with it, the root Cargo.toml becomes workspace-only
([workspace] + workspace.package + profiles), and the app package
inherits the workspace version. The screenshots example becomes a
standalone crate examples/simple_player/ with its own Cargo.toml.
Every crate now inherits the single workspace version
(version.workspace = true), and the workflows' crate paths and the
build docs follow the renames.
Validated with a clean cargo check --workspace.