Signatur
Beschreibung
Das Interface GPURenderBundle der WebGPU API steht für ein Bündel von Zeichenbefehlen, das bereits aufgezeichnet wurde. Ein Bundle entsteht nicht über einen Konstruktor. Man zeichnet die Befehle mit einem GPURenderBundleEncoder auf, den GPUDevice.createRenderBundleEncoder() liefert, und ruft dann GPURenderBundleEncoder.finish() auf. Das Ergebnis führt man in einem Render-Pass mit GPURenderPassEncoder.executeBundles() aus.
Render-Bundles lohnen sich vor allem bei statischen oder selten veränderten Szenen, die Bild für Bild mit denselben Befehlen gezeichnet werden, etwa viele Objekte mit festen Pipelines, Bind Groups und Vertex-Buffern. Die Validierung der aufgezeichneten Befehle findet nur einmal beim Aufzeichnen statt. Deshalb sinkt der CPU-Aufwand pro Frame, besonders wenn Hunderte oder Tausende Zeichenaufrufe anfallen.
Ein Bundle kann setPipeline(), setBindGroup(), setVertexBuffer(), setIndexBuffer(), draw(), drawIndexed() und die indirekten Varianten enthalten. Viewport, Scissor-Rechteck, Blend-Konstante und Stencil-Referenz lassen sich im Bundle nicht setzen. Diese Werte übernimmt das Bundle aus dem umgebenden Render-Pass. Die Inhalte der verwendeten Buffer dürfen sich aber ändern. So kann man z. B. Kameramatrizen über queue.writeBuffer() aktualisieren, ohne das Bundle neu aufzuzeichnen.
Das Interface selbst ist schlank. Es hat nur die Eigenschaft label, einen frei wählbaren Namen für Debugging und Fehlermeldungen. Konstruktor und Methoden hat es keine.
Parameter
| Name | Typ | Default | Beschreibung |
|---|---|---|---|
| $label | string | "" | Instanz-Eigenschaft (lesen und schreiben): Name des Bundles für Debug-Ausgaben und Validierungsfehler. Den Anfangswert übernimmt das Bundle aus dem label des Deskriptors von finish(). |
Rückgabewert
GPURenderBundleEncoder.finish(descriptor?) zurückgegeben und an GPURenderPassEncoder.executeBundles(bundles) übergeben.Beispiele
Bundle aufzeichnen und in jedem Frame ausführen
const adapter = await navigator.gpu.requestAdapter();
const device = await adapter.requestDevice();
const canvas = document.querySelector('canvas');
const context = canvas.getContext('webgpu');
const format = navigator.gpu.getPreferredCanvasFormat();
context.configure({ device, format });
const module = device.createShaderModule({
code: `
@vertex fn vs(@builtin(vertex_index) i: u32) -> @builtin(position) vec4f {
var p = array(vec2f(0.0, 0.5), vec2f(-0.5, -0.5), vec2f(0.5, -0.5));
return vec4f(p[i], 0.0, 1.0);
}
@fragment fn fs() -> @location(0) vec4f {
return vec4f(1.0, 0.4, 0.0, 1.0);
}`
});
const pipeline = device.createRenderPipeline({
layout: 'auto',
vertex: { module, entryPoint: 'vs' },
fragment: { module, entryPoint: 'fs', targets: [{ format }] }
});
// Einmalig aufzeichnen – colorFormats muss zum Render-Pass passen
const bundleEncoder = device.createRenderBundleEncoder({ colorFormats: [format] });
bundleEncoder.setPipeline(pipeline);
bundleEncoder.draw(3);
const bundle = bundleEncoder.finish({ label: 'Dreieck-Bundle' });
const frame = () => {
const encoder = device.createCommandEncoder();
const pass = encoder.beginRenderPass({
colorAttachments: [{
view: context.getCurrentTexture().createView(),
loadOp: 'clear',
clearValue: { r: 0, g: 0, b: 0, a: 1 },
storeOp: 'store'
}]
});
pass.executeBundles([bundle]); // wiederverwenden statt neu aufzeichnen
pass.end();
device.queue.submit([encoder.finish()]);
requestAnimationFrame(frame);
};
requestAnimationFrame(frame);
console.log(bundle.label);
Viele Objekte bündeln, Uniform-Daten trotzdem pro Frame ändern
// Annahme: device, pipeline, format, depthFormat sowie objects
// (Array mit { bindGroup, vertexBuffer, vertexCount }) existieren bereits.
const createSceneBundle = (objects) => {
const enc = device.createRenderBundleEncoder({
label: 'Szene',
colorFormats: [format],
depthStencilFormat: depthFormat
});
enc.setPipeline(pipeline);
for (const obj of objects) {
enc.setBindGroup(0, obj.bindGroup);
enc.setVertexBuffer(0, obj.vertexBuffer);
enc.draw(obj.vertexCount);
}
return enc.finish();
};
const sceneBundle = createSceneBundle(objects);
const render = (cameraMatrix, pass) => {
// Buffer-Inhalt aktualisieren – das Bundle bleibt gültig
device.queue.writeBuffer(cameraUniformBuffer, 0, cameraMatrix);
pass.executeBundles([sceneBundle]);
};
Label nachträglich setzen und Validierungsfehler abfangen
device.pushErrorScope('validation');
const enc = device.createRenderBundleEncoder({ colorFormats: ['rgba8unorm'] });
enc.setPipeline(pipeline); // Pipeline mit abweichendem Zielformat?
enc.draw(3);
const bundle = enc.finish();
bundle.label = 'HUD-Overlay';
const error = await device.popErrorScope();
if (error) {
console.warn(`Bundle "${bundle.label}" ungültig:`, error.message);
}
// Wichtig · Fallstricke
- Kompatible Formate:
colorFormats,depthStencilFormatundsampleCountdes Bundle-Encoders müssen exakt zu den Attachments des Render-Passes passen, sonst schlägtexecuteBundles()mit einem Validierungsfehler fehl. MitdepthReadOnly/stencilReadOnlymuss der Pass ebenfalls vereinbar sein. - Zustand wird zurückgesetzt: Nach
executeBundles()sind Pipeline, Bind Groups sowie Vertex- und Index-Buffer des Passes zurückgesetzt. Wer danach im Pass direkt weiterzeichnet, muss sie neu setzen. Umgekehrt erbt ein Bundle keinen Zustand aus dem Pass, mit Ausnahme von Viewport, Scissor, Blend-Konstante und Stencil-Referenz. - Unveränderlich: Ändern sich die Befehle selbst, z. B. Objekte kommen hinzu oder fallen weg, muss man das Bundle neu aufzeichnen. Nur Buffer- und Texturinhalte dürfen sich ändern. Zerstört man eine verwendete Ressource mit
destroy(), wird die Übermittlung ungültig. - Performance: Bei wenigen Zeichenaufrufen bringt ein Bundle kaum etwas. Messbar wird der Vorteil erst bei vielen Draw-Calls pro Frame.
- Kompatibilität: WebGPU steht nur in sicheren Kontexten zur Verfügung. Vor der Nutzung sollte man mit
'gpu' in navigatorprüfen, ob die API vorhanden ist. Auch in Web Workern ist sie nutzbar.