Skip to content

Commit c13a906

Browse files
committed
VkExpr: Implement multi-pass operations (Postfix only currently)
1 parent 0f67052 commit c13a906

File tree

7 files changed

+542
-98
lines changed

7 files changed

+542
-98
lines changed

README.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,22 @@ llvmexpr.VkExpr(clip[] clips, string[] expr[, int format, int boundary=0, int nu
7373
- `num_streams`: Number of concurrent Vulkan streams (default: 8). Increase this for better parallelism if you have a powerful GPU, or decrease it if you run into insufficient vram.
7474
- `device_id`: Selects which Vulkan physical device to run on (default: -1 = auto).
7575

76+
### Multi-Pass Pipeline (VkExpr only)
77+
78+
`VkExpr` supports executing multiple expressions sequentially for the same plane, with efficient zero-copy data transfer between them.
79+
80+
- **Separator**: Use `##` to separate different stages in the expression string.
81+
- **Intermediate Access**: Use `bufN` to access the result of the N-th stage (0-indexed). `buf0` is the result of the first expression, `buf1` is the second, and so on. Relative and absolute access for buffers are also supported.
82+
83+
**Example:**
84+
```python
85+
# Stage 1: x + 0.5 (result stored in buf0)
86+
# Stage 2: x + buf0 (calculates x + (x[1,1] + 0.5))
87+
core.llvmexpr.VkExpr(clip, expr="x 0.5 + ## x buf0[1,1] +")
88+
```
89+
90+
Intermediate buffers are stored as `float32` on the GPU, with no clamping / quantization.
91+
7692
### `llvmexpr.SingleExpr` (Per-Frame)
7793

7894
This function executes an expression only once per frame. It is not suitable for typical image filtering but is powerful for tasks that involve reading from arbitrary coordinates, calculating frame-wide metrics, and writing results to other pixels or to frame properties.

llvmexpr/codegen/glsl/GLSLGenerator.cpp

Lines changed: 75 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -30,12 +30,14 @@
3030

3131
GLSLGenerator::GLSLGenerator(
3232
const std::vector<Token>& tokens, int num_inputs,
33-
[[maybe_unused]] int width, [[maybe_unused]] int height,
34-
bool mirror_boundary,
33+
int num_intermediate_inputs, [[maybe_unused]] int width,
34+
[[maybe_unused]] int height, bool mirror_boundary,
3535
const std::map<std::pair<int, std::string>, int>& prop_map,
3636
const analysis::ExpressionAnalysisResults& analysis_results)
37-
: tokens(tokens), num_inputs(num_inputs), mirror_boundary(mirror_boundary),
38-
prop_map(prop_map), analysis(analysis_results) {
37+
: tokens(tokens), num_inputs(num_inputs),
38+
num_intermediate_inputs(num_intermediate_inputs),
39+
mirror_boundary(mirror_boundary), prop_map(prop_map),
40+
analysis(analysis_results) {
3941

4042
const auto& var_result = analysis.getVariableUsageResult();
4143
for (const auto& var_name : var_result.all_vars) {
@@ -350,10 +352,22 @@ void GLSLGenerator::emitBufferDeclarations() {
350352
emitNewline();
351353
}
352354

355+
// Intermediate buffers
356+
for (int i = 0; i < num_intermediate_inputs; ++i) {
357+
emitLine(std::format("layout(std430, set = 0, binding = {}) readonly "
358+
"buffer IntermediateBuffer{} {{",
359+
num_inputs + i, i));
360+
indent();
361+
emitLine("float data[];");
362+
dedent();
363+
emitLine(std::format("}} buf{};", i));
364+
emitNewline();
365+
}
366+
353367
// Output buffer
354368
emitLine(std::format("layout(std430, set = 0, binding = {}) writeonly "
355369
"buffer OutputBuffer {{",
356-
num_inputs));
370+
num_inputs + num_intermediate_inputs));
357371
indent();
358372
emitLine("float data[];");
359373
dedent();
@@ -363,7 +377,7 @@ void GLSLGenerator::emitBufferDeclarations() {
363377
// Props buffer
364378
emitLine(std::format(
365379
"layout(std430, set = 0, binding = {}) readonly buffer PropsBuffer {{",
366-
num_inputs + 1));
380+
num_inputs + num_intermediate_inputs + 1));
367381
indent();
368382
emitLine("float props[];");
369383
dedent();
@@ -1459,6 +1473,61 @@ void GLSLGenerator::processToken(const Token& token) {
14591473
emitLine(std::format("int {} = int(roundEven({}));", y_int, coord_y));
14601474

14611475
push(emitPixelLoad(payload.clip_idx, x_int, y_int, use_mirror));
1476+
push(emitPixelLoad(payload.clip_idx, x_int, y_int, use_mirror));
1477+
break;
1478+
}
1479+
1480+
case TokenType::BufferCur: {
1481+
const auto& payload = std::get<TokenPayloadBufferAccess>(token.payload);
1482+
std::string temp = newTemp();
1483+
emitLine(std::format("float {} = buf{}.data[gid];", temp,
1484+
payload.buffer_idx));
1485+
push(temp);
1486+
break;
1487+
}
1488+
case TokenType::BufferRel: {
1489+
const auto& payload = std::get<TokenPayloadBufferAccess>(token.payload);
1490+
bool use_mirror =
1491+
payload.has_mode ? payload.use_mirror : mirror_boundary;
1492+
1493+
std::string x_expr = std::format("X + {}", payload.rel_x);
1494+
std::string y_expr = std::format("Y + {}", payload.rel_y);
1495+
1496+
std::string final_x =
1497+
emitFinalCoord(x_expr, "int(pc.width)", use_mirror);
1498+
std::string final_y =
1499+
emitFinalCoord(y_expr, "int(pc.height)", use_mirror);
1500+
std::string idx = emitPixelIndex(final_x, final_y);
1501+
1502+
std::string temp = newTemp();
1503+
emitLine(std::format("float {} = buf{}.data[{}];", temp,
1504+
payload.buffer_idx, idx));
1505+
push(temp);
1506+
break;
1507+
}
1508+
case TokenType::BufferAbs: {
1509+
const auto& payload = std::get<TokenPayloadBufferAccess>(token.payload);
1510+
std::string coord_y = pop();
1511+
std::string coord_x = pop();
1512+
bool use_mirror =
1513+
payload.has_mode ? payload.use_mirror : mirror_boundary;
1514+
1515+
std::string x_int = newTemp();
1516+
std::string y_int = newTemp();
1517+
1518+
emitLine(std::format("int {} = int(roundEven({}));", x_int, coord_x));
1519+
emitLine(std::format("int {} = int(roundEven({}));", y_int, coord_y));
1520+
1521+
std::string final_x =
1522+
emitFinalCoord(x_int, "int(pc.width)", use_mirror);
1523+
std::string final_y =
1524+
emitFinalCoord(y_int, "int(pc.height)", use_mirror);
1525+
std::string idx = emitPixelIndex(final_x, final_y);
1526+
1527+
std::string temp = newTemp();
1528+
emitLine(std::format("float {} = buf{}.data[{}];", temp,
1529+
payload.buffer_idx, idx));
1530+
push(temp);
14621531
break;
14631532
}
14641533

llvmexpr/codegen/glsl/GLSLGenerator.hpp

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,9 @@
3434

3535
class GLSLGenerator {
3636
public:
37-
GLSLGenerator(const std::vector<Token>& tokens, int num_inputs, int width,
38-
int height, bool mirror_boundary,
37+
GLSLGenerator(const std::vector<Token>& tokens, int num_inputs,
38+
int num_intermediate_inputs, int width, int height,
39+
bool mirror_boundary,
3940
const std::map<std::pair<int, std::string>, int>& prop_map,
4041
const analysis::ExpressionAnalysisResults& analysis_results);
4142

@@ -44,6 +45,7 @@ class GLSLGenerator {
4445
private:
4546
const std::vector<Token>& tokens;
4647
int num_inputs;
48+
int num_intermediate_inputs;
4749
bool mirror_boundary;
4850
const std::map<std::pair<int, std::string>, int>& prop_map;
4951
const analysis::ExpressionAnalysisResults& analysis;

llvmexpr/frontend/Tokenizer.cpp

Lines changed: 84 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -97,15 +97,22 @@ constexpr Availability operator&(Availability lhs, Availability rhs) {
9797
}
9898

9999
constexpr Availability AVAILABILITY_ALL =
100-
Availability::Expr | Availability::SingleExpr;
100+
Availability::Expr | Availability::SingleExpr | Availability::VkExpr;
101101

102102
constexpr bool supports_mode(Availability availability, ExprMode mode) {
103103
if (mode == ExprMode::Expr) {
104104
return static_cast<std::uint8_t>(availability & Availability::Expr) !=
105105
0;
106106
}
107-
return static_cast<std::uint8_t>(availability & Availability::SingleExpr) !=
108-
0;
107+
if (mode == ExprMode::SingleExpr) {
108+
return static_cast<std::uint8_t>(availability &
109+
Availability::SingleExpr) != 0;
110+
}
111+
if (mode == ExprMode::VkExpr) {
112+
return static_cast<std::uint8_t>(availability & Availability::VkExpr) !=
113+
0;
114+
}
115+
return false;
109116
}
110117

111118
template <FixedString Str, TokenType Type>
@@ -552,6 +559,43 @@ inline std::optional<Token> parse_prop_store(std::string_view input) {
552559
return std::nullopt;
553560
}
554561

562+
inline std::optional<Token> parse_buffer_access(std::string_view input) {
563+
if (auto m = ctre::match<
564+
R"(^buf(\d+)(?:(?:(\[\]))|(?:\[\s*(-?\d+)\s*,\s*(-?\d+)\s*\]))?(?::([cmb]))?$)">(
565+
input)) {
566+
TokenPayloadBufferAccess data;
567+
data.buffer_idx = svtoi(m.template get<1>().to_view());
568+
569+
TokenType type = TokenType::BufferCur;
570+
571+
if (m.template get<2>()) {
572+
type = TokenType::BufferAbs;
573+
} else if (m.template get<3>()) {
574+
type = TokenType::BufferRel;
575+
data.rel_x = svtoi(m.template get<3>().to_view());
576+
data.rel_y = svtoi(m.template get<4>().to_view());
577+
}
578+
579+
// NOLINTNEXTLINE(cppcoreguidelines-avoid-magic-numbers)
580+
if (m.template get<5>()) {
581+
// NOLINTNEXTLINE(cppcoreguidelines-avoid-magic-numbers)
582+
char mode_char = m.template get<5>().to_view()[0];
583+
if (mode_char == 'm') {
584+
data.has_mode = true;
585+
data.use_mirror = true;
586+
} else if (mode_char == 'c') {
587+
data.has_mode = true;
588+
data.use_mirror = false;
589+
} else if (mode_char == 'b') {
590+
data.has_mode = false;
591+
}
592+
}
593+
594+
return Token{.type = type, .text = std::string(input), .payload = data};
595+
}
596+
return std::nullopt;
597+
}
598+
555599
inline std::optional<Token> parse_number(std::string_view input) {
556600
if (auto m = ctre::match<
557601
R"(^(?:(0x[0-9a-fA-F]+(?:\.[0-9a-fA-F]+(?:p[+\-]?\d+)?)?)|(0[0-7]+)|([+\-]?\d+(?:\.\d+)?(?:[eE][+\-]?\d+)?))$)">(
@@ -649,9 +693,9 @@ constexpr auto get_token_definitions() {
649693
make_literal_definition<FixedString{"?"}, TokenType::Ternary>(
650694
BEHAVIOR_TERNARY),
651695
make_literal_definition<FixedString{"X"}, TokenType::ConstantX>(
652-
BEHAVIOR_ZERO_PUSH, Availability::Expr),
696+
BEHAVIOR_ZERO_PUSH, Availability::Expr | Availability::VkExpr),
653697
make_literal_definition<FixedString{"Y"}, TokenType::ConstantY>(
654-
BEHAVIOR_ZERO_PUSH, Availability::Expr),
698+
BEHAVIOR_ZERO_PUSH, Availability::Expr | Availability::VkExpr),
655699
make_literal_definition<FixedString{"N"}, TokenType::ConstantN>(
656700
BEHAVIOR_ZERO_PUSH),
657701
make_literal_definition<FixedString{">="}, TokenType::Ge>(
@@ -695,7 +739,8 @@ constexpr auto get_token_definitions() {
695739
make_literal_definition<FixedString{"neg"}, TokenType::Neg>(
696740
BEHAVIOR_UNARY),
697741
make_literal_definition<FixedString{"@[]"}, TokenType::StoreAbs>(
698-
TokenBehavior{.arity = 3, .stack_effect = -3}, Availability::Expr),
742+
TokenBehavior{.arity = 3, .stack_effect = -3},
743+
Availability::Expr | Availability::VkExpr),
699744
make_literal_definition<FixedString{"clip"}, TokenType::Clip>(
700745
TokenBehavior{.arity = 3, .stack_effect = -2}),
701746
make_literal_definition<FixedString{"sqrt"}, TokenType::Sqrt>(
@@ -742,8 +787,24 @@ constexpr auto get_token_definitions() {
742787
BEHAVIOR_UNARY),
743788
make_literal_definition<FixedString{"height"},
744789
TokenType::ConstantHeight>(BEHAVIOR_ZERO_PUSH),
790+
TokenDefinition{.type = TokenType::BufferCur,
791+
.name = "bufN",
792+
.behavior = BEHAVIOR_ZERO_PUSH,
793+
.parser = parse_buffer_access,
794+
.availability = Availability::VkExpr},
795+
TokenDefinition{.type = TokenType::BufferRel,
796+
.name = "bufN",
797+
.behavior = BEHAVIOR_ZERO_PUSH,
798+
.parser = parse_buffer_access,
799+
.availability = Availability::VkExpr},
800+
TokenDefinition{.type = TokenType::BufferAbs,
801+
.name = "bufN",
802+
.behavior =
803+
TokenBehavior{.arity = 2, .stack_effect = -1},
804+
.parser = parse_buffer_access,
805+
.availability = Availability::VkExpr},
745806
make_literal_definition<FixedString{"^exit^"}, TokenType::ExitNoWrite>(
746-
BEHAVIOR_ZERO_PUSH, Availability::Expr),
807+
BEHAVIOR_ZERO_PUSH, Availability::Expr | Availability::VkExpr),
747808
make_literal_definition<FixedString{"copysign"}, TokenType::Copysign>(
748809
BEHAVIOR_BINARY),
749810
TokenDefinition{.type = TokenType::ConstantPlaneWidth,
@@ -866,7 +927,8 @@ constexpr auto get_token_definitions() {
866927
.name = "clip_rel",
867928
.behavior = BEHAVIOR_ZERO_PUSH,
868929
.parser = parse_clip_rel,
869-
.availability = Availability::Expr},
930+
.availability =
931+
Availability::Expr | Availability::VkExpr},
870932
TokenDefinition{.type = TokenType::ClipAbs,
871933
.name = "clip_abs",
872934
.behavior =
@@ -877,7 +939,8 @@ constexpr auto get_token_definitions() {
877939
.name = "clip_cur",
878940
.behavior = BEHAVIOR_ZERO_PUSH,
879941
.parser = parse_clip_cur,
880-
.availability = Availability::Expr},
942+
.availability =
943+
Availability::Expr | Availability::VkExpr},
881944
TokenDefinition{.type = TokenType::PropAccess,
882945
.name = "prop_access",
883946
.behavior = BEHAVIOR_ZERO_PUSH,
@@ -916,7 +979,7 @@ constexpr auto get_token_definitions() {
916979
} // anonymous namespace
917980

918981
std::vector<Token> tokenize(const std::string& expr, int num_inputs,
919-
ExprMode mode) {
982+
ExprMode mode, int num_intermediate_inputs) {
920983
std::vector<Token> tokens;
921984
int idx = 0;
922985

@@ -981,6 +1044,17 @@ std::vector<Token> tokenize(const std::string& expr, int num_inputs,
9811044
std::format("Invalid clip index in token: {} (idx {})",
9821045
std::string(str_token_view), idx));
9831046
}
1047+
} else if (parsed_token->type == TokenType::BufferRel ||
1048+
parsed_token->type == TokenType::BufferAbs ||
1049+
parsed_token->type == TokenType::BufferCur) {
1050+
if (std::get<TokenPayloadBufferAccess>(parsed_token->payload)
1051+
.buffer_idx < 0 ||
1052+
std::get<TokenPayloadBufferAccess>(parsed_token->payload)
1053+
.buffer_idx >= num_intermediate_inputs) {
1054+
throw std::runtime_error(
1055+
std::format("Invalid buffer index in token: {} (idx {})",
1056+
std::string(str_token_view), idx));
1057+
}
9841058
}
9851059

9861060
tokens.push_back(*parsed_token);

llvmexpr/frontend/Tokenizer.hpp

Lines changed: 25 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,9 @@ enum class TokenType : std::uint8_t {
6262
ClipAbsPlane, // src^plane[]
6363
StoreAbsPlane, // @[]^plane
6464
PropStore, // prop$
65+
BufferRel, // bufN[x,y]
66+
BufferAbs, // bufN[]
67+
BufferCur, // bufN
6568

6669
// Binary Operators
6770
Add,
@@ -205,13 +208,23 @@ struct TokenPayloadArrayOp {
205208
int static_size = 0; // ARRAY_ALLOC_STATIC
206209
};
207210

211+
struct TokenPayloadBufferAccess {
212+
int buffer_idx;
213+
int rel_x = 0;
214+
int rel_y = 0;
215+
bool use_mirror = false;
216+
bool has_mode = false;
217+
};
218+
208219
struct Token {
209-
using PayloadVariant = std::variant<
210-
std::monostate, TokenPayloadNumber, TokenPayloadVar, TokenPayloadLabel,
211-
TokenPayloadStackOp, TokenPayloadClipAccess, TokenPayloadPropAccess,
212-
TokenPayloadClipAccessPlane, TokenPayloadStoreAbsPlane,
213-
TokenPayloadPropStore, TokenPayloadPlaneDim, TokenPayloadClipDim,
214-
TokenPayloadClipPlaneDim, TokenPayloadArrayOp>;
220+
using PayloadVariant =
221+
std::variant<std::monostate, TokenPayloadNumber, TokenPayloadVar,
222+
TokenPayloadLabel, TokenPayloadStackOp,
223+
TokenPayloadClipAccess, TokenPayloadPropAccess,
224+
TokenPayloadClipAccessPlane, TokenPayloadStoreAbsPlane,
225+
TokenPayloadPropStore, TokenPayloadPlaneDim,
226+
TokenPayloadClipDim, TokenPayloadClipPlaneDim,
227+
TokenPayloadArrayOp, TokenPayloadBufferAccess>;
215228

216229
TokenType type;
217230
std::string text;
@@ -229,6 +242,7 @@ using BehaviorResolver = std::variant<TokenBehavior, DynamicBehaviorFn>;
229242
enum class ExprMode : std::uint8_t {
230243
Expr,
231244
SingleExpr,
245+
VkExpr,
232246
};
233247

234248
// Utility functions
@@ -250,15 +264,18 @@ struct TokenDefinition {
250264
enum class Availability : std::uint8_t {
251265
Expr = 1U << 0,
252266
SingleExpr = 1U << 1,
267+
VkExpr = 1U << 2,
253268
};
254269

255270
Availability availability = static_cast<Availability>(
256271
static_cast<std::uint8_t>(Availability::Expr) |
257-
static_cast<std::uint8_t>(Availability::SingleExpr));
272+
static_cast<std::uint8_t>(Availability::SingleExpr) |
273+
static_cast<std::uint8_t>(Availability::VkExpr));
258274
};
259275

260276
std::vector<Token> tokenize(const std::string& expr, int num_inputs,
261-
ExprMode mode = ExprMode::Expr);
277+
ExprMode mode = ExprMode::Expr,
278+
int num_intermediate_inputs = 0);
262279
TokenBehavior get_token_behavior(const Token& token);
263280

264281
#endif // LLVMEXPR_FRONTEND_TOKENIZER_HPP

0 commit comments

Comments
 (0)