The Stims rendering system provides automatic backend selection (WebGPU or WebGL), renderer pooling, and dynamic quality controls. It ensures optimal performance while minimizing GPU resource allocation.
Rendering Architecture
The rendering system uses a pooled renderer approach where WebGL/WebGPU renderers are initialized once and reused across toys:
Renderer Handle Interface
The render service provides a RendererHandle that includes all resources needed for rendering:
export type RendererHandle = {
renderer : THREE . WebGLRenderer | WebGPURenderer ;
backend : RendererBackend ;
info : RendererInitResult ;
canvas : HTMLCanvasElement ;
applySettings : (
options ?: Partial < RendererInitConfig >,
viewport ?: RendererViewport ,
) => void ;
release : () => void ;
};
RendererHandle Components
Show renderer - Three.js Renderer
The actual Three.js renderer instance (either WebGLRenderer or WebGPURenderer). Use this to render your scenes: rendererHandle . renderer . render ( scene , camera );
Show backend - Backend Type
Indicates which backend is active: type RendererBackend = 'webgpu' | 'webgl' ;
if ( rendererHandle . backend === 'webgpu' ) {
// Use WebGPU-specific features
}
Show info - Renderer Metadata
Contains initialization details including:
capabilities: Feature detection results
maxPixelRatio: Device pixel ratio cap
renderScale: Render resolution scale
exposure: Tone mapping exposure
Show canvas - HTMLCanvasElement
The canvas element used for rendering. Automatically attached to the container when the handle is acquired.
Show applySettings() - Update Quality
Applies new quality settings without recreating the renderer: rendererHandle . applySettings ({
maxPixelRatio: 2 ,
renderScale: 0.75 ,
exposure: 1.2 ,
});
Show release() - Return to Pool
Releases the renderer back to the pool. Must be called when the toy is disposed.
Requesting a Renderer
Use requestRenderer() to get a renderer for your toy:
import { requestRenderer } from './services/render-service' ;
import type { ToyInstance , ToyStartOptions } from './toy-interface' ;
export async function start ( options ?: ToyStartOptions ) : Promise < ToyInstance > {
const container = options ?. container ;
// Request a pooled renderer
const rendererHandle = await requestRenderer ({ host: container });
// Set up scene
const scene = new THREE . Scene ();
const camera = new THREE . PerspectiveCamera ();
// Animation loop
rendererHandle . renderer . setAnimationLoop (() => {
rendererHandle . renderer . render ( scene , camera );
});
return {
dispose () {
// Stop animation
rendererHandle . renderer . setAnimationLoop ( null );
// Clean up scene
scene . clear ();
// Release renderer back to pool
rendererHandle . release ();
},
};
}
Request Options
await requestRenderer ({
// Container to attach canvas to
host: document . querySelector ( '#toy-container' ),
// Optional custom canvas
canvas: existingCanvas ,
// Quality overrides
options: {
maxPixelRatio: 2 ,
renderScale: 0.8 ,
exposure: 1.0 ,
},
// Custom renderer init (for testing)
initRendererImpl: customInitRenderer ,
});
Capability Detection
The renderer service uses renderer-capabilities.ts to detect available graphics backends:
export type RendererCapabilities = {
webGPUAdapter : GPUAdapter | null ;
webGPUDevice : GPUDevice | null ;
preferredBackend : RendererBackend ;
fallbackReason ?: string ;
shouldRetryWebGPU ?: boolean ;
};
Detection Flow
Fallback Handling
If WebGPU initialization fails, the system automatically falls back to WebGL and stores the failure reason. The UI can display this reason and offer a retry button.
export function rememberRendererFallback ( reason : string ) {
if ( ! cachedCapabilities ) {
cachedCapabilities = {
webGPUAdapter: null ,
webGPUDevice: null ,
preferredBackend: 'webgl' ,
fallbackReason: reason ,
shouldRetryWebGPU: true ,
};
}
}
Renderer Pooling
The render service maintains a pool of initialized renderers:
type RendererPoolEntry = {
handle : RendererHandle ;
inUse : boolean ;
};
const rendererPool : RendererPoolEntry [] = [];
Pool Lifecycle
First request : Creates a new renderer and adds it to the pool
Subsequent requests : Reuses an idle renderer from the pool
Settings update : Applies current quality preset and render preferences
Release : Marks the renderer as idle, detaches canvas, stops animation loop
Reset : Optionally disposes all renderers and clears the pool
Pooling avoids expensive WebGPU/WebGL context creation when switching between toys. A typical renderer initialization can take 100-500ms, while reusing a pooled renderer is nearly instant.
Pool Management Functions
// Request a renderer (creates or reuses)
await requestRenderer ({ host: container });
// Prewarm capabilities before first use
await prewarmRendererCapabilities ();
// Reset pool (without disposal)
resetRendererPool ({ dispose: false });
// Reset pool and dispose renderers
resetRendererPool ({ dispose: true });
Quality Settings
The rendering system supports dynamic quality adjustments through presets and preferences:
export type QualityPreset = {
maxPixelRatio : number ;
renderScale ?: number ;
exposure ?: number ;
};
Quality Preset Examples
Preset maxPixelRatio renderScale Use Case Low 1.0 0.5 Mobile devices, battery saving Medium 1.5 0.75 Balanced quality/performance High 2.0 1.0 Desktop, high-end mobile Ultra 3.0 1.0 High-DPI displays, powerful GPUs
Settings Application
Quality settings are applied through a resolution chain:
function buildSettings (
options : Partial < RendererInitConfig > = {},
info ?: RendererInitResult | null ,
) : RendererInitConfig {
return resolveRendererSettings (
options , // Toy-specific overrides
info , // Renderer metadata
getRenderDefaults (), // Quality preset + render preferences
);
}
The resolution order (highest priority first):
Toy-specific overrides - Passed to applySettings()
Render preferences - User settings from UI
Quality presets - Active preset from settings panel
System defaults - Fallback values
Dynamic Settings Updates
The render service subscribes to quality and preference changes:
subscribeToQualityPreset (( preset ) => {
activeQuality = preset ;
// Apply to all active renderers
forEachActiveRenderer (( entry ) => entry . handle . applySettings ());
});
subscribeToRenderPreferences (( preferences ) => {
activeRenderPreferences = preferences ;
// Apply to all active renderers
forEachActiveRenderer (( entry ) => entry . handle . applySettings ());
});
When quality settings change, all active toys automatically receive the updates without needing to restart.
Renderer Settings
The applyRendererSettings() function updates an active renderer:
export function applyRendererSettings (
renderer : THREE . WebGLRenderer | WebGPURenderer ,
info : RendererInitResult ,
options : Partial < RendererInitConfig > = {},
defaults : Partial < RendererInitConfig > = {},
viewport ?: RendererViewport ,
) {
const settings = resolveRendererSettings ( options , info , defaults );
// Apply pixel ratio
const pixelRatio = Math . min (
settings . maxPixelRatio ,
window . devicePixelRatio || 1 ,
);
renderer . setPixelRatio ( pixelRatio );
// Apply render scale and viewport
if ( viewport ) {
const width = Math . floor ( viewport . width * settings . renderScale );
const height = Math . floor ( viewport . height * settings . renderScale );
renderer . setSize ( width , height , false );
}
// Apply tone mapping (WebGL only)
if ( 'toneMapping' in renderer ) {
renderer . toneMappingExposure = settings . exposure ;
}
}
Canvas Management
The render service automatically manages canvas lifecycle:
function attachCanvas ( canvas : HTMLCanvasElement , host ?: HTMLElement ) {
if ( ! host ) return ;
if ( canvas . parentElement !== host ) {
host . appendChild ( canvas );
}
}
function detachCanvas ( canvas : HTMLCanvasElement ) {
if ( canvas . parentElement ) {
canvas . parentElement . removeChild ( canvas );
}
}
Canvas Lifecycle
On acquire : Canvas is attached to the provided host element
On release : Canvas is detached from the DOM
On next acquire : Same canvas is reattached to new host
This prevents canvas recreation and preserves GPU contexts.
Renderer Initialization
The renderer-setup.ts module handles low-level renderer creation:
export type RendererInitConfig = {
maxPixelRatio : number ;
renderScale : number ;
exposure : number ;
antialias ?: boolean ;
alpha ?: boolean ;
powerPreference ?: 'high-performance' | 'low-power' | 'default' ;
};
export async function initRenderer (
canvas : HTMLCanvasElement ,
config : RendererInitConfig ,
) : Promise < RendererInitResult | null > {
const capabilities = await getRendererCapabilities ();
if ( capabilities . preferredBackend === 'webgpu' && capabilities . webGPUAdapter ) {
return initWebGPURenderer ( canvas , config , capabilities );
}
return initWebGLRenderer ( canvas , config );
}
WebGPU Initialization
async function initWebGPURenderer (
canvas : HTMLCanvasElement ,
config : RendererInitConfig ,
capabilities : RendererCapabilities ,
) : Promise < RendererInitResult > {
const { webGPUAdapter , webGPUDevice } = capabilities ;
const renderer = new WebGPURenderer ({
canvas ,
antialias: config . antialias ?? true ,
alpha: config . alpha ?? false ,
device: webGPUDevice ,
});
await renderer . init ();
return {
renderer ,
backend: 'webgpu' ,
capabilities ,
// ... metadata
};
}
WebGL Initialization
function initWebGLRenderer (
canvas : HTMLCanvasElement ,
config : RendererInitConfig ,
) : RendererInitResult {
const renderer = new THREE . WebGLRenderer ({
canvas ,
antialias: config . antialias ?? true ,
alpha: config . alpha ?? false ,
powerPreference: config . powerPreference ?? 'high-performance' ,
});
// Apply tone mapping
renderer . toneMapping = THREE . ACESFilmicToneMapping ;
renderer . toneMappingExposure = config . exposure ;
return {
renderer ,
backend: 'webgl' ,
// ... metadata
};
}
Best Practices
Always release renderer handles - Call rendererHandle.release() in your toy’s dispose() method to return the renderer to the pool.
Don’t dispose pooled renderers - Never call renderer.dispose() on a pooled renderer. Use release() instead.
Do’s and Don’ts
✅ Do:
Use requestRenderer() for standard toys
Call release() in your dispose() method
Use applySettings() for toy-specific quality adjustments
Stop animation loops before releasing
Respect quality presets from settings panel
❌ Don’t:
Call renderer.dispose() on pooled renderers
Manually resize the canvas (use applySettings() with viewport)
Hard-code devicePixelRatio (use maxPixelRatio setting)
Request multiple renderers in the same toy
Forget to release handles on disposal
Usage Patterns
Standard Rendering Setup
import { requestRenderer } from '../core/services/render-service' ;
import type { ToyInstance , ToyStartOptions } from '../core/toy-interface' ;
export async function start ( options ?: ToyStartOptions ) : Promise < ToyInstance > {
const container = options ?. container ;
// Request renderer
const rendererHandle = await requestRenderer ({ host: container });
// Create scene
const scene = new THREE . Scene ();
const camera = new THREE . PerspectiveCamera ( 75 , 1 , 0.1 , 1000 );
// Handle resize
const handleResize = () => {
const width = container ?. clientWidth || window . innerWidth ;
const height = container ?. clientHeight || window . innerHeight ;
camera . aspect = width / height ;
camera . updateProjectionMatrix ();
rendererHandle . applySettings ( undefined , { width , height });
};
window . addEventListener ( 'resize' , handleResize );
handleResize ();
// Animation loop
rendererHandle . renderer . setAnimationLoop (() => {
rendererHandle . renderer . render ( scene , camera );
});
return {
dispose () {
rendererHandle . renderer . setAnimationLoop ( null );
window . removeEventListener ( 'resize' , handleResize );
scene . clear ();
rendererHandle . release ();
},
};
}
Custom Quality Settings
// Request renderer with custom quality
const rendererHandle = await requestRenderer ({
host: container ,
options: {
maxPixelRatio: 1.5 , // Lower for better performance
renderScale: 0.8 , // Render at 80% resolution
exposure: 1.2 , // Brighter tone mapping
},
});
// Update settings dynamically
rendererHandle . applySettings ({
renderScale: 0.5 , // Drop to half resolution
});
Backend-Specific Code
const rendererHandle = await requestRenderer ({ host: container });
if ( rendererHandle . backend === 'webgpu' ) {
// Use WebGPU-specific features
console . log ( 'Running with WebGPU' );
} else {
// WebGL fallback path
console . log ( 'Running with WebGL' );
}
Debugging Rendering Issues
Show Black screen / no rendering
Check that renderer.setAnimationLoop() is called
Verify the canvas is attached to the DOM
Ensure camera is positioned correctly
Check scene has visible objects
Look for WebGL context loss in console
Lower maxPixelRatio to 1.0 or 1.5
Reduce renderScale to 0.5 or 0.75
Check scene complexity (draw calls, triangles)
Profile with Chrome DevTools Performance tab
Verify animation loop isn’t running multiple times
Show WebGPU not available
Check browser support: Chrome 113+, Edge 113+
Verify hardware acceleration is enabled
Check chrome://gpu for WebGPU status
Review capabilities.fallbackReason for details
Verify container has non-zero dimensions
Check CSS: canvas should not have display: none
Ensure applySettings() was called with viewport
Look for z-index or overflow issues
Next Steps
Architecture Overview Return to the architecture overview
Toy Lifecycle Learn about toy instance management