GPv3: Python API

With the migration from GPv2 to GPv3, the Python API has to be reimplemented. In the past weeks, there have been some discussions on how this should be done. I’d like to summarize all the thoughts so far, point out concerns from addon developers and finally ask other Blender developers to give their opinion.

Python API in GPv2

In GPv2, strokes and points are their own structs. This is reflected in the Python API. Each stroke and each point is its own object.

Here’s an example:

for layer in gp.layers:
    for frame in layer.frames:
        if frame.frame_number >= 20:
            continue
        for stroke in frame.strokes:
            if stroke.material_index != 0:
                stroke.select = False
                continue
            stroke.select = True

            for point in stroke.points:
                if point.pressure > 0.6:
                    print(point.co, point.pressure)
                    point.vertex_color = (1, 0, 0)

I’d like to go over the options for possible implementations of this API for GPv3:

Option 1

The first option is to simply stay as close to the previous API as possible.
See #121407 - WIP: GPv3: RNA for frame, drawing, stroke and point - blender - Blender Projects for an implementation.

Pros:

  • Virtually no change to the old API, easy to understand (object oriented).
  • Easy for python developers to port their addons.

Cons:

  • Doesn’t represent the underlying data, has to implement an abstraction layer in RNA on top.
  • No access to custom attributes, not extendable (each attribute has to be exposed individually).

Option 2

Use the existing attributes Python API (rna_attribute.cc).

Pros:

  • Has an existing implementation.
  • Is extendable (attributes are retrieved by their name).

Cons:

  • Invalidation. Currently the API doesn’t take care of invalidation. E.g.
    >>> cube = bpy.data.objects["Cube"].data
    >>> positions = cube.attributes["position"]
    >>> cube.attributes.new("custom", 'FLOAT', 'POINT')
    ...
    >>> positions.data
    bpy.data.meshes['Cube'].attributes["custom"].data
    
  • The API is a bit fiddly to use, because of current limitations in RNA. E.g.
    >>> cube = bpy.data.objects["Cube"].data
    >>> cube.attributes["position"].data[0].vector
    Vector((1.0, 1.0, 1.0))
    
    Notice the extra step of indirection: attributes["position"].data is a FloatVectorAttribute but data[0] is a FloatVectorAttributeValue with a property vector that is the actual Vector. This makes writing code like the GPv2 example above much more verbose and confusing.

Option 3

Implement a custom python object implementation for attribute arrays.
Similar to the IDProperty implementation. See #122094 - WIP: GPv3: Python API for frame, drawing and builtin geometry attributes - blender - Blender Projects for more details (thanks @Sietse-Brouwer !).

Pros:

  • Easier to understand, numpy-like, API.
  • Is extendable, and much more customizable.
  • Orthogonal to current attribute API (access using .data_array).

Cons:

  • New implementation (ideally this would replace the current attributes API eventually, so we don’t have to maintain the two)
  • Invalidation. Similar to Option 2, this does not cover cache invalidation at the moment.

Personally, I believe that we should: Consider Option 3 and try to make it work and fall back to Option 1 if we can’t.

In talking to addon developers, I don’t think Option 2 would work and would have to be changed afterwards, breaking the API again.

Since we’re going to have to break the API, I think we should go for something that actually represents GPv3s data layout well.

Let me know your thoughts, thank you.

3 Likes

I had a talk with @mont29 about this topic. To summarize:

  • The invalidation issue of attributes needs to be fixed regardless, so this is not a GPv3 related topic. Should maybe even be reported as a bug (if it doesn’t exist already).
  • Because Option 3 is basically an addition on top of Option 2, it should be handled and discussed as its own project with its own design task. It would be great to have something like this at the lower level for other geometry types.
  • There is one major blocking issue for Option 2 that needs to be tackled before it could be implemented: Non-ID owners of custom data.
    • There is also a potentially bigger problem with PointerRNA here. Currently, if we don’t know who owns the data pointer, we have to search through all candidates in the ID. But in the specific case of a PointerRNA to CustomDataLayer (attributes) in a grease pencil drawing, if implicit sharing is at play, the PointerRNA is ambiguous. E.g. two different drawings have the exact same PointerRNA.
      EDIT: In the rna_attribute.cc API, this is avoided by calling CustomData_ensure_data_is_mutable in rna_Attribute_data_begin. This enforces the data to be unique. Of course that means that in python, we don’t get the benefits of implicit sharing.

E.g. two different drawings have the exact same PointerRNA.

To more more precise, two different drawings have the exact same PointerRNA.data (i.e. same exact CustomDataLayer pointer)

I am not sure how exactly approach with attributes will work here. You would need to have convenient way of partitioning it into individual drawings. So there needs to be some higher-level abstraction on top of the attributes, and that is what I believe, you described in the Option 3.

The thing which I am not so happy with is this caching and invalidation. It always something where things easily go wrong.

There is also an alternative approach, where you keep the RNA API small and simple, without any caching, implementing bare index-based access to arrays, and implement a higher level abstraction in Python somewhere in the bpy_extras. Is it something you looked into already and discarded for some reasons, or is still an option to explore?

I’m not sure what you mean. The attributes are already per drawing. More precisely, a drawing is basically CurvesGeometry.

I might have been confused with some earlier designs, or eve something else. For some reason the data model was more flatter in my mind. But in any case, there are still room for improvement when partition attributes to strokes, i.e.

While I do think the Option 3 does provide quite strong foundation, what I don’t like about it is the requirement to make every Python API user to think in those low-level index offset terms. Keep in mind, the Python API users are not always on the same technical level as you. But also verbosity is also annoying.

You can imagine having a Stroke python class which holds a pointer to the drawing, and start/end indices of the stroke, and them

drawing.positions[point_range.start:point_range.stop] = new_strokes_pos[i]
drawing.radii[point_range.start:point_range.stop] = new_strokes_radius[i]
drawing.opacities.fill(1.0, range(point_range.start, point_range.stop))

becomes

stroke = Stroke(drawing, stroke_index)  # Creation of Stroke can be handled by an extension on top of Drawing().
stroke.positions.assign(new_strokes_pos[i])
stroke.radii.assign(new_strokes_radius[i])
stroke.opacities.fill(1.0)

To me the readability and robustness of the code worth possible sacrifice of performance (due to the extra python objects creations and methods indirection).

1 Like

We can definitley improve the usability of the core design. But from what I heard from other developers so far is that these abstractions should be secondary to the core API and that the core API should be close to the actual data layout.

In order to finish GPv3, I think we have to go for an option that we can get done in time (which might have to be Option 2 or something similar) and then see what else we can do during bcon1 (of Blender 4.3). If we can finish a design for Option 3 and get it approved, then that would be great. And in the worst case we at least have an API that should cover the functionality of GPv2.

I think something like option 1 is worth keeping. However this can be implemented fully in Python on top of a lower level API. See bpy_types.py for how to extend built-in types for this. The implementation of that should be pretty simple.

With the good API simple things should be simple, complex things should be possible. The current attributes-based API (including the one from the PS) does not spark the idea that simple thins are kept simple.

I do not think a purely Python implementation of some classes on top of a lower level API would take that much time.

And I would really like to see someone from the scripting side to become a stake-holder to bring the balance back, to offset some decision coming from a highly technical C++ people. From my experience good API is not just a thin binding to a core data structures.

1 Like

I agree that having a Python abstraction on top of a lower level API would be ideal. Do I understand correctly that you would therefore lean towards Option 2 (+python types abstraction)?

I’ve had some great discussions with @Pullup who’s the maintainer of many of our grease pencil addons. His voice is heard and I’m trying my best to keep his point of view in mind when talking to the developers in the C++ camp.

1 Like

I’d like to point out that the invalidation issues for Option 2 could be fixed with this patch: #113525 - Fix #107500: Custom-data Python validation to prevent crashes - blender - Blender Projects

@sergey @brecht Would be nice if you could look at #122091 - Python API: Direct access to attribute arrays of meshes, curves, point clouds and GP drawings - blender - Blender Projects and give some feedback there (Option 3). I’d like to agree on either sticking to Option 2 or persuing Option 3. And in both cases then building a python abstraction layer on top.

From a planning point of view, I think the first priority should be wrapping the attributes the same as it works for meshes and curves now. Improving the array access for attributes in general would be great but should not be a blocker for GPv3.

There is an issue to solve there regarding efficiently looking up the array length with multiple drawings. Also as a first step there I suggest to implement the inefficient solution (looping through all drawings and their custom data layers), and then work on making it faster.

2 Likes

I think this is what exactly the project now agreed on (2024-06-03 Grease Pencil Module Meeting)

@filedescriptor Keeping in mind that we can/will work on the future improvements of attributes API / accessing parents of RNA properties, are there some outstanding questions to discuss here, or we can consider we found the way forward and consider the topic resolved?

Yes for me the topic is resolved from the GPv3 point-of-view.

We should revisit the core attribute API (if we have time, maybe even during 4.3 bcon1) and agree on a design task. Sietse’s implementation could be a fundation for this.

1 Like