import { GPUTextureViewDimension, GPUIndexFormat, GPUFilterMode, GPUPrimitiveTopology, GPULoadOp, GPUStoreOp } from './WebGPUConstants.js'; class WebGPUTexturePassUtils { constructor( device ) { this.device = device; const mipmapVertexSource = ` struct VarysStruct { @builtin( position ) Position: vec4, @location( 0 ) vTex : vec2 }; @vertex fn main( @builtin( vertex_index ) vertexIndex : u32 ) -> VarysStruct { var Varys : VarysStruct; var pos = array< vec2, 4 >( vec2( -1.0, 1.0 ), vec2( 1.0, 1.0 ), vec2( -1.0, -1.0 ), vec2( 1.0, -1.0 ) ); var tex = array< vec2, 4 >( vec2( 0.0, 0.0 ), vec2( 1.0, 0.0 ), vec2( 0.0, 1.0 ), vec2( 1.0, 1.0 ) ); Varys.vTex = tex[ vertexIndex ]; Varys.Position = vec4( pos[ vertexIndex ], 0.0, 1.0 ); return Varys; } `; const mipmapFragmentSource = ` @group( 0 ) @binding( 0 ) var imgSampler : sampler; @group( 0 ) @binding( 1 ) var img : texture_2d; @fragment fn main( @location( 0 ) vTex : vec2 ) -> @location( 0 ) vec4 { return textureSample( img, imgSampler, vTex ); } `; const flipYFragmentSource = ` @group( 0 ) @binding( 0 ) var imgSampler : sampler; @group( 0 ) @binding( 1 ) var img : texture_2d; @fragment fn main( @location( 0 ) vTex : vec2 ) -> @location( 0 ) vec4 { return textureSample( img, imgSampler, vec2( vTex.x, 1.0 - vTex.y ) ); } `; this.mipmapSampler = device.createSampler( { minFilter: GPUFilterMode.Linear } ); this.flipYSampler = device.createSampler( { minFilter: GPUFilterMode.Nearest } ); //@TODO?: Consider using textureLoad() // We'll need a new pipeline for every texture format used. this.transferPipelines = {}; this.flipYPipelines = {}; this.mipmapVertexShaderModule = device.createShaderModule( { label: 'mipmapVertex', code: mipmapVertexSource } ); this.mipmapFragmentShaderModule = device.createShaderModule( { label: 'mipmapFragment', code: mipmapFragmentSource } ); this.flipYFragmentShaderModule = device.createShaderModule( { label: 'flipYFragment', code: flipYFragmentSource } ); } getTransferPipeline( format ) { let pipeline = this.transferPipelines[ format ]; if ( pipeline === undefined ) { pipeline = this.device.createRenderPipeline( { vertex: { module: this.mipmapVertexShaderModule, entryPoint: 'main' }, fragment: { module: this.mipmapFragmentShaderModule, entryPoint: 'main', targets: [ { format } ] }, primitive: { topology: GPUPrimitiveTopology.TriangleStrip, stripIndexFormat: GPUIndexFormat.Uint32 }, layout: 'auto' } ); this.transferPipelines[ format ] = pipeline; } return pipeline; } getFlipYPipeline( format ) { let pipeline = this.flipYPipelines[ format ]; if ( pipeline === undefined ) { pipeline = this.device.createRenderPipeline( { vertex: { module: this.mipmapVertexShaderModule, entryPoint: 'main' }, fragment: { module: this.flipYFragmentShaderModule, entryPoint: 'main', targets: [ { format } ] }, primitive: { topology: GPUPrimitiveTopology.TriangleStrip, stripIndexFormat: GPUIndexFormat.Uint32 }, layout: 'auto' } ); this.flipYPipelines[ format ] = pipeline; } return pipeline; } flipY( textureGPU, textureGPUDescriptor, baseArrayLayer = 0 ) { const format = textureGPUDescriptor.format; const { width, height } = textureGPUDescriptor.size; const transferPipeline = this.getTransferPipeline( format ); const flipYPipeline = this.getFlipYPipeline( format ); const tempTexture = this.device.createTexture( { size: { width, height, depthOrArrayLayers: 1 }, format, usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.TEXTURE_BINDING } ); const srcView = textureGPU.createView( { baseMipLevel: 0, mipLevelCount: 1, dimension: GPUTextureViewDimension.TwoD, baseArrayLayer } ); const dstView = tempTexture.createView( { baseMipLevel: 0, mipLevelCount: 1, dimension: GPUTextureViewDimension.TwoD, baseArrayLayer: 0 } ); const commandEncoder = this.device.createCommandEncoder( {} ); const pass = ( pipeline, sourceView, destinationView ) => { const bindGroupLayout = pipeline.getBindGroupLayout( 0 ); // @TODO: Consider making this static. const bindGroup = this.device.createBindGroup( { layout: bindGroupLayout, entries: [ { binding: 0, resource: this.flipYSampler }, { binding: 1, resource: sourceView } ] } ); const passEncoder = commandEncoder.beginRenderPass( { colorAttachments: [ { view: destinationView, loadOp: GPULoadOp.Clear, storeOp: GPUStoreOp.Store, clearValue: [ 0, 0, 0, 0 ] } ] } ); passEncoder.setPipeline( pipeline ); passEncoder.setBindGroup( 0, bindGroup ); passEncoder.draw( 4, 1, 0, 0 ); passEncoder.end(); }; pass( transferPipeline, srcView, dstView ); pass( flipYPipeline, dstView, srcView ); this.device.queue.submit( [ commandEncoder.finish() ] ); tempTexture.destroy(); } generateMipmaps( textureGPU, textureGPUDescriptor, baseArrayLayer = 0 ) { const pipeline = this.getTransferPipeline( textureGPUDescriptor.format ); const commandEncoder = this.device.createCommandEncoder( {} ); const bindGroupLayout = pipeline.getBindGroupLayout( 0 ); // @TODO: Consider making this static. let srcView = textureGPU.createView( { baseMipLevel: 0, mipLevelCount: 1, dimension: GPUTextureViewDimension.TwoD, baseArrayLayer } ); for ( let i = 1; i < textureGPUDescriptor.mipLevelCount; i ++ ) { const bindGroup = this.device.createBindGroup( { layout: bindGroupLayout, entries: [ { binding: 0, resource: this.mipmapSampler }, { binding: 1, resource: srcView } ] } ); const dstView = textureGPU.createView( { baseMipLevel: i, mipLevelCount: 1, dimension: GPUTextureViewDimension.TwoD, baseArrayLayer } ); const passEncoder = commandEncoder.beginRenderPass( { colorAttachments: [ { view: dstView, loadOp: GPULoadOp.Clear, storeOp: GPUStoreOp.Store, clearValue: [ 0, 0, 0, 0 ] } ] } ); passEncoder.setPipeline( pipeline ); passEncoder.setBindGroup( 0, bindGroup ); passEncoder.draw( 4, 1, 0, 0 ); passEncoder.end(); srcView = dstView; } this.device.queue.submit( [ commandEncoder.finish() ] ); } } export default WebGPUTexturePassUtils;