Signatur
Beschreibung
Die Methode createRenderBundleEncoder() der Schnittstelle GPUDevice gibt einen GPURenderBundleEncoder zurück. Damit zeichnet man Zeichenbefehle wie setPipeline(), setBindGroup(), setVertexBuffer() und draw() auf. Anschließend fasst finish() sie zu einem unveränderlichen GPURenderBundle zusammen. Dieses Bundle kann man mit GPURenderPassEncoder.executeBundles() in jedem passenden Render-Pass erneut ausführen.
Der Hauptnutzen ist weniger Arbeit für die CPU. Zeichnet man statische Szenenteile wie Gelände, Gebäude oder UI-Elemente, die sich zwischen den Frames nicht ändern, einmal auf, muss JavaScript die vielen einzelnen Befehle nicht in jedem Frame neu an die GPU schicken und prüfen lassen. Das bringt besonders bei Szenen mit vielen Draw-Calls einen deutlichen Leistungsgewinn.
Das Bundle ist an ein bestimmtes Attachment-Layout gebunden: Farbformate, Tiefen-/Stencil-Format und Sample-Anzahl. Diese Werte müssen genau zum Render-Pass passen, in dem das Bundle ausgeführt wird. Sonst entsteht ein Validierungsfehler. Buffer- und Texturinhalte dürfen sich dagegen ändern: Das Bundle speichert nur Verweise auf die Ressourcen, keine Kopien der Daten. Dynamische Werte wie Kameramatrizen lassen sich also weiter über queue.writeBuffer() aktualisieren.
Parameter
| Name | Typ | Default | Beschreibung |
|---|---|---|---|
| $descriptor Pflicht | GPURenderBundleEncoderDescriptor | Konfigurationsobjekt mit folgenden Feldern (siehe unten). | |
| $descriptor.colorFormats Pflicht | Array<GPUTextureFormat | null> | Formate der Farb-Attachments, z. B. [navigator.gpu.getPreferredCanvasFormat()]. Müssen mit den colorAttachments des Render-Passes übereinstimmen. |
|
| $descriptor.depthStencilFormat | GPUTextureFormat | undefined | Format des Tiefen-/Stencil-Attachments, z. B. "depth24plus". Weglassen, wenn der Pass keinen Tiefenpuffer hat. |
| $descriptor.sampleCount | number | 1 | Anzahl der Samples pro Pixel (Multisampling), z. B. 4 für MSAA. |
| $descriptor.depthReadOnly | boolean | false | Wenn true, schreibt das Bundle nicht in den Tiefenanteil. So ist es mit schreibgeschützten Tiefen-Attachments kompatibel. |
| $descriptor.stencilReadOnly | boolean | false | Wenn true, schreibt das Bundle nicht in den Stencil-Anteil. |
| $descriptor.label | string | "" | Name zur Identifikation in Fehlermeldungen und Debugging-Tools. |
Rückgabewert
finish() erhält man daraus ein GPURenderBundle.Beispiele
Bundle erstellen 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, 3>(vec2f(0, 0.5), vec2f(-0.5, -0.5), vec2f(0.5, -0.5));
return vec4f(p[i], 0, 1);
}
@fragment fn fs() -> @location(0) vec4f { return vec4f(1, 0.4, 0, 1); }
`
});
const pipeline = device.createRenderPipeline({
layout: 'auto',
vertex: { module, entryPoint: 'vs' },
fragment: { module, entryPoint: 'fs', targets: [{ format }] }
});
// Befehle einmalig aufzeichnen
const bundleEncoder = device.createRenderBundleEncoder({
label: 'Dreieck-Bundle',
colorFormats: [format]
});
bundleEncoder.setPipeline(pipeline);
bundleEncoder.draw(3);
const bundle = bundleEncoder.finish();
// In jedem Frame nur noch das Bundle abspielen
const frame = () => {
const encoder = device.createCommandEncoder();
const pass = encoder.beginRenderPass({
colorAttachments: [{
view: context.getCurrentTexture().createView(),
clearValue: { r: 0, g: 0, b: 0, a: 1 },
loadOp: 'clear',
storeOp: 'store'
}]
});
pass.executeBundles([bundle]);
pass.end();
device.queue.submit([encoder.finish()]);
requestAnimationFrame(frame);
};
requestAnimationFrame(frame);
Bundle mit Tiefenpuffer und MSAA
// Das Layout muss exakt zum späteren Render-Pass passen
const bundleEncoder = device.createRenderBundleEncoder({
label: 'Statische Szene',
colorFormats: [format],
depthStencilFormat: 'depth24plus',
sampleCount: 4
});
for (const mesh of staticMeshes) {
bundleEncoder.setPipeline(mesh.pipeline);
bundleEncoder.setBindGroup(0, mesh.bindGroup);
bundleEncoder.setVertexBuffer(0, mesh.vertexBuffer);
bundleEncoder.setIndexBuffer(mesh.indexBuffer, 'uint16');
bundleEncoder.drawIndexed(mesh.indexCount);
}
const sceneBundle = bundleEncoder.finish({ label: 'Szene' });
// Später: Render-Pass mit MSAA-Textur (sampleCount 4) und depth24plus
// pass.executeBundles([sceneBundle]);
console.log(sceneBundle.label);
Statische und dynamische Befehle kombinieren
const pass = encoder.beginRenderPass(renderPassDescriptor);
// Vorab aufgezeichnete, statische Objekte
pass.executeBundles([terrainBundle, buildingsBundle]);
// Danach dynamische Objekte direkt zeichnen.
// Achtung: executeBundles() setzt den Pass-Zustand zurück,
// Pipeline und Bind-Groups müssen neu gesetzt werden.
pass.setPipeline(playerPipeline);
pass.setBindGroup(0, playerBindGroup);
pass.setVertexBuffer(0, playerVertexBuffer);
pass.draw(playerVertexCount);
pass.end();