Adding to what Deltakosh said, because the memory answer explains the context loss but not the two things that come after it — and the second one has had no answer here at all.
Why restoreContext() leaves you with a black canvas
Babylon can only rebuild what it can re-fetch. A Texture created from a URL is restorable: the engine re-requests it. A RawTexture, a DynamicTexture, a RenderTargetTexture, or anything built from an ArrayBuffer or a Blob:/ObjectURL you have since revoked is not — there is no source to go back to. If your materials use any of those, restore completes “successfully” and draws nothing, which is exactly the black box you describe.
There is also a flag worth checking before you touch anything else: engine.doNotHandleContextLost. Left at its default false, Babylon keeps a CPU-side copy of vertex and index buffer data specifically so it can rebuild after a loss. That copy is real memory, in the same process that is already running out of it. So the setting that makes restore work is also making the loss more likely — and if some code in your stack has set it to true to save memory, restore cannot work by construction. Whichever way that flag currently sits, it is probably not sitting there deliberately.
The multi-touch lockup is a separate bug and not a WebGL one
An ArcRotateCamera that locks when touches overlap with an overlay is almost always lost pointer capture, not a camera problem. Two things to check, in this order:
touch-action: none on the canvas element itself, not on a parent. Without it the WebView keeps its own scroll/zoom gesture recogniser alive, decides mid-gesture that the sequence belongs to it, and fires pointercancel. Babylon’s input handler treats pointerup and pointercancel differently, and a cancel arriving for one of two active pointers leaves the camera with a pointer id it will never see released. That is the lock.
The overlay scripts are the usual trigger: a listener that calls preventDefault() on touchmove while the canvas is also receiving pointer events makes the two event models disagree about who owns the gesture. Log pointerdown/pointerup/pointercancel with event.pointerId and reproduce the lock once — you will see a pointercancel with no matching release immediately before it, or you will not, and either way it settles the question in one run.
One measurement worth making before any of this
Sum your textures at their actual resident size rather than their file size: width × height × 4 bytes, plus roughly a third again for mipmaps. A single 4096×4096 RGBA texture is 67 MB on the GPU no matter that the PNG was 3 MB, and the WebKit shell you are in has a smaller GPU budget than the Safari that runs the same scene at sixty. Five of those and you are over on a mid-range device before the first frame.
If the number comes out large, GPU-compressed textures are the fix that actually changes the arithmetic — KTX2/Basis stays compressed in VRAM instead of being expanded at upload, which is a 4–6× cut in resident memory rather than a smaller download. Nothing else on the list moves the number that far.
Two questions, because they change which of the above matters:
- What is the resident texture total, and are you on KTX2/Basis or on PNG/JPEG?
- Does a
pointercancel fire immediately before the camera locks?
I build browser 3D for a living — mostly industrial configurators that have to survive whatever mid-range Android is actually on the shop floor, so this failure mode is familiar.