Repository: napari/napari

Issue:
OpenGL runtime error when a multiscale image is moved outside the canvas
## 🐛 Bug

When viewing a multiscale 2D layer, if the object is moved out of the viewer window, a RuntimeError error is raised:
`RuntimeError: OpenGL got errors (Check before draw): GL_INVALID_VALUE`

This is illustrated here:
![Peek 2021-10-31 17-23](https://user-images.githubusercontent.com/6528957/139602114-cb4a4b82-bee2-426c-85e8-e5ae0c221465.gif)

## To Reproduce


Steps to reproduce the behavior:

1. Run the script below to generate a simple multi-scale layer
2. From the viewer, drag the rectangle until it is not visible and an error window will pop up

```Python
import napari
import numpy as np

with napari.gui_qt():
    viewer = napari.Viewer()
    img = np.ones((256, 512))
    img_multi = [img[::s, ::s] for s in (1, 2, 4)]
    viewer.add_image(img_multi)
```


<!-- If you have a code sample, error messages, stack traces, please provide it here as well -->


## Expected behavior

I would not expect this to cause an error (i.e. the same behavior as a non-multiscale 2D view).

## Environment
```
napari: 0.4.12.dev8+g3cb4b85d.d20210923
Platform: Linux-5.10.0-1050-oem-x86_64-with-glibc2.10
System: Ubuntu 20.04.3 LTS
Python: 3.8.10 | packaged by conda-forge | (default, May 11 2021, 07:01:05) [GCC 9.3.0]
Qt: 5.12.9
PyQt5: 5.12.3
NumPy: 1.21.2
SciPy: 1.7.0
Dask: 2021.09.1+11.gaa559ca4
VisPy: 0.9.3

OpenGL:
- GL version: 4.6.0 NVIDIA 465.19.01
- MAX_TEXTURE_SIZE: 32768

Screens:
- screen 1: resolution 1920x1080, scale 2.0

Plugins:
- nibabel: 0.1.dev6+g4395ab5.d20210831
- scikit-image: 0.4.12.dev8+g3cb4b85d.d20210923
```

## Additional context

<!-- Add any other context about the problem here. -->



Discussion:
Ha, interesting. I guess VisPy isn't too happy if we send an empty array to render. 😬 I guess we can set a minimum crop size and render the minimum off-canvas.  Thoughts @sofroniewn?

e.g. we could add a check here:

https://github.com/napari/napari/blob/785e49a1e4a54f10a09ce87fccb95dc19e3886d5/napari/layers/base/base.py#L1420-L1434

and default corners to a 1-pixel-by-1-pixel "image". 

Resolving patch:
diff --git a/napari/layers/base/base.py b/napari/layers/base/base.py
--- a/napari/layers/base/base.py
+++ b/napari/layers/base/base.py
@@ -1537,6 +1537,11 @@ def _update_draw(
             corners = np.zeros((2, self.ndim))
             corners[:, displayed_axes] = scaled_corners
             corners = corners.astype(int)
+            display_shape = tuple(
+                corners[1, displayed_axes] - corners[0, displayed_axes]
+            )
+            if any(s == 0 for s in display_shape):
+                return
             if self.data_level != level or not np.all(
                 self.corner_pixels == corners
             ):


Test patch:
diff --git a/napari/_qt/_tests/test_qt_viewer.py b/napari/_qt/_tests/test_qt_viewer.py
--- a/napari/_qt/_tests/test_qt_viewer.py
+++ b/napari/_qt/_tests/test_qt_viewer.py
@@ -595,3 +595,26 @@ def test_remove_add_image_3D(make_napari_viewer):
     layer = viewer.add_image(img)
     viewer.layers.remove(layer)
     viewer.layers.append(layer)
+
+
+@skip_on_win_ci
+@skip_local_popups
+def test_qt_viewer_multscale_image_out_of_view(make_napari_viewer):
+    """Test out-of-view multiscale image viewing fix.
+
+    Just verifies that no RuntimeError is raised in this scenario.
+
+    see: https://github.com/napari/napari/issues/3863.
+    """
+    # show=True required to test fix for OpenGL error
+    viewer = make_napari_viewer(ndisplay=2, show=True)
+    viewer.add_shapes(
+        data=[
+            np.array(
+                [[1500, 4500], [4500, 4500], [4500, 1500], [1500, 1500]],
+                dtype=float,
+            )
+        ],
+        shape_type=['polygon'],
+    )
+    viewer.add_image([np.eye(1024), np.eye(512), np.eye(256)])