Add a systematic C++ wrapper layer for the WebGPU backend, mirroring the
vulkan backend's vkbuilder patterns so invalid API usage is rejected at
compile time / type level instead of via scattered runtime flags.
New layer (src/modules/graphics/webgpu/):
- wgpu_types.h: cross-platform webgpu_cpp.h include + sv() helpers
- Capabilities: read-only snapshot of adapter/device limits (GetLimits),
sample-count/format/bind-group predicates
- InitFlow: typed Instance->Adapter->Device state machine wrapping the async
request + ProcessEvents busy-wait (skipping a step is a compile error)
- SamplerBuilder/BindGroupLayoutBuilder/PipelineBuilder: build the C++ wrapper
descriptors directly (zero-init == WebGPU defaults), eliminating the
writeMask=0 / sampleMask=0 / alphaMode default-mask traps
Graphics.cpp migration:
- initHeadless/initWithWindow now drive InitFlow; drop the 4 std::atomic<bool>
flags, waitForAdapter/Device, createInstanceAndAdapter/requestDevice
- convert 8 bind-group-layout, 7 pipeline-layout, 3 2D-pipeline functions and
makeWgslModule to build C++ wrapper descriptors directly (reinterpret_cast
127 -> 114; remaining texture/buffer/renderpass conversions deferred)
Design spec: docs/dev/superpowers/specs/2026-08-26-webgpu-vkbuilder-style-cpp-layer.md
Verified: build/webgpu-native (native Dawn, MSVC Debug+Ninja) compiles and
links eve.exe (1633/1633). SSAO/decal stub-tightening (PR4) was dropped as
obsolete - dev already has real implementations.
AnimSkin can now write CPU-skinning results back to the render mesh so skeletal animation actually plays from scripts:
- capture bind normals at fromModel and skin them per pose (updateSkinnedNormals / getSkinnedNormals, cofactor inverse-transpose + normalize)
- packed getSkinnedPositions() array getter, matching AnimLattice's script pattern
- one-call applyToMesh(gfx, mesh, pose): skin positions+normals then in-place gpu.updateMeshVertices
- Renderable3D.getMesh() script binding so createRenderable meshes are reachable
- Mesh::gpuVertexCount so getVertexCount() is correct for non-morph meshes (both backends)
- tests: packed positions == skinPositionsTo, normals follow pose and stay unit length, applyToMesh render test with in-place updates
- docs: animation/graphics user guides + design spec
WebGPU NDC is Y-up while the engine feeds Vulkan-convention (Y-down) clip
matrices, so every 3D pass (mesh, clustered, gbuffer, shadow, voxel) rendered
vertically flipped - the floor appeared on top and lighting/AO landed in the
wrong places. Mirror clip Y in the vertex shaders.
The default env cubemap was pure white while RenderSystem3D passes
envIntensity=1.0 by default, washing the whole scene toward white; make the
default black so only an explicitly set envMap contributes IBL.
Also fix the half-res AO UV mapping (was overrunning into [0,2] and clamping
to the AO edge) and add the missing Vulkan setMesh3DSSAO override so the
native desktop build compiles.
begin3DFrameToCanvas no longer throws: the 3D pass renders into the target
canvas (RGBA8Unorm, 1-sample) using a dedicated 1x mesh3d pipeline variant,
since the scene pipelines follow the active MSAA count. renderScene3DToCanvas
also gets validation scopes matching render3D. The async frame readback now
follows the last 3D target (scene color or canvas) so screenshots work for
canvas-rendered frames too.
Known limitation: reading back a canvas that was also drawn as a 2D overlay in
the same frame can return black (emdawnwebgpu resource-state quirk).
The scene color pass now renders into a multisampled attachment and resolves
to the single-sample color texture used by compositing and frame readback.
The mesh3d/voxel/custom-shader pipelines follow the active sample count and
are rebuilt when setMsaaSamples changes; WebGPU supports 1x/4x only, so values
>= 2 clamp to 4. setMsaaSamples also recreates the offscreen targets.