diff --git a/com/codemirror.js b/com/codemirror.js index 061fcd2..eadc0c8 100644 --- a/com/codemirror.js +++ b/com/codemirror.js @@ -91,7 +91,7 @@ AFRAME.registerComponent('codemirror', { // component events DOMready: function(e){ - this.isoterminal.emulator.read_file( this.data.file ) + this.isoterminal.worker.postMessage.promise({event:'read_file',data: this.data.file }) .then( this.isoterminal.convert.Uint8ArrayToString ) .then( (str) => { this.createEditor( str ) diff --git a/com/isoterminal.js b/com/isoterminal.js index 4a06672..12023d9 100644 --- a/com/isoterminal.js +++ b/com/isoterminal.js @@ -33,18 +33,18 @@ if( typeof AFRAME != 'undefined '){ AFRAME.registerComponent('isoterminal', { schema: { - iso: { type:"string", "default":"https://forgejo.isvery.ninja/assets/xrsh-buildroot/main/xrsh.iso" }, - overlayfs: { type:"string"}, - cols: { type: 'number',"default": 80 }, - rows: { type: 'number',"default": 20 }, - padding: { type: 'number',"default": 18 }, - minimized: { type: 'boolean',"default":false}, - maximized: { type: 'boolean',"default":false}, - transparent: { type:'boolean', "default":false }, // need good gpu - xterm: { type: 'boolean', "default":true }, // use xterm.js? (=slower) - memory: { type: 'number', "default":48 }, // VM memory (in MB) - bufferLatency:{ type: 'number', "default":200 } // bufferlatency from worker to xterm - // (re-uploading the canvas per character is slooow) + iso: { type:"string", "default":"https://forgejo.isvery.ninja/assets/xrsh-buildroot/main/xrsh.iso" }, + overlayfs: { type:"string"}, + cols: { type: 'number',"default": 80 }, + rows: { type: 'number',"default": 20 }, + padding: { type: 'number',"default": 18 }, + minimized: { type: 'boolean',"default":false}, + maximized: { type: 'boolean',"default":false}, + transparent: { type:'boolean', "default":false }, // need good gpu + xterm: { type: 'boolean', "default":true }, // use xterm.js? (=slower) + memory: { type: 'number', "default":48 }, // VM memory (in MB) + bufferLatency:{ type: 'number', "default":300 }, // in ms: bufferlatency from webworker to xterm (batch-update every char to texture) + canvasLatency:{ type: 'number', "default":300 } // in ms: time between canvas re-draws }, init: function(){ @@ -187,7 +187,7 @@ if( typeof AFRAME != 'undefined '){ jsconsole: "com/isoterminal/feat/jsconsole.js", indexhtml: "com/isoterminal/feat/index.html.js", indexjs: "com/isoterminal/feat/index.js.js", - //autorestore: "com/isoterminal/feat/autorestore.js", + autorestore: "com/isoterminal/feat/autorestore.js", }) this.el.setAttribute("selfcontainer","") @@ -218,7 +218,7 @@ if( typeof AFRAME != 'undefined '){ instance.addEventListener('window.oncreate', (e) => { instance.dom.classList.add('blink') - instance.setAttribute("xterm",`cols: ${this.data.cols}; rows: ${this.data.rows}`) + instance.setAttribute("xterm",`cols: ${this.data.cols}; rows: ${this.data.rows}; canvasLatency: ${this.data.canvasLatency}`) instance.addEventListener("xterm-input", (e) => this.isoterminal.send(e.detail,0) ) // run iso let opts = {dom:instance.dom} diff --git a/com/isoterminal/ISOTerminal.js b/com/isoterminal/ISOTerminal.js index f56c10f..b17fd0c 100644 --- a/com/isoterminal/ISOTerminal.js +++ b/com/isoterminal/ISOTerminal.js @@ -126,30 +126,52 @@ ISOTerminal.prototype.start = function(opts){ autostart: true, }; + /* + * the WebWorker (which runs v86) + * + */ + this.worker = new Worker("com/isoterminal/worker.js"); this.worker.onmessage = (e) => { + const xr = this.instance.sceneEl.renderer.xr const {event,data} = e.data - this.emit(event,data,"worker") + const cb = (event,data) => () => { + if( data.promiseId ){ + this.workerPromise.resolver(data) // forward to promise resolver + }else this.emit(event,data,"worker") // forward event to world + + } + // don't let workers cause framerate dropping + if( xr.isPresenting ){ + xr.getSession().requestAnimationFrame(cb(event,data)) + }else{ + window.requestAnimationFrame(cb(event,data)) + } } + + /* + * postMessage.promise basically performs this.worker.postMessage + * in a promise way (to easily retrieve async output) + */ - //this.term = new window.Terminal({ - // logLevel:"off", - // rows: 50, - // cols: 110 - //}) - //this.term.open( this.instance.dom ) - //this.term.onData( (data) => { - // for(let i = 0; i < data.length; i++){ - // this.worker.postMessage({event:"serial0-input", data: data.charCodeAt(i) }) - // } - //}) + this.worker.postMessage.promise = function(data){ + if( typeof data != 'object' ) data = {data} + this.resolvers = this.resolvers || {} + this.id = this.id == undefined ? 0 : this.id + data.id = this.id++ + // Send id and task to WebWorker + this.worker.postMessage(data) + return new Promise(resolve => this.resolvers[data.id] = resolve); + }.bind(this.worker.postMessage) - //this.instance.addEventListener('serial-output-byte', (e) => { - // const byte = e.detail - // this.term.write(byte) - //}) + this.worker.postMessage.promise.resolver = function(data){ + if( !data || !data.promiseId ) throw 'promiseId not given' + this.resolvers[ data.promiseId ](data); + delete this.resolvers[ data.promiseId ]; // Prevent memory leak + }.bind(this.worker.postMessage) - this.emit('runISO',opts) + + this.emit('runISO',{...opts, bufferLatency: this.opts.bufferLatency }) const loading = [ 'loading quantum bits and bytes', 'preparing quantum flux capacitors', @@ -194,40 +216,34 @@ ISOTerminal.prototype.start = function(opts){ //} let line = '' - let ready = false - this.addEventListener(`serial0-output-byte`, async (e) => { - const byte = e.detail - //this.emit("serial-output-byte",byte) // send to xterm - this.bufferOutput(`serial-output-byte`, byte) - var chr = String.fromCharCode(byte); - if(chr < " " && chr !== "\n" && chr !== "\t" || chr > "~") return + this.ready = false - if(chr === "\n") - { - var new_line = line; - line = ""; - } else if(chr >= " " && chr <= "~"){ line += chr } + this.addEventListener(`serial0-output-string`, async (e) => { + const str = e.detail - if( !ready && line.match(/^(\/ #|~%|\[.*\]>)/) ){ - this.emit('postReady',{}) - setTimeout( () => this.emit('ready',{}), 500 ) - ready = true - } - }); + // lets scan for a prompt so we can send a 'ready' event to the world + if( !this.ready && str.match(/\n(\/ #|~%|\[.*\]>)/) ){ + this.emit('postReady',{}) + this.ready = true + setTimeout( () => this.emit('ready',{}), 500 ) + } + if( this.ready ) this.emit('serial-output-string', e.detail ) + }) }); } -ISOTerminal.prototype.bufferOutput = function(type,byte){ - this.buffer = this.buffer || {str:""} - if( this.buffer.id ) this.buffer.str += String.fromCharCode(byte) - else{ - this.emit(type, byte ) // leading call - this.buffer.id = setTimeout( () => { // trailing calls - if( this.buffer.str ){ - this.emit('serial-output-string', this.buffer.str ) - } - this.buffer = {str:""} - }, this.opts.bufferLatency || 250) +ISOTerminal.prototype.bufferOutput = function(byte,cb,latency){ + const resetBuffer = () => ({str:""}) + this.buffer = this.buffer || resetBuffer() + this.buffer.str += String.fromCharCode(byte) + if( !this.buffer.id ){ + cb(this.buffer.str) // send out leading call + this.buffer = resetBuffer() + this.buffer.id = setTimeout( () => { // accumulate succesive calls + if( this.buffer.str ) cb(this.buffer.str) + this.buffer = resetBuffer() + }, this.latency || 250) } } + diff --git a/com/isoterminal/feat/autorestore.js b/com/isoterminal/feat/autorestore.js index 39ac2fa..7f68597 100644 --- a/com/isoterminal/feat/autorestore.js +++ b/com/isoterminal/feat/autorestore.js @@ -1,53 +1,76 @@ -ISOTerminal.addEventListener('emulator-started', function(e){ - return console.log("TODO: autorestore.js") - this.autorestore(e) -}) +if( typeof emulator != 'undefined' ){ + // inside worker-thread + + this['emulator.restore_state'] = async function(){ + await emulator.restore_state.apply(emulator, arguments[0]) + this.postMessage({event:"state_restored",data:false}) + } + this['emulator.save_state'] = async function(){ + let state = await emulator.save_state.apply(emulator, arguments[0]) + this.postMessage({event:"state_saved",data:state}) + } -ISOTerminal.prototype.autorestore = async function(e){ - - localforage.setDriver([ - localforage.INDEXEDDB, - localforage.WEBSQL, - localforage.LOCALSTORAGE - ]).then( () => { - - localforage.getItem("state", async (err,stateBase64) => { - if( stateBase64 && !err && confirm('continue last session?') ){ - this.noboot = true // see feat/boot.js - state = this.convert.base64ToArrayBuffer( stateBase64 ) - this.emulator.restore_state(state) - this.emit('postReady',e) - setTimeout( () => { - this.emit('ready',e) - // press CTRL+a l (=gnu screen redisplay) - setTimeout( () => this.send("l\n"),400 ) - // reload index.js - this.emulator.read_file("root/index.js") - .then( this.convert.Uint8ArrayToString ) - .then( this.runJavascript ) - .catch( console.error ) - // reload index.html - this.emulator.read_file("root/index.html") - .then( this.convert.Uint8ArrayToString ) - .then( this.runHTML ) - .catch( console.error ) - - }, 500 ) - } - }) - - this.save = async () => { - const state = await this.emulator.save_state() - console.log( String(this.convert.arrayBufferToBase64(state)).substr(0,5) ) - localforage.setItem("state", this.convert.arrayBufferToBase64(state) ) - } - - window.addEventListener("beforeunload", function (e) { - var confirmationMessage = "Sure you want to leave?\nTIP: enter 'save' to continue this session later"; - (e || window.event).returnValue = confirmationMessage; //Gecko + IE - return confirmationMessage; //Webkit, Safari, Chrome - }); +}else{ + // inside browser-thread + ISOTerminal.addEventListener('emulator-started', function(e){ + this.autorestore(e) }) -} + ISOTerminal.prototype.autorestore = async function(e){ + + localforage.setDriver([ + localforage.INDEXEDDB, + localforage.WEBSQL, + localforage.LOCALSTORAGE + ]).then( () => { + + localforage.getItem("state", async (err,stateBase64) => { + if( stateBase64 && !err && confirm('continue last session?') ){ + this.noboot = true // see feat/boot.js + state = this.convert.base64ToArrayBuffer( stateBase64 ) + + this.addEventListener('state_restored', function(){ + this.emit('postReady',e) + setTimeout( () => { + this.emit('ready',e) + // press CTRL+a l (=gnu screen redisplay) + setTimeout( () => this.send("l\n"),400 ) + // reload index.js + this.emulator.read_file("root/index.js") + .then( this.convert.Uint8ArrayToString ) + .then( this.runJavascript ) + .catch( console.error ) + // reload index.html + this.emulator.read_file("root/index.html") + .then( this.convert.Uint8ArrayToString ) + .then( this.runHTML ) + .catch( console.error ) + + }, 500 ) + }) + + this.worker.postMessage({event:'emulator.restore_state',data:state}) + } + }) + + this.save = async () => { + const state = await this.worker.postMessage({event:"save_state",data:false}) + console.log( String(this.convert.arrayBufferToBase64(state)).substr(0,5) ) + localforage.setItem("state", this.convert.arrayBufferToBase64(state) ) + } + + this.addEventListener('state_saved', function(data){ + debugger + }) + + window.addEventListener("beforeunload", function (e) { + var confirmationMessage = "Sure you want to leave?\nTIP: enter 'save' to continue this session later"; + (e || window.event).returnValue = confirmationMessage; //Gecko + IE + return confirmationMessage; //Webkit, Safari, Chrome + }); + + }) + } + +} diff --git a/com/isoterminal/images/buildroot-bzimage.bin b/com/isoterminal/images/buildroot-bzimage.bin deleted file mode 100644 index 5e149f7..0000000 Binary files a/com/isoterminal/images/buildroot-bzimage.bin and /dev/null differ diff --git a/com/isoterminal/worker.js b/com/isoterminal/worker.js index eccdd75..7cc733b 100644 --- a/com/isoterminal/worker.js +++ b/com/isoterminal/worker.js @@ -40,15 +40,24 @@ this.runISO = function(opts){ // event forwarding emulator.add_listener("serial0-output-byte", function(byte){ - this.postMessage({event:"serial0-output-byte",data:byte}); + ISOTerminal.prototype.bufferOutput(byte, (str) => { // we buffer to prevent framerate dropping + if( !str ) return + this.postMessage({event:"serial0-output-string",data:str}); + }, opts.bufferLatency ) }.bind(this)); emulator.add_listener("serial1-output-byte", function(byte){ - this.postMessage({event:"serial1-output-byte",data:byte}); + ISOTerminal.prototype.bufferOutput(byte, (str) => { // we buffer to prevent framerate dropping + if( !str ) return + this.postMessage({event:"serial1-output-string",data:str}); + }, opts.bufferLatency ) }.bind(this)); emulator.add_listener("serial2-output-byte", function(byte){ - this.postMessage({event:"serial2-output-byte",data:byte}); + ISOTerminal.prototype.bufferOutput(byte, (str) => { // we buffer to prevent framerate dropping + if( !str ) return + this.postMessage({event:"serial2-output-string",data:str}); + }, opts.bufferLatency ) }.bind(this)); emulator.add_listener("emulator-started", function(){ @@ -59,8 +68,8 @@ this.runISO = function(opts){ /* * forward events/functions so non-worker world can reach them */ - this['emulator.create_file'] = function(){ emulator.create_file.apply(emulator, arguments[0]) } - this['emulator.read_file'] = function(){ emulator.read_file.apply(emulator, arguments[0]) } + this['emulator.create_file'] = function(){ emulator.create_file.apply(emulator, arguments[0]) } + this['emulator.read_file'] = function(){ emulator.read_file.apply(emulator, arguments[0]) } // filename will be read from 9pfs: "/mnt/"+filename emulator.readFromPipe = function(filename,cb){ @@ -73,6 +82,7 @@ this.runISO = function(opts){ importScripts("feat/javascript.js") importScripts("feat/index.html.js") + importScripts("feat/autorestore.js") } /* * forward events/functions so non-worker world can reach them diff --git a/com/xterm.js b/com/xterm.js index 8c5a610..607d366 100644 --- a/com/xterm.js +++ b/com/xterm.js @@ -92,14 +92,9 @@ const TERMINAL_THEME = { AFRAME.registerComponent('xterm', { schema: Object.assign({ - cols: { - type: 'number', - default: 110, - }, - rows: { - type: 'number', - default: Math.floor( (window.innerHeight * 0.7 ) * 0.054 ) - }, + cols: { type: 'number', default: 110, }, + rows: { type: 'number', default: Math.floor( (window.innerHeight * 0.7 ) * 0.054 ) }, + canvasLatency:{ type:'number', default: 200 } }, TERMINAL_THEME), write: function(message) { @@ -139,6 +134,7 @@ AFRAME.registerComponent('xterm', { }, {}) const term = this.term = new Terminal({ + logLevel:"off", theme: theme, allowTransparency: true, cursorBlink: true, @@ -157,7 +153,7 @@ AFRAME.registerComponent('xterm', { //this.term._core.viewport._innerRefresh() this.term._core.renderer._renderDebouncer._innerRefresh() } - },150) + }, this.data.canvasLatency) this.term.open(terminalElement) this.term.focus() @@ -191,6 +187,7 @@ AFRAME.registerComponent('xterm', { const material = this.el.planeText.getObject3D('mesh').material if (!material.map ) return if( this.cursorCanvas ) this.canvasContext.drawImage(this.cursorCanvas, 0,0) + else console.log("no cursorCanvas") material.map.needsUpdate = true //material.needsUpdate = true }