Repository navigation
Conversation
|
Noted as a draft, so I am holding off on a full review until it is marked ready for |
|
@srperens I figured that this can't be merged until the PRs it is stacked on are merged. I'll bring it out of draft at that point. Though you can review it now if you want. (I really wish GitHub Stacked PRs worked with forks.) |
c8901ed to
158b457
Compare
srperens
left a comment
There was a problem hiding this comment.
Verdict: Comment. The design and fallback hold up. One thing blocks approval: none of the roughly 2,900 new lines has been compiled by this repo's CI. All of it sits behind cfg(target_os = "macos"), and Build (macOS) skipped on this PR.
Requested changes
- Maintainer: add the
ci:macoslabel soBuild (macOS)runscargo teston the PR merged with main. The job only runs on that label:.github/workflows/ci.yml:644—&& contains(github.event.pull_request.labels.*.name, 'ci:macos')) - Optional: a vImage error currently returns
FlowError::Errorand only writes a debug log, so the operator sees nothing beyondInternal data stream error. Post agst::element_error!naming the format pair and the vImage code:backend/src/gst/vimage/imp.rs:400—gst::error!(CAT, imp = self, "vImage conversion failed: error {}", err);
Claims
| Claim | Verdict | Evidence |
|---|---|---|
| Frames vImage cannot handle fall back instead of failing | CONFIRMED |
Interlaced input exits at backend/src/gst/vimage/plan.rs:97 — return None;. Odd sizes on subsampled formats also drop out (backend/src/gst/vimage/plan.rs:104 — !(even_w && !in_info.width().is_multiple_of(2)). None builds a GstVideoConverter in conversion_path |
| A later renegotiation is not stuck in passthrough | CONFIRMED |
backend/src/gst/vimage/imp.rs:204 — const PASSTHROUGH_ON_SAME_CAPS: bool = true; makes BaseTransform recompute passthrough before every set_info (EXTERNAL: upstream configure_caps). The forced passthrough at backend/src/gst/vimage/imp.rs:366 — self.obj().set_passthrough(true); only fires when the two VideoInfo are equal |
| The tests guard the change and pass | UNVERIFIED here |
They run and pass on the author's fork at this head: Build (macOS) job, gst::vimage::tests::*, gst::vimage::imp::tests::* and a_portrait_source_is_letterboxed_when_the_format_also_changes all ok, 754 passed. That run built the branch alone, not the branch merged with main. Main is 12 commits ahead; the only one touching these files is #910's gpu.rs test pruning, in a separate hunk |
| vImage's conversion state is safe to share across threads | EXTERNAL |
Assumes Apple's documented thread-safety for generated vImage_*ToYpCbCr state |
Diagnosis. The engine is picked once per negotiation, transform_frame makes a single vImage call or falls back to frame_ref, and nothing per frame spawns a thread or allocates. Coverage of the claimed speed-up is BOUNDED to RGB32 to and from NV12/I420/YV12/UYVY/YUY2, RGB32 shuffles and NV12/I420 interleaving. Everything else runs on the same code videoconvert runs.
Radius. GLOBAL on macOS, nothing elsewhere. Every converter built through video_convert_mode() switches, e.g. backend/src/blocks/builtin/videoenc.rs:122 — let convert_element_name = convert_mode.element_name(); and backend/src/blocks/builtin/devicesrc.rs:214 — let convert_element_name = video_convert_mode().element_name();, plus Video Format and four NDI paths. STROM_DISABLE_VIMAGE is the escape hatch: backend/src/gpu.rs:264 — return VideoConvertMode::Software;. The element holds no pipeline reference and no closures, and stop() drops its state.
Overlaps. None. #897 edits gpu.rs and backend/Cargo.toml in hunks that don't touch these. #682 (memory-format adaptation) is not settled here: GL memory passes through unchanged and is never converted.
Tests & CI. In this repo at a5a55d7: Check (Linux), both Linux builds, WASM, API Contract Check and Static JS Tests passed; Build (macOS) and Build (Windows) were skipped. The Linux jobs never compile gst::vimage. With this much new unsafe FFI, I would not merge on Linux-green and leave the problems for main's macOS run.
Confidence: HIGH
Colour conversion is the largest fixed CPU cost left in Strom's macOS pipelines: every encoder and WebRTC sink wants Y'CbCr while the HTML renderer and compositor produce packed RGB. Eyevinn#726 raised `videoconvert`'s thread count, which is as far as that element goes. Accelerate's vImage does the same work in hand-tuned NEON kernels across its own pool. `stromvimageconvert` is a drop-in for `videoconvert` and `videoconvertscale`. It carries the same `n-threads` property, so `configure_video_convert` reaches it unchanged and no call site moves; `VideoConvertMode` gains a macOS-only `VImage` variant that names it. Format pairs vImage has no path for — and every resize — run on `GstVideoConverter`, which is the code `videoconvert` is built on, so the element cannot fail a conversion `videoconvert` would have managed. Measured on an M4 Max, 1080p, 300 frames, 40 counterbalanced pairs, net of source: RGBA to NV12 goes from 186 ms to 54 ms, 3.4x the threaded path and 13.9x stock. The A/A control over the same harness returned +0.4% (t=0.60, not significant) and the identity baseline t=-0.20. Fixation departs from `videoconvert` in one place, and the measurement is why. Feeding `vtenc_h264_hw` from RGBA the encoder offers { AYUV64, UYVY, NV12, I420, P010_10LE, ARGB64_BE, RGBA64_LE }; `videoconvert` takes AYUV64, sixteen bits per component for an eight-bit source that the encoder then converts again. Scoring a deeper format ahead of a shallower one cost 15.7% end to end (t=-163), so a depth increase now outweighs every other penalty combined. The element picks UYVY there and the regression goes to zero. `vimage_output_matches_videoconvert` is the load-bearing test: it pushes one frame through this element and through stock `videoconvert` for all eleven claimed pairs and compares them pixel for pixel, asserting via the new read-only `conversion-path` property that the vImage path was actually taken rather than the fallback. Swapping the red and blue entries of the RGBA permute map fails it by 149; treating limited-range Y'CbCr as full range fails it by 20. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
GRAY8 has no chroma planes, so the subsampling terms in conversion_loss read zero for it. From RGBA, GRAY8 scored 11 and I420 scored 59, and x264enc under profile=high offers GRAY8 beside the 4:2:0 formats because High permits 4:0:0. stromvimageconvert fixated on it and the encoder produced a greyscale stream; videoenc_profile_negotiation_test (Eyevinn#850) failed for RGBA and UYVY input. A colour input going to a grey format now costs more than every other penalty combined, depth increase included, matching videoconvert's SCORE_COLOR_LOSS. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The element listed width, height and pixel-aspect-ratio as convertible, so in the Video Format block videoscale negotiated passthrough and handed the resize to the converter, which has no add-borders: a 1080x1920 source set to 1280x720 NV12 came out stretched. Size is now fixed across the element, as in videoconvert. videoscale letterboxes again, and the block's conversion runs on vImage instead of the fallback. vImage writes centred chroma, but caps without chroma-site default to left-cosited at 720 lines and above, and x264enc and vtenc carry that label into the stream. RGB to subsampled Y'CbCr output is now labelled jpeg, a Y'CbCr input keeps its siting through the chroma copies, and a siting the peer pins takes the GstVideoConverter fallback. Y'CbCr to RGB had no test. yuv_to_rgb_recovers_the_source_colours checks it against flat source colours: vImage repeats chroma samples where videoconvert interpolates, so the two differ at hard edges while both are correct. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…me a siting RGB has no chroma, so a chroma-site field on RGB input caps describes nothing. Copying it onto the output made the element label cosited chroma it does not write, and Plan::build then declined vImage for the RGB-to-Y'CbCr pair. The output now always gets the centred label vImage writes; a Y'CbCr input still keeps its own siting. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The pad templates were plain video/x-raw, so the element refused to link caps that videoconvert takes: field-per-buffer interlaced video (format:Interlaced) and GL memory passing through. The templates now carry video/x-raw(ANY) as well. transform_caps offers conversion only for system memory, interlaced or not; any other memory is offered unchanged, so it passes through and is never mapped. In passthrough, conversion-path named the engine set_info had built, though no frame reached it. set_info now reads the passthrough decision BaseTransform has already made, reports "passthrough", and builds no converter. Interlaced input taking the fallback, and identical caps passing through, now have tests that fail when those behaviours are reverted. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
fixate_caps wrote pixel-aspect-ratio=1/1 into the output when the input had none. AVFoundation cameras send no aspect ratio, so input and output caps differed, BaseTransform never went passthrough, and every camera frame was copied through GstVideoConverter where videoconvert copies nothing. GL frames in the same position were mapped and converted on the CPU. A peer that accepts the input caps now gets them unchanged, and a ratio is fixated only where the peer constrains it. set_info refuses to convert anything but system memory, so GL frames can never reach a CPU copy. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ames Fixation no longer hands the input caps on unchanged whenever the peer's caps merely overlap them. A consumer that requires a field the input leaves out, such as the pixel aspect ratio, interlace mode or colorimetry a camera omits, now gets that field written, as with videoconvert, instead of refusing the caps. set_info passes frames through when the two sides describe the same frames, so a field written at its default costs no copy. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A failed conversion returned FlowError::Error with only a debug-log line, so the operator saw "Internal data stream error" and nothing else. Post the conversion kind, the format pair and the vImage code, and name the STROM_DISABLE_VIMAGE escape hatch in the debug detail. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
cf943ab to
dbbc21a
Compare
|
Claude here, replying for @wagenet.
The branch is rebased onto main to clear a conflict in |
srperens
left a comment
There was a problem hiding this comment.
Verdict: Comment. This supersedes my review of 2026-10-02 (at a5a55d7), and the verdict stands for the same single reason: none of the ~2,900 macOS-only lines has been compiled by this repo's CI. The optional ask from last time is done, and the new head merges cleanly with main@d2ae537 (git merge-tree reports no conflict).
Requested changes
- Maintainer: add the
ci:macoslabel soBuild (macOS)builds and tests the PR merged with main..github/workflows/ci.yml:644—&& contains(github.event.pull_request.labels.*.name, 'ci:macos')). I am not applying it myself, because it starts a paid run.
What changed since my last review
Compared with a5a55d7, the code delta is one hunk. A failed vImage call now posts an element error and no longer only logs: backend/src/gst/vimage/imp.rs:409 — gst::element_imp_error!(. The message names the plan, both formats and the cause (backend/src/gst/vimage/imp.rs:404 — let cause = if err == acc::K_PLANE_UNAVAILABLE {), and it points the operator at STROM_DISABLE_VIMAGE. This is what BLOCK_GUIDELINES.md asks for ("A refusal fails the flow, with a message the operator can act on"). The format! runs only on the error path, so the per-frame cost is unchanged.
The rest of the tree difference is the rebase. backend/Cargo.toml gains gstreamer-base.workspace = true for the subclass. The tower-sessions/cookie and gpu.rs test-module lines are main's own changes, not this PR's.
Claims
| Claim | Verdict | Evidence |
|---|---|---|
| Frames vImage cannot handle fall back instead of failing | CONFIRMED |
plan.rs and the fallback in conversion_path are unchanged since my review of 2026-10-02 at a5a55d7, where I confirmed this |
The escape hatch restores videoconvert |
CONFIRMED |
backend/src/gpu.rs:259 — if std::env::var_os("STROM_DISABLE_VIMAGE").is_some() { |
| The tests guard the change and pass | UNVERIFIED |
No run in this repo compiles gst::vimage. The fork run cited in the body built the branch alone, not the branch merged with main, and main has since changed gpu.rs (#1061) |
element_imp_error! with this argument shape compiles against the pinned gstreamer-rs |
UNVERIFIED |
It is only compiled on macOS. The macOS build settles it |
Radius. GLOBAL on macOS, nothing elsewhere, as before. Every converter built through video_convert_mode() switches to the new element.
Overlaps. None found. No open PR touches backend/src/gst/vimage/. #897 and #1002 edit backend/Cargo.toml in other hunks.
Tests & CI. At dbbc21a: Check (Linux), both Linux builds, WASM, API Contract Check and Static JS Tests pass. Build (macOS) and Build (Windows) were skipped. The Linux jobs do not compile this module, so they are no evidence for it. With this much new unsafe FFI, I would still not merge on Linux-green alone.
Confidence: HIGH
Description
Adds
stromvimageconvert, a GStreamer element that converts colour through Accelerate/vImage on macOS, selected by a new macOS-onlyVideoConvertMode::VImage. It hasvideoconvert'sn-threadsproperty, soconfigure_video_convert()reaches it and no call site changed.STROM_DISABLE_VIMAGE=1restoresvideoconvert.Fallback. Anything vImage has no path for (an unlisted format pair, interlaced content, a matrix outside BT.601/BT.709, a chroma siting it does not write) runs on
GstVideoConverter, the codevideoconvertis built on. The read-onlyconversion-pathproperty reportsvimage/fallback/passthrough/unnegotiated. Likevideoconvert, the element does not resize; in Video Format,videoscaleahead of it does, with borders. It accepts any caps features, asvideoconvertdoes: field-per-buffer video converts on the fallback; GL memory only passes through.vImage paths: packed RGB32 (
RGBA/BGRA/ARGB/ABGRandxvariants) to and fromNV12,I420,YV12,UYVY,YUY2; RGB32 to RGB32;NV12to and fromI420/YV12.Chroma siting. vImage writes centred chroma. Caps without
chroma-sitemean left-cosited at 720 lines and above, and x264enc and vtenc copy that label into the stream, so RGB-to-Y'CbCr output is labelledjpeg. Converted at 1080p and decoded back to RGB, the element's output is closer to the source thanvideoconvert's in all 8 cases measured (bars, two text overlays, a photo; NV12 and I420): PSNR 33.0-38.6 dB against 31.6-37.3 dB.Format choice follows
videoconvert's loss score, except that an 8-bit source is never widened when an 8-bit target exists: fed RGBA,vtenc_h264_hwoffersAYUV64, whichvideoconverttakes at a cost of 15.7% end to end (t = −163, n = 40). Colour to grey outranks every other penalty, as invideoconvert.Numbers (M4 Max, 1080p RGBA→NV12, 300 frames, 40 counterbalanced pairs, debug build, net of source): 3.4× the threaded
videoconvert(186 ms → 54 ms), 13.9× stock. A/A control 1.01× (t = 0.60). The harness is#[ignore]d tests inbench.rs. Measured on an earlier head; the RGBA→NV12 path it times is unchanged.Related Issue
None.
Type of Change
Checklist
cargo fmt --allcargo clippy --workspace --all-targets -- -D warningscargo test --workspaceTests
PR CI does not run these tests. The module is
#[cfg(target_os = "macos")]andBuild (macOS)needs theci:macoslabel. A fork run of the macOS job: run 36618849668, on this head.Run at
a5a55d7bon macOS (M4 Max, Homebrew GStreamer):cargo fmt --check,cargo clippy --workspace --all-targets -D warnings, andcargo test --workspace --no-fail-fast(1094 passed, 0 failed, 7 ignored).Each guard below was run with its behaviour reverted and fails:
vimage_output_matches_videoconvert: RGB→Y'CbCr and the 4:2:0 shuffles, pixel for pixel againstvideoconvert, asserting the vImage path ran. Fails on a red/blue swap (by 149), YV12 planes swapped (149), limited range read as full (20).yuv_to_rgb_recovers_the_source_colours: Y'CbCr→RGB against flat source colours. It is not compared withvideoconvert, which interpolates chroma for some formats and not others, so the two differ at hard edges while both are correct.a_portrait_source_is_letterboxed_when_the_format_also_changes(Video Format block): fails with 0 border pixels if the element offers to resize.hd_output_is_labelled_with_the_siting_vimage_writes,a_pinned_cosited_siting_takes_the_fallback,a_chroma_copy_keeps_the_input_siting.an_eight_bit_target_beats_every_deeper_one,dropping_colour_costs_more_than_any_colour_target,unsupported_pair_falls_back_and_still_converts.identical_caps_pass_straight_through,input_without_pixel_aspect_ratio_passes_through(camera caps omit the ratio),a_field_the_peer_requires_is_written_into_the_output(a consumer that requires a field the camera omits),interlaced_input_takes_the_fallback,caps_features_link_like_videoconvert(GL memory offered for conversion, or field-per-buffer video refused).Not measured: vImage's thread pool with many converters running at once.
🤖 Generated with Claude Code