refactor: purge CHandle from module internals (M14 R5)

Module-internal object references are Rust types now (values, Arc,
Mutex); CHandle remains only at the oakengine C-ABI boundary:

- oakundo: the global stack holds UndoStack/UndoCommand values
  directly (stack token is the static's address)
- oaktimeline: marker/workarea boxes carry Arc<Mutex<T>>; commands
  share the same allocation through Arc clones (readers in oakengine
  stubs and the app's graphops updated to lock)
- oaktask/oakstorage: sessions, write-through bindings and the
  database backend pass ProjectArc; the Session drops its manual
  release bookkeeping; nodeutil keeps the CHandle<->Arc boundary
  conversion (release_project restored for the app)
- oakcodec: handle.rs deleted outright (no facade entry needed it);
  texture/block placeholders are unit structs
- oakrender: copier's project handle is an identity u64; alive-count
  machinery removed; handle.rs is make_owned/get/get_mut only
- oakplugin: the instance registry is gone (its unregister key never
  matched, leaking weak entries); handle.rs is the RefBox boundary type
- oaknode/oakcommon: only dead guard/borrow helpers removed; external
  payload handles (texture/processor) documented as the boundary

Flake hunts landed along the way: the audio recording test serializes
on the shared manager lock with a normalized state; the autocacher
cancel test uses a slow producer so cancellation is deterministic.
This commit is contained in:
2026-08-17 16:40:15 +08:00
parent ede03d0bfe
commit b36cbd6b6f
53 changed files with 1260 additions and 2369 deletions
+33 -34
View File
@@ -1,13 +1,17 @@
# oakcodec Rust crate
> Status: **implemented**. Implements `include/codec/*.h` verbatim
> (`src/ffi/`); every export has success + failure-path tests
> (`cargo test`: unit tests in `src/ffi/*.rs`, the contract tests in
> `tests/`, and real-media tests in `src/realmedia_tests.rs`). The FFmpeg
> engine is fully implemented through the [`ffmpeg-next`] crate (decode,
> probe, audio conform, encode); the OIIO engine remains a stub in this
> build. This crate mirrors the `crates/oaknode/` template (same FFI
> discipline, same testing layers).
> Status: **implemented**. Implements the `include/codec/*.h` contract;
> every function has success + failure-path tests (`cargo test`: unit
> tests in `src/`, the contract tests in `tests/`, and real-media tests
> in `src/realmedia_tests.rs`). The FFmpeg engine is fully implemented
> through the [`ffmpeg-next`] crate (decode, probe, audio conform,
> encode); the OIIO engine remains a stub in this build.
>
> Single-lib unification (M14, `docs/zh/plans/riir/single-lib.md`): the
> C-ABI export layer (`src/ffi.rs`) and the module-crossing bridge
> (`src/bridge/`) are gone. Other module crates and the oakengine facade
> call this crate's modules directly; the facade (`crates/oakengine`)
> serves the frozen `include/codec/*.h` functions.
## Scope
@@ -18,20 +22,18 @@ conform and proxy generation managers, export format/codec tables,
encoding parameters, and the background-task submit hook.
Public contract: `include/codec/*.h` (7 headers: frame.h, decoder.h,
encoder.h, conform.h, proxy.h, task.h, error.h) — frozen, implemented
verbatim by `src/ffi.rs`. Interim state (pre-M8) is documented in
encoder.h, conform.h, proxy.h, task.h, error.h) — frozen, served by the
oakengine facade. Interim state (pre-M8) is documented in
`src/codec/NOTES.md`: conform/proxy work is delegated to the global
task submit callback and otherwise reports unavailable, never crashes
and never blocks.
## Key architectural decisions (C++ → Rust mapping)
1. **`shared_ptr` → refcounted `RefBox` handle.** The C++ `Frame`/
`Decoder`/`Encoder` objects are heap boxes behind the neutral
by-value handle struct `{ctx, addref, release, abi_version}` (see
`handle.rs`), exactly as oaknode/oakplugin do. Handles are
deliberately duplicated per module: the function pointers always
point into the DLL that created the object.
1. **`shared_ptr` → `Arc`.** The C++ `Frame`/`Decoder`/`Encoder`
objects are handed around as plain Rust values (`Frame`) or
`Arc<dyn Decoder>` / `Arc<dyn Encoder>`. No refcounted C-handle
scaffolding remains (the former `handle.rs` was deleted in M14 R5).
2. **Inheritance → traits.** The C++ `Decoder`/`Encoder` abstract
bases plus their FFmpeg/OIIO subclasses become a Rust trait with
two implementors. The probe/dispatch (decide which implementation
@@ -39,15 +41,14 @@ and never blocks.
`PixelFormat`/`SampleFormat` support is a trait query, not a
virtual chain.
3. **`Frame` owns its params by value.** `olive::Frame` wraps an
`OakVideoParams` handle (an oakcommon by-value handle, NOT owned by
codec) plus a `Vec<u8>` pixel buffer. In Rust the params are held as
the oakcommon handle (refcounted through `bridge::common`) so the
byte-level ABI stays unchanged; the buffer is a plain `Vec<u8>`.
4. **No adapter layer.** Codec calls other modules' C ABIs directly
(`bridge/common.rs`, `bridge/render.rs`), keeping the 2026-08
`OakVideoParams` handle plus a `Vec<u8>` pixel buffer. In Rust the
params are held as an `oakcommon::videoparams::VideoParams` value
(single-lib unification dropped the refcounted oakcommon handle);
the buffer is a plain `Vec<u8>`.
4. **No adapter layer.** Codec calls the other module crates directly
(`oakcommon`, `oakcore-rs`, `oakffmpeg-link`), keeping the 2026-08
decision recorded in NOTES.md §6. Only genuinely repeated
conversions survive as small module-local helpers (e.g.
`fill_render_params`, `cancel_atom_is_cancelled`).
conversions survive as small module-local helpers.
5. **XML stays on the C++ side.** `EncodingParams::load/save` use
oakcommon's C++ `XmlStreamWriter/Reader` classes
(`src/common/src/xmlutils.h`), exactly as oaknode/oakrender do —
@@ -58,8 +59,8 @@ and never blocks.
7. **Enum values are the C contract.** `ExportFormat::Format`,
`ExportCodec::Codec`, `Interlacing`, `VideoScalingMethod`,
`SampleFormat::Format` all stay as the raw int values the C ABI
documents (oakengine/encoding.h), so `ffi.rs` marshals them without
translation.
documents (oakengine/encoding.h), so the facade marshals them
without translation.
## Layout
@@ -67,8 +68,7 @@ and never blocks.
src/
lib.rs crate doc + module map
error.rs error codes (mirrors include/codec/error.h)
handle.rs refcounted-handle scaffolding (same pattern as node)
frame.rs Frame (CPU pixel buffer + OakVideoParams handle)
frame.rs Frame (CPU pixel buffer + VideoParams value)
framemanager.rs FrameManager (buffer pool + background GC thread)
decoder.rs Decoder trait + CodecStream + RenderMode + probe
ffmpeg.rs FFmpegDecoder / FFmpegEncoder (ffmpeg-next)
@@ -85,17 +85,16 @@ src/
footagedescription.rs FootageDescription (codec-internal stream desc)
planarfiledevice.rs PlanarFileDevice (stdio plane-channel I/O)
realmedia_tests.rs real-media tests (demo.mp4, H.264 round-trip)
bridge/ C ABI imports: common.rs, render.rs
ffi.rs include/codec/*.h export layer
tests/ contract + golden tests (see test section below)
```
## Hard rules for the implementer
1. Every `extern "C"` body goes through `handle::guard*`; no panic
crosses FFI.
2. The handle is the only way out of the crate; the public API never
hands out raw `&Frame`/`&Decoder` references.
1. No panics cross a module boundary: the facade wraps every call in
its panic-catching shims, and callback types stay `unsafe extern
"C"` with panic-free bodies.
2. Objects leave the crate only as Rust types (`Arc`, values, `&`
refs); raw handles exist solely inside the facade.
3. Behavior parity with C++ is proven by the unchanged C ABI test
suite (`src/codec/tests`) plus the contract tests in `tests/`.
4. Where C++ behavior is genuinely load-bearing but ugly, port the