EmbarkStudios / rust-gpu

🐉 Making Rust a first-class language and ecosystem for GPU shaders 🚧
https://shader.rs
Apache License 2.0
7.32k stars 247 forks source link

Supporting Image Operands in spirv-std #364

Open XAMPPRocky opened 3 years ago

XAMPPRocky commented 3 years ago

A lot of image operations (mainly those around sampling, or getting/setting texels from images) support additional optional operands. At the SPIR-V level this comes in the form of an operand at the end of each instruction which is a 8 bit integer that acts as a bit mask for indicating the how many operands follow and what they are used for. Additionally certain operands can only be used with certain image parameters, (such as the Bias operand only being available for single sampled 1D, 2D, 3D, and Cube image.)

The question becomes how to best support using these operands at the Rust level and validate these constraints.

Possibilities

One example of where it's unclear with constant generics is the Grad operand. Which allows you to pass 2 vectors or scalars of the same size as the Dim, so right now I'm unsure how we'd allow that to generically while also validating the length.

Jasper-Bekkers commented 3 years ago

For reference; GLSL has the following texture sampling functions

ivec textureSize(gsampler sampler​, int lod​);
int textureQueryLevels(gsampler sampler​);
gvec texture(gsampler sampler​, vec texCoord​[, float bias​]);
gvec textureOffset(gsampler sampler​, vec texCoord​, ivec offset​[, float bias​]);
gvec textureProj(gsampler sampler​, vec projTexCoord​[, float bias​]);
gvec textureLod(gsampler sampler​, vec texCoord​, float lod​);
gvec textureGrad(gsampler sampler​, vec texCoord​, gradvec dTdx​, gradvec dTdy​);
gvec4 textureGather(gsampler sampler​​, vec texCoord​​, int comp​);
gvec4 textureGatherOffset(gsampler sampler​​, vec texCoord​​, ivec2 offset​, int comp​);
gvec4 textureGatherOffsets(gsampler sampler​​, vec texCoord​​, ivec2 offsets​[4], int comp​);
gvec textureProjLodOffset(gsampler sampler​, vec projTexCoord​, float lod​, ivec offset​);

Some of those have optional parameters; if we were to mimic these 1:1 in Rust we'd end up with these sampling functions

ivec texture_size(gsampler sampler​, int lod​);
int texture_query_levels(gsampler sampler​);
gvec texture(gsampler sampler​, vec texCoord​);
gvec texture_with_bias(gsampler sampler​, vec texCoord​, float bias​);
gvec texture_offset(gsampler sampler​, vec texCoord​, ivec offset​);
gvec texture_offset_with_bias(gsampler sampler​, vec texCoord​, ivec offset​, float bias​);
gvec texture_proj(gsampler sampler​, vec projTexCoord​);
gvec texture_proj_with_bias(gsampler sampler​, vec projTexCoord​, float bias​);
gvec texture_lod(gsampler sampler​, vec texCoord​, float lod​);
gvec texture_grad(gsampler sampler​, vec texCoord​, gradvec dTdx​, gradvec dTdy​);
gvec4 texture_gather(gsampler sampler​​, vec texCoord​​, int comp​);
gvec4 texture_gather_offset(gsampler sampler​​, vec texCoord​​, ivec2 offset​, int comp​);
gvec4 texture_gather_offsets(gsampler sampler​​, vec texCoord​​, ivec2 offsets​[4], int comp​);
gvec texture_proj_lod_offset(gsampler sampler​, vec projTexCoord​, float lod​, ivec offset​);

Which doesn't seem too bad?

XAMPPRocky commented 3 years ago

If it's just those it might be fine, though as far as I can tell the spec allows for much more combinations (for example you could have texture with Lod,Bias, MinLod, and Offset operands simultaneously) , and it applies not just to sampling functions, but also OpImageRead, OpImageWrite, and all the OpImageSparse* equivalents.

Another pattern I was thinking that could be useful here, is the builder pattern for the operands part, which would allow to us to only have one extra function for each instruction equivalent. So for example we could have the texture that doesn't include any and the texture_with_operands function which returns a ImageOperandBuilder so then we would just have methods for each of the operands individually, and then a final build/run method. For example handling the above case would look like the following.

image.texture_with_operands(sampler, coords)
    .bias(1.0)
    .lod(3.0)
    .offset(vec![1.0, 2.0])
    .min_lod(2.0)
    .run()
DeanBDean commented 3 years ago

So many AWS functions have lots and lots of optional parameters. The way that rusoto, the wrapper library for AWS in Rust, handles this by defining all possible inputs in a struct, with the Default trait also defined. You can then pretty ergonomically specific the parameters you want, and then call Default::default() to hydrate the rest of the struct. Here is an example from that library. In this case, the operands would just be wrapped in Option, and default would be None.

The builder pattern looks easier to use and reason to me, but I just wanted to throw this out there as an option

khyperia commented 3 years ago

Note that "dynamically" passing in values (like the builder methods, or arguments to a method) runs into compiler implementation difficulty: in the instruction, the extra operands bit pattern is embedded into the instruction itself. Doing a potentially dynamic thing like a builder pattern, or passing in the bit pattern as an argument, is really tough to implement (you could add some if statements to change what the resulting bit pattern is, which would somehow have to be a compiler error) - const generic argument should be fine though.

XAMPPRocky commented 3 years ago

Note that "dynamically" passing in values (like the builder methods, or arguments to a method) runs into compiler implementation difficulty

Well the builder itself could be passed in a const fashion, it wouldn't have the syntax above though.

The problem I was running into with it all being const generic arguments is enforcing things like ConstOffset requiring that the number of components it uses is equal to the number of components you passed in minus the array layer. And currently you can't have const generic parameters that depend on other parameters.

#![feature(const_generics)]

const fn bar(v: u8) -> u8 {
    match v {
        0 => 1,
        1 => 2,
        _ => 3
    }
}

// Errors
const fn foo<const V: u8, const A: [u8; { bar(V) }]>() {}
khyperia commented 3 years ago

Ah, just realized these issues weren't linked: https://github.com/EmbarkStudios/rust-gpu/issues/369 is about supporting image operands in asm!, and this issue is about supporting image operands in the user-visible spirv-std API (which are two distinct things).

oisyn commented 1 year ago

This came up again in the context of sampling from multisampled images. I really think we need some kind of generic way to specify the operands, rather than creating all sorts of permutations for the different sampling instructions.

A third option not mentioned is to use some kind of macro. That way we can add a bit of grammar catering to our needs and do something more elaborate at compile time.

Jasper-Bekkers commented 1 year ago

This came up again in the context of sampling from multisampled images. I really think we need some kind of generic way to specify the operands, rather than creating all sorts of permutations for the different sampling instructions.

A third option not mentioned is to use some kind of macro. That way we can add a bit of grammar catering to our needs and do something more elaborate at compile time.

You may want to consider a macro to generate the functions rather then one on the usage side of the API.