0
\$\begingroup\$

I have a problem when creating a texture with WebGL. The thing that I do not understand is that the first call with texSubImage2D with a canvas leads to the drawImage call consuming more time (10ms in image below) than subsequent calls and I am struggling with it. Can anyone explain why this happens?

The reason I'm doing this is because I'm trying to replace texImage2D (synchronous function) with multiple texSubImage2D calls spaced over time to improve performance.

Time log

class LoadTextureWebGL {
    public async loadTexture(tempKey: string, url: string) {
        const htmlImage = await new Promise<HTMLImageElement>((resolve, reject) => {
            const image = new Image()
            image.onerror = () => {
                reject()
            }
            image.onload = () => {
                resolve(image)
            }
            image.src = url
        })

        const texture = await this.createGLTextureFromImage(htmlImage)
    }

    private async createGLTextureFromImage(image: HTMLImageElement) {
        // Create a new canvas element
        const canvasGl = document.createElement('canvas')

        // Get a WebGL context
        const gl =
            canvasGl.getContext('webgl') ||
            (canvasGl.getContext('experimental-webgl') as WebGLRenderingContext)

        if (!gl) {
            throw new Error(
                'Unable to get WebGL context. Your browser or machine may not support it.'
            )
        }

        // const renderer = this.game.renderer as Phaser.Renderer.WebGL.WebGLRenderer
        // const gl = renderer.gl

        const minFilter: number = gl.NEAREST
        const magFilter: number = gl.NEAREST
        let wrap: number = gl.CLAMP_TO_EDGE

        const width = image.width
        const height = image.height

        const pow = Phaser.Math.Pow2.IsSize(width, height)

        if (pow) {
            wrap = gl.REPEAT
        }

        const pma = true
        const flipY = false

        // Step 1: Create a new WebGLTexture object
        console.time('createTexture')
        const texture = gl.createTexture()
        console.timeEnd('createTexture')

        if (!texture) {
            console.error('createGLTextureFromImage: Failed to create WebGL texture')
            return null
        }

        // Step 2: Activate a texture unit
        console.time('activeTexture')
        gl.activeTexture(gl.TEXTURE0)
        const oldTexture = gl.getParameter(gl.TEXTURE_BINDING_2D)
        console.timeEnd('activeTexture')

        // Step 3: Bind the texture object
        console.time('bindTexture')
        gl.bindTexture(gl.TEXTURE_2D, texture)
        console.timeEnd('bindTexture')

        // Step 4: Set the texture parameters
        console.time('texParameteri')
        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, minFilter)
        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, magFilter)
        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, wrap)
        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, wrap)

        gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, pma)
        gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, flipY)
        console.timeEnd('texParameteri')

        // Create a temporary canvas
        const canvas2D = document.createElement('canvas')
        const context = canvas2D.getContext('2d')

        if (!context) {
            console.error('createGLTextureFromImage: Failed to get 2D context')
            return null
        }

        console.time('texImage2D')
        // Initialize the texture to the correct size
        gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, width, height, 0, gl.RGBA, gl.UNSIGNED_BYTE, null)

        console.time('texSubImage2DWithBlankCanvas')
        gl.texSubImage2D(gl.TEXTURE_2D, 0, 0, 100, gl.RGBA, gl.UNSIGNED_BYTE, canvas2D)
        console.timeEnd('texSubImage2DWithBlankCanvas')

        // Assuming you want to split the image into 10 parts vertically
        const partHeight = image.height / 10
        for (let i = 0; i < 10; i++) {
            // Set the size of the canvas to match the size of the part
            canvas2D.width = image.width
            canvas2D.height = partHeight

            // Draw the part of the image onto the canvas
            context.drawImage(
                image,
                0,
                i * partHeight,
                image.width,
                partHeight,
                0,
                0,
                canvas2D.width,
                canvas2D.height
            )
            console.time('texSubImage2D')
            // Upload the part to the texture
            gl.texSubImage2D(
                gl.TEXTURE_2D,
                0,
                0,
                i * partHeight,
                gl.RGBA,
                gl.UNSIGNED_BYTE,
                canvas2D
            )
            console.timeEnd('texSubImage2D')
        }
        console.timeEnd('texImage2D')

        // Step 6: Generate the mipmap
        if (pow) {
            console.time('generateMipmap')
            gl.generateMipmap(gl.TEXTURE_2D)
            console.timeEnd('generateMipmap')
        }

        // Step 6: Rebind the old texture
        if (oldTexture) {
            gl.bindTexture(gl.TEXTURE_2D, oldTexture)
        }

        return texture
    }
}
\$\endgroup\$
4
  • \$\begingroup\$ First call overhead is due to moving the image data to the GPU (from CPU RAM to GPU RAM). Data is moved to the GPU only as/when needed and will remain there until GPU free RAM is too low and new data (from CPU RAM) overwrites existing (GPU RAM), or context is lost and GPU data must be restored (as/when needed) \$\endgroup\$ Commented Apr 15, 2024 at 19:52
  • \$\begingroup\$ What can I do in this situation? My game does lazy load texture. After it finishes loading, it will create texture that cause a frame drop. \$\endgroup\$ Commented Apr 16, 2024 at 9:10
  • \$\begingroup\$ My bad the problem is more complicated due to the use of the 2D canvas which also needs to move data to and from the GPU. You can try const context = canvas2D.getContext('2d', {desynchronized: true, willReadFrequently: true}); or any of the combinations of the two settings, Note the second setting 'willReadFrequently: true' will force the 2D canvas to use software rendering. The first desynchronized: true is only a hint. \$\endgroup\$ Commented Apr 17, 2024 at 0:46
  • \$\begingroup\$ I have tried all combinations, but default is the best choice. I also try using context.getImageData instead of passing canvas directly but still the same. In my opinion, the canvas holds all pixels data it needs to pass to GPU (not need to pass original image data). Each time, the drawImage called, it will be new pixels data so time consumption must be the same. I guess the first time textSubImage2D consumption (have called drawImage first) is for init memory. If my opinion is right, I have to accept a little drop frame in my game. Tks! @Blindman67 \$\endgroup\$ Commented Apr 17, 2024 at 8:32

0

You must log in to answer this question.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.