Рендеринг WebGPU
deno desktop доступен начиная с Deno v2.9.0. Если у вас более ранняя версия, обновите Deno, чтобы воспользоваться этой возможностью.
Низкоуровневый бэкенд предоставляет собственное окно без подключённого веб-движка. Вместо загрузки HTML вы сами рисуете в окне с помощью WebGPU. Этот бэкенд подходит для игр, визуализаций, эмуляторов и любых приложений, которые отрисовывают собственные пиксели, а не документ.
Связующим звеном между окном и WebGPU служит Deno.BrowserWindow.getNativeWindow(), возвращающий Deno.UnsafeWindowSurface. Эта поверхность предоставляет контекст холста WebGPU, поэтому тот же процесс context.configure() / getCurrentTexture() / present(), который используется в браузере, работает и с настоящим окном операционной системы.
Настройка
WebGPU доступен только при включённом нестабильном флаге, а низкоуровневый бэкенд выбирается в deno.json:
{
"desktop": {
"backend": "raw"
},
"unstable": ["webgpu"]
}
В отличие от cef и webview, raw нельзя передать с помощью --backend в командной строке — его можно выбрать только через поле desktop.backend. См. раздел «Бэкенды».
Минимальный пример
Самая простая полезная программа: открыть окно и залить его сплошным цветом. Это позволяет убедиться, что вся цепочка — адаптер, поверхность, контекст, вывод кадра — настроена правильно, прежде чем добавлять отрисовку.
// 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. В этом примере рисуется один треугольник, цвета вершин которого интерполируются по его поверхности; буферы вершин не нужны, поскольку координаты заданы непосредственно в шейдере.
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 — планируйте вывод кадров самостоятельно. В этом примере повторно используется конвейер рендеринга треугольника, а прошедшее время передаётся в шейдер через буфер униформ, чтобы вращать его.
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.
Последнее обновление:
© 2018–2025 the Deno authors
Licensed under the MIT License.
https://docs.deno.com/runtime/desktop/webgpu