This thread has been locked.

If you have a related question, please click the "Ask a related question" button in the top right corner. The newly created question will be automatically linked to this question.

TDA4VM: Off-screen rendering problem: 0x506 GL_INVALID_FRAMEBUFFER_OPERATION

Part Number: TDA4VM
Other Parts Discussed in Thread: TDA2

Use off-screen rendering on TDA4 to report 0x506 error GL_INVALID_FRAMEBUFFER_OPERATION.
QNX system, SDK version 7.3~

The same off-screen rendering is OK on TDA2 based on Linux system 3.7 and 3.8SDK. You said that TDA4 is supported, and the below is to implement the basic code.
Help analyze it. If you need to provide other supplements, please point out~

code show as below:

//initialization
void SvEglRenderBase::InitFrameBufferOpt()
{
//Create frame buffer object: FBO
glGenFramebuffers(1, &m_nSvTopviewFbo);
glGetIntegerv(GL_FRAMEBUFFER_BINDING, &m_nSvDefaultFbo);
glBindFramebuffer(GL_FRAMEBUFFER, m_nSvTopviewFbo);
int nTestId;
glGetIntegerv(GL_FRAMEBUFFER_BINDING, &nTestId);

//Create offline render texture
glGenTextures(1, &m_nSvTopviewTexture);
glBindTexture(GL_TEXTURE_2D, m_nSvTopviewTexture);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, SV_2D_LUT_WIDTH, SV_2D_LUT_HEIGHT, 0, GL_RGB, GL_UNSIGNED_BYTE, NULL);
//Setting Texture Parameters: Interpolation method. The interpolation methods of the farthest and nearest can be set differently.
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
//Binding 2D texture to default texture is generally used to break the previous texture binding relationship
//and restore the texture binding state of OpenGL to the default state.
glBindTexture(GL_TEXTURE_2D, 0);
//Bind textrue to FBO
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, m_nSvTopviewTexture, 0);
glBindFramebuffer(GL_FRAMEBUFFER, m_nSvDefaultFbo);

//Create frame buffer object: FBO
glGenFramebuffers(1, &m_nSvPart3dFbo);
//Create offline render texture
glGenTextures(1, &m_nSvPart3dTexture);
//If 3D off-screen rendering is required, depth Buffer is required here
glGenRenderbuffers(1, &m_nSvPart3dDepthId);
//Bind FBO: To use FBO, it must be bind firstly. Make it the current rendering buffer.
glBindFramebuffer(GL_FRAMEBUFFER, m_nSvPart3dFbo);
//Create offline render texture: Texture must be created befroe it is bind to FBO
glBindTexture(GL_TEXTURE_2D, m_nSvPart3dTexture);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, SV_3D_VIEW_WIDTH, SV_3D_VIEW_HEIGHT,0, GL_RGB, GL_UNSIGNED_BYTE, NULL);
//Setting Texture Parameters: Interpolation method. The interpolation methods of the farthest and nearest can be set differently.
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
//Binding 2D texture to default texture is generally used to break the previous texture binding relationship
//and restore the texture binding state of OpenGL to the default state.
glBindTexture(GL_TEXTURE_2D, 0);
//Bind textrue to FBO
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, m_nSvPart3dTexture, 0);


glBindRenderbuffer(GL_RENDERBUFFER, m_nSvPart3dDepthId);
glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_COMPONENT16, SV_3D_VIEW_WIDTH, SV_3D_VIEW_HEIGHT);
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, m_nSvPart3dDepthId);
glBindRenderbuffer(GL_RENDERBUFFER, 0);
glBindFramebuffer(GL_FRAMEBUFFER, m_nSvDefaultFbo);

//Type 0 in TDA4
//Type 0 in TDA4
//Type 0 in TDA4
//Type 0 in TDA4
GLenum eErrStatus = glCheckFramebufferStatus(GL_FRAMEBUFFER);
printf("[AVM] glCheckFramebufferStatus complete eErrStatus = 0x%x.\n",eErrStatus);
if(eErrStatus != GL_FRAMEBUFFER_COMPLETE)
{
switch(eErrStatus)
{
case GL_FRAMEBUFFER_COMPLETE:
printf("[AVM] Framebuffer complete.\n");
break;

case GL_FRAMEBUFFER_INCOMPLETE_ATTACHMENT:
printf("[AVM] [ERROR] Framebuffer incomplete: Attachment is NOT complete.\n");
break;

case GL_FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT:
printf("[AVM] [ERROR] Framebuffer incomplete: No image is attached to FBO.\n");
break;

case GL_FRAMEBUFFER_INCOMPLETE_DIMENSIONS:
printf("[AVM] [ERROR] Framebuffer incomplete: Attached images have different dimensions.\n");
break;

case GL_FRAMEBUFFER_UNSUPPORTED:
printf("[AVM] [ERROR] Unsupported by FBO implementation.\n");
break;

default:
printf("[AVM] [ERROR] Unknow error.\n");
break;
}
}

}

//Rendering and drawing
glBindFramebuffer(GL_FRAMEBUFFER, m_pEGLRenderBase->m_nSvTopviewFbo);
DrawOffline1();
DrawOffline2();
DrawOffline3();
glBindFramebuffer(GL_FRAMEBUFFER, m_pEGLRenderBase->m_nSvDefaultFbo);
DrawAll();
//Report 0x506 error here GL_INVALID_FRAMEBUFFER_OPERATION
GLenum nErrId = glGetError();
if (nErrId != GL_NO_ERROR

Help analyze it. If you need to provide other supplements, please point out~

Please~
Thanks~

  • Compared with the SDK of TDA2 3.07 version, the differences are:
    FBO is used outside TDA4:

    Why does TDA4 use FBO in the outermost rendering? In appEglBindFrameBufferl, the off-screen rendering is initialized and the FBO is bound, then render_renderFrame is called to render, and finally appEglSwap

    But TDA2 does not use FBO here, and directly calls render_renderFrame to render, and then System_eglSwap.

    The appEglBindFrameBuffer function in TDA4 is as follows,
    FBO is used inside.

    void appEglBindFrameBuffer(void *eglWindow, app_egl_tex_prop_t *prop)
    {
    int32_t texFound;
    int32_t texIndex;
    uint32_t i;
    PFNGLEGLIMAGETARGETTEXTURE2DOESPROC glEGLImageTargetTexture2DOES;
    PFNGLCLIPCONTROLEXTPROC glClipControlEXT;
    app_egl_obj_t *obj = (app_egl_obj_t*)eglWindow;
    app_egl_tex_obj_t *tex_obj;

    glEGLImageTargetTexture2DOES = (PFNGLEGLIMAGETARGETTEXTURE2DOESPROC)eglGetProcAddress("glEGLImageTargetTexture2DOES");
    glClipControlEXT = (PFNGLCLIPCONTROLEXTPROC)eglGetProcAddress("glClipControlEXT");

    texIndex = -1;
    texFound = 0;

    for(i = 0; i < APP_EGL_MAX_RENDER_TEXTURES; i++)
    {
    tex_obj = &obj->texRender[i];

    if(tex_obj->isAlloc
    && tex_obj->dmaBufFd == prop->dmaBufFd[0]
    && tex_obj->dmaBufFdOffset == prop->dmaBufFdOffset[0]
    )
    {
    texIndex = i;
    texFound = 1;
    break;
    }
    }

    if(!texFound)
    {
    /* find free slot amd create texture */
    for(i = 0; i < APP_EGL_MAX_RENDER_TEXTURES; i++)
    {
    tex_obj = &obj->texRender[i];

    if(!tex_obj->isAlloc)
    {
    int32_t status;

    status = appEglWindowSetupRenderTex(obj, prop, i);
    if(status==0)
    {
    texIndex = i;
    texFound = 1;
    }
    break;
    }
    }
    }

    if(texFound)
    {
    tex_obj = &obj->texRender[texIndex];

    glActiveTexture(GL_TEXTURE2);
    glBindTexture(GL_TEXTURE_2D, tex_obj->tex);
    //appEglCheckEglError("glBindTexture", EGL_TRUE);

    glEGLImageTargetTexture2DOES(GL_TEXTURE_2D, (GLeglImageOES)tex_obj->img);
    //appEglCheckEglError("glEGLImageTargetTexture2DOES", EGL_TRUE);

    glBindFramebuffer(GL_FRAMEBUFFER, tex_obj->fboId);
    // appEglCheckEglError("glBindFramebuffer", EGL_TRUE);

    glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0,GL_TEXTURE_2D, tex_obj->tex, 0);

    //appEglCheckEglError("glFramebufferTexture2D", EGL_TRUE);

    GLenum fbstatus = glCheckFramebufferStatus(GL_FRAMEBUFFER);
    if (fbstatus != GL_FRAMEBUFFER_COMPLETE)
    printf("EGL: ERROR: Frambuffer complete check failed 0x%x\n", fbstatus);
    }
    /* Binding FBO: move the origin to upper left */
    glClipControlEXT(GL_UPPER_LEFT_EXT, GL_NEGATIVE_ONE_TO_ONE_EXT);
    }

  • Hello,

    The below code snippet works on TDA4 Linux. Can you please try this out? If it still doesn't work, you may have to contact QNX.

         
            // Store the current frame buffer 
            glGetIntegerv(GL_FRAMEBUFFER_BINDING, &current_fbo);
            glActiveTexture(GL_TEXTURE0 + 15);
            glGenTextures(1, &offscreen_fbo_texture);
            glBindTexture(GL_TEXTURE_2D, offscreen_fbo_texture);
            glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
            glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
            glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
            glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
            glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, NULL);
            glActiveTexture(GL_TEXTURE0);
            glBindTexture(GL_TEXTURE_2D, 0);
    
            /* Depth buffer */
            glGenRenderbuffers(1, &offscreen_fbo_depth);
            glBindRenderbuffer(GL_RENDERBUFFER, offscreen_fbo_depth);
            glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_COMPONENT16, width, height);
            glBindRenderbuffer(GL_RENDERBUFFER, 0);
    
            /* Framebuffer to link everything together */
            glGenFramebuffers(1, &offscreen_fbo);
            glBindFramebuffer(GL_FRAMEBUFFER, offscreen_fbo);
            glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, offscreen_fbo_texture, 0);
            glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, offscreen_fbo_depth);
    
            if ((status = glCheckFramebufferStatus(GL_FRAMEBUFFER)) != GL_FRAMEBUFFER_COMPLETE) {
                    fprintf(stderr, "glCheckFramebufferStatus returned error %d", status);
                    return -1;
            }
            
            // Bind back original frame buffer
            glBindFramebuffer(GL_FRAMEBUFFER, current_fbo);
            
            // To draw to offscreen fbo, bind offscreen_fbo
            glBindFramebuffer(GL_FRAMEBUFFER, offscreen_fbo);
            // ....
            // Draw to Offscreen FBO
            // ....
            
            
    
    

  • Hello,

    The reason why TDA4 uses FBO for rendering is because of how the display works. The display in case of TDA2 was driven by EGL and thus the swap.

    In case of TDA4, the display is being driven by R5. The GPU rendered buffer is passed on to display node that takes care of displaying the buffer. This is why we need off screen buffer on TDA4.

    Regards

    Hemant

  • Okay, thank you very much for your prompt and detailed reply. I understand the reason why TDA4 adopts FBO.

    Now back to the problem:
    The rendering process in TDA4:
    ... ...
    1. texYuv[i] = appEglWindowGetTexYuv(pEglWindowObj, &texProp[i]);
    ... ...
    2. appEglBindFrameBuffer(glSrvParams->eglWindowObj, &renderTexProp);
    FBO is used in this function
    glGenTextures(1, &tex_obj->tex);
    glBindTexture(GL_TEXTURE_2D, tex_obj->tex);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
    glGenFramebuffers(1, &tex_obj->fboId);
    glBindFramebuffer(GL_FRAMEBUFFER, tex_obj->fboId);
    GLuint rboDepthStencil;
    glGenRenderbuffers(1, &rboDepthStencil);
    glBindRenderbuffer(GL_RENDERBUFFER, rboDepthStencil);
    glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH24_STENCIL8_OES, prop->width, prop->height);
    glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, rboDepthStencil);
    glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_STENCIL_ATTACHMENT, GL_RENDERBUFFER, rboDepthStencil);
    glActiveTexture(GL_TEXTURE2);
    glBindTexture(GL_TEXTURE_2D, tex_obj->tex);
    glEGLImageTargetTexture2DOES(GL_TEXTURE_2D, (GLeglImageOES)tex_obj->img);
    glBindFramebuffer(GL_FRAMEBUFFER, tex_obj->fboId);
    glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0,GL_TEXTURE_2D, tex_obj->tex, 0);

    ... ...
    3. render_renderFrame(&glSrvParams->render3DSRVObj,glSrvParams->eglWindowObj,texYuv);
    Here is the texture unit texYuv that has been bound in step 1.
    Now the problem is here: if I use a new FBO in render_renderFrame (not the same as tex_obj->fboId in appEglBindFrameBuffer in step 2),
    Here it will report 0x506 GL_INVALID_FRAMEBUFFER_OPERATION error, so nothing can be drawn.
    The reason why I use a new FBO here is that I want to draw A first, then draw B, and finally display it as a new texture after processing.

    My usage is as described before, here is the wrong method I used?
    Or is there any other way to achieve what I want?

    4. glFinish();
    glBindFramebuffer(GL_FRAMEBUFFER, 0);
    appEglSwap(glSrvParams->eglWindowObj);

    Looking forward to your prompt reply, thank you~

  • Thank you for your reply.
    I will experiment with the method you mentioned in the render_renderFrame function.
    It doesn't feel much different from my previous usage.
    Just one more step of glActiveTexture(GL_TEXTURE0 + 15) operation.
    And yours here seems to be the opposite of mine:
    // Bind back original frame buffer
    glBindFramebuffer(GL_FRAMEBUFFER, current_fbo); //here current_fbo is equivalent to 0, that is, glBindFramebuffer(GL_FRAMEBUFFER, 0);
    // To draw to offscreen fbo, bind offscreen_fbo
    glBindFramebuffer(GL_FRAMEBUFFER, offscreen_fbo); //offscreen_fbo here is not equal to 0
    // ....
    // Draw to Offscreen FBO
    // ....
    Here at the end do not use glBindFramebuffer(GL_FRAMEBUFFER, 0), and then draw the offscreen_fbo_texture?

    According to my thoughts:
    // To draw to offscreen fbo, bind offscreen_fbo
    glBindFramebuffer(GL_FRAMEBUFFER, offscreen_fbo);
    DrawA();
    DrawB();
    DrawC();
    // Bind back original frame buffer
    glBindFramebuffer(GL_FRAMEBUFFER, 0);
    //Draw offscreen_fbo_texture
    Draw(offscreen_fbo_texture);

    If there is progress, I will communicate with you in time.

  • I verified it according to your method today, but it still doesn’t work, and an error is still reported:
    GL: after render_renderFrame() glError (0x506)

    The screen is always black.

    I feel that the crux of the problem lies in:
    As you said before, in the case of TDA4, the display is driven by R5, so TDA4 uses FBO for rendering outside.
    In this way, we can no longer create and use other FBOs in render_renderFrame().

    What needs to be solved now is:
    TDA4 uses FBO outside,
    How to create and use other FBOs in render_renderFrame()?

  • Hello,

    Can you refer to this code:

    appEglWindowSetupRenderTex in vision_apps/utils/opengl/src/a72/app_gl_egl_utils_qnx.c

    This will show you how to set up render target. You can use this code as a reference or modify it as needed.

    Regards

    Hemant

  • what I want to do is this:

    • Create 2 framebuffers, let’s call them fb1 and fb2.
    • Render the scene to fb2 normally.
    • Render fb2 to fb1 with one post-processing effect.
    • Render fb1 to the default framebuffer (we’ll call it fb0) with another post-processing effect.
  • Hello,

    This works for me. Please look at an example implementation in offscreen.cpp. Shaders and header are included as well.

    offscreen.cppoffscreen.hoffscreen.fshoffscreen.vsh

    Let us assume that fb0 is the one that is being currently used. Let this be the framebuffer that is being used by default. Our target is to keep this as the final frame buffer that goes to the R5 for display.

    Now, let us create a second offscreen buffer and render surround view to it. For this, we will create and manage the framebuffer inside render.cpp and not change anything outside that.

    Include offscreen.h in render.cpp and make the following changes:

    In render_init function, add this (you can add after car_init):

        if(offscreen_init(&offscreen_fbo, width, height) != 0)
        {
                offscreen_fbo = 0;
                printf("offscreen_init failed. Cannot run in offscreen mode.");
        };

    Please define offscren_fbo (to be returned by offscreen_init) and define width and height as per your screen resolution and requirements.

    Now, in render_renderFrame, at the very beginning, save the default fbo and use the framebuffer initialized above. At the end of the function, switch to default fb and render the offscreen texture to final framebuffer

    void render_renderFrame(render_state_t *pObj, void *pEglObj, GLuint *texYuv)
    {
            GLint default_fbo;
    
            glGetIntegerv(GL_FRAMEBUFFER_BINDING, &default_fbo);
    
            // srv_param_offscreen is used to enable and disable offscreen processing
            if((srv_param_offscreen == true) && (offscreen_fbo !=0 ))
            {
                    offscreen_render = true;
                    glBindFramebuffer(GL_FRAMEBUFFER, offscreen_fbo);
            }
            else
            {
                    offscreen_render = false;
            }
            
            /// The rest of the SRV code can stay unmodified.
            // After srv_draw and car_draw for all viewports are done,
            // we can switch back to original fb
            
            // This piece of code below remains unchanged
            glClear(GL_COLOR_BUFFER_BIT);
            {
                for(int i = 0; i < num_viewports; i++)
                {
                    //..... surround view rendering code .....
                    // ...
                    // ...
                }
            }
            
            // Add the below lines to switch to original fb and draw the
            // offscreen texture to final frame buffer
            if(offscreen_render == true)
            {
                    glFinish();
                    glBindFramebuffer(GL_FRAMEBUFFER, default_fbo);
                    glViewport(0, 0, width, height);
                    offscreen_draw(width, height);
            }
            
            //... frame count/fps code stays as is
    }
    
    

    You can modify offscreen shaders and code to include any post processing. This example adds only one extra framebuffer. Once you get this working, you can have a second one as needed.

    This code works on linux. If you face any issues on QNX, please check with QNX. Hope this helps.

    Regards

    Hemant

  • Thank you very much for your reply.
    This method really works. But introduced a new problem: splash screen, the screen keeps flickering

    If you remove this, the display will be normal and there will be no flickering.

  • This sounds like the texture may not be rendered to. Just for experiment, can you change glFinish to glReadPixels and see if the flickering goes away?

    Regards

    Hemant

  • Thank you for your prompt reply, I feel that dawn is coming soon, and now only the splash screen problem is left

    First of all, the rendering was successful, but the total splash screen;

    Annotated the glFinish function inside render_renderFrame (reserved by glFinish outside render_renderFrame), the screen still flickers.

    After the appEglSwap and appPerfStatsHwaUpdateLoad functions outside render_renderFrame, call the glReadPixels function, which does not take effect, and the saved images are all black. (The saved screen is also black when it is not flickering).

    In addition, by the way, why must TDA4 GPU rendering be used:
    screen -c /usr/lib/graphics/jacinto7/graphics.conf.dss_on_r5

  • Hello,

    You need to try glReadPixels before glBindFramebuffer(GL_FRAMEBUFFER, default_fbo); in the example above.

    I would also recommend reaching out to QNX regarding this flicker and the screen command.

    Regards

    Hemant

  • After verification: changing glFinish to glReadPixels, the flickering phenomenon still exists.

  • I see such instructions elsewhere:
    OpenGL ES double-buffered drawing causes a splash screen. In fact, double buffering is used to solve the splash screen problem. But there is a situation that makes double-buffering drawing will cause a splash screen problem: in a process (due to certain restrictions, only one On-screen Surface can be applied for), two different locations need to be displayed at the same time.
    This situation will lead to the following problems (A and B represent Buffers that need to be displayed in different positions. A0 represents the 0th frame of A Buffer)
    Time\screen                                   on(up-screen Buffer)                      off(off-screen Buffer)
    T1                                                            A0 B0                                             A1 B0
    T2 (upper screen swap)                          A1 B0                                             A0 B0
    T3                                                            A1 B0                                             A0 B1
    T4 (upper screen swap)                          A0 B1                                             A1 B0
    -From T3 to T4, this time period will cause a flickering screen. Because double buffering is used to swap on the screen, swapping the data of on and off (in fact, the content pointed to by the pointer is changed). Therefore, when T4 is on the screen, the A position, the corresponding off-screen Buffer is old.

    solution
    This provides a solution: after swap, update the off-screen (off) Buffer. Details are as follows:
    Time\screen                                             on                                                 off
    T1                                                          A0 B0                                            A1 B0
    T2 (upper screen)                                  A1 B0                                            A0 B0
    T2 (update)                                            A1 B0                                            A1 B0
    T3                                                          A1 B0                                            A1 B1
    T4 (upper screen)                                  A1 B1                                            A1 B0
    T4 (update)                                            A1 B1                                            A1 B1
    -As shown above, after each swap, update the off-screen data. In this way, when you go to the screen again, it will not cause a splash screen.


    In the example you provided, how to update off-screen data?

  • After verification: changing glFinish to glReadPixels, the flickering phenomenon still exists.

    But the image saved by glReadPixels is normal.

  • I found this function,

    Is it possible to use the solution?

    GLenum glClientWaitSync( GLsync sync,
    GLbitfield flags,
    GLuint64 timeout);

  • Hello,

    Possibly - you may have to create sync objects to synchronize rendering - we haven't tried this in this case but worth trying. glFinish should have synchronized. It is not very efficient and as you mentioned, ideally, you want to have double buffering with synchronization.

    Did you get a chance to ask QNX about this?

    Regards

    Hemant

  • Thanks~
    But this actually has nothing to do with the operating system. We have also tested the Linux system, and there will be such a splash screen problem.
    It feels related to TDA4, because we are OK on TDA2.

    I feel that the crux of this problem lies in: how to synchronize double buffering?
    Maybe I don’t have enough knowledge in this area, can you provide some demo reference?