diff --git a/src/core/handlers/xrEntryHandler.ts b/src/core/handlers/xrEntryHandler.ts index 6210e10..a9a095c 100644 --- a/src/core/handlers/xrEntryHandler.ts +++ b/src/core/handlers/xrEntryHandler.ts @@ -1,6 +1,7 @@ -import { AbstractEngine, FreeCamera, Vector3 } from "@babylonjs/core"; +import { AbstractEngine, FreeCamera, Vector3, WebGPUEngine } from "@babylonjs/core"; import { DefaultScene } from "../defaultScene"; import { LevelConfig } from "../../levels/config/levelConfig"; +import { isWebGPUXRAvailable, createWebGPURenderTarget, getWebGPUSessionInit } from "../xr-webgpu/xrGpuEntryPoint"; import log from '../logger'; /** @@ -17,10 +18,9 @@ export async function enterXRMode( try { prePositionCamera(config); - const session = await DefaultScene.XR.baseExperience.enterXRAsync( - 'immersive-vr', - 'local-floor' - ); + const session = isWebGPUXRAvailable(engine) + ? await enterWebGPUXR(engine as WebGPUEngine) + : await enterWebGLXR(); log.debug('XR session started successfully'); return session; } catch (error) { @@ -30,6 +30,16 @@ export async function enterXRMode( } } +async function enterWebGPUXR(engine: WebGPUEngine): Promise { + const base = DefaultScene.XR!.baseExperience; + const gpuTarget = createWebGPURenderTarget(base.sessionManager, engine, DefaultScene.MainScene); + return base.enterXRAsync('immersive-vr', 'local-floor', gpuTarget, getWebGPUSessionInit()); +} + +async function enterWebGLXR(): Promise { + return DefaultScene.XR!.baseExperience.enterXRAsync('immersive-vr', 'local-floor'); +} + function prePositionCamera(config: LevelConfig): void { const spawnPos = config.ship?.position || [0, 0, 0]; const cockpitPosition = new Vector3(spawnPos[0], spawnPos[1] + 1.2, spawnPos[2]); diff --git a/src/core/sceneSetup.ts b/src/core/sceneSetup.ts index 3ee7b8b..2007517 100644 --- a/src/core/sceneSetup.ts +++ b/src/core/sceneSetup.ts @@ -49,18 +49,40 @@ export async function setupScene( } async function createEngine(canvas: HTMLCanvasElement): Promise { - let engine: AbstractEngine; - if (useWebGPU) { - log.info('[Engine] Creating WebGPU engine'); - log.warn('[Engine] WebXR/VR is still experimental with WebGPU engine'); - engine = await WebGPUEngine.CreateAsync(canvas, { antialias: true }); - } else { - log.info('[Engine] Creating WebGL engine'); - engine = new Engine(canvas, true); + const engine = useWebGPU + ? await tryCreateWebGPUEngine(canvas) + : null; + const finalEngine = engine ?? createWebGLEngine(canvas); + finalEngine.setHardwareScalingLevel(1 / window.devicePixelRatio); + window.onresize = () => finalEngine.resize(); + return finalEngine; +} + +async function tryCreateWebGPUEngine(canvas: HTMLCanvasElement): Promise { + if (!navigator.gpu) { + log.warn('[Engine] WebGPU requested but navigator.gpu not available'); + return null; } - engine.setHardwareScalingLevel(1 / window.devicePixelRatio); - window.onresize = () => engine.resize(); - return engine; + const adapter = await navigator.gpu.requestAdapter(); + if (!adapter) { + log.warn('[Engine] No WebGPU adapter found'); + return null; + } + log.info(`[Engine] WebGPU adapter: ${adapter.info?.vendor ?? 'unknown'}`); + try { + const gpuEngine = new WebGPUEngine(canvas, { antialias: true }); + await gpuEngine.initAsync(); + log.info('[Engine] WebGPU engine ready — WebXR will use XRGPUBinding if available'); + return gpuEngine; + } catch (e) { + log.error('[Engine] WebGPU initialization failed', e); + return null; + } +} + +function createWebGLEngine(canvas: HTMLCanvasElement): AbstractEngine { + log.info('[Engine] Creating WebGL engine'); + return new Engine(canvas, true); } function createMainScene(engine: AbstractEngine): void { diff --git a/src/core/xr-webgpu/xrGpuEntryPoint.ts b/src/core/xr-webgpu/xrGpuEntryPoint.ts new file mode 100644 index 0000000..7b5f52d --- /dev/null +++ b/src/core/xr-webgpu/xrGpuEntryPoint.ts @@ -0,0 +1,28 @@ +/** + * Public API for WebGPU XR support. + * Consumed by xrEntryHandler.ts to conditionally use the WebGPU XR path. + */ +import { WebGPUEngine } from "@babylonjs/core"; +import type { AbstractEngine, Scene, WebXRSessionManager } from "@babylonjs/core"; +import { XRGPURenderTarget } from "./xrGpuRenderTarget"; +import { buildWebGPUSessionInit } from "./xrGpuSessionSetup"; +import "./xrGpuTypes"; // ensure global XRGPUBinding augmentation is loaded + +export function isWebGPUXRAvailable(engine: AbstractEngine): boolean { + return ( + engine instanceof WebGPUEngine && + typeof globalThis.XRGPUBinding !== 'undefined' + ); +} + +export function createWebGPURenderTarget( + sessionManager: WebXRSessionManager, + engine: WebGPUEngine, + scene: Scene +): XRGPURenderTarget { + return new XRGPURenderTarget(sessionManager, engine, scene); +} + +export function getWebGPUSessionInit(): XRSessionInit { + return buildWebGPUSessionInit(); +} diff --git a/src/core/xr-webgpu/xrGpuLayerWrapper.ts b/src/core/xr-webgpu/xrGpuLayerWrapper.ts new file mode 100644 index 0000000..a90dc8e --- /dev/null +++ b/src/core/xr-webgpu/xrGpuLayerWrapper.ts @@ -0,0 +1,32 @@ +/** + * WebXRLayerWrapper subclass for XRProjectionLayer created via XRGPUBinding. + */ +import { WebGPUEngine } from "@babylonjs/core"; +import { WebXRLayerWrapper } from "@babylonjs/core/XR/webXRLayerWrapper"; +import type { Scene } from "@babylonjs/core"; +import { XRGPUTextureProvider } from "./xrGpuTextureProvider"; +import type { XRGPUBinding } from "./xrGpuTypes"; + +export class XRGPUProjectionLayerWrapper extends WebXRLayerWrapper { + constructor( + public override readonly layer: XRProjectionLayer, + private readonly _gpuBinding: XRGPUBinding, + private readonly _gpuEngine: WebGPUEngine, + private readonly _scene: Scene + ) { + super( + () => layer.textureWidth, + () => layer.textureHeight, + layer, + "XRProjectionLayer", + (sessionManager) => + new XRGPUTextureProvider( + sessionManager.scene, + this, + _gpuBinding, + layer, + _gpuEngine + ) + ); + } +} diff --git a/src/core/xr-webgpu/xrGpuRenderTarget.ts b/src/core/xr-webgpu/xrGpuRenderTarget.ts new file mode 100644 index 0000000..e306cc7 --- /dev/null +++ b/src/core/xr-webgpu/xrGpuRenderTarget.ts @@ -0,0 +1,54 @@ +/** + * WebXRRenderTarget implementation for WebGPU. + * Creates XRGPUBinding + projection layer instead of XRWebGLLayer. + */ +import { WebGPUEngine } from "@babylonjs/core"; +import type { Scene, Nullable, WebXRSessionManager, WebXRRenderTarget } from "@babylonjs/core"; +import { XRGPUProjectionLayerWrapper } from "./xrGpuLayerWrapper"; +import { getGpuDeviceFromEngine } from "./xrGpuSessionSetup"; +import type { XRGPUBinding, XRGPUBindingConstructor } from "./xrGpuTypes"; +import log from '../logger'; + +export class XRGPURenderTarget implements WebXRRenderTarget { + /** Unused in WebGPU path — required by interface */ + public canvasContext: WebGLRenderingContext = null as any; + /** Unused in WebGPU path — required by interface */ + public xrLayer: Nullable = null; + + private _sessionManager: WebXRSessionManager; + private _engine: WebGPUEngine; + private _scene: Scene; + + constructor(sessionManager: WebXRSessionManager, engine: WebGPUEngine, scene: Scene) { + this._sessionManager = sessionManager; + this._engine = engine; + this._scene = scene; + } + + public async initializeXRLayerAsync(xrSession: XRSession): Promise { + const device = getGpuDeviceFromEngine(this._engine); + const Binding = globalThis.XRGPUBinding as XRGPUBindingConstructor; + const gpuBinding: XRGPUBinding = new Binding(xrSession, device); + + const projectionLayer = gpuBinding.createProjectionLayer({ + textureFormat: 'rgba8unorm', + depthStencilFormat: 'depth24plus-stencil8', + }); + + log.info('[XR-WebGPU] Projection layer created:', projectionLayer.textureWidth, 'x', projectionLayer.textureHeight); + + xrSession.updateRenderState({ layers: [projectionLayer] } as any); + + const wrapper = new XRGPUProjectionLayerWrapper( + projectionLayer, gpuBinding, this._engine, this._scene + ); + this._sessionManager._setBaseLayerWrapper(wrapper); + + log.info('[XR-WebGPU] Layer wrapper set on session manager'); + return null as any; + } + + public dispose(): void { + /* nothing to clean up — layer lifetime is tied to XR session */ + } +} diff --git a/src/core/xr-webgpu/xrGpuSessionSetup.ts b/src/core/xr-webgpu/xrGpuSessionSetup.ts new file mode 100644 index 0000000..d0ff272 --- /dev/null +++ b/src/core/xr-webgpu/xrGpuSessionSetup.ts @@ -0,0 +1,30 @@ +/** + * Helpers for accessing the GPUDevice from WebGPUEngine + * and building XR session init options for WebGPU. + */ +import { WebGPUEngine } from "@babylonjs/core"; + +/** + * Extract the GPUDevice from a WebGPUEngine instance. + * WebGPUEngine stores _device privately; this accessor is stable across versions. + */ +export function getGpuDeviceFromEngine(engine: WebGPUEngine): GPUDevice { + const device = (engine as any)._device as GPUDevice | undefined; + if (!device) { + throw new Error('[XR-WebGPU] Could not access GPUDevice from engine'); + } + return device; +} + +/** + * Build XRSessionInit that adds 'webgpu' to requiredFeatures. + */ +export function buildWebGPUSessionInit( + baseInit: XRSessionInit = {} +): XRSessionInit { + const existing = baseInit.requiredFeatures ?? []; + return { + ...baseInit, + requiredFeatures: [...existing, 'webgpu'], + }; +} diff --git a/src/core/xr-webgpu/xrGpuTextureProvider.ts b/src/core/xr-webgpu/xrGpuTextureProvider.ts new file mode 100644 index 0000000..4582cb8 --- /dev/null +++ b/src/core/xr-webgpu/xrGpuTextureProvider.ts @@ -0,0 +1,85 @@ +/** + * WebGPU XR texture provider — gets GPUTexture per-view from XRGPUBinding + * and swaps it into the cached RenderTargetTexture each frame. + */ +import { RenderTargetTexture, WebGPUEngine, WebXRLayerRenderTargetTextureProvider } from "@babylonjs/core"; +import type { Viewport, Scene, Nullable } from "@babylonjs/core"; +import type { WebXRLayerWrapper } from "@babylonjs/core/XR/webXRLayerWrapper"; +import type { XRGPUBinding, XRGPUSubImage } from "./xrGpuTypes"; +import log from '../logger'; + +export class XRGPUTextureProvider extends WebXRLayerRenderTargetTextureProvider { + private _gpuBinding: XRGPUBinding; + private _projLayer: XRProjectionLayer; + private _gpuEngine: WebGPUEngine; + private _xrScene: Scene; + + constructor( + scene: Scene, + layerWrapper: WebXRLayerWrapper, + gpuBinding: XRGPUBinding, + projectionLayer: XRProjectionLayer, + gpuEngine: WebGPUEngine + ) { + super(scene, layerWrapper); + this._gpuBinding = gpuBinding; + this._projLayer = projectionLayer; + this._gpuEngine = gpuEngine; + this._xrScene = scene; + this._framebufferDimensions = { + framebufferWidth: projectionLayer.textureWidth, + framebufferHeight: projectionLayer.textureHeight, + }; + } + + public getRenderTargetTextureForView(view: XRView): Nullable { + const subImage = this._gpuBinding.getViewSubImage(this._projLayer, view); + const idx = view.eye === "right" ? 1 : 0; + if (!this._renderTargetTextures[idx]) { + this._renderTargetTextures[idx] = this._createGpuRTT(subImage); + } else { + this._swapGpuTexture(this._renderTargetTextures[idx], subImage); + } + return this._renderTargetTextures[idx]; + } + + public getRenderTargetTextureForEye(eye: XREye): Nullable { + return this._renderTargetTextures[eye === "right" ? 1 : 0] ?? null; + } + + public trySetViewportForView(viewport: Viewport, view: XRView): boolean { + const sub = this._gpuBinding.getViewSubImage(this._projLayer, view); + if (!sub) return false; + const w = this._projLayer.textureWidth; + const h = this._projLayer.textureHeight; + viewport.x = sub.viewport.x / w; + viewport.y = sub.viewport.y / h; + viewport.width = sub.viewport.width / w; + viewport.height = sub.viewport.height / h; + return true; + } + + private _createGpuRTT(subImage: XRGPUSubImage): RenderTargetTexture { + const w = this._projLayer.textureWidth; + const h = this._projLayer.textureHeight; + const internalTex = this._gpuEngine.wrapWebGPUTexture(subImage.colorTexture); + internalTex.width = w; + internalTex.height = h; + const rtt = new RenderTargetTexture("xrGpuRTT", { width: w, height: h }, this._xrScene); + const origTex = rtt._texture; + rtt._texture = internalTex; + rtt.renderTarget!.setTexture(internalTex, 0); + origTex?.dispose(); + rtt.disableRescaling(); + log.debug(`[XR-WebGPU] Created RTT ${w}x${h}`); + return rtt; + } + + private _swapGpuTexture(rtt: RenderTargetTexture, subImage: XRGPUSubImage): void { + const hwTex = rtt._texture?._hardwareTexture as any; + if (!hwTex?.set) return; + hwTex.set(subImage.colorTexture); + hwTex.view = null; + hwTex.viewForWriting = null; + } +} diff --git a/src/core/xr-webgpu/xrGpuTypes.ts b/src/core/xr-webgpu/xrGpuTypes.ts new file mode 100644 index 0000000..6ea7658 --- /dev/null +++ b/src/core/xr-webgpu/xrGpuTypes.ts @@ -0,0 +1,33 @@ +/** + * TypeScript declarations for WebXR-WebGPU Binding spec types. + * These are not yet in lib.dom or BabylonJS type definitions. + * @see https://github.com/immersive-web/WebXR-WebGPU-Binding/blob/main/explainer.md + */ + +export interface XRGPUProjectionLayerInit { + textureFormat?: GPUTextureFormat; + depthStencilFormat?: GPUTextureFormat; + scaleFactor?: number; +} + +export interface XRGPUSubImage { + colorTexture: GPUTexture; + depthStencilTexture?: GPUTexture; + imageIndex: number; + viewport: { x: number; y: number; width: number; height: number }; +} + +export interface XRGPUBinding { + createProjectionLayer(init?: XRGPUProjectionLayerInit): XRProjectionLayer; + getViewSubImage(layer: XRProjectionLayer, view: XRView): XRGPUSubImage; +} + +export interface XRGPUBindingConstructor { + new (session: XRSession, device: GPUDevice): XRGPUBinding; +} + +/** Global augmentation for XRGPUBinding constructor */ +declare global { + // eslint-disable-next-line no-var + var XRGPUBinding: XRGPUBindingConstructor | undefined; +} diff --git a/vite.config.ts b/vite.config.ts index bbc4e13..e112f96 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -26,21 +26,21 @@ export default defineConfig({ // Shaders must be explicitly included to avoid dynamic import failures through CloudFlare proxy include: [ '@babylonjs/core', - // Core shaders + // Core shaders (WebGL) '@babylonjs/core/Shaders/default.vertex', '@babylonjs/core/Shaders/default.fragment', '@babylonjs/core/Shaders/rgbdDecode.fragment', '@babylonjs/core/Shaders/procedural.vertex', - // PBR shaders + // PBR shaders (WebGL) '@babylonjs/core/Shaders/pbr.vertex', '@babylonjs/core/Shaders/pbr.fragment', '@babylonjs/core/Shaders/pbrDebug.fragment', - // Particle shaders + // Particle shaders (WebGL) '@babylonjs/core/Shaders/particles.vertex', '@babylonjs/core/Shaders/particles.fragment', '@babylonjs/core/Shaders/gpuRenderParticles.vertex', '@babylonjs/core/Shaders/gpuRenderParticles.fragment', - // Other common shaders + // Other common shaders (WebGL) '@babylonjs/core/Shaders/standard.fragment', '@babylonjs/core/Shaders/postprocess.vertex', '@babylonjs/core/Shaders/pass.fragment', @@ -48,6 +48,21 @@ export default defineConfig({ '@babylonjs/core/Shaders/shadowMap.fragment', '@babylonjs/core/Shaders/depth.vertex', '@babylonjs/core/Shaders/depth.fragment', + // WGSL shaders (WebGPU equivalents) + '@babylonjs/core/ShadersWGSL/default.vertex', + '@babylonjs/core/ShadersWGSL/default.fragment', + '@babylonjs/core/ShadersWGSL/rgbdDecode.fragment', + '@babylonjs/core/ShadersWGSL/procedural.vertex', + '@babylonjs/core/ShadersWGSL/pbr.vertex', + '@babylonjs/core/ShadersWGSL/pbr.fragment', + '@babylonjs/core/ShadersWGSL/particles.vertex', + '@babylonjs/core/ShadersWGSL/particles.fragment', + '@babylonjs/core/ShadersWGSL/postprocess.vertex', + '@babylonjs/core/ShadersWGSL/pass.fragment', + '@babylonjs/core/ShadersWGSL/shadowMap.vertex', + '@babylonjs/core/ShadersWGSL/shadowMap.fragment', + '@babylonjs/core/ShadersWGSL/depth.vertex', + '@babylonjs/core/ShadersWGSL/depth.fragment', '@babylonjs/loaders', '@babylonjs/havok', '@babylonjs/materials',