From 9f240fc7b1c0407527921f9b9802b99cfe4cfd1a Mon Sep 17 00:00:00 2001 From: Matt McClaskey Date: Wed, 2 Nov 2022 07:01:54 -0400 Subject: [PATCH] Major refactor of display class, support for QOI (#43) * Major refactor of display.js queue to assume async rect processing in order to accommodate threaded decode or udp * QOI lossless decoder on worker threads using WASM * rfb.js class to provide frame_id and rect counts per frame Co-authored-by: ryan.kuba Co-authored-by: matt --- app/ui.js | 43 ++++ core/decoders/copyrect.js | 5 +- core/decoders/hextile.js | 12 +- core/decoders/qoi/decoder.js | 342 +++++++++++++++++++++++++++ core/decoders/qoi/qoi_viewer_bg.wasm | Bin 0 -> 37493 bytes core/decoders/raw.js | 4 +- core/decoders/rre.js | 4 +- core/decoders/tight.js | 171 +++++++++++--- core/decoders/tightpng.js | 4 +- core/decoders/udp.js | 2 +- core/display.js | 324 ++++++++++++++++--------- core/encodings.js | 1 + core/rfb.js | 73 ++++-- vnc.html | 6 + 14 files changed, 815 insertions(+), 176 deletions(-) create mode 100644 core/decoders/qoi/decoder.js create mode 100644 core/decoders/qoi/qoi_viewer_bg.wasm diff --git a/app/ui.js b/app/ui.js index 206018ab..d253ce24 100644 --- a/app/ui.js +++ b/app/ui.js @@ -245,6 +245,7 @@ const UI = { UI.initSetting('enable_perf_stats', false); UI.initSetting('virtual_keyboard_visible', false); UI.initSetting('enable_ime', false); + UI.initSetting('enable_qoi', false); UI.initSetting('enable_webrtc', false); UI.toggleKeyboardControls(); @@ -558,6 +559,8 @@ const UI = { UI.addSettingChangeHandler('virtual_keyboard_visible', UI.toggleKeyboardControls); UI.addSettingChangeHandler('enable_ime'); UI.addSettingChangeHandler('enable_ime', UI.toggleIMEMode); + UI.addSettingChangeHandler('enable_qoi'); + UI.addSettingChangeHandler('enable_qoi', UI.toggleQOI); UI.addSettingChangeHandler('enable_webrtc'); UI.addSettingChangeHandler('enable_webrtc', UI.toggleWebRTC); }, @@ -1399,6 +1402,7 @@ const UI = { UI.rfb.clipboardSeamless = UI.getSetting('clipboard_seamless'); UI.rfb.keyboard.enableIME = UI.getSetting('enable_ime'); UI.rfb.clipboardBinary = supportsBinaryClipboard() && UI.rfb.clipboardSeamless; + UI.rfb.enableQOI = UI.getSetting('enable_qoi'); UI.rfb.enableWebRTC = UI.getSetting('enable_webrtc'); UI.rfb.mouseButtonMapper = UI.initMouseButtonMapper(); @@ -1651,6 +1655,18 @@ const UI = { UI.toggleIMEMode(); } break; + case 'disable_qoi': + if(UI.getSetting('enable_qoi')) { + UI.forceSetting('enable_qoi', false, false); + } + UI.toggleQOI(); + break; + case 'enable_qoi': + if(!UI.getSetting('enable_qoi')) { + UI.forceSetting('enable_qoi', true, false); + } + UI.toggleQOI(); + break; case 'enable_webrtc': if (!UI.getSetting('enable_webrtc')) { UI.forceSetting('enable_webrtc', true, false); @@ -1961,6 +1977,8 @@ const UI = { UI.enableSetting('video_out_time'); UI.showStatus("Refresh or reconnect to apply changes."); return; + case 5: //extreme+lossless + UI.forceSetting('enable_qoi', true, false); case 4: //extreme UI.forceSetting('dynamic_quality_min', 9); UI.forceSetting('dynamic_quality_max', 9); @@ -2024,6 +2042,12 @@ const UI = { break; } + //force QOI off if mode is below extreme + if (present_mode !== 4 && UI.getSetting('enable_qoi')) { + UI.showStatus("Lossless QOI disabled when not in extreme quality mode."); + UI.forceSetting('enable_qoi', false, false); + } + if (UI.rfb) { UI.rfb.qualityLevel = parseInt(UI.getSetting('quality')); UI.rfb.antiAliasing = parseInt(UI.getSetting('anti_aliasing')); @@ -2041,6 +2065,7 @@ const UI = { UI.rfb.frameRate = parseInt(UI.getSetting('framerate')); UI.rfb.enableWebP = UI.getSetting('enable_webp'); UI.rfb.videoQuality = parseInt(UI.getSetting('video_quality')); + UI.rfb.enableQOI = UI.getSetting('enable_qoi'); // Gracefully update settings server side UI.rfb.updateConnectionSettings(); @@ -2096,9 +2121,27 @@ const UI = { } else { UI.rfb.enableWebRTC = false; } + UI.updateQuality(); } }, + toggleQOI() { + if(UI.rfb) { + if(UI.getSetting('enable_qoi')) { + UI.rfb.enableQOI = true; + if (!UI.rfb.enableQOI) { + UI.showStatus("Enabling QOI failed, browser may not be compatible with WASM and/or Workers."); + UI.forceSetting('enable_qoi', false, false); + return; + } + UI.forceSetting('video_quality', 4, false); //force into extreme quality mode + } else { + UI.rfb.enableQOI = false; + } + UI.updateQuality(); + } + }, + showKeyboardControls() { document.getElementById('noVNC_keyboard_control').classList.add("is-visible"); }, diff --git a/core/decoders/copyrect.js b/core/decoders/copyrect.js index 9e6391a1..55fd1b34 100644 --- a/core/decoders/copyrect.js +++ b/core/decoders/copyrect.js @@ -8,7 +8,8 @@ */ export default class CopyRectDecoder { - decodeRect(x, y, width, height, sock, display, depth) { + + decodeRect(x, y, width, height, sock, display, depth, frame_id) { if (sock.rQwait("COPYRECT", 4)) { return false; } @@ -20,7 +21,7 @@ export default class CopyRectDecoder { return true; } - display.copyImage(deltaX, deltaY, x, y, width, height); + display.copyImage(deltaX, deltaY, x, y, width, height, frame_id); return true; } diff --git a/core/decoders/hextile.js b/core/decoders/hextile.js index ac21eff0..557e1036 100644 --- a/core/decoders/hextile.js +++ b/core/decoders/hextile.js @@ -16,7 +16,7 @@ export default class HextileDecoder { this._tileBuffer = new Uint8Array(16 * 16 * 4); } - decodeRect(x, y, width, height, sock, display, depth) { + decodeRect(x, y, width, height, sock, display, depth, frame_id) { if (this._tiles === 0) { this._tilesX = Math.ceil(width / 16); this._tilesY = Math.ceil(height / 16); @@ -85,7 +85,7 @@ export default class HextileDecoder { // Weird: ignore blanks are RAW Log.Debug(" Ignoring blank after RAW"); } else { - display.fillRect(tx, ty, tw, th, this._background); + display.fillRect(tx, ty, tw, th, this._background, frame_id); } } else if (subencoding & 0x01) { // Raw let pixels = tw * th; @@ -93,7 +93,7 @@ export default class HextileDecoder { for (let i = 0;i < pixels;i++) { rQ[rQi + i * 4 + 3] = 255; } - display.blitImage(tx, ty, tw, th, rQ, rQi); + display.blitImage(tx, ty, tw, th, rQ, rQi, frame_id); rQi += bytes - 1; } else { if (subencoding & 0x02) { // Background @@ -131,7 +131,7 @@ export default class HextileDecoder { this._subTile(sx, sy, sw, sh, color); } } - this._finishTile(display); + this._finishTile(display, frame_id); } sock.rQi = rQi; this._lastsubencoding = subencoding; @@ -183,9 +183,9 @@ export default class HextileDecoder { } // draw the current tile to the screen - _finishTile(display) { + _finishTile(display, frame_id) { display.blitImage(this._tileX, this._tileY, this._tileW, this._tileH, - this._tileBuffer, 0); + this._tileBuffer, 0, frame_id); } } diff --git a/core/decoders/qoi/decoder.js b/core/decoders/qoi/decoder.js new file mode 100644 index 00000000..eab72f13 --- /dev/null +++ b/core/decoders/qoi/decoder.js @@ -0,0 +1,342 @@ +let wasm; +const heap = new Array(32).fill(undefined); +heap.push(undefined, null, true, false); + +function getObject(idx) { + return heap[idx]; +} +let heap_next = heap.length; + +function dropObject(idx) { + if (idx < 36) return; + heap[idx] = heap_next; + heap_next = idx; +} + +function takeObject(idx) { + const ret = getObject(idx); + dropObject(idx); + return ret; +} +const cachedTextDecoder = new TextDecoder('utf-8', { + ignoreBOM: true, + fatal: true +}); +cachedTextDecoder.decode(); +let cachegetUint8Memory0 = null; + +function getUint8Memory0() { + if (cachegetUint8Memory0 === null || cachegetUint8Memory0.buffer !== wasm.memory.buffer) { + cachegetUint8Memory0 = new Uint8Array(wasm.memory.buffer); + } + return cachegetUint8Memory0; +} + +function getStringFromWasm0(ptr, len) { + return cachedTextDecoder.decode(getUint8Memory0().subarray(ptr, ptr + len)); +} + +let cachegetInt32Memory0 = null; + +function getInt32Memory0() { + if (cachegetInt32Memory0 === null || cachegetInt32Memory0.buffer !== wasm.memory.buffer) { + cachegetInt32Memory0 = new Int32Array(wasm.memory.buffer); + } + return cachegetInt32Memory0; +} + +function getArrayU8FromWasm0(ptr, len) { + return getUint8Memory0().subarray(ptr / 1, ptr / 1 + len); +} + +let WASM_VECTOR_LEN = 0; + +function passArray8ToWasm0(arg, malloc) { + const ptr = malloc(arg.length * 1); + getUint8Memory0().set(arg, ptr / 1); + WASM_VECTOR_LEN = arg.length; + return ptr; +} +/** + * @param {Uint8Array} bytes + * @returns {ImageData} + */ +function decode_qoi(bytes) { + try { + const retptr = wasm.__wbindgen_add_to_stack_pointer(-16); + const ptr0 = passArray8ToWasm0(bytes, wasm.__wbindgen_malloc); + const len0 = WASM_VECTOR_LEN; + wasm.decode_qoi(retptr, ptr0, len0); + var r0 = getInt32Memory0()[retptr / 4 + 0]; + var r1 = getInt32Memory0()[retptr / 4 + 1]; + var r2 = getInt32Memory0()[retptr / 4 + 2]; + if (r2) { + throw takeObject(r1); + } + return takeObject(r0); + } finally { + wasm.__wbindgen_add_to_stack_pointer(16); + } +} + +function addHeapObject(obj) { + if (heap_next === heap.length) heap.push(heap.length + 1); + const idx = heap_next; + heap_next = heap[idx]; + + heap[idx] = obj; + return idx; +} + +const cachedTextEncoder = new TextEncoder('utf-8'); + +const encodeString = (typeof cachedTextEncoder.encodeInto === 'function' ? + function(arg, view) { + return cachedTextEncoder.encodeInto(arg, view); + } : + function(arg, view) { + const buf = cachedTextEncoder.encode(arg); + view.set(buf); + return { + read: arg.length, + written: buf.length + }; + }); + +function passStringToWasm0(arg, malloc, realloc) { + + if (realloc === undefined) { + const buf = cachedTextEncoder.encode(arg); + const ptr = malloc(buf.length); + getUint8Memory0().subarray(ptr, ptr + buf.length).set(buf); + WASM_VECTOR_LEN = buf.length; + return ptr; + } + + let len = arg.length; + let ptr = malloc(len); + + const mem = getUint8Memory0(); + + let offset = 0; + + for (; offset < len; offset++) { + const code = arg.charCodeAt(offset); + if (code > 0x7F) break; + mem[ptr + offset] = code; + } + + if (offset !== len) { + if (offset !== 0) { + arg = arg.slice(offset); + } + ptr = realloc(ptr, len, len = offset + arg.length * 3); + const view = getUint8Memory0().subarray(ptr + offset, ptr + len); + const ret = encodeString(arg, view); + + offset += ret.written; + } + + WASM_VECTOR_LEN = offset; + return ptr; +} + +let cachegetUint8ClampedMemory0 = null; + +function getUint8ClampedMemory0() { + if (cachegetUint8ClampedMemory0 === null || cachegetUint8ClampedMemory0.buffer !== wasm.memory.buffer) { + cachegetUint8ClampedMemory0 = new Uint8ClampedArray(wasm.memory.buffer); + } + return cachegetUint8ClampedMemory0; +} + +function getClampedArrayU8FromWasm0(ptr, len) { + return getUint8ClampedMemory0().subarray(ptr / 1, ptr / 1 + len); +} + +function handleError(f, args) { + try { + return f.apply(this, args); + } catch (e) { + wasm.__wbindgen_exn_store(addHeapObject(e)); + } +} +/** + */ +class QoiImage { + + static __wrap(ptr) { + const obj = Object.create(QoiImage.prototype); + obj.ptr = ptr; + + return obj; + } + + __destroy_into_raw() { + const ptr = this.ptr; + this.ptr = 0; + + return ptr; + } + + free() { + const ptr = this.__destroy_into_raw(); + wasm.__wbg_qoiimage_free(ptr); + } + /** + */ + constructor() { + const ret = wasm.qoiimage_new(); + return QoiImage.__wrap(ret); + } + /** + * @returns {number} + */ + get_width() { + const ret = wasm.qoiimage_get_width(this.ptr); + return ret >>> 0; + } + /** + * @returns {number} + */ + get_height() { + const ret = wasm.qoiimage_get_height(this.ptr); + return ret >>> 0; + } + /** + * @returns {number} + */ + get_channels() { + const ret = wasm.qoiimage_get_channels(this.ptr); + return ret; + } + /** + * @returns {Uint8Array} + */ + get_bytes() { + try { + const retptr = wasm.__wbindgen_add_to_stack_pointer(-16); + wasm.qoiimage_get_bytes(retptr, this.ptr); + var r0 = getInt32Memory0()[retptr / 4 + 0]; + var r1 = getInt32Memory0()[retptr / 4 + 1]; + var v0 = getArrayU8FromWasm0(r0, r1).slice(); + wasm.__wbindgen_free(r0, r1 * 1); + return v0; + } finally { + wasm.__wbindgen_add_to_stack_pointer(16); + } + } +} + +async function load(module, imports) { + if (typeof Response === 'function' && module instanceof Response) { + const bytes = await module.arrayBuffer(); + return await WebAssembly.instantiate(bytes, imports); + } else { + const instance = await WebAssembly.instantiate(module, imports); + + if (instance instanceof WebAssembly.Instance) { + return { + instance, + module + }; + + } else { + return instance; + } + } +} + +async function init(input) { + if (typeof input === 'undefined') { + input = '/core/decoders/qoi/qoi_viewer_bg.wasm'; + } + const imports = {}; + imports.wbg = {}; + imports.wbg.__wbg_new_693216e109162396 = function() { + const ret = new Error(); + return addHeapObject(ret); + }; + imports.wbg.__wbg_stack_0ddaca5d1abfb52f = function(arg0, arg1) { + const ret = getObject(arg1).stack; + const ptr0 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); + const len0 = WASM_VECTOR_LEN; + getInt32Memory0()[arg0 / 4 + 1] = len0; + getInt32Memory0()[arg0 / 4 + 0] = ptr0; + }; + imports.wbg.__wbg_error_09919627ac0992f5 = function(arg0, arg1) { + try { + console.error(getStringFromWasm0(arg0, arg1)); + } finally { + wasm.__wbindgen_free(arg0, arg1); + } + }; + imports.wbg.__wbindgen_object_drop_ref = function(arg0) { + takeObject(arg0); + }; + imports.wbg.__wbg_newwithu8clampedarrayandsh_87d2f0a48030f922 = function() { + return handleError(function(arg0, arg1, arg2, arg3) { + const ret = new ImageData(getClampedArrayU8FromWasm0(arg0, arg1), arg2 >>> 0, arg3 >>> 0); + return addHeapObject(ret); + }, arguments) + }; + imports.wbg.__wbindgen_throw = function(arg0, arg1) { + throw new Error(getStringFromWasm0(arg0, arg1)); + }; + if (typeof input === 'string' || (typeof Request === 'function' && input instanceof Request) || (typeof URL === 'function' && input instanceof URL)) { + input = fetch(input); + } + const { + instance, + module + } = await load(await input, imports); + wasm = instance.exports; + init.__wbindgen_wasm_module = module; + return wasm; +} + +var arr; + +async function run() { + self.addEventListener('message', function(evt) { + try { + let length = evt.data.length; + let data = new Uint8Array(evt.data.sab.slice(0, length)); + let resultData = decode_qoi(data); + if (!arr) { + arr = new Uint8Array(evt.data.sabR); + } + let lengthR = resultData.data.length; + arr.set(resultData.data); + let img = { + colorSpace: resultData.colorSpace, + width: resultData.width, + height: resultData.height + }; + self.postMessage({ + result: 0, + img: img, + length: lengthR, + width: evt.data.width, + height: evt.data.height, + x: evt.data.x, + y: evt.data.y, + frame_id: evt.data.frame_id + }); + } catch (err) { + self.postMessage({ + result: 2, + error: err + }); + } + }, false); + + await init(); + + //Send message that worker is ready + self.postMessage({ + result: 1 + }) +} + +run(); diff --git a/core/decoders/qoi/qoi_viewer_bg.wasm b/core/decoders/qoi/qoi_viewer_bg.wasm new file mode 100644 index 0000000000000000000000000000000000000000..1447352a15a4e6b69f6bc305da9772c7a2b0fbb7 GIT binary patch literal 37493 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 literal 0 HcmV?d00001 diff --git a/core/decoders/raw.js b/core/decoders/raw.js index e8ea178e..fd2a9209 100644 --- a/core/decoders/raw.js +++ b/core/decoders/raw.js @@ -12,7 +12,7 @@ export default class RawDecoder { this._lines = 0; } - decodeRect(x, y, width, height, sock, display, depth) { + decodeRect(x, y, width, height, sock, display, depth, frame_id) { if ((width === 0) || (height === 0)) { return true; } @@ -54,7 +54,7 @@ export default class RawDecoder { data[i * 4 + 3] = 255; } - display.blitImage(x, curY, width, currHeight, data, index); + display.blitImage(x, curY, width, currHeight, data, index, frame_id); sock.rQskipBytes(currHeight * bytesPerLine); this._lines -= currHeight; if (this._lines > 0) { diff --git a/core/decoders/rre.js b/core/decoders/rre.js index 6219369d..0562fbc2 100644 --- a/core/decoders/rre.js +++ b/core/decoders/rre.js @@ -12,7 +12,7 @@ export default class RREDecoder { this._subrects = 0; } - decodeRect(x, y, width, height, sock, display, depth) { + decodeRect(x, y, width, height, sock, display, depth, frame_id) { if (this._subrects === 0) { if (sock.rQwait("RRE", 4 + 4)) { return false; @@ -34,7 +34,7 @@ export default class RREDecoder { let sy = sock.rQshift16(); let swidth = sock.rQshift16(); let sheight = sock.rQshift16(); - display.fillRect(x + sx, y + sy, swidth, sheight, color); + display.fillRect(x + sx, y + sy, swidth, sheight, color, frame_id); this._subrects--; } diff --git a/core/decoders/tight.js b/core/decoders/tight.js index 6e2799e3..dd111ece 100644 --- a/core/decoders/tight.js +++ b/core/decoders/tight.js @@ -12,12 +12,14 @@ import * as Log from '../util/logging.js'; import Inflator from "../inflator.js"; export default class TightDecoder { - constructor() { + constructor(display) { this._ctl = null; this._filter = null; this._numColors = 0; this._palette = new Uint8Array(1024); // 256 * 4 (max palette size * max bytes-per-pixel) this._len = 0; + this._enableQOI = false; + this._displayGlobal = display; this._zlibs = []; for (let i = 0; i < 4; i++) { @@ -25,7 +27,14 @@ export default class TightDecoder { } } - decodeRect(x, y, width, height, sock, display, depth) { + enableQOI() { + if (!this._enableQOI) { + this._enableQOIWorkers(); + } + return this._enableQOI; //did it succeed + } + + decodeRect(x, y, width, height, sock, display, depth, frame_id) { if (this._ctl === null) { if (sock.rQwait("TIGHT compression-control", 1)) { return false; @@ -37,7 +46,7 @@ export default class TightDecoder { for (let i = 0; i < 4; i++) { if ((this._ctl >> i) & 1) { this._zlibs[i].reset(); - Log.Debug("Reset zlib stream " + i); + Log.Info("Reset zlib stream " + i); } } @@ -49,19 +58,22 @@ export default class TightDecoder { if (this._ctl === 0x08) { ret = this._fillRect(x, y, width, height, - sock, display, depth); + sock, display, depth, frame_id); } else if (this._ctl === 0x09) { ret = this._jpegRect(x, y, width, height, - sock, display, depth); + sock, display, depth, frame_id); } else if (this._ctl === 0x0A) { ret = this._pngRect(x, y, width, height, - sock, display, depth); + sock, display, depth, frame_id); } else if ((this._ctl & 0x08) == 0) { ret = this._basicRect(this._ctl, x, y, width, height, - sock, display, depth); + sock, display, depth, frame_id); } else if (this._ctl === 0x0B) { ret = this._webpRect(x, y, width, height, - sock, display, depth); + sock, display, depth, frame_id); + } else if (this._ctl === 0x0C) { + ret = this._qoiRect(x, y, width, height, + sock, display, depth, frame_id); } else { throw new Error("Illegal tight compression received (ctl: " + this._ctl + ")"); @@ -74,7 +86,7 @@ export default class TightDecoder { return ret; } - _fillRect(x, y, width, height, sock, display, depth) { + _fillRect(x, y, width, height, sock, display, depth, frame_id) { if (sock.rQwait("TIGHT", 3)) { return false; } @@ -83,39 +95,81 @@ export default class TightDecoder { const rQ = sock.rQ; display.fillRect(x, y, width, height, - [rQ[rQi], rQ[rQi + 1], rQ[rQi + 2]], false); + [rQ[rQi], rQ[rQi + 1], rQ[rQi + 2]], frame_id, false); sock.rQskipBytes(3); return true; } - _jpegRect(x, y, width, height, sock, display, depth) { + _jpegRect(x, y, width, height, sock, display, depth, frame_id) { let data = this._readData(sock); if (data === null) { return false; } - display.imageRect(x, y, width, height, "image/jpeg", data); + display.imageRect(x, y, width, height, "image/jpeg", data, frame_id); return true; } - _webpRect(x, y, width, height, sock, display, depth) { + _webpRect(x, y, width, height, sock, display, depth, frame_id) { let data = this._readData(sock); if (data === null) { return false; } - display.imageRect(x, y, width, height, "image/webp", data); + display.imageRect(x, y, width, height, "image/webp", data, frame_id); return true; } - _pngRect(x, y, width, height, sock, display, depth) { + _processRectQ() { + while (this._availableWorkers.length > 0 && this._qoiRects.length > 0) { + let i = this._availableWorkers.pop(); + let worker = this._workers[i]; + let rect = this._qoiRects.shift(); + this._arrs[i].set(rect.data); + worker.postMessage({ + length: rect.data.length, + x: rect.x, + y: rect.y, + width: rect.width, + height: rect.height, + depth: rect.depth, + sab: this._sabs[i], + sabR: this._sabsR[i], + frame_id: rect.frame_id + }); + } + } + + _qoiRect(x, y, width, height, sock, display, depth, frame_id) { + let data = this._readData(sock); + if (data === null) { + return false; + } + + if (this._enableQOI) { + let dataClone = new Uint8Array(data); + let item = {x: x,y: y,width: width,height: height,data: dataClone,depth: depth, frame_id: frame_id}; + if (this._qoiRects.length < 1000) { + this._qoiRects.push(item); + this._processRectQ(); + } else { + Log.Warn("QOI queue exceeded limit."); + this._qoiRects.splice(0, 500); + } + + } + + return true; + } + + _pngRect(x, y, width, height, sock, display, depth, frame_id) { throw new Error("PNG received in standard Tight rect"); } - _basicRect(ctl, x, y, width, height, sock, display, depth) { + _basicRect(ctl, x, y, width, height, sock, display, depth, frame_id) { if (this._filter === null) { if (ctl & 0x4) { if (sock.rQwait("TIGHT", 1)) { @@ -136,15 +190,15 @@ export default class TightDecoder { switch (this._filter) { case 0: // CopyFilter ret = this._copyFilter(streamId, x, y, width, height, - sock, display, depth); + sock, display, depth, frame_id); break; case 1: // PaletteFilter ret = this._paletteFilter(streamId, x, y, width, height, - sock, display, depth); + sock, display, depth, frame_id); break; case 2: // GradientFilter ret = this._gradientFilter(streamId, x, y, width, height, - sock, display, depth); + sock, display, depth, frame_id); break; default: throw new Error("Illegal tight filter received (ctl: " + @@ -158,7 +212,7 @@ export default class TightDecoder { return ret; } - _copyFilter(streamId, x, y, width, height, sock, display, depth) { + _copyFilter(streamId, x, y, width, height, sock, display, depth, frame_id) { const uncompressedSize = width * height * 3; let data; @@ -191,12 +245,12 @@ export default class TightDecoder { rgbx[i + 3] = 255; // Alpha } - display.blitImage(x, y, width, height, rgbx, 0, false); + display.blitImage(x, y, width, height, rgbx, 0, frame_id, false); return true; } - _paletteFilter(streamId, x, y, width, height, sock, display, depth) { + _paletteFilter(streamId, x, y, width, height, sock, display, depth, frame_id) { if (this._numColors === 0) { if (sock.rQwait("TIGHT palette", 1)) { return false; @@ -244,9 +298,9 @@ export default class TightDecoder { // Convert indexed (palette based) image data to RGB if (this._numColors == 2) { - this._monoRect(x, y, width, height, data, this._palette, display); + this._monoRect(x, y, width, height, data, this._palette, display, frame_id); } else { - this._paletteRect(x, y, width, height, data, this._palette, display); + this._paletteRect(x, y, width, height, data, this._palette, display, frame_id); } this._numColors = 0; @@ -254,7 +308,7 @@ export default class TightDecoder { return true; } - _monoRect(x, y, width, height, data, palette, display) { + _monoRect(x, y, width, height, data, palette, display, frame_id) { // Convert indexed (palette based) image data to RGB // TODO: reduce number of calculations inside loop const dest = this._getScratchBuffer(width * height * 4); @@ -284,10 +338,10 @@ export default class TightDecoder { } } - display.blitImage(x, y, width, height, dest, 0, false); + display.blitImage(x, y, width, height, dest, 0, frame_id, false); } - _paletteRect(x, y, width, height, data, palette, display) { + _paletteRect(x, y, width, height, data, palette, display, frame_id) { // Convert indexed (palette based) image data to RGB const dest = this._getScratchBuffer(width * height * 4); const total = width * height * 4; @@ -299,10 +353,10 @@ export default class TightDecoder { dest[i + 3] = 255; } - display.blitImage(x, y, width, height, dest, 0, false); + display.blitImage(x, y, width, height, dest, 0, frame_id, false); } - _gradientFilter(streamId, x, y, width, height, sock, display, depth) { + _gradientFilter(streamId, x, y, width, height, sock, display, depth, frame_id) { throw new Error("Gradient filter not implemented"); } @@ -342,4 +396,63 @@ export default class TightDecoder { } return this._scratchBuffer; } + + _enableQOIWorkers() { + let sabTest = typeof SharedArrayBuffer; + if (sabTest !== 'undefined') { + this._enableQOI = true; + if ((window.navigator.hardwareConcurrency) && (window.navigator.hardwareConcurrency > 8)) { + this._threads = window.navigator.hardwareConcurrency; + } else { + this._threads = 8; + } + this._workerEnabled = false; + this._workers = []; + this._availableWorkers = []; + this._sabs = []; + this._sabsR = []; + this._arrs = []; + this._arrsR = []; + this._qoiRects = []; + this._rectQlooping = false; + for (let i = 0; i < this._threads; i++) { + this._workers.push(new Worker("/core/decoders/qoi/decoder.js")); + this._sabs.push(new SharedArrayBuffer(300000)); + this._sabsR.push(new SharedArrayBuffer(400000)); + this._arrs.push(new Uint8Array(this._sabs[i])); + this._arrsR.push(new Uint8ClampedArray(this._sabsR[i])); + this._workers[i].onmessage = (evt) => { + this._availableWorkers.push(i); + switch(evt.data.result) { + case 0: + let data = new Uint8ClampedArray(evt.data.length); + data.set(this._arrsR[i].slice(0, evt.data.length)); + let img = new ImageData(data, evt.data.img.width, evt.data.img.height, {colorSpace: evt.data.img.colorSpace}); + + this._displayGlobal.blitQoi( + evt.data.x, + evt.data.y, + evt.data.width, + evt.data.height, + img, + 0, + evt.data.frame_id, + false + ); + this._processRectQ(); + break; + case 1: + Log.Info("QOI Worker is now available."); + break; + case 2: + Log.Info("Error on worker: " + evt.error); + break; + } + }; + } + } else { + this._enableQOI = false; + Log.Warn("Enabling QOI Failed, client not compatible."); + } + } } diff --git a/core/decoders/tightpng.js b/core/decoders/tightpng.js index 82f492de..9ed28b8e 100644 --- a/core/decoders/tightpng.js +++ b/core/decoders/tightpng.js @@ -10,13 +10,13 @@ import TightDecoder from './tight.js'; export default class TightPNGDecoder extends TightDecoder { - _pngRect(x, y, width, height, sock, display, depth) { + _pngRect(x, y, width, height, sock, display, depth, frame_id) { let data = this._readData(sock); if (data === null) { return false; } - display.imageRect(x, y, width, height, "image/png", data); + display.imageRect(x, y, width, height, "image/png", data, frame_id); return true; } diff --git a/core/decoders/udp.js b/core/decoders/udp.js index 13650f79..f59e8119 100644 --- a/core/decoders/udp.js +++ b/core/decoders/udp.js @@ -22,7 +22,7 @@ export default class UDPDecoder { } } - decodeRect(x, y, width, height, data, display, depth) { + decodeRect(x, y, width, height, data, display, depth, frame_id) { let ctl = data[12]; ctl = ctl >> 4; diff --git a/core/display.js b/core/display.js index 96be93cb..be4aeaf4 100644 --- a/core/display.js +++ b/core/display.js @@ -9,12 +9,31 @@ import * as Log from './util/logging.js'; import Base64 from "./base64.js"; import { toSigned32bit } from './util/int.js'; +import { isWindows } from './util/browser.js'; export default class Display { constructor(target) { - this._renderQ = []; // queue drawing actions for in-oder rendering - this._currentFrame = []; - this._nextFrame = []; + Log.Debug(">> Display.constructor"); + + /* + For performance reasons we use a multi dimensional array + 1st Dimension of Array Represents Frames, each element is a Frame + 2nd Dimension contains 4 elements + 0 - int, FrameID + 1 - int, Rect Count + 2 - Array of Rect objects + 3 - bool, is the frame complete + 4 - int, index of current rect (post-processing) + */ + this._asyncFrameQueue = []; + /* + Buffer for incoming frames. The larger the buffer the more time there is to collect, process, and order rects + but the more delay there is. May need to adjust this higher for lower power devices when UDP is complete. + Decoders that use WASM in parallel can also cause out of order rects + */ + this._maxAsyncFrameQueue = 3; + this._clearAsyncQueue(); + this._flushing = false; // the full frame buffer (logical canvas) size @@ -22,12 +41,7 @@ export default class Display { this._fbHeight = 0; this._renderMs = 0; - this._prevDrawStyle = ""; - - Log.Debug(">> Display.constructor"); - - // The visible canvas this._target = target; if (!this._target) { @@ -49,20 +63,22 @@ export default class Display { Log.Debug("User Agent: " + navigator.userAgent); - // performance metrics, try to calc a fps equivelant + // performance metrics this._flipCnt = 0; this._lastFlip = Date.now(); + this._droppedFrames = 0; + this._droppedRects = 0; + this._missingRectCnt = 0; setInterval(function() { let delta = Date.now() - this._lastFlip; if (delta > 0) { this._fps = (this._flipCnt / (delta / 1000)).toFixed(2); } + Log.Info('Dropped Frames: ' + this._droppedFrames + ' Dropped Rects: ' + this._droppedRects + ' Missing Rect Cnt: ' + this._missingRectCnt); this._flipCnt = 0; this._lastFlip = Date.now(); }.bind(this), 5000); - Log.Debug("<< Display.constructor"); - // ===== PROPERTIES ===== this._scale = 1.0; @@ -73,6 +89,11 @@ export default class Display { // ===== EVENT HANDLERS ===== this.onflush = () => { }; // A flush request has finished + + // Use requestAnimationFrame to write to canvas, to match display refresh rate + window.requestAnimationFrame( () => { this._pushAsyncFrame(); }); + + Log.Debug("<< Display.constructor"); } // ===== PROPERTIES ===== @@ -149,8 +170,6 @@ export default class Display { return; } Log.Debug("viewportChange deltaX: " + deltaX + ", deltaY: " + deltaY); - - this.flip(); } viewportChangeSize(width, height) { @@ -186,8 +205,6 @@ export default class Display { // The position might need to be updated if we've grown this.viewportChangePos(0, 0); - this.flip(); - // Update the visible size of the target canvas this._rescale(this._scale); } @@ -243,54 +260,37 @@ export default class Display { } // rendering canvas - flip(fromQueue) { - if (!fromQueue) { - this._renderQPush({ - 'type': 'flip' - }); - } else { - for (let i = 0; i < this._currentFrame.length; i++) { - const a = this._currentFrame[i]; - switch (a.type) { - case 'copy': - this.copyImage(a.oldX, a.oldY, a.x, a.y, a.width, a.height, true); - break; - case 'fill': - this.fillRect(a.x, a.y, a.width, a.height, a.color, true); - break; - case 'blit': - this.blitImage(a.x, a.y, a.width, a.height, a.data, 0, true); - break; - case 'img': - this.drawImage(a.img, a.x, a.y, a.width, a.height); - break; - } - } - this._flipCnt += 1; - } + flip(frame_id, rect_cnt) { + this._asyncRenderQPush({ + 'type': 'flip', + 'frame_id': frame_id, + 'rect_cnt': rect_cnt + }); } pending() { - return this._renderQ.length > 0; + //is the slot in the queue for the newest frame in use + return this._asyncFrameQueue[this._maxAsyncFrameQueue - 1][0] > 0; } flush() { - if (this._renderQ.length === 0) { - this.onflush(); - } else { - this._flushing = true; - } + //force oldest frame to render + this._asyncFrameComplete(0, true); + + //this in effect blocks more incoming frames until the oldest frame has been rendered to canvas (tcp only) + this._flushing = true; } - fillRect(x, y, width, height, color, fromQueue) { + fillRect(x, y, width, height, color, frame_id, fromQueue) { if (!fromQueue) { - this._renderQPush({ + this._asyncRenderQPush({ 'type': 'fill', 'x': x, 'y': y, 'width': width, 'height': height, - 'color': color + 'color': color, + 'frame_id': frame_id }); } else { this._setFillColor(color); @@ -298,9 +298,9 @@ export default class Display { } } - copyImage(oldX, oldY, newX, newY, w, h, fromQueue) { + copyImage(oldX, oldY, newX, newY, w, h, frame_id, fromQueue) { if (!fromQueue) { - this._renderQPush({ + this._asyncRenderQPush({ 'type': 'copy', 'oldX': oldX, 'oldY': oldY, @@ -308,6 +308,7 @@ export default class Display { 'y': newY, 'width': w, 'height': h, + 'frame_id': frame_id }); } else { // Due to this bug among others [1] we need to disable the image-smoothing to @@ -328,39 +329,40 @@ export default class Display { } } - imageRect(x, y, width, height, mime, arr) { + imageRect(x, y, width, height, mime, arr, frame_id) { /* The internal logic cannot handle empty images, so bail early */ if ((width === 0) || (height === 0)) { return; } - const img = new Image(); img.src = "data: " + mime + ";base64," + Base64.encode(arr); - this._renderQPush({ + this._asyncRenderQPush({ 'type': 'img', 'img': img, 'x': x, 'y': y, 'width': width, - 'height': height + 'height': height, + 'frame_id': frame_id }); } - blitImage(x, y, width, height, arr, offset, fromQueue) { + blitImage(x, y, width, height, arr, offset, frame_id, fromQueue) { if (!fromQueue) { // NB(directxman12): it's technically more performant here to use preallocated arrays, // but it's a lot of extra work for not a lot of payoff -- if we're using the render queue, // this probably isn't getting called *nearly* as much const newArr = new Uint8Array(width * height * 4); newArr.set(new Uint8Array(arr.buffer, 0, newArr.length)); - this._renderQPush({ + this._asyncRenderQPush({ 'type': 'blit', 'data': newArr, 'x': x, 'y': y, 'width': width, 'height': height, + 'frame_id': frame_id }); } else { // NB(directxman12): arr must be an Type Array view @@ -372,9 +374,25 @@ export default class Display { } } + blitQoi(x, y, width, height, arr, offset, frame_id, fromQueue) { + if (!fromQueue) { + this._asyncRenderQPush({ + 'type': 'blitQ', + 'data': arr, + 'x': x, + 'y': y, + 'width': width, + 'height': height, + 'frame_id': frame_id + }); + } else { + this._targetCtx.putImageData(arr, x, y); + } + } + drawImage(img, x, y, w, h) { try { - if (img.width != w || img.height != h) { + if (img.width != w || img.height != h) { this._targetCtx.drawImage(img, x, y, w, h); } else { this._targetCtx.drawImage(img, x, y); @@ -407,6 +425,144 @@ export default class Display { // ===== PRIVATE METHODS ===== + /* + Process incoming rects into a frame buffer, assume rects are out of order due to either UDP or parallel processing of decoding + */ + _asyncRenderQPush(rect) { + let frameIx = -1; + let oldestFrameID = Number.MAX_SAFE_INTEGER; + let newestFrameID = 0; + for (let i=0; i= 0) { + if (rect.type == "flip") { + //flip rect contains the rect count for the frame + this._asyncFrameQueue[frameIx][1] = rect.rect_cnt; + } + + if (this._asyncFrameQueue[frameIx][1] == this._asyncFrameQueue[frameIx][2].length) { + //frame is complete + this._asyncFrameComplete(frameIx); + } + } else { + if (rect.frame_id < oldestFrameID) { + //rect is older than any frame in the queue, drop it + this._droppedRects++; + return; + } else if (rect.frame_id > newestFrameID) { + //frame is newer than any frame in the queue, drop old frames + this._asyncFrameQueue.shift(); + let rect_cnt = ((rect.type == "flip") ? rect.rect_cnt : 0); + this._asyncFrameQueue.push([ rect.frame_id, rect_cnt, [ rect ], (rect_cnt == 1), 0 ]); + this._droppedFrames++; + } + } + + } + + /* + Clear the async frame buffer + */ + _clearAsyncQueue() { + this._droppedFrames += this._asyncFrameQueue.length; + + this._asyncFrameQueue = []; + for (let i=0; i { this._asyncFrameComplete(frameIx); }); + this._asyncFrameQueue[frameIx][4] = currentFrameRectIx; + return; + } + currentFrameRectIx++; + } + } + this._asyncFrameQueue[frameIx][4] = currentFrameRectIx; + this._asyncFrameQueue[frameIx][3] = true; + } + + /* + Push the oldest frame in the buffer to the canvas if it is marked ready + */ + _pushAsyncFrame() { + if (this._asyncFrameQueue[0][3]) { + let frame = this._asyncFrameQueue.shift()[2]; + this._asyncFrameQueue.push([ 0, 0, [], false, 0 ]); + + //render the selected frame + for (let i = 0; i < frame.length; i++) { + + const a = frame[i]; + switch (a.type) { + case 'copy': + this.copyImage(a.oldX, a.oldY, a.x, a.y, a.width, a.height, a.frame_id, true); + break; + case 'fill': + this.fillRect(a.x, a.y, a.width, a.height, a.color, a.frame_id, true); + break; + case 'blit': + this.blitImage(a.x, a.y, a.width, a.height, a.data, 0, a.frame_id, true); + break; + case 'blitQ': + this.blitQoi(a.x, a.y, a.width, a.height, a.data, 0, a.frame_id, true); + break; + case 'img': + this.drawImage(a.img, a.x, a.y, a.width, a.height); + break; + } + } + this._flipCnt += 1; + + if (this._flushing) { + this._flushing = false; + this.onflush(); + } + } + + window.requestAnimationFrame( () => { this._pushAsyncFrame(); }); + } + _rescale(factor) { this._scale = factor; const vp = this._viewportLoc; @@ -445,56 +601,4 @@ export default class Display { this._prevDrawStyle = newStyle; } } - - _renderQPush(action) { - this._renderQ.push(action); - if (this._renderQ.length === 1) { - this._scanRenderQ(); - } - } - - _resumeRenderQ() { - this.removeEventListener('load', this._noVNCDisplay._resumeRenderQ); - this._noVNCDisplay._scanRenderQ(); - } - - _scanRenderQ() { - let ready = true; - let before = Date.now(); - while (ready && this._renderQ.length > 0) { - const a = this._renderQ[0]; - switch (a.type) { - case 'flip': - this._currentFrame = this._nextFrame; - this._nextFrame = []; - this.flip(true); - break; - case 'img': - if (a.img.complete) { - this._nextFrame.push(a); - } else { - a.img._noVNCDisplay = this; - a.img.addEventListener('load', this._resumeRenderQ); - // We need to wait for this image to 'load' - ready = false; - } - break; - default: - this._nextFrame.push(a); - break; - } - - if (ready) { - this._renderQ.shift(); - } - } - - if (this._renderQ.length === 0 && this._flushing) { - this._flushing = false; - this.onflush(); - } - - let elapsed = Date.now() - before; - this._renderMs += elapsed; - } } diff --git a/core/encodings.js b/core/encodings.js index c89e3730..af2f4493 100644 --- a/core/encodings.js +++ b/core/encodings.js @@ -53,6 +53,7 @@ export const encodings = { pseudoEncodingVideoScalingLevel9: -1987, pseudoEncodingVideoOutTimeLevel1: -1986, pseudoEncodingVideoOutTimeLevel100: -1887, + pseudoEncodingQOI: -1886, pseudoEncodingVMwareCursor: 0x574d5664, pseudoEncodingVMwareCursorPosition: 0x574d5666, diff --git a/core/rfb.js b/core/rfb.js index 8b2ff466..721358cd 100644 --- a/core/rfb.js +++ b/core/rfb.js @@ -43,6 +43,7 @@ const DEFAULT_BACKGROUND = 'rgb(40, 40, 40)'; var _videoQuality = 2; var _enableWebP = false; +var _enableQOI = false; // Minimum wait (ms) between two mouse moves const MOUSE_MOVE_DELAY = 17; @@ -139,6 +140,7 @@ export default class RFB extends EventTargetMixin { this._maxVideoResolutionY = 540; this._clipboardBinary = true; this._useUdp = true; + this._enableQOI = false; this.TransitConnectionStates = { Tcp: Symbol("tcp"), Udp: Symbol("udp"), @@ -173,12 +175,14 @@ export default class RFB extends EventTargetMixin { this._decoders = {}; this._FBU = { - rects: 0, + rects: 0, // current rect number x: 0, y: 0, width: 0, height: 0, encoding: null, + frame_id: 0, + rect_total: 0, //Total rects in frame }; // Mouse state @@ -248,15 +252,6 @@ export default class RFB extends EventTargetMixin { // initial cursor instead. this._cursorImage = RFB.cursors.none; - // populate decoder array with objects - this._decoders[encodings.encodingRaw] = new RawDecoder(); - this._decoders[encodings.encodingCopyRect] = new CopyRectDecoder(); - this._decoders[encodings.encodingRRE] = new RREDecoder(); - this._decoders[encodings.encodingHextile] = new HextileDecoder(); - this._decoders[encodings.encodingTight] = new TightDecoder(); - this._decoders[encodings.encodingTightPNG] = new TightPNGDecoder(); - this._decoders[encodings.encodingUDP] = new UDPDecoder(); - // NB: nothing that needs explicit teardown should be done // before this point, since this can throw an exception try { @@ -267,6 +262,15 @@ export default class RFB extends EventTargetMixin { } this._display.onflush = this._onFlush.bind(this); + // populate decoder array with objects + this._decoders[encodings.encodingRaw] = new RawDecoder(); + this._decoders[encodings.encodingCopyRect] = new CopyRectDecoder(); + this._decoders[encodings.encodingRRE] = new RREDecoder(); + this._decoders[encodings.encodingHextile] = new HextileDecoder(); + this._decoders[encodings.encodingTight] = new TightDecoder(this._display); + this._decoders[encodings.encodingTightPNG] = new TightPNGDecoder(); + this._decoders[encodings.encodingUDP] = new UDPDecoder(); + this._keyboard = new Keyboard(this._canvas, touchInput); this._keyboard.onkeyevent = this._handleKeyEvent.bind(this); @@ -481,6 +485,21 @@ export default class RFB extends EventTargetMixin { this._pendingApplyEncodingChanges = true; } + get enableQOI() { return this._enableQOI; } + set enableQOI(enabled) { + if(this._enableQOI === enabled) { + return; + } + if (enabled) { + if (!this._decoders[encodings.encodingTight].enableQOI()) { + //enabling qoi failed + return; + } + } + this._enableQOI = enabled; + this._pendingApplyEncodingChanges = true; + } + get antiAliasing() { return this._display.antiAliasing; } set antiAliasing(value) { this._display.antiAliasing = value; @@ -2520,12 +2539,8 @@ export default class RFB extends EventTargetMixin { encs.push(encodings.encodingRaw); // Psuedo-encoding settings - var quality = 6; - var compression = 2; - var screensize = this._screenSize(false); encs.push(encodings.pseudoEncodingQualityLevel0 + this._qualityLevel); encs.push(encodings.pseudoEncodingCompressLevel0 + this._compressionLevel); - encs.push(encodings.pseudoEncodingDesktopSize); encs.push(encodings.pseudoEncodingLastRect); encs.push(encodings.pseudoEncodingQEMUExtendedKeyEvent); @@ -2537,6 +2552,9 @@ export default class RFB extends EventTargetMixin { encs.push(encodings.pseudoEncodingExtendedClipboard); if (this._hasWebp()) encs.push(encodings.pseudoEncodingWEBP); + if (this._enableQOI) + encs.push(encodings.pseudoEncodingQOI); + // kasm settings; the server may be configured to ignore these encs.push(encodings.pseudoEncodingJpegVideoQualityLevel0 + this.jpegVideoQuality); @@ -3053,11 +3071,11 @@ export default class RFB extends EventTargetMixin { encoding: parseInt((data[8] << 24) + (data[9] << 16) + (data[10] << 8) + data[11], 10) }; - + switch (frame.encoding) { case encodings.pseudoEncodingLastRect: if (document.visibilityState !== "hidden") { - this._display.flip(); + this._display.flip(false); //TODO: UDP is now broken, flip needs rect count and frame number this._udpBuffer.clear(); } break; @@ -3158,6 +3176,9 @@ export default class RFB extends EventTargetMixin { this._sock.rQskipBytes(1); // Padding this._FBU.rects = this._sock.rQshift16(); + this._FBU.frame_id++; + this._FBU.rect_total = 0; + // Make sure the previous frame is fully rendered first // to avoid building up an excessive queue if (this._display.pending()) { @@ -3180,7 +3201,8 @@ export default class RFB extends EventTargetMixin { this._FBU.encoding = parseInt((hdr[8] << 24) + (hdr[9] << 16) + (hdr[10] << 8) + hdr[11], 10); } - + + if (!this._handleRect()) { return false; } @@ -3189,14 +3211,17 @@ export default class RFB extends EventTargetMixin { this._FBU.encoding = null; } - this._display.flip(); - + if (this._FBU.rect_total > 0) { + this._display.flip(this._FBU.frame_id, this._FBU.rect_total); + } + return true; // We finished this FBU } _handleRect() { switch (this._FBU.encoding) { case encodings.pseudoEncodingLastRect: + this._FBU.rect_total++; //only track rendered rects and last rect this._FBU.rects = 1; // Will be decreased when we return return true; @@ -3224,7 +3249,11 @@ export default class RFB extends EventTargetMixin { return this._handleExtendedDesktopSize(); default: - return this._handleDataRect(); + if (this._handleDataRect()) { + this._FBU.rect_total++; //only track rendered rects and last rect + return true; + } + return false; } } @@ -3524,9 +3553,9 @@ export default class RFB extends EventTargetMixin { return decoder.decodeRect(this._FBU.x, this._FBU.y, this._FBU.width, this._FBU.height, this._sock, this._display, - this._fbDepth); + this._fbDepth, this._FBU.frame_id); } catch (err) { - this._fail("Error decoding rect: " + err); + this._fail("Error decoding rect: " + err); return false; } } diff --git a/vnc.html b/vnc.html index 1ee56b00..2f071648 100644 --- a/vnc.html +++ b/vnc.html @@ -277,6 +277,12 @@ IME Input Mode +
  • + +