Files
test/source/blender/gpu/tests/shader_preprocess_test.cc
Clément Foucault 2513fbedca 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
2025-05-06 13:36:59 +02:00

352 lines
9.5 KiB
C++

/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: Apache-2.0 */
#include "glsl_preprocess/glsl_preprocess.hh"
#include "gpu_testing.hh"
namespace blender::gpu::tests {
static void test_preprocess_utilities()
{
using namespace shader;
using namespace std;
string input = "test (u, u(s,(s,s)), u) {t{{}},t,{};(,)} {u{}} end";
EXPECT_EQ(Preprocessor::get_content_between_balanced_pair(input, '{', '}'), "t{{}},t,{};(,)");
EXPECT_EQ(Preprocessor::get_content_between_balanced_pair(input, '{', '}', true), "u{}");
EXPECT_EQ(Preprocessor::replace_char_between_balanced_pair(input, '(', ')', ',', '!'),
"test (u! u(s!(s!s))! u) {t{{}},t,{};(!)} {u{}} end");
vector<string> split_expect{"test (u, u(s,(s,s", "", ", u", " {t{{}},t,{};(,", "} {u{}} end"};
vector<string> split_result = Preprocessor::split_string(input, ')');
EXPECT_EQ_VECTOR(split_expect, split_result);
string input2 = "u, u(s,(s,s)), u";
vector<string> split_expect2{"u", " u(s,(s,s))", " u"};
vector<string> split_result2 = Preprocessor::split_string_not_between_balanced_pair(
input2, ',', '(', ')');
EXPECT_EQ_VECTOR(split_expect2, split_result2);
string input_reference = "void func(int &a, int (&c)[2]) {{ int &b = a; }} int &b = a;";
int fn_ref_count = 0, arg_ref_count = 0, global_ref_count = 0;
Preprocessor::reference_search(
input_reference, [&](int parenthesis_depth, int bracket_depth, char & /*c*/) {
if ((parenthesis_depth == 1 || parenthesis_depth == 2) && bracket_depth == 0) {
arg_ref_count += 1;
}
else if (bracket_depth > 0) {
fn_ref_count += 1;
}
else if (bracket_depth == 0 && parenthesis_depth == 0) {
global_ref_count += 1;
}
});
EXPECT_EQ(arg_ref_count, 2);
EXPECT_EQ(fn_ref_count, 1);
EXPECT_EQ(global_ref_count, 1);
}
GPU_TEST(preprocess_utilities);
static std::string process_test_string(std::string str,
std::string &first_error,
shader::metadata::Source *r_metadata = nullptr)
{
using namespace shader;
Preprocessor preprocessor;
shader::metadata::Source metadata;
std::string result = preprocessor.process(
Preprocessor::SourceLanguage::BLENDER_GLSL,
str,
"test.glsl",
true,
true,
[&](const std::smatch & /*match*/, const char *err_msg) {
if (first_error.empty()) {
first_error = err_msg;
}
},
metadata);
if (r_metadata != nullptr) {
*r_metadata = metadata;
}
/* Strip first line directive as they are platform dependent. */
size_t newline = result.find('\n');
return result.substr(newline + 1);
}
static void test_preprocess_unroll()
{
using namespace shader;
using namespace std;
{
string input = R"([[gpu::unroll]] for (int i = 2; i < 4; i++, y++) { content += i; })";
string expect = R"({ int i = 2;
#line 1
{ content += i; }
#line 1
i++, y++;
#line 1
{ content += i; }
#line 1
i++, y++;
#line 1
})";
string error;
string output = process_test_string(input, error);
EXPECT_EQ(output, expect);
EXPECT_EQ(error, "");
}
{
string input = R"([[gpu::unroll]] for (int i = 2; i < 4 && i < y; i++, y++) { cont += i; })";
string expect = R"({ int i = 2;
#line 1
if (i < y) { cont += i; }
#line 1
i++, y++;
#line 1
if (i < y) { cont += i; }
#line 1
i++, y++;
#line 1
})";
string error;
string output = process_test_string(input, error);
EXPECT_EQ(output, expect);
EXPECT_EQ(error, "");
}
{
string input = R"([[gpu::unroll(2)]] for (; i < j;) { content += i; })";
string expect = R"({ ;
#line 1
if (i < j) { content += i; }
#line 1
;
#line 1
if (i < j) { content += i; }
#line 1
;
#line 1
})";
string error;
string output = process_test_string(input, error);
EXPECT_EQ(output, expect);
EXPECT_EQ(error, "");
}
{
string input = R"([[gpu::unroll(2)]] for (; i < j;) { [[gpu::unroll(2)]] for (; j < k;) {} })";
string expect = R"({ ;
#line 1
if (i < j) { { ;
#line 1
if (j < k) {}
#line 1
;
#line 1
if (j < k) {}
#line 1
;
#line 1
} }
#line 1
;
#line 1
if (i < j) { { ;
#line 1
if (j < k) {}
#line 1
;
#line 1
if (j < k) {}
#line 1
;
#line 1
} }
#line 1
;
#line 1
})";
string error;
string output = process_test_string(input, error);
EXPECT_EQ(output, expect);
EXPECT_EQ(error, "");
}
{
string input = R"([[gpu::unroll(2)]] for (; i < j;) { break; })";
string error;
string output = process_test_string(input, error);
EXPECT_EQ(error, "Error: Unrolled loop cannot contain \"break\" statement.");
}
{
string input = R"([[gpu::unroll(2)]] for (; i < j;) { continue; })";
string error;
string output = process_test_string(input, error);
EXPECT_EQ(error, "Error: Unrolled loop cannot contain \"continue\" statement.");
}
{
string input = R"([[gpu::unroll(2)]] for (; i < j;) { for (; j < k;) {break;continue;} })";
string expect = R"({ ;
#line 1
if (i < j) { for (; j < k;) {break;continue;} }
#line 1
;
#line 1
if (i < j) { for (; j < k;) {break;continue;} }
#line 1
;
#line 1
})";
string error;
string output = process_test_string(input, error);
EXPECT_EQ(output, expect);
EXPECT_EQ(error, "");
}
{
string input = R"([[gpu::unroll]] for (int i = 3; i > 2; i++) {})";
string error;
string output = process_test_string(input, error);
EXPECT_EQ(error, "Error: Unsupported condition in unrolled loop.");
}
}
GPU_TEST(preprocess_unroll);
static void test_preprocess_template()
{
using namespace shader;
using namespace std;
{
string input = R"(
template<typename T>
void func(T a) {a;}
template void func<float>(float a);)";
string expect = R"(
#define func_TEMPLATE(T) \
void func(T a) {a;}
func_TEMPLATE(float)/*float a*/)";
string error;
string output = process_test_string(input, error);
EXPECT_EQ(output, expect);
EXPECT_EQ(error, "");
}
{
string input = R"(
template<typename T, int i>
void func(T a) {
a;
}
template void func<float, 1>(float a);)";
string expect = R"(
#define func_TEMPLATE(T, i) \
void func_##T##_##i##_(T a) { \
a; \
}
func_TEMPLATE(float, 1)/*float a*/)";
string error;
string output = process_test_string(input, error);
EXPECT_EQ(output, expect);
EXPECT_EQ(error, "");
}
{
string input = R"(template<typename T, int i = 0> void func(T a) {a;)";
string error;
string output = process_test_string(input, error);
EXPECT_EQ(error, "Template declaration unsupported syntax");
}
{
string input = R"(template void func(float a);)";
string error;
string output = process_test_string(input, error);
EXPECT_EQ(error, "Template instantiation unsupported syntax");
}
{
string input = R"(func<float, 1>(a);)";
string expect = R"(TEMPLATE_GLUE2(func, float, 1)(a);)";
string error;
string output = process_test_string(input, error);
EXPECT_EQ(output, expect);
EXPECT_EQ(error, "");
}
}
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