From ed96742680b0f1a37741220489206b26f50ed5cb Mon Sep 17 00:00:00 2001 From: lucas lelievre Date: Thu, 26 Mar 2026 05:15:41 +0100 Subject: [PATCH 1/5] Electron fixes after RC feedbacks (#1784) --- .github/workflows/build.yml | 18 ++++ gui/electron/main/index.ts | 94 ++++++++++++------- gui/electron/main/logger.ts | 8 ++ gui/electron/main/paths.ts | 2 +- .../dev/slimevr/posestreamer/BVHRecorder.kt | 4 + .../tracking/trackers/udp/UDPDevice.kt | 34 ++++++- 6 files changed, 123 insertions(+), 37 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 9d6cf5343..f556ba37e 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -231,6 +231,24 @@ jobs: name: release-android path: SlimeVR-android.apk + - name: Build Google Play release bundle + if: startsWith(github.ref, 'refs/tags/') + run: ./gradlew :server:android:bundleRelease + env: + ANDROID_STORE_FILE: ${{ secrets.ANDROID_GPLAY_STORE_FILE }} + ANDROID_STORE_PASSWD: ${{ secrets.ANDROID_GPLAY_STORE_PASSWD }} + ANDROID_KEY_ALIAS: ${{ secrets.ANDROID_GPLAY_KEY_ALIAS }} + ANDROID_KEY_PASSWD: ${{ secrets.ANDROID_GPLAY_KEY_PASSWD }} + + - name: Upload the Google Play artifact + uses: actions/upload-artifact@v6 + if: startsWith(github.ref, 'refs/tags/') + with: + # Artifact name + name: 'SlimeVR-Android-GPDev' # optional, default is artifact + # A file, directory or wildcard pattern that describes what to upload + path: server/android/build/outputs/bundle/release/* + create-release: name: Finalize Release Draft needs: [package-desktop, bundle-android, build-server-jar, build-gui-frontend] diff --git a/gui/electron/main/index.ts b/gui/electron/main/index.ts index 35f29090f..34a766432 100644 --- a/gui/electron/main/index.ts +++ b/gui/electron/main/index.ts @@ -14,7 +14,7 @@ import { IPC_CHANNELS } from '../shared'; import path, { dirname, join } from 'path'; import open from 'open'; import trayIcon from '../resources/icons/icon.png?asset'; -import appleTrayIcon from '../resources/icons/appleTrayIcon.png?asset'; +import appleTrayIcon from '../resources/icons/Square30x30Logo.png?asset'; import { readFile, stat } from 'fs/promises'; import { getPlatform, handleIpc, isPortAvailable } from './utils'; import { @@ -26,7 +26,7 @@ import { getWindowStateFile, } from './paths'; import { stores } from './store'; -import { logger } from './logger'; +import { closeLogger, logger } from './logger'; import { writeFileSync } from 'node:fs'; import { spawn } from 'node:child_process'; @@ -36,11 +36,16 @@ import { ServerStatusEvent } from 'electron/preload/interface'; import { mkdir } from 'node:fs/promises'; import { MenuItem } from 'electron/main'; +// Fixes colors looking washed on linux +// Might affect hdr +if (process.platform === 'linux') { + app.commandLine.appendSwitch('disable-features', 'WaylandWpColorManagerV1'); + app.commandLine.appendSwitch('force-color-profile', 'srgb'); +} -app.setPath('userData', getGuiDataFolder()) -app.setPath('sessionData', join(getGuiDataFolder(), 'electron')) +app.setPath('userData', getGuiDataFolder()); +app.setPath('sessionData', join(getGuiDataFolder(), 'electron')); -// Register custom protocol to handle asset paths with leading slashes protocol.registerSchemesAsPrivileged([ { scheme: 'app', @@ -339,8 +344,7 @@ function createWindow() { menu.append(new MenuItem({ label: 'Copy', role: 'copy' })); menu.append(new MenuItem({ label: 'Paste', role: 'paste' })); - if (mainWindow) - menu.popup({ window: mainWindow }); + if (mainWindow) menu.popup({ window: mainWindow }); }); } @@ -353,7 +357,7 @@ const checkEnvironmentVariables = () => { 'SlimeVR', `You have environment variables ${set.join(', ')} set, which may cause the SlimeVR Server to fail to launch properly.` ); - app.exit(0); + app.quit(); } }; @@ -380,36 +384,60 @@ const spawnServer = async () => { 'SlimeVR', `Couldn't find a compatible Java version, please download Java 17 or higher` ); - app.exit(0); + app.quit() return; } logger.info({ javaBin, serverJar }, 'Found Java and server jar'); - - const process = spawn(javaBin, ['-Xmx128M', '-jar', serverJar, 'run']); - - process.stdout?.on('data', (message) => { - mainWindow?.webContents.send(IPC_CHANNELS.SERVER_STATUS, { - message: message.toString(), - type: 'stdout', - } satisfies ServerStatusEvent); + const platform = getPlatform(); + const serverWorkdir = getServerDataFolder() + const serverProcess = spawn(javaBin, ['-Xmx128M', '-jar', serverJar, 'run'], { + cwd: serverWorkdir, + shell: false, + env: + platform === 'windows' + ? { + ...process.env, + APPDATA: app.getPath('appData'), + LOCALAPPDATA: process.env['USERPROFILE'] ? path.join(process.env['USERPROFILE'], 'AppData', 'Local') : undefined, + } + : undefined, }); - process.stderr?.on('data', (message) => { - mainWindow?.webContents.send(IPC_CHANNELS.SERVER_STATUS, { - message: message.toString(), - type: 'stderr', - } satisfies ServerStatusEvent); + const sendToWindow = (event: ServerStatusEvent) => { + if (mainWindow && !mainWindow.webContents.isDestroyed()) { + mainWindow.webContents.send(IPC_CHANNELS.SERVER_STATUS, event); + } + }; + + serverProcess.stdout?.on('data', (message) => { + sendToWindow({ message: message.toString(), type: 'stdout' }); }); + serverProcess.stderr?.on('data', (message) => { + sendToWindow({ message: message.toString(), type: 'stderr' }); + }); + + serverProcess.on('error', (err) => { + logger.info({ err }, 'Error launching the java server'); + if (!isQuitting) app.quit(); + }) + + serverProcess.on('exit', () => { + logger.info('Server process exiting'); + }) + + const exited = new Promise((resolve) => serverProcess.once('exit', resolve)); + return { - process: process, - close: () => { - process.kill('SIGTERM'); - }, + process: serverProcess, + close: () => serverProcess.kill(), + waitForExit: () => exited, }; }; +let isQuitting = false; + app.whenReady().then(async () => { // Register protocol handler for app:// scheme to handle assets with leading slashes protocol.handle('app', (request) => { @@ -431,18 +459,18 @@ app.whenReady().then(async () => { } }); - process.on('exit', () => { - server?.close(); - }); - - app.on('before-quit', async () => { + app.on('before-quit', async (event) => { + if (isQuitting) return; + isQuitting = true; + event.preventDefault(); logger.info('App quitting, saving...'); server?.close(); + await server?.waitForExit(); stores.settings.save(); stores.cache.save(); - discordPresence.destroy(); - await saveWindowState(); + await closeLogger(); + app.exit(0); }); }); diff --git a/gui/electron/main/logger.ts b/gui/electron/main/logger.ts index b870b7169..e2f7d60a0 100644 --- a/gui/electron/main/logger.ts +++ b/gui/electron/main/logger.ts @@ -24,3 +24,11 @@ const transport = pino.transport({ }); export const logger = pino(transport); + +export const closeLogger = () => + new Promise((resolve) => { + logger.flush(() => { + transport.once('close', resolve); + transport.end(); + }); + }); diff --git a/gui/electron/main/paths.ts b/gui/electron/main/paths.ts index 437901022..da9dfb97c 100644 --- a/gui/electron/main/paths.ts +++ b/gui/electron/main/paths.ts @@ -103,12 +103,12 @@ export const findSystemJRE = async (sharedDir: string) => { export const findServerJar = () => { const paths = [ options.path ? path.resolve(options.path) : undefined, + app.isPackaged ? path.resolve(process.resourcesPath) : undefined, // AppImage passes the fakeroot in `APPDIR` env var. process.env['APPDIR'] ? path.resolve(join(process.env['APPDIR'], 'usr/share/slimevr/')) : undefined, path.dirname(app.getPath('exe')), - // For flatpack container path.resolve('/app/share/slimevr/'), path.resolve('/usr/share/slimevr/'), diff --git a/server/core/src/main/java/dev/slimevr/posestreamer/BVHRecorder.kt b/server/core/src/main/java/dev/slimevr/posestreamer/BVHRecorder.kt index 8fe09c92b..89d4959d8 100644 --- a/server/core/src/main/java/dev/slimevr/posestreamer/BVHRecorder.kt +++ b/server/core/src/main/java/dev/slimevr/posestreamer/BVHRecorder.kt @@ -19,6 +19,10 @@ class BVHRecorder(server: VRServer) { val file = if (filePath.isDirectory()) { getBvhFile(filePath) ?: return } else { + if (filePath.extension != "bvh") { + LogManager.severe("[BVH] Invalid file extension for bvh file \"${filePath}\".") + return + } filePath } diff --git a/server/core/src/main/java/dev/slimevr/tracking/trackers/udp/UDPDevice.kt b/server/core/src/main/java/dev/slimevr/tracking/trackers/udp/UDPDevice.kt index bed58741c..7eac78d83 100644 --- a/server/core/src/main/java/dev/slimevr/tracking/trackers/udp/UDPDevice.kt +++ b/server/core/src/main/java/dev/slimevr/tracking/trackers/udp/UDPDevice.kt @@ -44,6 +44,18 @@ class UDPDevice( @JvmField var lastPacketNumber: Long = -1 + @JvmField + var totalPacketsReceived: Int = 0 + + @JvmField + var acceptedPackets: Int = 0 + + @JvmField + var lastPacketCounterReset: Long = System.currentTimeMillis() + + val packetLossPercent: Float + get() = if (totalPacketsReceived == 0) 0f else (1f - acceptedPackets.toFloat() / totalPacketsReceived.toFloat()) + @JvmField var protocol: NetworkProtocol? = null @@ -68,9 +80,25 @@ class UDPDevice( var firmwareFeatures = FirmwareFeatures() fun isNextPacket(packetId: Long): Boolean { - if (packetId != 0L && packetId <= lastPacketNumber) return false - lastPacketNumber = packetId - return true + val now = System.currentTimeMillis() + if (now - lastPacketCounterReset >= 10_000L) { + totalPacketsReceived = 0 + acceptedPackets = 0 + lastPacketCounterReset = now + } + totalPacketsReceived++ + val accepted = packetId == 0L || packetId > lastPacketNumber + if (accepted) { + lastPacketNumber = packetId + acceptedPackets++ + } + val lost = totalPacketsReceived - acceptedPackets + trackers.values.forEach { + it.packetsReceived = totalPacketsReceived + it.packetsLost = lost + it.packetLoss = packetLossPercent + } + return accepted } override fun toString(): String = "udp:/$ipAddress" From a9f553729e45a41d8a43ec28593b7797d3b8bffd Mon Sep 17 00:00:00 2001 From: lucas lelievre Date: Thu, 26 Mar 2026 08:09:10 +0100 Subject: [PATCH 2/5] Electron fixes macos (#1797) --- gui/electron/main/index.ts | 7 ++++--- gui/electron/preload/index.ts | 1 + gui/electron/preload/interface.d.ts | 1 + gui/electron/shared.ts | 2 +- gui/src/components/TopBar.tsx | 2 +- 5 files changed, 8 insertions(+), 5 deletions(-) diff --git a/gui/electron/main/index.ts b/gui/electron/main/index.ts index 34a766432..2af187d18 100644 --- a/gui/electron/main/index.ts +++ b/gui/electron/main/index.ts @@ -273,6 +273,9 @@ function createWindow() { case 'close': mainWindow?.close(); break; + case 'hide': + mainWindow?.hide(); + break; case 'minimize': mainWindow?.minimize(); break; @@ -454,9 +457,7 @@ app.whenReady().then(async () => { logger.info('SlimeVR started!'); app.on('window-all-closed', () => { - if (process.platform !== 'darwin') { - app.quit(); - } + app.quit(); }); app.on('before-quit', async (event) => { diff --git a/gui/electron/preload/index.ts b/gui/electron/preload/index.ts index e9682ca0d..376393c94 100644 --- a/gui/electron/preload/index.ts +++ b/gui/electron/preload/index.ts @@ -12,6 +12,7 @@ contextBridge.exposeInMainWorld('electronAPI', { openUrl: (url) => ipcRenderer.invoke(IPC_CHANNELS.OPEN_URL, url), osStats: () => ipcRenderer.invoke(IPC_CHANNELS.OS_STATS), close: () => ipcRenderer.invoke(IPC_CHANNELS.WINDOW_ACTIONS, 'close'), + hide: () => ipcRenderer.invoke(IPC_CHANNELS.WINDOW_ACTIONS, 'hide'), minimize: () => ipcRenderer.invoke(IPC_CHANNELS.WINDOW_ACTIONS, 'minimize'), maximize: () => ipcRenderer.invoke(IPC_CHANNELS.WINDOW_ACTIONS, 'maximize'), getStorage: async (type) => { diff --git a/gui/electron/preload/interface.d.ts b/gui/electron/preload/interface.d.ts index 03c47a1f7..e7822f9a5 100644 --- a/gui/electron/preload/interface.d.ts +++ b/gui/electron/preload/interface.d.ts @@ -43,6 +43,7 @@ export interface IElectronAPI { openLogsFolder: () => Promise; openConfigFolder: () => Promise; close: () => void; + hide: () => void; minimize: () => void; maximize: () => void; showDecorations: (decorations: boolean) => void; diff --git a/gui/electron/shared.ts b/gui/electron/shared.ts index 1364550bd..5abe21876 100644 --- a/gui/electron/shared.ts +++ b/gui/electron/shared.ts @@ -25,7 +25,7 @@ export const IPC_CHANNELS = { export interface IpcInvokeMap { [IPC_CHANNELS.OPEN_URL]: (url: string) => void; [IPC_CHANNELS.OS_STATS]: () => Promise; - [IPC_CHANNELS.WINDOW_ACTIONS]: (action: 'close' | 'minimize' | 'maximize') => void; + [IPC_CHANNELS.WINDOW_ACTIONS]: (action: 'close' | 'minimize' | 'maximize' | 'hide') => void; [IPC_CHANNELS.LOG]: (type: 'info' | 'error' | 'warn', ...args: unknown[]) => void; [IPC_CHANNELS.OPEN_DIALOG]: ( options: OpenDialogOptions diff --git a/gui/src/components/TopBar.tsx b/gui/src/components/TopBar.tsx index e15006485..60d7b2d6e 100644 --- a/gui/src/components/TopBar.tsx +++ b/gui/src/components/TopBar.tsx @@ -81,7 +81,7 @@ export function TopBar({ } if (config?.useTray && !dontTray) { - electron.api.minimize(); + electron.api.hide(); } else if ( config?.connectedTrackersWarning && connectedIMUTrackers.filter( From 1a4b19a5e10048fd5ad3e3188bc18f5d1ff45ee7 Mon Sep 17 00:00:00 2001 From: lucas lelievre Date: Mon, 30 Mar 2026 05:42:47 +0200 Subject: [PATCH 3/5] Make sure tags are fetched for version number checks --- .github/workflows/build.yml | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index f556ba37e..37f18f563 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -34,6 +34,8 @@ jobs: uses: actions/checkout@v6 with: submodules: recursive + - name: Get tags + run: git fetch --tags origin --recurse-submodules=no --force - name: Setup PNPM uses: pnpm/action-setup@v4 - name: Setup Node @@ -71,6 +73,8 @@ jobs: uses: actions/checkout@v6 with: submodules: recursive + - name: Get tags + run: git fetch --tags origin --recurse-submodules=no --force - name: Set up JDK 17 uses: actions/setup-java@v5 with: @@ -98,6 +102,8 @@ jobs: uses: actions/checkout@v6 with: submodules: recursive + - name: Get tags + run: git fetch --tags origin --recurse-submodules=no --force - name: Setup PNPM uses: pnpm/action-setup@v4 - name: Setup Node @@ -129,6 +135,8 @@ jobs: uses: actions/checkout@v6 with: submodules: recursive + - name: Get tags + run: git fetch --tags origin --recurse-submodules=no --force - name: Setup PNPM uses: pnpm/action-setup@v4 - name: Setup Node @@ -201,6 +209,8 @@ jobs: uses: actions/checkout@v6 with: submodules: recursive + - name: Get tags + run: git fetch --tags origin --recurse-submodules=no --force - name: Set up JDK 17 uses: actions/setup-java@v5 with: From fb77d3cf8add96480d8593c828e86491faf47ff7 Mon Sep 17 00:00:00 2001 From: gorbit99 Date: Tue, 31 Mar 2026 21:02:01 +0200 Subject: [PATCH 4/5] Downgrade JavaOSC version (#1801) --- server/core/build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/core/build.gradle.kts b/server/core/build.gradle.kts index 0f34ebb91..6736c72f3 100644 --- a/server/core/build.gradle.kts +++ b/server/core/build.gradle.kts @@ -72,7 +72,7 @@ dependencies { implementation("org.apache.commons:commons-lang3:3.20.0") implementation("org.apache.commons:commons-collections4:4.5.0") - implementation("com.illposed.osc:javaosc-core:0.9") + implementation("com.illposed.osc:javaosc-core:0.8") implementation("org.java-websocket:Java-WebSocket:1.+") implementation("com.melloware:jintellitype:1.+") implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.10.0") From 181c6599b726740395e24fa0960d61b3abb8c628 Mon Sep 17 00:00:00 2001 From: lucas lelievre Date: Tue, 31 Mar 2026 21:10:00 +0200 Subject: [PATCH 5/5] Increment versionCodeOffset from 4 to 5 --- server/android/build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/android/build.gradle.kts b/server/android/build.gradle.kts index e48cd0c3f..6eba1c723 100644 --- a/server/android/build.gradle.kts +++ b/server/android/build.gradle.kts @@ -160,7 +160,7 @@ android { // adds an offset of the version code as we might do apk releases in the middle of actual // releases if we failed on bundling or stuff - val versionCodeOffset = 4 + val versionCodeOffset = 5 // Defines the version number of your app. versionCode = (extra["gitVersionCode"] as? Int)?.plus(versionCodeOffset) ?: 0