engine icon indicating copy to clipboard operation
engine copied to clipboard

Referencing Device in cache Map can cause errors when the app goes behind a Proxy

Open d0rianb opened this issue 2 months ago • 5 comments

I encountered this issue when using a WebComponent that embeds a PlayCanvas application inside a Vue project.
The component instance is accessed through a Vue $ref, which is actually a Proxy object. When I call a method that alter the rendering process on the component from outside (for example to add a gizmo), the application fails to retrieve the GraphicsDevice from the internal cache. This happens because the Proxy changes the memory reference of the application object and all of its childrens, therefore the device lookup no longer matches the original cached reference. https://github.com/playcanvas/engine/blob/4257ba5db9a58b8e57cf1c3c9c3ee3390e6e69c5/src/platform/graphics/device-cache.js#L26-L27 It then try to call the onCreate argument, which is often undefined ex : https://github.com/playcanvas/engine/blob/4257ba5db9a58b8e57cf1c3c9c3ee3390e6e69c5/src/scene/shader-lib/get-program-library.js#L20

Possible fix : I think changing the DeviceCache map from Map<GraphicsDevice, any> to Map<string, any> using the device.canvas.id as key could solve this issue. Maybe it has side effect on AR/VR devices?

d0rianb avatar Nov 06 '25 14:11 d0rianb

Thanks for reporting this issue! I'm working on understanding the root cause before implementing a fix.

Question about the Vue Proxy scenario: You mentioned that the component instance is accessed through a Vue $ref, which is a Proxy object. However, I want to clarify the exact mechanism causing the cache miss, because Vue Proxies typically shouldn't change object identity in the way described. When you access a property through a Vue Proxy like this:

const device1 = vueComponentRef.app.graphicsDevice;
const device2 = vueComponentRef.app.graphicsDevice;
// device1 === device2 should be true

The Proxy should return the same underlying object both times, so Map.has(device) should still work correctly.

mvaligursky avatar Nov 10 '25 11:11 mvaligursky

Thanks for investigating on this issue.

The problem occurs when the initialization happens before the object is handled by the Proxy.
For example, the context is initialized by a web component and, once it is ready, it is then retrieved by the Vue ref.

<template>
  <pc-viewer></pc-viewer>
</template>

<script>
const viewerElement = ref(null)
  
onMounted(() => {
  viewerElement.value = document.querySelector('pc-viewer')
  viewerElement.addRenderElement() // cache error because the graphic context was initialised by the WebComponent
})
</script>

d0rianb avatar Nov 10 '25 11:11 d0rianb

@marklundin - any thoughts on this Mark? Have you had similar issues on React with this?

mvaligursky avatar Nov 10 '25 12:11 mvaligursky

I think the Proxy is specific to Vue. it's not something I've encountered with the react library

marklundin avatar Nov 10 '25 13:11 marklundin

Try to Use key based caching instead of relying on memory reference.

ComeradeCharon avatar Nov 13 '25 12:11 ComeradeCharon

I managed to find a workaround that applies only to Vue. When I use shallowRef instead of ref, it only creates a proxy for the web component itself (non recursive). As a result, the memory addresses of the nested objects remain unchanged.

d0rianb avatar Nov 25 '25 10:11 d0rianb

Sounds great, I'll close the issue for now, thanks for the update.

mvaligursky avatar Nov 25 '25 10:11 mvaligursky