Source: rendering/renderers/gpu/texture/utils/GpuMipmapGenerator.ts

/**
 * A class which generates mipmaps for a GPUTexture.
 * Thanks to @toji for the original implementation
 * https://github.com/toji/web-texture-tool/blob/main/src/webgpu-mipmap-generator.js
 * @memberof rendering
 */
export class GpuMipmapGenerator
{
    public device: GPUDevice;
    public sampler: GPUSampler;
    public pipelines: Record<string, GPURenderPipeline>;

    public mipmapShaderModule: any;

    constructor(device: GPUDevice)
    {
        this.device = device;
        this.sampler = device.createSampler({ minFilter: 'linear' });
        // We'll need a new pipeline for every texture format used.
        this.pipelines = {};
    }

    private _getMipmapPipeline(format: GPUTextureFormat)
    {
        let pipeline = this.pipelines[format];

        if (!pipeline)
        {
            // Shader modules is shared between all pipelines, so only create once.
            if (!this.mipmapShaderModule)
            {
                this.mipmapShaderModule = this.device.createShaderModule({
                    code: /* wgsl */ `
                        var<private> pos : array<vec2<f32>, 3> = array<vec2<f32>, 3>(
                        vec2<f32>(-1.0, -1.0), vec2<f32>(-1.0, 3.0), vec2<f32>(3.0, -1.0));

                        struct VertexOutput {
                        @builtin(position) position : vec4<f32>,
                        @location(0) texCoord : vec2<f32>,
                        };

                        @vertex
                        fn vertexMain(@builtin(vertex_index) vertexIndex : u32) -> VertexOutput {
                        var output : VertexOutput;
                        output.texCoord = pos[vertexIndex] * vec2<f32>(0.5, -0.5) + vec2<f32>(0.5);
                        output.position = vec4<f32>(pos[vertexIndex], 0.0, 1.0);
                        return output;
                        }

                        @group(0) @binding(0) var imgSampler : sampler;
                        @group(0) @binding(1) var img : texture_2d<f32>;

                        @fragment
                        fn fragmentMain(@location(0) texCoord : vec2<f32>) -> @location(0) vec4<f32> {
                        return textureSample(img, imgSampler, texCoord);
                        }
                    `,
                });
            }

            pipeline = this.device.createRenderPipeline({
                layout: 'auto',
                vertex: {
                    module: this.mipmapShaderModule,
                    entryPoint: 'vertexMain',
                },
                fragment: {
                    module: this.mipmapShaderModule,
                    entryPoint: 'fragmentMain',
                    targets: [{ format }],
                }
            });

            this.pipelines[format] = pipeline;
        }

        return pipeline;
    }

    /**
     * Generates mipmaps for the given GPUTexture from the data in level 0.
     * @param {module:External.GPUTexture} texture - Texture to generate mipmaps for.
     * @returns {module:External.GPUTexture} - The originally passed texture
     */
    public generateMipmap(texture: GPUTexture)
    {
        const pipeline = this._getMipmapPipeline(texture.format);

        if (texture.dimension === '3d' || texture.dimension === '1d')
        {
            throw new Error('Generating mipmaps for non-2d textures is currently unsupported!');
        }

        let mipTexture = texture;
        const arrayLayerCount = texture.depthOrArrayLayers || 1; // Only valid for 2D textures.

        // If the texture was created with RENDER_ATTACHMENT usage we can render directly between mip levels.
        const renderToSource = texture.usage & GPUTextureUsage.RENDER_ATTACHMENT;

        if (!renderToSource)
        {
            // Otherwise we have to use a separate texture to render into. It can be one mip level smaller than the source
            // texture, since we already have the top level.
            const mipTextureDescriptor = {
                size: {
                    width: Math.ceil(texture.width / 2),
                    height: Math.ceil(texture.height / 2),
                    depthOrArrayLayers: arrayLayerCount,
                },
                format: texture.format,
                usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_SRC | GPUTextureUsage.RENDER_ATTACHMENT,
                mipLevelCount: texture.mipLevelCount - 1,
            };

            mipTexture = this.device.createTexture(mipTextureDescriptor);
        }

        const commandEncoder = this.device.createCommandEncoder({});
        // TODO: Consider making this static.
        const bindGroupLayout = pipeline.getBindGroupLayout(0);

        for (let arrayLayer = 0; arrayLayer < arrayLayerCount; ++arrayLayer)
        {
            let srcView = texture.createView({
                baseMipLevel: 0,
                mipLevelCount: 1,
                dimension: '2d',
                baseArrayLayer: arrayLayer,
                arrayLayerCount: 1,
            });

            let dstMipLevel = renderToSource ? 1 : 0;

            for (let i = 1; i < texture.mipLevelCount; ++i)
            {
                const dstView = mipTexture.createView({
                    baseMipLevel: dstMipLevel++,
                    mipLevelCount: 1,
                    dimension: '2d',
                    baseArrayLayer: arrayLayer,
                    arrayLayerCount: 1,
                });

                const passEncoder = commandEncoder.beginRenderPass({
                    colorAttachments: [{
                        view: dstView,
                        storeOp: 'store',
                        loadOp: 'clear',
                        clearValue: { r: 0, g: 0, b: 0, a: 0 },
                    }],
                });

                const bindGroup = this.device.createBindGroup({
                    layout: bindGroupLayout,
                    entries: [{
                        binding: 0,
                        resource: this.sampler,
                    }, {
                        binding: 1,
                        resource: srcView,
                    }],
                });

                passEncoder.setPipeline(pipeline);
                passEncoder.setBindGroup(0, bindGroup);
                passEncoder.draw(3, 1, 0, 0);

                passEncoder.end();

                srcView = dstView;
            }
        }

        // If we didn't render to the source texture, finish by copying the mip results from the temporary mipmap texture
        // to the source.
        if (!renderToSource)
        {
            const mipLevelSize = {
                width: Math.ceil(texture.width / 2),
                height: Math.ceil(texture.height / 2),
                depthOrArrayLayers: arrayLayerCount,
            };

            for (let i = 1; i < texture.mipLevelCount; ++i)
            {
                commandEncoder.copyTextureToTexture({
                    texture: mipTexture,
                    mipLevel: i - 1,
                }, {
                    texture,
                    mipLevel: i,
                }, mipLevelSize);

                mipLevelSize.width = Math.ceil(mipLevelSize.width / 2);
                mipLevelSize.height = Math.ceil(mipLevelSize.height / 2);
            }
        }

        this.device.queue.submit([commandEncoder.finish()]);

        if (!renderToSource)
        {
            mipTexture.destroy();
        }

        return texture;
    }
}