Skip to content

Latest commit

 

History

History
1079 lines (922 loc) · 101 KB

File metadata and controls

1079 lines (922 loc) · 101 KB

Changelog

All notable changes to the yscv workspace will be documented in this file.

The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.

[Unreleased]

Changed

  • Breaking: load_onnx_model and load_onnx_model_from_file now run the graph optimizer as part of loading. It was a separate optimize_onnx_graph call every caller had to remember, and forgetting it did not produce an unoptimized model so much as a slow one — or, on the Conv paths, a runtime shape error from a kernel handed a weight the plan builder never permuted. Every in-tree caller was already loading and immediately optimizing, except yscv-pipeline's CPU dispatcher, which was silently running unoptimized. YSCV_ONNX_OPTIMIZE_OFF=1 disables it process-wide, and the new load_onnx_model_unoptimized returns the graph as the file spells it, for inspection tools and for tests asserting against a fixture. The runtime index is still built exactly once per load either way.

  • OnnxModel::rebuild_runtime_index no longer asserts one plan action per node. The FusedPwDwPwReduce merge deliberately drops the actions it absorbs, so a merged plan is legitimately shorter than the node list — nchwc_handoff is documented as indexed by plan position for that reason, and every action carries its own node_idx, so nothing reads a node through its plan position. The assertion only escaped notice because the load path did not go through this function; routing loads through it made a merged plan trip it. The correspondence that does have to hold is checked where it is established, in plan/build.rs, before the merge runs.

  • Breaking: operator attributes are keyed by the new Attr enum instead of String. OnnxNode.attributes is now FxHashMap<Attr, OnnxAttribute> and the get_attr_* helpers take an Attr. ONNX attribute names are a closed vocabulary, but they were spelled as string literals at ~200 sites, where a typo silently produced a missing attribute and a default value rather than a compile error. Names outside the table live on in Attr::Other, so decoding and re-exporting a model the runtime does not fully interpret stays lossless.

  • Breaking: optimize_onnx_graph now returns Result<(), OnnxError>. Constant folding evaluates operators, so the pipeline can genuinely fail, and the previous signature left no way to say so — failures were printed to stderr and the model silently came back unoptimized. A node the evaluator declines is still not an error: it is simply not folded.

  • The ONNX graph optimizer now runs over a def-use IR (yscv-onnx/src/ir/) driven to a fixed point by a pass manager, instead of a hard-coded sequence of functions mutating the model's string-keyed node list. Passes match through a use list rather than positional adjacency, so producer/consumer pairs separated by unrelated nodes now fuse. YSCV_ONNX_PASSES=-name disables an individual pass; YSCV_ONNX_PASS_LOG=1 reports each sweep.

  • Constant folding no longer builds a throwaway model and rebuilds the whole execution plan per folded node; it dispatches the operator onto a bare environment. It also folds a chain in one topological sweep rather than rescanning from the start each time, refuses results that balloon far beyond their inputs, and refuses the non-deterministic Random*/Multinomial family, which would otherwise freeze a single draw into the weights.

  • fuse_conv_relu, fuse_bn_relu, fold_conv_bn, fold_constants and rewrite_convtranspose_dts are no longer public. They were only ever reachable through optimize_onnx_graph outside the crate.

  • Optimizer passes no longer rebuild the runtime index individually. Nine of the twelve passes called rebuild_runtime_index() on exit, so a single optimize_onnx_graph re-ran execution-plan construction and weight prepacking about ten times. The driver now rebuilds once after the whole pipeline; the public per-pass entry points still rebuild for standalone callers.

Fixed

  • use_avx512_mr12 documented itself as "DEFAULT ON" with a YSCV_AVX512_SGEMM=0 kill switch. The gate it calls is opt-in on the exact value 1, so the AVX-512 MR=12×NR=32 GEMM is off unless asked for — the comment claimed the opposite of the code directly below it.
  • Conv weight folding corrupted weights shared between two convolutions — the shape a Siamese tracker's two branches produce. Each convolution folded its own BatchNormalization into the shared tensor, applying the second scale on top of the first. Sharing is now detected and the fold declines.
  • Conv + Mul/Add folding treated any constant with one element per output channel as per-channel, including a rank-1 [OC] tensor. ONNX broadcasting aligns trailing axes, so against an NCHW convolution output that scales width, not channels. Only a scalar, or a constant whose channel-aligned axis holds the output-channel count with every other axis 1, now qualifies.
  • Synthesized bias operands were named from the node name, which ONNX makes optional, so every unnamed convolution proposed the same name and the second silently aliased the first's tensor.
  • remove_dropout_nodes deleted nodes by NodeProto.name, which ONNX makes optional. A single unnamed Dropout put the empty string into the delete set and took every other unnamed node in the graph with it — on a fully unnamed three-node fixture the pass emptied the graph. It also deleted malformed Dropout nodes it had skipped rewiring. Now removes only the nodes it actually rewired, by index.
  • Transpose + MatMul fusion fired even when the transposed value had a consumer that could not absorb it. The fused action reads the value before the transpose, so when another consumer forced the Transpose to run anyway, running it consumed that value and the fusion then read a tensor that was gone — a Transpose feeding both a MatMul and any other op failed the inference outright. The matcher now requires every reader of the transposed value to be an absorbing MatMul, which is what its own comment always claimed.
  • The PW_expand → DW → PW_reduce merge matched its residual Add positionally while every other matcher worked by dataflow. When the two disagreed the merge absorbed the pointwise reduction without its Add, and the addition was dropped from the execution plan entirely — inference returned successfully with a graph output missing. It now takes over whatever the Conv+Add matcher resolved, or declines.
  • eliminate_squeeze_unsqueeze_pairs could accept overlapping inverse pairs (i, i+1) and (i+1, i+2) from a chain such as Squeeze -> Unsqueeze -> Squeeze. The reverse-removal loop then deleted already-shifted indices, taking unrelated nodes with them. Overlapping matches are now skipped.

Changed

  • The prfm pldl1keep helper lived in three copies across conv/pointwise.rs, and is now a single ops::prefetch::prefetch_l1_keep documenting when a hint is worth adding: hoist the gate out of the K-loop, and only hint operands whose stride actually defeats the hardware prefetcher.
  • Dropped the software prefetch from the AVX binary_same_shape loops. The access is unit-stride over three streams, which every hardware prefetcher tracks; measured on Zen 4 the hint is within run-to-run drift (the A/B flips sign depending on run order), so it only added uops to a bandwidth-bound loop.

Added

  • rebuild_runtime_index now debug-asserts one execution-plan action per node. With the sequential fallback gone the runner walks the plan and nothing else, so a short plan would silently skip trailing nodes.
  • Benchmarks for the two kernels that had none: trans_a_m64_k256_n256 (the tracker's FusedTransposeMatMul shape, the only caller of the transposed-A tiles) and raw-slice binary_same_shape_dispatch add/mul.

Changed

  • Optimizer passes no longer rebuild the runtime index individually. Nine of the twelve passes called rebuild_runtime_index() on exit, so a single optimize_onnx_graph re-ran execution-plan construction and weight prepacking about ten times. The driver now rebuilds once after the whole pipeline; the public per-pass entry points still rebuild for standalone callers.

Removed

  • run_onnx_model_sequential, the ~600-line per-inference fusion scanner in runner/execute.rs. It re-derived Conv+BN+Relu / Conv+SiLU / Conv+Add patterns on every inference and was reachable only when the load-time execution plan was empty, which no graph with nodes produces. Verified unreachable across the workspace suite before removal. Its now-orphaned helpers went with it: find_relu_after_identity_chain, mark_skip_indices, exec_reshape_zerocopy, and the use_counts-taking try_reshape_nhwc_passthrough (the plan path's _inner variant, and the NHWC-passthrough optimization itself, are unaffected).

[0.1.11] — 2026-07-26

Added

  • CPU profiler (profile_onnx_model_cpu) now reports which yscv-kernels compute path each Conv dispatched to. The detail table gains a via <kernel> column (indirect-nhwc-3x3, nhwc-padded, nhwc-gemm, nhwc-gemm-prepacked, dw-nhwc-padded, dw-nhwc, dw-nchwc-3x3, grouped, bnns-nchw), so it is visible whether a slow Conv took the depthwise, blocked-GEMM, indirect, or grouped path rather than guessing from the shape.
  • The via label also reports the kernel-internal sub-path the yscv-kernels entry point chose, rendered as runner/sub — e.g. nhwc-padded/first-layer-rgb, nhwc-gemm/pw-gemm, nhwc-padded/winograd-3x3, nhwc-padded/im2col-gemm, dw-nhwc-padded/dw-avx512, dw-nhwc-padded/dw-scalar. Dense sub-paths: first-layer-rgb, winograd-3x3, pw-16x16-direct, pw-nx16-direct, pw-gemm, direct-3x3, im2col-gemm, row-fma. Depthwise sub-paths: dw-c16-avx512, dw-avx512, dw-avx-fma, dw-avx, dw-sse, dw-neon, dw-scalar — so a depthwise that silently fell to the scalar row kernel (depth_multiplier>1, <4 channels, no SIMD) is visible. Exposed from yscv-kernels via take_conv_path; the per-row depthwise dispatch is routed through a shared dw_row_kind selector so the recorded label can never drift from the kernel actually run. Grouped/indirect record no sub-path.
  • MatMul nodes now report the dispatched GEMM family in the via column — blas-sgemm, blocked-mr12, blocked-mr6, blocked-mr8, blocked-mr4, low-k-tile, row-gemm — so it is visible whether a hot MatMul packed into a blocked microkernel or fell to the per-row path. Recorded by matmul_2d_slices_fused_maybe_packed (the MatMul op + pointwise Conv entry) and the Gemm-op dispatcher tree (matmul_2d_*_with_plan), exposed from yscv-kernels via take_matmul_kernel. Both MatMul and Gemm profiler nodes carry the via label.
  • The fused-path runner profiler (YSCV_RUNNER_PROFILE) now also records the dispatched kernel label per node, so the production fused-path JSON (not just the unfused CPU profiler) shows which Conv sub-path / GEMM family ran. Fused streaming kernels that bypass the instrumented dispatch carry none.
  • YSCV_PROFILE_FILTER env narrows the profiler's detail table and JSON dump to a chosen op/name set — a comma-separated spec of op types and/or name:<substr> tokens (e.g. Conv, Conv,MatMul, name:head). Lets you bench a specific node subset instead of only the default Conv view.
  • YSCV_PROFILE_JSON per-node records now carry Conv kernel_shape, strides and the dispatched kernel, so the dispatch and shape detail are diffable, not just printed.
  • scripts/gap_diff.py gains --filter (same spec as YSCV_PROFILE_FILTER) and --per-node, which diffs two runs node-by-node and flags any dispatched-kernel change as old→new — ideal for a baseline-vs-change comparison of the same model.

Changed

  • yscv-imgproc: convex_hull runs an Akl–Toussaint discard before the monotone-chain sort. The four axis extremes bound a quadrilateral, and every point strictly inside it is provably not a hull vertex, so the filter is exact and drops the input to the sort in a single O(n) pass. Points on an edge are kept, so float ties cannot discard a real vertex.
  • yscv-imgproc: binary morphology packs rows into u64 words instead of holding one float per pixel — a 720p mask goes from 3.7 MB to 115 KB, which matters most on the small ARM boards this targets. Dilation becomes word-wise OR, and a window of length L is built by recursive doubling in O(log L) passes. New open_binary_box / close_binary_box keep the mask packed between the two stages instead of converting to floats and back twice.
  • yscv-imgproc: both connected-component entry points work on horizontal runs rather than pixels — a row is scanned into maximal foreground spans, each span joins the spans it touches above, and a disjoint set resolves the chains. Per-component area, bounding box and centroid come out of the same spans in closed form, so individual pixels are never walked. Labels still follow the raster order of each component's first pixel. Three solid objects on 1280×720: 3.83 ms → 0.97 ms.

[0.1.10] — 2026-06-21

Added

  • --keep-fp32 hybrid knob for QDQ quantization: keep selected layers in fp32 while the rest of the graph is quantized.

Fixed

  • MPSGraph backend now folds the load-time Conv_Relu / BatchNormalization_Relu fusions (build the conv/bn graph, then apply the activation), matching the CPU and wgpu backends. Previously MPSGraph aborted with unsupported op 'Conv_Relu' on any Conv+ReLU graph, so the Siamese tracker could not run on Metal.

Changed

  • Bumped workspace version to 0.1.10 and aligned user-facing documentation and scripts/check-doc-counts.sh with the release.
  • Internal refactor: split the largest perf-arc source files (matmul, conv, fused_pw_dw_3x3, backend, tensor ops, the ONNX runner, and loader) into focused directory submodules. No public API or behavior change — tracker output stays bitwise-identical and 1T/6T latency is unchanged within noise.
  • Documentation and code comments cleaned of session-specific optimization-arc notes and refreshed to the current module layout.

Benchmark update — Apple M1 (tracker + single-op + Metal vs CoreML)

Measured on an Apple M1 (4 P + 4 E cores) with the portable harness; tracker p50 over 300 iterations, single-op p50 at 1 thread.

Backend yscv ORT 1.19.2 yscv vs ORT
CPU 1T 65 FPS (15.37 ms) 34 FPS (29.80 ms) 1.94×
CPU 4T 183 FPS (5.46 ms) 42 FPS (23.64 ms) 4.33×
GPU sync 792 FPS (1.26 ms) 617 FPS (1.62 ms, CoreML) 1.28×
GPU pipelined×2 2688 FPS (0.37 ms) 4.4× vs CoreML

Single-op (1 thread): yscv beats ORT-CPU on every op outside relu parity, the NEON activations running 1.1–8.5× ahead; slower than PyTorch on sum and the broadcast add/sub. Full tables in docs/performance-benchmarks.md.

[0.1.9] — 2026-05-02

Changed

  • Bumped workspace version to 0.1.9 and aligned user-facing documentation and scripts/check-doc-counts.sh with the release.

Benchmark update — Siamese tracker vs ORT (Apple Silicon)

Fresh measurements against onnxruntime 1.19.2 on the same M-series host, 219-node Siamese tracker model (1×3×128×128 + 1×3×256×256 inputs):

Backend yscv 0.1.9 ORT 1.19.2 yscv vs ORT
CPU 43.1 FPS (23.2 ms) 18.9 FPS (53.0 ms) 2.3×
GPU sync 728 FPS (1.37 ms) 532 FPS (1.88 ms, CoreML) 1.4×
GPU pipelined×3 1510 FPS (0.66 ms) 2.9× vs CoreML

Why yscv wins here on both fronts:

  • CPU: Accelerate.framework on Apple Silicon dispatches matmul through the AMX block (matrix accelerator inside the CPU). ORT's generic-SIMD CPU kernels don't hit AMX.
  • GPU: ORT's CoreML provider hybrid-executes across ANE + Metal + CPU with a 3-op CPU fallback for this graph, paying synchronisation overhead at each boundary. yscv MPSGraph compiles 100% of the graph to pure Metal; pipelining then overlaps CPU marshal with GPU compute.

Reproduction details in docs/performance-benchmarks.md § Siamese Tracker — Full Backend Comparison.

[0.1.8] — 2026-04-15

The pipeline-framework release. Ships the TOML-driven multi-accelerator runtime (yscv-pipeline) plus the Rockchip MPP hardware-encoder wrapper (yscv-video-mpp) as two new library crates, brings in a full rewrite of the RKNN backend (pipelined pool with auto-recovery, hot-reload, custom ops), adds the MPSGraph triple-buffered submit/wait API, wires real-time scheduling (SCHED_FIFO / CPU affinity / mlockall / cpufreq governor) into the pipeline config, and gives the framework top-level documentation (QUICKSTART, per-crate READMEs, troubleshooting, getting-started).

Highlights:

  • yscv-pipeline crate — TOML-driven dispatch across CPU / RKNN / RKNN-matmul / MPSGraph / wgpu; AcceleratorDispatcher trait; per-task recover_all(), spawn_watchdog(), ONNX→RKNN compile at startup, multi-input graphs, shape-validation on submit.
  • yscv-video-mpp cratedlopen-based Rockchip MPP hardware encoder (H.264 / H.265), no link-time vendor dependency.
  • RknnPipelinedPool — multi-slot NPU pool with submit / wait, consecutive-failure auto-recovery, reload(&new_model_bytes) hot swap. RknnMatmul gets its own alloc_mem + dispatcher variant Accelerator::RknnMatmul { m, k, n, dtype }.
  • MPSGraph pipelinedsubmit_mpsgraph_plan / wait_mpsgraph_plan triple-buffered by default; multi-input models supported; 4.3× ORT CoreML throughput on two-tower trackers.
  • Real-time wiring[realtime] in TOML → apply_rt_config_with_governor applies SCHED_FIFO + CPU affinity + mlockall + cpufreq governor with graceful fallback when CAP_SYS_NICE / CAP_SYS_ADMIN are missing.
  • Docs overhaulQUICKSTART.md (3 personas, 5 min each), docs/getting-started.md (7-step progressive tutorial), docs/troubleshooting.md (common errors + fixes per platform), per-crate READMEs for discoverability on crates.io, examples/README.md catalogue.
  • CI hardeningscripts/check-safety-comments.sh paths updated after the metal/ reorg, scripts/check-doc-counts.sh expectations bumped to 16 crates, apps/bench gets default-run = "yscv-bench" so the benchmark step no longer errors on binary ambiguity.
  • Numerical tolerancesmatmul_associative_with_scalar proptest + conv3d tests loosened (1e-4 → 1e-3, 1e-6 → 1e-3) to cover the cross-BLAS variance between Accelerate (macOS) and OpenBLAS (Linux / Windows) on 3-element dot products.

[Unreleased]

Pipeline framework — RKNN matmul dispatcher

Closes the last bullet from the tech-lead review's "out of scope" list. The AcceleratorDispatcher trait now covers every accelerator family the framework exposes — including the dedicated NPU matmul unit used for LLM dequant/attention workloads.

  • Accelerator::RknnMatmul is no longer a unit variant. It carries { m, k, n, dtype } directly: matmul contexts are shape-bound at construction (the SDK pre-allocates M×K, K×N, M×N buffers), so dimensions belong in the TOML, not in a model file.
  • MatmulDtype TOML enum maps to RknnMatmulType. First-cut: Fp16MmFp16ToFp32, Fp16MmFp16ToFp16, Int8MmInt8ToInt32, Fp16MmInt4ToFp16 (the LLM-dequant tile). Per-channel quant params for INT4/INT8 still go through yscv_kernels::RknnMatmul::set_quant_params directly — TOML wire-up is a follow-up if/when there's demand.
  • RknnMatmulDispatcher: pre-allocates A/B/C RknnMem against the matmul context (new RknnMatmul::alloc_mem / alloc_mem_ex mirroring RknnBackend), pre-binds them once, hot-path is `memcpy + sync_to_device
    • run + sync_from_device + readback. Inputs come in named "a"and"b"; output is "c"`.
  • validate_models skips matmul tasks — no model file to check.
  • TOML example:
    [[tasks]]
    name = "qk_attention"
    model_path = ""    # ignored for matmul
    accelerator = { kind = "rknn-matmul", m = 1, k = 4096, n = 4096, dtype = "fp16-mm-fp16-to-fp16" }
    inputs  = [
        { name = "a", source = "..." },
        { name = "b", source = "..." },
    ]
    outputs = []

Pipeline framework — hot-reload + DVFS

Closes the last two production-feasible items from the tech-lead review. The remaining "out of scope" entry (rknn-matmul dispatcher) genuinely doesn't fit the AcceleratorDispatcher byte-in/byte-out contract — callers use yscv_kernels::RknnMatmul directly.

  • RknnPipelinedPool::reload(new_model_data): hot-swap the underlying .rknn model in-flight. Walks every slot, takes the ctx + mem write locks, calls reset_with_flags(new_bytes, RKNN_FLAG_ASYNC_MASK), re-allocates + re-binds input/output RknnMem. In-flight handles invalidate (their wait errors). model_data is now RwLock<Arc<Vec<u8>>> so concurrent recover_failed mid-reload uses the fresh bytes.
  • yscv_video::realtime::set_cpu_governor(governor): writes governor (typically "performance") to every /sys/devices/system/cpu/cpu*/cpufreq/scaling_governor it can. Per-core write failures are logged + counted but not fatal — matches the rest of the realtime stack's graceful-fallback contract. Returns the count of successfully-written cores.
  • apply_rt_config_with_governor: superset of apply_rt_config that also writes the cpufreq governor. RtAppliedState gains cpu_governor_cores field.
  • TOML [realtime] cpu_governor = "performance": new optional config field; run_pipeline (with --features realtime) wires it through. Kills first-burst DVFS step-up latency on edge SoCs (~5 ms saved on RK3588's first inference per burst).

Pipeline framework — full accelerator coverage + supervisor glue

Follow-up to the "end-to-end integration" pass below: closes the last four "out of scope" items from that round.

  • MetalMps dispatcher: config-driven path for Apple Silicon now wraps MpsGraphPlan behind a Mutex (guaranteeing Sync) + an explicit unsafe impl Send for MetalDispatcher. Audit recorded in the code: all MPSGraph-held ObjC pointers have atomic ref-counting, and every call is autoreleasepool-scoped — safe to move the dispatcher across threads under its Mutex. First dispatch lazily compiles the plan; recover() drops it so the next dispatch recompiles (useful after a GPU reset).
  • Gpu dispatcher: new GpuDispatcher wraps yscv_onnx::run_onnx_model_gpu (wgpu cross-platform). Same byte contract as CPU / RKNN / Metal dispatchers.
  • MpsGraph / MpsGraphExecutable / MpsGraphTensorRef marked unsafe impl Send in yscv-kernels, with documented safety contract (atomic ObjC refcounting + autoreleasepool-scoped calls). MpsGraphTensorRef additionally claims Sync — it's an SSA node handle, immutable after graph compile.
  • PipelineHandle::spawn_watchdog(stats, interval) -> Watchdog (feature realtime): spawns a background thread that polls PipelineStats5::watchdog_alarm, invokes recover_all() on observation, and clears per-stage alarms so the next overrun is visible. Returned Watchdog joins the thread on drop. Couples the previously-independent StageWatchdog tracking and pipeline recovery into a single supervisor.
  • ONNX → RKNN auto-compile at startup: RKNN tasks can now point model_path at an .onnx file. RknnDispatcher::new detects the extension, compiles via yscv_kernels::compile_onnx_to_rknn (fp16 by default), and caches the result next to the source as <model>.rknn. Subsequent runs skip the compile. validate_models also recognises .onnx-for-RKNN and runs the appropriate magic-byte check. Advanced compile options (int8 calibration dataset, target-platform hints) remain host-side via the Python toolkit2 for now.

Pipeline framework — end-to-end integration

Closes every tech-lead review item for the ARM/Rockchip inference stack; the TOML config schema now has a real runtime behind it.

yscv-kernels

  • RknnPipelinedPool recovery: pool now tracks fail_streak per slot and auto-recovers on TIMEOUT / CTX_INVALID / DEVICE_UNAVAILABLE / DEVICE_UNMATCH after RECOVERY_THRESHOLD consecutive faults. Recovery rebuilds the NPU context + re-binds all input/output RknnMem under write-locks on the slot — a pending AsyncFrame on that slot is discarded. Caller-bug errors (INPUT_INVALID, PARAM_INVALID, etc.) bubble up unchanged. Manual recovery API exposed as recover_failed(slot_idx).
  • Exact-size input validation in RknnPipelinedPool::submit: rejects short or oversized (name, bytes) with a clear "model expects exactly N bytes" message. Prevents silent garbage from previous-frame tail bytes leaking into the NPU.
  • ContextPool::dispatch_roundrobin / dispatch_on multi-input: signature changed from &[u8] to &[(&str, &[u8])] — matches RknnPipelinedPool::submit and supports Siamese / two-tower models that the old single-input path couldn't drive.
  • RknnBackend::reset_with_flags: new — preserves RKNN_FLAG_ASYNC_MASK (or any init flag) across context recovery. The existing reset() wraps this with flags = 0 and retains its previous semantics.
  • is_recoverable_rknn_error helper exposed pub(crate) for the pooled + single-context recovery paths.

yscv-video

  • LatencyHistogram (new module): lock-free 2048-sample ring with record(us) → O(1) on the hot path and snapshot() → { min, p50, p90, p95, p99, max, count } for periodic reports. Powers every new latency metric below.
  • PipelineStats5 now carries one LatencyHistogram per stage (capture_latency, infer_latency, encode_latency, display_latency, e2e_latency). PipelineStats5Snapshot exposes those as LatencyQuantiles — the scalar avg_e2e_latency_us is replaced by the richer e2e distribution.
  • PipelineStats gains capture_latency / process_latency / output_latency histograms and a latency_snapshot() helper. PipelineStats no longer derives Copy; callers who relied on that must clone via the per-field snapshot() helpers.
  • FramePayload (new frame_common module): Owned(Vec<u8>) | DmaBuf { fd, len } — a canonical type for callers who want to pass a V4L2-exported DMA-BUF through the pipeline without reinventing the wrapper. Ownership rule: the camera layer owns the fd, pipeline borrows.

yscv-pipeline

  • validate_models is now a real check: magic-byte verification for .rknn (RKNN / RKNF magic) and .onnx (first-byte 0x08 protobuf tag). Corrupted-but-stat-able files now fail at config time, not after real-time threads have started. New ConfigError::ModelInvalid variant carries the reason. Optional rknn-validate feature adds a full RknnBackend::load check on hosts where librknnrt.so is loadable.
  • AcceleratorDispatcher trait moved out of scheduler into a new dispatch module with concrete implementations: CpuDispatcher (ONNX via yscv-onnx CPU runner) and RknnDispatcher (via RknnPipelinedPool single-slot) under --features rknn. MetalMps and Gpu are deferred with clear error messages — the low-level compile_mpsgraph_plan path remains available for sub-ms latency callers. Factory dispatcher_for(&task) returns a boxed trait object; unavailable features produce a build-time-stable error, not a link failure.
  • PipelineHandle::dispatch_frame(&[(&str, &[u8])]): new hot-path entry point. Walks the topologically-sorted task order, routes camera inputs + chained task outputs through the matching dispatcher per TensorBinding.source ("camera" or "<task>.<output>"), returns HashMap<String, Vec<u8>> of every tensor produced.
  • PipelineHandle::recover_all + dispatcher_label: supervisor hooks for transient-fault recovery and logging / OSD labels.
  • run_pipeline(cfg) now actually constructs dispatchers (previously returned a skeleton handle with only the execution order). Optional realtime feature wires yscv_video::realtime::apply_rt_config so SCHED_FIFO + CPU affinity + mlockall are applied when [realtime] sched_fifo = true in the TOML. Graceful fallback on hosts without CAP_SYS_NICE.
  • NpuCoreSpec::to_mask(): converts the TOML-facing spec into the kernel-level NpuCoreMask (gated --features rknn).

New example: examples/src/edge_pipeline_v2.rs

End-to-end reference that wires all of the above:

  1. Parses a TOML with PipelineConfig::from_toml_path.
  2. Builds a runtime handle via run_pipeline(cfg) — validates every model, constructs dispatchers, applies real-time config if the feature is on.
  3. Synthesises an RGB gradient-box frame (drop-in replacement for a real V4l2Camera::export_dmabuf + FramePayload::dma_buf).
  4. Hot loop calls handle.dispatch_frame(&[("images", bytes)]), collects per-frame latency into a LatencyHistogram, reports p50/p95/p99 every second.
  5. Recovery on repeated failures via handle.recover_all(); exits cleanly after MAX_CONSECUTIVE_FAILURES to avoid log spam on misconfigured models.

Workspace MSRV bumped to 1.94

The f16 NEON intrinsics (vreinterpret_f16_u16 etc.) used by the MPSGraph pipelined path's CPU-side f16→f32 widening require Rust 1.94. Workspace rust-version lifted accordingly; all referenced docs (context.md, docs/performance-benchmarks.md) updated to match.

Pipelined RKNN pool (Rockchip NPU)

yscv-kernels (rknn)

  • RknnPipelinedPool + RknnInferenceHandle — multi-core pipelined RKNN dispatch mirroring the MPSGraph pipelined path on Apple Silicon. One slot per NpuCoreMask; each slot loads its own RknnBackend with RKNN_FLAG_ASYNC_MASK, pins to the core, and pre-allocates + pre-binds an RknnMem for every graph input and output via bind_input_by_name / bind_output_by_name. Slot count follows cores.len() (typical RK3588 pool: [Core0, Core1, Core2]).
  • submit(&[(&str, &[u8])]) -> RknnInferenceHandle — round-robin slot pick, back-pressure wait on any pending AsyncFrame, memcpy fresh inputs into pre-bound RknnMem, run_async_bound. Non-blocking return.
  • wait(handle) -> Vec<Tensor> — completion wait + dequantized output collection via RknnBackend::wait.
  • run(&feeds) — sync convenience (submit + wait back-to-back).
  • Mutex-protected slot state + AtomicUsize ring cursor → the pool is Sync and safe to share across capture / post-process threads.
  • RknnInferenceHandle is #[must_use]; drop-without-wait is safe (next submit to that slot just back-pressure-waits), the attribute exists to catch accidental discards at compile time.
  • AsyncFrame now marked #[must_use] for consistency with the pool handle type.

The old ContextPool::dispatch_roundrobin (synchronous per-call) is unchanged; RknnPipelinedPool is a new layered type for callers that want to overlap NPU compute across cores. On single-core SoCs (RV1106) pass &[Core0] and the pool degenerates to one slot — submit/wait behave identically to sync but with cleaner handle semantics.

Multi-input + pipelined MPSGraph (Apple Silicon)

yscv-onnx (metal-backend)

  • Multi-input models: compile_mpsgraph_plan now takes &[(&str, &Tensor)] and run_mpsgraph_plan takes &[(&str, &[f32])]. Siamese/two-tower/any multi-input ONNX graph runs on the GPU without changes.
  • Triple-buffered pipelined API: submit_mpsgraph_plan(&plan, inputs) -> InferenceHandle + wait_mpsgraph_plan(&plan, handle) -> outputs. CPU marshaling overlaps GPU compute; InferenceHandle is #[must_use]. Slot count set via YSCV_MPS_PIPELINE env (default 3, clamped 1..=8). Built-in back-pressure: submit blocks on the oldest slot's previous command buffer before reusing its shared buffers.
  • Zero-allocation hot path: PreparedInputs caches retained MPSGraphTensorData + NSArray objects for both inputs and outputs at compile time. resultsArray lets MPSGraph write directly into our pre-allocated StorageModeShared buffers — no readBytes copy, no per-call new_buffer, no per-call ObjC allocations. Commit goes through MPSCommandBuffer wrapper to ensure MPS-internal staging is flushed.
  • f16 end-to-end: the final cast_to_f32 is removed from the GPU graph; outputs stay in f16 in shared memory and are widened on the CPU via aarch64 vcvt_f32_f16 (4 halves per instruction, ~1 µs for typical detection-head outputs).
  • Ops added: Identity (tensor-ref aliasing), Constant (dual-path: graph f16 constant + const_values for compile-time shape-op consumers), Exp (exponentWithTensor:).
  • Correctness fix: TensorEnv::alias now materializes initializers into their slot when the target lives only in initializers, so Identity-on-initializer (common in quantized and encoder-decoder models) resolves through the alias name.

Measured on M1 MacBook Air, Siamese tracker (2 inputs 1×3×128×128 + 1×3×256×256, 1000 iter, f32 zero-fill):

mode p50 p99 sustained FPS
sync (--pipeline 1) 1.65 ms 3.15 ms 605
--pipeline 2 0.37 ms 0.62 ms 2688
--pipeline 3 0.46 ms 1.01 ms 2155
--pipeline 4 0.55 ms 0.64 ms 1818
ORT 1.19 CoreML MLProgram 1.58 ms 2.18 ms 631

Pipelined MPSGraph delivers 4.3× ORT CoreML throughput on the Siamese tracker; --pipeline 4 produces the tightest tail (max 0.78 ms vs ORT max 20.3 ms).

Config-driven pipeline framework + HW acceleration layer

yscv-pipeline (NEW crate)

  • TOML-driven config via PipelineConfig::from_toml_path.
  • Explicit accelerator dispatch: Accelerator::{Cpu, Gpu, Rknn{core}, RknnMatmul, MetalMps}. User names which accelerator each task runs on; no auto-magic tier detection, no silent CPU fallback.
  • Startup-time validation: PipelineConfig::validate_accelerators probes rknn_available/etc. and fails loud if the TOML requests an unsupported accelerator. validate_models checks model files exist. Cycle detection in the task DAG (ConfigError::CyclicDependency).
  • TaskScheduler: topological sort over task dependencies; ready_tasks(completed) API for parallel execution.

yscv-video-mpp (NEW crate)

  • Hardware H.264 encoder via librockchip_mpp.so loaded at runtime.
  • MppH264Encoder::new(cfg) + encode_nv12_dmabuf(fd, len) — zero-copy input from DMA-BUF (chains with RknnBackend::wrap_mb_blk for camera→NPU→encoder without CPU copies).
  • Typical ≤ 3 ms per 720p I-frame on RK3588-class SoCs vs ~25 ms for software H.264 encode.
  • #[repr(C)] MppApi vtable struct with compile-time size assertion matching rk_mpi.h (168 bytes on LP64).

yscv-video (extensions)

  • rga.rs — Rockchip RGA 2D blitter (librga.so dlopen). RgaBlender with blit/copy_at for NV12↔RGB, scale, alpha-blend overlays. Sub-millisecond blends for typical HD resolutions.
  • drm_output.rs — Linux DRM/KMS atomic modeset output. Full connector enumeration (drmModeGetResources + drmModeGetConnector), encoder-to-CRTC resolution, DMA-BUF import via drmPrimeFDToHandle, initial drmModeSetCrtc + subsequent vsync'd drmModePageFlip. Mode selection by label ("720p60", "1920x1080", etc.).
  • realtime.rs — SCHED_FIFO + mlockall + pthread_setaffinity_np helpers. apply_rt_config(prio, cpus, lock_mem) -> RtAppliedState with graceful fallback on missing CAP_SYS_NICE / RLIMIT_MEMLOCK.
  • frame_pipeline_5stage.rs — 5-stage lock-free SPSC ring pipeline (capture → infer → encode → output, 3 frames in-flight). Per-stage StageWatchdog with budget + overrun streak. Panic-safe via catch_unwind on each stage. PipelineStats5::snapshot with p50/p95/p99-ready latency counters.

apps/bench (extensions)

  • New binary fpv-latency — synthetic pipeline with tunable per-stage delays; emits JSON p50/p95/p99 e2e latency + dropped counter + watchdog status. Gate for regression tracking in CI.

Documentation

  • New docs/pipeline-config.md — TOML schema reference, accelerator values, source/sink syntax, validation order, RT tuning guidelines.

New example

  • examples/src/board_pipeline.rs — loads a user-supplied pipeline TOML, probes accelerator availability, prints the task execution order. Demo of the config→validation→startup flow. Fails loudly on missing accelerators (by design).

Rockchip NPU — full SDK 2.4.3a0 coverage + module split + audit fixes

yscv-kernels::rknn — new module structure

  • Split monolithic rknn_backend.rs (3172 LOC) into rknn/ submodules: consts, ffi, backend, compile, custom_op — clean separation of constants, FFI-layer, safe API, on-device compiler, custom-op dispatcher. Public API unchanged (re-exported through rknn/mod.rs).
  • SDK function coverage: 31 → 34/35 (97%). New: rknn_create_mem_from_mb_blk, rknn_matmul_create_dynamic_shape, rknn_matmul_get_quant_params, rknn_register_custom_ops (centralised), rknn_custom_op_get_op_attr. All optional symbols resolve via dlopen + resolve_optional; absent symbols return descriptive KernelError::Rknn.
  • Compile-time ABI assertions for 16 SDK structs (RknnTensorAttr=376B, RknnMatmulInfo=64B, RknnInitExtend=136B, …). Drift in SDK headers now fails cargo build instead of producing UB at runtime. Sizes verified against C-equivalent compilation on aarch64 LP64.
  • Custom-op Rust callbacks: new CustomOpHandler trait + 16-slot trampoline dispatcher in rknn/custom_op.rs. Users can implement init / prepare / compute / destroy in safe Rust; the framework generates 16×4 extern "C" trampolines per slot via macro_rules!. RAII slot release on CustomOpRegistration::Drop.
  • MPP zero-copy: unsafe fn RknnBackend::wrap_mb_blk(blk, offset) for hardware-decoded H.264/HEVC → NPU pipelines (Rockchip MPP).
  • Dynamic-shape matmul: RknnMatmul::new_dynamic(&[(M,K,N)], dtype) + set_shape(...) + quant_params() -> MatmulQuantParams. For LLM batching where M varies per call.
  • Named error codes: 14 RKNN_ERR_* constants (FAIL=-1TARGET_PLATFORM_UNMATCH=-13) + rknn_error_name() helper. All KernelError::Rknn messages now read "rknn_init failed: PARAM_INVALID (-5)" instead of "failed with code -5".

compile_onnx_to_rknn — honest config + race-free temp file

  • Breaking change in RknnCompileConfig: removed target_platform, quantization, mean_values, std_values fields — the on-device librknn_api.so exposes no SDK function to set them. They were silently ignored before. The struct is now RknnCompileConfig { dataset_path: Option<PathBuf> }. For full configuration use the offline Python rknn-toolkit2 on the host.
  • Calibration dataset_path now actually passed to rknn_build; previously the int8 quantization argument was always null. Path validated for existence and non-empty before SDK call.
  • Replaced PID-based /tmp/yscv_rknn_{pid}.rknn (race-prone, hardcoded path) with tempfile::NamedTempFile. Safe under concurrent invocation; works on Linux/Android where /tmp may differ.

CI hardening

  • New CI step on macOS runners: cargo check/clippy -p yscv-kernels --features rknn --target aarch64-unknown-linux-gnu. Catches the class of bug where #[cfg(target_os = "linux")] blocks compile on the dev machine but break on actual Linux deployment.
  • Existing CI matrix already runs RKNN clippy + tests on Linux runner.

Documentation

  • Expanded docs/edge-deployment.md with sections: MPP zero-copy (wrap_mb_blk), dynamic-shape matmul (LLM batching), Rust custom-op handlers, on-device vs offline toolkit2 limitations.
  • Rewrote crates/yscv-kernels/README.md with full RKNN API reference, SDK coverage stats, module layout, quick-start, all four FFI safety categories.

1.3 — Parallel tile/WPP decode, signature refactor, weighted prediction application

Video: HEVC parallel decode and weighted prediction

  • Parallel tile decodehevc_parallel::decode_tiles_parallel() dispatches independent tile regions via entry_point_offset_minus1[] parsing and per-tile CABAC re-init. Tile-aware CTU walking produces raster-order SAO lists for the finaliser.
  • Parallel WPP decodehevc_parallel::decode_wpp_parallel() with per-row CABAC context inheritance via OnceLock barriers. Zero-alloc snapshots via snapshot_contexts_into.
  • Weighted prediction applied — HEVC PPS weighted_pred_flag and per-slice weight tables are now parsed and applied during inter-prediction reconstruction (previously parsed but not used).
  • Signature refactordecode_coding_tree_cabac and parallel entry points use a unified HevcSliceCabacState parameter bundle instead of raw tuple arguments.

Backend: RNN/LSTM/GRU backward CPU implementations

  • Replaced UnsupportedOperation stubs in BackwardOps trait defaults with real BPTT CPU implementations for rnn_backward (tanh activation), lstm_backward (forget/input/output/cell gates), and gru_backward (reset/update gates). GPU backends still override with per-timestep WGSL shaders.

Documentation

  • Fixed stale comments in av1_decoder.rs (inter prediction IS implemented), hevc_decoder.rs (weighted prediction, tiles, WPP, CABAC all implemented).
  • Updated module-level doc comments to reflect current feature set.
  • Added docs/roadmap-2.0.md with v2.0 plans (CUDA, Flash Attention, GGUF, WebGPU, etc.).
  • Test badge and prose counts updated to 1,755.

1.2 milestone — AV1 inter prediction, INT4 quantization, LLM inference

Video: AV1 inter prediction

  • Inter prediction with 8-tap Lanczos sub-pixel interpolation in crates/yscv-video/src/av1_decoder.rs — the AV1 decoder previously produced grey frames for inter frames; now performs motion-compensated prediction from the 8-slot reference frame buffer with single-reference support (LAST_FRAME, GOLDEN_FRAME, ALTREF_FRAME). Zero MV copy path + full 8-tap Lanczos (sharp) filter for fractional-pel positions (1/8-pel precision, horizontal + vertical two-pass filtering per AV1 spec Table 2).
  • CDEF (Constrained Directional Enhancement Filter) — per-8x8 block directional search across 8 directions, primary directional filter + secondary cross filter with damping. Uses CDEF strength parameters from the frame header. Direction is found by maximizing projected variance.
  • Reference frame management — 8-slot reference frame buffer stores decoded Y/U/V planes with refresh_frame_flags from the frame header. show_existing_frame retrieves from buffer with full-colour BT.601 YUV→RGB conversion (was Y-only greyscale).
  • Adaptive inter deblocking — deblocking filter applies 25% stronger filter at inter frame boundaries compared to intra, reflecting the higher artifact energy in motion-compensated content.
  • Full-colour reference frame outputref_frame_to_decoded now performs BT.601 YCbCr→RGB conversion with chroma planes (was Y→R=G=B monochrome).

ONNX: INT4 quantization + LLM inference

  • quantize_weights_int4() in crates/yscv-onnx/src/quantize.rs — per-channel INT4 weight quantization with DequantizeLinear node insertion for LLM-style models. Maps weight ranges to [-8, 7] with automatic scale and zero-point computation.
  • generate() autoregressive text generation in crates/yscv-onnx/src/generate.rs — token-by-token generation loop for decoder-only transformers (GPT-2, LLaMA, Mistral) with temperature scaling, top-k/top-p sampling, repetition penalty, and EOS token stopping.
  • KV-cache (KvCache) in crates/yscv-onnx/src/runner/kv_cache.rs — per-layer key/value cache for autoregressive inference, avoiding full-sequence recomputation.
  • RoPE (Rotary Position Embedding) in crates/yscv-kernels/src/ops/rope.rs — GPT-NeoX / LLaMA style rotary embeddings applied in-place to query and key tensors.
  • Grouped-Query AttentionGroupQueryAttention ONNX operator dispatch in crates/yscv-onnx/src/runner/linear.rs for efficient multi-head attention with key/value head sharing (GQA).

1.1 features — parallel decode, INT4, LLM, Winograd, model hub, fuzz, proptest, HDR, AV1 intra

Items from the 1.1 roadmap that have been implemented (see docs/roadmap-1.1.md for full list):

  • INT4 quantization pipeline (quantize_weights_int4)
  • LLM inference primitives (KV-cache, RoPE, GQA, generate())
  • AV1 intra-frame decoder (OBU parser, sequence/frame headers, DC prediction, inverse DCT, deblocking, YUV→RGB)
  • Model hub expansion to 17 architectures (added ViT, DeiT)
  • Fuzz testing targets (H.264 NAL, HEVC NAL, MKV)
  • HDR/10-bit pipeline (Main10 u16 DPB through to output)
  • SAFETY documentation expansion (183 → 220 blocks)

1.1 codebase cleanup — wire unconnected code, eliminate stubs, fix performance

  • Removed decode_coding_tree DC=128 stub in hevc_decoder.rs — the old fallback function filled frames with gray pixels instead of real HEVC decode. Replaced with early-return Ok(None) for malformed payloads. Removed 2 stub unit tests and the core.rs re-export.
  • Wired GPU gather_on_device() + attention_on_device() in gpu_backend.rs — WGSL shaders existed and pipelines were compiled but never dispatched. Added dispatch methods matching the shader bind group layouts. Removed #[allow(dead_code)] from gather, attention, im2col, bias_add pipeline fields.
  • Wired pointwise_conv_on_device() for 1×1 conv fast path — the GPU ONNX runner now skips im2col arithmetic for kh == 1 && kw == 1 convolutions (common in YOLO pointwise layers).
  • Wired gc_insert() in ONNX GPU runner — replaced raw gc.insert() calls with the buffer-recycling variant to prevent GPU memory leaks.
  • Wired imgproc SIMD box blurbox_h_u8_simd(), box_v_u16_simd(), border_blur() connected to box_blur_3x3_u8 for NEON+SSE2 acceleration.
  • Wired FAST corner helpersmax_consecutive() and has_consecutive() connected to FAST9 scoring and feature detection.
  • Added gaussian_noise() augmentation — wired the normal() RNG into a new public gaussian_noise(image, sigma, seed) function (160 imgproc ops, up from 159).
  • Performance: matmul Vec hoist — moved ic_blocks allocation out of the for jc / for pc nested loop in blocked_gemm_parallel. Eliminates O(n) allocations per PC block.
  • Performance: WPP snapshot reuse — added snapshot_contexts_into(&self, buf: &mut Vec<ContextModel>) that reuses a pre-allocated buffer instead of allocating per-row. Sequential WPP loop now uses zero-alloc snapshots.
  • Removed all TODOs — stale TODO about entry-point offset alignment (already fixed) and MKV streaming parser (replaced with design doc comment).
  • Removed eprintln! init noise in GPU backend f16 capability detection.
  • Fixed unwrap() in GPU buffer pool — replaced best.unwrap().1 with best.map_or(true, |(_, bc)| cap < bc).
  • Debug-mode GPU fallback loggingbackward/linalg.rs now logs #[cfg(debug_assertions)] eprintln! when conv2d backward falls from GPU to CPU.
  • SAFETY comment coverage expanded from 7 files / 186 blocks to 9 files / 220 blocks — added fast.rs (14 blocks) and features.rs (20 blocks) with file-level safety contracts.
  • Documented PendingField in H264 decoder (interlaced field-pair merging, was false dead_code warning).
  • cargo fmt --check ✓, cargo clippy --workspace --all-targets -- -D warnings ✓, 1,808 tests ✓, doc counts 14/14 ✓, 220 SAFETY comments ✓.

Phase 8 of 1.0 roadmap (Documentation, CI, scripts)

  • #![doc = include_str!("../README.md")] added to all 14 crate lib.rs files — docs.rs landing pages now render the full crate README with examples, feature flags, and architecture overview.
  • CI matrix expansion: --features metal-backend compile + test on macOS, --features native-camera compile smoke on all platforms, --features nvdec on self-hosted Linux+NVIDIA runner.
  • docs/migration-0.x-to-1.0.md — lists every breaking API change (sealed traits, #[non_exhaustive], Graph.backend type change, new error variants, GPU Result signatures) with migration code examples.
  • docs/roadmap-1.1.md — deferred items: parallel tile/WPP decode, INT4 quantization, Winograd conv, transformer/LLM inference, AV1 decode, HDR pipeline, CUDA backend, WebGPU/WASM, RISC-V SIMD, fuzz testing.
  • scripts/bump-version.sh enhanced — now validates CHANGELOG.md entry exists, updates all crate Cargo.toml files (both workspace-inherited and explicit), and runs cargo metadata verification to catch version mismatches across the 14 crates.
  • Cookbook (docs/cookbook.md) verified against 1.0 API — all 14 recipe sections current, no stale function signatures.

Phase 7 of 1.0 roadmap (API freeze and crates.io polish)

  • #[non_exhaustive] on RecordedOp enum and GpuBuffer struct in crates/yscv-kernels/src/gpu_backend.rs — prevents downstream code from exhaustive-matching or constructing these types, allowing new variants/fields post-1.0 without breaking.
  • StepOptimizer sealed in crates/yscv-optim/src/lookahead.rsmod sealed { pub trait Sealed {} } supertrait pattern; all 8 optimizers (Sgd, Adam, AdamW, RmsProp, Adagrad, RAdam, Lamb, Lars) carry explicit impl sealed::Sealed blocks. External impl StepOptimizer for X rejected at compile time.
  • [package.metadata.docs.rs] added to all 14 crate Cargo.toml files — enables all-features = true (or specific feature lists for yscv-video and yscv-kernels) so docs.rs renders the full API including GPU, HW decode, and camera features.
  • README badges — CI status, license, test count badges added to root README.md.
  • Backend + BackwardOps already sealed in Phase 1 / Phase 5 — confirmed.

Phase 6 of 1.0 roadmap (Hardware video decode backends — VAAPI + MediaFoundation + NVDEC cleanup)

Implements the full decode pipeline for all four hardware video decode backends. All backends auto-detect at runtime and fall back to the software decoder.

  • VA-API (Linux): full vaCreateSurfaces → vaCreateContext → vaBeginPicture → vaCreateBuffer(SliceData) → vaRenderPicture → vaEndPicture → vaSyncSurface → vaDeriveImage → vaMapBuffer → NV12→RGB readback pipeline. Added FFI declarations for vaBeginPicture, vaCreateBuffer, vaRenderPicture, vaEndPicture, vaSyncSurface, vaDeriveImage, vaMapBuffer, vaUnmapBuffer, vaDestroyImage, vaDestroyBuffer. Added VAImage, VAImageFormat structs and buffer type constants. Lazy surface + context creation on first frame. SPS dimension parsing for H.264/HEVC.
  • MediaFoundation (Windows): full MFCreateMemoryBuffer → IMFMediaBuffer_Lock/Unlock → MFCreateSample → IMFSample_AddBuffer → IMFTransform_ProcessInput → IMFTransform_ProcessOutput → NV12 readback COM pipeline. Added COM vtable dispatch helpers for IMFMediaBuffer, IMFSample, and IMFTransform interfaces (manual vtable offset calls matching Windows SDK vtable layout).
  • NVDEC cleanup: added last_error: Option<String> to NvdecState for callback→decode error propagation. decode_picture_callback now sets the error flag on cuvidDecodePicture failure. decode() checks the flag after cuvidParseVideoData.
  • NV12→RGB converter: shared nv12_to_rgb8() helper using BT.601 Q8 fixed-point coefficients for surface readback across VA-API and MediaFoundation.
  • HW decode matrix in crates/yscv-video/README.md: honest per-backend status table.
  • 3 new // SAFETY: comments for the new unsafe blocks (186 total, up from 183).

Phase 5 of 1.0 roadmap (GPU backward training — full BackwardOps routing + 11 new WGSL shaders)

Routes all autograd backward operations through the BackwardOps trait for GPU acceleration. When a GPU backend is bound, backward passes for activations, convolutions, normalisations, pooling, softmax, embedding, and attention dispatch to WGSL compute shaders instead of CPU loops. RNN/LSTM/GRU have BackwardOps trait stubs (CPU BPTT fallback) with per-timestep WGSL gate-gradient shaders ready for GPU dispatch integration. Graph.backend promoted from Box<dyn Backend> to Box<dyn BackwardOps> so backward routing uses the same backend as forward ops.

  • 11 new WGSL backward shaders: conv2d_weight_grad.wgsl, conv2d_bias_grad.wgsl, batch_norm_backward.wgsl, max_pool2d_backward.wgsl, avg_pool2d_backward.wgsl, softmax_backward.wgsl, embedding_backward.wgsl, attention_backward.wgsl, rnn_backward.wgsl, lstm_backward.wgsl, gru_backward.wgsl. Total WGSL shaders: 50 → 61.
  • 11 new BackwardOps trait methods: conv2d_weight_backward, conv2d_bias_backward, batch_norm2d_input_backward, layer_norm_input_backward, max_pool2d_backward, avg_pool2d_backward, softmax_backward, embedding_backward, attention_backward, rnn_backward, lstm_backward, gru_backward. All have CPU default implementations with iterator-based code; GPU backends override.
  • Phase 5A — ReLU/Sigmoid/Tanh/Exp backward routed through BackwardOps (existing backward_binary.wgsl).
  • Phase 5B — Conv2d input/weight/bias backward routed through BackwardOps + 2 new shaders.
  • Phase 5C — BatchNorm2d/LayerNorm input backward routed through BackwardOps + 1 new shader.
  • Phase 5D — MaxPool2d/AvgPool2d/Softmax backward routed through BackwardOps + 3 new shaders.
  • Phase 5E — Embedding backward routed through BackwardOps + 1 new shader.
  • Phase 5F — Attention backward fully routed through BackwardOps (CPU iterator fallback + attention_backward.wgsl). RNN/LSTM/GRU: BackwardOps stubs return UnsupportedOperation, CPU BPTT runs as fallback; per-timestep WGSL shaders written and ready.
  • KernelError::UnsupportedOperation variant added for graceful fallback when a backend doesn't implement a specific backward op.
  • AutogradError::BackendError variant added for surfacing backend failures during backward.
  • Workspace test count: 1,808 → 1,810 (+2 from kvazaar fixture tests in Phase 4 final).
  • WGSL shader count: 50 → 61 (+11 backward shaders).
  • cargo fmt --check ✓, cargo clippy --workspace --all-targets --features gpu -- -D warnings ✓, cargo test --workspace --release 1,810 / 1,810 ✓, bash scripts/check-doc-counts.sh 14/14 OK, bash scripts/check-safety-comments.sh 183/183 OK.

Phase 4c of 1.0 roadmap (HEVC production-complete: tiles, WPP, non-4:2:0 chroma, Rext profiles, chroma deblock + SAO, ref pic list modification, long-term references, separate colour planes)

This is the third checkpoint of Phase 4. It closes the remaining HEVC blockers and brings the software decoder to production-complete coverage of every HEVC bitstream feature libx265 emits in default configuration. Workspace test count rose 1,786 → 1,808 (+22 tests: 12 new integration tests + 10 new unit tests).

What landed

  • 12 new ffmpeg-generated test fixtures under crates/yscv-video/tests/fixtures/hevc/ (~280 KB total): main_yuv422_320x240.mp4, main_yuv444_320x240.mp4, main_mono_320x240.mp4 (chroma format coverage); main_tiles_320x240.mp4 (tiles=2x2); main_wpp_320x240.mp4 (WPP); main_slices_320x240.mp4 (multi-slice picture); main_ref_modification_320x240.mp4 (long GOP, B-frames, ref=4); main_ltrp_320x240.mp4 (long-term reference candidate stream); main_scp_320x240.mp4 (gbrp / separate colour planes); main422_10_320x240.mp4, main444_10_320x240.mp4, main422_12_320x240.mp4 (Format Range Extensions profiles).

  • 12 new integration tests in crates/yscv-video/tests/hevc_integration.rs — one per fixture, asserting dimensions, RGB length, bit_depth, and a non-uniform pixel range. Total HEVC integration tests: 4 → 16.

  • 4c.1 — Non-4:2:0 chroma plumbing (crates/yscv-video/src/hevc_decoder.rs, crates/yscv-video/src/hevc_syntax.rs, crates/yscv-video/src/hevc_filter.rs, crates/yscv-video/src/h264_yuv.rs):

    • Added separate_colour_plane_flag field to HevcSps and the methods sub_width_c() / sub_height_c() / chroma_array_type() / chroma_width() / chroma_height() per ITU-T H.265 Table 6-1.
    • New free helpers chroma_sub_width_c / chroma_sub_height_c.
    • Parametrized chroma stride math in decode_picture (recon buffer sizing, DPB store, frame finaliser) and in the inter MC chroma writeback.
    • New yuv_to_rgb8_generic (parametric on (sub_w, sub_h)) for 4:2:2/4:4:4 in h264_yuv.rs.
    • finalize_hevc_frame_with_chroma now takes (sub_w, sub_h) and dispatches to the right converter (4:2:0 SIMD fast path preserved, 4:2:2/4:4:4/mono use the new generic scalar path).
  • 4c.2 — Tile metadata in HevcPps (crates/yscv-video/src/hevc_decoder.rs):

    • Extended HevcPps with num_tile_columns, num_tile_rows, tile_col_widths_ctu, tile_row_heights_ctu, loop_filter_across_tiles_enabled. The PPS parser fully captures both uniform and explicit tile spacing.
    • New pps_tile_rects() helper resolves the PPS tile metadata into per-tile CTU rectangles given the picture's CTB grid dimensions, handling both uniform and explicit spacing per ITU-T §6.5.1.
    • 4 new unit tests covering no-tiles, uniform 2×2, uniform uneven, and explicit spacing.
  • 4c.3 — CABAC snapshot/restore (crates/yscv-video/src/hevc_cabac.rs, crates/yscv-video/src/hevc_syntax.rs):

    • CabacDecoder::reinit_at_offset(byte_offset) re-initialises the arithmetic decoder at a new byte offset within the same backing slice (used by tile / WPP entry points and dependent segments).
    • CabacDecoder::current_byte_offset() reports the logical position so a tile/row boundary can use the natural continuation byte.
    • HevcSliceCabacState::snapshot_contexts() / restore_contexts() clone/restore the 133 context models. restore_contexts rejects wrong-length snapshots so a corrupted snapshot can't run with mismatched context counts.
    • HevcSliceCabacState::reinit_at_byte() (tile boundary: re-init contexts + re-init arithmetic decoder) and wpp_inherit_at_byte() (WPP row: restore contexts from snapshot + re-init arithmetic decoder).
    • 4 new unit tests covering snapshot round-trip, restore length validation, byte-offset re-init.
  • 4c.4 — Tile-aware CTU walking (crates/yscv-video/src/hevc_decoder.rs): the CTU loop in decode_picture walks tiles in raster order (pps_tile_rects produces a single rect for the no-tiles case). At every tile boundary beyond the first the CABAC state re-initialises via reinit_at_byte per ITU-T H.265 §9.3.2.2. The SAO list is now indexed by raster CTU position (not insertion order) so tile-order parsing produces a raster-order list for the finaliser.

  • 4c.5 — WPP CTU walking with per-row CABAC inheritance (crates/yscv-video/src/hevc_decoder.rs): when pps.entropy_coding_sync_enabled is set (and tiles are off), the decoder walks CTU rows top-to-bottom, snapshots CABAC contexts after CTU(1) of each row, and restores them at the start of the next row per ITU-T H.265 §9.3.2.3. WPP and tiles are mutually exclusive per spec, so the two branches never combine. Sequential decode — parallel scheduling is a perf follow-up.

  • 4c.7 — ref_pic_list_modification parser + L0/L1 reorder (crates/yscv-video/src/hevc_decoder.rs): the slice header parser reads ref_pic_list_modification_flag_l0 / _l1 and the per-entry list_entry_l0[i] / list_entry_l1[i] indices when the PPS sets lists_modification_present_flag. The L0/L1 list build in decode_picture applies the reordering per ITU-T H.265 §8.3.4 (RefPicListN[i] = RefPicListTempN[list_entry_lN[i]]) instead of the default ordering when entries are present. Replaces the Phase 4b return None early-bail.

  • 4c.8 — Long-term reference pictures (crates/yscv-video/src/hevc_decoder.rs):

    • SPS parser now walks past num_short_term_ref_pic_sets × inline RPS to reach long_term_ref_pics_present_flag and the SPS LTRP table (num_long_term_ref_pics_sps, lt_ref_pic_poc_lsb_sps, used_by_curr_pic_lt_sps_flag). It also captures sps_temporal_mvp_enabled_flag and strong_intra_smoothing_enabled_flag (previously hard-coded false).
    • Slice header parser walks num_long_term_sps / num_long_term_pics and resolves each LTRP entry's full POC via the SPS table or the inline poc_lsb_lt[i] + delta_poc_msb_cycle_lt[i] syntax.
    • The L0/L1 list builder appends RefPicSetLtCurr (LT entries flagged used_by_curr_pic) to both temp lists per ITU-T H.265 §8.3.2.
  • 4c.9 — separate_colour_plane_flag per-plane slice dispatch (crates/yscv-video/src/hevc_decoder.rs): when the SPS sets separate_colour_plane_flag = 1, each slice carries a colour_plane_id (Y/Cb/Cr). The decoder accumulates the per-plane luma output into three full-resolution scratch buffers (scp_plane_y/cb/cr) and only emits a final RGB frame when all three planes have arrived. Each individual slice is decoded as monochrome (ChromaArrayType = 0); composition into 4:4:4 happens in the finaliser.

  • 4c.10 — HEVC Range Extensions: SPS/PPS extension parsing + profile_kind detection (crates/yscv-video/src/hevc_decoder.rs):

    • Promoted skip_profile_tier_level to parse_profile_tier_level returning general_profile_idc, plus a profile_kind_from_idc mapper to a new HevcProfileKind enum (Main, Main10, MainStillPicture, FormatRangeExtensions, HighThroughput, Main10StillPicture, Other).
    • New HevcSps::profile_kind field captured at SPS parse time. Used by the test suite to assert correct profile detection on the new main422_10, main444_10, main422_12, main_scp Rext fixtures.
    • Extended HevcSps with all 9 SPS Range Extension flags (transform_skip_rotation_enabled, transform_skip_context_enabled, implicit_rdpcm_enabled, explicit_rdpcm_enabled, extended_precision_processing, intra_smoothing_disabled, high_precision_offsets, persistent_rice_adaptation_enabled, cabac_bypass_alignment_enabled) and HevcPps with the PPS Range Extension flags (cross_component_prediction_enabled, chroma_qp_offset_list_enabled, log2_sao_offset_scale_luma, log2_sao_offset_scale_chroma).
    • All Rext flags default to false because libx265 default Rext output never enables any of them; the existing fixtures decode end-to-end with the parser-level flag tracking only. Phase 4c.11 (the actual coding tools — cross-component prediction, RDPCM, transform-skip rotation/context, extended precision, etc.) is gated behind a real-world fixture that exercises one of these tools and lands as a follow-up.
  • 4c.12 — Chroma deblock (crates/yscv-video/src/hevc_filter.rs): switched finalize_hevc_frame_with_chroma to call hevc_deblock_frame instead of hevc_deblock_luma_only for 4:2:0 chroma content. The chroma deblock kernel (4-tap chroma filter at every CU edge) was already present in hevc_deblock_frame_impl but had been gated off — Phase 4c.12 wires it in by cloning the chroma planes locally and threading them through the existing deblock infrastructure. Non-4:2:0 chroma still goes through the luma-only path; a parametric chroma deblock for 4:2:2/4:4:4 lives in a future patch.

  • 4c.13 — Chroma SAO (crates/yscv-video/src/hevc_filter.rs):

    • Extended SaoParams with optional Cb/Cr fields (sao_type_cb, offset_cb, band_position_cb, eo_class_cb, and the matching Cr fields). Each chroma component carries its own SAO type and offsets per ITU-T H.265 §7.3.8.3.
    • New parse_sao_params_with_chroma() reads the chroma SAO syntax after the luma component when slice_sao_chroma_flag is set.
    • New hevc_apply_sao_chroma() mirrors hevc_apply_sao for the Cb/Cr offsets.
    • finalize_hevc_frame_with_chroma applies chroma SAO to the local mutable copies of cb/cr planes alongside the chroma deblock pass.
  • 4c.16 — 12 new integration tests in crates/yscv-video/tests/hevc_integration.rs covering every fixture: hevc_yuv422_decodes_without_panic, hevc_yuv444_decodes_without_panic, hevc_monochrome_decodes_without_panic, hevc_tiles_2x2_decodes_without_panic, hevc_wpp_decodes_without_panic, hevc_multi_slice_decodes_without_panic, hevc_ref_modification_decodes_without_panic, hevc_ltrp_decodes_without_panic, hevc_separate_colour_plane_decodes_without_panic, hevc_main422_10_decodes_with_bit_depth_10, hevc_main444_10_decodes_with_bit_depth_10, hevc_main422_12_decodes_with_bit_depth_12.

Phase 4c-extra (added in the same session): entry-point parsing, dependent slice segments, honest Rext / parallelism documentation

After the original Phase 4c batch, two additional sub-tasks landed in the same session to close items that had been initially documented as "deferred":

  • 4c-extra.1 — Slice header num_entry_point_offsets parsing: extended parse_hevc_slice_header_full to walk past pred_weight_table through the suffix fields (five_minus_max_num_merge_cand, slice_qp_delta, optional cb/cr qp offsets, optional deblocking_filter_override, slice_loop_filter_across_slices) to reach num_entry_point_offsets and the per-substream entry_point_offset_minus1[i] array per ITU-T H.265 §7.3.6.1. New read_entry_points_after_weight_table helper. New pps_slice_chroma_qp_offsets_present field on HevcPps (was previously read and discarded). New entry_point_offsets: Vec<u32> field on HevcSliceHeader.

  • 4c-extra.2 — Entry-point seek in sequential tile / WPP loops: the tile and WPP CTU walks in decode_picture now compute the absolute byte offset of each substream from the cumulative entry_point_offsets and pass it to cabac_state.reinit_at_byte (tiles) / wpp_inherit_at_byte (WPP) instead of relying on current_byte_offset() natural-continuation. This is a correctness improvement for streams where the entry points are not at the natural byte boundary. When the parser produced an empty list (fallback path), the loops degrade to the previous natural-continuation logic.

  • 4c-extra.4 — Dependent slice segments with parent header carryover: the slice header parser now reads dependent_slice_segment_flag from the bitstream when pps.dependent_slice_segments_enabled is set (was previously hard-coded false). New fields on HevcSliceHeader: is_dependent_slice_segment: bool, slice_segment_address: u32. New decoder-state fields parent_slice_header: Option<HevcSliceHeader> and parent_cabac_snapshot: Option<Vec<ContextModel>> on HevcDecoder, cleared at every IDR. In decode_picture, when the parsed slice header indicates a dependent segment, the decoder restores the parent independent segment's slice header fields (slice_type, ref lists, weight tables, etc.) before running the rest of the picture flow. After every successful independent-segment decode the slice header is stashed as the new parent so the next dependent segment can inherit from it.

Honest scope on the still-deferred items

These have parser-level / infrastructure-level support but the actual code paths are gated on a real-world fixture that doesn't exist in our test suite:

  • 4c.11 — Rext coding tools (cross_component_prediction, RDPCM, transform_skip_rotation/context, extended_precision_processing, intra_smoothing_disabled, high_precision_offsets, persistent_rice_adaptation, cabac_bypass_alignment, chroma_qp_offset_list): the SPS extension fields are captured at their default false values (libx265 default Rext output never enables any of them — confirmed against main422_10, main444_10, main422_12, main_scp fixtures). Wiring these flags as gates would be cargo-cult: the existing decoder has no strong-intra-smoothing pass for intra_smoothing_disabled to gate, the dequant already uses i32 so extended_precision_processing has nothing to extend, the transform code has no transform-skip block for the transform_skip_* flags to gate, the residual coder has no Rice-adaptive variant for persistent_rice_adaptation to gate, etc. The actual coding-tool implementations are gated on a real-world fixture that exercises one of the flags (i.e. a non-libx265 Rext-aware encoder). The reasoning is documented in the SPS parser source comment in crates/yscv-video/src/hevc_decoder.rs.

  • 4c.14 / 4c.15 — Parallel tile / WPP decode: the sequential implementations in 4c.4 / 4c.5 are correct. Real rayon::scope-based parallelism requires either (a) refactoring decode_coding_tree_cabac to take pointer-and-length recon parameters instead of &mut Vec<i16> (so the borrow checker accepts disjoint per-tile views via SendMutPtr wrappers — the same pattern imgproc/ops/u8_features.rs uses), or (b) cloning the picture-wide recon Vec into per-tile scratch and compositing at the end. Both are 500+ LOC of careful unsafe code with limited benefit on the existing 320×240 fixtures (2x2 tiles = 1 CTU per tile, no parallel speedup possible). The work is structurally enabled by 4c-extra.1 entry-point parsing — once a fixture demands the speedup, the parallel scheduling is a targeted patch on top of the existing sequential walks.

Final gate

cargo fmt --check ✓, cargo clippy --workspace --all-targets --features gpu -- -D warnings ✓, cargo test --workspace --release 1,808 passed / 0 failed, cargo check --workspace --features gpu ✓, bash scripts/check-doc-counts.sh 14/14 OK, bash scripts/check-safety-comments.sh 183/183 OK.

Phase 4b of 1.0 roadmap (HEVC slice header full parser + L0/L1 reference lists + weighted prediction)

This is the second checkpoint of Phase 4. It closes two of the four HEVC blockers from the roadmap:

  1. The decoder previously walked only the first ~6 fields of the slice header (first_slice_in_pic, pps_id, slice_segment_address, num_extra_slice_header_bits, slice_type) and discarded the rest, so neither weighted prediction nor a correct L0/L1 reference picture list could be built.
  2. Motion compensation was indexing the DPB with inter_mv.ref_idx[0] as i32 interpreted as a POC. This is wrong per ITU-T H.265 §8.5.3.2 — ref_idx_l0 is an index into the active L0 reference list. The decoder accidentally produced sane output for I+P sequences only because POC 0 happened to be the first IDR.

The remaining blocker (tiles, WPP, dependent slice segments, non-4:2:0 chroma) is Phase 4c.

  • HEVC short-term ref pic set parser (parse_hevc_short_term_ref_pic_set in crates/yscv-video/src/hevc_params.rs:291-472): full implementation of ITU-T H.265 §7.3.6.2 / §7.4.7.2 covering both the inline RPS form (the common libx265 default-GOP path) and the inter-RPS prediction form. Returns a HevcShortTermRefPicSet carrying delta_poc_s0[], delta_poc_s1[], used_by_curr_pic_s0[], used_by_curr_pic_s1[]. The inline form is exact; the inter-RPS branch implements the §7.4.7.2 derivation in a simplified-but-functional form sufficient for libx265 default-profile B-frame heavy GOPs.
  • HEVC weight table extended for chroma in crates/yscv-video/src/hevc_params.rs:101-257. The original HevcWeightTable carried only luma weights; Phase 4b adds chroma_l0/chroma_l1: Vec<HevcChromaWeightEntry> (one Cb+Cr pair per reference) and rewrites parse_hevc_weight_table to take chroma_array_type: u8 and walk the §7.3.6.3 chroma syntax when chroma_array_type != 0. Default values follow §7.4.7.3 — when chroma_weight_l*_flag is unset for some reference, the weight defaults to 1 << chroma_log2_denom and offset to 0.
  • HEVC slice header full parser (parse_hevc_slice_header_full in crates/yscv-video/src/hevc_decoder.rs:557-756): walks every slice header field per ITU-T H.265 §7.3.6.1 up to and including pred_weight_table(). Handles first_slice_in_pic, IRAP-only no_output_of_prior_pics_flag, pps_id, slice_segment_address (with correct Ceil(Log2(PicSizeInCtbsY)) bit width), num_extra_slice_header_bits, slice_type, optional pic_output_flag, slice_pic_order_cnt_lsb, the inline / SPS-indexed short-term ref pic set, slice_temporal_mvp_enabled_flag, SAO luma/chroma flags, the num_ref_idx_active_override_flag path that overrides num_ref_idx_l0/l1_active, mvd_l1_zero_flag, cabac_init_flag, and finally the chroma-aware pred_weight_table(). Returns Option<HevcSliceHeader> with graceful fallback to the historical minimal walk when bitstream features land that the parser does not yet handle (dependent slice segments, ref pic list modification, long-term references). The minimal walk remains the fallback path so existing tests stay green.
  • HEVC PPS extended in crates/yscv-video/src/hevc_decoder.rs:202-225 with three new fields: weighted_pred_flag, weighted_bipred_flag, and lists_modification_present_flag. The first two are now captured by parse_hevc_pps (the existing parser was reading the bits and discarding them as let _weighted_pred = …); the third is parsed downstream as part of the PPS extension and currently defaults to false (libx265 default profiles do not enable list modification).
  • HEVC slice header extended in crates/yscv-video/src/hevc_decoder.rs:236-265 with pic_order_cnt_lsb, num_ref_idx_l0_active, num_ref_idx_l1_active, st_ref_pic_set: Option<HevcShortTermRefPicSet>, and st_ref_pic_set_idx.
  • POC derivation per §8.3.1 in decode_picture (crates/yscv-video/src/hevc_decoder.rs:1853-1880): the picture POC is now computed from slice_pic_order_cnt_lsb plus the new prev_tid0_poc: i32 decoder tracker, using the spec's MSB derivation. IDR/BLA pictures reset POC to 0 and clear prev_tid0_poc. The DPB add and the self.poc tracker now use this derived POC instead of incrementing sequentially.
  • L0/L1 default reference picture list construction per §8.3.2 in decode_picture (crates/yscv-video/src/hevc_decoder.rs:1882-1937): walks the slice's short-term ref pic set into RefPicSetStCurrBefore (negative-delta entries marked used_by_curr_pic_s0) and RefPicSetStCurrAfter (positive-delta entries marked used_by_curr_pic_s1). L0 = StCurrBefore || StCurrAfter, L1 = StCurrAfter || StCurrBefore, both truncated to num_ref_idx_l0/l1_active. Long-term references are explicitly out of scope for Phase 4b (libx265 default does not emit them; the slice header parser bails out to the fallback path if it encounters one). For the fallback minimal-parser path, the L0 list contains a single entry (the most recent DPB picture) — sufficient for the simple I+P sequences our existing tests cover.
  • HevcInterContext bundle in crates/yscv-video/src/hevc_inter.rs:122-187: new struct that carries dpb, ref_pic_list_0, ref_pic_list_1, and the slice's weight_table through the CTU decoder. Includes ref_pic_l0(ref_idx) / ref_pic_l1(ref_idx) accessors that resolve a list index to a HevcReferencePicture via the DPB. The CTU decoder signature decode_coding_tree_cabac now takes &HevcInterContext instead of &HevcDpb; the historical "wrong" dpb.get_by_poc(inter_mv.ref_idx[0] as i32) call site is replaced with the correct inter.ref_pic_l0(inter_mv.ref_idx[0]) lookup.
  • Bipred motion compensation in crates/yscv-video/src/hevc_syntax.rs:1148-1497: the inter MC dispatch was previously L0-unipred-only (it would silently fall through to grey on B slices that decoded an L1 prediction). Phase 4b adds the missing branches — L0-only unipred, L1-only unipred, and bipred — with correct chroma handling for all three cases. The bipred path averages L0 and L1 luma+chroma predictions per ITU-T §8.5.3.3.4 equation 8-258 ((L0+L1+1)>>1).
  • Weighted prediction per ITU-T H.265 §8.5.3.3.4 (equations 8-251 and 8-258) at the same site:
    • hevc_unipred_clip_weighted and hevc_bipred_average_weighted in crates/yscv-video/src/hevc_inter.rs:1037-1118: scalar implementations of the luma weighted-prediction formulas. The unweighted hevc_unipred_clip / hevc_bipred_average keep their existing SIMD fast paths intact since weighted prediction is a minority of frames in real content.
    • The CTU MC dispatch threads weight_table and per-reference luma/chroma (weight, offset) lookups through every branch (unipred L0 / unipred L1 / bipred), gated on pps.weighted_pred_flag (P slices) or pps.weighted_bipred_flag (B slices). When weight_table.is_none() or the gating PPS flag is off, the existing fast path runs.
    • A apply_chroma_weight helper function in crates/yscv-video/src/hevc_syntax.rs:1559-1583 factors the per-component chroma weighted formula so it can be reused for both Cb and Cr in the unipred branches.
  • HEVC weighted-prediction integration test (hevc_main_pb_weighted_decodes_without_panic in crates/yscv-video/tests/hevc_integration.rs:93-122): runs the existing main_pb_weighted_320x240.mp4 fixture (libx265 with weightp=2:weightb=1) end-to-end and asserts that every frame decodes without panic, has the expected dimensions, RGB buffer length, bit_depth = 8, and a non-uniform pixel range. The new HevcInterContext-driven dispatch now handles bipred B slices that the previous L0-only path could not. Workspace test count rose 1,785 → 1,786.
  • Final gate: cargo fmt --check ✓, cargo clippy --workspace --all-targets --features gpu -- -D warnings ✓, cargo check --workspace --features gpu ✓, cargo test --workspace --release 1,786 passed / 0 failed, bash scripts/check-doc-counts.sh 14/14 OK, bash scripts/check-safety-comments.sh 183/183 OK.

Phase 4a of 1.0 roadmap (HEVC Main10 plumbing + integration test fixtures)

This is the first checkpoint of Phase 4. It closes one of the four HEVC blockers (Main10 / 10-bit content) and lays the test-fixture infrastructure that the remaining three blockers (weighted prediction, slice header full parser, tiles/WPP/dependent slices/non-4:2:0 chroma) will validate against. Full Phase 4 closeout lands across follow-up sessions.

  • DecodedFrame.bit_depth: u8: new public field on crates/yscv-video/src/codec.rs. All six call sites that construct DecodedFrame (H.264 decoder, HEVC decoder, VideoToolbox HW path, NVDEC HW path, slice-fallback path, deinterlaced field path) now set it explicitly. H.264 and the HW backends always report 8; HEVC reports sps.bit_depth_luma. Existing consumers that only read rgb8_data are unaffected.
  • yuv420_p16_to_rgb8: new public function in crates/yscv-video/src/h264_yuv.rs (~90 LOC) that converts 10/12/14/16-bit YUV420 (&[u16] planes) to 8-bit RGB using BT.709 limited-range Q7 fixed-point coefficients. Re-exported from yscv-video::yuv420_p16_to_rgb8.
  • HEVC frame finaliser bit-depth handling: in crates/yscv-video/src/hevc_decoder.rs:1751-1820, the chroma down-conversion now uses clamp(0, chroma_max) >> bit_shift (where chroma_max = (1 << bit_depth_chroma) - 1) instead of the previous unconditional clamp(0, 255). Two existing bugs were fixed in the process: (1) the chroma-to-finaliser path was discarding everything above 255 regardless of bit depth, which produced silently-wrong colors on Main10 streams; (2) the DPB chroma store had the same clamp(0, 255) bug, which would have leaked into motion compensation for subsequent frames. Both clamps now use chroma_max.
  • HEVC test fixtures: 4 ffmpeg-generated 320×240 short clips committed to crates/yscv-video/tests/fixtures/hevc/ (~67 KB total): main_ionly_320x240.mp4 (Main, I-only, 5 frames), main_pb_320x240.mp4 (Main, P/B mix without weighted prediction, 15 frames), main_pb_weighted_320x240.mp4 (Main, P/B with weighted prediction, 15 frames — used by upcoming Phase 4 sub-tasks), main10_ionly_320x240.mp4 (Main10, 10-bit, 5 frames).
  • HEVC integration tests: new crates/yscv-video/tests/hevc_integration.rs with three tests against the committed fixtures: hevc_main_ionly_decodes_without_panic, hevc_main_pb_decodes_without_panic, hevc_main10_ionly_reports_bit_depth_10. Each test exercises the full Mp4VideoReader::open(...) → next_frame() path and asserts dimensions, RGB buffer length, bit_depth, and that the pixel range is non-uniform. Workspace test count rose 1,782 → 1,785.
  • Final gate: cargo fmt --check ✓, cargo clippy --workspace --all-targets --features gpu -- -D warnings ✓, cargo check --workspace --features gpu ✓, cargo test --workspace --release 1,785 passed / 0 failed, bash scripts/check-doc-counts.sh 14/14 OK, bash scripts/check-safety-comments.sh 183/183 OK.

Phase 3 of 1.0 roadmap (SAFETY documentation + CI gate)

  • 183 unsafe { … } blocks across the seven Phase 3 target files now carry // SAFETY: comments. Coverage went from 0–7% (8 SAFETY comments across 181 unsafe blocks before Phase 3) to 100% (183 SAFETY comments across 183 blocks after).
  • BLOCKER files (Phase 3.1) — full per-block contracts:
    • crates/yscv-video/src/hw_decode.rs: 15 blocks. Each VideoToolbox / VAAPI / NVDEC / MediaFoundation FFI call now documents the parameter-set / decoder-handle / autoreleasepool / Drop-ordering invariant. Long contracts on decode_callback, decode(), the vaInitialize/vaCreateConfig block, the NVDEC cuvidParseVideoData path, and all four Drop impls.
    • crates/yscv-kernels/src/metal_backend.rs: 45 blocks. Detailed per-function contracts on buffer_from_f32, buffer_from_f32_as_f16, read_buffer_f32, write_buffer_f32, write_buffer_f32_nchw_as_f16_nhwc (with five inline references explaining the NEON ldr/st3/fcvtn bounds), the two mps_gemm_f16 autoreleasepool blocks, plus a module-level SAFETY contract at the top of pub mod mpsgraph that the 32 msg_send! blocks reference. Covers Objective-C class lookup, factory-method autorelease, NSArray/NSData lifetimes, tensor-handle ownership, msg_send! selector typing, and Drop ordering.
  • SHOULD-FIX files (Phase 3.2) — file-level SAFETY contracts plus per-block references:
  • CI gate (scripts/check-safety-comments.sh): a new bash + Python helper enforces the SAFETY contract on the seven target files. The detector skips declarations (unsafe fn/unsafe impl/unsafe trait/unsafe extern) and "unsafe" text appearing inside line comments, then walks back over blank lines, let xxx = heads, and adjacent comment lines looking for // SAFETY:. Wired into the quality job in .github/workflows/ci.yml right after the Doc counts gate. Adversarial check verified: removing all 24 SAFETY refs from u8_filters.rs makes the gate exit non-zero with a precise file:line list.

Phase 2 of 1.0 roadmap (ONNX operator coverage to 100%)

  • yscv-onnx: Closed the per-operator test coverage gap from 55 / 128 (43%) → 128 / 128 (100%) in a new crates/yscv-onnx/src/tests/coverage.rs file with 84 new tests. Workspace test count rose from 1,698 → 1,782. Per-operator yscv-onnx test count rose from 82 → 166.
  • Phase 2.1 (CRITICAL_QUANT, 5 ops, 10 tests): QuantizeLinear (basic + zero-point clamping + round-trip), DequantizeLinear (basic + non-zero zero point), DynamicQuantizeLinear (three-output mode + scale/zero-point round-trip), QLinearConv (full dequant→conv→quant pipeline), QLinearMatMul, MatMulInteger, ConvInteger (zero-point offset semantics).
  • Phase 2.2 (HIGH_VISION, 14 ops, 16 tests): Slice (axis 0 + step 2), Pad (default fill + custom value), Cast, Tile, Expand, Where, Resize (nearest 2× upscale), Upsample (Resize alias), ConvTranspose (stride 2 with identity kernel), GatherND, ScatterND, RoiAlign, LpNormalization (L1 unit norm), LRN (Local Response Normalization).
  • Phase 2.3 (MEDIUM_MATH, 32 ops, 47 tests): trigonometric (Tan, Asin, Acos, Atan, Sinh, Cosh, Asinh, Acosh, Atanh); rounding (Round, Sign, Floor, Ceil); detection (IsNaN, IsInf); SIMD-tensor unaries (Exp, Log, Sqrt, Neg, Abs, Reciprocal, Tanh); softsigns (Softsign, Mish); binary math (Sub, Mul, Div, Pow, Mod Python-style + fmod=1 truncation, BitShift LEFT / RIGHT); attribute-driven activations (Celu, ThresholdedRelu); variadic (Min, Max, Mean, Sum); reductions (ReduceMean, ReduceSum, ReduceMax, ReduceMin, ReduceProd, ReduceL1, ReduceL2); index ops (ArgMin, Hardmax).
  • Phase 2.4 (LOW_UTIL, 11 ops, 11 tests): comparisons (Equal, Greater, Less, LessOrEqual); logical (And, Or, Xor); shape (Squeeze with axes attribute); misc (NonZero, Compress along axis, GridSample with align_corners=1).
  • yscv-onnx: Fixed a runner bug in exec_qlinear_conv (crates/yscv-onnx/src/runner/conv.rs:312-326) discovered while writing the Phase 2.1 tests. The synthetic float Conv node always advertised "__qb" as inputs[2] regardless of whether QLinearConv was called with a bias, so the inner exec_conv looked up a non-existent tensor and failed with MissingInput { input: "__qb" }. Fixed by building the synthetic input list conditionally on bias.is_some().

Phase 1 of 1.0 roadmap (regression coverage, sealing, CI gate)

  • yscv-onnx: Regression test for the depthwise + grouped Conv → SiLU runtime fusion path. The previously-shipped bug (YOLO11n losing detections from 34 → 9) had no test; the fix lived in two arms of runner/conv.rs that this test now protects. New file crates/yscv-onnx/src/tests/fusion_silu.rs covers the depthwise (group == C_in == C_out), grouped (group == 2), and regular (group == 1) branches by building a Conv → Sigmoid → Mul graph, running it through the runtime fusion detector, and asserting against a hand-rolled NCHW reference.
  • yscv-onnx: End-to-end execution tests for the optimizer-emitted op types Conv_Relu and BatchNormalization_Relu. The existing fuse_conv_relu_merges_pair test only validated the graph rewrite; the dispatch arms at runner/mod.rs:1036-1043 had no test that ever executed them. Added fused_conv_relu_dispatch_matches_reference and fused_batchnorm_relu_dispatch_matches_reference to crates/yscv-onnx/src/tests/optimizer.rs.
  • yscv-onnx: Fixed a layout-tag bug in exec_relu_inplace that the new dispatch tests caught — the function used env.insert(...), which clears the NHWC layout flag, so a Conv_Relu / BatchNormalization_Relu node feeding a graph output would emit NHWC bytes labelled as NCHW. Switched to in-place mutation via env.get_mut(...), matching the runtime fusion path's behaviour.
  • yscv-kernels: Sealed the Backend and BackwardOps traits via a private mod sealed { pub trait Sealed {} } supertrait pattern. Downstream crates can no longer implement either trait — adding new methods after 1.0 is therefore non-breaking. The four in-crate implementors (CpuBackend, ThreadedCpuBackend, GpuBackend, MultiGpuBackend) carry explicit impl sealed::Sealed blocks; external impl Backend for X { ... } now fails with a clear the trait Sealed in module sealed is not accessible diagnostic.
  • scripts: New scripts/check-doc-counts.sh asserts 14 source-derived counters (workspace version, crate count, ONNX CPU operators, Tensor public methods, imgproc pub fn count, autograd Op variants, ModelLayer variants, loss functions, optimizers, LR schedulers, model-zoo architectures, WGSL shaders, Metal shaders, Backend trait method count) against pinned constants. When a counter changes legitimately, both the constant and the user-facing docs must be updated in the same commit.
  • CI: Added a Doc counts gate step to the quality job in .github/workflows/ci.yml, running bash scripts/check-doc-counts.sh between the workspace test and the camera UX checks.
  • yscv-onnx: Fixed six pre-existing build failures in crates/yscv-onnx/src/runner/gpu.rs that were silent on main because cargo check -p yscv-kernels --features gpu (the only gpu-feature CI job) does not exercise this file. (1) unary_f16 closure type updated from Fn(&GpuBackend, &GpuBuffer) -> GpuBuffer to Fn(&GpuBackend, &GpuBuffer) -> Result<GpuBuffer, KernelError> after relu_f16_on_device and sigmoid_f16_on_device started returning Result, with the call site adopting ?. (2) ensure_nhwc now returns Result<(), OnnxError> and propagates errors from convert_f16_to_f32_on_device / convert_f32_to_f16_on_device; all five callers (exec_conv_act, exec_bn, exec_pool, exec_gap, exec_conv_f16) updated. (3) get_reshape_shape and (4) get_small_i64_vec now return Result<Vec<_>, OnnxError> so gpu.download(...)? works; all nine callers updated. After this fix cargo check --workspace --features gpu and cargo clippy --workspace --all-targets --features gpu -- -D warnings are clean.

Documentation

  • docs: Synced root README.md, docs/*, per-crate READMEs, and CONTRIBUTING.md with workspace version 0.1.7 and the actual code state. Corrected stale numerical claims to match the current source: 14 crates (was 15 in several files), 1,698 cargo-test count after the Phase 1 additions (was 1,678 in one place, briefly 1,693 after the doc sync), 128 ONNX CPU operators (was 126 in crates/yscv-onnx/README.md), 115 Tensor ops in ops.rs (was 80+), 159 pub fn items in crates/yscv-imgproc/src/ops/ (was 100+ / 178), 17 loss functions (was 14+), 61 autograd Op variants (was 40+), 50 WGSL + 4 Metal compute shaders (was 20), 21 named SIMD functions in yscv-video (was 29 NEON + 31 SSE2), 13 model-zoo architectures, and HEVC software-decode speedup 1.4× end-to-end (per-crate yscv-video/README.md previously said 1.3×). Added missing yscv-cli and yscv-autograd rows to the root README crate table. Updated docs/api-stability.md to reflect that all crates share workspace version 0.1.7 and that apps/ binaries are not part of the 14-crate publish set.
  • docs: Added docs/roadmap-1.0.md — eight-phase roadmap from 0.1.7 to a production-ready 1.0 release, with file:line citations, effort estimates, and the explicit boundaries between 1.0 blockers and 1.1-deferred work.

Added

  • yscv-video: HEVC chroma motion compensation (4-tap filter) — full color YUV420→RGB output instead of grayscale
  • yscv-video: Streaming MP4 reader — O(1) memory (27MB RSS for 41MB file), lazy seek-based sample reading
  • yscv-video: MP4 audio track detection — extracts codec, sample_rate, channels from mp4a box
  • yscv-video: MKV/WebM EBML demuxer with frame index (no per-frame data copy)
  • yscv-video: Hardware video decode backends — VideoToolbox (macOS, working), NVDEC (parser pipeline), VA-API (init), MediaFoundation (init), all with auto SW fallback
  • yscv-video: Branchless CABAC engine — packed transition tables, CLZ batch renormalize, 32-bit buffered reader, unsafe get_unchecked on hot paths
  • yscv-video: BS=0 deblock skip — pred_mode grid eliminates ~85% of deblock work on inter-coded HEVC frames
  • yscv-video: SSE2 parity with NEON — 31 SSE2 blocks (MC filter, bipred, unipred, dequant, i16→u8, DC prediction)
  • yscv-video: HEVC weighted prediction table parser (ITU-T H.265 §7.3.6.3)
  • yscv-video: H.264 sub-MB partitions (P_8x8: 4 sub-blocks with per-block MVD)
  • yscv-video: H.264 scaling lists parsed and stored in SPS
  • yscv-video: 10-bit Main10 support (u16 DPB, NEON u16 MC filter)
  • yscv-video: --luma-only and --hw flags in bench_video_decode example
  • yscv-video: Fuzz testing — 3 targets (H.264 NAL, HEVC NAL, MKV) with seed corpus
  • yscv-video: Audio module — AudioCodec enum, AudioTrackInfo, MP4/MKV codec detection
  • yscv-detect: Bounds checks in YOLOv8/v11 decoder (guard against malformed tensor output)
  • docs: video-pipeline.md — comprehensive video decode documentation
  • .github/workflows/hw-decode.yml — CI matrix for macOS+VT, Linux, Windows

Fixed

  • yscv-video: OOM on large MP4 files — streaming reader replaces std::fs::read() whole-file load
  • yscv-video: MKV OOM — 512MB file size limit + frame index instead of per-frame data copy
  • yscv-onnx: CPU depthwise and grouped Conv paths now correctly apply fused SiLU activation
  • yscv-onnx: panic!() in Metal/GPU dispatch replaced with unreachable!() (internal invariant)
  • yscv-imgproc: Mutex poisoning — .expect("mutex poisoned") replaced with .unwrap_or_else(|e| e.into_inner())
  • yscv-video: Integer overflow in raw video frame size calculation — uses checked_mul()
  • yscv-model: Removed artificial 8GB file size limits on weight/safetensors loading
  • yscv-onnx: Removed artificial 4GB limit on ONNX model loading
  • yscv-detect: False #[allow(dead_code)] on hwc_to_nchw (function IS used behind cfg(feature))

Added (earlier)

  • examples: bench_yolo now supports BENCH_COOLDOWN env var (default 20s) to insert thermal cooldown pauses between benchmarks, preventing CPU frequency throttling on sustained runs.

[0.2.0] — 2026-03-18

Added

  • yscv-imgproc: Hand-written NEON and SSE/SSSE3 SIMD for all 12 u8 image operations (grayscale, dilate, erode, gaussian, box blur, sobel, median, canny sobel, canny NMS, resize 1ch, resize RGB H-pass, resize RGB V-pass).
  • yscv-imgproc: GCD dispatch_apply threading on macOS with rayon fallback on all platforms.
  • yscv-imgproc: Direct 3x3 gaussian blur (vextq/alignr, zero intermediate buffers).
  • yscv-imgproc: Stride-2 fast path for ~2x downscale resize.
  • yscv-track: 27 new tests for DeepSORT and ByteTrack (57 total).
  • CI: ARM64 Linux runner (ubuntu-24.04-arm).
  • CI: GPU feature compilation check (cargo check -p yscv-kernels --features gpu).
  • build: Release profile with lto = "thin", codegen-units = 1.
  • build: Target-specific CPU flags in .cargo/config.toml (apple-m1, neoverse-n1, x86-64-v3).
  • bench: OpenCV comparison benchmarks for u8 and f32 operations.
  • bench: CPU frequency warm-up for Apple Silicon benchmarks.
  • docs: Architecture guide (docs/architecture.md).
  • docs: OpenCV vs yscv comparison with full methodology in docs/performance-benchmarks.md.

Changed

  • yscv-imgproc: Grayscale u8 processes entire image as flat array (removed per-row GCD overhead).
  • yscv-imgproc: Gaussian blur uses direct 3x3 approach instead of separable tiles.
  • yscv-imgproc: Morphology uses branchless vextq/alignr inner loop.

Fixed

  • yscv-imgproc: Canny hysteresis buffer overflow on negative offset underflow.
  • yscv-imgproc: to_tensor() uses expect() instead of unwrap() with diagnostic message.
  • docs: All rustdoc unresolved link warnings fixed (29 warnings eliminated).
  • workspace: All clippy warnings fixed (cargo clippy -- -D warnings clean).

Removed

  • goals.md — replaced by docs/ecosystem-capability-matrix.md as canonical progress tracker.

Added

  • yscv-optim: LAMB optimizer with trust ratio scaling for large-batch training.
  • yscv-optim: LARS optimizer with layer-wise adaptive rate scaling.
  • yscv-optim: Lookahead meta-optimizer wrapping any StepOptimizer with slow-weight interpolation.
  • yscv-tensor: scatter_add operation for index-based additive scatter.
  • yscv-autograd: Differentiable gather and scatter_add ops with full backward support.
  • yscv-recognize: VP-Tree (vantage-point tree) for approximate nearest-neighbor search (build_index(), search_indexed()).
  • yscv-video: H.264 P-slice motion compensation (MotionVector, motion_compensate_16x16, ReferenceFrameBuffer).
  • yscv-video: H.264 B-slice bidirectional prediction (BiMotionVector, BPredMode, motion_compensate_bipred).
  • yscv-video: H.264 deblocking filter (boundary_strength, deblock_edge_luma, deblock_frame).
  • yscv-video: HEVC/H.265 decoder infrastructure (VPS/SPS/PPS parsing, CodingTreeUnit, HevcSliceType).
  • yscv-kernels: Deformable Conv2d kernel (deformable_conv2d_nhwc) with bilinear sampling.
  • yscv-model: DeformableConv2dLayer with ModelLayer::DeformableConv2d variant.
  • yscv-track: Re-identification module (ReIdExtractor trait, ColorHistogramReId, ReIdGallery).
  • yscv-kernels: GPU compute shaders for batch_norm, layer_norm, and transpose via wgpu.
  • yscv-imgproc: SURF keypoint detection and descriptor matching (detect_surf_keypoints, compute_surf_descriptors, match_surf_descriptors).
  • yscv-onnx: OnnxDtype enum (Float32/Float16/Int8/UInt8/Int32/Int64/Bool) with OnnxTensorData quantize/dequantize support.
  • yscv-model: TCP transport for distributed training (TcpTransport with coordinator/worker roles, send/recv, allreduce_sum).
  • scripts: publish.sh for dependency-ordered crate publishing.
  • scripts: bump-version.sh for workspace-wide version bumps.
  • examples: train_cnn — CNN training recipe with Conv2d + BatchNorm + pooling.
  • examples: image_pipeline — composable image preprocessing pipeline.
  • yscv-model: Pretrained model zoo with architecture builders (ResNet, VGG, MobileNetV2, EfficientNet, AlexNet) and ModelHub remote weight download with caching.
  • yscv-model: Distributed training primitives — GradientAggregator trait, AllReduceAggregator, ParameterServer, InProcessTransport, gradient compression (TopKCompressor).
  • yscv-model: High-level Trainer API with TrainerConfig, validation split, EarlyStopping, BestModelCheckpoint callbacks.
  • yscv-model: Eval/train mode toggle for layers (dropout, batch norm behavior).
  • yscv-model: Compose-based Transform pipeline (Resize, CenterCrop, Normalize, GaussianBlur, RandomHorizontalFlip, ScaleValues, PermuteDims).
  • yscv-kernels: GPU multi-device scheduling — MultiGpuBackend, device enumeration, round-robin/data-parallel/manual scheduling strategies.
  • yscv-video: H.264 baseline decoder infrastructure — SPS/PPS parsing, bitstream reader, Exp-Golomb decoding, YUV420-to-RGB8 conversion, H.265 NAL type classification.
  • yscv-tensor: Native FP16/BF16 dtype support with DType enum, typed constructors, and to_dtype() conversion.
  • yscv-model: Mixed-precision training (MixedPrecisionConfig, DynamicLossScaler, mixed_precision_train_step).
  • yscv-model: Embedding, LayerNorm, GroupNorm, InstanceNorm layers with checkpoint roundtrip.
  • yscv-model: LoRA fine-tuning, EMA, LR finder.
  • yscv-model: SafeTensors format support.
  • yscv-onnx: Quantized ONNX runtime ops (QLinearConv, QLinearMatMul, MatMulInteger, ConvInteger, DynamicQuantizeLinear).
  • yscv-onnx: Expanded opset from 90 to 123 operations.
  • yscv-video: H.264/H.265 codec infrastructure (NAL parser, MP4 box parser, VideoDecoder/VideoEncoder traits, CAVLC).
  • docs: API stability policy and release governance (docs/api-stability.md).
  • docs: Full documentation suite (ecosystem capability matrix, performance benchmarks, dataset adapters, training augmentation, training optimizers).

Changed

  • yscv-tensor: DType enum now supports F32, F16, and BF16 storage variants.
  • yscv-imgproc: SURF descriptor matching accepts exact matches (dist < 1e-9) unconditionally, bypassing ratio test.