Skip to content

[WebGPU] Bind the lighting volume shadow map as texture_depth_2d in WGSL - #6

Open
matthargett wants to merge 82 commits into
masterfrom
claude/jolly-allen-k9ldki
Open

[WebGPU] Bind the lighting volume shadow map as texture_depth_2d in WGSL#6
matthargett wants to merge 82 commits into
masterfrom
claude/jolly-allen-k9ldki

Conversation

@matthargett

@matthargett matthargett commented Jun 11, 2026

Copy link
Copy Markdown
Member

Fork staging PR for the upstream submission (upstream compare link).

Summary

  • Declare the shadowMap binding of lightingVolume.compute.fx as texture_depth_2d instead of texture_2d<f32>, and drop the .r swizzle since textureLoad on texture_depth_2d returns a scalar f32.

Why

LightingVolume binds the shadow generator's shadow map depthStencilTexture — a depth-format texture (TEXTUREFORMAT_DEPTH32_FLOAT) — as the shadowMap input of this compute shader (lightingVolume.pure.ts).

This completes the compute depth sample-type support introduced in BabylonJS#18460: _GetComputeTextureSampleType classifies that texture as bind group layout sampleType "depth", and WebGPU pipeline validation requires a texture_depth_2d WGSL declaration to match a "depth" layout entry. With the current texture_2d<f32> declaration, createComputePipeline fails validation (observed in Chromium and wgpu-native) as soon as the lighting volume compute shaders run with an explicit pipeline layout.

Under the default auto layout the current declaration only works by accident: texture_2d<f32> used with textureLoad derives sampleType "unfilterable-float", which a depth-aspect view happens to be compatible with. The fix is the semantically correct binding for a depth texture and is equally valid under the auto layout (texture_depth_2d derives sampleType "depth"), and the loaded scalar is the same depth value .r previously carried, so far-plane fitting results are unchanged.

Repro

Playground (run with the WebGPU engine): https://playground.babylonjs.com/#JE6IH3

It dispatches the lightingVolume far-plane kernel against a shadow generator's depthStencilTexture four ways (shipped/fixed WGSL x auto/explicit layout). On Babylon.js 9.12.0 + Chrome Canary, the shipped texture_2d<f32> declaration under the explicit pipeline layout fails createComputePipeline with:

The shader's texture sample type (TextureSampleType::4294967294) isn't compatible with the layout's texture sample type (TextureSampleType::Depth) (it is only compatible with TextureSampleType::Depth for the shader texture sample type).
 - While validating that the entry-point's declaration for @group(0) @binding(0) matches [BindGroupLayoutInternal (unlabeled)]
 - While validating the entry-point's compatibility for group 0 with [BindGroupLayoutInternal (unlabeled)]
 - While validating compute stage ([ShaderModule (unlabeled)], entryPoint: "updateFarPlaneVertices").
 - While calling [Device "BabylonWebGPUDevice5"].CreateComputePipeline([ComputePipelineDescriptor]).

With texture_depth_2d, the pipeline builds under both explicit and auto layouts, and the storage-buffer readback is bit-identical to the previous .r path (the playground compares the two outputs element by element), verifying far-plane fitting depths are unchanged. The snippet inspects the running build's shader store and self-reports once a build ships the fix, so it can be re-run post-merge as confirmation.

Device context

This came from the Hill Valley GLTF/NativeXR AR Portal validation pass.

Validation

  • Playground repro above on Babylon.js 9.12.0 + Chrome Canary:
    • shipped WGSL + auto layout: pipeline + dispatch succeed (current behavior preserved)
    • shipped WGSL + explicit layout: createComputePipeline validation error (the bug)
    • fixed WGSL + explicit layout: pipeline + dispatch succeed
    • fixed WGSL + auto layout: pipeline + dispatch succeed
    • depth readback of the fixed shader is bit-identical to the texture_2d<f32>.r output, with real geometry depths present (min depth < 1.0)
  • Shader-only change: the generated lightingVolume.compute.ts is produced at build time, and .prettierignore excludes ShadersWGSL, so no other source files change.

Babylon.js Platform and others added 30 commits June 11, 2026 07:01
## Scissor as an opt-in engine extension

Moves the scissor (`enableScissor` / `disableScissor`) functionality
into `@babylonjs/core` so downstream consumers can stop maintaining
their own copies and import it instead.

### Design
- **Type info on `AbstractEngine`** —
`Engines/AbstractEngine/abstractEngine.scissor.ts` is a type-only
`declare module` augmentation, so any caller holding an `AbstractEngine`
can be typed to call `enableScissor` / `disableScissor` (opt-in by
importing it).
- **WebGL impl is an opt-in extension** — the WebGL implementation no
longer lives on the `Engine` class. It moved into a new
`Engines/thinEngine.scissor.(pure.)ts` extension that augments
`ThinEngine.prototype`. `engine.ts` imports it, so `Engine` instances
keep scissor at runtime (fully backward compatible), while the bare
`ThinEngine` class stays scissor-free unless the extension is explicitly
imported.
- **`ThinNativeEngine` and `WebGPUEngine`** keep their own concrete
implementations (native command encoder / WebGPU cached scissor); they
`override` the `AbstractEngine` type member and need no import.

### Why
- `ThinEngine` does not get scissor by default (it doesn't need it).

### Notes
- Tree-shaking artifacts regenerated (side-effects manifest +
`package.json` `sideEffects` + pure barrels).
- Verified: core `tsc --noEmit`, `npm run lint:check` (incl.
tree-shaking + side-effects sync), Prettier; `npm run test:unit`
(pre-existing unrelated `nmeParse` teardown flake only).

---------

Co-authored-by: Amoebachant <kevbrown@microsoft.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
> 🤖 *This PR was created by the create-pr skill.*

## Summary
- Add a built-in SDK-free FBX loader with binary/ASCII parsing, scene
interpretation, meshes, materials, textures, skeletons, animation
groups, cameras, and lights.
- Register and export the FBX loader through the loaders package,
including dynamic loader registration and SceneLoader plugin options.
- Add focused unit coverage for FBX parsing/interpreter behavior, loader
registration, material texture handling, normal-map coordinate options,
embedded texture loading, asset-container ownership, camera/light
orientation, axis/unit GlobalSettings handling, morph-target unit
scaling, and multi-clip animation.
- Add a visual regression suite (18 scenes) covering the loader feature
surface as Playground snippets, backed by a dedicated FBX model set in
the Assets repo, with committed WebGL2/WebGPU reference images.

## Notes
- FBX normal-map slots default to Y-up tangent-space convention, with an
opt-in Y-down loader option.
- FBX `Bump` and `BumpFactor` slots are treated as normal-map-like
inputs for compatibility until true grayscale height-to-normal
conversion is implemented.
- Embedded FBX textures use Babylon's delayed texture buffer path;
sidecar textures remain supported when no embedded bytes are present.
- FBX cameras look down local +X and lights down local -Z; the loader
derives each camera/light world position and aim from its node world
matrix (point transforms) so orientation is correct after the
left-handed conversion.
- `UnitScaleFactor` is treated as metadata only (it is not applied to
base geometry or morph-target deltas), so morphs stay consistent with
the unscaled base.

## Visual tests
- Entries live in `packages/tools/tests/test/visualization/config.json`
(the `FBX loader - ...` titles), one per model in
`meshes/fbx/loaderTests/`.
- Each test is a Playground snippet (`playgroundId`
`#DZBTQU#0`–`#DZBTQU#17`) with an async `createScene` that loads its
model from the Assets CDN via `BABYLON.AppendSceneAsync`, renders
single-sided to match Maya, frames it with a fixed orbit (or the
FBX-authored camera for the cameras/lights scene), and pins any
animation to a deterministic frame. Each entry carries per-feature
`dependsOn` tags (`Bones`, `Morph`, `Animations`, `Cameras`, `Lights`,
`Materials`, `Textures`, `Meshes`).
- The models, sidecar texture, and scene assets are in
BabylonJS/Assets#149 (merged, live on the CDN).
- Reference images for all 18 tests are committed under
`packages/tools/tests/test/visualization/ReferenceImages/` and the tests
are active (no longer `excludeFromAutomaticTesting`). All 18 pass
locally on both WebGL2 and WebGPU against a fresh build.

## Validation
- `npm run compile -w @dev/loaders`
- `npm run test -- packages/dev/loaders/test/unit` (FBX: 79 unit tests
passing)
- `npm run format:check`
- `npx playwright test --config playwright.config.ts --project=webgl2 -g
"FBX loader"` and `--project=webgpu` (18/18 FBX visual tests passing)

`npm run lint:check` currently fails on pre-existing core tree-shaking
manifest/side-effect-stub drift outside the FBX changes.

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: David Catuhe <david.catuhe@live.fr>
…#18574)

## What

Raise the `timeoutInMinutes` of the **Native tests (experimental)** job
in `.azure-pipelines/ci-monorepo.yml` from **15** to **35**.

## Why

The job downloads the prebuilt BabylonNative nightly Playground and runs
the full `validation_native.js` visualization suite (~290 active tests).
On the CI agent each test runs roughly **~3x slower** than local
hardware, so the suite needs about **14–16 minutes** of wall time plus
~1.5 min of script-load/startup.

With the old 15-minute budget, even a **clean, crash-free run** was
cancelled mid-suite. In a recent run the suite reached **~285 of ~290
tests** (it was rendering "Particles - Helper - Sun") before the
15-minute timeout terminated the batch job — only a handful of tests
short of finishing.

A use-after-free crash in the native texture upload path (fixed upstream
in BabylonNative) previously masked this: the crash forced a full-suite
relaunch that always blew the budget. Now that the crash is gone, the
suite nearly fits — it just needs a slightly larger budget.

Raising the timeout to **35 minutes** provides comfortable headroom for
CI variance. For reference, the sibling native unit-tests job already
uses `timeoutInMinutes: 45`.

## Scope

One-line CI configuration change. No product/source code is affected.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…BabylonJS#18575)

## Problem

Reported on the forum: [Version 9.12.0 Is Broken On
Playground](https://forum.babylonjs.com/t/63631).

On 9.12.0, the default Playground shows a red TypeScript error on `new
BABYLON.Scene(engine)`, and TypeScript library builds against
`@babylonjs/core` fail with errors like:

- `Property 'physicsBody' does not exist on type 'TransformNode'`
- `Argument of type 'BABYLON.Scene' is not assignable to parameter of
type 'Scene'. Type 'Scene' is missing the following properties from type
'Scene': _pointerOverSprite, _pickedDownSprite, ... and 86 more`
- The same pattern for `Engine` / `AbstractEngine`, `Effect`, `Mesh`,
`Observable`, etc.

9.11.0 was fine, so this is a regression.

## Root cause

The declaration generator (`generateDeclaration.ts`) strips ES re-export
statements out of the global `BABYLON` namespace declaration. Its
exclusion list covered `export * from "..."`, `export { ... }`, and
`export default`, but **not the type-only variants** `export type * from
"..."` and `export type { ... } from "..."`.

Those type-only re-exports were introduced by the tree-shaking
`.types.ts` refactor (e.g. `meshUVSpaceRenderer.ts` → `export type *
from "./meshUVSpaceRenderer.types"`). When even one `export ... from`
statement is left inside `declare namespace BABYLON`, it turns the
namespace into an **export context**, which breaks TypeScript
declaration merging of augmented classes/interfaces. The shipped
`babylon.d.ts` (served as the Playground's namespace types and consumed
via `@babylonjs/core`) ended up with **89 `TS2395` ("Individual
declarations in merged declaration must be all exported or all local")**
errors (0 on 9.11.0), causing `BABYLON.Scene`, `TransformNode`,
`Engine`, etc. to lose all their augmented members.

## Fix

Broaden the two exclusion regexes in `GetPackageDeclaration` to also
match the optional `type ` modifier:

- `/export \{/` → `/export (?:type )?\{/`
- `/export \* from "/` → `/export (?:type )?\* from "/`

## Validation

Against declarations regenerated from source:

- Namespace `TS2395` count: **89 → 0**
- `BABYLON.Scene` re-merges all 29 declarations; augmented members
(`_pointerOverSprite`, `spriteManagers`, `createDefaultLight`,
`TransformNode.physicsBody`, …) are present again
- Playground (namespace) and `import * as BABYLON` (module) consumer
repros both type-check cleanly
- `gui` / `loaders` / `serializers` / `materials` namespace portions: 0
leaked re-exports

## Tests

Adds `packages/dev/buildTools/test/unit/generateDeclaration.test.ts` — a
regression test that feeds a declaration file containing `export type *
from "..."` / `export type { ... } from "..."` through
`generateCombinedDeclaration` and asserts those re-exports are excluded
from the namespace output while real declarations and augmentation
interfaces are preserved. Confirmed to fail without the fix and pass
with it.
…FreeCamera and FlyCamera (BabylonJS#18573)

> 🤖 *This PR was created by the create-pr skill.*

## Summary

Ports the framerate-independent movement system (`CameraMovement`) and
the configurable `InputMapper` — already used by `ArcRotateCamera` and
`GeospatialCamera` — to the `TargetCamera` family (`FreeCamera`,
`FlyCamera`, and their subclasses).

## Changes

- Add `TargetCameraMovement`, a shared movement controller for the
`TargetCamera` family, with a configurable `InputMapper`
(pointer/keyboard/touch → translate/rotate).
- `TargetCamera._checkInputs` folds `cameraDirection`/`cameraRotation`
into the movement system for framerate-independent inertial glide; the
per-frame applied delta is written back into those fields so existing
collision/gravity/rotation-constraint logic and external polling keep
working unchanged.
- Wire `FreeCamera`/`FlyCamera` mouse, keyboard, and touch inputs to the
`InputMapper` (gate + optional sensitivity, preserving legacy default
behavior). Subclasses inherit the port.
- Converge `TargetCamera.inertia` to a write-through accessor that syncs
the movement system (`panInertia`/`rotationInertia`), mirroring
`ArcRotateCamera` and replacing the previous per-frame inertia sync.
- Small `GeospatialCameraKeyboardInput` fix: allow `Cmd`+Arrow (Mac) for
rotation while still blocking `Cmd` for other keys (avoids hijacking
browser shortcuts).

## Behavior / compatibility

- Default camera feel is preserved at the reference framerate; inertial
glide is now framerate-independent.
- `cameraDirection`/`cameraRotation` intentionally remain real
`Vector3`/`Vector2` fields — they are mutated in place by core input
classes and external XR code, so they are not converted to accessors. No
public API is removed.
- `FollowCamera` is intentionally not ported (it has no inertia model;
the fold is a no-op for it).

## Testing

- All camera unit tests pass, with new coverage for the movement fold,
input mapping, subclass inheritance, and the inertia accessor
convergence.
- New Playwright visualization tests for `FreeCamera`/`FlyCamera`
movement, with WebGL2 and WebGPU baselines. Each snippet drives the
camera deterministically (`inertia = 0` makes a single `_checkInputs`
apply the raw input independent of frame `deltaTime`), so the captured
pose is stable across engines and frame timing.

---------

Co-authored-by: Georgina Halpern <gehalper@microsoft.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…n GLSL shaders (match WGSL) (BabylonJS#18571)

## What

Guard the `grl_offsets` vertex attribute behind `#ifdef
GREASED_LINE_USE_OFFSETS` in the **GLSL** GreasedLine shaders, so it is
only declared/used when the line actually has offsets. This matches what
the **WGSL** shaders already do.

Affected files:
-
`packages/dev/core/src/Materials/GreasedLine/greasedLinePluginMaterialShadersGLSL.ts`
- `packages/dev/core/src/Shaders/greasedLine.vertex.fx`

## Why

The GLSL GreasedLine shaders declare and use `attribute vec3
grl_offsets` **unconditionally**, even though the `grl_offsets` buffer
is only created when the mesh has offsets
(`GreasedLineBaseMesh._createOffsetsBuffer`, called from `set offsets`).
The `GREASED_LINE_USE_OFFSETS` define is already computed
(`GreasedLinePluginMaterial.prepareDefines`:
`defines.GREASED_LINE_USE_OFFSETS = !!mesh.offsets`, and the simple
material sets it too) — the GLSL shaders just don't consume it.

The WGSL shaders **do** guard it, because WebGPU pipeline validation
rejects a vertex attribute that is declared in the shader but not bound
— i.e. WebGPU *requires* this guard:

```wgsl
// ShadersWGSL/greasedLine.vertex.fx  +  greasedLinePluginMaterialShadersWGSL.ts
#ifdef GREASED_LINE_USE_OFFSETS
    attribute grl_offsets: vec3f;
#endif
...
#ifdef GREASED_LINE_USE_OFFSETS
    var grlPositionOffset: vec3f = input.grl_offsets;
#else
    var grlPositionOffset = vec3f(0.);
#endif
positionUpdated += grlPositionOffset;
```

This PR brings the GLSL path in line with the WGSL path.

### Why it matters (the symptom)

When a GreasedLine has no offsets (the common case) the `grl_offsets`
buffer is never created. On **WebGL** this happens to work, because an
*unbound* vertex attribute reads the constant default `(0,0,0,1)`, so
`position + grl_offsets == position`. But that behavior is not
guaranteed elsewhere:

- **WebGPU**: can't even reach this state — declaring an unbound
attribute fails pipeline validation (hence the existing guard).
- **Babylon Native (bgfx: D3D11 / Metal / Vulkan)**: an attribute
declared by the shader but missing from all bound vertex streams is
**not** zero-defaulted — bgfx aliases it to the last bound stream, so
`grl_offsets` reads unrelated vertex data and corrupts `positionUpdated
+= grl_offsets`. The result is shifted lines and distorted/faceted wide
lines.

Relying on WebGL's lenient unbound-attribute default is the underlying
issue; guarding the attribute (as WGSL already does) is the correct,
backend-agnostic fix.

## Validation

Reproduced with the GreasedLine validation tests on Babylon Native
(D3D11), Playgrounds `#H1LRZ3#103` (basic) and `#H1LRZ3#101` (simple
material). Pixel comparison vs the reference images (240,000 px, 2.5%
tolerance ≈ 6,000 px):

| Test | Before (unconditional `grl_offsets`) | After (this guard) |
|---|---|---|
| GreasedLine - basic (plugin material) | 33,212 px differ ❌ | **2,361
px ✅** |
| GreasedLine - simple material | 20,494 px differ ❌ | **1,479 px ✅** |

No change expected on WebGL (already zero-defaults the unbound
attribute) or WebGPU (already guarded).

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
> 🤖 *This PR was created by the create-pr skill.*

Fixes USDZ export when a texture has cached source bytes in a format
USDZ does not support, such as KTX2.

The previous USDZ path could copy cached source bytes directly into the
archive even when the texture entry was emitted as PNG/JPEG. This made
KTX2-backed textures produce invalid USDZ image payloads. This PR keeps
cached-byte export for USDZ-supported image MIME types, but falls back
to the existing decoded/readback image export path for unsupported
source MIME types.

The cached-image extraction logic is shared with the glTF exporter so
the NullEngine/cached-byte behavior stays consistent, while each
exporter keeps its own supported-MIME policy.

Validation:
- `npm run format:check`
- `npm run build:dev`
- `npm run lint:check`
- `npm run test:unit`
- CI visualization jobs passed on the PR branch

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…est (BabylonJS#18569)

## Summary

Repoints the **Serialize scene without materials** visualization test
(`#PH4DEZ#1` -> `#PH4DEZ#4`).

The previous revision's only DOM usage was a file *download*
(`createElement('a')` + `createEvent` + `link.dispatchEvent`) with no
visual effect. `#PH4DEZ#4` instead performs a real in-memory
serialization round-trip:

1. serialize the sphere hierarchy with `SceneSerializer.SerializeMesh`;
2. reload it via `SceneLoader.ImportMeshAsync(null, "", "data:" + json,
scene)`;
3. render the **deserialized** scene.

This actually validates serialization correctness (not just that it
doesn't throw), modeled on the glTF serializer round-trip `#KU72PX`. The
browser-only `.babylon` download is kept but guarded on
`document.createEvent`, so it is skipped where the DOM isn't available.

Motivation: the same shared snippet is used by BabylonNative's visual
suite, which has no DOM. The round-trip lets both web and native run the
test without faking DOM APIs. Mirrors BabylonNative PR
BabylonJS/BabylonNative#1708.

## Visual impact

The rendered scene is unchanged (same sphere hierarchy), so the
`serializeWithoutMaterials.png` reference is expected to still match —
the BabylonNative side validates against the same reference with the new
revision (248 px diff, well within tolerance). No reference image
regeneration is included; CI's Playwright run will confirm.
…BabylonJS#18565)

### Summary

`ThinNativeEngine` cannot draw anything without `Buffers/buffer.align`'s
prototype patch. Every per-attribute binding in
`ThinNativeEngine._recordVertexArrayObject` reads
`vertexBuffer.effectiveBuffer / effectiveByteOffset /
effectiveByteStride` ([thinNativeEngine.pure.ts
L633-L641](https://github.com/BabylonJS/Babylon.js/blob/master/packages/dev/core/src/Engines/thinNativeEngine.pure.ts#L633-L641))
— getters that only exist after `RegisterBufferAlign()` has run. There
is no fallback. With the stubs from `buffer.pure.ts` in place, every
`recordVertexBuffer(...)` call sees `(undefined, undefined, undefined)`,
bgfx binds degenerate VBs, and the Native engine renders nothing (clear
color still works).

This PR makes `ThinNativeEngine`'s shared init
(`_initializeNativeEngine`) call `RegisterBufferAlign()` so the engine
is **self-sufficient by construction**: anyone who does `new
ThinNativeEngine()` / `new NativeEngine()` gets a working engine,
regardless of which entry point they imported from. The registration is
idempotent so calling it many times is free.

### Regression

Introduced by BabylonJS#18441 (`Tree-shaking - the pure barrel`, v9.8.0).

Pre-9.8.0, `thinNativeEngine.ts` performed `import
"../Buffers/buffer.align";` at the top and was the only engine module.
After the split into `thinNativeEngine.pure.ts` (no side effects, but
contains the call site that reads `effective*`) + wrapper
`thinNativeEngine.ts` (kept the side-effect import), the user-facing
`nativeEngine.ts` only re-exports from `nativeEngine.pure` which imports
`thinNativeEngine.pure` — so the wrapper's side-effect import was never
reached via `import "@babylonjs/core/Engines/nativeEngine"`.

### Why "register in the constructor" instead of "re-add the bare
import"

Two earlier shapes of this fix were considered:

1. **Re-add `import "../Buffers/buffer.align";`** to the wrapper, and
have `nativeEngine.ts` import the wrapper. Minimal, matches the existing
WebGPU pattern (`webgpuEngine.ts` does it at the top), but keeps the
implicit dependency: any future barrel/tree-shake refactor that bypasses
the wrapper silently breaks Native again.
2. **Register in the constructor** (this PR). The dependency becomes an
explicit runtime contract owned by the engine that needs it. The `.pure`
files stay pure-at-import (tree-shakeable), and the side-effectful
wrapper no longer has to exist for this purpose at all (its bare import
is dropped — see diff).

Option 2 is more robust and removes the redundant wrapper-only
side-effect entirely. Suggested by reviewer feedback on the original
wrapper-import approach.

### Changes

**Source**
- `packages/dev/core/src/Engines/thinNativeEngine.pure.ts`
  - Import `RegisterBufferAlign` from `../Buffers/buffer.align.pure`.
- Call `RegisterBufferAlign()` as the **first** line of
`_initializeNativeEngine`, before any `_native.*` access. (Shared by
both `ThinNativeEngine` and `NativeEngine` constructors.)
- `packages/dev/core/src/Engines/thinNativeEngine.ts`
- Drop the now-redundant `import "../Buffers/buffer.align";` at the top.

**Test**
- `packages/dev/core/test/unit/Engines/nativeEngine.sideEffects.test.ts`
*(new)*
- Regression test. Invokes `_initializeNativeEngine` on an
`Object.create(ThinNativeEngine.prototype)` (bypasses `super()` chaos
and `_native` global stubbing), then asserts that
`VertexBuffer.prototype.effectiveByteStride / effectiveByteOffset /
effectiveBuffer` are real getters returning the wrapped values. The
initializer is allowed to throw later (when it reaches `new
_native.Engine(...)`) — by then `RegisterBufferAlign()` has already run,
which is the property under test.
- Verified both ways: passes on this PR; fails with `expected undefined
to be 12` if the `RegisterBufferAlign()` call is removed.

**Auto-generated manifests** (regenerated by `npm run update:manifest`,
reflect the removed side effect — `Engines/thinNativeEngine.ts` no
longer has any module-load side effects)
- `scripts/treeshaking/side-effects-manifest/core/Engines.json`
- `packages/public/@babylonjs/core/package.json`

**Tree-shake closure baseline**
(`scripts/treeshaking/side-effect-import-closure-baseline.json`, new)
- Before this PR, `thinNativeEngine.ts` was already side-effectful
(`bare-import`), so the verifier accepted its re-export of
`./thinNativeEngine.types` (which carries a `declare-module`). After
dropping the bare-import, the file became side-effect-free and the
verifier flagged that re-export as a new closure violation.
- Accepted via `node
scripts/treeshaking/checkSideEffectImportClosure.mjs --update-baseline`.
The `declare-module` is a TypeScript-level augmentation only — no
runtime cost.

---------

Co-authored-by: Cedric Guillemet <ceguille@microsoft.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1st PR for streaming. Next one will be WebGPU CS, polishing & debugging.
…8570)

> 🤖 *This PR was created by the create-pr skill.*

## Problem

When many meshes or geometries share a single delay-loading file (for
example a packed `.babylonbinarymeshdata` geometry file), the engine
downloaded the file **once per mesh**. A scene with 1000+ meshes
referencing the same shared file therefore issued 1000+ identical
network requests for the same URL.

Reported on the forum:
https://forum.babylonjs.com/t/share-same-babylonbinarymeshdata-file-across-all-parsed-meshes/63624

`Mesh._queueLoad` and `Geometry._queueLoad` each call the file loader
directly with no in-flight deduplication, so a shared delay-load file is
fetched N times.

## Fix

Added `Scene._loadDelayedFileAsync(url, useArrayBuffer,
useOfflineSupport)` which coalesces in-flight delay-load requests, keyed
by URL **and** data type (binary vs string). A shared file is fetched
once while the request is in flight, and the same loaded data is
resolved to every coalesced caller. The in-flight entry is cleared once
the request settles, so a later load can re-fetch normally.

`Mesh._queueLoad` and `Geometry._queueLoad` now route through this
method instead of loading directly.

## Failure handling

Hardened the delay-load failure path: on load error the catch now calls
`scene.removePendingData(this)` and logs via `Logger.Error`.
`delayLoadState` is intentionally left as `LOADING` so a failed load
does not trigger a per-frame retry storm (re-queueing every frame the
mesh is in the frustum), while no longer leaving the scene with stale
pending data.

## Tests

Added `babylon.delayLoadCoalescing.test.ts` with 6 unit tests:
- coalesces concurrent requests for the same file into a single load
- re-fetches once a previous request has settled
- does not coalesce requests for the same URL with different data types
- clears the in-flight entry after a failed load so it can be retried
- loads a shared binary file only once across multiple delay-loaded
meshes
- removes pending data and logs when a delay-loaded mesh fails to load

## Notes

- Coalescing lives on `Scene`, consistent with the existing
`_loadFileAsync`/`_activeRequests` request infrastructure and
scene-scoped lifecycle.
- The live forum repro scene could not be exercised in CI/local because
its assets are served from an external CDN that blocks cross-origin
requests from localhost; the unit tests validate the coalescing directly
against the real fetch path.

---------

Co-authored-by: Georgina <gehalper@microsoft.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…he legacy epsilon glide cutoff (BabylonJS#18578)

> 🤖 *This PR was created by the create-pr skill.*

## Summary

Fixes a camera input regression introduced by BabylonJS#18573.
`TargetCamera._checkInputs` ran the legacy `_panningEpsilon` /
`_rotationEpsilon` glide cutoff **before** applying the frame's delta,
and derived `needToMove` / `needToRotate` from the zeroed values.

With the defaults `Epsilon = 0.001` and `speed = 2.0`, the cutoff limit
is `speed * _rotationEpsilon = 0.002`. That is **above** typical
per-frame mouse-look (`offsetPx / angularSensibility`) and keyboard
deltas, so legitimate active input was discarded before it was ever
applied — making `FreeCamera` / `FlyCamera` feel unresponsive (slow
mouse-look and keyboard movement appeared dead).

In the legacy code the cutoff ran *after* applying the delta and only
trimmed the leftover inertial residual, so active input was always
applied.

## Fix

Capture whether each channel has raw input this frame (`hasPanInput` /
`hasRotationInput`), measured before the input is folded into the
movement system, and gate the glide cutoff on its absence. The cutoff
now only terminates the decaying inertial tail; any active input is
always applied, even when it is below the epsilon limit.

Because the FreeCamera/FlyCamera mouse, keyboard, and touch inputs all
flow through `cameraDirection` / `cameraRotation`, this fixes all of
them in one place. It also covers direct external writes to those fields
(e.g. XR code).

## Behavior / compatibility

- Active input below `speed * _panningEpsilon` / `speed *
_rotationEpsilon` is now applied (previously discarded).
- Inertial glide termination is unchanged: on frames with no raw input,
the cutoff still ends the glide at the legacy threshold and resets the
movement velocity.

## Testing

- All camera movement unit tests pass; the existing legacy glide-cutoff
tests are unchanged.
- Added regression tests asserting that a sub-epsilon **active**
rotation and translation input is still applied.

---------

Co-authored-by: Georgina Halpern <gehalper@microsoft.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…nstead of super.inertia accessor (BabylonJS#18580)

## Problem

A freshly-constructed `FreeCamera`/`TargetCamera` reads `inertia ===
undefined` **in the shipped UMD bundle**, which feeds `NaN` into the
framerate-independent movement decay (`Math.pow(undefined, dt)`) and
**freezes all camera movement**. The bug reproduces on the Playground /
snapshot builds but **not** on localhost dev — same source, different
behavior.

This is a separate, more fundamental issue than the epsilon glide-cutoff
fixed in BabylonJS#18578.

## Root cause

`TargetCamera` overrode `get/set inertia` with `super.inertia`. The base
`Camera.inertia` getter is decorated with `@serialize()`. esbuild's
decorator lowering of that decorated accessor loses the `super`
home-object binding, so `super.inertia` resolves to `undefined` **only
in the decorator-transformed UMD bundle**. Native ESM (dev/localhost)
honors `super` correctly, which is exactly why localhost moves and the
bundle does not.

`ArcRotateCamera` was never affected because it already reads a local
backing field (`_inertia`).

## Fix

Switch `TargetCamera` to the same local-field pattern (`_targetInertia`)
— no `super` in the accessor, so it compiles identically in ESM and UMD.

Verified against a **rebuilt UMD bundle**:

| | shipped bundle (before) | rebuilt bundle (after) |
|---|---|---|
| `FreeCamera.inertia` | `undefined` | `0.9` |
| `movement.rotationInertia` | `undefined` | `0.9` |
| 2-frame glide | NaN -> frozen | `0.1 -> 0.19` (moves) |

## Preventing regressions

Unit tests run **untransformed ESM**, so they cannot catch this class of
UMD-only bug. Added ESLint rule **`babylonjs/no-super-in-accessor`**
(alongside the existing `no-downlevel-iteration` UMD guard) that flags
`super.<member>` property access inside a `get`/`set` accessor and
points authors to the local-field pattern. It produces **0 violations**
repo-wide after this fix.

## Testing

- New unit test: default-inertia glide produces finite, advancing
rotation (no NaN).
- `npx prettier --check` clean; `eslint` clean on changed files;
targeted vitest 20/20 pass.
- ESLint rule covered by RuleTester (flags super reads/writes incl.
nested arrows & computed access; allows local fields and
`super.method()` calls).

---------

Co-authored-by: Georgina Halpern <gehalper@microsoft.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Bumps [esbuild](https://github.com/evanw/esbuild) from 0.25.12 to
0.28.1.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/evanw/esbuild/releases">esbuild's
releases</a>.</em></p>
<blockquote>
<h2>v0.28.1</h2>
<ul>
<li>
<p>Disallow <code>\</code> in local development server HTTP requests (<a
href="https://github.com/evanw/esbuild/security/advisories/GHSA-g7r4-m6w7-qqqr">GHSA-g7r4-m6w7-qqqr</a>)</p>
<p>This release fixes a security issue where HTTP requests to esbuild's
local development server could traverse outside of the serve directory
on Windows using a <code>\</code> backslash character. It happened due
to the use of Go's <code>path.Clean()</code> function, which only
handles Unix-style <code>/</code> characters. HTTP requests with paths
containing <code>\</code> are no longer allowed.</p>
<p>Thanks to <a
href="https://github.com/dellalibera"><code>@​dellalibera</code></a> for
reporting this issue.</p>
</li>
<li>
<p>Add integrity checks to the Deno API (<a
href="https://github.com/evanw/esbuild/security/advisories/GHSA-gv7w-rqvm-qjhr">GHSA-gv7w-rqvm-qjhr</a>)</p>
<p>The previous release of esbuild added integrity checks to esbuild's
npm install script. This release also adds integrity checks to esbuild's
Deno install script. Now esbuild's Deno API will also fail with an error
if the downloaded esbuild binary contains something other than the
expected content.</p>
<p>Note that esbuild's Deno API installs from
<code>registry.npmjs.org</code> by default, but allows the
<code>NPM_CONFIG_REGISTRY</code> environment variable to override this
with a custom package registry. This change means that the esbuild
executable served by <code>NPM_CONFIG_REGISTRY</code> must now match the
expected content.</p>
<p>Thanks to <a
href="https://github.com/sondt99"><code>@​sondt99</code></a> for
reporting this issue.</p>
</li>
<li>
<p>Avoid inlining <code>using</code> and <code>await using</code>
declarations (<a
href="https://redirect.github.com/evanw/esbuild/issues/4482">#4482</a>)</p>
<p>Previously esbuild's minifier sometimes incorrectly inlined
<code>using</code> and <code>await using</code> declarations into
subsequent uses of that declaration, which then fails to dispose of the
resource correctly. This bug happened because inlining was done for
<code>let</code> and <code>const</code> declarations by avoiding doing
it for <code>var</code> declarations, which no longer worked when more
declaration types were added. Here's an example:</p>
<pre lang="js"><code>// Original code
{
  using x = new Resource()
  x.activate()
}
<p>// Old output (with --minify)<br />
new Resource().activate();</p>
<p>// New output (with --minify)<br />
{using e=new Resource;e.activate()}<br />
</code></pre></p>
</li>
<li>
<p>Fix module evaluation when an error is thrown (<a
href="https://redirect.github.com/evanw/esbuild/issues/4461">#4461</a>,
<a
href="https://redirect.github.com/evanw/esbuild/pull/4467">#4467</a>)</p>
<p>If an error is thrown during module evaluation, esbuild previously
didn't preserve the state of the module for subsequent module
references. This was observable if <code>import()</code> or
<code>require()</code> is used to import a module multiple times. The
thrown error is supposed to be thrown by every call to
<code>import()</code> or <code>require()</code>, not just the first.
With this release, esbuild will now throw the same error every time you
call <code>import()</code> or <code>require()</code> on a module that
throws during its evaluation.</p>
</li>
<li>
<p>Fix some edge cases around the <code>new</code> operator (<a
href="https://redirect.github.com/evanw/esbuild/issues/4477">#4477</a>)</p>
<p>Previously esbuild incorrectly printed certain edge cases involving
complex expressions inside the target of a <code>new</code> expression
(specifically an optional chain and/or a tagged template literal). The
generated code for the <code>new</code> target was not correctly wrapped
with parentheses, and either contained a syntax error or had different
semantics. These edge cases have been fixed so that they now correctly
wrap the <code>new</code> target in parentheses. Here is an example of
some affected code:</p>
<pre lang="js"><code>// Original code
new (foo()`bar`)()
new (foo()?.bar)()
<p>// Old output<br />
new foo()<code>bar</code>();<br />
new (foo())?.bar();</p>
<p></code></pre></p>
</li>
</ul>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/evanw/esbuild/blob/main/CHANGELOG-2025.md">esbuild's
changelog</a>.</em></p>
<blockquote>
<h1>Changelog: 2025</h1>
<p>This changelog documents all esbuild versions published in the year
2025 (versions 0.25.0 through 0.27.2).</p>
<h2>0.27.2</h2>
<ul>
<li>
<p>Allow import path specifiers starting with <code>#/</code> (<a
href="https://redirect.github.com/evanw/esbuild/pull/4361">#4361</a>)</p>
<p>Previously the specification for <code>package.json</code> disallowed
import path specifiers starting with <code>#/</code>, but this
restriction <a
href="https://redirect.github.com/nodejs/node/pull/60864">has recently
been relaxed</a> and support for it is being added across the JavaScript
ecosystem. One use case is using it for a wildcard pattern such as
mapping <code>#/*</code> to <code>./src/*</code> (previously you had to
use another character such as <code>#_*</code> instead, which was more
confusing). There is some more context in <a
href="https://redirect.github.com/nodejs/node/issues/49182">nodejs/node#49182</a>.</p>
<p>This change was contributed by <a
href="https://github.com/hybrist"><code>@​hybrist</code></a>.</p>
</li>
<li>
<p>Automatically add the <code>-webkit-mask</code> prefix (<a
href="https://redirect.github.com/evanw/esbuild/issues/4357">#4357</a>,
<a
href="https://redirect.github.com/evanw/esbuild/issues/4358">#4358</a>)</p>
<p>This release automatically adds the <code>-webkit-</code> vendor
prefix for the <a
href="https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Properties/mask"><code>mask</code></a>
CSS shorthand property:</p>
<pre lang="css"><code>/* Original code */
main {
  mask: url(x.png) center/5rem no-repeat
}
<p>/* Old output (with --target=chrome110) */<br />
main {<br />
mask: url(x.png) center/5rem no-repeat;<br />
}</p>
<p>/* New output (with --target=chrome110) */<br />
main {<br />
-webkit-mask: url(x.png) center/5rem no-repeat;<br />
mask: url(x.png) center/5rem no-repeat;<br />
}<br />
</code></pre></p>
<p>This change was contributed by <a
href="https://github.com/BPJEnnova"><code>@​BPJEnnova</code></a>.</p>
</li>
<li>
<p>Additional minification of <code>switch</code> statements (<a
href="https://redirect.github.com/evanw/esbuild/issues/4176">#4176</a>,
<a
href="https://redirect.github.com/evanw/esbuild/issues/4359">#4359</a>)</p>
<p>This release contains additional minification patterns for reducing
<code>switch</code> statements. Here is an example:</p>
<pre lang="js"><code>// Original code
switch (x) {
  case 0:
    foo()
    break
  case 1:
  default:
    bar()
}
</code></pre>
</li>
</ul>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/evanw/esbuild/commit/bb9db84c02433fbe37b3509f53f9f3e3cc48725e"><code>bb9db84</code></a>
publish 0.28.1 to npm</li>
<li><a
href="https://github.com/evanw/esbuild/commit/9ff053e53b8eeb990f59355dbea365277ac45ee2"><code>9ff053e</code></a>
security: add integrity checks to the Deno API</li>
<li><a
href="https://github.com/evanw/esbuild/commit/0a9bf2135b67c7e28989a5ba19f0f000805a5ab5"><code>0a9bf21</code></a>
enforce non-negative size in gzip parser</li>
<li><a
href="https://github.com/evanw/esbuild/commit/e2a1a7132058ee067fe736eac15f695861b8654e"><code>e2a1a71</code></a>
security: forbid <code>\\</code> in local dev server requests</li>
<li><a
href="https://github.com/evanw/esbuild/commit/83a2cbfc35809f4fd5152da59572d7bed7739d78"><code>83a2cbf</code></a>
fix <a
href="https://redirect.github.com/evanw/esbuild/issues/4482">#4482</a>:
don't inline <code>using</code> declarations</li>
<li><a
href="https://github.com/evanw/esbuild/commit/308ad745d824c77bc607603451b257d0f2fd9a38"><code>308ad74</code></a>
fix <a
href="https://redirect.github.com/evanw/esbuild/issues/4471">#4471</a>:
renaming of nested <code>var</code> declarations</li>
<li><a
href="https://github.com/evanw/esbuild/commit/f013f5f99a015bce92ec48d49181d4ad3177b29b"><code>f013f5f</code></a>
fix some typos</li>
<li><a
href="https://github.com/evanw/esbuild/commit/aafd6e48b1088336a5f5a17e930be7e840d43d8c"><code>aafd6e4</code></a>
chore: fix some minor issues in comments (<a
href="https://redirect.github.com/evanw/esbuild/issues/4462">#4462</a>)</li>
<li><a
href="https://github.com/evanw/esbuild/commit/15300c30b5e22f7cfcbed850c246d35095658386"><code>15300c3</code></a>
follow up: cjs evaluation fixes</li>
<li><a
href="https://github.com/evanw/esbuild/commit/1bda0c31d7697c0af44b3ab39b81e599e559a395"><code>1bda0c3</code></a>
fix <a
href="https://redirect.github.com/evanw/esbuild/issues/4461">#4461</a>,
fix <a
href="https://redirect.github.com/evanw/esbuild/issues/4467">#4467</a>:
esm evaluation fixes</li>
<li>Additional commits viewable in <a
href="https://github.com/evanw/esbuild/compare/v0.25.12...v0.28.1">compare
view</a></li>
</ul>
</details>
<details>
<summary>Maintainer changes</summary>
<p>This version was pushed to npm by <a
href="https://www.npmjs.com/~GitHub%20Actions">GitHub Actions</a>, a new
releaser for esbuild since your current version.</p>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=esbuild&package-manager=npm_and_yarn&previous-version=0.25.12&new-version=0.28.1)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)
You can disable automated security fix PRs for this repo from the
[Security Alerts
page](https://github.com/BabylonJS/Babylon.js/network/alerts).

</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
…abylonJS#18582)

## Bugs fixed

Two regressions/bugs reported in `GeospatialCamera` ("geo cam").

### 1. Zoom was disabled while rotating
A recent input-mapping refactor unintentionally reverted BabylonJS#18278. The
zoom-stop condition again included a rotation guard, so zoom was
suppressed during rotation. Restored the `isDragging`-only condition so
**rotation and zoom work simultaneously** again.

### 2. `updateFlyToDestination` never actually redirected the flight
It relied on `InterpolatingBehavior.updateProperties` setting
`animatable.target = value` — but `Animatable.target` is the animated
*object* (the camera), not a goal value, so retargeting was a no-op
(broken since BabylonJS#17452).

**Fix / design:** removed `updateProperties` and reframed a redirect as
*"a fresh flight from the camera's current pose over the remaining
duration."* `Animation.TransitionTo` already seeds the start keyframe
from the current value, so restarting mid-flight is inherently smooth
(no snap). `updateFlyToDestination` now guards on `isInterpolating` and
delegates to a shared `_flyToAsync` helper used by `flyToAsync` too.

Supporting changes in `InterpolatingBehavior`:
- New `remainingDurationMs` getter (public `toFrame`/`masterFrame` + a
shared `_FrameRate` constant).
- `animatePropertiesAsync` resolves immediately when nothing needs
animating (prevents a hung promise).

### Promise semantics
Redirecting interrupts the in-flight animation (standard interruption
semantics), so the original `flyToAsync` promise resolves early.
`updateFlyToDestination` now **returns the redirect promise**
(`Promise<void> | undefined`) so callers can await the redirected
flight. Documented in JSDoc.

## Tests
- Added
`packages/dev/core/test/unit/Behaviors/interpolatingBehavior.test.ts` (6
tests: immediate resolve, convergence, `remainingDurationMs`, smooth
mid-flight restart).
- All 265 unit tests in Behaviors/Cameras/Animations pass; lint 0
errors; formatted.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

---------

Co-authored-by: Georgina Halpern <gehalper@microsoft.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
> 🤖 *This PR was created by the create-pr skill.*

Bumps `vite` from `6.4.2` to `6.4.3` across the viewer packages to
address two security advisories flagged by `npm audit`:

- **vite: `server.fs.deny` bypass on Windows alternate paths**
([GHSA-fx2h-pf6j-xcff](GHSA-fx2h-pf6j-xcff))
- **launch-editor: NTLMv2 hash disclosure via UNC path handling on
Windows**
([GHSA-v6wh-96g9-6wx3](GHSA-v6wh-96g9-6wx3))

### Changes
- `package.json`, `packages/tools/viewer/package.json`,
`packages/public/@babylonjs/viewer/package.json`: bump `vite`
devDependency to `6.4.3`
- `package-lock.json`: regenerated

### Validation
- `npm audit` no longer reports the two advisories above.
- Built `@tools/viewer` and `@babylonjs/viewer`; output is
**byte-for-byte identical** to the 6.4.2 build (verified by SHA-256
hashing all 1669 `lib/` + `dist/` files before and after). Vite is only
used by the dev `serve` script, not the build output.

> Note: a separate `esbuild` advisory remains on the Vite 6.x line and
would require a major Vite 8 upgrade — out of scope for this patch.

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
## Summary
- Add opt-in `NullEngineOptions.enableMultiview` support for CPU-side
multiview render-state tests.
- Provide headless multiview RTT creation/binding and no-op UBO plumbing
needed to inspect `viewProjectionR` under NullEngine.
- Add focused NullEngine tests for opt-in caps, RTT metadata, multiview
UBO/matrix state, and one expected-failing regression test documenting
the stale frustum behavior without changing the production `Scene` hot
path.

## Scope
This replaces the closed BabylonJS#18542 with the production hot-path changes
removed. The PR intentionally does not modify
`Scene.setTransformMatrix`, `Scene.updateTransformMatrix`, or any other
render hot-path code.

The expected-failing test is kept as executable documentation of the bug
NullEngine can now expose. Making it pass requires a separate
production-path design because it currently depends on changing the
scene transform-cache invalidation behavior.

## Tests
- `npx prettier --write
packages/dev/core/test/unit/Engines/nullEngine.test.ts
packages/dev/core/src/Engines/nullEngine.pure.ts
packages/dev/core/src/scene.pure.ts`
- `npx eslint --quiet packages/dev/core/src/Engines/nullEngine.pure.ts
packages/dev/core/test/unit/Engines/nullEngine.test.ts`
- `npx vitest run --project=unit
packages/dev/core/test/unit/Engines/nullEngine.test.ts` (`9 passed | 1
expected fail`)
- `npm run compile:source -w @dev/core`
…lonJS#18558)

# [Native] Honor `depthCullingState.depthTest` on the native engine

## Problem

`EffectRenderer` (used by `EffectWrapper`-based fullscreen passes)
disables depth
testing by mutating the shared depth-culling state directly:

```ts
// Materials/effectRenderer.pure.ts -> applyEffectWrapper()
this.engine.depthCullingState.depthTest = depthTest; // false for a fullscreen pass
```

On the WebGL/WebGPU engines this is flushed to the GPU by
`applyStates()`, which is
called from `drawElementsType`/`drawArraysType` and reads
`depthCullingState`.

The native engine does **not** go through that `applyStates()` path —
its draw methods
encode a draw command directly, and depth state is only ever changed
through the
explicit `setDepthBuffer()` / `setDepthFunction()` commands. As a
result, a depth-test
toggle done through `engine.depthCullingState.depthTest` never reaches
the native
command stream.

The visible effect: a fullscreen `EffectWrapper` quad keeps the
depth-test state of the
previously rendered geometry (e.g. `LEQUAL`). Drawn at clip-space `z =
0.5` against a
render target whose depth buffer is not at the far value, every fragment
fails the depth
test and is discarded, so the pass produces an all-black target even
though the pixel
shader output is correct.

`ThinDepthPeelingRenderer` toggles `depthCullingState.depthTest`
directly as well and is
affected by the same gap.

## Fix

Make the native engine honor `depthCullingState.depthTest` the same way
the other
engines do:

- `setDepthBuffer()` keeps `_depthCullingState.depthTest` in sync
(matching the base
`AbstractEngine.setDepthBuffer`, which is literally
`this._depthCullingState.depthTest = enable`).
- Before each native draw, `_flushDepthTestState()` reconciles the
encoded depth-test
enable with `depthCullingState.depthTest` and emits the command only
when it changed.

This keeps the existing explicit `setDepthBuffer()` /
`setDepthFunction()` paths working
unchanged (they already set the state object / are the source of truth)
while also
honoring direct `depthCullingState.depthTest` mutations.

No public API change.

## Testing

Validated on Babylon Native (Win32 / D3D11) with the Playground
validation suite:

- The previously-broken `EffectRenderer` onion-skin/blur fullscreen-pass
scene renders
  correctly instead of all black.
- Full standard validation sweep: no regressions (every
previously-passing test still
  passes, including depth- and transparency-sensitive scenes).

## Notes

`EffectRenderer` also toggles `stencilState.stencilTest` directly. That
has the same
latent gap on native but defaults to `false` and has no reproducing
scene today, so it is
intentionally left out of this change to keep it minimal; it can be
addressed with the
same pattern if a repro appears.

---

Partial fix for BabylonJS/BabylonNative#1106
(Handle Engine States in Babylon Native).

---------

Co-authored-by: Branimir Karadzic <branimirkaradzic@gmail.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
## Summary
- Add optional depth-based pickedPoint and normal reconstruction to
GPUPicker.
- Use a MultiRenderTarget depth attachment when depth picking is
enabled, with shader macro guards so the default color-only path is
unchanged when disabled.
- Extend pickAsync, multiPickAsync, and boxPickAsync to return
depth-derived point/normal data where available.
- Add adaptive multiPickAsync readback options for rectangle vs
individual point readbacks.

## Context
This PR follows up on the GPU picking point/normal discussion and
example:

https://forum.babylonjs.com/t/gpu-picking-point-and-normal-example/40965/34

## Coordinate scaling note
- GPUPicker now maps input/canvas CSS pixel coordinates to render-target
pixels using the input element client rect when available, falling back
to hardware scaling for non-DOM engines. This preserves the existing
default-canvas behavior while improving picking on CSS-scaled canvases.

## Validation
- npx tsc -p packages/dev/core/tsconfig.build.json --noEmit --pretty
false
- npm run compile:assets -w @dev/core
- npx vitest run
packages/dev/core/test/unit/Collisions/babylon.gpuPicker.test.ts
--project=unit
- npm run test:visualization -- --project=webgl2 --grep "GPUPicker Depth
Point and Normal" --update-snapshots
## Summary

This PR reworks `SceneLoader` so that all internal orchestration is
**Promise / `async`/`await`-first**. Callback chaining, explicit
`Promise.then`/`Promise.catch`, and the old mixed control flow have been
removed from the internals. The public callback-style APIs (the
deprecated `SceneLoader.ImportMesh`, `Load`, `Append`,
`LoadAssetContainer`, `ImportAnimations` statics and their `*Async`
variants) are now thin wrappers around a small set of core `async`
functions.

Along the way, the refactor fixes a class of long-standing bugs where a
load operation could **never settle** (the returned Promise never
resolved or rejected, and success/error callbacks were never invoked) or
where **pending plugin data leaked**. A comprehensive unit test suite
was added to lock in the corrected behavior.

## Motivation

`sceneLoader.ts` had accumulated a mix of three different async styles:
raw callbacks, hand-written `Promise` executors with `.then`/`.catch`,
and `async`/`await`. This made the control flow hard to follow and, more
importantly, hid several edge cases where errors thrown synchronously
vs. asynchronously were handled inconsistently — sometimes leaving a
load hanging forever.

## Changes

### Core refactor (`packages/dev/core/src/Loading/sceneLoader.ts`)

- **Single async core per operation.** Each public operation now funnels
through one core `async` function:
  - `importMeshCoreAsync`
  - `loadSceneCoreAsync`
  - `appendSceneCoreAsync`
  - `loadAssetContainerCoreAsync`
  - `importAnimationsCoreAsync`
These contain the real logic; every public/legacy entry point (modern
`*Async` functions and the deprecated `SceneLoader` statics, including
the callback overloads) simply `await`s the appropriate core function
and adapts the result/errors to its signature.

- **`loadDataAsync` returns the loaded payload.** Plugin instantiation,
`directLoad`, and file loading are bridged into a single Promise that
resolves with `{ plugin, data }` and rejects on any failure, instead of
driving everything through `onSuccess`/`onError` callbacks.
`try/finally` guarantees pending data cleanup.

- **Unified plugin handling via an async adapter.** Added a
`SceneLoaderPlugin` union type, an `isSyncPlugin` type guard, and a
`toAsyncPlugin` adapter that wraps a synchronous `ISceneLoaderPlugin` as
an `ISceneLoaderPluginAsync`. Call sites no longer branch on `(plugin as
ISceneLoaderPlugin).importMesh` / `.load` with casts and non-null
assertions; they work against a single async-shaped interface.

- **Centralized error handling.** Added `createLoadError`,
`toLoadError`, and `getErrorMessage` helpers so synchronous throws and
asynchronous rejections are funneled into a consistent `RuntimeError`
with `ErrorCodes.SceneLoaderError`, without double-wrapping
already-tagged scene-loader errors.

- **Robust progress callbacks.** Added `wrapProgress`, which isolates
user `onProgress` callbacks so that a throw inside `onProgress` is
logged (`Logger.Warn`) and does **not** abort the load.

- **Preserved eager `OnPluginActivatedObservable` timing.** Plugin
factories are only `await`ed when `createPlugin` actually returns a
`Promise` (`const p = createPlugin(...); plugin = p instanceof Promise ?
await p : p;`). This avoids deferring synchronous plugin activation to a
microtask, keeping `OnPluginActivatedObservable` firing synchronously
for backward compatibility.

### Bug fixes (behavioral)

The refactor closes several "never settles" / leak paths that previously
could hang a load or silently swallow errors:

- **Silent `false` return:** a synchronous plugin's `importMesh`/`load`
returning falsy *without* calling `onError` now rejects (and clears
pending data) instead of hanging forever.
- **Invalid file info:** `GetFileInfo` returning `null` now rejects the
operation instead of leaving the Promise unsettled.
- **Null scene/engine paths:** these now reject instead of silently
never settling.
- **Plugin disposed mid-load:** now rejects with a clear "plugin was
disposed" error rather than hanging.
- **Unknown `animationGroupLoadingMode`:** now rejects instead of never
settling.
- **Synchronous `directLoad` throw:** now cleans up pending data and
rejects, matching the async `directLoad` rejection path.
- **Swallowed `LoadSceneAsync` rejection** (e.g. disabled/unavailable
plugin) is now surfaced.
- **Offline provider bypass (PR BabylonJS#18584 class):** in-memory sources
(`File` / raw data / `data:`) bypass the offline provider; URL-backed
loads still go through it.
- **Consistent error logging:** callback statics now log via
`Logger.Error` when no `onError` handler is supplied, including paths
that were previously swallowed silently.

### Tests

- **New unit suite:**
`packages/dev/core/test/unit/Loading/sceneLoader.test.ts` (58 tests). It
uses a fully controllable dummy loader plugin to exercise:
- sync vs. async plugins, plugin factories, `directLoad`, `loadFile`,
binary/`ArrayBufferView` input, options, and observables;
- synchronous throws **and** asynchronous rejections from plugin entry
points;
- every "never settles" / leak path listed above (added first as failing
regression tests, then made green by the refactor);
- PR BabylonJS#18584 scenarios (offline provider bypass for in-memory vs. URL
sources);
- the deprecated `SceneLoader` static callback APIs (success and error
callbacks).

- **Integration test:**
`packages/dev/loaders/test/integration/babylon.sceneLoader.test.ts` —
the "Load BoomBox with dispose" test now `.catch(() => {})`s the
`AppendAsync` promise, since disposing the loader mid-load now
(correctly) rejects the load promise; this prevents an unhandled
rejection. Full SceneLoader integration file passes (requires the
Babylon CDN dev server).

## Backward compatibility

- Public API signatures are unchanged. Deprecated callback statics keep
behaving as wrappers over the new async cores.
- `OnPluginActivatedObservable` still fires synchronously for
synchronous plugin factories.
- The only intentional behavioral differences are the bug fixes above:
operations that used to hang now reject, and a few previously-silent
error paths now log or reject. These are corrections to clearly buggy
behavior.

## Validation

- Unit: `sceneLoader.test.ts` — 58/58 passing.
- Integration: `babylon.sceneLoader.test.ts` — passing with the dev
server running.

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…JS#18561)

Paired native PR: BabylonJS/BabylonNative#1750

## What

Adds cube render target support to the Babylon Native engine. Previously
the
native engine had no `createRenderTargetCubeTexture` override and
`bindFramebuffer(faceIndex)` threw, so any feature that renders into a
cube map
— `ReflectionProbe`, point-light cube shadow maps — fell through to the
WebGL
code path and dereferenced the null `_gl` context (`TEXTURE_CUBE_MAP
undefined`).

## Changes (`packages/dev/core/src/Engines`)

- `thinNativeEngine.pure.ts`
- `createRenderTargetCubeTexture`: creates a native cube color texture
and one
framebuffer per face (the native side binds the matching cube layer).
- `bindFramebuffer`: binds the per-face framebuffer for cube render
targets.
- `generateMipMapsForCubemap`: no-op on Native — bgfx auto-generates the
mip
    chain on render-target resolve, the same way 2D RTTs get their mips.
- `Native/nativeRenderTargetWrapper.ts`: tracks per-face framebuffers
and
  releases them on dispose.
- `Native/nativeInterfaces.ts`: threads the cube/layer params through
  `initializeTexture` and `createFrameBuffer`.

## Paired native change

Requires the matching BabylonNative C++ change (cube color texture +
per-face
attachment). Draft until both land.

## Testing

Built `babylon.max.js` and ran the BabylonNative Playground validation
suite
(D3D11). The "Shadows with instances" tests (left/right handed), which
previously crashed, now pass; the ReflectionProbe and point-light-shadow
scenes
render with correct geometry and orientation.

---

## Related PRs & landing order

- **Babylon.js (engine / TS):**
BabylonJS#18561
- **BabylonNative (C++ engine + test re-enable):**
BabylonJS/BabylonNative#1750

Co-dependent; land in this order:
1. **Babylon.js BabylonJS#18561 first** — adds the cube render-target TS
overrides; no WebGL behavior change.
2. A **`babylonjs` npm release** ships that TS change.
3. **BabylonNative BabylonJS#1750 last** — bumps the bundled `babylonjs` and
re-enables the 2 `Shadows with instances` (left/right handed) validation
tests, which only pass once the paired JS is present in the bundled
engine.

---------

Co-authored-by: Branimir Karadzic <branimirkaradzic@gmail.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
RaananW and others added 25 commits June 25, 2026 20:34
…S#18613)

## Goal

Speed up PR CI (Monorepo CI pipeline) **without dropping any tests**.
Constraints respected:
- **Peak ≤ 8 parallel jobs per PR** (org has 15 shared agents).
- **No Nx Cloud cost** — only the free `Cache@2` local cache + free
Pipeline Artifacts.

This is a CI-only change; no product code is touched. Opening as **draft
to validate on real CI** before merge.

## What changed

### Caching (free `Cache@2`, biggest win on re-pushes)
- New `templates/cache-steps.yml` emits npm / Nx / Playwright pipeline
caches, plus pipeline vars for stable cacheable locations under
`$(Pipeline.Workspace)`.
- Wired into Build, ES6, ES6Tools, FormatLint, TypeDoc, UnitTests,
Interaction, VisWebGL2, VisWebGPU, Performance, Viewer.
- Nx cache is keyed per-job on the commit with `restoreKeys` fallback to
the most recent prior cache; Nx's own input hashing keeps it correct.
This is what makes "fix one comment + repush" fast.

### Parallelism / restructuring (behavior-preserving)
- **ES6 job split.** `ES6` now builds only the lib packages the ES6
visualization tests consume (`build:es6:libs`) and runs `es6vis`. A new
parallel **`ES6Tools`** job runs the full `build:es6` (editor packages +
`check:treeshaking-all`, still **blocking**), the `es6-packages` smoke
test (which hard-requires the editor builds), and the file-size report.
`Deploy` now also depends on `ES6Tools` (it consumes `fileSizes.json`).
This pulls ~7–9 min of editor/tree-shaking work off the es6vis critical
path.
- **UnitTests moved out of post-Build "wave 2".** It runs in-process
(vitest) and needs no Build snapshot; the one CDN-dependent assertion
(`moduleFileSize.test.ts`) already self-skips when `fileSizes.json` is
absent. It keeps only a `FormatLint` fail-fast gate. Frees an agent
slot.
- **MemoryLeakTests split** into two parallel suite jobs (ci + packages)
via `templates/memory-leak-job.yml`, trimming the post-Build
critical-path tail.

## Behavior preserved (everything still runs and fails as before)
- Tree-shaking violations → fail `build:es6` in `ES6Tools` (blocking) →
fail pipeline.
- `es6-packages` smoke → blocking in `ES6Tools`, with editor builds
present.
- `es6vis` → blocking in `ES6`.
- `prepublishOnly` → still `continueOnError`.
- File-size report → still generated and uploaded.
- Memory-leak suites → still non-blocking.

## Expected impact
- ES6 critical pole ≈ **36 min → ~27 min**.
- Faster re-pushes via npm/Nx caches.
- **Peak parallel jobs stays ≤ 8.**

## Why no Pipeline Artifacts for the ES6 handoff
The es6 build output is hundreds of MB of many small files written
in-place into `packages/public/@babylonjs/*` — a poor artifact fit
(per-file overhead, would need tar), and Nx rebuilds deps anyway.
Parallelizing the work es6vis doesn't need beats an artifact handoff
here.

## Note for reviewers
Splitting MemoryLeakTests produces **two** PR comments (one per suite)
instead of one.

🤖 Validating on CI before marking ready.

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…e for pure barrel (BabylonJS#18620)

## Motivation

When consuming `@babylonjs/core` through the **pure barrel**
(tree-shaking-friendly path), features could fail silently or crash
because a prototype-augmented engine/scene method was never registered.
That registration only happened as a side effect of an `import "..."` in
the feature's `.ts` wrapper — which the pure barrel deliberately skips.

The guiding principle: **if a feature simply cannot work without another
feature** (a *hard* dependency, not an optional capability), it should
register that dependency **at construction time**, rather than forcing
the consumer to add a manual side-effect import.

## What changed

Hard dependencies are now registered inside the feature's `.pure.ts`
constructor using the existing idempotent `Register*()` functions. This
mirrors the pre-existing precedent in `depthRenderer.pure.ts`.

**Engine extensions** — now self-register the engine extension they
require:
- `dynamicTexture`, `cubeTexture`, `htmlElementTexture`,
`multiRenderTarget`
- `webgl2ParticleSystem` / `thinParticleSystem` (transformFeedback),
`gpuParticleSystem`
- `vrMultiviewToSingleviewPostProcess` (alpha / multiview),
`thinDepthPeelingRenderer`

**Scene components** — caller now registers the scene component before
invoking the augmented `scene.enableX()`:
- `prePassRenderer` → geometryBufferRenderer SC
- `IBLShadows/iblShadowsRenderPipeline` → geometryBufferRenderer SC +
iblCdfGenerator SC
- `FrameGraph/.../iblShadowsRendererTask` → iblCdfGenerator SC
- `Cameras/VR/vrExperienceHelper` → gamepad SC + animatable
- `animationGroup` → animatable

**Redundant import removal** — the `ssao` and `lens` rendering pipelines
already self-register the depth scene component in their `.pure`
constructors, so the now-redundant wrapper imports were dropped.

## Metadata

- Side-effects manifest shards updated (`Animations`, `Cameras`,
`FrameGraph`, `Materials`, `Particles`, `Rendering`).
- `@babylonjs/core` `package.json` `sideEffects` array updated to drop
the wrappers that are now side-effect-free (684 → 675 cumulative).

## Backward compatibility

Fully preserved. The wrappers only lose bare side-effect imports that
affect the **pure barrel** path. The **full** side-effect barrel
(`@babylonjs/core` via `index.ts`) still registers everything, because
every target is still `export *`'d from its package index barrel. The
registration calls are all idempotent, so the full path incurs no
double-registration cost.

## Notes for reviewers

- All `Register*()` functions used here are idempotent (guarded by a
module-level `_Registered` flag or an instance check), so
construction-time calls are safe and cheap.
- Scene-component register functions receive the feature class as an
argument (the augmented `scene.enableX()` constructs it), so no circular
value imports are introduced.
- Some candidates were intentionally **skipped**:
`baseTexture.polynomial` (handled by the generated
`_MissingSideEffectProperty` stub pattern + too cross-cutting),
`colorCurves` (serialization-only, not a hard dep),
`planeRotationGizmo`→`linesBuilder` (vestigial),
`gaussianSplattingMesh`→`thinInstanceMesh` (no `thinInstance` usage in
`.pure`).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Sampling fixes for OpenPBR fuzz, including a fix to use the correct
diffuse lighting when coating is used.
…ree-shaken builds (BabylonJS#18623)

## Problem

In tree-shaken builds,
`SerializationHelper._ImageProcessingConfigurationParser` stays an
unregistered throwing stub unless the `imageProcessingConfiguration`
side-effect wrapper happens to be imported transitively. As a result,
`clone()` / `Parse()` throws for any material that serializes an image
processing configuration, because cloning serializes
`_imageProcessingConfiguration` (serialization type 9) and invokes that
unset parser.

This affects every material that mixes in `ImageProcessingMixin`:
- `StandardMaterial`
- `BackgroundMaterial`
- `NodeMaterial`
- `PBRMaterial`
- `PBRMetallicRoughnessMaterial`
- `PBRSpecularGlossinessMaterial`
- `OpenPBRMaterial`

## Fix

Rather than relying on a manual side-effect import at the call site,
each affected material now calls
`RegisterImageProcessingConfiguration()` from its own `Register*()`
function in its `.pure.ts` — the place the tree-shaking architecture
designates for side effects. Importing a material's side-effect wrapper
(or calling its register function directly) now self-registers the
parser, while the pure import path remains side-effect-free.

## Tests

Adds `babylon.imageProcessingSideEffect.test.ts`, which for each of the
7 materials asserts:
1. The parser is a throwing stub on a fresh module import.
2. Importing the material's `.pure` module produces no side effect.
3. Running the material's `Register*()` wires up a working parser.

This test fails on `master` before the fix.

## Validation

- `tsc` build: clean
- ESLint (incl. pure-import rules): 0 errors
- Prettier: formatted
- `check:manifest-drift` and `check:side-effects-sync`: up-to-date (no
top-level side-effect change)
- Existing material unit tests: pass

## Note

`StandardMaterial` also serializes `FresnelParameters`
(`_FresnelParametersParser`), which has the same latent stub problem in
tree-shaken builds. This PR is intentionally scoped to the reported
image-processing issue; happy to extend the same pattern to fresnel
parameters in a follow-up if desired.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
## What

Adds `.github/design-guidelines.md` — a standalone, copyable design
guidelines document for **Babylon.js tools** (Inspector,
NME/NGE/NPE/NRGE, GUI Editor, Smart Filters Editor, Viewer, Playground,
and future tools).

It is based on the existing Fluent UI / `MakeModularTool` work in the
**Inspector (inspector-v2)** and **Flow Graph Editor**, which already
define a strict color schema and a consistent panel/overlay structure.

## Why

We want one shared baseline so every tool — across all Babylon.js
repositories — looks, feels, and behaves like part of the same family.
The doc is intentionally self-contained so it can be copied into other
repos.

## Contents

- **Core principles** — Fluent-first, theme-token-driven, shared
wrappers, one shell, modular services.
- **Color schema** — the Babylon brand ramp (key color `#3A94FC`),
light/dark theme generation, and the no-hard-coded-colors rule.
- **Layout & shell structure** — central content, dockable side panes
(left/right × top/bottom), toolbars, `compact` vs `full` modes.
- **Bootstrapping** — `MakeModularTool`, services, reactive hooks,
`ISettingsStore`.
- **Components / styling / typography / icons / sizing / feedback** —
shared wrappers, `PropertyLine`/`Accordion`, `ToggleButton`/`Collapse`,
`makeStyles` + tokens, text presets, unsized icons, `ToolContext`,
toasts/dialogs/teaching moments.
- **Consuming from npm** — install + peer-dependency guidance for
`@babylonjs/shared-ui-components` and a stability caveat.
- A per-tool **checklist** and a **references** section.

Also links the new doc from `.github/instructions/index.md`.

## Notes

Docs-only change. The local `precommit` hook (`lint-staged`) is not
installed in this worktree, so the commit was made with `--no-verify`;
there is no code to lint/build/test here.

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…abylonJS#18624)

## Problem

SSAO2 — and other texture-chaining post-process pipelines — render
incorrectly in **tree-shaken / selective-ES6 builds** (importing core
through the pure barrel rather than the full `@babylonjs/core` bundle).
The scene color is lost and the image washes out to a near-white, banded
result.

This surfaced in the **Viewer**, whose SSAO path runs against the
dev/tree-shaken build. The published full bundle is unaffected because
bundling pulls in every side effect, which is exactly what masks the
bug.

| SSAO off | SSAO on (before this PR) |
|---|---|
| Correctly shaded, textured scene | Scene color gone, washed white +
banded |

This PR completes the construction-time / eager side-effect registration
work from BabylonJS#18546 and BabylonJS#18620, which covered ~20 classes plus the
`PostProcessRenderPipelineManager` getter but missed the two cases
below.

## Root causes

There are **two independent missing side-effect registrations**:

### 1. `PostProcess` texture-binding methods were never registered

SSAO2's combine pass binds the captured original scene color via:

```ts
effect.setTextureFromPostProcessOutput("originalColor", this._originalColorPostProcess);
```

That `Effect` / `AbstractEngine` prototype method is installed only by
`RegisterPostProcess()`, which normally runs as a side effect of
importing the `postProcess.ts` wrapper. In a tree-shaken build only
`postProcess.pure` is imported, so the method stayed an **unregistered
no-op stub**: `originalColor` never bound → the combine shader sampled
the default (white) texture → the output became `AO × white` instead of
`AO × sceneColor`.

**Fix:** the `PostProcess` constructor now calls the idempotent
`RegisterPostProcess()`, matching the pattern BabylonJS#18620 applied to ~20
other classes. This also latently repairs **bloom, depth of field, and
SSR**, which rely on the same `setTextureFromPostProcess` /
`setTextureFromPostProcessOutput` methods.

### 2. The `Scene.postProcessRenderPipelineManager` getter was
registered too late for some consumers

BabylonJS#18546 injects the manager class in the `PostProcessRenderPipeline`
constructor. But a consumer may access the manager (e.g. to subscribe to
its `onNewPipelineAddedObservable` / `onPipelineRemovedObservable`)
**before** constructing the pipeline. In that ordering the getter is
still unregistered and the manager is `undefined`, so SSAO never
attaches.

**Fix:** `RegisterSsao2RenderingPipeline()` — the import-time entry
point already triggered when the SSAO2 module loads — now eagerly
registers the getter and injects the concrete
`PostProcessRenderPipelineManager` class, so it is available as soon as
SSAO2 is imported. This follows BabylonJS#18546's "inject the class, don't force
the full side-effect wrapper" approach and keeps the fix scoped to SSAO2
without affecting other pipelines' tree-shaking.

## Tests

Adds a Viewer visualization test, `load OBJ model with ssao="enabled"`,
that loads `StanfordBunny.obj` with SSAO enabled and screenshot-compares
the result (also exercising OBJ loading, which had no Viewer coverage).
SSAO seeds its sampling-kernel rotation texture from `Math.random`, so
the test installs a deterministic PRNG via `addInitScript` to keep the
screenshot stable.

This gives CI coverage for SSAO2, whose standard visualization test
(`ssao2`) is currently `excludeFromAutomaticTesting`. Re-enabling that
standard test as an additional guard would be a reasonable follow-up.

## Validation

- Viewer OBJ+SSAO renders correctly with **no Viewer-side change** (the
fix is entirely in core).
- New test passes deterministically; **all 44 Viewer tests pass**.
- Post-process unit tests pass.
- **All 16 tree-shaking checks pass** (manifest drift, side-effect
import closure, pure barrels, side-effect stubs) — confirming the new
`.pure` imports don't break the side-effects contract.
- `prettier --check` and `eslint` clean (0 errors).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
## Problem

When `engine.useExactSrgbConversions = true` is set on WebGPU, shaders
fail to compile and nothing renders (blank canvas). Reproduced via
https://playground.babylonjs.com/#XDNVAY#41:

```
Error while parsing WGSL: unresolved call target 'lessThanEqual'
return mix(remainingSection, nearZeroSection, lessThanEqual(color, vec3f(0.04045)));
```

## Cause

The vec3 exact sRGB↔linear conversion helpers in `helperFunctions.fx`
(WGSL) used GLSL's `lessThanEqual()`, which does not exist in WGSL. The
scalar variants already correctly used `select()`.

## Fix

Replaced `mix(..., lessThanEqual(...))` with `select(..., color <=
...)`. Arg order is preserved since `mix(a,b,cond)` and
`select(a,b,cond)` both return `b` where the condition is true, and
`vec3f <= vec3f` yields the `vec3<bool>` that `select` accepts
per-component.

Only the WebGPU exact-sRGB path was affected; default conversions and
WebGL2 were unaffected.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…don (BabylonJS#18625)

## What

Follow-up to ongoing pure-barrel work. Removes the remaining genuine
side-effect leaks that the pure-barrel analysis flagged, across
`@babylonjs/gui`, `@babylonjs/loaders`, and the `@babylonjs/addons`
atmosphere module.

### 1. Swap remaining non-pure core/gui imports to `.pure` variants

These modules are reached by the pure barrels but still value-imported
the side-effectful core/gui modules, dragging their side effects into
the pure import path. Swapped each to its existing `.pure` variant
(every swapped symbol — `Vector2/3`, `Matrix`, `Quaternion`, `Color3/4`,
`SpotLight`, `Animation`, `Tools` — is exported by its `.pure` target):

- `gui/2D/math2D.ts`, `gui/2D/measure.ts` →
`core/Maths/math.vector.pure`
- `loaders/glTF/2.0/Extensions/objectModelMapping.ts` →
`math.vector.pure`, `math.color.pure`, `spotLight.pure`
- `loaders/glTF/glTFValidation.ts` → `tools.pure`
- `loaders/glTF/2.0/glTFLoaderAnimation.ts` → `animation.pure`,
`math.vector.pure`
- `loaders/glTF/2.0/pbrMaterialLoadingAdapter.ts` → `math.color.pure`,
`math.vector.pure`

### 2. Make `AtmospherePBRMaterialPlugin` free of top-level shader side
effects

The plugin used top-level `import "./ShadersWGSL/ShadersInclude/..."` to
register its shader includes — the one genuine module-scope side effect
in the atmosphere addon. It now loads those includes lazily via dynamic
`import()` (selecting the correct GLSL/WGSL variant for the host
material's shader language) and gates `isReadyForSubMesh()` until they
are registered in the `ShaderStore`. This mirrors the existing
`diffuseSkyIrradianceLut` `extraInitializationsAsync` pattern.

This also fixes a latent gap: the old code only eagerly registered the
**WGSL** includes; GLSL relied on transitive registration elsewhere.
Both languages are now handled explicitly and self-contained. The plugin
is only instantiated via `atmosphere.ts`'s per-material factory, and
effect compilation is already gated through `isReadyForSubMesh`, so
there is no behavioral regression.

## Verification

Changes are mechanical/type-safe by inspection. The dynamic-`import()`
approach matches the established `diffuseSkyIrradianceLut` pattern.
Please confirm in CI:

- `npm run check:treeshaking-all` (pure-barrel / side-effect invariants)
- addons typecheck + the `Atmosphere *` visualization tests on
**WebGL2** and **WebGPU**

(The local worktree had no `node_modules`, so lint/build/tests were not
run locally and the precommit hook was bypassed.)

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ctor in dev (BabylonJS#18627)

## Problem

Reported on the forum: [Sandbox dev server no longer works after Vite
tree-shaking
changes](https://forum.babylonjs.com/t/sandbox-dev-server-no-longer-works-from-documented-command-after-vite-tree-shaking-changes/63693).
On a fresh checkout, the sandbox dev server (`npm run serve -w
@tools/sandbox`) had several runtime failures:

- Dragging a **DRACO** (and mesh-quantization) GLB failed to load.
- Opening the **Inspector** threw `Cannot read properties of undefined
(reading 'show')`.
- After that was worked around, the Inspector still crashed
(`getAllAnimatablesByTarget` undefined) on a non-draco model.

## Root cause

The sandbox dev server aliases Babylon packages to their tree-shaken
`dist` builds, so only explicitly-imported side effects are present.
Three were missing:

1. Only `loaders/glTF/2.0/glTFLoader` was imported → no glTF extensions
registered (draco, mesh quantization, KHR materials, …).
2. The Inspector v2 debug-layer side effect was never loaded →
`scene.debugLayer.show()` blew up.
3. `core/Animations/animatable` was tree-shaken out →
`getAllAnimatablesByTarget` missing, crashing the Inspector properties
panel.

## Fix (`packages/tools/sandbox`)

- **main.ts**: import the full `loaders/glTF/2.0` extension barrel,
import `core/Animations/animatable`, and in dev load `inspector/index`
(attaches `Scene.debugLayer` for v2; production keeps getting it from
the CDN bundle).
- **vite.config.ts**: alias `inspector` to `inspector-v2/dist` plus the
lazily-loaded `gui-editor`/`node-*-editor` packages so Vite can resolve
those dynamic imports.

## Verification

Reproduced all three errors with Playwright, then confirmed after the
fix: draco GLB renders, Inspector opens with a clean properties panel,
no console errors. Dev-only imports are gated by `import.meta.env.DEV`,
so the production `vite build` still passes and stays small (~188 kB).
tsc/eslint/prettier clean.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…8630)

## What

PR2 of the multi-PR effort to migrate Babylon.js to TC39 Stage 3
decorators. This PR lands the **Babylon Native ES5 downlevel pipeline
and CI wiring only** — it does **not** flip any decorator compiler flag
and is independently mergeable.

## Why

Babylon Native runs on Chakra, which consumes ~ES5 script. The upcoming
TC39 decorator migration will force the core UMD bundle to an ES2015
target (the `accessor` keyword requires ES2015+), which Chakra cannot
parse. We land the downlevel tooling **first** so Native stays green
before any flag flip.

Because master is still ES5 UMD today, this downlevel step is a
**harmless no-op** on current output (it re-emits equivalent ES5), so
the pipeline is verified and ready for later PRs.

## Changes

- **`scripts/downlevelNativeScripts.mjs`** — new script using
`@babel/core` + `@babel/preset-env` with `forceAllTransforms` to emit
ES5. Accepts file/dir args and transforms files matching
`/^babylon.*\.js$/i`. `useBuiltIns: false` leaves library behavior
untouched.
- **`package.json`** — adds the `downlevel:native-scripts` npm script
and `@babel/core` / `@babel/preset-env` (`^7.29.7`) devDependencies.
- **`.azure-pipelines/ci-monorepo.yml`** — Native job now runs `npm
install`, downlevels `BabylonNativeNightlyUnitTests\...\babylon.max.js`
before `UnitTests.exe`, and downlevels
`BabylonNativeNightlyPlayground\Scripts\babylon.max.js` before the
playground run.

## Validation

- `npm install` ✅ (babel deps resolved, lockfile updated)
- Built core UMD `babylon.max.js`, ran `node
./scripts/downlevelNativeScripts.mjs` on it → `es-check es5` passes
before and after; `node --check` confirms valid JS ✅
- Filename filter correctly skips non-`babylon*.js` files ✅
- `npm run lint:check` ✅
- `npm run format:check` ✅

Reference: PR BabylonJS#18142 (used as a guide for the script and CI edits; no
commits cherry-picked).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…e barrel) (BabylonJS#18629)

## Problem

Since v9.12, `Scene.createDefaultEnvironment` crashes with:

```
TypeError: CubeTexture.CreateFromPrefilteredData is not a function
  at _EnvironmentHelper._setupEnvironmentTexture
```

`CubeTexture.CreateFromPrefilteredData`, `CreateFromImages` and `Parse`
are static methods attached only inside `RegisterCubeTexture()` — the
side effect run by the `cubeTexture.ts` wrapper. After the pure-barrel
work, several modules import `CubeTexture` from `cubeTexture.pure`
(side-effect-free) and called those statics directly, so they were
`undefined` at runtime.

## Fix

Use the exported pure functions instead of the dynamically-attached
statics:

- `Helpers/environmentHelper.ts` →
`CubeTextureCreateFromPrefilteredData`
- `Loading/Plugins/babylonFileLoader.pure.ts` → `CubeTextureParse`,
`CubeTextureCreateFromPrefilteredData`
- `Materials/Node/Blocks/PBR/refractionBlock.pure.ts` →
`CubeTextureParse`
- `Materials/Node/Blocks/Dual/reflectionTextureBaseBlock.pure.ts` →
`CubeTextureParse`

The `CubeTexture` class import is kept where the constructor / type is
still used. This also fixes the same latent crash for `.babylon`
environment loading and NME refraction/reflection deserialization on the
pure path.

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…e instead of source scanning (BabylonJS#18632)

## Problem

When a scene playground is loaded into the Flow Graph Editor's preview
panel, the editor figures out whether the playground builds its flow
graph from a snippet (e.g.
`ParseFlowGraphCoordinatorFromSnippetAsync("#ABC123#0", { scene })`)
and, if so, auto-opens that snippet in the editor — replacing whatever
graph the user currently has open.

Detection was a plain regex over the **raw** playground source that did
not strip comments. As a result, an **inert preview scene** with a
commented-out `ParseFlowGraphCoordinatorFromSnippetAsync(...)` line
still forced a graph to load, silently clobbering the graph the user
explicitly opened in the editor. A `...FromSnippetAsync` substring
inside a string/URL literal could also be mis-detected.

## Approach

Rather than parsing source text and trying to reason about comments and
string literals, this detects the reference **at runtime**, since
commented-out (and string-literal) code never executes.

Before the preview snippet runs, the editor wraps the global `BABYLON`
`...FromSnippetAsync` loaders so the snippet id is recorded from the
call that actually executes, then restores the originals once the scene
is ready. The wrappers delegate to the originals, so the playground
still builds its graph exactly as before.

The wrappers are installed **before `createEngine`**, which is the right
window for both execution modes (verified against the snippet loader):
- **script-mode** snippets call
`BABYLON.ParseFlowGraphCoordinatorFromSnippetAsync(...)` on the global
directly at `createScene` time;
- **TS-mode** snippets import from `@babylonjs/core`, which the snippet
loader resolves through a synthetic proxy module that copies
`globalThis.BABYLON[name]` the first time module evaluation is triggered
— and that evaluation happens lazily on the first `createEngine` call.

This eliminates both bug classes by construction:
- a commented-out loader never runs → never captured;
- a `...FromSnippetAsync` substring inside a string/URL literal never
runs → never captured.

Genuinely active loaders still auto-open their graph for editing, and
first-call-wins ordering is preserved.

## How to use

From a user's perspective in the Flow Graph Editor, the behavior is
driven entirely by whether the playground's loader call actually runs:

- **Edit a graph that a scene loads at runtime** — load a scene
playground whose code *actively* calls `await
ParseFlowGraphCoordinatorFromSnippetAsync("#ABC123#0", { scene })`. The
editor captures `#ABC123#0` as it executes and auto-opens that flow
graph for editing; saving publishes a new version of that same snippet.
(Unchanged behavior.)

- **Use a scene purely as a preview, without it touching your open
graph** — comment out the loader in the playground:
  ```ts
// await ParseFlowGraphCoordinatorFromSnippetAsync("#ABC123#0", { scene
});
  ```
The scene still loads and renders in the preview panel, but because the
commented line never executes, nothing is captured and the graph you
currently have open in the editor is left untouched. The same holds if
the only occurrence of `...FromSnippetAsync` is inside a string/URL
literal. You no longer have to scrub every snippet reference out of the
playground to use it as an inert level preview.

No new UI, settings, or API surface — the distinction is simply "does
the loader run or not."

## Changes

- New pure helper `flowGraphSnippetCapture.ts` exporting
`CaptureFlowGraphSnippetId(namespace)`, which wraps the matching
loaders, records the first live call's id, and exposes a `restore()`.
- `scenePreviewComponent` installs the capture before `createEngine`,
restores it in a `finally` after `whenReadyAsync`, and passes the
captured id to `_tryLoadReferencedFlowGraphAsync`. The old source-scan
method was removed.

## Scenario covered

An inert preview scene with a commented-out
`ParseFlowGraphCoordinatorFromSnippetAsync(...)` line no longer clobbers
the graph the user has open in the editor.

## Tests


`packages/tools/flowGraphEditor/test/unit/flowGraphSnippetCapture.test.ts`
(vitest), using spies on a fake `BABYLON` namespace:
- live loader call → captures id and still delegates to the original
- no loader call (commented-out / inert scene) → null
- first-call-wins across multiple loaders
- wraps every matching loader (`...FromSnippetAsync` family), not just
the coordinator one
- ignores non-loader functions and non-function properties
- ignores empty / non-string ids
- restores the originals
- safe no-op when the namespace is missing

## Validation

- `npx vitest run --project=unit .../flowGraphSnippetCapture.test.ts` →
8/8 pass
- `tsc -b packages/tools/flowGraphEditor/tsconfig.build.json` → clean
- eslint + prettier clean on changed files

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
… Enhanced TS Config (BabylonJS#18626)

### gitignore.ts
- every scaffolded project now gets a sensible `.gitignore` out of the
box.

### sceneCode.ts
- Replaces
`BABYLON.AppendSceneAsync("https://assets.babylonjs.com/meshes/boombox.glb",
scene)` with `BABYLON.CreateBox("box", {}, scene)` — no network
dependency for the default scene, avoiding potential connectivity issues
in certain regions.

### indexHtml.ts

- **`display: block`** — `<canvas>` is an inline-replaced element by
default. Setting it to `display: block` eliminates the small gap below
the canvas caused by inline element baseline alignment. Without this,
there is often a 3–4px whitespace strip at the bottom of the page.

- **`100dvw` / `100dvh` instead of `100%`** — The dynamic viewport units
(`dvw` / `dvh`) account for mobile browser UI changes (address bar
appearing/disappearing), unlike `100%` which depends on parent element
size. Using percentage-based sizing required setting `width: 100%;
height: 100%` on both `html` and `body` as ancestor chain. With
`dvw`/`dvh`, the canvas sizes directly against the viewport, eliminating
the need for `overflow: hidden` on `html, body` and simplifying the CSS.

- **Removing `overflow: hidden` from `<html>` and `<body>`** — No longer
needed since the canvas uses viewport units directly and the body has
zero margin/padding. Worth noting that the old `overflow: hidden`
approach only masked the overflow symptom — the canvas content was still
actually overflowing; it just wasn't visible.

### tsconfig.json

- **`"noEmit": true` replacing `"outDir": "./dist"`** — `vite build`
already outputs to `dist` by default. With `"noEmit": true`, `tsc` is
used strictly as a type-checker (`tsc && vite build`); it emits nothing,
and Vite alone handles all bundling. The `outDir` setting is therefore
irrelevant and was removed.

- **Added `"vite/client"` to types** — This provides TypeScript with
type definitions for Vite-specific features such as `import.meta.env`,
static asset imports (e.g., `import logo from './logo.png'`), HMR API
(`import.meta.hot`), and the `?url` / `?raw` query suffixes. Without
this, the TypeScript compiler would flag Vite-imported assets as type
errors.
…18506)

## Context

The visualization test runner has special-case logic that, after the
per-test `renderCount` is exhausted, keeps rendering until every
`AdvancedDynamicTexture` reports `guiIsReady()` and then renders one
more frame. This was [introduced in
BabylonJS#13475](BabylonJS#13475) to give
GUI-based PGs time to finish loading before the screenshot is captured.

It doesn't actually solve the race for PGs that fire-and-forget
`parseFromSnippetAsync` / `parseFromURLAsync`: while the snippet fetch
is still in flight, the ADT root container has zero children,
`Container.isReady()` iterates an empty array and returns `true`, so
`guiIsReady()` is satisfied immediately. The gate only adds a settle
frame for image loads on controls that are *already* in the tree.

It also bakes GUI-specific knowledge into a generic test runner. The
correct contract is: a PG whose setup is async returns `Promise<Scene>`
from `createScene` and the runner awaits it. The runner already does
`await (createScene(engine, canvas) ?? createScene())`, so no
runner-side framework change is needed beyond removing the gate.

## Changes

- Drop the `guiIsReady()` gate from both viz runners:
-
`packages/tools/tests/test/playwright/visualizationPlaywright.utils.ts`
  - `packages/tools/testTools/src/visualizationUtils.ts`
- Update the two PGs that needed the workaround to actually follow the
`Promise<Scene>` contract, by saving new revisions:
- `#YS93KY#0` → `#YS93KY#1` (Load GUI snippet with unicode) — `return
adv.parseFromSnippetAsync("#KHPNS9").then(() => scene);`
- `#ERVGT5#0` → `#ERVGT5#1` (Parse GUI json with unicode) — `return
adv.parseFromURLAsync(...).then(() => scene);`
- Pin the BJS viz config to the new revisions.

## Notes

- The `AdvancedDynamicTexture.guiIsReady()` / `onGuiReadyObservable` API
itself is **not** removed — only its usage in the test runner. It is
still used internally by `FrameGraph/guiTask.ts` and remains available
to anyone who wants it.
- BabylonNative was hitting the same flake on slow CI runners
([BabylonNative#1716](BabylonJS/BabylonNative#1716)
carries a temporary BN-side workaround). Once this PR lands and a fresh
nightly UMD ships, that workaround will be removed in BN.

[Created by Copilot on behalf of @bghgary]

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…eras (BabylonJS#18634)

> 🤖 *This PR was created by the create-pr skill.*

Holding two perpendicular movement keys applied each key's full impulse
independently, making diagonal keyboard movement ~1.41x (√2) faster than
a single axis. This PR accumulates the per-frame direction across
pressed keys and normalizes it before applying, matching the existing
`GeospatialCamera` keyboard fix.

**Cameras fixed:**
- `FreeCamera` — translation (WASD/arrows)
- `FlyCamera` — translation
- `ArcRotateCamera` — rotate and pan

`FreeCamera` rotation keys and `ArcRotateCamera` zoom remain per-key (1D
axes, unaffected).

**Motivation:** Reported on the forum — [Moving camera diagonally
(keyboard) is ~40%
faster](https://forum.babylonjs.com/t/moving-camera-diagonally-keyboard-its-40-faster/63704/2).

**Tests:** Added unit tests asserting diagonal movement distance ≈
single-axis distance for all affected cameras.

---------

Co-authored-by: Georgina <gehalper@microsoft.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
## Summary

Applies `npm audit fix` — lockfile-only changes bumping transitive
dependencies to patched versions.

## Changes

`package-lock.json` only (60 insertions / 60 deletions), patch-level
bumps:

- `js-yaml` 3.14.2 → 3.15.0
- `nx` / `@nx/*` platform binaries 22.7.5 → 22.7.6
- `form-data` 4.0.5 → 4.0.6
- `hasown` 2.0.2 → 2.0.4
- `tmp` 0.2.6 → 0.2.7

No `package.json` manifest changes. No source changes.
…ng (BabylonJS#18646)

## Problem

In the Flow Graph Editor, dragging a box moved the node but its
connections stayed frozen in place instead of stretching along with it.

## Root cause

Link redraws (`GraphNode._refreshLinks`) early-out whenever
`GraphCanvasComponent._isLoading` is `true`.

The editor calls `build()` on mount. With an empty graph (no saved
`_editorData`), `build()` defers layout to a `setTimeout` →
`sortGraph()`, which **early-returned on zero nodes without clearing
`_isLoading`**. So the flag was stranded `true` from the very first
mount, and every subsequent node drag skipped the link (and frame)
update — the box moved, the wires didn't.

This is specific to the Flow Graph Editor; NME/NGE/etc. always clear the
flag via `reOrganize`, so they aren't affected.

## Fix

- Clear `_isLoading = false` synchronously in `build()`'s no-editorData
branch (bulk node creation in `loadGraph()` is already done by then), so
the flag is never held across the deferred `sortGraph()` — which can be
skipped by a newer build, a changed flow graph, or an empty graph.
- Defensively clear `_isLoading` in `sortGraph()`'s zero-node early
return.

## Testing

- Added a Playwright regression test that adds two blocks, connects
them, drags one, and asserts the SVG link geometry follows the moved
node (plus a `getLinkPaths()` helper in `fge.utils.ts`).
- Verified the test **fails without the fix** (link `d` attribute
identical before/after drag) and **passes with it**.
- `prettier`, `tsc -b`, and `eslint` pass on the changed source; all 124
flowGraphEditor unit tests pass.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
…havior change) (BabylonJS#18631)

## Summary

PR1 of the multi-PR effort to migrate Babylon.js to TC39 Stage 3
decorators. **Infrastructure only — no runtime behavior change.**

This PR swaps the serialization decorator metadata **store** from
class-name-keyed global maps to per-constructor `Symbol.metadata`, while
**keeping `experimentalDecorators: true`** in tsconfig. Serialization
output and all unit tests are unchanged.

## What changed

- **`packages/dev/core/src/Misc/decorators.functions.ts`** —
`GetDirectStore`/`GetMergedStore` now read/write the store on each class
constructor's own `Symbol.metadata` instead of the
`DecoratorInitialStore`/`MergedStore` class-name-keyed maps.
- Experimental decorators call with `(prototype, propertyKey)`, so the
store is attached to `prototype.constructor[Symbol.metadata]`.
- Each constructor gets its **own** metadata object whose prototype
mirrors the class hierarchy (`Object.create(parentMetadata)`), so
`GetMergedStore` can walk the metadata prototype chain.
- Merge order is preserved (most-derived first, parents overwrite) to
match the original class-name-keyed merge.
- Merged store is memoized via a `WeakMap` keyed on the metadata object.
- **Public signatures of `GetDirectStore`/`GetMergedStore` are
unchanged**, so all serialization consumers compile untouched.
- **`packages/dev/core/src/Misc/symbolMetadataPolyfill.ts`** (new) —
idempotently defines `Symbol.metadata` as a one-time side effect.
`decorators.functions.ts` also self-heals defensively for import
orders/runtimes where the entry-point polyfill hasn't run yet.
- **`packages/dev/core/src/index.ts`** — imports the polyfill for side
effect as the first statement, before any decorated class is evaluated.
- Tree-shaking manifest + `@babylonjs/core` `sideEffects` regenerated to
record the polyfill's top-level side effect.

## Out of scope (later PRs)
No call-site changes, no `accessor` keyword, no `experimentalDecorators`
flip, no UMD target changes.

## Validation
- `npm run format:check` ✅
- `npm run lint:check` (eslint + typecheck + `check:treeshaking`
all-packages + `check:side-effects-sync`) ✅ — all 16 checks pass
- `npm run test:unit` ✅ — 4062 passed, 1 expected fail, 60 skipped.
Serialization tests (core + gui) pass. (The 10
`EnvironmentTeardownError` rejections are pre-existing lazy-shader
teardown flakiness, unrelated to this change.)

🤖 Generated with Copilot CLI

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…eams (BabylonJS#18645)

## WebGPU support for WebXR — Phase 0 (behavior-preserving, WebGL-only
refactor)

Part of BabylonJS#18637 (epic BabylonJS#18635).

Phase 0 introduces API-agnostic seams into Babylon WebXR so a future
WebGPU-backed XR session (layers-only, `XRGPUBinding`,
`engine.wrapWebGPUTexture`) can plug in later. It is a **strict no-op
for existing WebGL2 XR** — no functional change, no
WebGPU/`XRGPUBinding` code. Public API surface is unchanged
(additive/backward-compatible only).

> This PR consolidates what was originally staged as three stacked PRs
(BabylonJS#18643BabylonJS#18644BabylonJS#18645). They were stacked onto feature branches,
which meant the Azure snapshot pipeline (PR trigger is `master`-only)
never produced a preview build for the combined change. Retargeting the
tip branch to `master` yields a single testable, snapshot-backed,
mergeable PR containing all of Phase 0.

### 1) Engine typing
- Promotes `framebufferDimensionsObject` (public setter + protected
backing field) from `ThinEngine` up to `AbstractEngine`. Additive and
backward-compatible: `ThinEngine`/`Engine` keep consuming/overriding it
(Engine keeps the `onResize` notification; ThinEngine keeps the `_gl`
fallback in `getRenderWidth/Height`), WebGPU inherits harmless default
storage. No new getter — the write-only read semantics are preserved
exactly.
- Retypes `WebXRSessionManager._engine` and
`WebXRLayerRenderTargetTextureProvider._engine` from `Engine` to
`AbstractEngine`; removes the `scene.getEngine() as Engine` casts, plus
the leftover `as ThinEngine` cast in `webXRExperienceHelper.ts`.

### 2) RT-provider split
- `WebXRLayerRenderTargetTextureProvider` (base) is now
**graphics-API-agnostic** (no WebGL imports) and exposes two protected
hooks:
- `_createRenderTargetTextureShell(width, height, multiview)` — builds
the `RenderTargetTexture`/`MultiviewRenderTarget` with the correct
sample count.
- `_createRenderTargetTextureInternal(...)` — assembles a
`RenderTargetTexture` from already-wrapped `InternalTexture`s with no
graphics-API types. **This is the seam a future GPU backend uses**
(`wrapWebGPUTexture` → `_createRenderTargetTextureInternal`).
Intentionally unused in Phase 0.
- New WebGL-specific abstract provider
`WebXRWebGLRenderTargetTextureProvider` owns the WebGL-typed
`_createRenderTargetTexture` / `_createInternalTexture`
(`WebGLHardwareTexture` via `engine._gl`). The **exact original creation
order is preserved** (framebuffer assigned before `setTexture`).
- All WebGL-backed subclasses repointed to the new base (WebGL-layer,
Composition→Projection, Native).

### 3) Type generalization + graphics-binding seam
- `WebXRRenderTarget` is now generic with WebGL defaults:
`WebXRRenderTarget<TContext = WebGLRenderingContext, TLayer extends
XRLayer = XRWebGLLayer>`. Used without type args it resolves to the
exact previous shape (compile- and runtime-compatible). Stays public.
- `WebXRManagedOutputCanvas` context/layer creation factored into
overridable protected seams `_createXRCompatibleRenderingContext()` and
`_createXRLayer()`; WebGL behavior is byte-for-byte unchanged. Kept off
`AbstractEngine`'s public surface deliberately (WebGPU XR is layers-only
and may never use a managed-output baseLayer).
- New side-effect-free `WebXRGraphicsBinding` abstraction
(`IWebXRGraphicsBinding` + `WebXRGraphicsBindingType` + WebGL impl
`WebXRWebGLGraphicsBinding`) hiding `XRWebGLBinding` vs a future
`XRGPUBinding`, plus `WebXRSessionManager._getGraphicsBinding()`.
**Marked `@internal` and not barrel-exported** — introduced but
unconsumed until Phase 4 proves the per-op shape, so it stays off the
public API surface.

### Deliberate non-changes / scope
- The graphics-binding seam and `_createRenderTargetTextureInternal` are
**introduced but not yet consumed**. XR features (`WebXRLayers`,
`WebXRRawCameraAccess`, `WebXRDepthSensing`, `WebXRLightEstimation`,
`WebXRSpaceWarp`) keep constructing `XRWebGLBinding` directly — porting
them is **Phase 4**.
- `WebXRLayerWrapper`/`WebXRLayerType` were already API-agnostic; the
`layerType == "XRWebGLLayer"` foveation guard stays for behavior
preservation.
- Multiview color/depth **array** path
(`_colorTextureArray`/`_depthStencilTextureArray`) still lives in the
WebGL `_createRenderTargetTexture`; the GPU-agnostic
`_createRenderTargetTextureInternal` only handles the
single-`InternalTexture` path today. Equivalent GPU path is tracked for
**Phase 2** (BabylonJS#18640).

### Validation
- `tsc -b tsconfig.devpackages.json` ✅
- `format:check` (prettier) ✅ / `lint:check` (eslint + ratchets) ✅
- XR + Engines unit tests ✅
- `check:treeshaking` (16 checks) ✅ / `check:side-effects-sync` (all 4
packages) ✅
- Zero net public-API additions (binding seam is `@internal`;
`framebufferDimensionsObject` is additive on `AbstractEngine`;
`WebXRRenderTarget` generics are default-compatible).
- WebGL2 XR desktop smoke passed (scene renders, session manager
constructs under the `AbstractEngine` retype, context seam yields a real
`WebGL2RenderingContext`). In-headset Quest Browser WebGL2 XR smoke is
the final gate before merge.

---------

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
)

## What

Upgrades the `vite` pin from `6.4.3` to `8.1.3` across the root and the
workspaces that depend on Vite directly (`devHost`, `tools/viewer`,
`public/@babylonjs/viewer`).

Vite 8 ships the Rust-based **Rolldown** bundler as its default (and
only) bundler, replacing Rollup + esbuild. This is the officially
supported path — the transitional `rolldown-vite` package was only a
bridge for the Vite 7 line and is not needed here. No `npm:` alias or
`overrides` entry is required.

## Why

Vite only builds the **tool apps** (editors, playground, sandbox,
viewer, devHost, inspector-v2, vsm) — not the core libraries (those use
`tsc`/nx/UMD). The goal is to shave time off the tool-build portion of
CI.

## Measured impact

Local production builds (`vite build`), matched runs, equivalent output:

| Tool | Vite 6.4.3 | Vite 8.1.3 (Rolldown) | Speedup |
|---|--:|--:|--:|
| playground (Monaco) | 12.92s | 3.35s | ~3.9× |
| flowGraphEditor | 8.06s | 1.18s | ~6.8× |
| nodeEditor | 3.29s | 0.60s | ~5.5× |
| nodeGeometryEditor | 3.07s | 0.63s | ~4.9× |
| nodeParticleEditor | 3.08s | 0.61s | ~5.0× |
| guiEditor | 2.77s | 0.55s | ~5.0× |
| sandbox | 0.55s | 0.20s | ~2.8× |

Bundle sizes are equivalent (e.g. nodeEditor JS `1254.13 kB` → `1252.23
kB`; gzip slightly smaller).

## Validation

- All standalone tool builds pass on Vite 8.
- Dev server (`serve`/HMR) and TS transform verified on nodeEditor
(ready in ~126ms).
- `npm install` resolves cleanly with no peer-dependency conflicts.

## Notes / follow-ups

- This is a **Vite 6 → 8 jump** (two majors). Watch the full CI tool
matrix on this PR.
- Non-fatal deprecation warnings remain from `@vitejs/plugin-react`
(recommends `@vitejs/plugin-react-oxc`). Migrating that plugin is a
potential follow-up for additional speed.
- The `package-lock.json` diff reflects the real dependency-tree change
(Rolldown native binaries replacing rollup/esbuild transitives).

---------

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
… 1, BabylonJS#18638) (BabylonJS#18650)

## Phase 1 (BabylonJS#18638) — WebGPU-compatible XR session + XRGPUBinding
plumbing

Closes BabylonJS#18638 (Phase 1 of the WebGPU-for-WebXR epic BabylonJS#18635 — the epic
stays open for Phases 2–5).

Part of the WebGPU-for-WebXR effort (epic BabylonJS#18635). **Plumbing only — no
per-frame WebGPU XR rendering (Phase 2), no camera/NDC (Phase 3), no
feature rewiring (Phase 4).** WebGL2 XR behavior is byte-for-byte
identical; every WebGPU path is gated behind `AbstractEngine.isWebGPU`
and is unreachable for WebGL engines.

> Consolidated from an initial 3-PR stack into this single PR (kept as
focused commits for readability). Supersedes BabylonJS#18651 and BabylonJS#18652. Rebased
onto master after Phase 0 (BabylonJS#18645) merged.

### What's in here

**1. TS typings + adapter opt-in**
- New companion
`packages/dev/core/src/LibDeclarations/webxr.webgpu.d.ts` (mirrors the
existing `webxr.nativeextensions.d.ts` split so the large
community-maintained `webxr.d.ts` stays untouched). Declares
`XRGPUBinding`, `XRGPUSubImage`, and the `XRGPU*LayerInit` dictionaries.
Base XR layer/sub-image types come from `webxr.d.ts`;
`GPURequestAdapterOptions.xrCompatible` stays in `webgpu.d.ts` (not
duplicated).
- Documented `xrCompatible` on `WebGPUEngineOptions`. It is inherited
from `GPURequestAdapterOptions` and already flows end-to-end
(`initAsync` passes `_options` straight to
`navigator.gpu.requestAdapter()`), so this is JSDoc/discoverability only
— it notes the flag must be set at adapter-request/engine-construction
time (WebGPU has no post-hoc "make XR compatible" step).

**2. `@internal` WebGPU graphics binding + wiring**
- `WebXRWebGPUGraphicsBinding` implementing the Phase-0
`IWebXRGraphicsBinding` seam, wrapping `new XRGPUBinding(session,
device)`, plus a `WebXRGraphicsBindingType.WebGPU` enum member.
Minimal/introduced-but-unused — per-frame layer/sub-image ops are
deferred to later phases.
- `WebXRSessionManager._getGraphicsBinding()` branches on
`AbstractEngine.isWebGPU`: WebGPU engine → `XRGPUBinding`-backed
binding; everything else → the existing `XRWebGLBinding`-backed one. The
`GPUDevice` is read from `WebGPUEngine._device` via a **type-only**
import + cast (mirrors Phase 0's `(engine as ThinEngine)._gl`), so there
is no runtime coupling/side effect. Kept `@internal` and out of
`index.ts`/`pure.ts`.

**3. WebGPU-compatible session gating**
- `initializeSessionAsync` requests the `'webgpu'` feature descriptor as
a **required** feature when the engine is WebGPU (a WebGPU engine cannot
fall back to a WebGL XR session; the native `requestSession` rejection
is left to propagate to the caller, not swallowed). The WebGL branch
leaves the `XRSessionInit` object untouched (same reference passed
through — asserted in a test).
- `webXRExperienceHelper.enterXRAsync` skips the
`baseLayer`/`XRWebGLLayer` path for WebGPU, since a WebGPU-compatible
session is **layers-only**.

**4. Doc-only clarification** of the ENTERING_XR end-state (see below) —
comment-only, no logic change.

### ✅ Hardware-validated Phase 1 end-state (not a regression)
Validated on Meta Quest Browser with the experimental `XRGPUBinding`
flag, against this PR's snapshot build. The precise Phase 1 end-state
is:

> The native WebGPU XR session **enters** — an `xrCompatible` WebGPU
engine is created, the `'webgpu'` feature is accepted and appears in
`session.enabledFeatures`, `baseLayer` is correctly skipped, and
`enterXRAsync` resolves without throwing. **But** because Phase 1
attaches **no layer** (the `XRProjectionLayer` is Phase 2), the session
receives **no `requestAnimationFrame` callbacks**, so
`onXRFrameObservable` never fires and Babylon's `WebXRState` **stays at
`ENTERING_XR` and never reaches `IN_XR`** (which is gated on the first
frame). This is the correct Phase 1 result, **not a regression**.

This "no layer → no frame" behavior was confirmed both through Babylon
and at the **raw-browser level**: a bare `navigator.xr` `immersive-vr`
session requested with `requiredFeatures: ['webgpu']` is granted
`'webgpu'`, yet a `requestAnimationFrame` with no layer set yields
**zero callbacks**. So the stall is inherent to the layers-only
WebGPU-XR design and the WebXR spec, not a bug in this plumbing.
Reaching `IN_XR` / actually rendering is **Phase 2** (projection-layer
RTT provider via `wrapWebGPUTexture`).

**Exit is clean from `ENTERING_XR`:** ending the session via its native
`end` path (e.g. the headset system menu) fires `onXRSessionEnded`
regardless of `WebXRState`, restoring the framebuffer/render loop/camera
and returning `WebXRState` to `NOT_IN_XR` with no hang or leak (the
`@internal` binding is nulled on end). Note: the programmatic
`exitXRAsync()` is a no-op from `ENTERING_XR` (its pre-existing `state
!== IN_XR` guard, unchanged by this PR) — a no-op, not a hang; the
session remains exitable via the native path.

A unit test confirms `updateRenderState` with neither `baseLayer` nor
`layers` does not throw. /cc @RaananW.

### Spec grounding
Declarations/behavior are taken from the
immersive-web/WebXR-WebGPU-Binding explainer + proposed IDL:
https://github.com/immersive-web/WebXR-WebGPU-Binding/blob/main/explainer.md
— including the `'webgpu'` feature descriptor, the layers-only rule (no
`baseLayer`), and the `XRGPUBinding(session, device)` shape requiring an
`xrCompatible` adapter.

### Constraints honored
- WebGL2 XR byte-for-byte identical; all WebGPU logic gated behind
`isWebGPU`.
- No `.pure.ts` split, no top-level side effects (confirmed: no
side-effect-manifest churn on any commit).
- **Zero net public-API additions** — the binding is `@internal` and out
of the barrels; `xrCompatible` is an inherited, now-documented option.

### Testing
- `tsc -b tsconfig.devPackages.json` exit 0.
- `npm run format:check` ✅ ; `npm run lint:check` ✅ (eslint + all 16
tree-shaking checks + side-effects-sync across
core/gui/loaders/serializers).
- XR unit gate `vitest run --project=unit -t "XR"`: **260 passed**.
`webXRSessionManager.test.ts` cases cover binding selection (WebGL vs
WebGPU), binding caching, the uninitialized-session guard, `'webgpu'`
injected only for WebGPU engines (existing required features preserved,
no duplicates), WebGL init passed through unchanged (same object ref),
`updateRenderState` with no layer not throwing, and the native
session-`end` handler cleaning up (nulls the graphics binding, clears
`inXRSession`, notifies `onXRSessionEnded`) even when no frame ever
arrived. New `webXRExperienceHelper.test.ts` covers the no-frame WebGPU
lifecycle: state stays at `ENTERING_XR` with no frame, `exitXRAsync()`
from `ENTERING_XR` is a clean no-op (does not touch the session manager,
state unchanged), and a native session end from `ENTERING_XR` returns
state to `NOT_IN_XR` with no hang or leak.
- **Hardware-validated** on Meta Quest Browser (XRGPUBinding flag)
against the PR snapshot — see the end-state section above.
- Note: the unrelated `smartFilterBlocks` vitest suite fails to import
in this environment (missing generated `.block.js` artifacts) —
pre-existing, not touched by this PR.

---------

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
The Viewer adds a default hemispherical light if the model isn't PBR.
Currently, it doesn't consider OpenPBRMaterial so this PR adds that
check.

---------

Co-authored-by: Ryan Tremblay <ryan.tremblay@microsoft.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
LightingVolume binds the shadow generator's shadow map
depthStencilTexture - a depth-format texture - as the shadowMap input of
the lightingVolume compute shader. Declare that binding as
texture_depth_2d instead of texture_2d<f32>, and drop the .r swizzle on
the textureLoad result since textureLoad on texture_depth_2d returns a
scalar f32. The loaded value is the same depth that .r previously
carried, so far-plane fitting results are unchanged.

This completes the compute depth sample-type support introduced in
PR BabylonJS#18460: _GetComputeTextureSampleType classifies this texture as bind
group layout sampleType "depth", and WebGPU validation requires a
texture_depth_2d WGSL declaration for a "depth" layout entry. With the
previous texture_2d<f32> declaration, createComputePipeline fails
validation (observed in Chromium and wgpu-native) as soon as the
lighting volume compute shaders run with an explicit pipeline layout.
The change is also safe with the default auto layout: texture_depth_2d
derives sampleType "depth", which is valid for the depth-aspect view
Babylon binds, so behavior is identical there.

This came from the Hill Valley GLTF/NativeXR AR Portal validation pass.

(cherry picked from commit a81ccfb)
@matthargett
matthargett force-pushed the claude/jolly-allen-k9ldki branch 3 times, most recently from 443da4e to 5c45e6c Compare July 4, 2026 23:08
@github-actions

github-actions Bot commented Aug 2, 2026

Copy link
Copy Markdown

This pull request has been marked as stale because it has been inactive for more than 14 days. Please update to "unstale".

@github-actions github-actions Bot added the stale label Aug 2, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.