From aa7ae774e60b0d8c26869a608d485950f63f4763 Mon Sep 17 00:00:00 2001 From: Sucukdeluxe <259325684+Sucukdeluxe@users.noreply.github.com> Date: Fri, 21 Aug 2026 07:51:11 +0200 Subject: [PATCH] fix: preserve remote-session compositor rendering Remove the RDP-specific hardware acceleration shutdown that forced Chromium renderers into software compositing on Windows Server sessions. Add a production-wide startup invariant and a hidden full-width Electron compositor regression covering renderer switches, viewport geometry, and both painted edges. --- lib/startup-renderer.js | 8 +-- main.js | 3 +- tests/startup-renderer.test.js | 102 ++++++++++++++++++++++++++++++--- 3 files changed, 95 insertions(+), 18 deletions(-) diff --git a/lib/startup-renderer.js b/lib/startup-renderer.js index a49801b..5d4bdca 100644 --- a/lib/startup-renderer.js +++ b/lib/startup-renderer.js @@ -1,9 +1,3 @@ -function configureStartupRenderer(app, env = process.env, platform = process.platform) { - const sessionName = String(env && env.SESSIONNAME || ''); - if (platform === 'win32' && /^RDP-/i.test(sessionName)) app.disableHardwareAcceleration(); - return app; -} - function resolveStartupLanguage(config) { return config && config.globalSettings && config.globalSettings.language === 'de' ? 'de' : 'en'; } @@ -44,4 +38,4 @@ function createStartupWindow(BrowserWindow, options) { }; } -module.exports = { configureStartupRenderer, createStartupWindow, resolveStartupLanguage, createStartupQuery }; +module.exports = { createStartupWindow, resolveStartupLanguage, createStartupQuery }; diff --git a/main.js b/main.js index f168069..670c402 100644 --- a/main.js +++ b/main.js @@ -1,8 +1,7 @@ process.env.UV_THREADPOOL_SIZE = process.env.UV_THREADPOOL_SIZE || '8'; const { monitorEventLoopDelay, PerformanceObserver } = require('perf_hooks'); const { app, BrowserWindow, ipcMain, dialog, clipboard, nativeTheme, Tray, Menu, nativeImage } = require('electron'); -const { configureStartupRenderer, createStartupWindow, createStartupQuery } = require('./lib/startup-renderer'); -configureStartupRenderer(app); +const { createStartupWindow, createStartupQuery } = require('./lib/startup-renderer'); nativeTheme.themeSource = 'dark'; const path = require('path'); const fs = require('fs'); diff --git a/tests/startup-renderer.test.js b/tests/startup-renderer.test.js index 9eac778..899e9a7 100644 --- a/tests/startup-renderer.test.js +++ b/tests/startup-renderer.test.js @@ -1,9 +1,11 @@ const test = require('node:test'); const assert = require('node:assert/strict'); const { EventEmitter } = require('node:events'); +const { execFileSync } = require('node:child_process'); const fs = require('node:fs'); +const os = require('node:os'); const path = require('node:path'); -const { configureStartupRenderer, createStartupWindow, resolveStartupLanguage, createStartupQuery } = require('../lib/startup-renderer'); +const { createStartupWindow, resolveStartupLanguage, createStartupQuery } = require('../lib/startup-renderer'); class TestBrowserWindow extends EventEmitter { constructor(options) { @@ -32,16 +34,98 @@ class TestBrowserWindow extends EventEmitter { } } -test('configureStartupRenderer leaves hardware acceleration enabled for a local Windows session', () => { - let calls = 0; - configureStartupRenderer({ disableHardwareAcceleration() { calls++; } }, { SESSIONNAME: 'Console' }, 'win32'); - assert.equal(calls, 0); +test('production startup never forces software compositing', () => { + const projectRoot = path.join(__dirname, '..'); + const pending = [path.join(projectRoot, 'main.js'), path.join(projectRoot, 'lib')]; + const sourceFiles = []; + while (pending.length) { + const target = pending.pop(); + const stat = fs.statSync(target); + if (stat.isDirectory()) { + for (const entry of fs.readdirSync(target)) pending.push(path.join(target, entry)); + } else if (target.endsWith('.js')) { + sourceFiles.push(target); + } + } + for (const sourceFile of sourceFiles) { + const source = fs.readFileSync(sourceFile, 'utf8'); + assert.doesNotMatch(source, /disableHardwareAcceleration|disable-gpu(?:-compositing)?/u, path.relative(projectRoot, sourceFile)); + } }); -test('configureStartupRenderer disables hardware acceleration for a Windows Remote Desktop session', () => { - let calls = 0; - configureStartupRenderer({ disableHardwareAcceleration() { calls++; } }, { SESSIONNAME: 'RDP-Tcp#12' }, 'win32'); - assert.equal(calls, 1); +test('Windows compositor paints the full hidden surface with an RDP session environment', { skip: process.platform !== 'win32' }, () => { + const projectRoot = path.join(__dirname, '..'); + const probeRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'mhu-rdp-compositor-')); + const probePath = path.join(probeRoot, 'probe.cjs'); + const outputPath = path.join(probeRoot, 'result.json'); + const userDataPath = path.join(probeRoot, 'user-data'); + const probeSource = ` +const { app, BrowserWindow } = require('electron'); +const { execFileSync } = require('node:child_process'); +const fs = require('node:fs'); +const outputPath = process.env.MHU_RDP_COMPOSITOR_OUTPUT; +function pixelAt(bitmap, width, x, y) { + const offset = (y * width + x) * 4; + return [bitmap[offset + 2], bitmap[offset + 1], bitmap[offset], bitmap[offset + 3]]; +} +app.whenReady().then(async () => { + const window = new BrowserWindow({ + show: false, + width: 2544, + height: 1353, + useContentSize: true, + backgroundColor: '#0f0f0f', + webPreferences: { contextIsolation: true, nodeIntegration: false } + }); + const readyToShow = new Promise(resolve => window.once('ready-to-show', resolve)); + const document = '
'; + await window.loadURL('data:text/html;charset=utf-8,' + encodeURIComponent(document)); + await readyToShow; + await app.getGPUInfo('complete'); + const image = await window.webContents.capturePage(); + const size = image.getSize(); + const bitmap = image.toBitmap(); + const dom = await window.webContents.executeJavaScript('({ innerWidth, innerHeight, devicePixelRatio })'); + const rendererPid = window.webContents.getOSProcessId(); + const rendererCommandLine = execFileSync('powershell.exe', ['-NoProfile', '-Command', '(Get-CimInstance Win32_Process -Filter "ProcessId = ' + rendererPid + '").CommandLine'], { encoding: 'utf8' }).trim(); + const middleY = Math.floor(size.height / 2); + fs.writeFileSync(outputPath, JSON.stringify({ + size, + dom, + gpuFeatureStatus: app.getGPUFeatureStatus(), + rendererCommandLine, + leftEdge: pixelAt(bitmap, size.width, 0, middleY), + rightEdge: pixelAt(bitmap, size.width, size.width - 1, middleY) + }), 'utf8'); + window.destroy(); + app.exit(0); +}).catch(error => { + fs.writeFileSync(outputPath, JSON.stringify({ error: error.stack || String(error) }), 'utf8'); + app.exit(1); +}); +`; + fs.writeFileSync(probePath, probeSource, 'utf8'); + try { + const electronPath = path.join(projectRoot, 'node_modules', 'electron', 'dist', 'electron.exe'); + execFileSync(electronPath, [probePath, `--user-data-dir=${userDataPath}`], { + cwd: projectRoot, + env: { ...process.env, SESSIONNAME: 'RDP-Tcp#12', MHU_RDP_COMPOSITOR_OUTPUT: outputPath }, + encoding: 'utf8', + stdio: ['ignore', 'pipe', 'pipe'], + timeout: 30000, + windowsHide: true + }); + const result = JSON.parse(fs.readFileSync(outputPath, 'utf8')); + assert.equal(result.error, undefined); + assert.doesNotMatch(result.rendererCommandLine, /--disable-gpu-compositing/u); + assert.equal(result.size.width, Math.round(result.dom.innerWidth * result.dom.devicePixelRatio)); + assert.equal(result.size.height, Math.round(result.dom.innerHeight * result.dom.devicePixelRatio)); + assert.ok(result.size.width > 2048); + assert.ok(result.leftEdge[1] > result.leftEdge[0] && result.leftEdge[1] > result.leftEdge[2]); + assert.ok(result.rightEdge[0] > result.rightEdge[1] && result.rightEdge[2] > result.rightEdge[1]); + } finally { + fs.rmSync(probeRoot, { recursive: true, force: true }); + } }); test('resolveStartupLanguage accepts only the supported persisted language', () => {