feat/webworker: work in progress [might break]
/ test (push) Successful in 6s Details

This commit is contained in:
Leon van Kammen 2024-10-04 09:08:39 +00:00
parent 0e8d173289
commit 2e56d235ed
7 changed files with 170 additions and 124 deletions

View File

@ -91,7 +91,7 @@ AFRAME.registerComponent('codemirror', {
// component events // component events
DOMready: function(e){ 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( this.isoterminal.convert.Uint8ArrayToString )
.then( (str) => { .then( (str) => {
this.createEditor( str ) this.createEditor( str )

View File

@ -33,18 +33,18 @@ 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 transparent: { type:'boolean', "default":false }, // need good gpu
xterm: { type: 'boolean', "default":true }, // use xterm.js? (=slower) xterm: { type: 'boolean', "default":true }, // use xterm.js? (=slower)
memory: { type: 'number', "default":48 }, // VM memory (in MB) memory: { type: 'number', "default":48 }, // VM memory (in MB)
bufferLatency:{ type: 'number', "default":200 } // bufferlatency from worker to xterm bufferLatency:{ type: 'number', "default":300 }, // in ms: bufferlatency from webworker to xterm (batch-update every char to texture)
// (re-uploading the canvas per character is slooow) canvasLatency:{ type: 'number', "default":300 } // in ms: time between canvas re-draws
}, },
init: function(){ init: function(){
@ -187,7 +187,7 @@ if( typeof AFRAME != 'undefined '){
jsconsole: "com/isoterminal/feat/jsconsole.js", jsconsole: "com/isoterminal/feat/jsconsole.js",
indexhtml: "com/isoterminal/feat/index.html.js", indexhtml: "com/isoterminal/feat/index.html.js",
indexjs: "com/isoterminal/feat/index.js.js", indexjs: "com/isoterminal/feat/index.js.js",
//autorestore: "com/isoterminal/feat/autorestore.js", autorestore: "com/isoterminal/feat/autorestore.js",
}) })
this.el.setAttribute("selfcontainer","") this.el.setAttribute("selfcontainer","")
@ -218,7 +218,7 @@ if( typeof AFRAME != 'undefined '){
instance.addEventListener('window.oncreate', (e) => { instance.addEventListener('window.oncreate', (e) => {
instance.dom.classList.add('blink') 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) ) instance.addEventListener("xterm-input", (e) => this.isoterminal.send(e.detail,0) )
// run iso // run iso
let opts = {dom:instance.dom} let opts = {dom:instance.dom}

View File

@ -126,30 +126,52 @@ ISOTerminal.prototype.start = function(opts){
autostart: true, autostart: true,
}; };
/*
* the WebWorker (which runs v86)
*
*/
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
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({ this.worker.postMessage.promise = function(data){
// logLevel:"off", if( typeof data != 'object' ) data = {data}
// rows: 50, this.resolvers = this.resolvers || {}
// cols: 110 this.id = this.id == undefined ? 0 : this.id
//}) data.id = this.id++
//this.term.open( this.instance.dom ) // Send id and task to WebWorker
//this.term.onData( (data) => { this.worker.postMessage(data)
// for(let i = 0; i < data.length; i++){ return new Promise(resolve => this.resolvers[data.id] = resolve);
// this.worker.postMessage({event:"serial0-input", data: data.charCodeAt(i) }) }.bind(this.worker.postMessage)
// }
//})
//this.instance.addEventListener('serial-output-byte', (e) => { this.worker.postMessage.promise.resolver = function(data){
// const byte = e.detail if( !data || !data.promiseId ) throw 'promiseId not given'
// this.term.write(byte) 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 = [ const loading = [
'loading quantum bits and bytes', 'loading quantum bits and bytes',
'preparing quantum flux capacitors', 'preparing quantum flux capacitors',
@ -194,40 +216,34 @@ ISOTerminal.prototype.start = function(opts){
//} //}
let line = '' let line = ''
let ready = false this.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
if(chr === "\n") this.addEventListener(`serial0-output-string`, async (e) => {
{ const str = e.detail
var new_line = line;
line = "";
} else if(chr >= " " && chr <= "~"){ line += chr }
if( !ready && line.match(/^(\/ #|~%|\[.*\]>)/) ){ // lets scan for a prompt so we can send a 'ready' event to the world
this.emit('postReady',{}) if( !this.ready && str.match(/\n(\/ #|~%|\[.*\]>)/) ){
setTimeout( () => this.emit('ready',{}), 500 ) this.emit('postReady',{})
ready = true this.ready = true
} setTimeout( () => this.emit('ready',{}), 500 )
}); }
if( this.ready ) this.emit('serial-output-string', e.detail )
})
}); });
} }
ISOTerminal.prototype.bufferOutput = function(type,byte){ ISOTerminal.prototype.bufferOutput = function(byte,cb,latency){
this.buffer = this.buffer || {str:""} const resetBuffer = () => ({str:""})
if( this.buffer.id ) this.buffer.str += String.fromCharCode(byte) this.buffer = this.buffer || resetBuffer()
else{ this.buffer.str += String.fromCharCode(byte)
this.emit(type, byte ) // leading call if( !this.buffer.id ){
this.buffer.id = setTimeout( () => { // trailing calls cb(this.buffer.str) // send out leading call
if( this.buffer.str ){ this.buffer = resetBuffer()
this.emit('serial-output-string', this.buffer.str ) this.buffer.id = setTimeout( () => { // accumulate succesive calls
} if( this.buffer.str ) cb(this.buffer.str)
this.buffer = {str:""} this.buffer = resetBuffer()
}, this.opts.bufferLatency || 250) }, this.latency || 250)
} }
} }

View File

@ -1,53 +1,76 @@
ISOTerminal.addEventListener('emulator-started', function(e){ if( typeof emulator != 'undefined' ){
return console.log("TODO: autorestore.js") // inside worker-thread
this.autorestore(e)
}) 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
});
})
}
}

View File

@ -40,15 +40,24 @@ this.runISO = function(opts){
// event forwarding // event forwarding
emulator.add_listener("serial0-output-byte", function(byte){ 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)); }.bind(this));
emulator.add_listener("serial1-output-byte", function(byte){ 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)); }.bind(this));
emulator.add_listener("serial2-output-byte", function(byte){ 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)); }.bind(this));
emulator.add_listener("emulator-started", function(){ emulator.add_listener("emulator-started", function(){
@ -59,8 +68,8 @@ this.runISO = function(opts){
/* /*
* forward events/functions so non-worker world can reach them * forward events/functions so non-worker world can reach them
*/ */
this['emulator.create_file'] = function(){ emulator.create_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]) } this['emulator.read_file'] = function(){ emulator.read_file.apply(emulator, arguments[0]) }
// filename will be read from 9pfs: "/mnt/"+filename // filename will be read from 9pfs: "/mnt/"+filename
emulator.readFromPipe = function(filename,cb){ emulator.readFromPipe = function(filename,cb){
@ -73,6 +82,7 @@ this.runISO = function(opts){
importScripts("feat/javascript.js") importScripts("feat/javascript.js")
importScripts("feat/index.html.js") importScripts("feat/index.html.js")
importScripts("feat/autorestore.js")
} }
/* /*
* forward events/functions so non-worker world can reach them * forward events/functions so non-worker world can reach them

View File

@ -92,14 +92,9 @@ const TERMINAL_THEME = {
AFRAME.registerComponent('xterm', { AFRAME.registerComponent('xterm', {
schema: Object.assign({ schema: Object.assign({
cols: { cols: { type: 'number', default: 110, },
type: 'number', rows: { type: 'number', default: Math.floor( (window.innerHeight * 0.7 ) * 0.054 ) },
default: 110, canvasLatency:{ type:'number', default: 200 }
},
rows: {
type: 'number',
default: Math.floor( (window.innerHeight * 0.7 ) * 0.054 )
},
}, TERMINAL_THEME), }, TERMINAL_THEME),
write: function(message) { write: function(message) {
@ -139,6 +134,7 @@ AFRAME.registerComponent('xterm', {
}, {}) }, {})
const term = this.term = new Terminal({ const term = this.term = new Terminal({
logLevel:"off",
theme: theme, theme: theme,
allowTransparency: true, allowTransparency: true,
cursorBlink: true, cursorBlink: true,
@ -157,7 +153,7 @@ AFRAME.registerComponent('xterm', {
//this.term._core.viewport._innerRefresh() //this.term._core.viewport._innerRefresh()
this.term._core.renderer._renderDebouncer._innerRefresh() this.term._core.renderer._renderDebouncer._innerRefresh()
} }
},150) }, this.data.canvasLatency)
this.term.open(terminalElement) this.term.open(terminalElement)
this.term.focus() this.term.focus()
@ -191,6 +187,7 @@ AFRAME.registerComponent('xterm', {
const material = this.el.planeText.getObject3D('mesh').material const material = this.el.planeText.getObject3D('mesh').material
if (!material.map ) return if (!material.map ) return
if( this.cursorCanvas ) this.canvasContext.drawImage(this.cursorCanvas, 0,0) if( this.cursorCanvas ) this.canvasContext.drawImage(this.cursorCanvas, 0,0)
else console.log("no cursorCanvas")
material.map.needsUpdate = true material.map.needsUpdate = true
//material.needsUpdate = true //material.needsUpdate = true
} }