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
|
|
@ -59,6 +59,39 @@ export namespace Crafter {
|
|||
};
|
||||
static_assert(sizeof(UIDispatchHeader) == 48);
|
||||
|
||||
// ─── fused-dispatch push-constant header (issue #47) ────────────────
|
||||
// Mirrors the PC block in shaders/ui-fused.comp.glsl byte-for-byte: every
|
||||
// member is vec4-aligned, no padding holes, exactly 128 bytes (the
|
||||
// guaranteed push-constant minimum). This is a SEPARATE contract — the
|
||||
// per-element Dispatch* path and the frozen 48-byte UIDispatchHeader are
|
||||
// untouched. DispatchFused builds this; users never fill it directly.
|
||||
struct UIFusedHeader {
|
||||
std::uint32_t itemBuffers[4]; // heap slots: quads, circles, images, text
|
||||
std::uint32_t itemCounts[4]; // item counts: quads, circles, images, text
|
||||
std::uint32_t outImage; // swapchain image heap slot
|
||||
std::uint32_t fontTexture; // font-atlas image slot (text phase)
|
||||
std::uint32_t fontSampler; // font-atlas sampler slot (text phase)
|
||||
std::uint32_t flags; // reserved — keep zeroed
|
||||
std::uint32_t surfaceWidth;
|
||||
std::uint32_t surfaceHeight;
|
||||
std::uint32_t frameIdx;
|
||||
std::uint32_t _pad; // reserved — keep zeroed
|
||||
float clipQuads[4]; // (x, y, w, h) clip per category
|
||||
float clipCircles[4];
|
||||
float clipImages[4];
|
||||
float clipText[4];
|
||||
};
|
||||
static_assert(sizeof(UIFusedHeader) == 128);
|
||||
|
||||
// One category's input to UIRenderer::DispatchFused. A zero itemCount
|
||||
// makes that category a free no-op (its kernel loop is zero-trip).
|
||||
// clipRectPx defaults to "no clip", matching the Dispatch* default.
|
||||
struct FusedBatch {
|
||||
std::uint32_t bufferSlot = 0;
|
||||
std::uint32_t itemCount = 0;
|
||||
std::array<float,4> clipRectPx = {0.0f, 0.0f, 1e9f, 1e9f};
|
||||
};
|
||||
|
||||
// ─── standard item PODs (match GLSL std430) ─────────────────────────
|
||||
struct QuadItem {
|
||||
float x, y, w, h;
|
||||
|
|
@ -143,6 +176,7 @@ export namespace Crafter {
|
|||
GraphicsComputeShader drawCircles;
|
||||
GraphicsComputeShader drawImages;
|
||||
GraphicsComputeShader drawText;
|
||||
GraphicsComputeShader drawFused;
|
||||
|
||||
FontAtlas* fontAtlas = nullptr;
|
||||
|
||||
|
|
@ -156,7 +190,8 @@ export namespace Crafter {
|
|||
std::filesystem::path quadsSpv = "ui-quads.comp.spv",
|
||||
std::filesystem::path circlesSpv = "ui-circles.comp.spv",
|
||||
std::filesystem::path imagesSpv = "ui-images.comp.spv",
|
||||
std::filesystem::path textSpv = "ui-text.comp.spv");
|
||||
std::filesystem::path textSpv = "ui-text.comp.spv",
|
||||
std::filesystem::path fusedSpv = "ui-fused.comp.spv");
|
||||
|
||||
void Record(GraphicsCommandBuffer cmd, std::uint32_t frameIdx, Window& window) override;
|
||||
|
||||
|
|
@ -186,6 +221,32 @@ export namespace Crafter {
|
|||
void DispatchText(GraphicsCommandBuffer cmd, std::uint32_t bufferSlot, std::uint32_t itemCount,
|
||||
std::array<float,4> clipRectPx = {0.0f, 0.0f, 1e9f, 1e9f});
|
||||
|
||||
// Fused UI dispatch (issue #47). Composites up to four standard
|
||||
// categories — quads → circles → images → text, canonical
|
||||
// back-to-front order — in ONE compute dispatch that loads the
|
||||
// destination image once and stores once, eliminating the per-pass
|
||||
// load+store and the inter-pass memory barriers a run of consecutive
|
||||
// Dispatch* calls would pay. The win materialises at >= 2 non-empty
|
||||
// categories; an empty category (itemCount 0) is a free no-op.
|
||||
//
|
||||
// Additive: the per-element Dispatch* calls and UIDispatchHeader are
|
||||
// untouched. Keep using those for single-category or fine-grained
|
||||
// work. 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.
|
||||
//
|
||||
// The text category uses the registered font atlas (set fontAtlas
|
||||
// before Initialize); passing a non-empty text batch without one
|
||||
// throws, exactly like DispatchText. On the WebGPU backend this falls
|
||||
// back to issuing the per-element Dispatch* calls in canonical order
|
||||
// (one texture/sampler per dispatch there — see DispatchImages),
|
||||
// so the result matches; the load/store fusion is Vulkan-only.
|
||||
void DispatchFused(GraphicsCommandBuffer cmd,
|
||||
const FusedBatch& quads,
|
||||
const FusedBatch& circles,
|
||||
const FusedBatch& images,
|
||||
const FusedBatch& text);
|
||||
|
||||
// Generic dispatch for user-authored shaders. On Vulkan, `shader` is
|
||||
// a SPIR-V compute pipeline (bindless via VK_EXT_descriptor_heap, so
|
||||
// any resource indices baked into push data resolve through the
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue