Skip to content

Conversation

@xinaesthete
Copy link
Contributor

Fixes #687

Background

#687 as well as elsewhere for discussion of UBO etc.

This is work-in-progress... started out just trying to update to latest luma.gl/deck.gl to deal with some peer-dependency issues downstream. Latest versions remove the deprecated setUniforms etc, so this is becoming more pressing.

This is still WIP as of this writing, but I think starting to take shape for a way of supporting N-channels without too much pain for extension authors.

Change List

  • declaring shader uniforms as UBO
  • setting props with model.shaderInputs rather than setUniforms
  • tending to prefer implementing updateState() rather than draw(), I think this is more in line with how things are supposed to be
  • trying to make sure any assumptions about MAX_CHANNELS = 6 is findable in the code etc...
  • shaders have a define for NUM_CHANNELS which should be used consistently where relevant
  • initial version of a shader code preprocessor wrapped in a new VivShaderAssembler class for expanding lines with <VIV_CHANNEL_INDEX>...

Checklist

  • Update JSdoc types if there is any API change.
  • Make sure Avivator works as expected with your change.
  • Update documentation for migrating shader extensions to the new form once the design is settled.

@xinaesthete xinaesthete changed the title Deck 9.2 Uniform buffers for shader props, allow variable number of channels Nov 8, 2025
@dbmi-svc-checkmarx
Copy link

dbmi-svc-checkmarx commented Nov 8, 2025

Logo
Checkmarx One – Scan Summary & Detailsb494cb8b-6d21-44a3-bb60-ce815ae431f7

Great job! No new security vulnerabilities introduced in this pull request


Use @Checkmarx to interact with Checkmarx PR Assistant.
Examples:
@Checkmarx how are you able to help me?
@Checkmarx rescan this PR

Copy link
Collaborator

@ilan-gold ilan-gold left a comment

Choose a reason for hiding this comment

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

I think it's reasonable to ask in deck.gl slack or on the github what the implications of a custom ShaderAssembler would be. I really couldn't know off hand. As you can probably tell, I did a lot of this by just looking at the underlying code and ballparking what would be needed. The upside is it works. The downside is that it has a bunch of private accesses. At the time, I was under paper pressure :/

I don't feel too strongly about how off-the-beaten-path this goes as long as it works. We advertise breaking changes on the minor version so I'm also not worried about any breaking changes. I'll try to download the deck.gl slack on Monday again.


const _RENDER = `\
float intensityArray[6] = float[6](intensityValue0, intensityValue1, intensityValue2, intensityValue3, intensityValue4, intensityValue5);
float intensityArray[NUM_CHANNELS] = float[NUM_CHANNELS](intensityValue0, intensityValue1, intensityValue2, intensityValue3, intensityValue4, intensityValue5);
Copy link
Collaborator

Choose a reason for hiding this comment

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

Yes, this is great!

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Note that there is one other place where I have something more like

float array[NUM_CHANNELS] = float[NUM_CHANNELS](
  value<VIV_CHANNEL_INDEX>,
);

and that also works...

Now I'm trying to get that to also work for uniform vec2 constastLimits<VIV_CHANNEL_INDEX>; with the new shaderInputs UBO binding and finding it tricky.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Or rather... the syntax I mentioned works ok, but the new

uniform xrLayerUniforms {
  vec2 contrastLimits<VIV_CHANNEL_INDEX>;
} xrLayer;

I'm struggling with.

Copy link
Contributor Author

@xinaesthete xinaesthete Nov 8, 2025

Choose a reason for hiding this comment

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

I think the issue may be that I was trying to implement setting shaderInputs in the draw() method and moving this to updateState() makes a significant difference (just about to try this). edit: also, working in js rather than ts and having typos.

Comment on lines +80 to +81
// does any of this existing logic need to change?
// (like, is there ever more than one model?)
Copy link
Collaborator

Choose a reason for hiding this comment

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

I think the way our extensions are structured, no, because extension is attached to XRLayer which does its own rendering?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes I don't anticipate a problem there... In some places I'm looping over this.getModels() and it may be that it would be somewhat safer to do that a bit more consistently, but I'm pretty sure it isn't likely to matter in foreseeable future. Slight balance between consistency and avoiding changes where there isn't a solid reason for it.

@xinaesthete
Copy link
Contributor Author

@ilan-gold Ib already expressed some enthusiasm about viv updating deck versions and I'm sure he'll be happy to give some more feedback at some point. I'll add a pointer to this PR over there now.

@xinaesthete
Copy link
Contributor Author

n.b. I've made all the CI pass and will try to keep commits clean WRT that, but bear in mind that I don't think the configuration is properly working for '@deck.gl/test-utils' as noted #921 so we need to be extra careful to manually verify that layers/extensions etc do actually work correctly.

@xinaesthete
Copy link
Contributor Author

Also I think that volume minimum intensity projection is already buggy - it looks similar in this branch to production avivator build, which is to say, not good.

@ilan-gold
Copy link
Collaborator

Also I think that volume minimum intensity projection is already buggy - it looks similar in this branch to production avivator build, which is to say, not good.

Yes it is, should eventually fix that...

@keller-mark
Copy link
Member

I am not sure that minimum intensity projection has bugs, it is just that it is not really useful, conceptually, for most images. I think it was implemented because it is very easy once you implement the max-intensity projection. But why would I want to view the "minimum-intensity" pixels, when they are just going to correspond the black space that I am not typically interested in?

I think the only time it would really make sense is if the image colors have been inverted for some reason.

(Maybe I am wrong, but before we assume it has bugs we should verify with another viewer that supports MinIP)

@ilan-gold
Copy link
Collaborator

You're right @keller-mark I was mostly speaking from an experience where I wanted the inversion actually and remember it not working properly. But I don't know for sure.

@xinaesthete
Copy link
Contributor Author

Anyway, for this PR, I think I found an actual concrete example of something that I don't think is currently broken with the changes, but easily could be.

Where previously in lens-module.js we had

useColorValue = get_use_color_float(vTexCoord, 0);
rgb += max(0., min(1., intensity0)) * max(vec3(colors[0]), (1. - useColorValue) * vec3(1., 1., 1.));

useColorValue = get_use_color_float(vTexCoord, 1);
rgb += max(0., min(1., intensity1)) * max(vec3(colors[1]), (1. - useColorValue) * vec3(1., 1., 1.));
...

In that particular instance, we now have

for(int i = 0; i < NUM_CHANNELS; i++) {
  float useColorValue = get_use_color_float(vTexCoord, i);
  rgb += max(0., min(1., intensity[i])) * max(vec3(colors[i]), (1. - useColorValue) * vec3(1., 1., 1.));
}

Which is fine - but in other cases where we (or some hypothetical user with a custom extension) aren't able to loop over an array (which even if luma.gl did allow us to have arrays in uniforms, we still wouldn't be able to have arrays of samplers), it might be written like

useColorValue = get_use_color_float(vTexCoord, ${I});
rgb += max(0., min(1., intensity${I)) * max(vec3(colors[${I}]), (1. - useColorValue) * vec3(1., 1., 1.));

which would then lead to the generated shader being

useColorValue = get_use_color_float(vTexCoord, 0);
useColorValue = get_use_color_float(vTexCoord, 1);
rgb += max(0., min(1., intensity0) * max(vec3(colors[0]), (1. - useColorValue) * vec3(1., 1., 1.));
rgb += max(0., min(1., intensity1) * max(vec3(colors[1]), (1. - useColorValue) * vec3(1., 1., 1.));

So... the current line duplicating logic is less than ideal. Without over-engineering it too much, we do probably need a way to guard against that particular kind of foot-gun and expose an appropriate abstraction. As alluded, I may if I get a chance want to have extensions that do more elaborate forms of code-generation, in which case I'd probably want to have a proper parser and more robust processes etc... but adding that to viv itself seems like overkill. I'll want a way of plugging in more processing steps where I can do such things (hopefully also some use to others), but with a low surface-area in terms of the API we expose from viv.

Hope that makes sense.

@xinaesthete
Copy link
Contributor Author

xinaesthete commented Nov 10, 2025

Whether we ultimately have a subclass of ShaderAssembler or not, more of that kind of code should move to the extensions package, and we can probably have some fairly basic and low-maintenance mechanism for wrapping blocks of code that are to be duplicated for each channel, and a bit more of a type-safe & ergonomic way of describing viv extensions.

Still need to get the shaderInput binding stuff sorted out, hopefully when I get back to it with a fresh mind it'll make better sense. That's the part that is required for updating luma & deck - and once we've done that, there's now a WebGPU backend which it'll be interesting to start experimenting with. Something to bear in mind for added complexity to extension design.

@ilan-gold
Copy link
Collaborator

ilan-gold commented Nov 12, 2025

Which is fine - but in other cases where we (or some hypothetical user with a custom extension) aren't able to loop over an array

So a different option would be to do our best, be super clear about what Viv does with its pipeline, and then just allow people to provide their own shaders completely (or almost completely, maybe just main plus custom uniforms). Does that seem reasonable?

@xinaesthete
Copy link
Contributor Author

Which is fine - but in other cases where we (or some hypothetical user with a custom extension) aren't able to loop over an array

So a different option would be to do our best, be super clear about what Viv does with its pipeline, and then just allow people to provide their own shaders completely (or almost completely, maybe just main plus custom uniforms). Does that seem reasonable?

Maybe, I'm hoping when I get back to this we can have something fairly clear and simple...

I'm not actually sure that it'll end up easier for anyone trying to figure out how we handle an 'almost complete shader' - I've a feeling the documentation and DX/props handling may not be so easy to figure out...

Anyway, fairly confident in coming up with something decent when I get back to it, fingers crossed.

@xinaesthete
Copy link
Contributor Author

xinaesthete commented Jan 8, 2026

So - it's occurred to me that the semi-hypothetical situation I was worried about where

float mutableThing = 0.;
mutableThing = opacity${I};
float value${I} = mutableThing;

expands to

float mutableThing = 0.;
mutableThing = opacity0;
mutableThing = opacity1;
float value0 = mutableThing;
float value1 = mutableThing;

Can actually be avoided by simply warning extension authors of the risk and being very explicit that they should instead collect things into arrays and process however they please... so we can keep just the simpler line-duplicating extension API rather than needing to have semantics for defining blocks of code. I don't think that would have necessarily been too disastrous - but there's a definite risk of it hiding some unintended complexity and still not allowing for more complex cases like where someone might want to have nested permutations or whatever.

I think if anyone does actually want to have some complex nest of channel permutations or whatever, or just a bit of mutable state, they will be able to... so, I'll try to make documentation as clear as possible, clean up the code etc (and actually implement the actual UBO binding bits which are still mostly pending). But I think I'm now satisfied that there's a KISS version of this extension API which should allow implementation of whatever arbitrary stuff someone might hypothetically want... and it's likely that hardly anyone (even me) will actually need it anyway - so if it turns out that there are edge-cases that do require some more complexity to support, we can cross that bridge when we come to it.

Hope that makes sense.

…ations, apply to contrast limit processing in xr-layer. Updated expandShaderModule to expand both uniform types and shader code. Adjusted XRLayer to utilize new channel intensity module structure.
move VivShaderAssembler to extensions package, add NUM_PLANES template.
faff with extensions build settings in package.json.
@ilan-gold
Copy link
Collaborator

Awesome @xinaesthete - my biggest concern is definitely just that everythign is 100% clear. If it's a small change or something subtle that most people won't notice, that's ok. Re-request review once you're ready and I'll have a look!

@xinaesthete
Copy link
Contributor Author

Awesome @xinaesthete - my biggest concern is definitely just that everythign is 100% clear. If it's a small change or something subtle that most people won't notice, that's ok. Re-request review once you're ready and I'll have a look!

Yeah it's a bit broken in some ways at the moment - mostly the colormaps in 2d that I'm aware of. Hoping to get that properly fixed soon and do a pass on documentation and cleanup, will let you know. It's a shame that the deck.gl layer-test stuff isn't really working, may even be worth trying to have a pass on that in a separate PR and merging in.

Probably once this is in a better state it might make sense to put the actual varying of number of channels at runtime into a separate PR, but I think I'm reasonably satisfied that the API for that is fairly stable and working.

@ilan-gold
Copy link
Collaborator

Probably once this is in a better state it might make sense to put the actual varying of number of channels at runtime into a separate PR, but I think I'm reasonably satisfied that the API for that is fairly stable and working.

That seems reasonable!

@xinaesthete
Copy link
Contributor Author

Also - now that I think it should not be using any APIs that aren't in the latest deck.gl, I will try to do the actual deck.gl update soon and see whether everything does indeed still work.

@xinaesthete
Copy link
Contributor Author

I might also be able to make a more detailed example of a moderately complex shader extension in the context of a spatialdata.js blog post with interactive sample.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Max channels allowed

4 participants