v0.7.0: SDL-free wasm worker backend (WebGPU + WebGL2) with on-demand assets
The engine now runs on a plain Web Worker (no SDL, no PROXY_TO_PTHREAD: in a pthread Emscripten proxies every filesystem syscall to the main browser thread, where synchronous XHR - and thus FS.createLazyFile - is forbidden). A page/worker pair transfers an OffscreenCanvas and forwards DOM input; assets load lazily from an embedded manifest, so entering a map fetches only that map and its tiles. Presentation: ra3.webgpu (WebGPU via Emscripten's emdawnwebgpu port, WGSL shaders, the default on wasm) and ra3.wasmgl (WebGL2, GLSL ES). The legacy SDL ra3.webgl backend is removed.
This commit is contained in:
@@ -0,0 +1,610 @@
|
||||
module;
|
||||
|
||||
// Dawn's header-only C++ wrapper for the WebGPU C API, provided by Emscripten's
|
||||
// emdawnwebgpu port (`--use-port=emdawnwebgpu`). This backend is wasm-only: on
|
||||
// the web WebGPU accepts WGSL only (SPIR-V is not available), so the shaders are
|
||||
// WGSL ports of the Vulkan/WebGL ones rather than a reuse of the SPIR-V blobs.
|
||||
#include <webgpu/webgpu_cpp.h>
|
||||
|
||||
#include <emscripten/em_asm.h>
|
||||
#include <emscripten/emscripten.h>
|
||||
#include <emscripten/html5.h>
|
||||
|
||||
#include "webgpu_wgsl_embedded.hpp"
|
||||
|
||||
export module ra3.webgpu;
|
||||
|
||||
import std;
|
||||
|
||||
export import ra3.core;
|
||||
export import ra3.render;
|
||||
export import ra3.terrain;
|
||||
export import ra3.client;
|
||||
import ra3.wasmgl;
|
||||
import ender.log;
|
||||
|
||||
/**
|
||||
* A WebGPU presentation backend for the wasm build, a peer of `ra3.wasmgl`.
|
||||
*
|
||||
* It runs in the same plain Web Worker and draws into the same transferred
|
||||
* OffscreenCanvas, but through WebGPU: a surface is created on `#canvas`, a
|
||||
* swapchain image is acquired each frame and presented. The uniform layout and
|
||||
* the terrain data plumbing are shared with the Vulkan/WebGL backends
|
||||
* (`terrain_uniforms`); only the API calls and the WGSL shaders differ.
|
||||
*
|
||||
* It reuses `ra3.wasmgl`'s `event_sink` so the page/worker input protocol is the
|
||||
* same for both wasm backends.
|
||||
*/
|
||||
export namespace ra3::webgpu {
|
||||
class webgpu_display final : public ra3::client::display, public ra3::wasmgl::event_sink {
|
||||
public:
|
||||
webgpu_display() = default;
|
||||
webgpu_display(const webgpu_display &) = delete;
|
||||
auto operator=(const webgpu_display &) -> webgpu_display & = delete;
|
||||
~webgpu_display() override { this->shutdown(); }
|
||||
|
||||
[[nodiscard]] auto init(const ra3::client::display_options &options) -> bool override {
|
||||
this->fps_limit_ = options.fps_limit;
|
||||
wgpu::Device device(emscripten_webgpu_get_device());
|
||||
if (device == nullptr) {
|
||||
ender::log::warn("ra3.webgpu: no preinitialized WebGPU device");
|
||||
return false;
|
||||
}
|
||||
device_ = std::move(device);
|
||||
queue_ = device_.GetQueue();
|
||||
instance_ = wgpu::CreateInstance();
|
||||
if (instance_ == nullptr) {
|
||||
ender::log::warn("ra3.webgpu: wgpuCreateInstance failed");
|
||||
return false;
|
||||
}
|
||||
|
||||
wgpu::EmscriptenSurfaceSourceCanvasHTMLSelector selector;
|
||||
selector.selector = "#canvas";
|
||||
// There is no DOM in the worker, so Emscripten's canvas lookup cannot
|
||||
// find #canvas by CSS selector; register the transferred OffscreenCanvas
|
||||
// (Module.canvas) as a special target for `wgpuInstanceCreateSurface`.
|
||||
EM_ASM({
|
||||
if (typeof specialHTMLTargets !== 'undefined' && Module['canvas']) {
|
||||
specialHTMLTargets['#canvas'] = Module['canvas'];
|
||||
}
|
||||
});
|
||||
wgpu::SurfaceDescriptor surface_descriptor;
|
||||
surface_descriptor.nextInChain = &selector;
|
||||
surface_ = instance_.CreateSurface(&surface_descriptor);
|
||||
if (surface_ == nullptr) {
|
||||
ender::log::warn("ra3.webgpu: CreateSurface failed (is #canvas an OffscreenCanvas?)");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!this->create_pipelines()) {
|
||||
ender::log::warn("ra3.webgpu: pipeline creation failed");
|
||||
this->shutdown();
|
||||
return false;
|
||||
}
|
||||
const auto [width, height] = this->window_size();
|
||||
this->configure(width > 0 ? width : options.width, height > 0 ? height : options.height);
|
||||
ra3::wasmgl::set_event_sink(this);
|
||||
ender::log::info("ra3.webgpu: WebGPU backend initialized");
|
||||
return true;
|
||||
}
|
||||
|
||||
[[nodiscard]] auto present(const ra3::render::image &frame, const ra3::render::view_rect &dest, bool changed) -> bool override {
|
||||
if (frame.empty()) return false;
|
||||
if (changed || !scene_.texture || static_cast<int>(frame.width()) != scene_.width || static_cast<int>(frame.height()) != scene_.height) {
|
||||
this->upload_image(scene_, frame);
|
||||
}
|
||||
const auto [window_w, window_h] = this->window_size();
|
||||
if (window_w <= 0 || window_h <= 0) return true;
|
||||
return this->draw({0.05F, 0.06F, 0.08F, 1.0F}, [&](wgpu::RenderPassEncoder &pass, int w, int h) {
|
||||
this->draw_image(pass, scene_, dest, w, h);
|
||||
});
|
||||
}
|
||||
|
||||
[[nodiscard]] auto poll_event(ra3::render::ui_event &out) -> bool override {
|
||||
if (events_.empty()) return false;
|
||||
out = events_.front();
|
||||
events_.pop_front();
|
||||
return true;
|
||||
}
|
||||
|
||||
[[nodiscard]] auto window_size() const -> std::pair<int, int> override {
|
||||
int w = 0;
|
||||
int h = 0;
|
||||
if (emscripten_get_canvas_element_size("#canvas", &w, &h) != EMSCRIPTEN_RESULT_SUCCESS) return {0, 0};
|
||||
return {w, h};
|
||||
}
|
||||
|
||||
[[nodiscard]] auto key_down(ra3::render::ui_key key) const -> bool override {
|
||||
const auto held = [this](ra3::wasmgl::detail::move_key which) { return held_[static_cast<std::size_t>(which)]; };
|
||||
switch (key) {
|
||||
case ra3::render::ui_key::up: return held(ra3::wasmgl::detail::move_key::w) || held(ra3::wasmgl::detail::move_key::arrow_up);
|
||||
case ra3::render::ui_key::down: return held(ra3::wasmgl::detail::move_key::s) || held(ra3::wasmgl::detail::move_key::arrow_down);
|
||||
case ra3::render::ui_key::left: return held(ra3::wasmgl::detail::move_key::a) || held(ra3::wasmgl::detail::move_key::arrow_left);
|
||||
case ra3::render::ui_key::right: return held(ra3::wasmgl::detail::move_key::d) || held(ra3::wasmgl::detail::move_key::arrow_right);
|
||||
default: return false;
|
||||
}
|
||||
}
|
||||
|
||||
[[nodiscard]] auto supports_terrain() const -> bool override { return true; }
|
||||
|
||||
[[nodiscard]] auto present_terrain(const ra3::terrain::gpu_terrain &terrain, const ra3::render::camera3d &camera, float time_s,
|
||||
const ra3::client::terrain_overlay &overlay) -> bool override {
|
||||
if (!terrain_ready_) {
|
||||
if (!this->create_terrain(terrain)) return false;
|
||||
terrain_ready_ = true;
|
||||
ender::log::info(std::format("ra3.webgpu: terrain ready ({}x{}, {} layers)", terrain.width, terrain.height, terrain.layer_count));
|
||||
}
|
||||
if (overlay.label_changed) this->upload_image(label_, overlay.label);
|
||||
if (overlay.minimap_changed) this->upload_image(minimap_, overlay.minimap);
|
||||
|
||||
const auto [window_w, window_h] = this->window_size();
|
||||
if (window_w <= 0 || window_h <= 0) return true;
|
||||
const auto aspect = static_cast<float>(window_w) / static_cast<float>(std::max(1, window_h));
|
||||
const auto uniforms = detail_uniforms(terrain, camera, time_s, aspect);
|
||||
queue_.WriteBuffer(terrain_.ubo, 0, uniforms.data(), uniforms.size() * sizeof(float));
|
||||
|
||||
const float margin = 12.0F;
|
||||
return this->draw({0.45F, 0.55F, 0.70F, 1.0F}, [&](wgpu::RenderPassEncoder &pass, int w, int h) {
|
||||
pass.SetPipeline(terrain_.pipeline);
|
||||
pass.SetBindGroup(0, terrain_.bind_group);
|
||||
pass.Draw(3);
|
||||
this->draw_image(pass, label_, {margin, margin, static_cast<float>(label_.width), static_cast<float>(label_.height)}, w, h);
|
||||
this->draw_image(pass, minimap_, {static_cast<float>(w) - static_cast<float>(minimap_.width) - margin, margin,
|
||||
static_cast<float>(minimap_.width), static_cast<float>(minimap_.height)},
|
||||
w, h);
|
||||
});
|
||||
}
|
||||
|
||||
auto shutdown() -> void override {
|
||||
if (ra3::wasmgl::detail::sink_slot() == this) ra3::wasmgl::set_event_sink(nullptr);
|
||||
terrain_ready_ = false;
|
||||
scene_ = {};
|
||||
label_ = {};
|
||||
minimap_ = {};
|
||||
terrain_ = {};
|
||||
surface_ = nullptr;
|
||||
queue_ = nullptr;
|
||||
device_ = nullptr;
|
||||
instance_ = nullptr;
|
||||
events_.clear();
|
||||
held_.fill(false);
|
||||
}
|
||||
|
||||
[[nodiscard]] auto name() const -> std::string_view override { return "webgpu"; }
|
||||
|
||||
// ---- worker input (ra3.wasmgl::event_sink) ---------------------------
|
||||
|
||||
auto on_key(std::string_view code, bool down) -> void override {
|
||||
if (const int index = ra3::wasmgl::detail::move_key_index(code); index >= 0) {
|
||||
held_[static_cast<std::size_t>(index)] = down;
|
||||
}
|
||||
if (!down) return;
|
||||
if (const auto key = ra3::wasmgl::detail::key_from_code(code); key != ra3::render::ui_key::none) {
|
||||
push_key(key);
|
||||
}
|
||||
}
|
||||
|
||||
auto on_text(int codepoint) -> void override {
|
||||
if (codepoint <= 0 || codepoint > 0x7F) return;
|
||||
ra3::render::ui_event out;
|
||||
out.type = ra3::render::ui_event_type::text;
|
||||
out.character = static_cast<char>(codepoint);
|
||||
events_.push_back(out);
|
||||
}
|
||||
|
||||
auto on_mouse_button(double x, double y, bool left) -> void override {
|
||||
ra3::render::ui_event out;
|
||||
out.type = ra3::render::ui_event_type::mouse_button;
|
||||
out.x = static_cast<float>(x);
|
||||
out.y = static_cast<float>(y);
|
||||
out.left = left;
|
||||
events_.push_back(out);
|
||||
}
|
||||
|
||||
auto on_mouse_move(double x, double y, double dx, double dy, bool left) -> void override {
|
||||
ra3::render::ui_event out;
|
||||
out.type = ra3::render::ui_event_type::mouse_move;
|
||||
out.x = static_cast<float>(x);
|
||||
out.y = static_cast<float>(y);
|
||||
out.dx = static_cast<float>(dx);
|
||||
out.dy = static_cast<float>(dy);
|
||||
out.left = left;
|
||||
events_.push_back(out);
|
||||
}
|
||||
|
||||
auto on_wheel(double delta_y) -> void override {
|
||||
ra3::render::ui_event out;
|
||||
out.type = ra3::render::ui_event_type::wheel;
|
||||
out.wheel = static_cast<float>(-delta_y);
|
||||
events_.push_back(out);
|
||||
}
|
||||
|
||||
auto on_quit() -> void override {
|
||||
ra3::render::ui_event out;
|
||||
out.type = ra3::render::ui_event_type::quit;
|
||||
events_.push_back(out);
|
||||
}
|
||||
|
||||
auto on_resize(int width, int height) -> void override {
|
||||
if (width > 0 && height > 0) emscripten_set_canvas_element_size("#canvas", width, height);
|
||||
}
|
||||
|
||||
private:
|
||||
/** A drawn image: its texture, uniform rect buffer and bind group. */
|
||||
struct image_target {
|
||||
wgpu::Texture texture;
|
||||
wgpu::TextureView view;
|
||||
wgpu::Buffer ubo;
|
||||
wgpu::BindGroup bind_group;
|
||||
int width = 0;
|
||||
int height = 0;
|
||||
int tex_width = 0;
|
||||
int tex_height = 0;
|
||||
};
|
||||
|
||||
struct terrain_target {
|
||||
wgpu::RenderPipeline pipeline;
|
||||
wgpu::Buffer ubo;
|
||||
wgpu::Texture height;
|
||||
wgpu::Texture cell;
|
||||
wgpu::Texture atlas;
|
||||
wgpu::TextureView height_view;
|
||||
wgpu::TextureView cell_view;
|
||||
wgpu::TextureView atlas_view;
|
||||
wgpu::BindGroup bind_group;
|
||||
};
|
||||
|
||||
static auto detail_uniforms(const ra3::terrain::gpu_terrain &terrain, const ra3::render::camera3d &camera, float time_s, float aspect)
|
||||
-> std::array<float, 20> {
|
||||
return {camera.target_x,
|
||||
camera.target_y,
|
||||
camera.yaw,
|
||||
camera.height,
|
||||
camera.pitch,
|
||||
camera.fov,
|
||||
terrain.water_z,
|
||||
terrain.has_water ? 1.0F : 0.0F,
|
||||
0.45F,
|
||||
0.35F,
|
||||
0.82F,
|
||||
0.38F,
|
||||
static_cast<float>(terrain.width),
|
||||
static_cast<float>(terrain.height),
|
||||
0.0F,
|
||||
terrain.z_scale,
|
||||
time_s,
|
||||
0.0F,
|
||||
terrain.cell_span,
|
||||
aspect};
|
||||
}
|
||||
|
||||
auto push_key(ra3::render::ui_key key) -> void {
|
||||
ra3::render::ui_event out;
|
||||
out.type = ra3::render::ui_event_type::key;
|
||||
out.key = key;
|
||||
events_.push_back(out);
|
||||
}
|
||||
|
||||
[[nodiscard]] auto make_shader(const char *wgsl) -> wgpu::ShaderModule {
|
||||
wgpu::ShaderSourceWGSL source;
|
||||
source.code = wgsl;
|
||||
wgpu::ShaderModuleDescriptor descriptor;
|
||||
descriptor.nextInChain = &source;
|
||||
return device_.CreateShaderModule(&descriptor);
|
||||
}
|
||||
|
||||
[[nodiscard]] auto create_pipelines() -> bool {
|
||||
scene_shader_ = this->make_shader(ra3_shaders::webgpu_scene_wgsl);
|
||||
terrain_shader_ = this->make_shader(ra3_shaders::webgpu_terrain_wgsl);
|
||||
if (scene_shader_ == nullptr || terrain_shader_ == nullptr) {
|
||||
ender::log::warn(std::format("ra3.webgpu: shader module creation failed (scene={} terrain={})",
|
||||
scene_shader_ != nullptr, terrain_shader_ != nullptr));
|
||||
return false;
|
||||
}
|
||||
|
||||
wgpu::SamplerDescriptor clamp_desc;
|
||||
clamp_desc.addressModeU = wgpu::AddressMode::ClampToEdge;
|
||||
clamp_desc.addressModeV = wgpu::AddressMode::ClampToEdge;
|
||||
clamp_desc.magFilter = wgpu::FilterMode::Linear;
|
||||
clamp_desc.minFilter = wgpu::FilterMode::Linear;
|
||||
clamp_desc.lodMaxClamp = 32.0F;
|
||||
clamp_sampler_ = device_.CreateSampler(&clamp_desc);
|
||||
|
||||
wgpu::SamplerDescriptor repeat_desc = clamp_desc;
|
||||
repeat_desc.addressModeU = wgpu::AddressMode::Repeat;
|
||||
repeat_desc.addressModeV = wgpu::AddressMode::Repeat;
|
||||
repeat_sampler_ = device_.CreateSampler(&repeat_desc);
|
||||
|
||||
// Scene: uniform(rect) + texture + sampler.
|
||||
wgpu::BindGroupLayoutEntry scene_entries[3] = {};
|
||||
scene_entries[0].binding = 0;
|
||||
scene_entries[0].visibility = wgpu::ShaderStage::Vertex | wgpu::ShaderStage::Fragment;
|
||||
scene_entries[0].buffer.type = wgpu::BufferBindingType::Uniform;
|
||||
scene_entries[0].buffer.minBindingSize = 16;
|
||||
scene_entries[1].binding = 1;
|
||||
scene_entries[1].visibility = wgpu::ShaderStage::Fragment;
|
||||
scene_entries[1].texture.sampleType = wgpu::TextureSampleType::Float;
|
||||
scene_entries[1].texture.viewDimension = wgpu::TextureViewDimension::e2D;
|
||||
scene_entries[2].binding = 2;
|
||||
scene_entries[2].visibility = wgpu::ShaderStage::Fragment;
|
||||
scene_entries[2].sampler.type = wgpu::SamplerBindingType::Filtering;
|
||||
wgpu::BindGroupLayoutDescriptor scene_layout_desc;
|
||||
scene_layout_desc.entryCount = 3;
|
||||
scene_layout_desc.entries = scene_entries;
|
||||
scene_bind_group_layout_ = device_.CreateBindGroupLayout(&scene_layout_desc);
|
||||
|
||||
wgpu::PipelineLayoutDescriptor scene_pipeline_layout_desc;
|
||||
scene_pipeline_layout_desc.bindGroupLayoutCount = 1;
|
||||
scene_pipeline_layout_desc.bindGroupLayouts = &scene_bind_group_layout_;
|
||||
scene_pipeline_layout_ = device_.CreatePipelineLayout(&scene_pipeline_layout_desc);
|
||||
scene_pipeline_ = this->create_pipeline(scene_pipeline_layout_, scene_shader_, ra3_shaders::webgpu_scene_wgsl);
|
||||
if (scene_pipeline_ == nullptr) {
|
||||
ender::log::warn("ra3.webgpu: scene render pipeline creation failed");
|
||||
}
|
||||
|
||||
// Terrain: uniform(data[5]) + R16Uint + RGBA16Uint + RGBA8 array + sampler.
|
||||
wgpu::BindGroupLayoutEntry terrain_entries[5] = {};
|
||||
terrain_entries[0].binding = 0;
|
||||
terrain_entries[0].visibility = wgpu::ShaderStage::Fragment;
|
||||
terrain_entries[0].buffer.type = wgpu::BufferBindingType::Uniform;
|
||||
terrain_entries[0].buffer.minBindingSize = 80;
|
||||
terrain_entries[1].binding = 1;
|
||||
terrain_entries[1].visibility = wgpu::ShaderStage::Fragment;
|
||||
terrain_entries[1].texture.sampleType = wgpu::TextureSampleType::Uint;
|
||||
terrain_entries[1].texture.viewDimension = wgpu::TextureViewDimension::e2D;
|
||||
terrain_entries[2].binding = 2;
|
||||
terrain_entries[2].visibility = wgpu::ShaderStage::Fragment;
|
||||
terrain_entries[2].texture.sampleType = wgpu::TextureSampleType::Uint;
|
||||
terrain_entries[2].texture.viewDimension = wgpu::TextureViewDimension::e2D;
|
||||
terrain_entries[3].binding = 3;
|
||||
terrain_entries[3].visibility = wgpu::ShaderStage::Fragment;
|
||||
terrain_entries[3].texture.sampleType = wgpu::TextureSampleType::Float;
|
||||
terrain_entries[3].texture.viewDimension = wgpu::TextureViewDimension::e2DArray;
|
||||
terrain_entries[4].binding = 4;
|
||||
terrain_entries[4].visibility = wgpu::ShaderStage::Fragment;
|
||||
terrain_entries[4].sampler.type = wgpu::SamplerBindingType::Filtering;
|
||||
wgpu::BindGroupLayoutDescriptor terrain_layout_desc;
|
||||
terrain_layout_desc.entryCount = 5;
|
||||
terrain_layout_desc.entries = terrain_entries;
|
||||
terrain_bind_group_layout_ = device_.CreateBindGroupLayout(&terrain_layout_desc);
|
||||
return scene_pipeline_ != nullptr;
|
||||
}
|
||||
|
||||
[[nodiscard]] auto create_pipeline(const wgpu::PipelineLayout &layout, const wgpu::ShaderModule &shader, const char *wgsl) -> wgpu::RenderPipeline {
|
||||
(void) wgsl;
|
||||
wgpu::ColorTargetState target;
|
||||
target.format = surface_format_;
|
||||
target.writeMask = wgpu::ColorWriteMask::All;
|
||||
wgpu::FragmentState fragment;
|
||||
fragment.module = shader;
|
||||
fragment.entryPoint = "fs_main";
|
||||
fragment.targetCount = 1;
|
||||
fragment.targets = ⌖
|
||||
wgpu::RenderPipelineDescriptor descriptor;
|
||||
descriptor.layout = layout;
|
||||
descriptor.vertex.module = shader;
|
||||
descriptor.vertex.entryPoint = "vs_main";
|
||||
descriptor.primitive.topology = wgpu::PrimitiveTopology::TriangleList;
|
||||
descriptor.fragment = &fragment;
|
||||
descriptor.multisample.count = 1;
|
||||
return device_.CreateRenderPipeline(&descriptor);
|
||||
}
|
||||
|
||||
auto configure(int width, int height) -> void {
|
||||
if (surface_ == nullptr || width <= 0 || height <= 0) return;
|
||||
if (width == surface_w_ && height == surface_h_) return;
|
||||
wgpu::SurfaceConfiguration config;
|
||||
config.device = device_;
|
||||
config.format = surface_format_;
|
||||
config.usage = wgpu::TextureUsage::RenderAttachment;
|
||||
config.width = static_cast<uint32_t>(width);
|
||||
config.height = static_cast<uint32_t>(height);
|
||||
config.presentMode = wgpu::PresentMode::Fifo;
|
||||
surface_.Configure(&config);
|
||||
surface_w_ = width;
|
||||
surface_h_ = height;
|
||||
}
|
||||
|
||||
auto make_target(image_target &target) -> void {
|
||||
wgpu::BufferDescriptor ubo_desc;
|
||||
ubo_desc.size = 16;
|
||||
ubo_desc.usage = wgpu::BufferUsage::Uniform | wgpu::BufferUsage::CopyDst;
|
||||
target.ubo = device_.CreateBuffer(&ubo_desc);
|
||||
}
|
||||
|
||||
auto upload_image(image_target &target, const ra3::render::image &frame) -> void {
|
||||
if (frame.empty()) return;
|
||||
target.width = static_cast<int>(frame.width());
|
||||
target.height = static_cast<int>(frame.height());
|
||||
if (target.texture == nullptr || target.tex_width != target.width || target.tex_height != target.height) {
|
||||
wgpu::TextureDescriptor descriptor;
|
||||
descriptor.size = {frame.width(), frame.height(), 1};
|
||||
descriptor.format = wgpu::TextureFormat::RGBA8Unorm;
|
||||
descriptor.usage = wgpu::TextureUsage::TextureBinding | wgpu::TextureUsage::CopyDst;
|
||||
descriptor.dimension = wgpu::TextureDimension::e2D;
|
||||
descriptor.mipLevelCount = 1;
|
||||
descriptor.sampleCount = 1;
|
||||
target.texture = device_.CreateTexture(&descriptor);
|
||||
target.view = target.texture.CreateView();
|
||||
target.tex_width = target.width;
|
||||
target.tex_height = target.height;
|
||||
if (!target.ubo) this->make_target(target);
|
||||
target.bind_group = this->make_image_bind_group(target);
|
||||
}
|
||||
wgpu::TexelCopyTextureInfo destination;
|
||||
destination.texture = target.texture;
|
||||
wgpu::TexelCopyBufferLayout layout;
|
||||
layout.bytesPerRow = frame.width() * 4;
|
||||
layout.rowsPerImage = frame.height();
|
||||
wgpu::Extent3D size = {frame.width(), frame.height(), 1};
|
||||
queue_.WriteTexture(&destination, frame.data(), static_cast<size_t>(frame.width()) * frame.height() * 4, &layout, &size);
|
||||
}
|
||||
|
||||
[[nodiscard]] auto make_image_bind_group(image_target &target) -> wgpu::BindGroup {
|
||||
wgpu::BindGroupEntry entries[3] = {};
|
||||
entries[0].binding = 0;
|
||||
entries[0].buffer = target.ubo;
|
||||
entries[0].size = 16;
|
||||
entries[1].binding = 1;
|
||||
entries[1].textureView = target.view;
|
||||
entries[2].binding = 2;
|
||||
entries[2].sampler = clamp_sampler_;
|
||||
wgpu::BindGroupDescriptor descriptor;
|
||||
descriptor.layout = scene_bind_group_layout_;
|
||||
descriptor.entryCount = 3;
|
||||
descriptor.entries = entries;
|
||||
return device_.CreateBindGroup(&descriptor);
|
||||
}
|
||||
|
||||
auto draw_image(wgpu::RenderPassEncoder &pass, image_target &target, const ra3::render::view_rect &dest, int window_w, int window_h) -> void {
|
||||
if (target.bind_group == nullptr || window_w <= 0 || window_h <= 0) return;
|
||||
const float rect[4] = {dest.x / static_cast<float>(window_w), dest.y / static_cast<float>(window_h),
|
||||
dest.w / static_cast<float>(window_w), dest.h / static_cast<float>(window_h)};
|
||||
queue_.WriteBuffer(target.ubo, 0, rect, sizeof(rect));
|
||||
pass.SetPipeline(scene_pipeline_);
|
||||
pass.SetBindGroup(0, target.bind_group);
|
||||
pass.Draw(3);
|
||||
}
|
||||
|
||||
[[nodiscard]] auto create_terrain(const ra3::terrain::gpu_terrain &terrain) -> bool {
|
||||
{
|
||||
wgpu::BufferDescriptor descriptor;
|
||||
descriptor.size = 80;
|
||||
descriptor.usage = wgpu::BufferUsage::Uniform | wgpu::BufferUsage::CopyDst;
|
||||
terrain_.ubo = device_.CreateBuffer(&descriptor);
|
||||
}
|
||||
terrain_.height = this->make_texture(wgpu::TextureFormat::R16Uint, terrain.width, terrain.height, 1);
|
||||
terrain_.cell = this->make_texture(wgpu::TextureFormat::RGBA16Uint, terrain.width, terrain.height, 1);
|
||||
terrain_.atlas = this->make_texture(wgpu::TextureFormat::RGBA8Unorm, terrain.layer_size, terrain.layer_size, terrain.layer_count);
|
||||
terrain_.height_view = terrain_.height.CreateView();
|
||||
terrain_.cell_view = terrain_.cell.CreateView();
|
||||
terrain_.atlas_view = terrain_.atlas.CreateView();
|
||||
|
||||
this->write_texture(terrain_.height, terrain.heights.data(), terrain.width, terrain.height, 1, 2);
|
||||
this->write_texture(terrain_.cell, terrain.cell_data.data(), terrain.width, terrain.height, 1, 8);
|
||||
this->write_texture(terrain_.atlas, terrain.layers.data(), terrain.layer_size, terrain.layer_size, terrain.layer_count, 4);
|
||||
|
||||
wgpu::BindGroupEntry entries[5] = {};
|
||||
entries[0].binding = 0;
|
||||
entries[0].buffer = terrain_.ubo;
|
||||
entries[0].size = 80;
|
||||
entries[1].binding = 1;
|
||||
entries[1].textureView = terrain_.height_view;
|
||||
entries[2].binding = 2;
|
||||
entries[2].textureView = terrain_.cell_view;
|
||||
entries[3].binding = 3;
|
||||
entries[3].textureView = terrain_.atlas_view;
|
||||
entries[4].binding = 4;
|
||||
entries[4].sampler = repeat_sampler_;
|
||||
wgpu::BindGroupDescriptor bind_desc;
|
||||
bind_desc.layout = terrain_bind_group_layout_;
|
||||
bind_desc.entryCount = 5;
|
||||
bind_desc.entries = entries;
|
||||
terrain_.bind_group = device_.CreateBindGroup(&bind_desc);
|
||||
|
||||
terrain_.pipeline = this->create_pipeline(terrain_pipeline_layout(), terrain_shader_, ra3_shaders::webgpu_terrain_wgsl);
|
||||
return terrain_.pipeline != nullptr && terrain_.bind_group != nullptr;
|
||||
}
|
||||
|
||||
[[nodiscard]] auto terrain_pipeline_layout() -> wgpu::PipelineLayout {
|
||||
if (terrain_pipeline_layout_ == nullptr) {
|
||||
wgpu::PipelineLayoutDescriptor descriptor;
|
||||
descriptor.bindGroupLayoutCount = 1;
|
||||
descriptor.bindGroupLayouts = &terrain_bind_group_layout_;
|
||||
terrain_pipeline_layout_ = device_.CreatePipelineLayout(&descriptor);
|
||||
}
|
||||
return terrain_pipeline_layout_;
|
||||
}
|
||||
|
||||
[[nodiscard]] auto make_texture(wgpu::TextureFormat format, uint32_t width, uint32_t height, uint32_t layers) -> wgpu::Texture {
|
||||
wgpu::TextureDescriptor descriptor;
|
||||
descriptor.size = {width, height, layers};
|
||||
descriptor.format = format;
|
||||
descriptor.usage = wgpu::TextureUsage::TextureBinding | wgpu::TextureUsage::CopyDst;
|
||||
descriptor.dimension = wgpu::TextureDimension::e2D;
|
||||
descriptor.mipLevelCount = 1;
|
||||
descriptor.sampleCount = 1;
|
||||
return device_.CreateTexture(&descriptor);
|
||||
}
|
||||
|
||||
auto write_texture(const wgpu::Texture &texture, const void *data, uint32_t width, uint32_t height, uint32_t layers, uint32_t bytes_per_pixel)
|
||||
-> void {
|
||||
wgpu::TexelCopyTextureInfo destination;
|
||||
destination.texture = texture;
|
||||
wgpu::TexelCopyBufferLayout layout;
|
||||
layout.bytesPerRow = width * bytes_per_pixel;
|
||||
layout.rowsPerImage = height;
|
||||
wgpu::Extent3D size = {width, height, layers};
|
||||
queue_.WriteTexture(&destination, data, static_cast<size_t>(width) * height * layers * bytes_per_pixel, &layout, &size);
|
||||
}
|
||||
|
||||
/** Clear, run `record`, submit and present. */
|
||||
template<typename Record>
|
||||
[[nodiscard]] auto draw(std::array<float, 4> clear, Record &&record) -> bool {
|
||||
const auto [window_w, window_h] = this->window_size();
|
||||
this->configure(window_w, window_h);
|
||||
wgpu::SurfaceTexture surface_texture;
|
||||
surface_.GetCurrentTexture(&surface_texture);
|
||||
if (surface_texture.status != wgpu::SurfaceGetCurrentTextureStatus::SuccessOptimal &&
|
||||
surface_texture.status != wgpu::SurfaceGetCurrentTextureStatus::SuccessSuboptimal) {
|
||||
return false;
|
||||
}
|
||||
wgpu::TextureView view = surface_texture.texture.CreateView();
|
||||
wgpu::RenderPassColorAttachment color;
|
||||
color.view = view;
|
||||
color.depthSlice = 0xFFFFFFFFu; // kWGPU_DEPTH_SLICE_UNDEFINED
|
||||
color.loadOp = wgpu::LoadOp::Clear;
|
||||
color.storeOp = wgpu::StoreOp::Store;
|
||||
color.clearValue = {clear[0], clear[1], clear[2], clear[3]};
|
||||
wgpu::RenderPassDescriptor pass_desc;
|
||||
pass_desc.colorAttachmentCount = 1;
|
||||
pass_desc.colorAttachments = &color;
|
||||
wgpu::CommandEncoder encoder = device_.CreateCommandEncoder();
|
||||
wgpu::RenderPassEncoder pass = encoder.BeginRenderPass(&pass_desc);
|
||||
record(pass, window_w, window_h);
|
||||
pass.End();
|
||||
wgpu::CommandBuffer commands = encoder.Finish();
|
||||
queue_.Submit(1, &commands);
|
||||
// WebGPU presents implicitly at the end of the frame; the port's
|
||||
// wgpuSurfacePresent is intentionally unsupported.
|
||||
this->mark_frame();
|
||||
return true;
|
||||
}
|
||||
|
||||
/** Publish a coarse frame counter to the page for the fps probe. */
|
||||
auto mark_frame() const -> void {
|
||||
++frames_;
|
||||
if (frames_ % 15U == 0U) {
|
||||
EM_ASM({ self.postMessage({ openra3Frames: $0 }); }, static_cast<int>(frames_));
|
||||
}
|
||||
}
|
||||
|
||||
wgpu::Instance instance_;
|
||||
wgpu::Device device_;
|
||||
wgpu::Queue queue_;
|
||||
wgpu::Surface surface_;
|
||||
wgpu::TextureFormat surface_format_ = wgpu::TextureFormat::BGRA8Unorm;
|
||||
int surface_w_ = 0;
|
||||
int surface_h_ = 0;
|
||||
|
||||
wgpu::ShaderModule scene_shader_;
|
||||
wgpu::ShaderModule terrain_shader_;
|
||||
wgpu::BindGroupLayout scene_bind_group_layout_;
|
||||
wgpu::BindGroupLayout terrain_bind_group_layout_;
|
||||
wgpu::PipelineLayout scene_pipeline_layout_;
|
||||
wgpu::PipelineLayout terrain_pipeline_layout_;
|
||||
wgpu::RenderPipeline scene_pipeline_;
|
||||
wgpu::Sampler clamp_sampler_;
|
||||
wgpu::Sampler repeat_sampler_;
|
||||
|
||||
image_target scene_;
|
||||
image_target label_;
|
||||
image_target minimap_;
|
||||
terrain_target terrain_;
|
||||
bool terrain_ready_ = false;
|
||||
|
||||
std::deque<ra3::render::ui_event> events_;
|
||||
std::array<bool, static_cast<std::size_t>(ra3::wasmgl::detail::move_key::count)> held_{};
|
||||
mutable unsigned frames_ = 0U;
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
export module ra3.webgpu;
|
||||
|
||||
import std;
|
||||
|
||||
export import ra3.core;
|
||||
export import ra3.render;
|
||||
export import ra3.client;
|
||||
|
||||
/**
|
||||
* Fallback WebGPU backend for builds without Emscripten. `init` fails so the
|
||||
* caller can select another display; the class name matches the real
|
||||
* `ra3.webgpu` module so the backend factory in `ra3.display` compiles
|
||||
* unchanged.
|
||||
*/
|
||||
export namespace ra3::webgpu {
|
||||
class webgpu_display final : public ra3::client::display {
|
||||
public:
|
||||
[[nodiscard]] auto init(const ra3::client::display_options &) -> bool override { return false; }
|
||||
[[nodiscard]] auto present(const ra3::render::image &, const ra3::render::view_rect &, bool) -> bool override { return false; }
|
||||
[[nodiscard]] auto poll_event(ra3::render::ui_event &) -> bool override { return false; }
|
||||
[[nodiscard]] auto window_size() const -> std::pair<int, int> override { return {0, 0}; }
|
||||
[[nodiscard]] auto key_down(ra3::render::ui_key) const -> bool override { return false; }
|
||||
auto shutdown() -> void override {}
|
||||
[[nodiscard]] auto name() const -> std::string_view override { return "webgpu(null)"; }
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user