Files
electron/spec/api-shared-texture-spec.ts
2025-11-07 09:50:28 +01:00

282 lines
9.6 KiB
TypeScript

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<string, CapturedTextureHolder>();
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);
});
});