From f75f34d6a6c6c9a9d4d8913a41580933979dfe69 Mon Sep 17 00:00:00 2001 From: Leon van Kammen Date: Tue, 17 Sep 2024 16:59:38 +0000 Subject: [PATCH] added restore + requestAnimationFrameXR to mitigate requestAnimationFrame issues --- com/codemirror.js | 14 ++++++-- com/dom.js | 13 +++---- com/html-as-texture-in-xr.js | 6 +++- com/isoterminal.js | 45 ++++++++++++++++------- com/isoterminal/core.js | 6 ++-- com/isoterminal/feat/autorestore.js | 55 +++++++++++++++++++++++++++++ com/isoterminal/feat/boot.js | 13 ++++--- com/isoterminal/feat/xterm.js | 6 ++-- com/requestAnimationFrameXR.js | 50 ++++++++++++++++++++++++++ com/window.js | 2 ++ 10 files changed, 177 insertions(+), 33 deletions(-) create mode 100644 com/isoterminal/feat/autorestore.js create mode 100644 com/requestAnimationFrameXR.js diff --git a/com/codemirror.js b/com/codemirror.js index 0d36726..de18dfd 100644 --- a/com/codemirror.js +++ b/com/codemirror.js @@ -1,3 +1,5 @@ +if( AFRAME.components.codemirror ) delete AFRAME.components.codemirror + AFRAME.registerComponent('codemirror', { schema: { foo: { type:"string"} @@ -16,7 +18,6 @@ AFRAME.registerComponent('codemirror', { requires:{ window: "com/window.js", - htmltexture: "com/html-as-texture-in-xr.js", codemirror: "https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.58.1/codemirror.js", codemirrorcss: "https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.35.0/codemirror.css", cmtheme: "https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.35.0/theme/shadowfox.css" @@ -31,6 +32,13 @@ AFRAME.registerComponent('codemirror', { css: (me) => `.codemirror{ width:100%; } + .codemirror *{ + font-size: 14px; + font-family: "Cousine",Liberation Mono,DejaVu Sans Mono,Courier New,monospace; + font-weight:500 !important; + letter-spacing: 0 !important; + text-shadow: 0px 0px 10px #F075; + } .wb-body + .codemirror{ overflow:hidden; } .CodeMirror { margin-top:18px; @@ -47,7 +55,6 @@ AFRAME.registerComponent('codemirror', { DOMready: function(e){ console.log(`title: codemirror; uid: ${this.el.dom.id}; attach: #overlay; dom: #${this.el.dom.id};`) this.el.setAttribute("window", `title: codemirror; uid: ${this.el.dom.id}; attach: #overlay; dom: #${this.el.dom.id};`) - this.el.setAttribute("html-as-texture-in-xr", `domid: #${this.el.dom.id}`) // only show aframe-html in xr this.editor = CodeMirror( this.el.dom, { value: "function myScript(){return 100;}\n", mode: "javascript", @@ -61,6 +68,9 @@ AFRAME.registerComponent('codemirror', { } }) this.editor.setOption("theme", "shadowfox") + setTimeout( () => { + this.el.setAttribute("html-as-texture-in-xr", `domid: #${this.el.dom.id}`) // only show aframe-html in xr + },1500) }, }, diff --git a/com/dom.js b/com/dom.js index e157249..3eb98b9 100644 --- a/com/dom.js +++ b/com/dom.js @@ -36,6 +36,10 @@ if( !AFRAME.components.dom ){ AFRAME.registerComponent('dom',{ + requires: { + "requestAnimationFrameXR": "com/requestAnimationFrameXR.js" + }, + init: function(){ Object.values(this.el.components) .map( (c) => { @@ -127,12 +131,9 @@ if( !AFRAME.components.dom ){ return this }, - stubRequestAnimationFrame: function(){ - // stub, because WebXR with overrule this (it will not call the callback as expected in immersive mode) - const requestAnimationFrame = window.requestAnimationFrame - window.requestAnimationFrame = (cb) => { - setTimeout( cb, 25 ) - } + stubRequestAnimationFrame: async function(){ + let s = await AFRAME.utils.require(this.requires) + this.el.setAttribute("requestAnimationFrameXR","") } }) diff --git a/com/html-as-texture-in-xr.js b/com/html-as-texture-in-xr.js index f440f38..80440f8 100644 --- a/com/html-as-texture-in-xr.js +++ b/com/html-as-texture-in-xr.js @@ -1,4 +1,4 @@ -if( !AFRAME.components['html-as-textre-in-xr'] ){ +if( !AFRAME.components['html-as-texture-in-xr'] ){ AFRAME.registerComponent('html-as-texture-in-xr', { schema: { @@ -12,6 +12,10 @@ if( !AFRAME.components['html-as-textre-in-xr'] ){ }, init: async function () { + let el = document.querySelector(this.data.domid) + if( ! el ){ + return console.error("html-as-texture-in-xr: cannot get dom element "+this.data.dom.id) + } let s = await AFRAME.utils.require(this.dependencies) this.el.setAttribute("html",`html: ${this.data.domid}; cursor:#cursor; xrlayer: true`) this.el.setAttribute("visible", AFRAME.utils.XD() == '3D' ? 'true' : 'false' ) diff --git a/com/isoterminal.js b/com/isoterminal.js index d8af0f5..2416643 100644 --- a/com/isoterminal.js +++ b/com/isoterminal.js @@ -37,11 +37,12 @@ if( typeof AFRAME != 'undefined '){ cols: { type: 'number',"default": 120 }, rows: { type: 'number',"default": 30 }, padding: { type: 'number',"default": 18 }, + minimized: { type: 'boolean',"default":false}, maximized: { type: 'boolean',"default":true}, transparent: { type:'boolean', "default":false }, // need good gpu xterm: { type: 'boolean', "default":true }, // use xterm.js (slower) - memory: { type: 'number', "default":32 } // VM memory (in MB) - }, + memory: { type: 'number', "default":48 } // VM memory (in MB) + }, init: async function(){ this.el.object3D.visible = false @@ -74,6 +75,8 @@ if( typeof AFRAME != 'undefined '){ javascript: "com/isoterminal/feat/javascript.js", indexhtml: "com/isoterminal/feat/index.html.js", indexjs: "com/isoterminal/feat/index.js.js", + autorestore: "com/isoterminal/feat/autorestore.js", + localforage: "https://cdn.rawgit.com/mozilla/localForage/master/dist/localforage.js" }, dom: { @@ -90,16 +93,26 @@ if( typeof AFRAME != 'undefined '){ width:100%; height:100%; } + @font-face { + font-family: 'Cousine'; + font-style: normal; + font-weight: 400; + src: url(./assets/Cousine.ttf) format('truetype'); + } .isoterminal *{ white-space: pre; - font-size: 14px; - font-family: Liberation Mono,DejaVu Sans Mono,Courier New,monospace; - font-weight:900 !important; - letter-spacing: 0 !important; line-height:16px; display:inline; overflow: hidden; } + .isoterminal *, + .xterm-dom-renderer-owner-1 .xterm-rows { + font-size: 14px; + font-family: "Cousine",Liberation Mono,DejaVu Sans Mono,Courier New,monospace; + font-weight:500 !important; + letter-spacing: 0 !important; + text-shadow: 0px 0px 10px #F075; + } .isoterminal style{ display:none } @@ -108,6 +121,10 @@ if( typeof AFRAME != 'undefined '){ overflow:hidden; } + .XR .wb-body:has(> .isoterminal){ + background: #000; + } + .isoterminal div{ display:block; } .isoterminal span{ display: inline } @@ -163,7 +180,7 @@ if( typeof AFRAME != 'undefined '){ //instance.winbox.resize(720,380) let size = this.data.xterm ? 'width: 1024px; height:600px' : 'width: 720px; height:455px' - instance.setAttribute("window", `title: xrsh.iso; uid: ${instance.uid}; attach: #overlay; dom: #${instance.dom.id}; ${size}`) + instance.setAttribute("window", `title: xrsh.iso; uid: ${instance.uid}; attach: #overlay; dom: #${instance.dom.id}; ${size}; min: ${this.data.minimized}; max: ${this.data.maximized}`) }) instance.addEventListener('window.oncreate', (e) => { @@ -179,8 +196,11 @@ if( typeof AFRAME != 'undefined '){ this.isoterminal.addEventListener('postReady', (e)=>{ // bugfix: send window dimensions to xterm (xterm.js does that from dom-sizechange to xterm via escape codes) - if( this.data.maximized ) instance.winbox.maximize() - else instance.winbox.resize() + let wb = instance.winbox + if( this.data.maximized ){ + wb.restore() + wb.maximize() + }else wb.resize() }) this.isoterminal.addEventListener('ready', (e)=>{ @@ -208,14 +228,15 @@ if( typeof AFRAME != 'undefined '){ instance.addEventListener('window.onmaximize', resize ) const focus = (e) => { - if( this.isoterminal?.emulator?.serial_adapter?.focus ){ + if( this.isoterminal?.emulator?.serial_adapter?.term ){ this.isoterminal.emulator.serial_adapter.term.focus() } } - //instance.addEventListener('obbcollisionstarted', focus ) + instance.addEventListener('obbcollisionstarted', focus ) + this.el.sceneEl.addEventListener('enter-vr', focus ) this.el.sceneEl.addEventListener('enter-ar', focus ) - + instance.object3D.quaternion.copy( AFRAME.scenes[0].camera.quaternion ) // face towards camera }, diff --git a/com/isoterminal/core.js b/com/isoterminal/core.js index 790be71..88ab666 100644 --- a/com/isoterminal/core.js +++ b/com/isoterminal/core.js @@ -47,7 +47,7 @@ ISOTerminal.prototype.runISO = function(opts){ uart3:true, // /dev/ttyS3 wasm_path: "com/isoterminal/v86.wasm", memory_size: opts.memory * 1024 * 1024, - vga_memory_size: 1024, //2 * 1024 * 1024, + vga_memory_size: 2 * 1024 * 1024, screen_container: opts.dom, //serial_container: opts.dom, bios: { @@ -145,8 +145,8 @@ ISOTerminal.prototype.runISO = function(opts){ line += chr; } if( !ready && line.match(/^(\/ #|~%|\[.*\]>)/) ){ - this.emit('postReady') - setTimeout( () => this.emit('ready'), 500 ) + this.emit('postReady',e) + setTimeout( () => this.emit('ready',e), 500 ) ready = true } }); diff --git a/com/isoterminal/feat/autorestore.js b/com/isoterminal/feat/autorestore.js new file mode 100644 index 0000000..d4663a1 --- /dev/null +++ b/com/isoterminal/feat/autorestore.js @@ -0,0 +1,55 @@ +ISOTerminal.addEventListener('emulator-started', function(e){ + this.autorestore(e) +}) + +ISOTerminal.prototype.convert = { + + arrayBufferToBase64: function(buffer){ + let binary = ''; + const bytes = new Uint8Array(buffer); + const len = bytes.byteLength; + for (let i = 0; i < len; i++) binary += String.fromCharCode(bytes[i]); + return window.btoa(binary); + }, + + base64ToArrayBuffer: function(base64) { + const binaryString = window.atob(base64); + const len = binaryString.length; + const bytes = new Uint8Array(len); + + for (let i = 0; i < len; i++) { + bytes[i] = binaryString.charCodeAt(i); + } + return bytes.buffer; + } +} + +ISOTerminal.prototype.autorestore = async function(e){ + + localforage.setDriver([ + localforage.INDEXEDDB, + localforage.WEBSQL, + localforage.LOCALSTORAGE + ]).then( () => { + + localforage.getItem("state", (err,stateBase64) => { + if( !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) + this.send("alert last session restored\n") + }, 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) ) + } + }) +} + diff --git a/com/isoterminal/feat/boot.js b/com/isoterminal/feat/boot.js index 357c1af..cfc943c 100644 --- a/com/isoterminal/feat/boot.js +++ b/com/isoterminal/feat/boot.js @@ -1,8 +1,8 @@ -ISOTerminal.addEventListener('ready', function(){ - this.boot() +ISOTerminal.addEventListener('ready', function(e){ + setTimeout( () => this.boot(), 50 ) // because of autorestore.js }) -ISOTerminal.prototype.boot = async function(){ +ISOTerminal.prototype.boot = async function(e){ // set environment let env = ['export BROWSER=1'] for ( let i in document.location ){ @@ -10,9 +10,12 @@ ISOTerminal.prototype.boot = async function(){ env.push( 'export '+String(i).toUpperCase()+'="'+document.location[i]+'"') } await this.emulator.create_file("profile.browser", this.toUint8Array( env.join('\n') ) ) + if( this.serial_input == 0 ){ - let boot = "source /etc/profile\n" - this.send(boot+"\n") + if( !this.noboot ){ + let boot = "source /etc/profile\n" + this.send(boot+"\n") + } } if( this.emulator.serial_adapter ) this.emulator.serial_adapter.term.focus() diff --git a/com/isoterminal/feat/xterm.js b/com/isoterminal/feat/xterm.js index 6342887..3d0cd58 100644 --- a/com/isoterminal/feat/xterm.js +++ b/com/isoterminal/feat/xterm.js @@ -16,9 +16,7 @@ ISOTerminal.prototype.xtermInit = function(){ window.Terminal = function(opts){ const term = new window._Terminal({ ...opts, cursorBlink:true, - onSelectionChange: function(e){ - debugger - }, + onSelectionChange: function(e){ console.log("selectchange") }, letterSpacing: 0 }) @@ -38,7 +36,7 @@ ISOTerminal.prototype.xtermInit = function(){ const resize = (w,h) => { setTimeout( () => { - isoterm.xtermAutoResize(isoterm.emulator.serial_adapter.term, isoterm.instance,-2) + isoterm.xtermAutoResize(isoterm.emulator.serial_adapter.term, isoterm.instance,-3) },800) // wait for resize anim } isoterm.instance.addEventListener('window.onresize', resize ) diff --git a/com/requestAnimationFrameXR.js b/com/requestAnimationFrameXR.js new file mode 100644 index 0000000..1192bec --- /dev/null +++ b/com/requestAnimationFrameXR.js @@ -0,0 +1,50 @@ +/* + * ## requestAnimationFrameXR + * + * reroutes requestAnimationFrame-calls to xrSession.requestAnimationFrame + * reason: in immersive mode this function behaves differently + * (causing HTML apps like xterm.js not getting updated due to relying + * on window.requestAnimationFrame) + * + * ```html + * + * ``` + */ + +if( !AFRAME.systems.requestAnimationFrameXR ){ + + AFRAME.registerSystem('requestAnimationFrameXR',{ + + init: function init(){ + if( document.location.hostname.match(/localhost/) ) return // allow webxr polyfill during development (they hang in XR) + AFRAME.systems.requestAnimationFrameXR.q = [] + this.sceneEl.addEventListener('enter-vr', this.enable ) + this.sceneEl.addEventListener('enter-ar', this.enable ) + this.sceneEl.addEventListener('exit-vr', this.disable ) + this.sceneEl.addEventListener('exit-ar', this.disable ) + }, + + enable: function enable(){ + this.requestAnimationFrame = window.requestAnimationFrame + // NOTE: we don't call xrSession.requestAnimationFrame directly like this: + // + // window.requestAnimationFrame = AFRAME.utils.throttleTick( (cb) => this.sceneEl.xrSession.requestAnimationFrame(cb), 50 ) + // + // as that breaks webxr polyfill (for in-browser testing) + // instead we defer calls to tick() (which is called both in XR and non-XR) + // + window.requestAnimationFrame = (cb) => AFRAME.systems.requestAnimationFrameXR.q.push(cb) + const q = AFRAME.systems.requestAnimationFrameXR.q + this.tick = AFRAME.utils.throttleTick( () => { + while( q.length != 0 ) (q.pop())() + },50) + }, + + disable: function disable(){ + delete this.tick + window.requestAnimationFrame = this.requestAnimationFrame + } + + + }) +} diff --git a/com/window.js b/com/window.js index f6d6d1d..1ebb60b 100644 --- a/com/window.js +++ b/com/window.js @@ -7,6 +7,7 @@ AFRAME.registerComponent('window', { attach: {type:'selector'}, dom: {type:'selector'}, max: {type:'boolean',"default":false}, + min: {type:'boolean',"default":false}, x: {type:'string',"default":"center"}, y: {type:'string',"default":"center"} }, @@ -35,6 +36,7 @@ AFRAME.registerComponent('window', { root: this.data.attach || document.body, mount: this.data.dom, max: this.data.max, + min: this.data.min, onresize: () => this.el.emit('window.onresize',{}), onmaximize: () => this.el.emit('window.onmaximize',{}), oncreate: (e) => {