载入中...
搜索中...
未找到
Graphics.cpp
浏览该文件的文档.
2#include "graphics/Batcher.h"
4#include "graphics/Light.h"
5#include "graphics/Mesh.h"
7#include "graphics/Shader.h"
8#include "graphics/Shadow.h"
9#include "graphics/Texture.h"
13
14#include "common/Exception.h"
15#include "common/config.h"
17#include "image/Image.h"
18#include "image/ImageData.h"
19
20#include <assimp/scene.h>
21
22#include <algorithm>
23#include <cstring>
24#include <map>
25#include <mutex>
26
27#include <glm/gtc/constants.hpp>
28
29#include <SDL2/SDL.h>
30#include <SDL2/SDL_syswm.h>
31
32#if defined(_WIN32)
33#include <windows.h>
34#endif
35
36#if defined(__EMSCRIPTEN__)
37#include <emscripten/emscripten.h>
38#endif
39
40namespace eve::graphics::webgpu {
41
42namespace {
43
45WGPUStringView sv(const char *s) {
46 return WGPUStringView{s, s ? std::strlen(s) : 0};
47}
48
49// Forward declaration for the readback helper (defined in the Readback section).
50bool copyTextureToCpu(wgpu::Instance &instance, wgpu::Device &device, wgpu::Queue &queue,
51 wgpu::Texture src, int width, int height, std::vector<uint8_t> &outRgba);
52
53// A shared default sampler used when no texture is provided.
54wgpu::Sampler createLinearSampler(wgpu::Device &dev) {
55 WGPUSamplerDescriptor d{};
56 d.label = sv("eve_linear");
57 d.addressModeU = WGPUAddressMode_ClampToEdge;
58 d.addressModeV = WGPUAddressMode_ClampToEdge;
59 d.addressModeW = WGPUAddressMode_ClampToEdge;
60 d.magFilter = WGPUFilterMode_Linear;
61 d.minFilter = WGPUFilterMode_Linear;
62 d.mipmapFilter = WGPUMipmapFilterMode_Linear;
63 d.lodMinClamp = 0.f;
64 d.lodMaxClamp = 1000.f;
65 d.maxAnisotropy = 1;
66 return dev.CreateSampler(reinterpret_cast<const wgpu::SamplerDescriptor*>(&d));
67}
68
69int nextPOT(int v) {
70 int p = 1;
71 while (p < v) p <<= 1;
72 return p;
73}
74
75} // namespace
76
78 for (uint32_t i = 0; i < kFramesInFlight; ++i) {
79 uboArenas.emplace_back();
80 vertexArenas.emplace_back();
81 }
82}
83
84Graphics::~Graphics() = default;
85
86// ---------------------------------------------------------------------------
87// Init
88// ---------------------------------------------------------------------------
89
90void Graphics::initWithWindow(void *nativeWindow) {
91 sdlWindow = nativeWindow;
92 if (deviceInitDone) return;
93
94 createInstanceAndAdapter();
95 requestDevice();
96 if (!device) throw Exception("WebGPU: device request failed");
97
98 queue = device.GetQueue();
99
100 // ---- Surface ----
101 WGPUSurfaceDescriptor surfDesc{};
102 surfDesc.label = sv("eve_surface");
103#if defined(__EMSCRIPTEN__)
104 WGPUEmscriptenSurfaceSourceCanvasHTMLSelector canvasSel{};
105 canvasSel.chain.sType = WGPUSType_EmscriptenSurfaceSourceCanvasHTMLSelector;
106 canvasSel.selector = sv("#canvas");
107 surfDesc.nextInChain = &canvasSel.chain;
108 surface = instance.CreateSurface(reinterpret_cast<const wgpu::SurfaceDescriptor*>(&surfDesc));
109 surfaceFormat = WGPUTextureFormat_BGRA8Unorm;
110#else
111 SDL_SysWMinfo wminfo;
112 SDL_VERSION(&wminfo.version);
113 if (!SDL_GetWindowWMInfo(static_cast<SDL_Window *>(sdlWindow), &wminfo))
114 throw Exception("WebGPU: SDL_GetWindowWMInfo failed: %s", SDL_GetError());
115#if defined(_WIN32)
116 WGPUSurfaceSourceWindowsHWND winChain{};
117 winChain.chain.sType = WGPUSType_SurfaceSourceWindowsHWND;
118 winChain.hwnd = wminfo.info.win.window;
119 winChain.hinstance = GetModuleHandle(nullptr);
120 surfDesc.nextInChain = &winChain.chain;
121 surface = instance.CreateSurface(reinterpret_cast<const wgpu::SurfaceDescriptor*>(&surfDesc));
122#elif defined(__linux__)
123 if (wminfo.subsystem == SDL_SYSWM_X11) {
124 WGPUSurfaceSourceXlibWindow x11Chain{};
125 x11Chain.chain.sType = WGPUSType_SurfaceSourceXlibWindow;
126 x11Chain.display = wminfo.info.x11.display;
127 x11Chain.window = wminfo.info.x11.window;
128 surfDesc.nextInChain = &x11Chain.chain;
129 surface = instance.CreateSurface(reinterpret_cast<const wgpu::SurfaceDescriptor*>(&surfDesc));
130 } else if (wminfo.subsystem == SDL_SYSWM_WAYLAND) {
131 WGPUSurfaceSourceWaylandSurface wlChain{};
132 wlChain.chain.sType = WGPUSType_SurfaceSourceWaylandSurface;
133 wlChain.display = wminfo.info.wl.display;
134 wlChain.surface = wminfo.info.wl.surface;
135 surfDesc.nextInChain = &wlChain.chain;
136 surface = instance.CreateSurface(reinterpret_cast<const wgpu::SurfaceDescriptor*>(&surfDesc));
137 } else {
138 throw Exception("WebGPU: unsupported SDL window subsystem on Linux");
139 }
140#elif defined(__APPLE__)
141 throw Exception("WebGPU: native macOS surface (Metal layer) not yet supported; "
142 "use the Emscripten browser build or the Vulkan backend on macOS");
143#else
144 throw Exception("WebGPU: unsupported native platform for surface creation");
145#endif
146 if (!surface) throw Exception("WebGPU: surface creation failed");
147 // Preferred surface format comes from wgpuSurfaceGetCapabilities in the
148 // current ABI (Surface::GetPreferredFormat was removed).
149 WGPUSurfaceCapabilities caps{};
150 if (wgpuSurfaceGetCapabilities(surface.Get(), adapter.Get(), &caps) == WGPUStatus_Success &&
151 caps.formatCount > 0) {
152 surfaceFormat = caps.formats[0];
153 }
154 wgpuSurfaceCapabilitiesFreeMembers(&caps);
155#endif
156
157 swapchainConfigured = false;
158 // Bind group layouts / pipelines must exist before the default textures
159 // (texture bind groups reference the 2D / mesh3d layouts), and the shadow
160 // depth array must exist before mesh bind groups are built at flush time.
161 createPipelineResources();
162 createShadowResources();
163 createDefaultTextures();
164 initialized = true;
165}
166
167void Graphics::createInstanceAndAdapter() {
168 instance = wgpu::CreateInstance();
169 if (!instance) throw Exception("WebGPU: wgpuCreateInstance failed");
170
171 WGPURequestAdapterOptions opts{};
172 opts.compatibleSurface = surface.Get();
173 opts.powerPreference = WGPUPowerPreference_HighPerformance;
174
175 adapterReceived = false;
176 WGPURequestAdapterCallbackInfo cbInfo{};
177 cbInfo.nextInChain = nullptr;
178 cbInfo.mode = WGPUCallbackMode_AllowProcessEvents;
179 cbInfo.callback = [](WGPURequestAdapterStatus status, WGPUAdapter a, WGPUStringView msg,
180 void *userdata1, void * /*userdata2*/) {
181 auto *self = static_cast<Graphics *>(userdata1);
182 if (status == WGPURequestAdapterStatus_Success && a) {
183 self->adapter = wgpu::Adapter(a);
184 } else {
185 self->adapterError =
186 msg.data ? std::string(msg.data, msg.length) : "unknown adapter error";
187 }
188 self->adapterReceived.store(true);
189 };
190 cbInfo.userdata1 = this;
191 cbInfo.userdata2 = nullptr;
192
193 wgpuInstanceRequestAdapter(instance.Get(), &opts, cbInfo);
194 waitForAdapter();
195 if (!adapter) {
196 throw Exception("WebGPU: no adapter found (%s)", adapterError.c_str());
197 }
198}
199
200void Graphics::requestDevice() {
201 WGPUDeviceDescriptor devDesc{};
202 devDesc.label = sv("eve_device");
203
204 deviceReceived = false;
205 WGPURequestDeviceCallbackInfo cbInfo{};
206 cbInfo.nextInChain = nullptr;
207 cbInfo.mode = WGPUCallbackMode_AllowProcessEvents;
208 cbInfo.callback = [](WGPURequestDeviceStatus status, WGPUDevice d, WGPUStringView msg,
209 void *userdata1, void * /*userdata2*/) {
210 auto *self = static_cast<Graphics *>(userdata1);
211 if (status == WGPURequestDeviceStatus_Success && d) {
212 self->device = wgpu::Device(d);
213 // Default device limits support 8+ uniform/vertex/storage buffers.
214 self->deviceReceived.store(true);
215 self->deviceError.clear();
216 } else {
217 self->deviceError =
218 msg.data ? std::string(msg.data, msg.length) : "unknown device error";
219 self->deviceReceived.store(true);
220 }
221 };
222 cbInfo.userdata1 = this;
223 cbInfo.userdata2 = nullptr;
224
225 wgpuAdapterRequestDevice(adapter.Get(), &devDesc, cbInfo);
226 waitForDevice();
227 if (!device) {
228 throw Exception("WebGPU: device request failed (%s)", deviceError.c_str());
229 }
230}
231
232void Graphics::waitForAdapter() {
233 while (!adapterReceived.load()) {
234#if defined(__EMSCRIPTEN__)
235 // Yield to the browser event loop so the JS requestAdapter promise can
236 // resolve and fire the callback, then flush it with ProcessEvents.
237 emscripten_sleep(0);
238#endif
239 wgpuInstanceProcessEvents(instance.Get());
240 }
241}
242
243void Graphics::waitForDevice() {
244 while (!deviceReceived.load()) {
245#if defined(__EMSCRIPTEN__)
246 emscripten_sleep(0);
247#endif
248 wgpuInstanceProcessEvents(instance.Get());
249 }
250}
251
253 if (vsyncEnabled == enabled) return;
255 markSwapchainDirty();
256}
257
258void Graphics::setViewportSize(int width, int height, int pixelwidth, int pixelheight) {
259 logicalW = width;
260 logicalH = height;
261 pixelW = pixelwidth;
262 pixelH = pixelheight;
263 if (width <= 0 || height <= 0) return;
264 if (initialized) configureSurface(width, height);
265}
266
267void Graphics::configureSurface(int width, int height) {
268 if (!surface || !device || width <= 0 || height <= 0) return;
269 WGPUSurfaceConfiguration cfg{};
270 cfg.nextInChain = nullptr;
271 cfg.device = device.Get();
272 cfg.width = static_cast<uint32_t>(width);
273 cfg.height = static_cast<uint32_t>(height);
274 cfg.format = surfaceFormat;
275 cfg.usage = WGPUTextureUsage_RenderAttachment;
276 cfg.viewFormatCount = 0;
277 cfg.viewFormats = nullptr;
278 // emdawnwebgpu only accepts Fifo / Undefined present modes.
279 cfg.presentMode = WGPUPresentMode_Fifo;
280 // Auto (0) maps to an empty slot in emdawnwebgpu's JS CompositeAlphaMode
281 // table �?`alphaMode: undefined`, which some browsers reject. Use Opaque,
282 // which maps to the JS value 'opaque' used by the working replica.
283 cfg.alphaMode = WGPUCompositeAlphaMode_Opaque;
284 surface.Configure(reinterpret_cast<const wgpu::SurfaceConfiguration*>(&cfg));
285 swapchainConfigured = true;
286 // Recreate the offscreen targets at the new size.
287 int w = pixelW > 0 ? pixelW : width;
288 int h = pixelH > 0 ? pixelH : height;
289 if (w > 0 && h > 0) {
290 createSceneColorResources(w, h);
291 createShadowResources();
292 }
293}
294
295void Graphics::rebuildSwapchainIfNeeded() {
296 if (!swapchainConfigured && surface && logicalW > 0 && logicalH > 0) {
297 configureSurface(logicalW, logicalH);
298 }
299}
300
301// ---------------------------------------------------------------------------
302// Default / placeholder resources
303// ---------------------------------------------------------------------------
304
305void Graphics::createDefaultTextures() {
306 uint8_t white[4] = {255, 255, 255, 255};
307 whiteTexture = static_cast<GpuTexture *>(newTexture(1, 1, white, false, false)->gpuHandle);
308
309 // Flat normal (0.5,0.5,1) in RGBA8 �?sampled as (0,0,1) normal.
310 uint8_t flatNrm[4] = {128, 128, 255, 255};
311 flatNormalTexture = static_cast<GpuTexture *>(newTexture(1, 1, flatNrm, false, false)->gpuHandle);
312 flatNormalTexture3D = flatNormalTexture;
313
314 uint8_t flatH[4] = {0, 0, 0, 255};
315 flatHeightTexture3D = static_cast<GpuTexture *>(newTexture(1, 1, flatH, false, false)->gpuHandle);
316
317 // 1x1 depth placeholder for the mesh3d scene-depth binding (9). Only X-ray
318 // shaders sample it; every other mesh draw binds this unused depth view.
319 {
320 auto *gpu = new GpuTexture();
321 WGPUTextureDescriptor td{};
322 td.label = sv("eve_flat_depth");
323 td.dimension = WGPUTextureDimension_2D;
324 td.size = {1, 1, 1};
325 td.sampleCount = 1;
326 td.format = WGPUTextureFormat_Depth32Float;
327 td.mipLevelCount = 1;
328 td.usage = WGPUTextureUsage_TextureBinding;
329 gpu->texture = device.CreateTexture(reinterpret_cast<const wgpu::TextureDescriptor*>(&td));
330 WGPUTextureViewDescriptor vd{};
331 vd.format = WGPUTextureFormat_Depth32Float;
332 vd.dimension = WGPUTextureViewDimension_2D;
333 vd.baseMipLevel = 0;
334 vd.mipLevelCount = 1;
335 vd.baseArrayLayer = 0;
336 vd.arrayLayerCount = 1;
337 gpu->view = gpu->texture.CreateView(reinterpret_cast<const wgpu::TextureViewDescriptor*>(&vd));
338 gpu->width = 1;
339 gpu->height = 1;
340 flatDepthTexture3D = gpu;
341 }
342
343 // 1x1 white cubemap.
344 uint8_t cubeFace[4] = {255, 255, 255, 255};
345 uint8_t cubeData[24];
346 for (int f = 0; f < 6; ++f) std::memcpy(cubeData + f * 4, cubeFace, 4);
347 defaultEnvCubemap =
348 static_cast<GpuTexture *>(newCubemap(1, cubeData)->gpuHandle);
349
350 // Shared filtering sampler for WGSL bindings declared as plain `sampler`.
351 TextureSampler def;
352 mainSampler = makeSampler(def, 1);
353}
354
355// ---------------------------------------------------------------------------
356// Pipeline resource creation
357// ---------------------------------------------------------------------------
358
359void Graphics::createPipelineResources() {
360 create2DPipelines();
361 createMesh3DPipelines();
362 createShadowPipelines();
363 createGbufferPipelines();
364 createVoxelPipelines();
365}
366
367// ---------------------------------------------------------------------------
368// Bind group layouts
369// ---------------------------------------------------------------------------
370
371wgpu::BindGroupLayout Graphics::make2DBindGroupLayout() {
372 WGPUBindGroupLayoutEntry entries[5]{};
373 // 0: color texture
374 entries[0].binding = 0;
375 entries[0].visibility = WGPUShaderStage_Fragment;
376 entries[0].texture.sampleType = WGPUTextureSampleType_Float;
377 entries[0].texture.viewDimension = WGPUTextureViewDimension_2D;
378 entries[0].texture.multisampled = false;
379 // 1: depth texture
380 entries[1].binding = 1;
381 entries[1].visibility = WGPUShaderStage_Fragment;
382 entries[1].texture.sampleType = WGPUTextureSampleType_Float;
383 entries[1].texture.viewDimension = WGPUTextureViewDimension_2D;
384 // 2: color sampler
385 entries[2].binding = 2;
386 entries[2].visibility = WGPUShaderStage_Fragment;
387 entries[2].sampler.type = WGPUSamplerBindingType_Filtering;
388 // 3: depth sampler
389 entries[3].binding = 3;
390 entries[3].visibility = WGPUShaderStage_Fragment;
391 entries[3].sampler.type = WGPUSamplerBindingType_Filtering;
392 // 4: Externals UBO (push-constant replacement, dynamic offset). Sized for
393 // the largest consumer: custom 2D shaders (128 B) and lit2D (Lighting2DUBO).
394 entries[4].binding = 4;
395 entries[4].visibility = WGPUShaderStage_Vertex | WGPUShaderStage_Fragment;
396 entries[4].buffer.type = WGPUBufferBindingType_Uniform;
397 entries[4].buffer.hasDynamicOffset = true;
398 entries[4].buffer.minBindingSize =
399 std::max<uint32_t>(Shader::kPushConstantBytes, uint32_t(sizeof(Lighting2DUBO)));
400
401 WGPUBindGroupLayoutDescriptor desc{};
402 desc.label = sv("eve_2d");
403 desc.entryCount = 5;
404 desc.entries = entries;
405 return device.CreateBindGroupLayout(reinterpret_cast<const wgpu::BindGroupLayoutDescriptor*>(&desc));
406}
407
408wgpu::BindGroupLayout Graphics::makeMesh3DBindGroupLayout() {
409 WGPUBindGroupLayoutEntry entries[10]{};
410 // 0: Frame UBO (dynamic)
411 entries[0].binding = 0;
412 entries[0].visibility = WGPUShaderStage_Vertex | WGPUShaderStage_Fragment;
413 entries[0].buffer.type = WGPUBufferBindingType_Uniform;
414 entries[0].buffer.hasDynamicOffset = true;
415 entries[0].buffer.minBindingSize = sizeof(Mesh3DUBO);
416 // 1: albedo
417 entries[1].binding = 1;
418 entries[1].visibility = WGPUShaderStage_Fragment;
419 entries[1].texture.sampleType = WGPUTextureSampleType_Float;
420 entries[1].texture.viewDimension = WGPUTextureViewDimension_2D;
421 // 2: normal
422 entries[2].binding = 2;
423 entries[2].visibility = WGPUShaderStage_Fragment;
424 entries[2].texture.sampleType = WGPUTextureSampleType_Float;
425 entries[2].texture.viewDimension = WGPUTextureViewDimension_2D;
426 // 3: env cubemap
427 entries[3].binding = 3;
428 entries[3].visibility = WGPUShaderStage_Fragment;
429 entries[3].texture.sampleType = WGPUTextureSampleType_Float;
430 entries[3].texture.viewDimension = WGPUTextureViewDimension_Cube;
431 // 4: Shadow UBO (dynamic)
432 entries[4].binding = 4;
433 entries[4].visibility = WGPUShaderStage_Fragment;
434 entries[4].buffer.type = WGPUBufferBindingType_Uniform;
435 entries[4].buffer.hasDynamicOffset = true;
436 entries[4].buffer.minBindingSize = sizeof(ShadowUBO);
437 // 5: shadow depth array
438 entries[5].binding = 5;
439 entries[5].visibility = WGPUShaderStage_Fragment;
440 entries[5].texture.sampleType = WGPUTextureSampleType_Depth;
441 entries[5].texture.viewDimension = WGPUTextureViewDimension_2DArray;
442 // 6: height
443 entries[6].binding = 6;
444 entries[6].visibility = WGPUShaderStage_Fragment;
445 entries[6].texture.sampleType = WGPUTextureSampleType_Float;
446 entries[6].texture.viewDimension = WGPUTextureViewDimension_2D;
447 // 7: shared filtering sampler (WGSL `mainSamp`)
448 entries[7].binding = 7;
449 entries[7].visibility = WGPUShaderStage_Fragment;
450 entries[7].sampler.type = WGPUSamplerBindingType_Filtering;
451 // 8: shadow comparison sampler
452 entries[8].binding = 8;
453 entries[8].visibility = WGPUShaderStage_Fragment;
454 entries[8].sampler.type = WGPUSamplerBindingType_Comparison;
455 // 9: scene depth (G-buffer hwDepth; X-ray shaders sample it). Depth view,
456 // sampled via textureSampleLevel with the shared mainSamp (binding 7).
457 entries[9].binding = 9;
458 entries[9].visibility = WGPUShaderStage_Fragment;
459 entries[9].texture.sampleType = WGPUTextureSampleType_Depth;
460 entries[9].texture.viewDimension = WGPUTextureViewDimension_2D;
461
462 WGPUBindGroupLayoutDescriptor desc{};
463 desc.label = sv("eve_mesh3d");
464 desc.entryCount = 10;
465 desc.entries = entries;
466 return device.CreateBindGroupLayout(reinterpret_cast<const wgpu::BindGroupLayoutDescriptor*>(&desc));
467}
468
469wgpu::BindGroupLayout Graphics::makeShadowBindGroupLayout() {
470 WGPUBindGroupLayoutEntry entries[1]{};
471 entries[0].binding = 0;
472 entries[0].visibility = WGPUShaderStage_Vertex;
473 entries[0].buffer.type = WGPUBufferBindingType_Uniform;
474 entries[0].buffer.hasDynamicOffset = true;
475 entries[0].buffer.minBindingSize = 64; // mat4 mvp
476
477 WGPUBindGroupLayoutDescriptor desc{};
478 desc.label = sv("eve_shadow");
479 desc.entryCount = 1;
480 desc.entries = entries;
481 return device.CreateBindGroupLayout(reinterpret_cast<const wgpu::BindGroupLayoutDescriptor*>(&desc));
482}
483
484wgpu::BindGroupLayout Graphics::makeGbufferBindGroupLayout() {
485 WGPUBindGroupLayoutEntry entries[3]{};
486 // 0: Push UBO (dynamic; 128 bytes)
487 entries[0].binding = 0;
488 entries[0].visibility = WGPUShaderStage_Vertex | WGPUShaderStage_Fragment;
489 entries[0].buffer.type = WGPUBufferBindingType_Uniform;
490 entries[0].buffer.hasDynamicOffset = true;
491 entries[0].buffer.minBindingSize = 128;
492 // 1: albedo
493 entries[1].binding = 1;
494 entries[1].visibility = WGPUShaderStage_Fragment;
495 entries[1].texture.sampleType = WGPUTextureSampleType_Float;
496 entries[1].texture.viewDimension = WGPUTextureViewDimension_2D;
497 // 2: sampler
498 entries[2].binding = 2;
499 entries[2].visibility = WGPUShaderStage_Fragment;
500 entries[2].sampler.type = WGPUSamplerBindingType_Filtering;
501
502 WGPUBindGroupLayoutDescriptor desc{};
503 desc.label = sv("eve_gbuffer");
504 desc.entryCount = 3;
505 desc.entries = entries;
506 return device.CreateBindGroupLayout(reinterpret_cast<const wgpu::BindGroupLayoutDescriptor*>(&desc));
507}
508
509wgpu::BindGroupLayout Graphics::makeVoxelBindGroupLayout() {
510 WGPUBindGroupLayoutEntry entries[3]{};
511 // 0: PC UBO (dynamic)
512 entries[0].binding = 0;
513 entries[0].visibility = WGPUShaderStage_Vertex;
514 entries[0].buffer.type = WGPUBufferBindingType_Uniform;
515 entries[0].buffer.hasDynamicOffset = true;
516 entries[0].buffer.minBindingSize = 112;
517 // 1: atlas texture
518 entries[1].binding = 1;
519 entries[1].visibility = WGPUShaderStage_Fragment;
520 entries[1].texture.sampleType = WGPUTextureSampleType_Float;
521 entries[1].texture.viewDimension = WGPUTextureViewDimension_2D;
522 // 2: atlas sampler
523 entries[2].binding = 2;
524 entries[2].visibility = WGPUShaderStage_Fragment;
525 entries[2].sampler.type = WGPUSamplerBindingType_Filtering;
526
527 WGPUBindGroupLayoutDescriptor desc{};
528 desc.label = sv("eve_voxel");
529 desc.entryCount = 3;
530 desc.entries = entries;
531 return device.CreateBindGroupLayout(reinterpret_cast<const wgpu::BindGroupLayoutDescriptor*>(&desc));
532}
533
534wgpu::PipelineLayout Graphics::make2DPipelineLayout() {
535 WGPUBindGroupLayout bgl = tex2DSetLayout.Get();
536 WGPUPipelineLayoutDescriptor d{};
537 d.label = sv("eve_2d_layout");
538 d.bindGroupLayoutCount = 1;
539 d.bindGroupLayouts = &bgl;
540 return device.CreatePipelineLayout(reinterpret_cast<const wgpu::PipelineLayoutDescriptor*>(&d));
541}
542
543wgpu::PipelineLayout Graphics::makeMesh3DPipelineLayout() {
544 WGPUBindGroupLayout bgl = mesh3dSetLayout.Get();
545 WGPUPipelineLayoutDescriptor d{};
546 d.label = sv("eve_mesh3d_layout");
547 d.bindGroupLayoutCount = 1;
548 d.bindGroupLayouts = &bgl;
549 return device.CreatePipelineLayout(reinterpret_cast<const wgpu::PipelineLayoutDescriptor*>(&d));
550}
551
552wgpu::PipelineLayout Graphics::makeShadowPipelineLayout() {
553 WGPUBindGroupLayout bgl = shadowSetLayout.Get();
554 WGPUPipelineLayoutDescriptor d{};
555 d.label = sv("eve_shadow_layout");
556 d.bindGroupLayoutCount = 1;
557 d.bindGroupLayouts = &bgl;
558 return device.CreatePipelineLayout(reinterpret_cast<const wgpu::PipelineLayoutDescriptor*>(&d));
559}
560
561wgpu::PipelineLayout Graphics::makeGbufferPipelineLayout() {
562 WGPUBindGroupLayout bgl = gbufferSetLayout.Get();
563 WGPUPipelineLayoutDescriptor d{};
564 d.label = sv("eve_gbuffer_layout");
565 d.bindGroupLayoutCount = 1;
566 d.bindGroupLayouts = &bgl;
567 return device.CreatePipelineLayout(reinterpret_cast<const wgpu::PipelineLayoutDescriptor*>(&d));
568}
569
570wgpu::PipelineLayout Graphics::makeVoxelPipelineLayout() {
571 WGPUBindGroupLayout bgl = voxelSetLayout.Get();
572 WGPUPipelineLayoutDescriptor d{};
573 d.label = sv("eve_voxel_layout");
574 d.bindGroupLayoutCount = 1;
575 d.bindGroupLayouts = &bgl;
576 return device.CreatePipelineLayout(reinterpret_cast<const wgpu::PipelineLayoutDescriptor*>(&d));
577}
578
579// ---------------------------------------------------------------------------
580// Pipelines
581// ---------------------------------------------------------------------------
582
583namespace {
584
585// Returns a RAII-held module: the caller must keep it alive across
586// CreateRenderPipeline. Returning a raw WGPUShaderModule would let the
587// temporary wgpu::ShaderModule destructor Release() the handle, dropping it
588// from emdawnwebgpu's jsObjects before the pipeline can reference it.
589wgpu::ShaderModule makeWgslModule(wgpu::Device &dev, const char *wgsl) {
590 WGPUShaderSourceWGSL wgslDesc{};
591 wgslDesc.chain.sType = WGPUSType_ShaderSourceWGSL;
592 wgslDesc.code = sv(wgsl);
593 WGPUShaderModuleDescriptor desc{};
594 desc.nextInChain = &wgslDesc.chain;
595 return dev.CreateShaderModule(reinterpret_cast<const wgpu::ShaderModuleDescriptor*>(&desc));
596}
597
598// Builds a WGSL shader-module descriptor (the chained WGPUShaderSourceWGSL is
599// static so its chain pointer stays valid). The returned descriptor is a value
600// that must outlive the CreateShaderModule call.
601WGPUShaderModuleDescriptor mdDesc(const std::string &code) {
602 static WGPUShaderSourceWGSL wd{};
603 wd.chain.sType = WGPUSType_ShaderSourceWGSL;
604 wd.code = sv(code.c_str());
605 WGPUShaderModuleDescriptor md{};
606 md.nextInChain = &wd.chain;
607 return md;
608}
609
610void fillVertexLayout(WGPUVertexBufferLayout &layout, uint64_t stride,
611 const WGPUVertexAttribute *attrs, uint32_t attrCount) {
612 layout.arrayStride = stride;
613 layout.stepMode = WGPUVertexStepMode_Vertex;
614 layout.attributeCount = attrCount;
615 layout.attributes = attrs;
616}
617
618WGPUBlendState alphaBlend() {
619 WGPUBlendState b{};
620 b.color.srcFactor = WGPUBlendFactor_SrcAlpha;
621 b.color.dstFactor = WGPUBlendFactor_OneMinusSrcAlpha;
622 b.color.operation = WGPUBlendOperation_Add;
623 b.alpha.srcFactor = WGPUBlendFactor_One;
624 b.alpha.dstFactor = WGPUBlendFactor_OneMinusSrcAlpha;
625 b.alpha.operation = WGPUBlendOperation_Add;
626 return b;
627}
628
629WGPUBlendState noBlend() {
630 WGPUBlendState b{};
631 b.color.srcFactor = WGPUBlendFactor_One;
632 b.color.dstFactor = WGPUBlendFactor_Zero;
633 b.color.operation = WGPUBlendOperation_Add;
634 b.alpha.srcFactor = WGPUBlendFactor_One;
635 b.alpha.dstFactor = WGPUBlendFactor_Zero;
636 b.alpha.operation = WGPUBlendOperation_Add;
637 return b;
638}
639
640WGPUBlendState additiveBlend() {
641 WGPUBlendState b{};
642 b.color.srcFactor = WGPUBlendFactor_SrcAlpha;
643 b.color.dstFactor = WGPUBlendFactor_One;
644 b.color.operation = WGPUBlendOperation_Add;
645 b.alpha.srcFactor = WGPUBlendFactor_One;
646 b.alpha.dstFactor = WGPUBlendFactor_One;
647 b.alpha.operation = WGPUBlendOperation_Add;
648 return b;
649}
650
651} // namespace
652
653namespace {
654
655wgpu::RenderPipeline make2DColorPipeline(wgpu::Device &dev, WGPUTextureFormat format,
656 BlendMode mode) {
657 WGPUVertexAttribute attrs[2] = {};
658 attrs[0].format = WGPUVertexFormat_Float32x2;
659 attrs[0].offset = 0;
660 attrs[0].shaderLocation = 0;
661 attrs[1].format = WGPUVertexFormat_Float32x4;
662 attrs[1].offset = 8;
663 attrs[1].shaderLocation = 1;
664 WGPUVertexBufferLayout vb{};
665 fillVertexLayout(vb, 24, attrs, 2);
666
667 WGPUColorTargetState target{};
668 target.format = format;
669 target.blend = nullptr;
670 // Zero-init yields WGPUColorWriteMask_None, which silently discards every
671 // fragment (the clear still shows because clearValue is unaffected by the
672 // mask). emdawnwebgpu forwards this explicit 0 to the browser, unlike the
673 // JS default of "all".
674 target.writeMask = WGPUColorWriteMask_All;
675 WGPUBlendState bs = alphaBlend();
676 if (mode == BlendMode::Additive)
677 bs = additiveBlend();
678 else if (mode == BlendMode::Opaque)
679 bs = noBlend();
680 if (mode != BlendMode::Opaque) target.blend = &bs;
681
682 // The solid-color shader declares no bindings, so it must use an empty
683 // pipeline layout. Reusing the textured 2D layout here would require a
684 // bind group for every solid draw (WebGPU validation fails the whole
685 // command buffer otherwise).
686 WGPUPipelineLayoutDescriptor pld{};
687 pld.label = sv("eve_2d_color_layout");
688 pld.bindGroupLayoutCount = 0;
689 pld.bindGroupLayouts = nullptr;
690 wgpu::PipelineLayout emptyLayout = dev.CreatePipelineLayout(reinterpret_cast<const wgpu::PipelineLayoutDescriptor*>(&pld));
691
692 WGPURenderPipelineDescriptor pd{};
693 pd.label = sv("eve_color2d");
694 // nullptr = auto (default) pipeline layout; the solid-color shader has no
695 // bindings so the derived layout matches.
696 pd.layout = nullptr;
697 wgpu::ShaderModule vertModule = makeWgslModule(dev, kColorVertWgsl);
698 wgpu::ShaderModule fragModule = makeWgslModule(dev, kColorFragWgsl);
699 pd.vertex.module = vertModule.Get();
700 pd.vertex.entryPoint = sv("vs_main");
701 pd.vertex.bufferCount = 1;
702 pd.vertex.buffers = &vb;
703 WGPUFragmentState fs{};
704 fs.module = fragModule.Get();
705 fs.entryPoint = sv("fs_main");
706 fs.targetCount = 1;
707 fs.targets = &target;
708 pd.fragment = &fs;
709 pd.primitive.topology = WGPUPrimitiveTopology_TriangleList;
710 pd.primitive.frontFace = WGPUFrontFace_CCW;
711 pd.primitive.cullMode = WGPUCullMode_None;
712 pd.primitive.stripIndexFormat = WGPUIndexFormat_Undefined;
713 pd.depthStencil = nullptr;
714 pd.multisample.count = 1;
715 // Zero-init would leave mask=0, which discards every fragment
716 // (sampleMask=0). The WebGPU default is 0xFFFFFFFF (all samples).
717 pd.multisample.mask = 0xFFFFFFFFu;
718 return dev.CreateRenderPipeline(reinterpret_cast<const wgpu::RenderPipelineDescriptor*>(&pd));
719}
720
721wgpu::RenderPipeline make2DTexturedPipeline(wgpu::Device &dev, wgpu::PipelineLayout layout,
722 WGPUTextureFormat format, BlendMode mode) {
723 WGPUVertexAttribute attrs[3] = {};
724 attrs[0].format = WGPUVertexFormat_Float32x2;
725 attrs[0].offset = 0;
726 attrs[0].shaderLocation = 0;
727 attrs[1].format = WGPUVertexFormat_Float32x4;
728 attrs[1].offset = 8;
729 attrs[1].shaderLocation = 1;
730 attrs[2].format = WGPUVertexFormat_Float32x2;
731 attrs[2].offset = 24;
732 attrs[2].shaderLocation = 2;
733 WGPUVertexBufferLayout vb{};
734 fillVertexLayout(vb, 32, attrs, 3);
735
736 WGPUColorTargetState target{};
737 target.format = format;
738 target.writeMask = WGPUColorWriteMask_All;
739 WGPUBlendState bs = alphaBlend();
740 if (mode == BlendMode::Additive)
741 bs = additiveBlend();
742 else if (mode == BlendMode::Opaque)
743 bs = noBlend();
744 if (mode != BlendMode::Opaque) target.blend = &bs;
745
746 WGPURenderPipelineDescriptor pd{};
747 pd.label = sv("eve_textured2d");
748 pd.layout = layout.Get();
749 wgpu::ShaderModule vertModule = makeWgslModule(dev, kTexturedVertWgsl);
750 wgpu::ShaderModule fragModule = makeWgslModule(dev, kTexturedFragWgsl);
751 pd.vertex.module = vertModule.Get();
752 pd.vertex.entryPoint = sv("vs_main");
753 pd.vertex.bufferCount = 1;
754 pd.vertex.buffers = &vb;
755 WGPUFragmentState fs{};
756 fs.module = fragModule.Get();
757 fs.entryPoint = sv("fs_main");
758 fs.targetCount = 1;
759 fs.targets = &target;
760 pd.fragment = &fs;
761 pd.primitive.topology = WGPUPrimitiveTopology_TriangleList;
762 pd.primitive.frontFace = WGPUFrontFace_CCW;
763 pd.primitive.cullMode = WGPUCullMode_None;
764 pd.primitive.stripIndexFormat = WGPUIndexFormat_Undefined;
765 pd.depthStencil = nullptr;
766 pd.multisample.count = 1;
767 // Zero-init would leave mask=0, which discards every fragment
768 // (sampleMask=0). The WebGPU default is 0xFFFFFFFF (all samples).
769 pd.multisample.mask = 0xFFFFFFFFu;
770 return dev.CreateRenderPipeline(reinterpret_cast<const wgpu::RenderPipelineDescriptor*>(&pd));
771}
772
773wgpu::RenderPipeline make2DLitPipeline(wgpu::Device &dev, wgpu::PipelineLayout layout,
774 WGPUTextureFormat format, bool blend) {
775 WGPUVertexAttribute attrs[3] = {};
776 attrs[0].format = WGPUVertexFormat_Float32x2;
777 attrs[0].offset = 0;
778 attrs[0].shaderLocation = 0;
779 attrs[1].format = WGPUVertexFormat_Float32x4;
780 attrs[1].offset = 8;
781 attrs[1].shaderLocation = 1;
782 attrs[2].format = WGPUVertexFormat_Float32x2;
783 attrs[2].offset = 24;
784 attrs[2].shaderLocation = 2;
785 WGPUVertexBufferLayout vb{};
786 fillVertexLayout(vb, 32, attrs, 3);
787
788 WGPUColorTargetState target{};
789 target.format = format;
790 target.writeMask = WGPUColorWriteMask_All;
791 WGPUBlendState bs = alphaBlend();
792 if (blend) target.blend = &bs;
793
794 WGPURenderPipelineDescriptor pd{};
795 pd.label = sv("eve_lit2d");
796 pd.layout = layout.Get();
797 wgpu::ShaderModule vertModule = makeWgslModule(dev, kTexturedVertWgsl);
798 wgpu::ShaderModule fragModule = makeWgslModule(dev, kLit2DFragWgsl);
799 pd.vertex.module = vertModule.Get();
800 pd.vertex.entryPoint = sv("vs_main");
801 pd.vertex.bufferCount = 1;
802 pd.vertex.buffers = &vb;
803 WGPUFragmentState fs{};
804 fs.module = fragModule.Get();
805 fs.entryPoint = sv("fs_main");
806 fs.targetCount = 1;
807 fs.targets = &target;
808 pd.fragment = &fs;
809 pd.primitive.topology = WGPUPrimitiveTopology_TriangleList;
810 pd.primitive.frontFace = WGPUFrontFace_CCW;
811 pd.primitive.cullMode = WGPUCullMode_None;
812 pd.primitive.stripIndexFormat = WGPUIndexFormat_Undefined;
813 pd.depthStencil = nullptr;
814 pd.multisample.count = 1;
815 // Zero-init would leave mask=0, which discards every fragment
816 // (sampleMask=0). The WebGPU default is 0xFFFFFFFF (all samples).
817 pd.multisample.mask = 0xFFFFFFFFu;
818 return dev.CreateRenderPipeline(reinterpret_cast<const wgpu::RenderPipelineDescriptor*>(&pd));
819}
820
821} // namespace
822
823void Graphics::create2DPipelines() {
824 tex2DSetLayout = make2DBindGroupLayout();
825 tex2DPipelineLayout = make2DPipelineLayout();
826
827 colorPipeline = make2DColorPipeline(device, surfaceFormat, BlendMode::Alpha);
828 texturedPipeline = make2DTexturedPipeline(device, tex2DPipelineLayout, surfaceFormat,
830 colorAdditivePipeline = make2DColorPipeline(device, surfaceFormat, BlendMode::Additive);
831 texturedAdditivePipeline = make2DTexturedPipeline(device, tex2DPipelineLayout, surfaceFormat,
833 colorOpaquePipeline = make2DColorPipeline(device, surfaceFormat, BlendMode::Opaque);
834 texturedOpaquePipeline = make2DTexturedPipeline(device, tex2DPipelineLayout, surfaceFormat,
836 lit2dPipeline = make2DLitPipeline(device, tex2DPipelineLayout, surfaceFormat, true);
837
838 offscreenColorPipeline = make2DColorPipeline(device, WGPUTextureFormat_RGBA8Unorm,
840 offscreenTexturedPipeline = make2DTexturedPipeline(device, tex2DPipelineLayout,
841 WGPUTextureFormat_RGBA8Unorm,
843 offscreenColorAdditivePipeline = make2DColorPipeline(device, WGPUTextureFormat_RGBA8Unorm,
845 offscreenTexturedAdditivePipeline =
846 make2DTexturedPipeline(device, tex2DPipelineLayout, WGPUTextureFormat_RGBA8Unorm,
848 offscreenColorOpaquePipeline = make2DColorPipeline(device, WGPUTextureFormat_RGBA8Unorm,
850 offscreenTexturedOpaquePipeline =
851 make2DTexturedPipeline(device, tex2DPipelineLayout, WGPUTextureFormat_RGBA8Unorm,
853 offscreenLitPipeline = make2DLitPipeline(device, tex2DPipelineLayout,
854 WGPUTextureFormat_RGBA8Unorm, true);
855}
856
857void Graphics::createMesh3DPipelines() {
858 mesh3dSetLayout = makeMesh3DBindGroupLayout();
859 mesh3dPipelineLayout = makeMesh3DPipelineLayout();
860
861 WGPUVertexAttribute attrs[3] = {};
862 attrs[0].format = WGPUVertexFormat_Float32x3; // pos
863 attrs[0].offset = 0;
864 attrs[0].shaderLocation = 0;
865 attrs[1].format = WGPUVertexFormat_Float32x3; // normal
866 attrs[1].offset = 12;
867 attrs[1].shaderLocation = 1;
868 attrs[2].format = WGPUVertexFormat_Float32x2; // uv
869 attrs[2].offset = 24;
870 attrs[2].shaderLocation = 2;
871 WGPUVertexBufferLayout vb{};
872 fillVertexLayout(vb, 32, attrs, 3);
873
874 WGPUDepthStencilState ds{};
875 ds.format = WGPUTextureFormat_Depth32Float;
876 ds.depthWriteEnabled = WGPUOptionalBool_True;
877 ds.depthCompare = WGPUCompareFunction_Less;
878 ds.stencilReadMask = 0;
879 ds.stencilWriteMask = 0;
880
881 WGPUColorTargetState target{};
882 target.format = sceneColorFormat;
883 target.blend = nullptr;
884 target.writeMask = WGPUColorWriteMask_All;
885
886 WGPURenderPipelineDescriptor pd{};
887 pd.label = sv("eve_mesh3d");
888 pd.layout = mesh3dPipelineLayout.Get();
889 wgpu::ShaderModule vertModule = makeWgslModule(device, kMesh3DVertWgsl);
890 wgpu::ShaderModule fragModule = makeWgslModule(device, kMesh3DFragWgsl);
891 pd.vertex.module = vertModule.Get();
892 pd.vertex.entryPoint = sv("vs_main");
893 pd.vertex.bufferCount = 1;
894 pd.vertex.buffers = &vb;
895 WGPUFragmentState fs{};
896 fs.module = fragModule.Get();
897 fs.entryPoint = sv("fs_main");
898 fs.targetCount = 1;
899 fs.targets = &target;
900 pd.fragment = &fs;
901 pd.primitive.topology = WGPUPrimitiveTopology_TriangleList;
902 pd.primitive.frontFace = WGPUFrontFace_CCW;
903 pd.primitive.cullMode = WGPUCullMode_None;
904 pd.primitive.stripIndexFormat = WGPUIndexFormat_Undefined;
905 pd.depthStencil = &ds;
906 pd.multisample.count = 1;
907 // Zero-init would leave mask=0, which discards every fragment
908 // (sampleMask=0). The WebGPU default is 0xFFFFFFFF (all samples).
909 pd.multisample.mask = 0xFFFFFFFFu;
910 mesh3dPipeline = device.CreateRenderPipeline(reinterpret_cast<const wgpu::RenderPipelineDescriptor*>(&pd));
911}
912
913void Graphics::createShadowPipelines() {
914 shadowSetLayout = makeShadowBindGroupLayout();
915 shadowPipelineLayout = makeShadowPipelineLayout();
916
917 WGPUVertexAttribute attrs[3] = {};
918 attrs[0].format = WGPUVertexFormat_Float32x3;
919 attrs[0].offset = 0;
920 attrs[0].shaderLocation = 0;
921 attrs[1].format = WGPUVertexFormat_Float32x3;
922 attrs[1].offset = 12;
923 attrs[1].shaderLocation = 1;
924 attrs[2].format = WGPUVertexFormat_Float32x2;
925 attrs[2].offset = 24;
926 attrs[2].shaderLocation = 2;
927 WGPUVertexBufferLayout vb{};
928 fillVertexLayout(vb, 32, attrs, 3);
929
930 WGPUDepthStencilState ds{};
931 ds.format = WGPUTextureFormat_Depth32Float;
932 ds.depthWriteEnabled = WGPUOptionalBool_True;
933 ds.depthCompare = WGPUCompareFunction_Less;
934 ds.stencilReadMask = 0;
935 ds.stencilWriteMask = 0;
936
937 WGPURenderPipelineDescriptor pd{};
938 pd.label = sv("eve_shadow");
939 pd.layout = shadowPipelineLayout.Get();
940 wgpu::ShaderModule vertModule = makeWgslModule(device, kMesh3DShadowVertWgsl);
941 pd.vertex.module = vertModule.Get();
942 pd.vertex.entryPoint = sv("vs_main");
943 pd.vertex.bufferCount = 1;
944 pd.vertex.buffers = &vb;
945 pd.fragment = nullptr;
946 pd.primitive.topology = WGPUPrimitiveTopology_TriangleList;
947 pd.primitive.frontFace = WGPUFrontFace_CCW;
948 pd.primitive.cullMode = WGPUCullMode_None;
949 pd.primitive.stripIndexFormat = WGPUIndexFormat_Undefined;
950 pd.depthStencil = &ds;
951 pd.multisample.count = 1;
952 // Zero-init would leave mask=0, which discards every fragment
953 // (sampleMask=0). The WebGPU default is 0xFFFFFFFF (all samples).
954 pd.multisample.mask = 0xFFFFFFFFu;
955 mesh3dShadowPipeline = device.CreateRenderPipeline(reinterpret_cast<const wgpu::RenderPipelineDescriptor*>(&pd));
956}
957
958void Graphics::createGbufferPipelines() {
959 gbufferSetLayout = makeGbufferBindGroupLayout();
960 gbufferPipelineLayout = makeGbufferPipelineLayout();
961
962 WGPUVertexAttribute attrs[3] = {};
963 attrs[0].format = WGPUVertexFormat_Float32x3;
964 attrs[0].offset = 0;
965 attrs[0].shaderLocation = 0;
966 attrs[1].format = WGPUVertexFormat_Float32x3;
967 attrs[1].offset = 12;
968 attrs[1].shaderLocation = 1;
969 attrs[2].format = WGPUVertexFormat_Float32x2;
970 attrs[2].offset = 24;
971 attrs[2].shaderLocation = 2;
972 WGPUVertexBufferLayout vb{};
973 fillVertexLayout(vb, 32, attrs, 3);
974
975 WGPUDepthStencilState ds{};
976 ds.format = WGPUTextureFormat_Depth32Float;
977 ds.depthWriteEnabled = WGPUOptionalBool_True;
978 ds.depthCompare = WGPUCompareFunction_Less;
979 ds.stencilReadMask = 0;
980 ds.stencilWriteMask = 0;
981
982 WGPUColorTargetState targets[3] = {};
983 for (int i = 0; i < 3; ++i) {
984 targets[i].format = WGPUTextureFormat_RGBA8Unorm;
985 targets[i].blend = nullptr;
986 targets[i].writeMask = WGPUColorWriteMask_All;
987 }
988
989 WGPURenderPipelineDescriptor pd{};
990 pd.label = sv("eve_gbuffer");
991 pd.layout = gbufferPipelineLayout.Get();
992 wgpu::ShaderModule vertModule = makeWgslModule(device, kMesh3DGbufferVertWgsl);
993 wgpu::ShaderModule fragModule = makeWgslModule(device, kMesh3DGbufferFragWgsl);
994 pd.vertex.module = vertModule.Get();
995 pd.vertex.entryPoint = sv("vs_main");
996 pd.vertex.bufferCount = 1;
997 pd.vertex.buffers = &vb;
998 WGPUFragmentState fs{};
999 fs.module = fragModule.Get();
1000 fs.entryPoint = sv("fs_main");
1001 fs.targetCount = 3;
1002 fs.targets = targets;
1003 pd.fragment = &fs;
1004 pd.primitive.topology = WGPUPrimitiveTopology_TriangleList;
1005 pd.primitive.frontFace = WGPUFrontFace_CCW;
1006 pd.primitive.cullMode = WGPUCullMode_None;
1007 pd.primitive.stripIndexFormat = WGPUIndexFormat_Undefined;
1008 pd.depthStencil = &ds;
1009 pd.multisample.count = 1;
1010 // Zero-init would leave mask=0, which discards every fragment
1011 // (sampleMask=0). The WebGPU default is 0xFFFFFFFF (all samples).
1012 pd.multisample.mask = 0xFFFFFFFFu;
1013 mesh3dGbufferPipeline = device.CreateRenderPipeline(reinterpret_cast<const wgpu::RenderPipelineDescriptor*>(&pd));
1014}
1015
1016void Graphics::createVoxelPipelines() {
1017 voxelSetLayout = makeVoxelBindGroupLayout();
1018 voxelPipelineLayout = makeVoxelPipelineLayout();
1019
1020 WGPUVertexAttribute attrs[2] = {};
1021 attrs[0].format = WGPUVertexFormat_Float32x2; // corner
1022 attrs[0].offset = 0;
1023 attrs[0].shaderLocation = 0;
1024 attrs[1].format = WGPUVertexFormat_Uint32; // packed
1025 attrs[1].offset = 8;
1026 attrs[1].shaderLocation = 1;
1027 WGPUVertexBufferLayout cornerVb{};
1028 fillVertexLayout(cornerVb, 12, attrs, 2);
1029 WGPUVertexBufferLayout instanceVb{};
1030 instanceVb.arrayStride = 4;
1031 instanceVb.stepMode = WGPUVertexStepMode_Instance;
1032 instanceVb.attributeCount = 0;
1033 instanceVb.attributes = nullptr;
1034 WGPUVertexBufferLayout vbs[2] = {cornerVb, instanceVb};
1035
1036 WGPUDepthStencilState ds{};
1037 ds.format = WGPUTextureFormat_Depth32Float;
1038 ds.depthWriteEnabled = WGPUOptionalBool_True;
1039 ds.depthCompare = WGPUCompareFunction_Less;
1040 ds.stencilReadMask = 0;
1041 ds.stencilWriteMask = 0;
1042
1043 WGPUColorTargetState target{};
1044 target.format = sceneColorFormat;
1045 target.blend = nullptr;
1046 target.writeMask = WGPUColorWriteMask_All;
1047
1048 WGPURenderPipelineDescriptor pd{};
1049 pd.label = sv("eve_voxel");
1050 pd.layout = voxelPipelineLayout.Get();
1051 wgpu::ShaderModule vertModule = makeWgslModule(device, kVoxelRectVertWgsl);
1052 wgpu::ShaderModule fragModule = makeWgslModule(device, kVoxelRectFragWgsl);
1053 pd.vertex.module = vertModule.Get();
1054 pd.vertex.entryPoint = sv("vs_main");
1055 pd.vertex.bufferCount = 2;
1056 pd.vertex.buffers = vbs;
1057 WGPUFragmentState fs{};
1058 fs.module = fragModule.Get();
1059 fs.entryPoint = sv("fs_main");
1060 fs.targetCount = 1;
1061 fs.targets = &target;
1062 pd.fragment = &fs;
1063 pd.primitive.topology = WGPUPrimitiveTopology_TriangleList;
1064 pd.primitive.frontFace = WGPUFrontFace_CCW;
1065 pd.primitive.cullMode = WGPUCullMode_None;
1066 pd.primitive.stripIndexFormat = WGPUIndexFormat_Undefined;
1067 pd.depthStencil = &ds;
1068 pd.multisample.count = 1;
1069 // Zero-init would leave mask=0, which discards every fragment
1070 // (sampleMask=0). The WebGPU default is 0xFFFFFFFF (all samples).
1071 pd.multisample.mask = 0xFFFFFFFFu;
1072 voxelRectPipeline = device.CreateRenderPipeline(reinterpret_cast<const wgpu::RenderPipelineDescriptor*>(&pd));
1073
1074 // Unit quad for instanced voxel faces (2 triangles, corner + packed uv slot).
1075 float quad[8] = {0, 0, 1, 0, 1, 1, 0, 1};
1076 WGPUBufferDescriptor bd{};
1077 bd.label = sv("eve_voxel_quad");
1078 bd.size = sizeof(quad);
1079 bd.usage = WGPUBufferUsage_CopyDst | WGPUBufferUsage_Vertex;
1080 bd.mappedAtCreation = false;
1081 voxelUnitQuadVerts = device.CreateBuffer(reinterpret_cast<const wgpu::BufferDescriptor*>(&bd));
1082 queue.WriteBuffer(voxelUnitQuadVerts, 0, quad, sizeof(quad));
1083
1084 uint32_t indices[6] = {0, 1, 2, 2, 3, 0};
1085 WGPUBufferDescriptor ibd{};
1086 ibd.label = sv("eve_voxel_quad_idx");
1087 ibd.size = sizeof(indices);
1088 ibd.usage = WGPUBufferUsage_CopyDst | WGPUBufferUsage_Index;
1089 ibd.mappedAtCreation = false;
1090 voxelUnitQuadIndices = device.CreateBuffer(reinterpret_cast<const wgpu::BufferDescriptor*>(&ibd));
1091 queue.WriteBuffer(voxelUnitQuadIndices, 0, indices, sizeof(indices));
1092}
1093
1094// ---------------------------------------------------------------------------
1095// Arena helpers
1096// ---------------------------------------------------------------------------
1097
1098uint32_t Graphics::UboArena::alloc(uint64_t size, uint64_t alignment) {
1099 uint64_t aligned = (used + alignment - 1) / alignment * alignment;
1100 used = aligned + size;
1101 return static_cast<uint32_t>(aligned);
1102}
1103
1104uint64_t Graphics::VertexArena::alloc(uint64_t bytes) {
1105 uint64_t at = used;
1106 used += bytes;
1107 return at;
1108}
1109
1110Graphics::UboArena &Graphics::currentUboArena() { return uboArenas[currentFrameSlot()]; }
1111Graphics::VertexArena &Graphics::currentVertexArena() { return vertexArenas[currentFrameSlot()]; }
1112
1113void Graphics::ensureUboArena(UboArena &arena, uint64_t bytes) {
1114 if (arena.buffer && arena.capacity >= bytes) return;
1115 uint64_t size = arena.capacity;
1116 while (size < bytes) size = size ? size * 2 : (64u << 10);
1117 WGPUBufferDescriptor bd{};
1118 bd.label = sv("eve_ubo_arena");
1119 bd.size = size;
1120 bd.usage = WGPUBufferUsage_CopyDst | WGPUBufferUsage_Uniform;
1121 bd.mappedAtCreation = false;
1122 arena.buffer = device.CreateBuffer(reinterpret_cast<const wgpu::BufferDescriptor*>(&bd));
1123 arena.capacity = size;
1124 arena.used = 0;
1125}
1126
1127void Graphics::ensureVertexArena(VertexArena &arena, uint64_t bytes) {
1128 if (arena.buffer && arena.capacity >= bytes) return;
1129 uint64_t size = arena.capacity;
1130 while (size < bytes) size = size ? size * 2 : (256u << 10);
1131 WGPUBufferDescriptor bd{};
1132 bd.label = sv("eve_vertex_arena");
1133 bd.size = size;
1134 bd.usage = WGPUBufferUsage_CopyDst | WGPUBufferUsage_Vertex;
1135 bd.mappedAtCreation = false;
1136 arena.buffer = device.CreateBuffer(reinterpret_cast<const wgpu::BufferDescriptor*>(&bd));
1137 arena.capacity = size;
1138 arena.used = 0;
1139}
1140
1141// ---------------------------------------------------------------------------
1142// Sampler
1143// ---------------------------------------------------------------------------
1144
1145wgpu::Sampler Graphics::makeSampler(const TextureSampler &s, uint32_t mipLevels) const {
1146 WGPUSamplerDescriptor d{};
1147 d.label = sv("eve_sampler");
1148 d.addressModeU = s.repeatU ? WGPUAddressMode_Repeat : WGPUAddressMode_ClampToEdge;
1149 d.addressModeV = s.repeatV ? WGPUAddressMode_Repeat : WGPUAddressMode_ClampToEdge;
1150 d.addressModeW = s.repeatW ? WGPUAddressMode_Repeat : WGPUAddressMode_ClampToEdge;
1151 d.magFilter = s.mag == FilterMode::Nearest ? WGPUFilterMode_Nearest : WGPUFilterMode_Linear;
1152 d.minFilter = s.min == FilterMode::Nearest ? WGPUFilterMode_Nearest : WGPUFilterMode_Linear;
1153 if (s.mipmap == MipmapMode::Disabled || mipLevels <= 1) {
1154 d.mipmapFilter = WGPUMipmapFilterMode_Nearest;
1155 d.lodMinClamp = 0.f;
1156 d.lodMaxClamp = 0.f;
1157 } else {
1158 d.mipmapFilter =
1159 s.mipmap == MipmapMode::Nearest ? WGPUMipmapFilterMode_Nearest : WGPUMipmapFilterMode_Linear;
1160 d.lodMinClamp = s.minLod;
1161 d.lodMaxClamp = std::min(std::max(s.maxLod, 0.f), float(mipLevels));
1162 }
1163 float aniso = s.maxAnisotropy > 1.f ? s.maxAnisotropy : 1.f;
1164 d.maxAnisotropy = std::min(std::max(aniso, 1.f), std::max(maxSamplerAnisotropy, 1.f));
1165 return device.CreateSampler(reinterpret_cast<const wgpu::SamplerDescriptor*>(&d));
1166}
1167
1168// ---------------------------------------------------------------------------
1169// Texture creation
1170// ---------------------------------------------------------------------------
1171
1172Texture *Graphics::newTexture(int width, int height, const uint8_t *rgba, bool repeatU,
1173 bool repeatV) {
1174 TextureCreateInfo info;
1175 info.sampler.repeatU = repeatU;
1176 info.sampler.repeatV = repeatV;
1177 return newTexture(width, height, rgba, info);
1178}
1179
1181 if (!data) throw Exception("newTexture: null ImageData");
1182 TextureCreateInfo info;
1183 return newTexture(data->getWidth(), data->getHeight(),
1184 static_cast<const uint8_t *>(data->getData()), info);
1185}
1186
1188 if (!data) throw Exception("newTexture: null ImageData");
1189 if (data->getFormat() != "RGBA8")
1190 throw Exception("newTexture: only RGBA8 supported");
1191 return newTexture(data->getWidth(), data->getHeight(),
1192 static_cast<const uint8_t *>(data->getData()), info);
1193}
1194
1195Texture *Graphics::newTexture(int width, int height, const uint8_t *rgba,
1196 const TextureCreateInfo &info) {
1197 if (width <= 0 || height <= 0) throw Exception("newTexture: invalid size %dx%d", width, height);
1198
1199 auto gpu = std::make_unique<GpuTexture>();
1200 gpu->width = width;
1201 gpu->height = height;
1202 gpu->samplerState = info.sampler;
1203 gpu->mipLevels = info.generateMipmaps ? uint32_t(mipmapCountForSize(width, height)) : 1u;
1204
1205 WGPUTextureDescriptor td{};
1206 td.label = sv("eve_tex2d");
1207 td.dimension = WGPUTextureDimension_2D;
1208 td.size.width = static_cast<uint32_t>(width);
1209 td.size.height = static_cast<uint32_t>(height);
1210 td.size.depthOrArrayLayers = 1;
1211 td.sampleCount = 1;
1212 td.format = WGPUTextureFormat_RGBA8Unorm;
1213 td.mipLevelCount = gpu->mipLevels;
1214 td.usage = WGPUTextureUsage_TextureBinding | WGPUTextureUsage_CopyDst |
1215 WGPUTextureUsage_RenderAttachment;
1216 gpu->texture = device.CreateTexture(reinterpret_cast<const wgpu::TextureDescriptor*>(&td));
1217
1218 uploadTexturePixelsMips(gpu.get(), rgba, width, height);
1219
1220 WGPUTextureViewDescriptor vd{};
1221 vd.format = WGPUTextureFormat_RGBA8Unorm;
1222 vd.dimension = WGPUTextureViewDimension_2D;
1223 vd.baseMipLevel = 0;
1224 vd.mipLevelCount = gpu->mipLevels;
1225 vd.baseArrayLayer = 0;
1226 vd.arrayLayerCount = 1;
1227 gpu->view = gpu->texture.CreateView(reinterpret_cast<const wgpu::TextureViewDescriptor*>(&vd));
1228 gpu->sampler = makeSampler(info.sampler, gpu->mipLevels);
1229
1230 auto *tex = new Texture();
1231 tex->width = width;
1232 tex->height = height;
1233 tex->mipmapCount = int(gpu->mipLevels);
1234 tex->sampler = info.sampler;
1235 tex->gpuHandle = gpu.get();
1236
1237 ownedGpuTextures.push_back(std::move(gpu));
1238 ownedTextures.push_back(std::unique_ptr<Texture>(tex));
1239 return tex;
1240}
1241
1242void Graphics::uploadTexturePixels(GpuTexture *gt, const uint8_t *rgba, int w, int h,
1243 const TextureCreateInfo &info) {
1244 (void)info;
1245 uploadTexturePixelsMips(gt, rgba, w, h);
1246}
1247
1248void Graphics::uploadTexturePixelsMips(GpuTexture *gt, const uint8_t *rgba, int w, int h) {
1249 if (!rgba) return;
1250 WGPUTexelCopyBufferLayout layout{};
1251 layout.offset = 0;
1252 layout.bytesPerRow = static_cast<uint32_t>(w * 4);
1253 layout.rowsPerImage = static_cast<uint32_t>(h);
1254 WGPUExtent3D extent{static_cast<uint32_t>(w), static_cast<uint32_t>(h), 1};
1255
1256 for (uint32_t m = 0; m < gt->mipLevels; ++m) {
1257 WGPUTexelCopyTextureInfo dst{};
1258 dst.texture = gt->texture.Get();
1259 dst.mipLevel = m;
1260 dst.aspect = WGPUTextureAspect_All;
1261 queue.WriteTexture(reinterpret_cast<const wgpu::TexelCopyTextureInfo*>(&dst), rgba,
1262 static_cast<uint64_t>(w) * h * 4,
1263 reinterpret_cast<const wgpu::TexelCopyBufferLayout*>(&layout),
1264 reinterpret_cast<const wgpu::Extent3D*>(&extent));
1265 if (m + 1 < gt->mipLevels) {
1266 // Box-filter downsample into the CPU buffer for the next level.
1267 std::vector<uint8_t> next((w / 2) * (h / 2) * 4);
1268 for (int y = 0; y < h / 2; ++y) {
1269 for (int x = 0; x < w / 2; ++x) {
1270 for (int c = 0; c < 4; ++c) {
1271 uint32_t acc = 0;
1272 acc += rgba[((y * 2 + 0) * w + (x * 2 + 0)) * 4 + c];
1273 acc += rgba[((y * 2 + 0) * w + (x * 2 + 1)) * 4 + c];
1274 acc += rgba[((y * 2 + 1) * w + (x * 2 + 0)) * 4 + c];
1275 acc += rgba[((y * 2 + 1) * w + (x * 2 + 1)) * 4 + c];
1276 next[(y * (w / 2) + x) * 4 + c] = uint8_t(acc / 4);
1277 }
1278 }
1279 }
1280 w /= 2;
1281 h /= 2;
1282 rgba = next.data();
1283 layout.bytesPerRow = static_cast<uint32_t>(w * 4);
1284 layout.rowsPerImage = static_cast<uint32_t>(h);
1285 extent = {static_cast<uint32_t>(w), static_cast<uint32_t>(h), 1};
1286 }
1287 }
1288}
1289
1290Texture *Graphics::newCubemap(int faceSize, const uint8_t *rgbaFaces) {
1291 TextureCreateInfo info;
1292 return newCubemap(faceSize, rgbaFaces, info);
1293}
1294
1295Texture *Graphics::newCubemap(int faceSize, const uint8_t *rgbaFaces,
1296 const TextureCreateInfo &info) {
1297 if (faceSize <= 0 || !rgbaFaces)
1298 throw Exception("newCubemap: invalid size or null data");
1299
1300 auto gpu = std::make_unique<GpuTexture>();
1301 gpu->width = faceSize;
1302 gpu->height = faceSize;
1303 gpu->samplerState = info.sampler;
1304 gpu->isCube = true;
1305 gpu->mipLevels = info.generateMipmaps ? uint32_t(mipmapCountForSize(faceSize, faceSize)) : 1u;
1306
1307 WGPUTextureDescriptor td{};
1308 td.label = sv("eve_cubemap");
1309 td.dimension = WGPUTextureDimension_2D;
1310 td.size.width = static_cast<uint32_t>(faceSize);
1311 td.size.height = static_cast<uint32_t>(faceSize);
1312 td.size.depthOrArrayLayers = 6;
1313 td.sampleCount = 1;
1314 td.format = WGPUTextureFormat_RGBA8Unorm;
1315 td.mipLevelCount = gpu->mipLevels;
1316 td.usage = WGPUTextureUsage_TextureBinding | WGPUTextureUsage_CopyDst;
1317 gpu->texture = device.CreateTexture(reinterpret_cast<const wgpu::TextureDescriptor*>(&td));
1318
1319 WGPUTexelCopyBufferLayout layout{};
1320 layout.offset = 0;
1321 layout.bytesPerRow = static_cast<uint32_t>(faceSize * 4);
1322 layout.rowsPerImage = static_cast<uint32_t>(faceSize);
1323 WGPUExtent3D extent{static_cast<uint32_t>(faceSize), static_cast<uint32_t>(faceSize), 1};
1324 for (int f = 0; f < 6; ++f) {
1325 WGPUTexelCopyTextureInfo dst{};
1326 dst.texture = gpu->texture.Get();
1327 dst.mipLevel = 0;
1328 dst.aspect = WGPUTextureAspect_All;
1329 dst.origin = {0, 0, static_cast<uint32_t>(f)};
1330 queue.WriteTexture(reinterpret_cast<const wgpu::TexelCopyTextureInfo*>(&dst),
1331 rgbaFaces + f * faceSize * faceSize * 4,
1332 static_cast<uint64_t>(faceSize) * faceSize * 4,
1333 reinterpret_cast<const wgpu::TexelCopyBufferLayout*>(&layout),
1334 reinterpret_cast<const wgpu::Extent3D*>(&extent));
1335 }
1336
1337 WGPUTextureViewDescriptor vd{};
1338 vd.format = WGPUTextureFormat_RGBA8Unorm;
1339 vd.dimension = WGPUTextureViewDimension_Cube;
1340 vd.baseMipLevel = 0;
1341 vd.mipLevelCount = gpu->mipLevels;
1342 vd.baseArrayLayer = 0;
1343 vd.arrayLayerCount = 6;
1344 gpu->view = gpu->texture.CreateView(reinterpret_cast<const wgpu::TextureViewDescriptor*>(&vd));
1345 gpu->sampler = makeSampler(info.sampler, gpu->mipLevels);
1346
1347 auto *tex = new Texture();
1348 tex->width = faceSize;
1349 tex->height = faceSize;
1350 tex->mipmapCount = int(gpu->mipLevels);
1351 tex->sampler = info.sampler;
1352 tex->gpuHandle = gpu.get();
1353
1354 ownedGpuTextures.push_back(std::move(gpu));
1355 ownedTextures.push_back(std::unique_ptr<Texture>(tex));
1356 return tex;
1357}
1358
1360 if (!texture) return;
1361 auto *gpu = gpuForTexture(texture);
1362 if (!gpu) return;
1363 gpu->samplerState = sampler;
1364 gpu->sampler = makeSampler(sampler, gpu->mipLevels);
1365 texture->sampler = sampler;
1366}
1367
1368float Graphics::getMaxAnisotropy() const { return maxSamplerAnisotropy; }
1369
1370Texture *Graphics::newTextureFromFile(const std::string &filename) {
1371 if (filename.empty()) throw Exception("newTextureFromFile: empty filename");
1372 auto it = texturesByPath.find(filename);
1373 if (it != texturesByPath.end()) {
1374 reloadTextureFromFile(filename);
1375 return it->second;
1376 }
1377 auto *imgMod = image::Image::create();
1378 eve::ref<image::ImageData> data(imgMod->newImageDataFromFile(filename));
1379 Texture *tex = newTexture(data.get());
1380 texturesByPath[filename] = tex;
1381 return tex;
1382}
1383
1384bool Graphics::reloadTextureFromFile(const std::string &filename) {
1385 auto it = texturesByPath.find(filename);
1386 if (it == texturesByPath.end()) return false;
1387
1388 image::ImageData *data = nullptr;
1389 try {
1390 auto *imgMod = image::Image::create();
1391 eve::ref<image::ImageData> cached(imgMod->newImageDataFromFile(filename));
1392 data = cached.get();
1393 } catch (...) {
1394 return false;
1395 }
1396 if (!data) return false;
1397
1398 Texture *tex = it->second;
1399 auto *gpu = gpuForTexture(tex);
1400 if (gpu && data->getWidth() == tex->width && data->getHeight() == tex->height) {
1401 uploadTexturePixelsMips(gpu, static_cast<const uint8_t *>(data->getData()), tex->width,
1402 tex->height);
1403 }
1404 return true;
1405}
1406
1408 if (!texture || !texture->gpuHandle) return false;
1409 // Renderer-owned fallback textures must never be released by callers.
1410 if (texture->gpuHandle == whiteTexture || texture->gpuHandle == flatNormalTexture ||
1411 texture->gpuHandle == flatNormalTexture3D ||
1412 texture->gpuHandle == defaultEnvCubemap)
1413 return false;
1414
1415 auto *gpu = static_cast<GpuTexture *>(texture->gpuHandle);
1416 auto gpuIt = std::find_if(ownedGpuTextures.begin(), ownedGpuTextures.end(),
1417 [&](const std::unique_ptr<GpuTexture> &g) {
1418 return g.get() == gpu;
1419 });
1420 if (gpuIt == ownedGpuTextures.end()) return false;
1421
1422 auto texIt = std::find_if(ownedTextures.begin(), ownedTextures.end(),
1423 [&](const std::unique_ptr<Texture> &t) {
1424 return t.get() == texture;
1425 });
1426 if (texIt == ownedTextures.end()) return false;
1427
1428 // Path-cached textures must leave the hot-reload cache once released.
1429 for (auto it = texturesByPath.begin(); it != texturesByPath.end();) {
1430 if (it->second == texture)
1431 it = texturesByPath.erase(it);
1432 else
1433 ++it;
1434 }
1435
1436 texture->gpuHandle = nullptr;
1437 ownedGpuTextures.erase(gpuIt);
1438 // Transfer the CPU facade to the caller instead of destroying it.
1439 (void)texIt->release();
1440 ownedTextures.erase(texIt);
1441 return true;
1442}
1443
1444GpuTexture *Graphics::gpuForTexture(Texture *t) const {
1445 return t ? static_cast<GpuTexture *>(t->gpuHandle) : nullptr;
1446}
1447
1448GpuTexture *Graphics::gpuForTextureOrWhite(Texture *t) const {
1449 GpuTexture *g = gpuForTexture(t);
1450 return g ? g : whiteTexture;
1451}
1452
1453wgpu::BindGroup Graphics::makeTex2DBindGroup(GpuTexture *color, GpuTexture *depth) {
1454 GpuTexture *c = color ? color : whiteTexture;
1455 GpuTexture *d = depth ? depth : c;
1456 WGPUBindGroupEntry entries[5]{};
1457 entries[0].binding = 0;
1458 entries[0].textureView = c->view.Get();
1459 entries[1].binding = 1;
1460 entries[1].textureView = d->view.Get();
1461 entries[2].binding = 2;
1462 entries[2].sampler = c->sampler.Get();
1463 entries[3].binding = 3;
1464 entries[3].sampler = d->sampler.Get();
1465 entries[4].binding = 4;
1466 entries[4].buffer = currentUboArena().buffer.Get(); // replaced per-draw via dynamic offset
1467 entries[4].size =
1468 std::max<uint32_t>(Shader::kPushConstantBytes, uint32_t(sizeof(Lighting2DUBO)));
1469 WGPUBindGroupDescriptor desc{};
1470 desc.label = sv("eve_tex2d_group");
1471 desc.layout = tex2DSetLayout.Get();
1472 desc.entryCount = 5;
1473 desc.entries = entries;
1474 return device.CreateBindGroup(reinterpret_cast<const wgpu::BindGroupDescriptor*>(&desc));
1475}
1476
1477wgpu::BindGroup Graphics::makeMeshBindGroup(GpuTexture *albedo, GpuTexture *normal, GpuTexture *env,
1478 GpuTexture *height, GpuTexture *depth,
1479 uint32_t frameUboOffset, uint32_t shadowUboOffset,
1480 uint32_t pushUboOffset) {
1481 GpuTexture *a = albedo ? albedo : whiteTexture;
1482 GpuTexture *n = normal ? normal : flatNormalTexture;
1483 GpuTexture *e = env ? env : defaultEnvCubemap;
1484 GpuTexture *h = height ? height : flatHeightTexture3D;
1485 GpuTexture *d = depth ? depth : flatDepthTexture3D;
1486
1487 WGPUBindGroupEntry entries[10]{};
1488 entries[0].binding = 0;
1489 entries[0].buffer = currentUboArena().buffer.Get();
1490 entries[0].size = sizeof(Mesh3DUBO);
1491 entries[1].binding = 1;
1492 entries[1].textureView = a->view.Get();
1493 entries[2].binding = 2;
1494 entries[2].textureView = n->view.Get();
1495 entries[3].binding = 3;
1496 entries[3].textureView = e->view.Get();
1497 entries[4].binding = 4;
1498 entries[4].buffer = currentUboArena().buffer.Get();
1499 entries[4].size = sizeof(ShadowUBO);
1500 entries[5].binding = 5;
1501 entries[5].textureView = shadowDepthArray ? shadowDepthArray->view.Get() : nullptr;
1502 entries[6].binding = 6;
1503 entries[6].textureView = h->view.Get();
1504 entries[7].binding = 7;
1505 entries[7].sampler = mainSampler.Get();
1506 entries[8].binding = 8;
1507 entries[8].sampler = shadowDepthArray ? shadowDepthArray->sampler.Get() : nullptr;
1508 entries[9].binding = 9;
1509 entries[9].textureView = d->view.Get();
1510
1511 (void)frameUboOffset;
1512 (void)shadowUboOffset;
1513 (void)pushUboOffset;
1514 WGPUBindGroupDescriptor desc{};
1515 desc.label = sv("eve_mesh_group");
1516 desc.layout = mesh3dSetLayout.Get();
1517 desc.entryCount = 10;
1518 desc.entries = entries;
1519 return device.CreateBindGroup(reinterpret_cast<const wgpu::BindGroupDescriptor*>(&desc));
1520}
1521
1522// ---------------------------------------------------------------------------
1523// Mesh creation
1524// ---------------------------------------------------------------------------
1525
1526Mesh *Graphics::newMeshFromArrays(const float *posXYZ, const float *nrmXYZ, const float *uvST,
1527 int vertexCount, const uint32_t *indices, int indexCount) {
1528 if (vertexCount <= 0 || !posXYZ) throw Exception("newMeshFromArrays: invalid vertex data");
1529
1530 std::vector<float> verts;
1531 verts.reserve(vertexCount * 8);
1532 for (int i = 0; i < vertexCount; ++i) {
1533 verts.push_back(posXYZ[i * 3 + 0]);
1534 verts.push_back(posXYZ[i * 3 + 1]);
1535 verts.push_back(posXYZ[i * 3 + 2]);
1536 if (nrmXYZ) {
1537 verts.push_back(nrmXYZ[i * 3 + 0]);
1538 verts.push_back(nrmXYZ[i * 3 + 1]);
1539 verts.push_back(nrmXYZ[i * 3 + 2]);
1540 } else {
1541 verts.push_back(0.f);
1542 verts.push_back(0.f);
1543 verts.push_back(1.f);
1544 }
1545 if (uvST) {
1546 verts.push_back(uvST[i * 2 + 0]);
1547 verts.push_back(uvST[i * 2 + 1]);
1548 } else {
1549 verts.push_back(0.f);
1550 verts.push_back(0.f);
1551 }
1552 }
1553
1554 auto gpu = std::make_unique<GpuMesh>();
1555 gpu->vertexCount = uint32_t(vertexCount);
1556 gpu->indexCount = indexCount > 0 ? uint32_t(indexCount) : 0;
1557 gpu->vertexStride = 32;
1558
1559 WGPUBufferDescriptor vbd{};
1560 vbd.label = sv("eve_mesh_vb");
1561 vbd.size = verts.size() * sizeof(float);
1562 vbd.usage = WGPUBufferUsage_CopyDst | WGPUBufferUsage_Vertex;
1563 vbd.mappedAtCreation = false;
1564 gpu->vertexBuffer = device.CreateBuffer(reinterpret_cast<const wgpu::BufferDescriptor*>(&vbd));
1565 queue.WriteBuffer(gpu->vertexBuffer, 0, verts.data(), vbd.size);
1566
1567 if (indexCount > 0) {
1568 // 16-bit index format halves index memory for meshes with <= 65535 verts.
1569 if (vertexCount <= 65535) {
1570 std::vector<uint16_t> idx16;
1571 idx16.reserve(indexCount);
1572 for (int i = 0; i < indexCount; ++i) idx16.push_back(uint16_t(indices[i]));
1573 WGPUBufferDescriptor ibd{};
1574 ibd.label = sv("eve_mesh_ib");
1575 ibd.size = uint64_t(indexCount) * sizeof(uint16_t);
1576 ibd.usage = WGPUBufferUsage_CopyDst | WGPUBufferUsage_Index;
1577 ibd.mappedAtCreation = false;
1578 gpu->indexBuffer = device.CreateBuffer(reinterpret_cast<const wgpu::BufferDescriptor*>(&ibd));
1579 queue.WriteBuffer(gpu->indexBuffer, 0, idx16.data(), ibd.size);
1580 gpu->indexFormat = wgpu::IndexFormat::Uint16;
1581 } else {
1582 WGPUBufferDescriptor ibd{};
1583 ibd.label = sv("eve_mesh_ib");
1584 ibd.size = uint64_t(indexCount) * sizeof(uint32_t);
1585 ibd.usage = WGPUBufferUsage_CopyDst | WGPUBufferUsage_Index;
1586 ibd.mappedAtCreation = false;
1587 gpu->indexBuffer = device.CreateBuffer(reinterpret_cast<const wgpu::BufferDescriptor*>(&ibd));
1588 queue.WriteBuffer(gpu->indexBuffer, 0, indices, ibd.size);
1589 gpu->indexFormat = wgpu::IndexFormat::Uint32;
1590 }
1591 }
1592
1593 auto *mesh = new Mesh();
1594 mesh->indexCount = indexCount;
1595 mesh->gpuHandle = gpu.get();
1596 mesh->computeBounds(posXYZ, vertexCount);
1597 ownedGpuMeshes.push_back(std::move(gpu));
1598 ownedMeshes.push_back(std::unique_ptr<Mesh>(mesh));
1599 return mesh;
1600}
1601
1603 aiMatrix4x4 ident;
1604 return newMeshFromAssimp(mesh, ident);
1605}
1606
1607Mesh *Graphics::newMeshFromAssimp(const ::aiMesh &mesh, const aiMatrix4x4 &worldTransform) {
1608 std::vector<float> pos, nrm, uv;
1609 std::vector<uint32_t> idx;
1610 pos.reserve(mesh.mNumVertices * 3);
1611 nrm.reserve(mesh.mNumVertices * 3);
1612 uv.reserve(mesh.mNumVertices * 2);
1613 idx.reserve(mesh.mNumFaces * 3);
1614
1615 for (unsigned i = 0; i < mesh.mNumVertices; ++i) {
1616 aiVector3D p = worldTransform * mesh.mVertices[i];
1617 pos.push_back(p.x);
1618 pos.push_back(p.y);
1619 pos.push_back(p.z);
1620 if (mesh.mNormals) {
1621 aiMatrix3x3 rot(worldTransform);
1622 aiVector3D n = rot * mesh.mNormals[i];
1623 n.Normalize();
1624 nrm.push_back(n.x);
1625 nrm.push_back(n.y);
1626 nrm.push_back(n.z);
1627 } else {
1628 nrm.push_back(0.f);
1629 nrm.push_back(0.f);
1630 nrm.push_back(1.f);
1631 }
1632 if (mesh.mTextureCoords[0]) {
1633 uv.push_back(mesh.mTextureCoords[0][i].x);
1634 uv.push_back(mesh.mTextureCoords[0][i].y);
1635 } else {
1636 uv.push_back(0.f);
1637 uv.push_back(0.f);
1638 }
1639 }
1640 for (unsigned f = 0; f < mesh.mNumFaces; ++f) {
1641 for (unsigned v = 0; v < mesh.mFaces[f].mNumIndices; ++v)
1642 idx.push_back(mesh.mFaces[f].mIndices[v]);
1643 }
1644 Mesh *m = newMeshFromArrays(pos.data(), nrm.data(), uv.data(), int(mesh.mNumVertices), idx.data(),
1645 int(idx.size()));
1646 if (m && mesh.mNumAnimMeshes > 0) {
1647 m->initMorphBase(int(mesh.mNumVertices), pos.data(), nrm.data(), uv.data());
1648 for (unsigned a = 0; a < mesh.mNumAnimMeshes; ++a) {
1649 const aiAnimMesh *am = mesh.mAnimMeshes[a];
1650 std::vector<float> absPos(am->mNumVertices * 3);
1651 for (unsigned i = 0; i < am->mNumVertices; ++i) {
1652 absPos[i * 3 + 0] = am->mVertices[i].x;
1653 absPos[i * 3 + 1] = am->mVertices[i].y;
1654 absPos[i * 3 + 2] = am->mVertices[i].z;
1655 }
1656 std::string name = am->mName.C_Str();
1657 if (name.empty()) name = "morph" + std::to_string(a);
1658 m->addMorphTargetAbsolute(name, absPos.data());
1659 }
1660 }
1661 return m;
1662}
1663
1665 if (!mesh || !mesh->hasMorphData() || !mesh->isMorphDirty()) return false;
1666 auto *gpu = static_cast<GpuMesh *>(mesh->gpuHandle);
1667 if (!gpu || !gpu->vertexBuffer) return false;
1668 std::vector<float> pos, nrm;
1669 mesh->computeMorphedPositions(pos, nrm);
1670 if (pos.empty()) return false;
1671 mesh->computeBounds(pos.data(), mesh->getVertexCount());
1672 std::vector<float> verts;
1673 verts.reserve(mesh->getVertexCount() * 8);
1674 const auto &uvs = mesh->baseUv();
1675 for (int i = 0; i < mesh->getVertexCount(); ++i) {
1676 verts.push_back(pos[i * 3 + 0]);
1677 verts.push_back(pos[i * 3 + 1]);
1678 verts.push_back(pos[i * 3 + 2]);
1679 if (nrm.size() >= size_t((i + 1) * 3)) {
1680 verts.push_back(nrm[i * 3 + 0]);
1681 verts.push_back(nrm[i * 3 + 1]);
1682 verts.push_back(nrm[i * 3 + 2]);
1683 } else {
1684 verts.push_back(0.f);
1685 verts.push_back(0.f);
1686 verts.push_back(1.f);
1687 }
1688 if (uvs.size() >= size_t((i + 1) * 2)) {
1689 verts.push_back(uvs[i * 2 + 0]);
1690 verts.push_back(uvs[i * 2 + 1]);
1691 } else {
1692 verts.push_back(0.f);
1693 verts.push_back(0.f);
1694 }
1695 }
1696 queue.WriteBuffer(gpu->vertexBuffer, 0, verts.data(), verts.size() * sizeof(float));
1697 mesh->markMorphClean();
1698 return true;
1699}
1700
1701bool Graphics::updateMeshVertices(Mesh *mesh, const float *posXYZ, const float *nrmXYZ,
1702 const float *uvST, int vertexCount, const uint32_t *indices,
1703 int indexCount) {
1704 // WebGPU backend keeps mesh buffers immutable; rebuild via newMeshFromArrays.
1705 (void)mesh;
1706 (void)posXYZ;
1707 (void)nrmXYZ;
1708 (void)uvST;
1709 (void)vertexCount;
1710 (void)indices;
1711 (void)indexCount;
1712 return false;
1713}
1714
1716 if (!mesh || !mesh->gpuHandle) return false;
1717
1718 auto *gpu = static_cast<GpuMesh *>(mesh->gpuHandle);
1719 auto gpuIt = std::find_if(ownedGpuMeshes.begin(), ownedGpuMeshes.end(),
1720 [&](const std::unique_ptr<GpuMesh> &g) {
1721 return g.get() == gpu;
1722 });
1723 if (gpuIt == ownedGpuMeshes.end()) return false;
1724
1725 auto meshIt = std::find_if(ownedMeshes.begin(), ownedMeshes.end(),
1726 [&](const std::unique_ptr<Mesh> &m) {
1727 return m.get() == mesh;
1728 });
1729 if (meshIt == ownedMeshes.end()) return false;
1730
1731 mesh->gpuHandle = nullptr;
1732 ownedGpuMeshes.erase(gpuIt);
1733 // Transfer the CPU facade to the caller instead of destroying it.
1734 (void)meshIt->release();
1735 ownedMeshes.erase(meshIt);
1736 return true;
1737}
1738
1739Mesh *Graphics::newMeshSphere(int slices, int stacks) {
1740 std::vector<float> pos, nrm, uv;
1741 std::vector<uint32_t> idx;
1742 for (int y = 0; y <= stacks; ++y) {
1743 for (int x = 0; x <= slices; ++x) {
1744 float u = float(x) / float(slices);
1745 float v = float(y) / float(stacks);
1746 float theta = u * 2.f * glm::pi<float>();
1747 float phi = v * glm::pi<float>();
1748 float sx = std::sin(phi) * std::cos(theta);
1749 float sy = std::cos(phi);
1750 float sz = std::sin(phi) * std::sin(theta);
1751 pos.push_back(sx);
1752 pos.push_back(sy);
1753 pos.push_back(sz);
1754 nrm.push_back(sx);
1755 nrm.push_back(sy);
1756 nrm.push_back(sz);
1757 uv.push_back(u);
1758 uv.push_back(v);
1759 }
1760 }
1761 for (int y = 0; y < stacks; ++y) {
1762 for (int x = 0; x < slices; ++x) {
1763 int a = y * (slices + 1) + x;
1764 int b = a + slices + 1;
1765 idx.push_back(a);
1766 idx.push_back(b);
1767 idx.push_back(a + 1);
1768 idx.push_back(b);
1769 idx.push_back(b + 1);
1770 idx.push_back(a + 1);
1771 }
1772 }
1773 return newMeshFromArrays(pos.data(), nrm.data(), uv.data(), int(pos.size() / 3), idx.data(),
1774 int(idx.size()));
1775}
1776
1777Mesh *Graphics::newMeshCylinder(int slices, int stacks, bool caps) {
1778 std::vector<float> pos, nrm, uv;
1779 std::vector<uint32_t> idx;
1780 for (int y = 0; y <= stacks; ++y) {
1781 for (int x = 0; x <= slices; ++x) {
1782 float u = float(x) / float(slices);
1783 float v = float(y) / float(stacks);
1784 float theta = u * 2.f * glm::pi<float>();
1785 pos.push_back(std::cos(theta));
1786 pos.push_back(v * 2.f - 1.f);
1787 pos.push_back(std::sin(theta));
1788 nrm.push_back(std::cos(theta));
1789 nrm.push_back(0.f);
1790 nrm.push_back(std::sin(theta));
1791 uv.push_back(u);
1792 uv.push_back(v);
1793 }
1794 }
1795 for (int y = 0; y < stacks; ++y) {
1796 for (int x = 0; x < slices; ++x) {
1797 int a = y * (slices + 1) + x;
1798 int b = a + slices + 1;
1799 idx.push_back(a);
1800 idx.push_back(b);
1801 idx.push_back(a + 1);
1802 idx.push_back(b);
1803 idx.push_back(b + 1);
1804 idx.push_back(a + 1);
1805 }
1806 }
1807 if (caps) {
1808 int base = int(pos.size() / 3);
1809 pos.push_back(0.f); pos.push_back(-1.f); pos.push_back(0.f);
1810 nrm.push_back(0.f); nrm.push_back(-1.f); nrm.push_back(0.f);
1811 uv.push_back(0.5f); uv.push_back(0.5f);
1812 for (int x = 0; x < slices; ++x) {
1813 float theta0 = float(x) / float(slices) * 2.f * glm::pi<float>();
1814 float theta1 = float(x + 1) / float(slices) * 2.f * glm::pi<float>();
1815 int i0 = int(pos.size() / 3);
1816 pos.push_back(std::cos(theta0)); pos.push_back(-1.f); pos.push_back(std::sin(theta0));
1817 nrm.push_back(0.f); nrm.push_back(-1.f); nrm.push_back(0.f);
1818 uv.push_back(0.5f + 0.5f * std::cos(theta0)); uv.push_back(0.5f + 0.5f * std::sin(theta0));
1819 int i1 = int(pos.size() / 3);
1820 pos.push_back(std::cos(theta1)); pos.push_back(-1.f); pos.push_back(std::sin(theta1));
1821 nrm.push_back(0.f); nrm.push_back(-1.f); nrm.push_back(0.f);
1822 uv.push_back(0.5f + 0.5f * std::cos(theta1)); uv.push_back(0.5f + 0.5f * std::sin(theta1));
1823 idx.push_back(base);
1824 idx.push_back(i1);
1825 idx.push_back(i0);
1826 }
1827 base = int(pos.size() / 3);
1828 pos.push_back(0.f); pos.push_back(1.f); pos.push_back(0.f);
1829 nrm.push_back(0.f); nrm.push_back(1.f); nrm.push_back(0.f);
1830 uv.push_back(0.5f); uv.push_back(0.5f);
1831 for (int x = 0; x < slices; ++x) {
1832 float theta0 = float(x) / float(slices) * 2.f * glm::pi<float>();
1833 float theta1 = float(x + 1) / float(slices) * 2.f * glm::pi<float>();
1834 int i0 = int(pos.size() / 3);
1835 pos.push_back(std::cos(theta0)); pos.push_back(1.f); pos.push_back(std::sin(theta0));
1836 nrm.push_back(0.f); nrm.push_back(1.f); nrm.push_back(0.f);
1837 uv.push_back(0.5f + 0.5f * std::cos(theta0)); uv.push_back(0.5f + 0.5f * std::sin(theta0));
1838 int i1 = int(pos.size() / 3);
1839 pos.push_back(std::cos(theta1)); pos.push_back(1.f); pos.push_back(std::sin(theta1));
1840 nrm.push_back(0.f); nrm.push_back(1.f); nrm.push_back(0.f);
1841 uv.push_back(0.5f + 0.5f * std::cos(theta1)); uv.push_back(0.5f + 0.5f * std::sin(theta1));
1842 idx.push_back(base);
1843 idx.push_back(i0);
1844 idx.push_back(i1);
1845 }
1846 }
1847 return newMeshFromArrays(pos.data(), nrm.data(), uv.data(), int(pos.size() / 3), idx.data(),
1848 int(idx.size()));
1849}
1850
1851// ---------------------------------------------------------------------------
1852// 2D drawing
1853// ---------------------------------------------------------------------------
1854
1855void Graphics::clear2DBatches() {
1856 solidBatches.clear();
1857 texturedBatches.clear();
1858 litBatches.clear();
1859 overlaySpans.clear();
1860 sceneColorComposited = false;
1861}
1862
1863void Graphics::noteSolidOverlay() {
1864 if (solidBatches.empty()) return;
1865 const uint32_t idx = uint32_t(solidBatches.size() - 1);
1866 const uint32_t n = uint32_t(solidBatches.back().batch.vertices().size());
1867 if (!overlaySpans.empty() && overlaySpans.back().kind == OverlayKind::Solid &&
1868 overlaySpans.back().index == idx) {
1869 overlaySpans.back().vertCount = n - overlaySpans.back().vertBegin;
1870 return;
1871 }
1872 const uint32_t begin = n >= 6u ? n - 6u : 0u;
1873 overlaySpans.push_back({OverlayKind::Solid, idx, begin, n - begin});
1874}
1875
1876void Graphics::noteTexturedOverlay(Texture *tex) {
1877 if (tex && tex == getSceneColorTexture()) sceneColorComposited = true;
1878 const uint32_t idx = texturedBatches.empty() ? 0u : uint32_t(texturedBatches.size() - 1);
1879 if (!overlaySpans.empty() && overlaySpans.back().kind == OverlayKind::Textured &&
1880 overlaySpans.back().index == idx)
1881 return;
1882 overlaySpans.push_back({OverlayKind::Textured, idx, 0, 0});
1883}
1884
1885void Graphics::drawSolidRect(float x, float y, float w, float h, const Color &color,
1886 BlendMode blend) {
1887 auto it = std::find_if(solidBatches.begin(), solidBatches.end(),
1888 [&](const SolidBatch &sb) { return sb.blend == blend; });
1889 if (it == solidBatches.end()) {
1890 solidBatches.push_back(SolidBatch{blend, Batcher{}});
1891 it = solidBatches.end() - 1;
1892 }
1893 it->batch.addRect(x, y, w, h, color);
1894 noteSolidOverlay();
1895}
1896
1897void Graphics::drawSolidRectRotated(float cx, float cy, float w, float h, float degrees,
1898 const Color &color, BlendMode blend) {
1899 auto it = std::find_if(solidBatches.begin(), solidBatches.end(),
1900 [&](const SolidBatch &sb) { return sb.blend == blend; });
1901 if (it == solidBatches.end()) {
1902 solidBatches.push_back(SolidBatch{blend, Batcher{}});
1903 it = solidBatches.end() - 1;
1904 }
1905 it->batch.addRectRotated(cx, cy, w, h, degrees, color);
1906 noteSolidOverlay();
1907}
1908
1909void Graphics::drawTexturedRect(Texture *texture, float x, float y, float w, float h,
1910 const Color &color) {
1911 drawTexturedRectUV(texture, x, y, w, h, 0.f, 0.f, 1.f, 1.f, color);
1912}
1913
1914void Graphics::drawTexturedRectShader(Texture *texture, Shader *shader, float x, float y, float w,
1915 float h, const Color &color) {
1916 drawTexturedRectShaderUV(texture, shader, x, y, w, h, 0.f, 0.f, 1.f, 1.f, color);
1917}
1918
1919void Graphics::drawTexturedRectUV(Texture *texture, float x, float y, float w, float h, float u0,
1920 float v0, float u1, float v1, const Color &color) {
1921 drawTexturedRectShaderUV(texture, currentShader, x, y, w, h, u0, v0, u1, v1, color);
1922}
1923
1924void Graphics::drawTexturedRectShaderUV(Texture *texture, Shader *shader, float x, float y, float w,
1925 float h, float u0, float v0, float u1, float v1,
1926 const Color &color, bool rotatedUV, BlendMode blend) {
1927 if (!texture) {
1928 drawSolidRect(x, y, w, h, color, blend);
1929 return;
1930 }
1931 if (texturedBatches.empty() || texturedBatches.back().texture != texture ||
1932 texturedBatches.back().shader != shader || texturedBatches.back().depth != nullptr ||
1933 texturedBatches.back().blend != blend) {
1934 texturedBatches.push_back(TexturedBatch{texture, nullptr, shader, blend, Batcher{}});
1935 }
1936 texturedBatches.back().batch.addTexturedRect(x, y, w, h, color, u0, v0, u1, v1, rotatedUV);
1937 noteTexturedOverlay(texture);
1938}
1939
1941 float w, float h, float degrees, float u0, float v0,
1942 float u1, float v1, const Color &color,
1943 bool rotatedUV, BlendMode blend) {
1944 if (!texture) {
1945 drawSolidRect(cx - w * 0.5f, cy - h * 0.5f, w, h, color, blend);
1946 return;
1947 }
1948 auto it = std::find_if(texturedBatches.begin(), texturedBatches.end(),
1949 [&](const TexturedBatch &tb) {
1950 return tb.texture == texture && tb.shader == shader &&
1951 tb.depth == nullptr && tb.blend == blend;
1952 });
1953 if (it == texturedBatches.end()) {
1954 texturedBatches.push_back(TexturedBatch{texture, nullptr, shader, blend, Batcher{}});
1955 it = texturedBatches.end() - 1;
1956 }
1957 it->batch.addTexturedRectRotated(cx, cy, w, h, degrees, color, u0, v0, u1, v1, rotatedUV);
1958 noteTexturedOverlay(texture);
1959}
1960
1962 float y, float w, float h, const Color &tint) {
1963 if (!color) {
1964 drawSolidRect(x, y, w, h, tint);
1965 return;
1966 }
1967 auto it = std::find_if(texturedBatches.begin(), texturedBatches.end(),
1968 [&](const TexturedBatch &tb) {
1969 return tb.texture == color && tb.shader == shader &&
1970 tb.depth == depth;
1971 });
1972 if (it == texturedBatches.end()) {
1973 texturedBatches.push_back(TexturedBatch{color, depth, shader, BlendMode::Alpha, Batcher{}});
1974 it = texturedBatches.end() - 1;
1975 }
1976 it->batch.addTexturedRect(x, y, w, h, tint, 0.f, 0.f, 1.f, 1.f, false);
1977 noteTexturedOverlay(color);
1978}
1979
1981 float h, float u0, float v0, float u1, float v1,
1982 const Color &color) {
1983 if (!albedo) {
1984 drawSolidRect(x, y, w, h, color);
1985 return;
1986 }
1987 auto it = std::find_if(litBatches.begin(), litBatches.end(),
1988 [&](const LitBatch &lb) {
1989 return lb.albedo == albedo && lb.normal == normal;
1990 });
1991 if (it == litBatches.end()) {
1992 litBatches.push_back(LitBatch{albedo, normal, Batcher{}});
1993 it = litBatches.end() - 1;
1994 }
1995 it->batch.addTexturedRect(x, y, w, h, color, u0, v0, u1, v1, false);
1996}
1997
1999 // Uploaded on demand before flushing lit batches.
2000 lighting2dFrame = ubo;
2001}
2002
2003void Graphics::flush2D(wgpu::RenderPassEncoder pass, int viewW, int viewH,
2004 WGPUTextureFormat format) {
2005 auto spans = std::move(overlaySpans);
2006 const bool offscreen = uint32_t(format) != uint32_t(surfaceFormat);
2007
2008 struct SolidUpload {
2009 BlendMode blend = BlendMode::Alpha;
2010 uint64_t offset = 0;
2011 uint64_t bytes = 0;
2012 };
2013 std::vector<SolidUpload> solidUploads;
2014 solidUploads.reserve(solidBatches.size());
2015 for (const auto &sb : solidBatches) {
2016 if (sb.batch.empty()) {
2017 solidUploads.push_back(SolidUpload{sb.blend, 0, 0});
2018 continue;
2019 }
2020 Batcher ndc = sb.batch;
2021 ndc.toNDC(viewW, viewH);
2022 auto verts = ndc.vertices();
2023 // Batcher::toNDC targets Vulkan's Y-down NDC ((-1,-1)=top-left), but
2024 // WebGPU's clip space is Y-up ((-1,-1)=bottom-left). Flip Y so logical
2025 // top-left maps to the swapchain's top-left.
2026 for (auto &v : verts) v.pos.y = -v.pos.y;
2027 std::vector<float> data;
2028 data.reserve(verts.size() * 6);
2029 for (const auto &v : verts) {
2030 data.push_back(v.pos.x);
2031 data.push_back(v.pos.y);
2032 data.push_back(v.color.r);
2033 data.push_back(v.color.g);
2034 data.push_back(v.color.b);
2035 data.push_back(v.color.a);
2036 }
2037 const uint64_t bytes = data.size() * sizeof(float);
2038 auto &arena = currentVertexArena();
2039 ensureVertexArena(arena, arena.used + bytes);
2040 const uint64_t offset = arena.alloc(bytes);
2041 queue.WriteBuffer(arena.buffer, offset, data.data(), bytes);
2042 solidUploads.push_back(SolidUpload{sb.blend, offset, bytes});
2043 }
2044
2045 auto solidPipe = [&](BlendMode mode) -> wgpu::RenderPipeline {
2046 switch (mode) {
2048 return offscreen ? offscreenColorAdditivePipeline : colorAdditivePipeline;
2049 case BlendMode::Opaque:
2050 return offscreen ? offscreenColorOpaquePipeline : colorOpaquePipeline;
2051 case BlendMode::Alpha:
2052 default:
2053 return offscreen ? offscreenColorPipeline : colorPipeline;
2054 }
2055 };
2056
2057 auto drawSolid = [&](uint32_t batchIndex, uint32_t first, uint32_t count) {
2058 if (batchIndex >= solidUploads.size()) return;
2059 const SolidUpload &u = solidUploads[batchIndex];
2060 if (u.bytes == 0 || count == 0) return;
2061 pass.SetPipeline(solidPipe(u.blend));
2062 pass.SetVertexBuffer(0, currentVertexArena().buffer, u.offset, u.bytes);
2063 pass.Draw(count, 1, first, 0);
2064 };
2065
2066 if (!spans.empty()) {
2067 for (const auto &sp : spans) {
2068 if (sp.kind == OverlayKind::Solid)
2069 drawSolid(sp.index, sp.vertBegin, sp.vertCount);
2070 else if (sp.kind == OverlayKind::Textured && sp.index < texturedBatches.size())
2071 drawTexturedBatch(pass, texturedBatches[sp.index], viewW, viewH, format, offscreen);
2072 else if (sp.kind == OverlayKind::Lit && sp.index < litBatches.size())
2073 drawLitBatch(pass, litBatches[sp.index], viewW, viewH, format);
2074 }
2075 } else {
2076 for (size_t i = 0; i < solidUploads.size(); ++i) {
2077 if (solidUploads[i].bytes > 0)
2078 drawSolid(uint32_t(i), 0, uint32_t(solidBatches[i].batch.vertices().size()));
2079 }
2080 for (auto &tb : texturedBatches) {
2081 if (tb.batch.empty()) continue;
2082 drawTexturedBatch(pass, tb, viewW, viewH, format, offscreen);
2083 }
2084 for (auto &lb : litBatches) {
2085 if (lb.batch.empty()) continue;
2086 drawLitBatch(pass, lb, viewW, viewH, format);
2087 }
2088 }
2089 clear2DBatches();
2090}
2091
2092void Graphics::drawTexturedBatch(wgpu::RenderPassEncoder pass, TexturedBatch &tb, int viewW,
2093 int viewH, WGPUTextureFormat format, bool offscreen) {
2094 Batcher ndc = tb.batch;
2095 ndc.toNDC(viewW, viewH);
2096 auto verts = ndc.vertices();
2097 for (auto &v : verts) v.pos.y = -v.pos.y;
2098 if (verts.empty()) return;
2099
2100 std::vector<float> data;
2101 data.reserve(verts.size() * 8);
2102 for (const auto &v : verts) {
2103 data.push_back(v.pos.x);
2104 data.push_back(v.pos.y);
2105 data.push_back(v.color.r);
2106 data.push_back(v.color.g);
2107 data.push_back(v.color.b);
2108 data.push_back(v.color.a);
2109 data.push_back(v.uv.x);
2110 data.push_back(v.uv.y);
2111 }
2112 uint64_t bytes = data.size() * sizeof(float);
2113 auto &arena = currentVertexArena();
2114 ensureVertexArena(arena, arena.used + bytes);
2115 uint64_t vtxOffset = arena.alloc(bytes);
2116 queue.WriteBuffer(arena.buffer, vtxOffset, data.data(), bytes);
2117
2118 GpuTexture *gpu = gpuForTexture(tb.texture);
2119 GpuTexture *depthGpu = gpuForTexture(tb.depth);
2120 auto &uboArena = currentUboArena();
2121 ensureUboArena(uboArena, uboArena.used + 256);
2122
2123 // Push-constant (Externals) block for custom shaders.
2124 uint32_t pushOffset = 0;
2125 if (tb.shader && tb.shader->pushConstantSize() > 0) {
2126 pushOffset = uboArena.alloc(Shader::kPushConstantBytes, 256);
2127 queue.WriteBuffer(uboArena.buffer, pushOffset, tb.shader->pushConstantData(),
2129 }
2130
2131 wgpu::BindGroup bg = makeTex2DBindGroup(gpu, depthGpu);
2132 uint32_t offsets[1] = {pushOffset};
2133
2134 wgpu::RenderPipeline pipe;
2135 if (tb.shader && tb.shader->gpuHandle) {
2136 auto *gs = static_cast<GpuShader *>(tb.shader->gpuHandle);
2137 pipe = offscreen ? gs->offscreenPipeline : gs->swapchainPipeline;
2138 } else {
2139 switch (tb.blend) {
2141 pipe = offscreen ? offscreenTexturedAdditivePipeline : texturedAdditivePipeline;
2142 break;
2143 case BlendMode::Opaque:
2144 pipe = offscreen ? offscreenTexturedOpaquePipeline : texturedOpaquePipeline;
2145 break;
2146 case BlendMode::Alpha:
2147 default:
2148 pipe = offscreen ? offscreenTexturedPipeline : texturedPipeline;
2149 break;
2150 }
2151 }
2152 if (!pipe) return;
2153 pass.SetPipeline(pipe);
2154 pass.SetBindGroup(0, bg, 1, offsets);
2155 pass.SetVertexBuffer(0, arena.buffer, vtxOffset, bytes);
2156 pass.Draw(static_cast<uint32_t>(verts.size()), 1, 0, 0);
2157}
2158
2159void Graphics::drawLitBatch(wgpu::RenderPassEncoder pass, LitBatch &lb, int viewW, int viewH,
2160 WGPUTextureFormat format) {
2161 Batcher ndc = lb.batch;
2162 ndc.toNDC(viewW, viewH);
2163 auto verts = ndc.vertices();
2164 for (auto &v : verts) v.pos.y = -v.pos.y;
2165 if (verts.empty()) return;
2166
2167 std::vector<float> data;
2168 data.reserve(verts.size() * 8);
2169 for (const auto &v : verts) {
2170 data.push_back(v.pos.x);
2171 data.push_back(v.pos.y);
2172 data.push_back(v.color.r);
2173 data.push_back(v.color.g);
2174 data.push_back(v.color.b);
2175 data.push_back(v.color.a);
2176 data.push_back(v.uv.x);
2177 data.push_back(v.uv.y);
2178 }
2179 uint64_t bytes = data.size() * sizeof(float);
2180 auto &arena = currentVertexArena();
2181 ensureVertexArena(arena, arena.used + bytes);
2182 uint64_t vtxOffset = arena.alloc(bytes);
2183 queue.WriteBuffer(arena.buffer, vtxOffset, data.data(), bytes);
2184
2185 GpuTexture *albedoGpu = gpuForTextureOrWhite(lb.albedo);
2186 GpuTexture *normalGpu = gpuForTextureOrWhite(lb.normal);
2187
2188 auto &uboArena = currentUboArena();
2189 ensureUboArena(uboArena, uboArena.used + 512);
2190 uint32_t uboOffset = uboArena.alloc(sizeof(Lighting2DUBO), 256);
2191 queue.WriteBuffer(uboArena.buffer, uboOffset, &lighting2dFrame, sizeof(Lighting2DUBO));
2192
2193 wgpu::BindGroup bg = makeTex2DBindGroup(albedoGpu, normalGpu);
2194 uint32_t offsets[1] = {uboOffset};
2195 wgpu::RenderPipeline pipe =
2196 uint32_t(format) == uint32_t(surfaceFormat) ? lit2dPipeline : offscreenLitPipeline;
2197 if (!pipe) return;
2198 pass.SetPipeline(pipe);
2199 pass.SetBindGroup(0, bg, 1, offsets);
2200 pass.SetVertexBuffer(0, arena.buffer, vtxOffset, bytes);
2201 pass.Draw(static_cast<uint32_t>(verts.size()), 1, 0, 0);
2202}
2203
2204// ---------------------------------------------------------------------------
2205// 3D rendering
2206// ---------------------------------------------------------------------------
2207
2209 if (!initialized || !device) return;
2210 frame3DStarted = true;
2211 frameHad3DThisFrame = false;
2212 // Match the desktop (Vulkan) backend: had3DThisFrame() must return true
2213 // once a 3D frame begins, or RenderSystem3D::render bails out before any
2214 // mesh is drawn.
2215 frameHad3D = true;
2216 sceneColorPassOpen = false;
2217 mesh3dDraws.clear();
2218 shadowPassDraws.clear();
2219 for (int c = 0; c < ShadowConfig::kCascades; ++c) shadowCascadeDraws[c].clear();
2220 voxelDraws.clear();
2221 if (surfaceNeedsRecreate.load()) {
2222 surfaceNeedsRecreate.store(false);
2223 markSwapchainDirty();
2224 }
2225 rebuildSwapchainIfNeeded();
2226}
2227
2229 throw eve::Exception("begin3DFrameToCanvas: not supported on the webgpu backend");
2230}
2232
2233void Graphics::setMesh3DViewProj(const glm::mat4 &viewProj) { mesh3dViewProj = viewProj; }
2234void Graphics::setMesh3DView(const glm::mat4 &view) { mesh3dView = view; }
2235void Graphics::setMesh3DClip(float nearZ, float farZ) {
2236 mesh3dNear = nearZ;
2237 mesh3dFar = farZ;
2238}
2239
2240Texture *Graphics::getSceneColorTexture() { return sceneColorTexture; }
2241
2242void Graphics::drawMesh(Mesh *mesh, const glm::mat4 &model, Texture *texture, const Color &tint) {
2243 drawMeshShader(mesh, model, texture, tint, nullptr);
2244}
2245
2246void Graphics::drawMeshShader(Mesh *mesh, const glm::mat4 &model, Texture *texture, const Color &tint,
2247 Shader *shader) {
2248 if (!mesh || !mesh->gpuHandle) return;
2249 frameHad3DThisFrame = true;
2250 frameHad3D = true;
2251 Mesh3dDraw d;
2252 d.mesh = mesh;
2253 d.texture = texture;
2254 d.model = model;
2255 d.tint = tint;
2256 d.shader = shader;
2257 mesh3dDraws.push_back(d);
2258}
2259
2263 // Custom SPIR-V mesh shaders (incl. xray) are unsupported on the WebGPU
2264 // backend (newMeshShaderFromSpv throws); store the handle for API parity so
2265 // callers work unchanged and a future WGSL xray path can sample it.
2266 mesh3dSceneDepthTexture = depth;
2267}
2269 mesh3dMetallic = metallic;
2270 mesh3dRoughness = roughness;
2271}
2272void Graphics::setMesh3DTexCellBomb(float cellScale, float strength, float rotAmount) {
2273 mesh3dTexBombScale = cellScale;
2274 mesh3dTexBombStrength = strength;
2275 mesh3dTexBombRot = rotAmount;
2276}
2277void Graphics::setMesh3DParallax(float scale, float minLayers, float maxLayers) {
2278 mesh3dParallaxScale = scale;
2279 mesh3dParallaxMin = minLayers;
2280 mesh3dParallaxMax = maxLayers;
2281}
2282void Graphics::setMesh3DLighting(const Lighting3DPack &pack) { mesh3dLighting = pack; }
2283void Graphics::setCloudShadows(float strength, float worldCell, float time, float windSpeed,
2284 float windAngle, float coverage, float detail) {
2285 mesh3dCloud = glm::vec4(std::clamp(strength, 0.f, 1.f), std::max(worldCell, 1e-4f), time, 0.f);
2286 mesh3dCloudWind = glm::vec4(std::cos(windAngle) * windSpeed, std::sin(windAngle) * windSpeed,
2287 std::clamp(coverage, 0.f, 1.f), std::clamp(detail, 0.f, 1.f));
2288}
2290 mesh3dClustered = upload;
2291 mesh3dClusteredActive = upload.active;
2292}
2293void Graphics::setMesh3DClusteredActive(bool active) { mesh3dClusteredActive = active; }
2294void Graphics::setMesh3DLight(const glm::vec3 &dir, const glm::vec3 &color) {
2295 mesh3dLighting.lights[0].posRadius = glm::vec4(dir, 0.f);
2296 mesh3dLighting.lights[0].color = glm::vec4(color, 1.f);
2297}
2298void Graphics::setMesh3DCameraPos(const glm::vec3 &eye) { mesh3dCameraPos = eye; }
2299void Graphics::setMesh3DEnv(Texture *cube, float intensity) {
2300 mesh3dEnvTexture = cube;
2301 mesh3dEnvIntensity = intensity;
2302}
2303void Graphics::setMesh3DShadows(const ShadowUpload &upload) { mesh3dShadows = upload; }
2304void Graphics::setMesh3DShadowReceive(bool receive) { mesh3dShadowReceive = receive; }
2305
2306void Graphics::beginShadowPass(int cascadeIndex) {
2307 shadowPassCascade = cascadeIndex;
2308 shadowPassDraws.clear();
2309}
2310
2311void Graphics::drawMeshShadow(Mesh *mesh, const glm::mat4 &lightMVP) {
2312 if (!mesh || !mesh->gpuHandle) return;
2313 ShadowDraw d;
2314 d.mesh = mesh;
2315 d.mvp = lightMVP;
2316 shadowPassDraws.push_back(d);
2317}
2318
2319void Graphics::drawMeshShadowAlpha(Mesh *mesh, const glm::mat4 &lightMVP, Texture *albedo) {
2320 // WebGPU shadow pipeline has no alpha-cutout variant; fall back to the
2321 // regular solid-quad shadow so callers keep working.
2322 drawMeshShadow(mesh, lightMVP);
2323 (void)albedo;
2324}
2325
2327 if (shadowPassCascade < 0 || shadowPassCascade >= ShadowConfig::kCascades) {
2328 shadowPassCascade = -1;
2329 shadowPassDraws.clear();
2330 return;
2331 }
2332 shadowCascadeDraws[shadowPassCascade] = shadowPassDraws;
2333 shadowPassDraws.clear();
2334 shadowPassCascade = -1;
2335}
2336
2337// ---------------------------------------------------------------------------
2338// GBuffer pass
2339// ---------------------------------------------------------------------------
2340
2342 if (!device) return;
2343 createGbufferResources(width, height);
2344 gbufferPassActive = true;
2345 gbufferPassPending = true;
2346 gbufferPassDraws.clear();
2347}
2348
2349void Graphics::drawMeshGBuffer(Mesh *mesh, const glm::mat4 &mvp, const glm::mat4 &model, float nearZ,
2350 float farZ, Texture *albedo, float tintR, float tintG, float tintB) {
2351 if (!mesh || !mesh->gpuHandle) return;
2352 GbufferDraw d;
2353 d.mesh = mesh;
2354 d.albedo = albedo;
2355 d.mvp = mvp;
2356 d.model = model;
2357 d.nearZ = nearZ;
2358 d.farZ = farZ;
2359 d.tint = glm::vec4(tintR, tintG, tintB, 1.f);
2360 gbufferPassDraws.push_back(d);
2361}
2362
2363void Graphics::drawMeshGBufferAlpha(Mesh *mesh, const glm::mat4 &mvp, const glm::mat4 &model,
2364 float nearZ, float farZ, Texture *albedo, float tintR,
2365 float tintG, float tintB) {
2366 // WebGPU gbuffer pipeline has no alpha-cutout variant; fall back to the
2367 // regular fill (full quad depth) so callers keep working.
2368 drawMeshGBuffer(mesh, mvp, model, nearZ, farZ, albedo, tintR, tintG, tintB);
2369}
2370
2372 gbufferPassActive = false;
2373 // Expose the G-buffer textures (depth/normal/albedo/hwDepth) to RenderControl
2374 // so post passes (AO, X-ray scene depth) can sample them this frame.
2375 if (renderControl_ && !gbufferSlots.empty()) {
2376 GbufferSlot &slot = gbufferSlots[currentFrameSlot()];
2377 renderControl_->getGBuffer()->setTargets(gbufferWidth, gbufferHeight, &slot.depthColorTex,
2378 &slot.normalTex, &slot.albedoTex, &slot.depthTex);
2379 }
2380}
2381
2382// ---------------------------------------------------------------------------
2383// Voxel
2384// ---------------------------------------------------------------------------
2385
2386void Graphics::drawVoxelFaceInstances(const uint32_t *packed, int count, float originX,
2387 float originY, float originZ, const std::string &faceDir,
2388 Texture *atlas, int tilesPerRow, const uint32_t *ao) {
2389 if (!device || count <= 0 || !packed) return;
2390 if (!voxelUnitQuadVerts) return;
2391 // TODO(webgpu): bake ao (2 bits per corner) into the WGSL voxel shader.
2392 (void)ao;
2393 frameHad3DThisFrame = true;
2394 frameHad3D = true;
2395
2396 int face = 5;
2397 if (faceDir == "posX" || faceDir == "+x") face = 0;
2398 else if (faceDir == "negX" || faceDir == "-x") face = 1;
2399 else if (faceDir == "posY" || faceDir == "+y") face = 2;
2400 else if (faceDir == "negY" || faceDir == "-y") face = 3;
2401 else if (faceDir == "posZ" || faceDir == "+z") face = 4;
2402
2403 VoxelDraw d;
2404 d.count = uint32_t(count);
2405 d.atlas = gpuForTextureOrWhite(atlas);
2406 d.viewProj = mesh3dViewProj;
2407 d.chunkOrigin = glm::vec4(originX, originY, originZ, float(face));
2408 d.atlasInfo = glm::vec4(float(tilesPerRow), 0.f, 0.f, 0.f);
2409 d.tint = glm::vec4(1.f);
2410 d.instanceBufferOffset = 0;
2411 d.pushUboOffset = 0;
2412
2413 // Upload packed instances into the voxel instance arena (per frame slot).
2414 auto &arena = voxelInstanceArena;
2415 if (!arena.buffer) {
2416 WGPUBufferDescriptor bd{};
2417 bd.label = sv("eve_voxel_instances");
2418 bd.size = 1u << 20;
2419 bd.usage = WGPUBufferUsage_CopyDst | WGPUBufferUsage_Vertex;
2420 bd.mappedAtCreation = false;
2421 arena.buffer = device.CreateBuffer(reinterpret_cast<const wgpu::BufferDescriptor*>(&bd));
2422 arena.capacity = 1u << 20;
2423 }
2424 uint64_t need = uint64_t(count) * 4;
2425 if (arena.used + need > arena.capacity) {
2426 uint64_t cap = arena.capacity * 2;
2427 WGPUBufferDescriptor bd{};
2428 bd.label = sv("eve_voxel_instances");
2429 bd.size = cap;
2430 bd.usage = WGPUBufferUsage_CopyDst | WGPUBufferUsage_Vertex;
2431 bd.mappedAtCreation = false;
2432 arena.buffer = device.CreateBuffer(reinterpret_cast<const wgpu::BufferDescriptor*>(&bd));
2433 arena.capacity = cap;
2434 arena.used = 0;
2435 }
2436 d.instanceBufferOffset = static_cast<uint32_t>(arena.used);
2437 queue.WriteBuffer(arena.buffer, arena.used, packed, need);
2438 arena.used += need;
2439 voxelDraws.push_back(d);
2440}
2441
2442// ---------------------------------------------------------------------------
2443// Scene color / shadow / gbuffer resources
2444// ---------------------------------------------------------------------------
2445
2446void Graphics::createSceneColorResources(int width, int height) {
2447 if (!device) return;
2448 if (sceneColorWidth == width && sceneColorHeight == height && !sceneColorSlots.empty()) return;
2449
2450 destroySceneColorResources();
2451 sceneColorWidth = width;
2452 sceneColorHeight = height;
2453 sceneColorSamples = 1;
2454 for (int s = 0; s < int(kFramesInFlight); ++s) {
2455 SceneColorSlot slot;
2456 slot.sampleCount = sceneColorSamples;
2457
2458 WGPUTextureDescriptor cd{};
2459 cd.label = sv("eve_scene_color");
2460 cd.dimension = WGPUTextureDimension_2D;
2461 cd.size = {static_cast<uint32_t>(width), static_cast<uint32_t>(height), 1};
2462 cd.sampleCount = sceneColorSamples;
2463 cd.format = sceneColorFormat;
2464 cd.mipLevelCount = 1;
2465 cd.usage = WGPUTextureUsage_TextureBinding | WGPUTextureUsage_RenderAttachment |
2466 WGPUTextureUsage_CopySrc;
2467 slot.color = device.CreateTexture(reinterpret_cast<const wgpu::TextureDescriptor*>(&cd));
2468 slot.colorView = slot.color.CreateView();
2469
2470 WGPUTextureDescriptor dd{};
2471 dd.label = sv("eve_scene_depth");
2472 dd.dimension = WGPUTextureDimension_2D;
2473 dd.size = {static_cast<uint32_t>(width), static_cast<uint32_t>(height), 1};
2474 dd.sampleCount = sceneColorSamples;
2475 dd.format = WGPUTextureFormat_Depth32Float;
2476 dd.mipLevelCount = 1;
2477 dd.usage = WGPUTextureUsage_RenderAttachment;
2478 slot.depth = device.CreateTexture(reinterpret_cast<const wgpu::TextureDescriptor*>(&dd));
2479 slot.depthView = slot.depth.CreateView();
2480
2481 slot.colorGpu.texture = slot.color;
2482 slot.colorGpu.view = slot.colorView;
2483 slot.colorGpu.width = width;
2484 slot.colorGpu.height = height;
2485 slot.colorGpu.sampler = createLinearSampler(device);
2486 slot.colorGpu.samplerState = TextureSampler::linearMipmap();
2487
2488 slot.colorTex.gpuHandle = &slot.colorGpu;
2489 slot.colorTex.width = width;
2490 slot.colorTex.height = height;
2491 slot.colorTex.mipmapCount = 1;
2492
2493 sceneColorSlots.push_back(std::move(slot));
2494 }
2495 sceneColorTexture = &sceneColorSlots[0].colorTex;
2496}
2497
2498void Graphics::destroySceneColorResources() {
2499 sceneColorSlots.clear();
2500 sceneColorTexture = nullptr;
2501}
2502
2503void Graphics::createShadowResources() {
2504 if (!device) return;
2505 if (shadowDepthArray) return;
2506 shadowMapSize = ShadowConfig::kMapSize;
2507
2508 // Depth array: 3 cascade layers, renderable + sampleable.
2509 auto *gpu = new GpuTexture();
2510 WGPUTextureDescriptor td{};
2511 td.label = sv("eve_shadow_depth");
2512 td.dimension = WGPUTextureDimension_2D;
2513 td.size = {static_cast<uint32_t>(shadowMapSize), static_cast<uint32_t>(shadowMapSize), 3};
2514 td.sampleCount = 1;
2515 td.format = WGPUTextureFormat_Depth32Float;
2516 td.mipLevelCount = 1;
2517 td.usage = WGPUTextureUsage_TextureBinding | WGPUTextureUsage_RenderAttachment;
2518 gpu->texture = device.CreateTexture(reinterpret_cast<const wgpu::TextureDescriptor*>(&td));
2519
2520 WGPUTextureViewDescriptor avd{};
2521 avd.format = WGPUTextureFormat_Depth32Float;
2522 avd.dimension = WGPUTextureViewDimension_2DArray;
2523 avd.baseMipLevel = 0;
2524 avd.mipLevelCount = 1;
2525 avd.baseArrayLayer = 0;
2526 avd.arrayLayerCount = 3;
2527 gpu->view = gpu->texture.CreateView(reinterpret_cast<const wgpu::TextureViewDescriptor*>(&avd));
2528
2529 WGPUSamplerDescriptor sd{};
2530 sd.label = sv("eve_shadow_sampler");
2531 sd.addressModeU = WGPUAddressMode_ClampToEdge;
2532 sd.addressModeV = WGPUAddressMode_ClampToEdge;
2533 sd.addressModeW = WGPUAddressMode_ClampToEdge;
2534 sd.magFilter = WGPUFilterMode_Linear;
2535 sd.minFilter = WGPUFilterMode_Linear;
2536 sd.mipmapFilter = WGPUMipmapFilterMode_Nearest;
2537 sd.compare = WGPUCompareFunction_LessEqual;
2538 sd.maxAnisotropy = 1.f;
2539 gpu->sampler = device.CreateSampler(reinterpret_cast<const wgpu::SamplerDescriptor*>(&sd));
2540 shadowDepthArray = gpu;
2541}
2542
2543void Graphics::destroyShadowResources() {
2544 shadowDepthArray = nullptr;
2545}
2546
2547void Graphics::createGbufferResources(int width, int height) {
2548 if (!device) return;
2549 if (gbufferWidth == width && gbufferHeight == height && !gbufferSlots.empty()) return;
2550 destroyGbufferResources();
2551 gbufferWidth = width;
2552 gbufferHeight = height;
2553
2554 WGPUTextureDescriptor td{};
2555 td.label = sv("eve_gbuffer_target");
2556 td.dimension = WGPUTextureDimension_2D;
2557 td.size = {static_cast<uint32_t>(width), static_cast<uint32_t>(height), 1};
2558 td.sampleCount = 1;
2559 td.format = WGPUTextureFormat_RGBA8Unorm;
2560 td.mipLevelCount = 1;
2561 td.usage = WGPUTextureUsage_TextureBinding | WGPUTextureUsage_RenderAttachment;
2562
2563 WGPUTextureDescriptor dd{};
2564 dd.label = sv("eve_gbuffer_depth");
2565 dd.dimension = WGPUTextureDimension_2D;
2566 dd.size = {static_cast<uint32_t>(width), static_cast<uint32_t>(height), 1};
2567 dd.sampleCount = 1;
2568 dd.format = WGPUTextureFormat_Depth32Float;
2569 dd.mipLevelCount = 1;
2570 // TextureBinding so X-ray (and AO) can sample the scene depth in a later pass.
2571 dd.usage = WGPUTextureUsage_TextureBinding | WGPUTextureUsage_RenderAttachment;
2572
2573 for (int s = 0; s < int(kFramesInFlight); ++s) {
2574 GbufferSlot slot;
2575 slot.normal = device.CreateTexture(reinterpret_cast<const wgpu::TextureDescriptor*>(&td));
2576 slot.normalView = slot.normal.CreateView();
2577 slot.depthColor = device.CreateTexture(reinterpret_cast<const wgpu::TextureDescriptor*>(&td));
2578 slot.depthColorView = slot.depthColor.CreateView();
2579 slot.albedo = device.CreateTexture(reinterpret_cast<const wgpu::TextureDescriptor*>(&td));
2580 slot.albedoView = slot.albedo.CreateView();
2581 slot.depth = device.CreateTexture(reinterpret_cast<const wgpu::TextureDescriptor*>(&dd));
2582 slot.depthView = slot.depth.CreateView();
2583
2584 slot.normalGpu.texture = slot.normal;
2585 slot.normalGpu.view = slot.normalView;
2586 slot.normalGpu.sampler = createLinearSampler(device);
2587 slot.depthColorGpu.texture = slot.depthColor;
2588 slot.depthColorGpu.view = slot.depthColorView;
2589 slot.depthColorGpu.sampler = createLinearSampler(device);
2590 slot.albedoGpu.texture = slot.albedo;
2591 slot.albedoGpu.view = slot.albedoView;
2592 slot.albedoGpu.sampler = createLinearSampler(device);
2593 slot.depthGpu.texture = slot.depth;
2594 slot.depthGpu.view = slot.depthView;
2595 slot.depthGpu.sampler = createLinearSampler(device);
2596
2597 slot.normalTex.gpuHandle = &slot.normalGpu;
2598 slot.normalTex.width = width;
2599 slot.normalTex.height = height;
2600 slot.depthColorTex.gpuHandle = &slot.depthColorGpu;
2601 slot.depthColorTex.width = width;
2602 slot.depthColorTex.height = height;
2603 slot.albedoTex.gpuHandle = &slot.albedoGpu;
2604 slot.albedoTex.width = width;
2605 slot.albedoTex.height = height;
2606 slot.depthTex.gpuHandle = &slot.depthGpu;
2607 slot.depthTex.width = width;
2608 slot.depthTex.height = height;
2609
2610 gbufferSlots.push_back(std::move(slot));
2611 }
2612}
2613
2614void Graphics::destroyGbufferResources() { gbufferSlots.clear(); }
2615
2616// ---------------------------------------------------------------------------
2617// Flush helpers
2618// ---------------------------------------------------------------------------
2619
2620void Graphics::flushMesh3D(wgpu::RenderPassEncoder pass, WGPUTextureFormat format) {
2621 if (mesh3dDraws.empty()) return;
2622
2623 auto &uboArena = currentUboArena();
2624 ensureUboArena(uboArena, uboArena.used + mesh3dDraws.size() * 2048);
2625 auto &vtxArena = currentVertexArena();
2626
2627 // Pre-allocate per-draw UBO slots.
2628 for (auto &d : mesh3dDraws) {
2629 d.frameUboOffset = uboArena.alloc(sizeof(Mesh3DUBO), 256);
2630 d.shadowUboOffset = uboArena.alloc(sizeof(ShadowUBO), 256);
2631 d.pushUboOffset = 0;
2632 if (d.shader && d.shader->pushConstantSize() > 0)
2633 d.pushUboOffset = uboArena.alloc(Shader::kPushConstantBytes, 256);
2634 }
2635
2636 // Upload the per-draw frame UBO + shared shadow UBO.
2637 for (auto &d : mesh3dDraws) {
2638 Mesh3DUBO ubo;
2639 ubo.mvp = mesh3dViewProj * d.model;
2640 ubo.model = d.model;
2641 ubo.lightDir = glm::vec4(glm::vec3(mesh3dLighting.lights[0].posRadius), float(mesh3dLighting.count));
2642 ubo.lightColor = mesh3dLighting.lights[0].color;
2643 ubo.tint = d.tint;
2644 ubo.cameraPos = glm::vec4(mesh3dCameraPos, mesh3dRoughness);
2645 ubo.ambient = glm::vec4(glm::vec3(mesh3dLighting.ambient), mesh3dMetallic);
2646 for (int i = 0; i < Lighting3DPack::kMaxLights; ++i)
2647 ubo.lights[i] = mesh3dLighting.lights[i];
2648 ubo.texBomb = glm::vec4(mesh3dTexBombScale, mesh3dTexBombStrength, mesh3dTexBombRot, 0.f);
2649 ubo.parallax = glm::vec4(mesh3dParallaxScale, mesh3dParallaxMin, mesh3dParallaxMax, 0.f);
2650 ubo.view = mesh3dView;
2651 ubo.clipInfo = glm::vec4(mesh3dNear, mesh3dFar, 0.f, 0.f);
2652 ubo.cloud = mesh3dCloud;
2653 ubo.cloudWind = mesh3dCloudWind;
2654 ubo.lightColor.w = mesh3dEnvIntensity;
2655 // X-ray params travel through the Frame UBO (no extra binding). Packed
2656 // in bindMeshUniforms("xray") order: colorR..G..B, bias, screenW, screenH,
2657 // rimPower, rimStrength, alpha.
2658 if (d.shader && d.shader->isXray() && d.shader->pushConstantSize() >= 9 * sizeof(float)) {
2659 const float *pc = d.shader->pushConstantData();
2660 ubo.texBomb = glm::vec4(pc[0], pc[1], pc[2], pc[8]); // color.xyz + alpha
2661 ubo.parallax = glm::vec4(pc[3], pc[4], pc[5], pc[7]); // bias, screenW, screenH, rimStrength
2662 ubo.clipInfo.z = pc[6]; // rimPower
2663 }
2664 queue.WriteBuffer(uboArena.buffer, d.frameUboOffset, &ubo, sizeof(ubo));
2665 }
2666
2667 // Shared shadow UBO written once.
2668 ShadowUBO shadowUbo = mesh3dShadows.ubo;
2669 if (!mesh3dShadows.active || !mesh3dShadowReceive) shadowUbo.bias.y = 0.f;
2670 for (auto &d : mesh3dDraws)
2671 queue.WriteBuffer(uboArena.buffer, d.shadowUboOffset, &shadowUbo, sizeof(shadowUbo));
2672
2673 for (auto &d : mesh3dDraws) {
2674 auto *gpuMesh = static_cast<GpuMesh *>(d.mesh->gpuHandle);
2675 if (!gpuMesh || !gpuMesh->vertexBuffer) continue;
2676
2677 wgpu::RenderPipeline pipe = mesh3dPipeline;
2678 if (d.shader && d.shader->gpuHandle) {
2679 auto *gs = static_cast<GpuShader *>(d.shader->gpuHandle);
2680 if (gs->isMesh3D && gs->mesh3dPipeline) {
2681 if (d.shader->isXray() && gs->mesh3dXrayPipeline)
2682 pipe = gs->mesh3dXrayPipeline;
2683 else
2684 pipe = gs->mesh3dPipeline;
2685 }
2686 }
2687 if (!pipe) continue;
2688 pass.SetPipeline(pipe);
2689
2690 GpuTexture *albedo = gpuForTexture(d.texture);
2691 GpuTexture *normal = gpuForTexture(mesh3dNormalTexture);
2692 GpuTexture *env = gpuForTexture(mesh3dEnvTexture);
2693 GpuTexture *height = gpuForTexture(mesh3dHeightTexture);
2694 GpuTexture *depth = mesh3dSceneDepthTexture ? gpuForTexture(mesh3dSceneDepthTexture)
2695 : flatDepthTexture3D;
2696 wgpu::BindGroup bg = makeMeshBindGroup(albedo, normal, env, height, depth,
2697 d.frameUboOffset, d.shadowUboOffset,
2698 d.pushUboOffset);
2699 uint32_t offsets[2] = {d.frameUboOffset, d.shadowUboOffset};
2700 pass.SetBindGroup(0, bg, 2, offsets);
2701
2702 if (gpuMesh->indexBuffer) {
2703 pass.SetVertexBuffer(0, gpuMesh->vertexBuffer, 0, gpuMesh->vertexCount * 32);
2704 pass.SetIndexBuffer(gpuMesh->indexBuffer, gpuMesh->indexFormat, 0,
2705 uint64_t(gpuMesh->indexCount) * 4);
2706 pass.DrawIndexed(gpuMesh->indexCount, 1, 0, 0, 0);
2707 } else {
2708 pass.SetVertexBuffer(0, gpuMesh->vertexBuffer, 0, gpuMesh->vertexCount * 32);
2709 pass.Draw(gpuMesh->vertexCount, 1, 0, 0);
2710 }
2711 }
2712 mesh3dDraws.clear();
2713}
2714
2715void Graphics::flushShadowPass(wgpu::RenderPassEncoder pass) {
2716 auto &uboArena = currentUboArena();
2717 ensureUboArena(uboArena, uboArena.used + 4096);
2718 pass.SetPipeline(mesh3dShadowPipeline);
2719
2720 for (int c = 0; c < ShadowConfig::kCascades; ++c) {
2721 if (shadowCascadeDraws[c].empty()) continue;
2722 for (auto &d : shadowCascadeDraws[c]) {
2723 auto *gpuMesh = static_cast<GpuMesh *>(d.mesh->gpuHandle);
2724 if (!gpuMesh || !gpuMesh->vertexBuffer) continue;
2725
2726 uint32_t offset = uboArena.alloc(256, 256);
2727 queue.WriteBuffer(uboArena.buffer, offset, &d.mvp, sizeof(glm::mat4));
2728
2729 WGPUBindGroupEntry entry{};
2730 entry.binding = 0;
2731 entry.buffer = uboArena.buffer.Get();
2732 entry.size = 64;
2733 WGPUBindGroupDescriptor bgd{};
2734 bgd.layout = shadowSetLayout.Get();
2735 bgd.entryCount = 1;
2736 bgd.entries = &entry;
2737 wgpu::BindGroup bg = device.CreateBindGroup(reinterpret_cast<const wgpu::BindGroupDescriptor*>(&bgd));
2738 uint32_t offsets[1] = {offset};
2739 pass.SetBindGroup(0, bg, 1, offsets);
2740
2741 if (gpuMesh->indexBuffer) {
2742 pass.SetVertexBuffer(0, gpuMesh->vertexBuffer, 0, gpuMesh->vertexCount * 32);
2743 pass.SetIndexBuffer(gpuMesh->indexBuffer, gpuMesh->indexFormat, 0,
2744 uint64_t(gpuMesh->indexCount) * 4);
2745 pass.DrawIndexed(gpuMesh->indexCount, 1, 0, 0, 0);
2746 } else {
2747 pass.SetVertexBuffer(0, gpuMesh->vertexBuffer, 0, gpuMesh->vertexCount * 32);
2748 pass.Draw(gpuMesh->vertexCount, 1, 0, 0);
2749 }
2750 }
2751 shadowCascadeDraws[c].clear();
2752 }
2753}
2754
2755void Graphics::flushGbufferPass(wgpu::RenderPassEncoder pass) {
2756 if (gbufferPassDraws.empty() || gbufferSlots.empty()) return;
2757 auto &uboArena = currentUboArena();
2758 ensureUboArena(uboArena, uboArena.used + gbufferPassDraws.size() * 512);
2759 pass.SetPipeline(mesh3dGbufferPipeline);
2760
2761 for (auto &d : gbufferPassDraws) {
2762 auto *gpuMesh = static_cast<GpuMesh *>(d.mesh->gpuHandle);
2763 if (!gpuMesh || !gpuMesh->vertexBuffer) continue;
2764
2765 struct GbufferPush {
2766 glm::mat4 mvp;
2767 glm::mat4 model;
2768 glm::vec4 clip;
2769 } push;
2770 push.mvp = d.mvp;
2771 push.model = d.model;
2772 push.clip = glm::vec4(d.nearZ, d.farZ, 0.f, 0.f);
2773
2774 uint32_t offset = uboArena.alloc(256, 256);
2775 queue.WriteBuffer(uboArena.buffer, offset, &push, sizeof(push));
2776
2777 GpuTexture *albedo = gpuForTextureOrWhite(d.albedo);
2778 WGPUBindGroupEntry entries[3]{};
2779 entries[0].binding = 0;
2780 entries[0].buffer = uboArena.buffer.Get();
2781 entries[0].size = 128;
2782 entries[1].binding = 1;
2783 entries[1].textureView = albedo->view.Get();
2784 entries[2].binding = 2;
2785 entries[2].sampler = albedo->sampler.Get();
2786 WGPUBindGroupDescriptor bgd{};
2787 bgd.layout = gbufferSetLayout.Get();
2788 bgd.entryCount = 3;
2789 bgd.entries = entries;
2790 wgpu::BindGroup bg = device.CreateBindGroup(reinterpret_cast<const wgpu::BindGroupDescriptor*>(&bgd));
2791 uint32_t offsets[1] = {offset};
2792 pass.SetBindGroup(0, bg, 1, offsets);
2793
2794 if (gpuMesh->indexBuffer) {
2795 pass.SetVertexBuffer(0, gpuMesh->vertexBuffer, 0, gpuMesh->vertexCount * 32);
2796 pass.SetIndexBuffer(gpuMesh->indexBuffer, gpuMesh->indexFormat, 0,
2797 uint64_t(gpuMesh->indexCount) * 4);
2798 pass.DrawIndexed(gpuMesh->indexCount, 1, 0, 0, 0);
2799 } else {
2800 pass.SetVertexBuffer(0, gpuMesh->vertexBuffer, 0, gpuMesh->vertexCount * 32);
2801 pass.Draw(gpuMesh->vertexCount, 1, 0, 0);
2802 }
2803 }
2804 gbufferPassDraws.clear();
2805 gbufferPassPending = false;
2806}
2807
2808void Graphics::flushVoxelDraws(wgpu::RenderPassEncoder pass, WGPUTextureFormat format) {
2809 if (voxelDraws.empty() || !voxelRectPipeline) return;
2810 auto &uboArena = currentUboArena();
2811 ensureUboArena(uboArena, uboArena.used + voxelDraws.size() * 512);
2812 pass.SetPipeline(voxelRectPipeline);
2813
2814 for (auto &d : voxelDraws) {
2815 uint32_t offset = uboArena.alloc(256, 256);
2816 struct VoxelPC {
2817 glm::mat4 viewProj;
2818 glm::vec4 chunkOrigin;
2819 glm::vec4 atlasInfo;
2820 glm::vec4 tint;
2821 } pc;
2822 pc.viewProj = d.viewProj;
2823 pc.chunkOrigin = d.chunkOrigin;
2824 pc.atlasInfo = d.atlasInfo;
2825 pc.tint = d.tint;
2826 queue.WriteBuffer(uboArena.buffer, offset, &pc, sizeof(pc));
2827
2828 WGPUBindGroupEntry entries[3]{};
2829 entries[0].binding = 0;
2830 entries[0].buffer = uboArena.buffer.Get();
2831 entries[0].size = sizeof(pc);
2832 entries[1].binding = 1;
2833 entries[1].textureView = d.atlas->view.Get();
2834 entries[2].binding = 2;
2835 entries[2].sampler = d.atlas->sampler.Get();
2836 WGPUBindGroupDescriptor bgd{};
2837 bgd.layout = voxelSetLayout.Get();
2838 bgd.entryCount = 3;
2839 bgd.entries = entries;
2840 wgpu::BindGroup bg = device.CreateBindGroup(reinterpret_cast<const wgpu::BindGroupDescriptor*>(&bgd));
2841 uint32_t offsets[1] = {offset};
2842 pass.SetBindGroup(0, bg, 1, offsets);
2843
2844 pass.SetVertexBuffer(0, voxelUnitQuadVerts, 0, 32);
2845 pass.SetVertexBuffer(1, voxelInstanceArena.buffer, d.instanceBufferOffset,
2846 uint64_t(d.count) * 4);
2847 pass.SetIndexBuffer(voxelUnitQuadIndices, wgpu::IndexFormat::Uint32, 0, 24);
2848 pass.DrawIndexed(6, d.count, 0, 0, 0);
2849 }
2850 voxelDraws.clear();
2851}
2852
2853// ---------------------------------------------------------------------------
2854// Present
2855// ---------------------------------------------------------------------------
2856
2857bool Graphics::acquireSurfaceTexture(wgpu::TextureView &view, wgpu::Texture &texture) {
2858 if (!surface || !device || !swapchainConfigured) return false;
2859 wgpu::SurfaceTexture surfTex{};
2860 surface.GetCurrentTexture(&surfTex);
2861 if (!surfTex.texture) return false;
2862 texture = surfTex.texture;
2863 view = texture.CreateView();
2864 return bool(view);
2865}
2866
2868#ifdef EVENGINE_WEBGPU
2869 if (device) device.PushErrorScope(wgpu::ErrorFilter::Validation);
2870#endif
2871}
2872
2874#ifdef EVENGINE_WEBGPU
2875 if (!device) return;
2876 device.PopErrorScope(wgpu::CallbackMode::AllowProcessEvents,
2877 [](wgpu::PopErrorScopeStatus status, wgpu::ErrorType type, const char *message) {
2878 if (message && type != wgpu::ErrorType::NoError) {
2879 EM_ASM({ console.log("[GPU_ERR] type=" + $0 + " msg=" + UTF8ToString($1)); },
2880 (int)type, message);
2881 }
2882 });
2883#endif
2884}
2885
2887 if (!device || !surface || !swapchainConfigured) return;
2888 rebuildSwapchainIfNeeded();
2889 if (!swapchainConfigured) return;
2890
2891 wgpu::TextureView surfaceView;
2892 wgpu::Texture surfaceTex;
2893 if (!acquireSurfaceTexture(surfaceView, surfaceTex)) {
2894 return;
2895 }
2896
2897 auto &uboArena = currentUboArena();
2898 uboArena.reset();
2899 auto &vtxArena = currentVertexArena();
2900 vtxArena.reset();
2901 voxelInstanceArena.used = 0;
2902 ensureUboArena(uboArena, 4096);
2903 ensureVertexArena(vtxArena, 4096);
2904 // Match the desktop (Vulkan) flow: the 3D/swapchain pass clears with the
2905 // background set by setBackgroundColor unless the script called clear().
2906 if (!hasPendingClear) clearColor = backgroundColor;
2907 hasPendingClear = false;
2908
2909 wgpu::CommandEncoder encoder = device.CreateCommandEncoder();
2910
2911 // 1. Shadow passes (CSM cascade layers).
2912 if (shadowDepthArray) {
2913 bool anyShadow = false;
2914 for (int c = 0; c < ShadowConfig::kCascades; ++c)
2915 if (!shadowCascadeDraws[c].empty()) anyShadow = true;
2916 if (anyShadow) {
2917 for (int c = 0; c < ShadowConfig::kCascades; ++c) {
2918 if (shadowCascadeDraws[c].empty()) continue;
2919 WGPUTextureViewDescriptor lvd{};
2920 lvd.format = WGPUTextureFormat_Depth32Float;
2921 lvd.dimension = WGPUTextureViewDimension_2D;
2922 lvd.baseMipLevel = 0;
2923 lvd.mipLevelCount = 1;
2924 lvd.baseArrayLayer = static_cast<uint32_t>(c);
2925 lvd.arrayLayerCount = 1;
2926 wgpu::TextureView layerView = shadowDepthArray->texture.CreateView(
2927 reinterpret_cast<const wgpu::TextureViewDescriptor*>(&lvd));
2928 WGPURenderPassDepthStencilAttachment ds{};
2929 ds.view = layerView.Get();
2930 ds.depthClearValue = 1.f;
2931 ds.depthLoadOp = WGPULoadOp_Clear;
2932 ds.depthStoreOp = WGPUStoreOp_Store;
2933 ds.stencilClearValue = 0;
2934 ds.stencilLoadOp = WGPULoadOp_Undefined;
2935 ds.stencilStoreOp = WGPUStoreOp_Undefined;
2936 WGPURenderPassDescriptor rp{};
2937 rp.depthStencilAttachment = &ds;
2938 wgpu::RenderPassEncoder pass = encoder.BeginRenderPass(reinterpret_cast<const wgpu::RenderPassDescriptor*>(&rp));
2939 flushShadowPass(pass);
2940 pass.End();
2941 }
2942 }
2943 }
2944
2945 // 2. Scene color pass (3D) into the offscreen target.
2946 wgpu::TextureView sceneView;
2947 wgpu::Texture sceneTex;
2948 if (frameHad3DThisFrame && !sceneColorSlots.empty()) {
2949 SceneColorSlot &slot = sceneColorSlots[currentFrameSlot()];
2950 WGPURenderPassColorAttachment colorAtt{};
2951 colorAtt.view = slot.colorView.Get();
2952 colorAtt.depthSlice = WGPU_DEPTH_SLICE_UNDEFINED;
2953 colorAtt.loadOp = WGPULoadOp_Clear;
2954 colorAtt.storeOp = WGPUStoreOp_Store;
2955 colorAtt.clearValue = {clearColor.r, clearColor.g, clearColor.b, 1.f};
2956 WGPURenderPassDepthStencilAttachment ds{};
2957 ds.view = slot.depthView.Get();
2958 ds.depthClearValue = 1.f;
2959 ds.depthLoadOp = WGPULoadOp_Clear;
2960 ds.depthStoreOp = WGPUStoreOp_Store;
2961 ds.stencilClearValue = 0;
2962 ds.stencilLoadOp = WGPULoadOp_Undefined;
2963 ds.stencilStoreOp = WGPUStoreOp_Undefined;
2964 WGPURenderPassDescriptor rp{};
2965 rp.colorAttachmentCount = 1;
2966 rp.colorAttachments = &colorAtt;
2967 rp.depthStencilAttachment = &ds;
2968 wgpu::RenderPassEncoder pass = encoder.BeginRenderPass(reinterpret_cast<const wgpu::RenderPassDescriptor*>(&rp));
2969 flushVoxelDraws(pass, sceneColorFormat);
2970 flushMesh3D(pass, sceneColorFormat);
2971 pass.End();
2972 sceneView = slot.colorView;
2973 sceneTex = slot.color;
2974 }
2975
2976 // 3. GBuffer pass.
2977 if (gbufferPassPending && !gbufferSlots.empty()) {
2978 GbufferSlot &slot = gbufferSlots[currentFrameSlot()];
2979 WGPURenderPassColorAttachment colorAtts[3]{};
2980 for (int i = 0; i < 3; ++i) {
2981 colorAtts[i].depthSlice = WGPU_DEPTH_SLICE_UNDEFINED;
2982 colorAtts[i].loadOp = WGPULoadOp_Clear;
2983 colorAtts[i].storeOp = WGPUStoreOp_Store;
2984 colorAtts[i].clearValue = {0.f, 0.f, 0.f, 1.f};
2985 }
2986 colorAtts[0].view = slot.normalView.Get();
2987 colorAtts[1].view = slot.depthColorView.Get();
2988 colorAtts[2].view = slot.albedoView.Get();
2989 WGPURenderPassDepthStencilAttachment ds{};
2990 ds.view = slot.depthView.Get();
2991 ds.depthClearValue = 1.f;
2992 ds.depthLoadOp = WGPULoadOp_Clear;
2993 ds.depthStoreOp = WGPUStoreOp_Store;
2994 ds.stencilClearValue = 0;
2995 ds.stencilLoadOp = WGPULoadOp_Undefined;
2996 ds.stencilStoreOp = WGPUStoreOp_Undefined;
2997 WGPURenderPassDescriptor rp{};
2998 rp.colorAttachmentCount = 3;
2999 rp.colorAttachments = colorAtts;
3000 rp.depthStencilAttachment = &ds;
3001 wgpu::RenderPassEncoder pass = encoder.BeginRenderPass(reinterpret_cast<const wgpu::RenderPassDescriptor*>(&rp));
3002 flushGbufferPass(pass);
3003 pass.End();
3004 }
3005
3006 // 4. Active canvas: flush 2D batches into the offscreen target instead.
3007 if (activeCanvas) {
3008 auto *oc = static_cast<OffscreenCanvas *>(activeCanvas);
3009 flush2DToCanvas(oc);
3010 }
3011
3012 // 5. Swapchain pass: composite scene color, draw 2D, run the overlay.
3013 {
3014 WGPURenderPassColorAttachment colorAtt{};
3015 colorAtt.view = surfaceView.Get();
3016 colorAtt.depthSlice = WGPU_DEPTH_SLICE_UNDEFINED;
3017 colorAtt.loadOp = WGPULoadOp_Clear;
3018 colorAtt.storeOp = WGPUStoreOp_Store;
3019 colorAtt.clearValue = {clearColor.r, clearColor.g, clearColor.b, 1.f};
3020 WGPURenderPassDescriptor rp{};
3021 rp.colorAttachmentCount = 1;
3022 rp.colorAttachments = &colorAtt;
3023 wgpu::RenderPassEncoder pass = encoder.BeginRenderPass(reinterpret_cast<const wgpu::RenderPassDescriptor*>(&rp));
3024
3025 if (sceneView && !sceneColorComposited) {
3026 if (!fullscreenQuadReady) {
3027 // Textured vertex: pos(2) + color(4) + uv(2) = 32 bytes.
3028 float verts[32] = {
3029 -1.f, -1.f, 1.f, 1.f, 1.f, 1.f, 0.f, 0.f,
3030 1.f, -1.f, 1.f, 1.f, 1.f, 1.f, 1.f, 0.f,
3031 1.f, 1.f, 1.f, 1.f, 1.f, 1.f, 1.f, 1.f,
3032 -1.f, 1.f, 1.f, 1.f, 1.f, 1.f, 0.f, 1.f,
3033 };
3034 uint32_t indices[6] = {0, 1, 2, 2, 3, 0};
3035 WGPUBufferDescriptor vbd{};
3036 vbd.label = sv("eve_fullscreen_vb");
3037 vbd.size = sizeof(verts);
3038 vbd.usage = WGPUBufferUsage_CopyDst | WGPUBufferUsage_Vertex;
3039 fullscreenQuadVb = device.CreateBuffer(reinterpret_cast<const wgpu::BufferDescriptor*>(&vbd));
3040 queue.WriteBuffer(fullscreenQuadVb, 0, verts, sizeof(verts));
3041 WGPUBufferDescriptor ibd{};
3042 ibd.label = sv("eve_fullscreen_ib");
3043 ibd.size = sizeof(indices);
3044 ibd.usage = WGPUBufferUsage_CopyDst | WGPUBufferUsage_Index;
3045 fullscreenQuadIb = device.CreateBuffer(reinterpret_cast<const wgpu::BufferDescriptor*>(&ibd));
3046 queue.WriteBuffer(fullscreenQuadIb, 0, indices, sizeof(indices));
3047 fullscreenQuadReady = true;
3048 }
3049
3050 GpuTexture sceneGpu;
3051 sceneGpu.texture = sceneTex;
3052 sceneGpu.view = sceneView;
3053 sceneGpu.sampler = createLinearSampler(device);
3054 wgpu::BindGroup bg = makeTex2DBindGroup(&sceneGpu, nullptr);
3055 uint32_t offsets[1] = {0};
3056 pass.SetPipeline(texturedPipeline);
3057 pass.SetBindGroup(0, bg, 1, offsets);
3058 pass.SetVertexBuffer(0, fullscreenQuadVb, 0, 4 * 32);
3059 pass.SetIndexBuffer(fullscreenQuadIb, wgpu::IndexFormat::Uint32, 0, 24);
3060 pass.DrawIndexed(6, 1, 0, 0, 0);
3061 }
3062
3063 if (!activeCanvas) {
3064 flush2D(pass, pixelW > 0 ? pixelW : logicalW, pixelH > 0 ? pixelH : logicalH,
3065 surfaceFormat);
3066 }
3067
3068 if (presentOverlayFn_) {
3069 WGPURenderPassEncoder cPass = pass.Get();
3071 }
3072 pass.End();
3073 }
3074
3075 wgpu::CommandBuffer cmd = encoder.Finish();
3076 queue.Submit(1, &cmd);
3077 // emdawnwebgpu implements the GPU on the JS main thread: queued commands
3078 // (draws, writes) only execute when ProcessEvents drives the work queue.
3079 // Without this, render-pass draws are silently never executed (only the
3080 // swapchain clear, handled by the browser present, is visible).
3081#ifdef __EMSCRIPTEN__
3082 if (instance) instance.ProcessEvents();
3083#endif
3084 // emdawnwebgpu does not implement wgpuSurfacePresent (it aborts); the
3085 // browser presents the canvas automatically once the command buffer is
3086 // submitted from within a requestAnimationFrame callback. On Emscripten
3087 // the engine frame is driven by emscripten_set_main_loop (rAF), so the
3088 // Squirrel main loop no longer blocks and no sleep is needed here.
3089#if !defined(__EMSCRIPTEN__)
3090 surface.Present();
3091#endif
3092
3093 frameIndex++;
3094 frame3DStarted = false;
3095 frameHad3DThisFrame = false;
3096 frameHad3D = false;
3097 sceneColorPassOpen = false;
3098 gbufferPassPending = false;
3099}
3100
3101// ---------------------------------------------------------------------------
3102// Canvas
3103// ---------------------------------------------------------------------------
3104
3106 auto *c = new OffscreenCanvas(this, width, height);
3107 ownedCanvases.push_back(std::unique_ptr<eve::graphics::Canvas>(c));
3108 return c;
3109}
3110
3112 if (activeCanvas && canvas != activeCanvas) {
3113 flush2DToCanvas(static_cast<OffscreenCanvas *>(activeCanvas));
3114 }
3115 activeCanvas = canvas;
3116}
3117
3118bool Graphics::isCanvasActive() const { return activeCanvas != nullptr; }
3119Canvas *Graphics::getCanvas() const { return activeCanvas; }
3120
3122 if (!canvas || !canvas->getTexture()) return;
3123 auto &uboArena = currentUboArena();
3124 uboArena.reset();
3125 auto &vtxArena = currentVertexArena();
3126 vtxArena.reset();
3127
3128 wgpu::CommandEncoder enc = device.CreateCommandEncoder();
3129 WGPURenderPassColorAttachment ca{};
3130 ca.view = canvas->colorView.Get();
3131 ca.depthSlice = WGPU_DEPTH_SLICE_UNDEFINED;
3132 ca.loadOp = canvas->clearRequested ? WGPULoadOp_Clear : WGPULoadOp_Load;
3133 ca.storeOp = WGPUStoreOp_Store;
3134 ca.clearValue = {canvas->clearColor.r, canvas->clearColor.g, canvas->clearColor.b,
3135 canvas->clearColor.a};
3136 WGPURenderPassDescriptor rp{};
3137 rp.colorAttachmentCount = 1;
3138 rp.colorAttachments = &ca;
3139 wgpu::RenderPassEncoder pass = enc.BeginRenderPass(reinterpret_cast<const wgpu::RenderPassDescriptor*>(&rp));
3140 flush2D(pass, canvas->getWidth(), canvas->getHeight(), WGPUTextureFormat_RGBA8Unorm);
3141 pass.End();
3142 wgpu::CommandBuffer cmd = enc.Finish();
3143 queue.Submit(1, &cmd);
3144 canvas->clearRequested = false;
3145}
3146
3148 if (activeCanvas) return activeCanvas->getTexture();
3149 return sceneColorTexture;
3150}
3151
3152void Graphics::draw(eve::graphics::Graphics *gfx, const glm::mat4 &matrix) const {
3153 (void)gfx;
3154 (void)matrix;
3155}
3156
3157void Graphics::draw(Canvas *C, const glm::mat4 &matrix) const {
3158 (void)C;
3159 (void)matrix;
3160}
3161
3162void Graphics::clear(std::optional<Color> color, std::optional<int> /*stencil*/,
3163 std::optional<double> /*depth*/) {
3164 clearColor = color.value_or(backgroundColor);
3165 hasPendingClear = true;
3166}
3167
3169 if (activeCanvas) return getPixelImpl(static_cast<OffscreenCanvas *>(activeCanvas), x, y);
3170 if (!sceneColorSlots.empty()) {
3171 return getPixelImpl(nullptr, x, y);
3172 }
3173 return clearColor;
3174}
3175
3177 if (activeCanvas) return newImageDataImpl(static_cast<OffscreenCanvas *>(activeCanvas));
3178 if (!sceneColorSlots.empty()) return newImageDataImpl(nullptr);
3179 return new image::ImageData(1, 1, "RGBA8");
3180}
3181
3182// ---------------------------------------------------------------------------
3183// Readback
3184// ---------------------------------------------------------------------------
3185
3186namespace {
3187
3188bool copyTextureToCpu(wgpu::Instance &instance, wgpu::Device &device, wgpu::Queue &queue,
3189 wgpu::Texture src, int width, int height, std::vector<uint8_t> &outRgba) {
3190 if (!src) return false;
3191 uint64_t bytesPerRow = static_cast<uint64_t>(width * 4);
3192 bytesPerRow = (bytesPerRow + 255) / 256 * 256; // copy alignment 256
3193 uint64_t size = bytesPerRow * height;
3194
3195 WGPUBufferDescriptor bd{};
3196 bd.label = sv("eve_readback");
3197 bd.size = size;
3198 bd.usage = WGPUBufferUsage_CopyDst | WGPUBufferUsage_MapRead;
3199 bd.mappedAtCreation = false;
3200 wgpu::Buffer dst = device.CreateBuffer(reinterpret_cast<const wgpu::BufferDescriptor*>(&bd));
3201
3202 wgpu::CommandEncoder enc = device.CreateCommandEncoder();
3203 WGPUTexelCopyTextureInfo from{};
3204 from.texture = src.Get();
3205 from.mipLevel = 0;
3206 from.aspect = WGPUTextureAspect_All;
3207 from.origin = {0, 0, 0};
3208 WGPUTexelCopyBufferInfo to{};
3209 to.buffer = dst.Get();
3210 to.layout.offset = 0;
3211 to.layout.bytesPerRow = static_cast<uint32_t>(bytesPerRow);
3212 to.layout.rowsPerImage = static_cast<uint32_t>(height);
3213 WGPUExtent3D extent{static_cast<uint32_t>(width), static_cast<uint32_t>(height), 1};
3214 enc.CopyTextureToBuffer(reinterpret_cast<const wgpu::TexelCopyTextureInfo*>(&from),
3215 reinterpret_cast<const wgpu::TexelCopyBufferInfo*>(&to),
3216 reinterpret_cast<const wgpu::Extent3D*>(&extent));
3217 wgpu::CommandBuffer cmd = enc.Finish();
3218 queue.Submit(1, &cmd);
3219
3220 bool mapped = false;
3221 WGPUBufferMapCallbackInfo cbInfo{};
3222 cbInfo.mode = WGPUCallbackMode_AllowProcessEvents;
3223 cbInfo.callback = [](WGPUMapAsyncStatus status, WGPUStringView /*message*/, void *userdata1,
3224 void * /*userdata2*/) {
3225 bool *ok = static_cast<bool *>(userdata1);
3226 *ok = (status == WGPUMapAsyncStatus_Success);
3227 };
3228 cbInfo.userdata1 = &mapped;
3229 wgpuBufferMapAsync(dst.Get(), WGPUMapMode_Read, 0, size, cbInfo);
3230
3231 int guard = 0;
3232 while (!mapped && guard < 2000) {
3233#if defined(__EMSCRIPTEN__)
3234 emscripten_sleep(0);
3235#endif
3236 wgpuInstanceProcessEvents(instance.Get());
3237 ++guard;
3238 }
3239 if (!mapped) return false;
3240
3241 const uint8_t *data = static_cast<const uint8_t *>(dst.GetConstMappedRange(0, size));
3242 if (!data) return false;
3243 outRgba.resize(static_cast<size_t>(width) * height * 4);
3244 for (int y = 0; y < height; ++y)
3245 std::memcpy(outRgba.data() + size_t(y) * width * 4, data + size_t(y) * bytesPerRow,
3246 size_t(width) * 4);
3247 dst.Unmap();
3248 return true;
3249}
3250
3251} // namespace
3252
3254 int w = canvas ? canvas->getWidth() : (sceneColorWidth > 0 ? sceneColorWidth : 1);
3255 int h = canvas ? canvas->getHeight() : (sceneColorHeight > 0 ? sceneColorHeight : 1);
3256 if (x < 0 || y < 0 || x >= w || y >= h) return Color(0.f, 0.f, 0.f, 0.f);
3257 std::vector<uint8_t> rgba;
3258 wgpu::Texture src = canvas ? canvas->color : (sceneColorSlots.empty() ? nullptr : sceneColorSlots[0].color);
3259 if (!src || !copyTextureToCpu(instance, device, queue, src, w, h, rgba)) return clearColor;
3260 const uint8_t *p = rgba.data() + (size_t(y) * w + x) * 4;
3261 return Color(p[0] / 255.f, p[1] / 255.f, p[2] / 255.f, p[3] / 255.f);
3262}
3263
3265 int w = canvas ? canvas->getWidth() : (sceneColorWidth > 0 ? sceneColorWidth : 1);
3266 int h = canvas ? canvas->getHeight() : (sceneColorHeight > 0 ? sceneColorHeight : 1);
3267 auto *img = new image::ImageData(w, h, "RGBA8");
3268 std::vector<uint8_t> rgba;
3269 wgpu::Texture src = canvas ? canvas->color : (sceneColorSlots.empty() ? nullptr : sceneColorSlots[0].color);
3270 if (src && copyTextureToCpu(instance, device, queue, src, w, h, rgba)) {
3271 std::memcpy(img->getData(), rgba.data(), rgba.size());
3272 }
3273 return img;
3274}
3275
3276// ---------------------------------------------------------------------------
3277// Shader creation
3278// ---------------------------------------------------------------------------
3279
3280namespace {
3281
3282wgpu::RenderPipeline buildPipelineFromWgsl(wgpu::Device &dev, wgpu::PipelineLayout layout,
3283 WGPUTextureFormat format, const std::string &vert,
3284 const std::string &frag, bool depth, bool blend,
3285 bool mesh3d, bool hair, bool shadow, bool gbuffer,
3286 uint32_t sampleCount) {
3287 WGPURenderPipelineDescriptor pd{};
3288 pd.label = sv("eve_custom_shader");
3289 pd.layout = layout.Get();
3290
3291 WGPUVertexAttribute attrs[3]{};
3292 WGPUVertexBufferLayout vb{};
3293 if (mesh3d || shadow || gbuffer) {
3294 attrs[0].format = WGPUVertexFormat_Float32x3;
3295 attrs[0].offset = 0;
3296 attrs[0].shaderLocation = 0;
3297 attrs[1].format = WGPUVertexFormat_Float32x3;
3298 attrs[1].offset = 12;
3299 attrs[1].shaderLocation = 1;
3300 attrs[2].format = WGPUVertexFormat_Float32x2;
3301 attrs[2].offset = 24;
3302 attrs[2].shaderLocation = 2;
3303 vb.arrayStride = 32;
3304 vb.stepMode = WGPUVertexStepMode_Vertex;
3305 vb.attributeCount = 3;
3306 vb.attributes = attrs;
3307 } else {
3308 attrs[0].format = WGPUVertexFormat_Float32x2;
3309 attrs[0].offset = 0;
3310 attrs[0].shaderLocation = 0;
3311 attrs[1].format = WGPUVertexFormat_Float32x4;
3312 attrs[1].offset = 8;
3313 attrs[1].shaderLocation = 1;
3314 attrs[2].format = WGPUVertexFormat_Float32x2;
3315 attrs[2].offset = 24;
3316 attrs[2].shaderLocation = 2;
3317 vb.arrayStride = 32;
3318 vb.stepMode = WGPUVertexStepMode_Vertex;
3319 vb.attributeCount = 3;
3320 vb.attributes = attrs;
3321 }
3322 pd.vertex.bufferCount = 1;
3323 pd.vertex.buffers = &vb;
3324
3325 if (!vert.empty()) {
3326 WGPUShaderModuleDescriptor md = mdDesc(vert);
3327 wgpu::ShaderModule vm = dev.CreateShaderModule(
3328 reinterpret_cast<const wgpu::ShaderModuleDescriptor*>(&md));
3329 pd.vertex.module = vm.Get();
3330 pd.vertex.entryPoint = sv("vs_main");
3331 } else {
3332 // Default textured vertex shader.
3333 WGPUShaderModuleDescriptor md = mdDesc(kTexturedVertWgsl);
3334 wgpu::ShaderModule vm = dev.CreateShaderModule(
3335 reinterpret_cast<const wgpu::ShaderModuleDescriptor*>(&md));
3336 pd.vertex.module = vm.Get();
3337 pd.vertex.entryPoint = sv("vs_main");
3338 }
3339
3340 if (!frag.empty()) {
3341 WGPUFragmentState fs{};
3342 WGPUShaderModuleDescriptor md = mdDesc(frag);
3343 wgpu::ShaderModule fm = dev.CreateShaderModule(
3344 reinterpret_cast<const wgpu::ShaderModuleDescriptor*>(&md));
3345 fs.module = fm.Get();
3346 fs.entryPoint = sv("fs_main");
3347 fs.targetCount = 1;
3348 WGPUColorTargetState target{};
3349 target.format = format;
3350 target.writeMask = WGPUColorWriteMask_All;
3351 if (blend) {
3352 static WGPUBlendState bs = alphaBlend();
3353 target.blend = &bs;
3354 }
3355 fs.targets = &target;
3356 pd.fragment = &fs;
3357 } else {
3358 pd.fragment = nullptr;
3359 }
3360
3361 pd.primitive.topology = WGPUPrimitiveTopology_TriangleList;
3362 pd.primitive.frontFace = WGPUFrontFace_CCW;
3363 pd.primitive.cullMode = hair ? WGPUCullMode_None : WGPUCullMode_None;
3364 pd.primitive.stripIndexFormat = WGPUIndexFormat_Undefined;
3365
3366 if (depth && !shadow) {
3367 static WGPUDepthStencilState ds{};
3368 ds.format = WGPUTextureFormat_Depth32Float;
3369 ds.depthWriteEnabled = WGPUOptionalBool_True;
3370 ds.depthCompare = WGPUCompareFunction_Less;
3371 pd.depthStencil = &ds;
3372 }
3373 pd.multisample.count = sampleCount ? sampleCount : 1;
3374 // mask=0 (zero-init) silently discards all fragments; use all-samples.
3375 pd.multisample.mask = 0xFFFFFFFFu;
3376 return dev.CreateRenderPipeline(reinterpret_cast<const wgpu::RenderPipelineDescriptor*>(&pd));
3377}
3378
3379} // namespace
3380
3381Shader *Graphics::newShaderFromSpv(const std::vector<uint32_t> &vertSpv,
3382 const std::vector<uint32_t> &fragSpv) {
3383 if (!device) throw Exception("newShaderFromSpv: device not initialized");
3384 // Native Dawn can consume Vulkan SPIR-V directly; the browser build cannot
3385 // (browsers only accept WGSL). The WebGPU backend therefore requires WGSL
3386 // for custom shaders; SPIR-V input is rejected with a clear message.
3387 throw Exception("newShaderFromSpv: SPIR-V custom shaders are not supported on the "
3388 "WebGPU backend (browsers accept WGSL only). Recompile the shader "
3389 "with the WebGPU toolchain (glslc+tint) and use newShaderFromWgsl.");
3390}
3391
3392Shader *Graphics::newShaderFromSpvFile(const std::string &vertPath, const std::string &fragPath) {
3393 throw Exception("newShaderFromSpvFile: SPIR-V custom shaders are not supported on the "
3394 "WebGPU backend. Use WGSL shaders instead.");
3395}
3396
3398 if (!shader || !shader->gpuHandle) return false;
3399
3400 auto *gpu = static_cast<GpuShader *>(shader->gpuHandle);
3401 auto gpuIt = std::find_if(ownedGpuShaders.begin(), ownedGpuShaders.end(),
3402 [&](const std::unique_ptr<GpuShader> &g) {
3403 return g.get() == gpu;
3404 });
3405 if (gpuIt == ownedGpuShaders.end()) return false;
3406
3407 auto shIt = std::find_if(ownedShaders.begin(), ownedShaders.end(),
3408 [&](const std::unique_ptr<Shader> &s) {
3409 return s.get() == shader;
3410 });
3411 if (shIt == ownedShaders.end()) return false;
3412
3413 shader->gpuHandle = nullptr;
3414 ownedGpuShaders.erase(gpuIt);
3415 // Transfer the CPU facade to the caller instead of destroying it.
3416 (void)shIt->release();
3417 ownedShaders.erase(shIt);
3418 return true;
3419}
3420
3421Shader *Graphics::newShader(const std::string &vertGlsl, const std::string &fragGlsl) {
3422 (void)vertGlsl;
3423 (void)fragGlsl;
3424 throw Exception("newShader: runtime GLSL compilation is not available on the WebGPU "
3425 "backend (browser WGSL only). Ship pre-compiled WGSL shaders.");
3426}
3427
3428Shader *Graphics::newMeshShaderFromSpv(const std::vector<uint32_t> &vertSpv,
3429 const std::vector<uint32_t> &fragSpv) {
3430 (void)vertSpv;
3431 (void)fragSpv;
3432 throw Exception("newMeshShaderFromSpv: SPIR-V custom mesh shaders are not supported on the "
3433 "WebGPU backend. Use WGSL shaders instead.");
3434}
3435
3436Shader *Graphics::newMeshShaderFromWgsl(const std::string &vertWgsl, const std::string &fragWgsl) {
3437 if (!device) throw Exception("newMeshShaderFromWgsl: device not initialized");
3438 if (fragWgsl.empty()) throw Exception("newMeshShaderFromWgsl: empty fragment WGSL");
3439 if (!mesh3dSetLayout) throw Exception("newMeshShaderFromWgsl: mesh3d layout missing");
3440
3441 std::string vert = vertWgsl.empty() ? kMesh3DVertWgsl : vertWgsl;
3442
3443 auto gpu = std::make_unique<GpuShader>();
3444 gpu->isMesh3D = true;
3445 gpu->wgslVert = vert;
3446 gpu->wgslFrag = fragWgsl;
3447 gpu->mesh3dPipeline =
3448 buildPipelineFromWgsl(device, mesh3dPipelineLayout, sceneColorFormat, vert, fragWgsl,
3449 /*depth*/ true, /*blend*/ false, /*mesh3d*/ true, /*hair*/ false,
3450 /*shadow*/ false, /*gbuffer*/ false, /*sampleCount*/ 1);
3451 // X-ray variant: depth test/write off + alpha blend so occluded silhouettes
3452 // paint over the building (the shader discards visible fragments itself).
3453 gpu->mesh3dXrayPipeline =
3454 buildPipelineFromWgsl(device, mesh3dPipelineLayout, sceneColorFormat, vert, fragWgsl,
3455 /*depth*/ false, /*blend*/ true, /*mesh3d*/ true, /*hair*/ false,
3456 /*shadow*/ false, /*gbuffer*/ false, /*sampleCount*/ 1);
3457
3458 auto sh = std::make_unique<Shader>();
3459 sh->setKind(Shader::Kind::eMesh3D);
3460 sh->gpuHandle = gpu.get();
3461
3462 Shader *raw = sh.get();
3463 ownedShaders.push_back(std::move(sh));
3464 ownedGpuShaders.push_back(std::move(gpu));
3465 return raw;
3466}
3467
3468Shader *Graphics::newMeshShader(const std::string &vertGlsl, const std::string &fragGlsl) {
3469 (void)vertGlsl;
3470 (void)fragGlsl;
3471 throw Exception("newMeshShader: runtime GLSL compilation is not available on the WebGPU "
3472 "backend. Ship pre-compiled WGSL shaders.");
3473}
3474
3475Shader *Graphics::newHairShaderFromSpv(const std::vector<uint32_t> &vertSpv,
3476 const std::vector<uint32_t> &fragSpv) {
3477 (void)vertSpv;
3478 (void)fragSpv;
3479 throw Exception("newHairShaderFromSpv: SPIR-V custom hair shaders are not supported on the "
3480 "WebGPU backend. Use WGSL shaders instead.");
3481}
3482
3483} // namespace eve::graphics::webgpu
3484
3485namespace eve::graphics {
3486// Font support lives in the font module, which is not part of the WebGPU/WASM
3487// build. Fail loudly instead of leaving the interface unsatisfied at link time.
3488Font *Graphics::newFont(font::FontData *, std::string) {
3489 throw Exception(
3490 "newFont: fonts are not supported on the WebGPU backend (font module is not "
3491 "part of the WASM build)");
3492}
3493
3494void Graphics::print(const std::string &, float, float, const Color &, float) {
3495 throw Exception(
3496 "print: fonts are not supported on the WebGPU backend (font module is not part "
3497 "of the WASM build)");
3498}
3499} // namespace eve::graphics
std::vector< std::uint32_t > verts
Definition Builder.cpp:27
bool active
Definition CardTypes.cpp:34
float cx
Definition CardTypes.cpp:31
float cy
Definition CardTypes.cpp:32
float degrees
Definition CardTypes.cpp:33
HSQUIRRELVM vm
Definition ECS.cpp:20
std::string type
std::string layout
vkb::Device & device
vk::ShaderModule vert
vk::ShaderModule frag
int y
Definition Grass.cpp:135
uint32_t i1
Definition Grass.cpp:62
uint32_t i0
Definition Grass.cpp:62
float height
Definition Grass.cpp:235
float u
Definition Grass.cpp:234
int x
Definition Grass.cpp:135
glm::vec3 n
Definition Grass.cpp:64
int h
int w
JobStatus status
float depth
uint32_t a
uint32_t b
uint32_t c
float roughness
float tb
Texture * normal
Texture * albedo
float metallic
int width
int idx
float f
glm::vec3 eye
glm::vec4 p[6]
Mesh * mesh
glm::mat4 viewProj
Shader * shader
bool hair
glm::mat4 view
glm::mat4 model
Light2D::Data * data
const char * name
Definition RockMesh.cpp:21
bool repeatV
bool repeatU
bool enabled
int d
int v
image::ImageData::Colorf color
float scale
Definition TreeMesh.cpp:122
V3 dir
Definition TreeMesh.cpp:121
float m[16]
uint32_t s
Definition Weather.cpp:28
CPU-side decoded font face (FreeType FT_Face + owned font bytes). Does not upload to GPU — rasterize ...
Definition FontData.h:23
Accumulates solid / textured quads in logical (Y-down) coordinates. Used by RenderSystem; not a publi...
Definition Batcher.h:22
virtual Texture * getTexture()=0
Sampleable color buffer; screen Canvas returns nullptr.
GPU-side font: wraps a decoded font::FontData and rasterizes a fixed set of codepoints into a single ...
Definition Font.h:32
virtual void setVSync(bool enabled)
Prefer uncapped present (IMMEDIATE/MAILBOX) when false, vsync (MAILBOX/FIFO) when true....
Definition Graphics.h:604
std::unique_ptr< RenderControl > renderControl_
Definition Graphics.h:990
PresentOverlayFn presentOverlayFn_
Definition Graphics.h:985
GPU mesh handle (+ optional CPU morph targets).
Definition Mesh.h:18
Custom GPU program.
Definition Shader.h:30
static constexpr uint32_t kPushConstantBytes
Definition Shader.h:33
GPU texture created via Graphics::newTexture. Owns GPU resources through an opaque backend handle.
Definition Texture.h:17
TextureSampler sampler
Definition Texture.h:43
void drawMeshShadowAlpha(Mesh *mesh, const glm::mat4 &lightMVP, Texture *albedo=nullptr) override
Shadow pass draw with alpha-cutout discard (card/billboard geometry such as sprite-stack slices): tra...
void pushValidationScope() override
Backend hooks so the platform-independent render3D() can wrap its work in a GPU validation error scop...
Texture * newCubemap(int faceSize, const uint8_t *rgbaFaces) override
Create an RGBA8 cubemap from 6 faces packed as +X,-X,+Y,-Y,+Z,-Z (each faceSize×faceSize,...
void setMesh3DEnv(Texture *cube, float intensity) override
Specular IBL environment for subsequent default mesh draws. cube must be from newCubemap (or nullptr ...
void setMesh3DLight(const glm::vec3 &dir, const glm::vec3 &color) override
Directional light for subsequent drawMesh calls (world-space direction toward surface).
void drawMeshShadow(Mesh *mesh, const glm::mat4 &lightMVP) override
Shader * newMeshShader(const std::string &vertGlsl, const std::string &fragGlsl) override
void flush2DToCanvas(OffscreenCanvas *canvas)
Flush accumulated 2D batches into an offscreen canvas target.
void setMesh3DViewProj(const glm::mat4 &viewProj) override
void draw(eve::graphics::Graphics *gfx, const glm::mat4 &matrix) const override
Draws the object with the specified transformation matrix.
void setMesh3DShadowReceive(bool receive) override
Per-draw: when false, shadow sampling is forced off for the next mesh draw.
Shader * newShaderFromSpvFile(const std::string &vertPath, const std::string &fragPath) override
Load SPIR-V from files via Filesystem (empty vertPath → default textured vert).
float getMaxAnisotropy() const override
Device max supported anisotropy (1 if unsupported). Valid after initWithWindow.
Mesh * newMeshSphere(int slices=32, int stacks=16) override
Procedural UV sphere (radius 1, Y-up). Owned by Graphics. slices = longitude divisions,...
Color getPixelImpl(OffscreenCanvas *canvas, int x, int y)
Blocking CPU readback of an offscreen canvas or scene color target.
void clear(std::optional< Color > color, std::optional< int > stencil, std::optional< double > depth) override
void drawMeshGBufferAlpha(Mesh *mesh, const glm::mat4 &mvp, const glm::mat4 &model, float nearZ, float farZ, Texture *albedo=nullptr, float tintR=1.f, float tintG=1.f, float tintB=1.f) override
GBuffer fill with alpha-cutout discard (card/billboard geometry such as sprite-stack slices): same ou...
void setMesh3DNormalTexture(Texture *normal) override
Optional normal map for the next drawMesh / drawMeshShader (nullptr = flat).
void setMesh3DHeightTexture(Texture *height) override
Optional height map for parallax (R channel; nullptr = flat / off).
void begin3DFrame() override
Begin a 3D frame: shadow/gbuffer (if pending) then a sampleable scene color pass (color+depth)....
Mesh * newMeshFromAssimp(const ::aiMesh &mesh) override
void setMesh3DCameraPos(const glm::vec3 &eye) override
Camera eye used by mesh shaders that need view/rim (stored in Mesh3DUBO).
void beginGBufferPass(int width, int height) override
Depth/normal(/albedo) fill pass for mid/post effects. One-shot submit (like shadow); call before begi...
Shader * newMeshShaderFromWgsl(const std::string &vertWgsl, const std::string &fragWgsl) override
Create a Mesh3D custom shader from WGSL source (WebGPU backend). The WGSL must declare the engine's F...
bool bakeMeshMorph(Mesh *mesh) override
If mesh morph weights are dirty, bake blended positions and upload to the GPU VBO....
Texture * newTextureFromFile(const std::string &filename) override
void setViewportSize(int width, int height, int pixelwidth, int pixelheight) override
Sets the current graphics display viewport dimensions.
Definition Graphics.cpp:258
void setMesh3DLighting(const Lighting3DPack &pack) override
Per-frame ambient + up to 8 lights packed into Mesh3DUBO.
image::ImageData * newImageData() override
void drawMeshShader(Mesh *mesh, const glm::mat4 &model, Texture *texture, const Color &tint, Shader *shader) override
Draw mesh with an explicit Mesh3D Shader (nullptr = default PBR pipeline).
void setMesh3DMaterial(float metallic, float roughness) override
Metallic (0..1) and roughness (0..1) for the next default mesh draw.
void drawSolidRectRotated(float cx, float cy, float w, float h, float degrees, const Color &color, BlendMode blend=BlendMode::Alpha) override
Rotated solid quad degrees clockwise (screen Y-down) around (cx, cy).
void setLighting2D(const Lighting2DUBO &ubo) override
Upload per-frame / per-canvas 2D lighting constants for subsequent lit draws.
void setMesh3DShadows(const ShadowUpload &upload) override
Upload CSM constants for subsequent default mesh draws (active=false disables).
Shader * newMeshShaderFromSpv(const std::vector< uint32_t > &vertSpv, const std::vector< uint32_t > &fragSpv) override
Create a Mesh3D custom shader (MeshVertex + Frame UBO + albedo). Empty vert → default mesh3d....
Texture * newTexture(int width, int height, const uint8_t *rgba, bool repeatU=false, bool repeatV=false) override
bool updateMeshVertices(Mesh *mesh, const float *posXYZ, const float *nrmXYZ, const float *uvST, int vertexCount, const uint32_t *indices, int indexCount) override
In-place update of a mesh's vertex/index data (CPU -> host-visible VBO). Mirrors bakeMeshMorph: the u...
void drawMesh(Mesh *mesh, const glm::mat4 &model, Texture *texture, const Color &tint) override
Draw one mesh with model matrix. Requires begin3DFrame() (or an open swapchain pass).
Shader * newHairShaderFromSpv(const std::vector< uint32_t > &vertSpv, const std::vector< uint32_t > &fragSpv) override
Hair/fur card shader (alpha blend + Kajiya-Kay). Empty vert → mesh3d_hair.vert. Owned by Graphics.
Shader * newShaderFromSpv(const std::vector< uint32_t > &vertSpv, const std::vector< uint32_t > &fragSpv) override
Create a custom 2D shader from SPIR-V words (vert + frag). Owned by Graphics. Vertex stage may be emp...
void setMesh3DView(const glm::mat4 &view) override
Camera view matrix for subsequent drawMesh (view-space depth / CSM select).
void drawTexturedRect(Texture *texture, float x, float y, float w, float h, const Color &color) override
void begin3DFrameToCanvas(Canvas *canvas) override
Open a 3D render pass targeting an offscreen Canvas (color + depth) at the canvas size....
void drawTexturedRectUV(Texture *texture, float x, float y, float w, float h, float u0, float v0, float u1, float v1, const Color &color) override
Draw a textured sub-rect (atlas / tile UVs). texture may be null → solid.
bool releaseShader(Shader *shader) override
Eagerly releases a shader created by this Graphics.
void drawMeshGBuffer(Mesh *mesh, const glm::mat4 &mvp, const glm::mat4 &model, float nearZ, float farZ, Texture *albedo=nullptr, float tintR=1.f, float tintG=1.f, float tintB=1.f) override
void setMesh3DClusteredLighting(const ClusteredLightingUpload &upload) override
Enable clustered forward path for subsequent default mesh draws (SSBO light lists)....
void setMesh3DTexCellBomb(float cellScale, float strength, float rotAmount=1.f) override
Texture cell bombing for the next default mesh draw (breaks tiling). cellScale: cells per UV unit (ty...
void initWithWindow(void *nativeWindow) override
Bind to an existing native window (SDL_Window*) and create Vulkan device/swapchain....
Definition Graphics.cpp:90
Mesh * newMeshCylinder(int slices=32, int stacks=1, bool caps=true) override
Procedural Y-up cylinder (radius 1, height 2 centered at origin). slices = longitude divisions; stack...
image::ImageData * newImageDataImpl(OffscreenCanvas *canvas)
void drawTexturedRectShaderUVRotated(Texture *texture, Shader *shader, float cx, float cy, float w, float h, float degrees, float u0, float v0, float u1, float v1, const Color &color, bool rotatedUV=false, BlendMode blend=BlendMode::Alpha) override
UV draw rotated degrees clockwise (screen Y-down) around the rect center. texture may be null → solid...
void setMesh3DParallax(float scale, float minLayers=8.f, float maxLayers=32.f) override
Parallax occlusion mapping for the next default mesh draw. scale: UV displacement strength (0=off)....
void drawTexturedRectLitUV(Texture *albedo, Texture *normal, float x, float y, float w, float h, float u0, float v0, float u1, float v1, const Color &color) override
Lit 2D draw (albedo + normal map). Uses Lighting2DUBO from setLighting2D. normal may be null → treate...
Mesh * newMeshFromArrays(const float *posXYZ, const float *nrmXYZ, const float *uvST, int vertexCount, const uint32_t *indices, int indexCount) override
Upload a triangle mesh from packed CPU arrays. Owned by Graphics. posXYZ required (vertexCount*3)....
Shader * newShader(const std::string &vertGlsl, const std::string &fragGlsl) override
Compile GLSL source with glslc (must be on PATH). Empty vertGlsl → default textured vert....
void setMesh3DClusteredActive(bool active) override
Cheap per-draw toggle for the already-uploaded clustered light table. Unlike setMesh3DClusteredLighti...
bool reloadTextureFromFile(const std::string &filename) override
Reload a path-cached texture from disk in place (pointer stable). False if unbound.
void setCloudShadows(float strength, float worldCell, float time, float windSpeed, float windAngle, float coverage, float detail) override
Dynamic cloud shadows cast on the ground by the default PBR mesh path. strength 0 disables (no change...
void setTextureSampler(Texture *texture, const TextureSampler &sampler) override
Recreate the sampler for an existing texture (keeps image / mip chain). No-op when texture is null or...
bool releaseMesh(Mesh *mesh) override
Eagerly releases a mesh created by this Graphics.
void setMesh3DClip(float nearZ, float farZ) override
Near/far used to pack linear depth into scene color A (SSGI).
void setMesh3DSceneDepth(Texture *depth) override
Optional scene hardware depth (G-buffer hwDepth, Vulkan NDC z) bound to mesh3d shader binding 7....
bool releaseTexture(Texture *texture) override
Eagerly releases a texture created by this Graphics.
Color getPixel(int x, int y) override
void drawTexturedRectShaderUV(Texture *texture, Shader *shader, float x, float y, float w, float h, float u0, float v0, float u1, float v1, const Color &color, bool rotatedUV=false, BlendMode blend=BlendMode::Alpha) override
UV draw with an explicit Shader (nullptr = default textured pipeline).
Texture * getTexture() override
Sampleable color buffer; screen Canvas returns nullptr.
void drawSolidRect(float x, float y, float w, float h, const Color &color, BlendMode blend=BlendMode::Alpha) override
Internal immediate-mode helper used by RenderSystem / Batcher.
void drawVoxelFaceInstances(const uint32_t *packed, int count, float originX, float originY, float originZ, const std::string &faceDir, Texture *atlas, int tilesPerRow=16, const uint32_t *ao=nullptr) override
Instanced voxel face rectangles (32-bit packed instances). ao: optional per-instance ambient-occlusio...
Canvas * newCanvas(int width, int height) override
Create an offscreen render target (sampleable). Owned by Graphics.
void drawTexturedRectShader(Texture *texture, Shader *shader, float x, float y, float w, float h, const Color &color) override
Draw with an explicit Shader (nullptr = default textured pipeline).
void drawTexturedRectShaderDepth(Texture *color, Texture *depth, Shader *shader, float x, float y, float w, float h, const Color &tint) override
Fullscreen/post draw sampling color at binding 0 and depth at binding 1 (hardware D32,...
Canvas * getCanvas() const override
void setVSync(bool enabled) override
Prefer uncapped present (IMMEDIATE/MAILBOX) when false, vsync (MAILBOX/FIFO) when true....
Definition Graphics.cpp:252
bool isCanvasActive() const override
void beginShadowPass(int cascadeIndex) override
Depth-only shadow pass for one cascade layer (0..2). Draws are recorded into the next begin3DFrame co...
Texture * getSceneColorTexture() override
Sampleable 3D color target for the current frame (RGB = lit, A = linear depth). Valid after begin3DFr...
Offscreen render target (RGBA8Unorm color, optional Depth32Float). 2D batches are flushed into the ca...
Definition Canvas.h:16
int getWidth() const override
Definition Canvas.h:21
int getHeight() const override
Definition Canvas.h:22
Texture * getTexture() override
Sampleable color buffer; screen Canvas returns nullptr.
Definition Canvas.h:23
Represents raw pixel data.
Definition ImageData.h:26
ref is a smart pointer that automatically calls ref() and unref() on the object. Type T must be a sub...
Definition Object.h:54
T * get()
Definition Object.h:94
const char * kColorVertWgsl
const char * kMesh3DFragWgsl
const char * kColorFragWgsl
const char * kTexturedVertWgsl
const char * kMesh3DVertWgsl
const char * kMesh3DGbufferFragWgsl
const char * kMesh3DShadowVertWgsl
const char * kTexturedFragWgsl
const char * kLit2DFragWgsl
const char * kVoxelRectVertWgsl
const char * kVoxelRectFragWgsl
const char * kMesh3DGbufferVertWgsl
卡牌游戏 UI 工具模块:工厂 + 脚本绑定入口。 功能参考 ycarowr/UiCard:扇形手牌布局、抽牌/洗牌、悬浮放大、拖拽到落牌区、 敌方手牌(背面/偷看)、费用不足置灰,以及可实时调节的布局...
Definition AnimTrail.h:6
int mipmapCountForSize(int width, int height)
Full mip chain count for a 2D image (including base level).
glm::vec4 Color
RGBA color used by every graphics draw call. Lives inside eve::graphics so including a graphics heade...
Definition Color.h:13
BlendMode
2D quad blend mode (drawn in draw order within a layer).
Definition BlendMode.h:6
CPU-built clustered lighting upload for one frame/camera. Point lights are clustered; directional lig...
static constexpr int kMaxLights
Definition Light.h:91
Light3DGpu lights[kMaxLights]
Definition Light.h:93
static constexpr int kMapSize
Definition Shadow.h:9
static constexpr int kCascades
Definition Shadow.h:8
Options for Graphics::newTexture / newCubemap. When generateMipmaps is true and sampler....
Sampler state for a Texture (filter, wrap, mip LOD, anisotropy). Defaults match historical engine beh...
static TextureSampler linearMipmap()
Trilinear (linear + linear mips). Caller should create the texture with generateMipmaps.
Vertex/index buffers for one mesh.
Definition Graphics.h:70
A compiled shader: one WebGPU pipeline + layout. Also holds the WGSL sources so custom shaders can be...
Definition Graphics.h:83
Texture resources backed by a wgpu texture + view + sampler + bind groups.
Definition Graphics.h:51