Skip to content

Conversation

slipher
Copy link
Member

@slipher slipher commented Aug 29, 2025

Fix the code for laying out the std430 struct containing the uniforms for material system GLSL shaders. There is no functional change given the exact sets of uniforms we have on our GLSL shaders right now, but adding a new one could cause things to blow up as demonstrated at #1696 (comment).

I'm not attempting to run the added unit tests on CI for now as the client test does full client initialization including opening a window. So it wouldn't easily run in a headless environment.

Explicitly writing the padding seems pointless and the way
the padding is calculated looks suspicious.
Use DAEMON_ASSERT_xyz instead of ASSERT_xyz in headers, so that the
headers are compatible with Googletest. Also document this requirement
in Assert.h and provide a non-shorthand version of ASSERT_UNREACHABLE.
If the uniform size is divisible by 16 it wrongly thought
that 16 bytes of padding (instead of 0) were needed.

Add a unit test demonstrating the case that was broken.
@VReaperV
Copy link
Contributor

Remove padding from end of material uniform struct
Explicitly writing the padding seems pointless and the way
the padding is calculated looks suspicious.

The point is that I can copy it from shader source to buffer view in Nsight and immediately see the values read by the shader.

@slipher
Copy link
Member Author

slipher commented Aug 29, 2025

The point is that I can copy it from shader source to buffer view in Nsight and immediately see the values read by the shader.

I don't get it, why would adding dummy fields at the end make this more effective?

@VReaperV
Copy link
Contributor

The point is that I can copy it from shader source to buffer view in Nsight and immediately see the values read by the shader.

I don't get it, why would adding dummy fields at the end make this more effective?

Because otherwise I'd need to add them manually.

@slipher
Copy link
Member Author

slipher commented Aug 29, 2025

OK I found that in the documentation:

Alignments are not considered; all types are considered packed.

Does the latest commit work? I would try it but the last time I tried to update the software, it broke - it can no longer establish the socket connection.

The original code seemed strange since it could only add padding at the end. I assume you need to add it anywhere else there are holes too? That's what I did.

@VReaperV
Copy link
Contributor

Thx, I'll try that in a bit.

Yeah, the original code might be wrong on that.

@VReaperV
Copy link
Contributor

Does the latest commit work? I would try it but the last time I tried to update the software, it broke - it can no longer establish the socket connection.

That does work, yes.

The change in struct packing does, however, break when the material uniforms add up to less than 4 components/16 bytes. In that case the size is calculated as if they're tightly packed, when in the actual buffer interface they're rounded up to a vec4. The std430Size naming is misleading at this point, since the only buffer interfaces that use the generated structs are now using std140. The related variables functions can be renamed or a boolean flag added to round up the alignment to vec4. Personally though, I don't have any plans to add buffers with generated struct arrays and std430 interfaces.

Also, I believe the implementation in this pr doesn't calculate the size for arrays correctly as the component count is never taken into account.

Fix some bugs and add more unit tests.
It's actually std140 not std430, so fix comment and GLShader member
names to reflect that. (GLUniform still has the old _std430XXX names
though.)

Since it cannot handle arrays, add an assert that the uniforms are not
arrays. Also delete WriteToBuffer implementations that assume std430
layout for arrays.
@slipher
Copy link
Member Author

slipher commented Aug 30, 2025

The change in struct packing does, however, break when the material uniforms add up to less than 4 components/16 bytes. In that case the size is calculated as if they're tightly packed, when in the actual buffer interface they're rounded up to a vec4.

I didn't realize it actually uses std140, not std430. Fixed. I renamed some of the variable/function names in GLShader to reflect this.

Also, I believe the implementation in this pr doesn't calculate the size for arrays correctly as the component count is never taken into account.

The code was not prepared to handle arrays anyway since the WriteToBuffer functions all assumed std430 packing, plus they would have multiplied any padding at the end by the number of elements. So I added an assert that arrays are not used and NUKED WriteToBuffer implementations that are not compatible with std140.

@VReaperV
Copy link
Contributor

VReaperV commented Aug 30, 2025

The code was not prepared to handle arrays anyway since the WriteToBuffer functions all assumed std430 packing, plus they would have multiplied any padding at the end by the number of elements. So I added an assert that arrays are not used and NUKED WriteToBuffer implementations that are not compatible with std140.

I have it working fine in #1684 for u_Frustum. It's enough to check that the underlying value is 16 bytes and multiply by the array size when calculating struct size. Padding is never needed for such a uniform since it's always aligned.

The change to GLUniform1Bool seems erroneous?

@slipher
Copy link
Member Author

slipher commented Aug 31, 2025

I have it working fine in #1684 for u_Frustum. It's enough to check that the underlying value is 16 bytes and multiply by the array size when calculating struct size. Padding is never needed for such a uniform since it's always aligned.

OK I added pack support for 4-aligned arrays.

The change to GLUniform1Bool seems erroneous?

It's not strictly related to lack of array support, but I deleted it in passing since it also looks wrong. It only writes 1 byte out of 4 and even if you can get away with that, it's definitely not endian-aware.

@DolceTriade
Copy link
Contributor

DolceTriade commented Sep 1, 2025

Does this fix the edge case where if you add a uniform that is 4 byte aligned, it messes up the alignment of the struct and causes view corruption? I hit that in some of my testing..

I hacked it like this:

@@ -2135,7 +2135,7 @@ void GLShader::PostProcessUniforms() {
                GLuint size = _materialSystemUniforms[0]->_std430Size;
                GLuint components = _materialSystemUniforms[0]->_components;
                size = components ? PAD( size, 4 ) * components : size;
-               GLuint alignmentConsume = 4 - size % 4;
+               GLuint alignmentConsume = (size % 4 == 0) ? 0 : 4 - size % 4;
 
                GLUniform* tmpUniform = _materialSystemUniforms[0];
                tmp.emplace_back( _materialSystemUniforms[0] );

(which is probably wrong, but it fixed my corruption)

@slipher
Copy link
Member Author

slipher commented Sep 1, 2025

Does this fix the edge case where if you add a uniform that is 4 byte aligned, it messes up the alignment of the struct and causes view corruption? I hit that in some of my testing..

I hacked it like this:

@@ -2135,7 +2135,7 @@ void GLShader::PostProcessUniforms() {
                GLuint size = _materialSystemUniforms[0]->_std430Size;
                GLuint components = _materialSystemUniforms[0]->_components;
                size = components ? PAD( size, 4 ) * components : size;
-               GLuint alignmentConsume = 4 - size % 4;
+               GLuint alignmentConsume = (size % 4 == 0) ? 0 : 4 - size % 4;
 
                GLUniform* tmpUniform = _materialSystemUniforms[0];
                tmp.emplace_back( _materialSystemUniforms[0] );

(which is probably wrong, but it fixed my corruption)

Yes. The Fix bad material SSBO uniform layout for multiple of 16 commit gets that one.

if ( uniforms[i]->_std430Size <= alignment ) {
return i;
static auto FindUniformForOffset( std::vector<GLUniform*>& uniforms, const GLuint baseOffset ) {
for ( auto it = uniforms.begin(); it != uniforms.end(); ++it ) {
Copy link
Contributor

Choose a reason for hiding this comment

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

You can just do it : uniforms if you're making it an iterator instead of an index.

Copy link
Member Author

Choose a reason for hiding this comment

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

I don't see how - you just get a reference to the element that way, not an iterator.

} else {
( *iterNext )->_std430Size = ( *iterNext )->_std430BaseSize;
if ( ( *iterNext )->_components ) {
ASSERT_GE( ( *iterNext )->_std430Alignment, 4 ); // aligment less than 4 not compatible with std130
Copy link
Contributor

Choose a reason for hiding this comment

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

*std140

Copy link
Member Author

Choose a reason for hiding this comment

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

Clarified the comment.

@VReaperV
Copy link
Contributor

VReaperV commented Sep 2, 2025

It's not strictly related to lack of array support, but I deleted it in passing since it also looks wrong. It only writes 1 byte out of 4 and even if you can get away with that, it's definitely not endian-aware.

I see. #1685 should fix that once merged, and will get rid of the WriteToBuffer() overloads.

@slipher slipher merged commit 1649189 into DaemonEngine:master Sep 4, 2025
9 checks passed
@slipher slipher deleted the structfix branch September 4, 2025 16:56
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.

3 participants