webgpu

a valid external instance reference no longer exists


After drawing a line, after a while, the browser produces a warning: a valid external instance reference no longer exists. I wonder if it was because of my setVertexBuffer in requestAnimationFrame, or maybe for other reasons, I want to know where my error is, and how to solve it.

All Code:

struct MyVSInput {
    @location(0) position : vec4f,
};

@vertex
fn myVSMain(v : MyVSInput) -> @builtin(position) vec4f {
    return v.position;
}

@fragment
fn myFSMain() -> @location(0) vec4f {
    return vec4f(1, 1, 0, 1);
}


import ShaderCode from "./basic.wgsl?raw";

const adapter = await navigator.gpu.requestAdapter();
const device = await adapter.requestDevice();

const canvas = document.querySelector( 'canvas' );
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
const context = canvas.getContext( 'webgpu' );
const presentationFormat = navigator.gpu.getPreferredCanvasFormat();
context.configure( {
    device,
    format: presentationFormat,
    alphaMode: 'premultiplied',
} );

const sampleCount = 4;
const module = device.createShaderModule( { code: ShaderCode } );
const pipeline = device.createRenderPipeline( {
    layout: 'auto',
    vertex: {
        module,
        buffers: [
            {
                arrayStride: 2 * 4,
                attributes: [
                    {
                        shaderLocation: 0,
                        offset: 0,
                        format: 'float32x2'
                    },
                ],
            },
        ],
    },
    fragment: {
        module,
        targets: [
            { format: presentationFormat },
        ],
    },
    primitive: {
        topology: "line-strip",
    },
    multisample: {
        count: sampleCount
    }
} );

const pointsList: number[][] = [];
const buffers: GPUBuffer[] = [];
const pointsPerBuffer = 1024;  // should make this much larger (16k) but keeping it small for testing.
let numPoints = 0;

const addPoint = ( x: number, y: number ) => {

    let buffer = buffers[ buffers.length - 1 ];
    const points = pointsList[ pointsList.length - 1 ];
    const offset = points.length / 2;

    if ( points.length > 0 && points.length % pointsPerBuffer * 2 === 0 ) {

        // GPUBuffer需要扩容
        const pop = buffers.pop();
        pop.destroy();

        buffer = device.createBuffer( {
            size: buffer.size + pointsPerBuffer * 2 * 4,
            usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,
            mappedAtCreation: true
        } );

        new Float32Array( buffer.getMappedRange() ).set( points );

        buffer.unmap();

        buffers.push( buffer );

    }

    device.queue.writeBuffer( buffer, offset * 2 * 4, new Float32Array( [ x, y ] ) );
    points.push( x, y );

    ++numPoints;
};

const renderPassDescriptor: GPURenderPassDescriptor = {
    colorAttachments: [
        {
            view: undefined, // Assigned later
            resolveTarget: undefined,
            clearValue: [ 0.2, 0.2, 0.2, 1.0 ],
            loadOp: 'clear',
            storeOp: 'store',
        },
    ],
};

function render () {

    const texture = device.createTexture( {
        size: [ canvas.width, canvas.height ],
        format: presentationFormat,
        sampleCount: sampleCount,
        usage: GPUTextureUsage.RENDER_ATTACHMENT
    } );
    renderPassDescriptor.colorAttachments[ 0 ].view = texture.createView();

    const canvasTexture = context.getCurrentTexture();
    renderPassDescriptor.colorAttachments[ 0 ].resolveTarget = canvasTexture.createView();

    const encoder = device.createCommandEncoder();
    const pass = encoder.beginRenderPass( renderPassDescriptor );
    pass.setPipeline( pipeline );
    buffers.forEach( ( buffer, i ) => {
        pass.setVertexBuffer( 0, buffer );
        pass.draw( pointsList[ i ].length / 2 );
    } );
    pass.end();
    device.queue.submit( [ encoder.finish() ] );

    requestAnimationFrame( render );
}
requestAnimationFrame( render );

function createBuffer ( buffers: GPUBuffer[] ) {
    const buffer = device.createBuffer( {
        size: pointsPerBuffer * 2 * 4,
        usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,
    } );
    buffers.push( buffer );
}

function createPoints ( pointsList: number[][] ) {
    const points = [];
    pointsList.push( points );
}

let needCreateBuffer = false;
let beginDraw = false;
canvas.addEventListener( "mousedown", ( event ) => {
    if ( event.button === 0 ) {
        needCreateBuffer = true;
        beginDraw = true;
    }
} );
canvas.addEventListener( "mouseup", ( event ) => {
    if ( event.button === 0 ) {
        beginDraw = false;
    }
} );
canvas.addEventListener( "mousemove", ( event ) => {
    if ( beginDraw === true ) {
        if ( needCreateBuffer === true ) {
            createBuffer( buffers );
            createPoints( pointsList );
            needCreateBuffer = false;
        }
        const rect = canvas.getBoundingClientRect();
        const x = ( event.clientX - rect.left ) / rect.width * 2 - 1;
        const y = ( event.clientY - rect.top ) / rect.height * -2 + 1;
        addPoint( x, y );
    }
} );


Solution

  • The code is creating a new multisample texture every frame and probably running out of memory.

    A better pattern would be create one and keep it around unless it's the wrong size. If it is the wrong size, destroy the old one and create a new one.

    body { margin: 0; }
    canvas { display: block; }
    <canvas></canvas>
    <script type="module">
    
    const ShaderCode = `
    struct MyVSInput {
        @location(0) position : vec4f,
    };
    
    @vertex
    fn myVSMain(v : MyVSInput) -> @builtin(position) vec4f {
        return v.position;
    }
    
    @fragment
    fn myFSMain() -> @location(0) vec4f {
        return vec4f(1, 1, 0, 1);
    }
    `;
    
    
    const adapter = await navigator.gpu.requestAdapter();
    const device = await adapter.requestDevice();
    
    const canvas = document.querySelector( 'canvas' );
    canvas.width = window.innerWidth;
    canvas.height = window.innerHeight;
    const context = canvas.getContext( 'webgpu' );
    const presentationFormat = navigator.gpu.getPreferredCanvasFormat();
    context.configure( {
        device,
        format: presentationFormat,
        alphaMode: 'premultiplied',
    } );
    
    const sampleCount = 4;
    const module = device.createShaderModule( { code: ShaderCode } );
    const pipeline = device.createRenderPipeline( {
        layout: 'auto',
        vertex: {
            module,
            buffers: [
                {
                    arrayStride: 2 * 4,
                    attributes: [
                        {
                            shaderLocation: 0,
                            offset: 0,
                            format: 'float32x2'
                        },
                    ],
                },
            ],
        },
        fragment: {
            module,
            targets: [
                { format: presentationFormat },
            ],
        },
        primitive: {
            topology: "line-strip",
        },
        multisample: {
            count: sampleCount
        }
    } );
    
    const pointsList/*: number[][]*/ = [];
    const buffers/*: GPUBuffer[]*/ = [];
    const pointsPerBuffer = 1024;  // should make this much larger (16k) but keeping it small for testing.
    let numPoints = 0;
    
    const addPoint = ( x/*: number*/, y/*: number*/ ) => {
    
        let buffer = buffers[ buffers.length - 1 ];
        const points = pointsList[ pointsList.length - 1 ];
        const offset = points.length / 2;
    
        if ( points.length > 0 && points.length % pointsPerBuffer * 2 === 0 ) {
    
            // GPUBuffer需要扩容
            const pop = buffers.pop();
            pop.destroy();
    
            buffer = device.createBuffer( {
                size: buffer.size + pointsPerBuffer * 2 * 4,
                usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,
                mappedAtCreation: true
            } );
    
            new Float32Array( buffer.getMappedRange() ).set( points );
    
            buffer.unmap();
    
            buffers.push( buffer );
    
        }
    
        device.queue.writeBuffer( buffer, offset * 2 * 4, new Float32Array( [ x, y ] ) );
        points.push( x, y );
    
        ++numPoints;
    };
    
    const renderPassDescriptor/*: GPURenderPassDescriptor*/ = {
        colorAttachments: [
            {
                view: undefined, // Assigned later
                resolveTarget: undefined,
                clearValue: [ 0.2, 0.2, 0.2, 1.0 ],
                loadOp: 'clear',
                storeOp: 'store',
            },
        ],
    };
    
    
    let texture;
    
    function render () {
    
        const canvasTexture = context.getCurrentTexture();
        if (!texture || texture.width !== canvasTexture.width || texture.height !== canvasTexture.height) {
          texture?.destroy();
          texture = device.createTexture( {
            size: [ canvas.width, canvas.height ],
            format: presentationFormat,
            sampleCount: sampleCount,
            usage: GPUTextureUsage.RENDER_ATTACHMENT
          } );
        }
        renderPassDescriptor.colorAttachments[ 0 ].view = texture.createView();
        renderPassDescriptor.colorAttachments[ 0 ].resolveTarget = canvasTexture.createView();
    
        const encoder = device.createCommandEncoder();
        const pass = encoder.beginRenderPass( renderPassDescriptor );
        pass.setPipeline( pipeline );
        buffers.forEach( ( buffer, i ) => {
            pass.setVertexBuffer( 0, buffer );
            pass.draw( pointsList[ i ].length / 2 );
        } );
        pass.end();
        device.queue.submit( [ encoder.finish() ] );
    
        requestAnimationFrame( render );
    }
    requestAnimationFrame( render );
    
    function createBuffer ( buffers/*: GPUBuffer[]*/ ) {
        const buffer = device.createBuffer( {
            size: pointsPerBuffer * 2 * 4,
            usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,
        } );
        buffers.push( buffer );
    }
    
    function createPoints ( pointsList/*: number[][]*/ ) {
        const points = [];
        pointsList.push( points );
    }
    
    let needCreateBuffer = false;
    let beginDraw = false;
    canvas.addEventListener( "mousedown", ( event ) => {
        if ( event.button === 0 ) {
            needCreateBuffer = true;
            beginDraw = true;
        }
    } );
    canvas.addEventListener( "mouseup", ( event ) => {
        if ( event.button === 0 ) {
            beginDraw = false;
        }
    } );
    canvas.addEventListener( "mousemove", ( event ) => {
        if ( beginDraw === true ) {
            if ( needCreateBuffer === true ) {
                createBuffer( buffers );
                createPoints( pointsList );
                needCreateBuffer = false;
            }
            const rect = canvas.getBoundingClientRect();
            const x = ( event.clientX - rect.left ) / rect.width * 2 - 1;
            const y = ( event.clientY - rect.top ) / rect.height * -2 + 1;
            addPoint( x, y );
        }
    } );
    </script>