Repository: pygfx/pygfx
Pull request #222 (2021-12-21 15:33:46)

Updates for latest wgpu-py
* Pin to latest wgpu.
* The shader `[[block]]` has been removed.
* The total size of a uniform struct must now be a multiple of 16.
* Flush now submits its own commands, fixing the examples that used a manual flush. This is possible now that #219 is no longer an issue.
* Fix issue with line starts being all weird (shader does not like if we try indexing out of bounds?)
* Remove depth pick (caused an error related to depth clipping now, but we wanted to remove it anyway).
* Rename the simple1 and simple2 blend modes to ordered1 and "ordered2.
* I now know that recorded commands cannot be re-used - need to use render bundles for that .... mañana.

This ticks off the last items in #196!

almarklein: I'm running into a few more quircks.
almarklein: Ready. I'll go over all the examples, making sure they work, and converting to `wgou.gui.auto`, in a next PR.

Patch:
diff --git a/examples/custom_object.py b/examples/custom_object.py
index 003aa1f..424b375 100644
--- a/examples/custom_object.py
+++ b/examples/custom_object.py
@@ -24,7 +24,6 @@ class TriangleMaterial(gfx.Material):
 
 shader_source = """
 
-[[block]]
 struct Stdinfo {
     cam_transform: mat4x4<f32>;
     cam_transform_inv: mat4x4<f32>;
diff --git a/examples/line_basic.py b/examples/line_basic.py
index 59185d1..b167716 100644
--- a/examples/line_basic.py
+++ b/examples/line_basic.py
@@ -16,7 +16,6 @@ renderer = gfx.WgpuRenderer(canvas)
 renderer_svg = gfx.SvgRenderer(640, 480, "~/line.svg")
 
 scene = gfx.Scene()
-
 positions = [[200 + np.sin(i) * i * 6, 200 + np.cos(i) * i * 6, 0] for i in range(20)]
 positions += [[400 - np.sin(i) * i * 6, 200 + np.cos(i) * i * 6, 0] for i in range(20)]
 positions += [
diff --git a/examples/picking_color_and_depth.py b/examples/picking_color.py
similarity index 70%
rename from examples/picking_color_and_depth.py
rename to examples/picking_color.py
index 8fe7d28..b8a61f3 100644
--- a/examples/picking_color_and_depth.py
+++ b/examples/picking_color.py
@@ -1,8 +1,6 @@
 """
-Example showing picking the color and depth from the scene. This info
-is always available, regardless of the object being clicked. See the
-other examples for using picking info specific to certain
-objects/materials.
+Example showing picking the color from the scene. Depending on the
+object being clicked, more detailed picking info is available.
 """
 
 import numpy as np
@@ -18,13 +16,6 @@ class PickingWgpuCanvas(WgpuCanvas):
         # Get a dict with info about the clicked location
         xy = event.position().x(), event.position().y()
         info = renderer.get_pick_info(xy)
-        # Add more info
-        pos = gfx.linalg.Vector3(*info["ndc"])
-        pos.apply_matrix4(camera.projection_matrix_inverse)
-        pos.apply_matrix4(camera.matrix_world)
-        info["position"] = tuple(pos.to_array())
-        info["distance_to_camera"] = pos.distance_to(camera.position)
-        # Show
         for key, val in info.items():
             print(key, "=", val)
 
@@ -34,6 +25,9 @@ canvas = PickingWgpuCanvas()
 renderer = gfx.renderers.WgpuRenderer(canvas)
 scene = gfx.Scene()
 
+background = gfx.Background(None, gfx.BackgroundMaterial((0, 1, 0, 1), (0, 0, 1, 1)))
+scene.add(background)
+
 im = imageio.imread("imageio:astronaut.png").astype(np.float32) / 255
 tex = gfx.Texture(im, dim=2).get_view(filter="linear", address_mode="repeat")
 
diff --git a/examples/post_processing1.py b/examples/post_processing1.py
index c447ba0..2657903 100644
--- a/examples/post_processing1.py
+++ b/examples/post_processing1.py
@@ -64,7 +64,6 @@ struct FragmentOutput {
     [[location(1)]] pick: vec4<i32>;
 };
 
-[[block]]
 struct Render {
     opacity: f32;
     time: f32;
diff --git a/examples/scene_overlay.py b/examples/scene_overlay.py
index 456fe94..48c6b74 100644
--- a/examples/scene_overlay.py
+++ b/examples/scene_overlay.py
@@ -35,7 +35,7 @@ scene2 = gfx.Scene()
 
 positions = np.array(
     [
-        [-1, -1, 1],
+        [-1, -1, 0.5],
         [-1, +1, 0.5],
         [+1, +1, 0.5],
         [+1, -1, 0.5],
diff --git a/examples/transparency1.py b/examples/transparency1.py
index 072368b..2d22f13 100644
--- a/examples/transparency1.py
+++ b/examples/transparency1.py
@@ -22,8 +22,8 @@ class WgpuCanvasWithInputEvents(WgpuCanvas):
             m = [
                 None,
                 "opaque",
-                "simple1",
-                "simple2",
+                "ordered1",
+                "ordered2",
                 "weighted",
                 "weighted_depth",
                 "weighted_plus",
diff --git a/examples/transparency2.py b/examples/transparency2.py
index c7a667f..e989e76 100644
--- a/examples/transparency2.py
+++ b/examples/transparency2.py
@@ -42,8 +42,8 @@ class WgpuCanvasWithInputEvents(WgpuCanvas):
             m = [
                 None,
                 "opaque",
-                "simple1",
-                "simple2",
+                "ordered1",
+                "ordered2",
                 "weighted",
                 "weighted_depth",
                 "weighted_plus",
diff --git a/pygfx/renderers/wgpu/_blender.py b/pygfx/renderers/wgpu/_blender.py
index d304b01..a5c0e3c 100644
--- a/pygfx/renderers/wgpu/_blender.py
+++ b/pygfx/renderers/wgpu/_blender.py
@@ -560,21 +560,21 @@ class OpaqueFragmentBlender(BaseFragmentBlender):
     passes = [FullOpaquePass()]
 
 
-class Simple1FragmentBlender(BaseFragmentBlender):
+class Ordered1FragmentBlender(BaseFragmentBlender):
     """A minimal fragment blender that uses the classic alpha blending
     equation, without differentiating between opaque and transparent
     objects. This is a common approach for applications using a single
-    pass.
+    pass. Order dependent.
     """
 
     passes = [SimpleSinglePass()]
 
 
-class Simple2FragmentBlender(BaseFragmentBlender):
+class Ordered2FragmentBlender(BaseFragmentBlender):
     """A first step towards better blending: separating the opaque from
     the transparent fragments, and blending the latter using the alpha
     blending equation. The second step has depth-testing, but no
-    depth-writing. Not order-independent.
+    depth-writing. Order dependent.
     """
 
     passes = [OpaquePass(), SimpleTransparencyPass()]
diff --git a/pygfx/renderers/wgpu/_flusher.py b/pygfx/renderers/wgpu/_flusher.py
index 1e7c3e1..ceb446f 100644
--- a/pygfx/renderers/wgpu/_flusher.py
+++ b/pygfx/renderers/wgpu/_flusher.py
@@ -170,7 +170,6 @@ class RenderFlusher:
         device = self._device
 
         bindings_code = """
-            [[block]]
             struct Render {
                 size: vec2<f32>;
                 sigma: f32;
diff --git a/pygfx/renderers/wgpu/_renderer.py b/pygfx/renderers/wgpu/_renderer.py
index 6e28bcc..2ebe050 100644
--- a/pygfx/renderers/wgpu/_renderer.py
+++ b/pygfx/renderers/wgpu/_renderer.py
@@ -202,7 +202,7 @@ class WgpuRenderer(Renderer):
         # Initialize a small buffer to read pixel info into
         # Make it 256 bytes just in case (for bytes_per_row)
         self._pixel_info_buffer = self._shared.device.create_buffer(
-            size=256,
+            size=16,
             usage=wgpu.BufferUsage.COPY_DST | wgpu.BufferUsage.MAP_READ,
         )
 
@@ -248,13 +248,13 @@ class WgpuRenderer(Renderer):
     def blend_mode(self):
         """The method for handling transparency:
 
-        * "default" or None: Select the default: currently this is "simple2".
+        * "default" or None: Select the default: currently this is "ordered2".
         * "opaque": single-pass approach that consider every fragment opaque.
-        * "simple1": single-pass approach that blends fragments (using alpha blending).
+        * "ordered1": single-pass approach that blends fragments (using alpha blending).
           Can only produce correct results if fragments are drawn from back to front.
-        * "simple2": two-pass approach that first processes all opaque fragments and
-          then blends transparent fragments (using alpha blending) with depth-write disabled.
-          The visual results are usually better than simple1, but depend on the drawing order.
+        * "ordered2": two-pass approach that first processes all opaque fragments and then
+          blends transparent fragments (using alpha blending) with depth-write disabled. The
+          visual results are usually better than ordered1, but still depend on the drawing order.
         * "weighted": two-pass approach that for order independent transparency,
           using alpha weights.
         * "weighted_depth": two-pass approach for order independent transparency,
@@ -273,12 +273,12 @@ class WgpuRenderer(Renderer):
             value = "default"
         value = value.lower()
         if value == "default":
-            value = "simple2"
+            value = "ordered2"
         # Map string input to a class
         m = {
             "opaque": blender_module.OpaqueFragmentBlender,
-            "simple1": blender_module.Simple1FragmentBlender,
-            "simple2": blender_module.Simple2FragmentBlender,
+            "ordered1": blender_module.Ordered1FragmentBlender,
+            "ordered2": blender_module.Ordered2FragmentBlender,
             "weighted": blender_module.WeightedFragmentBlender,
             "weighted_depth": blender_module.WeightedDepthFragmentBlender,
             "weighted_plus": blender_module.WeightedPlusFragmentBlender,
@@ -448,37 +448,30 @@ class WgpuRenderer(Renderer):
                 any_has_changed |= has_changed
                 wobject_tuples.append((wobject, wobject_pipeline))
 
-        # Need a new recording, or can we re-use the previous one?
-        # I *think* that (eventually?) it should be possible to record the commands
-        # and re-submit them, resulting in better performance. But if I try this now
-        # it panics with 'Cannot remove a vacant resource'.
+        # Command buffers cannot be reused. If we want some sort of re-use we should
+        # look into render bundles. See https://github.com/gfx-rs/wgpu-native/issues/154
         # If we do get this to work, we should trigger a new recording
         # when the wobject's children, visibile, render_order, or render_pass changes.
-        need_recording = any_has_changed or True
 
         # Sort objects
         if self.sort_objects:
             sort_func = _get_sort_function(camera)
             wobject_tuples.sort(key=sort_func)
-            need_recording = True
 
-        if need_recording or not hasattr(scene, "_wgpu_command_buffers"):
-
-            # Record the rendering of all world objects, or re-use previous recording
-            command_buffers = []
-            command_buffers += self._render_recording(
-                wobject_tuples, physical_viewport, clear_color
-            )
-            command_buffers += self._blender.perform_combine_pass(self._shared.device)
-            scene._wgpu_command_buffers = command_buffers
+        # Record the rendering of all world objects, or re-use previous recording
+        command_buffers = []
+        command_buffers += self._render_recording(
+            wobject_tuples, physical_viewport, clear_color
+        )
+        command_buffers += self._blender.perform_combine_pass(self._shared.device)
+        command_buffers
 
         # Collect commands and submit
-        command_buffers = []
-        command_buffers += scene._wgpu_command_buffers
-        if flush:
-            command_buffers += self.flush()
         device.queue.submit(command_buffers)
 
+        if flush:
+            self.flush()
+
     def flush(self):
         """Render the result into the target texture view. This method is
         called automatically unless you use ``.render(..., flush=False)``.
@@ -500,12 +493,13 @@ class WgpuRenderer(Renderer):
         # Reset counter (so we can auto-clear the first next draw)
         self._renders_since_last_flush = 0
 
-        return self._flusher.render(
+        command_buffers = self._flusher.render(
             self._blender.color_view,
             None,
             raw_texture_view,
             self._target_tex_format,
         )
+        self.device.queue.submit(command_buffers)
 
     def _render_recording(
         self,
@@ -624,30 +618,24 @@ class WgpuRenderer(Renderer):
         _, logical_size = get_size_from_render_target(self._target)
         float_pos = pos[0] / logical_size[0], pos[1] / logical_size[1]
 
-        can_sample_color = self._blender.color_tex is not None
-
         # Sample
         encoder = self.device.create_command_encoder()
-        self._copy_pixel(encoder, self._blender.depth_tex, float_pos, 0)
-        if can_sample_color:
-            self._copy_pixel(encoder, self._blender.color_tex, float_pos, 8)
-        self._copy_pixel(encoder, self._blender.pick_tex, float_pos, 16)
+        self._copy_pixel(encoder, self._blender.color_tex, float_pos, 0)
+        self._copy_pixel(encoder, self._blender.pick_tex, float_pos, 8)
         queue = self.device.queue
         queue.submit([encoder.finish()])
 
         # Collect data from the buffer
         data = self._pixel_info_buffer.map_read()
-        depth = data[0:4].cast("f")[0]
-        color = tuple(data[8:12].cast("B"))
-        pick_value = tuple(data[16:24].cast("Q"))[0]
+        color = tuple(data[0:4].cast("B"))
+        pick_value = tuple(data[8:16].cast("Q"))[0]
         wobject_id = pick_value & 1048575  # 2**20-1
         wobject = id_provider.get_object_from_id(wobject_id)
         # Note: the position in world coordinates is not included because
         # it depends on the camera, but we don't "own" the camera.
 
         info = {
-            "ndc": (2 * float_pos[0] - 1, 2 * float_pos[1] - 1, depth),
-            "rgba": color if can_sample_color else (0, 0, 0, 0),
+            "rgba": color,
             "world_object": wobject,
         }
 
diff --git a/pygfx/renderers/wgpu/_shadercomposer.py b/pygfx/renderers/wgpu/_shadercomposer.py
index 1861d86..cd9a0ee 100644
--- a/pygfx/renderers/wgpu/_shadercomposer.py
+++ b/pygfx/renderers/wgpu/_shadercomposer.py
@@ -348,7 +348,6 @@ class BaseShader:
 
         structname = "Struct_" + binding.name
         code = f"""
-        [[block]]
         struct {structname} {{
         """.rstrip()
 
@@ -488,7 +487,6 @@ class BaseShader:
 
         # Produce the type definition
         code = f"""
-        [[block]]
         struct {typename} {{
             data: [[stride({stride})]] array<{element_type1}>;
         }};
diff --git a/pygfx/renderers/wgpu/linerender.py b/pygfx/renderers/wgpu/linerender.py
index 25a7aa6..4dde653 100644
--- a/pygfx/renderers/wgpu/linerender.py
+++ b/pygfx/renderers/wgpu/linerender.py
@@ -265,9 +265,10 @@ class LineShader(WorldObjectShader):
             let i = index / 5;
 
             // Sample the current node and it's two neighbours, and convert to NDC
-            let npos1 = get_point_ndc(i - 1);
+            // Note that if we sample out of bounds, this affects the shader in mysterious ways (21-12-2021).
+            let npos1 = get_point_ndc(max(0, i - 1));
             let npos2 = get_point_ndc(i);
-            let npos3 = get_point_ndc(i + 1);
+            let npos3 = get_point_ndc(min(u_renderer.last_i, i + 1));
 
             // Convert to logical screen coordinates, because that's were the lines work
             let ppos1 = (npos1.xy + 1.0) * screen_factor;
@@ -502,7 +503,7 @@ class LineShader(WorldObjectShader):
             $$ endif
 
             // The outer edges with lower alpha for aa are pushed a bit back to avoid artifacts.
-            // This is only necessary for blend method "simple1"
+            // This is only necessary for blend method "ordered1"
             //out.depth = varyings.position.z + 0.0001 * (0.8 - min(0.8, alpha));
 
             return out;
diff --git a/pygfx/renderers/wgpu/meshrender.py b/pygfx/renderers/wgpu/meshrender.py
index d38f184..e429fbe 100644
--- a/pygfx/renderers/wgpu/meshrender.py
+++ b/pygfx/renderers/wgpu/meshrender.py
@@ -219,7 +219,6 @@ class MeshShader(WorldObjectShader):
             transform: mat4x4<f32>;
             id: u32;
         };
-        [[block]]
         struct InstanceInfos {
             data: [[stride(80)]] array<InstanceInfo>;
         };
diff --git a/pygfx/utils/__init__.py b/pygfx/utils/__init__.py
index a0d965d..a316012 100644
--- a/pygfx/utils/__init__.py
+++ b/pygfx/utils/__init__.py
@@ -56,6 +56,12 @@ def array_from_shadertype(shadertype):
     array_names.append("")
     dtype_fields.append(("__".join(array_names), "uint8", (0,)))
 
+    # Add padding: uniform buffers must align to 16 bytes.
+    size = np.dtype(dtype_fields).itemsize
+    n16 = int(np.ceil(size / 16))
+    padding = n16 * 16 - size
+    dtype_fields.append(("__padding", "uint8", (padding,)))
+
     # Create a scalar of this type
     uniform_data = np.zeros((), dtype=dtype_fields)
     return uniform_data
diff --git a/setup.py b/setup.py
index daae2b8..3185fee 100644
--- a/setup.py
+++ b/setup.py
@@ -12,7 +12,7 @@ with open(f"{NAME}/__init__.py") as fh:
 
 runtime_deps = [
     "numpy",
-    "wgpu>=0.5.8,<0.6.0",
+    "wgpu>=0.7.0,<0.8.0",
     "Jinja2",
 ]
 


Test patch:
diff --git a/tests/renderers/test_shader_composer.py b/tests/renderers/test_shader_composer.py
index 28dbe5c..fbe1e14 100644
--- a/tests/renderers/test_shader_composer.py
+++ b/tests/renderers/test_shader_composer.py
@@ -87,7 +87,6 @@ def test_uniform_definitions():
     assert (
         shader.get_definitions().strip()
         == """
-        [[block]]
         struct Struct_zz {
             foo: f32;
             bar: i32;
@@ -104,7 +103,6 @@ def test_uniform_definitions():
     assert (
         shader.get_definitions().strip()
         == """
-        [[block]]
         struct Struct_zz {
             foo: vec4<f32>;
             bar: vec2<i32>;
@@ -121,7 +119,6 @@ def test_uniform_definitions():
     assert (
         shader.get_definitions().strip()
         == """
-        [[block]]
         struct Struct_zz {
             foo: mat4x4<f32>;
             bar: mat3x2<i32>;