Skip to content

Commit f640bcf

Browse files
committed
Fix OHOS deferred rendering across seeks
1 parent 5a34476 commit f640bcf

19 files changed

Lines changed: 595 additions & 68 deletions

File tree

‎modern/ARCHITECTURE.md‎

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,20 @@ newer snapshot is already current. A presentation-generation boundary clears
195195
all retained retries and invokes each renderer's non-blocking pending-frame
196196
invalidation hook. That hook may cancel a producer/image association which has
197197
not entered submission; it may not wait for already submitted GPU work.
198+
The playback worker does not join native render calls before a hardware-decoder
199+
flush: a codec present/release IPC may itself need that flush to make progress.
200+
The decoder flush becomes the cancellation authority for native producers
201+
which never published a consumer callback. Player publishes that completed
202+
generation and invokes the renderer's second-phase completion hook. A retired
203+
render call which overlaps the first pass repeats invalidation and completion
204+
when it returns. This two-phase discontinuity protocol has no caller, UI, or
205+
render-thread wait, no per-frame cost, and no wait for submitted GPU work.
206+
Native producer callbacks are a separate synchronization domain from renderer
207+
state. A redraw callback may run synchronously before the platform producer
208+
call returns, so forwarding it must not acquire a renderer/selector state lock
209+
which the graphics thread holds across that producer call. The active renderer
210+
identity is published independently, and one producer decision emits at most
211+
one redraw even when its consumer callback arrives before the decision returns.
198212
Timer-backoff busy results remain eligible for a high-level output's existing
199213
latest-frame supersession policy and are not pinned by Player.
200214

@@ -458,7 +472,19 @@ invalidates it before import, the callback path acquires and immediately returns
458472
its consumer buffer, then clears the exact frame key before admitting another
459473
OHCodec output. Player repeats invalidation after an overlapping backend call,
460474
closing the race where an old render attempt queues its producer after seek was
461-
published. This path adds no per-frame GPU wait or decoded-source CPU copy.
475+
published. Player flushes the decoder without joining that native render call,
476+
then invokes a second completion phase. If OHCodec cancelled the invalidated
477+
producer without a callback, that phase clears the exact association. An
478+
overlapping retired call repeats both phases after it returns; any later orphan
479+
callback is drained instead of being matched to a newer frame. This path adds
480+
no render-thread or per-frame GPU wait and no decoded-source CPU copy.
481+
OHOS Surface notification can run before the native output-present call
482+
returns. The adapter and mobile selector therefore forward redraw through an
483+
event-only lock and atomically published active renderer identity, never their
484+
render-state locks. Interop records an early callback while the producer call
485+
is in flight and hands off exactly one redraw after it returns. This avoids both
486+
a callback/producer lock cycle and a second present of the same exactly-once
487+
OHCodec token.
462488

463489
The Vulkan interop has three explicitly reported format routes:
464490

‎modern/DECISIONS.md‎

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -771,6 +771,10 @@ remain unchanged.
771771
make statistics optional
772772
- Amended: 2026-08-12 to make exact retry-frame retention and pending-producer
773773
invalidation a Player/backend responsibility
774+
- Amended: 2026-08-12 to make decoder-flush cancellation a two-phase
775+
discontinuity contract
776+
- Amended: 2026-08-12 to isolate synchronous native callbacks from renderer
777+
state locks and coalesce one redraw per producer decision
774778
- Status: Accepted
775779
- Scope: Core render snapshots and cross-platform deferred-producer
776780
invalidation, plus Windows D3D11 context scheduling, composition output retry
@@ -794,6 +798,25 @@ removed that collision, then reason-level diagnostics exposed the remaining
794798
owner: FFmpeg's reservation-aware D3D11VA work could release and immediately
795799
reacquire the recursive immediate-context lock before a timer-only retry.
796800

801+
The later OHOS seek stress matrix exposed a second lifecycle boundary. A
802+
non-blocking generation invalidation can mark an asynchronous producer stale,
803+
but some codec outputs never publish their native consumer callback because
804+
`avcodec_flush_buffers()` synchronously cancels them. Waiting for that callback
805+
before flushing is circular and produced a one-second seek timeout. Clearing
806+
the association before all old CPU render calls return is also unsafe because
807+
one of those calls can republish the retired producer after the clear.
808+
Joining those calls before flush is equally circular on OHCodec: a native
809+
`ReleaseOutputBuffer`/producer operation can itself wait for decoder flush and
810+
caused the foreground process to exceed the six-second thread-block watchdog.
811+
The consumer callback may also arrive before that producer operation returns.
812+
The OHOS adapter and mobile selector previously forwarded the callback through
813+
their recursive renderer-state locks while the render thread held those locks
814+
around the producer call. The callback then waited for the render thread, while
815+
the render thread's next native producer operation waited for callback return.
816+
Forwarding both the callback edge and the producer-return edge could also
817+
schedule two redraws for one exactly-once OHCodec token; the second present was
818+
a successful no-op and could not generate another consumer callback.
819+
797820
### Decision
798821

799822
1. Add `Player::renderVideoDetailed()` and `VideoRenderResult`. Each attempt
@@ -834,6 +857,20 @@ reacquire the recursive immediate-context lock before a timer-only retry.
834857
retries and invoke a non-blocking renderer invalidation hook for producer
835858
work which has not entered GPU submission. Recheck and invalidate again
836859
after an overlapping backend call closes the old-render/new-seek race.
860+
8. A hardware-decoder seek uses a two-phase discontinuity protocol. First,
861+
publish generation invalidation, then flush the decoder without joining
862+
native render calls and publish the completed cancellation generation.
863+
`completePendingFrameInvalidation()` may retire an invalidated association
864+
whose callback the decoder flush cancelled; it may not wait for submitted
865+
GPU work. An old render call which returns after that generation repeats
866+
invalidation and completion, closing the late-republish race without a
867+
render-thread barrier. Both public hooks default to no-op.
868+
9. Backend redraw callbacks use synchronization independent of renderer state
869+
held across a native producer/present call. The mobile selector validates a
870+
redraw against atomically published active API/generation identity without
871+
entering its render-state mutex. When a native consumer callback arrives
872+
before its producer call returns, interop records that edge and publishes
873+
exactly one redraw after producer completion.
837874

838875
### Consequences
839876

@@ -852,6 +889,13 @@ reacquire the recursive immediate-context lock before a timer-only retry.
852889
- The new public result and Windows statistics are additive API. Existing code
853890
using `renderVideo()` and `skippedRenders` continues to compile, with the
854891
clarified terminal-drop meaning for the latter.
892+
- Applications do not wait for, flush, or track backend producer state around
893+
Player controls. Decoder cancellation and the repeated completion pass add
894+
no steady-state frame cost and preserve the lifetime of GPU work already
895+
submitted.
896+
- Callback delivery cannot form a renderer-state/native-producer lock cycle,
897+
and one exactly-once decoder output cannot leave a duplicate redraw waiting
898+
for a callback which will never exist.
855899
- D3D11 statistics use `Off`, `Counters`, or `Timing`. The high-level output no
856900
longer reads, clears, and re-aggregates renderer atomics after every frame;
857901
retry behavior remains identical when statistics are off.

‎modern/MIGRATION.md‎

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -868,8 +868,14 @@ implementation.
868868
call `VideoRenderAPI::invalidatePendingFrames()` automatically. The default
869869
implementation is a non-blocking no-op; asynchronous native-buffer backends
870870
cancel only producer associations which have not entered GPU submission.
871-
Existing custom renderer source does not need an override, but consumers
872-
must rebuild for the appended public virtual;
871+
A hardware-decoder seek then flushes the decoder without joining native
872+
render calls, publishes that cancellation generation, and calls
873+
`completePendingFrameInvalidation()`. Backends use that second phase to
874+
retire a producer callback synchronously cancelled by the flush. A retired
875+
render call which overlaps the first pass repeats both hooks when it returns.
876+
Both appended virtuals default to no-op, so custom renderer source remains
877+
compatible after consumers rebuild; no application-side interop flush,
878+
render-thread wait, or per-frame wait is required;
873879
- `OpenGLPresentCallback` runs on that same graphics-owner thread after
874880
framebuffer submission and before hardware-source release; it is intended
875881
for the adapter's bounded window-present call, not general application work;
@@ -885,7 +891,10 @@ implementation.
885891
wrapper). Its default implementation calls the legacy boolean `render()`;
886892
backend event callbacks may request another player state;
887893
- audio-sink and video-render backend event callbacks run on the thread chosen
888-
by the backend and may request another player state;
894+
by the backend and may request another player state. A native producer may
895+
invoke its redraw callback before the producer call returns; renderer
896+
adapters therefore forward redraw through synchronization independent of
897+
render-state locks and coalesce that producer decision to one redraw;
889898
- compressed audio packets cross a bounded queue to their decode worker, then
890899
decoded audio crosses a second bounded queue to the dedicated audio-output worker;
891900
ordinary conversion, time stretching, sink writes, and primary device-clock

‎modern/PLAN.md‎

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -100,13 +100,20 @@ The following accepted decisions constrain all remaining work:
100100
- The OHCodec/Vulkan deferred-frame freeze root cause is repaired in C++.
101101
Player now retains the exact backend-deferred frame per renderer key and
102102
automatically invalidates pending producer associations at every
103-
presentation-generation boundary. The OHCodec consumer path drains an
104-
invalidated one-buffer output before admitting a new frame, without a CPU
105-
copy, renderer fallback, or GPU-wide wait. Deterministic Windows playback
106-
tests, OHOS arm64/API 23 shared/static cross-builds, the player native link,
107-
and signed HAP packaging pass. Replaying the forward/backward seek matrix on
108-
`legend.mkv` and `wednesday.mp4` remains a connected-device gate requiring
109-
explicit installation approval; see
103+
presentation-generation boundary, then completes decoder-cancelled
104+
associations after synchronous flush without joining native render calls.
105+
The OHCodec consumer path drains invalidated/orphaned buffers before
106+
admitting a new frame. Native redraw callbacks no longer enter renderer
107+
state locks held across producer calls, and an early callback is coalesced to
108+
exactly one redraw per OHCodec output. The repair adds no CPU copy, renderer
109+
fallback, caller-side flush, per-frame wait, or GPU-wide wait. Deterministic
110+
playback coverage, OHOS arm64/API 23 shared/static cross-builds, the player
111+
native link, and signed HAP packaging pass. The overwrite-installed HAP then
112+
passed cold-start, forward-seek, and backward-seek cells on both `legend.mkv`
113+
and `wednesday.mp4` at 24--25 FPS with balanced native release/callback
114+
counts; eight additional Wednesday forward/backward pairs continued decoding
115+
and presenting without a seek timeout, thread-block watchdog, or crash. The
116+
cold cells did not show `当前媒体不可定位`; see
110117
[`PLAN_HISTORY_2026-08-12_OHOS_DEFERRED_RENDER_SEEK.md`](PLAN_HISTORY_2026-08-12_OHOS_DEFERRED_RENDER_SEEK.md).
111118
- The OHOS software-decode follow-up now keeps FFmpeg LTO/NEON and the shared
112119
small-build policy while overriding the effective arm64 optimization from
@@ -314,6 +321,9 @@ all those frames still used an RGBA16F normalization image.
314321
still reported tone mapping. The 2026-08-12 follow-up then replaced the
315322
former Profile 5 Vulkan-to-OpenGL fallback with opaque identity Vulkan
316323
sampling and passed repeated `wednesday -> legend -> wednesday` switching.
324+
The separate deferred-render regression now also passes cold-start plus
325+
forward/backward seek on both local files and an eight-pair Wednesday seek
326+
stress run without presentation freeze.
317327
Physical audio identity and the remaining broad manual cells stay open.
318328

319329
The four-thread software-decode build, signed-device counters, temperatures,

‎modern/PLAN_HISTORY_2026-08-12_OHOS_DEFERRED_RENDER_SEEK.md‎

Lines changed: 59 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ captured `legend.mkv` session advanced from roughly 2:52 to 3:25 and continued
1010
receiving OHCodec video outputs, while successful presentation FPS stayed at
1111
zero. Only the first few private-consumer callbacks/imports completed.
1212

13-
The failure was not demux seekability, audio clocking, software decode
13+
The first failure was not demux seekability, audio clocking, software decode
1414
throughput, ArkTS slider state, or a Vulkan color path. The asynchronous
1515
OHCodec/Vulkan handshake queued frame A into its private one-buffer
1616
`OH_ConsumerSurface` and correctly returned `DeferredUntilRedraw`. Before the
@@ -40,6 +40,45 @@ invalidating Player's snapshot without canceling the interop's queued A.
4040
- Player invokes invalidation again when generation changes during a backend
4141
call, closing the race where an old render attempt queues its producer just
4242
after seek publishes the new generation.
43+
- Stressing consecutive forward/backward seeks exposed that invalidation alone
44+
was insufficient: OHCodec can cancel a surface output during decoder flush
45+
without publishing the callback which would normally drain it. A preliminary
46+
callback-before-flush wait reproduced `seek.error: Operation timed out`,
47+
proving that ordering circular.
48+
- A second diagnostic ordering which joined old native render calls before
49+
flush was also rejected on-device. Wednesday's backward seek left
50+
`ReleaseOutputBuffer` and `QueueInputBuffer` IPC calls blocked for five
51+
seconds and triggered HarmonyOS `THREAD_BLOCK_6S`; the system killed the
52+
foreground process. The codec flush must be allowed to break that native
53+
dependency, not wait behind it.
54+
- The final C++ contract is a non-joining two-phase protocol. Player first
55+
publishes non-blocking invalidation, flushes the hardware decoder, publishes
56+
the completed cancellation generation, and calls
57+
`completePendingFrameInvalidation()`. OHCodec acquires and releases an
58+
available invalidated buffer, or clears the association known to have been
59+
cancelled by the synchronous flush. A retired render call which returns
60+
after the first pass repeats invalidation and completion, closing the
61+
late-republish race. A later callback without an association is drained as
62+
an orphan and cannot poison the next exact frame.
63+
- The remaining freeze was a callback lock cycle. OHOS can invoke the private
64+
Surface callback before `OH_VideoDecoder_RenderOutputBuffer()` returns. The
65+
adapter and mobile selector used to reacquire renderer-state locks which the
66+
render thread still held around that native call; the callback waited for the
67+
render thread while the next producer operation waited for callback return.
68+
Redraw forwarding now uses a separate event lock and atomically published
69+
active renderer API/generation, so it never enters those state locks.
70+
- An early callback and the producer-return path also used to schedule two
71+
redraws for the same exactly-once OHCodec output. The second present was a
72+
successful no-op and could never produce the callback needed to continue.
73+
Interop now records callbacks while the producer call is in flight and emits
74+
exactly one redraw after it returns. Orphan acquisition remains on the Vulkan
75+
render thread because a callback may precede the buffer becoming acquirable.
76+
- `Player::seek()` now accepts the latest request during asynchronous input
77+
opening without changing the interrupt epoch which protects that open. The
78+
request is clamped and executed after metadata is available, and its callback
79+
receives failure if opening cannot complete. The OHOS Demo forwards the
80+
request to this core contract instead of rejecting it from a stale ArkTS
81+
`seekable` snapshot.
4382

4483
There is no per-frame GPU wait, decoded-source map, software transfer, staging
4584
copy, upload, decoder fallback, or ArkTS/caller-owned retry-frame state. GPU
@@ -59,6 +98,22 @@ submissions already in flight retain their existing timeline-based lifetime.
5998
- The OHOS player native library linked and the signed debug HAP packaged at
6099
`modern/examples/ohos/player-hap/entry/build/default/outputs/default/entry-default-signed.hap`.
61100

62-
The HAP was not installed or launched in this repair turn. Connected-device
63-
replay with local `legend.mkv` and `wednesday.mp4` remains gated on explicit
64-
installation approval.
101+
The final signed HAP was overwrite-installed on the connected ALN-AL80. Both
102+
document-picker cold starts omitted the former `当前媒体不可定位` toast, and
103+
both processes survived the complete matrix:
104+
105+
| Media / cell | Position | FPS | Hardware decoded | Presented | Native release/callback |
106+
|---|---:|---:|---:|---:|---:|
107+
| `legend.mkv` cold | 0:10 / 45:44 | 25.1 | 257 | 254 | 255/255 |
108+
| `legend.mkv` forward | 30:15 / 45:44 | 25.1 | 769 | 766 | 767/767 |
109+
| `legend.mkv` backward | 6:05 / 45:44 | 25.0 | 1,312 | 1,307 | 1,308/1,308 |
110+
| `wednesday.mp4` cold | 0:10 / 59:28 | 24.1 | 244 | 239 | 240/240 |
111+
| `wednesday.mp4` forward | 39:15 / 59:28 | 24.1 | 787 | 782 | 783/783 |
112+
| `wednesday.mp4` backward | 7:49 / 59:28 | 24.0 | 1,254 | 1,247 | 1,249/1,249 |
113+
114+
An additional Wednesday stress run completed eight forward/backward pairs.
115+
After the last seek it remained Loaded at 24.1 FPS; hardware decode/presentation
116+
rose from 2,163/2,113 to 2,354/2,304 during the next five seconds, native
117+
release and callback stayed balanced at 2,311/2,311, and Player drops remained
118+
two. The final process emitted no seek timeout, media error, `THREAD_BLOCK`, or
119+
crash evidence.

0 commit comments

Comments
 (0)