improved performance on low-end smartphone
/ mirror_to_github (push) Successful in 19s Details
/ test (push) Successful in 3s Details

This commit is contained in:
Leon van Kammen 2024-10-04 15:49:15 +00:00
parent 2e56d235ed
commit 9aff2133a0
6 changed files with 142 additions and 100 deletions

View File

@ -33,22 +33,25 @@ if( typeof AFRAME != 'undefined '){
AFRAME.registerComponent('isoterminal', { AFRAME.registerComponent('isoterminal', {
schema: { schema: {
iso: { type:"string", "default":"https://forgejo.isvery.ninja/assets/xrsh-buildroot/main/xrsh.iso" }, iso: { type:"string", "default":"https://forgejo.isvery.ninja/assets/xrsh-buildroot/main/xrsh.iso" },
overlayfs: { type:"string"}, overlayfs: { type:"string"},
cols: { type: 'number',"default": 80 }, cols: { type: 'number',"default": 80 },
rows: { type: 'number',"default": 20 }, rows: { type: 'number',"default": 20 },
padding: { type: 'number',"default": 18 }, padding: { type: 'number',"default": 18 },
minimized: { type: 'boolean',"default":false}, minimized: { type: 'boolean',"default":false},
maximized: { type: 'boolean',"default":false}, maximized: { type: 'boolean',"default":false},
transparent: { type:'boolean', "default":false }, // need good gpu muteUntilPrompt:{ type: 'boolean',"default":true}, // mute stdout until a prompt is detected in ISO
xterm: { type: 'boolean', "default":true }, // use xterm.js? (=slower) HUD: { type: 'boolean',"default":false}, // link to camera movement
memory: { type: 'number', "default":48 }, // VM memory (in MB) transparent: { type:'boolean', "default":false }, // need good gpu
bufferLatency:{ type: 'number', "default":300 }, // in ms: bufferlatency from webworker to xterm (batch-update every char to texture) xterm: { type: 'boolean', "default":true }, // use xterm.js? (=slower)
canvasLatency:{ type: 'number', "default":300 } // in ms: time between canvas re-draws 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(){ init: function(){
this.el.object3D.visible = false this.el.object3D.visible = false
this.initHud()
fetch(this.data.iso,{method: 'HEAD'}) fetch(this.data.iso,{method: 'HEAD'})
.then( (res) => { .then( (res) => {
if( res.status != 200 ) throw 'not found' 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-vr', focus(false) )
this.el.sceneEl.addEventListener('enter-ar', focus(false) ) this.el.sceneEl.addEventListener('enter-ar', focus(false) )
this.el.sceneEl.addEventListener('exit-vr', focus(true) ) 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 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:{ events:{
// combined AFRAME+DOM reactive events // combined AFRAME+DOM reactive events

View File

@ -17,12 +17,14 @@ function ISOTerminal(instance,opts){
ISOTerminal.prototype.emit = function(event,data,sender){ ISOTerminal.prototype.emit = function(event,data,sender){
data = data || false data = data || false
const evObj = new CustomEvent(event, {detail: data} ) const evObj = new CustomEvent(event, {detail: data} )
// forward event to worker/instance/AFRAME element or component-function //this.preventFrameDrop( () => {
// this feels complex, but actually keeps event- and function-names more concise in codebase // forward event to worker/instance/AFRAME element or component-function
this.dispatchEvent( evObj ) // this feels complex, but actually keeps event- and function-names more concise in codebase
if( sender != "instance" && this.instance ) this.instance.dispatchEvent(evObj) this.dispatchEvent( evObj )
if( sender != "worker" && this.worker ) this.worker.postMessage({event,data}) if( sender != "instance" && this.instance ) this.instance.dispatchEvent(evObj)
if( sender !== undefined && typeof this[event] == 'function' ) this[event].apply(this, data && data.push ? data : [data] ) 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) => { 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.emulator.keyboard_send_text(str) // vga screen
}else{ }else{
this.convert.toUint8Array( str ).map( (c) => { 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 = new Worker("com/isoterminal/worker.js");
this.worker.onmessage = (e) => { this.worker.onmessage = (e) => {
const xr = this.instance.sceneEl.renderer.xr
const {event,data} = e.data const {event,data} = e.data
const cb = (event,data) => () => { const cb = (event,data) => () => {
if( data.promiseId ){ if( data.promiseId ){
@ -141,12 +144,7 @@ ISOTerminal.prototype.start = function(opts){
}else this.emit(event,data,"worker") // forward event to world }else this.emit(event,data,"worker") // forward event to world
} }
// don't let workers cause framerate dropping this.preventFrameDrop( cb(event,data) )
if( xr.isPresenting ){
xr.getSession().requestAnimationFrame(cb(event,data))
}else{
window.requestAnimationFrame(cb(event,data))
}
} }
/* /*
@ -160,7 +158,7 @@ ISOTerminal.prototype.start = function(opts){
this.id = this.id == undefined ? 0 : this.id this.id = this.id == undefined ? 0 : this.id
data.id = this.id++ data.id = this.id++
// Send id and task to WebWorker // 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); return new Promise(resolve => this.resolvers[data.id] = resolve);
}.bind(this.worker.postMessage) }.bind(this.worker.postMessage)
@ -196,12 +194,52 @@ ISOTerminal.prototype.start = function(opts){
'Transcending earthly limits' '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 isnt 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_color = "\r"
const text_reset = "\033[0m" 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('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) => { this.addEventListener('emulator-started', async (e) => {
@ -222,17 +260,25 @@ ISOTerminal.prototype.start = function(opts){
const str = e.detail const str = e.detail
// lets scan for a prompt so we can send a 'ready' event to the world // lets scan for a prompt so we can send a 'ready' event to the world
if( !this.ready && str.match(/\n(\/ #|~%|\[.*\]>)/) ){ if( !this.ready && str.match(/\n(\/ #|~%|\[.*\]>)/) ) this.postBoot()
this.emit('postReady',{})
this.ready = true if( this.ready || !this.opts.muteUntilPrompt ) this.emit('serial-output-string', e.detail )
setTimeout( () => this.emit('ready',{}), 500 )
}
if( this.ready ) 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){ ISOTerminal.prototype.bufferOutput = function(byte,cb,latency){
const resetBuffer = () => ({str:""}) const resetBuffer = () => ({str:""})
this.buffer = this.buffer || resetBuffer() 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
}

View File

@ -1,12 +1,14 @@
if( typeof emulator != 'undefined' ){ if( typeof emulator != 'undefined' ){
// inside worker-thread // inside worker-thread
this['emulator.restore_state'] = async function(){ this['emulator.restore_state'] = async function(data){
await emulator.restore_state.apply(emulator, arguments[0]) await emulator.restore_state(data)
console.log("restored state")
this.postMessage({event:"state_restored",data:false}) this.postMessage({event:"state_restored",data:false})
} }
this['emulator.save_state'] = async function(){ 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}) this.postMessage({event:"state_saved",data:state})
} }
@ -31,23 +33,8 @@ if( typeof emulator != 'undefined' ){
state = this.convert.base64ToArrayBuffer( stateBase64 ) state = this.convert.base64ToArrayBuffer( stateBase64 )
this.addEventListener('state_restored', function(){ this.addEventListener('state_restored', function(){
this.emit('postReady',e) // simulate / fastforward boot events
setTimeout( () => { this.postBoot( () => this.send("l\n") )
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.worker.postMessage({event:'emulator.restore_state',data:state})
@ -55,13 +42,13 @@ if( typeof emulator != 'undefined' ){
}) })
this.save = async () => { this.save = async () => {
const state = await this.worker.postMessage({event:"save_state",data:false}) const state = await this.worker.postMessage({event:"emulator.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){ this.addEventListener('state_saved', function(e){
debugger const state = e.detail
localforage.setItem("state", this.convert.arrayBufferToBase64(state) )
console.log("state saved")
}) })
window.addEventListener("beforeunload", function (e) { window.addEventListener("beforeunload", function (e) {

View File

@ -14,8 +14,7 @@ ISOTerminal.prototype.boot = async function(e){
if( this.serial_input == 0 ){ if( this.serial_input == 0 ){
if( !this.noboot ){ if( !this.noboot ){
let boot = "source /etc/profile\n" this.send("source /etc/profile # \\o/ FOSS powa!\n")
this.send(boot+"\n")
} }
} }

View File

@ -1,34 +1,6 @@
importScripts("libv86.js"); importScripts("libv86.js");
importScripts("ISOTerminal.js") // we don't instance it again here (just use its functions) 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){ this.runISO = function(opts){
if( opts.cdrom && !opts.cdrom.url.match(/^http/) ) opts.cdrom.url = "../../"+opts.cdrom.url 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 if( opts.bzimage && !opts.cdrom.url.match(/^http/) ) opts.bzimage.url = "../../"+opts.bzimage.url

View File

@ -103,8 +103,8 @@ AFRAME.registerComponent('xterm', {
init: function () { init: function () {
const terminalElement = document.createElement('div') const terminalElement = document.createElement('div')
terminalElement.setAttribute('style', ` terminalElement.setAttribute('style', `
width: ${Math.floor( window.innerWidth * 0.7 )}px; width: 800px;
height: ${Math.floor( window.innerHeight * 0.7 )}px; height: ${Math.floor( 800 * 0.527 )}px;
overflow: hidden; overflow: hidden;
`) `)
@ -133,6 +133,8 @@ AFRAME.registerComponent('xterm', {
return theme return theme
}, {}) }, {})
this.fontSize = 14
const term = this.term = new Terminal({ const term = this.term = new Terminal({
logLevel:"off", logLevel:"off",
theme: theme, theme: theme,
@ -141,12 +143,13 @@ AFRAME.registerComponent('xterm', {
disableStdin: false, disableStdin: false,
rows: this.data.rows, rows: this.data.rows,
cols: this.data.cols, cols: this.data.cols,
fontSize: 14, fontSize: this.fontSize,
lineHeight: 1.15, lineHeight: 1.15,
useFlowControl: true,
rendererType: this.renderType // 'dom' // 'canvas' rendererType: this.renderType // 'dom' // 'canvas'
}) })
this.tick = AFRAME.utils.throttle( () => { this.tick = AFRAME.utils.throttleLeadingAndTrailing( () => {
if( this.el.sceneEl.renderer.xr.isPresenting ){ if( this.el.sceneEl.renderer.xr.isPresenting ){
// workaround // workaround
// xterm relies on window.requestAnimationFrame (which is not called WebXR immersive mode) // 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') const $screen = terminalElement.querySelector('.xterm-screen')
$screen.style.width = '100%' $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) => { term.on('data', (data) => {
this.el.emit('xterm-input', data) this.el.emit('xterm-input', data)
}) })
@ -200,14 +203,14 @@ AFRAME.registerComponent('xterm', {
if( type == 'dom'){ if( type == 'dom'){
this.el.dom.appendChild(this.el.terminalElement) this.el.dom.appendChild(this.el.terminalElement)
this.term.setOption('fontSize', 14 ) this.term.setOption('fontSize', this.fontSize )
this.term.setOption('rendererType',type ) this.term.setOption('rendererType',type )
this.renderType = type this.renderType = type
} }
if( type == 'canvas'){ if( type == 'canvas'){
this.el.appendChild(this.el.terminalElement) this.el.appendChild(this.el.terminalElement)
this.term.setOption('fontSize', 48 ) this.term.setOption('fontSize', this.fontSize * 3 )
this.term.setOption('rendererType',type ) this.term.setOption('rendererType',type )
this.renderType = type this.renderType = type
this.update() this.update()
@ -218,8 +221,8 @@ AFRAME.registerComponent('xterm', {
this.cursorCanvas = this.el.terminalElement.querySelector('.xterm-cursor-layer') this.cursorCanvas = this.el.terminalElement.querySelector('.xterm-cursor-layer')
// Create a texture from the canvas // Create a texture from the canvas
const canvasTexture = new THREE.Texture(this.canvas) const canvasTexture = new THREE.Texture(this.canvas)
//canvasTexture.minFilter = THREE.LinearFilter //canvasTexture.minFilter = THREE.NearestFilter //LinearFilter
//canvasTexture.magFilter = THREE.LinearFilter //canvasTexture.magFilter = THREE.LinearMipMapLinearFilter //THREE.NearestFilter //LinearFilter
canvasTexture.needsUpdate = true; // Ensure the texture updates canvasTexture.needsUpdate = true; // Ensure the texture updates
let plane = this.el.planeText.getObject3D("mesh") //this.el.getObject3D('mesh') let plane = this.el.planeText.getObject3D("mesh") //this.el.getObject3D('mesh')
if( plane.material ) plane.material.dispose() if( plane.material ) plane.material.dispose()