KhronosGroup/WebGL

Clarification regarding deleting buffers attached to active TransformFeedback object

Opened this issue · 5 comments

djg commented

I have a two part question, looking for clarification on the semantics for deleting an attached buffer to an active XFB object.

Part 1: Deleting buffers attached to active XFB

Take the following example code:

    var tf = gl.createTransformFeedback();
    gl.bindTransformFeedback(gl.TRANSFORM_FEEDBACK, tf);

    var buffer = gl.createBuffer();
    gl.bindBuffer(gl.ARRAY_BUFFER, sumBuffer);
    gl.bufferData(gl.ARRAY_BUFFER, 24, gl.STATIC_DRAW);        
            
    // bind buffer to XFB index 0
    gl.bindBufferBase(gl.TRANSFORM_FEEDBACK_BUFFER, 0, buffer);
            
    gl.beginTransformFeedback(gl.TRIANGLES);
    // XFB is now active and writing output into buffer
    gl.deleteBuffer(buffer); //free buffer object

The OpenGL ES 3.2 spec states (2.6.11 Transform Feedback Objects, pg 29)1:

Transform feedback objects are container objects including references to buffer objects, [...]

So it would seem that 5.1.3 Delete Object and Object Name Lifetimes, pg 45 1 applies (emphasise is mine):

When a buffer, query, renderbuffer, sampler, sync, or texture object is deleted, its name immediately becomes invalid (e.g. is marked unused), but the underlying object will not be deleted until it is no longer in use.
A buffer, renderbuffer, sampler, or texture object is in use if any of the following conditions are satisfied:
• the object is attached to any container object (such as a buffer object attached to a vertex array object, or a renderbuffer or texture attached to a framebuffer object)

So buffer in the sample code should become invalid but the underlying storage kept until the the XFB object is no longer using it. What does not longer in use mean in this case? Until the XFB is inactive? Until a new buffer is attached to the index with BindBufferBase or BindBufferRange?

Part 2: Semantics of gl.deleteBuffer when unbind XFB attachments

When deleting a buffer that is attached, 5.1.2 Automatic Unbinding of Deleted Objects, pg 45 1 states:

When a buffer, texture, transform feedback or renderbuffer object is successfully deleted, it is unbound from any bind points it is bound to in the current context, and detached from any attachments of container objects that are bound to the current context [...]

Is this equivalent of calling gl.bindBufferBase(gl.TRANSFORM_FEEDBACK_BUFFER, 0, null)?

If so, isn't this an error that should result in gl.INVALID_OPERATION per 12.2 Transform Feedback, pg 344 1:

• by BindBufferRange or BindBufferBase if target is TRANSFORM_FEEDBACK_BUFFER and transform feedback is currently active.

djg commented

Mesa appears to consider it an error to delete a buffer attached to an active XFB.

https://gitlab.freedesktop.org/mesa/mesa/-/blob/main/src/mesa/main/bufferobj.c#L1873

        /* unbind transform feedback binding points */
         if (ctx->TransformFeedback.CurrentBuffer == bufObj) {
            bind_buffer_object(ctx, &ctx->TransformFeedback.CurrentBuffer, 0, false);
         }
         for (j = 0; j < MAX_FEEDBACK_BUFFERS; j++) {
            if (ctx->TransformFeedback.CurrentObject->Buffers[j] == bufObj) {
               _mesa_bind_buffer_base_transform_feedback(ctx,
                                           ctx->TransformFeedback.CurrentObject,
                                           j, NULL, false);
            }
         }

https://gitlab.freedesktop.org/mesa/mesa/-/blob/main/src/mesa/main/transformfeedback.c#L668

void
_mesa_bind_buffer_base_transform_feedback(struct gl_context *ctx,
                                          struct gl_transform_feedback_object *obj,
                                          GLuint index,
                                          struct gl_buffer_object *bufObj,
                                          bool dsa)
{
   if (obj->Active) {
      _mesa_error(ctx, GL_INVALID_OPERATION,
                  "%s(transform feedback active)",
                  dsa ? "glTransformFeedbackBufferBase" : "glBindBufferBase");
      return;
   }
   ...
}

Great questions. :)

Part 1 has an answer: It's kept alive as long as it being alive is still needed by GL.
Basically deleteBuffer only does this: glcontext.buffers_by_handle[name] : RefPtr<bufferT> = null;
(And then it does other detaches that we'll get to in part 2)
Then when GL's internal refcount for the buffer hits zero, the final freeing of resources happens.
Technically speaking this is merely "SHOULD". There's no way to tell "is this resource actually still alive". GL doesn't expose anything like weak_ptr information here.

What does not longer in use mean in this case? Until the XFB is inactive? Until a new buffer is attached to the index with BindBufferBase or BindBufferRange?

Until it can be freed! So until BindBuffer*, or DeleteTransformFeedback, or something somehow clears the last internal strong-ref to the buffer.

The actual answer for your example depends on what we choose below in part 2.

However I can answer this for you:

var tf = gl.createTransformFeedback();
    gl.bindTransformFeedback(gl.TRANSFORM_FEEDBACK, tf);

    var buffer = gl.createBuffer();
    gl.bindBuffer(gl.ARRAY_BUFFER, sumBuffer);
    gl.bufferData(gl.ARRAY_BUFFER, 24, gl.STATIC_DRAW);        
            
    // bind buffer to XFB index 0
    gl.bindBufferBase(gl.TRANSFORM_FEEDBACK_BUFFER, 0, buffer);
            
    gl.beginTransformFeedback(gl.TRIANGLES);
    // XFB is now active and writing output into buffer
+    gl.pauseTransformFeedback();
+    gl.bindTransformFeedback(gl.TRANSFORM_FEEDBACK, null);
    gl.deleteBuffer(buffer); //free buffer object

+    gl.bindTransformFeedback(gl.TRANSFORM_FEEDBACK, tf);
+    gl.resumeTransformFeedback();

+    gl.drawArrays(gl.TRIANGLES, 0, 0);

gl.deleteBuffer causes no change because tf is unbound, and rebinding and resuming it will allow draws to continue like normal.

However now this seemingly-no-op will INVALID_VALUE: gl.bindBufferBase(gl.TRANSFORM_FEEDBACK_BUFFER, 0, gl.getIndexedParameter(gl.TRANSFORM_FEEDBACK_BUFFER_BINDING, 0));


Part 2:
TL;DR: The spec doesn't say, so we do need to pick.

For reference, GLES says:

When a buffer, texture, transform feedback or renderbuffer object is successfully
deleted, it is unbound from any bind points it is bound to in the current context, and
detached from any attachments of container objects that are bound to the current
context, as described for DeleteBuffers, DeleteTextures, DeleteTransformFeed-
backs and DeleteRenderbuffers. If the object binding was established with other
related state (such as a buffer range in BindBufferRange or selected level and layer
information in FramebufferTexture or BindImageTexture), all such related state
are restored to default values by the automatic unbind. Bind points in other con-
texts are not affected. Attachments to unbound container objects, such as deletion
of a buffer attached to a vertex array object which is not bound to the context, are
not affected and continue to act as references on the deleted object, as described in
the following section.

Specifically, #2 "Semantics of gl.deleteBuffer when unbinding XFB attachments" isn't clearly defined, and worse, there are multiple intuitions I see:

2a: deleteBuffer behaves as if we've called e.g. vertexAttribPointer(null) to reset state. Thus we might treat deleteBuffer as a call to gl.bindBufferBase(gl.TRANSFORM_FEEDBACK_BUFFER, 0, null), therefore deleteBuffer should INVALID_OPERATION. This was my first instinct. it sounds like Mesa does this. Indeed glDeleteTransformFeedback specifies INVALID_OP when called on active TFOs.

2b: deleteBuffer is supposed to detach anything "trivially detachable". (E.g. only detach from currently bound containers) We might say that an active TFO/XFB is "not trivially detachable" and just silently ignore buffer attachments to the current TFO if it's active. Gecko does it this way. I bet Blink/ANGLE does this but someone should check. I think we probably have tests for this, but I'm not positive.

2c: I could see an argument that deleteBuffer might try really really hard to ensure "all such related state
are restored to default values by the automatic unbind", and actually de-activate a currently bound TFO. (an extreme version of restored-to-default) This is indeed what WebGL made deleteQuery do!

I don't think it's viable for deleteBuffer to detach from the TFO but leave the TFO active.


We should check if we accidentally centered on 2b in practice in shipping implementations, and if so, add that to the spec.
Otherwise, we need to decide which of these three prior arts to follow.

djg commented

Thanks for the thorough response. My reading of the other errors that should be signalled when an XFB is active (pg 343), it's my opinion that the intent is that changing anything related to XFB whilst it is active is verboten, but I'd like input from ES spec maintainers on that.

Relevant recent change in ANGLE: https://chromium-review.googlesource.com/c/angle/angle/+/3498262

If I'm reading the code correctly, the draw calls after the buffer deletion are made to fail in ANGLE.

djg commented

Relevant recent change in ANGLE: https://chromium-review.googlesource.com/c/angle/angle/+/3498262

If I'm reading the code correctly, the draw calls after the buffer deletion are made to fail in ANGLE.

If XFB is a container object, given what @kdashg wrote, that feels... wrong.