fix(mediaplayer): re-anchor the present clock on seek (+ Ycbcr filter gating, HLS leak fix)#971
Merged
dooly123 merged 11 commits intoJul 19, 2026
Conversation
Seek was demuxer-only — it repositioned the byte source but never told the decoder, so on a seek the decoder kept serving its pre-seek buffers. Two user-visible symptoms, both cross-platform: - Post-seek audio silence for several seconds on muxed AAC/Opus. The PCM ring kept pre-seek chunks whose media-time gate blocked the post-seek audio queued behind them (audio-only streams serve ungated, which is why bare MP3/Ogg were unaffected — the priming correlation was coincidental). - Video/clock freeze up to ~18s on a cold forward seek. The present clock stayed clamped to the stale decode edge (newest ring PTS) and only moved once a post-seek video frame happened to arrive. Add basis_decoder_seek, called from basis_media_seek_us after the seek generation is published. It latches the target and bumps a generation each decoder leg observes; each leg flushes and re-anchors ON ITS OWN THREAD, so nothing is touched across threads: - audio-submit leg (demux thread): flush the PCM ring + the MF/AAC decoder - video-submit leg (Android, demux thread): flush the video codec + release the pre-seek frame ring (it owns the codec and writes that ring) - render leg: clear the frame ring (Windows) and reset the present clock so the existing prime/anchor path re-locks it to the first post-seek frame Position snaps to the target on seek so the seek bar tracks immediately. Covers both backends: Windows Media Foundation and Android AMediaCodec.
…reset Follow-ups on the seek decoder-notify hook: - Flush the video MFT (Windows) on the video-submit thread before the first post-seek AU. Clearing the ring alone left vdec's reorder buffer holding pre-seek frames that repopulated the ring on the next drain. Adds a video-submit generation latch, mirroring the Android video-codec flush. - Reset the Opus decoder state on seek (OPUS_RESET_STATE via the runtime ctl). opusDec bypasses the MF flush and kept its pre-seek predictive history across the discontinuity. - Seed aPtsFallback from the post-seek AU's PTS instead of 0, so chunks that Media Foundation leaves un-timestamped land on the target timeline rather than being trimmed or mis-gated at time 0. - Clear the Android frame ring in the render leg too, so present_select can't present a pre-seek frame (and overwrite the target position) in the window before the video-submit leg clears the ring.
The Android Vulkan present forced VK_FILTER_LINEAR for the Y'CbCr chroma reconstruction and the sampler mag/min filter unconditionally. Vulkan requires NEAREST when the format lacks the corresponding feature bit, and Qualcomm's UBWC external formats (VP9/AV1 on Adreno) frequently advertise nearest-only chroma, so forcing LINEAR is undefined behaviour. Gate the filter on VK_FORMAT_FEATURE_SAMPLED_IMAGE_YCBCR_CONVERSION_LINEAR_FILTER_BIT and keep mag/min equal to chromaFilter so separate-reconstruction is never required. Adds a one-shot log of the buffer's format-properties per distinct external format (external format, VkFormat, features, ycbcr model/range, chroma offsets) to diagnose the VP9 UBWC green corruption on-device: the conversion inputs are all buffer-derived and correct, so the remaining suspect is Adreno UBWC import handling, which needs the on-device dump to pin down. This filter fix is a real conformance fix but is not expected to be the green fix.
fMP4-HLS (moof+mdat segments) stalled every fragment — play ~2-3s, stall ~4s, repeat. The mp4 demuxer consumes a whole moof+mdat fragment before emitting any sample (it reads the entire mdat first), then plays it out over the fragment's duration; the 4 MiB read-ahead ring held only ~one fragment, so there was no runway to bank the next fragment ahead and playback starved at every segment boundary. TS-segmented HLS was immune because it emits AUs incrementally as packets arrive. Raise HLS_RING_CAP to 16 MiB so the producer banks several fragments ahead, hiding each fragment's download behind the previous fragment's playout. One heap allocation per stream, and TS benefits too. A fuller fix — streaming the fMP4 mdat sample-by-sample like the progressive-MP4 path so it pipelines regardless of ring size — is left as a follow-up.
The two early-return paths in the HLS open — read-ahead ring allocation failure and producer-thread start failure — freed only the handle (and the ring), leaking vod_uri and vod_dur_ms once they had been allocated for a VOD playlist. Free them before free(h) so a repeatedly failing open can't accumulate leaks. Both are NULL for live streams, so the added frees are no-ops there.
Chroma reconstruction and the ordinary sampler mag/min are separate Vulkan capabilities: chroma-linear needs YCBCR_CONVERSION_LINEAR_FILTER, linear mag/min needs SAMPLED_IMAGE_FILTER_LINEAR, and the two filters may only differ when SEPARATE_RECONSTRUCTION_FILTER is present. Choose each filter from its own feature bit, and when separate reconstruction is absent force them equal (downgrading chroma to nearest if the sampler cannot scale linearly). Prevents an invalid sampler on formats that advertise linear chroma but not linear sampling, and keeps linear luma when the format supports it.
…h PCM on seek Two seek-path corrections on both backends: - Centralise the post-seek frame-ring reset on the demux thread that owns the ring. The render leg no longer clears the ring itself; it re-anchors the present clock and then waits on a generation ack the demux publishes after flushing the decoder and dropping the pre-seek frames. Clearing from the render leg raced the producer and could delete post-seek frames the demux had already enqueued — most visibly when seeking while paused, where the render leg runs after the demux has repopulated the ring. - Flush the queued PCM in basis_decoder_seek so the audio callback stops serving pre-seek samples immediately instead of up to the next audio AU. The ring flush is mutex / critical-section guarded and safe from the caller thread; the decoder-state reset stays on the submit thread that owns it.
…nostic The Android VP9 format-properties diagnostic referenced an undefined LOG_TAG; this translation unit tags its logs with the "basis_media" string literal. Build-blocking on arm64.
The earlier bump to 16 MiB let the VOD producer reach end-of-playlist well ahead of playout, which sets producer_done — and request_seek rejects every seek once producer_done is set. On a 71s HLS VOD that broke backward seeks across the last third of the stream (measured on Quest: seeks from 49-66s all snapped forward). Back to 4 MiB so the reject window is just the final segment, matching the developer-branch behaviour. The fMP4-HLS stall the bump was meant to mitigate is deferred to the fuller sample-by-sample fMP4 streaming fix, which has no such side effect.
Windows x64 + Android arm64 rebuilt from the branch source: seek decoder-notify + videoSeekAck handshake, Ycbcr filter conformance + VP9 format diagnostic, HLS VOD-metadata leak fix, HLS read-ahead ring reverted to 4 MiB.
Diagnostic-only, not needed in shipped code. The formatFeatures-gated chroma/sampler filter fix it sat beside is unchanged. Android arm64 .so rebuilt to match.
towneh
marked this pull request as ready for review
July 19, 2026 00:06
dooly123
added a commit
that referenced
this pull request
Jul 20, 2026
…io-only position, seek WAV (#972) ## Summary Fast-follow to #971. That PR fixed the audio-only position freeze and the broad seek regression; this one closes out the muxed-audio recovery lag and the seek-bar bounce that were still hanging around, and makes WAV actually seekable while I was in the demux layer. Several related things, mostly native (decode/demux/present threads) plus one C# UI tweak in the media-player panel: **Muxed audio recovered seconds late after a seek.** Video re-anchored promptly but the audio leg was still being throttled by the delivery pace-gate on the way back, so it crept in a few seconds behind. The pace-gate now only applies to the split-audio leg (or before a video format is seen) — muxed audio recovers with the video instead of trailing it. **Audio-only position froze at 0:00 (the #971 fix, carried forward here).** Audio-only playback has no video decoder, so pinning the present clock left it stuck at -1. Position now comes from the audio playback front for audio-only, with a short settle that holds the reported time at the seek target during recovery. **Audio-only position froze at the seek target on Android (found in the device pass).** Separate from the above, and Android-only: the render tick was unconditionally pinning the present clock to the seek target after a seek, even with no video decoder — and since nothing ever presents a frame to advance it, audio-only position stayed frozen at the target while audio played on. Windows already gates its render tick out for audio-only (no video writes); Android didn't. Fixed by gating that write on having a video codec, matching the seek path. Verified on Quest for Opus and WAV. **Seek bar bounced back to the pre-seek spot before settling.** Two causes: the decoder could serve a frame that was already in flight before the seek was taken, and the reported position could latch onto that slipped frame mid-recovery. Fixed by dropping frames posted before the per-leg seek ack, and holding reported position at the target until the playback front is closer to the target than to the origin it seeked from. The C# panel's slider-hold uses the same closer-to-target-than-origin test so a mid-recovery sample can't bounce the slider. **HLS is excluded from that pre-seek drop.** HLS reposition is async (it lands on a read-reposition boundary), so the seek-sequence gate can't tell an in-flight pre-seek frame from a valid post-seek one — gating it would drop good audio. HLS keeps its own reposition path. **WAV now seeks.** Wired a data-offset reseek into the RIFF/WAVE demuxer, so on a range host it reports duration and seeks frame-aligned, matching the MP4/WebM/Ogg/MP3 lanes. Seekability is only exposed when the byte source is actually reseekable, so a streaming capture still plays to EOF without a dead seek bar. There's also a chore commit folding in the multichannel streaming test prefab used for the 7.1 lane. Native binaries (Win x64 + Android arm64) rebuilt from this source, RIST-enabled. ### Verified Full Editor regression pass (Windows, D3D11) across the curated lanes: - Ogg Opus — seek both directions recovers within ~1s, position tracks, no slider stall. - MP3 VBR — same, Xing seek intact. - WAV — seeks clean at all gap sizes, no bounce. - HLS-TS and HLS-fMP4 — paced recovery from target. - Progressive + sidx MP4 seek. - AAC 5.1 and LPCM 7.1 multichannel — audio keeps flowing across seeks, A/V stays in sync. Android device pass (Quest Pro, arm64) across the same lanes: - Ogg Opus and WAV (audio-only) — seek both directions tracks cleanly after the render-tick fix above. - HLS-TS, progressive MP4, sidx fMP4 (muxed) — audio recovers with video, paced from the target, A/V in sync. - AAC 5.1 and LPCM 7.1 multichannel — correct channel placement by ear, in sync. - HLS-fMP4 and LPCM 7.1 play clean but expose no seek bar (their containers report no duration — unrelated to this change). ### Out of scope (pre-existing, not introduced here) - Small progressive files that fully download before a seek can't reseek on the completed connection — the byte source needs a fresh fetch on reseek-after-EOF. Logged separately. - Backward seeks on the multichannel LPCM lane can take several seconds to refill audio — a separate delivery issue, unchanged by this branch. - **MP3 audio-only position freezes at the seek target on Android, intermittently.** Distinct from the audio-only fix above (Opus and WAV track fine on the same build). MP3 carries no container timestamps — Android's MediaCodec passes the demuxer's derived PTS straight through, where Windows' Media Foundation re-timestamps and masks it. A pre-existing Android MP3 gap (never in a device pass before), logged separately for its own fix. - **Muxed backward seeks bounce the position bar to the pre-seek spot for ~1s on Android before settling.** Cosmetic, self-corrects; audio recovery and final position are correct. The video present clock briefly reads a lingering pre-seek frame before the flush lands. Logged separately. ## Required checks All boxes below must be ticked before this PR can merge. If a check is genuinely N/A, tick it anyway and explain under **Notes**. <!-- required-checks-start --> <!-- Tick the boxes in place — do not edit the line text. The pr-checklist workflow parses this block; per-PR context goes under Notes. --> - [x] **Tested** — I built and ran this locally. The change works in the editor and (where relevant) in a built player. - [x] **Transform access is combined and limited** — In hot paths, transform reads/writes go through `TransformAccessArray` or are otherwise batched. I have not added per-frame `transform.position` / `transform.rotation` / `transform.localPosition` calls inside loops. Whenever I need both position and rotation, I use the combined APIs — `SetPositionAndRotation` / `SetLocalPositionAndRotation` for writes, `GetPositionAndRotation` / `GetLocalPositionAndRotation` for reads — instead of two separate property accesses; the combined call does one local-to-world matrix traversal instead of two. - [x] **Addressables used for asset/memory loading** — Any new asset loads go through Addressables. No new `Resources.Load`, no direct asset references that pull large content into memory on scene load. - [x] **No new `GetComponent` / `AddComponent` where avoidable** — Where unavoidable, the result is cached on a field, and any `GetComponent<T>` is replaced with `TryGetComponent<T>(out var x)` — bare `GetComponent` will be denied. `TryGetComponent` is the modern API (Unity 2019.2+) and skips the Editor-only GC allocation `GetComponent` causes when a component is missing: Unity wraps the `null` return in a managed "fake null" object so its overloaded `==` operator can still detect destroyed C++ objects, and constructing that wrapper allocates; `TryGetComponent` returns a `bool` plus `out` parameter and never builds the wrapper. None of these calls run inside `Update`, `LateUpdate`, `FixedUpdate`, jobs, or other per-frame code paths. - [x] **Per-frame work is scheduled through `BasisEventDriver`** — Any new per-frame work hooks into `BasisEventDriver` rather than adding standalone `Update` / `LateUpdate` / `FixedUpdate` callbacks on a MonoBehaviour. - [x] **Anything added to `BasisEventDriver` is bulletproof, or guarded by `try`/`catch`** — `BasisEventDriver` runs the single per-frame tick that drives the whole framework (network apply, local player sim, blendshapes, JigglePhysics, nameplates, and more) as one sequential chain. An unhandled exception anywhere in that chain aborts the rest of the tick, so every step after the throwing one is silently skipped for that frame. New work added to the driver must either be guaranteed not to throw, or be wrapped in a `try`/`catch` that contains the failure and surfaces it through `BasisDebug` — logged once / rate-limited, never every frame (see the existing `HVRBasisBuiltInAddresses.Simulate()` guard for the pattern). Expect this to be scrutinized closely in review. - [x] **Considered jobification** — I asked whether this work can be moved to a Unity Job (Burst-compiled where possible). If it can, it is. If it cannot, the reason is in **Notes**. - [x] **No needless `{ get; set; }` properties or access lockdowns** — Public fields are fine; Basis is meant to be read and modified freely, so don't wall things off `private`/`internal` without a real reason. Don't wrap a field in `{ get; set; }` when the accessors do nothing — property accessors have a real performance cost vs direct field access, and the lead maintainer prefers plain fields (or a method / setter-only property when only the setter needs logic) over a noop-getter pair. For `.Instance` singletons, callers reassigning `Type.Instance` is allowed; if that would break your code, log a warning or throw — don't block the assignment. Locking down access is not your call. - [x] **Camera access goes through `BasisLocalCameraDriver`** — Code that needs the local camera (transform, projection, rig data, etc.) pulls it from `BasisLocalCameraDriver` rather than looking one up itself. Don't roll a separate camera discovery path. - [x] **Logging uses `BasisDebug`** — All new logging calls go through `BasisDebug.Log` / `BasisDebug.LogWarning` / `BasisDebug.LogError` (with an appropriate `LogTag`) instead of `UnityEngine.Debug.Log` / `Debug.LogWarning` / `Debug.LogError`. `BasisDebug` routes through Basis's tagged, color-coded logger and respects the project-wide `LoggingDisabled` toggle so logging can be killed at runtime; bare `Debug.Log` calls bypass that and will be denied. - [x] **No scene-wide discovery for dependencies** — New code is architected so it does not need `FindObjectOfType` / `FindObjectsOfType` / `GameObject.Find` / `FindGameObjectsWithTag` to locate what it depends on. References are wired in — registered through an existing manager/driver, injected at init, or passed in by the caller — rather than discovered by scanning the scene at runtime. If a scene scan is genuinely unavoidable, justify it under **Notes**. - [x] **No allocations in hot paths** — Per-frame code (Update / LateUpdate / FixedUpdate, simulation loops, jobs, anything called once per frame or more) does not allocate. No `new` on reference types, no LINQ, no `string` concatenation/interpolation, no boxing, no `foreach` over interface-typed collections. Allocate once at init and reuse the buffer. - [x] **No debugging in hot paths** — No log calls of any kind on per-frame paths, including `BasisDebug`. Hot-path logging floods the console and incurs cost on every frame regardless of whether the message is filtered out downstream. If a hot-path log is needed while iterating, gate it behind `#if UNITY_EDITOR` and remove (or leave gated) before merge. - [x] **Hot-path collection access is optimized** — Cache `.Count` (lists) / `.Length` (arrays) into a local `int` before the loop instead of re-reading the property each iteration. Prefer `T[]` (with a separate length int when the array is over-sized) over `List<T>` where the data is hot — Unity's mono BCL doesn't expose `CollectionsMarshal.AsSpan(List<T>)`, so a list can't be fed into `Span<T>` / unsafe paths cleanly. Where the perf justifies it, drop into `Span<T>` / `ref` locals / `Unsafe.As` / `unsafe` pointer code to skip bounds checks and copies, and call out the invariants you're relying on under **Notes** so reviewers can sanity-check them. <!-- required-checks-end --> ## Testing details Tick the platforms you actually tested on. Leave the rest unticked — these are informational and do not block merge. - [x] Windows - [ ] Linux - [x] Android - [ ] iOS - [ ] macOS Input / control mode coverage: - [ ] Tested in VR (note headset under **Notes**) - [ ] Tested in desktop / non-VR mode - [ ] Tested with phone controls (mobile touch input) - [x] N/A — change does not touch player/XR/input code Where applicable, confirm these flows still work after your changes: - [ ] Hot-switching (desktop ↔ VR mode swap at runtime) - [ ] Avatar swapping - [ ] Server swapping (joining / leaving / changing servers) - [x] N/A — change does not touch any of the above ## Notes The bulk of this is native C/C++ on the decode/demux/present threads (`basis_media_core.c`, `basis_win_decode.cpp`, `basis_android_decode.c`, `basis_mp3.c`, `basis_wav.c`) plus the two rebuilt binaries. The only managed change is one method in `BasisMediaPlayerPanelProvider` that holds the seek slider until the engine clock lands — it does a couple of `Math.Abs` comparisons on doubles, no allocation, no logging, and runs in the existing panel refresh rather than a new per-frame callback. The Unity-pattern boxes above are ticked N/A on that basis: no transforms, no asset loads, no `GetComponent`, no camera lookup, no scene discovery, no new `BasisEventDriver` work. Jobification N/A — the work is on OS-decoder and demuxer threads (Media Foundation / MediaCodec / the C demuxers), not Unity job-eligible code. Tested on Windows in the Editor (D3D11) and on a Quest Pro (Android arm64) device pass. The device pass turned up the Android audio-only render-tick freeze fixed here, plus two pre-existing issues logged separately (MP3 audio-only freeze, muxed backward-seek bar bounce).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Fixes a batch of media-player seek and playback regressions found in a full pass across the Unity Editor, Windows standalone, and Quest Pro.
The main one: a seek repositioned the demuxer but never told the decoder, so the presentation clock stayed clamped to the stale decode edge and the picture froze until a post-seek frame happened to arrive (up to ~18s on a cold forward seek). This adds a decoder-side seek —
basis_decoder_seekflushes the decoder and the PCM ring and re-anchors the present clock to the target. The render leg no longer clears the frame ring itself; it waits on a small generation ack the demux thread (which owns the ring) publishes after it has flushed, so the two threads don't both reset the ring and the render leg can never erase a post-seek frame the demux just wrote. Verified on Quest and in the Windows Editor: video re-anchors straight to the target, no freeze, no stale pre-seek frames, backward and forward.Also in here:
VK_FILTER_LINEARunconditionally. Formats that only advertise nearest (some UBWC external formats on Adreno) were getting linear forced on them, which is undefined.vod_uri/vod_dur_ms) on the two open-failure paths; they were leaked.Windows x64 and Android arm64 native binaries are rebuilt from this source and included.
Known follow-ups (tracked, not in this PR)
The clock fix is correct, but re-anchoring to the true target surfaced a separate weakness the old frozen clock had been masking. Flagging these so reviewers know the shape of what's left:
Required checks
This is a native C/C++ change to the media-player plugin (decode / demux / present); it touches no C# and none of the Unity per-frame, component, Addressables, camera, logging, or event-driver surfaces the checks below cover. Ticked as N/A per the template, with the substantive one (Tested) genuinely done.
TransformAccessArrayor are otherwise batched. I have not added per-frametransform.position/transform.rotation/transform.localPositioncalls inside loops. Whenever I need both position and rotation, I use the combined APIs —SetPositionAndRotation/SetLocalPositionAndRotationfor writes,GetPositionAndRotation/GetLocalPositionAndRotationfor reads — instead of two separate property accesses; the combined call does one local-to-world matrix traversal instead of two.Resources.Load, no direct asset references that pull large content into memory on scene load.GetComponent/AddComponentwhere avoidable — Where unavoidable, the result is cached on a field, and anyGetComponent<T>is replaced withTryGetComponent<T>(out var x)— bareGetComponentwill be denied.TryGetComponentis the modern API (Unity 2019.2+) and skips the Editor-only GC allocationGetComponentcauses when a component is missing: Unity wraps thenullreturn in a managed "fake null" object so its overloaded==operator can still detect destroyed C++ objects, and constructing that wrapper allocates;TryGetComponentreturns aboolplusoutparameter and never builds the wrapper. None of these calls run insideUpdate,LateUpdate,FixedUpdate, jobs, or other per-frame code paths.BasisEventDriver— Any new per-frame work hooks intoBasisEventDriverrather than adding standaloneUpdate/LateUpdate/FixedUpdatecallbacks on a MonoBehaviour.BasisEventDriveris bulletproof, or guarded bytry/catch—BasisEventDriverruns the single per-frame tick that drives the whole framework (network apply, local player sim, blendshapes, JigglePhysics, nameplates, and more) as one sequential chain. An unhandled exception anywhere in that chain aborts the rest of the tick, so every step after the throwing one is silently skipped for that frame. New work added to the driver must either be guaranteed not to throw, or be wrapped in atry/catchthat contains the failure and surfaces it throughBasisDebug— logged once / rate-limited, never every frame (see the existingHVRBasisBuiltInAddresses.Simulate()guard for the pattern). Expect this to be scrutinized closely in review.{ get; set; }properties or access lockdowns — Public fields are fine; Basis is meant to be read and modified freely, so don't wall things offprivate/internalwithout a real reason. Don't wrap a field in{ get; set; }when the accessors do nothing — property accessors have a real performance cost vs direct field access, and the lead maintainer prefers plain fields (or a method / setter-only property when only the setter needs logic) over a noop-getter pair. For.Instancesingletons, callers reassigningType.Instanceis allowed; if that would break your code, log a warning or throw — don't block the assignment. Locking down access is not your call.BasisLocalCameraDriver— Code that needs the local camera (transform, projection, rig data, etc.) pulls it fromBasisLocalCameraDriverrather than looking one up itself. Don't roll a separate camera discovery path.BasisDebug— All new logging calls go throughBasisDebug.Log/BasisDebug.LogWarning/BasisDebug.LogError(with an appropriateLogTag) instead ofUnityEngine.Debug.Log/Debug.LogWarning/Debug.LogError.BasisDebugroutes through Basis's tagged, color-coded logger and respects the project-wideLoggingDisabledtoggle so logging can be killed at runtime; bareDebug.Logcalls bypass that and will be denied.FindObjectOfType/FindObjectsOfType/GameObject.Find/FindGameObjectsWithTagto locate what it depends on. References are wired in — registered through an existing manager/driver, injected at init, or passed in by the caller — rather than discovered by scanning the scene at runtime. If a scene scan is genuinely unavoidable, justify it under Notes.newon reference types, no LINQ, nostringconcatenation/interpolation, no boxing, noforeachover interface-typed collections. Allocate once at init and reuse the buffer.BasisDebug. Hot-path logging floods the console and incurs cost on every frame regardless of whether the message is filtered out downstream. If a hot-path log is needed while iterating, gate it behind#if UNITY_EDITORand remove (or leave gated) before merge..Count(lists) /.Length(arrays) into a localintbefore the loop instead of re-reading the property each iteration. PreferT[](with a separate length int when the array is over-sized) overList<T>where the data is hot — Unity's mono BCL doesn't exposeCollectionsMarshal.AsSpan(List<T>), so a list can't be fed intoSpan<T>/ unsafe paths cleanly. Where the perf justifies it, drop intoSpan<T>/reflocals /Unsafe.As/unsafepointer code to skip bounds checks and copies, and call out the invariants you're relying on under Notes so reviewers can sanity-check them.Testing details
Input / control mode coverage:
Where applicable, confirm these flows still work after your changes:
Notes
Native plugin change only — no C# — so the Unity-pattern required checks above are N/A and ticked as such per the template.
Tested on Quest Pro (Android arm64, VR) and the Windows Editor (Direct3D). Seek was exercised both directions on progressive MP4, integrated fMP4 (sidx), and TS-segmented HLS; playback verified on VP9/WebM, Opus, MP3, and AAC 5.1. The seek clock fix was corroborated against the diagnostics CSV on both platforms (video position re-anchors to the target immediately). The audio-recovery follow-up above is the same CSV showing audio catching up a few seconds behind on a large backward seek.