feat(immersive): add Phase 0 infrastructure for WebGL2 rendering

- Create src/lib/immersive/ module structure
- Add types.ts with ImmersiveState, ImmersiveConfig interfaces
- Add config.ts with build-time and runtime feature flags
- Add ImmersiveRenderer.ts with lifecycle management and stores
- Add index.ts as public API boundary
- Add VITE_IMMERSIVE_ENABLED to vite.config.js
- Add app.d.ts for TypeScript env declarations

Build without immersive: QBZ_IMMERSIVE=false pnpm build

No visual changes yet - this is capability detection and guards only.
This commit is contained in:
vicrodh committed 2026-02-01 10:05:12 -06:00
1 parent 600a33026a
commit 19d0916a46
6 files changed
+555

No files matched your search

+23
View File
@@ -0,0 +1,23 @@
// See https://svelte.dev/docs/kit/types#app.d.ts
declare global {
namespace App {
// interface Error {}
// interface Locals {}
// interface PageData {}
// interface PageState {}
// interface Platform {}
}
}
// Extend Vite's ImportMetaEnv with our custom env variables
interface ImportMetaEnv {
readonly VITE_BUILD_DATE: string;
readonly VITE_IMMERSIVE_ENABLED: string;
}
interface ImportMeta {
readonly env: ImportMetaEnv;
}
export {};
+268
View File
@@ -0,0 +1,268 @@
/**
* Immersive Renderer - Lifecycle Manager
*
* This module owns the immersive rendering lifecycle:
* - Capability detection
* - Initialization / destruction
* - State management
* - Debug instrumentation
*
* UI components should use this as the single entry point.
*/
import { writable, derived, get } from 'svelte/store';
import {
BUILD_IMMERSIVE_ENABLED,
shouldImmersiveBeAvailable,
getWebGL2Info,
getConfig,
setRuntimeEnabled as setConfigRuntimeEnabled,
} from './config';
import type {
ImmersiveState,
ImmersiveBackend,
UnavailableReason,
ImmersiveMetrics,
} from './types';
// ============================================================================
// State Store
// ============================================================================
const initialState: ImmersiveState = {
active: false,
backend: 'disabled',
unavailableReason: BUILD_IMMERSIVE_ENABLED ? undefined : 'build-disabled',
};
/** Internal writable store */
const stateStore = writable<ImmersiveState>(initialState);
/** Public read-only store for immersive state */
export const immersiveState = { subscribe: stateStore.subscribe };
/** Derived store: is immersive currently active? */
export const isImmersiveActive = derived(stateStore, ($state) => $state.active);
/** Derived store: current backend */
export const immersiveBackend = derived(stateStore, ($state) => $state.backend);
/** Derived store: debug metrics (null if not active) */
export const immersiveMetrics = derived(stateStore, ($state) => $state.metrics ?? null);
// ============================================================================
// Metrics Tracking
// ============================================================================
let frameCount = 0;
let lastFpsUpdate = 0;
let currentFps = 0;
let lastFrameTime = 0;
/**
* Update metrics from render loop.
* Called by ImmersiveAmbientCanvas on each frame.
*/
export function updateMetrics(frameTimeMs: number): void {
frameCount++;
lastFrameTime = frameTimeMs;
const now = performance.now();
if (now - lastFpsUpdate >= 1000) {
currentFps = frameCount;
frameCount = 0;
lastFpsUpdate = now;
// Update store with new metrics
stateStore.update((state) => {
if (!state.active) return state;
return {
...state,
metrics: {
...state.metrics!,
fps: currentFps,
frameTimeMs: lastFrameTime,
},
};
});
}
}
/**
* Set GPU info in metrics (called once during init).
*/
function setGpuInfo(renderer: string): void {
stateStore.update((state) => ({
...state,
metrics: {
fps: 0,
frameTimeMs: 0,
gpuRenderer: renderer,
textureCount: 0,
gpuMemoryBytes: 0,
},
}));
}
/**
* Update texture count in metrics.
*/
export function updateTextureCount(count: number, memoryBytes: number): void {
stateStore.update((state) => {
if (!state.metrics) return state;
return {
...state,
metrics: {
...state.metrics,
textureCount: count,
gpuMemoryBytes: memoryBytes,
},
};
});
}
// ============================================================================
// Public API
// ============================================================================
/**
* Check if immersive rendering is available.
* This checks build flags, runtime preferences, and WebGL2 capability.
*/
export function isAvailable(): boolean {
return shouldImmersiveBeAvailable().available;
}
/**
* Check if immersive is currently enabled (user preference).
*/
export function isEnabled(): boolean {
const { available, reason } = shouldImmersiveBeAvailable();
if (!available && reason === 'runtime-disabled') {
return false;
}
return available;
}
/**
* Enable or disable immersive rendering (user preference).
*/
export function setEnabled(enabled: boolean): void {
setConfigRuntimeEnabled(enabled);
if (enabled) {
// Check if we can actually enable
const { available, reason } = shouldImmersiveBeAvailable();
if (!available) {
stateStore.set({
active: false,
backend: 'disabled',
unavailableReason: reason,
});
}
} else {
stateStore.set({
active: false,
backend: 'disabled',
unavailableReason: 'runtime-disabled',
});
}
}
/**
* Initialize immersive rendering.
* Call this when the immersive view is mounted.
*
* Returns true if initialization succeeded.
*/
export async function init(): Promise<boolean> {
const { available, reason } = shouldImmersiveBeAvailable();
if (!available) {
console.log(`[Immersive] Not available: ${reason}`);
stateStore.set({
active: false,
backend: 'disabled',
unavailableReason: reason,
});
return false;
}
// Get GPU info for debugging
const gpuInfo = getWebGL2Info();
const renderer = gpuInfo?.renderer ?? 'Unknown GPU';
console.log(`[Immersive] Initializing with WebGL2 - ${renderer}`);
// Set initial state
stateStore.set({
active: true,
backend: 'webgl2',
metrics: {
fps: 0,
frameTimeMs: 0,
gpuRenderer: renderer,
textureCount: 0,
gpuMemoryBytes: 0,
},
});
return true;
}
/**
* Destroy immersive rendering.
* Call this when the immersive view is unmounted.
*/
export function destroy(): void {
console.log('[Immersive] Destroyed');
// Reset metrics
frameCount = 0;
lastFpsUpdate = 0;
currentFps = 0;
stateStore.set({
active: false,
backend: 'disabled',
});
}
/**
* Handle WebGL context loss.
* Call this from the canvas component when context is lost.
*/
export function handleContextLost(): void {
console.warn('[Immersive] WebGL context lost');
stateStore.update((state) => ({
...state,
active: false,
backend: 'fallback',
unavailableReason: 'context-lost',
}));
}
/**
* Handle WebGL context restored.
* Call this from the canvas component when context is restored.
*/
export function handleContextRestored(): void {
console.log('[Immersive] WebGL context restored');
stateStore.update((state) => ({
...state,
active: true,
backend: 'webgl2',
unavailableReason: undefined,
}));
}
/**
* Get current state snapshot (for debugging).
*/
export function getState(): ImmersiveState {
return get(stateStore);
}
/**
* Get current configuration.
*/
export { getConfig } from './config';
+140
View File
@@ -0,0 +1,140 @@
/**
* Immersive Rendering Configuration
*
* Handles build-time and runtime feature flags for the immersive system.
* This module is the single source of truth for "should immersive be active?"
*/
import { DEFAULT_IMMERSIVE_CONFIG, type ImmersiveConfig } from './types';
/** Storage key for runtime enable/disable preference */
const STORAGE_KEY_ENABLED = 'qbz.immersive.enabled';
const STORAGE_KEY_CONFIG = 'qbz.immersive.config';
/**
* Build-time flag: Is immersive compiled into this build?
*
* Set via environment variable at build time:
* QBZ_IMMERSIVE=false pnpm build
*
* When false, all immersive code should be tree-shaken out.
*/
export const BUILD_IMMERSIVE_ENABLED: boolean =
import.meta.env.VITE_IMMERSIVE_ENABLED !== 'false';
/**
* Check if WebGL2 is available in the current environment.
* This is a capability check, not a preference check.
*/
export function isWebGL2Available(): boolean {
if (typeof document === 'undefined') {
// SSR or headless - no WebGL
return false;
}
try {
const canvas = document.createElement('canvas');
const gl = canvas.getContext('webgl2');
return gl !== null;
} catch {
return false;
}
}
/**
* Get detailed WebGL2 information for debugging.
* Returns null if WebGL2 is unavailable.
*/
export function getWebGL2Info(): { renderer: string; vendor: string; version: string } | null {
if (typeof document === 'undefined') return null;
try {
const canvas = document.createElement('canvas');
const gl = canvas.getContext('webgl2');
if (!gl) return null;
const debugInfo = gl.getExtension('WEBGL_debug_renderer_info');
return {
renderer: debugInfo
? gl.getParameter(debugInfo.UNMASKED_RENDERER_WEBGL)
: gl.getParameter(gl.RENDERER),
vendor: debugInfo
? gl.getParameter(debugInfo.UNMASKED_VENDOR_WEBGL)
: gl.getParameter(gl.VENDOR),
version: gl.getParameter(gl.VERSION),
};
} catch {
return null;
}
}
/**
* Check if immersive is enabled at runtime (user preference).
* Only meaningful if BUILD_IMMERSIVE_ENABLED is true.
*/
export function isRuntimeEnabled(): boolean {
if (!BUILD_IMMERSIVE_ENABLED) return false;
if (typeof localStorage === 'undefined') return true; // Default to enabled
const stored = localStorage.getItem(STORAGE_KEY_ENABLED);
return stored !== 'false'; // Default to enabled if not set
}
/**
* Set runtime enabled state (user preference).
*/
export function setRuntimeEnabled(enabled: boolean): void {
if (typeof localStorage === 'undefined') return;
localStorage.setItem(STORAGE_KEY_ENABLED, enabled ? 'true' : 'false');
}
/**
* Get the current immersive configuration.
*/
export function getConfig(): ImmersiveConfig {
if (typeof localStorage === 'undefined') return { ...DEFAULT_IMMERSIVE_CONFIG };
try {
const stored = localStorage.getItem(STORAGE_KEY_CONFIG);
if (!stored) return { ...DEFAULT_IMMERSIVE_CONFIG };
const parsed = JSON.parse(stored);
return { ...DEFAULT_IMMERSIVE_CONFIG, ...parsed };
} catch {
return { ...DEFAULT_IMMERSIVE_CONFIG };
}
}
/**
* Update immersive configuration.
*/
export function setConfig(config: Partial<ImmersiveConfig>): void {
if (typeof localStorage === 'undefined') return;
const current = getConfig();
const updated = { ...current, ...config };
localStorage.setItem(STORAGE_KEY_CONFIG, JSON.stringify(updated));
}
/**
* Check if immersive should be available (all conditions met).
* This is the main entry point for "can we use immersive?"
*/
export function shouldImmersiveBeAvailable(): {
available: boolean;
reason?: 'build-disabled' | 'runtime-disabled' | 'no-webgl2';
} {
if (!BUILD_IMMERSIVE_ENABLED) {
return { available: false, reason: 'build-disabled' };
}
if (!isRuntimeEnabled()) {
return { available: false, reason: 'runtime-disabled' };
}
if (!isWebGL2Available()) {
return { available: false, reason: 'no-webgl2' };
}
return { available: true };
}
+56
View File
@@ -0,0 +1,56 @@
/**
* Immersive Rendering Module - Public API
*
* This is the ONLY file that should be imported from outside the module.
*
* Usage:
* import { isAvailable, init, destroy } from '$lib/immersive';
*
* IMPORTANT: No file outside this module should:
* - Import WebGL APIs directly
* - Import shader files
* - Assume canvas/GPU availability
*/
// Re-export types (always safe to import)
export type {
ImmersiveState,
ImmersiveBackend,
UnavailableReason,
ImmersiveMetrics,
ImmersiveConfig,
} from './types';
export { DEFAULT_IMMERSIVE_CONFIG } from './types';
// Re-export config utilities
export {
BUILD_IMMERSIVE_ENABLED,
isWebGL2Available,
getWebGL2Info,
isRuntimeEnabled,
setRuntimeEnabled,
shouldImmersiveBeAvailable,
} from './config';
// Re-export renderer API
export {
// Stores
immersiveState,
isImmersiveActive,
immersiveBackend,
immersiveMetrics,
// Functions
isAvailable,
isEnabled,
setEnabled,
init,
destroy,
getState,
getConfig,
// Internal (for canvas component)
updateMetrics,
updateTextureCount,
handleContextLost,
handleContextRestored,
} from './ImmersiveRenderer';
+63
View File
@@ -0,0 +1,63 @@
/**
* Immersive Rendering Types
*
* Type definitions for the WebGL2-based immersive rendering system.
* This module has no runtime dependencies and can be safely imported anywhere.
*/
/** Rendering backend currently in use */
export type ImmersiveBackend = 'webgl2' | 'fallback' | 'disabled';
/** Reason why immersive is unavailable */
export type UnavailableReason =
| 'build-disabled' // Compiled out via QBZ_IMMERSIVE=false
| 'runtime-disabled' // User disabled in settings
| 'no-webgl2' // Browser/WebView doesn't support WebGL2
| 'context-lost' // WebGL context was lost
| 'init-failed'; // Initialization failed for other reason
/** Current state of the immersive renderer */
export interface ImmersiveState {
/** Whether immersive rendering is currently active */
active: boolean;
/** Current backend in use */
backend: ImmersiveBackend;
/** If not active, why */
unavailableReason?: UnavailableReason;
/** Debug metrics (only populated when active) */
metrics?: ImmersiveMetrics;
}
/** Performance metrics for debugging */
export interface ImmersiveMetrics {
/** Frames per second */
fps: number;
/** Last frame time in milliseconds */
frameTimeMs: number;
/** GPU renderer string (e.g., "Mesa Intel UHD 620") */
gpuRenderer: string;
/** Number of textures currently loaded */
textureCount: number;
/** Estimated GPU memory usage in bytes */
gpuMemoryBytes: number;
}
/** Configuration options for the immersive renderer */
export interface ImmersiveConfig {
/** Target FPS for animation (lower = less power) */
targetFps: number;
/** Ambient motion intensity (0 = static, 1 = full motion) */
ambientIntensity: number;
/** Whether to pause rendering when tab is hidden */
pauseWhenHidden: boolean;
/** Enable debug overlay with metrics */
debugOverlay: boolean;
}
/** Default configuration */
export const DEFAULT_IMMERSIVE_CONFIG: ImmersiveConfig = {
targetFps: 30,
ambientIntensity: 0.5,
pauseWhenHidden: true,
debugOverlay: false,
};