fix(display): prevent render queue deadlock with video decoder pipeline

The render queue blocks on unready video frames and triggers the
`_flushing` mechanism in rfb.js, which stops all VNC message
processing. This starves the VideoDecoder of input, preventing it
from producing output — creating a deadlock where the queue waits
for decoder output that can never arrive.

Video frames now skip the queue blocking and draw asynchronously
via the decoder's output callback. The flip operation waits for all
pending frames to resolve before executing, preserving visual
correctness without blocking the decoder input pipeline.
This commit is contained in:
keradoxchen 2026-02-09 02:38:21 +00:00
parent 3b76636bdf
commit 5f71939e0d
1 changed files with 44 additions and 25 deletions

View File

@ -16,6 +16,7 @@ export default class Display {
this._renderQ = []; // queue drawing actions for in-order rendering this._renderQ = []; // queue drawing actions for in-order rendering
this._flushPromise = null; this._flushPromise = null;
this._pendingFrames = []; // video frames awaiting decoder output
// the full frame buffer (logical canvas) size // the full frame buffer (logical canvas) size
this._fbWidth = 0; this._fbWidth = 0;
@ -479,6 +480,22 @@ export default class Display {
} }
} }
_drawVideoFrame(pendingFrame, rect) {
let frame = pendingFrame.frame;
if (!frame) {
return;
}
if (frame.codedWidth < rect.width || frame.codedHeight < rect.height) {
Log.Warn("Decoded video frame does not cover its full rectangle area. Expecting at least " +
rect.width + "x" + rect.height + " but got " +
frame.codedWidth + "x" + frame.codedHeight);
}
this.drawImage(frame,
0, 0, rect.width, rect.height,
rect.x, rect.y, rect.width, rect.height);
frame.close();
}
_renderQPush(action) { _renderQPush(action) {
this._renderQ.push(action); this._renderQ.push(action);
if (this._renderQ.length === 1) { if (this._renderQ.length === 1) {
@ -501,7 +518,21 @@ export default class Display {
const a = this._renderQ[0]; const a = this._renderQ[0];
switch (a.type) { switch (a.type) {
case 'flip': case 'flip':
this.flip(true); if (this._pendingFrames.length > 0) {
// Wait for all pending video frames to be
// decoded before flipping, so they are
// visible on screen.
let display = this;
let frames = this._pendingFrames;
this._pendingFrames = [];
Promise.all(frames.map(f => f.promise)).then(() => {
display.flip(true);
display._scanRenderQ();
});
ready = false;
} else {
this.flip(true);
}
break; break;
case 'copy': case 'copy':
this.copyImage(a.oldX, a.oldY, a.x, a.y, a.width, a.height, true); this.copyImage(a.oldX, a.oldY, a.x, a.y, a.width, a.height, true);
@ -533,32 +564,20 @@ export default class Display {
} }
break; break;
case 'frame': case 'frame':
if (a.frame.ready) { if (!a.frame.ready) {
// The encoded frame may be larger than the rect due to // Don't block the queue — the video decoder
// limitations of the encoder, so we need to crop the // pipeline needs continued input to produce
// frame. // output. Register a callback to draw later,
let frame = a.frame.frame; // and let the queue keep feeding the decoder.
if (frame.codedWidth < a.width || frame.codedHeight < a.height) {
Log.Warn("Decoded video frame does not cover its full rectangle area. Expecting at least " +
a.width + "x" + a.height + " but got " +
frame.codedWidth + "x" + frame.codedHeight);
}
const sx = 0;
const sy = 0;
const sw = a.width;
const sh = a.height;
const dx = a.x;
const dy = a.y;
const dw = sw;
const dh = sh;
this.drawImage(frame, sx, sy, sw, sh, dx, dy, dw, dh);
frame.close();
} else {
let display = this; let display = this;
a.frame.promise.then(() => { let pendingFrame = a.frame;
display._scanRenderQ(); let rect = { x: a.x, y: a.y, width: a.width, height: a.height };
pendingFrame.promise.then(() => {
display._drawVideoFrame(pendingFrame, rect);
}); });
ready = false; this._pendingFrames.push(pendingFrame);
} else {
this._drawVideoFrame(a.frame, a);
} }
break; break;
} }