Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
164 changes: 164 additions & 0 deletions sample/generateMipmap/generateMipmap.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
import generateMipmapWGSL from './generateMipmap.wgsl';

export function numMipLevels(...sizes: number[]) {
const maxSize = Math.max(...sizes);
return (1 + Math.log2(maxSize)) | 0;
}

/**
* Get the default viewDimension
* Note: It's only a guess. The user needs to tell us to be
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For my own edification: this duplicates the guess logic from the browser, no? In which case it's not needed for the actual binding, just to determine the correct shader to use. In fact, I don't think this is actually passed to the textureBindingViewDimension in the TextureDescriptor. So this sample only works for the default dimensions, and wouldn't work if you actually set it to cube or 1-layer array? (This is fine I suppose I'm just trying to understand.)

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The sample works for cube maps. The reason it doesn't pass the textureBindingViewDimension from the GPUTextureDescriptor is because the descriptor is unavailable. generateMips only input is the GPUTexture. The textureBindingViewDimesion is lost after texture creation. It's common to call generateMips after updating the contents of the first mip and it would be inconvenient if your code that wasn't keeping the texture descriptors around suddenly had to keep them around just for this function. As it is you don't have to keep the descriptor around. You just pass 'cube' in the case of cubemaps, otherwise it just works.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK, so what you're saying is, this is a useful utility function and the given sample just exercises some of its capabilities. That's fine, I'm just trying to understand it.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I could add examples of all 4 types of textures 2d, 2d-array, cube, cube-array (skipped in compat). It would make the sample larger. Then the question of how to show them. I guess I could spinning draw cubes with and without mipmaps

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If textureBindingViewDimension were reflected from the GPUTexture, could we get rid of this helper?
We already reflect other stuff, it seems like we can reflect this too, especially if it's useful.

In Core we would probably return undefined.

* correct in all cases because we can't distinguish between
* a 2d texture and a 2d-array texture with 1 layer, nor can
* we distinguish between a 2d-array texture with 6 layers and
* a cubemap.
*/
export function getDefaultViewDimensionForTexture(
dimension: GPUTextureDimension,
depthOrArrayLayers: number
) {
switch (dimension) {
case '1d':
return '1d';
default:
case '2d':
return depthOrArrayLayers > 1 ? '2d-array' : '2d';
case '3d':
return '3d';
}
}

type DeviceSpecificInfo = {
sampler: GPUSampler;
module: GPUShaderModule;
pipelineByFormatAndView: Map<string, GPURenderPipeline>;
};

function createDeviceSpecificInfo(device: GPUDevice): DeviceSpecificInfo {
const module = device.createShaderModule({
label: 'textured quad shaders for mip level generation',
code: generateMipmapWGSL,
});

const sampler = device.createSampler({
minFilter: 'linear',
magFilter: 'linear',
});

return {
module,
sampler,
pipelineByFormatAndView: new Map(),
};
}

const s_deviceToDeviceSpecificInfo = new WeakMap<
GPUDevice,
DeviceSpecificInfo
>();

/**
* Generates mip levels 1 to texture.mipLevelCount - 1 using
* mip level 0 as the source.
* @param device The device
* @param texture The texture to generate mips for
* @param textureBindingViewDimension The view dimension, needed for
* compatibility mode if the texture is a cube map OR if the texture
* is a 1 layer 2d-array.
*/
export function generateMips(
device: GPUDevice,
texture: GPUTexture,
textureBindingViewDimension?: GPUTextureViewDimension
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like you never actually call this function with a value for this parameter, and always rely on the default. Is that correct? If so, it seems like you'll never exercise generating mips for cube maps or 1-layer arrays. Is that intentional or did I miss something?

Maybe I'm Compat-biased, but I feel like we should just make this parameter mandatory, rather than duplicate the browser's default logic if it's omitted.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's the opposite of your advice when I wrote tests in the past. You've said in the past, only set textureBindingViewDimension if it's required, otherwise rely on the defaults.

I think someone coming into this just wants it to work without having the specify anything. It will work except for cube and 1 layer 2d-array. No one is likely to make a 1-layer 2d array so that leaves cube. For cube they'll get a validation error alone the lines of tried to use a 2d-array as a cube, can't do that in compat mode

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I agree with myself. :) Don't pass tbvd if you don't need to. It's just that it seems a bit sad to have to duplicate the browser's default logic for it, but perhaps that's unavoidable if you want to have the simple case be simple (2D).

What I'm getting at is, the sample doesn't show how to correctly generate cube maps, even though the utility function suggests it can do it. So I feel like the utility function should either be stripped down to just 2d, or the sample should exercise cube maps as well.

All that said, I think this is a great sample and it's also fine as is.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I updated to show 4 cubes. One uses 2d, one 2d-array, one cube, one cube-array if it can, else cube

) {
textureBindingViewDimension =
textureBindingViewDimension ??
getDefaultViewDimensionForTexture(
texture.dimension,
texture.depthOrArrayLayers
);
const deviceSpecificInfo =
s_deviceToDeviceSpecificInfo.get(device) ??
createDeviceSpecificInfo(device);
s_deviceToDeviceSpecificInfo.set(device, deviceSpecificInfo);
const { sampler, module, pipelineByFormatAndView } = deviceSpecificInfo;

const id = `${texture.format}.${textureBindingViewDimension}`;

if (!pipelineByFormatAndView.get(id)) {
// Choose an fragment shader based on the viewDimension (removes the '-' from 2d-array and cube-array)
const entryPoint = `fs${textureBindingViewDimension.replace('-', '')}`;
pipelineByFormatAndView.set(
id,
device.createRenderPipeline({
label: `mip level generator pipeline for ${textureBindingViewDimension}, format: ${texture.format}`,
layout: 'auto',
vertex: {
module,
},
fragment: {
module,
entryPoint,
targets: [{ format: texture.format }],
},
})
);
}

const pipeline = pipelineByFormatAndView.get(id);
const encoder = device.createCommandEncoder({
label: 'mip gen encoder',
});

// For each mip level > 0, sample the previous mip level
// while rendering into this one.
for (
let baseMipLevel = 1;
baseMipLevel < texture.mipLevelCount;
++baseMipLevel
) {
for (let layer = 0; layer < texture.depthOrArrayLayers; ++layer) {
const bindGroup = device.createBindGroup({
layout: pipeline.getBindGroupLayout(0),
entries: [
{ binding: 0, resource: sampler },
{
binding: 1,
resource: texture.createView({
dimension: textureBindingViewDimension,
baseMipLevel: baseMipLevel - 1,
mipLevelCount: 1,
}),
},
],
});

const renderPassDescriptor: GPURenderPassDescriptor = {
label: 'our basic canvas renderPass',
colorAttachments: [
{
view: texture.createView({
dimension: '2d',
baseMipLevel,
mipLevelCount: 1,
baseArrayLayer: layer,
arrayLayerCount: 1,
}),
loadOp: 'clear',
storeOp: 'store',
},
],
};

const pass = encoder.beginRenderPass(renderPassDescriptor);
pass.setPipeline(pipeline);
pass.setBindGroup(0, bindGroup);
// draw 3 vertices, 1 instance, first instance (instance_index) = layer
pass.draw(3, 1, 0, layer);
pass.end();
}
}

const commandBuffer = encoder.finish();
device.queue.submit([commandBuffer]);
}
63 changes: 63 additions & 0 deletions sample/generateMipmap/generateMipmap.wgsl
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
const faceMat = array(
mat3x3f( 0, 0, -2, 0, -2, 0, 1, 1, 1), // pos-x
mat3x3f( 0, 0, 2, 0, -2, 0, -1, 1, -1), // neg-x
mat3x3f( 2, 0, 0, 0, 0, 2, -1, 1, -1), // pos-y
mat3x3f( 2, 0, 0, 0, 0, -2, -1, -1, 1), // neg-y
mat3x3f( 2, 0, 0, 0, -2, 0, -1, 1, 1), // pos-z
mat3x3f(-2, 0, 0, 0, -2, 0, 1, 1, -1)); // neg-z

struct VSOutput {
@builtin(position) position: vec4f,
@location(0) texcoord: vec2f,
@location(1) @interpolate(flat, either) baseArrayLayer: u32,
};

@vertex fn vs(
@builtin(vertex_index) vertexIndex : u32,
@builtin(instance_index) baseArrayLayer: u32,
) -> VSOutput {
var pos = array<vec2f, 3>(
vec2f(-1.0, -1.0),
vec2f(-1.0, 3.0),
vec2f( 3.0, -1.0),
);

var vsOutput: VSOutput;
let xy = pos[vertexIndex];
vsOutput.position = vec4f(xy, 0.0, 1.0);
vsOutput.texcoord = xy * vec2f(0.5, -0.5) + vec2f(0.5);
vsOutput.baseArrayLayer = baseArrayLayer;
return vsOutput;
}

@group(0) @binding(0) var ourSampler: sampler;

@group(0) @binding(1) var ourTexture2d: texture_2d<f32>;
@fragment fn fs2d(fsInput: VSOutput) -> @location(0) vec4f {
return textureSample(ourTexture2d, ourSampler, fsInput.texcoord);
}

@group(0) @binding(1) var ourTexture2dArray: texture_2d_array<f32>;
@fragment fn fs2darray(fsInput: VSOutput) -> @location(0) vec4f {
return textureSample(
ourTexture2dArray,
ourSampler,
fsInput.texcoord,
fsInput.baseArrayLayer);
}

@group(0) @binding(1) var ourTextureCube: texture_cube<f32>;
@fragment fn fscube(fsInput: VSOutput) -> @location(0) vec4f {
return textureSample(
ourTextureCube,
ourSampler,
faceMat[fsInput.baseArrayLayer] * vec3f(fract(fsInput.texcoord), 1));
}

@group(0) @binding(1) var ourTextureCubeArray: texture_cube_array<f32>;
@fragment fn fscubearray(fsInput: VSOutput) -> @location(0) vec4f {
return textureSample(
ourTextureCubeArray,
ourSampler,
faceMat[fsInput.baseArrayLayer] * vec3f(fract(fsInput.texcoord), 1), fsInput.baseArrayLayer);
}
30 changes: 30 additions & 0 deletions sample/generateMipmap/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<!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: generateMipmap</title>
<style>
:root {
color-scheme: light dark;
}
html, body {
margin: 0; /* remove default margin */
height: 100%; /* make body fill the browser window */
display: flex;
place-content: center center;
}
canvas {
width: 600px;
height: 600px;
max-width: 100%;
display: block;
}
</style>
<script defer src="main.js" type="module"></script>
<script defer type="module" src="../../js/iframe-helper.js"></script>
</head>
<body>
<canvas></canvas>
</body>
</html>
Loading