2026-07-22 18:09:06 +02:00
|
|
|
//SPDX-License-Identifier: LGPL-3.0-only
|
|
|
|
|
//SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
|
|
|
|
|
|
2026-05-18 04:58:52 +02:00
|
|
|
/*
|
|
|
|
|
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));
|
|
|
|
|
}
|
2026-05-24 13:32:08 +02:00
|
|
|
// Partial upload — write the bytes [srcByteOffset, srcByteOffset+byteCount)
|
|
|
|
|
// of the host mirror to GPU offset `dstByteOffset`. BuildTLAS uses
|
|
|
|
|
// this to leave the GPU-owned transform field of an RTInstance
|
|
|
|
|
// intact (the physics-tlas-transform compute shader is its sole
|
|
|
|
|
// writer) while still pushing the CPU-side metadata fields.
|
|
|
|
|
void FlushDeviceRange(std::uint32_t dstByteOffset,
|
|
|
|
|
std::uint32_t srcByteOffset,
|
|
|
|
|
std::uint32_t byteCount) requires(Mapped) {
|
|
|
|
|
const auto* base = reinterpret_cast<const char*>(this->value);
|
|
|
|
|
WebGPU::wgpuWriteBufferRange(handle, dstByteOffset,
|
|
|
|
|
base + srcByteOffset,
|
|
|
|
|
static_cast<std::int32_t>(byteCount));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Push one element's worth of bytes from the host mirror to GPU.
|
|
|
|
|
// Use when a single SoA slot was mutated (body construction,
|
|
|
|
|
// per-instance flag flip) and a full FlushDevice would clobber
|
|
|
|
|
// the GPU-side updates the sim has applied to neighboring slots.
|
|
|
|
|
void FlushDeviceSlot(std::uint32_t idx) requires(Mapped) {
|
|
|
|
|
constexpr std::uint32_t kStride = sizeof(T);
|
|
|
|
|
const std::uint32_t off = idx * kStride;
|
|
|
|
|
FlushDeviceRange(off, off, kStride);
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-18 14:00:36 +00:00
|
|
|
// Schedule a GPU→CPU readback of this buffer. Asynchronous; data
|
|
|
|
|
// isn't ready until a later PollReadback returns true. Successive
|
|
|
|
|
// Enqueues without a Poll are dropped — they're a no-op while the
|
|
|
|
|
// previous map is in flight.
|
|
|
|
|
//
|
|
|
|
|
// `byteCount` (0 = the whole buffer) bounds the readback to the
|
|
|
|
|
// live prefix: an over-provisioned event-queue buffer need only
|
|
|
|
|
// copy its header / used span GPU→staging→wasm, not the full
|
|
|
|
|
// capacity, every drain. Clamped to `size`. The staging buffer is
|
|
|
|
|
// sized to the full capacity, so the prefix length may vary from
|
|
|
|
|
// one Enqueue to the next; pass the SAME byteCount to the matching
|
|
|
|
|
// PollReadback so the right number of bytes lands in `.value`.
|
2026-05-24 13:32:08 +02:00
|
|
|
//
|
|
|
|
|
// `resetBytes` ≥ 0 — if non-zero, the first `resetBytes` bytes
|
|
|
|
|
// of THIS buffer are clearBuffer-cleared on the GPU command
|
|
|
|
|
// encoder immediately after the copy, so the readback captures
|
|
|
|
|
// the pre-clear bytes and the next frame's writers see zeros.
|
|
|
|
|
// The reset is tied to a successful enqueue (skipped enqueue =
|
|
|
|
|
// skipped reset), preserving accumulated state across missed
|
|
|
|
|
// drains.
|
2026-06-18 14:00:36 +00:00
|
|
|
void EnqueueReadback(std::uint32_t resetBytes = 0, std::uint32_t byteCount = 0) {
|
|
|
|
|
const std::uint32_t n = (byteCount == 0 || byteCount > size) ? size : byteCount;
|
2026-05-24 13:32:08 +02:00
|
|
|
WebGPU::wgpuReadbackEnqueue(handle,
|
2026-06-18 14:00:36 +00:00
|
|
|
static_cast<std::int32_t>(n),
|
2026-05-24 13:32:08 +02:00
|
|
|
static_cast<std::int32_t>(resetBytes));
|
|
|
|
|
}
|
|
|
|
|
// Try to copy the readback bytes into this->value. Returns true
|
|
|
|
|
// if the previous EnqueueReadback resolved and the data is now
|
|
|
|
|
// mirrored into .value; false if the map is still pending.
|
2026-06-18 14:00:36 +00:00
|
|
|
// `byteCount` must match the value passed to the paired
|
|
|
|
|
// EnqueueReadback (0 = whole buffer); it bounds the staging→wasm
|
|
|
|
|
// copy to the live prefix that was captured.
|
|
|
|
|
bool PollReadback(std::uint32_t byteCount = 0) requires(Mapped) {
|
|
|
|
|
const std::uint32_t n = (byteCount == 0 || byteCount > size) ? size : byteCount;
|
2026-05-24 13:32:08 +02:00
|
|
|
return WebGPU::wgpuReadbackPoll(handle, this->value,
|
2026-06-18 14:00:36 +00:00
|
|
|
static_cast<std::int32_t>(n)) != 0;
|
2026-05-24 13:32:08 +02:00
|
|
|
}
|
|
|
|
|
// Non-consuming readiness probe. Returns true if a subsequent
|
|
|
|
|
// PollReadback would succeed without changing state otherwise.
|
|
|
|
|
// Use to verify a sibling buffer is also ready before consuming.
|
|
|
|
|
bool IsReadbackReady() const {
|
|
|
|
|
return WebGPU::wgpuReadbackReady(handle) != 0;
|
|
|
|
|
}
|
2026-05-18 04:58:52 +02:00
|
|
|
|
|
|
|
|
~WebGPUBuffer() { Clear(); }
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
#endif // CRAFTER_GRAPHICS_WINDOW_DOM
|