Maniphest T94202

crash or error when using GPUFrameBuffer.read_color(... data=data)
Closed, ResolvedBUG

Assigned To
Germano Cavalcante (mano-wii)
Authored By
Jakub Uhlik (carbon14)
Dec 17 2021, 8:25 PM
Tags
  • BF Blender
  • Python API
Subscribers
Germano Cavalcante (mano-wii)
Jakub Uhlik (carbon14)

Description

System Information
Operating system: macOS-10.13.6-x86_64-i386-64bit 64 Bits
as well on macOS 10.15.7

Blender Version
Broken: version: 3.0.0, branch: master, commit date: 2021-12-02 18:35, hash: rBf1cca3055776
Worked: ?

Short description of error
crash or error when using GPUFrameBuffer.read_color(... data=data) i.e. when buffer is created before calling

Exact steps for others to reproduce the error
what i want to do: draw something offscreen, then get color and depth buffers, draw to viewport processed in fragment shader

following code works, read_color is used without premade buffer, buffer returned is further used in texture (sorry for not so minimal example, i removed most of not relevant shader code)

import bpy
import gpu
import numpy as np
from gpu_extras.batch import batch_for_shader
from mathutils import Matrix


vert_3d = '''
in vec3 position;
in vec4 color;
uniform mat4 model;
uniform mat4 view;
uniform mat4 projection;
uniform float size = 3.0;
uniform float alpha = 1.0;
out vec4 v_color;

void main()
{
    gl_Position = projection * view * model * vec4(position, 1.0);
    gl_PointSize = size;
    v_color = vec4(vec3(color.rgb), alpha);
}
'''
frag_3d = '''
in vec4 v_color;
out vec4 fragColor;

void main()
{
    vec4 f_color = v_color;
    fragColor = blender_srgb_to_framebuffer_space(f_color);
}
'''

vert_2d = '''
uniform mat4 ModelViewProjectionMatrix;
in vec2 pos;
out vec2 uv;
void main()
{
    gl_Position = ModelViewProjectionMatrix * vec4(pos.xy, 1.0f, 1.0f);
    uv = pos.xy;
}
'''
frag_2d = '''
in vec2 uv;
uniform sampler2D color;
uniform sampler2D depth;
uniform float near;
uniform float far;
out vec4 fragColor;

// source/blender/blenlib/intern/math_color.c
float linearrgb_to_srgb(float c)
{
    if (c < 0.0031308f) {
        return (c < 0.0f) ? 0.0f : c * 12.92f;
    }
    return 1.055f * pow(c, 1.0f / 2.4f) - 0.055f;
}
float srgb_to_linearrgb(float c)
{
    if (c < 0.04045f) {
        return (c < 0.0f) ? 0.0f : c * (1.0f / 12.92f);
    }
    return pow((c + 0.055f) * (1.0f / 1.055f), 2.4f);
}

vec4 convert_color(vec4 c)
{
    return vec4(srgb_to_linearrgb(c.r), srgb_to_linearrgb(c.g), srgb_to_linearrgb(c.b), c.a);
}

float linearize_depth(float d, float near, float far)
{
    return near * far / (far + d * (near - far));
}

void main()
{
    float d = texture(depth, uv).x;
    d = linearize_depth(d, near, far);
	d = (d == 1.0) ? 0.0 : d;
    vec4 rgba = texture(color, uv);
    fragColor = convert_color(vec4(vec3(d / 10), rgba.a));
}
'''


shader = gpu.types.GPUShader(vert_3d, frag_3d, )
n = 10000
vs = np.random.normal(0, 2, (n, 3), )
vs = vs.astype(np.float32)
cs = np.random.random((n, 4), )
cs = cs.astype(np.float32)
cs[:, 3] = 1.0
batch = batch_for_shader(shader, 'POINTS', {"position": vs, "color": cs, })


def redraw():
    for w in bpy.context.window_manager.windows:
        for a in w.screen.areas:
            if(a.type == 'VIEW_3D'):
                a.tag_redraw()


def draw_pre_view(self, context, ):
    context = bpy.context
    scene = context.scene
    region_data = context.region_data
    view = region_data.view_matrix
    projection = region_data.window_matrix
    
    viewport_info = gpu.state.viewport_get()
    width = viewport_info[2]
    height = viewport_info[3]
    
    if(self.offscreen is None):
        offscreen = gpu.types.GPUOffScreen(width, height)
        self.offscreen = offscreen
    else:
        offscreen = self.offscreen
        if(offscreen.width != width or offscreen.height != height):
            offscreen.free()
            offscreen = gpu.types.GPUOffScreen(width, height)
            self.offscreen = offscreen
    
    with offscreen.bind():
        fb = gpu.state.active_framebuffer_get()
        fb.clear(color=(0.0, 0.0, 0.0, 0.0), depth=1.0, stencil=0, )
        
        gpu.state.program_point_size_set(True)
        gpu.state.depth_test_set('LESS_EQUAL')
        gpu.state.blend_set('ALPHA')
        
        shader.bind()
        
        region_data = bpy.context.region_data
        model = Matrix()
        view = region_data.view_matrix
        projection = region_data.window_matrix
        shader.uniform_float("model", model)
        shader.uniform_float("view", view)
        shader.uniform_float("projection", projection)
        shader.uniform_float("size", 6)
        shader.uniform_float("alpha", 1.0)
        
        with gpu.matrix.push_pop():
            gpu.matrix.load_matrix(Matrix.Identity(4))
            gpu.matrix.load_projection_matrix(Matrix.Identity(4))
            batch.draw(shader)
        
        gpu.state.program_point_size_set(False)
        gpu.state.depth_test_set('NONE')
        gpu.state.blend_set('NONE')
        
        self.buffer = fb.read_color(0, 0, width, height, 4, 0, 'FLOAT', )
        self.depth = fb.read_depth(0, 0, width, height, )


def draw_post_pixel(self, context, ):
    context = bpy.context
    scene = context.scene
    region_data = context.region_data
    view = region_data.view_matrix
    projection = region_data.window_matrix
    space = context.space_data
    
    viewport_info = gpu.state.viewport_get()
    width = viewport_info[2]
    height = viewport_info[3]
    
    image = gpu.types.GPUTexture((width, height), format='RGBA32F', data=self.buffer, )
    depth = gpu.types.GPUTexture((width, height), format='DEPTH_COMPONENT32F', data=self.depth, )
    
    coords = ((0, 0), (1, 0), (1, 1), (0, 1))
    shader = gpu.types.GPUShader(vert_2d, frag_2d)
    batch = batch_for_shader(shader, 'TRI_FAN', {"pos": coords, }, )
    position = (0, 0)
    
    gpu.state.blend_set('ALPHA')
    
    with gpu.matrix.push_pop():
        gpu.matrix.translate(position)
        gpu.matrix.scale((width, height))
        
        shader.bind()
        shader.uniform_sampler("color", image)
        shader.uniform_sampler("depth", depth)
        shader.uniform_float("near", space.clip_start)
        shader.uniform_float("far", space.clip_end)
        
        batch.draw(shader)
    
    gpu.state.blend_set('NONE')


class ModalFramebuffer(bpy.types.Operator):
    bl_idname = "view3d.modal_framebuffer"
    bl_label = "Modal Framebuffer"

    def __init__(self):
        print("start..")
    
    def __del__(self):
        redraw()
        print("end.")
    
    def modal(self, context, event):
        if(event.type in {'ESC', }):
            print("exiting..")
            bpy.types.SpaceView3D.draw_handler_remove(self.handle_pre_view, 'WINDOW')
            bpy.types.SpaceView3D.draw_handler_remove(self.handle_post_pixel, 'WINDOW')
            if(self.offscreen is not None):
                self.offscreen.free()
                self.offscreen = None
            return {'CANCELLED'}
        
        return {'PASS_THROUGH'}
    
    def invoke(self, context, event):
        print("invoke..")
        
        self.offscreen = None
        self.handle_pre_view = bpy.types.SpaceView3D.draw_handler_add(draw_pre_view, (self, context), 'WINDOW', 'PRE_VIEW')
        self.handle_post_pixel = bpy.types.SpaceView3D.draw_handler_add(draw_post_pixel, (self, context), 'WINDOW', 'POST_PIXEL')
        
        context.window_manager.modal_handler_add(self)
        
        redraw()
        return {'RUNNING_MODAL'}


def register():
    bpy.utils.register_class(ModalFramebuffer)


def unregister():
    bpy.utils.unregister_class(ModalFramebuffer)


if __name__ == "__main__":
    register()
    bpy.ops.view3d.modal_framebuffer('INVOKE_DEFAULT')

for performance reasons (on fullscreen it is no longer acceptable) i wanted to NOT create new buffers and textures on each redraw, hence this code

import bpy
import gpu
import numpy as np
from gpu_extras.batch import batch_for_shader
from mathutils import Matrix


vert_3d = '''
in vec3 position;
in vec4 color;
uniform mat4 model;
uniform mat4 view;
uniform mat4 projection;
uniform float size = 3.0;
uniform float alpha = 1.0;
out vec4 v_color;

void main()
{
    gl_Position = projection * view * model * vec4(position, 1.0);
    gl_PointSize = size;
    v_color = vec4(vec3(color.rgb), alpha);
}
'''
frag_3d = '''
in vec4 v_color;
out vec4 fragColor;

void main()
{
    vec4 f_color = v_color;
    fragColor = blender_srgb_to_framebuffer_space(f_color);
}
'''

vert_2d = '''
uniform mat4 ModelViewProjectionMatrix;
in vec2 pos;
out vec2 uv;
void main()
{
    gl_Position = ModelViewProjectionMatrix * vec4(pos.xy, 1.0f, 1.0f);
    uv = pos.xy;
}
'''
frag_2d = '''
in vec2 uv;
uniform sampler2D color;
uniform sampler2D depth;
uniform float near;
uniform float far;
out vec4 fragColor;

// source/blender/blenlib/intern/math_color.c
float linearrgb_to_srgb(float c)
{
    if (c < 0.0031308f) {
        return (c < 0.0f) ? 0.0f : c * 12.92f;
    }
    return 1.055f * pow(c, 1.0f / 2.4f) - 0.055f;
}
float srgb_to_linearrgb(float c)
{
    if (c < 0.04045f) {
        return (c < 0.0f) ? 0.0f : c * (1.0f / 12.92f);
    }
    return pow((c + 0.055f) * (1.0f / 1.055f), 2.4f);
}

vec4 convert_color(vec4 c)
{
    return vec4(srgb_to_linearrgb(c.r), srgb_to_linearrgb(c.g), srgb_to_linearrgb(c.b), c.a);
}

float linearize_depth(float d, float near, float far)
{
    return near * far / (far + d * (near - far));
}

void main()
{
    float d = texture(depth, uv).x;
    d = linearize_depth(d, near, far);
	d = (d == 1.0) ? 0.0 : d;
    vec4 rgba = texture(color, uv);
    fragColor = convert_color(vec4(vec3(d / 10), rgba.a));
}
'''


shader = gpu.types.GPUShader(vert_3d, frag_3d, )
n = 10000
vs = np.random.normal(0, 2, (n, 3), )
vs = vs.astype(np.float32)
cs = np.random.random((n, 4), )
cs = cs.astype(np.float32)
cs[:, 3] = 1.0
batch = batch_for_shader(shader, 'POINTS', {"position": vs, "color": cs, })


def redraw():
    for w in bpy.context.window_manager.windows:
        for a in w.screen.areas:
            if(a.type == 'VIEW_3D'):
                a.tag_redraw()


def draw_pre_view(self, context, ):
    context = bpy.context
    scene = context.scene
    region_data = context.region_data
    view = region_data.view_matrix
    projection = region_data.window_matrix
    
    viewport_info = gpu.state.viewport_get()
    width = viewport_info[2]
    height = viewport_info[3]
    
    if(self.offscreen is None):
        offscreen = gpu.types.GPUOffScreen(width, height)
        self.offscreen = offscreen
        
        self.color_buffer = gpu.types.Buffer('FLOAT', (width, height, 4))
        self.depth_buffer = gpu.types.Buffer('FLOAT', (width, height, 1))
        self.color_tex = gpu.types.GPUTexture((width, height), format='RGBA32F', data=self.color_buffer, )
        self.depth_tex = gpu.types.GPUTexture((width, height), format='DEPTH_COMPONENT32F', data=self.depth_buffer, )
    else:
        offscreen = self.offscreen
        if(offscreen.width != width or offscreen.height != height):
            offscreen.free()
            offscreen = gpu.types.GPUOffScreen(width, height)
            self.offscreen = offscreen
            
            self.color_buffer = gpu.types.Buffer('FLOAT', (width, height, 4))
            self.depth_buffer = gpu.types.Buffer('FLOAT', (width, height, 1))
            self.color_tex = gpu.types.GPUTexture((width, height), format='RGBA32F', data=self.color_buffer, )
            self.depth_tex = gpu.types.GPUTexture((width, height), format='DEPTH_COMPONENT32F', data=self.depth_buffer, )
    
    with offscreen.bind():
        fb = gpu.state.active_framebuffer_get()
        fb.clear(color=(0.0, 0.0, 0.0, 0.0), depth=1.0, stencil=0, )
        
        gpu.state.program_point_size_set(True)
        gpu.state.depth_test_set('LESS_EQUAL')
        gpu.state.blend_set('ALPHA')
        
        shader.bind()
        
        region_data = bpy.context.region_data
        model = Matrix()
        view = region_data.view_matrix
        projection = region_data.window_matrix
        shader.uniform_float("model", model)
        shader.uniform_float("view", view)
        shader.uniform_float("projection", projection)
        shader.uniform_float("size", 6)
        shader.uniform_float("alpha", 1.0)
        
        with gpu.matrix.push_pop():
            gpu.matrix.load_matrix(Matrix.Identity(4))
            gpu.matrix.load_projection_matrix(Matrix.Identity(4))
            batch.draw(shader)
        
        gpu.state.program_point_size_set(False)
        gpu.state.depth_test_set('NONE')
        gpu.state.blend_set('NONE')
        
        fb.read_color(0, 0, width, height, 4, 0, 'FLOAT', data=self.color_buffer, )
        fb.read_depth(0, 0, width, height, data=self.depth_buffer, )


def draw_post_pixel(self, context, ):
    context = bpy.context
    scene = context.scene
    region_data = context.region_data
    view = region_data.view_matrix
    projection = region_data.window_matrix
    space = context.space_data
    
    viewport_info = gpu.state.viewport_get()
    width = viewport_info[2]
    height = viewport_info[3]
    
    coords = ((0, 0), (1, 0), (1, 1), (0, 1))
    shader = gpu.types.GPUShader(vert_2d, frag_2d)
    batch = batch_for_shader(shader, 'TRI_FAN', {"pos": coords, }, )
    position = (0, 0)
    
    gpu.state.blend_set('ALPHA')
    
    with gpu.matrix.push_pop():
        gpu.matrix.translate(position)
        gpu.matrix.scale((width, height))
        
        shader.bind()
        shader.uniform_sampler("color", self.color_tex)
        shader.uniform_sampler("depth", self.depth_tex)
        shader.uniform_float("near", space.clip_start)
        shader.uniform_float("far", space.clip_end)
        
        batch.draw(shader)
    
    gpu.state.blend_set('NONE')


class ModalFramebuffer(bpy.types.Operator):
    bl_idname = "view3d.modal_framebuffer"
    bl_label = "Modal Framebuffer"

    def __init__(self):
        print("start..")
    
    def __del__(self):
        redraw()
        print("end.")
    
    def modal(self, context, event):
        if(event.type in {'ESC', }):
            print("exiting..")
            bpy.types.SpaceView3D.draw_handler_remove(self.handle_pre_view, 'WINDOW')
            bpy.types.SpaceView3D.draw_handler_remove(self.handle_post_pixel, 'WINDOW')
            
            if(self.offscreen is not None):
                self.offscreen.free()
                self.offscreen = None
            
            self.color_buffer = None
            self.depth_buffer = None
            self.color_tex = None
            self.depth_tex = None
            
            return {'CANCELLED'}
        
        return {'PASS_THROUGH'}
    
    def invoke(self, context, event):
        print("invoke..")
        
        self.offscreen = None
        
        self.color_buffer = None
        self.depth_buffer = None
        self.color_tex = None
        self.depth_tex = None
        
        self.handle_pre_view = bpy.types.SpaceView3D.draw_handler_add(draw_pre_view, (self, context), 'WINDOW', 'PRE_VIEW')
        self.handle_post_pixel = bpy.types.SpaceView3D.draw_handler_add(draw_post_pixel, (self, context), 'WINDOW', 'POST_PIXEL')
        
        context.window_manager.modal_handler_add(self)
        
        redraw()
        return {'RUNNING_MODAL'}


def register():
    bpy.utils.register_class(ModalFramebuffer)


def unregister():
    bpy.utils.unregister_class(ModalFramebuffer)


if __name__ == "__main__":
    register()
    bpy.ops.view3d.modal_framebuffer('INVOKE_DEFAULT')

the only difference is, buffers and textures are created on first run or when viewport dimensions are changed

running second code result in either instant crash, sometime with log, sometimes without, if log is created it contains this:

0   Blender                             0x0000000110374307 BLI_system_backtrace + 55
1   Blender                             0x00000001063e0cf8 sig_handle_crash + 392
2   libsystem_platform.dylib            0x00007fff5702af5a _sigtramp + 26
3   Blender                             0x0000000106ffab31 Struct_properties_next + 17
4   Blender                             0x000000010ec4225f meth_dealloc + 15
5   Blender                             0x000000010ecefe95 call_function + 837
6   Blender                             0x000000010ecec75a _PyEval_EvalFrameDefault + 27114
7   Blender                             0x000000010ebfee35 function_code_fastcall + 229
8   Blender                             0x00000001070b4917 cb_region_draw + 39
9   Blender                             0x00000001072314ae ED_region_draw_cb_draw + 78
10  Blender                             0x0000000106ae8137 DRW_draw_render_loop_ex + 1479
11  Blender                             0x00000001075eba08 view3d_main_region_draw + 136
12  Blender                             0x00000001070c4851 ED_region_do_draw + 337
13  Blender                             0x000000010697162d wm_draw_update + 1757
14  Blender                             0x000000010696e5e0 WM_main + 48
15  Blender                             0x00000001063dd4eb main + 907
16  libdyld.dylib                       0x00007fff56d1c015 start + 1
17  ???                                 0x0000000000000004 0x0 + 4

less often it results in series of errors in terminal:

Traceback (most recent call last):
  File "/crash.py", line 169, in draw_pre_view
TypeError: read_color() argument 8 must be Buffer, not TypeError
Traceback (most recent call last):
  File "/crash.py", line 169, in draw_pre_view
TypeError: read_color() argument 8 must be Buffer, not builtin_function_or_method
Traceback (most recent call last):
  File "/crash.py", line 169, in draw_pre_view
TypeError: read_color() argument 8 must be Buffer, not builtin_function_or_method
Traceback (most recent call last):
  File "/crash.py", line 169, in draw_pre_view
TypeError: read_color() argument 8 must be Buffer, not TypeError
Traceback (most recent call last):
  File "/crash.py", line 169, in draw_pre_view
TypeError: read_color() argument 8 must be Buffer, not builtin_function_or_method
Traceback (most recent call last):
  File "/crash.py", line 169, in draw_pre_view
TypeError: read_color() argument 8 must be Buffer, not builtin_function_or_method
Traceback (most recent call last):
  File "/crash.py", line 169, in draw_pre_view
TypeError: read_color() argument 8 must be Buffer, not TypeError
Traceback (most recent call last):
  File "/crash.py", line 169, in draw_pre_view
TypeError: read_color() argument 8 must be Buffer, not builtin_function_or_method
Traceback (most recent call last):
  File "/crash.py", line 169, in draw_pre_view
TypeError: read_color() argument 8 must be Buffer, not builtin_function_or_method
Traceback (most recent call last):
  File "/crash.py", line 169, in draw_pre_view
TypeError: read_color() argument 8 must be Buffer, not TypeError
Traceback (most recent call last):
  File "/crash.py", line 169, in draw_pre_view
TypeError: read_color() argument 8 must be Buffer, not builtin_function_or_method

Revisions and Commits

rB Blender

Event Timeline

Jakub Uhlik (carbon14) created this task.Dec 17 2021, 8:25 PM
Jakub Uhlik (carbon14) added a project: Python API.
Jakub Uhlik (carbon14) updated the task description.Dec 17 2021, 8:32 PM
Jakub Uhlik (carbon14) updated the task description.Dec 20 2021, 1:17 PM
Germano Cavalcante (mano-wii) changed the task status from Needs Triage to Confirmed.Jan 18 2022, 11:07 PM
Germano Cavalcante (mano-wii) changed the subtype of this task from "Report" to "Bug".
Germano Cavalcante (mano-wii) added a subscriber: Germano Cavalcante (mano-wii).

Thanks for the report. I found the problem in the Blender code.
But for the record, please try to simplify the code a bit more before reporting.
Walls of Text don't look good in bug reports (see https://wiki.blender.org/wiki/Process/Bug_Reports#Avoid_Walls_of_Text).

The script in the example could be simplified to:

import gpu

offscreen = None
color_buffer = None
depth_buffer = None
color_tex = None
depth_tex = None

def offscreen_get(width, height):
    global offscreen
    global color_buffer
    global depth_buffer
    global color_tex
    global depth_tex

    if (offscreen is None) or (offscreen.width != width or offscreen.height != height):
        if offscreen:
            offscreen.free()
        offscreen = gpu.types.GPUOffScreen(width, height)
        
        color_buffer = gpu.types.Buffer('FLOAT', (width, height, 4))
        depth_buffer = gpu.types.Buffer('FLOAT', (width, height, 1))
        color_tex = gpu.types.GPUTexture((width, height), format='RGBA32F', data=color_buffer)
        depth_tex = gpu.types.GPUTexture((width, height), format='DEPTH_COMPONENT32F', data=depth_buffer)

    return offscreen


def draw_pre_view():
    offscreen = offscreen_get(width, height)
    with offscreen.bind():
        fb = gpu.state.active_framebuffer_get() 
        fb.read_color(0, 0, width, height, 4, 0, 'FLOAT', data=color_buffer)
        #fb.read_depth(0, 0, width, height, data=depth_buffer, )

handle_pre_view = bpy.types.SpaceView3D.draw_handler_add(draw_pre_view, (), 'WINDOW', 'PRE_VIEW')
Germano Cavalcante (mano-wii) closed this task as Resolved by committing rB2e5aecf557fb: Fix T94202: GPUFrameBuffer: wrong refcount in the buffer passed for read_color….Jan 18 2022, 11:23 PM
Germano Cavalcante (mano-wii) claimed this task.
Germano Cavalcante (mano-wii) added a commit: rB2e5aecf557fb: Fix T94202: GPUFrameBuffer: wrong refcount in the buffer passed for read_color….
Philipp Oeser (lichtwerk) added a commit: rB63b9e5378b81: Fix T94202: GPUFrameBuffer: wrong refcount in the buffer passed for read_color….Jan 20 2022, 9:25 PM