custom shader webgpu

This commit is contained in:
Jorijn van der Graaf 2026-05-18 05:39:17 +02:00
commit 64116cd980
12 changed files with 445 additions and 36 deletions

View file

@ -0,0 +1,58 @@
// DOM-mode port of inverse-circle.comp.glsl. Inverts RGB inside each
// user-supplied circle; passes through every other pixel so the
// ping-pong carries the prior dispatch's scene forward.
//
// Bind-group contract (mirrors :WebGPUComputeShader.cppm):
// group 0 binding 0 uniform UIDispatchHeader (auto-injected)
// group 1 binding 0 texture_storage_2d<rgba8unorm, write> out (auto)
// group 1 binding 1 texture_2d<f32> prev (auto)
// group 2 binding 0 items SSBO declared by the C++ side via
// UICustomBinding { group=2, binding=0,
// kind=Buffer, pushOffset=offsetof(hdr.itemBuffer) }
struct UIDispatchHeader {
outImage: u32,
itemBuffer: u32,
surfaceW: u32,
surfaceH: u32,
clipX: f32,
clipY: f32,
clipW: f32,
clipH: f32,
itemCount: u32,
frameIdx: u32,
flags: u32,
_pad: u32,
};
@group(0) @binding(0) var<uniform> hdr : UIDispatchHeader;
@group(1) @binding(0) var outTex : texture_storage_2d<rgba8unorm, write>;
@group(1) @binding(1) var prevTex : texture_2d<f32>;
struct InverseCircleItem {
centerRadius: vec4<f32>,
};
@group(2) @binding(0) var<storage, read> items : array<InverseCircleItem>;
@compute @workgroup_size(8, 8, 1)
fn main(@builtin(global_invocation_id) gid: vec3<u32>) {
if (gid.x >= hdr.surfaceW || gid.y >= hdr.surfaceH) { return; }
let coord = vec2<i32>(i32(gid.x), i32(gid.y));
let sp = vec2<f32>(f32(gid.x) + 0.5, f32(gid.y) + 0.5);
// Max-coverage over all items, so overlapping circles don't
// double-invert back to the original.
var coverage: f32 = 0.0;
for (var i: u32 = 0u; i < hdr.itemCount; i = i + 1u) {
let it = items[i];
let c = it.centerRadius.xy;
let r = it.centerRadius.z;
let d = length(sp - c) - r;
coverage = max(coverage, clamp(0.5 - d, 0.0, 1.0));
}
var dst = textureLoad(prevTex, coord, 0);
if (coverage > 0.0) {
dst = vec4<f32>(mix(dst.rgb, vec3<f32>(1.0) - dst.rgb, coverage), dst.a);
}
textureStore(outTex, coord, dst);
}

View file

@ -2,8 +2,17 @@
// standard ones. The custom shader inverts RGB in the area covered by a
// list of circles. The mouse-tracking circle moves; two static ones sit
// on a striped background drawn with the standard DrawQuads shader.
//
// Works on both Vulkan (native) and WebGPU (DOM). The shader source is
// in two files — inverse-circle.comp.glsl (native, SPIR-V) and
// inverse-circle.comp.wgsl (DOM). The C++ surface differs only at the
// shader-load / buffer-flag sites; the per-frame UI building logic is
// identical.
#ifndef CRAFTER_GRAPHICS_WINDOW_DOM
#include "vulkan/vulkan.h"
#endif
#include <cstddef> // offsetof — a macro, so not visible via `import std;`
import Crafter.Graphics;
import Crafter.Event;
@ -11,18 +20,22 @@ import std;
using namespace Crafter;
// Application-side item POD. Matches `struct InverseCircleItem { vec4
// centerRadius; }` in inverse-circle.comp.glsl byte-for-byte.
// centerRadius; }` in inverse-circle.comp.{glsl,wgsl} byte-for-byte.
struct InverseCircleItem {
float cx, cy, radius, _pad;
};
int main() {
Device::Initialize();
#ifdef CRAFTER_GRAPHICS_WINDOW_DOM
static Window window(1280, 720, "Custom Shader");
#else
Window window(1280, 720, "Custom Shader");
#endif
VkCommandBuffer init = window.StartInit();
auto init = window.StartInit();
DescriptorHeapVulkan heap;
GraphicsDescriptorHeap heap;
heap.Initialize(/*images*/ 8, /*buffers*/ 8, /*samplers*/ 4);
window.descriptorHeap = &heap;
@ -30,26 +43,46 @@ int main() {
ui.Initialize(window, heap, init);
window.passes.push_back(&ui);
// Load the user-authored shader. Same wrapper as the four shipped with
// the library — there is no privileged path.
ComputeShader inverseCircle;
// Load the user-authored shader. On native it's an offline-compiled
// SPIR-V from .comp.glsl; on DOM it's WGSL source compiled at
// startup by the device. The DOM Load takes an extra `bindings` arg
// declaring resources the renderer should bind at dispatch time —
// here just one entry: the items SSBO at group 2, with its heap slot
// read from `hdr.itemBuffer` in the push data.
GraphicsComputeShader inverseCircle;
#ifndef CRAFTER_GRAPHICS_WINDOW_DOM
inverseCircle.Load("inverse-circle.comp.spv");
#else
UICustomBinding invBindings[] = {
{ .group = 2,
.binding = 0,
.kind = UICustomBindingKind::Buffer,
._pad = 0,
.pushOffset = static_cast<std::uint32_t>(offsetof(Crafter::UIDispatchHeader, itemBuffer)) },
};
inverseCircle.Load(std::filesystem::path("inverse-circle.comp.wgsl"), invBindings);
#endif
// User-owned buffers.
VulkanBuffer<QuadItem, true> quadsBuf;
VulkanBuffer<InverseCircleItem, true> invBuf;
GraphicsBuffer<QuadItem, true> quadsBuf;
GraphicsBuffer<InverseCircleItem, true> invBuf;
#ifndef CRAFTER_GRAPHICS_WINDOW_DOM
quadsBuf.Create(
VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT,
VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, 64);
invBuf.Create(
VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT,
VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, 16);
#else
quadsBuf.Create(64);
invBuf.Create(16);
#endif
auto quadsSlot = ui.RegisterBuffer(quadsBuf);
auto invSlot = ui.RegisterBuffer(invBuf);
EventListener<UIBuildArgs> buildSub(&ui.onBuild, [&](UIBuildArgs a) {
VkCommandBuffer cmd = a.cmd;
auto cmd = a.cmd;
Rect canvas = Rect::FromWindow(window);
@ -82,15 +115,24 @@ int main() {
// Standard dispatch first — paints the stripes.
if (qc > 0) {
#ifndef CRAFTER_GRAPHICS_WINDOW_DOM
quadsBuf.FlushDevice(cmd, VK_ACCESS_SHADER_READ_BIT, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT);
#else
quadsBuf.FlushDevice();
#endif
ui.DispatchQuads(cmd, quadsSlot, qc);
}
// Custom dispatch second — reads the stripes, inverts under
// circles, writes back. The library inserts the inter-dispatch
// SHADER_WRITE → SHADER_READ|WRITE barrier automatically.
// SHADER_WRITE → SHADER_READ|WRITE barrier automatically on
// Vulkan; WebGPU's compute-pass tracking does it for free.
if (ic > 0) {
#ifndef CRAFTER_GRAPHICS_WINDOW_DOM
invBuf.FlushDevice(cmd, VK_ACCESS_SHADER_READ_BIT, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT);
#else
invBuf.FlushDevice();
#endif
struct PC { UIDispatchHeader hdr; } pc { ui.FillHeader(invSlot, ic) };
std::uint32_t gx = (window.width + 7) / 8;
std::uint32_t gy = (window.height + 7) / 8;

View file

@ -4,6 +4,14 @@ namespace fs = std::filesystem;
using namespace Crafter;
extern "C" Configuration CrafterBuildProject(std::span<const std::string_view> args) {
bool isWasm = false;
for (std::string_view a : args) {
if (a.starts_with("--target=") && a.find("wasm") != std::string_view::npos) {
isWasm = true;
break;
}
}
Configuration* graphics = LocalProject({
.projectFile = "../../project.cpp",
.args = std::vector<std::string>(args.begin(), args.end()),
@ -13,6 +21,14 @@ extern "C" Configuration CrafterBuildProject(std::span<const std::string_view> a
cfg.path = "./";
cfg.name = "CustomShader";
cfg.outputName = "CustomShader";
cfg.type = ConfigurationType::Executable;
if (isWasm) {
cfg.target = "wasm32-wasip1";
cfg.defines.push_back({"CRAFTER_GRAPHICS_WINDOW_DOM", ""});
// Match the -msimd128 that Crafter.Math/Crafter.Graphics's wasm
// PCMs were compiled with.
cfg.compileFlags.push_back("-msimd128");
}
ApplyStandardArgs(cfg, args);
cfg.dependencies = { graphics };
@ -20,6 +36,14 @@ extern "C" Configuration CrafterBuildProject(std::span<const std::string_view> a
std::array<fs::path, 1> impls = { "main" };
cfg.GetInterfacesAndImplementations(ifaces, impls);
cfg.shaders.emplace_back(fs::path("inverse-circle.comp.glsl"), std::string("main"), ShaderType::Compute);
if (isWasm) {
// WGSL source is shipped as a static file and loaded via the
// WASI VFS at runtime through WebGPUComputeShader::Load(path).
cfg.files.emplace_back(fs::path("inverse-circle.comp.wgsl"));
EnableWasiBrowserRuntime(cfg);
} else {
cfg.shaders.emplace_back(fs::path("inverse-circle.comp.glsl"),
std::string("main"), ShaderType::Compute);
}
return cfg;
}

View file

@ -25,6 +25,10 @@ extern "C" Configuration CrafterBuildProject(std::span<const std::string_view> a
if (isWasm) {
cfg.target = "wasm32-wasip1";
cfg.defines.push_back({"CRAFTER_GRAPHICS_WINDOW_DOM", ""});
// Match the -msimd128 that Crafter.Math/Crafter.Graphics's wasm
// PCMs were compiled with, otherwise PCM imports trip a config-
// mismatch error in recent clangs.
cfg.compileFlags.push_back("-msimd128");
}
ApplyStandardArgs(cfg, args);
cfg.dependencies = { graphics };