From 9aff2133a076315899f756aec87067011be743e4 Mon Sep 17 00:00:00 2001 From: Leon van Kammen Date: Fri, 4 Oct 2024 15:49:15 +0000 Subject: [PATCH] improved performance on low-end smartphone --- com/isoterminal.js | 36 ++++++--- com/isoterminal/ISOTerminal.js | 117 ++++++++++++++++++++++------ com/isoterminal/feat/autorestore.js | 37 +++------ com/isoterminal/feat/boot.js | 3 +- com/isoterminal/worker.js | 28 ------- com/xterm.js | 21 ++--- 6 files changed, 142 insertions(+), 100 deletions(-) diff --git a/com/isoterminal.js b/com/isoterminal.js index 12023d9..33d4ae3 100644 --- a/com/isoterminal.js +++ b/com/isoterminal.js @@ -33,22 +33,25 @@ 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":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 + 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}, + muteUntilPrompt:{ type: 'boolean',"default":true}, // mute stdout until a prompt is detected in ISO + HUD: { type: 'boolean',"default":false}, // link to camera movement + transparent: { type:'boolean', "default":false }, // need good gpu + xterm: { type: 'boolean', "default":true }, // use xterm.js? (=slower) + memory: { type: 'number', "default":64 }, // 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(){ this.el.object3D.visible = false + this.initHud() fetch(this.data.iso,{method: 'HEAD'}) .then( (res) => { if( res.status != 200 ) throw 'not found' @@ -267,6 +270,7 @@ if( typeof AFRAME != 'undefined '){ } } + this.el.addEventListener('obbcollisionstarted', focus(false) ) this.el.sceneEl.addEventListener('enter-vr', focus(false) ) this.el.sceneEl.addEventListener('enter-ar', focus(false) ) this.el.sceneEl.addEventListener('exit-vr', focus(true) ) @@ -275,6 +279,14 @@ if( typeof AFRAME != 'undefined '){ instance.object3D.quaternion.copy( AFRAME.scenes[0].camera.quaternion ) // face towards camera }, + initHud: function(){ + if( AFRAME.utils.device.isMobile() ) this.data.HUD = true + if( this.data.HUD ){ + document.querySelector('[camera]').appendChild( this.el ) + this.el.setAttribute("position","0 -0.03 -0.4") + } + }, + events:{ // combined AFRAME+DOM reactive events diff --git a/com/isoterminal/ISOTerminal.js b/com/isoterminal/ISOTerminal.js index b17fd0c..b172be8 100644 --- a/com/isoterminal/ISOTerminal.js +++ b/com/isoterminal/ISOTerminal.js @@ -17,12 +17,14 @@ function ISOTerminal(instance,opts){ ISOTerminal.prototype.emit = function(event,data,sender){ data = data || false const evObj = new CustomEvent(event, {detail: data} ) - // forward event to worker/instance/AFRAME element or component-function - // this feels complex, but actually keeps event- and function-names more concise in codebase - this.dispatchEvent( evObj ) - if( sender != "instance" && this.instance ) this.instance.dispatchEvent(evObj) - if( sender != "worker" && this.worker ) this.worker.postMessage({event,data}) - if( sender !== undefined && typeof this[event] == 'function' ) this[event].apply(this, data && data.push ? data : [data] ) + //this.preventFrameDrop( () => { + // forward event to worker/instance/AFRAME element or component-function + // this feels complex, but actually keeps event- and function-names more concise in codebase + this.dispatchEvent( evObj ) + if( sender != "instance" && this.instance ) this.instance.dispatchEvent(evObj) + if( sender != "worker" && this.worker ) this.worker.postMessage({event,data}, this.getTransferable(data) ) + if( sender !== undefined && typeof this[event] == 'function' ) this[event].apply(this, data && data.push ? data : [data] ) + //}) } ISOTerminal.addEventListener = (event,cb) => { @@ -45,7 +47,9 @@ ISOTerminal.prototype.send = function(str, ttyNr){ }else this.emulator.keyboard_send_text(str) // vga screen }else{ this.convert.toUint8Array( str ).map( (c) => { - this.worker.postMessage({event:`serial${ttyNr}-input`,data:c}) + this.preventFrameDrop( + () => this.worker.postMessage({event:`serial${ttyNr}-input`,data:c}) + ) }) } } @@ -133,7 +137,6 @@ ISOTerminal.prototype.start = function(opts){ this.worker = new Worker("com/isoterminal/worker.js"); this.worker.onmessage = (e) => { - const xr = this.instance.sceneEl.renderer.xr const {event,data} = e.data const cb = (event,data) => () => { if( data.promiseId ){ @@ -141,12 +144,7 @@ ISOTerminal.prototype.start = function(opts){ }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)) - } + this.preventFrameDrop( cb(event,data) ) } /* @@ -160,7 +158,7 @@ ISOTerminal.prototype.start = function(opts){ this.id = this.id == undefined ? 0 : this.id data.id = this.id++ // Send id and task to WebWorker - this.worker.postMessage(data) + this.preventFrameDrop( () => this.worker.postMessage(data,getTransferable(data) ) ) return new Promise(resolve => this.resolvers[data.id] = resolve); }.bind(this.worker.postMessage) @@ -196,12 +194,52 @@ ISOTerminal.prototype.start = function(opts){ 'Transcending earthly limits' ] - const loadmsg = loading[ Math.floor(Math.random()*1000) % loading.length ] + "..(please wait..)" + const empower = [ + "FOSS gives users control over their software, offering freedom to modify and share", + "Feeling powerless with tech? FOSS escapes a mindset known as learned helplessness", + "FOSS breaks this cycle by showing that anyone can learn and contribute", + "Proprietary software can make users dependent, but FOSS offers real choices", + "FOSS communities provide support and encourage users to develop new skills", + "Learned helplessness fades when we realize tech isn’t too complex to understand", + "FOSS empowers users to customize and improve their tools", + "Engaging with FOSS helps build confidence and self-reliance in tech", + "FOSS tools are accessible and often better than closed alternatives", + "FOSS shows that anyone can shape the digital world with curiosity and effort", + "Linux can revive old computers, extending their life and reducing e-waste", + "Many lightweight Linux distributions run smoothly on older hardware", + "Installing Linux on aging devices keeps them functional instead of sending them to the landfill", + "Linux uses fewer resources, making it ideal for reusing older machines", + "By using Linux, you can avoid buying new hardware, cutting down on tech waste", + "Instead of discarding slow devices, Linux can bring them back to life", + "Linux supports a wide range of devices, helping to prevent e-waste", + "Open-source drivers in Linux enable compatibility with old peripherals, reducing the need for replacements", + "Free Linux software helps users avoid planned obsolescence in commercial products", + "Switching to Linux promotes sustainability by reducing demand for new gadgets and lowering e-waste" + ] + + const motd = ` +\r . . ____ _____________ ________. ._. ._. . . +\r . . .\\ \\/ /\\______ \\/ _____// | \\. . +\r . . . \\ / | _/\\_____ \\/ ~ \\ . +\r . . . / \\ | | \\/ \\ Y / . +\r . . ./___/\\ \\ |____|_ /_______ /\\___|_ /. . +\r . . . . . .\\_/. . . . \\/ . . . .\\/ . . _ \\/ . . +\r ▬▬▬▬▬▬▬▬ https://xrsh.isvery.ninja ▬▬▬▬▬▬▬▬▬▬▬▬ +\r local-first, polyglot, unixy WebXR IDE & runtime +\r +\r credits: NLnet | @nlnet@nlnet.nl +\r MrDoob | THREE.js +\r Diego Marcos | AFRAME.js +\r Leon van Kammen | @lvk@mastodon.online +\r Fabien Benetou | @utopiah@mastodon.pirateparty.be + ` + const text_color = "\r" const text_reset = "\033[0m" + const loadmsg = "\n\r "+loading[ Math.floor(Math.random()*1000) % loading.length ] + "..[please wait]" + const empowermsg = "\n\r "+text_reset+'"'+empower[ Math.floor(Math.random()*1000) % empower.length ] + '"\n\r' this.emit('status',loadmsg) - this.emit('serial-output-string', text_color + loadmsg + text_reset + "\n\r") - + this.emit('serial-output-string', motd + empowermsg + text_color + loadmsg + text_reset+"\n\r") this.addEventListener('emulator-started', async (e) => { @@ -222,17 +260,25 @@ ISOTerminal.prototype.start = function(opts){ const str = e.detail // 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 ) + if( !this.ready && str.match(/\n(\/ #|~%|\[.*\]>)/) ) this.postBoot() + + if( this.ready || !this.opts.muteUntilPrompt ) this.emit('serial-output-string', e.detail ) }) }); } + +ISOTerminal.prototype.postBoot = function(cb){ + this.emit('postReady',{}) + this.ready = true + setTimeout( () => { + this.emit('ready',{}) + if( cb ) cb() + }, 500 ) +} + +// this is allows (unsophisticated) outputbuffering ISOTerminal.prototype.bufferOutput = function(byte,cb,latency){ const resetBuffer = () => ({str:""}) this.buffer = this.buffer || resetBuffer() @@ -247,3 +293,26 @@ ISOTerminal.prototype.bufferOutput = function(byte,cb,latency){ } } +ISOTerminal.prototype.preventFrameDrop = function(cb){ + // don't let workers cause framerate dropping + const xr = this.instance.sceneEl.renderer.xr + if( xr.isPresenting ){ + xr.getSession().requestAnimationFrame(cb) + }else{ + window.requestAnimationFrame(cb) + } +} + +ISOTerminal.prototype.getTransferable = function(data){ + function isTransferable(obj) { + return obj instanceof ArrayBuffer || + obj instanceof MessagePort || + obj instanceof ImageBitmap || + (typeof OffscreenCanvas !== 'undefined' && obj instanceof OffscreenCanvas) || + (typeof ReadableStream !== 'undefined' && obj instanceof ReadableStream) || + (typeof WritableStream !== 'undefined' && obj instanceof WritableStream) || + (typeof TransformStream !== 'undefined' && obj instanceof TransformStream); + } + if( isTransferable(data) ) console.log("Transferable!") + if( isTransferable(data) ) return isTransferable(data) ? [data] : undefined +} diff --git a/com/isoterminal/feat/autorestore.js b/com/isoterminal/feat/autorestore.js index 7f68597..80d3e3d 100644 --- a/com/isoterminal/feat/autorestore.js +++ b/com/isoterminal/feat/autorestore.js @@ -1,12 +1,14 @@ if( typeof emulator != 'undefined' ){ // inside worker-thread - this['emulator.restore_state'] = async function(){ - await emulator.restore_state.apply(emulator, arguments[0]) + this['emulator.restore_state'] = async function(data){ + await emulator.restore_state(data) + console.log("restored state") this.postMessage({event:"state_restored",data:false}) } this['emulator.save_state'] = async function(){ - let state = await emulator.save_state.apply(emulator, arguments[0]) + console.log("saving session") + let state = await emulator.save_state() this.postMessage({event:"state_saved",data:state}) } @@ -31,23 +33,8 @@ if( typeof emulator != 'undefined' ){ 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 ) + // simulate / fastforward boot events + this.postBoot( () => this.send("l\n") ) }) this.worker.postMessage({event:'emulator.restore_state',data:state}) @@ -55,13 +42,13 @@ if( typeof emulator != 'undefined' ){ }) 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) ) + const state = await this.worker.postMessage({event:"emulator.save_state",data:false}) } - this.addEventListener('state_saved', function(data){ - debugger + this.addEventListener('state_saved', function(e){ + const state = e.detail + localforage.setItem("state", this.convert.arrayBufferToBase64(state) ) + console.log("state saved") }) window.addEventListener("beforeunload", function (e) { diff --git a/com/isoterminal/feat/boot.js b/com/isoterminal/feat/boot.js index d2267d8..fe20b6b 100644 --- a/com/isoterminal/feat/boot.js +++ b/com/isoterminal/feat/boot.js @@ -14,8 +14,7 @@ ISOTerminal.prototype.boot = async function(e){ if( this.serial_input == 0 ){ if( !this.noboot ){ - let boot = "source /etc/profile\n" - this.send(boot+"\n") + this.send("source /etc/profile # \\o/ FOSS powa!\n") } } diff --git a/com/isoterminal/worker.js b/com/isoterminal/worker.js index 7cc733b..869069b 100644 --- a/com/isoterminal/worker.js +++ b/com/isoterminal/worker.js @@ -1,34 +1,6 @@ importScripts("libv86.js"); importScripts("ISOTerminal.js") // we don't instance it again here (just use its functions) -//var emulator = new V86({ -// wasm_path: "../build/v86.wasm", -// memory_size: 32 * 1024 * 1024, -// vga_memory_size: 2 * 1024 * 1024, -// bios: { -// url: "../bios/seabios.bin", -// }, -// vga_bios: { -// url: "../bios/vgabios.bin", -// }, -// cdrom: { -// url: "../images/linux4.iso", -// }, -// autostart: true, -//}); -// -// -//emulator.add_listener("serial0-output-byte", function(byte) -//{ -// var chr = String.fromCharCode(byte); -// this.postMessage(chr); -//}.bind(this)); -// -//this.onmessage = function(e) -//{ -// emulator.serial0_send(e.data); -//}; - this.runISO = function(opts){ if( opts.cdrom && !opts.cdrom.url.match(/^http/) ) opts.cdrom.url = "../../"+opts.cdrom.url if( opts.bzimage && !opts.cdrom.url.match(/^http/) ) opts.bzimage.url = "../../"+opts.bzimage.url diff --git a/com/xterm.js b/com/xterm.js index 607d366..8f3497e 100644 --- a/com/xterm.js +++ b/com/xterm.js @@ -103,8 +103,8 @@ AFRAME.registerComponent('xterm', { init: function () { const terminalElement = document.createElement('div') terminalElement.setAttribute('style', ` - width: ${Math.floor( window.innerWidth * 0.7 )}px; - height: ${Math.floor( window.innerHeight * 0.7 )}px; + width: 800px; + height: ${Math.floor( 800 * 0.527 )}px; overflow: hidden; `) @@ -133,6 +133,8 @@ AFRAME.registerComponent('xterm', { return theme }, {}) + this.fontSize = 14 + const term = this.term = new Terminal({ logLevel:"off", theme: theme, @@ -141,12 +143,13 @@ AFRAME.registerComponent('xterm', { disableStdin: false, rows: this.data.rows, cols: this.data.cols, - fontSize: 14, + fontSize: this.fontSize, lineHeight: 1.15, + useFlowControl: true, rendererType: this.renderType // 'dom' // 'canvas' }) - this.tick = AFRAME.utils.throttle( () => { + this.tick = AFRAME.utils.throttleLeadingAndTrailing( () => { if( this.el.sceneEl.renderer.xr.isPresenting ){ // workaround // xterm relies on window.requestAnimationFrame (which is not called WebXR immersive mode) @@ -165,7 +168,7 @@ AFRAME.registerComponent('xterm', { const $screen = terminalElement.querySelector('.xterm-screen') $screen.style.width = '100%' - term.on('refresh', AFRAME.utils.throttle( () => this.update(), 150 ) ) + term.on('refresh', AFRAME.utils.throttleLeadingAndTrailing( () => this.update(), 150 ) ) term.on('data', (data) => { this.el.emit('xterm-input', data) }) @@ -200,14 +203,14 @@ AFRAME.registerComponent('xterm', { if( type == 'dom'){ this.el.dom.appendChild(this.el.terminalElement) - this.term.setOption('fontSize', 14 ) + this.term.setOption('fontSize', this.fontSize ) this.term.setOption('rendererType',type ) this.renderType = type } if( type == 'canvas'){ this.el.appendChild(this.el.terminalElement) - this.term.setOption('fontSize', 48 ) + this.term.setOption('fontSize', this.fontSize * 3 ) this.term.setOption('rendererType',type ) this.renderType = type this.update() @@ -218,8 +221,8 @@ AFRAME.registerComponent('xterm', { this.cursorCanvas = this.el.terminalElement.querySelector('.xterm-cursor-layer') // Create a texture from the canvas const canvasTexture = new THREE.Texture(this.canvas) - //canvasTexture.minFilter = THREE.LinearFilter - //canvasTexture.magFilter = THREE.LinearFilter + //canvasTexture.minFilter = THREE.NearestFilter //LinearFilter + //canvasTexture.magFilter = THREE.LinearMipMapLinearFilter //THREE.NearestFilter //LinearFilter canvasTexture.needsUpdate = true; // Ensure the texture updates let plane = this.el.planeText.getObject3D("mesh") //this.el.getObject3D('mesh') if( plane.material ) plane.material.dispose()