Skip to content

Commit

Permalink
A ResizeObserver HD-DPI example
Browse files Browse the repository at this point in the history
I'm not sure if adding image-rendering fits here. Maybe
that should be a separate example?
  • Loading branch information
greggman committed Mar 7, 2024
1 parent 9a71404 commit ab99174
Show file tree
Hide file tree
Showing 5 changed files with 233 additions and 0 deletions.
25 changes: 25 additions & 0 deletions sample/resizeObserverHDDPI/checker.wgsl
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
struct Uniforms {
color0: vec4f,
color1: vec4f,
size: u32,
};

@group(0) @binding(0) var<uniform> 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);
}

36 changes: 36 additions & 0 deletions sample/resizeObserverHDDPI/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<title>webgpu-samples: resizeObserverHDDPI</title>
<style>
html, body {
margin: 0; /* remove default margin */
height: 100%; /* make body fill the browser window */
}
canvas {
width: 100%;
height: 100%;
display: block;
}
.fit-container {
width: 100%;
height: 100%;
}
.resizable {
resize: both;
overflow: scroll; /* required because resize doesn't work with overflow: visible, the default */
max-width: 100%;
max-height: 100%;
}
</style>
<script defer src="main.js" type="module"></script>
<gscript defer type="module" src="../../js/iframe-helper.js"></script>
</head>
<body>
<div id="container" class="fit-container">
<canvas></canvas>
</div>
</body>
</html>
164 changes: 164 additions & 0 deletions sample/resizeObserverHDDPI/main.ts
Original file line number Diff line number Diff line change
@@ -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);
6 changes: 6 additions & 0 deletions sample/resizeObserverHDDPI/meta.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export default {
name: 'ResizeObserver HD-DPI Fullscreen',
description: `This example shows how use ResizeObserver, handle HD-DPI correctly, and Fullscreen`,
filename: 'sample/resizeObserverHDDPI',
sources: [{ path: 'main.ts' }, { path: 'checker.wgsl' }],
};
2 changes: 2 additions & 0 deletions src/samples.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import normalMap from '../sample/normalMap/meta';
import particles from '../sample/particles/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';
Expand Down Expand Up @@ -118,6 +119,7 @@ export const pageCategories: PageCategory[] = [
'Demos integrating WebGPU with other functionalities of the web platform.',
samples: {
resizeCanvas,
resizeObserverHDDPI,
videoUploading,
worker,
},
Expand Down

0 comments on commit ab99174

Please sign in to comment.