webgpu sponza

This commit is contained in:
Jorijn van der Graaf 2026-05-19 00:27:09 +02:00
commit b5d0f52da0
21 changed files with 1426 additions and 58 deletions

View file

@ -181,5 +181,15 @@ export namespace Crafter {
}
return *this;
}
// Convenience: create the "standard" linear-filter clamp-to-edge sampler,
// allocate a slot for it, and return the slot. The wgpu* bridge call is
// intentionally kept inside the library — example code shouldn't need to
// reach into Crafter::WebGPU directly.
inline SamplerSlot AllocateLinearClampSampler(DescriptorHeapWebGPU& heap) {
DescriptorRange r = heap.AllocateSamplerSlots(1);
heap.samplerTable[r.firstElement] = WebGPU::wgpuCreateLinearClampSampler();
return SamplerSlot(&heap, r.firstElement);
}
}
#endif // CRAFTER_GRAPHICS_WINDOW_DOM

View file

@ -0,0 +1,166 @@
/*
Crafter®.Graphics
Copyright (C) 2026 Catcrafts®
catcrafts.net
This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License version 3.0 as published by the Free Software Foundation;
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public
License along with this library; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/
// Image2D<T> — portable 2D image type whose API surface is intentionally
// backend-specific via #ifdef. On Vulkan it aliases the existing
// ImageVulkan<T> (full VkFormat / usage / layout control). On WebGPU it's
// a thin handle around an rgba8unorm GPUTexture; sizes are u16 and the
// only update path is from a CompressedTextureAsset.
//
// The "no shared no-op signatures" principle is deliberate: callers do
// the same #ifdef the library does, and write the backend-specific
// invocation. The unified type name Image2D<T> is the only thing
// portable between the two — that's the whole point.
export module Crafter.Graphics:Image2D;
#ifndef CRAFTER_GRAPHICS_WINDOW_DOM
import :ImageVulkan;
export namespace Crafter {
// Vulkan target: Image2D is just the existing ImageVulkan. New name,
// same shape — keeps existing ImageVulkan callers (e.g. examples/
// Decompression) working without a churn-rename.
template <typename PixelType>
using Image2D = ImageVulkan<PixelType>;
}
#endif // !CRAFTER_GRAPHICS_WINDOW_DOM
#ifdef CRAFTER_GRAPHICS_WINDOW_DOM
import std;
import Crafter.Asset;
import :DescriptorHeapWebGPU;
import :WebGPU;
export namespace Crafter {
template <typename PixelType>
class Image2D {
public:
WebGPUTextureRef handle = 0;
std::uint16_t width = 0;
std::uint16_t height = 0;
void Create(std::uint16_t w, std::uint16_t h) {
width = w;
height = h;
handle = WebGPU::wgpuCreateImage2D(w, h);
}
// CPU-decompress the .ctex blob (no GPU decompression on WebGPU)
// and upload via wgpuWriteImage2D. The intermediate `pixels` vector
// lives only for the duration of this call — the underlying
// queue.writeTexture in JS makes its own copy.
void Update(const CompressedTextureAsset& asset) {
if (asset.pixelStride != sizeof(PixelType)) {
std::println(std::cerr,
"Image2D::Update: pixel stride mismatch (got {}, expected {})",
asset.pixelStride, sizeof(PixelType));
std::abort();
}
std::vector<PixelType> pixels(
static_cast<std::size_t>(asset.sizeX) * asset.sizeY);
std::array<std::span<std::byte>, 1> outputs = {
std::as_writable_bytes(std::span(pixels)),
};
Compression::DecompressCPU(asset.blob, outputs);
WebGPU::wgpuWriteImage2D(
handle,
pixels.data(),
static_cast<std::int32_t>(pixels.size() * sizeof(PixelType)),
asset.sizeX, asset.sizeY);
}
// Register the texture in a descriptor heap slot so a custom RT
// pipeline can bind it via UICustomBinding::SampledTexture.
ImageSlot AllocateSlot(DescriptorHeapWebGPU& heap) {
DescriptorRange r = heap.AllocateImageSlots(1);
heap.imageTable[r.firstElement] = handle;
return ImageSlot(&heap, r.firstElement);
}
void Destroy() {
if (handle != 0) {
WebGPU::wgpuDestroyTexture(handle);
handle = 0;
}
}
};
// 2D texture array — `layers` × (w × h) rgba8unorm. Each layer is
// populated independently from a CompressedTextureAsset whose dims
// must match the array's (w × h). Layer 0 is sampled at array
// index 0 in WGSL; bind through UICustomBindingKind::SampledTextureArray.
template <typename PixelType>
class Image2DArray {
public:
WebGPUTextureRef handle = 0;
std::uint16_t width = 0;
std::uint16_t height = 0;
std::uint16_t layers = 0;
void Create(std::uint16_t w, std::uint16_t h, std::uint16_t layerCount) {
width = w;
height = h;
layers = layerCount;
handle = WebGPU::wgpuCreateImage2DArray(w, h, layerCount);
}
// Decompress `tex` and upload to `layer`. The asset's dims must
// match the array's (w × h) — resize beforehand on the host with
// TextureAsset<RGBA8>::Resize() if they don't.
void UpdateLayer(std::uint16_t layer, const CompressedTextureAsset& tex) {
if (tex.pixelStride != sizeof(PixelType)) {
std::println(std::cerr,
"Image2DArray::UpdateLayer: pixel stride mismatch (got {}, expected {})",
tex.pixelStride, sizeof(PixelType));
std::abort();
}
if (tex.sizeX != width || tex.sizeY != height) {
std::println(std::cerr,
"Image2DArray::UpdateLayer: layer {} dims {}x{} don't match array dims {}x{}",
layer, tex.sizeX, tex.sizeY, width, height);
std::abort();
}
std::vector<PixelType> pixels(static_cast<std::size_t>(width) * height);
std::array<std::span<std::byte>, 1> outputs = {
std::as_writable_bytes(std::span(pixels)),
};
Compression::DecompressCPU(tex.blob, outputs);
WebGPU::wgpuWriteImage2DLayer(
handle, layer,
pixels.data(),
static_cast<std::int32_t>(pixels.size() * sizeof(PixelType)),
width, height);
}
ImageSlot AllocateSlot(DescriptorHeapWebGPU& heap) {
DescriptorRange r = heap.AllocateImageSlots(1);
heap.imageTable[r.firstElement] = handle;
return ImageSlot(&heap, r.firstElement);
}
void Destroy() {
if (handle != 0) {
WebGPU::wgpuDestroyTexture(handle);
handle = 0;
}
}
};
}
#endif // CRAFTER_GRAPHICS_WINDOW_DOM

View file

@ -64,6 +64,7 @@ export namespace Crafter {
#ifdef CRAFTER_GRAPHICS_WINDOW_DOM
import std;
import Crafter.Math;
import Crafter.Asset;
import :WebGPU;
export namespace Crafter {
@ -108,6 +109,15 @@ export namespace Crafter {
void Build(std::span<Crafter::Vector<float, 3, 3>> vertices,
std::span<std::uint32_t> indices,
WebGPUCommandEncoderRef cmd = 0);
// CPU-decompress the .cmesh blob (no VK_EXT_memory_decompression
// equivalent in WebGPU) and forward to the positions+indices path,
// plus push the optional `data` region into the per-vertex attribs
// heap so closest-hit shaders can sample UVs / normals / tangents.
// The data layout is example-defined — the heap is exposed in WGSL
// as `vertexAttribs : array<u32>` with a per-mesh u32-word offset.
void Build(const ::Crafter::CompressedMeshAsset& asset,
WebGPUCommandEncoderRef cmd = 0);
};
}
#endif // CRAFTER_GRAPHICS_WINDOW_DOM

View file

@ -26,22 +26,31 @@ import std;
import :RT;
import :WebGPU;
import :ShaderBindingTableWebGPU;
import :WebGPUComputeShader;
export namespace Crafter {
class PipelineRTWebGPU {
public:
std::uint32_t pipelineHandle = 0;
// Mirror of the bindings handed to Init. Kept for the example /
// RTPass to consult when packing the handles[] array at dispatch
// time (one resolved u32 handle per binding, in declaration order).
std::vector<UICustomBinding> userBindings;
// Build the megakernel pipeline. Groups carry indices into
// `sbt.shaders`. The library generates one `case` per registered
// group: closest-hit groups dispatch to their closestHitShader's
// entryFn, miss groups to their generalShader's entryFn, etc.
// The `cmd` parameter is unused on WebGPU; kept for API symmetry.
// `userBindings` declares extra @group(2)+ resources the user's
// closest-hit / miss / raygen WGSL touches (material SSBOs,
// albedo textures, samplers).
void Init(WebGPUCommandEncoderRef cmd,
std::span<const RTShaderGroup> raygenGroups,
std::span<const RTShaderGroup> missGroups,
std::span<const RTShaderGroup> hitGroups,
const ShaderBindingTableWebGPU& sbt);
const ShaderBindingTableWebGPU& sbt,
std::span<const UICustomBinding> bindings = {});
PipelineRTWebGPU() = default;
PipelineRTWebGPU(const PipelineRTWebGPU&) = delete;

View file

@ -66,6 +66,12 @@ export namespace Crafter {
// RTDispatchHeader. Null means "no extra data".
const void* pushPtr = nullptr;
std::uint32_t pushBytes = 0;
// Resolved WebGPU resource handles for each user binding the
// pipeline was loaded with, in declaration order. The example
// owns the storage (typically a small std::array of u32). Null /
// 0 means "no user bindings".
const void* handlesPtr = nullptr;
std::uint32_t handlesCount = 0;
RTPass(PipelineRTWebGPU* p) : pipeline(p) {}
@ -80,7 +86,9 @@ export namespace Crafter {
tlas.buffer.handle,
static_cast<std::int32_t>(tlas.builtInstanceCount),
static_cast<std::int32_t>(gx),
static_cast<std::int32_t>(gy));
static_cast<std::int32_t>(gy),
handlesPtr,
static_cast<std::int32_t>(handlesCount));
}
};
}

View file

@ -49,6 +49,27 @@ namespace Crafter::WebGPU {
__attribute__((import_module("env"), import_name("wgpuDestroyTexture")))
extern "C" void wgpuDestroyTexture(std::uint32_t handle);
// General-purpose rgba8unorm 2D texture for material albedo etc.
// Separate from the atlas path because atlas uses r8unorm + sub-region
// writes; this one takes the whole image in one shot.
__attribute__((import_module("env"), import_name("wgpuCreateImage2D")))
extern "C" std::uint32_t wgpuCreateImage2D(std::int32_t w, std::int32_t h);
__attribute__((import_module("env"), import_name("wgpuWriteImage2D")))
extern "C" void wgpuWriteImage2D(std::uint32_t handle, const void* srcPtr,
std::int32_t byteSize,
std::int32_t w, std::int32_t h);
// 2D texture array — `layerCount` rgba8unorm layers of identical (w × h).
// Sampled via `texture_2d_array<f32>` in WGSL (UICustomBindingKind 3).
// Used by Image2DArray<RGBA8> to stack per-material albedos for one
// multi-material scene.
__attribute__((import_module("env"), import_name("wgpuCreateImage2DArray")))
extern "C" std::uint32_t wgpuCreateImage2DArray(std::int32_t w, std::int32_t h, std::int32_t layerCount);
__attribute__((import_module("env"), import_name("wgpuWriteImage2DLayer")))
extern "C" void wgpuWriteImage2DLayer(std::uint32_t handle, std::int32_t layer,
const void* srcPtr, std::int32_t byteSize,
std::int32_t w, std::int32_t h);
__attribute__((import_module("env"), import_name("wgpuCreateLinearClampSampler")))
extern "C" std::uint32_t wgpuCreateLinearClampSampler();
@ -96,6 +117,11 @@ namespace Crafter::WebGPU {
// stores in RTInstance::accelerationStructureReference; the WebGPU
// TLAS-build compute shader resolves it back to root AABB + heap
// offsets at dispatch time. Returns 0 on failure.
// The optional `attribsPtr` / `attribsByteCount` carry per-vertex
// attribute payload (normals, UVs, etc. — layout is example-defined)
// that gets appended to a global attribs heap and exposed to RT
// closest-hit shaders as `vertexAttribs : array<u32>` at
// @group(1) @binding(7). Pass (nullptr, 0) for positions-only meshes.
__attribute__((import_module("env"), import_name("wgpuRegisterMeshBLAS")))
extern "C" std::uint32_t wgpuRegisterMeshBLAS(
float minX, float minY, float minZ,
@ -103,25 +129,34 @@ namespace Crafter::WebGPU {
const void* verticesPtr, std::int32_t vertexCount,
const void* indicesPtr, std::int32_t indexCount,
const void* bvhNodesPtr, std::int32_t bvhNodeCount,
const void* primRemapPtr, std::int32_t primRemapCount);
const void* primRemapPtr, std::int32_t primRemapCount,
const void* attribsPtr, std::int32_t attribsByteCount);
// RT pipeline build. The library composes WGSL by concatenating the
// traversal library, generated hit-group switches, and the user-
// supplied raygen / miss / closesthit / anyhit bodies. Returns an
// opaque pipeline handle.
// supplied raygen / miss / closesthit / anyhit bodies. `bindings` is
// UICustomBinding-shaped (8 bytes each) declaring extra @group(2)+
// resources the user's closest-hit / miss / raygen WGSL references.
// Pass (nullptr, 0) for a pipeline with no user-declared bindings.
// Returns an opaque pipeline handle.
__attribute__((import_module("env"), import_name("wgpuLoadRTPipeline")))
extern "C" std::uint32_t wgpuLoadRTPipeline(const void* wgslPtr, std::int32_t wgslLen);
extern "C" std::uint32_t wgpuLoadRTPipeline(const void* wgslPtr, std::int32_t wgslLen,
const void* bindingsPtr, std::int32_t bindingsCount);
// Dispatch a TraceRays-equivalent pass: the RT pipeline is dispatched
// over a (gx, gy) tile grid; the library writes the push data (camera,
// payload, etc. — opaque) into a uniform ring buffer, attaches the TLAS
// + global mesh heap, and runs one workgroup per 8x8 screen tile.
// `handles[]` carries resolved WebGPU resource handles for every user
// binding declared at pipeline-load time, in the same order. Pass
// (nullptr, 0) for a pipeline with no user bindings.
__attribute__((import_module("env"), import_name("wgpuDispatchRT")))
extern "C" void wgpuDispatchRT(std::uint32_t pipelineHandle,
const void* pushPtr, std::int32_t pushBytes,
std::uint32_t tlasBufHandle,
std::int32_t instanceCount,
std::int32_t gx, std::int32_t gy);
std::int32_t gx, std::int32_t gy,
const void* handlesPtr, std::int32_t handlesCount);
// GPU TLAS-build dispatch. Reads the instance buffer (host-uploaded or
// GPU-written), produces per-instance world-space AABBs + per-instance

View file

@ -32,9 +32,10 @@ import :WebGPU;
export namespace Crafter {
enum class UICustomBindingKind : std::uint8_t {
Buffer = 0, // read-only-storage SSBO, handle is a slot into heap.bufferTable
SampledTexture = 1, // sampled texture_2d<f32>, handle is a slot into heap.imageTable
Sampler = 2, // filtering sampler, handle is a slot into heap.samplerTable
Buffer = 0, // read-only-storage SSBO, handle is a slot into heap.bufferTable
SampledTexture = 1, // sampled texture_2d<f32>, handle is a slot into heap.imageTable
Sampler = 2, // filtering sampler, handle is a slot into heap.samplerTable
SampledTextureArray = 3, // sampled texture_2d_array<f32>, handle is a slot into heap.imageTable
};
struct UICustomBinding {

View file

@ -47,6 +47,7 @@ export import :ShaderBindingTableVulkan;
export import :PipelineRTVulkan;
export import :RenderingElement3D;
export import :ImageVulkan;
export import :Image2D;
export import :SamplerVulkan;
export import :DescriptorHeapVulkan;
export import :RenderPass;