diff --git a/src/api/utils/hash.ts b/src/api/utils/hash.ts index 4d79a90..a82051f 100644 --- a/src/api/utils/hash.ts +++ b/src/api/utils/hash.ts @@ -80,6 +80,7 @@ export function bindingsDescriptorEquals( b.storageBufferBindings = b.storageBufferBindings || []; b.storageTextureBindings = b.storageTextureBindings || []; + if (a.pipeline !== b.pipeline) return false; if (a.samplerBindings.length !== b.samplerBindings.length) return false; if (!arrayEqual(a.samplerBindings, b.samplerBindings, samplerBindingEquals)) return false; diff --git a/src/webgpu/Device.ts b/src/webgpu/Device.ts index 55db08d..f910d8f 100644 --- a/src/webgpu/Device.ts +++ b/src/webgpu/Device.ts @@ -84,6 +84,7 @@ import { } from './utils'; import { preprocessShader_GLSL } from '../shader'; import { RenderBundle_WebGPU } from './RenderBundle'; +import { MipmapGenerator } from './MipmapGenerator'; export class Device_WebGPU implements SwapChain, IDevice_WebGPU { private swapChainWidth = 0; @@ -123,6 +124,7 @@ export class Device_WebGPU implements SwapChain, IDevice_WebGPU { private canvasContext: GPUCanvasContext; private glsl_compile: typeof glsl_compile_; private WGSLComposer: WGSLComposer; + private mipmapGenerator: MipmapGenerator; constructor( adapter: GPUAdapter, @@ -137,6 +139,7 @@ export class Device_WebGPU implements SwapChain, IDevice_WebGPU { this.canvasContext = canvasContext; this.glsl_compile = glsl_compile; this.WGSLComposer = wGSLComposer; + this.mipmapGenerator = new MipmapGenerator(device); this.fallbackTexture2D = this.createFallbackTexture( TextureDimension.TEXTURE_2D, @@ -266,6 +269,10 @@ export class Device_WebGPU implements SwapChain, IDevice_WebGPU { return this; } + getMipmapGenerator(): MipmapGenerator { + return this.mipmapGenerator; + } + getCanvas(): HTMLCanvasElement | OffscreenCanvas { return this.canvas; } diff --git a/src/webgpu/MipmapGenerator.ts b/src/webgpu/MipmapGenerator.ts new file mode 100644 index 0000000..9038bdf --- /dev/null +++ b/src/webgpu/MipmapGenerator.ts @@ -0,0 +1,127 @@ +export class MipmapGenerator { + private readonly _device: GPUDevice; + private readonly _mipmapShader: GPUShaderModule; + private readonly _mipmapSampler: GPUSampler; + private readonly _pipelines: Map; + + constructor(device: GPUDevice) { + this._device = device; + + this._mipmapShader = this._device.createShaderModule({ + label: 'MipmapGenerator', + code: ` + struct VSOutput { + @builtin(position) position: vec4f, + @location(0) texcoord: vec2f, + }; + + @vertex fn vs( + @builtin(vertex_index) vertexIndex : u32 + ) -> VSOutput { + let pos = array( + // 1st triangle + vec2f( 0.0, 0.0), // center + vec2f( 1.0, 0.0), // right, center + vec2f( 0.0, 1.0), // center, top + + // 2nd triangle + vec2f( 0.0, 1.0), // center, top + vec2f( 1.0, 0.0), // right, center + vec2f( 1.0, 1.0), // right, top + ); + + var vsOutput: VSOutput; + let xy = pos[vertexIndex]; + vsOutput.position = vec4f(xy * 2.0 - 1.0, 0.0, 1.0); + vsOutput.texcoord = vec2f(xy.x, 1.0 - xy.y); + return vsOutput; + } + + @group(0) @binding(0) var ourSampler: sampler; + @group(0) @binding(1) var ourTexture: texture_2d; + + @fragment fn fs(fsInput: VSOutput) -> @location(0) vec4f { + return textureSample(ourTexture, ourSampler, fsInput.texcoord); + } + `, + }); + this._mipmapSampler = this._device.createSampler({ + minFilter: 'linear', + }); + this._pipelines = new Map(); + } + + private _requestPipeline(format: GPUTextureFormat) { + let pipeline = this._pipelines.get(format); + if (!pipeline) { + pipeline = this._device.createRenderPipeline({ + layout: 'auto', + vertex: { + module: this._mipmapShader, + entryPoint: 'vs', + }, + fragment: { + module: this._mipmapShader, + entryPoint: 'fs', + targets: [{ format }], + }, + }); + this._pipelines.set(format, pipeline); + } + return pipeline; + } + + generateMipmap(texture: GPUTexture) { + const commandEncoder = this._device.createCommandEncoder({ + label: 'mipmap generator command encoder', + }); + const pipeline = this._requestPipeline(texture.format,); + + let width = texture.width; + let height = texture.height; + let baseMipLevel = 0; + + while ((width > 1 || height > 1) && baseMipLevel < texture.mipLevelCount - 1) { + width = Math.max(1, width / 2); + height = Math.max(1, height / 2); + + const bindGroup = this._device.createBindGroup({ + layout: pipeline.getBindGroupLayout(0), + entries: [ + { + binding: 0, + resource: this._mipmapSampler, + }, + { + binding: 1, + resource: texture.createView({ + baseMipLevel, + mipLevelCount: 1, + }), + }, + ], + }); + + ++baseMipLevel; + + const pass = commandEncoder.beginRenderPass({ + colorAttachments: [ + { + view: texture.createView({ + baseMipLevel, + mipLevelCount: 1, + }), + loadOp: 'clear', + storeOp: 'store', + }, + ], + }); + pass.setPipeline(pipeline); + pass.setBindGroup(0, bindGroup); + pass.draw(6); + pass.end(); + } + + this._device.queue.submit([commandEncoder.finish()]); + } +} diff --git a/src/webgpu/RenderPass.ts b/src/webgpu/RenderPass.ts index 0765d39..659238a 100644 --- a/src/webgpu/RenderPass.ts +++ b/src/webgpu/RenderPass.ts @@ -220,7 +220,7 @@ export class RenderPass_WebGPU implements RenderPass { } private flipY(y: number, h: number) { - const height = this.device['swapChainHeight']; + const height = this.gfxColorAttachment[0].height; return height - y - h; } diff --git a/src/webgpu/Texture.ts b/src/webgpu/Texture.ts index 8b1776d..01b6e53 100644 --- a/src/webgpu/Texture.ts +++ b/src/webgpu/Texture.ts @@ -6,6 +6,7 @@ import { getBlockInformationFromFormat, translateTextureViewDimension, } from './utils'; +import { Device_WebGPU } from './Device'; export class Texture_WebGPU extends ResourceBase_WebGPU @@ -86,6 +87,7 @@ export class Texture_WebGPU GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST | GPUTextureUsage.RENDER_ATTACHMENT, + mipLevelCount: this.mipLevelCount, }; const texture = device.createTexture(textureDescriptor); @@ -97,6 +99,10 @@ export class Texture_WebGPU ); } + if(this.mipLevelCount > 1) { + (this.device as Device_WebGPU).getMipmapGenerator().generateMipmap(texture); + } + return [texture, width, height]; }