#include "graphics/webgpu/Graphics.h" #include "graphics/Batcher.h" #include "graphics/AntiAliasing.h" #include "graphics/ClusteredLight.h" #include "graphics/CubemapPrefilter.h" #include "graphics/shaders/ReflectionProbeWgsl.h" #include "graphics/GBuffer.h" #include "graphics/GraphicsCapabilities.h" #include "graphics/Light.h" #include "graphics/Mesh.h" #include "graphics/RenderControl.h" #include "graphics/Shader.h" #include "graphics/Shadow.h" #include "graphics/Texture.h" #include "graphics/TextureSampler.h" #include "graphics/webgpu/BindGroupLayoutBuilder.h" #include "graphics/webgpu/Canvas.h" #include "graphics/webgpu/InitFlow.h" #include "graphics/webgpu/PipelineBuilder.h" #include "graphics/webgpu/wgsl_shaders.h" #include "common/Exception.h" #include "common/config.h" #include "filesystem/Filesystem.h" #include "filesystem/FileData.h" #include "image/Image.h" #include "image/ImageData.h" #include #include #include #include #include #include #include #include #include #include #include #include #if defined(__APPLE__) && !defined(__EMSCRIPTEN__) #include #include #endif #if defined(_WIN32) #include #endif #if defined(__EMSCRIPTEN__) #include #endif namespace eve::graphics::webgpu { namespace { // Forward declaration for the readback helper (defined in the Readback section). bool copyTextureToCpu(wgpu::Instance &instance, wgpu::Device &device, wgpu::Queue &queue, wgpu::Texture src, int width, int height, std::vector &outRgba, int bytesPerPixel = 4); // A shared default sampler used when no texture is provided. wgpu::Sampler createLinearSampler(wgpu::Device &dev) { WGPUSamplerDescriptor d{}; d.label = sv("eve_linear"); d.addressModeU = WGPUAddressMode_ClampToEdge; d.addressModeV = WGPUAddressMode_ClampToEdge; d.addressModeW = WGPUAddressMode_ClampToEdge; d.magFilter = WGPUFilterMode_Linear; d.minFilter = WGPUFilterMode_Linear; d.mipmapFilter = WGPUMipmapFilterMode_Linear; d.lodMinClamp = 0.f; d.lodMaxClamp = 1000.f; d.maxAnisotropy = 1; return dev.CreateSampler(reinterpret_cast(&d)); } int nextPOT(int v) { int p = 1; while (p < v) p <<= 1; return p; } } // namespace void fillMeshAttributes(WGPUVertexAttribute (&attrs)[5]); Graphics::Graphics() { for (uint32_t i = 0; i < kFramesInFlight; ++i) { uboArenas.emplace_back(); vertexArenas.emplace_back(); } } Graphics::~Graphics() { detachGraphicsArtifactProvider(this); } // --------------------------------------------------------------------------- // Init // --------------------------------------------------------------------------- void Graphics::initHeadless(int width, int height) { if (deviceInitDone) { if (headless_) { setViewportSize(width, height, width, height); return; } throw Exception("Graphics::initHeadless: already initialized with a window"); } if (width <= 0 || height <= 0) throw Exception("Graphics::initHeadless: invalid size"); auto inst = InitFlow::createInstance(); auto adp = InitFlow::requestAdapter(std::move(inst), surface); auto dev = InitFlow::requestDevice(std::move(adp)); instance = std::move(dev.instance); adapter = std::move(dev.adapter); device = std::move(dev.device); queue = std::move(dev.queue); caps = std::move(dev.caps); surfaceFormat = WGPUTextureFormat_BGRA8Unorm; createPipelineResources(); createShadowResources(); createDefaultTextures(); setViewportSize(width, height, width, height); createSceneColorResources(width, height); headless_ = true; initialized = true; deviceInitDone = true; } void Graphics::initWithWindow(void *nativeWindow) { sdlWindow = nativeWindow; if (deviceInitDone) return; auto inst = InitFlow::createInstance(); auto adp = InitFlow::requestAdapter(std::move(inst), surface); auto dev = InitFlow::requestDevice(std::move(adp)); instance = std::move(dev.instance); adapter = std::move(dev.adapter); device = std::move(dev.device); queue = std::move(dev.queue); caps = std::move(dev.caps); // ---- Surface ---- WGPUSurfaceDescriptor surfDesc{}; surfDesc.label = sv("eve_surface"); #if defined(__EMSCRIPTEN__) WGPUEmscriptenSurfaceSourceCanvasHTMLSelector canvasSel{}; canvasSel.chain.sType = WGPUSType_EmscriptenSurfaceSourceCanvasHTMLSelector; canvasSel.selector = sv("#canvas"); surfDesc.nextInChain = &canvasSel.chain; surface = instance.CreateSurface(reinterpret_cast(&surfDesc)); surfaceFormat = WGPUTextureFormat_BGRA8Unorm; #else wgpu::SurfaceDescriptor nativeSurfDesc{}; nativeSurfDesc.label = "eve_surface"; SDL_SysWMinfo wminfo; SDL_VERSION(&wminfo.version); if (!SDL_GetWindowWMInfo(static_cast(sdlWindow), &wminfo)) throw Exception("WebGPU: SDL_GetWindowWMInfo failed: %s", SDL_GetError()); #if defined(_WIN32) wgpu::SurfaceSourceWindowsHWND winChain{}; winChain.hwnd = wminfo.info.win.window; winChain.hinstance = GetModuleHandle(nullptr); nativeSurfDesc.nextInChain = &winChain; surface = instance.CreateSurface(&nativeSurfDesc); #elif defined(__linux__) if (wminfo.subsystem == SDL_SYSWM_X11) { wgpu::SurfaceSourceXlibWindow x11Chain{}; x11Chain.display = wminfo.info.x11.display; x11Chain.window = wminfo.info.x11.window; nativeSurfDesc.nextInChain = &x11Chain; surface = instance.CreateSurface(&nativeSurfDesc); #if defined(SDL_VIDEO_DRIVER_WAYLAND) } else if (wminfo.subsystem == SDL_SYSWM_WAYLAND) { wgpu::SurfaceSourceWaylandSurface wlChain{}; wlChain.display = wminfo.info.wl.display; wlChain.surface = wminfo.info.wl.surface; nativeSurfDesc.nextInChain = &wlChain; surface = instance.CreateSurface(&nativeSurfDesc); #endif } else { throw Exception("WebGPU: unsupported SDL window subsystem on Linux"); } #elif defined(__APPLE__) // SDL exposes an NSWindow, while Dawn requires its content view's // CAMetalLayer. Runtime messaging keeps this source portable C++. using SendId = id (*)(id, SEL); using SendVoidId = void (*)(id, SEL, id); using SendVoidBool = void (*)(id, SEL, BOOL); using SendIsKind = BOOL (*)(id, SEL, Class); const auto sendId = reinterpret_cast(objc_msgSend); const auto sendVoidId = reinterpret_cast(objc_msgSend); const auto sendVoidBool = reinterpret_cast(objc_msgSend); const auto sendIsKind = reinterpret_cast(objc_msgSend); id window = reinterpret_cast(wminfo.info.cocoa.window); id view = sendId(window, sel_registerName("contentView")); if (view == nil) throw Exception("WebGPU: SDL Cocoa window has no content view"); sendVoidBool(view, sel_registerName("setWantsLayer:"), YES); id layer = sendId(view, sel_registerName("layer")); Class metalLayerClass = objc_getClass("CAMetalLayer"); if (metalLayerClass == Nil) throw Exception("WebGPU: CAMetalLayer class unavailable"); if (layer == nil || !sendIsKind(layer, sel_registerName("isKindOfClass:"), metalLayerClass)) { layer = sendId(reinterpret_cast(metalLayerClass), sel_registerName("layer")); sendVoidId(view, sel_registerName("setLayer:"), layer); } if (layer == nil) throw Exception("WebGPU: failed to create CAMetalLayer"); WGPUSurfaceSourceMetalLayer metalChain{}; metalChain.chain.sType = WGPUSType_SurfaceSourceMetalLayer; metalChain.layer = layer; surfDesc.nextInChain = &metalChain.chain; surface = instance.CreateSurface(reinterpret_cast(&surfDesc)); #else throw Exception("WebGPU: unsupported native platform for surface creation"); #endif if (!surface) throw Exception("WebGPU: surface creation failed"); // Preferred surface format comes from wgpuSurfaceGetCapabilities in the // current ABI (Surface::GetPreferredFormat was removed). WGPUSurfaceCapabilities caps{}; if (wgpuSurfaceGetCapabilities(surface.Get(), adapter.Get(), &caps) == WGPUStatus_Success && caps.formatCount > 0) { // Match Vulkan's display-space convention: the scene tone-map pass // writes sRGB-encoded values to UNORM, then UI is composited without // an additional hardware transfer function. Fall back only when the // platform does not expose either common UNORM surface format. surfaceFormat = caps.formats[0]; for (size_t i = 0; i < caps.formatCount; ++i) { if (caps.formats[i] == WGPUTextureFormat_BGRA8Unorm) { surfaceFormat = caps.formats[i]; break; } if (caps.formats[i] == WGPUTextureFormat_RGBA8Unorm) { surfaceFormat = caps.formats[i]; } } } wgpuSurfaceCapabilitiesFreeMembers(caps); #endif swapchainConfigured = false; // Bind group layouts / pipelines must exist before the default textures // (texture bind groups reference the 2D / mesh3d layouts), and the shadow // depth array must exist before mesh bind groups are built at flush time. createPipelineResources(); createShadowResources(); createDefaultTextures(); initialized = true; deviceInitDone = true; } void Graphics::setVSync(bool enabled) { if (vsyncEnabled == enabled) return; eve::graphics::Graphics::setVSync(enabled); markSwapchainDirty(); } void Graphics::setViewportSize(int width, int height, int pixelwidth, int pixelheight) { // Mirror the Vulkan backend: keep the base-class viewport members in sync // so getWidth()/getPixelWidth() (used by RenderSystem3D for the GBuffer // size) return the real dimensions instead of 0. this->width = width; this->height = height; this->pixelWidth = pixelwidth; this->pixelHeight = pixelheight; logicalW = width; logicalH = height; pixelW = pixelwidth; pixelH = pixelheight; if (width <= 0 || height <= 0) return; if (initialized) configureSurface(width, height); } void Graphics::configureSurface(int width, int height) { if (!surface || !device || width <= 0 || height <= 0) return; WGPUSurfaceConfiguration cfg{}; cfg.nextInChain = nullptr; cfg.device = device.Get(); cfg.width = static_cast(width); cfg.height = static_cast(height); cfg.format = surfaceFormat; cfg.usage = WGPUTextureUsage_RenderAttachment; cfg.viewFormatCount = 0; cfg.viewFormats = nullptr; // emdawnwebgpu only accepts Fifo / Undefined present modes. cfg.presentMode = WGPUPresentMode_Fifo; // Auto (0) maps to an empty slot in emdawnwebgpu's JS CompositeAlphaMode // table �?`alphaMode: undefined`, which some browsers reject. Use Opaque, // which maps to the JS value 'opaque' used by the working replica. cfg.alphaMode = WGPUCompositeAlphaMode_Opaque; surface.Configure(reinterpret_cast(&cfg)); swapchainConfigured = true; // Recreate the offscreen targets at the new size. int w = pixelW > 0 ? pixelW : width; int h = pixelH > 0 ? pixelH : height; if (w > 0 && h > 0) { createSceneColorResources(w, h); createShadowResources(); } } void Graphics::rebuildSwapchainIfNeeded() { if (!swapchainConfigured && surface && logicalW > 0 && logicalH > 0) { configureSurface(logicalW, logicalH); } } // --------------------------------------------------------------------------- // Default / placeholder resources // --------------------------------------------------------------------------- void Graphics::createDefaultTextures() { uint8_t white[4] = {255, 255, 255, 255}; whiteTexture = static_cast(newTexture(1, 1, white, false, false)->gpuHandle); // Flat normal (0.5,0.5,1) in RGBA8 �?sampled as (0,0,1) normal. uint8_t flatNrm[4] = {128, 128, 255, 255}; flatNormalTexture = static_cast(newTexture(1, 1, flatNrm, false, false)->gpuHandle); flatNormalTexture3D = flatNormalTexture; uint8_t flatH[4] = {0, 0, 0, 255}; flatHeightTexture3D = static_cast(newTexture(1, 1, flatH, false, false)->gpuHandle); // 1x1 depth placeholder for the mesh3d scene-depth binding (9). Only X-ray // shaders sample it; every other mesh draw binds this unused depth view. { auto *gpu = new GpuTexture(); WGPUTextureDescriptor td{}; td.label = sv("eve_flat_depth"); td.dimension = WGPUTextureDimension_2D; td.size = {1, 1, 1}; td.sampleCount = 1; td.format = WGPUTextureFormat_Depth32Float; td.mipLevelCount = 1; td.usage = WGPUTextureUsage_TextureBinding; gpu->texture = device.CreateTexture(reinterpret_cast(&td)); WGPUTextureViewDescriptor vd{}; vd.format = WGPUTextureFormat_Depth32Float; vd.dimension = WGPUTextureViewDimension_2D; vd.baseMipLevel = 0; vd.mipLevelCount = 1; vd.baseArrayLayer = 0; vd.arrayLayerCount = 1; gpu->view = gpu->texture.CreateView(reinterpret_cast(&vd)); gpu->width = 1; gpu->height = 1; flatDepthTexture3D = gpu; } // 1x1x3 depth-array placeholder for the mesh3d shadow bindings (5/8). // sampleShadowPCF() early-outs (bias.y < 0.5) when shadows are disabled, // so the texture is never actually sampled. { auto *gpu = new GpuTexture(); WGPUTextureDescriptor td{}; td.label = sv("eve_default_shadow_depth"); td.dimension = WGPUTextureDimension_2D; td.size = {1, 1, 3}; td.sampleCount = 1; td.format = WGPUTextureFormat_Depth32Float; td.mipLevelCount = 1; td.usage = WGPUTextureUsage_TextureBinding; gpu->texture = device.CreateTexture(reinterpret_cast(&td)); WGPUTextureViewDescriptor vd{}; vd.format = WGPUTextureFormat_Depth32Float; vd.dimension = WGPUTextureViewDimension_2DArray; vd.baseMipLevel = 0; vd.mipLevelCount = 1; vd.baseArrayLayer = 0; vd.arrayLayerCount = 3; gpu->view = gpu->texture.CreateView(reinterpret_cast(&vd)); WGPUSamplerDescriptor sd{}; sd.label = sv("eve_default_shadow_sampler"); sd.addressModeU = WGPUAddressMode_ClampToEdge; sd.addressModeV = WGPUAddressMode_ClampToEdge; sd.addressModeW = WGPUAddressMode_ClampToEdge; sd.magFilter = WGPUFilterMode_Linear; sd.minFilter = WGPUFilterMode_Linear; sd.mipmapFilter = WGPUMipmapFilterMode_Nearest; sd.compare = WGPUCompareFunction_LessEqual; sd.maxAnisotropy = 1.f; gpu->sampler = device.CreateSampler(reinterpret_cast(&sd)); defaultShadowTex = gpu; } // Default env cubemap is black so an unset environment (RenderSystem3D // still passes envIntensity=1.0 by default) does not wash the scene // toward white. Only an explicitly set envMap contributes IBL. uint8_t cubeFace[4] = {0, 0, 0, 0}; uint8_t cubeData[24]; for (int f = 0; f < 6; ++f) std::memcpy(cubeData + f * 4, cubeFace, 4); defaultEnvCubemap = static_cast(newCubemap(1, cubeData)->gpuHandle); // Shared filtering sampler for WGSL bindings declared as plain `sampler`. TextureSampler def; mainSampler = makeSampler(def, 1); const uint8_t transparent[4] = {0, 0, 0, 0}; const uint8_t decalNormal[4] = {128, 128, 255, 0}; const uint8_t decalParams[4] = {0, 0, 0, 0}; decalFlatAlbedo = newTexture(1, 1, transparent); decalFlatNormal = newTexture(1, 1, decalNormal); decalFlatParams = newTexture(1, 1, decalParams); } // --------------------------------------------------------------------------- // Pipeline resource creation // --------------------------------------------------------------------------- void Graphics::createPipelineResources() { // Only layouts are required during initialization. Native Dawn compiles // render pipelines synchronously, so eagerly materializing every blend, // depth, cull, target and MSAA variant made each isolated CTest spend // roughly a minute compiling pipelines it never used. create2DPipelines(); createMesh3DPipelines(); } // --------------------------------------------------------------------------- // Bind group layouts // --------------------------------------------------------------------------- wgpu::BindGroupLayout Graphics::make2DBindGroupLayout() { BindGroupLayoutBuilder b; // 0: color texture b.texture(0, wgpu::ShaderStage::Fragment, wgpu::TextureSampleType::Float, wgpu::TextureViewDimension::e2D); // 1: depth texture b.texture(1, wgpu::ShaderStage::Fragment, wgpu::TextureSampleType::Float, wgpu::TextureViewDimension::e2D); // 2: color sampler b.sampler(2, wgpu::ShaderStage::Fragment, wgpu::SamplerBindingType::Filtering); // 3: depth sampler b.sampler(3, wgpu::ShaderStage::Fragment, wgpu::SamplerBindingType::Filtering); // 4: Externals UBO (push-constant replacement, dynamic offset). Sized for // the largest consumer: custom 2D shaders (128 B) and lit2D (Lighting2DUBO). b.buffer(4, wgpu::ShaderStage::Vertex | wgpu::ShaderStage::Fragment, wgpu::BufferBindingType::Uniform, true, std::max(Shader::kPushConstantBytes, uint32_t(sizeof(Lighting2DUBO)))); b.texture(5, wgpu::ShaderStage::Fragment, wgpu::TextureSampleType::Float, wgpu::TextureViewDimension::e2D); b.sampler(6, wgpu::ShaderStage::Fragment, wgpu::SamplerBindingType::Filtering); b.texture(7, wgpu::ShaderStage::Fragment, wgpu::TextureSampleType::Float, wgpu::TextureViewDimension::e2D); b.sampler(8, wgpu::ShaderStage::Fragment, wgpu::SamplerBindingType::Filtering); b.texture(9, wgpu::ShaderStage::Fragment, wgpu::TextureSampleType::Float, wgpu::TextureViewDimension::e2D); b.sampler(10, wgpu::ShaderStage::Fragment, wgpu::SamplerBindingType::Filtering); return b.build(device, "eve_2d"); } wgpu::BindGroupLayout Graphics::makeMesh3DBindGroupLayout() { BindGroupLayoutBuilder b; // 0: Frame UBO (dynamic) b.buffer(0, wgpu::ShaderStage::Vertex | wgpu::ShaderStage::Fragment, wgpu::BufferBindingType::Uniform, true, sizeof(Mesh3DUBO)); // 1: albedo b.texture(1, wgpu::ShaderStage::Fragment, wgpu::TextureSampleType::Float, wgpu::TextureViewDimension::e2D); // 2: normal b.texture(2, wgpu::ShaderStage::Fragment, wgpu::TextureSampleType::Float, wgpu::TextureViewDimension::e2D); // 3: env cubemap b.texture(3, wgpu::ShaderStage::Fragment, wgpu::TextureSampleType::Float, wgpu::TextureViewDimension::Cube); // 4: Shadow UBO (dynamic) b.buffer(4, wgpu::ShaderStage::Fragment, wgpu::BufferBindingType::Uniform, true, sizeof(ShadowUBO)); // 5: shadow depth array b.texture(5, wgpu::ShaderStage::Fragment, wgpu::TextureSampleType::Depth, wgpu::TextureViewDimension::e2DArray); // 6: height b.texture(6, wgpu::ShaderStage::Fragment, wgpu::TextureSampleType::Float, wgpu::TextureViewDimension::e2D); // 7: shared filtering sampler (WGSL `mainSamp`) b.sampler(7, wgpu::ShaderStage::Fragment, wgpu::SamplerBindingType::Filtering); // 8: shadow comparison sampler b.sampler(8, wgpu::ShaderStage::Fragment, wgpu::SamplerBindingType::Comparison); // 9: scene depth (G-buffer hwDepth; X-ray shaders sample it). Depth view, // sampled via textureSampleLevel with the shared mainSamp (binding 7). b.texture(9, wgpu::ShaderStage::Fragment, wgpu::TextureSampleType::Depth, wgpu::TextureViewDimension::e2D); // 10: SSAO occlusion texture (white when AO is disabled) b.texture(10, wgpu::ShaderStage::Fragment, wgpu::TextureSampleType::Float, wgpu::TextureViewDimension::e2D); // 11: shared AO sampler b.sampler(11, wgpu::ShaderStage::Fragment, wgpu::SamplerBindingType::Filtering); for (uint32_t i = 12; i <= 14; ++i) { b.texture(i, wgpu::ShaderStage::Fragment, wgpu::TextureSampleType::Float, wgpu::TextureViewDimension::e2D); } // Custom mesh shaders may use their external parameters for vertex // deformation as well as fragment shading (for example grass billboards). b.buffer(15, wgpu::ShaderStage::Vertex | wgpu::ShaderStage::Fragment, wgpu::BufferBindingType::Uniform, true, Shader::kPushConstantBytes); b.texture(16, wgpu::ShaderStage::Fragment, wgpu::TextureSampleType::Float, wgpu::TextureViewDimension::Cube); b.texture(17, wgpu::ShaderStage::Fragment, wgpu::TextureSampleType::Float, wgpu::TextureViewDimension::Cube); return b.build(device, "eve_mesh3d"); } wgpu::BindGroupLayout Graphics::makeShadowBindGroupLayout() { BindGroupLayoutBuilder b; b.buffer(0, wgpu::ShaderStage::Vertex, wgpu::BufferBindingType::Uniform, true, sizeof(SkinPassUBO)); b.texture(1, wgpu::ShaderStage::Fragment, wgpu::TextureSampleType::Float, wgpu::TextureViewDimension::e2D); b.sampler(2, wgpu::ShaderStage::Fragment, wgpu::SamplerBindingType::Filtering); return b.build(device, "eve_shadow"); } wgpu::BindGroupLayout Graphics::makeGbufferBindGroupLayout() { BindGroupLayoutBuilder b; // 0: pass and skinning UBO (dynamic) b.buffer(0, wgpu::ShaderStage::Vertex | wgpu::ShaderStage::Fragment, wgpu::BufferBindingType::Uniform, true, sizeof(SkinPassUBO)); // 1: albedo b.texture(1, wgpu::ShaderStage::Fragment, wgpu::TextureSampleType::Float, wgpu::TextureViewDimension::e2D); // 2: sampler b.sampler(2, wgpu::ShaderStage::Fragment, wgpu::SamplerBindingType::Filtering); return b.build(device, "eve_gbuffer"); } wgpu::BindGroupLayout Graphics::makeDecalBindGroupLayout() { BindGroupLayoutBuilder b; b.buffer(0, wgpu::ShaderStage::Fragment, wgpu::BufferBindingType::Uniform, true, 240); for (uint32_t i = 1; i <= 3; ++i) { b.texture(i, wgpu::ShaderStage::Fragment, wgpu::TextureSampleType::Float, wgpu::TextureViewDimension::e2D); } b.texture(4, wgpu::ShaderStage::Fragment, wgpu::TextureSampleType::Depth, wgpu::TextureViewDimension::e2D); b.texture(5, wgpu::ShaderStage::Fragment, wgpu::TextureSampleType::Float, wgpu::TextureViewDimension::e2D); b.sampler(6, wgpu::ShaderStage::Fragment, wgpu::SamplerBindingType::Filtering); return b.build(device, "eve_decal"); } wgpu::BindGroupLayout Graphics::makeVoxelBindGroupLayout() { BindGroupLayoutBuilder b; // 0: PC UBO (dynamic) b.buffer(0, wgpu::ShaderStage::Vertex, wgpu::BufferBindingType::Uniform, true, 112); // 1: atlas texture b.texture(1, wgpu::ShaderStage::Fragment, wgpu::TextureSampleType::Float, wgpu::TextureViewDimension::e2D); // 2: atlas sampler b.sampler(2, wgpu::ShaderStage::Fragment, wgpu::SamplerBindingType::Filtering); return b.build(device, "eve_voxel"); } wgpu::PipelineLayout Graphics::make2DPipelineLayout() { wgpu::PipelineLayoutDescriptor d{}; d.label = "eve_2d_layout"; d.bindGroupLayoutCount = 1; d.bindGroupLayouts = &tex2DSetLayout; return device.CreatePipelineLayout(&d); } wgpu::PipelineLayout Graphics::makeMesh3DPipelineLayout() { wgpu::PipelineLayoutDescriptor d{}; d.label = "eve_mesh3d_layout"; d.bindGroupLayoutCount = 1; d.bindGroupLayouts = &mesh3dSetLayout; return device.CreatePipelineLayout(&d); } wgpu::BindGroupLayout Graphics::makeMesh3DClusteredBindGroupLayout() { BindGroupLayoutBuilder b; // 0: Frame UBO (dynamic; clustered layout) b.buffer(0, wgpu::ShaderStage::Vertex | wgpu::ShaderStage::Fragment, wgpu::BufferBindingType::Uniform, true, sizeof(Mesh3DClusteredUBO)); // 1: albedo b.texture(1, wgpu::ShaderStage::Fragment, wgpu::TextureSampleType::Float, wgpu::TextureViewDimension::e2D); // 2: normal map b.texture(2, wgpu::ShaderStage::Fragment, wgpu::TextureSampleType::Float, wgpu::TextureViewDimension::e2D); // 3: env cubemap b.texture(3, wgpu::ShaderStage::Fragment, wgpu::TextureSampleType::Float, wgpu::TextureViewDimension::Cube); // 4: shadow UBO (dynamic) b.buffer(4, wgpu::ShaderStage::Fragment, wgpu::BufferBindingType::Uniform, true, sizeof(ShadowUBO)); // 5: shadow depth array b.texture(5, wgpu::ShaderStage::Fragment, wgpu::TextureSampleType::Depth, wgpu::TextureViewDimension::e2DArray); // 6: height/parallax (unused fallback) b.texture(6, wgpu::ShaderStage::Fragment, wgpu::TextureSampleType::Float, wgpu::TextureViewDimension::e2D); // 7: shared filtering sampler b.sampler(7, wgpu::ShaderStage::Fragment, wgpu::SamplerBindingType::Filtering); // 8: shadow comparison sampler b.sampler(8, wgpu::ShaderStage::Fragment, wgpu::SamplerBindingType::Comparison); // 9: scene depth (G-buffer hwDepth; sampled by X-ray variants) b.texture(9, wgpu::ShaderStage::Fragment, wgpu::TextureSampleType::Depth, wgpu::TextureViewDimension::e2D); // 10..12: clustered-forward SSBOs b.buffer(10, wgpu::ShaderStage::Fragment, wgpu::BufferBindingType::ReadOnlyStorage, false, sizeof(ClusteredLightGpu)); b.buffer(11, wgpu::ShaderStage::Fragment, wgpu::BufferBindingType::ReadOnlyStorage, false, sizeof(ClusterTableEntry)); b.buffer(12, wgpu::ShaderStage::Fragment, wgpu::BufferBindingType::ReadOnlyStorage, false, sizeof(uint32_t)); // 13/14: SSAO occlusion texture + sampler b.texture(13, wgpu::ShaderStage::Fragment, wgpu::TextureSampleType::Float, wgpu::TextureViewDimension::e2D); b.sampler(14, wgpu::ShaderStage::Fragment, wgpu::SamplerBindingType::Filtering); for (uint32_t i = 15; i <= 17; ++i) { b.texture(i, wgpu::ShaderStage::Fragment, wgpu::TextureSampleType::Float, wgpu::TextureViewDimension::e2D); } b.texture(18, wgpu::ShaderStage::Fragment, wgpu::TextureSampleType::Float, wgpu::TextureViewDimension::Cube); b.texture(19, wgpu::ShaderStage::Fragment, wgpu::TextureSampleType::Float, wgpu::TextureViewDimension::Cube); return b.build(device, "eve_mesh3d_clustered"); } wgpu::PipelineLayout Graphics::makeMesh3DClusteredPipelineLayout() { wgpu::PipelineLayoutDescriptor d{}; d.label = "eve_mesh3d_clustered_layout"; d.bindGroupLayoutCount = 1; d.bindGroupLayouts = &mesh3dClusteredSetLayout; return device.CreatePipelineLayout(&d); } wgpu::PipelineLayout Graphics::makeShadowPipelineLayout() { wgpu::PipelineLayoutDescriptor d{}; d.label = "eve_shadow_layout"; d.bindGroupLayoutCount = 1; d.bindGroupLayouts = &shadowSetLayout; return device.CreatePipelineLayout(&d); } wgpu::PipelineLayout Graphics::makeGbufferPipelineLayout() { wgpu::PipelineLayoutDescriptor d{}; d.label = "eve_gbuffer_layout"; d.bindGroupLayoutCount = 1; d.bindGroupLayouts = &gbufferSetLayout; return device.CreatePipelineLayout(&d); } wgpu::PipelineLayout Graphics::makeDecalPipelineLayout() { wgpu::PipelineLayoutDescriptor d{}; d.label = "eve_decal_layout"; d.bindGroupLayoutCount = 1; d.bindGroupLayouts = &decalSetLayout; return device.CreatePipelineLayout(&d); } wgpu::PipelineLayout Graphics::makeVoxelPipelineLayout() { wgpu::PipelineLayoutDescriptor d{}; d.label = "eve_voxel_layout"; d.bindGroupLayoutCount = 1; d.bindGroupLayouts = &voxelSetLayout; return device.CreatePipelineLayout(&d); } // --------------------------------------------------------------------------- // Pipelines // --------------------------------------------------------------------------- namespace { // Returns a RAII-held module: the caller must keep it alive across // CreateRenderPipeline. Returning a raw WGPUShaderModule would let the // temporary wgpu::ShaderModule destructor Release() the handle, dropping it // from emdawnwebgpu's jsObjects before the pipeline can reference it. wgpu::ShaderModule makeWgslModule(wgpu::Device &dev, const char *wgsl) { wgpu::ShaderSourceWGSL wgslDesc{}; wgslDesc.code = wgsl; wgpu::ShaderModuleDescriptor desc{}; desc.nextInChain = &wgslDesc; return dev.CreateShaderModule(&desc); } // Builds a WGSL shader-module descriptor (the chained WGPUShaderSourceWGSL is // static so its chain pointer stays valid). The returned descriptor is a value // that must outlive the CreateShaderModule call. WGPUShaderModuleDescriptor mdDesc(const std::string &code) { static WGPUShaderSourceWGSL wd{}; wd.chain.sType = WGPUSType_ShaderSourceWGSL; wd.code = sv(code.c_str()); WGPUShaderModuleDescriptor md{}; md.nextInChain = &wd.chain; return md; } void fillVertexLayout(WGPUVertexBufferLayout &layout, uint64_t stride, const WGPUVertexAttribute *attrs, uint32_t attrCount) { layout.arrayStride = stride; layout.stepMode = WGPUVertexStepMode_Vertex; layout.attributeCount = attrCount; layout.attributes = attrs; } WGPUBlendState alphaBlend() { WGPUBlendState b{}; b.color.srcFactor = WGPUBlendFactor_SrcAlpha; b.color.dstFactor = WGPUBlendFactor_OneMinusSrcAlpha; b.color.operation = WGPUBlendOperation_Add; b.alpha.srcFactor = WGPUBlendFactor_One; b.alpha.dstFactor = WGPUBlendFactor_OneMinusSrcAlpha; b.alpha.operation = WGPUBlendOperation_Add; return b; } WGPUBlendState noBlend() { WGPUBlendState b{}; b.color.srcFactor = WGPUBlendFactor_One; b.color.dstFactor = WGPUBlendFactor_Zero; b.color.operation = WGPUBlendOperation_Add; b.alpha.srcFactor = WGPUBlendFactor_One; b.alpha.dstFactor = WGPUBlendFactor_Zero; b.alpha.operation = WGPUBlendOperation_Add; return b; } WGPUBlendState additiveBlend() { WGPUBlendState b{}; b.color.srcFactor = WGPUBlendFactor_SrcAlpha; b.color.dstFactor = WGPUBlendFactor_One; b.color.operation = WGPUBlendOperation_Add; b.alpha.srcFactor = WGPUBlendFactor_One; b.alpha.dstFactor = WGPUBlendFactor_One; b.alpha.operation = WGPUBlendOperation_Add; return b; } WGPUBlendState premultipliedBlend() { WGPUBlendState b = alphaBlend(); b.color.srcFactor = WGPUBlendFactor_One; return b; } WGPUBlendState multiplyBlend() { WGPUBlendState b = alphaBlend(); b.color.srcFactor = WGPUBlendFactor_Dst; return b; } size_t meshPipelineIndex(BlendMode blend, bool depthWrite, bool doubleSided) { return size_t(blend) * 4u + (depthWrite ? 2u : 0u) + (doubleSided ? 1u : 0u); } WGPUBlendState blendState(BlendMode mode) { switch (mode) { case BlendMode::Additive: return additiveBlend(); case BlendMode::Premultiplied: return premultipliedBlend(); case BlendMode::Multiply: return multiplyBlend(); case BlendMode::Opaque: return noBlend(); case BlendMode::Alpha: default: return alphaBlend(); } } } // namespace namespace { wgpu::RenderPipeline make2DColorPipeline(wgpu::Device &dev, WGPUTextureFormat format, BlendMode mode) { wgpu::VertexAttribute attrs[2] = {}; attrs[0].format = wgpu::VertexFormat::Float32x2; attrs[0].offset = 0; attrs[0].shaderLocation = 0; attrs[1].format = wgpu::VertexFormat::Float32x4; attrs[1].offset = 8; attrs[1].shaderLocation = 1; PipelineBuilder b; // The solid-color shader declares no bindings, so it must use an empty // (auto-derived) pipeline layout. Reusing the textured 2D layout here would // require a bind group for every solid draw. b.vertexLayout(24, wgpu::VertexStepMode::Vertex, attrs, 2); b.shader(makeWgslModule(dev, kColorVertWgsl), "vs_main", makeWgslModule(dev, kColorFragWgsl), "fs_main"); b.colorTarget(format, mode); // The WGSL vertex shader mirrors clip Y to match Vulkan, which flips // object-space CCW winding in framebuffer space. b.frontFace(wgpu::FrontFace::CW); b.cull(wgpu::CullMode::Back); return b.build(dev); } wgpu::RenderPipeline make2DTexturedPipeline(wgpu::Device &dev, wgpu::PipelineLayout layout, WGPUTextureFormat format, BlendMode mode, bool toneMapScene = false) { WGPUVertexAttribute attrs[3] = {}; attrs[0].format = WGPUVertexFormat_Float32x2; attrs[0].offset = 0; attrs[0].shaderLocation = 0; attrs[1].format = WGPUVertexFormat_Float32x4; attrs[1].offset = 8; attrs[1].shaderLocation = 1; attrs[2].format = WGPUVertexFormat_Float32x2; attrs[2].offset = 24; attrs[2].shaderLocation = 2; WGPUVertexBufferLayout vb{}; fillVertexLayout(vb, 32, attrs, 3); WGPUColorTargetState target{}; target.format = format; target.writeMask = WGPUColorWriteMask_All; WGPUBlendState bs = alphaBlend(); if (mode == BlendMode::Additive) bs = additiveBlend(); else if (mode == BlendMode::Premultiplied) bs = premultipliedBlend(); else if (mode == BlendMode::Multiply) bs = multiplyBlend(); else if (mode == BlendMode::Opaque) bs = noBlend(); if (mode != BlendMode::Opaque) target.blend = &bs; WGPURenderPipelineDescriptor pd{}; pd.label = sv("eve_textured2d"); pd.layout = layout.Get(); wgpu::ShaderModule vertModule = makeWgslModule(dev, kTexturedVertWgsl); static constexpr const char *kSceneTonemapFragWgsl = R"wgsl( struct FSIn { @location(0) color: vec4f, @location(1) uv: vec2f, }; @group(0) @binding(0) var colorTex: texture_2d; @group(0) @binding(2) var colorSampler: sampler; @group(0) @binding(1) var rawSceneTex: texture_2d; @group(0) @binding(3) var rawSceneSampler: sampler; fn acesFitted(c0: vec3f) -> vec3f { let c = max(c0, vec3f(0.0)); return clamp((c * (2.51 * c + vec3f(0.03))) / (c * (2.43 * c + vec3f(0.59)) + vec3f(0.14)), vec3f(0.0), vec3f(1.0)); } fn linearToSrgb(c: vec3f) -> vec3f { let low = c * 12.92; let high = 1.055 * pow(max(c, vec3f(0.0)), vec3f(1.0 / 2.4)) - vec3f(0.055); return select(low, high, c > vec3f(0.0031308)); } fn automaticExposure(packedLimits: f32, raw: bool) -> f32 { var logLuminance: array; var sampleIndex = 0; for (var y = 0; y < 4; y = y + 1) { for (var x = 0; x < 4; x = x + 1) { let uv = (vec2f(f32(x), f32(y)) + vec2f(0.5)) * 0.25; var sampleColor = textureSampleLevel(colorTex, colorSampler, uv, 0.0).rgb; if (raw) { sampleColor = textureSampleLevel(rawSceneTex, rawSceneSampler, uv, 0.0).rgb; } sampleColor = max(sampleColor, vec3f(0.0)); let luminance = dot(sampleColor, vec3f(0.2126, 0.7152, 0.0722)); logLuminance[sampleIndex] = log2(clamp(luminance, 0.0001, 65504.0)); sampleIndex += 1; } } for (var i = 1; i < 16; i = i + 1) { let value = logLuminance[i]; var j = i - 1; while (j >= 0 && logLuminance[j] > value) { logLuminance[j + 1] = logLuminance[j]; j -= 1; } logLuminance[j + 1] = value; } var trimmedLogSum = 0.0; for (var i = 2; i < 14; i = i + 1) { trimmedLogSum += logLuminance[i]; } let geometricMean = exp2(trimmedLogSum * (1.0 / 12.0)); let packed = round(packedLimits); let minEV = (packed - floor(packed * (1.0 / 256.0)) * 256.0) * (1.0 / 8.0) - 16.0; let maxEV = floor(packed * (1.0 / 256.0)) * (1.0 / 8.0) - 16.0; return clamp(0.18 / max(geometricMean, 0.0001), exp2(minEV), exp2(maxEV)); } fn bloomPrefilter(color: vec3f, threshold: f32) -> vec3f { let brightness = max(color.r, max(color.g, color.b)); let knee = max(threshold * 0.1, 0.01); let soft = clamp((brightness - threshold + knee) / (2.0 * knee), 0.0, 1.0); let contribution = max(brightness - threshold, soft * soft * knee); return color * (contribution / max(brightness, 0.0001)); } fn sampleBloom(uv: vec2f, threshold: f32) -> vec3f { let texel = 1.0 / vec2f(textureDimensions(colorTex)); var bloom = bloomPrefilter(textureSampleLevel(colorTex, colorSampler, uv, 0.0).rgb, threshold) * 0.2; let offsets = array( vec2f(2.00, 0.00), vec2f(-1.47, 1.35), vec2f(0.17, -1.99), vec2f(1.22, 1.58), vec2f(4.22, -2.68), vec2f(-4.92, -0.87), vec2f(3.04, 3.97), vec2f(-0.44, -4.98), vec2f(-5.28, 7.28), vec2f(8.89, -1.41), vec2f(-7.82, -4.46), vec2f(2.07, 8.76)); let weights = array( 0.10, 0.10, 0.10, 0.10, 0.06, 0.06, 0.06, 0.06, 0.04, 0.04, 0.04, 0.04); for (var q = 0; q < 12; q = q + 1) { bloom += bloomPrefilter(textureSampleLevel(colorTex, colorSampler, uv + offsets[q] * texel, 0.0).rgb, threshold) * weights[q]; } return bloom; } @fragment fn fs_main(in: FSIn) -> @location(0) vec4f { let hdr = textureSample(colorTex, colorSampler, in.uv); let exposure = 1.0; // Applied once by the dedicated HDR pre-exposure pass. let encodeSrgb = in.color.a >= 65536.0; let bloomPacked = round(in.color.a) - floor(round(in.color.a) * (1.0 / 65536.0)) * 65536.0; let bloomIntensity = (bloomPacked - floor(bloomPacked * (1.0 / 256.0)) * 256.0) * (1.0 / 32.0); let bloomThreshold = floor(bloomPacked * (1.0 / 256.0)) * (1.0 / 16.0); var bloom = vec3f(0.0); if (bloomIntensity > 0.0) { bloom = sampleBloom(in.uv, bloomThreshold); } var displayColor = acesFitted((hdr.rgb + bloom * bloomIntensity) * exposure); if (encodeSrgb) { displayColor = linearToSrgb(displayColor); } return vec4f(displayColor, hdr.a); } )wgsl"; wgpu::ShaderModule fragModule = makeWgslModule(dev, toneMapScene ? kSceneTonemapFragWgsl : kTexturedFragWgsl); pd.vertex.module = vertModule.Get(); pd.vertex.entryPoint = sv("vs_main"); pd.vertex.bufferCount = 1; pd.vertex.buffers = &vb; WGPUFragmentState fs{}; fs.module = fragModule.Get(); fs.entryPoint = sv("fs_main"); fs.targetCount = 1; fs.targets = ⌖ pd.fragment = &fs; pd.primitive.topology = WGPUPrimitiveTopology_TriangleList; pd.primitive.frontFace = WGPUFrontFace_CCW; pd.primitive.cullMode = WGPUCullMode_None; pd.primitive.stripIndexFormat = WGPUIndexFormat_Undefined; pd.depthStencil = nullptr; pd.multisample.count = 1; // Zero-init would leave mask=0, which discards every fragment // (sampleMask=0). The WebGPU default is 0xFFFFFFFF (all samples). pd.multisample.mask = 0xFFFFFFFFu; return dev.CreateRenderPipeline(reinterpret_cast(&pd)); } wgpu::RenderPipeline make2DLitPipeline(wgpu::Device &dev, wgpu::PipelineLayout layout, WGPUTextureFormat format, bool blend) { wgpu::VertexAttribute attrs[3] = {}; attrs[0].format = wgpu::VertexFormat::Float32x2; attrs[0].offset = 0; attrs[0].shaderLocation = 0; attrs[1].format = wgpu::VertexFormat::Float32x4; attrs[1].offset = 8; attrs[1].shaderLocation = 1; attrs[2].format = wgpu::VertexFormat::Float32x2; attrs[2].offset = 24; attrs[2].shaderLocation = 2; PipelineBuilder b; b.vertexLayout(32, wgpu::VertexStepMode::Vertex, attrs, 3); b.shader(makeWgslModule(dev, kLit2DVertWgsl), "vs_main", makeWgslModule(dev, kLit2DFragWgsl), "fs_main"); b.layout(layout); // Lit pipeline always uses alpha blending; `blend` currently true in all // call sites, so map to Alpha (or Opaque when disabled). b.colorTarget(format, blend ? BlendMode::Alpha : BlendMode::Opaque); return b.build(dev); } } // namespace void Graphics::create2DPipelines() { tex2DSetLayout = make2DBindGroupLayout(); tex2DPipelineLayout = make2DPipelineLayout(); colorPipeline = make2DColorPipeline(device, surfaceFormat, BlendMode::Alpha); texturedPipeline = make2DTexturedPipeline(device, tex2DPipelineLayout, surfaceFormat, BlendMode::Alpha); colorAdditivePipeline = make2DColorPipeline(device, surfaceFormat, BlendMode::Additive); texturedAdditivePipeline = make2DTexturedPipeline(device, tex2DPipelineLayout, surfaceFormat, BlendMode::Additive); colorPremultipliedPipeline = make2DColorPipeline(device, surfaceFormat, BlendMode::Premultiplied); texturedPremultipliedPipeline = make2DTexturedPipeline( device, tex2DPipelineLayout, surfaceFormat, BlendMode::Premultiplied); colorMultiplyPipeline = make2DColorPipeline(device, surfaceFormat, BlendMode::Multiply); texturedMultiplyPipeline = make2DTexturedPipeline( device, tex2DPipelineLayout, surfaceFormat, BlendMode::Multiply); colorOpaquePipeline = make2DColorPipeline(device, surfaceFormat, BlendMode::Opaque); texturedOpaquePipeline = make2DTexturedPipeline(device, tex2DPipelineLayout, surfaceFormat, BlendMode::Opaque); sceneTonemapPipeline = make2DTexturedPipeline(device, tex2DPipelineLayout, surfaceFormat, BlendMode::Opaque, true); lit2dPipeline = make2DLitPipeline(device, tex2DPipelineLayout, surfaceFormat, true); offscreenColorPipeline = make2DColorPipeline(device, WGPUTextureFormat_RGBA8Unorm, BlendMode::Alpha); offscreenTexturedPipeline = make2DTexturedPipeline(device, tex2DPipelineLayout, WGPUTextureFormat_RGBA8Unorm, BlendMode::Alpha); offscreenColorAdditivePipeline = make2DColorPipeline(device, WGPUTextureFormat_RGBA8Unorm, BlendMode::Additive); offscreenTexturedAdditivePipeline = make2DTexturedPipeline(device, tex2DPipelineLayout, WGPUTextureFormat_RGBA8Unorm, BlendMode::Additive); offscreenColorPremultipliedPipeline = make2DColorPipeline( device, WGPUTextureFormat_RGBA8Unorm, BlendMode::Premultiplied); offscreenTexturedPremultipliedPipeline = make2DTexturedPipeline( device, tex2DPipelineLayout, WGPUTextureFormat_RGBA8Unorm, BlendMode::Premultiplied); offscreenColorMultiplyPipeline = make2DColorPipeline( device, WGPUTextureFormat_RGBA8Unorm, BlendMode::Multiply); offscreenTexturedMultiplyPipeline = make2DTexturedPipeline( device, tex2DPipelineLayout, WGPUTextureFormat_RGBA8Unorm, BlendMode::Multiply); offscreenColorOpaquePipeline = make2DColorPipeline(device, WGPUTextureFormat_RGBA8Unorm, BlendMode::Opaque); offscreenTexturedOpaquePipeline = make2DTexturedPipeline(device, tex2DPipelineLayout, WGPUTextureFormat_RGBA8Unorm, BlendMode::Opaque); hdrOffscreenTexturedPipeline = make2DTexturedPipeline(device, tex2DPipelineLayout, WGPUTextureFormat_RGBA16Float, BlendMode::Alpha); hdrOffscreenTexturedOpaquePipeline = make2DTexturedPipeline(device, tex2DPipelineLayout, WGPUTextureFormat_RGBA16Float, BlendMode::Opaque); offscreenLitPipeline = make2DLitPipeline(device, tex2DPipelineLayout, WGPUTextureFormat_RGBA8Unorm, true); } wgpu::RenderPipeline Graphics::get2DColorPipeline(BlendMode blend, bool offscreen) { switch (blend) { case BlendMode::Additive: return offscreen ? offscreenColorAdditivePipeline : colorAdditivePipeline; case BlendMode::Premultiplied: return offscreen ? offscreenColorPremultipliedPipeline : colorPremultipliedPipeline; case BlendMode::Multiply: return offscreen ? offscreenColorMultiplyPipeline : colorMultiplyPipeline; case BlendMode::Opaque: return offscreen ? offscreenColorOpaquePipeline : colorOpaquePipeline; case BlendMode::Alpha: default: return offscreen ? offscreenColorPipeline : colorPipeline; } } wgpu::RenderPipeline Graphics::get2DTexturedPipeline(BlendMode blend, bool offscreen) { switch (blend) { case BlendMode::Additive: return offscreen ? offscreenTexturedAdditivePipeline : texturedAdditivePipeline; case BlendMode::Premultiplied: return offscreen ? offscreenTexturedPremultipliedPipeline : texturedPremultipliedPipeline; case BlendMode::Multiply: return offscreen ? offscreenTexturedMultiplyPipeline : texturedMultiplyPipeline; case BlendMode::Opaque: return offscreen ? offscreenTexturedOpaquePipeline : texturedOpaquePipeline; case BlendMode::Alpha: default: return offscreen ? offscreenTexturedPipeline : texturedPipeline; } } wgpu::RenderPipeline Graphics::get2DLitPipeline(bool offscreen) { return offscreen ? offscreenLitPipeline : lit2dPipeline; } void Graphics::createMesh3DPipelines() { mesh3dSetLayout = makeMesh3DBindGroupLayout(); // Bind groups reference the layout; drop cached groups when it changes. clearMeshBindGroupCache(); mesh3dPipelineLayout = makeMesh3DPipelineLayout(); } wgpu::RenderPipeline Graphics::getMesh3DPipeline(BlendMode blend, bool depthWrite, bool doubleSided, bool canvasTarget) { const size_t index = meshPipelineIndex(blend, depthWrite, doubleSided); wgpu::RenderPipeline &pipeline = canvasTarget ? mesh3dCanvasPipelines[index] : mesh3dPipelines[index]; if (pipeline) return pipeline; WGPUVertexAttribute attrs[5] = {}; fillMeshAttributes(attrs); WGPUVertexBufferLayout vb{}; fillVertexLayout(vb, sizeof(MeshVertex), attrs, 5); WGPUDepthStencilState ds{}; ds.format = WGPUTextureFormat_Depth32Float; ds.depthWriteEnabled = depthWrite ? WGPUOptionalBool_True : WGPUOptionalBool_False; ds.depthCompare = WGPUCompareFunction_Less; WGPUBlendState blendDesc = blendState(blend); WGPUColorTargetState target{}; target.format = sceneColorFormat; target.blend = blend == BlendMode::Opaque ? nullptr : &blendDesc; target.writeMask = WGPUColorWriteMask_All; wgpu::ShaderModule vertModule = makeWgslModule(device, kMesh3DVertWgsl); wgpu::ShaderModule fragModule = makeWgslModule(device, kMesh3DFragWgsl); WGPUFragmentState fs{}; fs.module = fragModule.Get(); fs.entryPoint = sv("fs_main"); fs.targetCount = 1; fs.targets = ⌖ WGPURenderPipelineDescriptor pd{}; pd.label = canvasTarget ? sv("eve_mesh3d_canvas_surface") : sv("eve_mesh3d_surface"); pd.layout = mesh3dPipelineLayout.Get(); pd.vertex.module = vertModule.Get(); pd.vertex.entryPoint = sv("vs_main"); pd.vertex.bufferCount = 1; pd.vertex.buffers = &vb; pd.fragment = &fs; pd.primitive.topology = WGPUPrimitiveTopology_TriangleList; pd.primitive.frontFace = WGPUFrontFace_CCW; pd.primitive.cullMode = doubleSided ? WGPUCullMode_None : WGPUCullMode_Back; pd.primitive.stripIndexFormat = WGPUIndexFormat_Undefined; pd.depthStencil = &ds; pd.multisample.count = canvasTarget ? 1 : sceneColorSamples; pd.multisample.mask = 0xFFFFFFFFu; for (int blendValue = int(BlendMode::Alpha); blendValue <= int(BlendMode::Multiply); ++blendValue) { const BlendMode blend = BlendMode(blendValue); for (int depthValue = 0; depthValue < 2; ++depthValue) { for (int doubleValue = 0; doubleValue < 2; ++doubleValue) { const bool depthWrite = depthValue != 0; const bool doubleSided = doubleValue != 0; const size_t index = meshPipelineIndex(blend, depthWrite, doubleSided); WGPUBlendState blendDesc = blendState(blend); target.blend = blend == BlendMode::Opaque ? nullptr : &blendDesc; target.format = sceneColorFormat; ds.depthWriteEnabled = depthWrite ? WGPUOptionalBool_True : WGPUOptionalBool_False; pd.primitive.cullMode = doubleSided ? WGPUCullMode_None : WGPUCullMode_Back; pd.multisample.count = sceneColorSamples; pd.label = sv("eve_mesh3d_surface"); mesh3dPipelines[index] = device.CreateRenderPipeline( reinterpret_cast(&pd)); target.format = WGPUTextureFormat_RGBA8Unorm; pd.multisample.count = 1; pd.label = sv("eve_mesh3d_canvas_surface"); mesh3dCanvasPipelines[index] = device.CreateRenderPipeline( reinterpret_cast(&pd)); } } } mesh3dPipeline = mesh3dPipelines[meshPipelineIndex(BlendMode::Opaque, true, false)]; mesh3dTransparentPipeline = mesh3dPipelines[meshPipelineIndex(BlendMode::Alpha, false, false)]; mesh3dCanvasPipeline = mesh3dCanvasPipelines[meshPipelineIndex(BlendMode::Opaque, true, false)]; } void Graphics::createMesh3DClusteredPipeline() { mesh3dClusteredSetLayout = makeMesh3DClusteredBindGroupLayout(); mesh3dClusteredPipelineLayout = makeMesh3DClusteredPipelineLayout(); WGPUVertexAttribute attrs[5] = {}; fillMeshAttributes(attrs); attrs[0].format = WGPUVertexFormat_Float32x3; // pos attrs[0].offset = 0; attrs[0].shaderLocation = 0; attrs[1].format = WGPUVertexFormat_Float32x3; // normal attrs[1].offset = 12; attrs[1].shaderLocation = 1; attrs[2].format = WGPUVertexFormat_Float32x2; // uv attrs[2].offset = 24; attrs[2].shaderLocation = 2; WGPUVertexBufferLayout vb{}; fillVertexLayout(vb, sizeof(MeshVertex), attrs, 5); WGPUDepthStencilState ds{}; ds.format = WGPUTextureFormat_Depth32Float; ds.depthWriteEnabled = WGPUOptionalBool_True; ds.depthCompare = WGPUCompareFunction_Less; ds.stencilReadMask = 0; ds.stencilWriteMask = 0; WGPUColorTargetState target{}; target.format = sceneColorFormat; target.blend = nullptr; target.writeMask = WGPUColorWriteMask_All; WGPURenderPipelineDescriptor pd{}; pd.label = sv("eve_mesh3d_clustered"); pd.layout = mesh3dClusteredPipelineLayout.Get(); wgpu::ShaderModule vertModule = makeWgslModule(device, kMesh3DClusteredVertWgsl); wgpu::ShaderModule fragModule = makeWgslModule(device, kMesh3DClusteredFragWgsl); pd.vertex.module = vertModule.Get(); pd.vertex.entryPoint = sv("vs_main"); pd.vertex.bufferCount = 1; pd.vertex.buffers = &vb; WGPUFragmentState fs{}; fs.module = fragModule.Get(); fs.entryPoint = sv("fs_main"); fs.targetCount = 1; fs.targets = ⌖ pd.fragment = &fs; pd.primitive.topology = WGPUPrimitiveTopology_TriangleList; pd.primitive.frontFace = WGPUFrontFace_CCW; pd.primitive.cullMode = WGPUCullMode_Back; pd.primitive.stripIndexFormat = WGPUIndexFormat_Undefined; pd.depthStencil = &ds; pd.multisample.count = sceneColorSamples; pd.multisample.mask = 0xFFFFFFFFu; mesh3dClusteredPipeline = device.CreateRenderPipeline(reinterpret_cast(&pd)); } void Graphics::createShadowPipelines() { shadowSetLayout = makeShadowBindGroupLayout(); shadowPipelineLayout = makeShadowPipelineLayout(); WGPUVertexAttribute attrs[5] = {}; fillMeshAttributes(attrs); attrs[0].format = WGPUVertexFormat_Float32x3; attrs[0].offset = 0; attrs[0].shaderLocation = 0; attrs[1].format = WGPUVertexFormat_Float32x3; attrs[1].offset = 12; attrs[1].shaderLocation = 1; attrs[2].format = WGPUVertexFormat_Float32x2; attrs[2].offset = 24; attrs[2].shaderLocation = 2; WGPUVertexBufferLayout vb{}; fillVertexLayout(vb, sizeof(MeshVertex), attrs, 5); WGPUDepthStencilState ds{}; ds.format = WGPUTextureFormat_Depth32Float; ds.depthWriteEnabled = WGPUOptionalBool_True; ds.depthCompare = WGPUCompareFunction_Less; ds.stencilReadMask = 0; ds.stencilWriteMask = 0; WGPURenderPipelineDescriptor pd{}; pd.label = sv("eve_shadow"); pd.layout = shadowPipelineLayout.Get(); wgpu::ShaderModule vertModule = makeWgslModule(device, kMesh3DShadowVertWgsl); pd.vertex.module = vertModule.Get(); pd.vertex.entryPoint = sv("vs_main"); pd.vertex.bufferCount = 1; pd.vertex.buffers = &vb; pd.fragment = nullptr; pd.primitive.topology = WGPUPrimitiveTopology_TriangleList; pd.primitive.frontFace = WGPUFrontFace_CCW; pd.primitive.cullMode = WGPUCullMode_None; pd.primitive.stripIndexFormat = WGPUIndexFormat_Undefined; pd.depthStencil = &ds; pd.multisample.count = 1; // Zero-init would leave mask=0, which discards every fragment // (sampleMask=0). The WebGPU default is 0xFFFFFFFF (all samples). pd.multisample.mask = 0xFFFFFFFFu; mesh3dShadowPipeline = device.CreateRenderPipeline(reinterpret_cast(&pd)); wgpu::ShaderModule alphaVertModule = makeWgslModule(device, kMesh3DShadowAlphaVertWgsl); wgpu::ShaderModule alphaFragModule = makeWgslModule(device, kMesh3DShadowAlphaFragWgsl); pd.label = sv("eve_shadow_alpha"); pd.vertex.module = alphaVertModule.Get(); WGPUFragmentState alphaFs{}; alphaFs.module = alphaFragModule.Get(); alphaFs.entryPoint = sv("fs_main"); alphaFs.targetCount = 0; alphaFs.targets = nullptr; pd.fragment = &alphaFs; mesh3dShadowAlphaPipeline = device.CreateRenderPipeline(reinterpret_cast(&pd)); } void Graphics::createGbufferPipelines() { gbufferSetLayout = makeGbufferBindGroupLayout(); gbufferPipelineLayout = makeGbufferPipelineLayout(); WGPUVertexAttribute attrs[5] = {}; fillMeshAttributes(attrs); attrs[0].format = WGPUVertexFormat_Float32x3; attrs[0].offset = 0; attrs[0].shaderLocation = 0; attrs[1].format = WGPUVertexFormat_Float32x3; attrs[1].offset = 12; attrs[1].shaderLocation = 1; attrs[2].format = WGPUVertexFormat_Float32x2; attrs[2].offset = 24; attrs[2].shaderLocation = 2; WGPUVertexBufferLayout vb{}; fillVertexLayout(vb, sizeof(MeshVertex), attrs, 5); WGPUDepthStencilState ds{}; ds.format = WGPUTextureFormat_Depth32Float; ds.depthWriteEnabled = WGPUOptionalBool_True; ds.depthCompare = WGPUCompareFunction_Less; ds.stencilReadMask = 0; ds.stencilWriteMask = 0; WGPUColorTargetState targets[3] = {}; for (int i = 0; i < 3; ++i) { targets[i].format = WGPUTextureFormat_RGBA8Unorm; targets[i].blend = nullptr; targets[i].writeMask = WGPUColorWriteMask_All; } WGPURenderPipelineDescriptor pd{}; pd.label = sv("eve_gbuffer"); pd.layout = gbufferPipelineLayout.Get(); wgpu::ShaderModule vertModule = makeWgslModule(device, kMesh3DGbufferVertWgsl); wgpu::ShaderModule fragModule = makeWgslModule(device, kMesh3DGbufferFragWgsl); pd.vertex.module = vertModule.Get(); pd.vertex.entryPoint = sv("vs_main"); pd.vertex.bufferCount = 1; pd.vertex.buffers = &vb; WGPUFragmentState fs{}; fs.module = fragModule.Get(); fs.entryPoint = sv("fs_main"); fs.targetCount = 3; fs.targets = targets; pd.fragment = &fs; pd.primitive.topology = WGPUPrimitiveTopology_TriangleList; pd.primitive.frontFace = WGPUFrontFace_CCW; pd.primitive.cullMode = WGPUCullMode_None; pd.primitive.stripIndexFormat = WGPUIndexFormat_Undefined; pd.depthStencil = &ds; pd.multisample.count = 1; // Zero-init would leave mask=0, which discards every fragment // (sampleMask=0). The WebGPU default is 0xFFFFFFFF (all samples). pd.multisample.mask = 0xFFFFFFFFu; mesh3dGbufferPipeline = device.CreateRenderPipeline(reinterpret_cast(&pd)); wgpu::ShaderModule alphaFragModule = makeWgslModule(device, kMesh3DGbufferAlphaFragWgsl); pd.label = sv("eve_gbuffer_alpha"); fs.module = alphaFragModule.Get(); mesh3dGbufferAlphaPipeline = device.CreateRenderPipeline(reinterpret_cast(&pd)); } void Graphics::createDecalPipeline() { decalSetLayout = makeDecalBindGroupLayout(); decalPipelineLayout = makeDecalPipelineLayout(); wgpu::ShaderModule vert = makeWgslModule(device, kDecalVertWgsl); wgpu::ShaderModule frag = makeWgslModule(device, kDecalFragWgsl); WGPUBlendState blend = alphaBlend(); WGPUColorTargetState targets[3]{}; for (auto &target : targets) { target.format = WGPUTextureFormat_RGBA8Unorm; target.blend = &blend; target.writeMask = WGPUColorWriteMask_All; } WGPUFragmentState fs{}; fs.module = frag.Get(); fs.entryPoint = sv("fs_main"); fs.targetCount = 3; fs.targets = targets; WGPURenderPipelineDescriptor pd{}; pd.label = sv("eve_decal"); pd.layout = decalPipelineLayout.Get(); pd.vertex.module = vert.Get(); pd.vertex.entryPoint = sv("vs_main"); pd.fragment = &fs; pd.primitive.topology = WGPUPrimitiveTopology_TriangleList; // kMesh3DVertWgsl mirrors clip-space Y, so object-space CCW becomes // clockwise in framebuffer space (the Vulkan pipeline uses Clockwise). pd.primitive.frontFace = WGPUFrontFace_CW; pd.primitive.cullMode = WGPUCullMode_None; pd.multisample.count = 1; pd.multisample.mask = 0xFFFFFFFFu; decalPipeline = device.CreateRenderPipeline(reinterpret_cast(&pd)); } void Graphics::createVoxelPipelines() { voxelSetLayout = makeVoxelBindGroupLayout(); voxelPipelineLayout = makeVoxelPipelineLayout(); WGPUVertexAttribute attrs[1] = {}; attrs[0].format = WGPUVertexFormat_Float32x2; // corner attrs[0].offset = 0; attrs[0].shaderLocation = 0; WGPUVertexBufferLayout cornerVb{}; cornerVb.arrayStride = 8; cornerVb.stepMode = WGPUVertexStepMode_Vertex; cornerVb.attributeCount = 1; cornerVb.attributes = attrs; WGPUVertexAttribute packedAttr{}; packedAttr.format = WGPUVertexFormat_Uint32; // packed rect word packedAttr.offset = 0; packedAttr.shaderLocation = 1; WGPUVertexBufferLayout packedVb{}; packedVb.arrayStride = 4; packedVb.stepMode = WGPUVertexStepMode_Instance; packedVb.attributeCount = 1; packedVb.attributes = &packedAttr; WGPUVertexAttribute aoAttr{}; aoAttr.format = WGPUVertexFormat_Uint32; // 2 bits per corner aoAttr.offset = 0; aoAttr.shaderLocation = 2; WGPUVertexBufferLayout aoVb{}; aoVb.arrayStride = 4; aoVb.stepMode = WGPUVertexStepMode_Instance; aoVb.attributeCount = 1; aoVb.attributes = &aoAttr; WGPUVertexBufferLayout vbs[3] = {cornerVb, packedVb, aoVb}; WGPUDepthStencilState ds{}; ds.format = WGPUTextureFormat_Depth32Float; ds.depthWriteEnabled = WGPUOptionalBool_True; ds.depthCompare = WGPUCompareFunction_Less; ds.stencilReadMask = 0; ds.stencilWriteMask = 0; WGPUColorTargetState target{}; target.format = sceneColorFormat; target.blend = nullptr; target.writeMask = WGPUColorWriteMask_All; WGPURenderPipelineDescriptor pd{}; pd.label = sv("eve_voxel"); pd.layout = voxelPipelineLayout.Get(); wgpu::ShaderModule vertModule = makeWgslModule(device, kVoxelRectVertWgsl); wgpu::ShaderModule fragModule = makeWgslModule(device, kVoxelRectFragWgsl); pd.vertex.module = vertModule.Get(); pd.vertex.entryPoint = sv("vs_main"); pd.vertex.bufferCount = 3; pd.vertex.buffers = vbs; WGPUFragmentState fs{}; fs.module = fragModule.Get(); fs.entryPoint = sv("fs_main"); fs.targetCount = 1; fs.targets = ⌖ pd.fragment = &fs; pd.primitive.topology = WGPUPrimitiveTopology_TriangleList; pd.primitive.frontFace = WGPUFrontFace_CCW; pd.primitive.cullMode = WGPUCullMode_None; pd.primitive.stripIndexFormat = WGPUIndexFormat_Undefined; pd.depthStencil = &ds; pd.multisample.count = sceneColorSamples; // Zero-init would leave mask=0, which discards every fragment // (sampleMask=0). The WebGPU default is 0xFFFFFFFF (all samples). pd.multisample.mask = 0xFFFFFFFFu; voxelRectPipeline = device.CreateRenderPipeline(reinterpret_cast(&pd)); // Unit quad for instanced voxel faces (2 triangles, corner + packed uv slot). float quad[8] = {0, 0, 1, 0, 1, 1, 0, 1}; WGPUBufferDescriptor bd{}; bd.label = sv("eve_voxel_quad"); bd.size = sizeof(quad); bd.usage = WGPUBufferUsage_CopyDst | WGPUBufferUsage_Vertex; bd.mappedAtCreation = false; voxelUnitQuadVerts = device.CreateBuffer(reinterpret_cast(&bd)); queue.WriteBuffer(voxelUnitQuadVerts, 0, quad, sizeof(quad)); uint32_t indices[6] = {0, 1, 2, 2, 3, 0}; WGPUBufferDescriptor ibd{}; ibd.label = sv("eve_voxel_quad_idx"); ibd.size = sizeof(indices); ibd.usage = WGPUBufferUsage_CopyDst | WGPUBufferUsage_Index; ibd.mappedAtCreation = false; voxelUnitQuadIndices = device.CreateBuffer(reinterpret_cast(&ibd)); queue.WriteBuffer(voxelUnitQuadIndices, 0, indices, sizeof(indices)); } // --------------------------------------------------------------------------- // SSAO (screen-space ambient occlusion) // --------------------------------------------------------------------------- void Graphics::ensureAOResources(int width, int height) { if (!device || width <= 0 || height <= 0) return; if (!aoSetLayout) { BindGroupLayoutBuilder b; b.buffer(0, wgpu::ShaderStage::Fragment, wgpu::BufferBindingType::Uniform, false, 32); b.texture(1, wgpu::ShaderStage::Fragment, wgpu::TextureSampleType::Depth, wgpu::TextureViewDimension::e2D); b.sampler(2, wgpu::ShaderStage::Fragment, wgpu::SamplerBindingType::Filtering); aoSetLayout = b.build(device, "eve_ao"); WGPUBindGroupLayout layouts[1] = {aoSetLayout.Get()}; WGPUPipelineLayoutDescriptor pl{}; pl.label = sv("eve_ao_layout"); pl.bindGroupLayoutCount = 1; pl.bindGroupLayouts = layouts; aoPipelineLayout = device.CreatePipelineLayout(reinterpret_cast(&pl)); WGPUVertexAttribute attrs[3]{}; attrs[0].format = WGPUVertexFormat_Float32x2; attrs[0].offset = 0; attrs[0].shaderLocation = 0; attrs[1].format = WGPUVertexFormat_Float32x4; attrs[1].offset = 8; attrs[1].shaderLocation = 1; attrs[2].format = WGPUVertexFormat_Float32x2; attrs[2].offset = 24; attrs[2].shaderLocation = 2; WGPUVertexBufferLayout vb{}; fillVertexLayout(vb, 32, attrs, 3); WGPUColorTargetState target{}; target.format = WGPUTextureFormat_RGBA8Unorm; target.blend = nullptr; target.writeMask = WGPUColorWriteMask_All; WGPURenderPipelineDescriptor pd{}; pd.label = sv("eve_ssao"); pd.layout = aoPipelineLayout.Get(); wgpu::ShaderModule vertModule = makeWgslModule(device, kTexturedVertWgsl); wgpu::ShaderModule fragModule = makeWgslModule(device, kSSAOFragWgsl); pd.vertex.module = vertModule.Get(); pd.vertex.entryPoint = sv("vs_main"); pd.vertex.bufferCount = 1; pd.vertex.buffers = &vb; WGPUFragmentState fs{}; fs.module = fragModule.Get(); fs.entryPoint = sv("fs_main"); fs.targetCount = 1; fs.targets = ⌖ pd.fragment = &fs; pd.primitive.topology = WGPUPrimitiveTopology_TriangleList; pd.primitive.frontFace = WGPUFrontFace_CCW; pd.primitive.cullMode = WGPUCullMode_None; pd.primitive.stripIndexFormat = WGPUIndexFormat_Undefined; pd.multisample.count = 1; pd.multisample.mask = 0xFFFFFFFFu; aoPipeline = device.CreateRenderPipeline(reinterpret_cast(&pd)); } if (!aoUbo) { WGPUBufferDescriptor bd{}; bd.label = sv("eve_ao_ubo"); bd.size = 64; bd.usage = WGPUBufferUsage_CopyDst | WGPUBufferUsage_Uniform; bd.mappedAtCreation = false; aoUbo = device.CreateBuffer(reinterpret_cast(&bd)); } if (!aoTex[0]) { for (int i = 0; i < 2; ++i) { WGPUTextureDescriptor td{}; td.label = sv("eve_ao"); td.dimension = WGPUTextureDimension_2D; // Half-resolution AO: the bilinear upsample in the mesh pass blurs // the per-pixel sampling grain that otherwise reads as noise. td.size = {static_cast((width + 1) / 2), static_cast((height + 1) / 2), 1}; td.sampleCount = 1; td.format = WGPUTextureFormat_RGBA8Unorm; td.mipLevelCount = 1; td.usage = WGPUTextureUsage_TextureBinding | WGPUTextureUsage_RenderAttachment; aoTex[i] = device.CreateTexture(reinterpret_cast(&td)); aoView[i] = aoTex[i].CreateView(); } aoReady = true; // New AO binding �?rebuild cached mesh bind groups. clearMeshBindGroupCache(); } if (!fullscreenQuadReady) { float verts[32] = { -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, 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, }; uint32_t indices[6] = {0, 1, 2, 2, 3, 0}; WGPUBufferDescriptor vbd{}; vbd.label = sv("eve_fullscreen_vb"); vbd.size = sizeof(verts); vbd.usage = WGPUBufferUsage_CopyDst | WGPUBufferUsage_Vertex; fullscreenQuadVb = device.CreateBuffer(reinterpret_cast(&vbd)); queue.WriteBuffer(fullscreenQuadVb, 0, verts, sizeof(verts)); WGPUBufferDescriptor ibd{}; ibd.label = sv("eve_fullscreen_ib"); ibd.size = sizeof(indices); ibd.usage = WGPUBufferUsage_CopyDst | WGPUBufferUsage_Index; fullscreenQuadIb = device.CreateBuffer(reinterpret_cast(&ibd)); queue.WriteBuffer(fullscreenQuadIb, 0, indices, sizeof(indices)); fullscreenQuadReady = true; } } wgpu::BindGroup Graphics::makeAOBindGroup(wgpu::TextureView depthView) { WGPUBindGroupEntry entries[3]{}; entries[0].binding = 0; entries[0].buffer = aoUbo.Get(); entries[0].size = 32; entries[1].binding = 1; entries[1].textureView = depthView.Get(); entries[2].binding = 2; entries[2].sampler = mainSampler.Get(); WGPUBindGroupDescriptor desc{}; desc.label = sv("eve_ao_group"); desc.layout = aoSetLayout.Get(); desc.entryCount = 3; desc.entries = entries; return device.CreateBindGroup(reinterpret_cast(&desc)); } // --------------------------------------------------------------------------- // Arena helpers // --------------------------------------------------------------------------- uint32_t Graphics::UboArena::alloc(uint64_t size, uint64_t alignment) { uint64_t aligned = (used + alignment - 1) / alignment * alignment; used = aligned + size; return static_cast(aligned); } uint64_t Graphics::VertexArena::alloc(uint64_t bytes) { uint64_t at = used; used += bytes; return at; } Graphics::UboArena &Graphics::currentUboArena() { return uboArenas[currentFrameSlot()]; } Graphics::VertexArena &Graphics::currentVertexArena() { return vertexArenas[currentFrameSlot()]; } void Graphics::ensureUboArena(UboArena &arena, uint64_t bytes) { if (arena.buffer && arena.capacity >= bytes) return; uint64_t size = arena.capacity; while (size < bytes) size = size ? size * 2 : (64u << 10); WGPUBufferDescriptor bd{}; bd.label = sv("eve_ubo_arena"); bd.size = size; bd.usage = WGPUBufferUsage_CopyDst | WGPUBufferUsage_Uniform; bd.mappedAtCreation = false; arena.buffer = device.CreateBuffer(reinterpret_cast(&bd)); arena.capacity = size; arena.used = 0; } void Graphics::ensureVertexArena(VertexArena &arena, uint64_t bytes) { if (arena.buffer && arena.capacity >= bytes) return; uint64_t size = arena.capacity; while (size < bytes) size = size ? size * 2 : (256u << 10); WGPUBufferDescriptor bd{}; bd.label = sv("eve_vertex_arena"); bd.size = size; bd.usage = WGPUBufferUsage_CopyDst | WGPUBufferUsage_Vertex; bd.mappedAtCreation = false; arena.buffer = device.CreateBuffer(reinterpret_cast(&bd)); arena.capacity = size; arena.used = 0; } // --------------------------------------------------------------------------- // Sampler // --------------------------------------------------------------------------- wgpu::Sampler Graphics::makeSampler(const TextureSampler &s, uint32_t mipLevels) const { WGPUSamplerDescriptor d{}; d.label = sv("eve_sampler"); d.addressModeU = s.repeatU ? WGPUAddressMode_Repeat : WGPUAddressMode_ClampToEdge; d.addressModeV = s.repeatV ? WGPUAddressMode_Repeat : WGPUAddressMode_ClampToEdge; d.addressModeW = s.repeatW ? WGPUAddressMode_Repeat : WGPUAddressMode_ClampToEdge; d.magFilter = s.mag == FilterMode::Nearest ? WGPUFilterMode_Nearest : WGPUFilterMode_Linear; d.minFilter = s.min == FilterMode::Nearest ? WGPUFilterMode_Nearest : WGPUFilterMode_Linear; if (s.mipmap == MipmapMode::Disabled || mipLevels <= 1) { d.mipmapFilter = WGPUMipmapFilterMode_Nearest; d.lodMinClamp = 0.f; d.lodMaxClamp = 0.f; } else { d.mipmapFilter = s.mipmap == MipmapMode::Nearest ? WGPUMipmapFilterMode_Nearest : WGPUMipmapFilterMode_Linear; d.lodMinClamp = s.minLod; d.lodMaxClamp = std::min(std::max(s.maxLod, 0.f), float(mipLevels - 1)); if (d.lodMaxClamp < d.lodMinClamp) d.lodMaxClamp = d.lodMinClamp; } float aniso = s.maxAnisotropy > 1.f ? s.maxAnisotropy : 1.f; d.maxAnisotropy = std::min(std::max(aniso, 1.f), std::max(maxSamplerAnisotropy, 1.f)); return device.CreateSampler(reinterpret_cast(&d)); } // --------------------------------------------------------------------------- // Texture creation // --------------------------------------------------------------------------- Texture *Graphics::newTexture(int width, int height, const uint8_t *rgba, bool repeatU, bool repeatV) { TextureCreateInfo info; info.sampler.repeatU = repeatU; info.sampler.repeatV = repeatV; return newTexture(width, height, rgba, info); } Texture *Graphics::newTexture(image::ImageData *data) { if (!data) throw Exception("newTexture: null ImageData"); TextureCreateInfo info; return newTexture(data->getWidth(), data->getHeight(), static_cast(data->getData()), info); } Texture *Graphics::newTexture(image::ImageData *data, const TextureCreateInfo &info) { if (!data) throw Exception("newTexture: null ImageData"); if (data->getFormat() != "RGBA8") throw Exception("newTexture: only RGBA8 supported"); return newTexture(data->getWidth(), data->getHeight(), static_cast(data->getData()), info); } Texture *Graphics::newTexture(int width, int height, const uint8_t *rgba, const TextureCreateInfo &rawInfo) { if (width <= 0 || height <= 0) throw Exception("newTexture: invalid size %dx%d", width, height); TextureCreateInfo info = rawInfo; if (info.generateMipmaps && info.sampler.mipmap == MipmapMode::Disabled) info.sampler.mipmap = MipmapMode::Linear; if (info.sampler.maxAnisotropy < 1.f) info.sampler.maxAnisotropy = 1.f; auto gpu = std::make_unique(); gpu->width = width; gpu->height = height; gpu->samplerState = info.sampler; gpu->mipLevels = info.generateMipmaps ? uint32_t(mipmapCountForSize(width, height)) : 1u; WGPUTextureDescriptor td{}; td.label = sv("eve_tex2d"); td.dimension = WGPUTextureDimension_2D; td.size.width = static_cast(width); td.size.height = static_cast(height); td.size.depthOrArrayLayers = 1; td.sampleCount = 1; td.format = WGPUTextureFormat_RGBA8Unorm; td.mipLevelCount = gpu->mipLevels; td.usage = WGPUTextureUsage_TextureBinding | WGPUTextureUsage_CopyDst | WGPUTextureUsage_RenderAttachment; gpu->texture = device.CreateTexture(reinterpret_cast(&td)); uploadTexturePixelsMips(gpu.get(), rgba, width, height); WGPUTextureViewDescriptor vd{}; vd.format = WGPUTextureFormat_RGBA8Unorm; vd.dimension = WGPUTextureViewDimension_2D; vd.baseMipLevel = 0; vd.mipLevelCount = gpu->mipLevels; vd.baseArrayLayer = 0; vd.arrayLayerCount = 1; gpu->view = gpu->texture.CreateView(reinterpret_cast(&vd)); gpu->sampler = makeSampler(info.sampler, gpu->mipLevels); auto *tex = new Texture(); tex->width = width; tex->height = height; tex->mipmapCount = int(gpu->mipLevels); tex->sampler = info.sampler; tex->gpuHandle = gpu.get(); ownedGpuTextures.push_back(std::move(gpu)); ownedTextures.push_back(std::unique_ptr(tex)); return tex; } void Graphics::uploadTexturePixels(GpuTexture *gt, const uint8_t *rgba, int w, int h, const TextureCreateInfo &info) { (void)info; uploadTexturePixelsMips(gt, rgba, w, h); } void Graphics::uploadTexturePixelsMips(GpuTexture *gt, const uint8_t *rgba, int w, int h) { if (!rgba) return; std::vector current; WGPUTexelCopyBufferLayout layout{}; layout.offset = 0; layout.bytesPerRow = static_cast(w * 4); layout.rowsPerImage = static_cast(h); WGPUExtent3D extent{static_cast(w), static_cast(h), 1}; for (uint32_t m = 0; m < gt->mipLevels; ++m) { WGPUTexelCopyTextureInfo dst{}; dst.texture = gt->texture.Get(); dst.mipLevel = m; dst.aspect = WGPUTextureAspect_All; queue.WriteTexture(reinterpret_cast(&dst), rgba, static_cast(w) * h * 4, reinterpret_cast(&layout), reinterpret_cast(&extent)); if (m + 1 < gt->mipLevels) { // Box-filter downsample into the CPU buffer for the next level. const int nextW = std::max(w / 2, 1); const int nextH = std::max(h / 2, 1); std::vector next(size_t(nextW) * size_t(nextH) * 4u); for (int y = 0; y < nextH; ++y) { const int y0 = std::min(y * 2, h - 1); const int y1 = std::min(y0 + 1, h - 1); for (int x = 0; x < nextW; ++x) { const int x0 = std::min(x * 2, w - 1); const int x1 = std::min(x0 + 1, w - 1); for (int c = 0; c < 4; ++c) { uint32_t acc = 0; acc += rgba[(size_t(y0) * w + x0) * 4u + c]; acc += rgba[(size_t(y0) * w + x1) * 4u + c]; acc += rgba[(size_t(y1) * w + x0) * 4u + c]; acc += rgba[(size_t(y1) * w + x1) * 4u + c]; next[(size_t(y) * nextW + x) * 4u + c] = uint8_t((acc + 2u) / 4u); } } } current = std::move(next); w = nextW; h = nextH; rgba = current.data(); layout.bytesPerRow = static_cast(w * 4); layout.rowsPerImage = static_cast(h); extent = {static_cast(w), static_cast(h), 1}; } } } Texture *Graphics::newCubemap(int faceSize, const uint8_t *rgbaFaces) { TextureCreateInfo info; return newCubemap(faceSize, rgbaFaces, info); } Texture *Graphics::newCubemap(int faceSize, const uint8_t *rgbaFaces, const TextureCreateInfo &rawInfo) { if (faceSize <= 0 || !rgbaFaces) throw Exception("newCubemap: invalid size or null data"); TextureCreateInfo info = rawInfo; if (info.generateMipmaps && info.sampler.mipmap == MipmapMode::Disabled) info.sampler.mipmap = MipmapMode::Linear; if (info.sampler.maxAnisotropy < 1.f) info.sampler.maxAnisotropy = 1.f; auto gpu = std::make_unique(); gpu->width = faceSize; gpu->height = faceSize; gpu->samplerState = info.sampler; gpu->isCube = true; gpu->mipLevels = info.generateMipmaps ? uint32_t(mipmapCountForSize(faceSize, faceSize)) : 1u; WGPUTextureDescriptor td{}; td.label = sv("eve_cubemap"); td.dimension = WGPUTextureDimension_2D; td.size.width = static_cast(faceSize); td.size.height = static_cast(faceSize); td.size.depthOrArrayLayers = 6; td.sampleCount = 1; td.format = WGPUTextureFormat_RGBA8Unorm; td.mipLevelCount = gpu->mipLevels; td.usage = WGPUTextureUsage_TextureBinding | WGPUTextureUsage_CopyDst; gpu->texture = device.CreateTexture(reinterpret_cast(&td)); const std::vector mipChain = buildGgxCubemapMipChain(rgbaFaces, uint32_t(faceSize), gpu->mipLevels); size_t mipOffset = 0; int mipW = faceSize; int mipH = faceSize; for (uint32_t mip = 0; mip < gpu->mipLevels; ++mip) { const size_t mipFaceBytes = size_t(mipW) * mipH * 4u; for (int f = 0; f < 6; ++f) { const uint8_t *pixels = mipChain.data() + mipOffset + size_t(f) * mipFaceBytes; WGPUTexelCopyBufferLayout layout{}; layout.offset = 0; layout.bytesPerRow = static_cast(mipW * 4); layout.rowsPerImage = static_cast(mipH); WGPUExtent3D extent{static_cast(mipW), static_cast(mipH), 1}; WGPUTexelCopyTextureInfo dst{}; dst.texture = gpu->texture.Get(); dst.mipLevel = mip; dst.aspect = WGPUTextureAspect_All; dst.origin = {0, 0, static_cast(f)}; queue.WriteTexture(reinterpret_cast(&dst), pixels, static_cast(mipW) * mipH * 4, reinterpret_cast(&layout), reinterpret_cast(&extent)); } mipOffset += mipFaceBytes * 6u; mipW = std::max(mipW / 2, 1); mipH = std::max(mipH / 2, 1); } WGPUTextureViewDescriptor vd{}; vd.format = WGPUTextureFormat_RGBA8Unorm; vd.dimension = WGPUTextureViewDimension_Cube; vd.baseMipLevel = 0; vd.mipLevelCount = gpu->mipLevels; vd.baseArrayLayer = 0; vd.arrayLayerCount = 6; gpu->view = gpu->texture.CreateView(reinterpret_cast(&vd)); gpu->sampler = makeSampler(info.sampler, gpu->mipLevels); auto *tex = new Texture(); tex->width = faceSize; tex->height = faceSize; tex->mipmapCount = int(gpu->mipLevels); tex->sampler = info.sampler; tex->gpuHandle = gpu.get(); ownedGpuTextures.push_back(std::move(gpu)); ownedTextures.push_back(std::unique_ptr(tex)); return tex; } Texture *Graphics::newHDRCubemap(int faceSize) { if (faceSize <= 0) throw Exception("newHDRCubemap: invalid face size"); auto gpu = std::make_unique(); gpu->width = faceSize; gpu->height = faceSize; gpu->isCube = true; gpu->isHDR = true; gpu->mipLevels = uint32_t(mipmapCountForSize(faceSize, faceSize)); gpu->samplerState = TextureSampler::linear(); gpu->samplerState.mipmap = MipmapMode::Linear; WGPUTextureDescriptor td{}; td.label = sv("eve_hdr_reflection_probe_staging"); td.dimension = WGPUTextureDimension_2D; td.size = {static_cast(faceSize), static_cast(faceSize), 6}; td.sampleCount = 1; td.format = WGPUTextureFormat_RGBA16Float; td.mipLevelCount = gpu->mipLevels; td.usage = WGPUTextureUsage_TextureBinding | WGPUTextureUsage_CopyDst | WGPUTextureUsage_CopySrc | WGPUTextureUsage_RenderAttachment; gpu->texture = device.CreateTexture(reinterpret_cast(&td)); WGPUTextureViewDescriptor vd{}; vd.format = WGPUTextureFormat_RGBA16Float; vd.dimension = WGPUTextureViewDimension_Cube; vd.baseMipLevel = 0; vd.mipLevelCount = gpu->mipLevels; vd.baseArrayLayer = 0; vd.arrayLayerCount = 6; gpu->view = gpu->texture.CreateView(reinterpret_cast(&vd)); gpu->sampler = makeSampler(gpu->samplerState, gpu->mipLevels); auto *texture = new Texture(); texture->width = faceSize; texture->height = faceSize; texture->mipmapCount = int(gpu->mipLevels); texture->sampler = gpu->samplerState; texture->gpuHandle = gpu.get(); ownedGpuTextures.push_back(std::move(gpu)); ownedTextures.emplace_back(texture); return texture; } bool Graphics::copyHDRCanvasToCubemapFace(Canvas *source, Texture *cubemap, int face) { auto *canvas = dynamic_cast(source); if (!canvas || !canvas->hdr || !cubemap || !cubemap->gpuHandle || face < 0 || face >= 6) return false; auto *target = static_cast(cubemap->gpuHandle); if (!target->isCube || !target->isHDR || target->width != canvas->width || target->height != canvas->height) return false; wgpu::CommandEncoder encoder = device.CreateCommandEncoder(); WGPUTexelCopyTextureInfo src{}; src.texture = canvas->color.Get(); src.mipLevel = 0; src.aspect = WGPUTextureAspect_All; src.origin = {0, 0, 0}; WGPUTexelCopyTextureInfo dst{}; dst.texture = target->texture.Get(); dst.mipLevel = 0; dst.aspect = WGPUTextureAspect_All; dst.origin = {0, 0, static_cast(face)}; WGPUExtent3D extent{static_cast(canvas->width), static_cast(canvas->height), 1}; encoder.CopyTextureToTexture(reinterpret_cast(&src), reinterpret_cast(&dst), reinterpret_cast(&extent)); wgpu::CommandBuffer command = encoder.Finish(); queue.Submit(1, &command); return true; } bool Graphics::copyHDRCanvasesToCubemap(Canvas *const *sources, int faceCount, Texture *cubemap) { if (!sources || faceCount < 1 || faceCount > 6 || !cubemap || !cubemap->gpuHandle) return false; auto *target = static_cast(cubemap->gpuHandle); if (!target->isCube || !target->isHDR) return false; std::array canvases{}; for (int face = 0; face < faceCount; ++face) { auto *canvas = dynamic_cast(sources[face]); if (!canvas || !canvas->hdr || target->width != canvas->width || target->height != canvas->height) return false; canvases[static_cast(face)] = canvas; } wgpu::CommandEncoder encoder = device.CreateCommandEncoder(); for (int face = 0; face < faceCount; ++face) { const auto *canvas = canvases[static_cast(face)]; WGPUTexelCopyTextureInfo src{}; src.texture = canvas->color.Get(); src.mipLevel = 0; src.aspect = WGPUTextureAspect_All; src.origin = {0, 0, 0}; WGPUTexelCopyTextureInfo dst{}; dst.texture = target->texture.Get(); dst.mipLevel = 0; dst.aspect = WGPUTextureAspect_All; dst.origin = {0, 0, static_cast(face)}; WGPUExtent3D extent{static_cast(canvas->width), static_cast(canvas->height), 1}; encoder.CopyTextureToTexture(reinterpret_cast(&src), reinterpret_cast(&dst), reinterpret_cast(&extent)); } wgpu::CommandBuffer command = encoder.Finish(); queue.Submit(1, &command); return true; } bool Graphics::filterHDRReflectionCubemap(Texture *cubemap, int sampleCount) { if (!cubemap || !cubemap->gpuHandle) return false; auto *target = static_cast(cubemap->gpuHandle); if (!target->isCube || !target->isHDR || target->mipLevels < 2) return false; sampleCount = std::clamp(sampleCount, 8, 512); wgpuInstanceProcessEvents(instance.Get()); OffscreenTimestampReadback *timestampSlot = nullptr; if (offscreenTimestampSupported && offscreenTimestampQuerySet && offscreenTimestampResolveBuffer) { for (size_t candidate = 0; candidate < offscreenTimestampReadbacks.size(); ++candidate) { const size_t index = (offscreenTimestampReadbackCursor + candidate) % offscreenTimestampReadbacks.size(); if (!offscreenTimestampReadbacks[index].pending) { timestampSlot = &offscreenTimestampReadbacks[index]; offscreenTimestampReadbackCursor = (index + 1) % offscreenTimestampReadbacks.size(); break; } } } if (!reflectionProbeFilterPipeline) { WGPUBindGroupLayoutEntry entries[3]{}; entries[0].binding = 0; entries[0].visibility = WGPUShaderStage_Fragment; entries[0].texture.sampleType = WGPUTextureSampleType_Float; entries[0].texture.viewDimension = WGPUTextureViewDimension_Cube; entries[1].binding = 1; entries[1].visibility = WGPUShaderStage_Fragment; entries[1].sampler.type = WGPUSamplerBindingType_Filtering; entries[2].binding = 2; entries[2].visibility = WGPUShaderStage_Fragment; entries[2].buffer.type = WGPUBufferBindingType_Uniform; entries[2].buffer.hasDynamicOffset = true; entries[2].buffer.minBindingSize = 16; WGPUBindGroupLayoutDescriptor bgl{}; bgl.label = sv("eve_reflection_probe_filter_bgl"); bgl.entryCount = 3; bgl.entries = entries; reflectionProbeFilterSetLayout = device.CreateBindGroupLayout( reinterpret_cast(&bgl)); WGPUBindGroupLayout rawLayout = reflectionProbeFilterSetLayout.Get(); WGPUPipelineLayoutDescriptor pld{}; pld.label = sv("eve_reflection_probe_filter_layout"); pld.bindGroupLayoutCount = 1; pld.bindGroupLayouts = &rawLayout; reflectionProbeFilterPipelineLayout = device.CreatePipelineLayout( reinterpret_cast(&pld)); wgpu::ShaderModule module = makeWgslModule(device, shaders::kReflectionProbeFilterWgsl); WGPUColorTargetState colorTarget{}; colorTarget.format = WGPUTextureFormat_RGBA16Float; colorTarget.writeMask = WGPUColorWriteMask_All; WGPUFragmentState fragment{}; fragment.module = module.Get(); fragment.entryPoint = sv("fs_main"); fragment.targetCount = 1; fragment.targets = &colorTarget; WGPURenderPipelineDescriptor pipeline{}; pipeline.label = sv("eve_reflection_probe_filter"); pipeline.layout = reflectionProbeFilterPipelineLayout.Get(); pipeline.vertex.module = module.Get(); pipeline.vertex.entryPoint = sv("vs_main"); pipeline.fragment = &fragment; pipeline.primitive.topology = WGPUPrimitiveTopology_TriangleList; pipeline.primitive.frontFace = WGPUFrontFace_CCW; pipeline.primitive.cullMode = WGPUCullMode_None; pipeline.multisample.count = 1; pipeline.multisample.mask = 0xFFFFFFFFu; reflectionProbeFilterPipeline = device.CreateRenderPipeline( reinterpret_cast(&pipeline)); } const uint32_t passCount = (target->mipLevels - 1u) * 6u; WGPUBufferDescriptor bufferDesc{}; bufferDesc.label = sv("eve_reflection_probe_filter_params"); bufferDesc.size = uint64_t(passCount) * 256u; bufferDesc.usage = WGPUBufferUsage_Uniform | WGPUBufferUsage_CopyDst; wgpu::Buffer paramsBuffer = device.CreateBuffer(reinterpret_cast(&bufferDesc)); WGPUTextureViewDescriptor sourceViewDesc{}; sourceViewDesc.format = WGPUTextureFormat_RGBA16Float; sourceViewDesc.dimension = WGPUTextureViewDimension_Cube; sourceViewDesc.baseMipLevel = 0; sourceViewDesc.mipLevelCount = 1; sourceViewDesc.baseArrayLayer = 0; sourceViewDesc.arrayLayerCount = 6; wgpu::TextureView sourceView = target->texture.CreateView( reinterpret_cast(&sourceViewDesc)); WGPUBindGroupEntry groupEntries[3]{}; groupEntries[0].binding = 0; groupEntries[0].textureView = sourceView.Get(); groupEntries[1].binding = 1; groupEntries[1].sampler = target->sampler.Get(); groupEntries[2].binding = 2; groupEntries[2].buffer = paramsBuffer.Get(); groupEntries[2].size = 16; WGPUBindGroupDescriptor groupDesc{}; groupDesc.label = sv("eve_reflection_probe_filter_group"); groupDesc.layout = reflectionProbeFilterSetLayout.Get(); groupDesc.entryCount = 3; groupDesc.entries = groupEntries; wgpu::BindGroup group = device.CreateBindGroup(reinterpret_cast(&groupDesc)); wgpu::CommandEncoder encoder = device.CreateCommandEncoder(); uint32_t passIndex = 0; for (uint32_t mip = 1; mip < target->mipLevels; ++mip) { const bool diffuse = mip + 1u == target->mipLevels; const float roughness = diffuse ? 1.f : float(mip) / float(target->mipLevels - 1u); for (uint32_t face = 0; face < 6; ++face, ++passIndex) { const float params[4] = {float(face), roughness, diffuse ? 1.f : 0.f, float(sampleCount)}; queue.WriteBuffer(paramsBuffer, uint64_t(passIndex) * 256u, params, sizeof(params)); WGPUTextureViewDescriptor viewDesc{}; viewDesc.format = WGPUTextureFormat_RGBA16Float; viewDesc.dimension = WGPUTextureViewDimension_2D; viewDesc.baseMipLevel = mip; viewDesc.mipLevelCount = 1; viewDesc.baseArrayLayer = face; viewDesc.arrayLayerCount = 1; wgpu::TextureView faceView = target->texture.CreateView( reinterpret_cast(&viewDesc)); WGPURenderPassColorAttachment attachment{}; attachment.view = faceView.Get(); attachment.loadOp = WGPULoadOp_Clear; attachment.storeOp = WGPUStoreOp_Store; WGPURenderPassDescriptor passDesc{}; passDesc.colorAttachmentCount = 1; passDesc.colorAttachments = &attachment; WGPUPassTimestampWrites timestampWrites{}; if (timestampSlot) { timestampWrites.querySet = offscreenTimestampQuerySet.Get(); timestampWrites.beginningOfPassWriteIndex = passIndex == 0 ? 0 : UINT32_MAX; timestampWrites.endOfPassWriteIndex = passIndex + 1u == passCount ? 1 : UINT32_MAX; passDesc.timestampWrites = ×tampWrites; } wgpu::RenderPassEncoder pass = encoder.BeginRenderPass( reinterpret_cast(&passDesc)); pass.SetPipeline(reflectionProbeFilterPipeline); const uint32_t offset = passIndex * 256u; pass.SetBindGroup(0, group, 1, &offset); pass.Draw(3); pass.End(); } } if (timestampSlot) { encoder.ResolveQuerySet(offscreenTimestampQuerySet, 0, 2, offscreenTimestampResolveBuffer, 0); encoder.CopyBufferToBuffer(offscreenTimestampResolveBuffer, 0, timestampSlot->buffer, 0, sizeof(uint64_t) * 2); } wgpu::CommandBuffer command = encoder.Finish(); queue.Submit(1, &command); if (timestampSlot) { timestampSlot->pending = true; WGPUBufferMapCallbackInfo callback{}; callback.mode = WGPUCallbackMode_AllowProcessEvents; callback.callback = [](WGPUMapAsyncStatus status, WGPUStringView, void *userdata1, void *) { auto *slot = static_cast(userdata1); if (status == WGPUMapAsyncStatus_Success && slot && slot->owner) { const auto *ticks = static_cast( slot->buffer.GetConstMappedRange(0, sizeof(uint64_t) * 2)); if (ticks && ticks[1] >= ticks[0]) { slot->owner->completedOffscreenTimestampMs.fetch_add( float(ticks[1] - ticks[0]) * 1.0e-6f); } slot->buffer.Unmap(); } if (slot) slot->pending = false; }; callback.userdata1 = timestampSlot; wgpuBufferMapAsync(timestampSlot->buffer.Get(), WGPUMapMode_Read, 0, sizeof(uint64_t) * 2, callback); } return true; } void Graphics::setTextureSampler(Texture *texture, const TextureSampler &sampler) { if (!texture) return; auto *gpu = gpuForTexture(texture); if (!gpu) return; gpu->samplerState = sampler; gpu->sampler = makeSampler(sampler, gpu->mipLevels); texture->sampler = sampler; } float Graphics::getMaxAnisotropy() const { return maxSamplerAnisotropy; } Texture *Graphics::newTextureFromFile(const std::string &filename) { if (filename.empty()) throw Exception("newTextureFromFile: empty filename"); auto it = texturesByPath.find(filename); if (it != texturesByPath.end()) { reloadTextureFromFile(filename); return it->second; } auto *imgMod = image::Image::create(); eve::ref data(imgMod->newImageDataFromFile(filename)); Texture *tex = newTexture(data.get()); texturesByPath[filename] = tex; return tex; } bool Graphics::reloadTextureFromFile(const std::string &filename) { auto it = texturesByPath.find(filename); if (it == texturesByPath.end()) return false; image::ImageData *data = nullptr; try { auto *imgMod = image::Image::create(); eve::ref cached(imgMod->newImageDataFromFile(filename)); data = cached.get(); } catch (...) { return false; } if (!data) return false; Texture *tex = it->second; return updateTexture(tex, data->getWidth(), data->getHeight(), static_cast(data->getData())); } bool Graphics::releaseTexture(Texture *texture) { if (!texture || !texture->gpuHandle) return false; // Renderer-owned fallback textures must never be released by callers. if (texture->gpuHandle == whiteTexture || texture->gpuHandle == flatNormalTexture || texture->gpuHandle == flatNormalTexture3D || texture->gpuHandle == defaultEnvCubemap) return false; auto *gpu = static_cast(texture->gpuHandle); auto gpuIt = std::find_if(ownedGpuTextures.begin(), ownedGpuTextures.end(), [&](const std::unique_ptr &g) { return g.get() == gpu; }); if (gpuIt == ownedGpuTextures.end()) return false; auto texIt = std::find_if(ownedTextures.begin(), ownedTextures.end(), [&](const std::unique_ptr &t) { return t.get() == texture; }); if (texIt == ownedTextures.end()) return false; // Path-cached textures must leave the hot-reload cache once released. for (auto it = texturesByPath.begin(); it != texturesByPath.end();) { if (it->second == texture) it = texturesByPath.erase(it); else ++it; } texture->gpuHandle = nullptr; ownedGpuTextures.erase(gpuIt); // Transfer the CPU facade to the caller instead of destroying it. (void)texIt->release(); ownedTextures.erase(texIt); return true; } bool Graphics::updateTexture(Texture *texture, int width, int height, const uint8_t *rgba) { if (!texture || !rgba || width <= 0 || height <= 0) return false; auto *gpu = gpuForTexture(texture); if (!gpu || width != texture->width || height != texture->height) return false; const auto owned = std::find_if(ownedGpuTextures.begin(), ownedGpuTextures.end(), [gpu](const std::unique_ptr &candidate) { return candidate.get() == gpu; }); if (owned == ownedGpuTextures.end() || gpu->isCube) return false; uploadTexturePixelsMips(gpu, rgba, width, height); return true; } GpuTexture *Graphics::gpuForTexture(Texture *t) const { return t ? static_cast(t->gpuHandle) : nullptr; } GpuTexture *Graphics::gpuForTextureOrWhite(Texture *t) const { GpuTexture *g = gpuForTexture(t); return g ? g : whiteTexture; } wgpu::BindGroup Graphics::makeTex2DBindGroup(GpuTexture *color, GpuTexture *depth, GpuTexture *motion, GpuTexture *extra, GpuTexture *specular) { GpuTexture *c = color ? color : whiteTexture; GpuTexture *d = depth ? depth : c; GpuTexture *m = motion ? motion : c; GpuTexture *e = extra ? extra : c; GpuTexture *s = specular ? specular : c; WGPUBindGroupEntry entries[11]{}; entries[0].binding = 0; entries[0].textureView = c->view.Get(); entries[1].binding = 1; entries[1].textureView = d->view.Get(); entries[2].binding = 2; entries[2].sampler = c->sampler.Get(); entries[3].binding = 3; entries[3].sampler = d->sampler.Get(); entries[4].binding = 4; entries[4].buffer = currentUboArena().buffer.Get(); // replaced per-draw via dynamic offset entries[4].size = std::max(Shader::kPushConstantBytes, uint32_t(sizeof(Lighting2DUBO))); entries[5].binding = 5; entries[5].textureView = m->view.Get(); entries[6].binding = 6; entries[6].sampler = m->sampler.Get(); entries[7].binding = 7; entries[7].textureView = e->view.Get(); entries[8].binding = 8; entries[8].sampler = e->sampler.Get(); entries[9].binding = 9; entries[9].textureView = s->view.Get(); entries[10].binding = 10; entries[10].sampler = s->sampler.Get(); WGPUBindGroupDescriptor desc{}; desc.label = sv("eve_tex2d_group"); desc.layout = tex2DSetLayout.Get(); desc.entryCount = 11; desc.entries = entries; return device.CreateBindGroup(reinterpret_cast(&desc)); } wgpu::BindGroup Graphics::makeMeshBindGroup(GpuTexture *albedo, GpuTexture *normal, GpuTexture *env, GpuTexture *height, GpuTexture *depth, uint32_t frameUboOffset, uint32_t shadowUboOffset, uint32_t pushUboOffset) { GpuTexture *a = albedo ? albedo : whiteTexture; GpuTexture *n = normal ? normal : flatNormalTexture; GpuTexture *e = env ? env : defaultEnvCubemap; auto localProbe = [&](int index) -> GpuTexture * { if (index < mesh3dReflectionProbes.count) { GpuTexture *gpu = gpuForTexture(mesh3dReflectionProbes.probes[index].cubemap); if (gpu && gpu->isCube) return gpu; } return defaultEnvCubemap; }; GpuTexture *probe0 = localProbe(0); GpuTexture *probe1 = localProbe(1); GpuTexture *h = height ? height : flatHeightTexture3D; GpuTexture *d = depth ? depth : flatDepthTexture3D; GpuTexture *shadow = shadowDepthArray ? shadowDepthArray : defaultShadowTex; wgpu::TextureView aoView_ = aoReady ? aoView[(aoWriteIndex + 1) % 2] : (whiteTexture ? whiteTexture->view : wgpu::TextureView()); wgpu::TextureView decalAlbedoView = gpuForTextureOrWhite(decalFlatAlbedo)->view; wgpu::TextureView decalNormalView = gpuForTextureOrWhite(decalFlatNormal)->view; wgpu::TextureView decalParamsView = gpuForTextureOrWhite(decalFlatParams)->view; if (decalReady && lastDecalSlot < decalSlots.size()) { decalAlbedoView = decalSlots[lastDecalSlot].albedoView; decalNormalView = decalSlots[lastDecalSlot].normalView; decalParamsView = decalSlots[lastDecalSlot].paramsView; } MeshBindGroupKey key{reinterpret_cast(currentUboArena().buffer.Get()), reinterpret_cast(a->view.Get()), reinterpret_cast(n->view.Get()), reinterpret_cast(e->view.Get()), reinterpret_cast(h->view.Get()), reinterpret_cast(d->view.Get()), reinterpret_cast(shadow->view.Get()), reinterpret_cast(shadow->sampler.Get()), reinterpret_cast(aoView_.Get()), reinterpret_cast(decalAlbedoView.Get()), reinterpret_cast(decalNormalView.Get()), reinterpret_cast(decalParamsView.Get()), reinterpret_cast(probe0->view.Get()), reinterpret_cast(probe1->view.Get())}; auto cached = meshBindGroupCache_.find(key); if (cached != meshBindGroupCache_.end()) return cached->second; if (meshBindGroupCache_.size() >= kMaxMeshBindGroupCache) meshBindGroupCache_.clear(); WGPUBindGroupEntry entries[18]{}; entries[0].binding = 0; entries[0].buffer = currentUboArena().buffer.Get(); entries[0].size = sizeof(Mesh3DUBO); entries[1].binding = 1; entries[1].textureView = a->view.Get(); entries[2].binding = 2; entries[2].textureView = n->view.Get(); entries[3].binding = 3; entries[3].textureView = e->view.Get(); entries[4].binding = 4; entries[4].buffer = currentUboArena().buffer.Get(); entries[4].size = sizeof(ShadowUBO); entries[5].binding = 5; entries[5].textureView = shadow->view.Get(); entries[6].binding = 6; entries[6].textureView = h->view.Get(); entries[7].binding = 7; // The mesh shader currently has one material sampler binding. Follow the // albedo texture's public sampler state instead of silently substituting // the backend-global linear sampler (Vulkan also samples albedo with the // sampler owned by that texture). entries[7].sampler = a->sampler.Get(); entries[8].binding = 8; entries[8].sampler = shadow->sampler.Get(); entries[9].binding = 9; entries[9].textureView = d->view.Get(); entries[10].binding = 10; entries[10].textureView = aoView_.Get(); entries[11].binding = 11; entries[11].sampler = mainSampler.Get(); entries[12].binding = 12; entries[12].textureView = decalAlbedoView.Get(); entries[13].binding = 13; entries[13].textureView = decalNormalView.Get(); entries[14].binding = 14; entries[14].textureView = decalParamsView.Get(); entries[15].binding = 15; entries[15].buffer = currentUboArena().buffer.Get(); entries[15].size = Shader::kPushConstantBytes; entries[16].binding = 16; entries[16].textureView = probe0->view.Get(); entries[17].binding = 17; entries[17].textureView = probe1->view.Get(); (void)frameUboOffset; (void)shadowUboOffset; (void)pushUboOffset; WGPUBindGroupDescriptor desc{}; desc.label = sv("eve_mesh_group"); desc.layout = mesh3dSetLayout.Get(); desc.entryCount = 18; desc.entries = entries; wgpu::BindGroup bg = device.CreateBindGroup(reinterpret_cast(&desc)); meshBindGroupCache_.emplace(key, bg); return bg; } wgpu::BindGroup Graphics::makeMesh3DClusteredBindGroup(GpuTexture *albedo, GpuTexture *normal, GpuTexture *env, GpuTexture *height, GpuTexture *depth, wgpu::TextureView aoView, uint32_t frameUboOffset, uint32_t shadowUboOffset) { GpuTexture *a = albedo ? albedo : whiteTexture; GpuTexture *n = normal ? normal : flatNormalTexture3D; GpuTexture *e = env ? env : defaultEnvCubemap; auto localProbe = [&](int index) -> GpuTexture * { if (index < mesh3dReflectionProbes.count) { GpuTexture *gpu = gpuForTexture(mesh3dReflectionProbes.probes[index].cubemap); if (gpu && gpu->isCube) return gpu; } return defaultEnvCubemap; }; GpuTexture *probe0 = localProbe(0); GpuTexture *probe1 = localProbe(1); GpuTexture *h = height ? height : flatHeightTexture3D; GpuTexture *d = depth ? depth : flatDepthTexture3D; GpuTexture *shadow = shadowDepthArray ? shadowDepthArray : defaultShadowTex; ClusteredStorage &st = clusteredStorage[currentFrameSlot()]; wgpu::TextureView decalAlbedoView = gpuForTextureOrWhite(decalFlatAlbedo)->view; wgpu::TextureView decalNormalView = gpuForTextureOrWhite(decalFlatNormal)->view; wgpu::TextureView decalParamsView = gpuForTextureOrWhite(decalFlatParams)->view; if (decalReady && lastDecalSlot < decalSlots.size()) { decalAlbedoView = decalSlots[lastDecalSlot].albedoView; decalNormalView = decalSlots[lastDecalSlot].normalView; decalParamsView = decalSlots[lastDecalSlot].paramsView; } WGPUBindGroupEntry entries[20]{}; entries[0].binding = 0; entries[0].buffer = currentUboArena().buffer.Get(); entries[0].size = sizeof(Mesh3DClusteredUBO); entries[1].binding = 1; entries[1].textureView = a->view.Get(); entries[2].binding = 2; entries[2].textureView = n->view.Get(); entries[3].binding = 3; entries[3].textureView = e->view.Get(); entries[4].binding = 4; entries[4].buffer = currentUboArena().buffer.Get(); entries[4].size = sizeof(ShadowUBO); entries[5].binding = 5; entries[5].textureView = shadow->view.Get(); entries[6].binding = 6; entries[6].textureView = h->view.Get(); entries[7].binding = 7; entries[7].sampler = a->sampler.Get(); entries[8].binding = 8; entries[8].sampler = shadow->sampler.Get(); entries[9].binding = 9; entries[9].textureView = d->view.Get(); entries[10].binding = 10; entries[10].buffer = st.lights.Get(); entries[10].size = st.lightsCap; entries[11].binding = 11; entries[11].buffer = st.table.Get(); entries[11].size = st.tableCap; entries[12].binding = 12; entries[12].buffer = st.indices.Get(); entries[12].size = st.indicesCap; entries[13].binding = 13; entries[13].textureView = aoView.Get(); entries[14].binding = 14; entries[14].sampler = mainSampler.Get(); entries[15].binding = 15; entries[15].textureView = decalAlbedoView.Get(); entries[16].binding = 16; entries[16].textureView = decalNormalView.Get(); entries[17].binding = 17; entries[17].textureView = decalParamsView.Get(); entries[18].binding = 18; entries[18].textureView = probe0->view.Get(); entries[19].binding = 19; entries[19].textureView = probe1->view.Get(); (void)frameUboOffset; (void)shadowUboOffset; WGPUBindGroupDescriptor desc{}; desc.label = sv("eve_mesh3d_clustered_group"); desc.layout = mesh3dClusteredSetLayout.Get(); desc.entryCount = 20; desc.entries = entries; return device.CreateBindGroup(reinterpret_cast(&desc)); } // --------------------------------------------------------------------------- // Mesh creation // --------------------------------------------------------------------------- Mesh *Graphics::newMeshFromArrays(const float *posXYZ, const float *nrmXYZ, const float *uvST, int vertexCount, const uint32_t *indices, int indexCount) { if (vertexCount <= 0 || !posXYZ) throw Exception("newMeshFromArrays: invalid vertex data"); if (indexCount < 0 || (indexCount > 0 && (!indices || indexCount % 3 != 0))) throw Exception("newMeshFromArrays: invalid index data"); for (int i = 0; i < indexCount; ++i) { if (indices[i] >= uint32_t(vertexCount)) throw Exception("newMeshFromArrays: index out of range"); } std::vector verts(static_cast(vertexCount)); for (int i = 0; i < vertexCount; ++i) { auto &v = verts[static_cast(i)]; v.pos = {posXYZ[i * 3 + 0], posXYZ[i * 3 + 1], posXYZ[i * 3 + 2]}; if (nrmXYZ) { v.normal = {nrmXYZ[i * 3 + 0], nrmXYZ[i * 3 + 1], nrmXYZ[i * 3 + 2]}; } else { v.normal = {0.f, 0.f, 1.f}; } if (uvST) { v.uv = {uvST[i * 2 + 0], uvST[i * 2 + 1]}; } else { v.uv = {0.f, 0.f}; } } auto gpu = std::make_unique(); gpu->vertexCount = uint32_t(vertexCount); gpu->indexCount = indexCount > 0 ? uint32_t(indexCount) : 0; gpu->vertexStride = sizeof(MeshVertex); gpu->cpuVertices = verts; WGPUBufferDescriptor vbd{}; vbd.label = sv("eve_mesh_vb"); vbd.size = verts.size() * sizeof(MeshVertex); vbd.usage = WGPUBufferUsage_CopyDst | WGPUBufferUsage_Vertex | WGPUBufferUsage_Storage; vbd.mappedAtCreation = false; gpu->vertexBuffer = device.CreateBuffer(reinterpret_cast(&vbd)); queue.WriteBuffer(gpu->vertexBuffer, 0, verts.data(), vbd.size); gpu->vertexCapacity = vbd.size; if (indexCount > 0) { // 16-bit index format halves index memory for meshes with <= 65535 verts. if (vertexCount <= 65535) { std::vector idx16; idx16.reserve(indexCount); for (int i = 0; i < indexCount; ++i) idx16.push_back(uint16_t(indices[i])); WGPUBufferDescriptor ibd{}; ibd.label = sv("eve_mesh_ib"); if (idx16.size() % 2 != 0) idx16.push_back(0); // WriteBuffer size must align to 4. ibd.size = uint64_t(idx16.size()) * sizeof(uint16_t); ibd.usage = WGPUBufferUsage_CopyDst | WGPUBufferUsage_Index | WGPUBufferUsage_Storage; ibd.mappedAtCreation = false; gpu->indexBuffer = device.CreateBuffer(reinterpret_cast(&ibd)); queue.WriteBuffer(gpu->indexBuffer, 0, idx16.data(), ibd.size); gpu->indexFormat = wgpu::IndexFormat::Uint16; gpu->indexCapacity = ibd.size; } else { WGPUBufferDescriptor ibd{}; ibd.label = sv("eve_mesh_ib"); ibd.size = uint64_t(indexCount) * sizeof(uint32_t); ibd.usage = WGPUBufferUsage_CopyDst | WGPUBufferUsage_Index | WGPUBufferUsage_Storage; ibd.mappedAtCreation = false; gpu->indexBuffer = device.CreateBuffer(reinterpret_cast(&ibd)); queue.WriteBuffer(gpu->indexBuffer, 0, indices, ibd.size); gpu->indexFormat = wgpu::IndexFormat::Uint32; gpu->indexCapacity = ibd.size; } } auto *mesh = new Mesh(); mesh->indexCount = indexCount; mesh->gpuVertexCount = vertexCount; mesh->gpuHandle = gpu.get(); mesh->computeBounds(posXYZ, vertexCount); ownedGpuMeshes.push_back(std::move(gpu)); ownedMeshes.push_back(std::unique_ptr(mesh)); return mesh; } std::optional Graphics::describeMesh(Mesh *mesh) const { if (!mesh || !mesh->gpuHandle) return std::nullopt; const auto *gpu = static_cast(mesh->gpuHandle); const auto owned = std::find_if(ownedGpuMeshes.begin(), ownedGpuMeshes.end(), [gpu](const std::unique_ptr &candidate) { return candidate.get() == gpu; }); if (owned == ownedGpuMeshes.end()) return std::nullopt; return eve::graphics::MeshBackendDescriptor{gpu->vertexCount, gpu->indexCount, gpu->vertexStride, gpu->indexFormat == wgpu::IndexFormat::Uint16 ? 2u : 4u}; } Mesh *Graphics::newMeshFromAssimp(const ::aiMesh &mesh) { aiMatrix4x4 ident; return newMeshFromAssimp(mesh, ident); } Mesh *Graphics::newMeshFromAssimp(const ::aiMesh &mesh, const aiMatrix4x4 &worldTransform) { std::vector pos, nrm, uv; std::vector idx; pos.reserve(mesh.mNumVertices * 3); nrm.reserve(mesh.mNumVertices * 3); uv.reserve(mesh.mNumVertices * 2); idx.reserve(mesh.mNumFaces * 3); for (unsigned i = 0; i < mesh.mNumVertices; ++i) { aiVector3D p = worldTransform * mesh.mVertices[i]; pos.push_back(p.x); pos.push_back(p.y); pos.push_back(p.z); if (mesh.mNormals) { aiMatrix3x3 rot(worldTransform); aiVector3D n = rot * mesh.mNormals[i]; n.Normalize(); nrm.push_back(n.x); nrm.push_back(n.y); nrm.push_back(n.z); } else { nrm.push_back(0.f); nrm.push_back(0.f); nrm.push_back(1.f); } if (mesh.mTextureCoords[0]) { uv.push_back(mesh.mTextureCoords[0][i].x); uv.push_back(mesh.mTextureCoords[0][i].y); } else { uv.push_back(0.f); uv.push_back(0.f); } } for (unsigned f = 0; f < mesh.mNumFaces; ++f) { for (unsigned v = 0; v < mesh.mFaces[f].mNumIndices; ++v) idx.push_back(mesh.mFaces[f].mIndices[v]); } Mesh *m = newMeshFromArrays(pos.data(), nrm.data(), uv.data(), int(mesh.mNumVertices), idx.data(), int(idx.size())); if (m) m->captureImportedAttributes(mesh); if (m && mesh.mNumAnimMeshes > 0) { m->initMorphBase(int(mesh.mNumVertices), pos.data(), nrm.data(), uv.data()); for (unsigned a = 0; a < mesh.mNumAnimMeshes; ++a) { const aiAnimMesh *am = mesh.mAnimMeshes[a]; std::vector absPos(am->mNumVertices * 3); for (unsigned i = 0; i < am->mNumVertices; ++i) { absPos[i * 3 + 0] = am->mVertices[i].x; absPos[i * 3 + 1] = am->mVertices[i].y; absPos[i * 3 + 2] = am->mVertices[i].z; } std::string name = am->mName.C_Str(); if (name.empty()) name = "morph" + std::to_string(a); m->addMorphTargetAbsolute(name, absPos.data()); } } return m; } bool Graphics::bakeMeshMorph(Mesh *mesh) { if (!mesh || !mesh->hasMorphData() || !mesh->isMorphDirty()) return false; auto *gpu = static_cast(mesh->gpuHandle); if (!gpu || !gpu->vertexBuffer) return false; std::vector pos, nrm; mesh->computeMorphedPositions(pos, nrm); if (pos.empty()) return false; mesh->computeBounds(pos.data(), mesh->getVertexCount()); auto &verts = gpu->cpuVertices; if (verts.size() != static_cast(mesh->getVertexCount())) return false; const auto &uvs = mesh->baseUv(); for (int i = 0; i < mesh->getVertexCount(); ++i) { auto &v = verts[static_cast(i)]; v.pos = {pos[i * 3 + 0], pos[i * 3 + 1], pos[i * 3 + 2]}; if (nrm.size() >= size_t((i + 1) * 3)) { v.normal = {nrm[i * 3 + 0], nrm[i * 3 + 1], nrm[i * 3 + 2]}; } else { v.normal = {0.f, 0.f, 1.f}; } if (uvs.size() >= size_t((i + 1) * 2)) { v.uv = {uvs[i * 2 + 0], uvs[i * 2 + 1]}; } else { v.uv = {0.f, 0.f}; } } queue.WriteBuffer(gpu->vertexBuffer, 0, verts.data(), verts.size() * sizeof(MeshVertex)); mesh->markMorphClean(); return true; } bool Graphics::updateMeshVertices(Mesh *mesh, const float *posXYZ, const float *nrmXYZ, const float *uvST, int vertexCount, const uint32_t *indices, int indexCount) { if (!mesh || !mesh->gpuHandle || !posXYZ || vertexCount <= 0 || indexCount < 0) return false; if (indexCount > 0 && (!indices || indexCount % 3 != 0)) return false; for (int i = 0; i < indexCount; ++i) { if (indices[i] >= uint32_t(vertexCount)) return false; } auto *gpu = static_cast(mesh->gpuHandle); const auto owned = std::find_if(ownedGpuMeshes.begin(), ownedGpuMeshes.end(), [gpu](const std::unique_ptr &candidate) { return candidate.get() == gpu; }); if (owned == ownedGpuMeshes.end()) return false; std::vector verts; verts.reserve(size_t(vertexCount) * 8u); for (int i = 0; i < vertexCount; ++i) { verts.insert(verts.end(), posXYZ + size_t(i) * 3u, posXYZ + size_t(i) * 3u + 3u); if (nrmXYZ) verts.insert(verts.end(), nrmXYZ + size_t(i) * 3u, nrmXYZ + size_t(i) * 3u + 3u); else verts.insert(verts.end(), {0.f, 0.f, 1.f}); if (uvST) verts.insert(verts.end(), uvST + size_t(i) * 2u, uvST + size_t(i) * 2u + 2u); else verts.insert(verts.end(), {0.f, 0.f}); } const uint64_t vertexBytes = verts.size() * sizeof(float); if (vertexBytes > gpu->vertexCapacity) { WGPUBufferDescriptor desc{}; desc.label = sv("eve_mesh_dynamic_vb"); desc.size = vertexBytes; desc.usage = WGPUBufferUsage_CopyDst | WGPUBufferUsage_Vertex | WGPUBufferUsage_Storage; gpu->vertexBuffer = device.CreateBuffer(reinterpret_cast(&desc)); gpu->vertexCapacity = vertexBytes; } queue.WriteBuffer(gpu->vertexBuffer, 0, verts.data(), vertexBytes); gpu->vertexCount = uint32_t(vertexCount); if (indexCount > 0) { const wgpu::IndexFormat format = vertexCount <= 65535 ? wgpu::IndexFormat::Uint16 : wgpu::IndexFormat::Uint32; std::vector idx16; const void *indexData = indices; uint64_t indexBytes = uint64_t(indexCount) * sizeof(uint32_t); if (format == wgpu::IndexFormat::Uint16) { idx16.reserve(size_t(indexCount) + 1u); for (int i = 0; i < indexCount; ++i) idx16.push_back(uint16_t(indices[i])); if (idx16.size() % 2 != 0) idx16.push_back(0); indexData = idx16.data(); indexBytes = idx16.size() * sizeof(uint16_t); } if (format != gpu->indexFormat || indexBytes > gpu->indexCapacity) { WGPUBufferDescriptor desc{}; desc.label = sv("eve_mesh_dynamic_ib"); desc.size = indexBytes; desc.usage = WGPUBufferUsage_CopyDst | WGPUBufferUsage_Index | WGPUBufferUsage_Storage; gpu->indexBuffer = device.CreateBuffer(reinterpret_cast(&desc)); gpu->indexCapacity = indexBytes; } queue.WriteBuffer(gpu->indexBuffer, 0, indexData, indexBytes); gpu->indexFormat = format; gpu->indexCount = uint32_t(indexCount); } mesh->computeBounds(posXYZ, vertexCount); mesh->gpuVertexCount = int(gpu->vertexCount); mesh->indexCount = int(gpu->indexCount); return true; } void fillMeshAttributes(WGPUVertexAttribute (&attrs)[5]) { attrs[0].format = WGPUVertexFormat_Float32x3; attrs[0].offset = 0; attrs[0].shaderLocation = 0; attrs[1].format = WGPUVertexFormat_Float32x3; attrs[1].offset = 12; attrs[1].shaderLocation = 1; attrs[2].format = WGPUVertexFormat_Float32x2; attrs[2].offset = 24; attrs[2].shaderLocation = 2; attrs[3].format = WGPUVertexFormat_Uint16x4; attrs[3].offset = 32; attrs[3].shaderLocation = 3; attrs[4].format = WGPUVertexFormat_Float32x4; attrs[4].offset = 40; attrs[4].shaderLocation = 4; } bool Graphics::setMeshSkinningData(Mesh *mesh, const uint16_t *joints4, const float *weights4, int vertexCount) { if (!mesh || !mesh->gpuHandle || !joints4 || !weights4) return false; auto *gpu = static_cast(mesh->gpuHandle); if (vertexCount <= 0 || gpu->cpuVertices.size() != static_cast(vertexCount)) return false; for (int i = 0; i < vertexCount; ++i) { const size_t base = static_cast(i) * 4u; auto &v = gpu->cpuVertices[static_cast(i)]; v.joints = glm::u16vec4(joints4[base], joints4[base + 1], joints4[base + 2], joints4[base + 3]); v.weights = glm::vec4(weights4[base], weights4[base + 1], weights4[base + 2], weights4[base + 3]); } queue.WriteBuffer(gpu->vertexBuffer, 0, gpu->cpuVertices.data(), gpu->cpuVertices.size() * sizeof(MeshVertex)); mesh->markGpuSkinned(true); return true; } bool Graphics::releaseMesh(Mesh *mesh) { if (!mesh || !mesh->gpuHandle) return false; auto *gpu = static_cast(mesh->gpuHandle); auto gpuIt = std::find_if(ownedGpuMeshes.begin(), ownedGpuMeshes.end(), [&](const std::unique_ptr &g) { return g.get() == gpu; }); if (gpuIt == ownedGpuMeshes.end()) return false; auto meshIt = std::find_if(ownedMeshes.begin(), ownedMeshes.end(), [&](const std::unique_ptr &m) { return m.get() == mesh; }); if (meshIt == ownedMeshes.end()) return false; mesh->gpuHandle = nullptr; ownedGpuMeshes.erase(gpuIt); // Transfer the CPU facade to the caller instead of destroying it. (void)meshIt->release(); ownedMeshes.erase(meshIt); return true; } Mesh *Graphics::newMeshSphere(int slices, int stacks) { std::vector pos, nrm, uv; std::vector idx; for (int y = 0; y <= stacks; ++y) { for (int x = 0; x <= slices; ++x) { float u = float(x) / float(slices); float v = float(y) / float(stacks); float theta = u * 2.f * glm::pi(); float phi = v * glm::pi(); float sx = std::sin(phi) * std::cos(theta); float sy = std::cos(phi); float sz = std::sin(phi) * std::sin(theta); pos.push_back(sx); pos.push_back(sy); pos.push_back(sz); nrm.push_back(sx); nrm.push_back(sy); nrm.push_back(sz); uv.push_back(u); uv.push_back(v); } } for (int y = 0; y < stacks; ++y) { for (int x = 0; x < slices; ++x) { int a = y * (slices + 1) + x; int b = a + slices + 1; idx.push_back(a); idx.push_back(a + 1); idx.push_back(b); idx.push_back(b); idx.push_back(a + 1); idx.push_back(b + 1); } } return newMeshFromArrays(pos.data(), nrm.data(), uv.data(), int(pos.size() / 3), idx.data(), int(idx.size())); } Mesh *Graphics::newMeshCylinder(int slices, int stacks, bool caps) { std::vector pos, nrm, uv; std::vector idx; for (int y = 0; y <= stacks; ++y) { for (int x = 0; x <= slices; ++x) { float u = float(x) / float(slices); float v = float(y) / float(stacks); float theta = u * 2.f * glm::pi(); pos.push_back(std::cos(theta)); pos.push_back(v * 2.f - 1.f); pos.push_back(std::sin(theta)); nrm.push_back(std::cos(theta)); nrm.push_back(0.f); nrm.push_back(std::sin(theta)); uv.push_back(u); uv.push_back(v); } } for (int y = 0; y < stacks; ++y) { for (int x = 0; x < slices; ++x) { int a = y * (slices + 1) + x; int b = a + slices + 1; idx.push_back(a); idx.push_back(b); idx.push_back(a + 1); idx.push_back(b); idx.push_back(b + 1); idx.push_back(a + 1); } } if (caps) { int base = int(pos.size() / 3); pos.push_back(0.f); pos.push_back(-1.f); pos.push_back(0.f); nrm.push_back(0.f); nrm.push_back(-1.f); nrm.push_back(0.f); uv.push_back(0.5f); uv.push_back(0.5f); for (int x = 0; x < slices; ++x) { float theta0 = float(x) / float(slices) * 2.f * glm::pi(); float theta1 = float(x + 1) / float(slices) * 2.f * glm::pi(); int i0 = int(pos.size() / 3); pos.push_back(std::cos(theta0)); pos.push_back(-1.f); pos.push_back(std::sin(theta0)); nrm.push_back(0.f); nrm.push_back(-1.f); nrm.push_back(0.f); uv.push_back(0.5f + 0.5f * std::cos(theta0)); uv.push_back(0.5f + 0.5f * std::sin(theta0)); int i1 = int(pos.size() / 3); pos.push_back(std::cos(theta1)); pos.push_back(-1.f); pos.push_back(std::sin(theta1)); nrm.push_back(0.f); nrm.push_back(-1.f); nrm.push_back(0.f); uv.push_back(0.5f + 0.5f * std::cos(theta1)); uv.push_back(0.5f + 0.5f * std::sin(theta1)); idx.push_back(base); idx.push_back(i0); idx.push_back(i1); } base = int(pos.size() / 3); pos.push_back(0.f); pos.push_back(1.f); pos.push_back(0.f); nrm.push_back(0.f); nrm.push_back(1.f); nrm.push_back(0.f); uv.push_back(0.5f); uv.push_back(0.5f); for (int x = 0; x < slices; ++x) { float theta0 = float(x) / float(slices) * 2.f * glm::pi(); float theta1 = float(x + 1) / float(slices) * 2.f * glm::pi(); int i0 = int(pos.size() / 3); pos.push_back(std::cos(theta0)); pos.push_back(1.f); pos.push_back(std::sin(theta0)); nrm.push_back(0.f); nrm.push_back(1.f); nrm.push_back(0.f); uv.push_back(0.5f + 0.5f * std::cos(theta0)); uv.push_back(0.5f + 0.5f * std::sin(theta0)); int i1 = int(pos.size() / 3); pos.push_back(std::cos(theta1)); pos.push_back(1.f); pos.push_back(std::sin(theta1)); nrm.push_back(0.f); nrm.push_back(1.f); nrm.push_back(0.f); uv.push_back(0.5f + 0.5f * std::cos(theta1)); uv.push_back(0.5f + 0.5f * std::sin(theta1)); idx.push_back(base); idx.push_back(i1); idx.push_back(i0); } } return newMeshFromArrays(pos.data(), nrm.data(), uv.data(), int(pos.size() / 3), idx.data(), int(idx.size())); } // --------------------------------------------------------------------------- // 2D drawing // --------------------------------------------------------------------------- void Graphics::clear2DBatches() { solidBatches.clear(); texturedBatches.clear(); litBatches.clear(); overlaySpans.clear(); sceneColorComposited = false; } void Graphics::noteSolidOverlay(uint32_t idx) { if (idx >= solidBatches.size()) return; const uint32_t n = uint32_t(solidBatches[idx].batch.vertices().size()); if (!overlaySpans.empty() && overlaySpans.back().kind == OverlayKind::Solid && overlaySpans.back().index == idx) { overlaySpans.back().vertCount = n - overlaySpans.back().vertBegin; return; } const uint32_t begin = n >= 6u ? n - 6u : 0u; overlaySpans.push_back({OverlayKind::Solid, idx, begin, n - begin}); } void Graphics::noteTexturedOverlay(Texture *tex, uint32_t idx) { if (tex && tex == getSceneColorTexture()) sceneColorComposited = true; if (!overlaySpans.empty() && overlaySpans.back().kind == OverlayKind::Textured && overlaySpans.back().index == idx) return; overlaySpans.push_back({OverlayKind::Textured, idx, 0, 0}); } void Graphics::noteLitOverlay(uint32_t idx) { if (!overlaySpans.empty() && overlaySpans.back().kind == OverlayKind::Lit && overlaySpans.back().index == idx) return; overlaySpans.push_back({OverlayKind::Lit, idx, 0, 0}); } void Graphics::drawSolidRect(float x, float y, float w, float h, const Color &color, BlendMode blend) { auto it = std::find_if(solidBatches.begin(), solidBatches.end(), [&](const SolidBatch &sb) { return sb.blend == blend; }); if (it == solidBatches.end()) { solidBatches.push_back(SolidBatch{blend, Batcher{}}); it = solidBatches.end() - 1; } it->batch.addRect(x, y, w, h, color); noteSolidOverlay(uint32_t(it - solidBatches.begin())); } void Graphics::drawSolidRectRotated(float cx, float cy, float w, float h, float degrees, const Color &color, BlendMode blend) { auto it = std::find_if(solidBatches.begin(), solidBatches.end(), [&](const SolidBatch &sb) { return sb.blend == blend; }); if (it == solidBatches.end()) { solidBatches.push_back(SolidBatch{blend, Batcher{}}); it = solidBatches.end() - 1; } it->batch.addRectRotated(cx, cy, w, h, degrees, color); noteSolidOverlay(uint32_t(it - solidBatches.begin())); } void Graphics::drawTexturedRect(Texture *texture, float x, float y, float w, float h, const Color &color) { drawTexturedRectUV(texture, x, y, w, h, 0.f, 0.f, 1.f, 1.f, color); } void Graphics::drawTexturedRectShader(Texture *texture, Shader *shader, float x, float y, float w, float h, const Color &color) { drawTexturedRectShaderUV(texture, shader, x, y, w, h, 0.f, 0.f, 1.f, 1.f, color); } void Graphics::drawTexturedRectUV(Texture *texture, float x, float y, float w, float h, float u0, float v0, float u1, float v1, const Color &color) { drawTexturedRectShaderUV(texture, currentShader, x, y, w, h, u0, v0, u1, v1, color); } void Graphics::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, BlendMode blend) { if (!texture) { drawSolidRect(x, y, w, h, color, blend); return; } if (texturedBatches.empty() || texturedBatches.back().texture != texture || texturedBatches.back().shader != shader || texturedBatches.back().depth != nullptr || texturedBatches.back().blend != blend) { texturedBatches.push_back(TexturedBatch{texture, nullptr, shader, blend, Batcher{}}); } texturedBatches.back().batch.addTexturedRect(x, y, w, h, color, u0, v0, u1, v1, rotatedUV); noteTexturedOverlay(texture, uint32_t(texturedBatches.size() - 1)); } void Graphics::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, BlendMode blend) { if (!texture) { drawSolidRect(cx - w * 0.5f, cy - h * 0.5f, w, h, color, blend); return; } if (texturedBatches.empty() || texturedBatches.back().texture != texture || texturedBatches.back().shader != shader || texturedBatches.back().depth != nullptr || texturedBatches.back().blend != blend) { texturedBatches.push_back(TexturedBatch{texture, nullptr, shader, blend, Batcher{}}); } texturedBatches.back().batch.addTexturedRectRotated(cx, cy, w, h, degrees, color, u0, v0, u1, v1, rotatedUV); noteTexturedOverlay(texture, uint32_t(texturedBatches.size() - 1)); } void Graphics::drawTexturedRectShaderDepth(Texture *color, Texture *depth, Shader *shader, float x, float y, float w, float h, const Color &tint) { if (!color) { drawSolidRect(x, y, w, h, tint); return; } if (texturedBatches.empty() || texturedBatches.back().texture != color || texturedBatches.back().shader != shader || texturedBatches.back().depth != depth || texturedBatches.back().blend != BlendMode::Alpha) { texturedBatches.push_back(TexturedBatch{color, depth, shader, BlendMode::Alpha, Batcher{}}); } texturedBatches.back().batch.addTexturedRect(x, y, w, h, tint, 0.f, 0.f, 1.f, 1.f, false); noteTexturedOverlay(color, uint32_t(texturedBatches.size() - 1)); } void Graphics::drawTexturedRectShaderDepthMotion(Texture *color, Texture *depth, Texture *motion, Shader *shader, float x, float y, float w, float h, const Color &tint) { if (!color) { drawSolidRect(x, y, w, h, tint); return; } if (texturedBatches.empty() || texturedBatches.back().texture != color || texturedBatches.back().shader != shader || texturedBatches.back().depth != depth || texturedBatches.back().motion != motion || texturedBatches.back().blend != BlendMode::Opaque) { TexturedBatch batch{color, depth, shader, BlendMode::Opaque, Batcher{}}; batch.motion = motion; texturedBatches.push_back(std::move(batch)); } texturedBatches.back().batch.addTexturedRect(x, y, w, h, tint, 0.f, 0.f, 1.f, 1.f, false); noteTexturedOverlay(color, uint32_t(texturedBatches.size() - 1)); } void Graphics::drawTexturedRectShader4(Texture *color, Texture *depth, Texture *motion, Texture *extra, Shader *shader, float x, float y, float w, float h, const Color &tint) { if (!color) { drawSolidRect(x, y, w, h, tint); return; } if (texturedBatches.empty() || texturedBatches.back().texture != color || texturedBatches.back().shader != shader || texturedBatches.back().depth != depth || texturedBatches.back().motion != motion || texturedBatches.back().extra != extra || texturedBatches.back().blend != BlendMode::Opaque) { TexturedBatch batch{color, depth, shader, BlendMode::Opaque, Batcher{}}; batch.motion = motion; batch.extra = extra; texturedBatches.push_back(std::move(batch)); } texturedBatches.back().batch.addTexturedRect(x, y, w, h, tint, 0.f, 0.f, 1.f, 1.f, false); noteTexturedOverlay(color, uint32_t(texturedBatches.size() - 1)); } void Graphics::drawTexturedRectShader5(Texture *color, Texture *depth, Texture *motion, Texture *extra, Texture *specular, Shader *shader, float x, float y, float w, float h, const Color &tint) { if (!color) { drawSolidRect(x, y, w, h, tint); return; } if (texturedBatches.empty() || texturedBatches.back().texture != color || texturedBatches.back().shader != shader || texturedBatches.back().depth != depth || texturedBatches.back().motion != motion || texturedBatches.back().extra != extra || texturedBatches.back().specular != specular || texturedBatches.back().blend != BlendMode::Opaque) { TexturedBatch batch{color, depth, shader, BlendMode::Opaque, Batcher{}}; batch.motion = motion; batch.extra = extra; batch.specular = specular; texturedBatches.push_back(std::move(batch)); } texturedBatches.back().batch.addTexturedRect(x, y, w, h, tint, 0.f, 0.f, 1.f, 1.f, false); noteTexturedOverlay(color, uint32_t(texturedBatches.size() - 1)); } void Graphics::drawTexturedRectLitUV(Texture *albedo, Texture *normal, float x, float y, float w, float h, float u0, float v0, float u1, float v1, const Color &color) { if (!albedo) { drawSolidRect(x, y, w, h, color); return; } if (litBatches.empty() || litBatches.back().albedo != albedo || litBatches.back().normal != normal) { litBatches.push_back(LitBatch{albedo, normal, Batcher{}}); } litBatches.back().batch.addTexturedRect(x, y, w, h, color, u0, v0, u1, v1, false); noteLitOverlay(uint32_t(litBatches.size() - 1)); } void Graphics::setLighting2D(const Lighting2DUBO &ubo) { // Uploaded on demand before flushing lit batches. lighting2dFrame = ubo; } void Graphics::flush2D(wgpu::RenderPassEncoder pass, int viewW, int viewH, WGPUTextureFormat format) { auto spans = std::move(overlaySpans); const bool offscreen = uint32_t(format) != uint32_t(surfaceFormat); struct SolidUpload { BlendMode blend = BlendMode::Alpha; uint64_t offset = 0; uint64_t bytes = 0; }; std::vector solidUploads; solidUploads.reserve(solidBatches.size()); for (const auto &sb : solidBatches) { if (sb.batch.empty()) { solidUploads.push_back(SolidUpload{sb.blend, 0, 0}); continue; } Batcher ndc = sb.batch; ndc.toNDC(viewW, viewH); auto verts = ndc.vertices(); // Batcher::toNDC targets Vulkan's Y-down NDC ((-1,-1)=top-left), but // WebGPU's clip space is Y-up ((-1,-1)=bottom-left). Flip Y so logical // top-left maps to the swapchain's top-left. for (auto &v : verts) v.pos.y = -v.pos.y; std::vector data; data.reserve(verts.size() * 6); for (const auto &v : verts) { data.push_back(v.pos.x); data.push_back(v.pos.y); data.push_back(v.color.r); data.push_back(v.color.g); data.push_back(v.color.b); data.push_back(v.color.a); } const uint64_t bytes = data.size() * sizeof(float); auto &arena = currentVertexArena(); ensureVertexArena(arena, arena.used + bytes); const uint64_t offset = arena.alloc(bytes); queue.WriteBuffer(arena.buffer, offset, data.data(), bytes); solidUploads.push_back(SolidUpload{sb.blend, offset, bytes}); } auto solidPipe = [&](BlendMode mode) -> wgpu::RenderPipeline { return get2DColorPipeline(mode, offscreen); }; auto drawSolid = [&](uint32_t batchIndex, uint32_t first, uint32_t count) { if (batchIndex >= solidUploads.size()) return; const SolidUpload &u = solidUploads[batchIndex]; if (u.bytes == 0 || count == 0) return; pass.SetPipeline(solidPipe(u.blend)); pass.SetVertexBuffer(0, currentVertexArena().buffer, u.offset, u.bytes); pass.Draw(count, 1, first, 0); }; if (!spans.empty()) { for (const auto &sp : spans) { if (sp.kind == OverlayKind::Solid) drawSolid(sp.index, sp.vertBegin, sp.vertCount); else if (sp.kind == OverlayKind::Textured && sp.index < texturedBatches.size()) drawTexturedBatch(pass, texturedBatches[sp.index], viewW, viewH, format, offscreen); else if (sp.kind == OverlayKind::Lit && sp.index < litBatches.size()) drawLitBatch(pass, litBatches[sp.index], viewW, viewH, format); } } else { for (size_t i = 0; i < solidUploads.size(); ++i) { if (solidUploads[i].bytes > 0) drawSolid(uint32_t(i), 0, uint32_t(solidBatches[i].batch.vertices().size())); } for (auto &tb : texturedBatches) { if (tb.batch.empty()) continue; drawTexturedBatch(pass, tb, viewW, viewH, format, offscreen); } for (auto &lb : litBatches) { if (lb.batch.empty()) continue; drawLitBatch(pass, lb, viewW, viewH, format); } } clear2DBatches(); } void Graphics::drawTexturedBatch(wgpu::RenderPassEncoder pass, TexturedBatch &tb, int viewW, int viewH, WGPUTextureFormat format, bool offscreen) { Batcher ndc = tb.batch; ndc.toNDC(viewW, viewH); auto verts = ndc.vertices(); for (auto &v : verts) v.pos.y = -v.pos.y; if (verts.empty()) return; std::vector data; data.reserve(verts.size() * 8); for (const auto &v : verts) { data.push_back(v.pos.x); data.push_back(v.pos.y); data.push_back(v.color.r); data.push_back(v.color.g); data.push_back(v.color.b); data.push_back(v.color.a); data.push_back(v.uv.x); data.push_back(v.uv.y); } uint64_t bytes = data.size() * sizeof(float); auto &arena = currentVertexArena(); ensureVertexArena(arena, arena.used + bytes); uint64_t vtxOffset = arena.alloc(bytes); queue.WriteBuffer(arena.buffer, vtxOffset, data.data(), bytes); GpuTexture *gpu = gpuForTexture(tb.texture); GpuTexture *depthGpu = gpuForTexture(tb.depth); GpuTexture *motionGpu = gpuForTexture(tb.motion); GpuTexture *extraGpu = gpuForTexture(tb.extra); GpuTexture *specularGpu = gpuForTexture(tb.specular); auto &uboArena = currentUboArena(); ensureUboArena(uboArena, uboArena.used + 256); // Push-constant (Externals) block for custom shaders. uint32_t pushOffset = 0; if (tb.shader && tb.shader->pushConstantSize() > 0) { pushOffset = uboArena.alloc(Shader::kPushConstantBytes, 256); queue.WriteBuffer(uboArena.buffer, pushOffset, tb.shader->pushConstantData(), Shader::kPushConstantBytes); } wgpu::BindGroup bg = makeTex2DBindGroup(gpu, depthGpu, motionGpu, extraGpu, specularGpu); uint32_t offsets[1] = {pushOffset}; wgpu::RenderPipeline pipe; if (tb.shader && tb.shader->gpuHandle) { auto *gs = static_cast(tb.shader->gpuHandle); const bool hdr = format == WGPUTextureFormat_RGBA16Float; if (tb.blend == BlendMode::Opaque) pipe = hdr ? gs->hdrOffscreenOpaquePipeline : (offscreen ? gs->offscreenOpaquePipeline : gs->swapchainOpaquePipeline); else pipe = hdr ? gs->hdrOffscreenPipeline : (offscreen ? gs->offscreenPipeline : gs->swapchainPipeline); } else { switch (tb.blend) { case BlendMode::Additive: pipe = offscreen ? offscreenTexturedAdditivePipeline : texturedAdditivePipeline; break; case BlendMode::Premultiplied: pipe = offscreen ? offscreenTexturedPremultipliedPipeline : texturedPremultipliedPipeline; break; case BlendMode::Multiply: pipe = offscreen ? offscreenTexturedMultiplyPipeline : texturedMultiplyPipeline; break; case BlendMode::Opaque: pipe = format == WGPUTextureFormat_RGBA16Float ? hdrOffscreenTexturedOpaquePipeline : (offscreen ? offscreenTexturedOpaquePipeline : texturedOpaquePipeline); break; case BlendMode::Alpha: default: pipe = format == WGPUTextureFormat_RGBA16Float ? hdrOffscreenTexturedPipeline : (offscreen ? offscreenTexturedPipeline : texturedPipeline); break; } } if (!pipe) return; pass.SetPipeline(pipe); pass.SetBindGroup(0, bg, 1, offsets); pass.SetVertexBuffer(0, arena.buffer, vtxOffset, bytes); pass.Draw(static_cast(verts.size()), 1, 0, 0); } void Graphics::drawLitBatch(wgpu::RenderPassEncoder pass, LitBatch &lb, int viewW, int viewH, WGPUTextureFormat format) { Batcher ndc = lb.batch; ndc.toNDC(viewW, viewH); auto verts = ndc.vertices(); for (auto &v : verts) v.pos.y = -v.pos.y; if (verts.empty()) return; std::vector data; data.reserve(verts.size() * 8); for (const auto &v : verts) { data.push_back(v.pos.x); data.push_back(v.pos.y); data.push_back(v.color.r); data.push_back(v.color.g); data.push_back(v.color.b); data.push_back(v.color.a); data.push_back(v.uv.x); data.push_back(v.uv.y); } uint64_t bytes = data.size() * sizeof(float); auto &arena = currentVertexArena(); ensureVertexArena(arena, arena.used + bytes); uint64_t vtxOffset = arena.alloc(bytes); queue.WriteBuffer(arena.buffer, vtxOffset, data.data(), bytes); GpuTexture *albedoGpu = gpuForTextureOrWhite(lb.albedo); GpuTexture *normalGpu = gpuForTextureOrWhite(lb.normal); auto &uboArena = currentUboArena(); ensureUboArena(uboArena, uboArena.used + 512); uint32_t uboOffset = uboArena.alloc(sizeof(Lighting2DUBO), 256); queue.WriteBuffer(uboArena.buffer, uboOffset, &lighting2dFrame, sizeof(Lighting2DUBO)); wgpu::BindGroup bg = makeTex2DBindGroup(albedoGpu, normalGpu); uint32_t offsets[1] = {uboOffset}; wgpu::RenderPipeline pipe = get2DLitPipeline(uint32_t(format) != uint32_t(surfaceFormat)); if (!pipe) return; pass.SetPipeline(pipe); pass.SetBindGroup(0, bg, 1, offsets); pass.SetVertexBuffer(0, arena.buffer, vtxOffset, bytes); pass.Draw(static_cast(verts.size()), 1, 0, 0); } // --------------------------------------------------------------------------- // 3D rendering // --------------------------------------------------------------------------- void Graphics::begin3DFrame() { if (!initialized || !device) return; frame3DStarted = true; frameHad3DThisFrame = false; // Match the desktop (Vulkan) backend: had3DThisFrame() must return true // once a 3D frame begins, or RenderSystem3D::render bails out before any // mesh is drawn. frameHad3D = true; sceneColorPassOpen = false; mesh3dDraws.clear(); shadowPassDraws.clear(); voxelDraws.clear(); if (surfaceNeedsRecreate.load()) { surfaceNeedsRecreate.store(false); markSwapchainDirty(); } rebuildSwapchainIfNeeded(); } void Graphics::begin3DFrameToCanvas(Canvas *canvas) { if (!canvas) throw Exception("begin3DFrameToCanvas: null canvas"); if (!device) throw Exception("begin3DFrameToCanvas: device not initialized"); active3DCanvas = static_cast(canvas); begin3DFrame(); } void Graphics::end3DFrameToCanvas() { OffscreenCanvas *canvas = active3DCanvas; if (!canvas || !device) return; auto &uboArena = currentUboArena(); uboArena.reset(); ensureUboArena(uboArena, 4096); auto &vertexArena = currentVertexArena(); vertexArena.reset(); wgpuInstanceProcessEvents(instance.Get()); OffscreenTimestampReadback *timestampSlot = nullptr; if (offscreenTimestampSupported && offscreenTimestampQuerySet && offscreenTimestampResolveBuffer) { for (size_t candidate = 0; candidate < offscreenTimestampReadbacks.size(); ++candidate) { const size_t index = (offscreenTimestampReadbackCursor + candidate) % offscreenTimestampReadbacks.size(); if (!offscreenTimestampReadbacks[index].pending) { timestampSlot = &offscreenTimestampReadbacks[index]; offscreenTimestampReadbackCursor = (index + 1) % offscreenTimestampReadbacks.size(); break; } } } wgpu::CommandEncoder encoder = device.CreateCommandEncoder(); WGPURenderPassColorAttachment color{}; color.view = canvas->colorView.Get(); color.depthSlice = WGPU_DEPTH_SLICE_UNDEFINED; color.loadOp = WGPULoadOp_Clear; color.storeOp = WGPUStoreOp_Store; const Color canvasClear = canvas->clearRequested ? canvas->clearColor : clearColor; color.clearValue = {canvasClear.r, canvasClear.g, canvasClear.b, canvasClear.a}; WGPURenderPassDepthStencilAttachment depth{}; depth.view = canvas->depthView.Get(); depth.depthClearValue = 1.f; depth.depthLoadOp = WGPULoadOp_Clear; depth.depthStoreOp = WGPUStoreOp_Store; depth.stencilLoadOp = WGPULoadOp_Undefined; depth.stencilStoreOp = WGPUStoreOp_Undefined; WGPURenderPassDescriptor descriptor{}; descriptor.colorAttachmentCount = 1; descriptor.colorAttachments = &color; descriptor.depthStencilAttachment = &depth; WGPUPassTimestampWrites timestampWrites{}; if (timestampSlot) { timestampWrites.querySet = offscreenTimestampQuerySet.Get(); timestampWrites.beginningOfPassWriteIndex = 0; timestampWrites.endOfPassWriteIndex = 1; descriptor.timestampWrites = ×tampWrites; } wgpu::RenderPassEncoder pass = encoder.BeginRenderPass( reinterpret_cast(&descriptor)); flushMesh3D(pass, canvas->isHDR() ? WGPUTextureFormat_RGBA16Float : WGPUTextureFormat_RGBA8Unorm, true); pass.End(); if (timestampSlot) { encoder.ResolveQuerySet(offscreenTimestampQuerySet, 0, 2, offscreenTimestampResolveBuffer, 0); encoder.CopyBufferToBuffer(offscreenTimestampResolveBuffer, 0, timestampSlot->buffer, 0, sizeof(uint64_t) * 2); } wgpu::CommandBuffer command = encoder.Finish(); queue.Submit(1, &command); if (timestampSlot) { timestampSlot->pending = true; WGPUBufferMapCallbackInfo callback{}; callback.mode = WGPUCallbackMode_AllowProcessEvents; callback.callback = [](WGPUMapAsyncStatus status, WGPUStringView, void *userdata1, void *) { auto *slot = static_cast(userdata1); if (status == WGPUMapAsyncStatus_Success && slot && slot->owner) { const auto *ticks = static_cast( slot->buffer.GetConstMappedRange(0, sizeof(uint64_t) * 2)); if (ticks && ticks[1] >= ticks[0]) { const float milliseconds = float(ticks[1] - ticks[0]) * 1.0e-6f; slot->owner->completedOffscreenTimestampMs.fetch_add(milliseconds); } slot->buffer.Unmap(); } if (slot) slot->pending = false; }; callback.userdata1 = timestampSlot; wgpuBufferMapAsync(timestampSlot->buffer.Get(), WGPUMapMode_Read, 0, sizeof(uint64_t) * 2, callback); } canvas->clearRequested = false; active3DCanvas = nullptr; frame3DStarted = false; frameHad3DThisFrame = false; frameHad3D = false; } void Graphics::setMesh3DViewProj(const glm::mat4 &viewProj) { mesh3dViewProj = viewProj; } void Graphics::setMesh3DView(const glm::mat4 &view) { mesh3dView = view; } void Graphics::setMesh3DClip(float nearZ, float farZ) { mesh3dNear = nearZ; mesh3dFar = farZ; } Texture *Graphics::getSceneColorTexture() { return sceneColorTexture; } void Graphics::drawMesh(Mesh *mesh, const glm::mat4 &model, Texture *texture, const Color &tint) { drawMeshShader(mesh, model, texture, tint, nullptr); } void Graphics::drawMeshShader(Mesh *mesh, const glm::mat4 &model, Texture *texture, const Color &tint, Shader *shader) { if (!mesh || !mesh->gpuHandle) return; frameHad3DThisFrame = true; frameHad3D = true; Mesh3dDraw d; d.mesh = mesh; d.texture = texture; d.model = model; d.tint = tint; d.shader = shader; d.surfaceMode = mesh3dSurfaceMode; d.surfaceBlend = mesh3dSurfaceBlend; d.depthWrite = mesh3dSurfaceDepthWrite; d.doubleSided = mesh3dSurfaceDoubleSided; d.shadowReceive = mesh3dShadowReceive; d.alphaCutoff = mesh3dAlphaCutoff; d.alphaTechnique = mesh3dAlphaTechnique; mesh3dDraws.push_back(d); } void Graphics::setMesh3DNormalTexture(Texture* normal) { mesh3dNormalTexture = normal; } void Graphics::setMesh3DHeightTexture(Texture *height) { mesh3dHeightTexture = height; } void Graphics::setMesh3DSceneDepth(Texture *depth) { mesh3dSceneDepthTexture = depth; } void Graphics::setMesh3DMaterial(float metallic, float roughness) { mesh3dMetallic = metallic; mesh3dRoughness = roughness; } void Graphics::setMesh3DSurface(SurfaceMode mode, BlendMode blend, bool depthWrite, bool doubleSided, float alphaCutoff, const std::string &alphaTechnique) { mesh3dSurfaceMode = mode; mesh3dSurfaceBlend = blend; mesh3dSurfaceDepthWrite = depthWrite; mesh3dSurfaceDoubleSided = doubleSided; mesh3dAlphaCutoff = std::clamp(alphaCutoff, 0.f, 1.f); mesh3dAlphaTechnique = alphaTechnique; } void Graphics::setMesh3DTexCellBomb(float cellScale, float strength, float rotAmount) { mesh3dTexBombScale = cellScale; mesh3dTexBombStrength = strength; mesh3dTexBombRot = rotAmount; } void Graphics::setMesh3DParallax(float scale, float minLayers, float maxLayers) { mesh3dParallaxScale = scale; mesh3dParallaxMin = minLayers; mesh3dParallaxMax = maxLayers; } void Graphics::setMesh3DLighting(const Lighting3DPack &pack) { mesh3dLighting = pack; } void Graphics::setCloudShadows(float strength, float worldCell, float time, float windSpeed, float windAngle, float coverage, float detail) { mesh3dCloud = glm::vec4(std::clamp(strength, 0.f, 1.f), std::max(worldCell, 1e-4f), time, 0.f); mesh3dCloudWind = glm::vec4(std::cos(windAngle) * windSpeed, std::sin(windAngle) * windSpeed, std::clamp(coverage, 0.f, 1.f), std::clamp(detail, 0.f, 1.f)); } void Graphics::setMesh3DClusteredLighting(const ClusteredLightingUpload &upload) { mesh3dClustered = upload; mesh3dClusteredActive = upload.active; if (upload.active) uploadClusteredLighting(upload); } void Graphics::setMesh3DClusteredActive(bool active) { mesh3dClusteredActive = active; } void Graphics::setMesh3DSSAO(float intensity) { mesh3dSsaoIntensity = std::clamp(intensity, 0.f, 1.f); } void Graphics::uploadClusteredLighting(const ClusteredLightingUpload &upload) { if (!device) return; ClusteredStorage &st = clusteredStorage[currentFrameSlot()]; auto ensure = [&](wgpu::Buffer &buf, uint64_t &cap, uint64_t need) { if (need == 0) need = 4; if (buf && cap >= need) return; WGPUBufferDescriptor bd{}; bd.label = sv("eve_clustered_ssbo"); bd.size = std::max(need, cap ? cap * 2 : need); bd.usage = WGPUBufferUsage_CopyDst | WGPUBufferUsage_Storage; bd.mappedAtCreation = false; buf = device.CreateBuffer(reinterpret_cast(&bd)); cap = bd.size; }; const uint64_t lightsBytes = std::max(1, upload.lights.size()) * sizeof(ClusteredLightGpu); const uint64_t tableBytes = std::max(1, upload.clusterTable.size()) * sizeof(ClusterTableEntry); const uint64_t indicesBytes = std::max(1, upload.lightIndices.size()) * sizeof(uint32_t); ensure(st.lights, st.lightsCap, lightsBytes); ensure(st.table, st.tableCap, tableBytes); ensure(st.indices, st.indicesCap, indicesBytes); ClusteredLightGpu zero{}; if (!upload.lights.empty()) queue.WriteBuffer(st.lights, 0, upload.lights.data(), lightsBytes); else queue.WriteBuffer(st.lights, 0, &zero, sizeof(zero)); if (!upload.clusterTable.empty()) queue.WriteBuffer(st.table, 0, upload.clusterTable.data(), tableBytes); if (!upload.lightIndices.empty()) queue.WriteBuffer(st.indices, 0, upload.lightIndices.data(), indicesBytes); } void Graphics::setMesh3DLight(const glm::vec3 &dir, const glm::vec3 &color) { mesh3dLighting.lights[0].posRadius = glm::vec4(dir, 0.f); mesh3dLighting.lights[0].color = glm::vec4(color, 1.f); } void Graphics::setMesh3DCameraPos(const glm::vec3 &eye) { mesh3dCameraPos = eye; } void Graphics::setMesh3DEnv(Texture *cube, float intensity) { mesh3dEnvTexture = cube; mesh3dEnvIntensity = intensity; } void Graphics::setMesh3DEnvProbe(const glm::vec3 ¢er, const glm::vec3 &extent) { mesh3dEnvProbeCenter = center; mesh3dEnvProbeExtent = glm::max(extent, glm::vec3(0.f)); } void Graphics::setMesh3DReflectionProbes(const ReflectionProbeUpload &upload) { mesh3dReflectionProbes = upload; } void Graphics::setMesh3DShadows(const ShadowUpload &upload) { mesh3dShadows = upload; } void Graphics::setMesh3DShadowReceive(bool receive) { mesh3dShadowReceive = receive; } void Graphics::beginShadowPass(int cascadeIndex) { shadowPassCascade = cascadeIndex; shadowPassDraws.clear(); } void Graphics::drawMeshShadow(Mesh *mesh, const glm::mat4 &lightMVP) { if (!mesh || !mesh->gpuHandle) return; ShadowDraw d; d.mesh = mesh; d.mvp = lightMVP; shadowPassDraws.push_back(d); } void Graphics::drawMeshShadowAlpha(Mesh *mesh, const glm::mat4 &lightMVP, Texture *albedo) { if (!mesh || !mesh->gpuHandle) return; ShadowDraw d; d.mesh = mesh; d.albedo = albedo; d.mvp = lightMVP; d.alphaTest = true; shadowPassDraws.push_back(d); } void Graphics::endShadowPass() { if (shadowPassCascade < 0 || shadowPassCascade >= ShadowConfig::kCascades) { shadowPassCascade = -1; shadowPassDraws.clear(); return; } shadowCascadeDraws[shadowPassCascade] = shadowPassDraws; shadowPassDraws.clear(); shadowPassCascade = -1; } // --------------------------------------------------------------------------- // GBuffer pass // --------------------------------------------------------------------------- void Graphics::beginGBufferPass(int width, int height) { if (!device) return; createGbufferResources(width, height); gbufferPassActive = true; gbufferPassPending = true; gbufferPassDraws.clear(); } void Graphics::drawMeshGBuffer(Mesh *mesh, const glm::mat4 &mvp, const glm::mat4 &model, float nearZ, float farZ, Texture *albedo, float tintR, float tintG, float tintB, float motionX, float motionY, float roughness, float metallic) { if (!mesh || !mesh->gpuHandle) return; GbufferDraw d; d.mesh = mesh; d.albedo = albedo; d.mvp = mvp; d.model = model; d.nearZ = nearZ; d.farZ = farZ; d.tint = glm::vec4(tintR, tintG, tintB, 1.f); d.motion = glm::vec2(motionX, motionY); d.roughness = roughness; d.metallic = metallic; gbufferPassDraws.push_back(d); } void Graphics::drawMeshGBufferAlpha(Mesh *mesh, const glm::mat4 &mvp, const glm::mat4 &model, float nearZ, float farZ, Texture *albedo, float tintR, float tintG, float tintB, float motionX, float motionY, float roughness, float metallic) { if (!mesh || !mesh->gpuHandle) return; GbufferDraw d; d.mesh = mesh; d.albedo = albedo; d.mvp = mvp; d.model = model; d.nearZ = nearZ; d.farZ = farZ; d.tint = glm::vec4(tintR, tintG, tintB, 1.f); d.motion = glm::vec2(motionX, motionY); d.roughness = roughness; d.metallic = metallic; d.alphaTest = true; gbufferPassDraws.push_back(d); } void Graphics::endGBufferPass() { gbufferPassActive = false; // Expose the G-buffer textures (depth/normal/albedo/hwDepth) to RenderControl // so post passes (AO, X-ray scene depth) can sample them this frame. if (renderControl_ && !gbufferSlots.empty()) { GbufferSlot &slot = gbufferSlots[currentFrameSlot()]; renderControl_->getGBuffer()->setTargets(gbufferWidth, gbufferHeight, &slot.depthColorTex, &slot.normalTex, &slot.albedoTex, &slot.depthTex); } } void Graphics::beginDecalPass(int width, int height) { if (!device || width <= 0 || height <= 0) return; createDecalResources(width, height); if (!decalFlatAlbedo) { const uint8_t transparent[4] = {0, 0, 0, 0}; const uint8_t flatNormal[4] = {128, 128, 255, 255}; const uint8_t neutralParams[4] = {128, 128, 0, 255}; decalFlatAlbedo = newTexture(1, 1, transparent); decalFlatNormal = newTexture(1, 1, flatNormal); decalFlatParams = newTexture(1, 1, neutralParams); } decalPassActive = true; decalPassPending = false; decalPassDraws.clear(); } void Graphics::setDecalCamera(const glm::mat4 &viewProj, float nearZ, float farZ) { (void)nearZ; (void)farZ; decalViewProj = viewProj; } void Graphics::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) { if (!decalPassActive) return; DecalDraw draw; draw.model = model; draw.albedo = albedo ? albedo : decalFlatAlbedo; draw.normal = normal ? normal : decalFlatNormal; draw.params = params ? params : decalFlatParams; if (uvRect) draw.uvRect = glm::vec4(uvRect[0], uvRect[1], uvRect[2], uvRect[3]); draw.fadeParams = glm::vec4(fade, normalStrength, roughnessStrength, metalStrength); draw.extraParams = glm::vec4(emissiveStrength, float(blendMode == 1), 0.f, 0.f); decalPassDraws.push_back(draw); } void Graphics::endDecalPass() { if (!decalPassActive) return; decalPassActive = false; decalPassPending = true; } // --------------------------------------------------------------------------- // Voxel // --------------------------------------------------------------------------- void Graphics::drawVoxelFaceInstances(const uint32_t *packed, int count, float originX, float originY, float originZ, const std::string &faceDir, Texture *atlas, int tilesPerRow, const uint32_t *ao) { if (!device || count <= 0 || !packed) return; if (!voxelUnitQuadVerts) return; frameHad3DThisFrame = true; frameHad3D = true; int face = -1; if (faceDir == "posX" || faceDir == "+x") face = 0; else if (faceDir == "negX" || faceDir == "-x") face = 1; else if (faceDir == "posY" || faceDir == "+y") face = 2; else if (faceDir == "negY" || faceDir == "-y") face = 3; else if (faceDir == "posZ" || faceDir == "+z") face = 4; else if (faceDir == "negZ" || faceDir == "-z") face = 5; else throw Exception("drawVoxelFaceInstances: unknown faceDir '%s'", faceDir.c_str()); VoxelDraw d; d.count = uint32_t(count); d.atlas = gpuForTextureOrWhite(atlas); d.viewProj = mesh3dViewProj; d.chunkOrigin = glm::vec4(originX, originY, originZ, float(face)); d.atlasInfo = glm::vec4(float(std::max(1, tilesPerRow)), 0.f, 0.f, 0.f); d.tint = glm::vec4(1.f); d.instanceBufferOffset = 0; d.pushUboOffset = 0; // Upload packed instances + the parallel AO word (2 bits per corner) into // the per-frame instance arenas. auto &arena = voxelInstanceArena; auto &aoArena = voxelAoArena; auto ensureArena = [&](VertexArena &a) { if (!a.buffer) { WGPUBufferDescriptor bd{}; bd.label = sv("eve_voxel_instances"); bd.size = 1u << 20; bd.usage = WGPUBufferUsage_CopyDst | WGPUBufferUsage_Vertex; bd.mappedAtCreation = false; a.buffer = device.CreateBuffer(reinterpret_cast(&bd)); a.capacity = 1u << 20; } uint64_t need = uint64_t(count) * 4; if (a.used + need > a.capacity) { uint64_t cap = a.capacity * 2; WGPUBufferDescriptor bd{}; bd.label = sv("eve_voxel_instances"); bd.size = cap; bd.usage = WGPUBufferUsage_CopyDst | WGPUBufferUsage_Vertex; bd.mappedAtCreation = false; a.buffer = device.CreateBuffer(reinterpret_cast(&bd)); a.capacity = cap; a.used = 0; } }; ensureArena(arena); ensureArena(aoArena); const uint32_t defaultAO = 0xFFu; // all four corners AO=3 (full bright) std::vector aoDefaults; if (!ao) { aoDefaults.assign(size_t(count), defaultAO); ao = aoDefaults.data(); } const uint64_t need = uint64_t(count) * 4; d.instanceBufferOffset = static_cast(arena.used); queue.WriteBuffer(arena.buffer, arena.used, packed, need); arena.used += need; d.aoBufferOffset = static_cast(aoArena.used); queue.WriteBuffer(aoArena.buffer, aoArena.used, ao, need); aoArena.used += need; voxelDraws.push_back(d); } // --------------------------------------------------------------------------- // Scene color / shadow / gbuffer resources // --------------------------------------------------------------------------- void Graphics::createSceneColorResources(int width, int height) { if (!device) return; const bool featureMsaa = !renderControl_ || renderControl_->isEnabled("msaa"); // WebGPU only supports 1x and 4x multisampling; clamp anything >= 2 to 4. const uint32_t want = featureMsaa && msaaSamples >= 2 ? 4u : 1u; if (sceneColorWidth == width && sceneColorHeight == height && sceneColorSamples == want && !sceneColorSlots.empty()) return; destroySceneColorResources(); sceneColorWidth = width; sceneColorHeight = height; const uint32_t oldSamples = sceneColorSamples; const bool samplesChanged = oldSamples != want; sceneColorSamples = want; sceneColorSlots.reserve(kFramesInFlight); for (int s = 0; s < int(kFramesInFlight); ++s) { sceneColorSlots.emplace_back(); SceneColorSlot &slot = sceneColorSlots.back(); slot.sampleCount = sceneColorSamples; WGPUTextureDescriptor cd{}; cd.label = sv("eve_scene_color"); cd.dimension = WGPUTextureDimension_2D; cd.size = {static_cast(width), static_cast(height), 1}; // Single-sample resolve target: composited to the swapchain and used // as the frame-readback source. The multisampled color lives in // msaaColor (created below) and is resolved into this texture. cd.sampleCount = 1; cd.format = sceneColorFormat; cd.mipLevelCount = 1; cd.usage = WGPUTextureUsage_TextureBinding | WGPUTextureUsage_RenderAttachment | WGPUTextureUsage_CopySrc; slot.color = device.CreateTexture(reinterpret_cast(&cd)); slot.colorView = slot.color.CreateView(); if (sceneColorSamples > 1) { // Multisampled color target; the scene pass resolves it into the // single-sample `color` above for compositing / readback. WGPUTextureDescriptor mcd{}; mcd.label = sv("eve_scene_color_msaa"); mcd.dimension = WGPUTextureDimension_2D; mcd.size = {static_cast(width), static_cast(height), 1}; mcd.sampleCount = sceneColorSamples; mcd.format = sceneColorFormat; mcd.mipLevelCount = 1; mcd.usage = WGPUTextureUsage_RenderAttachment; slot.msaaColor = device.CreateTexture(reinterpret_cast(&mcd)); slot.msaaView = slot.msaaColor.CreateView(); } WGPUTextureDescriptor dd{}; dd.label = sv("eve_scene_depth"); dd.dimension = WGPUTextureDimension_2D; dd.size = {static_cast(width), static_cast(height), 1}; dd.sampleCount = sceneColorSamples; dd.format = WGPUTextureFormat_Depth32Float; dd.mipLevelCount = 1; dd.usage = WGPUTextureUsage_RenderAttachment; slot.depth = device.CreateTexture(reinterpret_cast(&dd)); slot.depthView = slot.depth.CreateView(); slot.colorGpu.texture = slot.color; slot.colorGpu.view = slot.colorView; slot.colorGpu.width = width; slot.colorGpu.height = height; slot.colorGpu.sampler = createLinearSampler(device); slot.colorGpu.samplerState = TextureSampler::linearMipmap(); slot.colorTex.gpuHandle = &slot.colorGpu; slot.colorTex.width = width; slot.colorTex.height = height; slot.colorTex.mipmapCount = 1; } sceneColorTexture = &sceneColorSlots[0].colorTex; // 3D pipelines that render into the scene target must match its sample // count. Invalidate only the affected cached variants; they are rebuilt // individually if a later draw actually needs them. if (samplesChanged) { mesh3dPipelines = {}; mesh3dPipeline = {}; mesh3dTransparentPipeline = {}; mesh3dClusteredPipeline = {}; voxelRectPipeline = {}; } if (samplesChanged && gpuDrivenCullPipeline_) { // The forward indirect pipeline inherits the scene target sample // count. Recreate the GPU-driven layouts/pipelines lazily on the next // cull after an MSAA toggle. gpuDrivenCullPipeline_ = {}; gpuDrivenRenderPipeline_ = {}; gpuDrivenCanvasPipeline_ = {}; gpuDrivenComputePipelineLayout_ = {}; gpuDrivenRenderPipelineLayout_ = {}; gpuDrivenComputeSetLayout_ = {}; gpuDrivenRenderSetLayout_ = {}; gpuDrivenComputeBindGroup_ = {}; gpuDrivenRenderBindGroup_ = {}; } } void Graphics::destroySceneColorResources() { sceneColorSlots.clear(); sceneColorTexture = nullptr; } void Graphics::setMsaaSamples(int samples) { msaaSamples = samples > 0 ? samples : 0; if (!initialized || sceneColorWidth <= 0 || sceneColorHeight <= 0) return; // Force the offscreen targets (and the 3D pipelines that bind them) to // rebuild at the new sample count. destroySceneColorResources(); createSceneColorResources(sceneColorWidth, sceneColorHeight); } void Graphics::createShadowResources() { if (!device) return; if (shadowDepthArray) return; shadowMapSize = ShadowConfig::kMapSize; // Depth array: 3 cascade layers, renderable + sampleable. auto *gpu = new GpuTexture(); WGPUTextureDescriptor td{}; td.label = sv("eve_shadow_depth"); td.dimension = WGPUTextureDimension_2D; td.size = {static_cast(shadowMapSize), static_cast(shadowMapSize), 3}; td.sampleCount = 1; td.format = WGPUTextureFormat_Depth32Float; td.mipLevelCount = 1; td.usage = WGPUTextureUsage_TextureBinding | WGPUTextureUsage_RenderAttachment | WGPUTextureUsage_CopySrc; gpu->texture = device.CreateTexture(reinterpret_cast(&td)); WGPUTextureViewDescriptor avd{}; avd.format = WGPUTextureFormat_Depth32Float; avd.dimension = WGPUTextureViewDimension_2DArray; avd.baseMipLevel = 0; avd.mipLevelCount = 1; avd.baseArrayLayer = 0; avd.arrayLayerCount = 3; gpu->view = gpu->texture.CreateView(reinterpret_cast(&avd)); WGPUSamplerDescriptor sd{}; sd.label = sv("eve_shadow_sampler"); sd.addressModeU = WGPUAddressMode_ClampToEdge; sd.addressModeV = WGPUAddressMode_ClampToEdge; sd.addressModeW = WGPUAddressMode_ClampToEdge; sd.magFilter = WGPUFilterMode_Linear; sd.minFilter = WGPUFilterMode_Linear; sd.mipmapFilter = WGPUMipmapFilterMode_Nearest; sd.compare = WGPUCompareFunction_LessEqual; sd.maxAnisotropy = 1.f; gpu->sampler = device.CreateSampler(reinterpret_cast(&sd)); shadowDepthArray = gpu; } void Graphics::destroyShadowResources() { shadowDepthArray = nullptr; } void Graphics::createGbufferResources(int width, int height) { if (!device) return; if (gbufferWidth == width && gbufferHeight == height && !gbufferSlots.empty()) return; destroyGbufferResources(); gbufferWidth = width; gbufferHeight = height; WGPUTextureDescriptor td{}; td.label = sv("eve_gbuffer_target"); td.dimension = WGPUTextureDimension_2D; td.size = {static_cast(width), static_cast(height), 1}; td.sampleCount = 1; td.format = WGPUTextureFormat_RGBA8Unorm; td.mipLevelCount = 1; td.usage = WGPUTextureUsage_TextureBinding | WGPUTextureUsage_RenderAttachment | WGPUTextureUsage_CopySrc; WGPUTextureDescriptor dd{}; dd.label = sv("eve_gbuffer_depth"); dd.dimension = WGPUTextureDimension_2D; dd.size = {static_cast(width), static_cast(height), 1}; dd.sampleCount = 1; dd.format = WGPUTextureFormat_Depth32Float; dd.mipLevelCount = 1; // TextureBinding so X-ray (and AO) can sample the scene depth in a later pass. dd.usage = WGPUTextureUsage_TextureBinding | WGPUTextureUsage_RenderAttachment; WGPUTextureDescriptor visIdDesc = td; visIdDesc.label = sv("eve_visibility_id"); visIdDesc.format = WGPUTextureFormat_RG32Uint; WGPUTextureDescriptor visBaryDesc = td; visBaryDesc.label = sv("eve_visibility_bary"); visBaryDesc.format = WGPUTextureFormat_RG16Float; gbufferSlots.reserve(kFramesInFlight); for (int s = 0; s < int(kFramesInFlight); ++s) { gbufferSlots.emplace_back(); GbufferSlot &slot = gbufferSlots.back(); slot.normal = device.CreateTexture(reinterpret_cast(&td)); slot.normalView = slot.normal.CreateView(); slot.depthColor = device.CreateTexture(reinterpret_cast(&td)); slot.depthColorView = slot.depthColor.CreateView(); slot.albedo = device.CreateTexture(reinterpret_cast(&td)); slot.albedoView = slot.albedo.CreateView(); slot.depth = device.CreateTexture(reinterpret_cast(&dd)); slot.depthView = slot.depth.CreateView(); slot.visID = device.CreateTexture( reinterpret_cast(&visIdDesc)); slot.visIDView = slot.visID.CreateView(); slot.visBary = device.CreateTexture( reinterpret_cast(&visBaryDesc)); slot.visBaryView = slot.visBary.CreateView(); slot.normalGpu.texture = slot.normal; slot.normalGpu.view = slot.normalView; slot.normalGpu.sampler = createLinearSampler(device); slot.depthColorGpu.texture = slot.depthColor; slot.depthColorGpu.view = slot.depthColorView; slot.depthColorGpu.sampler = createLinearSampler(device); slot.albedoGpu.texture = slot.albedo; slot.albedoGpu.view = slot.albedoView; slot.albedoGpu.sampler = createLinearSampler(device); slot.depthGpu.texture = slot.depth; slot.depthGpu.view = slot.depthView; slot.depthGpu.sampler = createLinearSampler(device); slot.normalTex.gpuHandle = &slot.normalGpu; slot.normalTex.width = width; slot.normalTex.height = height; slot.depthColorTex.gpuHandle = &slot.depthColorGpu; slot.depthColorTex.width = width; slot.depthColorTex.height = height; slot.albedoTex.gpuHandle = &slot.albedoGpu; slot.albedoTex.width = width; slot.albedoTex.height = height; slot.depthTex.gpuHandle = &slot.depthGpu; slot.depthTex.width = width; slot.depthTex.height = height; } } void Graphics::destroyGbufferResources() { gbufferSlots.clear(); gbufferDepthValid_ = false; } void Graphics::createDecalResources(int width, int height) { if (!device) return; if (decalWidth == width && decalHeight == height && !decalSlots.empty()) return; destroyDecalResources(); decalWidth = width; decalHeight = height; WGPUTextureDescriptor td{}; td.label = sv("eve_decal_target"); td.dimension = WGPUTextureDimension_2D; td.size = {static_cast(width), static_cast(height), 1}; td.sampleCount = 1; td.format = WGPUTextureFormat_RGBA8Unorm; td.mipLevelCount = 1; td.usage = WGPUTextureUsage_TextureBinding | WGPUTextureUsage_RenderAttachment | WGPUTextureUsage_CopySrc; decalSlots.resize(kFramesInFlight); for (auto &slot : decalSlots) { slot.albedo = device.CreateTexture(reinterpret_cast(&td)); slot.albedoView = slot.albedo.CreateView(); slot.normal = device.CreateTexture(reinterpret_cast(&td)); slot.normalView = slot.normal.CreateView(); slot.params = device.CreateTexture(reinterpret_cast(&td)); slot.paramsView = slot.params.CreateView(); } } void Graphics::destroyDecalResources() { decalSlots.clear(); decalWidth = 0; decalHeight = 0; } // --------------------------------------------------------------------------- // Flush helpers // --------------------------------------------------------------------------- void Graphics::flushMesh3D(wgpu::RenderPassEncoder pass, WGPUTextureFormat format, bool canvasTarget) { if (mesh3dDraws.empty()) return; if (mesh3dClusteredActive && !canvasTarget && !mesh3dClusteredPipeline) { for (const auto &d : mesh3dDraws) { const bool customShader = d.shader && d.shader->gpuHandle; if (!customShader && !d.doubleSided && d.surfaceMode != SurfaceMode::Transparent && d.mesh && !d.mesh->hasGpuSkinning()) { createMesh3DClusteredPipeline(); break; } } } auto &uboArena = currentUboArena(); ensureUboArena(uboArena, uboArena.used + mesh3dDraws.size() * 10240); auto &vtxArena = currentVertexArena(); // Pre-allocate per-draw UBO slots. for (auto &d : mesh3dDraws) { d.frameUboOffset = uboArena.alloc(sizeof(Mesh3DUBO), 256); d.shadowUboOffset = uboArena.alloc(sizeof(ShadowUBO), 256); d.clusteredUboOffset = 0; if (mesh3dClusteredActive && !canvasTarget && d.mesh && !d.mesh->hasGpuSkinning()) d.clusteredUboOffset = uboArena.alloc(sizeof(Mesh3DClusteredUBO), 256); d.pushUboOffset = 0; if (d.shader && d.shader->pushConstantSize() > 0) d.pushUboOffset = uboArena.alloc(Shader::kPushConstantBytes, 256); } // Upload the per-draw frame UBO + shared shadow UBO. for (auto &d : mesh3dDraws) { if (mesh3dClusteredActive && !canvasTarget && mesh3dClusteredPipeline && d.clusteredUboOffset) { Mesh3DClusteredUBO cubo; cubo.mvp = mesh3dViewProj * d.model; cubo.model = d.model; cubo.view = mesh3dView; cubo.lightDir = glm::vec4(glm::vec3(mesh3dClustered.primaryDir), mesh3dClustered.primaryDir.w); cubo.lightColor = glm::vec4(glm::vec3(mesh3dClustered.primaryColor), mesh3dEnvIntensity); cubo.tint = d.tint; cubo.cameraPos = glm::vec4(mesh3dCameraPos, mesh3dRoughness); cubo.ambient = glm::vec4(glm::vec3(mesh3dClustered.ambient), mesh3dMetallic); cubo.gridInfo = mesh3dClustered.gridInfo; cubo.clipInfo = mesh3dClustered.clipInfo; cubo.texBomb = glm::vec4(mesh3dTexBombScale, mesh3dTexBombStrength, mesh3dTexBombRot, 0.f); cubo.parallax = glm::vec4(mesh3dParallaxScale, mesh3dParallaxMin, mesh3dParallaxMax, 0.f); float surfaceCode = float(int(d.surfaceMode)); if (d.surfaceMode == SurfaceMode::Masked && d.alphaTechnique == "dither") surfaceCode = 3.f; else if (d.surfaceMode == SurfaceMode::Masked && d.alphaTechnique == "coverage") surfaceCode = 4.f; const float aoStrength = (renderControl_ && renderControl_->isEnabled("ao")) ? mesh3dSsaoIntensity : 0.f; cubo.surface = glm::vec4(surfaceCode, d.alphaCutoff, aoStrength, 0.f); cubo.envProbeCenter = glm::vec4(mesh3dEnvProbeCenter, 1.f); cubo.envProbeExtent = glm::vec4(mesh3dEnvProbeExtent, 0.f); for (int i = 0; i < ReflectionProbeUpload::kMaxProbes; ++i) { if (i >= mesh3dReflectionProbes.count) continue; const auto &probe = mesh3dReflectionProbes.probes[i]; GpuTexture *gpu = gpuForTexture(probe.cubemap); if (!gpu || !gpu->isCube) continue; cubo.reflectionProbeCenter[i] = glm::vec4(probe.center, probe.intensity); cubo.reflectionProbeExtent[i] = glm::vec4(probe.extent, probe.blendDistance); } queue.WriteBuffer(uboArena.buffer, d.clusteredUboOffset, &cubo, sizeof(cubo)); continue; } Mesh3DUBO ubo; ubo.mvp = mesh3dViewProj * d.model; ubo.model = d.model; ubo.lightDir = glm::vec4(glm::vec3(mesh3dLighting.lights[0].posRadius), float(mesh3dLighting.count)); ubo.lightColor = mesh3dLighting.lights[0].color; ubo.tint = d.tint; ubo.cameraPos = glm::vec4(mesh3dCameraPos, mesh3dRoughness); ubo.ambient = glm::vec4(glm::vec3(mesh3dLighting.ambient), mesh3dMetallic); for (int i = 0; i < Lighting3DPack::kMaxLights; ++i) ubo.lights[i] = mesh3dLighting.lights[i]; float surfaceCode = float(int(d.surfaceMode)); if (d.surfaceMode == SurfaceMode::Masked && d.alphaTechnique == "dither") surfaceCode = 3.f; else if (d.surfaceMode == SurfaceMode::Masked && d.alphaTechnique == "coverage") surfaceCode = 4.f; ubo.texBomb = glm::vec4(mesh3dTexBombScale, mesh3dTexBombStrength, mesh3dTexBombRot, 0.f); ubo.parallax = glm::vec4(mesh3dParallaxScale, mesh3dParallaxMin, mesh3dParallaxMax, d.alphaCutoff); const float aoStrength = (renderControl_ && renderControl_->isEnabled("ao")) ? mesh3dSsaoIntensity : 0.f; ubo.surface = glm::vec4(surfaceCode, d.alphaCutoff, aoStrength, 0.f); ubo.view = mesh3dView; ubo.clipInfo = glm::vec4(mesh3dNear, mesh3dFar, 0.f, 0.f); ubo.cloud = mesh3dCloud; ubo.cloudWind = mesh3dCloudWind; ubo.envProbeCenter = glm::vec4(mesh3dEnvProbeCenter, 1.f); ubo.envProbeExtent = glm::vec4(mesh3dEnvProbeExtent, 0.f); for (int i = 0; i < ReflectionProbeUpload::kMaxProbes; ++i) { if (i >= mesh3dReflectionProbes.count) continue; const auto &probe = mesh3dReflectionProbes.probes[i]; GpuTexture *gpu = gpuForTexture(probe.cubemap); if (!gpu || !gpu->isCube) continue; ubo.reflectionProbeCenter[i] = glm::vec4(probe.center, probe.intensity); ubo.reflectionProbeExtent[i] = glm::vec4(probe.extent, probe.blendDistance); } ubo.lightColor.w = mesh3dEnvIntensity; if (d.mesh && d.mesh->hasGpuSkinning()) { const int count = std::min(d.mesh->getSkinPaletteCount(), Mesh::kMaxSkinBones); ubo.skinInfo.x = static_cast(count); const auto &palette = d.mesh->skinPalette(); for (int i = 0; i < count; ++i) { std::memcpy(&ubo.skinBones[i], palette.data() + static_cast(i) * 16u, sizeof(glm::mat4)); } } // X-ray params travel through the Frame UBO (no extra binding). Packed // in bindMeshUniforms("xray") order: colorR..G..B, bias, screenW, screenH, // rimPower, rimStrength, alpha. if (d.shader && d.shader->isXray() && d.shader->pushConstantSize() >= 9 * sizeof(float)) { const float *pc = d.shader->pushConstantData(); ubo.texBomb = glm::vec4(pc[0], pc[1], pc[2], pc[8]); // color.xyz + alpha ubo.parallax = glm::vec4(pc[3], pc[4], pc[5], pc[7]); // bias, screenW, screenH, rimStrength ubo.clipInfo.z = pc[6]; // rimPower } queue.WriteBuffer(uboArena.buffer, d.frameUboOffset, &ubo, sizeof(ubo)); if (d.shader && d.shader->pushConstantSize() > 0) queue.WriteBuffer(uboArena.buffer, d.pushUboOffset, d.shader->pushConstantData(), Shader::kPushConstantBytes); } // Capture receive-shadow per draw; RenderSystem3D changes it between queued meshes. for (auto &d : mesh3dDraws) { ShadowUBO shadowUbo = mesh3dShadows.ubo; if (!mesh3dShadows.active || !d.shadowReceive) shadowUbo.bias.y = 0.f; queue.WriteBuffer(uboArena.buffer, d.shadowUboOffset, &shadowUbo, sizeof(shadowUbo)); } for (auto &d : mesh3dDraws) { auto *gpuMesh = static_cast(d.mesh->gpuHandle); if (!gpuMesh || !gpuMesh->vertexBuffer) continue; const bool transparent = d.surfaceMode == SurfaceMode::Transparent; const BlendMode blend = transparent ? d.surfaceBlend : BlendMode::Opaque; const bool depthWrite = !transparent || d.depthWrite; // Canvas targets are 1-sample; scene pipelines follow the active MSAA // count. Both sets preserve the per-draw material raster state. const bool customShader = d.shader && d.shader->gpuHandle; wgpu::RenderPipeline pipe; if (d.shader && d.shader->gpuHandle) { auto *gs = static_cast(d.shader->gpuHandle); if (gs->isMesh3D && gs->mesh3dPipeline) { if (d.shader->isXray() && gs->mesh3dXrayPipeline) pipe = gs->mesh3dXrayPipeline; else pipe = gs->mesh3dPipeline; } } if (!pipe) pipe = getMesh3DPipeline(blend, depthWrite, d.doubleSided, canvasTarget); const bool useClustered = !canvasTarget && !customShader && !d.doubleSided && mesh3dClusteredActive && d.surfaceMode != SurfaceMode::Transparent && mesh3dClusteredPipeline && d.clusteredUboOffset; if (useClustered) pipe = mesh3dClusteredPipeline; if (!pipe) continue; pass.SetPipeline(pipe); GpuTexture *albedo = gpuForTexture(d.texture); GpuTexture *normal = gpuForTexture(mesh3dNormalTexture); GpuTexture *env = gpuForTexture(mesh3dEnvTexture); GpuTexture *height = gpuForTexture(mesh3dHeightTexture); GpuTexture *depth = mesh3dSceneDepthTexture ? gpuForTexture(mesh3dSceneDepthTexture) : flatDepthTexture3D; wgpu::BindGroup bg; uint32_t offsets[3]; if (useClustered) { wgpu::TextureView aoView_ = aoReady ? aoView[(aoWriteIndex + 1) % 2] : (whiteTexture ? whiteTexture->view : wgpu::TextureView()); bg = makeMesh3DClusteredBindGroup(albedo, normal, env, height, depth, aoView_, d.clusteredUboOffset, d.shadowUboOffset); offsets[0] = d.clusteredUboOffset; offsets[1] = d.shadowUboOffset; pass.SetBindGroup(0, bg, 2, offsets); } else { bg = makeMeshBindGroup(albedo, normal, env, height, depth, d.frameUboOffset, d.shadowUboOffset, d.pushUboOffset); offsets[0] = d.frameUboOffset; offsets[1] = d.shadowUboOffset; offsets[2] = d.pushUboOffset; pass.SetBindGroup(0, bg, 3, offsets); } if (gpuMesh->indexBuffer) { pass.SetVertexBuffer(0, gpuMesh->vertexBuffer, 0, uint64_t(gpuMesh->vertexCount) * gpuMesh->vertexStride); const uint64_t indexBytes = gpuMesh->indexFormat == wgpu::IndexFormat::Uint16 ? 2u : 4u; pass.SetIndexBuffer(gpuMesh->indexBuffer, gpuMesh->indexFormat, 0, uint64_t(gpuMesh->indexCount) * indexBytes); pass.DrawIndexed(gpuMesh->indexCount, 1, 0, 0, 0); } else { pass.SetVertexBuffer(0, gpuMesh->vertexBuffer, 0, uint64_t(gpuMesh->vertexCount) * gpuMesh->vertexStride); pass.Draw(gpuMesh->vertexCount, 1, 0, 0); } } mesh3dDraws.clear(); } void Graphics::flushShadowPass(wgpu::RenderPassEncoder pass, int cascade) { auto &uboArena = currentUboArena(); if (cascade < 0 || cascade >= ShadowConfig::kCascades) return; if (!mesh3dShadowPipeline) createShadowPipelines(); ensureUboArena(uboArena, uboArena.used + shadowCascadeDraws[cascade].size() * 8448); for (auto &d : shadowCascadeDraws[cascade]) { auto *gpuMesh = static_cast(d.mesh->gpuHandle); if (!gpuMesh || !gpuMesh->vertexBuffer) continue; pass.SetPipeline(d.alphaTest ? mesh3dShadowAlphaPipeline : mesh3dShadowPipeline); SkinPassUBO ubo; ubo.mvp = d.mvp; if (d.mesh->hasGpuSkinning()) { const int count = std::min(d.mesh->getSkinPaletteCount(), Mesh::kMaxSkinBones); ubo.skinInfo.x = static_cast(count); const auto &palette = d.mesh->skinPalette(); std::memcpy(ubo.skinBones, palette.data(), static_cast(count) * sizeof(glm::mat4)); } uint32_t offset = uboArena.alloc(sizeof(SkinPassUBO), 256); queue.WriteBuffer(uboArena.buffer, offset, &ubo, sizeof(ubo)); GpuTexture *albedo = gpuForTextureOrWhite(d.albedo); WGPUBindGroupEntry entries[3]{}; entries[0].binding = 0; entries[0].buffer = uboArena.buffer.Get(); entries[0].size = sizeof(SkinPassUBO); entries[1].binding = 1; entries[1].textureView = albedo->view.Get(); entries[2].binding = 2; entries[2].sampler = albedo->sampler.Get(); WGPUBindGroupDescriptor bgd{}; bgd.layout = shadowSetLayout.Get(); bgd.entryCount = 3; bgd.entries = entries; wgpu::BindGroup bg = device.CreateBindGroup( reinterpret_cast(&bgd)); uint32_t offsets[1] = {offset}; pass.SetBindGroup(0, bg, 1, offsets); if (gpuMesh->indexBuffer) { pass.SetVertexBuffer(0, gpuMesh->vertexBuffer, 0, uint64_t(gpuMesh->vertexCount) * gpuMesh->vertexStride); const uint64_t indexBytes = gpuMesh->indexFormat == wgpu::IndexFormat::Uint16 ? 2u : 4u; pass.SetIndexBuffer(gpuMesh->indexBuffer, gpuMesh->indexFormat, 0, uint64_t(gpuMesh->indexCount) * indexBytes); pass.DrawIndexed(gpuMesh->indexCount, 1, 0, 0, 0); } else { pass.SetVertexBuffer(0, gpuMesh->vertexBuffer, 0, uint64_t(gpuMesh->vertexCount) * gpuMesh->vertexStride); pass.Draw(gpuMesh->vertexCount, 1, 0, 0); } } shadowCascadeDraws[cascade].clear(); } void Graphics::flushGbufferPass(wgpu::RenderPassEncoder pass) { if (gbufferPassDraws.empty() || gbufferSlots.empty()) return; if (!mesh3dGbufferPipeline) createGbufferPipelines(); auto &uboArena = currentUboArena(); ensureUboArena(uboArena, uboArena.used + gbufferPassDraws.size() * 8448); for (auto &d : gbufferPassDraws) { auto *gpuMesh = static_cast(d.mesh->gpuHandle); if (!gpuMesh || !gpuMesh->vertexBuffer) continue; pass.SetPipeline(d.alphaTest ? mesh3dGbufferAlphaPipeline : mesh3dGbufferPipeline); SkinPassUBO ubo; ubo.mvp = d.mvp; ubo.model = d.model; auto u6 = [](float value) { return uint32_t(std::lround(std::clamp(value, 0.f, 1.f) * 63.f)); }; const uint32_t rough3 = uint32_t(std::lround(std::clamp(d.roughness, 0.f, 1.f) * 7.f)); const uint32_t metal3 = uint32_t(std::lround(std::clamp(d.metallic, 0.f, 1.f) * 7.f)); const uint32_t packedTint = u6(d.tint.r) | (u6(d.tint.g) << 6) | (u6(d.tint.b) << 12) | (rough3 << 18) | (metal3 << 21); auto motion12 = [](float value) { return uint32_t(std::lround(std::clamp(value, -1.f, 1.f) * 2047.f)) + 2047u; }; const uint32_t packedMotion = motion12(d.motion.x) | (motion12(d.motion.y) << 12); ubo.clip = glm::vec4(d.nearZ, d.farZ, float(packedTint), float(packedMotion)); if (d.mesh->hasGpuSkinning()) { const int count = std::min(d.mesh->getSkinPaletteCount(), Mesh::kMaxSkinBones); ubo.skinInfo.x = static_cast(count); const auto &palette = d.mesh->skinPalette(); std::memcpy(ubo.skinBones, palette.data(), static_cast(count) * sizeof(glm::mat4)); } uint32_t offset = uboArena.alloc(sizeof(SkinPassUBO), 256); queue.WriteBuffer(uboArena.buffer, offset, &ubo, sizeof(ubo)); GpuTexture *albedo = gpuForTextureOrWhite(d.albedo); WGPUBindGroupEntry entries[3]{}; entries[0].binding = 0; entries[0].buffer = uboArena.buffer.Get(); entries[0].size = sizeof(SkinPassUBO); entries[1].binding = 1; entries[1].textureView = albedo->view.Get(); entries[2].binding = 2; entries[2].sampler = albedo->sampler.Get(); WGPUBindGroupDescriptor bgd{}; bgd.layout = gbufferSetLayout.Get(); bgd.entryCount = 3; bgd.entries = entries; wgpu::BindGroup bg = device.CreateBindGroup(reinterpret_cast(&bgd)); uint32_t offsets[1] = {offset}; pass.SetBindGroup(0, bg, 1, offsets); if (gpuMesh->indexBuffer) { pass.SetVertexBuffer(0, gpuMesh->vertexBuffer, 0, uint64_t(gpuMesh->vertexCount) * gpuMesh->vertexStride); const uint64_t indexBytes = gpuMesh->indexFormat == wgpu::IndexFormat::Uint16 ? 2u : 4u; pass.SetIndexBuffer(gpuMesh->indexBuffer, gpuMesh->indexFormat, 0, uint64_t(gpuMesh->indexCount) * indexBytes); pass.DrawIndexed(gpuMesh->indexCount, 1, 0, 0, 0); } else { pass.SetVertexBuffer(0, gpuMesh->vertexBuffer, 0, uint64_t(gpuMesh->vertexCount) * gpuMesh->vertexStride); pass.Draw(gpuMesh->vertexCount, 1, 0, 0); } } gbufferPassDraws.clear(); gbufferPassPending = false; } void Graphics::flushDecalPass(wgpu::RenderPassEncoder pass) { if (decalPassDraws.empty() || gbufferSlots.empty()) { decalPassPending = false; return; } if (!decalPipeline) createDecalPipeline(); auto &uboArena = currentUboArena(); ensureUboArena(uboArena, uboArena.used + decalPassDraws.size() * 512); GbufferSlot &gbuffer = gbufferSlots[lastGbufferSlot]; pass.SetPipeline(decalPipeline); struct DecalUniforms { glm::mat4 invViewProj; glm::mat4 invModel; glm::vec4 modelR0; glm::vec4 modelR1; glm::vec4 modelR2; glm::vec4 uvRect; glm::vec4 fadeParams; glm::vec4 extraParams; glm::vec4 texel; }; static_assert(sizeof(DecalUniforms) == 240); for (const auto &draw : decalPassDraws) { DecalUniforms uniforms{}; uniforms.invViewProj = glm::inverse(decalViewProj); uniforms.invModel = glm::inverse(draw.model); uniforms.modelR0 = draw.model[0]; uniforms.modelR1 = draw.model[1]; uniforms.modelR2 = draw.model[2]; uniforms.uvRect = draw.uvRect; uniforms.fadeParams = draw.fadeParams; uniforms.extraParams = draw.extraParams; uniforms.texel = glm::vec4(1.f / float(decalWidth), 1.f / float(decalHeight), 0.f, 0.f); uint32_t offset = uboArena.alloc(256, 256); queue.WriteBuffer(uboArena.buffer, offset, &uniforms, sizeof(uniforms)); GpuTexture *albedo = gpuForTextureOrWhite(draw.albedo); GpuTexture *normal = gpuForTextureOrWhite(draw.normal); GpuTexture *params = gpuForTextureOrWhite(draw.params); WGPUBindGroupEntry entries[7]{}; entries[0].binding = 0; entries[0].buffer = uboArena.buffer.Get(); entries[0].size = sizeof(uniforms); entries[1].binding = 1; entries[1].textureView = albedo->view.Get(); entries[2].binding = 2; entries[2].textureView = normal->view.Get(); entries[3].binding = 3; entries[3].textureView = params->view.Get(); entries[4].binding = 4; entries[4].textureView = gbuffer.depthView.Get(); entries[5].binding = 5; entries[5].textureView = gbuffer.normalView.Get(); entries[6].binding = 6; entries[6].sampler = mainSampler.Get(); WGPUBindGroupDescriptor descriptor{}; descriptor.layout = decalSetLayout.Get(); descriptor.entryCount = 7; descriptor.entries = entries; wgpu::BindGroup group = device.CreateBindGroup( reinterpret_cast(&descriptor)); uint32_t offsets[1] = {offset}; pass.SetBindGroup(0, group, 1, offsets); pass.Draw(3, 1, 0, 0); } decalPassDraws.clear(); decalPassPending = false; decalReady = true; clearMeshBindGroupCache(); } void Graphics::submitPendingDeferredPasses() { if ((!gbufferPassPending || gbufferSlots.empty()) && (!decalPassPending || decalSlots.empty())) return; auto &uboArena = currentUboArena(); uboArena.reset(); ensureUboArena(uboArena, 4096 + (gbufferPassDraws.size() + decalPassDraws.size()) * 512); wgpu::CommandEncoder encoder = device.CreateCommandEncoder(); if (gbufferPassPending && !gbufferSlots.empty()) { lastGbufferSlot = currentFrameSlot(); GbufferSlot &slot = gbufferSlots[lastGbufferSlot]; WGPURenderPassColorAttachment colors[3]{}; for (auto &color : colors) { color.depthSlice = WGPU_DEPTH_SLICE_UNDEFINED; color.loadOp = WGPULoadOp_Clear; color.storeOp = WGPUStoreOp_Store; color.clearValue = {0.f, 0.f, 0.f, 0.f}; } colors[1].clearValue = {1.f, 1.f, 1.f, 1.f}; colors[0].view = slot.normalView.Get(); colors[1].view = slot.depthColorView.Get(); colors[2].view = slot.albedoView.Get(); WGPURenderPassDepthStencilAttachment depth{}; depth.view = slot.depthView.Get(); depth.depthClearValue = 1.f; depth.depthLoadOp = WGPULoadOp_Clear; depth.depthStoreOp = WGPUStoreOp_Store; depth.stencilLoadOp = WGPULoadOp_Undefined; depth.stencilStoreOp = WGPUStoreOp_Undefined; WGPURenderPassDescriptor descriptor{}; descriptor.colorAttachmentCount = 3; descriptor.colorAttachments = colors; descriptor.depthStencilAttachment = &depth; wgpu::RenderPassEncoder pass = encoder.BeginRenderPass( reinterpret_cast(&descriptor)); flushGbufferPass(pass); pass.End(); gbufferDepthValid_ = true; } if (decalPassPending && !decalSlots.empty() && !gbufferSlots.empty()) { lastDecalSlot = currentFrameSlot(); DecalSlot &slot = decalSlots[lastDecalSlot]; WGPURenderPassColorAttachment colors[3]{}; for (auto &color : colors) { color.depthSlice = WGPU_DEPTH_SLICE_UNDEFINED; color.loadOp = WGPULoadOp_Clear; color.storeOp = WGPUStoreOp_Store; color.clearValue = {0.f, 0.f, 0.f, 0.f}; } colors[0].view = slot.albedoView.Get(); colors[1].view = slot.normalView.Get(); colors[2].view = slot.paramsView.Get(); WGPURenderPassDescriptor descriptor{}; descriptor.colorAttachmentCount = 3; descriptor.colorAttachments = colors; wgpu::RenderPassEncoder pass = encoder.BeginRenderPass( reinterpret_cast(&descriptor)); flushDecalPass(pass); pass.End(); } wgpu::CommandBuffer command = encoder.Finish(); queue.Submit(1, &command); } void Graphics::flushVoxelDraws(wgpu::RenderPassEncoder pass, WGPUTextureFormat format) { if (voxelDraws.empty()) return; if (!voxelRectPipeline) createVoxelPipelines(); auto &uboArena = currentUboArena(); ensureUboArena(uboArena, uboArena.used + voxelDraws.size() * 512); pass.SetPipeline(voxelRectPipeline); for (auto &d : voxelDraws) { uint32_t offset = uboArena.alloc(256, 256); struct VoxelPC { glm::mat4 viewProj; glm::vec4 chunkOrigin; glm::vec4 atlasInfo; glm::vec4 tint; } pc; pc.viewProj = d.viewProj; pc.chunkOrigin = d.chunkOrigin; pc.atlasInfo = d.atlasInfo; pc.tint = d.tint; queue.WriteBuffer(uboArena.buffer, offset, &pc, sizeof(pc)); WGPUBindGroupEntry entries[3]{}; entries[0].binding = 0; entries[0].buffer = uboArena.buffer.Get(); entries[0].size = sizeof(pc); entries[1].binding = 1; entries[1].textureView = d.atlas->view.Get(); entries[2].binding = 2; entries[2].sampler = d.atlas->sampler.Get(); WGPUBindGroupDescriptor bgd{}; bgd.layout = voxelSetLayout.Get(); bgd.entryCount = 3; bgd.entries = entries; wgpu::BindGroup bg = device.CreateBindGroup(reinterpret_cast(&bgd)); uint32_t offsets[1] = {offset}; pass.SetBindGroup(0, bg, 1, offsets); pass.SetVertexBuffer(0, voxelUnitQuadVerts, 0, 32); pass.SetVertexBuffer(1, voxelInstanceArena.buffer, d.instanceBufferOffset, uint64_t(d.count) * 4); pass.SetVertexBuffer(2, voxelAoArena.buffer, d.aoBufferOffset, uint64_t(d.count) * 4); pass.SetIndexBuffer(voxelUnitQuadIndices, wgpu::IndexFormat::Uint32, 0, 24); pass.DrawIndexed(6, d.count, 0, 0, 0); } voxelDraws.clear(); } // --------------------------------------------------------------------------- // Present // --------------------------------------------------------------------------- bool Graphics::acquireSurfaceTexture(wgpu::TextureView &view, wgpu::Texture &texture) { if (!surface || !device || !swapchainConfigured) return false; wgpu::SurfaceTexture surfTex{}; surface.GetCurrentTexture(&surfTex); if (!surfTex.texture) return false; texture = surfTex.texture; view = texture.CreateView(); return bool(view); } void Graphics::pushValidationScope() { #ifdef EVENGINE_WEBGPU if (device) device.PushErrorScope(wgpu::ErrorFilter::Validation); #endif } void Graphics::popValidationScope() { #ifdef EVENGINE_WEBGPU if (!device) return; device.PopErrorScope(wgpu::CallbackMode::AllowProcessEvents, [](wgpu::PopErrorScopeStatus status, wgpu::ErrorType type, wgpu::StringView message) { (void)status; if (message.data && type != wgpu::ErrorType::NoError) { #if defined(__EMSCRIPTEN__) EM_ASM({ console.log("[GPU_ERR] type=" + $0 + " msg=" + UTF8ToString($1)); }, (int)type, message.data); #else std::fprintf(stderr, "[webgpu] validation error type=%d: %.*s\n", int(type), static_cast(message.length), message.data); #endif } }); #endif } struct Graphics::PendingReadback { std::string path; int width = 0; int height = 0; uint64_t bytesPerRow = 0; wgpu::Buffer dst; bool mapped = false; bool done = false; bool ok = false; }; void Graphics::present() { if (!device || !surface || !swapchainConfigured) return; pumpReadback(); rebuildSwapchainIfNeeded(); if (!swapchainConfigured) return; // RenderControl can toggle MSAA without resizing the window. Re-evaluate // the offscreen sample count every frame, matching the Vulkan backend. if (sceneColorWidth > 0 && sceneColorHeight > 0) createSceneColorResources(sceneColorWidth, sceneColorHeight); const bool aoActive = renderControl_ && renderControl_->isEnabled("ao"); wgpu::TextureView surfaceView; wgpu::Texture surfaceTex; if (!acquireSurfaceTexture(surfaceView, surfaceTex)) { return; } auto &uboArena = currentUboArena(); uboArena.reset(); auto &vtxArena = currentVertexArena(); vtxArena.reset(); voxelInstanceArena.used = 0; voxelAoArena.used = 0; ensureUboArena(uboArena, 4096); ensureVertexArena(vtxArena, 4096); // Match the desktop (Vulkan) flow: the 3D/swapchain pass clears with the // background set by setBackgroundColor unless the script called clear(). if (!hasPendingClear) clearColor = backgroundColor; hasPendingClear = false; wgpu::CommandEncoder encoder = device.CreateCommandEncoder(); // Compute compaction writes the visible-model table and indexed-indirect // commands consumed by the scene render pass below. Ending the compute // pass establishes the required WebGPU storage-to-indirect dependency. recordGpuDrivenCompute(encoder); // 1. Shadow passes (CSM cascade layers). if (shadowDepthArray) { // Every cascade is sampled by receivers, including cascades with no // caster draw this frame. Clear all layers while shadows are active so // an empty cascade deterministically means fully lit (depth 1) instead // of retaining undefined or previous-frame depth. if (mesh3dShadows.active) { for (int c = 0; c < ShadowConfig::kCascades; ++c) { WGPUTextureViewDescriptor lvd{}; lvd.format = WGPUTextureFormat_Depth32Float; lvd.dimension = WGPUTextureViewDimension_2D; lvd.baseMipLevel = 0; lvd.mipLevelCount = 1; lvd.baseArrayLayer = static_cast(c); lvd.arrayLayerCount = 1; wgpu::TextureView layerView = shadowDepthArray->texture.CreateView( reinterpret_cast(&lvd)); WGPURenderPassDepthStencilAttachment ds{}; ds.view = layerView.Get(); ds.depthClearValue = 1.f; ds.depthLoadOp = WGPULoadOp_Clear; ds.depthStoreOp = WGPUStoreOp_Store; ds.stencilClearValue = 0; ds.stencilLoadOp = WGPULoadOp_Undefined; ds.stencilStoreOp = WGPUStoreOp_Undefined; WGPURenderPassDescriptor rp{}; rp.depthStencilAttachment = &ds; wgpu::RenderPassEncoder pass = encoder.BeginRenderPass(reinterpret_cast(&rp)); flushShadowPass(pass, c); pass.End(); } } } // Stage-3 visibility pass consumes the compute-compacted instance table // before the scene pass resolves material shading. recordGpuDrivenVisibility(encoder); // 2. Scene color pass (3D) into the offscreen target. wgpu::TextureView sceneView; wgpu::Texture sceneTex; if (frameHad3DThisFrame) { if (active3DCanvas) { // 3D into an offscreen canvas: render the mesh pass into the // canvas color/depth (RGBA8Unorm, matches the mesh3d pipelines). OffscreenCanvas *oc = active3DCanvas; WGPURenderPassColorAttachment colorAtt{}; colorAtt.view = oc->colorView.Get(); colorAtt.depthSlice = WGPU_DEPTH_SLICE_UNDEFINED; colorAtt.loadOp = WGPULoadOp_Clear; colorAtt.storeOp = WGPUStoreOp_Store; colorAtt.clearValue = {clearColor.r, clearColor.g, clearColor.b, 1.f}; WGPURenderPassDepthStencilAttachment ds{}; ds.view = oc->depthView.Get(); ds.depthClearValue = 1.f; ds.depthLoadOp = WGPULoadOp_Clear; ds.depthStoreOp = WGPUStoreOp_Store; ds.stencilClearValue = 0; ds.stencilLoadOp = WGPULoadOp_Undefined; ds.stencilStoreOp = WGPUStoreOp_Undefined; WGPURenderPassDescriptor rp{}; rp.colorAttachmentCount = 1; rp.colorAttachments = &colorAtt; rp.depthStencilAttachment = &ds; wgpu::RenderPassEncoder pass = encoder.BeginRenderPass(reinterpret_cast(&rp)); // Voxel draws are not expected on the canvas path (voxel module is // trimmed from the web build), and its pipeline follows the scene // sample count which would mismatch the 1x canvas attachment. flushMesh3D(pass, WGPUTextureFormat_RGBA8Unorm, /*canvasTarget*/ true); flushGpuDrivenDraws(pass, /*canvasTarget*/ true); pass.End(); oc->clearRequested = false; // The script draws the canvas texture explicitly (2D path); it is // not composited into the swapchain. lastReadbackTex = oc->color; lastReadbackW = oc->getWidth(); lastReadbackH = oc->getHeight(); } else if (!sceneColorSlots.empty()) { SceneColorSlot &slot = sceneColorSlots[currentFrameSlot()]; WGPURenderPassColorAttachment colorAtt{}; colorAtt.view = slot.sampleCount > 1 ? slot.msaaView.Get() : slot.colorView.Get(); colorAtt.resolveTarget = slot.sampleCount > 1 ? slot.colorView.Get() : nullptr; colorAtt.depthSlice = WGPU_DEPTH_SLICE_UNDEFINED; colorAtt.loadOp = WGPULoadOp_Clear; // When resolving to a single-sample target, the multisampled // attachment must be discarded (WebGPU spec). colorAtt.storeOp = slot.sampleCount > 1 ? WGPUStoreOp_Discard : WGPUStoreOp_Store; colorAtt.clearValue = {clearColor.r, clearColor.g, clearColor.b, 1.f}; WGPURenderPassDepthStencilAttachment ds{}; ds.view = slot.depthView.Get(); ds.depthClearValue = 1.f; ds.depthLoadOp = WGPULoadOp_Clear; ds.depthStoreOp = WGPUStoreOp_Store; ds.stencilClearValue = 0; ds.stencilLoadOp = WGPULoadOp_Undefined; ds.stencilStoreOp = WGPUStoreOp_Undefined; WGPURenderPassDescriptor rp{}; rp.colorAttachmentCount = 1; rp.colorAttachments = &colorAtt; rp.depthStencilAttachment = &ds; wgpu::RenderPassEncoder pass = encoder.BeginRenderPass(reinterpret_cast(&rp)); flushVoxelDraws(pass, sceneColorFormat); flushGpuDrivenResolve(pass); flushMesh3D(pass, sceneColorFormat); flushGpuDrivenDraws(pass, /*canvasTarget*/ false); pass.End(); lastPresentSlot = currentFrameSlot(); lastReadbackTex = slot.color; lastReadbackW = sceneColorWidth; lastReadbackH = sceneColorHeight; sceneView = slot.colorView; sceneTex = slot.color; } } // 3. GBuffer pass. if (gbufferPassPending && !gbufferSlots.empty()) { lastGbufferSlot = currentFrameSlot(); GbufferSlot &slot = gbufferSlots[lastGbufferSlot]; WGPURenderPassColorAttachment colorAtts[3]{}; for (int i = 0; i < 3; ++i) { colorAtts[i].depthSlice = WGPU_DEPTH_SLICE_UNDEFINED; colorAtts[i].loadOp = WGPULoadOp_Clear; colorAtts[i].storeOp = WGPUStoreOp_Store; colorAtts[i].clearValue = {0.f, 0.f, 0.f, 1.f}; } colorAtts[0].view = slot.normalView.Get(); colorAtts[1].view = slot.depthColorView.Get(); colorAtts[2].view = slot.albedoView.Get(); WGPURenderPassDepthStencilAttachment ds{}; ds.view = slot.depthView.Get(); ds.depthClearValue = 1.f; ds.depthLoadOp = WGPULoadOp_Clear; ds.depthStoreOp = WGPUStoreOp_Store; ds.stencilClearValue = 0; ds.stencilLoadOp = WGPULoadOp_Undefined; ds.stencilStoreOp = WGPUStoreOp_Undefined; WGPURenderPassDescriptor rp{}; rp.colorAttachmentCount = 3; rp.colorAttachments = colorAtts; rp.depthStencilAttachment = &ds; wgpu::RenderPassEncoder pass = encoder.BeginRenderPass(reinterpret_cast(&rp)); flushGbufferPass(pass); pass.End(); // 3b. SSAO pass: derive a screen-space occlusion texture from the // G-buffer linear depth; the forward mesh pass samples the previous // slot (one frame of latency). if (aoActive) { ensureAOResources(sceneColorWidth, sceneColorHeight); if (aoPipeline && aoTex[0]) { pushValidationScope(); GbufferSlot &gslot = gbufferSlots[currentFrameSlot()]; struct AOUbo { glm::vec4 params; // radius, power, nearZ, farZ float intensity; float invScale; // AO target size / depth size float pad; } aou; // Near/far are passed so the shader can linearize the // hardware depth into world units for the occlusion delta. aou.params = glm::vec4(0.05f, 1.1f, mesh3dNear, mesh3dFar); aou.intensity = 1.0f; aou.invScale = 0.5f; aou.pad = 0.f; queue.WriteBuffer(aoUbo, 0, &aou, sizeof(aou)); wgpu::BindGroup aoBg = makeAOBindGroup(gslot.depthView); WGPURenderPassColorAttachment colorAtt{}; colorAtt.view = aoView[aoWriteIndex].Get(); colorAtt.depthSlice = WGPU_DEPTH_SLICE_UNDEFINED; colorAtt.loadOp = WGPULoadOp_Clear; colorAtt.storeOp = WGPUStoreOp_Store; colorAtt.clearValue = {1.f, 1.f, 1.f, 1.f}; WGPURenderPassDescriptor rp{}; rp.colorAttachmentCount = 1; rp.colorAttachments = &colorAtt; wgpu::RenderPassEncoder apass = encoder.BeginRenderPass(reinterpret_cast(&rp)); apass.SetPipeline(aoPipeline); apass.SetBindGroup(0, aoBg, 0, nullptr); apass.SetVertexBuffer(0, fullscreenQuadVb, 0, 4 * 32); apass.SetIndexBuffer(fullscreenQuadIb, wgpu::IndexFormat::Uint32, 0, 24); apass.DrawIndexed(6, 1, 0, 0, 0); apass.End(); popValidationScope(); aoWriteIndex ^= 1; } } } // 3c. Screen-space decals consume the freshly written GBuffer depth and // world normal, then write three transparent layer attachments. if (decalPassPending && !decalSlots.empty() && !gbufferSlots.empty()) { lastDecalSlot = currentFrameSlot(); DecalSlot &slot = decalSlots[lastDecalSlot]; WGPURenderPassColorAttachment colorAtts[3]{}; for (auto &attachment : colorAtts) { attachment.depthSlice = WGPU_DEPTH_SLICE_UNDEFINED; attachment.loadOp = WGPULoadOp_Clear; attachment.storeOp = WGPUStoreOp_Store; attachment.clearValue = {0.f, 0.f, 0.f, 0.f}; } colorAtts[0].view = slot.albedoView.Get(); colorAtts[1].view = slot.normalView.Get(); colorAtts[2].view = slot.paramsView.Get(); WGPURenderPassDescriptor rp{}; rp.colorAttachmentCount = 3; rp.colorAttachments = colorAtts; wgpu::RenderPassEncoder pass = encoder.BeginRenderPass(reinterpret_cast(&rp)); flushDecalPass(pass); pass.End(); } Texture *taaResolved = nullptr; if (false && sceneView && !sceneColorComposited && renderControl_ && renderControl_->isEnabled("taa")) { if (!fullscreenQuadReady) { float verts[32] = { -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, 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, }; uint32_t indices[6] = {0, 1, 2, 2, 3, 0}; WGPUBufferDescriptor vbd{}; vbd.label = sv("eve_fullscreen_vb"); vbd.size = sizeof(verts); vbd.usage = WGPUBufferUsage_CopyDst | WGPUBufferUsage_Vertex; fullscreenQuadVb = device.CreateBuffer(reinterpret_cast(&vbd)); queue.WriteBuffer(fullscreenQuadVb, 0, verts, sizeof(verts)); WGPUBufferDescriptor ibd{}; ibd.label = sv("eve_fullscreen_ib"); ibd.size = sizeof(indices); ibd.usage = WGPUBufferUsage_CopyDst | WGPUBufferUsage_Index; fullscreenQuadIb = device.CreateBuffer(reinterpret_cast(&ibd)); queue.WriteBuffer(fullscreenQuadIb, 0, indices, sizeof(indices)); fullscreenQuadReady = true; } AntiAliasing *aa = pipelineAntiAliasing(); aa->setMode("taa"); const std::string requested = renderControl_->getPostProcessQuality(); const std::string quality = requested == "ultra" ? "high" : requested; if (aa->getQuality() != quality) aa->setQuality(quality); Texture *motion = renderControl_->getGBuffer()->getVelocityTexture(); auto *writeCanvas = static_cast(aa->beginTemporalFrame(sceneColorTexture, motion)); Texture *history = aa->getTemporalReadTexture(); Shader *taaShader = aa->getTaaShader(); auto *taaGpu = taaShader ? static_cast(taaShader->gpuHandle) : nullptr; if (writeCanvas && taaGpu && taaGpu->hdrOffscreenOpaquePipeline) { GpuTexture sceneGpu; sceneGpu.texture = sceneTex; sceneGpu.view = sceneView; sceneGpu.sampler = createLinearSampler(device); GpuTexture *historyGpu = gpuForTexture(history); GpuTexture *motionGpu = gpuForTexture(motion); ensureUboArena(uboArena, uboArena.used + 256); uint32_t pushOffset = uboArena.alloc(Shader::kPushConstantBytes, 256); queue.WriteBuffer(uboArena.buffer, pushOffset, taaShader->pushConstantData(), Shader::kPushConstantBytes); wgpu::BindGroup bg = makeTex2DBindGroup(&sceneGpu, historyGpu, motionGpu); uint32_t offsets[1] = {pushOffset}; WGPURenderPassColorAttachment colorAtt{}; colorAtt.view = writeCanvas->colorView.Get(); colorAtt.depthSlice = WGPU_DEPTH_SLICE_UNDEFINED; colorAtt.loadOp = WGPULoadOp_Clear; colorAtt.storeOp = WGPUStoreOp_Store; colorAtt.clearValue = {0.f, 0.f, 0.f, 1.f}; WGPURenderPassDescriptor rp{}; rp.colorAttachmentCount = 1; rp.colorAttachments = &colorAtt; wgpu::RenderPassEncoder taaPass = encoder.BeginRenderPass(reinterpret_cast(&rp)); taaPass.SetPipeline(taaGpu->hdrOffscreenOpaquePipeline); taaPass.SetBindGroup(0, bg, 1, offsets); taaPass.SetVertexBuffer(0, fullscreenQuadVb, 0, 4 * 32); taaPass.SetIndexBuffer(fullscreenQuadIb, wgpu::IndexFormat::Uint32, 0, 24); taaPass.DrawIndexed(6, 1, 0, 0, 0); taaPass.End(); taaResolved = writeCanvas->getTexture(); aa->endTemporalFrame(); } } if (sceneView && !sceneColorComposited) { wgpu::CommandBuffer sceneCommand = encoder.Finish(); queue.Submit(1, &sceneCommand); auto savedSolid = std::move(solidBatches); auto savedTextured = std::move(texturedBatches); auto savedLit = std::move(litBatches); auto savedSpans = std::move(overlaySpans); solidBatches.clear(); texturedBatches.clear(); litBatches.clear(); overlaySpans.clear(); Texture *motion = renderControl_ ? renderControl_->getGBuffer()->getVelocityTexture() : nullptr; Texture *resolveSource = takeFinalSceneTexture(); if (!resolveSource) resolveSource = sceneColorTexture; taaResolved = prepareFinalSceneTexture(resolveSource, motion); solidBatches = std::move(savedSolid); texturedBatches = std::move(savedTextured); litBatches = std::move(savedLit); overlaySpans = std::move(savedSpans); encoder = device.CreateCommandEncoder(); } // 4. Active canvas: flush 2D batches into the offscreen target instead. if (activeCanvas) { auto *oc = static_cast(activeCanvas); flush2DToCanvas(oc); } // 5. Swapchain pass: composite scene color, draw 2D, run the overlay. { WGPURenderPassColorAttachment colorAtt{}; colorAtt.view = surfaceView.Get(); colorAtt.depthSlice = WGPU_DEPTH_SLICE_UNDEFINED; colorAtt.loadOp = WGPULoadOp_Clear; colorAtt.storeOp = WGPUStoreOp_Store; colorAtt.clearValue = {clearColor.r, clearColor.g, clearColor.b, 1.f}; WGPURenderPassDescriptor rp{}; rp.colorAttachmentCount = 1; rp.colorAttachments = &colorAtt; wgpu::RenderPassEncoder pass = encoder.BeginRenderPass(reinterpret_cast(&rp)); if (sceneView && !sceneColorComposited) { if (!fullscreenQuadReady) { // Textured vertex: pos(2) + color(4) + uv(2) = 32 bytes. float verts[32] = { -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, 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, }; uint32_t indices[6] = {0, 1, 2, 2, 3, 0}; WGPUBufferDescriptor vbd{}; vbd.label = sv("eve_fullscreen_vb"); vbd.size = sizeof(verts); vbd.usage = WGPUBufferUsage_CopyDst | WGPUBufferUsage_Vertex; fullscreenQuadVb = device.CreateBuffer(reinterpret_cast(&vbd)); queue.WriteBuffer(fullscreenQuadVb, 0, verts, sizeof(verts)); WGPUBufferDescriptor ibd{}; ibd.label = sv("eve_fullscreen_ib"); ibd.size = sizeof(indices); ibd.usage = WGPUBufferUsage_CopyDst | WGPUBufferUsage_Index; fullscreenQuadIb = device.CreateBuffer(reinterpret_cast(&ibd)); queue.WriteBuffer(fullscreenQuadIb, 0, indices, sizeof(indices)); fullscreenQuadReady = true; } GpuTexture sceneGpu; sceneGpu.texture = sceneTex; sceneGpu.view = sceneView; sceneGpu.sampler = createLinearSampler(device); GpuTexture *compositeGpu = gpuForTexture(taaResolved); if (!compositeGpu) { compositeGpu = &sceneGpu; } wgpu::BindGroup bg = makeTex2DBindGroup(compositeGpu, &sceneGpu); uint32_t offsets[1] = {0}; const float autoEnabled = sceneAutoExposure ? 1.f : 0.f; const float minEvQ = std::round(std::clamp(sceneAutoExposureMinEV, -16.f, 15.875f) * 8.f + 128.f); const float maxEvQ = std::round(std::clamp(sceneAutoExposureMaxEV, -16.f, 15.875f) * 8.f + 128.f); const float exposurePack = minEvQ + maxEvQ * 256.f; const float bloomQ = 0.f; // Bloom is precomposited in the linear HDR pyramid. const float thresholdQ = std::round(std::clamp(sceneBloomThreshold, 0.f, 15.9375f) * 16.f); const bool outputIsSrgb = surfaceFormat == WGPUTextureFormat_BGRA8UnormSrgb || surfaceFormat == WGPUTextureFormat_RGBA8UnormSrgb; const float bloomPack = bloomQ + thresholdQ * 256.f + (outputIsSrgb ? 0.f : 65536.f); float toneVerts[32] = { -1.f, -1.f, sceneExposure, autoEnabled, exposurePack, bloomPack, 0.f, 0.f, 1.f, -1.f, sceneExposure, autoEnabled, exposurePack, bloomPack, 1.f, 0.f, 1.f, 1.f, sceneExposure, autoEnabled, exposurePack, bloomPack, 1.f, 1.f, -1.f, 1.f, sceneExposure, autoEnabled, exposurePack, bloomPack, 0.f, 1.f, }; if (!toneMapQuadVb) { WGPUBufferDescriptor bd{}; bd.label = sv("eve_tonemap_vb"); bd.size = sizeof(toneVerts); bd.usage = WGPUBufferUsage_CopyDst | WGPUBufferUsage_Vertex; toneMapQuadVb = device.CreateBuffer( reinterpret_cast(&bd)); } queue.WriteBuffer(toneMapQuadVb, 0, toneVerts, sizeof(toneVerts)); pass.SetPipeline(sceneTonemapPipeline); pass.SetBindGroup(0, bg, 1, offsets); pass.SetVertexBuffer(0, toneMapQuadVb, 0, sizeof(toneVerts)); pass.SetIndexBuffer(fullscreenQuadIb, wgpu::IndexFormat::Uint32, 0, 24); pass.DrawIndexed(6, 1, 0, 0, 0); } if (!activeCanvas) { flush2D(pass, pixelW > 0 ? pixelW : logicalW, pixelH > 0 ? pixelH : logicalH, surfaceFormat); } if (presentOverlayFn_) { WGPURenderPassEncoder cPass = pass.Get(); presentOverlayFn_(presentOverlayUser_, &cPass); } pass.End(); } wgpu::CommandBuffer cmd = encoder.Finish(); queue.Submit(1, &cmd); // emdawnwebgpu implements the GPU on the JS main thread: queued commands // (draws, writes) only execute when ProcessEvents drives the work queue. // Without this, render-pass draws are silently never executed (only the // swapchain clear, handled by the browser present, is visible). #ifdef __EMSCRIPTEN__ if (instance) instance.ProcessEvents(); #endif // emdawnwebgpu does not implement wgpuSurfacePresent (it aborts); the // browser presents the canvas automatically once the command buffer is // submitted from within a requestAnimationFrame callback. On Emscripten // the engine frame is driven by emscripten_set_main_loop (rAF), so the // Squirrel main loop no longer blocks and no sleep is needed here. #if !defined(__EMSCRIPTEN__) surface.Present(); #endif frameIndex++; frame3DStarted = false; frameHad3DThisFrame = false; frameHad3D = false; sceneColorPassOpen = false; active3DCanvas = nullptr; gbufferPassPending = false; } // --------------------------------------------------------------------------- // Canvas // --------------------------------------------------------------------------- Canvas *Graphics::newCanvas(int width, int height) { auto *c = new OffscreenCanvas(this, width, height); ownedCanvases.push_back(std::unique_ptr(c)); return c; } Canvas *Graphics::newHDRCanvas(int width, int height) { auto *c = new OffscreenCanvas(this, width, height, true); ownedCanvases.push_back(std::unique_ptr(c)); return c; } void Graphics::setCanvas(Canvas *canvas) { if (activeCanvas && canvas != activeCanvas) { flush2DToCanvas(static_cast(activeCanvas)); } activeCanvas = canvas; } bool Graphics::isCanvasActive() const { return activeCanvas != nullptr; } Canvas *Graphics::getCanvas() const { return activeCanvas; } void Graphics::flush2DToCanvas(OffscreenCanvas *canvas) { if (!canvas || !canvas->getTexture()) return; auto &uboArena = currentUboArena(); uboArena.reset(); auto &vtxArena = currentVertexArena(); vtxArena.reset(); wgpu::CommandEncoder enc = device.CreateCommandEncoder(); WGPURenderPassColorAttachment ca{}; ca.view = canvas->colorView.Get(); ca.depthSlice = WGPU_DEPTH_SLICE_UNDEFINED; ca.loadOp = canvas->clearRequested ? WGPULoadOp_Clear : WGPULoadOp_Load; ca.storeOp = WGPUStoreOp_Store; ca.clearValue = {canvas->clearColor.r, canvas->clearColor.g, canvas->clearColor.b, canvas->clearColor.a}; WGPURenderPassDescriptor rp{}; rp.colorAttachmentCount = 1; rp.colorAttachments = &ca; wgpu::RenderPassEncoder pass = enc.BeginRenderPass(reinterpret_cast(&rp)); flush2D(pass, canvas->getWidth(), canvas->getHeight(), canvas->isHDR() ? WGPUTextureFormat_RGBA16Float : WGPUTextureFormat_RGBA8Unorm); pass.End(); wgpu::CommandBuffer cmd = enc.Finish(); queue.Submit(1, &cmd); canvas->clearRequested = false; } Texture *Graphics::getTexture() { if (activeCanvas) return activeCanvas->getTexture(); return sceneColorTexture; } void Graphics::draw(eve::graphics::Graphics *gfx, const glm::mat4 &matrix) const { (void)gfx; (void)matrix; } void Graphics::draw(Canvas *C, const glm::mat4 &matrix) const { (void)C; (void)matrix; } void Graphics::clear(std::optional color, std::optional /*stencil*/, std::optional /*depth*/) { if (frameHad3D && activeCanvas == nullptr) return; clearColor = color.value_or(backgroundColor); hasPendingClear = true; clear2DBatches(); if (auto *canvas = dynamic_cast(activeCanvas)) { canvas->clear(clearColor, std::nullopt, std::nullopt); } } Color Graphics::getPixel(int x, int y) { if (activeCanvas) return getPixelImpl(static_cast(activeCanvas), x, y); if (!sceneColorSlots.empty()) { return getPixelImpl(nullptr, x, y); } return clearColor; } image::ImageData *Graphics::newImageData() { if (activeCanvas) return newImageDataImpl(static_cast(activeCanvas)); if (!sceneColorSlots.empty()) return newImageDataImpl(nullptr); return new image::ImageData(1, 1, "RGBA8"); } // --------------------------------------------------------------------------- // Readback // --------------------------------------------------------------------------- namespace { bool copyTextureToCpu(wgpu::Instance &instance, wgpu::Device &device, wgpu::Queue &queue, wgpu::Texture src, int width, int height, std::vector &outRgba, int bytesPerPixel) { if (!src) return false; uint64_t bytesPerRow = static_cast(width) * bytesPerPixel; bytesPerRow = (bytesPerRow + 255) / 256 * 256; // copy alignment 256 uint64_t size = bytesPerRow * height; WGPUBufferDescriptor bd{}; bd.label = sv("eve_readback"); bd.size = size; bd.usage = WGPUBufferUsage_CopyDst | WGPUBufferUsage_MapRead; bd.mappedAtCreation = false; wgpu::Buffer dst = device.CreateBuffer(reinterpret_cast(&bd)); wgpu::CommandEncoder enc = device.CreateCommandEncoder(); WGPUTexelCopyTextureInfo from{}; from.texture = src.Get(); from.mipLevel = 0; from.aspect = WGPUTextureAspect_All; from.origin = {0, 0, 0}; WGPUTexelCopyBufferInfo to{}; to.buffer = dst.Get(); to.layout.offset = 0; to.layout.bytesPerRow = static_cast(bytesPerRow); to.layout.rowsPerImage = static_cast(height); WGPUExtent3D extent{static_cast(width), static_cast(height), 1}; enc.CopyTextureToBuffer(reinterpret_cast(&from), reinterpret_cast(&to), reinterpret_cast(&extent)); wgpu::CommandBuffer cmd = enc.Finish(); queue.Submit(1, &cmd); struct MapState { bool done = false; bool success = false; WGPUMapAsyncStatus status = WGPUMapAsyncStatus_Force32; } map; WGPUBufferMapCallbackInfo cbInfo{}; #if defined(__EMSCRIPTEN__) cbInfo.mode = WGPUCallbackMode_AllowProcessEvents; #else cbInfo.mode = WGPUCallbackMode_WaitAnyOnly; #endif cbInfo.callback = [](WGPUMapAsyncStatus status, WGPUStringView /*message*/, void* userdata1, void* /*userdata2*/) { auto* state = static_cast(userdata1); state->status = status; state->success = status == WGPUMapAsyncStatus_Success; state->done = true; }; cbInfo.userdata1 = ↦ WGPUFuture future = wgpuBufferMapAsync(dst.Get(), WGPUMapMode_Read, 0, size, cbInfo); #if defined(__EMSCRIPTEN__) int guard = 0; while (!map.done && guard < 2000) { emscripten_sleep(0); wgpuInstanceProcessEvents(instance.Get()); ++guard; } #else WGPUFutureWaitInfo waitInfo{}; waitInfo.future = future; (void)wgpuInstanceWaitAny(instance.Get(), 1, &waitInfo, UINT64_MAX); #endif if (!map.success) { std::fprintf(stderr, "[webgpu] texture readback map failed: status=%d done=%d\n", int(map.status), map.done ? 1 : 0); return false; } const uint8_t* data = static_cast(dst.GetConstMappedRange(0, size)); if (!data) return false; const size_t tightRowBytes = static_cast(width) * bytesPerPixel; outRgba.resize(tightRowBytes * height); for (int y = 0; y < height; ++y) std::memcpy(outRgba.data() + size_t(y) * tightRowBytes, data + size_t(y) * bytesPerRow, tightRowBytes); dst.Unmap(); return true; } float decodeHalf(uint16_t value) { const uint32_t sign = uint32_t(value & 0x8000u) << 16u; uint32_t exponent = (value >> 10u) & 0x1fu; uint32_t mantissa = value & 0x03ffu; uint32_t bits = 0; if (exponent == 0) { if (mantissa == 0) { bits = sign; } else { exponent = 1; while ((mantissa & 0x0400u) == 0) { mantissa <<= 1u; --exponent; } mantissa &= 0x03ffu; bits = sign | ((exponent + 112u) << 23u) | (mantissa << 13u); } } else if (exponent == 31u) { bits = sign | 0x7f800000u | (mantissa << 13u); } else { bits = sign | ((exponent + 112u) << 23u) | (mantissa << 13u); } float result = 0.f; std::memcpy(&result, &bits, sizeof(result)); return result; } float hdrToDisplay(float value) { const float linear = std::max(value, 0.f); const float mapped = std::clamp((linear * (2.51f * linear + 0.03f)) / (linear * (2.43f * linear + 0.59f) + 0.14f), 0.f, 1.f); return mapped <= 0.0031308f ? mapped * 12.92f : 1.055f * std::pow(mapped, 1.f / 2.4f) - 0.055f; } Color hdrPixelToColor(const uint8_t *pixel) { uint16_t channels[4]{}; std::memcpy(channels, pixel, sizeof(channels)); return Color(hdrToDisplay(decodeHalf(channels[0])), hdrToDisplay(decodeHalf(channels[1])), hdrToDisplay(decodeHalf(channels[2])), std::clamp(decodeHalf(channels[3]), 0.f, 1.f)); } } // namespace Color Graphics::getPixelImpl(OffscreenCanvas* canvas, int x, int y) { int w = canvas ? canvas->getWidth() : (sceneColorWidth > 0 ? sceneColorWidth : 1); int h = canvas ? canvas->getHeight() : (sceneColorHeight > 0 ? sceneColorHeight : 1); if (x < 0 || y < 0 || x >= w || y >= h) return Color(0.f, 0.f, 0.f, 0.f); std::vector rgba; wgpu::Texture src = canvas ? canvas->color : (sceneColorSlots.empty() ? nullptr : sceneColorSlots[lastPresentSlot].color); const bool hdrScene = canvas == nullptr && sceneColorFormat == WGPUTextureFormat_RGBA16Float; if (!src || !copyTextureToCpu(instance, device, queue, src, w, h, rgba, hdrScene ? 8 : 4)) return clearColor; if (hdrScene) return hdrPixelToColor(rgba.data() + (size_t(y) * w + x) * 8); const uint8_t *p = rgba.data() + (size_t(y) * w + x) * 4; return Color(p[0] / 255.f, p[1] / 255.f, p[2] / 255.f, p[3] / 255.f); } image::ImageData *Graphics::newImageDataImpl(OffscreenCanvas *canvas) { int w = canvas ? canvas->getWidth() : (sceneColorWidth > 0 ? sceneColorWidth : 1); int h = canvas ? canvas->getHeight() : (sceneColorHeight > 0 ? sceneColorHeight : 1); auto *img = new image::ImageData(w, h, "RGBA8"); std::vector rgba; wgpu::Texture src = canvas ? canvas->color : (sceneColorSlots.empty() ? nullptr : sceneColorSlots[lastPresentSlot].color); const bool hdrScene = canvas == nullptr && sceneColorFormat == WGPUTextureFormat_RGBA16Float; if (src && copyTextureToCpu(instance, device, queue, src, w, h, rgba, hdrScene ? 8 : 4)) { if (!hdrScene) { std::memcpy(img->getData(), rgba.data(), rgba.size()); } else { auto *dst = static_cast(img->getData()); for (size_t pixel = 0; pixel < size_t(w) * h; ++pixel) { const Color color = hdrPixelToColor(rgba.data() + pixel * 8); dst[pixel * 4 + 0] = uint8_t(std::clamp(color.r * 255.f, 0.f, 255.f)); dst[pixel * 4 + 1] = uint8_t(std::clamp(color.g * 255.f, 0.f, 255.f)); dst[pixel * 4 + 2] = uint8_t(std::clamp(color.b * 255.f, 0.f, 255.f)); dst[pixel * 4 + 3] = uint8_t(std::clamp(color.a * 255.f, 0.f, 255.f)); } } } return img; } image::ImageData *Graphics::newHDRImageDataImpl(OffscreenCanvas *canvas) { if (!canvas || !canvas->hdr) return nullptr; const int w = canvas->getWidth(); const int h = canvas->getHeight(); std::vector rgba16f; if (!copyTextureToCpu(instance, device, queue, canvas->color, w, h, rgba16f, 8)) return nullptr; auto *img = new image::ImageData(w, h, "RGBA16F"); std::memcpy(img->getData(), rgba16f.data(), rgba16f.size()); return img; } image::ImageData *Graphics::readGBufferToImageData(const std::string &attachment) { submitPendingDeferredPasses(); if (!device || gbufferSlots.empty() || gbufferWidth <= 0 || gbufferHeight <= 0) return nullptr; GbufferSlot &slot = gbufferSlots[std::min(lastGbufferSlot, gbufferSlots.size() - 1)]; wgpu::Texture src; if (attachment == "depth") src = slot.depthColor; else if (attachment == "normal") src = slot.normal; else if (attachment == "albedo") src = slot.albedo; else return nullptr; std::vector rgba; if (!copyTextureToCpu(instance, device, queue, src, gbufferWidth, gbufferHeight, rgba)) return nullptr; auto *image = new image::ImageData(gbufferWidth, gbufferHeight, "RGBA8"); std::memcpy(image->getData(), rgba.data(), rgba.size()); return image; } image::ImageData *Graphics::readDecalLayerToImageData(const std::string &attachment) { submitPendingDeferredPasses(); if (!device || decalSlots.empty() || decalWidth <= 0 || decalHeight <= 0) return nullptr; DecalSlot &slot = decalSlots[std::min(lastDecalSlot, decalSlots.size() - 1)]; wgpu::Texture src; if (attachment == "albedo") src = slot.albedo; else if (attachment == "normal") src = slot.normal; else if (attachment == "params") src = slot.params; else return nullptr; std::vector rgba; if (!copyTextureToCpu(instance, device, queue, src, decalWidth, decalHeight, rgba)) return nullptr; auto *image = new image::ImageData(decalWidth, decalHeight, "RGBA8"); std::memcpy(image->getData(), rgba.data(), rgba.size()); return image; } // --------------------------------------------------------------------------- // Async frame readback (browser) // --------------------------------------------------------------------------- namespace { bool encodeRgbaToPng(const std::string &path, int width, int height, const std::vector &rgba) { try { image::ImageData img(width, height, "RGBA8"); std::memcpy(img.getData(), rgba.data(), rgba.size()); std::unique_ptr png( img.encode(medialoader::FormatHandler::ENCODED_PNG, path.c_str(), false)); if (!png) return false; std::ofstream out(path, std::ios::binary); if (!out) return false; out.write(static_cast(png->getData()), static_cast(png->getSize())); return out.good(); } catch (...) { return false; } } } // namespace bool Graphics::beginFrameReadback(const std::string &path) { // A finished readback can be replaced; only refuse while one is in flight. if ((pendingReadback_ && !pendingReadback_->done) || !device) return false; wgpu::Texture src = lastReadbackTex; int w = lastReadbackW; int h = lastReadbackH; if (!src || w <= 0 || h <= 0) { // Fall back to the scene color slot before anything was rendered. if (sceneColorSlots.empty()) return false; const uint32_t slot = lastPresentSlot; if (slot >= sceneColorSlots.size()) return false; src = sceneColorSlots[slot].color; w = sceneColorWidth; h = sceneColorHeight; if (!src || w <= 0 || h <= 0) return false; } uint64_t bytesPerRow = static_cast(w * 4); bytesPerRow = (bytesPerRow + 255) / 256 * 256; const uint64_t size = bytesPerRow * static_cast(h); WGPUBufferDescriptor bd{}; bd.label = sv("eve_readback"); bd.size = size; bd.usage = WGPUBufferUsage_CopyDst | WGPUBufferUsage_MapRead; bd.mappedAtCreation = false; wgpu::Buffer dst = device.CreateBuffer(reinterpret_cast(&bd)); if (!dst) return false; // Copy the resolved scene color into the readback buffer, then map // asynchronously (two-phase: the pump waits for the map callback on the // browser event loop without ASYNCIFY sleeps). wgpu::CommandEncoder enc = device.CreateCommandEncoder(); WGPUTexelCopyTextureInfo from{}; from.texture = src.Get(); from.mipLevel = 0; from.aspect = WGPUTextureAspect_All; from.origin = {0, 0, 0}; WGPUTexelCopyBufferInfo to{}; to.buffer = dst.Get(); to.layout.offset = 0; to.layout.bytesPerRow = static_cast(bytesPerRow); to.layout.rowsPerImage = static_cast(h); WGPUExtent3D extent{static_cast(w), static_cast(h), 1}; enc.CopyTextureToBuffer(reinterpret_cast(&from), reinterpret_cast(&to), reinterpret_cast(&extent)); wgpu::CommandBuffer cmd = enc.Finish(); queue.Submit(1, &cmd); auto pr = std::make_unique(); pr->path = path; pr->width = w; pr->height = h; pr->bytesPerRow = bytesPerRow; pr->dst = dst; WGPUBufferMapCallbackInfo cbInfo{}; cbInfo.mode = WGPUCallbackMode_AllowProcessEvents; cbInfo.callback = [](WGPUMapAsyncStatus status, WGPUStringView /*message*/, void *userdata1, void * /*userdata2*/) { auto *p = static_cast(userdata1); p->mapped = (status == WGPUMapAsyncStatus_Success); }; cbInfo.userdata1 = pr.get(); wgpuBufferMapAsync(dst.Get(), WGPUMapMode_Read, 0, size, cbInfo); pendingReadback_ = std::move(pr); return true; } int Graphics::frameReadbackStatus() const { if (!pendingReadback_) return 0; if (!pendingReadback_->done) return 1; return pendingReadback_->ok ? 2 : 3; } void Graphics::pumpReadback() { if (!pendingReadback_ || pendingReadback_->done) return; auto &pr = *pendingReadback_; if (!pr.mapped) { // The map callback is delivered on the browser event loop between // frames (emdawnwebgpu callUserCallback), so no ASYNCIFY sleep is // needed here �?this runs from present() on the main loop. #if defined(__EMSCRIPTEN__) wgpuInstanceProcessEvents(instance.Get()); #endif return; } const uint8_t *data = static_cast( pr.dst.GetConstMappedRange(0, pr.bytesPerRow * static_cast(pr.height))); std::vector rgba(static_cast(pr.width) * pr.height * 4); if (data) { for (int y = 0; y < pr.height; ++y) std::memcpy(rgba.data() + size_t(y) * pr.width * 4, data + size_t(y) * pr.bytesPerRow, size_t(pr.width) * 4); } pr.dst.Unmap(); pr.ok = data && encodeRgbaToPng(pr.path, pr.width, pr.height, rgba); pr.done = true; } // --------------------------------------------------------------------------- // Shader creation // --------------------------------------------------------------------------- namespace { wgpu::RenderPipeline buildPipelineFromWgsl(wgpu::Device &dev, wgpu::PipelineLayout layout, WGPUTextureFormat format, const std::string &vert, const std::string &frag, bool depth, bool blend, bool mesh3d, bool hair, bool shadow, bool gbuffer, uint32_t sampleCount) { WGPURenderPipelineDescriptor pd{}; pd.label = sv("eve_custom_shader"); pd.layout = layout.Get(); WGPUVertexAttribute attrs[5]{}; WGPUVertexBufferLayout vb{}; if (mesh3d || shadow || gbuffer) { fillMeshAttributes(attrs); attrs[0].format = WGPUVertexFormat_Float32x3; attrs[0].offset = 0; attrs[0].shaderLocation = 0; attrs[1].format = WGPUVertexFormat_Float32x3; attrs[1].offset = 12; attrs[1].shaderLocation = 1; attrs[2].format = WGPUVertexFormat_Float32x2; attrs[2].offset = 24; attrs[2].shaderLocation = 2; vb.arrayStride = sizeof(MeshVertex); vb.stepMode = WGPUVertexStepMode_Vertex; vb.attributeCount = 5; vb.attributes = attrs; } else { attrs[0].format = WGPUVertexFormat_Float32x2; attrs[0].offset = 0; attrs[0].shaderLocation = 0; attrs[1].format = WGPUVertexFormat_Float32x4; attrs[1].offset = 8; attrs[1].shaderLocation = 1; attrs[2].format = WGPUVertexFormat_Float32x2; attrs[2].offset = 24; attrs[2].shaderLocation = 2; vb.arrayStride = 32; vb.stepMode = WGPUVertexStepMode_Vertex; vb.attributeCount = 3; vb.attributes = attrs; } pd.vertex.bufferCount = 1; pd.vertex.buffers = &vb; // Keep both modules alive until CreateRenderPipeline has consumed the // descriptor. Storing only their raw handles in pd and destroying the // wrappers at the end of these branches leaves dangling handles for Dawn. wgpu::ShaderModule vertexModule; wgpu::ShaderModule fragmentModule; if (!vert.empty()) { WGPUShaderModuleDescriptor md = mdDesc(vert); vertexModule = dev.CreateShaderModule( reinterpret_cast(&md)); pd.vertex.module = vertexModule.Get(); pd.vertex.entryPoint = sv("vs_main"); } else { // Default textured vertex shader. WGPUShaderModuleDescriptor md = mdDesc(kTexturedVertWgsl); vertexModule = dev.CreateShaderModule( reinterpret_cast(&md)); pd.vertex.module = vertexModule.Get(); pd.vertex.entryPoint = sv("vs_main"); } if (!frag.empty()) { WGPUFragmentState fs{}; WGPUShaderModuleDescriptor md = mdDesc(frag); fragmentModule = dev.CreateShaderModule( reinterpret_cast(&md)); fs.module = fragmentModule.Get(); fs.entryPoint = sv("fs_main"); fs.targetCount = 1; WGPUColorTargetState target{}; target.format = format; target.writeMask = WGPUColorWriteMask_All; if (blend) { static WGPUBlendState bs = alphaBlend(); target.blend = &bs; } fs.targets = ⌖ pd.fragment = &fs; } else { pd.fragment = nullptr; } pd.primitive.topology = WGPUPrimitiveTopology_TriangleList; pd.primitive.frontFace = WGPUFrontFace_CCW; pd.primitive.cullMode = hair ? WGPUCullMode_None : WGPUCullMode_None; pd.primitive.stripIndexFormat = WGPUIndexFormat_Undefined; if (depth && !shadow) { static WGPUDepthStencilState ds{}; ds.format = WGPUTextureFormat_Depth32Float; ds.depthWriteEnabled = WGPUOptionalBool_True; ds.depthCompare = WGPUCompareFunction_Less; pd.depthStencil = &ds; } pd.multisample.count = sampleCount ? sampleCount : 1; // mask=0 (zero-init) silently discards all fragments; use all-samples. pd.multisample.mask = 0xFFFFFFFFu; return dev.CreateRenderPipeline(reinterpret_cast(&pd)); } } // namespace Shader *Graphics::newShaderFromSpv(const std::vector &vertSpv, const std::vector &fragSpv) { if (!device) throw Exception("newShaderFromSpv: device not initialized"); // Native Dawn can consume Vulkan SPIR-V directly; the browser build cannot // (browsers only accept WGSL). The WebGPU backend therefore requires WGSL // for custom shaders; SPIR-V input is rejected with a clear message. throw Exception("newShaderFromSpv: SPIR-V custom shaders are not supported on the " "WebGPU backend (browsers accept WGSL only). Recompile the shader " "with the WebGPU toolchain (glslc+tint) and use newShaderFromWgsl."); } Shader *Graphics::newShaderFromSpvFile(const std::string &vertPath, const std::string &fragPath) { throw Exception("newShaderFromSpvFile: SPIR-V custom shaders are not supported on the " "WebGPU backend. Use WGSL shaders instead."); } bool Graphics::releaseShader(Shader *shader) { if (!shader || !shader->gpuHandle) return false; auto *gpu = static_cast(shader->gpuHandle); auto gpuIt = std::find_if(ownedGpuShaders.begin(), ownedGpuShaders.end(), [&](const std::unique_ptr &g) { return g.get() == gpu; }); if (gpuIt == ownedGpuShaders.end()) return false; auto shIt = std::find_if(ownedShaders.begin(), ownedShaders.end(), [&](const std::unique_ptr &s) { return s.get() == shader; }); if (shIt == ownedShaders.end()) return false; shader->gpuHandle = nullptr; ownedGpuShaders.erase(gpuIt); // Transfer the CPU facade to the caller instead of destroying it. (void)shIt->release(); ownedShaders.erase(shIt); return true; } Shader *Graphics::newShader(const std::string &vertGlsl, const std::string &fragGlsl) { (void)vertGlsl; (void)fragGlsl; throw Exception("newShader: runtime GLSL compilation is not available on the WebGPU " "backend (browser WGSL only). Ship pre-compiled WGSL shaders."); } Shader *Graphics::newMeshShaderFromSpv(const std::vector &vertSpv, const std::vector &fragSpv) { (void)vertSpv; (void)fragSpv; throw Exception("newMeshShaderFromSpv: SPIR-V custom mesh shaders are not supported on the " "WebGPU backend. Use WGSL shaders instead."); } Shader *Graphics::newMeshShaderFromWgsl(const std::string &vertWgsl, const std::string &fragWgsl) { if (!device) throw Exception("newMeshShaderFromWgsl: device not initialized"); if (fragWgsl.empty()) throw Exception("newMeshShaderFromWgsl: empty fragment WGSL"); if (!mesh3dSetLayout) throw Exception("newMeshShaderFromWgsl: mesh3d layout missing"); std::string vert = vertWgsl.empty() ? kMesh3DVertWgsl : vertWgsl; auto gpu = std::make_unique(); gpu->isMesh3D = true; gpu->wgslVert = vert; gpu->wgslFrag = fragWgsl; gpu->mesh3dPipeline = buildPipelineFromWgsl(device, mesh3dPipelineLayout, sceneColorFormat, vert, fragWgsl, /*depth*/ true, /*blend*/ false, /*mesh3d*/ true, /*hair*/ false, /*shadow*/ false, /*gbuffer*/ false, /*sampleCount*/ sceneColorSamples); // X-ray variant: depth test/write off + alpha blend so occluded silhouettes // paint over the building (the shader discards visible fragments itself). gpu->mesh3dXrayPipeline = buildPipelineFromWgsl(device, mesh3dPipelineLayout, sceneColorFormat, vert, fragWgsl, /*depth*/ false, /*blend*/ true, /*mesh3d*/ true, /*hair*/ false, /*shadow*/ false, /*gbuffer*/ false, /*sampleCount*/ sceneColorSamples); auto sh = std::make_unique(); sh->setKind(Shader::Kind::eMesh3D); sh->gpuHandle = gpu.get(); Shader *raw = sh.get(); ownedShaders.push_back(std::move(sh)); ownedGpuShaders.push_back(std::move(gpu)); return raw; } Shader *Graphics::newMeshShader(const std::string &vertGlsl, const std::string &fragGlsl) { (void)vertGlsl; (void)fragGlsl; throw Exception("newMeshShader: runtime GLSL compilation is not available on the WebGPU " "backend. Ship pre-compiled WGSL shaders."); } Shader *Graphics::newShaderFromWgsl(const std::string &vertWgsl, const std::string &fragWgsl) { if (!device) throw Exception("newShaderFromWgsl: device not initialized"); if (fragWgsl.empty()) throw Exception("newShaderFromWgsl: empty fragment WGSL"); if (!tex2DPipelineLayout) throw Exception("newShaderFromWgsl: 2D pipeline layout missing"); std::string vert = vertWgsl.empty() ? kTexturedVertWgsl : vertWgsl; auto gpu = std::make_unique(); gpu->isMesh3D = false; gpu->wgslVert = vert; gpu->wgslFrag = fragWgsl; // 2D custom shader: alpha blend, no depth; one pipeline per target format // (swapchain surface vs RGBA8Unorm offscreen canvas). gpu->swapchainPipeline = createPipelineForShader(gpu.get(), wgpu::TextureFormat(surfaceFormat), /*depth*/ false, /*mesh3d*/ false, /*hair*/ false, /*shadow*/ false, /*gbuffer*/ false, tex2DPipelineLayout); gpu->offscreenPipeline = createPipelineForShader(gpu.get(), wgpu::TextureFormat::RGBA8Unorm, /*depth*/ false, /*mesh3d*/ false, /*hair*/ false, /*shadow*/ false, /*gbuffer*/ false, tex2DPipelineLayout); gpu->swapchainOpaquePipeline = buildPipelineFromWgsl( device, tex2DPipelineLayout, WGPUTextureFormat(surfaceFormat), vert, fragWgsl, false, false, false, false, false, false, 1u); gpu->offscreenOpaquePipeline = buildPipelineFromWgsl( device, tex2DPipelineLayout, WGPUTextureFormat_RGBA8Unorm, vert, fragWgsl, false, false, false, false, false, false, 1u); gpu->hdrOffscreenPipeline = createPipelineForShader( gpu.get(), wgpu::TextureFormat::RGBA16Float, false, false, false, false, false, tex2DPipelineLayout); gpu->hdrOffscreenOpaquePipeline = buildPipelineFromWgsl( device, tex2DPipelineLayout, WGPUTextureFormat_RGBA16Float, vert, fragWgsl, false, false, false, false, false, false, 1u); auto sh = std::make_unique(); sh->setKind(Shader::Kind::eSprite2D); sh->gpuHandle = gpu.get(); Shader *raw = sh.get(); ownedShaders.push_back(std::move(sh)); ownedGpuShaders.push_back(std::move(gpu)); return raw; } wgpu::RenderPipeline Graphics::createPipelineForShader(GpuShader *gs, wgpu::TextureFormat format, bool depth, bool mesh3d, bool hair, bool shadow, bool gbuffer, wgpu::PipelineLayout layout) { if (!gs || gs->wgslFrag.empty()) return {}; // Custom 2D/mesh shaders blend only when the caller asks (x-ray / 2D UI); // opaque meshes and shadow/gbuffer variants keep write-through targets. const bool blend = !depth; const uint32_t samples = mesh3d || hair || shadow || gbuffer ? sceneColorSamples : 1u; return buildPipelineFromWgsl(device, layout, WGPUTextureFormat(format), gs->wgslVert, gs->wgslFrag, depth, blend, mesh3d, hair, shadow, gbuffer, samples); } Shader *Graphics::newHairShaderFromSpv(const std::vector &vertSpv, const std::vector &fragSpv) { (void)vertSpv; (void)fragSpv; throw Exception("newHairShaderFromSpv: SPIR-V hair shaders are not supported on WebGPU; " "use newHairShaderFromWgsl"); } Shader *Graphics::newHairShaderFromWgsl(const std::string &vertWgsl, const std::string &fragWgsl) { if (!device) throw Exception("newHairShaderFromWgsl: device not initialized"); if (fragWgsl.empty()) throw Exception("newHairShaderFromWgsl: empty fragment WGSL"); auto gpu = std::make_unique(); gpu->isMesh3D = true; gpu->isHair3D = true; gpu->wgslVert = vertWgsl.empty() ? kMesh3DVertWgsl : vertWgsl; gpu->wgslFrag = fragWgsl; gpu->mesh3dPipeline = buildPipelineFromWgsl( device, mesh3dPipelineLayout, sceneColorFormat, gpu->wgslVert, gpu->wgslFrag, true, true, true, true, false, false, sceneColorSamples); auto shader = std::make_unique(); shader->setKind(Shader::Kind::eMesh3D); shader->gpuHandle = gpu.get(); Shader *raw = shader.get(); ownedShaders.push_back(std::move(shader)); ownedGpuShaders.push_back(std::move(gpu)); return raw; } } // namespace eve::graphics::webgpu