From 8c1cde888c766c2fc48291c648742b7da61adf49 Mon Sep 17 00:00:00 2001 From: Gregg Tavares Date: Wed, 6 Mar 2024 21:17:14 -0800 Subject: [PATCH] A ResizeObserver HD-DPI example I'm not sure if adding image-rendering fits here. Maybe that should be a separate example? --- sample/resizeObserverHDDPI/checker.wgsl | 25 ++++ sample/resizeObserverHDDPI/index.html | 36 ++++++ sample/resizeObserverHDDPI/main.ts | 164 ++++++++++++++++++++++++ sample/resizeObserverHDDPI/meta.ts | 6 + src/samples.ts | 2 + 5 files changed, 233 insertions(+) create mode 100644 sample/resizeObserverHDDPI/checker.wgsl create mode 100644 sample/resizeObserverHDDPI/index.html create mode 100644 sample/resizeObserverHDDPI/main.ts create mode 100644 sample/resizeObserverHDDPI/meta.ts diff --git a/sample/resizeObserverHDDPI/checker.wgsl b/sample/resizeObserverHDDPI/checker.wgsl new file mode 100644 index 00000000..1794fc6d --- /dev/null +++ b/sample/resizeObserverHDDPI/checker.wgsl @@ -0,0 +1,25 @@ +struct Uniforms { + color0: vec4f, + color1: vec4f, + size: u32, +}; + +@group(0) @binding(0) var uni: Uniforms; + +@vertex +fn vs(@builtin(vertex_index) vertexIndex : u32) -> @builtin(position) vec4f { + const pos = array( + vec2f(-1.0, -1.0), + vec2f( 3.0, -1.0), + vec2f(-1.0, 3.0), + ); + return vec4f(pos[vertexIndex], 0.0, 1.0); +} + +@fragment +fn fs(@builtin(position) position: vec4f) -> @location(0) vec4f { + let grid = vec2u(position.xy) / uni.size; + let checker = (grid.x + grid.y) % 2 == 1; + return select(uni.color0, uni.color1, checker); +} + diff --git a/sample/resizeObserverHDDPI/index.html b/sample/resizeObserverHDDPI/index.html new file mode 100644 index 00000000..4ebbac8f --- /dev/null +++ b/sample/resizeObserverHDDPI/index.html @@ -0,0 +1,36 @@ + + + + + + webgpu-samples: resizeObserverHDDPI + + + + + +
+ +
+ + diff --git a/sample/resizeObserverHDDPI/main.ts b/sample/resizeObserverHDDPI/main.ts new file mode 100644 index 00000000..f3f7e4d1 --- /dev/null +++ b/sample/resizeObserverHDDPI/main.ts @@ -0,0 +1,164 @@ +import { GUI } from 'dat.gui'; +import checkerWGSL from './checker.wgsl'; + +const canvas = document.querySelector('canvas') as HTMLCanvasElement; +const adapter = await navigator.gpu.requestAdapter(); +const device = await adapter.requestDevice(); + +const context = canvas.getContext('webgpu') as GPUCanvasContext; + +const presentationFormat = navigator.gpu.getPreferredCanvasFormat(); + +context.configure({ + device, + format: presentationFormat, + alphaMode: 'premultiplied', +}); + +const module = device.createShaderModule({ + code: checkerWGSL, +}); +const pipeline = device.createRenderPipeline({ + layout: 'auto', + vertex: { module }, + fragment: { + module, + targets: [ + { + format: presentationFormat, + }, + ], + }, +}); + +// These offsets are in f32/u32 offset. +enum UniformOffset { + color0 = 0, + color1 = 4, + size = 8, +} + +const uniformValuesAsF32 = new Float32Array(12); // 2 vec4fs, 1 u32, 3 padding +const uniformValuesAsU32 = new Uint32Array(uniformValuesAsF32.buffer); +const uniformBuffer = device.createBuffer({ + size: uniformValuesAsF32.byteLength, + usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST, +}); + +const bindGroup = device.createBindGroup({ + layout: pipeline.getBindGroupLayout(0), + entries: [ + { + binding: 0, + resource: { buffer: uniformBuffer }, + }, + ], +}); + +const settings = { + color0: '#FF0000', + color1: '#00FFFF', + size: 1, + resizable: false, + fullscreen() { + if (document.fullscreenElement) { + document.exitFullscreen(); + } else { + document.body.requestFullscreen(); + } + }, +}; + +const containerElem = document.querySelector('#container') as HTMLElement; + +const gui = new GUI(); +gui.addColor(settings, 'color0').onChange(frame); +gui.addColor(settings, 'color1').onChange(frame); +gui.add(settings, 'size', 1, 32, 1).name('checker size').onChange(frame); +gui.add(settings, 'fullscreen'); +gui.add(settings, 'resizable').onChange(() => { + const { resizable } = settings; + // Get these before we adjust the CSS because our canvas is sized in device pixels + // and so will expand if we stop constraining it with CSS + const width = containerElem.clientWidth; + const height = containerElem.clientHeight; + + containerElem.classList.toggle('resizable', resizable); + containerElem.classList.toggle('fit-container', !resizable); + + containerElem.style.width = resizable ? `${width}px` : ''; + containerElem.style.height = resizable ? `${height}px` : ''; +}); + +// Given a CSS color, returns the color in 0 to 1 RGBA values. +const cssColorToRGBA = (function () { + const ctx = new OffscreenCanvas(1, 1).getContext('2d', { + willReadFrequently: true, + }); + return function (color: string) { + ctx.clearRect(0, 0, 1, 1); + ctx.fillStyle = color; + ctx.fillRect(0, 0, 1, 1); + return [...ctx.getImageData(0, 0, 1, 1).data].map((v) => v / 255); + }; +})(); + +function frame() { + uniformValuesAsF32.set(cssColorToRGBA(settings.color0), UniformOffset.color0); + uniformValuesAsF32.set(cssColorToRGBA(settings.color1), UniformOffset.color1); + uniformValuesAsU32[UniformOffset.size] = settings.size; + + device.queue.writeBuffer(uniformBuffer, 0, uniformValuesAsF32); + + const commandEncoder = device.createCommandEncoder(); + + const renderPassDescriptor: GPURenderPassDescriptor = { + colorAttachments: [ + { + view: context.getCurrentTexture().createView(), + clearValue: { r: 0.2, g: 0.2, b: 0.2, a: 1.0 }, + loadOp: 'clear', + storeOp: 'store', + }, + ], + }; + + const passEncoder = commandEncoder.beginRenderPass(renderPassDescriptor); + passEncoder.setPipeline(pipeline); + passEncoder.setBindGroup(0, bindGroup); + passEncoder.draw(3); + passEncoder.end(); + + device.queue.submit([commandEncoder.finish()]); +} + +function getDevicePixelContentBoxSize(entry: ResizeObserverEntry) { + // Safari does not support devicePixelContentBoxSize + if (entry.devicePixelContentBoxSize) { + return { + width: entry.devicePixelContentBoxSize[0].inlineSize, + height: entry.devicePixelContentBoxSize[0].blockSize, + }; + } else { + // These values not correct but they're as close as you can get in Safari + return { + width: entry.contentBoxSize[0].inlineSize * devicePixelRatio, + height: entry.contentBoxSize[0].blockSize * devicePixelRatio, + }; + } +} + +const { maxTextureDimension2D } = device.limits; +const observer = new ResizeObserver(([entry]) => { + // Note: If you are using requestAnimationFrame you should + // only record the size here but set it in the requestAnimationFrame callback + // otherwise you'll get flicker when resizing. + const { width, height } = getDevicePixelContentBoxSize(entry); + + // A size of 0 will cause an error when we call getCurrentTexture. + // A size > maxTextureDimension2D will also an error when we call getCurrentTexture. + canvas.width = Math.max(1, Math.min(width, maxTextureDimension2D)); + canvas.height = Math.max(1, Math.min(height, maxTextureDimension2D)); + frame(); +}); +observer.observe(canvas); diff --git a/sample/resizeObserverHDDPI/meta.ts b/sample/resizeObserverHDDPI/meta.ts new file mode 100644 index 00000000..3ca360d3 --- /dev/null +++ b/sample/resizeObserverHDDPI/meta.ts @@ -0,0 +1,6 @@ +export default { + name: 'ResizeObserver HD-DPI Fullscreen', + description: `This example shows how to use ResizeObserver, handle HD-DPI correctly, and Fullscreen`, + filename: 'sample/resizeObserverHDDPI', + sources: [{ path: 'main.ts' }, { path: 'checker.wgsl' }], +}; diff --git a/src/samples.ts b/src/samples.ts index 212e2585..22dd81ff 100644 --- a/src/samples.ts +++ b/src/samples.ts @@ -20,6 +20,7 @@ import particles from '../sample/particles/meta'; import pristineGrid from '../sample/pristineGrid/meta'; import renderBundles from '../sample/renderBundles/meta'; import resizeCanvas from '../sample/resizeCanvas/meta'; +import resizeObserverHDDPI from '../sample/resizeObserverHDDPI/meta'; import reversedZ from '../sample/reversedZ/meta'; import rotatingCube from '../sample/rotatingCube/meta'; import samplerParameters from '../sample/samplerParameters/meta'; @@ -125,6 +126,7 @@ export const pageCategories: PageCategory[] = [ 'Demos integrating WebGPU with other functionalities of the web platform.', samples: { resizeCanvas, + resizeObserverHDDPI, videoUploading, worker, },