custom shader webgpu
This commit is contained in:
parent
dedf6b0467
commit
64116cd980
12 changed files with 445 additions and 36 deletions
58
examples/CustomShader/inverse-circle.comp.wgsl
Normal file
58
examples/CustomShader/inverse-circle.comp.wgsl
Normal 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);
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue