Clearing children from xrextras-named-image-target triggers a warning

I’m working on a project with over 200 image targets, and I’m using pooling to create xrextras-named-image-target dynamically. When an image target is found, a video element is attached as a child to xrextras-named-image-target, but whenever the image target is lost, and I want to remove the child from DOM, I get continuous warnings stating:

xr-simd-24.1.2.2165.js:18 WebGL: INVALID_OPERATION: useProgram: attempt to use a deleted object"

The warning propagation stops when I find an image target again. I can’t understand the erroneous code because it’s minified:

// xr-simd.js

if (Object.keys(y).forEach((function(I) {
    A[I] = function() {
        for (var g, C = arguments.length, Q = new Array(C), B = 0; B < C; B++)
Q[B] = arguments[B];
        var E = (g = H[I]).call.apply(g, [A].concat(Q));  // g IS EMPTY
        return Y.apply(void 0, [I].concat(Q)),
        E
    }
}

this: WebGL2RenderingContext
B: 1
C: 1
E: undefined
Q: [WebGLProgram]
g: undefined

Problem
Everything seems to work despite the warning, but I wonder why the warning happens, and how I get rid of it.

My code
The functionality in short:

  1. Image target is found,
  2. #imageTarget with the ec-named-image-target component is added to the scene via sceneEl.components.pool__imagetarget.requestEntity(),
  3. The content of #imagetarget-video - an a-entity that plays a video, is appended to #imageTarget,
  4. When the image target is lost, the child element #imagetarget-video is removed from DOM.
  5. The warning is triggered.
<!-- index.html -->

<a-scene
  xrextras-loading
  pool__imagetarget="mixin: imagetarget; size: 3; dynamic: true"
  xrextras-runtime-error
  camera-feed-delegator
  renderer="colorManagement:true; webgl2: true;"
  xrweb="disableWorldTracking: true;"
>

  <a-assets>
    <img id="imagetarget-video-thumbnail" crossorigin="anonymous" src="./assets/img/thumbs/video-play-button.png" />

     <!-- "src" is set in dynamicTarget.js when an image target is found -->
    <video id="imagetarget-video-asset"
      playsinline
      crossorigin="anonymous"
      loop="false"
      src=""
    ></video>

     <!-- ec-named-image-target is a modification of xrextras-named-image-target-->
    <a-mixin id="imagetarget" ec-named-image-target></a-mixin>
  </a-assets>

<!-- This entity is injected into #imageTarget in dynamicTarget.js -->
<template id="imagetarget-video">
  <a-entity
    ec-play-video="video: #imagetarget-video-asset; thumb: #imagetarget-video-thumbnail; canstop: true;"
    geometry="primitive: plane; height: 1.38; width: 0.776;"
    material="transparent: true">
  ></a-entity>
</template>
// index.js

import { cameraFeedDelegator } from './src/js/components/image-targeting/camera-feed-delegator.js';
import { ecNamedImageTarget } from './src/js/components/image-targeting/ec-named-image-target.js';

// Register components and primitives
AFRAME.registerComponent('camera-feed-delegator', cameraFeedDelegator);
AFRAME.registerComponent('ec-named-image-target', ecNamedImageTarget);
// ecNamedImageTarget.js - just a modification of xrextras-image-target, but with the event listener for  `xrimagefound` moved to dynamicTargeting.js

const ecNamedImageTarget = {
  schema: {
    name: {type: 'string'},
    target: {type: 'string'}
  },
  init() {
    const {object3D} = this.el

    const updateImage = ({detail}) => {
      if (this.data.name === detail.name) {
        object3D.position.copy(detail.position)
        object3D.quaternion.copy(detail.rotation)
        object3D.scale.set(detail.scale, detail.scale, detail.scale)
        object3D.visible = true
      }
    }

    const removeComponent = ({detail}) => {
      if (this.data.name === detail.name) {
        this.data.name = null;
        object3D.visible = false;

        this.el.innerHTML = '';  // THIS LINE CREATES THE ERROR, when ec-named-image-target is removed from the DOM.
        
        this.el.sceneEl.components.pool__imagetarget.returnEntity(this.el);
      }
    }

    this.el.sceneEl.addEventListener('xrimageupdated', updateImage);
    this.el.sceneEl.addEventListener('xrimagelost', removeComponent);
  },
}

export {ecNamedImageTarget}
// cameraFeedDelegator.js

const cameraFeedDelegator = {
  init() {
    const dynamicTargeting = new DynamicTargeting();

    // sets all the target images that the app should loop through. Not relevant for this case.
    dynamicTargeting.updateTargetElements() 

    window.addEventListener('xrimagefound', ({detail}) => {
        dynamicTargeting.foundTarget(detail.name, this.el.sceneEl);
    });
  
    window.addEventListener('xrimagelost', ({detail}) => {
      dynamicTargeting.lostTarget(detail.name, this.el.sceneEl);
    });
  }
}

export {cameraFeedDelegator}
// dynamicTargeting.js

export default class DynamicTargeting {
    targets = [];
    found = new Set();

    constructor() {
      this.videoEl = document.getElementById("imagetarget-video").content.firstElementChild;
      this.videoAssetEl = document.getElementById("imagetarget-video-asset");
    }

    updateTargetElements(imageTargets) {
        // ... code for looping through image targets, using this.found and this.targets.
    }

    /**
     * Adds ec-named-image-target from the pool to a-scene
     * Always making sure that the found image target is being scanned
     */
    foundTarget(imageTargetName, sceneEl) {
        this.found.add(imageTargetName);
        this.targets.splice(this.targets.indexOf(imageTargetName), 1);
  
        let imageTargetEl = sceneEl.components.pool__imagetarget.requestEntity();
        imageTargetEl.innerHTML = '';  // REMOVING THE CHILD HERE doesn't trigger the error.
  
        imageTargetEl.appendChild(childEl);

        // Sets the name of the image target that is being tracked.
        imageTargetEl.setAttribute('ec-named-image-target', `name: ${imageTargetName}`);
      }
    }

    /**
     * Restores this.targets if the image target is lost.
     */
    lostTarget(imageTargetName, sceneEl) {
      this.found.delete(name);
      this.targets.push(name);
    }
}

Have you tried calling .remove() on the child element instead of removing it by setting the .innerHTML to an empty string?

Sorry to say, but using this.el.firstElementChild?.remove(); from ec-named-target-image results in the same warning. However, if I remove the geometry component from the child component,

  <a-entity
    ec-play-video="video: #imagetarget-video-asset; thumb: #imagetarget-video-thumbnail; canstop: true;"
    geometry="primitive: plane; height: 1.38; width: 0.776;"
    material="transparent: true">
  ></a-entity>

I don’t get an error. So there is something to it. Been trying to read up on disposal in Three.js in order to release the geometry component from memory … to no avail.

After a hefty number of hours of reading, exploring, and frequently trying, I got rid of the error message.

Instead of using this.el.innerHTML = '' in ecNamedmageTarget.js, I could remove the child with the following two lines:

this.el.firstElementChild.removeFromParent();
this.el.firstElementChild.remove();

Thank you for pointing me in another direction, @Evan!

2 Likes

Glad to hear you got it working. Thanks for sharing your solution!

1 Like

This topic was automatically closed 4 days after the last reply. New replies are no longer allowed.