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.
-
Breaking:
load_onnx_modelandload_onnx_model_from_filenow run the graph optimizer as part of loading. It was a separateoptimize_onnx_graphcall 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, exceptyscv-pipeline's CPU dispatcher, which was silently running unoptimized.YSCV_ONNX_OPTIMIZE_OFF=1disables it process-wide, and the newload_onnx_model_unoptimizedreturns 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_indexno longer asserts one plan action per node. TheFusedPwDwPwReducemerge deliberately drops the actions it absorbs, so a merged plan is legitimately shorter than the node list —nchwc_handoffis documented as indexed by plan position for that reason, and every action carries its ownnode_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, inplan/build.rs, before the merge runs. -
Breaking: operator attributes are keyed by the new
Attrenum instead ofString.OnnxNode.attributesis nowFxHashMap<Attr, OnnxAttribute>and theget_attr_*helpers take anAttr. 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 inAttr::Other, so decoding and re-exporting a model the runtime does not fully interpret stays lossless. -
Breaking:
optimize_onnx_graphnow returnsResult<(), 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=-namedisables an individual pass;YSCV_ONNX_PASS_LOG=1reports 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*/Multinomialfamily, which would otherwise freeze a single draw into the weights. -
fuse_conv_relu,fuse_bn_relu,fold_conv_bn,fold_constantsandrewrite_convtranspose_dtsare no longer public. They were only ever reachable throughoptimize_onnx_graphoutside the crate. -
Optimizer passes no longer rebuild the runtime index individually. Nine of the twelve passes called
rebuild_runtime_index()on exit, so a singleoptimize_onnx_graphre-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.
use_avx512_mr12documented itself as "DEFAULT ON" with aYSCV_AVX512_SGEMM=0kill switch. The gate it calls is opt-in on the exact value1, 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/Addfolding 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_nodesdeleted nodes byNodeProto.name, which ONNX makes optional. A single unnamedDropoutput 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 malformedDropoutnodes it had skipped rewiring. Now removes only the nodes it actually rewired, by index.Transpose+MatMulfusion 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 theTransposeto run anyway, running it consumed that value and the fusion then read a tensor that was gone — aTransposefeeding both aMatMuland any other op failed the inference outright. The matcher now requires every reader of the transposed value to be an absorbingMatMul, which is what its own comment always claimed.- The
PW_expand → DW → PW_reducemerge matched its residualAddpositionally while every other matcher worked by dataflow. When the two disagreed the merge absorbed the pointwise reduction without itsAdd, and the addition was dropped from the execution plan entirely — inference returned successfully with a graph output missing. It now takes over whatever theConv+Addmatcher resolved, or declines. eliminate_squeeze_unsqueeze_pairscould accept overlapping inverse pairs(i, i+1)and(i+1, i+2)from a chain such asSqueeze -> Unsqueeze -> Squeeze. The reverse-removal loop then deleted already-shifted indices, taking unrelated nodes with them. Overlapping matches are now skipped.
- The
prfm pldl1keephelper lived in three copies acrossconv/pointwise.rs, and is now a singleops::prefetch::prefetch_l1_keepdocumenting 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_shapeloops. 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.
rebuild_runtime_indexnow 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'sFusedTransposeMatMulshape, the only caller of the transposed-A tiles) and raw-slicebinary_same_shape_dispatchadd/mul.
- Optimizer passes no longer rebuild the runtime index individually. Nine of
the twelve passes called
rebuild_runtime_index()on exit, so a singleoptimize_onnx_graphre-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.
run_onnx_model_sequential, the ~600-line per-inference fusion scanner inrunner/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 theuse_counts-takingtry_reshape_nhwc_passthrough(the plan path's_innervariant, and the NHWC-passthrough optimization itself, are unaffected).
- CPU profiler (
profile_onnx_model_cpu) now reports whichyscv-kernelscompute path each Conv dispatched to. The detail table gains avia <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
vialabel also reports the kernel-internal sub-path theyscv-kernelsentry point chose, rendered asrunner/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 fromyscv-kernelsviatake_conv_path; the per-row depthwise dispatch is routed through a shareddw_row_kindselector so the recorded label can never drift from the kernel actually run. Grouped/indirect record no sub-path. MatMulnodes now report the dispatched GEMM family in theviacolumn —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 bymatmul_2d_slices_fused_maybe_packed(theMatMulop + pointwise Conv entry) and theGemm-op dispatcher tree (matmul_2d_*_with_plan), exposed fromyscv-kernelsviatake_matmul_kernel. BothMatMulandGemmprofiler nodes carry thevialabel.- The fused-path runner profiler (
YSCV_RUNNER_PROFILE) now also records the dispatchedkernellabel 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_FILTERenv narrows the profiler's detail table and JSON dump to a chosen op/name set — a comma-separated spec of op types and/orname:<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_JSONper-node records now carry Convkernel_shape,stridesand the dispatchedkernel, so the dispatch and shape detail are diffable, not just printed.scripts/gap_diff.pygains--filter(same spec asYSCV_PROFILE_FILTER) and--per-node, which diffs two runs node-by-node and flags any dispatched-kernel change asold→new— ideal for a baseline-vs-change comparison of the same model.
- yscv-imgproc:
convex_hullruns 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 singleO(n)pass. Points on an edge are kept, so float ties cannot discard a real vertex. - yscv-imgproc: binary morphology packs rows into
u64words 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 lengthLis built by recursive doubling inO(log L)passes. Newopen_binary_box/close_binary_boxkeep 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.
--keep-fp32hybrid knob for QDQ quantization: keep selected layers in fp32 while the rest of the graph is quantized.
- MPSGraph backend now folds the load-time
Conv_Relu/BatchNormalization_Relufusions (build the conv/bn graph, then apply the activation), matching the CPU and wgpu backends. Previously MPSGraph aborted withunsupported op 'Conv_Relu'on any Conv+ReLU graph, so the Siamese tracker could not run on Metal.
- Bumped workspace version to
0.1.10and aligned user-facing documentation andscripts/check-doc-counts.shwith the release. - Internal refactor: split the largest perf-arc source files (
matmul,conv,fused_pw_dw_3x3,backend, tensorops, the ONNXrunner, andloader) 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.
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.
- Bumped workspace version to
0.1.9and aligned user-facing documentation andscripts/check-doc-counts.shwith the release.
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.frameworkon 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.
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-pipelinecrate — TOML-driven dispatch across CPU / RKNN / RKNN-matmul / MPSGraph / wgpu;AcceleratorDispatchertrait; per-taskrecover_all(),spawn_watchdog(), ONNX→RKNN compile at startup, multi-input graphs, shape-validation on submit.yscv-video-mppcrate —dlopen-based Rockchip MPP hardware encoder (H.264 / H.265), no link-time vendor dependency.RknnPipelinedPool— multi-slot NPU pool withsubmit/wait, consecutive-failure auto-recovery,reload(&new_model_bytes)hot swap.RknnMatmulgets its ownalloc_mem+ dispatcher variantAccelerator::RknnMatmul { m, k, n, dtype }.- MPSGraph pipelined —
submit_mpsgraph_plan/wait_mpsgraph_plantriple-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_governorapplies SCHED_FIFO + CPU affinity +mlockall+ cpufreq governor with graceful fallback whenCAP_SYS_NICE/CAP_SYS_ADMINare missing. - Docs overhaul —
QUICKSTART.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.mdcatalogue. - CI hardening —
scripts/check-safety-comments.shpaths updated after themetal/reorg,scripts/check-doc-counts.shexpectations bumped to 16 crates,apps/benchgetsdefault-run = "yscv-bench"so the benchmark step no longer errors on binary ambiguity. - Numerical tolerances —
matmul_associative_with_scalarproptest +conv3dtests 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.
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::RknnMatmulis no longer a unit variant. It carries{ m, k, n, dtype }directly: matmul contexts are shape-bound at construction (the SDK pre-allocatesM×K,K×N,M×Nbuffers), so dimensions belong in the TOML, not in a model file.MatmulDtypeTOML enum maps toRknnMatmulType. First-cut:Fp16MmFp16ToFp32,Fp16MmFp16ToFp16,Int8MmInt8ToInt32,Fp16MmInt4ToFp16(the LLM-dequant tile). Per-channel quant params for INT4/INT8 still go throughyscv_kernels::RknnMatmul::set_quant_paramsdirectly — TOML wire-up is a follow-up if/when there's demand.RknnMatmulDispatcher: pre-allocates A/B/CRknnMemagainst the matmul context (newRknnMatmul::alloc_mem/alloc_mem_exmirroringRknnBackend), 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"`.
- run + sync_from_device + readback
validate_modelsskips 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 = []
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.rknnmodel in-flight. Walks every slot, takes the ctx + mem write locks, callsreset_with_flags(new_bytes, RKNN_FLAG_ASYNC_MASK), re-allocates + re-binds input/outputRknnMem. In-flight handles invalidate (theirwaiterrors).model_datais nowRwLock<Arc<Vec<u8>>>so concurrentrecover_failedmid-reload uses the fresh bytes.yscv_video::realtime::set_cpu_governor(governor): writesgovernor(typically"performance") to every/sys/devices/system/cpu/cpu*/cpufreq/scaling_governorit 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 ofapply_rt_configthat also writes the cpufreq governor.RtAppliedStategainscpu_governor_coresfield.- 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).
Follow-up to the "end-to-end integration" pass below: closes the last four "out of scope" items from that round.
MetalMpsdispatcher: config-driven path for Apple Silicon now wrapsMpsGraphPlanbehind aMutex(guaranteeingSync) + an explicitunsafe impl Send for MetalDispatcher. Audit recorded in the code: allMPSGraph-held ObjC pointers have atomic ref-counting, and every call isautoreleasepool-scoped — safe to move the dispatcher across threads under its Mutex. Firstdispatchlazily compiles the plan;recover()drops it so the nextdispatchrecompiles (useful after a GPU reset).Gpudispatcher: newGpuDispatcherwrapsyscv_onnx::run_onnx_model_gpu(wgpu cross-platform). Same byte contract as CPU / RKNN / Metal dispatchers.MpsGraph/MpsGraphExecutable/MpsGraphTensorRefmarkedunsafe impl Sendinyscv-kernels, with documented safety contract (atomic ObjC refcounting + autoreleasepool-scoped calls).MpsGraphTensorRefadditionally claimsSync— it's an SSA node handle, immutable after graph compile.PipelineHandle::spawn_watchdog(stats, interval) -> Watchdog(featurerealtime): spawns a background thread that pollsPipelineStats5::watchdog_alarm, invokesrecover_all()on observation, and clears per-stage alarms so the next overrun is visible. ReturnedWatchdogjoins the thread on drop. Couples the previously-independentStageWatchdogtracking and pipeline recovery into a single supervisor.- ONNX → RKNN auto-compile at startup: RKNN tasks can now point
model_pathat an.onnxfile.RknnDispatcher::newdetects the extension, compiles viayscv_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_modelsalso 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.
Closes every tech-lead review item for the ARM/Rockchip inference stack; the TOML config schema now has a real runtime behind it.
RknnPipelinedPoolrecovery: pool now tracksfail_streakper slot and auto-recovers onTIMEOUT/CTX_INVALID/DEVICE_UNAVAILABLE/DEVICE_UNMATCHafterRECOVERY_THRESHOLDconsecutive faults. Recovery rebuilds the NPU context + re-binds all input/outputRknnMemunder write-locks on the slot — a pendingAsyncFrameon that slot is discarded. Caller-bug errors (INPUT_INVALID,PARAM_INVALID, etc.) bubble up unchanged. Manual recovery API exposed asrecover_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_onmulti-input: signature changed from&[u8]to&[(&str, &[u8])]— matchesRknnPipelinedPool::submitand supports Siamese / two-tower models that the old single-input path couldn't drive.RknnBackend::reset_with_flags: new — preservesRKNN_FLAG_ASYNC_MASK(or any init flag) across context recovery. The existingreset()wraps this withflags = 0and retains its previous semantics.is_recoverable_rknn_errorhelper exposedpub(crate)for the pooled + single-context recovery paths.
LatencyHistogram(new module): lock-free 2048-sample ring withrecord(us) → O(1)on the hot path andsnapshot() → { min, p50, p90, p95, p99, max, count }for periodic reports. Powers every new latency metric below.PipelineStats5now carries oneLatencyHistogramper stage (capture_latency,infer_latency,encode_latency,display_latency,e2e_latency).PipelineStats5Snapshotexposes those asLatencyQuantiles— the scalaravg_e2e_latency_usis replaced by the richere2edistribution.PipelineStatsgainscapture_latency/process_latency/output_latencyhistograms and alatency_snapshot()helper.PipelineStatsno longer derivesCopy; callers who relied on that must clone via the per-fieldsnapshot()helpers.FramePayload(newframe_commonmodule):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.
validate_modelsis now a real check: magic-byte verification for.rknn(RKNN/RKNFmagic) and.onnx(first-byte0x08protobuf tag). Corrupted-but-stat-able files now fail at config time, not after real-time threads have started. NewConfigError::ModelInvalidvariant carries the reason. Optionalrknn-validatefeature adds a fullRknnBackend::loadcheck on hosts wherelibrknnrt.sois loadable.AcceleratorDispatchertrait moved out ofschedulerinto a newdispatchmodule with concrete implementations:CpuDispatcher(ONNX viayscv-onnxCPU runner) andRknnDispatcher(viaRknnPipelinedPoolsingle-slot) under--features rknn.MetalMpsandGpuare deferred with clear error messages — the low-levelcompile_mpsgraph_planpath remains available for sub-ms latency callers. Factorydispatcher_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 perTensorBinding.source("camera"or"<task>.<output>"), returnsHashMap<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). Optionalrealtimefeature wiresyscv_video::realtime::apply_rt_configso SCHED_FIFO + CPU affinity +mlockallare applied when[realtime] sched_fifo = truein the TOML. Graceful fallback on hosts withoutCAP_SYS_NICE.NpuCoreSpec::to_mask(): converts the TOML-facing spec into the kernel-levelNpuCoreMask(gated--features rknn).
End-to-end reference that wires all of the above:
- Parses a TOML with
PipelineConfig::from_toml_path. - Builds a runtime handle via
run_pipeline(cfg)— validates every model, constructs dispatchers, applies real-time config if the feature is on. - Synthesises an RGB gradient-box frame (drop-in replacement for a
real
V4l2Camera::export_dmabuf+FramePayload::dma_buf). - Hot loop calls
handle.dispatch_frame(&[("images", bytes)]), collects per-frame latency into aLatencyHistogram, reportsp50/p95/p99every second. - Recovery on repeated failures via
handle.recover_all(); exits cleanly afterMAX_CONSECUTIVE_FAILURESto avoid log spam on misconfigured models.
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.
RknnPipelinedPool+RknnInferenceHandle— multi-core pipelined RKNN dispatch mirroring the MPSGraph pipelined path on Apple Silicon. One slot perNpuCoreMask; each slot loads its ownRknnBackendwithRKNN_FLAG_ASYNC_MASK, pins to the core, and pre-allocates + pre-binds anRknnMemfor every graph input and output viabind_input_by_name/bind_output_by_name. Slot count followscores.len()(typical RK3588 pool:[Core0, Core1, Core2]).submit(&[(&str, &[u8])]) -> RknnInferenceHandle— round-robin slot pick, back-pressure wait on any pendingAsyncFrame, memcpy fresh inputs into pre-boundRknnMem,run_async_bound. Non-blocking return.wait(handle) -> Vec<Tensor>— completion wait + dequantized output collection viaRknnBackend::wait.run(&feeds)— sync convenience (submit + wait back-to-back).- Mutex-protected slot state +
AtomicUsizering cursor → the pool isSyncand safe to share across capture / post-process threads. RknnInferenceHandleis#[must_use]; drop-without-wait is safe (nextsubmitto that slot just back-pressure-waits), the attribute exists to catch accidental discards at compile time.AsyncFramenow 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 models:
compile_mpsgraph_plannow takes&[(&str, &Tensor)]andrun_mpsgraph_plantakes&[(&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;InferenceHandleis#[must_use]. Slot count set viaYSCV_MPS_PIPELINEenv (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:
PreparedInputscaches retainedMPSGraphTensorData+NSArrayobjects for both inputs and outputs at compile time.resultsArraylets MPSGraph write directly into our pre-allocatedStorageModeSharedbuffers — noreadBytescopy, no per-callnew_buffer, no per-call ObjC allocations. Commit goes throughMPSCommandBufferwrapper to ensure MPS-internal staging is flushed. - f16 end-to-end: the final
cast_to_f32is removed from the GPU graph; outputs stay in f16 in shared memory and are widened on the CPU via aarch64vcvt_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_valuesfor compile-time shape-op consumers),Exp(exponentWithTensor:). - Correctness fix:
TensorEnv::aliasnow materializes initializers into their slot when the target lives only ininitializers, soIdentity-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).
- 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_acceleratorsprobesrknn_available/etc. and fails loud if the TOML requests an unsupported accelerator.validate_modelschecks model files exist. Cycle detection in the task DAG (ConfigError::CyclicDependency). TaskScheduler: topological sort over task dependencies;ready_tasks(completed)API for parallel execution.
- Hardware H.264 encoder via
librockchip_mpp.soloaded at runtime. MppH264Encoder::new(cfg)+encode_nv12_dmabuf(fd, len)— zero-copy input from DMA-BUF (chains withRknnBackend::wrap_mb_blkfor 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)]MppApivtable struct with compile-time size assertion matchingrk_mpi.h(168 bytes on LP64).
rga.rs— Rockchip RGA 2D blitter (librga.sodlopen).RgaBlenderwithblit/copy_atfor 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 viadrmPrimeFDToHandle, initialdrmModeSetCrtc+ subsequent vsync'ddrmModePageFlip. Mode selection by label ("720p60","1920x1080", etc.).realtime.rs— SCHED_FIFO +mlockall+pthread_setaffinity_nphelpers.apply_rt_config(prio, cpus, lock_mem) -> RtAppliedStatewith graceful fallback on missingCAP_SYS_NICE/RLIMIT_MEMLOCK.frame_pipeline_5stage.rs— 5-stage lock-free SPSC ring pipeline (capture → infer → encode → output, 3 frames in-flight). Per-stageStageWatchdogwith budget + overrun streak. Panic-safe viacatch_unwindon each stage.PipelineStats5::snapshotwith p50/p95/p99-ready latency counters.
- 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.
- New
docs/pipeline-config.md— TOML schema reference, accelerator values, source/sink syntax, validation order, RT tuning guidelines.
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).
- Split monolithic
rknn_backend.rs(3172 LOC) intorknn/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 throughrknn/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 viadlopen+resolve_optional; absent symbols return descriptiveKernelError::Rknn. - Compile-time ABI assertions for 16 SDK structs (
RknnTensorAttr=376B,RknnMatmulInfo=64B,RknnInitExtend=136B, …). Drift in SDK headers now failscargo buildinstead of producing UB at runtime. Sizes verified against C-equivalent compilation on aarch64 LP64. - Custom-op Rust callbacks: new
CustomOpHandlertrait + 16-slot trampoline dispatcher inrknn/custom_op.rs. Users can implementinit/prepare/compute/destroyin safe Rust; the framework generates 16×4extern "C"trampolines per slot viamacro_rules!. RAII slot release onCustomOpRegistration::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=-1…TARGET_PLATFORM_UNMATCH=-13) +rknn_error_name()helper. AllKernelError::Rknnmessages now read"rknn_init failed: PARAM_INVALID (-5)"instead of"failed with code -5".
- Breaking change in
RknnCompileConfig: removedtarget_platform,quantization,mean_values,std_valuesfields — the on-devicelibrknn_api.soexposes no SDK function to set them. They were silently ignored before. The struct is nowRknnCompileConfig { dataset_path: Option<PathBuf> }. For full configuration use the offline Python rknn-toolkit2 on the host. - Calibration
dataset_pathnow actually passed torknn_build; previously the int8 quantization argument was alwaysnull. Path validated for existence and non-empty before SDK call. - Replaced PID-based
/tmp/yscv_rknn_{pid}.rknn(race-prone, hardcoded path) withtempfile::NamedTempFile. Safe under concurrent invocation; works on Linux/Android where/tmpmay differ.
- 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.
- Expanded
docs/edge-deployment.mdwith 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.mdwith full RKNN API reference, SDK coverage stats, module layout, quick-start, all four FFI safety categories.
- Parallel tile decode —
hevc_parallel::decode_tiles_parallel()dispatches independent tile regions viaentry_point_offset_minus1[]parsing and per-tile CABAC re-init. Tile-aware CTU walking produces raster-order SAO lists for the finaliser. - Parallel WPP decode —
hevc_parallel::decode_wpp_parallel()with per-row CABAC context inheritance viaOnceLockbarriers. Zero-alloc snapshots viasnapshot_contexts_into. - Weighted prediction applied — HEVC PPS
weighted_pred_flagand per-slice weight tables are now parsed and applied during inter-prediction reconstruction (previously parsed but not used). - Signature refactor —
decode_coding_tree_cabacand parallel entry points use a unifiedHevcSliceCabacStateparameter bundle instead of raw tuple arguments.
- Replaced
UnsupportedOperationstubs inBackwardOpstrait defaults with real BPTT CPU implementations forrnn_backward(tanh activation),lstm_backward(forget/input/output/cell gates), andgru_backward(reset/update gates). GPU backends still override with per-timestep WGSL shaders.
- 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.mdwith v2.0 plans (CUDA, Flash Attention, GGUF, WebGPU, etc.). - Test badge and prose counts updated to 1,755.
- 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_flagsfrom the frame header.show_existing_frameretrieves 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 output —
ref_frame_to_decodednow performs BT.601 YCbCr→RGB conversion with chroma planes (was Y→R=G=B monochrome).
quantize_weights_int4()incrates/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 incrates/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) incrates/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 Attention —
GroupQueryAttentionONNX operator dispatch incrates/yscv-onnx/src/runner/linear.rsfor efficient multi-head attention with key/value head sharing (GQA).
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)
- Removed
decode_coding_treeDC=128 stub inhevc_decoder.rs— the old fallback function filled frames with gray pixels instead of real HEVC decode. Replaced with early-returnOk(None)for malformed payloads. Removed 2 stub unit tests and thecore.rsre-export. - Wired GPU
gather_on_device()+attention_on_device()ingpu_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)]fromgather,attention,im2col,bias_addpipeline fields. - Wired
pointwise_conv_on_device()for 1×1 conv fast path — the GPU ONNX runner now skips im2col arithmetic forkh == 1 && kw == 1convolutions (common in YOLO pointwise layers). - Wired
gc_insert()in ONNX GPU runner — replaced rawgc.insert()calls with the buffer-recycling variant to prevent GPU memory leaks. - Wired imgproc SIMD box blur —
box_h_u8_simd(),box_v_u16_simd(),border_blur()connected tobox_blur_3x3_u8for NEON+SSE2 acceleration. - Wired FAST corner helpers —
max_consecutive()andhas_consecutive()connected to FAST9 scoring and feature detection. - Added
gaussian_noise()augmentation — wired thenormal()RNG into a new publicgaussian_noise(image, sigma, seed)function (160 imgproc ops, up from 159). - Performance: matmul Vec hoist — moved
ic_blocksallocation out of thefor jc / for pcnested loop inblocked_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 — replacedbest.unwrap().1withbest.map_or(true, |(_, bc)| cap < bc). - Debug-mode GPU fallback logging —
backward/linalg.rsnow 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) andfeatures.rs(20 blocks) with file-level safety contracts. - Documented
PendingFieldin 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 ✓.
#![doc = include_str!("../README.md")]added to all 14 cratelib.rsfiles — docs.rs landing pages now render the full crate README with examples, feature flags, and architecture overview.- CI matrix expansion:
--features metal-backendcompile + test on macOS,--features native-cameracompile smoke on all platforms,--features nvdecon self-hosted Linux+NVIDIA runner. docs/migration-0.x-to-1.0.md— lists every breaking API change (sealed traits,#[non_exhaustive],Graph.backendtype change, new error variants, GPUResultsignatures) 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.shenhanced — now validates CHANGELOG.md entry exists, updates all crate Cargo.toml files (both workspace-inherited and explicit), and runscargo metadataverification 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.
#[non_exhaustive]onRecordedOpenum andGpuBufferstruct incrates/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.StepOptimizersealed incrates/yscv-optim/src/lookahead.rs—mod sealed { pub trait Sealed {} }supertrait pattern; all 8 optimizers (Sgd, Adam, AdamW, RmsProp, Adagrad, RAdam, Lamb, Lars) carry explicitimpl sealed::Sealedblocks. Externalimpl StepOptimizer for Xrejected at compile time.[package.metadata.docs.rs]added to all 14 crate Cargo.toml files — enablesall-features = true(or specific feature lists foryscv-videoandyscv-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.
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 readbackpipeline. Added FFI declarations forvaBeginPicture,vaCreateBuffer,vaRenderPicture,vaEndPicture,vaSyncSurface,vaDeriveImage,vaMapBuffer,vaUnmapBuffer,vaDestroyImage,vaDestroyBuffer. AddedVAImage,VAImageFormatstructs 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 readbackCOM pipeline. Added COM vtable dispatch helpers forIMFMediaBuffer,IMFSample, andIMFTransforminterfaces (manual vtable offset calls matching Windows SDK vtable layout). - NVDEC cleanup: added
last_error: Option<String>toNvdecStatefor callback→decode error propagation.decode_picture_callbacknow sets the error flag oncuvidDecodePicturefailure.decode()checks the flag aftercuvidParseVideoData. - 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).
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
BackwardOpstrait 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(existingbackward_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:BackwardOpsstubs returnUnsupportedOperation, CPU BPTT runs as fallback; per-timestep WGSL shaders written and ready. KernelError::UnsupportedOperationvariant added for graceful fallback when a backend doesn't implement a specific backward op.AutogradError::BackendErrorvariant 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 --release1,810 / 1,810 ✓,bash scripts/check-doc-counts.sh14/14 OK,bash scripts/check-safety-comments.sh183/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).
-
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_flagfield toHevcSpsand the methodssub_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 inh264_yuv.rs. finalize_hevc_frame_with_chromanow 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).
- Added
-
4c.2 — Tile metadata in HevcPps (crates/yscv-video/src/hevc_decoder.rs):
- Extended
HevcPpswithnum_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.
- Extended
-
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_contextsrejects 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) andwpp_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_picturewalks tiles in raster order (pps_tile_rectsproduces a single rect for the no-tiles case). At every tile boundary beyond the first the CABAC state re-initialises viareinit_at_byteper 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_enabledis 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_modificationparser + L0/L1 reorder (crates/yscv-video/src/hevc_decoder.rs): the slice header parser readsref_pic_list_modification_flag_l0/_l1and the per-entrylist_entry_l0[i]/list_entry_l1[i]indices when the PPS setslists_modification_present_flag. The L0/L1 list build indecode_pictureapplies 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 4breturn Noneearly-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 reachlong_term_ref_pics_present_flagand 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 capturessps_temporal_mvp_enabled_flagandstrong_intra_smoothing_enabled_flag(previously hard-coded false). - Slice header parser walks
num_long_term_sps/num_long_term_picsand resolves each LTRP entry's full POC via the SPS table or the inlinepoc_lsb_lt[i]+delta_poc_msb_cycle_lt[i]syntax. - The L0/L1 list builder appends
RefPicSetLtCurr(LT entries flaggedused_by_curr_pic) to both temp lists per ITU-T H.265 §8.3.2.
- SPS parser now walks past
-
4c.9 —
separate_colour_plane_flagper-plane slice dispatch (crates/yscv-video/src/hevc_decoder.rs): when the SPS setsseparate_colour_plane_flag = 1, each slice carries acolour_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_leveltoparse_profile_tier_levelreturninggeneral_profile_idc, plus aprofile_kind_from_idcmapper to a newHevcProfileKindenum (Main,Main10,MainStillPicture,FormatRangeExtensions,HighThroughput,Main10StillPicture,Other). - New
HevcSps::profile_kindfield captured at SPS parse time. Used by the test suite to assert correct profile detection on the newmain422_10,main444_10,main422_12,main_scpRext fixtures. - Extended
HevcSpswith 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) andHevcPpswith 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.
- Promoted
-
4c.12 — Chroma deblock (crates/yscv-video/src/hevc_filter.rs): switched
finalize_hevc_frame_with_chromato callhevc_deblock_frameinstead ofhevc_deblock_luma_onlyfor 4:2:0 chroma content. The chroma deblock kernel (4-tap chroma filter at every CU edge) was already present inhevc_deblock_frame_implbut 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
SaoParamswith 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 whenslice_sao_chroma_flagis set. - New
hevc_apply_sao_chroma()mirrorshevc_apply_saofor the Cb/Cr offsets. finalize_hevc_frame_with_chromaapplies chroma SAO to the local mutable copies of cb/cr planes alongside the chroma deblock pass.
- Extended
-
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_offsetsparsing: extendedparse_hevc_slice_header_fullto walk pastpred_weight_tablethrough 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 reachnum_entry_point_offsetsand the per-substreamentry_point_offset_minus1[i]array per ITU-T H.265 §7.3.6.1. Newread_entry_points_after_weight_tablehelper. Newpps_slice_chroma_qp_offsets_presentfield onHevcPps(was previously read and discarded). Newentry_point_offsets: Vec<u32>field onHevcSliceHeader. -
4c-extra.2 — Entry-point seek in sequential tile / WPP loops: the tile and WPP CTU walks in
decode_picturenow compute the absolute byte offset of each substream from the cumulativeentry_point_offsetsand pass it tocabac_state.reinit_at_byte(tiles) /wpp_inherit_at_byte(WPP) instead of relying oncurrent_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_flagfrom the bitstream whenpps.dependent_slice_segments_enabledis set (was previously hard-coded false). New fields onHevcSliceHeader:is_dependent_slice_segment: bool,slice_segment_address: u32. New decoder-state fieldsparent_slice_header: Option<HevcSliceHeader>andparent_cabac_snapshot: Option<Vec<ContextModel>>onHevcDecoder, cleared at every IDR. Indecode_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.
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
falsevalues (libx265 default Rext output never enables any of them — confirmed againstmain422_10,main444_10,main422_12,main_scpfixtures). Wiring these flags as gates would be cargo-cult: the existing decoder has no strong-intra-smoothing pass forintra_smoothing_disabledto gate, the dequant already usesi32soextended_precision_processinghas nothing to extend, the transform code has no transform-skip block for thetransform_skip_*flags to gate, the residual coder has no Rice-adaptive variant forpersistent_rice_adaptationto 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) refactoringdecode_coding_tree_cabacto take pointer-and-length recon parameters instead of&mut Vec<i16>(so the borrow checker accepts disjoint per-tile views viaSendMutPtrwrappers — the same patternimgproc/ops/u8_features.rsuses), 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.
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:
- 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. - Motion compensation was indexing the DPB with
inter_mv.ref_idx[0] as i32interpreted as a POC. This is wrong per ITU-T H.265 §8.5.3.2 —ref_idx_l0is 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_setin 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 aHevcShortTermRefPicSetcarryingdelta_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
HevcWeightTablecarried only luma weights; Phase 4b addschroma_l0/chroma_l1: Vec<HevcChromaWeightEntry>(one Cb+Cr pair per reference) and rewritesparse_hevc_weight_tableto takechroma_array_type: u8and walk the §7.3.6.3 chroma syntax whenchroma_array_type != 0. Default values follow §7.4.7.3 — whenchroma_weight_l*_flagis unset for some reference, the weight defaults to1 << chroma_log2_denomand offset to0. - HEVC slice header full parser (
parse_hevc_slice_header_fullin 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 includingpred_weight_table(). Handlesfirst_slice_in_pic, IRAP-onlyno_output_of_prior_pics_flag,pps_id,slice_segment_address(with correctCeil(Log2(PicSizeInCtbsY))bit width),num_extra_slice_header_bits,slice_type, optionalpic_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, thenum_ref_idx_active_override_flagpath that overridesnum_ref_idx_l0/l1_active,mvd_l1_zero_flag,cabac_init_flag, and finally the chroma-awarepred_weight_table(). ReturnsOption<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, andlists_modification_present_flag. The first two are now captured byparse_hevc_pps(the existing parser was reading the bits and discarding them aslet _weighted_pred = …); the third is parsed downstream as part of the PPS extension and currently defaults tofalse(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>, andst_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 fromslice_pic_order_cnt_lsbplus the newprev_tid0_poc: i32decoder tracker, using the spec's MSB derivation. IDR/BLA pictures reset POC to 0 and clearprev_tid0_poc. The DPB add and theself.poctracker 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 intoRefPicSetStCurrBefore(negative-delta entries markedused_by_curr_pic_s0) andRefPicSetStCurrAfter(positive-delta entries markedused_by_curr_pic_s1). L0 =StCurrBefore || StCurrAfter, L1 =StCurrAfter || StCurrBefore, both truncated tonum_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. HevcInterContextbundle in crates/yscv-video/src/hevc_inter.rs:122-187: new struct that carriesdpb,ref_pic_list_0,ref_pic_list_1, and the slice'sweight_tablethrough the CTU decoder. Includesref_pic_l0(ref_idx)/ref_pic_l1(ref_idx)accessors that resolve a list index to aHevcReferencePicturevia the DPB. The CTU decoder signaturedecode_coding_tree_cabacnow takes&HevcInterContextinstead of&HevcDpb; the historical "wrong"dpb.get_by_poc(inter_mv.ref_idx[0] as i32)call site is replaced with the correctinter.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_weightedandhevc_bipred_average_weightedin crates/yscv-video/src/hevc_inter.rs:1037-1118: scalar implementations of the luma weighted-prediction formulas. The unweightedhevc_unipred_clip/hevc_bipred_averagekeep their existing SIMD fast paths intact since weighted prediction is a minority of frames in real content.- The CTU MC dispatch threads
weight_tableand per-reference luma/chroma(weight, offset)lookups through every branch (unipred L0 / unipred L1 / bipred), gated onpps.weighted_pred_flag(P slices) orpps.weighted_bipred_flag(B slices). Whenweight_table.is_none()or the gating PPS flag is off, the existing fast path runs. - A
apply_chroma_weighthelper 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_panicin crates/yscv-video/tests/hevc_integration.rs:93-122): runs the existingmain_pb_weighted_320x240.mp4fixture (libx265 withweightp=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 newHevcInterContext-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 --release1,786 passed / 0 failed,bash scripts/check-doc-counts.sh14/14 OK,bash scripts/check-safety-comments.sh183/183 OK.
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 constructDecodedFrame(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 report8; HEVC reportssps.bit_depth_luma. Existing consumers that only readrgb8_dataare 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 fromyscv-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(wherechroma_max = (1 << bit_depth_chroma) - 1) instead of the previous unconditionalclamp(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 sameclamp(0, 255)bug, which would have leaked into motion compensation for subsequent frames. Both clamps now usechroma_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 fullMp4VideoReader::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 --release1,785 passed / 0 failed,bash scripts/check-doc-counts.sh14/14 OK,bash scripts/check-safety-comments.sh183/183 OK.
- 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(), thevaInitialize/vaCreateConfigblock, the NVDECcuvidParseVideoDatapath, and all fourDropimpls. - 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 NEONldr/st3/fcvtnbounds), the twomps_gemm_f16autoreleasepool blocks, plus a module-level SAFETY contract at the top ofpub mod mpsgraphthat the 32msg_send!blocks reference. Covers Objective-C class lookup, factory-method autorelease, NSArray/NSData lifetimes, tensor-handle ownership,msg_send!selector typing, andDropordering.
- 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
- SHOULD-FIX files (Phase 3.2) — file-level SAFETY contracts plus per-block references:
- crates/yscv-imgproc/src/ops/u8_features.rs: 40 blocks. File-level contract covers (A) slice reconstruction across rayon parallel-fors via
SendConstPtr/SendMutPtr, (B) NEON/SSE/AVX intrinsic feature gating, and (C) internalunsafe fnhelper invocation. - crates/yscv-imgproc/src/ops/color.rs: 31 blocks. Same A/B contract.
- crates/yscv-imgproc/src/ops/u8_filters.rs: 24 blocks. Same A/B/C contract.
- crates/yscv-imgproc/src/ops/f32_ops.rs: 18 blocks. Same A/B/C contract, adapted for f32 alignment guarantees.
- crates/yscv-onnx/src/runner/metal/run.rs: 10 blocks. File-level contract covers
buf.contents()host-pointer stability on Apple SiliconStorageModeSharedbuffers, thecount * sizeof::<u16>() ≤ buf.length()guard checked at every call site, and the no-concurrent-GPU-write invariant established bywait_until_completed.
- crates/yscv-imgproc/src/ops/u8_features.rs: 40 blocks. File-level contract covers (A) slice reconstruction across rayon parallel-fors via
- 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 thequalityjob in .github/workflows/ci.yml right after theDoc counts gate. Adversarial check verified: removing all 24 SAFETY refs fromu8_filters.rsmakes the gate exit non-zero with a precise file:line list.
- 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.rsfile 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,ModPython-style +fmod=1truncation,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 (Squeezewithaxesattribute); misc (NonZero,Compressalong axis,GridSamplewith 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 floatConvnode always advertised"__qb"asinputs[2]regardless of whether QLinearConv was called with a bias, so the innerexec_convlooked up a non-existent tensor and failed withMissingInput { input: "__qb" }. Fixed by building the synthetic input list conditionally onbias.is_some().
- 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.rsthat this test now protects. New filecrates/yscv-onnx/src/tests/fusion_silu.rscovers the depthwise (group == C_in == C_out), grouped (group == 2), and regular (group == 1) branches by building aConv → Sigmoid → Mulgraph, 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_ReluandBatchNormalization_Relu. The existingfuse_conv_relu_merges_pairtest only validated the graph rewrite; the dispatch arms atrunner/mod.rs:1036-1043had no test that ever executed them. Addedfused_conv_relu_dispatch_matches_referenceandfused_batchnorm_relu_dispatch_matches_referencetocrates/yscv-onnx/src/tests/optimizer.rs. - yscv-onnx: Fixed a layout-tag bug in
exec_relu_inplacethat the new dispatch tests caught — the function usedenv.insert(...), which clears the NHWC layout flag, so aConv_Relu/BatchNormalization_Relunode feeding a graph output would emit NHWC bytes labelled as NCHW. Switched to in-place mutation viaenv.get_mut(...), matching the runtime fusion path's behaviour. - yscv-kernels: Sealed the
BackendandBackwardOpstraits via a privatemod 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 explicitimpl sealed::Sealedblocks; externalimpl Backend for X { ... }now fails with a clearthe trait Sealed in module sealed is not accessiblediagnostic. - scripts: New
scripts/check-doc-counts.shasserts 14 source-derived counters (workspace version, crate count, ONNX CPU operators,Tensorpublic methods, imgprocpub fncount, autogradOpvariants,ModelLayervariants, loss functions, optimizers, LR schedulers, model-zoo architectures, WGSL shaders, Metal shaders,Backendtrait 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 gatestep to thequalityjob in.github/workflows/ci.yml, runningbash scripts/check-doc-counts.shbetween the workspace test and the camera UX checks. - yscv-onnx: Fixed six pre-existing build failures in
crates/yscv-onnx/src/runner/gpu.rsthat were silent onmainbecausecargo check -p yscv-kernels --features gpu(the onlygpu-feature CI job) does not exercise this file. (1)unary_f16closure type updated fromFn(&GpuBackend, &GpuBuffer) -> GpuBuffertoFn(&GpuBackend, &GpuBuffer) -> Result<GpuBuffer, KernelError>afterrelu_f16_on_deviceandsigmoid_f16_on_devicestarted returningResult, with the call site adopting?. (2)ensure_nhwcnow returnsResult<(), OnnxError>and propagates errors fromconvert_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_shapeand (4)get_small_i64_vecnow returnResult<Vec<_>, OnnxError>sogpu.download(...)?works; all nine callers updated. After this fixcargo check --workspace --features gpuandcargo clippy --workspace --all-targets --features gpu -- -D warningsare clean.
- docs: Synced root
README.md,docs/*, per-crate READMEs, andCONTRIBUTING.mdwith workspace version0.1.7and 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 incrates/yscv-onnx/README.md), 115Tensorops inops.rs(was 80+), 159pub fnitems incrates/yscv-imgproc/src/ops/(was 100+ / 178), 17 loss functions (was 14+), 61 autogradOpvariants (was 40+), 50 WGSL + 4 Metal compute shaders (was 20), 21 named SIMD functions inyscv-video(was 29 NEON + 31 SSE2), 13 model-zoo architectures, and HEVC software-decode speedup1.4×end-to-end (per-crateyscv-video/README.mdpreviously said1.3×). Added missingyscv-cliandyscv-autogradrows to the root README crate table. Updateddocs/api-stability.mdto reflect that all crates share workspace version0.1.7and thatapps/binaries are not part of the 14-crate publish set. - docs: Added
docs/roadmap-1.0.md— eight-phase roadmap from0.1.7to 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.
- 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-onlyand--hwflags 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
- 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 withunreachable!()(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)]onhwc_to_nchw(function IS used behind cfg(feature))
- examples:
bench_yolonow supportsBENCH_COOLDOWNenv var (default 20s) to insert thermal cooldown pauses between benchmarks, preventing CPU frequency throttling on sustained runs.
- 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_applythreading 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.
- 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.
- yscv-imgproc: Canny hysteresis buffer overflow on negative offset underflow.
- yscv-imgproc:
to_tensor()usesexpect()instead ofunwrap()with diagnostic message. - docs: All rustdoc unresolved link warnings fixed (29 warnings eliminated).
- workspace: All clippy warnings fixed (
cargo clippy -- -D warningsclean).
goals.md— replaced bydocs/ecosystem-capability-matrix.mdas canonical progress tracker.
- 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
StepOptimizerwith slow-weight interpolation. - yscv-tensor:
scatter_addoperation for index-based additive scatter. - yscv-autograd: Differentiable
gatherandscatter_addops 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:
DeformableConv2dLayerwithModelLayer::DeformableConv2dvariant. - yscv-track: Re-identification module (
ReIdExtractortrait,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:
OnnxDtypeenum (Float32/Float16/Int8/UInt8/Int32/Int64/Bool) withOnnxTensorDataquantize/dequantize support. - yscv-model: TCP transport for distributed training (
TcpTransportwith coordinator/worker roles,send/recv,allreduce_sum). - scripts:
publish.shfor dependency-ordered crate publishing. - scripts:
bump-version.shfor 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
ModelHubremote weight download with caching. - yscv-model: Distributed training primitives —
GradientAggregatortrait,AllReduceAggregator,ParameterServer,InProcessTransport, gradient compression (TopKCompressor). - yscv-model: High-level
TrainerAPI withTrainerConfig, validation split,EarlyStopping,BestModelCheckpointcallbacks. - yscv-model: Eval/train mode toggle for layers (dropout, batch norm behavior).
- yscv-model: Compose-based
Transformpipeline (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
DTypeenum, typed constructors, andto_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).
- yscv-tensor:
DTypeenum now supports F32, F16, and BF16 storage variants. - yscv-imgproc: SURF descriptor matching accepts exact matches (dist < 1e-9) unconditionally, bypassing ratio test.