diff --git a/sample/alphaToCoverage/emulatedAlphaToCoverage.ts b/sample/alphaToCoverage/emulatedAlphaToCoverage.ts new file mode 100644 index 00000000..ed7afb44 --- /dev/null +++ b/sample/alphaToCoverage/emulatedAlphaToCoverage.ts @@ -0,0 +1,57 @@ +/** + * Database of alpha-to-coverage patterns from different devices. + * + * Name of device -> + * Array of patterns from a=0.0 to a=1.0, evenly spaced, excluding endpoints -> + * Array of N*N masks depending on the block size of the pattern used + * (in row-major order) + */ +export const alphaToCoverageDatabase: { [k: string]: PatternSequence } = { + 'NVIDIA GeForce RTX 3070': [[0b1000], [0b1001], [0b1011]], + 'Intel HD Graphics 4400': [[0b0001], [0b0011], [0b0111]], +}; + +type PatternSequence = ReadonlyArray; +type Pattern = ReadonlyArray; +type Mask = number; + +/** + * For each device name, provides the source for a WGSL function which emulates + * the alpha-to-coverage algorithm of that device by mapping (alpha, x, y) to + * a sample mask. + */ +export const kEmulatedAlphaToCoverage = { + 'Apple M1 Pro': `\ + fn emulatedAlphaToCoverage(alpha: f32, x: u32, y: u32) -> u32 { + let u = x % 2; + let v = y % 2; + if (alpha < 0.5 / 16) { return ${0b0000}; } + // FIXME returning values out of an array is not working, always returns 0 + if (alpha < 1.5 / 16) { return array(array(${0b0001}u, ${0b0000}), array(${0b0000}, ${0b0000}))[v][u]; } + if (alpha < 2.5 / 16) { return array(array(${0b0001}u, ${0b0000}), array(${0b0000}, ${0b0001}))[v][u]; } + if (alpha < 3.5 / 16) { return array(array(${0b0001}u, ${0b0001}), array(${0b0000}, ${0b0001}))[v][u]; } + if (alpha < 4.5 / 16) { return array(array(${0b0001}u, ${0b0001}), array(${0b0001}, ${0b0001}))[v][u]; } + if (alpha < 5.5 / 16) { return array(array(${0b1001}u, ${0b0001}), array(${0b0001}, ${0b0001}))[v][u]; } + if (alpha < 6.5 / 16) { return array(array(${0b1001}u, ${0b0001}), array(${0b0001}, ${0b1001}))[v][u]; } + if (alpha < 7.5 / 16) { return array(array(${0b1001}u, ${0b1001}), array(${0b0001}, ${0b1001}))[v][u]; } + if (alpha < 8.5 / 16) { return array(array(${0b1001}u, ${0b1001}), array(${0b1001}, ${0b1001}))[v][u]; } + if (alpha < 9.5 / 16) { return array(array(${0b1011}u, ${0b1001}), array(${0b1001}, ${0b1001}))[v][u]; } + if (alpha < 10.5 / 16) { return array(array(${0b1011}u, ${0b1001}), array(${0b1001}, ${0b1011}))[v][u]; } + if (alpha < 11.5 / 16) { return array(array(${0b1011}u, ${0b1011}), array(${0b1001}, ${0b1011}))[v][u]; } + if (alpha < 12.5 / 16) { return array(array(${0b1011}u, ${0b1011}), array(${0b1011}, ${0b1011}))[v][u]; } + if (alpha < 13.5 / 16) { return array(array(${0b1111}u, ${0b1011}), array(${0b1011}, ${0b1011}))[v][u]; } + if (alpha < 14.5 / 16) { return array(array(${0b1111}u, ${0b1011}), array(${0b1011}, ${0b1111}))[v][u]; } + if (alpha < 15.5 / 16) { return array(array(${0b1111}u, ${0b1111}), array(${0b1011}, ${0b1111}))[v][u]; } + return ${0b1111}; + } + `.trimEnd(), + 'NVIDIA GeForce RTX 3070': `\ + fn emulatedAlphaToCoverage(alpha: f32, x: u32, y: u32) -> u32 { + if (alpha < 0.5 / 4) { return ${0b0000}; } + if (alpha < 1.5 / 4) { return ${0b1000}; } + if (alpha < 2.5 / 4) { return ${0b1001}; } + if (alpha < 3.5 / 4) { return ${0b1011}; } + return ${0b1111}; + } + `.trimEnd(), +}; diff --git a/sample/alphaToCoverage/main.ts b/sample/alphaToCoverage/main.ts index cde25c0a..0043e7b3 100644 --- a/sample/alphaToCoverage/main.ts +++ b/sample/alphaToCoverage/main.ts @@ -2,7 +2,9 @@ import { GUI } from 'dat.gui'; import showMultisampleTextureWGSL from './showMultisampleTexture.wgsl'; import renderWithAlphaToCoverageWGSL from './renderWithAlphaToCoverage.wgsl'; +import renderWithEmulatedAlphaToCoverageWGSL from './renderWithEmulatedAlphaToCoverage.wgsl'; import { quitIfWebGPUNotAvailable } from '../util'; +import { kEmulatedAlphaToCoverage } from './emulatedAlphaToCoverage'; const canvas = document.querySelector('canvas') as HTMLCanvasElement; const adapter = await navigator.gpu?.requestAdapter(); @@ -14,6 +16,8 @@ quitIfWebGPUNotAvailable(adapter, device); // const kInitConfig = { + scene: 'solid_colors', + emulatedDevice: 'none', sizeLog2: 3, showResolvedColor: true, color1: 0x0000ff, @@ -34,17 +38,23 @@ gui.width = 300; }, }; + gui.add(config, 'scene', ['solid_colors']); + gui.add(config, 'emulatedDevice', [ + 'none', + ...Object.keys(kEmulatedAlphaToCoverage), + ]); + const settings = gui.addFolder('Settings'); settings.open(); settings.add(config, 'sizeLog2', 0, 8, 1).name('size = 2**'); settings.add(config, 'showResolvedColor', true); - const draw1Panel = gui.addFolder('Draw 1'); + const draw1Panel = gui.addFolder('solid_colors Draw 1'); draw1Panel.open(); draw1Panel.addColor(config, 'color1').name('color'); draw1Panel.add(config, 'alpha1', 0, 255).name('alpha'); - const draw2Panel = gui.addFolder('Draw 2'); + const draw2Panel = gui.addFolder('solid_colors Draw 2'); draw2Panel.open(); draw2Panel.addColor(config, 'color2').name('color'); draw2Panel.add(config, 'alpha2', 0, 255).name('alpha'); @@ -80,23 +90,32 @@ const bufInstanceColors = device.createBuffer({ size: 8, }); -let multisampleTexture: GPUTexture, multisampleTextureView: GPUTextureView; +let actualMSTexture: GPUTexture, actualMSTextureView: GPUTextureView; +let emulatedMSTexture: GPUTexture, emulatedMSTextureView: GPUTextureView; let resolveTexture: GPUTexture, resolveTextureView: GPUTextureView; let lastSize = 0; -function resetMultisampleTexture() { +let renderWithEmulatedAlphaToCoveragePipeline: GPURenderPipeline | null; +let lastEmulatedDevice = 'none'; +function resetConfiguredObjects() { const size = 2 ** config.sizeLog2; if (lastSize !== size) { - if (multisampleTexture) { - multisampleTexture.destroy(); + if (actualMSTexture) { + actualMSTexture.destroy(); } - multisampleTexture = device.createTexture({ - format: 'rgba8unorm', + if (emulatedMSTexture) { + emulatedMSTexture.destroy(); + } + const msTextureDesc = { + format: 'rgba8unorm' as const, usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.TEXTURE_BINDING, size: [size, size], sampleCount: 4, - }); - multisampleTextureView = multisampleTexture.createView(); + }; + actualMSTexture = device.createTexture(msTextureDesc); + actualMSTextureView = actualMSTexture.createView(); + emulatedMSTexture = device.createTexture(msTextureDesc); + emulatedMSTextureView = emulatedMSTexture.createView(); if (resolveTexture) { resolveTexture.destroy(); @@ -111,6 +130,40 @@ function resetMultisampleTexture() { lastSize = size; } + + if ( + config.emulatedDevice !== 'none' && + lastEmulatedDevice !== config.emulatedDevice + ) { + // Pipeline to render to a multisampled texture using *emulated* alpha-to-coverage + const renderWithEmulatedAlphaToCoverageModule = device.createShaderModule({ + code: + renderWithEmulatedAlphaToCoverageWGSL + + kEmulatedAlphaToCoverage[config.emulatedDevice], + }); + renderWithEmulatedAlphaToCoveragePipeline = device.createRenderPipeline({ + label: 'renderWithEmulatedAlphaToCoveragePipeline', + layout: 'auto', + vertex: { + module: renderWithEmulatedAlphaToCoverageModule, + buffers: [ + { + stepMode: 'instance', + arrayStride: 4, + attributes: [{ shaderLocation: 0, format: 'unorm8x4', offset: 0 }], + }, + ], + }, + fragment: { + module: renderWithEmulatedAlphaToCoverageModule, + targets: [{ format: 'rgba8unorm' }], + }, + multisample: { count: 4, alphaToCoverageEnabled: false }, + primitive: { topology: 'triangle-list' }, + }); + } else { + renderWithEmulatedAlphaToCoveragePipeline = null; + } } function applyConfig() { @@ -129,7 +182,7 @@ function applyConfig() { ]); device.queue.writeBuffer(bufInstanceColors, 0, data); - resetMultisampleTexture(); + resetConfiguredObjects(); } // @@ -170,7 +223,16 @@ const showMultisampleTextureModule = device.createShaderModule({ const showMultisampleTexturePipeline = device.createRenderPipeline({ label: 'showMultisampleTexturePipeline', layout: 'auto', - vertex: { module: showMultisampleTextureModule }, + vertex: { + module: showMultisampleTextureModule, + buffers: [ + { + stepMode: 'instance', + arrayStride: 4, + attributes: [{ shaderLocation: 0, format: 'unorm8x4', offset: 0 }], + }, + ], + }, fragment: { module: showMultisampleTextureModule, targets: [{ format: presentationFormat }], @@ -186,8 +248,15 @@ function render() { const showMultisampleTextureBG = device.createBindGroup({ layout: showMultisampleTextureBGL, entries: [ - { binding: 0, resource: multisampleTextureView }, - { binding: 1, resource: resolveTextureView }, + { binding: 0, resource: actualMSTextureView }, + { + binding: 1, + resource: + config.emulatedDevice === 'none' + ? actualMSTextureView + : emulatedMSTextureView, + }, + { binding: 2, resource: resolveTextureView }, ], }); @@ -212,7 +281,7 @@ function render() { label: 'renderWithAlphaToCoverage pass', colorAttachments: [ { - view: multisampleTextureView, + view: actualMSTextureView, resolveTarget: config.showResolvedColor ? resolveTextureView : undefined, @@ -227,6 +296,24 @@ function render() { pass.draw(6, 2); pass.end(); } + // renderWithEmulatedAlphaToCoverage pass + if (renderWithEmulatedAlphaToCoveragePipeline) { + const pass = commandEncoder.beginRenderPass({ + label: 'renderWithEmulatedAlphaToCoverage pass', + colorAttachments: [ + { + view: emulatedMSTextureView, + clearValue: [0, 0, 0, 1], // black background + loadOp: 'clear', + storeOp: 'store', + }, + ], + }); + pass.setPipeline(renderWithEmulatedAlphaToCoveragePipeline); + pass.setVertexBuffer(0, bufInstanceColors); + pass.draw(6, 2); + pass.end(); + } // showMultisampleTexture pass { const pass = commandEncoder.beginRenderPass({ @@ -242,6 +329,7 @@ function render() { }); pass.setPipeline(showMultisampleTexturePipeline); pass.setBindGroup(0, showMultisampleTextureBG); + pass.setVertexBuffer(0, bufInstanceColors); pass.draw(6); pass.end(); } diff --git a/sample/alphaToCoverage/renderWithEmulatedAlphaToCoverage.wgsl b/sample/alphaToCoverage/renderWithEmulatedAlphaToCoverage.wgsl new file mode 100644 index 00000000..da49e95e --- /dev/null +++ b/sample/alphaToCoverage/renderWithEmulatedAlphaToCoverage.wgsl @@ -0,0 +1,30 @@ +struct Varying { + @builtin(position) pos: vec4f, + // Color from instance-step-mode vertex buffer + @location(0) color: vec4f, +} + +@vertex +fn vmain( + @builtin(vertex_index) vertex_index: u32, + @location(0) color: vec4f, +) -> Varying { + var square = array( + vec2f(-1, -1), vec2f(-1, 1), vec2f( 1, -1), + vec2f( 1, -1), vec2f(-1, 1), vec2f( 1, 1), + ); + + return Varying(vec4(square[vertex_index], 0, 1), color); +} + +struct FragOut { + @location(0) color: vec4f, + @builtin(sample_mask) mask: u32, +} + +@fragment +fn fmain(vary: Varying) -> FragOut { + let mask = emulatedAlphaToCoverage(vary.color.a, u32(vary.pos.x), u32(vary.pos.y)); + return FragOut(vec4f(vary.color.rgb, 1.0), mask); +} + diff --git a/sample/alphaToCoverage/showMultisampleTexture.wgsl b/sample/alphaToCoverage/showMultisampleTexture.wgsl index 99b22a03..7080eaa7 100644 --- a/sample/alphaToCoverage/showMultisampleTexture.wgsl +++ b/sample/alphaToCoverage/showMultisampleTexture.wgsl @@ -1,5 +1,6 @@ -@group(0) @binding(0) var tex: texture_multisampled_2d; -@group(0) @binding(1) var resolved: texture_2d; +@group(0) @binding(0) var tex_left: texture_multisampled_2d; +@group(0) @binding(1) var tex_right: texture_multisampled_2d; +@group(0) @binding(2) var resolved: texture_2d; struct Varying { @builtin(position) pos: vec4f, @@ -42,7 +43,7 @@ const kSampleOuterRadius = kSampleDistanceFromCloseEdge + kGridEdgeHalfWidth; @fragment fn fmain(vary: Varying) -> @location(0) vec4f { - let dim = textureDimensions(tex); + let dim = textureDimensions(tex_left); let dimMax = max(dim.x, dim.y); let xy = vary.uv * f32(dimMax); @@ -56,8 +57,13 @@ fn fmain(vary: Varying) -> @location(0) vec4f { let distanceFromSample = distance(xyFrac, kSamplePositions[sampleIndex]); if distanceFromSample < kSampleInnerRadius { // Draw a circle for the sample value - let val = textureLoad(tex, xyInt, sampleIndex).rgb; - return vec4f(val, 1); + if xyFrac.x < kSamplePositions[sampleIndex].x { + let val = textureLoad(tex_left, xyInt, sampleIndex).rgb; + return vec4f(val, 1); + } else { + let val = textureLoad(tex_right, xyInt, sampleIndex).rgb; + return vec4f(val, 1); + } } else if distanceFromSample < kSampleOuterRadius { // Draw a ring around the circle return vec4f(0, 0, 0, 1);