Files
mindbot/deps/cloudxr/helpers/WebGLStateApply.ts
yt lee 623e05f250 Add CloudXR VR streaming support for PICO 4 Ultra (Early Access)
Replaces manual H264/TCP stereo streaming with NVIDIA CloudXR for
higher-quality stereoscopic rendering and lower latency.

Changes:
- teleop_xr_agent.py: add --cloudxr flag (enables Isaac Sim XR mode,
  disables manual StreamingManager)
- deps/cloudxr/: NVIDIA CloudXR.js SDK (Early Access) with Isaac Lab
  teleop React web client
- deps/cloudxr/Dockerfile.wss.proxy: HAProxy WSS proxy for PICO 4 Ultra
  HTTPS mode (routes wss://48322 → ws://49100)
- deps/cloudxr/isaac/webpack.dev.js: disable file watching to avoid
  EMFILE errors with large node_modules
- deps/cloudxr/INSTALL.md: full setup guide

Usage:
  # Start CloudXR Runtime + Isaac Lab
  cd ~/IsaacLab && ./docker/container.py start \
      --files docker-compose.cloudxr-runtime.patch.yaml \
      --env-file .env.cloudxr-runtime

  # Run teleop with CloudXR
  ~/IsaacLab/isaaclab.sh -p teleop_xr_agent.py \
      --task Isaac-MindRobot-2i-DualArm-IK-Abs-v0 --cloudxr

  # Serve web client
  cd deps/cloudxr/isaac && npm run dev-server:https
2026-03-26 14:29:03 +08:00

1221 lines
40 KiB
TypeScript

import {
WebGLState,
WebGLBufferState,
WebGLTextureState,
WebGLTextureUnitState,
WebGLProgramState,
WebGLFramebufferState,
WebGLVertexArrayState,
WebGLVertexAttribState,
WebGLCapabilityState,
WebGLViewportState,
WebGLClearState,
WebGLBlendState,
WebGLDepthState,
WebGLStencilState,
WebGLColorState,
WebGLCullingState,
WebGLLineState,
WebGLPolygonOffsetState,
WebGLSampleState,
WebGLPixelStoreState,
WebGLTransformFeedbackState,
WebGLRenderbufferState,
WebGLSamplerState,
WebGLQueryState,
WebGLIndexedBufferBinding,
GL_MAX_VERTEX_ATTRIBS,
GL_MAX_UNIFORM_BUFFER_BINDINGS,
GL_MAX_TRANSFORM_FEEDBACK_SEPARATE_ATTRIBS,
GL_MAX_COMBINED_TEXTURE_IMAGE_UNITS,
GL_MAX_COLOR_ATTACHMENTS,
GLUndefined,
isDefined,
} from './WebGLState';
/**
* Global flag to enable WebGL error checking after each state operation.
* Set to true for debugging to catch GL errors immediately after they occur.
* Default: false
*/
export let CHECK_ERRORS = false;
/**
* Helper function to check if a property is defined on a state object,
* handling the case where the state object itself might be undefined
*/
function isPropertyDefined<T>(state: T | undefined, prop: keyof T): boolean {
return state !== undefined && isDefined((state as any)[prop]);
}
/**
* Helper function to determine if state should be applied based on saved vs current state.
* Returns true if the states differ or if one is undefined and the other is not.
*
* @param saved - The saved/desired state (may be undefined to reset to defaults)
* @param current - The current state (may be undefined if not tracked)
* @returns true if the state should be applied, false if no change needed
*/
function shouldApplyState<T extends { equals(other: T): boolean }>(
saved: T | undefined,
current: T | undefined
): boolean {
// Both undefined - no change needed
if (saved === undefined && current === undefined) {
return false;
}
// One is undefined and the other isn't - need to apply/reset
if (saved === undefined || current === undefined) {
return true;
}
// Both defined - check if they're different using equals()
return !saved.equals(current);
}
/**
* Helper function to check and log WebGL errors after state application
* @param gl - The WebGL2RenderingContext to check
* @param stepName - Name of the state application step for logging
*/
function checkGLError(gl: WebGL2RenderingContext, stepName: string): void {
if (!CHECK_ERRORS) return;
const error = gl.getError();
if (error !== gl.NO_ERROR) {
const errorName = getGLErrorName(gl, error);
const message = `[WebGLStateApply] GL error after ${stepName}: ${errorName} (0x${error.toString(16)})`;
throw new Error(message);
}
}
/**
* Get human-readable name for a WebGL error code
*/
function getGLErrorName(gl: WebGL2RenderingContext, error: number): string {
switch (error) {
case gl.INVALID_ENUM:
return 'INVALID_ENUM';
case gl.INVALID_VALUE:
return 'INVALID_VALUE';
case gl.INVALID_OPERATION:
return 'INVALID_OPERATION';
case gl.INVALID_FRAMEBUFFER_OPERATION:
return 'INVALID_FRAMEBUFFER_OPERATION';
case gl.OUT_OF_MEMORY:
return 'OUT_OF_MEMORY';
case gl.CONTEXT_LOST_WEBGL:
return 'CONTEXT_LOST_WEBGL';
default:
return 'UNKNOWN_ERROR';
}
}
/**
* Apply all defined state from a WebGLState object to a WebGL context
* @param gl - The WebGL2RenderingContext to apply state to
* @param state - The WebGLState object containing tracked state
* @param current - The current WebGLState for comparison
*/
export function apply(gl: WebGL2RenderingContext, state: WebGLState, current: WebGLState): void {
// Apply state in a specific order to handle dependencies
// 2. Vertex Array Object binding (this may temporarily change ARRAY_BUFFER)
if (shouldApplyState(state.vertexArrays, current.vertexArrays)) {
applyVertexArrayState(gl, state.vertexArrays, current.vertexArrays);
checkGLError(gl, 'vertex array state');
}
// 1. Buffer bindings (needed before VAO state)
if (shouldApplyState(state.buffers, current.buffers)) {
applyBufferState(gl, state.buffers, current.buffers, state.validBuffers);
checkGLError(gl, 'buffer state');
}
// 4. Texture bindings
if (shouldApplyState(state.textures, current.textures)) {
applyTextureState(gl, state.textures, current.textures);
checkGLError(gl, 'texture state');
}
// 5. Sampler bindings
if (shouldApplyState(state.samplers, current.samplers)) {
applySamplerState(gl, state.samplers, current.samplers);
checkGLError(gl, 'sampler state');
}
// 6. Program binding
if (shouldApplyState(state.programs, current.programs)) {
applyProgramState(gl, state.programs, current.programs);
checkGLError(gl, 'program state');
}
// 7. Framebuffer bindings and attachments
if (shouldApplyState(state.framebuffers, current.framebuffers)) {
applyFramebufferState(gl, state.framebuffers, current.framebuffers);
checkGLError(gl, 'framebuffer state');
}
// 8. Renderbuffer binding
if (shouldApplyState(state.renderbuffer, current.renderbuffer)) {
applyRenderbufferState(gl, state.renderbuffer, current.renderbuffer);
checkGLError(gl, 'renderbuffer state');
}
// 9. Transform feedback
if (shouldApplyState(state.transformFeedback, current.transformFeedback)) {
applyTransformFeedbackState(gl, state.transformFeedback, current.transformFeedback);
checkGLError(gl, 'transform feedback state');
}
// 10. Viewport and scissor
if (shouldApplyState(state.viewport, current.viewport)) {
applyViewportState(gl, state.viewport, current.viewport);
checkGLError(gl, 'viewport state');
}
// 11. Capabilities (enable/disable)
if (shouldApplyState(state.capabilities, current.capabilities)) {
applyCapabilityState(gl, state.capabilities, current.capabilities);
checkGLError(gl, 'capability state');
}
// 12. Clear values
if (shouldApplyState(state.clear, current.clear)) {
applyClearState(gl, state.clear, current.clear);
checkGLError(gl, 'clear state');
}
// 13. Blend state
if (shouldApplyState(state.blend, current.blend)) {
applyBlendState(gl, state.blend, current.blend);
checkGLError(gl, 'blend state');
}
// 14. Depth state
if (shouldApplyState(state.depth, current.depth)) {
applyDepthState(gl, state.depth, current.depth);
checkGLError(gl, 'depth state');
}
// 15. Stencil state
if (shouldApplyState(state.stencil, current.stencil)) {
applyStencilState(gl, state.stencil, current.stencil);
checkGLError(gl, 'stencil state');
}
// 16. Color write mask
if (shouldApplyState(state.color, current.color)) {
applyColorState(gl, state.color, current.color);
checkGLError(gl, 'color state');
}
// 17. Culling state
if (shouldApplyState(state.culling, current.culling)) {
applyCullingState(gl, state.culling, current.culling);
checkGLError(gl, 'culling state');
}
// 18. Line width
if (shouldApplyState(state.line, current.line)) {
applyLineState(gl, state.line, current.line);
checkGLError(gl, 'line state');
}
// 19. Polygon offset
if (shouldApplyState(state.polygonOffset, current.polygonOffset)) {
applyPolygonOffsetState(gl, state.polygonOffset, current.polygonOffset);
checkGLError(gl, 'polygon offset state');
}
// 20. Sample coverage
if (shouldApplyState(state.sample, current.sample)) {
applySampleState(gl, state.sample, current.sample);
checkGLError(gl, 'sample state');
}
// 21. Pixel store parameters
if (shouldApplyState(state.pixelStore, current.pixelStore)) {
applyPixelStoreState(gl, state.pixelStore, current.pixelStore);
checkGLError(gl, 'pixel store state');
}
// 22. Query objects
if (state.queries && (!current.queries || !state.queries.equals(current.queries))) {
applyQueryState(gl, state.queries);
checkGLError(gl, 'query state');
}
}
/**
* Apply buffer binding state
*/
function applyBufferState(
gl: WebGL2RenderingContext,
buffers: WebGLBufferState | undefined,
current: WebGLBufferState | undefined,
validBuffers: Set<WebGLBuffer>
): void {
// Helper to validate a buffer
const validateBuffer = (buffer: WebGLBuffer | null, bufferName: string): boolean => {
if (!buffer) return true;
if (!validBuffers.has(buffer)) {
console.warn(
`[WebGLStateApply] Cannot bind ${bufferName}: buffer has been deleted. Skipping.`
);
return false;
}
return true;
};
if (isPropertyDefined(buffers, 'arrayBuffer')) {
if (validateBuffer(buffers!.arrayBuffer, 'ARRAY_BUFFER')) {
gl.bindBuffer(gl.ARRAY_BUFFER, buffers!.arrayBuffer);
}
} else if (isDefined(current?.arrayBuffer)) {
gl.bindBuffer(gl.ARRAY_BUFFER, null);
}
// NOTE: ELEMENT_ARRAY_BUFFER is NOT restored here!
// Per OpenGL ES 3.0 spec section 2.10, ELEMENT_ARRAY_BUFFER binding is per-VAO state.
// It is automatically restored when binding the VAO in applyVertexArrayState() above.
if (isPropertyDefined(buffers, 'uniformBuffer')) {
if (validateBuffer(buffers!.uniformBuffer, 'UNIFORM_BUFFER')) {
gl.bindBuffer(gl.UNIFORM_BUFFER, buffers!.uniformBuffer);
}
} else if (isDefined(current?.uniformBuffer)) {
gl.bindBuffer(gl.UNIFORM_BUFFER, null);
}
if (isPropertyDefined(buffers, 'transformFeedbackBuffer')) {
if (validateBuffer(buffers!.transformFeedbackBuffer, 'TRANSFORM_FEEDBACK_BUFFER')) {
gl.bindBuffer(gl.TRANSFORM_FEEDBACK_BUFFER, buffers!.transformFeedbackBuffer);
}
} else if (isDefined(current?.transformFeedbackBuffer)) {
gl.bindBuffer(gl.TRANSFORM_FEEDBACK_BUFFER, null);
}
if (isPropertyDefined(buffers, 'pixelPackBuffer')) {
if (validateBuffer(buffers!.pixelPackBuffer, 'PIXEL_PACK_BUFFER')) {
gl.bindBuffer(gl.PIXEL_PACK_BUFFER, buffers!.pixelPackBuffer);
}
} else if (isDefined(current?.pixelPackBuffer)) {
gl.bindBuffer(gl.PIXEL_PACK_BUFFER, null);
}
if (isPropertyDefined(buffers, 'pixelUnpackBuffer')) {
if (validateBuffer(buffers!.pixelUnpackBuffer, 'PIXEL_UNPACK_BUFFER')) {
gl.bindBuffer(gl.PIXEL_UNPACK_BUFFER, buffers!.pixelUnpackBuffer);
}
} else if (isDefined(current?.pixelUnpackBuffer)) {
gl.bindBuffer(gl.PIXEL_UNPACK_BUFFER, null);
}
if (isPropertyDefined(buffers, 'copyReadBuffer')) {
if (validateBuffer(buffers!.copyReadBuffer, 'COPY_READ_BUFFER')) {
gl.bindBuffer(gl.COPY_READ_BUFFER, buffers!.copyReadBuffer);
}
} else if (isDefined(current?.copyReadBuffer)) {
gl.bindBuffer(gl.COPY_READ_BUFFER, null);
}
if (isPropertyDefined(buffers, 'copyWriteBuffer')) {
if (validateBuffer(buffers!.copyWriteBuffer, 'COPY_WRITE_BUFFER')) {
gl.bindBuffer(gl.COPY_WRITE_BUFFER, buffers!.copyWriteBuffer);
}
} else if (isDefined(current?.copyWriteBuffer)) {
gl.bindBuffer(gl.COPY_WRITE_BUFFER, null);
}
// Apply indexed buffer bindings
if (buffers?.uniformBufferBindings) {
for (let index = 0; index < GL_MAX_UNIFORM_BUFFER_BINDINGS; index++) {
const bindingOrUndefined = buffers.uniformBufferBindings.get(index);
if (!isDefined(bindingOrUndefined)) {
continue;
}
const binding = bindingOrUndefined as WebGLIndexedBufferBinding;
if (isDefined(binding.buffer)) {
// Validate buffer exists
if (!validateBuffer(binding.buffer, `UNIFORM_BUFFER at index ${index}`)) {
continue;
}
const size = binding.size as number;
const offset = binding.offset as number;
// Per WebGLState.ts bindBufferBase logic: size=0 means entire buffer (bindBufferBase)
// size>0 means specific range (bindBufferRange)
if (size === 0) {
// bindBufferBase was used originally
gl.bindBufferBase(gl.UNIFORM_BUFFER, index, binding.buffer);
} else {
// bindBufferRange was used originally
gl.bindBufferRange(gl.UNIFORM_BUFFER, index, binding.buffer, offset, size);
}
}
}
}
if (buffers?.transformFeedbackBufferBindings) {
for (let index = 0; index < GL_MAX_TRANSFORM_FEEDBACK_SEPARATE_ATTRIBS; index++) {
const bindingOrUndefined = buffers.transformFeedbackBufferBindings.get(index);
if (!isDefined(bindingOrUndefined)) {
continue;
}
const binding = bindingOrUndefined as WebGLIndexedBufferBinding;
if (isDefined(binding.buffer)) {
// Validate buffer exists
if (!validateBuffer(binding.buffer, `TRANSFORM_FEEDBACK_BUFFER at index ${index}`)) {
continue;
}
const size = binding.size as number;
const offset = binding.offset as number;
// Per WebGLState.ts bindBufferBase logic: size=0 means entire buffer (bindBufferBase)
// size>0 means specific range (bindBufferRange)
if (size === 0) {
// bindBufferBase was used originally
gl.bindBufferBase(gl.TRANSFORM_FEEDBACK_BUFFER, index, binding.buffer);
} else {
// bindBufferRange was used originally
gl.bindBufferRange(gl.TRANSFORM_FEEDBACK_BUFFER, index, binding.buffer, offset, size);
}
}
}
}
}
/**
* Apply vertex array object state
*/
function applyVertexArrayState(
gl: WebGL2RenderingContext,
vertexArrays: WebGLVertexArrayState | undefined,
current?: WebGLVertexArrayState
): void {
// TODO: Currently only restores GL state for the currently bound VAO.
// This means if you have multiple VAOs configured, save state, switch VAOs,
// and restore, the non-current VAO states will not be restored in the tracker.
// To fix this, we would need to:
// 1. Copy all VAO states from saved state to current tracker's vaoStates Map
// 2. Ensure this doesn't break existing behavior or cause performance issues
// 3. Handle edge cases where VAOs are deleted between save and restore
// Bind the VAO
if (isPropertyDefined(vertexArrays, 'vertexArrayObject')) {
const vaoToBind = vertexArrays!.vertexArrayObject;
// Check if the VAO was deleted between save and restore
// Since bindVertexArray now ensures all bound VAOs have vaoStates entries,
// if the VAO is missing from current.vaoStates, it means it was deleted
if (vaoToBind !== null && current && !current.vaoStates.has(vaoToBind)) {
console.warn(
'[WebGLStateApply] Cannot restore VAO state: the saved VAO has been deleted. ' +
'Binding will revert to default VAO (null).'
);
gl.bindVertexArray(null);
return;
}
gl.bindVertexArray(vaoToBind);
} else if (isDefined(current?.vertexArrayObject)) {
gl.bindVertexArray(null);
}
// Restore per-VAO state for the currently bound VAO
// Per OpenGL ES 3.0 spec section 2.10: ELEMENT_ARRAY_BUFFER and vertex attributes are per-VAO state
const vao =
vertexArrays?.vertexArrayObject === GLUndefined ? null : vertexArrays?.vertexArrayObject;
const vaoState = vertexArrays?.vaoStates.get(vao!);
if (vaoState) {
// Restore ELEMENT_ARRAY_BUFFER binding for this VAO
if (isPropertyDefined(vaoState, 'elementArrayBuffer')) {
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, vaoState.elementArrayBuffer);
}
// Restore vertex attribute state
if (vaoState.attributes) {
// Iterate through all possible vertex attribute indices
for (let idx = 0; idx < GL_MAX_VERTEX_ATTRIBS; idx++) {
const attribOrUndefined = vaoState.attributes.get(idx);
// Skip undefined attributes (never been set)
if (!isDefined(attribOrUndefined)) {
continue;
}
const attrib = attribOrUndefined as WebGLVertexAttribState;
// Check if we have a complete vertex attribute configuration
const hasCompleteConfig =
isDefined(attrib.size) &&
isDefined(attrib.type) &&
isDefined(attrib.stride) &&
isDefined(attrib.offset);
// Only restore this attribute if we have complete configuration OR if we're explicitly disabling it
const shouldRestore = hasCompleteConfig || (isDefined(attrib.enabled) && !attrib.enabled);
if (!shouldRestore) {
// Skip this attribute - incomplete configuration and not explicitly disabled
continue;
}
// Bind the buffer that was associated with this attribute if defined
if (isDefined(attrib.buffer)) {
gl.bindBuffer(gl.ARRAY_BUFFER, attrib.buffer);
}
// Restore vertexAttribPointer configuration if all required parameters are defined
if (hasCompleteConfig) {
// Restore the vertex attribute pointer
if (isDefined(attrib.normalized)) {
gl.vertexAttribPointer(
idx,
attrib.size as number,
attrib.type as number,
attrib.normalized as boolean,
attrib.stride as number,
attrib.offset as number
);
} else {
// For vertexAttribIPointer (integer attributes)
gl.vertexAttribIPointer(
idx,
attrib.size as number,
attrib.type as number,
attrib.stride as number,
attrib.offset as number
);
}
}
// Handle enable/disable state
if (isDefined(attrib.enabled)) {
if (attrib.enabled) {
// Only enable if we have complete configuration
if (hasCompleteConfig) {
gl.enableVertexAttribArray(idx);
}
} else {
// Always allow disabling, even without complete configuration
gl.disableVertexAttribArray(idx);
}
}
if (isDefined(attrib.divisor)) {
gl.vertexAttribDivisor(idx, attrib.divisor as number);
}
}
} // end if (vaoState.attributes)
} // end if (vaoState)
}
/**
* Apply texture binding state
*/
function applyTextureState(
gl: WebGL2RenderingContext,
textures: WebGLTextureState | undefined,
current?: WebGLTextureState
): void {
// Set active texture unit first
if (isPropertyDefined(textures, 'activeTexture')) {
gl.activeTexture(textures!.activeTexture as number);
} else if (isDefined(current?.activeTexture)) {
gl.activeTexture(gl.TEXTURE0);
}
// Bind textures to their respective units
if (textures?.textureUnits) {
for (let unitIndex = 0; unitIndex < GL_MAX_COMBINED_TEXTURE_IMAGE_UNITS; unitIndex++) {
const unitKey = `TEXTURE${unitIndex}`;
const unitStateOrUndefined = textures.textureUnits.get(unitKey);
if (!isDefined(unitStateOrUndefined)) {
continue;
}
const unitState = unitStateOrUndefined as WebGLTextureUnitState;
gl.activeTexture(gl.TEXTURE0 + unitIndex);
if (isDefined(unitState.texture2D)) {
gl.bindTexture(gl.TEXTURE_2D, unitState.texture2D);
}
if (isDefined(unitState.textureCubeMap)) {
gl.bindTexture(gl.TEXTURE_CUBE_MAP, unitState.textureCubeMap);
}
if (isDefined(unitState.texture3D)) {
gl.bindTexture(gl.TEXTURE_3D, unitState.texture3D);
}
if (isDefined(unitState.texture2DArray)) {
gl.bindTexture(gl.TEXTURE_2D_ARRAY, unitState.texture2DArray);
}
}
// Restore the active texture unit
if (isPropertyDefined(textures, 'activeTexture')) {
gl.activeTexture(textures!.activeTexture as number);
}
}
}
/**
* Apply sampler binding state
*/
function applySamplerState(
gl: WebGL2RenderingContext,
samplers: WebGLSamplerState | undefined,
current?: WebGLSamplerState
): void {
if (samplers?.samplerBindings) {
for (let unit = 0; unit < GL_MAX_COMBINED_TEXTURE_IMAGE_UNITS; unit++) {
const sampler = samplers.samplerBindings[unit];
if (isDefined(sampler)) {
gl.bindSampler(unit, sampler);
}
}
} else if (current?.samplerBindings) {
// Reset all sampler bindings to null
for (let unit = 0; unit < GL_MAX_COMBINED_TEXTURE_IMAGE_UNITS; unit++) {
if (isDefined(current.samplerBindings[unit])) {
gl.bindSampler(unit, null);
}
}
}
}
/**
* Apply program binding state
*/
function applyProgramState(
gl: WebGL2RenderingContext,
programs: WebGLProgramState | undefined,
current?: WebGLProgramState
): void {
if (isPropertyDefined(programs, 'currentProgram')) {
gl.useProgram(programs!.currentProgram);
} else if (isDefined(current?.currentProgram)) {
gl.useProgram(null);
}
}
/**
* Apply framebuffer binding and attachment state
*/
function applyFramebufferState(
gl: WebGL2RenderingContext,
framebuffers: WebGLFramebufferState | undefined,
current?: WebGLFramebufferState
): void {
// Bind framebuffers first
if (isPropertyDefined(framebuffers, 'framebuffer')) {
gl.bindFramebuffer(gl.FRAMEBUFFER, framebuffers!.framebuffer);
} else if (isDefined(current?.framebuffer)) {
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
}
if (isPropertyDefined(framebuffers, 'drawFramebuffer')) {
gl.bindFramebuffer(gl.DRAW_FRAMEBUFFER, framebuffers!.drawFramebuffer);
} else if (isDefined(current?.drawFramebuffer)) {
gl.bindFramebuffer(gl.DRAW_FRAMEBUFFER, null);
}
if (isPropertyDefined(framebuffers, 'readFramebuffer')) {
gl.bindFramebuffer(gl.READ_FRAMEBUFFER, framebuffers!.readFramebuffer);
} else if (isDefined(current?.readFramebuffer)) {
gl.bindFramebuffer(gl.READ_FRAMEBUFFER, null);
}
// Apply default framebuffer state (only drawBuffers and readBuffer)
// NOTE: We do NOT apply attachments to the default framebuffer (null) because:
// 1. The default framebuffer is provided by the canvas/context
// 2. You cannot call framebufferTexture2D/framebufferRenderbuffer on the default framebuffer
// 3. Attempting to do so results in INVALID_OPERATION errors
const isDefaultDrawFB = framebuffers?.drawFramebuffer === null;
const hasDefaultFBState = framebuffers?.defaultFramebufferState;
// Only apply drawBuffers and readBuffer for default framebuffer
if (isDefaultDrawFB && hasDefaultFBState) {
const fbState = framebuffers!.defaultFramebufferState;
// Apply draw buffers (this IS valid for default framebuffer)
if (isDefined(fbState.drawBuffers)) {
gl.drawBuffers(fbState.drawBuffers as number[]);
}
// Apply read buffer (this IS valid for default framebuffer)
if (isDefined(fbState.readBuffer)) {
gl.readBuffer(fbState.readBuffer as number);
}
}
}
/**
* Apply renderbuffer binding state
*/
function applyRenderbufferState(
gl: WebGL2RenderingContext,
renderbuffer: WebGLRenderbufferState | undefined,
current?: WebGLRenderbufferState
): void {
if (isPropertyDefined(renderbuffer, 'renderbuffer')) {
gl.bindRenderbuffer(gl.RENDERBUFFER, renderbuffer!.renderbuffer);
} else if (isDefined(current?.renderbuffer)) {
gl.bindRenderbuffer(gl.RENDERBUFFER, null);
}
}
/**
* Apply transform feedback state
*/
function applyTransformFeedbackState(
gl: WebGL2RenderingContext,
transformFeedback: WebGLTransformFeedbackState | undefined,
current?: WebGLTransformFeedbackState
): void {
if (isPropertyDefined(transformFeedback, 'transformFeedback')) {
gl.bindTransformFeedback(gl.TRANSFORM_FEEDBACK, transformFeedback!.transformFeedback);
} else if (isDefined(current?.transformFeedback)) {
gl.bindTransformFeedback(gl.TRANSFORM_FEEDBACK, null);
}
if (
isPropertyDefined(transformFeedback, 'transformFeedbackActive') &&
transformFeedback!.transformFeedbackActive
) {
// Note: beginTransformFeedback requires a primitive mode parameter
// which isn't tracked, so this is a simplified version
// gl.beginTransformFeedback(primitiveMode);
}
if (
isPropertyDefined(transformFeedback, 'transformFeedbackPaused') &&
transformFeedback!.transformFeedbackPaused
) {
gl.pauseTransformFeedback();
}
}
/**
* Apply viewport and scissor state
*/
function applyViewportState(
gl: WebGL2RenderingContext,
viewport: WebGLViewportState | undefined,
current?: WebGLViewportState
): void {
// Viewport - default: typically [0, 0, canvas.width, canvas.height]
if (isPropertyDefined(viewport, 'viewport')) {
const vp = viewport!.viewport as Int32Array;
gl.viewport(vp[0], vp[1], vp[2], vp[3]);
} else if (isDefined(current?.viewport)) {
// Reset to canvas dimensions
const canvas = gl.canvas;
gl.viewport(0, 0, canvas.width, canvas.height);
}
// Scissor box - default: typically [0, 0, canvas.width, canvas.height]
if (isPropertyDefined(viewport, 'scissorBox')) {
const scissor = viewport!.scissorBox as Int32Array;
gl.scissor(scissor[0], scissor[1], scissor[2], scissor[3]);
} else if (isDefined(current?.scissorBox)) {
// Reset to canvas dimensions
const canvas = gl.canvas;
gl.scissor(0, 0, canvas.width, canvas.height);
}
}
/**
* Apply capability enable/disable state
* @param gl - The WebGL2RenderingContext
* @param capabilities - The desired capability state (undefined resets all to defaults)
* @param currentCapabilities - Optional current capability state. If provided and a capability
* is currently enabled (true) but not defined in capabilities,
* it will be explicitly disabled.
*/
function applyCapabilityState(
gl: WebGL2RenderingContext,
capabilities: WebGLCapabilityState | undefined,
currentCapabilities?: WebGLCapabilityState
): void {
// Helper to apply a capability state
const applyCapability = (cap: keyof WebGLCapabilityState, glEnum: number) => {
const desired = capabilities?.[cap];
const current = currentCapabilities?.[cap];
if (isDefined(desired)) {
// If desired state is defined, apply it
desired ? gl.enable(glEnum) : gl.disable(glEnum);
} else if (isDefined(current) && current === true) {
// If desired state is undefined but current is enabled, disable it
gl.disable(glEnum);
}
};
applyCapability('blend', gl.BLEND);
applyCapability('cullFace', gl.CULL_FACE);
applyCapability('depthTest', gl.DEPTH_TEST);
applyCapability('dither', gl.DITHER);
applyCapability('polygonOffsetFill', gl.POLYGON_OFFSET_FILL);
applyCapability('sampleAlphaToCoverage', gl.SAMPLE_ALPHA_TO_COVERAGE);
applyCapability('sampleCoverage', gl.SAMPLE_COVERAGE);
applyCapability('scissorTest', gl.SCISSOR_TEST);
applyCapability('stencilTest', gl.STENCIL_TEST);
applyCapability('rasterDiscard', gl.RASTERIZER_DISCARD);
}
/**
* Apply clear value state
*/
function applyClearState(
gl: WebGL2RenderingContext,
clear: WebGLClearState | undefined,
current?: WebGLClearState
): void {
// ColorClearValue - default: [0, 0, 0, 0]
if (isPropertyDefined(clear, 'colorClearValue')) {
const color = clear!.colorClearValue as Float32Array;
gl.clearColor(color[0], color[1], color[2], color[3]);
} else if (isDefined(current?.colorClearValue)) {
gl.clearColor(0, 0, 0, 0);
}
// DepthClearValue - default: 1
if (isPropertyDefined(clear, 'depthClearValue')) {
gl.clearDepth(clear!.depthClearValue as number);
} else if (isDefined(current?.depthClearValue)) {
gl.clearDepth(1);
}
// StencilClearValue - default: 0
if (isPropertyDefined(clear, 'stencilClearValue')) {
gl.clearStencil(clear!.stencilClearValue as number);
} else if (isDefined(current?.stencilClearValue)) {
gl.clearStencil(0);
}
}
/**
* Apply blend state
*/
function applyBlendState(
gl: WebGL2RenderingContext,
blend: WebGLBlendState | undefined,
current?: WebGLBlendState
): void {
// BlendColor - default: [0, 0, 0, 0]
if (isPropertyDefined(blend, 'blendColor')) {
const color = blend!.blendColor as Float32Array;
gl.blendColor(color[0], color[1], color[2], color[3]);
} else if (isDefined(current?.blendColor)) {
gl.blendColor(0, 0, 0, 0);
}
// BlendEquation - default: FUNC_ADD for both RGB and Alpha
if (
isPropertyDefined(blend, 'blendEquationRgb') &&
isPropertyDefined(blend, 'blendEquationAlpha')
) {
gl.blendEquationSeparate(
blend!.blendEquationRgb as number,
blend!.blendEquationAlpha as number
);
} else if (isDefined(current?.blendEquationRgb) && isDefined(current?.blendEquationAlpha)) {
gl.blendEquationSeparate(gl.FUNC_ADD, gl.FUNC_ADD);
}
// BlendFunc - default: src=ONE, dst=ZERO for both RGB and Alpha
if (
isPropertyDefined(blend, 'blendSrcRgb') &&
isPropertyDefined(blend, 'blendDstRgb') &&
isPropertyDefined(blend, 'blendSrcAlpha') &&
isPropertyDefined(blend, 'blendDstAlpha')
) {
gl.blendFuncSeparate(
blend!.blendSrcRgb as number,
blend!.blendDstRgb as number,
blend!.blendSrcAlpha as number,
blend!.blendDstAlpha as number
);
} else if (
isDefined(current?.blendSrcRgb) &&
isDefined(current?.blendDstRgb) &&
isDefined(current?.blendSrcAlpha) &&
isDefined(current?.blendDstAlpha)
) {
gl.blendFuncSeparate(gl.ONE, gl.ZERO, gl.ONE, gl.ZERO);
}
}
/**
* Apply depth state
*/
function applyDepthState(
gl: WebGL2RenderingContext,
depth: WebGLDepthState | undefined,
current?: WebGLDepthState
): void {
// DepthFunc - default: LESS
if (isPropertyDefined(depth, 'depthFunc')) {
gl.depthFunc(depth!.depthFunc as number);
} else if (isDefined(current?.depthFunc)) {
gl.depthFunc(gl.LESS);
}
// DepthWritemask - default: true
if (isPropertyDefined(depth, 'depthWritemask')) {
gl.depthMask(depth!.depthWritemask as boolean);
} else if (isDefined(current?.depthWritemask)) {
gl.depthMask(true);
}
// DepthRange - default: [0, 1]
if (isPropertyDefined(depth, 'depthRange')) {
const range = depth!.depthRange as Float32Array;
gl.depthRange(range[0], range[1]);
} else if (isDefined(current?.depthRange)) {
gl.depthRange(0, 1);
}
}
/**
* Apply stencil state
*/
function applyStencilState(
gl: WebGL2RenderingContext,
stencil: WebGLStencilState | undefined,
current?: WebGLStencilState
): void {
// Front face stencil func - default: ALWAYS, ref=0, mask=0xFFFFFFFF
if (
isPropertyDefined(stencil, 'stencilFunc') &&
isPropertyDefined(stencil, 'stencilRef') &&
isPropertyDefined(stencil, 'stencilValueMask')
) {
gl.stencilFuncSeparate(
gl.FRONT,
stencil!.stencilFunc as number,
stencil!.stencilRef as number,
stencil!.stencilValueMask as number
);
} else if (
isDefined(current?.stencilFunc) &&
isDefined(current?.stencilRef) &&
isDefined(current?.stencilValueMask)
) {
gl.stencilFuncSeparate(gl.FRONT, gl.ALWAYS, 0, 0xffffffff);
}
// Front face stencil writemask - default: 0xFFFFFFFF
if (isPropertyDefined(stencil, 'stencilWritemask')) {
gl.stencilMaskSeparate(gl.FRONT, stencil!.stencilWritemask as number);
} else if (isDefined(current?.stencilWritemask)) {
gl.stencilMaskSeparate(gl.FRONT, 0xffffffff);
}
// Front face stencil operations - default: KEEP for all
if (
isPropertyDefined(stencil, 'stencilFail') &&
isPropertyDefined(stencil, 'stencilPassDepthFail') &&
isPropertyDefined(stencil, 'stencilPassDepthPass')
) {
gl.stencilOpSeparate(
gl.FRONT,
stencil!.stencilFail as number,
stencil!.stencilPassDepthFail as number,
stencil!.stencilPassDepthPass as number
);
} else if (
isDefined(current?.stencilFail) &&
isDefined(current?.stencilPassDepthFail) &&
isDefined(current?.stencilPassDepthPass)
) {
gl.stencilOpSeparate(gl.FRONT, gl.KEEP, gl.KEEP, gl.KEEP);
}
// Back face stencil func - default: ALWAYS, ref=0, mask=0xFFFFFFFF
if (
isPropertyDefined(stencil, 'stencilBackFunc') &&
isPropertyDefined(stencil, 'stencilBackRef') &&
isPropertyDefined(stencil, 'stencilBackValueMask')
) {
gl.stencilFuncSeparate(
gl.BACK,
stencil!.stencilBackFunc as number,
stencil!.stencilBackRef as number,
stencil!.stencilBackValueMask as number
);
} else if (
isDefined(current?.stencilBackFunc) &&
isDefined(current?.stencilBackRef) &&
isDefined(current?.stencilBackValueMask)
) {
gl.stencilFuncSeparate(gl.BACK, gl.ALWAYS, 0, 0xffffffff);
}
// Back face stencil writemask - default: 0xFFFFFFFF
if (isPropertyDefined(stencil, 'stencilBackWritemask')) {
gl.stencilMaskSeparate(gl.BACK, stencil!.stencilBackWritemask as number);
} else if (isDefined(current?.stencilBackWritemask)) {
gl.stencilMaskSeparate(gl.BACK, 0xffffffff);
}
// Back face stencil operations - default: KEEP for all
if (
isPropertyDefined(stencil, 'stencilBackFail') &&
isPropertyDefined(stencil, 'stencilBackPassDepthFail') &&
isPropertyDefined(stencil, 'stencilBackPassDepthPass')
) {
gl.stencilOpSeparate(
gl.BACK,
stencil!.stencilBackFail as number,
stencil!.stencilBackPassDepthFail as number,
stencil!.stencilBackPassDepthPass as number
);
} else if (
isDefined(current?.stencilBackFail) &&
isDefined(current?.stencilBackPassDepthFail) &&
isDefined(current?.stencilBackPassDepthPass)
) {
gl.stencilOpSeparate(gl.BACK, gl.KEEP, gl.KEEP, gl.KEEP);
}
}
/**
* Apply color write mask state
*/
function applyColorState(
gl: WebGL2RenderingContext,
color: WebGLColorState | undefined,
current?: WebGLColorState
): void {
// ColorWritemask - default: [true, true, true, true]
if (isPropertyDefined(color, 'colorWritemask')) {
const mask = color!.colorWritemask as boolean[];
gl.colorMask(mask[0], mask[1], mask[2], mask[3]);
} else if (isDefined(current?.colorWritemask)) {
gl.colorMask(true, true, true, true);
}
}
/**
* Apply culling state
*/
function applyCullingState(
gl: WebGL2RenderingContext,
culling: WebGLCullingState | undefined,
current?: WebGLCullingState
): void {
// CullFaceMode - default: BACK
if (isPropertyDefined(culling, 'cullFaceMode')) {
gl.cullFace(culling!.cullFaceMode as number);
} else if (isDefined(current?.cullFaceMode)) {
gl.cullFace(gl.BACK);
}
// FrontFace - default: CCW (counter-clockwise)
if (isPropertyDefined(culling, 'frontFace')) {
gl.frontFace(culling!.frontFace as number);
} else if (isDefined(current?.frontFace)) {
gl.frontFace(gl.CCW);
}
}
/**
* Apply line width state
*/
function applyLineState(
gl: WebGL2RenderingContext,
line: WebGLLineState | undefined,
current?: WebGLLineState
): void {
// LineWidth - default: 1
if (isPropertyDefined(line, 'lineWidth')) {
gl.lineWidth(line!.lineWidth as number);
} else if (isDefined(current?.lineWidth)) {
gl.lineWidth(1);
}
}
/**
* Apply polygon offset state
*/
function applyPolygonOffsetState(
gl: WebGL2RenderingContext,
polygonOffset: WebGLPolygonOffsetState | undefined,
current?: WebGLPolygonOffsetState
): void {
// PolygonOffset - default: factor=0, units=0
if (
isPropertyDefined(polygonOffset, 'polygonOffsetFactor') &&
isPropertyDefined(polygonOffset, 'polygonOffsetUnits')
) {
gl.polygonOffset(
polygonOffset!.polygonOffsetFactor as number,
polygonOffset!.polygonOffsetUnits as number
);
} else if (isDefined(current?.polygonOffsetFactor) && isDefined(current?.polygonOffsetUnits)) {
gl.polygonOffset(0, 0);
}
}
/**
* Apply sample coverage state
*/
function applySampleState(
gl: WebGL2RenderingContext,
sample: WebGLSampleState | undefined,
current?: WebGLSampleState
): void {
// SampleCoverage - default: value=1, invert=false
if (
isPropertyDefined(sample, 'sampleCoverageValue') &&
isPropertyDefined(sample, 'sampleCoverageInvert')
) {
gl.sampleCoverage(
sample!.sampleCoverageValue as number,
sample!.sampleCoverageInvert as boolean
);
} else if (isDefined(current?.sampleCoverageValue) && isDefined(current?.sampleCoverageInvert)) {
gl.sampleCoverage(1, false);
}
}
/**
* Apply pixel store parameters
*/
function applyPixelStoreState(
gl: WebGL2RenderingContext,
pixelStore: WebGLPixelStoreState | undefined,
current?: WebGLPixelStoreState
): void {
// PackAlignment - default: 4
if (isPropertyDefined(pixelStore, 'packAlignment')) {
gl.pixelStorei(gl.PACK_ALIGNMENT, pixelStore!.packAlignment as number);
} else if (isDefined(current?.packAlignment)) {
gl.pixelStorei(gl.PACK_ALIGNMENT, 4);
}
// UnpackAlignment - default: 4
if (isPropertyDefined(pixelStore, 'unpackAlignment')) {
gl.pixelStorei(gl.UNPACK_ALIGNMENT, pixelStore!.unpackAlignment as number);
} else if (isDefined(current?.unpackAlignment)) {
gl.pixelStorei(gl.UNPACK_ALIGNMENT, 4);
}
// UnpackFlipY - default: false
if (isPropertyDefined(pixelStore, 'unpackFlipY')) {
gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, pixelStore!.unpackFlipY ? 1 : 0);
} else if (isDefined(current?.unpackFlipY)) {
gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, 0);
}
// UnpackPremultiplyAlpha - default: false
if (isPropertyDefined(pixelStore, 'unpackPremultiplyAlpha')) {
gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, pixelStore!.unpackPremultiplyAlpha ? 1 : 0);
} else if (isDefined(current?.unpackPremultiplyAlpha)) {
gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, 0);
}
// PackRowLength - default: 0
if (isPropertyDefined(pixelStore, 'packRowLength')) {
gl.pixelStorei(gl.PACK_ROW_LENGTH, pixelStore!.packRowLength as number);
} else if (isDefined(current?.packRowLength)) {
gl.pixelStorei(gl.PACK_ROW_LENGTH, 0);
}
// PackSkipPixels - default: 0
if (isPropertyDefined(pixelStore, 'packSkipPixels')) {
gl.pixelStorei(gl.PACK_SKIP_PIXELS, pixelStore!.packSkipPixels as number);
} else if (isDefined(current?.packSkipPixels)) {
gl.pixelStorei(gl.PACK_SKIP_PIXELS, 0);
}
// PackSkipRows - default: 0
if (isPropertyDefined(pixelStore, 'packSkipRows')) {
gl.pixelStorei(gl.PACK_SKIP_ROWS, pixelStore!.packSkipRows as number);
} else if (isDefined(current?.packSkipRows)) {
gl.pixelStorei(gl.PACK_SKIP_ROWS, 0);
}
// UnpackRowLength - default: 0
if (isPropertyDefined(pixelStore, 'unpackRowLength')) {
gl.pixelStorei(gl.UNPACK_ROW_LENGTH, pixelStore!.unpackRowLength as number);
} else if (isDefined(current?.unpackRowLength)) {
gl.pixelStorei(gl.UNPACK_ROW_LENGTH, 0);
}
// UnpackImageHeight - default: 0
if (isPropertyDefined(pixelStore, 'unpackImageHeight')) {
gl.pixelStorei(gl.UNPACK_IMAGE_HEIGHT, pixelStore!.unpackImageHeight as number);
} else if (isDefined(current?.unpackImageHeight)) {
gl.pixelStorei(gl.UNPACK_IMAGE_HEIGHT, 0);
}
// UnpackSkipPixels - default: 0
if (isPropertyDefined(pixelStore, 'unpackSkipPixels')) {
gl.pixelStorei(gl.UNPACK_SKIP_PIXELS, pixelStore!.unpackSkipPixels as number);
} else if (isDefined(current?.unpackSkipPixels)) {
gl.pixelStorei(gl.UNPACK_SKIP_PIXELS, 0);
}
// UnpackSkipRows - default: 0
if (isPropertyDefined(pixelStore, 'unpackSkipRows')) {
gl.pixelStorei(gl.UNPACK_SKIP_ROWS, pixelStore!.unpackSkipRows as number);
} else if (isDefined(current?.unpackSkipRows)) {
gl.pixelStorei(gl.UNPACK_SKIP_ROWS, 0);
}
// UnpackSkipImages - default: 0
if (isPropertyDefined(pixelStore, 'unpackSkipImages')) {
gl.pixelStorei(gl.UNPACK_SKIP_IMAGES, pixelStore!.unpackSkipImages as number);
} else if (isDefined(current?.unpackSkipImages)) {
gl.pixelStorei(gl.UNPACK_SKIP_IMAGES, 0);
}
}
/**
* Apply query state
*/
function applyQueryState(gl: WebGL2RenderingContext, queries: WebGLQueryState): void {
// Note: Query state includes active queries, but we can't directly set them
// as beginQuery requires starting a new query operation.
// This is here for completeness but may need special handling.
if (isDefined(queries.currentAnySamplesPassed) && queries.currentAnySamplesPassed) {
console.warn(
'[WebGLStateApply] Cannot restore active query state for ANY_SAMPLES_PASSED - queries cannot be directly restored'
);
// gl.beginQuery(gl.ANY_SAMPLES_PASSED, queries.currentAnySamplesPassed);
}
if (
isDefined(queries.currentAnySamplesPassedConservative) &&
queries.currentAnySamplesPassedConservative
) {
console.warn(
'[WebGLStateApply] Cannot restore active query state for ANY_SAMPLES_PASSED_CONSERVATIVE - queries cannot be directly restored'
);
// gl.beginQuery(gl.ANY_SAMPLES_PASSED_CONSERVATIVE, queries.currentAnySamplesPassedConservative);
}
if (
isDefined(queries.currentTransformFeedbackPrimitivesWritten) &&
queries.currentTransformFeedbackPrimitivesWritten
) {
console.warn(
'[WebGLStateApply] Cannot restore active query state for TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN - queries cannot be directly restored'
);
// gl.beginQuery(gl.TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN, queries.currentTransformFeedbackPrimitivesWritten);
}
}