载入中...
搜索中...
未找到
Graphics.cpp
浏览该文件的文档.
3#include "graphics/Batcher.h"
6#include "graphics/GBuffer.h"
8#include "graphics/Light.h"
9#include "graphics/Mesh.h"
13#include "graphics/Shader.h"
14#include "graphics/Shadow.h"
15#include "graphics/Texture.h"
23
24#include "common/Exception.h"
25#include "common/Resource.h"
26#include "common/config.h"
27#include "filesystem/FileData.h"
29#include "image/Image.h"
30#include "image/ImageData.h"
31
32// Header-only Assimp mesh types for newMeshFromAssimp. The Assimp library is
33// not linked on Emscripten; only these POD headers are required here.
34#include <assimp/matrix3x3.h>
35#include <assimp/matrix4x4.h>
36#include <assimp/mesh.h>
37#include <assimp/vector3.h>
38
39#include <algorithm>
40#include <bit>
41#include <cmath>
42#include <cstring>
43#include <fstream>
44#include <map>
45#include <memory>
46#include <mutex>
47#include <vector>
48
49#include <glm/gtc/constants.hpp>
50
51#include <SDL2/SDL.h>
52#include <SDL2/SDL_syswm.h>
53#if defined(__APPLE__) && !defined(__EMSCRIPTEN__)
54#include <objc/message.h>
55#include <objc/runtime.h>
56#endif
57
58#if defined(_WIN32)
59#include <windows.h>
60#endif
61
62#if defined(__EMSCRIPTEN__)
63#include <emscripten/emscripten.h>
64#endif
65
66namespace eve::graphics::webgpu {
67
68namespace {
69
70// A shared default sampler used when no texture is provided.
71wgpu::Sampler createLinearSampler(wgpu::Device& dev) {
72 WGPUSamplerDescriptor d{};
73 d.label = sv("eve_linear");
74 d.addressModeU = WGPUAddressMode_ClampToEdge;
75 d.addressModeV = WGPUAddressMode_ClampToEdge;
76 d.addressModeW = WGPUAddressMode_ClampToEdge;
77 d.magFilter = WGPUFilterMode_Linear;
78 d.minFilter = WGPUFilterMode_Linear;
79 d.mipmapFilter = WGPUMipmapFilterMode_Linear;
80 d.lodMinClamp = 0.f;
81 d.lodMaxClamp = 1000.f;
82 d.maxAnisotropy = 1;
83 return dev.CreateSampler(reinterpret_cast<const wgpu::SamplerDescriptor*>(&d));
84}
85
86int nextPOT(int v) {
87 int p = 1;
88 while (p < v) p <<= 1;
89 return p;
90}
91
92} // namespace
93
94void fillMeshAttributes(WGPUVertexAttribute (&attrs)[6]);
95
97 for (uint32_t i = 0; i < kFramesInFlight; ++i) {
98 uboArenas.emplace_back();
99 vertexArenas.emplace_back();
100 }
101}
102
104 destroyDeferredLightingResources();
107}
108
109// ---------------------------------------------------------------------------
110// Init
111// ---------------------------------------------------------------------------
112
114 if (deviceInitDone) {
115 if (headless_) {
117 return;
118 }
119 throw Exception("Graphics::initHeadless: already initialized with a window");
120 }
121 if (width <= 0 || height <= 0) throw Exception("Graphics::initHeadless: invalid size");
122
123 auto inst = InitFlow::createInstance();
124 auto adp = InitFlow::requestAdapter(std::move(inst), surface);
125 auto dev = InitFlow::requestDevice(std::move(adp));
126
127 instance = std::move(dev.instance);
128 adapter = std::move(dev.adapter);
129 device = std::move(dev.device);
130 queue = std::move(dev.queue);
131 caps = std::move(dev.caps);
132 surfaceFormat = WGPUTextureFormat_BGRA8Unorm;
133
134 createPipelineResources();
135 createShadowResources();
136 createDefaultTextures();
138 createSceneColorResources(width, height);
139 headless_ = true;
140 initialized = true;
141 deviceInitDone = true;
142}
143
144void Graphics::initWithWindow(void* nativeWindow) {
145 sdlWindow = nativeWindow;
146 if (deviceInitDone) return;
147
148 auto inst = InitFlow::createInstance();
149 auto adp = InitFlow::requestAdapter(std::move(inst), surface);
150 auto dev = InitFlow::requestDevice(std::move(adp));
151
152 instance = std::move(dev.instance);
153 adapter = std::move(dev.adapter);
154 device = std::move(dev.device);
155 queue = std::move(dev.queue);
156 caps = std::move(dev.caps);
157
158 // ---- Surface ----
159 WGPUSurfaceDescriptor surfDesc{};
160 surfDesc.label = sv("eve_surface");
161#if defined(__EMSCRIPTEN__)
162 WGPUEmscriptenSurfaceSourceCanvasHTMLSelector canvasSel{};
163 canvasSel.chain.sType = WGPUSType_EmscriptenSurfaceSourceCanvasHTMLSelector;
164 canvasSel.selector = sv("#canvas");
165 surfDesc.nextInChain = &canvasSel.chain;
166 surface = instance.CreateSurface(reinterpret_cast<const wgpu::SurfaceDescriptor*>(&surfDesc));
167 surfaceFormat = WGPUTextureFormat_BGRA8Unorm;
168#else
169 wgpu::SurfaceDescriptor nativeSurfDesc{};
170 nativeSurfDesc.label = "eve_surface";
171 SDL_SysWMinfo wminfo;
172 SDL_VERSION(&wminfo.version);
173 if (!SDL_GetWindowWMInfo(static_cast<SDL_Window*>(sdlWindow), &wminfo))
174 throw Exception("WebGPU: SDL_GetWindowWMInfo failed: %s", SDL_GetError());
175#if defined(_WIN32)
176 wgpu::SurfaceSourceWindowsHWND winChain{};
177 winChain.hwnd = wminfo.info.win.window;
178 winChain.hinstance = GetModuleHandle(nullptr);
179 nativeSurfDesc.nextInChain = &winChain;
180 surface = instance.CreateSurface(&nativeSurfDesc);
181#elif defined(__linux__)
182 if (wminfo.subsystem == SDL_SYSWM_X11) {
183 wgpu::SurfaceSourceXlibWindow x11Chain{};
184 x11Chain.display = wminfo.info.x11.display;
185 x11Chain.window = wminfo.info.x11.window;
186 nativeSurfDesc.nextInChain = &x11Chain;
187 surface = instance.CreateSurface(&nativeSurfDesc);
188#if defined(SDL_VIDEO_DRIVER_WAYLAND)
189 } else if (wminfo.subsystem == SDL_SYSWM_WAYLAND) {
190 wgpu::SurfaceSourceWaylandSurface wlChain{};
191 wlChain.display = wminfo.info.wl.display;
192 wlChain.surface = wminfo.info.wl.surface;
193 nativeSurfDesc.nextInChain = &wlChain;
194 surface = instance.CreateSurface(&nativeSurfDesc);
195#endif
196 } else {
197 throw Exception("WebGPU: unsupported SDL window subsystem on Linux");
198 }
199#elif defined(__APPLE__)
200 // SDL exposes an NSWindow, while Dawn requires its content view's
201 // CAMetalLayer. Runtime messaging keeps this source portable C++.
202 using SendId = id (*)(id, SEL);
203 using SendVoidId = void (*)(id, SEL, id);
204 using SendVoidBool = void (*)(id, SEL, BOOL);
205 using SendIsKind = BOOL (*)(id, SEL, Class);
206 const auto sendId = reinterpret_cast<SendId>(objc_msgSend);
207 const auto sendVoidId = reinterpret_cast<SendVoidId>(objc_msgSend);
208 const auto sendVoidBool = reinterpret_cast<SendVoidBool>(objc_msgSend);
209 const auto sendIsKind = reinterpret_cast<SendIsKind>(objc_msgSend);
210 id window = reinterpret_cast<id>(wminfo.info.cocoa.window);
211 id view = sendId(window, sel_registerName("contentView"));
212 if (view == nil) throw Exception("WebGPU: SDL Cocoa window has no content view");
213 sendVoidBool(view, sel_registerName("setWantsLayer:"), YES);
214 id layer = sendId(view, sel_registerName("layer"));
215 Class metalLayerClass = objc_getClass("CAMetalLayer");
216 if (metalLayerClass == Nil) throw Exception("WebGPU: CAMetalLayer class unavailable");
217 if (layer == nil || !sendIsKind(layer, sel_registerName("isKindOfClass:"), metalLayerClass)) {
218 layer = sendId(reinterpret_cast<id>(metalLayerClass), sel_registerName("layer"));
219 sendVoidId(view, sel_registerName("setLayer:"), layer);
220 }
221 if (layer == nil) throw Exception("WebGPU: failed to create CAMetalLayer");
222 WGPUSurfaceSourceMetalLayer metalChain{};
223 metalChain.chain.sType = WGPUSType_SurfaceSourceMetalLayer;
224 metalChain.layer = layer;
225 surfDesc.nextInChain = &metalChain.chain;
226 surface = instance.CreateSurface(reinterpret_cast<const wgpu::SurfaceDescriptor*>(&surfDesc));
227#else
228 throw Exception("WebGPU: unsupported native platform for surface creation");
229#endif
230 if (!surface) throw Exception("WebGPU: surface creation failed");
231 // Preferred surface format comes from wgpuSurfaceGetCapabilities in the
232 // current ABI (Surface::GetPreferredFormat was removed).
233 WGPUSurfaceCapabilities caps{};
234 if (wgpuSurfaceGetCapabilities(surface.Get(), adapter.Get(), &caps) == WGPUStatus_Success && caps.formatCount > 0) {
235 // Match Vulkan's display-space convention: the scene tone-map pass
236 // writes sRGB-encoded values to UNORM, then UI is composited without
237 // an additional hardware transfer function. Fall back only when the
238 // platform does not expose either common UNORM surface format.
239 surfaceFormat = caps.formats[0];
240 for (size_t i = 0; i < caps.formatCount; ++i) {
241 if (caps.formats[i] == WGPUTextureFormat_BGRA8Unorm) {
242 surfaceFormat = caps.formats[i];
243 break;
244 }
245 if (caps.formats[i] == WGPUTextureFormat_RGBA8Unorm) {
246 surfaceFormat = caps.formats[i];
247 }
248 }
249 }
250 wgpuSurfaceCapabilitiesFreeMembers(caps);
251#endif
252
253 swapchainConfigured = false;
254 // Bind group layouts / pipelines must exist before the default textures
255 // (texture bind groups reference the 2D / mesh3d layouts), and the shadow
256 // depth array must exist before mesh bind groups are built at flush time.
257 createPipelineResources();
258 createShadowResources();
259 createDefaultTextures();
260 initialized = true;
261 deviceInitDone = true;
262}
263
265 if (vsyncEnabled == enabled) return;
267 markSwapchainDirty();
268}
269
270void Graphics::setViewportSize(int width, int height, int pixelwidth, int pixelheight) {
271 // Mirror the Vulkan backend: keep the base-class viewport members in sync
272 // so getWidth()/getPixelWidth() (used by RenderSystem3D for the GBuffer
273 // size) return the real dimensions instead of 0.
274 this->width = width;
275 this->height = height;
276 this->pixelWidth = pixelwidth;
277 this->pixelHeight = pixelheight;
278 logicalW = width;
279 logicalH = height;
280 pixelW = pixelwidth;
281 pixelH = pixelheight;
282 if (width <= 0 || height <= 0) return;
283 if (initialized) configureSurface(width, height);
284}
285
286void Graphics::configureSurface(int width, int height) {
287 if (!surface || !device || width <= 0 || height <= 0) return;
288 WGPUSurfaceConfiguration cfg{};
289 cfg.nextInChain = nullptr;
290 cfg.device = device.Get();
291 cfg.width = static_cast<uint32_t>(width);
292 cfg.height = static_cast<uint32_t>(height);
293 cfg.format = surfaceFormat;
294 WGPUSurfaceCapabilities caps{};
295 surfaceCanCopySrc = wgpuSurfaceGetCapabilities(surface.Get(), adapter.Get(), &caps) == WGPUStatus_Success &&
296 (caps.usages & WGPUTextureUsage_CopySrc) != 0;
297 wgpuSurfaceCapabilitiesFreeMembers(caps);
298 cfg.usage = WGPUTextureUsage_RenderAttachment | (surfaceCanCopySrc ? WGPUTextureUsage_CopySrc : 0);
299 cfg.viewFormatCount = 0;
300 cfg.viewFormats = nullptr;
301 // emdawnwebgpu only accepts Fifo / Undefined present modes.
302 cfg.presentMode = WGPUPresentMode_Fifo;
303 // Auto (0) maps to an empty slot in emdawnwebgpu's JS CompositeAlphaMode
304 // table �?`alphaMode: undefined`, which some browsers reject. Use Opaque,
305 // which maps to the JS value 'opaque' used by the working replica.
306 cfg.alphaMode = WGPUCompositeAlphaMode_Opaque;
307 surface.Configure(reinterpret_cast<const wgpu::SurfaceConfiguration*>(&cfg));
308 swapchainConfigured = true;
309 // Recreate the offscreen targets at the new size.
310 int w = pixelW > 0 ? pixelW : width;
311 int h = pixelH > 0 ? pixelH : height;
312 if (w > 0 && h > 0) {
313 createSceneColorResources(w, h);
314 createShadowResources();
315 }
316}
317
318void Graphics::rebuildSwapchainIfNeeded() {
319 if (!swapchainConfigured && surface && logicalW > 0 && logicalH > 0) {
320 configureSurface(logicalW, logicalH);
321 }
322}
323
324// ---------------------------------------------------------------------------
325// Default / placeholder resources
326// ---------------------------------------------------------------------------
327
328void Graphics::createDefaultTextures() {
329 uint8_t white[4] = {255, 255, 255, 255};
330 whiteTexture = static_cast<GpuTexture*>(newTexture(1, 1, white, false, false)->gpuHandle);
331
332 // Flat normal (0.5,0.5,1) in RGBA8 �?sampled as (0,0,1) normal.
333 uint8_t flatNrm[4] = {128, 128, 255, 255};
334 flatNormalTexture = static_cast<GpuTexture*>(newTexture(1, 1, flatNrm, false, false)->gpuHandle);
335 flatNormalTexture3D = flatNormalTexture;
336
337 uint8_t flatH[4] = {0, 0, 0, 255};
338 flatHeightTexture3D = static_cast<GpuTexture*>(newTexture(1, 1, flatH, false, false)->gpuHandle);
339
340 // 1x1 depth placeholder for the mesh3d scene-depth binding (9). Only X-ray
341 // shaders sample it; every other mesh draw binds this unused depth view.
342 {
343 auto* gpu = new GpuTexture();
344 WGPUTextureDescriptor td{};
345 td.label = sv("eve_flat_depth");
346 td.dimension = WGPUTextureDimension_2D;
347 td.size = {1, 1, 1};
348 td.sampleCount = 1;
349 td.format = WGPUTextureFormat_Depth32Float;
350 td.mipLevelCount = 1;
351 td.usage = WGPUTextureUsage_TextureBinding;
352 gpu->texture = device.CreateTexture(reinterpret_cast<const wgpu::TextureDescriptor*>(&td));
353 WGPUTextureViewDescriptor vd{};
354 vd.format = WGPUTextureFormat_Depth32Float;
355 vd.dimension = WGPUTextureViewDimension_2D;
356 vd.baseMipLevel = 0;
357 vd.mipLevelCount = 1;
358 vd.baseArrayLayer = 0;
359 vd.arrayLayerCount = 1;
360 gpu->view = gpu->texture.CreateView(reinterpret_cast<const wgpu::TextureViewDescriptor*>(&vd));
361 gpu->width = 1;
362 gpu->height = 1;
363 flatDepthTexture3D = gpu;
364 }
365
366 // 1x1xN depth-array placeholder for the mesh3d shadow bindings (5/8).
367 // sampleShadowPCF() early-outs (bias.y < 0.5) when shadows are disabled,
368 // so the texture is never actually sampled.
369 {
370 auto* gpu = new GpuTexture();
371 WGPUTextureDescriptor td{};
372 td.label = sv("eve_default_shadow_depth");
373 td.dimension = WGPUTextureDimension_2D;
374 td.size = {1, 1, static_cast<uint32_t>(ShadowConfig::kTotalLayers)};
375 td.sampleCount = 1;
376 td.format = WGPUTextureFormat_Depth32Float;
377 td.mipLevelCount = 1;
378 td.usage = WGPUTextureUsage_TextureBinding;
379 gpu->texture = device.CreateTexture(reinterpret_cast<const wgpu::TextureDescriptor*>(&td));
380 WGPUTextureViewDescriptor vd{};
381 vd.format = WGPUTextureFormat_Depth32Float;
382 vd.dimension = WGPUTextureViewDimension_2DArray;
383 vd.baseMipLevel = 0;
384 vd.mipLevelCount = 1;
385 vd.baseArrayLayer = 0;
386 vd.arrayLayerCount = static_cast<uint32_t>(ShadowConfig::kTotalLayers);
387 gpu->view = gpu->texture.CreateView(reinterpret_cast<const wgpu::TextureViewDescriptor*>(&vd));
388 WGPUSamplerDescriptor sd{};
389 sd.label = sv("eve_default_shadow_sampler");
390 sd.addressModeU = WGPUAddressMode_ClampToEdge;
391 sd.addressModeV = WGPUAddressMode_ClampToEdge;
392 sd.addressModeW = WGPUAddressMode_ClampToEdge;
393 sd.magFilter = WGPUFilterMode_Linear;
394 sd.minFilter = WGPUFilterMode_Linear;
395 sd.mipmapFilter = WGPUMipmapFilterMode_Nearest;
396 sd.compare = WGPUCompareFunction_LessEqual;
397 sd.maxAnisotropy = 1.f;
398 gpu->sampler = device.CreateSampler(reinterpret_cast<const wgpu::SamplerDescriptor*>(&sd));
399 defaultShadowTex = gpu;
400 }
401
402 // Default env cubemap is black so an unset environment (RenderSystem3D
403 // still passes envIntensity=1.0 by default) does not wash the scene
404 // toward white. Only an explicitly set envMap contributes IBL.
405 uint8_t cubeFace[4] = {0, 0, 0, 0};
406 uint8_t cubeData[24];
407 for (int f = 0; f < 6; ++f) std::memcpy(cubeData + f * 4, cubeFace, 4);
408 defaultEnvCubemap = static_cast<GpuTexture*>(newCubemap(1, cubeData)->gpuHandle);
409
410 // Shared filtering sampler for WGSL bindings declared as plain `sampler`.
411 TextureSampler def;
412 mainSampler = makeSampler(def, 1);
413
414 const uint8_t transparent[4] = {0, 0, 0, 0};
415 const uint8_t decalNormal[4] = {128, 128, 255, 0};
416 const uint8_t decalParams[4] = {0, 0, 0, 0};
417 decalFlatAlbedo = newTexture(1, 1, transparent);
418 decalFlatNormal = newTexture(1, 1, decalNormal);
419 decalFlatParams = newTexture(1, 1, decalParams);
420}
421
422// ---------------------------------------------------------------------------
423// Pipeline resource creation
424// ---------------------------------------------------------------------------
425
426void Graphics::createPipelineResources() {
427 // Only layouts are required during initialization. Native Dawn compiles
428 // render pipelines synchronously, so eagerly materializing every blend,
429 // depth, cull, target and MSAA variant made each isolated CTest spend
430 // roughly a minute compiling pipelines it never used.
431 create2DPipelines();
432 createMesh3DPipelines();
433}
434
435// ---------------------------------------------------------------------------
436// Bind group layouts
437// ---------------------------------------------------------------------------
438
439wgpu::BindGroupLayout Graphics::make2DBindGroupLayout() {
440 BindGroupLayoutBuilder b;
441 // 0: color texture
442 b.texture(0, wgpu::ShaderStage::Fragment, wgpu::TextureSampleType::Float, wgpu::TextureViewDimension::e2D);
443 // 1: depth texture
444 b.texture(1, wgpu::ShaderStage::Fragment, wgpu::TextureSampleType::Float, wgpu::TextureViewDimension::e2D);
445 // 2: color sampler
446 b.sampler(2, wgpu::ShaderStage::Fragment, wgpu::SamplerBindingType::Filtering);
447 // 3: depth sampler
448 b.sampler(3, wgpu::ShaderStage::Fragment, wgpu::SamplerBindingType::Filtering);
449 // 4: Externals UBO (push-constant replacement, dynamic offset). Sized for
450 // the largest consumer: custom 2D shaders (128 B) and lit2D (Lighting2DUBO).
451 b.buffer(4, wgpu::ShaderStage::Vertex | wgpu::ShaderStage::Fragment, wgpu::BufferBindingType::Uniform, true,
452 std::max<uint32_t>(Shader::kPushConstantBytes, uint32_t(sizeof(Lighting2DUBO))));
453 b.texture(5, wgpu::ShaderStage::Fragment, wgpu::TextureSampleType::Float, wgpu::TextureViewDimension::e2D);
454 b.sampler(6, wgpu::ShaderStage::Fragment, wgpu::SamplerBindingType::Filtering);
455 b.texture(7, wgpu::ShaderStage::Fragment, wgpu::TextureSampleType::Float, wgpu::TextureViewDimension::e2D);
456 b.sampler(8, wgpu::ShaderStage::Fragment, wgpu::SamplerBindingType::Filtering);
457 b.texture(9, wgpu::ShaderStage::Fragment, wgpu::TextureSampleType::Float, wgpu::TextureViewDimension::e2D);
458 b.sampler(10, wgpu::ShaderStage::Fragment, wgpu::SamplerBindingType::Filtering);
459 return b.build(device, "eve_2d");
460}
461
462wgpu::BindGroupLayout Graphics::makeMesh3DBindGroupLayout() {
463 BindGroupLayoutBuilder b;
464 // 0: Frame UBO (dynamic)
465 b.buffer(0, wgpu::ShaderStage::Vertex | wgpu::ShaderStage::Fragment, wgpu::BufferBindingType::Uniform, true,
466 sizeof(Mesh3DUBO));
467 // 1: albedo
468 b.texture(1, wgpu::ShaderStage::Fragment, wgpu::TextureSampleType::Float, wgpu::TextureViewDimension::e2D);
469 // 2: normal
470 b.texture(2, wgpu::ShaderStage::Fragment, wgpu::TextureSampleType::Float, wgpu::TextureViewDimension::e2D);
471 // 3: env cubemap
472 b.texture(3, wgpu::ShaderStage::Fragment, wgpu::TextureSampleType::Float, wgpu::TextureViewDimension::Cube);
473 // 4: Shadow UBO (dynamic)
474 b.buffer(4, wgpu::ShaderStage::Fragment, wgpu::BufferBindingType::Uniform, true, sizeof(ShadowUBO));
475 // 5: shadow depth array
476 b.texture(5, wgpu::ShaderStage::Fragment, wgpu::TextureSampleType::Depth, wgpu::TextureViewDimension::e2DArray);
477 // 6: height
478 b.texture(6, wgpu::ShaderStage::Fragment, wgpu::TextureSampleType::Float, wgpu::TextureViewDimension::e2D);
479 // 7: shared filtering sampler (WGSL `mainSamp`)
480 b.sampler(7, wgpu::ShaderStage::Fragment, wgpu::SamplerBindingType::Filtering);
481 // 8: shadow comparison sampler
482 b.sampler(8, wgpu::ShaderStage::Fragment, wgpu::SamplerBindingType::Comparison);
483 // 9: scene depth (G-buffer hwDepth; X-ray shaders sample it). Depth view,
484 // sampled via textureSampleLevel with the shared mainSamp (binding 7).
485 b.texture(9, wgpu::ShaderStage::Fragment, wgpu::TextureSampleType::Depth, wgpu::TextureViewDimension::e2D);
486 // 10: SSAO occlusion texture (white when AO is disabled)
487 b.texture(10, wgpu::ShaderStage::Fragment, wgpu::TextureSampleType::Float, wgpu::TextureViewDimension::e2D);
488 // 11: shared AO sampler
489 b.sampler(11, wgpu::ShaderStage::Fragment, wgpu::SamplerBindingType::Filtering);
490 for (uint32_t i = 12; i <= 14; ++i) {
491 b.texture(i, wgpu::ShaderStage::Fragment, wgpu::TextureSampleType::Float, wgpu::TextureViewDimension::e2D);
492 }
493 // Custom mesh shaders may use their external parameters for vertex
494 // deformation as well as fragment shading (for example grass billboards).
495 b.buffer(15, wgpu::ShaderStage::Vertex | wgpu::ShaderStage::Fragment, wgpu::BufferBindingType::Uniform, true,
497 b.texture(16, wgpu::ShaderStage::Fragment, wgpu::TextureSampleType::Float, wgpu::TextureViewDimension::Cube);
498 b.texture(17, wgpu::ShaderStage::Fragment, wgpu::TextureSampleType::Float, wgpu::TextureViewDimension::Cube);
499 b.texture(18, wgpu::ShaderStage::Fragment, wgpu::TextureSampleType::Float, wgpu::TextureViewDimension::e2D);
500 b.sampler(19, wgpu::ShaderStage::Fragment, wgpu::SamplerBindingType::Filtering);
501 b.buffer(21, wgpu::ShaderStage::Vertex, wgpu::BufferBindingType::ReadOnlyStorage, false, 64);
502 return b.build(device, "eve_mesh3d");
503}
504
505wgpu::BindGroupLayout Graphics::makeShadowBindGroupLayout() {
506 BindGroupLayoutBuilder b;
507 b.buffer(0, wgpu::ShaderStage::Vertex | wgpu::ShaderStage::Fragment,
508 wgpu::BufferBindingType::Uniform, true, sizeof(SkinPassUBO));
509 b.texture(1, wgpu::ShaderStage::Fragment, wgpu::TextureSampleType::Float, wgpu::TextureViewDimension::e2D);
510 b.sampler(2, wgpu::ShaderStage::Fragment, wgpu::SamplerBindingType::Filtering);
511 b.buffer(3, wgpu::ShaderStage::Vertex, wgpu::BufferBindingType::ReadOnlyStorage, false, 64);
512 return b.build(device, "eve_shadow");
513}
514
515wgpu::BindGroupLayout Graphics::makeGbufferBindGroupLayout() {
516 BindGroupLayoutBuilder b;
517 // 0: pass and skinning UBO (dynamic)
518 b.buffer(0, wgpu::ShaderStage::Vertex | wgpu::ShaderStage::Fragment, wgpu::BufferBindingType::Uniform, true,
519 sizeof(SkinPassUBO));
520 // 1: albedo
521 b.texture(1, wgpu::ShaderStage::Fragment, wgpu::TextureSampleType::Float, wgpu::TextureViewDimension::e2D);
522 // 2: sampler
523 b.sampler(2, wgpu::ShaderStage::Fragment, wgpu::SamplerBindingType::Filtering);
524 b.buffer(3, wgpu::ShaderStage::Vertex, wgpu::BufferBindingType::ReadOnlyStorage, false, 64);
525 return b.build(device, "eve_gbuffer");
526}
527
528wgpu::BindGroupLayout Graphics::makeDecalBindGroupLayout() {
529 BindGroupLayoutBuilder b;
530 b.buffer(0, wgpu::ShaderStage::Fragment, wgpu::BufferBindingType::Uniform, true, 256);
531 for (uint32_t i = 1; i <= 3; ++i) {
532 b.texture(i, wgpu::ShaderStage::Fragment, wgpu::TextureSampleType::Float, wgpu::TextureViewDimension::e2D);
533 }
534 b.texture(4, wgpu::ShaderStage::Fragment, wgpu::TextureSampleType::Depth, wgpu::TextureViewDimension::e2D);
535 b.texture(5, wgpu::ShaderStage::Fragment, wgpu::TextureSampleType::Float, wgpu::TextureViewDimension::e2D);
536 b.sampler(6, wgpu::ShaderStage::Fragment, wgpu::SamplerBindingType::Filtering);
537 return b.build(device, "eve_decal");
538}
539
540wgpu::BindGroupLayout Graphics::makeVoxelBindGroupLayout() {
541 BindGroupLayoutBuilder b;
542 // 0: PC UBO (dynamic)
543 b.buffer(0, wgpu::ShaderStage::Vertex, wgpu::BufferBindingType::Uniform, true, 112);
544 // 1: atlas texture
545 b.texture(1, wgpu::ShaderStage::Fragment, wgpu::TextureSampleType::Float, wgpu::TextureViewDimension::e2D);
546 // 2: atlas sampler
547 b.sampler(2, wgpu::ShaderStage::Fragment, wgpu::SamplerBindingType::Filtering);
548 return b.build(device, "eve_voxel");
549}
550
551wgpu::PipelineLayout Graphics::make2DPipelineLayout() {
552 wgpu::PipelineLayoutDescriptor d{};
553 d.label = "eve_2d_layout";
554 d.bindGroupLayoutCount = 1;
555 d.bindGroupLayouts = &tex2DSetLayout;
556 return device.CreatePipelineLayout(&d);
557}
558
559wgpu::PipelineLayout Graphics::makeMesh3DPipelineLayout() {
560 wgpu::PipelineLayoutDescriptor d{};
561 d.label = "eve_mesh3d_layout";
562 d.bindGroupLayoutCount = 1;
563 d.bindGroupLayouts = &mesh3dSetLayout;
564 return device.CreatePipelineLayout(&d);
565}
566
567wgpu::BindGroupLayout Graphics::makeMesh3DClusteredBindGroupLayout() {
568 BindGroupLayoutBuilder b;
569 // 0: Frame UBO (dynamic; clustered layout)
570 b.buffer(0, wgpu::ShaderStage::Vertex | wgpu::ShaderStage::Fragment, wgpu::BufferBindingType::Uniform, true,
571 sizeof(Mesh3DClusteredUBO));
572 // 1: albedo
573 b.texture(1, wgpu::ShaderStage::Fragment, wgpu::TextureSampleType::Float, wgpu::TextureViewDimension::e2D);
574 // 2: normal map
575 b.texture(2, wgpu::ShaderStage::Fragment, wgpu::TextureSampleType::Float, wgpu::TextureViewDimension::e2D);
576 // 3: env cubemap
577 b.texture(3, wgpu::ShaderStage::Fragment, wgpu::TextureSampleType::Float, wgpu::TextureViewDimension::Cube);
578 // 4: shadow UBO (dynamic)
579 b.buffer(4, wgpu::ShaderStage::Fragment, wgpu::BufferBindingType::Uniform, true, sizeof(ShadowUBO));
580 // 5: shadow depth array
581 b.texture(5, wgpu::ShaderStage::Fragment, wgpu::TextureSampleType::Depth, wgpu::TextureViewDimension::e2DArray);
582 // 6: height/parallax (unused fallback)
583 b.texture(6, wgpu::ShaderStage::Fragment, wgpu::TextureSampleType::Float, wgpu::TextureViewDimension::e2D);
584 // 7: shared filtering sampler
585 b.sampler(7, wgpu::ShaderStage::Fragment, wgpu::SamplerBindingType::Filtering);
586 // 8: shadow comparison sampler
587 b.sampler(8, wgpu::ShaderStage::Fragment, wgpu::SamplerBindingType::Comparison);
588 // 9: scene depth (G-buffer hwDepth; sampled by X-ray variants)
589 b.texture(9, wgpu::ShaderStage::Fragment, wgpu::TextureSampleType::Depth, wgpu::TextureViewDimension::e2D);
590 // 10..12: clustered-forward SSBOs
591 b.buffer(10, wgpu::ShaderStage::Fragment, wgpu::BufferBindingType::ReadOnlyStorage, false,
592 sizeof(ClusteredLightGpu));
593 b.buffer(11, wgpu::ShaderStage::Fragment, wgpu::BufferBindingType::ReadOnlyStorage, false,
594 sizeof(ClusterTableEntry));
595 b.buffer(12, wgpu::ShaderStage::Fragment, wgpu::BufferBindingType::ReadOnlyStorage, false, sizeof(uint32_t));
596 // 13/14: SSAO occlusion texture + sampler
597 b.texture(13, wgpu::ShaderStage::Fragment, wgpu::TextureSampleType::Float, wgpu::TextureViewDimension::e2D);
598 b.sampler(14, wgpu::ShaderStage::Fragment, wgpu::SamplerBindingType::Filtering);
599 for (uint32_t i = 15; i <= 17; ++i) {
600 b.texture(i, wgpu::ShaderStage::Fragment, wgpu::TextureSampleType::Float, wgpu::TextureViewDimension::e2D);
601 }
602
603 b.texture(18, wgpu::ShaderStage::Fragment, wgpu::TextureSampleType::Float, wgpu::TextureViewDimension::Cube);
604 b.texture(19, wgpu::ShaderStage::Fragment, wgpu::TextureSampleType::Float, wgpu::TextureViewDimension::Cube);
605 return b.build(device, "eve_mesh3d_clustered");
606}
607
608wgpu::PipelineLayout Graphics::makeMesh3DClusteredPipelineLayout() {
609 wgpu::PipelineLayoutDescriptor d{};
610 d.label = "eve_mesh3d_clustered_layout";
611 d.bindGroupLayoutCount = 1;
612 d.bindGroupLayouts = &mesh3dClusteredSetLayout;
613 return device.CreatePipelineLayout(&d);
614}
615
616wgpu::PipelineLayout Graphics::makeShadowPipelineLayout() {
617 wgpu::PipelineLayoutDescriptor d{};
618 d.label = "eve_shadow_layout";
619 d.bindGroupLayoutCount = 1;
620 d.bindGroupLayouts = &shadowSetLayout;
621 return device.CreatePipelineLayout(&d);
622}
623
624wgpu::PipelineLayout Graphics::makeGbufferPipelineLayout() {
625 wgpu::PipelineLayoutDescriptor d{};
626 d.label = "eve_gbuffer_layout";
627 d.bindGroupLayoutCount = 1;
628 d.bindGroupLayouts = &gbufferSetLayout;
629 return device.CreatePipelineLayout(&d);
630}
631
632wgpu::PipelineLayout Graphics::makeDecalPipelineLayout() {
633 wgpu::PipelineLayoutDescriptor d{};
634 d.label = "eve_decal_layout";
635 d.bindGroupLayoutCount = 1;
636 d.bindGroupLayouts = &decalSetLayout;
637 return device.CreatePipelineLayout(&d);
638}
639
640wgpu::PipelineLayout Graphics::makeVoxelPipelineLayout() {
641 wgpu::PipelineLayoutDescriptor d{};
642 d.label = "eve_voxel_layout";
643 d.bindGroupLayoutCount = 1;
644 d.bindGroupLayouts = &voxelSetLayout;
645 return device.CreatePipelineLayout(&d);
646}
647
648// ---------------------------------------------------------------------------
649// Pipelines
650// ---------------------------------------------------------------------------
651
652namespace {
653
654
655// Builds a WGSL shader-module descriptor (the chained WGPUShaderSourceWGSL is
656// static so its chain pointer stays valid). The returned descriptor is a value
657// that must outlive the CreateShaderModule call.
658WGPUShaderModuleDescriptor mdDesc(const std::string& code) {
659 static WGPUShaderSourceWGSL wd{};
660 wd.chain.sType = WGPUSType_ShaderSourceWGSL;
661 wd.code = sv(code.c_str());
662 WGPUShaderModuleDescriptor md{};
663 md.nextInChain = &wd.chain;
664 return md;
665}
666
667void fillVertexLayout(WGPUVertexBufferLayout& layout, uint64_t stride, const WGPUVertexAttribute* attrs,
668 uint32_t attrCount) {
669 layout.arrayStride = stride;
670 layout.stepMode = WGPUVertexStepMode_Vertex;
671 layout.attributeCount = attrCount;
672 layout.attributes = attrs;
673}
674
675WGPUBlendState alphaBlend() {
676 WGPUBlendState b{};
677 b.color.srcFactor = WGPUBlendFactor_SrcAlpha;
678 b.color.dstFactor = WGPUBlendFactor_OneMinusSrcAlpha;
679 b.color.operation = WGPUBlendOperation_Add;
680 b.alpha.srcFactor = WGPUBlendFactor_One;
681 b.alpha.dstFactor = WGPUBlendFactor_OneMinusSrcAlpha;
682 b.alpha.operation = WGPUBlendOperation_Add;
683 return b;
684}
685
686WGPUBlendState noBlend() {
687 WGPUBlendState b{};
688 b.color.srcFactor = WGPUBlendFactor_One;
689 b.color.dstFactor = WGPUBlendFactor_Zero;
690 b.color.operation = WGPUBlendOperation_Add;
691 b.alpha.srcFactor = WGPUBlendFactor_One;
692 b.alpha.dstFactor = WGPUBlendFactor_Zero;
693 b.alpha.operation = WGPUBlendOperation_Add;
694 return b;
695}
696
697WGPUBlendState additiveBlend() {
698 WGPUBlendState b{};
699 b.color.srcFactor = WGPUBlendFactor_SrcAlpha;
700 b.color.dstFactor = WGPUBlendFactor_One;
701 b.color.operation = WGPUBlendOperation_Add;
702 b.alpha.srcFactor = WGPUBlendFactor_One;
703 b.alpha.dstFactor = WGPUBlendFactor_One;
704 b.alpha.operation = WGPUBlendOperation_Add;
705 return b;
706}
707
708WGPUBlendState premultipliedBlend() {
709 WGPUBlendState b = alphaBlend();
710 b.color.srcFactor = WGPUBlendFactor_One;
711 return b;
712}
713
714WGPUBlendState multiplyBlend() {
715 WGPUBlendState b = alphaBlend();
716 b.color.srcFactor = WGPUBlendFactor_Dst;
717 return b;
718}
719
720size_t meshPipelineIndex(BlendMode blend, bool depthWrite, bool doubleSided) {
721 return size_t(blend) * 4u + (depthWrite ? 2u : 0u) + (doubleSided ? 1u : 0u);
722}
723
724WGPUBlendState blendState(BlendMode mode) {
725 switch (mode) {
726 case BlendMode::Additive: return additiveBlend();
727 case BlendMode::Premultiplied: return premultipliedBlend();
728 case BlendMode::Multiply: return multiplyBlend();
729 case BlendMode::Opaque: return noBlend();
730 case BlendMode::Alpha:
731 default: return alphaBlend();
732 }
733}
734
735} // namespace
736
737namespace {
738
739wgpu::RenderPipeline make2DColorPipeline(wgpu::Device& dev, WGPUTextureFormat format, BlendMode mode) {
740 wgpu::VertexAttribute attrs[2] = {};
741 attrs[0].format = wgpu::VertexFormat::Float32x2;
742 attrs[0].offset = 0;
743 attrs[0].shaderLocation = 0;
744 attrs[1].format = wgpu::VertexFormat::Float32x4;
745 attrs[1].offset = 8;
746 attrs[1].shaderLocation = 1;
747
748 PipelineBuilder b;
749 // The solid-color shader declares no bindings, so it must use an empty
750 // (auto-derived) pipeline layout. Reusing the textured 2D layout here would
751 // require a bind group for every solid draw.
752 b.vertexLayout(24, wgpu::VertexStepMode::Vertex, attrs, 2);
753 b.shader(makeWgslModule(dev, kColorVertWgsl), "vs_main", makeWgslModule(dev, kColorFragWgsl), "fs_main");
754 b.colorTarget(format, mode);
755 // The WGSL vertex shader mirrors clip Y to match Vulkan, which flips
756 // object-space CCW winding in framebuffer space.
757 b.frontFace(wgpu::FrontFace::CW);
758 b.cull(wgpu::CullMode::Back);
759 return b.build(dev);
760}
761
762wgpu::RenderPipeline make2DTexturedPipeline(wgpu::Device& dev, wgpu::PipelineLayout layout, WGPUTextureFormat format,
763 BlendMode mode, bool toneMapScene = false) {
764 WGPUVertexAttribute attrs[3] = {};
765 attrs[0].format = WGPUVertexFormat_Float32x2;
766 attrs[0].offset = 0;
767 attrs[0].shaderLocation = 0;
768 attrs[1].format = WGPUVertexFormat_Float32x4;
769 attrs[1].offset = 8;
770 attrs[1].shaderLocation = 1;
771 attrs[2].format = WGPUVertexFormat_Float32x2;
772 attrs[2].offset = 24;
773 attrs[2].shaderLocation = 2;
774 WGPUVertexBufferLayout vb{};
775 fillVertexLayout(vb, 32, attrs, 3);
776
777 WGPUColorTargetState target{};
778 target.format = format;
779 target.writeMask = WGPUColorWriteMask_All;
780 WGPUBlendState bs = alphaBlend();
781 if (mode == BlendMode::Additive)
782 bs = additiveBlend();
783 else if (mode == BlendMode::Premultiplied)
784 bs = premultipliedBlend();
785 else if (mode == BlendMode::Multiply)
786 bs = multiplyBlend();
787 else if (mode == BlendMode::Opaque)
788 bs = noBlend();
789 if (mode != BlendMode::Opaque) target.blend = &bs;
790
791 WGPURenderPipelineDescriptor pd{};
792 pd.label = sv("eve_textured2d");
793 pd.layout = layout.Get();
794 wgpu::ShaderModule vertModule = makeWgslModule(dev, kTexturedVertWgsl);
795 static constexpr const char* kSceneTonemapFragWgsl = R"wgsl(
796struct FSIn {
797 @location(0) color: vec4f,
798 @location(1) uv: vec2f,
799};
800@group(0) @binding(0) var colorTex: texture_2d<f32>;
801@group(0) @binding(2) var colorSampler: sampler;
802@group(0) @binding(1) var rawSceneTex: texture_2d<f32>;
803@group(0) @binding(3) var rawSceneSampler: sampler;
804fn acesFitted(c0: vec3f) -> vec3f {
805 let c = max(c0, vec3f(0.0));
806 return clamp((c * (2.51 * c + vec3f(0.03))) /
807 (c * (2.43 * c + vec3f(0.59)) + vec3f(0.14)),
808 vec3f(0.0), vec3f(1.0));
809}
810fn linearToSrgb(c: vec3f) -> vec3f {
811 let low = c * 12.92;
812 let high = 1.055 * pow(max(c, vec3f(0.0)), vec3f(1.0 / 2.4)) - vec3f(0.055);
813 return select(low, high, c > vec3f(0.0031308));
814}
815fn automaticExposure(packedLimits: f32, raw: bool) -> f32 {
816 var logLuminance: array<f32, 16>;
817 var sampleIndex = 0;
818 for (var y = 0; y < 4; y = y + 1) {
819 for (var x = 0; x < 4; x = x + 1) {
820 let uv = (vec2f(f32(x), f32(y)) + vec2f(0.5)) * 0.25;
821 var sampleColor = textureSampleLevel(colorTex, colorSampler, uv, 0.0).rgb;
822 if (raw) {
823 sampleColor = textureSampleLevel(rawSceneTex, rawSceneSampler, uv, 0.0).rgb;
824 }
825 sampleColor = max(sampleColor, vec3f(0.0));
826 let luminance = dot(sampleColor, vec3f(0.2126, 0.7152, 0.0722));
827 logLuminance[sampleIndex] = log2(clamp(luminance, 0.0001, 65504.0));
828 sampleIndex += 1;
829 }
830 }
831 for (var i = 1; i < 16; i = i + 1) {
832 let value = logLuminance[i];
833 var j = i - 1;
834 while (j >= 0 && logLuminance[j] > value) {
835 logLuminance[j + 1] = logLuminance[j];
836 j -= 1;
837 }
838 logLuminance[j + 1] = value;
839 }
840 var trimmedLogSum = 0.0;
841 for (var i = 2; i < 14; i = i + 1) {
842 trimmedLogSum += logLuminance[i];
843 }
844 let geometricMean = exp2(trimmedLogSum * (1.0 / 12.0));
845 let packed = round(packedLimits);
846 let minEV = (packed - floor(packed * (1.0 / 256.0)) * 256.0) *
847 (1.0 / 8.0) - 16.0;
848 let maxEV = floor(packed * (1.0 / 256.0)) * (1.0 / 8.0) - 16.0;
849 return clamp(0.18 / max(geometricMean, 0.0001), exp2(minEV), exp2(maxEV));
850}
851fn bloomPrefilter(color: vec3f, threshold: f32) -> vec3f {
852 let brightness = max(color.r, max(color.g, color.b));
853 let knee = max(threshold * 0.1, 0.01);
854 let soft = clamp((brightness - threshold + knee) / (2.0 * knee), 0.0, 1.0);
855 let contribution = max(brightness - threshold, soft * soft * knee);
856 return color * (contribution / max(brightness, 0.0001));
857}
858fn sampleBloom(uv: vec2f, threshold: f32) -> vec3f {
859 let texel = 1.0 / vec2f(textureDimensions(colorTex));
860 var bloom = bloomPrefilter(textureSampleLevel(colorTex, colorSampler, uv, 0.0).rgb,
861 threshold) * 0.2;
862 let offsets = array<vec2f, 12>(
863 vec2f(2.00, 0.00), vec2f(-1.47, 1.35), vec2f(0.17, -1.99), vec2f(1.22, 1.58),
864 vec2f(4.22, -2.68), vec2f(-4.92, -0.87), vec2f(3.04, 3.97), vec2f(-0.44, -4.98),
865 vec2f(-5.28, 7.28), vec2f(8.89, -1.41), vec2f(-7.82, -4.46), vec2f(2.07, 8.76));
866 let weights = array<f32, 12>(
867 0.10, 0.10, 0.10, 0.10,
868 0.06, 0.06, 0.06, 0.06,
869 0.04, 0.04, 0.04, 0.04);
870 for (var q = 0; q < 12; q = q + 1) {
871 bloom += bloomPrefilter(textureSampleLevel(colorTex, colorSampler,
872 uv + offsets[q] * texel, 0.0).rgb, threshold) * weights[q];
873 }
874 return bloom;
875}
876@fragment
877fn fs_main(in: FSIn) -> @location(0) vec4f {
878 let hdr = textureSample(colorTex, colorSampler, in.uv);
879 let exposure = 1.0; // Applied once by the dedicated HDR pre-exposure pass.
880 let encodeSrgb = in.color.a >= 65536.0;
881 let bloomPacked = round(in.color.a) - floor(round(in.color.a) * (1.0 / 65536.0)) * 65536.0;
882 let bloomIntensity = (bloomPacked - floor(bloomPacked * (1.0 / 256.0)) * 256.0) *
883 (1.0 / 32.0);
884 let bloomThreshold = floor(bloomPacked * (1.0 / 256.0)) * (1.0 / 16.0);
885 var bloom = vec3f(0.0);
886 if (bloomIntensity > 0.0) {
887 bloom = sampleBloom(in.uv, bloomThreshold);
888 }
889 var displayColor = acesFitted((hdr.rgb + bloom * bloomIntensity) * exposure);
890 if (encodeSrgb) {
891 displayColor = linearToSrgb(displayColor);
892 }
893 return vec4f(displayColor, hdr.a);
894}
895)wgsl";
896 wgpu::ShaderModule fragModule = makeWgslModule(dev, toneMapScene ? kSceneTonemapFragWgsl : kTexturedFragWgsl);
897 pd.vertex.module = vertModule.Get();
898 pd.vertex.entryPoint = sv("vs_main");
899 pd.vertex.bufferCount = 1;
900 pd.vertex.buffers = &vb;
901 WGPUFragmentState fs{};
902 fs.module = fragModule.Get();
903 fs.entryPoint = sv("fs_main");
904 fs.targetCount = 1;
905 fs.targets = &target;
906 pd.fragment = &fs;
907 pd.primitive.topology = WGPUPrimitiveTopology_TriangleList;
908 pd.primitive.frontFace = WGPUFrontFace_CCW;
909 pd.primitive.cullMode = WGPUCullMode_None;
910 pd.primitive.stripIndexFormat = WGPUIndexFormat_Undefined;
911 pd.depthStencil = nullptr;
912 pd.multisample.count = 1;
913 // Zero-init would leave mask=0, which discards every fragment
914 // (sampleMask=0). The WebGPU default is 0xFFFFFFFF (all samples).
915 pd.multisample.mask = 0xFFFFFFFFu;
916 return dev.CreateRenderPipeline(reinterpret_cast<const wgpu::RenderPipelineDescriptor*>(&pd));
917}
918
919wgpu::RenderPipeline make2DLitPipeline(wgpu::Device& dev, wgpu::PipelineLayout layout, WGPUTextureFormat format,
921 wgpu::VertexAttribute attrs[3] = {};
922 attrs[0].format = wgpu::VertexFormat::Float32x2;
923 attrs[0].offset = 0;
924 attrs[0].shaderLocation = 0;
925 attrs[1].format = wgpu::VertexFormat::Float32x4;
926 attrs[1].offset = 8;
927 attrs[1].shaderLocation = 1;
928 attrs[2].format = wgpu::VertexFormat::Float32x2;
929 attrs[2].offset = 24;
930 attrs[2].shaderLocation = 2;
931
932 PipelineBuilder b;
933 b.vertexLayout(32, wgpu::VertexStepMode::Vertex, attrs, 3);
934 b.shader(makeWgslModule(dev, kLit2DVertWgsl), "vs_main", makeWgslModule(dev, kLit2DFragWgsl), "fs_main");
935 b.layout(layout);
936 b.colorTarget(format, blend);
937 return b.build(dev);
938}
939
940} // namespace
941
942void Graphics::create2DPipelines() {
943 tex2DSetLayout = make2DBindGroupLayout();
944 tex2DPipelineLayout = make2DPipelineLayout();
945
946 colorPipeline = make2DColorPipeline(device, surfaceFormat, BlendMode::Alpha);
947 texturedPipeline = make2DTexturedPipeline(device, tex2DPipelineLayout, surfaceFormat, BlendMode::Alpha);
948 colorAdditivePipeline = make2DColorPipeline(device, surfaceFormat, BlendMode::Additive);
949 texturedAdditivePipeline = make2DTexturedPipeline(device, tex2DPipelineLayout, surfaceFormat, BlendMode::Additive);
950 colorPremultipliedPipeline = make2DColorPipeline(device, surfaceFormat, BlendMode::Premultiplied);
951 texturedPremultipliedPipeline =
952 make2DTexturedPipeline(device, tex2DPipelineLayout, surfaceFormat, BlendMode::Premultiplied);
953 colorMultiplyPipeline = make2DColorPipeline(device, surfaceFormat, BlendMode::Multiply);
954 texturedMultiplyPipeline = make2DTexturedPipeline(device, tex2DPipelineLayout, surfaceFormat, BlendMode::Multiply);
955 colorOpaquePipeline = make2DColorPipeline(device, surfaceFormat, BlendMode::Opaque);
956 texturedOpaquePipeline = make2DTexturedPipeline(device, tex2DPipelineLayout, surfaceFormat, BlendMode::Opaque);
957 sceneTonemapPipeline = make2DTexturedPipeline(device, tex2DPipelineLayout, surfaceFormat, BlendMode::Opaque, true);
958 lit2dPipeline = make2DLitPipeline(device, tex2DPipelineLayout, surfaceFormat, BlendMode::Alpha);
959 lit2dAdditivePipeline = make2DLitPipeline(device, tex2DPipelineLayout, surfaceFormat, BlendMode::Additive);
960 lit2dPremultipliedPipeline =
961 make2DLitPipeline(device, tex2DPipelineLayout, surfaceFormat, BlendMode::Premultiplied);
962 lit2dMultiplyPipeline = make2DLitPipeline(device, tex2DPipelineLayout, surfaceFormat, BlendMode::Multiply);
963 lit2dOpaquePipeline = make2DLitPipeline(device, tex2DPipelineLayout, surfaceFormat, BlendMode::Opaque);
964
965 offscreenColorPipeline = make2DColorPipeline(device, WGPUTextureFormat_RGBA8Unorm, BlendMode::Alpha);
966 offscreenTexturedPipeline =
967 make2DTexturedPipeline(device, tex2DPipelineLayout, WGPUTextureFormat_RGBA8Unorm, BlendMode::Alpha);
968 offscreenColorAdditivePipeline = make2DColorPipeline(device, WGPUTextureFormat_RGBA8Unorm, BlendMode::Additive);
969 offscreenTexturedAdditivePipeline =
970 make2DTexturedPipeline(device, tex2DPipelineLayout, WGPUTextureFormat_RGBA8Unorm, BlendMode::Additive);
971 offscreenColorPremultipliedPipeline =
972 make2DColorPipeline(device, WGPUTextureFormat_RGBA8Unorm, BlendMode::Premultiplied);
973 offscreenTexturedPremultipliedPipeline =
974 make2DTexturedPipeline(device, tex2DPipelineLayout, WGPUTextureFormat_RGBA8Unorm, BlendMode::Premultiplied);
975 offscreenColorMultiplyPipeline = make2DColorPipeline(device, WGPUTextureFormat_RGBA8Unorm, BlendMode::Multiply);
976 offscreenTexturedMultiplyPipeline =
977 make2DTexturedPipeline(device, tex2DPipelineLayout, WGPUTextureFormat_RGBA8Unorm, BlendMode::Multiply);
978 offscreenColorOpaquePipeline = make2DColorPipeline(device, WGPUTextureFormat_RGBA8Unorm, BlendMode::Opaque);
979 offscreenTexturedOpaquePipeline =
980 make2DTexturedPipeline(device, tex2DPipelineLayout, WGPUTextureFormat_RGBA8Unorm, BlendMode::Opaque);
981 hdrOffscreenTexturedPipeline =
982 make2DTexturedPipeline(device, tex2DPipelineLayout, WGPUTextureFormat_RGBA16Float, BlendMode::Alpha);
983 hdrOffscreenTexturedOpaquePipeline =
984 make2DTexturedPipeline(device, tex2DPipelineLayout, WGPUTextureFormat_RGBA16Float, BlendMode::Opaque);
985 offscreenLitPipeline =
986 make2DLitPipeline(device, tex2DPipelineLayout, WGPUTextureFormat_RGBA8Unorm, BlendMode::Alpha);
987 offscreenLitAdditivePipeline =
988 make2DLitPipeline(device, tex2DPipelineLayout, WGPUTextureFormat_RGBA8Unorm, BlendMode::Additive);
989 offscreenLitPremultipliedPipeline =
990 make2DLitPipeline(device, tex2DPipelineLayout, WGPUTextureFormat_RGBA8Unorm, BlendMode::Premultiplied);
991 offscreenLitMultiplyPipeline =
992 make2DLitPipeline(device, tex2DPipelineLayout, WGPUTextureFormat_RGBA8Unorm, BlendMode::Multiply);
993 offscreenLitOpaquePipeline =
994 make2DLitPipeline(device, tex2DPipelineLayout, WGPUTextureFormat_RGBA8Unorm, BlendMode::Opaque);
995 hdrOffscreenLitPipeline =
996 make2DLitPipeline(device, tex2DPipelineLayout, WGPUTextureFormat_RGBA16Float, BlendMode::Alpha);
997 hdrOffscreenLitAdditivePipeline =
998 make2DLitPipeline(device, tex2DPipelineLayout, WGPUTextureFormat_RGBA16Float, BlendMode::Additive);
999 hdrOffscreenLitPremultipliedPipeline =
1000 make2DLitPipeline(device, tex2DPipelineLayout, WGPUTextureFormat_RGBA16Float, BlendMode::Premultiplied);
1001 hdrOffscreenLitMultiplyPipeline =
1002 make2DLitPipeline(device, tex2DPipelineLayout, WGPUTextureFormat_RGBA16Float, BlendMode::Multiply);
1003 hdrOffscreenLitOpaquePipeline =
1004 make2DLitPipeline(device, tex2DPipelineLayout, WGPUTextureFormat_RGBA16Float, BlendMode::Opaque);
1005}
1006
1007wgpu::RenderPipeline Graphics::get2DColorPipeline(BlendMode blend, bool offscreen) {
1008 switch (blend) {
1009 case BlendMode::Additive: return offscreen ? offscreenColorAdditivePipeline : colorAdditivePipeline;
1011 return offscreen ? offscreenColorPremultipliedPipeline : colorPremultipliedPipeline;
1012 case BlendMode::Multiply: return offscreen ? offscreenColorMultiplyPipeline : colorMultiplyPipeline;
1013 case BlendMode::Opaque: return offscreen ? offscreenColorOpaquePipeline : colorOpaquePipeline;
1014 case BlendMode::Alpha:
1015 default: return offscreen ? offscreenColorPipeline : colorPipeline;
1016 }
1017}
1018
1019wgpu::RenderPipeline Graphics::get2DTexturedPipeline(BlendMode blend, bool offscreen) {
1020 switch (blend) {
1021 case BlendMode::Additive: return offscreen ? offscreenTexturedAdditivePipeline : texturedAdditivePipeline;
1023 return offscreen ? offscreenTexturedPremultipliedPipeline : texturedPremultipliedPipeline;
1024 case BlendMode::Multiply: return offscreen ? offscreenTexturedMultiplyPipeline : texturedMultiplyPipeline;
1025 case BlendMode::Opaque: return offscreen ? offscreenTexturedOpaquePipeline : texturedOpaquePipeline;
1026 case BlendMode::Alpha:
1027 default: return offscreen ? offscreenTexturedPipeline : texturedPipeline;
1028 }
1029}
1030
1031wgpu::RenderPipeline Graphics::get2DLitPipeline(BlendMode blend, WGPUTextureFormat format) {
1032 const bool hdr = format == WGPUTextureFormat_RGBA16Float;
1033 const bool offscreen = hdr || uint32_t(format) != uint32_t(surfaceFormat);
1034 auto pick = [](BlendMode mode, wgpu::RenderPipeline alpha, wgpu::RenderPipeline additive,
1035 wgpu::RenderPipeline premultiplied, wgpu::RenderPipeline multiply,
1036 wgpu::RenderPipeline opaque) -> wgpu::RenderPipeline {
1037 switch (mode) {
1038 case BlendMode::Additive: return additive;
1039 case BlendMode::Premultiplied: return premultiplied;
1040 case BlendMode::Multiply: return multiply;
1041 case BlendMode::Opaque: return opaque;
1042 case BlendMode::Alpha:
1043 default: return alpha;
1044 }
1045 };
1046 if (hdr) {
1047 return pick(blend, hdrOffscreenLitPipeline, hdrOffscreenLitAdditivePipeline,
1048 hdrOffscreenLitPremultipliedPipeline, hdrOffscreenLitMultiplyPipeline,
1049 hdrOffscreenLitOpaquePipeline);
1050 }
1051 if (offscreen) {
1052 return pick(blend, offscreenLitPipeline, offscreenLitAdditivePipeline, offscreenLitPremultipliedPipeline,
1053 offscreenLitMultiplyPipeline, offscreenLitOpaquePipeline);
1054 }
1055 return pick(blend, lit2dPipeline, lit2dAdditivePipeline, lit2dPremultipliedPipeline, lit2dMultiplyPipeline,
1056 lit2dOpaquePipeline);
1057}
1058
1059void Graphics::createMesh3DPipelines() {
1060 mesh3dSetLayout = makeMesh3DBindGroupLayout();
1061 // Bind groups reference the layout; drop cached groups when it changes.
1062 clearMeshBindGroupCache();
1063 mesh3dPipelineLayout = makeMesh3DPipelineLayout();
1064}
1065
1066wgpu::RenderPipeline Graphics::getMesh3DPipeline(BlendMode blend, bool depthWrite, bool doubleSided,
1067 bool canvasTarget) {
1068 const size_t index = meshPipelineIndex(blend, depthWrite, doubleSided);
1069 wgpu::RenderPipeline& pipeline = canvasTarget ? mesh3dCanvasPipelines[index] : mesh3dPipelines[index];
1070 if (pipeline) return pipeline;
1071
1072 WGPUVertexAttribute attrs[6] = {};
1073 fillMeshAttributes(attrs);
1074 WGPUVertexBufferLayout vb{};
1075 fillVertexLayout(vb, sizeof(MeshVertex), attrs, 6);
1076
1077 WGPUDepthStencilState ds{};
1078 ds.format = WGPUTextureFormat_Depth32Float;
1079 ds.depthWriteEnabled = depthWrite ? WGPUOptionalBool_True : WGPUOptionalBool_False;
1080 ds.depthCompare = WGPUCompareFunction_Less;
1081
1082 WGPUBlendState blendDesc = blendState(blend);
1083 WGPUColorTargetState target{};
1084 target.format = sceneColorFormat;
1085 target.blend = blend == BlendMode::Opaque ? nullptr : &blendDesc;
1086 target.writeMask = WGPUColorWriteMask_All;
1087
1088 wgpu::ShaderModule vertModule = makeWgslModule(device, kMesh3DVertWgsl);
1089 wgpu::ShaderModule fragModule = makeWgslModule(device, kMesh3DFragWgsl);
1090 WGPUFragmentState fs{};
1091 fs.module = fragModule.Get();
1092 fs.entryPoint = sv("fs_main");
1093 fs.targetCount = 1;
1094 fs.targets = &target;
1095
1096 WGPURenderPipelineDescriptor pd{};
1097 pd.label = canvasTarget ? sv("eve_mesh3d_canvas_surface") : sv("eve_mesh3d_surface");
1098 pd.layout = mesh3dPipelineLayout.Get();
1099 pd.vertex.module = vertModule.Get();
1100 pd.vertex.entryPoint = sv("vs_main");
1101 pd.vertex.bufferCount = 1;
1102 pd.vertex.buffers = &vb;
1103 pd.fragment = &fs;
1104 pd.primitive.topology = WGPUPrimitiveTopology_TriangleList;
1105 pd.primitive.frontFace = WGPUFrontFace_CCW;
1106 pd.primitive.cullMode = doubleSided ? WGPUCullMode_None : WGPUCullMode_Back;
1107 pd.primitive.stripIndexFormat = WGPUIndexFormat_Undefined;
1108 pd.depthStencil = &ds;
1109 pd.multisample.count = canvasTarget ? 1 : sceneColorSamples;
1110 pd.multisample.mask = 0xFFFFFFFFu;
1111 pipeline = device.CreateRenderPipeline(reinterpret_cast<const wgpu::RenderPipelineDescriptor*>(&pd));
1112
1113 if (!canvasTarget && blend == BlendMode::Opaque && depthWrite && !doubleSided) mesh3dPipeline = pipeline;
1114 if (!canvasTarget && blend == BlendMode::Alpha && !depthWrite && !doubleSided) mesh3dTransparentPipeline = pipeline;
1115 if (canvasTarget && blend == BlendMode::Opaque && depthWrite && !doubleSided) mesh3dCanvasPipeline = pipeline;
1116 return pipeline;
1117}
1118
1119
1120void Graphics::createMesh3DClusteredPipeline() {
1121 mesh3dClusteredSetLayout = makeMesh3DClusteredBindGroupLayout();
1122 mesh3dClusteredPipelineLayout = makeMesh3DClusteredPipelineLayout();
1123
1124 WGPUVertexAttribute attrs[6] = {};
1125 fillMeshAttributes(attrs);
1126 attrs[0].format = WGPUVertexFormat_Float32x3; // pos
1127 attrs[0].offset = 0;
1128 attrs[0].shaderLocation = 0;
1129 attrs[1].format = WGPUVertexFormat_Float32x3; // normal
1130 attrs[1].offset = 12;
1131 attrs[1].shaderLocation = 1;
1132 attrs[2].format = WGPUVertexFormat_Float32x2; // uv
1133 attrs[2].offset = 24;
1134 attrs[2].shaderLocation = 2;
1135 WGPUVertexBufferLayout vb{};
1136 fillVertexLayout(vb, sizeof(MeshVertex), attrs, 6);
1137
1138 WGPUDepthStencilState ds{};
1139 ds.format = WGPUTextureFormat_Depth32Float;
1140 ds.depthWriteEnabled = WGPUOptionalBool_True;
1141 ds.depthCompare = WGPUCompareFunction_Less;
1142 ds.stencilReadMask = 0;
1143 ds.stencilWriteMask = 0;
1144
1145 WGPUColorTargetState target{};
1146 target.format = sceneColorFormat;
1147 target.blend = nullptr;
1148 target.writeMask = WGPUColorWriteMask_All;
1149
1150 WGPURenderPipelineDescriptor pd{};
1151 pd.label = sv("eve_mesh3d_clustered");
1152 pd.layout = mesh3dClusteredPipelineLayout.Get();
1153 wgpu::ShaderModule vertModule = makeWgslModule(device, kMesh3DClusteredVertWgsl);
1154 wgpu::ShaderModule fragModule = makeWgslModule(device, kMesh3DClusteredFragWgsl);
1155 pd.vertex.module = vertModule.Get();
1156 pd.vertex.entryPoint = sv("vs_main");
1157 pd.vertex.bufferCount = 1;
1158 pd.vertex.buffers = &vb;
1159 WGPUFragmentState fs{};
1160 fs.module = fragModule.Get();
1161 fs.entryPoint = sv("fs_main");
1162 fs.targetCount = 1;
1163 fs.targets = &target;
1164 pd.fragment = &fs;
1165 pd.primitive.topology = WGPUPrimitiveTopology_TriangleList;
1166 pd.primitive.frontFace = WGPUFrontFace_CCW;
1167 pd.primitive.cullMode = WGPUCullMode_Back;
1168 pd.primitive.stripIndexFormat = WGPUIndexFormat_Undefined;
1169 pd.depthStencil = &ds;
1170 pd.multisample.count = sceneColorSamples;
1171 pd.multisample.mask = 0xFFFFFFFFu;
1172 mesh3dClusteredPipeline = device.CreateRenderPipeline(reinterpret_cast<const wgpu::RenderPipelineDescriptor*>(&pd));
1173}
1174
1175void Graphics::createShadowPipelines() {
1176 shadowSetLayout = makeShadowBindGroupLayout();
1177 shadowPipelineLayout = makeShadowPipelineLayout();
1178
1179 WGPUVertexAttribute attrs[6] = {};
1180 fillMeshAttributes(attrs);
1181 attrs[0].format = WGPUVertexFormat_Float32x3;
1182 attrs[0].offset = 0;
1183 attrs[0].shaderLocation = 0;
1184 attrs[1].format = WGPUVertexFormat_Float32x3;
1185 attrs[1].offset = 12;
1186 attrs[1].shaderLocation = 1;
1187 attrs[2].format = WGPUVertexFormat_Float32x2;
1188 attrs[2].offset = 24;
1189 attrs[2].shaderLocation = 2;
1190 WGPUVertexBufferLayout vb{};
1191 fillVertexLayout(vb, sizeof(MeshVertex), attrs, 6);
1192
1193 WGPUDepthStencilState ds{};
1194 ds.format = WGPUTextureFormat_Depth32Float;
1195 ds.depthWriteEnabled = WGPUOptionalBool_True;
1196 ds.depthCompare = WGPUCompareFunction_Less;
1197 ds.stencilReadMask = 0;
1198 ds.stencilWriteMask = 0;
1199
1200 WGPURenderPipelineDescriptor pd{};
1201 pd.label = sv("eve_shadow");
1202 pd.layout = shadowPipelineLayout.Get();
1203 wgpu::ShaderModule vertModule = makeWgslModule(device, kMesh3DShadowVertWgsl);
1204 pd.vertex.module = vertModule.Get();
1205 pd.vertex.entryPoint = sv("vs_main");
1206 pd.vertex.bufferCount = 1;
1207 pd.vertex.buffers = &vb;
1208 pd.fragment = nullptr;
1209 pd.primitive.topology = WGPUPrimitiveTopology_TriangleList;
1210 pd.primitive.frontFace = WGPUFrontFace_CCW;
1211 pd.primitive.cullMode = WGPUCullMode_None;
1212 pd.primitive.stripIndexFormat = WGPUIndexFormat_Undefined;
1213 pd.depthStencil = &ds;
1214 pd.multisample.count = 1;
1215 // Zero-init would leave mask=0, which discards every fragment
1216 // (sampleMask=0). The WebGPU default is 0xFFFFFFFF (all samples).
1217 pd.multisample.mask = 0xFFFFFFFFu;
1218 mesh3dShadowPipeline = device.CreateRenderPipeline(reinterpret_cast<const wgpu::RenderPipelineDescriptor*>(&pd));
1219 pd.label = sv("eve_shadow_single_sided");
1220 pd.primitive.cullMode = WGPUCullMode_Back;
1221 mesh3dShadowSingleSidedPipeline =
1222 device.CreateRenderPipeline(reinterpret_cast<const wgpu::RenderPipelineDescriptor*>(&pd));
1223
1224 wgpu::ShaderModule alphaVertModule = makeWgslModule(device, kMesh3DShadowAlphaVertWgsl);
1225 wgpu::ShaderModule alphaFragModule = makeWgslModule(device, kMesh3DShadowAlphaFragWgsl);
1226 pd.label = sv("eve_shadow_alpha");
1227 pd.primitive.cullMode = WGPUCullMode_None;
1228 pd.vertex.module = alphaVertModule.Get();
1229 WGPUFragmentState alphaFs{};
1230 alphaFs.module = alphaFragModule.Get();
1231 alphaFs.entryPoint = sv("fs_main");
1232 alphaFs.targetCount = 0;
1233 alphaFs.targets = nullptr;
1234 pd.fragment = &alphaFs;
1235 mesh3dShadowAlphaPipeline =
1236 device.CreateRenderPipeline(reinterpret_cast<const wgpu::RenderPipelineDescriptor*>(&pd));
1237 pd.label = sv("eve_shadow_alpha_single_sided");
1238 pd.primitive.cullMode = WGPUCullMode_Back;
1239 mesh3dShadowAlphaSingleSidedPipeline =
1240 device.CreateRenderPipeline(reinterpret_cast<const wgpu::RenderPipelineDescriptor*>(&pd));
1241}
1242
1243void Graphics::createGbufferPipelines() {
1244 gbufferSetLayout = makeGbufferBindGroupLayout();
1245 gbufferPipelineLayout = makeGbufferPipelineLayout();
1246
1247 WGPUVertexAttribute attrs[6] = {};
1248 fillMeshAttributes(attrs);
1249 attrs[0].format = WGPUVertexFormat_Float32x3;
1250 attrs[0].offset = 0;
1251 attrs[0].shaderLocation = 0;
1252 attrs[1].format = WGPUVertexFormat_Float32x3;
1253 attrs[1].offset = 12;
1254 attrs[1].shaderLocation = 1;
1255 attrs[2].format = WGPUVertexFormat_Float32x2;
1256 attrs[2].offset = 24;
1257 attrs[2].shaderLocation = 2;
1258 WGPUVertexBufferLayout vb{};
1259 fillVertexLayout(vb, sizeof(MeshVertex), attrs, 6);
1260
1261 WGPUDepthStencilState ds{};
1262 ds.format = WGPUTextureFormat_Depth32Float;
1263 ds.depthWriteEnabled = WGPUOptionalBool_True;
1264 ds.depthCompare = WGPUCompareFunction_Less;
1265 ds.stencilReadMask = 0;
1266 ds.stencilWriteMask = 0;
1267
1268 WGPUColorTargetState targets[5] = {};
1269 for (int i = 0; i < 5; ++i) {
1270 targets[i].format = WGPUTextureFormat_RGBA8Unorm;
1271 targets[i].blend = nullptr;
1272 targets[i].writeMask = WGPUColorWriteMask_All;
1273 }
1274
1275 WGPURenderPipelineDescriptor pd{};
1276 pd.label = sv("eve_gbuffer");
1277 pd.layout = gbufferPipelineLayout.Get();
1278 wgpu::ShaderModule vertModule = makeWgslModule(device, kMesh3DGbufferVertWgsl);
1279 wgpu::ShaderModule fragModule = makeWgslModule(device, kMesh3DGbufferFragWgsl);
1280 pd.vertex.module = vertModule.Get();
1281 pd.vertex.entryPoint = sv("vs_main");
1282 pd.vertex.bufferCount = 1;
1283 pd.vertex.buffers = &vb;
1284 WGPUFragmentState fs{};
1285 fs.module = fragModule.Get();
1286 fs.entryPoint = sv("fs_main");
1287 fs.targetCount = 5;
1288 fs.targets = targets;
1289 pd.fragment = &fs;
1290 pd.primitive.topology = WGPUPrimitiveTopology_TriangleList;
1291 pd.primitive.frontFace = WGPUFrontFace_CCW;
1292 pd.primitive.cullMode = WGPUCullMode_None;
1293 pd.primitive.stripIndexFormat = WGPUIndexFormat_Undefined;
1294 pd.depthStencil = &ds;
1295 pd.multisample.count = 1;
1296 // Zero-init would leave mask=0, which discards every fragment
1297 // (sampleMask=0). The WebGPU default is 0xFFFFFFFF (all samples).
1298 pd.multisample.mask = 0xFFFFFFFFu;
1299 mesh3dGbufferPipeline = device.CreateRenderPipeline(reinterpret_cast<const wgpu::RenderPipelineDescriptor*>(&pd));
1300
1301 wgpu::ShaderModule alphaFragModule = makeWgslModule(device, kMesh3DGbufferAlphaFragWgsl);
1302 pd.label = sv("eve_gbuffer_alpha");
1303 fs.module = alphaFragModule.Get();
1304 mesh3dGbufferAlphaPipeline =
1305 device.CreateRenderPipeline(reinterpret_cast<const wgpu::RenderPipelineDescriptor*>(&pd));
1306}
1307
1308void Graphics::createDecalPipeline() {
1309 decalSetLayout = makeDecalBindGroupLayout();
1310 decalPipelineLayout = makeDecalPipelineLayout();
1311 wgpu::ShaderModule vert = makeWgslModule(device, kDecalVertWgsl);
1312 wgpu::ShaderModule frag = makeWgslModule(device, kDecalFragWgsl);
1313 WGPUBlendState blend = alphaBlend();
1314 WGPUColorTargetState targets[3]{};
1315 for (auto& target : targets) {
1316 target.format = WGPUTextureFormat_RGBA8Unorm;
1317 target.blend = &blend;
1318 target.writeMask = WGPUColorWriteMask_All;
1319 }
1320 WGPUFragmentState fs{};
1321 fs.module = frag.Get();
1322 fs.entryPoint = sv("fs_main");
1323 fs.targetCount = 3;
1324 fs.targets = targets;
1325 WGPURenderPipelineDescriptor pd{};
1326 pd.label = sv("eve_decal");
1327 pd.layout = decalPipelineLayout.Get();
1328 pd.vertex.module = vert.Get();
1329 pd.vertex.entryPoint = sv("vs_main");
1330 pd.fragment = &fs;
1331 pd.primitive.topology = WGPUPrimitiveTopology_TriangleList;
1332 // kMesh3DVertWgsl mirrors clip-space Y, so object-space CCW becomes
1333 // clockwise in framebuffer space (the Vulkan pipeline uses Clockwise).
1334 pd.primitive.frontFace = WGPUFrontFace_CW;
1335 pd.primitive.cullMode = WGPUCullMode_None;
1336 pd.multisample.count = 1;
1337 pd.multisample.mask = 0xFFFFFFFFu;
1338 decalPipeline = device.CreateRenderPipeline(reinterpret_cast<const wgpu::RenderPipelineDescriptor*>(&pd));
1339}
1340
1341
1342// ---------------------------------------------------------------------------
1343// SSAO (screen-space ambient occlusion)
1344// ---------------------------------------------------------------------------
1345
1346void Graphics::ensureAOResources(int width, int height) {
1347 if (!device || width <= 0 || height <= 0) return;
1348 if (!aoSetLayout) {
1349 BindGroupLayoutBuilder b;
1350 b.buffer(0, wgpu::ShaderStage::Fragment, wgpu::BufferBindingType::Uniform, false, 32);
1351 b.texture(1, wgpu::ShaderStage::Fragment, wgpu::TextureSampleType::Depth, wgpu::TextureViewDimension::e2D);
1352 b.sampler(2, wgpu::ShaderStage::Fragment, wgpu::SamplerBindingType::Filtering);
1353 aoSetLayout = b.build(device, "eve_ao");
1354 WGPUBindGroupLayout layouts[1] = {aoSetLayout.Get()};
1355 WGPUPipelineLayoutDescriptor pl{};
1356 pl.label = sv("eve_ao_layout");
1357 pl.bindGroupLayoutCount = 1;
1358 pl.bindGroupLayouts = layouts;
1359 aoPipelineLayout = device.CreatePipelineLayout(reinterpret_cast<const wgpu::PipelineLayoutDescriptor*>(&pl));
1360
1361 WGPUVertexAttribute attrs[3]{};
1362 attrs[0].format = WGPUVertexFormat_Float32x2;
1363 attrs[0].offset = 0;
1364 attrs[0].shaderLocation = 0;
1365 attrs[1].format = WGPUVertexFormat_Float32x4;
1366 attrs[1].offset = 8;
1367 attrs[1].shaderLocation = 1;
1368 attrs[2].format = WGPUVertexFormat_Float32x2;
1369 attrs[2].offset = 24;
1370 attrs[2].shaderLocation = 2;
1371 WGPUVertexBufferLayout vb{};
1372 fillVertexLayout(vb, 32, attrs, 3);
1373
1374 WGPUColorTargetState target{};
1375 target.format = WGPUTextureFormat_RGBA8Unorm;
1376 target.blend = nullptr;
1377 target.writeMask = WGPUColorWriteMask_All;
1378
1379 WGPURenderPipelineDescriptor pd{};
1380 pd.label = sv("eve_ssao");
1381 pd.layout = aoPipelineLayout.Get();
1382 wgpu::ShaderModule vertModule = makeWgslModule(device, kTexturedVertWgsl);
1383 wgpu::ShaderModule fragModule = makeWgslModule(device, kSSAOFragWgsl);
1384 pd.vertex.module = vertModule.Get();
1385 pd.vertex.entryPoint = sv("vs_main");
1386 pd.vertex.bufferCount = 1;
1387 pd.vertex.buffers = &vb;
1388 WGPUFragmentState fs{};
1389 fs.module = fragModule.Get();
1390 fs.entryPoint = sv("fs_main");
1391 fs.targetCount = 1;
1392 fs.targets = &target;
1393 pd.fragment = &fs;
1394 pd.primitive.topology = WGPUPrimitiveTopology_TriangleList;
1395 pd.primitive.frontFace = WGPUFrontFace_CCW;
1396 pd.primitive.cullMode = WGPUCullMode_None;
1397 pd.primitive.stripIndexFormat = WGPUIndexFormat_Undefined;
1398 pd.multisample.count = 1;
1399 pd.multisample.mask = 0xFFFFFFFFu;
1400 aoPipeline = device.CreateRenderPipeline(reinterpret_cast<const wgpu::RenderPipelineDescriptor*>(&pd));
1401 }
1402 if (!aoUbo) {
1403 WGPUBufferDescriptor bd{};
1404 bd.label = sv("eve_ao_ubo");
1405 bd.size = 64;
1406 bd.usage = WGPUBufferUsage_CopyDst | WGPUBufferUsage_Uniform;
1407 bd.mappedAtCreation = false;
1408 aoUbo = device.CreateBuffer(reinterpret_cast<const wgpu::BufferDescriptor*>(&bd));
1409 }
1410 if (!aoTex[0]) {
1411 for (int i = 0; i < 2; ++i) {
1412 WGPUTextureDescriptor td{};
1413 td.label = sv("eve_ao");
1414 td.dimension = WGPUTextureDimension_2D;
1415 // Half-resolution AO: the bilinear upsample in the mesh pass blurs
1416 // the per-pixel sampling grain that otherwise reads as noise.
1417 td.size = {static_cast<uint32_t>((width + 1) / 2), static_cast<uint32_t>((height + 1) / 2), 1};
1418 td.sampleCount = 1;
1419 td.format = WGPUTextureFormat_RGBA8Unorm;
1420 td.mipLevelCount = 1;
1421 td.usage = WGPUTextureUsage_TextureBinding | WGPUTextureUsage_RenderAttachment;
1422 aoTex[i] = device.CreateTexture(reinterpret_cast<const wgpu::TextureDescriptor*>(&td));
1423 aoView[i] = aoTex[i].CreateView();
1424 }
1425 aoReady = true;
1426 // New AO binding �?rebuild cached mesh bind groups.
1427 clearMeshBindGroupCache();
1428 }
1429 if (!fullscreenQuadReady) {
1430 float verts[32] = {
1431 -1.f, -1.f, 1.f, 1.f, 1.f, 1.f, 0.f, 0.f, 1.f, -1.f, 1.f, 1.f, 1.f, 1.f, 1.f, 0.f,
1432 1.f, 1.f, 1.f, 1.f, 1.f, 1.f, 1.f, 1.f, -1.f, 1.f, 1.f, 1.f, 1.f, 1.f, 0.f, 1.f,
1433 };
1434 uint32_t indices[6] = {0, 1, 2, 2, 3, 0};
1435 WGPUBufferDescriptor vbd{};
1436 vbd.label = sv("eve_fullscreen_vb");
1437 vbd.size = sizeof(verts);
1438 vbd.usage = WGPUBufferUsage_CopyDst | WGPUBufferUsage_Vertex;
1439 fullscreenQuadVb = device.CreateBuffer(reinterpret_cast<const wgpu::BufferDescriptor*>(&vbd));
1440 queue.WriteBuffer(fullscreenQuadVb, 0, verts, sizeof(verts));
1441 WGPUBufferDescriptor ibd{};
1442 ibd.label = sv("eve_fullscreen_ib");
1443 ibd.size = sizeof(indices);
1444 ibd.usage = WGPUBufferUsage_CopyDst | WGPUBufferUsage_Index;
1445 fullscreenQuadIb = device.CreateBuffer(reinterpret_cast<const wgpu::BufferDescriptor*>(&ibd));
1446 queue.WriteBuffer(fullscreenQuadIb, 0, indices, sizeof(indices));
1447 fullscreenQuadReady = true;
1448 }
1449}
1450
1451wgpu::BindGroup Graphics::makeAOBindGroup(wgpu::TextureView depthView) {
1452 WGPUBindGroupEntry entries[3]{};
1453 entries[0].binding = 0;
1454 entries[0].buffer = aoUbo.Get();
1455 entries[0].size = 32;
1456 entries[1].binding = 1;
1457 entries[1].textureView = depthView.Get();
1458 entries[2].binding = 2;
1459 entries[2].sampler = mainSampler.Get();
1460 WGPUBindGroupDescriptor desc{};
1461 desc.label = sv("eve_ao_group");
1462 desc.layout = aoSetLayout.Get();
1463 desc.entryCount = 3;
1464 desc.entries = entries;
1465 return device.CreateBindGroup(reinterpret_cast<const wgpu::BindGroupDescriptor*>(&desc));
1466}
1467
1468// ---------------------------------------------------------------------------
1469// Arena helpers
1470// ---------------------------------------------------------------------------
1471
1472uint32_t Graphics::UboArena::alloc(uint64_t size, uint64_t alignment) {
1473 uint64_t aligned = (used + alignment - 1) / alignment * alignment;
1474 used = aligned + size;
1475 return static_cast<uint32_t>(aligned);
1476}
1477
1478uint64_t Graphics::VertexArena::alloc(uint64_t bytes) {
1479 uint64_t at = used;
1480 used += bytes;
1481 return at;
1482}
1483
1484Graphics::UboArena& Graphics::currentUboArena() { return uboArenas[currentFrameSlot()]; }
1485Graphics::VertexArena& Graphics::currentVertexArena() { return vertexArenas[currentFrameSlot()]; }
1486
1487void Graphics::ensureUboArena(UboArena& arena, uint64_t bytes) {
1488 if (arena.buffer && arena.capacity >= bytes) return;
1489 uint64_t size = arena.capacity;
1490 while (size < bytes) size = size ? size * 2 : (64u << 10);
1491 WGPUBufferDescriptor bd{};
1492 bd.label = sv("eve_ubo_arena");
1493 bd.size = size;
1494 bd.usage = WGPUBufferUsage_CopyDst | WGPUBufferUsage_Uniform;
1495 bd.mappedAtCreation = false;
1496 arena.buffer = device.CreateBuffer(reinterpret_cast<const wgpu::BufferDescriptor*>(&bd));
1497 arena.capacity = size;
1498 arena.used = 0;
1499}
1500
1501void Graphics::ensureVertexArena(VertexArena& arena, uint64_t bytes) {
1502 if (arena.buffer && arena.capacity >= bytes) return;
1503 uint64_t size = arena.capacity;
1504 while (size < bytes) size = size ? size * 2 : (256u << 10);
1505 WGPUBufferDescriptor bd{};
1506 bd.label = sv("eve_vertex_arena");
1507 bd.size = size;
1508 bd.usage = WGPUBufferUsage_CopyDst | WGPUBufferUsage_Vertex;
1509 bd.mappedAtCreation = false;
1510 arena.buffer = device.CreateBuffer(reinterpret_cast<const wgpu::BufferDescriptor*>(&bd));
1511 arena.capacity = size;
1512 arena.used = 0;
1513}
1514
1515// ---------------------------------------------------------------------------
1516// Sampler
1517// ---------------------------------------------------------------------------
1518
1519wgpu::Sampler Graphics::makeSampler(const TextureSampler& s, uint32_t mipLevels) const {
1520 WGPUSamplerDescriptor d{};
1521 d.label = sv("eve_sampler");
1522 d.addressModeU = s.repeatU ? WGPUAddressMode_Repeat : WGPUAddressMode_ClampToEdge;
1523 d.addressModeV = s.repeatV ? WGPUAddressMode_Repeat : WGPUAddressMode_ClampToEdge;
1524 d.addressModeW = s.repeatW ? WGPUAddressMode_Repeat : WGPUAddressMode_ClampToEdge;
1525 d.magFilter = s.mag == FilterMode::Nearest ? WGPUFilterMode_Nearest : WGPUFilterMode_Linear;
1526 d.minFilter = s.min == FilterMode::Nearest ? WGPUFilterMode_Nearest : WGPUFilterMode_Linear;
1527 if (s.mipmap == MipmapMode::Disabled || mipLevels <= 1) {
1528 d.mipmapFilter = WGPUMipmapFilterMode_Nearest;
1529 d.lodMinClamp = 0.f;
1530 d.lodMaxClamp = 0.f;
1531 } else {
1532 d.mipmapFilter = s.mipmap == MipmapMode::Nearest ? WGPUMipmapFilterMode_Nearest : WGPUMipmapFilterMode_Linear;
1533 d.lodMinClamp = s.minLod;
1534 d.lodMaxClamp = std::min(std::max(s.maxLod, 0.f), float(mipLevels - 1));
1535 if (d.lodMaxClamp < d.lodMinClamp) d.lodMaxClamp = d.lodMinClamp;
1536 }
1537 float aniso = s.maxAnisotropy > 1.f ? s.maxAnisotropy : 1.f;
1538 d.maxAnisotropy = std::min(std::max(aniso, 1.f), std::max(maxSamplerAnisotropy, 1.f));
1539 return device.CreateSampler(reinterpret_cast<const wgpu::SamplerDescriptor*>(&d));
1540}
1541
1542// ---------------------------------------------------------------------------
1543// Texture creation
1544// ---------------------------------------------------------------------------
1545
1546Texture* Graphics::newTexture(int width, int height, const uint8_t* rgba, bool repeatU, bool repeatV) {
1549 info.sampler.repeatV = repeatV;
1550 return newTexture(width, height, rgba, info);
1551}
1552
1554 if (!data) throw Exception("newTexture: null ImageData");
1556 return newTexture(data->getWidth(), data->getHeight(), static_cast<const uint8_t*>(data->getData()), info);
1557}
1558
1560 if (!data) throw Exception("newTexture: null ImageData");
1561 if (data->getFormat() != "RGBA8") throw Exception("newTexture: only RGBA8 supported");
1562 return newTexture(data->getWidth(), data->getHeight(), static_cast<const uint8_t*>(data->getData()), info);
1563}
1564
1566 if (!data || contentKey.empty() || data->getFormat() != "RGBA8")
1568 "RGBA8 image and nonempty content key are required", {},
1569 {}, "graphics.texture.shared"));
1570 if (auto found = sharedTexturesByContent.find(contentKey); found != sharedTexturesByContent.end())
1571 return ResultRef<Texture>::success(std::ref(*found->second));
1572 try {
1573 Texture* texture = newTexture(data);
1574 if (!texture) throw Exception("shared texture upload produced no texture");
1575 sharedTexturesByContent.emplace(contentKey, texture);
1576 return ResultRef<Texture>::success(std::ref(*texture));
1577 } catch (const std::exception& error) {
1579 Diagnostic::error(DiagnosticCode::Failed, error.what(), {}, {}, "graphics.texture.shared"));
1580 }
1581}
1582
1583Texture* Graphics::newTexture(int width, int height, const uint8_t* rgba, const TextureCreateInfo& rawInfo) {
1584 if (width <= 0 || height <= 0) throw Exception("newTexture: invalid size %dx%d", width, height);
1585
1586 TextureCreateInfo info = rawInfo;
1587 if (info.generateMipmaps && info.sampler.mipmap == MipmapMode::Disabled) info.sampler.mipmap = MipmapMode::Linear;
1588 if (info.sampler.maxAnisotropy < 1.f) info.sampler.maxAnisotropy = 1.f;
1589
1590 auto gpu = std::make_unique<GpuTexture>();
1591 gpu->width = width;
1592 gpu->height = height;
1593 gpu->samplerState = info.sampler;
1594 gpu->mipLevels = info.generateMipmaps ? uint32_t(mipmapCountForSize(width, height)) : 1u;
1595
1596 WGPUTextureDescriptor td{};
1597 td.label = sv("eve_tex2d");
1598 td.dimension = WGPUTextureDimension_2D;
1599 td.size.width = static_cast<uint32_t>(width);
1600 td.size.height = static_cast<uint32_t>(height);
1601 td.size.depthOrArrayLayers = 1;
1602 td.sampleCount = 1;
1603 td.format = WGPUTextureFormat_RGBA8Unorm;
1604 td.mipLevelCount = gpu->mipLevels;
1605 td.usage = WGPUTextureUsage_TextureBinding | WGPUTextureUsage_CopyDst | WGPUTextureUsage_RenderAttachment;
1606 gpu->texture = device.CreateTexture(reinterpret_cast<const wgpu::TextureDescriptor*>(&td));
1607
1608 uploadTexturePixelsMips(gpu.get(), rgba, width, height);
1609
1610 WGPUTextureViewDescriptor vd{};
1611 vd.format = WGPUTextureFormat_RGBA8Unorm;
1612 vd.dimension = WGPUTextureViewDimension_2D;
1613 vd.baseMipLevel = 0;
1614 vd.mipLevelCount = gpu->mipLevels;
1615 vd.baseArrayLayer = 0;
1616 vd.arrayLayerCount = 1;
1617 gpu->view = gpu->texture.CreateView(reinterpret_cast<const wgpu::TextureViewDescriptor*>(&vd));
1618 gpu->sampler = makeSampler(info.sampler, gpu->mipLevels);
1619
1620 auto* tex = new Texture();
1621 tex->width = width;
1622 tex->height = height;
1623 tex->mipmapCount = int(gpu->mipLevels);
1624 tex->sampler = info.sampler;
1625 tex->gpuHandle = gpu.get();
1626
1627 ownedGpuTextures.push_back(std::move(gpu));
1628 ownedTextures.push_back(std::unique_ptr<Texture>(tex));
1629 return tex;
1630}
1631
1632void Graphics::uploadTexturePixels(GpuTexture* gt, const uint8_t* rgba, int w, int h, const TextureCreateInfo& info) {
1633 (void)info;
1634 uploadTexturePixelsMips(gt, rgba, w, h);
1635}
1636
1637void Graphics::uploadTexturePixelsMips(GpuTexture* gt, const uint8_t* rgba, int w, int h) {
1638 if (!rgba) return;
1639 std::vector<uint8_t> current;
1640 WGPUTexelCopyBufferLayout layout{};
1641 layout.offset = 0;
1642 layout.bytesPerRow = static_cast<uint32_t>(w * 4);
1643 layout.rowsPerImage = static_cast<uint32_t>(h);
1644 WGPUExtent3D extent{static_cast<uint32_t>(w), static_cast<uint32_t>(h), 1};
1645
1646 for (uint32_t m = 0; m < gt->mipLevels; ++m) {
1647 WGPUTexelCopyTextureInfo dst{};
1648 dst.texture = gt->texture.Get();
1649 dst.mipLevel = m;
1650 dst.aspect = WGPUTextureAspect_All;
1651 queue.WriteTexture(reinterpret_cast<const wgpu::TexelCopyTextureInfo*>(&dst), rgba,
1652 static_cast<uint64_t>(w) * h * 4,
1653 reinterpret_cast<const wgpu::TexelCopyBufferLayout*>(&layout),
1654 reinterpret_cast<const wgpu::Extent3D*>(&extent));
1655 if (m + 1 < gt->mipLevels) {
1656 // Box-filter downsample into the CPU buffer for the next level.
1657 const int nextW = std::max(w / 2, 1);
1658 const int nextH = std::max(h / 2, 1);
1659 std::vector<uint8_t> next(size_t(nextW) * size_t(nextH) * 4u);
1660 for (int y = 0; y < nextH; ++y) {
1661 const int y0 = std::min(y * 2, h - 1);
1662 const int y1 = std::min(y0 + 1, h - 1);
1663 for (int x = 0; x < nextW; ++x) {
1664 const int x0 = std::min(x * 2, w - 1);
1665 const int x1 = std::min(x0 + 1, w - 1);
1666 for (int c = 0; c < 4; ++c) {
1667 uint32_t acc = 0;
1668 acc += rgba[(size_t(y0) * w + x0) * 4u + c];
1669 acc += rgba[(size_t(y0) * w + x1) * 4u + c];
1670 acc += rgba[(size_t(y1) * w + x0) * 4u + c];
1671 acc += rgba[(size_t(y1) * w + x1) * 4u + c];
1672 next[(size_t(y) * nextW + x) * 4u + c] = uint8_t((acc + 2u) / 4u);
1673 }
1674 }
1675 }
1676 current = std::move(next);
1677 w = nextW;
1678 h = nextH;
1679 rgba = current.data();
1680 layout.bytesPerRow = static_cast<uint32_t>(w * 4);
1681 layout.rowsPerImage = static_cast<uint32_t>(h);
1682 extent = {static_cast<uint32_t>(w), static_cast<uint32_t>(h), 1};
1683 }
1684 }
1685}
1686
1687Texture* Graphics::newCubemap(int faceSize, const uint8_t* rgbaFaces) {
1689 return newCubemap(faceSize, rgbaFaces, info);
1690}
1691
1692Texture* Graphics::newCubemap(int faceSize, const uint8_t* rgbaFaces, const TextureCreateInfo& rawInfo) {
1693 if (faceSize <= 0 || !rgbaFaces) throw Exception("newCubemap: invalid size or null data");
1694
1695 TextureCreateInfo info = rawInfo;
1696 if (info.generateMipmaps && info.sampler.mipmap == MipmapMode::Disabled) info.sampler.mipmap = MipmapMode::Linear;
1697 if (info.sampler.maxAnisotropy < 1.f) info.sampler.maxAnisotropy = 1.f;
1698
1699 auto gpu = std::make_unique<GpuTexture>();
1700 gpu->width = faceSize;
1701 gpu->height = faceSize;
1702 gpu->samplerState = info.sampler;
1703 gpu->isCube = true;
1704 gpu->mipLevels = info.generateMipmaps ? uint32_t(mipmapCountForSize(faceSize, faceSize)) : 1u;
1705
1706 WGPUTextureDescriptor td{};
1707 td.label = sv("eve_cubemap");
1708 td.dimension = WGPUTextureDimension_2D;
1709 td.size.width = static_cast<uint32_t>(faceSize);
1710 td.size.height = static_cast<uint32_t>(faceSize);
1711 td.size.depthOrArrayLayers = 6;
1712 td.sampleCount = 1;
1713 td.format = WGPUTextureFormat_RGBA8Unorm;
1714 td.mipLevelCount = gpu->mipLevels;
1715 td.usage = WGPUTextureUsage_TextureBinding | WGPUTextureUsage_CopyDst;
1716 gpu->texture = device.CreateTexture(reinterpret_cast<const wgpu::TextureDescriptor*>(&td));
1717
1718 const std::vector<uint8_t> mipChain = buildGgxCubemapMipChain(rgbaFaces, uint32_t(faceSize), gpu->mipLevels);
1719 size_t mipOffset = 0;
1720 int mipW = faceSize;
1721 int mipH = faceSize;
1722 for (uint32_t mip = 0; mip < gpu->mipLevels; ++mip) {
1723 const size_t mipFaceBytes = size_t(mipW) * mipH * 4u;
1724 for (int f = 0; f < 6; ++f) {
1725 const uint8_t* pixels = mipChain.data() + mipOffset + size_t(f) * mipFaceBytes;
1726 WGPUTexelCopyBufferLayout layout{};
1727 layout.offset = 0;
1728 layout.bytesPerRow = static_cast<uint32_t>(mipW * 4);
1729 layout.rowsPerImage = static_cast<uint32_t>(mipH);
1730 WGPUExtent3D extent{static_cast<uint32_t>(mipW), static_cast<uint32_t>(mipH), 1};
1731 WGPUTexelCopyTextureInfo dst{};
1732 dst.texture = gpu->texture.Get();
1733 dst.mipLevel = mip;
1734 dst.aspect = WGPUTextureAspect_All;
1735 dst.origin = {0, 0, static_cast<uint32_t>(f)};
1736 queue.WriteTexture(reinterpret_cast<const wgpu::TexelCopyTextureInfo*>(&dst), pixels,
1737 static_cast<uint64_t>(mipW) * mipH * 4,
1738 reinterpret_cast<const wgpu::TexelCopyBufferLayout*>(&layout),
1739 reinterpret_cast<const wgpu::Extent3D*>(&extent));
1740 }
1741 mipOffset += mipFaceBytes * 6u;
1742 mipW = std::max(mipW / 2, 1);
1743 mipH = std::max(mipH / 2, 1);
1744 }
1745
1746 WGPUTextureViewDescriptor vd{};
1747 vd.format = WGPUTextureFormat_RGBA8Unorm;
1748 vd.dimension = WGPUTextureViewDimension_Cube;
1749 vd.baseMipLevel = 0;
1750 vd.mipLevelCount = gpu->mipLevels;
1751 vd.baseArrayLayer = 0;
1752 vd.arrayLayerCount = 6;
1753 gpu->view = gpu->texture.CreateView(reinterpret_cast<const wgpu::TextureViewDescriptor*>(&vd));
1754 gpu->sampler = makeSampler(info.sampler, gpu->mipLevels);
1755
1756 auto* tex = new Texture();
1757 tex->width = faceSize;
1758 tex->height = faceSize;
1759 tex->mipmapCount = int(gpu->mipLevels);
1760 tex->sampler = info.sampler;
1761 tex->gpuHandle = gpu.get();
1762
1763 ownedGpuTextures.push_back(std::move(gpu));
1764 ownedTextures.push_back(std::unique_ptr<Texture>(tex));
1765 return tex;
1766}
1767
1769 if (faceSize <= 0) throw Exception("newHDRCubemap: invalid face size");
1770 auto gpu = std::make_unique<GpuTexture>();
1771 gpu->width = faceSize;
1772 gpu->height = faceSize;
1773 gpu->isCube = true;
1774 gpu->isHDR = true;
1775 gpu->mipLevels = uint32_t(mipmapCountForSize(faceSize, faceSize));
1776 gpu->samplerState = TextureSampler::linear();
1777 gpu->samplerState.mipmap = MipmapMode::Linear;
1778
1779 WGPUTextureDescriptor td{};
1780 td.label = sv("eve_hdr_reflection_probe_staging");
1781 td.dimension = WGPUTextureDimension_2D;
1782 td.size = {static_cast<uint32_t>(faceSize), static_cast<uint32_t>(faceSize), 6};
1783 td.sampleCount = 1;
1784 td.format = WGPUTextureFormat_RGBA16Float;
1785 td.mipLevelCount = gpu->mipLevels;
1786 td.usage = WGPUTextureUsage_TextureBinding | WGPUTextureUsage_CopyDst | WGPUTextureUsage_CopySrc |
1787 WGPUTextureUsage_RenderAttachment;
1788 gpu->texture = device.CreateTexture(reinterpret_cast<const wgpu::TextureDescriptor*>(&td));
1789
1790 WGPUTextureViewDescriptor vd{};
1791 vd.format = WGPUTextureFormat_RGBA16Float;
1792 vd.dimension = WGPUTextureViewDimension_Cube;
1793 vd.baseMipLevel = 0;
1794 vd.mipLevelCount = gpu->mipLevels;
1795 vd.baseArrayLayer = 0;
1796 vd.arrayLayerCount = 6;
1797 gpu->view = gpu->texture.CreateView(reinterpret_cast<const wgpu::TextureViewDescriptor*>(&vd));
1798 gpu->sampler = makeSampler(gpu->samplerState, gpu->mipLevels);
1799
1800 auto* texture = new Texture();
1801 texture->width = faceSize;
1802 texture->height = faceSize;
1803 texture->mipmapCount = int(gpu->mipLevels);
1804 texture->sampler = gpu->samplerState;
1805 texture->gpuHandle = gpu.get();
1806 ownedGpuTextures.push_back(std::move(gpu));
1807 ownedTextures.emplace_back(texture);
1808 return texture;
1809}
1810
1812 auto* canvas = dynamic_cast<OffscreenCanvas*>(source);
1813 if (!canvas || !canvas->hdr || !cubemap || !cubemap->gpuHandle || face < 0 || face >= 6) return false;
1814 auto* target = static_cast<GpuTexture*>(cubemap->gpuHandle);
1815 if (!target->isCube || !target->isHDR || target->width != canvas->width || target->height != canvas->height)
1816 return false;
1817
1818 wgpu::CommandEncoder encoder = device.CreateCommandEncoder();
1819 WGPUTexelCopyTextureInfo src{};
1820 src.texture = canvas->color.Get();
1821 src.mipLevel = 0;
1822 src.aspect = WGPUTextureAspect_All;
1823 src.origin = {0, 0, 0};
1824 WGPUTexelCopyTextureInfo dst{};
1825 dst.texture = target->texture.Get();
1826 dst.mipLevel = 0;
1827 dst.aspect = WGPUTextureAspect_All;
1828 dst.origin = {0, 0, static_cast<uint32_t>(face)};
1829 WGPUExtent3D extent{static_cast<uint32_t>(canvas->width), static_cast<uint32_t>(canvas->height), 1};
1830 encoder.CopyTextureToTexture(reinterpret_cast<const wgpu::TexelCopyTextureInfo*>(&src),
1831 reinterpret_cast<const wgpu::TexelCopyTextureInfo*>(&dst),
1832 reinterpret_cast<const wgpu::Extent3D*>(&extent));
1833 wgpu::CommandBuffer command = encoder.Finish();
1834 queue.Submit(1, &command);
1835 return true;
1836}
1837
1838bool Graphics::copyHDRCanvasesToCubemap(Canvas* const* sources, int faceCount, Texture* cubemap) {
1839 if (!sources || faceCount < 1 || faceCount > 6 || !cubemap || !cubemap->gpuHandle) return false;
1840 auto* target = static_cast<GpuTexture*>(cubemap->gpuHandle);
1841 if (!target->isCube || !target->isHDR) return false;
1842
1843 std::array<OffscreenCanvas*, 6> canvases{};
1844 for (int face = 0; face < faceCount; ++face) {
1845 auto* canvas = dynamic_cast<OffscreenCanvas*>(sources[face]);
1846 if (!canvas || !canvas->hdr || target->width != canvas->width || target->height != canvas->height) return false;
1847 canvases[static_cast<size_t>(face)] = canvas;
1848 }
1849
1850 wgpu::CommandEncoder encoder = device.CreateCommandEncoder();
1851 for (int face = 0; face < faceCount; ++face) {
1852 const auto* canvas = canvases[static_cast<size_t>(face)];
1853 WGPUTexelCopyTextureInfo src{};
1854 src.texture = canvas->color.Get();
1855 src.mipLevel = 0;
1856 src.aspect = WGPUTextureAspect_All;
1857 src.origin = {0, 0, 0};
1858 WGPUTexelCopyTextureInfo dst{};
1859 dst.texture = target->texture.Get();
1860 dst.mipLevel = 0;
1861 dst.aspect = WGPUTextureAspect_All;
1862 dst.origin = {0, 0, static_cast<uint32_t>(face)};
1863 WGPUExtent3D extent{static_cast<uint32_t>(canvas->width), static_cast<uint32_t>(canvas->height), 1};
1864 encoder.CopyTextureToTexture(reinterpret_cast<const wgpu::TexelCopyTextureInfo*>(&src),
1865 reinterpret_cast<const wgpu::TexelCopyTextureInfo*>(&dst),
1866 reinterpret_cast<const wgpu::Extent3D*>(&extent));
1867 }
1868 wgpu::CommandBuffer command = encoder.Finish();
1869 queue.Submit(1, &command);
1870 return true;
1871}
1872
1873bool Graphics::filterHDRReflectionCubemap(Texture* cubemap, int sampleCount) {
1874 if (!cubemap || !cubemap->gpuHandle) return false;
1875 auto* target = static_cast<GpuTexture*>(cubemap->gpuHandle);
1876 if (!target->isCube || !target->isHDR || target->mipLevels < 2) return false;
1877 sampleCount = std::clamp(sampleCount, 8, 512);
1878 wgpuInstanceProcessEvents(instance.Get());
1879 OffscreenTimestampReadback* timestampSlot = nullptr;
1880 if (offscreenTimestampSupported && offscreenTimestampQuerySet && offscreenTimestampResolveBuffer) {
1881 for (size_t candidate = 0; candidate < offscreenTimestampReadbacks.size(); ++candidate) {
1882 const size_t index = (offscreenTimestampReadbackCursor + candidate) % offscreenTimestampReadbacks.size();
1883 if (!offscreenTimestampReadbacks[index].pending) {
1884 timestampSlot = &offscreenTimestampReadbacks[index];
1885 offscreenTimestampReadbackCursor = (index + 1) % offscreenTimestampReadbacks.size();
1886 break;
1887 }
1888 }
1889 }
1890
1891 if (!reflectionProbeFilterPipeline) {
1892 WGPUBindGroupLayoutEntry entries[3]{};
1893 entries[0].binding = 0;
1894 entries[0].visibility = WGPUShaderStage_Fragment;
1895 entries[0].texture.sampleType = WGPUTextureSampleType_Float;
1896 entries[0].texture.viewDimension = WGPUTextureViewDimension_Cube;
1897 entries[1].binding = 1;
1898 entries[1].visibility = WGPUShaderStage_Fragment;
1899 entries[1].sampler.type = WGPUSamplerBindingType_Filtering;
1900 entries[2].binding = 2;
1901 entries[2].visibility = WGPUShaderStage_Fragment;
1902 entries[2].buffer.type = WGPUBufferBindingType_Uniform;
1903 entries[2].buffer.hasDynamicOffset = true;
1904 entries[2].buffer.minBindingSize = 16;
1905 WGPUBindGroupLayoutDescriptor bgl{};
1906 bgl.label = sv("eve_reflection_probe_filter_bgl");
1907 bgl.entryCount = 3;
1908 bgl.entries = entries;
1909 reflectionProbeFilterSetLayout =
1910 device.CreateBindGroupLayout(reinterpret_cast<const wgpu::BindGroupLayoutDescriptor*>(&bgl));
1911 WGPUBindGroupLayout rawLayout = reflectionProbeFilterSetLayout.Get();
1912 WGPUPipelineLayoutDescriptor pld{};
1913 pld.label = sv("eve_reflection_probe_filter_layout");
1914 pld.bindGroupLayoutCount = 1;
1915 pld.bindGroupLayouts = &rawLayout;
1916 reflectionProbeFilterPipelineLayout =
1917 device.CreatePipelineLayout(reinterpret_cast<const wgpu::PipelineLayoutDescriptor*>(&pld));
1918
1919 wgpu::ShaderModule module = makeWgslModule(device, shaders::kReflectionProbeFilterWgsl);
1920 WGPUColorTargetState colorTarget{};
1921 colorTarget.format = WGPUTextureFormat_RGBA16Float;
1922 colorTarget.writeMask = WGPUColorWriteMask_All;
1923 WGPUFragmentState fragment{};
1924 fragment.module = module.Get();
1925 fragment.entryPoint = sv("fs_main");
1926 fragment.targetCount = 1;
1927 fragment.targets = &colorTarget;
1928 WGPURenderPipelineDescriptor pipeline{};
1929 pipeline.label = sv("eve_reflection_probe_filter");
1930 pipeline.layout = reflectionProbeFilterPipelineLayout.Get();
1931 pipeline.vertex.module = module.Get();
1932 pipeline.vertex.entryPoint = sv("vs_main");
1933 pipeline.fragment = &fragment;
1934 pipeline.primitive.topology = WGPUPrimitiveTopology_TriangleList;
1935 pipeline.primitive.frontFace = WGPUFrontFace_CCW;
1936 pipeline.primitive.cullMode = WGPUCullMode_None;
1937 pipeline.multisample.count = 1;
1938 pipeline.multisample.mask = 0xFFFFFFFFu;
1939 reflectionProbeFilterPipeline =
1940 device.CreateRenderPipeline(reinterpret_cast<const wgpu::RenderPipelineDescriptor*>(&pipeline));
1941 }
1942
1943 const uint32_t passCount = (target->mipLevels - 1u) * 6u;
1944 WGPUBufferDescriptor bufferDesc{};
1945 bufferDesc.label = sv("eve_reflection_probe_filter_params");
1946 bufferDesc.size = uint64_t(passCount) * 256u;
1947 bufferDesc.usage = WGPUBufferUsage_Uniform | WGPUBufferUsage_CopyDst;
1948 wgpu::Buffer paramsBuffer = device.CreateBuffer(reinterpret_cast<const wgpu::BufferDescriptor*>(&bufferDesc));
1949 WGPUTextureViewDescriptor sourceViewDesc{};
1950 sourceViewDesc.format = WGPUTextureFormat_RGBA16Float;
1951 sourceViewDesc.dimension = WGPUTextureViewDimension_Cube;
1952 sourceViewDesc.baseMipLevel = 0;
1953 sourceViewDesc.mipLevelCount = 1;
1954 sourceViewDesc.baseArrayLayer = 0;
1955 sourceViewDesc.arrayLayerCount = 6;
1956 wgpu::TextureView sourceView =
1957 target->texture.CreateView(reinterpret_cast<const wgpu::TextureViewDescriptor*>(&sourceViewDesc));
1958 WGPUBindGroupEntry groupEntries[3]{};
1959 groupEntries[0].binding = 0;
1960 groupEntries[0].textureView = sourceView.Get();
1961 groupEntries[1].binding = 1;
1962 groupEntries[1].sampler = target->sampler.Get();
1963 groupEntries[2].binding = 2;
1964 groupEntries[2].buffer = paramsBuffer.Get();
1965 groupEntries[2].size = 16;
1966 WGPUBindGroupDescriptor groupDesc{};
1967 groupDesc.label = sv("eve_reflection_probe_filter_group");
1968 groupDesc.layout = reflectionProbeFilterSetLayout.Get();
1969 groupDesc.entryCount = 3;
1970 groupDesc.entries = groupEntries;
1971 wgpu::BindGroup group = device.CreateBindGroup(reinterpret_cast<const wgpu::BindGroupDescriptor*>(&groupDesc));
1972
1973 wgpu::CommandEncoder encoder = device.CreateCommandEncoder();
1974 uint32_t passIndex = 0;
1975 for (uint32_t mip = 1; mip < target->mipLevels; ++mip) {
1976 const bool diffuse = mip + 1u == target->mipLevels;
1977 const float roughness = diffuse ? 1.f : float(mip) / float(target->mipLevels - 1u);
1978 for (uint32_t face = 0; face < 6; ++face, ++passIndex) {
1979 const float params[4] = {float(face), roughness, diffuse ? 1.f : 0.f, float(sampleCount)};
1980 queue.WriteBuffer(paramsBuffer, uint64_t(passIndex) * 256u, params, sizeof(params));
1981 WGPUTextureViewDescriptor viewDesc{};
1982 viewDesc.format = WGPUTextureFormat_RGBA16Float;
1983 viewDesc.dimension = WGPUTextureViewDimension_2D;
1984 viewDesc.baseMipLevel = mip;
1985 viewDesc.mipLevelCount = 1;
1986 viewDesc.baseArrayLayer = face;
1987 viewDesc.arrayLayerCount = 1;
1988 wgpu::TextureView faceView =
1989 target->texture.CreateView(reinterpret_cast<const wgpu::TextureViewDescriptor*>(&viewDesc));
1990 WGPURenderPassColorAttachment attachment{};
1991 attachment.view = faceView.Get();
1992 attachment.loadOp = WGPULoadOp_Clear;
1993 attachment.storeOp = WGPUStoreOp_Store;
1994 WGPURenderPassDescriptor passDesc{};
1995 passDesc.colorAttachmentCount = 1;
1996 passDesc.colorAttachments = &attachment;
1997 WGPUPassTimestampWrites timestampWrites{};
1998 if (timestampSlot) {
1999 timestampWrites.querySet = offscreenTimestampQuerySet.Get();
2000 timestampWrites.beginningOfPassWriteIndex = passIndex == 0 ? 0 : UINT32_MAX;
2001 timestampWrites.endOfPassWriteIndex = passIndex + 1u == passCount ? 1 : UINT32_MAX;
2002 passDesc.timestampWrites = &timestampWrites;
2003 }
2004 wgpu::RenderPassEncoder pass =
2005 encoder.BeginRenderPass(reinterpret_cast<const wgpu::RenderPassDescriptor*>(&passDesc));
2006 pass.SetPipeline(reflectionProbeFilterPipeline);
2007 const uint32_t offset = passIndex * 256u;
2008 pass.SetBindGroup(0, group, 1, &offset);
2009 pass.Draw(3);
2010 pass.End();
2011 }
2012 }
2013 if (timestampSlot) {
2014 encoder.ResolveQuerySet(offscreenTimestampQuerySet, 0, 2, offscreenTimestampResolveBuffer, 0);
2015 encoder.CopyBufferToBuffer(offscreenTimestampResolveBuffer, 0, timestampSlot->buffer, 0, sizeof(uint64_t) * 2);
2016 }
2017 wgpu::CommandBuffer command = encoder.Finish();
2018 queue.Submit(1, &command);
2019 if (timestampSlot) {
2020 timestampSlot->pending = true;
2021 WGPUBufferMapCallbackInfo callback{};
2022 callback.mode = WGPUCallbackMode_AllowProcessEvents;
2023 callback.callback = [](WGPUMapAsyncStatus status, WGPUStringView, void* userdata1, void*) {
2024 auto* slot = static_cast<OffscreenTimestampReadback*>(userdata1);
2025 if (status == WGPUMapAsyncStatus_Success && slot && slot->owner) {
2026 const auto* ticks =
2027 static_cast<const uint64_t*>(slot->buffer.GetConstMappedRange(0, sizeof(uint64_t) * 2));
2028 if (ticks && ticks[1] >= ticks[0]) {
2029 slot->owner->completedOffscreenTimestampMs.fetch_add(float(ticks[1] - ticks[0]) * 1.0e-6f);
2030 }
2031 slot->buffer.Unmap();
2032 }
2033 if (slot) slot->pending = false;
2034 };
2035 callback.userdata1 = timestampSlot;
2036 wgpuBufferMapAsync(timestampSlot->buffer.Get(), WGPUMapMode_Read, 0, sizeof(uint64_t) * 2, callback);
2037 }
2038 return true;
2039}
2040
2042 if (!texture) return;
2043 auto* gpu = gpuForTexture(texture);
2044 if (!gpu) return;
2045 gpu->samplerState = sampler;
2046 gpu->sampler = makeSampler(sampler, gpu->mipLevels);
2047 texture->sampler = sampler;
2048}
2049
2050float Graphics::getMaxAnisotropy() const { return maxSamplerAnisotropy; }
2051
2052Texture* Graphics::newTextureFromFile(const std::string& filename) {
2053 if (filename.empty()) throw Exception("newTextureFromFile: empty filename");
2054 if (!fileTextureSourceExists(filename)) throw Exception("Could not load image file: %s", filename.c_str());
2055
2056 auto it = texturesByPath.find(filename);
2057 if (it != texturesByPath.end() && it->second) {
2058 requestFileImageDecode(filename);
2059 if (it->second->hasDeferredFilePixels()) return it->second;
2060 auto waited = eve::ResourceManager::getInstance().waitFor(filename);
2061 if (!waited.ok()) throw Exception("%s", waited.status().describe().c_str());
2062 auto* data = dynamic_cast<image::ImageData*>(&waited.value().get());
2063 if (!data || !updateTexture(it->second, data->getWidth(), data->getHeight(),
2064 static_cast<const uint8_t*>(data->getData())))
2065 throw Exception("newTextureFromFile: reload failed '%s'", filename.c_str());
2066 return it->second;
2067 }
2068
2069 requestFileImageDecode(filename);
2070 auto tex = std::make_unique<Texture>();
2071 tex->markDeferredFilePixels(this);
2072 Texture* raw = tex.get();
2073 ownedTextures.push_back(std::move(tex));
2074 texturesByPath[filename] = raw;
2075 deferredFileTextures_.push_back({filename, raw});
2076 return raw;
2077}
2078
2080 if (!texture || !data) return false;
2081 if (texture->gpuHandle)
2082 return updateTexture(texture, data->getWidth(), data->getHeight(),
2083 static_cast<const uint8_t*>(data->getData()));
2084 Texture* fresh = newTexture(data);
2085 if (!fresh || !fresh->gpuHandle) return false;
2086 texture->gpuHandle = fresh->gpuHandle;
2087 texture->width = fresh->width;
2088 texture->height = fresh->height;
2089 texture->pixelWidth = fresh->pixelWidth;
2090 texture->pixelHeight = fresh->pixelHeight;
2091 texture->mipmapCount = fresh->mipmapCount;
2092 texture->sampler = fresh->sampler;
2093 fresh->gpuHandle = nullptr;
2094 auto texIt = std::find_if(ownedTextures.begin(), ownedTextures.end(),
2095 [&](const std::unique_ptr<Texture>& t) { return t.get() == fresh; });
2096 if (texIt == ownedTextures.end()) return false;
2097 (void)texIt->release();
2098 ownedTextures.erase(texIt);
2099 delete fresh;
2100 return true;
2101}
2102
2103bool Graphics::reloadTextureFromFile(const std::string& filename) {
2104 auto it = texturesByPath.find(filename);
2105 if (it == texturesByPath.end()) return false;
2106
2108
2109 // The provider hands back a cache-owned ImageData; the pin keeps it alive until
2110 // the pixels have been copied out of it.
2111 image::ImageData* data = nullptr;
2112 eve::ResourcePin keepAlive;
2113 try {
2114 auto* imgMod = image::Image::create();
2115 data = imgMod->newImageDataFromFile(filename);
2116 if (data != nullptr) {
2118 if (!pinned.ok()) return false;
2119 keepAlive = std::move(pinned).takeValue();
2120 // The pin is the authority from here on; the borrowed pointer may have gone
2121 // stale before the pin was taken.
2122 data = static_cast<image::ImageData*>(keepAlive.get());
2123 }
2124 } catch (...) {
2125 return false;
2126 }
2127 if (!data) return false;
2128
2129 Texture* tex = it->second;
2130 return updateTexture(tex, data->getWidth(), data->getHeight(), static_cast<const uint8_t*>(data->getData()));
2131}
2132
2134 if (!texture) return false;
2135 dropDeferredFileTexture(texture);
2136 if (!texture->gpuHandle) {
2137 auto texIt = std::find_if(ownedTextures.begin(), ownedTextures.end(),
2138 [&](const std::unique_ptr<Texture>& t) { return t.get() == texture; });
2139 if (texIt == ownedTextures.end()) return false;
2140 for (auto it = texturesByPath.begin(); it != texturesByPath.end();) {
2141 if (it->second == texture)
2142 it = texturesByPath.erase(it);
2143 else
2144 ++it;
2145 }
2146 for (auto it = sharedTexturesByContent.begin(); it != sharedTexturesByContent.end();) {
2147 if (it->second == texture)
2148 it = sharedTexturesByContent.erase(it);
2149 else
2150 ++it;
2151 }
2152 (void)texIt->release();
2153 ownedTextures.erase(texIt);
2154 return true;
2155 }
2156 // Renderer-owned fallback textures must never be released by callers.
2157 if (texture->gpuHandle == whiteTexture || texture->gpuHandle == flatNormalTexture ||
2158 texture->gpuHandle == flatNormalTexture3D || texture->gpuHandle == defaultEnvCubemap)
2159 return false;
2160
2161 auto* gpu = static_cast<GpuTexture*>(texture->gpuHandle);
2162 auto gpuIt = std::find_if(ownedGpuTextures.begin(), ownedGpuTextures.end(),
2163 [&](const std::unique_ptr<GpuTexture>& g) { return g.get() == gpu; });
2164 if (gpuIt == ownedGpuTextures.end()) return false;
2165
2166 auto texIt = std::find_if(ownedTextures.begin(), ownedTextures.end(),
2167 [&](const std::unique_ptr<Texture>& t) { return t.get() == texture; });
2168 if (texIt == ownedTextures.end()) return false;
2169
2170 // Path-cached textures must leave the hot-reload cache once released.
2171 for (auto it = texturesByPath.begin(); it != texturesByPath.end();) {
2172 if (it->second == texture)
2173 it = texturesByPath.erase(it);
2174 else
2175 ++it;
2176 }
2177 for (auto it = sharedTexturesByContent.begin(); it != sharedTexturesByContent.end();) {
2178 if (it->second == texture)
2179 it = sharedTexturesByContent.erase(it);
2180 else
2181 ++it;
2182 }
2183
2184 texture->gpuHandle = nullptr;
2185 ownedGpuTextures.erase(gpuIt);
2186 // Transfer the CPU facade to the caller instead of destroying it.
2187 (void)texIt->release();
2188 ownedTextures.erase(texIt);
2189 return true;
2190}
2191
2192bool Graphics::updateTexture(Texture* texture, int width, int height, const uint8_t* rgba) {
2193 if (!texture || !rgba || width <= 0 || height <= 0) return false;
2194 auto* gpu = gpuForTexture(texture);
2195 if (!gpu || width != texture->width || height != texture->height) return false;
2196 const auto owned =
2197 std::find_if(ownedGpuTextures.begin(), ownedGpuTextures.end(),
2198 [gpu](const std::unique_ptr<GpuTexture>& candidate) { return candidate.get() == gpu; });
2199 if (owned == ownedGpuTextures.end() || gpu->isCube) return false;
2200 uploadTexturePixelsMips(gpu, rgba, width, height);
2201 return true;
2202}
2203
2205 std::span<const std::uint8_t> rgba, std::size_t bytesPerRow) {
2206 const TextureRegionUpload upload{x, y, width, height, rgba, bytesPerRow};
2207 return updateTextureRegions(texture, std::span<const TextureRegionUpload>(&upload, 1));
2208}
2209
2210eve::Result<void> Graphics::updateTextureRegions(Texture* texture, std::span<const TextureRegionUpload> regions) {
2211 auto* gpu = gpuForTexture(texture);
2212 if (!texture || !gpu || gpu->isCube)
2214 "texture is not an owned 2D texture",
2215 "graphics.updateTextureRegions.texture"));
2216 if (texture->mipmapCount != 1)
2218 "partial updates require a single-mip texture",
2219 "graphics.updateTextureRegions.mipmaps"));
2220
2221 for (const TextureRegionUpload& region : regions) {
2222 if (region.x < 0 || region.y < 0 || region.width <= 0 || region.height <= 0 ||
2223 region.x > texture->width - region.width || region.y > texture->height - region.height)
2225 "invalid texture region",
2226 "graphics.updateTextureRegions.region"));
2227 const std::size_t packedRow = std::size_t(region.width) * 4U;
2228 const std::size_t stride = region.bytesPerRow == 0 ? packedRow : region.bytesPerRow;
2229 const std::size_t requiredBytes = stride * std::size_t(region.height - 1) + packedRow;
2230 if (stride < packedRow || region.rgba.size() < requiredBytes)
2232 "source bytes do not cover the texture region",
2233 "graphics.updateTextureRegions.bytes"));
2234 }
2235
2236 for (const TextureRegionUpload& region : regions) {
2237 const std::size_t packedRow = std::size_t(region.width) * 4U;
2238 const std::size_t stride = region.bytesPerRow == 0 ? packedRow : region.bytesPerRow;
2239 const std::size_t requiredBytes = stride * std::size_t(region.height - 1) + packedRow;
2240 WGPUTexelCopyTextureInfo dst{};
2241 dst.texture = gpu->texture.Get();
2242 dst.mipLevel = 0;
2243 dst.origin = {std::uint32_t(region.x), std::uint32_t(region.y), 0};
2244 dst.aspect = WGPUTextureAspect_All;
2245 WGPUTexelCopyBufferLayout layout{};
2246 layout.offset = 0;
2247 layout.bytesPerRow = std::uint32_t(stride);
2248 layout.rowsPerImage = std::uint32_t(region.height);
2249 WGPUExtent3D extent{std::uint32_t(region.width), std::uint32_t(region.height), 1};
2250 queue.WriteTexture(reinterpret_cast<const wgpu::TexelCopyTextureInfo*>(&dst), region.rgba.data(), requiredBytes,
2251 reinterpret_cast<const wgpu::TexelCopyBufferLayout*>(&layout),
2252 reinterpret_cast<const wgpu::Extent3D*>(&extent));
2253 }
2255}
2256
2257GpuTexture* Graphics::gpuForTexture(Texture* t) const { return t ? static_cast<GpuTexture*>(t->gpuHandle) : nullptr; }
2258
2259GpuTexture* Graphics::gpuForTextureOrWhite(Texture* t) const {
2260 GpuTexture* g = gpuForTexture(t);
2261 return g ? g : whiteTexture;
2262}
2263
2264wgpu::BindGroup Graphics::makeTex2DBindGroup(GpuTexture* color, GpuTexture* depth, GpuTexture* motion,
2265 GpuTexture* extra, GpuTexture* specular) {
2266 GpuTexture* c = color ? color : whiteTexture;
2267 GpuTexture* d = depth ? depth : c;
2268 GpuTexture* m = motion ? motion : c;
2269 GpuTexture* e = extra ? extra : c;
2270 GpuTexture* s = specular ? specular : c;
2271 WGPUBindGroupEntry entries[11]{};
2272 entries[0].binding = 0;
2273 entries[0].textureView = c->view.Get();
2274 entries[1].binding = 1;
2275 entries[1].textureView = d->view.Get();
2276 entries[2].binding = 2;
2277 entries[2].sampler = c->sampler.Get();
2278 entries[3].binding = 3;
2279 entries[3].sampler = d->sampler.Get();
2280 entries[4].binding = 4;
2281 entries[4].buffer = currentUboArena().buffer.Get(); // replaced per-draw via dynamic offset
2282 entries[4].size = std::max<uint32_t>(Shader::kPushConstantBytes, uint32_t(sizeof(Lighting2DUBO)));
2283 entries[5].binding = 5;
2284 entries[5].textureView = m->view.Get();
2285 entries[6].binding = 6;
2286 entries[6].sampler = m->sampler.Get();
2287 entries[7].binding = 7;
2288 entries[7].textureView = e->view.Get();
2289 entries[8].binding = 8;
2290 entries[8].sampler = e->sampler.Get();
2291 entries[9].binding = 9;
2292 entries[9].textureView = s->view.Get();
2293 entries[10].binding = 10;
2294 entries[10].sampler = s->sampler.Get();
2295 WGPUBindGroupDescriptor desc{};
2296 desc.label = sv("eve_tex2d_group");
2297 desc.layout = tex2DSetLayout.Get();
2298 desc.entryCount = 11;
2299 desc.entries = entries;
2300 return device.CreateBindGroup(reinterpret_cast<const wgpu::BindGroupDescriptor*>(&desc));
2301}
2302
2303wgpu::BindGroup Graphics::makeMeshBindGroup(GpuTexture* albedo, GpuTexture* normal, GpuTexture* env, GpuTexture* height,
2304 GpuTexture* depth, GpuTexture* sceneColor, uint32_t frameUboOffset,
2305 uint32_t shadowUboOffset, uint32_t pushUboOffset,
2306 const wgpu::Buffer& skinBuffer) {
2307 GpuTexture* a = albedo ? albedo : whiteTexture;
2308 GpuTexture* n = normal ? normal : flatNormalTexture;
2309 GpuTexture* e = env ? env : defaultEnvCubemap;
2310 auto localProbe = [&](int index) -> GpuTexture* {
2311 if (index < mesh3dReflectionProbes.count) {
2312 GpuTexture* gpu = gpuForTexture(mesh3dReflectionProbes.probes[index].cubemap);
2313 if (gpu && gpu->isCube) return gpu;
2314 }
2315 return defaultEnvCubemap;
2316 };
2317 GpuTexture* probe0 = localProbe(0);
2318 GpuTexture* probe1 = localProbe(1);
2319 GpuTexture* h = height ? height : flatHeightTexture3D;
2320 GpuTexture* d = depth ? depth : flatDepthTexture3D;
2321 GpuTexture* c = sceneColor ? sceneColor : whiteTexture;
2322 GpuTexture* shadow = shadowDepthArray ? shadowDepthArray : defaultShadowTex;
2323 wgpu::TextureView aoView_ =
2324 aoReady ? aoView[(aoWriteIndex + 1) % 2] : (whiteTexture ? whiteTexture->view : wgpu::TextureView());
2325 wgpu::TextureView decalAlbedoView = gpuForTextureOrWhite(decalFlatAlbedo)->view;
2326 wgpu::TextureView decalNormalView = gpuForTextureOrWhite(decalFlatNormal)->view;
2327 wgpu::TextureView decalParamsView = gpuForTextureOrWhite(decalFlatParams)->view;
2328 if (decalReady && lastDecalSlot < decalSlots.size()) {
2329 decalAlbedoView = decalSlots[lastDecalSlot].albedoView;
2330 decalNormalView = decalSlots[lastDecalSlot].normalView;
2331 decalParamsView = decalSlots[lastDecalSlot].paramsView;
2332 }
2333
2334 MeshBindGroupKey key{reinterpret_cast<uintptr_t>(currentUboArena().buffer.Get()),
2335 reinterpret_cast<uintptr_t>(a->view.Get()),
2336 reinterpret_cast<uintptr_t>(n->view.Get()),
2337 reinterpret_cast<uintptr_t>(e->view.Get()),
2338 reinterpret_cast<uintptr_t>(h->view.Get()),
2339 reinterpret_cast<uintptr_t>(d->view.Get()),
2340 reinterpret_cast<uintptr_t>(shadow->view.Get()),
2341 reinterpret_cast<uintptr_t>(shadow->sampler.Get()),
2342 reinterpret_cast<uintptr_t>(aoView_.Get()),
2343 reinterpret_cast<uintptr_t>(decalAlbedoView.Get()),
2344 reinterpret_cast<uintptr_t>(decalNormalView.Get()),
2345 reinterpret_cast<uintptr_t>(decalParamsView.Get()),
2346 reinterpret_cast<uintptr_t>(probe0->view.Get()),
2347 reinterpret_cast<uintptr_t>(probe1->view.Get()),
2348 reinterpret_cast<uintptr_t>(c->view.Get()),
2349 reinterpret_cast<uintptr_t>(skinBuffer.Get())};
2350 auto cached = meshBindGroupCache_.find(key);
2351 if (cached != meshBindGroupCache_.end()) return cached->second;
2352 if (meshBindGroupCache_.size() >= kMaxMeshBindGroupCache) meshBindGroupCache_.clear();
2353
2354 WGPUBindGroupEntry entries[21]{};
2355 entries[0].binding = 0;
2356 entries[0].buffer = currentUboArena().buffer.Get();
2357 entries[0].size = sizeof(Mesh3DUBO);
2358 entries[1].binding = 1;
2359 entries[1].textureView = a->view.Get();
2360 entries[2].binding = 2;
2361 entries[2].textureView = n->view.Get();
2362 entries[3].binding = 3;
2363 entries[3].textureView = e->view.Get();
2364 entries[4].binding = 4;
2365 entries[4].buffer = currentUboArena().buffer.Get();
2366 entries[4].size = sizeof(ShadowUBO);
2367 entries[5].binding = 5;
2368 entries[5].textureView = shadow->view.Get();
2369 entries[6].binding = 6;
2370 entries[6].textureView = h->view.Get();
2371 entries[7].binding = 7;
2372 // The mesh shader currently has one material sampler binding. Follow the
2373 // albedo texture's public sampler state instead of silently substituting
2374 // the backend-global linear sampler (Vulkan also samples albedo with the
2375 // sampler owned by that texture).
2376 entries[7].sampler = a->sampler.Get();
2377 entries[8].binding = 8;
2378 entries[8].sampler = shadow->sampler.Get();
2379 entries[9].binding = 9;
2380 entries[9].textureView = d->view.Get();
2381 entries[10].binding = 10;
2382 entries[10].textureView = aoView_.Get();
2383 entries[11].binding = 11;
2384 entries[11].sampler = mainSampler.Get();
2385 entries[12].binding = 12;
2386 entries[12].textureView = decalAlbedoView.Get();
2387 entries[13].binding = 13;
2388 entries[13].textureView = decalNormalView.Get();
2389 entries[14].binding = 14;
2390 entries[14].textureView = decalParamsView.Get();
2391 entries[15].binding = 15;
2392 entries[15].buffer = currentUboArena().buffer.Get();
2394 entries[16].binding = 16;
2395 entries[16].textureView = probe0->view.Get();
2396 entries[17].binding = 17;
2397 entries[17].textureView = probe1->view.Get();
2398 entries[18].binding = 18;
2399 entries[18].textureView = c->view.Get();
2400 entries[19].binding = 19;
2401 entries[19].sampler = c->sampler.Get();
2402
2403 entries[20].binding = 21;
2404 entries[20].buffer = skinBuffer.Get();
2405 entries[20].size = skinBuffer.GetSize();
2406 (void)frameUboOffset;
2407 (void)shadowUboOffset;
2408 (void)pushUboOffset;
2409 WGPUBindGroupDescriptor desc{};
2410 desc.label = sv("eve_mesh_group");
2411 desc.layout = mesh3dSetLayout.Get();
2412 desc.entryCount = 21;
2413 desc.entries = entries;
2414 wgpu::BindGroup bg = device.CreateBindGroup(reinterpret_cast<const wgpu::BindGroupDescriptor*>(&desc));
2415 meshBindGroupCache_.emplace(key, bg);
2416 return bg;
2417}
2418
2419wgpu::BindGroup Graphics::makeMesh3DClusteredBindGroup(GpuTexture* albedo, GpuTexture* normal, GpuTexture* env,
2420 GpuTexture* height, GpuTexture* depth, wgpu::TextureView aoView,
2421 uint32_t frameUboOffset, uint32_t shadowUboOffset) {
2422 GpuTexture* a = albedo ? albedo : whiteTexture;
2423 GpuTexture* n = normal ? normal : flatNormalTexture3D;
2424 GpuTexture* e = env ? env : defaultEnvCubemap;
2425 auto localProbe = [&](int index) -> GpuTexture* {
2426 if (index < mesh3dReflectionProbes.count) {
2427 GpuTexture* gpu = gpuForTexture(mesh3dReflectionProbes.probes[index].cubemap);
2428 if (gpu && gpu->isCube) return gpu;
2429 }
2430 return defaultEnvCubemap;
2431 };
2432 GpuTexture* probe0 = localProbe(0);
2433 GpuTexture* probe1 = localProbe(1);
2434 GpuTexture* h = height ? height : flatHeightTexture3D;
2435 GpuTexture* d = depth ? depth : flatDepthTexture3D;
2436 GpuTexture* shadow = shadowDepthArray ? shadowDepthArray : defaultShadowTex;
2437 ClusteredStorage& st = clusteredStorage[currentFrameSlot()];
2438 wgpu::TextureView decalAlbedoView = gpuForTextureOrWhite(decalFlatAlbedo)->view;
2439 wgpu::TextureView decalNormalView = gpuForTextureOrWhite(decalFlatNormal)->view;
2440 wgpu::TextureView decalParamsView = gpuForTextureOrWhite(decalFlatParams)->view;
2441 if (decalReady && lastDecalSlot < decalSlots.size()) {
2442 decalAlbedoView = decalSlots[lastDecalSlot].albedoView;
2443 decalNormalView = decalSlots[lastDecalSlot].normalView;
2444 decalParamsView = decalSlots[lastDecalSlot].paramsView;
2445 }
2446
2447 WGPUBindGroupEntry entries[20]{};
2448 entries[0].binding = 0;
2449 entries[0].buffer = currentUboArena().buffer.Get();
2450 entries[0].size = sizeof(Mesh3DClusteredUBO);
2451 entries[1].binding = 1;
2452 entries[1].textureView = a->view.Get();
2453 entries[2].binding = 2;
2454 entries[2].textureView = n->view.Get();
2455 entries[3].binding = 3;
2456 entries[3].textureView = e->view.Get();
2457 entries[4].binding = 4;
2458 entries[4].buffer = currentUboArena().buffer.Get();
2459 entries[4].size = sizeof(ShadowUBO);
2460 entries[5].binding = 5;
2461 entries[5].textureView = shadow->view.Get();
2462 entries[6].binding = 6;
2463 entries[6].textureView = h->view.Get();
2464 entries[7].binding = 7;
2465 entries[7].sampler = a->sampler.Get();
2466 entries[8].binding = 8;
2467 entries[8].sampler = shadow->sampler.Get();
2468 entries[9].binding = 9;
2469 entries[9].textureView = d->view.Get();
2470 entries[10].binding = 10;
2471 entries[10].buffer = st.lights.Get();
2472 entries[10].size = st.lightsCap;
2473 entries[11].binding = 11;
2474 entries[11].buffer = st.table.Get();
2475 entries[11].size = st.tableCap;
2476 entries[12].binding = 12;
2477 entries[12].buffer = st.indices.Get();
2478 entries[12].size = st.indicesCap;
2479 entries[13].binding = 13;
2480 entries[13].textureView = aoView.Get();
2481 entries[14].binding = 14;
2482 entries[14].sampler = mainSampler.Get();
2483 entries[15].binding = 15;
2484 entries[15].textureView = decalAlbedoView.Get();
2485 entries[16].binding = 16;
2486 entries[16].textureView = decalNormalView.Get();
2487 entries[17].binding = 17;
2488 entries[17].textureView = decalParamsView.Get();
2489 entries[18].binding = 18;
2490 entries[18].textureView = probe0->view.Get();
2491 entries[19].binding = 19;
2492 entries[19].textureView = probe1->view.Get();
2493
2494 (void)frameUboOffset;
2495 (void)shadowUboOffset;
2496 WGPUBindGroupDescriptor desc{};
2497 desc.label = sv("eve_mesh3d_clustered_group");
2498 desc.layout = mesh3dClusteredSetLayout.Get();
2499 desc.entryCount = 20;
2500 desc.entries = entries;
2501 return device.CreateBindGroup(reinterpret_cast<const wgpu::BindGroupDescriptor*>(&desc));
2502}
2503
2504// ---------------------------------------------------------------------------
2505// Mesh creation
2506// ---------------------------------------------------------------------------
2507
2508Mesh* Graphics::newMeshFromArrays(const float* posXYZ, const float* nrmXYZ, const float* uvST, int vertexCount,
2509 const uint32_t* indices, int indexCount) {
2510 return newMeshFromArraysColored(posXYZ,nrmXYZ,uvST,nullptr,vertexCount,indices,indexCount);
2511}
2512
2513Mesh* Graphics::newMeshFromArraysColored(const float* posXYZ, const float* nrmXYZ, const float* uvST,
2514 const float* colorRGBA,int vertexCount,
2515 const uint32_t* indices, int indexCount) {
2516 if (vertexCount <= 0 || !posXYZ) throw Exception("newMeshFromArrays: invalid vertex data");
2517 if (indexCount < 0 || (indexCount > 0 && (!indices || indexCount % 3 != 0)))
2518 throw Exception("newMeshFromArrays: invalid index data");
2519 for (int i = 0; i < indexCount; ++i) {
2520 if (indices[i] >= uint32_t(vertexCount)) throw Exception("newMeshFromArrays: index out of range");
2521 }
2522
2523 std::vector<MeshVertex> verts(static_cast<size_t>(vertexCount));
2524 for (int i = 0; i < vertexCount; ++i) {
2525 auto& v = verts[static_cast<size_t>(i)];
2526 v.pos = {posXYZ[i * 3 + 0], posXYZ[i * 3 + 1], posXYZ[i * 3 + 2]};
2527 if (nrmXYZ) {
2528 v.normal = {nrmXYZ[i * 3 + 0], nrmXYZ[i * 3 + 1], nrmXYZ[i * 3 + 2]};
2529 } else {
2530 v.normal = {0.f, 0.f, 1.f};
2531 }
2532 if (uvST) {
2533 v.uv = {uvST[i * 2 + 0], uvST[i * 2 + 1]};
2534 } else {
2535 v.uv = {0.f, 0.f};
2536 }
2537 if (colorRGBA) v.color = {colorRGBA[i*4],colorRGBA[i*4+1],colorRGBA[i*4+2],colorRGBA[i*4+3]};
2538 }
2539
2540 auto gpu = std::make_unique<GpuMesh>();
2541 gpu->vertexCount = uint32_t(vertexCount);
2542 gpu->indexCount = indexCount > 0 ? uint32_t(indexCount) : 0;
2543 gpu->vertexStride = sizeof(MeshVertex);
2544 gpu->cpuVertices = verts;
2545
2546 WGPUBufferDescriptor vbd{};
2547 vbd.label = sv("eve_mesh_vb");
2548 vbd.size = verts.size() * sizeof(MeshVertex);
2549 vbd.usage = WGPUBufferUsage_CopyDst | WGPUBufferUsage_Vertex | WGPUBufferUsage_Storage;
2550 vbd.mappedAtCreation = false;
2551 gpu->vertexBuffer = device.CreateBuffer(reinterpret_cast<const wgpu::BufferDescriptor*>(&vbd));
2552 queue.WriteBuffer(gpu->vertexBuffer, 0, verts.data(), vbd.size);
2553 gpu->vertexCapacity = vbd.size;
2554
2555 if (indexCount > 0) {
2556 // 16-bit index format halves index memory for meshes with <= 65535 verts.
2557 if (vertexCount <= 65535) {
2558 std::vector<uint16_t> idx16;
2559 idx16.reserve(indexCount);
2560 for (int i = 0; i < indexCount; ++i) idx16.push_back(uint16_t(indices[i]));
2561 WGPUBufferDescriptor ibd{};
2562 ibd.label = sv("eve_mesh_ib");
2563 if (idx16.size() % 2 != 0) idx16.push_back(0); // WriteBuffer size must align to 4.
2564 ibd.size = uint64_t(idx16.size()) * sizeof(uint16_t);
2565 ibd.usage = WGPUBufferUsage_CopyDst | WGPUBufferUsage_Index | WGPUBufferUsage_Storage;
2566 ibd.mappedAtCreation = false;
2567 gpu->indexBuffer = device.CreateBuffer(reinterpret_cast<const wgpu::BufferDescriptor*>(&ibd));
2568 queue.WriteBuffer(gpu->indexBuffer, 0, idx16.data(), ibd.size);
2569 gpu->indexFormat = wgpu::IndexFormat::Uint16;
2570 gpu->indexCapacity = ibd.size;
2571 } else {
2572 WGPUBufferDescriptor ibd{};
2573 ibd.label = sv("eve_mesh_ib");
2574 ibd.size = uint64_t(indexCount) * sizeof(uint32_t);
2575 ibd.usage = WGPUBufferUsage_CopyDst | WGPUBufferUsage_Index | WGPUBufferUsage_Storage;
2576 ibd.mappedAtCreation = false;
2577 gpu->indexBuffer = device.CreateBuffer(reinterpret_cast<const wgpu::BufferDescriptor*>(&ibd));
2578 queue.WriteBuffer(gpu->indexBuffer, 0, indices, ibd.size);
2579 gpu->indexFormat = wgpu::IndexFormat::Uint32;
2580 gpu->indexCapacity = ibd.size;
2581 }
2582 }
2583
2584 auto* mesh = new Mesh();
2585 mesh->indexCount = indexCount;
2586 mesh->gpuVertexCount = vertexCount;
2587 mesh->gpuHandle = gpu.get();
2588 mesh->computeBounds(posXYZ, vertexCount);
2589 ownedGpuMeshes.push_back(std::move(gpu));
2590 ownedMeshes.push_back(std::unique_ptr<Mesh>(mesh));
2591 return mesh;
2592}
2593
2594std::optional<eve::graphics::MeshBackendDescriptor> Graphics::describeMesh(Mesh* mesh) const {
2595 if (!mesh || !mesh->gpuHandle) return std::nullopt;
2596 const auto* gpu = static_cast<const GpuMesh*>(mesh->gpuHandle);
2597 const auto owned =
2598 std::find_if(ownedGpuMeshes.begin(), ownedGpuMeshes.end(),
2599 [gpu](const std::unique_ptr<GpuMesh>& candidate) { return candidate.get() == gpu; });
2600 if (owned == ownedGpuMeshes.end()) return std::nullopt;
2601 return eve::graphics::MeshBackendDescriptor{gpu->vertexCount, gpu->indexCount, gpu->vertexStride,
2602 gpu->indexFormat == wgpu::IndexFormat::Uint16 ? 2u : 4u};
2603}
2604
2606 aiMatrix4x4 ident;
2607 return newMeshFromAssimp(mesh, ident);
2608}
2609
2610Mesh* Graphics::newMeshFromAssimp(const ::aiMesh& mesh, const aiMatrix4x4& worldTransform) {
2611 std::vector<float> pos, nrm, uv;
2612 std::vector<uint32_t> idx;
2613 pos.reserve(mesh.mNumVertices * 3);
2614 nrm.reserve(mesh.mNumVertices * 3);
2615 uv.reserve(mesh.mNumVertices * 2);
2616 idx.reserve(mesh.mNumFaces * 3);
2617
2618 for (unsigned i = 0; i < mesh.mNumVertices; ++i) {
2619 aiVector3D p = worldTransform * mesh.mVertices[i];
2620 pos.push_back(p.x);
2621 pos.push_back(p.y);
2622 pos.push_back(p.z);
2623 if (mesh.mNormals) {
2624 aiMatrix3x3 rot(worldTransform);
2625 aiVector3D n = rot * mesh.mNormals[i];
2626 n.Normalize();
2627 nrm.push_back(n.x);
2628 nrm.push_back(n.y);
2629 nrm.push_back(n.z);
2630 } else {
2631 nrm.push_back(0.f);
2632 nrm.push_back(0.f);
2633 nrm.push_back(1.f);
2634 }
2635 if (mesh.mTextureCoords[0]) {
2636 uv.push_back(mesh.mTextureCoords[0][i].x);
2637 uv.push_back(mesh.mTextureCoords[0][i].y);
2638 } else {
2639 uv.push_back(0.f);
2640 uv.push_back(0.f);
2641 }
2642 }
2643 for (unsigned f = 0; f < mesh.mNumFaces; ++f) {
2644 for (unsigned v = 0; v < mesh.mFaces[f].mNumIndices; ++v) idx.push_back(mesh.mFaces[f].mIndices[v]);
2645 }
2646 Mesh* m = newMeshFromArrays(pos.data(), nrm.data(), uv.data(), int(mesh.mNumVertices), idx.data(), int(idx.size()));
2647 if (m) m->captureImportedAttributes(mesh);
2648 if (m && mesh.mNumAnimMeshes > 0) {
2649 m->initMorphBase(int(mesh.mNumVertices), pos.data(), nrm.data(), uv.data());
2650 for (unsigned a = 0; a < mesh.mNumAnimMeshes; ++a) {
2651 const aiAnimMesh* am = mesh.mAnimMeshes[a];
2652 std::vector<float> absPos(am->mNumVertices * 3);
2653 for (unsigned i = 0; i < am->mNumVertices; ++i) {
2654 absPos[i * 3 + 0] = am->mVertices[i].x;
2655 absPos[i * 3 + 1] = am->mVertices[i].y;
2656 absPos[i * 3 + 2] = am->mVertices[i].z;
2657 }
2658 std::string name = am->mName.C_Str();
2659 if (name.empty()) name = "morph" + std::to_string(a);
2660 m->addMorphTargetAbsolute(name, absPos.data());
2661 }
2662 }
2663 return m;
2664}
2665
2667 if (!mesh || !mesh->hasMorphData() || !mesh->isMorphDirty()) return false;
2668 auto* gpu = static_cast<GpuMesh*>(mesh->gpuHandle);
2669 if (!gpu || !gpu->vertexBuffer) return false;
2670 std::vector<float> pos, nrm;
2671 mesh->computeMorphedPositions(pos, nrm);
2672 if (pos.empty()) return false;
2673 mesh->computeBounds(pos.data(), mesh->getVertexCount());
2674 auto& verts = gpu->cpuVertices;
2675 if (verts.size() != static_cast<size_t>(mesh->getVertexCount())) return false;
2676 const auto& uvs = mesh->baseUv();
2677 for (int i = 0; i < mesh->getVertexCount(); ++i) {
2678 auto& v = verts[static_cast<size_t>(i)];
2679 v.pos = {pos[i * 3 + 0], pos[i * 3 + 1], pos[i * 3 + 2]};
2680 if (nrm.size() >= size_t((i + 1) * 3)) {
2681 v.normal = {nrm[i * 3 + 0], nrm[i * 3 + 1], nrm[i * 3 + 2]};
2682 } else {
2683 v.normal = {0.f, 0.f, 1.f};
2684 }
2685 if (uvs.size() >= size_t((i + 1) * 2)) {
2686 v.uv = {uvs[i * 2 + 0], uvs[i * 2 + 1]};
2687 } else {
2688 v.uv = {0.f, 0.f};
2689 }
2690 }
2691 queue.WriteBuffer(gpu->vertexBuffer, 0, verts.data(), verts.size() * sizeof(MeshVertex));
2692 mesh->markMorphClean();
2693 return true;
2694}
2695
2696bool Graphics::updateMeshVertices(Mesh* mesh, const float* posXYZ, const float* nrmXYZ, const float* uvST,
2697 int vertexCount, const uint32_t* indices, int indexCount) {
2698 if (!mesh || !mesh->gpuHandle || !posXYZ || vertexCount <= 0 || indexCount < 0) return false;
2699 if (indexCount > 0 && (!indices || indexCount % 3 != 0)) return false;
2700 for (int i = 0; i < indexCount; ++i) {
2701 if (indices[i] >= uint32_t(vertexCount)) return false;
2702 }
2703 auto* gpu = static_cast<GpuMesh*>(mesh->gpuHandle);
2704 const auto owned =
2705 std::find_if(ownedGpuMeshes.begin(), ownedGpuMeshes.end(),
2706 [gpu](const std::unique_ptr<GpuMesh>& candidate) { return candidate.get() == gpu; });
2707 if (owned == ownedGpuMeshes.end()) return false;
2708
2709 std::vector<float> verts;
2710 verts.reserve(size_t(vertexCount) * 8u);
2711 for (int i = 0; i < vertexCount; ++i) {
2712 verts.insert(verts.end(), posXYZ + size_t(i) * 3u, posXYZ + size_t(i) * 3u + 3u);
2713 if (nrmXYZ)
2714 verts.insert(verts.end(), nrmXYZ + size_t(i) * 3u, nrmXYZ + size_t(i) * 3u + 3u);
2715 else
2716 verts.insert(verts.end(), {0.f, 0.f, 1.f});
2717 if (uvST)
2718 verts.insert(verts.end(), uvST + size_t(i) * 2u, uvST + size_t(i) * 2u + 2u);
2719 else
2720 verts.insert(verts.end(), {0.f, 0.f});
2721 }
2722
2723 const uint64_t vertexBytes = verts.size() * sizeof(float);
2724 if (vertexBytes > gpu->vertexCapacity) {
2725 WGPUBufferDescriptor desc{};
2726 desc.label = sv("eve_mesh_dynamic_vb");
2727 desc.size = vertexBytes;
2728 desc.usage = WGPUBufferUsage_CopyDst | WGPUBufferUsage_Vertex | WGPUBufferUsage_Storage;
2729 gpu->vertexBuffer = device.CreateBuffer(reinterpret_cast<const wgpu::BufferDescriptor*>(&desc));
2730 gpu->vertexCapacity = vertexBytes;
2731 }
2732 queue.WriteBuffer(gpu->vertexBuffer, 0, verts.data(), vertexBytes);
2733 gpu->vertexCount = uint32_t(vertexCount);
2734
2735 if (indexCount > 0) {
2736 const wgpu::IndexFormat format = vertexCount <= 65535 ? wgpu::IndexFormat::Uint16 : wgpu::IndexFormat::Uint32;
2737 std::vector<uint16_t> idx16;
2738 const void* indexData = indices;
2739 uint64_t indexBytes = uint64_t(indexCount) * sizeof(uint32_t);
2740 if (format == wgpu::IndexFormat::Uint16) {
2741 idx16.reserve(size_t(indexCount) + 1u);
2742 for (int i = 0; i < indexCount; ++i) idx16.push_back(uint16_t(indices[i]));
2743 if (idx16.size() % 2 != 0) idx16.push_back(0);
2744 indexData = idx16.data();
2745 indexBytes = idx16.size() * sizeof(uint16_t);
2746 }
2747 if (format != gpu->indexFormat || indexBytes > gpu->indexCapacity) {
2748 WGPUBufferDescriptor desc{};
2749 desc.label = sv("eve_mesh_dynamic_ib");
2750 desc.size = indexBytes;
2751 desc.usage = WGPUBufferUsage_CopyDst | WGPUBufferUsage_Index | WGPUBufferUsage_Storage;
2752 gpu->indexBuffer = device.CreateBuffer(reinterpret_cast<const wgpu::BufferDescriptor*>(&desc));
2753 gpu->indexCapacity = indexBytes;
2754 }
2755 queue.WriteBuffer(gpu->indexBuffer, 0, indexData, indexBytes);
2756 gpu->indexFormat = format;
2757 gpu->indexCount = uint32_t(indexCount);
2758 }
2759
2760 mesh->computeBounds(posXYZ, vertexCount);
2761 mesh->gpuVertexCount = int(gpu->vertexCount);
2762 mesh->indexCount = int(gpu->indexCount);
2763 return true;
2764}
2765
2766void fillMeshAttributes(WGPUVertexAttribute (&attrs)[6]) {
2767 attrs[0].format = WGPUVertexFormat_Float32x3;
2768 attrs[0].offset = 0;
2769 attrs[0].shaderLocation = 0;
2770 attrs[1].format = WGPUVertexFormat_Float32x3;
2771 attrs[1].offset = 12;
2772 attrs[1].shaderLocation = 1;
2773 attrs[2].format = WGPUVertexFormat_Float32x2;
2774 attrs[2].offset = 24;
2775 attrs[2].shaderLocation = 2;
2776 attrs[3].format = WGPUVertexFormat_Uint16x4;
2777 attrs[3].offset = 32;
2778 attrs[3].shaderLocation = 3;
2779 attrs[4].format = WGPUVertexFormat_Float32x4;
2780 attrs[4].offset = 40;
2781 attrs[4].shaderLocation = 4;
2782 attrs[5].format = WGPUVertexFormat_Float32x4;
2783 attrs[5].offset = 56;
2784 attrs[5].shaderLocation = 5;
2785}
2786
2787bool Graphics::setMeshSkinningData(Mesh* mesh, const uint16_t* joints4, const float* weights4, int vertexCount) {
2788 if (!mesh || !mesh->gpuHandle || !joints4 || !weights4) return false;
2789 auto* gpu = static_cast<GpuMesh*>(mesh->gpuHandle);
2790 if (vertexCount <= 0 || gpu->cpuVertices.size() != static_cast<size_t>(vertexCount)) return false;
2791 for (int i = 0; i < vertexCount; ++i) {
2792 const size_t base = static_cast<size_t>(i) * 4u;
2793 auto& v = gpu->cpuVertices[static_cast<size_t>(i)];
2794 v.joints = glm::u16vec4(joints4[base], joints4[base + 1], joints4[base + 2], joints4[base + 3]);
2795 v.weights = glm::vec4(weights4[base], weights4[base + 1], weights4[base + 2], weights4[base + 3]);
2796 }
2797 queue.WriteBuffer(gpu->vertexBuffer, 0, gpu->cpuVertices.data(), gpu->cpuVertices.size() * sizeof(MeshVertex));
2798 mesh->markGpuSkinned(true);
2799 return true;
2800}
2801
2803 if (!mesh || !mesh->gpuHandle) return false;
2804
2805 auto* gpu = static_cast<GpuMesh*>(mesh->gpuHandle);
2806 auto gpuIt = std::find_if(ownedGpuMeshes.begin(), ownedGpuMeshes.end(),
2807 [&](const std::unique_ptr<GpuMesh>& g) { return g.get() == gpu; });
2808 if (gpuIt == ownedGpuMeshes.end()) return false;
2809
2810 auto meshIt = std::find_if(ownedMeshes.begin(), ownedMeshes.end(),
2811 [&](const std::unique_ptr<Mesh>& m) { return m.get() == mesh; });
2812 if (meshIt == ownedMeshes.end()) return false;
2813
2814 mesh->gpuHandle = nullptr;
2815 ownedGpuMeshes.erase(gpuIt);
2816 // Transfer the CPU facade to the caller instead of destroying it.
2817 (void)meshIt->release();
2818 ownedMeshes.erase(meshIt);
2819 return true;
2820}
2821
2822Mesh* Graphics::newMeshSphere(int slices, int stacks) {
2823 std::vector<float> pos, nrm, uv;
2824 std::vector<uint32_t> idx;
2825 for (int y = 0; y <= stacks; ++y) {
2826 for (int x = 0; x <= slices; ++x) {
2827 float u = float(x) / float(slices);
2828 float v = float(y) / float(stacks);
2829 float theta = u * 2.f * glm::pi<float>();
2830 float phi = v * glm::pi<float>();
2831 float sx = std::sin(phi) * std::cos(theta);
2832 float sy = std::cos(phi);
2833 float sz = std::sin(phi) * std::sin(theta);
2834 pos.push_back(sx);
2835 pos.push_back(sy);
2836 pos.push_back(sz);
2837 nrm.push_back(sx);
2838 nrm.push_back(sy);
2839 nrm.push_back(sz);
2840 uv.push_back(u);
2841 uv.push_back(v);
2842 }
2843 }
2844 for (int y = 0; y < stacks; ++y) {
2845 for (int x = 0; x < slices; ++x) {
2846 int a = y * (slices + 1) + x;
2847 int b = a + slices + 1;
2848 idx.push_back(a);
2849 idx.push_back(a + 1);
2850 idx.push_back(b);
2851 idx.push_back(b);
2852 idx.push_back(a + 1);
2853 idx.push_back(b + 1);
2854 }
2855 }
2856 return newMeshFromArrays(pos.data(), nrm.data(), uv.data(), int(pos.size() / 3), idx.data(), int(idx.size()));
2857}
2858
2859Mesh* Graphics::newMeshCylinder(int slices, int stacks, bool caps) {
2860 std::vector<float> pos, nrm, uv;
2861 std::vector<uint32_t> idx;
2862 for (int y = 0; y <= stacks; ++y) {
2863 for (int x = 0; x <= slices; ++x) {
2864 float u = float(x) / float(slices);
2865 float v = float(y) / float(stacks);
2866 float theta = u * 2.f * glm::pi<float>();
2867 pos.push_back(std::cos(theta));
2868 pos.push_back(v * 2.f - 1.f);
2869 pos.push_back(std::sin(theta));
2870 nrm.push_back(std::cos(theta));
2871 nrm.push_back(0.f);
2872 nrm.push_back(std::sin(theta));
2873 uv.push_back(u);
2874 uv.push_back(v);
2875 }
2876 }
2877 for (int y = 0; y < stacks; ++y) {
2878 for (int x = 0; x < slices; ++x) {
2879 int a = y * (slices + 1) + x;
2880 int b = a + slices + 1;
2881 idx.push_back(a);
2882 idx.push_back(b);
2883 idx.push_back(a + 1);
2884 idx.push_back(b);
2885 idx.push_back(b + 1);
2886 idx.push_back(a + 1);
2887 }
2888 }
2889 if (caps) {
2890 int base = int(pos.size() / 3);
2891 pos.push_back(0.f);
2892 pos.push_back(-1.f);
2893 pos.push_back(0.f);
2894 nrm.push_back(0.f);
2895 nrm.push_back(-1.f);
2896 nrm.push_back(0.f);
2897 uv.push_back(0.5f);
2898 uv.push_back(0.5f);
2899 for (int x = 0; x < slices; ++x) {
2900 float theta0 = float(x) / float(slices) * 2.f * glm::pi<float>();
2901 float theta1 = float(x + 1) / float(slices) * 2.f * glm::pi<float>();
2902 int i0 = int(pos.size() / 3);
2903 pos.push_back(std::cos(theta0));
2904 pos.push_back(-1.f);
2905 pos.push_back(std::sin(theta0));
2906 nrm.push_back(0.f);
2907 nrm.push_back(-1.f);
2908 nrm.push_back(0.f);
2909 uv.push_back(0.5f + 0.5f * std::cos(theta0));
2910 uv.push_back(0.5f + 0.5f * std::sin(theta0));
2911 int i1 = int(pos.size() / 3);
2912 pos.push_back(std::cos(theta1));
2913 pos.push_back(-1.f);
2914 pos.push_back(std::sin(theta1));
2915 nrm.push_back(0.f);
2916 nrm.push_back(-1.f);
2917 nrm.push_back(0.f);
2918 uv.push_back(0.5f + 0.5f * std::cos(theta1));
2919 uv.push_back(0.5f + 0.5f * std::sin(theta1));
2920 idx.push_back(base);
2921 idx.push_back(i0);
2922 idx.push_back(i1);
2923 }
2924 base = int(pos.size() / 3);
2925 pos.push_back(0.f);
2926 pos.push_back(1.f);
2927 pos.push_back(0.f);
2928 nrm.push_back(0.f);
2929 nrm.push_back(1.f);
2930 nrm.push_back(0.f);
2931 uv.push_back(0.5f);
2932 uv.push_back(0.5f);
2933 for (int x = 0; x < slices; ++x) {
2934 float theta0 = float(x) / float(slices) * 2.f * glm::pi<float>();
2935 float theta1 = float(x + 1) / float(slices) * 2.f * glm::pi<float>();
2936 int i0 = int(pos.size() / 3);
2937 pos.push_back(std::cos(theta0));
2938 pos.push_back(1.f);
2939 pos.push_back(std::sin(theta0));
2940 nrm.push_back(0.f);
2941 nrm.push_back(1.f);
2942 nrm.push_back(0.f);
2943 uv.push_back(0.5f + 0.5f * std::cos(theta0));
2944 uv.push_back(0.5f + 0.5f * std::sin(theta0));
2945 int i1 = int(pos.size() / 3);
2946 pos.push_back(std::cos(theta1));
2947 pos.push_back(1.f);
2948 pos.push_back(std::sin(theta1));
2949 nrm.push_back(0.f);
2950 nrm.push_back(1.f);
2951 nrm.push_back(0.f);
2952 uv.push_back(0.5f + 0.5f * std::cos(theta1));
2953 uv.push_back(0.5f + 0.5f * std::sin(theta1));
2954 idx.push_back(base);
2955 idx.push_back(i1);
2956 idx.push_back(i0);
2957 }
2958 }
2959 return newMeshFromArrays(pos.data(), nrm.data(), uv.data(), int(pos.size() / 3), idx.data(), int(idx.size()));
2960}
2961
2962// ---------------------------------------------------------------------------
2963// 2D drawing
2964// ---------------------------------------------------------------------------
2965
2966void Graphics::clear2DBatches() {
2967 solidBatches.clear();
2968 texturedBatches.clear();
2969 litBatches.clear();
2970 overlaySpans.clear();
2971 sceneColorComposited = false;
2972}
2973
2974void Graphics::noteSolidOverlay(uint32_t idx) {
2975 if (idx >= solidBatches.size()) return;
2976 const uint32_t n = uint32_t(solidBatches[idx].batch.vertices().size());
2977 if (!overlaySpans.empty() && overlaySpans.back().kind == OverlayKind::Solid && overlaySpans.back().index == idx) {
2978 overlaySpans.back().vertCount = n - overlaySpans.back().vertBegin;
2979 return;
2980 }
2981 const uint32_t begin = n >= 6u ? n - 6u : 0u;
2982 overlaySpans.push_back({OverlayKind::Solid, idx, begin, n - begin});
2983}
2984
2985void Graphics::noteTexturedOverlay(Texture* tex, uint32_t idx) {
2986 if (tex && tex == getSceneColorTexture()) sceneColorComposited = true;
2987 if (!overlaySpans.empty() && overlaySpans.back().kind == OverlayKind::Textured && overlaySpans.back().index == idx)
2988 return;
2989 overlaySpans.push_back({OverlayKind::Textured, idx, 0, 0});
2990}
2991
2992void Graphics::noteLitOverlay(uint32_t idx) {
2993 if (!overlaySpans.empty() && overlaySpans.back().kind == OverlayKind::Lit && overlaySpans.back().index == idx)
2994 return;
2995 overlaySpans.push_back({OverlayKind::Lit, idx, 0, 0});
2996}
2997
2998void Graphics::drawSolidRect(float x, float y, float w, float h, const Color& color, BlendMode blend) {
2999 auto it =
3000 std::find_if(solidBatches.begin(), solidBatches.end(), [&](const SolidBatch& sb) { return sb.blend == blend; });
3001 if (it == solidBatches.end()) {
3002 solidBatches.push_back(SolidBatch{blend, Batcher{}});
3003 it = solidBatches.end() - 1;
3004 }
3005 it->batch.addRect(x, y, w, h, color);
3006 noteSolidOverlay(uint32_t(it - solidBatches.begin()));
3007}
3008
3010 const int targetWidth = activeCanvas ? activeCanvas->getWidth() : getWidth();
3011 const int targetHeight = activeCanvas ? activeCanvas->getHeight() : getHeight();
3012 const auto triangles = resolvePrimitiveStrokes2D(canvas, {targetWidth, targetHeight});
3013 if (triangles.vertices.empty()) return;
3014 auto logicalPoint = [targetWidth, targetHeight](const PrimitiveTriangleVertex& vertex) {
3015 const glm::vec2 ndc = glm::vec2(vertex.clipPosition) / vertex.clipPosition.w;
3016 return glm::vec2((ndc.x + 1.f) * 0.5f * static_cast<float>(targetWidth),
3017 (ndc.y + 1.f) * 0.5f * static_cast<float>(targetHeight));
3018 };
3019 for (const ResolvedPrimitiveBatch2D& resolvedBatch : triangles.batches2D) {
3020 auto it = std::find_if(solidBatches.begin(), solidBatches.end(),
3021 [&](const SolidBatch& batch) { return batch.blend == resolvedBatch.blend; });
3022 if (it == solidBatches.end()) {
3023 solidBatches.push_back(SolidBatch{resolvedBatch.blend, Batcher{}});
3024 it = solidBatches.end() - 1;
3025 }
3026 for (std::size_t i = resolvedBatch.firstVertex; i < resolvedBatch.firstVertex + resolvedBatch.vertexCount;
3027 i += 3) {
3028 it->batch.addTriangle(logicalPoint(triangles.vertices[i]), logicalPoint(triangles.vertices[i + 1]),
3029 logicalPoint(triangles.vertices[i + 2]), triangles.vertices[i].color,
3030 triangles.vertices[i + 1].color, triangles.vertices[i + 2].color);
3031 }
3032 const auto batchIndex = static_cast<std::uint32_t>(it - solidBatches.begin());
3033 const std::uint32_t count = static_cast<std::uint32_t>(resolvedBatch.vertexCount);
3034 const std::uint32_t end = static_cast<std::uint32_t>(it->batch.vertices().size());
3035 overlaySpans.push_back({OverlayKind::Solid, batchIndex, end - count, count});
3036 }
3037}
3038
3039void Graphics::drawSolidRectRotated(float cx, float cy, float w, float h, float degrees, const Color& color,
3040 BlendMode blend) {
3041 auto it =
3042 std::find_if(solidBatches.begin(), solidBatches.end(), [&](const SolidBatch& sb) { return sb.blend == blend; });
3043 if (it == solidBatches.end()) {
3044 solidBatches.push_back(SolidBatch{blend, Batcher{}});
3045 it = solidBatches.end() - 1;
3046 }
3047 it->batch.addRectRotated(cx, cy, w, h, degrees, color);
3048 noteSolidOverlay(uint32_t(it - solidBatches.begin()));
3049}
3050
3051void Graphics::drawTexturedRect(Texture* texture, float x, float y, float w, float h, const Color& color) {
3052 drawTexturedRectUV(texture, x, y, w, h, 0.f, 0.f, 1.f, 1.f, color);
3053}
3054
3055void Graphics::drawTexturedRectShader(Texture* texture, Shader* shader, float x, float y, float w, float h,
3056 const Color& color) {
3057 drawTexturedRectShaderUV(texture, shader, x, y, w, h, 0.f, 0.f, 1.f, 1.f, color);
3058}
3059
3060void Graphics::drawTexturedRectUV(Texture* texture, float x, float y, float w, float h, float u0, float v0, float u1,
3061 float v1, const Color& color) {
3062 drawTexturedRectShaderUV(texture, currentShader, x, y, w, h, u0, v0, u1, v1, color);
3063}
3064
3065void Graphics::drawTexturedRectShaderUV(Texture* texture, Shader* shader, float x, float y, float w, float h, float u0,
3066 float v0, float u1, float v1, const Color& color, bool rotatedUV,
3067 BlendMode blend) {
3068 if (!texture) {
3069 drawSolidRect(x, y, w, h, color, blend);
3070 return;
3071 }
3072 if (texturedBatches.empty() || texturedBatches.back().texture != texture ||
3073 texturedBatches.back().shader != shader || texturedBatches.back().depth != nullptr ||
3074 texturedBatches.back().blend != blend) {
3075 texturedBatches.push_back(TexturedBatch{texture, nullptr, shader, blend, Batcher{}});
3076 }
3077 texturedBatches.back().batch.addTexturedRect(x, y, w, h, color, u0, v0, u1, v1, rotatedUV);
3078 noteTexturedOverlay(texture, uint32_t(texturedBatches.size() - 1));
3079}
3080
3081void Graphics::drawTexturedRectShaderUVRotated(Texture* texture, Shader* shader, float cx, float cy, float w, float h,
3082 float degrees, float u0, float v0, float u1, float v1,
3083 const Color& color, bool rotatedUV, BlendMode blend) {
3084 if (!texture) {
3085 drawSolidRect(cx - w * 0.5f, cy - h * 0.5f, w, h, color, blend);
3086 return;
3087 }
3088 if (texturedBatches.empty() || texturedBatches.back().texture != texture ||
3089 texturedBatches.back().shader != shader || texturedBatches.back().depth != nullptr ||
3090 texturedBatches.back().blend != blend) {
3091 texturedBatches.push_back(TexturedBatch{texture, nullptr, shader, blend, Batcher{}});
3092 }
3093 texturedBatches.back().batch.addTexturedRectRotated(cx, cy, w, h, degrees, color, u0, v0, u1, v1, rotatedUV);
3094 noteTexturedOverlay(texture, uint32_t(texturedBatches.size() - 1));
3095}
3096
3098 float h, const Color& tint) {
3099 if (!color) {
3100 drawSolidRect(x, y, w, h, tint);
3101 return;
3102 }
3103 if (texturedBatches.empty() || texturedBatches.back().texture != color || texturedBatches.back().shader != shader ||
3104 texturedBatches.back().depth != depth || texturedBatches.back().blend != BlendMode::Alpha) {
3105 texturedBatches.push_back(TexturedBatch{color, depth, shader, BlendMode::Alpha, Batcher{}});
3106 }
3107 texturedBatches.back().batch.addTexturedRect(x, y, w, h, tint, 0.f, 0.f, 1.f, 1.f, false);
3108 noteTexturedOverlay(color, uint32_t(texturedBatches.size() - 1));
3109}
3110
3112 float x, float y, float w, float h, const Color& tint) {
3113 if (!color) {
3114 drawSolidRect(x, y, w, h, tint);
3115 return;
3116 }
3117 if (texturedBatches.empty() || texturedBatches.back().texture != color || texturedBatches.back().shader != shader ||
3118 texturedBatches.back().depth != depth || texturedBatches.back().motion != motion ||
3119 texturedBatches.back().blend != BlendMode::Opaque) {
3121 batch.motion = motion;
3122 texturedBatches.push_back(std::move(batch));
3123 }
3124 texturedBatches.back().batch.addTexturedRect(x, y, w, h, tint, 0.f, 0.f, 1.f, 1.f, false);
3125 noteTexturedOverlay(color, uint32_t(texturedBatches.size() - 1));
3126}
3127
3129 float x, float y, float w, float h, const Color& tint) {
3130 if (!color) {
3131 drawSolidRect(x, y, w, h, tint);
3132 return;
3133 }
3134 if (texturedBatches.empty() || texturedBatches.back().texture != color || texturedBatches.back().shader != shader ||
3135 texturedBatches.back().depth != depth || texturedBatches.back().motion != motion ||
3136 texturedBatches.back().extra != extra || texturedBatches.back().blend != BlendMode::Opaque) {
3138 batch.motion = motion;
3139 batch.extra = extra;
3140 texturedBatches.push_back(std::move(batch));
3141 }
3142 texturedBatches.back().batch.addTexturedRect(x, y, w, h, tint, 0.f, 0.f, 1.f, 1.f, false);
3143 noteTexturedOverlay(color, uint32_t(texturedBatches.size() - 1));
3144}
3145
3147 Texture* specular, Shader* shader, float x, float y, float w, float h,
3148 const Color& tint) {
3149 if (!color) {
3150 drawSolidRect(x, y, w, h, tint);
3151 return;
3152 }
3153 if (texturedBatches.empty() || texturedBatches.back().texture != color || texturedBatches.back().shader != shader ||
3154 texturedBatches.back().depth != depth || texturedBatches.back().motion != motion ||
3155 texturedBatches.back().extra != extra || texturedBatches.back().specular != specular ||
3156 texturedBatches.back().blend != BlendMode::Opaque) {
3158 batch.motion = motion;
3159 batch.extra = extra;
3160 batch.specular = specular;
3161 texturedBatches.push_back(std::move(batch));
3162 }
3163 texturedBatches.back().batch.addTexturedRect(x, y, w, h, tint, 0.f, 0.f, 1.f, 1.f, false);
3164 noteTexturedOverlay(color, uint32_t(texturedBatches.size() - 1));
3165}
3166
3167void Graphics::drawTexturedRectLitUV(Texture* albedo, Texture* normal, float x, float y, float w, float h, float u0,
3168 float v0, float u1, float v1, const Color& color, BlendMode blend) {
3169 if (!albedo) {
3170 drawSolidRect(x, y, w, h, color, blend);
3171 return;
3172 }
3173 if (litBatches.empty() || litBatches.back().albedo != albedo || litBatches.back().normal != normal ||
3174 litBatches.back().blend != blend) {
3175 litBatches.push_back(LitBatch{albedo, normal, blend, Batcher{}});
3176 }
3177 litBatches.back().batch.addTexturedRect(x, y, w, h, color, u0, v0, u1, v1, false);
3178 noteLitOverlay(uint32_t(litBatches.size() - 1));
3179}
3180
3182 float degrees, float u0, float v0, float u1, float v1, const Color& color,
3183 BlendMode blend) {
3184 if (!albedo) {
3186 return;
3187 }
3188 if (litBatches.empty() || litBatches.back().albedo != albedo || litBatches.back().normal != normal ||
3189 litBatches.back().blend != blend) {
3190 litBatches.push_back(LitBatch{albedo, normal, blend, Batcher{}});
3191 }
3192 litBatches.back().batch.addTexturedRectRotated(cx, cy, w, h, degrees, color, u0, v0, u1, v1, false);
3193 noteLitOverlay(uint32_t(litBatches.size() - 1));
3194}
3195
3197 // Uploaded on demand before flushing lit batches.
3198 lighting2dFrame = ubo;
3199}
3200
3201void Graphics::flush2D(wgpu::RenderPassEncoder pass, int viewW, int viewH, WGPUTextureFormat format) {
3203 auto spans = std::move(overlaySpans);
3204 const bool offscreen = uint32_t(format) != uint32_t(surfaceFormat);
3205
3206 struct SolidUpload {
3207 BlendMode blend = BlendMode::Alpha;
3208 uint64_t offset = 0;
3209 uint64_t bytes = 0;
3210 };
3211 std::vector<SolidUpload> solidUploads;
3212 solidUploads.reserve(solidBatches.size());
3213 for (const auto& sb : solidBatches) {
3214 if (sb.batch.empty()) {
3215 solidUploads.push_back(SolidUpload{sb.blend, 0, 0});
3216 continue;
3217 }
3218 Batcher ndc = sb.batch;
3219 ndc.toNDC(viewW, viewH);
3220 auto verts = ndc.vertices();
3221 // Batcher::toNDC targets Vulkan's Y-down NDC ((-1,-1)=top-left), but
3222 // WebGPU's clip space is Y-up ((-1,-1)=bottom-left). Flip Y so logical
3223 // top-left maps to the swapchain's top-left.
3224 for (auto& v : verts) v.pos.y = -v.pos.y;
3225 std::vector<float> data;
3226 data.reserve(verts.size() * 6);
3227 for (const auto& v : verts) {
3228 data.push_back(v.pos.x);
3229 data.push_back(v.pos.y);
3230 data.push_back(v.color.r);
3231 data.push_back(v.color.g);
3232 data.push_back(v.color.b);
3233 data.push_back(v.color.a);
3234 }
3235 const uint64_t bytes = data.size() * sizeof(float);
3236 auto& arena = currentVertexArena();
3237 ensureVertexArena(arena, arena.used + bytes);
3238 const uint64_t offset = arena.alloc(bytes);
3239 queue.WriteBuffer(arena.buffer, offset, data.data(), bytes);
3240 solidUploads.push_back(SolidUpload{sb.blend, offset, bytes});
3241 }
3242
3243 auto solidPipe = [&](BlendMode mode) -> wgpu::RenderPipeline { return get2DColorPipeline(mode, offscreen); };
3244
3245 auto drawSolid = [&](uint32_t batchIndex, uint32_t first, uint32_t count) {
3246 if (batchIndex >= solidUploads.size()) return;
3247 const SolidUpload& u = solidUploads[batchIndex];
3248 if (u.bytes == 0 || count == 0) return;
3249 pass.SetPipeline(solidPipe(u.blend));
3250 pass.SetVertexBuffer(0, currentVertexArena().buffer, u.offset, u.bytes);
3251 pass.Draw(count, 1, first, 0);
3252 };
3253
3254 if (!spans.empty()) {
3255 for (const auto& sp : spans) {
3256 if (sp.kind == OverlayKind::Solid)
3257 drawSolid(sp.index, sp.vertBegin, sp.vertCount);
3258 else if (sp.kind == OverlayKind::Textured && sp.index < texturedBatches.size())
3259 drawTexturedBatch(pass, texturedBatches[sp.index], viewW, viewH, format, offscreen);
3260 else if (sp.kind == OverlayKind::Lit && sp.index < litBatches.size())
3261 drawLitBatch(pass, litBatches[sp.index], viewW, viewH, format);
3262 }
3263 } else {
3264 for (size_t i = 0; i < solidUploads.size(); ++i) {
3265 if (solidUploads[i].bytes > 0) drawSolid(uint32_t(i), 0, uint32_t(solidBatches[i].batch.vertices().size()));
3266 }
3267 for (auto& tb : texturedBatches) {
3268 if (tb.batch.empty()) continue;
3269 drawTexturedBatch(pass, tb, viewW, viewH, format, offscreen);
3270 }
3271 for (auto& lb : litBatches) {
3272 if (lb.batch.empty()) continue;
3273 drawLitBatch(pass, lb, viewW, viewH, format);
3274 }
3275 }
3276 clear2DBatches();
3277}
3278
3279void Graphics::drawTexturedBatch(wgpu::RenderPassEncoder pass, TexturedBatch& tb, int viewW, int viewH,
3280 WGPUTextureFormat format, bool offscreen) {
3281 Batcher ndc = tb.batch;
3282 ndc.toNDC(viewW, viewH);
3283 auto verts = ndc.vertices();
3284 for (auto& v : verts) v.pos.y = -v.pos.y;
3285 if (verts.empty()) return;
3286
3287 std::vector<float> data;
3288 data.reserve(verts.size() * 8);
3289 for (const auto& v : verts) {
3290 data.push_back(v.pos.x);
3291 data.push_back(v.pos.y);
3292 data.push_back(v.color.r);
3293 data.push_back(v.color.g);
3294 data.push_back(v.color.b);
3295 data.push_back(v.color.a);
3296 data.push_back(v.uv.x);
3297 data.push_back(v.uv.y);
3298 }
3299 uint64_t bytes = data.size() * sizeof(float);
3300 auto& arena = currentVertexArena();
3301 ensureVertexArena(arena, arena.used + bytes);
3302 uint64_t vtxOffset = arena.alloc(bytes);
3303 queue.WriteBuffer(arena.buffer, vtxOffset, data.data(), bytes);
3304
3305 GpuTexture* gpu = gpuForTexture(tb.texture);
3306 GpuTexture* depthGpu = gpuForTexture(tb.depth);
3307 GpuTexture* motionGpu = gpuForTexture(tb.motion);
3308 GpuTexture* extraGpu = gpuForTexture(tb.extra);
3309 GpuTexture* specularGpu = gpuForTexture(tb.specular);
3310 auto& uboArena = currentUboArena();
3311 ensureUboArena(uboArena, uboArena.used + 256);
3312
3313 // Push-constant (Externals) block for custom shaders.
3314 uint32_t pushOffset = 0;
3315 if (tb.shader && tb.shader->pushConstantSize() > 0) {
3316 pushOffset = uboArena.alloc(Shader::kPushConstantBytes, 256);
3317 queue.WriteBuffer(uboArena.buffer, pushOffset, tb.shader->pushConstantData(), Shader::kPushConstantBytes);
3318 }
3319
3320 wgpu::BindGroup bg = makeTex2DBindGroup(gpu, depthGpu, motionGpu, extraGpu, specularGpu);
3321 uint32_t offsets[1] = {pushOffset};
3322
3323 wgpu::RenderPipeline pipe;
3324 if (tb.shader && tb.shader->gpuHandle) {
3325 auto* gs = static_cast<GpuShader*>(tb.shader->gpuHandle);
3326 const bool hdr = format == WGPUTextureFormat_RGBA16Float;
3327 if (tb.blend == BlendMode::Opaque)
3328 pipe = hdr ? gs->hdrOffscreenOpaquePipeline
3329 : (offscreen ? gs->offscreenOpaquePipeline : gs->swapchainOpaquePipeline);
3330 else
3331 pipe = hdr ? gs->hdrOffscreenPipeline : (offscreen ? gs->offscreenPipeline : gs->swapchainPipeline);
3332 } else {
3333 switch (tb.blend) {
3335 pipe = offscreen ? offscreenTexturedAdditivePipeline : texturedAdditivePipeline;
3336 break;
3338 pipe = offscreen ? offscreenTexturedPremultipliedPipeline : texturedPremultipliedPipeline;
3339 break;
3341 pipe = offscreen ? offscreenTexturedMultiplyPipeline : texturedMultiplyPipeline;
3342 break;
3343 case BlendMode::Opaque:
3344 pipe = format == WGPUTextureFormat_RGBA16Float
3345 ? hdrOffscreenTexturedOpaquePipeline
3346 : (offscreen ? offscreenTexturedOpaquePipeline : texturedOpaquePipeline);
3347 break;
3348 case BlendMode::Alpha:
3349 default:
3350 pipe = format == WGPUTextureFormat_RGBA16Float
3351 ? hdrOffscreenTexturedPipeline
3352 : (offscreen ? offscreenTexturedPipeline : texturedPipeline);
3353 break;
3354 }
3355 }
3356 if (!pipe) return;
3357 pass.SetPipeline(pipe);
3358 pass.SetBindGroup(0, bg, 1, offsets);
3359 pass.SetVertexBuffer(0, arena.buffer, vtxOffset, bytes);
3360 pass.Draw(static_cast<uint32_t>(verts.size()), 1, 0, 0);
3361}
3362
3363void Graphics::drawLitBatch(wgpu::RenderPassEncoder pass, LitBatch& lb, int viewW, int viewH,
3364 WGPUTextureFormat format) {
3365 Batcher ndc = lb.batch;
3366 ndc.toNDC(viewW, viewH);
3367 auto verts = ndc.vertices();
3368 for (auto& v : verts) v.pos.y = -v.pos.y;
3369 if (verts.empty()) return;
3370
3371 std::vector<float> data;
3372 data.reserve(verts.size() * 8);
3373 for (const auto& v : verts) {
3374 data.push_back(v.pos.x);
3375 data.push_back(v.pos.y);
3376 data.push_back(v.color.r);
3377 data.push_back(v.color.g);
3378 data.push_back(v.color.b);
3379 data.push_back(v.color.a);
3380 data.push_back(v.uv.x);
3381 data.push_back(v.uv.y);
3382 }
3383 uint64_t bytes = data.size() * sizeof(float);
3384 auto& arena = currentVertexArena();
3385 ensureVertexArena(arena, arena.used + bytes);
3386 uint64_t vtxOffset = arena.alloc(bytes);
3387 queue.WriteBuffer(arena.buffer, vtxOffset, data.data(), bytes);
3388
3389 GpuTexture* albedoGpu = gpuForTextureOrWhite(lb.albedo);
3390 // Null / missing normal → flat +Z (128,128,255), not white (which is a
3391 // bright wrong normal and washes out lit sprites without a map).
3392 GpuTexture* normalGpu = gpuForTexture(lb.normal);
3393 if (!normalGpu) normalGpu = flatNormalTexture ? flatNormalTexture : whiteTexture;
3394
3395 auto& uboArena = currentUboArena();
3396 ensureUboArena(uboArena, uboArena.used + 512);
3397 uint32_t uboOffset = uboArena.alloc(sizeof(Lighting2DUBO), 256);
3398 queue.WriteBuffer(uboArena.buffer, uboOffset, &lighting2dFrame, sizeof(Lighting2DUBO));
3399
3400 wgpu::BindGroup bg = makeTex2DBindGroup(albedoGpu, normalGpu);
3401 uint32_t offsets[1] = {uboOffset};
3402 wgpu::RenderPipeline pipe = get2DLitPipeline(lb.blend, format);
3403 if (!pipe) return;
3404 pass.SetPipeline(pipe);
3405 pass.SetBindGroup(0, bg, 1, offsets);
3406 pass.SetVertexBuffer(0, arena.buffer, vtxOffset, bytes);
3407 pass.Draw(static_cast<uint32_t>(verts.size()), 1, 0, 0);
3408}
3409
3410// ---------------------------------------------------------------------------
3411// 3D rendering
3412// ---------------------------------------------------------------------------
3413
3415 if (!initialized || !device) return;
3416 frame3DStarted = true;
3417 frameHad3DThisFrame = true;
3418 // Match the desktop (Vulkan) backend: had3DThisFrame() must return true
3419 // once a 3D frame begins, or RenderSystem3D::render bails out before any
3420 // mesh is drawn.
3421 frameHad3D = true;
3422 sceneColorPassOpen = false;
3423 mesh3dDraws.clear();
3424 primitive3DDraws.clear();
3425 shadowPassDraws.clear();
3426 voxelDraws.clear();
3427 if (surfaceNeedsRecreate.load()) {
3428 surfaceNeedsRecreate.store(false);
3429 markSwapchainDirty();
3430 }
3431 rebuildSwapchainIfNeeded();
3432}
3433
3434
3436 if (!canvas) throw Exception("begin3DFrameToCanvas: null canvas");
3437 if (!device) throw Exception("begin3DFrameToCanvas: device not initialized");
3438 auto* offscreen = dynamic_cast<OffscreenCanvas*>(canvas);
3439 if (!offscreen) throw Exception("begin3DFrameToCanvas: not an offscreen canvas");
3440 clearColor = backgroundColor;
3441 active3DCanvas = offscreen;
3442 begin3DFrame();
3443}
3445 OffscreenCanvas* canvas = active3DCanvas;
3446 if (!canvas || !device) return;
3447 auto& uboArena = currentUboArena();
3448 uboArena.reset();
3449 ensureUboArena(uboArena, 4096);
3450 auto& vertexArena = currentVertexArena();
3451 vertexArena.reset();
3452
3453 wgpuInstanceProcessEvents(instance.Get());
3454 OffscreenTimestampReadback* timestampSlot = nullptr;
3455 if (offscreenTimestampSupported && offscreenTimestampQuerySet && offscreenTimestampResolveBuffer) {
3456 for (size_t candidate = 0; candidate < offscreenTimestampReadbacks.size(); ++candidate) {
3457 const size_t index = (offscreenTimestampReadbackCursor + candidate) % offscreenTimestampReadbacks.size();
3458 if (!offscreenTimestampReadbacks[index].pending) {
3459 timestampSlot = &offscreenTimestampReadbacks[index];
3460 offscreenTimestampReadbackCursor = (index + 1) % offscreenTimestampReadbacks.size();
3461 break;
3462 }
3463 }
3464 }
3465
3466 wgpu::CommandEncoder encoder = device.CreateCommandEncoder();
3467 WGPURenderPassColorAttachment color{};
3468 color.view = canvas->colorView.Get();
3469 color.depthSlice = WGPU_DEPTH_SLICE_UNDEFINED;
3470 color.loadOp = WGPULoadOp_Clear;
3471 color.storeOp = WGPUStoreOp_Store;
3472 const Color canvasClear = canvas->clearRequested ? canvas->clearColor : clearColor;
3473 color.clearValue = {canvasClear.r, canvasClear.g, canvasClear.b, canvasClear.a};
3474 WGPURenderPassDepthStencilAttachment depth{};
3475 depth.view = canvas->depthView.Get();
3476 depth.depthClearValue = 1.f;
3477 depth.depthLoadOp = WGPULoadOp_Clear;
3478 depth.depthStoreOp = WGPUStoreOp_Store;
3479 depth.stencilLoadOp = WGPULoadOp_Undefined;
3480 depth.stencilStoreOp = WGPUStoreOp_Undefined;
3481 WGPURenderPassDescriptor descriptor{};
3482 descriptor.colorAttachmentCount = 1;
3483 descriptor.colorAttachments = &color;
3484 descriptor.depthStencilAttachment = &depth;
3485 WGPUPassTimestampWrites timestampWrites{};
3486 if (timestampSlot) {
3487 timestampWrites.querySet = offscreenTimestampQuerySet.Get();
3488 timestampWrites.beginningOfPassWriteIndex = 0;
3489 timestampWrites.endOfPassWriteIndex = 1;
3490 descriptor.timestampWrites = &timestampWrites;
3491 }
3492 wgpu::RenderPassEncoder pass =
3493 encoder.BeginRenderPass(reinterpret_cast<const wgpu::RenderPassDescriptor*>(&descriptor));
3494 flushMesh3D(pass, canvas->isHDR() ? WGPUTextureFormat_RGBA16Float : WGPUTextureFormat_RGBA8Unorm, true);
3495 flushPrimitive3D(pass, canvas->isHDR() ? WGPUTextureFormat_RGBA16Float : WGPUTextureFormat_RGBA8Unorm, 1);
3496 pass.End();
3497 if (timestampSlot) {
3498 encoder.ResolveQuerySet(offscreenTimestampQuerySet, 0, 2, offscreenTimestampResolveBuffer, 0);
3499 encoder.CopyBufferToBuffer(offscreenTimestampResolveBuffer, 0, timestampSlot->buffer, 0, sizeof(uint64_t) * 2);
3500 }
3501 wgpu::CommandBuffer command = encoder.Finish();
3502 queue.Submit(1, &command);
3503 if (timestampSlot) {
3504 timestampSlot->pending = true;
3505 WGPUBufferMapCallbackInfo callback{};
3506 callback.mode = WGPUCallbackMode_AllowProcessEvents;
3507 callback.callback = [](WGPUMapAsyncStatus status, WGPUStringView, void* userdata1, void*) {
3508 auto* slot = static_cast<OffscreenTimestampReadback*>(userdata1);
3509 if (status == WGPUMapAsyncStatus_Success && slot && slot->owner) {
3510 const auto* ticks =
3511 static_cast<const uint64_t*>(slot->buffer.GetConstMappedRange(0, sizeof(uint64_t) * 2));
3512 if (ticks && ticks[1] >= ticks[0]) {
3513 const float milliseconds = float(ticks[1] - ticks[0]) * 1.0e-6f;
3514 slot->owner->completedOffscreenTimestampMs.fetch_add(milliseconds);
3515 }
3516 slot->buffer.Unmap();
3517 }
3518 if (slot) slot->pending = false;
3519 };
3520 callback.userdata1 = timestampSlot;
3521 wgpuBufferMapAsync(timestampSlot->buffer.Get(), WGPUMapMode_Read, 0, sizeof(uint64_t) * 2, callback);
3522 }
3523 canvas->clearRequested = false;
3524 active3DCanvas = nullptr;
3525 frame3DStarted = false;
3526 frameHad3DThisFrame = false;
3527 frameHad3D = false;
3528}
3529
3530void Graphics::setMesh3DViewProj(const glm::mat4& viewProj) { mesh3dViewProj = viewProj; }
3531void Graphics::setMesh3DView(const glm::mat4& view) { mesh3dView = view; }
3532void Graphics::setMesh3DClip(float nearZ, float farZ) {
3533 mesh3dNear = nearZ;
3534 mesh3dFar = farZ;
3535}
3536
3537Texture* Graphics::getSceneColorTexture() { return sceneColorTexture; }
3538
3540 GBuffer* gbuffer = renderControl_ ? renderControl_->getGBuffer() : nullptr;
3541 return gbuffer && gbuffer->isValid() ? gbuffer->getDepthTexture() : nullptr;
3542}
3543
3544void Graphics::drawMesh(Mesh* mesh, const glm::mat4& model, Texture* texture, const Color& tint) {
3545 drawMeshShader(mesh, model, texture, tint, nullptr);
3546}
3547
3548void Graphics::drawMeshShader(Mesh* mesh, const glm::mat4& model, Texture* texture, const Color& tint, Shader* shader) {
3549 if (!mesh || !mesh->gpuHandle) return;
3550 frameHad3DThisFrame = true;
3551 frameHad3D = true;
3552 Mesh3dDraw d;
3553 d.mesh = mesh;
3554 d.texture = texture;
3555 d.normalTexture = mesh3dNormalTexture;
3556 d.heightTexture = mesh3dHeightTexture;
3557 d.virtualTexture = mesh3dVirtualTexture;
3558 d.virtualAtlas = mesh3dVirtualAtlas;
3559 d.model = model;
3560 d.tint = tint;
3561 d.shader = shader;
3562 d.surfaceMode = mesh3dSurfaceMode;
3563 d.surfaceBlend = mesh3dSurfaceBlend;
3564 d.depthWrite = mesh3dSurfaceDepthWrite;
3565 d.doubleSided = mesh3dSurfaceDoubleSided;
3566 d.shadowReceive = mesh3dShadowReceive;
3567 d.metallic = mesh3dMetallic;
3568 d.roughness = mesh3dRoughness;
3569 d.viewProj = mesh3dViewProj;
3570 d.view = mesh3dView;
3571 d.cameraPos = mesh3dCameraPos;
3572 d.lighting = mesh3dLighting;
3573 d.shadows = mesh3dShadows;
3574 d.environment = mesh3dEnvTexture;
3575 d.environmentIntensity = mesh3dEnvIntensity;
3576 d.skinInfluenceLimit = mesh3dSkinInfluenceLimit;
3577 d.alphaCutoff = mesh3dAlphaCutoff;
3578 d.alphaTechnique = mesh3dAlphaTechnique;
3579 d.lodFade = mesh3dLodFade;
3580 d.pbrSurface = mesh3dPbrSurface;
3581 mesh3dDraws.push_back(d);
3582}
3583
3585void Graphics::setMesh3DPackedNormalMask(bool enabled) { mesh3dVirtualAtlas.z = enabled ? 1.f : 0.f; }
3587
3588void Graphics::setMesh3DVirtualTexture(bool enabled, int pageCountX, int pageCountY, int atlasSlotsX, int atlasSlotsY,
3589 float borderFraction) {
3590 mesh3dVirtualTexture =
3591 enabled ? glm::vec4(1.f, float(pageCountX), float(pageCountY), borderFraction) : glm::vec4(0.f);
3592 const float packedNormalMask = mesh3dVirtualAtlas.z;
3593 mesh3dVirtualAtlas = enabled ? glm::vec4(float(atlasSlotsX), float(atlasSlotsY), packedNormalMask, 0.f)
3594 : glm::vec4(0.f, 0.f, packedNormalMask, 0.f);
3595}
3596void Graphics::setMesh3DSceneDepth(Texture* depth) { mesh3dSceneDepthTexture = depth; }
3597void Graphics::setMesh3DSceneColor(Texture* color) { mesh3dSceneColorTexture = color; }
3598
3600 if (mesh3dSceneColorTexture) return Mesh3DSceneColorCaptureStatus::ExplicitOverride;
3601 if (active3DCanvas)
3602 return sceneColorHistoryValid ? Mesh3DSceneColorCaptureStatus::HistoryReuse
3604 if (sceneColorSlots.size() < 2 || sceneColorSamples != 1)
3605 return sceneColorHistoryValid ? Mesh3DSceneColorCaptureStatus::HistoryReuse
3607 if (!mesh3dSceneColorCaptureIndex) mesh3dSceneColorCaptureIndex = mesh3dDraws.size();
3609}
3611 mesh3dMetallic = metallic;
3612 mesh3dRoughness = roughness;
3613}
3615 const std::string& alphaTechnique) {
3616 mesh3dSurfaceMode = mode;
3617 mesh3dSurfaceBlend = blend;
3618 mesh3dSurfaceDepthWrite = depthWrite;
3619 mesh3dSurfaceDoubleSided = doubleSided;
3620 mesh3dAlphaCutoff = std::clamp(alphaCutoff, 0.f, 1.f);
3621 mesh3dAlphaTechnique = alphaTechnique;
3622}
3623void Graphics::setMesh3DTexCellBomb(float cellScale, float strength, float rotAmount) {
3624 mesh3dTexBombScale = cellScale;
3625 mesh3dTexBombStrength = strength;
3626 mesh3dTexBombRot = rotAmount;
3627}
3628void Graphics::setMesh3DParallax(float scale, float minLayers, float maxLayers) {
3629 mesh3dParallaxScale = scale;
3630 mesh3dParallaxMin = minLayers;
3631 mesh3dParallaxMax = maxLayers;
3632}
3633void Graphics::setMesh3DLodDither(float weight, bool reverse, bool enabled) {
3634 mesh3dLodFade = glm::vec4(std::clamp(weight, 0.f, 1.f), reverse ? 1.f : 0.f,
3635 enabled ? 1.f : 0.f, 0.f);
3636}
3637void Graphics::setMesh3DLighting(const Lighting3DPack& pack) { mesh3dLighting = pack; }
3638void Graphics::setCloudShadows(float strength, float worldCell, float time, float windSpeed, float windAngle,
3639 float coverage, float detail) {
3640 mesh3dCloud = glm::vec4(std::clamp(strength, 0.f, 1.f), std::max(worldCell, 1e-4f), time, 0.f);
3641 mesh3dCloudWind = glm::vec4(std::cos(windAngle) * windSpeed, std::sin(windAngle) * windSpeed,
3642 std::clamp(coverage, 0.f, 1.f), std::clamp(detail, 0.f, 1.f));
3643}
3645 mesh3dClustered = upload;
3646 mesh3dClusteredActive = upload.active;
3647 if (upload.active) uploadClusteredLighting(upload);
3648}
3649void Graphics::setMesh3DClusteredActive(bool active) { mesh3dClusteredActive = active; }
3650void Graphics::setMesh3DSSAO(float intensity) { mesh3dSsaoIntensity = std::clamp(intensity, 0.f, 1.f); }
3651
3652void Graphics::uploadClusteredLighting(const ClusteredLightingUpload& upload) {
3653 if (!device) return;
3654 ClusteredStorage& st = clusteredStorage[currentFrameSlot()];
3655 auto ensure = [&](wgpu::Buffer& buf, uint64_t& cap, uint64_t need) {
3656 if (need == 0) need = 4;
3657 if (buf && cap >= need) return;
3658 WGPUBufferDescriptor bd{};
3659 bd.label = sv("eve_clustered_ssbo");
3660 bd.size = std::max(need, cap ? cap * 2 : need);
3661 bd.usage = WGPUBufferUsage_CopyDst | WGPUBufferUsage_Storage;
3662 bd.mappedAtCreation = false;
3663 buf = device.CreateBuffer(reinterpret_cast<const wgpu::BufferDescriptor*>(&bd));
3664 cap = bd.size;
3665 };
3666 const uint64_t lightsBytes = std::max<uint64_t>(1, upload.lights.size()) * sizeof(ClusteredLightGpu);
3667 const uint64_t tableBytes = std::max<uint64_t>(1, upload.clusterTable.size()) * sizeof(ClusterTableEntry);
3668 const uint64_t indicesBytes = std::max<uint64_t>(1, upload.lightIndices.size()) * sizeof(uint32_t);
3669 ensure(st.lights, st.lightsCap, lightsBytes);
3670 ensure(st.table, st.tableCap, tableBytes);
3671 ensure(st.indices, st.indicesCap, indicesBytes);
3672
3673 ClusteredLightGpu zero{};
3674 if (!upload.lights.empty())
3675 queue.WriteBuffer(st.lights, 0, upload.lights.data(), lightsBytes);
3676 else
3677 queue.WriteBuffer(st.lights, 0, &zero, sizeof(zero));
3678 if (!upload.clusterTable.empty()) queue.WriteBuffer(st.table, 0, upload.clusterTable.data(), tableBytes);
3679 if (!upload.lightIndices.empty()) queue.WriteBuffer(st.indices, 0, upload.lightIndices.data(), indicesBytes);
3680}
3681void Graphics::setMesh3DLight(const glm::vec3& dir, const glm::vec3& color) {
3682 mesh3dLighting.lights[0].posRadius = glm::vec4(dir, 0.f);
3683 mesh3dLighting.lights[0].color = glm::vec4(color, 1.f);
3684}
3685void Graphics::setMesh3DCameraPos(const glm::vec3& eye) { mesh3dCameraPos = eye; }
3686void Graphics::setMesh3DEnv(Texture* cube, float intensity) {
3687 mesh3dEnvTexture = cube;
3688 mesh3dEnvIntensity = intensity;
3689}
3690
3691void Graphics::setMesh3DEnvProbe(const glm::vec3& center, const glm::vec3& extent) {
3692 mesh3dEnvProbeCenter = center;
3693 mesh3dEnvProbeExtent = glm::max(extent, glm::vec3(0.f));
3694}
3695
3696void Graphics::setMesh3DReflectionProbes(const ReflectionProbeUpload& upload) { mesh3dReflectionProbes = upload; }
3697void Graphics::setMesh3DShadows(const ShadowUpload& upload) { mesh3dShadows = upload; }
3698void Graphics::setMesh3DShadowReceive(bool receive) { mesh3dShadowReceive = receive; }
3700 mesh3dSkinInfluenceLimit = static_cast<int>(count);
3701}
3702
3703void Graphics::beginShadowPass(int cascadeIndex) {
3704 shadowPassCascade = cascadeIndex;
3705 shadowPassDraws.clear();
3706}
3707
3708void Graphics::drawMeshShadow(Mesh* mesh, const glm::mat4& lightMVP, bool doubleSided) {
3709 if (!mesh || !mesh->gpuHandle) return;
3710 ShadowDraw d;
3711 d.mesh = mesh;
3712 d.mvp = lightMVP;
3713 d.doubleSided = doubleSided;
3714 d.skinInfluenceLimit = mesh3dSkinInfluenceLimit;
3715 shadowPassDraws.push_back(d);
3716}
3717
3718void Graphics::drawMeshShadowAlpha(Mesh* mesh, const glm::mat4& lightMVP, Texture* albedo,
3719 bool doubleSided, float lodWeight, bool lodFadeReverse,
3720 bool lodDither) {
3721 if (!mesh || !mesh->gpuHandle) return;
3722 ShadowDraw d;
3723 d.mesh = mesh;
3724 d.albedo = albedo;
3725 d.mvp = lightMVP;
3726 d.alphaTest = true;
3727 d.doubleSided = doubleSided;
3728 d.skinInfluenceLimit = mesh3dSkinInfluenceLimit;
3729 d.lodFade = glm::vec4(std::clamp(lodWeight, 0.f, 1.f), lodFadeReverse ? 1.f : 0.f,
3730 lodDither ? 1.f : 0.f, 0.f);
3731 shadowPassDraws.push_back(d);
3732}
3733
3735 if (shadowPassCascade < 0 || shadowPassCascade >= ShadowConfig::kTotalLayers) {
3736 shadowPassCascade = -1;
3737 shadowPassDraws.clear();
3738 return;
3739 }
3740 shadowCascadeDraws[shadowPassCascade] = shadowPassDraws;
3741 shadowPassDraws.clear();
3742 shadowPassCascade = -1;
3743}
3744
3745// ---------------------------------------------------------------------------
3746// GBuffer pass
3747// ---------------------------------------------------------------------------
3748
3750 if (!device) return;
3751 createGbufferResources(width, height);
3752 gbufferPassActive = true;
3753 gbufferPassPending = true;
3754 gbufferPassDraws.clear();
3755}
3756
3757void Graphics::drawMeshGBuffer(Mesh* mesh, const glm::mat4& mvp, const glm::mat4& model, float nearZ, float farZ,
3758 Texture* albedo, float tintR, float tintG, float tintB, float motionX, float motionY,
3759 float roughness, float metallic) {
3760 if (!mesh || !mesh->gpuHandle) return;
3761 GbufferDraw d;
3762 d.mesh = mesh;
3763 d.albedo = albedo;
3764 d.mvp = mvp;
3765 d.model = model;
3766 d.nearZ = nearZ;
3767 d.farZ = farZ;
3768 d.tint = glm::vec4(tintR, tintG, tintB, 1.f);
3769 d.motion = glm::vec2(motionX, motionY);
3770 d.roughness = roughness;
3771 d.metallic = metallic;
3772 d.skinInfluenceLimit = mesh3dSkinInfluenceLimit;
3773 gbufferPassDraws.push_back(d);
3774}
3775
3776void Graphics::drawMeshGBufferAlpha(Mesh* mesh, const glm::mat4& mvp, const glm::mat4& model, float nearZ, float farZ,
3777 Texture* albedo, float tintR, float tintG, float tintB, float motionX,
3778 float motionY, float roughness, float metallic) {
3779 if (!mesh || !mesh->gpuHandle) return;
3780 GbufferDraw d;
3781 d.mesh = mesh;
3782 d.albedo = albedo;
3783 d.mvp = mvp;
3784 d.model = model;
3785 d.nearZ = nearZ;
3786 d.farZ = farZ;
3787 d.tint = glm::vec4(tintR, tintG, tintB, 1.f);
3788 d.motion = glm::vec2(motionX, motionY);
3789 d.roughness = roughness;
3790 d.metallic = metallic;
3791 d.alphaTest = true;
3792 d.skinInfluenceLimit = mesh3dSkinInfluenceLimit;
3793 gbufferPassDraws.push_back(d);
3794}
3795
3797 gbufferPassActive = false;
3798 // Expose the G-buffer textures (depth/normal/albedo/hwDepth/pbr/emissive) to
3799 // RenderControl so post passes (AO, X-ray scene depth) can sample them this frame.
3800 if (!gbufferSlots.empty()) {
3801 GbufferSlot& slot = gbufferSlots[currentFrameSlot()];
3802 getRenderControl()->getGBuffer()->setTargets(gbufferWidth, gbufferHeight, &slot.depthColorTex, &slot.normalTex,
3803 &slot.albedoTex, &slot.depthTex, &slot.pbrParamsTex,
3804 &slot.emissiveTex);
3805 }
3806}
3807
3809 if (!device || width <= 0 || height <= 0) return;
3810 createDecalResources(width, height);
3811 if (!decalFlatAlbedo) {
3812 const uint8_t transparent[4] = {0, 0, 0, 0};
3813 const uint8_t flatNormal[4] = {128, 128, 255, 255};
3814 const uint8_t neutralParams[4] = {128, 128, 0, 255};
3815 decalFlatAlbedo = newTexture(1, 1, transparent);
3816 decalFlatNormal = newTexture(1, 1, flatNormal);
3817 decalFlatParams = newTexture(1, 1, neutralParams);
3818 }
3819 decalPassActive = true;
3820 decalPassPending = false;
3821 decalPassDraws.clear();
3822}
3823
3824void Graphics::setDecalCamera(const glm::mat4& viewProj, float nearZ, float farZ) {
3825 (void)nearZ;
3826 (void)farZ;
3827 decalViewProj = viewProj;
3828}
3829
3831 Texture *params, const float uvRect[4], float fade,
3832 float normalStrength, float roughnessStrength, float metalStrength,
3833 float emissiveStrength, int blendMode, int projectionMode,
3834 float blendSharpness, float parallaxScale, float parallaxMinLayers,
3835 float parallaxMaxLayers, float edgeFadeWidth) {
3836 if (!decalPassActive) return;
3837 DecalDraw draw;
3838 draw.model = model;
3839 draw.albedo = albedo ? albedo : decalFlatAlbedo;
3840 draw.normal = normal ? normal : decalFlatNormal;
3841 draw.params = params ? params : decalFlatParams;
3842 if (uvRect) draw.uvRect = glm::vec4(uvRect[0], uvRect[1], uvRect[2], uvRect[3]);
3843 draw.fadeParams = glm::vec4(fade, normalStrength, roughnessStrength, metalStrength);
3844 draw.extraParams = glm::vec4(emissiveStrength, float(blendMode == 1),
3845 float(std::clamp(projectionMode, 0, 3)),
3846 blendSharpness > 0.f ? blendSharpness : 4.f);
3847 const float minLayers = std::clamp(parallaxMinLayers, 1.f, 64.f);
3848 draw.surfaceParams = glm::vec4(std::clamp(parallaxScale, 0.f, 1.f), minLayers,
3849 std::clamp(parallaxMaxLayers, minLayers, 64.f),
3850 std::clamp(edgeFadeWidth, 0.f, 0.49f));
3851 decalPassDraws.push_back(draw);
3852}
3853
3855 if (!decalPassActive) return;
3856 decalPassActive = false;
3857 decalPassPending = true;
3858}
3859
3860// ---------------------------------------------------------------------------
3861// Voxel
3862// ---------------------------------------------------------------------------
3863
3864
3865// ---------------------------------------------------------------------------
3866// Scene color / shadow / gbuffer resources
3867// ---------------------------------------------------------------------------
3868
3869void Graphics::createSceneColorResources(int width, int height) {
3870 if (!device) return;
3871 // Keep the main WebGPU scene single-sampled. Dawn/Metal does not complete
3872 // the current MSAA resolve followed by synchronous scene readback chain.
3873 // Temporal AA remains available; Vulkan owns the multisampled main path.
3874 const uint32_t want = 1u;
3875 if (sceneColorWidth == width && sceneColorHeight == height && sceneColorSamples == want && !sceneColorSlots.empty())
3876 return;
3877
3878 destroySceneColorResources();
3879 sceneColorWidth = width;
3880 sceneColorHeight = height;
3881 const uint32_t oldSamples = sceneColorSamples;
3882 const bool samplesChanged = oldSamples != want;
3883 sceneColorSamples = want;
3884 sceneColorSlots.reserve(kFramesInFlight);
3885 for (int s = 0; s < int(kFramesInFlight); ++s) {
3886 sceneColorSlots.emplace_back();
3887 SceneColorSlot& slot = sceneColorSlots.back();
3888 slot.sampleCount = sceneColorSamples;
3889
3890 WGPUTextureDescriptor cd{};
3891 cd.label = sv("eve_scene_color");
3892 cd.dimension = WGPUTextureDimension_2D;
3893 cd.size = {static_cast<uint32_t>(width), static_cast<uint32_t>(height), 1};
3894 // Single-sample resolve target: composited to the swapchain and used
3895 // as the frame-readback source. The multisampled color lives in
3896 // msaaColor (created below) and is resolved into this texture.
3897 cd.sampleCount = 1;
3898 cd.format = sceneColorFormat;
3899 cd.mipLevelCount = 1;
3900 cd.usage = WGPUTextureUsage_TextureBinding | WGPUTextureUsage_RenderAttachment | WGPUTextureUsage_CopySrc;
3901 slot.color = device.CreateTexture(reinterpret_cast<const wgpu::TextureDescriptor*>(&cd));
3902 slot.colorView = slot.color.CreateView();
3903
3904 if (sceneColorSamples > 1) {
3905 // Multisampled color target; the scene pass resolves it into the
3906 // single-sample `color` above for compositing / readback.
3907 WGPUTextureDescriptor mcd{};
3908 mcd.label = sv("eve_scene_color_msaa");
3909 mcd.dimension = WGPUTextureDimension_2D;
3910 mcd.size = {static_cast<uint32_t>(width), static_cast<uint32_t>(height), 1};
3911 mcd.sampleCount = sceneColorSamples;
3912 mcd.format = sceneColorFormat;
3913 mcd.mipLevelCount = 1;
3914 mcd.usage = WGPUTextureUsage_RenderAttachment;
3915 slot.msaaColor = device.CreateTexture(reinterpret_cast<const wgpu::TextureDescriptor*>(&mcd));
3916 slot.msaaView = slot.msaaColor.CreateView();
3917 }
3918
3919 WGPUTextureDescriptor dd{};
3920 dd.label = sv("eve_scene_depth");
3921 dd.dimension = WGPUTextureDimension_2D;
3922 dd.size = {static_cast<uint32_t>(width), static_cast<uint32_t>(height), 1};
3923 dd.sampleCount = sceneColorSamples;
3924 dd.format = WGPUTextureFormat_Depth32Float;
3925 dd.mipLevelCount = 1;
3926 dd.usage = WGPUTextureUsage_RenderAttachment;
3927 slot.depth = device.CreateTexture(reinterpret_cast<const wgpu::TextureDescriptor*>(&dd));
3928 slot.depthView = slot.depth.CreateView();
3929
3930 slot.colorGpu.texture = slot.color;
3931 slot.colorGpu.view = slot.colorView;
3932 slot.colorGpu.width = width;
3933 slot.colorGpu.height = height;
3934 slot.colorGpu.sampler = createLinearSampler(device);
3935 slot.colorGpu.samplerState = TextureSampler::linearMipmap();
3936
3937 slot.colorTex.gpuHandle = &slot.colorGpu;
3938 slot.colorTex.width = width;
3939 slot.colorTex.height = height;
3940 slot.colorTex.mipmapCount = 1;
3941 }
3942 sceneColorTexture = &sceneColorSlots[0].colorTex;
3943 // 3D pipelines that render into the scene target must match its sample
3944 // count. Invalidate only the affected cached variants; they are rebuilt
3945 // individually if a later draw actually needs them.
3946 if (samplesChanged) {
3947 mesh3dPipelines = {};
3948 mesh3dPipeline = {};
3949 mesh3dTransparentPipeline = {};
3950 mesh3dClusteredPipeline = {};
3951 voxelRectPipeline = {};
3952 // Deferred lighting is 1x-only; recreate when scene sample count changes.
3953 destroyDeferredLightingResources();
3954 }
3955 if (samplesChanged && gpuDrivenCullPipeline_) {
3956 // The forward indirect pipeline inherits the scene target sample
3957 // count. Recreate the GPU-driven layouts/pipelines lazily on the next
3958 // cull after an MSAA toggle.
3959 gpuDrivenCullPipeline_ = {};
3960 gpuDrivenRenderPipeline_ = {};
3961 gpuDrivenCanvasPipeline_ = {};
3962 gpuDrivenComputePipelineLayout_ = {};
3963 gpuDrivenRenderPipelineLayout_ = {};
3964 gpuDrivenComputeSetLayout_ = {};
3965 gpuDrivenRenderSetLayout_ = {};
3966 gpuDrivenComputeBindGroup_ = {};
3967 gpuDrivenRenderBindGroup_ = {};
3968 }
3969}
3970
3971void Graphics::destroySceneColorResources() {
3972 sceneColorSlots.clear();
3973 sceneColorTexture = nullptr;
3974 sceneColorHistoryValid = false;
3975}
3976
3977void Graphics::setMsaaSamples(int samples) {
3978 msaaSamples = samples > 0 ? samples : 0;
3979 if (!initialized || sceneColorWidth <= 0 || sceneColorHeight <= 0) return;
3980 // Force the offscreen targets (and the 3D pipelines that bind them) to
3981 // rebuild at the new sample count.
3982 destroySceneColorResources();
3983 createSceneColorResources(sceneColorWidth, sceneColorHeight);
3984}
3985
3986void Graphics::createShadowResources() {
3987 if (!device) return;
3988 if (shadowDepthArray) return;
3989 shadowMapSize = ShadowConfig::kMapSize;
3990
3991 // Depth array: 3 cascade layers, renderable + sampleable.
3992 auto* gpu = new GpuTexture();
3993 WGPUTextureDescriptor td{};
3994 td.label = sv("eve_shadow_depth");
3995 td.dimension = WGPUTextureDimension_2D;
3996 td.size = {static_cast<uint32_t>(shadowMapSize), static_cast<uint32_t>(shadowMapSize),
3997 static_cast<uint32_t>(ShadowConfig::kTotalLayers)};
3998 td.sampleCount = 1;
3999 td.format = WGPUTextureFormat_Depth32Float;
4000 td.mipLevelCount = 1;
4001 td.usage = WGPUTextureUsage_TextureBinding | WGPUTextureUsage_RenderAttachment | WGPUTextureUsage_CopySrc |
4002 WGPUTextureUsage_CopyDst;
4003 gpu->texture = device.CreateTexture(reinterpret_cast<const wgpu::TextureDescriptor*>(&td));
4004
4005 WGPUTextureViewDescriptor avd{};
4006 avd.format = WGPUTextureFormat_Depth32Float;
4007 avd.dimension = WGPUTextureViewDimension_2DArray;
4008 avd.baseMipLevel = 0;
4009 avd.mipLevelCount = 1;
4010 avd.baseArrayLayer = 0;
4011 avd.arrayLayerCount = 3;
4012 gpu->view = gpu->texture.CreateView(reinterpret_cast<const wgpu::TextureViewDescriptor*>(&avd));
4013
4014 WGPUSamplerDescriptor sd{};
4015 sd.label = sv("eve_shadow_sampler");
4016 sd.addressModeU = WGPUAddressMode_ClampToEdge;
4017 sd.addressModeV = WGPUAddressMode_ClampToEdge;
4018 sd.addressModeW = WGPUAddressMode_ClampToEdge;
4019 sd.magFilter = WGPUFilterMode_Linear;
4020 sd.minFilter = WGPUFilterMode_Linear;
4021 sd.mipmapFilter = WGPUMipmapFilterMode_Nearest;
4022 sd.compare = WGPUCompareFunction_LessEqual;
4023 sd.maxAnisotropy = 1.f;
4024 gpu->sampler = device.CreateSampler(reinterpret_cast<const wgpu::SamplerDescriptor*>(&sd));
4025 shadowDepthArray = gpu;
4026}
4027
4028void Graphics::destroyShadowResources() { shadowDepthArray = nullptr; }
4029
4030void Graphics::createGbufferResources(int width, int height) {
4031 if (!device) return;
4032 if (gbufferWidth == width && gbufferHeight == height && !gbufferSlots.empty()) return;
4033 destroyGbufferResources();
4034 gbufferWidth = width;
4035 gbufferHeight = height;
4036
4037 WGPUTextureDescriptor td{};
4038 td.label = sv("eve_gbuffer_target");
4039 td.dimension = WGPUTextureDimension_2D;
4040 td.size = {static_cast<uint32_t>(width), static_cast<uint32_t>(height), 1};
4041 td.sampleCount = 1;
4042 td.format = WGPUTextureFormat_RGBA8Unorm;
4043 td.mipLevelCount = 1;
4044 td.usage = WGPUTextureUsage_TextureBinding | WGPUTextureUsage_RenderAttachment | WGPUTextureUsage_CopySrc;
4045
4046 WGPUTextureDescriptor dd{};
4047 dd.label = sv("eve_gbuffer_depth");
4048 dd.dimension = WGPUTextureDimension_2D;
4049 dd.size = {static_cast<uint32_t>(width), static_cast<uint32_t>(height), 1};
4050 dd.sampleCount = 1;
4051 dd.format = WGPUTextureFormat_Depth32Float;
4052 dd.mipLevelCount = 1;
4053 // TextureBinding so X-ray (and AO) can sample the scene depth in a later pass.
4054 dd.usage = WGPUTextureUsage_TextureBinding | WGPUTextureUsage_RenderAttachment;
4055
4056 WGPUTextureDescriptor visIdDesc = td;
4057 visIdDesc.label = sv("eve_visibility_id");
4058 visIdDesc.format = WGPUTextureFormat_RG32Uint;
4059 WGPUTextureDescriptor visBaryDesc = td;
4060 visBaryDesc.label = sv("eve_visibility_bary");
4061 visBaryDesc.format = WGPUTextureFormat_RG16Float;
4062
4063 gbufferSlots.reserve(kFramesInFlight);
4064 for (int s = 0; s < int(kFramesInFlight); ++s) {
4065 gbufferSlots.emplace_back();
4066 GbufferSlot& slot = gbufferSlots.back();
4067 slot.normal = device.CreateTexture(reinterpret_cast<const wgpu::TextureDescriptor*>(&td));
4068 slot.normalView = slot.normal.CreateView();
4069 slot.depthColor = device.CreateTexture(reinterpret_cast<const wgpu::TextureDescriptor*>(&td));
4070 slot.depthColorView = slot.depthColor.CreateView();
4071 slot.albedo = device.CreateTexture(reinterpret_cast<const wgpu::TextureDescriptor*>(&td));
4072 slot.albedoView = slot.albedo.CreateView();
4073 slot.pbrParams = device.CreateTexture(reinterpret_cast<const wgpu::TextureDescriptor*>(&td));
4074 slot.pbrParamsView = slot.pbrParams.CreateView();
4075 slot.emissive = device.CreateTexture(reinterpret_cast<const wgpu::TextureDescriptor*>(&td));
4076 slot.emissiveView = slot.emissive.CreateView();
4077 slot.depth = device.CreateTexture(reinterpret_cast<const wgpu::TextureDescriptor*>(&dd));
4078 slot.depthView = slot.depth.CreateView();
4079 slot.visID = device.CreateTexture(reinterpret_cast<const wgpu::TextureDescriptor*>(&visIdDesc));
4080 slot.visIDView = slot.visID.CreateView();
4081 slot.visBary = device.CreateTexture(reinterpret_cast<const wgpu::TextureDescriptor*>(&visBaryDesc));
4082 slot.visBaryView = slot.visBary.CreateView();
4083
4084 slot.normalGpu.texture = slot.normal;
4085 slot.normalGpu.view = slot.normalView;
4086 slot.normalGpu.sampler = createLinearSampler(device);
4087 slot.depthColorGpu.texture = slot.depthColor;
4088 slot.depthColorGpu.view = slot.depthColorView;
4089 slot.depthColorGpu.sampler = createLinearSampler(device);
4090 slot.albedoGpu.texture = slot.albedo;
4091 slot.albedoGpu.view = slot.albedoView;
4092 slot.albedoGpu.sampler = createLinearSampler(device);
4093 slot.pbrParamsGpu.texture = slot.pbrParams;
4094 slot.pbrParamsGpu.view = slot.pbrParamsView;
4095 slot.pbrParamsGpu.sampler = createLinearSampler(device);
4096 slot.emissiveGpu.texture = slot.emissive;
4097 slot.emissiveGpu.view = slot.emissiveView;
4098 slot.emissiveGpu.sampler = createLinearSampler(device);
4099 slot.depthGpu.texture = slot.depth;
4100 slot.depthGpu.view = slot.depthView;
4101 slot.depthGpu.sampler = createLinearSampler(device);
4102
4103 slot.normalTex.gpuHandle = &slot.normalGpu;
4104 slot.normalTex.width = width;
4105 slot.normalTex.height = height;
4106 slot.depthColorTex.gpuHandle = &slot.depthColorGpu;
4107 slot.depthColorTex.width = width;
4108 slot.depthColorTex.height = height;
4109 slot.albedoTex.gpuHandle = &slot.albedoGpu;
4110 slot.albedoTex.width = width;
4111 slot.albedoTex.height = height;
4112 slot.pbrParamsTex.gpuHandle = &slot.pbrParamsGpu;
4113 slot.pbrParamsTex.width = width;
4114 slot.pbrParamsTex.height = height;
4115 slot.emissiveTex.gpuHandle = &slot.emissiveGpu;
4116 slot.emissiveTex.width = width;
4117 slot.emissiveTex.height = height;
4118 slot.depthTex.gpuHandle = &slot.depthGpu;
4119 slot.depthTex.width = width;
4120 slot.depthTex.height = height;
4121 }
4122}
4123
4124void Graphics::destroyGbufferResources() {
4125 gbufferSlots.clear();
4126 gbufferDepthValid_ = false;
4127}
4128
4129void Graphics::createDecalResources(int width, int height) {
4130 if (!device) return;
4131 if (decalWidth == width && decalHeight == height && !decalSlots.empty()) return;
4132 destroyDecalResources();
4133 decalWidth = width;
4134 decalHeight = height;
4135 WGPUTextureDescriptor td{};
4136 td.label = sv("eve_decal_target");
4137 td.dimension = WGPUTextureDimension_2D;
4138 td.size = {static_cast<uint32_t>(width), static_cast<uint32_t>(height), 1};
4139 td.sampleCount = 1;
4140 td.format = WGPUTextureFormat_RGBA8Unorm;
4141 td.mipLevelCount = 1;
4142 td.usage = WGPUTextureUsage_TextureBinding | WGPUTextureUsage_RenderAttachment | WGPUTextureUsage_CopySrc;
4143 decalSlots.resize(kFramesInFlight);
4144 for (auto& slot : decalSlots) {
4145 slot.albedo = device.CreateTexture(reinterpret_cast<const wgpu::TextureDescriptor*>(&td));
4146 slot.albedoView = slot.albedo.CreateView();
4147 slot.normal = device.CreateTexture(reinterpret_cast<const wgpu::TextureDescriptor*>(&td));
4148 slot.normalView = slot.normal.CreateView();
4149 slot.params = device.CreateTexture(reinterpret_cast<const wgpu::TextureDescriptor*>(&td));
4150 slot.paramsView = slot.params.CreateView();
4151 }
4152}
4153
4154void Graphics::destroyDecalResources() {
4155 decalSlots.clear();
4156 decalWidth = 0;
4157 decalHeight = 0;
4158}
4159
4160// ---------------------------------------------------------------------------
4161// Flush helpers
4162// ---------------------------------------------------------------------------
4163
4164
4165void Graphics::flushMesh3D(wgpu::RenderPassEncoder pass, WGPUTextureFormat format, bool canvasTarget) {
4166 if (mesh3dDraws.empty()) return;
4167
4168 if (mesh3dClusteredActive && !canvasTarget && !mesh3dClusteredPipeline) {
4169 for (const auto& d : mesh3dDraws) {
4170 const bool customShader = d.shader && d.shader->gpuHandle;
4171 if (!customShader && !d.doubleSided && d.surfaceMode != SurfaceMode::Transparent && d.mesh &&
4172 !d.mesh->hasGpuSkinning()) {
4173 createMesh3DClusteredPipeline();
4174 break;
4175 }
4176 }
4177 }
4178
4179 auto& uboArena = currentUboArena();
4180 ensureUboArena(uboArena, uboArena.used + mesh3dDraws.size() * 10240);
4181 auto& vtxArena = currentVertexArena();
4182
4183 // Pre-allocate per-draw UBO slots.
4184 for (auto& d : mesh3dDraws) {
4185 d.frameUboOffset = uboArena.alloc(sizeof(Mesh3DUBO), 256);
4186 d.shadowUboOffset = uboArena.alloc(sizeof(ShadowUBO), 256);
4187 d.clusteredUboOffset = 0;
4188 if (mesh3dClusteredActive && !canvasTarget && d.mesh && !d.mesh->hasGpuSkinning())
4189 d.clusteredUboOffset = uboArena.alloc(sizeof(Mesh3DClusteredUBO), 256);
4190 d.pushUboOffset = 0;
4191 if (d.shader && d.shader->pushConstantSize() > 0)
4192 d.pushUboOffset = uboArena.alloc(Shader::kPushConstantBytes, 256);
4193 }
4194
4195 // Upload the per-draw frame UBO + shared shadow UBO.
4196 for (auto& d : mesh3dDraws) {
4197 if (mesh3dClusteredActive && !canvasTarget && mesh3dClusteredPipeline && d.clusteredUboOffset) {
4198 Mesh3DClusteredUBO cubo;
4199 cubo.mvp = mesh3dViewProj * d.model;
4200 cubo.model = d.model;
4201 cubo.view = mesh3dView;
4202 cubo.lightDir = glm::vec4(glm::vec3(mesh3dClustered.primaryDir), mesh3dClustered.primaryDir.w);
4203 cubo.lightColor = glm::vec4(glm::vec3(mesh3dClustered.primaryColor), mesh3dEnvIntensity);
4204 cubo.tint = d.tint;
4205 cubo.cameraPos = glm::vec4(mesh3dCameraPos, mesh3dRoughness);
4206 cubo.ambient = glm::vec4(glm::vec3(mesh3dClustered.ambient), mesh3dMetallic);
4207 cubo.gridInfo = mesh3dClustered.gridInfo;
4208 cubo.clipInfo = mesh3dClustered.clipInfo;
4209 cubo.texBomb = glm::vec4(mesh3dTexBombScale, mesh3dTexBombStrength, mesh3dTexBombRot, 0.f);
4210 cubo.parallax = glm::vec4(mesh3dParallaxScale, mesh3dParallaxMin, mesh3dParallaxMax, 0.f);
4211 cubo.virtualTexture = d.virtualTexture;
4212 cubo.virtualAtlas = d.virtualAtlas;
4213 float surfaceCode = float(int(d.surfaceMode));
4214 if (d.surfaceMode == SurfaceMode::Masked && d.alphaTechnique == "dither")
4215 surfaceCode = 3.f;
4216 else if (d.surfaceMode == SurfaceMode::Masked && d.alphaTechnique == "coverage")
4217 surfaceCode = 4.f;
4218 const float aoStrength = (renderControl_ && renderControl_->isEnabled("ao")) ? mesh3dSsaoIntensity : 0.f;
4219 cubo.surface = glm::vec4(surfaceCode, d.alphaCutoff, aoStrength, 0.f);
4220 cubo.envProbeCenter = glm::vec4(mesh3dEnvProbeCenter, 1.f);
4221 cubo.envProbeExtent = glm::vec4(mesh3dEnvProbeExtent, 0.f);
4222 for (int i = 0; i < ReflectionProbeUpload::kMaxProbes; ++i) {
4223 if (i >= mesh3dReflectionProbes.count) continue;
4224 const auto& probe = mesh3dReflectionProbes.probes[i];
4225 GpuTexture* gpu = gpuForTexture(probe.cubemap);
4226 if (!gpu || !gpu->isCube) continue;
4227 cubo.reflectionProbeCenter[i] = glm::vec4(probe.center, probe.intensity);
4228 cubo.reflectionProbeExtent[i] = glm::vec4(probe.extent, probe.blendDistance);
4229 }
4230 queue.WriteBuffer(uboArena.buffer, d.clusteredUboOffset, &cubo, sizeof(cubo));
4231 continue;
4232 }
4233 Mesh3DUBO ubo;
4234 ubo.mvp = mesh3dViewProj * d.model;
4235 ubo.model = d.model;
4236 ubo.lightDir = glm::vec4(glm::vec3(mesh3dLighting.lights[0].posRadius), float(mesh3dLighting.count));
4237 ubo.lightColor = mesh3dLighting.lights[0].color;
4238 ubo.tint = d.tint;
4239 ubo.cameraPos = glm::vec4(mesh3dCameraPos, mesh3dRoughness);
4240 ubo.ambient = glm::vec4(glm::vec3(mesh3dLighting.ambient), mesh3dMetallic);
4241 for (int i = 0; i < Lighting3DPack::kMaxLights; ++i) ubo.lights[i] = mesh3dLighting.lights[i];
4242 for (size_t i = 0; i < mesh3dLighting.diffuseProbeSh.size(); ++i)
4243 ubo.diffuseProbeSh[i] = mesh3dLighting.diffuseProbeSh[i];
4244 ubo.diffuseProbeInfo.x = mesh3dLighting.diffuseProbeShEnabled ? 1.f : 0.f;
4245 for (size_t i = 0; i < mesh3dLighting.diffuseVolumePosition.size(); ++i) {
4246 ubo.diffuseVolumePosition[i] = mesh3dLighting.diffuseVolumePosition[i];
4247 ubo.diffuseVolumeExtent[i] = mesh3dLighting.diffuseVolumeExtent[i];
4248 }
4249 for (size_t i = 0; i < mesh3dLighting.diffuseVolumeSh.size(); ++i)
4250 ubo.diffuseVolumeSh[i] = mesh3dLighting.diffuseVolumeSh[i];
4251 ubo.diffuseVolumeInfo.x = static_cast<float>(mesh3dLighting.diffuseVolumeProbeCount);
4252 ubo.diffuseVolumeInfo.y = mesh3dLighting.diffuseVolumeTrilinearCell ? 1.f : 0.f;
4253 ubo.lodFade = d.lodFade;
4254 float surfaceCode = float(int(d.surfaceMode));
4255 if (d.surfaceMode == SurfaceMode::Masked && d.alphaTechnique == "dither")
4256 surfaceCode = 3.f;
4257 else if (d.surfaceMode == SurfaceMode::Masked && d.alphaTechnique == "coverage")
4258 surfaceCode = 4.f;
4259 ubo.texBomb = glm::vec4(mesh3dTexBombScale, mesh3dTexBombStrength, mesh3dTexBombRot, 0.f);
4260 ubo.parallax = glm::vec4(mesh3dParallaxScale, mesh3dParallaxMin, mesh3dParallaxMax, d.alphaCutoff);
4261 ubo.virtualTexture = d.virtualTexture;
4262 ubo.virtualAtlas = d.virtualAtlas;
4263 const float aoStrength = (renderControl_ && renderControl_->isEnabled("ao")) ? mesh3dSsaoIntensity : 0.f;
4264 ubo.surface = glm::vec4(surfaceCode, d.alphaCutoff, aoStrength, 0.f);
4265 ubo.view = mesh3dView;
4266 ubo.clipInfo = glm::vec4(mesh3dNear, mesh3dFar, 0.f, 0.f);
4267 ubo.cloud = mesh3dCloud;
4268 ubo.cloudWind = mesh3dCloudWind;
4269 ubo.envProbeCenter = glm::vec4(mesh3dEnvProbeCenter, 1.f);
4270 ubo.envProbeExtent = glm::vec4(mesh3dEnvProbeExtent, 0.f);
4271 for (int i = 0; i < ReflectionProbeUpload::kMaxProbes; ++i) {
4272 if (i >= mesh3dReflectionProbes.count) continue;
4273 const auto& probe = mesh3dReflectionProbes.probes[i];
4274 GpuTexture* gpu = gpuForTexture(probe.cubemap);
4275 if (!gpu || !gpu->isCube) continue;
4276 ubo.reflectionProbeCenter[i] = glm::vec4(probe.center, probe.intensity);
4277 ubo.reflectionProbeExtent[i] = glm::vec4(probe.extent, probe.blendDistance);
4278 }
4279 ubo.lightColor.w = mesh3dEnvIntensity;
4280 if (d.mesh && d.mesh->hasGpuSkinning()) ubo.skinInfo.x = static_cast<float>(d.mesh->getSkinPaletteCount());
4281 ubo.skinInfo.y = static_cast<float>(d.skinInfluenceLimit);
4282 d.skinBuffer = uploadSkinPalette(d.mesh);
4283 // X-ray params travel through the Frame UBO (no extra binding). Packed
4284 // in bindMeshUniforms("xray") order: colorR..G..B, bias, screenW, screenH,
4285 // rimPower, rimStrength, alpha.
4286 if (d.shader && d.shader->isXray() && d.shader->pushConstantSize() >= 9 * sizeof(float)) {
4287 const float* pc = d.shader->pushConstantData();
4288 ubo.texBomb = glm::vec4(pc[0], pc[1], pc[2], pc[8]); // color.xyz + alpha
4289 ubo.parallax = glm::vec4(pc[3], pc[4], pc[5], pc[7]); // bias, screenW, screenH, rimStrength
4290 ubo.clipInfo.z = pc[6]; // rimPower
4291 }
4292 queue.WriteBuffer(uboArena.buffer, d.frameUboOffset, &ubo, sizeof(ubo));
4293 if (d.shader && d.shader->pushConstantSize() > 0)
4294 queue.WriteBuffer(uboArena.buffer, d.pushUboOffset, d.shader->pushConstantData(),
4296 }
4297
4298 // Capture receive-shadow per draw; RenderSystem3D changes it between queued meshes.
4299 for (auto& d : mesh3dDraws) {
4300 ShadowUBO shadowUbo = mesh3dShadows.ubo;
4301 if (!mesh3dShadows.active || !d.shadowReceive) shadowUbo.bias.y = 0.f;
4302 queue.WriteBuffer(uboArena.buffer, d.shadowUboOffset, &shadowUbo, sizeof(shadowUbo));
4303 }
4304
4305 for (auto& d : mesh3dDraws) {
4306 auto* gpuMesh = static_cast<GpuMesh*>(d.mesh->gpuHandle);
4307 if (!gpuMesh || !gpuMesh->vertexBuffer) continue;
4308
4309 const bool transparent = d.surfaceMode == SurfaceMode::Transparent;
4310 const BlendMode blend = transparent ? d.surfaceBlend : BlendMode::Opaque;
4311 const bool depthWrite = !transparent || d.depthWrite;
4312 // Canvas targets are 1-sample; scene pipelines follow the active MSAA
4313 // count. Both sets preserve the per-draw material raster state.
4314 const bool customShader = d.shader && d.shader->gpuHandle;
4315 if (d.pbrSurface && !customShader &&
4316 drawPbrMesh(pass, format, canvasTarget, d, *gpuMesh))
4317 continue;
4318 wgpu::RenderPipeline pipe;
4319 if (d.shader && d.shader->gpuHandle) {
4320 auto* gs = static_cast<GpuShader*>(d.shader->gpuHandle);
4321 if (gs->isMesh3D && gs->mesh3dPipeline) {
4322 if (d.shader->isXray() && gs->mesh3dXrayPipeline)
4323 pipe = gs->mesh3dXrayPipeline;
4324 else
4325 pipe = gs->mesh3dPipeline;
4326 }
4327 }
4328 if (!pipe) pipe = getMesh3DPipeline(blend, depthWrite, d.doubleSided, canvasTarget);
4329 const bool useClustered = !canvasTarget && !customShader && !d.doubleSided && mesh3dClusteredActive &&
4330 d.surfaceMode != SurfaceMode::Transparent && mesh3dClusteredPipeline &&
4331 d.clusteredUboOffset && !d.mesh->hasGpuSkinning();
4332 if (useClustered) pipe = mesh3dClusteredPipeline;
4333 if (!pipe) continue;
4334 pass.SetPipeline(pipe);
4335
4336 GpuTexture* albedo = gpuForTexture(d.texture);
4337 GpuTexture* normal = gpuForTexture(d.normalTexture);
4338 GpuTexture* env = gpuForTexture(mesh3dEnvTexture);
4339 GpuTexture* height = gpuForTexture(d.heightTexture);
4340 GpuTexture* depth = mesh3dSceneDepthTexture ? gpuForTexture(mesh3dSceneDepthTexture) : flatDepthTexture3D;
4341 GpuTexture* sceneColor = mesh3dSceneColorTexture ? gpuForTexture(mesh3dSceneColorTexture)
4342 : sceneColorHistoryValid && lastPresentSlot < sceneColorSlots.size()
4343 ? &sceneColorSlots[lastPresentSlot].colorGpu
4344 : whiteTexture;
4345 wgpu::BindGroup bg;
4346 uint32_t offsets[3];
4347 if (useClustered) {
4348 wgpu::TextureView aoView_ =
4349 aoReady ? aoView[(aoWriteIndex + 1) % 2] : (whiteTexture ? whiteTexture->view : wgpu::TextureView());
4350 bg = makeMesh3DClusteredBindGroup(albedo, normal, env, height, depth, aoView_, d.clusteredUboOffset,
4351 d.shadowUboOffset);
4352 offsets[0] = d.clusteredUboOffset;
4353 offsets[1] = d.shadowUboOffset;
4354 pass.SetBindGroup(0, bg, 2, offsets);
4355 } else {
4356 bg = makeMeshBindGroup(albedo, normal, env, height, depth, sceneColor, d.frameUboOffset, d.shadowUboOffset,
4357 d.pushUboOffset, d.skinBuffer);
4358 offsets[0] = d.frameUboOffset;
4359 offsets[1] = d.shadowUboOffset;
4360 offsets[2] = d.pushUboOffset;
4361 pass.SetBindGroup(0, bg, 3, offsets);
4362 }
4363
4364 if (gpuMesh->indexBuffer) {
4365 pass.SetVertexBuffer(0, gpuMesh->vertexBuffer, 0, uint64_t(gpuMesh->vertexCount) * gpuMesh->vertexStride);
4366 const uint64_t indexBytes = gpuMesh->indexFormat == wgpu::IndexFormat::Uint16 ? 2u : 4u;
4367 pass.SetIndexBuffer(gpuMesh->indexBuffer, gpuMesh->indexFormat, 0,
4368 uint64_t(gpuMesh->indexCount) * indexBytes);
4369 pass.DrawIndexed(gpuMesh->indexCount, 1, 0, 0, 0);
4370 } else {
4371 pass.SetVertexBuffer(0, gpuMesh->vertexBuffer, 0, uint64_t(gpuMesh->vertexCount) * gpuMesh->vertexStride);
4372 pass.Draw(gpuMesh->vertexCount, 1, 0, 0);
4373 }
4374 }
4375 mesh3dDraws.clear();
4376}
4377
4378void Graphics::flushShadowPass(wgpu::RenderPassEncoder pass, int cascade) {
4379 auto& uboArena = currentUboArena();
4380 if (cascade < 0 || cascade >= ShadowConfig::kTotalLayers) return;
4381 if (!mesh3dShadowPipeline) createShadowPipelines();
4382 ensureUboArena(uboArena, uboArena.used + shadowCascadeDraws[cascade].size() * 256);
4383 for (auto& d : shadowCascadeDraws[cascade]) {
4384 auto* gpuMesh = static_cast<GpuMesh*>(d.mesh->gpuHandle);
4385 if (!gpuMesh || !gpuMesh->vertexBuffer) continue;
4386
4387 pass.SetPipeline(d.alphaTest
4388 ? (d.doubleSided ? mesh3dShadowAlphaPipeline : mesh3dShadowAlphaSingleSidedPipeline)
4389 : (d.doubleSided ? mesh3dShadowPipeline : mesh3dShadowSingleSidedPipeline));
4390
4391 SkinPassUBO ubo;
4392 ubo.mvp = d.mvp;
4393 ubo.clip = d.lodFade;
4394 if (d.mesh->hasGpuSkinning()) ubo.skinInfo.x = static_cast<float>(d.mesh->getSkinPaletteCount());
4395 ubo.skinInfo.y = static_cast<float>(d.skinInfluenceLimit);
4396 const auto skinBuffer = uploadSkinPalette(d.mesh);
4397 uint32_t offset = uboArena.alloc(sizeof(SkinPassUBO), 256);
4398 queue.WriteBuffer(uboArena.buffer, offset, &ubo, sizeof(ubo));
4399
4400 GpuTexture* albedo = gpuForTextureOrWhite(d.albedo);
4401 WGPUBindGroupEntry entries[4]{};
4402 entries[0].binding = 0;
4403 entries[0].buffer = uboArena.buffer.Get();
4404 entries[0].size = sizeof(SkinPassUBO);
4405 entries[1].binding = 1;
4406 entries[1].textureView = albedo->view.Get();
4407 entries[2].binding = 2;
4408 entries[2].sampler = albedo->sampler.Get();
4409 entries[3].binding = 3;
4410 entries[3].buffer = skinBuffer.Get();
4411 entries[3].size = skinBuffer.GetSize();
4412 WGPUBindGroupDescriptor bgd{};
4413 bgd.layout = shadowSetLayout.Get();
4414 bgd.entryCount = 4;
4415 bgd.entries = entries;
4416 wgpu::BindGroup bg = device.CreateBindGroup(reinterpret_cast<const wgpu::BindGroupDescriptor*>(&bgd));
4417 uint32_t offsets[1] = {offset};
4418 pass.SetBindGroup(0, bg, 1, offsets);
4419
4420 if (gpuMesh->indexBuffer) {
4421 pass.SetVertexBuffer(0, gpuMesh->vertexBuffer, 0, uint64_t(gpuMesh->vertexCount) * gpuMesh->vertexStride);
4422 const uint64_t indexBytes = gpuMesh->indexFormat == wgpu::IndexFormat::Uint16 ? 2u : 4u;
4423 pass.SetIndexBuffer(gpuMesh->indexBuffer, gpuMesh->indexFormat, 0,
4424 uint64_t(gpuMesh->indexCount) * indexBytes);
4425 pass.DrawIndexed(gpuMesh->indexCount, 1, 0, 0, 0);
4426 } else {
4427 pass.SetVertexBuffer(0, gpuMesh->vertexBuffer, 0, uint64_t(gpuMesh->vertexCount) * gpuMesh->vertexStride);
4428 pass.Draw(gpuMesh->vertexCount, 1, 0, 0);
4429 }
4430 }
4431 shadowCascadeDraws[cascade].clear();
4432}
4433
4434void Graphics::flushGbufferPass(wgpu::RenderPassEncoder pass) {
4435 if (gbufferPassDraws.empty() || gbufferSlots.empty()) return;
4436 if (!mesh3dGbufferPipeline) createGbufferPipelines();
4437 auto& uboArena = currentUboArena();
4438 ensureUboArena(uboArena, uboArena.used + gbufferPassDraws.size() * 256);
4439 for (auto& d : gbufferPassDraws) {
4440 auto* gpuMesh = static_cast<GpuMesh*>(d.mesh->gpuHandle);
4441 if (!gpuMesh || !gpuMesh->vertexBuffer) continue;
4442
4443 pass.SetPipeline(d.alphaTest ? mesh3dGbufferAlphaPipeline : mesh3dGbufferPipeline);
4444
4445 SkinPassUBO ubo;
4446 ubo.mvp = d.mvp;
4447 ubo.model = d.model;
4448 auto u6 = [](float value) { return uint32_t(std::lround(std::clamp(value, 0.f, 1.f) * 63.f)); };
4449 // Phase B: tint RGB6 | rough7 | metal7 — bit-cast (exceeds float's 24-bit exact ints).
4450 const uint32_t rough7 = uint32_t(std::lround(std::clamp(d.roughness, 0.f, 1.f) * 127.f));
4451 const uint32_t metal7 = uint32_t(std::lround(std::clamp(d.metallic, 0.f, 1.f) * 127.f));
4452 const uint32_t packedTint =
4453 u6(d.tint.r) | (u6(d.tint.g) << 6) | (u6(d.tint.b) << 12) | (rough7 << 18) | (metal7 << 25);
4454 auto motion12 = [](float value) {
4455 return uint32_t(std::lround(std::clamp(value, -1.f, 1.f) * 2047.f)) + 2047u;
4456 };
4457 const uint32_t packedMotion = motion12(d.motion.x) | (motion12(d.motion.y) << 12);
4458 ubo.clip = glm::vec4(d.nearZ, d.farZ, std::bit_cast<float>(packedTint), float(packedMotion));
4459 if (d.mesh->hasGpuSkinning()) ubo.skinInfo.x = static_cast<float>(d.mesh->getSkinPaletteCount());
4460 ubo.skinInfo.y = static_cast<float>(d.skinInfluenceLimit);
4461 const auto skinBuffer = uploadSkinPalette(d.mesh);
4462
4463 uint32_t offset = uboArena.alloc(sizeof(SkinPassUBO), 256);
4464 queue.WriteBuffer(uboArena.buffer, offset, &ubo, sizeof(ubo));
4465
4466 GpuTexture* albedo = gpuForTextureOrWhite(d.albedo);
4467 WGPUBindGroupEntry entries[4]{};
4468 entries[0].binding = 0;
4469 entries[0].buffer = uboArena.buffer.Get();
4470 entries[0].size = sizeof(SkinPassUBO);
4471 entries[1].binding = 1;
4472 entries[1].textureView = albedo->view.Get();
4473 entries[2].binding = 2;
4474 entries[2].sampler = albedo->sampler.Get();
4475 entries[3].binding = 3;
4476 entries[3].buffer = skinBuffer.Get();
4477 entries[3].size = skinBuffer.GetSize();
4478 WGPUBindGroupDescriptor bgd{};
4479 bgd.layout = gbufferSetLayout.Get();
4480 bgd.entryCount = 4;
4481 bgd.entries = entries;
4482 wgpu::BindGroup bg = device.CreateBindGroup(reinterpret_cast<const wgpu::BindGroupDescriptor*>(&bgd));
4483 uint32_t offsets[1] = {offset};
4484 pass.SetBindGroup(0, bg, 1, offsets);
4485
4486 if (gpuMesh->indexBuffer) {
4487 pass.SetVertexBuffer(0, gpuMesh->vertexBuffer, 0, uint64_t(gpuMesh->vertexCount) * gpuMesh->vertexStride);
4488 const uint64_t indexBytes = gpuMesh->indexFormat == wgpu::IndexFormat::Uint16 ? 2u : 4u;
4489 pass.SetIndexBuffer(gpuMesh->indexBuffer, gpuMesh->indexFormat, 0,
4490 uint64_t(gpuMesh->indexCount) * indexBytes);
4491 pass.DrawIndexed(gpuMesh->indexCount, 1, 0, 0, 0);
4492 } else {
4493 pass.SetVertexBuffer(0, gpuMesh->vertexBuffer, 0, uint64_t(gpuMesh->vertexCount) * gpuMesh->vertexStride);
4494 pass.Draw(gpuMesh->vertexCount, 1, 0, 0);
4495 }
4496 }
4497 gbufferPassDraws.clear();
4498 gbufferPassPending = false;
4499}
4500
4501void Graphics::flushDecalPass(wgpu::RenderPassEncoder pass) {
4502 if (decalPassDraws.empty() || gbufferSlots.empty()) {
4503 decalPassPending = false;
4504 return;
4505 }
4506 if (!decalPipeline) createDecalPipeline();
4507 auto& uboArena = currentUboArena();
4508 ensureUboArena(uboArena, uboArena.used + decalPassDraws.size() * 512);
4509 GbufferSlot& gbuffer = gbufferSlots[lastGbufferSlot];
4510 pass.SetPipeline(decalPipeline);
4511 struct DecalUniforms {
4512 glm::mat4 invViewProj;
4513 glm::mat4 invModel;
4514 glm::vec4 modelR0;
4515 glm::vec4 modelR1;
4516 glm::vec4 modelR2;
4517 glm::vec4 uvRect;
4518 glm::vec4 fadeParams;
4519 glm::vec4 extraParams;
4520 glm::vec4 surfaceParams;
4521 glm::vec4 texel;
4522 };
4523 static_assert(sizeof(DecalUniforms) == 256);
4524 for (const auto& draw : decalPassDraws) {
4525 DecalUniforms uniforms{};
4526 uniforms.invViewProj = glm::inverse(decalViewProj);
4527 uniforms.invModel = glm::inverse(draw.model);
4528 uniforms.modelR0 = draw.model[0];
4529 uniforms.modelR1 = draw.model[1];
4530 uniforms.modelR2 = draw.model[2];
4531 uniforms.uvRect = draw.uvRect;
4532 uniforms.fadeParams = draw.fadeParams;
4533 uniforms.extraParams = draw.extraParams;
4534 uniforms.surfaceParams = draw.surfaceParams;
4535 uniforms.texel = glm::vec4(1.f / float(decalWidth), 1.f / float(decalHeight), 0.f, 0.f);
4536 uint32_t offset = uboArena.alloc(256, 256);
4537 queue.WriteBuffer(uboArena.buffer, offset, &uniforms, sizeof(uniforms));
4538 GpuTexture* albedo = gpuForTextureOrWhite(draw.albedo);
4539 GpuTexture* normal = gpuForTextureOrWhite(draw.normal);
4540 GpuTexture* params = gpuForTextureOrWhite(draw.params);
4541 WGPUBindGroupEntry entries[7]{};
4542 entries[0].binding = 0;
4543 entries[0].buffer = uboArena.buffer.Get();
4544 entries[0].size = sizeof(uniforms);
4545 entries[1].binding = 1;
4546 entries[1].textureView = albedo->view.Get();
4547 entries[2].binding = 2;
4548 entries[2].textureView = normal->view.Get();
4549 entries[3].binding = 3;
4550 entries[3].textureView = params->view.Get();
4551 entries[4].binding = 4;
4552 entries[4].textureView = gbuffer.depthView.Get();
4553 entries[5].binding = 5;
4554 entries[5].textureView = gbuffer.normalView.Get();
4555 entries[6].binding = 6;
4556 entries[6].sampler = mainSampler.Get();
4557 WGPUBindGroupDescriptor descriptor{};
4558 descriptor.layout = decalSetLayout.Get();
4559 descriptor.entryCount = 7;
4560 descriptor.entries = entries;
4561 wgpu::BindGroup group = device.CreateBindGroup(reinterpret_cast<const wgpu::BindGroupDescriptor*>(&descriptor));
4562 uint32_t offsets[1] = {offset};
4563 pass.SetBindGroup(0, group, 1, offsets);
4564 pass.Draw(3, 1, 0, 0);
4565 }
4566 decalPassDraws.clear();
4567 decalPassPending = false;
4568 decalReady = true;
4569 clearMeshBindGroupCache();
4570}
4571
4572bool Graphics::flushGBufferPassInto(wgpu::CommandEncoder& encoder, bool runAo) {
4573 if (!gbufferPassPending || gbufferSlots.empty()) return false;
4574 lastGbufferSlot = currentFrameSlot();
4575 GbufferSlot& slot = gbufferSlots[lastGbufferSlot];
4576 WGPURenderPassColorAttachment colorAtts[5]{};
4577 for (int i = 0; i < 5; ++i) {
4578 colorAtts[i].depthSlice = WGPU_DEPTH_SLICE_UNDEFINED;
4579 colorAtts[i].loadOp = WGPULoadOp_Clear;
4580 colorAtts[i].storeOp = WGPUStoreOp_Store;
4581 colorAtts[i].clearValue = {0.f, 0.f, 0.f, 1.f};
4582 }
4583 // Match Vulkan GBuffer clears: depthColor=1, pbrParams=(0,1,1,1).
4584 colorAtts[1].clearValue = {1.f, 1.f, 1.f, 1.f};
4585 colorAtts[3].clearValue = {0.f, 1.f, 1.f, 1.f};
4586 colorAtts[0].view = slot.normalView.Get();
4587 colorAtts[1].view = slot.depthColorView.Get();
4588 colorAtts[2].view = slot.albedoView.Get();
4589 colorAtts[3].view = slot.pbrParamsView.Get();
4590 colorAtts[4].view = slot.emissiveView.Get();
4591 WGPURenderPassDepthStencilAttachment ds{};
4592 ds.view = slot.depthView.Get();
4593 ds.depthClearValue = 1.f;
4594 ds.depthLoadOp = WGPULoadOp_Clear;
4595 ds.depthStoreOp = WGPUStoreOp_Store;
4596 ds.stencilClearValue = 0;
4597 ds.stencilLoadOp = WGPULoadOp_Undefined;
4598 ds.stencilStoreOp = WGPUStoreOp_Undefined;
4599 WGPURenderPassDescriptor rp{};
4600 rp.colorAttachmentCount = 5;
4601 rp.colorAttachments = colorAtts;
4602 rp.depthStencilAttachment = &ds;
4603 wgpu::RenderPassEncoder pass = encoder.BeginRenderPass(reinterpret_cast<const wgpu::RenderPassDescriptor*>(&rp));
4604 flushGbufferPass(pass);
4605 pass.End();
4606 gbufferPassPending = false;
4607 gbufferDepthValid_ = true;
4608
4609 if (runAo) {
4610 ensureAOResources(sceneColorWidth, sceneColorHeight);
4611 if (aoPipeline && aoTex[0]) {
4613 GbufferSlot& gslot = gbufferSlots[currentFrameSlot()];
4614 struct AOUbo {
4615 glm::vec4 params; // radius, power, nearZ, farZ
4616 float intensity;
4617 float invScale; // AO target size / depth size
4618 float pad;
4619 } aou;
4620 aou.params = glm::vec4(0.05f, 1.1f, mesh3dNear, mesh3dFar);
4621 aou.intensity = 1.0f;
4622 aou.invScale = 0.5f;
4623 aou.pad = 0.f;
4624 queue.WriteBuffer(aoUbo, 0, &aou, sizeof(aou));
4625 wgpu::BindGroup aoBg = makeAOBindGroup(gslot.depthView);
4626
4627 WGPURenderPassColorAttachment colorAtt{};
4628 colorAtt.view = aoView[aoWriteIndex].Get();
4629 colorAtt.depthSlice = WGPU_DEPTH_SLICE_UNDEFINED;
4630 colorAtt.loadOp = WGPULoadOp_Clear;
4631 colorAtt.storeOp = WGPUStoreOp_Store;
4632 colorAtt.clearValue = {1.f, 1.f, 1.f, 1.f};
4633 WGPURenderPassDescriptor aoRp{};
4634 aoRp.colorAttachmentCount = 1;
4635 aoRp.colorAttachments = &colorAtt;
4636 wgpu::RenderPassEncoder apass =
4637 encoder.BeginRenderPass(reinterpret_cast<const wgpu::RenderPassDescriptor*>(&aoRp));
4638 apass.SetPipeline(aoPipeline);
4639 apass.SetBindGroup(0, aoBg, 0, nullptr);
4640 apass.SetVertexBuffer(0, fullscreenQuadVb, 0, 4 * 32);
4641 apass.SetIndexBuffer(fullscreenQuadIb, wgpu::IndexFormat::Uint32, 0, 24);
4642 apass.DrawIndexed(6, 1, 0, 0, 0);
4643 apass.End();
4645 aoWriteIndex ^= 1;
4646 }
4647 }
4648 return true;
4649}
4650
4651void Graphics::submitPendingDeferredPasses() {
4652 if ((!gbufferPassPending || gbufferSlots.empty()) && (!decalPassPending || decalSlots.empty())) return;
4653 auto& uboArena = currentUboArena();
4654 uboArena.reset();
4655 ensureUboArena(uboArena, 4096 + (gbufferPassDraws.size() + decalPassDraws.size()) * 512);
4656 wgpu::CommandEncoder encoder = device.CreateCommandEncoder();
4657 if (gbufferPassPending && !gbufferSlots.empty()) {
4658 lastGbufferSlot = currentFrameSlot();
4659 GbufferSlot& slot = gbufferSlots[lastGbufferSlot];
4660 WGPURenderPassColorAttachment colors[5]{};
4661 for (auto& color : colors) {
4662 color.depthSlice = WGPU_DEPTH_SLICE_UNDEFINED;
4663 color.loadOp = WGPULoadOp_Clear;
4664 color.storeOp = WGPUStoreOp_Store;
4665 color.clearValue = {0.f, 0.f, 0.f, 0.f};
4666 }
4667 colors[1].clearValue = {1.f, 1.f, 1.f, 1.f};
4668 colors[3].clearValue = {0.f, 1.f, 1.f, 1.f}; // metal=0, rough/ao/spec=1
4669 colors[0].view = slot.normalView.Get();
4670 colors[1].view = slot.depthColorView.Get();
4671 colors[2].view = slot.albedoView.Get();
4672 colors[3].view = slot.pbrParamsView.Get();
4673 colors[4].view = slot.emissiveView.Get();
4674 WGPURenderPassDepthStencilAttachment depth{};
4675 depth.view = slot.depthView.Get();
4676 depth.depthClearValue = 1.f;
4677 depth.depthLoadOp = WGPULoadOp_Clear;
4678 depth.depthStoreOp = WGPUStoreOp_Store;
4679 depth.stencilLoadOp = WGPULoadOp_Undefined;
4680 depth.stencilStoreOp = WGPUStoreOp_Undefined;
4681 WGPURenderPassDescriptor descriptor{};
4682 descriptor.colorAttachmentCount = 5;
4683 descriptor.colorAttachments = colors;
4684 descriptor.depthStencilAttachment = &depth;
4685 wgpu::RenderPassEncoder pass =
4686 encoder.BeginRenderPass(reinterpret_cast<const wgpu::RenderPassDescriptor*>(&descriptor));
4687 flushGbufferPass(pass);
4688 pass.End();
4689 gbufferDepthValid_ = true;
4690 }
4691 if (decalPassPending && !decalSlots.empty() && !gbufferSlots.empty()) {
4692 lastDecalSlot = currentFrameSlot();
4693 DecalSlot& slot = decalSlots[lastDecalSlot];
4694 WGPURenderPassColorAttachment colors[3]{};
4695 for (auto& color : colors) {
4696 color.depthSlice = WGPU_DEPTH_SLICE_UNDEFINED;
4697 color.loadOp = WGPULoadOp_Clear;
4698 color.storeOp = WGPUStoreOp_Store;
4699 color.clearValue = {0.f, 0.f, 0.f, 0.f};
4700 }
4701 colors[0].view = slot.albedoView.Get();
4702 colors[1].view = slot.normalView.Get();
4703 colors[2].view = slot.paramsView.Get();
4704 WGPURenderPassDescriptor descriptor{};
4705 descriptor.colorAttachmentCount = 3;
4706 descriptor.colorAttachments = colors;
4707 wgpu::RenderPassEncoder pass =
4708 encoder.BeginRenderPass(reinterpret_cast<const wgpu::RenderPassDescriptor*>(&descriptor));
4709 flushDecalPass(pass);
4710 pass.End();
4711 }
4712 wgpu::CommandBuffer command = encoder.Finish();
4713 queue.Submit(1, &command);
4714}
4715
4716
4717// ---------------------------------------------------------------------------
4718// Present
4719// ---------------------------------------------------------------------------
4720
4721bool Graphics::acquireSurfaceTexture(wgpu::TextureView& view, wgpu::Texture& texture) {
4722 if (!surface || !device || !swapchainConfigured) return false;
4723 wgpu::SurfaceTexture surfTex{};
4724 surface.GetCurrentTexture(&surfTex);
4725 if (!surfTex.texture) return false;
4726 texture = surfTex.texture;
4727 view = texture.CreateView();
4728 return bool(view);
4729}
4730
4732#ifdef EVENGINE_WEBGPU
4733 if (device) device.PushErrorScope(wgpu::ErrorFilter::Validation);
4734#endif
4735}
4736
4738#ifdef EVENGINE_WEBGPU
4739 if (!device) return;
4740 device.PopErrorScope(wgpu::CallbackMode::AllowProcessEvents, [](wgpu::PopErrorScopeStatus status,
4741 wgpu::ErrorType type, wgpu::StringView message) {
4742 (void)status;
4743 if (message.data && type != wgpu::ErrorType::NoError) {
4744#if defined(__EMSCRIPTEN__)
4745 EM_ASM({ console.log("[GPU_ERR] type=" + $0 + " msg=" + UTF8ToString($1)); }, (int)type, message.data);
4746#else
4747 std::fprintf(stderr, "[webgpu] validation error type=%d: %.*s\n", int(type),
4748 static_cast<int>(message.length), message.data);
4749#endif
4750 }
4751 });
4752#endif
4753}
4754
4755
4758 if (!device || !surface || !swapchainConfigured) return;
4759 pumpReadback();
4760 rebuildSwapchainIfNeeded();
4761 if (!swapchainConfigured) return;
4762 // RenderControl can toggle MSAA without resizing the window. Re-evaluate
4763 // the offscreen sample count every frame, matching the Vulkan backend.
4764 if (sceneColorWidth > 0 && sceneColorHeight > 0) createSceneColorResources(sceneColorWidth, sceneColorHeight);
4765 const bool aoActive = renderControl_ && renderControl_->isEnabled("ao");
4766
4767 wgpu::TextureView surfaceView;
4768 wgpu::Texture surfaceTex;
4769 if (!acquireSurfaceTexture(surfaceView, surfaceTex)) {
4770 return;
4771 }
4772
4773 auto& uboArena = currentUboArena();
4774 uboArena.reset();
4775 auto& vtxArena = currentVertexArena();
4776 vtxArena.reset();
4777 voxelInstanceArena.used = 0;
4778 voxelAoArena.used = 0;
4779 ensureUboArena(uboArena, 4096);
4780 ensureVertexArena(vtxArena, 4096);
4781 // Match the desktop (Vulkan) flow: the 3D/swapchain pass clears with the
4782 // background set by setBackgroundColor unless the script called clear().
4783 if (!hasPendingClear) clearColor = backgroundColor;
4784 hasPendingClear = false;
4785
4786 wgpu::CommandEncoder encoder = device.CreateCommandEncoder();
4787 // Compute compaction writes the visible-model table and indexed-indirect
4788 // commands consumed by the scene render pass below. Ending the compute
4789 // pass establishes the required WebGPU storage-to-indirect dependency.
4790 recordGpuDrivenCompute(encoder);
4791
4792 // 1. Shadow passes (CSM cascade layers).
4793 if (shadowDepthArray) {
4794 // Every cascade is sampled by receivers, including cascades with no
4795 // caster draw this frame. Clear all layers while shadows are active so
4796 // an empty cascade deterministically means fully lit (depth 1) instead
4797 // of retaining undefined or previous-frame depth.
4798 if (mesh3dShadows.active) {
4799 for (int c = 0; c < ShadowConfig::kCascades; ++c) {
4800 WGPUTextureViewDescriptor lvd{};
4801 lvd.format = WGPUTextureFormat_Depth32Float;
4802 lvd.dimension = WGPUTextureViewDimension_2D;
4803 lvd.baseMipLevel = 0;
4804 lvd.mipLevelCount = 1;
4805 lvd.baseArrayLayer = static_cast<uint32_t>(c);
4806 lvd.arrayLayerCount = 1;
4807 wgpu::TextureView layerView =
4808 shadowDepthArray->texture.CreateView(reinterpret_cast<const wgpu::TextureViewDescriptor*>(&lvd));
4809 WGPURenderPassDepthStencilAttachment ds{};
4810 ds.view = layerView.Get();
4811 ds.depthClearValue = 1.f;
4812 ds.depthLoadOp = WGPULoadOp_Clear;
4813 ds.depthStoreOp = WGPUStoreOp_Store;
4814 ds.stencilClearValue = 0;
4815 ds.stencilLoadOp = WGPULoadOp_Undefined;
4816 ds.stencilStoreOp = WGPUStoreOp_Undefined;
4817 WGPURenderPassDescriptor rp{};
4818 rp.depthStencilAttachment = &ds;
4819 wgpu::RenderPassEncoder pass =
4820 encoder.BeginRenderPass(reinterpret_cast<const wgpu::RenderPassDescriptor*>(&rp));
4821 flushShadowPass(pass, c);
4822 pass.End();
4823 }
4824 }
4825 }
4826
4827 // Stage-3 visibility pass consumes the compute-compacted instance table
4828 // before the scene pass resolves material shading.
4829 recordGpuDrivenVisibility(encoder);
4830
4831 // Hybrid: GBuffer must be written before deferred lighting samples it into
4832 // scene color. Forward+ keeps the historical order (GBuffer after scene).
4833 const bool wantDeferredLighting = deferredLightingPending_;
4834 if (wantDeferredLighting) flushGBufferPassInto(encoder, aoActive);
4835
4836 // 2. Scene color pass (3D) into the offscreen target.
4837 wgpu::TextureView sceneView;
4838 wgpu::Texture sceneTex;
4839 if (frameHad3DThisFrame) {
4840 if (active3DCanvas) {
4841 // 3D into an offscreen canvas: render the mesh pass into the
4842 // canvas color/depth (RGBA8Unorm, matches the mesh3d pipelines).
4843 OffscreenCanvas* oc = active3DCanvas;
4844 WGPURenderPassColorAttachment colorAtt{};
4845 colorAtt.view = oc->colorView.Get();
4846 colorAtt.depthSlice = WGPU_DEPTH_SLICE_UNDEFINED;
4847 colorAtt.loadOp = WGPULoadOp_Clear;
4848 colorAtt.storeOp = WGPUStoreOp_Store;
4849 colorAtt.clearValue = {clearColor.r, clearColor.g, clearColor.b, 1.f};
4850 WGPURenderPassDepthStencilAttachment ds{};
4851 ds.view = oc->depthView.Get();
4852 ds.depthClearValue = 1.f;
4853 ds.depthLoadOp = WGPULoadOp_Clear;
4854 ds.depthStoreOp = WGPUStoreOp_Store;
4855 ds.stencilClearValue = 0;
4856 ds.stencilLoadOp = WGPULoadOp_Undefined;
4857 ds.stencilStoreOp = WGPUStoreOp_Undefined;
4858 WGPURenderPassDescriptor rp{};
4859 rp.colorAttachmentCount = 1;
4860 rp.colorAttachments = &colorAtt;
4861 rp.depthStencilAttachment = &ds;
4862 wgpu::RenderPassEncoder pass =
4863 encoder.BeginRenderPass(reinterpret_cast<const wgpu::RenderPassDescriptor*>(&rp));
4864 // Voxel draws are not expected on the canvas path (voxel module is
4865 // trimmed from the web build), and its pipeline follows the scene
4866 // sample count which would mismatch the 1x canvas attachment.
4867 flushMesh3D(pass, WGPUTextureFormat_RGBA8Unorm, /*canvasTarget*/ true);
4868 flushGpuDrivenDraws(pass, /*canvasTarget*/ true);
4869 flushPrimitive3D(pass, WGPUTextureFormat_RGBA8Unorm, 1);
4870 pass.End();
4871 oc->clearRequested = false;
4872 // The script draws the canvas texture explicitly (2D path); it is
4873 // not composited into the swapchain.
4874 lastReadbackTex = oc->color;
4875 lastReadbackW = oc->getWidth();
4876 lastReadbackH = oc->getHeight();
4877 } else if (!sceneColorSlots.empty()) {
4878 SceneColorSlot& slot = sceneColorSlots[currentFrameSlot()];
4879 std::vector<Mesh3dDraw> refractiveDraws;
4880 const bool splitSceneColor = mesh3dSceneColorCaptureIndex.has_value() &&
4881 *mesh3dSceneColorCaptureIndex <= mesh3dDraws.size() &&
4882 sceneColorSlots.size() > 1 && slot.sampleCount == 1;
4883 if (splitSceneColor) {
4884 auto split = mesh3dDraws.begin() + static_cast<std::ptrdiff_t>(*mesh3dSceneColorCaptureIndex);
4885 refractiveDraws.assign(std::make_move_iterator(split), std::make_move_iterator(mesh3dDraws.end()));
4886 mesh3dDraws.erase(split, mesh3dDraws.end());
4887 }
4888 WGPURenderPassColorAttachment colorAtt{};
4889 colorAtt.view = slot.sampleCount > 1 ? slot.msaaView.Get() : slot.colorView.Get();
4890 colorAtt.resolveTarget = slot.sampleCount > 1 ? slot.colorView.Get() : nullptr;
4891 colorAtt.depthSlice = WGPU_DEPTH_SLICE_UNDEFINED;
4892 colorAtt.loadOp = WGPULoadOp_Clear;
4893 // When resolving to a single-sample target, the multisampled
4894 // attachment must be discarded (WebGPU spec).
4895 colorAtt.storeOp = slot.sampleCount > 1 ? WGPUStoreOp_Discard : WGPUStoreOp_Store;
4896 colorAtt.clearValue = {clearColor.r, clearColor.g, clearColor.b, 1.f};
4897 WGPURenderPassDepthStencilAttachment ds{};
4898 ds.view = slot.depthView.Get();
4899 ds.depthClearValue = 1.f;
4900 ds.depthLoadOp = WGPULoadOp_Clear;
4901 ds.depthStoreOp = WGPUStoreOp_Store;
4902 ds.stencilClearValue = 0;
4903 ds.stencilLoadOp = WGPULoadOp_Undefined;
4904 ds.stencilStoreOp = WGPUStoreOp_Undefined;
4905 WGPURenderPassDescriptor rp{};
4906 rp.colorAttachmentCount = 1;
4907 rp.colorAttachments = &colorAtt;
4908 rp.depthStencilAttachment = &ds;
4909 wgpu::RenderPassEncoder pass =
4910 encoder.BeginRenderPass(reinterpret_cast<const wgpu::RenderPassDescriptor*>(&rp));
4911 // Phase D Hybrid: clustered deferred lighting fills opaque scene
4912 // color + depth before transparent Forward+ mesh draws.
4913 if (wantDeferredLighting) flushDeferredLighting(pass);
4914 flushVoxelDraws(pass, sceneColorFormat);
4915 flushGpuDrivenResolve(pass);
4916 flushMesh3D(pass, sceneColorFormat);
4917 flushGpuDrivenDraws(pass, /*canvasTarget*/ false);
4918 flushPrimitive3D(pass, sceneColorFormat, slot.sampleCount);
4919 pass.End();
4920 if (splitSceneColor) {
4921 const uint32_t snapshotIndex =
4922 (currentFrameSlot() + 1u) % static_cast<uint32_t>(sceneColorSlots.size());
4923 SceneColorSlot& snapshot = sceneColorSlots[snapshotIndex];
4924 WGPUTexelCopyTextureInfo source{};
4925 source.texture = slot.color.Get();
4926 source.mipLevel = 0;
4927 source.origin = {0, 0, 0};
4928 source.aspect = WGPUTextureAspect_All;
4929 WGPUTexelCopyTextureInfo destination{};
4930 destination.texture = snapshot.color.Get();
4931 destination.mipLevel = 0;
4932 destination.origin = {0, 0, 0};
4933 destination.aspect = WGPUTextureAspect_All;
4934 WGPUExtent3D extent{static_cast<uint32_t>(sceneColorWidth), static_cast<uint32_t>(sceneColorHeight), 1};
4935 encoder.CopyTextureToTexture(reinterpret_cast<const wgpu::TexelCopyTextureInfo*>(&source),
4936 reinterpret_cast<const wgpu::TexelCopyTextureInfo*>(&destination),
4937 reinterpret_cast<const wgpu::Extent3D*>(&extent));
4938
4939 lastPresentSlot = snapshotIndex;
4940 sceneColorHistoryValid = true;
4941 mesh3dDraws = std::move(refractiveDraws);
4942 colorAtt.resolveTarget = nullptr;
4943 colorAtt.loadOp = WGPULoadOp_Load;
4944 colorAtt.storeOp = WGPUStoreOp_Store;
4945 ds.depthLoadOp = WGPULoadOp_Load;
4946 wgpu::RenderPassEncoder transparentPass =
4947 encoder.BeginRenderPass(reinterpret_cast<const wgpu::RenderPassDescriptor*>(&rp));
4948 flushMesh3D(transparentPass, sceneColorFormat);
4949 transparentPass.End();
4950 }
4951 mesh3dSceneColorCaptureIndex.reset();
4952 lastPresentSlot = currentFrameSlot();
4953 sceneColorHistoryValid = true;
4954 lastReadbackTex = slot.color;
4955 lastReadbackW = sceneColorWidth;
4956 lastReadbackH = sceneColorHeight;
4957 sceneView = slot.colorView;
4958 sceneTex = slot.color;
4959 }
4960 }
4961
4962 // 3. GBuffer pass (Forward+ / post-only path). Hybrid already flushed above.
4963 if (!wantDeferredLighting) flushGBufferPassInto(encoder, aoActive);
4964
4965 // 3c. Screen-space decals consume the freshly written GBuffer depth and
4966 // world normal, then write three transparent layer attachments.
4967 if (decalPassPending && !decalSlots.empty() && !gbufferSlots.empty()) {
4968 lastDecalSlot = currentFrameSlot();
4969 DecalSlot& slot = decalSlots[lastDecalSlot];
4970 WGPURenderPassColorAttachment colorAtts[3]{};
4971 for (auto& attachment : colorAtts) {
4972 attachment.depthSlice = WGPU_DEPTH_SLICE_UNDEFINED;
4973 attachment.loadOp = WGPULoadOp_Clear;
4974 attachment.storeOp = WGPUStoreOp_Store;
4975 attachment.clearValue = {0.f, 0.f, 0.f, 0.f};
4976 }
4977 colorAtts[0].view = slot.albedoView.Get();
4978 colorAtts[1].view = slot.normalView.Get();
4979 colorAtts[2].view = slot.paramsView.Get();
4980 WGPURenderPassDescriptor rp{};
4981 rp.colorAttachmentCount = 3;
4982 rp.colorAttachments = colorAtts;
4983 wgpu::RenderPassEncoder pass =
4984 encoder.BeginRenderPass(reinterpret_cast<const wgpu::RenderPassDescriptor*>(&rp));
4985 flushDecalPass(pass);
4986 pass.End();
4987 }
4988
4989 // 4. Active canvas: flush 2D batches into the offscreen target instead.
4990 if (activeCanvas) {
4991 auto* oc = static_cast<OffscreenCanvas*>(activeCanvas);
4992 flush2DToCanvas(oc);
4993 }
4994
4995 // 5. Swapchain pass: composite scene color, draw 2D, run the overlay.
4996 {
4997 WGPURenderPassColorAttachment colorAtt{};
4998 colorAtt.view = surfaceView.Get();
4999 colorAtt.depthSlice = WGPU_DEPTH_SLICE_UNDEFINED;
5000 colorAtt.loadOp = WGPULoadOp_Clear;
5001 colorAtt.storeOp = WGPUStoreOp_Store;
5002 colorAtt.clearValue = {clearColor.r, clearColor.g, clearColor.b, 1.f};
5003 WGPURenderPassDescriptor rp{};
5004 rp.colorAttachmentCount = 1;
5005 rp.colorAttachments = &colorAtt;
5006 wgpu::RenderPassEncoder pass =
5007 encoder.BeginRenderPass(reinterpret_cast<const wgpu::RenderPassDescriptor*>(&rp));
5008
5009 if (sceneView && !sceneColorComposited) {
5010 if (!fullscreenQuadReady) {
5011 // Textured vertex: pos(2) + color(4) + uv(2) = 32 bytes.
5012 float verts[32] = {
5013 -1.f, -1.f, 1.f, 1.f, 1.f, 1.f, 0.f, 0.f, 1.f, -1.f, 1.f, 1.f, 1.f, 1.f, 1.f, 0.f,
5014 1.f, 1.f, 1.f, 1.f, 1.f, 1.f, 1.f, 1.f, -1.f, 1.f, 1.f, 1.f, 1.f, 1.f, 0.f, 1.f,
5015 };
5016 uint32_t indices[6] = {0, 1, 2, 2, 3, 0};
5017 WGPUBufferDescriptor vbd{};
5018 vbd.label = sv("eve_fullscreen_vb");
5019 vbd.size = sizeof(verts);
5020 vbd.usage = WGPUBufferUsage_CopyDst | WGPUBufferUsage_Vertex;
5021 fullscreenQuadVb = device.CreateBuffer(reinterpret_cast<const wgpu::BufferDescriptor*>(&vbd));
5022 queue.WriteBuffer(fullscreenQuadVb, 0, verts, sizeof(verts));
5023 WGPUBufferDescriptor ibd{};
5024 ibd.label = sv("eve_fullscreen_ib");
5025 ibd.size = sizeof(indices);
5026 ibd.usage = WGPUBufferUsage_CopyDst | WGPUBufferUsage_Index;
5027 fullscreenQuadIb = device.CreateBuffer(reinterpret_cast<const wgpu::BufferDescriptor*>(&ibd));
5028 queue.WriteBuffer(fullscreenQuadIb, 0, indices, sizeof(indices));
5029 fullscreenQuadReady = true;
5030 }
5031
5032 GpuTexture sceneGpu;
5033 sceneGpu.texture = sceneTex;
5034 sceneGpu.view = sceneView;
5035 sceneGpu.sampler = createLinearSampler(device);
5036 wgpu::BindGroup bg = makeTex2DBindGroup(&sceneGpu, nullptr);
5037 uint32_t offsets[1] = {0};
5038 pass.SetPipeline(get2DTexturedPipeline(BlendMode::Alpha, false));
5039 pass.SetBindGroup(0, bg, 1, offsets);
5040 pass.SetVertexBuffer(0, fullscreenQuadVb, 0, 4 * 32);
5041 pass.SetIndexBuffer(fullscreenQuadIb, wgpu::IndexFormat::Uint32, 0, 24);
5042 pass.DrawIndexed(6, 1, 0, 0, 0);
5043 }
5044
5045 if (!activeCanvas) {
5046 flush2D(pass, pixelW > 0 ? pixelW : logicalW, pixelH > 0 ? pixelH : logicalH, surfaceFormat);
5047 }
5048
5050 WGPURenderPassEncoder cPass = pass.Get();
5052 }
5053 pass.End();
5054 }
5055
5056 if (screenReadbackEnabled) recordPresentedReadback(encoder, surfaceTex);
5057 wgpu::CommandBuffer cmd = encoder.Finish();
5058 queue.Submit(1, &cmd);
5059 // emdawnwebgpu implements the GPU on the JS main thread: queued commands
5060 // (draws, writes) only execute when ProcessEvents drives the work queue.
5061 // Without this, render-pass draws are silently never executed (only the
5062 // swapchain clear, handled by the browser present, is visible).
5063#ifdef __EMSCRIPTEN__
5064 if (instance) instance.ProcessEvents();
5065#endif
5066 // emdawnwebgpu does not implement wgpuSurfacePresent (it aborts); the
5067 // browser presents the canvas automatically once the command buffer is
5068 // submitted from within a requestAnimationFrame callback. On Emscripten
5069 // the engine frame is driven by emscripten_set_main_loop (rAF), so the
5070 // Squirrel main loop no longer blocks and no sleep is needed here.
5071#if !defined(__EMSCRIPTEN__)
5072 surface.Present();
5073#endif
5074
5075 frameIndex++;
5076 frame3DStarted = false;
5077 frameHad3DThisFrame = false;
5078 frameHad3D = false;
5079 sceneColorPassOpen = false;
5080 active3DCanvas = nullptr;
5081 gbufferPassPending = false;
5082 deferredLightingPending_ = false;
5083}
5084
5085// ---------------------------------------------------------------------------
5086// Canvas
5087// ---------------------------------------------------------------------------
5088
5090 auto* c = new OffscreenCanvas(this, width, height);
5091 ownedCanvases.push_back(std::unique_ptr<eve::graphics::Canvas>(c));
5092 return c;
5093}
5094
5096 auto* c = new OffscreenCanvas(this, width, height, true);
5097 ownedCanvases.push_back(std::unique_ptr<eve::graphics::Canvas>(c));
5098 return c;
5099}
5100
5102 if (activeCanvas && canvas != activeCanvas) {
5103 flush2DToCanvas(static_cast<OffscreenCanvas*>(activeCanvas));
5104 }
5105 activeCanvas = canvas;
5106}
5107
5108bool Graphics::isCanvasActive() const { return activeCanvas != nullptr; }
5109Canvas* Graphics::getCanvas() const { return activeCanvas; }
5110
5112 if (!canvas || !canvas->getTexture()) return;
5113 auto& uboArena = currentUboArena();
5114 uboArena.reset();
5115 auto& vtxArena = currentVertexArena();
5116 vtxArena.reset();
5117
5118 wgpu::CommandEncoder enc = device.CreateCommandEncoder();
5119 WGPURenderPassColorAttachment ca{};
5120 ca.view = canvas->colorView.Get();
5121 ca.depthSlice = WGPU_DEPTH_SLICE_UNDEFINED;
5122 ca.loadOp = canvas->clearRequested ? WGPULoadOp_Clear : WGPULoadOp_Load;
5123 ca.storeOp = WGPUStoreOp_Store;
5124 ca.clearValue = {canvas->clearColor.r, canvas->clearColor.g, canvas->clearColor.b, canvas->clearColor.a};
5125 WGPURenderPassDescriptor rp{};
5126 rp.colorAttachmentCount = 1;
5127 rp.colorAttachments = &ca;
5128 wgpu::RenderPassEncoder pass = enc.BeginRenderPass(reinterpret_cast<const wgpu::RenderPassDescriptor*>(&rp));
5129 flush2D(pass, canvas->getWidth(), canvas->getHeight(),
5130 canvas->isHDR() ? WGPUTextureFormat_RGBA16Float : WGPUTextureFormat_RGBA8Unorm);
5131 pass.End();
5132 wgpu::CommandBuffer cmd = enc.Finish();
5133 queue.Submit(1, &cmd);
5134 canvas->clearRequested = false;
5135}
5136
5138 if (activeCanvas) return activeCanvas->getTexture();
5139 return sceneColorTexture;
5140}
5141
5142void Graphics::draw(eve::graphics::Graphics* gfx, const glm::mat4& matrix) const {
5143 (void)gfx;
5144 (void)matrix;
5145}
5146
5147void Graphics::draw(Canvas* C, const glm::mat4& matrix) const {
5148 (void)C;
5149 (void)matrix;
5150}
5151
5152void Graphics::clear(std::optional<Color> color, std::optional<int> /*stencil*/, std::optional<double> /*depth*/) {
5153 if (frameHad3D && activeCanvas == nullptr) return;
5154 clearColor = color.value_or(backgroundColor);
5155 hasPendingClear = true;
5156 clear2DBatches();
5157 if (auto* canvas = dynamic_cast<OffscreenCanvas*>(activeCanvas)) {
5158 canvas->clear(clearColor, std::nullopt, std::nullopt);
5159 }
5160}
5161
5162// ---------------------------------------------------------------------------
5163// Shader creation
5164// ---------------------------------------------------------------------------
5165
5166namespace {
5167
5168wgpu::RenderPipeline buildPipelineFromWgsl(wgpu::Device& dev, wgpu::PipelineLayout layout, WGPUTextureFormat format,
5169 const std::string& vert, const std::string& frag, bool depth, bool blend,
5170 bool mesh3d, bool hair, bool shadow, bool gbuffer, uint32_t sampleCount,
5171 bool depthWrite = true) {
5172 WGPURenderPipelineDescriptor pd{};
5173 pd.label = sv("eve_custom_shader");
5174 pd.layout = layout.Get();
5175
5176 WGPUVertexAttribute attrs[6]{};
5177 WGPUVertexBufferLayout vb{};
5178 if (mesh3d || shadow || gbuffer) {
5179 fillMeshAttributes(attrs);
5180 attrs[0].format = WGPUVertexFormat_Float32x3;
5181 attrs[0].offset = 0;
5182 attrs[0].shaderLocation = 0;
5183 attrs[1].format = WGPUVertexFormat_Float32x3;
5184 attrs[1].offset = 12;
5185 attrs[1].shaderLocation = 1;
5186 attrs[2].format = WGPUVertexFormat_Float32x2;
5187 attrs[2].offset = 24;
5188 attrs[2].shaderLocation = 2;
5189 vb.arrayStride = sizeof(MeshVertex);
5190 vb.stepMode = WGPUVertexStepMode_Vertex;
5191 vb.attributeCount = 6;
5192 vb.attributes = attrs;
5193 } else {
5194 attrs[0].format = WGPUVertexFormat_Float32x2;
5195 attrs[0].offset = 0;
5196 attrs[0].shaderLocation = 0;
5197 attrs[1].format = WGPUVertexFormat_Float32x4;
5198 attrs[1].offset = 8;
5199 attrs[1].shaderLocation = 1;
5200 attrs[2].format = WGPUVertexFormat_Float32x2;
5201 attrs[2].offset = 24;
5202 attrs[2].shaderLocation = 2;
5203 vb.arrayStride = 32;
5204 vb.stepMode = WGPUVertexStepMode_Vertex;
5205 vb.attributeCount = 3;
5206 vb.attributes = attrs;
5207 }
5208 pd.vertex.bufferCount = 1;
5209 pd.vertex.buffers = &vb;
5210
5211 // Keep both modules alive until CreateRenderPipeline has consumed the
5212 // descriptor. Storing only their raw handles in pd and destroying the
5213 // wrappers at the end of these branches leaves dangling handles for Dawn.
5214 wgpu::ShaderModule vertexModule;
5215 wgpu::ShaderModule fragmentModule;
5216 if (!vert.empty()) {
5217 WGPUShaderModuleDescriptor md = mdDesc(vert);
5218 vertexModule = dev.CreateShaderModule(reinterpret_cast<const wgpu::ShaderModuleDescriptor*>(&md));
5219 pd.vertex.module = vertexModule.Get();
5220 pd.vertex.entryPoint = sv("vs_main");
5221 } else {
5222 // Default textured vertex shader.
5223 WGPUShaderModuleDescriptor md = mdDesc(kTexturedVertWgsl);
5224 vertexModule = dev.CreateShaderModule(reinterpret_cast<const wgpu::ShaderModuleDescriptor*>(&md));
5225 pd.vertex.module = vertexModule.Get();
5226 pd.vertex.entryPoint = sv("vs_main");
5227 }
5228
5229 if (!frag.empty()) {
5230 WGPUFragmentState fs{};
5231 WGPUShaderModuleDescriptor md = mdDesc(frag);
5232 fragmentModule = dev.CreateShaderModule(reinterpret_cast<const wgpu::ShaderModuleDescriptor*>(&md));
5233 fs.module = fragmentModule.Get();
5234 fs.entryPoint = sv("fs_main");
5235 fs.targetCount = 1;
5236 WGPUColorTargetState target{};
5237 target.format = format;
5238 target.writeMask = WGPUColorWriteMask_All;
5239 if (blend) {
5240 static WGPUBlendState bs = alphaBlend();
5241 target.blend = &bs;
5242 }
5243 fs.targets = &target;
5244 pd.fragment = &fs;
5245 } else {
5246 pd.fragment = nullptr;
5247 }
5248
5249 pd.primitive.topology = WGPUPrimitiveTopology_TriangleList;
5250 pd.primitive.frontFace = WGPUFrontFace_CCW;
5251 pd.primitive.cullMode = hair ? WGPUCullMode_None : WGPUCullMode_None;
5252 pd.primitive.stripIndexFormat = WGPUIndexFormat_Undefined;
5253
5254 if (depth && !shadow) {
5255 static WGPUDepthStencilState ds{};
5256 ds.format = WGPUTextureFormat_Depth32Float;
5257 ds.depthWriteEnabled = depthWrite ? WGPUOptionalBool_True : WGPUOptionalBool_False;
5258 ds.depthCompare = WGPUCompareFunction_Less;
5259 pd.depthStencil = &ds;
5260 }
5261 pd.multisample.count = sampleCount ? sampleCount : 1;
5262 // mask=0 (zero-init) silently discards all fragments; use all-samples.
5263 pd.multisample.mask = 0xFFFFFFFFu;
5264 return dev.CreateRenderPipeline(reinterpret_cast<const wgpu::RenderPipelineDescriptor*>(&pd));
5265}
5266
5267} // namespace
5268
5269Shader* Graphics::newShaderFromSpv(const std::vector<uint32_t>& vertSpv, const std::vector<uint32_t>& fragSpv) {
5270 if (!device) throw Exception("newShaderFromSpv: device not initialized");
5271 // Native Dawn can consume Vulkan SPIR-V directly; the browser build cannot
5272 // (browsers only accept WGSL). The WebGPU backend therefore requires WGSL
5273 // for custom shaders; SPIR-V input is rejected with a clear message.
5274 throw Exception(
5275 "newShaderFromSpv: SPIR-V custom shaders are not supported on the "
5276 "WebGPU backend (browsers accept WGSL only). Recompile the shader "
5277 "with the WebGPU toolchain (glslc+tint) and use newShaderFromWgsl.");
5278}
5279
5280Shader* Graphics::newShaderFromSpvFile(const std::string& vertPath, const std::string& fragPath) {
5281 throw Exception(
5282 "newShaderFromSpvFile: SPIR-V custom shaders are not supported on the "
5283 "WebGPU backend. Use WGSL shaders instead.");
5284}
5285
5287 if (!shader || !shader->gpuHandle) return false;
5288
5289 auto* gpu = static_cast<GpuShader*>(shader->gpuHandle);
5290 auto gpuIt = std::find_if(ownedGpuShaders.begin(), ownedGpuShaders.end(),
5291 [&](const std::unique_ptr<GpuShader>& g) { return g.get() == gpu; });
5292 if (gpuIt == ownedGpuShaders.end()) return false;
5293
5294 auto shIt = std::find_if(ownedShaders.begin(), ownedShaders.end(),
5295 [&](const std::unique_ptr<Shader>& s) { return s.get() == shader; });
5296 if (shIt == ownedShaders.end()) return false;
5297
5298 shader->gpuHandle = nullptr;
5299 ownedGpuShaders.erase(gpuIt);
5300 // Transfer the CPU facade to the caller instead of destroying it.
5301 (void)shIt->release();
5302 ownedShaders.erase(shIt);
5303 return true;
5304}
5305
5306Shader* Graphics::newShader(const std::string& vertGlsl, const std::string& fragGlsl) {
5307 (void)vertGlsl;
5308 (void)fragGlsl;
5309 throw Exception(
5310 "newShader: runtime GLSL compilation is not available on the WebGPU "
5311 "backend (browser WGSL only). Ship pre-compiled WGSL shaders.");
5312}
5313
5314Shader* Graphics::newMeshShaderFromSpv(const std::vector<uint32_t>& vertSpv, const std::vector<uint32_t>& fragSpv) {
5315 (void)vertSpv;
5316 (void)fragSpv;
5317 throw Exception(
5318 "newMeshShaderFromSpv: SPIR-V custom mesh shaders are not supported on the "
5319 "WebGPU backend. Use WGSL shaders instead.");
5320}
5321
5322Shader* Graphics::newMeshShaderFromWgsl(const std::string& vertWgsl, const std::string& fragWgsl) {
5323 if (!device) throw Exception("newMeshShaderFromWgsl: device not initialized");
5324 if (!mesh3dSetLayout) throw Exception("newMeshShaderFromWgsl: mesh3d layout missing");
5325
5326 std::string vert = vertWgsl.empty() ? kMesh3DVertWgsl : vertWgsl;
5327 std::string frag = fragWgsl.empty() ? kMesh3DFragWgsl : fragWgsl;
5328
5329 auto gpu = std::make_unique<GpuShader>();
5330 gpu->isMesh3D = true;
5331 gpu->wgslVert = vert;
5332 gpu->wgslFrag = frag;
5333 gpu->mesh3dPipeline = buildPipelineFromWgsl(device, mesh3dPipelineLayout, sceneColorFormat, vert, frag,
5334 /*depth*/ true, /*blend*/ false, /*mesh3d*/ true, /*hair*/ false,
5335 /*shadow*/ false, /*gbuffer*/ false,
5336 /*sampleCount*/ sceneColorSamples);
5337 // X-ray variant: depth test/write off + alpha blend so occluded silhouettes
5338 // paint over the building (the shader discards visible fragments itself).
5339 gpu->mesh3dXrayPipeline = buildPipelineFromWgsl(device, mesh3dPipelineLayout, sceneColorFormat, vert, frag,
5340 /*depth*/ false, /*blend*/ true, /*mesh3d*/ true, /*hair*/ false,
5341 /*shadow*/ false, /*gbuffer*/ false,
5342 /*sampleCount*/ sceneColorSamples);
5343
5344 auto sh = std::make_unique<Shader>();
5345 sh->setKind(Shader::Kind::eMesh3D);
5346 sh->gpuHandle = gpu.get();
5347
5348 Shader* raw = sh.get();
5349 ownedShaders.push_back(std::move(sh));
5350 ownedGpuShaders.push_back(std::move(gpu));
5351 return raw;
5352}
5353
5355 bool doubleSided) {
5356 auto found = std::find_if(ownedGpuShaders.begin(), ownedGpuShaders.end(),
5357 [&](const auto& gpu) { return gpu.get() == shader.gpuHandle; });
5358 if (found == ownedGpuShaders.end() || !(*found)->isMesh3D)
5360 eve::Diagnostic::error(eve::DiagnosticCode::InvalidArgument, "Expected an owned WebGPU mesh shader"));
5361 auto& gpu = **found;
5362 const bool alphaBlend = blend == BlendMode::Alpha;
5363 gpu.mesh3dPipeline =
5364 buildPipelineFromWgsl(device, mesh3dPipelineLayout, sceneColorFormat, gpu.wgslVert, gpu.wgslFrag, true,
5365 alphaBlend, true, doubleSided, false, false, sceneColorSamples, depthWrite);
5366 shader.meshBlend = blend;
5367 shader.meshDepthWrite = depthWrite;
5368 shader.meshDoubleSided = doubleSided;
5370}
5371
5372Shader* Graphics::newMeshShader(const std::string& vertGlsl, const std::string& fragGlsl) {
5373 (void)vertGlsl;
5374 (void)fragGlsl;
5375 throw Exception(
5376 "newMeshShader: runtime GLSL compilation is not available on the WebGPU "
5377 "backend. Ship pre-compiled WGSL shaders.");
5378}
5379
5380Shader* Graphics::newShaderFromWgsl(const std::string& vertWgsl, const std::string& fragWgsl) {
5381 if (!device) throw Exception("newShaderFromWgsl: device not initialized");
5382 if (fragWgsl.empty()) throw Exception("newShaderFromWgsl: empty fragment WGSL");
5383 if (!tex2DPipelineLayout) throw Exception("newShaderFromWgsl: 2D pipeline layout missing");
5384
5385 std::string vert = vertWgsl.empty() ? kTexturedVertWgsl : vertWgsl;
5386
5387 auto gpu = std::make_unique<GpuShader>();
5388 gpu->isMesh3D = false;
5389 gpu->wgslVert = vert;
5390 gpu->wgslFrag = fragWgsl;
5391 // 2D custom shader: alpha blend, no depth; one pipeline per target format
5392 // (swapchain surface vs RGBA8Unorm offscreen canvas).
5393 gpu->swapchainPipeline = createPipelineForShader(gpu.get(), wgpu::TextureFormat(surfaceFormat),
5394 /*depth*/ false, /*mesh3d*/ false,
5395 /*hair*/ false, /*shadow*/ false,
5396 /*gbuffer*/ false, tex2DPipelineLayout);
5397 gpu->offscreenPipeline = createPipelineForShader(gpu.get(), wgpu::TextureFormat::RGBA8Unorm,
5398 /*depth*/ false, /*mesh3d*/ false,
5399 /*hair*/ false, /*shadow*/ false,
5400 /*gbuffer*/ false, tex2DPipelineLayout);
5401 gpu->swapchainOpaquePipeline = buildPipelineFromWgsl(device, tex2DPipelineLayout, WGPUTextureFormat(surfaceFormat),
5402 vert, fragWgsl, false, false, false, false, false, false, 1u);
5403 gpu->offscreenOpaquePipeline = buildPipelineFromWgsl(device, tex2DPipelineLayout, WGPUTextureFormat_RGBA8Unorm,
5404 vert, fragWgsl, false, false, false, false, false, false, 1u);
5405 gpu->hdrOffscreenPipeline = createPipelineForShader(gpu.get(), wgpu::TextureFormat::RGBA16Float, false, false,
5406 false, false, false, tex2DPipelineLayout);
5407 gpu->hdrOffscreenOpaquePipeline =
5408 buildPipelineFromWgsl(device, tex2DPipelineLayout, WGPUTextureFormat_RGBA16Float, vert, fragWgsl, false, false,
5409 false, false, false, false, 1u);
5410
5411 auto sh = std::make_unique<Shader>();
5412 sh->setKind(Shader::Kind::eSprite2D);
5413 sh->gpuHandle = gpu.get();
5414
5415 Shader* raw = sh.get();
5416 ownedShaders.push_back(std::move(sh));
5417 ownedGpuShaders.push_back(std::move(gpu));
5418 return raw;
5419}
5420
5421wgpu::RenderPipeline Graphics::createPipelineForShader(GpuShader* gs, wgpu::TextureFormat format, bool depth,
5422 bool mesh3d, bool hair, bool shadow, bool gbuffer,
5423 wgpu::PipelineLayout layout) {
5424 if (!gs || gs->wgslFrag.empty()) return {};
5425 // Custom 2D/mesh shaders blend only when the caller asks (x-ray / 2D UI);
5426 // opaque meshes and shadow/gbuffer variants keep write-through targets.
5427 const bool blend = !depth;
5428 const uint32_t samples = mesh3d || hair || shadow || gbuffer ? sceneColorSamples : 1u;
5429 return buildPipelineFromWgsl(device, layout, WGPUTextureFormat(format), gs->wgslVert, gs->wgslFrag, depth, blend,
5430 mesh3d, hair, shadow, gbuffer, samples);
5431}
5432
5433Shader* Graphics::newHairShaderFromSpv(const std::vector<uint32_t>& vertSpv, const std::vector<uint32_t>& fragSpv) {
5434 (void)vertSpv;
5435 (void)fragSpv;
5436 throw Exception(
5437 "newHairShaderFromSpv: SPIR-V hair shaders are not supported on WebGPU; "
5438 "use newHairShaderFromWgsl");
5439}
5440
5441Shader* Graphics::newHairShaderFromWgsl(const std::string& vertWgsl, const std::string& fragWgsl) {
5442 if (!device) throw Exception("newHairShaderFromWgsl: device not initialized");
5443 if (fragWgsl.empty()) throw Exception("newHairShaderFromWgsl: empty fragment WGSL");
5444 auto gpu = std::make_unique<GpuShader>();
5445 gpu->isMesh3D = true;
5446 gpu->isHair3D = true;
5447 gpu->wgslVert = vertWgsl.empty() ? kMesh3DVertWgsl : vertWgsl;
5448 gpu->wgslFrag = fragWgsl;
5449 gpu->mesh3dPipeline = buildPipelineFromWgsl(device, mesh3dPipelineLayout, sceneColorFormat, gpu->wgslVert,
5450 gpu->wgslFrag, true, true, true, true, false, false, sceneColorSamples);
5451 auto shader = std::make_unique<Shader>();
5452 shader->setKind(Shader::Kind::eMesh3D);
5453 shader->gpuHandle = gpu.get();
5454 Shader* raw = shader.get();
5455 ownedShaders.push_back(std::move(shader));
5456 ownedGpuShaders.push_back(std::move(gpu));
5457 return raw;
5458}
5459
5460} // namespace eve::graphics::webgpu
LogicalId target
double value
bool & active
float w
Definition AnimClip.cpp:738
float y
Definition AnimClip.cpp:738
float x
Definition AnimClip.cpp:738
std::string descriptor
ActiveSource owned
const std::string & s
std::vector< QuestEvent > pending
std::unordered_map< std::string, QuestRuntime > entries
std::vector< std::uint32_t > verts
Definition Builder.cpp:27
building::EdgeCurveGroup group
float aoStrength
float normalStrength
float cx
Definition CardTypes.cpp:33
float cy
Definition CardTypes.cpp:34
float degrees
Definition CardTypes.cpp:35
float uv
bool split
Definition CaveMesh.cpp:123
graphics::Texture * albedo
glm::vec4 p[6]
scene::NodeDesc desc
std::string layout
std::string message
DiagnosticCode code
std::uint32_t vertexCount
std::uint32_t indexCount
tensor::Graph g
Definition GpuGraph.cpp:7
vk::ShaderModule vert
vk::ShaderModule frag
std::uint32_t key
vk::UniqueSampler sampler
wgpu::PopErrorScopeStatus status
glm::vec4 tint
glm::mat4 mvp
uint32_t i1
Definition Grass.cpp:61
uint32_t i0
Definition Grass.cpp:61
float u
Definition Grass.cpp:233
glm::vec3 n
Definition Grass.cpp:63
std::vector< std::uint32_t > indices
float v
std::int32_t c
std::int32_t first
float blend
eve::ResourcePin pinned
int h
std::uint32_t height
std::uint32_t width
size_t offset
std::array< float, 3 > scale
std::uint64_t bytes
std::string name
MeleePoint3 b
Definition MeleeHit.cpp:41
MeleePoint3 a
Definition MeleeHit.cpp:40
std::vector< TriangleRef > triangles
MeshVfxBatchedDraw draw
float roughness
float tb
bool doubleSided
Texture * normal
float alphaCutoff
float metallic
double & milliseconds
Definition OnnxGpgpu.cpp:19
std::string error
Definition Package.cpp:60
TileLayer * layer
int idx
float f
std::string id
Definition PlayHost.cpp:108
float begin
float d
int detail
float t
uint8_t * pixels
bool lodFadeReverse
glm::vec3 eye
Mesh * mesh
float lodWeight
glm::mat4 viewProj
Shader * shader
bool hair
glm::mat4 view
glm::mat4 model
std::vector< float > colors
bool found
bool repeatV
bool repeatU
double current
std::uint32_t count
float size
Definition TreeMesh.cpp:156
int caps
Definition TreeMesh.cpp:162
V3 dir
Definition TreeMesh.cpp:150
uint32_t index
const UnitySourceAsset & source
std::uint32_t depth
std::size_t at
std::vector< double > phi
ViewPreparation callback
float m[16]
static Diagnostic error(DiagnosticCode code, std::string message, std::string path={}, DiagnosticDetails details={}, std::string source={})
Construct an error diagnostic with the standard error severity.
Definition Diagnostic.h:125
EVENGINE_API_FOUNDATION public API.
Definition Exception.h:13
eve::Result< ResourcePin > pin(Resource *resource)
Keeps one cached resource alive across unload()/clear()/reload.
Definition Resource.cpp:311
ResultRef< Resource > waitFor(std::string key)
Block until key is cached, failed, or unclaimed.
Definition Resource.cpp:226
static ResourceManager & getInstance()
Returns the instance.
Definition Resource.cpp:15
Move-only operation result carrying either a value or Status.
Definition Result.h:155
static Result success(T value)
Construct a successful result owning value.
Definition Result.h:164
static Result failure(Status status)
Construct a failed result from a structured status.
Definition Result.h:175
Accumulates solid / textured quads in logical (Y-down) coordinates. Used by RenderSystem; not a publi...
Definition Batcher.h:24
Canvas public API.
Definition Canvas.h:17
virtual int getWidth() const =0
Returns the width.
virtual Texture * getTexture()=0
Sampleable color buffer; screen Canvas returns nullptr.
virtual int getHeight() const =0
Returns the height.
Screen-space buffers for mid/post effects and Hybrid deferred lighting.
Definition GBuffer.h:28
Texture * getDepthTexture() const
Returns the depth texture.
Definition GBuffer.h:51
void setTargets(int width, int height, Texture *depth, Texture *normal, Texture *albedo, Texture *hwDepth=nullptr, Texture *pbrParams=nullptr, Texture *emissive=nullptr)
Called by Graphics after a G-buffer pass (or clear).
Definition GBuffer.cpp:31
bool isValid() const
True when valid.
Definition GBuffer.cpp:5
virtual void setVSync(bool enabled)
Prefer uncapped present (IMMEDIATE/MAILBOX) when false, vsync (MAILBOX/FIFO) when true....
Definition Graphics.h:1528
int getWidth() const
Returns the width.
Definition Graphics.h:502
std::vector< DeferredFileTexture > deferredFileTextures_
Definition Graphics.h:2239
void retireResourceLifetime() const
Retire resource lifetime.
bool fileTextureSourceExists(const std::string &filename) const
File texture source exists.
void ensureFileTexturesReady()
Finish CPU decode and GPU upload for outstanding newTextureFromFile results. @thread Game/render thre...
void setCanvas()
Sets the canvas.
Definition Graphics.h:1992
std::unique_ptr< RenderControl > renderControl_
Definition Graphics.h:2264
void requestFileImageDecode(const std::string &key)
Request file image decode.
Mesh3DSceneColorCaptureStatus
Observable result of requesting a same-frame mesh SceneColor snapshot.
Definition Graphics.h:1168
RenderControl * getRenderControl()
Shared compilable 3D render control (features → pass list + GBuffer). Owned by Graphics; valid for th...
Definition Graphics.cpp:254
PresentOverlayFn presentOverlayFn_
Definition Graphics.h:2259
void dropDeferredFileTexture(Texture *texture)
Drop deferred file texture.
int getHeight() const
Returns the height.
Definition Graphics.h:504
GPU mesh handle (+ optional CPU morph targets).
Definition Mesh.h:25
Frame-local Skia-style recorder for 2D line primitives.
GBuffer * getGBuffer()
Returns the g buffer.
Custom GPU program.
Definition Shader.h:39
static constexpr uint32_t kPushConstantBytes
Definition Shader.h:43
GPU texture created via Graphics::newTexture. Owns GPU resources through an opaque backend handle.
Definition Texture.h:18
TextureSampler sampler
Definition Texture.h:63
void setMesh3DLodDither(float weight, bool reverse, bool enabled) override
Sets the mesh 3 d lod dither.
void pushValidationScope() override
Pushes validation scope.
Texture * newCubemap(int faceSize, const uint8_t *rgbaFaces) override
Creates a cubemap. @ownership Caller deletes unless documented otherwise.
void setMesh3DEnv(Texture *cube, float intensity) override
Sets the mesh 3 d env.
void setMesh3DLight(const glm::vec3 &dir, const glm::vec3 &color) override
Sets the mesh 3 d light.
void setMesh3DPackedNormalMask(bool enabled) override
Sets the mesh 3 d packed normal mask.
void setMesh3DReflectionProbes(const ReflectionProbeUpload &upload) override
Sets the mesh 3 d reflection probes.
void drawTexturedRectLitUVRotated(Texture *albedo, Texture *normal, float cx, float cy, float w, float h, float degrees, float u0, float v0, float u1, float v1, const Color &color, BlendMode blend=BlendMode::Alpha) override
Draws textured rect lit uv rotated.
void present() override
Present.
Shader * newMeshShader(const std::string &vertGlsl, const std::string &fragGlsl) override
Creates a mesh shader. @ownership Caller deletes unless documented otherwise.
void flush2DToCanvas(OffscreenCanvas *canvas)
Flush accumulated 2D batches into an offscreen canvas target.
Mesh3DSceneColorCaptureStatus captureMesh3DSceneColor() override
Capture mesh 3 d scene color.
void setMesh3DViewProj(const glm::mat4 &viewProj) override
Sets the mesh 3 d view proj.
void draw(eve::graphics::Graphics *gfx, const glm::mat4 &matrix) const override
Draws .
void endDecalPass() override
Ends decal pass.
void setMesh3DShadowReceive(bool receive) override
Sets the mesh 3 d shadow receive.
void pumpReadback()
Advances a pending frame readback; called every present().
Shader * newShaderFromSpvFile(const std::string &vertPath, const std::string &fragPath) override
Creates a shader from spv file. @ownership Caller deletes unless documented otherwise.
void setMsaaSamples(int samples) override
Sets the msaa samples.
float getMaxAnisotropy() const override
Returns the max anisotropy.
ResultRef< Texture > newSharedTexture(image::ImageData *data, const std::string &contentKey) override
Reuse immutable imported pixels. Graphics owns the returned texture until release or shutdown.
Mesh * newMeshSphere(int slices=32, int stacks=16) override
Creates a mesh sphere. @ownership Caller deletes unless documented otherwise.
void popValidationScope() override
Pops validation scope.
void clear(std::optional< Color > color, std::optional< int > stencil, std::optional< double > depth) override
Clears .
void setMesh3DNormalTexture(Texture *normal) override
Sets the mesh 3 d normal texture.
Mesh * newMeshFromArraysColored(const float *posXYZ, const float *nrmXYZ, const float *uvST, const float *colorRGBA, int vertexCount, const uint32_t *indices, int indexCount) override
Create a mesh with a packed linear RGBA vertex-color stream.
void setMesh3DHeightTexture(Texture *height) override
Sets the mesh 3 d height texture.
void begin3DFrame() override
Begins 3 d frame.
Mesh * newMeshFromAssimp(const ::aiMesh &mesh) override
Creates a mesh from assimp. @ownership Caller deletes unless documented otherwise.
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, BlendMode blend=BlendMode::Alpha) override
Draws textured rect lit uv.
eve::Result< void > updateTextureRegions(Texture *texture, std::span< const TextureRegionUpload > regions) override
Updates texture regions.
std::optional< eve::graphics::MeshBackendDescriptor > describeMesh(Mesh *mesh) const override
Return layout facts from the owned WebGPU mesh upload.
bool uploadDeferredFileTexture(Texture *texture, image::ImageData *data) override
Uploads deferred file texture.
void setMesh3DCameraPos(const glm::vec3 &eye) override
Sets the mesh 3 d camera pos.
void beginGBufferPass(int width, int height) override
Begins g buffer pass.
void setMesh3DVirtualTexture(bool enabled, int pageCountX, int pageCountY, int atlasSlotsX, int atlasSlotsY, float borderFraction) override
Sets the mesh 3 d virtual texture.
Shader * newMeshShaderFromWgsl(const std::string &vertWgsl, const std::string &fragWgsl) override
Creates a mesh shader from wgsl. @ownership Caller deletes unless documented otherwise.
Shader * newHairShaderFromWgsl(const std::string &vertWgsl, const std::string &fragWgsl) override
Creates a hair shader from wgsl. @ownership Caller deletes unless documented otherwise.
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, float motionX=0.f, float motionY=0.f, float roughness=0.45f, float metallic=0.f) override
Draws mesh g buffer alpha.
bool bakeMeshMorph(Mesh *mesh) override
If mesh morph weights are dirty, bake blended positions and upload to the GPU VBO....
void setMesh3DEnvProbe(const glm::vec3 &center, const glm::vec3 &extent) override
Sets the mesh 3 d env probe.
Texture * newTextureFromFile(const std::string &filename) override
Creates a texture from file. @ownership Caller deletes unless documented otherwise.
void drawTexturedRectShader5(Texture *color, Texture *depth, Texture *motion, Texture *extra, Texture *specular, Shader *shader, float x, float y, float w, float h, const Color &tint) override
Draws textured rect shader 5.
void setViewportSize(int width, int height, int pixelwidth, int pixelheight) override
Sets the viewport size.
Definition Graphics.cpp:270
void setMesh3DLighting(const Lighting3DPack &pack) override
Sets the mesh 3 d lighting.
void drawMeshShader(Mesh *mesh, const glm::mat4 &model, Texture *texture, const Color &tint, Shader *shader) override
Draws mesh shader.
void setMesh3DMaterial(float metallic, float roughness) override
Sets the mesh 3 d material.
void drawSolidRectRotated(float cx, float cy, float w, float h, float degrees, const Color &color, BlendMode blend=BlendMode::Alpha) override
Draws solid rect rotated.
void setMesh3DSkinInfluenceLimit(SkinInfluenceLimit count) override
Sets the mesh 3 d skin influence limit.
void setLighting2D(const Lighting2DUBO &ubo) override
Sets the lighting 2 d.
void endGBufferPass() override
Ends g buffer pass.
void setMesh3DShadows(const ShadowUpload &upload) override
Sets the mesh 3 d shadows.
Shader * newMeshShaderFromSpv(const std::vector< uint32_t > &vertSpv, const std::vector< uint32_t > &fragSpv) override
Creates a mesh shader from spv. @ownership Caller deletes unless documented otherwise.
Texture * newTexture(int width, int height, const uint8_t *rgba, bool repeatU=false, bool repeatV=false) override
Creates a texture. @ownership Caller deletes unless documented otherwise.
void end3DFrameToCanvas() override
Ends 3 d frame to canvas.
bool copyHDRCanvasesToCubemap(Canvas *const *sources, int faceCount, Texture *cubemap) override
Copies hdr canvases to cubemap.
void drawTexturedRectShader4(Texture *color, Texture *depth, Texture *motion, Texture *extra, Shader *shader, float x, float y, float w, float h, const Color &tint) override
Draws textured rect shader 4.
bool updateMeshVertices(Mesh *mesh, const float *posXYZ, const float *nrmXYZ, const float *uvST, int vertexCount, const uint32_t *indices, int indexCount) override
Updates mesh vertices.
void drawMesh(Mesh *mesh, const glm::mat4 &model, Texture *texture, const Color &tint) override
Draws mesh.
Shader * newHairShaderFromSpv(const std::vector< uint32_t > &vertSpv, const std::vector< uint32_t > &fragSpv) override
Creates a hair shader from spv. @ownership Caller deletes unless documented otherwise.
Shader * newShaderFromSpv(const std::vector< uint32_t > &vertSpv, const std::vector< uint32_t > &fragSpv) override
Creates a shader from spv. @ownership Caller deletes unless documented otherwise.
void setMesh3DView(const glm::mat4 &view) override
Sets the mesh 3 d view.
void drawTexturedRect(Texture *texture, float x, float y, float w, float h, const Color &color) override
Draws textured rect.
void begin3DFrameToCanvas(Canvas *canvas) override
Begins 3 d frame to canvas.
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, float motionX=0.f, float motionY=0.f, float roughness=0.45f, float metallic=0.f) override
Draws mesh g buffer.
void drawTexturedRectUV(Texture *texture, float x, float y, float w, float h, float u0, float v0, float u1, float v1, const Color &color) override
Draws textured rect uv.
bool releaseShader(Shader *shader) override
Release shader.
void setMesh3DClusteredLighting(const ClusteredLightingUpload &upload) override
Sets the mesh 3 d clustered lighting.
void setMesh3DTexCellBomb(float cellScale, float strength, float rotAmount=1.f) override
Sets the mesh 3 d tex cell bomb.
void initWithWindow(void *nativeWindow) override
Initializes with window.
Definition Graphics.cpp:144
void setMesh3DSceneColor(Texture *color) override
Sets the mesh 3 d scene color.
~Graphics() override
Graphics.
Definition Graphics.cpp:103
Mesh * newMeshCylinder(int slices=32, int stacks=1, bool caps=true) override
Creates a mesh cylinder. @ownership Caller deletes unless documented otherwise.
eve::Result< void > configureMeshShaderSurface(Shader &shader, BlendMode blend, bool depthWrite, bool doubleSided) override
Configure mesh shader surface.
void beginDecalPass(int width, int height) override
Begins decal pass.
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
Draws textured rect shader uv rotated.
void setMesh3DParallax(float scale, float minLayers=8.f, float maxLayers=32.f) override
Sets the mesh 3 d parallax.
void drawTexturedRectShaderDepthMotion(Texture *color, Texture *depth, Texture *motion, Shader *shader, float x, float y, float w, float h, const Color &tint) override
Draws textured rect shader depth motion.
Mesh * newMeshFromArrays(const float *posXYZ, const float *nrmXYZ, const float *uvST, int vertexCount, const uint32_t *indices, int indexCount) override
Creates a mesh from arrays. @ownership Caller deletes unless documented otherwise.
Shader * newShader(const std::string &vertGlsl, const std::string &fragGlsl) override
Creates a shader. @ownership Caller deletes unless documented otherwise.
Texture * getSceneLinearDepthTexture() override
Return the active sampleable scene linear-depth texture when available. @ownership Borrowed from the ...
void drawMeshShadow(Mesh *mesh, const glm::mat4 &lightMVP, bool doubleSided=true) override
Draws mesh shadow.
void setMesh3DClusteredActive(bool active) override
Sets the mesh 3 d clustered active.
bool copyHDRCanvasToCubemapFace(Canvas *source, Texture *cubemap, int face) override
Copies hdr canvas to cubemap face.
bool setMeshSkinningData(Mesh *mesh, const uint16_t *joints4, const float *weights4, int vertexCount) override
Sets the mesh skinning data.
void drawPrimitiveCanvas(const PrimitiveCanvas2D &canvas) override
Draws primitive canvas.
bool reloadTextureFromFile(const std::string &filename) override
Reloads texture from file.
void drawMeshShadowAlpha(Mesh *mesh, const glm::mat4 &lightMVP, Texture *albedo=nullptr, bool doubleSided=true, float lodWeight=1.f, bool lodFadeReverse=false, bool lodDither=false) override
Draws mesh shadow alpha.
void setCloudShadows(float strength, float worldCell, float time, float windSpeed, float windAngle, float coverage, float detail) override
Sets the cloud shadows.
eve::Result< void > updateTextureRegion(Texture *texture, int x, int y, int width, int height, std::span< const std::uint8_t > rgba, std::size_t bytesPerRow=0) override
Updates texture region.
bool filterHDRReflectionCubemap(Texture *cubemap, int sampleCount=64) override
Filter hdr reflection cubemap.
void setTextureSampler(Texture *texture, const TextureSampler &sampler) override
Sets the texture sampler.
bool releaseMesh(Mesh *mesh) override
Release mesh.
void setMesh3DClip(float nearZ, float farZ) override
Sets the mesh 3 d clip.
void endShadowPass() override
Ends shadow pass.
void drawDecal(const glm::mat4 &model, Texture *albedo, Texture *normal, Texture *params, const float uvRect[4], float fade, float normalStrength, float roughnessStrength, float metalStrength, float emissiveStrength, int blendMode=0, int projectionMode=0, float blendSharpness=4.f, float parallaxScale=0.f, float parallaxMinLayers=8.f, float parallaxMaxLayers=24.f, float edgeFadeWidth=0.06f) override
Draws decal.
void setDecalCamera(const glm::mat4 &viewProj, float nearZ, float farZ) override
Sets the decal camera.
void setMesh3DSceneDepth(Texture *depth) override
Sets the mesh 3 d scene depth.
bool releaseTexture(Texture *texture) override
Release texture.
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
Draws textured rect shader uv.
Shader * newShaderFromWgsl(const std::string &vertWgsl, const std::string &fragWgsl) override
Creates a shader from wgsl. @ownership Caller deletes unless documented otherwise.
Texture * getTexture() override
Returns the texture.
Canvas * newHDRCanvas(int width, int height) override
Creates a hdr canvas. @ownership Caller deletes unless documented otherwise.
void drawSolidRect(float x, float y, float w, float h, const Color &color, BlendMode blend=BlendMode::Alpha) override
Draws solid rect.
Canvas * newCanvas(int width, int height) override
Creates a canvas. @ownership Caller deletes unless documented otherwise.
void drawTexturedRectShader(Texture *texture, Shader *shader, float x, float y, float w, float h, const Color &color) override
Draws textured rect shader.
void drawTexturedRectShaderDepth(Texture *color, Texture *depth, Shader *shader, float x, float y, float w, float h, const Color &tint) override
Draws textured rect shader depth.
Texture * newHDRCubemap(int faceSize) override
Creates a hdr cubemap. @ownership Caller deletes unless documented otherwise.
Canvas * getCanvas() const override
Returns the canvas.
void setVSync(bool enabled) override
Sets the v sync.
Definition Graphics.cpp:264
bool updateTexture(Texture *texture, int width, int height, const uint8_t *rgba) override
Updates texture.
void setMesh3DSSAO(float intensity) override
Sets the mesh 3 dssao.
bool isCanvasActive() const override
True when canvas active.
void setMesh3DSurface(SurfaceMode mode, BlendMode blend, bool depthWrite, bool doubleSided, float alphaCutoff, const std::string &alphaTechnique="cutoff") override
Sets the mesh 3 d surface.
void beginShadowPass(int cascadeIndex) override
Begins shadow pass.
Texture * getSceneColorTexture() override
Returns the scene color texture.
void initHeadless(int width, int height) override
Initialize the renderer without a window or swapchain (headless mode). Creates a GPU device and offsc...
Definition Graphics.cpp:113
static DeviceDone requestDevice(AdapterDone &&prev)
Blocking device request (plus queue + capability capture). Throws eve::graphics::Exception on failure...
Definition InitFlow.cpp:76
static InstanceDone createInstance()
Create a wgpu Instance (native requests the TimedWaitAny feature).
Definition InitFlow.cpp:15
static AdapterDone requestAdapter(InstanceDone &&prev, const wgpu::Surface &compatibleSurface)
Blocking adapter request. Drives ProcessEvents until the callback fires; throws eve::graphics::Except...
Definition InitFlow.cpp:32
Offscreen render target (RGBA8Unorm color, optional Depth32Float). 2D batches are flushed into the ca...
Definition Canvas.h:16
int getWidth() const override
Returns the width.
Definition Canvas.h:24
int getHeight() const override
Returns the height.
Definition Canvas.h:26
Texture * getTexture() override
Returns the texture.
Definition Canvas.h:28
bool isHDR() const
True when hdr.
Definition Canvas.h:30
Represents raw pixel data.
Definition ImageData.h:38
RAII keep-alive for one RuntimeObjectRegistry entry.
T * get() const noexcept
Borrows the pinned object; valid until the pin is released.
std::vector< ParamSpec > params
const char * kDecalFragWgsl
const char * kColorVertWgsl
const char * kMesh3DFragWgsl
const char * kDecalVertWgsl
const char * kColorFragWgsl
const char * kTexturedVertWgsl
WGPUStringView sv(const char *s)
Build a WGPUStringView from a C string (null-safe, length auto-computed).
Definition wgpu_types.h:20
const char * kMesh3DVertWgsl
Immutable shader source. @borrowed Static storage; valid for the process lifetime.
const char * kMesh3DGbufferFragWgsl
Immutable GBuffer fill WGSL. @borrowed Static storage; valid for the process lifetime.
const char * kMesh3DShadowAlphaVertWgsl
Immutable shader source. @borrowed Static storage; valid for the process lifetime.
const char * kLit2DVertWgsl
const char * kMesh3DShadowVertWgsl
Immutable shader source. @borrowed Static storage; valid for the process lifetime.
const char * kSSAOFragWgsl
const char * kTexturedFragWgsl
const char * kLit2DFragWgsl
wgpu::ShaderModule makeWgslModule(wgpu::Device &dev, const char *wgsl)
Create an owning shader module; retain it until pipeline creation completes.
Definition wgpu_types.h:34
const char * kMesh3DShadowAlphaFragWgsl
const char * kMesh3DClusteredVertWgsl
const char * kMesh3DGbufferAlphaFragWgsl
Immutable GBuffer alpha-cutout WGSL. @borrowed Static storage; valid for the process lifetime.
const char * kMesh3DClusteredFragWgsl
const char * kMesh3DGbufferVertWgsl
Immutable shader source. @borrowed Static storage; valid for the process lifetime.
void fillMeshAttributes(WGPUVertexAttribute(&attrs)[6])
std::vector< uint8_t > buildGgxCubemapMipChain(const uint8_t *rgbaFaces, uint32_t faceSize, uint32_t mipLevels, uint32_t sampleCount=32u)
Build a mip-major RGBA8 IBL chain: GGX specular mips plus final diffuse irradiance.
SurfaceMode
How a 3D surface contributes to depth and color passes.
Definition SurfaceMode.h:6
ResolvedPrimitiveTriangles resolvePrimitiveStrokes2D(const PrimitiveCanvas2D &canvas, glm::ivec2 viewport)
Resolves 2D stroke segment bodies into clip-space triangles.
eve::Color Color
RGBA color used by every graphics draw call. Lives inside eve::graphics so including a graphics heade...
Definition Color.h:13
Light3DGpu ClusteredLightGpu
int mipmapCountForSize(int width, int height)
Full mip chain count for a 2D image (including base level).
void detachGraphicsArtifactProvider(Graphics *graphics) noexcept
Detach a Graphics instance before its derived backend is destroyed.
eve::BlendMode BlendMode
Compatibility alias for the shared 2D blend mode.
Definition BlendMode.h:8
SkinInfluenceLimit
Number of strongest vertex influences retained by GPU skinning.
Definition IGraphics3D.h:19
constexpr HexDirection next(HexDirection d) noexcept
The next direction clockwise (NW wraps to NE).
Definition HexMetrics.h:76
std::vector< int64_t > attrs(const Node &n, const char *key, std::vector< int64_t > fallback)
Attrs.
BlendMode
Render-neutral 2D blend mode shared by graphics-facing modules.
Definition RenderTypes.h:11
bool enabled
CPU-built clustered lighting upload for one frame/camera. Point lights are clustered; directional lig...
std::vector< ClusteredLightGpu > lights
std::vector< ClusterTableEntry > clusterTable
Lighting2DUBO public API.
Definition Light.h:53
Lighting3DPack public API.
Definition Light.h:172
std::array< glm::vec4, kMaxDiffuseVolumeProbes > diffuseVolumePosition
Definition Light.h:180
std::array< glm::vec4, kMaxDiffuseVolumeProbes > diffuseVolumeExtent
Definition Light.h:181
std::array< glm::vec4, kMaxDiffuseVolumeProbes *9 > diffuseVolumeSh
Definition Light.h:182
static constexpr int kMaxLights
Definition Light.h:173
Light3DGpu lights[kMaxLights]
Definition Light.h:175
std::array< glm::vec4, 9 > diffuseProbeSh
Definition Light.h:177
Backend-owned layout facts for a mesh uploaded through Graphics.
Definition Graphics.h:86
Resolved triangle vertex consumed by primitive GPU backends.
ReflectionProbeUpload public API.
Definition IGraphics3D.h:26
One contiguous 2D blend/order batch in the resolved vertex stream.
static constexpr int kMapSize
Definition Shadow.h:28
static constexpr int kCascades
Definition Shadow.h:24
static constexpr int kTotalLayers
Definition Shadow.h:27
ShadowUpload public API.
Definition Shadow.h:54
Options for Graphics::newTexture / newCubemap. When generateMipmaps is true and sampler....
One borrowed RGBA8 source rectangle for a batched texture update. @ownership rgba remains owned by th...
Definition Graphics.h:70
Sampler state for a Texture (filter, wrap, mip LOD, anisotropy). Defaults match historical engine beh...
static TextureSampler linear()
Linear.
static TextureSampler linearMipmap()
Trilinear (linear + linear mips). Caller should create the texture with generateMipmaps.
Vertex/index buffers for one mesh.
Definition Graphics.h:153
std::vector< MeshVertex > cpuVertices
Definition Graphics.h:162
A compiled shader: one WebGPU pipeline + layout. Also holds the WGSL sources so custom shaders can be...
Definition Graphics.h:169
Texture resources backed by a wgpu texture + view + sampler + bind groups.
Definition Graphics.h:131
MeshVertex public API.
Definition Graphics.h:82
glm::mat4 invViewProj
glm::uvec4 info
uint32_t pad[2]
glm::vec4 color