tess 1.0.0
Performance-first tile and path simulation substrate
Loading...
Searching...
No Matches
webgpu_backend.h
1#pragma once
2
3// Optional WebGPU C backend. The consumer supplies and includes the stable
4// <webgpu/webgpu.h> API before this header and links Dawn/Emdawnwebgpu (or a
5// compatible implementation). CPU-only builds never parse or link this code.
6
7#if defined(TESS_ENABLE_WEBGPU)
8
9#ifndef WEBGPU_H_
10#error "tess/gpu/webgpu_backend.h requires <webgpu/webgpu.h> first"
11#endif
12
13#include <tess/gpu/backend.h>
14
15#include <atomic>
16#include <cstddef>
17#include <cstdint>
18#include <limits>
19#include <memory>
20#include <new>
21#include <optional>
22#include <type_traits>
23#include <vector>
24
25namespace tess::gpu {
26
29 std::uint64_t max_buffer_bytes = std::uint64_t{256} * 1024u * 1024u;
30 std::uint64_t max_dispatch_chunks = 65535;
31 // WebGPU guarantees at least 65,535 workgroups in one dispatch dimension.
32 // Supply a lower adapter/device limit here when one is imposed externally.
33 std::uint32_t max_dispatch_workgroups_x = 65535;
34 std::uint64_t max_inflight_readback_bytes = std::uint64_t{4} * 1024u * 1024u;
35 std::size_t field_capacity = 16;
36 std::size_t product_capacity = 64;
37 bool allow_full_field_readback = false;
38};
39
41enum class WebGpuReadbackStatus : std::uint8_t {
42 Complete,
43 Failed,
44};
45
55using WebGpuReadbackCallback = void (*)(GpuProductHandle, WebGpuReadbackStatus,
56 const void*, std::size_t,
57 void*) noexcept;
58
70 std::uint64_t product_key = 0;
71 std::uint32_t input_field_index = 0;
72 WGPUComputePipeline pipeline = nullptr;
73 WGPUBindGroup bind_group = nullptr;
74 WGPUBuffer readback_source = nullptr;
75 std::uint64_t readback_source_offset = 0;
76 std::uint64_t readback_byte_size = 0;
77 WebGpuReadbackCallback readback_callback = nullptr;
78 void* readback_userdata = nullptr;
79};
80
81namespace detail {
82
83struct WebGpuSharedState {
84 std::atomic<bool> available{true};
85 std::atomic<std::uint64_t> inflight_readback_bytes{0};
86 std::uint64_t max_inflight_readback_bytes = 0;
87};
88
89struct WebGpuReadbackOperation {
90 WGPUBuffer staging = nullptr;
91 GpuProductHandle handle{};
92 std::size_t byte_size = 0;
93 WebGpuReadbackCallback callback = nullptr;
94 void* userdata = nullptr;
95 std::shared_ptr<WebGpuSharedState> shared;
96};
97
98template <typename Size>
99[[nodiscard]] constexpr bool fits_size(std::uint64_t value) noexcept {
100 static_assert(std::is_integral_v<Size> && std::is_unsigned_v<Size>);
101 if constexpr (std::numeric_limits<Size>::digits >=
102 std::numeric_limits<std::uint64_t>::digits) {
103 return true;
104 } else {
105 return value <=
106 static_cast<std::uint64_t>(std::numeric_limits<Size>::max());
107 }
108}
109
110inline void webgpu_readback_complete(WGPUMapAsyncStatus status, WGPUStringView,
111 void* userdata1, void*) noexcept {
112 auto* operation = static_cast<WebGpuReadbackOperation*>(userdata1);
113 const void* data = nullptr;
114 auto result = WebGpuReadbackStatus::Failed;
115 // This acquire load is the linearization point against device notifications:
116 // whichever atomic operation wins decides whether this already-submitted
117 // result is still safe to expose to the application callback.
118 const auto device_available =
119 operation->shared->available.load(std::memory_order_acquire);
120 if (status == WGPUMapAsyncStatus_Success && device_available) {
121 data = wgpuBufferGetConstMappedRange(operation->staging, 0,
122 operation->byte_size);
123 if (data != nullptr) {
124 result = WebGpuReadbackStatus::Complete;
125 }
126 }
127 operation->callback(operation->handle, result, data,
128 data == nullptr ? 0 : operation->byte_size,
129 operation->userdata);
130 if (status == WGPUMapAsyncStatus_Success) {
131 wgpuBufferUnmap(operation->staging);
132 }
133 wgpuBufferRelease(operation->staging);
134 operation->shared->inflight_readback_bytes.fetch_sub(
135 operation->byte_size, std::memory_order_relaxed);
136 delete operation;
137}
138
139[[nodiscard]] inline bool reserve_readback_bytes(
140 const std::shared_ptr<WebGpuSharedState>& shared,
141 std::uint64_t bytes) noexcept {
142 auto current =
143 shared->inflight_readback_bytes.load(std::memory_order_relaxed);
144 for (;;) {
145 if (bytes > shared->max_inflight_readback_bytes - current) {
146 return false;
147 }
148 if (shared->inflight_readback_bytes.compare_exchange_weak(
149 current, current + bytes, std::memory_order_relaxed)) {
150 return true;
151 }
152 }
153}
154
155} // namespace detail
156
169 public:
171 explicit WebGpuBackend(WGPUDevice device, WebGpuBackendConfig config = {})
172 : device_(device),
173 config_(config),
174 shared_(std::make_shared<detail::WebGpuSharedState>()) {
175 shared_->max_inflight_readback_bytes = config_.max_inflight_readback_bytes;
176 fields_.reserve(config_.field_capacity);
177 products_.reserve(config_.product_capacity);
178 if (device_ == nullptr || config_.max_buffer_bytes == 0 ||
179 config_.max_dispatch_chunks == 0 ||
180 config_.max_dispatch_workgroups_x == 0) {
181 shared_->available.store(false, std::memory_order_relaxed);
182 // No reference was retained on this disabled construction path.
183 device_ = nullptr;
184 return;
185 }
186 wgpuDeviceAddRef(device_);
187 queue_ = wgpuDeviceGetQueue(device_);
188 if (queue_ == nullptr) {
189 shared_->available.store(false, std::memory_order_relaxed);
190 }
191 }
192
193 WebGpuBackend(const WebGpuBackend&) = delete;
194 auto operator=(const WebGpuBackend&) -> WebGpuBackend& = delete;
195 WebGpuBackend(WebGpuBackend&&) = delete;
196 auto operator=(WebGpuBackend&&) -> WebGpuBackend& = delete;
197
200 for (auto& product : products_) {
201 release_product(product);
202 }
203 for (auto& field : fields_) {
204 if (field.buffer != nullptr) {
205 wgpuBufferRelease(field.buffer);
206 }
207 }
208 if (queue_ != nullptr) {
209 wgpuQueueRelease(queue_);
210 }
211 if (device_ != nullptr) {
212 wgpuDeviceRelease(device_);
213 }
214 }
215
218 [[nodiscard]] auto capabilities() const noexcept -> GpuCapabilities {
219 if (!available()) {
220 return {};
221 }
222 return GpuCapabilities{
223 .compute = true,
224 .async_dispatch = true,
225 .async_readback = true,
226 .max_buffer_bytes = config_.max_buffer_bytes,
227 .max_dispatch_chunks = config_.max_dispatch_chunks,
228 .buffer_alignment = 4,
229 };
230 }
231
233 [[nodiscard]] bool register_field(FieldMirrorDesc desc) {
234 if (!available() || !desc.total_bytes_fits() || desc.total_bytes() == 0 ||
235 desc.total_bytes() > config_.max_buffer_bytes ||
236 fields_.size() >= config_.field_capacity ||
237 find_field(desc.field_index) != nullptr) {
238 return false;
239 }
240 const auto bytes = aligned_size(desc.total_bytes());
241 if (!bytes.has_value() || *bytes > config_.max_buffer_bytes) {
242 return false;
243 }
244 auto buffer_desc = WGPU_BUFFER_DESCRIPTOR_INIT;
245 buffer_desc.usage = WGPUBufferUsage_Storage | WGPUBufferUsage_CopyDst;
246 buffer_desc.size = *bytes;
247 auto buffer = wgpuDeviceCreateBuffer(device_, &buffer_desc);
248 if (buffer == nullptr) {
249 return false;
250 }
251 fields_.push_back(FieldSlot{desc, buffer, *bytes});
252 return true;
253 }
254
256 [[nodiscard]] auto field_buffer(std::uint32_t field_index) const noexcept
257 -> WGPUBuffer {
258 const auto* field = find_field(field_index);
259 return field == nullptr ? nullptr : field->buffer;
260 }
261
263 [[nodiscard]] auto register_product(WebGpuProductDesc desc)
264 -> std::optional<GpuProductHandle> {
265 if (!available() || desc.product_key == 0 || desc.pipeline == nullptr ||
266 desc.bind_group == nullptr ||
267 find_product(desc.product_key) != nullptr) {
268 return std::nullopt;
269 }
270 if (desc.readback_source == nullptr) {
271 // Source-less products do not perform readback. Reject orphaned callback
272 // metadata instead of accepting a configuration whose callback can
273 // never fire.
274 if (desc.readback_byte_size != 0 || desc.readback_source_offset != 0 ||
275 desc.readback_callback != nullptr ||
276 desc.readback_userdata != nullptr) {
277 return std::nullopt;
278 }
279 } else {
280 if (desc.readback_byte_size == 0 || desc.readback_callback == nullptr ||
281 (wgpuBufferGetUsage(desc.readback_source) &
282 WGPUBufferUsage_CopySrc) == 0 ||
283 (desc.readback_source_offset & 3u) != 0 ||
284 (desc.readback_byte_size & 3u) != 0 ||
285 desc.readback_source_offset >
286 wgpuBufferGetSize(desc.readback_source) ||
287 desc.readback_byte_size > wgpuBufferGetSize(desc.readback_source) -
288 desc.readback_source_offset) {
289 return std::nullopt;
290 }
291 }
292 ProductSlot* slot = nullptr;
293 for (auto& candidate : products_) {
294 if (!candidate.active) {
295 slot = &candidate;
296 break;
297 }
298 }
299 if (slot == nullptr) {
300 if (products_.size() >= config_.product_capacity) {
301 return std::nullopt;
302 }
303 products_.emplace_back();
304 slot = &products_.back();
305 }
306 wgpuComputePipelineAddRef(desc.pipeline);
307 wgpuBindGroupAddRef(desc.bind_group);
308 if (desc.readback_source != nullptr) {
309 wgpuBufferAddRef(desc.readback_source);
310 }
311 slot->active = true;
312 slot->generation = ++generation_clock_;
313 if (slot->generation == 0) {
314 slot->generation = ++generation_clock_;
315 }
316 slot->desc = desc;
317 return GpuProductHandle{desc.product_key, slot->generation};
318 }
319
321 bool unregister_product(GpuProductHandle handle) noexcept {
322 auto* product = find_product(handle.key);
323 if (product == nullptr || product->generation != handle.generation) {
324 return false;
325 }
326 release_product(*product);
327 return true;
328 }
329
331 [[nodiscard]] bool valid(GpuProductHandle handle) const noexcept {
332 const auto* product = find_product(handle.key);
333 return product != nullptr && product->generation == handle.generation;
334 }
335
337 [[nodiscard]] bool upload(const UploadDesc& upload) {
338 const auto* field = find_field(upload.field_index);
339 if (!available() || field == nullptr || upload.data == nullptr ||
340 upload.byte_size == 0 || (upload.buffer_offset & 3u) != 0 ||
341 (upload.byte_size & 3u) != 0 ||
342 !detail::fits_size<std::size_t>(upload.byte_size) ||
343 upload.buffer_offset > field->allocated_bytes ||
344 upload.byte_size > field->allocated_bytes - upload.buffer_offset) {
345 return false;
346 }
347 wgpuQueueWriteBuffer(queue_, field->buffer, upload.buffer_offset,
348 upload.data,
349 static_cast<std::size_t>(upload.byte_size));
350 return true;
351 }
352
354 [[nodiscard]] bool dispatch(const DispatchDesc& dispatch) {
355 const auto* product = find_product(dispatch.handle.key);
356 if (!available() || product == nullptr ||
357 find_field(dispatch.input_field_index) == nullptr ||
358 product->generation != dispatch.handle.generation ||
359 product->desc.input_field_index != dispatch.input_field_index ||
360 dispatch.chunk_count == 0 || dispatch.workgroups_per_chunk == 0 ||
361 dispatch.chunk_count > config_.max_dispatch_chunks ||
362 dispatch.chunk_count >
363 config_.max_dispatch_workgroups_x / dispatch.workgroups_per_chunk) {
364 return false;
365 }
366 auto encoder_desc = WGPU_COMMAND_ENCODER_DESCRIPTOR_INIT;
367 auto encoder = wgpuDeviceCreateCommandEncoder(device_, &encoder_desc);
368 if (encoder == nullptr) {
369 return false;
370 }
371 auto pass_desc = WGPU_COMPUTE_PASS_DESCRIPTOR_INIT;
372 auto pass = wgpuCommandEncoderBeginComputePass(encoder, &pass_desc);
373 if (pass == nullptr) {
374 wgpuCommandEncoderRelease(encoder);
375 return false;
376 }
377 wgpuComputePassEncoderSetPipeline(pass, product->desc.pipeline);
378 wgpuComputePassEncoderSetBindGroup(pass, 0, product->desc.bind_group, 0,
379 nullptr);
380 wgpuComputePassEncoderDispatchWorkgroups(
381 pass,
382 static_cast<std::uint32_t>(dispatch.chunk_count) *
383 dispatch.workgroups_per_chunk,
384 1, 1);
385 wgpuComputePassEncoderEnd(pass);
386 wgpuComputePassEncoderRelease(pass);
387 const auto submitted = submit(encoder);
388 wgpuCommandEncoderRelease(encoder);
389 return submitted;
390 }
391
398 [[nodiscard]] bool readback(const ReadbackDesc& readback) {
399 const auto* product = find_product(readback.handle.key);
400 if (!available() || product == nullptr ||
401 product->generation != readback.handle.generation ||
402 readback.policy == ReadbackPolicy::None ||
403 (readback.policy == ReadbackPolicy::FullField &&
404 !config_.allow_full_field_readback) ||
405 readback.byte_size == 0 || (readback.byte_size & 3u) != 0 ||
406 product->desc.readback_source == nullptr ||
407 product->desc.readback_callback == nullptr ||
408 readback.byte_size > product->desc.readback_byte_size ||
409 !detail::fits_size<std::size_t>(readback.byte_size) ||
410 !detail::reserve_readback_bytes(shared_, readback.byte_size)) {
411 return false;
412 }
413 const auto size = static_cast<std::size_t>(readback.byte_size);
414 auto buffer_desc = WGPU_BUFFER_DESCRIPTOR_INIT;
415 buffer_desc.usage = WGPUBufferUsage_MapRead | WGPUBufferUsage_CopyDst;
416 buffer_desc.size = readback.byte_size;
417 auto staging = wgpuDeviceCreateBuffer(device_, &buffer_desc);
418 if (staging == nullptr) {
419 release_readback_bytes(size);
420 return false;
421 }
422 auto* operation = new (std::nothrow) detail::WebGpuReadbackOperation{
423 staging,
424 readback.handle,
425 size,
426 product->desc.readback_callback,
427 product->desc.readback_userdata,
428 shared_,
429 };
430 if (operation == nullptr) {
431 wgpuBufferRelease(staging);
432 release_readback_bytes(size);
433 return false;
434 }
435
436 auto encoder_desc = WGPU_COMMAND_ENCODER_DESCRIPTOR_INIT;
437 auto encoder = wgpuDeviceCreateCommandEncoder(device_, &encoder_desc);
438 if (encoder == nullptr) {
439 abandon_readback(operation);
440 return false;
441 }
442 wgpuCommandEncoderCopyBufferToBuffer(encoder, product->desc.readback_source,
443 product->desc.readback_source_offset,
444 staging, 0, readback.byte_size);
445 const auto submitted = submit(encoder);
446 wgpuCommandEncoderRelease(encoder);
447 if (!submitted) {
448 abandon_readback(operation);
449 return false;
450 }
451 auto callback = WGPU_BUFFER_MAP_CALLBACK_INFO_INIT;
452 // Spontaneous delivery avoids making this transport own or pump the
453 // application's WGPUInstance. It can run inline or on another thread.
454 // Backend cleanup below uses only buffer operations that the stable
455 // WebGPU API explicitly permits in spontaneous callbacks; application
456 // callbacks inherit the stricter public contract documented above.
457 callback.mode = WGPUCallbackMode_AllowSpontaneous;
458 callback.callback = detail::webgpu_readback_complete;
459 callback.userdata1 = operation;
460 const auto future =
461 wgpuBufferMapAsync(staging, WGPUMapMode_Read, 0, size, callback);
462 // A null future never delivers its callback, so this call still owns the
463 // operation. A non-null future may already have completed inline and
464 // deleted it; do not inspect operation after this branch.
465 if (future.id == 0) {
466 abandon_readback(operation);
467 return false;
468 }
469 return true;
470 }
471
474 void notify_device_lost() noexcept {
475 shared_->available.store(false, std::memory_order_release);
476 }
477
488 void notify_device_error() noexcept {
489 shared_->available.store(false, std::memory_order_release);
490 }
491
492 private:
493 struct FieldSlot {
494 FieldMirrorDesc desc{};
495 WGPUBuffer buffer = nullptr;
496 std::uint64_t allocated_bytes = 0;
497 };
498
499 struct ProductSlot {
500 bool active = false;
501 std::uint64_t generation = 0;
502 WebGpuProductDesc desc{};
503 };
504
505 [[nodiscard]] bool available() const noexcept {
506 return device_ != nullptr && queue_ != nullptr &&
507 shared_->available.load(std::memory_order_acquire);
508 }
509
510 [[nodiscard]] static auto aligned_size(std::uint64_t size) noexcept
511 -> std::optional<std::uint64_t> {
512 if (size > std::numeric_limits<std::uint64_t>::max() - 3) {
513 return std::nullopt;
514 }
515 return (size + 3) & ~std::uint64_t{3};
516 }
517
518 [[nodiscard]] auto find_field(std::uint32_t field_index) noexcept
519 -> FieldSlot* {
520 for (auto& field : fields_) {
521 if (field.desc.field_index == field_index) {
522 return &field;
523 }
524 }
525 return nullptr;
526 }
527
528 [[nodiscard]] auto find_field(std::uint32_t field_index) const noexcept
529 -> const FieldSlot* {
530 for (const auto& field : fields_) {
531 if (field.desc.field_index == field_index) {
532 return &field;
533 }
534 }
535 return nullptr;
536 }
537
538 [[nodiscard]] auto find_product(std::uint64_t key) noexcept -> ProductSlot* {
539 for (auto& product : products_) {
540 if (product.active && product.desc.product_key == key) {
541 return &product;
542 }
543 }
544 return nullptr;
545 }
546
547 [[nodiscard]] auto find_product(std::uint64_t key) const noexcept
548 -> const ProductSlot* {
549 for (const auto& product : products_) {
550 if (product.active && product.desc.product_key == key) {
551 return &product;
552 }
553 }
554 return nullptr;
555 }
556
557 static void release_product(ProductSlot& product) noexcept {
558 if (!product.active) {
559 return;
560 }
561 wgpuComputePipelineRelease(product.desc.pipeline);
562 wgpuBindGroupRelease(product.desc.bind_group);
563 if (product.desc.readback_source != nullptr) {
564 wgpuBufferRelease(product.desc.readback_source);
565 }
566 product.active = false;
567 product.desc = {};
568 }
569
570 [[nodiscard]] bool submit(WGPUCommandEncoder encoder) {
571 auto command_desc = WGPU_COMMAND_BUFFER_DESCRIPTOR_INIT;
572 auto command = wgpuCommandEncoderFinish(encoder, &command_desc);
573 if (command == nullptr) {
574 return false;
575 }
576 wgpuQueueSubmit(queue_, 1, &command);
577 wgpuCommandBufferRelease(command);
578 return true;
579 }
580
581 void release_readback_bytes(std::size_t size) noexcept {
582 shared_->inflight_readback_bytes.fetch_sub(size, std::memory_order_relaxed);
583 }
584
585 void abandon_readback(detail::WebGpuReadbackOperation* operation) noexcept {
586 wgpuBufferRelease(operation->staging);
587 release_readback_bytes(operation->byte_size);
588 delete operation;
589 }
590
591 WGPUDevice device_ = nullptr;
592 WGPUQueue queue_ = nullptr;
593 WebGpuBackendConfig config_{};
594 std::shared_ptr<detail::WebGpuSharedState> shared_;
595 std::vector<FieldSlot> fields_;
596 std::vector<ProductSlot> products_;
597 std::uint64_t generation_clock_ = 0;
598};
599
600static_assert(GpuBackend<WebGpuBackend>);
601
602} // namespace tess::gpu
603
604#endif // defined(TESS_ENABLE_WEBGPU)
auto register_product(WebGpuProductDesc desc) -> std::optional< GpuProductHandle >
Definition webgpu_backend.h:263
bool valid(GpuProductHandle handle) const noexcept
Definition webgpu_backend.h:331
bool register_field(FieldMirrorDesc desc)
Definition webgpu_backend.h:233
bool unregister_product(GpuProductHandle handle) noexcept
Definition webgpu_backend.h:321
~WebGpuBackend()
Definition webgpu_backend.h:199
bool readback(const ReadbackDesc &readback)
Definition webgpu_backend.h:398
bool dispatch(const DispatchDesc &dispatch)
Definition webgpu_backend.h:354
void notify_device_error() noexcept
Definition webgpu_backend.h:488
auto field_buffer(std::uint32_t field_index) const noexcept -> WGPUBuffer
Definition webgpu_backend.h:256
auto capabilities() const noexcept -> GpuCapabilities
Definition webgpu_backend.h:218
void notify_device_lost() noexcept
Definition webgpu_backend.h:474
bool upload(const UploadDesc &upload)
Definition webgpu_backend.h:337
WebGpuBackend(WGPUDevice device, WebGpuBackendConfig config={})
Definition webgpu_backend.h:171
Definition backend.h:40
Definition descriptors.h:182
Definition descriptors.h:78
constexpr auto total_bytes_fits() const noexcept -> bool
Definition descriptors.h:87
constexpr auto total_bytes() const noexcept -> std::uint64_t
Definition descriptors.h:100
Definition backend.h:20
Definition descriptors.h:154
Definition descriptors.h:200
Definition descriptors.h:145
Definition webgpu_backend.h:28
Definition webgpu_backend.h:69