perf(ui): add additive DispatchFused to collapse consecutive UI passes (#47)
Add a fifth UI compute path — DispatchFused — alongside the four per-element Dispatch* calls. It composites quads → circles → images → text (canonical back-to-front order) in ONE dispatch that loads the destination image once and stores once, eliminating the per-pass load+store and the inter-pass VkMemoryBarriers a run of consecutive Dispatch* calls would pay. The win materialises at >= 2 non-empty categories; an absent category is a zero-trip, push-constant-uniform loop (~free). The new uber-kernel (shaders/ui-fused.comp.glsl) reuses each category's exact cooperative shared-memory tile-cull + per-pixel accumulate, so a fused category is pixel-identical to its standalone pass. Categories run sequentially per thread and share one set of shared-memory scratch, so the VGPR/shared high-water mark stays at the per-category level, not the sum. Additive and non-breaking: the per-element Dispatch* calls and the frozen 48-byte UIDispatchHeader are untouched. DispatchFused gets its own 128-byte push-constant layout (UIFusedHeader: four uvec4 headers + four per-category clip vec4s). To interleave a custom ui.Dispatch() between standard passes, bracket it with two DispatchFused calls — the dispatch boundary is the explicit, app-declared flush point. WebGPU (Vulkan-second): DispatchFused falls back to the per-element Dispatch* calls in canonical order — one texture/sampler per dispatch there can't feed the bindless image phase — so the result matches; only the load/store fusion is Vulkan-only. HelloUI now composites its background quads, mouse circle, and label text in a single DispatchFused. Verified rendering identical on a live Vulkan/Wayland session. Resolves #47 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
c58b68ec40
commit
574242dd30
8 changed files with 720 additions and 8 deletions
|
|
@ -26,7 +26,8 @@ void UIRenderer::Initialize(Window& window, GraphicsDescriptorHeap& heap, Graphi
|
|||
std::filesystem::path /*quadsSpv*/,
|
||||
std::filesystem::path /*circlesSpv*/,
|
||||
std::filesystem::path /*imagesSpv*/,
|
||||
std::filesystem::path /*textSpv*/) {
|
||||
std::filesystem::path /*textSpv*/,
|
||||
std::filesystem::path /*fusedSpv*/) {
|
||||
(void)initCmd;
|
||||
window_ = &window;
|
||||
heap_ = &heap;
|
||||
|
|
@ -144,6 +145,25 @@ void UIRenderer::DispatchText(GraphicsCommandBuffer /*cmd*/, std::uint32_t buffe
|
|||
texHandle, sampHandle);
|
||||
}
|
||||
|
||||
void UIRenderer::DispatchFused(GraphicsCommandBuffer cmd,
|
||||
const FusedBatch& quads,
|
||||
const FusedBatch& circles,
|
||||
const FusedBatch& images,
|
||||
const FusedBatch& text) {
|
||||
// WebGPU is Vulkan-second here (issue #47): one bind group can carry only
|
||||
// one texture/sampler per dispatch, so the single-load/store uber-kernel
|
||||
// can't bind per-item image textures the way Vulkan bindless does. Fall
|
||||
// back to the per-element Dispatch* calls in the same canonical order
|
||||
// (quads → circles → images → text), which produces the identical result —
|
||||
// it just pays the per-pass load/store + barriers the Vulkan path fuses
|
||||
// away. The common quads+circles+text screen still works perfectly; only
|
||||
// the image category leans on DispatchImages' font-atlas fallback.
|
||||
DispatchQuads(cmd, quads.bufferSlot, quads.itemCount, quads.clipRectPx);
|
||||
DispatchCircles(cmd, circles.bufferSlot, circles.itemCount, circles.clipRectPx);
|
||||
DispatchImages(cmd, images.bufferSlot, images.itemCount, images.clipRectPx);
|
||||
DispatchText(cmd, text.bufferSlot, text.itemCount, text.clipRectPx);
|
||||
}
|
||||
|
||||
SamplerSlot UIRenderer::RegisterLinearClampSampler() {
|
||||
auto range = heap_->AllocateSamplerSlots(1);
|
||||
heap_->samplerTable[range.firstElement] = WebGPU::wgpuCreateLinearClampSampler();
|
||||
|
|
|
|||
|
|
@ -39,15 +39,17 @@ void UIRenderer::Initialize(Window& window, GraphicsDescriptorHeap& heap, Graphi
|
|||
std::filesystem::path quadsSpv,
|
||||
std::filesystem::path circlesSpv,
|
||||
std::filesystem::path imagesSpv,
|
||||
std::filesystem::path textSpv) {
|
||||
std::filesystem::path textSpv,
|
||||
std::filesystem::path fusedSpv) {
|
||||
window_ = &window;
|
||||
heap_ = &heap;
|
||||
|
||||
// Load the four standard pipelines.
|
||||
// Load the four standard pipelines plus the fused uber-kernel (issue #47).
|
||||
drawQuads.Load(quadsSpv);
|
||||
drawCircles.Load(circlesSpv);
|
||||
drawImages.Load(imagesSpv);
|
||||
drawText.Load(textSpv);
|
||||
drawFused.Load(fusedSpv);
|
||||
|
||||
// Allocate one image slot for the swapchain output. Each per-frame heap
|
||||
// copy will hold ITS frame's image at this slot.
|
||||
|
|
@ -176,6 +178,61 @@ void UIRenderer::DispatchText(GraphicsCommandBuffer cmd, std::uint32_t bufferSlo
|
|||
TilesFor(window_->width), TilesFor(window_->height), 1u);
|
||||
}
|
||||
|
||||
// ─── fused dispatch (issue #47) ─────────────────────────────────────────
|
||||
//
|
||||
// One uber-kernel composites quads → circles → images → text into the output
|
||||
// image with a single load and a single store. Routed through the generic
|
||||
// Dispatch() so it still gets the standard write-after-write barrier when it
|
||||
// is not the first dispatch of the frame, and a following dispatch barriers
|
||||
// against it — the only barriers it removes are the INTRA-fuse ones a run of
|
||||
// Dispatch* calls would have inserted between its categories.
|
||||
|
||||
void UIRenderer::DispatchFused(GraphicsCommandBuffer cmd,
|
||||
const FusedBatch& quads,
|
||||
const FusedBatch& circles,
|
||||
const FusedBatch& images,
|
||||
const FusedBatch& text) {
|
||||
// Nothing to draw → no dispatch (and, crucially, no barrier bump).
|
||||
if (quads.itemCount == 0 && circles.itemCount == 0 &&
|
||||
images.itemCount == 0 && text.itemCount == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (text.itemCount != 0 && !fontAtlasImageSlot_) {
|
||||
throw std::runtime_error("UIRenderer::DispatchFused: text batch given but no FontAtlas registered (set fontAtlas before Initialize)");
|
||||
}
|
||||
// Flush any glyphs ShapeText rasterised during this onBuild, mirroring
|
||||
// DispatchText — only relevant when there is text to draw.
|
||||
if (text.itemCount != 0 && fontAtlas != nullptr && fontAtlas->dirty) {
|
||||
fontAtlas->Update(cmd);
|
||||
}
|
||||
|
||||
UIFusedHeader pc{};
|
||||
pc.itemBuffers[0] = quads.bufferSlot;
|
||||
pc.itemBuffers[1] = circles.bufferSlot;
|
||||
pc.itemBuffers[2] = images.bufferSlot;
|
||||
pc.itemBuffers[3] = text.bufferSlot;
|
||||
pc.itemCounts[0] = quads.itemCount;
|
||||
pc.itemCounts[1] = circles.itemCount;
|
||||
pc.itemCounts[2] = images.itemCount;
|
||||
pc.itemCounts[3] = text.itemCount;
|
||||
pc.outImage = outImageSlot_;
|
||||
pc.fontTexture = fontAtlasImageSlot_;
|
||||
pc.fontSampler = fontAtlasSamplerSlot_;
|
||||
pc.flags = 0;
|
||||
pc.surfaceWidth = window_->width;
|
||||
pc.surfaceHeight = window_->height;
|
||||
pc.frameIdx = window_->currentBuffer;
|
||||
pc._pad = 0;
|
||||
std::copy(quads.clipRectPx.begin(), quads.clipRectPx.end(), pc.clipQuads);
|
||||
std::copy(circles.clipRectPx.begin(), circles.clipRectPx.end(), pc.clipCircles);
|
||||
std::copy(images.clipRectPx.begin(), images.clipRectPx.end(), pc.clipImages);
|
||||
std::copy(text.clipRectPx.begin(), text.clipRectPx.end(), pc.clipText);
|
||||
|
||||
Dispatch(cmd, drawFused, &pc, sizeof(pc),
|
||||
TilesFor(window_->width), TilesFor(window_->height), 1u);
|
||||
}
|
||||
|
||||
// ─── generic Dispatch (with barrier) ────────────────────────────────────
|
||||
|
||||
void UIRenderer::Dispatch(GraphicsCommandBuffer cmd, const GraphicsComputeShader& shader,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue