Files
MapLibreParticleLab/src/mln/webgpu/headless_backend.cpp
2026-09-01 18:35:29 +03:00

391 lines
12 KiB
C++

#include <mln/webgpu/headless_backend.hpp>
#include <mln/webgpu/renderable_resource.hpp>
#include <mln/webgpu/context.hpp>
#include <mln/webgpu/offscreen_texture.hpp>
#include <mln/util/logging.hpp>
#include <mln/webgpu/wgpu_cpp_compat.hpp>
#include <mln/webgpu/texture2d.hpp>
#if MLN_WEBGPU_IMPL_DAWN
#include <dawn/native/DawnNative.h>
#elif MLN_WEBGPU_IMPL_WGPU
#include <wgpu.h>
#endif
namespace mln {
namespace webgpu {
class HeadlessRenderableResource final : public webgpu::RenderableResource {
public:
HeadlessRenderableResource(HeadlessBackend&, webgpu::Context& context_, Size size_)
: context(context_),
size(size_) {
// Create offscreen texture using the proper WebGPU OffscreenTexture class
offscreenTexture = context.createOffscreenTexture(size, gfx::TextureChannelDataType::UnsignedByte, true, true);
}
~HeadlessRenderableResource() noexcept override = default;
void bind() override {
// Bind the offscreen texture
if (offscreenTexture) {
offscreenTexture->getResource<webgpu::RenderableResource>().bind();
}
}
void swap() override {
// For headless rendering, we don't need to swap
}
const mln::webgpu::RendererBackend& getBackend() const override { return context.getBackend(); }
const WGPUCommandEncoder& getCommandEncoder() const override {
// This will be set by the command encoder when needed
static WGPUCommandEncoder dummy = nullptr;
return dummy;
}
WGPURenderPassEncoder getRenderPassEncoder() const override {
// This will be set by the render pass when needed
return nullptr;
}
WGPUTextureView getColorTextureView() override {
if (offscreenTexture) {
return offscreenTexture->getResource<webgpu::RenderableResource>().getColorTextureView();
}
return nullptr;
}
std::optional<wgpu::TextureFormat> getColorTextureFormat() const override {
if (offscreenTexture) {
return offscreenTexture->getResource<webgpu::RenderableResource>().getColorTextureFormat();
}
return std::nullopt;
}
WGPUTextureView getDepthStencilTextureView() override {
if (offscreenTexture) {
return offscreenTexture->getResource<webgpu::RenderableResource>().getDepthStencilTextureView();
}
return nullptr;
}
std::optional<wgpu::TextureFormat> getDepthStencilTextureFormat() const override {
if (offscreenTexture) {
return offscreenTexture->getResource<webgpu::RenderableResource>().getDepthStencilTextureFormat();
}
return std::nullopt;
}
PremultipliedImage readStillImage() {
if (offscreenTexture) {
return offscreenTexture->readStillImage();
}
return PremultipliedImage(size);
}
gfx::Texture2DPtr takeTexture() { return static_cast<OffscreenTexture*>(offscreenTexture.get())->takeTexture(); }
private:
webgpu::Context& context;
Size size;
std::unique_ptr<gfx::OffscreenTexture> offscreenTexture;
};
class HeadlessBackend::Impl {
public:
#if MLN_WEBGPU_IMPL_DAWN
std::unique_ptr<dawn::native::Instance> instance;
#elif MLN_WEBGPU_IMPL_WGPU
wgpu::Instance instance;
wgpu::Adapter adapter;
#endif
wgpu::Device device;
wgpu::Queue queue;
wgpu::Texture offscreenTexture;
wgpu::TextureView offscreenTextureView;
wgpu::Texture depthTexture;
wgpu::TextureView depthTextureView;
Size offscreenTextureSize;
};
HeadlessBackend::HeadlessBackend(Size size_, SwapBehaviour swapBehaviour_, gfx::ContextMode mode)
: webgpu::RendererBackend(mode),
gfx::HeadlessBackend(size_),
impl(std::make_unique<Impl>()) {
static_cast<void>(swapBehaviour_);
#if MLN_WEBGPU_IMPL_DAWN
// Initialize Dawn instance with TimedWaitAny feature enabled
wgpu::InstanceFeatureName timedWaitAnyFeature = wgpu::InstanceFeatureName::TimedWaitAny;
wgpu::InstanceDescriptor instanceDesc = {};
instanceDesc.requiredFeatureCount = 1;
instanceDesc.requiredFeatures = &timedWaitAnyFeature;
impl->instance = std::make_unique<dawn::native::Instance>(&instanceDesc);
// Enumerate adapters using the correct API
std::vector<dawn::native::Adapter> adapters = impl->instance->EnumerateAdapters();
if (adapters.empty()) {
Log::Error(Event::Render, "WebGPU HeadlessBackend: No adapters found");
return;
}
// Use the first adapter (typically Vulkan on Linux)
dawn::native::Adapter& selectedAdapter = adapters[0];
// Create device descriptor
wgpu::DeviceDescriptor deviceDesc = {};
deviceDesc.label = "MapLibre Headless WebGPU Device";
// Create device
WGPUDevice rawDevice = selectedAdapter.CreateDevice(&deviceDesc);
if (!rawDevice) {
Log::Error(Event::Render, "WebGPU HeadlessBackend: Failed to create device");
return;
}
impl->device = wgpu::Device::Acquire(rawDevice);
impl->queue = impl->device.GetQueue();
// Store device and queue in base class
setDevice(impl->device.Get());
setQueue(impl->queue.Get());
setInstance(impl->instance->Get());
#elif MLN_WEBGPU_IMPL_WGPU && !MLN_WEBGPU_IMPL_FFI
// wgpu-native backend initialization
wgpu::InstanceDescriptor instanceDesc = {};
impl->instance = wgpu::createInstance(instanceDesc);
if (!impl->instance) {
Log::Error(Event::Render, "WebGPU HeadlessBackend: Failed to create instance");
return;
}
// Request adapter using simpler blocking API
wgpu::RequestAdapterOptions adapterOpts = {};
adapterOpts.powerPreference = wgpu::PowerPreference::HighPerformance;
impl->adapter = impl->instance.requestAdapter(adapterOpts);
if (!impl->adapter) {
Log::Error(Event::Render, "WebGPU HeadlessBackend: No adapter found");
return;
}
// Request device using simpler blocking API
wgpu::DeviceDescriptor deviceDesc = {};
impl->device = impl->adapter.requestDevice(deviceDesc);
if (!impl->device) {
Log::Error(Event::Render, "WebGPU HeadlessBackend: Failed to create device");
return;
}
impl->queue = impl->device.getQueue();
// Store device, queue, and instance in base class
setDevice(static_cast<WGPUDevice>(impl->device));
setQueue(static_cast<WGPUQueue>(impl->queue));
setInstance(static_cast<WGPUInstance>(impl->instance));
#endif
// Create offscreen render texture
createOffscreenTextures();
}
HeadlessBackend::~HeadlessBackend() {
// Explicitly reset the renderable resource
setResource(nullptr);
context.reset();
if (impl) {
// Clean up textures
impl->offscreenTextureView = nullptr;
impl->offscreenTexture = nullptr;
impl->depthTextureView = nullptr;
impl->depthTexture = nullptr;
// Clean up device and queue
impl->queue = nullptr;
impl->device = nullptr;
impl->instance = nullptr;
}
}
void HeadlessBackend::createOffscreenTextures() {
if (!impl->device) return;
const auto renderableSize = getSize();
impl->offscreenTextureSize = renderableSize;
// Release old textures if they exist
impl->offscreenTextureView = nullptr;
impl->offscreenTexture = nullptr;
impl->depthTextureView = nullptr;
impl->depthTexture = nullptr;
// Create offscreen color texture
wgpu::TextureDescriptor colorDesc = {};
colorDesc.usage = wgpu::TextureUsage::RenderAttachment | wgpu::TextureUsage::CopySrc;
#if MLN_WEBGPU_IMPL_DAWN
colorDesc.dimension = wgpu::TextureDimension::e2D;
#elif MLN_WEBGPU_IMPL_WGPU
colorDesc.dimension = wgpu::TextureDimension::_2D;
#endif
colorDesc.size = {renderableSize.width, renderableSize.height, 1};
colorDesc.format = wgpu::TextureFormat::RGBA8Unorm;
colorDesc.mipLevelCount = 1;
colorDesc.sampleCount = 1;
#if MLN_WEBGPU_IMPL_DAWN
colorDesc.label = "Headless Color Texture";
impl->offscreenTexture = impl->device.CreateTexture(&colorDesc);
#elif MLN_WEBGPU_IMPL_WGPU
impl->offscreenTexture = impl->device.createTexture(colorDesc);
#endif
if (impl->offscreenTexture) {
wgpu::TextureViewDescriptor viewDesc = {};
viewDesc.format = wgpu::TextureFormat::RGBA8Unorm;
#if MLN_WEBGPU_IMPL_DAWN
viewDesc.dimension = wgpu::TextureViewDimension::e2D;
#elif MLN_WEBGPU_IMPL_WGPU
viewDesc.dimension = wgpu::TextureViewDimension::_2D;
#endif
viewDesc.baseMipLevel = 0;
viewDesc.mipLevelCount = 1;
viewDesc.baseArrayLayer = 0;
viewDesc.arrayLayerCount = 1;
viewDesc.aspect = wgpu::TextureAspect::All;
#if MLN_WEBGPU_IMPL_DAWN
viewDesc.label = "Headless Color TextureView";
impl->offscreenTextureView = impl->offscreenTexture.CreateView(&viewDesc);
#elif MLN_WEBGPU_IMPL_WGPU
impl->offscreenTextureView = impl->offscreenTexture.createView(viewDesc);
#endif
}
// Create depth/stencil texture
wgpu::TextureDescriptor depthDesc = {};
depthDesc.usage = wgpu::TextureUsage::RenderAttachment;
#if MLN_WEBGPU_IMPL_DAWN
depthDesc.dimension = wgpu::TextureDimension::e2D;
#elif MLN_WEBGPU_IMPL_WGPU
depthDesc.dimension = wgpu::TextureDimension::_2D;
#endif
depthDesc.size = {renderableSize.width, renderableSize.height, 1};
depthDesc.format = wgpu::TextureFormat::Depth24PlusStencil8;
depthDesc.mipLevelCount = 1;
depthDesc.sampleCount = 1;
#if MLN_WEBGPU_IMPL_DAWN
depthDesc.label = "Headless Depth/Stencil Texture";
impl->depthTexture = impl->device.CreateTexture(&depthDesc);
#elif MLN_WEBGPU_IMPL_WGPU
impl->depthTexture = impl->device.createTexture(depthDesc);
#endif
if (impl->depthTexture) {
wgpu::TextureViewDescriptor viewDesc = {};
viewDesc.format = wgpu::TextureFormat::Depth24PlusStencil8;
#if MLN_WEBGPU_IMPL_DAWN
viewDesc.dimension = wgpu::TextureViewDimension::e2D;
#elif MLN_WEBGPU_IMPL_WGPU
viewDesc.dimension = wgpu::TextureViewDimension::_2D;
#endif
viewDesc.baseMipLevel = 0;
viewDesc.mipLevelCount = 1;
viewDesc.baseArrayLayer = 0;
viewDesc.arrayLayerCount = 1;
viewDesc.aspect = wgpu::TextureAspect::All;
#if MLN_WEBGPU_IMPL_DAWN
viewDesc.label = "Headless Depth/Stencil TextureView";
impl->depthTextureView = impl->depthTexture.CreateView(&viewDesc);
#elif MLN_WEBGPU_IMPL_WGPU
impl->depthTextureView = impl->depthTexture.createView(viewDesc);
#endif
}
}
gfx::Renderable& HeadlessBackend::getDefaultRenderable() {
if (!hasResource()) {
setResource(std::make_unique<HeadlessRenderableResource>(
*this, static_cast<webgpu::Context&>(getContext()), getSize()));
}
return *this;
}
PremultipliedImage HeadlessBackend::readStillImage() {
if (hasResource()) {
return getResource<HeadlessRenderableResource>().readStillImage();
}
return PremultipliedImage(getSize());
}
gfx::Texture2DPtr HeadlessBackend::takeTexture() {
if (hasResource()) {
return getResource<HeadlessRenderableResource>().takeTexture();
}
return nullptr;
}
RendererBackend* HeadlessBackend::getRendererBackend() {
return this;
}
void HeadlessBackend::activate() {
active = true;
// Ensure textures are created if size changed
if (impl && impl->offscreenTextureSize != getSize()) {
createOffscreenTextures();
}
}
void HeadlessBackend::deactivate() {
active = false;
}
void* HeadlessBackend::getCurrentTextureView() {
if (hasResource()) {
return getResource<HeadlessRenderableResource>().getColorTextureView();
}
return nullptr;
}
void* HeadlessBackend::getDepthStencilView() {
if (hasResource()) {
return getResource<HeadlessRenderableResource>().getDepthStencilTextureView();
}
return nullptr;
}
mln::Size HeadlessBackend::getFramebufferSize() const {
return getSize();
}
} // namespace webgpu
namespace gfx {
template <>
std::unique_ptr<gfx::HeadlessBackend> Backend::Create<Backend::Type::WebGPU>(
Size size, gfx::Renderable::SwapBehaviour swapBehaviour, gfx::ContextMode contextMode) {
return std::make_unique<webgpu::HeadlessBackend>(size, swapBehaviour, contextMode);
}
} // namespace gfx
} // namespace mln