Spec-Zone.ru › Deno 2

Рендеринг WebGPU

Доступно в Deno 2.9

deno desktop доступен начиная с Deno v2.9.0. Если у вас более ранняя версия, обновите Deno, чтобы воспользоваться этой возможностью.

Низкоуровневый бэкенд предоставляет собственное окно без подключённого веб-движка. Вместо загрузки HTML вы сами рисуете в окне с помощью WebGPU. Этот бэкенд подходит для игр, визуализаций, эмуляторов и любых приложений, которые отрисовывают собственные пиксели, а не документ.

Связующим звеном между окном и WebGPU служит Deno.BrowserWindow.getNativeWindow(), возвращающий Deno.UnsafeWindowSurface. Эта поверхность предоставляет контекст холста WebGPU, поэтому тот же процесс context.configure() / getCurrentTexture() / present(), который используется в браузере, работает и с настоящим окном операционной системы.

Настройка

WebGPU доступен только при включённом нестабильном флаге, а низкоуровневый бэкенд выбирается в deno.json:

deno.json
{
  "desktop": {
    "backend": "raw"
  },
  "unstable": ["webgpu"]
}

В отличие от cef и webview, raw нельзя передать с помощью --backend в командной строке — его можно выбрать только через поле desktop.backend. См. раздел «Бэкенды».

Минимальный пример

Самая простая полезная программа: открыть окно и залить его сплошным цветом. Это позволяет убедиться, что вся цепочка — адаптер, поверхность, контекст, вывод кадра — настроена правильно, прежде чем добавлять отрисовку.

main.ts
// A WebGPU context must exist before the native surface can be wrapped, so
// acquire the adapter and device first.
const adapter = await navigator.gpu.requestAdapter();
if (!adapter) throw new Error("no WebGPU adapter available");
const device = await adapter.requestDevice();

const win = new Deno.BrowserWindow({
  title: "WebGPU",
  width: 640,
  height: 480,
});

// Wrap the native window as a surface and configure a WebGPU context on it.
const surface = win.getNativeWindow();
const format = navigator.gpu.getPreferredCanvasFormat();
const context = surface.getContext("webgpu");
context.configure({ device, format, alphaMode: "opaque" });

// Match the surface to the window before the first frame.
const [width, height] = win.getSize();
surface.width = width;
surface.height = height;

// Clear the frame to teal and present it.
const encoder = device.createCommandEncoder();
encoder.beginRenderPass({
  colorAttachments: [{
    view: context.getCurrentTexture().createView(),
    clearValue: { r: 0, g: 0.5, b: 0.5, a: 1 },
    loadOp: "clear",
    storeOp: "store",
  }],
}).end();
device.queue.submit([encoder.finish()]);
surface.present();

Соберите и запустите программу:

›_
deno desktop main.ts
./main      # macOS / Linux
.\main.exe  # Windows

Именно surface.present() фактически выводит закодированный кадр на экран; без него окно останется пустым. Однократный вызов, как в примере выше, оставляет статичный кадр на экране, пока окно не закроется.

Отрисовка геометрии

Заливка цветом позволяет проверить поверхность, но ничего не рисует. Простой пример графики на GPU — конвейер рендеринга с шейдером WGSL. В этом примере рисуется один треугольник, цвета вершин которого интерполируются по его поверхности; буферы вершин не нужны, поскольку координаты заданы непосредственно в шейдере.

triangle.ts
const adapter = await navigator.gpu.requestAdapter();
if (!adapter) throw new Error("no WebGPU adapter available");
const device = await adapter.requestDevice();

const win = new Deno.BrowserWindow({
  title: "Triangle",
  width: 640,
  height: 480,
});

const surface = win.getNativeWindow();
const format = navigator.gpu.getPreferredCanvasFormat();
const context = surface.getContext("webgpu");
context.configure({ device, format, alphaMode: "opaque" });

const [width, height] = win.getSize();
surface.width = width;
surface.height = height;

// The vertex stage emits three corners; the fragment stage receives the
// color interpolated between them.
const shader = device.createShaderModule({
  code: `
    struct VertexOut {
      @builtin(position) pos: vec4f,
      @location(0) color: vec3f,
    };

    @vertex
    fn vs(@builtin(vertex_index) i: u32) -> VertexOut {
      var positions = array<vec2f, 3>(
        vec2f( 0.0,  0.6),
        vec2f(-0.6, -0.6),
        vec2f( 0.6, -0.6),
      );
      var colors = array<vec3f, 3>(
        vec3f(1.0, 0.0, 0.0),
        vec3f(0.0, 1.0, 0.0),
        vec3f(0.0, 0.0, 1.0),
      );
      var out: VertexOut;
      out.pos = vec4f(positions[i], 0.0, 1.0);
      out.color = colors[i];
      return out;
    }

    @fragment
    fn fs(in: VertexOut) -> @location(0) vec4f {
      return vec4f(in.color, 1.0);
    }
  `,
});

const pipeline = device.createRenderPipeline({
  layout: "auto",
  vertex: { module: shader, entryPoint: "vs" },
  fragment: { module: shader, entryPoint: "fs", targets: [{ format }] },
  primitive: { topology: "triangle-list" },
});

const encoder = device.createCommandEncoder();
const pass = encoder.beginRenderPass({
  colorAttachments: [{
    view: context.getCurrentTexture().createView(),
    clearValue: { r: 0.05, g: 0.05, b: 0.08, a: 1 },
    loadOp: "clear",
    storeOp: "store",
  }],
});
pass.setPipeline(pipeline);
pass.draw(3);
pass.end();
device.queue.submit([encoder.finish()]);
surface.present();

Анимация с циклом рендеринга

Чтобы анимировать объекты, рисуйте кадры повторно. В низкоуровневом бэкенде нет DOM, поэтому нет и requestAnimationFrame — планируйте вывод кадров самостоятельно. В этом примере повторно используется конвейер рендеринга треугольника, а прошедшее время передаётся в шейдер через буфер униформ, чтобы вращать его.

spin.ts
const adapter = await navigator.gpu.requestAdapter();
if (!adapter) throw new Error("no WebGPU adapter available");
const device = await adapter.requestDevice();

const win = new Deno.BrowserWindow({ title: "Spin", width: 640, height: 480 });

const surface = win.getNativeWindow();
const format = navigator.gpu.getPreferredCanvasFormat();
const context = surface.getContext("webgpu");
context.configure({ device, format, alphaMode: "opaque" });

// Keep the surface sized to the window, and reconfigure whenever it resizes.
function resize() {
  const [width, height] = win.getSize();
  surface.width = width;
  surface.height = height;
}
resize();
win.addEventListener("resize", resize);

const shader = device.createShaderModule({
  code: `
    @group(0) @binding(0) var<uniform> angle: f32;

    struct VertexOut {
      @builtin(position) pos: vec4f,
      @location(0) color: vec3f,
    };

    @vertex
    fn vs(@builtin(vertex_index) i: u32) -> VertexOut {
      var base = array<vec2f, 3>(
        vec2f( 0.0,  0.6),
        vec2f(-0.6, -0.6),
        vec2f( 0.6, -0.6),
      );
      var colors = array<vec3f, 3>(
        vec3f(1.0, 0.0, 0.0),
        vec3f(0.0, 1.0, 0.0),
        vec3f(0.0, 0.0, 1.0),
      );
      let s = sin(angle);
      let c = cos(angle);
      let p = base[i];
      var out: VertexOut;
      out.pos = vec4f(p.x * c - p.y * s, p.x * s + p.y * c, 0.0, 1.0);
      out.color = colors[i];
      return out;
    }

    @fragment
    fn fs(in: VertexOut) -> @location(0) vec4f {
      return vec4f(in.color, 1.0);
    }
  `,
});

const uniform = device.createBuffer({
  size: 4, // one f32
  usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
});

const pipeline = device.createRenderPipeline({
  layout: "auto",
  vertex: { module: shader, entryPoint: "vs" },
  fragment: { module: shader, entryPoint: "fs", targets: [{ format }] },
  primitive: { topology: "triangle-list" },
});

const bindGroup = device.createBindGroup({
  layout: pipeline.getBindGroupLayout(0),
  entries: [{ binding: 0, resource: { buffer: uniform } }],
});

const start = performance.now();

function frame() {
  if (win.isClosed()) return;

  const angle = (performance.now() - start) / 1000;
  device.queue.writeBuffer(uniform, 0, new Float32Array([angle]));

  const encoder = device.createCommandEncoder();
  const pass = encoder.beginRenderPass({
    colorAttachments: [{
      view: context.getCurrentTexture().createView(),
      clearValue: { r: 0.05, g: 0.05, b: 0.08, a: 1 },
      loadOp: "clear",
      storeOp: "store",
    }],
  });
  pass.setPipeline(pipeline);
  pass.setBindGroup(0, bindGroup);
  pass.draw(3);
  pass.end();
  device.queue.submit([encoder.finish()]);
  surface.present();

  setTimeout(frame, 16); // ~60 fps
}

win.addEventListener("close", () => Deno.exit(0));
frame();

Самопланирующийся вызов setTimeout выдаёт кадр примерно каждые 16 мс. Проверка win.isClosed() останавливает цикл, когда окно закрывается, а обработчик close завершает процесс; в противном случае ожидающий таймер не позволил бы среде выполнения завершиться, хотя на экране уже ничего нет.

Основные сведения

  • Запросите адаптер до привязки окна. Для getNativeWindow() требуется активный контекст WebGPU; вызов до navigator.gpu.requestAdapter() приведёт к ошибке.

  • Задайте размер поверхности и обновляйте его при изменении размера окна. Установите surface.width / surface.height до первого кадра и обновляйте их (позволяя контексту перенастроиться) при каждом событии resize окна. Если размер поверхности не совпадает с размером окна, изображение будет растянуто или обрезано.

  • Вызывайте present() при выводе каждого кадра. Кодирование и отправка прохода рендеринга выполняют отрисовку в текстуру цепочки обмена; именно present() выводит её на экран. Без этого вызова окно останется пустым.

  • Получайте новую текстуру для каждого кадра. Вызывайте context.getCurrentTexture().createView() внутри цикла — цепочка обмена предоставляет отдельную текстуру для каждого кадра.

  • Закрытие окна заменяется его скрытием. После получения поверхности из окна вызов close() скрывает окно, а не уничтожает его, чтобы нативные дескрипторы, в которые выполняется рендеринг WebGPU, не были освобождены. Чтобы завершить процесс, вызовите Deno.exit(), как это делает цикл рендеринга выше при событии close.

См. также

  • Бэкенды — когда выбрать raw вместо cef / webview.
  • Окна — жизненный цикл, изменение размера и события Deno.BrowserWindow.
  • API WebGPU и спецификация WGSL.

Последнее обновление: 8 июля 2026 г.

© 2018–2025 the Deno authors
Licensed under the MIT License.
https://docs.deno.com/runtime/desktop/webgpu

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API