首页
/ Electron SharedTextureTransfer 对象详解:共享纹理跨进程传输的机制与实战

Electron SharedTextureTransfer 对象详解:共享纹理跨进程传输的机制与实战

2026-09-06 16:43:32作者:郦嵘贵Just

SharedTextureTransfer 是 Electron sharedTexture 模块中用于在浏览器进程与渲染进程之间传输共享纹理的核心数据结构:它把一个已在某进程导入的 SharedImage 序列化为纯数据对象,使纹理引用可以像普通 IPC 消息一样被传递。读懂这个对象的六个字段及其底层生成/消费流程后,你能够基于 OSR(离屏渲染)产出的纹理句柄,将外部共享纹理导入 Electron、跨进程转移到渲染进程,并用 VideoFrame + WebGPU 完成零拷贝渲染与安全的资源释放。

SharedTextureTransfer 在 sharedTexture API 体系中的位置

sharedTexture 模块负责把平台特定的纹理句柄(Windows 的 NT HANDLE、macOS 的 IOSurfaceRef、Linux 的 NativePixmapHandle)导入 Electron 并转换为标准的 Web VideoFrame,详见 sharedTexture 模块文档。整个跨进程流转涉及四类对象:

对象 作用 文档
SharedTextureSubtle 底层细粒度 API:importSharedTexturefinishTransferSharedTexture shared-texture-subtle.md
SharedTextureImportedSubtle 已导入纹理的句柄对象:getVideoFramereleasestartTransferSharedTexturegetFrameCreationSyncTokensetReleaseSyncToken shared-texture-imported-subtle.md
SharedTextureTransfer 本文主题:可序列化、可跨进程传输的纹理描述数据 shared-texture-transfer.md
SharedTextureSyncToken 不透明同步令牌数据,用于 GPU 侧的资源生命周期同步 shared-texture-sync-token.md

典型链路为:

  1. 主进程从 OSR paint 事件(offscreen: { useSharedTexture: true })或其他原生渠道获得纹理句柄;
  2. 调用 sharedTexture.subtle.importSharedTexture(textureInfo) 得到 SharedTextureImportedSubtle
  3. 调用其 startTransferSharedTexture() 得到 SharedTextureTransfer
  4. 通过 IPC(webContents.send 或托管 API sendSharedTexture)把该对象送入渲染进程;
  5. 渲染进程调用 sharedTexture.subtle.finishTransferSharedTexture(transfer) 取回一个新的 SharedTextureImportedSubtle,之后用 getVideoFrame() 参与渲染。

shared-texture-transfer.md 文档的最后一句正点明了第 5 步:“Use sharedTexture.subtle.finishTransferSharedTexture to get SharedTextureImportedSubtle back.”

字段逐项解析:六个 Readonly 属性

SharedTextureTransfer 的全部属性均为 _Readonly_,在 C++ 侧通过 SetReadOnly 绑定(见 electron_api_shared_texture.cc#L284-L293),生成后不可修改:

字段 类型 文档说明 源码层面的实际含义
transfer string 共享纹理的不透明传输数据,可跨 Electron 进程传递 gpu::ClientSharedImage::Export() 得到的 gpu::ExportedSharedImage 经 mojo 序列化器编码后再 Base64 的字符串,承载了指向 GPU 进程中 SharedImageBackingMailbox 引用
syncToken string 帧创建用的不透明同步令牌数据 源对象 frame_creation_sync_token 的 Base64 编码(gpu::SyncToken 原始字节流,见 GetBase64StringFromSyncToken),接收端用于 WaitSyncToken,保证目标对象在 GPU 侧真正取得资源之前源对象不会释放它
pixelFormat string 正在传输的纹理的像素格式 TransferVideoPixelFormatToString 映射,见下文取值表
codedSize Size 共享纹理的完整尺寸 gfx::Size coded_size,即帧数据完整维度
visibleRect Rectangle [0, 0, codedSize.width(), codedSize.height()] 内的子区域,常见情况下就是全帧 gfx::Rect visible_rect;OSR 场景下默认取整个 codedSize(导入转换时若未提供即 gfx::Rect(coded_size),见 Converter::FromV8
timestamp number 微秒级时间戳,会被反映到 VideoFrame int64_t timestamp(自采集开始起算的微秒数),创建 VideoFrame 时以 base::Microseconds(timestamp) 传入 media::VideoFrame::WrapSharedImage

pixelFormat 字符串与内部枚举的双向映射定义在 TransferVideoPixelFormatToString 及导入时的 Converter<ImportSharedTextureInfo>::FromV8

字符串 内部 media::VideoPixelFormat
bgra PIXEL_FORMAT_ARGB
rgba PIXEL_FORMAT_ABGR
rgbaf16 PIXEL_FORMAT_RGBAF16
nv12 PIXEL_FORMAT_NV12
nv16 PIXEL_FORMAT_NV16
p010le PIXEL_FORMAT_P010LE

注意一个反直觉细节:ARGB 在传输对象上表现为 "bgra"ABGR 表现为 "rgba"。此外 getVideoFrame() 只能用于渲染进程——源码中浏览器进程调用会直接抛出 “The VideoFrame cannot be created at current process.”(ImportedTextureGetVideoFrame),所以“主进程生成 Transfer、渲染进程消费 Transfer”正是被设计支持的方向。

生成端:startTransferSharedTexture 的序列化实现

SharedTextureTransferSharedTextureImportedSubtle.startTransferSharedTexture() 创建,C++ 实现是 ImportedSharedTexture::StartTransferSharedTexture

v8::Local<v8::Value> ImportedSharedTexture::StartTransferSharedTexture(
    v8::Isolate* isolate) {
  auto exported = client_shared_image->Export();

  // Use mojo to serialize the exported shared image.
  mojo::Message message(0, 0, MOJO_CREATE_MESSAGE_FLAG_UNLIMITED_SIZE, 0);
  mojo::internal::MessageFragment<
      gpu::mojom::internal::ExportedSharedImage_Data>
      data(message);
  data.Allocate();
  mojo::internal::Serializer<gpu::mojom::ExportedSharedImageDataView,
                             gpu::ExportedSharedImage>::Serialize(exported,
                                                                  data);

  auto encoded = base::Base64Encode(...message payload...);
  gin_helper::Dictionary root(isolate, v8::Object::New(isolate));
  root.SetReadOnly("transfer", encoded);
  root.SetReadOnly("syncToken", GetBase64StringFromSyncToken(frame_creation_sync_token));
  root.SetReadOnly("pixelFormat", TransferVideoPixelFormatToString(pixel_format));
  root.SetReadOnly("codedSize", coded_size);
  root.SetReadOnly("visibleRect", visible_rect);
  root.SetReadOnly("timestamp", timestamp);

  return gin::ConvertToV8(isolate, root);
}

从实现可以看出三点关键事实:

  1. 传输的是 GPU 进程的引用,而不是像素数据。 ClientSharedImage::Export() 导出的是 ExportedSharedImage,其核心是 Mailbox——指向 GPU 进程中同一块 SharedImageBacking 的引用。这与 shared_texture 设计文档 的设计说明一致:正是 SharedImageMailbox 的持有使得跨进程共享成为可能(该设计文档注明基于 Electron 37 / Chromium 137 编写,且提示 Chromium 的 SharedImage 接口尤其是 GpuMemoryBufferVideoFrame 生命周期管理部分可能快速演进)。
  2. transfer 是 Base64 字符串,因此可被任意 IPC 机制透传。 mojo 消息体被编码为字符串后,就能走 webContents.sendipcRenderer.invoke 等结构化克隆通道,这正是“can be transferred across Electron processes”的由来。
  3. 调用前提: 对象未被 release(),且当前进程 GPU 可用,否则分别抛出 “The shared texture has been released.” 或 “Failed to start shared texture transfer: GPU is not available”(ImportedTextureStartTransferSharedTexture)。

消费端:finishTransferSharedTexture 的反序列化与同步保证

接收侧的 C++ 实现是 FinishTransferSharedTexture,与生成端严格对称:

v8::Local<v8::Value> FinishTransferSharedTexture(v8::Isolate* isolate,
                                                 v8::Local<v8::Value>& options) {
  // ... 从 options 中取出 id / transfer / syncToken 三个字符串 ...
  auto transfer_data = base::Base64Decode(transfer);

  // Use mojo to deserialize the exported shared image.
  mojo::Message message(transfer_data.value(), {});
  // ... Claim + Deserialize 出 gpu::ExportedSharedImage ...

  auto* sii = GetSharedImageInterface();  // 无 GPU 时抛错
  auto si = sii->ImportSharedImage(std::move(exported));

  auto source_st = GetSyncTokenFromBase64String(sync_token_data);
  sii->WaitSyncToken(source_st);   // 等待源对象的帧创建同步令牌

  ImportedSharedTexture* imported = new ImportedSharedTexture();
  imported->pixel_format = partial.pixel_format;
  imported->codedSize / visibleRect / timestamp 从传输对象恢复;
  imported->frame_creation_sync_token = sii->GenUnverifiedSyncToken();
  imported->client_shared_image = std::move(si);
  imported->id = id;
  return CreateImportedSharedTextureFromSharedImage(isolate, imported);
}

其中 WaitSyncToken(source_st) 是跨进程生命周期的关键一环:目标对象在 GPU 侧真正“取得”纹理引用之前,源对象侧通过 gpu::SharedImageInterface 的同步令牌机制不会销毁底层资源。源码注释(electron_api_shared_texture.h 对应实现区)明确了两种生命周期管理方式:

  • 回调方式(默认):对目标对象调用 release(callback),GPU 命令缓冲用完后回调触发,再由用户去释放源对象——简单直观;
  • 同步令牌方式(进阶):目标对象拿到 getFrameCreationSyncToken() 后,传给调用过 startTransferSharedTexture 的源对象并执行 setReleaseSyncToken,此后源对象可直接 release(),无需关心目标进程是否完成异步获取。

finishTransferSharedTexture 接收的参数就是整个 SharedTextureTransfer 对象(其字段名 transfersyncToken 被逐一读取)。绑定入口在 InitializeimportSharedTexturefinishTransferSharedTexture 都注册在 Node 绑定 electron_common_shared_texturenode_bindings.cc#L113)下,对应 JS 侧的 sharedTexture.subtle

实战一:subtle 手动模式(主进程导入 → IPC 传 Transfer → 渲染进程消费)

以下流程改编自仓库内测试 spec/api-shared-texture-spec.ts 中 “successfully imported and rendered with subtle api” 用例(该用例从 OSR 窗口捕获 paint 事件,最终用 capturePage 与目标图像做像素级比对验证整条链路):

主进程:

const { BrowserWindow, sharedTexture, ipcMain } = require('electron');

const capturedTextures = new Map(); // id -> { importedSubtle, texture }

const win = new BrowserWindow({
  width: 256, height: 256,
  webPreferences: { preload: preloadPath }
});

// OSR 窗口:useSharedTexture 让 paint 事件直接携带纹理句柄
const osr = new BrowserWindow({
  width: 128, height: 128,
  webPreferences: { offscreen: { useSharedTexture: true } }
});
osr.webContents.setFrameRate(1);

osr.webContents.on('paint', (event) => {
  const texture = event.texture;
  if (!texture) return; // GPU 可能不可用

  // Step 2: 把 OSR 纹理句柄导入为 SharedTextureImportedSubtle
  const importedSubtle = sharedTexture.subtle.importSharedTexture(texture.textureInfo);

  // Step 3: 生成可跨进程传递的 SharedTextureTransfer
  const transfer = importedSubtle.startTransferSharedTexture();

  const id = randomUUID();
  capturedTextures.set(id, { importedSubtle, texture });

  // Step 4: 通过 IPC 把 transfer 对象(纯数据)发给渲染进程
  win.webContents.send('shared-texture', id, transfer);
});

ipcMain.on('shared-texture-done', (event, id) => {
  // Step 12-14: 渲染进程释放完成后,主进程再释放导入对象与源纹理
  const { importedSubtle, texture } = capturedTextures.get(id);
  capturedTextures.delete(id);
  importedSubtle.release(() => texture.release());
});

渲染进程 preload(参考 spec/fixtures/api/shared-texture/subtle/preload.js):

const { sharedTexture } = require('electron');
const { ipcRenderer, contextBridge } = require('electron/renderer');

contextBridge.exposeInMainWorld('textures', {
  onSharedTexture: (cb) => {
    ipcRenderer.on('shared-texture', async (e, id, transfer) => {
      // Step 5: 用 SharedTextureTransfer 完成传输,取回 SharedTextureImportedSubtle
      const importedSubtle = sharedTexture.subtle.finishTransferSharedTexture(transfer);

      // Step 6: 交给 WebGPU 渲染(getVideoFrame() 只能在渲染进程调用)
      await cb(id, importedSubtle);

      // Step 10: 用 release 的回调精确等待 GPU 命令缓冲结束
      importedSubtle.release(() => {
        ipcRenderer.send('shared-texture-done', id);
      });
    });
  }
});

几个值得注意的约束(均有仓库依据):

  • 纹理句柄必须对调用进程可见。设计文档明确:Windows 上必须是 CreateSharedHandle 产生的 NT HANDLE(非 NT 的旧式 HANDLE 会被 Chromium 的 CloseHandle 清理逻辑破坏导致崩溃),macOS 上 IOSurface 需通过 mach_port 跨进程传递;因此调用 importSharedTexture 前,句柄须已可用(ImportSharedTextureInfo 的平台注释)。
  • release() 是手动义务:若对象被 GC 而未调用 release(),弱引用回调会打出 “The imported shared texture ... was garbage collected before calling release(). You have to manually release the resource once you're done with it.” 的 ERROR 日志(PersistentCallbackPass1)。
  • 如果一次传输产生了多个 SharedTextureImported 对象(比如同一纹理转发给多个进程),每一个都必须 release,GPU 侧资源在最后一个被释放时才销毁(SharedTextureImportedSubtle 文档)。

实战二:managed 托管模式(sendSharedTexture + setSharedTextureReceiver)

sharedTexture 模块还封装了托管流程,免去手动 IPC:

  • 渲染进程先注册 sharedTexture.setSharedTextureReceiver(callback)
  • 主进程调用 sharedTexture.sendSharedTexture({ frame: webContents.mainFrame, importedSharedTexture }, ...args),该方法有 1000ms 超时,要求接收端已就绪、渲染进程存活(sharedTexture 模块文档)。

渲染进程侧的实现见 lib/renderer/api/shared-texture.ts

ipcRendererInternal.on(transferChannelName, async (event, requestId, ...args) => {
  const replyChannel = `${transferChannelName}_RESPONSE_${requestId}`;
  const transfer = args[0] as Electron.SharedTextureTransfer;
  const textureId = args[1] as string;
  // 1) 在渲染进程完成 finishTransferSharedTexture
  const imported = sharedTextureNative.finishTransferSharedTexture(Object.assign(transfer, { id: textureId }));
  // 2) 把 getFrameCreationSyncToken() 回传给主进程,
  //    供源对象 setReleaseSyncToken 使用(对应上文“同步令牌方式”)
  event.sender.send(replyChannel, null, imported.getFrameCreationSyncToken());
  // 3) 组装 SharedTextureImported 包装对象并回调用户
  await sharedTextureReceiverCallback?.(data, ...args.slice(2));
});

对应测试 spec/fixtures/api/shared-texture/managed/preload.js 中,release() 无需回调——注释说明“util function automatically managed the lifetime via sync token”,即托管模式已经替你走完了 setReleaseSyncToken 路径。渲染端用法(managed/renderer.js)非常简洁:

window.textures.onSharedTexture(async (imported) => {
  const frame = imported.getVideoFrame(); // Step 6: 取 VideoFrame
  await window.renderFrame(frame);        // Step 7: WebGPU 渲染
  frame.close();                          // Step 8: 用完即关
  imported.release();
});

设计原理小结:为什么 Transfer 是字符串而非句柄直传

shared_texture 设计文档 解释了这条链路背后的四个工程障碍,其中两个直接决定了 SharedTextureTransfer 的形态:

  1. 共享句柄是进程局部的:Windows NT HANDLE 需要 DuplicateHandle 才能跨进程,macOS 的 IOSurface 需要经 mach_port 传递。SharedTextureTransfer 绕开了原生句柄传递,因为 transfer 字段里装的是 GPU 进程侧 SharedImageBacking 的引用序列化数据——任何拿到它的进程都通过 SharedImageInterface::ImportSharedImage 在 GPU 进程中“认领”同一份 backing,无需操作平台句柄。
  2. GPU 使用何时结束无法同步得知:两个进程各持有一份引用同一 MailboxSharedImage,谁都不能先释放。这就是 syncToken 字段与 release 回调 / setReleaseSyncToken 机制存在的意义——底层通过 gpu::ContextSupport::SignalSyncToken 在 GPU 命令缓冲真正完成后触发 JS 回调(SetupReleaseSyncTokenCallback)。

适用前提与限制

  • 实验性 APIsharedTexture 模块全部方法在文档中标记 Experimental,未来可能被移除(shared-texture.md)。
  • GPU 必须可用startTransferSharedTexture / finishTransferSharedTexture / getFrameCreationSyncToken / setReleaseSyncToken 在无 GPU 上下文时都会抛出 “GPU is not available” 错误;测试用例也因此在捕获不到纹理时跳过比对。
  • 测试平台基线:当前仓库的端到端测试 api-shared-texture-spec.ts 仅在 macOS arm64 上运行(process.platform !== 'darwin' || process.arch !== 'arm64' 时整体 ifdescribe 跳过);设计文档亦声明其基于 Electron 37 / Chromium 137 编写,且 Chromium SharedImage 相关接口演进较快,其他平台的细节以实际仓库源码为准。
  • 进程分工importSharedTexturestartTransferSharedTexture 可在主进程调用;getVideoFrame() 只能在渲染进程调用。若要导入渲染进程侧加载的原生代码产出的纹理,需自行保证句柄对该进程可见(设计文档 “Example” 一节)。

相关仓库资料索引

登录后查看全文
热门项目推荐
相关项目推荐