webgpu support

This commit is contained in:
Jorijn van der Graaf 2026-05-18 04:58:52 +02:00
commit dedf6b0467
22 changed files with 1656 additions and 324 deletions

View file

@ -0,0 +1,185 @@
/*
Crafter®.Graphics
Copyright (C) 2026 Catcrafts®
catcrafts.net
*/
// DOM-mode parallel to DescriptorHeapVulkan. WebGPU has no real bindless,
// so the "heap" is purely a CPU-side slot allocator with a side-table
// mapping slot → JS-side WebGPU handle. UIRenderer looks up the handle by
// slot at dispatch time to build (or fetch from cache) the bind group.
export module Crafter.Graphics:DescriptorHeapWebGPU;
#ifdef CRAFTER_GRAPHICS_WINDOW_DOM
import std;
import :WebGPU;
export namespace Crafter {
struct DescriptorHeapWebGPU;
struct DescriptorRange {
std::uint16_t firstElement;
std::uint16_t count;
};
class BufferSlot {
public:
DescriptorHeapWebGPU* heap = nullptr;
std::uint16_t firstElement = 0xFFFF;
BufferSlot() = default;
BufferSlot(DescriptorHeapWebGPU* h, std::uint16_t f) : heap(h), firstElement(f) {}
BufferSlot(const BufferSlot&) = delete;
BufferSlot& operator=(const BufferSlot&) = delete;
BufferSlot(BufferSlot&& o) noexcept : heap(o.heap), firstElement(o.firstElement) { o.firstElement = 0xFFFF; }
BufferSlot& operator=(BufferSlot&& o) noexcept;
~BufferSlot();
explicit operator bool() const noexcept { return firstElement != 0xFFFF; }
operator std::uint16_t() const noexcept { return firstElement; }
};
class ImageSlot {
public:
DescriptorHeapWebGPU* heap = nullptr;
std::uint16_t firstElement = 0xFFFF;
ImageSlot() = default;
ImageSlot(DescriptorHeapWebGPU* h, std::uint16_t f) : heap(h), firstElement(f) {}
ImageSlot(const ImageSlot&) = delete;
ImageSlot& operator=(const ImageSlot&) = delete;
ImageSlot(ImageSlot&& o) noexcept : heap(o.heap), firstElement(o.firstElement) { o.firstElement = 0xFFFF; }
ImageSlot& operator=(ImageSlot&& o) noexcept;
~ImageSlot();
explicit operator bool() const noexcept { return firstElement != 0xFFFF; }
operator std::uint16_t() const noexcept { return firstElement; }
};
class SamplerSlot {
public:
DescriptorHeapWebGPU* heap = nullptr;
std::uint16_t firstElement = 0xFFFF;
SamplerSlot() = default;
SamplerSlot(DescriptorHeapWebGPU* h, std::uint16_t f) : heap(h), firstElement(f) {}
SamplerSlot(const SamplerSlot&) = delete;
SamplerSlot& operator=(const SamplerSlot&) = delete;
SamplerSlot(SamplerSlot&& o) noexcept : heap(o.heap), firstElement(o.firstElement) { o.firstElement = 0xFFFF; }
SamplerSlot& operator=(SamplerSlot&& o) noexcept;
~SamplerSlot();
explicit operator bool() const noexcept { return firstElement != 0xFFFF; }
operator std::uint16_t() const noexcept { return firstElement; }
};
struct DescriptorHeapWebGPU {
std::vector<WebGPUBufferRef> bufferTable;
std::vector<WebGPUTextureRef> imageTable;
std::vector<WebGPUSamplerRef> samplerTable;
std::vector<std::uint16_t> bufferFreelist;
std::vector<std::uint16_t> imageFreelist;
std::vector<std::uint16_t> samplerFreelist;
std::uint16_t nextBuffer = 0;
std::uint16_t nextImage = 0;
std::uint16_t nextSampler = 0;
void Initialize(std::uint16_t images, std::uint16_t buffers, std::uint16_t samplers) {
imageTable.assign(images, 0);
bufferTable.assign(buffers, 0);
samplerTable.assign(samplers, 0);
imageFreelist.reserve(images);
bufferFreelist.reserve(buffers);
samplerFreelist.reserve(samplers);
}
DescriptorRange AllocateBufferSlots(std::uint16_t count) {
if (count == 1 && !bufferFreelist.empty()) {
auto f = bufferFreelist.back(); bufferFreelist.pop_back();
return { f, 1 };
}
if (nextBuffer + count > bufferTable.size()) {
std::println("DescriptorHeapWebGPU: buffer slots exhausted");
std::abort();
}
DescriptorRange r{ nextBuffer, count };
nextBuffer = static_cast<std::uint16_t>(nextBuffer + count);
return r;
}
DescriptorRange AllocateImageSlots(std::uint16_t count) {
if (count == 1 && !imageFreelist.empty()) {
auto f = imageFreelist.back(); imageFreelist.pop_back();
return { f, 1 };
}
if (nextImage + count > imageTable.size()) {
std::println("DescriptorHeapWebGPU: image slots exhausted");
std::abort();
}
DescriptorRange r{ nextImage, count };
nextImage = static_cast<std::uint16_t>(nextImage + count);
return r;
}
DescriptorRange AllocateSamplerSlots(std::uint16_t count) {
if (count == 1 && !samplerFreelist.empty()) {
auto f = samplerFreelist.back(); samplerFreelist.pop_back();
return { f, 1 };
}
if (nextSampler + count > samplerTable.size()) {
std::println("DescriptorHeapWebGPU: sampler slots exhausted");
std::abort();
}
DescriptorRange r{ nextSampler, count };
nextSampler = static_cast<std::uint16_t>(nextSampler + count);
return r;
}
void FreeBufferSlots(std::uint16_t first, std::uint16_t count) noexcept {
for (std::uint16_t i = 0; i < count; ++i) {
bufferTable[first + i] = 0;
bufferFreelist.push_back(first + i);
}
}
void FreeImageSlots(std::uint16_t first, std::uint16_t count) noexcept {
for (std::uint16_t i = 0; i < count; ++i) {
imageTable[first + i] = 0;
imageFreelist.push_back(first + i);
}
}
void FreeSamplerSlots(std::uint16_t first, std::uint16_t count) noexcept {
for (std::uint16_t i = 0; i < count; ++i) {
samplerTable[first + i] = 0;
samplerFreelist.push_back(first + i);
}
}
};
// ─── slot dtors (defined here since they reference DescriptorHeapWebGPU) ─
inline BufferSlot::~BufferSlot() {
if (firstElement != 0xFFFF && heap) heap->FreeBufferSlots(firstElement, 1);
}
inline BufferSlot& BufferSlot::operator=(BufferSlot&& o) noexcept {
if (this != &o) {
if (firstElement != 0xFFFF && heap) heap->FreeBufferSlots(firstElement, 1);
heap = o.heap; firstElement = o.firstElement; o.firstElement = 0xFFFF;
}
return *this;
}
inline ImageSlot::~ImageSlot() {
if (firstElement != 0xFFFF && heap) heap->FreeImageSlots(firstElement, 1);
}
inline ImageSlot& ImageSlot::operator=(ImageSlot&& o) noexcept {
if (this != &o) {
if (firstElement != 0xFFFF && heap) heap->FreeImageSlots(firstElement, 1);
heap = o.heap; firstElement = o.firstElement; o.firstElement = 0xFFFF;
}
return *this;
}
inline SamplerSlot::~SamplerSlot() {
if (firstElement != 0xFFFF && heap) heap->FreeSamplerSlots(firstElement, 1);
}
inline SamplerSlot& SamplerSlot::operator=(SamplerSlot&& o) noexcept {
if (this != &o) {
if (firstElement != 0xFFFF && heap) heap->FreeSamplerSlots(firstElement, 1);
heap = o.heap; firstElement = o.firstElement; o.firstElement = 0xFFFF;
}
return *this;
}
}
#endif // CRAFTER_GRAPHICS_WINDOW_DOM

View file

@ -19,13 +19,8 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/
module;
#ifndef CRAFTER_GRAPHICS_WINDOW_DOM
#include "../lib/stb_truetype.h"
#endif // !CRAFTER_GRAPHICS_WINDOW_DOM
export module Crafter.Graphics:Font;
#ifndef CRAFTER_GRAPHICS_WINDOW_DOM
import std;
namespace Crafter {
@ -36,7 +31,6 @@ namespace Crafter {
if (i >= text.size()) return 0;
std::uint8_t b0 = static_cast<std::uint8_t>(text[i]);
// Single-byte ASCII is the common path.
if (b0 < 0x80) { ++i; return b0; }
int extra;
@ -44,13 +38,13 @@ namespace Crafter {
if ((b0 & 0xE0) == 0xC0) { extra = 1; cp = b0 & 0x1F; }
else if ((b0 & 0xF0) == 0xE0) { extra = 2; cp = b0 & 0x0F; }
else if ((b0 & 0xF8) == 0xF0) { extra = 3; cp = b0 & 0x07; }
else { ++i; return 0xFFFD; } // continuation byte at start, or 5+-byte leader
else { ++i; return 0xFFFD; }
++i;
for (int k = 0; k < extra; ++k) {
if (i >= text.size()) return 0xFFFD;
std::uint8_t b = static_cast<std::uint8_t>(text[i]);
if ((b & 0xC0) != 0x80) return 0xFFFD; // missing continuation
if ((b & 0xC0) != 0x80) return 0xFFFD;
cp = (cp << 6) | (b & 0x3Fu);
++i;
}
@ -67,8 +61,7 @@ namespace Crafter {
Font(const std::filesystem::path& font);
std::uint32_t GetLineWidth(const std::string_view text, float size);
float LineHeight(float size);
float AscentPx(float size); // baseline offset from line-top
float ScaleForSize(float size); // stb's pixel-units-per-em factor
float AscentPx(float size);
float ScaleForSize(float size);
};
}
#endif // !CRAFTER_GRAPHICS_WINDOW_DOM

View file

@ -19,70 +19,61 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
module;
#ifndef CRAFTER_GRAPHICS_WINDOW_DOM
#include "vulkan/vulkan.h"
#endif // !CRAFTER_GRAPHICS_WINDOW_DOM
#endif
export module Crafter.Graphics:FontAtlas;
#ifndef CRAFTER_GRAPHICS_WINDOW_DOM
import std;
import :Font;
import :GraphicsTypes;
#ifndef CRAFTER_GRAPHICS_WINDOW_DOM
import :ImageVulkan;
import :Device;
#else
import :WebGPU;
#endif
export namespace Crafter {
// Per-glyph metrics. UVs are 0..1 in atlas space; on-screen sizes /
// offsets / advance are in *atlas pixels at the base size* and scale
// linearly with the requested font size at draw time.
struct Glyph {
float u0 = 0, v0 = 0; // top-left UV in the atlas
float u1 = 0, v1 = 0; // bottom-right UV in the atlas
float w = 0, h = 0; // glyph quad size in atlas px (= the bitmap size)
float xoff = 0, yoff = 0; // glyph bearing relative to baseline cursor
float advance = 0; // horizontal advance at base size, in atlas px
float u0 = 0, v0 = 0;
float u1 = 0, v1 = 0;
float w = 0, h = 0;
float xoff = 0, yoff = 0;
float advance = 0;
};
// Single-channel SDF atlas. Glyphs are rasterised with stb_truetype's
// GetGlyphSDF at a fixed `kBaseSize` resolution and packed via a simple
// shelf allocator. Drawing scales the glyph quad linearly; the shader
// resolves edge AA via screen-space derivatives, so a single atlas
// serves all sizes and DPI scales without re-bake.
class FontAtlas {
public:
// Build-time constants. Tweak in one place if needed; values picked
// to give crisp text from ~10pt to ~96pt and leave headroom in the
// SDF distance band so smoothstep is in the linear regime.
static constexpr int kAtlasSize = 1024;
static constexpr float kBaseSize = 32.0f; // pixel-height at which we rasterise
static constexpr int kPadding = 4; // distance-field padding around each glyph
static constexpr int kOnEdgeValue = 128; // 8-bit value mapped to "0 distance"
static constexpr float kPixelDistScale = 32.0f; // how many distance units per pixel — wider = softer AA range
static constexpr float kBaseSize = 32.0f;
static constexpr int kPadding = 4;
static constexpr int kOnEdgeValue = 128;
static constexpr float kPixelDistScale = 32.0f;
#ifndef CRAFTER_GRAPHICS_WINDOW_DOM
ImageVulkan<std::uint8_t> image;
bool dirty = false; // staging has unflushed writes
#else
WebGPUTextureRef textureHandle = 0;
std::vector<std::uint8_t> staging;
#endif
bool dirty = false;
// Allocate the GPU image and zero-clear it. Must be called once
// with a one-shot init command buffer.
void Initialize(VkCommandBuffer cmd);
void Initialize(GraphicsCommandBuffer cmd);
// Returns the row-major byte pointer the CPU writes pixels into.
// Same shape on both backends.
std::uint8_t* PixelPtr() noexcept;
// Rasterise + pack the glyph if it isn't cached yet. Returns
// false only if the atlas is out of space (V2: grow). After a
// successful Ensure the bitmap lives in `image.buffer.value` and
// `dirty` is true; call Update(cmd) before reading on the GPU.
bool Ensure(Font& font, std::uint32_t codepoint);
// Lookup is cheap (hash-table). Returns nullptr if the glyph
// hasn't been Ensured.
const Glyph* Lookup(Font& font, std::uint32_t codepoint) const;
// If `dirty`, flushes staging into the GPU image and transitions
// it back to SHADER_READ_ONLY_OPTIMAL. No-op if not dirty.
void Update(VkCommandBuffer cmd);
void Update(GraphicsCommandBuffer cmd);
private:
// Shelf packer state.
struct Shelf { int y = 0; int height = 0; int cursorX = 0; };
std::vector<Shelf> shelves_;
int nextShelfY_ = 0;
// (font*, codepoint) → Glyph cache.
struct Key {
const Font* font;
std::uint32_t cp;
@ -97,8 +88,6 @@ export namespace Crafter {
};
std::unordered_map<Key, Glyph, KeyHash> cache_;
// Place a wxh glyph; returns true + writes top-left into outX/outY.
bool ShelfPlace(int w, int h, int& outX, int& outY);
};
}
#endif // !CRAFTER_GRAPHICS_WINDOW_DOM

View file

@ -0,0 +1,42 @@
/*
Crafter®.Graphics
Copyright (C) 2026 Catcrafts®
catcrafts.net
*/
// Backend-portable type aliases. NOT an abstraction layer — these are pure
// `using` declarations that resolve to the backend's native types per the
// active CRAFTER_GRAPHICS_WINDOW_* define.
module;
#ifndef CRAFTER_GRAPHICS_WINDOW_DOM
#include "vulkan/vulkan.h"
#endif
export module Crafter.Graphics:GraphicsTypes;
#ifndef CRAFTER_GRAPHICS_WINDOW_DOM
import :VulkanBuffer;
import :DescriptorHeapVulkan;
import :ComputeShader;
export namespace Crafter {
using GraphicsCommandBuffer = VkCommandBuffer;
using GraphicsDescriptorHeap = DescriptorHeapVulkan;
using GraphicsComputeShader = ComputeShader;
template<class T, bool Mapped>
using GraphicsBuffer = VulkanBuffer<T, Mapped>;
}
#else
import :WebGPU;
import :WebGPUBuffer;
import :DescriptorHeapWebGPU;
import :WebGPUComputeShader;
export namespace Crafter {
using GraphicsCommandBuffer = WebGPUCommandEncoderRef;
using GraphicsDescriptorHeap = DescriptorHeapWebGPU;
using GraphicsComputeShader = WebGPUComputeShader;
template<class T, bool Mapped>
using GraphicsBuffer = WebGPUBuffer<T, Mapped>;
}
#endif

View file

@ -16,20 +16,15 @@ 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
*/
module;
#ifndef CRAFTER_GRAPHICS_WINDOW_DOM
#include "vulkan/vulkan.h"
#endif // !CRAFTER_GRAPHICS_WINDOW_DOM
export module Crafter.Graphics:RenderPass;
#ifndef CRAFTER_GRAPHICS_WINDOW_DOM
import std;
import :GraphicsTypes;
export namespace Crafter {
struct Window;
struct RenderPass {
virtual void Record(VkCommandBuffer cmd, std::uint32_t frameIdx, Window& window) = 0;
virtual void Record(GraphicsCommandBuffer cmd, std::uint32_t frameIdx, Window& window) = 0;
virtual ~RenderPass() = default;
};
}
#endif // !CRAFTER_GRAPHICS_WINDOW_DOM

View file

@ -19,20 +19,27 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
module;
#ifndef CRAFTER_GRAPHICS_WINDOW_DOM
#include "vulkan/vulkan.h"
#endif // !CRAFTER_GRAPHICS_WINDOW_DOM
#endif
export module Crafter.Graphics:UI;
#ifndef CRAFTER_GRAPHICS_WINDOW_DOM
import std;
import Crafter.Event;
import :Device;
import :Window;
import :RenderPass;
import :GraphicsTypes;
import :FontAtlas;
import :Font;
#ifndef CRAFTER_GRAPHICS_WINDOW_DOM
import :Device;
import :DescriptorHeapVulkan;
import :ImageVulkan;
import :VulkanBuffer;
import :ComputeShader;
import :FontAtlas;
import :Font;
#else
import :DescriptorHeapWebGPU;
import :WebGPU;
import :WebGPUBuffer;
import :WebGPUComputeShader;
#endif
export namespace Crafter {
// ─── push-constant header ───────────────────────────────────────────
@ -56,8 +63,8 @@ export namespace Crafter {
struct QuadItem {
float x, y, w, h;
float r, g, b, a;
float cTL, cTR, cBR, cBL; // per-corner radius in px
float outline, oR, oG, oB; // outline thickness + RGB
float cTL, cTR, cBR, cBL;
float outline, oR, oG, oB;
};
static_assert(sizeof(QuadItem) == 64);
@ -84,15 +91,11 @@ export namespace Crafter {
static_assert(sizeof(GlyphItem) == 48);
// ─── tiny rect-carving helper ───────────────────────────────────────
// Pure value semantics. No engine, just convenience. Skip if you'd rather
// compute pixels yourself.
struct Rect {
float x = 0, y = 0, w = 0, h = 0;
enum class Anchor { Top, Bottom, Left, Right };
// Returns a sub-rect of `size` along the given anchor edge of self.
// Does not modify `*this`. (Use `.Inset(...)` to drop a margin first.)
Rect SubRect(float size, Anchor a) const noexcept {
switch (a) {
case Anchor::Top: return { x, y, w, std::min(size, h) };
@ -123,121 +126,71 @@ export namespace Crafter {
// ─── per-frame callback args ────────────────────────────────────────
struct UIBuildArgs {
VkCommandBuffer cmd;
std::uint32_t frameIdx;
GraphicsCommandBuffer cmd;
std::uint32_t frameIdx;
};
// ─── UIRenderer ─────────────────────────────────────────────────────
// One per Window (typically). Owns the four standard compute shaders,
// pre-allocates heap slots for the swapchain images, and exposes a thin
// dispatch helper for both the standard shaders and user-supplied ones.
//
// Workflow:
// 1. Construct, configure (set fontAtlas if drawing text).
// 2. Initialize(window, heap, initCmd) — once, after window.descriptorHeap
// is set and before window.FinishInit().
// 3. window.passes.push_back(&ui).
// 4. Listen on `onBuild`. Inside the callback, fill your item buffers,
// flush them, and call DispatchQuads / DispatchCircles / DispatchImages
// / DispatchText / Dispatch as needed. Library inserts a SHADER_WRITE
// → SHADER_READ|WRITE memory barrier between consecutive dispatches.
class UIRenderer : public RenderPass {
public:
// Pre-loaded standard shaders (public so users can call Dispatch
// directly with them if they want to embed extra push-constant fields
// beyond the standard header).
ComputeShader drawQuads;
ComputeShader drawCircles;
ComputeShader drawImages;
ComputeShader drawText;
GraphicsComputeShader drawQuads;
GraphicsComputeShader drawCircles;
GraphicsComputeShader drawImages;
GraphicsComputeShader drawText;
// Optional. If set before Initialize, the atlas is registered into a
// sampled-image slot + linear sampler slot, and Update(cmd) is called
// at the top of every Record() so any glyphs ensured during onBuild
// make it to the GPU before the text dispatch reads them.
FontAtlas* fontAtlas = nullptr;
// User callback. Subscribe by holding a Crafter::EventListener<UIBuildArgs>:
// EventListener<UIBuildArgs> sub(&ui.onBuild, [&](UIBuildArgs a) { ... });
// Listener lifetime governs the subscription.
Crafter::Event<UIBuildArgs> onBuild;
UIRenderer() = default;
UIRenderer(const UIRenderer&) = delete;
UIRenderer& operator=(const UIRenderer&) = delete;
// Default shader paths assume Crafter.Build placed the .spv files
// alongside the consumer binary (this is what cfg.shaders does).
void Initialize(Window& window, DescriptorHeapVulkan& heap, VkCommandBuffer initCmd,
void Initialize(Window& window, GraphicsDescriptorHeap& heap, GraphicsCommandBuffer initCmd,
std::filesystem::path quadsSpv = "ui-quads.comp.spv",
std::filesystem::path circlesSpv = "ui-circles.comp.spv",
std::filesystem::path imagesSpv = "ui-images.comp.spv",
std::filesystem::path textSpv = "ui-text.comp.spv");
// RenderPass interface — invoked from Window::Render.
void Record(VkCommandBuffer cmd, std::uint32_t frameIdx, Window& window) override;
void Record(GraphicsCommandBuffer cmd, std::uint32_t frameIdx, Window& window) override;
// ─── helpers used inside `onBuild` ─────────────────────────────
// Builds a populated header. `clipRectPx` defaults to "no clip".
UIDispatchHeader FillHeader(std::uint32_t itemBufferSlot,
std::uint32_t itemCount,
std::array<float,4> clipRectPx = {0.0f, 0.0f, 1e9f, 1e9f},
std::uint32_t flags = 0) const noexcept;
// Convenience: dispatches the named standard shader. Group count is
// computed from the window's surface size — the standard shaders
// dispatch one workgroup per 8×8 screen tile and iterate every item
// in the buffer in order, so item ORDER in the buffer == draw order
// on screen (later items overdraw earlier ones, race-free).
void DispatchQuads(VkCommandBuffer cmd, std::uint32_t bufferSlot, std::uint32_t itemCount,
void DispatchQuads(GraphicsCommandBuffer cmd, std::uint32_t bufferSlot, std::uint32_t itemCount,
std::array<float,4> clipRectPx = {0.0f, 0.0f, 1e9f, 1e9f});
void DispatchCircles(VkCommandBuffer cmd, std::uint32_t bufferSlot, std::uint32_t itemCount,
void DispatchCircles(GraphicsCommandBuffer cmd, std::uint32_t bufferSlot, std::uint32_t itemCount,
std::array<float,4> clipRectPx = {0.0f, 0.0f, 1e9f, 1e9f});
void DispatchImages(VkCommandBuffer cmd, std::uint32_t bufferSlot, std::uint32_t itemCount,
void DispatchImages(GraphicsCommandBuffer cmd, std::uint32_t bufferSlot, std::uint32_t itemCount,
std::array<float,4> clipRectPx = {0.0f, 0.0f, 1e9f, 1e9f});
// For DispatchText, the font atlas image+sampler slots are taken from
// UIRenderer's Initialize-time registration (see fontAtlasImageSlot()
// / fontAtlasSamplerSlot()). Set `fontAtlas` before Initialize.
void DispatchText(VkCommandBuffer cmd, std::uint32_t bufferSlot, std::uint32_t itemCount,
void DispatchText(GraphicsCommandBuffer cmd, std::uint32_t bufferSlot, std::uint32_t itemCount,
std::array<float,4> clipRectPx = {0.0f, 0.0f, 1e9f, 1e9f});
// Generic dispatch — for user-authored shaders. Inserts the standard
// pre-dispatch barrier (skipped on the first call per frame).
void Dispatch(VkCommandBuffer cmd, const ComputeShader& shader,
#ifndef CRAFTER_GRAPHICS_WINDOW_DOM
// Generic dispatch — user-authored shaders. Vulkan-only in v1; on DOM
// the WebGPU side has no bindless and would need per-shader bind-group
// declaration. See plan section 3b for the design path.
void Dispatch(GraphicsCommandBuffer cmd, const ComputeShader& shader,
const void* push, std::uint32_t pushBytes,
std::uint32_t gx, std::uint32_t gy = 1, std::uint32_t gz = 1);
#endif
// Allocates a heap slot for the buffer and writes its descriptor into
// every per-frame heap. The user's mapped buffer is shared across
// frames — fine because Window::Render currently waits idle before
// submitting the next frame. Returns a move-only BufferSlot handle
// whose destructor returns the slot to the heap. Implicitly converts
// to the absolute heap index when passed to FillHeader / Dispatch*.
// Allocates a heap slot for the buffer and registers the GPU handle.
// Returns a move-only BufferSlot RAII handle.
template<typename T, bool Mapped>
BufferSlot RegisterBuffer(VulkanBuffer<T, Mapped>& buffer);
BufferSlot RegisterBuffer(GraphicsBuffer<T, Mapped>& buffer);
// Same for an ImageVulkan-managed sampled image (e.g. a user texture).
// Caller specifies the layout the image will be sampled in (typically
// VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL).
#ifndef CRAFTER_GRAPHICS_WINDOW_DOM
template<typename Pixel>
ImageSlot RegisterImage(ImageVulkan<Pixel>& image, VkFormat format,
VkImageLayout layout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL);
// Allocates a sampler slot and writes a VkSamplerCreateInfo into
// every per-frame sampler heap. v1 takes the create-info inline.
SamplerSlot RegisterSampler(const VkSamplerCreateInfo& info);
// Convenience: a linear-filter, clamp-to-edge sampler. Returns a
// SamplerSlot handle. Useful for the FontAtlas and most plain image
// sampling.
#endif
SamplerSlot RegisterLinearClampSampler();
// Shapes a UTF-8 string into glyph quads at (x, y) baseline. Calls
// FontAtlas::Ensure for each codepoint (rasterising on first use),
// emits one GlyphItem per visible glyph, returns the count written.
// Use this to fill a GlyphItem buffer that you then dispatch.
// Cursor advances along +X. No line-wrap, no kerning — single line.
std::uint32_t ShapeText(Font& font, float pxSize,
float x, float baselineY,
std::string_view utf8,
@ -245,79 +198,53 @@ export namespace Crafter {
GlyphItem* out, std::uint32_t outCapacity,
float* outAdvance = nullptr);
// Read after Initialize: the slot the font atlas was registered into.
// 0xFFFF means "no atlas" (set fontAtlas before Initialize).
std::uint16_t FontAtlasImageSlot() const noexcept { return fontAtlasImageSlot_; }
std::uint16_t FontAtlasSamplerSlot() const noexcept { return fontAtlasSamplerSlot_; }
// Heap slot whose descriptor in each per-frame heap points at that
// frame's swapchain image. Other passes (e.g. a ray-tracing pass
// that wants to render the world directly into the swapchain) can
// write to the same image by referencing this slot. Order in
// window.passes controls compositing — push such passes BEFORE
// the UI pass so UI overlays render on top.
std::uint16_t OutImageSlot() const noexcept { return outImageSlot_; }
private:
Window* window_ = nullptr;
DescriptorHeapVulkan* heap_ = nullptr;
// One image slot used for the swapchain output. In each per-frame
// heap, that slot points at THAT frame's swapchain image. So the
// shader's `uiImages[hdr.outImage]` is always the current frame's
// swapchain image regardless of which heap is bound.
ImageSlot outImageSlot_;
// Stable VkImageViewCreateInfos for the descriptor heap to ingest.
// These must outlive the write call.
VkImageViewCreateInfo atlasViewCreateInfo_{};
GraphicsDescriptorHeap* heap_ = nullptr;
ImageSlot outImageSlot_;
ImageSlot fontAtlasImageSlot_;
SamplerSlot fontAtlasSamplerSlot_;
#ifndef CRAFTER_GRAPHICS_WINDOW_DOM
VkImageViewCreateInfo atlasViewCreateInfo_{};
bool firstDispatchThisFrame_ = true;
#endif
// Subscription to window.onResize. Each resize destroys the old
// swapchain images, so the per-frame heap entries we wrote at
// outImageSlot_ now reference dangling VkImage handles. The
// listener re-writes them and flushes the descriptor heaps.
Crafter::EventListener<void> resizeSub_;
#ifndef CRAFTER_GRAPHICS_WINDOW_DOM
void WriteSwapchainDescriptors();
void WriteFontAtlasDescriptor();
// Helper used by RegisterBuffer template (defined in impl). Writes the
// address-range descriptor at `slot` into all per-frame heaps.
void WriteBufferDescriptor(std::uint16_t slot, VkDeviceAddress address, std::uint32_t size);
// Helper used by RegisterImage template — writes a sampled-image at
// `slot` referring to a stable VkImageViewCreateInfo (caller stores).
void WriteSampledImageDescriptor(std::uint16_t slot,
const VkImageViewCreateInfo& viewInfo,
VkImageLayout layout);
#endif
};
// ─── template-method implementations ────────────────────────────────
template<typename T, bool Mapped>
BufferSlot UIRenderer::RegisterBuffer(VulkanBuffer<T, Mapped>& buffer) {
BufferSlot UIRenderer::RegisterBuffer(GraphicsBuffer<T, Mapped>& buffer) {
auto range = heap_->AllocateBufferSlots(1);
#ifndef CRAFTER_GRAPHICS_WINDOW_DOM
WriteBufferDescriptor(range.firstElement, buffer.address, buffer.size);
// BufferSlot's operator uint16_t() folds in heap_->bufferStartElement,
// so callers receive the absolute heap index when they convert.
#else
heap_->bufferTable[range.firstElement] = buffer.handle;
#endif
return BufferSlot{heap_, range.firstElement};
}
#ifndef CRAFTER_GRAPHICS_WINDOW_DOM
template<typename Pixel>
ImageSlot UIRenderer::RegisterImage(ImageVulkan<Pixel>& image, VkFormat format,
VkImageLayout layout) {
auto range = heap_->AllocateImageSlots(1);
// Build a stable view-create-info that lives as long as the heap reads
// it. We co-locate it on the renderer for the font atlas; for arbitrary
// user images we lean on the fact that vkWriteResourceDescriptorsEXT
// copies the view descriptor immediately. (Validated by the heap spec:
// the descriptor is materialised at write time, the create-info need
// not persist past the call.)
VkImageViewCreateInfo info {
.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO,
.image = image.image,
@ -338,5 +265,5 @@ export namespace Crafter {
WriteSampledImageDescriptor(range.firstElement, info, layout);
return ImageSlot{heap_, range.firstElement};
}
#endif
}
#endif // !CRAFTER_GRAPHICS_WINDOW_DOM

View file

@ -16,12 +16,7 @@ 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
*/
module;
#ifndef CRAFTER_GRAPHICS_WINDOW_DOM
#include "vulkan/vulkan.h"
#endif // !CRAFTER_GRAPHICS_WINDOW_DOM
export module Crafter.Graphics:UIComponents;
#ifndef CRAFTER_GRAPHICS_WINDOW_DOM
import std;
import :UI;
import :Font;
@ -143,4 +138,3 @@ export namespace Crafter {
std::array<float, 4> tint = {1, 1, 1, 1},
std::array<float, 4> uv = {0, 0, 1, 1});
}
#endif // !CRAFTER_GRAPHICS_WINDOW_DOM

View file

@ -0,0 +1,75 @@
/*
Crafter®.Graphics
Copyright (C) 2026 Catcrafts®
catcrafts.net
*/
// JS bridge declarations for the DOM-mode WebGPU backend. Each function
// corresponds to one entry in `additional/dom-webgpu.js`. Handles are
// opaque uint32 cookies into the JS-side handle tables.
export module Crafter.Graphics:WebGPU;
#ifdef CRAFTER_GRAPHICS_WINDOW_DOM
import std;
export namespace Crafter {
using WebGPUBufferRef = std::uint32_t;
using WebGPUTextureRef = std::uint32_t;
using WebGPUSamplerRef = std::uint32_t;
using WebGPUCommandEncoderRef = std::uint32_t; // unused as a real handle; just a marker type for portability
}
namespace Crafter::WebGPU {
__attribute__((import_module("env"), import_name("wgpuGetCanvasWidth")))
extern "C" std::int32_t wgpuGetCanvasWidth();
__attribute__((import_module("env"), import_name("wgpuGetCanvasHeight")))
extern "C" std::int32_t wgpuGetCanvasHeight();
__attribute__((import_module("env"), import_name("wgpuSurfaceWidth")))
extern "C" std::int32_t wgpuSurfaceWidth();
__attribute__((import_module("env"), import_name("wgpuSurfaceHeight")))
extern "C" std::int32_t wgpuSurfaceHeight();
__attribute__((import_module("env"), import_name("wgpuInit")))
extern "C" void wgpuInit();
__attribute__((import_module("env"), import_name("wgpuCreateBuffer")))
extern "C" std::uint32_t wgpuCreateBuffer(std::int32_t byteSize);
__attribute__((import_module("env"), import_name("wgpuWriteBuffer")))
extern "C" void wgpuWriteBuffer(std::uint32_t handle, const void* srcPtr, std::int32_t byteSize);
__attribute__((import_module("env"), import_name("wgpuDestroyBuffer")))
extern "C" void wgpuDestroyBuffer(std::uint32_t handle);
__attribute__((import_module("env"), import_name("wgpuCreateAtlasTexture")))
extern "C" std::uint32_t wgpuCreateAtlasTexture(std::int32_t w, std::int32_t h);
__attribute__((import_module("env"), import_name("wgpuWriteAtlasRegion")))
extern "C" void wgpuWriteAtlasRegion(std::uint32_t handle, const void* srcPtr,
std::int32_t srcW, std::int32_t srcH,
std::int32_t srcBytesPerRow,
std::int32_t dstX, std::int32_t dstY,
std::int32_t copyW, std::int32_t copyH);
__attribute__((import_module("env"), import_name("wgpuDestroyTexture")))
extern "C" void wgpuDestroyTexture(std::uint32_t handle);
__attribute__((import_module("env"), import_name("wgpuCreateLinearClampSampler")))
extern "C" std::uint32_t wgpuCreateLinearClampSampler();
__attribute__((import_module("env"), import_name("wgpuFrameBegin")))
extern "C" void wgpuFrameBegin();
__attribute__((import_module("env"), import_name("wgpuFrameEnd")))
extern "C" void wgpuFrameEnd();
__attribute__((import_module("env"), import_name("wgpuDispatchQuads")))
extern "C" void wgpuDispatchQuads(std::uint32_t itemsHandle, const void* headerPtr,
std::int32_t gx, std::int32_t gy);
__attribute__((import_module("env"), import_name("wgpuDispatchCircles")))
extern "C" void wgpuDispatchCircles(std::uint32_t itemsHandle, const void* headerPtr,
std::int32_t gx, std::int32_t gy);
__attribute__((import_module("env"), import_name("wgpuDispatchImages")))
extern "C" void wgpuDispatchImages(std::uint32_t itemsHandle, const void* headerPtr,
std::int32_t gx, std::int32_t gy,
std::uint32_t texHandle, std::uint32_t sampHandle);
__attribute__((import_module("env"), import_name("wgpuDispatchText")))
extern "C" void wgpuDispatchText(std::uint32_t itemsHandle, const void* headerPtr,
std::int32_t gx, std::int32_t gy,
std::uint32_t atlasHandle, std::uint32_t sampHandle);
}
#endif // CRAFTER_GRAPHICS_WINDOW_DOM

View file

@ -0,0 +1,85 @@
/*
Crafter®.Graphics
Copyright (C) 2026 Catcrafts®
catcrafts.net
*/
// WebGPU buffer wrapper — DOM-mode parallel to VulkanBuffer<T, Mapped>.
// Holds a JS-side GPUBuffer handle + (when Mapped) a wasm-memory staging
// array. `.value` points to the staging memory; the user writes into it
// directly, and `.Flush(cmd)` copies to the GPU via queue.writeBuffer.
export module Crafter.Graphics:WebGPUBuffer;
#ifdef CRAFTER_GRAPHICS_WINDOW_DOM
import std;
import :WebGPU;
export namespace Crafter {
class WebGPUBufferBase {
public:
WebGPUBufferRef handle = 0;
std::uint32_t size = 0; // bytes
};
template<typename T>
class WebGPUBufferMapped {
public:
T* value = nullptr;
};
class WebGPUBufferMappedEmpty {};
template<typename T, bool Mapped>
using WebGPUBufferMappedConditional =
std::conditional_t<Mapped, WebGPUBufferMapped<T>, WebGPUBufferMappedEmpty>;
template<typename T, bool Mapped>
class WebGPUBuffer : public WebGPUBufferBase, public WebGPUBufferMappedConditional<T, Mapped> {
public:
WebGPUBuffer() = default;
WebGPUBuffer(const WebGPUBuffer&) = delete;
WebGPUBuffer& operator=(const WebGPUBuffer&) = delete;
WebGPUBuffer(WebGPUBuffer&& other) noexcept {
handle = other.handle;
size = other.size;
other.handle = 0;
if constexpr (Mapped) {
this->value = other.value;
other.value = nullptr;
}
}
void Create(std::uint32_t count) {
size = static_cast<std::uint32_t>(count * sizeof(T));
handle = WebGPU::wgpuCreateBuffer(static_cast<std::int32_t>(size));
if constexpr (Mapped) {
this->value = new T[count]();
}
}
void Clear() {
if (handle != 0) {
WebGPU::wgpuDestroyBuffer(handle);
handle = 0;
}
if constexpr (Mapped) {
if (this->value) { delete[] this->value; this->value = nullptr; }
}
}
void Resize(std::uint32_t count) {
if (handle != 0) Clear();
Create(count);
}
void Flush(WebGPUCommandEncoderRef /*cmd*/) requires(Mapped) {
WebGPU::wgpuWriteBuffer(handle, this->value, static_cast<std::int32_t>(size));
}
void FlushDevice() requires(Mapped) {
WebGPU::wgpuWriteBuffer(handle, this->value, static_cast<std::int32_t>(size));
}
~WebGPUBuffer() { Clear(); }
};
}
#endif // CRAFTER_GRAPHICS_WINDOW_DOM

View file

@ -0,0 +1,25 @@
/*
Crafter®.Graphics
Copyright (C) 2026 Catcrafts®
catcrafts.net
*/
// Placeholder ComputeShader for DOM mode. The four standard UI pipelines
// are compiled JS-side at startup (see additional/dom-webgpu.js); the C++
// side never sees a WGSL handle. This type exists so UIRenderer can
// declare `WebGPUComputeShader drawQuads;` members for symmetry with the
// Vulkan side, but `Load()` and `Dispatch()` are intentionally absent —
// the DispatchQuads / DispatchCircles / etc convenience methods on
// UIRenderer route directly to the JS bridge.
export module Crafter.Graphics:WebGPUComputeShader;
#ifdef CRAFTER_GRAPHICS_WINDOW_DOM
import std;
export namespace Crafter {
struct WebGPUComputeShader {
// Marker only; pipelines live JS-side per dispatchStandard in
// dom-webgpu.js. No state required.
};
}
#endif // CRAFTER_GRAPHICS_WINDOW_DOM

View file

@ -50,6 +50,10 @@ import std;
import :Types;
import :Keys;
import Crafter.Event;
#ifdef CRAFTER_GRAPHICS_WINDOW_DOM
import :WebGPU;
import :DescriptorHeapWebGPU;
#endif
export namespace Crafter {
#ifndef CRAFTER_GRAPHICS_WINDOW_DOM
@ -61,6 +65,9 @@ export namespace Crafter {
};
struct RenderPass;
struct DescriptorHeapVulkan;
#else
struct RenderPass;
struct DescriptorHeapWebGPU;
#endif
struct Window {
@ -242,12 +249,21 @@ export namespace Crafter {
DescriptorHeapVulkan* descriptorHeap = nullptr;
std::optional<std::array<float, 4>> clearColor;
#else
// DOM mode: the page IS the window. `numFrames` stays as a public
// constant so cross-platform code can refer to Window::numFrames
// without #ifdef'ing the reference; nothing else lives here yet.
// V2 (WebGPU compute) will hang its GPUContext / swapchain texture
// members off this branch.
// DOM mode: the page IS the window. WebGPU device and canvas are
// owned JS-side (see additional/dom-webgpu.js); this struct just
// holds the per-Window state Crafter::Window users expect:
// a list of render passes and a pointer to the descriptor heap.
static constexpr std::uint8_t numFrames = 1;
std::uint32_t currentBuffer = 0;
std::vector<RenderPass*> passes;
DescriptorHeapWebGPU* descriptorHeap = nullptr;
std::optional<std::array<float, 4>> clearColor;
// DOM-mode StartInit/FinishInit are no-ops returning an opaque
// command-buffer marker so cross-platform user code (HelloUI's
// `auto init = window.StartInit();`) compiles unchanged.
WebGPUCommandEncoderRef StartInit();
void FinishInit();
#endif
};
}

View file

@ -34,6 +34,7 @@ export import :Input;
export import :Device;
export import :Animation;
export import :ForwardDeclarations;
export import :GraphicsTypes;
export import :Clipboard;
// Vulkan-backed partitions — empty under DOM.
@ -61,3 +62,7 @@ export import :Decompress;
export import :Dom;
export import :DomEvents;
export import :Router;
export import :WebGPU;
export import :WebGPUBuffer;
export import :DescriptorHeapWebGPU;
export import :WebGPUComputeShader;