Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Design the shader parameters logic #19

Closed
kvark opened this issue Jun 23, 2014 · 9 comments · Fixed by #40 or #116
Closed

Design the shader parameters logic #19

kvark opened this issue Jun 23, 2014 · 9 comments · Fixed by #40 or #116

Comments

@kvark
Copy link
Member

kvark commented Jun 23, 2014

A draw call from the user side should provide the following entities:

  • SubMesh, which is a slice into the attributes and vertex indices, which themselves are linked to raw buffers
  • target::Frame, which contains the surfaces to be bound as colors and depth targets
  • shader::Program, which is not just the shader program ID, but also has meta-data attached about all the used attributes and uniform values/buffers
  • shader::Environment, the unresolved key structure to encapsulate all needed shader parameters in some form, including the material data, camera, lights, etc.

The last thing is what needs to be designed efficiently, since it needs to be accessed on every call, matched against shader meta-data (see Draw Call Verification), and also passed around a lot.
In Claymore I used the Map to store parameters by name:

pub struct DataMap( HashMap<~str,Uniform> );

pub enum Uniform {
    Uninitialized,
    UniFloat(f32),
    UniInt(i32),
    UniFloatVec(Vec4<f32>),
    UniIntVec(Vec4<i32>),
    UniFloatVecArray(~[Vec4<f32>]),
    UniMatrix(bool,Mat4<f32>),
    UniTexture(uint,texture::TexturePtr,Option<texture::Sampler>),
}

I consider it to be not efficient enough, because there is a lot of allocations, and carrying out names everywhere (and comparing them) is a bit too heavy. We'll need something very generic and more efficient than this.

@kvark kvark added this to the Rust Game Tech Meetup milestone Jun 23, 2014
@kvark kvark added the question label Jun 23, 2014
@kvark
Copy link
Member Author

kvark commented Jun 23, 2014

Notes from @csherratt:

...it would be interesting if you treated them like a uniform buffer. The use can use a macro to build a structure that binds the shader parameters to a POD structure. The POD structure implements a trait that allows it to throw up its content into a GLSL shader when the graphics pipeline requires it.

I think it can be made to work with or without UBOs. Without a UBO it would just be given an interface to writing to the shader's uniforms, in the UBO case it could be a memory mapped uniform buffer or something similar. I wonder just how difficult it would be to cache the shader inspection using such an interface.

@kvark
Copy link
Member Author

kvark commented Jun 23, 2014

Notes from @glennw:

What I've done previously is have a fixed enum of shader constant types rather than strings. This makes it easy to efficiently pack the data and very efficient. It also means that all of the common parameters (projection matrices, lights etc) are consistent for every shader. Then we had a small set of custom/user defined parameters available per shader - these are internally still referenced as enums (e.g. user vector 0-31, user matrix 0-15), but there is a mapping in the shader of parameter string name <-> enum id.

so the user constants have to look like CustomFloat1, CustomMatrix2, etc?

That's what the render engine sees once they are resolved, but there is a mapping in the material file which maps custom parameter name in the shader (a string) to one of the user enum constants.
So per shader you set up that mapping, and then use custom strings/names in the shader, but they get mapped to one of the fixed user constant locations.
That's how we did it anyway, that's just one solution to the problem.

@kvark
Copy link
Member Author

kvark commented Jun 30, 2014

I've advanced with this issue quite a bit. Sorry for the wall of text... I hope to get some feedback.

Shader parameters are just a more complex case of the same problem we have with vertex attributes. Those also need to be matched against shader inputs. Comparing strings at that point is not the biggest threat - we also need to verify the semantics to match, and (more importantly) we need to iterate given resources for any requested one. So there is a total of 4 components that need to be optimized out:

  1. String comparison
  2. Iteration
  3. Semantic verification
  4. Cloning the data per draw call

The first two points apply equally to vertex attributes. Semantic verification is an open question about attributes, and we may want to add it later on. No cloning is done by the user (or render client) for vertex attributes, because they are already stored on the render thread.

Here is my proposed solution:

pub type EnvBlockHandle = u8;
pub type EnvUniformHandle = u16;
pub type EnvTextureHandle = u8;

struct Environment {
    blocks: Vec<(String, device::dev::Buffer)>,
    uniforms: Vec<(String, device::shade::UniformValue)>,
    textures: Vec<(String, device::dev::Texture, device::dev::Sampler)>,
}

impl Environment {
    pub fn new() -> Environment;
    pub fn add_block(&mut self, &str, device::dev::Buffer) -> EnvBlockHandle ;
    pub fn add_uniform(&mut self, &str, device::shade::UniformValue) -> EnvUniformHandle;
    pub fn add_texture(&mut self, &str, device::dev::Texture, device::dev::Sampler) -> EnvTextureHandle;
}

impl render::Client {
    // sends the environment for the render task to store
    fn register_environment(&mut self, Environment) -> EnvironmentHandle;
    fn unregister_environment(&mut self, EnvironmentHandle);
    // these methods are to change existing environment variables
    // passing an index instead of a full name is both cheap and DRY (yay!)
    fn set_env_block(&mut self, EnvironmentHandle, EnvBlockHandle, device::dev::Buffer);
    fn set_env_uniform(&mut self, EnvironmentHandle, EnvUniformHandle, device::shade::UniformValue);
    fn set_env_texture(&mut self, EnvironmentHandle, EnvTextureHandle, device::dev::Texture, device::dev::Sampler);
}

Here is an example of the user code:

// at initialization
    let mut env = gfx::Environment::new();
    // we get a hold of this one to change it later
    let var_color = env.add_uniform("color", gfx::ValueF32Vec([0.5, ..4]));
    // not going to change it, so ignoring the result
    env.add_texture("diffuse", my_texture_handle, my_sampler_handle);
    let env_handle = renderer.register_environment(env_handle);
...
// at run-time
    renderer.set_env_uniform(env_handle, var_color, my_color_value);
    renderer.draw(..., env_handle);

During the rendering, render::Server can keep the cache in the following form:

pub type EnvironmentCache = HashMap<(EnvironmentHandle, ProgramHandle), EnvironmentShortcut>;

Where the EnvironmentShortcut has baked-in indices of all the resources that the program wants. We could find a more efficient representation for those, but for starters this should suffice:

struct EnvironmentShortcut
    blocks: Vec<EnvBlockHandle>, // size equals to ProgramMeta::blocks
    uniforms: Vec<EnvUniformHandle>, // size equals to ProgramMeta::uniforms
    textures: Vec<EnvTextureHandle> // size equals to ProgramMeta::textures
}

If the shortcut is not found, it is constructed by looking up the corresponding parameters in the Environment by matching names and semantics. In 99% of the draw calls the shortcut will be found, and walking it is the most efficient path to bind shader parameters. For each parameter, render thread will just jump to the value (by index) in the Environment and bind it.

@brendanzab
Copy link
Contributor

So is this Environment is a builder kind of thing that can subsequently baked?

@kvark
Copy link
Member Author

kvark commented Jun 30, 2014

@bjz Baking is not the right term. Environment is not replaced by EnvironmentShortcut. The latter just accelerates access to the former (upon program binding) to avoid iteration, string comparisons, and semantic verification.

@brendanzab
Copy link
Contributor

Ah ok, thanks for the clarification.

@kvark kvark mentioned this issue Jul 1, 2014
9 tasks
@kvark kvark self-assigned this Jul 1, 2014
@ghost ghost closed this as completed in #40 Jul 1, 2014
@kvark
Copy link
Member Author

kvark commented Jul 2, 2014

Here is a breakdown of our choices:

  1. Assign parameters per program, which will have its mutable state exposed. Cost is low, ergonomics is pretty good. Chance to shoot yourself in the foot (SYF chance) is high.
  2. Have Environment structure to contain all the parameters, it is partially-mutable, and can be used with multiple programs at will (that's what we have now). Cost is pretty low due to shortcut cache, ergonomics is a bit worse (due to the need to care about environment), and the SYF chance is normal (parameter values can still leak if user forgets to set them).
  3. Pass all the parameters with the draw call, thus making the program state immutable from the user perspective. Cost is higher, but depends on the implementation of the parameter descriptor. Cloning Environment would involve 3 heap allocations for the vectors, for example. Ergonomics is the same as in 2, and the SYF chance is low (user is in full control of the parameters)

Current consensus leans towards Choice 3, but we need to figure out how to make it performance effective (reduce the number of heap allocations and the memory to copy) and yet ergonomic.

@kvark kvark reopened this Jul 2, 2014
@brendanzab brendanzab reopened this Jul 3, 2014
@kvark
Copy link
Member Author

kvark commented Jul 11, 2014

Here is how Irrlicht handles it: http://irrlicht.sourceforge.net/docu/example010.html
Looks like it only supports arrays of floats, which is a bit too primitive for us... Note the IMaterialRendererServices visitor.

@kvark
Copy link
Member Author

kvark commented Jul 15, 2014

Proposal prototype N2: https://gist.github.com/kvark/182cb352b7ef599dcdbc

Apparently, we can skip N2 and aim further (from @csherratt):

I imagine I would take the environment and take each shader uniform index and throw it into a structure on the client side.

kvark pushed a commit to kvark/gfx that referenced this issue Jun 14, 2015
Refactored base vertex/instance calls
kvark pushed a commit to kvark/gfx that referenced this issue Jun 14, 2015
adamnemecek pushed a commit to adamnemecek/gfx that referenced this issue Apr 1, 2021
19: Texture creation r=kvark a=grovesNL



Co-authored-by: Joshua Groves <josh@joshgroves.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging a pull request may close this issue.

2 participants