diff --git a/core/display.js b/core/display.js index e6239f48..d9f66d7f 100644 --- a/core/display.js +++ b/core/display.js @@ -20,6 +20,7 @@ this._c_forceCanvas = false; this._renderQ = []; // queue drawing actions for in-oder rendering + this._flushing = false; // the full frame buffer (logical canvas) size this._fb_width = 0; @@ -44,7 +45,8 @@ 'colourMap': [], 'scale': 1.0, 'viewport': false, - 'render_mode': '' + 'render_mode': '', + "onFlush": function () {}, }); Util.Debug(">> Display.constructor"); @@ -363,9 +365,21 @@ this._renderQ = []; }, + pending: function() { + return this._renderQ.length > 0; + }, + + flush: function() { + if (this._renderQ.length === 0) { + this._onFlush(); + } else { + this._flushing = true; + } + }, + fillRect: function (x, y, width, height, color, from_queue) { if (this._renderQ.length !== 0 && !from_queue) { - this.renderQ_push({ + this._renderQ_push({ 'type': 'fill', 'x': x, 'y': y, @@ -381,7 +395,7 @@ copyImage: function (old_x, old_y, new_x, new_y, w, h, from_queue) { if (this._renderQ.length !== 0 && !from_queue) { - this.renderQ_push({ + this._renderQ_push({ 'type': 'copy', 'old_x': old_x, 'old_y': old_y, @@ -400,6 +414,17 @@ } }, + imageRect: function(x, y, mime, arr) { + var img = new Image(); + img.src = "data: " + mime + ";base64," + Base64.encode(arr); + this._renderQ_push({ + 'type': 'img', + 'img': img, + 'x': x, + 'y': y + }); + }, + // start updating a tile startTile: function (x, y, width, height, color) { this._tile_x = x; @@ -480,7 +505,7 @@ // this probably isn't getting called *nearly* as much var new_arr = new Uint8Array(width * height * 4); new_arr.set(new Uint8Array(arr.buffer, 0, new_arr.length)); - this.renderQ_push({ + this._renderQ_push({ 'type': 'blit', 'data': new_arr, 'x': x, @@ -502,7 +527,7 @@ // this probably isn't getting called *nearly* as much var new_arr = new Uint8Array(width * height * 3); new_arr.set(new Uint8Array(arr.buffer, 0, new_arr.length)); - this.renderQ_push({ + this._renderQ_push({ 'type': 'blitRgb', 'data': new_arr, 'x': x, @@ -525,7 +550,7 @@ // this probably isn't getting called *nearly* as much var new_arr = new Uint8Array(width * height * 4); new_arr.set(new Uint8Array(arr.buffer, 0, new_arr.length)); - this.renderQ_push({ + this._renderQ_push({ 'type': 'blitRgbx', 'data': new_arr, 'x': x, @@ -552,16 +577,6 @@ this._drawCtx.drawImage(img, x - this._viewportLoc.x, y - this._viewportLoc.y); }, - renderQ_push: function (action) { - this._renderQ.push(action); - if (this._renderQ.length === 1) { - // If this can be rendered immediately it will be, otherwise - // the scanner will start polling the queue (every - // requestAnimationFrame interval) - this._scan_renderQ(); - } - }, - changeCursor: function (pixels, mask, hotx, hoty, w, h) { if (this._cursor_uri === false) { Util.Warn("changeCursor called but no cursor data URI support"); @@ -741,6 +756,22 @@ this._drawCtx.putImageData(img, x - vx, y - vy); }, + _renderQ_push: function (action) { + this._renderQ.push(action); + if (this._renderQ.length === 1) { + // If this can be rendered immediately it will be, otherwise + // the scanner will wait for the relevant event + this._scan_renderQ(); + } + }, + + _resume_renderQ: function() { + // "this" is the object that is ready, not the + // display object + this.removeEventListener('load', this._noVNC_display._resume_renderQ); + this._noVNC_display._scan_renderQ(); + }, + _scan_renderQ: function () { var ready = true; while (ready && this._renderQ.length > 0) { @@ -765,6 +796,8 @@ if (a.img.complete) { this.drawImage(a.img, a.x, a.y); } else { + a.img._noVNC_display = this; + a.img.addEventListener('load', this._resume_renderQ); // We need to wait for this image to 'load' // to keep things in-order ready = false; @@ -777,8 +810,9 @@ } } - if (this._renderQ.length > 0) { - requestAnimationFrame(this._scan_renderQ.bind(this)); + if (this._renderQ.length === 0 && this._flushing) { + this._flushing = false; + this._onFlush(); } }, }; @@ -799,7 +833,9 @@ ['render_mode', 'ro', 'str'], // Canvas rendering mode (read-only) ['prefer_js', 'rw', 'str'], // Prefer Javascript over canvas methods - ['cursor_uri', 'rw', 'raw'] // Can we render cursor using data URI + ['cursor_uri', 'rw', 'raw'], // Can we render cursor using data URI + + ['onFlush', 'rw', 'func'], // onFlush(): A flush request has finished ]); // Class Methods diff --git a/core/rfb.js b/core/rfb.js index 7f8ae6d0..224e822b 100644 --- a/core/rfb.js +++ b/core/rfb.js @@ -78,10 +78,10 @@ this._sock = null; // Websock object this._display = null; // Display object + this._flushing = false; // Display flushing state this._keyboard = null; // Keyboard input handler object this._mouse = null; // Mouse input handler object this._disconnTimer = null; // disconnection timer - this._msgTimer = null; // queued handle_msg timer this._supportsFence = false; @@ -190,7 +190,8 @@ // NB: nothing that needs explicit teardown should be done // before this point, since this can throw an exception try { - this._display = new Display({target: this._target}); + this._display = new Display({target: this._target, + onFlush: this._onFlush.bind(this)}); } catch (exc) { Util.Error("Display exception: " + exc); throw exc; @@ -415,11 +416,6 @@ }, _cleanup: function () { - if (this._msgTimer) { - clearInterval(this._msgTimer); - this._msgTimer = null; - } - if (this._display && this._display.get_context()) { if (!this._view_only) { this._keyboard.ungrab(); } if (!this._view_only) { this._mouse.ungrab(); } @@ -573,17 +569,15 @@ Util.Error("Got data while disconnected"); break; case 'connected': - if (this._normal_msg() && this._sock.rQlen() > 0) { - // true means we can continue processing - // Give other events a chance to run - if (this._msgTimer === null) { - Util.Debug("More data to process, creating timer"); - this._msgTimer = setTimeout(function () { - this._msgTimer = null; - this._handle_message(); - }.bind(this), 0); - } else { - Util.Debug("More data to process, existing timer"); + while (true) { + if (this._flushing) { + break; + } + if (!this._normal_msg()) { + break; + } + if (this._sock.rQlen() === 0) { + break; } } break; @@ -1250,6 +1244,14 @@ } }, + _onFlush: function() { + this._flushing = false; + // Resume processing + if (this._sock.rQlen() > 0) { + this._handle_message(); + } + }, + _framebufferUpdate: function () { var ret = true; var now; @@ -1264,6 +1266,14 @@ now = (new Date()).getTime(); Util.Info("First FBU latency: " + (now - this._timing.fbu_rt_start)); } + + // Make sure the previous frame is fully rendered first + // to avoid building up an excessive queue + if (this._display.pending()) { + this._flushing = true; + this._display.flush(); + return false; + } } while (this._FBU.rects > 0) { @@ -1710,10 +1720,6 @@ return (new DES(passwd)).encrypt(challenge); }; - RFB.extract_data_uri = function (arr) { - return ";base64," + Base64.encode(arr); - }; - RFB.encodingHandlers = { RAW: function () { if (this._FBU.lines === 0) { @@ -2216,16 +2222,8 @@ // We have everything, render it this._sock.rQskipBytes(1 + cl_header); // shift off clt + compact length - var img = new Image(); - img.src = "data: image/" + cmode + - RFB.extract_data_uri(this._sock.rQshiftBytes(cl_data)); - this._display.renderQ_push({ - 'type': 'img', - 'img': img, - 'x': this._FBU.x, - 'y': this._FBU.y - }); - img = null; + data = this._sock.rQshiftBytes(cl_data); + this._display.imageRect(this._FBU.x, this._FBU.y, "image/" + cmode, data); break; case "filter": var filterId = rQ[rQi + 1]; diff --git a/tests/test.display.js b/tests/test.display.js index 7c7c693b..3c7a28fc 100644 --- a/tests/test.display.js +++ b/tests/test.display.js @@ -384,11 +384,6 @@ describe('Display/Canvas Helper', function () { display = new Display({ target: document.createElement('canvas'), prefer_js: false }); display.resize(4, 4); sinon.spy(display, '_scan_renderQ'); - this.old_requestAnimationFrame = window.requestAnimationFrame; - window.requestAnimationFrame = function (cb) { - this.next_frame_cb = cb; - }.bind(this); - this.next_frame = function () { this.next_frame_cb(); }; }); afterEach(function () { @@ -396,18 +391,18 @@ describe('Display/Canvas Helper', function () { }); it('should try to process an item when it is pushed on, if nothing else is on the queue', function () { - display.renderQ_push({ type: 'noop' }); // does nothing + display._renderQ_push({ type: 'noop' }); // does nothing expect(display._scan_renderQ).to.have.been.calledOnce; }); it('should not try to process an item when it is pushed on if we are waiting for other items', function () { display._renderQ.length = 2; - display.renderQ_push({ type: 'noop' }); + display._renderQ_push({ type: 'noop' }); expect(display._scan_renderQ).to.not.have.been.called; }); it('should wait until an image is loaded to attempt to draw it and the rest of the queue', function () { - var img = { complete: false }; + var img = { complete: false, addEventListener: sinon.spy() } display._renderQ = [{ type: 'img', x: 3, y: 4, img: img }, { type: 'fill', x: 1, y: 2, width: 3, height: 4, color: 5 }]; display.drawImage = sinon.spy(); @@ -416,44 +411,46 @@ describe('Display/Canvas Helper', function () { display._scan_renderQ(); expect(display.drawImage).to.not.have.been.called; expect(display.fillRect).to.not.have.been.called; + expect(img.addEventListener).to.have.been.calledOnce; display._renderQ[0].img.complete = true; - this.next_frame(); + display._scan_renderQ(); expect(display.drawImage).to.have.been.calledOnce; expect(display.fillRect).to.have.been.calledOnce; + expect(img.addEventListener).to.have.been.calledOnce; }); it('should draw a blit image on type "blit"', function () { display.blitImage = sinon.spy(); - display.renderQ_push({ type: 'blit', x: 3, y: 4, width: 5, height: 6, data: [7, 8, 9] }); + display._renderQ_push({ type: 'blit', x: 3, y: 4, width: 5, height: 6, data: [7, 8, 9] }); expect(display.blitImage).to.have.been.calledOnce; expect(display.blitImage).to.have.been.calledWith(3, 4, 5, 6, [7, 8, 9], 0); }); it('should draw a blit RGB image on type "blitRgb"', function () { display.blitRgbImage = sinon.spy(); - display.renderQ_push({ type: 'blitRgb', x: 3, y: 4, width: 5, height: 6, data: [7, 8, 9] }); + display._renderQ_push({ type: 'blitRgb', x: 3, y: 4, width: 5, height: 6, data: [7, 8, 9] }); expect(display.blitRgbImage).to.have.been.calledOnce; expect(display.blitRgbImage).to.have.been.calledWith(3, 4, 5, 6, [7, 8, 9], 0); }); it('should copy a region on type "copy"', function () { display.copyImage = sinon.spy(); - display.renderQ_push({ type: 'copy', x: 3, y: 4, width: 5, height: 6, old_x: 7, old_y: 8 }); + display._renderQ_push({ type: 'copy', x: 3, y: 4, width: 5, height: 6, old_x: 7, old_y: 8 }); expect(display.copyImage).to.have.been.calledOnce; expect(display.copyImage).to.have.been.calledWith(7, 8, 3, 4, 5, 6); }); it('should fill a rect with a given color on type "fill"', function () { display.fillRect = sinon.spy(); - display.renderQ_push({ type: 'fill', x: 3, y: 4, width: 5, height: 6, color: [7, 8, 9]}); + display._renderQ_push({ type: 'fill', x: 3, y: 4, width: 5, height: 6, color: [7, 8, 9]}); expect(display.fillRect).to.have.been.calledOnce; expect(display.fillRect).to.have.been.calledWith(3, 4, 5, 6, [7, 8, 9]); }); it('should draw an image from an image object on type "img" (if complete)', function () { display.drawImage = sinon.spy(); - display.renderQ_push({ type: 'img', x: 3, y: 4, img: { complete: true } }); + display._renderQ_push({ type: 'img', x: 3, y: 4, img: { complete: true } }); expect(display.drawImage).to.have.been.calledOnce; expect(display.drawImage).to.have.been.calledWith({ complete: true }, 3, 4); }); diff --git a/tests/test.rfb.js b/tests/test.rfb.js index 43633da2..b8bc9d22 100644 --- a/tests/test.rfb.js +++ b/tests/test.rfb.js @@ -1274,7 +1274,7 @@ describe('Remote Frame Buffer Protocol Client', function() { client._sock._websocket._receive_data(new Uint8Array([0, 0, 0, 3])); expect(client._sock._websocket._get_sent_data()).to.have.length(0); - client._framebufferUpdate = function () { return true; }; // we magically have enough data + client._framebufferUpdate = function () { this._sock.rQskip8(); return true; }; // we magically have enough data // 247 should *not* be used as the message type here client._sock._websocket._receive_data(new Uint8Array([247])); expect(client._sock).to.have.sent(expected_msg._sQ); @@ -2080,14 +2080,12 @@ describe('Remote Frame Buffer Protocol Client', function() { expect(client._init_msg).to.have.been.calledOnce; }); - it('should split up the handling of muplitle normal messages across 10ms intervals', function () { + it('should process all normal messages directly', function () { client.connect('host', 8675); client._sock._websocket._open(); client._rfb_connection_state = 'connected'; client.set_onBell(sinon.spy()); client._sock._websocket._receive_data(new Uint8Array([0x02, 0x02])); - expect(client.get_onBell()).to.have.been.calledOnce; - this.clock.tick(20); expect(client.get_onBell()).to.have.been.calledTwice; });