Created
November 19, 2023 22:03
-
-
Save doxy-ai/eadc1f4d4d34103f14d6c33fd5c51859 to your computer and use it in GitHub Desktop.
Some helpers to make https://github.com/eliemichel/WebGPU-Cpp a tiny bit easier to use (as well as a simple fullscreen texture/manual rasterizer system)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#include <webgpu/webgpu.hpp> | |
#include <utility> | |
#include <string_view> | |
#include <span> | |
#include <cmath> | |
#include <iostream> | |
namespace helpers { | |
constexpr static size_t invalid = std::numeric_limits<size_t>::max(); | |
template<typename T> | |
T throwIfNull(T value, std::string_view message = "Provided value was null!") { | |
if(value == nullptr) | |
throw std::runtime_error(std::string(message)); | |
return value; | |
} | |
template<typename T, class C, typename F> | |
std::vector<T> enumerate(C& object, F function) { | |
std::vector<T> out((object.*function)(nullptr), static_cast<T::W>(0)); | |
(object.*function)(out.data()); | |
return out; | |
} | |
template<typename T, class C, typename F> | |
T returnize(C& object, F function) { | |
T out; | |
(object.*function)(&out); | |
return out; | |
} | |
template<typename T> | |
struct AutoRelease { | |
T handle; | |
AutoRelease(T handle = nullptr): handle(handle) {} | |
~AutoRelease() { if(handle) handle.release(); } | |
AutoRelease(const AutoRelease&) = default; | |
AutoRelease(AutoRelease&& o) { *this = std::move(o); } | |
AutoRelease& operator=(const AutoRelease&) = default; | |
AutoRelease& operator=(AutoRelease&& o) { | |
handle = std::exchange(o.handle, nullptr); | |
return *this; | |
} | |
operator T() { return handle; } | |
T* operator->() { return &handle; } | |
T& operator*() { return handle; } | |
}; | |
wgpu::TextureFormat getPreferredSurfaceFormat(wgpu::Surface& surface, wgpu::Adapter& adapter) { | |
#ifdef WEBGPU_BACKEND_DAWN | |
(void)surface; | |
(void)adapter; | |
return wgpu::TextureFormat::BGRA8Unorm; | |
#else | |
return surface.getPreferredFormat(adapter); | |
#endif | |
} | |
std::pair<wgpu::CommandEncoder, wgpu::RenderPassEncoder> beginRenderPass( | |
wgpu::TextureView& surfaceTexture, wgpu::Device& gpu, | |
wgpu::Color clearColor = {0, 0, 0, 1} | |
) { | |
auto encoder = throwIfNull(gpu.createCommandEncoder(wgpu::Default), "Failed to create Command Encoder"); | |
wgpu::RenderPassDescriptor pass = wgpu::Default; // Boring default | |
pass.label = "Render Pass"; | |
pass.colorAttachmentCount = 1; | |
wgpu::RenderPassColorAttachment color = wgpu::Default; | |
color.view = surfaceTexture; | |
color.resolveTarget = nullptr; // Used for multisampling | |
color.loadOp = wgpu::LoadOp::Clear; // Clear resets to clear color, load leaves whatever was there before | |
color.storeOp = wgpu::StoreOp::Store; // Discard obvious | |
color.clearValue = clearColor; | |
pass.colorAttachments = &color; | |
pass.depthStencilAttachment = nullptr; | |
// pass.timestampWrite = 0; | |
pass.timestampWrites = nullptr; | |
return {encoder, throwIfNull(encoder.beginRenderPass(pass), "Failed to create Render Pass")}; | |
} | |
void endRenderPass(wgpu::CommandEncoder encoder, wgpu::RenderPassEncoder pass, wgpu::Queue& queue) { | |
pass.end(); | |
queue.submit(encoder.finish(wgpu::Default)); | |
} | |
void endRenderPass(std::pair<wgpu::CommandEncoder, wgpu::RenderPassEncoder> pair, wgpu::Queue& queue) { endRenderPass(pair.first, pair.second, queue); } | |
wgpu::RenderPipelineDescriptor describeDefaultMaterial(wgpu::ShaderModule shaderModule, wgpu::TextureFormat surfaceFormat){ | |
static wgpu::FragmentState fragmentState; | |
static wgpu::BlendState blend; | |
static wgpu::ColorTargetState colorState; | |
wgpu::RenderPipelineDescriptor descriptor = wgpu::Default; | |
descriptor.label = "Default Material"; | |
descriptor.vertex.module = shaderModule; | |
descriptor.vertex.entryPoint = "vertex"; | |
descriptor.vertex.constantCount = 0; | |
descriptor.vertex.constants = nullptr; | |
blend.color.srcFactor = wgpu::BlendFactor::SrcAlpha; | |
blend.color.dstFactor = wgpu::BlendFactor::OneMinusSrcAlpha; | |
blend.color.operation = wgpu::BlendOperation::Add; | |
blend.alpha.srcFactor = wgpu::BlendFactor::Zero; | |
blend.alpha.dstFactor = wgpu::BlendFactor::One; | |
blend.alpha.operation = wgpu::BlendOperation::Add; | |
colorState.blend = &blend; | |
colorState.format = surfaceFormat; | |
colorState.writeMask = wgpu::ColorWriteMask::All; | |
fragmentState = wgpu::Default; | |
fragmentState.module = shaderModule; | |
fragmentState.entryPoint = "fragment"; | |
fragmentState.targetCount = 1; | |
fragmentState.targets = &colorState; | |
fragmentState.constantCount = 0; | |
fragmentState.constants = nullptr; | |
descriptor.fragment = &fragmentState; | |
descriptor.primitive.frontFace = wgpu::FrontFace::CCW; | |
descriptor.primitive.cullMode = wgpu::CullMode::None; | |
descriptor.primitive.stripIndexFormat = wgpu::IndexFormat::Undefined; | |
descriptor.primitive.topology = wgpu::PrimitiveTopology::TriangleList; | |
descriptor.depthStencil = nullptr; // No depth testing | |
// materialOptions.multisample = // Defaults fine | |
descriptor.layout = nullptr; // No external buffers! | |
return descriptor; | |
} | |
wgpu::ShaderModule compileWGSLShaders(std::string_view shaderSourceCode, wgpu::Device gpu, std::string_view label = "Default Vertex/Fragment Shader"){ | |
static wgpu::ShaderModuleWGSLDescriptor shaderCode; | |
shaderCode = wgpu::Default; | |
// shaderCode.chain.next = nullptr; | |
// shaderCode.chain.sType = wgpu::SType::ShaderModuleWGSLDescriptor; | |
shaderCode.code = shaderSourceCode.data(); | |
auto dbg = &shaderCode.chain; | |
wgpu::ShaderModuleDescriptor desc = wgpu::Default; | |
desc.nextInChain = dbg; | |
desc.label = label.data(); | |
#ifdef WEBGPU_BACKEND_WGPU | |
desc.hintCount = 0; | |
desc.hints = nullptr; | |
#endif | |
return gpu.createShaderModule(desc); | |
} | |
template<typename T = float> | |
struct Color { T r, g, b, a; }; | |
template<typename T> | |
Color<T> operator*(Color<T> c, T s) { return { c.r * s, c.g * s, c.b * s, c.a * s}; } | |
template<typename T> | |
Color<T> operator*(T s, Color<T> c) { return { c.r * s, c.g * s, c.b * s, c.a * s}; } | |
template<typename T> | |
Color<T> operator+(Color<T> a, Color<T> b) { return { a.r + b.r, a.g + b.g, a.b + b.b, a.a + b.a}; } | |
struct FullscreenTexture { | |
constexpr static std::string_view shaderTemplate = R"SHADER( | |
const verts = array<vec2f, 4>(vec2f(-1.0, -1.0), vec2f(-1.0, 1.0), vec2f(1.0, -1.0), vec2f(1.0, 1.0)); | |
fn calculate_vert(index: u32) -> vec2f { | |
switch index { | |
case 0u: { return verts[0]; } | |
case 1u: { return verts[1]; } | |
case 2u: { return verts[2]; } | |
case 3u: { return verts[1]; } | |
case 4u: { return verts[3]; } | |
case 5u: { return verts[2]; } | |
default: { return verts[0]; } | |
} | |
} | |
@vertex | |
fn vertex(@builtin(vertex_index) in_vertex_index: u32) -> @builtin(position) vec4f { | |
return vec4f(calculate_vert(in_vertex_index), 0.0, 1.0); | |
} | |
@group(0) @binding(0) var fullscreenTexture: texture_2d<f32>; | |
@fragment | |
fn fragment(@builtin(position) position: vec4f) -> @location(0) vec4f { | |
return vec4f(textureLoad(fullscreenTexture, vec2i(position.xy / %f), 0).rgb, 1.0); | |
})SHADER"; | |
float ratio = 1; // A divisor aplied to the width and height passed in, | |
AutoRelease<wgpu::Texture> texture; | |
AutoRelease<wgpu::TextureView> view; | |
AutoRelease<wgpu::ShaderModule> shader; | |
AutoRelease<wgpu::RenderPipeline> pipeline; | |
AutoRelease<wgpu::PipelineLayout> layout; | |
AutoRelease<wgpu::BindGroupLayout> bindLayout; | |
AutoRelease<wgpu::BindGroup> bindGroup; | |
void reinit(wgpu::TextureFormat surfaceFormat, uint32_t width, uint32_t height, wgpu::Device& gpu) { | |
width /= ratio; | |
height /= ratio; | |
texture = throwIfNull(gpu.createTexture(WGPUTextureDescriptor{ | |
.nextInChain = nullptr, | |
.label = "Full Screen Texture", | |
.usage = wgpu::TextureUsage::TextureBinding | wgpu::TextureUsage::CopyDst, | |
.dimension = wgpu::TextureDimension::_2D, | |
.size = {width, height, 1}, | |
.format = wgpu::TextureFormat::RGBA32Float, | |
.mipLevelCount = 1, | |
.sampleCount = 1, | |
.viewFormatCount = 0, | |
.viewFormats = nullptr, | |
}), "Failed to create Texture"); | |
view = throwIfNull(texture->createView(WGPUTextureViewDescriptor{ | |
.nextInChain = nullptr, | |
.label = "Full Screen Texture View", | |
.format = texture->getFormat(), | |
.dimension = wgpu::TextureViewDimension::_2D, | |
.baseMipLevel = 0, | |
.mipLevelCount = 1, | |
.baseArrayLayer = 0, | |
.arrayLayerCount = 1, | |
.aspect = wgpu::TextureAspect::All, | |
}), "Failed to create Texture View"); | |
wgpu::BindGroupLayoutEntry entry; | |
entry.binding = 0; | |
entry.visibility = wgpu::ShaderStage::Fragment; | |
entry.texture.sampleType = wgpu::TextureSampleType::UnfilterableFloat; | |
entry.texture.viewDimension = wgpu::TextureViewDimension::_2D; | |
bindLayout = throwIfNull(gpu.createBindGroupLayout(WGPUBindGroupLayoutDescriptor{ | |
.nextInChain = nullptr, | |
.label = "Full Screen Texture Layout", | |
.entryCount = 1, | |
.entries = &entry, | |
}), "Failed to create Bind Layout"); | |
wgpu::BindGroupEntry binding; | |
binding.binding = 0; | |
binding.textureView = *view; | |
bindGroup = throwIfNull(gpu.createBindGroup(WGPUBindGroupDescriptor{ | |
.nextInChain = nullptr, | |
.label = "Full Screen Texture Bind Group", | |
.layout = *bindLayout, | |
.entryCount = 1, | |
.entries = &binding, | |
}), "Failed to create Bind Group"); | |
layout = throwIfNull(gpu.createPipelineLayout(WGPUPipelineLayoutDescriptor{ | |
.nextInChain = nullptr, | |
.label = "Full Screen Texture Pipeline Layout", | |
.bindGroupLayoutCount = 1, | |
.bindGroupLayouts = (WGPUBindGroupLayout*) &bindLayout.handle, | |
}), "Failed to create Pipeline Layout"); | |
// Inject the ratio into the shader code! | |
auto shaderCode = std::string(shaderTemplate); | |
shaderCode.resize(shaderCode.size() + 20); | |
sprintf(shaderCode.data(), shaderTemplate.data(), ratio); | |
shader = throwIfNull(compileWGSLShaders(shaderCode, gpu), "Failed to create Shader"); | |
wgpu::RenderPipelineDescriptor desc = describeDefaultMaterial(*shader, surfaceFormat); | |
desc.layout = *layout; | |
// desc.layout = nullptr; | |
pipeline = throwIfNull(gpu.createRenderPipeline(desc), "Failed to create Pipeline"); | |
} | |
void draw(wgpu::RenderPassEncoder& renderPass) { | |
renderPass.setPipeline(*pipeline); | |
renderPass.setBindGroup(0, *bindGroup, 0, nullptr); | |
renderPass.draw(6, 1, 0, 0); | |
} | |
template<size_t N = std::dynamic_extent> | |
void upload(std::span<Color<float>, N> pixels, wgpu::Queue& queue) { | |
wgpu::Extent3D size = {texture->getWidth(), texture->getHeight(), 1}; | |
wgpu::ImageCopyTexture destination; | |
destination.texture = *texture; | |
destination.mipLevel = 0; | |
destination.origin = {0, 0, 0}; | |
destination.aspect = wgpu::TextureAspect::All; | |
wgpu::TextureDataLayout source; | |
source.offset = 0; | |
source.bytesPerRow = sizeof(Color<float>) * size.width; // Minimum 256! | |
source.rowsPerImage = size.height; | |
queue.writeTexture(destination, pixels.data(), pixels.size() * sizeof(Color<float>), source, size); | |
queue.submit(0, nullptr); | |
} | |
void uploadGradient(wgpu::Queue& queue) { | |
wgpu::Extent3D size = {texture->getWidth(), texture->getHeight(), 1}; | |
std::vector<Color<float>> pixels(size.width * size.height); | |
for (uint32_t i = 0; i < size.width; ++i) { | |
for (uint32_t j = 0; j < size.height; ++j) { | |
Color<float>& p = pixels[j * size.width + i]; | |
p.r = i / float(size.width); | |
p.g = j / float(size.height); | |
p.b = .5; | |
p.a = 1; | |
} | |
} | |
upload({pixels}, queue); | |
} | |
size_t width() { return texture->getWidth(); } | |
size_t height() { return texture->getHeight(); } | |
std::pair<size_t, size_t> size() { return std::pair{width(), height()}; } | |
size_t linearLength() { return width() * height(); } | |
}; | |
namespace rasterize { | |
void point( | |
size_t pointX, size_t pointY, size_t radius, helpers::Color<float> color, | |
std::vector<helpers::Color<float>>& pixels, std::pair<size_t, size_t> dims | |
) { | |
auto [width, height] = dims; | |
for(size_t y = pointY - radius; y <= pointY + radius; y++) // For each row | |
for(size_t x = pointX - radius; x <= pointX + radius; x++) // For each column | |
pixels[y * width + x] = color; | |
} | |
void roundedPoint( | |
size_t pointX, size_t pointY, size_t innerRadius, size_t outerRadius, helpers::Color<float> color, | |
std::vector<helpers::Color<float>>& pixels, std::pair<size_t, size_t> dims | |
) { | |
auto [width, height] = dims; | |
for(size_t y = pointY - outerRadius; y <= pointY + outerRadius; y++) // For each row | |
for(size_t x = pointX - outerRadius; x <= pointX + outerRadius; x++) { // For each column | |
float deltaX = float(x) - pointX; | |
float deltaY = float(y) - pointY; | |
float magnitudeSquared = deltaX * deltaX + deltaY * deltaY; | |
if (magnitudeSquared < outerRadius * outerRadius) { | |
float mag = sqrt(magnitudeSquared) - innerRadius; | |
float t = std::min<float>(mag / (outerRadius - innerRadius), 1); | |
pixels[y * width + x] = pixels[y * width + x] * t + (1 - t) * color; | |
} | |
} | |
} | |
// Version which sets the inner radius to be the same as the outer radius (no anti-aliasing) | |
void roundedPoint( | |
size_t pointX, size_t pointY, size_t radius, helpers::Color<float> color, | |
std::vector<helpers::Color<float>>& pixels, std::pair<size_t, size_t> dims | |
) { roundedPoint(pointX, pointY, radius, radius, color, pixels, dims); } | |
// Bresenham's Line Algorithm | |
void line( | |
size_t startX, size_t startY, helpers::Color<float> startColor, size_t endX, size_t endY, helpers::Color<float> endColor, | |
std::vector<helpers::Color<float>>& pixels, std::pair<size_t, size_t> dims | |
) { | |
constexpr static auto impl = []( | |
bool isHigh, size_t startX, size_t startY, helpers::Color<float> startColor, size_t endX, size_t endY, helpers::Color<float> endColor, | |
std::vector<helpers::Color<float>>& pixels, std::pair<size_t, size_t> dims | |
) { | |
auto [width, _] = dims; | |
int64_t dx = endX - startX; | |
int64_t dy = endY - startY; | |
auto dc = isHigh ? dx : dy; | |
auto dco = isHigh ? dy : dx; | |
auto i = 1; | |
if (dc < 0) { | |
i = -1; | |
dc = -dc; | |
} | |
auto D = (2 * dc) - dco; | |
auto y = startY; | |
auto x = startX; | |
auto& c = isHigh ? y : x; | |
auto& o = isHigh ? x : y; | |
for (auto end = isHigh ? endY : endX, start = isHigh ? startY : startX; c <= end; c++){ | |
float t = float(c - start) / (end - start); | |
pixels[y * width + x] = startColor * (1 - t) + t * endColor; | |
if (D > 0) { | |
o = o + i; | |
D = D + (2 * (dc - dco)); | |
} else | |
D = D + 2*dc; | |
} | |
}; | |
if (abs(endY - startY) < abs(endX - startX)) { | |
if (startX > endX) | |
impl(false, endX, endY, endColor, startX, startY, startColor, pixels, dims); | |
else | |
impl(false, startX, startY, startColor, endX, endY, endColor, pixels, dims); | |
} else { | |
if (startY > endY) | |
impl(true, endX, endY, endColor, startX, startY, startColor, pixels, dims); | |
else | |
impl(true, startX, startY, startColor, endX, endY, endColor, pixels, dims); | |
} | |
} | |
// Version with no interploation | |
void line( | |
size_t startX, size_t startY, size_t endX, size_t endY, helpers::Color<float> color, | |
std::vector<helpers::Color<float>>& pixels, std::pair<size_t, size_t> dims | |
) { line(startX, startY, color, endX, endY, color, pixels, dims); } | |
void triangle( | |
size_t x1, size_t y1, helpers::Color<float> color1, size_t x2, size_t y2, helpers::Color<float> color2, size_t x3, size_t y3, helpers::Color<float> color3, | |
std::vector<helpers::Color<float>>& pixels, std::pair<size_t, size_t> dims | |
) { | |
auto [width, _] = dims; | |
auto minX = std::min({x1, x2, x3}); | |
auto maxX = std::max({x1, x2, x3}); | |
auto minY = std::min({y1, y2, y3}); | |
auto maxY = std::max({y1, y2, y3}); | |
rasterize::line(x1, y1, color1, x2, y2, color2, pixels, dims); | |
rasterize::line(x2, y2, color2, x3, y3, color3, pixels, dims); | |
rasterize::line(x3, y3, color3, x1, y1, color1, pixels, dims); | |
for(auto y = minY; y <= maxY; y++) { | |
size_t start = helpers::invalid, end = helpers::invalid; | |
bool foundFilled = false; | |
bool lookingForSecond = false; | |
for(auto x = minX; x <= maxX; x++) | |
if(auto& p = pixels[y * width + x]; !(p.r == 0 && p.g == 0 && p.b == 0) ) { | |
// First filled block | |
if(!lookingForSecond) { | |
// First filled pixel | |
if(!foundFilled) | |
foundFilled = true; | |
// Second filled block | |
} else { | |
end = x - 1; | |
break; | |
} | |
} else if(!lookingForSecond && foundFilled) { | |
start = x; | |
lookingForSecond = true; | |
} | |
if (start == helpers::invalid || end == helpers::invalid) | |
continue; | |
// Expand the region to include the already drawn pixelss | |
start--; | |
end++; | |
rasterize::line(start, y, pixels[y * width + start], end, y, pixels[y * width + end], pixels, dims); | |
} | |
} | |
void triangle( | |
size_t x1, size_t y1, size_t x2, size_t y2, size_t x3, size_t y3, helpers::Color<float> color, | |
std::vector<helpers::Color<float>>& pixels, std::pair<size_t, size_t> dims | |
) { triangle(x1, y1, color, x2, y2, color, x3, y3, color, pixels, dims); } | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment