import { BaseWindow } from 'electron'; import { expect } from 'chai'; import { randomUUID } from 'node:crypto'; import * as path from 'node:path'; import { closeWindow } from './lib/window-helpers'; const fixtures = path.resolve(__dirname, 'fixtures'); describe('sharedTexture module', () => { const { nativeImage } = require('electron'); const debugSpec = false; const dirPath = path.join(fixtures, 'api', 'shared-texture'); const osrPath = path.join(dirPath, 'osr.html'); const imagePath = path.join(dirPath, 'image.png'); const targetImage = nativeImage.createFromPath(imagePath); describe('import shared texture produced by osr', () => { const { app, BrowserWindow, sharedTexture, ipcMain } = require('electron'); afterEach(async () => { ipcMain.removeAllListeners(); for (const w of BaseWindow.getAllWindows()) { await closeWindow(w); } }); it('successfully imported and rendered with subtle api', (done) => { type CapturedTextureHolder = { importedSubtle: Electron.SharedTextureImportedSubtle, texture: Electron.OffscreenSharedTexture } const capturedTextures = new Map(); const preloadPath = path.join(dirPath, 'subtle', 'preload.js'); const htmlPath = path.join(dirPath, 'subtle', 'index.html'); const createWindow = () => { const win = new BrowserWindow({ width: 256, height: 256, show: debugSpec, webPreferences: { preload: preloadPath } }); const osr = new BrowserWindow({ width: 128, height: 128, show: debugSpec, webPreferences: { offscreen: { useSharedTexture: true } } }); osr.webContents.setFrameRate(1); osr.webContents.on('paint', (event: any) => { // Step 1: Input source of shared texture handle. const texture = event.texture; if (!texture) { console.error('No texture, GPU may be unavailable, skipping.'); done(); return; } // Step 2: Import as SharedTextureImported console.log(texture.textureInfo); const importedSubtle = sharedTexture.subtle.importSharedTexture(texture.textureInfo); // Step 3: Prepare for transfer to another process (win's renderer) const transfer = importedSubtle.startTransferSharedTexture(); const id = randomUUID(); capturedTextures.set(id, { importedSubtle, texture }); // Step 4: Send the shared texture to the renderer process (goto preload.js) win.webContents.send('shared-texture', id, transfer); }); ipcMain.on('shared-texture-done', (event: any, id: string) => { // Step 12: Release the shared texture resources at main process const data = capturedTextures.get(id); if (data) { capturedTextures.delete(id); const { importedSubtle, texture } = data; // Step 13: Release the imported shared texture importedSubtle.release(() => { // Step 14: Release the shared texture once GPU is done texture.release(); }); // Step 15: Slightly timeout and capture the node screenshot setTimeout(async () => { // Step 16: Compare the captured image with the target image const captured = await win.webContents.capturePage({ x: 16, y: 16, width: 128, height: 128 }); // Step 17: Resize the target image to match the captured image size, in case dpr != 1 const target = targetImage.resize({ ...captured.getSize() }); // Step 18: nativeImage have error comparing pixel data when color space is different, // send to browser for comparison using canvas. win.webContents.send('verify-captured-image', { captured: captured.toDataURL(), target: target.toDataURL() }); }, 300); } }); ipcMain.on('verify-captured-image-done', (event: any, result: { difference: number, total: number }) => { // Step 22: Verify the result from renderer process try { // macOS may have tiny color difference after the whole rendering process, // and the color may change slightly when resizing at device pixel ratio != 1. // Limit error should not be different more than 1% of the whole image. const ratio = result.difference / result.total; console.log('image difference: ', ratio); expect(ratio).to.be.lessThan(0.01); done(); } catch (e) { done(e); } }); ipcMain.on('webgpu-unavailable', () => { console.error('WebGPU is not available, skipping.'); done(); }); win.loadFile(htmlPath); osr.loadFile(osrPath); }; app.whenReady().then(() => { createWindow(); }); }).timeout(debugSpec ? 100000 : 10000); const runSharedTextureManagedTest = (done: Mocha.Done, iframe: boolean) => { const preloadPath = path.join(dirPath, 'managed', 'preload.js'); const htmlPath = path.join(dirPath, 'managed', iframe ? 'frame.html' : 'index.html'); const createWindow = () => { const win = new BrowserWindow({ width: 256, height: 256, show: debugSpec, webPreferences: { preload: preloadPath, nodeIntegrationInSubFrames: iframe } }); const osr = new BrowserWindow({ width: 128, height: 128, show: debugSpec, webPreferences: { offscreen: { useSharedTexture: true } } }); osr.webContents.setFrameRate(1); osr.webContents.on('paint', async (event: any) => { const targetFrame = iframe ? win.webContents.mainFrame.frames[0] : win.webContents.mainFrame; if (!targetFrame) { done(new Error('Target frame not found')); return; } // Step 1: Input source of shared texture handle. const texture = event.texture; if (!texture) { console.error('No texture, GPU may be unavailable, skipping.'); done(); return; } // Step 2: Import as SharedTextureImported console.log(texture.textureInfo); const imported = sharedTexture.importSharedTexture({ textureInfo: texture.textureInfo, allReferencesReleased: () => { // Release the shared texture source once GPU is done. // Will be called when all processes have finished using the shared texture. texture.release(); // Slightly timeout and capture the node screenshot setTimeout(async () => { // Compare the captured image with the target image const captured = await win.webContents.capturePage({ x: 16, y: 16, width: 128, height: 128 }); // Resize the target image to match the captured image size, in case dpr != 1 const target = targetImage.resize({ ...captured.getSize() }); // nativeImage have error comparing pixel data when color space is different, // send to browser for comparison using canvas. targetFrame.send('verify-captured-image', { captured: captured.toDataURL(), target: target.toDataURL() }); }, 300); } }); // Step 3: Transfer to another process (win's renderer) await sharedTexture.sendSharedTexture({ frame: iframe ? targetFrame : win.webContents.mainFrame, importedSharedTexture: imported }); // Step 4: Release the imported and wait for signal to release the source imported.release(); }); ipcMain.on('verify-captured-image-done', (event: any, result: { difference: number, total: number }) => { // Verify the result from renderer process try { // macOS may have tiny color difference after the whole rendering process, // and the color may change slightly when resizing at device pixel ratio != 1. // Limit error should not be different more than 1% of the whole image. const ratio = result.difference / result.total; console.log('image difference: ', ratio); expect(ratio).to.be.lessThan(0.01); done(); } catch (e) { setTimeout(() => done(e), 1000000); } }); ipcMain.on('webgpu-unavailable', () => { console.error('WebGPU is not available, skipping.'); done(); }); win.loadFile(htmlPath); osr.loadFile(osrPath); }; app.whenReady().then(() => { createWindow(); }); }; it('successfully imported and rendered with managed api, without iframe', (done) => { runSharedTextureManagedTest(done, false); }).timeout(debugSpec ? 100000 : 10000); it('successfully imported and rendered with managed api, with iframe', (done) => { runSharedTextureManagedTest(done, true); }).timeout(debugSpec ? 100000 : 10000); }); });