webgpu support
This commit is contained in:
parent
5352ef69a2
commit
dedf6b0467
22 changed files with 1656 additions and 324 deletions
|
|
@ -2,47 +2,53 @@
|
|||
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
|
||||
*/
|
||||
module;
|
||||
#ifndef CRAFTER_GRAPHICS_WINDOW_DOM
|
||||
#include "vulkan/vulkan.h"
|
||||
#endif
|
||||
#include "../lib/stb_truetype.h"
|
||||
module Crafter.Graphics:FontAtlas_impl;
|
||||
import :FontAtlas;
|
||||
import :Font;
|
||||
import :GraphicsTypes;
|
||||
#ifndef CRAFTER_GRAPHICS_WINDOW_DOM
|
||||
import :ImageVulkan;
|
||||
import :Device;
|
||||
#else
|
||||
import :WebGPU;
|
||||
#endif
|
||||
import std;
|
||||
|
||||
using namespace Crafter;
|
||||
|
||||
void FontAtlas::Initialize(VkCommandBuffer cmd) {
|
||||
std::uint8_t* FontAtlas::PixelPtr() noexcept {
|
||||
#ifndef CRAFTER_GRAPHICS_WINDOW_DOM
|
||||
return image.buffer.value;
|
||||
#else
|
||||
return staging.data();
|
||||
#endif
|
||||
}
|
||||
|
||||
void FontAtlas::Initialize(GraphicsCommandBuffer cmd) {
|
||||
#ifndef CRAFTER_GRAPHICS_WINDOW_DOM
|
||||
image.Create(
|
||||
kAtlasSize, kAtlasSize, /*mipLevels*/ 1, cmd,
|
||||
VK_FORMAT_R8_UNORM,
|
||||
VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_SAMPLED_BIT,
|
||||
VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL
|
||||
);
|
||||
// Staging buffer is mapped; clear it so empty atlas regions sample as
|
||||
// distance < onedge (i.e. fully outside any glyph).
|
||||
std::memset(image.buffer.value, 0, kAtlasSize * kAtlasSize);
|
||||
dirty = true;
|
||||
#else
|
||||
(void)cmd;
|
||||
staging.assign(kAtlasSize * kAtlasSize, 0);
|
||||
textureHandle = WebGPU::wgpuCreateAtlasTexture(kAtlasSize, kAtlasSize);
|
||||
dirty = true;
|
||||
#endif
|
||||
}
|
||||
|
||||
bool FontAtlas::ShelfPlace(int w, int h, int& outX, int& outY) {
|
||||
// Try existing shelves first — same height heuristic keeps fragmentation low.
|
||||
for (Shelf& s : shelves_) {
|
||||
if (h <= s.height && s.cursorX + w <= kAtlasSize) {
|
||||
outX = s.cursorX;
|
||||
|
|
@ -51,7 +57,6 @@ bool FontAtlas::ShelfPlace(int w, int h, int& outX, int& outY) {
|
|||
return true;
|
||||
}
|
||||
}
|
||||
// New shelf below current ones.
|
||||
if (nextShelfY_ + h > kAtlasSize) return false;
|
||||
Shelf s{};
|
||||
s.y = nextShelfY_;
|
||||
|
|
@ -70,7 +75,6 @@ bool FontAtlas::Ensure(Font& font, std::uint32_t codepoint) {
|
|||
|
||||
float fontScale = stbtt_ScaleForPixelHeight(&font.font, kBaseSize);
|
||||
|
||||
// Advance is always present, even for empty glyphs (e.g. space).
|
||||
int advanceUnits = 0, lsb = 0;
|
||||
stbtt_GetCodepointHMetrics(&font.font, static_cast<int>(codepoint), &advanceUnits, &lsb);
|
||||
|
||||
|
|
@ -90,12 +94,12 @@ bool FontAtlas::Ensure(Font& font, std::uint32_t codepoint) {
|
|||
int px = 0, py = 0;
|
||||
if (!ShelfPlace(sw, sh, px, py)) {
|
||||
stbtt_FreeSDF(sdf, nullptr);
|
||||
return false; // V1: silently drop overflow; V2: grow atlas
|
||||
return false;
|
||||
}
|
||||
// Blit row-by-row into the mapped staging buffer.
|
||||
std::uint8_t* dst = PixelPtr();
|
||||
for (int row = 0; row < sh; ++row) {
|
||||
std::memcpy(
|
||||
image.buffer.value + (py + row) * kAtlasSize + px,
|
||||
dst + (py + row) * kAtlasSize + px,
|
||||
sdf + row * sw,
|
||||
static_cast<std::size_t>(sw)
|
||||
);
|
||||
|
|
@ -110,8 +114,6 @@ bool FontAtlas::Ensure(Font& font, std::uint32_t codepoint) {
|
|||
g.v1 = static_cast<float>(py + sh) / kAtlasSize;
|
||||
dirty = true;
|
||||
}
|
||||
// For empty glyphs (whitespace) we still cache the entry — the size-0
|
||||
// fields tell the emitter to skip the quad but advance the cursor.
|
||||
|
||||
cache_.emplace(key, g);
|
||||
return true;
|
||||
|
|
@ -122,8 +124,18 @@ const Glyph* FontAtlas::Lookup(Font& font, std::uint32_t codepoint) const {
|
|||
return it == cache_.end() ? nullptr : &it->second;
|
||||
}
|
||||
|
||||
void FontAtlas::Update(VkCommandBuffer cmd) {
|
||||
void FontAtlas::Update(GraphicsCommandBuffer cmd) {
|
||||
if (!dirty) return;
|
||||
#ifndef CRAFTER_GRAPHICS_WINDOW_DOM
|
||||
image.Update(cmd, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL);
|
||||
#else
|
||||
(void)cmd;
|
||||
// Full-atlas upload. Future: track dirty region.
|
||||
WebGPU::wgpuWriteAtlasRegion(
|
||||
textureHandle, staging.data(),
|
||||
kAtlasSize, kAtlasSize, kAtlasSize,
|
||||
0, 0, kAtlasSize, kAtlasSize
|
||||
);
|
||||
#endif
|
||||
dirty = false;
|
||||
}
|
||||
|
|
|
|||
86
implementations/Crafter.Graphics-UI-Shared.cpp
Normal file
86
implementations/Crafter.Graphics-UI-Shared.cpp
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
/*
|
||||
Crafter®.Graphics
|
||||
Copyright (C) 2026 Catcrafts®
|
||||
catcrafts.net
|
||||
*/
|
||||
|
||||
// Backend-agnostic UIRenderer methods. FillHeader and ShapeText do not
|
||||
// touch any GPU API; they only read window dimensions and (for ShapeText)
|
||||
// the CPU-side font atlas. Split out so both Vulkan and WebGPU impls
|
||||
// share the same source.
|
||||
|
||||
module Crafter.Graphics:UI_shared_impl;
|
||||
import :UI;
|
||||
import :Window;
|
||||
import :Font;
|
||||
import :FontAtlas;
|
||||
import :GraphicsTypes;
|
||||
import std;
|
||||
|
||||
using namespace Crafter;
|
||||
|
||||
UIDispatchHeader UIRenderer::FillHeader(std::uint32_t itemBufferSlot,
|
||||
std::uint32_t itemCount,
|
||||
std::array<float,4> clipRectPx,
|
||||
std::uint32_t flags) const noexcept {
|
||||
UIDispatchHeader h{};
|
||||
h.outImage = outImageSlot_;
|
||||
h.itemBuffer = itemBufferSlot;
|
||||
h.surfaceWidth = window_->width;
|
||||
h.surfaceHeight = window_->height;
|
||||
h.clipX = clipRectPx[0];
|
||||
h.clipY = clipRectPx[1];
|
||||
h.clipW = clipRectPx[2];
|
||||
h.clipH = clipRectPx[3];
|
||||
h.itemCount = itemCount;
|
||||
#ifndef CRAFTER_GRAPHICS_WINDOW_DOM
|
||||
h.frameIdx = window_->currentBuffer;
|
||||
#else
|
||||
h.frameIdx = 0;
|
||||
#endif
|
||||
h.flags = flags;
|
||||
h._pad = 0;
|
||||
return h;
|
||||
}
|
||||
|
||||
std::uint32_t UIRenderer::ShapeText(Font& font, float pxSize,
|
||||
float x, float baselineY,
|
||||
std::string_view utf8,
|
||||
std::array<float,4> color,
|
||||
GlyphItem* out, std::uint32_t outCapacity,
|
||||
float* outAdvance) {
|
||||
if (fontAtlas == nullptr) {
|
||||
std::println("UIRenderer::ShapeText: no FontAtlas (set fontAtlas before Initialize)");
|
||||
std::abort();
|
||||
}
|
||||
|
||||
const float scale = pxSize / FontAtlas::kBaseSize;
|
||||
float cursor = x;
|
||||
std::uint32_t written = 0;
|
||||
|
||||
std::size_t i = 0;
|
||||
while (i < utf8.size() && written < outCapacity) {
|
||||
std::uint32_t cp = DecodeUtf8(utf8, i);
|
||||
if (cp == 0) break;
|
||||
if (cp == '\n') { continue; }
|
||||
|
||||
fontAtlas->Ensure(font, cp);
|
||||
const Glyph* g = fontAtlas->Lookup(font, cp);
|
||||
if (g == nullptr) continue;
|
||||
|
||||
if (g->w > 0 && g->h > 0) {
|
||||
GlyphItem& gi = out[written++];
|
||||
gi.x = cursor + g->xoff * scale;
|
||||
gi.y = baselineY + g->yoff * scale;
|
||||
gi.w = g->w * scale;
|
||||
gi.h = g->h * scale;
|
||||
gi.u0 = g->u0; gi.v0 = g->v0;
|
||||
gi.u1 = g->u1; gi.v1 = g->v1;
|
||||
gi.r = color[0]; gi.g = color[1]; gi.b = color[2]; gi.a = color[3];
|
||||
}
|
||||
cursor += g->advance * scale;
|
||||
}
|
||||
|
||||
if (outAdvance) *outAdvance = cursor - x;
|
||||
return written;
|
||||
}
|
||||
140
implementations/Crafter.Graphics-UI-WebGPU.cpp
Normal file
140
implementations/Crafter.Graphics-UI-WebGPU.cpp
Normal file
|
|
@ -0,0 +1,140 @@
|
|||
/*
|
||||
Crafter®.Graphics
|
||||
Copyright (C) 2026 Catcrafts®
|
||||
catcrafts.net
|
||||
*/
|
||||
|
||||
// WebGPU UIRenderer implementation — DOM mode parallel to
|
||||
// Crafter.Graphics-UI.cpp. Compute pipelines and bind groups live JS-side
|
||||
// in additional/dom-webgpu.js; this file just translates UIRenderer's
|
||||
// public method calls into wgpu* import calls.
|
||||
|
||||
module Crafter.Graphics:UI_webgpu_impl;
|
||||
import :UI;
|
||||
import :Window;
|
||||
import :Font;
|
||||
import :FontAtlas;
|
||||
import :WebGPU;
|
||||
import :WebGPUBuffer;
|
||||
import :DescriptorHeapWebGPU;
|
||||
import :GraphicsTypes;
|
||||
import std;
|
||||
|
||||
using namespace Crafter;
|
||||
|
||||
void UIRenderer::Initialize(Window& window, GraphicsDescriptorHeap& heap, GraphicsCommandBuffer initCmd,
|
||||
std::filesystem::path /*quadsSpv*/,
|
||||
std::filesystem::path /*circlesSpv*/,
|
||||
std::filesystem::path /*imagesSpv*/,
|
||||
std::filesystem::path /*textSpv*/) {
|
||||
(void)initCmd;
|
||||
window_ = &window;
|
||||
heap_ = &heap;
|
||||
|
||||
// The JS bridge owns the compute pipelines (4 of them, precompiled at
|
||||
// page load). The C++ side has nothing to load. We do reserve one
|
||||
// image slot for the swapchain output (kept symmetrical with Vulkan)
|
||||
// and register the font atlas if one was set.
|
||||
auto outRange = heap_->AllocateImageSlots(1);
|
||||
outImageSlot_ = ImageSlot{heap_, outRange.firstElement};
|
||||
// No JS texture handle stored at this slot — the canvas's
|
||||
// getCurrentTexture() is per-frame and the JS bridge resolves it
|
||||
// internally. The slot value is only meaningful through
|
||||
// UIDispatchHeader.outImage, which the WGSL shaders ignore.
|
||||
|
||||
if (fontAtlas != nullptr) {
|
||||
auto atlasImg = heap_->AllocateImageSlots(1);
|
||||
fontAtlasImageSlot_ = ImageSlot{heap_, atlasImg.firstElement};
|
||||
heap_->imageTable[atlasImg.firstElement] = fontAtlas->textureHandle;
|
||||
fontAtlasSamplerSlot_ = RegisterLinearClampSampler();
|
||||
}
|
||||
|
||||
resizeSub_.SetEvent(&window.onResize, [this]() {
|
||||
// Storage textures + bind groups are recreated JS-side at next
|
||||
// wgpuFrameBegin (via the ensureSized path). Nothing to do here.
|
||||
});
|
||||
}
|
||||
|
||||
void UIRenderer::Record(GraphicsCommandBuffer cmd, std::uint32_t frameIdx, Window& window) {
|
||||
if (fontAtlas != nullptr && fontAtlas->dirty) {
|
||||
fontAtlas->Update(cmd);
|
||||
}
|
||||
onBuild.Invoke({cmd, frameIdx});
|
||||
if (fontAtlas != nullptr && fontAtlas->dirty) {
|
||||
fontAtlas->Update(cmd);
|
||||
}
|
||||
(void)window;
|
||||
}
|
||||
|
||||
namespace {
|
||||
inline std::uint32_t TilesFor(std::uint32_t dim) { return (dim + 7u) / 8u; }
|
||||
}
|
||||
|
||||
void UIRenderer::DispatchQuads(GraphicsCommandBuffer /*cmd*/, std::uint32_t bufferSlot,
|
||||
std::uint32_t itemCount,
|
||||
std::array<float,4> clipRectPx) {
|
||||
if (itemCount == 0) return;
|
||||
UIDispatchHeader hdr = FillHeader(bufferSlot, itemCount, clipRectPx);
|
||||
auto handle = heap_->bufferTable[bufferSlot];
|
||||
WebGPU::wgpuDispatchQuads(handle, &hdr,
|
||||
static_cast<std::int32_t>(TilesFor(window_->width)),
|
||||
static_cast<std::int32_t>(TilesFor(window_->height)));
|
||||
}
|
||||
|
||||
void UIRenderer::DispatchCircles(GraphicsCommandBuffer /*cmd*/, std::uint32_t bufferSlot,
|
||||
std::uint32_t itemCount,
|
||||
std::array<float,4> clipRectPx) {
|
||||
if (itemCount == 0) return;
|
||||
UIDispatchHeader hdr = FillHeader(bufferSlot, itemCount, clipRectPx);
|
||||
auto handle = heap_->bufferTable[bufferSlot];
|
||||
WebGPU::wgpuDispatchCircles(handle, &hdr,
|
||||
static_cast<std::int32_t>(TilesFor(window_->width)),
|
||||
static_cast<std::int32_t>(TilesFor(window_->height)));
|
||||
}
|
||||
|
||||
void UIRenderer::DispatchImages(GraphicsCommandBuffer /*cmd*/, std::uint32_t bufferSlot,
|
||||
std::uint32_t itemCount,
|
||||
std::array<float,4> clipRectPx) {
|
||||
if (itemCount == 0) return;
|
||||
UIDispatchHeader hdr = FillHeader(bufferSlot, itemCount, clipRectPx);
|
||||
auto handle = heap_->bufferTable[bufferSlot];
|
||||
// For DispatchImages, the WGSL expects a texture + sampler in group 3.
|
||||
// The library v1 doesn't expose user-image registration on DOM (out of
|
||||
// scope per plan). If the user calls DispatchImages without a registered
|
||||
// image, fall back to using the font atlas binding — the user's items
|
||||
// should reference texSlot/sampSlot but on DOM those are ignored. For
|
||||
// now, route through the font atlas texture if available; otherwise
|
||||
// skip the dispatch.
|
||||
if (fontAtlasImageSlot_) {
|
||||
auto texHandle = heap_->imageTable[fontAtlasImageSlot_];
|
||||
auto sampHandle = heap_->samplerTable[fontAtlasSamplerSlot_];
|
||||
WebGPU::wgpuDispatchImages(handle, &hdr,
|
||||
static_cast<std::int32_t>(TilesFor(window_->width)),
|
||||
static_cast<std::int32_t>(TilesFor(window_->height)),
|
||||
texHandle, sampHandle);
|
||||
}
|
||||
}
|
||||
|
||||
void UIRenderer::DispatchText(GraphicsCommandBuffer /*cmd*/, std::uint32_t bufferSlot,
|
||||
std::uint32_t itemCount,
|
||||
std::array<float,4> clipRectPx) {
|
||||
if (itemCount == 0) return;
|
||||
if (!fontAtlasImageSlot_) {
|
||||
std::println("UIRenderer::DispatchText: no FontAtlas registered (set fontAtlas before Initialize)");
|
||||
std::abort();
|
||||
}
|
||||
UIDispatchHeader hdr = FillHeader(bufferSlot, itemCount, clipRectPx);
|
||||
auto bufHandle = heap_->bufferTable[bufferSlot];
|
||||
auto texHandle = heap_->imageTable[fontAtlasImageSlot_];
|
||||
auto sampHandle = heap_->samplerTable[fontAtlasSamplerSlot_];
|
||||
WebGPU::wgpuDispatchText(bufHandle, &hdr,
|
||||
static_cast<std::int32_t>(TilesFor(window_->width)),
|
||||
static_cast<std::int32_t>(TilesFor(window_->height)),
|
||||
texHandle, sampHandle);
|
||||
}
|
||||
|
||||
SamplerSlot UIRenderer::RegisterLinearClampSampler() {
|
||||
auto range = heap_->AllocateSamplerSlots(1);
|
||||
heap_->samplerTable[range.firstElement] = WebGPU::wgpuCreateLinearClampSampler();
|
||||
return SamplerSlot{heap_, range.firstElement};
|
||||
}
|
||||
|
|
@ -28,13 +28,14 @@ import :ImageVulkan;
|
|||
import :VulkanBuffer;
|
||||
import :FontAtlas;
|
||||
import :Font;
|
||||
import :GraphicsTypes;
|
||||
import std;
|
||||
|
||||
using namespace Crafter;
|
||||
|
||||
// ─── Initialize ─────────────────────────────────────────────────────────
|
||||
|
||||
void UIRenderer::Initialize(Window& window, DescriptorHeapVulkan& heap, VkCommandBuffer initCmd,
|
||||
void UIRenderer::Initialize(Window& window, GraphicsDescriptorHeap& heap, GraphicsCommandBuffer initCmd,
|
||||
std::filesystem::path quadsSpv,
|
||||
std::filesystem::path circlesSpv,
|
||||
std::filesystem::path imagesSpv,
|
||||
|
|
@ -81,7 +82,7 @@ void UIRenderer::Initialize(Window& window, DescriptorHeapVulkan& heap, VkComman
|
|||
|
||||
// ─── per-frame Record ───────────────────────────────────────────────────
|
||||
|
||||
void UIRenderer::Record(VkCommandBuffer cmd, std::uint32_t frameIdx, Window& window) {
|
||||
void UIRenderer::Record(GraphicsCommandBuffer cmd, std::uint32_t frameIdx, Window& window) {
|
||||
// Reset per-frame state.
|
||||
firstDispatchThisFrame_ = true;
|
||||
|
||||
|
|
@ -102,28 +103,6 @@ void UIRenderer::Record(VkCommandBuffer cmd, std::uint32_t frameIdx, Window& win
|
|||
(void)window;
|
||||
}
|
||||
|
||||
// ─── header builder ─────────────────────────────────────────────────────
|
||||
|
||||
UIDispatchHeader UIRenderer::FillHeader(std::uint32_t itemBufferSlot,
|
||||
std::uint32_t itemCount,
|
||||
std::array<float,4> clipRectPx,
|
||||
std::uint32_t flags) const noexcept {
|
||||
UIDispatchHeader h{};
|
||||
h.outImage = outImageSlot_;
|
||||
h.itemBuffer = itemBufferSlot;
|
||||
h.surfaceWidth = window_->width;
|
||||
h.surfaceHeight = window_->height;
|
||||
h.clipX = clipRectPx[0];
|
||||
h.clipY = clipRectPx[1];
|
||||
h.clipW = clipRectPx[2];
|
||||
h.clipH = clipRectPx[3];
|
||||
h.itemCount = itemCount;
|
||||
h.frameIdx = window_->currentBuffer;
|
||||
h.flags = flags;
|
||||
h._pad = 0;
|
||||
return h;
|
||||
}
|
||||
|
||||
// ─── group-count helper ────────────────────────────────────────────────
|
||||
|
||||
namespace {
|
||||
|
|
@ -142,7 +121,7 @@ namespace {
|
|||
// without inter-workgroup races on imageLoad/imageStore — the bug that the
|
||||
// per-item dispatch model had.
|
||||
|
||||
void UIRenderer::DispatchQuads(VkCommandBuffer cmd, std::uint32_t bufferSlot,
|
||||
void UIRenderer::DispatchQuads(GraphicsCommandBuffer cmd, std::uint32_t bufferSlot,
|
||||
std::uint32_t itemCount,
|
||||
std::array<float,4> clipRectPx) {
|
||||
if (itemCount == 0) return;
|
||||
|
|
@ -151,7 +130,7 @@ void UIRenderer::DispatchQuads(VkCommandBuffer cmd, std::uint32_t bufferSlot,
|
|||
TilesFor(window_->width), TilesFor(window_->height), 1u);
|
||||
}
|
||||
|
||||
void UIRenderer::DispatchCircles(VkCommandBuffer cmd, std::uint32_t bufferSlot,
|
||||
void UIRenderer::DispatchCircles(GraphicsCommandBuffer cmd, std::uint32_t bufferSlot,
|
||||
std::uint32_t itemCount,
|
||||
std::array<float,4> clipRectPx) {
|
||||
if (itemCount == 0) return;
|
||||
|
|
@ -160,7 +139,7 @@ void UIRenderer::DispatchCircles(VkCommandBuffer cmd, std::uint32_t bufferSlot,
|
|||
TilesFor(window_->width), TilesFor(window_->height), 1u);
|
||||
}
|
||||
|
||||
void UIRenderer::DispatchImages(VkCommandBuffer cmd, std::uint32_t bufferSlot,
|
||||
void UIRenderer::DispatchImages(GraphicsCommandBuffer cmd, std::uint32_t bufferSlot,
|
||||
std::uint32_t itemCount,
|
||||
std::array<float,4> clipRectPx) {
|
||||
if (itemCount == 0) return;
|
||||
|
|
@ -169,7 +148,7 @@ void UIRenderer::DispatchImages(VkCommandBuffer cmd, std::uint32_t bufferSlot,
|
|||
TilesFor(window_->width), TilesFor(window_->height), 1u);
|
||||
}
|
||||
|
||||
void UIRenderer::DispatchText(VkCommandBuffer cmd, std::uint32_t bufferSlot,
|
||||
void UIRenderer::DispatchText(GraphicsCommandBuffer cmd, std::uint32_t bufferSlot,
|
||||
std::uint32_t itemCount,
|
||||
std::array<float,4> clipRectPx) {
|
||||
if (itemCount == 0) return;
|
||||
|
|
@ -199,7 +178,7 @@ void UIRenderer::DispatchText(VkCommandBuffer cmd, std::uint32_t bufferSlot,
|
|||
|
||||
// ─── generic Dispatch (with barrier) ────────────────────────────────────
|
||||
|
||||
void UIRenderer::Dispatch(VkCommandBuffer cmd, const ComputeShader& shader,
|
||||
void UIRenderer::Dispatch(GraphicsCommandBuffer cmd, const ComputeShader& shader,
|
||||
const void* push, std::uint32_t pushBytes,
|
||||
std::uint32_t gx, std::uint32_t gy, std::uint32_t gz) {
|
||||
if (!firstDispatchThisFrame_) {
|
||||
|
|
@ -356,46 +335,3 @@ SamplerSlot UIRenderer::RegisterLinearClampSampler() {
|
|||
return RegisterSampler(s);
|
||||
}
|
||||
|
||||
// ─── ShapeText ─────────────────────────────────────────────────────────
|
||||
|
||||
std::uint32_t UIRenderer::ShapeText(Font& font, float pxSize,
|
||||
float x, float baselineY,
|
||||
std::string_view utf8,
|
||||
std::array<float,4> color,
|
||||
GlyphItem* out, std::uint32_t outCapacity,
|
||||
float* outAdvance) {
|
||||
if (fontAtlas == nullptr) {
|
||||
throw std::runtime_error("UIRenderer::ShapeText: no FontAtlas (set fontAtlas before Initialize)");
|
||||
}
|
||||
|
||||
const float scale = pxSize / FontAtlas::kBaseSize;
|
||||
float cursor = x;
|
||||
std::uint32_t written = 0;
|
||||
|
||||
std::size_t i = 0;
|
||||
while (i < utf8.size() && written < outCapacity) {
|
||||
std::uint32_t cp = DecodeUtf8(utf8, i);
|
||||
if (cp == 0) break;
|
||||
if (cp == '\n') { /* single-line shaper — ignore */ continue; }
|
||||
|
||||
fontAtlas->Ensure(font, cp);
|
||||
const Glyph* g = fontAtlas->Lookup(font, cp);
|
||||
if (g == nullptr) continue;
|
||||
|
||||
// Empty glyph (whitespace) — advance only.
|
||||
if (g->w > 0 && g->h > 0) {
|
||||
GlyphItem& gi = out[written++];
|
||||
gi.x = cursor + g->xoff * scale;
|
||||
gi.y = baselineY + g->yoff * scale;
|
||||
gi.w = g->w * scale;
|
||||
gi.h = g->h * scale;
|
||||
gi.u0 = g->u0; gi.v0 = g->v0;
|
||||
gi.u1 = g->u1; gi.v1 = g->v1;
|
||||
gi.r = color[0]; gi.g = color[1]; gi.b = color[2]; gi.a = color[3];
|
||||
}
|
||||
cursor += g->advance * scale;
|
||||
}
|
||||
|
||||
if (outAdvance) *outAdvance = cursor - x;
|
||||
return written;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -69,6 +69,10 @@ import :Gamepad;
|
|||
import :VulkanTransition;
|
||||
import :DescriptorHeapVulkan;
|
||||
import :RenderPass;
|
||||
#ifdef CRAFTER_GRAPHICS_WINDOW_DOM
|
||||
import :WebGPU;
|
||||
import :DescriptorHeapWebGPU;
|
||||
#endif
|
||||
import std;
|
||||
|
||||
using namespace Crafter;
|
||||
|
|
@ -1323,23 +1327,22 @@ Window::Window(std::uint32_t w, std::uint32_t h, const std::string_view title)
|
|||
|
||||
Window::Window(std::uint32_t w, std::uint32_t h) : width(w), height(h) {
|
||||
if (g_domWindow != nullptr) {
|
||||
// Only one Window per page in V1. Subsequent constructions are
|
||||
// a programming error — log loudly and clobber the previous
|
||||
// pointer so the new Window's events at least fire.
|
||||
// (stderr isn't reachable via `import std;` on wasi-sdk yet; just log
|
||||
// to cout. The browser console pipes both to the same place.)
|
||||
std::println("Crafter::Window: only one DOM Window per page; "
|
||||
"overwriting the previous instance.");
|
||||
}
|
||||
g_domWindow = this;
|
||||
|
||||
// Use the browser-reported viewport size as the initial dimensions
|
||||
// unless the caller asked for something specific. Browser owns the
|
||||
// real size; w/h passed in are advisory.
|
||||
if (w == 0 || h == 0) {
|
||||
width = static_cast<std::uint32_t>(Crafter::DomEnv::domGetInnerWidth());
|
||||
height = static_cast<std::uint32_t>(Crafter::DomEnv::domGetInnerHeight());
|
||||
}
|
||||
// Browser owns the real surface size. The width/height passed in are
|
||||
// advisory only — useful as a native-side hint, ignored on DOM. We
|
||||
// always sync to innerWidth/innerHeight so:
|
||||
// - window.width/.height match the canvas's CSS pixel size,
|
||||
// - MouseEvent.clientX/.clientY (CSS pixels) compare correctly
|
||||
// against any layout done with window.width/.height,
|
||||
// - the dispatch group count from window.width/8 covers the
|
||||
// canvas exactly.
|
||||
(void)w; (void)h;
|
||||
width = static_cast<std::uint32_t>(Crafter::DomEnv::domGetInnerWidth());
|
||||
height = static_cast<std::uint32_t>(Crafter::DomEnv::domGetInnerHeight());
|
||||
|
||||
// The handle passed to attach is just a non-zero token the JS side
|
||||
// includes back in every dispatcher call. We don't use it on the
|
||||
|
|
@ -1394,10 +1397,18 @@ void Window::SetDefaultCursor() {
|
|||
}
|
||||
|
||||
void Window::StartSync() {
|
||||
// Hand the loop to rAF. Returns immediately; the wasm `_start`
|
||||
// (main) finishes, and the runtime keeps the module alive while
|
||||
// the JS-side rAF chain ticks `__crafterDom_frame`.
|
||||
// Hand the loop to rAF, then exit the wasm via _Exit so wasi-libc
|
||||
// skips __wasm_call_dtors. If we let main return normally, _start
|
||||
// calls __wasm_call_dtors → static destructors fire (including
|
||||
// Window's own), then __wasi_proc_exit → wasm trap. Subsequent rAF
|
||||
// calls into the wasm would then trap too, killing rendering.
|
||||
// _Exit jumps straight to __wasi_proc_exit, which our runtime.js
|
||||
// catches via a thrown sentinel so the instance stays alive while
|
||||
// every static-allocated object (Window, UIRenderer, GPU buffers,
|
||||
// event listeners) remains untouched. Callers' code after
|
||||
// StartSync() never runs — match that contract on native too.
|
||||
Crafter::DomEnv::domStartFrameLoop();
|
||||
std::_Exit(0);
|
||||
}
|
||||
|
||||
void Window::StartUpdate() {
|
||||
|
|
@ -1421,10 +1432,25 @@ void Window::Update() {
|
|||
}
|
||||
|
||||
void Window::Render() {
|
||||
// V1: no rendering in DOM mode. Kept as a callable no-op so
|
||||
// existing cross-platform code paths (e.g. main loops calling
|
||||
// window.Render() before window.StartSync()) compile. V2 will
|
||||
// hang the WebGPU command-submit here.
|
||||
if (!open) return;
|
||||
Crafter::WebGPU::wgpuFrameBegin();
|
||||
for (RenderPass* p : passes) {
|
||||
if (p) p->Record(/*cmd*/ 0u, currentBuffer, *this);
|
||||
}
|
||||
Crafter::WebGPU::wgpuFrameEnd();
|
||||
}
|
||||
|
||||
WebGPUCommandEncoderRef Window::StartInit() {
|
||||
// DOM init: no command buffer needed — texture / buffer creation goes
|
||||
// through synchronous wgpu* imports. Return 0 as a placeholder; the
|
||||
// value is opaque to user code (auto-typed in HelloUI).
|
||||
Crafter::WebGPU::wgpuInit();
|
||||
return 0;
|
||||
}
|
||||
|
||||
void Window::FinishInit() {
|
||||
// Nothing to submit in DOM mode; all init writes are queued at call
|
||||
// time via queue.writeBuffer / writeTexture.
|
||||
}
|
||||
|
||||
// ─── C exports the JS bridge calls back into ──────────────────────────
|
||||
|
|
@ -1437,6 +1463,7 @@ extern "C" {
|
|||
g_domWindow->onBeforeUpdate.Invoke();
|
||||
if (g_domWindow->updating) {
|
||||
g_domWindow->Update();
|
||||
g_domWindow->Render();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue