Skip to content

Instantly share code, notes, and snippets.

@doxy-ai
Created November 19, 2023 22:03
Show Gist options
  • Save doxy-ai/eadc1f4d4d34103f14d6c33fd5c51859 to your computer and use it in GitHub Desktop.
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)
#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