GPU: Shader: Add support for references

Implementation of #137341

This adds support for using references to any variable in a local scope
inside the shader codebase.

Example:
```cpp
int a = 0;
int &b = a;
b++; /* a == 1 */
```
Using `auto` is supported for reference definition as the type is not
preserved by the copy paste procedure. Type checking is done by the
C++ shader compilation or after the copy paste procedure during shader
compilation. `auto` is still unsupported for other variable declarations.

Reference to opaque types (`image`, `sampler`) are supported since
they are never really assigned to a temp variable.

This implements all safety feature related to the implementation being
copy pasting the definition string. That is:
- No `--`, `++` operators.
- No function calls.
- Array subscript index needs to be int constants or constant variable.

The copy pasting does not replace member access:
`auto &a = b; a.a = c;` becomes  `b.a = c;`
The copy pasting does not replace function calls:
`auto &a = b; a = a();` becomes  `b = a();`

While limited, this already allows for nicer syntax (aliasing) for
accessing SSBOs and the potential overhead of a copy semantic:
```cpp
ViewMatrices matrices  = drw_view_buf[0];
matrices.viewmat = float4x4(1);
drw_view_buf[0] = matrices;
```
Can now be written as;
```cpp
ViewMatrices &matrices  = drw_view_buf[0];
matrices.viewmat = float4x4(1);
```
Which expands to;
```cpp
drw_view_buf[0].viewmat = float4x4(1);
```

Note that the reference semantic is not carried through function call
because arguments are transformed to `inout` in GLSL. `inout` has
copy semantic but it is often implemented as reference by some
implementations.

Another important note is that this copy-pasting doesn't check if a
symbol is a variable. It can match a typename. But given that our
typenames have different capitalizations style this is unlikely to be
an issue. If that issue arise, we can add a check for it.

Rel #137446

Pull Request: https://projects.blender.org/blender/blender/pulls/138412
This commit is contained in:
Clément Foucault
2025-05-06 13:36:59 +02:00
committed by Clément Foucault
parent 0b59d9f00d
commit 2513fbedca
4 changed files with 203 additions and 17 deletions

View File

@@ -151,8 +151,10 @@ void main()
barrier();
/* Reusing local_radiance for directions. */
local_radiance[local_index] = float4(normalize(direction), 1.0f) * sample_weight *
length(radiance_sun.xyz);
auto &local_direction = local_radiance;
local_direction[local_index] = float4(normalize(direction), 1.0f) * sample_weight *
length(radiance_sun.xyz);
/* OpenGL/Intel drivers have known issues where it isn't able to compile barriers inside for
* loops. Unroll is needed as driver might decide to not unroll in shaders with more
* complexity. */
@@ -160,13 +162,13 @@ void main()
{
barrier();
if (local_index < stride) {
local_radiance[local_index] += local_radiance[local_index + stride];
local_direction[local_index] += local_direction[local_index + stride];
}
}
barrier();
if (gl_LocalInvocationIndex == 0u) {
out_sun[work_group_index].direction = local_radiance[0];
out_sun[work_group_index].direction = local_direction[0];
}
barrier();
}

View File

@@ -45,28 +45,29 @@ void main()
directional_range_changed = 0;
int clip_index = tilemap.clip_data_index;
const int clip_index = tilemap.clip_data_index;
if (clip_index == -1) {
/* NOP. This is the case for unused tile-maps that are getting pushed to the free heap. */
}
else if (tilemap.projection_type != SHADOW_PROJECTION_CUBEFACE) {
ShadowTileMapClip clip_data = tilemaps_clip_buf[clip_index];
ShadowTileMapClip &clip_data = tilemaps_clip_buf[clip_index];
float clip_near_new = orderedIntBitsToFloat(clip_data.clip_near);
float clip_far_new = orderedIntBitsToFloat(clip_data.clip_far);
bool near_changed = clip_near_new != clip_data.clip_near_stored;
bool far_changed = clip_far_new != clip_data.clip_far_stored;
directional_range_changed = int(near_changed || far_changed);
/* NOTE(fclem): This assumes clip near/far are computed each time the initial phase runs. */
tilemaps_clip_buf[clip_index].clip_near_stored = clip_near_new;
tilemaps_clip_buf[clip_index].clip_far_stored = clip_far_new;
clip_data.clip_near_stored = clip_near_new;
clip_data.clip_far_stored = clip_far_new;
/* Reset for next update. */
tilemaps_clip_buf[clip_index].clip_near = floatBitsToOrderedInt(FLT_MAX);
tilemaps_clip_buf[clip_index].clip_far = floatBitsToOrderedInt(-FLT_MAX);
clip_data.clip_near = floatBitsToOrderedInt(FLT_MAX);
clip_data.clip_far = floatBitsToOrderedInt(-FLT_MAX);
}
else {
/* For cube-faces, simply use the light near and far distances. */
tilemaps_clip_buf[clip_index].clip_near_stored = tilemap.clip_near;
tilemaps_clip_buf[clip_index].clip_far_stored = tilemap.clip_far;
ShadowTileMapClip &clip_data = tilemaps_clip_buf[clip_index];
clip_data.clip_near_stored = tilemap.clip_near;
clip_data.clip_far_stored = tilemap.clip_far;
}
}

View File

@@ -226,6 +226,7 @@ class Preprocessor {
str = remove_quotes(str);
str = enum_macro_injection(str);
str = argument_reference_mutation(str);
str = variable_reference_mutation(str, report_error);
str = template_definition_mutation(str, report_error);
str = template_call_mutation(str);
}
@@ -912,6 +913,117 @@ class Preprocessor {
return std::regex_replace(out, regex, "$1 inout $2 $3$4");
}
/* To be run after `argument_reference_mutation()`. */
std::string variable_reference_mutation(const std::string &str, report_callback report_error)
{
using namespace std;
/* Processing regex and logic is expensive. Check if they are needed at all. */
bool valid_match = false;
string next_str = str;
reference_search(next_str, [&](int parenthesis_depth, int /*bracket_depth*/, char &c) {
/* Check if inside a function body. */
if (parenthesis_depth == 0) {
valid_match = true;
/* Modify the & into @ to make sure we only match these references in the regex
* below. @ being forbidden in the shader language, it is safe to use a temp
* character. */
c = '@';
}
});
if (!valid_match) {
return str;
}
string out_str;
/* Example: `const float &var = value;` */
regex regex_ref(R"(\ ?(?:const)?\s*\w+\s+\@(\w+) =\s*([^;]+);)");
smatch match;
while (regex_search(next_str, match, regex_ref)) {
const string definition = match[0].str();
const string name = match[1].str();
const string value = match[2].str();
const string prefix = match.prefix().str();
const string suffix = match.suffix().str();
out_str += prefix;
/** IMPORTANT: `match` is invalid after the assignment. */
next_str = definition + suffix;
/* Assert definition doesn't contain any side effect. */
if (value.find("++") != string::npos || value.find("--") != string::npos) {
report_error(match, "Reference definitions cannot have side effects.");
return str;
}
if (value.find("(") != string::npos) {
report_error(match, "Reference definitions cannot contain function calls.");
return str;
}
if (value.find("[") != string::npos) {
const string index_var = get_content_between_balanced_pair(value, '[', ']');
if (index_var.find(' ') != string::npos) {
report_error(match,
"Array subscript inside reference declaration must be a single variable or "
"a constant, not an expression.");
return str;
}
/* Add a space to avoid empty scope breaking the loop. */
string scope_depth = " }";
bool found_var = false;
while (!found_var) {
string scope = get_content_between_balanced_pair(out_str + scope_depth, '{', '}', true);
scope_depth += '}';
if (scope.empty()) {
break;
}
/* Remove nested scopes. Avoid variable shadowing to mess with the detection. */
scope = regex_replace(scope, regex(R"(\{[^\}]*\})"), "{}");
/* Search if index variable definition qualifies it as `const`. */
regex regex_definition(R"((const)? \w+ )" + index_var + " =");
smatch match_definition;
if (regex_search(scope, match_definition, regex_definition)) {
found_var = true;
if (match_definition[1].matched == false) {
report_error(match, "Array subscript variable must be declared as const qualified.");
return str;
}
}
}
if (!found_var) {
report_error(match,
"Cannot locate array subscript variable declaration. "
"If it is a global variable, assign it to a temporary const variable for "
"indexing inside the reference.");
return str;
}
}
/* Find scope this definition is active in. */
const string scope = get_content_between_balanced_pair('{' + suffix, '{', '}');
if (scope.empty()) {
report_error(match, "Reference is defined inside a global or unterminated scope.");
return str;
}
string original = definition + scope;
string modified = original;
/* Replace definition by nothing. Keep number of lines. */
string newlines(line_count(definition), '\n');
replace_all(modified, definition, newlines);
/* Replace every occurrence of the reference. Avoid matching other symbols like class members
* and functions with the same name. */
modified = regex_replace(
modified, regex(R"(([^\.])\b)" + name + R"(\b([^(]))"), "$1" + value + "$2");
/* Replace whole modified scope in output string. */
replace_all(next_str, original, modified);
}
out_str += next_str;
return out_str;
}
std::string argument_decorator_macro_injection(const std::string &str)
{
/* Example: `out float var[2]` > `out float _out_sta var _out_end[2]` */
@@ -952,8 +1064,7 @@ class Preprocessor {
}
/* Assume formatted source with our code style. Cannot be applied to python shaders. */
template<typename ReportErrorF>
void global_scope_constant_linting(const std::string &str, const ReportErrorF &report_error)
void global_scope_constant_linting(const std::string &str, report_callback report_error)
{
/* Example: `const uint global_var = 1u;`. Matches if not indented (i.e. inside a scope). */
std::regex regex(R"(const \w+ \w+ =)");
@@ -978,8 +1089,7 @@ class Preprocessor {
});
}
template<typename ReportErrorF>
void array_constructor_linting(const std::string &str, const ReportErrorF &report_error)
void array_constructor_linting(const std::string &str, report_callback report_error)
{
std::regex regex(R"(=\s*(\w+)\s*\[[^\]]*\]\s*\()");
regex_global_search(str, regex, [&](const std::smatch &match) {
@@ -1253,7 +1363,7 @@ class Preprocessor {
char next_char = str[pos + 1];
/* Validate it is not an operator (`&`, `&&`, `&=`). */
if (prev_char == ' ' || prev_char == '(') {
if (next_char != ' ' && next_char != '&' && next_char != '=') {
if (next_char != ' ' && next_char != '\n' && next_char != '&' && next_char != '=') {
callback(parenthesis_depth, bracket_depth, c);
}
}

View File

@@ -275,4 +275,77 @@ func_TEMPLATE(float, 1)/*float a*/)";
}
GPU_TEST(preprocess_template);
static void test_preprocess_reference()
{
using namespace shader;
using namespace std;
{
string input = R"(void func() { auto &a = b; a.a = 0; c = a(a); a_c_a = a; })";
string expect = R"(void func() { b.a = 0; c = a(b); a_c_a = b; })";
string error;
string output = process_test_string(input, error);
EXPECT_EQ(output, expect);
EXPECT_EQ(error, "");
}
{
string input = R"(void func() { const int &a = b; a.a = 0; c = a(a); })";
string expect = R"(void func() { b.a = 0; c = a(b); })";
string error;
string output = process_test_string(input, error);
EXPECT_EQ(output, expect);
EXPECT_EQ(error, "");
}
{
string input = R"(void func() { const int i = 0; auto &a = b[i]; a.a = 0; })";
string expect = R"(void func() { const int i = 0; b[i].a = 0; })";
string error;
string output = process_test_string(input, error);
EXPECT_EQ(output, expect);
EXPECT_EQ(error, "");
}
{
string input = R"(void func() { auto &a = b(0); })";
string error;
string output = process_test_string(input, error);
EXPECT_EQ(error, "Reference definitions cannot contain function calls.");
}
{
string input = R"(void func() { int i = 0; auto &a = b[i++]; })";
string error;
string output = process_test_string(input, error);
EXPECT_EQ(error, "Reference definitions cannot have side effects.");
}
{
string input = R"(void func() { auto &a = b[0 + 1]; })";
string error;
string output = process_test_string(input, error);
EXPECT_EQ(error,
"Array subscript inside reference declaration must be a single variable or a "
"constant, not an expression.");
}
{
string input = R"(void func() { auto &a = b[c]; })";
string error;
string output = process_test_string(input, error);
EXPECT_EQ(error,
"Cannot locate array subscript variable declaration. "
"If it is a global variable, assign it to a temporary const variable for "
"indexing inside the reference.");
}
{
string input = R"(void func() { int c = 0; auto &a = b[c]; })";
string error;
string output = process_test_string(input, error);
EXPECT_EQ(error, "Array subscript variable must be declared as const qualified.");
}
{
string input = R"(auto &a = b;)";
string error;
string output = process_test_string(input, error);
EXPECT_EQ(error, "Reference is defined inside a global or unterminated scope.");
}
}
GPU_TEST(preprocess_reference);
} // namespace blender::gpu::tests